mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-18 05:34:01 +00:00
Compare commits
408 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b3a99cc5e | |||
| 935c43e88f | |||
| 2893c031ab | |||
| 94cdb16ca9 | |||
| 84d4066986 | |||
| 2153c2dd10 | |||
| a7393c3492 | |||
| 794fece8cc | |||
| 1bf95e85a0 | |||
| a85357a8af | |||
| 7bf3e02f6c | |||
| 459e8d0b24 | |||
| 93585d846a | |||
| 4eb5c4603e | |||
| f42ad5ffc6 | |||
| d0bd8e50be | |||
| 337e3f4449 | |||
| 53bbf74f4d | |||
| 19a66fac20 | |||
| 36e1b9a36e | |||
| 1ca9288bbe | |||
| 4ca2d1e352 | |||
| 0b11730835 | |||
| 6069145b68 | |||
| 2ccda5aebd | |||
| 02810bf527 | |||
| b58ee761a1 | |||
| 5b9d898202 | |||
| 5f8e44e7b7 | |||
| ec80859d93 | |||
| 5e9d92d65c | |||
| c3bb9c0a16 | |||
| bd475e6f9c | |||
| 0f1708d8fd | |||
| efa0e270ad | |||
| ff882024d4 | |||
| 60d95447fb | |||
| a474bf8fe3 | |||
| 1bf0d2b3e6 | |||
| ea02d98a59 | |||
| 9624d7b429 | |||
| 4f440055cb | |||
| 7354ba8fd5 | |||
| b7b4a10485 | |||
| cca0e18f5a | |||
| 8846926b68 | |||
| 7ceefb1b81 | |||
| 861f684d00 | |||
| 17b146b960 | |||
| 02251eeeb4 | |||
| c486572d3d | |||
| a6ed6e3099 | |||
| 175e5e2e8c | |||
| 7bc27038be | |||
| 609bf3219f | |||
| bdbd616a84 | |||
| 1cbe5cb293 | |||
| df6003c08f | |||
| 50fcbf0f5a | |||
| 5a1676a0b9 | |||
| 309f2fbd2c | |||
| 650aacfaa7 | |||
| 8655027a5b | |||
| de937f02d4 | |||
| 2c0446e83d | |||
| 9e6699d8cc | |||
| ef92c5bc0a | |||
| 10f6600f52 | |||
| aeb607e73b | |||
| 8479d2edf0 | |||
| d2e9a8dc06 | |||
| 4571cb0b61 | |||
| 351f7b13b3 | |||
| 5953cb57b0 | |||
| b60b873428 | |||
| a3249cae26 | |||
| f7f784c0f4 | |||
| f4f538f69b | |||
| bbd1a5a2ef | |||
| bc71238046 | |||
| a9df9dde69 | |||
| 7a87e3f3af | |||
| 68f960e4e7 | |||
| 6784fe72dd | |||
| 312df0fcec | |||
| 2913a41aec | |||
| adabbfbef1 | |||
| 2b7962fa7c | |||
| 645f02e2f3 | |||
| b178462e47 | |||
| 1639348b94 | |||
| f67fe262bb | |||
| 0681117af6 | |||
| b5c244a09b | |||
| dceb7255b0 | |||
| 62307b93d7 | |||
| 61ce8b11a6 | |||
| 211f40ddf3 | |||
| 01dd0c79ed | |||
| be7b402c95 | |||
| a11929d2ca | |||
| 0bacb31d17 | |||
| cc7e4aec8a | |||
| 15c338e5b3 | |||
| 80bfcd5eb1 | |||
| b96f14762f | |||
| 0feacfa102 | |||
| a1496c1b79 | |||
| 1b73a138b6 | |||
| efcedd9005 | |||
| 86d4b17257 | |||
| f6a437f7fb | |||
| 3db8a5ac5b | |||
| 266a626acf | |||
| 411e2fb93a | |||
| 6ed83c08e3 | |||
| 9929376acf | |||
| 5135b6404d | |||
| 790669c242 | |||
| 4bb8b1882c | |||
| 56702ca7c2 | |||
| 9be4b9596d | |||
| 37c6b35fb7 | |||
| 5cd7a09797 | |||
| a1856b9bb0 | |||
| 29a36c8658 | |||
| 264b1b38ba | |||
| 65e71508ac | |||
| e5546159c5 | |||
| d7c961ea4f | |||
| ad987f2b5e | |||
| defb934c3f | |||
| 5a850984b9 | |||
| 1185b67b28 | |||
| abb42e8df4 | |||
| 7fdb1af69d | |||
| 78713cc2af | |||
| 951890eaff | |||
| fb00bfac3f | |||
| a6a1483083 | |||
| ef03f020ce | |||
| 070eb4003e | |||
| fca373d936 | |||
| 537cf2e79b | |||
| af424aa5f8 | |||
| 70e0f2913f | |||
| b28283271b | |||
| 4926cf8df5 | |||
| b3312a704a | |||
| 31e8019642 | |||
| 9525738118 | |||
| 62310ad9a9 | |||
| 4819a2f4e2 | |||
| 2435ee780f | |||
| 5d602ece5c | |||
| 900086c1c9 | |||
| 07be74701f | |||
| f46e20c977 | |||
| 4bc752b7a8 | |||
| c28b608146 | |||
| bd4948602a | |||
| 4774b3097f | |||
| de81156223 | |||
| e7e1bedcad | |||
| d8930db8af | |||
| 014db9e233 | |||
| 933920edfd | |||
| cf25d85d51 | |||
| 9af1308e29 | |||
| f4496d8f45 | |||
| 966d1c938b | |||
| f537c7896e | |||
| 4a1479957c | |||
| dc9a16c54b | |||
| a476647d70 | |||
| 95a48e5693 | |||
| 09404ca198 | |||
| 564f76b47d | |||
| c6424f1623 | |||
| a4325ac20d | |||
| d8a6422d37 | |||
| cc963c3562 | |||
| 63f99742fc | |||
| a4636d7872 | |||
| bd2ee03e44 | |||
| deeb81c183 | |||
| 12dbf957b3 | |||
| e1cc0b1ad0 | |||
| f079d57fc6 | |||
| 09b9ed642f | |||
| e74a9f3b41 | |||
| ff7a9d10cb | |||
| 5cfd7f1dc2 | |||
| 853ca2ef53 | |||
| cb0a4854f5 | |||
| 1188157a3e | |||
| 2ae639412d | |||
| 37386c29cb | |||
| e2ad0a1b1a | |||
| 96cdb2e9e0 | |||
| 002ef720ee | |||
| 2214533894 | |||
| 06b7a0419e | |||
| d24ff7b3bc | |||
| a24b3ad3de | |||
| 91c6e3fc30 | |||
| 411f6c0a34 | |||
| 0a3aa706ef | |||
| 5ca74db2f9 | |||
| f24523c2f1 | |||
| e7c5a862da | |||
| b27c738b69 | |||
| 8c565bbf15 | |||
| 37aa9c046f | |||
| 195a6224a0 | |||
| ea238f4988 | |||
| 3514a9a608 | |||
| e2bca9aebb | |||
| 6ae164e59b | |||
| e6ad5efbb7 | |||
| 7bd370b5bc | |||
| 828eb67a52 | |||
| c4df2e66be | |||
| 47e95c0c47 | |||
| 3b5890d0aa | |||
| bee2dc17af | |||
| d6b0173e7a | |||
| 8eafe27955 | |||
| 2af95456a2 | |||
| 8ce48dbcf6 | |||
| 92072b4225 | |||
| 4e823dd3a3 | |||
| 53d9cecdaf | |||
| 680f1f49ff | |||
| 6574272ca8 | |||
| f5c4aa2be2 | |||
| 2498861003 | |||
| 25923e82a4 | |||
| 61cb57e48d | |||
| 4985d836ea | |||
| e1a79a0ff1 | |||
| d72d935607 | |||
| 4ed06b9eff | |||
| fa4623db7c | |||
| a44e8b9921 | |||
| 6b4f1a552e | |||
| 3d32682a67 | |||
| 29e495a17f | |||
| 0c599f7d3a | |||
| 006e4c9fce | |||
| 63be4de418 | |||
| 497ca3f934 | |||
| 60de625c37 | |||
| a09c0e4c2d | |||
| a60f4ce9a8 | |||
| fcb681f4ae | |||
| 8530fa8194 | |||
| 89e8956df1 | |||
| 82ba35c162 | |||
| d750afe38f | |||
| 9b665a43c5 | |||
| 80c769b97d | |||
| 44dd8fd083 | |||
| 4e128e8930 | |||
| c2279e297c | |||
| cdd524b9f3 | |||
| 99809c7919 | |||
| 2390d79111 | |||
| 3b41d73ddd | |||
| 3d46c28911 | |||
| ca7840557b | |||
| ceaa115025 | |||
| 8dbcefbc75 | |||
| b28b96fb4a | |||
| e15a8ed2d8 | |||
| 19b77bf03b | |||
| 7b1541d2e8 | |||
| 830fea63ed | |||
| 3d59de55f8 | |||
| 5c1f5a7d2e | |||
| 4e985420d4 | |||
| 3e584a9652 | |||
| 39acaf846a | |||
| 76948cffa8 | |||
| 4ea8abb53b | |||
| 32085f317b | |||
| faa4367295 | |||
| 3f8a4957af | |||
| b5ea81cf96 | |||
| e14fa5eab0 | |||
| 960ecf616c | |||
| 996ccb0e40 | |||
| 407fee23b9 | |||
| 199570d4a5 | |||
| 5cd9c377d2 | |||
| e08273b8b8 | |||
| 6b0f90c321 | |||
| 0dea488e98 | |||
| e60c4dc34b | |||
| 0c75713820 | |||
| 6a28987a36 | |||
| 7331979107 | |||
| f22808f8ca | |||
| e1c519d71e | |||
| 909f37f89b | |||
| 63b0093321 | |||
| fa06df1400 | |||
| f83027d451 | |||
| 7ebf3d3cdd | |||
| 5daa0dfbb1 | |||
| 7b669d7001 | |||
| ba2407d691 | |||
| 5743d3a3b9 | |||
| 7f1c0f6f7e | |||
| 650d299c99 | |||
| 3a5c2da66c | |||
| d29826e6f2 | |||
| 34fefd1cad | |||
| a5c7eb2fbc | |||
| 6298388fe1 | |||
| 19491ce3f7 | |||
| 3db2053c21 | |||
| ccd7bd6fc2 | |||
| fff78ce00f | |||
| 8bd3d6249d | |||
| f65e743c4c | |||
| 843880d22e | |||
| ec3cc7a859 | |||
| b1733cbd2a | |||
| 4ec2d64cb7 | |||
| d370dae170 | |||
| 72f0a1be54 | |||
| a2e5e03f07 | |||
| 47eda54d6a | |||
| db81b9b1e1 | |||
| b9de1918b4 | |||
| b87d26f271 | |||
| d326ec831c | |||
| faac5b45c6 | |||
| 5e930a7717 | |||
| aff5e22843 | |||
| 344a8dc0a0 | |||
| 7576432e44 | |||
| 3ddc753c88 | |||
| d3a7414e51 | |||
| 1406c81e03 | |||
| 0f28fefc82 | |||
| 496b124baf | |||
| 5a5445e980 | |||
| eecdd3acda | |||
| c66df8c807 | |||
| f2a7617b1b | |||
| af448cf444 | |||
| 9c39061590 | |||
| 28319652c1 | |||
| 7e8a139732 | |||
| e3e179e380 | |||
| 4f768140c1 | |||
| e5728b3501 | |||
| f66f438487 | |||
| 2fd3bf0136 | |||
| c3264f0d15 | |||
| 8d7fdeb633 | |||
| 69011754c9 | |||
| e50d8dc148 | |||
| 23c24355ab | |||
| 0dcf2af50d | |||
| cf07428a5d | |||
| 35b381ae77 | |||
| 0fc748a234 | |||
| 638ea78db9 | |||
| e81747ef7b | |||
| 89e24cb358 | |||
| 00667cecf9 | |||
| 53fcdedb88 | |||
| 5bdf9363e9 | |||
| 6b44a77ef6 | |||
| edc6dc819c | |||
| ffd72eb232 | |||
| feee1f7ab3 | |||
| 2d79cd4b24 | |||
| 637bcfdbc7 | |||
| 22c9dd31bf | |||
| 9e63d80667 | |||
| 67b5b57533 | |||
| c2314f4838 | |||
| 1cb682473a | |||
| 63dd85cbfc | |||
| 6dbc8460b4 | |||
| ec8bf04baa | |||
| a1189faad1 | |||
| ba9cc55b5a | |||
| c2e527bce2 | |||
| 98f3bf151f | |||
| 802a4e243b | |||
| 215ad4fd4c | |||
| 7150d95a98 | |||
| 80636c53da | |||
| 874d3c251c | |||
| 6c541af0fd | |||
| 4d1269051b | |||
| be1b2716d6 | |||
| 34d46a9d5d | |||
| cac3a7cf1e | |||
| 0e86e63e97 | |||
| c0ff5715de | |||
| 24ce1b9893 | |||
| edd8e7c3b9 |
@@ -1,28 +1,55 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
raw_git = os.popen('git log next --since="24 hours" --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
|
||||
import requests
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# Set up your personal access token and the repository details
|
||||
token = os.environ.get('GH_TOKEN')
|
||||
repo_owner = "eried"
|
||||
repo_name = "portapack-mayhem"
|
||||
|
||||
|
||||
def compute_username(line):
|
||||
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
|
||||
def print_stable_changelog(previous_sha):
|
||||
url = f"compare/{previous_sha}...next"
|
||||
commits = handle_get_request(url)
|
||||
for commit in commits["commits"]:
|
||||
# Print the commit details
|
||||
print(format_output(commit))
|
||||
|
||||
pattern = re.compile("[$@+&?].*[$@+&?]")
|
||||
if pattern.match(stripped):
|
||||
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
|
||||
stripped = re.match(r'.+?(?=-)', stripped).group(0)
|
||||
|
||||
def print_nightly_changelog():
|
||||
# Calculate the date and time 24 hours ago
|
||||
since_time = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat() # Check that this is UTC
|
||||
url = "commits"
|
||||
commits = handle_get_request(url, since_time)
|
||||
for commit in commits:
|
||||
# Print the commit details
|
||||
print(format_output(commit))
|
||||
|
||||
|
||||
def handle_get_request(path, offset=None):
|
||||
headers = {} if token == None else {"Authorization": f"Bearer {token}"}
|
||||
params = {"since": offset}
|
||||
url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/"
|
||||
response = requests.get(url_base + path, headers=headers, params=params)
|
||||
|
||||
# Check if the request was successful (status code 200)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
stripped = re.sub(r'^.*?-', "", stripped)
|
||||
return "@" + stripped
|
||||
print(f"Request failed with status code: {response.status_code}")
|
||||
return None
|
||||
|
||||
|
||||
def compile_line(line):
|
||||
username = compute_username(line)
|
||||
line = re.sub(r'[*].*[*]', "", line)
|
||||
line = line.replace("{USERNAME}", username)
|
||||
return line
|
||||
def format_output(commit):
|
||||
message_lines = commit["commit"]["message"].split("\n")
|
||||
author = commit["author"]["login"] if commit["author"] and "login" in commit["author"] else commit["commit"]["author"]["name"]
|
||||
return '- ' + commit["sha"][:8] + ' - @' + author + ': ' + message_lines[0]
|
||||
|
||||
|
||||
for row in raw_git.splitlines():
|
||||
print(compile_line(row))
|
||||
if len(sys.argv) < 2:
|
||||
print_nightly_changelog()
|
||||
else:
|
||||
past_version = sys.argv[1]
|
||||
print_stable_changelog(past_version)
|
||||
|
||||
@@ -47,13 +47,24 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/build
|
||||
- name: Run the Docker image
|
||||
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev
|
||||
- name: Create Small SD Card ZIP - No World Map
|
||||
run: |
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard-no-map.zip sdcard
|
||||
- name: Download world map
|
||||
run: |
|
||||
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
|
||||
- name: Unzip world map
|
||||
run: |
|
||||
unzip world_map.zip -d sdcard/ADSB
|
||||
- name: Create Firmware ZIP
|
||||
run: |
|
||||
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
|
||||
- name: Create SD Card ZIP
|
||||
run: |
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && zip -r sdcard.zip sdcard
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard.zip sdcard
|
||||
- name: Create changelog
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
CHANGELOG=$(python3 .github/workflows/changelog.py)
|
||||
CHANGELOG="${CHANGELOG//'%'/'%25'}"
|
||||
@@ -97,3 +108,13 @@ jobs:
|
||||
asset_path: ./sdcard.zip
|
||||
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload SD Card Assets - No Map
|
||||
id: upload-sd-card-asset-no-map
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./sdcard-no-map.zip
|
||||
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -31,15 +31,26 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/build
|
||||
- name: Run the Docker image
|
||||
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev
|
||||
- name: Create Small SD Card ZIP - No World Map
|
||||
run: |
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard-no-map.zip sdcard
|
||||
- name: Download world map
|
||||
run: |
|
||||
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
|
||||
- name: Unzip world map
|
||||
run: |
|
||||
unzip world_map.zip -d sdcard/ADSB
|
||||
- name: Create Firmware ZIP
|
||||
run: |
|
||||
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
|
||||
- name: Create SD Card ZIP
|
||||
run: |
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && zip -r sdcard.zip sdcard
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && zip -r sdcard.zip sdcard
|
||||
- name: Create changelog
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
CHANGELOG=$(python3 .github/workflows/stable_changelog.py ${{ steps.past_version.outputs.past_version }})
|
||||
CHANGELOG=$(python3 .github/workflows/changelog.py ${{ steps.past_version.outputs.past_version }})
|
||||
CHANGELOG="${CHANGELOG//'%'/'%25'}"
|
||||
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
|
||||
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
|
||||
@@ -63,11 +74,12 @@ jobs:
|
||||
**Full Changelog**: https://github.com/eried/portapack-mayhem/compare/${{ steps.past_version.outputs.past_version }}...${{ steps.version.outputs.version }}
|
||||
|
||||
## Installation
|
||||
Check the [wiki](https://github.com/eried/portapack-havoc/wiki/Update-firmware) for details how to upgrade.
|
||||
Check the [wiki](https://github.com/eried/portapack-havoc/wiki/Update-firmware) for details how to upgrade.
|
||||
__Warning:__ Since release 1.8.0, some applications has been moved to the SD card as we ran out of flash space.
|
||||
|
||||
### MicroSD card files
|
||||
|
||||
For certain functionality, like 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.
|
||||
For certain functionality, like external apps, the world map, GPS simulator, and others you need to uncompress (using [7-zip](https://www.7-zip.org/download.html)) the files from `mayhem_vX.Y.Z_COPY_TO_SDCARD.zip` to a FAT32 formatted MicroSD card.
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload Firmware Asset
|
||||
@@ -90,4 +102,14 @@ jobs:
|
||||
asset_path: ./sdcard.zip
|
||||
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload SD Card Assets - No Map
|
||||
id: upload-sd-card-asset-no-map
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./sdcard-no-map.zip
|
||||
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.6.0
|
||||
v1.9.0
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
past_version = sys.argv[1]
|
||||
|
||||
raw_git = os.popen('git log ' + past_version + '..next --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
|
||||
|
||||
|
||||
def compute_username(line):
|
||||
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
|
||||
|
||||
pattern = re.compile("[$@+&?].*[$@+&?]")
|
||||
if pattern.match(stripped):
|
||||
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
|
||||
stripped = re.match(r'.+?(?=-)', stripped).group(0)
|
||||
else:
|
||||
stripped = re.sub(r'^.*?-', "", stripped)
|
||||
return "@" + stripped
|
||||
|
||||
|
||||
def compile_line(line):
|
||||
username = compute_username(line)
|
||||
line = re.sub(r'[*].*[*]', "", line)
|
||||
line = line.replace("{USERNAME}", username)
|
||||
return line
|
||||
|
||||
|
||||
for row in raw_git.splitlines():
|
||||
print(compile_line(row))
|
||||
@@ -1 +1 @@
|
||||
v1.7.0
|
||||
v1.9.1
|
||||
|
||||
+2
-1
@@ -55,10 +55,11 @@ CMakeFiles/
|
||||
# Debugging
|
||||
.gdbinit*
|
||||
|
||||
# Editor files
|
||||
# Editor/IDE files
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# VSCodium extensions
|
||||
.history
|
||||
|
||||
+10
-1
@@ -18,13 +18,15 @@
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
cmake_minimum_required(VERSION 2.8.9)
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_policy(SET CMP0005 NEW)
|
||||
|
||||
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_CURRENT_LIST_DIR}/firmware/toolchain-arm-cortex-m.cmake)
|
||||
|
||||
project(portapack-h1)
|
||||
|
||||
set(EXPECTED_GCC_VERSION "9.2.1")
|
||||
|
||||
set(VERSION "$ENV{VERSION_STRING}")
|
||||
if ("$ENV{VERSION_STRING}" STREQUAL "")
|
||||
execute_process(
|
||||
@@ -42,6 +44,12 @@ if ("$ENV{VERSION_STRING}" STREQUAL "")
|
||||
endif (GIT_VERSION_FOUND)
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND bash "-c" "echo ${VERSION} | md5sum | head -c 8"
|
||||
OUTPUT_VARIABLE VERSION_MD5
|
||||
)
|
||||
set(VERSION_MD5 "0x${VERSION_MD5}")
|
||||
|
||||
set(LICENSE_PATH ${CMAKE_CURRENT_LIST_DIR}/LICENSE)
|
||||
set(HARDWARE_PATH ${CMAKE_CURRENT_LIST_DIR}/hardware)
|
||||
|
||||
@@ -58,4 +66,5 @@ set(HACKRF_CPLD_XSVF_PATH ${HACKRF_PATH}/firmware/cpld/sgpio_if/${HACKRF_CPLD_XS
|
||||
set(HACKRF_FIRMWARE_DFU_IMAGE ${hackrf_usb_BINARY_DIR}/${HACKRF_FIRMWARE_DFU_FILENAME})
|
||||
set(HACKRF_FIRMWARE_BIN_IMAGE ${hackrf_usb_BINARY_DIR}/${HACKRF_FIRMWARE_BIN_FILENAME})
|
||||
|
||||
enable_testing()
|
||||
add_subdirectory(firmware)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
@@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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
|
||||
@@ -329,11 +329,11 @@ necessary. Here is a sample; alter the names:
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
Public License instead of this License.
|
||||
@@ -1,7 +0,0 @@
|
||||
# License
|
||||
Portapack Mayhem is distributed under [GPL v3.0](LICENSE), however some sub projects might have different (GPL v3.0 compatible) licenses.
|
||||
|
||||
## Other Licenses (compatible with GPL v3.0)
|
||||
- Most of Portapack Mayhem is distributed under GPL-2.0-or-later. [license](LICENSE.GPL-2.0-or-later)
|
||||
- ChibiOS is distributed under GPL v3.0. [license](/firmware/chibios/license.txt)
|
||||
- Portapack-ChibiOS is distributed under Apache-2.0 [license](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
@@ -1,6 +1,6 @@
|
||||
# PortaPack Mayhem
|
||||
|
||||
[](https://travis-ci.com/eried/portapack-mayhem) [](https://github.com/eried/portapack-mayhem/actions/workflows/create_nightly_release.yml) [](https://codescene.io/projects/8381) [](https://github.com/eried/portapack-mayhem/releases) [](https://github.com/eried/portapack-mayhem/releases/latest) [](https://hub.docker.com/r/eried/portapack) [](https://discord.gg/tuwVMv3) [](https://www.bountysource.com/teams/portapack-mayhem/issues)
|
||||
[](https://travis-ci.com/eried/portapack-mayhem) [](https://github.com/eried/portapack-mayhem/actions/workflows/create_nightly_release.yml) [](https://codescene.io/projects/8381) [](https://github.com/eried/portapack-mayhem/releases) [](https://github.com/eried/portapack-mayhem/releases/latest) [](https://hub.docker.com/r/eried/portapack) [](https://discord.gg/tuwVMv3)
|
||||
|
||||
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.
|
||||
|
||||
@@ -8,11 +8,11 @@ This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmw
|
||||
|
||||
*[PortaPack H2+HackRF+battery](https://s.click.aliexpress.com/e/_DmU7GQX) (clone) with a custom [3d printed case](https://github.com/eried/portapack-mayhem/wiki/H2-Enclosure)*
|
||||
|
||||
# Quick overview
|
||||
# What is this?
|
||||
|
||||
If you are new to *HackRF+PortaPack+Mayhem*, there is an awesome introductory video by [Tech Minds](https://www.youtube.com/channel/UC9a8Z6Sp6eb2s3O79pX5Zvg) available:
|
||||
If you are new to *HackRF+PortaPack+Mayhem*, check this video:
|
||||
|
||||
[](https://www.youtube.com/watch?v=kjFB58Y1TAo)
|
||||
[](https://www.youtube.com/watch?v=alrFbY5vxt4)
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
@@ -20,7 +20,9 @@ This repository expands upon the previous work by many people and aims to consta
|
||||
|
||||
## What to buy?
|
||||
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://s.click.aliexpress.com/e/_DmU7GQX) pack, that includes everything you need. Sadly, the people making the H2 never made the updated schematics available (against the terms of the license).
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://s.click.aliexpress.com/e/_DmU7GQX), that includes everything you need with the plastic case "inspired" on [this](https://github.com/eried/portapack-mayhem/wiki/H2-Enclosure).
|
||||
|
||||
:heavy_check_mark: Our friends at OpenSourceSDRLab give away five units every three months in our discord (check the badge on top) of their [PortaPack H2](https://www.aliexpress.com/item/4000247041639.html?gatewayAdapt=4itemAdapt), you can support them too by ordering.
|
||||
|
||||
:heavy_check_mark: Another popular option is the clone of the [PortaPack H1](https://s.click.aliexpress.com/e/_Dkbqs2X).
|
||||
|
||||
@@ -45,16 +47,14 @@ Consider that the hardware and firmware has been created and maintain by a [lot]
|
||||
|
||||
To support the people behind the hardware, please buy a genuine [HackRF](https://greatscottgadgets.com/hackrf/) and [PortaPack](https://store.sharebrained.com/products/portapack-for-hackrf-one-kit).
|
||||
|
||||
As a last option, if you want to send money directly to me for getting more boards, antennas and such:
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CBPQA4HRRPJQ6&source=url)
|
||||
|
||||
## What if I really want something specific?
|
||||
If what you need can be relevant in general, you can [request a feature](https://github.com/eried/portapack-mayhem/issues/new?labels=enhancement&template=feature_request.md).
|
||||
|
||||
You can create a bounty and invite people to your own bounty. This will incentivize coders to work on a new feature, solving a bug or even writting documentation. Start a bounty by [creating](https://github.com/eried/portapack-mayhem/issues/new/choose) or [choosing](https://github.com/eried/portapack-mayhem/issues/) an existing issue. Then, go to [Bountysource](https://www.bountysource.com/) and post a bounty using the link to that specific [issue](https://www.bountysource.com/teams/portapack-mayhem/issues).
|
||||
<del>You can create a bounty and invite people to your own bounty. This will incentivize coders to work on a new feature, solving a bug or even writting documentation. Start a bounty by [creating](https://github.com/eried/portapack-mayhem/issues/new/choose) or [choosing](https://github.com/eried/portapack-mayhem/issues/) an existing issue. Then, go to [Bountysource](https://www.bountysource.com/) and post a bounty using the link to that specific [issue](https://www.bountysource.com/teams/portapack-mayhem/issues).</del>
|
||||
|
||||
Promote your bounty over our Discord by clicking the chat badge on [top](#portapack-mayhem).
|
||||
<del>Promote your bounty over our Discord by clicking the chat badge on [top](#portapack-mayhem).</del>
|
||||
|
||||
Bountysource has not been reliable lately, so until this changes, please **DO NOT** post a bounty there. Go to our Discord by clicking the chat badge on [top](#portapack-mayhem) and discuss there.
|
||||
|
||||
## What if I need help?
|
||||
First, check the [documentation](https://github.com/eried/portapack-mayhem/wiki). If you find a bug or you think the problem is related to the current repository, please open an [issue](https://github.com/eried/portapack-mayhem/issues/new/choose).
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
# Set location to download ARM toolkit from.
|
||||
# This will need to be changed over time or replaced with a static link to the latest release.
|
||||
ENV ARMBINURL=https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-aarch64-linux.tar.bz2?revision=4583ce78-e7e7-459a-ad9f-bff8e94839f1&rev=4583ce78e7e7459aad9fbff8e94839f1&hash=C9B942D74CEA05FF9DE28380F267BB7D3F3B17A6 \
|
||||
PATH=$HOME/bin:$PATH:/opt/build/armbin/bin
|
||||
|
||||
#Create volume /havoc/bin for compiled firmware binaries
|
||||
VOLUME /havoc
|
||||
WORKDIR /havoc/firmware
|
||||
|
||||
# Fetch dependencies from APT
|
||||
RUN apt-get update && \
|
||||
apt-get install -y git tar wget dfu-util cmake python3 ccache bzip2 liblz4-tool curl && \
|
||||
apt-get -qy autoremove
|
||||
|
||||
#Install current pip from PyPa
|
||||
RUN curl https://bootstrap.pypa.io/pip/3.4/get-pip.py -o get-pip.py && \
|
||||
python3 get-pip.py
|
||||
|
||||
#Fetch additional dependencies from Python 3.x pip
|
||||
RUN pip install pyyaml
|
||||
RUN ln -s /usr/bin/python3 /usr/bin/python && \
|
||||
ln -s /usr/bin/pip3 /usr/bin/pip
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
# Grab the GNU ARM toolchain from arm.com
|
||||
# Then extract contents to /opt/build/armbin/
|
||||
RUN mkdir /opt/build && cd /opt/build && \
|
||||
wget -O gcc-arm-none-eabi $ARMBINURL && \
|
||||
mkdir armbin && \
|
||||
tar --strip=1 -xjvf gcc-arm-none-eabi -C armbin
|
||||
|
||||
# Configure CCACHE
|
||||
RUN mkdir ~/bin && cd ~/bin && \
|
||||
for tool in gcc g++ cpp c++;do ln -s $(which ccache) arm-none-eabi-$tool;done
|
||||
|
||||
CMD cd .. && cd build && \
|
||||
cmake .. && make firmware
|
||||
+33
-3
@@ -26,6 +26,7 @@ set(CHIBIOS ${PROJECT_SOURCE_DIR}/chibios)
|
||||
set(CHIBIOS_PORTAPACK ${PROJECT_SOURCE_DIR}/chibios-portapack)
|
||||
|
||||
set(EXTRACT_CPLD_DATA ${PROJECT_SOURCE_DIR}/tools/extract_cpld_data.py)
|
||||
set(EXPORT_EXTERNAL_APP_IMAGES ${PROJECT_SOURCE_DIR}/tools/export_external_apps.py)
|
||||
set(MAKE_SPI_IMAGE ${PROJECT_SOURCE_DIR}/tools/make_spi_image.py)
|
||||
set(MAKE_IMAGE_CHUNK ${PROJECT_SOURCE_DIR}/tools/make_image_chunk.py)
|
||||
set(LZ4 lz4)
|
||||
@@ -33,8 +34,20 @@ set(LZ4 lz4)
|
||||
set(FIRMWARE_NAME portapack-h1_h2-mayhem)
|
||||
set(FIRMWARE_FILENAME ${FIRMWARE_NAME}.bin)
|
||||
|
||||
# In our current build container cmake need a little help to get the version :)
|
||||
if(NOT DEFINED ${CMAKE_CXX_COMPILER_VERSION})
|
||||
execute_process(COMMAND bash "-c" "arm-none-eabi-g++ -v 2>&1 | grep 'gcc version' | awk '{print $3}'" OUTPUT_VARIABLE CMAKE_CXX_COMPILER_VERSION)
|
||||
string(STRIP ${CMAKE_CXX_COMPILER_VERSION} CMAKE_CXX_COMPILER_VERSION)
|
||||
endif()
|
||||
|
||||
set(GCC_VERSION_MISMATCH 0)
|
||||
if(NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_EQUAL ${EXPECTED_GCC_VERSION})
|
||||
set(GCC_VERSION_MISMATCH 1)
|
||||
endif()
|
||||
|
||||
add_subdirectory(application)
|
||||
add_subdirectory(baseband)
|
||||
add_subdirectory(test)
|
||||
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
@@ -50,12 +63,29 @@ add_custom_target(
|
||||
DEPENDS ${FIRMWARE_FILENAME} ${HACKRF_FIRMWARE_DFU_FILENAME}
|
||||
)
|
||||
|
||||
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})
|
||||
add_custom_command(
|
||||
TARGET firmware POST_BUILD
|
||||
COMMAND echo ${COMPILER_MISMATCH_MESSAGE}
|
||||
VERBATIM)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
program
|
||||
COMMAND dfu-util --device 1fc9:000c --download ${HACKRF_FIRMWARE_DFU_IMAGE}
|
||||
COMMAND ${PROJECT_SOURCE_DIR}/tools/enter_mode.sh hackrf
|
||||
COMMAND dfu-util --device 1fc9:000c --download ${HACKRF_FIRMWARE_DFU_IMAGE} || (exit 0) # We need to add it for dfu-utils v.011 , (in v.09 it is not necessary)
|
||||
COMMAND sleep 3s
|
||||
COMMAND hackrf_spiflash -w ${FIRMWARE_FILENAME}
|
||||
DEPENDS ${FIRMWARE_FILENAME}
|
||||
COMMAND hackrf_spiflash -i -R -w ${FIRMWARE_FILENAME}
|
||||
DEPENDS firmware ${FIRMWARE_FILENAME}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
program-external-apps
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMAND ${PROJECT_SOURCE_DIR}/tools/copy_external_apps.sh
|
||||
DEPENDS program
|
||||
)
|
||||
|
||||
# TODO: Bad hack to fix location of LICENSE file for tar.
|
||||
|
||||
@@ -27,16 +27,24 @@
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
include(CheckCXXCompilerFlag)
|
||||
|
||||
project(application)
|
||||
|
||||
# Compiler options here.
|
||||
set(USE_OPT "-Os -g --specs=nano.specs")
|
||||
set(USE_OPT "-Os -g --specs=nano.specs -specs=nosys.specs")
|
||||
|
||||
# C specific options here (added to USE_OPT).
|
||||
set(USE_COPT "-std=gnu99")
|
||||
|
||||
# C++ specific options here (added to USE_OPT).
|
||||
set(USE_CPPOPT "-std=c++17 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
|
||||
check_cxx_compiler_flag("-std=c++20" cpp20_supported)
|
||||
if(cpp20_supported)
|
||||
set(USE_CPPOPT "-std=c++20")
|
||||
else()
|
||||
set(USE_CPPOPT "-std=c++17")
|
||||
endif()
|
||||
set(USE_CPPOPT "${USE_CPPOPT} -flto -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized -Wno-volatile")
|
||||
|
||||
# Enable this if you want the linker to remove unused code and data
|
||||
set(USE_LINK_GC yes)
|
||||
@@ -89,6 +97,7 @@ include(${CHIBIOS_PORTAPACK}/os/ports/GCC/ARMCMx/LPC43xx_M0/port.cmake)
|
||||
include(${CHIBIOS}/os/kernel/kernel.cmake)
|
||||
include(${CHIBIOS_PORTAPACK}/os/various/fatfs_bindings/fatfs.cmake)
|
||||
include(${CHIBIOS}/test/test.cmake)
|
||||
include(external/external.cmake)
|
||||
|
||||
# Define linker script file here
|
||||
set(LDSCRIPT ${PORTLD}/LPC43xx_M0.ld)
|
||||
@@ -104,6 +113,17 @@ set(CSRC
|
||||
${BOARDSRC}
|
||||
${FATFSSRC}
|
||||
firmware_info.c
|
||||
usb_serial_cdc.c
|
||||
usb_serial_descriptor.c
|
||||
usb_serial_endpoints.c
|
||||
usb_serial_io.c
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${CHIBIOS}/os/various/shell.c
|
||||
${CHIBIOS}/os/various/chprintf.c
|
||||
)
|
||||
|
||||
# C++ sources that can be compiled in ARM or THUMB mode depending on the global
|
||||
@@ -153,6 +173,7 @@ set(CPPSRC
|
||||
${COMMON}/ui_painter.cpp
|
||||
${COMMON}/ui_text.cpp
|
||||
${COMMON}/ui_widget.cpp
|
||||
${COMMON}/ui_language.cpp
|
||||
${COMMON}/utility.cpp
|
||||
${COMMON}/wm8731.cpp
|
||||
${COMMON}/performance_counter.cpp
|
||||
@@ -167,15 +188,24 @@ set(CPPSRC
|
||||
#emu_cc1101.cpp
|
||||
rfm69.cpp
|
||||
event_m0.cpp
|
||||
file_reader.cpp
|
||||
file.cpp
|
||||
freqman_db.cpp
|
||||
freqman.cpp
|
||||
io_convert.cpp
|
||||
io_file.cpp
|
||||
io_wave.cpp
|
||||
iq_trim.cpp
|
||||
irq_controls.cpp
|
||||
irq_lcd_frame.cpp
|
||||
irq_rtc.cpp
|
||||
log_file.cpp
|
||||
metadata_file.cpp
|
||||
portapack.cpp
|
||||
usb_serial_shell.cpp
|
||||
usb_serial_event.cpp
|
||||
usb_serial_thread.cpp
|
||||
usb_serial.cpp
|
||||
qrcodegen.cpp
|
||||
radio.cpp
|
||||
receiver_model.cpp
|
||||
@@ -218,80 +248,89 @@ set(CPPSRC
|
||||
ui/ui_btngrid.cpp
|
||||
ui/ui_receiver.cpp
|
||||
ui/ui_rssi.cpp
|
||||
ui/ui_freqlist.cpp
|
||||
ui/ui_tv.cpp
|
||||
ui/ui_spectrum.cpp
|
||||
ui/ui_styles.cpp
|
||||
ui/ui_tabview.cpp
|
||||
ui/ui_textentry.cpp
|
||||
ui/ui_tone_key.cpp
|
||||
ui/ui_transmitter.cpp
|
||||
apps/acars_app.cpp
|
||||
apps/ais_app.cpp
|
||||
apps/analog_audio_app.cpp
|
||||
# apps/analog_tv_app.cpp
|
||||
apps/ble_comm_app.cpp
|
||||
apps/ble_rx_app.cpp
|
||||
apps/ble_tx_app.cpp
|
||||
apps/capture_app.cpp
|
||||
apps/ert_app.cpp
|
||||
apps/gps_sim_app.cpp
|
||||
# apps/lge_app.cpp
|
||||
apps/pocsag_app.cpp
|
||||
# apps/replay_app.cpp
|
||||
apps/soundboard_app.cpp
|
||||
apps/tpms_app.cpp
|
||||
apps/tpms_app.cpp
|
||||
apps/ui_about_simple.cpp
|
||||
apps/ui_adsb_rx.cpp
|
||||
apps/ui_adsb_tx.cpp
|
||||
apps/ui_afsk_rx.cpp
|
||||
apps/ui_aprs_rx.cpp
|
||||
apps/ui_btle_rx.cpp
|
||||
apps/ui_nrf_rx.cpp
|
||||
apps/ui_aprs_tx.cpp
|
||||
apps/ui_bht_tx.cpp
|
||||
apps/ui_dfu_menu.cpp
|
||||
apps/ui_coasterp.cpp
|
||||
apps/ui_btle_rx.cpp
|
||||
# apps/ui_coasterp.cpp
|
||||
apps/ui_debug.cpp
|
||||
apps/ui_dfu_menu.cpp
|
||||
apps/ui_encoders.cpp
|
||||
apps/ui_fileman.cpp
|
||||
apps/ui_flash_utility.cpp
|
||||
apps/ui_sd_over_usb.cpp
|
||||
apps/ui_freqman.cpp
|
||||
apps/ui_jammer.cpp
|
||||
#apps/ui_keyfob.cpp
|
||||
apps/ui_lcr.cpp
|
||||
apps/lge_app.cpp
|
||||
apps/ui_fsk_rx.cpp
|
||||
apps/ui_iq_trim.cpp
|
||||
# apps/ui_jammer.cpp
|
||||
# apps/ui_keyfob.cpp
|
||||
# apps/ui_lcr.cpp
|
||||
apps/ui_level.cpp
|
||||
apps/ui_looking_glass_app.cpp
|
||||
apps/ui_mictx.cpp
|
||||
apps/ui_modemsetup.cpp
|
||||
apps/ui_morse.cpp
|
||||
# apps/ui_nrf_rx.cpp
|
||||
# apps/ui_nuoptix.cpp
|
||||
apps/ui_playlist.cpp
|
||||
apps/ui_pocsag_tx.cpp
|
||||
apps/ui_rds.cpp
|
||||
apps/ui_recon_settings.cpp
|
||||
apps/ui_recon.cpp
|
||||
apps/ui_remote.cpp
|
||||
apps/ui_scanner.cpp
|
||||
apps/ui_search.cpp
|
||||
apps/ui_sd_over_usb.cpp
|
||||
apps/ui_sd_wipe.cpp
|
||||
apps/ui_search.cpp
|
||||
apps/ui_settings.cpp
|
||||
apps/ui_siggen.cpp
|
||||
apps/ui_sonde.cpp
|
||||
apps/ui_spectrum_painter_image.cpp
|
||||
apps/ui_spectrum_painter_text.cpp
|
||||
apps/ui_spectrum_painter.cpp
|
||||
apps/ui_ss_viewer.cpp
|
||||
apps/ui_sstvtx.cpp
|
||||
apps/ui_subghzd.cpp
|
||||
# apps/ui_test.cpp
|
||||
apps/ui_text_editor.cpp
|
||||
apps/ui_tone_search.cpp
|
||||
apps/ui_touch_calibration.cpp
|
||||
apps/ui_touchtunes.cpp
|
||||
apps/ui_view_wav.cpp
|
||||
apps/ui_weatherstation.cpp
|
||||
apps/ui_whipcalc.cpp
|
||||
apps/acars_app.cpp
|
||||
apps/ais_app.cpp
|
||||
apps/analog_audio_app.cpp
|
||||
apps/analog_tv_app.cpp
|
||||
apps/capture_app.cpp
|
||||
apps/ert_app.cpp
|
||||
apps/lge_app.cpp
|
||||
apps/pocsag_app.cpp
|
||||
apps/replay_app.cpp
|
||||
apps/ui_playlist.cpp
|
||||
apps/gps_sim_app.cpp
|
||||
apps/soundboard_app.cpp
|
||||
apps/ui_recon.cpp
|
||||
apps/ui_recon_settings.cpp
|
||||
apps/ui_level.cpp
|
||||
apps/tpms_app.cpp
|
||||
apps/tpms_app.cpp
|
||||
apps/ui_spectrum_painter.cpp
|
||||
apps/ui_spectrum_painter_image.cpp
|
||||
apps/ui_spectrum_painter_text.cpp
|
||||
protocols/aprs.cpp
|
||||
protocols/ax25.cpp
|
||||
protocols/bht.cpp
|
||||
protocols/dcs.cpp
|
||||
protocols/encoders.cpp
|
||||
protocols/lcr.cpp
|
||||
# protocols/lcr.cpp
|
||||
protocols/modems.cpp
|
||||
protocols/rds.cpp
|
||||
# ui_handwrite.cpp
|
||||
@@ -300,9 +339,13 @@ set(CPPSRC
|
||||
# ui_replay_view.cpp
|
||||
# ui_script.cpp
|
||||
ui_sd_card_debug.cpp
|
||||
config_mode.cpp
|
||||
${CPLD_20150901_DATA_CPP}
|
||||
${CPLD_20170522_DATA_CPP}
|
||||
${HACKRF_CPLD_DATA_CPP}
|
||||
ui_external_items_menu_loader.cpp
|
||||
|
||||
${EXTCPPSRC}
|
||||
)
|
||||
|
||||
# C sources to be compiled in ARM mode regardless of the global setting.
|
||||
@@ -335,6 +378,9 @@ set(INCDIR ${CMAKE_CURRENT_BINARY_DIR} ${COMMON} ${PORTINC} ${KERNINC} ${TESTINC
|
||||
${HALINC} ${PLATFORMINC} ${BOARDINC}
|
||||
${FATFSINC}
|
||||
${CHIBIOS}/os/various
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware
|
||||
ui
|
||||
hw
|
||||
apps
|
||||
@@ -378,7 +424,7 @@ set(CPPWARN "-Wall -Wextra -Wno-psabi")
|
||||
# List all default C defines here, like -D_DEBUG=1
|
||||
# TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI?
|
||||
# NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'")
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"' -DVERSION_MD5=${VERSION_MD5} -D'GCC_VERSION_MISMATCH=${GCC_VERSION_MISMATCH}'")
|
||||
|
||||
# List all default ASM defines here, like -D_DEBUG=1
|
||||
set(DADEFS)
|
||||
@@ -447,12 +493,13 @@ set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
|
||||
add_definitions(${DEFS})
|
||||
include_directories(. ${INCDIR})
|
||||
link_directories(${LLIBDIR})
|
||||
target_link_libraries(${PROJECT_NAME}.elf ${LIBS})
|
||||
target_link_libraries(${PROJECT_NAME}.elf ${LIBS} "-L${CMAKE_CURRENT_LIST_DIR}/external")
|
||||
target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PROJECT_NAME}.bin
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
|
||||
COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin --remove-section=.external_app_*
|
||||
COMMAND ${EXPORT_EXTERNAL_APP_IMAGES} ${PROJECT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_OBJCOPY} ${EXTAPPLIST}
|
||||
DEPENDS ${PROJECT_NAME}.elf
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2022 Arjan Onwezen
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -23,89 +24,281 @@
|
||||
|
||||
#include "app_settings.hpp"
|
||||
|
||||
#include "convert.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <cstring>
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
namespace std {
|
||||
namespace fs = std::filesystem;
|
||||
using namespace portapack;
|
||||
|
||||
int app_settings::load(std::string application, AppSettings* settings) {
|
||||
if (portapack::persistent_memory::load_app_settings()) {
|
||||
file_path = folder + "/" + application + ".ini";
|
||||
namespace {
|
||||
fs::path get_settings_path(const std::string& app_name) {
|
||||
return fs::path{u"/SETTINGS"} / app_name + u".ini";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto error = settings_file.open(file_path);
|
||||
if (!error.is_valid()) {
|
||||
auto error = settings_file.read(file_content, std::min((int)settings_file.size(), MAX_FILE_CONTENT_SIZE));
|
||||
|
||||
settings->baseband_bandwidth = std::app_settings::read_long_long(file_content, "baseband_bandwidth=");
|
||||
settings->channel_bandwidth = std::app_settings::read_long_long(file_content, "channel_bandwidth=");
|
||||
settings->lna = std::app_settings::read_long_long(file_content, "lna=");
|
||||
settings->modulation = std::app_settings::read_long_long(file_content, "modulation=");
|
||||
settings->rx_amp = std::app_settings::read_long_long(file_content, "rx_amp=");
|
||||
settings->rx_frequency = std::app_settings::read_long_long(file_content, "rx_frequency=");
|
||||
settings->sampling_rate = std::app_settings::read_long_long(file_content, "sampling_rate=");
|
||||
settings->vga = std::app_settings::read_long_long(file_content, "vga=");
|
||||
settings->tx_amp = std::app_settings::read_long_long(file_content, "tx_amp=");
|
||||
settings->tx_frequency = std::app_settings::read_long_long(file_content, "tx_frequency=");
|
||||
settings->tx_gain = std::app_settings::read_long_long(file_content, "tx_gain=");
|
||||
settings->step = std::app_settings::read_long_long(file_content, "step=");
|
||||
settings->modulation = std::app_settings::read_long_long(file_content, "modulation=");
|
||||
settings->am_config_index = std::app_settings::read_long_long(file_content, "am_config_index=");
|
||||
settings->nbfm_config_index = std::app_settings::read_long_long(file_content, "nbfm_config_index=");
|
||||
settings->wfm_config_index = std::app_settings::read_long_long(file_content, "wfm_config_index=");
|
||||
settings->squelch = std::app_settings::read_long_long(file_content, "squelch=");
|
||||
|
||||
rc = SETTINGS_OK;
|
||||
} else
|
||||
rc = SETTINGS_UNABLE_TO_LOAD;
|
||||
} else
|
||||
rc = SETTINGS_DISABLED;
|
||||
return (rc);
|
||||
void BoundSetting::parse(std::string_view value) {
|
||||
switch (type_) {
|
||||
case SettingType::I64:
|
||||
parse_int(value, as<int64_t>());
|
||||
break;
|
||||
case SettingType::I32:
|
||||
parse_int(value, as<int32_t>());
|
||||
break;
|
||||
case SettingType::U32:
|
||||
parse_int(value, as<uint32_t>());
|
||||
break;
|
||||
case SettingType::U8:
|
||||
parse_int(value, as<uint8_t>());
|
||||
break;
|
||||
case SettingType::String:
|
||||
as<std::string>() = trim(value);
|
||||
break;
|
||||
case SettingType::Bool: {
|
||||
int parsed = 0;
|
||||
parse_int(value, parsed);
|
||||
as<bool>() = (parsed != 0);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
int app_settings::save(std::string application, AppSettings* settings) {
|
||||
if (portapack::persistent_memory::save_app_settings()) {
|
||||
file_path = folder + "/" + application + ".ini";
|
||||
make_new_directory(folder);
|
||||
void BoundSetting::write(File& file) const {
|
||||
// NB: Write directly without allocations. This happens on every
|
||||
// app exit when enabled so should be fast to keep the UX responsive.
|
||||
StringFormatBuffer buffer;
|
||||
size_t length = 0;
|
||||
|
||||
auto error = settings_file.create(file_path);
|
||||
if (!error.is_valid()) {
|
||||
// Save common setting
|
||||
settings_file.write_line("baseband_bandwidth=" + to_string_dec_uint(portapack::receiver_model.baseband_bandwidth()));
|
||||
settings_file.write_line("channel_bandwidth=" + to_string_dec_uint(portapack::transmitter_model.channel_bandwidth()));
|
||||
settings_file.write_line("lna=" + to_string_dec_uint(portapack::receiver_model.lna()));
|
||||
settings_file.write_line("rx_amp=" + to_string_dec_uint(portapack::receiver_model.rf_amp()));
|
||||
settings_file.write_line("sampling_rate=" + to_string_dec_uint(portapack::receiver_model.sampling_rate()));
|
||||
settings_file.write_line("tx_amp=" + to_string_dec_uint(portapack::transmitter_model.rf_amp()));
|
||||
settings_file.write_line("tx_gain=" + to_string_dec_uint(portapack::transmitter_model.tx_gain()));
|
||||
settings_file.write_line("vga=" + to_string_dec_uint(portapack::receiver_model.vga()));
|
||||
// Save other settings from struct
|
||||
settings_file.write_line("rx_frequency=" + to_string_dec_uint(settings->rx_frequency));
|
||||
settings_file.write_line("tx_frequency=" + to_string_dec_uint(settings->tx_frequency));
|
||||
settings_file.write_line("step=" + to_string_dec_uint(settings->step));
|
||||
settings_file.write_line("modulation=" + to_string_dec_uint(settings->modulation));
|
||||
settings_file.write_line("am_config_index=" + to_string_dec_uint(settings->am_config_index));
|
||||
settings_file.write_line("nbfm_config_index=" + to_string_dec_uint(settings->nbfm_config_index));
|
||||
settings_file.write_line("wfm_config_index=" + to_string_dec_uint(settings->wfm_config_index));
|
||||
settings_file.write_line("squelch=" + to_string_dec_uint(settings->squelch));
|
||||
file.write(name_.data(), name_.length());
|
||||
file.write("=", 1);
|
||||
|
||||
rc = SETTINGS_OK;
|
||||
} else
|
||||
rc = SETTINGS_UNABLE_TO_SAVE;
|
||||
} else
|
||||
rc = SETTINGS_DISABLED;
|
||||
return (rc);
|
||||
}
|
||||
|
||||
long long int app_settings::read_long_long(char* file_content, const char* setting_text) {
|
||||
auto position = strstr(file_content, (char*)setting_text);
|
||||
if (position) {
|
||||
position += strlen((char*)setting_text);
|
||||
setting_value = strtoll(position, nullptr, 10);
|
||||
switch (type_) {
|
||||
case SettingType::I64:
|
||||
file.write(to_string_dec_int(as<int64_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::I32:
|
||||
file.write(to_string_dec_int(as<int32_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::U32:
|
||||
file.write(to_string_dec_uint(as<uint32_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::U8:
|
||||
file.write(to_string_dec_uint(as<uint8_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::String: {
|
||||
const auto& str = as<std::string>();
|
||||
file.write(str.data(), str.length());
|
||||
break;
|
||||
}
|
||||
case SettingType::Bool:
|
||||
file.write(as<bool>() ? "1" : "0", 1);
|
||||
break;
|
||||
}
|
||||
return (setting_value);
|
||||
|
||||
file.write("\r\n", 2);
|
||||
}
|
||||
|
||||
} /* namespace std */
|
||||
SettingsStore::SettingsStore(std::string_view store_name, SettingBindings bindings)
|
||||
: store_name_{store_name}, bindings_{bindings} {
|
||||
reload();
|
||||
}
|
||||
|
||||
SettingsStore::~SettingsStore() {
|
||||
save();
|
||||
}
|
||||
|
||||
void SettingsStore::reload() {
|
||||
load_settings(store_name_, bindings_);
|
||||
}
|
||||
|
||||
void SettingsStore::save() const {
|
||||
save_settings(store_name_, bindings_);
|
||||
}
|
||||
|
||||
bool load_settings(std::string_view store_name, SettingBindings& bindings) {
|
||||
File f;
|
||||
auto path = get_settings_path(std::string{store_name});
|
||||
|
||||
auto error = f.open(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
auto reader = FileLineReader(f);
|
||||
for (const auto& line : reader) {
|
||||
auto cols = split_string(line, '=');
|
||||
|
||||
if (cols.size() != 2)
|
||||
continue;
|
||||
|
||||
// Find a binding with the name.
|
||||
auto it = std::find_if(
|
||||
bindings.begin(), bindings.end(),
|
||||
[name = cols[0]](auto& bound_setting) {
|
||||
return name == bound_setting.name();
|
||||
});
|
||||
|
||||
// If found, parse the value.
|
||||
if (it != bindings.end())
|
||||
it->parse(cols[1]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool save_settings(std::string_view store_name, const SettingBindings& bindings) {
|
||||
File f;
|
||||
auto path = get_settings_path(std::string{store_name});
|
||||
|
||||
make_new_directory(SETTINGS_DIR);
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
for (const auto& bound_setting : bindings)
|
||||
bound_setting.write(f);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace app_settings {
|
||||
|
||||
void copy_to_radio_model(const AppSettings& settings) {
|
||||
// NB: Don't actually adjust the radio here or it may hang.
|
||||
// Specifically 'modulation' which requires a running baseband.
|
||||
|
||||
if (flags_enabled(settings.mode, Mode::TX)) {
|
||||
if (!flags_enabled(settings.options, Options::UseGlobalTargetFrequency))
|
||||
persistent_memory::set_target_frequency(settings.tx_frequency);
|
||||
|
||||
transmitter_model.configure_from_app_settings(settings);
|
||||
}
|
||||
|
||||
if (flags_enabled(settings.mode, Mode::RX)) {
|
||||
if (!flags_enabled(settings.options, Options::UseGlobalTargetFrequency))
|
||||
persistent_memory::set_target_frequency(settings.rx_frequency);
|
||||
|
||||
receiver_model.configure_from_app_settings(settings);
|
||||
receiver_model.set_configuration_without_update(
|
||||
static_cast<ReceiverModel::Mode>(settings.modulation),
|
||||
settings.step,
|
||||
settings.am_config_index,
|
||||
settings.nbfm_config_index,
|
||||
settings.wfm_config_index,
|
||||
settings.squelch);
|
||||
}
|
||||
|
||||
receiver_model.set_frequency_step(settings.step);
|
||||
receiver_model.set_normalized_headphone_volume(settings.volume);
|
||||
}
|
||||
|
||||
void copy_from_radio_model(AppSettings& settings) {
|
||||
if (flags_enabled(settings.mode, Mode::TX)) {
|
||||
settings.tx_frequency = transmitter_model.target_frequency();
|
||||
settings.baseband_bandwidth = transmitter_model.baseband_bandwidth();
|
||||
settings.sampling_rate = transmitter_model.sampling_rate();
|
||||
settings.tx_amp = transmitter_model.rf_amp();
|
||||
settings.tx_gain = transmitter_model.tx_gain();
|
||||
settings.channel_bandwidth = transmitter_model.channel_bandwidth();
|
||||
}
|
||||
|
||||
if (flags_enabled(settings.mode, Mode::RX)) {
|
||||
settings.rx_frequency = receiver_model.target_frequency();
|
||||
settings.baseband_bandwidth = receiver_model.baseband_bandwidth();
|
||||
settings.sampling_rate = receiver_model.sampling_rate();
|
||||
settings.lna = receiver_model.lna();
|
||||
settings.vga = receiver_model.vga();
|
||||
settings.rx_amp = receiver_model.rf_amp();
|
||||
settings.squelch = receiver_model.squelch_level();
|
||||
|
||||
settings.modulation = static_cast<uint8_t>(receiver_model.modulation());
|
||||
settings.am_config_index = receiver_model.am_configuration();
|
||||
settings.nbfm_config_index = receiver_model.nbfm_configuration();
|
||||
settings.wfm_config_index = receiver_model.wfm_configuration();
|
||||
}
|
||||
|
||||
settings.step = receiver_model.frequency_step();
|
||||
settings.volume = receiver_model.normalized_headphone_volume();
|
||||
}
|
||||
|
||||
/* SettingsManager *************************************************/
|
||||
SettingsManager::SettingsManager(std::string_view app_name, Mode mode, Options options)
|
||||
: SettingsManager{app_name, mode, options, {}} {}
|
||||
|
||||
SettingsManager::SettingsManager(
|
||||
std::string_view app_name,
|
||||
Mode mode,
|
||||
SettingBindings additional_settings)
|
||||
: SettingsManager{app_name, mode, Options::None, std::move(additional_settings)} {}
|
||||
|
||||
SettingsManager::SettingsManager(
|
||||
std::string_view app_name,
|
||||
Mode mode,
|
||||
Options options,
|
||||
SettingBindings additional_settings)
|
||||
: app_name_{app_name},
|
||||
settings_{},
|
||||
bindings_{},
|
||||
loaded_{false} {
|
||||
settings_.mode = mode;
|
||||
settings_.options = options;
|
||||
|
||||
// Pre-alloc enough for app settings and additional settings.
|
||||
additional_settings.reserve(COMMON_APP_SETTINGS_COUNT + additional_settings.size());
|
||||
bindings_ = std::move(additional_settings);
|
||||
|
||||
// Settings should always be loaded because apps now rely
|
||||
// on being able to store UI settings, config, etc.
|
||||
|
||||
// Transmitter model settings.
|
||||
if (flags_enabled(mode, Mode::TX)) {
|
||||
bindings_.emplace_back("tx_frequency"sv, &settings_.tx_frequency);
|
||||
bindings_.emplace_back("tx_amp"sv, &settings_.tx_amp);
|
||||
bindings_.emplace_back("tx_gain"sv, &settings_.tx_gain);
|
||||
bindings_.emplace_back("channel_bandwidth"sv, &settings_.channel_bandwidth);
|
||||
}
|
||||
|
||||
// Receiver model settings.
|
||||
if (flags_enabled(mode, Mode::RX)) {
|
||||
bindings_.emplace_back("rx_frequency"sv, &settings_.rx_frequency);
|
||||
bindings_.emplace_back("lna"sv, &settings_.lna);
|
||||
bindings_.emplace_back("vga"sv, &settings_.vga);
|
||||
bindings_.emplace_back("rx_amp"sv, &settings_.rx_amp);
|
||||
bindings_.emplace_back("modulation"sv, &settings_.modulation);
|
||||
bindings_.emplace_back("am_config_index"sv, &settings_.am_config_index);
|
||||
bindings_.emplace_back("nbfm_config_index"sv, &settings_.nbfm_config_index);
|
||||
bindings_.emplace_back("wfm_config_index"sv, &settings_.wfm_config_index);
|
||||
bindings_.emplace_back("squelch"sv, &settings_.squelch);
|
||||
}
|
||||
|
||||
// Common model settings.
|
||||
bindings_.emplace_back("baseband_bandwidth"sv, &settings_.baseband_bandwidth);
|
||||
bindings_.emplace_back("sampling_rate"sv, &settings_.sampling_rate);
|
||||
bindings_.emplace_back("step"sv, &settings_.step);
|
||||
bindings_.emplace_back("volume"sv, &settings_.volume);
|
||||
|
||||
// RadioState should have initialized default radio parameters before this function;
|
||||
// copy them to settings_ before checking settings .ini file (in case the file doesn't exist
|
||||
// or doesn't include all parameters). Settings in the file can overwrite all, or a subset of parameters.
|
||||
copy_from_radio_model(settings_);
|
||||
|
||||
loaded_ = load_settings(app_name_, bindings_);
|
||||
|
||||
// Only copy to the radio if load was successful.
|
||||
if (loaded_)
|
||||
copy_to_radio_model(settings_);
|
||||
}
|
||||
|
||||
SettingsManager::~SettingsManager() {
|
||||
copy_from_radio_model(settings_);
|
||||
|
||||
save_settings(app_name_, bindings_);
|
||||
}
|
||||
|
||||
} // namespace app_settings
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2022 Arjan Onwezen
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -27,53 +28,167 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "file.hpp"
|
||||
#include "max283x.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace std {
|
||||
class app_settings {
|
||||
public:
|
||||
#define SETTINGS_OK 0 // settings found
|
||||
#define SETTINGS_UNABLE_TO_LOAD -1 // settings (file) not found
|
||||
#define SETTINGS_UNABLE_TO_SAVE -2 // unable to save settings
|
||||
#define SETTINGS_DISABLED -3 // load/save settings disabled in settings
|
||||
// Folder to store app settings, pmem_fileflag, and date_fileflag
|
||||
#define SETTINGS_DIR u"/SETTINGS"
|
||||
|
||||
struct AppSettings {
|
||||
uint32_t baseband_bandwidth;
|
||||
uint32_t channel_bandwidth;
|
||||
uint8_t lna;
|
||||
uint8_t modulation;
|
||||
uint8_t rx_amp;
|
||||
uint32_t rx_frequency;
|
||||
uint32_t sampling_rate;
|
||||
uint8_t tx_amp;
|
||||
uint32_t tx_frequency;
|
||||
uint8_t tx_gain;
|
||||
uint8_t vga;
|
||||
uint32_t step;
|
||||
uint8_t am_config_index;
|
||||
uint8_t nbfm_config_index;
|
||||
uint8_t wfm_config_index;
|
||||
uint8_t squelch;
|
||||
// Bring in the string_view literal.
|
||||
using std::literals::operator""sv;
|
||||
|
||||
#define COMMON_APP_SETTINGS_COUNT 19
|
||||
|
||||
/* Represents a named setting bound to a variable instance. */
|
||||
/* Using void* instead of std::variant, because variant is a pain to dispatch over. */
|
||||
class BoundSetting {
|
||||
/* The type of bound setting. */
|
||||
enum class SettingType : uint8_t {
|
||||
I64,
|
||||
I32,
|
||||
U32,
|
||||
U8,
|
||||
String,
|
||||
Bool,
|
||||
};
|
||||
|
||||
int load(std::string application, AppSettings* settings);
|
||||
int save(std::string application, AppSettings* settings);
|
||||
public:
|
||||
BoundSetting(std::string_view name, int64_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::I64} {}
|
||||
|
||||
BoundSetting(std::string_view name, int32_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::I32} {}
|
||||
|
||||
BoundSetting(std::string_view name, uint32_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::U32} {}
|
||||
|
||||
BoundSetting(std::string_view name, uint8_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::U8} {}
|
||||
|
||||
BoundSetting(std::string_view name, std::string* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::String} {}
|
||||
|
||||
BoundSetting(std::string_view name, bool* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::Bool} {}
|
||||
|
||||
std::string_view name() const { return name_; }
|
||||
void parse(std::string_view value);
|
||||
void write(File& file) const;
|
||||
|
||||
private:
|
||||
#define MAX_FILE_CONTENT_SIZE 1000
|
||||
template <typename T>
|
||||
constexpr auto& as() const {
|
||||
return *reinterpret_cast<T*>(target_);
|
||||
}
|
||||
|
||||
char file_content[MAX_FILE_CONTENT_SIZE] = {};
|
||||
std::string file_path = "";
|
||||
std::string folder = "SETTINGS";
|
||||
int rc = SETTINGS_OK;
|
||||
File settings_file{};
|
||||
long long int setting_value{};
|
||||
std::string_view name_;
|
||||
void* target_;
|
||||
SettingType type_;
|
||||
};
|
||||
|
||||
long long int read_long_long(char* file_content, const char* setting_text);
|
||||
using SettingBindings = std::vector<BoundSetting>;
|
||||
|
||||
}; // class app_settings
|
||||
} // namespace std
|
||||
/* RAII wrapper for Settings that loads/saves to the SD card. */
|
||||
class SettingsStore {
|
||||
public:
|
||||
SettingsStore(std::string_view store_name, SettingBindings bindings);
|
||||
~SettingsStore();
|
||||
|
||||
void reload();
|
||||
void save() const;
|
||||
|
||||
private:
|
||||
std::string_view store_name_;
|
||||
SettingBindings bindings_;
|
||||
};
|
||||
|
||||
bool load_settings(std::string_view store_name, SettingBindings& bindings);
|
||||
bool save_settings(std::string_view store_name, const SettingBindings& bindings);
|
||||
|
||||
namespace app_settings {
|
||||
|
||||
enum class Mode : uint8_t {
|
||||
RX = 0x01,
|
||||
TX = 0x02,
|
||||
RX_TX = 0x03, // Both TX/RX
|
||||
};
|
||||
|
||||
enum class Options {
|
||||
None = 0x0000,
|
||||
|
||||
/* Don't use target frequency from app settings. */
|
||||
UseGlobalTargetFrequency = 0x0001,
|
||||
};
|
||||
|
||||
/* NB: See RX/TX model headers for default values. */
|
||||
struct AppSettings {
|
||||
Mode mode = Mode::RX;
|
||||
Options options = Options::None;
|
||||
uint32_t baseband_bandwidth = max283x::filter::bandwidth_minimum;
|
||||
uint32_t sampling_rate = 3072000; // Good for 48k audio.
|
||||
uint8_t lna = 32;
|
||||
uint8_t vga = 32;
|
||||
uint8_t rx_amp = 0;
|
||||
uint8_t tx_amp = 0;
|
||||
uint8_t tx_gain = 35;
|
||||
uint32_t channel_bandwidth = 1;
|
||||
uint32_t rx_frequency;
|
||||
uint32_t tx_frequency;
|
||||
uint32_t step = 25000;
|
||||
uint8_t modulation = 1; // NFM
|
||||
uint8_t am_config_index = 0;
|
||||
uint8_t nbfm_config_index = 0;
|
||||
uint8_t wfm_config_index = 0;
|
||||
uint8_t squelch = 80;
|
||||
uint8_t volume;
|
||||
// NOTE: update COMMON_APP_SETTINGS_COUNT when adding to this
|
||||
};
|
||||
|
||||
/* Copies common values to the receiver/transmitter models. */
|
||||
void copy_to_radio_model(const AppSettings& settings);
|
||||
|
||||
/* Copies common values from the receiver/transmitter models. */
|
||||
void copy_from_radio_model(AppSettings& settings);
|
||||
|
||||
/* RAII wrapper for automatically loading and saving settings for an app.
|
||||
* "Additional" settings are always loaded, but radio settings are conditionally
|
||||
* saved/loaded if the global app settings options are enabled.
|
||||
* NB: This should be added to a class before any LNA/VGA controls so that
|
||||
* the receiver/transmitter models are set before the control ctors run. */
|
||||
class SettingsManager {
|
||||
public:
|
||||
SettingsManager(std::string_view app_name, Mode mode, Options options = Options::None);
|
||||
SettingsManager(std::string_view app_name, Mode mode, SettingBindings additional_settings);
|
||||
SettingsManager(std::string_view app_name, Mode mode, Options options, SettingBindings additional_settings);
|
||||
~SettingsManager();
|
||||
|
||||
SettingsManager(const SettingsManager&) = delete;
|
||||
SettingsManager& operator=(const SettingsManager&) = delete;
|
||||
SettingsManager(SettingsManager&&) = delete;
|
||||
SettingsManager& operator=(SettingsManager&&) = delete;
|
||||
|
||||
/* True if settings were successfully loaded from file. */
|
||||
bool loaded() const { return loaded_; }
|
||||
|
||||
Mode mode() const { return settings_.mode; }
|
||||
|
||||
AppSettings& raw() { return settings_; }
|
||||
|
||||
private:
|
||||
std::string_view app_name_;
|
||||
AppSettings settings_;
|
||||
SettingBindings bindings_;
|
||||
bool loaded_;
|
||||
};
|
||||
|
||||
} // namespace app_settings
|
||||
|
||||
ENABLE_FLAGS_OPERATORS(app_settings::Mode);
|
||||
ENABLE_FLAGS_OPERATORS(app_settings::Options);
|
||||
|
||||
#endif /*__APP_SETTINGS_H__*/
|
||||
|
||||
@@ -55,12 +55,8 @@ void ACARSLogger::log_raw_data(const acars::Packet& packet, const uint32_t frequ
|
||||
|
||||
namespace ui {
|
||||
|
||||
void ACARSAppView::update_freq(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
portapack::persistent_memory::set_tuned_frequency(f); // Maybe not ?
|
||||
}
|
||||
|
||||
ACARSAppView::ACARSAppView(NavigationView& nav) {
|
||||
ACARSAppView::ACARSAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_acars);
|
||||
|
||||
add_children({&rssi,
|
||||
@@ -72,25 +68,8 @@ ACARSAppView::ACARSAppView(NavigationView& nav) {
|
||||
&check_log,
|
||||
&console});
|
||||
|
||||
receiver_model.set_sampling_rate(2457600);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
update_freq(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
check_log.set_value(logging);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging = v;
|
||||
@@ -132,16 +111,7 @@ void ACARSAppView::on_packet(const acars::Packet& packet) {
|
||||
|
||||
// Log raw data whatever it contains
|
||||
if (logger && logging)
|
||||
logger->log_raw_data(packet, target_frequency());
|
||||
}
|
||||
|
||||
void ACARSAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
receiver_model.set_tuning_frequency(new_value);
|
||||
}
|
||||
|
||||
uint32_t ACARSAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
logger->log_raw_data(packet, receiver_model.target_frequency());
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
#ifndef __ACARS_APP_H__
|
||||
#define __ACARS_APP_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "acars_packet.hpp"
|
||||
@@ -56,6 +58,15 @@ class ACARSAppView : public View {
|
||||
std::string title() const override { return "ACARS (WIP)"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
131550000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
2457600 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_acars", app_settings::Mode::RX};
|
||||
|
||||
bool logging{false};
|
||||
uint32_t packet_counter{0};
|
||||
|
||||
@@ -66,15 +77,13 @@ class ACARSAppView : public View {
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 8},
|
||||
};
|
||||
nav_};
|
||||
Checkbox check_log{
|
||||
{22 * 8, 21},
|
||||
3,
|
||||
@@ -86,15 +95,8 @@ class ACARSAppView : public View {
|
||||
|
||||
std::unique_ptr<ACARSLogger> logger{};
|
||||
|
||||
uint32_t target_frequency_{};
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
|
||||
void on_packet(const acars::Packet& packet);
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::ACARSPacket,
|
||||
[this](Message* const p) {
|
||||
|
||||
@@ -283,8 +283,8 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
|
||||
});
|
||||
|
||||
button_done.on_select = [this](const ui::Button&) {
|
||||
if (this->on_close) {
|
||||
this->on_close();
|
||||
if (on_close) {
|
||||
on_close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -380,33 +380,20 @@ AISAppView::AISAppView(NavigationView& nav)
|
||||
&recent_entry_detail_view,
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_ais", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
target_frequency_ = app_settings.rx_frequency;
|
||||
} else
|
||||
target_frequency_ = initial_target_frequency;
|
||||
|
||||
recent_entry_detail_view.hidden(true);
|
||||
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
receiver_model.enable();
|
||||
|
||||
options_channel.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_frequency_changed(v);
|
||||
receiver_model.set_target_frequency(v);
|
||||
};
|
||||
options_channel.set_by_value(target_frequency());
|
||||
options_channel.set_by_value(receiver_model.target_frequency());
|
||||
|
||||
recent_entries_view.on_select = [this](const AISRecentEntry& entry) {
|
||||
this->on_show_detail(entry);
|
||||
on_show_detail(entry);
|
||||
};
|
||||
recent_entry_detail_view.on_close = [this]() {
|
||||
this->on_show_list();
|
||||
on_show_list();
|
||||
};
|
||||
|
||||
logger = std::make_unique<AISLogger>();
|
||||
@@ -416,12 +403,7 @@ AISAppView::AISAppView(NavigationView& nav)
|
||||
}
|
||||
|
||||
AISAppView::~AISAppView() {
|
||||
// save app settings
|
||||
app_settings.rx_frequency = target_frequency_;
|
||||
settings.save("rx_ais", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias.
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
@@ -465,21 +447,4 @@ void AISAppView::on_show_detail(const AISRecentEntry& entry) {
|
||||
recent_entry_detail_view.focus();
|
||||
}
|
||||
|
||||
void AISAppView::on_frequency_changed(const uint32_t new_target_frequency) {
|
||||
set_target_frequency(new_target_frequency);
|
||||
}
|
||||
|
||||
void AISAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
radio::set_tuning_frequency(tuning_frequency());
|
||||
}
|
||||
|
||||
uint32_t AISAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
}
|
||||
|
||||
uint32_t AISAppView::tuning_frequency() const {
|
||||
return target_frequency() - (sampling_rate / 4);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ais_packet.hpp"
|
||||
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
@@ -154,9 +155,6 @@ class AISAppView : public View {
|
||||
~AISAppView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
// Prevent painting of region covered entirely by a child.
|
||||
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
@@ -164,13 +162,13 @@ class AISAppView : public View {
|
||||
std::string title() const override { return "AIS RX"; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 162025000;
|
||||
static constexpr uint32_t sampling_rate = 2457600;
|
||||
static constexpr uint32_t baseband_bandwidth = 1750000;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
RxRadioState radio_state_{
|
||||
162025000 /* frequency*/,
|
||||
1750000 /* bandwidth */,
|
||||
2457600 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_ais", app_settings::Mode::RX};
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
@@ -225,18 +223,9 @@ class AISAppView : public View {
|
||||
}
|
||||
}};
|
||||
|
||||
uint32_t target_frequency_ = initial_target_frequency;
|
||||
|
||||
void on_packet(const ais::Packet& packet);
|
||||
void on_show_list();
|
||||
void on_show_detail(const AISRecentEntry& entry);
|
||||
|
||||
void on_frequency_changed(const uint32_t new_target_frequency);
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
|
||||
uint32_t tuning_frequency() const;
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -22,29 +22,27 @@
|
||||
|
||||
#include "analog_audio_app.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
using namespace portapack;
|
||||
using namespace tonekey;
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "freqman.hpp"
|
||||
using namespace portapack;
|
||||
using namespace tonekey;
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* AMOptionsView *********************************************************/
|
||||
|
||||
static const Style& style_options_group = Styles::bg_blue;
|
||||
|
||||
AMOptionsView::AMOptionsView(
|
||||
const Rect parent_rect,
|
||||
const Style* const style)
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
@@ -54,8 +52,8 @@ AMOptionsView::AMOptionsView(
|
||||
});
|
||||
|
||||
freqman_set_bandwidth_option(AM_MODULATION, options_config); // adding the common message from freqman.cpp to the options_config
|
||||
options_config.set_selected_index(receiver_model.am_configuration());
|
||||
options_config.on_change = [this](size_t n, OptionsField::value_t) {
|
||||
options_config.set_by_value(receiver_model.am_configuration());
|
||||
options_config.on_change = [this](size_t, OptionsField::value_t n) {
|
||||
receiver_model.set_am_configuration(n);
|
||||
};
|
||||
}
|
||||
@@ -63,8 +61,8 @@ AMOptionsView::AMOptionsView(
|
||||
/* NBFMOptionsView *******************************************************/
|
||||
|
||||
NBFMOptionsView::NBFMOptionsView(
|
||||
const Rect parent_rect,
|
||||
const Style* const style)
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
@@ -74,8 +72,8 @@ NBFMOptionsView::NBFMOptionsView(
|
||||
&field_squelch});
|
||||
|
||||
freqman_set_bandwidth_option(NFM_MODULATION, options_config); // adding the common message from freqman.cpp to the options_config
|
||||
options_config.set_selected_index(receiver_model.nbfm_configuration());
|
||||
options_config.on_change = [this](size_t n, OptionsField::value_t) {
|
||||
options_config.set_by_value(receiver_model.nbfm_configuration());
|
||||
options_config.on_change = [this](size_t, OptionsField::value_t n) {
|
||||
receiver_model.set_nbfm_configuration(n);
|
||||
};
|
||||
|
||||
@@ -88,8 +86,8 @@ NBFMOptionsView::NBFMOptionsView(
|
||||
/* WFMOptionsView *******************************************************/
|
||||
|
||||
WFMOptionsView::WFMOptionsView(
|
||||
const Rect parent_rect,
|
||||
const Style* const style)
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
@@ -99,8 +97,8 @@ WFMOptionsView::WFMOptionsView(
|
||||
});
|
||||
|
||||
freqman_set_bandwidth_option(WFM_MODULATION, options_config); // adding the common message from freqman.cpp to the options_config
|
||||
options_config.set_selected_index(receiver_model.wfm_configuration());
|
||||
options_config.on_change = [this](size_t n, OptionsField::value_t) {
|
||||
options_config.set_by_value(receiver_model.wfm_configuration());
|
||||
options_config.on_change = [this](size_t, OptionsField::value_t n) {
|
||||
receiver_model.set_wfm_configuration(n);
|
||||
};
|
||||
}
|
||||
@@ -109,8 +107,8 @@ WFMOptionsView::WFMOptionsView(
|
||||
|
||||
SPECOptionsView::SPECOptionsView(
|
||||
AnalogAudioView* view,
|
||||
const Rect parent_rect,
|
||||
const Style* const style)
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
@@ -135,6 +133,9 @@ SPECOptionsView::SPECOptionsView(
|
||||
AnalogAudioView::AnalogAudioView(
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
// A baseband image _must_ be running before add waterfall view.
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum);
|
||||
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&audio,
|
||||
@@ -147,35 +148,9 @@ AnalogAudioView::AnalogAudioView(
|
||||
&record_view,
|
||||
&waterfall});
|
||||
|
||||
// Set on_change before initialising the field
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
};
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_audio", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
receiver_model.set_rf_amp(app_settings.rx_amp);
|
||||
// field_frequency.set_value(app_settings.rx_frequency);
|
||||
receiver_model.set_configuration_without_init(static_cast<ReceiverModel::Mode>(app_settings.modulation), app_settings.step, app_settings.am_config_index, app_settings.nbfm_config_index, app_settings.wfm_config_index, app_settings.squelch);
|
||||
}
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
|
||||
// Filename Datetime and Frequency
|
||||
record_view.set_filename_date_frequency(true);
|
||||
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency.on_show_options = [this]() {
|
||||
this->on_show_options_frequency();
|
||||
};
|
||||
@@ -188,9 +163,12 @@ AnalogAudioView::AnalogAudioView(
|
||||
this->on_show_options_rf_gain();
|
||||
};
|
||||
|
||||
const auto modulation = receiver_model.modulation();
|
||||
options_modulation.set_by_value(toUType(modulation));
|
||||
auto modulation = receiver_model.modulation();
|
||||
// This app doesn't handle "Capture" mode.
|
||||
if (modulation > ReceiverModel::Mode::SpectrumAnalysis)
|
||||
modulation = ReceiverModel::Mode::SpectrumAnalysis;
|
||||
|
||||
options_modulation.set_by_value(toUType(modulation));
|
||||
options_modulation.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_modulation_changed(static_cast<ReceiverModel::Mode>(v));
|
||||
};
|
||||
@@ -198,23 +176,28 @@ AnalogAudioView::AnalogAudioView(
|
||||
this->on_show_options_modulation();
|
||||
};
|
||||
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
field_volume.on_change = [this](int32_t v) {
|
||||
this->on_headphone_volume_changed(v);
|
||||
};
|
||||
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
waterfall.on_select = [this](int32_t offset) {
|
||||
field_frequency.set_value(receiver_model.tuning_frequency() + offset);
|
||||
field_frequency.set_value(receiver_model.target_frequency() + offset);
|
||||
};
|
||||
|
||||
audio::output::start();
|
||||
|
||||
update_modulation(static_cast<ReceiverModel::Mode>(modulation));
|
||||
on_modulation_changed(static_cast<ReceiverModel::Mode>(modulation));
|
||||
// This call starts the correct baseband image to run
|
||||
// and sets the radio up as necessary for the given modulation.
|
||||
on_modulation_changed(modulation);
|
||||
}
|
||||
|
||||
AnalogAudioView::AnalogAudioView(
|
||||
NavigationView& nav,
|
||||
ReceiverModel::settings_t override)
|
||||
: AnalogAudioView(nav) {
|
||||
// TODO: Which other settings make sense to override?
|
||||
on_frequency_step_changed(override.frequency_step);
|
||||
options_modulation.set_by_value(toUType(override.mode));
|
||||
}
|
||||
|
||||
size_t AnalogAudioView::get_spec_bw_index() {
|
||||
@@ -241,40 +224,15 @@ void AnalogAudioView::set_spec_trigger(uint16_t trigger) {
|
||||
}
|
||||
|
||||
AnalogAudioView::~AnalogAudioView() {
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
app_settings.lna = receiver_model.lna();
|
||||
app_settings.vga = receiver_model.vga();
|
||||
app_settings.rx_amp = receiver_model.rf_amp();
|
||||
app_settings.step = receiver_model.frequency_step();
|
||||
app_settings.modulation = (uint8_t)receiver_model.modulation();
|
||||
app_settings.am_config_index = receiver_model.am_configuration();
|
||||
app_settings.nbfm_config_index = receiver_model.nbfm_configuration();
|
||||
app_settings.wfm_config_index = receiver_model.wfm_configuration();
|
||||
app_settings.squelch = receiver_model.squelch_level();
|
||||
settings.save("rx_audio", &app_settings);
|
||||
|
||||
// TODO: Manipulating audio codec here, and in ui_receiver.cpp. Good to do
|
||||
// both?
|
||||
audio::output::stop();
|
||||
|
||||
receiver_model.set_sampling_rate(3072000); // Just a hack to avoid hanging other apps if the last modulation was SPEC
|
||||
receiver_model.disable();
|
||||
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_hide() {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
|
||||
void AnalogAudioView::set_parent_rect(const Rect new_parent_rect) {
|
||||
void AnalogAudioView::set_parent_rect(Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
@@ -282,21 +240,19 @@ void AnalogAudioView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_baseband_bandwidth_changed(uint32_t bandwidth_hz) {
|
||||
receiver_model.set_baseband_bandwidth(bandwidth_hz);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_modulation_changed(const ReceiverModel::Mode modulation) {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
void AnalogAudioView::on_modulation_changed(ReceiverModel::Mode modulation) {
|
||||
// This app doesn't know what to do with "Capture" mode.
|
||||
if (modulation > ReceiverModel::Mode::SpectrumAnalysis)
|
||||
modulation = ReceiverModel::Mode::SpectrumAnalysis;
|
||||
|
||||
baseband::spectrum_streaming_stop();
|
||||
update_modulation(modulation);
|
||||
on_show_options_modulation();
|
||||
waterfall.on_show();
|
||||
baseband::spectrum_streaming_start();
|
||||
}
|
||||
|
||||
void AnalogAudioView::remove_options_widget() {
|
||||
@@ -348,7 +304,7 @@ void AnalogAudioView::on_show_options_rf_gain() {
|
||||
void AnalogAudioView::on_show_options_modulation() {
|
||||
std::unique_ptr<Widget> widget;
|
||||
|
||||
const auto modulation = static_cast<ReceiverModel::Mode>(receiver_model.modulation());
|
||||
const auto modulation = receiver_model.modulation();
|
||||
switch (modulation) {
|
||||
case ReceiverModel::Mode::AMAudio:
|
||||
widget = std::make_unique<AMOptionsView>(options_view_rect, &style_options_group);
|
||||
@@ -375,6 +331,7 @@ void AnalogAudioView::on_show_options_modulation() {
|
||||
break;
|
||||
|
||||
default:
|
||||
chDbgPanic("Unhandled Mode");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -391,12 +348,7 @@ void AnalogAudioView::on_reference_ppm_correction_changed(int32_t v) {
|
||||
persistent_memory::set_correction_ppb(v * 1000);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_headphone_volume_changed(int32_t v) {
|
||||
const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
receiver_model.set_headphone_volume(new_volume);
|
||||
}
|
||||
|
||||
void AnalogAudioView::update_modulation(const ReceiverModel::Mode modulation) {
|
||||
void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) {
|
||||
audio::output::mute();
|
||||
record_view.stop();
|
||||
|
||||
@@ -417,7 +369,8 @@ void AnalogAudioView::update_modulation(const ReceiverModel::Mode modulation) {
|
||||
image_tag = portapack::spi_flash::image_tag_wideband_spectrum;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
chDbgPanic("Unhandled Mode");
|
||||
break;
|
||||
}
|
||||
|
||||
baseband::run_image(image_tag);
|
||||
@@ -456,25 +409,8 @@ void AnalogAudioView::update_modulation(const ReceiverModel::Mode modulation) {
|
||||
}
|
||||
}
|
||||
|
||||
void AnalogAudioView::handle_coded_squelch(const uint32_t value) {
|
||||
float diff, min_diff = value;
|
||||
size_t min_idx{0};
|
||||
size_t c;
|
||||
|
||||
// Find nearest match
|
||||
for (c = 0; c < tone_keys.size(); c++) {
|
||||
diff = abs(((float)value / 100.0) - tone_keys[c].second);
|
||||
if (diff < min_diff) {
|
||||
min_idx = c;
|
||||
min_diff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary confidence threshold
|
||||
if (min_diff < 40)
|
||||
text_ctcss.set("CTCSS " + tone_keys[min_idx].first);
|
||||
else
|
||||
text_ctcss.set("???");
|
||||
void AnalogAudioView::handle_coded_squelch(uint32_t value) {
|
||||
text_ctcss.set(tone_key_string_by_value(value, text_ctcss.parent_rect().width() / 8));
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -26,23 +26,19 @@
|
||||
#include "receiver_model.hpp"
|
||||
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "tone_key.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
constexpr Style style_options_group{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::blue(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
class AMOptionsView : public View {
|
||||
public:
|
||||
AMOptionsView(const Rect parent_rect, const Style* const style);
|
||||
AMOptionsView(Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config{
|
||||
@@ -52,15 +48,15 @@ class AMOptionsView : public View {
|
||||
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
5,
|
||||
6, // Max option length
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
};
|
||||
|
||||
class NBFMOptionsView : public View {
|
||||
public:
|
||||
NBFMOptionsView(const Rect parent_rect, const Style* const style);
|
||||
NBFMOptionsView(Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config{
|
||||
@@ -69,16 +65,16 @@ class NBFMOptionsView : public View {
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
4,
|
||||
3, // Max option length
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
|
||||
Text text_squelch{
|
||||
{9 * 8, 0 * 16, 8 * 8, 1 * 16},
|
||||
{7 * 8, 0 * 16, 8 * 8, 1 * 16},
|
||||
"SQ /99"};
|
||||
NumberField field_squelch{
|
||||
{12 * 8, 0 * 16},
|
||||
{10 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
@@ -88,7 +84,7 @@ class NBFMOptionsView : public View {
|
||||
|
||||
class WFMOptionsView : public View {
|
||||
public:
|
||||
WFMOptionsView(const Rect parent_rect, const Style* const style);
|
||||
WFMOptionsView(Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config{
|
||||
@@ -97,9 +93,9 @@ class WFMOptionsView : public View {
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
4,
|
||||
4, // Max option length
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
};
|
||||
|
||||
@@ -107,7 +103,7 @@ class AnalogAudioView;
|
||||
|
||||
class SPECOptionsView : public View {
|
||||
public:
|
||||
SPECOptionsView(AnalogAudioView* view, const Rect parent_rect, const Style* const style);
|
||||
SPECOptionsView(AnalogAudioView* view, Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config{
|
||||
@@ -142,12 +138,10 @@ class SPECOptionsView : public View {
|
||||
class AnalogAudioView : public View {
|
||||
public:
|
||||
AnalogAudioView(NavigationView& nav);
|
||||
AnalogAudioView(NavigationView& nav, ReceiverModel::settings_t override);
|
||||
~AnalogAudioView();
|
||||
|
||||
void on_hide() override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void set_parent_rect(Rect new_parent_rect) override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Audio RX"; };
|
||||
@@ -161,9 +155,11 @@ class AnalogAudioView : public View {
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_audio", app_settings::Mode::RX,
|
||||
app_settings::Options::UseGlobalTargetFrequency};
|
||||
|
||||
const Rect options_view_rect{0 * 8, 1 * 16, 30 * 8, 1 * 16};
|
||||
const Rect nbfm_view_rect{0 * 8, 1 * 16, 18 * 8, 1 * 16};
|
||||
@@ -172,24 +168,18 @@ class AnalogAudioView : public View {
|
||||
uint32_t spec_bw = 20000000;
|
||||
uint16_t spec_trigger = 63;
|
||||
|
||||
NavigationView& nav_;
|
||||
// bool exit_on_squelch { false };
|
||||
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Audio audio{
|
||||
{21 * 8, 10, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 10, 6 * 8, 4}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
RxFrequencyField field_frequency{
|
||||
{5 * 8, 0 * 16},
|
||||
};
|
||||
nav_};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
@@ -207,16 +197,11 @@ class AnalogAudioView : public View {
|
||||
{"SPEC", toUType(ReceiverModel::Mode::SpectrumAnalysis)},
|
||||
}};
|
||||
|
||||
NumberField field_volume{
|
||||
{28 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
Text text_ctcss{
|
||||
{19 * 8, 1 * 16, 11 * 8, 1 * 16},
|
||||
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
std::unique_ptr<Widget> options_widget{};
|
||||
@@ -229,38 +214,26 @@ class AnalogAudioView : public View {
|
||||
4096,
|
||||
4};
|
||||
|
||||
spectrum::WaterfallWidget waterfall{true};
|
||||
spectrum::WaterfallView waterfall{true};
|
||||
|
||||
void on_tuning_frequency_changed(rf::Frequency f);
|
||||
void on_baseband_bandwidth_changed(uint32_t bandwidth_hz);
|
||||
void on_modulation_changed(const ReceiverModel::Mode modulation);
|
||||
void on_modulation_changed(ReceiverModel::Mode modulation);
|
||||
void on_show_options_frequency();
|
||||
void on_show_options_rf_gain();
|
||||
void on_show_options_modulation();
|
||||
void on_frequency_step_changed(rf::Frequency f);
|
||||
void on_reference_ppm_correction_changed(int32_t v);
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
void on_edit_frequency();
|
||||
|
||||
void remove_options_widget();
|
||||
void set_options_widget(std::unique_ptr<Widget> new_widget);
|
||||
|
||||
void update_modulation(const ReceiverModel::Mode modulation);
|
||||
void update_modulation(ReceiverModel::Mode modulation);
|
||||
|
||||
// void squelched();
|
||||
void handle_coded_squelch(const uint32_t value);
|
||||
|
||||
/*MessageHandlerRegistration message_handler_squelch_signal {
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->squelched();
|
||||
}
|
||||
};*/
|
||||
void handle_coded_squelch(uint32_t value);
|
||||
|
||||
MessageHandlerRegistration message_handler_coded_squelch{
|
||||
Message::ID::CodedSquelch,
|
||||
[this](const Message* const p) {
|
||||
[this](const Message* p) {
|
||||
const auto message = *reinterpret_cast<const CodedSquelchMessage*>(p);
|
||||
this->handle_coded_squelch(message.value);
|
||||
}};
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 "ble_comm_app.hpp"
|
||||
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_text.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
|
||||
void BLECommLogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
void BLECommView::focus() {
|
||||
options_channel.focus();
|
||||
}
|
||||
|
||||
BLECommView::BLECommView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_channel,
|
||||
&field_frequency,
|
||||
&label_send_adv,
|
||||
&button_send_adv,
|
||||
&check_log,
|
||||
&label_packets_sent,
|
||||
&text_packets_sent,
|
||||
&console});
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
button_send_adv.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
check_log.set_value(logging);
|
||||
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
str_log = "";
|
||||
logging = v;
|
||||
|
||||
if (logger && logging)
|
||||
logger->append(LOG_ROOT_DIR "/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t, int32_t i) {
|
||||
// If we selected Auto don't do anything and Auto will handle changing.
|
||||
if (i == 40) {
|
||||
auto_channel = true;
|
||||
return;
|
||||
} else {
|
||||
auto_channel = false;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(i));
|
||||
channel_number_rx = i;
|
||||
channel_number_tx = i;
|
||||
};
|
||||
|
||||
options_channel.set_selected_index(3, true);
|
||||
|
||||
logger = std::make_unique<BLECommLogger>();
|
||||
|
||||
// Generate new random Mac Address upon each new startup.
|
||||
generateRandomMacAddress(randomMac);
|
||||
|
||||
// Setup Initial Advertise Packet.
|
||||
advertisePacket = build_adv_packet();
|
||||
}
|
||||
|
||||
void BLECommView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
console.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLECommView::~BLECommView() {
|
||||
receiver_model.disable();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
bool BLECommView::in_tx_mode() const {
|
||||
return (bool)is_running_tx;
|
||||
}
|
||||
|
||||
void BLECommView::toggle() {
|
||||
if (in_tx_mode()) {
|
||||
sendAdvertisement(false);
|
||||
} else {
|
||||
sendAdvertisement(true);
|
||||
}
|
||||
}
|
||||
|
||||
BLETxPacket BLECommView::build_adv_packet() {
|
||||
BLETxPacket bleTxPacket;
|
||||
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
|
||||
|
||||
std::string dataString = "11094861636b524620506f7274617061636b";
|
||||
|
||||
strncpy(bleTxPacket.macAddress, randomMac, 12);
|
||||
strncpy(bleTxPacket.advertisementData, dataString.c_str(), dataString.length());
|
||||
|
||||
// Duty cycle of 40% per 100ms advertisment periods.
|
||||
strncpy(bleTxPacket.packetCount, "80", 3);
|
||||
bleTxPacket.packet_count = 80;
|
||||
|
||||
bleTxPacket.packetType = PKT_TYPE_DISCOVERY;
|
||||
|
||||
return bleTxPacket;
|
||||
}
|
||||
|
||||
void BLECommView::sendAdvertisement(bool enable) {
|
||||
if (enable) {
|
||||
startTx(advertisePacket);
|
||||
is_adv = true;
|
||||
} else {
|
||||
is_adv = false;
|
||||
stopTx();
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::startTx(BLETxPacket packetToSend) {
|
||||
int randomChannel = channel_number_tx;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
randomChannel = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
|
||||
}
|
||||
|
||||
if (!in_tx_mode()) {
|
||||
switch_rx_tx(false);
|
||||
|
||||
currentPacket = packetToSend;
|
||||
packet_counter = currentPacket.packet_count;
|
||||
|
||||
button_send_adv.set_bitmap(&bitmap_stop);
|
||||
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
|
||||
transmitter_model.set_tx_gain(47);
|
||||
transmitter_model.enable();
|
||||
|
||||
is_running_tx = true;
|
||||
} else {
|
||||
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
|
||||
}
|
||||
|
||||
if ((packet_counter % 10) == 0) {
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
}
|
||||
|
||||
is_sending = true;
|
||||
|
||||
packet_counter--;
|
||||
}
|
||||
|
||||
void BLECommView::stopTx() {
|
||||
button_send_adv.set_bitmap(&bitmap_play);
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
|
||||
switch_rx_tx(true);
|
||||
|
||||
baseband::set_btlerx(channel_number_rx);
|
||||
receiver_model.enable();
|
||||
|
||||
is_running_tx = false;
|
||||
}
|
||||
|
||||
void BLECommView::switch_rx_tx(bool inRxMode) {
|
||||
if (inRxMode) {
|
||||
// Start Rx
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
} else {
|
||||
// Start Tx
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::on_data(BlePacketData* packet) {
|
||||
parse_received_packet(packet, (ADV_PDU_TYPE)packet->type);
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BLECommView::on_timer() {
|
||||
// Send advertise burst only once every 100ms
|
||||
if (++timer_counter == timer_period) {
|
||||
timer_counter = 0;
|
||||
|
||||
if (!is_adv) {
|
||||
sendAdvertisement(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::on_tx_progress(const bool done) {
|
||||
if (done) {
|
||||
if (in_tx_mode()) {
|
||||
is_sending = false;
|
||||
|
||||
if (packet_counter == 0) {
|
||||
if (is_adv) {
|
||||
sendAdvertisement(false);
|
||||
} else {
|
||||
stopTx();
|
||||
}
|
||||
} else {
|
||||
startTx(currentPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::parse_received_packet(const BlePacketData* packet, ADV_PDU_TYPE pdu_type) {
|
||||
std::string data_string;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
data_string += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
receivedPacket.dbValue = packet->max_dB;
|
||||
receivedPacket.timestamp = to_string_timestamp(rtc_time::now());
|
||||
receivedPacket.dataString = data_string;
|
||||
|
||||
receivedPacket.packetData.type = packet->type;
|
||||
receivedPacket.packetData.size = packet->size;
|
||||
receivedPacket.packetData.dataLen = packet->dataLen;
|
||||
|
||||
receivedPacket.packetData.macAddress[0] = packet->macAddress[0];
|
||||
receivedPacket.packetData.macAddress[1] = packet->macAddress[1];
|
||||
receivedPacket.packetData.macAddress[2] = packet->macAddress[2];
|
||||
receivedPacket.packetData.macAddress[3] = packet->macAddress[3];
|
||||
receivedPacket.packetData.macAddress[4] = packet->macAddress[4];
|
||||
receivedPacket.packetData.macAddress[5] = packet->macAddress[5];
|
||||
|
||||
receivedPacket.numHits++;
|
||||
|
||||
for (int i = 0; i < packet->dataLen; i++) {
|
||||
receivedPacket.packetData.data[i] = packet->data[i];
|
||||
}
|
||||
|
||||
if (pdu_type == SCAN_REQ || pdu_type == CONNECT_REQ) {
|
||||
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
|
||||
|
||||
std::reverse(directed_mac_data->A1, directed_mac_data->A1 + 6);
|
||||
console.clear(true);
|
||||
std::string str_console = "";
|
||||
std::string pduTypeStr = "";
|
||||
|
||||
if (pdu_type == SCAN_REQ) {
|
||||
pduTypeStr += "SCAN_REQ";
|
||||
} else if (pdu_type == CONNECT_REQ) {
|
||||
pduTypeStr += "CONNECT_REQ";
|
||||
}
|
||||
|
||||
str_console += "PACKET TYPE:" + pduTypeStr + "\n";
|
||||
str_console += "MY MAC:" + to_string_formatted_mac_address(randomMac) + "\n";
|
||||
str_console += "SCAN MAC:" + to_string_mac_address(directed_mac_data->A1, 6, false) + "\n";
|
||||
console.write(str_console);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 __BLE_COMM_APP_H__
|
||||
#define __BLE_COMM_APP_H__
|
||||
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_tx_app.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
class BLECommLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
class BLECommView : public View {
|
||||
public:
|
||||
BLECommView(NavigationView& nav);
|
||||
~BLECommView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BLE Comm"; };
|
||||
|
||||
private:
|
||||
BLETxPacket build_adv_packet();
|
||||
void switch_rx_tx(bool inRxMode);
|
||||
void startTx(BLETxPacket packetToSend);
|
||||
void stopTx();
|
||||
void toggle();
|
||||
bool in_tx_mode() const;
|
||||
void on_timer();
|
||||
void on_data(BlePacketData* packetData);
|
||||
void on_tx_progress(const bool done);
|
||||
void parse_received_packet(const BlePacketData* packet, ADV_PDU_TYPE pdu_type);
|
||||
void sendAdvertisement(bool enable);
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_rx_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
|
||||
TxRadioState radio_state_tx_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */
|
||||
};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"BLE Comm Tx", app_settings::Mode::RX_TX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
uint8_t channel_number_tx = 37;
|
||||
uint8_t channel_number_rx = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
char randomMac[13] = "010203040506";
|
||||
|
||||
bool is_running_tx = false;
|
||||
bool is_sending = false;
|
||||
bool is_adv = false;
|
||||
|
||||
int16_t timer_period{6}; // Delay each packet by 16ms.
|
||||
int16_t timer_counter = 0;
|
||||
int16_t timer_rx_counter = 0;
|
||||
int16_t timer_rx_period{12}; // Poll Rx for at least 200ms. (TBD)
|
||||
|
||||
uint32_t packet_counter{0};
|
||||
BLETxPacket advertisePacket{};
|
||||
BLETxPacket currentPacket{};
|
||||
BleRecentEntry receivedPacket{};
|
||||
|
||||
static constexpr auto header_height = 5 * 16;
|
||||
|
||||
OptionsField options_channel{
|
||||
{0 * 8, 0 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{6 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Labels label_send_adv{
|
||||
{{0 * 8, 2 * 8}, "Send Advertisement:", Color::light_grey()}};
|
||||
|
||||
ImageButton button_send_adv{
|
||||
{21 * 8, 1 * 16, 10 * 8, 2 * 16},
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
|
||||
Checkbox check_log{
|
||||
{24 * 8, 2 * 8},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Labels label_packets_sent{
|
||||
{{0 * 8, 4 * 8}, "Packets Left:", Color::light_grey()}};
|
||||
|
||||
Text text_packets_sent{
|
||||
{13 * 8, 2 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
|
||||
std::string str_log{""};
|
||||
bool logging{false};
|
||||
|
||||
std::unique_ptr<BLECommLogger> logger{};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::BlePacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const BLEPacketMessage*>(p);
|
||||
this->on_data(message->packet);
|
||||
}};
|
||||
|
||||
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.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -0,0 +1,936 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 "ble_rx_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
void BLELogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
std::string pad_string_with_spaces(int snakes) {
|
||||
std::string paddedStr(snakes, ' ');
|
||||
return paddedStr;
|
||||
}
|
||||
|
||||
uint64_t copy_mac_address_to_uint64(const uint8_t* macAddress) {
|
||||
uint64_t result = 0;
|
||||
|
||||
// Copy each byte of the MAC address to the corresponding byte in the uint64_t.
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
result |= static_cast<uint64_t>(macAddress[i]) << ((5 - i) * 8);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void reverse_byte_array(uint8_t* arr, int length) {
|
||||
int start = 0;
|
||||
int end = length - 1;
|
||||
|
||||
while (start < end) {
|
||||
// Swap elements at start and end
|
||||
uint8_t temp = arr[start];
|
||||
arr[start] = arr[end];
|
||||
arr[end] = temp;
|
||||
|
||||
// Move the indices towards the center
|
||||
start++;
|
||||
end--;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
std::string pdu_type_to_string(ADV_PDU_TYPE type) {
|
||||
std::string pduTypeStr = "";
|
||||
|
||||
switch (type) {
|
||||
case ADV_IND:
|
||||
pduTypeStr += "ADV_IND";
|
||||
break;
|
||||
case ADV_DIRECT_IND:
|
||||
pduTypeStr += "ADV_DIRECT_IND";
|
||||
break;
|
||||
case ADV_NONCONN_IND:
|
||||
pduTypeStr += "ADV_NONCONN_IND";
|
||||
break;
|
||||
case SCAN_REQ:
|
||||
pduTypeStr += "SCAN_REQ";
|
||||
break;
|
||||
case SCAN_RSP:
|
||||
pduTypeStr += "SCAN_RSP";
|
||||
break;
|
||||
case CONNECT_REQ:
|
||||
pduTypeStr += "CONNECT_REQ";
|
||||
break;
|
||||
case ADV_SCAN_IND:
|
||||
pduTypeStr += "ADV_SCAN_IND";
|
||||
break;
|
||||
case RESERVED0:
|
||||
case RESERVED1:
|
||||
case RESERVED2:
|
||||
case RESERVED3:
|
||||
case RESERVED4:
|
||||
case RESERVED5:
|
||||
case RESERVED6:
|
||||
case RESERVED7:
|
||||
case RESERVED8:
|
||||
pduTypeStr += "RESERVED";
|
||||
break;
|
||||
default:
|
||||
pduTypeStr += "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
|
||||
return pduTypeStr;
|
||||
}
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<BleRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
std::string line{};
|
||||
line.reserve(30);
|
||||
|
||||
if (!entry.nameString.empty() && entry.include_name) {
|
||||
line = entry.nameString;
|
||||
|
||||
if (line.length() < 17) {
|
||||
line += pad_string_with_spaces(17 - line.length());
|
||||
} else {
|
||||
line = truncate(line, 17);
|
||||
}
|
||||
} else {
|
||||
line = to_string_mac_address(entry.packetData.macAddress, 6, false);
|
||||
}
|
||||
|
||||
// Pushing single digit values down right justified.
|
||||
std::string hitsStr = to_string_dec_int(entry.numHits);
|
||||
int hitsDigits = hitsStr.length();
|
||||
uint8_t hits_spacing = 8 - hitsDigits;
|
||||
|
||||
// Pushing single digit values down right justified.
|
||||
std::string dbStr = to_string_dec_int(entry.dbValue);
|
||||
int dbDigits = dbStr.length();
|
||||
uint8_t db_spacing = 5 - dbDigits;
|
||||
|
||||
line += pad_string_with_spaces(hits_spacing) + hitsStr;
|
||||
|
||||
line += pad_string_with_spaces(db_spacing) + dbStr;
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&button_done,
|
||||
&button_send,
|
||||
&button_save,
|
||||
&label_mac_address,
|
||||
&text_mac_address,
|
||||
&label_pdu_type,
|
||||
&text_pdu_type,
|
||||
&labels});
|
||||
|
||||
text_mac_address.set(to_string_mac_address(entry.packetData.macAddress, 6, false));
|
||||
text_pdu_type.set(pdu_type_to_string(entry.pduType));
|
||||
|
||||
button_done.on_select = [&nav](const ui::Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_send.on_select = [this, &nav](const ui::Button&) {
|
||||
auto packetToSend = build_packet();
|
||||
nav.set_on_pop([packetToSend, &nav]() {
|
||||
nav.replace<BLETxView>(packetToSend);
|
||||
});
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_save.on_select = [this, &nav](const ui::Button&) {
|
||||
auto packetToSave = build_packet();
|
||||
|
||||
packetFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
packetFileBuffer,
|
||||
64,
|
||||
[this, packetToSave](std::string& buffer) {
|
||||
on_save_file(buffer, packetToSave);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::on_save_file(const std::string value, BLETxPacket packetToSave) {
|
||||
ensure_directory(packet_save_path);
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path, packetToSave);
|
||||
}
|
||||
|
||||
bool BleRecentEntryDetailView::saveFile(const std::filesystem::path& path, BLETxPacket packetToSave) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
std::string macAddressStr = packetToSave.macAddress;
|
||||
std::string advertisementDataStr = packetToSave.advertisementData;
|
||||
std::string packetCountStr = packetToSave.packetCount;
|
||||
|
||||
std::string packetString = macAddressStr + ' ' + advertisementDataStr + ' ' + packetCountStr;
|
||||
|
||||
f.write(packetString.c_str(), packetString.length());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::update_data() {
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
Rect BleRecentEntryDetailView::draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value) {
|
||||
const int label_length_max = 4;
|
||||
|
||||
painter.draw_string(Point{draw_rect.left(), draw_rect.top()}, style, label);
|
||||
painter.draw_string(Point{draw_rect.left() + (label_length_max + 1) * 8, draw_rect.top()}, style, value);
|
||||
|
||||
return {draw_rect.left(), draw_rect.top() + draw_rect.height(), draw_rect.width(), draw_rect.height()};
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::paint(Painter& painter) {
|
||||
View::paint(painter);
|
||||
|
||||
const auto s = style();
|
||||
const auto rect = screen_rect();
|
||||
|
||||
auto field_rect = Rect{rect.left(), rect.top() + 64, rect.width(), 16};
|
||||
|
||||
uint8_t type[total_data_lines];
|
||||
uint8_t length[total_data_lines];
|
||||
uint8_t data[total_data_lines][40];
|
||||
|
||||
int currentByte = 0;
|
||||
int currentPacket = 0;
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int k = 0;
|
||||
|
||||
switch (entry_.pduType) {
|
||||
case ADV_IND:
|
||||
case ADV_NONCONN_IND:
|
||||
case SCAN_RSP:
|
||||
case ADV_SCAN_IND: {
|
||||
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry_.packetData.data;
|
||||
|
||||
for (currentByte = 0; (currentByte < entry_.packetData.dataLen) && (currentPacket < total_data_lines);) {
|
||||
length[currentPacket] = advertiseData->Data[currentByte++];
|
||||
type[currentPacket] = advertiseData->Data[currentByte++];
|
||||
|
||||
// Subtract 1 because type is part of the length.
|
||||
for (i = 0; i < length[currentPacket] - 1; i++) {
|
||||
data[currentPacket][i] = advertiseData->Data[currentByte++];
|
||||
}
|
||||
|
||||
currentPacket++;
|
||||
}
|
||||
|
||||
for (i = 0; i < currentPacket; i++) {
|
||||
uint8_t number_data_lines = ceil((float)(length[i] - 1) / 10.0);
|
||||
uint8_t current_line = 0;
|
||||
std::array<std::string, total_data_lines> data_strings{};
|
||||
|
||||
for (j = 0; (j < (number_data_lines * 10)) && (j < length[i] - 1); j++) {
|
||||
if ((j / 10) != current_line) {
|
||||
current_line++;
|
||||
}
|
||||
|
||||
data_strings[current_line] += to_string_hex(data[i][j], 2);
|
||||
}
|
||||
|
||||
// Read the type back to the total length.
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(length[i]), to_string_hex(type[i]) + pad_string_with_spaces(3) + data_strings[0]);
|
||||
|
||||
if (number_data_lines > 1) {
|
||||
for (k = 1; k < number_data_lines; k++) {
|
||||
if (!data_strings[k].empty()) {
|
||||
field_rect = draw_field(painter, field_rect, s, "", pad_string_with_spaces(5) + data_strings[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case ADV_DIRECT_IND:
|
||||
case SCAN_REQ: {
|
||||
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)entry_.packetData.data;
|
||||
uint8_t type = 0xFF;
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(entry_.packetData.dataLen), to_string_hex(type) + pad_string_with_spaces(3) + to_string_mac_address(directed_mac_data->A1, 6, false));
|
||||
} break;
|
||||
|
||||
case CONNECT_REQ:
|
||||
default: {
|
||||
uint8_t type = 0xFF;
|
||||
|
||||
// TODO: Display Connect Request Information. For right now just printing full hex data.
|
||||
// This struct will eventually be used to break apart containing data of Connect Request.
|
||||
// ADV_PDU_PAYLOAD_TYPE_5 * connect_req = (ADV_PDU_PAYLOAD_TYPE_5 *)entry_.packetData.data;
|
||||
|
||||
for (currentByte = 0; (currentByte < entry_.packetData.dataLen); currentByte++) {
|
||||
data[0][currentByte] = entry_.packetData.data[currentByte];
|
||||
}
|
||||
|
||||
uint8_t number_data_lines = ceil((float)entry_.packetData.dataLen / 10.0);
|
||||
uint8_t current_line = 0;
|
||||
std::array<std::string, total_data_lines> data_strings{};
|
||||
|
||||
for (j = 0; (j < (number_data_lines * 10)) && (j < entry_.packetData.dataLen); j++) {
|
||||
if ((j / 10) != current_line) {
|
||||
current_line++;
|
||||
}
|
||||
|
||||
data_strings[current_line] += to_string_hex(data[0][j], 2);
|
||||
}
|
||||
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(entry_.packetData.dataLen), to_string_hex(type) + pad_string_with_spaces(3) + data_strings[0]);
|
||||
|
||||
if (number_data_lines > 1) {
|
||||
for (k = 1; k < number_data_lines; k++) {
|
||||
if (!data_strings[k].empty()) {
|
||||
field_rect = draw_field(painter, field_rect, s, "", pad_string_with_spaces(5) + data_strings[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::set_entry(const BleRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
BLETxPacket BleRecentEntryDetailView::build_packet() {
|
||||
BLETxPacket bleTxPacket;
|
||||
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
|
||||
|
||||
std::string macAddressStr = to_string_mac_address(entry_.packetData.macAddress, 6, true);
|
||||
|
||||
strncpy(bleTxPacket.macAddress, macAddressStr.c_str(), 12);
|
||||
strncpy(bleTxPacket.advertisementData, entry_.dataString.c_str(), entry_.packetData.dataLen * 2);
|
||||
strncpy(bleTxPacket.packetCount, "50", 3);
|
||||
bleTxPacket.packet_count = 50;
|
||||
|
||||
return bleTxPacket;
|
||||
}
|
||||
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
void BLERxView::focus() {
|
||||
options_channel.focus();
|
||||
}
|
||||
|
||||
void BLERxView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
BLERxView::BLERxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_channel,
|
||||
&field_frequency,
|
||||
&check_log,
|
||||
&button_find,
|
||||
&check_name,
|
||||
&label_sort,
|
||||
&options_sort,
|
||||
&label_found,
|
||||
&text_found_count,
|
||||
&check_serial_log,
|
||||
&button_filter,
|
||||
&button_save_list,
|
||||
&button_clear_list,
|
||||
&button_switch,
|
||||
&recent_entries_view});
|
||||
|
||||
recent_entries_view.on_select = [this](const BleRecentEntry& entry) {
|
||||
nav_.push<BleRecentEntryDetailView>(entry);
|
||||
};
|
||||
|
||||
check_serial_log.on_select = [this](Checkbox&, bool v) {
|
||||
serial_logging = v;
|
||||
if (v) {
|
||||
usb_serial_thread = std::make_unique<UsbSerialThread>();
|
||||
} else {
|
||||
usb_serial_thread.reset();
|
||||
}
|
||||
};
|
||||
check_serial_log.set_value(serial_logging);
|
||||
|
||||
ensure_directory(find_packet_path);
|
||||
ensure_directory(log_packets_path);
|
||||
ensure_directory(packet_save_path);
|
||||
|
||||
filterBuffer = filter;
|
||||
|
||||
button_filter.on_select = [this](Button&) {
|
||||
text_prompt(
|
||||
nav_,
|
||||
filterBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_filter_change(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
logger = std::make_unique<BLELogger>();
|
||||
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
str_log = "";
|
||||
logging = v;
|
||||
|
||||
if (logger && logging)
|
||||
logger->append(
|
||||
"BLERX/Logs"
|
||||
"/BLELOG_" +
|
||||
to_string_timestamp(rtc_time::now()) + ".TXT");
|
||||
};
|
||||
check_log.set_value(logging);
|
||||
|
||||
button_save_list.on_select = [this, &nav](const ui::Button&) {
|
||||
listFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
listFileBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_save_file(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
button_clear_list.on_select = [this](Button&) {
|
||||
recent.clear();
|
||||
};
|
||||
|
||||
button_switch.on_select = [&nav](Button&) {
|
||||
nav.replace<BLETxView>();
|
||||
};
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
check_name.set_value(name_enable);
|
||||
|
||||
check_name.on_select = [this](Checkbox&, bool v) {
|
||||
name_enable = v;
|
||||
setAllMembersToValue(recent, &BleRecentEntry::include_name, v);
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t index, int32_t v) {
|
||||
channel_index = (uint8_t)index;
|
||||
|
||||
// If we selected Auto don't do anything and Auto will handle changing.
|
||||
if (v == 40) {
|
||||
auto_channel = true;
|
||||
return;
|
||||
} else {
|
||||
auto_channel = false;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(v));
|
||||
channel_number = v;
|
||||
|
||||
baseband::set_btlerx(channel_number);
|
||||
};
|
||||
|
||||
options_sort.on_change = [this](size_t index, int32_t v) {
|
||||
sort_index = (uint8_t)index;
|
||||
handle_entries_sort(v);
|
||||
};
|
||||
|
||||
options_channel.set_selected_index(channel_index, true);
|
||||
options_sort.set_selected_index(sort_index, true);
|
||||
|
||||
button_find.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
|
||||
// nav_.set_on_pop([this]() { button_play.focus(); });
|
||||
};
|
||||
};
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_btlerx(channel_number);
|
||||
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
std::string BLERxView::build_line_str(BleRecentEntry entry) {
|
||||
std::string macAddressStr = to_string_mac_address(entry.packetData.macAddress, 6, false) + ",";
|
||||
std::string timestameStr = entry.timestamp + ",";
|
||||
std::string nameStr = entry.nameString + ",";
|
||||
std::string pduStr = pdu_type_to_string(entry.pduType) + ",";
|
||||
std::string dataStr = "0x" + entry.dataString + ",";
|
||||
std::string hitsStr = to_string_dec_int(entry.numHits) + ",";
|
||||
std::string dbStr = to_string_dec_int(entry.dbValue) + ",";
|
||||
std::string channelStr = to_string_dec_int(entry.channelNumber) + ",";
|
||||
|
||||
std::string lineStr = timestameStr + macAddressStr + nameStr + pduStr + dataStr + hitsStr + dbStr + channelStr;
|
||||
lineStr += pad_string_with_spaces(maxLineLength - lineStr.length());
|
||||
|
||||
return lineStr;
|
||||
}
|
||||
|
||||
void BLERxView::on_save_file(const std::string value) {
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path);
|
||||
}
|
||||
|
||||
bool BLERxView::saveFile(const std::filesystem::path& path) {
|
||||
// Check to see if file was previously saved.
|
||||
bool file_existed = file_exists(path);
|
||||
|
||||
// Attempt to open, if it can't be opened. Create new.
|
||||
auto src = std::make_unique<File>();
|
||||
auto error = src->open(path, false, true);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : recent) {
|
||||
tempList.emplace_back(entry);
|
||||
}
|
||||
|
||||
if (!file_existed) {
|
||||
src->write_line(headerStr.c_str());
|
||||
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry entry = (BleRecentEntry)*it;
|
||||
src->write_line(build_line_str(entry).c_str());
|
||||
it++;
|
||||
}
|
||||
} else {
|
||||
// Check file for macAddressStr before adding.
|
||||
char currentLine[maxLineLength];
|
||||
uint64_t startPos = headerStr.length();
|
||||
uint64_t bytesRead = 0;
|
||||
uint64_t bytePos = 0;
|
||||
|
||||
File::Size currentSize = src->size();
|
||||
|
||||
auto dst = std::make_unique<File>();
|
||||
const std::filesystem::path tempFilePath = path + "~";
|
||||
auto error = dst->open(tempFilePath, false, true);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dst->write_line(headerStr.c_str());
|
||||
|
||||
src->seek(startPos);
|
||||
|
||||
// Look for ones found and rewrite.
|
||||
do {
|
||||
memset(currentLine, 0, maxLineLength);
|
||||
|
||||
bytesRead = readUntil(*src, currentLine, currentSize, '\n');
|
||||
|
||||
if (!bytesRead) {
|
||||
break;
|
||||
}
|
||||
|
||||
bytePos += bytesRead;
|
||||
|
||||
std::string lineStr = "";
|
||||
std::string macAddressStr = "";
|
||||
BleRecentEntry foundEntry;
|
||||
|
||||
char* token;
|
||||
token = strtok(currentLine, ",");
|
||||
|
||||
while (token != NULL) {
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry& entry = reinterpret_cast<BleRecentEntry&>(*it);
|
||||
|
||||
macAddressStr = to_string_mac_address(entry.packetData.macAddress, 6, false);
|
||||
|
||||
if (strstr(token, macAddressStr.c_str()) != NULL) {
|
||||
entry.entryFound = true;
|
||||
foundEntry = entry;
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
if (foundEntry.entryFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
if (foundEntry.entryFound) {
|
||||
dst->write_line(build_line_str(foundEntry).c_str());
|
||||
}
|
||||
|
||||
} while (bytePos <= currentSize);
|
||||
|
||||
// Write the ones not found.
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry entry = (BleRecentEntry)*it;
|
||||
|
||||
if (!entry.entryFound) {
|
||||
dst->write_line(build_line_str(entry).c_str());
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
// Close files before renaming/deleting.
|
||||
src.reset();
|
||||
dst.reset();
|
||||
|
||||
// Delete original and overwrite with temp file.
|
||||
delete_file(path);
|
||||
rename_file(tempFilePath, path);
|
||||
}
|
||||
|
||||
tempList.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLERxView::on_data(BlePacketData* packet) {
|
||||
if (!logging) {
|
||||
str_log = "";
|
||||
}
|
||||
|
||||
str_console += pdu_type_to_string((ADV_PDU_TYPE)packet->type);
|
||||
str_console += " Len:";
|
||||
str_console += to_string_dec_uint(packet->size);
|
||||
str_console += " Mac:";
|
||||
str_console += to_string_mac_address(packet->macAddress, 6, false);
|
||||
str_console += " Data:";
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
str_console += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
uint64_t macAddressEncoded = copy_mac_address_to_uint64(packet->macAddress);
|
||||
|
||||
// Start of Packet stuffing.
|
||||
// Masking off the top 2 bytes to avoid invalid keys.
|
||||
auto& entry = ::on_packet(recent, macAddressEncoded & 0xFFFFFFFFFFFF);
|
||||
updateEntry(packet, entry, (ADV_PDU_TYPE)packet->type);
|
||||
|
||||
// Add entries if they meet the criteria.
|
||||
auto value = filter;
|
||||
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
|
||||
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
|
||||
});
|
||||
|
||||
handle_entries_sort(options_sort.selected_index());
|
||||
|
||||
// Log at End of Packet.
|
||||
if (logger && logging) {
|
||||
logger->log_raw_data(str_console + "\r\n");
|
||||
}
|
||||
|
||||
if (serial_logging) {
|
||||
usb_serial_thread->serial_str = str_console + "\r\n";
|
||||
usb_serial_thread->str_ready = true;
|
||||
}
|
||||
str_console = "";
|
||||
|
||||
if (!searchList.empty()) {
|
||||
auto it = searchList.begin();
|
||||
|
||||
while (it != searchList.end()) {
|
||||
std::string searchStr = (std::string)*it;
|
||||
|
||||
if (entry.dataString.find(searchStr) != std::string::npos) {
|
||||
searchList.erase(it);
|
||||
found_count++;
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
text_found_count.set(to_string_dec_uint(found_count) + "/" + to_string_dec_uint(total_count));
|
||||
}
|
||||
}
|
||||
|
||||
void BLERxView::on_filter_change(std::string value) {
|
||||
// New filter? Reset list from recent entries.
|
||||
if (filter != value) {
|
||||
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
|
||||
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
|
||||
});
|
||||
}
|
||||
|
||||
filter = value;
|
||||
}
|
||||
|
||||
void BLERxView::on_file_changed(const std::filesystem::path& new_file_path) {
|
||||
file_path = fs::path(u"/") + new_file_path;
|
||||
found_count = 0;
|
||||
total_count = 0;
|
||||
searchList.clear();
|
||||
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path, true, false);
|
||||
if (error) {
|
||||
file_error();
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t bytesRead = 0;
|
||||
uint64_t bytePos = 0;
|
||||
char currentLine[maxLineLength];
|
||||
|
||||
do {
|
||||
memset(currentLine, 0, maxLineLength);
|
||||
|
||||
bytesRead = readUntil(data_file, currentLine, maxLineLength, '\n');
|
||||
|
||||
// Remove return if found.
|
||||
if (currentLine[strlen(currentLine)] == '\r') {
|
||||
currentLine[strlen(currentLine)] = '\0';
|
||||
}
|
||||
|
||||
if (!bytesRead) {
|
||||
break;
|
||||
}
|
||||
|
||||
searchList.push_back(currentLine);
|
||||
total_count++;
|
||||
|
||||
bytePos += bytesRead;
|
||||
|
||||
} while (bytePos <= data_file.size());
|
||||
}
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BLERxView::on_timer() {
|
||||
if (++timer_count == timer_period) {
|
||||
timer_count = 0;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
int randomChannel = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
|
||||
baseband::set_btlerx(randomChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLERxView::handle_entries_sort(uint8_t index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.macAddress; }, true);
|
||||
break;
|
||||
case 1:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.numHits; }, false);
|
||||
break;
|
||||
case 2:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.dbValue; }, false);
|
||||
break;
|
||||
case 3:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.timestamp; }, false);
|
||||
break;
|
||||
case 4:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.nameString; }, true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
void BLERxView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height - switch_button_height};
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLERxView::~BLERxView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type) {
|
||||
std::string data_string;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
data_string += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
entry.dbValue = packet->max_dB;
|
||||
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;
|
||||
|
||||
// Mac Address of sender.
|
||||
entry.packetData.macAddress[0] = packet->macAddress[0];
|
||||
entry.packetData.macAddress[1] = packet->macAddress[1];
|
||||
entry.packetData.macAddress[2] = packet->macAddress[2];
|
||||
entry.packetData.macAddress[3] = packet->macAddress[3];
|
||||
entry.packetData.macAddress[4] = packet->macAddress[4];
|
||||
entry.packetData.macAddress[5] = packet->macAddress[5];
|
||||
|
||||
entry.numHits++;
|
||||
entry.pduType = pdu_type;
|
||||
entry.channelNumber = channel_number;
|
||||
|
||||
// Parse Data Section into buffer to be interpretted later.
|
||||
for (int i = 0; i < packet->dataLen; i++) {
|
||||
entry.packetData.data[i] = packet->data[i];
|
||||
}
|
||||
|
||||
entry.include_name = check_name.value();
|
||||
|
||||
// Only parse name for advertisment packets and empty name entries
|
||||
if ((pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND) && entry.nameString.empty()) {
|
||||
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry.packetData.data;
|
||||
|
||||
uint8_t currentByte = 0;
|
||||
uint8_t length = 0;
|
||||
uint8_t type = 0;
|
||||
|
||||
std::string decoded_data;
|
||||
for (currentByte = 0; (currentByte < entry.packetData.dataLen);) {
|
||||
length = advertiseData->Data[currentByte++];
|
||||
type = advertiseData->Data[currentByte++];
|
||||
|
||||
// Subtract 1 because type is part of the length.
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
if (type == 0x08 || type == 0x09) {
|
||||
decoded_data += (char)advertiseData->Data[currentByte];
|
||||
}
|
||||
currentByte++;
|
||||
}
|
||||
if (!decoded_data.empty()) {
|
||||
entry.nameString = std::move(decoded_data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (pdu_type == ADV_DIRECT_IND || pdu_type == SCAN_REQ) {
|
||||
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)entry.packetData.data;
|
||||
reverse_byte_array(directed_mac_data->A1, 6);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 __BLE_RX_APP_H__
|
||||
#define __BLE_RX_APP_H__
|
||||
|
||||
#include "ble_tx_app.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "usb_serial_thread.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
class BLELogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
typedef enum {
|
||||
ADV_IND = 0,
|
||||
ADV_DIRECT_IND = 1,
|
||||
ADV_NONCONN_IND = 2,
|
||||
SCAN_REQ = 3,
|
||||
SCAN_RSP = 4,
|
||||
CONNECT_REQ = 5,
|
||||
ADV_SCAN_IND = 6,
|
||||
RESERVED0 = 7,
|
||||
RESERVED1 = 8,
|
||||
RESERVED2 = 9,
|
||||
RESERVED3 = 10,
|
||||
RESERVED4 = 11,
|
||||
RESERVED5 = 12,
|
||||
RESERVED6 = 13,
|
||||
RESERVED7 = 14,
|
||||
RESERVED8 = 15
|
||||
} ADV_PDU_TYPE;
|
||||
|
||||
struct BleRecentEntry {
|
||||
using Key = uint64_t;
|
||||
|
||||
static constexpr Key invalid_key = 0xffffffff;
|
||||
|
||||
uint64_t macAddress;
|
||||
int dbValue;
|
||||
BlePacketData packetData;
|
||||
std::string timestamp;
|
||||
std::string dataString;
|
||||
std::string nameString;
|
||||
bool include_name;
|
||||
uint16_t numHits;
|
||||
ADV_PDU_TYPE pduType;
|
||||
uint8_t channelNumber;
|
||||
bool entryFound;
|
||||
|
||||
BleRecentEntry()
|
||||
: BleRecentEntry{0} {
|
||||
}
|
||||
|
||||
BleRecentEntry(
|
||||
const uint64_t macAddress)
|
||||
: macAddress{macAddress},
|
||||
dbValue{},
|
||||
packetData{},
|
||||
timestamp{},
|
||||
dataString{},
|
||||
nameString{},
|
||||
include_name{},
|
||||
numHits{},
|
||||
pduType{},
|
||||
channelNumber{},
|
||||
entryFound{} {
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return macAddress;
|
||||
}
|
||||
};
|
||||
|
||||
using BleRecentEntries = RecentEntries<BleRecentEntry>;
|
||||
using BleRecentEntriesView = RecentEntriesView<BleRecentEntries>;
|
||||
|
||||
class BleRecentEntryDetailView : public View {
|
||||
public:
|
||||
BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry);
|
||||
|
||||
void set_entry(const BleRecentEntry& new_entry);
|
||||
const BleRecentEntry& entry() const { return entry_; };
|
||||
|
||||
void update_data();
|
||||
void focus() override;
|
||||
void paint(Painter&) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
BleRecentEntry entry_{};
|
||||
BLETxPacket build_packet();
|
||||
void on_save_file(const std::string value, BLETxPacket packetToSave);
|
||||
bool saveFile(const std::filesystem::path& path, BLETxPacket packetToSave);
|
||||
std::string packetFileBuffer{};
|
||||
std::filesystem::path packet_save_path{u"BLERX/Lists/????.csv"};
|
||||
|
||||
static constexpr uint8_t total_data_lines{5};
|
||||
|
||||
Labels label_mac_address{
|
||||
{{0 * 8, 0 * 16}, "Mac Address:", Color::light_grey()}};
|
||||
|
||||
Text text_mac_address{
|
||||
{12 * 8, 0 * 16, 17 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_pdu_type{
|
||||
{{0 * 8, 1 * 16}, "PDU Type:", Color::light_grey()}};
|
||||
|
||||
Text text_pdu_type{
|
||||
{9 * 8, 1 * 16, 17 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 3 * 16}, "Len", Color::light_grey()},
|
||||
{{5 * 8, 3 * 16}, "Type", Color::light_grey()},
|
||||
{{10 * 8, 3 * 16}, "Value", Color::light_grey()},
|
||||
};
|
||||
|
||||
Button button_send{
|
||||
{19, 224, 96, 24},
|
||||
"Send"};
|
||||
|
||||
Button button_done{
|
||||
{125, 224, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Button button_save{
|
||||
{72, 264, 96, 24},
|
||||
"Save"};
|
||||
|
||||
bool send_updates{false};
|
||||
|
||||
Rect draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value);
|
||||
};
|
||||
|
||||
class BLERxView : public View {
|
||||
public:
|
||||
BLERxView(NavigationView& nav);
|
||||
~BLERxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BLE RX"; };
|
||||
|
||||
private:
|
||||
std::string build_line_str(BleRecentEntry entry);
|
||||
void on_save_file(const std::string value);
|
||||
bool saveFile(const std::filesystem::path& path);
|
||||
std::unique_ptr<UsbSerialThread> usb_serial_thread{};
|
||||
void on_data(BlePacketData* packetData);
|
||||
void on_filter_change(std::string value);
|
||||
void on_file_changed(const std::filesystem::path& new_file_path);
|
||||
void file_error();
|
||||
void on_timer();
|
||||
void handle_entries_sort(uint8_t index);
|
||||
void updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type);
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
2402000000 /* frequency */,
|
||||
4000000 /* bandwidth */,
|
||||
4000000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
|
||||
uint8_t channel_index{0};
|
||||
uint8_t sort_index{0};
|
||||
std::string filter{};
|
||||
bool logging{false};
|
||||
bool serial_logging{false};
|
||||
|
||||
bool name_enable{true};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_ble",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"channel_index"sv, &channel_index},
|
||||
{"sort_index"sv, &sort_index},
|
||||
{"filter"sv, &filter},
|
||||
{"log"sv, &logging},
|
||||
// disabled to always start without USB serial activated until we can make it non blocking if not connected
|
||||
// {"serial_log"sv, &serial_logging},
|
||||
{"name"sv, &name_enable},
|
||||
}};
|
||||
|
||||
std::string str_console = "";
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
uint8_t channel_number = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
int16_t timer_count{0};
|
||||
int16_t timer_period{6}; // 100ms
|
||||
|
||||
std::string filterBuffer{};
|
||||
std::string listFileBuffer{};
|
||||
std::string headerStr = "Timestamp, MAC Address, Name, Packet Type, Data, Hits, dB, Channel";
|
||||
uint16_t maxLineLength = 140;
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
uint64_t found_count = 0;
|
||||
uint64_t total_count = 0;
|
||||
std::vector<std::string> searchList{};
|
||||
|
||||
std::filesystem::path find_packet_path{u"BLERX/Find/????.TXT"};
|
||||
std::filesystem::path log_packets_path{u"BLERX/Logs/????.TXT"};
|
||||
std::filesystem::path packet_save_path{u"BLERX/Lists/????.csv"};
|
||||
|
||||
static constexpr auto header_height = 4 * 16;
|
||||
static constexpr auto switch_button_height = 3 * 16;
|
||||
|
||||
OptionsField options_channel{
|
||||
{0 * 8, 0 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{6 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Labels label_sort{
|
||||
{{0 * 8, 3 * 8}, "Sort:", Color::light_grey()}};
|
||||
|
||||
OptionsField options_sort{
|
||||
{5 * 8, 3 * 8},
|
||||
4,
|
||||
{{"MAC", 0},
|
||||
{"Hits", 1},
|
||||
{"dB", 2},
|
||||
{"Time", 3},
|
||||
{"Name", 4}}};
|
||||
|
||||
Button button_filter{
|
||||
{11 * 8, 3 * 8, 4 * 8, 16},
|
||||
"Filter"};
|
||||
|
||||
Checkbox check_log{
|
||||
{17 * 8, 3 * 8},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Checkbox check_name{
|
||||
{23 * 8, 3 * 8},
|
||||
3,
|
||||
"Name",
|
||||
true};
|
||||
|
||||
Button button_find{
|
||||
{0 * 8, 6 * 8, 4 * 8, 16},
|
||||
"Find"};
|
||||
|
||||
Labels label_found{
|
||||
{{5 * 8, 6 * 8}, "Found:", Color::light_grey()}};
|
||||
|
||||
Text text_found_count{
|
||||
{11 * 8, 3 * 16, 20 * 8, 16},
|
||||
"0/0"};
|
||||
|
||||
Checkbox check_serial_log{
|
||||
{17 * 8, 3 * 16 - 2},
|
||||
7,
|
||||
"USB Log",
|
||||
true};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
|
||||
Button button_clear_list{
|
||||
{2 * 8, 320 - (16 + 32), 7 * 8, 32},
|
||||
"Clear"};
|
||||
|
||||
Button button_save_list{
|
||||
{11 * 8, 320 - (16 + 32), 11 * 8, 32},
|
||||
"Export CSV"};
|
||||
|
||||
Button button_switch{
|
||||
{240 - 6 * 8, 320 - (16 + 32), 4 * 8, 32},
|
||||
"Tx"};
|
||||
|
||||
std::string str_log{""};
|
||||
std::unique_ptr<BLELogger> logger{};
|
||||
|
||||
BleRecentEntries recent{};
|
||||
BleRecentEntries tempList{};
|
||||
|
||||
const RecentEntriesColumns columns{{
|
||||
{"Mac Address", 17},
|
||||
{"Hits", 7},
|
||||
{"dB", 4},
|
||||
}};
|
||||
|
||||
BleRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::BlePacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const BLEPacketMessage*>(p);
|
||||
this->on_data(message->packet);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -0,0 +1,561 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 "ble_tx_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
void BLELoggerTx::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
bool hasValidHexPairs(const std::string& str, int totalPairs) {
|
||||
int validPairs = 0;
|
||||
|
||||
for (int i = 0; i < totalPairs * 2; i += 2) {
|
||||
char c1 = str[i];
|
||||
char c2 = str[i + 1];
|
||||
|
||||
if (!(std::isxdigit(c1) && std::isxdigit(c2))) {
|
||||
return false; // Return false if any pair is invalid.
|
||||
}
|
||||
|
||||
validPairs++;
|
||||
}
|
||||
|
||||
return (validPairs == totalPairs);
|
||||
}
|
||||
|
||||
std::vector<std::string> splitIntoStrings(const char* input) {
|
||||
std::vector<std::string> result;
|
||||
int length = std::strlen(input);
|
||||
int start = 0;
|
||||
|
||||
while (start < length) {
|
||||
int remaining = length - start;
|
||||
int chunkSize = (remaining > 29) ? 29 : remaining;
|
||||
result.push_back(std::string(input + start, chunkSize));
|
||||
start += chunkSize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t stringToUint32(const std::string& str) {
|
||||
size_t pos = 0;
|
||||
uint32_t result = 0;
|
||||
|
||||
while (pos < str.size() && std::isdigit(str[pos])) {
|
||||
int digit = str[pos] - '0';
|
||||
|
||||
// Check for overflow before adding the next digit
|
||||
if (result > (UINT32_MAX - digit) / 10) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
result = result * 10 + digit;
|
||||
pos++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
void BLETxView::focus() {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
bool BLETxView::is_active() const {
|
||||
return (bool)is_running;
|
||||
}
|
||||
|
||||
void BLETxView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
bool BLETxView::saveFile(const std::filesystem::path& path) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
for (uint32_t i = 0; i < num_packets; i++) {
|
||||
std::string macAddressStr = packets[i].macAddress;
|
||||
std::string advertisementDataStr = packets[i].advertisementData;
|
||||
std::string packetCountStr = packets[i].packetCount;
|
||||
|
||||
std::string packetString = macAddressStr + ' ' + advertisementDataStr + ' ' + packetCountStr;
|
||||
|
||||
// Are we on the last line?
|
||||
if (i != num_packets - 1) {
|
||||
packetString += '\n';
|
||||
}
|
||||
|
||||
f.write(packetString.c_str(), packetString.length());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLETxView::toggle() {
|
||||
if (is_active()) {
|
||||
stop();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::start() {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
|
||||
transmitter_model.enable();
|
||||
|
||||
// Generate new random Mac Address.
|
||||
generateRandomMacAddress(randomMac);
|
||||
|
||||
// If this is our first run, check file.
|
||||
if (!is_active()) {
|
||||
File data_file;
|
||||
|
||||
auto error = data_file.open(file_path);
|
||||
if (error && !file_override) {
|
||||
file_error();
|
||||
check_loop.set_value(false);
|
||||
return;
|
||||
}
|
||||
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
is_running = true;
|
||||
}
|
||||
|
||||
char advertisementData[63] = {0};
|
||||
strcpy(advertisementData, packets[current_packet].advertisementData);
|
||||
|
||||
// TODO: Make this a checkbox.
|
||||
if (!markedBytes.empty()) {
|
||||
for (size_t i = 0; i < strlen(advertisementData); i++) {
|
||||
bool found = false;
|
||||
|
||||
auto it = std::find(markedBytes.begin(), markedBytes.end(), i);
|
||||
|
||||
if (it != markedBytes.end()) {
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
uint8_t hexDigit;
|
||||
switch (marked_data_sequence.selected_index_value()) {
|
||||
case 0:
|
||||
hexDigit = marked_counter++;
|
||||
break;
|
||||
case 1:
|
||||
hexDigit = marked_counter--;
|
||||
break;
|
||||
case 2: {
|
||||
uint8_t min = 0x00;
|
||||
uint8_t max = 0x0F;
|
||||
|
||||
hexDigit = min + std::rand() % (max - min + 1);
|
||||
} break;
|
||||
default:
|
||||
hexDigit = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
advertisementData[i] = uint_to_char(hexDigit, 16);
|
||||
|
||||
// Bounding to Hex.
|
||||
if (marked_counter == 16) {
|
||||
marked_counter = 0;
|
||||
} else if (marked_counter == 255) {
|
||||
marked_counter = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup next packet configuration.
|
||||
progressbar.set_max(packets[current_packet].packet_count);
|
||||
baseband::set_btletx(channel_number, random_mac ? randomMac : packets[current_packet].macAddress, advertisementData, pduType);
|
||||
}
|
||||
|
||||
void BLETxView::stop() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
progressbar.set_value(0);
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
check_loop.set_value(false);
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
|
||||
is_running = false;
|
||||
}
|
||||
|
||||
void BLETxView::reset() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BLETxView::on_timer() {
|
||||
if (++timer_count == timer_period) {
|
||||
timer_count = 0;
|
||||
|
||||
if (is_active()) {
|
||||
// Reached end of current packet repeats.
|
||||
if (packet_counter == 0) {
|
||||
// Done sending all packets.
|
||||
if (current_packet == (num_packets - 1)) {
|
||||
current_packet = 0;
|
||||
|
||||
// If looping, restart from beginning.
|
||||
if (check_loop.value()) {
|
||||
update_current_packet(packets[current_packet], current_packet);
|
||||
reset();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
} else {
|
||||
current_packet++;
|
||||
update_current_packet(packets[current_packet], current_packet);
|
||||
reset();
|
||||
}
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (++auto_channel_counter == auto_channel_period) {
|
||||
auto_channel_counter = 0;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
channel_number = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::on_tx_progress(const bool done) {
|
||||
if (done) {
|
||||
if (is_active()) {
|
||||
if ((packet_counter % 10) == 0) {
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
}
|
||||
|
||||
packet_counter--;
|
||||
progressbar.set_value(packets[current_packet].packet_count - packet_counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BLETxView::BLETxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&button_open,
|
||||
&text_filename,
|
||||
&progressbar,
|
||||
&check_rand_mac,
|
||||
&field_frequency,
|
||||
&tx_view, // now it handles previous rfgain, rfamp.
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&label_speed,
|
||||
&options_speed,
|
||||
&options_channel,
|
||||
&options_adv_type,
|
||||
&label_marked_data,
|
||||
&marked_data_sequence,
|
||||
&label_packet_index,
|
||||
&text_packet_index,
|
||||
&label_packets_sent,
|
||||
&text_packets_sent,
|
||||
&label_mac_address,
|
||||
&text_mac_address,
|
||||
&label_data_packet,
|
||||
&dataEditView,
|
||||
&button_clear_marked,
|
||||
&button_save_packet,
|
||||
&button_switch});
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
ensure_directory(packet_save_path);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t, int32_t i) {
|
||||
// If we selected Auto don't do anything and Auto will handle changing.
|
||||
if (i == 40) {
|
||||
auto_channel = true;
|
||||
return;
|
||||
} else {
|
||||
auto_channel = false;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(i));
|
||||
channel_number = i;
|
||||
};
|
||||
|
||||
options_speed.on_change = [this](size_t, int32_t i) {
|
||||
timer_period = i;
|
||||
timer_count = 0;
|
||||
};
|
||||
|
||||
options_adv_type.on_change = [this](size_t, int32_t i) {
|
||||
pduType = (PKT_TYPE)i;
|
||||
};
|
||||
|
||||
options_speed.set_selected_index(0);
|
||||
options_channel.set_selected_index(0);
|
||||
options_adv_type.set_selected_index(0);
|
||||
|
||||
check_rand_mac.set_value(false);
|
||||
check_rand_mac.on_select = [this](Checkbox&, bool v) {
|
||||
random_mac = v;
|
||||
};
|
||||
|
||||
button_open.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
|
||||
nav_.set_on_pop([this]() { button_play.focus(); });
|
||||
};
|
||||
};
|
||||
|
||||
button_save_packet.on_select = [this, &nav](Button&) {
|
||||
packetFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
packetFileBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_save_file(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
button_switch.on_select = [&nav](Button&) {
|
||||
nav.replace<BLERxView>();
|
||||
};
|
||||
|
||||
dataEditView.on_select = [this] {
|
||||
// Save last selected cursor.
|
||||
cursor_pos.line = dataEditView.line();
|
||||
cursor_pos.col = dataEditView.col();
|
||||
|
||||
// Reject setting newline at index 29.
|
||||
if (cursor_pos.col != 29) {
|
||||
uint16_t dataBytePos = (cursor_pos.line * 29) + cursor_pos.col;
|
||||
|
||||
auto it = std::find(markedBytes.begin(), markedBytes.end(), dataBytePos);
|
||||
|
||||
if (it != markedBytes.end()) {
|
||||
markedBytes.erase(it);
|
||||
} else {
|
||||
markedBytes.push_back(dataBytePos);
|
||||
}
|
||||
|
||||
dataEditView.cursor_mark_selected();
|
||||
}
|
||||
};
|
||||
|
||||
button_clear_marked.on_select = [this](Button&) {
|
||||
marked_counter = 0;
|
||||
markedBytes.clear();
|
||||
dataEditView.cursor_clear_marked();
|
||||
};
|
||||
}
|
||||
|
||||
BLETxView::BLETxView(
|
||||
NavigationView& nav,
|
||||
BLETxPacket packet)
|
||||
: BLETxView(nav) {
|
||||
packets[0] = packet;
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
|
||||
num_packets = 1;
|
||||
file_override = true;
|
||||
}
|
||||
|
||||
void BLETxView::on_file_changed(const fs::path& new_file_path) {
|
||||
file_path = fs::path(u"/") + new_file_path;
|
||||
num_packets = 0;
|
||||
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path);
|
||||
if (error) {
|
||||
file_error();
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
|
||||
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
|
||||
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
|
||||
|
||||
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
|
||||
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
|
||||
uint64_t packetCountSize = strlen(packets[num_packets].packetCount);
|
||||
|
||||
packets[num_packets].packet_count = stringToUint32(packets[num_packets].packetCount);
|
||||
|
||||
// Verify Data.
|
||||
if ((macAddressSize == mac_address_size_str) && (advertisementDataSize < max_packet_size_str) && (packetCountSize < max_packet_repeat_str) &&
|
||||
hasValidHexPairs(packets[num_packets].macAddress, macAddressSize / 2) && hasValidHexPairs(packets[num_packets].advertisementData, advertisementDataSize / 2) && (packets[num_packets].packet_count >= 1) && (packets[num_packets].packet_count < max_packet_repeat_count)) {
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
} else {
|
||||
// Did not find any packets.
|
||||
if (num_packets == 0) {
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
num_packets++;
|
||||
|
||||
} while (num_packets < max_num_packets);
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::on_save_file(const std::string value) {
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path);
|
||||
}
|
||||
|
||||
void BLETxView::on_data(uint32_t value, bool is_data) {
|
||||
std::string str_console = "";
|
||||
|
||||
if (is_data) {
|
||||
str_console += (char)(value);
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex) {
|
||||
std::string formattedMacAddress = to_string_formatted_mac_address(packet.macAddress);
|
||||
|
||||
std::vector<std::string> strings = splitIntoStrings(packet.advertisementData);
|
||||
|
||||
text_packet_index.set(to_string_dec_uint(current_packet));
|
||||
|
||||
text_packets_sent.set(to_string_dec_uint(packet.packet_count));
|
||||
|
||||
text_mac_address.set(formattedMacAddress);
|
||||
|
||||
packet_counter = packet.packet_count;
|
||||
current_packet = currentIndex;
|
||||
|
||||
dataFile.create(dataTempFilePath);
|
||||
|
||||
for (const std::string& str : strings) {
|
||||
dataFile.write(str.c_str(), str.size());
|
||||
dataFile.write("\n", 1);
|
||||
;
|
||||
}
|
||||
|
||||
dataFile.~File();
|
||||
|
||||
auto result = FileWrapper::open(dataTempFilePath);
|
||||
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
dataFileWrapper = *std::move(result);
|
||||
|
||||
dataEditView.set_font_zoom(true);
|
||||
dataEditView.set_file(*dataFileWrapper);
|
||||
dataEditView.redraw(true, true);
|
||||
}
|
||||
|
||||
void BLETxView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height - switch_button_height};
|
||||
dataEditView.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLETxView::~BLETxView() {
|
||||
delete_file(dataTempFilePath);
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 __BLE_TX_APP_H__
|
||||
#define __BLE_TX_APP_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
class BLELoggerTx {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
enum PKT_TYPE {
|
||||
PKT_TYPE_INVALID_TYPE,
|
||||
PKT_TYPE_RAW,
|
||||
PKT_TYPE_DISCOVERY,
|
||||
PKT_TYPE_IBEACON,
|
||||
PKT_TYPE_ADV_IND,
|
||||
PKT_TYPE_ADV_DIRECT_IND,
|
||||
PKT_TYPE_ADV_NONCONN_IND,
|
||||
PKT_TYPE_ADV_SCAN_IND,
|
||||
PKT_TYPE_SCAN_REQ,
|
||||
PKT_TYPE_SCAN_RSP,
|
||||
PKT_TYPE_CONNECT_REQ,
|
||||
PKT_TYPE_LL_DATA,
|
||||
PKT_TYPE_LL_CONNECTION_UPDATE_REQ,
|
||||
PKT_TYPE_LL_CHANNEL_MAP_REQ,
|
||||
PKT_TYPE_LL_TERMINATE_IND,
|
||||
PKT_TYPE_LL_ENC_REQ,
|
||||
PKT_TYPE_LL_ENC_RSP,
|
||||
PKT_TYPE_LL_START_ENC_REQ,
|
||||
PKT_TYPE_LL_START_ENC_RSP,
|
||||
PKT_TYPE_LL_UNKNOWN_RSP,
|
||||
PKT_TYPE_LL_FEATURE_REQ,
|
||||
PKT_TYPE_LL_FEATURE_RSP,
|
||||
PKT_TYPE_LL_PAUSE_ENC_REQ,
|
||||
PKT_TYPE_LL_PAUSE_ENC_RSP,
|
||||
PKT_TYPE_LL_VERSION_IND,
|
||||
PKT_TYPE_LL_REJECT_IND,
|
||||
PKT_TYPE_NUM_PKT_TYPE
|
||||
};
|
||||
|
||||
struct BLETxPacket {
|
||||
char macAddress[13];
|
||||
char advertisementData[63];
|
||||
char packetCount[11];
|
||||
uint32_t packet_count;
|
||||
PKT_TYPE packetType;
|
||||
};
|
||||
|
||||
class BLETxView : public View {
|
||||
public:
|
||||
BLETxView(NavigationView& nav);
|
||||
BLETxView(NavigationView& nav, BLETxPacket packet);
|
||||
~BLETxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
bool is_active() const;
|
||||
void toggle();
|
||||
void start();
|
||||
void stop();
|
||||
void reset();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
bool saveFile(const std::filesystem::path& path);
|
||||
|
||||
std::string title() const override { return "BLE TX"; };
|
||||
|
||||
private:
|
||||
void on_timer();
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
void on_file_changed(const std::filesystem::path& new_file_path);
|
||||
void on_save_file(const std::string value);
|
||||
void on_tx_progress(const bool done);
|
||||
void on_random_data_change(std::string value);
|
||||
void update_current_packet(BLETxPacket packet, uint32_t currentIndex);
|
||||
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_ble", app_settings::Mode::TX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
std::filesystem::path packet_save_path{u"BLETX/BLETX_????.TXT"};
|
||||
uint8_t channel_number = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
char randomMac[13] = "010203040506";
|
||||
|
||||
bool is_running = false;
|
||||
|
||||
int16_t timer_count{0};
|
||||
int16_t timer_period{1};
|
||||
int16_t auto_channel_counter = 0;
|
||||
int16_t auto_channel_period{6};
|
||||
|
||||
bool repeatLoop = false;
|
||||
uint32_t packet_counter{0};
|
||||
uint32_t num_packets{0};
|
||||
uint32_t current_packet{0};
|
||||
bool random_mac = false;
|
||||
bool file_override = false;
|
||||
|
||||
typedef struct {
|
||||
uint16_t line;
|
||||
uint16_t col;
|
||||
} CursorPos;
|
||||
|
||||
std::unique_ptr<FileWrapper> dataFileWrapper{};
|
||||
File dataFile{};
|
||||
std::filesystem::path dataTempFilePath{u"BLETX/dataFileTemp.TXT"};
|
||||
std::vector<uint16_t> markedBytes{};
|
||||
CursorPos cursor_pos{};
|
||||
uint8_t marked_counter = 0;
|
||||
|
||||
static constexpr uint8_t mac_address_size_str{12};
|
||||
static constexpr uint8_t max_packet_size_str{62};
|
||||
static constexpr uint8_t max_packet_repeat_str{10};
|
||||
static constexpr uint32_t max_packet_repeat_count{UINT32_MAX};
|
||||
static constexpr uint32_t max_num_packets{32};
|
||||
|
||||
BLETxPacket packets[max_num_packets];
|
||||
|
||||
PKT_TYPE pduType = {PKT_TYPE_DISCOVERY};
|
||||
|
||||
static constexpr auto header_height = 10 * 16;
|
||||
static constexpr auto switch_button_height = 6 * 16;
|
||||
|
||||
Button button_open{
|
||||
{0 * 8, 0 * 16, 10 * 8, 2 * 16},
|
||||
"Open file"};
|
||||
|
||||
Text text_filename{
|
||||
{11 * 8, 0 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{11 * 8, 1 * 16, 9 * 8, 16}};
|
||||
|
||||
Checkbox check_rand_mac{
|
||||
{21 * 8, 1 * 16},
|
||||
6,
|
||||
"?? Mac",
|
||||
true};
|
||||
|
||||
TxFrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
4,
|
||||
"Loop",
|
||||
true};
|
||||
|
||||
ImageButton button_play{
|
||||
{28 * 8, 2 * 16, 2 * 8, 1 * 16},
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
|
||||
Labels label_speed{
|
||||
{{0 * 8, 6 * 8}, "Speed:", Color::light_grey()}};
|
||||
|
||||
OptionsField options_speed{
|
||||
{7 * 8, 6 * 8},
|
||||
3,
|
||||
{{"1 ", 1}, // 16ms
|
||||
{"2 ", 2}, // 32ms
|
||||
{"3 ", 3}, // 48ms
|
||||
{"4 ", 6}, // 100ms
|
||||
{"5 ", 12}}}; // 200ms
|
||||
|
||||
OptionsField options_channel{
|
||||
{11 * 8, 6 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
OptionsField options_adv_type{
|
||||
{17 * 8, 6 * 8},
|
||||
14,
|
||||
{{"DISCOVERY ", PKT_TYPE_DISCOVERY},
|
||||
{"ADV_IND", PKT_TYPE_ADV_IND},
|
||||
{"ADV_DIRECT", PKT_TYPE_ADV_DIRECT_IND},
|
||||
{"ADV_NONCONN", PKT_TYPE_ADV_NONCONN_IND},
|
||||
{"ADV_SCAN_IND", PKT_TYPE_ADV_SCAN_IND},
|
||||
{"SCAN_REQ", PKT_TYPE_SCAN_REQ},
|
||||
{"SCAN_RSP", PKT_TYPE_SCAN_RSP},
|
||||
{"CONNECT_REQ", PKT_TYPE_CONNECT_REQ}}};
|
||||
|
||||
Labels label_marked_data{
|
||||
{{0 * 8, 4 * 16}, "Marked Data:", Color::light_grey()}};
|
||||
|
||||
OptionsField marked_data_sequence{
|
||||
{12 * 8, 8 * 8},
|
||||
8,
|
||||
{{"Ascend", 0},
|
||||
{"Descend", 1},
|
||||
{"Random", 2}}};
|
||||
|
||||
Labels label_packet_index{
|
||||
{{0 * 8, 12 * 8}, "Packet Index:", Color::light_grey()}};
|
||||
|
||||
Text text_packet_index{
|
||||
{13 * 8, 6 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_packets_sent{
|
||||
{{0 * 8, 14 * 8}, "Repeat Count:", Color::light_grey()}};
|
||||
|
||||
Text text_packets_sent{
|
||||
{13 * 8, 7 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_mac_address{
|
||||
{{0 * 8, 16 * 8}, "Mac Address:", Color::light_grey()}};
|
||||
|
||||
Text text_mac_address{
|
||||
{12 * 8, 8 * 16, 20 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_data_packet{
|
||||
{{0 * 8, 9 * 16}, "Packet Data:", Color::light_grey()}};
|
||||
|
||||
Console console{
|
||||
{0, 9 * 18, 240, 240}};
|
||||
|
||||
TextViewer dataEditView{
|
||||
{0, 9 * 18, 240, 240}};
|
||||
|
||||
Button button_clear_marked{
|
||||
{1 * 8, 14 * 16, 13 * 8, 3 * 8},
|
||||
"Clear Marked"};
|
||||
|
||||
Button button_save_packet{
|
||||
{1 * 8, 16 * 16, 13 * 8, 2 * 16},
|
||||
"Save Packet"};
|
||||
|
||||
Button button_switch{
|
||||
{16 * 8, 16 * 16, 13 * 8, 2 * 16},
|
||||
"Switch to Rx"};
|
||||
|
||||
std::string str_log{""};
|
||||
bool logging{true};
|
||||
bool logging_done{false};
|
||||
std::string packetFileBuffer{};
|
||||
|
||||
std::unique_ptr<BLELoggerTx> logger{};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->on_data(message->value, message->is_data);
|
||||
}};
|
||||
|
||||
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.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -21,15 +21,16 @@
|
||||
*/
|
||||
|
||||
#include "capture_app.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
CaptureAppView::CaptureAppView(NavigationView& nav) {
|
||||
CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_capture);
|
||||
|
||||
add_children({
|
||||
@@ -42,90 +43,61 @@ CaptureAppView::CaptureAppView(NavigationView& nav) {
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&option_bandwidth,
|
||||
&option_format,
|
||||
&check_trim,
|
||||
&record_view,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
// Hack for initialization
|
||||
// TODO: This should be included in a more global section so apps dont need to do it
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
//-------------------
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency_step.set_by_value(receiver_model.frequency_step());
|
||||
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
this->field_frequency.set_step(v);
|
||||
};
|
||||
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
|
||||
sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side
|
||||
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz , when selected 1 Mhz BW ...*/
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */
|
||||
|
||||
waterfall.on_hide();
|
||||
record_view.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
/* Set up proper anti aliasing BPF bandwith in MAX2837 before ADC sampling according to the new added BW Options . */
|
||||
|
||||
switch (sampling_rate) { // we use the var fs (sampling_rate) , to set up BPF aprox < fs_max/2 by Nyquist theorem.
|
||||
|
||||
case 0 ... 2000000: // BW Captured range (0 <= 250kHz max ) fs = 8 x 250 kHz
|
||||
anti_alias_baseband_bandwidth_filter = 1750000; // Minimum BPF MAX2837 for all those lower BW options.
|
||||
break;
|
||||
|
||||
case 4000000 ... 6000000: // BW capture range (500k ... 750kHz max ) fs_max = 8 x 750kHz = 6Mhz
|
||||
// BW 500k ... 750kHz , ex. 500kHz (fs = 8*BW = 4Mhz) , BW 600kHz (fs = 4,8Mhz) , BW 750 kHz (fs = 6Mhz)
|
||||
anti_alias_baseband_bandwidth_filter = 2500000; // in some IC MAX2837 appear 2250000 , but both works similar.
|
||||
break;
|
||||
|
||||
case 8800000: // BW capture 1,1Mhz fs = 8 x 1,1Mhz = 8,8Mhz . (1Mhz showed slightly higher noise background).
|
||||
anti_alias_baseband_bandwidth_filter = 3500000;
|
||||
break;
|
||||
|
||||
case 14000000: // BW capture 1,75Mhz , fs = 8 x 1,75Mhz = 14Mhz
|
||||
// good BPF ,good matching, but LCD making flicker , refresh rate should be < 20 Hz , but reasonable picture
|
||||
anti_alias_baseband_bandwidth_filter = 5000000;
|
||||
break;
|
||||
|
||||
case 16000000: // BW capture 2Mhz , fs = 8 x 2Mhz = 16Mhz
|
||||
// good BPF ,good matching, but LCD making flicker , refresh rate should be < 20 Hz , but reasonable picture
|
||||
anti_alias_baseband_bandwidth_filter = 6000000;
|
||||
break;
|
||||
|
||||
case 20000000: // BW capture 2,5Mhz , fs= 8 x 2,5 Mhz = 20Mhz
|
||||
// good BPF ,good matching, but LCD making flicker , refresh rate should be < 20 Hz , but reasonable picture
|
||||
anti_alias_baseband_bandwidth_filter = 7000000;
|
||||
break;
|
||||
|
||||
default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz= 22Mhz max ADC sampling) and others.
|
||||
// We tested also 9Mhz FPB stightly too much noise floor , better 8Mhz
|
||||
anti_alias_baseband_bandwidth_filter = 8000000;
|
||||
}
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
|
||||
waterfall.on_show();
|
||||
option_format.set_selected_index(0); // Default to C16
|
||||
option_format.on_change = [this](size_t, uint32_t file_type) {
|
||||
record_view.set_file_type((RecordView::FileType)file_type);
|
||||
};
|
||||
|
||||
option_bandwidth.set_selected_index(7); // 500k, Preselected starting default option 500kHz
|
||||
check_trim.on_select = [this](Checkbox&, bool v) {
|
||||
record_view.set_auto_trim(v);
|
||||
};
|
||||
|
||||
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
|
||||
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
|
||||
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
|
||||
auto sample_rate = bandwidth;
|
||||
|
||||
/* base_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
waterfall.stop();
|
||||
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
// NB: record_view is what actually updates proc_capture baseband settings.
|
||||
auto actual_sample_rate = record_view.set_sampling_rate(sample_rate);
|
||||
|
||||
// Update the radio model with the actual sampling rate.
|
||||
receiver_model.set_sampling_rate(actual_sample_rate);
|
||||
|
||||
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
|
||||
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
|
||||
|
||||
// Automatically switch default capture format to C8 when bandwidth setting is increased to >=1.5MHz
|
||||
if ((bandwidth >= 1500000) && (previous_bandwidth < 1500000)) {
|
||||
option_format.set_selected_index(1);
|
||||
}
|
||||
previous_bandwidth = bandwidth;
|
||||
|
||||
waterfall.start();
|
||||
};
|
||||
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
|
||||
receiver_model.enable();
|
||||
option_bandwidth.set_by_value(500000);
|
||||
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
@@ -133,28 +105,14 @@ CaptureAppView::CaptureAppView(NavigationView& nav) {
|
||||
}
|
||||
|
||||
CaptureAppView::~CaptureAppView() {
|
||||
// Hack for preventing halting other apps
|
||||
// TODO: This should be also part of something global
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
// ----------------------------
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void CaptureAppView::on_hide() {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
|
||||
void CaptureAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
@@ -162,8 +120,4 @@ void CaptureAppView::focus() {
|
||||
record_view.focus();
|
||||
}
|
||||
|
||||
void CaptureAppView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -26,25 +26,11 @@
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
|
||||
#define BW_OPTIONS \
|
||||
{" 8k5", 8500}, \
|
||||
{" 11k", 11000}, \
|
||||
{" 16k", 16000}, \
|
||||
{" 25k", 25000}, \
|
||||
{" 50k", 50000}, \
|
||||
{" 100k", 100000}, \
|
||||
{" 250k", 250000}, \
|
||||
{" 500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/ \
|
||||
{" 600k", 600000}, /* That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec) */ \
|
||||
{" 750k", 750000}, /* From that BW onwards, the LCD is ok, but the recorded file is auto decimated,(not real file size) */ \
|
||||
{"1100k", 1100000}, \
|
||||
{"1750k", 1750000}, \
|
||||
{"2000k", 2000000}, \
|
||||
{"2500k", 2500000}, \
|
||||
{"2750k", 2750000}, // That is our max Capture option , to keep using later / 8 decimation (22Mhz sampling ADC)
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -53,41 +39,38 @@ class CaptureAppView : public View {
|
||||
CaptureAppView(NavigationView& nav);
|
||||
~CaptureAppView();
|
||||
|
||||
void on_hide() override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void focus() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
std::string title() const override { return "Capture"; };
|
||||
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
uint32_t previous_bandwidth{500000};
|
||||
|
||||
uint32_t sampling_rate = 0;
|
||||
uint32_t anti_alias_baseband_bandwidth_filter = 2500000; // we rename the previous var , and change type static constexpr to normal var.
|
||||
|
||||
void on_tuning_frequency_changed(rf::Frequency f);
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{ReceiverModel::Mode::Capture};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_capture", app_settings::Mode::RX,
|
||||
app_settings::Options::UseGlobalTargetFrequency};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "Rate:", Color::light_grey()},
|
||||
{{11 * 8, 1 * 16}, "Format:", Color::light_grey()},
|
||||
};
|
||||
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
};
|
||||
nav_};
|
||||
|
||||
FrequencyStepView field_frequency_step{
|
||||
{10 * 8, 0 * 16},
|
||||
};
|
||||
{10 * 8, 0 * 16}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
@@ -101,7 +84,19 @@ class CaptureAppView : public View {
|
||||
OptionsField option_bandwidth{
|
||||
{5 * 8, 1 * 16},
|
||||
5,
|
||||
{BW_OPTIONS}};
|
||||
{}};
|
||||
|
||||
OptionsField option_format{
|
||||
{18 * 8, 1 * 16},
|
||||
3,
|
||||
{{"C16", RecordView::FileType::RawS16},
|
||||
{"C8", RecordView::FileType::RawS8}}};
|
||||
|
||||
Checkbox check_trim{
|
||||
{23 * 8, 1 * 16},
|
||||
4,
|
||||
"Trim",
|
||||
/*small*/ true};
|
||||
|
||||
RecordView record_view{
|
||||
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
|
||||
@@ -111,7 +106,7 @@ class CaptureAppView : public View {
|
||||
16384,
|
||||
3};
|
||||
|
||||
spectrum::WaterfallWidget waterfall{};
|
||||
spectrum::WaterfallView waterfall{};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -65,9 +65,13 @@ std::string commodity_type(CommodityType value) {
|
||||
|
||||
} /* namespace ert */
|
||||
|
||||
void ERTLogger::on_packet(const ert::Packet& packet) {
|
||||
void ERTLogger::on_packet(const ert::Packet& packet, const uint32_t target_frequency) {
|
||||
const auto formatted = packet.symbols_formatted();
|
||||
std::string entry = ert::format::type(packet.type()) + " " + formatted.data + "/" + formatted.errors;
|
||||
|
||||
// TODO: function doesn't take uint64_t, so when >= 1<<32, weirdness will ensue!
|
||||
const auto target_frequency_str = to_string_dec_uint(target_frequency, 10);
|
||||
|
||||
std::string entry = target_frequency_str + " " + ert::format::type(packet.type()) + " " + formatted.data + "/" + formatted.errors;
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
}
|
||||
|
||||
@@ -99,10 +103,12 @@ void RecentEntriesTable<ERTRecentEntries>::draw(
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
ERTAppView::ERTAppView(NavigationView&) {
|
||||
ERTAppView::ERTAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ert);
|
||||
|
||||
add_children({
|
||||
&field_frequency,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
@@ -110,28 +116,9 @@ ERTAppView::ERTAppView(NavigationView&) {
|
||||
&recent_entries_view,
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_ert", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
}
|
||||
field_frequency.set_step(1000000);
|
||||
|
||||
receiver_model.set_tuning_frequency(initial_target_frequency);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
|
||||
/* radio::enable({
|
||||
initial_target_frequency,
|
||||
sampling_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Receive,
|
||||
receiver_model.rf_amp(),
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga()),
|
||||
}); */
|
||||
receiver_model.enable();
|
||||
|
||||
logger = std::make_unique<ERTLogger>();
|
||||
if (logger) {
|
||||
@@ -140,11 +127,7 @@ ERTAppView::ERTAppView(NavigationView&) {
|
||||
}
|
||||
|
||||
ERTAppView::~ERTAppView() {
|
||||
// save app settings
|
||||
settings.save("rx_ert", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias and change flag enabled_
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
@@ -159,7 +142,7 @@ void ERTAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
|
||||
void ERTAppView::on_packet(const ert::Packet& packet) {
|
||||
if (logger) {
|
||||
logger->on_packet(packet);
|
||||
logger->on_packet(packet, receiver_model.target_frequency());
|
||||
}
|
||||
|
||||
if (packet.crc_ok()) {
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_channel.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "ert_packet.hpp"
|
||||
@@ -94,7 +96,7 @@ class ERTLogger {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(const ert::Packet& packet);
|
||||
void on_packet(const ert::Packet& packet, const uint32_t target_frequency);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
@@ -108,10 +110,6 @@ using ERTRecentEntriesView = RecentEntriesView<ERTRecentEntries>;
|
||||
|
||||
class ERTAppView : public View {
|
||||
public:
|
||||
static constexpr uint32_t initial_target_frequency = 911600000;
|
||||
static constexpr uint32_t sampling_rate = 4194304;
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
|
||||
ERTAppView(NavigationView& nav);
|
||||
~ERTAppView();
|
||||
|
||||
@@ -129,9 +127,13 @@ class ERTAppView : public View {
|
||||
ERTRecentEntries recent{};
|
||||
std::unique_ptr<ERTLogger> logger{};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
911600000 /* frequency */,
|
||||
2500000 /* bandwidth */,
|
||||
4194304 /* sampling rate */};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_ert", app_settings::Mode::RX};
|
||||
|
||||
const RecentEntriesColumns columns{{
|
||||
{"ID", 10},
|
||||
@@ -143,6 +145,10 @@ class ERTAppView : public View {
|
||||
|
||||
static constexpr auto header_height = 1 * 16;
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -40,54 +42,42 @@ void GpsSimAppView::set_ready() {
|
||||
ready_signal = true;
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_file_changed(std::filesystem::path new_file_path) {
|
||||
File data_file, info_file;
|
||||
char file_data[257];
|
||||
void GpsSimAppView::on_file_changed(const fs::path& new_file_path) {
|
||||
file_path = fs::path(u"/") + new_file_path;
|
||||
File::Size file_size{};
|
||||
|
||||
// Get file size
|
||||
auto data_open_error = data_file.open("/" + new_file_path.string());
|
||||
if (data_open_error.is_valid()) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
file_path = new_file_path;
|
||||
|
||||
// Get original record frequency if available
|
||||
std::filesystem::path info_file_path = file_path;
|
||||
info_file_path.replace_extension(u".TXT");
|
||||
|
||||
sample_rate = 500000;
|
||||
|
||||
auto info_open_error = info_file.open("/" + info_file_path.string());
|
||||
if (!info_open_error.is_valid()) {
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = info_file.read(file_data, 256);
|
||||
if (!read_size.is_error()) {
|
||||
auto pos1 = strstr(file_data, "center_frequency=");
|
||||
if (pos1) {
|
||||
pos1 += 17;
|
||||
field_frequency.set_value(strtoll(pos1, nullptr, 10));
|
||||
}
|
||||
|
||||
auto pos2 = strstr(file_data, "sample_rate=");
|
||||
if (pos2) {
|
||||
pos2 += 12;
|
||||
sample_rate = strtoll(pos2, nullptr, 10);
|
||||
}
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path);
|
||||
if (error) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
file_size = data_file.size();
|
||||
}
|
||||
|
||||
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 1) + "Hz");
|
||||
// Get original record frequency if available.
|
||||
auto metadata_path = get_metadata_path(file_path);
|
||||
auto metadata = read_metadata_file(metadata_path);
|
||||
|
||||
auto file_size = data_file.size();
|
||||
auto duration = (file_size * 1000) / (1 * 2 * sample_rate);
|
||||
if (metadata) {
|
||||
field_frequency.set_value(metadata->center_frequency);
|
||||
transmitter_model.set_sampling_rate(metadata->sample_rate);
|
||||
}
|
||||
|
||||
progressbar.set_max(file_size / 1024);
|
||||
text_filename.set(file_path.filename().string().substr(0, 12));
|
||||
// UI Fixup.
|
||||
text_sample_rate.set(unit_auto_scale(transmitter_model.sampling_rate(), 3, 1) + "Hz");
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
auto duration = ms_duration(file_size, transmitter_model.sampling_rate(), 2);
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
// TODO: fix in UI framework with 'try_focus()'?
|
||||
// Hack around focus getting called by ctor before parent is set.
|
||||
if (parent())
|
||||
button_play.focus();
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_tx_progress(const uint32_t progress) {
|
||||
@@ -129,7 +119,6 @@ void GpsSimAppView::start() {
|
||||
|
||||
if (reader) {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
baseband::set_sample_rate(sample_rate);
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
@@ -141,15 +130,7 @@ void GpsSimAppView::start() {
|
||||
});
|
||||
}
|
||||
|
||||
rf_amp = (transmitter_model.rf_amp()); // recover rf_amp settings applied from ui_transmiter.cpp
|
||||
|
||||
radio::enable({receiver_model.tuning_frequency(),
|
||||
sample_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())});
|
||||
transmitter_model.enable();
|
||||
}
|
||||
|
||||
void GpsSimAppView::stop(const bool do_loop) {
|
||||
@@ -159,7 +140,7 @@ void GpsSimAppView::stop(const bool do_loop) {
|
||||
if (do_loop && check_loop.value()) {
|
||||
start();
|
||||
} else {
|
||||
radio::disable();
|
||||
transmitter_model.disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
@@ -189,26 +170,12 @@ GpsSimAppView::GpsSimAppView(
|
||||
&text_duration,
|
||||
&progressbar,
|
||||
&field_frequency,
|
||||
&tx_view, // now it handles previous rfgain , rfamp.
|
||||
&tx_view, // now it handles previous rfgain, rfamp.
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
field_frequency.set_value(target_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(this->target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency.set_step(5000);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
@@ -224,9 +191,8 @@ GpsSimAppView::GpsSimAppView(
|
||||
}
|
||||
|
||||
GpsSimAppView::~GpsSimAppView() {
|
||||
radio::disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_hide() {
|
||||
@@ -245,17 +211,4 @@ void GpsSimAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_target_frequency_changed(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
}
|
||||
|
||||
void GpsSimAppView::set_target_frequency(const rf::Frequency new_value) {
|
||||
persistent_memory::set_tuned_frequency(new_value);
|
||||
;
|
||||
}
|
||||
|
||||
rf::Frequency GpsSimAppView::target_frequency() const {
|
||||
return persistent_memory::tuned_frequency();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -24,18 +24,18 @@
|
||||
#ifndef __GPS_SIM_APP_HPP__
|
||||
#define __GPS_SIM_APP_HPP__
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -52,23 +52,22 @@ class GpsSimAppView : public View {
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{
|
||||
1575420000 /* frequency */,
|
||||
15000000 /* bandwidth */,
|
||||
2600000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_gps", app_settings::Mode::TX};
|
||||
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
uint32_t sample_rate = 0;
|
||||
int32_t tx_gain{47};
|
||||
bool rf_amp{true}; // aux private var to store temporal, same as Replay App rf_amp user selection.
|
||||
static constexpr uint32_t baseband_bandwidth = 3000000; // filter bandwidth
|
||||
const size_t read_size{16384};
|
||||
const size_t buffer_count{3};
|
||||
|
||||
void on_file_changed(std::filesystem::path new_file_path);
|
||||
void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_file_changed(const std::filesystem::path& new_file_path);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
void set_target_frequency(const rf::Frequency new_value);
|
||||
rf::Frequency target_frequency() const;
|
||||
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
@@ -98,15 +97,13 @@ class GpsSimAppView : public View {
|
||||
ProgressBar progressbar{
|
||||
{18 * 8, 1 * 16, 12 * 8, 16}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
TxFrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
};
|
||||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
||||
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Used in Replay / GPS Simul / Playlist App)
|
||||
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
|
||||
};
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
@@ -119,7 +116,7 @@ class GpsSimAppView : public View {
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
|
||||
spectrum::WaterfallWidget waterfall{};
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error{
|
||||
Message::ID::ReplayThreadDone,
|
||||
|
||||
@@ -22,15 +22,15 @@
|
||||
|
||||
#include "pocsag_app.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace pocsag;
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "audio.hpp"
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency) {
|
||||
std::string entry = "Raw: F:" + to_string_dec_uint(frequency) + "Hz " +
|
||||
@@ -43,200 +43,309 @@ void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32
|
||||
log_file.write_entry(packet.timestamp(), entry);
|
||||
}
|
||||
|
||||
void POCSAGLogger::log_decoded(
|
||||
const pocsag::POCSAGPacket& packet,
|
||||
const std::string text) {
|
||||
log_file.write_entry(packet.timestamp(), text);
|
||||
void POCSAGLogger::log_decoded(Timestamp timestamp, const std::string& text) {
|
||||
log_file.write_entry(timestamp, text);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
void POCSAGAppView::update_freq(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
portapack::persistent_memory::set_tuned_frequency(f); // Maybe not ?
|
||||
POCSAGSettingsView::POCSAGSettingsView(
|
||||
NavigationView& nav,
|
||||
POCSAGSettings& settings)
|
||||
: settings_{settings} {
|
||||
add_children(
|
||||
{&labels,
|
||||
&check_log,
|
||||
&check_log_raw,
|
||||
&check_small_font,
|
||||
&check_hide_bad,
|
||||
&check_hide_addr_only,
|
||||
&opt_filter_mode,
|
||||
&field_filter_address,
|
||||
&button_save});
|
||||
|
||||
check_log.set_value(settings_.enable_logging);
|
||||
check_log_raw.set_value(settings_.enable_raw_log);
|
||||
check_small_font.set_value(settings_.enable_small_font);
|
||||
check_hide_bad.set_value(settings_.hide_bad_data);
|
||||
check_hide_addr_only.set_value(settings_.hide_addr_only);
|
||||
opt_filter_mode.set_by_value(settings_.filter_mode);
|
||||
field_filter_address.set_value(settings_.filter_address);
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
settings_.enable_logging = check_log.value();
|
||||
settings_.enable_raw_log = check_log_raw.value();
|
||||
settings_.enable_small_font = check_small_font.value();
|
||||
settings_.hide_bad_data = check_hide_bad.value();
|
||||
settings_.hide_addr_only = check_hide_addr_only.value();
|
||||
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
||||
settings_.filter_address = field_filter_address.to_integer();
|
||||
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
POCSAGAppView::POCSAGAppView(NavigationView& nav) {
|
||||
uint32_t ignore_address;
|
||||
POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
|
||||
add_children(
|
||||
{&rssi,
|
||||
&audio,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&field_squelch,
|
||||
&field_volume,
|
||||
&image_status,
|
||||
&text_packet_count,
|
||||
&widget_baud,
|
||||
&widget_bits,
|
||||
&widget_frames,
|
||||
&button_filter_last,
|
||||
&button_config,
|
||||
&console});
|
||||
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&audio,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&check_log,
|
||||
&field_volume,
|
||||
&check_ignore,
|
||||
&sym_ignore,
|
||||
&console});
|
||||
|
||||
// Set on_change before initialising the field
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_pocsag", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
field_frequency.set_value(app_settings.rx_frequency);
|
||||
} else
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
check_log.set_value(logging);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging = v;
|
||||
};
|
||||
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
field_volume.on_change = [this](int32_t v) {
|
||||
this->on_headphone_volume_changed(v);
|
||||
};
|
||||
|
||||
check_ignore.set_value(ignore);
|
||||
check_ignore.on_select = [this](Checkbox&, bool v) {
|
||||
ignore = v;
|
||||
};
|
||||
|
||||
ignore_address = persistent_memory::pocsag_ignore_address();
|
||||
for (size_t c = 0; c < 7; c++) {
|
||||
sym_ignore.set_sym(6 - c, ignore_address % 10);
|
||||
ignore_address /= 10;
|
||||
// No app settings, use fallbacks from pmem.
|
||||
if (!app_settings_.loaded()) {
|
||||
settings_.filter_address = pmem::pocsag_ignore_address();
|
||||
settings_.filter_mode = (settings_.filter_address == 0)
|
||||
? FILTER_NONE
|
||||
: FILTER_DROP;
|
||||
}
|
||||
|
||||
logger = std::make_unique<POCSAGLogger>();
|
||||
if (logger)
|
||||
logger->append(LOG_ROOT_DIR "/POCSAG.TXT");
|
||||
logger.append(LOG_ROOT_DIR "/POCSAG.TXT");
|
||||
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
|
||||
button_filter_last.on_select = [this](Button&) {
|
||||
if (settings_.filter_mode == FILTER_NONE)
|
||||
settings_.filter_mode = FILTER_DROP;
|
||||
settings_.filter_address = last_address;
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
button_config.on_select = [this](Button&) {
|
||||
nav_.push<POCSAGSettingsView>(settings_);
|
||||
nav_.set_on_pop([this]() { refresh_ui(); });
|
||||
};
|
||||
|
||||
refresh_ui();
|
||||
audio::output::start();
|
||||
audio::output::unmute();
|
||||
|
||||
receiver_model.enable();
|
||||
baseband::set_pocsag();
|
||||
}
|
||||
|
||||
POCSAGAppView::~POCSAGAppView() {
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
settings.save("rx_pocsag", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
|
||||
// Save ignored address
|
||||
persistent_memory::set_pocsag_ignore_address(sym_ignore.value_dec_u32());
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void POCSAGAppView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_headphone_volume_changed(int32_t v) {
|
||||
const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
receiver_model.set_headphone_volume(new_volume);
|
||||
POCSAGAppView::~POCSAGAppView() {
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
// Save pmem settings.
|
||||
pmem::set_pocsag_ignore_address(settings_.filter_address);
|
||||
pmem::set_pocsag_last_address(last_address); // For POCSAG TX.
|
||||
}
|
||||
|
||||
// Useless ?
|
||||
void POCSAGAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
void POCSAGAppView::refresh_ui() {
|
||||
// Set console font style.
|
||||
console.set_style(
|
||||
settings_.enable_small_font
|
||||
? &Styles::white_small
|
||||
: &Styles::white);
|
||||
|
||||
// Update filter button text.
|
||||
std::string btn_text = "Filter Last";
|
||||
switch (settings_.filter_mode) {
|
||||
case FILTER_DROP:
|
||||
btn_text = "Ignore Last";
|
||||
break;
|
||||
|
||||
case FILTER_KEEP:
|
||||
btn_text = "Keep Last";
|
||||
break;
|
||||
|
||||
case FILTER_NONE:
|
||||
default:
|
||||
btn_text = "Filter Last";
|
||||
break;
|
||||
}
|
||||
button_filter_last.set_text(btn_text);
|
||||
}
|
||||
|
||||
bool POCSAGAppView::ignore_address(uint32_t address) const {
|
||||
switch (settings_.filter_mode) {
|
||||
case FILTER_DROP:
|
||||
return address == settings_.filter_address;
|
||||
|
||||
case FILTER_KEEP:
|
||||
return address != settings_.filter_address;
|
||||
|
||||
case FILTER_NONE:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
|
||||
bool bad_data = pocsag_state.errors >= 3;
|
||||
|
||||
// Too many errors for reliable decode.
|
||||
if (bad_data && hide_bad_data()) {
|
||||
console.write("\n" STR_COLOR_MAGENTA + prefix + " Too many decode errors.");
|
||||
last_address = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore address? TODO: could filter earlier.
|
||||
if (ignore_address(pocsag_state.address)) {
|
||||
console.write("\n" STR_COLOR_CYAN + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
|
||||
last_address = pocsag_state.address;
|
||||
return;
|
||||
}
|
||||
|
||||
// Color indicates the message has a lot of decoding errors.
|
||||
std::string color = bad_data ? STR_COLOR_MAGENTA : STR_COLOR_WHITE;
|
||||
|
||||
std::string console_info = "\n" + color + prefix;
|
||||
console_info += " #" + to_string_dec_uint(pocsag_state.address);
|
||||
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
||||
|
||||
if (pocsag_state.out_type == ADDRESS) {
|
||||
last_address = pocsag_state.address;
|
||||
|
||||
if (!hide_addr_only()) {
|
||||
console.write(console_info);
|
||||
|
||||
if (logging()) {
|
||||
logger.log_decoded(
|
||||
timestamp,
|
||||
to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function));
|
||||
}
|
||||
}
|
||||
|
||||
} else if (pocsag_state.out_type == MESSAGE) {
|
||||
if (pocsag_state.address != last_address) {
|
||||
// New message
|
||||
last_address = pocsag_state.address;
|
||||
console.writeln(console_info);
|
||||
console.write(color + pocsag_state.output);
|
||||
} else {
|
||||
// Message continues...
|
||||
console.write(color + pocsag_state.output);
|
||||
}
|
||||
|
||||
if (logging()) {
|
||||
logger.log_decoded(
|
||||
timestamp,
|
||||
to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" " + pocsag_state.output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Color get_status_color(const POCSAGState& state) {
|
||||
if (state.out_type == IDLE)
|
||||
return Color::white();
|
||||
|
||||
switch (state.mode) {
|
||||
case STATE_CLEAR:
|
||||
return Color::cyan();
|
||||
case STATE_HAVE_ADDRESS:
|
||||
return Color::yellow();
|
||||
case STATE_GETTING_MSG:
|
||||
return Color::green();
|
||||
}
|
||||
|
||||
// Shouldn't get here...
|
||||
return Color::red();
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
||||
std::string alphanum_text = "";
|
||||
const uint32_t roundVal = 50;
|
||||
const uint32_t bitrate_rounded = roundVal * ((message->packet.bitrate() + (roundVal / 2)) / roundVal);
|
||||
auto bitrate = to_string_dec_uint(bitrate_rounded);
|
||||
auto timestamp = to_string_datetime(message->packet.timestamp(), HM);
|
||||
auto prefix = timestamp + " " + bitrate;
|
||||
|
||||
if (message->packet.flag() != NORMAL)
|
||||
console.writeln("\n\x1B\x0CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
||||
else {
|
||||
pocsag_decode_batch(message->packet, &pocsag_state);
|
||||
// Display packet count to be able to tell whether baseband sent a packet for a tone.
|
||||
++packet_count;
|
||||
text_packet_count.set(to_string_dec_uint(packet_count));
|
||||
|
||||
if ((ignore) && (pocsag_state.address == sym_ignore.value_dec_u32())) {
|
||||
// Ignore (inform, but no log)
|
||||
// console.write("\n\x1B\x03" + to_string_time(message->packet.timestamp()) +
|
||||
// " Ignored address " + to_string_dec_uint(pocsag_state.address));
|
||||
return;
|
||||
}
|
||||
// Too many errors for reliable decode
|
||||
if ((ignore) && (pocsag_state.errors >= 3)) {
|
||||
return;
|
||||
}
|
||||
if (logging_raw())
|
||||
logger.log_raw_data(message->packet, receiver_model.target_frequency());
|
||||
|
||||
std::string console_info;
|
||||
const uint32_t roundVal = 50;
|
||||
const uint32_t bitrate = roundVal * ((message->packet.bitrate() + (roundVal / 2)) / roundVal);
|
||||
console_info = "\n" + to_string_datetime(message->packet.timestamp(), HM);
|
||||
console_info += " " + to_string_dec_uint(bitrate);
|
||||
console_info += " ADDR:" + to_string_dec_uint(pocsag_state.address);
|
||||
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
||||
if (message->packet.flag() != NORMAL) {
|
||||
console.writeln("\n" STR_COLOR_RED + prefix + " CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
||||
last_address = 0;
|
||||
} else {
|
||||
// Set color before to be able to see if decode gets stuck.
|
||||
image_status.set_foreground(Color::magenta());
|
||||
pocsag_state.codeword_index = 0;
|
||||
pocsag_state.errors = 0;
|
||||
|
||||
// Store last received address for POCSAG TX
|
||||
persistent_memory::set_pocsag_last_address(pocsag_state.address);
|
||||
// Handle multiple messages (if any).
|
||||
while (pocsag_decode_batch(message->packet, pocsag_state))
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
|
||||
if (pocsag_state.out_type == ADDRESS) {
|
||||
// Address only
|
||||
|
||||
console.write(console_info);
|
||||
|
||||
if (logger && logging) {
|
||||
logger->log_decoded(message->packet, to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" Address only");
|
||||
}
|
||||
|
||||
last_address = pocsag_state.address;
|
||||
} else if (pocsag_state.out_type == MESSAGE) {
|
||||
if (pocsag_state.address != last_address) {
|
||||
// New message
|
||||
console.writeln(console_info);
|
||||
console.write(pocsag_state.output);
|
||||
|
||||
last_address = pocsag_state.address;
|
||||
} else {
|
||||
// Message continues...
|
||||
console.write(pocsag_state.output);
|
||||
}
|
||||
|
||||
if (logger && logging)
|
||||
logger->log_decoded(message->packet, to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" Alpha: " + pocsag_state.output);
|
||||
}
|
||||
// Handle the remainder.
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
}
|
||||
|
||||
// Log raw data whatever it contains
|
||||
if (logger && logging)
|
||||
logger->log_raw_data(message->packet, target_frequency());
|
||||
// Set status icon color to indicate state machine state.
|
||||
image_status.set_foreground(get_status_color(pocsag_state));
|
||||
}
|
||||
|
||||
void POCSAGAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
receiver_model.set_tuning_frequency(new_value);
|
||||
void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) {
|
||||
widget_baud.set_rate(stats->baud_rate);
|
||||
widget_bits.set_bits(stats->current_bits);
|
||||
widget_frames.set_frames(stats->current_frames);
|
||||
widget_frames.set_sync(stats->has_sync);
|
||||
}
|
||||
|
||||
uint32_t POCSAGAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
void BaudIndicator::paint(Painter& painter) {
|
||||
auto p = screen_pos();
|
||||
char top = '-';
|
||||
char bot = '-';
|
||||
|
||||
if (rate_ > 0) {
|
||||
auto r = rate_ / 100;
|
||||
top = (r / 10) + '0';
|
||||
bot = (r % 10) + '0';
|
||||
}
|
||||
|
||||
painter.draw_char(p, Styles::white_small, top);
|
||||
painter.draw_char({p.x(), p.y() + 8}, Styles::white_small, bot);
|
||||
}
|
||||
|
||||
void BitsIndicator::paint(Painter&) {
|
||||
auto p = screen_pos();
|
||||
for (size_t i = 0; i < sizeof(bits_) * 8; ++i) {
|
||||
auto is_set = ((bits_ >> i) & 0x1) == 1;
|
||||
|
||||
int x = p.x() + (i / height);
|
||||
int y = p.y() + (i % height);
|
||||
display.draw_pixel({x, y}, is_set ? Color::white() : Color::black());
|
||||
}
|
||||
}
|
||||
|
||||
void FrameIndicator::paint(Painter& painter) {
|
||||
auto p = screen_pos();
|
||||
painter.draw_rectangle({p, {2, height}}, has_sync_ ? Color::green() : Color::grey());
|
||||
|
||||
for (size_t i = 0; i < height; ++i) {
|
||||
auto p2 = p + Point{2, 15 - (int)i};
|
||||
painter.draw_hline(p2, 2, i < frame_count_ ? Color::white() : Color::black());
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -24,13 +24,18 @@
|
||||
#define __POCSAG_APP_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "pocsag.hpp"
|
||||
#include "pocsag_packet.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class POCSAGLogger {
|
||||
public:
|
||||
@@ -39,7 +44,7 @@ class POCSAGLogger {
|
||||
}
|
||||
|
||||
void log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency);
|
||||
void log_decoded(const pocsag::POCSAGPacket& packet, const std::string text);
|
||||
void log_decoded(Timestamp timestamp, const std::string& text);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
@@ -47,85 +52,246 @@ class POCSAGLogger {
|
||||
|
||||
namespace ui {
|
||||
|
||||
class BaudIndicator : public Widget {
|
||||
public:
|
||||
BaudIndicator(Point position)
|
||||
: Widget{{position, {5, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_rate(uint16_t rate) {
|
||||
if (rate != rate_) {
|
||||
rate_ = rate;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint16_t rate_ = 0;
|
||||
};
|
||||
|
||||
class BitsIndicator : public Widget {
|
||||
public:
|
||||
BitsIndicator(Point position)
|
||||
: Widget{{position, {2, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_bits(uint32_t bits) {
|
||||
if (bits != bits_) {
|
||||
bits_ = bits;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint32_t bits_ = 0;
|
||||
};
|
||||
|
||||
class FrameIndicator : public Widget {
|
||||
public:
|
||||
FrameIndicator(Point position)
|
||||
: Widget{{position, {4, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_frames(uint8_t frame_count) {
|
||||
if (frame_count != frame_count_) {
|
||||
frame_count_ = frame_count;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void set_sync(bool has_sync) {
|
||||
if (has_sync != has_sync_) {
|
||||
has_sync_ = has_sync;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint8_t frame_count_ = 0;
|
||||
bool has_sync_ = false;
|
||||
};
|
||||
|
||||
enum POCSAGFilter : uint8_t {
|
||||
FILTER_NONE,
|
||||
FILTER_DROP,
|
||||
FILTER_KEEP
|
||||
};
|
||||
|
||||
struct POCSAGSettings {
|
||||
bool enable_small_font = false;
|
||||
bool enable_logging = false;
|
||||
bool enable_raw_log = false;
|
||||
bool hide_bad_data = false;
|
||||
bool hide_addr_only = false;
|
||||
uint8_t filter_mode = false;
|
||||
uint32_t filter_address = 0;
|
||||
};
|
||||
|
||||
class POCSAGSettingsView : public View {
|
||||
public:
|
||||
POCSAGSettingsView(NavigationView& nav, POCSAGSettings& settings);
|
||||
|
||||
std::string title() const override { return "POCSAG Config"; };
|
||||
void focus() override { button_save.focus(); }
|
||||
|
||||
private:
|
||||
POCSAGSettings& settings_;
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 12 * 16}, "Filter Mode:", Color::light_grey()},
|
||||
{{2 * 8, 13 * 16}, "Filter Addr:", Color::light_grey()},
|
||||
};
|
||||
|
||||
Checkbox check_log{
|
||||
{2 * 8, 2 * 16},
|
||||
10,
|
||||
"Enable Log"};
|
||||
|
||||
Checkbox check_log_raw{
|
||||
{2 * 8, 4 * 16},
|
||||
12,
|
||||
"Log Raw Data"};
|
||||
|
||||
Checkbox check_small_font{
|
||||
{2 * 8, 6 * 16},
|
||||
4,
|
||||
"Use Small Font"};
|
||||
|
||||
Checkbox check_hide_bad{
|
||||
{2 * 8, 8 * 16},
|
||||
22,
|
||||
"Hide Bad Data"};
|
||||
|
||||
Checkbox check_hide_addr_only{
|
||||
{2 * 8, 10 * 16},
|
||||
22,
|
||||
"Hide Addr Only"};
|
||||
|
||||
OptionsField opt_filter_mode{
|
||||
{15 * 8, 12 * 16},
|
||||
4,
|
||||
{{"None", FILTER_NONE},
|
||||
{"Drop", FILTER_DROP},
|
||||
{"Keep", FILTER_KEEP}}};
|
||||
|
||||
SymField field_filter_address{
|
||||
{15 * 8, 13 * 16},
|
||||
7,
|
||||
SymField::Type::Dec,
|
||||
true /*explicit_edit*/};
|
||||
|
||||
Button button_save{
|
||||
{11 * 8, 16 * 16, 10 * 8, 2 * 16},
|
||||
"Save"};
|
||||
};
|
||||
|
||||
class POCSAGAppView : public View {
|
||||
public:
|
||||
POCSAGAppView(NavigationView& nav);
|
||||
~POCSAGAppView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
std::string title() const override { return "POCSAG RX"; };
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "POCSAG RX"; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 466175000;
|
||||
bool logging() const { return settings_.enable_logging; };
|
||||
bool logging_raw() const { return settings_.enable_raw_log; };
|
||||
bool hide_bad_data() const { return settings_.hide_bad_data; };
|
||||
bool hide_addr_only() const { return settings_.hide_addr_only; };
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
466175000 /* frequency*/,
|
||||
max283x::filter::bandwidth_minimum /* bandwidth */,
|
||||
3072000 /* sampling rate */
|
||||
};
|
||||
|
||||
bool logging{false};
|
||||
bool ignore{false};
|
||||
uint32_t last_address = 0xFFFFFFFF;
|
||||
pocsag::POCSAGState pocsag_state{};
|
||||
// Settings
|
||||
POCSAGSettings settings_{};
|
||||
app_settings::SettingsManager app_settings_{
|
||||
"rx_pocsag"sv,
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"small_font"sv, &settings_.enable_small_font},
|
||||
{"enable_logging"sv, &settings_.enable_logging},
|
||||
{"enable_raw_log"sv, &settings_.enable_raw_log},
|
||||
{"filter_mode"sv, &settings_.filter_mode},
|
||||
{"filter_address"sv, &settings_.filter_address},
|
||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
||||
}};
|
||||
|
||||
void refresh_ui();
|
||||
bool ignore_address(uint32_t address) const;
|
||||
void handle_decoded(Timestamp timestamp, const std::string& prefix);
|
||||
void on_packet(const POCSAGPacketMessage* message);
|
||||
void on_stats(const POCSAGStatsMessage* stats);
|
||||
|
||||
uint32_t last_address = 0;
|
||||
pocsag::EccContainer ecc{};
|
||||
pocsag::POCSAGState pocsag_state{&ecc};
|
||||
POCSAGLogger logger{};
|
||||
uint16_t packet_count = 0;
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 8},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
{11 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
{13 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
Audio audio{
|
||||
{21 * 8, 10, 6 * 8, 4},
|
||||
};
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
{0 * 8, 0 * 8},
|
||||
};
|
||||
NumberField field_volume{
|
||||
{28 * 8, 0 * 16},
|
||||
RSSI rssi{
|
||||
{19 * 8 - 4, 3, 6 * 8, 4}};
|
||||
Audio audio{
|
||||
{19 * 8 - 4, 8, 6 * 8, 4}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{25 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
true /*wrap*/};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
Checkbox check_ignore{
|
||||
{0 * 8, 21},
|
||||
8,
|
||||
"Ign addr",
|
||||
false};
|
||||
SymField sym_ignore{
|
||||
{13 * 8, 25},
|
||||
7,
|
||||
SymField::SYMFIELD_DEC};
|
||||
Checkbox check_log{
|
||||
{240 - 8 * 8, 21},
|
||||
3,
|
||||
"LOG",
|
||||
false};
|
||||
Image image_status{
|
||||
{0 * 8 + 4, 1 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_pocsag,
|
||||
Color::white(),
|
||||
Color::black()};
|
||||
|
||||
Text text_packet_count{
|
||||
{3 * 8, 1 * 16 + 2, 5 * 8, 16},
|
||||
"0"};
|
||||
|
||||
BitsIndicator widget_bits{
|
||||
{8 * 8 + 1, 1 * 16 + 2}};
|
||||
|
||||
FrameIndicator widget_frames{
|
||||
{8 * 8 + 4, 1 * 16 + 2}};
|
||||
|
||||
BaudIndicator widget_baud{
|
||||
{8 * 9 + 1, 1 * 16 + 2}};
|
||||
|
||||
Button button_filter_last{
|
||||
{10 * 8, 1 * 16, 12 * 8, 20},
|
||||
"Filter Last"};
|
||||
|
||||
Button button_config{
|
||||
{22 * 8, 1 * 16, 8 * 8, 20},
|
||||
"Config"};
|
||||
|
||||
// 54 == status bar (16) + top controls (2 * 16 + 6).
|
||||
Console console{
|
||||
{0, 3 * 16, 240, 256}};
|
||||
|
||||
std::unique_ptr<POCSAGLogger> logger{};
|
||||
|
||||
uint32_t target_frequency_ = initial_target_frequency;
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
|
||||
void on_packet(const POCSAGPacketMessage* message);
|
||||
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
{0, 2 * 16 + 6, screen_width, screen_height - 54}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::POCSAGPacket,
|
||||
@@ -133,6 +299,13 @@ class POCSAGAppView : public View {
|
||||
const auto message = static_cast<const POCSAGPacketMessage*>(p);
|
||||
this->on_packet(message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_stats{
|
||||
Message::ID::POCSAGStats,
|
||||
[this](Message* const p) {
|
||||
const auto stats = static_cast<const POCSAGStatsMessage*>(p);
|
||||
this->on_stats(stats);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -26,12 +26,16 @@
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "io_convert.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -39,51 +43,42 @@ void ReplayAppView::set_ready() {
|
||||
ready_signal = true;
|
||||
}
|
||||
|
||||
void ReplayAppView::on_file_changed(std::filesystem::path new_file_path) {
|
||||
File data_file, info_file;
|
||||
char file_data[257];
|
||||
void ReplayAppView::on_file_changed(const fs::path& new_file_path) {
|
||||
file_path = fs::path(u"/") + new_file_path;
|
||||
File::Size file_size{};
|
||||
|
||||
// Get file size
|
||||
auto data_open_error = data_file.open("/" + new_file_path.string());
|
||||
if (data_open_error.is_valid()) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
file_path = new_file_path;
|
||||
|
||||
// Get original record frequency if available
|
||||
std::filesystem::path info_file_path = file_path;
|
||||
info_file_path.replace_extension(u".TXT");
|
||||
|
||||
sample_rate = 500000;
|
||||
|
||||
auto info_open_error = info_file.open("/" + info_file_path.string());
|
||||
if (!info_open_error.is_valid()) {
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = info_file.read(file_data, 256);
|
||||
if (!read_size.is_error()) {
|
||||
auto pos1 = strstr(file_data, "center_frequency=");
|
||||
if (pos1) {
|
||||
pos1 += 17;
|
||||
field_frequency.set_value(strtoll(pos1, nullptr, 10));
|
||||
}
|
||||
|
||||
auto pos2 = strstr(file_data, "sample_rate=");
|
||||
if (pos2) {
|
||||
pos2 += 12;
|
||||
sample_rate = strtoll(pos2, nullptr, 10);
|
||||
}
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path);
|
||||
if (error) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
file_size = data_file.size();
|
||||
}
|
||||
|
||||
// Get original record frequency if available.
|
||||
auto metadata_path = get_metadata_path(file_path);
|
||||
auto metadata = read_metadata_file(metadata_path);
|
||||
|
||||
if (metadata) {
|
||||
field_frequency.set_value(metadata->center_frequency);
|
||||
sample_rate = metadata->sample_rate;
|
||||
} else {
|
||||
// TODO: This is interesting because it implies that the
|
||||
// The capture will just be replayed at the freq set on the
|
||||
// FrequencyField. Is that an intentional behavior?
|
||||
sample_rate = 500000;
|
||||
}
|
||||
|
||||
// UI Fixup.
|
||||
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 0) + "Hz");
|
||||
|
||||
auto file_size = data_file.size();
|
||||
auto duration = (file_size * 1000) / (2 * 2 * sample_rate);
|
||||
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(file_path.filename().string().substr(0, 12));
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
uint8_t sample_size = capture_file_sample_size(current()->path);
|
||||
auto duration = ms_duration(file_size, sample_rate, sample_size);
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
@@ -118,7 +113,7 @@ void ReplayAppView::start() {
|
||||
|
||||
std::unique_ptr<stream::Reader> reader;
|
||||
|
||||
auto p = std::make_unique<FileReader>();
|
||||
auto p = std::make_unique<FileConvertReader>();
|
||||
auto open_error = p->open(file_path);
|
||||
if (open_error.is_valid()) {
|
||||
file_error();
|
||||
@@ -129,7 +124,7 @@ void ReplayAppView::start() {
|
||||
|
||||
if (reader) {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
baseband::set_sample_rate(sample_rate * 8);
|
||||
baseband::set_sample_rate(sample_rate, OversampleRate::x8);
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
@@ -141,18 +136,9 @@ void ReplayAppView::start() {
|
||||
});
|
||||
}
|
||||
|
||||
// Enable Bias Tee if selected
|
||||
radio::set_antenna_bias(portapack::get_antenna_bias());
|
||||
|
||||
rf_amp = (transmitter_model.rf_amp()); // recover rf_amp settings applied from ui_transmiter.cpp
|
||||
|
||||
radio::enable({receiver_model.tuning_frequency(),
|
||||
sample_rate * 8,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())});
|
||||
transmitter_model.set_sampling_rate(sample_rate * toUType(OversampleRate::x8));
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
|
||||
if (portapack::persistent_memory::stealth_mode()) {
|
||||
DisplaySleepMessage message;
|
||||
@@ -167,8 +153,7 @@ void ReplayAppView::stop(const bool do_loop) {
|
||||
if (do_loop && check_loop.value()) {
|
||||
start();
|
||||
} else {
|
||||
radio::set_antenna_bias(false); // Turn off Bias Tee
|
||||
radio::disable();
|
||||
transmitter_model.disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
@@ -198,48 +183,27 @@ ReplayAppView::ReplayAppView(
|
||||
&text_duration,
|
||||
&progressbar,
|
||||
&field_frequency,
|
||||
&tx_view, // now it handles previous rfgain , rfamp.
|
||||
&tx_view, // now it handles previous rfgain, rfamp.
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
field_frequency.set_value(target_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(this->target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency.set_step(5000);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
button_open.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".C16");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
auto open_view = nav.push<FileLoadView>(".C*");
|
||||
open_view->on_changed = [this](fs::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
ReplayAppView::~ReplayAppView() {
|
||||
radio::disable();
|
||||
|
||||
display.fill_rectangle({0, 0, 240, 320}, Color::black()); // Solving sometimes visible bottom waterfall artifacts, clearing all LCD pixels.
|
||||
chThdSleepMilliseconds(40); // (that happened sometimes if we interrupt the waterfall play at the beggining of the play around 25% and exit )
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX reception ok, without "ghost interference signal problem" at the exit .
|
||||
|
||||
baseband::shutdown(); // better this function at the end, after load_sram(). If not , sometimes produced hang up (now not , it is ok).
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void ReplayAppView::on_hide() {
|
||||
@@ -257,17 +221,4 @@ void ReplayAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void ReplayAppView::on_target_frequency_changed(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
}
|
||||
|
||||
void ReplayAppView::set_target_frequency(const rf::Frequency new_value) {
|
||||
persistent_memory::set_tuned_frequency(new_value);
|
||||
;
|
||||
}
|
||||
|
||||
rf::Frequency ReplayAppView::target_frequency() const {
|
||||
return persistent_memory::tuned_frequency();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -23,18 +23,18 @@
|
||||
#ifndef __REPLAY_APP_HPP__
|
||||
#define __REPLAY_APP_HPP__
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -51,6 +51,9 @@ class ReplayAppView : public View {
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_replay", app_settings::Mode::TX};
|
||||
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
@@ -61,13 +64,9 @@ class ReplayAppView : public View {
|
||||
const size_t read_size{16384};
|
||||
const size_t buffer_count{3};
|
||||
|
||||
void on_file_changed(std::filesystem::path new_file_path);
|
||||
void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_file_changed(const std::filesystem::path& new_file_path);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
void set_target_frequency(const rf::Frequency new_value);
|
||||
rf::Frequency target_frequency() const;
|
||||
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
@@ -97,15 +96,13 @@ class ReplayAppView : public View {
|
||||
ProgressBar progressbar{
|
||||
{18 * 8, 1 * 16, 12 * 8, 16}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
TxFrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
};
|
||||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
||||
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Uused in Repay / GPS Simul / Playlist App)
|
||||
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
|
||||
};
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
@@ -118,7 +115,7 @@ class ReplayAppView : public View {
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
|
||||
spectrum::WaterfallWidget waterfall{};
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error{
|
||||
Message::ID::ReplayThreadDone,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "soundboard_app.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "tonesets.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "ui_tone_key.hpp"
|
||||
|
||||
using namespace tonekey;
|
||||
using namespace portapack;
|
||||
@@ -106,6 +106,7 @@ void SoundBoardView::start_tx(const uint32_t id) {
|
||||
EventDispatcher::send_message(message);
|
||||
});
|
||||
|
||||
// TODO: Delete all this and use tx model.
|
||||
baseband::set_audiotx_config(
|
||||
1536000 / 20, // Update vu-meter at 20Hz
|
||||
transmitter_model.channel_bandwidth(),
|
||||
@@ -119,8 +120,6 @@ void SoundBoardView::start_tx(const uint32_t id) {
|
||||
);
|
||||
baseband::set_sample_rate(sample_rate);
|
||||
|
||||
transmitter_model.set_sampling_rate(1536000);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
tx_view.set_transmitting(true);
|
||||
@@ -235,15 +234,6 @@ SoundBoardView::SoundBoardView(
|
||||
&button_next_page,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_soundboard", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
refresh_list();
|
||||
|
||||
button_next_page.on_select = [this](Button&) {
|
||||
@@ -266,9 +256,9 @@ SoundBoardView::SoundBoardView(
|
||||
check_random.set_value(false);
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -283,14 +273,9 @@ SoundBoardView::SoundBoardView(
|
||||
}
|
||||
|
||||
SoundBoardView::~SoundBoardView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_soundboard", &app_settings);
|
||||
|
||||
stop();
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit.
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
#include "baseband_api.hpp"
|
||||
#include "lfsr_random.hpp"
|
||||
#include "io_wave.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -49,11 +49,15 @@ class SoundBoardView : public View {
|
||||
std::string title() const override { return "Soundbrd TX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
1536000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_soundboard", app_settings::Mode::TX};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
NavigationView& nav_;
|
||||
|
||||
enum tx_modes {
|
||||
NORMAL = 0,
|
||||
|
||||
@@ -32,7 +32,7 @@ class SpectrumAnalysisModel {
|
||||
|
||||
namespace ui {
|
||||
|
||||
class SpectrumAnalysisView : public spectrum::WaterfallWidget {
|
||||
class SpectrumAnalysisView : public spectrum::WaterfallView {
|
||||
public:
|
||||
private:
|
||||
SpectrumAnalysisModel model;
|
||||
|
||||
@@ -34,9 +34,6 @@ namespace tpms {
|
||||
|
||||
namespace format {
|
||||
|
||||
static bool use_kpa = true;
|
||||
static bool use_celsius = true;
|
||||
|
||||
std::string type(Reading::Type type) {
|
||||
return to_string_dec_uint(toUType(type), 2);
|
||||
}
|
||||
@@ -46,11 +43,11 @@ std::string id(TransponderID id) {
|
||||
}
|
||||
|
||||
std::string pressure(Pressure pressure) {
|
||||
return to_string_dec_int(use_kpa ? pressure.kilopascal() : pressure.psi(), 3);
|
||||
return to_string_dec_int(units_psi ? pressure.psi() : pressure.kilopascal(), 3);
|
||||
}
|
||||
|
||||
std::string temperature(Temperature temperature) {
|
||||
return to_string_dec_int(use_celsius ? temperature.celsius() : temperature.fahrenheit(), 3);
|
||||
return to_string_dec_int(units_fahr ? temperature.fahrenheit() : temperature.celsius(), 3);
|
||||
}
|
||||
|
||||
std::string flags(Flags flags) {
|
||||
@@ -78,9 +75,9 @@ void TPMSLogger::on_packet(const tpms::Packet& packet, const uint32_t target_fre
|
||||
const auto hex_formatted = packet.symbols_formatted();
|
||||
|
||||
// TODO: function doesn't take uint64_t, so when >= 1<<32, weirdness will ensue!
|
||||
const auto tuning_frequency_str = to_string_dec_uint(target_frequency, 10);
|
||||
const auto target_frequency_str = to_string_dec_uint(target_frequency, 10);
|
||||
|
||||
std::string entry = tuning_frequency_str + " " + tpms::format::signal_type(packet.signal_type()) + " " + hex_formatted.data + "/" + hex_formatted.errors;
|
||||
std::string entry = target_frequency_str + " " + tpms::format::signal_type(packet.signal_type()) + " " + hex_formatted.data + "/" + hex_formatted.errors;
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
}
|
||||
|
||||
@@ -157,49 +154,24 @@ TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
&field_vga,
|
||||
&recent_entries_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_tpms", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
options_band.set_by_value(app_settings.rx_frequency);
|
||||
} else
|
||||
options_band.set_by_value(receiver_model.tuning_frequency());
|
||||
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
|
||||
/* radio::enable({
|
||||
tuning_frequency(),
|
||||
sampling_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Receive,
|
||||
receiver_model.rf_amp(),
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga()),
|
||||
}); */
|
||||
receiver_model.enable();
|
||||
|
||||
options_band.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_band_changed(v);
|
||||
receiver_model.set_target_frequency(v);
|
||||
};
|
||||
options_band.set_by_value(target_frequency());
|
||||
options_band.set_by_value(receiver_model.target_frequency());
|
||||
|
||||
options_pressure.on_change = [this](size_t, int32_t i) {
|
||||
tpms::format::use_kpa = !i;
|
||||
tpms::format::units_psi = (bool)i;
|
||||
update_view();
|
||||
};
|
||||
|
||||
options_pressure.set_selected_index(0, true);
|
||||
options_pressure.set_selected_index(tpms::format::units_psi, true);
|
||||
|
||||
options_temperature.on_change = [this](size_t, int32_t i) {
|
||||
tpms::format::use_celsius = !i;
|
||||
tpms::format::units_fahr = (bool)i;
|
||||
update_view();
|
||||
};
|
||||
|
||||
options_temperature.set_selected_index(0, true);
|
||||
options_temperature.set_selected_index(tpms::format::units_fahr, true);
|
||||
|
||||
logger = std::make_unique<TPMSLogger>();
|
||||
if (logger) {
|
||||
@@ -208,12 +180,7 @@ TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
}
|
||||
|
||||
TPMSAppView::~TPMSAppView() {
|
||||
// save app settings
|
||||
app_settings.rx_frequency = target_frequency_;
|
||||
settings.save("rx_tpms", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias and change flag enabled_
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
@@ -235,7 +202,7 @@ void TPMSAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
|
||||
void TPMSAppView::on_packet(const tpms::Packet& packet) {
|
||||
if (logger) {
|
||||
logger->on_packet(packet, target_frequency());
|
||||
logger->on_packet(packet, receiver_model.target_frequency());
|
||||
}
|
||||
|
||||
const auto reading_opt = packet.reading();
|
||||
@@ -252,21 +219,4 @@ void TPMSAppView::on_show_list() {
|
||||
recent_entries_view.focus();
|
||||
}
|
||||
|
||||
void TPMSAppView::on_band_changed(const uint32_t new_band_frequency) {
|
||||
set_target_frequency(new_band_frequency);
|
||||
}
|
||||
|
||||
void TPMSAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
radio::set_tuning_frequency(tuning_frequency());
|
||||
}
|
||||
|
||||
uint32_t TPMSAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
}
|
||||
|
||||
uint32_t TPMSAppView::tuning_frequency() const {
|
||||
return target_frequency() - (sampling_rate / 4);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_channel.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "event_m0.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
@@ -36,11 +37,18 @@
|
||||
|
||||
#include "tpms_packet.hpp"
|
||||
|
||||
namespace std {
|
||||
namespace tpms {
|
||||
|
||||
constexpr bool operator==(const tpms::TransponderID& lhs, const tpms::TransponderID& rhs) {
|
||||
return (lhs.value() == rhs.value());
|
||||
}
|
||||
namespace format {
|
||||
|
||||
static bool units_psi{false};
|
||||
static bool units_fahr{false};
|
||||
|
||||
} /* namespace format */
|
||||
|
||||
} /* namespace tpms */
|
||||
|
||||
namespace std {
|
||||
|
||||
} /* namespace std */
|
||||
|
||||
@@ -105,13 +113,18 @@ class TPMSAppView : public View {
|
||||
std::string title() const override { return "TPMS RX"; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 315000000;
|
||||
static constexpr uint32_t sampling_rate = 2457600;
|
||||
static constexpr uint32_t baseband_bandwidth = 1750000;
|
||||
RxRadioState radio_state_{
|
||||
314950000 /* frequency*/,
|
||||
1750000 /* bandwidth */,
|
||||
2457600 /* sampling rate */};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_tpms",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"units_psi"sv, &tpms::format::units_psi},
|
||||
{"units_fahr"sv, &tpms::format::units_fahr},
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::TPMSPacket,
|
||||
@@ -133,12 +146,13 @@ class TPMSAppView : public View {
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
|
||||
// "315 MHz" TPMS sensors transmit at either 314.9 or 315 MHz but we should pick up either
|
||||
OptionsField options_band{
|
||||
{0 * 8, 0 * 16},
|
||||
3,
|
||||
{
|
||||
{"315", 315000000},
|
||||
{"433", 433920000},
|
||||
{"315", 314950000},
|
||||
{"434", 433920000},
|
||||
}};
|
||||
|
||||
OptionsField options_pressure{
|
||||
@@ -149,9 +163,9 @@ class TPMSAppView : public View {
|
||||
|
||||
OptionsField options_temperature{
|
||||
{9 * 8, 0 * 16},
|
||||
1,
|
||||
{{"C", 0},
|
||||
{"F", 1}}};
|
||||
2,
|
||||
{{STR_DEGREES_C, 0},
|
||||
{STR_DEGREES_F, 1}}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
@@ -175,18 +189,9 @@ class TPMSAppView : public View {
|
||||
}};
|
||||
TPMSRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
uint32_t target_frequency_ = initial_target_frequency;
|
||||
|
||||
void on_packet(const tpms::Packet& packet);
|
||||
void on_show_list();
|
||||
void update_view();
|
||||
|
||||
void on_band_changed(const uint32_t new_band_frequency);
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
|
||||
uint32_t tuning_frequency() const;
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ using namespace portapack;
|
||||
namespace ui {
|
||||
|
||||
void AboutView::on_show() {
|
||||
transmitter_model.set_tuning_frequency(1337000000); // TODO: Change
|
||||
transmitter_model.set_target_frequency(1337000000); // TODO: Change
|
||||
transmitter_model.set_baseband_configuration({
|
||||
.mode = 0,
|
||||
.sampling_rate = 1536000,
|
||||
@@ -56,7 +56,6 @@ void AboutView::on_show() {
|
||||
transmitter_model.set_rf_amp(true);
|
||||
transmitter_model.set_lna(40);
|
||||
transmitter_model.set_vga(40);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_audiotx_data(32, 50, false, 0);
|
||||
@@ -178,7 +177,7 @@ void AboutView::draw_demoglyph(ui::Point p, char ch, ui::Color* pal) {
|
||||
int16_t lbx, il;
|
||||
|
||||
// Map ASCII to font bitmap
|
||||
if ((ch >= 32) || (ch < 96))
|
||||
if ((ch >= 32) && (ch < 96))
|
||||
che = char_map[ch - 32];
|
||||
else
|
||||
che = 0xFF;
|
||||
|
||||
@@ -8,7 +8,7 @@ AboutView::AboutView(NavigationView& nav) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
console.writeln("\x1B\x07List of contributors:\x1B\x10");
|
||||
console.writeln(STR_COLOR_LIGHT_GREY "List of contributors:");
|
||||
console.writeln("");
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ void AboutView::update() {
|
||||
case 1:
|
||||
// TODO: Generate this automatically from github
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2022-01-01&from=2020-04-12&type=c
|
||||
console.writeln("\x1B\x06Mayhem:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "Mayhem:");
|
||||
console.writeln("eried,euquiq,gregoryfenton");
|
||||
console.writeln("johnelder,jwetzell,nnemanjan00");
|
||||
console.writeln("N0vaPixel,klockee,GullCode");
|
||||
@@ -33,12 +33,17 @@ void AboutView::update() {
|
||||
console.writeln("heurist1,intoxsick,ckuethe");
|
||||
console.writeln("notpike,jLynx,zigad");
|
||||
console.writeln("MichalLeonBorsuk,jimilinuxguy");
|
||||
console.writeln("kallanreed,bernd-herzog");
|
||||
break;
|
||||
case 2:
|
||||
console.writeln("NotherNgineer,zxkmm,u-foka");
|
||||
console.writeln("Netro,HTotoo");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 2:
|
||||
case 3:
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2020-04-12&from=2015-07-31&type=c
|
||||
console.writeln("\x1B\x06Havoc:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "Havoc:");
|
||||
console.writeln("furrtek,mrmookie,NotPike");
|
||||
console.writeln("mjwaxios,ImDroided,Giorgiofox");
|
||||
console.writeln("F4GEV,z4ziggy,xmycroftx");
|
||||
@@ -49,16 +54,16 @@ void AboutView::update() {
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 3:
|
||||
case 4:
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?from=2014-07-05&to=2015-07-31&type=c
|
||||
console.writeln("\x1B\x06PortaPack:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "PortaPack:");
|
||||
console.writeln("jboone,argilo");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 4:
|
||||
case 5:
|
||||
// https://github.com/mossmann/hackrf/graphs/contributors
|
||||
console.writeln("\x1B\x06HackRF:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "HackRF:");
|
||||
console.writeln("mossmann,dominicgs,bvernoux");
|
||||
console.writeln("bgamari,schneider42,miek");
|
||||
console.writeln("willcode,hessu,Sec42");
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -20,46 +21,50 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <strings.h>
|
||||
#include <algorithm>
|
||||
|
||||
#include "ui_adsb_rx.hpp"
|
||||
#include "ui_alphanum.hpp"
|
||||
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
static std::string get_map_tag(const AircraftRecentEntry& entry) {
|
||||
return trimr(entry.callsign.empty() ? entry.icao_str : entry.callsign);
|
||||
}
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
char aged_color;
|
||||
Color target_color;
|
||||
auto entry_age = entry.age;
|
||||
std::string entry_string;
|
||||
|
||||
// Color decay for flights not being updated anymore
|
||||
if (entry_age < ADSB_CURRENT) {
|
||||
aged_color = 0x10;
|
||||
target_color = Color::green();
|
||||
} else if (entry_age < ADSB_RECENT) {
|
||||
aged_color = 0x07;
|
||||
target_color = Color::light_grey();
|
||||
} else {
|
||||
aged_color = 0x08;
|
||||
target_color = Color::dark_grey();
|
||||
}
|
||||
switch (entry.state) {
|
||||
case ADSBAgeState::Invalid:
|
||||
case ADSBAgeState::Current:
|
||||
entry_string = "";
|
||||
target_color = Color::green();
|
||||
break;
|
||||
case ADSBAgeState::Recent:
|
||||
entry_string = STR_COLOR_LIGHT_GREY;
|
||||
target_color = Color::light_grey();
|
||||
break;
|
||||
default:
|
||||
entry_string = STR_COLOR_DARK_GREY;
|
||||
target_color = Color::grey();
|
||||
};
|
||||
|
||||
std::string entry_string = "\x1B";
|
||||
entry_string += aged_color;
|
||||
entry_string +=
|
||||
(entry.callsign[0] != ' ' ? entry.callsign + " " : entry.icaoStr + " ") +
|
||||
(entry.callsign.empty() ? entry.icao_str + " " : entry.callsign + " ") +
|
||||
to_string_dec_uint((unsigned int)(entry.pos.altitude / 100), 4) +
|
||||
to_string_dec_uint((unsigned int)entry.velo.speed, 4) +
|
||||
to_string_dec_uint((unsigned int)(entry.amp >> 9), 4) + " " +
|
||||
@@ -72,47 +77,59 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
entry_string);
|
||||
|
||||
if (entry.pos.valid)
|
||||
painter.draw_bitmap(target_rect.location() + Point(8 * 8, 0), bitmap_target, target_color, style.background);
|
||||
painter.draw_bitmap(target_rect.location() + Point(8 * 8, 0),
|
||||
bitmap_target, target_color, style.background);
|
||||
}
|
||||
|
||||
void ADSBLogger::log_str(std::string& logline) {
|
||||
log_file.write_entry(logline);
|
||||
/* ADSBLogger ********************************************/
|
||||
|
||||
void ADSBLogger::log(const ADSBLogEntry& log_entry) {
|
||||
std::string log_line;
|
||||
log_line.reserve(100);
|
||||
|
||||
log_line = log_entry.raw_data;
|
||||
log_line += "ICAO:" + log_entry.icao;
|
||||
|
||||
if (!log_entry.callsign.empty())
|
||||
log_line += " " + log_entry.callsign;
|
||||
|
||||
if (log_entry.pos.valid)
|
||||
log_line += " Alt:" + to_string_dec_int(log_entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(log_entry.pos.latitude, 7) +
|
||||
" Lon:" + to_string_decimal(log_entry.pos.longitude, 7);
|
||||
|
||||
if (log_entry.vel.valid)
|
||||
log_line += " Type:" + to_string_dec_uint(log_entry.vel_type) +
|
||||
" Hdg:" + to_string_dec_uint(log_entry.vel.heading) +
|
||||
" Spd: " + to_string_dec_int(log_entry.vel.speed);
|
||||
|
||||
log_file.write_entry(log_line);
|
||||
}
|
||||
|
||||
// Aircraft Details
|
||||
void ADSBRxAircraftDetailsView::focus() {
|
||||
button_close.focus();
|
||||
}
|
||||
|
||||
ADSBRxAircraftDetailsView::~ADSBRxAircraftDetailsView() {
|
||||
on_close_();
|
||||
}
|
||||
/* ADSBRxAircraftDetailsView *****************************/
|
||||
|
||||
ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
NavigationView& nav,
|
||||
const AircraftRecentEntry& entry,
|
||||
const std::function<void(void)> on_close)
|
||||
: entry_copy(entry),
|
||||
on_close_(on_close) {
|
||||
add_children({&labels,
|
||||
&text_icao_address,
|
||||
&text_registration,
|
||||
&text_manufacturer,
|
||||
&text_model,
|
||||
&text_type,
|
||||
&text_number_of_engines,
|
||||
&text_engine_type,
|
||||
&text_owner,
|
||||
&text_operator,
|
||||
&button_close});
|
||||
const AircraftRecentEntry& entry) {
|
||||
add_children(
|
||||
{&labels,
|
||||
&text_icao_address,
|
||||
&text_registration,
|
||||
&text_manufacturer,
|
||||
&text_model,
|
||||
&text_type,
|
||||
&text_number_of_engines,
|
||||
&text_engine_type,
|
||||
&text_owner,
|
||||
&text_operator,
|
||||
&button_close});
|
||||
|
||||
std::unique_ptr<ADSBLogger> logger{};
|
||||
|
||||
icao_code = to_string_hex(entry_copy.ICAO_address, 6);
|
||||
text_icao_address.set(to_string_hex(entry_copy.ICAO_address, 6));
|
||||
text_icao_address.set(entry.icao_str);
|
||||
|
||||
// Try getting the aircraft information from icao24.db
|
||||
return_code = db.retrieve_aircraft_record(&aircraft_record, icao_code);
|
||||
std::database db{};
|
||||
std::database::AircraftDBRecord aircraft_record;
|
||||
auto return_code = db.retrieve_aircraft_record(&aircraft_record, entry.icao_str);
|
||||
switch (return_code) {
|
||||
case DATABASE_RECORD_FOUND:
|
||||
text_registration.set(aircraft_record.aircraft_registration);
|
||||
@@ -120,6 +137,7 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_model.set(aircraft_record.aircraft_model);
|
||||
text_owner.set(aircraft_record.aircraft_owner);
|
||||
text_operator.set(aircraft_record.aircraft_operator);
|
||||
|
||||
// Check for ICAO type, e.g. L2J
|
||||
if (strlen(aircraft_record.icao_type) == 3) {
|
||||
switch (aircraft_record.icao_type[0]) {
|
||||
@@ -142,7 +160,8 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_type.set("Tilt-wing aircraft");
|
||||
break;
|
||||
}
|
||||
text_number_of_engines.set(std::string(1, aircraft_record.icao_type[1]));
|
||||
|
||||
text_number_of_engines.set(std::string{1, aircraft_record.icao_type[1]});
|
||||
switch (aircraft_record.icao_type[2]) {
|
||||
case 'P':
|
||||
text_engine_type.set("Piston engine");
|
||||
@@ -157,8 +176,8 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_engine_type.set("Electric engine");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Check for ICAO type designator
|
||||
else if (strlen(aircraft_record.icao_type) == 4) {
|
||||
if (strcmp(aircraft_record.icao_type, "SHIP") == 0)
|
||||
@@ -179,75 +198,49 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_type.set("Powered parachute/paraplane");
|
||||
}
|
||||
break;
|
||||
|
||||
case DATABASE_NOT_FOUND:
|
||||
text_manufacturer.set("No icao24.db file");
|
||||
break;
|
||||
}
|
||||
|
||||
button_close.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
};
|
||||
|
||||
// End of Aicraft details
|
||||
|
||||
void ADSBRxDetailsView::focus() {
|
||||
button_see_map.focus();
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) {
|
||||
entry_copy = entry;
|
||||
uint32_t age = entry_copy.age;
|
||||
|
||||
if (age < 60)
|
||||
text_last_seen.set(to_string_dec_uint(age) + " seconds ago");
|
||||
else
|
||||
text_last_seen.set(to_string_dec_uint(age / 60) + " minutes ago");
|
||||
|
||||
text_infos.set(entry_copy.info_string);
|
||||
if (entry_copy.velo.heading < 360 && entry_copy.velo.speed >= 0) { // I don't like this but...
|
||||
text_info2.set("Hdg:" + to_string_dec_uint(entry_copy.velo.heading) + " Spd:" + to_string_dec_int(entry_copy.velo.speed));
|
||||
} else {
|
||||
text_info2.set("");
|
||||
}
|
||||
text_frame_pos_even.set(to_string_hex_array(entry_copy.frame_pos_even.get_raw_data(), 14));
|
||||
text_frame_pos_odd.set(to_string_hex_array(entry_copy.frame_pos_odd.get_raw_data(), 14));
|
||||
|
||||
if (send_updates) {
|
||||
geomap_view->update_tag(trimr(entry.callsign[0] != ' ' ? entry.callsign : to_string_hex(entry.ICAO_address, 6)));
|
||||
geomap_view->update_position(entry_copy.pos.latitude, entry_copy.pos.longitude, entry_copy.velo.heading, entry_copy.pos.altitude);
|
||||
}
|
||||
void ADSBRxAircraftDetailsView::focus() {
|
||||
button_close.focus();
|
||||
}
|
||||
|
||||
ADSBRxDetailsView::~ADSBRxDetailsView() {
|
||||
on_close_();
|
||||
}
|
||||
/* ADSBRxDetailsView *************************************/
|
||||
|
||||
ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
NavigationView& nav,
|
||||
const AircraftRecentEntry& entry,
|
||||
const std::function<void(void)> on_close)
|
||||
: entry_copy(entry),
|
||||
on_close_(on_close) {
|
||||
add_children({&labels,
|
||||
&text_icao_address,
|
||||
&text_callsign,
|
||||
&text_last_seen,
|
||||
&text_airline,
|
||||
&text_country,
|
||||
&text_infos,
|
||||
&text_info2,
|
||||
&text_frame_pos_even,
|
||||
&text_frame_pos_odd,
|
||||
&button_aircraft_details,
|
||||
&button_see_map});
|
||||
const AircraftRecentEntry& entry)
|
||||
: entry_(entry) {
|
||||
add_children(
|
||||
{&labels,
|
||||
&text_icao_address,
|
||||
&text_callsign,
|
||||
&text_last_seen,
|
||||
&text_airline,
|
||||
&text_country,
|
||||
&text_infos,
|
||||
&text_info2,
|
||||
&text_frame_pos_even,
|
||||
&text_frame_pos_odd,
|
||||
&button_aircraft_details,
|
||||
&button_see_map});
|
||||
|
||||
std::unique_ptr<ADSBLogger> logger{};
|
||||
update(entry_copy);
|
||||
// The following won't change for a given airborne aircraft.
|
||||
// Try getting the airline's name from airlines.db.
|
||||
// NB: Only works once callsign has been read and won't be updated.
|
||||
std::database db;
|
||||
std::database::AirlinesDBRecord airline_record;
|
||||
std::string airline_code = entry_.callsign.substr(0, 3);
|
||||
auto return_code = db.retrieve_airline_record(&airline_record, airline_code);
|
||||
|
||||
// The following won't (shouldn't !) change for a given airborne aircraft
|
||||
// Try getting the airline's name from airlines.db
|
||||
airline_code = entry_copy.callsign.substr(0, 3);
|
||||
return_code = db.retrieve_airline_record(&airline_record, airline_code);
|
||||
switch (return_code) {
|
||||
case DATABASE_RECORD_FOUND:
|
||||
text_airline.set(airline_record.airline);
|
||||
@@ -258,285 +251,312 @@ ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
break;
|
||||
}
|
||||
|
||||
text_callsign.set(entry_copy.callsign);
|
||||
text_icao_address.set(to_string_hex(entry_copy.ICAO_address, 6));
|
||||
text_icao_address.set(entry_.icao_str);
|
||||
|
||||
button_aircraft_details.on_select = [this, &nav](Button&) {
|
||||
aircraft_details_view = nav.push<ADSBRxAircraftDetailsView>(entry_copy, [this]() { send_updates = false; });
|
||||
send_updates = false;
|
||||
aircraft_details_view_ = nav.push<ADSBRxAircraftDetailsView>(entry_);
|
||||
nav.set_on_pop([this]() {
|
||||
aircraft_details_view_ = nullptr;
|
||||
refresh_ui();
|
||||
});
|
||||
};
|
||||
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
if (!send_updates) { // Prevent recursively launching the map
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
trimr(entry_copy.callsign[0] != ' ' ? entry_copy.callsign : entry_copy.icaoStr),
|
||||
entry_copy.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
entry_copy.pos.latitude,
|
||||
entry_copy.pos.longitude,
|
||||
entry_copy.velo.heading,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
});
|
||||
send_updates = true;
|
||||
}
|
||||
geomap_view_ = nav.push<GeoMapView>(
|
||||
get_map_tag(entry_),
|
||||
entry_.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
entry_.pos.latitude,
|
||||
entry_.pos.longitude,
|
||||
entry_.velo.heading);
|
||||
nav.set_on_pop([this]() {
|
||||
geomap_view_ = nullptr;
|
||||
refresh_ui();
|
||||
});
|
||||
};
|
||||
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
void ADSBRxView::focus() {
|
||||
field_vga.focus();
|
||||
void ADSBRxDetailsView::focus() {
|
||||
button_see_map.focus();
|
||||
}
|
||||
|
||||
ADSBRxView::~ADSBRxView() {
|
||||
receiver_model.set_tuning_frequency(prevFreq); // Restore previous frequency on exit
|
||||
void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
|
||||
// save app settings
|
||||
settings.save("rx_adsb", &app_settings);
|
||||
|
||||
// TODO: once all apps keep there own settin previous frequency logic can be removed
|
||||
receiver_model.set_tuning_frequency(prevFreq);
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
AircraftRecentEntry ADSBRxView::find_or_create_entry(uint32_t ICAO_address) {
|
||||
auto it = find(recent, ICAO_address);
|
||||
|
||||
// If not found
|
||||
if (it == std::end(recent)) {
|
||||
recent.emplace_front(ICAO_address); // Add it
|
||||
it = find(recent, ICAO_address); // Find it again
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
|
||||
void ADSBRxView::replace_entry(AircraftRecentEntry& entry) {
|
||||
uint32_t ICAO_address = entry.ICAO_address;
|
||||
|
||||
std::replace_if(
|
||||
recent.begin(), recent.end(),
|
||||
[ICAO_address](const AircraftRecentEntry& compEntry) { return ICAO_address == compEntry.ICAO_address; },
|
||||
entry);
|
||||
}
|
||||
|
||||
void ADSBRxView::remove_old_entries() {
|
||||
auto it = recent.rbegin();
|
||||
auto end = recent.rend();
|
||||
while (it != end) {
|
||||
if (it->age_state >= 4) {
|
||||
std::advance(it, 1);
|
||||
recent.erase(it.base());
|
||||
} else {
|
||||
break; // stop looking because the list is sorted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxView::sort_entries_by_state() {
|
||||
// Sorting List pn age_state using lambda function as comparator
|
||||
recent.sort([](const AircraftRecentEntry& left, const AircraftRecentEntry& right) { return (left.age_state < right.age_state); });
|
||||
}
|
||||
|
||||
void ADSBRxView::on_frame(const ADSBFrameMessage* message) {
|
||||
logger = std::make_unique<ADSBLogger>();
|
||||
rtc::RTC datetime;
|
||||
std::string callsign;
|
||||
std::string str_info;
|
||||
std::string logentry;
|
||||
|
||||
auto frame = message->frame;
|
||||
uint32_t ICAO_address = frame.get_ICAO_address();
|
||||
|
||||
if (frame.check_CRC() && ICAO_address) {
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
auto entry = find_or_create_entry(ICAO_address);
|
||||
frame.set_rx_timestamp(datetime.minute() * 60 + datetime.second());
|
||||
entry.reset_age();
|
||||
if (entry.hits == 0) {
|
||||
entry.amp = message->amp; // Store amplitude on first hit
|
||||
} else {
|
||||
entry.amp = ((entry.amp * 15) + message->amp) >> 4; // Update smoothed amplitude on updates
|
||||
}
|
||||
|
||||
entry.inc_hit();
|
||||
if (logger) {
|
||||
logentry += to_string_hex_array(frame.get_raw_data(), 14) + " ";
|
||||
logentry += "ICAO:" + entry.icaoStr + " ";
|
||||
}
|
||||
|
||||
if (frame.get_DF() == DF_ADSB) {
|
||||
uint8_t msg_type = frame.get_msg_type();
|
||||
uint8_t msg_sub = frame.get_msg_sub();
|
||||
uint8_t* raw_data = frame.get_raw_data();
|
||||
|
||||
// 4: // surveillance, altitude reply
|
||||
if ((msg_type >= AIRCRAFT_ID_L) && (msg_type <= AIRCRAFT_ID_H)) {
|
||||
callsign = decode_frame_id(frame);
|
||||
entry.set_callsign(callsign);
|
||||
if (logger) {
|
||||
logentry += callsign + " ";
|
||||
}
|
||||
}
|
||||
// 9:
|
||||
// 18: { // Extended squitter/non-transponder
|
||||
// 21: // Comm-B, identity reply
|
||||
// 20: // Comm-B, altitude reply
|
||||
else if (((msg_type >= AIRBORNE_POS_BARO_L) && (msg_type <= AIRBORNE_POS_BARO_H)) ||
|
||||
((msg_type >= AIRBORNE_POS_GPS_L) && (msg_type <= AIRBORNE_POS_GPS_H))) {
|
||||
entry.set_frame_pos(frame, raw_data[6] & 4);
|
||||
|
||||
if (entry.pos.valid) {
|
||||
str_info = "Alt:" + to_string_dec_int(entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(entry.pos.latitude, 2) +
|
||||
" Lon:" + to_string_decimal(entry.pos.longitude, 2);
|
||||
|
||||
entry.set_info_string(str_info);
|
||||
|
||||
if (logger) {
|
||||
// printing the coordinates in the log file with more
|
||||
// resolution, as we are not constrained by screen
|
||||
// real estate there:
|
||||
|
||||
std::string log_info = "Alt:" + to_string_dec_int(entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(entry.pos.latitude, 7) +
|
||||
" Lon:" + to_string_decimal(entry.pos.longitude, 7);
|
||||
logentry += log_info + " ";
|
||||
}
|
||||
}
|
||||
} else if (msg_type == AIRBORNE_VEL && msg_sub >= VEL_GND_SUBSONIC && msg_sub <= VEL_AIR_SUPERSONIC) {
|
||||
entry.set_frame_velo(frame);
|
||||
if (logger) {
|
||||
logger->append(LOG_ROOT_DIR "/ADSB.TXT");
|
||||
logentry += "Type:" + to_string_dec_uint(msg_sub) +
|
||||
" Hdg:" + to_string_dec_uint(entry.velo.heading) +
|
||||
" Spd: " + to_string_dec_int(entry.velo.speed);
|
||||
}
|
||||
}
|
||||
replace_entry(entry);
|
||||
} // frame.get_DF() == DF_ADSB
|
||||
|
||||
if (logger) {
|
||||
logger->append(LOG_ROOT_DIR "/ADSB.TXT");
|
||||
// will log each frame in format:
|
||||
// 20171103100227 8DADBEEFDEADBEEFDEADBEEFDEADBEEF ICAO:nnnnnn callsign Alt:nnnnnn Latnnn.nn Lonnnn.nn
|
||||
logger->log_str(logentry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxView::on_tick_second() {
|
||||
if (recent.size() <= 16) { // Not many entries update everything (16 is one screen full)
|
||||
updateDetailsAndMap(1);
|
||||
updateRecentEntries();
|
||||
} else if (updateState == 0) { // Even second
|
||||
updateState = 1;
|
||||
updateDetailsAndMap(2);
|
||||
} else { // Odd second only performed when there are many entries
|
||||
updateState = 0;
|
||||
updateRecentEntries();
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxView::updateDetailsAndMap(int ageStep) {
|
||||
ui::GeoMarker marker;
|
||||
bool storeNewMarkers = false;
|
||||
|
||||
// Sort and truncate the entries, grouped, newest group first
|
||||
sort_entries_by_state();
|
||||
truncate_entries(recent);
|
||||
remove_old_entries();
|
||||
|
||||
// Calculate if it is time to update markers
|
||||
if (send_updates && details_view && details_view->geomap_view) {
|
||||
ticksSinceMarkerRefresh += ageStep;
|
||||
if (ticksSinceMarkerRefresh >= MARKER_UPDATE_SECONDS) { // Update other aircraft every few seconds
|
||||
storeNewMarkers = true;
|
||||
ticksSinceMarkerRefresh = 0;
|
||||
}
|
||||
if (aircraft_details_view_) {
|
||||
// AC Details view is showing, nothing to update.
|
||||
} else if (geomap_view_) {
|
||||
// Map is showing, update the current item.
|
||||
geomap_view_->update_tag(get_map_tag(entry_));
|
||||
geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude);
|
||||
} else {
|
||||
ticksSinceMarkerRefresh = MARKER_UPDATE_SECONDS; // Send the markers as soon as the geoview exists
|
||||
}
|
||||
|
||||
// Increment age, and pass updates to the details and map
|
||||
const bool otherMarkersCanBeSent = send_updates && storeNewMarkers && details_view && details_view->geomap_view; // Save retesting all of this
|
||||
MapMarkerStored markerStored = MARKER_NOT_STORED;
|
||||
if (otherMarkersCanBeSent) {
|
||||
details_view->geomap_view->clear_markers();
|
||||
}
|
||||
// Loop through all entries
|
||||
for (auto& entry : recent) {
|
||||
entry.inc_age(ageStep);
|
||||
|
||||
// Only if there is a details view
|
||||
if (send_updates && details_view) {
|
||||
if (entry.key() == detailed_entry_key) // Check if the ICAO address match
|
||||
{
|
||||
details_view->update(entry);
|
||||
}
|
||||
// Store if the view is present and the list isn't full
|
||||
else if (otherMarkersCanBeSent && (markerStored != MARKER_LIST_FULL) && entry.pos.valid && (entry.age_state <= 2)) {
|
||||
marker.lon = entry.pos.longitude;
|
||||
marker.lat = entry.pos.latitude;
|
||||
marker.angle = entry.velo.heading;
|
||||
marker.tag = trimr(entry.callsign[0] != ' ' ? entry.callsign : entry.icaoStr);
|
||||
markerStored = details_view->geomap_view->store_marker(marker);
|
||||
}
|
||||
}
|
||||
} // Loop through all entries, if only to update the age
|
||||
}
|
||||
|
||||
void ADSBRxView::updateRecentEntries() {
|
||||
// Redraw the list of aircraft
|
||||
if (!send_updates) {
|
||||
recent_entries_view.set_dirty();
|
||||
// Details is showing, update details.
|
||||
refresh_ui();
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::clear_map_markers() {
|
||||
if (geomap_view_)
|
||||
geomap_view_->clear_markers();
|
||||
}
|
||||
|
||||
bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) {
|
||||
// Map not shown, can't add markers.
|
||||
if (!geomap_view_)
|
||||
return false;
|
||||
|
||||
GeoMarker marker{};
|
||||
marker.lon = entry.pos.longitude;
|
||||
marker.lat = entry.pos.latitude;
|
||||
marker.angle = entry.velo.heading;
|
||||
marker.tag = get_map_tag(entry);
|
||||
|
||||
auto markerStored = geomap_view_->store_marker(marker);
|
||||
return markerStored == MARKER_STORED;
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::refresh_ui() {
|
||||
auto age = entry_.age;
|
||||
if (age < 60)
|
||||
text_last_seen.set(to_string_dec_uint(age) + " seconds ago");
|
||||
else
|
||||
text_last_seen.set(to_string_dec_uint(age / 60) + " minutes ago");
|
||||
|
||||
text_callsign.set(entry_.callsign);
|
||||
text_infos.set(entry_.info_string);
|
||||
if (entry_.velo.heading < 360 && entry_.velo.speed >= 0)
|
||||
text_info2.set("Hdg:" + to_string_dec_uint(entry_.velo.heading) +
|
||||
" Spd:" + to_string_dec_int(entry_.velo.speed));
|
||||
else
|
||||
text_info2.set("");
|
||||
|
||||
text_frame_pos_even.set(to_string_hex_array(entry_.frame_pos_even.get_raw_data(), 14));
|
||||
text_frame_pos_odd.set(to_string_hex_array(entry_.frame_pos_odd.get_raw_data(), 14));
|
||||
}
|
||||
|
||||
/* ADSBRxView ********************************************/
|
||||
|
||||
ADSBRxView::ADSBRxView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_adsb_rx);
|
||||
add_children({&labels,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&rssi,
|
||||
&recent_entries_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_adsb", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
} else {
|
||||
field_lna.set_value(40);
|
||||
field_vga.set_value(40);
|
||||
}
|
||||
add_children(
|
||||
{&labels,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&rssi,
|
||||
&recent_entries_view,
|
||||
&status_frame,
|
||||
&status_good_frame});
|
||||
|
||||
recent_entries_view.set_parent_rect({0, 16, 240, 272});
|
||||
recent_entries_view.on_select = [this, &nav](const AircraftRecentEntry& entry) {
|
||||
detailed_entry_key = entry.key();
|
||||
details_view = nav.push<ADSBRxDetailsView>(
|
||||
entry,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
});
|
||||
send_updates = true;
|
||||
detail_key = entry.key();
|
||||
details_view = nav.push<ADSBRxDetailsView>(entry);
|
||||
|
||||
nav.set_on_pop([this]() {
|
||||
detail_key = AircraftRecentEntry::invalid_key;
|
||||
details_view = nullptr;
|
||||
});
|
||||
};
|
||||
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
|
||||
prevFreq = receiver_model.tuning_frequency(); // Store previous frequency on creation
|
||||
logger = std::make_unique<ADSBLogger>();
|
||||
logger->append(LOG_ROOT_DIR "/ADSB.TXT");
|
||||
|
||||
baseband::set_adsb();
|
||||
|
||||
receiver_model.set_tuning_frequency(1090000000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::SpectrumAnalysis);
|
||||
receiver_model.set_sampling_rate(2000000);
|
||||
receiver_model.set_baseband_bandwidth(2500000);
|
||||
receiver_model.enable();
|
||||
baseband::set_adsb();
|
||||
}
|
||||
|
||||
ADSBRxView::~ADSBRxView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void ADSBRxView::focus() {
|
||||
field_vga.focus();
|
||||
}
|
||||
|
||||
void ADSBRxView::on_frame(const ADSBFrameMessage* message) {
|
||||
auto frame = message->frame;
|
||||
uint32_t ICAO_address = frame.get_ICAO_address();
|
||||
status_frame.toggle();
|
||||
|
||||
// Bad frame, skip it.
|
||||
if (!frame.check_CRC() || ICAO_address == 0)
|
||||
return;
|
||||
|
||||
ADSBLogEntry log_entry;
|
||||
status_good_frame.toggle();
|
||||
|
||||
rtc::RTC datetime;
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
frame.set_rx_timestamp(datetime.minute() * 60 + datetime.second());
|
||||
|
||||
// NB: Reference to update entry in-place.
|
||||
auto& entry = find_or_create_entry(ICAO_address);
|
||||
entry.inc_hit();
|
||||
entry.reset_age();
|
||||
|
||||
// Store smoothed amplitude on updates.
|
||||
entry.amp = entry.hits == 0
|
||||
? message->amp
|
||||
: ((entry.amp * 15) + message->amp) >> 4;
|
||||
|
||||
log_entry.raw_data = to_string_hex_array(frame.get_raw_data(), 14);
|
||||
log_entry.icao = entry.icao_str;
|
||||
|
||||
if (frame.get_DF() == DF_ADSB) {
|
||||
uint8_t msg_type = frame.get_msg_type();
|
||||
uint8_t msg_sub = frame.get_msg_sub();
|
||||
uint8_t* raw_data = frame.get_raw_data();
|
||||
|
||||
// 4: // surveillance, altitude reply
|
||||
if ((msg_type >= AIRCRAFT_ID_L) && (msg_type <= AIRCRAFT_ID_H)) {
|
||||
entry.set_callsign(decode_frame_id(frame));
|
||||
log_entry.callsign = entry.callsign;
|
||||
}
|
||||
|
||||
// 9:
|
||||
// 18: // Extended squitter/non-transponder
|
||||
// 21: // Comm-B, identity reply
|
||||
// 20: // Comm-B, altitude reply
|
||||
else if (((msg_type >= AIRBORNE_POS_BARO_L) && (msg_type <= AIRBORNE_POS_BARO_H)) ||
|
||||
((msg_type >= AIRBORNE_POS_GPS_L) && (msg_type <= AIRBORNE_POS_GPS_H))) {
|
||||
entry.set_frame_pos(frame, raw_data[6] & 4);
|
||||
log_entry.pos = entry.pos;
|
||||
|
||||
if (entry.pos.valid) {
|
||||
std::string str_info =
|
||||
"Alt:" + to_string_dec_int(entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(entry.pos.latitude, 2) +
|
||||
" Lon:" + to_string_decimal(entry.pos.longitude, 2);
|
||||
|
||||
entry.set_info_string(std::move(str_info));
|
||||
}
|
||||
|
||||
} else if (msg_type == AIRBORNE_VEL && msg_sub >= VEL_GND_SUBSONIC && msg_sub <= VEL_AIR_SUPERSONIC) {
|
||||
entry.set_frame_velo(frame);
|
||||
log_entry.vel = entry.velo;
|
||||
log_entry.vel_type = msg_sub;
|
||||
}
|
||||
}
|
||||
|
||||
logger->log(log_entry);
|
||||
}
|
||||
|
||||
void ADSBRxView::on_tick_second() {
|
||||
status_frame.reset();
|
||||
status_good_frame.reset();
|
||||
|
||||
++tick_count;
|
||||
++ticks_since_marker_refresh;
|
||||
|
||||
// Small list, update all at once.
|
||||
if (recent.size() <= max_update_entries) {
|
||||
update_recent_entries(/*age_delta*/ 1);
|
||||
refresh_ui();
|
||||
return;
|
||||
}
|
||||
|
||||
// Too many items, split update work into two phases:
|
||||
// Entry maintenance and UI update.
|
||||
if ((tick_count & 1) == 0)
|
||||
update_recent_entries(/*age_delta*/ 2);
|
||||
else
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void ADSBRxView::refresh_ui() {
|
||||
// There's only one ticks handler, but 3 UIs that need to be updated.
|
||||
// This code will dispatch updates to the currently active view.
|
||||
|
||||
if (details_view) {
|
||||
// The details view is showing, forward updates to that UI.
|
||||
bool current_updated = false;
|
||||
bool map_needs_update = false;
|
||||
|
||||
if (details_view->map_active()) {
|
||||
// Is it time to clear and refresh the map's markers?
|
||||
if (ticks_since_marker_refresh >= MARKER_UPDATE_SECONDS) {
|
||||
map_needs_update = true;
|
||||
ticks_since_marker_refresh = 0;
|
||||
details_view->clear_map_markers();
|
||||
}
|
||||
} else {
|
||||
// Refresh map immediately once active.
|
||||
ticks_since_marker_refresh = MARKER_UPDATE_SECONDS;
|
||||
}
|
||||
|
||||
// Process the entries list.
|
||||
for (const auto& entry : recent) {
|
||||
// Found the entry being shown in details view. Update it.
|
||||
if (entry.key() == detail_key) {
|
||||
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.valid && entry.state <= ADSBAgeState::Recent) {
|
||||
map_needs_update = details_view->add_map_marker(entry);
|
||||
}
|
||||
|
||||
// Any work left to do?
|
||||
if (current_updated && !map_needs_update)
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Main page is the top view. Redraw the entries view.
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxView::update_recent_entries(int age_delta) {
|
||||
for (auto& entry : recent)
|
||||
entry.inc_age(age_delta);
|
||||
|
||||
// Sort and truncate the entries, grouped by state, newest first.
|
||||
sort_entries_by_state();
|
||||
truncate_entries(recent);
|
||||
remove_expired_entries();
|
||||
}
|
||||
|
||||
AircraftRecentEntry& ADSBRxView::find_or_create_entry(uint32_t ICAO_address) {
|
||||
// Find ...
|
||||
auto it = find(recent, ICAO_address);
|
||||
if (it != recent.end())
|
||||
return *it;
|
||||
|
||||
// ... or Create.
|
||||
return recent.emplace_front(ICAO_address);
|
||||
}
|
||||
|
||||
void ADSBRxView::sort_entries_by_state() {
|
||||
recent.sort([](const auto& left, const auto& right) {
|
||||
return (left.state < right.state);
|
||||
});
|
||||
}
|
||||
|
||||
void ADSBRxView::remove_expired_entries() {
|
||||
// NB: Assumes entried are sorted with oldest last.
|
||||
auto it = recent.rbegin();
|
||||
auto end = recent.rend();
|
||||
|
||||
// Find the first !expired entry from the back.
|
||||
while (it != end) {
|
||||
if (it->state != ADSBAgeState::Expired)
|
||||
break;
|
||||
|
||||
std::advance(it, 1);
|
||||
}
|
||||
|
||||
// Remove the range of expired items.
|
||||
recent.erase(it.base(), recent.end());
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -21,29 +22,23 @@
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
#include "database.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "adsb.hpp"
|
||||
#include "message.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "crc.hpp"
|
||||
#include "database.hpp"
|
||||
#include "file.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "message.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace adsb;
|
||||
|
||||
namespace ui {
|
||||
|
||||
#define ADSB_CURRENT 10 // Seconds
|
||||
#define ADSB_RECENT 30 // Seconds
|
||||
#define ADSB_REMOVE 300 // Used for removing old entries
|
||||
|
||||
#define AIRCRAFT_ID_L 1 // aircraft ID message type (lowest type id)
|
||||
#define AIRCRAFT_ID_H 4 // aircraft ID message type (highest type id)
|
||||
|
||||
@@ -66,8 +61,26 @@ 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 O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define MARKER_UPDATE_SECONDS 5 // "other" map marker redraw interval
|
||||
|
||||
/* Thresholds (in seconds) that define the transition between ages. */
|
||||
struct ADSBAgeLimit {
|
||||
static constexpr int Current = 10;
|
||||
static constexpr int Recent = 30;
|
||||
static constexpr int Expired = 300;
|
||||
};
|
||||
|
||||
/* Age states used for sorting and drawing recent entries. */
|
||||
enum class ADSBAgeState : uint8_t {
|
||||
Invalid,
|
||||
Current,
|
||||
Recent,
|
||||
Old,
|
||||
Expired,
|
||||
};
|
||||
|
||||
/* Data extracted from ADSB frames. */
|
||||
struct AircraftRecentEntry {
|
||||
using Key = uint32_t;
|
||||
|
||||
@@ -76,30 +89,30 @@ struct AircraftRecentEntry {
|
||||
uint32_t ICAO_address{};
|
||||
uint16_t hits{0};
|
||||
|
||||
uint16_t age_state{1};
|
||||
uint32_t age{0};
|
||||
ADSBAgeState state{ADSBAgeState::Invalid};
|
||||
uint32_t age{0}; // In seconds
|
||||
uint32_t amp{0};
|
||||
adsb_pos pos{false, 0, 0, 0};
|
||||
adsb_vel velo{false, 0, 999, 0};
|
||||
ADSBFrame frame_pos_even{};
|
||||
ADSBFrame frame_pos_odd{};
|
||||
|
||||
std::string icaoStr{" "};
|
||||
std::string callsign{" "};
|
||||
std::string info_string{""};
|
||||
std::string icao_str{};
|
||||
std::string callsign{};
|
||||
std::string info_string{};
|
||||
|
||||
AircraftRecentEntry(
|
||||
const uint32_t ICAO_address)
|
||||
AircraftRecentEntry(const uint32_t ICAO_address)
|
||||
: ICAO_address{ICAO_address} {
|
||||
this->icaoStr = to_string_hex(ICAO_address, 6);
|
||||
this->icao_str = to_string_hex(ICAO_address, 6);
|
||||
}
|
||||
|
||||
/* RecentEntries helpers expect a "key" on every item. */
|
||||
Key key() const {
|
||||
return ICAO_address;
|
||||
}
|
||||
|
||||
void set_callsign(std::string& new_callsign) {
|
||||
callsign = new_callsign;
|
||||
void set_callsign(std::string new_callsign) {
|
||||
callsign = std::move(new_callsign);
|
||||
}
|
||||
|
||||
void inc_hit() {
|
||||
@@ -122,8 +135,8 @@ struct AircraftRecentEntry {
|
||||
velo = decode_frame_velo(frame);
|
||||
}
|
||||
|
||||
void set_info_string(std::string& new_info_string) {
|
||||
info_string = new_info_string;
|
||||
void set_info_string(std::string new_info_string) {
|
||||
info_string = std::move(new_info_string);
|
||||
}
|
||||
|
||||
void reset_age() {
|
||||
@@ -132,59 +145,59 @@ struct AircraftRecentEntry {
|
||||
|
||||
void inc_age(int delta) {
|
||||
age += delta;
|
||||
if (age < ADSB_CURRENT) {
|
||||
age_state = pos.valid ? 0 : 1;
|
||||
} else if (age < ADSB_RECENT) {
|
||||
age_state = 2;
|
||||
} else if (age < ADSB_REMOVE) {
|
||||
age_state = 3;
|
||||
} else {
|
||||
age_state = 4;
|
||||
}
|
||||
|
||||
if (age < ADSBAgeLimit::Current)
|
||||
state = pos.valid ? ADSBAgeState::Invalid
|
||||
: ADSBAgeState::Current;
|
||||
|
||||
else if (age < ADSBAgeLimit::Recent)
|
||||
state = ADSBAgeState::Recent;
|
||||
|
||||
else if (age < ADSBAgeLimit::Expired)
|
||||
state = ADSBAgeState::Old;
|
||||
|
||||
else
|
||||
state = ADSBAgeState::Expired;
|
||||
}
|
||||
};
|
||||
|
||||
// NB: uses std::list underneath so assuming refs are NOT invalidated.
|
||||
using AircraftRecentEntries = RecentEntries<AircraftRecentEntry>;
|
||||
|
||||
/* Holds data for logging. */
|
||||
struct ADSBLogEntry {
|
||||
std::string raw_data{};
|
||||
std::string icao{};
|
||||
std::string callsign{};
|
||||
adsb_pos pos{};
|
||||
adsb_vel vel{};
|
||||
uint8_t vel_type{};
|
||||
};
|
||||
|
||||
// TODO: Make logging optional.
|
||||
/* Logs entries to a log file. */
|
||||
class ADSBLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
void log_str(std::string& logline);
|
||||
void log(const ADSBLogEntry& log_entry);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
/* Shows detailed information about an aircraft. */
|
||||
class ADSBRxAircraftDetailsView : public View {
|
||||
public:
|
||||
ADSBRxAircraftDetailsView(NavigationView&, const AircraftRecentEntry& entry, const std::function<void(void)> on_close);
|
||||
~ADSBRxAircraftDetailsView();
|
||||
|
||||
ADSBRxAircraftDetailsView(const ADSBRxAircraftDetailsView&) = delete;
|
||||
ADSBRxAircraftDetailsView(ADSBRxAircraftDetailsView&&) = delete;
|
||||
ADSBRxAircraftDetailsView& operator=(const ADSBRxAircraftDetailsView&) = delete;
|
||||
ADSBRxAircraftDetailsView& operator=(ADSBRxAircraftDetailsView&&) = delete;
|
||||
ADSBRxAircraftDetailsView(
|
||||
NavigationView&,
|
||||
const AircraftRecentEntry& entry);
|
||||
|
||||
void focus() override;
|
||||
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
std::string title() const override { return "AC Details"; };
|
||||
|
||||
AircraftRecentEntry get_current_entry() { return entry_copy; }
|
||||
|
||||
std::database::AircraftDBRecord aircraft_record = {};
|
||||
std::string title() const override { return "AC Details"; }
|
||||
|
||||
private:
|
||||
AircraftRecentEntry entry_copy{0};
|
||||
std::function<void(void)> on_close_{};
|
||||
bool send_updates{false};
|
||||
std::database db = {};
|
||||
std::string icao_code = "";
|
||||
int return_code = 0;
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "ICAO:", Color::light_grey()},
|
||||
{{0 * 8, 2 * 16}, "Registration:", Color::light_grey()},
|
||||
@@ -237,36 +250,34 @@ class ADSBRxAircraftDetailsView : public View {
|
||||
"Back"};
|
||||
};
|
||||
|
||||
/* Shows detailed information about an aircraft's flight. */
|
||||
class ADSBRxDetailsView : public View {
|
||||
public:
|
||||
ADSBRxDetailsView(NavigationView&, const AircraftRecentEntry& entry, const std::function<void(void)> on_close);
|
||||
~ADSBRxDetailsView();
|
||||
ADSBRxDetailsView(NavigationView&, const AircraftRecentEntry& entry);
|
||||
|
||||
ADSBRxDetailsView(const ADSBRxDetailsView&) = delete;
|
||||
ADSBRxDetailsView(ADSBRxDetailsView&&) = delete;
|
||||
ADSBRxDetailsView& operator=(const ADSBRxDetailsView&) = delete;
|
||||
ADSBRxDetailsView& operator=(ADSBRxDetailsView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
std::string title() const override { return "Details"; };
|
||||
/* 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);
|
||||
|
||||
AircraftRecentEntry get_current_entry() { return entry_copy; }
|
||||
|
||||
std::database::AirlinesDBRecord airline_record = {};
|
||||
|
||||
GeoMapView* geomap_view{nullptr};
|
||||
std::string title() const override { return "Details"; }
|
||||
|
||||
private:
|
||||
AircraftRecentEntry entry_copy{0};
|
||||
std::function<void(void)> on_close_{};
|
||||
ADSBRxAircraftDetailsView* aircraft_details_view{nullptr};
|
||||
bool send_updates{false};
|
||||
std::database db = {};
|
||||
std::string airline_code = "";
|
||||
int return_code = 0;
|
||||
void refresh_ui();
|
||||
|
||||
GeoMapView* geomap_view_{nullptr};
|
||||
ADSBRxAircraftDetailsView* aircraft_details_view_{nullptr};
|
||||
|
||||
// NB: Keeping a copy so that it doesn't end up dangling
|
||||
// if removed from the recent entries list.
|
||||
AircraftRecentEntry entry_{AircraftRecentEntry::invalid_key};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "ICAO:", Color::light_grey()},
|
||||
@@ -321,6 +332,7 @@ class ADSBRxDetailsView : public View {
|
||||
"See on map"};
|
||||
};
|
||||
|
||||
/* Main ADSB application view and message dispatch. */
|
||||
class ADSBRxView : public View {
|
||||
public:
|
||||
ADSBRxView(NavigationView& nav);
|
||||
@@ -332,42 +344,52 @@ class ADSBRxView : public View {
|
||||
ADSBRxView& operator=(ADSBRxView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "ADS-B RX"; };
|
||||
|
||||
void replace_entry(AircraftRecentEntry& entry);
|
||||
void remove_old_entries();
|
||||
AircraftRecentEntry find_or_create_entry(uint32_t ICAO_address);
|
||||
void sort_entries_by_state();
|
||||
|
||||
private:
|
||||
rf::Frequency prevFreq = {0};
|
||||
RxRadioState radio_state_{
|
||||
1'090'000'000 /* frequency */,
|
||||
2'500'000 /* bandwidth */,
|
||||
2'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::SpectrumAnalysis};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_adsb", app_settings::Mode::RX};
|
||||
|
||||
std::unique_ptr<ADSBLogger> logger{};
|
||||
|
||||
/* Event Handlers */
|
||||
void on_frame(const ADSBFrameMessage* message);
|
||||
void on_tick_second();
|
||||
int updateState = {0};
|
||||
void updateRecentEntries();
|
||||
void updateDetailsAndMap(int ageStep);
|
||||
|
||||
#define MARKER_UPDATE_SECONDS (5)
|
||||
int ticksSinceMarkerRefresh{MARKER_UPDATE_SECONDS - 1};
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
|
||||
const RecentEntriesColumns columns{{{"ICAO/Call", 9},
|
||||
{"Lvl", 3},
|
||||
{"Spd", 3},
|
||||
{"Amp", 3},
|
||||
{"Hit", 3},
|
||||
{"Age", 4}}};
|
||||
AircraftRecentEntries recent{};
|
||||
RecentEntriesView<RecentEntries<AircraftRecentEntry>> recent_entries_view{columns, recent};
|
||||
void refresh_ui();
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
uint8_t tick_count = 0;
|
||||
uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS};
|
||||
|
||||
/* Max number of entries that can be updated in a single pass.
|
||||
* 16 is one screen of recent entries. */
|
||||
static constexpr uint8_t max_update_entries = 16;
|
||||
|
||||
/* Recent Entries */
|
||||
const RecentEntriesColumns columns{
|
||||
{{"ICAO/Call", 9},
|
||||
{"Lvl", 3},
|
||||
{"Spd", 3},
|
||||
{"Amp", 3},
|
||||
{"Hit", 3},
|
||||
{"Age", 4}}};
|
||||
AircraftRecentEntries recent{};
|
||||
RecentEntriesView<AircraftRecentEntries> recent_entries_view{columns, recent};
|
||||
|
||||
/* Entry Management */
|
||||
void update_recent_entries(int age_delta);
|
||||
AircraftRecentEntry& find_or_create_entry(uint32_t ICAO_address);
|
||||
void sort_entries_by_state();
|
||||
void remove_expired_entries();
|
||||
|
||||
/* The key of the entry in the details view if shown. */
|
||||
AircraftRecentEntry::Key detail_key{AircraftRecentEntry::invalid_key};
|
||||
ADSBRxDetailsView* details_view{nullptr};
|
||||
uint32_t detailed_entry_key{0};
|
||||
bool send_updates{false};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 8}, "LNA: VGA: AMP:", Color::light_grey()}};
|
||||
@@ -382,7 +404,17 @@ class ADSBRxView : public View {
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{20 * 8, 4, 10 * 8, 8},
|
||||
{20 * 8, 4, 10 * 7, 8},
|
||||
};
|
||||
|
||||
ActivityDot status_frame{
|
||||
{screen_width - 3, 5, 2, 2},
|
||||
Color::white(),
|
||||
};
|
||||
|
||||
ActivityDot status_good_frame{
|
||||
{screen_width - 3, 9, 2, 2},
|
||||
Color::green(),
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame{
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "manchester.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include <cstring>
|
||||
@@ -187,13 +186,12 @@ ADSBSquawkView::ADSBSquawkView(
|
||||
&field_squawk});
|
||||
}
|
||||
|
||||
void ADSBSquawkView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
|
||||
void ADSBSquawkView::collect_frames(const uint32_t /*ICAO_address*/, std::vector<ADSBFrame>& frame_list) {
|
||||
if (!enabled) return;
|
||||
|
||||
ADSBFrame temp_frame;
|
||||
(void)ICAO_address;
|
||||
|
||||
encode_frame_squawk(temp_frame, field_squawk.concatenate_4_octal_u16());
|
||||
encode_frame_squawk(temp_frame, field_squawk.to_integer());
|
||||
|
||||
frame_list.emplace_back(temp_frame);
|
||||
}
|
||||
@@ -273,17 +271,12 @@ void ADSBTxView::focus() {
|
||||
}
|
||||
|
||||
ADSBTxView::~ADSBTxView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_adsb", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, withouth ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void ADSBTxView::generate_frames() {
|
||||
const uint32_t ICAO_address = sym_icao.value_hex_u64();
|
||||
const uint32_t ICAO_address = sym_icao.to_integer();
|
||||
|
||||
frames.clear();
|
||||
|
||||
@@ -303,8 +296,9 @@ void ADSBTxView::generate_frames() {
|
||||
void ADSBTxView::start_tx() {
|
||||
generate_frames();
|
||||
|
||||
transmitter_model.set_sampling_rate(4000000U);
|
||||
transmitter_model.set_baseband_bandwidth(10000000);
|
||||
/* Already tested , with SDR Angel + another Hackrf RX and dump1090 + SDR RLT. Final conclusion is TX LPF 6 Mhz is
|
||||
* the best settings to fulfill ADSB transponder spectrum mask requirements (<=-20 dB's at +-7Mhz , <=-40 dB's at +-23Mhz )
|
||||
* and not showing any ADSB data decoding degradation.*/
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_adsb();
|
||||
@@ -327,18 +321,10 @@ ADSBTxView::ADSBTxView(
|
||||
&text_frame,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_adsb", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "message.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
using namespace adsb;
|
||||
@@ -134,7 +135,7 @@ class ADSBSquawkView : public OptionTabView {
|
||||
SymField field_squawk{
|
||||
{10 * 8, 2 * 16},
|
||||
4,
|
||||
SymField::SYMFIELD_OCT};
|
||||
SymField::Type::Oct};
|
||||
};
|
||||
|
||||
class ADSBTXThread {
|
||||
@@ -201,9 +202,13 @@ class ADSBTxView : public View {
|
||||
-1
|
||||
};*/
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
TxRadioState radio_state_{
|
||||
1090000000 /* frequency */,
|
||||
6000000 /* bandwidth */,
|
||||
4000000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_adsb", app_settings::Mode::TX};
|
||||
|
||||
// tx_modes tx_mode = IDLE;
|
||||
NavigationView& nav_;
|
||||
@@ -231,7 +236,7 @@ class ADSBTxView : public View {
|
||||
SymField sym_icao{
|
||||
{10 * 8, 4 * 8},
|
||||
6,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
|
||||
Text text_frame{
|
||||
{1 * 8, 29 * 8, 14 * 8, 16},
|
||||
|
||||
@@ -42,15 +42,12 @@ void RecentEntriesTable<APRSRecentEntries>::draw(
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
char aged_color;
|
||||
Color target_color;
|
||||
// auto entry_age = entry.age;
|
||||
|
||||
target_color = Color::green();
|
||||
|
||||
aged_color = 0x10;
|
||||
std::string entry_string = "\x1B";
|
||||
entry_string += aged_color;
|
||||
std::string entry_string = "";
|
||||
|
||||
entry_string += entry.source_formatted;
|
||||
entry_string.append(10 - entry.source_formatted.size(), ' ');
|
||||
@@ -73,12 +70,8 @@ void APRSRxView::focus() {
|
||||
options_region.focus();
|
||||
}
|
||||
|
||||
void APRSRxView::update_freq(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
}
|
||||
|
||||
APRSRxView::APRSRxView(NavigationView& nav, Rect parent_rect)
|
||||
: View(parent_rect) {
|
||||
: View(parent_rect), nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_aprs_rx);
|
||||
|
||||
add_children({&rssi,
|
||||
@@ -86,19 +79,12 @@ APRSRxView::APRSRxView(NavigationView& nav, Rect parent_rect)
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_volume,
|
||||
&options_region,
|
||||
&field_frequency,
|
||||
&record_view,
|
||||
&console});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_aprs", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
}
|
||||
|
||||
// DEBUG
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
@@ -118,19 +104,7 @@ APRSRxView::APRSRxView(NavigationView& nav, Rect parent_rect)
|
||||
}
|
||||
};
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(100);
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
options_region.set_selected_index(0, true);
|
||||
|
||||
logger = std::make_unique<APRSLogger>();
|
||||
@@ -142,9 +116,6 @@ APRSRxView::APRSRxView(NavigationView& nav, Rect parent_rect)
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
@@ -210,9 +181,6 @@ void APRSRxView::on_show() {
|
||||
}
|
||||
|
||||
APRSRxView::~APRSRxView() {
|
||||
// save app settings
|
||||
settings.save("rx_aprs", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_record_view.hpp" // DEBUG
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "ui_tabview.hpp"
|
||||
|
||||
@@ -187,9 +189,14 @@ class APRSRxView : public View {
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
bool reset_console = false;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
144390000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
3072000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_aprs", app_settings::Mode::RX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
std::string str_log{""};
|
||||
@@ -201,11 +208,12 @@ class APRSRxView : public View {
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
OptionsField options_region{
|
||||
{0 * 8, 0 * 8},
|
||||
@@ -215,9 +223,9 @@ class APRSRxView : public View {
|
||||
{"AUS", 2},
|
||||
{"NZ ", 3}}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
RxFrequencyField field_frequency{
|
||||
{3 * 8, 0 * 16},
|
||||
};
|
||||
nav_};
|
||||
|
||||
// DEBUG
|
||||
RecordView record_view{
|
||||
@@ -231,8 +239,6 @@ class APRSRxView : public View {
|
||||
Console console{
|
||||
{0, 2 * 16, 240, 240}};
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
|
||||
std::unique_ptr<APRSLogger> logger{};
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "aprs.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
@@ -44,27 +43,20 @@ void APRSTXView::focus() {
|
||||
}
|
||||
|
||||
APRSTXView::~APRSTXView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_aprs", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit.
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void APRSTXView::start_tx() {
|
||||
// TODO: Clean up this API to take string_views to avoid allocations.
|
||||
make_aprs_frame(
|
||||
sym_source.value_string().c_str(), num_ssid_source.value(),
|
||||
sym_dest.value_string().c_str(), num_ssid_dest.value(),
|
||||
sym_source.to_string().c_str(), num_ssid_source.value(),
|
||||
sym_dest.to_string().c_str(), num_ssid_dest.value(),
|
||||
payload);
|
||||
|
||||
// uint8_t * bb_data_ptr = shared_memory.bb_data.data;
|
||||
// text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15));
|
||||
|
||||
transmitter_model.set_tuning_frequency(persistent_memory::tuned_frequency());
|
||||
transmitter_model.set_sampling_rate(AFSK_TX_SAMPLERATE);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_afsk_data(
|
||||
@@ -97,14 +89,6 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
&button_set,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_aprs", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
button_set.on_select = [this, &nav](Button&) {
|
||||
text_prompt(
|
||||
nav,
|
||||
@@ -116,9 +100,9 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -24,12 +24,13 @@
|
||||
#include "ui_textentry.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include "message.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
@@ -44,9 +45,13 @@ class APRSTXView : public View {
|
||||
std::string title() const override { return "APRS TX"; };
|
||||
|
||||
private:
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
TxRadioState radio_state_{
|
||||
144390000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
AFSK_TX_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_aprs", app_settings::Mode::TX};
|
||||
|
||||
std::string payload{""};
|
||||
|
||||
@@ -64,7 +69,8 @@ class APRSTXView : public View {
|
||||
SymField sym_source{
|
||||
{7 * 8, 1 * 16},
|
||||
6,
|
||||
SymField::SYMFIELD_ALPHANUM};
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_source{
|
||||
{19 * 8, 1 * 16},
|
||||
2,
|
||||
@@ -75,7 +81,7 @@ class APRSTXView : public View {
|
||||
SymField sym_dest{
|
||||
{7 * 8, 2 * 16},
|
||||
6,
|
||||
SymField::SYMFIELD_ALPHANUM};
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_dest{
|
||||
{19 * 8, 2 * 16},
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -137,13 +136,8 @@ void BHTView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
}
|
||||
|
||||
BHTView::~BHTView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_bht", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
BHTView::BHTView(NavigationView& nav) {
|
||||
@@ -157,21 +151,12 @@ BHTView::BHTView(NavigationView& nav) {
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_bht", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
field_speed.set_value(1);
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -233,7 +218,6 @@ EPARView::EPARView(
|
||||
|
||||
field_city.set_value(0);
|
||||
field_group.set_selected_index(2);
|
||||
|
||||
field_city.on_change = [this](int32_t) { generate_message(); };
|
||||
field_group.on_change = [this](size_t, int32_t) { generate_message(); };
|
||||
|
||||
@@ -243,11 +227,11 @@ EPARView::EPARView(
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& relay_state : relay_states) {
|
||||
relay_state.on_change = relay_state_fn;
|
||||
relay_state.set_parent_rect({static_cast<Coord>(90 + (n * 36)),
|
||||
80,
|
||||
24, 24});
|
||||
relay_state.set_options(relay_options);
|
||||
relay_state.on_change = relay_state_fn; // NB: set after set_options to avoid startup call.
|
||||
add_child(&relay_state);
|
||||
n++;
|
||||
}
|
||||
@@ -316,11 +300,11 @@ XylosView::XylosView(
|
||||
};
|
||||
|
||||
field_header_a.on_change = field_fn;
|
||||
field_header_b.on_change = [this](int32_t) { generate_message(); };
|
||||
field_city.on_change = [this](int32_t) { generate_message(); };
|
||||
field_family.on_change = [this](int32_t) { generate_message(); };
|
||||
field_subfamily.on_change = [this](int32_t) { generate_message(); };
|
||||
field_receiver.on_change = [this](int32_t) { generate_message(); };
|
||||
field_header_b.on_change = field_fn;
|
||||
field_city.on_change = field_fn;
|
||||
field_family.on_change = field_fn;
|
||||
field_subfamily.on_change = field_fn;
|
||||
field_receiver.on_change = field_fn;
|
||||
|
||||
checkbox_wcsubfamily.on_select = [this](Checkbox&, bool v) {
|
||||
field_subfamily.set_focusable(!v);
|
||||
@@ -341,11 +325,11 @@ XylosView::XylosView(
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& relay_state : relay_states) {
|
||||
relay_state.on_change = relay_state_fn;
|
||||
relay_state.set_parent_rect({static_cast<Coord>(54 + (n * 36)),
|
||||
134,
|
||||
24, 24});
|
||||
relay_state.set_options(relay_options);
|
||||
relay_state.on_change = relay_state_fn; // NB: set after set_options to avoid startup call.
|
||||
add_child(&relay_state);
|
||||
n++;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include "ui_tabview.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include "bht.hpp"
|
||||
#include "bitmap.hpp"
|
||||
@@ -33,6 +32,7 @@
|
||||
#include "transmitter_model.hpp"
|
||||
#include "encoders.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
@@ -166,9 +166,9 @@ class BHTView : public View {
|
||||
std::string title() const override { return "BHT TX"; };
|
||||
|
||||
private:
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
TxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_bht", app_settings::Mode::TX};
|
||||
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void start_tx();
|
||||
|
||||
@@ -40,11 +40,8 @@ void BTLERxView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void BTLERxView::update_freq(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
}
|
||||
|
||||
BTLERxView::BTLERxView(NavigationView& nav) {
|
||||
BTLERxView::BTLERxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
|
||||
add_children({&rssi,
|
||||
@@ -56,16 +53,7 @@ BTLERxView::BTLERxView(NavigationView& nav) {
|
||||
&button_modem_setup,
|
||||
&console});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_btle", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
}
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
update_freq(2426000000);
|
||||
// Auto-configure modem for LCR RX (TODO: remove later)
|
||||
auto def_bell202 = &modem_defs[0];
|
||||
persistent_memory::set_modem_baudrate(def_bell202->baudrate);
|
||||
serial_format_t serial_format;
|
||||
@@ -75,32 +63,18 @@ BTLERxView::BTLERxView(NavigationView& nav) {
|
||||
serial_format.bit_order = LSB_FIRST;
|
||||
persistent_memory::set_serial_format(serial_format);
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(100);
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
button_modem_setup.on_select = [&nav](Button&) {
|
||||
nav.push<ModemSetupView>();
|
||||
};
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_btle(persistent_memory::modem_baudrate(), 8, 0, false);
|
||||
baseband::set_btlerx(channel_number);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.set_sampling_rate(4000000);
|
||||
receiver_model.set_baseband_bandwidth(4000000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
@@ -148,10 +122,6 @@ void BTLERxView::on_data(uint32_t value, bool is_data) {
|
||||
}
|
||||
|
||||
BTLERxView::~BTLERxView() {
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
settings.save("rx_btle", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
@@ -27,8 +27,10 @@
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "ui_record_view.hpp" // DEBUG
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
@@ -46,13 +48,20 @@ class BTLERxView : public View {
|
||||
private:
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
2426000000 /* frequency */,
|
||||
4000000 /* bandwidth */,
|
||||
4000000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_btle", app_settings::Mode::RX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
static constexpr uint8_t channel_number = 37;
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
@@ -60,15 +69,13 @@ class BTLERxView : public View {
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
};
|
||||
nav_};
|
||||
|
||||
Button button_modem_setup{
|
||||
{240 - 12 * 8, 1 * 16, 96, 24},
|
||||
@@ -77,9 +84,6 @@ class BTLERxView : public View {
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
// void on_data_afsk(const AFSKDataMessage& message);
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
|
||||
@@ -20,15 +20,21 @@
|
||||
*/
|
||||
|
||||
#include "ui_debug.hpp"
|
||||
#include "debug.hpp"
|
||||
|
||||
#include "ch.h"
|
||||
|
||||
#include "radio.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "crc.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
|
||||
#include "ui_sd_card_debug.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
@@ -118,12 +124,12 @@ void TemperatureWidget::paint(Painter& painter) {
|
||||
}
|
||||
|
||||
TemperatureWidget::temperature_t TemperatureWidget::temperature(const sample_t sensor_value) const {
|
||||
/*It seems to be a temperature difference of 25C*/
|
||||
return -40 + (sensor_value * 4.31) + 25; // max2837 datasheet temp 25ºC has sensor value: 15
|
||||
// Scaling is different for MAX2837 vs MAX2839 so it's now done in the respective chip-specific module
|
||||
return sensor_value;
|
||||
}
|
||||
|
||||
std::string TemperatureWidget::temperature_str(const temperature_t temperature) const {
|
||||
return to_string_dec_int(temperature, temp_len - 1) + "C";
|
||||
return to_string_dec_int(temperature, temp_len - 2) + STR_DEGREES_C;
|
||||
}
|
||||
|
||||
Coord TemperatureWidget::screen_y(
|
||||
@@ -153,11 +159,8 @@ void TemperatureView::focus() {
|
||||
/* RegistersWidget *******************************************************/
|
||||
|
||||
RegistersWidget::RegistersWidget(
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader)
|
||||
: Widget{},
|
||||
config(std::move(config)),
|
||||
reader(std::move(reader)) {
|
||||
RegistersWidgetConfig&& config)
|
||||
: Widget{}, config(std::move(config)), page_number(0) {
|
||||
}
|
||||
|
||||
void RegistersWidget::update() {
|
||||
@@ -173,12 +176,15 @@ void RegistersWidget::paint(Painter& painter) {
|
||||
|
||||
void RegistersWidget::draw_legend(const Coord left, Painter& painter) {
|
||||
const auto pos = screen_pos();
|
||||
const std::string spaces(config.legend_length(), ' ');
|
||||
|
||||
for (uint32_t i = 0; i < config.registers_per_page; i += config.registers_per_row()) {
|
||||
uint32_t r = page_number * config.registers_per_page + i;
|
||||
|
||||
for (size_t i = 0; i < config.registers_count; i += config.registers_per_row()) {
|
||||
const Point offset{
|
||||
left, static_cast<int>((i / config.registers_per_row()) * row_height)};
|
||||
|
||||
const auto text = to_string_hex(i, config.legend_length());
|
||||
const auto text = (r >= config.registers_count) ? spaces : to_string_hex(r, config.legend_length());
|
||||
painter.draw_string(
|
||||
pos + offset,
|
||||
style().invert(),
|
||||
@@ -190,15 +196,16 @@ void RegistersWidget::draw_values(
|
||||
const Coord left,
|
||||
Painter& painter) {
|
||||
const auto pos = screen_pos();
|
||||
const std::string spaces(config.value_length(), ' ');
|
||||
|
||||
for (uint32_t i = 0; i < config.registers_per_page; i++) {
|
||||
uint32_t r = page_number * config.registers_per_page + i;
|
||||
|
||||
for (size_t i = 0; i < config.registers_count; i++) {
|
||||
const Point offset = {
|
||||
static_cast<int>(left + config.legend_width() + 8 + (i % config.registers_per_row()) * (config.value_width() + 8)),
|
||||
static_cast<int>((i / config.registers_per_row()) * row_height)};
|
||||
|
||||
const auto value = reader(i);
|
||||
|
||||
const auto text = to_string_hex(value, config.value_length());
|
||||
const auto text = (r >= config.registers_count) ? spaces : to_string_hex(reg_read(r), config.value_length());
|
||||
painter.draw_string(
|
||||
pos + offset,
|
||||
style(),
|
||||
@@ -206,19 +213,61 @@ void RegistersWidget::draw_values(
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t RegistersWidget::reg_read(const uint32_t register_number) {
|
||||
if (register_number < config.registers_count) {
|
||||
switch (config.chip_type) {
|
||||
case CT_PMEM:
|
||||
return portapack::persistent_memory::pmem_data_word(register_number / 4) >> (register_number % 4 * 8);
|
||||
case CT_RFFC5072:
|
||||
return radio::debug::first_if::register_read(register_number);
|
||||
case CT_MAX283X:
|
||||
return radio::debug::second_if::register_read(register_number);
|
||||
case CT_SI5351:
|
||||
return portapack::clock_generator.read_register(register_number);
|
||||
case CT_AUDIO:
|
||||
return audio::debug::reg_read(register_number);
|
||||
}
|
||||
}
|
||||
return 0xFFFF;
|
||||
}
|
||||
|
||||
void RegistersWidget::reg_write(const uint32_t register_number, const uint32_t value) {
|
||||
if (register_number < config.registers_count) {
|
||||
switch (config.chip_type) {
|
||||
case CT_PMEM:
|
||||
break;
|
||||
case CT_RFFC5072:
|
||||
radio::debug::first_if::register_write(register_number, value);
|
||||
break;
|
||||
case CT_MAX283X:
|
||||
radio::debug::second_if::register_write(register_number, value);
|
||||
break;
|
||||
case CT_SI5351:
|
||||
portapack::clock_generator.write_register(register_number, value);
|
||||
break;
|
||||
case CT_AUDIO:
|
||||
audio::debug::reg_write(register_number, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* RegistersView *********************************************************/
|
||||
|
||||
RegistersView::RegistersView(
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader)
|
||||
: registers_widget{std::move(config), std::move(reader)} {
|
||||
RegistersWidgetConfig&& config)
|
||||
: registers_widget{std::move(config)} {
|
||||
add_children({
|
||||
&text_title,
|
||||
®isters_widget,
|
||||
&button_update,
|
||||
&button_done,
|
||||
&labels,
|
||||
&field_write_reg_num,
|
||||
&field_write_data_val,
|
||||
&button_write,
|
||||
});
|
||||
|
||||
button_update.on_select = [this](Button&) {
|
||||
@@ -227,16 +276,38 @@ RegistersView::RegistersView(
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
|
||||
registers_widget.set_parent_rect({0, 48, 240, 192});
|
||||
registers_widget.set_page(0);
|
||||
|
||||
text_title.set_parent_rect({(240 - static_cast<int>(title.size()) * 8) / 2, 16,
|
||||
static_cast<int>(title.size()) * 8, 16});
|
||||
text_title.set(title);
|
||||
|
||||
field_write_reg_num.on_change = [this](SymField&) {
|
||||
field_write_data_val.set_value(this->registers_widget.reg_read(field_write_reg_num.to_integer()));
|
||||
field_write_data_val.set_dirty();
|
||||
};
|
||||
|
||||
const auto value = registers_widget.reg_read(0);
|
||||
field_write_data_val.set_value(value);
|
||||
|
||||
button_write.set_style(&Styles::red);
|
||||
button_write.on_select = [this](Button&) {
|
||||
this->registers_widget.reg_write(field_write_reg_num.to_integer(), field_write_data_val.to_integer());
|
||||
this->registers_widget.update();
|
||||
};
|
||||
}
|
||||
|
||||
void RegistersView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
bool RegistersView::on_encoder(const EncoderEvent delta) {
|
||||
registers_widget.set_page(std::max(0ul, std::min(registers_widget.page_count() - 1, registers_widget.page() + delta)));
|
||||
registers_widget.update();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ControlsSwitchesWidget ************************************************/
|
||||
|
||||
void ControlsSwitchesWidget::on_show() {
|
||||
@@ -247,36 +318,41 @@ void ControlsSwitchesWidget::on_show() {
|
||||
|
||||
bool ControlsSwitchesWidget::on_key(const KeyEvent key) {
|
||||
key_event_mask = 1 << toUType(key);
|
||||
long_press_key_event_mask = key_is_long_pressed(key) ? key_event_mask : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControlsSwitchesWidget::paint(Painter& painter) {
|
||||
const auto pos = screen_pos();
|
||||
|
||||
const std::array<Rect, 8> button_rects{{
|
||||
const std::array<Rect, 9> button_rects{{
|
||||
{64, 32, 16, 16}, // Right
|
||||
{0, 32, 16, 16}, // Left
|
||||
{32, 64, 16, 16}, // Down
|
||||
{32, 0, 16, 16}, // Up
|
||||
{32, 32, 16, 16}, // Select
|
||||
{96, 0, 16, 16}, // Dfu
|
||||
{16, 96, 16, 16}, // Encoder phase 0
|
||||
{48, 96, 16, 16}, // Encoder phase 1
|
||||
{96, 0, 16, 16}, // Dfu
|
||||
{96, 64, 16, 16}, // Touch
|
||||
}};
|
||||
|
||||
for (const auto r : button_rects) {
|
||||
painter.fill_rectangle(r + pos, Color::blue());
|
||||
}
|
||||
|
||||
if (get_touch_frame().touch)
|
||||
painter.fill_rectangle(button_rects[8] + pos, Color::yellow());
|
||||
|
||||
const std::array<Rect, 8> raw_rects{{
|
||||
{64 + 1, 32 + 1, 16 - 2, 16 - 2}, // Right
|
||||
{0 + 1, 32 + 1, 16 - 2, 16 - 2}, // Left
|
||||
{32 + 1, 64 + 1, 16 - 2, 16 - 2}, // Down
|
||||
{32 + 1, 0 + 1, 16 - 2, 16 - 2}, // Up
|
||||
{32 + 1, 32 + 1, 16 - 2, 16 - 2}, // Select
|
||||
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
|
||||
{16 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 0
|
||||
{48 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 1
|
||||
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
|
||||
}};
|
||||
|
||||
auto switches_raw = control::debug::switches();
|
||||
@@ -320,6 +396,14 @@ void ControlsSwitchesWidget::paint(Painter& painter) {
|
||||
|
||||
switches_event >>= 1;
|
||||
}
|
||||
|
||||
switches_event = long_press_key_event_mask;
|
||||
for (const auto r : events_rects) {
|
||||
if (switches_event & 1)
|
||||
painter.fill_rectangle(r + pos, Color::cyan());
|
||||
|
||||
switches_event >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
void ControlsSwitchesWidget::on_frame_sync() {
|
||||
@@ -330,12 +414,21 @@ void ControlsSwitchesWidget::on_frame_sync() {
|
||||
|
||||
DebugControlsView::DebugControlsView(NavigationView& nav) {
|
||||
add_children({
|
||||
&text_title,
|
||||
&labels,
|
||||
&switches_widget,
|
||||
&options_switches_mode,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
set_switches_long_press_config(0);
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
options_switches_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
(void)v;
|
||||
set_switches_long_press_config(options_switches_mode.selected_index_value());
|
||||
};
|
||||
}
|
||||
|
||||
void DebugControlsView::focus() {
|
||||
@@ -348,18 +441,10 @@ DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) {
|
||||
const char* max283x = hackrf_r9 ? "MAX2839" : "MAX2837";
|
||||
const char* si5351x = hackrf_r9 ? "Si5351A" : "Si5351C";
|
||||
add_items({
|
||||
{"RFFC5072", ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>(
|
||||
"RFFC5072", RegistersWidgetConfig{31, 16},
|
||||
[](const size_t register_number) { return radio::debug::first_if::register_read(register_number); }); }},
|
||||
{max283x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, max283x]() { nav.push<RegistersView>(
|
||||
max283x, RegistersWidgetConfig{32, 10},
|
||||
[](const size_t register_number) { return radio::debug::second_if::register_read(register_number); }); }},
|
||||
{si5351x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, si5351x]() { nav.push<RegistersView>(
|
||||
si5351x, RegistersWidgetConfig{96, 8},
|
||||
[](const size_t register_number) { return portapack::clock_generator.read_register(register_number); }); }},
|
||||
{audio::debug::codec_name(), ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>(
|
||||
audio::debug::codec_name(), RegistersWidgetConfig{audio::debug::reg_count(), audio::debug::reg_bits()},
|
||||
[](const size_t register_number) { return audio::debug::reg_read(register_number); }); }},
|
||||
{"RFFC5072", ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>("RFFC5072", RegistersWidgetConfig{CT_RFFC5072, 31, 31, 16}); }},
|
||||
{max283x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, max283x]() { nav.push<RegistersView>(max283x, RegistersWidgetConfig{CT_MAX283X, 32, 32, 10}); }},
|
||||
{si5351x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, si5351x]() { nav.push<RegistersView>(si5351x, RegistersWidgetConfig{CT_SI5351, 188, 96, 8}); }},
|
||||
{audio::debug::codec_name(), ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>(audio::debug::codec_name(), RegistersWidgetConfig{CT_AUDIO, audio::debug::reg_count(), audio::debug::reg_count(), audio::debug::reg_bits()}); }},
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
@@ -371,16 +456,149 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
|
||||
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||
}
|
||||
add_items({
|
||||
{"Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
|
||||
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
|
||||
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
|
||||
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
|
||||
{"Memory Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryDumpView>(); }},
|
||||
{"Memory Usage", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
|
||||
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
|
||||
{"Pers. Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
|
||||
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
|
||||
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SDCardDebugView>(); }},
|
||||
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
|
||||
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
|
||||
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
|
||||
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
|
||||
});
|
||||
|
||||
for (auto const& gridItem : ExternalItemsMenuLoader::load_external_items(app_location_t::DEBUG, nav)) {
|
||||
add_item(gridItem);
|
||||
};
|
||||
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
/* DebugMemoryDumpView *********************************************************/
|
||||
|
||||
DebugMemoryDumpView::DebugMemoryDumpView(NavigationView& nav) {
|
||||
add_children({
|
||||
&button_dump,
|
||||
&button_read,
|
||||
&button_write,
|
||||
&button_done,
|
||||
&labels,
|
||||
&field_starting_address,
|
||||
&field_byte_count,
|
||||
&field_rw_address,
|
||||
&field_data_value,
|
||||
});
|
||||
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
|
||||
button_dump.on_select = [this](Button&) {
|
||||
if (field_byte_count.to_integer() != 0)
|
||||
memory_dump((uint32_t*)field_starting_address.to_integer(), ((uint32_t)field_byte_count.to_integer() + 3) / 4, false);
|
||||
};
|
||||
|
||||
button_read.on_select = [this](Button&) {
|
||||
field_data_value.set_value(*(uint32_t*)field_rw_address.to_integer());
|
||||
field_data_value.set_dirty();
|
||||
};
|
||||
|
||||
button_write.set_style(&Styles::red);
|
||||
button_write.on_select = [this](Button&) {
|
||||
*(uint32_t*)field_rw_address.to_integer() = (uint32_t)field_data_value.to_integer();
|
||||
};
|
||||
}
|
||||
|
||||
void DebugMemoryDumpView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
/* DebugPmemView *********************************************************/
|
||||
|
||||
DebugPmemView::DebugPmemView(NavigationView& nav)
|
||||
: registers_widget(RegistersWidgetConfig{CT_PMEM, PMEM_SIZE_BYTES, page_size, 8}) {
|
||||
add_children({®isters_widget, &text_checksum, &text_checksum2, &button_ok});
|
||||
|
||||
registers_widget.set_parent_rect({0, 32, 240, 192});
|
||||
|
||||
text_checksum.set("Size: " + to_string_dec_uint(portapack::persistent_memory::data_size(), 3) + " CRC: " + to_string_hex(portapack::persistent_memory::pmem_stored_checksum(), 8));
|
||||
text_checksum2.set("Calculated CRC: " + to_string_hex(portapack::persistent_memory::pmem_calculated_checksum(), 8));
|
||||
|
||||
button_ok.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool DebugPmemView::on_encoder(const EncoderEvent delta) {
|
||||
registers_widget.set_page(std::max(0ul, std::min((uint32_t)page_count - 1, registers_widget.page() + delta)));
|
||||
|
||||
update();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebugPmemView::focus() {
|
||||
button_ok.focus();
|
||||
}
|
||||
|
||||
void DebugPmemView::update() {
|
||||
registers_widget.update();
|
||||
}
|
||||
|
||||
/* DebugScreenTest ****************************************************/
|
||||
|
||||
DebugScreenTest::DebugScreenTest(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_key(const KeyEvent key) {
|
||||
Painter painter;
|
||||
switch (key) {
|
||||
case KeyEvent::Select:
|
||||
nav_.pop();
|
||||
break;
|
||||
case KeyEvent::Down:
|
||||
painter.fill_rectangle({0, 0, screen_width, screen_height}, semirand());
|
||||
break;
|
||||
case KeyEvent::Left:
|
||||
pen_color = semirand();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_encoder(EncoderEvent delta) {
|
||||
pen_size = clip<int32_t>(pen_size + delta, 1, screen_width);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_touch(const TouchEvent event) {
|
||||
Painter painter;
|
||||
pen_pos = event.point;
|
||||
painter.fill_rectangle({pen_pos.x() - pen_size / 2, pen_pos.y() - pen_size / 2, pen_size, pen_size}, pen_color);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t DebugScreenTest::semirand() {
|
||||
static uint64_t seed{0x0102030405060708};
|
||||
seed = seed * 137;
|
||||
seed = (seed >> 1) | ((seed & 0x01) << 63);
|
||||
return (uint16_t)seed;
|
||||
}
|
||||
|
||||
void DebugScreenTest::paint(Painter& painter) {
|
||||
painter.fill_rectangle({0, 16, screen_width, screen_height - 16}, Color::white());
|
||||
painter.draw_string({10 * 8, screen_height / 2}, Styles::white, "Use Stylus");
|
||||
pen_color = semirand();
|
||||
}
|
||||
|
||||
/* DebugLCRView *******************************************************/
|
||||
|
||||
/*DebugLCRView::DebugLCRView(NavigationView& nav, std::string lcr_string) {
|
||||
|
||||
std::string debug_text;
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
#include "rffc507x.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "memory_map.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
@@ -46,8 +48,8 @@ class DebugMemoryView : public View {
|
||||
|
||||
private:
|
||||
Text text_title{
|
||||
{96, 96, 48, 16},
|
||||
"Memory",
|
||||
{72, 96, 96, 16},
|
||||
"Memory Usage",
|
||||
};
|
||||
|
||||
Text text_label_m0_core_free{
|
||||
@@ -103,7 +105,7 @@ class TemperatureWidget : public Widget {
|
||||
static constexpr temperature_t display_temp_min = -10; // Accomodate negative values, present in cold startup cases
|
||||
static constexpr temperature_t display_temp_scale = 3;
|
||||
static constexpr int bar_width = 1;
|
||||
static constexpr int temp_len = 4; // Now scale shows up to 4 chars ("-10C")
|
||||
static constexpr int temp_len = 5; // Now scale shows up to 5 chars ("-10ºC")
|
||||
};
|
||||
|
||||
class TemperatureView : public View {
|
||||
@@ -129,9 +131,19 @@ class TemperatureView : public View {
|
||||
"Done"};
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
CT_PMEM,
|
||||
CT_RFFC5072,
|
||||
CT_MAX283X,
|
||||
CT_SI5351,
|
||||
CT_AUDIO,
|
||||
} chip_type_t;
|
||||
|
||||
struct RegistersWidgetConfig {
|
||||
size_t registers_count;
|
||||
size_t register_bits;
|
||||
chip_type_t chip_type;
|
||||
uint32_t registers_count;
|
||||
uint32_t registers_per_page;
|
||||
uint32_t register_bits;
|
||||
|
||||
constexpr size_t legend_length() const {
|
||||
return (registers_count >= 0x10) ? 2 : 1;
|
||||
@@ -172,17 +184,22 @@ struct RegistersWidgetConfig {
|
||||
|
||||
class RegistersWidget : public Widget {
|
||||
public:
|
||||
RegistersWidget(
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader);
|
||||
RegistersWidget(RegistersWidgetConfig&& config);
|
||||
|
||||
void update();
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
uint32_t reg_read(const uint32_t register_number);
|
||||
void reg_write(const uint32_t register_number, const uint32_t value);
|
||||
|
||||
void set_page(int32_t value) { page_number = value; }
|
||||
uint32_t page(void) { return page_number; }
|
||||
uint32_t page_count(void) { return (config.registers_count + config.registers_per_page - 1) / config.registers_per_page; }
|
||||
|
||||
private:
|
||||
const RegistersWidgetConfig config;
|
||||
const std::function<uint32_t(const size_t register_number)> reader;
|
||||
uint32_t page_number;
|
||||
|
||||
static constexpr size_t row_height = 16;
|
||||
|
||||
@@ -192,13 +209,9 @@ class RegistersWidget : public Widget {
|
||||
|
||||
class RegistersView : public View {
|
||||
public:
|
||||
RegistersView(
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader);
|
||||
|
||||
void focus();
|
||||
RegistersView(NavigationView& nav, const std::string& title, RegistersWidgetConfig&& config);
|
||||
void focus() override;
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
|
||||
private:
|
||||
Text text_title{};
|
||||
@@ -206,12 +219,30 @@ class RegistersView : public View {
|
||||
RegistersWidget registers_widget;
|
||||
|
||||
Button button_update{
|
||||
{16, 256, 96, 24},
|
||||
{16, 280, 96, 24},
|
||||
"Update"};
|
||||
|
||||
Button button_done{
|
||||
{128, 256, 96, 24},
|
||||
{128, 280, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Button button_write{
|
||||
{144, 248, 80, 20},
|
||||
"Write"};
|
||||
|
||||
Labels labels{
|
||||
{{1 * 8, 248}, "Reg:", Color::light_grey()},
|
||||
{{8 * 8, 248}, "Data:", Color::light_grey()}};
|
||||
|
||||
SymField field_write_reg_num{
|
||||
{5 * 8, 248},
|
||||
2,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_write_data_val{
|
||||
{13 * 8, 248},
|
||||
4,
|
||||
SymField::Type::Hex};
|
||||
};
|
||||
|
||||
class ControlsSwitchesWidget : public Widget {
|
||||
@@ -219,7 +250,8 @@ class ControlsSwitchesWidget : public Widget {
|
||||
ControlsSwitchesWidget(
|
||||
Rect parent_rect)
|
||||
: Widget{parent_rect},
|
||||
key_event_mask(0) {
|
||||
key_event_mask(0),
|
||||
long_press_key_event_mask{0} {
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
@@ -230,6 +262,7 @@ class ControlsSwitchesWidget : public Widget {
|
||||
|
||||
private:
|
||||
uint8_t key_event_mask;
|
||||
uint8_t long_press_key_event_mask;
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
@@ -249,20 +282,119 @@ class DebugControlsView : public View {
|
||||
std::string title() const override { return "Buttons Test"; };
|
||||
|
||||
private:
|
||||
Text text_title{
|
||||
{64, 16, 184, 16},
|
||||
"Controls State",
|
||||
};
|
||||
Labels labels{
|
||||
{{8 * 8, 1 * 16}, "Controls State", Color::white()},
|
||||
{{0 * 8, 14 * 16}, "Long-Press Mode:", Color::grey()}};
|
||||
|
||||
ControlsSwitchesWidget switches_widget{
|
||||
{80, 80, 80, 112},
|
||||
};
|
||||
|
||||
OptionsField options_switches_mode{
|
||||
{17 * 8, 14 * 16},
|
||||
8,
|
||||
{
|
||||
{"Disabled", 0},
|
||||
{"Enabled", 0xFF}, // all KeyEvent bits to long-press mode
|
||||
}};
|
||||
|
||||
Button button_done{
|
||||
{72, 264, 96, 24},
|
||||
"Done"};
|
||||
};
|
||||
|
||||
class DebugMemoryDumpView : public View {
|
||||
public:
|
||||
DebugMemoryDumpView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Memory Dump"; };
|
||||
|
||||
private:
|
||||
Button button_dump{
|
||||
{72, 4 * 16, 96, 24},
|
||||
"Dump"};
|
||||
|
||||
Button button_read{
|
||||
{16, 11 * 16, 96, 24},
|
||||
"Read"};
|
||||
|
||||
Button button_write{
|
||||
{128, 11 * 16, 96, 24},
|
||||
"Write"};
|
||||
|
||||
Button button_done{
|
||||
{128, 240, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Labels labels{
|
||||
{{5 * 8, 1 * 16}, "Dump Range to File", Color::yellow()},
|
||||
{{0 * 8, 2 * 16}, "Starting Address: 0x", Color::light_grey()},
|
||||
{{0 * 8, 3 * 16}, "Byte Count: 0x", Color::light_grey()},
|
||||
{{3 * 8, 8 * 16}, "Read/Write Single Word", Color::yellow()},
|
||||
{{0 * 8, 9 * 16}, "Memory Address: 0x", Color::light_grey()},
|
||||
{{0 * 8, 10 * 16}, "Data Value: 0x", Color::light_grey()}};
|
||||
|
||||
SymField field_starting_address{
|
||||
{20 * 8, 2 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_byte_count{
|
||||
{20 * 8, 3 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_rw_address{
|
||||
{20 * 8, 9 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_data_value{
|
||||
{20 * 8, 10 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
};
|
||||
|
||||
class DebugPmemView : public View {
|
||||
public:
|
||||
DebugPmemView(NavigationView& nav);
|
||||
void focus() override;
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
std::string title() const override { return "P.Mem Debug"; }
|
||||
|
||||
private:
|
||||
static constexpr uint8_t page_size{96}; // Must be multiply of 4 otherwise bit shifting for register view wont work properly
|
||||
static constexpr uint8_t page_count{(portapack::memory::map::backup_ram.size() + page_size - 1) / page_size};
|
||||
|
||||
RegistersWidget registers_widget;
|
||||
|
||||
Text text_checksum{{16, 232, 208, 16}};
|
||||
Text text_checksum2{{16, 248, 208, 16}};
|
||||
|
||||
Button button_ok{
|
||||
{240 / 3, 270, 240 / 3, 24},
|
||||
"OK",
|
||||
};
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
class DebugScreenTest : public View {
|
||||
public:
|
||||
DebugScreenTest(NavigationView& nav);
|
||||
bool on_key(KeyEvent key) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
bool on_touch(TouchEvent event) override;
|
||||
uint16_t semirand();
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
Point pen_pos{};
|
||||
Color pen_color{0};
|
||||
int16_t pen_size{10};
|
||||
};
|
||||
|
||||
/*class DebugLCRView : public View {
|
||||
public:
|
||||
DebugLCRView(NavigationView& nav, std::string lcrstring);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "ui_dfu_menu.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "performance_counter.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -80,4 +81,61 @@ void DfuMenu::paint(Painter& painter) {
|
||||
ui::Color::dark_cyan());
|
||||
}
|
||||
|
||||
DfuMenu2::DfuMenu2(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
add_children({&text_head,
|
||||
&labels,
|
||||
&text_info_line_1,
|
||||
&text_info_line_2,
|
||||
&text_info_line_3,
|
||||
&text_info_line_4,
|
||||
&text_info_line_5,
|
||||
&text_info_line_6,
|
||||
&text_info_line_7,
|
||||
&text_info_line_8,
|
||||
&text_info_line_9,
|
||||
&text_info_line_10});
|
||||
}
|
||||
|
||||
void DfuMenu2::paint(Painter& painter) {
|
||||
text_info_line_1.set(to_string_dec_uint(portapack::receiver_model.target_frequency(), 10));
|
||||
text_info_line_2.set(to_string_dec_uint(portapack::receiver_model.baseband_bandwidth(), 10));
|
||||
text_info_line_3.set(to_string_dec_uint(portapack::receiver_model.sampling_rate(), 10));
|
||||
text_info_line_4.set(to_string_dec_uint((uint32_t)portapack::receiver_model.modulation(), 10));
|
||||
text_info_line_5.set(to_string_dec_uint(portapack::receiver_model.am_configuration(), 10));
|
||||
text_info_line_6.set(to_string_dec_uint(portapack::receiver_model.nbfm_configuration(), 10));
|
||||
text_info_line_7.set(to_string_dec_uint(portapack::receiver_model.wfm_configuration(), 10));
|
||||
text_info_line_8.set(to_string_dec_uint(portapack::transmitter_model.target_frequency(), 10));
|
||||
text_info_line_9.set(to_string_dec_uint(portapack::transmitter_model.baseband_bandwidth(), 10));
|
||||
text_info_line_10.set(to_string_dec_uint(portapack::transmitter_model.sampling_rate(), 10));
|
||||
|
||||
constexpr auto margin = 5;
|
||||
constexpr auto lines = 10 + 2;
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
|
||||
{19 * CHARACTER_WIDTH + margin * 2, lines * LINE_HEIGHT + margin * 2}},
|
||||
ui::Color::black());
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{4 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
|
||||
{CHARACTER_WIDTH, lines * LINE_HEIGHT + margin * 2}},
|
||||
ui::Color::dark_cyan());
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{24 * CHARACTER_WIDTH + margin, 3 * LINE_HEIGHT - margin},
|
||||
{CHARACTER_WIDTH, lines * LINE_HEIGHT + margin * 2}},
|
||||
ui::Color::dark_cyan());
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{4 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin - 8},
|
||||
{21 * CHARACTER_WIDTH + margin * 2, 8}},
|
||||
ui::Color::dark_cyan());
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{4 * CHARACTER_WIDTH - margin, (lines + 3) * LINE_HEIGHT + margin},
|
||||
{21 * CHARACTER_WIDTH + margin * 2, 8}},
|
||||
ui::Color::dark_cyan());
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -57,14 +57,50 @@ class DfuMenu : public View {
|
||||
{{6 * CHARACTER_WIDTH, 11 * LINE_HEIGHT}, "M4 miss:", Color::dark_cyan()},
|
||||
{{6 * CHARACTER_WIDTH, 12 * LINE_HEIGHT}, "uptime:", Color::dark_cyan()}};
|
||||
|
||||
Text text_info_line_1{{15 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_2{{15 * CHARACTER_WIDTH, 6 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_3{{15 * CHARACTER_WIDTH, 7 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_4{{15 * CHARACTER_WIDTH, 8 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_5{{15 * CHARACTER_WIDTH, 9 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_6{{15 * CHARACTER_WIDTH, 10 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_7{{15 * CHARACTER_WIDTH, 11 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_8{{15 * CHARACTER_WIDTH, 12 * LINE_HEIGHT, 5 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_1{{15 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_2{{15 * CHARACTER_WIDTH, 6 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_3{{15 * CHARACTER_WIDTH, 7 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_4{{15 * CHARACTER_WIDTH, 8 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_5{{15 * CHARACTER_WIDTH, 9 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_6{{15 * CHARACTER_WIDTH, 10 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_7{{15 * CHARACTER_WIDTH, 11 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_8{{15 * CHARACTER_WIDTH, 12 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
};
|
||||
|
||||
class DfuMenu2 : public View {
|
||||
public:
|
||||
DfuMenu2(NavigationView& nav);
|
||||
~DfuMenu2() = default;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
Text text_head{{6 * CHARACTER_WIDTH, 3 * LINE_HEIGHT, 14 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, "Radio Settings"};
|
||||
|
||||
Labels labels{
|
||||
{{5 * CHARACTER_WIDTH, 5 * LINE_HEIGHT}, "RX Freq:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 6 * LINE_HEIGHT}, "RX BW:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 7 * LINE_HEIGHT}, "RX SampR:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 8 * LINE_HEIGHT}, "Modulatn:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 9 * LINE_HEIGHT}, "AM cfg:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 10 * LINE_HEIGHT}, "NBFM cfg:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 11 * LINE_HEIGHT}, "WFM cfg:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 12 * LINE_HEIGHT}, "TX Freq:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 13 * LINE_HEIGHT}, "TX BW:", Color::dark_cyan()},
|
||||
{{5 * CHARACTER_WIDTH, 14 * LINE_HEIGHT}, "TX SampR:", Color::dark_cyan()}};
|
||||
|
||||
Text text_info_line_1{{14 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_2{{14 * CHARACTER_WIDTH, 6 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_3{{14 * CHARACTER_WIDTH, 7 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_4{{14 * CHARACTER_WIDTH, 8 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_5{{14 * CHARACTER_WIDTH, 9 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_6{{14 * CHARACTER_WIDTH, 10 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_7{{14 * CHARACTER_WIDTH, 11 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_8{{14 * CHARACTER_WIDTH, 12 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_9{{14 * CHARACTER_WIDTH, 13 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_10{{14 * CHARACTER_WIDTH, 14 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -50,7 +49,6 @@ EncodersConfigView::EncodersConfigView(
|
||||
&field_clk_step,
|
||||
&field_frameduration,
|
||||
&field_frameduration_step,
|
||||
&symfield_word,
|
||||
&text_format,
|
||||
&waveform});
|
||||
|
||||
@@ -65,10 +63,6 @@ EncodersConfigView::EncodersConfigView(
|
||||
options_enctype.set_options(enc_options);
|
||||
options_enctype.set_selected_index(0);
|
||||
|
||||
symfield_word.on_change = [this]() {
|
||||
generate_frame();
|
||||
};
|
||||
|
||||
// Selecting input clock changes symbol and word duration
|
||||
field_clk.on_change = [this](int32_t value) {
|
||||
// value is in kHz, new_value is in us
|
||||
@@ -99,33 +93,46 @@ void EncodersConfigView::focus() {
|
||||
}
|
||||
|
||||
void EncodersConfigView::on_type_change(size_t index) {
|
||||
std::string format_string = "";
|
||||
size_t word_length;
|
||||
char symbol_type;
|
||||
// Remove existing SymField controls.
|
||||
for (auto& symfield : symfields_word)
|
||||
remove_child(symfield.get());
|
||||
symfields_word.clear();
|
||||
|
||||
encoder_def = &encoder_defs[index];
|
||||
|
||||
field_clk.set_value(encoder_def->default_speed / 1000);
|
||||
field_repeat_min.set_value(encoder_def->repeat_min);
|
||||
|
||||
// SymField setup
|
||||
word_length = encoder_def->word_length;
|
||||
symfield_word.set_length(word_length);
|
||||
size_t n = 0, i = 0;
|
||||
while (n < word_length) {
|
||||
symbol_type = encoder_def->word_format[i++];
|
||||
if (symbol_type == 'A') {
|
||||
symfield_word.set_symbol_list(n++, encoder_def->address_symbols);
|
||||
format_string += 'A';
|
||||
} else if (symbol_type == 'D') {
|
||||
symfield_word.set_symbol_list(n++, encoder_def->data_symbols);
|
||||
format_string += 'D';
|
||||
// Add new SymFields.
|
||||
Point pos{2 * 8, 9 * 8};
|
||||
std::string format_string;
|
||||
uint8_t word_length = encoder_def->word_length;
|
||||
auto on_change_handler = [this](SymField&) {
|
||||
generate_frame();
|
||||
};
|
||||
|
||||
for (uint8_t i = 0; i < word_length; i++) {
|
||||
auto symbol_type = encoder_def->word_format[i];
|
||||
symfields_word.push_back(std::make_unique<SymField>(pos, 1));
|
||||
auto& symfield = symfields_word.back();
|
||||
symfield->on_change = on_change_handler;
|
||||
|
||||
switch (symbol_type) {
|
||||
case 'A':
|
||||
symfield->set_symbol_list(encoder_def->address_symbols);
|
||||
format_string += 'A';
|
||||
break;
|
||||
case 'D':
|
||||
symfield->set_symbol_list(encoder_def->data_symbols);
|
||||
format_string += 'D';
|
||||
break;
|
||||
}
|
||||
|
||||
add_child(symfield.get());
|
||||
pos += Point{8, 0};
|
||||
}
|
||||
|
||||
// Ugly :( Pad to erase
|
||||
format_string.append(24 - format_string.size(), ' ');
|
||||
|
||||
text_format.set(format_string);
|
||||
|
||||
generate_frame();
|
||||
@@ -147,17 +154,18 @@ void EncodersConfigView::draw_waveform() {
|
||||
}
|
||||
|
||||
void EncodersConfigView::generate_frame() {
|
||||
size_t i = 0;
|
||||
|
||||
frame_fragments.clear();
|
||||
|
||||
size_t i = 0;
|
||||
for (auto c : encoder_def->word_format) {
|
||||
if (c == 'S')
|
||||
frame_fragments += encoder_def->sync;
|
||||
else if (!c)
|
||||
else if (c == '\0')
|
||||
break;
|
||||
else
|
||||
frame_fragments += encoder_def->bit_format[symfield_word.get_sym(i++)];
|
||||
else {
|
||||
auto offset = symfields_word[i++]->get_offset(0);
|
||||
frame_fragments += encoder_def->bit_format[offset];
|
||||
}
|
||||
}
|
||||
|
||||
draw_waveform();
|
||||
@@ -200,13 +208,8 @@ void EncodersView::focus() {
|
||||
}
|
||||
|
||||
EncodersView::~EncodersView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_ook", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // ghost signal c/m to the problem at the exit .
|
||||
baseband::shutdown(); // better this function after load_sram()
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void EncodersView::update_progress() {
|
||||
@@ -266,8 +269,9 @@ void EncodersView::start_tx(const bool scan) {
|
||||
repeat_index = 1;
|
||||
update_progress();
|
||||
|
||||
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
/* Setting TX LPF 1M75 in this TX OOK App , We got same results as fw 1.7.4
|
||||
* Looking max BW of this app, we tested , Selecting OOK type 145026 with CLK 455K and max DEV. 150k,
|
||||
* and we got BW +-2Mhz , with that TX LPF 1M75, it is fine.*/
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_ook_data(
|
||||
@@ -290,19 +294,10 @@ EncodersView::EncodersView(
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_ook", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
#include "encoders.hpp"
|
||||
#include "de_bruijn.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
using namespace encoders;
|
||||
|
||||
@@ -112,10 +116,7 @@ class EncodersConfigView : public View {
|
||||
{"100", 100},
|
||||
{"1000", 1000}}};
|
||||
|
||||
SymField symfield_word{
|
||||
{2 * 8, 9 * 8},
|
||||
20,
|
||||
SymField::SYMFIELD_DEF};
|
||||
std::vector<std::unique_ptr<SymField>> symfields_word{};
|
||||
|
||||
Text text_format{
|
||||
{2 * 8, 11 * 8, 24 * 8, 16},
|
||||
@@ -183,9 +184,13 @@ class EncodersView : public View {
|
||||
SCAN
|
||||
};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
TxRadioState radio_state_{
|
||||
433920000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
OOK_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_ook", app_settings::Mode::TX};
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
uint32_t repeat_index{0};
|
||||
@@ -195,17 +200,6 @@ class EncodersView : public View {
|
||||
void start_tx(const bool scan);
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
|
||||
/*const Style style_address {
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::red(),
|
||||
};
|
||||
const Style style_data {
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::blue(),
|
||||
};*/
|
||||
|
||||
Rect view_rect = {0, 4 * 8, 240, 168};
|
||||
|
||||
EncodersConfigView view_config{nav_, view_rect};
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_playlist.hpp"
|
||||
#include "ui_remote.hpp"
|
||||
#include "ui_ss_viewer.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_iq_trim.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "event_m0.hpp"
|
||||
@@ -33,6 +38,17 @@
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
static const fs::path txt_ext{u".TXT"};
|
||||
static const fs::path ppl_ext{u".PPL"};
|
||||
static const fs::path c8_ext{u".C8"};
|
||||
static const fs::path c16_ext{u".C16"};
|
||||
static const fs::path cxx_ext{u".C*"};
|
||||
static const fs::path png_ext{u".PNG"};
|
||||
static const fs::path bmp_ext{u".BMP"};
|
||||
static const fs::path rem_ext{u".REM"};
|
||||
} // namespace ui
|
||||
|
||||
namespace {
|
||||
using namespace ui;
|
||||
|
||||
@@ -45,36 +61,18 @@ std::string truncate(const fs::path& path, size_t max_length) {
|
||||
return ::truncate(path.string(), max_length);
|
||||
}
|
||||
|
||||
// Case insensitive path equality on underlying "native" string.
|
||||
bool iequal(
|
||||
const fs::path& lhs,
|
||||
const fs::path& rhs) {
|
||||
const auto& lhs_str = lhs.native();
|
||||
const auto& rhs_str = rhs.native();
|
||||
|
||||
// NB: Not correct for Unicode/locales.
|
||||
if (lhs_str.length() == rhs_str.length()) {
|
||||
for (size_t i = 0; i < lhs_str.length(); ++i)
|
||||
if (towupper(lhs_str[i]) != towupper(rhs_str[i]))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Inserts the entry into the entry list sorted directories first then by file name.
|
||||
void insert_sorted(std::vector<fileman_entry>& entries, fileman_entry&& entry) {
|
||||
auto it = std::lower_bound(std::begin(entries), std::end(entries), entry,
|
||||
[](const fileman_entry& lhs, const fileman_entry& rhs) {
|
||||
if (lhs.is_directory && !rhs.is_directory)
|
||||
return true;
|
||||
else if (!lhs.is_directory && rhs.is_directory)
|
||||
return false;
|
||||
else
|
||||
return lhs.path < rhs.path;
|
||||
});
|
||||
auto it = std::lower_bound(
|
||||
std::begin(entries), std::end(entries), entry,
|
||||
[](const fileman_entry& lhs, const fileman_entry& rhs) {
|
||||
if (lhs.is_directory && !rhs.is_directory)
|
||||
return true;
|
||||
else if (!lhs.is_directory && rhs.is_directory)
|
||||
return false;
|
||||
else
|
||||
return lhs.path < rhs.path;
|
||||
});
|
||||
|
||||
entries.insert(it, std::move(entry));
|
||||
}
|
||||
@@ -83,19 +81,17 @@ void insert_sorted(std::vector<fileman_entry>& entries, fileman_entry&& entry) {
|
||||
fs::path get_partner_file(fs::path path) {
|
||||
if (fs::is_directory(path))
|
||||
return {};
|
||||
|
||||
const fs::path txt_path{u".TXT"};
|
||||
const fs::path c16_path{u".C16"};
|
||||
auto ext = path.extension();
|
||||
|
||||
if (iequal(ext, txt_path))
|
||||
ext = c16_path;
|
||||
else if (iequal(ext, c16_path))
|
||||
ext = txt_path;
|
||||
else
|
||||
if (is_cxx_capture_file(path))
|
||||
path.replace_extension(txt_ext);
|
||||
else if (path_iequal(ext, txt_ext)) {
|
||||
path.replace_extension(c8_ext);
|
||||
if (!fs::file_exists(path))
|
||||
path.replace_extension(c16_ext);
|
||||
} else
|
||||
return {};
|
||||
|
||||
path.replace_extension(ext);
|
||||
return fs::file_exists(path) && !fs::is_directory(path) ? path : fs::path{};
|
||||
}
|
||||
|
||||
@@ -113,14 +109,17 @@ bool partner_file_prompt(
|
||||
if (partner.empty())
|
||||
return false;
|
||||
|
||||
nav.push_under_current<ModalMessageView>(
|
||||
"Partner File",
|
||||
partner.filename().string() + "\n" + action_name + " this file too?",
|
||||
YESNO,
|
||||
[&nav, partner, on_partner_action](bool choice) {
|
||||
if (on_partner_action)
|
||||
on_partner_action(partner, choice);
|
||||
});
|
||||
// Display the continuation UI once the current has been popped.
|
||||
nav.set_on_pop([&nav, partner, action_name, on_partner_action] {
|
||||
nav.display_modal(
|
||||
"Partner File",
|
||||
partner.filename().string() + "\n" + action_name + " this file too?",
|
||||
YESNO,
|
||||
[&nav, partner, on_partner_action](bool choice) {
|
||||
if (on_partner_action)
|
||||
on_partner_action(partner, choice);
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -151,16 +150,17 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
|
||||
current_path = dir_path;
|
||||
entry_list.clear();
|
||||
auto filtering = !extension_filter.empty();
|
||||
bool cxx_file = path_iequal(cxx_ext, extension_filter);
|
||||
|
||||
text_current.set(dir_path.empty() ? "(sd root)" : truncate(dir_path, 24));
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(dir_path, u"*")) {
|
||||
// Hide files starting with '.' (hidden / tmp).
|
||||
if (is_hidden_file(entry.path()))
|
||||
if (!show_hidden_files && is_hidden_file(entry.path()))
|
||||
continue;
|
||||
|
||||
if (fs::is_regular_file(entry.status())) {
|
||||
if (!filtering || iequal(entry.path().extension(), extension_filter))
|
||||
if (!filtering || path_iequal(entry.path().extension(), extension_filter) || (cxx_file && is_cxx_capture_file(entry.path())))
|
||||
insert_sorted(entry_list, {entry.path(), (uint32_t)entry.size(), false});
|
||||
} else if (fs::is_directory(entry.status())) {
|
||||
insert_sorted(entry_list, {entry.path(), 0, true});
|
||||
@@ -191,11 +191,10 @@ FileManBaseView::FileManBaseView(
|
||||
extension_filter{filter} {
|
||||
add_children({&labels,
|
||||
&text_current,
|
||||
&text_info,
|
||||
&button_exit});
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
button_exit.on_select = [this](Button&) {
|
||||
nav_.pop();
|
||||
};
|
||||
|
||||
if (!sdcIsCardInserted(&SDCD1)) {
|
||||
@@ -256,8 +255,13 @@ void FileManBaseView::refresh_list() {
|
||||
auto entry_name = truncate(entry.path, 20);
|
||||
|
||||
if (entry.is_directory) {
|
||||
auto size_str =
|
||||
(entry.path == parent_dir_path)
|
||||
? ""
|
||||
: to_string_dec_uint(file_count(current_path / entry.path));
|
||||
|
||||
menu_view.add_item(
|
||||
{entry_name,
|
||||
{entry_name + std::string(21 - entry_name.length(), ' ') + size_str,
|
||||
ui::Color::yellow(),
|
||||
&bitmap_icon_dir,
|
||||
[this](KeyEvent key) {
|
||||
@@ -284,7 +288,6 @@ void FileManBaseView::refresh_list() {
|
||||
break;
|
||||
}
|
||||
|
||||
text_info.set(menu_view.item_count() >= max_items_shown ? "Too many files!" : "");
|
||||
menu_view.set_highlighted(prev_highlight);
|
||||
}
|
||||
|
||||
@@ -298,7 +301,7 @@ const FileManBaseView::file_assoc_t& FileManBaseView::get_assoc(
|
||||
size_t index = 0;
|
||||
|
||||
for (; index < file_types.size() - 1; ++index)
|
||||
if (iequal(ext, file_types[index].extension))
|
||||
if (path_iequal(ext, file_types[index].extension))
|
||||
return file_types[index];
|
||||
|
||||
// Default to last entry in the list.
|
||||
@@ -326,9 +329,9 @@ FileLoadView::FileLoadView(
|
||||
if (get_selected_entry().is_directory) {
|
||||
push_dir(get_selected_entry().path);
|
||||
} else {
|
||||
nav_.pop();
|
||||
if (on_changed)
|
||||
on_changed(get_selected_full_path());
|
||||
nav_.pop();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -348,7 +351,6 @@ FileSaveView::FileSaveView(
|
||||
file_{ file }
|
||||
{
|
||||
add_children({
|
||||
&labels,
|
||||
&text_path,
|
||||
&button_edit_path,
|
||||
&text_name,
|
||||
@@ -397,53 +399,79 @@ void FileSaveView::refresh_widgets() {
|
||||
*/
|
||||
/* FileManagerView ***********************************************************/
|
||||
|
||||
void FileManagerView::on_rename() {
|
||||
auto& entry = get_selected_entry();
|
||||
name_buffer = entry.path.filename().string();
|
||||
uint32_t cursor_pos = (uint32_t)name_buffer.length();
|
||||
void FileManagerView::refresh_widgets(const bool v) {
|
||||
button_rename.hidden(v);
|
||||
button_delete.hidden(v);
|
||||
button_cut.hidden(v);
|
||||
button_copy.hidden(v);
|
||||
button_paste.hidden(v);
|
||||
button_new_dir.hidden(v);
|
||||
button_new_file.hidden(v);
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void FileManagerView::on_rename(std::string_view hint) {
|
||||
auto& entry = get_selected_entry();
|
||||
|
||||
// Append the hint to the filename stem as a rename suggestion.
|
||||
name_buffer = entry.path.stem().string();
|
||||
if (!hint.empty())
|
||||
name_buffer += "_" + std::string{hint};
|
||||
name_buffer += entry.path.extension().string();
|
||||
|
||||
// Set the rename cursor to before the extension to make renaming simpler.
|
||||
uint32_t cursor_pos = (uint32_t)name_buffer.length();
|
||||
if (auto pos = name_buffer.find_last_of(".");
|
||||
pos != name_buffer.npos && !entry.is_directory)
|
||||
cursor_pos = pos;
|
||||
|
||||
text_prompt(nav_, name_buffer, cursor_pos, max_filename_length,
|
||||
[this](std::string& renamed) {
|
||||
auto renamed_path = fs::path{renamed};
|
||||
rename_file(get_selected_full_path(), current_path / renamed_path);
|
||||
text_prompt(
|
||||
nav_, name_buffer, cursor_pos, max_filename_length,
|
||||
[this](std::string& renamed) {
|
||||
auto renamed_path = fs::path{renamed};
|
||||
rename_file(get_selected_full_path(), current_path / renamed_path);
|
||||
|
||||
auto has_partner = partner_file_prompt(nav_, get_selected_full_path(), "Rename",
|
||||
[this, renamed_path](const fs::path& partner, bool should_rename) mutable {
|
||||
if (should_rename) {
|
||||
auto new_name = renamed_path.replace_extension(partner.extension());
|
||||
rename_file(partner, current_path / new_name);
|
||||
}
|
||||
reload_current();
|
||||
});
|
||||
|
||||
if (!has_partner)
|
||||
reload_current();
|
||||
auto has_partner = partner_file_prompt(
|
||||
nav_, get_selected_full_path(), "Rename",
|
||||
[this, renamed_path](const fs::path& partner, bool should_rename) mutable {
|
||||
if (should_rename) {
|
||||
auto new_name = renamed_path.replace_extension(partner.extension());
|
||||
rename_file(partner, current_path / new_name);
|
||||
}
|
||||
reload_current();
|
||||
});
|
||||
|
||||
if (!has_partner)
|
||||
reload_current();
|
||||
});
|
||||
}
|
||||
|
||||
void FileManagerView::on_delete() {
|
||||
if (is_directory(get_selected_full_path()) && !is_empty_directory(get_selected_full_path())) {
|
||||
nav_.display_modal("Delete", "Directory not empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = get_selected_entry().path.filename().string();
|
||||
nav_.push<ModalMessageView>("Delete", "Delete " + name + "\nAre you sure?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
delete_file(get_selected_full_path());
|
||||
nav_.push<ModalMessageView>(
|
||||
"Delete", "Delete " + name + "\nAre you sure?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
delete_file(get_selected_full_path());
|
||||
|
||||
auto has_partner = partner_file_prompt(
|
||||
nav_, get_selected_full_path(), "Delete",
|
||||
[this](const fs::path& partner, bool should_delete) {
|
||||
if (should_delete)
|
||||
delete_file(partner);
|
||||
reload_current();
|
||||
});
|
||||
auto has_partner = partner_file_prompt(
|
||||
nav_, get_selected_full_path(), "Delete",
|
||||
[this](const fs::path& partner, bool should_delete) {
|
||||
if (should_delete)
|
||||
delete_file(partner);
|
||||
reload_current();
|
||||
});
|
||||
|
||||
if (!has_partner)
|
||||
reload_current();
|
||||
}
|
||||
});
|
||||
if (!has_partner)
|
||||
reload_current();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void FileManagerView::on_new_dir() {
|
||||
@@ -454,14 +482,6 @@ void FileManagerView::on_new_dir() {
|
||||
});
|
||||
}
|
||||
|
||||
void FileManagerView::on_new_file() {
|
||||
name_buffer = "";
|
||||
text_prompt(nav_, name_buffer, max_filename_length, [this](std::string& file_name) {
|
||||
make_new_file(current_path / file_name);
|
||||
reload_current();
|
||||
});
|
||||
}
|
||||
|
||||
void FileManagerView::on_paste() {
|
||||
// TODO: handle partner file. Need to fix nav stack first.
|
||||
auto new_name = get_unique_filename(current_path, clipboard_path.filename());
|
||||
@@ -482,23 +502,48 @@ void FileManagerView::on_paste() {
|
||||
reload_current();
|
||||
}
|
||||
|
||||
void FileManagerView::on_new_file() {
|
||||
name_buffer = "";
|
||||
text_prompt(nav_, name_buffer, max_filename_length, [this](std::string& file_name) {
|
||||
make_new_file(current_path / file_name);
|
||||
reload_current();
|
||||
});
|
||||
}
|
||||
|
||||
bool FileManagerView::handle_file_open() {
|
||||
if (!selected_is_valid())
|
||||
return false;
|
||||
|
||||
auto path = get_selected_full_path();
|
||||
auto ext = path.extension();
|
||||
|
||||
if (path_iequal(txt_ext, ext)) {
|
||||
nav_.push<TextEditorView>(path);
|
||||
return true;
|
||||
} else if (is_cxx_capture_file(path) || path_iequal(ppl_ext, ext)) {
|
||||
// TODO: Enough memory to push?
|
||||
nav_.push<PlaylistView>(path);
|
||||
return true;
|
||||
} else if (path_iequal(png_ext, ext)) {
|
||||
nav_.push<ScreenshotViewer>(path);
|
||||
return true;
|
||||
} else if (path_iequal(bmp_ext, ext)) {
|
||||
nav_.push<SplashViewer>(path);
|
||||
reload_current();
|
||||
return true;
|
||||
} else if (path_iequal(rem_ext, ext)) {
|
||||
nav_.push<RemoteView>(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileManagerView::selected_is_valid() const {
|
||||
return !entry_list.empty() &&
|
||||
get_selected_entry().path != parent_dir_path;
|
||||
}
|
||||
|
||||
void FileManagerView::refresh_widgets(const bool v) {
|
||||
button_rename.hidden(v);
|
||||
button_delete.hidden(v);
|
||||
button_cut.hidden(v);
|
||||
button_copy.hidden(v);
|
||||
button_paste.hidden(v);
|
||||
button_new_dir.hidden(v);
|
||||
button_new_file.hidden(v);
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
FileManagerView::FileManagerView(
|
||||
NavigationView& nav)
|
||||
: FileManBaseView(nav, "") {
|
||||
@@ -510,22 +555,33 @@ FileManagerView::FileManagerView(
|
||||
refresh_widgets(v);
|
||||
};
|
||||
|
||||
add_children({&menu_view,
|
||||
&labels,
|
||||
&text_date,
|
||||
&button_rename,
|
||||
&button_delete,
|
||||
&button_cut,
|
||||
&button_copy,
|
||||
&button_paste,
|
||||
&button_new_dir,
|
||||
&button_new_file});
|
||||
add_children({
|
||||
&menu_view,
|
||||
&text_date,
|
||||
&button_rename,
|
||||
&button_delete,
|
||||
&button_cut,
|
||||
&button_copy,
|
||||
&button_paste,
|
||||
&button_new_dir,
|
||||
&button_new_file,
|
||||
&button_open_notepad,
|
||||
&button_rename_timestamp,
|
||||
&button_open_iq_trim,
|
||||
&button_show_hidden_files,
|
||||
});
|
||||
|
||||
menu_view.on_highlight = [this]() {
|
||||
if (selected_is_valid())
|
||||
text_date.set(to_string_FAT_timestamp(file_created_date(get_selected_full_path())));
|
||||
else
|
||||
text_date.set("");
|
||||
if (menu_view.highlighted_index() >= max_items_shown - 1) {
|
||||
text_date.set_style(&Styles::red);
|
||||
text_date.set("Too many files!");
|
||||
} else {
|
||||
text_date.set_style(&Styles::grey);
|
||||
if (selected_is_valid())
|
||||
text_date.set((is_directory(get_selected_full_path()) ? "Created " : "Modified ") + to_string_FAT_timestamp(file_created_date(get_selected_full_path())));
|
||||
else
|
||||
text_date.set("");
|
||||
}
|
||||
};
|
||||
|
||||
refresh_list();
|
||||
@@ -533,6 +589,8 @@ FileManagerView::FileManagerView(
|
||||
on_select_entry = [this](KeyEvent key) {
|
||||
if (key == KeyEvent::Select && get_selected_entry().is_directory) {
|
||||
push_dir(get_selected_entry().path);
|
||||
} else if (key == KeyEvent::Select && handle_file_open()) {
|
||||
return;
|
||||
} else {
|
||||
button_rename.focus();
|
||||
}
|
||||
@@ -540,7 +598,7 @@ FileManagerView::FileManagerView(
|
||||
|
||||
button_rename.on_select = [this]() {
|
||||
if (selected_is_valid())
|
||||
on_rename();
|
||||
on_rename("");
|
||||
};
|
||||
|
||||
button_delete.on_select = [this]() {
|
||||
@@ -578,6 +636,35 @@ FileManagerView::FileManagerView(
|
||||
button_new_file.on_select = [this]() {
|
||||
on_new_file();
|
||||
};
|
||||
|
||||
button_open_notepad.on_select = [this]() {
|
||||
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
||||
auto path = get_selected_full_path();
|
||||
nav_.push<TextEditorView>(path);
|
||||
} else
|
||||
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
|
||||
};
|
||||
|
||||
button_rename_timestamp.on_select = [this]() {
|
||||
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
||||
on_rename(::truncate(to_string_timestamp(rtc_time::now()), 8));
|
||||
} else
|
||||
nav_.display_modal("Timestamp Rename", "Can't rename that.");
|
||||
};
|
||||
|
||||
button_open_iq_trim.on_select = [this]() {
|
||||
auto path = get_selected_full_path();
|
||||
if (selected_is_valid() && !get_selected_entry().is_directory && is_cxx_capture_file(path)) {
|
||||
nav_.push<IQTrimView>(path);
|
||||
} else
|
||||
nav_.display_modal("IQ Trim", "Not a capture file.");
|
||||
};
|
||||
|
||||
button_show_hidden_files.on_select = [this]() {
|
||||
show_hidden_files = !show_hidden_files;
|
||||
button_show_hidden_files.set_color(show_hidden_files ? Color::green() : Color::dark_grey());
|
||||
reload_current();
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -77,7 +77,9 @@ class FileManBaseView : public View {
|
||||
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
||||
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
||||
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
|
||||
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
|
||||
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
|
||||
{u".REM", &bitmap_icon_remote, ui::Color::orange()}, // Remote
|
||||
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
|
||||
};
|
||||
|
||||
std::filesystem::path get_selected_full_path() const;
|
||||
@@ -102,6 +104,8 @@ class FileManBaseView : public View {
|
||||
std::vector<fileman_entry> entry_list{};
|
||||
std::vector<uint32_t> saved_index_stack{};
|
||||
|
||||
bool show_hidden_files{false};
|
||||
|
||||
Labels labels{
|
||||
{{0, 0}, "Path:", Color::light_grey()}};
|
||||
|
||||
@@ -114,13 +118,8 @@ class FileManBaseView : public View {
|
||||
{0, 2 * 8, 240, 26 * 8},
|
||||
true};
|
||||
|
||||
// HACK: for item count limit.
|
||||
Text text_info{
|
||||
{1 * 8, 35 * 8, 15 * 8, 16},
|
||||
""};
|
||||
|
||||
Button button_exit{
|
||||
{21 * 8, 34 * 8, 9 * 8, 32},
|
||||
{22 * 8, 34 * 8, 8 * 8, 32},
|
||||
"Exit"};
|
||||
};
|
||||
|
||||
@@ -206,20 +205,19 @@ class FileManagerView : public FileManBaseView {
|
||||
ClipboardMode clipboard_mode{ClipboardMode::None};
|
||||
|
||||
void refresh_widgets(const bool v);
|
||||
void on_rename();
|
||||
void on_rename(std::string_view hint);
|
||||
void on_delete();
|
||||
void on_paste();
|
||||
void on_new_dir();
|
||||
void on_new_file();
|
||||
|
||||
bool handle_file_open();
|
||||
|
||||
// True if the selected entry is a real file item.
|
||||
bool selected_is_valid() const;
|
||||
|
||||
Labels labels{
|
||||
{{0, 26 * 8}, "Created ", Color::light_grey()}};
|
||||
|
||||
Text text_date{
|
||||
{8 * 8, 26 * 8, 19 * 8, 16},
|
||||
{0 * 8, 26 * 8, 28 * 8, 16},
|
||||
""};
|
||||
|
||||
NewButton button_rename{
|
||||
@@ -263,6 +261,31 @@ class FileManagerView : public FileManBaseView {
|
||||
{},
|
||||
&bitmap_icon_new_file,
|
||||
Color::green()};
|
||||
|
||||
NewButton button_open_notepad{
|
||||
{0 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_notepad,
|
||||
Color::orange()};
|
||||
|
||||
NewButton button_rename_timestamp{
|
||||
{4 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_options_datetime,
|
||||
Color::orange(),
|
||||
/*vcenter*/ true};
|
||||
|
||||
NewButton button_open_iq_trim{
|
||||
{9 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_trim,
|
||||
Color::orange()};
|
||||
|
||||
NewButton button_show_hidden_files{
|
||||
{13 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_hide,
|
||||
Color::dark_grey()};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
static const char16_t* firmware_folder = u"/FIRMWARE";
|
||||
|
||||
Thread* FlashUtilityView::thread{nullptr};
|
||||
static constexpr size_t max_filename_length = 26;
|
||||
|
||||
@@ -35,7 +37,9 @@ FlashUtilityView::FlashUtilityView(NavigationView& nav)
|
||||
|
||||
menu_view.set_parent_rect({0, 3 * 8, 240, 33 * 8});
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(u"FIRMWARE", u"*.bin")) {
|
||||
ensure_directory(firmware_folder);
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(firmware_folder, u"*.bin")) {
|
||||
auto filename = entry.path().filename();
|
||||
auto path = entry.path().native();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -22,312 +23,451 @@
|
||||
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "binder.hpp"
|
||||
#include "event_m0.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// TODO: Clean up after moving to better lookup tables.
|
||||
using options_t = OptionsField::options_t;
|
||||
extern options_t freqman_modulations;
|
||||
extern options_t freqman_bandwidths[4];
|
||||
extern options_t freqman_steps;
|
||||
extern options_t freqman_steps_short;
|
||||
|
||||
/* Set options. */
|
||||
void freqman_set_modulation_option(OptionsField& option) {
|
||||
option.set_options(freqman_modulations);
|
||||
}
|
||||
|
||||
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
|
||||
if (is_valid(modulation))
|
||||
option.set_options(freqman_bandwidths[modulation]);
|
||||
}
|
||||
|
||||
void freqman_set_step_option(OptionsField& option) {
|
||||
option.set_options(freqman_steps);
|
||||
}
|
||||
|
||||
void freqman_set_step_option_short(OptionsField& option) {
|
||||
option.set_options(freqman_steps_short);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
static int32_t last_category_id{0};
|
||||
/* FreqManBaseView ***************************************/
|
||||
|
||||
size_t FreqManBaseView::current_category_index = 0;
|
||||
|
||||
FreqManBaseView::FreqManBaseView(
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
file_list = get_freqman_files();
|
||||
add_children(
|
||||
{&label_category,
|
||||
&options_category,
|
||||
&button_exit});
|
||||
|
||||
add_children({&label_category,
|
||||
&button_exit});
|
||||
|
||||
if (file_list.size()) {
|
||||
add_child(&options_category);
|
||||
populate_categories();
|
||||
} else
|
||||
error_ = ERROR_NOFILES;
|
||||
|
||||
// initialize
|
||||
change_category(last_category_id);
|
||||
|
||||
// Default function
|
||||
on_change_category = [this](int32_t category_id) {
|
||||
change_category(category_id);
|
||||
options_category.on_change = [this](size_t new_index, int32_t) {
|
||||
change_category(new_index);
|
||||
};
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
refresh_categories();
|
||||
};
|
||||
|
||||
void FreqManBaseView::focus() {
|
||||
button_exit.focus();
|
||||
|
||||
// TODO: Shouldn't be on focus.
|
||||
if (error_ == ERROR_ACCESS) {
|
||||
nav_.display_modal("Error", "File acces error", ABORT, nullptr);
|
||||
nav_.display_modal("Error", "File access error", ABORT);
|
||||
} else if (error_ == ERROR_NOFILES) {
|
||||
nav_.display_modal("Error", "No database files\nin /freqman", ABORT, nullptr);
|
||||
nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT);
|
||||
} else {
|
||||
options_category.focus();
|
||||
}
|
||||
}
|
||||
|
||||
void FreqManBaseView::populate_categories() {
|
||||
categories.clear();
|
||||
void FreqManBaseView::change_category(size_t new_index) {
|
||||
if (categories().empty())
|
||||
return;
|
||||
|
||||
for (size_t n = 0; n < file_list.size(); n++)
|
||||
categories.emplace_back(std::make_pair(file_list[n].substr(0, 14), n));
|
||||
current_category_index = new_index;
|
||||
if (!db_.open(get_freqman_path(current_category()))) {
|
||||
error_ = ERROR_ACCESS;
|
||||
}
|
||||
|
||||
// Alphabetical sort
|
||||
std::sort(categories.begin(), categories.end(), [](auto& left, auto& right) {
|
||||
freqlist_view.set_db(db_);
|
||||
}
|
||||
|
||||
void FreqManBaseView::refresh_categories() {
|
||||
OptionsField::options_t new_categories;
|
||||
|
||||
scan_root_files(
|
||||
freqman_dir, u"*.TXT", [&new_categories](const fs::path& path) {
|
||||
// Skip temp/hidden files.
|
||||
if (path.empty() || path.native()[0] == u'.')
|
||||
return;
|
||||
|
||||
// The UI layer will truncate long file names when displaying.
|
||||
new_categories.emplace_back(path.stem().string(), new_categories.size());
|
||||
});
|
||||
|
||||
// Alphabetically sort the categories.
|
||||
std::sort(new_categories.begin(), new_categories.end(), [](auto& left, auto& right) {
|
||||
return left.first < right.first;
|
||||
});
|
||||
|
||||
options_category.set_options(categories);
|
||||
options_category.set_selected_index(last_category_id);
|
||||
|
||||
options_category.on_change = [this](size_t category_id, int32_t) {
|
||||
if (on_change_category)
|
||||
on_change_category(category_id);
|
||||
};
|
||||
// Preserve last selection; ensure in range.
|
||||
current_category_index = clip(current_category_index, 0u, new_categories.size());
|
||||
auto saved_index = current_category_index;
|
||||
options_category.set_options(std::move(new_categories));
|
||||
options_category.set_selected_index(saved_index);
|
||||
}
|
||||
|
||||
void FreqManBaseView::change_category(int32_t category_id) {
|
||||
if (!file_list.size()) return;
|
||||
|
||||
last_category_id = current_category_id = category_id;
|
||||
|
||||
if (!load_freqman_file(file_list[categories[current_category_id].second], database))
|
||||
error_ = ERROR_ACCESS;
|
||||
else
|
||||
refresh_list();
|
||||
void FreqManBaseView::refresh_list(int delta_selected) {
|
||||
// Update the index and ensures in bounds.
|
||||
freqlist_view.set_index(freqlist_view.get_index() + delta_selected);
|
||||
freqlist_view.set_dirty();
|
||||
}
|
||||
|
||||
void FreqManBaseView::refresh_list() {
|
||||
if (!database.size()) {
|
||||
if (on_refresh_widgets)
|
||||
on_refresh_widgets(true);
|
||||
} else {
|
||||
if (on_refresh_widgets)
|
||||
on_refresh_widgets(false);
|
||||
|
||||
menu_view.clear();
|
||||
|
||||
for (size_t n = 0; n < database.size(); n++) {
|
||||
menu_view.add_item({freqman_item_string(database[n], 30),
|
||||
ui::Color::white(),
|
||||
nullptr,
|
||||
[this](KeyEvent) {
|
||||
if (on_select_frequency)
|
||||
on_select_frequency();
|
||||
}});
|
||||
}
|
||||
|
||||
menu_view.set_highlighted(0); // Refresh
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencySaveView::save_current_file() {
|
||||
if (database.size() > FREQMAN_MAX_PER_FILE) {
|
||||
nav_.display_modal(
|
||||
"Error", "Too many entries, maximum is\n" FREQMAN_MAX_PER_FILE_STR ". Trim list ?",
|
||||
YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
database.resize(FREQMAN_MAX_PER_FILE);
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
}
|
||||
nav_.pop();
|
||||
});
|
||||
} else {
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
nav_.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencySaveView::on_save_name() {
|
||||
text_prompt(nav_, desc_buffer, 28, [this](std::string& buffer) {
|
||||
database.push_back({value_, 0, buffer, SINGLE});
|
||||
save_current_file();
|
||||
});
|
||||
}
|
||||
|
||||
void FrequencySaveView::on_save_timestamp() {
|
||||
database.push_back({value_, 0, live_timestamp.string(), SINGLE});
|
||||
save_current_file();
|
||||
}
|
||||
/* FrequencySaveView *************************************/
|
||||
|
||||
FrequencySaveView::FrequencySaveView(
|
||||
NavigationView& nav,
|
||||
const rf::Frequency value)
|
||||
: FreqManBaseView(nav),
|
||||
value_(value) {
|
||||
desc_buffer.reserve(28);
|
||||
: FreqManBaseView(nav) {
|
||||
add_children(
|
||||
{&labels,
|
||||
&big_display,
|
||||
&button_save,
|
||||
&field_description});
|
||||
|
||||
// Todo: add back ?
|
||||
/*for (size_t n = 0; n < database.size(); n++) {
|
||||
if (database[n].value == value_) {
|
||||
error_ = ERROR_DUPLICATE;
|
||||
break;
|
||||
}
|
||||
}*/
|
||||
entry_.type = freqman_type::Single;
|
||||
entry_.frequency_a = value;
|
||||
entry_.description = to_string_timestamp(rtc_time::now());
|
||||
|
||||
add_children({&labels,
|
||||
&big_display,
|
||||
&button_save_name,
|
||||
&button_save_timestamp,
|
||||
&live_timestamp});
|
||||
bind(field_description, entry_.description, nav);
|
||||
|
||||
big_display.set(value);
|
||||
|
||||
button_save_name.on_select = [this, &nav](Button&) {
|
||||
on_save_name();
|
||||
};
|
||||
button_save_timestamp.on_select = [this, &nav](Button&) {
|
||||
on_save_timestamp();
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
db_.insert_entry(db_.entry_count(), entry_);
|
||||
nav_.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void FrequencyLoadView::refresh_widgets(const bool v) {
|
||||
menu_view.hidden(v);
|
||||
text_empty.hidden(!v);
|
||||
// display.fill_rectangle(menu_view.screen_rect(), Color::black());
|
||||
set_dirty();
|
||||
void FrequencySaveView::focus() {
|
||||
refresh_ui();
|
||||
FreqManBaseView::focus();
|
||||
}
|
||||
|
||||
void FrequencySaveView::refresh_ui() {
|
||||
big_display.set(entry_.frequency_a);
|
||||
}
|
||||
|
||||
/* FrequencyLoadView *************************************/
|
||||
|
||||
FrequencyLoadView::FrequencyLoadView(
|
||||
NavigationView& nav)
|
||||
: FreqManBaseView(nav) {
|
||||
on_refresh_widgets = [this](bool v) {
|
||||
refresh_widgets(v);
|
||||
add_children({&freqlist_view});
|
||||
|
||||
// Resize to fill screen. +2 keeps text out of border.
|
||||
freqlist_view.set_parent_rect({0, 3 * 8, screen_width, 15 * 16 + 2});
|
||||
|
||||
freqlist_view.on_select = [&nav, this](size_t index) {
|
||||
auto entry = db_[index];
|
||||
// TODO: Maybe return center of range if user choses a range when the app
|
||||
// needs a unique frequency, instead of frequency_a?
|
||||
auto has_range = entry.type == freqman_type::Range ||
|
||||
entry.type == freqman_type::HamRadio;
|
||||
|
||||
if (on_range_loaded && has_range)
|
||||
on_range_loaded(entry.frequency_a, entry.frequency_b);
|
||||
else if (on_frequency_loaded)
|
||||
on_frequency_loaded(entry.frequency_a);
|
||||
|
||||
nav_.pop(); // NB: this will call dtor.
|
||||
};
|
||||
|
||||
add_children({&menu_view,
|
||||
&text_empty});
|
||||
|
||||
// Resize menu view to fill screen
|
||||
menu_view.set_parent_rect({0, 3 * 8, 240, 30 * 8});
|
||||
|
||||
// Just to allow exit on left
|
||||
menu_view.on_left = [&nav, this]() {
|
||||
nav.pop();
|
||||
freqlist_view.on_leave = [this]() {
|
||||
button_exit.focus();
|
||||
};
|
||||
}
|
||||
|
||||
change_category(last_category_id);
|
||||
refresh_list();
|
||||
/* FrequencyManagerView **********************************/
|
||||
|
||||
on_select_frequency = [&nav, this]() {
|
||||
nav_.pop();
|
||||
void FrequencyManagerView::on_edit_entry() {
|
||||
auto edit_view = nav_.push<FrequencyEditView>(current_entry());
|
||||
edit_view->on_save = [this](const freqman_entry& entry) {
|
||||
db_.replace_entry(current_index(), entry);
|
||||
freqlist_view.set_dirty();
|
||||
};
|
||||
}
|
||||
|
||||
auto& entry = database[menu_view.highlighted_index()];
|
||||
void FrequencyManagerView::on_edit_freq() {
|
||||
auto freq_edit_view = nav_.push<FrequencyKeypadView>(current_entry().frequency_a);
|
||||
freq_edit_view->on_changed = [this](rf::Frequency f) {
|
||||
auto entry = current_entry();
|
||||
entry.frequency_a = f;
|
||||
db_.replace_entry(current_index(), entry);
|
||||
freqlist_view.set_dirty();
|
||||
};
|
||||
}
|
||||
|
||||
if (entry.type == RANGE) {
|
||||
// User chose a frequency range entry
|
||||
if (on_range_loaded)
|
||||
on_range_loaded(entry.frequency_a, entry.frequency_b);
|
||||
else if (on_frequency_loaded)
|
||||
on_frequency_loaded(entry.frequency_a);
|
||||
// TODO: Maybe return center of range if user choses a range when the app needs a unique frequency, instead of frequency_a ?
|
||||
} else {
|
||||
// User chose an unique frequency entry
|
||||
if (on_frequency_loaded)
|
||||
on_frequency_loaded(entry.frequency_a);
|
||||
void FrequencyManagerView::on_edit_desc() {
|
||||
temp_buffer_ = current_entry().description;
|
||||
text_prompt(nav_, temp_buffer_, desc_edit_max, [this](std::string& new_desc) {
|
||||
auto entry = current_entry();
|
||||
entry.description = std::move(new_desc);
|
||||
db_.replace_entry(current_index(), entry);
|
||||
freqlist_view.set_dirty();
|
||||
});
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_add_category() {
|
||||
temp_buffer_.clear();
|
||||
text_prompt(nav_, temp_buffer_, 20, [this](std::string& new_name) {
|
||||
if (!new_name.empty()) {
|
||||
create_freqman_file(new_name);
|
||||
refresh_categories();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_del_category() {
|
||||
nav_.push<ModalMessageView>(
|
||||
"Delete", "Delete " + current_category() + "\nAre you sure?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
db_.close(); // Ensure file is closed.
|
||||
auto path = get_freqman_path(current_category());
|
||||
delete_file(path);
|
||||
refresh_categories();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_add_entry() {
|
||||
freqman_entry entry{
|
||||
.frequency_a = 100'000'000,
|
||||
.description = std::string{"Entry "} + to_string_dec_uint(db_.entry_count()),
|
||||
.type = freqman_type::Single,
|
||||
};
|
||||
|
||||
// Add will insert below the currently selected item.
|
||||
db_.insert_entry(current_index() + 1, entry);
|
||||
refresh_list(1);
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_edit_freq(rf::Frequency f) {
|
||||
database[menu_view.highlighted_index()].frequency_a = f;
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
refresh_list();
|
||||
}
|
||||
void FrequencyManagerView::on_del_entry() {
|
||||
if (db_.empty())
|
||||
return;
|
||||
|
||||
void FrequencyManagerView::on_edit_desc(NavigationView& nav) {
|
||||
text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) {
|
||||
database[menu_view.highlighted_index()].description = buffer;
|
||||
refresh_list();
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
});
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_new_category(NavigationView& nav) {
|
||||
text_prompt(nav, desc_buffer, 12, [this](std::string& buffer) {
|
||||
File freqman_file;
|
||||
create_freqman_file(buffer, freqman_file);
|
||||
});
|
||||
populate_categories();
|
||||
refresh_list();
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_delete() {
|
||||
database.erase(database.begin() + menu_view.highlighted_index());
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
refresh_list();
|
||||
}
|
||||
|
||||
void FrequencyManagerView::refresh_widgets(const bool v) {
|
||||
button_edit_freq.hidden(v);
|
||||
button_edit_desc.hidden(v);
|
||||
button_delete.hidden(v);
|
||||
menu_view.hidden(v);
|
||||
text_empty.hidden(!v);
|
||||
// display.fill_rectangle(menu_view.screen_rect(), Color::black());
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
FrequencyManagerView::~FrequencyManagerView() {
|
||||
// save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
nav_.push<ModalMessageView>(
|
||||
"Delete", "Delete " + trim(pretty_string(current_entry(), 23)) + "\nAre you sure?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice) {
|
||||
db_.delete_entry(current_index());
|
||||
refresh_list();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FrequencyManagerView::FrequencyManagerView(
|
||||
NavigationView& nav)
|
||||
: FreqManBaseView(nav) {
|
||||
on_refresh_widgets = [this](bool v) {
|
||||
refresh_widgets(v);
|
||||
add_children(
|
||||
{&freqlist_view,
|
||||
&button_add_category,
|
||||
&button_del_category,
|
||||
&button_edit_entry,
|
||||
&rect_padding,
|
||||
&button_edit_freq,
|
||||
&button_edit_desc,
|
||||
&button_add_entry,
|
||||
&button_del_entry});
|
||||
|
||||
freqlist_view.on_select = [this](size_t) {
|
||||
button_edit_entry.focus();
|
||||
};
|
||||
// Allows for quickly exiting control.
|
||||
freqlist_view.on_leave = [this]() {
|
||||
button_edit_entry.focus();
|
||||
};
|
||||
|
||||
add_children({&labels,
|
||||
&button_new_category,
|
||||
&menu_view,
|
||||
&text_empty,
|
||||
&button_edit_freq,
|
||||
&button_edit_desc,
|
||||
&button_delete});
|
||||
|
||||
// Just to allow exit on left
|
||||
menu_view.on_left = [&nav, this]() {
|
||||
nav.pop();
|
||||
button_add_category.on_select = [this]() {
|
||||
on_add_category();
|
||||
};
|
||||
|
||||
change_category(last_category_id);
|
||||
refresh_list();
|
||||
|
||||
on_select_frequency = [this]() {
|
||||
button_edit_freq.focus();
|
||||
button_del_category.on_select = [this]() {
|
||||
on_del_category();
|
||||
};
|
||||
|
||||
button_new_category.on_select = [this, &nav](Button&) {
|
||||
desc_buffer = "";
|
||||
on_new_category(nav);
|
||||
button_edit_entry.on_select = [this](Button&) {
|
||||
on_edit_entry();
|
||||
};
|
||||
|
||||
button_edit_freq.on_select = [this, &nav](Button&) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(database[menu_view.highlighted_index()].frequency_a);
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
on_edit_freq(f);
|
||||
};
|
||||
button_edit_freq.on_select = [this](Button&) {
|
||||
on_edit_freq();
|
||||
};
|
||||
|
||||
button_edit_desc.on_select = [this, &nav](Button&) {
|
||||
desc_buffer = database[menu_view.highlighted_index()].description;
|
||||
on_edit_desc(nav);
|
||||
button_edit_desc.on_select = [this](Button&) {
|
||||
on_edit_desc();
|
||||
};
|
||||
|
||||
button_delete.on_select = [this, &nav](Button&) {
|
||||
nav.push<ModalMessageView>("Confirm", "Are you sure ?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice)
|
||||
on_delete();
|
||||
});
|
||||
button_add_entry.on_select = [this]() {
|
||||
on_add_entry();
|
||||
};
|
||||
|
||||
button_del_entry.on_select = [this]() {
|
||||
on_del_entry();
|
||||
};
|
||||
}
|
||||
|
||||
/* FrequencyEditView *************************************/
|
||||
|
||||
FrequencyEditView::FrequencyEditView(
|
||||
NavigationView& nav,
|
||||
freqman_entry entry)
|
||||
: nav_{nav},
|
||||
entry_{std::move(entry)} {
|
||||
add_children({&labels,
|
||||
&field_type,
|
||||
&field_freq_a,
|
||||
&field_freq_b,
|
||||
&field_modulation,
|
||||
&field_bandwidth,
|
||||
&field_step,
|
||||
&field_tone,
|
||||
&field_description,
|
||||
&text_validation,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
freqman_set_modulation_option(field_modulation);
|
||||
populate_bandwidth_options();
|
||||
populate_step_options();
|
||||
populate_tone_options();
|
||||
|
||||
// Add the "invalid/unset" option. Kind of hacky, but...
|
||||
field_modulation.options().insert(
|
||||
field_modulation.options().begin(), {"None", -1});
|
||||
field_step.options().insert(
|
||||
field_step.options().begin(), {"None", -1});
|
||||
|
||||
bind(field_type, entry_.type, [this](auto) {
|
||||
refresh_ui();
|
||||
});
|
||||
|
||||
bind(field_freq_a, entry_.frequency_a, nav, [this](auto) {
|
||||
refresh_ui();
|
||||
});
|
||||
|
||||
bind(field_freq_b, entry_.frequency_b, nav, [this](auto) {
|
||||
refresh_ui();
|
||||
});
|
||||
|
||||
bind(field_modulation, entry_.modulation, [this](auto) {
|
||||
populate_bandwidth_options();
|
||||
});
|
||||
|
||||
bind(field_bandwidth, entry_.bandwidth);
|
||||
bind(field_step, entry_.step);
|
||||
bind(field_tone, entry_.tone);
|
||||
bind(field_description, entry_.description, nav_);
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
if (on_save)
|
||||
on_save(std::move(entry_));
|
||||
nav_.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [this](Button&) {
|
||||
nav_.pop();
|
||||
};
|
||||
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void FrequencyEditView::focus() {
|
||||
button_cancel.focus();
|
||||
}
|
||||
|
||||
void FrequencyEditView::refresh_ui() {
|
||||
// This needs to be called whenever a UI option is changed
|
||||
// in a way that causes fields to be unused or would make the
|
||||
// entry fail validation.
|
||||
|
||||
auto is_range = entry_.type == freqman_type::Range;
|
||||
auto is_ham = entry_.type == freqman_type::HamRadio;
|
||||
auto is_repeater = entry_.type == freqman_type::Repeater;
|
||||
auto has_freq_b = is_range || is_ham || is_repeater;
|
||||
|
||||
field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey);
|
||||
field_step.set_style(is_range ? &Styles::white : &Styles::grey);
|
||||
field_tone.set_style(is_ham ? &Styles::white : &Styles::grey);
|
||||
|
||||
if (is_valid(entry_)) {
|
||||
text_validation.set("Valid");
|
||||
text_validation.set_style(&Styles::green);
|
||||
} else {
|
||||
text_validation.set("Error");
|
||||
text_validation.set_style(&Styles::red);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: The following functions shouldn't be needed once
|
||||
// freqman_db lookup tables are complete.
|
||||
void FrequencyEditView::populate_bandwidth_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
if (entry_.modulation < std::size(freqman_bandwidths)) {
|
||||
auto& bandwidths = freqman_bandwidths[entry_.modulation];
|
||||
for (auto i = 0u; i < bandwidths.size(); ++i) {
|
||||
auto& item = bandwidths[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
}
|
||||
|
||||
field_bandwidth.set_options(std::move(options));
|
||||
}
|
||||
|
||||
void FrequencyEditView::populate_step_options() {
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 0u; i < freqman_steps.size(); ++i) {
|
||||
auto& item = freqman_steps[i];
|
||||
options.push_back({item.first, (OptionsField::value_t)i});
|
||||
}
|
||||
|
||||
field_step.set_options(std::move(options));
|
||||
}
|
||||
|
||||
void FrequencyEditView::populate_tone_options() {
|
||||
using namespace tonekey;
|
||||
OptionsField::options_t options;
|
||||
options.push_back({"None", -1});
|
||||
|
||||
for (auto i = 0u; i < tone_keys.size(); ++i) {
|
||||
auto& item = tone_keys[i];
|
||||
options.push_back({fx100_string(item.second), (OptionsField::value_t)i});
|
||||
}
|
||||
|
||||
field_tone.set_options(std::move(options));
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -20,93 +21,93 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "freqman.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_freqlist.hpp"
|
||||
#include "ui_menu.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class FreqManBaseView : public View {
|
||||
public:
|
||||
FreqManBaseView(
|
||||
NavigationView& nav);
|
||||
FreqManBaseView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
static constexpr size_t desc_edit_max = 0x80;
|
||||
|
||||
protected:
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
using options_t = OptionsField::options_t;
|
||||
|
||||
NavigationView& nav_;
|
||||
freqman_error error_{NO_ERROR};
|
||||
options_t categories{};
|
||||
std::function<void(int32_t category_id)> on_change_category{nullptr};
|
||||
std::function<void(void)> on_select_frequency{nullptr};
|
||||
std::function<void(bool)> on_refresh_widgets{nullptr};
|
||||
std::vector<std::string> file_list{};
|
||||
int32_t current_category_id{0};
|
||||
std::function<void(void)> on_select_frequency{};
|
||||
|
||||
void populate_categories();
|
||||
void change_category(int32_t category_id);
|
||||
void refresh_list();
|
||||
void change_category(size_t new_index);
|
||||
/* Access the categories directly from the OptionsField.
|
||||
* This avoids holding multiple copies of the file list. */
|
||||
const options_t& categories() const { return options_category.options(); }
|
||||
const auto& current_category() const { return options_category.selected_index_name(); }
|
||||
auto current_index() const { return freqlist_view.get_index(); }
|
||||
freqman_entry current_entry() const { return db_[current_index()]; }
|
||||
void refresh_categories();
|
||||
void refresh_list(int delta_selected = 0);
|
||||
|
||||
freqman_db database{};
|
||||
FreqmanDB db_{};
|
||||
|
||||
/* The top section (category) is 20px tall. */
|
||||
Labels label_category{
|
||||
{{0, 4}, "Category:", Color::light_grey()}};
|
||||
{{0, 2}, "Category:", Color::light_grey()}};
|
||||
|
||||
OptionsField options_category{
|
||||
{9 * 8, 4},
|
||||
14,
|
||||
{9 * 8, 2},
|
||||
14 /* length */,
|
||||
{}};
|
||||
|
||||
MenuView menu_view{
|
||||
{0, 3 * 8, 240, 23 * 8},
|
||||
true};
|
||||
Text text_empty{
|
||||
{7 * 8, 12 * 8, 16 * 8, 16},
|
||||
"Empty category !",
|
||||
};
|
||||
FreqManUIList freqlist_view{
|
||||
{0, 3 * 8, screen_width, 12 * 16 + 2 /* 2 Keeps text out of border. */}};
|
||||
|
||||
Button button_exit{
|
||||
{20 * 8, 34 * 8, 10 * 8, 4 * 8},
|
||||
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||
"Exit"};
|
||||
|
||||
protected:
|
||||
/* Static so selected category is persisted across UI instances. */
|
||||
static size_t current_category_index;
|
||||
};
|
||||
|
||||
// TODO: support for new category.
|
||||
class FrequencySaveView : public FreqManBaseView {
|
||||
public:
|
||||
FrequencySaveView(NavigationView& nav, const rf::Frequency value);
|
||||
|
||||
std::string title() const override { return "Save freq."; };
|
||||
std::string title() const override { return "Save freq"; }
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
std::string desc_buffer{};
|
||||
rf::Frequency value_{};
|
||||
freqman_entry entry_{};
|
||||
|
||||
void on_save_name();
|
||||
void on_save_timestamp();
|
||||
void save_current_file();
|
||||
void refresh_ui();
|
||||
|
||||
BigFrequency big_display{
|
||||
{4, 2 * 16, 28 * 8, 32},
|
||||
{0, 2 * 16, 28 * 8, 4 * 16},
|
||||
0};
|
||||
|
||||
Labels labels{
|
||||
{{1 * 8, 12 * 8}, "Save as:", Color::white()}};
|
||||
{{0 * 8, 6 * 16}, "Description:", Color::white()}};
|
||||
|
||||
Button button_save_name{
|
||||
{1 * 8, 17 * 8, 12 * 8, 48},
|
||||
"Name (set)"};
|
||||
Button button_save_timestamp{
|
||||
{1 * 8, 25 * 8, 12 * 8, 48},
|
||||
"Timestamp:"};
|
||||
LiveDateTime live_timestamp{
|
||||
{14 * 8, 27 * 8, 16 * 8, 16}};
|
||||
TextField field_description{
|
||||
{0 * 8, 7 * 16, 30 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
Button button_save{
|
||||
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||
"Save"};
|
||||
};
|
||||
|
||||
class FrequencyLoadView : public FreqManBaseView {
|
||||
@@ -115,46 +116,147 @@ class FrequencyLoadView : public FreqManBaseView {
|
||||
std::function<void(rf::Frequency, rf::Frequency)> on_range_loaded{};
|
||||
|
||||
FrequencyLoadView(NavigationView& nav);
|
||||
|
||||
std::string title() const override { return "Load freq."; };
|
||||
|
||||
private:
|
||||
void refresh_widgets(const bool v);
|
||||
std::string title() const override { return "Load freq"; }
|
||||
};
|
||||
|
||||
class FrequencyManagerView : public FreqManBaseView {
|
||||
public:
|
||||
FrequencyManagerView(NavigationView& nav);
|
||||
~FrequencyManagerView();
|
||||
|
||||
std::string title() const override { return "Freqman"; };
|
||||
std::string title() const override { return "Freqman"; }
|
||||
|
||||
private:
|
||||
std::string desc_buffer{};
|
||||
std::string temp_buffer_{};
|
||||
|
||||
void refresh_widgets(const bool v);
|
||||
void on_edit_freq(rf::Frequency f);
|
||||
void on_edit_desc(NavigationView& nav);
|
||||
void on_new_category(NavigationView& nav);
|
||||
void on_delete();
|
||||
void on_edit_entry();
|
||||
void on_edit_freq();
|
||||
void on_edit_desc();
|
||||
void on_add_category();
|
||||
void on_del_category();
|
||||
void on_add_entry();
|
||||
void on_del_entry();
|
||||
|
||||
Labels labels{
|
||||
{{4 * 8 + 4, 26 * 8}, "Edit:", Color::light_grey()}};
|
||||
NewButton button_add_category{
|
||||
{23 * 8, 0 * 16, 7 * 4, 20},
|
||||
{},
|
||||
&bitmap_icon_new_file,
|
||||
Color::white(),
|
||||
true};
|
||||
|
||||
Button button_new_category{
|
||||
{23 * 8, 2, 7 * 8, 20},
|
||||
"New"};
|
||||
NewButton button_del_category{
|
||||
{26 * 8 + 4, 0 * 16, 7 * 4, 20},
|
||||
{},
|
||||
&bitmap_icon_trash,
|
||||
Color::red(),
|
||||
true};
|
||||
|
||||
Button button_edit_entry{
|
||||
{0 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
|
||||
"Edit"};
|
||||
|
||||
Rectangle rect_padding{
|
||||
{15 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
|
||||
Color::grey()};
|
||||
|
||||
Button button_edit_freq{
|
||||
{0 * 8, 29 * 8, 14 * 8, 32},
|
||||
{0 * 8, 15 * 16, 15 * 8, 2 * 16},
|
||||
"Frequency"};
|
||||
|
||||
Button button_edit_desc{
|
||||
{0 * 8, 34 * 8, 14 * 8, 32},
|
||||
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||
"Description"};
|
||||
|
||||
Button button_delete{
|
||||
{18 * 8, 27 * 8, 12 * 8, 32},
|
||||
"Delete"};
|
||||
NewButton button_add_entry{
|
||||
{15 * 8, 15 * 16, 7 * 8 + 4, 2 * 16},
|
||||
{},
|
||||
&bitmap_icon_add,
|
||||
Color::white(),
|
||||
true};
|
||||
|
||||
NewButton button_del_entry{
|
||||
{22 * 8 + 4, 15 * 16, 7 * 8 + 4, 2 * 16},
|
||||
{},
|
||||
&bitmap_icon_delete,
|
||||
Color::red(),
|
||||
true};
|
||||
};
|
||||
|
||||
class FrequencyEditView : public View {
|
||||
public:
|
||||
std::function<void(const freqman_entry&)> on_save{};
|
||||
|
||||
FrequencyEditView(
|
||||
NavigationView& nav,
|
||||
freqman_entry entry);
|
||||
std::string title() const override { return "Freqman Edit"; }
|
||||
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
freqman_entry entry_;
|
||||
std::string temp_buffer_{};
|
||||
|
||||
void refresh_ui();
|
||||
void populate_bandwidth_options();
|
||||
void populate_step_options();
|
||||
void populate_tone_options();
|
||||
|
||||
Labels labels{
|
||||
{{5 * 8, 1 * 16}, "Edit Frequency Entry", Color::white()},
|
||||
{{0 * 8, 3 * 16}, "Entry Type :", Color::light_grey()},
|
||||
{{0 * 8, 4 * 16}, "Frequency A:", Color::light_grey()},
|
||||
{{0 * 8, 5 * 16}, "Frequency B:", Color::light_grey()},
|
||||
{{0 * 8, 6 * 16}, "Modulation :", Color::light_grey()},
|
||||
{{0 * 8, 7 * 16}, "Bandwidth :", Color::light_grey()},
|
||||
{{0 * 8, 8 * 16}, "Step :", Color::light_grey()},
|
||||
{{0 * 8, 9 * 16}, "Tone Freq :", Color::light_grey()},
|
||||
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField field_type{
|
||||
{13 * 8, 3 * 16},
|
||||
8,
|
||||
{
|
||||
{"Single", 0},
|
||||
{"Range", 1},
|
||||
{"HamRadio", 2},
|
||||
{"Repeater", 3},
|
||||
{"Raw", 4},
|
||||
}};
|
||||
|
||||
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
|
||||
|
||||
FrequencyField field_freq_b{{13 * 8, 5 * 16}};
|
||||
|
||||
OptionsField field_modulation{{13 * 8, 6 * 16}, 5, {}};
|
||||
|
||||
OptionsField field_bandwidth{{13 * 8, 7 * 16}, 7, {}};
|
||||
|
||||
OptionsField field_step{{13 * 8, 8 * 16}, 12, {}};
|
||||
|
||||
OptionsField field_tone{{13 * 8, 9 * 16}, 13, {}};
|
||||
|
||||
TextField field_description{
|
||||
{13 * 8, 10 * 16, 17 * 8, 1 * 16},
|
||||
{}};
|
||||
|
||||
Text text_validation{
|
||||
{12 * 8, 12 * 16, 5 * 8, 1 * 16},
|
||||
{}};
|
||||
|
||||
Button button_save{
|
||||
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||
"Cancel"};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
|
||||
void freqman_set_modulation_option(ui::OptionsField& option);
|
||||
void freqman_set_step_option(ui::OptionsField& option);
|
||||
void freqman_set_step_option_short(ui::OptionsField& option);
|
||||
void freqman_set_tone_option(ui::OptionsField& option);
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* 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_fsk_rx.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
void FskRxLogger::log_raw_data(const std::string& data, const uint32_t frequency) {
|
||||
std::string entry = "Raw: F:" + to_string_dec_uint(frequency) + "Hz";
|
||||
|
||||
// // Raw hex dump of all the codewords
|
||||
// for (size_t c = 0; c < 16; c++)
|
||||
// entry += to_string_hex(packet[c], 8) + " ";
|
||||
|
||||
log_file.write_entry(data + entry);
|
||||
}
|
||||
|
||||
void FskRxLogger::log_decoded(Timestamp timestamp, const std::string& text) {
|
||||
log_file.write_entry(timestamp, text);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
// Console View
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
FskRxAppConsoleView::FskRxAppConsoleView(NavigationView& nav, Rect parent_rect)
|
||||
: View(parent_rect), nav_{nav} {
|
||||
add_child(&console);
|
||||
};
|
||||
|
||||
void FskRxAppConsoleView::on_packet(uint32_t value, bool is_data) {
|
||||
if (is_data) {
|
||||
console.write(to_string_dec_uint(value) + " ");
|
||||
}
|
||||
}
|
||||
|
||||
void FskRxAppConsoleView::on_show() {
|
||||
hidden(false);
|
||||
}
|
||||
|
||||
void FskRxAppConsoleView::on_hide() {
|
||||
hidden(true);
|
||||
}
|
||||
|
||||
FskRxAppConsoleView::~FskRxAppConsoleView() {
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
// Spectrum View
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
FskRxAppView::FskRxAppView(NavigationView& nav, Rect parent_rect)
|
||||
: View(parent_rect), nav_{nav} {
|
||||
add_child(&waterfall);
|
||||
hidden(true);
|
||||
}
|
||||
|
||||
FskRxAppView::~FskRxAppView() {
|
||||
}
|
||||
|
||||
void FskRxAppView::focus() {
|
||||
}
|
||||
|
||||
void FskRxAppView::on_show() {
|
||||
hidden(false);
|
||||
waterfall.start();
|
||||
}
|
||||
|
||||
void FskRxAppView::on_hide() {
|
||||
hidden(true);
|
||||
waterfall.stop();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
// Base View
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
FskxRxMainView::FskxRxMainView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&tab_view,
|
||||
&view_data,
|
||||
&view_stream,
|
||||
&labels,
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&deviation_frequency,
|
||||
&record_view});
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fskrx);
|
||||
|
||||
// DEBUG
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
deviation_frequency.on_change = [this](rf::Frequency f) {
|
||||
refresh_ui(f);
|
||||
};
|
||||
|
||||
// Set initial sampling rate
|
||||
/* Bandwidth of 2FSK is 2 * Deviation */
|
||||
record_view.set_sampling_rate(initial_deviation * 2);
|
||||
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
deviation_frequency.set_value(initial_deviation);
|
||||
|
||||
logger.append(LOG_ROOT_DIR "/FSKRX.TXT");
|
||||
|
||||
baseband::set_fsk(initial_deviation);
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void FskxRxMainView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
|
||||
if (logging()) {
|
||||
logger.log_decoded(timestamp, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
void FskxRxMainView::refresh_ui(rf::Frequency deviationHz) {
|
||||
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
|
||||
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
|
||||
/* Bandwidth of 2FSK is 2 * Deviation */
|
||||
auto sample_rate = deviationHz * 2;
|
||||
|
||||
/* base_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
if (!view_stream.hidden()) {
|
||||
view_stream.waterfall.stop();
|
||||
}
|
||||
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
// NB: record_view is what actually updates proc_capture baseband settings.
|
||||
auto actual_sample_rate = record_view.set_sampling_rate(sample_rate);
|
||||
|
||||
// Update the radio model with the actual sampling rate.
|
||||
receiver_model.set_sampling_rate(actual_sample_rate);
|
||||
|
||||
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
|
||||
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
|
||||
|
||||
if (!view_stream.hidden()) {
|
||||
view_stream.waterfall.start();
|
||||
}
|
||||
}
|
||||
|
||||
void FskxRxMainView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void FskxRxMainView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
ui::Rect waterfall_rect{0, 0, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
view_stream.waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
FskxRxMainView::~FskxRxMainView() {
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* 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_FSK_RX_H__
|
||||
#define __UI_FSK_RX_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "ui_tabview.hpp"
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "pocsag_app.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class FskRxLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data, const uint32_t frequency);
|
||||
void log_decoded(Timestamp timestamp, const std::string& text);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
class FskRxAppConsoleView : public View {
|
||||
public:
|
||||
FskRxAppConsoleView(NavigationView& nav, Rect parent_rec);
|
||||
~FskRxAppConsoleView();
|
||||
|
||||
std::string title() const override { return "FSK RX Data"; };
|
||||
|
||||
void on_packet(uint32_t value, bool is_data);
|
||||
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
Console console{
|
||||
{0, 0, 240, 224}};
|
||||
};
|
||||
|
||||
class FskRxAppView : public View {
|
||||
public:
|
||||
FskRxAppView(NavigationView& nav, Rect parent_rect);
|
||||
~FskRxAppView();
|
||||
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
std::string title() const override { return "FSK RX Stream"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
};
|
||||
|
||||
class FskxRxMainView : public View {
|
||||
public:
|
||||
FskxRxMainView(NavigationView& nav);
|
||||
~FskxRxMainView();
|
||||
|
||||
void focus() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
std::string title() const override { return "FSK RX"; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 902'075'000;
|
||||
static constexpr ui::Dim header_height = (5 * 16);
|
||||
|
||||
uint32_t initial_deviation{3750};
|
||||
|
||||
bool logging() const { return false; };
|
||||
bool logging_raw() const { return false; };
|
||||
|
||||
NavigationView& nav_;
|
||||
Rect view_rect = {0, header_height, 240, 224};
|
||||
|
||||
FskRxAppView view_stream{nav_, view_rect};
|
||||
FskRxAppConsoleView view_data{nav_, view_rect};
|
||||
|
||||
TabView tab_view{
|
||||
{"Data", Color::yellow(), &view_data},
|
||||
{"Stream", Color::cyan(), &view_stream}};
|
||||
|
||||
void refresh_ui(rf::Frequency f);
|
||||
void on_packet(uint32_t value, bool is_data);
|
||||
void handle_decoded(Timestamp timestamp, const std::string& prefix);
|
||||
|
||||
uint32_t last_address = 0;
|
||||
FskRxLogger logger{};
|
||||
uint16_t packet_count = 0;
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 4 * 8},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{11 * 8, 2 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{13 * 8, 2 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{16 * 8, 2 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{19 * 8 - 4, 35, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{19 * 8 - 4, 40, 6 * 8, 4}};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 3 * 16}, "Deviation:", Color::light_grey()},
|
||||
};
|
||||
|
||||
FrequencyField deviation_frequency{
|
||||
{10 * 8, 3 * 16},
|
||||
{3750, 500000},
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
RecordView record_view{
|
||||
{0 * 8, 4 * 16, 30 * 8, 1 * 16},
|
||||
u"FSKRX_????.C16",
|
||||
u"FSKRX",
|
||||
RecordView::FileType::RawS16,
|
||||
16384,
|
||||
3};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->view_data.on_packet(message->value, message->is_data);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__POCSAG_APP_H__*/
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* 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_iq_trim.hpp"
|
||||
|
||||
#include "complex.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
IQTrimView::IQTrimView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({
|
||||
&labels,
|
||||
&field_path,
|
||||
&field_start,
|
||||
&field_end,
|
||||
&text_samples,
|
||||
&text_max,
|
||||
&field_cutoff,
|
||||
&field_amplify,
|
||||
&button_trim,
|
||||
});
|
||||
|
||||
field_path.on_select = [this](TextField&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".C*");
|
||||
open_view->push_dir(u"CAPTURES");
|
||||
open_view->on_changed = [this](fs::path path) {
|
||||
open_file(path);
|
||||
};
|
||||
};
|
||||
|
||||
text_samples.set_style(&Styles::light_grey);
|
||||
text_max.set_style(&Styles::light_grey);
|
||||
|
||||
field_start.on_change = [this](int32_t v) {
|
||||
if (field_end.value() < v)
|
||||
field_end.set_value(v, false);
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
field_end.on_change = [this](int32_t v) {
|
||||
if (field_start.value() > v)
|
||||
field_start.set_value(v, false);
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
field_cutoff.set_value(7); // 7% of max is a good default.
|
||||
field_cutoff.on_change = [this](int32_t) {
|
||||
compute_range();
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
field_amplify.set_value(1); // 1X is default (no amplification)
|
||||
field_amplify.on_change = [this](int32_t) {
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
button_trim.on_select = [this](Button&) {
|
||||
if (trim_capture()) {
|
||||
profile_capture();
|
||||
compute_range();
|
||||
refresh_ui();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
IQTrimView::IQTrimView(NavigationView& nav, const fs::path& path)
|
||||
: IQTrimView(nav) {
|
||||
open_file(path);
|
||||
}
|
||||
|
||||
void IQTrimView::open_file(const std::filesystem::path& path) {
|
||||
path_ = std::move(path);
|
||||
profile_capture();
|
||||
compute_range();
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void IQTrimView::paint(Painter& painter) {
|
||||
if (info_) {
|
||||
uint32_t power_cutoff = field_cutoff.value() * static_cast<uint64_t>(info_->max_power) / 100;
|
||||
|
||||
// Draw power buckets.
|
||||
for (size_t i = 0; i < power_buckets_.size(); ++i) {
|
||||
auto power = power_buckets_[i].power;
|
||||
uint8_t amp = 0;
|
||||
|
||||
if (power > power_cutoff && info_->max_power > 0)
|
||||
amp = (255ULL * power) / info_->max_power;
|
||||
|
||||
painter.draw_vline(
|
||||
pos_lines + Point{(int)i, 0},
|
||||
height_lines,
|
||||
Color(amp, amp, amp));
|
||||
}
|
||||
|
||||
// Draw trim range edges.
|
||||
int start_x = screen_width * field_start.value() / info_->sample_count;
|
||||
int end_x = screen_width * field_end.value() / info_->sample_count;
|
||||
|
||||
painter.draw_vline(
|
||||
pos_lines + Point{start_x, 0},
|
||||
height_lines,
|
||||
Color::dark_green());
|
||||
painter.draw_vline(
|
||||
pos_lines + Point{end_x, 0},
|
||||
height_lines,
|
||||
Color::dark_red());
|
||||
}
|
||||
}
|
||||
|
||||
void IQTrimView::focus() {
|
||||
field_path.focus();
|
||||
}
|
||||
|
||||
void IQTrimView::refresh_ui() {
|
||||
field_path.set_text(path_.filename().string());
|
||||
text_samples.set(to_string_dec_uint(info_->sample_count));
|
||||
|
||||
// show max power after amplification applied
|
||||
uint64_t power_amp = field_amplify.value() * field_amplify.value() * field_amplify.value() * field_amplify.value();
|
||||
text_max.set(to_string_dec_uint(info_->max_power * power_amp));
|
||||
|
||||
// show max power in red if amplification is too high, causing clipping
|
||||
uint32_t clipping_limit = (fs::capture_file_sample_size(path_) == sizeof(complex8_t)) ? 0x80 : 0x8000;
|
||||
if ((field_amplify.value() * info_->max_iq) > clipping_limit)
|
||||
text_max.set_style(&Styles::red);
|
||||
else
|
||||
text_max.set_style(&Styles::light_grey);
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
|
||||
auto max_range = info_ ? info_->sample_count : 0;
|
||||
auto step = info_ ? info_->sample_count / screen_width : 0;
|
||||
|
||||
field_start.set_range(0, max_range);
|
||||
field_start.set_step(step);
|
||||
field_end.set_range(0, max_range);
|
||||
field_end.set_step(step);
|
||||
|
||||
field_start.set_value(trim_range.start_sample, false);
|
||||
field_end.set_value(trim_range.end_sample, false);
|
||||
}
|
||||
|
||||
void IQTrimView::profile_capture() {
|
||||
power_buckets_ = {};
|
||||
iq::PowerBuckets buckets{
|
||||
.p = power_buckets_.data(),
|
||||
.size = power_buckets_.size()};
|
||||
|
||||
progress_ui.show_reading();
|
||||
info_ = iq::profile_capture(path_, buckets);
|
||||
progress_ui.clear();
|
||||
}
|
||||
|
||||
void IQTrimView::compute_range() {
|
||||
if (!info_)
|
||||
return;
|
||||
|
||||
iq::PowerBuckets buckets{
|
||||
.p = power_buckets_.data(),
|
||||
.size = power_buckets_.size()};
|
||||
auto trim_range = iq::compute_trim_range(*info_, buckets, field_cutoff.value());
|
||||
|
||||
update_range_controls(trim_range);
|
||||
}
|
||||
|
||||
bool IQTrimView::trim_capture() {
|
||||
if (!info_) {
|
||||
nav_.display_modal("Error", "Open a file first.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool trimmed = false;
|
||||
iq::TrimRange trim_range{
|
||||
static_cast<uint32_t>(field_start.value()),
|
||||
static_cast<uint32_t>(field_end.value()),
|
||||
info_->sample_size};
|
||||
|
||||
if (trim_range.start_sample >= trim_range.end_sample) {
|
||||
nav_.display_modal("Error", "Invalid trimming range.");
|
||||
return false;
|
||||
}
|
||||
|
||||
progress_ui.show_trimming();
|
||||
trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback(), field_amplify.value());
|
||||
progress_ui.clear();
|
||||
|
||||
if (!trimmed)
|
||||
nav_.display_modal("Error", "Trimming failed.");
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* 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_IQ_TRIM_H__
|
||||
#define __UI_IQ_TRIM_H__
|
||||
|
||||
#include "file.hpp"
|
||||
#include "iq_trim.hpp"
|
||||
#include "optional.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* Helper for drawing progress related to IQ trimming. */
|
||||
/* TODO: Ideally this would all be done on second thread. */
|
||||
class TrimProgressUI {
|
||||
public:
|
||||
void show_reading() {
|
||||
clear();
|
||||
p.draw_string({6 * 8, 5 * 16}, Styles::yellow, "Reading Capture...");
|
||||
}
|
||||
|
||||
void show_trimming() {
|
||||
clear();
|
||||
p.draw_string({5 * 8, 5 * 16}, Styles::yellow, "Trimming Capture...");
|
||||
}
|
||||
|
||||
void show_progress(uint8_t percent) {
|
||||
auto width = percent * screen_width / 100;
|
||||
p.draw_hline({0, 6 * 16 + 2}, width, Color::yellow());
|
||||
}
|
||||
|
||||
void clear() {
|
||||
p.fill_rectangle({0 * 8, 4 * 16, screen_width, 3 * 16}, Color::black());
|
||||
}
|
||||
|
||||
auto get_callback() {
|
||||
return [this](uint8_t percent) { show_progress(percent); };
|
||||
}
|
||||
|
||||
private:
|
||||
Painter p{};
|
||||
};
|
||||
|
||||
class IQTrimView : public View {
|
||||
public:
|
||||
IQTrimView(NavigationView& nav);
|
||||
IQTrimView(NavigationView& nav, const std::filesystem::path& path);
|
||||
|
||||
std::string title() const override { return "IQ Trim"; }
|
||||
void paint(Painter& painter) override;
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
void open_file(const std::filesystem::path& path);
|
||||
|
||||
/* Update controls with latest values. */
|
||||
void refresh_ui();
|
||||
|
||||
/* Update the start/end controls with trim range info. */
|
||||
void update_range_controls(iq::TrimRange trim_range);
|
||||
|
||||
/* Collect capture info and samples to draw the UI. */
|
||||
void profile_capture();
|
||||
|
||||
/* Determine the start and end buckets based on the cutoff. */
|
||||
void compute_range();
|
||||
|
||||
/* Trims the capture file based on the settings. */
|
||||
bool trim_capture();
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
std::filesystem::path path_{};
|
||||
Optional<iq::CaptureInfo> info_{};
|
||||
std::array<iq::PowerBuckets::Bucket, screen_width> power_buckets_{};
|
||||
TrimProgressUI progress_ui{};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "Capture File:", Color::light_grey()},
|
||||
{{0 * 8, 6 * 16}, "Start :", Color::light_grey()},
|
||||
{{0 * 8, 7 * 16}, "End :", Color::light_grey()},
|
||||
{{0 * 8, 8 * 16}, "Samples:", Color::light_grey()},
|
||||
{{0 * 8, 9 * 16}, "Max Pwr:", Color::light_grey()},
|
||||
{{0 * 8, 10 * 16}, "Cutoff :", Color::light_grey()},
|
||||
{{12 * 8, 10 * 16}, "%", Color::light_grey()},
|
||||
{{0 * 8, 12 * 16}, "Amplify:", Color::light_grey()},
|
||||
{{10 * 8, 12 * 16}, "x", Color::light_grey()},
|
||||
};
|
||||
|
||||
TextField field_path{
|
||||
{0 * 8, 1 * 16, 30 * 8, 1 * 16},
|
||||
"Open File..."};
|
||||
|
||||
Point pos_lines{0 * 8, 4 * 16};
|
||||
Dim height_lines{2 * 16};
|
||||
|
||||
NumberField field_start{
|
||||
{9 * 8, 6 * 16},
|
||||
10,
|
||||
{0, 0},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_end{
|
||||
{9 * 8, 7 * 16},
|
||||
10,
|
||||
{0, 0},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Text text_samples{
|
||||
{9 * 8, 8 * 16, 10 * 8, 1 * 16},
|
||||
"0"};
|
||||
|
||||
Text text_max{
|
||||
{9 * 8, 9 * 16, 20 * 8, 1 * 16},
|
||||
"0"};
|
||||
|
||||
NumberField field_cutoff{
|
||||
{9 * 8, 10 * 16},
|
||||
3,
|
||||
{1, 100},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_amplify{
|
||||
{9 * 8, 12 * 16},
|
||||
1,
|
||||
{1, 9},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Button button_trim{
|
||||
{20 * 8, 16 * 16, 8 * 8, 2 * 16},
|
||||
"Trim"};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_IQ_TRIM_H__*/
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "ui_keyfob.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -98,12 +97,12 @@ size_t KeyfobView::generate_frame() {
|
||||
uint64_t payload;
|
||||
|
||||
// Symfield word to frame
|
||||
payload = field_payload_a.value_hex_u64();
|
||||
payload = field_payload_a.to_integer();
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
frame[4 - i] = payload & 0xFF;
|
||||
payload >>= 8;
|
||||
}
|
||||
payload = field_payload_b.value_hex_u64();
|
||||
payload = field_payload_b.to_integer();
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
frame[9 - i] = payload & 0xFF;
|
||||
payload >>= 8;
|
||||
@@ -137,12 +136,8 @@ void KeyfobView::focus() {
|
||||
}
|
||||
|
||||
KeyfobView::~KeyfobView() {
|
||||
// save app settings
|
||||
settings.save("tx_keyfob", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void KeyfobView::update_progress(const uint32_t progress) {
|
||||
@@ -169,12 +164,12 @@ void KeyfobView::on_make_change(size_t index) {
|
||||
// DEBUG
|
||||
void KeyfobView::update_symfields() {
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
field_payload_a.set_sym(i << 1, frame[i] >> 4);
|
||||
field_payload_a.set_sym((i << 1) + 1, frame[i] & 0x0F);
|
||||
field_payload_a.set_offset(i << 1, frame[i] >> 4);
|
||||
field_payload_a.set_offset((i << 1) + 1, frame[i] & 0x0F);
|
||||
}
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
field_payload_b.set_sym(i << 1, frame[5 + i] >> 4);
|
||||
field_payload_b.set_sym((i << 1) + 1, frame[5 + i] & 0x0F);
|
||||
field_payload_b.set_offset(i << 1, frame[5 + i] >> 4);
|
||||
field_payload_b.set_offset((i << 1) + 1, frame[5 + i] & 0x0F);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +185,6 @@ void KeyfobView::start_tx() {
|
||||
|
||||
size_t bitstream_length = generate_frame();
|
||||
|
||||
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_ook_data(
|
||||
@@ -216,13 +209,6 @@ KeyfobView::KeyfobView(
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_keyfob", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
frame[0] = 0x55;
|
||||
update_symfields();
|
||||
|
||||
@@ -236,12 +222,10 @@ KeyfobView::KeyfobView(
|
||||
|
||||
options_make.set_selected_index(0);
|
||||
|
||||
transmitter_model.set_tuning_frequency(433920000); // Fixed 433.92MHz
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "encoders.hpp"
|
||||
|
||||
using namespace encoders;
|
||||
@@ -42,9 +43,13 @@ class KeyfobView : public View {
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
TxRadioState radio_state_{
|
||||
433920000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
OOK_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_keyfob", app_settings::Mode::TX};
|
||||
|
||||
// 1013210ns / bit
|
||||
static constexpr uint32_t subaru_samples_per_bit = (OOK_SAMPLERATE * 0.00101321);
|
||||
@@ -94,11 +99,11 @@ class KeyfobView : public View {
|
||||
SymField field_payload_a{
|
||||
{2 * 8, 5 * 16},
|
||||
10,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
SymField field_payload_b{
|
||||
{13 * 8, 5 * 16},
|
||||
10,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
|
||||
Text text_status{
|
||||
{2 * 8, 13 * 16, 128, 16},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -22,29 +23,22 @@
|
||||
|
||||
#include "ui_level.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "oversample.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace tonekey;
|
||||
using portapack::memory::map::backup_ram;
|
||||
|
||||
namespace ui {
|
||||
|
||||
bool LevelView::check_sd_card() {
|
||||
return (sd_card::status() == sd_card::Status::Mounted) ? true : false;
|
||||
}
|
||||
|
||||
void LevelView::focus() {
|
||||
button_frequency.focus();
|
||||
}
|
||||
|
||||
LevelView::~LevelView() {
|
||||
// save app settings
|
||||
app_settings.lna = field_lna.value();
|
||||
app_settings.vga = field_vga.value();
|
||||
app_settings.rx_amp = field_rf_amp.value();
|
||||
|
||||
settings.save("level", &app_settings);
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
@@ -71,56 +65,34 @@ LevelView::LevelView(NavigationView& nav)
|
||||
|
||||
rssi.set_vertical_rssi(true);
|
||||
|
||||
field_volume.on_change = [this](int32_t v) { this->on_headphone_volume_changed(v); };
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
|
||||
// Level directory
|
||||
if (check_sd_card()) { // Check to see if SD Card is mounted
|
||||
make_new_directory(u"/LEVEL");
|
||||
sd_card_mounted = true;
|
||||
}
|
||||
|
||||
change_mode(NFM_MODULATION); // Start on AM
|
||||
field_mode.set_by_value(NFM_MODULATION); // Reflect the mode into the manual selector
|
||||
|
||||
// HELPER: Pre-setting a manual range, based on stored frequency
|
||||
freq = persistent_memory::tuned_frequency();
|
||||
receiver_model.set_tuning_frequency(freq);
|
||||
button_frequency.set_text("<" + to_string_short_freq(freq) + " MHz>");
|
||||
|
||||
// load auto common app settings
|
||||
if (sd_card_mounted) {
|
||||
auto rc = settings.load("level", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
receiver_model.set_rf_amp(app_settings.rx_amp);
|
||||
}
|
||||
}
|
||||
freq_ = receiver_model.target_frequency();
|
||||
button_frequency.set_text("<" + to_string_short_freq(freq_) + " MHz>");
|
||||
|
||||
button_frequency.on_select = [this, &nav](ButtonWithEncoder& button) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(freq);
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(freq_);
|
||||
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||
freq = f;
|
||||
receiver_model.set_tuning_frequency(f); // Retune to actual freq
|
||||
button_frequency.set_text("<" + to_string_short_freq(freq) + " MHz>");
|
||||
freq_ = f;
|
||||
receiver_model.set_target_frequency(f); // Retune to actual freq
|
||||
button_frequency.set_text("<" + to_string_short_freq(freq_) + " MHz>");
|
||||
};
|
||||
};
|
||||
|
||||
button_frequency.on_change = [this]() {
|
||||
int64_t def_step = freqman_entry_get_step_value(step_mode.selected_index());
|
||||
freq = freq + (button_frequency.get_encoder_delta() * def_step);
|
||||
if (freq < 1) {
|
||||
freq = 1;
|
||||
freq_ = freq_ + (button_frequency.get_encoder_delta() * def_step);
|
||||
if (freq_ < 1) {
|
||||
freq_ = 1;
|
||||
}
|
||||
if (freq > (MAX_UFREQ - def_step)) {
|
||||
freq = MAX_UFREQ;
|
||||
if (freq_ > (MAX_UFREQ - def_step)) {
|
||||
freq_ = MAX_UFREQ;
|
||||
}
|
||||
button_frequency.set_encoder_delta(0);
|
||||
|
||||
receiver_model.set_tuning_frequency(freq); // Retune to actual freq
|
||||
button_frequency.set_text("<" + to_string_short_freq(freq) + " MHz>");
|
||||
receiver_model.set_target_frequency(freq_); // Retune to actual freq
|
||||
button_frequency.set_text("<" + to_string_short_freq(freq_) + " MHz>");
|
||||
};
|
||||
|
||||
field_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
@@ -146,7 +118,7 @@ LevelView::LevelView(NavigationView& nav)
|
||||
audio::output::stop();
|
||||
} else if (v == 1) {
|
||||
audio::output::start();
|
||||
this->on_headphone_volume_changed((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack.
|
||||
} else {
|
||||
}
|
||||
};
|
||||
@@ -165,41 +137,29 @@ LevelView::LevelView(NavigationView& nav)
|
||||
// FILL STEP OPTIONS
|
||||
freqman_set_modulation_option(field_mode);
|
||||
freqman_set_step_option_short(step_mode);
|
||||
freq_stats_rssi.set_style(&style_white);
|
||||
freq_stats_db.set_style(&style_white);
|
||||
}
|
||||
|
||||
void LevelView::on_headphone_volume_changed(int32_t v) {
|
||||
const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
receiver_model.set_headphone_volume(new_volume);
|
||||
freq_stats_rssi.set_style(&Styles::white);
|
||||
freq_stats_db.set_style(&Styles::white);
|
||||
}
|
||||
|
||||
void LevelView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
static int last_max_db = -1000;
|
||||
static int last_min_rssi = -1000;
|
||||
static int last_avg_rssi = -1000;
|
||||
static int last_max_rssi = -1000;
|
||||
static int16_t last_max_db = -1000;
|
||||
static int16_t last_min_rssi = -1000;
|
||||
static int16_t last_avg_rssi = -1000;
|
||||
static int16_t last_max_rssi = -1000;
|
||||
|
||||
rssi_graph.add_values(rssi.get_min(), rssi.get_avg(), rssi.get_max(), statistics.max_db);
|
||||
|
||||
bool refresh_db = false;
|
||||
bool refresh_rssi = false;
|
||||
|
||||
// refresh db
|
||||
if (last_max_db != statistics.max_db) {
|
||||
refresh_db = true;
|
||||
}
|
||||
if (last_min_rssi != rssi.get_min() || last_avg_rssi != rssi.get_avg() || last_max_rssi != rssi.get_max()) {
|
||||
refresh_rssi = true;
|
||||
}
|
||||
if (refresh_db) {
|
||||
last_max_db = statistics.max_db;
|
||||
freq_stats_db.set("Power: " + to_string_dec_int(statistics.max_db) + " db");
|
||||
}
|
||||
if (refresh_rssi) {
|
||||
last_min_rssi = rssi.get_min();
|
||||
last_avg_rssi = rssi.get_avg();
|
||||
last_max_rssi = rssi.get_max();
|
||||
freq_stats_rssi.set("RSSI: " + to_string_dec_int(rssi.get_min()) + "/" + to_string_dec_int(rssi.get_avg()) + "/" + to_string_dec_int(rssi.get_max()) + ",dt: " + to_string_dec_int(rssi.get_delta()));
|
||||
// refresh rssi
|
||||
if (last_min_rssi != rssi_graph.get_graph_min() || last_avg_rssi != rssi_graph.get_graph_avg() || last_max_rssi != rssi_graph.get_graph_max()) {
|
||||
last_min_rssi = rssi_graph.get_graph_min();
|
||||
last_avg_rssi = rssi_graph.get_graph_avg();
|
||||
last_max_rssi = rssi_graph.get_graph_max();
|
||||
freq_stats_rssi.set("RSSI: " + to_string_dec_int(last_min_rssi) + "/" + to_string_dec_int(last_avg_rssi) + "/" + to_string_dec_int(last_max_rssi) + ",dt: " + to_string_dec_int(rssi_graph.get_graph_delta()));
|
||||
}
|
||||
} /* on_statistic_updates */
|
||||
|
||||
@@ -209,74 +169,64 @@ size_t LevelView::change_mode(freqman_index_t new_mod) {
|
||||
switch (new_mod) {
|
||||
case AM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw DSB (0) default
|
||||
field_bw.set_selected_index(0);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
receiver_model.set_am_configuration(field_bw.selected_index());
|
||||
field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_am_configuration(n); };
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_am_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_am_configuration(n); };
|
||||
// bw DSB (0) default
|
||||
field_bw.set_by_value(0);
|
||||
text_ctcss.set(" ");
|
||||
break;
|
||||
case NFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw 16k (2) default
|
||||
field_bw.set_selected_index(2);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
receiver_model.set_nbfm_configuration(field_bw.selected_index());
|
||||
field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_nbfm_configuration(n); };
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_nbfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_nbfm_configuration(n); };
|
||||
// bw 16k (2) default
|
||||
field_bw.set_by_value(2);
|
||||
break;
|
||||
case WFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw 200k (0) only/default
|
||||
field_bw.set_selected_index(0);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
receiver_model.set_wfm_configuration(field_bw.selected_index());
|
||||
field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_wfm_configuration(n); };
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_wfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_wfm_configuration(n); };
|
||||
// bw 200k (0) only/default
|
||||
field_bw.set_by_value(0);
|
||||
text_ctcss.set(" ");
|
||||
break;
|
||||
case SPEC_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_capture);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t sampling_rate) {
|
||||
// Baseband needs to know the desired sampling and oversampling rates.
|
||||
baseband::set_sample_rate(sampling_rate, get_oversample_rate(sampling_rate));
|
||||
|
||||
// The radio needs to know the effective sampling rate.
|
||||
auto actual_sampling_rate = get_actual_sample_rate(sampling_rate);
|
||||
receiver_model.set_sampling_rate(actual_sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate));
|
||||
};
|
||||
field_bw.set_by_value(0);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (new_mod != SPEC_MODULATION) {
|
||||
// Reset receiver model to fix bug when going from SPEC to audio, the sound is distorted.
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
}
|
||||
|
||||
return step_mode.selected_index();
|
||||
}
|
||||
|
||||
void LevelView::handle_coded_squelch(const uint32_t value) {
|
||||
static int32_t last_idx = -1;
|
||||
|
||||
float diff, min_diff = value;
|
||||
size_t min_idx{0};
|
||||
size_t c;
|
||||
|
||||
if (field_mode.selected_index() != NFM_MODULATION) {
|
||||
text_ctcss.set(" ");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find nearest match
|
||||
for (c = 0; c < tone_keys.size(); c++) {
|
||||
diff = abs(((float)value / 100.0) - tone_keys[c].second);
|
||||
if (diff < min_diff) {
|
||||
min_idx = c;
|
||||
min_diff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary confidence threshold
|
||||
if (last_idx < 0 || (unsigned)last_idx != min_idx) {
|
||||
last_idx = min_idx;
|
||||
if (min_diff < 40)
|
||||
text_ctcss.set("T: " + tone_keys[min_idx].first);
|
||||
else
|
||||
text_ctcss.set(" ");
|
||||
}
|
||||
if (field_mode.selected_index() == NFM_MODULATION)
|
||||
text_ctcss.set(tone_key_string_by_value(value, text_ctcss.parent_rect().width() / 8));
|
||||
else
|
||||
text_ctcss.set(" ");
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -23,20 +24,21 @@
|
||||
#ifndef _UI_LEVEL
|
||||
#define _UI_LEVEL
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "ui_mictx.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_mictx.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -47,62 +49,22 @@ class LevelView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
const Style style_grey{
|
||||
// level
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::grey(),
|
||||
};
|
||||
|
||||
const Style style_white{
|
||||
// level
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
const Style style_yellow{
|
||||
// Found signal
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::yellow(),
|
||||
};
|
||||
|
||||
const Style style_green{
|
||||
// Found signal
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::green(),
|
||||
};
|
||||
|
||||
const Style style_red{
|
||||
// erasing freq
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::red(),
|
||||
};
|
||||
|
||||
const Style style_blue{
|
||||
// quick level, wait == 0
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::blue(),
|
||||
};
|
||||
|
||||
std::string title() const override { return "Level"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_level", app_settings::Mode::RX};
|
||||
|
||||
size_t change_mode(freqman_index_t mod_type);
|
||||
void on_statistics_update(const ChannelStatistics& statistics);
|
||||
void set_display_freq(int64_t freq);
|
||||
bool check_sd_card();
|
||||
|
||||
// TODO: needed?
|
||||
int32_t db{0};
|
||||
long long int MAX_UFREQ = {7200000000}; // maximum usable freq
|
||||
bool sd_card_mounted = false;
|
||||
rf::Frequency freq = {0};
|
||||
rf::Frequency freq_ = {0};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "LNA: VGA: AMP: VOL: ", Color::light_grey()},
|
||||
@@ -118,13 +80,8 @@ class LevelView : public View {
|
||||
RFAmpField field_rf_amp{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
NumberField field_volume{
|
||||
{24 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
AudioVolumeField field_volume{
|
||||
{24 * 8, 0 * 16}};
|
||||
|
||||
OptionsField field_bw{
|
||||
{3 * 8, 1 * 16},
|
||||
@@ -133,7 +90,7 @@ class LevelView : public View {
|
||||
|
||||
OptionsField field_mode{
|
||||
{15 * 8, 1 * 16},
|
||||
3,
|
||||
4,
|
||||
{}};
|
||||
|
||||
OptionsField step_mode{
|
||||
@@ -152,11 +109,11 @@ class LevelView : public View {
|
||||
{"audio off", 0},
|
||||
{"audio on", 1}
|
||||
//{"tone on", 2},
|
||||
//{"tone off", 2},
|
||||
//{"tone off", 3},
|
||||
}};
|
||||
|
||||
Text text_ctcss{
|
||||
{22 * 8, 3 * 16 + 4, 14 * 8, 1 * 8},
|
||||
{22 * 8, 3 * 16 + 4, 8 * 8, 1 * 8},
|
||||
""};
|
||||
|
||||
// RSSI: XX/XX/XXX,dt: XX
|
||||
@@ -203,7 +160,6 @@ class LevelView : public View {
|
||||
};
|
||||
|
||||
void handle_coded_squelch(const uint32_t value);
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
|
||||
MessageHandlerRegistration message_handler_coded_squelch{
|
||||
Message::ID::CodedSquelch,
|
||||
@@ -217,9 +173,6 @@ class LevelView : public View {
|
||||
[this](const Message* const p) {
|
||||
this->on_statistics_update(static_cast<const ChannelStatisticsMessage*>(p)->statistics);
|
||||
}};
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2020 euquiq
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -21,12 +22,15 @@
|
||||
*/
|
||||
|
||||
#include "ui_looking_glass_app.hpp"
|
||||
#include "convert.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
void GlassView::focus() {
|
||||
button_marker.focus();
|
||||
range_presets.focus();
|
||||
}
|
||||
|
||||
GlassView::~GlassView() {
|
||||
@@ -37,7 +41,7 @@ GlassView::~GlassView() {
|
||||
|
||||
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power) {
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
// analog audio app like view
|
||||
// <20MHz spectrum mode
|
||||
if (bin < 120) {
|
||||
if (spectrum.db[SPEC_NB_BINS - 120 + bin] > max_power)
|
||||
max_power = spectrum.db[SPEC_NB_BINS - 120 + bin];
|
||||
@@ -45,80 +49,51 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
|
||||
if (spectrum.db[bin - 120] > max_power)
|
||||
max_power = spectrum.db[bin - 120];
|
||||
}
|
||||
} else if (mode == LOOKING_GLASS_FASTSCAN) {
|
||||
// view is made in multiple pass, use original bin picking
|
||||
// Center 12 bins are ignored (DC spike is blanked) Leftmost and rightmost 2 bins are ignored
|
||||
} else {
|
||||
// FAST or SLOW mode
|
||||
if (bin < 120) {
|
||||
if (spectrum.db[SPEC_NB_BINS - 2 - 120 + bin] > max_power)
|
||||
max_power = spectrum.db[SPEC_NB_BINS - 2 - 120 + bin];
|
||||
if (spectrum.db[134 + bin] > max_power)
|
||||
max_power = spectrum.db[134 + bin];
|
||||
} else {
|
||||
if (spectrum.db[2 + bin - 120] > max_power)
|
||||
max_power = spectrum.db[2 + bin - 120];
|
||||
}
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN )
|
||||
{
|
||||
if (bin < 120) {
|
||||
if (spectrum.db[SPEC_NB_BINS - offset - 120 + bin] > max_power)
|
||||
max_power = spectrum.db[SPEC_NB_BINS - offset - 120 + bin];
|
||||
} else {
|
||||
if (spectrum.db[offset + bin - 120] > max_power)
|
||||
max_power = spectrum.db[offset + bin - 120];
|
||||
if (spectrum.db[bin - 118] > max_power)
|
||||
max_power = spectrum.db[bin - 118];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
|
||||
rf::Frequency freq_at_pos = 0;
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
// starting from the middle, minus 8 ignored bin on each side. Since pos is [-120,120] after the (pos - 120), it's divided by SCREEN_W(240)/2 => 120
|
||||
freq_at_pos = f_center_ini + ((pos - 120) * ((looking_glass_range - ((16 * looking_glass_range) / SPEC_NB_BINS)) / 2)) / (SCREEN_W / 2);
|
||||
} else
|
||||
freq_at_pos = f_min + (2 * offset * each_bin_size) + (pos * looking_glass_range) / SCREEN_W;
|
||||
|
||||
return freq_at_pos;
|
||||
}
|
||||
|
||||
void GlassView::on_marker_change() {
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
marker = f_min + (marker_pixel_index * looking_glass_range) / SCREEN_W;
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
{
|
||||
marker = f_min + (offset * each_bin_size) + (marker_pixel_index * looking_glass_range) / SCREEN_W;
|
||||
}
|
||||
button_marker.set_text(to_string_short_freq(marker));
|
||||
PlotMarker(marker_pixel_index); // Refresh marker on screen
|
||||
}
|
||||
|
||||
// Returns the next multiple of num that is a multiple of multiplier
|
||||
int64_t GlassView::next_mult_of(int64_t num, int64_t multiplier) {
|
||||
return ((num / multiplier) + 1) * multiplier;
|
||||
}
|
||||
|
||||
void GlassView::adjust_range(int64_t* f_min, int64_t* f_max, int64_t width) {
|
||||
int64_t span = *f_max - *f_min;
|
||||
int64_t num_intervals = span / width;
|
||||
if (span % width != 0) {
|
||||
num_intervals++;
|
||||
}
|
||||
int64_t new_span = num_intervals * width;
|
||||
int64_t delta_span = (new_span - span) / 2;
|
||||
*f_min -= delta_span;
|
||||
*f_max += delta_span;
|
||||
marker = get_freq_from_bin_pos(marker_pixel_index);
|
||||
field_marker.set_text(to_string_short_freq(marker));
|
||||
plot_marker(marker_pixel_index); // Refresh marker on screen
|
||||
}
|
||||
|
||||
void GlassView::retune() {
|
||||
// Start a new sweep
|
||||
radio::set_tuning_frequency(f_center); // tune rx for this new slice directly, faster than using persistent memory saving
|
||||
chThdSleepMilliseconds(5);
|
||||
// Start a new sweep.
|
||||
// Tune rx for this new slice directly because the model
|
||||
// saves to persistent memory which is slower.
|
||||
radio::set_tuning_frequency(f_center);
|
||||
chThdSleepMilliseconds(5); // stabilize freq
|
||||
baseband::spectrum_streaming_start(); // Do the RX
|
||||
}
|
||||
|
||||
void GlassView::on_lna_changed(int32_t v_db) {
|
||||
receiver_model.set_lna(v_db);
|
||||
}
|
||||
|
||||
void GlassView::on_vga_changed(int32_t v_db) {
|
||||
receiver_model.set_vga(v_db);
|
||||
}
|
||||
|
||||
void GlassView::reset_live_view(bool clear_screen) {
|
||||
void GlassView::reset_live_view() {
|
||||
max_freq_hold = 0;
|
||||
max_freq_power = -1000;
|
||||
if (clear_screen) {
|
||||
// only clear screen in peak mode
|
||||
if (live_frequency_view == 2) {
|
||||
display.fill_rectangle({{0, 108 + 16}, {SCREEN_W, 320 - (108 + 16)}}, {0, 0, 0});
|
||||
}
|
||||
}
|
||||
|
||||
// Clear screen in peak mode.
|
||||
if (live_frequency_view == 2)
|
||||
display.fill_rectangle({{0, 108 + 16}, {SCREEN_W, SCREEN_H - (108 + 16)}}, {0, 0, 0});
|
||||
}
|
||||
|
||||
void GlassView::add_spectrum_pixel(uint8_t power) {
|
||||
@@ -143,22 +118,21 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
|
||||
// save max powerwull freq
|
||||
if (spectrum_data[xpos] > max_freq_power) {
|
||||
max_freq_power = spectrum_data[xpos];
|
||||
max_freq_hold = f_center + ((looking_glass_range)*xpos) / SCREEN_W;
|
||||
max_freq_hold = get_freq_from_bin_pos(xpos);
|
||||
}
|
||||
|
||||
int16_t point = y_max_range.clip(((spectrum_data[xpos] - raw_min) * (320 - (108 + 16))) / raw_delta);
|
||||
uint8_t color_gradient = (point * 255) / 212;
|
||||
// clear if not in peak view
|
||||
if (live_frequency_view != 2) {
|
||||
display.fill_rectangle({{xpos, 108 + 16}, {1, 320 - point}}, {0, 0, 0});
|
||||
display.fill_rectangle({{xpos, 108 + 16}, {1, SCREEN_H - point}}, {0, 0, 0});
|
||||
}
|
||||
display.fill_rectangle({{xpos, 320 - point}, {1, point}}, {color_gradient, 0, uint8_t(255 - color_gradient)});
|
||||
display.fill_rectangle({{xpos, SCREEN_H - point}, {1, point}}, {color_gradient, 0, uint8_t(255 - color_gradient)});
|
||||
}
|
||||
if (last_max_freq != max_freq_hold) {
|
||||
last_max_freq = max_freq_hold;
|
||||
freq_stats.set("MAX HOLD: " + to_string_short_freq(max_freq_hold));
|
||||
}
|
||||
PlotMarker(marker_pixel_index);
|
||||
plot_marker(marker_pixel_index);
|
||||
} else {
|
||||
display.draw_pixels({{0, display.scroll(1)}, {SCREEN_W, 1}}, spectrum_row); // new line at top, one less var, speedier
|
||||
}
|
||||
@@ -166,43 +140,58 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
|
||||
}
|
||||
}
|
||||
|
||||
bool GlassView::process_bins(uint8_t* powerlevel) {
|
||||
bins_hz_size += each_bin_size; // add pixel to fulfilled bag of Hz
|
||||
if (bins_hz_size >= marker_pixel_step) // new pixel fullfilled
|
||||
{
|
||||
if (*powerlevel > min_color_power)
|
||||
add_spectrum_pixel(*powerlevel); // Pixel will represent max_power
|
||||
else
|
||||
add_spectrum_pixel(0); // Filtered out, show black
|
||||
*powerlevel = 0;
|
||||
|
||||
if (!pixel_index) // Received indication that a waterfall line has been completed
|
||||
{
|
||||
bins_hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin"
|
||||
if (mode != LOOKING_GLASS_SINGLEPASS) {
|
||||
f_center = f_center_ini;
|
||||
retune();
|
||||
} else
|
||||
baseband::spectrum_streaming_start();
|
||||
return true; // signal a new line
|
||||
}
|
||||
bins_hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apparently, the spectrum object returns an array of SPEC_NB_BINS (256) bins
|
||||
// Each having the radio signal power for it's corresponding frequency slot
|
||||
// Each having the radio signal power for its corresponding frequency slot
|
||||
void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
baseband::spectrum_streaming_stop();
|
||||
// Convert bins of this spectrum slice into a representative max_power and when enough, into pixels
|
||||
// we actually need SCREEN_W (240) of those bins
|
||||
for (bin = offset; bin < bin_length + offset; bin++) {
|
||||
for (uint8_t bin = 0; bin < bin_length; bin++) {
|
||||
get_max_power(spectrum, bin, max_power);
|
||||
if (ignore_dc && bin == 119) {
|
||||
// process dc spike if enable
|
||||
if (bin == 119) {
|
||||
uint8_t next_max_power = 0;
|
||||
get_max_power(spectrum, 120, next_max_power);
|
||||
bins_Hz_size += 12 * each_bin_size; // add the ignored DC spike to "pixel fulfilled bag of Hz"
|
||||
max_power = (max_power + next_max_power) / 2;
|
||||
}
|
||||
bins_Hz_size += each_bin_size; // add this bin Hz count into the "pixel fulfilled bag of Hz"
|
||||
|
||||
if (bins_Hz_size >= marker_pixel_step) // new pixel fullfilled
|
||||
{
|
||||
if (min_color_power < max_power)
|
||||
add_spectrum_pixel(max_power); // Pixel will represent max_power
|
||||
else
|
||||
add_spectrum_pixel(0); // Filtered out, show black
|
||||
|
||||
max_power = 0;
|
||||
|
||||
if (!pixel_index) // Received indication that a waterfall line has been completed
|
||||
{
|
||||
bins_Hz_size = 0; // Since this is an entire pixel line, we don't carry "Pixels into next bin"
|
||||
f_center = f_center_ini;
|
||||
retune();
|
||||
return; // signal a new line
|
||||
get_max_power(spectrum, bin + 1, next_max_power);
|
||||
for (uint8_t it = 0; it < ignore_dc; it++) {
|
||||
uint8_t med_max_power = (max_power + next_max_power) / 2; // due to the way process_bins works we have to keep resetting the color
|
||||
if (process_bins(&med_max_power) == true)
|
||||
return; // new line signaled, return
|
||||
}
|
||||
bins_Hz_size -= marker_pixel_step; // reset bins size, but carrying the eventual excess Hz into next pixel
|
||||
}
|
||||
// process actual bin
|
||||
if (process_bins(&max_power) == true)
|
||||
return; // new line signaled, return
|
||||
}
|
||||
f_center += looking_glass_step;
|
||||
retune();
|
||||
if (mode != LOOKING_GLASS_SINGLEPASS) {
|
||||
f_center += looking_glass_step;
|
||||
retune();
|
||||
} else
|
||||
baseband::spectrum_streaming_start();
|
||||
}
|
||||
|
||||
void GlassView::on_hide() {
|
||||
@@ -216,80 +205,62 @@ void GlassView::on_show() {
|
||||
}
|
||||
|
||||
void GlassView::on_range_changed() {
|
||||
reset_live_view(false);
|
||||
reset_live_view();
|
||||
f_min = field_frequency_min.value();
|
||||
f_max = field_frequency_max.value();
|
||||
f_min = f_min * MHZ_DIV; // Transpose into full frequency realm
|
||||
f_max = f_max * MHZ_DIV;
|
||||
looking_glass_range = f_max - f_min;
|
||||
if (looking_glass_range < LOOKING_GLASS_SLICE_WIDTH_MAX) {
|
||||
mode = LOOKING_GLASS_SINGLEPASS;
|
||||
} else {
|
||||
mode = scan_type.selected_index_value();
|
||||
}
|
||||
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
if (looking_glass_range <= LOOKING_GLASS_SLICE_WIDTH_MAX) {
|
||||
// if the view is done in one pass, show it like in analog_audio_app
|
||||
offset = 0;
|
||||
mode = LOOKING_GLASS_SINGLEPASS;
|
||||
offset = 2;
|
||||
bin_length = SCREEN_W;
|
||||
ignore_dc = 0;
|
||||
} else if (mode == LOOKING_GLASS_FASTSCAN) {
|
||||
// view is made in multiple pass, use original bin picking
|
||||
offset = 0;
|
||||
bin_length = SCREEN_W;
|
||||
ignore_dc = 1;
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN )
|
||||
{
|
||||
offset = 16;
|
||||
bin_length = 80;
|
||||
ignore_dc = 0;
|
||||
}
|
||||
each_bin_size = looking_glass_bandwidth / SPEC_NB_BINS;
|
||||
if (mode != LOOKING_GLASS_SINGLEPASS) {
|
||||
looking_glass_step = (bin_length + (ignore_dc * 12)) * each_bin_size;
|
||||
} else {
|
||||
looking_glass_step = SPEC_NB_BINS * each_bin_size;
|
||||
}
|
||||
adjust_range(&f_min, &f_max, SCREEN_W);
|
||||
looking_glass_range = f_max - f_min;
|
||||
marker_pixel_step = looking_glass_range / SCREEN_W; // Each pixel value in Hz
|
||||
search_span = looking_glass_range / MHZ_DIV;
|
||||
button_range.set_text(" "); // clear up to 6 chars
|
||||
if (locked_range) {
|
||||
button_range.set_text(">" + to_string_dec_uint(search_span) + "<");
|
||||
} else {
|
||||
button_range.set_text(" " + to_string_dec_uint(search_span) + " ");
|
||||
}
|
||||
|
||||
pixel_index = 0; // reset pixel counter
|
||||
max_power = 0; // reset save max power level
|
||||
bins_Hz_size = 0; // reset amount of Hz filled up by pixels
|
||||
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
looking_glass_bandwidth = looking_glass_range;
|
||||
looking_glass_sampling_rate = looking_glass_bandwidth / 2;
|
||||
looking_glass_sampling_rate = looking_glass_bandwidth;
|
||||
each_bin_size = looking_glass_bandwidth / SCREEN_W;
|
||||
} else // if ( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
{
|
||||
looking_glass_sampling_rate = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
looking_glass_step = looking_glass_bandwidth;
|
||||
f_center_ini = f_min + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
|
||||
} else {
|
||||
// view is made in multiple pass, use original bin picking
|
||||
mode = scan_type.selected_index_value();
|
||||
looking_glass_bandwidth = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
each_bin_size = looking_glass_bandwidth / SPEC_NB_BINS;
|
||||
looking_glass_sampling_rate = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
each_bin_size = LOOKING_GLASS_SLICE_WIDTH_MAX / SPEC_NB_BINS;
|
||||
if (mode == LOOKING_GLASS_FASTSCAN) {
|
||||
offset = 2;
|
||||
ignore_dc = 4;
|
||||
bin_length = SCREEN_W;
|
||||
} else { // if( mode == LOOKING_GLASS_SLOWSCAN )
|
||||
offset = 2;
|
||||
bin_length = 80;
|
||||
ignore_dc = 0;
|
||||
}
|
||||
looking_glass_step = (bin_length + ignore_dc) * each_bin_size;
|
||||
f_center_ini = f_min - (offset * each_bin_size) + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
|
||||
}
|
||||
search_span = looking_glass_range / MHZ_DIV;
|
||||
marker_pixel_step = looking_glass_range / SCREEN_W; // Each pixel value in Hz
|
||||
|
||||
pixel_index = 0;
|
||||
max_power = 0;
|
||||
bins_hz_size = 0;
|
||||
|
||||
on_marker_change();
|
||||
update_range_field();
|
||||
|
||||
// set the sample rate and bandwidth
|
||||
receiver_model.set_sampling_rate(looking_glass_sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(looking_glass_bandwidth);
|
||||
|
||||
receiver_model.set_squelch_level(0);
|
||||
f_center_ini = f_min + (looking_glass_bandwidth / 2); // Initial center frequency for sweep
|
||||
f_center = f_center_ini; // Reset sweep into first slice
|
||||
baseband::set_spectrum(looking_glass_sampling_rate, field_trigger.value());
|
||||
receiver_model.set_tuning_frequency(f_center); // tune rx for this slice
|
||||
f_center = f_center_ini; // Reset sweep into first slice
|
||||
baseband::set_spectrum(looking_glass_bandwidth, trigger);
|
||||
receiver_model.set_target_frequency(f_center); // tune rx for this slice
|
||||
}
|
||||
|
||||
void GlassView::PlotMarker(uint8_t pos) {
|
||||
void GlassView::plot_marker(uint8_t pos) {
|
||||
uint8_t shift_y = 0;
|
||||
if (live_frequency_view > 0) // plot one line down when in live view
|
||||
{
|
||||
@@ -301,6 +272,50 @@ void GlassView::PlotMarker(uint8_t pos) {
|
||||
portapack::display.fill_rectangle({pos, 106 + shift_y, 1, 2}, Color::red()); // Red marker bottom
|
||||
}
|
||||
|
||||
void GlassView::update_min(int32_t v) {
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v > 7200 - min_size) {
|
||||
v = 7200 - min_size;
|
||||
}
|
||||
if (v > (field_frequency_max.value() - min_size))
|
||||
field_frequency_max.set_value(v + min_size, false);
|
||||
if (locked_range)
|
||||
field_frequency_max.set_value(v + min_size, false);
|
||||
else
|
||||
field_frequency_min.set_value(v, false);
|
||||
}
|
||||
|
||||
void GlassView::update_max(int32_t v) {
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v < min_size) {
|
||||
v = min_size;
|
||||
}
|
||||
if (v < (field_frequency_min.value() + min_size))
|
||||
field_frequency_min.set_value(v - min_size, false);
|
||||
if (locked_range)
|
||||
field_frequency_min.set_value(v - min_size, false);
|
||||
else
|
||||
field_frequency_max.set_value(v, false);
|
||||
}
|
||||
|
||||
void GlassView::update_range_field() {
|
||||
if (!locked_range) {
|
||||
field_range.set_style(&Styles::white);
|
||||
field_range.set_text(" " + to_string_dec_uint(search_span) + " ");
|
||||
} else {
|
||||
field_range.set_style(&Styles::red);
|
||||
field_range.set_text(">" + to_string_dec_uint(search_span) + "<");
|
||||
}
|
||||
}
|
||||
|
||||
GlassView::GlassView(
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
@@ -311,7 +326,7 @@ GlassView::GlassView(
|
||||
&field_frequency_max,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&button_range,
|
||||
&field_range,
|
||||
&steps_config,
|
||||
&scan_type,
|
||||
&view_config,
|
||||
@@ -319,305 +334,212 @@ GlassView::GlassView(
|
||||
&filter_config,
|
||||
&field_rf_amp,
|
||||
&range_presets,
|
||||
&button_marker,
|
||||
&field_marker,
|
||||
&field_trigger,
|
||||
&button_jump,
|
||||
&button_rst,
|
||||
&freq_stats});
|
||||
|
||||
load_Presets(); // Load available presets from TXT files (or default)
|
||||
load_presets(); // Load available presets from TXT files (or default).
|
||||
preset_index = clip<uint8_t>(preset_index, 0, presets_db.size());
|
||||
|
||||
field_frequency_min.set_value(f_min / MHZ_DIV);
|
||||
field_frequency_min.on_change = [this](int32_t v) {
|
||||
reset_live_view(true);
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v > 7200 - min_size) {
|
||||
v = 7200 - min_size;
|
||||
field_frequency_min.set_value(v);
|
||||
}
|
||||
if (v > (field_frequency_max.value() - min_size))
|
||||
field_frequency_max.set_value(v + min_size);
|
||||
if (locked_range)
|
||||
field_frequency_max.set_value(v + min_size);
|
||||
range_presets.set_selected_index(0); // Manual
|
||||
update_min(v);
|
||||
on_range_changed();
|
||||
};
|
||||
field_frequency_min.set_value(presets_db[0].min); // Defaults to first preset
|
||||
field_frequency_min.set_step(steps);
|
||||
|
||||
field_frequency_min.on_select = [this, &nav](NumberField& field) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(field_frequency_min.value() * MHZ_DIV);
|
||||
new_view->on_changed = [this, &field](rf::Frequency f) {
|
||||
int32_t freq = f / MHZ_DIV;
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (freq > (7200 - min_size))
|
||||
freq = 7200 - min_size;
|
||||
field_frequency_min.set_value(freq);
|
||||
if (field_frequency_max.value() < (freq + min_size))
|
||||
field_frequency_max.set_value(freq + min_size);
|
||||
on_range_changed();
|
||||
field_frequency_min.set_value(f / MHZ_DIV);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency_max.set_value(f_max / MHZ_DIV);
|
||||
field_frequency_max.on_change = [this](int32_t v) {
|
||||
reset_live_view(true);
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
if (v < min_size) {
|
||||
v = min_size;
|
||||
field_frequency_max.set_value(v);
|
||||
}
|
||||
if (v < (field_frequency_min.value() + min_size))
|
||||
field_frequency_min.set_value(v - min_size);
|
||||
if (locked_range)
|
||||
field_frequency_min.set_value(v - min_size);
|
||||
range_presets.set_selected_index(0); // Manual
|
||||
update_max(v);
|
||||
on_range_changed();
|
||||
};
|
||||
field_frequency_max.set_value(presets_db[0].max); // Defaults to first preset
|
||||
field_frequency_max.set_step(steps);
|
||||
|
||||
field_frequency_max.on_select = [this, &nav](NumberField& field) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(field_frequency_max.value() * MHZ_DIV);
|
||||
new_view->on_changed = [this, &field](rf::Frequency f) {
|
||||
int32_t min_size = steps;
|
||||
if (locked_range)
|
||||
min_size = search_span;
|
||||
if (min_size < 2)
|
||||
min_size = 2;
|
||||
int32_t freq = f / MHZ_DIV;
|
||||
if (freq < min_size)
|
||||
freq = min_size;
|
||||
field_frequency_max.set_value(freq);
|
||||
if (field_frequency_min.value() > (freq - min_size))
|
||||
field_frequency_min.set_value(freq - min_size);
|
||||
on_range_changed();
|
||||
field_frequency_max.set_value(f / MHZ_DIV);
|
||||
};
|
||||
};
|
||||
|
||||
field_lna.on_change = [this](int32_t v) {
|
||||
reset_live_view(true);
|
||||
this->on_lna_changed(v);
|
||||
};
|
||||
field_lna.set_value(receiver_model.lna());
|
||||
|
||||
field_vga.on_change = [this](int32_t v_db) {
|
||||
reset_live_view(true);
|
||||
this->on_vga_changed(v_db);
|
||||
};
|
||||
field_vga.set_value(receiver_model.vga());
|
||||
|
||||
steps_config.on_change = [this](size_t n, OptionsField::value_t v) {
|
||||
(void)n;
|
||||
steps_config.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
field_frequency_min.set_step(v);
|
||||
field_frequency_max.set_step(v);
|
||||
steps = v;
|
||||
};
|
||||
steps_config.set_selected_index(0); // default of 1 Mhz steps
|
||||
steps_config.set_selected_index(0); // 1 Mhz step.
|
||||
|
||||
scan_type.on_change = [this](size_t n, OptionsField::value_t v) {
|
||||
(void)n;
|
||||
scan_type.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
mode = v;
|
||||
on_range_changed();
|
||||
};
|
||||
scan_type.set_selected_index(0); // default legacy fast scan
|
||||
scan_type.set_selected_index(mode);
|
||||
|
||||
view_config.on_change = [this](size_t n, OptionsField::value_t v) {
|
||||
(void)n;
|
||||
// clear between changes
|
||||
reset_live_view(true);
|
||||
if (v == 0) {
|
||||
live_frequency_view = 0;
|
||||
level_integration.hidden(true);
|
||||
freq_stats.hidden(true);
|
||||
button_jump.hidden(true);
|
||||
button_rst.hidden(true);
|
||||
display.scroll_set_area(109, 319); // Restart scroll on the correct coordinates
|
||||
} else if (v == 1) {
|
||||
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
|
||||
live_frequency_view = 1;
|
||||
display.scroll_disable();
|
||||
level_integration.hidden(false);
|
||||
freq_stats.hidden(false);
|
||||
button_jump.hidden(false);
|
||||
button_rst.hidden(false);
|
||||
} else if (v == 2) {
|
||||
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
|
||||
live_frequency_view = 2;
|
||||
display.scroll_disable();
|
||||
level_integration.hidden(false);
|
||||
freq_stats.hidden(false);
|
||||
button_jump.hidden(false);
|
||||
button_rst.hidden(false);
|
||||
view_config.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
reset_live_view(); // Clear between changes.
|
||||
live_frequency_view = v;
|
||||
|
||||
switch (v) {
|
||||
case 0: // SPEC
|
||||
level_integration.hidden(true);
|
||||
freq_stats.hidden(true);
|
||||
button_jump.hidden(true);
|
||||
button_rst.hidden(true);
|
||||
display.scroll_set_area(109, 319); // Restart scroll on the correct coordinates.
|
||||
break;
|
||||
|
||||
case 1: // LEVEL
|
||||
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
|
||||
display.scroll_disable();
|
||||
level_integration.hidden(false);
|
||||
freq_stats.hidden(false);
|
||||
button_jump.hidden(false);
|
||||
button_rst.hidden(false);
|
||||
break;
|
||||
|
||||
case 2: // PEAK
|
||||
default:
|
||||
display.fill_rectangle({{0, 108}, {SCREEN_W, 24}}, {0, 0, 0});
|
||||
display.scroll_disable();
|
||||
level_integration.hidden(false);
|
||||
freq_stats.hidden(false);
|
||||
button_jump.hidden(false);
|
||||
button_rst.hidden(false);
|
||||
break;
|
||||
}
|
||||
|
||||
set_dirty();
|
||||
};
|
||||
view_config.set_selected_index(0); // default spectrum
|
||||
view_config.set_selected_index(live_frequency_view);
|
||||
|
||||
level_integration.on_change = [this](size_t n, OptionsField::value_t v) {
|
||||
(void)n;
|
||||
reset_live_view(true);
|
||||
level_integration.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
reset_live_view();
|
||||
live_frequency_integrate = v;
|
||||
};
|
||||
level_integration.set_selected_index(3); // default integration of ( 3 * old value + new_value ) / 4
|
||||
level_integration.set_selected_index(live_frequency_integrate);
|
||||
|
||||
filter_config.on_change = [this](size_t n, OptionsField::value_t v) {
|
||||
(void)n;
|
||||
reset_live_view(true);
|
||||
filter_config.on_change = [this](size_t ix, OptionsField::value_t v) {
|
||||
reset_live_view();
|
||||
min_color_power = v;
|
||||
filter_index = ix;
|
||||
};
|
||||
filter_config.set_selected_index(0);
|
||||
filter_config.set_selected_index(filter_index);
|
||||
|
||||
range_presets.on_change = [this](size_t n, OptionsField::value_t v) {
|
||||
(void)n;
|
||||
range_presets.on_change = [this](size_t ix, OptionsField::value_t v) {
|
||||
preset_index = ix;
|
||||
if (ix == 0) return; // Don't update range for "Manual".
|
||||
|
||||
// NB: Don't trigger updates, presets directly set the range
|
||||
// values without applying step or range lock.
|
||||
field_frequency_min.set_value(presets_db[v].min, false);
|
||||
field_frequency_max.set_value(presets_db[v].max, false);
|
||||
on_range_changed();
|
||||
};
|
||||
range_presets.set_selected_index(preset_index);
|
||||
|
||||
button_marker.on_change = [this]() {
|
||||
if (((int)marker_pixel_index + button_marker.get_encoder_delta()) < 0) {
|
||||
marker_pixel_index = 0;
|
||||
} else if (((int)marker_pixel_index + button_marker.get_encoder_delta()) > SCREEN_W) {
|
||||
marker_pixel_index = SCREEN_W;
|
||||
} else {
|
||||
marker_pixel_index = marker_pixel_index + button_marker.get_encoder_delta();
|
||||
}
|
||||
field_marker.on_encoder_change = [this](TextField&, EncoderEvent delta) {
|
||||
marker_pixel_index = clip<uint8_t>(marker_pixel_index + delta, 0, SCREEN_W);
|
||||
on_marker_change();
|
||||
button_marker.set_encoder_delta(0);
|
||||
};
|
||||
|
||||
button_marker.on_select = [this](ButtonWithEncoder&) {
|
||||
receiver_model.set_tuning_frequency(marker); // Center tune rx in marker freq.
|
||||
receiver_model.set_frequency_step(MHZ_DIV); // Preset a 1 MHz frequency step into RX -> AUDIO
|
||||
nav_.pop();
|
||||
nav_.push<AnalogAudioView>(); // Jump into audio view
|
||||
field_marker.on_select = [this](TextField&) {
|
||||
// Launch Audio with marker frequency.
|
||||
launch_audio(marker);
|
||||
};
|
||||
|
||||
field_trigger.on_change = [this](int32_t v) {
|
||||
baseband::set_spectrum(looking_glass_sampling_rate, v);
|
||||
trigger = v;
|
||||
baseband::set_spectrum(looking_glass_bandwidth, trigger);
|
||||
};
|
||||
field_trigger.set_value(32); // Defaults to 32, as normal triggering resolution
|
||||
field_trigger.set_value(trigger);
|
||||
|
||||
button_range.on_select = [this](Button&) {
|
||||
if (locked_range) {
|
||||
locked_range = false;
|
||||
button_range.set_style(&style_white);
|
||||
button_range.set_text(" " + to_string_dec_uint(search_span) + " ");
|
||||
} else {
|
||||
locked_range = true;
|
||||
button_range.set_style(&style_red);
|
||||
button_range.set_text(">" + to_string_dec_uint(search_span) + "<");
|
||||
}
|
||||
field_range.on_select = [this](TextField&) {
|
||||
locked_range = !locked_range;
|
||||
update_range_field();
|
||||
};
|
||||
|
||||
button_jump.on_select = [this](Button&) {
|
||||
receiver_model.set_tuning_frequency(max_freq_hold); // Center tune rx in marker freq.
|
||||
receiver_model.set_frequency_step(MHZ_DIV); // Preset a 1 MHz frequency step into RX -> AUDIO
|
||||
nav_.pop();
|
||||
nav_.push<AnalogAudioView>(); // Jump into audio view
|
||||
// Launch Audio with peak frequency.
|
||||
launch_audio(max_freq_hold);
|
||||
};
|
||||
|
||||
button_rst.on_select = [this](Button&) {
|
||||
reset_live_view(true);
|
||||
reset_live_view();
|
||||
};
|
||||
|
||||
display.scroll_set_area(109, 319);
|
||||
baseband::set_spectrum(looking_glass_sampling_rate, field_trigger.value()); // trigger:
|
||||
// Discord User jteich: WidebandSpectrum::on_message to set the trigger value. In WidebandSpectrum::execute ,
|
||||
// it keeps adding the output of the fft to the buffer until "trigger" number of calls are made,
|
||||
// at which time it pushes the buffer up with channel_spectrum.feed
|
||||
|
||||
marker_pixel_index = 120;
|
||||
on_range_changed();
|
||||
// trigger:
|
||||
// Discord User jteich: WidebandSpectrum::on_message to set the trigger value. In WidebandSpectrum::execute,
|
||||
// it keeps adding the output of the fft to the buffer until "trigger" number of calls are made,
|
||||
// at which time it pushes the buffer up with channel_spectrum.feed
|
||||
baseband::set_spectrum(looking_glass_bandwidth, trigger);
|
||||
|
||||
marker_pixel_index = SCREEN_W / 2;
|
||||
on_range_changed(); // Force a UI update.
|
||||
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::SpectrumAnalysis);
|
||||
receiver_model.set_sampling_rate(looking_glass_sampling_rate); // 20mhz
|
||||
receiver_model.set_baseband_bandwidth(looking_glass_bandwidth); // possible values: 1.75/2.5/3.5/5/5.5/6/7/8/9/10/12/14/15/20/24/28MHz
|
||||
receiver_model.set_squelch_level(0);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void GlassView::load_Presets() {
|
||||
File presets_file; // LOAD /WHIPCALC/ANTENNAS.TXT from microSD
|
||||
auto result = presets_file.open("LOOKINGGLASS/PRESETS.TXT");
|
||||
presets_db.clear(); // Start with fresh db
|
||||
if (result.is_valid()) {
|
||||
presets_Default(); // There is no txt, store a default range
|
||||
} else {
|
||||
std::string line; // There is a txt file
|
||||
char one_char[1]; // Read it char by char
|
||||
for (size_t pointer = 0; pointer < presets_file.size(); pointer++) {
|
||||
presets_file.seek(pointer);
|
||||
presets_file.read(one_char, 1);
|
||||
if ((int)one_char[0] > 31) { // ascii space upwards
|
||||
line += one_char[0]; // Add it to the textline
|
||||
} else if (one_char[0] == '\n') { // New Line
|
||||
txtline_process(line); // make sense of this textline
|
||||
line.clear(); // Ready for next textline
|
||||
}
|
||||
void GlassView::load_presets() {
|
||||
File presets_file;
|
||||
auto error = presets_file.open("LOOKINGGLASS/PRESETS.TXT");
|
||||
presets_db.clear();
|
||||
|
||||
// Add the "Manual" entry.
|
||||
presets_db.push_back({0, 0, "Manual"});
|
||||
|
||||
if (!error) {
|
||||
auto reader = FileLineReader(presets_file);
|
||||
for (const auto& line : reader) {
|
||||
if (line.length() == 0 || line[0] == '#')
|
||||
continue;
|
||||
|
||||
auto cols = split_string(line, ',');
|
||||
if (cols.size() != 3)
|
||||
continue;
|
||||
|
||||
preset_entry entry{};
|
||||
parse_int(cols[0], entry.min);
|
||||
parse_int(cols[1], entry.max);
|
||||
entry.label = trimr(cols[2]);
|
||||
|
||||
if (entry.min == 0 || entry.max == 0 || entry.min >= entry.max)
|
||||
continue; // Invalid line.
|
||||
|
||||
presets_db.emplace_back(std::move(entry));
|
||||
}
|
||||
if (line.length() > 0)
|
||||
txtline_process(line); // Last line had no newline at end ?
|
||||
if (!presets_db.size())
|
||||
presets_Default(); // no antenna on txt, use default
|
||||
}
|
||||
populate_Presets();
|
||||
|
||||
populate_presets();
|
||||
}
|
||||
|
||||
void GlassView::txtline_process(std::string& line) {
|
||||
if (line.find("#") != std::string::npos)
|
||||
return; // Line is just a comment
|
||||
|
||||
size_t comma = line.find(","); // Get first comma position
|
||||
if (comma == std::string::npos)
|
||||
return; // No comma at all
|
||||
|
||||
size_t previous = 0;
|
||||
preset_entry new_preset;
|
||||
|
||||
new_preset.min = std::stoi(line.substr(0, comma));
|
||||
if (!new_preset.min)
|
||||
return; // No frequency!
|
||||
|
||||
previous = comma + 1;
|
||||
comma = line.find(",", previous); // Search for next delimiter
|
||||
if (comma == std::string::npos)
|
||||
return; // No comma at all
|
||||
|
||||
new_preset.max = std::stoi(line.substr(previous, comma - previous));
|
||||
if (!new_preset.max)
|
||||
return; // No frequency!
|
||||
|
||||
new_preset.label = line.substr(comma + 1);
|
||||
if (new_preset.label.size() == 0)
|
||||
return; // No label ?
|
||||
|
||||
presets_db.push_back(new_preset); // Add this preset.
|
||||
}
|
||||
|
||||
void GlassView::populate_Presets() {
|
||||
void GlassView::populate_presets() {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t entries;
|
||||
|
||||
for (preset_entry preset : presets_db) { // go thru all available presets
|
||||
for (const auto& preset : presets_db)
|
||||
entries.emplace_back(preset.label, entries.size());
|
||||
}
|
||||
range_presets.set_options(entries);
|
||||
|
||||
range_presets.set_options(std::move(entries));
|
||||
}
|
||||
|
||||
void GlassView::presets_Default() {
|
||||
presets_db.clear();
|
||||
presets_db.push_back({2320, 2560, "DEFAULT WIFI 2.4GHz"});
|
||||
void GlassView::launch_audio(rf::Frequency center_freq) {
|
||||
receiver_model.set_target_frequency(center_freq);
|
||||
auto settings = receiver_model.settings();
|
||||
settings.frequency_step = MHZ_DIV; // Preset a 1 MHz frequency step into RX -> AUDIO
|
||||
nav_.replace<AnalogAudioView>(settings); // Jump into audio view
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -26,16 +26,20 @@
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "spectrum_color_lut.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
#define LOOKING_GLASS_SLICE_WIDTH_MAX 20000000
|
||||
#define MHZ_DIV 1000000
|
||||
|
||||
@@ -59,7 +63,7 @@ class GlassView : public View {
|
||||
GlassView& operator=(const GlassView& nav);
|
||||
|
||||
~GlassView();
|
||||
std::string title() const override { return "LookingGlass"; };
|
||||
std::string title() const override { return "Looking Glass"; };
|
||||
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
@@ -67,6 +71,29 @@ class GlassView : public View {
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{ReceiverModel::Mode::SpectrumAnalysis};
|
||||
// Settings
|
||||
rf::Frequency f_min = 260 * MHZ_DIV; // Default to 315/433 remote range.
|
||||
rf::Frequency f_max = 500 * MHZ_DIV;
|
||||
uint8_t preset_index = 0; // Manual
|
||||
uint8_t filter_index = 0; // OFF
|
||||
uint8_t trigger = 32;
|
||||
uint8_t mode = LOOKING_GLASS_FASTSCAN;
|
||||
uint8_t live_frequency_view = 0; // Spectrum
|
||||
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_glass"sv,
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"min"sv, &f_min},
|
||||
{"max"sv, &f_max},
|
||||
{"preset"sv, &preset_index},
|
||||
{"filter"sv, &filter_index},
|
||||
{"trigger"sv, &trigger},
|
||||
{"scan_mode"sv, &mode},
|
||||
{"freq_view"sv, &live_frequency_view},
|
||||
{"freq_integrate"sv, &live_frequency_integrate},
|
||||
}};
|
||||
|
||||
struct preset_entry {
|
||||
rf::Frequency min{};
|
||||
@@ -74,81 +101,66 @@ class GlassView : public View {
|
||||
std::string label{};
|
||||
};
|
||||
|
||||
const Style style_white{
|
||||
// free range
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
const Style style_red{
|
||||
// locked range
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::red(),
|
||||
};
|
||||
|
||||
std::vector<preset_entry> presets_db{};
|
||||
void update_min(int32_t v);
|
||||
void update_max(int32_t v);
|
||||
void update_range_field();
|
||||
void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power);
|
||||
rf::Frequency get_freq_from_bin_pos(uint8_t pos);
|
||||
void on_marker_change();
|
||||
int64_t next_mult_of(int64_t num, int64_t multiplier);
|
||||
void adjust_range(int64_t* f_min, int64_t* f_max, int64_t width);
|
||||
void retune();
|
||||
bool move_to_next_position();
|
||||
bool process_bins(uint8_t* powerlevel);
|
||||
void on_channel_spectrum(const ChannelSpectrum& spectrum);
|
||||
void do_timers();
|
||||
int64_t next_mult_of(int64_t num, int64_t multiplier);
|
||||
void adjust_range(int64_t* f_min, int64_t* f_max, int64_t width);
|
||||
void on_range_changed();
|
||||
void on_lna_changed(int32_t v_db);
|
||||
void on_vga_changed(int32_t v_db);
|
||||
void reset_live_view(bool clear_screen);
|
||||
void reset_live_view();
|
||||
void add_spectrum_pixel(uint8_t power);
|
||||
void PlotMarker(uint8_t pos);
|
||||
void load_Presets();
|
||||
void txtline_process(std::string& line);
|
||||
void populate_Presets();
|
||||
void presets_Default();
|
||||
void plot_marker(uint8_t pos);
|
||||
void load_presets();
|
||||
void populate_presets();
|
||||
void launch_audio(rf::Frequency center_freq);
|
||||
|
||||
rf::Frequency f_min{0}, f_max{0};
|
||||
rf::Frequency search_span{0};
|
||||
rf::Frequency f_center{0};
|
||||
rf::Frequency f_center_ini{0};
|
||||
|
||||
rf::Frequency marker{0};
|
||||
uint8_t marker_pixel_index{0};
|
||||
rf::Frequency marker_pixel_step{0};
|
||||
// size of one spectrum bin in Hz
|
||||
|
||||
// Size of one spectrum bin in Hz.
|
||||
rf::Frequency each_bin_size{0};
|
||||
// consumed number of Hz, used to know if we have filled a 'bag' , a corresponding pixel length on screen
|
||||
rf::Frequency bins_Hz_size{0};
|
||||
// Bandwidth of a single spectrum bin.
|
||||
rf::Frequency bins_hz_size{0};
|
||||
rf::Frequency looking_glass_sampling_rate{0};
|
||||
rf::Frequency looking_glass_bandwidth{0};
|
||||
rf::Frequency looking_glass_range{0};
|
||||
rf::Frequency looking_glass_step{0};
|
||||
uint8_t min_color_power{0};
|
||||
uint8_t min_color_power{0}; // Filter cutoff level.
|
||||
uint32_t pixel_index{0};
|
||||
std::array<Color, SCREEN_W> spectrum_row = {0};
|
||||
std::array<uint8_t, SCREEN_W> spectrum_data = {0};
|
||||
ChannelSpectrumFIFO* fifo{nullptr};
|
||||
uint8_t max_power = 0;
|
||||
int32_t steps = 0;
|
||||
uint8_t live_frequency_view = 0;
|
||||
int16_t live_frequency_integrate = 3;
|
||||
int64_t max_freq_hold = 0;
|
||||
int16_t max_freq_power = -1000;
|
||||
|
||||
std::array<Color, SCREEN_W> spectrum_row{};
|
||||
std::array<uint8_t, SCREEN_W> spectrum_data{};
|
||||
ChannelSpectrumFIFO* fifo{};
|
||||
|
||||
int32_t steps = 1;
|
||||
bool locked_range = false;
|
||||
|
||||
uint8_t max_power = 0;
|
||||
rf::Frequency max_freq_hold = 0;
|
||||
rf::Frequency last_max_freq = 0;
|
||||
int16_t max_freq_power = -1000;
|
||||
uint8_t bin_length = SCREEN_W;
|
||||
uint8_t real_bin_length = SCREEN_W;
|
||||
uint8_t offset = 0;
|
||||
uint8_t tune_offset = 0;
|
||||
uint8_t bin = 0;
|
||||
int64_t last_max_freq = 0;
|
||||
uint8_t mode = LOOKING_GLASS_FASTSCAN;
|
||||
uint8_t ignore_dc = 0;
|
||||
|
||||
Labels labels{
|
||||
{{0, 0}, "MIN: MAX: LNA VGA ", Color::light_grey()},
|
||||
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Color::light_grey()},
|
||||
{{0, 0 * 16}, "MIN: MAX: LNA VGA ", Color::light_grey()},
|
||||
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Color::light_grey()},
|
||||
{{0, 2 * 16}, "PRESET:", Color::light_grey()},
|
||||
{{0, 3 * 16}, "MARKER: MHz", Color::light_grey()},
|
||||
{{0, 3 * 16}, "MARKER: MHz", Color::light_grey()},
|
||||
{{0, 4 * 16}, "RES: STEP:", Color::light_grey()}};
|
||||
|
||||
NumberField field_frequency_min{
|
||||
@@ -171,8 +183,8 @@ class GlassView : public View {
|
||||
VGAGainField field_vga{
|
||||
{27 * 8, 0 * 16}};
|
||||
|
||||
Button button_range{
|
||||
{7 * 8, 1 * 16, 4 * 8, 16},
|
||||
TextField field_range{
|
||||
{6 * 8, 1 * 16, 6 * 8, 16},
|
||||
""};
|
||||
|
||||
OptionsField filter_config{
|
||||
@@ -180,23 +192,21 @@ class GlassView : public View {
|
||||
4,
|
||||
{
|
||||
{"OFF ", 0},
|
||||
{"MID ", 118}, // 85 +25 (110) + a bit more to kill all blue
|
||||
{"MID ", 118}, // 85 + 25 (110) + a bit more to kill all blue
|
||||
{"HIGH", 202}, // 168 + 25 (193)
|
||||
}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{29 * 8, 1 * 16}};
|
||||
{28 * 8, 1 * 16}};
|
||||
|
||||
OptionsField range_presets{
|
||||
{7 * 8, 2 * 16},
|
||||
20,
|
||||
{
|
||||
{" NONE (WIFI 2.4GHz)", 0},
|
||||
}};
|
||||
{}};
|
||||
|
||||
ButtonWithEncoder button_marker{
|
||||
{7 * 8, 3 * 16, 10 * 8, 16},
|
||||
" "};
|
||||
TextField field_marker{
|
||||
{7 * 8, 3 * 16, 9 * 8, 16},
|
||||
""};
|
||||
|
||||
NumberField field_trigger{
|
||||
{4 * 8, 4 * 16},
|
||||
|
||||
@@ -22,22 +22,21 @@
|
||||
|
||||
#include "ui_mictx.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "audio.hpp"
|
||||
|
||||
#include "wm8731.hpp"
|
||||
using wolfson::wm8731::WM8731;
|
||||
|
||||
#include "tonesets.hpp"
|
||||
#include "portapack_hal.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "portapack_hal.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "tonesets.hpp"
|
||||
#include "ui_tone_key.hpp"
|
||||
#include "wm8731.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
using namespace tonekey;
|
||||
using namespace portapack;
|
||||
using wolfson::wm8731::WM8731;
|
||||
|
||||
WM8731 audio_codec_wm8731{i2c0, 0x1a};
|
||||
|
||||
@@ -52,7 +51,7 @@ void MicTXView::focus() {
|
||||
field_rxfrequency.focus();
|
||||
break;
|
||||
default:
|
||||
field_va.focus();
|
||||
check_va.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -61,23 +60,56 @@ void MicTXView::update_vumeter() {
|
||||
vumeter.set_value(audio_level);
|
||||
}
|
||||
|
||||
void MicTXView::update_tx_icon() {
|
||||
tx_icon.set_foreground(transmitting ? Color::red() : Color::black());
|
||||
tx_icon.set_background(transmitting ? Color::yellow() : Color::black());
|
||||
}
|
||||
|
||||
void MicTXView::on_tx_progress(const bool done) {
|
||||
// Roger beep played, stop transmitting
|
||||
if (done)
|
||||
set_tx(false);
|
||||
}
|
||||
|
||||
uint8_t MicTXView::shift_bits(void) {
|
||||
uint8_t shift_bits_s16{4}; // shift bits factor to the captured ADC S16 audio sample.
|
||||
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
switch (ak4951_alc_and_wm8731_boost_GUI) {
|
||||
case 0: // +12 dB’s respect reference level orig fw 1.5.x fw FM : when +20dB's boost ON) and shift bits (>>8),
|
||||
shift_bits_s16 = 6; // now mic-boost on (+20dBs) and shift bits (>>6), +20+12=32 dB’s (orig fw +20 dBs+ 0dBs)=> +12dB's respect ref.
|
||||
break;
|
||||
case 1: // +06 dB’s reference level, (when +20dB's boost ON)
|
||||
shift_bits_s16 = 7; // now mic-boost on (+20dBs) and shift bits (>>7), +20+06=26 dB’s (orig fw +20 dBs+ 0dBs) => +06dB's respect ref.
|
||||
break;
|
||||
case 2:
|
||||
shift_bits_s16 = 4; // +04 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (4) (+0+24dB's)=24 dBs => +04dB's respect ref.
|
||||
case 3:
|
||||
shift_bits_s16 = 5; // -02 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (5) (+0+18dB's)=18 dBs => -02dB's respect ref.
|
||||
case 4:
|
||||
shift_bits_s16 = 6; // -08 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (6) (+0+12dB's)=12 dBs => -08dB's respect ref.
|
||||
}
|
||||
} else {
|
||||
shift_bits_s16 = 8; // Initialized default fixed >>8_FM for FM tx mod, shift audio data for AK4951, using top 8 bits s16 data (>>8)
|
||||
}
|
||||
return shift_bits_s16;
|
||||
}
|
||||
|
||||
void MicTXView::configure_baseband() {
|
||||
// TODO: Can this use the transmitter model instead?
|
||||
baseband::set_audiotx_config(
|
||||
sampling_rate / 20, // Update vu-meter at 20Hz
|
||||
transmitting ? transmitter_model.channel_bandwidth() : 0,
|
||||
mic_gain,
|
||||
shift_bits_s16, // to be used in dsp_modulate
|
||||
mic_gain_x10 / 10.0,
|
||||
shift_bits(), // to be used in dsp_modulate
|
||||
TONES_F2D(tone_key_frequency(tone_key_index), sampling_rate),
|
||||
enable_am,
|
||||
enable_dsb,
|
||||
enable_usb,
|
||||
enable_lsb);
|
||||
(mic_mod_index == MIC_MOD_AM),
|
||||
(mic_mod_index == MIC_MOD_DSB),
|
||||
(mic_mod_index == MIC_MOD_USB),
|
||||
(mic_mod_index == MIC_MOD_LSB));
|
||||
}
|
||||
|
||||
void MicTXView::set_tx(bool enable) {
|
||||
@@ -86,7 +118,11 @@ void MicTXView::set_tx(bool enable) {
|
||||
rxaudio(false); // Then turn off audio RX
|
||||
transmitting = true;
|
||||
configure_baseband();
|
||||
transmitter_model.set_tuning_frequency(tx_frequency); // Now,no need: transmitter_model.set_tx_gain(tx_gain), nor (rf_amp);
|
||||
transmitter_model.set_target_frequency(tx_frequency); // Now, no need: transmitter_model.set_tx_gain(tx_gain), nor (rf_amp);
|
||||
|
||||
/* The max. Power Spectrum Densitiy in WFM with High tone mod level (80%) and high 32kHZ subtone as fmod. with max fdeviation 150k ,
|
||||
BW aprox = 2 *(150K + 32K) = 364khz, then we just select the minimum TX LPF 1M75. */
|
||||
transmitter_model.set_baseband_bandwidth(1'750'000);
|
||||
transmitter_model.enable();
|
||||
portapack::pin_i2s0_rx_sda.mode(3); // This is already done in audio::init but gets changed by the CPLD overlay reprogramming
|
||||
} else {
|
||||
@@ -97,12 +133,24 @@ void MicTXView::set_tx(bool enable) {
|
||||
transmitting = false;
|
||||
configure_baseband();
|
||||
transmitter_model.disable();
|
||||
if (rx_enabled) // If audio RX is enabled and we've been transmitting
|
||||
|
||||
if (rx_enabled) {
|
||||
rxaudio(true); // Turn back on audio RX
|
||||
|
||||
// TODO FIXME: this isn't working: vu meter isn't going to 0:
|
||||
vumeter.set_value(0); // Reset vumeter
|
||||
vumeter.dirty(); // Force to refresh vumeter.
|
||||
}
|
||||
}
|
||||
}
|
||||
update_tx_icon();
|
||||
}
|
||||
|
||||
bool MicTXView::tx_button_held() {
|
||||
const auto switches_state = get_switches_state();
|
||||
return switches_state[(size_t)ui::KeyEvent::Select];
|
||||
};
|
||||
|
||||
void MicTXView::do_timing() {
|
||||
if (va_enabled) {
|
||||
if (!transmitting) {
|
||||
@@ -124,7 +172,8 @@ void MicTXView::do_timing() {
|
||||
if ((decay_timer >> 8) >= decay_ms) {
|
||||
decay_timer = 0;
|
||||
attack_timer = 0;
|
||||
set_tx(false);
|
||||
if (!button_touch && !tx_button_held())
|
||||
set_tx(false);
|
||||
} else {
|
||||
decay_timer += lcd_frame_duration;
|
||||
}
|
||||
@@ -134,54 +183,40 @@ void MicTXView::do_timing() {
|
||||
}
|
||||
} else {
|
||||
// Check for PTT release
|
||||
const auto switches_state = get_switches_state();
|
||||
if (!switches_state[4] && transmitting && !button_touch) // Select button
|
||||
if (transmitting && !button_touch && !tx_button_held())
|
||||
set_tx(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hmmmm. Maybe useless now.
|
||||
void MicTXView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
//if ( rx_enabled )
|
||||
receiver_model.set_tuning_frequency(f); //Update freq also for RX
|
||||
}
|
||||
*/
|
||||
|
||||
void MicTXView::rxaudio(bool is_on) {
|
||||
if (is_on) {
|
||||
void MicTXView::rxaudio(bool enable) {
|
||||
if (enable) {
|
||||
audio::input::stop();
|
||||
baseband::shutdown();
|
||||
|
||||
if (enable_am || enable_usb || enable_lsb || enable_dsb) { // "NFM/FM",0 ," WFM ",1 , " AM ",2, " USB ", 3, " LSB ",4, " DSB-SC", 5
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); // that AM demodulation engine is common to all Amplitude mod : AM/USB/LSB/DSB (2,3,4,5)
|
||||
if (options_mode.selected_index() < 5) // We will be called here with 2,3,4,5 . We treat here demod. filter 2,3,4; (excluding DSB-C case (5) it is treated more down).
|
||||
receiver_model.set_am_configuration(options_mode.selected_index() - 1); // selecting proper filter(2,3,4). 2-1=1=>6k-AM(1) , 3-1=2=>+3k-USB(2), 4-1=3=>-3K-LSB(3),
|
||||
} else { // We are in NFM/FM or WFM (NFM BW:8k5 or 11k / FM BW 16k / WFM BW:200k)
|
||||
|
||||
if (enable_wfm) { // WFM , BW 200Khz aprox , or the two new addional BW filters (180k, 40k)
|
||||
switch (mic_mod_index) {
|
||||
case MIC_MOD_NFM: // NFM BW:8k5 or 11k / FM BW 16k
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
// receiver_model.set_nbfm_configuration(n); is called above, depending user's selection (8k5, 11k, 16k).
|
||||
break;
|
||||
case MIC_MOD_WFM: // WFM, BW 200Khz aprox, or the two new addional BW filters (180k, 40k)
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
// receiver_model.set_wfm_configuration(n); // it is called above , depending user's selection (200k, 180k,40k).
|
||||
} else { // NFM BW:8k5 or 11k / FM BW 16k
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); //
|
||||
// receiver_model.set_nbfm_configuration(n); is called above , depending user's selection (8k5, 11k, 16k).
|
||||
}
|
||||
// receiver_model.set_wfm_configuration(n); is called above, depending user's selection (200k, 180k, 0k).
|
||||
break;
|
||||
case MIC_MOD_AM:
|
||||
case MIC_MOD_DSB:
|
||||
case MIC_MOD_USB:
|
||||
case MIC_MOD_LSB:
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); // that AM demodulation engine is common to all Amplitude mod : AM/USB/LSB/DSB (2,3,4,5)
|
||||
if (options_mode.selected_index() < 5) // We will be called here with 2,3,4,5. We treat here demod. filter 2,3,4; (excluding DSB-C case (5) it is treated more down).
|
||||
receiver_model.set_am_configuration(options_mode.selected_index() - 1); // selecting proper filter(2,3,4). 2-1=1=>6k-AM(1), 3-1=2=>+3k-USB(2), 4-1=3=>-3K-LSB(3),
|
||||
break;
|
||||
}
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
// receiver_model.set_tuning_frequency(field_frequency.value()); //probably this too can be commented out.
|
||||
if (bool_same_F_tx_rx_enabled) // when stop TX ,define to which freq RX we return
|
||||
receiver_model.set_tuning_frequency(tx_frequency); // Update freq also for RX = TX
|
||||
else
|
||||
receiver_model.set_tuning_frequency(rx_frequency); // Now with seperate freq controls!
|
||||
receiver_model.set_lna(rx_lna);
|
||||
receiver_model.set_vga(rx_vga);
|
||||
receiver_model.set_rf_amp(rx_amp);
|
||||
|
||||
receiver_model.set_target_frequency(bool_same_F_tx_rx_enabled ? tx_frequency : rx_frequency);
|
||||
receiver_model.enable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to have a good RX without any ghost inside Mic App
|
||||
audio::output::start();
|
||||
} else { // These incredibly convoluted steps are required for the vumeter to reappear when stopping RX.
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); // This fixes something with AM RX...
|
||||
@@ -190,21 +225,82 @@ void MicTXView::rxaudio(bool is_on) {
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_mic_tx);
|
||||
audio::output::stop();
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // When detected AK4951 => set up ALC mode; when detected WM8731 => set up mic_boost ON/OFF.
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // set up ALC mode (AK4951) or set up mic_boost ON/OFF (WM8731). and the check box "Hear Mic"
|
||||
portapack::pin_i2s0_rx_sda.mode(3);
|
||||
configure_baseband();
|
||||
}
|
||||
}
|
||||
|
||||
void MicTXView::on_headphone_volume_changed(int32_t v) {
|
||||
// if (rx_enabled) {
|
||||
const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
receiver_model.set_headphone_volume(new_volume);
|
||||
//}
|
||||
void MicTXView::set_rxbw_options(void) {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t rxbw; // Aux structure to change dynamically field_rxbw contents
|
||||
|
||||
switch (mic_mod_index) {
|
||||
case MIC_MOD_NFM:
|
||||
freqman_set_bandwidth_option(NFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with NFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(receiver_model.nbfm_configuration());
|
||||
break;
|
||||
case MIC_MOD_WFM:
|
||||
freqman_set_bandwidth_option(WFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with WFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(receiver_model.wfm_configuration());
|
||||
break;
|
||||
case MIC_MOD_AM:
|
||||
rxbw.emplace_back("DSB 9k", 0); // we offer in AM DSB two audio BW 9k / 6k.
|
||||
rxbw.emplace_back("DSB 6k", 1);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
case MIC_MOD_DSB:
|
||||
rxbw.emplace_back("USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-SC case to the field_rxbw value.
|
||||
rxbw.emplace_back("LSB-3k", 1);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
case MIC_MOD_USB:
|
||||
rxbw.emplace_back("USB+3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
case MIC_MOD_LSB:
|
||||
rxbw.emplace_back("LSB-3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MicTXView::set_ptt_visibility(bool v) {
|
||||
tx_button.hidden(!v);
|
||||
void MicTXView::set_rxbw_defaults(bool use_app_settings) {
|
||||
if (use_app_settings) {
|
||||
field_bw.set_value(transmitter_model.channel_bandwidth() / 1000);
|
||||
field_rxbw.set_by_value(rxbw_index);
|
||||
} else if (mic_mod_index == MIC_MOD_NFM) {
|
||||
field_bw.set_value(10); // NFM TX bw 10k, RX bw 16k (2) default
|
||||
field_rxbw.set_by_value(2);
|
||||
} else if (mic_mod_index == MIC_MOD_WFM) {
|
||||
field_bw.set_value(75); // WFM TX bw 75K, RX bw 200k (0) default
|
||||
field_rxbw.set_by_value(0);
|
||||
}
|
||||
// field_bw is hidden in other modulation cases
|
||||
}
|
||||
|
||||
void MicTXView::update_receiver_rxbw(void) {
|
||||
// In Previous fw versions, that nbfm_configuration(n) was done in any mode (FM/AM/SSB/DSB)...strictly speaking only need it in (NFM/FM)
|
||||
// In AM TX mode, we will allow both independent RX audio BW : AM 9K (9K00AE3 / AM 6K (6K00AE3). (In AM option v can be 0 (9k), 1 (6k)
|
||||
// In DSB-SC TX mode, we will allow both independent RX SSB demodulation (USB / LSB side band). in that submenu, v is 0 (SSB1 USB) or 1 (SSB2 LSB)
|
||||
switch (mic_mod_index) {
|
||||
case MIC_MOD_NFM:
|
||||
receiver_model.set_nbfm_configuration(rxbw_index); // we are in NFM/FM case, we need to select proper NFM/FM RX channel filter, NFM BW 8K5(0), NFM BW 11K(1), FM BW 16K (2)
|
||||
break;
|
||||
case MIC_MOD_WFM:
|
||||
receiver_model.set_wfm_configuration(rxbw_index); // we are in WFM case, we need to select proper WFB RX BW filter, WFM BW 200K(0), WFM BW 180K(1), WFM BW 40K(2)
|
||||
break;
|
||||
case MIC_MOD_AM:
|
||||
receiver_model.set_am_configuration(rxbw_index); // we are in AM TX mode, we need to select proper AM full path config AM-9K filter. 0+0 =>AM-9K(0), 0+1=1 =>AM-6K(1),
|
||||
break;
|
||||
case MIC_MOD_USB:
|
||||
case MIC_MOD_LSB:
|
||||
break; // change can't happen in these modes because there's only one BW choice
|
||||
case MIC_MOD_DSB:
|
||||
receiver_model.set_am_configuration(rxbw_index + 2); // we are in DSB-SC TX mode, we need to select proper SSB filter. 0+2 =>usb(2), 1+2=3 =>lsb(3),
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MicTXView::MicTXView(
|
||||
@@ -213,426 +309,317 @@ MicTXView::MicTXView(
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_mic_tx);
|
||||
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
add_children({&labels_WM8731, // we have audio codec WM8731, same MIC menu as original.
|
||||
&vumeter,
|
||||
&options_gain, // MIC GAIN float factor on the GUI.
|
||||
&options_wm8731_boost_mode,
|
||||
// &check_va,
|
||||
&field_va,
|
||||
&field_va_level,
|
||||
&field_va_attack,
|
||||
&field_va_decay,
|
||||
&field_bw,
|
||||
&tx_view, // it already integrates previous rfgain, rfamp.
|
||||
&options_mode,
|
||||
&field_frequency,
|
||||
&options_tone_key,
|
||||
&check_rogerbeep,
|
||||
&check_common_freq_tx_rx, // added to handle common or separate freq- TX/RX
|
||||
&check_rxactive,
|
||||
&field_volume,
|
||||
&field_rxbw,
|
||||
&field_squelch,
|
||||
&field_rxfrequency,
|
||||
&field_rxlna,
|
||||
&field_rxvga,
|
||||
&field_rxamp,
|
||||
&tx_button});
|
||||
bool wm = (audio::debug::codec_name() == "WM8731");
|
||||
add_children({&labels_both,
|
||||
wm ? &labels_WM8731 : &labels_AK4951, // enable ALC if AK4951
|
||||
&vumeter,
|
||||
&options_gain, // MIC GAIN float factor on the GUI.
|
||||
wm ? &options_wm8731_boost_mode : &options_ak4951_alc_mode,
|
||||
&check_va,
|
||||
&field_va_level,
|
||||
&field_va_attack,
|
||||
&field_va_decay,
|
||||
&field_bw,
|
||||
&tx_view, // it already integrates previous rfgain, rfamp.
|
||||
&options_mode,
|
||||
&field_frequency,
|
||||
&options_tone_key,
|
||||
&check_rogerbeep,
|
||||
&check_mic_to_HP, // check box to activate "hear mic"
|
||||
&check_common_freq_tx_rx, // added to handle common or separate freq- TX/RX
|
||||
&check_rxactive,
|
||||
&field_volume,
|
||||
&field_rxbw,
|
||||
&field_squelch,
|
||||
&field_rxfrequency,
|
||||
&field_rxlna,
|
||||
&field_rxvga,
|
||||
&field_rxamp,
|
||||
&tx_button,
|
||||
&tx_icon});
|
||||
|
||||
} else {
|
||||
add_children({&labels_AK4951, // we have audio codec AK4951, enable Automatic Level Control
|
||||
&vumeter,
|
||||
&options_gain,
|
||||
&options_ak4951_alc_mode,
|
||||
// &check_va,
|
||||
&field_va,
|
||||
&field_va_level,
|
||||
&field_va_attack,
|
||||
&field_va_decay,
|
||||
&field_bw,
|
||||
&tx_view, // it already integrates previous rfgain, rfamp.
|
||||
&options_mode,
|
||||
&field_frequency,
|
||||
&options_tone_key,
|
||||
&check_rogerbeep,
|
||||
&check_common_freq_tx_rx, // added to handle common or separate freq- TX/RX
|
||||
&check_rxactive,
|
||||
&field_volume,
|
||||
&field_rxbw,
|
||||
&field_squelch,
|
||||
&field_rxfrequency,
|
||||
&field_rxlna,
|
||||
&field_rxvga,
|
||||
&field_rxamp,
|
||||
&tx_button});
|
||||
}
|
||||
set_rxbw_options();
|
||||
set_rxbw_defaults(settings_.loaded());
|
||||
|
||||
tone_keys_populate(options_tone_key);
|
||||
options_tone_key.set_selected_index(tone_key_index);
|
||||
options_tone_key.on_change = [this](size_t i, int32_t) {
|
||||
tone_key_index = i;
|
||||
};
|
||||
options_tone_key.set_selected_index(0);
|
||||
|
||||
check_rogerbeep.set_value(rogerbeep_enabled);
|
||||
check_rogerbeep.on_select = [this](Checkbox&, bool v) {
|
||||
rogerbeep_enabled = v;
|
||||
};
|
||||
|
||||
field_rxlna.set_value(receiver_model.lna());
|
||||
field_rxlna.on_change = [this](int32_t v) {
|
||||
receiver_model.set_lna(v);
|
||||
};
|
||||
|
||||
field_rxvga.set_value(receiver_model.vga());
|
||||
field_rxvga.on_change = [this](int32_t v) {
|
||||
receiver_model.set_vga(v);
|
||||
};
|
||||
|
||||
field_rxamp.set_value(receiver_model.rf_amp());
|
||||
field_rxamp.on_change = [this](int32_t v) {
|
||||
receiver_model.set_rf_amp(v);
|
||||
};
|
||||
|
||||
options_gain.on_change = [this](size_t, int32_t v) {
|
||||
mic_gain = v / 10.0;
|
||||
mic_gain_x10 = v;
|
||||
configure_baseband();
|
||||
};
|
||||
options_gain.set_selected_index(1); // x1.0 preselected default.
|
||||
options_gain.set_by_value(mic_gain_x10); // x1.0 preselected default.
|
||||
|
||||
field_rxbw.on_change = [this](size_t, int32_t v) {
|
||||
rxbw_index = v;
|
||||
update_receiver_rxbw();
|
||||
};
|
||||
field_rxbw.set_by_value(rxbw_index);
|
||||
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
options_wm8731_boost_mode.on_change = [this](size_t, int8_t v) {
|
||||
switch (v) {
|
||||
case 0: // +12 dB’s respect reference level orig fw 1.5.x fw FM : when +20dB's boost ON) and shift bits (>>8),
|
||||
shift_bits_s16 = 6; // now mic-boost on (+20dBs) and shift bits (>>6), +20+12=32 dB’s (orig fw +20 dBs+ 0dBs)=> +12dB's respect ref.
|
||||
break;
|
||||
case 1: // +06 dB’s reference level , (when +20dB's boost ON)
|
||||
shift_bits_s16 = 7; // now mic-boost on (+20dBs) and shift bits (>>7), +20+06=26 dB’s (orig fw +20 dBs+ 0dBs) => +06dB's respect ref.
|
||||
break;
|
||||
case 2:
|
||||
shift_bits_s16 = 4; // +04 dB’s respect ref level , (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (4) (+0+24dB's)=24 dBs => +04dB's respect ref.
|
||||
case 3:
|
||||
shift_bits_s16 = 5; // -02 dB’s respect ref level , (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (5) (+0+18dB's)=18 dBs => -02dB's respect ref.
|
||||
case 4:
|
||||
shift_bits_s16 = 6; // -08 dB’s respect ref level , (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (6) (+0+12dB's)=12 dBs => -08dB's respect ref.
|
||||
}
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0,..4 WM8731_boost dB's options, (combination boost on/off , and effective gain in captured data >>x)
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // Detected (WM8731) , set up the proper wm_boost on/off , 0..4 (0,1) boost_on , (2,3,4) boost_0ff
|
||||
configure_baseband(); // to update in real timme,sending msg , var-parameters >>shift_bits FM msg ,to audio_tx from M0 to M4 Proc -
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0..4, WM8731_boost dB's options, (combination boost on/off, and effective gain in captured data >>x)
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // Detected (WM8731), set up the proper wm_boost on/off, 0..4 (0,1) boost_on, (2,3,4) boost_off,and the check box "Hear Mic"
|
||||
configure_baseband(); // to update in real time, sending msg, var-parameters >>shift_bits FM msg, to audio_tx from M0 to M4 Proc -
|
||||
};
|
||||
options_wm8731_boost_mode.set_selected_index(3); // preset GUI index 3 as default WM -> -02 dB's .
|
||||
if (!settings_.loaded()) {
|
||||
ak4951_alc_and_wm8731_boost_GUI = 3; // preset GUI index 3 as default WM -> -02 dB's
|
||||
}
|
||||
options_wm8731_boost_mode.set_selected_index(ak4951_alc_and_wm8731_boost_GUI);
|
||||
} else {
|
||||
shift_bits_s16 = 8; // Initialized default fixed >>8_FM for FM tx mod , shift audio data for AK4951 ,using top 8 bits s16 data (>>8)
|
||||
options_ak4951_alc_mode.on_change = [this](size_t, int8_t v) {
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0,..11, AK4951 Mic -Automatic volume Level Control options,
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // Detected (AK4951) ==> Set up proper ALC mode from 0..11 options
|
||||
configure_baseband(); // sending fixed >>8_FM , var-parameters msg , to audiotx from this M0 to M4 process.
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0..11, AK4951 Mic -Automatic volume Level Control options,
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // Detected (AK4951) ==> Set up proper ALC mode from 0..11 options, and the check box "Hear Mic"
|
||||
configure_baseband(); // sending fixed >>8_FM, var-parameters msg, to audiotx from this M0 to M4 process.
|
||||
};
|
||||
if (!settings_.loaded())
|
||||
ak4951_alc_and_wm8731_boost_GUI = 0;
|
||||
options_ak4951_alc_mode.set_selected_index(ak4951_alc_and_wm8731_boost_GUI);
|
||||
}
|
||||
|
||||
// options_ak4951_alc_mode.set_selected_index(0);
|
||||
|
||||
tx_frequency = transmitter_model.tuning_frequency();
|
||||
field_frequency.set_value(transmitter_model.tuning_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
// WARNING: transmitter_model.set_target_frequency() and receiver_model.set_target_frequency() both update the same tuning freq, but one has an offset!
|
||||
tx_frequency = transmitter_model.target_frequency();
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
tx_frequency = f;
|
||||
if (!rx_enabled) { // not activated receiver. just update freq TX
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
} else { // activated receiver.
|
||||
if (bool_same_F_tx_rx_enabled) // user selected common freq- TX = RX
|
||||
receiver_model.set_tuning_frequency(f); // Update common freq also for RX
|
||||
}
|
||||
if (!rx_enabled)
|
||||
transmitter_model.set_target_frequency(f);
|
||||
else if (bool_same_F_tx_rx_enabled)
|
||||
receiver_model.set_target_frequency(f);
|
||||
// else tuning freq will be updated when rx is enabled, transmitting starts, or when rx_frequency is changed (if rx_enabled)
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
focused_ui = 0;
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(tx_frequency);
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
tx_frequency = f;
|
||||
if (!rx_enabled) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
} else {
|
||||
if (bool_same_F_tx_rx_enabled)
|
||||
receiver_model.set_tuning_frequency(f); // Update freq also for RX
|
||||
}
|
||||
this->field_frequency.set_value(f);
|
||||
set_dirty();
|
||||
};
|
||||
};
|
||||
field_frequency.set_value(tx_frequency);
|
||||
|
||||
field_bw.on_change = [this](uint32_t v) {
|
||||
transmitter_model.set_channel_bandwidth(v * 1000);
|
||||
};
|
||||
field_bw.set_value(10); // pre-default first time, TX deviation FM for NFM / FM
|
||||
// TODO: would be nice if frequency step was configurable in this app
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
|
||||
// now , no need direct update , field_rfgain , field_rfamp (it is done in ui_transmitter.cpp)
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t v) { //{ "NFM/FM", 0 }, { " WFM ", 1 },{ "AM", 2 },{ "USB", 3 },{ "LSB", 4 },{ "DSB", 5 }
|
||||
enable_am = false;
|
||||
enable_usb = false;
|
||||
enable_lsb = false;
|
||||
enable_dsb = false;
|
||||
enable_wfm = false;
|
||||
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t rxbw; // Aux structure to change dynamically field_rxbw contents,
|
||||
|
||||
switch (v) {
|
||||
case 0: //{ "FM", 0 }
|
||||
enable_am = false;
|
||||
enable_usb = false;
|
||||
enable_lsb = false;
|
||||
enable_dsb = false;
|
||||
field_bw.set_value(10); // pre-default deviation FM for WFM
|
||||
// field_bw.set_value(transmitter_model.channel_bandwidth() / 1000);
|
||||
// if (rx_enabled)
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.hidden(0); // we are in FM mode , we should have active the Key-tones & CTCSS option.
|
||||
|
||||
rxbw.emplace_back(" NFM1:8k5 ", 0); // restore the original dynamic field_rxbw value.
|
||||
rxbw.emplace_back(" NFM2:11k ", 1);
|
||||
rxbw.emplace_back(" FM :16k ", 2);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
|
||||
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter , in non FM mode.
|
||||
break;
|
||||
case 1: //{ "WFM", 1 }
|
||||
enable_am = false;
|
||||
enable_usb = false;
|
||||
enable_lsb = false;
|
||||
enable_dsb = false;
|
||||
enable_wfm = true;
|
||||
field_bw.set_value(75); // pre-default deviation FM for WFM
|
||||
// field_bw.set_value(transmitter_model.channel_bandwidth() / 1000);
|
||||
// if (rx_enabled)
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.hidden(0); // we are in WFM mode , we should have active the Key-tones & CTCSS option.
|
||||
|
||||
rxbw.emplace_back(" 200k-WFM ", 0); // We allow the user selection of the 3 x WFM BW filters, (0) WFM-200K, (1) WFM-180K , (2) WFM-40K .
|
||||
rxbw.emplace_back(" 180k-WFM ", 1);
|
||||
rxbw.emplace_back(" 40k-WFM ", 2);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter .
|
||||
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter , in non FM mode.
|
||||
break;
|
||||
case 2: //{ "AM", 2 }
|
||||
enable_am = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.set_selected_index(0); // we are NOT in FM mode , we reset the possible previous key-tones &CTCSS selection.
|
||||
set_dirty(); // Refresh display
|
||||
options_tone_key.hidden(1); // we hide that Key-tones & CTCSS input selecction, (no meaning in AM/DSB/SSB).
|
||||
|
||||
rxbw.emplace_back(" DSB1-9k ", 0); // we offer in AM DSB two audio BW 9k / 6k .
|
||||
rxbw.emplace_back(" DSB2-6k ", 1);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
field_rxbw.hidden(0); // we show fixed RX AM BW 6Khz
|
||||
field_bw.hidden(1); // we hide the FM TX deviation parameter , in non FM mode.
|
||||
check_rogerbeep.hidden(0); // make visible again the "rogerbeep" selection.
|
||||
break;
|
||||
case 3: //{ "USB", 3 }
|
||||
enable_usb = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB , by now.
|
||||
check_rogerbeep.hidden(1); // hide that roger beep selection.
|
||||
|
||||
rxbw.emplace_back(" USB+3k ", 0); // locked a fixed option , to display it .
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
set_dirty(); // Refresh display
|
||||
break;
|
||||
case 4: //{ "LSB", 4 }
|
||||
enable_lsb = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB , by now.
|
||||
check_rogerbeep.hidden(1); // hide that roger beep selection.
|
||||
|
||||
rxbw.emplace_back(" LSB-3k ", 0); // locked a fixed option , to display it .
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
set_dirty(); // Refresh display
|
||||
break;
|
||||
case 5: //{ "DSB-SC", 5 }
|
||||
enable_dsb = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
check_rogerbeep.hidden(0); // make visible again the "rogerbeep" selection.
|
||||
|
||||
rxbw.emplace_back("SSB1:USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-C case to the field_rxbw value.
|
||||
rxbw.emplace_back("SSB2:LSB-3k", 1);
|
||||
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
break;
|
||||
}
|
||||
// configure_baseband();
|
||||
};
|
||||
|
||||
/*
|
||||
check_va.on_select = [this](Checkbox&, bool v) {
|
||||
va_enabled = v;
|
||||
text_ptt.hidden(v); //hide / show PTT text
|
||||
check_rxactive.hidden(v); //hide / show the RX AUDIO
|
||||
set_dirty(); //Refresh display
|
||||
};
|
||||
*/
|
||||
field_va.set_selected_index(1);
|
||||
field_va.on_change = [this](size_t, int32_t v) {
|
||||
switch (v) {
|
||||
case 0:
|
||||
va_enabled = 0;
|
||||
this->set_ptt_visibility(0);
|
||||
check_rxactive.hidden(0);
|
||||
ptt_enabled = 0;
|
||||
break;
|
||||
case 1:
|
||||
va_enabled = 0;
|
||||
this->set_ptt_visibility(1);
|
||||
check_rxactive.hidden(0);
|
||||
ptt_enabled = 1;
|
||||
break;
|
||||
case 2:
|
||||
if (!rx_enabled) {
|
||||
va_enabled = 1;
|
||||
this->set_ptt_visibility(0);
|
||||
check_rxactive.hidden(1);
|
||||
ptt_enabled = 0;
|
||||
} else {
|
||||
field_va.set_selected_index(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
check_rogerbeep.on_select = [this](Checkbox&, bool v) {
|
||||
rogerbeep_enabled = v;
|
||||
};
|
||||
|
||||
check_common_freq_tx_rx.on_select = [this](Checkbox&, bool v) {
|
||||
bool_same_F_tx_rx_enabled = v;
|
||||
field_rxfrequency.hidden(v); // Hide or show separated freq RX field . (When no hide user can enter down indep. freq for RX)
|
||||
set_dirty(); // Refresh GUI interface
|
||||
receiver_model.set_tuning_frequency(v ? tx_frequency : rx_frequency); // To go to the proper tuned freq. when toggling it
|
||||
};
|
||||
|
||||
field_va_level.on_change = [this](int32_t v) {
|
||||
va_level = v;
|
||||
vumeter.set_mark(v);
|
||||
};
|
||||
field_va_level.set_value(40);
|
||||
|
||||
field_va_attack.on_change = [this](int32_t v) {
|
||||
attack_ms = v;
|
||||
};
|
||||
field_va_attack.set_value(500);
|
||||
|
||||
field_va_decay.on_change = [this](int32_t v) {
|
||||
decay_ms = v;
|
||||
};
|
||||
field_va_decay.set_value(1000);
|
||||
|
||||
check_rxactive.on_select = [this](Checkbox&, bool v) {
|
||||
// vumeter.set_value(0); //Start with a clean vumeter
|
||||
rx_enabled = v;
|
||||
// check_va.hidden(v); //Hide or show voice activation
|
||||
rxaudio(v); // Activate-Deactivate audio rx accordingly
|
||||
set_dirty(); // Refresh interface
|
||||
};
|
||||
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
field_volume.on_change = [this](int32_t v) { this->on_headphone_volume_changed(v); };
|
||||
|
||||
field_rxbw.on_change = [this](size_t, int32_t v) {
|
||||
if (!(enable_am || enable_usb || enable_lsb || enable_dsb || enable_wfm)) {
|
||||
// In Previous fw versions, that nbfm_configuration(n) was done in any mode (FM/AM/SSB/DSB)...strictly speaking only need it in (NFM/FM)
|
||||
receiver_model.set_nbfm_configuration(v); // we are in NFM/FM case, we need to select proper NFM/FM RX channel filter , NFM BW 8K5(0), NFM BW 11K(1) , FM BW 16K (2)
|
||||
} else { // we are not in NFM/FM mode .(we could be in any of the rest : AM /USB/LSB/DSB-SC)
|
||||
if (enable_am) { // we are in AM TX mode , we will allow both independent RX audio BW : AM 9K (9K00AE3 / AM 6K (6K00AE3). (In AM option v can be 0 (9k) , 1 (6k)
|
||||
receiver_model.set_am_configuration(v); // we are in AM TX mode , we need to select proper AM full path config AM-9K filter. 0+0 =>AM-9K(0), 0+1=1 =>AM-6K(1),
|
||||
}
|
||||
if (enable_dsb) { // we are in DSB-SC in TX mode , we will allow both independent RX SSB demodulation (USB / LSB side band). in that submenu, v is 0 (SSB1 USB) or 1 (SSB2 LSB)
|
||||
receiver_model.set_am_configuration(v + 2); // we are in DSB-SC TX mode , we need to select proper SSB filter. 0+2 =>usb(2), 1+2=3 =>lsb(3),
|
||||
}
|
||||
if (enable_wfm) {
|
||||
receiver_model.set_wfm_configuration(v); // we are in WFM case, we need to select proper WFB RX BW filter , WFM BW 200K(0), WFM BW 180K(1) , WFM BW 40K(2)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(100 - v);
|
||||
};
|
||||
field_squelch.set_value(0);
|
||||
receiver_model.set_squelch_level(0);
|
||||
|
||||
rx_frequency = receiver_model.tuning_frequency();
|
||||
// WARNING: transmitter_model.set_target_frequency() and receiver_model.set_target_frequency() both update the same tuning freq, but one has an offset!
|
||||
field_rxfrequency.set_value(rx_frequency);
|
||||
field_rxfrequency.set_step(receiver_model.frequency_step());
|
||||
field_rxfrequency.on_change = [this](rf::Frequency f) { // available when field rxfrequency not hidden => user selected separated freq RX/TX-
|
||||
rx_frequency = f;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
receiver_model.set_target_frequency(rx_frequency);
|
||||
};
|
||||
field_rxfrequency.on_edit = [this, &nav]() { // available when field rxfrequency not hidden => user selected separated freq RX/TX-
|
||||
focused_ui = 1;
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(rx_frequency);
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
rx_frequency = f;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
this->field_rxfrequency.set_value(f);
|
||||
set_dirty();
|
||||
};
|
||||
};
|
||||
|
||||
rx_lna = receiver_model.lna();
|
||||
field_rxlna.on_change = [this](int32_t v) {
|
||||
rx_lna = v;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_lna(v);
|
||||
};
|
||||
field_rxlna.set_value(rx_lna);
|
||||
// TODO: would be nice if frequency step was configurable in this app
|
||||
field_rxfrequency.set_step(receiver_model.frequency_step());
|
||||
|
||||
rx_vga = receiver_model.vga();
|
||||
field_rxvga.on_change = [this](int32_t v) {
|
||||
rx_vga = v;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_vga(v);
|
||||
check_common_freq_tx_rx.on_select = [this](Checkbox&, bool v) {
|
||||
bool_same_F_tx_rx_enabled = v;
|
||||
field_rxfrequency.hidden(v);
|
||||
set_dirty();
|
||||
receiver_model.set_target_frequency(bool_same_F_tx_rx_enabled ? tx_frequency : rx_frequency); // To go to the proper tuned freq. when toggling it
|
||||
};
|
||||
field_rxvga.set_value(rx_vga);
|
||||
check_common_freq_tx_rx.set_value(bool_same_F_tx_rx_enabled);
|
||||
|
||||
rx_amp = receiver_model.rf_amp();
|
||||
field_rxamp.on_change = [this](int32_t v) {
|
||||
rx_amp = v;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_rf_amp(rx_amp);
|
||||
field_bw.on_change = [this](uint32_t v) {
|
||||
transmitter_model.set_channel_bandwidth(v * 1000);
|
||||
};
|
||||
field_rxamp.set_value(rx_amp);
|
||||
field_bw.set_value(transmitter_model.channel_bandwidth() / 1000); // pre-default first time, TX deviation FM for NFM / FM
|
||||
|
||||
// now, no need direct update, field_rfgain, field_rfamp (it is done in ui_transmitter.cpp)
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t v) {
|
||||
mic_mod_index = v;
|
||||
|
||||
// update bw & tone key visibility - only visible in NFM/WFM modes
|
||||
if ((mic_mod_index == MIC_MOD_NFM) || (mic_mod_index == MIC_MOD_WFM)) {
|
||||
field_bw.hidden(false);
|
||||
options_tone_key.hidden(false);
|
||||
} else {
|
||||
field_bw.hidden(true);
|
||||
options_tone_key.set_selected_index(0);
|
||||
options_tone_key.hidden(true);
|
||||
}
|
||||
|
||||
// update rogerbeep visibility - disable in USB/LSB modes
|
||||
if ((mic_mod_index == MIC_MOD_USB) || (mic_mod_index == MIC_MOD_LSB)) {
|
||||
check_rogerbeep.set_value(false);
|
||||
check_rogerbeep.hidden(true);
|
||||
} else {
|
||||
check_rogerbeep.hidden(false);
|
||||
}
|
||||
|
||||
// update squelch visibility - only visible in NFM mode
|
||||
field_squelch.hidden(mic_mod_index != MIC_MOD_NFM);
|
||||
|
||||
// update bandwidth
|
||||
set_rxbw_options();
|
||||
set_rxbw_defaults(false);
|
||||
field_rxbw.hidden(false);
|
||||
set_dirty(); // Refresh display
|
||||
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
// configure_baseband();
|
||||
};
|
||||
options_mode.set_selected_index(mic_mod_index);
|
||||
|
||||
check_mic_to_HP.on_select = [this](Checkbox&, bool v) {
|
||||
mic_to_HP_enabled = v;
|
||||
if (mic_to_HP_enabled) { // When user click to "hear mic to HP", we will select the higher acoustic sound Option MODE ALC or BOOST-
|
||||
audio::input::loopback_mic_to_hp_enable();
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
options_wm8731_boost_mode.set_selected_index(0); // In WM we always go to Boost +12 dB’s respect reference level
|
||||
} else if (ak4951_alc_and_wm8731_boost_GUI == 0) // In AK we are changing only that ALC index =0, because in that option, there is no sound.
|
||||
options_ak4951_alc_mode.set_selected_index(1); // alc_mode =0 , means no ALC,no DIGITAL filter block (by passed), and that mode has no loopback mode.
|
||||
} else {
|
||||
audio::input::loopback_mic_to_hp_disable();
|
||||
}
|
||||
};
|
||||
check_mic_to_HP.set_value(mic_to_HP_enabled);
|
||||
|
||||
check_rxactive.on_select = [this](Checkbox&, bool v) {
|
||||
// vumeter.set_value(0); //Start with a clean vumeter
|
||||
rx_enabled = v;
|
||||
check_mic_to_HP.hidden(rx_enabled); // Toggle Hide / show "Hear Mic" checkbox depending if we activate or not the receiver. (if RX on => no visible "Mic Hear" option)
|
||||
if ((rx_enabled) && (transmitting))
|
||||
check_mic_to_HP.set_value(transmitting); // Once we activate the "Rx audio" in reception time we disable "Hear Mic", but we allow it again in TX time.
|
||||
|
||||
if (rx_enabled)
|
||||
check_va.set_value(false); // Disallow voice activation during RX audio (for now) - Future TODO: Should allow VOX during RX audio
|
||||
|
||||
rxaudio(v); // Activate-Deactivate audio RX (receiver) accordingly
|
||||
set_dirty(); // Refresh interface
|
||||
};
|
||||
check_rxactive.set_value(rx_enabled);
|
||||
|
||||
tx_button.on_select = [this](Button&) {
|
||||
if (ptt_enabled && !transmitting) {
|
||||
if (!transmitting) {
|
||||
set_tx(true);
|
||||
}
|
||||
};
|
||||
|
||||
tx_button.on_touch_release = [this](Button&) {
|
||||
if (button_touch) {
|
||||
button_touch = false;
|
||||
set_tx(false);
|
||||
}
|
||||
button_touch = false;
|
||||
};
|
||||
|
||||
tx_button.on_touch_press = [this](Button&) {
|
||||
if (!transmitting) {
|
||||
button_touch = true;
|
||||
}
|
||||
button_touch = true;
|
||||
};
|
||||
|
||||
field_va_level.on_change = [this](int32_t v) {
|
||||
va_level = v;
|
||||
vumeter.set_mark(v);
|
||||
};
|
||||
field_va_level.set_value(va_level);
|
||||
|
||||
field_va_attack.set_value(attack_ms);
|
||||
field_va_attack.on_change = [this](int32_t v) {
|
||||
attack_ms = v;
|
||||
};
|
||||
|
||||
field_va_decay.set_value(decay_ms);
|
||||
field_va_decay.on_change = [this](int32_t v) {
|
||||
decay_ms = v;
|
||||
};
|
||||
|
||||
check_va.on_select = [this](Checkbox&, bool v) {
|
||||
va_enabled = v;
|
||||
if (va_enabled)
|
||||
check_rxactive.set_value(false); // Disallow RX-audio in VOX mode (for now) - Future TODO: Should allow VOX during RX audio
|
||||
};
|
||||
check_va.set_value(va_enabled);
|
||||
|
||||
// These shouldn't be necessary, but because
|
||||
// this app uses both transmitter_model and directly
|
||||
// configures the baseband, these end up being required.
|
||||
transmitter_model.set_sampling_rate(sampling_rate);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
|
||||
set_tx(false);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // When detected AK4951 => set up ALC mode; when detected WM8731 => set up mic_boost ON/OFF.
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // set up ALC mode (AK4951) or set up mic_boost ON/OFF (WM8731). and the check box "Hear Mic"
|
||||
|
||||
// Workaround for bad RX reception when app first started -- shouldn't be necessary:
|
||||
// Trigger receiver to update modulation.
|
||||
if (rx_enabled)
|
||||
receiver_model.set_squelch_level(receiver_model.squelch_level());
|
||||
}
|
||||
|
||||
MicTXView::MicTXView(
|
||||
NavigationView& nav,
|
||||
ReceiverModel::settings_t override)
|
||||
: MicTXView(nav) {
|
||||
// Try to use the modulation/bandwidth from RX settings.
|
||||
// TODO: These concepts should be merged so there's only one.
|
||||
switch (override.mode) {
|
||||
case ReceiverModel::Mode::AMAudio:
|
||||
options_mode.set_selected_index(MIC_MOD_AM);
|
||||
break;
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio:
|
||||
options_mode.set_selected_index(MIC_MOD_NFM);
|
||||
break;
|
||||
case ReceiverModel::Mode::WidebandFMAudio:
|
||||
options_mode.set_selected_index(MIC_MOD_WFM);
|
||||
break;
|
||||
|
||||
// Unsupported modulations.
|
||||
case ReceiverModel::Mode::SpectrumAnalysis:
|
||||
case ReceiverModel::Mode::Capture:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
check_common_freq_tx_rx.set_value(true); // freq passed from other app is in tx_frequency, so set rx_frequency=tx_frequency
|
||||
|
||||
// TODO: bandwidth selection is tied too tightly to the UI
|
||||
// controls. It's not possible to set the bandwidth here without
|
||||
// refactoring. Also options_mode seems to have a category error.
|
||||
}
|
||||
|
||||
MicTXView::~MicTXView() {
|
||||
audio::input::stop();
|
||||
transmitter_model.set_tuning_frequency(tx_frequency); // Save Tx frequency instead of Rx. Or maybe we need some "System Wide" changes to seperate Tx and Rx frequency.
|
||||
transmitter_model.disable();
|
||||
if (rx_enabled) // Also turn off audio rx if enabled
|
||||
if (rx_enabled) { // Also turn off both (audio rx if enabled, and disable mic_loop to HP)
|
||||
rxaudio(false);
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
audio::input::loopback_mic_to_hp_disable(); // Leave Mic audio off in the main menu (as original audio path, otherwise we had No audio in next "Audio App")
|
||||
}
|
||||
transmitter_model.set_target_frequency(tx_frequency); // Set tx_frequency here to be saved in app_settings (might not match rx_frequency)
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -20,18 +20,18 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
// TODO: Consolidate Modulation/Bandwidth modes/settings with freqman/receiver_model.
|
||||
|
||||
#ifndef __UI_MICTX_H__
|
||||
#define __UI_MICTX_H__
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "message.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
@@ -41,6 +41,7 @@ namespace ui {
|
||||
class MicTXView : public View {
|
||||
public:
|
||||
MicTXView(NavigationView& nav);
|
||||
MicTXView(NavigationView& nav, ReceiverModel::settings_t override);
|
||||
~MicTXView();
|
||||
|
||||
MicTXView(const MicTXView&) = delete;
|
||||
@@ -69,86 +70,101 @@ class MicTXView : public View {
|
||||
static constexpr uint32_t lcd_frame_duration = (256 * 1000UL) / 60; // 1 frame @ 60fps in ms .8 fixed point /60
|
||||
|
||||
void update_vumeter();
|
||||
bool tx_button_held();
|
||||
void do_timing();
|
||||
void set_tx(bool enable);
|
||||
// void on_tuning_frequency_changed(rf::Frequency f);
|
||||
void on_tx_progress(const bool done);
|
||||
void update_tx_icon();
|
||||
uint8_t shift_bits(void);
|
||||
void configure_baseband();
|
||||
|
||||
void set_rxbw_options(void);
|
||||
void set_rxbw_defaults(bool use_app_settings);
|
||||
void update_receiver_rxbw(void);
|
||||
void rxaudio(bool is_on);
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
|
||||
void set_ptt_visibility(bool v);
|
||||
RxRadioState rx_radio_state_{};
|
||||
TxRadioState tx_radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
sampling_rate /* sampling rate */
|
||||
};
|
||||
|
||||
bool transmitting{false};
|
||||
enum Mic_Modulation : uint32_t {
|
||||
MIC_MOD_NFM = 0,
|
||||
MIC_MOD_WFM = 1,
|
||||
MIC_MOD_AM = 2,
|
||||
MIC_MOD_USB = 3,
|
||||
MIC_MOD_LSB = 4,
|
||||
MIC_MOD_DSB = 5,
|
||||
};
|
||||
|
||||
// Settings
|
||||
uint32_t mic_mod_index{0};
|
||||
uint32_t rxbw_index{0};
|
||||
bool va_enabled{false};
|
||||
bool ptt_enabled{true};
|
||||
bool rogerbeep_enabled{false};
|
||||
bool mic_to_HP_enabled{false};
|
||||
bool bool_same_F_tx_rx_enabled{false};
|
||||
rf::Frequency rx_frequency{0};
|
||||
bool rx_enabled{false};
|
||||
uint32_t tone_key_index{};
|
||||
float mic_gain{1.0};
|
||||
uint32_t tone_key_index{0};
|
||||
uint32_t mic_gain_x10{1};
|
||||
uint8_t ak4951_alc_and_wm8731_boost_GUI{0};
|
||||
uint32_t va_level{40};
|
||||
uint32_t attack_ms{500};
|
||||
uint32_t decay_ms{1000};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_mic",
|
||||
app_settings::Mode::RX_TX,
|
||||
app_settings::Options::UseGlobalTargetFrequency,
|
||||
{
|
||||
{"mic_mod_index"sv, &mic_mod_index},
|
||||
{"rxbw_index"sv, &rxbw_index},
|
||||
{"same_F_tx_rx"sv, &bool_same_F_tx_rx_enabled},
|
||||
{"mic_rx_frequency"sv, &rx_frequency},
|
||||
{"rx_enabled"sv, &rx_enabled},
|
||||
{"mic_gain_x10"sv, &mic_gain_x10},
|
||||
{"mic_to_HP"sv, &mic_to_HP_enabled},
|
||||
{"alc_and_boost"sv, &ak4951_alc_and_wm8731_boost_GUI},
|
||||
{"va_level"sv, &va_level},
|
||||
{"attack_ms"sv, &attack_ms},
|
||||
{"decay_ms"sv, &decay_ms},
|
||||
{"vox"sv, &va_enabled},
|
||||
{"rogerbeep"sv, &rogerbeep_enabled},
|
||||
{"tone_key_index"sv, &tone_key_index},
|
||||
}};
|
||||
|
||||
rf::Frequency tx_frequency{0};
|
||||
bool transmitting{false};
|
||||
uint32_t audio_level{0};
|
||||
uint32_t va_level{};
|
||||
uint32_t attack_ms{};
|
||||
uint32_t decay_ms{};
|
||||
uint32_t attack_timer{0};
|
||||
uint32_t decay_timer{0};
|
||||
int32_t tx_gain{47};
|
||||
bool rf_amp{false};
|
||||
int32_t rx_lna{32};
|
||||
int32_t rx_vga{32};
|
||||
bool rx_amp{false};
|
||||
rf::Frequency tx_frequency{0};
|
||||
rf::Frequency rx_frequency{0};
|
||||
int32_t focused_ui{2};
|
||||
bool button_touch{false};
|
||||
uint8_t shift_bits_s16{4}; // shift bits factor to the captured ADC S16 audio sample.
|
||||
|
||||
// AM TX Stuff
|
||||
bool enable_am{false};
|
||||
bool enable_dsb{false};
|
||||
bool enable_usb{false};
|
||||
bool enable_lsb{false};
|
||||
bool enable_wfm{false}; // added to distinguish in the FM mode , RX BW : NFM (8K5, 11K), FM (16K), WFM(200K)
|
||||
|
||||
Labels labels_WM8731{
|
||||
Labels labels_both{
|
||||
{{3 * 8, 1 * 8}, "MIC-GAIN:", Color::light_grey()},
|
||||
{{17 * 8, 1 * 8}, "Boost", Color::light_grey()},
|
||||
{{3 * 8, 3 * 8}, "F:", Color::light_grey()},
|
||||
{{15 * 8, 3 * 8}, "FM TXBW: kHz", Color::light_grey()}, // to be more symetric and consistent to the below FM RXBW
|
||||
{{18 * 8, (5 * 8)}, "Mode:", Color::light_grey()}, // now , no need to handle GAIN , Amp here It is handled by ui_transmitter.cpp
|
||||
{{3 * 8, 8 * 8}, "TX Activation:", Color::light_grey()}, // we delete { { 3 * 8, 5 * 8 }, "GAIN:", Color::light_grey() },
|
||||
{{18 * 8, (5 * 8)}, "Mode:", Color::light_grey()}, // now, no need to handle GAIN, Amp here It is handled by ui_transmitter.cpp
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
{{4 * 8, (13 * 8) - 2}, "TONE KEY:", Color::light_grey()},
|
||||
{{7 * 8, 23 * 8}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, 23 * 8}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, 25 * 8}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, 25 * 8}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, 27 * 8}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, 27 * 8}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, 27 * 8}, "AMP:", Color::light_grey()}};
|
||||
{{3 * 8, (13 * 8) - 5}, "TONE KEY:", Color::light_grey()},
|
||||
{{3 * 8, (18 * 8) - 1}, "======== Receiver ========", Color::green()},
|
||||
{{5 * 8, (23 * 8) + 2}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, (23 * 8) + 2}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, (25 * 8) + 2}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, (25 * 8) + 2}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, (27 * 8) + 2}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, (27 * 8) + 2}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, (27 * 8) + 2}, "AMP:", Color::light_grey()}};
|
||||
Labels labels_WM8731{
|
||||
{{17 * 8, 1 * 8}, "Boost", Color::light_grey()}};
|
||||
Labels labels_AK4951{
|
||||
{{3 * 8, 1 * 8}, "MIC-GAIN:", Color::light_grey()},
|
||||
{{17 * 8, 1 * 8}, "ALC", Color::light_grey()},
|
||||
{{3 * 8, 3 * 8}, "F:", Color::light_grey()},
|
||||
{{15 * 8, 3 * 8}, "FM TXBW: kHz", Color::light_grey()},
|
||||
{{18 * 8, (5 * 8)}, "Mode:", Color::light_grey()}, // now , no need to handle GAIN , Amp here It is handled by ui_transmitter.cpp
|
||||
{{3 * 8, 8 * 8}, "TX Activation:", Color::light_grey()}, // we delete { { 3 * 8, 5 * 8 }, "GAIN:", Color::light_grey() },
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
{{4 * 8, (13 * 8) - 2}, "TONE KEY:", Color::light_grey()},
|
||||
{{(6 * 8) + 4, 23 * 8}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, 23 * 8}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, 25 * 8}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, 25 * 8}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, 27 * 8}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, 27 * 8}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, 27 * 8}, "AMP:", Color::light_grey()}};
|
||||
{{17 * 8, 1 * 8}, "ALC", Color::light_grey()}};
|
||||
|
||||
VuMeter vumeter{
|
||||
{0 * 8, 1 * 8, 2 * 8, 33 * 8},
|
||||
@@ -164,15 +180,15 @@ class MicTXView : public View {
|
||||
{"x2.0", 20}}};
|
||||
|
||||
OptionsField options_ak4951_alc_mode{
|
||||
{20 * 8, 1 * 8}, // Coordinates are: int:x (px), int:y (px)
|
||||
11,
|
||||
{20 * 8, 1 * 8},
|
||||
10, // Label has 10 chars
|
||||
{
|
||||
{" OFF-12kHz", 0}, // Nothing changed from ORIGINAL,keeping ALL programmable AK4951 Digital Block->OFF, sampling 24Khz)
|
||||
{" OFF-12kHz", 0}, // Nothing changed from ORIGINAL, keeping ALL programmable AK4951 Digital Block->OFF, sampling 24Khz)
|
||||
{"+12dB-6kHz", 1}, // ALC-> on, (+12dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
|
||||
{"+09dB-6kHz", 2}, // ALC-> on, (+09dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
|
||||
{"+06dB-6kHz", 3}, // ALC-> on, (+06dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
|
||||
{"+03dB-2kHz", 4}, // ALC-> on, (+03dB's) Auto Vol max + Wind Noise cancel + LPF 3,5k + Pre-amp Mic (+21dB=original)+ EQ boosting ~<2kHz (f0~1k1,fb:1,7K, k=1,8)
|
||||
{"+03dB-4kHz", 5}, // ALC-> on, (+03dB's) Auto Vol max + Wind Noise cancel + LPF 4kHz + Pre-amp Mic (+21dB=original)+ EQ boosting ~<3kHz (f0~1k4,fb~2,4k, k=1,8)
|
||||
{"+03dB-2kHz", 4}, // ALC-> on, (+03dB's) Auto Vol max + Wind Noise cancel + LPF 3,5k + Pre-amp Mic (+21dB=original)+ EQ boosting ~<2kHz (f0~1k1, fb:1,7K, k=1,8)
|
||||
{"+03dB-4kHz", 5}, // ALC-> on, (+03dB's) Auto Vol max + Wind Noise cancel + LPF 4kHz + Pre-amp Mic (+21dB=original)+ EQ boosting ~<3kHz (f0~1k4, fb~2,4k, k=1,8)
|
||||
{"+03dB-6kHz", 6}, // ALC-> on, (+03dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
|
||||
{"+00dB-6kHz", 7}, // ALC-> on, (+00dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
|
||||
{"-03dB-6kHz", 8}, // ALC-> on, (-03dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
|
||||
@@ -182,16 +198,17 @@ class MicTXView : public View {
|
||||
}};
|
||||
|
||||
OptionsField options_wm8731_boost_mode{
|
||||
{22 * 8, 1 * 8}, // Coordinates are: int:x (px), int:y (px)
|
||||
5,
|
||||
{22 * 8, 1 * 8},
|
||||
8, // Label has 8 chars
|
||||
{
|
||||
{"ON +12dB", 0}, // WM8731 Mic Boost ON ,original+12dBs condition, easy to saturate ADC sat in high voice ,relative G = +12 dB's respect ref level
|
||||
{"ON +06dB", 1}, // WM8731 Mic Boost ON ,original+6 dBs condition, easy to saturate ADC sat in high voice ,relative G = +06 dB's respect ref level
|
||||
{"OFF+04dB", 2}, // WM8731 Mic Boost OFF to avoid ADC sat in high voice ,relative G = +04 dB's (respect ref level) , always effective sampling 24khz
|
||||
{"OFF-02dB", 3}, // WM8731 Mic Boost OFF to avoid ADC sat in high voice ,relative G = -02 dB's (respect ref level)
|
||||
{"OFF-08dB", 4}, // WM8731 Mic Boost OFF to avoid ADC sat in high voice ,relative G = -12 dB's (respect ref level)
|
||||
{"ON +12dB", 0}, // WM8731 Mic Boost ON, original+12dBs condition, easy to saturate ADC sat in high voice, relative G = +12 dB's respect ref level
|
||||
{"ON +06dB", 1}, // WM8731 Mic Boost ON, original+6 dBs condition, easy to saturate ADC sat in high voice, relative G = +06 dB's respect ref level
|
||||
{"OFF+04dB", 2}, // WM8731 Mic Boost OFF to avoid ADC sat in high voice, relative G = +04 dB's (respect ref level), always effective sampling 24khz
|
||||
{"OFF-02dB", 3}, // WM8731 Mic Boost OFF to avoid ADC sat in high voice, relative G = -02 dB's (respect ref level)
|
||||
{"OFF-08dB", 4}, // WM8731 Mic Boost OFF to avoid ADC sat in high voice, relative G = -12 dB's (respect ref level)
|
||||
}};
|
||||
|
||||
// TODO: Use TxFrequencyField
|
||||
FrequencyField field_frequency{
|
||||
{5 * 8, 3 * 8},
|
||||
};
|
||||
@@ -203,37 +220,26 @@ class MicTXView : public View {
|
||||
' '};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
||||
// 3*8, 2*8, SHORT_UI // x(columns), y (line) position. (used in Replay / GPS Simul / Playlist App's)
|
||||
3 * 8, 2 * 8, NORMAL_UI // x(columns), y (line) position. (used in Mic App)
|
||||
};
|
||||
{3 * 8, 5 * 8},
|
||||
/*short_ui*/ false};
|
||||
|
||||
OptionsField options_mode{
|
||||
{24 * 8, 5 * 8},
|
||||
4,
|
||||
6,
|
||||
{
|
||||
{"NFM/FM", 0},
|
||||
{" WFM ", 1},
|
||||
{" AM ", 2}, // in fact that TX mode = AM -DSB with carrier .
|
||||
{" USB ", 3},
|
||||
{" LSB ", 4},
|
||||
{"DSB-SC", 5} // We are TX Double Side AM Band with suppressed carrier, and allowing in RX both indep SSB lateral band (USB/LSB).
|
||||
{"NFM/FM", MIC_MOD_NFM},
|
||||
{" WFM ", MIC_MOD_WFM},
|
||||
{" AM ", MIC_MOD_AM}, // in fact that TX mode = AM -DSB with carrier.
|
||||
{" USB ", MIC_MOD_USB},
|
||||
{" LSB ", MIC_MOD_LSB},
|
||||
{"DSB-SC", MIC_MOD_DSB} // We are TX Double Side AM Band with suppressed carrier, and allowing in RX both indep SSB lateral band (USB/LSB).
|
||||
}};
|
||||
/*
|
||||
Checkbox check_va {
|
||||
{ 3 * 8, (10 * 8) - 4 },
|
||||
7,
|
||||
"Voice activation",
|
||||
false
|
||||
};
|
||||
*/
|
||||
|
||||
OptionsField field_va{
|
||||
{17 * 8, 8 * 8},
|
||||
3,
|
||||
{{" OFF", 0},
|
||||
{" PTT", 1},
|
||||
{"AUTO", 2}}};
|
||||
Checkbox check_va{
|
||||
{3 * 8, 8 * 7},
|
||||
10,
|
||||
"VOX enable",
|
||||
false};
|
||||
|
||||
NumberField field_va_level{
|
||||
{8 * 8, 10 * 8},
|
||||
@@ -255,59 +261,61 @@ class MicTXView : public View {
|
||||
' '};
|
||||
|
||||
OptionsField options_tone_key{
|
||||
{10 * 8, (15 * 8) - 2},
|
||||
23,
|
||||
{12 * 8, (13 * 8) - 5},
|
||||
18,
|
||||
{}};
|
||||
|
||||
Checkbox check_rogerbeep{
|
||||
{3 * 8, (16 * 8) + 7},
|
||||
{3 * 8, (14 * 8) + 4},
|
||||
10,
|
||||
"Roger beep",
|
||||
false};
|
||||
|
||||
Checkbox check_mic_to_HP{
|
||||
{18 * 8, (14 * 8) + 4},
|
||||
10,
|
||||
"Hear Mic",
|
||||
false};
|
||||
|
||||
Checkbox check_rxactive{
|
||||
{3 * 8, (21 * 8) - 4},
|
||||
18, // it was 8, but if it is string size should be 18
|
||||
"RX audio listening",
|
||||
{3 * 8, (21 * 8) - 7},
|
||||
8, // it was 18, but if it is string size should be 8
|
||||
"RX audio",
|
||||
false};
|
||||
|
||||
Checkbox check_common_freq_tx_rx{
|
||||
{18 * 8, (16 * 8) + 7},
|
||||
{18 * 8, (21 * 8) - 7},
|
||||
8,
|
||||
"F = F_RX",
|
||||
"F RX=TX",
|
||||
false};
|
||||
|
||||
NumberField field_volume{
|
||||
{11 * 8, 23 * 8},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
AudioVolumeField field_volume{
|
||||
{9 * 8, (23 * 8) + 2}};
|
||||
|
||||
OptionsField field_rxbw{
|
||||
{19 * 8, 23 * 8},
|
||||
3,
|
||||
{19 * 8, (23 * 8) + 2},
|
||||
7,
|
||||
{
|
||||
{" NFM1:8k5 ", 0},
|
||||
{" NFM2:11k ", 1},
|
||||
{" FM :16k ", 2},
|
||||
{" 8k5 ", 0}, // Initial dynamic values when we start Mic App.
|
||||
{" 11k ", 1},
|
||||
{" 16k ", 2},
|
||||
}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{23 * 8, 25 * 8},
|
||||
{23 * 8, (25 * 8) + 2},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
// TODO: Use RxFrequencyField
|
||||
FrequencyField field_rxfrequency{
|
||||
{10 * 8, 25 * 8},
|
||||
{10 * 8, (25 * 8) + 2},
|
||||
};
|
||||
|
||||
NumberField field_rxlna{
|
||||
{9 * 8, 27 * 8},
|
||||
{9 * 8, (27 * 8) + 2},
|
||||
2,
|
||||
{0, 40},
|
||||
8,
|
||||
@@ -315,7 +323,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
NumberField field_rxvga{
|
||||
{16 * 8, 27 * 8},
|
||||
{16 * 8, (27 * 8) + 2},
|
||||
2,
|
||||
{0, 62},
|
||||
2,
|
||||
@@ -323,7 +331,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
NumberField field_rxamp{
|
||||
{24 * 8, 27 * 8},
|
||||
{24 * 8, (27 * 8) + 2},
|
||||
1,
|
||||
{0, 1},
|
||||
1,
|
||||
@@ -332,9 +340,15 @@ class MicTXView : public View {
|
||||
|
||||
Button tx_button{
|
||||
{10 * 8, 30 * 8, 10 * 8, 5 * 8},
|
||||
"TX",
|
||||
"PTT TX",
|
||||
true};
|
||||
|
||||
Image tx_icon{
|
||||
{6 * 8, 31 * 8 + 4, 16, 16},
|
||||
&bitmap_icon_microphone,
|
||||
Color::black(),
|
||||
Color::black()};
|
||||
|
||||
MessageHandlerRegistration message_handler_lcd_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -41,15 +40,20 @@ ModemSetupView::ModemSetupView(
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t modem_options;
|
||||
|
||||
add_children({&labels,
|
||||
&field_baudrate,
|
||||
&field_mark,
|
||||
&field_space,
|
||||
&field_repeat,
|
||||
&options_modem,
|
||||
&button_set_modem,
|
||||
&sym_format,
|
||||
&button_save});
|
||||
add_children({
|
||||
&labels,
|
||||
&field_baudrate,
|
||||
&field_mark,
|
||||
&field_space,
|
||||
&field_repeat,
|
||||
&options_modem,
|
||||
&button_set_modem,
|
||||
&sym_format_data,
|
||||
&sym_format_parity,
|
||||
&sym_format_stop,
|
||||
&sym_format_msb,
|
||||
&button_save,
|
||||
});
|
||||
|
||||
// Only list AFSK modems for now
|
||||
for (size_t i = 0; i < MODEM_DEF_COUNT; i++) {
|
||||
@@ -59,15 +63,10 @@ ModemSetupView::ModemSetupView(
|
||||
options_modem.set_options(modem_options);
|
||||
options_modem.set_selected_index(0);
|
||||
|
||||
sym_format.set_symbol_list(0, "6789"); // Data bits
|
||||
sym_format.set_symbol_list(1, "NEo"); // Parity
|
||||
sym_format.set_symbol_list(2, "012"); // Stop bits
|
||||
sym_format.set_symbol_list(3, "ML"); // MSB/LSB first
|
||||
|
||||
sym_format.set_sym(0, persistent_memory::serial_format().data_bits - 6);
|
||||
sym_format.set_sym(1, persistent_memory::serial_format().parity);
|
||||
sym_format.set_sym(2, persistent_memory::serial_format().stop_bits);
|
||||
sym_format.set_sym(3, persistent_memory::serial_format().bit_order);
|
||||
sym_format_data.set_offset(0, persistent_memory::serial_format().data_bits - 6);
|
||||
sym_format_parity.set_offset(0, persistent_memory::serial_format().parity);
|
||||
sym_format_stop.set_offset(0, persistent_memory::serial_format().stop_bits);
|
||||
sym_format_msb.set_offset(0, persistent_memory::serial_format().bit_order);
|
||||
|
||||
field_mark.set_value(persistent_memory::afsk_mark_freq());
|
||||
field_space.set_value(persistent_memory::afsk_space_freq());
|
||||
@@ -84,18 +83,17 @@ ModemSetupView::ModemSetupView(
|
||||
};
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
serial_format_t serial_format;
|
||||
|
||||
persistent_memory::set_afsk_mark(field_mark.value());
|
||||
persistent_memory::set_afsk_space(field_space.value());
|
||||
|
||||
persistent_memory::set_modem_baudrate(field_baudrate.value());
|
||||
persistent_memory::set_modem_repeat(field_repeat.value());
|
||||
|
||||
serial_format.data_bits = sym_format.get_sym(0) + 6;
|
||||
serial_format.parity = (parity_enum)sym_format.get_sym(1);
|
||||
serial_format.stop_bits = sym_format.get_sym(2);
|
||||
serial_format.bit_order = (order_enum)sym_format.get_sym(3);
|
||||
serial_format_t serial_format{};
|
||||
serial_format.data_bits = sym_format_data.get_offset(0) + 6;
|
||||
serial_format.parity = (parity_enum)sym_format_parity.get_offset(0);
|
||||
serial_format.stop_bits = sym_format_stop.get_offset(0);
|
||||
serial_format.bit_order = (order_enum)sym_format_msb.get_offset(0);
|
||||
|
||||
persistent_memory::set_serial_format(serial_format);
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ class ModemSetupView : public View {
|
||||
std::string title() const override { return "Modem setup"; };
|
||||
|
||||
private:
|
||||
void update_freq(rf::Frequency f);
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 11 * 8}, "Baudrate:", Color::light_grey()},
|
||||
{{2 * 8, 13 * 8}, "Mark: Hz", Color::light_grey()},
|
||||
@@ -81,10 +79,25 @@ class ModemSetupView : public View {
|
||||
7,
|
||||
{}};
|
||||
|
||||
SymField sym_format{
|
||||
SymField sym_format_data{
|
||||
{16 * 8, 22 * 8},
|
||||
4,
|
||||
SymField::SYMFIELD_DEF};
|
||||
1,
|
||||
"6789"};
|
||||
|
||||
SymField sym_format_parity{
|
||||
{17 * 8, 22 * 8},
|
||||
1,
|
||||
"NEo"};
|
||||
|
||||
SymField sym_format_stop{
|
||||
{18 * 8, 22 * 8},
|
||||
1,
|
||||
"012"};
|
||||
|
||||
SymField sym_format_msb{
|
||||
{19 * 8, 22 * 8},
|
||||
1,
|
||||
"ML"};
|
||||
|
||||
Button button_set_modem{
|
||||
{23 * 8, 6 * 8 - 4, 6 * 8, 24},
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "baseband_api.hpp"
|
||||
#include "hackrf_gpio.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
@@ -57,13 +56,23 @@ static msg_t ookthread_fn(void* arg) {
|
||||
v = (symbol < 2) ? 1 : 0; // TX on for dot or dash, off for pause
|
||||
delay = morse_symbols[symbol];
|
||||
|
||||
gpio_og_tx.write(v);
|
||||
if (hackrf_r9) { // r9 has a common PIN 54 for MODE CONTROL TX / RX, <=r6 has two independent MODE CONTROL pins (TX and RX)
|
||||
gpio_r9_rx.write(!v); // in hackrf r9 opposite logic "0" means tx , "1" rx = no tx)
|
||||
} else {
|
||||
gpio_og_tx.write(v); // in hackrf <=r6, "1" means tx , "0" no tx.
|
||||
}
|
||||
|
||||
arg_c->on_tx_progress(i, false);
|
||||
|
||||
chThdSleepMilliseconds(delay * arg_c->time_unit_ms);
|
||||
}
|
||||
|
||||
gpio_og_tx.write(0); // Ensure TX is off
|
||||
if (hackrf_r9) { // r9 has a common PIN 54 for MODE CONTROL TX / RX, <=r6 has two independent MODE CONTROL pins (TX and RX)
|
||||
gpio_r9_rx.write(1); // Hackrf_r9 , common pin, rx="1" (we ensure TX is off) / tx="0"
|
||||
} else {
|
||||
gpio_og_tx.write(0); // Hackrf <=r6 independent TX / RX pins ,now touching the tx pin ==> Ensure TX is off
|
||||
}
|
||||
|
||||
arg_c->on_tx_progress(0, true);
|
||||
chThdExit(0);
|
||||
|
||||
@@ -98,13 +107,8 @@ void MorseView::focus() {
|
||||
}
|
||||
|
||||
MorseView::~MorseView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_morse", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void MorseView::paint(Painter&) {
|
||||
@@ -123,15 +127,14 @@ bool MorseView::start_tx() {
|
||||
}
|
||||
|
||||
progressbar.set_max(symbol_count);
|
||||
|
||||
transmitter_model.set_sampling_rate(1536000U);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
// By experimental spectrum measurment, we left exactly same TX LPF settings as previous fw 1.7.4, TX LPF= 1M75 (min).
|
||||
transmitter_model.set_baseband_bandwidth(1'750'000); // Min TX LPF .already tested in FM morse max tone 9,999k , max dev 150khz
|
||||
transmitter_model.enable();
|
||||
|
||||
if (modulation == CW) {
|
||||
baseband::set_tones_config(transmitter_model.channel_bandwidth(), 0, symbol_count, false, false);
|
||||
|
||||
if (mode_cw) {
|
||||
ookthread = chThdCreateStatic(ookthread_wa, sizeof(ookthread_wa), NORMALPRIO + 10, ookthread_fn, this);
|
||||
} else if (modulation == FM) {
|
||||
baseband::set_tones_config(transmitter_model.channel_bandwidth(), 0, symbol_count, false, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -140,8 +143,8 @@ bool MorseView::start_tx() {
|
||||
void MorseView::update_tx_duration() {
|
||||
uint32_t duration_ms;
|
||||
|
||||
time_unit_ms = 1200 / field_speed.value();
|
||||
symbol_count = morse_encode(message, time_unit_ms, field_tone.value(), &time_units);
|
||||
time_unit_ms = 1200 / speed;
|
||||
symbol_count = morse_encode(message, time_unit_ms, tone, &time_units);
|
||||
|
||||
if (symbol_count) {
|
||||
duration_ms = time_units * time_unit_ms;
|
||||
@@ -182,7 +185,7 @@ void MorseView::on_loop_progress(const uint32_t progress, const bool done) {
|
||||
}
|
||||
|
||||
void MorseView::set_foxhunt(size_t i) {
|
||||
message = foxhunt_codes[i];
|
||||
message = foxhunt_codes[i - 1];
|
||||
buffer = message.c_str();
|
||||
text_message.set(message);
|
||||
update_tx_duration();
|
||||
@@ -195,7 +198,7 @@ MorseView::MorseView(
|
||||
|
||||
add_children({&labels,
|
||||
&checkbox_foxhunt,
|
||||
&options_foxhunt,
|
||||
&field_foxhunt,
|
||||
&field_speed,
|
||||
&field_tone,
|
||||
&options_modulation,
|
||||
@@ -206,54 +209,55 @@ MorseView::MorseView(
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_morse", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
// Default settings
|
||||
field_speed.set_value(15); // 15wps
|
||||
field_tone.set_value(700); // 700Hz FM tone
|
||||
options_modulation.set_selected_index(0); // CW mode
|
||||
options_loop.set_selected_index(0); // Off
|
||||
field_speed.set_value(speed);
|
||||
field_tone.set_value(tone);
|
||||
options_modulation.set_by_value(mode_cw);
|
||||
options_loop.set_by_value(loop);
|
||||
checkbox_foxhunt.set_value(foxhunt_mode);
|
||||
field_foxhunt.set_value(foxhunt_code);
|
||||
|
||||
checkbox_foxhunt.on_select = [this](Checkbox&, bool value) {
|
||||
foxhunt_mode = value;
|
||||
|
||||
if (foxhunt_mode)
|
||||
set_foxhunt(options_foxhunt.selected_index_value());
|
||||
set_foxhunt(foxhunt_code);
|
||||
};
|
||||
|
||||
options_foxhunt.on_change = [this](size_t i, int32_t) {
|
||||
field_foxhunt.on_change = [this](int32_t value) {
|
||||
foxhunt_code = value;
|
||||
|
||||
if (foxhunt_mode)
|
||||
set_foxhunt(i);
|
||||
set_foxhunt(foxhunt_code);
|
||||
};
|
||||
|
||||
options_modulation.on_change = [this](size_t i, int32_t) {
|
||||
modulation = (modulation_t)i;
|
||||
};
|
||||
|
||||
options_loop.on_change = [this](size_t i, uint32_t n) {
|
||||
options_modulation.on_change = [this](size_t i, int32_t value) {
|
||||
(void)i; // avoid unused warning
|
||||
loop = n;
|
||||
mode_cw = (bool)value;
|
||||
};
|
||||
|
||||
field_speed.on_change = [this](int32_t) {
|
||||
options_loop.on_change = [this](size_t i, uint32_t value) {
|
||||
(void)i; // avoid unused warning
|
||||
loop = value;
|
||||
};
|
||||
|
||||
field_speed.on_change = [this](int32_t value) {
|
||||
speed = value;
|
||||
update_tx_duration();
|
||||
};
|
||||
|
||||
field_tone.on_change = [this](int32_t value) {
|
||||
tone = value;
|
||||
};
|
||||
|
||||
button_message.on_select = [this, &nav](Button&) {
|
||||
this->on_set_text(nav);
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "volume.hpp"
|
||||
@@ -64,19 +65,33 @@ class MorseView : public View {
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
std::string buffer{"PORTAPACK"};
|
||||
std::string message{};
|
||||
uint32_t time_units{0};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
|
||||
enum modulation_t {
|
||||
CW = 0,
|
||||
FM = 1
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
1536000 /* sampling rate */
|
||||
};
|
||||
modulation_t modulation{CW};
|
||||
|
||||
std::string buffer{"PORTAPACK"};
|
||||
bool mode_cw{true};
|
||||
bool foxhunt_mode{false};
|
||||
uint32_t foxhunt_code{1};
|
||||
uint32_t speed{15};
|
||||
uint32_t tone{700};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_morse",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"message"sv, &buffer},
|
||||
{"foxhunt"sv, &foxhunt_mode},
|
||||
{"foxhunt_code"sv, &foxhunt_code},
|
||||
{"speed"sv, &speed},
|
||||
{"tone"sv, &tone},
|
||||
{"mode_cw"sv, &mode_cw},
|
||||
{"loop"sv, &loop},
|
||||
}};
|
||||
|
||||
bool start_tx();
|
||||
void update_tx_duration();
|
||||
@@ -85,7 +100,6 @@ class MorseView : public View {
|
||||
|
||||
Thread* ookthread{nullptr};
|
||||
Thread* loopthread{nullptr};
|
||||
bool foxhunt_mode{false};
|
||||
bool run{false};
|
||||
|
||||
Labels labels{
|
||||
@@ -99,20 +113,12 @@ class MorseView : public View {
|
||||
{4 * 8, 16},
|
||||
8,
|
||||
"Foxhunt:"};
|
||||
OptionsField options_foxhunt{
|
||||
NumberField field_foxhunt{
|
||||
{17 * 8, 16 + 4},
|
||||
7,
|
||||
{{"1 (MOE)", 0},
|
||||
{"2 (MOI)", 1},
|
||||
{"3 (MOS)", 2},
|
||||
{"4 (MOH)", 3},
|
||||
{"5 (MO5)", 4},
|
||||
{"6 (MON)", 5},
|
||||
{"7 (MOD)", 6},
|
||||
{"8 (MOB)", 7},
|
||||
{"9 (MO6)", 8},
|
||||
{"X (MO) ", 9},
|
||||
{"T (S) ", 10}}};
|
||||
2,
|
||||
{1, 11},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_speed{
|
||||
{10 * 8, 6 * 8},
|
||||
@@ -131,8 +137,8 @@ class MorseView : public View {
|
||||
OptionsField options_modulation{
|
||||
{15 * 8, 10 * 8},
|
||||
2,
|
||||
{{"CW", 0},
|
||||
{"FM", 1}}};
|
||||
{{"CW", true},
|
||||
{"FM", false}}};
|
||||
|
||||
OptionsField options_loop{
|
||||
{9 * 8, 12 * 8},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
#include "ui_numbers.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
@@ -38,7 +39,7 @@ namespace ui {
|
||||
|
||||
void NumbersStationView::focus() {
|
||||
if (file_error)
|
||||
nav_.display_modal("No voices", "No valid voices found in\nthe /numbers directory.", ABORT, nullptr);
|
||||
nav_.display_modal("No voices", "No valid voices found in\nthe /numbers directory.", ABORT);
|
||||
else
|
||||
button_exit.focus();
|
||||
}
|
||||
@@ -75,7 +76,7 @@ void NumbersStationView::prepare_audio() {
|
||||
return;
|
||||
}
|
||||
|
||||
code = symfield_code.get_sym(code_index);
|
||||
code = symfield_code.get_offset(code_index);
|
||||
|
||||
if (code >= 10) {
|
||||
memset(audio_buffer, 0, 1024);
|
||||
@@ -129,9 +130,7 @@ void NumbersStationView::start_tx() {
|
||||
|
||||
prepare_audio();
|
||||
|
||||
transmitter_model.set_sampling_rate(1536000U);
|
||||
transmitter_model.set_rf_amp(true);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_audiotx_data(
|
||||
@@ -146,7 +145,7 @@ void NumbersStationView::on_tick_second() {
|
||||
armed_blink = not armed_blink;
|
||||
|
||||
if (armed_blink)
|
||||
check_armed.set_style(&style_red);
|
||||
check_armed.set_style(&Styles::red);
|
||||
else
|
||||
check_armed.set_style(&style());
|
||||
|
||||
@@ -154,14 +153,12 @@ void NumbersStationView::on_tick_second() {
|
||||
}
|
||||
|
||||
void NumbersStationView::on_voice_changed(size_t index) {
|
||||
std::string code_list = "";
|
||||
std::string code_list;
|
||||
|
||||
for (const auto& wavs : voices[index].available_wavs)
|
||||
code_list += wavs.code;
|
||||
|
||||
for (uint32_t c = 0; c < 25; c++)
|
||||
symfield_code.set_symbol_list(c, code_list);
|
||||
|
||||
symfield_code.set_symbol_list(code_list);
|
||||
current_voice = &voices[index];
|
||||
}
|
||||
|
||||
@@ -267,17 +264,17 @@ NumbersStationView::NumbersStationView(
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
symfield_code.set_sym(0, 10);
|
||||
symfield_code.set_sym(1, 3);
|
||||
symfield_code.set_sym(2, 4);
|
||||
symfield_code.set_sym(3, 11);
|
||||
symfield_code.set_sym(4, 6);
|
||||
symfield_code.set_sym(5, 1);
|
||||
symfield_code.set_sym(6, 9);
|
||||
symfield_code.set_sym(7, 7);
|
||||
symfield_code.set_sym(8, 8);
|
||||
symfield_code.set_sym(9, 0);
|
||||
symfield_code.set_sym(10, 12); // End
|
||||
symfield_code.set_offset(0, 10);
|
||||
symfield_code.set_offset(1, 3);
|
||||
symfield_code.set_offset(2, 4);
|
||||
symfield_code.set_offset(3, 11);
|
||||
symfield_code.set_offset(4, 6);
|
||||
symfield_code.set_offset(5, 1);
|
||||
symfield_code.set_offset(6, 9);
|
||||
symfield_code.set_offset(7, 7);
|
||||
symfield_code.set_offset(8, 8);
|
||||
symfield_code.set_offset(9, 0);
|
||||
symfield_code.set_offset(10, 12); // End
|
||||
|
||||
/*
|
||||
rtc::RTC datetime;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "clock_manager.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
@@ -35,6 +34,7 @@
|
||||
#include "message.hpp"
|
||||
#include "file.hpp"
|
||||
#include "io_wave.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -55,6 +55,12 @@ class NumbersStationView : public View {
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
1536000 /* sampling rate */
|
||||
};
|
||||
|
||||
// Sequencing state machine
|
||||
enum segments {
|
||||
IDLE = 0,
|
||||
@@ -63,11 +69,6 @@ class NumbersStationView : public View {
|
||||
SIGNOFF
|
||||
};
|
||||
|
||||
Style style_red{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::red()};
|
||||
|
||||
typedef struct {
|
||||
char code;
|
||||
uint32_t index;
|
||||
@@ -142,16 +143,19 @@ class NumbersStationView : public View {
|
||||
SymField symfield_code{
|
||||
{1 * 8, 10 * 8},
|
||||
25,
|
||||
SymField::SYMFIELD_DEF};
|
||||
SymField::Type::Custom};
|
||||
|
||||
Checkbox check_armed{
|
||||
{2 * 8, 13 * 16},
|
||||
5,
|
||||
"Armed"};
|
||||
/*Button button_tx_now {
|
||||
{ 18 * 8, 13 * 16, 10 * 8, 32 },
|
||||
"TX now"
|
||||
};*/
|
||||
|
||||
/*
|
||||
Button button_tx_now {
|
||||
{ 18 * 8, 13 * 16, 10 * 8, 32 },
|
||||
"TX now"};
|
||||
*/
|
||||
|
||||
Button button_exit{
|
||||
{21 * 8, 16 * 16, 64, 32},
|
||||
"Exit"};
|
||||
|
||||
@@ -77,11 +77,9 @@ void NuoptixView::transmit(bool setup) {
|
||||
timecode = number_timecode.value();
|
||||
}
|
||||
|
||||
transmitter_model.set_sampling_rate(1536000U);
|
||||
transmitter_model.set_rf_amp(true);
|
||||
transmitter_model.set_lna(40);
|
||||
transmitter_model.set_vga(40);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
dtmf_message[0] = '*'; // "Pre-tone for restart" method #1
|
||||
@@ -156,9 +154,9 @@ NuoptixView::NuoptixView(
|
||||
number_timecode.set_value(1);
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
transmitter_model.target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
@@ -34,6 +33,7 @@
|
||||
#include "message.hpp"
|
||||
#include "volume.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
#define NUOPTIX_TONE_LENGTH ((TONES_SAMPLERATE * 0.049) - 1) // 49ms
|
||||
|
||||
@@ -55,9 +55,14 @@ class NuoptixView : public View {
|
||||
IMPROVISE
|
||||
};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
1536000 /* sampling rate */
|
||||
};
|
||||
|
||||
tx_modes tx_mode{IDLE};
|
||||
|
||||
void on_tuning_frequency_changed(rf::Frequency f);
|
||||
void transmit(bool setup);
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyleft (ↄ) 2022 NotPike
|
||||
* Copyright (C) 2023 Kyle Reed, zxkmm
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -22,207 +23,342 @@
|
||||
*/
|
||||
|
||||
#include "ui_playlist.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "io_file.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "convert.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "io_convert.hpp"
|
||||
#include "oversample.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fstream>
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
/* TODO
|
||||
* - Should frequency overrides be saved in the playlist?
|
||||
* - Start playing from current track (vs start over)?
|
||||
*/
|
||||
|
||||
namespace ui {
|
||||
|
||||
void PlaylistView::set_ready() {
|
||||
ready_signal = true;
|
||||
}
|
||||
// TODO: consolidate extesions into a shared header?
|
||||
static const fs::path ppl_ext = u".PPL";
|
||||
|
||||
void PlaylistView::load_file(std::filesystem::path playlist_path) {
|
||||
void PlaylistView::load_file(const fs::path& playlist_path) {
|
||||
File playlist_file;
|
||||
|
||||
auto error = playlist_file.open(playlist_path.string());
|
||||
if (!error.is_valid()) {
|
||||
std::string line;
|
||||
char one_char[1];
|
||||
for (size_t pointer = 0; pointer < playlist_file.size(); pointer++) {
|
||||
playlist_file.seek(pointer);
|
||||
playlist_file.read(one_char, 1);
|
||||
if ((int)one_char[0] >= ' ') {
|
||||
line += one_char[0];
|
||||
} else if (one_char[0] == '\n') {
|
||||
txtline_process(line);
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
if (line.length() > 0) {
|
||||
txtline_process(line);
|
||||
}
|
||||
|
||||
if (error)
|
||||
return;
|
||||
|
||||
auto reader = FileLineReader(playlist_file);
|
||||
for (const auto& line : reader) {
|
||||
if (line.length() == 0 || line[0] == '#')
|
||||
continue; // Empty or comment line.
|
||||
|
||||
auto cols = split_string(line, ',');
|
||||
auto entry = load_entry(trim(cols[0]));
|
||||
|
||||
if (!entry)
|
||||
continue;
|
||||
|
||||
// Read optional delay value.
|
||||
if (cols.size() > 1)
|
||||
parse_int(cols[1], entry->ms_delay);
|
||||
|
||||
playlist_db_.emplace_back(*std::move(entry));
|
||||
}
|
||||
playlist_masterdb = playlist_db;
|
||||
return;
|
||||
}
|
||||
|
||||
void PlaylistView::txtline_process(std::string& line) {
|
||||
playlist_entry new_item;
|
||||
rf::Frequency f;
|
||||
size_t previous = 0;
|
||||
size_t current = line.find(',');
|
||||
std::string freqs = line.substr(0, current);
|
||||
previous = current + 1;
|
||||
current = line.find(',', previous);
|
||||
std::string file = line.substr(previous, current - previous);
|
||||
previous = current + 1;
|
||||
current = line.find(',', previous);
|
||||
uint32_t sample_rate = strtoll(line.substr(previous).c_str(), nullptr, 10);
|
||||
Optional<PlaylistView::playlist_entry> PlaylistView::load_entry(fs::path&& path) {
|
||||
File capture_file;
|
||||
|
||||
f = strtoll(freqs.c_str(), nullptr, 0);
|
||||
new_item.replay_frequency = f;
|
||||
new_item.replay_file = "/" + file;
|
||||
new_item.sample_rate = sample_rate;
|
||||
new_item.next_delay = 0;
|
||||
auto error = capture_file.open(path);
|
||||
if (error)
|
||||
return {};
|
||||
|
||||
playlist_db.push_back(std::move(new_item));
|
||||
// Read metafile if it exists.
|
||||
auto metadata_path = get_metadata_path(path);
|
||||
auto metadata = read_metadata_file(metadata_path);
|
||||
|
||||
// If no metadata found, fallback to the TX frequency.
|
||||
if (!metadata)
|
||||
metadata = {transmitter_model.target_frequency(), 500'000};
|
||||
|
||||
return playlist_entry{
|
||||
std::move(path),
|
||||
*metadata,
|
||||
capture_file.size(),
|
||||
0u};
|
||||
}
|
||||
|
||||
void PlaylistView::on_file_changed(std::filesystem::path new_file_path, rf::Frequency replay_frequency, uint32_t replay_sample_rate) {
|
||||
File data_file;
|
||||
// Get file size
|
||||
auto data_open_error = data_file.open("/" + new_file_path.string());
|
||||
if (data_open_error.is_valid()) {
|
||||
file_error();
|
||||
void PlaylistView::on_file_changed(const fs::path& new_file_path) {
|
||||
stop();
|
||||
|
||||
current_index_ = 0;
|
||||
playlist_path_ = new_file_path;
|
||||
playlist_db_.clear();
|
||||
load_file(playlist_path_);
|
||||
|
||||
update_ui();
|
||||
|
||||
// TODO: fix in UI framework with 'try_focus()'?
|
||||
// Hack around focus getting called by ctor before parent is set.
|
||||
if (parent())
|
||||
button_play.focus();
|
||||
}
|
||||
|
||||
void PlaylistView::open_file(bool prompt_save) {
|
||||
if (playlist_dirty_ && prompt_save) {
|
||||
nav_.display_modal(
|
||||
"Save?", "Save changes?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice)
|
||||
save_file(false);
|
||||
});
|
||||
nav_.set_on_pop([this]() { open_file(false); });
|
||||
return;
|
||||
}
|
||||
|
||||
file_path = new_file_path;
|
||||
field_frequency.set_value(replay_frequency);
|
||||
|
||||
sample_rate = replay_sample_rate;
|
||||
|
||||
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 0) + "Hz");
|
||||
|
||||
auto file_size = data_file.size();
|
||||
auto duration = (file_size * 1000) / (2 * 2 * sample_rate);
|
||||
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(file_path.filename().string().substr(0, 12));
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
auto open_view = nav_.push<FileLoadView>(".PPL");
|
||||
open_view->push_dir(u"PLAYLIST");
|
||||
open_view->on_changed = [this](fs::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
};
|
||||
}
|
||||
|
||||
void PlaylistView::on_tx_progress(const uint32_t progress) {
|
||||
progressbar.set_value(progress);
|
||||
void PlaylistView::save_file(bool show_dialogs) {
|
||||
if (!playlist_dirty_ || playlist_path_.empty())
|
||||
return;
|
||||
|
||||
ensure_directory(playlist_path_.parent_path());
|
||||
|
||||
File playlist_file;
|
||||
auto error = playlist_file.create(playlist_path_.string());
|
||||
|
||||
if (error) {
|
||||
if (show_dialogs)
|
||||
nav_.display_modal(
|
||||
"Save Error",
|
||||
"Could not save file\n" + playlist_path_.string());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : playlist_db_) {
|
||||
playlist_file.write_line(
|
||||
entry.path.string() + "," +
|
||||
to_string_dec_uint(entry.ms_delay));
|
||||
}
|
||||
|
||||
playlist_dirty_ = false;
|
||||
|
||||
if (show_dialogs)
|
||||
nav_.display_modal(
|
||||
"Save",
|
||||
"Saved playlist\n" + playlist_path_.string());
|
||||
}
|
||||
|
||||
void PlaylistView::focus() {
|
||||
button_open.focus();
|
||||
void PlaylistView::add_entry(fs::path&& path) {
|
||||
if (playlist_path_.empty()) {
|
||||
playlist_path_ = next_filename_matching_pattern(u"/PLAYLIST/PLAY_????.PPL");
|
||||
|
||||
// Hack around focus getting called by ctor before parent is set.
|
||||
if (parent())
|
||||
button_play.focus();
|
||||
}
|
||||
|
||||
auto entry = load_entry(std::move(path));
|
||||
if (entry) {
|
||||
playlist_db_.emplace_back(*std::move(entry));
|
||||
current_index_ = playlist_db_.size() - 1;
|
||||
playlist_dirty_ = true;
|
||||
}
|
||||
|
||||
update_ui();
|
||||
}
|
||||
|
||||
void PlaylistView::file_error() {
|
||||
nav_.display_modal("Error", "File " + file_path.string() + " read error. " + file_path.string());
|
||||
void PlaylistView::delete_entry() {
|
||||
if (playlist_db_.empty())
|
||||
return;
|
||||
|
||||
// Ugh, the STL is gross.
|
||||
playlist_db_.erase(playlist_db_.begin() + current_index_);
|
||||
|
||||
if (current_index_ > 0)
|
||||
--current_index_;
|
||||
|
||||
playlist_dirty_ = true;
|
||||
update_ui();
|
||||
}
|
||||
|
||||
void PlaylistView::show_file_error(const fs::path& path, const std::string& message) {
|
||||
nav_.display_modal("Error", "Error opening file \n" + path.string() + "\n" + message);
|
||||
}
|
||||
|
||||
PlaylistView::playlist_entry* PlaylistView::current() {
|
||||
return playlist_db_.empty() ? nullptr : &playlist_db_[current_index_];
|
||||
}
|
||||
|
||||
bool PlaylistView::is_active() const {
|
||||
return (bool)replay_thread;
|
||||
return replay_thread_ != nullptr;
|
||||
}
|
||||
|
||||
bool PlaylistView::loop() const {
|
||||
return (bool)playlist_db.size();
|
||||
bool PlaylistView::at_end() const {
|
||||
return current_index_ >= playlist_db_.size();
|
||||
}
|
||||
|
||||
void PlaylistView::toggle() {
|
||||
if (is_active()) {
|
||||
stop(false);
|
||||
} else {
|
||||
if (is_active())
|
||||
stop();
|
||||
else
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void PlaylistView::start() {
|
||||
stop(false);
|
||||
if (playlist_db_.empty())
|
||||
return; // Nothing to do.
|
||||
|
||||
playlist_entry item = playlist_db.front();
|
||||
playlist_db.pop_front();
|
||||
// playlist_entry item = playlist_db[0];
|
||||
// for (playlist_entry item : playlist_db) {
|
||||
// file_path = item.replay_file;
|
||||
// rf::Frequency replay_frequency = strtoll(item.replay_frequency.c_str(),nullptr,10);
|
||||
on_file_changed(item.replay_file, item.replay_frequency, item.sample_rate);
|
||||
on_target_frequency_changed(item.replay_frequency);
|
||||
|
||||
std::unique_ptr<stream::Reader> reader;
|
||||
|
||||
auto p = std::make_unique<FileReader>();
|
||||
auto open_error = p->open(file_path);
|
||||
if (open_error.is_valid()) {
|
||||
file_error();
|
||||
return; // Fixes TX bug if there's a file error
|
||||
} else {
|
||||
reader = std::move(p);
|
||||
}
|
||||
|
||||
if (reader) {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
baseband::set_sample_rate(sample_rate * 8);
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
read_size, buffer_count,
|
||||
&ready_signal,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message{return_code};
|
||||
EventDispatcher::send_message(message);
|
||||
});
|
||||
}
|
||||
|
||||
// Enable Bias Tee if selected
|
||||
radio::set_antenna_bias(portapack::get_antenna_bias());
|
||||
|
||||
rf_amp = (transmitter_model.rf_amp()); // recover rf_amp settings applied from ui_transmiter.cpp
|
||||
|
||||
radio::enable({receiver_model.tuning_frequency(),
|
||||
sample_rate * 8,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())});
|
||||
|
||||
// }
|
||||
current_index_ = 0u;
|
||||
send_current_track();
|
||||
}
|
||||
|
||||
void PlaylistView::stop(const bool do_loop) {
|
||||
if (is_active()) {
|
||||
replay_thread.reset();
|
||||
}
|
||||
if (do_loop) {
|
||||
if (playlist_db.size() > 0) {
|
||||
start();
|
||||
} else {
|
||||
playlist_db = playlist_masterdb;
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
radio::set_antenna_bias(false); // Turn off Bias Tee
|
||||
radio::disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
/* Advance to the next track in the playlist. */
|
||||
bool PlaylistView::next_track() {
|
||||
++current_index_;
|
||||
|
||||
// Reset to the 0 once at the end to prevent current_index_
|
||||
// from being outside the bounds of playlist_db_ when painting.
|
||||
if (at_end()) {
|
||||
current_index_ = 0;
|
||||
return check_loop.value(); // Keep going if looping.
|
||||
}
|
||||
|
||||
ready_signal = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaylistView::handle_replay_thread_done(const uint32_t return_code) {
|
||||
/* Transmits the current_entry_ */
|
||||
void PlaylistView::send_current_track() {
|
||||
// Prepare to send a file.
|
||||
replay_thread_.reset();
|
||||
transmitter_model.disable();
|
||||
ready_signal_ = false;
|
||||
|
||||
if (!current())
|
||||
return;
|
||||
|
||||
// TODO: Use a timer so the UI isn't frozen.
|
||||
if (current()->ms_delay > 0)
|
||||
chThdSleepMilliseconds(current()->ms_delay);
|
||||
|
||||
// Open the sample file to send.
|
||||
auto reader = std::make_unique<FileConvertReader>();
|
||||
auto error = reader->open(current()->path);
|
||||
if (error) {
|
||||
show_file_error(current()->path, "Can't open file to send.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the sample rate in proc_replay baseband.
|
||||
baseband::set_sample_rate(current()->metadata.sample_rate,
|
||||
get_oversample_rate(current()->metadata.sample_rate));
|
||||
|
||||
// ReplayThread starts immediately on construction; must be set before creating.
|
||||
transmitter_model.set_target_frequency(current()->metadata.center_frequency);
|
||||
transmitter_model.set_sampling_rate(get_actual_sample_rate(current()->metadata.sample_rate));
|
||||
transmitter_model.set_baseband_bandwidth(current()->metadata.sample_rate <= 500'000 ? 1'750'000 : 2'500'000); // TX LPF min 1M75 for SR <=500K, and 2M5 (by experimental test) for SR >500K
|
||||
transmitter_model.enable();
|
||||
|
||||
// Reset the transmit progress bar.
|
||||
progressbar_transmit.set_value(0);
|
||||
|
||||
// Use the ReplayThread class to send the data.
|
||||
replay_thread_ = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
/* read_size */ 0x4000,
|
||||
/* buffer_count */ 3,
|
||||
&ready_signal_,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message{return_code};
|
||||
EventDispatcher::send_message(message);
|
||||
});
|
||||
|
||||
// Now it's sending, update the UI.
|
||||
update_ui();
|
||||
}
|
||||
|
||||
void PlaylistView::stop() {
|
||||
// This terminates the underlying chThread.
|
||||
replay_thread_.reset();
|
||||
transmitter_model.disable();
|
||||
|
||||
// Reset the transmit progress bar.
|
||||
progressbar_transmit.set_value(0);
|
||||
update_ui();
|
||||
}
|
||||
|
||||
void PlaylistView::update_ui() {
|
||||
if (playlist_db_.empty()) {
|
||||
text_filename.set("-");
|
||||
text_sample_rate.set("-");
|
||||
text_duration.set("-");
|
||||
|
||||
if (playlist_path_.empty())
|
||||
text_track.set("Open playlist or add capture.");
|
||||
else
|
||||
text_track.set(playlist_path_.filename().string());
|
||||
|
||||
progressbar_track.set_value(0);
|
||||
progressbar_track.set_max(0);
|
||||
progressbar_transmit.set_max(0);
|
||||
|
||||
} else {
|
||||
chDbgAssert(!at_end(), "update_ui #1", "current_index_ invalid");
|
||||
|
||||
text_filename.set(current()->path.filename().string());
|
||||
text_sample_rate.set(unit_auto_scale(current()->metadata.sample_rate, 3, (current()->metadata.sample_rate > 1000000) ? 2 : 0) + "Hz");
|
||||
|
||||
uint8_t sample_size = capture_file_sample_size(current()->path);
|
||||
auto duration = ms_duration(current()->file_size, current()->metadata.sample_rate, sample_size);
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
field_frequency.set_value(current()->metadata.center_frequency);
|
||||
|
||||
text_track.set(
|
||||
to_string_dec_uint(current_index_ + 1) + "/" +
|
||||
to_string_dec_uint(playlist_db_.size()) + " " +
|
||||
playlist_path_.filename().string());
|
||||
|
||||
progressbar_track.set_max(playlist_db_.size() - 1);
|
||||
progressbar_track.set_value(current_index_);
|
||||
progressbar_transmit.set_max(current()->file_size * sizeof(complex16_t) / sample_size);
|
||||
}
|
||||
|
||||
button_play.set_bitmap(is_active() ? &bitmap_stop : &bitmap_play);
|
||||
}
|
||||
|
||||
void PlaylistView::on_tx_progress(uint32_t progress) {
|
||||
progressbar_transmit.set_value(progress);
|
||||
}
|
||||
|
||||
void PlaylistView::handle_replay_thread_done(uint32_t return_code) {
|
||||
if (return_code == ReplayThread::END_OF_FILE) {
|
||||
stop(true);
|
||||
} else if (return_code == ReplayThread::READ_ERROR) {
|
||||
stop(false);
|
||||
file_error();
|
||||
if (next_track()) {
|
||||
send_current_track();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
progressbar.set_value(0);
|
||||
if (return_code == ReplayThread::READ_ERROR)
|
||||
show_file_error(current()->path, "Replay read failed.");
|
||||
|
||||
stop();
|
||||
}
|
||||
|
||||
PlaylistView::PlaylistView(
|
||||
@@ -231,77 +367,129 @@ PlaylistView::PlaylistView(
|
||||
baseband::run_image(portapack::spi_flash::image_tag_replay);
|
||||
|
||||
add_children({
|
||||
&button_open,
|
||||
&text_filename,
|
||||
&text_sample_rate,
|
||||
&text_duration,
|
||||
&progressbar,
|
||||
&progressbar_track,
|
||||
&progressbar_transmit,
|
||||
&field_frequency,
|
||||
&tx_view, // this handles now the previous rfgain, rfamp
|
||||
&tx_view,
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&text_track,
|
||||
&button_prev,
|
||||
&button_add,
|
||||
&button_delete,
|
||||
&button_open,
|
||||
&button_save,
|
||||
&button_next,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
field_frequency.set_value(target_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
ensure_directory(u"PLAYLIST");
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
|
||||
field_frequency.set_value(100'000'000);
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
if (current())
|
||||
current()->metadata.center_frequency = f;
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(this->target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
field_frequency.on_edit = [this]() {
|
||||
auto freq_view = nav_.push<FrequencyKeypadView>(field_frequency.value());
|
||||
freq_view->on_changed = [this](rf::Frequency f) {
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency.set_step(5000);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
toggle();
|
||||
};
|
||||
|
||||
button_open.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
load_file(new_file_path);
|
||||
button_add.on_select = [this, &nav]() {
|
||||
if (is_active())
|
||||
return;
|
||||
auto open_view = nav_.push<FileLoadView>(".C*");
|
||||
open_view->push_dir(u"CAPTURES");
|
||||
open_view->on_changed = [this](fs::path path) {
|
||||
add_entry(std::move(path));
|
||||
};
|
||||
};
|
||||
|
||||
button_delete.on_select = [this, &nav]() {
|
||||
if (is_active())
|
||||
return;
|
||||
delete_entry();
|
||||
};
|
||||
|
||||
button_open.on_select = [this, &nav]() {
|
||||
if (is_active())
|
||||
stop();
|
||||
open_file();
|
||||
};
|
||||
|
||||
button_save.on_select = [this]() {
|
||||
if (is_active())
|
||||
stop();
|
||||
save_file();
|
||||
};
|
||||
|
||||
button_prev.on_select = [this]() {
|
||||
if (is_active())
|
||||
return;
|
||||
--current_index_;
|
||||
if (at_end())
|
||||
current_index_ = playlist_db_.size() - 1;
|
||||
update_ui();
|
||||
};
|
||||
|
||||
button_next.on_select = [this]() {
|
||||
if (is_active())
|
||||
return;
|
||||
++current_index_;
|
||||
if (at_end())
|
||||
current_index_ = 0;
|
||||
update_ui();
|
||||
};
|
||||
|
||||
update_ui();
|
||||
}
|
||||
|
||||
PlaylistView::PlaylistView(
|
||||
NavigationView& nav,
|
||||
const fs::path& path)
|
||||
: PlaylistView(nav) {
|
||||
auto ext = path.extension();
|
||||
if (path_iequal(ext, ppl_ext))
|
||||
on_file_changed(path);
|
||||
else if (is_cxx_capture_file(path))
|
||||
add_entry(fs::path{path});
|
||||
}
|
||||
|
||||
PlaylistView::~PlaylistView() {
|
||||
radio::disable();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void PlaylistView::set_parent_rect(Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
ui::Rect waterfall_rect{
|
||||
0, header_height, new_parent_rect.width(),
|
||||
new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void PlaylistView::focus() {
|
||||
if (playlist_path_.empty())
|
||||
button_add.focus();
|
||||
else
|
||||
button_play.focus();
|
||||
}
|
||||
|
||||
void PlaylistView::on_hide() {
|
||||
stop(false);
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
stop();
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
|
||||
void PlaylistView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void PlaylistView::on_target_frequency_changed(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
}
|
||||
|
||||
void PlaylistView::set_target_frequency(const rf::Frequency new_value) {
|
||||
persistent_memory::set_tuned_frequency(new_value);
|
||||
;
|
||||
}
|
||||
|
||||
rf::Frequency PlaylistView::target_frequency() const {
|
||||
return persistent_memory::tuned_frequency();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed, zxkmm
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -20,135 +21,190 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "bitmap.hpp"
|
||||
#include "file.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <deque>
|
||||
#include "ui_transmitter.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace ui {
|
||||
|
||||
class PlaylistView : public View {
|
||||
public:
|
||||
PlaylistView(NavigationView& nav);
|
||||
PlaylistView(NavigationView& nav, const std::filesystem::path& path);
|
||||
~PlaylistView();
|
||||
|
||||
void on_hide() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
// Following 2 called by 'NavigationView::update_view' after view is created.
|
||||
void set_parent_rect(Rect new_parent_rect) override;
|
||||
void focus() override;
|
||||
void on_hide() override;
|
||||
|
||||
std::string title() const override { return "Playlist"; };
|
||||
std::string title() const override { return "Replay"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_playlist", app_settings::Mode::TX};
|
||||
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
// More header == less spectrum view.
|
||||
static constexpr ui::Dim header_height = 6 * 16;
|
||||
|
||||
struct playlist_entry {
|
||||
rf::Frequency replay_frequency{0};
|
||||
std::string replay_file{};
|
||||
uint32_t sample_rate{};
|
||||
uint32_t next_delay{};
|
||||
std::filesystem::path path{};
|
||||
capture_metadata metadata{};
|
||||
File::Size file_size{};
|
||||
uint32_t ms_delay{};
|
||||
};
|
||||
std::deque<playlist_entry> playlist_db{};
|
||||
std::deque<playlist_entry> playlist_masterdb{};
|
||||
uint32_t sample_rate = 0;
|
||||
int32_t tx_gain{47};
|
||||
bool rf_amp{true}; // aux private var to store temporal, Replay App rf_amp user selection.
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
const size_t read_size{16384};
|
||||
const size_t buffer_count{3};
|
||||
void load_file(std::filesystem::path playlist_path);
|
||||
void txtline_process(std::string&);
|
||||
void on_file_changed(std::filesystem::path new_file_path, rf::Frequency replay_frequency, uint32_t replay_sample_rate);
|
||||
void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
void set_target_frequency(const rf::Frequency new_value);
|
||||
rf::Frequency target_frequency() const;
|
||||
std::unique_ptr<ReplayThread> replay_thread_{};
|
||||
bool ready_signal_{}; // Used to signal the ReplayThread.
|
||||
|
||||
size_t current_index_{0};
|
||||
bool playlist_dirty_{};
|
||||
std::vector<playlist_entry> playlist_db_{};
|
||||
std::filesystem::path playlist_path_{};
|
||||
|
||||
void load_file(const std::filesystem::path& path);
|
||||
Optional<playlist_entry> load_entry(std::filesystem::path&& path);
|
||||
void on_file_changed(const std::filesystem::path& path);
|
||||
void open_file(bool prompt_save = true);
|
||||
void save_file(bool show_dialogs = true);
|
||||
void add_entry(std::filesystem::path&& path);
|
||||
void delete_entry();
|
||||
void reset();
|
||||
void show_file_error(
|
||||
const std::filesystem::path& path,
|
||||
const std::string& message);
|
||||
|
||||
playlist_entry* current();
|
||||
|
||||
bool is_active() const;
|
||||
bool at_end() const;
|
||||
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
bool is_active() const;
|
||||
bool loop() const;
|
||||
void set_ready();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
bool next_track();
|
||||
void send_current_track();
|
||||
void stop();
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
std::unique_ptr<ReplayThread> replay_thread{};
|
||||
bool ready_signal{false};
|
||||
void update_ui();
|
||||
|
||||
Button button_open{
|
||||
{0 * 8, 0 * 16, 10 * 8, 2 * 16},
|
||||
"Open file"};
|
||||
/* There are called by Message handlers. */
|
||||
void on_tx_progress(uint32_t progress);
|
||||
void handle_replay_thread_done(uint32_t return_code);
|
||||
|
||||
Text text_filename{
|
||||
{11 * 8, 0 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
Text text_sample_rate{
|
||||
{24 * 8, 0 * 16, 6 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_duration{
|
||||
{11 * 8, 1 * 16, 6 * 8, 16},
|
||||
"-"};
|
||||
ProgressBar progressbar{
|
||||
{18 * 8, 1 * 16, 12 * 8, 16}};
|
||||
{0 * 8, 0 * 16, 30 * 8, 16}};
|
||||
|
||||
FrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
};
|
||||
{0 * 8, 1 * 16}};
|
||||
|
||||
Text text_sample_rate{
|
||||
{10 * 8, 1 * 16, 7 * 8, 16}};
|
||||
|
||||
ProgressBar progressbar_track{
|
||||
{18 * 8, 1 * 16, 12 * 8, 8 + 1}};
|
||||
|
||||
// (-1) to overlap with progressbar_track so there's
|
||||
// only 1 pixel between them instead of 2.
|
||||
ProgressBar progressbar_transmit{
|
||||
{18 * 8, 3 * 8 - 1, 12 * 8, 8}};
|
||||
|
||||
Text text_duration{
|
||||
{0 * 8, 2 * 16, 5 * 8, 16}};
|
||||
|
||||
// TODO: delay duration field.
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
||||
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Used in Repay / GPS Simul / Play list App)
|
||||
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
|
||||
};
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
4,
|
||||
"Loop",
|
||||
true};
|
||||
|
||||
ImageButton button_play{
|
||||
{28 * 8, 2 * 16, 2 * 8, 1 * 16},
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
|
||||
spectrum::WaterfallWidget waterfall{};
|
||||
Text text_track{
|
||||
{0 * 8, 3 * 16, 30 * 8, 16}};
|
||||
|
||||
NewButton button_prev{
|
||||
{2 * 8, 4 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_arrow_left,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_next{
|
||||
{6 * 8, 4 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_arrow_right,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_add{
|
||||
{11 * 8, 4 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_icon_new_file,
|
||||
Color::orange()};
|
||||
|
||||
NewButton button_delete{
|
||||
{15 * 8, 4 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_icon_delete,
|
||||
Color::orange()};
|
||||
|
||||
NewButton button_open{
|
||||
{20 * 8, 4 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_icon_load,
|
||||
Color::dark_blue()};
|
||||
|
||||
NewButton button_save{
|
||||
{24 * 8, 4 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_icon_save,
|
||||
Color::dark_blue()};
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error{
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
this->handle_replay_thread_done(message.return_code);
|
||||
[this](const Message* p) {
|
||||
auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
handle_replay_thread_done(message.return_code);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal{
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
[this](const Message* p) {
|
||||
auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
this->set_ready();
|
||||
ready_signal_ = true;
|
||||
}
|
||||
}};
|
||||
|
||||
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);
|
||||
[this](const Message* p) {
|
||||
auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
on_tx_progress(message.progress);
|
||||
}};
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "ui_pocsag_tx.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
@@ -39,13 +38,8 @@ void POCSAGTXView::focus() {
|
||||
}
|
||||
|
||||
POCSAGTXView::~POCSAGTXView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_pocsag", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void POCSAGTXView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
@@ -62,7 +56,7 @@ bool POCSAGTXView::start_tx() {
|
||||
pocsag::BitRate bitrate;
|
||||
std::vector<uint32_t> codewords;
|
||||
|
||||
address = field_address.value_dec_u32();
|
||||
address = field_address.to_integer();
|
||||
if (address > 0x1FFFFFU) {
|
||||
nav_.display_modal("Bad address", "Address must be less\nthan 2097152.");
|
||||
return false;
|
||||
@@ -85,11 +79,10 @@ bool POCSAGTXView::start_tx() {
|
||||
|
||||
progressbar.set_max(total_frames);
|
||||
|
||||
transmitter_model.set_sampling_rate(2280000);
|
||||
transmitter_model.set_rf_amp(true);
|
||||
transmitter_model.set_lna(40);
|
||||
transmitter_model.set_vga(40);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.set_tx_gain(40);
|
||||
// We left exactly same TX LPF settings as previous fw 1.7.4, TX LPF= 1M75 (min). It is fine even in max FM dev. 150khz and 2400 bauds.
|
||||
transmitter_model.set_baseband_bandwidth(1'750'000); // Min TX LPF . Pocsag is NBFM , using BW channel 25khz or 12khz
|
||||
transmitter_model.enable();
|
||||
|
||||
uint8_t* data_ptr = shared_memory.bb_data.data;
|
||||
@@ -143,24 +136,10 @@ POCSAGTXView::POCSAGTXView(
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_pocsag", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
options_bitrate.set_selected_index(1); // 1200bps
|
||||
options_type.set_selected_index(0); // Address only
|
||||
|
||||
// TODO: set_value for whole symfield
|
||||
uint32_t reload_address = persistent_memory::pocsag_last_address();
|
||||
for (uint32_t c = 0; c < 7; c++) {
|
||||
field_address.set_sym(6 - c, reload_address % 10);
|
||||
reload_address /= 10;
|
||||
}
|
||||
field_address.set_value(persistent_memory::pocsag_last_address());
|
||||
|
||||
options_type.on_change = [this](size_t, int32_t i) {
|
||||
if (i == 2)
|
||||
@@ -172,9 +151,9 @@ POCSAGTXView::POCSAGTXView(
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "message.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "pocsag.hpp"
|
||||
|
||||
using namespace pocsag;
|
||||
@@ -58,6 +59,14 @@ class POCSAGTXView : public View {
|
||||
std::string message{};
|
||||
NavigationView& nav_;
|
||||
|
||||
TxRadioState radio_state_{
|
||||
466175000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
2280000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_pocsag", app_settings::Mode::TX};
|
||||
|
||||
BCHCode BCH_code{
|
||||
{1, 0, 1, 0, 0, 1},
|
||||
5,
|
||||
@@ -65,10 +74,6 @@ class POCSAGTXView : public View {
|
||||
21,
|
||||
2};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
|
||||
void on_set_text(NavigationView& nav);
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
bool start_tx();
|
||||
@@ -90,8 +95,7 @@ class POCSAGTXView : public View {
|
||||
|
||||
SymField field_address{
|
||||
{11 * 8, 6 * 8},
|
||||
7,
|
||||
SymField::SYMFIELD_DEC};
|
||||
7};
|
||||
|
||||
OptionsField options_type{
|
||||
{11 * 8, 8 * 8},
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include <cstring>
|
||||
@@ -160,17 +159,12 @@ void RDSView::focus() {
|
||||
}
|
||||
|
||||
RDSView::~RDSView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_rds", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit.
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void RDSView::start_tx() {
|
||||
rds_flags.PI_code = sym_pi_code.value_hex_u64();
|
||||
rds_flags.PI_code = sym_pi_code.to_integer();
|
||||
rds_flags.PTY = options_pty.selected_index_value();
|
||||
rds_flags.DI = view_PSN.mono_stereo ? 1 : 0;
|
||||
rds_flags.TP = check_TP.value();
|
||||
@@ -193,8 +187,6 @@ void RDSView::start_tx() {
|
||||
else
|
||||
frame_datetime.clear();
|
||||
|
||||
transmitter_model.set_sampling_rate(2280000U);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
tx_thread = std::make_unique<RDSThread>(frames);
|
||||
@@ -218,30 +210,19 @@ RDSView::RDSView(
|
||||
&tx_view,
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_rds", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
check_TP.set_value(true);
|
||||
|
||||
sym_pi_code.set_sym(0, 0xF);
|
||||
sym_pi_code.set_sym(1, 0x3);
|
||||
sym_pi_code.set_sym(2, 0xE);
|
||||
sym_pi_code.set_sym(3, 0x0);
|
||||
sym_pi_code.on_change = [this]() {
|
||||
rds_flags.PI_code = sym_pi_code.value_hex_u64();
|
||||
sym_pi_code.set_value(0xF3E0);
|
||||
sym_pi_code.on_change = [this](SymField&) {
|
||||
rds_flags.PI_code = sym_pi_code.to_integer();
|
||||
};
|
||||
|
||||
options_pty.set_selected_index(0); // None
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "ui_textentry.hpp"
|
||||
#include "ui_tabview.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "rds.hpp"
|
||||
|
||||
using namespace rds;
|
||||
@@ -140,9 +141,13 @@ class RDSView : public View {
|
||||
NavigationView& nav_;
|
||||
RDS_flags rds_flags{};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
2280000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_rds", app_settings::Mode::TX};
|
||||
|
||||
std::vector<RDSGroup> frame_psn{};
|
||||
std::vector<RDSGroup> frame_radiotext{};
|
||||
@@ -283,7 +288,7 @@ class RDSView : public View {
|
||||
SymField sym_pi_code{
|
||||
{13 * 8, 28 + 16},
|
||||
4,
|
||||
SymField::SYMFIELD_HEX};
|
||||
SymField::Type::Hex};
|
||||
|
||||
/*OptionsField options_coverage {
|
||||
{ 17 * 8, 32 + 8 },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -26,7 +27,7 @@
|
||||
#include "ui.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "audio.hpp"
|
||||
@@ -38,10 +39,29 @@
|
||||
#include "string_format.hpp"
|
||||
#include "file.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_recon_settings.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace ui {
|
||||
|
||||
#define RECON_CFG_FILE u"SETTINGS/recon.cfg"
|
||||
|
||||
enum class recon_mode : uint8_t {
|
||||
Recon,
|
||||
Scanner,
|
||||
Manual
|
||||
};
|
||||
|
||||
class ReconView : public View {
|
||||
public:
|
||||
ReconView(NavigationView& nav);
|
||||
@@ -49,48 +69,6 @@ class ReconView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
const Style style_grey{
|
||||
// recon
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::grey(),
|
||||
};
|
||||
|
||||
const Style style_white{
|
||||
// recon
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
const Style style_yellow{
|
||||
// found signal
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::yellow(),
|
||||
};
|
||||
|
||||
const Style style_green{
|
||||
// Found signal
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::green(),
|
||||
};
|
||||
|
||||
const Style style_red{
|
||||
// erasing freq
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::red(),
|
||||
};
|
||||
|
||||
const Style style_blue{
|
||||
// quick recon, wait == 0
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::blue(),
|
||||
};
|
||||
|
||||
std::string title() const override { return "Recon"; };
|
||||
|
||||
// void set_parent_rect(const Rect new_parent_rect) override;
|
||||
@@ -98,87 +76,153 @@ class ReconView : public View {
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState rx_radio_state_{};
|
||||
TxRadioState tx_radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
500000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_tx_recon"sv, app_settings::Mode::RX_TX};
|
||||
;
|
||||
|
||||
void reload_restart_recon();
|
||||
void check_update_ranges_from_current();
|
||||
void set_loop_config(bool v);
|
||||
void clear_freqlist_for_ui_action();
|
||||
void reset_indexes();
|
||||
void update_description();
|
||||
void audio_output_start();
|
||||
bool check_sd_card();
|
||||
size_t change_mode(freqman_index_t mod_type);
|
||||
void show_max(bool refresh_display = false);
|
||||
void recon_pause();
|
||||
void recon_resume();
|
||||
void frequency_file_load(bool stop_all_before = false);
|
||||
void frequency_file_load();
|
||||
void on_statistics_update(const ChannelStatistics& statistics);
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
void on_index_delta(int32_t v);
|
||||
void on_stepper_delta(int32_t v);
|
||||
void colorize_waits();
|
||||
void recon_redraw();
|
||||
void handle_retune();
|
||||
void handle_coded_squelch(const uint32_t value);
|
||||
bool ReconSetupLoadStrings(const std::string& source, std::string& input_file, std::string& output_file, uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, int32_t& recon_squelch_level, uint32_t& recon_match_mode, int32_t& wait, int32_t& volume);
|
||||
bool ReconSetupSaveStrings(const std::string& dest, const std::string& input_file, const std::string& output_file, uint32_t recon_lock_duration, uint32_t recon_lock_nb_match, int32_t recon_squelch_level, uint32_t recon_match_mode, int32_t wait, int32_t volume);
|
||||
bool ReconSaveFreq(const std::string& freq_file_path, size_t index, bool warn_if_exists);
|
||||
void handle_remove_current_item();
|
||||
void load_persisted_settings();
|
||||
bool recon_save_freq(const std::filesystem::path& path, size_t index, bool warn_if_exists);
|
||||
// placeholder for possible void recon_start_recording();
|
||||
void recon_stop_recording(bool exiting);
|
||||
|
||||
// Returns true if 'current_index' is in bounds of frequency_list.
|
||||
bool current_is_valid();
|
||||
freqman_entry& current_entry();
|
||||
|
||||
// TODO: consolidate mode bools and use recon_mode.
|
||||
recon_mode mode() const {
|
||||
if (manual_mode) return recon_mode::Manual;
|
||||
if (scanner_mode) return recon_mode::Scanner;
|
||||
return recon_mode::Recon;
|
||||
}
|
||||
|
||||
jammer::jammer_range_t frequency_range{false, 0, MAX_UFREQ}; // perfect for manual recon task too...
|
||||
int32_t squelch{0};
|
||||
int32_t squelch{RECON_DEF_SQUELCH};
|
||||
int32_t db{0};
|
||||
int32_t timer{0};
|
||||
int32_t wait{RECON_DEF_WAIT_DURATION}; // in msec. if > 0 wait duration after a lock, if < 0 duration is set to 'wait' unless there is no more activity
|
||||
freqman_db frequency_list = {};
|
||||
freqman_db frequency_list{};
|
||||
int32_t current_index{0};
|
||||
bool userpause{false};
|
||||
bool continuous_lock{false};
|
||||
std::string input_file = {"RECON"};
|
||||
std::string output_file = {"RECON_RESULTS"};
|
||||
bool autosave = {true};
|
||||
bool autostart = {true};
|
||||
bool continuous = {true};
|
||||
bool filedelete = {true};
|
||||
bool load_freqs = {true};
|
||||
bool load_ranges = {true};
|
||||
bool load_hamradios = {true};
|
||||
bool update_ranges = {true};
|
||||
bool fwd = {true};
|
||||
bool recon = true;
|
||||
uint32_t recon_lock_nb_match = {3};
|
||||
uint32_t recon_lock_duration = {RECON_MIN_LOCK_DURATION};
|
||||
uint32_t recon_match_mode = {RECON_MATCH_CONTINUOUS};
|
||||
bool freqlist_cleared_for_ui_action{false}; // flag positioned by ui widgets to manage freqlist unload/load
|
||||
std::string input_file{"RECON"};
|
||||
std::string output_file{"RECON_RESULTS"};
|
||||
std::string description{"...no description..."};
|
||||
bool autosave{true};
|
||||
bool autostart{true};
|
||||
bool continuous{true};
|
||||
bool filedelete{true};
|
||||
bool load_freqs{true};
|
||||
bool load_ranges{true};
|
||||
bool load_hamradios{true};
|
||||
bool load_repeaters{true};
|
||||
bool update_ranges{true};
|
||||
bool fwd{true};
|
||||
bool recon{true};
|
||||
bool user_pause{false};
|
||||
bool auto_record_locked{false};
|
||||
bool is_recording{false};
|
||||
uint32_t recon_lock_nb_match{RECON_DEF_NB_MATCH};
|
||||
uint32_t recon_lock_duration{RECON_MIN_LOCK_DURATION};
|
||||
uint32_t recon_match_mode{RECON_MATCH_CONTINUOUS};
|
||||
bool scanner_mode{false};
|
||||
bool manual_mode{false};
|
||||
bool sd_card_mounted = false;
|
||||
int32_t volume = 40;
|
||||
int32_t stepper = 0;
|
||||
int32_t index_stepper = 0;
|
||||
int64_t freq = 0;
|
||||
uint32_t step = 0;
|
||||
freqman_index_t def_modulation = 0;
|
||||
freqman_index_t def_bandwidth = 0;
|
||||
freqman_index_t def_step = 0;
|
||||
tone_index tone = 0;
|
||||
freqman_entry last_entry = {};
|
||||
bool entry_has_changed = false;
|
||||
bool sd_card_mounted{false};
|
||||
int32_t stepper{0};
|
||||
int32_t index_stepper{0};
|
||||
int64_t freq{0};
|
||||
uint32_t step{0};
|
||||
freqman_index_t def_step{0};
|
||||
freqman_entry last_entry{};
|
||||
bool entry_has_changed{false};
|
||||
uint32_t freq_lock{0};
|
||||
int64_t minfreq = 0;
|
||||
int64_t maxfreq = 0;
|
||||
bool has_looped = false;
|
||||
int8_t status = -1; // 0 recon , 1 locking , 2 locked
|
||||
int32_t last_timer = -1;
|
||||
int8_t last_db = -127;
|
||||
uint16_t last_nb_match = 999;
|
||||
uint16_t last_freq_lock = 999;
|
||||
size_t last_list_size = 0;
|
||||
int8_t last_rssi_min = -127;
|
||||
int8_t last_rssi_med = -127;
|
||||
int8_t last_rssi_max = -127;
|
||||
int32_t last_index = -1;
|
||||
int32_t last_squelch_index = -1;
|
||||
int64_t last_freq = 0;
|
||||
std::string freq_file_path = {};
|
||||
int64_t minfreq{0};
|
||||
int64_t maxfreq{0};
|
||||
bool has_looped{false};
|
||||
int8_t status{-1}; // 0 recon , 1 locking , 2 locked
|
||||
int32_t last_timer{-1};
|
||||
int8_t last_db{-127};
|
||||
uint16_t last_nb_match{999};
|
||||
uint16_t last_freq_lock{999};
|
||||
size_t last_list_size{0};
|
||||
int8_t last_rssi_min{127};
|
||||
int8_t last_rssi_med{0};
|
||||
int8_t last_rssi_max{-127};
|
||||
int32_t last_index{-1};
|
||||
int64_t last_freq{0};
|
||||
std::string freq_file_path{};
|
||||
systime_t chrono_start{};
|
||||
systime_t chrono_end{};
|
||||
|
||||
const std::filesystem::path repeat_rec_file = u"RECON_REPEAT.C16";
|
||||
const std::filesystem::path repeat_rec_meta = u"RECON_REPEAT.TXT";
|
||||
const std::filesystem::path repeat_rec_path = u"CAPTURES";
|
||||
const size_t repeat_read_size{16384};
|
||||
const size_t repeat_buffer_count{3};
|
||||
int8_t repeat_cur_rep = 0;
|
||||
int64_t repeat_sample_rate = 0;
|
||||
static constexpr uint32_t repeat_bandwidth = 2500000;
|
||||
void on_repeat_tx_progress(const uint32_t progress);
|
||||
void start_repeat();
|
||||
void stop_repeat(const bool do_loop);
|
||||
bool is_repeat_active() const;
|
||||
void handle_repeat_thread_done(const uint32_t return_code);
|
||||
void repeat_file_error(const std::filesystem::path& path, const std::string& message);
|
||||
std::filesystem::path repeat_file_path{};
|
||||
std::unique_ptr<ReplayThread> replay_thread{};
|
||||
bool repeat_ready_signal{false};
|
||||
bool recon_tx{false};
|
||||
|
||||
// Persisted settings.
|
||||
SettingsStore ui_settings{
|
||||
"recon"sv,
|
||||
{
|
||||
{"input_file"sv, &input_file},
|
||||
{"output_file"sv, &output_file},
|
||||
{"lock_duration"sv, &recon_lock_duration},
|
||||
{"lock_nb_match"sv, &recon_lock_nb_match},
|
||||
{"squelch_level"sv, &squelch},
|
||||
{"match_mode"sv, &recon_match_mode},
|
||||
{"match_wait"sv, &wait},
|
||||
{"range_min"sv, &frequency_range.min},
|
||||
{"range_max"sv, &frequency_range.max},
|
||||
}};
|
||||
|
||||
std::unique_ptr<RecordView> record_view{};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "LNA: VGA: AMP: VOL: ", Color::light_grey()},
|
||||
{{0 * 8, 1 * 16}, "BW: SQ: W,L: , ", Color::light_grey()},
|
||||
{{3 * 8, 10 * 16}, "START END MANUAL", Color::light_grey()},
|
||||
{{0 * 8, (26 * 8) + 4}, "MODE:", Color::light_grey()},
|
||||
{{11 * 8, (26 * 8) + 4}, "STEP:", Color::light_grey()},
|
||||
};
|
||||
{{3 * 8, 8 * 16}, "START END", Color::light_grey()},
|
||||
{{0 * 8, (22 * 8)}, " S: ", Color::light_grey()},
|
||||
{{0 * 8, (24 * 8) + 4}, "NBLCKS:x W,L: , ", Color::light_grey()},
|
||||
{{0 * 8, (26 * 8) + 4}, "MODE: , SQUELCH: ", Color::light_grey()}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{4 * 8, 0 * 16}};
|
||||
@@ -189,67 +233,44 @@ class ReconView : public View {
|
||||
RFAmpField field_rf_amp{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
NumberField field_volume{
|
||||
{24 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
AudioVolumeField field_volume{
|
||||
{24 * 8, 0 * 16}};
|
||||
|
||||
OptionsField field_bw{
|
||||
{3 * 8, 1 * 16},
|
||||
6,
|
||||
{}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{12 * 8, 1 * 16},
|
||||
3,
|
||||
{-90, 20},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
NumberField field_wait{
|
||||
{20 * 8, 1 * 16},
|
||||
5,
|
||||
{-RECON_MAX_LOCK_DURATION, RECON_MAX_LOCK_DURATION},
|
||||
STATS_UPDATE_INTERVAL,
|
||||
' ',
|
||||
};
|
||||
|
||||
NumberField field_lock_wait{
|
||||
{26 * 8, 1 * 16},
|
||||
4,
|
||||
{RECON_MIN_LOCK_DURATION, RECON_MAX_LOCK_DURATION},
|
||||
STATS_UPDATE_INTERVAL,
|
||||
' ',
|
||||
};
|
||||
|
||||
RSSI rssi{
|
||||
{0 * 16, 2 * 16, SCREEN_W - 8 * 8 + 4, 16},
|
||||
};
|
||||
|
||||
ButtonWithEncoder text_cycle{
|
||||
{0, 3 * 16, 4 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_max{
|
||||
{4 * 8, 3 * 16, SCREEN_W - 7 * 8 - 4 * 8, 16},
|
||||
Text file_name{
|
||||
// show file used
|
||||
{0, 1 * 16, SCREEN_W, 16},
|
||||
};
|
||||
|
||||
Text desc_cycle{
|
||||
{0, 4 * 16, SCREEN_W, 16},
|
||||
{0, 2 * 16, SCREEN_W, 16},
|
||||
};
|
||||
|
||||
RSSI rssi{
|
||||
{0 * 16, 3 * 16 + 2, SCREEN_W - 8 * 8 + 4, 12},
|
||||
};
|
||||
|
||||
ButtonWithEncoder text_cycle{
|
||||
{0, 4 * 16, 4 * 8, 16},
|
||||
""};
|
||||
|
||||
// "/XXX -XXX db" => 12 chars max
|
||||
Text text_max{
|
||||
{4 * 8, 4 * 16, 12 * 8, 16},
|
||||
};
|
||||
|
||||
// "XX/XX" => 5 chars max
|
||||
Text text_nb_locks{
|
||||
{16 * 8, 4 * 16, 5 * 8, 16},
|
||||
};
|
||||
|
||||
Text big_display{
|
||||
// Show frequency in text mode
|
||||
{0, 5 * 16, 23 * 8, 16},
|
||||
{0, 5 * 16, 21 * 8, 16},
|
||||
};
|
||||
|
||||
Text freq_stats{
|
||||
// Show frequency stats in text mode
|
||||
{0, 6 * 16, 23 * 8, 16},
|
||||
{0, 6 * 16, 21 * 8, 16},
|
||||
};
|
||||
|
||||
// TIMER: 9999
|
||||
@@ -260,48 +281,87 @@ class ReconView : public View {
|
||||
|
||||
// T: Senn. 32.000k
|
||||
Text text_ctcss{
|
||||
{12 * 8 + 4, 7 * 16, 14 * 8, 1 * 8},
|
||||
{14 * 8, 7 * 16, 8 * 8, 1 * 8},
|
||||
""};
|
||||
|
||||
Button button_recon_setup{
|
||||
{SCREEN_W - 7 * 8, 2 * 16, 7 * 8, 28},
|
||||
"CONFIG"};
|
||||
|
||||
Button button_looking_glass{
|
||||
{SCREEN_W - 7 * 8, 5 * 16, 7 * 8, 28},
|
||||
"GLASS"};
|
||||
|
||||
// Button can be RECON or SCANNER
|
||||
Button button_scanner_mode{
|
||||
{SCREEN_W - 7 * 8, 8 * 16, 7 * 8, 28},
|
||||
{SCREEN_W - 7 * 8, 3 * 16, 7 * 8, 28},
|
||||
"RECON"};
|
||||
|
||||
Text file_name{
|
||||
// show file used
|
||||
{0, 8 * 16 + 6, SCREEN_W - 7 * 8, 16},
|
||||
};
|
||||
Button button_loop_config{
|
||||
{SCREEN_W - 7 * 8, 5 * 16, 7 * 8, 28},
|
||||
"[LOOP]"};
|
||||
|
||||
Button button_config{
|
||||
{SCREEN_W - 7 * 8, 7 * 16, 7 * 8, 28},
|
||||
"CONFIG"};
|
||||
|
||||
ButtonWithEncoder button_manual_start{
|
||||
{0 * 8, 11 * 16, 11 * 8, 28},
|
||||
{0 * 8, 9 * 16, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
ButtonWithEncoder button_manual_end{
|
||||
{12 * 8 - 6, 11 * 16, 11 * 8, 28},
|
||||
{12 * 8 - 6, 9 * 16, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
OptionsField field_recon_match_mode{
|
||||
{0 * 8, 11 * 16},
|
||||
16, // CONTINUOUS MATCH MODE / SPARSE TIMED MATCH MODE
|
||||
{
|
||||
{"MATCH:CONTINOUS", 0},
|
||||
{"MATCH:SPARSE", 1}}};
|
||||
|
||||
OptionsField step_mode{
|
||||
{18 * 8, 11 * 16},
|
||||
12,
|
||||
{}};
|
||||
|
||||
Button button_manual_recon{
|
||||
{23 * 8 - 3, 11 * 16, 7 * 8, 28},
|
||||
{23 * 8, 9 * 16, 7 * 8, 28},
|
||||
"SEARCH"};
|
||||
|
||||
NumberField field_nblocks{
|
||||
{8 * 8, 24 * 8 + 4},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
NumberField field_wait{
|
||||
{19 * 8, 24 * 8 + 4},
|
||||
5,
|
||||
{-RECON_MAX_LOCK_DURATION, RECON_MAX_LOCK_DURATION},
|
||||
STATS_UPDATE_INTERVAL,
|
||||
' ',
|
||||
};
|
||||
|
||||
NumberField field_lock_wait{
|
||||
{25 * 8, 24 * 8 + 4},
|
||||
4,
|
||||
{STATS_UPDATE_INTERVAL, RECON_MAX_LOCK_DURATION},
|
||||
STATS_UPDATE_INTERVAL,
|
||||
' ',
|
||||
};
|
||||
|
||||
OptionsField field_mode{
|
||||
{5 * 8, (26 * 8) + 4},
|
||||
{6 * 8, (26 * 8) + 4},
|
||||
4,
|
||||
{}};
|
||||
|
||||
OptionsField field_bw{
|
||||
{11 * 8, (26 * 8) + 4},
|
||||
6,
|
||||
{}};
|
||||
|
||||
OptionsField step_mode{
|
||||
{17 * 8, (26 * 8) + 4},
|
||||
12,
|
||||
{}};
|
||||
NumberField field_squelch{
|
||||
{26 * 8, (26 * 8) + 4},
|
||||
3,
|
||||
{-90, 20},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
ButtonWithEncoder button_pause{
|
||||
{0, (15 * 16) - 4, 72, 28},
|
||||
@@ -331,21 +391,48 @@ class ReconView : public View {
|
||||
{168, (35 * 8) - 4, 72, 28},
|
||||
"<REMOVE>"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{0 * 8, SCREEN_H / 2 - 16, SCREEN_W, 32}};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
MessageHandlerRegistration message_handler_coded_squelch{
|
||||
Message::ID::CodedSquelch,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const CodedSquelchMessage*>(p);
|
||||
this->handle_coded_squelch(message.value);
|
||||
handle_coded_squelch(message.value);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_stats{
|
||||
Message::ID::ChannelStatistics,
|
||||
[this](const Message* const p) {
|
||||
this->on_statistics_update(static_cast<const ChannelStatisticsMessage*>(p)->statistics);
|
||||
on_statistics_update(static_cast<const ChannelStatisticsMessage*>(p)->statistics);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error{
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* p) {
|
||||
auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
handle_repeat_thread_done(message.return_code);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal{
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* p) {
|
||||
auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
repeat_ready_signal = true;
|
||||
}
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* p) {
|
||||
auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
on_repeat_tx_progress(message.progress);
|
||||
}};
|
||||
// app save settings
|
||||
std::app_settings settings{};
|
||||
std::app_settings::AppSettings app_settings{};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -20,12 +21,13 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_recon_settings.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_recon_settings.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
@@ -37,30 +39,26 @@ namespace ui {
|
||||
ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, std::string input_file, std::string output_file)
|
||||
: View(parent_rect), _input_file{input_file}, _output_file{output_file} {
|
||||
hidden(true);
|
||||
add_children({&button_load_freqs,
|
||||
add_children({&button_input_file,
|
||||
&text_input_file,
|
||||
&button_save_freqs,
|
||||
&button_output_file,
|
||||
&button_choose_output_file,
|
||||
&button_choose_output_name,
|
||||
&checkbox_autosave_freqs,
|
||||
&checkbox_autostart_recon,
|
||||
&checkbox_continuous,
|
||||
&checkbox_clear_output});
|
||||
|
||||
checkbox_autosave_freqs.set_value(persistent_memory::recon_autosave_freqs());
|
||||
checkbox_autostart_recon.set_value(persistent_memory::recon_autostart_recon());
|
||||
checkbox_continuous.set_value(persistent_memory::recon_continuous());
|
||||
checkbox_clear_output.set_value(persistent_memory::recon_clear_output());
|
||||
|
||||
text_input_file.set(_input_file);
|
||||
button_output_file.set_text(_output_file);
|
||||
button_choose_output_name.set_text(_output_file);
|
||||
|
||||
button_load_freqs.on_select = [this, &nav](Button&) {
|
||||
button_input_file.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
std::string dir_filter = "FREQMAN/";
|
||||
std::string str_file_path = new_file_path.string();
|
||||
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
|
||||
// get the filename without txt extension so we can use load_freqman_file fcn
|
||||
if (new_file_path.native().find(freqman_dir.native()) == 0) {
|
||||
_input_file = new_file_path.stem().string();
|
||||
text_input_file.set(_input_file);
|
||||
} else {
|
||||
@@ -69,75 +67,107 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
|
||||
};
|
||||
};
|
||||
|
||||
button_save_freqs.on_select = [this, &nav](Button&) {
|
||||
button_choose_output_file.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
std::string dir_filter = "FREQMAN/";
|
||||
std::string str_file_path = new_file_path.string();
|
||||
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
|
||||
if (new_file_path.native().find(freqman_dir.native()) == 0) {
|
||||
_output_file = new_file_path.stem().string();
|
||||
button_output_file.set_text(_output_file);
|
||||
button_choose_output_name.set_text(_output_file);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
nav.display_modal("SAVE ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
button_output_file.on_select = [this, &nav](Button&) {
|
||||
text_prompt(nav, _output_file, 28,
|
||||
[this](std::string& buffer) {
|
||||
_output_file = buffer;
|
||||
button_output_file.set_text(_output_file);
|
||||
});
|
||||
button_choose_output_name.on_select = [this, &nav](Button&) {
|
||||
text_prompt(nav, _output_file, 28, [this](std::string& buffer) {
|
||||
_output_file = buffer;
|
||||
button_choose_output_name.set_text(_output_file);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
void ReconSetupViewMain::Save(std::string& input_file, std::string& output_file) {
|
||||
void ReconSetupViewMain::save(std::string& input_file, std::string& output_file) {
|
||||
persistent_memory::set_recon_autosave_freqs(checkbox_autosave_freqs.value());
|
||||
persistent_memory::set_recon_autostart_recon(checkbox_autostart_recon.value());
|
||||
persistent_memory::set_recon_continuous(checkbox_continuous.value());
|
||||
persistent_memory::set_recon_clear_output(checkbox_clear_output.value());
|
||||
input_file = _input_file;
|
||||
output_file = _output_file;
|
||||
};
|
||||
void ReconSetupViewMore::Save(uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, uint32_t& recon_match_mode) {
|
||||
void ReconSetupViewMore::save() {
|
||||
persistent_memory::set_recon_load_freqs(checkbox_load_freqs.value());
|
||||
persistent_memory::set_recon_load_ranges(checkbox_load_ranges.value());
|
||||
persistent_memory::set_recon_load_hamradios(checkbox_load_hamradios.value());
|
||||
persistent_memory::set_recon_load_repeaters(checkbox_load_repeaters.value());
|
||||
persistent_memory::set_recon_update_ranges_when_recon(checkbox_update_ranges_when_recon.value());
|
||||
recon_lock_duration = field_recon_lock_duration.value();
|
||||
recon_lock_nb_match = field_recon_lock_nb_match.value();
|
||||
recon_match_mode = field_recon_match_mode.selected_index_value();
|
||||
persistent_memory::set_recon_auto_record_locked(checkbox_auto_record_locked.value());
|
||||
persistent_memory::set_recon_repeat_recorded(checkbox_repeat_recorded.value());
|
||||
persistent_memory::set_recon_repeat_nb(field_repeat_nb.value());
|
||||
persistent_memory::set_recon_repeat_amp(checkbox_repeat_amp.value());
|
||||
persistent_memory::set_recon_repeat_gain(field_repeat_gain.value());
|
||||
persistent_memory::set_recon_repeat_delay(field_repeat_delay.value());
|
||||
};
|
||||
|
||||
void ReconSetupViewMain::focus() {
|
||||
button_load_freqs.focus();
|
||||
button_input_file.focus();
|
||||
}
|
||||
|
||||
ReconSetupViewMore::ReconSetupViewMore(NavigationView& nav, Rect parent_rect, uint32_t recon_lock_duration, uint32_t recon_lock_nb_match, uint32_t recon_match_mode)
|
||||
: View(parent_rect), _recon_lock_duration{recon_lock_duration}, _recon_lock_nb_match{recon_lock_nb_match}, _recon_match_mode{recon_match_mode} {
|
||||
ReconSetupViewMore::ReconSetupViewMore(NavigationView& nav, Rect parent_rect)
|
||||
: View(parent_rect) {
|
||||
(void)nav;
|
||||
hidden(true);
|
||||
|
||||
add_children({
|
||||
&checkbox_load_freqs,
|
||||
&checkbox_load_ranges,
|
||||
&checkbox_load_hamradios,
|
||||
&checkbox_update_ranges_when_recon,
|
||||
&text_recon_lock_duration,
|
||||
&field_recon_lock_duration,
|
||||
&text_recon_lock_nb,
|
||||
&field_recon_lock_nb_match,
|
||||
&field_recon_match_mode,
|
||||
});
|
||||
add_children({&checkbox_load_freqs,
|
||||
&checkbox_load_repeaters,
|
||||
&checkbox_load_ranges,
|
||||
&checkbox_load_hamradios,
|
||||
&checkbox_update_ranges_when_recon,
|
||||
&checkbox_auto_record_locked,
|
||||
&checkbox_repeat_recorded,
|
||||
&text_repeat_nb,
|
||||
&field_repeat_nb,
|
||||
&checkbox_repeat_amp,
|
||||
&text_repeat_gain,
|
||||
&field_repeat_gain,
|
||||
&text_repeat_delay,
|
||||
&field_repeat_delay});
|
||||
|
||||
// tx options have to be in yellow to inform the users that activating them will make the device transmit
|
||||
checkbox_repeat_recorded.set_style(&Styles::yellow);
|
||||
text_repeat_nb.set_style(&Styles::yellow);
|
||||
field_repeat_nb.set_style(&Styles::yellow);
|
||||
checkbox_repeat_amp.set_style(&Styles::yellow);
|
||||
text_repeat_gain.set_style(&Styles::yellow);
|
||||
field_repeat_gain.set_style(&Styles::yellow);
|
||||
text_repeat_delay.set_style(&Styles::yellow);
|
||||
field_repeat_delay.set_style(&Styles::yellow);
|
||||
|
||||
checkbox_load_freqs.set_value(persistent_memory::recon_load_freqs());
|
||||
checkbox_load_repeaters.set_value(persistent_memory::recon_load_repeaters());
|
||||
checkbox_load_ranges.set_value(persistent_memory::recon_load_ranges());
|
||||
checkbox_load_hamradios.set_value(persistent_memory::recon_load_hamradios());
|
||||
checkbox_update_ranges_when_recon.set_value(persistent_memory::recon_update_ranges_when_recon());
|
||||
field_recon_lock_duration.set_value(_recon_lock_duration);
|
||||
field_recon_lock_nb_match.set_value(_recon_lock_nb_match);
|
||||
field_recon_match_mode.set_by_value(_recon_match_mode);
|
||||
checkbox_auto_record_locked.set_value(persistent_memory::recon_auto_record_locked());
|
||||
checkbox_repeat_recorded.set_value(persistent_memory::recon_repeat_recorded());
|
||||
checkbox_repeat_amp.set_value(persistent_memory::recon_repeat_amp());
|
||||
field_repeat_nb.set_value(persistent_memory::recon_repeat_nb());
|
||||
field_repeat_gain.set_value(persistent_memory::recon_repeat_gain());
|
||||
field_repeat_delay.set_value(persistent_memory::recon_repeat_delay());
|
||||
|
||||
// tx warning modal
|
||||
checkbox_repeat_recorded.on_select = [this, &nav](Checkbox&, bool v) {
|
||||
if (v) {
|
||||
nav.display_modal(
|
||||
"TX WARNING",
|
||||
"This activate TX ability\nin Recon !",
|
||||
YESNO,
|
||||
[this, &nav](bool choice) {
|
||||
if (!choice)
|
||||
checkbox_repeat_recorded.set_value(choice);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
void ReconSetupViewMore::focus() {
|
||||
@@ -148,28 +178,19 @@ void ReconSetupView::focus() {
|
||||
viewMain.focus();
|
||||
}
|
||||
|
||||
ReconSetupView::ReconSetupView(
|
||||
NavigationView& nav,
|
||||
std::string _input_file,
|
||||
std::string _output_file,
|
||||
uint32_t _recon_lock_duration,
|
||||
uint32_t _recon_lock_nb_match,
|
||||
uint32_t _recon_match_mode)
|
||||
: nav_{nav}, input_file{_input_file}, output_file{_output_file}, recon_lock_duration{_recon_lock_duration}, recon_lock_nb_match{_recon_lock_nb_match}, recon_match_mode{_recon_match_mode} {
|
||||
ReconSetupView::ReconSetupView(NavigationView& nav, std::string _input_file, std::string _output_file)
|
||||
: nav_{nav}, input_file{_input_file}, output_file{_output_file} {
|
||||
add_children({&tab_view,
|
||||
&viewMain,
|
||||
&viewMore,
|
||||
&button_save});
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
viewMain.Save(input_file, output_file);
|
||||
viewMore.Save(recon_lock_duration, recon_lock_nb_match, recon_match_mode);
|
||||
viewMain.save(input_file, output_file);
|
||||
viewMore.save();
|
||||
std::vector<std::string> messages;
|
||||
messages.push_back(input_file);
|
||||
messages.push_back(output_file);
|
||||
messages.push_back(to_string_dec_uint(recon_lock_duration));
|
||||
messages.push_back(to_string_dec_uint(recon_lock_nb_match));
|
||||
messages.push_back(to_string_dec_uint(recon_match_mode));
|
||||
on_changed(messages);
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -29,9 +30,7 @@
|
||||
#include "ui_tabview.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
// maximum usable freq
|
||||
#define MAX_UFREQ 7200000000
|
||||
#include "ui_styles.hpp"
|
||||
|
||||
// 1Mhz helper
|
||||
#ifdef OneMHz
|
||||
@@ -49,6 +48,8 @@
|
||||
// maximum lock duration
|
||||
#define RECON_MAX_LOCK_DURATION 9900
|
||||
|
||||
#define RECON_DEF_SQUELCH -14
|
||||
|
||||
// default number of match to have a lock
|
||||
#define RECON_DEF_NB_MATCH 3
|
||||
#define RECON_MIN_LOCK_DURATION 100 // have to be >= and a multiple of STATS_UPDATE_INTERVAL
|
||||
@@ -59,31 +60,32 @@
|
||||
//#define SCREEN_H 320
|
||||
|
||||
// recon settings nb params
|
||||
#define RECON_SETTINGS_NB_PARAMS 8
|
||||
#define RECON_SETTINGS_NB_PARAMS 7
|
||||
|
||||
namespace ui {
|
||||
|
||||
class ReconSetupViewMain : public View {
|
||||
public:
|
||||
ReconSetupViewMain(NavigationView& nav, Rect parent_rect, std::string input_file, std::string output_file);
|
||||
void Save(std::string& input_file, std::string& output_file);
|
||||
void save(std::string& input_file, std::string& output_file);
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
std::string _input_file = {"RECON"};
|
||||
std::string _output_file = {"RECON_RESULTS"};
|
||||
std::string _input_file{"RECON"};
|
||||
std::string _output_file{"RECON_RESULTS"};
|
||||
|
||||
Button button_load_freqs{
|
||||
Button button_input_file{
|
||||
{1 * 8, 12, 18 * 8, 22},
|
||||
"select input file"};
|
||||
Text text_input_file{
|
||||
{1 * 8, 4 + 2 * 16, 18 * 8, 22},
|
||||
"RECON"};
|
||||
|
||||
Button button_save_freqs{
|
||||
Button button_choose_output_file{
|
||||
{1 * 8, 4 * 16 - 8, 18 * 8, 22},
|
||||
"select output file"};
|
||||
Button button_output_file{
|
||||
|
||||
Button button_choose_output_name{
|
||||
{1 * 8, 5 * 16 - 2, 18 * 8, 22},
|
||||
"RECON_RESULTS"};
|
||||
|
||||
@@ -97,80 +99,105 @@ class ReconSetupViewMain : public View {
|
||||
3,
|
||||
"autostart recon"};
|
||||
|
||||
Checkbox checkbox_continuous{
|
||||
{1 * 8, 11 * 16 - 4},
|
||||
3,
|
||||
"continuous"};
|
||||
Checkbox checkbox_clear_output{
|
||||
{1 * 8, 13 * 16 - 4},
|
||||
{1 * 8, 11 * 16 - 4},
|
||||
3,
|
||||
"clear output at start"};
|
||||
};
|
||||
|
||||
class ReconSetupViewMore : public View {
|
||||
public:
|
||||
ReconSetupViewMore(NavigationView& nav, Rect parent_rect, uint32_t _recon_lock_duration, uint32_t _recon_lock_nb_match, uint32_t _recon_match_mode);
|
||||
ReconSetupViewMore(NavigationView& nav, Rect parent_rect);
|
||||
|
||||
void Save(uint32_t& recon_lock_duration, uint32_t& recon_lock_nb_match, uint32_t& recon_match_mode);
|
||||
void save();
|
||||
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
uint32_t _recon_lock_duration = STATS_UPDATE_INTERVAL;
|
||||
uint32_t _recon_lock_nb_match = RECON_DEF_NB_MATCH;
|
||||
uint32_t _recon_match_mode = RECON_MATCH_CONTINUOUS;
|
||||
|
||||
Checkbox checkbox_load_freqs{
|
||||
{1 * 8, 12},
|
||||
3,
|
||||
"input: load freqs"};
|
||||
"load freq",
|
||||
true};
|
||||
|
||||
Checkbox checkbox_load_repeaters{
|
||||
{14 * 8, 12},
|
||||
3,
|
||||
"load repeater",
|
||||
true};
|
||||
|
||||
Checkbox checkbox_load_ranges{
|
||||
{1 * 8, 42},
|
||||
3,
|
||||
"input: load ranges"};
|
||||
"load range",
|
||||
true};
|
||||
|
||||
Checkbox checkbox_load_hamradios{
|
||||
{1 * 8, 72},
|
||||
3,
|
||||
"input: load hamradios"};
|
||||
"load hamradio",
|
||||
true};
|
||||
|
||||
Checkbox checkbox_update_ranges_when_recon{
|
||||
{1 * 8, 102},
|
||||
3,
|
||||
"auto update m-ranges"};
|
||||
Text text_recon_lock_duration{
|
||||
{1 * 8, 132, 22 * 8, 22},
|
||||
" ms (lock duration)"};
|
||||
NumberField field_recon_lock_duration{
|
||||
{1 * 8, 132}, // position X , Y
|
||||
4, // number of displayed digits (even empty)
|
||||
{-RECON_MAX_LOCK_DURATION, RECON_MAX_LOCK_DURATION}, // range of number
|
||||
STATS_UPDATE_INTERVAL, // rotary encoder increment
|
||||
' ', // filling character
|
||||
false // can loop
|
||||
};
|
||||
Text text_recon_lock_nb{
|
||||
{1 * 8, 162, 25 * 8, 22},
|
||||
" x (nb lock to match freq)"};
|
||||
NumberField field_recon_lock_nb_match{
|
||||
|
||||
Checkbox checkbox_auto_record_locked{
|
||||
{1 * 8, 132},
|
||||
3,
|
||||
"record locked periods"};
|
||||
|
||||
Checkbox checkbox_repeat_recorded{
|
||||
{1 * 8, 162},
|
||||
4,
|
||||
3,
|
||||
"repeater,"};
|
||||
|
||||
Text text_repeat_nb{
|
||||
{14 * 8, 165, 3 * 8, 22},
|
||||
"nb:"};
|
||||
|
||||
NumberField field_repeat_nb{
|
||||
{17 * 8, 165},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' ',
|
||||
false};
|
||||
OptionsField field_recon_match_mode{
|
||||
};
|
||||
|
||||
Checkbox checkbox_repeat_amp{
|
||||
{1 * 8, 192},
|
||||
20, // CONTINUOUS MATCH MODE / SPARSE TIMED MATCH MODE
|
||||
{
|
||||
{"SQL MATCH: CONTINOUS", 0},
|
||||
{"SQL MATCH: SPARSE", 1}}};
|
||||
3,
|
||||
"AMP,"};
|
||||
|
||||
Text text_repeat_gain{
|
||||
{9 * 8, 196, 5 * 8, 22},
|
||||
"GAIN:"};
|
||||
|
||||
NumberField field_repeat_gain{
|
||||
{14 * 8, 196},
|
||||
2,
|
||||
{0, 47},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
Text text_repeat_delay{
|
||||
{16 * 8, 196, 8 * 8, 22},
|
||||
", delay:"};
|
||||
|
||||
NumberField field_repeat_delay{
|
||||
{24 * 8, 196},
|
||||
3,
|
||||
{0, 254},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
};
|
||||
|
||||
class ReconSetupView : public View {
|
||||
public:
|
||||
ReconSetupView(NavigationView& nav, std::string _input_file, std::string _output_file, uint32_t _recon_lock_duration, uint32_t _recon_lock_nb_match, uint32_t _recon_match_mode);
|
||||
ReconSetupView(NavigationView& nav, std::string _input_file, std::string _output_file);
|
||||
|
||||
std::function<void(std::vector<std::string> messages)> on_changed{};
|
||||
|
||||
@@ -181,20 +208,18 @@ class ReconSetupView : public View {
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
std::string input_file = {"RECON"};
|
||||
std::string output_file = {"RECON_RESULTS"};
|
||||
uint32_t recon_lock_duration = STATS_UPDATE_INTERVAL;
|
||||
uint32_t recon_lock_nb_match = RECON_DEF_NB_MATCH;
|
||||
uint32_t recon_match_mode = RECON_MATCH_CONTINUOUS;
|
||||
std::string input_file{"RECON"};
|
||||
std::string output_file{"RECON_RESULTS"};
|
||||
|
||||
Rect view_rect = {0, 3 * 8, SCREEN_W, 230};
|
||||
Rect view_rect{0, 3 * 8, SCREEN_W, 230};
|
||||
|
||||
ReconSetupViewMain viewMain{nav_, view_rect, input_file, output_file};
|
||||
ReconSetupViewMore viewMore{nav_, view_rect, recon_lock_duration, recon_lock_nb_match, recon_match_mode};
|
||||
ReconSetupViewMore viewMore{nav_, view_rect};
|
||||
|
||||
TabView tab_view{
|
||||
{"Main", Color::cyan(), &viewMain},
|
||||
{"More", Color::green(), &viewMore}};
|
||||
|
||||
Button button_save{
|
||||
{9 * 8, 255, 14 * 8, 40},
|
||||
"SAVE"};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user