Compare commits
42
Commits
master
...
mui-wifi-maps
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
712cdd2287 | ||
|
|
44523b4ba7 | ||
|
|
6b7a1819fb | ||
|
|
d3a86841b9 | ||
|
|
e14b8d385a | ||
|
|
8ce1a872eb | ||
|
|
abfa346630 | ||
|
|
2ffe2d829c | ||
|
|
bfaf6c6b20 | ||
|
|
d293d654a0 | ||
|
|
8384659608 | ||
|
|
a632d0133f | ||
|
|
f982b58d61 | ||
|
|
efb0b299b2 | ||
|
|
eb80d3141d | ||
|
|
3604c1255d | ||
|
|
22d63fa69c | ||
|
|
fc030d2d19 | ||
|
|
4a534f02a4 | ||
|
|
0ea408e12b | ||
|
|
644d0d4a15 | ||
|
|
f04746a928 | ||
|
|
1be2529fb9 | ||
|
|
959756abf1 | ||
|
|
c88b802e32 | ||
|
|
9f74fc11de | ||
|
|
1525f6fae3 | ||
|
|
f85bda87e0 | ||
|
|
8891b55be1 | ||
|
|
2ef09d17b9 | ||
|
|
19d070c284 | ||
|
|
e24db2994b | ||
|
|
ac7a58cd45 | ||
|
|
53c21eb30d | ||
|
|
e51e6cad84 | ||
|
|
4890f7084f | ||
|
|
3fcbfe4370 | ||
|
|
da808cb43b | ||
|
|
82580c6798 | ||
|
|
016e68ec53 | ||
|
|
79f469ce71 | ||
|
|
0501e177e9 |
@@ -8,8 +8,6 @@ runs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
|
||||
@@ -16,8 +16,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-debian-src:
|
||||
@@ -28,8 +27,6 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
path: meshtasticd
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
@@ -42,6 +39,7 @@ jobs:
|
||||
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
|
||||
|
||||
- name: Import GPG key
|
||||
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
|
||||
@@ -60,7 +58,7 @@ jobs:
|
||||
run: debian/ci_pack_sdeb.sh
|
||||
env:
|
||||
SERIES: ${{ inputs.series }}
|
||||
GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }}
|
||||
GPG_KEY_ID: ${{ steps.gpg.outputs.keyid || '' }}
|
||||
PKG_VERSION: ${{ steps.version.outputs.deb }}
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
|
||||
@@ -26,8 +26,6 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Build ${{ inputs.platform }}
|
||||
id: build
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
gather-artifacts:
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [version, build]
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop # Default branch, same as 'cron' above
|
||||
paths:
|
||||
- debian/**
|
||||
- "*.rpkg"
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
- .github/workflows/hook_copr.yml
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -37,7 +37,7 @@ on:
|
||||
value: ${{ jobs.docker-build.outputs.digest }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
@@ -50,8 +50,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Get release version string
|
||||
run: |
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
@@ -86,8 +86,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Get release version string
|
||||
run: |
|
||||
|
||||
@@ -11,8 +11,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-copr-hook:
|
||||
@@ -22,8 +21,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{ github.ref }}
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
- name: Trigger COPR build
|
||||
uses: vidplace7/copr-build@main
|
||||
|
||||
@@ -15,8 +15,7 @@ on:
|
||||
- "**.md"
|
||||
- version.properties
|
||||
|
||||
# Note: This is different from "pull_request". Need to specify ref when doing checkouts.
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
@@ -29,6 +28,8 @@ on:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
strategy:
|
||||
@@ -88,8 +89,6 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
- name: Check ${{ matrix.check.board }}
|
||||
uses: meshtastic/gh-action-firmware@main
|
||||
with:
|
||||
@@ -126,9 +125,16 @@ jobs:
|
||||
|
||||
test-native:
|
||||
if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }}
|
||||
permissions: # Needed for dorny/test-reporter.
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
uses: ./.github/workflows/test_native.yml
|
||||
|
||||
docker:
|
||||
permissions: # Needed for pushing to GHCR.
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -153,9 +159,6 @@ jobs:
|
||||
gather-artifacts:
|
||||
# trunk-ignore(checkov/CKV2_GHA_1)
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -173,9 +176,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
@@ -231,13 +231,6 @@ jobs:
|
||||
path: ./*.elf
|
||||
retention-days: 30
|
||||
|
||||
- uses: scruplelesswizard/comment-artifact@main
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
shame:
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
continue-on-error: true
|
||||
@@ -245,7 +238,7 @@ jobs:
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
if: github.event_name == 'pull_request_target'
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
|
||||
fetch-depth: 0
|
||||
@@ -263,24 +256,26 @@ jobs:
|
||||
overwrite: true
|
||||
path: manifests-new/*.mt.json
|
||||
- name: Find the merge base
|
||||
if: github.event_name == 'pull_request_target'
|
||||
if: github.event_name == 'pull_request'
|
||||
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
|
||||
env:
|
||||
base: ${{ github.base_ref }}
|
||||
head: ${{ github.sha }}
|
||||
# Currently broken (for-loop through EVERY artifact -- rate limiting)
|
||||
# - name: Download the old manifests
|
||||
# if: github.event_name == 'pull_request_target'
|
||||
# if: github.event_name == 'pull_request'
|
||||
# run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/
|
||||
# env:
|
||||
# GH_TOKEN: ${{ github.token }}
|
||||
# merge_base: ${{ env.MERGE_BASE }}
|
||||
# repo: ${{ github.repository }}
|
||||
# - name: Do scan and post comment
|
||||
# if: github.event_name == 'pull_request_target'
|
||||
# if: github.event_name == 'pull_request'
|
||||
# run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
|
||||
|
||||
release-artifacts:
|
||||
permissions: # Needed for 'gh release upload'.
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
|
||||
outputs:
|
||||
@@ -372,6 +367,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-firmware:
|
||||
permissions: # Needed for 'gh release upload'.
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
name: Merge Queue
|
||||
# Not sure how concurrency works in merge_queue, removing for now.
|
||||
# concurrency:
|
||||
# group: merge-queue-${{ github.head_ref || github.run_id }}
|
||||
# cancel-in-progress: true
|
||||
on:
|
||||
# Merge group is a special trigger that is used to trigger the workflow when a merge group is created.
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
arch:
|
||||
- all
|
||||
- check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
cache: pip
|
||||
- run: pip install -U platformio
|
||||
- name: Generate matrix
|
||||
id: jsonStep
|
||||
run: |
|
||||
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
|
||||
else
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr)
|
||||
fi
|
||||
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
|
||||
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
all: ${{ steps.jsonStep.outputs.all }}
|
||||
check: ${{ steps.jsonStep.outputs.check }}
|
||||
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get release version string
|
||||
run: |
|
||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
env:
|
||||
BUILD_LOCATION: local
|
||||
outputs:
|
||||
long: ${{ steps.version.outputs.long }}
|
||||
deb: ${{ steps.version.outputs.deb }}
|
||||
|
||||
check:
|
||||
needs: setup
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
check: ${{ fromJson(needs.setup.outputs.check) }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Build base
|
||||
id: base
|
||||
uses: ./.github/actions/setup-base
|
||||
- name: Check ${{ matrix.check.board }}
|
||||
run: bin/check-all.sh ${{ matrix.check.board }}
|
||||
|
||||
build:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
matrix:
|
||||
build: ${{ fromJson(needs.setup.outputs.all) }}
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
pio_env: ${{ matrix.build.board }}
|
||||
platform: ${{ matrix.build.platform }}
|
||||
|
||||
build-debian-src:
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
uses: ./.github/workflows/build_debian_src.yml
|
||||
with:
|
||||
series: UNRELEASED
|
||||
build_location: local
|
||||
secrets: inherit
|
||||
|
||||
package-pio-deps-native-tft:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/package_pio_deps.yml
|
||||
with:
|
||||
pio_env: native-tft
|
||||
secrets: inherit
|
||||
|
||||
test-native:
|
||||
if: ${{ !contains(github.ref_name, 'event/') }}
|
||||
uses: ./.github/workflows/test_native.yml
|
||||
|
||||
docker:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
distro: [debian, alpine]
|
||||
platform: [linux/amd64, linux/arm64, linux/arm/v7]
|
||||
pio_env: [native, native-tft]
|
||||
exclude:
|
||||
- distro: alpine
|
||||
platform: linux/arm/v7
|
||||
- pio_env: native-tft
|
||||
platform: linux/arm64
|
||||
- pio_env: native-tft
|
||||
platform: linux/arm/v7
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: ${{ matrix.distro }}
|
||||
platform: ${{ matrix.platform }}
|
||||
runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||
pio_env: ${{ matrix.pio_env }}
|
||||
push: false
|
||||
|
||||
gather-artifacts:
|
||||
# trunk-ignore(checkov/CKV2_GHA_1)
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
runs-on: ubuntu-latest
|
||||
needs: [version, build]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-${{matrix.arch}}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
|
||||
- name: Move files up
|
||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
path: |
|
||||
./firmware-*.bin
|
||||
./firmware-*.uf2
|
||||
./firmware-*.hex
|
||||
./firmware-*.zip
|
||||
./device-*.sh
|
||||
./device-*.bat
|
||||
./littlefs-*.bin
|
||||
./bleota*bin
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./output
|
||||
|
||||
# For diagnostics
|
||||
- name: Show artifacts
|
||||
run: ls -lR
|
||||
|
||||
- name: Device scripts permissions
|
||||
run: |
|
||||
chmod +x ./output/device-install.sh || true
|
||||
chmod +x ./output/device-update.sh || true
|
||||
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
path: ./*.elf
|
||||
retention-days: 30
|
||||
|
||||
- uses: scruplelesswizard/comment-artifact@main
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
needs:
|
||||
- version
|
||||
- gather-artifacts
|
||||
- build-debian-src
|
||||
- package-pio-deps-native-tft
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
id: create_release
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
||||
tag_name: v${{ needs.version.outputs.long }}
|
||||
body: |
|
||||
Autogenerated by github action, developer should edit as required before publishing...
|
||||
|
||||
- name: Download source deb
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
||||
merge-multiple: true
|
||||
path: ./output/debian-src
|
||||
|
||||
- name: Download `native-tft` pio deps
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./output/pio-deps-native-tft
|
||||
|
||||
- name: Zip Linux sources
|
||||
working-directory: output
|
||||
run: |
|
||||
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
|
||||
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
|
||||
|
||||
# For diagnostics
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -lR
|
||||
|
||||
- name: Add Linux sources to GtiHub Release
|
||||
# Only run when targeting master branch with workflow_dispatch
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
run: |
|
||||
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
|
||||
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-firmware:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
needs: [release-artifacts, version]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./output
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -lR
|
||||
|
||||
- name: Device scripts permissions
|
||||
run: |
|
||||
chmod +x ./output/device-install.sh || true
|
||||
chmod +x ./output/device-update.sh || true
|
||||
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./elfs
|
||||
|
||||
- name: Zip debug elfs
|
||||
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
|
||||
|
||||
# For diagnostics
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -lR
|
||||
|
||||
- name: Add bins and debug elfs to GitHub Release
|
||||
# Only run when targeting master branch with workflow_dispatch
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
run: |
|
||||
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-firmware:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
needs: [release-firmware, version]
|
||||
env:
|
||||
targets: |-
|
||||
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./publish
|
||||
|
||||
- name: Publish firmware to meshtastic.github.io
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
env:
|
||||
# On event/* branches, use the event name as the destination prefix
|
||||
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
|
||||
with:
|
||||
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
|
||||
external_repository: meshtastic/meshtastic.github.io
|
||||
publish_branch: master
|
||||
publish_dir: ./publish
|
||||
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
|
||||
keep_files: true
|
||||
user_name: github-actions[bot]
|
||||
user_email: github-actions[bot]@users.noreply.github.com
|
||||
commit_message: ${{ needs.version.outputs.long }}
|
||||
enable_jekyll: true
|
||||
@@ -18,8 +18,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-debian-src:
|
||||
|
||||
@@ -16,8 +16,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pkg-pio-libdeps:
|
||||
@@ -27,8 +26,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -16,8 +16,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-debian-src:
|
||||
@@ -36,8 +35,6 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
path: meshtasticd
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
@@ -70,5 +67,6 @@ jobs:
|
||||
|
||||
- name: Publish with dput
|
||||
if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
|
||||
timeout-minutes: 15 # dput is terrible, sometimes runs 'forever'
|
||||
run: |
|
||||
dput ${{ inputs.ppa_repo }} meshtasticd_${{ steps.version.outputs.deb }}~${{ inputs.series }}_source.changes
|
||||
|
||||
@@ -16,8 +16,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup native build
|
||||
@@ -72,8 +70,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup native build
|
||||
@@ -128,9 +124,6 @@ jobs:
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Get release version string
|
||||
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -6,7 +6,7 @@ permissions: read-all
|
||||
jobs:
|
||||
update-protobufs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
permissions: # Needed for peter-evans/create-pull-request.
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x4405"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"],
|
||||
["0x2886", "0x1667"]
|
||||
],
|
||||
"usb_product": "HT-n5262G",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "heltec_mesh_node_t096",
|
||||
"variants_dir": "variants",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": ["jlink"],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Heltec nrf (Adafruit BSP)",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://heltec.org/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
Vendored
+7
-2
@@ -27,5 +27,10 @@ rm -rf debian/changelog
|
||||
dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \
|
||||
"GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES"
|
||||
|
||||
# Build the source deb
|
||||
debuild -S -nc -k"$GPG_KEY_ID"
|
||||
if [[ -n $GPG_KEY_ID ]]; then
|
||||
# Build and sign the source deb
|
||||
debuild -S -nc -k"$GPG_KEY_ID"
|
||||
else
|
||||
# Build the source deb without signing (forks)
|
||||
debuild -S -nc
|
||||
fi
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/f36d2a953524e372b78c5b4147ec55f38716964e.zip
|
||||
https://github.com/meshtastic/device-ui/archive/de2e490882105fc2778d761740d9a1711d1dab78.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
|
||||
@@ -4,7 +4,8 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
bool usePreset)
|
||||
{
|
||||
|
||||
// If use_preset is false, always return "Custom"
|
||||
// If use_preset is false, always return "Custom" — callers such as RadioInterface and Channels
|
||||
// rely on this being a stable literal for channel-name hashing and default-channel detection.
|
||||
if (!usePreset) {
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
@@ -78,6 +78,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// Configuration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Pre-hop drop handling (compile-time flag).
|
||||
#ifndef MESHTASTIC_PREHOP_DROP
|
||||
#define MESHTASTIC_PREHOP_DROP 0
|
||||
#endif
|
||||
|
||||
/// Convert a preprocessor name into a quoted string
|
||||
#define xstr(s) ystr(s)
|
||||
#define ystr(s) #s
|
||||
@@ -151,8 +156,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#ifdef USE_KCT8103L_PA
|
||||
// Power Amps are often non-linear, so we can use an array of values for the power curve
|
||||
#if defined(HELTEC_WIRELESS_TRACKER_V2)
|
||||
#define NUM_PA_POINTS 22
|
||||
#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 11, 10, 9, 8, 7
|
||||
#elif defined(HELTEC_MESH_NODE_T096)
|
||||
#define NUM_PA_POINTS 22
|
||||
#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 11, 10, 9, 8, 7
|
||||
#else
|
||||
// If a board enables USE_KCT8103L_PA but does not match a known variant and has
|
||||
// not already provided a PA curve, fail at compile time to avoid unsafe defaults.
|
||||
#if !defined(NUM_PA_POINTS) || !defined(TX_GAIN_LORA)
|
||||
#error "USE_KCT8103L_PA is defined, but no PA gain curve (NUM_PA_POINTS / TX_GAIN_LORA) is configured for this board."
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef RAK13302
|
||||
@@ -499,6 +515,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
|
||||
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
|
||||
#define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1
|
||||
#define MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT 1
|
||||
#define MESHTASTIC_EXCLUDE_ATAK 1
|
||||
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
|
||||
#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1
|
||||
|
||||
@@ -103,6 +103,14 @@ static int32_t gpsSwitch()
|
||||
if (gps) {
|
||||
int currentState = digitalRead(PIN_GPS_SWITCH);
|
||||
|
||||
// Respect explicit NOT_PRESENT mode and do not let the hardware switch re-enable GPS.
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
|
||||
gps->disable();
|
||||
lastState = currentState;
|
||||
firstrun = false;
|
||||
return 1000;
|
||||
}
|
||||
|
||||
// if the switch is set to zero, disable the GPS Thread
|
||||
if (firstrun)
|
||||
if (currentState == LOW)
|
||||
|
||||
@@ -1348,7 +1348,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
tft->displayOn();
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
|
||||
tft->wakeup();
|
||||
tft->powerSaveOff();
|
||||
#endif
|
||||
@@ -1359,7 +1359,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
#ifdef UNPHONE
|
||||
unphone.backlight(true); // using unPhone library
|
||||
#endif
|
||||
#ifdef RAK14014
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(M5STACK) && !defined(ST7789_CS) && \
|
||||
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
|
||||
tft->setBrightness(172);
|
||||
@@ -1375,7 +1375,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
tft->displayOff();
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
|
||||
tft->sleep();
|
||||
tft->powerSaveOn();
|
||||
#endif
|
||||
@@ -1386,7 +1386,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
#ifdef UNPHONE
|
||||
unphone.backlight(false); // using unPhone library
|
||||
#endif
|
||||
#ifdef RAK14014
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
|
||||
tft->setBrightness(0);
|
||||
#endif
|
||||
@@ -1401,7 +1401,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
|
||||
void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
|
||||
{
|
||||
#ifdef RAK14014
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
// todo
|
||||
#elif !defined(HACKADAY_COMMUNICATOR)
|
||||
tft->setBrightness(_brightness);
|
||||
@@ -1421,7 +1421,7 @@ bool TFTDisplay::hasTouch(void)
|
||||
{
|
||||
#ifdef RAK14014
|
||||
return true;
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
|
||||
return tft->touch() != nullptr;
|
||||
#else
|
||||
return false;
|
||||
@@ -1440,7 +1440,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
|
||||
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
|
||||
return tft->getTouch(x, y);
|
||||
#else
|
||||
return false;
|
||||
@@ -1457,7 +1457,7 @@ bool TFTDisplay::connect()
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_INFO("Do TFT init");
|
||||
#ifdef RAK14014
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
|
||||
tft = new TFT_eSPI;
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
@@ -1494,7 +1494,7 @@ bool TFTDisplay::connect()
|
||||
ft6336u.begin();
|
||||
pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP);
|
||||
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
|
||||
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
|
||||
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) || defined(HELTEC_MESH_NODE_T096)
|
||||
tft->setRotation(1); // T-Deck has the TFT in landscape
|
||||
#elif defined(T_WATCH_S3)
|
||||
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
||||
|
||||
@@ -408,7 +408,16 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
display->drawString(nameX, getTextPositions(display)[line++], device_role);
|
||||
|
||||
// === Third Row: Radio Preset ===
|
||||
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
||||
// For custom modem settings show the actual parameters; for presets use the preset name.
|
||||
char modeStr[16];
|
||||
if (!config.lora.use_preset) {
|
||||
snprintf(modeStr, sizeof(modeStr), "BW%u-SF%u-CR%u", static_cast<unsigned>(config.lora.bandwidth),
|
||||
static_cast<unsigned>(config.lora.spread_factor), static_cast<unsigned>(config.lora.coding_rate));
|
||||
} else {
|
||||
strncpy(modeStr, DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, true),
|
||||
sizeof(modeStr) - 1);
|
||||
modeStr[sizeof(modeStr) - 1] = '\0';
|
||||
}
|
||||
|
||||
char regionradiopreset[25];
|
||||
const char *region = myRegion ? myRegion->name : NULL;
|
||||
@@ -416,7 +425,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
|
||||
} else {
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, modeStr);
|
||||
}
|
||||
}
|
||||
textWidth = display->getStringWidth(regionradiopreset);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "main.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "mesh/RadioLibInterface.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
@@ -25,6 +26,7 @@
|
||||
#include "modules/TraceRouteModule.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
@@ -159,31 +161,20 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard: without a reboot, reconfigure() applies the region directly.
|
||||
// Reject LORA_24 on sub-GHz-only hardware — getRadio() used to catch this post-reboot.
|
||||
if (selectedRegion == meshtastic_Config_LoRaConfig_RegionCode_LORA_24 &&
|
||||
!(RadioLibInterface::instance && RadioLibInterface::instance->wideLora())) {
|
||||
LOG_WARN("Radio hardware does not support 2.4 GHz; ignoring LORA_24 selection");
|
||||
return;
|
||||
}
|
||||
|
||||
config.lora.region = selectedRegion;
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
|
||||
// FIXME: This should be a method consolidated with the same logic in the admin message as well
|
||||
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (!owner.is_licensed) {
|
||||
bool keygenSuccess = false;
|
||||
if (config.security.private_key.size == 32) {
|
||||
// public key is derived from private, so this will always have the same result.
|
||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||
keygenSuccess = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_INFO("Generate new PKI keys");
|
||||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||
keygenSuccess = true;
|
||||
}
|
||||
if (keygenSuccess) {
|
||||
config.security.public_key.size = 32;
|
||||
config.security.private_key.size = 32;
|
||||
owner.public_key.size = 32;
|
||||
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||
}
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
}
|
||||
#endif
|
||||
config.lora.tx_enabled = true;
|
||||
@@ -199,7 +190,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
}
|
||||
|
||||
service->reloadConfig(changes);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
});
|
||||
|
||||
bannerOptions.durationMs = duration;
|
||||
@@ -265,13 +255,24 @@ void menuHandler::FrequencySlotPicker()
|
||||
optionsEnumArray[options++] = 0;
|
||||
|
||||
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
||||
|
||||
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
|
||||
: bwCodeToKHz(loraConfig.bandwidth);
|
||||
|
||||
uint32_t numChannels = 0;
|
||||
if (myRegion) {
|
||||
numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0)));
|
||||
// Match RadioInterface::applyModemConfig(): include padding, add spacing in numerator, and use round()
|
||||
const double spacing = myRegion->profile->spacing;
|
||||
const double padding = myRegion->profile->padding;
|
||||
const double channelBandwidthMHz = bw / 1000.0;
|
||||
const double numerator = (myRegion->freqEnd - myRegion->freqStart) + spacing;
|
||||
const double denominator = spacing + (padding * 2) + channelBandwidthMHz;
|
||||
if (denominator > 0.0) {
|
||||
numChannels = static_cast<uint32_t>(round(numerator / denominator));
|
||||
} else {
|
||||
LOG_WARN("Invalid region configuration: non-positive channel spacing/width");
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Region not set, cannot calculate number of channels");
|
||||
return;
|
||||
@@ -307,7 +308,6 @@ void menuHandler::FrequencySlotPicker()
|
||||
|
||||
config.lora.channel_num = selected;
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
};
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -346,7 +346,6 @@ void menuHandler::radioPresetPicker()
|
||||
config.lora.channel_num = 0; // Reset to default channel for the preset
|
||||
config.lora.override_frequency = 0; // Clear any custom frequency
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
});
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
|
||||
@@ -10,7 +10,7 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = {
|
||||
0xE0, // Number of chars: 224
|
||||
|
||||
// Jump Table:
|
||||
0xFF, 0xFF, 0x00, 0x0A, // 32
|
||||
0xFF, 0xFF, 0x00, 0x03, // 32
|
||||
0x00, 0x00, 0x04, 0x03, // 33
|
||||
0x00, 0x04, 0x05, 0x04, // 34
|
||||
0x00, 0x09, 0x09, 0x06, // 35
|
||||
@@ -1766,4 +1766,4 @@ const uint8_t ArialMT_Plain_24_RU[] PROGMEM = {
|
||||
0x3F, // 255
|
||||
};
|
||||
|
||||
#endif // OLED_RU
|
||||
#endif // OLED_RU
|
||||
|
||||
@@ -9,7 +9,7 @@ const uint8_t ArialMT_Plain_10_UA[] PROGMEM = {
|
||||
0x20, // First char: 32
|
||||
0xE0, // Number of chars: 224
|
||||
// Jump Table:
|
||||
0xFF, 0xFF, 0x00, 0x0A, // 32
|
||||
0xFF, 0xFF, 0x00, 0x03, // 32
|
||||
0x00, 0x00, 0x04, 0x03, // 33
|
||||
0x00, 0x04, 0x05, 0x04, // 34
|
||||
0x00, 0x09, 0x09, 0x06, // 35
|
||||
@@ -1924,4 +1924,4 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = {
|
||||
0xFF, // 1103
|
||||
};
|
||||
|
||||
#endif // OLED_UA
|
||||
#endif // OLED_UA
|
||||
|
||||
@@ -177,24 +177,8 @@ static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region)
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (!owner.is_licensed) {
|
||||
bool keygenSuccess = false;
|
||||
|
||||
if (config.security.private_key.size == 32) {
|
||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||
keygenSuccess = true;
|
||||
}
|
||||
} else {
|
||||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||
keygenSuccess = true;
|
||||
}
|
||||
|
||||
if (keygenSuccess) {
|
||||
config.security.public_key.size = 32;
|
||||
config.security.private_key.size = 32;
|
||||
owner.public_key.size = 32;
|
||||
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||
}
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
DeviceScreen *deviceScreen = nullptr;
|
||||
|
||||
#ifndef TFT_TASK_STACK_SIZE
|
||||
#define TFT_TASK_STACK_SIZE 16384
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when the system is entering light sleep
|
||||
CallbackObserver<DeviceScreen, void *> tftSleepObserver =
|
||||
@@ -127,7 +131,7 @@ void tftSetup(void)
|
||||
#ifdef ARCH_ESP32
|
||||
tftSleepObserver.observe(¬ifyLightSleep);
|
||||
endSleepObserver.observe(¬ifyLightSleepEnd);
|
||||
xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0);
|
||||
xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
std::thread *tft_task = new std::thread([] { tft_task_handler(); });
|
||||
#endif
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
#if defined(BUTTON_PIN_TOUCH)
|
||||
ButtonThread *TouchButtonThread = nullptr;
|
||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||
#if defined(PIN_EINK_EN)
|
||||
static bool touchBacklightWasOn = false;
|
||||
static bool touchBacklightActive = false;
|
||||
#endif
|
||||
@@ -220,8 +220,8 @@ void InputBroker::Init()
|
||||
};
|
||||
touchConfig.singlePress = INPUT_BROKER_NONE;
|
||||
touchConfig.longPress = INPUT_BROKER_BACK;
|
||||
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
|
||||
// On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds
|
||||
#if defined(PIN_EINK_EN)
|
||||
// Touch pad drives the backlight on devices with e-ink backlight pin
|
||||
touchConfig.longPress = INPUT_BROKER_NONE;
|
||||
touchConfig.suppressLeadUpSound = true;
|
||||
touchConfig.onPress = []() {
|
||||
|
||||
@@ -61,6 +61,33 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user)
|
||||
{
|
||||
if (user.is_licensed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool keygenSuccess = false;
|
||||
if (security.private_key.size == 32) {
|
||||
if (regeneratePublicKey(security.public_key.bytes, security.private_key.bytes)) {
|
||||
keygenSuccess = true;
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Generate new PKI keys");
|
||||
generateKeyPair(security.public_key.bytes, security.private_key.bytes);
|
||||
keygenSuccess = true;
|
||||
}
|
||||
|
||||
if (keygenSuccess) {
|
||||
security.public_key.size = 32;
|
||||
security.private_key.size = 32;
|
||||
user.public_key.size = 32;
|
||||
memcpy(user.public_key.bytes, security.public_key.bytes, 32);
|
||||
}
|
||||
|
||||
return keygenSuccess;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,7 @@ class CryptoEngine
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN)
|
||||
virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey);
|
||||
virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey);
|
||||
virtual bool ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user);
|
||||
|
||||
#endif
|
||||
void setDHPrivateKey(uint8_t *_private_key);
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
#define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour
|
||||
#define min_neighbor_info_broadcast_secs 4 * 60 * 60
|
||||
#define default_map_publish_interval_secs 60 * 60
|
||||
|
||||
// Traffic management defaults
|
||||
#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells
|
||||
#define default_traffic_mgmt_position_min_interval_secs ONE_DAY // 1 day between identical positions
|
||||
|
||||
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
||||
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
||||
#else
|
||||
|
||||
@@ -91,10 +91,27 @@ void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (nodeDB)
|
||||
nodeDB->updateFrom(*p);
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
if (traceRouteModule && p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
||||
// If we got a packet that is not decoded, try to decode it so we can check for traceroute.
|
||||
auto decodedState = perhapsDecode(const_cast<meshtastic_MeshPacket *>(p));
|
||||
if (decodedState == DecodeState::DECODE_SUCCESS) {
|
||||
// parsing was successful, print for debugging
|
||||
printPacket("reprocessPacket(DUP)", p);
|
||||
} else {
|
||||
// Fatal decoding error, we can't do anything with this packet
|
||||
LOG_WARN(
|
||||
"FloodingRouter::reprocessPacket: Fatal decode error (state=%d, id=0x%08x, from=%u), can't check for traceroute",
|
||||
static_cast<int>(decodedState), p->id, getFrom(p));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP)
|
||||
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) {
|
||||
traceRouteModule->processUpgradedPacket(*p);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,11 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
||||
err = lora.setOutputPower(power);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
|
||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("LR11x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
||||
|
||||
startReceive(); // restart receiving
|
||||
|
||||
return RADIOLIB_ERR_NONE;
|
||||
|
||||
@@ -173,14 +173,16 @@ void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
|
||||
#endif
|
||||
#elif defined(USE_KCT8103L_PA)
|
||||
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
|
||||
if (lna_enabled) {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
|
||||
} else {
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
|
||||
}
|
||||
#if defined(ARCH_ESP32)
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
|
||||
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void LoRaFEMInterface::setLNAEnable(bool enabled)
|
||||
|
||||
+37
-3
@@ -5,18 +5,52 @@
|
||||
#include "PointerQueue.h"
|
||||
#include "configuration.h"
|
||||
|
||||
// Sentinel marking the end of a modem preset array
|
||||
static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
||||
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
|
||||
|
||||
// Region profile: bundles the preset list with regulatory parameters shared across regions
|
||||
struct RegionProfile {
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default
|
||||
float spacing; // gaps between radio channels
|
||||
float padding; // padding at each side of the "operating channel"
|
||||
bool audioPermitted;
|
||||
bool licensedOnly; // a region profile for licensed operators only
|
||||
int8_t textThrottle; // throttle for text - future expansion
|
||||
int8_t positionThrottle; // throttle for location data - future expansion
|
||||
int8_t telemetryThrottle; // throttle for telemetry - future expansion
|
||||
uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place
|
||||
};
|
||||
|
||||
extern const RegionProfile PROFILE_STD;
|
||||
extern const RegionProfile PROFILE_EU868;
|
||||
extern const RegionProfile PROFILE_UNDEF;
|
||||
// extern const RegionProfile PROFILE_LITE;
|
||||
// extern const RegionProfile PROFILE_NARROW;
|
||||
// extern const RegionProfile PROFILE_HAM;
|
||||
|
||||
// Map from old region names to new region enums
|
||||
struct RegionInfo {
|
||||
meshtastic_Config_LoRaConfig_RegionCode code;
|
||||
float freqStart;
|
||||
float freqEnd;
|
||||
float dutyCycle;
|
||||
float spacing;
|
||||
float dutyCycle; // modified by getEffectiveDutyCycle
|
||||
uint8_t powerLimit; // Or zero for not set
|
||||
bool audioPermitted;
|
||||
bool freqSwitching;
|
||||
bool wideLora;
|
||||
const RegionProfile *profile;
|
||||
const char *name; // EU433 etc
|
||||
|
||||
// Preset accessors (delegate through profile)
|
||||
meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return profile->presets[0]; }
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *getAvailablePresets() const { return profile->presets; }
|
||||
size_t getNumPresets() const
|
||||
{
|
||||
size_t n = 0;
|
||||
while (profile->presets[n] != MODEM_PRESET_END)
|
||||
n++;
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
extern const RegionInfo regions[];
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
#include "modules/TraceRouteModule.h"
|
||||
#endif
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#include "modules/TrafficManagementModule.h"
|
||||
#endif
|
||||
#include "NodeDB.h"
|
||||
|
||||
NextHopRouter::NextHopRouter() {}
|
||||
@@ -126,15 +129,28 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
/* Check if we should be rebroadcasting this packet if so, do so. */
|
||||
bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) {
|
||||
// Check if traffic management wants to exhaust this packet's hops
|
||||
bool exhaustHops = false;
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
if (trafficManagementModule && trafficManagementModule->shouldExhaustHops(*p)) {
|
||||
exhaustHops = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Allow rebroadcast if hop_limit > 0 OR if we're exhausting hops (which sets hop_limit = 0 but still needs one relay)
|
||||
if (!isToUs(p) && !isFromUs(p) && (p->hop_limit > 0 || exhaustHops)) {
|
||||
if (p->id != 0) {
|
||||
if (isRebroadcaster()) {
|
||||
if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) {
|
||||
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
|
||||
LOG_INFO("Rebroadcast received message coming from %x", p->relay_node);
|
||||
|
||||
// Use shared logic to determine if hop_limit should be decremented
|
||||
if (shouldDecrementHopLimit(p)) {
|
||||
// If exhausting hops, force hop_limit = 0 regardless of other logic
|
||||
if (exhaustHops) {
|
||||
tosend->hop_limit = 0;
|
||||
LOG_INFO("Traffic management: exhausting hops for 0x%08x, setting hop_limit=0", getFrom(p));
|
||||
} else if (shouldDecrementHopLimit(p)) {
|
||||
// Use shared logic to determine if hop_limit should be decremented
|
||||
tosend->hop_limit--; // bump down the hop count
|
||||
} else {
|
||||
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit");
|
||||
|
||||
+36
-1
@@ -1299,7 +1299,7 @@ void NodeDB::loadFromDisk()
|
||||
// Coerce LoRa config fields derived from presets while bootstrapping.
|
||||
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
|
||||
if (config.has_lora && config.lora.use_preset) {
|
||||
RadioInterface::bootstrapLoRaConfigFromPreset(config.lora);
|
||||
RadioInterface::clampConfigLora(config.lora);
|
||||
}
|
||||
|
||||
#if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED
|
||||
@@ -1650,6 +1650,25 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p)
|
||||
return delta;
|
||||
}
|
||||
|
||||
HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p)
|
||||
{
|
||||
// Guard against invalid values.
|
||||
if (p.hop_start < p.hop_limit)
|
||||
return HopStartStatus::INVALID;
|
||||
|
||||
if (p.hop_start == 0) {
|
||||
// Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a
|
||||
// bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware
|
||||
// version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as
|
||||
// the bitfield is encrypted under the channel encryption key.
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)
|
||||
return HopStartStatus::VALID;
|
||||
return HopStartStatus::MISSING_OR_UNKNOWN;
|
||||
}
|
||||
|
||||
return HopStartStatus::VALID;
|
||||
}
|
||||
|
||||
int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown)
|
||||
{
|
||||
// Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a
|
||||
@@ -1687,6 +1706,22 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
|
||||
#include "MeshModule.h"
|
||||
#include "Throttle.h"
|
||||
|
||||
static constexpr uint32_t HOPSTART_DROP_LOG_INTERVAL_MS = 15000;
|
||||
|
||||
void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context)
|
||||
{
|
||||
static uint32_t lastLogMs = 0;
|
||||
if (Throttle::isWithinTimespanMs(lastLogMs, HOPSTART_DROP_LOG_INTERVAL_MS)) {
|
||||
return;
|
||||
}
|
||||
lastLogMs = millis();
|
||||
const bool decoded = (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag);
|
||||
const bool hasBitfield = decoded && p.decoded.has_bitfield;
|
||||
LOG_DEBUG(
|
||||
"Drop packet (%s): hop_start invalid/missing (from=0x%x id=%u hop_start=%u hop_limit=%u decoded=%d has_bitfield=%d)",
|
||||
context ? context : "unknown", p.from, p.id, p.hop_start, p.hop_limit, decoded, hasBitfield);
|
||||
}
|
||||
|
||||
/** Update position info for this node based on received position data
|
||||
*/
|
||||
void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src)
|
||||
|
||||
@@ -114,6 +114,27 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p);
|
||||
/// Returns defaultIfUnknown if the number of hops couldn't be determined.
|
||||
int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1);
|
||||
|
||||
enum class HopStartStatus : uint8_t { VALID = 0, MISSING_OR_UNKNOWN, INVALID };
|
||||
|
||||
/// Classify hop_start validity for forwarding decisions.
|
||||
HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p);
|
||||
|
||||
inline bool shouldDropPacketForPreHop(const meshtastic_MeshPacket &p)
|
||||
{
|
||||
#if !MESHTASTIC_PREHOP_DROP
|
||||
(void)p;
|
||||
return false;
|
||||
#else
|
||||
if (isFromUs(&p)) {
|
||||
return false; // local-originated packets should never be dropped by pre-hop drop policy
|
||||
}
|
||||
return classifyHopStart(p) != HopStartStatus::VALID;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Rate-limited debug log when hop_start is invalid/missing and packet is dropped.
|
||||
void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context);
|
||||
|
||||
enum LoadFileResult {
|
||||
// Successfully opened the file
|
||||
LOAD_SUCCESS = 1,
|
||||
|
||||
@@ -449,6 +449,11 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_traffic_management_tag:
|
||||
LOG_DEBUG("Send module config: traffic management");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("Unknown module config type %d", config_state);
|
||||
}
|
||||
|
||||
+307
-134
@@ -21,6 +21,7 @@
|
||||
#include <assert.h>
|
||||
#include <pb_decode.h>
|
||||
#include <pb_encode.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
@@ -32,10 +33,32 @@
|
||||
#include "STM32WLE5JCInterface.h"
|
||||
#endif
|
||||
|
||||
#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
||||
MODEM_PRESET_END};
|
||||
|
||||
// Region profiles: bundle preset list + regulatory parameters shared across regions
|
||||
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot
|
||||
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0};
|
||||
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0};
|
||||
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0};
|
||||
|
||||
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \
|
||||
{ \
|
||||
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \
|
||||
frequency_switching, wide_lora, #name \
|
||||
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \
|
||||
wide_lora, &profile_ptr, #name \
|
||||
}
|
||||
|
||||
const RegionInfo regions[] = {
|
||||
@@ -43,7 +66,7 @@ const RegionInfo regions[] = {
|
||||
https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf
|
||||
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
|
||||
*/
|
||||
RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false),
|
||||
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
|
||||
@@ -51,8 +74,7 @@ const RegionInfo regions[] = {
|
||||
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf
|
||||
FIXME: https://github.com/meshtastic/firmware/issues/3371
|
||||
*/
|
||||
RDEF(EU_433, 433.0f, 434.0f, 10, 0, 10, true, false, false),
|
||||
|
||||
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD),
|
||||
/*
|
||||
https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/
|
||||
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
|
||||
@@ -67,33 +89,33 @@ const RegionInfo regions[] = {
|
||||
AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.)
|
||||
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf
|
||||
*/
|
||||
RDEF(EU_868, 869.4f, 869.65f, 10, 0, 27, false, false, false),
|
||||
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
*/
|
||||
RDEF(CN, 470.0f, 510.0f, 100, 0, 19, true, false, false),
|
||||
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf
|
||||
https://qiita.com/ammo0613/items/d952154f1195b64dc29f
|
||||
*/
|
||||
RDEF(JP, 920.5f, 923.5f, 100, 0, 13, true, false, false),
|
||||
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf
|
||||
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
|
||||
Also used in Brazil.
|
||||
*/
|
||||
RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false),
|
||||
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions
|
||||
AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence
|
||||
NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100
|
||||
*/
|
||||
RDEF(ANZ_433, 433.05f, 434.79f, 100, 0, 14, true, false, false),
|
||||
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf
|
||||
@@ -101,13 +123,13 @@ const RegionInfo regions[] = {
|
||||
Note:
|
||||
- We do LBT, so 100% is allowed.
|
||||
*/
|
||||
RDEF(RU, 868.7f, 869.2f, 100, 0, 20, true, false, false),
|
||||
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0
|
||||
https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters
|
||||
*/
|
||||
RDEF(KR, 920.0f, 923.0f, 100, 0, 23, true, false, false),
|
||||
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor.
|
||||
@@ -115,42 +137,38 @@ const RegionInfo regions[] = {
|
||||
https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF
|
||||
https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283
|
||||
*/
|
||||
RDEF(TW, 920.0f, 925.0f, 100, 0, 27, true, false, false),
|
||||
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
*/
|
||||
RDEF(IN, 865.0f, 867.0f, 100, 0, 30, true, false, false),
|
||||
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752
|
||||
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
|
||||
*/
|
||||
RDEF(NZ_865, 864.0f, 868.0f, 100, 0, 36, true, false, false),
|
||||
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
*/
|
||||
RDEF(TH, 920.0f, 925.0f, 100, 0, 16, true, false, false),
|
||||
RDEF(TH, 920.0f, 925.0f, 100, 16, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
433,05-434,7 Mhz 10 mW
|
||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
||||
*/
|
||||
RDEF(UA_433, 433.0f, 434.7f, 10, 0, 10, true, false, false),
|
||||
|
||||
/*
|
||||
868,0-868,6 Mhz 25 mW
|
||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
||||
*/
|
||||
RDEF(UA_868, 868.0f, 868.6f, 1, 0, 14, true, false, false),
|
||||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD),
|
||||
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Malaysia
|
||||
433 - 435 MHz at 100mW, no restrictions.
|
||||
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
|
||||
*/
|
||||
RDEF(MY_433, 433.0f, 435.0f, 100, 0, 20, true, false, false),
|
||||
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Malaysia
|
||||
@@ -159,14 +177,14 @@ const RegionInfo regions[] = {
|
||||
Frequency hopping is used for 919 - 923 MHz.
|
||||
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
|
||||
*/
|
||||
RDEF(MY_919, 919.0f, 924.0f, 100, 0, 27, true, true, false),
|
||||
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Singapore
|
||||
SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions.
|
||||
https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf
|
||||
*/
|
||||
RDEF(SG_923, 917.0f, 925.0f, 100, 0, 20, true, false, false),
|
||||
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Philippines
|
||||
@@ -176,8 +194,9 @@ const RegionInfo regions[] = {
|
||||
https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135
|
||||
*/
|
||||
|
||||
RDEF(PH_433, 433.0f, 434.7f, 100, 0, 10, true, false, false), RDEF(PH_868, 868.0f, 869.4f, 100, 0, 14, true, false, false),
|
||||
RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false),
|
||||
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD),
|
||||
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD),
|
||||
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Kazakhstan
|
||||
@@ -185,37 +204,38 @@ const RegionInfo regions[] = {
|
||||
863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields
|
||||
https://github.com/meshtastic/firmware/issues/7204
|
||||
*/
|
||||
RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false),
|
||||
RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false),
|
||||
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD),
|
||||
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Nepal
|
||||
865 MHz to 868 MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use,
|
||||
specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf
|
||||
*/
|
||||
RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false),
|
||||
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
Brazil
|
||||
902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
|
||||
https://github.com/meshtastic/firmware/issues/3741
|
||||
*/
|
||||
RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false),
|
||||
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD),
|
||||
|
||||
/*
|
||||
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
|
||||
*/
|
||||
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 0, 10, true, false, true),
|
||||
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD),
|
||||
|
||||
/*
|
||||
This needs to be last. Same as US.
|
||||
*/
|
||||
RDEF(UNSET, 902.0f, 928.0f, 100, 0, 30, true, false, false)
|
||||
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF)
|
||||
|
||||
};
|
||||
|
||||
const RegionInfo *myRegion;
|
||||
bool RadioInterface::uses_default_frequency_slot = true;
|
||||
bool RadioInterface::uses_custom_channel_name = false;
|
||||
|
||||
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
|
||||
|
||||
@@ -501,45 +521,14 @@ void initRegion()
|
||||
myRegion = r;
|
||||
}
|
||||
|
||||
void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig)
|
||||
const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||||
{
|
||||
if (!loraConfig.use_preset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find region info to determine whether "wide" LoRa is permitted (2.4 GHz uses wider bandwidth codes).
|
||||
const RegionInfo *r = regions;
|
||||
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; r++)
|
||||
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != code; r++)
|
||||
;
|
||||
|
||||
const bool regionWideLora = r->wideLora;
|
||||
|
||||
float bwKHz = 0;
|
||||
uint8_t sf = 0;
|
||||
uint8_t cr = 0;
|
||||
modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr);
|
||||
|
||||
// If selected preset requests a bandwidth larger than the region span, fall back to LONG_FAST.
|
||||
if (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && (r->freqEnd - r->freqStart) < (bwKHz / 1000.0f)) {
|
||||
loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr);
|
||||
}
|
||||
|
||||
loraConfig.bandwidth = bwKHzToCode(bwKHz);
|
||||
loraConfig.spread_factor = sf;
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* ## LoRaWAN for North America
|
||||
|
||||
LoRaWAN defines 64, 125 kHz channels from 902.3 to 914.9 MHz increments.
|
||||
|
||||
The maximum output power for North America is +30 dBM.
|
||||
|
||||
The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are
|
||||
separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency.
|
||||
*/
|
||||
|
||||
uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received)
|
||||
{
|
||||
uint32_t pl = 0;
|
||||
@@ -749,7 +738,7 @@ void RadioInterface::saveFreq(float freq)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save our channel for later reuse.
|
||||
* Save our frequency slot (aka channel) for later reuse.
|
||||
*/
|
||||
void RadioInterface::saveChannelNum(uint32_t channel_num)
|
||||
{
|
||||
@@ -772,113 +761,297 @@ uint32_t RadioInterface::getChannelNum()
|
||||
return savedChannelNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an error-level client notification. Safe to call when service is null (e.g. in tests).
|
||||
*/
|
||||
static void sendErrorNotification(const char *msg)
|
||||
{
|
||||
if (!service)
|
||||
return;
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
if (!cn)
|
||||
return;
|
||||
cn->level = meshtastic_LogRecord_Level_ERROR;
|
||||
snprintf(cn->message, sizeof(cn->message), "%s", msg);
|
||||
service->sendClientNotification(cn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a region is valid for the current settings.
|
||||
* Returns false if not compatible.
|
||||
*/
|
||||
bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig)
|
||||
{
|
||||
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
||||
|
||||
// If you are not licensed, you can't use ham regions.
|
||||
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) {
|
||||
char err_string[160];
|
||||
snprintf(err_string, sizeof(err_string), "Region %s requires licensed mode", newRegion->name);
|
||||
LOG_ERROR("%s", err_string);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
sendErrorNotification(err_string);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper: validate or clamp a LoRa config against its region.
|
||||
* When clamp==false, returns false on first error (pure validation).
|
||||
* When clamp==true, fixes invalid settings in-place and returns true.
|
||||
*/
|
||||
bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp)
|
||||
{
|
||||
char err_string[160];
|
||||
float check_bw;
|
||||
|
||||
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
||||
|
||||
const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
|
||||
|
||||
// Check preset validity (only when use_preset is true)
|
||||
if (loraConfig.use_preset) {
|
||||
check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora);
|
||||
|
||||
bool preset_valid = false;
|
||||
for (size_t i = 0; i < newRegion->getNumPresets(); i++) {
|
||||
if (loraConfig.modem_preset == newRegion->getAvailablePresets()[i]) {
|
||||
preset_valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!preset_valid) {
|
||||
const char *defaultName = DisplayFormatters::getModemPresetDisplayName(newRegion->getDefaultPreset(), false, true);
|
||||
if (clamp) {
|
||||
snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s, using %s", presetName, newRegion->name,
|
||||
defaultName);
|
||||
} else {
|
||||
snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s", presetName, newRegion->name);
|
||||
}
|
||||
LOG_ERROR("%s", err_string);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
sendErrorNotification(err_string);
|
||||
|
||||
if (clamp) {
|
||||
loraConfig.modem_preset = newRegion->getDefaultPreset();
|
||||
check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
check_bw = bwCodeToKHz(loraConfig.bandwidth);
|
||||
}
|
||||
|
||||
// Calculate width of slots (aka channels) based on bandwidth and any spacing or padding required by the region:
|
||||
// spacing = gap between slots (0 for continuous spectrum) and at the beginning of the band
|
||||
// padding = gap at the beginning and end of the slots (0 for no padding)
|
||||
float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz
|
||||
uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth);
|
||||
|
||||
// Check if the region supports the requested bandwidth
|
||||
if ((newRegion->freqEnd - newRegion->freqStart) < freqSlotWidth) {
|
||||
const float regionSpanKHz = (newRegion->freqEnd - newRegion->freqStart) * 1000.0f;
|
||||
snprintf(err_string, sizeof(err_string), "%s span %.0fkHz < requested %.0fkHz", newRegion->name, regionSpanKHz, check_bw);
|
||||
LOG_ERROR("%s", err_string);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
sendErrorNotification(err_string);
|
||||
|
||||
if (clamp) {
|
||||
loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora));
|
||||
check_bw = bwCodeToKHz(loraConfig.bandwidth);
|
||||
|
||||
// Recompute slot width and number of slots based on the new bandwidth
|
||||
freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz
|
||||
numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||||
const char *presetNameDisplay =
|
||||
DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
|
||||
uint32_t channelNameHashSlot = hash(channelName) % numFreqSlots;
|
||||
uint32_t presetNameHashSlot = hash(presetNameDisplay) % numFreqSlots;
|
||||
|
||||
if (loraConfig.override_frequency == 0) {
|
||||
|
||||
// Check if we use the default frequency slot
|
||||
uses_default_frequency_slot =
|
||||
(loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default
|
||||
(newRegion->profile->overrideSlot != 0 &&
|
||||
loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches override
|
||||
((newRegion->profile->overrideSlot == 0) &&
|
||||
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset hash, no override
|
||||
|
||||
// check if user setting different to preset name
|
||||
uses_custom_channel_name = (strcmp(channelName, presetNameDisplay) != 0);
|
||||
|
||||
if (loraConfig.channel_num > numFreqSlots) {
|
||||
snprintf(err_string, sizeof(err_string), "Channel number %u invalid for %s, max is %u", loraConfig.channel_num,
|
||||
newRegion->name, numFreqSlots);
|
||||
LOG_ERROR("%s", err_string);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
sendErrorNotification(err_string);
|
||||
|
||||
if (clamp) {
|
||||
if (uses_custom_channel_name) { // clamp to channel name hash
|
||||
loraConfig.channel_num =
|
||||
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
|
||||
} else if ((loraConfig.use_preset) && (newRegion->profile->overrideSlot != 0)) { // clamp to preset override slot
|
||||
loraConfig.channel_num =
|
||||
newRegion->profile->overrideSlot; // use the override slot specified by the region profile
|
||||
uses_default_frequency_slot = true;
|
||||
} else if (loraConfig.use_preset) { // clamp to preset slot
|
||||
loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
|
||||
uses_default_frequency_slot = true;
|
||||
} else { // if not using preset, and no custom channel name, just clamp to default anyway
|
||||
uses_default_frequency_slot = true;
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} // end of channel number check
|
||||
} else {
|
||||
// if we have a frequency override, we ignore the channel number and just use the override frequency
|
||||
snprintf(err_string, sizeof(err_string), "Frequency override in place, using %.3f", loraConfig.override_frequency);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RadioInterface::validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig)
|
||||
{
|
||||
auto copy = loraConfig;
|
||||
return checkOrClampConfigLora(copy, false);
|
||||
}
|
||||
|
||||
void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig)
|
||||
{
|
||||
checkOrClampConfigLora(loraConfig, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull our channel settings etc... from protobufs to the dumb interface settings
|
||||
* Note: this must be given only settings which have been validated or clamped!
|
||||
*/
|
||||
void RadioInterface::applyModemConfig()
|
||||
{
|
||||
// Set up default configuration
|
||||
// No Sync Words in LORA mode
|
||||
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||
bool validConfig = false; // We need to check for a valid configuration
|
||||
while (!validConfig) {
|
||||
if (loraConfig.use_preset) {
|
||||
modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr);
|
||||
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) {
|
||||
cr = loraConfig.coding_rate;
|
||||
LOG_INFO("Using custom Coding Rate %u", cr);
|
||||
}
|
||||
} else {
|
||||
sf = loraConfig.spread_factor;
|
||||
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
||||
myRegion = newRegion;
|
||||
|
||||
if (loraConfig.use_preset) {
|
||||
if (!validateConfigLora(loraConfig)) {
|
||||
loraConfig.modem_preset = newRegion->getDefaultPreset();
|
||||
}
|
||||
uint8_t newcr;
|
||||
modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr);
|
||||
// If custom CR is being used already, check if the new preset is higher
|
||||
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate < newcr) {
|
||||
cr = newcr;
|
||||
LOG_INFO("Default Coding Rate is higher than custom setting, using %u", cr);
|
||||
}
|
||||
// If the custom CR is higher than the preset, use it
|
||||
else if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate > newcr) {
|
||||
cr = loraConfig.coding_rate;
|
||||
bw = bwCodeToKHz(loraConfig.bandwidth);
|
||||
}
|
||||
|
||||
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
|
||||
const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f;
|
||||
const float requestedBwKHz = bw;
|
||||
const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset
|
||||
const char *presetName =
|
||||
DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
|
||||
|
||||
char err_string[160];
|
||||
if (isWideRequest) {
|
||||
snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.",
|
||||
myRegion->name, presetName);
|
||||
} else {
|
||||
snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.",
|
||||
myRegion->name, regionSpanKHz, requestedBwKHz);
|
||||
}
|
||||
LOG_ERROR("%s", err_string);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_ERROR;
|
||||
snprintf(cn->message, sizeof(cn->message), "%s", err_string);
|
||||
service->sendClientNotification(cn);
|
||||
|
||||
// Set to default modem preset
|
||||
loraConfig.use_preset = true;
|
||||
loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
LOG_INFO("Using custom Coding Rate %u", cr);
|
||||
} else {
|
||||
validConfig = true;
|
||||
cr = loraConfig.coding_rate;
|
||||
}
|
||||
|
||||
} else { // if not using preset, then just use the custom settings
|
||||
if (validateConfigLora(loraConfig)) {
|
||||
} else {
|
||||
LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults",
|
||||
newRegion->name);
|
||||
clampConfigLora(loraConfig);
|
||||
}
|
||||
bw = bwCodeToKHz(loraConfig.bandwidth);
|
||||
sf = loraConfig.spread_factor;
|
||||
cr = loraConfig.coding_rate;
|
||||
}
|
||||
|
||||
power = loraConfig.tx_power;
|
||||
|
||||
if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed))
|
||||
power = myRegion->powerLimit;
|
||||
if ((power == 0) || ((power > newRegion->powerLimit) && !devicestate.owner.is_licensed))
|
||||
power = newRegion->powerLimit;
|
||||
|
||||
if (power == 0)
|
||||
power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults
|
||||
power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of newRegion defaults
|
||||
// to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this
|
||||
// variable set to 0 don't have a valid power limit)
|
||||
|
||||
// Set final tx_power back onto config
|
||||
loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger
|
||||
|
||||
// Calculate the number of channels
|
||||
uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000)));
|
||||
uint32_t channel_num;
|
||||
float freq;
|
||||
|
||||
// If user has manually specified a channel num, then use that, otherwise generate one by hashing the name
|
||||
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||||
// channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1)
|
||||
uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels;
|
||||
// Calculate number of frequency slots (aka Channels):
|
||||
// spacing = gap between channels (0 for continuous spectrum) and at the beginning of the band
|
||||
// padding = gap at the beginning and end of the channel (0 for no padding)
|
||||
float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (bw / 1000); // in MHz
|
||||
uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth);
|
||||
|
||||
// Check if we use the default frequency slot
|
||||
RadioInterface::uses_default_frequency_slot =
|
||||
channel_num ==
|
||||
hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels;
|
||||
|
||||
// Old frequency selection formula
|
||||
// float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num);
|
||||
|
||||
// New frequency selection formula
|
||||
float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000));
|
||||
// Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one.
|
||||
// Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to
|
||||
// (numFreqSlots - 1).
|
||||
uint32_t presetNameHashSlot =
|
||||
hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots;
|
||||
|
||||
// override if we have a verbatim frequency
|
||||
if (loraConfig.override_frequency) {
|
||||
freq = loraConfig.override_frequency;
|
||||
channel_num = -1;
|
||||
uses_default_frequency_slot = false;
|
||||
} else {
|
||||
|
||||
// If user has not manually specified a frequency slot, or has not specified one that is different than the default or the
|
||||
// override for the new region, then use the default or override. If the user has not specified one, but has specified a
|
||||
// custom channel name, then use the hash of that channel name to pick a frequency slot. Note that channel_num is actually
|
||||
// (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to (numFreqSlots - 1).
|
||||
// NB: channel_num is also know as frequency slot but it's too late to fix now.
|
||||
if (uses_default_frequency_slot) {
|
||||
// if there's an override slot, use that
|
||||
if (newRegion->profile->overrideSlot != 0) {
|
||||
channel_num = newRegion->profile->overrideSlot - 1;
|
||||
} else {
|
||||
channel_num = presetNameHashSlot;
|
||||
}
|
||||
} else { // use the manually defined one
|
||||
channel_num = loraConfig.channel_num - 1;
|
||||
}
|
||||
|
||||
// Calculate frequency: freqStart is band edge, add half bandwidth (plus optional padding) to get middle of first channel
|
||||
// subsequent channels are spaced by freqSlotWidth
|
||||
freq = newRegion->freqStart + (bw / 2000) + newRegion->profile->padding + (channel_num * freqSlotWidth); // in MHz
|
||||
}
|
||||
|
||||
saveChannelNum(channel_num);
|
||||
saveFreq(freq + loraConfig.frequency_offset);
|
||||
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||||
|
||||
slotTimeMsec = computeSlotTimeMsec();
|
||||
preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw);
|
||||
|
||||
LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset);
|
||||
LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset,
|
||||
LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", newRegion->name, channelName, loraConfig.modem_preset,
|
||||
channel_num, power);
|
||||
LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd,
|
||||
myRegion->freqEnd - myRegion->freqStart);
|
||||
LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw);
|
||||
LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd,
|
||||
newRegion->freqEnd - newRegion->freqStart);
|
||||
LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw);
|
||||
if (newRegion->profile->overrideSlot != 0) {
|
||||
LOG_INFO("Using region override slot: %d", newRegion->profile->overrideSlot);
|
||||
}
|
||||
LOG_INFO("channel_num: %d", channel_num + 1);
|
||||
LOG_INFO("frequency: %f", getFreq());
|
||||
LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec);
|
||||
}
|
||||
} // end of applyModemConfig
|
||||
|
||||
/** Slottime is the time to detect a transmission has started, consisting of:
|
||||
- CAD duration;
|
||||
@@ -992,4 +1165,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
|
||||
|
||||
sendingPacket = p;
|
||||
return p->encrypted.size + sizeof(PacketHeader);
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ class RadioInterface
|
||||
* Coerce LoRa config fields (bandwidth/spread_factor) derived from presets.
|
||||
* This is used during early bootstrapping so UIs that display these fields directly remain consistent.
|
||||
*/
|
||||
static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig);
|
||||
// static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); // maybe superseded?
|
||||
|
||||
/**
|
||||
* Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving)
|
||||
@@ -234,6 +234,20 @@ class RadioInterface
|
||||
// Whether we use the default frequency slot given our LoRa config (region and modem preset)
|
||||
static bool uses_default_frequency_slot;
|
||||
|
||||
// Whether we have a custom channel name
|
||||
static bool uses_custom_channel_name;
|
||||
|
||||
static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp);
|
||||
|
||||
// Check if a candidate region is compatible and valid.
|
||||
static bool validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig);
|
||||
|
||||
// Check if a candidate radio configuration is valid.
|
||||
static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig);
|
||||
|
||||
// Make a candidate radio configuration valid, even if it isn't.
|
||||
static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig);
|
||||
|
||||
protected:
|
||||
int8_t power = 17; // Set by applyModemConfig()
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#include "modules/TrafficManagementModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
#include "mqtt/MQTT.h"
|
||||
#endif
|
||||
@@ -95,6 +98,20 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
||||
return true;
|
||||
}
|
||||
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
// When router_preserve_hops is enabled, preserve hops for decoded packets that are not
|
||||
// position or telemetry (those have their own exhaust_hop controls).
|
||||
if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled &&
|
||||
moduleConfig.traffic_management.router_preserve_hops && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum != meshtastic_PortNum_POSITION_APP && p->decoded.portnum != meshtastic_PortNum_TELEMETRY_APP) {
|
||||
LOG_DEBUG("Router hop preserved: port=%d from=0x%08x (traffic_management)", p->decoded.portnum, getFrom(p));
|
||||
if (trafficManagementModule) {
|
||||
trafficManagementModule->recordRouterHopPreserved();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// For subsequent hops, check if previous relay is a favorite router
|
||||
// Optimized search for favorite routers with matching last byte
|
||||
// Check ordering optimized for IoT devices (cheapest checks first)
|
||||
@@ -858,6 +875,12 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldDropPacketForPreHop(*p)) {
|
||||
logHopStartDrop(*p, "pre-hop drop");
|
||||
packetPool.release(p);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldFilterReceived(p)) {
|
||||
LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from);
|
||||
packetPool.release(p);
|
||||
|
||||
@@ -253,6 +253,11 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
|
||||
LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
// Apply RX gain mode — valid in STDBY (datasheet §9.6), matches resetAGC() pattern
|
||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("SX126X setRxBoostedGainMode %s%d", radioLibErr, err);
|
||||
|
||||
startReceive(); // restart receiving
|
||||
|
||||
return RADIOLIB_ERR_NONE;
|
||||
|
||||
@@ -69,6 +69,22 @@ static inline int get_max_num_nodes()
|
||||
/// Max number of channels allowed
|
||||
#define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0]))
|
||||
|
||||
// Traffic Management module configuration
|
||||
// Enable per-variant by defining HAS_TRAFFIC_MANAGEMENT=1 in variant.h
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 0
|
||||
#endif
|
||||
|
||||
// Cache size for traffic management (number of nodes to track)
|
||||
// Can be overridden per-variant based on available memory
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000
|
||||
#else
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// helper function for encoding a record as a protobuf, any failures to encode are fatal and we will panic
|
||||
/// returns the encoded packet size
|
||||
size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct);
|
||||
@@ -90,4 +106,4 @@ bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count);
|
||||
*/
|
||||
bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count);
|
||||
|
||||
#define is_in_repeated(name, n) is_in_helper(n, name, name##_count)
|
||||
#define is_in_repeated(name, n) is_in_helper(n, name, name##_count)
|
||||
|
||||
+123
-93
@@ -24,7 +24,9 @@
|
||||
|
||||
#include "Default.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "mesh/RadioLibInterface.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
#include "mqtt/MQTT.h"
|
||||
@@ -197,10 +199,35 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
handleSetOwner(r->set_owner);
|
||||
break;
|
||||
|
||||
case meshtastic_AdminMessage_set_config_tag:
|
||||
case meshtastic_AdminMessage_set_config_tag: {
|
||||
LOG_DEBUG("Client set config");
|
||||
handleSetConfig(r->set_config);
|
||||
|
||||
// Non-LoRa configs need no further validation.
|
||||
if (r->set_config.which_payload_variant != meshtastic_Config_lora_tag) {
|
||||
LOG_DEBUG("Non-LoRa config, applying directly");
|
||||
handleSetConfig(r->set_config, fromOthers);
|
||||
break;
|
||||
}
|
||||
|
||||
// Only LORA_24 requires hardware capability validation.
|
||||
if (r->set_config.payload_variant.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) {
|
||||
LOG_DEBUG("LoRa config, region is not LORA_24, applying directly");
|
||||
handleSetConfig(r->set_config, fromOthers);
|
||||
break;
|
||||
}
|
||||
|
||||
// Hardware supports 2.4 GHz — apply the config.
|
||||
// Fail closed: null instance is treated as incapable.
|
||||
if (RadioLibInterface::instance && RadioLibInterface::instance->wideLora()) {
|
||||
LOG_DEBUG("LORA_24 requested, radio hardware supports 2.4 GHz, applying");
|
||||
handleSetConfig(r->set_config, fromOthers);
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_WARN("Radio hardware does not support 2.4 GHz; rejecting LORA_24 region");
|
||||
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
||||
break;
|
||||
}
|
||||
|
||||
case meshtastic_AdminMessage_set_module_config_tag:
|
||||
LOG_DEBUG("Client set module config");
|
||||
@@ -626,7 +653,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
||||
}
|
||||
}
|
||||
|
||||
void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
{
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
auto existingRole = config.device.role;
|
||||
@@ -768,18 +795,57 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
validatedLora.spread_factor = LORA_SF_DEFAULT;
|
||||
}
|
||||
|
||||
// If no lora radio parameters change, don't need to reboot
|
||||
if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region &&
|
||||
oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth &&
|
||||
oldLoraConfig.spread_factor == validatedLora.spread_factor &&
|
||||
oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power &&
|
||||
oldLoraConfig.frequency_offset == validatedLora.frequency_offset &&
|
||||
oldLoraConfig.override_frequency == validatedLora.override_frequency &&
|
||||
oldLoraConfig.channel_num == validatedLora.channel_num &&
|
||||
oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) {
|
||||
requiresReboot = false;
|
||||
// If we're setting a new region, check the region is valid and then init the region or discard the change
|
||||
if (validatedLora.region != myRegion->code) {
|
||||
// Region has changed so check whether it is valid for e.g. licensing conditions and if the lora config is valid
|
||||
if (RadioInterface::validateConfigRegion(validatedLora) && RadioInterface::validateConfigLora(validatedLora)) {
|
||||
// If we're setting region for the first time, init the region and regenerate the keys
|
||||
if (isRegionUnset && validatedLora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
}
|
||||
#endif
|
||||
// new region is valid and we're coming from an unset region, so enable tx
|
||||
validatedLora.tx_enabled = true;
|
||||
}
|
||||
// If we're unsetting the region for some reason, disable tx
|
||||
if (!isRegionUnset && validatedLora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
validatedLora.tx_enabled = false;
|
||||
}
|
||||
// Ensure initRegion() uses the newly validated region
|
||||
config.lora.region = validatedLora.region;
|
||||
initRegion();
|
||||
if (myRegion->dutyCycle < 100) {
|
||||
validatedLora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||||
}
|
||||
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
||||
// Default root is in use, so subscribe to the appropriate MQTT topic for this region
|
||||
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||
}
|
||||
changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG;
|
||||
} else {
|
||||
// Region validation has failed, so just copy all of the old config over the new config
|
||||
validatedLora = oldLoraConfig;
|
||||
}
|
||||
} // end of new region handling
|
||||
|
||||
if (!RadioInterface::validateConfigLora(validatedLora)) {
|
||||
if (fromOthers) {
|
||||
LOG_WARN("Invalid LoRa config received from another node, rejecting changes");
|
||||
// modem_preset set to use the old setting if the check fails
|
||||
validatedLora.modem_preset = oldLoraConfig.modem_preset;
|
||||
} else {
|
||||
LOG_WARN("Invalid LoRa config received from client, using corrected values");
|
||||
RadioInterface::clampConfigLora(validatedLora);
|
||||
}
|
||||
// use_preset and bandwidth are coerced into valid values by the check.
|
||||
}
|
||||
|
||||
// All LoRa radio changes apply live via configChanged observer → reconfigure().
|
||||
// reconfigure() puts the radio in standby, reprograms all modem parameters, and restarts receive.
|
||||
requiresReboot = false;
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
// If running on portduino and using SimRadio, do not require reboot
|
||||
if (SimRadio::instance) {
|
||||
@@ -795,63 +861,21 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
digitalWrite(RF95_FAN_EN, HIGH ^ 0);
|
||||
}
|
||||
#endif
|
||||
config.lora = validatedLora;
|
||||
|
||||
#if HAS_LORA_FEM
|
||||
// Apply FEM LNA mode from config (only meaningful on hardware that supports it)
|
||||
// Note that a rejected lora config will revert this as well.
|
||||
if (loraFEMInterface.isLnaCanControl()) {
|
||||
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED);
|
||||
} else if (config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT) {
|
||||
loraFEMInterface.setLNAEnable(validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED);
|
||||
} else if (validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT) {
|
||||
// Hardware FEM does not support LNA control; normalize stored config to match actual capability
|
||||
LOG_WARN("FEM LNA mode configured but current FEM does not support LNA control; normalizing to NOT_PRESENT");
|
||||
config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT;
|
||||
validatedLora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT;
|
||||
}
|
||||
#endif
|
||||
// If we're setting region for the first time, init the region and regenerate the keys
|
||||
if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (!owner.is_licensed) {
|
||||
bool keygenSuccess = false;
|
||||
if (config.security.private_key.size == 32) {
|
||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||
keygenSuccess = true;
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Generate new PKI keys");
|
||||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||
keygenSuccess = true;
|
||||
}
|
||||
if (keygenSuccess) {
|
||||
config.security.public_key.size = 32;
|
||||
config.security.private_key.size = 32;
|
||||
owner.public_key.size = 32;
|
||||
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
config.lora.tx_enabled = true;
|
||||
initRegion();
|
||||
if (myRegion->dutyCycle < 100) {
|
||||
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||||
}
|
||||
// Compare the entire string, we are sure of the length as a topic has never been set
|
||||
if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) {
|
||||
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||
changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG;
|
||||
}
|
||||
}
|
||||
if (config.lora.region != myRegion->code) {
|
||||
// Region has changed so check whether there is a regulatory one we should be using instead.
|
||||
// Additionally as a side-effect, assume a new value under myRegion
|
||||
initRegion();
|
||||
|
||||
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
||||
// Default root is in use, so subscribe to the appropriate MQTT topic for this region
|
||||
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||
}
|
||||
config.lora = validatedLora; // Finally, return the validated config back to the main config
|
||||
|
||||
changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_Config_bluetooth_tag:
|
||||
@@ -899,10 +923,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
}
|
||||
if (requiresReboot && !hasOpenEditTransaction) {
|
||||
disableBluetooth();
|
||||
}
|
||||
} // end of switch case which_payload_variant
|
||||
|
||||
saveChanges(changes, requiresReboot);
|
||||
}
|
||||
} // end of handleSetConfig
|
||||
|
||||
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
{
|
||||
@@ -1008,6 +1032,11 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
moduleConfig.statusmessage = c.payload_variant.statusmessage;
|
||||
shouldReboot = false;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_traffic_management_tag:
|
||||
LOG_INFO("Set module config: Traffic Management");
|
||||
moduleConfig.has_traffic_management = true;
|
||||
moduleConfig.traffic_management = c.payload_variant.traffic_management;
|
||||
break;
|
||||
}
|
||||
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
|
||||
return true;
|
||||
@@ -1122,78 +1151,85 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
||||
meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default;
|
||||
|
||||
if (req.decoded.want_response) {
|
||||
const char *configName = "?";
|
||||
switch (configType) {
|
||||
case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG:
|
||||
LOG_INFO("Get module config: MQTT");
|
||||
configName = "MQTT";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag;
|
||||
res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG:
|
||||
LOG_INFO("Get module config: Serial");
|
||||
configName = "Serial";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag;
|
||||
res.get_module_config_response.payload_variant.serial = moduleConfig.serial;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG:
|
||||
LOG_INFO("Get module config: External Notification");
|
||||
configName = "External Notification";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag;
|
||||
res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG:
|
||||
LOG_INFO("Get module config: Store & Forward");
|
||||
configName = "Store & Forward";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag;
|
||||
res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG:
|
||||
LOG_INFO("Get module config: Range Test");
|
||||
configName = "Range Test";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag;
|
||||
res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG:
|
||||
LOG_INFO("Get module config: Telemetry");
|
||||
configName = "Telemetry";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag;
|
||||
res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG:
|
||||
LOG_INFO("Get module config: Canned Message");
|
||||
configName = "Canned Message";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag;
|
||||
res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG:
|
||||
LOG_INFO("Get module config: Audio");
|
||||
configName = "Audio";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag;
|
||||
res.get_module_config_response.payload_variant.audio = moduleConfig.audio;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG:
|
||||
LOG_INFO("Get module config: Remote Hardware");
|
||||
configName = "Remote Hardware";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag;
|
||||
res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG:
|
||||
LOG_INFO("Get module config: Neighbor Info");
|
||||
configName = "Neighbor Info";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag;
|
||||
res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG:
|
||||
LOG_INFO("Get module config: Detection Sensor");
|
||||
configName = "Detection Sensor";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag;
|
||||
res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG:
|
||||
LOG_INFO("Get module config: Ambient Lighting");
|
||||
configName = "Ambient Lighting";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag;
|
||||
res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG:
|
||||
LOG_INFO("Get module config: Paxcounter");
|
||||
configName = "Paxcounter";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
|
||||
LOG_INFO("Get module config: StatusMessage");
|
||||
configName = "StatusMessage";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
|
||||
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG:
|
||||
configName = "Traffic Management";
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag;
|
||||
res.get_module_config_response.payload_variant.traffic_management = moduleConfig.traffic_management;
|
||||
break;
|
||||
}
|
||||
LOG_INFO("Get module config: %s", configName);
|
||||
|
||||
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.
|
||||
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
|
||||
@@ -1376,23 +1412,17 @@ void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uic
|
||||
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
||||
{
|
||||
// Validate ham parameters before setting since this would bypass validation in the owner struct
|
||||
if (*p.call_sign) {
|
||||
const char *start = p.call_sign;
|
||||
// Skip all whitespace
|
||||
while (*start && isspace((unsigned char)*start))
|
||||
start++;
|
||||
if (*start == '\0') {
|
||||
LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (*p.short_name) {
|
||||
const char *start = p.short_name;
|
||||
while (*start && isspace((unsigned char)*start))
|
||||
start++;
|
||||
if (*start == '\0') {
|
||||
LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character");
|
||||
return;
|
||||
const char *fieldsToCheck[] = {p.call_sign, p.short_name};
|
||||
const char *fieldNames[] = {"call_sign", "short_name"};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (*fieldsToCheck[i]) {
|
||||
const char *start = fieldsToCheck[i];
|
||||
while (*start && isspace((unsigned char)*start))
|
||||
start++;
|
||||
if (*start == '\0') {
|
||||
LOG_WARN("Rejected ham %s: must contain at least 1 non-whitespace character", fieldNames[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
*/
|
||||
void handleSetOwner(const meshtastic_User &o);
|
||||
void handleSetChannel(const meshtastic_Channel &cc);
|
||||
void handleSetConfig(const meshtastic_Config &c);
|
||||
|
||||
protected:
|
||||
void handleSetConfig(const meshtastic_Config &c, bool fromOthers);
|
||||
|
||||
private:
|
||||
bool handleSetModuleConfig(const meshtastic_ModuleConfig &c);
|
||||
void handleSetChannel();
|
||||
void handleSetHamMode(const meshtastic_HamParameters &req);
|
||||
|
||||
@@ -350,12 +350,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
{
|
||||
// Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles
|
||||
if (moduleConfig.external_notification.enabled && !isSilenced) {
|
||||
#ifdef T_WATCH_S3
|
||||
drv.setWaveform(0, 75);
|
||||
drv.setWaveform(1, 56);
|
||||
drv.setWaveform(2, 0);
|
||||
drv.go();
|
||||
#endif
|
||||
if (!isFromUs(&mp)) {
|
||||
// Check if the message contains a bell character. Don't do this loop for every pin, just once.
|
||||
auto &p = mp.decoded;
|
||||
@@ -380,60 +374,69 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
const bool buzzerModeIsDirectOnly =
|
||||
(config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY);
|
||||
|
||||
if (containsBell || !is_muted) {
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message ||
|
||||
moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_message_vibra ||
|
||||
((moduleConfig.external_notification.alert_bell_buzzer ||
|
||||
moduleConfig.external_notification.alert_message_buzzer) &&
|
||||
canBuzz())) {
|
||||
nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout
|
||||
? (moduleConfig.external_notification.nag_timeout * 1000)
|
||||
: moduleConfig.external_notification.output_ms);
|
||||
LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff);
|
||||
isNagging = true;
|
||||
}
|
||||
// Each output evaluates its own alert condition independently:
|
||||
// alert_bell_* fires only when a bell character is present.
|
||||
// alert_message_* fires on any non-muted message.
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell");
|
||||
setExternalState(0, true);
|
||||
}
|
||||
// Alert when receiving a bell = alertBell: true
|
||||
// Alert when receiving a message = alertMessage: true
|
||||
const bool genericShouldAlert = (moduleConfig.external_notification.alert_bell && containsBell) ||
|
||||
(moduleConfig.external_notification.alert_message && !is_muted);
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_message_vibra) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)");
|
||||
setExternalState(1, true);
|
||||
}
|
||||
// Alert GPIO Vibra when receiving a bell = alertBellVibra: true
|
||||
// Alert GPIO Vibra when receiving a message = alertMessageVibra: true
|
||||
const bool vibraShouldAlert = (moduleConfig.external_notification.alert_bell_vibra && containsBell) ||
|
||||
(moduleConfig.external_notification.alert_message_vibra && !is_muted);
|
||||
|
||||
if ((moduleConfig.external_notification.alert_bell_buzzer ||
|
||||
moduleConfig.external_notification.alert_message_buzzer) &&
|
||||
canBuzz()) {
|
||||
LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)");
|
||||
if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) {
|
||||
LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
|
||||
} else {
|
||||
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
|
||||
#ifdef T_LORA_PAGER
|
||||
drv.setWaveform(0, 16); // Long buzzer 100%
|
||||
drv.setWaveform(1, 0); // Pause
|
||||
drv.setWaveform(2, 16);
|
||||
drv.setWaveform(3, 0);
|
||||
drv.setWaveform(4, 16);
|
||||
drv.setWaveform(5, 0);
|
||||
drv.setWaveform(6, 16);
|
||||
drv.setWaveform(7, 0);
|
||||
drv.go();
|
||||
// Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true
|
||||
// Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true
|
||||
const bool buzzerShouldAlert = canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) ||
|
||||
(moduleConfig.external_notification.alert_message_buzzer && !is_muted));
|
||||
|
||||
if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) {
|
||||
nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout
|
||||
? (moduleConfig.external_notification.nag_timeout * 1000)
|
||||
: moduleConfig.external_notification.output_ms);
|
||||
LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff);
|
||||
isNagging = true;
|
||||
}
|
||||
|
||||
if (genericShouldAlert) {
|
||||
LOG_INFO("externalNotificationModule - Generic alert");
|
||||
setExternalState(0, true);
|
||||
}
|
||||
|
||||
if (vibraShouldAlert) {
|
||||
LOG_INFO("externalNotificationModule - Vibra alert");
|
||||
setExternalState(1, true);
|
||||
}
|
||||
|
||||
if (buzzerShouldAlert) {
|
||||
LOG_INFO("externalNotificationModule - Buzzer alert");
|
||||
if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) {
|
||||
LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY");
|
||||
} else {
|
||||
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
|
||||
#ifdef HAS_DRV2605
|
||||
drv.setWaveform(0, 16); // Long buzzer 100%
|
||||
drv.setWaveform(1, 0); // Pause
|
||||
drv.setWaveform(2, 16);
|
||||
drv.setWaveform(3, 0);
|
||||
drv.setWaveform(4, 16);
|
||||
drv.setWaveform(5, 0);
|
||||
drv.setWaveform(6, 16);
|
||||
drv.setWaveform(7, 0);
|
||||
drv.go();
|
||||
#endif
|
||||
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
#ifdef HAS_I2S
|
||||
if (moduleConfig.external_notification.use_i2s_as_buzzer) {
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
} else
|
||||
audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone));
|
||||
#endif
|
||||
if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
} else {
|
||||
setExternalState(2, true);
|
||||
}
|
||||
} else if (moduleConfig.external_notification.use_pwm) {
|
||||
rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone);
|
||||
} else {
|
||||
setExternalState(2, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,4 +516,4 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
#include "modules/PowerStressModule.h"
|
||||
#endif
|
||||
#include "modules/RoutingModule.h"
|
||||
#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT
|
||||
#include "modules/TrafficManagementModule.h"
|
||||
#endif
|
||||
#include "modules/TextMessageModule.h"
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
#include "modules/TraceRouteModule.h"
|
||||
@@ -120,6 +123,14 @@ void setupModules()
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
new ReplyBotModule();
|
||||
#endif
|
||||
|
||||
#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT
|
||||
// Instantiate only when enabled to avoid extra memory use and background work.
|
||||
if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled) {
|
||||
trafficManagementModule = new TrafficManagementModule();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminModule = new AdminModule();
|
||||
#endif
|
||||
|
||||
@@ -20,10 +20,11 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh
|
||||
if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) &&
|
||||
(nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user))
|
||||
return false;
|
||||
} else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) {
|
||||
// Don't let licensed users to rebroadcast packets from unlicensed users
|
||||
} else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) ||
|
||||
(nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) {
|
||||
// Don't let licensed users to rebroadcast packets to or from unlicensed users
|
||||
// If we know they are in-fact unlicensed
|
||||
LOG_DEBUG("Packet from unlicensed user, ignoring packet");
|
||||
LOG_DEBUG("Packet to or from unlicensed user, ignoring packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,434 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshModule.h"
|
||||
#include "concurrency/Lock.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include "mesh/generated/meshtastic/telemetry.pb.h"
|
||||
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
|
||||
/**
|
||||
* TrafficManagementModule - Packet inspection and traffic shaping for mesh networks.
|
||||
*
|
||||
* This module provides:
|
||||
* - Position deduplication (drop redundant position broadcasts)
|
||||
* - Per-node rate limiting (throttle chatty nodes)
|
||||
* - Unknown packet filtering (drop undecoded packets from repeat offenders)
|
||||
* - NodeInfo direct response (answer queries from cache to reduce mesh chatter)
|
||||
* - Local-only telemetry/position (exhaust hop_limit for local broadcasts)
|
||||
* - Router hop preservation (maintain hop_limit for router-to-router traffic)
|
||||
*
|
||||
* Memory Optimization:
|
||||
* Uses a unified cache with cuckoo hashing for O(1) lookups and 56% memory reduction
|
||||
* compared to separate per-feature caches. Timestamps are stored as 8-bit relative
|
||||
* offsets from a rolling epoch to further reduce memory footprint.
|
||||
*/
|
||||
class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
TrafficManagementModule();
|
||||
~TrafficManagementModule();
|
||||
|
||||
// Singleton — no copying or moving
|
||||
TrafficManagementModule(const TrafficManagementModule &) = delete;
|
||||
TrafficManagementModule &operator=(const TrafficManagementModule &) = delete;
|
||||
|
||||
meshtastic_TrafficManagementStats getStats() const;
|
||||
void resetStats();
|
||||
void recordRouterHopPreserved();
|
||||
|
||||
/**
|
||||
* Check if this packet should have its hops exhausted.
|
||||
* Called from perhapsRebroadcast() to force hop_limit = 0 regardless of
|
||||
* router_preserve_hops or favorite node logic.
|
||||
*/
|
||||
bool shouldExhaustHops(const meshtastic_MeshPacket &mp) const
|
||||
{
|
||||
return exhaustRequested && exhaustRequestedFrom == getFrom(&mp) && exhaustRequestedId == mp.id;
|
||||
}
|
||||
|
||||
protected:
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
|
||||
void alterReceived(meshtastic_MeshPacket &mp) override;
|
||||
int32_t runOnce() override;
|
||||
// Protected so test shims can force epoch rollover behavior.
|
||||
void resetEpoch(uint32_t nowMs);
|
||||
|
||||
private:
|
||||
// =========================================================================
|
||||
// Unified Cache Entry (10 bytes) - Same for ALL platforms
|
||||
// =========================================================================
|
||||
//
|
||||
// A single compact structure used across ESP32, NRF52, and all other platforms.
|
||||
// Memory: 10 bytes × 2048 entries = 20KB
|
||||
//
|
||||
// Position Fingerprinting:
|
||||
// Instead of storing full coordinates (8 bytes) or a computed hash,
|
||||
// we store an 8-bit fingerprint derived deterministically from the
|
||||
// truncated lat/lon. This extracts the lower 4 significant bits from
|
||||
// each coordinate: fingerprint = (lat_low4 << 4) | lon_low4
|
||||
//
|
||||
// Benefits over hash:
|
||||
// - Adjacent grid cells have sequential fingerprints (no collision)
|
||||
// - Two positions only collide if 16+ grid cells apart in BOTH dimensions
|
||||
// - Deterministic: same input always produces same output
|
||||
//
|
||||
// Adaptive Timestamp Resolution:
|
||||
// All timestamps use 8-bit values with adaptive resolution calculated
|
||||
// from config at startup. Resolution = max(60, min(339, interval/2)).
|
||||
// - Min 60 seconds ensures reasonable precision
|
||||
// - Max 339 seconds allows ~24 hour range (255 * 339 = 86445 sec)
|
||||
// - interval/2 ensures at least 2 ticks per configured interval
|
||||
//
|
||||
// Layout:
|
||||
// [0-3] node - NodeNum (4 bytes)
|
||||
// [4] pos_fingerprint - 4 bits lat + 4 bits lon (1 byte)
|
||||
// [5] rate_count - Packets in current window (1 byte)
|
||||
// [6] unknown_count - Unknown packets count (1 byte)
|
||||
// [7] pos_time - Position timestamp (1 byte, adaptive resolution)
|
||||
// [8] rate_time - Rate window start (1 byte, adaptive resolution)
|
||||
// [9] unknown_time - Unknown tracking start (1 byte, adaptive resolution)
|
||||
//
|
||||
struct __attribute__((packed)) UnifiedCacheEntry {
|
||||
NodeNum node; // 4 bytes - Node identifier (0 = empty slot)
|
||||
uint8_t pos_fingerprint; // 1 byte - Lower 4 bits of lat + lon
|
||||
uint8_t rate_count; // 1 byte - Packet count (saturates at 255)
|
||||
uint8_t unknown_count; // 1 byte - Unknown packet count (saturates at 255)
|
||||
uint8_t pos_time; // 1 byte - Position timestamp (adaptive resolution)
|
||||
uint8_t rate_time; // 1 byte - Rate window start (adaptive resolution)
|
||||
uint8_t unknown_time; // 1 byte - Unknown tracking start (adaptive resolution)
|
||||
};
|
||||
static_assert(sizeof(UnifiedCacheEntry) == 10, "UnifiedCacheEntry should be 10 bytes");
|
||||
|
||||
// =========================================================================
|
||||
// Cuckoo Hash Table Implementation
|
||||
// =========================================================================
|
||||
//
|
||||
// Cuckoo hashing provides O(1) worst-case lookup time using two hash functions.
|
||||
// Each key can be in one of two possible locations (h1 or h2). On collision,
|
||||
// the existing entry is "kicked" to its alternate location.
|
||||
//
|
||||
// Benefits over linear scan:
|
||||
// - O(1) lookup vs O(n) - critical at packet processing rates
|
||||
// - O(1) insertion (amortized) with simple eviction on cycles
|
||||
// - ~95% load factor achievable
|
||||
//
|
||||
// Cache size rounds to power-of-2 for fast modulo via bitmask.
|
||||
// TRAFFIC_MANAGEMENT_CACHE_SIZE=2000 → cacheSize()=2048
|
||||
//
|
||||
static constexpr uint16_t cacheSize();
|
||||
static constexpr uint16_t cacheMask();
|
||||
|
||||
// Hash functions for cuckoo hashing
|
||||
inline uint16_t cuckooHash1(NodeNum node) const { return node & cacheMask(); }
|
||||
inline uint16_t cuckooHash2(NodeNum node) const { return ((node * 2654435769u) >> (32 - cuckooHashBits())) & cacheMask(); }
|
||||
static constexpr uint8_t cuckooHashBits();
|
||||
|
||||
// NodeInfo cache configuration (PSRAM path):
|
||||
// - Payload lives in PSRAM
|
||||
// - DRAM keeps packed 12-bit tags with 4-way bucketed cuckoo hashing
|
||||
// (Fan et al., CoNEXT 2014). Tag value 0 is reserved as "empty".
|
||||
static constexpr uint16_t kNodeInfoIndexMetadataBudgetBytes = 3072; // 3KB DRAM tag store
|
||||
static constexpr uint8_t kNodeInfoTargetOccupancyPercent = 95;
|
||||
static constexpr uint8_t kNodeInfoBucketSize = 4;
|
||||
static constexpr uint8_t kNodeInfoTagBits = 12;
|
||||
static constexpr uint16_t kNodeInfoTagMask = static_cast<uint16_t>((1u << kNodeInfoTagBits) - 1u);
|
||||
static constexpr uint16_t kNodeInfoIndexSlotsRaw =
|
||||
static_cast<uint16_t>((kNodeInfoIndexMetadataBudgetBytes * 8u) / kNodeInfoTagBits);
|
||||
static constexpr uint16_t kNodeInfoIndexSlots =
|
||||
static_cast<uint16_t>(kNodeInfoIndexSlotsRaw - (kNodeInfoIndexSlotsRaw % kNodeInfoBucketSize));
|
||||
static constexpr uint16_t kNodeInfoTargetEntries =
|
||||
static_cast<uint16_t>((kNodeInfoIndexSlots * kNodeInfoTargetOccupancyPercent) / 100u);
|
||||
static_assert((kNodeInfoIndexSlots % kNodeInfoBucketSize) == 0, "NodeInfo slot count must align to bucket size");
|
||||
static_assert(kNodeInfoTargetEntries < (1u << kNodeInfoTagBits), "NodeInfo tag bits must encode payload index");
|
||||
|
||||
static constexpr uint16_t nodeInfoTargetEntries();
|
||||
static constexpr uint16_t nodeInfoIndexMetadataBudgetBytes();
|
||||
static constexpr uint8_t nodeInfoTargetOccupancyPercent();
|
||||
static constexpr uint8_t nodeInfoBucketSize();
|
||||
static constexpr uint8_t nodeInfoTagBits();
|
||||
static constexpr uint16_t nodeInfoTagMask();
|
||||
static constexpr uint16_t nodeInfoIndexSlots();
|
||||
static constexpr uint16_t nodeInfoBucketCount();
|
||||
static constexpr uint16_t nodeInfoBucketMask();
|
||||
static constexpr uint8_t nodeInfoBucketHashBits();
|
||||
inline uint16_t nodeInfoHash1(NodeNum node) const { return node & nodeInfoBucketMask(); }
|
||||
inline uint16_t nodeInfoHash2(NodeNum node) const
|
||||
{
|
||||
return ((node * 2246822519u) >> (32 - nodeInfoBucketHashBits())) & nodeInfoBucketMask();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Adaptive Timestamp Resolution
|
||||
// =========================================================================
|
||||
//
|
||||
// All timestamps use 8-bit values with adaptive resolution calculated from
|
||||
// config at startup. This allows ~24 hour range while maintaining precision.
|
||||
//
|
||||
// Resolution formula: max(60, min(339, interval/2))
|
||||
// - 60 sec minimum ensures reasonable precision
|
||||
// - 339 sec maximum allows 24 hour range (255 * 339 ≈ 86400 sec)
|
||||
// - interval/2 ensures at least 2 ticks per configured interval
|
||||
//
|
||||
// Since config changes require reboot, resolution is calculated once.
|
||||
//
|
||||
uint32_t cacheEpochMs = 0;
|
||||
uint16_t posTimeResolution = 60; // Seconds per tick for position
|
||||
uint16_t rateTimeResolution = 60; // Seconds per tick for rate limiting
|
||||
uint16_t unknownTimeResolution = 60; // Seconds per tick for unknown tracking
|
||||
|
||||
// Calculate resolution from configured interval (called once at startup)
|
||||
static uint16_t calcTimeResolution(uint32_t intervalSecs)
|
||||
{
|
||||
// Resolution = interval/2 to ensure at least 2 ticks per interval
|
||||
// Clamped to [60, 339] for min precision and max 24h range
|
||||
uint32_t res = (intervalSecs > 0) ? (intervalSecs / 2) : 60;
|
||||
if (res < 60)
|
||||
res = 60;
|
||||
if (res > 339)
|
||||
res = 339;
|
||||
return static_cast<uint16_t>(res);
|
||||
}
|
||||
|
||||
// Convert to/from 8-bit relative timestamps with given resolution
|
||||
uint8_t toRelativeTime(uint32_t nowMs, uint16_t resolutionSecs) const
|
||||
{
|
||||
uint32_t ticks = (nowMs - cacheEpochMs) / (resolutionSecs * 1000UL);
|
||||
return (ticks > UINT8_MAX) ? UINT8_MAX : static_cast<uint8_t>(ticks);
|
||||
}
|
||||
uint32_t fromRelativeTime(uint8_t ticks, uint16_t resolutionSecs) const
|
||||
{
|
||||
return cacheEpochMs + (static_cast<uint32_t>(ticks) * resolutionSecs * 1000UL);
|
||||
}
|
||||
|
||||
// Convenience wrappers for each timestamp type
|
||||
uint8_t toRelativePosTime(uint32_t nowMs) const { return toRelativeTime(nowMs, posTimeResolution); }
|
||||
uint32_t fromRelativePosTime(uint8_t t) const { return fromRelativeTime(t, posTimeResolution); }
|
||||
|
||||
uint8_t toRelativeRateTime(uint32_t nowMs) const { return toRelativeTime(nowMs, rateTimeResolution); }
|
||||
uint32_t fromRelativeRateTime(uint8_t t) const { return fromRelativeTime(t, rateTimeResolution); }
|
||||
|
||||
uint8_t toRelativeUnknownTime(uint32_t nowMs) const { return toRelativeTime(nowMs, unknownTimeResolution); }
|
||||
uint32_t fromRelativeUnknownTime(uint8_t t) const { return fromRelativeTime(t, unknownTimeResolution); }
|
||||
|
||||
// Epoch reset when any timestamp approaches overflow
|
||||
// With max resolution of 339 sec, 200 ticks = ~19 hours (safe margin for 24h max)
|
||||
bool needsEpochReset(uint32_t nowMs) const
|
||||
{
|
||||
uint16_t maxRes = posTimeResolution;
|
||||
if (rateTimeResolution > maxRes)
|
||||
maxRes = rateTimeResolution;
|
||||
if (unknownTimeResolution > maxRes)
|
||||
maxRes = unknownTimeResolution;
|
||||
return (nowMs - cacheEpochMs) > (200UL * maxRes * 1000UL);
|
||||
}
|
||||
// =========================================================================
|
||||
// Position Fingerprint
|
||||
// =========================================================================
|
||||
//
|
||||
// Computes 8-bit fingerprint from truncated lat/lon coordinates.
|
||||
// Extracts lower 4 significant bits from each coordinate.
|
||||
//
|
||||
// fingerprint = (lat_low4 << 4) | lon_low4
|
||||
//
|
||||
// Unlike a hash, adjacent grid cells have sequential fingerprints,
|
||||
// so nearby positions never collide. Collisions only occur for
|
||||
// positions 16+ grid cells apart in both dimensions.
|
||||
//
|
||||
// Guards: If precision < 4 bits, uses min(precision, 4) bits.
|
||||
//
|
||||
static uint8_t computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision);
|
||||
|
||||
// =========================================================================
|
||||
// Cache Storage
|
||||
// =========================================================================
|
||||
|
||||
mutable concurrency::Lock cacheLock; // Protects all cache access
|
||||
UnifiedCacheEntry *cache = nullptr; // Cuckoo hash table (unified for all platforms)
|
||||
bool cacheFromPsram = false; // Tracks allocator for correct deallocation
|
||||
|
||||
struct NodeInfoPayloadEntry {
|
||||
// Node identifier associated with this payload slot.
|
||||
// 0 means the slot is currently unused.
|
||||
NodeNum node;
|
||||
|
||||
// Cached NODEINFO_APP payload body. This is separate from NodeDB and is only
|
||||
// used by the PSRAM-backed direct-response path in this module.
|
||||
meshtastic_User user;
|
||||
|
||||
// Extra response metadata captured from the latest observed NODEINFO_APP
|
||||
// packet for this node. shouldRespondToNodeInfo() uses this metadata when
|
||||
// building spoofed replies for requesting clients.
|
||||
|
||||
// Last local uptime tick (millis) when this entry was refreshed.
|
||||
uint32_t lastObservedMs;
|
||||
|
||||
// Last RTC/packet timestamp (seconds) observed for this NodeInfo frame.
|
||||
// If unavailable in packet, remains 0.
|
||||
uint32_t lastObservedRxTime;
|
||||
|
||||
// Channel where we most recently heard this node's NodeInfo.
|
||||
uint8_t sourceChannel;
|
||||
|
||||
// Cached decoded bitfield metadata from the source packet.
|
||||
// We preserve non-OK_TO_MQTT bits in direct replies when available.
|
||||
bool hasDecodedBitfield;
|
||||
uint8_t decodedBitfield;
|
||||
};
|
||||
|
||||
NodeInfoPayloadEntry *nodeInfoPayload = nullptr; // NodeInfo payloads in PSRAM
|
||||
bool nodeInfoPayloadFromPsram = false; // Tracks allocator for correct deallocation
|
||||
uint8_t *nodeInfoIndex = nullptr; // Packed 12-bit NodeInfo tags in DRAM
|
||||
uint16_t nodeInfoAllocHint = 0;
|
||||
uint16_t nodeInfoEvictCursor = 0;
|
||||
|
||||
meshtastic_TrafficManagementStats stats;
|
||||
|
||||
// Flag set during alterReceived() when packet should be exhausted.
|
||||
// Checked by perhapsRebroadcast() to force hop_limit = 0 only for the
|
||||
// matching packet key (from + id). Reset at start of handleReceived().
|
||||
bool exhaustRequested = false;
|
||||
NodeNum exhaustRequestedFrom = 0;
|
||||
PacketId exhaustRequestedId = 0;
|
||||
|
||||
// =========================================================================
|
||||
// Cache Operations
|
||||
// =========================================================================
|
||||
|
||||
// Find or create entry for node using cuckoo hashing
|
||||
// Returns nullptr if cache is full and eviction fails
|
||||
UnifiedCacheEntry *findOrCreateEntry(NodeNum node, bool *isNew);
|
||||
|
||||
// Find existing entry (no creation)
|
||||
UnifiedCacheEntry *findEntry(NodeNum node);
|
||||
|
||||
// NodeInfo cache operations (bucketed cuckoo index + PSRAM payloads)
|
||||
const NodeInfoPayloadEntry *findNodeInfoEntry(NodeNum node) const;
|
||||
NodeInfoPayloadEntry *findOrCreateNodeInfoEntry(NodeNum node, bool *usedEmptySlot);
|
||||
uint16_t findNodeInfoPayloadIndex(NodeNum node) const;
|
||||
bool removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex);
|
||||
uint16_t allocateNodeInfoPayloadSlot();
|
||||
uint16_t evictNodeInfoPayloadSlot();
|
||||
bool tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag);
|
||||
uint16_t encodeNodeInfoTag(uint16_t payloadIndex) const;
|
||||
uint16_t decodeNodeInfoPayloadIndex(uint16_t tag) const;
|
||||
uint16_t getNodeInfoTag(uint16_t slot) const;
|
||||
void setNodeInfoTag(uint16_t slot, uint16_t tag);
|
||||
uint16_t countNodeInfoEntriesLocked() const;
|
||||
void cacheNodeInfoPacket(const meshtastic_MeshPacket &mp);
|
||||
|
||||
// =========================================================================
|
||||
// Traffic Management Logic
|
||||
// =========================================================================
|
||||
|
||||
bool shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs);
|
||||
bool shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse);
|
||||
bool isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const;
|
||||
bool isRateLimited(NodeNum from, uint32_t nowMs);
|
||||
bool shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs);
|
||||
|
||||
void logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const;
|
||||
void incrementStat(uint32_t *field);
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Compile-time Cache Size Calculations
|
||||
// =========================================================================
|
||||
//
|
||||
// Round TRAFFIC_MANAGEMENT_CACHE_SIZE up to next power of 2 for efficient
|
||||
// cuckoo hash indexing (allows bitmask instead of modulo).
|
||||
//
|
||||
// These use C++11-compatible constexpr (single return statement).
|
||||
//
|
||||
|
||||
namespace detail
|
||||
{
|
||||
// Helper: round up to next power of 2 using bit manipulation
|
||||
constexpr uint16_t nextPow2(uint16_t n)
|
||||
{
|
||||
return n == 0 ? 0 : (((n - 1) | ((n - 1) >> 1) | ((n - 1) >> 2) | ((n - 1) >> 4) | ((n - 1) >> 8)) + 1);
|
||||
}
|
||||
|
||||
// Helper: floor(log2(n)) for n >= 0, C++11-compatible constexpr.
|
||||
constexpr uint8_t log2Floor(uint16_t n)
|
||||
{
|
||||
return n <= 1 ? 0 : static_cast<uint8_t>(1 + log2Floor(static_cast<uint16_t>(n >> 1)));
|
||||
}
|
||||
|
||||
// Helper: ceil(log2(n)) for n >= 1, C++11-compatible constexpr.
|
||||
constexpr uint8_t log2Ceil(uint16_t n)
|
||||
{
|
||||
return n <= 1 ? 0 : static_cast<uint8_t>(1 + log2Floor(static_cast<uint16_t>(n - 1)));
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::cacheSize()
|
||||
{
|
||||
return detail::nextPow2(TRAFFIC_MANAGEMENT_CACHE_SIZE);
|
||||
}
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::cacheMask()
|
||||
{
|
||||
return cacheSize() > 0 ? cacheSize() - 1 : 0;
|
||||
}
|
||||
|
||||
constexpr uint8_t TrafficManagementModule::cuckooHashBits()
|
||||
{
|
||||
return detail::log2Floor(cacheSize());
|
||||
}
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::nodeInfoTargetEntries()
|
||||
{
|
||||
return kNodeInfoTargetEntries;
|
||||
}
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::nodeInfoIndexMetadataBudgetBytes()
|
||||
{
|
||||
return kNodeInfoIndexMetadataBudgetBytes;
|
||||
}
|
||||
|
||||
constexpr uint8_t TrafficManagementModule::nodeInfoTargetOccupancyPercent()
|
||||
{
|
||||
return kNodeInfoTargetOccupancyPercent;
|
||||
}
|
||||
|
||||
constexpr uint8_t TrafficManagementModule::nodeInfoBucketSize()
|
||||
{
|
||||
return kNodeInfoBucketSize;
|
||||
}
|
||||
|
||||
constexpr uint8_t TrafficManagementModule::nodeInfoTagBits()
|
||||
{
|
||||
return kNodeInfoTagBits;
|
||||
}
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::nodeInfoTagMask()
|
||||
{
|
||||
return kNodeInfoTagMask;
|
||||
}
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::nodeInfoIndexSlots()
|
||||
{
|
||||
return kNodeInfoIndexSlots;
|
||||
}
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::nodeInfoBucketCount()
|
||||
{
|
||||
return static_cast<uint16_t>(nodeInfoIndexSlots() / nodeInfoBucketSize());
|
||||
}
|
||||
|
||||
constexpr uint16_t TrafficManagementModule::nodeInfoBucketMask()
|
||||
{
|
||||
return nodeInfoBucketCount() > 0 ? nodeInfoBucketCount() - 1 : 0;
|
||||
}
|
||||
|
||||
constexpr uint8_t TrafficManagementModule::nodeInfoBucketHashBits()
|
||||
{
|
||||
return detail::log2Floor(nodeInfoBucketCount());
|
||||
}
|
||||
|
||||
extern TrafficManagementModule *trafficManagementModule;
|
||||
|
||||
#endif
|
||||
@@ -100,7 +100,7 @@ AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_
|
||||
// moduleConfig.audio.i2s_sck = 14;
|
||||
// moduleConfig.audio.ptt_pin = 39;
|
||||
|
||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) {
|
||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) {
|
||||
LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1);
|
||||
codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1);
|
||||
memcpy(tx_header.magic, c2_magic, sizeof(c2_magic));
|
||||
@@ -143,7 +143,7 @@ void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
|
||||
int32_t AudioModule::runOnce()
|
||||
{
|
||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) {
|
||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) {
|
||||
esp_err_t res;
|
||||
if (firstTime) {
|
||||
// Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC
|
||||
@@ -270,7 +270,7 @@ void AudioModule::sendPayload(NodeNum dest, bool wantReplies)
|
||||
|
||||
ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) {
|
||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) {
|
||||
auto &p = mp.decoded;
|
||||
if (!isFromUs(&mp)) {
|
||||
memcpy(rx_encode_frame, p.payload.bytes, p.payload.size);
|
||||
|
||||
+22
-10
@@ -651,22 +651,34 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
|
||||
|
||||
if (config.enabled && !config.proxy_to_client_enabled) {
|
||||
#if HAS_NETWORKING
|
||||
std::unique_ptr<MQTTClient> clientConnection;
|
||||
if (config.tls_enabled) {
|
||||
#if MQTT_SUPPORTS_TLS
|
||||
MQTTClientTLS *tlsClient = new MQTTClientTLS;
|
||||
clientConnection.reset(tlsClient);
|
||||
tlsClient->setInsecure();
|
||||
#else
|
||||
#if !MQTT_SUPPORTS_TLS
|
||||
LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node");
|
||||
return false;
|
||||
#endif
|
||||
} else {
|
||||
clientConnection.reset(new MQTTClient);
|
||||
}
|
||||
std::unique_ptr<PubSubClient> pubSub(new PubSubClient);
|
||||
// Perform a lightweight TCP connectivity check without using connectPubSub(),
|
||||
// which mutates the module's isConnected state. This only checks if the server
|
||||
// is reachable — it does not establish an MQTT session.
|
||||
// Settings are always saved regardless of the result.
|
||||
if (isConnectedToNetwork()) {
|
||||
return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection);
|
||||
MQTTClient testClient;
|
||||
if (!testClient.connect(parsed.serverAddr.c_str(), parsed.serverPort)) {
|
||||
const char *warning = "Could not reach the MQTT server. Settings will be saved, but please verify the server "
|
||||
"address and credentials.";
|
||||
LOG_WARN(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
if (cn) {
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
cn->time = getValidTime(RTCQualityFromNet);
|
||||
strncpy(cn->message, warning, sizeof(cn->message) - 1);
|
||||
cn->message[sizeof(cn->message) - 1] = '\0';
|
||||
service->sendClientNotification(cn);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
testClient.stop();
|
||||
}
|
||||
#else
|
||||
const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network";
|
||||
|
||||
@@ -0,0 +1,814 @@
|
||||
/**
|
||||
* Tests for the radio configuration validation and clamping functions
|
||||
* introduced in the radio_interface_cherrypick branch.
|
||||
*
|
||||
* Targets:
|
||||
* 1. getRegion()
|
||||
* 2. RadioInterface::validateConfigRegion()
|
||||
* 3. RadioInterface::validateConfigLora()
|
||||
* 4. RadioInterface::clampConfigLora()
|
||||
* 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF)
|
||||
* 6. Channel spacing calculation (placeholder for future protobuf changes)
|
||||
*/
|
||||
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "TestUtil.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include <unity.h>
|
||||
|
||||
#include "meshtastic/config.pb.h"
|
||||
|
||||
class MockMeshService : public MeshService
|
||||
{
|
||||
public:
|
||||
void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); }
|
||||
};
|
||||
|
||||
static MockMeshService *mockMeshService;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// getRegion() tests
|
||||
// -----------------------------------------------------------------------
|
||||
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
|
||||
|
||||
static void test_getRegion_returnsCorrectRegion_US()
|
||||
{
|
||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code);
|
||||
TEST_ASSERT_EQUAL_STRING("US", r->name);
|
||||
}
|
||||
|
||||
static void test_getRegion_returnsCorrectRegion_EU868()
|
||||
{
|
||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code);
|
||||
TEST_ASSERT_EQUAL_STRING("EU_868", r->name);
|
||||
}
|
||||
|
||||
static void test_getRegion_returnsCorrectRegion_LORA24()
|
||||
{
|
||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code);
|
||||
TEST_ASSERT_TRUE(r->wideLora);
|
||||
}
|
||||
|
||||
static void test_getRegion_unsetCodeReturnsUnsetEntry()
|
||||
{
|
||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
||||
TEST_ASSERT_EQUAL_STRING("UNSET", r->name);
|
||||
}
|
||||
|
||||
static void test_getRegion_unknownCodeFallsToUnset()
|
||||
{
|
||||
// A code not in the table should iterate to the UNSET sentinel
|
||||
const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255);
|
||||
TEST_ASSERT_NOT_NULL(r);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// validateConfigRegion() tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test_validateConfigRegion_validRegionReturnsTrue()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
|
||||
// Ensure owner is not licensed (should not matter for non-licensed-only regions)
|
||||
devicestate.owner.is_licensed = false;
|
||||
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigRegion_unsetRegionReturnsTrue()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
|
||||
devicestate.owner.is_licensed = false;
|
||||
|
||||
// UNSET region has licensedOnly=false, so should pass
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Shadow tables for testing (preset lists → profiles → regions → lookup)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// A minimal preset list with only one entry
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
||||
MODEM_PRESET_END,
|
||||
};
|
||||
|
||||
// A preset list that includes all turbo variants only
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
||||
MODEM_PRESET_END,
|
||||
};
|
||||
|
||||
// A restricted list simulating a hypothetical tight-regulation region
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
||||
MODEM_PRESET_END,
|
||||
};
|
||||
|
||||
// Mirrors PROFILE_STD but with non-zero spacing/padding for testing
|
||||
static const RegionProfile TEST_PROFILE_SPACED = {
|
||||
TEST_PRESETS_SINGLE,
|
||||
/* spacing */ 0.025f,
|
||||
/* padding */ 0.010f,
|
||||
/* audioPermitted */ true,
|
||||
/* licensedOnly */ false,
|
||||
/* textThrottle */ 0,
|
||||
/* positionThrottle */ 0,
|
||||
/* telemetryThrottle */ 0,
|
||||
/* overrideSlot */ 0,
|
||||
};
|
||||
|
||||
// A licensed-only profile for testing access control
|
||||
static const RegionProfile TEST_PROFILE_LICENSED = {
|
||||
TEST_PRESETS_RESTRICTED,
|
||||
/* spacing */ 0.0f,
|
||||
/* padding */ 0.0f,
|
||||
/* audioPermitted */ false,
|
||||
/* licensedOnly */ true,
|
||||
/* textThrottle */ 5,
|
||||
/* positionThrottle */ 10,
|
||||
/* telemetryThrottle */ 10,
|
||||
/* overrideSlot */ 3,
|
||||
};
|
||||
|
||||
// Turbo-only profile
|
||||
static const RegionProfile TEST_PROFILE_TURBO = {
|
||||
TEST_PRESETS_TURBO_ONLY,
|
||||
/* spacing */ 0.0f,
|
||||
/* padding */ 0.0f,
|
||||
/* audioPermitted */ true,
|
||||
/* licensedOnly */ false,
|
||||
/* textThrottle */ 0,
|
||||
/* positionThrottle */ 0,
|
||||
/* telemetryThrottle */ 0,
|
||||
/* overrideSlot */ 0,
|
||||
};
|
||||
|
||||
static const RegionInfo testRegions[] = {
|
||||
// A wide US-like region with spacing + padding
|
||||
{meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_US_SPACED"},
|
||||
|
||||
// A narrow band simulating tight EU regulation
|
||||
{meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED,
|
||||
"TEST_EU_LICENSED"},
|
||||
|
||||
// A wide-LoRa region with turbo-only presets
|
||||
{meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO,
|
||||
"TEST_LORA24_TURBO"},
|
||||
|
||||
// Sentinel — must be last
|
||||
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_UNSET"},
|
||||
};
|
||||
|
||||
static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||||
{
|
||||
const RegionInfo *r = testRegions;
|
||||
while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
if (r->code == code)
|
||||
return r;
|
||||
r++;
|
||||
}
|
||||
return r; // Returns the UNSET sentinel
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Shadow table tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test_shadowTable_spacedProfileHasNonZeroSpacing()
|
||||
{
|
||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
||||
TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding);
|
||||
}
|
||||
|
||||
static void test_shadowTable_licensedProfileFlagsCorrect()
|
||||
{
|
||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
TEST_ASSERT_TRUE(r->profile->licensedOnly);
|
||||
TEST_ASSERT_FALSE(r->profile->audioPermitted);
|
||||
TEST_ASSERT_EQUAL(3, r->profile->overrideSlot);
|
||||
}
|
||||
|
||||
static void test_shadowTable_presetCountMatchesExpected()
|
||||
{
|
||||
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
||||
TEST_ASSERT_EQUAL(1, spaced->getNumPresets());
|
||||
|
||||
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
TEST_ASSERT_EQUAL(2, licensed->getNumPresets());
|
||||
|
||||
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
||||
TEST_ASSERT_EQUAL(2, turbo->getNumPresets());
|
||||
}
|
||||
|
||||
static void test_shadowTable_defaultPresetIsFirstInList()
|
||||
{
|
||||
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset());
|
||||
|
||||
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset());
|
||||
|
||||
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset());
|
||||
}
|
||||
|
||||
static void test_shadowTable_channelSpacingWithPadding()
|
||||
{
|
||||
// Verify channel count when spacing + padding are non-zero
|
||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
||||
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
||||
float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f);
|
||||
|
||||
// spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250
|
||||
// channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing);
|
||||
|
||||
uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f);
|
||||
// (928 - 902 + 0.025) / 0.295 = 88.2 → 88
|
||||
TEST_ASSERT_EQUAL_UINT32(88, numChannels);
|
||||
}
|
||||
|
||||
static void test_shadowTable_turboOnlyOnWideLora()
|
||||
{
|
||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
||||
TEST_ASSERT_TRUE(r->wideLora);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset());
|
||||
|
||||
// Verify wide-LoRa bandwidth for SHORT_TURBO
|
||||
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode
|
||||
}
|
||||
|
||||
static void test_shadowTable_unknownCodeFallsToSentinel()
|
||||
{
|
||||
const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
||||
TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// validateConfigLora() tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test_validateConfigLora_validPresetForUS()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_allStdPresetsValidForUS()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = stdPresets[i];
|
||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US");
|
||||
}
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_turboPresetsInvalidForEU868()
|
||||
{
|
||||
// EU_868 has PRESETS_EU_868 which excludes SHORT_TURBO and LONG_TURBO
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.use_preset = true;
|
||||
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868");
|
||||
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO;
|
||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868");
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_validPresetsForEU868()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) {
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = eu868Presets[i];
|
||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868");
|
||||
}
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_customBandwidthTooWideForEU868()
|
||||
{
|
||||
// EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz
|
||||
// A 500 kHz custom BW should be rejected
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.use_preset = false;
|
||||
cfg.bandwidth = 500;
|
||||
cfg.spread_factor = 11;
|
||||
cfg.coding_rate = 5;
|
||||
|
||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_customBandwidthFitsUS()
|
||||
{
|
||||
// US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.use_preset = false;
|
||||
cfg.bandwidth = 250;
|
||||
cfg.spread_factor = 11;
|
||||
cfg.coding_rate = 5;
|
||||
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_customBandwidthFitsEU868()
|
||||
{
|
||||
// EU_868 spans 250 kHz, 125 kHz BW should fit
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.use_preset = false;
|
||||
cfg.bandwidth = 125;
|
||||
cfg.spread_factor = 12;
|
||||
cfg.coding_rate = 8;
|
||||
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_bogusPresetRejected()
|
||||
{
|
||||
// A fabricated preset value not in any list should be rejected
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
||||
|
||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast()
|
||||
{
|
||||
// UNSET uses PROFILE_UNDEF which has only LONG_FAST
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
cfg.use_preset = true;
|
||||
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_FAST should be valid for UNSET");
|
||||
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_FAST should be invalid for UNSET");
|
||||
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for UNSET");
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_allPresetsValidForLORA24()
|
||||
{
|
||||
// LORA_24 uses PROFILE_STD (9 presets) with wideLora=true
|
||||
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = stdPresets[i];
|
||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// clampConfigLora() tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test_clampConfigLora_invalidPresetClampedToDefault()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset);
|
||||
}
|
||||
|
||||
static void test_clampConfigLora_validPresetUnchanged()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
||||
}
|
||||
|
||||
static void test_clampConfigLora_customBwTooWideClampedToDefaultBw()
|
||||
{
|
||||
// EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW.
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.use_preset = false;
|
||||
cfg.bandwidth = 500;
|
||||
cfg.spread_factor = 11;
|
||||
cfg.coding_rate = 5;
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora);
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth);
|
||||
}
|
||||
|
||||
static void test_clampConfigLora_customBwValidLeftUnchanged()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.use_preset = false;
|
||||
cfg.bandwidth = 125;
|
||||
cfg.spread_factor = 12;
|
||||
cfg.coding_rate = 8;
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth);
|
||||
}
|
||||
|
||||
static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast()
|
||||
{
|
||||
// UNSET uses PROFILE_UNDEF with only LONG_FAST; any other preset should clamp to it
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
||||
}
|
||||
|
||||
static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault()
|
||||
{
|
||||
// LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD)
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
||||
cfg.use_preset = true;
|
||||
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
||||
TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// RegionInfo preset list integrity tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test_presetsStd_hasNineEntries()
|
||||
{
|
||||
// PROFILE_STD should have exactly 9 presets
|
||||
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
||||
TEST_ASSERT_EQUAL(9, us->getNumPresets());
|
||||
TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets());
|
||||
}
|
||||
|
||||
static void test_presetsEU868_hasSevenEntries()
|
||||
{
|
||||
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
TEST_ASSERT_EQUAL(7, eu->getNumPresets());
|
||||
TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets());
|
||||
}
|
||||
|
||||
static void test_presetsUndef_hasOneEntry()
|
||||
{
|
||||
const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
||||
TEST_ASSERT_EQUAL(1, unset->getNumPresets());
|
||||
TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets());
|
||||
}
|
||||
|
||||
static void test_defaultPresetIsInAvailablePresets()
|
||||
{
|
||||
// For every region, the defaultPreset must appear in its own availablePresets list
|
||||
const RegionInfo *r = regions;
|
||||
while (true) {
|
||||
bool found = false;
|
||||
for (size_t i = 0; i < r->getNumPresets(); i++) {
|
||||
if (r->getAvailablePresets()[i] == r->getDefaultPreset()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
char msg[80];
|
||||
snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name);
|
||||
TEST_ASSERT_TRUE_MESSAGE(found, msg);
|
||||
|
||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
||||
break; // UNSET is the sentinel, stop after it
|
||||
r++;
|
||||
}
|
||||
}
|
||||
|
||||
static void test_regionFieldsAreSane()
|
||||
{
|
||||
// Basic sanity check: all regions have freqEnd > freqStart and a non-null name
|
||||
const RegionInfo *r = regions;
|
||||
while (true) {
|
||||
char msg[80];
|
||||
snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name);
|
||||
TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg);
|
||||
TEST_ASSERT_NOT_NULL(r->name);
|
||||
TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0");
|
||||
TEST_ASSERT_NOT_NULL(r->getAvailablePresets());
|
||||
|
||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
||||
break;
|
||||
r++;
|
||||
}
|
||||
}
|
||||
|
||||
static void test_onlyLORA24HasWideLora()
|
||||
{
|
||||
// Verify that LORA_24 is the only region with wideLora=true
|
||||
const RegionInfo *r = regions;
|
||||
while (true) {
|
||||
char msg[80];
|
||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) {
|
||||
snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name);
|
||||
TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg);
|
||||
} else {
|
||||
snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name);
|
||||
TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg);
|
||||
}
|
||||
|
||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
||||
break;
|
||||
r++;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Channel spacing calculation (placeholder for future protobuf updates)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test_channelSpacingCalculation_US_LONG_FAST()
|
||||
{
|
||||
// Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000)
|
||||
// US: spacing=0, padding=0
|
||||
// LONG_FAST on non-wide region: bw=250 kHz
|
||||
// channelSpacing = 0 + 0 + 0.250 = 0.250 MHz
|
||||
// numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104
|
||||
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
||||
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
||||
float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f);
|
||||
uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f);
|
||||
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
||||
TEST_ASSERT_EQUAL_UINT32(104, numChannels);
|
||||
}
|
||||
|
||||
static void test_channelSpacingCalculation_EU868_LONG_FAST()
|
||||
{
|
||||
// EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0
|
||||
// LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz
|
||||
// numChannels = round((0.25 + 0) / 0.250) = 1
|
||||
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora);
|
||||
float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f);
|
||||
uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f);
|
||||
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, numChannels);
|
||||
}
|
||||
|
||||
// Placeholder: when protobuf region definitions include non-zero padding/spacing,
|
||||
// add tests here to verify the channel count and frequency calculations.
|
||||
static void test_channelSpacingCalculation_placeholder()
|
||||
{
|
||||
// TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values,
|
||||
// verify:
|
||||
// - Channel count matches expected value for each (region, preset) pair
|
||||
// - First channel frequency = freqStart + (bw/2000) + padding
|
||||
// - Nth channel frequency = first + (n * channelSpacing)
|
||||
// - overrideSlot, when non-zero, forces the channel_num
|
||||
TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// handleSetConfig fromOthers dispatch tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
class AdminModuleTestShim : public AdminModule
|
||||
{
|
||||
public:
|
||||
using AdminModule::handleSetConfig;
|
||||
};
|
||||
|
||||
static AdminModuleTestShim *testAdmin;
|
||||
|
||||
static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset preset)
|
||||
{
|
||||
meshtastic_Config c = meshtastic_Config_init_zero;
|
||||
c.which_payload_variant = meshtastic_Config_lora_tag;
|
||||
c.payload_variant.lora.region = region;
|
||||
c.payload_variant.lora.use_preset = usePreset;
|
||||
c.payload_variant.lora.modem_preset = preset;
|
||||
return c;
|
||||
}
|
||||
|
||||
static void test_handleSetConfig_fromOthers_invalidPresetRejected()
|
||||
{
|
||||
// Set up a known-good baseline in the global config
|
||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
config.lora.use_preset = true;
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
initRegion();
|
||||
|
||||
// Build an admin set_config with an invalid preset for EU_868
|
||||
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
||||
|
||||
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
||||
|
||||
// fromOthers=true: invalid preset should be rejected, old preset preserved
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
||||
}
|
||||
|
||||
static void test_handleSetConfig_fromLocal_invalidPresetClamped()
|
||||
{
|
||||
// Set up a known-good baseline
|
||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
config.lora.use_preset = true;
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
initRegion();
|
||||
|
||||
// Build an admin set_config with an invalid preset for EU_868
|
||||
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
||||
|
||||
testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)
|
||||
|
||||
// fromOthers=false: invalid preset should be clamped to the region's default
|
||||
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
||||
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset);
|
||||
}
|
||||
|
||||
static void test_handleSetConfig_fromOthers_validPresetAccepted()
|
||||
{
|
||||
// Set up baseline
|
||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
config.lora.use_preset = true;
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
initRegion();
|
||||
|
||||
// Build an admin set_config with a valid preset for EU_868
|
||||
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);
|
||||
|
||||
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
||||
|
||||
// Valid preset should be accepted regardless of fromOthers
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test runner
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
mockMeshService = new MockMeshService();
|
||||
service = mockMeshService;
|
||||
testAdmin = new AdminModuleTestShim();
|
||||
}
|
||||
void tearDown(void)
|
||||
{
|
||||
service = nullptr;
|
||||
delete mockMeshService;
|
||||
mockMeshService = nullptr;
|
||||
delete testAdmin;
|
||||
testAdmin = nullptr;
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
delay(10);
|
||||
delay(2000);
|
||||
|
||||
initializeTestEnvironment();
|
||||
|
||||
UNITY_BEGIN();
|
||||
|
||||
// getRegion()
|
||||
RUN_TEST(test_getRegion_returnsCorrectRegion_US);
|
||||
RUN_TEST(test_getRegion_returnsCorrectRegion_EU868);
|
||||
RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24);
|
||||
RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry);
|
||||
RUN_TEST(test_getRegion_unknownCodeFallsToUnset);
|
||||
|
||||
// validateConfigRegion()
|
||||
RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue);
|
||||
RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue);
|
||||
|
||||
// Shadow table tests
|
||||
RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing);
|
||||
RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect);
|
||||
RUN_TEST(test_shadowTable_presetCountMatchesExpected);
|
||||
RUN_TEST(test_shadowTable_defaultPresetIsFirstInList);
|
||||
RUN_TEST(test_shadowTable_channelSpacingWithPadding);
|
||||
RUN_TEST(test_shadowTable_turboOnlyOnWideLora);
|
||||
RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel);
|
||||
|
||||
// validateConfigLora()
|
||||
RUN_TEST(test_validateConfigLora_validPresetForUS);
|
||||
RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS);
|
||||
RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868);
|
||||
RUN_TEST(test_validateConfigLora_validPresetsForEU868);
|
||||
RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868);
|
||||
RUN_TEST(test_validateConfigLora_customBandwidthFitsUS);
|
||||
RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868);
|
||||
RUN_TEST(test_validateConfigLora_bogusPresetRejected);
|
||||
RUN_TEST(test_validateConfigLora_unsetRegionOnlyAcceptsLongFast);
|
||||
RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24);
|
||||
|
||||
// clampConfigLora()
|
||||
RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
|
||||
RUN_TEST(test_clampConfigLora_validPresetUnchanged);
|
||||
RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw);
|
||||
RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged);
|
||||
RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast);
|
||||
RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault);
|
||||
|
||||
// RegionInfo preset list integrity
|
||||
RUN_TEST(test_presetsStd_hasNineEntries);
|
||||
RUN_TEST(test_presetsEU868_hasSevenEntries);
|
||||
RUN_TEST(test_presetsUndef_hasOneEntry);
|
||||
RUN_TEST(test_defaultPresetIsInAvailablePresets);
|
||||
RUN_TEST(test_regionFieldsAreSane);
|
||||
RUN_TEST(test_onlyLORA24HasWideLora);
|
||||
|
||||
// Channel spacing (current + placeholder)
|
||||
RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST);
|
||||
RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST);
|
||||
RUN_TEST(test_channelSpacingCalculation_placeholder);
|
||||
|
||||
// handleSetConfig fromOthers dispatch
|
||||
RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected);
|
||||
RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped);
|
||||
RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted);
|
||||
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
+12
-21
@@ -818,16 +818,13 @@ void test_configEmptyIsValid(void)
|
||||
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
|
||||
}
|
||||
|
||||
// Empty 'enabled' configuration is valid.
|
||||
// Empty 'enabled' configuration is valid. A lightweight TCP check may be performed
|
||||
// but does not affect the result.
|
||||
void test_configEnabledEmptyIsValid(void)
|
||||
{
|
||||
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true};
|
||||
MockPubSubServer client;
|
||||
|
||||
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
|
||||
TEST_ASSERT_TRUE(client.connected_);
|
||||
TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str());
|
||||
TEST_ASSERT_EQUAL(1883, client.port_);
|
||||
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
|
||||
}
|
||||
|
||||
// Configuration with the default server is valid.
|
||||
@@ -846,38 +843,32 @@ void test_configWithDefaultServerAndInvalidPort(void)
|
||||
TEST_ASSERT_FALSE(MQTT::isValidConfig(config));
|
||||
}
|
||||
|
||||
// isValidConfig connects to a custom host and port.
|
||||
// Custom host and port is valid. TCP reachability is checked but does not block saving.
|
||||
void test_configCustomHostAndPort(void)
|
||||
{
|
||||
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"};
|
||||
MockPubSubServer client;
|
||||
|
||||
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
|
||||
TEST_ASSERT_TRUE(client.connected_);
|
||||
TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str());
|
||||
TEST_ASSERT_EQUAL(1234, client.port_);
|
||||
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
|
||||
}
|
||||
|
||||
// isValidConfig returns false if a connection cannot be established.
|
||||
void test_configWithConnectionFailure(void)
|
||||
// An unreachable server is still a valid config — settings always save.
|
||||
// A warning notification is sent in non-test builds, but isValidConfig returns true.
|
||||
void test_configWithUnreachableServerIsStillValid(void)
|
||||
{
|
||||
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"};
|
||||
MockPubSubServer client;
|
||||
client.refuseConnection_ = true;
|
||||
|
||||
TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client));
|
||||
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
|
||||
}
|
||||
|
||||
// isValidConfig returns true when tls_enabled is supported, or false otherwise.
|
||||
void test_configWithTLSEnabled(void)
|
||||
{
|
||||
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true};
|
||||
MockPubSubServer client;
|
||||
|
||||
#if MQTT_SUPPORTS_TLS
|
||||
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
|
||||
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
|
||||
#else
|
||||
TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client));
|
||||
TEST_ASSERT_FALSE(MQTT::isValidConfig(config));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -927,7 +918,7 @@ void setup()
|
||||
RUN_TEST(test_configWithDefaultServer);
|
||||
RUN_TEST(test_configWithDefaultServerAndInvalidPort);
|
||||
RUN_TEST(test_configCustomHostAndPort);
|
||||
RUN_TEST(test_configWithConnectionFailure);
|
||||
RUN_TEST(test_configWithUnreachableServerIsStillValid);
|
||||
RUN_TEST(test_configWithTLSEnabled);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "TestUtil.h"
|
||||
#include <unity.h>
|
||||
|
||||
#include "meshtastic/config.pb.h"
|
||||
|
||||
class MockMeshService : public MeshService
|
||||
{
|
||||
public:
|
||||
void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); }
|
||||
};
|
||||
|
||||
static MockMeshService *mockMeshService;
|
||||
|
||||
static void test_bwCodeToKHz_specialMappings()
|
||||
{
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31));
|
||||
@@ -21,7 +30,7 @@ static void test_bwCodeToKHz_passthrough()
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250));
|
||||
}
|
||||
|
||||
static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse()
|
||||
static void test_validateConfigLora_noopWhenUsePresetFalse()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.use_preset = false;
|
||||
@@ -30,55 +39,78 @@ static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse()
|
||||
cfg.bandwidth = 123;
|
||||
cfg.spread_factor = 8;
|
||||
|
||||
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||
RadioInterface::validateConfigLora(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth);
|
||||
TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor);
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
||||
}
|
||||
|
||||
static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion()
|
||||
static void test_validateConfigLora_validPreset_nonWideRegion()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.use_preset = true;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||
|
||||
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth);
|
||||
TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor);
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion()
|
||||
static void test_validateConfigLora_validPreset_wideRegion()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.use_preset = true;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||
|
||||
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth);
|
||||
TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor);
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan()
|
||||
static void test_validateConfigLora_rejectsInvalidPresetForRegion()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.use_preset = true;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
||||
|
||||
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
||||
TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth);
|
||||
TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor);
|
||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
||||
}
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
static void test_clampConfigLora_invalidPresetClampedToDefault()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.use_preset = true;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
||||
}
|
||||
|
||||
static void test_clampConfigLora_validPresetUnchanged()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.use_preset = true;
|
||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||
|
||||
RadioInterface::clampConfigLora(cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
mockMeshService = new MockMeshService();
|
||||
service = mockMeshService;
|
||||
}
|
||||
void tearDown(void)
|
||||
{
|
||||
service = nullptr;
|
||||
delete mockMeshService;
|
||||
mockMeshService = nullptr;
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
@@ -90,10 +122,12 @@ void setup()
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_bwCodeToKHz_specialMappings);
|
||||
RUN_TEST(test_bwCodeToKHz_passthrough);
|
||||
RUN_TEST(test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse);
|
||||
RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion);
|
||||
RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion);
|
||||
RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan);
|
||||
RUN_TEST(test_validateConfigLora_noopWhenUsePresetFalse);
|
||||
RUN_TEST(test_validateConfigLora_validPreset_nonWideRegion);
|
||||
RUN_TEST(test_validateConfigLora_validPreset_wideRegion);
|
||||
RUN_TEST(test_validateConfigLora_rejectsInvalidPresetForRegion);
|
||||
RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
|
||||
RUN_TEST(test_clampConfigLora_validPresetUnchanged);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,4 +13,5 @@ build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-D TLORA_V1
|
||||
-I variants/esp32/tlora_v1
|
||||
-ULED_BUILTIN
|
||||
upload_speed = 115200
|
||||
|
||||
@@ -68,7 +68,10 @@ build_flags =
|
||||
-D INPUTDRIVER_BUTTON_TYPE=0
|
||||
-D HAS_SCREEN=1
|
||||
-D HAS_TFT=1
|
||||
-D RAM_SIZE=1860
|
||||
-D MAP_TILES_GREY ; required for 2MB PSRAM
|
||||
-D RAM_SIZE=1432
|
||||
-D STBI_ARENA_SIZE=450000
|
||||
-D LV_CACHE_DEF_SIZE=0
|
||||
-D LV_LVGL_H_INCLUDE_SIMPLE
|
||||
-D LV_CONF_INCLUDE_SIMPLE
|
||||
-D LV_COMP_CONF_INCLUDE_SIMPLE
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
// Enable Traffic Management Module for Heltec V4
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
||||
#endif
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
|
||||
#endif
|
||||
|
||||
// ---- GC1109 RF FRONT END CONFIGURATION ----
|
||||
// The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA
|
||||
// RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna
|
||||
@@ -89,4 +97,4 @@
|
||||
// Seems to be missing on this new board
|
||||
#define GPS_TX_PIN (38) // This is for bits going TOWARDS the CPU
|
||||
#define GPS_RX_PIN (39) // This is for bits going TOWARDS the GPS
|
||||
#define GPS_THREAD_INTERVAL 50
|
||||
#define GPS_THREAD_INTERVAL 50
|
||||
|
||||
@@ -40,6 +40,14 @@ Board Information: https://wiki.uniteng.com/en/meshtastic/station-g2
|
||||
#define SX126X_MAX_POWER 19
|
||||
#endif
|
||||
|
||||
// Enable Traffic Management Module for Station G2
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
||||
#endif
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
|
||||
#endif
|
||||
|
||||
/*
|
||||
#define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO4_CHANNEL
|
||||
|
||||
@@ -8,3 +8,11 @@
|
||||
|
||||
// RAK12002 RTC Module
|
||||
#define RV3028_RTC (uint8_t)0b1010010
|
||||
|
||||
// Enable Traffic Management Module for native/portduino
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
||||
#endif
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
; First prototype nrf52840/sx1262 device
|
||||
[env:heltec-mesh-node-t096]
|
||||
custom_meshtastic_hw_model = 127
|
||||
custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T096
|
||||
custom_meshtastic_architecture = nrf52840
|
||||
custom_meshtastic_actively_supported = true
|
||||
custom_meshtastic_support_level = 1
|
||||
custom_meshtastic_display_name = Heltec Mesh Node 096
|
||||
custom_meshtastic_images = heltec-mesh-node-t096.svg, heltec-mesh-node-t096-case.svg
|
||||
custom_meshtastic_tags = Heltec
|
||||
|
||||
extends = nrf52840_base
|
||||
board = heltec_mesh_node_t096
|
||||
board_level = pr
|
||||
debug_tool = jlink
|
||||
|
||||
build_flags = ${nrf52840_base.build_flags}
|
||||
-Ivariants/nrf52840/heltec_mesh_node_t096
|
||||
-D HAS_LORA_FEM=1
|
||||
-D HELTEC_MESH_NODE_T096
|
||||
-D USE_TFTDISPLAY=1
|
||||
-D USER_SETUP_LOADED
|
||||
-D ST7735_DRIVER
|
||||
-D ST7735_REDTAB160x80
|
||||
-D TFT_SPI_PORT=SPI1
|
||||
-D TFT_CS=ST7735_CS ; Chip select control
|
||||
-D TFT_DC=ST7735_RS ; Data Command control pin
|
||||
-D TFT_RST=ST7735_RESET ; Reset pin
|
||||
-D TFT_BL=ST7735_BL ; LED back-light
|
||||
-D TFT_BACKLIGHT_ON=LOW
|
||||
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t096>
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
bodmer/TFT_eSPI@2.5.43 ; renovate: datasource=platformio-registry depName=bodmer/TFT_eSPI
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library 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 Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "variant.h"
|
||||
#include "nrf.h"
|
||||
#include "wiring_constants.h"
|
||||
#include "wiring_digital.h"
|
||||
|
||||
const uint32_t g_ADigitalPinMap[] = {
|
||||
// P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
|
||||
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
|
||||
|
||||
// P1
|
||||
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
|
||||
void initVariant()
|
||||
{
|
||||
// LED1
|
||||
pinMode(PIN_LED1, OUTPUT);
|
||||
ledOff(PIN_LED1);
|
||||
}
|
||||
|
||||
void variant_shutdown()
|
||||
{
|
||||
nrf_gpio_cfg_default(VEXT_ENABLE);
|
||||
nrf_gpio_cfg_default(ST7735_CS);
|
||||
nrf_gpio_cfg_default(ST7735_RS);
|
||||
nrf_gpio_cfg_default(ST7735_SDA);
|
||||
nrf_gpio_cfg_default(ST7735_SCK);
|
||||
nrf_gpio_cfg_default(ST7735_RESET);
|
||||
nrf_gpio_cfg_default(ST7735_BL);
|
||||
|
||||
nrf_gpio_cfg_default(PIN_LED1);
|
||||
|
||||
// nrf_gpio_cfg_default(LORA_PA_POWER);
|
||||
pinMode(LORA_PA_POWER, OUTPUT);
|
||||
digitalWrite(LORA_PA_POWER, LOW);
|
||||
|
||||
nrf_gpio_cfg_default(LORA_KCT8103L_PA_CSD);
|
||||
nrf_gpio_cfg_default(LORA_KCT8103L_PA_CTX);
|
||||
|
||||
pinMode(ADC_CTRL, OUTPUT);
|
||||
digitalWrite(ADC_CTRL, LOW);
|
||||
|
||||
nrf_gpio_cfg_default(SX126X_CS);
|
||||
nrf_gpio_cfg_default(SX126X_DIO1);
|
||||
nrf_gpio_cfg_default(SX126X_BUSY);
|
||||
nrf_gpio_cfg_default(SX126X_RESET);
|
||||
|
||||
nrf_gpio_cfg_default(PIN_SPI_MISO);
|
||||
nrf_gpio_cfg_default(PIN_SPI_MOSI);
|
||||
nrf_gpio_cfg_default(PIN_SPI_SCK);
|
||||
|
||||
nrf_gpio_cfg_default(PIN_GPS_PPS);
|
||||
nrf_gpio_cfg_default(PIN_GPS_RESET);
|
||||
nrf_gpio_cfg_default(PIN_GPS_EN);
|
||||
nrf_gpio_cfg_default(GPS_TX_PIN);
|
||||
nrf_gpio_cfg_default(GPS_RX_PIN);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
This library 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 Lesser General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _VARIANT_HELTEC_NRF_
|
||||
#define _VARIANT_HELTEC_NRF_
|
||||
/** Master clock frequency */
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
|
||||
#define USE_LFXO // Board uses 32khz crystal for LF
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Headers
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "WVariant.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
#define VEXT_ENABLE (0 + 26)
|
||||
#define VEXT_ON_VALUE HIGH
|
||||
|
||||
// ST7735S TFT LCD
|
||||
#define ST7735_CS (0 + 22)
|
||||
#define ST7735_RS (0 + 15) // DC
|
||||
#define ST7735_SDA (0 + 17) // MOSI
|
||||
#define ST7735_SCK (0 + 20)
|
||||
#define ST7735_RESET (0 + 13)
|
||||
#define ST7735_MISO -1
|
||||
#define ST7735_BUSY -1
|
||||
#define ST7735_BL (32 + 12)
|
||||
#define SPI_FREQUENCY 40000000
|
||||
#define SPI_READ_FREQUENCY 16000000
|
||||
#define SCREEN_ROTATE
|
||||
#define TFT_HEIGHT 160
|
||||
#define TFT_WIDTH 80
|
||||
#define TFT_OFFSET_X 24
|
||||
#define TFT_OFFSET_Y 0
|
||||
#define TFT_INVERT false
|
||||
#define SCREEN_TRANSITION_FRAMERATE 3 // fps
|
||||
#define DISPLAY_FORCE_SMALL_FONTS
|
||||
|
||||
// Number of pins defined in PinDescription array
|
||||
#define PINS_COUNT (48)
|
||||
#define NUM_DIGITAL_PINS (48)
|
||||
#define NUM_ANALOG_INPUTS (1)
|
||||
#define NUM_ANALOG_OUTPUTS (0)
|
||||
|
||||
// LEDs
|
||||
#define PIN_LED1 (0 + 28) // green (confirmed on 1.0 board)
|
||||
#define LED_BLUE PIN_LED1 // fake for bluefruit library
|
||||
#define LED_GREEN PIN_LED1
|
||||
#define LED_STATE_ON 1 // State when LED is lit
|
||||
|
||||
// #define HAS_NEOPIXEL // Enable the use of neopixels
|
||||
// #define NEOPIXEL_COUNT 2 // How many neopixels are connected
|
||||
// #define NEOPIXEL_DATA 14 // gpio pin used to send data to the neopixels
|
||||
// #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use
|
||||
|
||||
/*
|
||||
* Buttons
|
||||
*/
|
||||
#define PIN_BUTTON1 (32 + 10)
|
||||
// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular
|
||||
// GPIO
|
||||
|
||||
/*
|
||||
No longer populated on PCB
|
||||
*/
|
||||
#define PIN_SERIAL2_RX (0 + 9)
|
||||
#define PIN_SERIAL2_TX (0 + 10)
|
||||
|
||||
/*
|
||||
* I2C
|
||||
*/
|
||||
|
||||
#define WIRE_INTERFACES_COUNT 2
|
||||
|
||||
// I2C bus 0
|
||||
#define PIN_WIRE_SDA (0 + 7) // SDA
|
||||
#define PIN_WIRE_SCL (0 + 8) // SCL
|
||||
|
||||
// I2C bus 1
|
||||
#define PIN_WIRE1_SDA (0 + 4) // SDA (secondary bus)
|
||||
#define PIN_WIRE1_SCL (0 + 27) // SCL (secondary bus)
|
||||
|
||||
/*
|
||||
* Lora radio
|
||||
*/
|
||||
|
||||
#define USE_SX1262
|
||||
#define SX126X_CS (0 + 5) // FIXME - we really should define LORA_CS instead
|
||||
#define LORA_CS (0 + 5)
|
||||
#define SX126X_DIO1 (0 + 21)
|
||||
#define SX126X_BUSY (0 + 19)
|
||||
#define SX126X_RESET (0 + 16)
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
// ---- KCT8103L RF FRONT END CONFIGURATION ----
|
||||
// The heltec_wireless_tracker_v2 uses a KCT8103L FEM chip with integrated PA and LNA
|
||||
// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna
|
||||
// Control logic (from KCT8103L datasheet):
|
||||
// Transmit PA: CSD=1, CTX=1, CPS=1
|
||||
// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF)
|
||||
// Receive bypass: CSD=1, CTX=1, CPS=0
|
||||
// Shutdown: CSD=0, CTX=X, CPS=X
|
||||
// Pin mapping:
|
||||
// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH)
|
||||
// CSD (pin 4) -> GPIO12: Chip enable (HIGH=on, LOW=shutdown)
|
||||
// CTX (pin 6) -> GPIO41: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA)
|
||||
// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO30
|
||||
// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH)
|
||||
|
||||
#define USE_KCT8103L_PA
|
||||
#define LORA_PA_POWER (0 + 30) // VFEM_Ctrl - KCT8103L LDO power enable
|
||||
#define LORA_KCT8103L_PA_CSD (0 + 12) // CSD - KCT8103L chip enable (HIGH=on)
|
||||
#define LORA_KCT8103L_PA_CTX \
|
||||
(32 + 9) // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA)
|
||||
|
||||
/*
|
||||
* SPI Interfaces
|
||||
*/
|
||||
#define SPI_INTERFACES_COUNT 2
|
||||
|
||||
// For LORA, spi 0
|
||||
#define PIN_SPI_MISO (0 + 14)
|
||||
#define PIN_SPI_MOSI (0 + 11)
|
||||
#define PIN_SPI_SCK (32 + 8)
|
||||
|
||||
#define PIN_SPI1_MISO \
|
||||
ST7735_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO
|
||||
#define PIN_SPI1_MOSI ST7735_SDA
|
||||
#define PIN_SPI1_SCK ST7735_SCK
|
||||
|
||||
/*
|
||||
* GPS pins
|
||||
*/
|
||||
#define GPS_UC6580
|
||||
#define GPS_BAUDRATE 115200
|
||||
#define PIN_GPS_RESET (32 + 14) // An output to reset UC6580 GPS. As per datasheet, low for > 100ms will reset the UC6580
|
||||
#define GPS_RESET_MODE LOW
|
||||
#define PIN_GPS_EN (0 + 6)
|
||||
#define GPS_EN_ACTIVE LOW
|
||||
#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing
|
||||
#define PIN_GPS_PPS (32 + 11)
|
||||
#define GPS_TX_PIN (0 + 25) // This is for bits going TOWARDS the CPU
|
||||
#define GPS_RX_PIN (0 + 23) // This is for bits going TOWARDS the GPS
|
||||
|
||||
#define GPS_THREAD_INTERVAL 50
|
||||
|
||||
#define PIN_SERIAL1_RX GPS_RX_PIN
|
||||
#define PIN_SERIAL1_TX GPS_TX_PIN
|
||||
|
||||
#define ADC_CTRL (32 + 15)
|
||||
#define ADC_CTRL_ENABLED HIGH
|
||||
#define BATTERY_PIN (0 + 3)
|
||||
#define ADC_RESOLUTION 14
|
||||
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#define BATTERY_SENSE_RESOLUTION 4096.0
|
||||
#undef AREF_VOLTAGE
|
||||
#define AREF_VOLTAGE 3.0
|
||||
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
|
||||
#define ADC_MULTIPLIER (4.916F)
|
||||
|
||||
// rf52840 AIN1 = Pin 3
|
||||
#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_1
|
||||
|
||||
// We have AIN1 with a VBAT divider so AIN1 = VBAT * (100/490)
|
||||
// We have the device going deep sleep under 3.1V, which is AIN1 = 0.63V
|
||||
// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN2 = 0.67V
|
||||
// Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means
|
||||
// VBAT=4.04V
|
||||
#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8
|
||||
|
||||
#define HAS_RTC 0
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Arduino objects - C++ only
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
#endif
|
||||
@@ -154,6 +154,15 @@ extern "C" {
|
||||
|
||||
#define HAS_SCREEN 0
|
||||
|
||||
// Enable Traffic Management Module for testing on T1000-E
|
||||
// NRF52840 has 256KB RAM - 1024 entries uses ~10KB
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
||||
#endif
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1024
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user