mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-21 10:42:35 +10:00
Compare commits
129
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ff7bc322f | ||
|
|
7adfc3f992 | ||
|
|
ba9cadc14d | ||
|
|
2815c9abc0 | ||
|
|
761ac4e08a | ||
|
|
f8368f5bd2 | ||
|
|
f60d329574 | ||
|
|
897c591ffe | ||
|
|
924411de59 | ||
|
|
d9195944df | ||
|
|
83a98c81f6 | ||
|
|
837637b70c | ||
|
|
e5caf2d52f | ||
|
|
53413ee502 | ||
|
|
04b5f14969 | ||
|
|
7b3f58875a | ||
|
|
b2d980fc25 | ||
|
|
2ed7bba5e7 | ||
|
|
22d50fe437 | ||
|
|
cde5a08bc5 | ||
|
|
55bf8c25fc | ||
|
|
399dde0f4b | ||
|
|
66971a0a26 | ||
|
|
4c24218afb | ||
|
|
4b4914736f | ||
|
|
a6b1a69630 | ||
|
|
28e705de5c | ||
|
|
fcb9ec0c2d | ||
|
|
a4b55bc6f2 | ||
|
|
b53fe7a1e7 | ||
|
|
db9fdd6794 | ||
|
|
3b4c66439d | ||
|
|
945f4780ea | ||
|
|
0e38a15d46 | ||
|
|
63bce1f01a | ||
|
|
4090d9f2b3 | ||
|
|
d50caf231b | ||
|
|
f396200d38 | ||
|
|
6c04c37294 | ||
|
|
34aa5e995b | ||
|
|
c8dac10348 | ||
|
|
aab4cd086f | ||
|
|
6208c243f9 | ||
|
|
3a87e746a8 | ||
|
|
cd04206334 | ||
|
|
2768080edf | ||
|
|
466cc4cecd | ||
|
|
fe90c49795 | ||
|
|
31418ca821 | ||
|
|
0ee5777c15 | ||
|
|
026213aab7 | ||
|
|
0cab43fb43 | ||
|
|
1341cd4078 | ||
|
|
4059202a5c | ||
|
|
d24d8806e1 | ||
|
|
a67eb15ad3 | ||
|
|
9e182a595c | ||
|
|
4587dc2d64 | ||
|
|
00fccd87f9 | ||
|
|
01bd4cfb73 | ||
|
|
125c1b7f13 | ||
|
|
d6cf67b6bc | ||
|
|
77f378dd53 | ||
|
|
16dcafa7fb | ||
|
|
4ac74edf38 | ||
|
|
69495dcd98 | ||
|
|
288d7a3e7b | ||
|
|
4990bf51e3 | ||
|
|
b2c8cbb78d | ||
|
|
8061f83704 | ||
|
|
a3b49b9028 | ||
|
|
17945884a5 | ||
|
|
1116f06139 | ||
|
|
c728cfdaf4 | ||
|
|
9322bcdb21 | ||
|
|
2f19a1d7a4 | ||
|
|
71c8143f72 | ||
|
|
222d3e6b62 | ||
|
|
2e6519bb98 | ||
|
|
726d539174 | ||
|
|
e7ee4bea18 | ||
|
|
efd2613bd7 | ||
|
|
f88bc732cc | ||
|
|
0746f8cfff | ||
|
|
0ac95e370b | ||
|
|
283ccb83d1 | ||
|
|
5319bc7c2c | ||
|
|
db694f2f24 | ||
|
|
814f1289f6 | ||
|
|
0b7556b590 | ||
|
|
f7e4ac3e43 | ||
|
|
c36ae159ed | ||
|
|
33e7f16c05 | ||
|
|
d3a86841b9 | ||
|
|
e14b8d385a | ||
|
|
8ce1a872eb | ||
|
|
abfa346630 | ||
|
|
2ffe2d829c | ||
|
|
bfaf6c6b20 | ||
|
|
d293d654a0 | ||
|
|
8384659608 | ||
|
|
a632d0133f | ||
|
|
f982b58d61 | ||
|
|
efb0b299b2 | ||
|
|
eb80d3141d | ||
|
|
3604c1255d | ||
|
|
22d63fa69c | ||
|
|
fc030d2d19 | ||
|
|
4a534f02a4 | ||
|
|
0ea408e12b | ||
|
|
644d0d4a15 | ||
|
|
f04746a928 | ||
|
|
1be2529fb9 | ||
|
|
959756abf1 | ||
|
|
c88b802e32 | ||
|
|
9f74fc11de | ||
|
|
2ef09d17b9 | ||
|
|
19d070c284 | ||
|
|
e24db2994b | ||
|
|
ac7a58cd45 | ||
|
|
53c21eb30d | ||
|
|
e51e6cad84 | ||
|
|
4890f7084f | ||
|
|
3fcbfe4370 | ||
|
|
da808cb43b | ||
|
|
82580c6798 | ||
|
|
016e68ec53 | ||
|
|
79f469ce71 | ||
|
|
0501e177e9 |
@@ -13,7 +13,6 @@ Meshtastic is an open-source LoRa mesh networking project for long-range, low-po
|
|||||||
- **RP2040/RP2350** - Raspberry Pi Pico variants
|
- **RP2040/RP2350** - Raspberry Pi Pico variants
|
||||||
- **STM32WL** - STM32 with integrated LoRa
|
- **STM32WL** - STM32 with integrated LoRa
|
||||||
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
|
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
|
||||||
- **macOS native** - Headless `meshtasticd` on Apple Silicon / x86_64; see `variants/native/portduino/platformio.ini` for Homebrew prereqs + CH341 LoRa setup
|
|
||||||
|
|
||||||
### Supported Radio Chips
|
### Supported Radio Chips
|
||||||
|
|
||||||
@@ -370,7 +369,7 @@ To reduce avoidable agent mistakes, assume these tools are available (or install
|
|||||||
- **Required CLI basics**: `bash`, `git`, `find`, `grep`, `sed`, `awk`, `xargs`
|
- **Required CLI basics**: `bash`, `git`, `find`, `grep`, `sed`, `awk`, `xargs`
|
||||||
- **Strongly recommended**: `rg` (ripgrep) for fast file/text search, `jq` for JSON processing
|
- **Strongly recommended**: `rg` (ripgrep) for fast file/text search, `jq` for JSON processing
|
||||||
- **Build/test tools**: `python3`, `pip`, virtualenv (`python3 -m venv`), `platformio` (`pio`)
|
- **Build/test tools**: `python3`, `pip`, virtualenv (`python3 -m venv`), `platformio` (`pio`)
|
||||||
- **Containerized native testing**: `docker` (fallback for non-Linux hosts; macOS can also build natively via `pio run -e native-macos`)
|
- **Containerized native testing**: `docker` (especially important on macOS / non-Linux hosts)
|
||||||
|
|
||||||
Fallback expectations for agents:
|
Fallback expectations for agents:
|
||||||
|
|
||||||
@@ -389,7 +388,6 @@ Build commands:
|
|||||||
pio run -e tbeam # Build specific target
|
pio run -e tbeam # Build specific target
|
||||||
pio run -e tbeam -t upload # Build and upload
|
pio run -e tbeam -t upload # Build and upload
|
||||||
pio run -e native # Build native/Linux version
|
pio run -e native # Build native/Linux version
|
||||||
pio run -e native-macos # Build headless macOS meshtasticd (Homebrew prereqs in variants/native/portduino/platformio.ini)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build Manifest
|
### Build Manifest
|
||||||
@@ -575,8 +573,6 @@ Grouped by purpose. Full argument shapes in `mcp-server/README.md`; a few high-v
|
|||||||
|
|
||||||
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
|
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
|
||||||
|
|
||||||
**TCP / native-host nodes.** Setting `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` makes `list_devices` surface a `meshtasticd` daemon (e.g. the `native-macos` build) as a synthetic `tcp://host:port` entry, and `connect()` routes through `meshtastic.tcp_interface.TCPInterface` instead of `SerialInterface`. Every read/write/admin tool that flows through `connect()` works against the daemon transparently. USB-only tools (`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`, `serial_open`, `esptool_*`, `nrfutil_*`, `picotool_*`) raise a clear `ConnectionError` when handed a `tcp://` port; `pio_flash` against a `native*` env raises a `FlashError` (no upload step — use `build` and run the binary directly). The pytest harness still assumes USB-attached devices per role; TCP-aware fixtures are deferred. See `mcp-server/README.md` § "TCP / native-host nodes".
|
|
||||||
|
|
||||||
### Hardware test suite (`mcp-server/run-tests.sh`)
|
### Hardware test suite (`mcp-server/run-tests.sh`)
|
||||||
|
|
||||||
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
|
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
name: Build MacOS Binary
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
macos_ver:
|
|
||||||
required: false
|
|
||||||
default: "26" # ARM64
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-MacOS:
|
|
||||||
runs-on: macos-${{ inputs.macos_ver }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Install deps
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
brew update
|
|
||||||
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
|
|
||||||
|
|
||||||
- name: Get release version string
|
|
||||||
run: |
|
|
||||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
|
||||||
id: version
|
|
||||||
|
|
||||||
- name: Build for MacOS
|
|
||||||
run: |
|
|
||||||
platformio run -e native-macos
|
|
||||||
env:
|
|
||||||
PKG_VERSION: ${{ steps.version.outputs.long }}
|
|
||||||
# Errors in this step should not fail the entire workflow while MacOS support is in development.
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: List output files
|
|
||||||
run: ls -lah .pio/build/native-macos/
|
|
||||||
|
|
||||||
- name: Store binaries as an artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: firmware-macos-${{ inputs.macos_ver }}-${{ steps.version.outputs.long }}
|
|
||||||
overwrite: true
|
|
||||||
path: |
|
|
||||||
.pio/build/native-macos/meshtasticd
|
|
||||||
@@ -4,9 +4,14 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
# trunk-ignore(checkov/CKV_GHA_7)
|
# trunk-ignore(checkov/CKV_GHA_7)
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
|
||||||
arch:
|
arch:
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
|
- all
|
||||||
- esp32
|
- esp32
|
||||||
- esp32s3
|
- esp32s3
|
||||||
- esp32c3
|
- esp32c3
|
||||||
@@ -15,32 +20,18 @@ on:
|
|||||||
- rp2040
|
- rp2040
|
||||||
- rp2350
|
- rp2350
|
||||||
- stm32
|
- stm32
|
||||||
target:
|
description: Choose an arch to limit the search, or 'all' to search all architectures.
|
||||||
type: string
|
default: all
|
||||||
required: false
|
|
||||||
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
|
|
||||||
# find-target:
|
|
||||||
# type: boolean
|
|
||||||
# default: true
|
|
||||||
# description: 'Find the available targets'
|
|
||||||
|
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
find-targets:
|
find-targets:
|
||||||
if: ${{ inputs.target == '' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch:
|
arch:
|
||||||
- esp32
|
- all
|
||||||
- esp32s3
|
|
||||||
- esp32c3
|
|
||||||
- esp32c6
|
|
||||||
- nrf52840
|
|
||||||
- rp2040
|
|
||||||
- rp2350
|
|
||||||
- stm32
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -51,14 +42,37 @@ jobs:
|
|||||||
- run: pip install -U platformio
|
- run: pip install -U platformio
|
||||||
- name: Generate matrix
|
- name: Generate matrix
|
||||||
id: jsonStep
|
id: jsonStep
|
||||||
|
env:
|
||||||
|
BUILDTARGET: ${{ inputs.target }}
|
||||||
|
MATRIXARCH: ${{ inputs.arch }}
|
||||||
run: |
|
run: |
|
||||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra)
|
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra)
|
||||||
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
|
if [ "$BUILDTARGET" = "" ]; then
|
||||||
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
|
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
|
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
|
echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Targets:" >> $GITHUB_STEP_SUMMARY
|
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
|
||||||
echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY
|
echo "## 🎯 The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo $TARGETS | jq -r 'sort_by(.board) | sort_by(.platform) |.[] | "| " + .platform + " | " + .board + " |" ' >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "We build this one:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ARCH=$(echo "$TARGETS" | jq --arg BUILDTARGET "$BUILDTARGET" -r '.[] | select(.board==$BUILDTARGET) | .platform')
|
||||||
|
echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| $ARCH | "$BUILDTARGET" |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [[ "$ARCH" == "" ]]; then
|
||||||
|
echo "## ❌ Error: Target "$BUILDTARGET" not found!" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "## ✅ Target "$BUILDTARGET" found, proceeding to build." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "You may need to refresh this page to make the built firmware appear below." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "arch=$ARCH" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
outputs:
|
||||||
|
arch: ${{ steps.jsonStep.outputs.arch }}
|
||||||
|
|
||||||
version:
|
version:
|
||||||
if: ${{ inputs.target != '' }}
|
if: ${{ inputs.target != '' }}
|
||||||
@@ -78,12 +92,12 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
|
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
|
||||||
needs: [version]
|
needs: [version, find-targets]
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
version: ${{ needs.version.outputs.long }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
pio_env: ${{ inputs.target }}
|
pio_env: ${{ inputs.target }}
|
||||||
platform: ${{ inputs.arch }}
|
platform: ${{ needs.find-targets.outputs.arch }}
|
||||||
|
|
||||||
gather-artifacts:
|
gather-artifacts:
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ jobs:
|
|||||||
- name: Sanitize platform string
|
- name: Sanitize platform string
|
||||||
id: sanitize_platform
|
id: sanitize_platform
|
||||||
# Replace slashes with underscores
|
# Replace slashes with underscores
|
||||||
env:
|
run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
||||||
plat: ${{ inputs.platform }}
|
|
||||||
run: echo "cleaned_platform=${plat}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Docker login
|
- name: Docker login
|
||||||
if: ${{ inputs.push }}
|
if: ${{ inputs.push }}
|
||||||
|
|||||||
@@ -43,15 +43,6 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
docker-debian-riscv64:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: debian
|
|
||||||
platform: linux/riscv64
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
push: true
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
docker-alpine-amd64:
|
docker-alpine-amd64:
|
||||||
uses: ./.github/workflows/docker_build.yml
|
uses: ./.github/workflows/docker_build.yml
|
||||||
with:
|
with:
|
||||||
@@ -79,27 +70,16 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
docker-alpine-riscv64:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: alpine
|
|
||||||
platform: linux/riscv64
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
push: true
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
docker-manifest:
|
docker-manifest:
|
||||||
needs:
|
needs:
|
||||||
# Debian
|
# Debian
|
||||||
- docker-debian-amd64
|
- docker-debian-amd64
|
||||||
- docker-debian-arm64
|
- docker-debian-arm64
|
||||||
- docker-debian-armv7
|
- docker-debian-armv7
|
||||||
- docker-debian-riscv64
|
|
||||||
# Alpine
|
# Alpine
|
||||||
- docker-alpine-amd64
|
- docker-alpine-amd64
|
||||||
- docker-alpine-arm64
|
- docker-alpine-arm64
|
||||||
- docker-alpine-armv7
|
- docker-alpine-armv7
|
||||||
- docker-alpine-riscv64
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -182,7 +162,6 @@ jobs:
|
|||||||
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-debian-riscv64.outputs.digest }}
|
|
||||||
|
|
||||||
- name: Docker meta (Alpine)
|
- name: Docker meta (Alpine)
|
||||||
id: meta_alpine
|
id: meta_alpine
|
||||||
@@ -203,4 +182,3 @@ jobs:
|
|||||||
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-alpine-riscv64.outputs.digest }}
|
|
||||||
|
|||||||
@@ -116,20 +116,6 @@ jobs:
|
|||||||
build_location: local
|
build_location: local
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
MacOS:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
macos_ver:
|
|
||||||
- "26" # ARM64
|
|
||||||
# - '26-intel' # x86_64
|
|
||||||
- "15" # ARM64
|
|
||||||
# - '15-intel' # x86_64
|
|
||||||
uses: ./.github/workflows/build_macos_bin.yml
|
|
||||||
with:
|
|
||||||
macos_ver: ${{ matrix.macos_ver }}
|
|
||||||
# secrets: inherit
|
|
||||||
|
|
||||||
package-pio-deps-native-tft:
|
package-pio-deps-native-tft:
|
||||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
||||||
uses: ./.github/workflows/package_pio_deps.yml
|
uses: ./.github/workflows/package_pio_deps.yml
|
||||||
@@ -300,7 +286,6 @@ jobs:
|
|||||||
- gather-artifacts
|
- gather-artifacts
|
||||||
- build-debian-src
|
- build-debian-src
|
||||||
- package-pio-deps-native-tft
|
- package-pio-deps-native-tft
|
||||||
# - MacOS
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -86,7 +86,13 @@ jobs:
|
|||||||
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
||||||
|
|
||||||
- name: PlatformIO Tests
|
- name: PlatformIO Tests
|
||||||
run: platformio test -e coverage -v --junit-output-path testreport.xml
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
# Filter out SKIPPED summary rows for hardware variants that can't run on the
|
||||||
|
# native host. They flood the log and make it harder to spot real failures.
|
||||||
|
# The JUnit XML is written directly to testreport.xml before the pipe, so
|
||||||
|
# the test artifact is unaffected.
|
||||||
|
platformio test -e coverage -v --junit-output-path testreport.xml 2>&1 | grep -v "[[:space:]]SKIPPED$"
|
||||||
|
|
||||||
- name: Save test results
|
- name: Save test results
|
||||||
if: always() # run this step even if previous step failed
|
if: always() # run this step even if previous step failed
|
||||||
|
|||||||
+5
-5
@@ -4,19 +4,19 @@ cli:
|
|||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.8.0
|
ref: v1.7.6
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.525
|
- checkov@3.2.524
|
||||||
- renovate@43.150.0
|
- renovate@43.139.6
|
||||||
- prettier@3.8.3
|
- prettier@3.8.3
|
||||||
- trufflehog@3.95.2
|
- trufflehog@3.95.2
|
||||||
- yamllint@1.38.0
|
- yamllint@1.38.0
|
||||||
- bandit@1.9.4
|
- bandit@1.9.4
|
||||||
- trivy@0.70.0
|
- trivy@0.70.0
|
||||||
- taplo@0.10.0
|
- taplo@0.10.0
|
||||||
- ruff@0.15.12
|
- ruff@0.15.11
|
||||||
- isort@8.0.1
|
- isort@8.0.1
|
||||||
- markdownlint@0.48.0
|
- markdownlint@0.48.0
|
||||||
- oxipng@10.1.1
|
- oxipng@10.1.1
|
||||||
@@ -36,7 +36,7 @@ lint:
|
|||||||
- bin/**
|
- bin/**
|
||||||
runtimes:
|
runtimes:
|
||||||
enabled:
|
enabled:
|
||||||
- python@3.14.4
|
- python@3.10.8
|
||||||
- go@1.21.0
|
- go@1.21.0
|
||||||
- node@22.16.0
|
- node@22.16.0
|
||||||
actions:
|
actions:
|
||||||
|
|||||||
@@ -10,18 +10,17 @@ This file (`AGENTS.md`) is a short pointer + quick reference for agents that don
|
|||||||
|
|
||||||
## Quick command reference
|
## Quick command reference
|
||||||
|
|
||||||
| Action | Command |
|
| Action | Command |
|
||||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
| -------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||||
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
|
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
||||||
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
||||||
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
| Run firmware unit tests (native) | `pio test -e native` |
|
||||||
| Run firmware unit tests (native) | `pio test -e native` |
|
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||||
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
| Format before commit | `trunk fmt` |
|
||||||
| Format before commit | `trunk fmt` |
|
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
|
||||||
|
|
||||||
## MCP server (device + test automation)
|
## MCP server (device + test automation)
|
||||||
|
|
||||||
@@ -122,21 +121,19 @@ Sequence these; don't parallelize on the same port.
|
|||||||
- **Device fully wedged (no DFU)?** `mcp__meshtastic__uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles it via USB hub PPPS. Needs `uhubctl` installed (`brew install uhubctl` / `apt install uhubctl`); on Linux without udev rules, permission errors fail fast, so use `sudo uhubctl` yourself or configure udev access.
|
- **Device fully wedged (no DFU)?** `mcp__meshtastic__uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles it via USB hub PPPS. Needs `uhubctl` installed (`brew install uhubctl` / `apt install uhubctl`); on Linux without udev rules, permission errors fail fast, so use `sudo uhubctl` yourself or configure udev access.
|
||||||
- **Port busy?** `lsof <port>` to find the holder. Usually a stale `pio device monitor` or zombie `meshtastic_mcp` process. Kill it.
|
- **Port busy?** `lsof <port>` to find the holder. Usually a stale `pio device monitor` or zombie `meshtastic_mcp` process. Kill it.
|
||||||
- **Multiple MCP servers running?** `ps aux | grep meshtastic_mcp` — zombies hold ports. Kill all but the one your host spawned.
|
- **Multiple MCP servers running?** `ps aux | grep meshtastic_mcp` — zombies hold ports. Kill all but the one your host spawned.
|
||||||
- **macOS: `LIBUSB_ERROR_BUSY` on a CH341 LoRa adapter?** A third-party WCH `CH34xVCPDriver` is claiming interface 0. Find the bundle ID with `ioreg -p IOUSB -l -w 0 | grep -B2 -A30 0x5512`, then `sudo kmutil unload -b <bundleID>`. Apple's bundled CH34x kext targets the CH340 UART (PID 0x7523), not the SPI bridge — it's never the culprit.
|
|
||||||
|
|
||||||
## Environment variables (test harness)
|
## Environment variables (test harness)
|
||||||
|
|
||||||
| Var | Purpose |
|
| Var | Purpose |
|
||||||
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
|
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
|
||||||
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
|
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
|
||||||
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
|
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
|
||||||
| `MESHTASTIC_MCP_TCP_HOST` | `host` or `host:port` of a `meshtasticd` daemon (e.g. the `native-macos` build). Surfaces it in `list_devices` as `tcp://host:port` so `connect()`-based tools target it transparently. Default port 4403. |
|
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
|
||||||
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
|
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
|
||||||
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
|
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
|
||||||
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
|
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
|
||||||
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
|
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
|
||||||
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
|
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
|
||||||
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
|
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
|
||||||
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
|
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
|
||||||
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
|
|
||||||
|
|||||||
+1
-3
@@ -3,17 +3,15 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
FROM debian:trixie AS builder
|
FROM python:3.14-slim-trixie AS builder
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV TZ=Etc/UTC
|
ENV TZ=Etc/UTC
|
||||||
|
|
||||||
# Install Dependencies
|
# Install Dependencies
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
curl wget g++ zip git ca-certificates pkg-config \
|
curl wget g++ zip git ca-certificates pkg-config \
|
||||||
python3-pip python3-grpc-tools \
|
|
||||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||||
|
|||||||
+2
-9
@@ -3,19 +3,12 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
||||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
# Ensure the Alpine version is updated in both stages of the container!
|
FROM python:3.14-alpine3.22 AS builder
|
||||||
FROM alpine:3.23 AS builder
|
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
|
|
||||||
# Enable Alpine community repository (for 'py3-grpcio-tools')
|
|
||||||
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v$(cut -d. -f1,2 /etc/alpine-release)/community" >> /etc/apk/repositories
|
|
||||||
|
|
||||||
# Install Dependencies
|
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||||
py3-pip py3-grpcio-tools \
|
|
||||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||||
|
|||||||
+1
-2
@@ -31,6 +31,5 @@ basename=meshtasticd-$1-$VERSION
|
|||||||
pio pkg install --environment "$PIO_ENV" || platformioFailed
|
pio pkg install --environment "$PIO_ENV" || platformioFailed
|
||||||
pio run --environment "$PIO_ENV" || platformioFailed
|
pio run --environment "$PIO_ENV" || platformioFailed
|
||||||
|
|
||||||
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
|
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||||
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_${os_name}_$(uname -m)"
|
|
||||||
cp bin/native-install.* $OUTDIR/
|
cp bin/native-install.* $OUTDIR/
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
Lora:
|
||||||
|
## Ebyte E80-900M22S
|
||||||
|
## This is a bit experimental
|
||||||
|
##
|
||||||
|
##
|
||||||
|
Module: lr1121
|
||||||
|
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||||
|
DIO3_TCXO_VOLTAGE: 1.8
|
||||||
|
CS: 16 #pin6 / GPIO48 1C0
|
||||||
|
IRQ: 23 #pin17 / GPIO55 1C7
|
||||||
|
Busy: 22 #pin16 / GPIO54 1C6
|
||||||
|
Reset: 25 #pin13 / GPIO57 1D1
|
||||||
|
|
||||||
|
|
||||||
|
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||||
|
spiSpeed: 2000000
|
||||||
|
|
||||||
|
rfswitch_table:
|
||||||
|
pins: [DIO5, DIO6, DIO7]
|
||||||
|
MODE_STBY: [LOW, LOW, LOW]
|
||||||
|
MODE_RX: [LOW, HIGH, LOW]
|
||||||
|
MODE_TX: [HIGH, HIGH, LOW]
|
||||||
|
MODE_TX_HP: [HIGH, LOW, LOW]
|
||||||
|
MODE_TX_HF: [LOW, LOW, LOW]
|
||||||
|
MODE_GNSS: [LOW, LOW, HIGH]
|
||||||
|
MODE_WIFI: [LOW, LOW, LOW]
|
||||||
|
|
||||||
|
General:
|
||||||
|
MACAddressSource: eth0
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
Lora:
|
||||||
|
## Ebyte E80-900M22S
|
||||||
|
## This is a bit experimental
|
||||||
|
##
|
||||||
|
##
|
||||||
|
Module: lr1121
|
||||||
|
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||||
|
DIO3_TCXO_VOLTAGE: 1.8
|
||||||
|
CS: 16 #pin6 / GPIO48 1C0
|
||||||
|
IRQ: 23 #pin17 / GPIO55 1C7
|
||||||
|
Busy: 22 #pin16 / GPIO54 1C6
|
||||||
|
Reset: 25 #pin13 / GPIO57 1D1
|
||||||
|
|
||||||
|
|
||||||
|
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||||
|
spiSpeed: 2000000
|
||||||
|
|
||||||
|
rfswitch_table:
|
||||||
|
pins:
|
||||||
|
- DIO5
|
||||||
|
- DIO6
|
||||||
|
MODE_STBY:
|
||||||
|
- LOW
|
||||||
|
- LOW
|
||||||
|
MODE_RX:
|
||||||
|
- HIGH
|
||||||
|
- LOW
|
||||||
|
MODE_TX:
|
||||||
|
- HIGH
|
||||||
|
- HIGH
|
||||||
|
MODE_TX_HP:
|
||||||
|
- LOW
|
||||||
|
- HIGH
|
||||||
|
MODE_TX_HF:
|
||||||
|
- LOW
|
||||||
|
- LOW
|
||||||
|
MODE_GNSS:
|
||||||
|
- LOW
|
||||||
|
- LOW
|
||||||
|
MODE_WIFI:
|
||||||
|
- LOW
|
||||||
|
- LOW
|
||||||
|
|
||||||
|
General:
|
||||||
|
MACAddressSource: eth0
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
Lora:
|
||||||
|
## Ebyte E80-900M22S
|
||||||
|
## This is a bit experimental
|
||||||
|
##
|
||||||
|
##
|
||||||
|
Module: lr1121
|
||||||
|
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||||
|
DIO3_TCXO_VOLTAGE: 1.8
|
||||||
|
CS: 16 #pin6 / GPIO48 1C0
|
||||||
|
IRQ: 23 #pin17 / GPIO55 1C7
|
||||||
|
Busy: 22 #pin16 / GPIO54 1C6
|
||||||
|
Reset: 25 #pin13 / GPIO57 1D1
|
||||||
|
|
||||||
|
|
||||||
|
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||||
|
spiSpeed: 2000000
|
||||||
|
|
||||||
|
rfswitch_table:
|
||||||
|
pins: [DIO5, DIO6, DIO7]
|
||||||
|
MODE_STBY: [LOW, LOW, LOW]
|
||||||
|
MODE_RX: [LOW, LOW, LOW]
|
||||||
|
MODE_TX: [LOW, HIGH, LOW]
|
||||||
|
MODE_TX_HP: [HIGH, LOW, LOW]
|
||||||
|
# MODE_TX_HF: []
|
||||||
|
# MODE_GNSS: []
|
||||||
|
MODE_WIFI: [LOW, LOW, LOW]
|
||||||
|
|
||||||
|
General:
|
||||||
|
MACAddressSource: eth0
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script to show commits in develop that are not in master
|
|
||||||
# with their associated PR info and commit hashes
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./show-unmerged-prs.sh # Show all unmerged commits
|
|
||||||
# ./show-unmerged-prs.sh --bugfix # Show only bugfix-labeled PRs
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
REPO="firmware"
|
|
||||||
OWNER="meshtastic"
|
|
||||||
BASE_BRANCH="master"
|
|
||||||
HEAD_BRANCH="develop"
|
|
||||||
LIMIT=100
|
|
||||||
FILTER_LABEL=""
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
for arg in "$@"; do
|
|
||||||
case $arg in
|
|
||||||
--bugfix)
|
|
||||||
FILTER_LABEL="bugfix"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--feature)
|
|
||||||
FILTER_LABEL="feature"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
echo "Usage: $0 [OPTIONS]"
|
|
||||||
echo "Options:"
|
|
||||||
echo " --bugfix Show only PRs labeled with 'bugfix'"
|
|
||||||
echo " --feature Show only PRs labeled with 'feature'"
|
|
||||||
echo " --help Show this help message"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$FILTER_LABEL" ]; then
|
|
||||||
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH (filtered by label: $FILTER_LABEL)..."
|
|
||||||
else
|
|
||||||
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH..."
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if gh CLI is available
|
|
||||||
if ! command -v gh &> /dev/null; then
|
|
||||||
echo "ERROR: GitHub CLI (gh) not found. Please install it first."
|
|
||||||
echo "Visit: https://cli.github.com/"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get commits in develop that are not in master
|
|
||||||
# For each commit, try to find associated PR
|
|
||||||
git fetch origin develop master 2>/dev/null || true
|
|
||||||
|
|
||||||
# Use git to get the list of commits
|
|
||||||
commits=$(git log --pretty=format:"%H|%s" origin/master..origin/develop | head -n $LIMIT)
|
|
||||||
|
|
||||||
count=0
|
|
||||||
displayed=0
|
|
||||||
echo "Commits in $HEAD_BRANCH not in $BASE_BRANCH:"
|
|
||||||
echo "=============================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
while IFS='|' read -r hash subject; do
|
|
||||||
((count++))
|
|
||||||
|
|
||||||
# Try to find the PR for this commit
|
|
||||||
# Extract PR number, title, description, and labels
|
|
||||||
pr_response=$(gh api -X GET "/repos/$OWNER/$REPO/commits/$hash/pulls" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" 2>/dev/null | \
|
|
||||||
jq -r '.[0] | "\(.number)|\(.title)|\(.body // "No description")|\(.labels | map(.name) | join(","))"' 2>/dev/null || echo "||||")
|
|
||||||
|
|
||||||
if [ -z "$pr_response" ] || [ "$pr_response" = "||||" ]; then
|
|
||||||
# If no PR found, skip if filter is active, otherwise show the commit
|
|
||||||
if [ -z "$FILTER_LABEL" ]; then
|
|
||||||
((displayed++))
|
|
||||||
echo "[$displayed] Commit: $hash"
|
|
||||||
echo " Subject: $subject"
|
|
||||||
echo " PR: Not found in GitHub"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
IFS='|' read -r pr_num pr_title pr_desc pr_labels <<< "$pr_response"
|
|
||||||
|
|
||||||
# Check if filter matches
|
|
||||||
if [ -n "$FILTER_LABEL" ]; then
|
|
||||||
# Only show if the label is in the labels list
|
|
||||||
if ! echo "$pr_labels" | grep -q "$FILTER_LABEL"; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
((displayed++))
|
|
||||||
echo "[$displayed] PR #$pr_num - $pr_title"
|
|
||||||
echo " Commit: $hash"
|
|
||||||
if [ -n "$pr_desc" ] && [ "$pr_desc" != "No description" ]; then
|
|
||||||
# Truncate description to 200 chars
|
|
||||||
desc_short="${pr_desc:0:200}"
|
|
||||||
[ ${#pr_desc} -gt 200 ] && desc_short+="..."
|
|
||||||
echo " Description: $desc_short"
|
|
||||||
fi
|
|
||||||
if [ -n "$pr_labels" ] && [ "$pr_labels" != "" ]; then
|
|
||||||
echo " Labels: $pr_labels"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
done <<< "$commits"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
if [ -n "$FILTER_LABEL" ]; then
|
|
||||||
echo "Done. Showing $displayed PRs with label '$FILTER_LABEL' from $displayed commits checked."
|
|
||||||
else
|
|
||||||
echo "Done. Showing $displayed commits from $HEAD_BRANCH not in $BASE_BRANCH."
|
|
||||||
fi
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"build": {
|
|
||||||
"arduino": {
|
|
||||||
"ldscript": "esp32s3_out.ld",
|
|
||||||
"partitions": "default_16MB.csv",
|
|
||||||
"memory_type": "qio_opi"
|
|
||||||
},
|
|
||||||
"core": "esp32",
|
|
||||||
"extra_flags": [
|
|
||||||
"-DBOARD_HAS_PSRAM",
|
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
|
||||||
"-DARDUINO_USB_MODE=1",
|
|
||||||
"-DARDUINO_RUNNING_CORE=1",
|
|
||||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
|
||||||
],
|
|
||||||
"f_cpu": "240000000L",
|
|
||||||
"f_flash": "80000000L",
|
|
||||||
"flash_mode": "qio",
|
|
||||||
"psram_type": "opi",
|
|
||||||
"hwids": [["0x303A", "0x1001"]],
|
|
||||||
"mcu": "esp32s3",
|
|
||||||
"variant": "heltec_v4_r8"
|
|
||||||
},
|
|
||||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
|
||||||
"debug": {
|
|
||||||
"default_tool": "esp-builtin",
|
|
||||||
"onboard_tools": ["esp-builtin"],
|
|
||||||
"openocd_target": "esp32s3.cfg"
|
|
||||||
},
|
|
||||||
"frameworks": ["arduino", "espidf"],
|
|
||||||
"name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)",
|
|
||||||
"upload": {
|
|
||||||
"flash_size": "16MB",
|
|
||||||
"maximum_ram_size": 327680,
|
|
||||||
"maximum_size": 16777216,
|
|
||||||
"use_1200bps_touch": true,
|
|
||||||
"wait_for_upload_port": true,
|
|
||||||
"require_upload_port": true,
|
|
||||||
"speed": 921600
|
|
||||||
},
|
|
||||||
"url": "https://heltec.org/",
|
|
||||||
"vendor": "heltec"
|
|
||||||
}
|
|
||||||
Vendored
-1
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
set -e
|
|
||||||
export DEBEMAIL="jbennett@incomsystems.biz"
|
export DEBEMAIL="jbennett@incomsystems.biz"
|
||||||
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
|
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
|
||||||
export PLATFORMIO_PACKAGES_DIR=pio/packages
|
export PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||||
|
|||||||
+9
-67
@@ -166,73 +166,15 @@ rather than auto-`sudo`'ing mid-run.
|
|||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Var | Default | Purpose |
|
| Var | Default | Purpose |
|
||||||
| -------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
| -------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||||
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
|
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
|
||||||
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
|
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
|
||||||
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
|
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
|
||||||
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
|
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
|
||||||
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
|
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
|
||||||
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
|
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
|
||||||
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
|
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
|
||||||
| `MESHTASTIC_MCP_TCP_HOST` | unset | `host` or `host:port` of a `meshtasticd` daemon to surface as a TCP device (see "TCP / native-host nodes" below) |
|
|
||||||
|
|
||||||
## TCP / native-host nodes
|
|
||||||
|
|
||||||
The `native-macos` and `native` PlatformIO envs build a headless `meshtasticd`
|
|
||||||
binary that runs on the host (Apple Silicon / Intel macOS, or Linux Portduino).
|
|
||||||
The daemon exposes the meshtastic TCP API on port `4403` rather than a USB
|
|
||||||
serial endpoint — point the MCP server at it via `MESHTASTIC_MCP_TCP_HOST`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build + run a daemon on this host (see variants/native/portduino/platformio.ini
|
|
||||||
# for full Homebrew prereqs and CH341 LoRa-adapter setup).
|
|
||||||
pio run -e native-macos
|
|
||||||
~/.meshtasticd/meshtasticd
|
|
||||||
|
|
||||||
# 2. Point the MCP server at it.
|
|
||||||
export MESHTASTIC_MCP_TCP_HOST=localhost # or host:port, default port 4403
|
|
||||||
```
|
|
||||||
|
|
||||||
**First-run gotcha — MAC address.** `meshtasticd` derives its MAC from the
|
|
||||||
USB adapter's serial-number / product strings. Many cheap CH341 dongles
|
|
||||||
(MeshStick included — VID 0x1A86 / PID 0x5512) ship with `iSerialNumber=0`
|
|
||||||
and `iProduct=0`, so the daemon aborts on boot with `*** Blank MAC Address
|
|
||||||
not allowed!`. Set the MAC explicitly in `config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Under General:
|
|
||||||
MACAddress: 02:CA:FE:BA:BE:01
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a locally-administered address (first byte's second-LSB set, e.g.
|
|
||||||
`02:*` / `06:*` / `0A:*` / `0E:*`) to avoid colliding with a real OUI.
|
|
||||||
|
|
||||||
There is also a `--hwid AA:BB:CC:DD:EE:FF` CLI flag visible in
|
|
||||||
`meshtasticd --help`, but it is **currently broken** in
|
|
||||||
`MAC_from_string()` (`src/platform/portduino/PortduinoGlue.cpp`): the
|
|
||||||
function strips colons from its parameter but then reads bytes from the
|
|
||||||
global `portduino_config.mac_address`, so `--hwid` is silently overridden
|
|
||||||
when `MACAddress:` is also set, and crashes the daemon (uncaught
|
|
||||||
`std::invalid_argument: stoi: no conversion`) when it isn't. Use the YAML
|
|
||||||
form until that's fixed upstream.
|
|
||||||
|
|
||||||
`list_devices` will surface the daemon as `tcp://localhost:4403` with
|
|
||||||
`likely_meshtastic=True`, so `device_info`, `list_nodes`, `get_config`,
|
|
||||||
`set_config`, `set_owner`, `send_text`, `userprefs_*`, and the admin RPCs
|
|
||||||
auto-select it when no `port` is passed. Pass `port="tcp://other-host:9999"`
|
|
||||||
explicitly to target a different daemon.
|
|
||||||
|
|
||||||
**Tools that don't apply to a TCP/native node** (no USB hardware to operate
|
|
||||||
on) raise a clear `ConnectionError` rather than failing mysteriously:
|
|
||||||
`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`,
|
|
||||||
`serial_open` (use info/admin tools directly), and the vendor escape hatches
|
|
||||||
`esptool_*`, `nrfutil_*`, `picotool_*`. `pio_flash` against a `native*` env
|
|
||||||
similarly raises — there's no upload step; use `build` and run the binary
|
|
||||||
directly.
|
|
||||||
|
|
||||||
The pytest harness in `tests/` still assumes USB-attached devices per role —
|
|
||||||
TCP-aware fixtures are not part of this surface yet.
|
|
||||||
|
|
||||||
## Hardware Test Suite
|
## Hardware Test Suite
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Context manager for meshtastic interface connections (serial + TCP).
|
"""Context manager for meshtastic.SerialInterface connections.
|
||||||
|
|
||||||
Every info/admin tool goes through `connect(port)` so we have a single place
|
Every info/admin tool goes through `connect(port)` so we have a single place
|
||||||
that:
|
that:
|
||||||
@@ -6,16 +6,8 @@ that:
|
|||||||
- fails fast if a serial_session is already holding the port,
|
- fails fast if a serial_session is already holding the port,
|
||||||
- guarantees `.close()` is called, even on exception.
|
- guarantees `.close()` is called, even on exception.
|
||||||
|
|
||||||
Two transports:
|
The `SerialInterface` blocks on construction waiting for the node database;
|
||||||
- Serial: USB-attached firmware on `/dev/cu.*` / `/dev/ttyUSB*` / `COM*`.
|
that's fine for v1 since every tool is a short-lived request.
|
||||||
- TCP: a `meshtasticd` daemon (e.g. the native macOS / Linux Portduino
|
|
||||||
headless build) addressed as `tcp://host[:port]` (default port 4403).
|
|
||||||
Surfaced by `devices.list_devices()` when `MESHTASTIC_MCP_TCP_HOST` is
|
|
||||||
set, so `resolve_port(None)` auto-selects it like a USB candidate.
|
|
||||||
|
|
||||||
Both `SerialInterface` and `TCPInterface` block on construction waiting for
|
|
||||||
the node database; that's fine for v1 since every tool is a short-lived
|
|
||||||
request.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -25,107 +17,20 @@ from typing import Iterator
|
|||||||
|
|
||||||
from . import devices, registry
|
from . import devices, registry
|
||||||
|
|
||||||
DEFAULT_TCP_PORT = 4403
|
|
||||||
TCP_SCHEME = "tcp://"
|
|
||||||
TCP_HOST_ENV = "MESHTASTIC_MCP_TCP_HOST"
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionError(RuntimeError):
|
class ConnectionError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def is_tcp_port(port: str | None) -> bool:
|
|
||||||
return bool(port) and port.startswith(TCP_SCHEME)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_tcp_port(port: str) -> tuple[str, int]:
|
|
||||||
"""Parse `tcp://host[:port]` → (host, port). Defaults to 4403.
|
|
||||||
|
|
||||||
Validates host shape (non-empty, no path separators) and port range
|
|
||||||
(1..65535). Raises `ConnectionError` on malformed input — never lets
|
|
||||||
a raw `ValueError` bubble up to a tool surface.
|
|
||||||
"""
|
|
||||||
if not port.startswith(TCP_SCHEME):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: expected '{TCP_SCHEME}host[:port]'."
|
|
||||||
)
|
|
||||||
rest = port[len(TCP_SCHEME) :]
|
|
||||||
if ":" in rest:
|
|
||||||
host, port_str = rest.rsplit(":", 1)
|
|
||||||
try:
|
|
||||||
tcp_port = int(port_str)
|
|
||||||
except ValueError as e:
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: port {port_str!r} is not an integer."
|
|
||||||
) from e
|
|
||||||
else:
|
|
||||||
host, tcp_port = rest, DEFAULT_TCP_PORT
|
|
||||||
if not host:
|
|
||||||
raise ConnectionError(f"Invalid TCP endpoint {port!r}: empty host.")
|
|
||||||
if any(c in host for c in ("/", "\\")):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: host {host!r} contains a path "
|
|
||||||
"separator. TCP hostnames cannot contain '/' or '\\' — did you "
|
|
||||||
"pass a serial port path or a Windows drive path by mistake?"
|
|
||||||
)
|
|
||||||
if not (1 <= tcp_port <= 65535):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: port {tcp_port} out of range "
|
|
||||||
"(must be 1..65535)."
|
|
||||||
)
|
|
||||||
return host, tcp_port
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_tcp_endpoint(endpoint: str) -> str:
|
|
||||||
r"""Normalize `host`, `host:port`, or `tcp://host[:port]` → canonical
|
|
||||||
`tcp://host:port` form. One place that owns the lock-key shape.
|
|
||||||
|
|
||||||
Defers all validation to `parse_tcp_port`, so path-like inputs
|
|
||||||
(`/dev/cu.foo`, `C:\Windows\…`), empty hosts, non-integer ports,
|
|
||||||
and out-of-range ports raise `ConnectionError` here too.
|
|
||||||
"""
|
|
||||||
if endpoint.startswith(TCP_SCHEME):
|
|
||||||
canonical = endpoint
|
|
||||||
elif ":" in endpoint:
|
|
||||||
canonical = f"{TCP_SCHEME}{endpoint}"
|
|
||||||
else:
|
|
||||||
canonical = f"{TCP_SCHEME}{endpoint}:{DEFAULT_TCP_PORT}"
|
|
||||||
host, port = parse_tcp_port(canonical)
|
|
||||||
return f"{TCP_SCHEME}{host}:{port}"
|
|
||||||
|
|
||||||
|
|
||||||
def reject_if_tcp(port: str | None, tool_name: str) -> None:
|
|
||||||
"""Raise if `port` is a TCP endpoint — for tools that need real USB
|
|
||||||
hardware (flash, bootloader, vendor escape hatches, serial monitor).
|
|
||||||
|
|
||||||
Only checks the explicit arg; auto-selection via env var is the caller's
|
|
||||||
responsibility to handle if it matters.
|
|
||||||
"""
|
|
||||||
if is_tcp_port(port):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"{tool_name} is not applicable to TCP/native nodes ({port}). "
|
|
||||||
"This tool requires USB-attached hardware."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_port(port: str | None) -> str:
|
def resolve_port(port: str | None) -> str:
|
||||||
"""Pick a port: explicit > sole likely_meshtastic candidate > error.
|
"""Pick a port: explicit > sole likely_meshtastic candidate > error."""
|
||||||
|
|
||||||
A `tcp://` string passes through (after canonicalization). When `port`
|
|
||||||
is None and no USB candidates are present, `MESHTASTIC_MCP_TCP_HOST`
|
|
||||||
is consulted via `devices.list_devices()`.
|
|
||||||
"""
|
|
||||||
if port:
|
if port:
|
||||||
if is_tcp_port(port):
|
|
||||||
return normalize_tcp_endpoint(port)
|
|
||||||
return port
|
return port
|
||||||
candidates = [d for d in devices.list_devices() if d["likely_meshtastic"]]
|
candidates = [d for d in devices.list_devices() if d["likely_meshtastic"]]
|
||||||
if not candidates:
|
if not candidates:
|
||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
"No Meshtastic devices detected. Plug one in, set "
|
"No Meshtastic devices detected. Plug one in or pass `port` explicitly. "
|
||||||
f"{TCP_HOST_ENV}=<host[:port]> for a meshtasticd daemon, "
|
"Run `list_devices` with include_unknown=True to see all serial ports."
|
||||||
"or pass `port` explicitly. Run `list_devices` with "
|
|
||||||
"include_unknown=True to see all serial ports."
|
|
||||||
)
|
)
|
||||||
if len(candidates) > 1:
|
if len(candidates) > 1:
|
||||||
ports = ", ".join(c["port"] for c in candidates)
|
ports = ", ".join(c["port"] for c in candidates)
|
||||||
@@ -138,62 +43,17 @@ def resolve_port(port: str | None) -> str:
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
|
def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
|
||||||
"""Open a meshtastic interface (serial or TCP) and always close it.
|
"""Open a `meshtastic.SerialInterface` and always close it.
|
||||||
|
|
||||||
For serial: raises `ConnectionError` immediately if another serial
|
Raises `ConnectionError` immediately if another serial session holds the
|
||||||
session holds the port (a `pio device monitor` in `serial_sessions/`).
|
port (a `pio device monitor` in `serial_sessions/`, for instance).
|
||||||
For TCP: no exclusive-access requirement, so the serial-session check
|
|
||||||
is skipped — but the `port_lock` still serializes parallel `connect()`
|
|
||||||
calls to the same daemon endpoint.
|
|
||||||
|
|
||||||
`timeout_s` is plumbed through to both `SerialInterface(timeout=...)`
|
|
||||||
and `TCPInterface(timeout=...)`. The meshtastic library uses the value
|
|
||||||
as the reply-wait deadline for `localNode.waitForConfig()` during
|
|
||||||
construction and for any subsequent admin RPC. `int()`-converted at
|
|
||||||
the boundary because the upstream API expects whole seconds.
|
|
||||||
"""
|
"""
|
||||||
resolved = resolve_port(port)
|
|
||||||
timeout = int(timeout_s)
|
|
||||||
|
|
||||||
if is_tcp_port(resolved):
|
|
||||||
from meshtastic.tcp_interface import (
|
|
||||||
TCPInterface, # type: ignore[import-untyped]
|
|
||||||
)
|
|
||||||
|
|
||||||
host, tcp_port = parse_tcp_port(resolved)
|
|
||||||
lock = registry.port_lock(resolved)
|
|
||||||
if not lock.acquire(blocking=False):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"TCP endpoint {resolved} is busy — another device operation "
|
|
||||||
"is in flight. Retry shortly."
|
|
||||||
)
|
|
||||||
|
|
||||||
iface = None
|
|
||||||
try:
|
|
||||||
iface = TCPInterface(
|
|
||||||
hostname=host,
|
|
||||||
portNumber=tcp_port,
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
yield iface
|
|
||||||
finally:
|
|
||||||
if iface is not None:
|
|
||||||
try:
|
|
||||||
iface.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
lock.release()
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
from meshtastic.serial_interface import (
|
from meshtastic.serial_interface import (
|
||||||
SerialInterface, # type: ignore[import-untyped]
|
SerialInterface, # type: ignore[import-untyped]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
resolved = resolve_port(port)
|
||||||
|
|
||||||
active = registry.active_session_for_port(resolved)
|
active = registry.active_session_for_port(resolved)
|
||||||
if active is not None:
|
if active is not None:
|
||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
@@ -210,12 +70,7 @@ def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
|
|||||||
|
|
||||||
iface = None
|
iface = None
|
||||||
try:
|
try:
|
||||||
iface = SerialInterface(
|
iface = SerialInterface(devPath=resolved, connectNow=True, noProto=False)
|
||||||
devPath=resolved,
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
yield iface
|
yield iface
|
||||||
finally:
|
finally:
|
||||||
if iface is not None:
|
if iface is not None:
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
"""USB/serial + TCP device discovery.
|
"""USB/serial device discovery.
|
||||||
|
|
||||||
Combines the canonical `meshtastic.util.findPorts()` allowlist/blocklist with
|
Combines the canonical `meshtastic.util.findPorts()` allowlist/blocklist with
|
||||||
the richer metadata (`serial.tools.list_ports.comports()`) so callers see
|
the richer metadata (`serial.tools.list_ports.comports()`) so callers see
|
||||||
VID/PID, descriptions, and manufacturer strings alongside the "is this likely
|
VID/PID, descriptions, and manufacturer strings alongside the "is this likely
|
||||||
a Meshtastic device" signal.
|
a Meshtastic device" signal.
|
||||||
|
|
||||||
If `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` is set, a synthetic entry for the
|
|
||||||
`meshtasticd` daemon at that endpoint is prepended to the result, so
|
|
||||||
`resolve_port(None)` auto-selects it like a USB candidate.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from serial.tools import list_ports
|
from serial.tools import list_ports
|
||||||
@@ -24,45 +19,6 @@ def _to_hex(value: int | None) -> str | None:
|
|||||||
return f"0x{value:04x}"
|
return f"0x{value:04x}"
|
||||||
|
|
||||||
|
|
||||||
def _tcp_endpoint_from_env() -> dict[str, Any] | None:
|
|
||||||
"""Synthesize a TCP device entry from MESHTASTIC_MCP_TCP_HOST, if set.
|
|
||||||
|
|
||||||
If the env var is malformed (non-integer port, path-like host, etc.),
|
|
||||||
return an entry with `likely_meshtastic=False` and the parser error in
|
|
||||||
the description, rather than raising — `list_devices` is the diagnostic
|
|
||||||
tool a user reaches for when their env var isn't working, so it must
|
|
||||||
not crash on misconfiguration.
|
|
||||||
"""
|
|
||||||
host = os.environ.get("MESHTASTIC_MCP_TCP_HOST")
|
|
||||||
if not host:
|
|
||||||
return None
|
|
||||||
# Lazy import to avoid a circular dependency (connection imports devices).
|
|
||||||
from . import connection
|
|
||||||
|
|
||||||
try:
|
|
||||||
port = connection.normalize_tcp_endpoint(host)
|
|
||||||
description = "meshtasticd (TCP)"
|
|
||||||
likely = True
|
|
||||||
except connection.ConnectionError as e:
|
|
||||||
# Surface the raw env-var value plus the parser's reason so the
|
|
||||||
# user can see exactly what they set and why it was rejected.
|
|
||||||
# Don't double the scheme if the user already prefixed `tcp://`.
|
|
||||||
port = host if host.startswith(connection.TCP_SCHEME) else f"tcp://{host}"
|
|
||||||
description = f"meshtasticd (TCP) — invalid MESHTASTIC_MCP_TCP_HOST: {e}"
|
|
||||||
likely = False
|
|
||||||
return {
|
|
||||||
"port": port,
|
|
||||||
"vid": None,
|
|
||||||
"pid": None,
|
|
||||||
"description": description,
|
|
||||||
"manufacturer": None,
|
|
||||||
"product": None,
|
|
||||||
"serial_number": None,
|
|
||||||
"likely_meshtastic": likely,
|
|
||||||
"blacklisted": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
|
def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
|
||||||
"""Return enriched info for serial ports, flagging Meshtastic candidates.
|
"""Return enriched info for serial ports, flagging Meshtastic candidates.
|
||||||
|
|
||||||
@@ -114,22 +70,6 @@ def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Append the TCP endpoint (if env var set) and sort everything together.
|
# Stable ordering: likely_meshtastic first, then by port path
|
||||||
tcp_entry = _tcp_endpoint_from_env()
|
results.sort(key=lambda r: (not r["likely_meshtastic"], r["port"]))
|
||||||
if tcp_entry is not None:
|
|
||||||
results.append(tcp_entry)
|
|
||||||
|
|
||||||
# Stable ordering: likely_meshtastic first; within rank, TCP wins over
|
|
||||||
# USB (explicit env-var configuration takes precedence over USB
|
|
||||||
# enumeration); then by port path. A misconfigured TCP entry has
|
|
||||||
# likely_meshtastic=False and lands among the other ignored entries —
|
|
||||||
# it does NOT pre-empt real USB devices at the top of the list.
|
|
||||||
results.sort(
|
|
||||||
key=lambda r: (
|
|
||||||
not r["likely_meshtastic"],
|
|
||||||
not r["port"].startswith("tcp://"),
|
|
||||||
r["port"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from typing import Any
|
|||||||
|
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
from . import boards, config, connection, devices, pio, userprefs
|
from . import boards, config, devices, pio, userprefs
|
||||||
|
|
||||||
# Meshtastic variants use both `esp32s3` and `esp32-s3` style names across
|
# Meshtastic variants use both `esp32s3` and `esp32-s3` style names across
|
||||||
# variants/*/platformio.ini (no consistency enforced). Accept both spellings.
|
# variants/*/platformio.ini (no consistency enforced). Accept both spellings.
|
||||||
@@ -46,18 +46,6 @@ def _require_confirm(confirm: bool, operation: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _reject_native_env(env: str, operation: str) -> None:
|
|
||||||
"""`native*` envs build a host executable, not firmware — there's no
|
|
||||||
upload step. The user wants `build` (or just runs the binary directly).
|
|
||||||
"""
|
|
||||||
if env.startswith("native"):
|
|
||||||
raise FlashError(
|
|
||||||
f"{operation} is not applicable for env {env!r}: native envs "
|
|
||||||
"produce a host executable, not flashable firmware. Use `build` "
|
|
||||||
"instead, then run the resulting binary directly."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _artifacts_for(env: str) -> list[Path]:
|
def _artifacts_for(env: str) -> list[Path]:
|
||||||
build_dir = config.firmware_root() / ".pio" / "build" / env
|
build_dir = config.firmware_root() / ".pio" / "build" / env
|
||||||
if not build_dir.is_dir():
|
if not build_dir.is_dir():
|
||||||
@@ -153,8 +141,6 @@ def flash(
|
|||||||
that pio performs will pick up the injected values.
|
that pio performs will pick up the injected values.
|
||||||
"""
|
"""
|
||||||
_require_confirm(confirm, "flash")
|
_require_confirm(confirm, "flash")
|
||||||
_reject_native_env(env, "flash")
|
|
||||||
connection.reject_if_tcp(port, "flash")
|
|
||||||
with userprefs.temporary_overrides(userprefs_overrides) as effective:
|
with userprefs.temporary_overrides(userprefs_overrides) as effective:
|
||||||
result = pio.run(
|
result = pio.run(
|
||||||
["run", "-e", env, "-t", "upload", "--upload-port", port],
|
["run", "-e", env, "-t", "upload", "--upload-port", port],
|
||||||
@@ -214,7 +200,6 @@ def erase_and_flash(
|
|||||||
in that case) since a cached factory.bin would not reflect the new prefs.
|
in that case) since a cached factory.bin would not reflect the new prefs.
|
||||||
"""
|
"""
|
||||||
_require_confirm(confirm, "erase_and_flash")
|
_require_confirm(confirm, "erase_and_flash")
|
||||||
connection.reject_if_tcp(port, "erase_and_flash")
|
|
||||||
_check_esp32_env(env)
|
_check_esp32_env(env)
|
||||||
|
|
||||||
if userprefs_overrides and skip_build:
|
if userprefs_overrides and skip_build:
|
||||||
@@ -272,7 +257,6 @@ def update_flash(
|
|||||||
overrides are provided we always force a rebuild.
|
overrides are provided we always force a rebuild.
|
||||||
"""
|
"""
|
||||||
_require_confirm(confirm, "update_flash")
|
_require_confirm(confirm, "update_flash")
|
||||||
connection.reject_if_tcp(port, "update_flash")
|
|
||||||
_check_esp32_env(env)
|
_check_esp32_env(env)
|
||||||
|
|
||||||
if userprefs_overrides and skip_build:
|
if userprefs_overrides and skip_build:
|
||||||
@@ -407,7 +391,6 @@ def touch_1200bps(
|
|||||||
|
|
||||||
Returns `{ok, former_port, new_port, new_port_vid_pid, attempts}`.
|
Returns `{ok, former_port, new_port, new_port_vid_pid, attempts}`.
|
||||||
"""
|
"""
|
||||||
connection.reject_if_tcp(port, "touch_1200bps")
|
|
||||||
before_list = devices.list_devices(include_unknown=True)
|
before_list = devices.list_devices(include_unknown=True)
|
||||||
before_ports = {d["port"] for d in before_list}
|
before_ports = {d["port"] for d in before_list}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Sequence
|
from typing import Any, Sequence
|
||||||
|
|
||||||
from . import config, connection, pio
|
from . import config, pio
|
||||||
|
|
||||||
_TIMEOUT_SHORT = 30
|
_TIMEOUT_SHORT = 30
|
||||||
_TIMEOUT_LONG = 600
|
_TIMEOUT_LONG = 600
|
||||||
@@ -102,7 +102,6 @@ def _parse_esptool_chip_info(stdout: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def esptool_chip_info(port: str) -> dict[str, Any]:
|
def esptool_chip_info(port: str) -> dict[str, Any]:
|
||||||
connection.reject_if_tcp(port, "esptool_chip_info")
|
|
||||||
binary = config.esptool_bin()
|
binary = config.esptool_bin()
|
||||||
# `chip_id` prints chip + mac + crystal + features. `flash_id` adds flash.
|
# `chip_id` prints chip + mac + crystal + features. `flash_id` adds flash.
|
||||||
combined = _run(binary, ["--port", port, "flash_id"], timeout=_TIMEOUT_SHORT)
|
combined = _run(binary, ["--port", port, "flash_id"], timeout=_TIMEOUT_SHORT)
|
||||||
@@ -117,7 +116,6 @@ def esptool_chip_info(port: str) -> dict[str, Any]:
|
|||||||
def esptool_erase_flash(port: str, confirm: bool = False) -> dict[str, Any]:
|
def esptool_erase_flash(port: str, confirm: bool = False) -> dict[str, Any]:
|
||||||
"""Full-chip erase. Leaves the device unbootable until reflashed."""
|
"""Full-chip erase. Leaves the device unbootable until reflashed."""
|
||||||
_require_confirm(confirm, "esptool_erase_flash")
|
_require_confirm(confirm, "esptool_erase_flash")
|
||||||
connection.reject_if_tcp(port, "esptool_erase_flash")
|
|
||||||
binary = config.esptool_bin()
|
binary = config.esptool_bin()
|
||||||
# esptool v5 uses `erase-flash`, older uses `erase_flash`. Try the new name
|
# esptool v5 uses `erase-flash`, older uses `erase_flash`. Try the new name
|
||||||
# first; if it fails with unknown command, retry old.
|
# first; if it fails with unknown command, retry old.
|
||||||
@@ -136,7 +134,6 @@ def esptool_raw(
|
|||||||
"""Raw esptool passthrough. Destructive subcommands require confirm=True."""
|
"""Raw esptool passthrough. Destructive subcommands require confirm=True."""
|
||||||
if not args:
|
if not args:
|
||||||
raise ToolError("args must not be empty")
|
raise ToolError("args must not be empty")
|
||||||
connection.reject_if_tcp(port, "esptool_raw")
|
|
||||||
# Find the first non-flag arg (the subcommand).
|
# Find the first non-flag arg (the subcommand).
|
||||||
subcommand = next((a for a in args if not a.startswith("-")), None)
|
subcommand = next((a for a in args if not a.startswith("-")), None)
|
||||||
if subcommand and subcommand.replace("-", "_") in {
|
if subcommand and subcommand.replace("-", "_") in {
|
||||||
@@ -159,7 +156,6 @@ NRFUTIL_DESTRUCTIVE = {"dfu", "settings"}
|
|||||||
|
|
||||||
def nrfutil_dfu(port: str, package_path: str, confirm: bool = False) -> dict[str, Any]:
|
def nrfutil_dfu(port: str, package_path: str, confirm: bool = False) -> dict[str, Any]:
|
||||||
_require_confirm(confirm, "nrfutil_dfu")
|
_require_confirm(confirm, "nrfutil_dfu")
|
||||||
connection.reject_if_tcp(port, "nrfutil_dfu")
|
|
||||||
pkg = Path(package_path).expanduser()
|
pkg = Path(package_path).expanduser()
|
||||||
if not pkg.is_file():
|
if not pkg.is_file():
|
||||||
raise ToolError(f"Package not found: {pkg}")
|
raise ToolError(f"Package not found: {pkg}")
|
||||||
@@ -217,7 +213,6 @@ def _parse_picotool_info(stdout: str) -> dict[str, Any]:
|
|||||||
def picotool_info(port: str | None = None) -> dict[str, Any]:
|
def picotool_info(port: str | None = None) -> dict[str, Any]:
|
||||||
"""Read device info from a Pico in BOOTSEL mode. `port` is informational
|
"""Read device info from a Pico in BOOTSEL mode. `port` is informational
|
||||||
only — picotool auto-detects."""
|
only — picotool auto-detects."""
|
||||||
connection.reject_if_tcp(port, "picotool_info")
|
|
||||||
binary = config.picotool_bin()
|
binary = config.picotool_bin()
|
||||||
res = _run(binary, ["info", "-a"], timeout=_TIMEOUT_SHORT)
|
res = _run(binary, ["info", "-a"], timeout=_TIMEOUT_SHORT)
|
||||||
if res["exit_code"] != 0:
|
if res["exit_code"] != 0:
|
||||||
|
|||||||
@@ -71,10 +71,6 @@ def open_session(
|
|||||||
If `env` is supplied, pio resolves baud and filters from platformio.ini.
|
If `env` is supplied, pio resolves baud and filters from platformio.ini.
|
||||||
Otherwise uses the supplied `baud` and `filters` (default `['direct']`).
|
Otherwise uses the supplied `baud` and `filters` (default `['direct']`).
|
||||||
"""
|
"""
|
||||||
# Lazy import to avoid circular: registry imports serial_session.
|
|
||||||
from . import connection
|
|
||||||
|
|
||||||
connection.reject_if_tcp(port, "serial_open")
|
|
||||||
args = ["device", "monitor", "--port", port, "--no-reconnect"]
|
args = ["device", "monitor", "--port", port, "--no-reconnect"]
|
||||||
effective_filters: list[str]
|
effective_filters: list[str]
|
||||||
effective_baud: int = baud
|
effective_baud: int = baud
|
||||||
|
|||||||
@@ -1,383 +0,0 @@
|
|||||||
"""TCP transport plumbing in connection.py + devices.py.
|
|
||||||
|
|
||||||
Pure-Python tests — no real device or daemon required. Mocks `TCPInterface`
|
|
||||||
when exercising `connect()`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from meshtastic_mcp import connection, devices
|
|
||||||
|
|
||||||
# ---------- helpers --------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestIsTcpPort:
|
|
||||||
def test_tcp_scheme(self) -> None:
|
|
||||||
assert connection.is_tcp_port("tcp://localhost") is True
|
|
||||||
assert connection.is_tcp_port("tcp://localhost:4403") is True
|
|
||||||
assert connection.is_tcp_port("tcp://192.168.1.50:9999") is True
|
|
||||||
|
|
||||||
def test_serial_paths(self) -> None:
|
|
||||||
assert connection.is_tcp_port("/dev/cu.usbmodem1234") is False
|
|
||||||
assert connection.is_tcp_port("/dev/ttyUSB0") is False
|
|
||||||
assert connection.is_tcp_port("COM3") is False
|
|
||||||
|
|
||||||
def test_empty_or_none(self) -> None:
|
|
||||||
assert connection.is_tcp_port(None) is False
|
|
||||||
assert connection.is_tcp_port("") is False
|
|
||||||
|
|
||||||
|
|
||||||
class TestParseTcpPort:
|
|
||||||
def test_default_port(self) -> None:
|
|
||||||
assert connection.parse_tcp_port("tcp://localhost") == ("localhost", 4403)
|
|
||||||
|
|
||||||
def test_explicit_port(self) -> None:
|
|
||||||
assert connection.parse_tcp_port("tcp://localhost:9999") == (
|
|
||||||
"localhost",
|
|
||||||
9999,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_ip_with_port(self) -> None:
|
|
||||||
assert connection.parse_tcp_port("tcp://192.168.1.50:4403") == (
|
|
||||||
"192.168.1.50",
|
|
||||||
4403,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNormalizeTcpEndpoint:
|
|
||||||
def test_bare_host(self) -> None:
|
|
||||||
assert connection.normalize_tcp_endpoint("localhost") == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
def test_host_port(self) -> None:
|
|
||||||
assert (
|
|
||||||
connection.normalize_tcp_endpoint("localhost:5000")
|
|
||||||
== "tcp://localhost:5000"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_full_url(self) -> None:
|
|
||||||
assert (
|
|
||||||
connection.normalize_tcp_endpoint("tcp://1.2.3.4") == "tcp://1.2.3.4:4403"
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
connection.normalize_tcp_endpoint("tcp://1.2.3.4:9999")
|
|
||||||
== "tcp://1.2.3.4:9999"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_idempotent(self) -> None:
|
|
||||||
once = connection.normalize_tcp_endpoint("localhost:4403")
|
|
||||||
twice = connection.normalize_tcp_endpoint(once)
|
|
||||||
assert once == twice == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
def test_path_like_endpoint_rejected(self) -> None:
|
|
||||||
# Serial port paths and Windows drive paths are common config typos
|
|
||||||
# (someone passes a serial path to MESHTASTIC_MCP_TCP_HOST). Reject
|
|
||||||
# rather than producing a nonsense `tcp:///dev/cu.foo:4403` URL.
|
|
||||||
with pytest.raises(connection.ConnectionError, match="path separator"):
|
|
||||||
connection.normalize_tcp_endpoint("/dev/cu.foo")
|
|
||||||
with pytest.raises(connection.ConnectionError):
|
|
||||||
connection.normalize_tcp_endpoint("tcp:///dev/cu.foo:4403")
|
|
||||||
with pytest.raises(connection.ConnectionError):
|
|
||||||
connection.normalize_tcp_endpoint(r"C:\Windows\System32")
|
|
||||||
|
|
||||||
def test_non_integer_port_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="not an integer"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://host:notaport")
|
|
||||||
with pytest.raises(connection.ConnectionError, match="not an integer"):
|
|
||||||
connection.normalize_tcp_endpoint("host:notaport")
|
|
||||||
|
|
||||||
def test_empty_host_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="empty host"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://:4403")
|
|
||||||
|
|
||||||
def test_port_out_of_range_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://host:0")
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://host:65536")
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.normalize_tcp_endpoint("host:99999")
|
|
||||||
|
|
||||||
|
|
||||||
class TestParseTcpPortValidation:
|
|
||||||
def test_missing_scheme_rejected(self) -> None:
|
|
||||||
# parse_tcp_port is a low-level helper that requires the scheme.
|
|
||||||
# Misuse should fail loudly rather than silently mis-parsing.
|
|
||||||
with pytest.raises(connection.ConnectionError, match="expected"):
|
|
||||||
connection.parse_tcp_port("localhost:4403")
|
|
||||||
|
|
||||||
def test_negative_port_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.parse_tcp_port("tcp://host:-1")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- reject_if_tcp --------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestRejectIfTcp:
|
|
||||||
def test_rejects_tcp(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="not applicable"):
|
|
||||||
connection.reject_if_tcp("tcp://localhost", "esptool_chip_info")
|
|
||||||
|
|
||||||
def test_passes_through_serial(self) -> None:
|
|
||||||
connection.reject_if_tcp("/dev/cu.usbmodem1", "esptool_chip_info") # no raise
|
|
||||||
|
|
||||||
def test_passes_through_none(self) -> None:
|
|
||||||
# None means "auto-detect"; not the explicit-arg case we guard.
|
|
||||||
connection.reject_if_tcp(None, "esptool_chip_info") # no raise
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- resolve_port ---------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestResolvePort:
|
|
||||||
def test_explicit_serial_passthrough(self) -> None:
|
|
||||||
assert connection.resolve_port("/dev/cu.usbmodem999") == "/dev/cu.usbmodem999"
|
|
||||||
|
|
||||||
def test_explicit_tcp_normalized(self) -> None:
|
|
||||||
assert connection.resolve_port("tcp://localhost") == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
def test_no_port_no_devices_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
with patch.object(devices, "list_devices", return_value=[]):
|
|
||||||
with pytest.raises(
|
|
||||||
connection.ConnectionError, match="No Meshtastic devices"
|
|
||||||
):
|
|
||||||
connection.resolve_port(None)
|
|
||||||
|
|
||||||
def test_no_port_one_candidate_selected(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
fake = [{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True}]
|
|
||||||
with patch.object(devices, "list_devices", return_value=fake):
|
|
||||||
assert connection.resolve_port(None) == "/dev/cu.usbmodem1"
|
|
||||||
|
|
||||||
def test_no_port_multiple_candidates_errors(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
fake = [
|
|
||||||
{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True},
|
|
||||||
{"port": "/dev/cu.usbmodem2", "likely_meshtastic": True},
|
|
||||||
]
|
|
||||||
with patch.object(devices, "list_devices", return_value=fake):
|
|
||||||
with pytest.raises(connection.ConnectionError, match="Multiple"):
|
|
||||||
connection.resolve_port(None)
|
|
||||||
|
|
||||||
def test_env_var_surfaces_tcp_via_devices(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
|
|
||||||
# Don't patch list_devices — let the real env-var path run, but stub
|
|
||||||
# the USB enumeration to keep the test hermetic.
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
assert connection.resolve_port(None) == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- devices.list_devices TCP entry --------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestDevicesTcpEntry:
|
|
||||||
def test_no_env_var_no_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
assert all(not d["port"].startswith("tcp://") for d in ds)
|
|
||||||
|
|
||||||
def test_env_var_adds_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "myhost:9999")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
tcp = [d for d in ds if d["port"].startswith("tcp://")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["port"] == "tcp://myhost:9999"
|
|
||||||
assert tcp[0]["likely_meshtastic"] is True
|
|
||||||
assert tcp[0]["description"] == "meshtasticd (TCP)"
|
|
||||||
|
|
||||||
def test_tcp_entry_first_in_results(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
assert ds, "expected at least the TCP entry"
|
|
||||||
assert ds[0]["port"].startswith("tcp://")
|
|
||||||
|
|
||||||
def test_invalid_env_var_does_not_break_list_devices(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# `list_devices` is the diagnostic tool reached for when an env var
|
|
||||||
# isn't working — it must not throw on misconfiguration.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices(include_unknown=True)
|
|
||||||
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["likely_meshtastic"] is False
|
|
||||||
assert "invalid MESHTASTIC_MCP_TCP_HOST" in tcp[0]["description"]
|
|
||||||
assert "not an integer" in tcp[0]["description"]
|
|
||||||
|
|
||||||
def test_invalid_env_var_excluded_from_resolve_port_autodetect(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# `likely_meshtastic=False` keeps the bad TCP entry out of the
|
|
||||||
# auto-select path — `resolve_port(None)` should still report
|
|
||||||
# "no Meshtastic devices" rather than picking a broken endpoint.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
with pytest.raises(connection.ConnectionError, match="No Meshtastic"):
|
|
||||||
connection.resolve_port(None)
|
|
||||||
|
|
||||||
def test_invalid_env_var_does_not_double_tcp_scheme(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# If a user mistakenly sets `MESHTASTIC_MCP_TCP_HOST=tcp://host:bad`,
|
|
||||||
# the diagnostic entry must surface the raw value as-is rather than
|
|
||||||
# producing `tcp://tcp://host:bad`.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "tcp://host:notaport")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices(include_unknown=True)
|
|
||||||
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["port"] == "tcp://host:notaport"
|
|
||||||
assert "tcp://tcp://" not in tcp[0]["port"]
|
|
||||||
|
|
||||||
def test_invalid_env_var_does_not_pre_empt_real_usb_devices(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# Sort ordering: a misconfigured TCP env var must NOT take position 0
|
|
||||||
# ahead of real USB candidates. Position 0 is reserved for the highest
|
|
||||||
# rank (likely_meshtastic=True), with TCP-before-USB as a tiebreaker
|
|
||||||
# within rank.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
|
|
||||||
|
|
||||||
# Stub a USB Meshtastic candidate (Espressif VID, port present in
|
|
||||||
# findPorts).
|
|
||||||
class FakeInfo:
|
|
||||||
def __init__(self, device: str, vid: int, pid: int) -> None:
|
|
||||||
self.device = device
|
|
||||||
self.vid = vid
|
|
||||||
self.pid = pid
|
|
||||||
self.description = "Heltec V3"
|
|
||||||
self.manufacturer = "Espressif"
|
|
||||||
self.product = "USB JTAG/serial"
|
|
||||||
self.serial_number = "abc"
|
|
||||||
|
|
||||||
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
|
|
||||||
with patch(
|
|
||||||
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
|
|
||||||
), patch(
|
|
||||||
"meshtastic.util.findPorts",
|
|
||||||
return_value=["/dev/cu.usbmodem4201"],
|
|
||||||
):
|
|
||||||
ds = devices.list_devices(include_unknown=True)
|
|
||||||
|
|
||||||
assert ds, "expected at least the USB + TCP entries"
|
|
||||||
# Real USB candidate must be at position 0 — it's likely_meshtastic.
|
|
||||||
assert ds[0]["port"] == "/dev/cu.usbmodem4201"
|
|
||||||
assert ds[0]["likely_meshtastic"] is True
|
|
||||||
# The malformed TCP entry exists but lands among the unlikely entries.
|
|
||||||
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["likely_meshtastic"] is False
|
|
||||||
assert ds.index(tcp[0]) > 0
|
|
||||||
|
|
||||||
def test_likely_tcp_entry_wins_tiebreak_over_usb(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# Conversely, a *valid* TCP env var should sort ahead of USB
|
|
||||||
# candidates of equal likely_meshtastic rank — explicit env-var
|
|
||||||
# configuration is a precedence signal.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost:4403")
|
|
||||||
|
|
||||||
class FakeInfo:
|
|
||||||
def __init__(self, device: str, vid: int, pid: int) -> None:
|
|
||||||
self.device = device
|
|
||||||
self.vid = vid
|
|
||||||
self.pid = pid
|
|
||||||
self.description = "Heltec V3"
|
|
||||||
self.manufacturer = "Espressif"
|
|
||||||
self.product = "USB JTAG/serial"
|
|
||||||
self.serial_number = "abc"
|
|
||||||
|
|
||||||
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
|
|
||||||
with patch(
|
|
||||||
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
|
|
||||||
), patch(
|
|
||||||
"meshtastic.util.findPorts",
|
|
||||||
return_value=["/dev/cu.usbmodem4201"],
|
|
||||||
):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
|
|
||||||
assert ds[0]["port"] == "tcp://localhost:4403"
|
|
||||||
assert ds[0]["likely_meshtastic"] is True
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- connect() routing ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestConnectRoutesTcp:
|
|
||||||
def test_connect_uses_tcp_interface_for_tcp_port(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
"""Verify the TCP branch instantiates `TCPInterface(hostname, portNumber)`
|
|
||||||
and never touches `SerialInterface`."""
|
|
||||||
# Make sure the env var doesn't leak in and confuse resolve_port.
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
|
|
||||||
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp, patch(
|
|
||||||
"meshtastic.serial_interface.SerialInterface"
|
|
||||||
) as mock_serial:
|
|
||||||
mock_tcp.return_value.close.return_value = None
|
|
||||||
with connection.connect(port="tcp://example.com:1234", timeout_s=12.0):
|
|
||||||
pass
|
|
||||||
|
|
||||||
mock_tcp.assert_called_once_with(
|
|
||||||
hostname="example.com",
|
|
||||||
portNumber=1234,
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=12,
|
|
||||||
)
|
|
||||||
mock_serial.assert_not_called()
|
|
||||||
|
|
||||||
def test_connect_plumbs_timeout_to_serial_interface(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
"""Verify the serial branch also propagates `timeout_s` so callers
|
|
||||||
passing a custom timeout to `device_info` / `list_nodes` / etc. don't
|
|
||||||
silently get the library default."""
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
|
|
||||||
with patch("meshtastic.serial_interface.SerialInterface") as mock_serial, patch(
|
|
||||||
"meshtastic.tcp_interface.TCPInterface"
|
|
||||||
) as mock_tcp:
|
|
||||||
mock_serial.return_value.close.return_value = None
|
|
||||||
with connection.connect(port="/dev/cu.fake", timeout_s=20.0):
|
|
||||||
pass
|
|
||||||
|
|
||||||
mock_serial.assert_called_once_with(
|
|
||||||
devPath="/dev/cu.fake",
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=20,
|
|
||||||
)
|
|
||||||
mock_tcp.assert_not_called()
|
|
||||||
|
|
||||||
def test_connect_releases_lock_on_tcp_failure(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
|
|
||||||
mock_tcp.side_effect = RuntimeError("boom")
|
|
||||||
with pytest.raises(RuntimeError, match="boom"):
|
|
||||||
with connection.connect(port="tcp://locktest:4403"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Lock should be released — a second connect attempt must not fail
|
|
||||||
# with "busy".
|
|
||||||
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
|
|
||||||
mock_tcp.return_value.close.return_value = None
|
|
||||||
with connection.connect(port="tcp://locktest:4403"):
|
|
||||||
pass
|
|
||||||
+4
-4
@@ -29,7 +29,6 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DUSE_THREAD_NAMES
|
-DUSE_THREAD_NAMES
|
||||||
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
|
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
|
||||||
-DPB_ENABLE_MALLOC=1
|
-DPB_ENABLE_MALLOC=1
|
||||||
-DPB_VALIDATE_UTF8=1
|
|
||||||
-DRADIOLIB_EXCLUDE_CC1101=1
|
-DRADIOLIB_EXCLUDE_CC1101=1
|
||||||
-DRADIOLIB_EXCLUDE_NRF24=1
|
-DRADIOLIB_EXCLUDE_NRF24=1
|
||||||
-DRADIOLIB_EXCLUDE_RF69=1
|
-DRADIOLIB_EXCLUDE_RF69=1
|
||||||
@@ -57,7 +56,6 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
||||||
-DMESHTASTIC_EXCLUDE_STATUS=1
|
|
||||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||||
#-D OLED_PL=1
|
#-D OLED_PL=1
|
||||||
@@ -68,7 +66,7 @@ monitor_speed = 115200
|
|||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/6bfd1f135e1ebe37afd6050bb4b9964cea3fcfda.zip
|
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||||
@@ -126,7 +124,7 @@ lib_deps =
|
|||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/4bf593a82100b911ff816dddf7158ffdee2114cd.zip
|
https://github.com/meshtastic/device-ui/archive/56e1da4e7d30abcd746a2092a30e422f8cf5fc2b.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
@@ -227,6 +225,8 @@ lib_deps =
|
|||||||
https://github.com/Sensirion/arduino-i2c-sfa3x/archive/refs/tags/1.0.0.zip
|
https://github.com/Sensirion/arduino-i2c-sfa3x/archive/refs/tags/1.0.0.zip
|
||||||
# renovate: datasource=github-tags depName=Sensirion I2C SCD30 packageName=sensirion/arduino-i2c-scd30
|
# renovate: datasource=github-tags depName=Sensirion I2C SCD30 packageName=sensirion/arduino-i2c-scd30
|
||||||
https://github.com/Sensirion/arduino-i2c-scd30/archive/refs/tags/1.0.0.zip
|
https://github.com/Sensirion/arduino-i2c-scd30/archive/refs/tags/1.0.0.zip
|
||||||
|
# renovate: datasource=github-tags depName=arduino-sht packageName=sensirion/arduino-sht
|
||||||
|
https://github.com/Sensirion/arduino-sht/archive/refs/tags/v1.2.6.zip
|
||||||
|
|
||||||
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
|
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
|
||||||
[environmental_extra]
|
[environmental_extra]
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: 1d6f1a71ff...d004f503bb
@@ -4,7 +4,8 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
|||||||
bool usePreset)
|
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) {
|
if (!usePreset) {
|
||||||
return "Custom";
|
return "Custom";
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-136
@@ -40,6 +40,22 @@
|
|||||||
#include "concurrency/LockGuard.h"
|
#include "concurrency/LockGuard.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(ARCH_STM32WL) && defined(BATTERY_PIN)
|
||||||
|
#include "stm32yyxx_ll_adc.h"
|
||||||
|
|
||||||
|
/* Analog read resolution */
|
||||||
|
#if defined(LL_ADC_RESOLUTION_12B)
|
||||||
|
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
|
||||||
|
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||||
|
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
|
||||||
|
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
|
||||||
|
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||||
|
#else
|
||||||
|
#error "ADC resolution could not be defined!"
|
||||||
|
#endif
|
||||||
|
#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
|
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
|
||||||
#include "mqtt/MQTT.h"
|
#include "mqtt/MQTT.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
@@ -52,11 +68,7 @@
|
|||||||
#define ETH ETH2
|
#define ETH ETH2
|
||||||
#endif // HAS_ETHERNET
|
#endif // HAS_ETHERNET
|
||||||
|
|
||||||
#endif
|
#endif // MQTT
|
||||||
|
|
||||||
#ifndef DELAY_FOREVER
|
|
||||||
#define DELAY_FOREVER portMAX_DELAY
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(BATTERY_PIN) && defined(ARCH_ESP32)
|
#if defined(BATTERY_PIN) && defined(ARCH_ESP32)
|
||||||
|
|
||||||
@@ -78,19 +90,6 @@ static const adc_atten_t atten = ADC_ATTENUATION;
|
|||||||
#endif
|
#endif
|
||||||
#endif // BATTERY_PIN && ARCH_ESP32
|
#endif // BATTERY_PIN && ARCH_ESP32
|
||||||
|
|
||||||
#ifdef EXT_CHRG_DETECT
|
|
||||||
#ifndef EXT_CHRG_DETECT_MODE
|
|
||||||
static const uint8_t ext_chrg_detect_mode = INPUT;
|
|
||||||
#else
|
|
||||||
static const uint8_t ext_chrg_detect_mode = EXT_CHRG_DETECT_MODE;
|
|
||||||
#endif
|
|
||||||
#ifndef EXT_CHRG_DETECT_VALUE
|
|
||||||
static const uint8_t ext_chrg_detect_value = HIGH;
|
|
||||||
#else
|
|
||||||
static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE;
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
#if __has_include(<Adafruit_INA219.h>)
|
#if __has_include(<Adafruit_INA219.h>)
|
||||||
INA219Sensor ina219Sensor;
|
INA219Sensor ina219Sensor;
|
||||||
@@ -129,7 +128,7 @@ MAX17048Sensor max17048Sensor;
|
|||||||
NullSensor max17048Sensor;
|
NullSensor max17048Sensor;
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif // !MESHTASTIC_EXCLUDE_I2C
|
||||||
|
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT
|
||||||
RAK9154Sensor rak9154Sensor;
|
RAK9154Sensor rak9154Sensor;
|
||||||
@@ -147,46 +146,12 @@ XPowersPPM *PPM = NULL;
|
|||||||
|
|
||||||
#ifdef HAS_PMU
|
#ifdef HAS_PMU
|
||||||
XPowersLibInterface *PMU = NULL;
|
XPowersLibInterface *PMU = NULL;
|
||||||
#else
|
|
||||||
|
|
||||||
// Copy of the base class defined in axp20x.h.
|
|
||||||
// I'd rather not include axp20x.h as it brings Wire dependency.
|
|
||||||
class HasBatteryLevel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
|
||||||
*/
|
|
||||||
virtual int getBatteryPercent() { return -1; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The raw voltage of the battery or NAN if unknown
|
|
||||||
*/
|
|
||||||
virtual uint16_t getBattVoltage() { return 0; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return true if there is a battery installed in this unit
|
|
||||||
*/
|
|
||||||
virtual bool isBatteryConnect() { return false; }
|
|
||||||
|
|
||||||
virtual bool isVbusIn() { return false; }
|
|
||||||
virtual bool isCharging() { return false; }
|
|
||||||
};
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool pmu_irq = false;
|
|
||||||
|
|
||||||
Power *power;
|
Power *power;
|
||||||
|
|
||||||
using namespace meshtastic;
|
using namespace meshtastic;
|
||||||
|
|
||||||
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
|
||||||
// make sure it's included. If something is wrong with NRF52
|
|
||||||
// definition - compilation will fail on missing definition
|
|
||||||
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
|
||||||
#define AREF_VOLTAGE 3.3
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this board has a battery level sensor, set this to a valid implementation
|
* If this board has a battery level sensor, set this to a valid implementation
|
||||||
*/
|
*/
|
||||||
@@ -229,7 +194,7 @@ static void battery_adcDisable()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif // BATTERY_PIN
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple battery level sensor that assumes the battery voltage is attached
|
* A simple battery level sensor that assumes the battery voltage is attached
|
||||||
@@ -288,7 +253,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The raw voltage of the batteryin millivolts or NAN if unknown
|
* The raw voltage of the battery in millivolts or NAN if unknown
|
||||||
*/
|
*/
|
||||||
virtual uint16_t getBattVoltage() override
|
virtual uint16_t getBattVoltage() override
|
||||||
{
|
{
|
||||||
@@ -305,16 +270,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef ADC_MULTIPLIER
|
|
||||||
#define ADC_MULTIPLIER 2.0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef BATTERY_SENSE_SAMPLES
|
|
||||||
#define BATTERY_SENSE_SAMPLES \
|
|
||||||
15 // Set the number of samples, it has an effect of increasing sensitivity in
|
|
||||||
// complex electromagnetic environment.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef BATTERY_PIN
|
#ifdef BATTERY_PIN
|
||||||
// Override variant or default ADC_MULTIPLIER if we have the override pref
|
// Override variant or default ADC_MULTIPLIER if we have the override pref
|
||||||
float operativeAdcMultiplier =
|
float operativeAdcMultiplier =
|
||||||
@@ -328,11 +283,17 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
float scaled = 0;
|
float scaled = 0;
|
||||||
|
|
||||||
battery_adcEnable();
|
battery_adcEnable();
|
||||||
#ifdef ARCH_ESP32 // ADC block for espressif platforms
|
#ifdef ARCH_STM32WL
|
||||||
|
// STM32 ADC with VREFINT runtime calibration
|
||||||
|
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
|
||||||
|
raw = analogRead(BATTERY_PIN);
|
||||||
|
scaled = __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, raw, LL_ADC_RESOLUTION);
|
||||||
|
scaled *= operativeAdcMultiplier;
|
||||||
|
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
|
||||||
raw = espAdcRead();
|
raw = espAdcRead();
|
||||||
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
|
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
|
||||||
scaled *= operativeAdcMultiplier;
|
scaled *= operativeAdcMultiplier;
|
||||||
#else // block for all other platforms
|
#else // block for all other platforms
|
||||||
#ifdef ARCH_NRF52
|
#ifdef ARCH_NRF52
|
||||||
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
|
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
|
||||||
#endif
|
#endif
|
||||||
@@ -447,28 +408,14 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
// Detect if an external power source is connected if we don’t have a PMIC;
|
||||||
/// pumping in power On some boards we don't have the power management chip
|
// Firstly prefer EXT_PWR_DETECT GPIO if available,
|
||||||
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
|
// secondly try an nRF52-specific routine on some variants,
|
||||||
/// source
|
// lastly provide a fallback to indicate external power when fully charged.
|
||||||
virtual bool isVbusIn() override
|
virtual bool isVbusIn() override
|
||||||
{
|
{
|
||||||
#ifdef EXT_PWR_DETECT
|
#ifdef EXT_PWR_DETECT
|
||||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE;
|
||||||
// if external powered that pin will be pulled down
|
|
||||||
if (digitalRead(EXT_PWR_DETECT) == LOW) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// if it's not LOW - check the battery
|
|
||||||
#else
|
|
||||||
// if external powered that pin will be pulled up
|
|
||||||
if (digitalRead(EXT_PWR_DETECT) == HIGH) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// if it's not HIGH - check the battery
|
|
||||||
#endif
|
|
||||||
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// technically speaking this should work for all(?) NRF52 boards
|
// technically speaking this should work for all(?) NRF52 boards
|
||||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||||
@@ -489,9 +436,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if defined(ELECROW_ThinkNode_M6)
|
#if defined(ELECROW_ThinkNode_M6)
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
|
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || isVbusIn();
|
||||||
#elif EXT_CHRG_DETECT
|
#elif EXT_CHRG_DETECT
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||||
#elif defined(BATTERY_CHARGING_INV)
|
#elif defined(BATTERY_CHARGING_INV)
|
||||||
return !digitalRead(BATTERY_CHARGING_INV);
|
return !digitalRead(BATTERY_CHARGING_INV);
|
||||||
#else
|
#else
|
||||||
@@ -530,6 +477,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
bool initial_read_done = false;
|
bool initial_read_done = false;
|
||||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||||
uint32_t last_read_time_ms = 0;
|
uint32_t last_read_time_ms = 0;
|
||||||
|
#ifdef ARCH_STM32WL
|
||||||
|
// 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
|
||||||
|
// (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
|
||||||
|
uint32_t Vref = 3300;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
|
||||||
|
|
||||||
@@ -619,14 +571,10 @@ Power::Power() : OSThread("Power")
|
|||||||
bool Power::analogInit()
|
bool Power::analogInit()
|
||||||
{
|
{
|
||||||
#ifdef EXT_PWR_DETECT
|
#ifdef EXT_PWR_DETECT
|
||||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||||||
pinMode(EXT_PWR_DETECT, INPUT_PULLUP);
|
|
||||||
#else
|
|
||||||
pinMode(EXT_PWR_DETECT, INPUT);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef EXT_CHRG_DETECT
|
#ifdef EXT_CHRG_DETECT
|
||||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef BATTERY_PIN
|
#ifdef BATTERY_PIN
|
||||||
@@ -639,7 +587,9 @@ bool Power::analogInit()
|
|||||||
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ARCH_ESP32 // ESP32 needs special analog stuff
|
#ifdef ARCH_STM32WL
|
||||||
|
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||||
|
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
|
||||||
|
|
||||||
#ifndef ADC_WIDTH // max resolution by default
|
#ifndef ADC_WIDTH // max resolution by default
|
||||||
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
|
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
|
||||||
@@ -649,7 +599,7 @@ bool Power::analogInit()
|
|||||||
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
|
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
|
||||||
adc1_config_width(width);
|
adc1_config_width(width);
|
||||||
adc1_config_channel_atten(adc_channel, atten);
|
adc1_config_channel_atten(adc_channel, atten);
|
||||||
#else // ADC2
|
#else // ADC2
|
||||||
adc2_config_channel_atten(adc_channel, atten);
|
adc2_config_channel_atten(adc_channel, atten);
|
||||||
#ifndef CONFIG_IDF_TARGET_ESP32S3
|
#ifndef CONFIG_IDF_TARGET_ESP32S3
|
||||||
// ADC2 wifi bug workaround
|
// ADC2 wifi bug workaround
|
||||||
@@ -679,7 +629,7 @@ bool Power::analogInit()
|
|||||||
|
|
||||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||||
|
|
||||||
#ifndef ARCH_ESP32
|
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32WL)
|
||||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -755,10 +705,8 @@ void Power::reboot()
|
|||||||
rp2040.reboot();
|
rp2040.reboot();
|
||||||
#elif defined(ARCH_PORTDUINO)
|
#elif defined(ARCH_PORTDUINO)
|
||||||
deInitApiServer();
|
deInitApiServer();
|
||||||
#ifdef __linux__
|
|
||||||
if (aLinuxInputImpl)
|
if (aLinuxInputImpl)
|
||||||
aLinuxInputImpl->deInit();
|
aLinuxInputImpl->deInit();
|
||||||
#endif
|
|
||||||
SPI.end();
|
SPI.end();
|
||||||
Wire.end();
|
Wire.end();
|
||||||
Serial1.end();
|
Serial1.end();
|
||||||
@@ -976,6 +924,14 @@ int32_t Power::runOnce()
|
|||||||
powerFSM.trigger(EVENT_POWER_CONNECTED);
|
powerFSM.trigger(EVENT_POWER_CONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef PMU_POWER_BUTTON_IS_CANCEL
|
||||||
|
// cancel action also turns the screen on and off.
|
||||||
|
if (PMU->isPekeyShortPressIrq()) {
|
||||||
|
LOG_INFO("Input: Corona Button Click");
|
||||||
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
|
inputBroker->injectInputEvent(&event);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
/*
|
/*
|
||||||
Other things we could check if we cared...
|
Other things we could check if we cared...
|
||||||
|
|
||||||
@@ -992,13 +948,6 @@ int32_t Power::runOnce()
|
|||||||
LOG_DEBUG("Battery removed");
|
LOG_DEBUG("Battery removed");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3?
|
|
||||||
if (PMU->isPekeyLongPressIrq()) {
|
|
||||||
LOG_DEBUG("PEK long button press");
|
|
||||||
if (screen)
|
|
||||||
screen->setOn(false);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
PMU->clearIrqStatus();
|
PMU->clearIrqStatus();
|
||||||
}
|
}
|
||||||
@@ -1067,8 +1016,8 @@ void Power::attachPowerInterrupts()
|
|||||||
if (PMU) {
|
if (PMU) {
|
||||||
attachInterrupt(
|
attachInterrupt(
|
||||||
PMU_IRQ,
|
PMU_IRQ,
|
||||||
[] {
|
[]() {
|
||||||
pmu_irq = true;
|
power->pmu_irq = true;
|
||||||
power->setIntervalFromNow(0);
|
power->setIntervalFromNow(0);
|
||||||
runASAP = true;
|
runASAP = true;
|
||||||
},
|
},
|
||||||
@@ -1370,19 +1319,16 @@ bool Power::axpChipInit()
|
|||||||
uint64_t pmuIrqMask = 0;
|
uint64_t pmuIrqMask = 0;
|
||||||
|
|
||||||
if (PMU->getChipModel() == XPOWERS_AXP192) {
|
if (PMU->getChipModel() == XPOWERS_AXP192) {
|
||||||
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
|
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
|
||||||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||||||
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
|
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
|
||||||
}
|
}
|
||||||
|
|
||||||
pinMode(PMU_IRQ, INPUT);
|
pinMode(PMU_IRQ, INPUT);
|
||||||
|
|
||||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
// We wake on IRQ, so only enable the IRQs that we care about.
|
||||||
// because it occurs repeatedly while there is no battery also it could cause
|
// we want USB plug and unplug to update the screen and LED status,
|
||||||
// inadvertent waking from light sleep just because the battery filled we
|
// and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off).
|
||||||
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
|
|
||||||
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
|
|
||||||
// don't have anything hooked to vbus
|
|
||||||
PMU->enableIRQ(pmuIrqMask);
|
PMU->enableIRQ(pmuIrqMask);
|
||||||
|
|
||||||
PMU->clearIrqStatus();
|
PMU->clearIrqStatus();
|
||||||
@@ -1690,7 +1636,7 @@ bool Power::lipoChargerInit()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else // HAS_PPM
|
||||||
/**
|
/**
|
||||||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||||
*/
|
*/
|
||||||
@@ -1698,7 +1644,7 @@ bool Power::lipoChargerInit()
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif // HAS_PPM
|
||||||
|
|
||||||
#ifdef HELTEC_MESH_SOLAR
|
#ifdef HELTEC_MESH_SOLAR
|
||||||
#include "meshSolarApp.h"
|
#include "meshSolarApp.h"
|
||||||
@@ -1760,7 +1706,7 @@ bool Power::meshSolarInit()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else // HELTEC_MESH_SOLAR
|
||||||
/**
|
/**
|
||||||
* The meshSolar battery level sensor is unavailable - default to
|
* The meshSolar battery level sensor is unavailable - default to
|
||||||
* AnalogBatteryLevel
|
* AnalogBatteryLevel
|
||||||
@@ -1769,7 +1715,7 @@ bool Power::meshSolarInit()
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif // HELTEC_MESH_SOLAR
|
||||||
|
|
||||||
#ifdef HAS_SERIAL_BATTERY_LEVEL
|
#ifdef HAS_SERIAL_BATTERY_LEVEL
|
||||||
#include <SoftwareSerial.h>
|
#include <SoftwareSerial.h>
|
||||||
@@ -1777,7 +1723,7 @@ bool Power::meshSolarInit()
|
|||||||
/**
|
/**
|
||||||
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
|
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
|
||||||
*/
|
*/
|
||||||
class SerialBatteryLevel : public HasBatteryLevel
|
class SerialBatteryLevel : public AnalogBatteryLevel
|
||||||
{
|
{
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -1848,22 +1794,12 @@ class SerialBatteryLevel : public HasBatteryLevel
|
|||||||
{
|
{
|
||||||
#if defined(EXT_CHRG_DETECT)
|
#if defined(EXT_CHRG_DETECT)
|
||||||
|
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool isCharging() override
|
|
||||||
{
|
|
||||||
#ifdef EXT_CHRG_DETECT
|
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
// by default, we check the battery voltage only
|
|
||||||
return isVbusIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
|
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
|
||||||
uint8_t Data[6] = {0};
|
uint8_t Data[6] = {0};
|
||||||
@@ -1879,10 +1815,10 @@ SerialBatteryLevel serialBatteryLevel;
|
|||||||
bool Power::serialBatteryInit()
|
bool Power::serialBatteryInit()
|
||||||
{
|
{
|
||||||
#ifdef EXT_PWR_DETECT
|
#ifdef EXT_PWR_DETECT
|
||||||
pinMode(EXT_PWR_DETECT, INPUT);
|
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||||||
#endif
|
#endif
|
||||||
#ifdef EXT_CHRG_DETECT
|
#ifdef EXT_CHRG_DETECT
|
||||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool result = serialBatteryLevel.runOnce();
|
bool result = serialBatteryLevel.runOnce();
|
||||||
@@ -1893,7 +1829,7 @@ bool Power::serialBatteryInit()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else // HAS_SERIAL_BATTERY_LEVEL
|
||||||
/**
|
/**
|
||||||
* If this device has no serial battery level sensor, don't try to use it.
|
* If this device has no serial battery level sensor, don't try to use it.
|
||||||
*/
|
*/
|
||||||
@@ -1901,4 +1837,4 @@ bool Power::serialBatteryInit()
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif // HAS_SERIAL_BATTERY_LEVEL
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
|||||||
if (color) {
|
if (color) {
|
||||||
::printf("\u001b[0m");
|
::printf("\u001b[0m");
|
||||||
}
|
}
|
||||||
::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000);
|
::printf("| %02d:%02d:%02d %u.%03u ", hour, min, sec, millis() / 1000, millis() % 1000);
|
||||||
#else
|
#else
|
||||||
printf("%s ", logLevel);
|
printf("%s ", logLevel);
|
||||||
if (color) {
|
if (color) {
|
||||||
@@ -151,7 +151,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
|||||||
if (color) {
|
if (color) {
|
||||||
::printf("\u001b[0m");
|
::printf("\u001b[0m");
|
||||||
}
|
}
|
||||||
::printf("| ??:??:?? %u ", millis() / 1000);
|
::printf("| ??:??:?? %u.%03u ", millis() / 1000, millis() % 1000);
|
||||||
#else
|
#else
|
||||||
printf("%s ", logLevel);
|
printf("%s ", logLevel);
|
||||||
if (color) {
|
if (color) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../freertosinc.h"
|
#include "../freertosinc.h"
|
||||||
#include "Print.h"
|
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
#include <Print.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ SerialConsole *console;
|
|||||||
|
|
||||||
void consoleInit()
|
void consoleInit()
|
||||||
{
|
{
|
||||||
|
if (console) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
|
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
|
||||||
|
|
||||||
#if defined(SERIAL_HAS_ON_RECEIVE)
|
#if defined(SERIAL_HAS_ON_RECEIVE)
|
||||||
|
|||||||
+2
-2
@@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel.
|
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel.
|
||||||
|
|
||||||
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including
|
TX_LOG + RX_ALL_LOG = Total air time for a particular meshtastic channel, including
|
||||||
other lora radios.
|
other lora radios.
|
||||||
|
|
||||||
RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel.
|
RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+10
-6
@@ -78,6 +78,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
// Configuration
|
// 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
|
/// Convert a preprocessor name into a quoted string
|
||||||
#define xstr(s) ystr(s)
|
#define xstr(s) ystr(s)
|
||||||
#define ystr(s) #s
|
#define ystr(s) #s
|
||||||
@@ -157,9 +162,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#elif defined(HELTEC_MESH_NODE_T096)
|
#elif defined(HELTEC_MESH_NODE_T096)
|
||||||
#define NUM_PA_POINTS 22
|
#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
|
#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
|
||||||
#elif defined(HELTEC_V4_R8)
|
|
||||||
#define NUM_PA_POINTS 22
|
|
||||||
#define TX_GAIN_LORA 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7
|
|
||||||
#else
|
#else
|
||||||
// If a board enables USE_KCT8103L_PA but does not match a known variant and has
|
// 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.
|
// not already provided a PA curve, fail at compile time to avoid unsafe defaults.
|
||||||
@@ -229,7 +231,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define BME_ADDR 0x76
|
#define BME_ADDR 0x76
|
||||||
#define BME_ADDR_ALTERNATE 0x77
|
#define BME_ADDR_ALTERNATE 0x77
|
||||||
#define MCP9808_ADDR 0x18
|
#define MCP9808_ADDR 0x18
|
||||||
#define INA_ADDR 0x40
|
#define INA_ADDR 0x40 // same as SHT2X
|
||||||
#define INA_ADDR_ALTERNATE 0x41
|
#define INA_ADDR_ALTERNATE 0x41
|
||||||
#define INA_ADDR_WAVESHARE_UPS 0x43
|
#define INA_ADDR_WAVESHARE_UPS 0x43
|
||||||
#define INA3221_ADDR 0x42
|
#define INA3221_ADDR 0x42
|
||||||
@@ -242,8 +244,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define LPS22HB_ADDR 0x5C
|
#define LPS22HB_ADDR 0x5C
|
||||||
#define LPS22HB_ADDR_ALT 0x5D
|
#define LPS22HB_ADDR_ALT 0x5D
|
||||||
#define SFA30_ADDR 0x5D
|
#define SFA30_ADDR 0x5D
|
||||||
#define SHT31_4x_ADDR 0x44
|
#define SHTXX_ADDR 0x44
|
||||||
#define SHT31_4x_ADDR_ALT 0x45
|
#define SHTXX_ADDR_ALT 0x45
|
||||||
#define PMSA003I_ADDR 0x12
|
#define PMSA003I_ADDR 0x12
|
||||||
#define QMA6100P_ADDR 0x12
|
#define QMA6100P_ADDR 0x12
|
||||||
#define AHT10_ADDR 0x38
|
#define AHT10_ADDR 0x38
|
||||||
@@ -497,6 +499,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define MESHTASTIC_EXCLUDE_PKI 1
|
#define MESHTASTIC_EXCLUDE_PKI 1
|
||||||
#define MESHTASTIC_EXCLUDE_POWER_FSM 1
|
#define MESHTASTIC_EXCLUDE_POWER_FSM 1
|
||||||
#define MESHTASTIC_EXCLUDE_TZ 1
|
#define MESHTASTIC_EXCLUDE_TZ 1
|
||||||
|
#define MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Turn off all optional modules
|
// Turn off all optional modules
|
||||||
@@ -513,6 +516,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
|
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
|
||||||
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
|
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
|
||||||
#define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1
|
#define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1
|
||||||
|
#define MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT 1
|
||||||
#define MESHTASTIC_EXCLUDE_ATAK 1
|
#define MESHTASTIC_EXCLUDE_ATAK 1
|
||||||
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
|
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
|
||||||
#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1
|
#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint1
|
|||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for SEN5X detection
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||||
|
// FIXME Move to a separate file for detection of sensors that require more complex interactions?
|
||||||
|
// For SEN5X detection
|
||||||
// Note, this code needs to be called before setting the I2C bus speed
|
// Note, this code needs to be called before setting the I2C bus speed
|
||||||
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
||||||
// detection will not work
|
// detection will not work
|
||||||
@@ -174,6 +176,46 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
|||||||
|
|
||||||
return String(productName);
|
return String(productName);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
|
bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
|
||||||
|
{
|
||||||
|
|
||||||
|
i2cBus->beginTransmission(address);
|
||||||
|
i2cBus->write(0xFA);
|
||||||
|
i2cBus->write(0x0F);
|
||||||
|
|
||||||
|
if (i2cBus->endTransmission() != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (i2cBus->requestFrom(address, (uint8_t)8) != 8)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Just flush the data
|
||||||
|
while (i2cBus->available() < 8) {
|
||||||
|
i2cBus->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
i2cBus->beginTransmission(address);
|
||||||
|
i2cBus->write(0xFC);
|
||||||
|
i2cBus->write(0xC9);
|
||||||
|
|
||||||
|
if (i2cBus->endTransmission() != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (i2cBus->requestFrom(address, (uint8_t)6) != 6)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Just flush the data
|
||||||
|
while (i2cBus->available() < 6) {
|
||||||
|
i2cBus->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume we detect the SHT21 if something came back from the request
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||||
case ADDR: \
|
case ADDR: \
|
||||||
@@ -371,27 +413,47 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
#if !defined(M5STACK_UNITC6L)
|
#if !defined(M5STACK_UNITC6L)
|
||||||
case INA_ADDR:
|
case INA_ADDR: // Same as SHT2X
|
||||||
case INA_ADDR_ALTERNATE:
|
case INA_ADDR_ALTERNATE:
|
||||||
case INA_ADDR_WAVESHARE_UPS:
|
case INA_ADDR_WAVESHARE_UPS: {
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
uint16_t mfg = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||||
LOG_DEBUG("Register MFG_UID: 0x%x", registerValue);
|
|
||||||
if (registerValue == 0x5449) {
|
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
|
|
||||||
LOG_DEBUG("Register DIE_UID: 0x%x", registerValue);
|
|
||||||
|
|
||||||
if (registerValue == 0x2260) {
|
LOG_DEBUG("Register MFG_UID: 0x%x", mfg);
|
||||||
|
|
||||||
|
// Only read DIE_UID for vendors we recognize as INA-compatible to avoid
|
||||||
|
// an extra I2C transaction + delay on other devices sharing this address.
|
||||||
|
if (mfg == 0x5449 || mfg == 0x190F) {
|
||||||
|
uint16_t die = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
|
||||||
|
LOG_DEBUG("Register DIE_UID: 0x%x", die);
|
||||||
|
|
||||||
|
// TI INA226 or fully compatible clones (e.g. TPA626)
|
||||||
|
if (mfg == 0x5449 && die == 0x2260) {
|
||||||
logFoundDevice("INA226", (uint8_t)addr.address);
|
logFoundDevice("INA226", (uint8_t)addr.address);
|
||||||
type = INA226;
|
type = INA226;
|
||||||
} else {
|
}
|
||||||
|
// Silergy SQ52201 (INA226-compatible with different IDs)
|
||||||
|
else if (mfg == 0x190F && die == 0x0000) {
|
||||||
|
logFoundDevice("INA226 (SQ52201)", (uint8_t)addr.address);
|
||||||
|
type = INA226;
|
||||||
|
}
|
||||||
|
// TI INA260
|
||||||
|
else if (mfg == 0x5449) {
|
||||||
logFoundDevice("INA260", (uint8_t)addr.address);
|
logFoundDevice("INA260", (uint8_t)addr.address);
|
||||||
type = INA260;
|
type = INA260;
|
||||||
}
|
}
|
||||||
} else { // Assume INA219 if INA260 ID is not found
|
}
|
||||||
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
|
if (type == NONE && detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) {
|
||||||
|
logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address);
|
||||||
|
type = SHTXX;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
else { // Assume INA219 if none of the above ones are found
|
||||||
logFoundDevice("INA219", (uint8_t)addr.address);
|
logFoundDevice("INA219", (uint8_t)addr.address);
|
||||||
type = INA219;
|
type = INA219;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case INA3221_ADDR:
|
case INA3221_ADDR:
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||||
LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue);
|
LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue);
|
||||||
@@ -448,22 +510,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
case SHTXX_ADDR: // same as OPT3001_ADDR_ALT
|
||||||
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
case SHTXX_ADDR_ALT: // same as OPT3001_ADDR
|
||||||
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
||||||
type = OPT3001;
|
type = OPT3001;
|
||||||
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
||||||
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
|
} else { // SHTXX
|
||||||
type = SHT4X;
|
type = SHTXX;
|
||||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
logFoundDevice("SHTXX", (uint8_t)addr.address);
|
||||||
} else {
|
|
||||||
type = SHT31;
|
|
||||||
logFoundDevice("SHT31", (uint8_t)addr.address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTXX, "SHTXX", (uint8_t)addr.address)
|
||||||
case RCWL9620_ADDR:
|
case RCWL9620_ADDR:
|
||||||
// get MAX30102 PARTID
|
// get MAX30102 PARTID
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1);
|
||||||
@@ -699,6 +758,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||||
String prod = "";
|
String prod = "";
|
||||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||||
if (prod.startsWith("SEN55")) {
|
if (prod.startsWith("SEN55")) {
|
||||||
@@ -714,6 +774,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
logFoundDevice("Sensirion SEN50", addr.address);
|
logFoundDevice("Sensirion SEN50", addr.address);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (addr.address == BMX160_ADDR) {
|
if (addr.address == BMX160_ADDR) {
|
||||||
type = BMX160;
|
type = BMX160;
|
||||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||||
|
|||||||
@@ -103,6 +103,14 @@ static int32_t gpsSwitch()
|
|||||||
if (gps) {
|
if (gps) {
|
||||||
int currentState = digitalRead(PIN_GPS_SWITCH);
|
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 the switch is set to zero, disable the GPS Thread
|
||||||
if (firstrun)
|
if (firstrun)
|
||||||
if (currentState == LOW)
|
if (currentState == LOW)
|
||||||
|
|||||||
@@ -70,25 +70,20 @@ bool GPSUpdateScheduling::isUpdateDue()
|
|||||||
// Have we been searching for a GPS position for too long?
|
// Have we been searching for a GPS position for too long?
|
||||||
bool GPSUpdateScheduling::searchedTooLong()
|
bool GPSUpdateScheduling::searchedTooLong()
|
||||||
{
|
{
|
||||||
constexpr uint32_t oneMinuteMs = 60UL * 1000UL;
|
|
||||||
constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs; // Hard cap: 15 minutes is always too long
|
|
||||||
uint32_t elapsed = elapsedSearchMs();
|
|
||||||
|
|
||||||
// Anything over 15 minutes is too long, regardless of the broadcast interval.
|
|
||||||
// TODO: Make a smarter algorithm that backs off the search dwell time when not getting a lock.
|
|
||||||
if (elapsed > maxSearchClampMs)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
uint32_t minimumOrConfiguredSecs =
|
uint32_t minimumOrConfiguredSecs =
|
||||||
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
|
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
|
||||||
uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);
|
uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);
|
||||||
|
// If broadcast interval set to max, no such thing as "too long"
|
||||||
|
if (maxSearchMs == UINT32_MAX)
|
||||||
|
return false;
|
||||||
|
|
||||||
// If we've been searching longer than our position broadcast interval: that's too long
|
// If we've been searching longer than our position broadcast interval: that's too long
|
||||||
if (elapsed > maxSearchMs)
|
else if (elapsedSearchMs() > maxSearchMs)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Otherwise, not too long yet!
|
// Otherwise, not too long yet!
|
||||||
return false;
|
else
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation
|
// Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation
|
||||||
|
|||||||
+99
-7
@@ -60,6 +60,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "mesh/Channels.h"
|
#include "mesh/Channels.h"
|
||||||
|
#include "mesh/Default.h"
|
||||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
@@ -98,6 +99,7 @@ namespace graphics
|
|||||||
|
|
||||||
// This means the *visible* area (sh1106 can address 132, but shows 128 for example)
|
// This means the *visible* area (sh1106 can address 132, but shows 128 for example)
|
||||||
#define IDLE_FRAMERATE 1 // in fps
|
#define IDLE_FRAMERATE 1 // in fps
|
||||||
|
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||||
|
|
||||||
// DEBUG
|
// DEBUG
|
||||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||||
@@ -135,6 +137,60 @@ static bool heartbeat = false;
|
|||||||
|
|
||||||
extern bool hasUnreadMessage;
|
extern bool hasUnreadMessage;
|
||||||
|
|
||||||
|
static inline float wrapHeading360(float heading)
|
||||||
|
{
|
||||||
|
if (heading < 0.0f) {
|
||||||
|
heading += 360.0f;
|
||||||
|
} else if (heading >= 360.0f) {
|
||||||
|
heading -= 360.0f;
|
||||||
|
}
|
||||||
|
return heading;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::setHeading(float heading)
|
||||||
|
{
|
||||||
|
const float wrappedHeading = wrapHeading360(heading);
|
||||||
|
|
||||||
|
if (!hasCompass) {
|
||||||
|
hasCompass = true;
|
||||||
|
compassHeading = wrappedHeading;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate using shortest-path angular delta to avoid jumps around 0/360.
|
||||||
|
float delta = wrappedHeading - compassHeading;
|
||||||
|
if (delta > 180.0f) {
|
||||||
|
delta -= 360.0f;
|
||||||
|
} else if (delta < -180.0f) {
|
||||||
|
delta += 360.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adaptive filtering:
|
||||||
|
// - Strong damping for tiny deltas (jitter)
|
||||||
|
// - Faster response for larger turns
|
||||||
|
const float absDelta = (delta >= 0.0f) ? delta : -delta;
|
||||||
|
if (absDelta < 1.0f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float alpha = 0.35f;
|
||||||
|
if (absDelta > 25.0f) {
|
||||||
|
alpha = 0.85f;
|
||||||
|
} else if (absDelta > 10.0f) {
|
||||||
|
alpha = 0.65f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float step = delta * alpha;
|
||||||
|
const float maxStep = 12.0f;
|
||||||
|
if (step > maxStep) {
|
||||||
|
step = maxStep;
|
||||||
|
} else if (step < -maxStep) {
|
||||||
|
step = -maxStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
compassHeading = wrapHeading360(compassHeading + step);
|
||||||
|
}
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// Overlay Alert Banner Renderer
|
// Overlay Alert Banner Renderer
|
||||||
// ==============================
|
// ==============================
|
||||||
@@ -272,10 +328,25 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
|||||||
float Screen::estimatedHeading(double lat, double lon)
|
float Screen::estimatedHeading(double lat, double lon)
|
||||||
{
|
{
|
||||||
static double oldLat, oldLon;
|
static double oldLat, oldLon;
|
||||||
static float b;
|
static float b = -1.0f;
|
||||||
|
static uint32_t lastHeadingAtMs = 0;
|
||||||
|
const uint32_t now = millis();
|
||||||
|
const uint32_t gpsUpdateIntervalSecs =
|
||||||
|
Default::getConfiguredOrDefault(config.position.gps_update_interval, default_gps_update_interval);
|
||||||
|
uint32_t effectiveUpdateIntervalSecs = gpsUpdateIntervalSecs;
|
||||||
|
if (config.position.position_broadcast_smart_enabled) {
|
||||||
|
const uint32_t smartMinIntervalSecs = Default::getConfiguredOrDefault(
|
||||||
|
config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs);
|
||||||
|
if (smartMinIntervalSecs > effectiveUpdateIntervalSecs) {
|
||||||
|
effectiveUpdateIntervalSecs = smartMinIntervalSecs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Two expected update windows; keep arithmetic 32-bit to avoid pulling in larger 64-bit helpers.
|
||||||
|
const uint32_t headingStaleMs =
|
||||||
|
(effectiveUpdateIntervalSecs > (UINT32_MAX / 2000U)) ? UINT32_MAX : (effectiveUpdateIntervalSecs * 2000U);
|
||||||
|
|
||||||
if (oldLat == 0) {
|
if (oldLat == 0) {
|
||||||
// just prepare for next time
|
// Need at least two position points before we can infer heading.
|
||||||
oldLat = lat;
|
oldLat = lat;
|
||||||
oldLon = lon;
|
oldLon = lon;
|
||||||
|
|
||||||
@@ -283,12 +354,20 @@ float Screen::estimatedHeading(double lat, double lon)
|
|||||||
}
|
}
|
||||||
|
|
||||||
float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon);
|
float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon);
|
||||||
if (d < 10) // haven't moved enough, just keep current bearing
|
if (d < 10) { // haven't moved enough, keep previous heading (invalid until first real movement)
|
||||||
|
if (lastHeadingAtMs != 0 && (now - lastHeadingAtMs) >= headingStaleMs) {
|
||||||
|
// Heading is stale after prolonged no-movement; force reacquire.
|
||||||
|
b = -1.0f;
|
||||||
|
oldLat = lat;
|
||||||
|
oldLon = lon;
|
||||||
|
}
|
||||||
return b;
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
|
b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
|
||||||
oldLat = lat;
|
oldLat = lat;
|
||||||
oldLon = lon;
|
oldLon = lon;
|
||||||
|
lastHeadingAtMs = now;
|
||||||
|
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
@@ -923,9 +1002,22 @@ int32_t Screen::runOnce()
|
|||||||
// but we should only call setTargetFPS when framestate changes, because
|
// but we should only call setTargetFPS when framestate changes, because
|
||||||
// otherwise that breaks animations.
|
// otherwise that breaks animations.
|
||||||
|
|
||||||
if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) {
|
uint32_t desiredFramerate = IDLE_FRAMERATE;
|
||||||
|
#if HAS_GPS && !defined(USE_EINK)
|
||||||
|
if (showingNormalScreen && hasCompass) {
|
||||||
|
const uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||||
|
if ((framesetInfo.positions.gps != 255 && currentFrame == framesetInfo.positions.gps) ||
|
||||||
|
(framesetInfo.positions.waypoint != 255 && currentFrame == framesetInfo.positions.waypoint) ||
|
||||||
|
(framesetInfo.positions.firstFavorite != 255 && currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||||
|
currentFrame <= framesetInfo.positions.lastFavorite)) {
|
||||||
|
desiredFramerate = COMPASS_ACTIVE_FRAMERATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (targetFramerate != desiredFramerate && ui->getUiState()->frameState == FIXED) {
|
||||||
// oldFrameState = ui->getUiState()->frameState;
|
// oldFrameState = ui->getUiState()->frameState;
|
||||||
targetFramerate = IDLE_FRAMERATE;
|
targetFramerate = desiredFramerate;
|
||||||
|
|
||||||
ui->setTargetFPS(targetFramerate);
|
ui->setTargetFPS(targetFramerate);
|
||||||
forceDisplay();
|
forceDisplay();
|
||||||
@@ -1197,7 +1289,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,7 +1318,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
|
||||||
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed)
|
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
||||||
|
|
||||||
// Focus on a specific frame, in the frame set we just created
|
// Focus on a specific frame, in the frame set we just created
|
||||||
switch (focus) {
|
switch (focus) {
|
||||||
|
|||||||
@@ -330,15 +330,11 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
// Function to allow the AccelerometerThread to set the heading if a sensor provides it
|
// Function to allow the AccelerometerThread to set the heading if a sensor provides it
|
||||||
// Mutex needed?
|
// Mutex needed?
|
||||||
void setHeading(long _heading)
|
void setHeading(float heading);
|
||||||
{
|
|
||||||
hasCompass = true;
|
|
||||||
compassHeading = fmod(_heading, 360);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasHeading() { return hasCompass; }
|
bool hasHeading() { return hasCompass; }
|
||||||
|
|
||||||
long getHeading() { return compassHeading; }
|
float getHeading() { return compassHeading; }
|
||||||
|
|
||||||
void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; }
|
void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; }
|
||||||
uint32_t getEndCalibration() { return endCalibrationAt; }
|
uint32_t getEndCalibration() { return endCalibrationAt; }
|
||||||
|
|||||||
+19
-22
@@ -428,7 +428,7 @@ static LGFX *tft = nullptr;
|
|||||||
|
|
||||||
#elif defined(ST7789_CS)
|
#elif defined(ST7789_CS)
|
||||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||||
#if defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)
|
#ifdef HELTEC_V4_TFT
|
||||||
#include "chsc6x.h"
|
#include "chsc6x.h"
|
||||||
#include "lgfx/v1/Touch.hpp"
|
#include "lgfx/v1/Touch.hpp"
|
||||||
namespace lgfx
|
namespace lgfx
|
||||||
@@ -450,11 +450,7 @@ class TOUCH_CHSC6X : public ITouch
|
|||||||
bool init(void) override
|
bool init(void) override
|
||||||
{
|
{
|
||||||
if (chsc6xTouch == nullptr) {
|
if (chsc6xTouch == nullptr) {
|
||||||
#if (TOUCH_I2C_PORT == 1)
|
|
||||||
chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
|
chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
|
||||||
#else
|
|
||||||
chsc6xTouch = new chsc6x(&Wire, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
chsc6xTouch->chsc6x_init();
|
chsc6xTouch->chsc6x_init();
|
||||||
return true;
|
return true;
|
||||||
@@ -491,7 +487,7 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
#if HAS_TOUCHSCREEN
|
#if HAS_TOUCHSCREEN
|
||||||
#if defined(T_WATCH_S3) || defined(ELECROW)
|
#if defined(T_WATCH_S3) || defined(ELECROW)
|
||||||
lgfx::Touch_FT5x06 _touch_instance;
|
lgfx::Touch_FT5x06 _touch_instance;
|
||||||
#elif defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)
|
#elif defined(HELTEC_V4_TFT)
|
||||||
lgfx::TOUCH_CHSC6X _touch_instance;
|
lgfx::TOUCH_CHSC6X _touch_instance;
|
||||||
#else
|
#else
|
||||||
lgfx::Touch_GT911 _touch_instance;
|
lgfx::Touch_GT911 _touch_instance;
|
||||||
@@ -510,11 +506,7 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
|
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
|
||||||
// 80MHz by an integer)
|
// 80MHz by an integer)
|
||||||
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
|
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
|
||||||
#ifdef SPI_3_WIRE
|
cfg.spi_3wire = false;
|
||||||
cfg.spi_3wire = SPI_3_WIRE;
|
|
||||||
#else
|
|
||||||
cfg.spi_3wire = true; // Set to true if reception is done on the MOSI pin
|
|
||||||
#endif
|
|
||||||
cfg.use_lock = true; // Set to true to use transaction locking
|
cfg.use_lock = true; // Set to true to use transaction locking
|
||||||
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
|
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
|
||||||
// SPI_DMA_CH_AUTO=auto setting)
|
// SPI_DMA_CH_AUTO=auto setting)
|
||||||
@@ -564,11 +556,8 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
|
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
|
||||||
cfg.dlen_16bit =
|
cfg.dlen_16bit =
|
||||||
false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
|
false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
|
||||||
#if defined(HAS_SDCARD)
|
|
||||||
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
|
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
|
||||||
#else
|
|
||||||
cfg.bus_shared = false;
|
|
||||||
#endif
|
|
||||||
// Set the following only when the display is shifted with a driver with a variable number of pixels, such as the
|
// Set the following only when the display is shifted with a driver with a variable number of pixels, such as the
|
||||||
// ST7735 or ILI9163.
|
// ST7735 or ILI9163.
|
||||||
// cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
|
// cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
|
||||||
@@ -1265,14 +1254,14 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
|
|
||||||
// Did we find a pixel that needs updating on this row?
|
// Did we find a pixel that needs updating on this row?
|
||||||
if (x_FirstPixelUpdate < displayWidth) {
|
if (x_FirstPixelUpdate < displayWidth) {
|
||||||
|
// Align the first pixel for update to an even number so the total alignment of
|
||||||
|
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
|
||||||
|
x_FirstPixelUpdate &= ~1;
|
||||||
|
|
||||||
// Quickly write out the first changed pixel (saves another array lookup)
|
// Step 3a: copy rest of the pixels in this row into the pixel line buffer,
|
||||||
linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
|
// while also recording the last pixel in the row that needs updating.
|
||||||
x_LastPixelUpdate = x_FirstPixelUpdate;
|
// Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set.
|
||||||
|
for (x = x_FirstPixelUpdate; x < displayWidth; x++) {
|
||||||
// Step 3: copy all remaining pixels in this row into the pixel line buffer,
|
|
||||||
// while also recording the last pixel in the row that needs updating
|
|
||||||
for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) {
|
|
||||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||||
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
|
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
|
||||||
|
|
||||||
@@ -1285,6 +1274,14 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
x_LastPixelUpdate = x;
|
x_LastPixelUpdate = x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Step 3b: Round up the last pixel to odd number to maintain 32-bit alignment for SPIs.
|
||||||
|
// Most displays will have even number of pixels in a row -- this will be in bounds
|
||||||
|
// of the displayWidth. (Hopefully odd displays will just ignore that extra pixel.)
|
||||||
|
x_LastPixelUpdate |= 1;
|
||||||
|
// Ensure the last pixel index does not exceed the display width.
|
||||||
|
if (x_LastPixelUpdate >= displayWidth) {
|
||||||
|
x_LastPixelUpdate = displayWidth - 1;
|
||||||
|
}
|
||||||
#if defined(HACKADAY_COMMUNICATOR)
|
#if defined(HACKADAY_COMMUNICATOR)
|
||||||
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
|
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
|
||||||
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
|
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
#include "CompassRenderer.h"
|
#include "CompassRenderer.h"
|
||||||
#include "NodeDB.h"
|
|
||||||
#include "UIRenderer.h"
|
|
||||||
#include "configuration.h"
|
|
||||||
#include "gps/GeoCoord.h"
|
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -21,8 +17,8 @@ struct Point {
|
|||||||
|
|
||||||
void rotate(float angle)
|
void rotate(float angle)
|
||||||
{
|
{
|
||||||
float cos_a = cos(angle);
|
float cos_a = cosf(angle);
|
||||||
float sin_a = sin(angle);
|
float sin_a = sinf(angle);
|
||||||
float new_x = x * cos_a - y * sin_a;
|
float new_x = x * cos_a - y * sin_a;
|
||||||
float new_y = x * sin_a + y * cos_a;
|
float new_y = x * sin_a + y * cos_a;
|
||||||
x = new_x;
|
x = new_x;
|
||||||
@@ -51,21 +47,30 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
|||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
radius += 4;
|
radius += 4;
|
||||||
}
|
}
|
||||||
Point north(0, -radius);
|
float northX = 0.0f;
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
float northY = -radius;
|
||||||
north.rotate(-myHeading);
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
|
||||||
north.translate(compassX, compassY);
|
const float c = cosf(-myHeading);
|
||||||
|
const float s = sinf(-myHeading);
|
||||||
|
const float rx = northX * c - northY * s;
|
||||||
|
const float ry = northX * s + northY * c;
|
||||||
|
northX = rx;
|
||||||
|
northY = ry;
|
||||||
|
}
|
||||||
|
northX += compassX;
|
||||||
|
northY += compassY;
|
||||||
|
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
|
const int16_t nLabelWidth = display->getStringWidth("N");
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
||||||
} else {
|
} else {
|
||||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
||||||
}
|
}
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->drawString(north.x, north.y - 3, "N");
|
display->drawString(northX, northY - 3, "N");
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||||
@@ -113,11 +118,46 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f
|
|||||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
float estimatedHeading(double lat, double lon)
|
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
||||||
{
|
{
|
||||||
// Simple magnetic declination estimation
|
headingRadian = 0.0f;
|
||||||
// This is a very basic implementation - the original might be more sophisticated
|
|
||||||
return 0.0f; // Return 0 for now, indicating no heading available
|
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!screen)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (screen->hasHeading()) {
|
||||||
|
headingRadian = screen->getHeading() * DEG_TO_RAD;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float estimatedHeadingDeg = screen->estimatedHeading(lat, lon);
|
||||||
|
if (!(estimatedHeadingDeg >= 0.0f))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
headingRadian = estimatedHeadingDeg * DEG_TO_RAD;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float adjustBearingForCompassMode(float bearingRadian, float headingRadian)
|
||||||
|
{
|
||||||
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||||
|
return bearingRadian - headingRadian;
|
||||||
|
|
||||||
|
return bearingRadian;
|
||||||
|
}
|
||||||
|
|
||||||
|
float radiansToDegrees360(float angleRadian)
|
||||||
|
{
|
||||||
|
constexpr float fullTurnDeg = 360.0f;
|
||||||
|
float degrees = angleRadian * RAD_TO_DEG;
|
||||||
|
if (degrees < 0.0f)
|
||||||
|
degrees += fullTurnDeg;
|
||||||
|
else if (degrees >= fullTurnDeg)
|
||||||
|
degrees -= fullTurnDeg;
|
||||||
|
return degrees;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
#include <OLEDDisplayUi.h>
|
#include <OLEDDisplayUi.h>
|
||||||
|
|
||||||
@@ -25,7 +24,9 @@ void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, u
|
|||||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing);
|
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing);
|
||||||
|
|
||||||
// Navigation and location functions
|
// Navigation and location functions
|
||||||
float estimatedHeading(double lat, double lon);
|
bool getHeadingRadians(double lat, double lon, float &headingRadian);
|
||||||
|
float adjustBearingForCompassMode(float bearingRadian, float headingRadian);
|
||||||
|
float radiansToDegrees360(float angleRadian);
|
||||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
||||||
|
|
||||||
} // namespace CompassRenderer
|
} // namespace CompassRenderer
|
||||||
|
|||||||
@@ -408,7 +408,16 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
display->drawString(nameX, getTextPositions(display)[line++], device_role);
|
display->drawString(nameX, getTextPositions(display)[line++], device_role);
|
||||||
|
|
||||||
// === Third Row: Radio Preset ===
|
// === 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];
|
char regionradiopreset[25];
|
||||||
const char *region = myRegion ? myRegion->name : NULL;
|
const char *region = myRegion ? myRegion->name : NULL;
|
||||||
@@ -416,7 +425,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
if (currentResolution == ScreenResolution::UltraLow) {
|
||||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
|
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
|
||||||
} else {
|
} else {
|
||||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, modeStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textWidth = display->getStringWidth(regionradiopreset);
|
textWidth = display->getStringWidth(regionradiopreset);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "mesh/Default.h"
|
#include "mesh/Default.h"
|
||||||
#include "mesh/MeshTypes.h"
|
#include "mesh/MeshTypes.h"
|
||||||
|
#include "mesh/RadioLibInterface.h"
|
||||||
#include "modules/AdminModule.h"
|
#include "modules/AdminModule.h"
|
||||||
#include "modules/CannedMessageModule.h"
|
#include "modules/CannedMessageModule.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
#include "modules/TraceRouteModule.h"
|
#include "modules/TraceRouteModule.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -159,31 +161,22 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
return;
|
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.
|
||||||
|
// TODO: change this to either use the validateLoraConfig() logic or at least check the region for wideLora
|
||||||
|
// rather than a hardcoded check for LORA_24.
|
||||||
|
if (selectedRegion == meshtastic_Config_LoRaConfig_RegionCode_LORA_24 &&
|
||||||
|
!(RadioLibInterface::instance && RadioLibInterface::instance->wideLora())) {
|
||||||
|
LOG_WARN("Radio hardware does not support 2.4 GHz; ignoring region selection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
config.lora.region = selectedRegion;
|
config.lora.region = selectedRegion;
|
||||||
auto changes = SEGMENT_CONFIG;
|
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 !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||||
if (!owner.is_licensed) {
|
if (crypto) {
|
||||||
bool keygenSuccess = false;
|
crypto->ensurePkiKeys(config.security, owner);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
config.lora.tx_enabled = true;
|
config.lora.tx_enabled = true;
|
||||||
@@ -199,7 +192,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
}
|
}
|
||||||
|
|
||||||
service->reloadConfig(changes);
|
service->reloadConfig(changes);
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bannerOptions.durationMs = duration;
|
bannerOptions.durationMs = duration;
|
||||||
@@ -265,13 +257,24 @@ void menuHandler::FrequencySlotPicker()
|
|||||||
optionsEnumArray[options++] = 0;
|
optionsEnumArray[options++] = 0;
|
||||||
|
|
||||||
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
||||||
|
|
||||||
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||||
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
|
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
|
||||||
: bwCodeToKHz(loraConfig.bandwidth);
|
: bwCodeToKHz(loraConfig.bandwidth);
|
||||||
|
|
||||||
uint32_t numChannels = 0;
|
uint32_t numChannels = 0;
|
||||||
if (myRegion) {
|
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 {
|
} else {
|
||||||
LOG_WARN("Region not set, cannot calculate number of channels");
|
LOG_WARN("Region not set, cannot calculate number of channels");
|
||||||
return;
|
return;
|
||||||
@@ -307,7 +310,6 @@ void menuHandler::FrequencySlotPicker()
|
|||||||
|
|
||||||
config.lora.channel_num = selected;
|
config.lora.channel_num = selected;
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
@@ -346,7 +348,6 @@ void menuHandler::radioPresetPicker()
|
|||||||
config.lora.channel_num = 0; // Reset to default channel for the preset
|
config.lora.channel_num = 0; // Reset to default channel for the preset
|
||||||
config.lora.override_frequency = 0; // Clear any custom frequency
|
config.lora.override_frequency = 0; // Clear any custom frequency
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#include "CompassRenderer.h"
|
#include "CompassRenderer.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "NodeListRenderer.h"
|
#include "NodeListRenderer.h"
|
||||||
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
|
#include "modules/StatusMessageModule.h"
|
||||||
|
#endif
|
||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
#include "gps/RTC.h" // for getTime() function
|
#include "gps/RTC.h" // for getTime() function
|
||||||
@@ -92,8 +95,41 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
|||||||
|
|
||||||
// 1) Choose target candidate (long vs short) only if present
|
// 1) Choose target candidate (long vs short) only if present
|
||||||
const char *raw = nullptr;
|
const char *raw = nullptr;
|
||||||
if (node && node->has_user) {
|
|
||||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
|
// If long-name mode is enabled, and we have a recent status for this node,
|
||||||
|
// prefer "(short_name) statusText" as the raw candidate.
|
||||||
|
std::string composedFromStatus;
|
||||||
|
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) {
|
||||||
|
const auto &recent = statusMessageModule->getRecentReceived();
|
||||||
|
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||||
|
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||||
|
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||||
|
found = &(*it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
const char *shortName = node->user.short_name;
|
||||||
|
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
|
||||||
|
composedFromStatus += "(";
|
||||||
|
if (shortName && *shortName) {
|
||||||
|
composedFromStatus += shortName;
|
||||||
|
}
|
||||||
|
composedFromStatus += ") ";
|
||||||
|
composedFromStatus += found->statusText;
|
||||||
|
|
||||||
|
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// If we didn't compose from status, use normal long/short selection
|
||||||
|
if (!raw) {
|
||||||
|
if (node && node->has_user) {
|
||||||
|
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Preserve UTF-8 names so emotes can be detected and rendered.
|
// 2) Preserve UTF-8 names so emotes can be detected and rendered.
|
||||||
@@ -239,9 +275,12 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
|
|
||||||
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
||||||
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||||
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
constexpr int kBarCount = 4;
|
||||||
|
constexpr int kBarWidth = 2;
|
||||||
|
constexpr int kBarGap = 1;
|
||||||
|
|
||||||
int barsXOffset = columnWidth - barsOffset;
|
int barsXOffset = columnWidth - barsOffset;
|
||||||
|
int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth;
|
||||||
|
|
||||||
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
||||||
char nodeName[96];
|
char nodeName[96];
|
||||||
@@ -268,28 +307,35 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw signal strength bars
|
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
||||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
|
||||||
int barWidth = 2;
|
|
||||||
int barStartX = x + barsXOffset;
|
|
||||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
|
||||||
|
|
||||||
for (int b = 0; b < 4; b++) {
|
// Show signal only for direct neighbors (0 hops)
|
||||||
if (b < bars) {
|
if (isZeroHop) {
|
||||||
int height = (b * 2);
|
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||||
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
|
int barStartX = x + barsXOffset;
|
||||||
|
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||||
|
|
||||||
|
for (int b = 0; b < kBarCount; b++) {
|
||||||
|
if (b < bars) {
|
||||||
|
int height = (b * 2);
|
||||||
|
display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw hop count
|
// Draw hop count + hop icon
|
||||||
char hopStr[6] = "";
|
if (node->has_hops_away && node->hops_away > 0) {
|
||||||
if (node->has_hops_away && node->hops_away > 0)
|
char hopCount[6];
|
||||||
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
|
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
|
||||||
|
|
||||||
if (hopStr[0] != '\0') {
|
const int hopCountWidth = display->getStringWidth(hopCount);
|
||||||
int rightEdge = x + columnWidth - hopOffset;
|
const int gap = 1;
|
||||||
int textWidth = display->getStringWidth(hopStr);
|
const int totalWidth = hopCountWidth + gap + hop_width;
|
||||||
display->drawString(rightEdge - textWidth, y, hopStr);
|
const int hopX = barsRightEdge - totalWidth;
|
||||||
|
const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||||
|
|
||||||
|
display->drawString(hopX, y, hopCount);
|
||||||
|
display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,14 +419,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strlen(distStr) > 0) {
|
const char *distanceLabel = (strlen(distStr) > 0) ? distStr : "?";
|
||||||
int offset = (currentResolution == ScreenResolution::High)
|
int offset = (currentResolution == ScreenResolution::High)
|
||||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||||
int rightEdge = x + columnWidth - offset;
|
int rightEdge = x + columnWidth - offset;
|
||||||
int textWidth = display->getStringWidth(distStr);
|
int textWidth = display->getStringWidth(distanceLabel);
|
||||||
display->drawString(rightEdge - textWidth, y, distStr);
|
display->drawString(rightEdge - textWidth, y, distanceLabel);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
@@ -431,8 +476,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||||
double userLat, double userLon)
|
float myHeadingRadian, double userLat, double userLon)
|
||||||
{
|
{
|
||||||
if (!nodeDB->hasValidPosition(node))
|
if (!nodeDB->hasValidPosition(node))
|
||||||
return;
|
return;
|
||||||
@@ -446,11 +491,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
double nodeLat = node->position.latitude_i * 1e-7;
|
double nodeLat = node->position.latitude_i * 1e-7;
|
||||||
double nodeLon = node->position.longitude_i * 1e-7;
|
double nodeLon = node->position.longitude_i * 1e-7;
|
||||||
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
||||||
float bearingToNode = RAD_TO_DEG * bearing;
|
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
||||||
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
||||||
// Shrink size by 2px
|
// Shrink size by 2px
|
||||||
int size = FONT_HEIGHT_SMALL - 5;
|
int size = FONT_HEIGHT_SMALL - 5;
|
||||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing);
|
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearingDeg);
|
||||||
/*
|
/*
|
||||||
float angle = relativeBearing * DEG_TO_RAD;
|
float angle = relativeBearing * DEG_TO_RAD;
|
||||||
float halfSize = size / 2.0;
|
float halfSize = size / 2.0;
|
||||||
@@ -480,12 +525,27 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void drawCompassUnknown(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float, double,
|
||||||
|
double)
|
||||||
|
{
|
||||||
|
if (!nodeDB->hasValidPosition(node))
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||||
|
int centerX = x + columnWidth - arrowXOffset;
|
||||||
|
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display->drawString(centerX, y, "?");
|
||||||
|
}
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// Main Screen Functions
|
// Main Screen Functions
|
||||||
// =============================
|
// =============================
|
||||||
|
|
||||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||||
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon)
|
EntryRenderer renderer, NodeExtrasRenderer extras, float headingRadian, double lat, double lon)
|
||||||
{
|
{
|
||||||
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||||
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||||
@@ -570,7 +630,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
renderer(display, node, xPos, yPos, columnWidth);
|
renderer(display, node, xPos, yPos, columnWidth);
|
||||||
|
|
||||||
if (extras)
|
if (extras)
|
||||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
extras(display, node, xPos, yPos, columnWidth, headingRadian, lat, lon);
|
||||||
|
|
||||||
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||||
yOffset += rowYOffset;
|
yOffset += rowYOffset;
|
||||||
@@ -765,9 +825,13 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
#endif
|
#endif
|
||||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
float heading = 0;
|
float headingRadian = 0.0f;
|
||||||
bool validHeading = false;
|
|
||||||
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) {
|
||||||
|
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
double lat = DegD(ourNode->position.latitude_i);
|
double lat = DegD(ourNode->position.latitude_i);
|
||||||
double lon = DegD(ourNode->position.longitude_i);
|
double lon = DegD(ourNode->position.longitude_i);
|
||||||
|
|
||||||
@@ -779,21 +843,12 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
|||||||
lastSwitchTime = now;
|
lastSwitchTime = now;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
if (!CompassRenderer::getHeadingRadians(lat, lon, headingRadian)) {
|
||||||
#if HAS_GPS
|
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, lat, lon);
|
||||||
if (screen->hasHeading()) {
|
return;
|
||||||
heading = screen->getHeading(); // degrees
|
|
||||||
validHeading = true;
|
|
||||||
} else {
|
|
||||||
heading = screen->estimatedHeading(lat, lon);
|
|
||||||
validHeading = !isnan(heading);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!validHeading)
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon);
|
|
||||||
|
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATIO
|
|||||||
|
|
||||||
// Main node list screen function
|
// Main node list screen function
|
||||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
|
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float headingRadian = 0, double lat = 0,
|
||||||
double lon = 0);
|
double lon = 0);
|
||||||
|
|
||||||
// Entry renderers
|
// Entry renderers
|
||||||
@@ -43,8 +43,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
|||||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||||
|
|
||||||
// Extras renderers
|
// Extras renderers
|
||||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||||
double userLat, double userLon);
|
float myHeadingRadian, double userLat, double userLon);
|
||||||
|
|
||||||
// Screen frame functions
|
// Screen frame functions
|
||||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|||||||
+229
-180
@@ -5,6 +5,9 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "NodeListRenderer.h"
|
#include "NodeListRenderer.h"
|
||||||
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
|
#include "modules/StatusMessageModule.h"
|
||||||
|
#endif
|
||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
@@ -38,6 +41,15 @@ static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void drawCompassStatusText(OLEDDisplay *display, int16_t compassX, int16_t compassY, const char *statusLine1,
|
||||||
|
const char *statusLine2)
|
||||||
|
{
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1);
|
||||||
|
display->drawString(compassX, compassY, statusLine2);
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
void graphics::UIRenderer::rebuildFavoritedNodes()
|
void graphics::UIRenderer::rebuildFavoritedNodes()
|
||||||
{
|
{
|
||||||
favoritedNodes.clear();
|
favoritedNodes.clear();
|
||||||
@@ -290,7 +302,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
|||||||
// * Favorite Node Info *
|
// * Favorite Node Info *
|
||||||
// **********************
|
// **********************
|
||||||
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
|
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
|
||||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
if (favoritedNodes.empty())
|
if (favoritedNodes.empty())
|
||||||
return;
|
return;
|
||||||
@@ -342,10 +354,61 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 2. Signal and Hops (combined on one line, if available) ===
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
char signalHopsStr[32] = "";
|
// === Optional: Last received StatusMessage line for this node ===
|
||||||
|
// Display it directly under the username line (if we have one).
|
||||||
|
if (statusMessageModule) {
|
||||||
|
const auto &recent = statusMessageModule->getRecentReceived();
|
||||||
|
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||||
|
|
||||||
|
// Search newest-to-oldest
|
||||||
|
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||||
|
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||||
|
found = &(*it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
std::string statusLine = std::string(" Status: ") + found->statusText;
|
||||||
|
{
|
||||||
|
const int screenW = display->getWidth();
|
||||||
|
const int ellipseW = display->getStringWidth("...");
|
||||||
|
int w = display->getStringWidth(statusLine.c_str());
|
||||||
|
|
||||||
|
// Only do work if it overflows
|
||||||
|
if (w > screenW) {
|
||||||
|
bool truncated = false;
|
||||||
|
if (ellipseW > screenW) {
|
||||||
|
statusLine.clear();
|
||||||
|
} else {
|
||||||
|
while (!statusLine.empty()) {
|
||||||
|
// remove one char (byte) at a time
|
||||||
|
statusLine.pop_back();
|
||||||
|
truncated = true;
|
||||||
|
|
||||||
|
// Measure candidate with ellipsis appended
|
||||||
|
std::string candidate = statusLine + "...";
|
||||||
|
if (display->getStringWidth(candidate.c_str()) <= screenW) {
|
||||||
|
statusLine = std::move(candidate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusLine.empty() && ellipseW <= screenW) {
|
||||||
|
statusLine = "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], statusLine.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// === 2. Signal/Hops line (if available) ===
|
||||||
bool haveSignal = false;
|
bool haveSignal = false;
|
||||||
int bars = 0;
|
int bars = 0;
|
||||||
|
const char *qualityLabel = nullptr;
|
||||||
|
|
||||||
// Helper to get SNR limit based on modem preset
|
// Helper to get SNR limit based on modem preset
|
||||||
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
|
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
|
||||||
@@ -366,80 +429,51 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate signal grade using modem preset and SNR only
|
|
||||||
float snrLimit = getSnrLimit(config.lora.modem_preset);
|
|
||||||
float snr = node->snr;
|
|
||||||
|
|
||||||
// Determine signal quality label and bars using SNR-only grading
|
|
||||||
const char *qualityLabel = nullptr;
|
|
||||||
|
|
||||||
if (snr > snrLimit + 10) {
|
|
||||||
qualityLabel = "Good";
|
|
||||||
bars = 4;
|
|
||||||
} else if (snr > snrLimit + 6) {
|
|
||||||
qualityLabel = "Good";
|
|
||||||
bars = 3;
|
|
||||||
} else if (snr > snrLimit + 2) {
|
|
||||||
qualityLabel = "Good";
|
|
||||||
bars = 2;
|
|
||||||
} else if (snr > snrLimit - 4) {
|
|
||||||
qualityLabel = "Fair";
|
|
||||||
bars = 1;
|
|
||||||
} else {
|
|
||||||
qualityLabel = "Bad";
|
|
||||||
bars = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add extra spacing on the left if we have an API connection to account for the common footer icons
|
// Add extra spacing on the left if we have an API connection to account for the common footer icons
|
||||||
const char *leftSideSpacing =
|
const char *leftSideSpacing =
|
||||||
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
|
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
|
||||||
|
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
||||||
|
|
||||||
// --- Build the Signal/Hops line ---
|
// Signal text/bars are only for direct (zero-hop) nodes with valid SNR.
|
||||||
// Only show signal if we have valid SNR
|
if (isZeroHop) {
|
||||||
if (snr > -100 && snr != 0) {
|
float snr = node->snr;
|
||||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
|
if (snr > -100 && snr != 0) {
|
||||||
haveSignal = true;
|
float snrLimit = getSnrLimit(config.lora.modem_preset);
|
||||||
}
|
// Determine signal quality label and bars using SNR-only grading.
|
||||||
|
if (snr > snrLimit + 10) {
|
||||||
if (node->hops_away > 0) {
|
qualityLabel = "Good";
|
||||||
size_t len = strlen(signalHopsStr);
|
bars = 4;
|
||||||
if (haveSignal) {
|
} else if (snr > snrLimit + 6) {
|
||||||
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
|
qualityLabel = "Good";
|
||||||
} else {
|
bars = 3;
|
||||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
|
} else if (snr > snrLimit + 2) {
|
||||||
}
|
qualityLabel = "Good";
|
||||||
}
|
bars = 2;
|
||||||
|
} else if (snr > snrLimit - 4) {
|
||||||
if (signalHopsStr[0]) {
|
qualityLabel = "Fair";
|
||||||
int yPos = getTextPositions(display)[line++];
|
bars = 1;
|
||||||
int curX = x;
|
} else {
|
||||||
|
qualityLabel = "Bad";
|
||||||
// Split combined string into signal text and hop suffix
|
bars = 1;
|
||||||
char sigPart[20] = "";
|
|
||||||
const char *hopPart = nullptr;
|
|
||||||
|
|
||||||
char *bracket = strchr(signalHopsStr, '[');
|
|
||||||
if (bracket) {
|
|
||||||
size_t n = (size_t)(bracket - signalHopsStr);
|
|
||||||
if (n >= sizeof(sigPart))
|
|
||||||
n = sizeof(sigPart) - 1;
|
|
||||||
memcpy(sigPart, signalHopsStr, n);
|
|
||||||
sigPart[n] = '\0';
|
|
||||||
|
|
||||||
// Trim trailing spaces
|
|
||||||
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
|
|
||||||
sigPart[strlen(sigPart) - 1] = '\0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hopPart = bracket; // "[n Hop(s)]"
|
haveSignal = true;
|
||||||
} else {
|
|
||||||
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
|
|
||||||
sigPart[sizeof(sigPart) - 1] = '\0';
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw signal quality text
|
const bool showHops = node->has_hops_away && node->hops_away > 0;
|
||||||
display->drawString(curX, yPos, sigPart);
|
|
||||||
curX += display->getStringWidth(sigPart) + 4;
|
if (haveSignal || showHops) {
|
||||||
|
int yPos = getTextPositions(display)[line++];
|
||||||
|
int curX = x + display->getStringWidth(leftSideSpacing);
|
||||||
|
|
||||||
|
// Draw signal quality text for zero-hop nodes when present.
|
||||||
|
if (haveSignal && qualityLabel) {
|
||||||
|
char signalLabel[20];
|
||||||
|
snprintf(signalLabel, sizeof(signalLabel), "Sig:%s", qualityLabel);
|
||||||
|
display->drawString(curX, yPos, signalLabel);
|
||||||
|
curX += display->getStringWidth(signalLabel) + 4;
|
||||||
|
}
|
||||||
|
|
||||||
// Draw signal bars (skip on UltraLow, text only)
|
// Draw signal bars (skip on UltraLow, text only)
|
||||||
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
|
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
|
||||||
@@ -478,12 +512,12 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
|
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw hops AFTER the bars as: [ number + hop icon ]
|
// Draw hops for non-zero-hop nodes as: number + hop icon.
|
||||||
if (hopPart && node->hops_away > 0) {
|
// This path is mutually exclusive with the zero-hop signal-bars path above.
|
||||||
|
if (showHops) {
|
||||||
// open bracket
|
// hop label
|
||||||
display->drawString(curX, yPos, "[");
|
display->drawString(curX, yPos, "Hop:");
|
||||||
curX += display->getStringWidth("[") + 1;
|
curX += display->getStringWidth("Hop:") + 2;
|
||||||
|
|
||||||
// hop count
|
// hop count
|
||||||
char hopCount[6];
|
char hopCount[6];
|
||||||
@@ -495,9 +529,6 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||||
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
|
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
|
||||||
curX += hop_width + 1;
|
curX += hop_width + 1;
|
||||||
|
|
||||||
// closing bracket
|
|
||||||
display->drawString(curX, yPos, "]");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,51 +669,54 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
display->drawString(x, getTextPositions(display)[line++], batLine);
|
display->drawString(x, getTextPositions(display)[line++], batLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool showCompass = false;
|
||||||
|
float myHeading = 0.0f;
|
||||||
|
float bearing = 0.0f;
|
||||||
|
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||||
|
const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
|
||||||
|
const char *statusLine1 = nullptr;
|
||||||
|
const char *statusLine2 = nullptr;
|
||||||
|
if (hasOwnPositionFix && hasNodePositionFix) {
|
||||||
|
const auto &op = ourNode->position;
|
||||||
|
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
||||||
|
if (showCompass) {
|
||||||
|
const auto &p = node->position;
|
||||||
|
bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||||
|
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
|
||||||
|
} else {
|
||||||
|
statusLine1 = "No";
|
||||||
|
statusLine2 = "Heading";
|
||||||
|
}
|
||||||
|
} else if (!hasOwnPositionFix || !hasNodePositionFix) {
|
||||||
|
statusLine1 = "No";
|
||||||
|
statusLine2 = "Fix";
|
||||||
|
}
|
||||||
|
|
||||||
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
|
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
|
||||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||||
bool showCompass = false;
|
if (showCompass || statusLine1) {
|
||||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) {
|
|
||||||
showCompass = true;
|
|
||||||
}
|
|
||||||
if (showCompass) {
|
|
||||||
const int16_t topY = getTextPositions(display)[1];
|
const int16_t topY = getTextPositions(display)[1];
|
||||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
|
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
|
||||||
const int16_t usableHeight = bottomY - topY - 5;
|
const int16_t usableHeight = bottomY - topY - 5;
|
||||||
int16_t compassRadius = usableHeight / 2;
|
int16_t compassRadius = usableHeight / 2;
|
||||||
if (compassRadius < 8)
|
if (compassRadius < 8)
|
||||||
compassRadius = 8;
|
compassRadius = 8;
|
||||||
const int16_t compassDiam = compassRadius * 2;
|
|
||||||
const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8;
|
const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8;
|
||||||
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||||
|
const int16_t compassDiam = compassRadius * 2;
|
||||||
const auto &op = ourNode->position;
|
|
||||||
float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
|
||||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
|
||||||
|
|
||||||
const auto &p = node->position;
|
|
||||||
/* unused
|
|
||||||
float d =
|
|
||||||
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
|
||||||
*/
|
|
||||||
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
|
||||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
|
||||||
myHeading = 0;
|
|
||||||
} else {
|
|
||||||
bearing -= myHeading;
|
|
||||||
}
|
|
||||||
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
display->drawCircle(compassX, compassY, compassRadius);
|
||||||
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
if (showCompass) {
|
||||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||||
|
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
||||||
|
} else {
|
||||||
|
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// else show nothing
|
// else show nothing
|
||||||
} else {
|
} else {
|
||||||
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
||||||
bool showCompass = false;
|
if (showCompass || statusLine1) {
|
||||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) {
|
|
||||||
showCompass = true;
|
|
||||||
}
|
|
||||||
if (showCompass) {
|
|
||||||
int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2)
|
int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2)
|
||||||
: getTextPositions(display)[1];
|
: getTextPositions(display)[1];
|
||||||
const int margin = 4;
|
const int margin = 4;
|
||||||
@@ -693,8 +727,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
#else
|
#else
|
||||||
const int navBarHeight = 0;
|
const int navBarHeight = 0;
|
||||||
#endif
|
#endif
|
||||||
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
|
||||||
// --------- END PATCH FOR EINK NAV BAR -----------
|
// --------- END PATCH FOR EINK NAV BAR -----------
|
||||||
|
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||||
|
|
||||||
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
||||||
return;
|
return;
|
||||||
@@ -708,25 +742,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
int compassX = x + SCREEN_WIDTH / 2;
|
int compassX = x + SCREEN_WIDTH / 2;
|
||||||
int compassY = yBelowContent + availableHeight / 2;
|
int compassY = yBelowContent + availableHeight / 2;
|
||||||
|
|
||||||
const auto &op = ourNode->position;
|
|
||||||
float myHeading = 0;
|
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
|
||||||
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
|
||||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
|
||||||
}
|
|
||||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
|
||||||
|
|
||||||
const auto &p = node->position;
|
|
||||||
/* unused
|
|
||||||
float d =
|
|
||||||
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
|
||||||
*/
|
|
||||||
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
|
|
||||||
bearing -= myHeading;
|
|
||||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
|
||||||
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
display->drawCircle(compassX, compassY, compassRadius);
|
||||||
|
if (showCompass) {
|
||||||
|
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||||
|
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
||||||
|
} else {
|
||||||
|
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// else show nothing
|
// else show nothing
|
||||||
}
|
}
|
||||||
@@ -1162,6 +1184,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
const int *textPos = getTextPositions(display);
|
||||||
|
|
||||||
// === First Row: My Location ===
|
// === First Row: My Location ===
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
@@ -1176,12 +1199,12 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
} else {
|
} else {
|
||||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||||
}
|
}
|
||||||
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
drawSatelliteIcon(display, x, textPos[line]);
|
||||||
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
|
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
|
||||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
display->drawString(x + 11 + xOffset, textPos[line++], displayLine);
|
||||||
} else {
|
} else {
|
||||||
// Onboard GPS
|
// Onboard GPS
|
||||||
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
UIRenderer::drawGps(display, 0, textPos[line++], gpsStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.display.heading_bold = origBold;
|
config.display.heading_bold = origBold;
|
||||||
@@ -1190,18 +1213,36 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
|
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
|
||||||
int32_t(gpsStatus->getAltitude()));
|
int32_t(gpsStatus->getAltitude()));
|
||||||
|
|
||||||
// === Determine Compass Heading ===
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
float heading = 0;
|
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||||
|
const bool hasLiveGpsFix =
|
||||||
|
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
|
||||||
|
const bool hasSensorHeading = screen->hasHeading();
|
||||||
|
float heading = 0.0f;
|
||||||
bool validHeading = false;
|
bool validHeading = false;
|
||||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
const char *statusLine1 = nullptr;
|
||||||
validHeading = true;
|
const char *statusLine2 = nullptr;
|
||||||
} else {
|
if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) {
|
||||||
if (screen->hasHeading()) {
|
double headingLat = 0.0;
|
||||||
heading = radians(screen->getHeading());
|
double headingLon = 0.0;
|
||||||
validHeading = true;
|
if (hasLiveGpsFix) {
|
||||||
|
headingLat = DegD(gpsStatus->getLatitude());
|
||||||
|
headingLon = DegD(gpsStatus->getLongitude());
|
||||||
|
} else if (hasOwnPositionFix) {
|
||||||
|
const auto &op = ourNode->position;
|
||||||
|
headingLat = DegD(op.latitude_i);
|
||||||
|
headingLon = DegD(op.longitude_i);
|
||||||
|
}
|
||||||
|
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validHeading) {
|
||||||
|
if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) {
|
||||||
|
statusLine1 = "No";
|
||||||
|
statusLine2 = "Heading";
|
||||||
} else {
|
} else {
|
||||||
heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
|
statusLine1 = "No";
|
||||||
validHeading = !isnan(heading);
|
statusLine2 = "Fix";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1219,18 +1260,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
|
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
|
display->drawString(0, textPos[line++], uptimeStr);
|
||||||
} else {
|
} else {
|
||||||
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
|
display->drawString(0, textPos[line++], "Last: ?");
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Third Row: Line 1 GPS Info ===
|
// === Third Row: Line 1 GPS Info ===
|
||||||
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1");
|
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line1");
|
||||||
|
|
||||||
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
|
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
|
||||||
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
|
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
|
||||||
// === Fourth Row: Line 2 GPS Info ===
|
// === Fourth Row: Line 2 GPS Info ===
|
||||||
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
|
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line2");
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Final Row: Altitude ===
|
// === Final Row: Altitude ===
|
||||||
@@ -1241,14 +1282,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
} else {
|
} else {
|
||||||
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
|
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
|
||||||
}
|
}
|
||||||
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
|
display->drawString(x, textPos[line++], altitudeLine);
|
||||||
}
|
}
|
||||||
#if !defined(M5STACK_UNITC6L)
|
#if !defined(M5STACK_UNITC6L)
|
||||||
// === Draw Compass if heading is valid ===
|
// === Draw Compass ===
|
||||||
if (validHeading) {
|
if (validHeading || statusLine1) {
|
||||||
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
|
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
|
||||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||||
const int16_t topY = getTextPositions(display)[1];
|
const int16_t topY = textPos[1];
|
||||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height
|
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height
|
||||||
const int16_t usableHeight = bottomY - topY - 5;
|
const int16_t usableHeight = bottomY - topY - 5;
|
||||||
|
|
||||||
@@ -1261,29 +1302,33 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
// Center vertically and nudge down slightly to keep "N" clear of header
|
// Center vertically and nudge down slightly to keep "N" clear of header
|
||||||
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||||
|
|
||||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading);
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
display->drawCircle(compassX, compassY, compassRadius);
|
||||||
|
if (validHeading) {
|
||||||
|
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading);
|
||||||
|
|
||||||
// "N" label
|
// "N" label
|
||||||
float northAngle = 0;
|
float northAngle = 0;
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||||
northAngle = -heading;
|
northAngle = -heading;
|
||||||
float radius = compassRadius;
|
float radius = compassRadius;
|
||||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||||
|
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||||
|
} else {
|
||||||
|
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
||||||
// For E-Ink screens, account for navigation bar at the bottom!
|
// For E-Ink screens, account for navigation bar at the bottom!
|
||||||
int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2;
|
int yBelowContent = textPos[5] + FONT_HEIGHT_SMALL + 2;
|
||||||
const int margin = 4;
|
const int margin = 4;
|
||||||
int availableHeight =
|
int availableHeight =
|
||||||
#if defined(USE_EINK)
|
#if defined(USE_EINK)
|
||||||
@@ -1304,25 +1349,29 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
int compassX = x + SCREEN_WIDTH / 2;
|
int compassX = x + SCREEN_WIDTH / 2;
|
||||||
int compassY = yBelowContent + availableHeight / 2;
|
int compassY = yBelowContent + availableHeight / 2;
|
||||||
|
|
||||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading);
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
display->drawCircle(compassX, compassY, compassRadius);
|
||||||
|
if (validHeading) {
|
||||||
|
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading);
|
||||||
|
|
||||||
// "N" label
|
// "N" label
|
||||||
float northAngle = 0;
|
float northAngle = 0;
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||||
northAngle = -heading;
|
northAngle = -heading;
|
||||||
float radius = compassRadius;
|
float radius = compassRadius;
|
||||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||||
|
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||||
|
} else {
|
||||||
|
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class UIRenderer
|
|||||||
// Navigation bar overlay
|
// Navigation bar overlay
|
||||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
|
||||||
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
|||||||
@@ -177,24 +177,8 @@ static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region)
|
|||||||
auto changes = SEGMENT_CONFIG;
|
auto changes = SEGMENT_CONFIG;
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||||
if (!owner.is_licensed) {
|
if (crypto) {
|
||||||
bool keygenSuccess = false;
|
crypto->ensurePkiKeys(config.security, owner);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -390,11 +390,8 @@ void InputBroker::Init()
|
|||||||
seesawRotary = nullptr;
|
seesawRotary = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ifdef __linux__
|
|
||||||
// Linux evdev keyboard input only — macOS has no <linux/input.h>.
|
|
||||||
aLinuxInputImpl = new LinuxInputImpl();
|
aLinuxInputImpl = new LinuxInputImpl();
|
||||||
aLinuxInputImpl->init();
|
aLinuxInputImpl->init();
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// Linux evdev keyboard input. Only compiled on Linux portduino targets;
|
#if ARCH_PORTDUINO
|
||||||
// macOS / non-Linux builds have no <linux/input.h> or epoll, and the
|
|
||||||
// headless build doesn't need real keyboards anyway.
|
|
||||||
#if ARCH_PORTDUINO && defined(__linux__)
|
|
||||||
#include "InputBroker.h"
|
#include "InputBroker.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Linux evdev impl. Same Linux-only gating as LinuxInput.h.
|
#ifdef ARCH_PORTDUINO
|
||||||
#if defined(ARCH_PORTDUINO) && defined(__linux__)
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "LinuxInput.h"
|
#include "LinuxInput.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
|||||||
+23
-5
@@ -10,8 +10,26 @@
|
|||||||
#include "memGet.h"
|
#include "memGet.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#ifdef ARCH_STM32WL
|
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
|
||||||
#include <malloc.h>
|
#include <malloc.h>
|
||||||
|
#include <unistd.h> // sbrk
|
||||||
|
|
||||||
|
#ifdef ARCH_STM32WL
|
||||||
|
// Returns the uncommitted sbrk headroom: addressable space between the current heap
|
||||||
|
// break and the stack pointer that has not yet been committed to the arena.
|
||||||
|
static uint32_t sbrkHeadroom()
|
||||||
|
{
|
||||||
|
// defined in STM32 linker script
|
||||||
|
extern char _estack;
|
||||||
|
extern char _Min_Stack_Size;
|
||||||
|
|
||||||
|
uint32_t max_sp = (uint32_t)(&_estack - &_Min_Stack_Size);
|
||||||
|
uint32_t heap_end = (uint32_t)sbrk(0);
|
||||||
|
return (max_sp > heap_end) ? (max_sp - heap_end) : 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#error Unsupported architecture!
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
MemGet memGet;
|
MemGet memGet;
|
||||||
@@ -28,9 +46,9 @@ uint32_t MemGet::getFreeHeap()
|
|||||||
return dbgHeapFree();
|
return dbgHeapFree();
|
||||||
#elif defined(ARCH_RP2040)
|
#elif defined(ARCH_RP2040)
|
||||||
return rp2040.getFreeHeap();
|
return rp2040.getFreeHeap();
|
||||||
#elif defined(ARCH_STM32WL)
|
#elif defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
||||||
struct mallinfo m = mallinfo();
|
struct mallinfo m = mallinfo();
|
||||||
return m.fordblks; // Total free space (bytes)
|
return m.fordblks + sbrkHeadroom(); // Free space within arena + uncommitted sbrk headroom
|
||||||
#else
|
#else
|
||||||
// this platform does not have heap management function implemented
|
// this platform does not have heap management function implemented
|
||||||
return UINT32_MAX;
|
return UINT32_MAX;
|
||||||
@@ -49,9 +67,9 @@ uint32_t MemGet::getHeapSize()
|
|||||||
return dbgHeapTotal();
|
return dbgHeapTotal();
|
||||||
#elif defined(ARCH_RP2040)
|
#elif defined(ARCH_RP2040)
|
||||||
return rp2040.getTotalHeap();
|
return rp2040.getTotalHeap();
|
||||||
#elif defined(ARCH_STM32WL)
|
#elif defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
||||||
struct mallinfo m = mallinfo();
|
struct mallinfo m = mallinfo();
|
||||||
return m.arena; // Non-mmapped space allocated (bytes)
|
return m.arena + sbrkHeadroom(); // Non-mmapped space allocated + uncommitted sbrk headroom
|
||||||
#else
|
#else
|
||||||
// this platform does not have heap management function implemented
|
// this platform does not have heap management function implemented
|
||||||
return UINT32_MAX;
|
return UINT32_MAX;
|
||||||
|
|||||||
@@ -71,6 +71,33 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey)
|
|||||||
}
|
}
|
||||||
return true;
|
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
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class CryptoEngine
|
|||||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN)
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN)
|
||||||
virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey);
|
virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey);
|
||||||
virtual bool regeneratePublicKey(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
|
#endif
|
||||||
void setDHPrivateKey(uint8_t *_private_key);
|
void setDHPrivateKey(uint8_t *_private_key);
|
||||||
|
|||||||
+7
-17
@@ -2,21 +2,18 @@
|
|||||||
|
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
|
|
||||||
// Convert seconds to ms, clamping at INT32_MAX (~24.86 days)
|
|
||||||
static inline uint32_t secondsToMsClamped(uint32_t secs)
|
|
||||||
{
|
|
||||||
constexpr uint32_t MAX_MS = static_cast<uint32_t>(INT32_MAX);
|
|
||||||
return (secs > MAX_MS / 1000U) ? MAX_MS : secs * 1000U;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval)
|
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval)
|
||||||
{
|
{
|
||||||
return secondsToMsClamped(configuredInterval > 0 ? configuredInterval : defaultInterval);
|
if (configuredInterval > 0)
|
||||||
|
return configuredInterval * 1000;
|
||||||
|
return defaultInterval * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval)
|
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval)
|
||||||
{
|
{
|
||||||
return secondsToMsClamped(configuredInterval > 0 ? configuredInterval : default_broadcast_interval_secs);
|
if (configuredInterval > 0)
|
||||||
|
return configuredInterval * 1000;
|
||||||
|
return default_broadcast_interval_secs * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue)
|
uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue)
|
||||||
@@ -50,14 +47,7 @@ uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t d
|
|||||||
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER))
|
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER))
|
||||||
return getConfiguredOrDefaultMs(configured, defaultValue);
|
return getConfiguredOrDefaultMs(configured, defaultValue);
|
||||||
|
|
||||||
// Saturate at INT32_MAX to match secondsToMsClamped: float→uint32_t when
|
return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes);
|
||||||
// out of range is UB, and the result is consumed as an int32_t downstream.
|
|
||||||
constexpr uint32_t MAX_MS = static_cast<uint32_t>(INT32_MAX);
|
|
||||||
uint32_t base = getConfiguredOrDefaultMs(configured, defaultValue);
|
|
||||||
float coef = congestionScalingCoefficient(numOnlineNodes);
|
|
||||||
if (static_cast<double>(base) * static_cast<double>(coef) >= static_cast<double>(MAX_MS))
|
|
||||||
return MAX_MS;
|
|
||||||
return base * coef;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue)
|
uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue)
|
||||||
|
|||||||
@@ -30,6 +30,11 @@
|
|||||||
#define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour
|
#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 min_neighbor_info_broadcast_secs 4 * 60 * 60
|
||||||
#define default_map_publish_interval_secs 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 / 2) // 12 hours between identical positions
|
||||||
|
|
||||||
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
||||||
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -19,12 +19,8 @@ extern Adafruit_nRFCrypto nRFCrypto;
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#elif defined(ARCH_PORTDUINO)
|
#elif defined(ARCH_PORTDUINO)
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <sys/random.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#ifdef __linux__
|
|
||||||
#include <sys/random.h> // getrandom()
|
|
||||||
#else
|
|
||||||
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace HardwareRNG
|
namespace HardwareRNG
|
||||||
@@ -52,11 +48,8 @@ bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
|
|||||||
// and return false so callers know no extra mixing occurred.
|
// and return false so callers know no extra mixing occurred.
|
||||||
RadioLibInterface *radio = RadioLibInterface::instance;
|
RadioLibInterface *radio = RadioLibInterface::instance;
|
||||||
if (!radio) {
|
if (!radio) {
|
||||||
// This path can run during portduinoSetup() before the console is initialized,
|
// Intentionally silent: this path runs during portduinoSetup() before the
|
||||||
// both for unit-test binaries and the simulator's meshtasticd; LOG_* dereferences `console`.
|
// console/SerialConsole is initialized, so LOG_* here would dereference a null pointer.
|
||||||
if (console) {
|
|
||||||
LOG_ERROR("No radio instance available to provide entropy");
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,16 +116,10 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
|||||||
filled = true;
|
filled = true;
|
||||||
#elif defined(ARCH_PORTDUINO)
|
#elif defined(ARCH_PORTDUINO)
|
||||||
// Prefer the host OS RNG first when running under Portduino.
|
// Prefer the host OS RNG first when running under Portduino.
|
||||||
#ifdef __linux__
|
|
||||||
ssize_t generated = ::getrandom(buffer, length, 0);
|
ssize_t generated = ::getrandom(buffer, length, 0);
|
||||||
if (generated == static_cast<ssize_t>(length)) {
|
if (generated == static_cast<ssize_t>(length)) {
|
||||||
filled = true;
|
filled = true;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
// arc4random_buf is available on Darwin/BSD and cannot fail.
|
|
||||||
::arc4random_buf(buffer, length);
|
|
||||||
filled = true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!filled) {
|
if (!filled) {
|
||||||
fillWithRandomDevice(buffer, length);
|
fillWithRandomDevice(buffer, length);
|
||||||
|
|||||||
@@ -71,12 +71,10 @@ template <typename T> bool LR11x0Interface<T>::init()
|
|||||||
|
|
||||||
RadioLibInterface::init();
|
RadioLibInterface::init();
|
||||||
|
|
||||||
limitPower(LR1110_MAX_POWER);
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
||||||
|
limitPower(LR1120_MAX_POWER);
|
||||||
if ((power > LR1120_MAX_POWER) &&
|
} else {
|
||||||
(config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range
|
limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range
|
||||||
power = LR1120_MAX_POWER;
|
|
||||||
preambleLength = 12; // 12 is the default for operation above 2GHz
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef LR11X0_RF_SWITCH_SUBGHZ
|
#ifdef LR11X0_RF_SWITCH_SUBGHZ
|
||||||
@@ -177,6 +175,12 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
|||||||
err = lora.setSyncWord(syncWord);
|
err = lora.setSyncWord(syncWord);
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
assert(err == RADIOLIB_ERR_NONE);
|
||||||
|
|
||||||
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
||||||
|
limitPower(LR1120_MAX_POWER);
|
||||||
|
} else {
|
||||||
|
limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range
|
||||||
|
}
|
||||||
|
|
||||||
err = lora.setPreambleLength(preambleLength);
|
err = lora.setPreambleLength(preambleLength);
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
assert(err == RADIOLIB_ERR_NONE);
|
||||||
|
|
||||||
@@ -184,14 +188,14 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
|||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||||
|
|
||||||
if (power > LR1110_MAX_POWER) // This chip has lower power limits than some
|
|
||||||
power = LR1110_MAX_POWER;
|
|
||||||
if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit
|
|
||||||
power = LR1120_MAX_POWER;
|
|
||||||
|
|
||||||
err = lora.setOutputPower(power);
|
err = lora.setOutputPower(power);
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
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
|
startReceive(); // restart receiving
|
||||||
|
|
||||||
return RADIOLIB_ERR_NONE;
|
return RADIOLIB_ERR_NONE;
|
||||||
|
|||||||
+15
-8
@@ -6,11 +6,8 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "detect/LoRaRadioType.h"
|
#include "detect/LoRaRadioType.h"
|
||||||
|
|
||||||
// Sentinel marking the end of a modem preset array. Declared `const` rather
|
// Sentinel marking the end of a modem preset array
|
||||||
// than `constexpr` because the cast from 0xFF to the enum is out-of-range and
|
static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
||||||
// therefore not a valid constant expression on Clang 16+ (Apple Clang on
|
|
||||||
// macOS). The value is only ever compared at runtime, so static-init is fine.
|
|
||||||
static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
|
||||||
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
|
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
|
||||||
|
|
||||||
// Region profile: bundles the preset list with regulatory parameters shared across regions
|
// Region profile: bundles the preset list with regulatory parameters shared across regions
|
||||||
@@ -38,13 +35,23 @@ struct RegionInfo {
|
|||||||
meshtastic_Config_LoRaConfig_RegionCode code;
|
meshtastic_Config_LoRaConfig_RegionCode code;
|
||||||
float freqStart;
|
float freqStart;
|
||||||
float freqEnd;
|
float freqEnd;
|
||||||
float dutyCycle;
|
float dutyCycle; // modified by getEffectiveDutyCycle
|
||||||
float spacing;
|
|
||||||
uint8_t powerLimit; // Or zero for not set
|
uint8_t powerLimit; // Or zero for not set
|
||||||
bool audioPermitted;
|
|
||||||
bool freqSwitching;
|
bool freqSwitching;
|
||||||
bool wideLora;
|
bool wideLora;
|
||||||
|
const RegionProfile *profile;
|
||||||
const char *name; // EU433 etc
|
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[];
|
extern const RegionInfo regions[];
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||||
#include "modules/TraceRouteModule.h"
|
#include "modules/TraceRouteModule.h"
|
||||||
#endif
|
#endif
|
||||||
|
#if HAS_TRAFFIC_MANAGEMENT
|
||||||
|
#include "modules/TrafficManagementModule.h"
|
||||||
|
#endif
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
|
|
||||||
NextHopRouter::NextHopRouter() {}
|
NextHopRouter::NextHopRouter() {}
|
||||||
@@ -98,9 +101,12 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
|||||||
if (origTx) {
|
if (origTx) {
|
||||||
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
|
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
|
||||||
// directly from the destination
|
// directly from the destination
|
||||||
bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to);
|
// Single lookup for both relayer checks on the same (request_id, to) pair
|
||||||
|
bool wasAlreadyRelayer = false;
|
||||||
bool weWereSoleRelayer = false;
|
bool weWereSoleRelayer = false;
|
||||||
bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer);
|
bool weWereRelayer = false;
|
||||||
|
checkRelayers(p->relay_node, ourRelayID, p->decoded.request_id, p->to, &wasAlreadyRelayer, &weWereRelayer,
|
||||||
|
&weWereSoleRelayer);
|
||||||
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
|
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
|
||||||
if (origTx->next_hop != p->relay_node) { // Not already set
|
if (origTx->next_hop != p->relay_node) { // Not already set
|
||||||
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
|
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
|
||||||
@@ -126,15 +132,28 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
|||||||
/* Check if we should be rebroadcasting this packet if so, do so. */
|
/* Check if we should be rebroadcasting this packet if so, do so. */
|
||||||
bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
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 (p->id != 0) {
|
||||||
if (isRebroadcaster()) {
|
if (isRebroadcaster()) {
|
||||||
if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) {
|
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
|
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);
|
LOG_INFO("Rebroadcast received message coming from %x", p->relay_node);
|
||||||
|
|
||||||
// Use shared logic to determine if hop_limit should be decremented
|
// If exhausting hops, force hop_limit = 0 regardless of other logic
|
||||||
if (shouldDecrementHopLimit(p)) {
|
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
|
tosend->hop_limit--; // bump down the hop count
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit");
|
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit");
|
||||||
|
|||||||
+39
-4
@@ -688,7 +688,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
|||||||
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
|
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
|
||||||
|
|
||||||
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
|
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
|
||||||
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)) && \
|
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \
|
||||||
HAS_TFT
|
HAS_TFT
|
||||||
// switch BT off by default; use TFT programming mode or hotkey to enable
|
// switch BT off by default; use TFT programming mode or hotkey to enable
|
||||||
config.bluetooth.enabled = false;
|
config.bluetooth.enabled = false;
|
||||||
@@ -1205,11 +1205,11 @@ void NodeDB::loadFromDisk()
|
|||||||
spiLock->unlock();
|
spiLock->unlock();
|
||||||
#endif
|
#endif
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
#if defined(FACTORY_INSTALL) && !defined(ARCH_PORTDUINO)
|
#ifdef FACTORY_INSTALL
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) {
|
if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) {
|
||||||
LOG_WARN("Factory Install Reset!");
|
LOG_WARN("Factory Install Reset!");
|
||||||
rmDir("/prefs");
|
FSCom.format();
|
||||||
FSCom.mkdir("/prefs");
|
FSCom.mkdir("/prefs");
|
||||||
File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE);
|
File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE);
|
||||||
if (f2) {
|
if (f2) {
|
||||||
@@ -1306,7 +1306,7 @@ void NodeDB::loadFromDisk()
|
|||||||
// Coerce LoRa config fields derived from presets while bootstrapping.
|
// Coerce LoRa config fields derived from presets while bootstrapping.
|
||||||
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
|
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
|
||||||
if (config.has_lora && config.lora.use_preset) {
|
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
|
#if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED
|
||||||
@@ -1657,6 +1657,25 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p)
|
|||||||
return delta;
|
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)
|
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
|
// Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a
|
||||||
@@ -1694,6 +1713,22 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
|
|||||||
#include "MeshModule.h"
|
#include "MeshModule.h"
|
||||||
#include "Throttle.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
|
/** Update position info for this node based on received position data
|
||||||
*/
|
*/
|
||||||
void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src)
|
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.
|
/// Returns defaultIfUnknown if the number of hops couldn't be determined.
|
||||||
int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1);
|
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 {
|
enum LoadFileResult {
|
||||||
// Successfully opened the file
|
// Successfully opened the file
|
||||||
LOAD_SUCCESS = 1,
|
LOAD_SUCCESS = 1,
|
||||||
|
|||||||
+169
-13
@@ -1,6 +1,7 @@
|
|||||||
#include "PacketHistory.h"
|
#include "PacketHistory.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
|
#include "meshUtils.h"
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
@@ -23,6 +24,14 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
|
|||||||
size = PACKETHISTORY_MAX; // Use default size if invalid
|
size = PACKETHISTORY_MAX; // Use default size if invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
|
// Ensure capacity fits in uint16_t hash index (HASH_EMPTY = 0xFFFF is the sentinel)
|
||||||
|
if (size >= HASH_EMPTY) {
|
||||||
|
LOG_WARN("Packet History - Clamping size %d to %d (hash index limit)", size, HASH_EMPTY - 1);
|
||||||
|
size = HASH_EMPTY - 1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Allocate memory for the recent packets array
|
// Allocate memory for the recent packets array
|
||||||
recentPacketsCapacity = size;
|
recentPacketsCapacity = size;
|
||||||
recentPackets = new PacketRecord[recentPacketsCapacity];
|
recentPackets = new PacketRecord[recentPacketsCapacity];
|
||||||
@@ -35,6 +44,20 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
|
|||||||
|
|
||||||
// Initialize the recent packets array to zero
|
// Initialize the recent packets array to zero
|
||||||
memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity);
|
memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity);
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
|
// Allocate hash index with load factor <= 0.5 for short probe chains
|
||||||
|
hashCapacity = nextPowerOf2(recentPacketsCapacity * 2);
|
||||||
|
hashMask = hashCapacity - 1;
|
||||||
|
hashIndex = new uint16_t[hashCapacity];
|
||||||
|
if (!hashIndex) {
|
||||||
|
LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity);
|
||||||
|
hashCapacity = 0;
|
||||||
|
hashMask = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
PacketHistory::~PacketHistory()
|
PacketHistory::~PacketHistory()
|
||||||
@@ -42,6 +65,12 @@ PacketHistory::~PacketHistory()
|
|||||||
recentPacketsCapacity = 0;
|
recentPacketsCapacity = 0;
|
||||||
delete[] recentPackets;
|
delete[] recentPackets;
|
||||||
recentPackets = NULL;
|
recentPackets = NULL;
|
||||||
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
|
delete[] hashIndex;
|
||||||
|
hashIndex = NULL;
|
||||||
|
hashCapacity = 0;
|
||||||
|
hashMask = 0;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update recentPackets and return true if we have already seen this packet */
|
/** Update recentPackets and return true if we have already seen this packet */
|
||||||
@@ -194,7 +223,78 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
|
|||||||
return seenRecently;
|
return seenRecently;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find a packet record in history.
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
|
// Hash function for (sender, id) pairs. Uses xor-shift mixing for good distribution.
|
||||||
|
uint32_t PacketHistory::hashSlot(NodeNum sender, PacketId id) const
|
||||||
|
{
|
||||||
|
uint32_t h = sender ^ (id * 0x9E3779B9); // Fibonacci hashing constant
|
||||||
|
h ^= h >> 16;
|
||||||
|
h *= 0x45d9f3b;
|
||||||
|
h ^= h >> 16;
|
||||||
|
return h & hashMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PacketHistory::hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx)
|
||||||
|
{
|
||||||
|
if (!hashIndex)
|
||||||
|
return;
|
||||||
|
uint32_t bucket = hashSlot(sender, id);
|
||||||
|
// Guard against infinite loop if hash table is corrupted (no HASH_EMPTY slots)
|
||||||
|
for (uint32_t i = 0; i < hashCapacity; i++) {
|
||||||
|
if (hashIndex[bucket] == HASH_EMPTY) {
|
||||||
|
hashIndex[bucket] = slotIdx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bucket = (bucket + 1) & hashMask;
|
||||||
|
}
|
||||||
|
LOG_ERROR("Packet History - hashInsert: table full or corrupted, rebuilding");
|
||||||
|
hashRebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PacketHistory::hashRemove(NodeNum sender, PacketId id)
|
||||||
|
{
|
||||||
|
if (!hashIndex)
|
||||||
|
return;
|
||||||
|
uint32_t bucket = hashSlot(sender, id);
|
||||||
|
for (uint32_t i = 0; i < hashCapacity; i++) {
|
||||||
|
if (hashIndex[bucket] == HASH_EMPTY)
|
||||||
|
return;
|
||||||
|
uint16_t idx = hashIndex[bucket];
|
||||||
|
if (idx < recentPacketsCapacity && recentPackets[idx].sender == sender && recentPackets[idx].id == id) {
|
||||||
|
// Found it — delete and re-insert subsequent entries to maintain probe chain integrity
|
||||||
|
hashIndex[bucket] = HASH_EMPTY;
|
||||||
|
uint32_t next = (bucket + 1) & hashMask;
|
||||||
|
for (uint32_t j = 0; j < hashCapacity; j++) {
|
||||||
|
if (hashIndex[next] == HASH_EMPTY)
|
||||||
|
break;
|
||||||
|
uint16_t displaced = hashIndex[next];
|
||||||
|
hashIndex[next] = HASH_EMPTY;
|
||||||
|
if (displaced < recentPacketsCapacity) {
|
||||||
|
const auto &rec = recentPackets[displaced];
|
||||||
|
hashInsert(rec.sender, rec.id, displaced);
|
||||||
|
}
|
||||||
|
next = (next + 1) & hashMask;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bucket = (bucket + 1) & hashMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PacketHistory::hashRebuild()
|
||||||
|
{
|
||||||
|
if (!hashIndex)
|
||||||
|
return;
|
||||||
|
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity);
|
||||||
|
for (uint32_t i = 0; i < recentPacketsCapacity; i++) {
|
||||||
|
if (recentPackets[i].rxTimeMsec != 0)
|
||||||
|
hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Find a packet record in history using the hash index for O(1) average lookup.
|
||||||
|
* Falls back to linear scan if hash index is unavailable.
|
||||||
* @return pointer to PacketRecord if found, NULL if not found */
|
* @return pointer to PacketRecord if found, NULL if not found */
|
||||||
PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
|
PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
|
||||||
{
|
{
|
||||||
@@ -205,23 +305,40 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
PacketRecord *it = NULL;
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
|
// Use hash index for O(1) lookup when available
|
||||||
if (it->id == id && it->sender == sender) {
|
if (hashIndex) {
|
||||||
|
uint32_t bucket = hashSlot(sender, id);
|
||||||
|
for (uint32_t i = 0; i < hashCapacity; i++) {
|
||||||
|
if (hashIndex[bucket] == HASH_EMPTY)
|
||||||
|
break;
|
||||||
|
uint16_t idx = hashIndex[bucket];
|
||||||
|
if (idx < recentPacketsCapacity && recentPackets[idx].id == id && recentPackets[idx].sender == sender) {
|
||||||
#if VERBOSE_PACKET_HISTORY
|
#if VERBOSE_PACKET_HISTORY
|
||||||
LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender,
|
LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d",
|
||||||
it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec),
|
recentPackets[idx].sender, recentPackets[idx].id, recentPackets[idx].next_hop,
|
||||||
it - recentPackets, recentPacketsCapacity);
|
recentPackets[idx].relayed_by[0], recentPackets[idx].relayed_by[1], recentPackets[idx].relayed_by[2],
|
||||||
|
millis() - (recentPackets[idx].rxTimeMsec), idx, recentPacketsCapacity);
|
||||||
#endif
|
#endif
|
||||||
// only the first match is returned, so be careful not to create duplicate entries
|
return &recentPackets[idx];
|
||||||
return it; // Return pointer to the found record
|
}
|
||||||
|
bucket = (bucket + 1) & hashMask;
|
||||||
|
}
|
||||||
|
#if VERBOSE_PACKET_HISTORY
|
||||||
|
LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id);
|
||||||
|
#endif
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Linear scan (sole path when hash excluded, fallback when hash allocation failed)
|
||||||
|
for (PacketRecord *it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
|
||||||
|
if (it->id == id && it->sender == sender) {
|
||||||
|
return it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if VERBOSE_PACKET_HISTORY
|
return NULL;
|
||||||
LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id);
|
|
||||||
#endif
|
|
||||||
return NULL; // Not found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert/Replace oldest PacketRecord in recentPackets. */
|
/** Insert/Replace oldest PacketRecord in recentPackets. */
|
||||||
@@ -327,8 +444,22 @@ void PacketHistory::insert(const PacketRecord &r)
|
|||||||
return; // Return early if we can't update the history
|
return; // Return early if we can't update the history
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
|
// Maintain hash index: remove old entry if evicting a different packet, then insert new entry
|
||||||
|
bool isMatchingSlot = (tu->id == r.id && tu->sender == r.sender);
|
||||||
|
if (!isMatchingSlot && tu->rxTimeMsec != 0) {
|
||||||
|
hashRemove(tu->sender, tu->id);
|
||||||
|
}
|
||||||
|
|
||||||
*tu = r; // store the packet
|
*tu = r; // store the packet
|
||||||
|
|
||||||
|
if (!isMatchingSlot) {
|
||||||
|
hashInsert(r.sender, r.id, (uint16_t)(tu - recentPackets));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
*tu = r; // store the packet
|
||||||
|
#endif
|
||||||
|
|
||||||
#if VERBOSE_PACKET_HISTORY
|
#if VERBOSE_PACKET_HISTORY
|
||||||
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER",
|
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER",
|
||||||
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
|
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
|
||||||
@@ -396,6 +527,31 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, boo
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check two relayers against the same packet record with a single find() call,
|
||||||
|
// avoiding redundant O(N) lookups when both are checked for the same (id, sender) pair.
|
||||||
|
void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
|
||||||
|
bool *r2WasSole)
|
||||||
|
{
|
||||||
|
*r1Result = false;
|
||||||
|
*r2Result = false;
|
||||||
|
if (r2WasSole)
|
||||||
|
*r2WasSole = false;
|
||||||
|
|
||||||
|
if (!initOk()) {
|
||||||
|
LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PacketRecord *found = find(sender, id);
|
||||||
|
if (!found)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (relayer1 != 0)
|
||||||
|
*r1Result = wasRelayer(relayer1, *found);
|
||||||
|
if (relayer2 != 0)
|
||||||
|
*r2Result = wasRelayer(relayer2, *found, r2WasSole);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
|
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
|
||||||
void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
|
void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ class PacketHistory
|
|||||||
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
|
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
|
||||||
PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat.
|
PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat.
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
|
// Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan.
|
||||||
|
// Maps (sender, id) -> index into recentPackets[]. Uses linear probing with a load factor <= 0.5.
|
||||||
|
// The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity),
|
||||||
|
// and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot).
|
||||||
|
static constexpr uint16_t HASH_EMPTY = 0xFFFF;
|
||||||
|
uint16_t *hashIndex = NULL;
|
||||||
|
uint32_t hashCapacity = 0; // Always a power of 2
|
||||||
|
uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing
|
||||||
|
|
||||||
|
uint32_t hashSlot(NodeNum sender, PacketId id) const;
|
||||||
|
void hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx);
|
||||||
|
void hashRemove(NodeNum sender, PacketId id);
|
||||||
|
void hashRebuild();
|
||||||
|
#endif
|
||||||
|
|
||||||
/** Find a packet record in history.
|
/** Find a packet record in history.
|
||||||
* @param sender NodeNum
|
* @param sender NodeNum
|
||||||
* @param id PacketId
|
* @param id PacketId
|
||||||
@@ -70,6 +86,16 @@ class PacketHistory
|
|||||||
* @return true if node was indeed a relayer, false if not */
|
* @return true if node was indeed a relayer, false if not */
|
||||||
bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr);
|
bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check two relayers against the same packet record with a single lookup.
|
||||||
|
* Avoids redundant find() calls when checking multiple relayers for the same (id, sender) pair.
|
||||||
|
* @param r1Result set to true if relayer1 was a relayer
|
||||||
|
* @param r2Result set to true if relayer2 was a relayer
|
||||||
|
* @param r2WasSole if not nullptr, set to true if relayer2 was the sole relayer
|
||||||
|
*/
|
||||||
|
void checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
|
||||||
|
bool *r2WasSole = nullptr);
|
||||||
|
|
||||||
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
|
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
|
||||||
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
|
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
|
||||||
|
|
||||||
|
|||||||
+17
-8
@@ -465,8 +465,18 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||||
fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter;
|
fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||||
break;
|
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;
|
||||||
|
case meshtastic_ModuleConfig_tak_tag:
|
||||||
|
LOG_DEBUG("Send module config: tak");
|
||||||
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_tak_tag;
|
||||||
|
fromRadioScratch.moduleConfig.payload_variant.tak = moduleConfig.tak;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LOG_ERROR("Unknown module config type %d", config_state);
|
LOG_DEBUG("Unhandled module config type %d", config_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
config_state++;
|
config_state++;
|
||||||
@@ -512,6 +522,11 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
|
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
|
||||||
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
|
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
|
||||||
|
|
||||||
|
// Occasional progress logging. (readIndex==2 will be true for the first non-us node)
|
||||||
|
if (readIndex == 2 || readIndex % 20 == 0) {
|
||||||
|
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
|
||||||
|
}
|
||||||
|
|
||||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
||||||
fromRadioScratch.node_info = infoToSend;
|
fromRadioScratch.node_info = infoToSend;
|
||||||
prefetchNodeInfos();
|
prefetchNodeInfos();
|
||||||
@@ -642,11 +657,9 @@ void PhoneAPI::releaseQueueStatusPhonePacket()
|
|||||||
void PhoneAPI::prefetchNodeInfos()
|
void PhoneAPI::prefetchNodeInfos()
|
||||||
{
|
{
|
||||||
bool added = false;
|
bool added = false;
|
||||||
bool wasEmpty = false;
|
|
||||||
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
||||||
{
|
{
|
||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||||
wasEmpty = nodeInfoQueue.empty();
|
|
||||||
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
|
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
|
||||||
auto nextNode = nodeDB->readNextMeshNode(readIndex);
|
auto nextNode = nodeDB->readNextMeshNode(readIndex);
|
||||||
if (!nextNode)
|
if (!nextNode)
|
||||||
@@ -660,15 +673,11 @@ void PhoneAPI::prefetchNodeInfos()
|
|||||||
info.via_mqtt = isUs ? false : info.via_mqtt;
|
info.via_mqtt = isUs ? false : info.via_mqtt;
|
||||||
info.is_favorite = info.is_favorite || isUs;
|
info.is_favorite = info.is_favorite || isUs;
|
||||||
nodeInfoQueue.push_back(info);
|
nodeInfoQueue.push_back(info);
|
||||||
// Log progress here (at fetch time) so readIndex is accurate and each value logs only once.
|
|
||||||
if (readIndex == 2 || readIndex % 20 == 0) {
|
|
||||||
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
|
|
||||||
}
|
|
||||||
added = true;
|
added = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (added && wasEmpty)
|
if (added)
|
||||||
onNowHasData(0);
|
onNowHasData(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -240,8 +240,7 @@ bool RF95Interface::reconfigure()
|
|||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||||
|
|
||||||
if (power > RF95_MAX_POWER) // This chip has lower power limits than some
|
limitPower(RF95_MAX_POWER);
|
||||||
power = RF95_MAX_POWER;
|
|
||||||
|
|
||||||
#ifdef USE_RF95_RFO
|
#ifdef USE_RF95_RFO
|
||||||
err = lora->setOutputPower(power, true);
|
err = lora->setOutputPower(power, true);
|
||||||
|
|||||||
+313
-133
@@ -21,6 +21,7 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <pb_decode.h>
|
#include <pb_decode.h>
|
||||||
#include <pb_encode.h>
|
#include <pb_encode.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
@@ -32,10 +33,32 @@
|
|||||||
#include "STM32WLE5JCInterface.h"
|
#include "STM32WLE5JCInterface.h"
|
||||||
#endif
|
#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, \
|
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \
|
||||||
frequency_switching, wide_lora, #name \
|
wide_lora, &profile_ptr, #name \
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegionInfo regions[] = {
|
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://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf
|
||||||
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
|
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]
|
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
|
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
|
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/duty-cycle/
|
||||||
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
|
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.)
|
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
|
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
|
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://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://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf
|
||||||
https://qiita.com/ammo0613/items/d952154f1195b64dc29f
|
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://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
|
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
|
||||||
Also used in Brazil.
|
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
|
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
|
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
|
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
|
https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf
|
||||||
@@ -101,13 +123,13 @@ const RegionInfo regions[] = {
|
|||||||
Note:
|
Note:
|
||||||
- We do LBT, so 100% is allowed.
|
- 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://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0
|
||||||
https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters
|
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.
|
Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor.
|
||||||
@@ -115,44 +137,40 @@ const RegionInfo regions[] = {
|
|||||||
https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF
|
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
|
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
|
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://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
|
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
|
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||||
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
|
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
|
||||||
Thailand 920–925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
Thailand 920–925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
||||||
*/
|
*/
|
||||||
RDEF(TH, 920.0f, 925.0f, 10, 0, 27, true, false, false),
|
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
433,05-434,7 Mhz 10 mW
|
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
|
868,0-868,6 Mhz 25 mW
|
||||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
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
|
Malaysia
|
||||||
433 - 435 MHz at 100mW, no restrictions.
|
433 - 435 MHz at 100mW, no restrictions.
|
||||||
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
|
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
|
Malaysia
|
||||||
@@ -161,14 +179,14 @@ const RegionInfo regions[] = {
|
|||||||
Frequency hopping is used for 919 - 923 MHz.
|
Frequency hopping is used for 919 - 923 MHz.
|
||||||
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
|
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
|
Singapore
|
||||||
SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions.
|
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
|
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
|
Philippines
|
||||||
@@ -178,8 +196,9 @@ const RegionInfo regions[] = {
|
|||||||
https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135
|
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_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD),
|
||||||
RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false),
|
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
|
Kazakhstan
|
||||||
@@ -187,37 +206,38 @@ const RegionInfo regions[] = {
|
|||||||
863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields
|
863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields
|
||||||
https://github.com/meshtastic/firmware/issues/7204
|
https://github.com/meshtastic/firmware/issues/7204
|
||||||
*/
|
*/
|
||||||
RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false),
|
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD),
|
||||||
RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false),
|
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Nepal
|
Nepal
|
||||||
865 MHz to 868 MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use,
|
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
|
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
|
Brazil
|
||||||
902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
|
902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
|
||||||
https://github.com/meshtastic/firmware/issues/3741
|
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.
|
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.
|
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;
|
const RegionInfo *myRegion;
|
||||||
bool RadioInterface::uses_default_frequency_slot = true;
|
bool RadioInterface::uses_default_frequency_slot = true;
|
||||||
|
bool RadioInterface::uses_custom_channel_name = false;
|
||||||
|
|
||||||
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
|
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
|
||||||
|
|
||||||
@@ -503,45 +523,14 @@ void initRegion()
|
|||||||
myRegion = r;
|
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;
|
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++)
|
||||||
;
|
;
|
||||||
|
return 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ## 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 RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received)
|
||||||
{
|
{
|
||||||
uint32_t pl = 0;
|
uint32_t pl = 0;
|
||||||
@@ -751,7 +740,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)
|
void RadioInterface::saveChannelNum(uint32_t channel_num)
|
||||||
{
|
{
|
||||||
@@ -774,113 +763,304 @@ uint32_t RadioInterface::getChannelNum()
|
|||||||
return savedChannelNum;
|
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
|
* 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()
|
void RadioInterface::applyModemConfig()
|
||||||
{
|
{
|
||||||
// Set up default configuration
|
// Set up default configuration
|
||||||
// No Sync Words in LORA mode
|
// No Sync Words in LORA mode
|
||||||
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||||
bool validConfig = false; // We need to check for a valid configuration
|
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
||||||
while (!validConfig) {
|
myRegion = newRegion;
|
||||||
if (loraConfig.use_preset) {
|
|
||||||
modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr);
|
if (loraConfig.use_preset) {
|
||||||
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) {
|
if (!validateConfigLora(loraConfig)) {
|
||||||
cr = loraConfig.coding_rate;
|
loraConfig.modem_preset = newRegion->getDefaultPreset();
|
||||||
LOG_INFO("Using custom Coding Rate %u", cr);
|
}
|
||||||
}
|
uint8_t newcr;
|
||||||
} else {
|
modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr);
|
||||||
sf = loraConfig.spread_factor;
|
// 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;
|
cr = loraConfig.coding_rate;
|
||||||
bw = bwCodeToKHz(loraConfig.bandwidth);
|
LOG_INFO("Using custom Coding Rate %u", cr);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
validConfig = true;
|
cr = newcr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} 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;
|
power = loraConfig.tx_power;
|
||||||
|
|
||||||
if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed))
|
if ((power == 0) || ((power > newRegion->powerLimit) && !devicestate.owner.is_licensed))
|
||||||
power = myRegion->powerLimit;
|
power = newRegion->powerLimit;
|
||||||
|
|
||||||
if (power == 0)
|
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
|
// 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)
|
// variable set to 0 don't have a valid power limit)
|
||||||
|
|
||||||
// Set final tx_power back onto config
|
// Set final tx_power back onto config
|
||||||
loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger
|
loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger
|
||||||
|
|
||||||
// Calculate the number of channels
|
uint32_t channel_num;
|
||||||
uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000)));
|
float freq;
|
||||||
|
|
||||||
// If user has manually specified a channel num, then use that, otherwise generate one by hashing the name
|
// Calculate number of frequency slots (aka Channels):
|
||||||
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
// spacing = gap between channels (0 for continuous spectrum) and at the beginning of the band
|
||||||
// channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1)
|
// padding = gap at the beginning and end of the channel (0 for no padding)
|
||||||
uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels;
|
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
|
// Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one.
|
||||||
RadioInterface::uses_default_frequency_slot =
|
// Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to
|
||||||
channel_num ==
|
// (numFreqSlots - 1).
|
||||||
hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels;
|
uint32_t presetNameHashSlot =
|
||||||
|
hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots;
|
||||||
// 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));
|
|
||||||
|
|
||||||
// override if we have a verbatim frequency
|
// override if we have a verbatim frequency
|
||||||
if (loraConfig.override_frequency) {
|
if (loraConfig.override_frequency) {
|
||||||
freq = loraConfig.override_frequency;
|
freq = loraConfig.override_frequency;
|
||||||
channel_num = -1;
|
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);
|
saveChannelNum(channel_num);
|
||||||
saveFreq(freq + loraConfig.frequency_offset);
|
saveFreq(freq + loraConfig.frequency_offset);
|
||||||
|
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||||||
|
|
||||||
|
if (newRegion->wideLora) { // clamp if wide freq range
|
||||||
|
preambleLength = wideLoraPreambleLengthDefault; // 12 is the default for operation above 2GHz
|
||||||
|
} else {
|
||||||
|
preambleLength =
|
||||||
|
preambleLengthDefault; // 8 is default, but we use longer to increase the amount of sleep time when receiving
|
||||||
|
}
|
||||||
|
|
||||||
slotTimeMsec = computeSlotTimeMsec();
|
slotTimeMsec = computeSlotTimeMsec();
|
||||||
preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw);
|
preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw);
|
||||||
|
|
||||||
LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset);
|
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);
|
channel_num, power);
|
||||||
LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd,
|
LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd,
|
||||||
myRegion->freqEnd - myRegion->freqStart);
|
newRegion->freqEnd - newRegion->freqStart);
|
||||||
LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw);
|
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("channel_num: %d", channel_num + 1);
|
||||||
LOG_INFO("frequency: %f", getFreq());
|
LOG_INFO("frequency: %f", getFreq());
|
||||||
LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec);
|
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:
|
/** Slottime is the time to detect a transmission has started, consisting of:
|
||||||
- CAD duration;
|
- CAD duration;
|
||||||
|
|||||||
@@ -92,15 +92,20 @@ class RadioInterface
|
|||||||
uint8_t sf = 9;
|
uint8_t sf = 9;
|
||||||
uint8_t cr = 5;
|
uint8_t cr = 5;
|
||||||
|
|
||||||
const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48
|
static constexpr uint8_t NUM_SYM_CAD =
|
||||||
const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280
|
2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48
|
||||||
|
static constexpr uint8_t NUM_SYM_CAD_24GHZ =
|
||||||
|
4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280
|
||||||
uint32_t slotTimeMsec = computeSlotTimeMsec();
|
uint32_t slotTimeMsec = computeSlotTimeMsec();
|
||||||
uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
|
uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
|
||||||
uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast
|
static constexpr uint16_t preambleLengthDefault =
|
||||||
const uint32_t PROCESSING_TIME_MSEC =
|
16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
|
||||||
4500; // time to construct, process and construct a packet again (empirically determined)
|
static constexpr uint16_t wideLoraPreambleLengthDefault = 12; // 12 is default for wide Lora
|
||||||
const uint8_t CWmin = 3; // minimum CWsize
|
uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast
|
||||||
const uint8_t CWmax = 8; // maximum CWsize
|
static constexpr uint32_t PROCESSING_TIME_MSEC =
|
||||||
|
4500; // time to construct, process and construct a packet again (empirically determined)
|
||||||
|
static constexpr uint8_t CWmin = 3; // minimum CWsize
|
||||||
|
static constexpr uint8_t CWmax = 8; // maximum CWsize
|
||||||
|
|
||||||
meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending
|
meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending
|
||||||
uint32_t lastTxStart = 0L;
|
uint32_t lastTxStart = 0L;
|
||||||
@@ -127,7 +132,7 @@ class RadioInterface
|
|||||||
* Coerce LoRa config fields (bandwidth/spread_factor) derived from presets.
|
* 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.
|
* 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)
|
* 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 +239,20 @@ class RadioInterface
|
|||||||
// Whether we use the default frequency slot given our LoRa config (region and modem preset)
|
// Whether we use the default frequency slot given our LoRa config (region and modem preset)
|
||||||
static bool uses_default_frequency_slot;
|
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:
|
protected:
|
||||||
int8_t power = 17; // Set by applyModemConfig()
|
int8_t power = 17; // Set by applyModemConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RadioLibInterface::~RadioLibInterface()
|
||||||
|
{
|
||||||
|
// If the static `instance` pointer still references us, clear it.
|
||||||
|
// A later successful init() may have replaced `instance` with a newer
|
||||||
|
// interface — don't clobber that case.
|
||||||
|
if (instance == this) {
|
||||||
|
instance = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
// ESP32 doesn't use that flag
|
// ESP32 doesn't use that flag
|
||||||
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
||||||
@@ -109,7 +119,7 @@ bool RadioLibInterface::canSendImmediately()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag)
|
bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag)
|
||||||
{
|
{
|
||||||
bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag));
|
bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag));
|
||||||
// Handle false detections
|
// Handle false detections
|
||||||
|
|||||||
@@ -136,6 +136,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
|||||||
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||||
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
|
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the static `instance` pointer if it still points at us, so callers
|
||||||
|
* that check `RadioLibInterface::instance != nullptr` don't dereference a
|
||||||
|
* freed object after a failed init() + unique_ptr reset.
|
||||||
|
*/
|
||||||
|
virtual ~RadioLibInterface();
|
||||||
|
|
||||||
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
|
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,7 +220,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
|||||||
protected:
|
protected:
|
||||||
uint32_t activeReceiveStart = 0;
|
uint32_t activeReceiveStart = 0;
|
||||||
|
|
||||||
bool receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag);
|
bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag);
|
||||||
|
|
||||||
/** Do any hardware setup needed on entry into send configuration for the radio.
|
/** Do any hardware setup needed on entry into send configuration for the radio.
|
||||||
* Subclasses can customize, but must also call this base method */
|
* Subclasses can customize, but must also call this base method */
|
||||||
|
|||||||
+29
-9
@@ -11,6 +11,9 @@
|
|||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/RoutingModule.h"
|
#include "modules/RoutingModule.h"
|
||||||
|
#if HAS_TRAFFIC_MANAGEMENT
|
||||||
|
#include "modules/TrafficManagementModule.h"
|
||||||
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||||
#include "mqtt/MQTT.h"
|
#include "mqtt/MQTT.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -95,6 +98,20 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
|||||||
return true;
|
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
|
// For subsequent hops, check if previous relay is a favorite router
|
||||||
// Optimized search for favorite routers with matching last byte
|
// Optimized search for favorite routers with matching last byte
|
||||||
// Check ordering optimized for IoT devices (cheapest checks first)
|
// Check ordering optimized for IoT devices (cheapest checks first)
|
||||||
@@ -482,9 +499,9 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
meshtastic_Data decodedtmp;
|
meshtastic_Data decodedtmp;
|
||||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||||
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
|
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
|
||||||
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
|
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
|
||||||
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||||
LOG_ERROR("Invalid portnum (bad psk?)!");
|
LOG_DEBUG("Invalid portnum (bad psk?)");
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
|
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
|
||||||
LOG_WARN("Rejecting legacy DM");
|
LOG_WARN("Rejecting legacy DM");
|
||||||
@@ -719,13 +736,9 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
// Also, we should set the time from the ISR and it should have msec level resolution
|
// Also, we should set the time from the ISR and it should have msec level resolution
|
||||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||||
|
|
||||||
// Store a copy of the encrypted packet for MQTT.
|
// Store a copy of encrypted packet for MQTT
|
||||||
// Local, not a class member: handleReceived re-enters itself when a module
|
|
||||||
// reply broadcast goes through MeshService::sendToMesh -> Router::sendLocal,
|
|
||||||
// and a member would be silently overwritten without release on the inner
|
|
||||||
// call. Each invocation now owns its own copy (issue #9632, #10101, #8729).
|
|
||||||
DEBUG_HEAP_BEFORE;
|
DEBUG_HEAP_BEFORE;
|
||||||
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
|
p_encrypted = packetPool.allocCopy(*p);
|
||||||
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
|
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
|
||||||
|
|
||||||
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
||||||
@@ -819,7 +832,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
packetPool.release(p_encrypted); // Release the encrypted packet (release() handles nullptr)
|
packetPool.release(p_encrypted); // Release the encrypted packet
|
||||||
|
p_encrypted = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||||
@@ -861,6 +875,12 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldDropPacketForPreHop(*p)) {
|
||||||
|
logHopStartDrop(*p, "pre-hop drop");
|
||||||
|
packetPool.release(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldFilterReceived(p)) {
|
if (shouldFilterReceived(p)) {
|
||||||
LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from);
|
LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from);
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
|||||||
before us */
|
before us */
|
||||||
uint32_t rxDupe = 0, txRelayCanceled = 0;
|
uint32_t rxDupe = 0, txRelayCanceled = 0;
|
||||||
|
|
||||||
|
// pointer to the encrypted packet
|
||||||
|
meshtastic_MeshPacket *p_encrypted = nullptr;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend class RoutingModule;
|
friend class RoutingModule;
|
||||||
|
|
||||||
|
|||||||
@@ -245,14 +245,21 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
|
|||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||||
|
|
||||||
if (power > SX126X_MAX_POWER) // This chip has lower power limits than some
|
limitPower(SX126X_MAX_POWER);
|
||||||
power = SX126X_MAX_POWER;
|
// Make sure we reach the minimum power supported to turn the chip on (-9dBm)
|
||||||
|
if (power < -9)
|
||||||
|
power = -9;
|
||||||
|
|
||||||
err = lora.setOutputPower(power);
|
err = lora.setOutputPower(power);
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err);
|
LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err);
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
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
|
startReceive(); // restart receiving
|
||||||
|
|
||||||
return RADIOLIB_ERR_NONE;
|
return RADIOLIB_ERR_NONE;
|
||||||
|
|||||||
@@ -144,8 +144,7 @@ template <typename T> bool SX128xInterface<T>::reconfigure()
|
|||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||||
|
|
||||||
if (power > SX128X_MAX_POWER) // This chip has lower power limits than some
|
limitPower(SX128X_MAX_POWER);
|
||||||
power = SX128X_MAX_POWER;
|
|
||||||
|
|
||||||
err = lora.setOutputPower(power);
|
err = lora.setOutputPower(power);
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "TypeConversions.h"
|
#include "TypeConversions.h"
|
||||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
#include "meshUtils.h"
|
|
||||||
|
|
||||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
|
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
|
||||||
{
|
{
|
||||||
@@ -83,10 +82,8 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
|||||||
|
|
||||||
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
||||||
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
||||||
sanitizeUtf8(lite.long_name, sizeof(lite.long_name));
|
|
||||||
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
||||||
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
||||||
sanitizeUtf8(lite.short_name, sizeof(lite.short_name));
|
|
||||||
lite.hw_model = user.hw_model;
|
lite.hw_model = user.hw_model;
|
||||||
lite.role = user.role;
|
lite.role = user.role;
|
||||||
lite.is_licensed = user.is_licensed;
|
lite.is_licensed = user.is_licensed;
|
||||||
@@ -105,10 +102,8 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User
|
|||||||
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
||||||
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
||||||
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
||||||
sanitizeUtf8(user.long_name, sizeof(user.long_name));
|
|
||||||
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
||||||
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
||||||
sanitizeUtf8(user.short_name, sizeof(user.short_name));
|
|
||||||
user.hw_model = lite.hw_model;
|
user.hw_model = lite.hw_model;
|
||||||
user.role = lite.role;
|
user.role = lite.role;
|
||||||
user.is_licensed = lite.is_licensed;
|
user.is_licensed = lite.is_licensed;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ extern const pb_msgdesc_t meshtastic_ChannelSet_msg;
|
|||||||
|
|
||||||
/* Maximum encoded size of messages (where known) */
|
/* Maximum encoded size of messages (where known) */
|
||||||
#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size
|
#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size
|
||||||
#define meshtastic_ChannelSet_size 685
|
#define meshtastic_ChannelSet_size 682
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
} /* extern "C" */
|
} /* extern "C" */
|
||||||
|
|||||||
@@ -289,14 +289,7 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
|
|||||||
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
|
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
|
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
|
||||||
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
|
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28,
|
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28
|
||||||
/* EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
|
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_EU_866 = 29,
|
|
||||||
/* EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
|
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_EU_874 = 30,
|
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_EU_917 = 31,
|
|
||||||
/* EU 868MHz band, with narrow presets */
|
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32
|
|
||||||
} meshtastic_Config_LoRaConfig_RegionCode;
|
} meshtastic_Config_LoRaConfig_RegionCode;
|
||||||
|
|
||||||
/* Standard predefined channel settings
|
/* Standard predefined channel settings
|
||||||
@@ -326,24 +319,7 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
|
|||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8,
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8,
|
||||||
/* Long Range - Turbo
|
/* Long Range - Turbo
|
||||||
This preset performs similarly to LongFast, but with 500Khz bandwidth. */
|
This preset performs similarly to LongFast, but with 500Khz bandwidth. */
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9,
|
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9
|
||||||
/* Lite Fast
|
|
||||||
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
|
||||||
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC. */
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST = 10,
|
|
||||||
/* Lite Slow
|
|
||||||
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
|
||||||
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC. */
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW = 11,
|
|
||||||
/* Narrow Fast
|
|
||||||
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
|
||||||
Comparable link budget to SHORT_SLOW, but with half the data rate.
|
|
||||||
Intended to avoid interference with other devices. */
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST = 12,
|
|
||||||
/* Narrow Slow
|
|
||||||
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
|
||||||
Comparable link budget and data rate to LONG_FAST. */
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13
|
|
||||||
} meshtastic_Config_LoRaConfig_ModemPreset;
|
} meshtastic_Config_LoRaConfig_ModemPreset;
|
||||||
|
|
||||||
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
|
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
|
||||||
@@ -618,8 +594,6 @@ typedef struct _meshtastic_Config_LoRaConfig {
|
|||||||
bool config_ok_to_mqtt;
|
bool config_ok_to_mqtt;
|
||||||
/* Set where LORA FEM is enabled, disabled, or not present */
|
/* Set where LORA FEM is enabled, disabled, or not present */
|
||||||
meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode;
|
meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode;
|
||||||
/* Don't use radiolib to initialize the radio, instead listen for a serialHal connection */
|
|
||||||
bool serial_hal_only;
|
|
||||||
} meshtastic_Config_LoRaConfig;
|
} meshtastic_Config_LoRaConfig;
|
||||||
|
|
||||||
typedef struct _meshtastic_Config_BluetoothConfig {
|
typedef struct _meshtastic_Config_BluetoothConfig {
|
||||||
@@ -732,12 +706,12 @@ extern "C" {
|
|||||||
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
|
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
|
||||||
|
|
||||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
|
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
|
||||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_EU_N_868
|
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M
|
||||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868+1))
|
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M+1))
|
||||||
|
|
||||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
|
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
|
||||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW
|
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
|
||||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW+1))
|
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1))
|
||||||
|
|
||||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
|
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
|
||||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT
|
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT
|
||||||
@@ -781,7 +755,7 @@ extern "C" {
|
|||||||
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
|
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
|
||||||
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
|
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
|
||||||
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||||
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN}
|
||||||
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||||
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||||
#define meshtastic_Config_SessionkeyConfig_init_default {0}
|
#define meshtastic_Config_SessionkeyConfig_init_default {0}
|
||||||
@@ -792,7 +766,7 @@ extern "C" {
|
|||||||
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
|
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
|
||||||
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
|
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
|
||||||
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||||
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN}
|
||||||
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||||
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||||
#define meshtastic_Config_SessionkeyConfig_init_zero {0}
|
#define meshtastic_Config_SessionkeyConfig_init_zero {0}
|
||||||
@@ -879,7 +853,6 @@ extern "C" {
|
|||||||
#define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104
|
#define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104
|
||||||
#define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105
|
#define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105
|
||||||
#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106
|
#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106
|
||||||
#define meshtastic_Config_LoRaConfig_serial_hal_only_tag 107
|
|
||||||
#define meshtastic_Config_BluetoothConfig_enabled_tag 1
|
#define meshtastic_Config_BluetoothConfig_enabled_tag 1
|
||||||
#define meshtastic_Config_BluetoothConfig_mode_tag 2
|
#define meshtastic_Config_BluetoothConfig_mode_tag 2
|
||||||
#define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3
|
#define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3
|
||||||
@@ -1032,8 +1005,7 @@ X(a, STATIC, SINGULAR, BOOL, pa_fan_disabled, 15) \
|
|||||||
X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \
|
X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \
|
X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \
|
X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \
|
||||||
X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106) \
|
X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106)
|
||||||
X(a, STATIC, SINGULAR, BOOL, serial_hal_only, 107)
|
|
||||||
#define meshtastic_Config_LoRaConfig_CALLBACK NULL
|
#define meshtastic_Config_LoRaConfig_CALLBACK NULL
|
||||||
#define meshtastic_Config_LoRaConfig_DEFAULT NULL
|
#define meshtastic_Config_LoRaConfig_DEFAULT NULL
|
||||||
|
|
||||||
@@ -1090,7 +1062,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
|
|||||||
#define meshtastic_Config_BluetoothConfig_size 10
|
#define meshtastic_Config_BluetoothConfig_size 10
|
||||||
#define meshtastic_Config_DeviceConfig_size 100
|
#define meshtastic_Config_DeviceConfig_size 100
|
||||||
#define meshtastic_Config_DisplayConfig_size 36
|
#define meshtastic_Config_DisplayConfig_size 36
|
||||||
#define meshtastic_Config_LoRaConfig_size 91
|
#define meshtastic_Config_LoRaConfig_size 88
|
||||||
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
|
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
|
||||||
#define meshtastic_Config_NetworkConfig_size 204
|
#define meshtastic_Config_NetworkConfig_size 204
|
||||||
#define meshtastic_Config_PositionConfig_size 62
|
#define meshtastic_Config_PositionConfig_size 62
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
|||||||
/* Maximum encoded size of messages (where known) */
|
/* Maximum encoded size of messages (where known) */
|
||||||
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
||||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
||||||
#define meshtastic_BackupPreferences_size 2432
|
#define meshtastic_BackupPreferences_size 2429
|
||||||
#define meshtastic_ChannelFile_size 718
|
#define meshtastic_ChannelFile_size 718
|
||||||
#define meshtastic_DeviceState_size 1737
|
#define meshtastic_DeviceState_size 1737
|
||||||
#define meshtastic_NodeInfoLite_size 196
|
#define meshtastic_NodeInfoLite_size 196
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
|
|||||||
|
|
||||||
/* Maximum encoded size of messages (where known) */
|
/* Maximum encoded size of messages (where known) */
|
||||||
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
|
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
|
||||||
#define meshtastic_LocalConfig_size 757
|
#define meshtastic_LocalConfig_size 754
|
||||||
#define meshtastic_LocalModuleConfig_size 820
|
#define meshtastic_LocalModuleConfig_size 820
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -315,8 +315,6 @@ typedef enum _meshtastic_HardwareModel {
|
|||||||
meshtastic_HardwareModel_THINKNODE_M7 = 129,
|
meshtastic_HardwareModel_THINKNODE_M7 = 129,
|
||||||
meshtastic_HardwareModel_THINKNODE_M8 = 130,
|
meshtastic_HardwareModel_THINKNODE_M8 = 130,
|
||||||
meshtastic_HardwareModel_THINKNODE_M9 = 131,
|
meshtastic_HardwareModel_THINKNODE_M9 = 131,
|
||||||
/* The Heltec-V4-R8 uses an ESP32S3R8 chip, plus an SX1262. */
|
|
||||||
meshtastic_HardwareModel_HELTEC_V4_R8 = 132,
|
|
||||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
/* Automatically generated nanopb constant definitions */
|
|
||||||
/* Generated by nanopb-0.4.9.1 */
|
|
||||||
|
|
||||||
#include "meshtastic/serial_hal.pb.h"
|
|
||||||
#if PB_PROTO_HEADER_VERSION != 40
|
|
||||||
#error Regenerate this file with the current version of nanopb generator.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_SerialHalCommand, meshtastic_SerialHalCommand, 2)
|
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_SerialHalResponse, meshtastic_SerialHalResponse, 2)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/* Automatically generated nanopb header */
|
|
||||||
/* Generated by nanopb-0.4.9.1 */
|
|
||||||
|
|
||||||
#ifndef PB_MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_INCLUDED
|
|
||||||
#define PB_MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_INCLUDED
|
|
||||||
#include <pb.h>
|
|
||||||
|
|
||||||
#if PB_PROTO_HEADER_VERSION != 40
|
|
||||||
#error Regenerate this file with the current version of nanopb generator.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Enum definitions */
|
|
||||||
typedef enum _meshtastic_SerialHalCommand_Type {
|
|
||||||
meshtastic_SerialHalCommand_Type_UNSET = 0,
|
|
||||||
meshtastic_SerialHalCommand_Type_PIN_MODE = 1,
|
|
||||||
meshtastic_SerialHalCommand_Type_DIGITAL_WRITE = 2,
|
|
||||||
meshtastic_SerialHalCommand_Type_DIGITAL_READ = 3,
|
|
||||||
meshtastic_SerialHalCommand_Type_ATTACH_INTERRUPT = 4,
|
|
||||||
meshtastic_SerialHalCommand_Type_DETACH_INTERRUPT = 5,
|
|
||||||
meshtastic_SerialHalCommand_Type_SPI_TRANSFER = 6,
|
|
||||||
meshtastic_SerialHalCommand_Type_NOOP = 7
|
|
||||||
} meshtastic_SerialHalCommand_Type;
|
|
||||||
|
|
||||||
typedef enum _meshtastic_SerialHalResponse_Result {
|
|
||||||
meshtastic_SerialHalResponse_Result_OK = 0,
|
|
||||||
meshtastic_SerialHalResponse_Result_ERROR = 1,
|
|
||||||
meshtastic_SerialHalResponse_Result_BAD_REQUEST = 2,
|
|
||||||
meshtastic_SerialHalResponse_Result_UNSUPPORTED = 3
|
|
||||||
} meshtastic_SerialHalResponse_Result;
|
|
||||||
|
|
||||||
/* Struct definitions */
|
|
||||||
typedef PB_BYTES_ARRAY_T(512) meshtastic_SerialHalCommand_data_t;
|
|
||||||
typedef struct _meshtastic_SerialHalCommand {
|
|
||||||
/* Host-assigned request id. Replies echo this id back in
|
|
||||||
SerialHalResponse.transaction_id. */
|
|
||||||
uint32_t transaction_id;
|
|
||||||
meshtastic_SerialHalCommand_Type type;
|
|
||||||
uint32_t pin;
|
|
||||||
uint32_t value;
|
|
||||||
uint32_t mode;
|
|
||||||
meshtastic_SerialHalCommand_data_t data;
|
|
||||||
} meshtastic_SerialHalCommand;
|
|
||||||
|
|
||||||
typedef PB_BYTES_ARRAY_T(512) meshtastic_SerialHalResponse_data_t;
|
|
||||||
typedef struct _meshtastic_SerialHalResponse {
|
|
||||||
/* Matches the originating SerialHalCommand.transaction_id for normal
|
|
||||||
request/response traffic.
|
|
||||||
|
|
||||||
A value of 0 indicates an unsolicited interrupt notification generated by
|
|
||||||
the device. In that case, the host should interpret value as the GPIO pin
|
|
||||||
that triggered. */
|
|
||||||
uint32_t transaction_id;
|
|
||||||
meshtastic_SerialHalResponse_Result result;
|
|
||||||
/* Used by DIGITAL_READ replies and interrupt notifications. For interrupt
|
|
||||||
notifications (transaction_id == 0), this carries the pin number. */
|
|
||||||
uint32_t value;
|
|
||||||
meshtastic_SerialHalResponse_data_t data;
|
|
||||||
char error[80];
|
|
||||||
} meshtastic_SerialHalResponse;
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Helper constants for enums */
|
|
||||||
#define _meshtastic_SerialHalCommand_Type_MIN meshtastic_SerialHalCommand_Type_UNSET
|
|
||||||
#define _meshtastic_SerialHalCommand_Type_MAX meshtastic_SerialHalCommand_Type_NOOP
|
|
||||||
#define _meshtastic_SerialHalCommand_Type_ARRAYSIZE ((meshtastic_SerialHalCommand_Type)(meshtastic_SerialHalCommand_Type_NOOP+1))
|
|
||||||
|
|
||||||
#define _meshtastic_SerialHalResponse_Result_MIN meshtastic_SerialHalResponse_Result_OK
|
|
||||||
#define _meshtastic_SerialHalResponse_Result_MAX meshtastic_SerialHalResponse_Result_UNSUPPORTED
|
|
||||||
#define _meshtastic_SerialHalResponse_Result_ARRAYSIZE ((meshtastic_SerialHalResponse_Result)(meshtastic_SerialHalResponse_Result_UNSUPPORTED+1))
|
|
||||||
|
|
||||||
#define meshtastic_SerialHalCommand_type_ENUMTYPE meshtastic_SerialHalCommand_Type
|
|
||||||
|
|
||||||
#define meshtastic_SerialHalResponse_result_ENUMTYPE meshtastic_SerialHalResponse_Result
|
|
||||||
|
|
||||||
|
|
||||||
/* Initializer values for message structs */
|
|
||||||
#define meshtastic_SerialHalCommand_init_default {0, _meshtastic_SerialHalCommand_Type_MIN, 0, 0, 0, {0, {0}}}
|
|
||||||
#define meshtastic_SerialHalResponse_init_default {0, _meshtastic_SerialHalResponse_Result_MIN, 0, {0, {0}}, ""}
|
|
||||||
#define meshtastic_SerialHalCommand_init_zero {0, _meshtastic_SerialHalCommand_Type_MIN, 0, 0, 0, {0, {0}}}
|
|
||||||
#define meshtastic_SerialHalResponse_init_zero {0, _meshtastic_SerialHalResponse_Result_MIN, 0, {0, {0}}, ""}
|
|
||||||
|
|
||||||
/* Field tags (for use in manual encoding/decoding) */
|
|
||||||
#define meshtastic_SerialHalCommand_transaction_id_tag 1
|
|
||||||
#define meshtastic_SerialHalCommand_type_tag 2
|
|
||||||
#define meshtastic_SerialHalCommand_pin_tag 3
|
|
||||||
#define meshtastic_SerialHalCommand_value_tag 4
|
|
||||||
#define meshtastic_SerialHalCommand_mode_tag 5
|
|
||||||
#define meshtastic_SerialHalCommand_data_tag 6
|
|
||||||
#define meshtastic_SerialHalResponse_transaction_id_tag 1
|
|
||||||
#define meshtastic_SerialHalResponse_result_tag 2
|
|
||||||
#define meshtastic_SerialHalResponse_value_tag 3
|
|
||||||
#define meshtastic_SerialHalResponse_data_tag 4
|
|
||||||
#define meshtastic_SerialHalResponse_error_tag 5
|
|
||||||
|
|
||||||
/* Struct field encoding specification for nanopb */
|
|
||||||
#define meshtastic_SerialHalCommand_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, transaction_id, 1) \
|
|
||||||
X(a, STATIC, SINGULAR, UENUM, type, 2) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, pin, 3) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, value, 4) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, mode, 5) \
|
|
||||||
X(a, STATIC, SINGULAR, BYTES, data, 6)
|
|
||||||
#define meshtastic_SerialHalCommand_CALLBACK NULL
|
|
||||||
#define meshtastic_SerialHalCommand_DEFAULT NULL
|
|
||||||
|
|
||||||
#define meshtastic_SerialHalResponse_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, transaction_id, 1) \
|
|
||||||
X(a, STATIC, SINGULAR, UENUM, result, 2) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, value, 3) \
|
|
||||||
X(a, STATIC, SINGULAR, BYTES, data, 4) \
|
|
||||||
X(a, STATIC, SINGULAR, STRING, error, 5)
|
|
||||||
#define meshtastic_SerialHalResponse_CALLBACK NULL
|
|
||||||
#define meshtastic_SerialHalResponse_DEFAULT NULL
|
|
||||||
|
|
||||||
extern const pb_msgdesc_t meshtastic_SerialHalCommand_msg;
|
|
||||||
extern const pb_msgdesc_t meshtastic_SerialHalResponse_msg;
|
|
||||||
|
|
||||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
|
||||||
#define meshtastic_SerialHalCommand_fields &meshtastic_SerialHalCommand_msg
|
|
||||||
#define meshtastic_SerialHalResponse_fields &meshtastic_SerialHalResponse_msg
|
|
||||||
|
|
||||||
/* Maximum encoded size of messages (where known) */
|
|
||||||
#define MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_MAX_SIZE meshtastic_SerialHalResponse_size
|
|
||||||
#define meshtastic_SerialHalCommand_size 541
|
|
||||||
#define meshtastic_SerialHalResponse_size 610
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} /* extern "C" */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -69,6 +69,22 @@ static inline int get_max_num_nodes()
|
|||||||
/// Max number of channels allowed
|
/// Max number of channels allowed
|
||||||
#define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0]))
|
#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
|
/// helper function for encoding a record as a protobuf, any failures to encode are fatal and we will panic
|
||||||
/// returns the encoded packet size
|
/// 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);
|
size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct);
|
||||||
|
|||||||
@@ -118,92 +118,3 @@ size_t pb_string_length(const char *str, size_t max_len)
|
|||||||
}
|
}
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sanitizeUtf8(char *buf, size_t bufSize)
|
|
||||||
{
|
|
||||||
if (!buf || bufSize == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Ensure null-terminated within buffer; report if we had to enforce it
|
|
||||||
bool replaced = (buf[bufSize - 1] != '\0');
|
|
||||||
buf[bufSize - 1] = '\0';
|
|
||||||
|
|
||||||
size_t i = 0;
|
|
||||||
size_t len = strlen(buf);
|
|
||||||
|
|
||||||
while (i < len) {
|
|
||||||
uint8_t b = (uint8_t)buf[i];
|
|
||||||
|
|
||||||
// Determine expected sequence length from lead byte
|
|
||||||
size_t seqLen;
|
|
||||||
uint32_t minCodepoint;
|
|
||||||
if (b <= 0x7F) {
|
|
||||||
// ASCII — valid single byte
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
} else if ((b & 0xE0) == 0xC0) {
|
|
||||||
seqLen = 2;
|
|
||||||
minCodepoint = 0x80; // Reject overlong
|
|
||||||
} else if ((b & 0xF0) == 0xE0) {
|
|
||||||
seqLen = 3;
|
|
||||||
minCodepoint = 0x800;
|
|
||||||
} else if ((b & 0xF8) == 0xF0) {
|
|
||||||
seqLen = 4;
|
|
||||||
minCodepoint = 0x10000;
|
|
||||||
} else {
|
|
||||||
// Invalid lead byte (0x80-0xBF or 0xF8+)
|
|
||||||
buf[i] = '?';
|
|
||||||
replaced = true;
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we have enough bytes remaining
|
|
||||||
if (i + seqLen > len) {
|
|
||||||
// Truncated sequence at end of string — replace remaining bytes
|
|
||||||
for (size_t j = i; j < len; j++) {
|
|
||||||
buf[j] = '?';
|
|
||||||
}
|
|
||||||
replaced = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate continuation bytes (must be 10xxxxxx)
|
|
||||||
bool valid = true;
|
|
||||||
for (size_t j = 1; j < seqLen; j++) {
|
|
||||||
if (((uint8_t)buf[i + j] & 0xC0) != 0x80) {
|
|
||||||
valid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
// Decode codepoint to check for overlong encodings and surrogates
|
|
||||||
uint32_t cp = 0;
|
|
||||||
if (seqLen == 2)
|
|
||||||
cp = b & 0x1F;
|
|
||||||
else if (seqLen == 3)
|
|
||||||
cp = b & 0x0F;
|
|
||||||
else
|
|
||||||
cp = b & 0x07;
|
|
||||||
for (size_t j = 1; j < seqLen; j++)
|
|
||||||
cp = (cp << 6) | ((uint8_t)buf[i + j] & 0x3F);
|
|
||||||
|
|
||||||
if (cp < minCodepoint || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
|
|
||||||
// Overlong encoding, out of Unicode range, or surrogate half
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
i += seqLen;
|
|
||||||
} else {
|
|
||||||
// Replace only the lead byte; continuation bytes will be caught on next iteration
|
|
||||||
buf[i] = '?';
|
|
||||||
replaced = true;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return replaced;
|
|
||||||
}
|
|
||||||
+18
-4
@@ -11,6 +11,24 @@ template <class T> constexpr const T &clamp(const T &v, const T &lo, const T &hi
|
|||||||
return (v < lo) ? lo : (hi < v) ? hi : v;
|
return (v < lo) ? lo : (hi < v) ? hi : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the smallest power of 2 >= n (undefined for n > 2^31)
|
||||||
|
static inline uint32_t nextPowerOf2(uint32_t n)
|
||||||
|
{
|
||||||
|
if (n <= 1)
|
||||||
|
return 1;
|
||||||
|
#if defined(__GNUC__)
|
||||||
|
return 1U << (32 - __builtin_clz(n - 1));
|
||||||
|
#else
|
||||||
|
n--;
|
||||||
|
n |= n >> 1;
|
||||||
|
n |= n >> 2;
|
||||||
|
n |= n >> 4;
|
||||||
|
n |= n >> 8;
|
||||||
|
n |= n >> 16;
|
||||||
|
return n + 1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
#define IF_SCREEN(X) \
|
#define IF_SCREEN(X) \
|
||||||
if (screen) { \
|
if (screen) { \
|
||||||
@@ -38,10 +56,6 @@ const std::string vformat(const char *const zcFormat, ...);
|
|||||||
// Get actual string length for nanopb char array fields.
|
// Get actual string length for nanopb char array fields.
|
||||||
size_t pb_string_length(const char *str, size_t max_len);
|
size_t pb_string_length(const char *str, size_t max_len);
|
||||||
|
|
||||||
// Sanitize a fixed-size char buffer in-place by replacing invalid UTF-8 sequences with '?'.
|
|
||||||
// Ensures the result is null-terminated within bufSize. Returns true if any bytes were replaced.
|
|
||||||
bool sanitizeUtf8(char *buf, size_t bufSize);
|
|
||||||
|
|
||||||
/// Calculate 2^n without calling pow() - used for spreading factor and other calculations
|
/// Calculate 2^n without calling pow() - used for spreading factor and other calculations
|
||||||
inline uint32_t pow_of_2(uint32_t n)
|
inline uint32_t pow_of_2(uint32_t n)
|
||||||
{
|
{
|
||||||
|
|||||||
+124
-102
@@ -24,7 +24,9 @@
|
|||||||
|
|
||||||
#include "Default.h"
|
#include "Default.h"
|
||||||
#include "MeshRadio.h"
|
#include "MeshRadio.h"
|
||||||
|
#include "RadioInterface.h"
|
||||||
#include "TypeConversions.h"
|
#include "TypeConversions.h"
|
||||||
|
#include "mesh/RadioLibInterface.h"
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||||
#include "mqtt/MQTT.h"
|
#include "mqtt/MQTT.h"
|
||||||
@@ -197,10 +199,35 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
handleSetOwner(r->set_owner);
|
handleSetOwner(r->set_owner);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case meshtastic_AdminMessage_set_config_tag:
|
case meshtastic_AdminMessage_set_config_tag: {
|
||||||
LOG_DEBUG("Client set config");
|
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;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case meshtastic_AdminMessage_set_module_config_tag:
|
case meshtastic_AdminMessage_set_module_config_tag:
|
||||||
LOG_DEBUG("Client set module config");
|
LOG_DEBUG("Client set module config");
|
||||||
@@ -453,7 +480,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
|
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
|
||||||
#endif
|
#endif
|
||||||
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
|
#if defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
|
||||||
enterDfuMode();
|
enterDfuMode();
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
@@ -599,14 +626,10 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
|||||||
if (*o.long_name) {
|
if (*o.long_name) {
|
||||||
changed |= strcmp(owner.long_name, o.long_name);
|
changed |= strcmp(owner.long_name, o.long_name);
|
||||||
strncpy(owner.long_name, o.long_name, sizeof(owner.long_name));
|
strncpy(owner.long_name, o.long_name, sizeof(owner.long_name));
|
||||||
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
|
||||||
sanitizeUtf8(owner.long_name, sizeof(owner.long_name));
|
|
||||||
}
|
}
|
||||||
if (*o.short_name) {
|
if (*o.short_name) {
|
||||||
changed |= strcmp(owner.short_name, o.short_name);
|
changed |= strcmp(owner.short_name, o.short_name);
|
||||||
strncpy(owner.short_name, o.short_name, sizeof(owner.short_name));
|
strncpy(owner.short_name, o.short_name, sizeof(owner.short_name));
|
||||||
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
|
||||||
sanitizeUtf8(owner.short_name, sizeof(owner.short_name));
|
|
||||||
}
|
}
|
||||||
snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum());
|
snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum());
|
||||||
|
|
||||||
@@ -630,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 changes = SEGMENT_CONFIG;
|
||||||
auto existingRole = config.device.role;
|
auto existingRole = config.device.role;
|
||||||
@@ -774,18 +797,57 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
|||||||
validatedLora.spread_factor = LORA_SF_DEFAULT;
|
validatedLora.spread_factor = LORA_SF_DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no lora radio parameters change, don't need to reboot
|
// If we're setting a new region, check the region is valid and then init the region or discard the change
|
||||||
if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region &&
|
if (validatedLora.region != myRegion->code) {
|
||||||
oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth &&
|
// Region has changed so check whether it is valid for e.g. licensing conditions and if the lora config is valid
|
||||||
oldLoraConfig.spread_factor == validatedLora.spread_factor &&
|
if (RadioInterface::validateConfigRegion(validatedLora) && RadioInterface::validateConfigLora(validatedLora)) {
|
||||||
oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power &&
|
// If we're setting region for the first time, init the region and regenerate the keys
|
||||||
oldLoraConfig.frequency_offset == validatedLora.frequency_offset &&
|
if (isRegionUnset && validatedLora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||||
oldLoraConfig.override_frequency == validatedLora.override_frequency &&
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||||
oldLoraConfig.channel_num == validatedLora.channel_num &&
|
if (crypto) {
|
||||||
oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) {
|
crypto->ensurePkiKeys(config.security, owner);
|
||||||
requiresReboot = false;
|
}
|
||||||
|
#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 defined(ARCH_PORTDUINO)
|
||||||
// If running on portduino and using SimRadio, do not require reboot
|
// If running on portduino and using SimRadio, do not require reboot
|
||||||
if (SimRadio::instance) {
|
if (SimRadio::instance) {
|
||||||
@@ -801,63 +863,21 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
|||||||
digitalWrite(RF95_FAN_EN, HIGH ^ 0);
|
digitalWrite(RF95_FAN_EN, HIGH ^ 0);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
config.lora = validatedLora;
|
|
||||||
|
|
||||||
#if HAS_LORA_FEM
|
#if HAS_LORA_FEM
|
||||||
// Apply FEM LNA mode from config (only meaningful on hardware that supports it)
|
// 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()) {
|
if (loraFEMInterface.isLnaCanControl()) {
|
||||||
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED);
|
loraFEMInterface.setLNAEnable(validatedLora.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) {
|
} 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
|
// 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");
|
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
|
#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) {
|
config.lora = validatedLora; // Finally, return the validated config back to the main config
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case meshtastic_Config_bluetooth_tag:
|
case meshtastic_Config_bluetooth_tag:
|
||||||
@@ -905,10 +925,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
|||||||
}
|
}
|
||||||
if (requiresReboot && !hasOpenEditTransaction) {
|
if (requiresReboot && !hasOpenEditTransaction) {
|
||||||
disableBluetooth();
|
disableBluetooth();
|
||||||
}
|
} // end of switch case which_payload_variant
|
||||||
|
|
||||||
saveChanges(changes, requiresReboot);
|
saveChanges(changes, requiresReboot);
|
||||||
}
|
} // end of handleSetConfig
|
||||||
|
|
||||||
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||||
{
|
{
|
||||||
@@ -1014,6 +1034,11 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
|||||||
moduleConfig.statusmessage = c.payload_variant.statusmessage;
|
moduleConfig.statusmessage = c.payload_variant.statusmessage;
|
||||||
shouldReboot = false;
|
shouldReboot = false;
|
||||||
break;
|
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);
|
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
|
||||||
return true;
|
return true;
|
||||||
@@ -1128,78 +1153,85 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
|||||||
meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default;
|
meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default;
|
||||||
|
|
||||||
if (req.decoded.want_response) {
|
if (req.decoded.want_response) {
|
||||||
|
const char *configName = "?";
|
||||||
switch (configType) {
|
switch (configType) {
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag;
|
||||||
res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt;
|
res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_serial_tag;
|
||||||
res.get_module_config_response.payload_variant.serial = moduleConfig.serial;
|
res.get_module_config_response.payload_variant.serial = moduleConfig.serial;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag;
|
||||||
res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification;
|
res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag;
|
||||||
res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward;
|
res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_range_test_tag;
|
||||||
res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test;
|
res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag;
|
||||||
res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry;
|
res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag;
|
||||||
res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message;
|
res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_audio_tag;
|
||||||
res.get_module_config_response.payload_variant.audio = moduleConfig.audio;
|
res.get_module_config_response.payload_variant.audio = moduleConfig.audio;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag;
|
||||||
res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware;
|
res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag;
|
||||||
res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info;
|
res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag;
|
||||||
res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor;
|
res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag;
|
||||||
res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
|
res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||||
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
|
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.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
|
||||||
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
|
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
|
||||||
break;
|
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.
|
// 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
|
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
|
||||||
@@ -1382,33 +1414,23 @@ void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uic
|
|||||||
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
||||||
{
|
{
|
||||||
// Validate ham parameters before setting since this would bypass validation in the owner struct
|
// Validate ham parameters before setting since this would bypass validation in the owner struct
|
||||||
if (*p.call_sign) {
|
const char *fieldsToCheck[] = {p.call_sign, p.short_name};
|
||||||
const char *start = p.call_sign;
|
const char *fieldNames[] = {"call_sign", "short_name"};
|
||||||
// Skip all whitespace
|
for (int i = 0; i < 2; i++) {
|
||||||
while (*start && isspace((unsigned char)*start))
|
if (*fieldsToCheck[i]) {
|
||||||
start++;
|
const char *start = fieldsToCheck[i];
|
||||||
if (*start == '\0') {
|
while (*start && isspace((unsigned char)*start))
|
||||||
LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character");
|
start++;
|
||||||
return;
|
if (*start == '\0') {
|
||||||
}
|
LOG_WARN("Rejected ham %s: must contain at least 1 non-whitespace character", fieldNames[i]);
|
||||||
}
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set call sign and override lora limitations for licensed use
|
// Set call sign and override lora limitations for licensed use
|
||||||
strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name));
|
strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name));
|
||||||
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
|
||||||
sanitizeUtf8(owner.long_name, sizeof(owner.long_name));
|
|
||||||
strncpy(owner.short_name, p.short_name, sizeof(owner.short_name));
|
strncpy(owner.short_name, p.short_name, sizeof(owner.short_name));
|
||||||
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
|
||||||
sanitizeUtf8(owner.short_name, sizeof(owner.short_name));
|
|
||||||
owner.is_licensed = true;
|
owner.is_licensed = true;
|
||||||
config.lora.override_duty_cycle = true;
|
config.lora.override_duty_cycle = true;
|
||||||
config.lora.tx_power = p.tx_power;
|
config.lora.tx_power = p.tx_power;
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
|||||||
*/
|
*/
|
||||||
void handleSetOwner(const meshtastic_User &o);
|
void handleSetOwner(const meshtastic_User &o);
|
||||||
void handleSetChannel(const meshtastic_Channel &cc);
|
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);
|
bool handleSetModuleConfig(const meshtastic_ModuleConfig &c);
|
||||||
void handleSetChannel();
|
void handleSetChannel();
|
||||||
void handleSetHamMode(const meshtastic_HamParameters &req);
|
void handleSetHamMode(const meshtastic_HamParameters &req);
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
#include "modules/PowerStressModule.h"
|
#include "modules/PowerStressModule.h"
|
||||||
#endif
|
#endif
|
||||||
#include "modules/RoutingModule.h"
|
#include "modules/RoutingModule.h"
|
||||||
|
#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT
|
||||||
|
#include "modules/TrafficManagementModule.h"
|
||||||
|
#endif
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||||
#include "modules/TraceRouteModule.h"
|
#include "modules/TraceRouteModule.h"
|
||||||
@@ -120,6 +123,14 @@ void setupModules()
|
|||||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||||
new ReplyBotModule();
|
new ReplyBotModule();
|
||||||
#endif
|
#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
|
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||||
adminModule = new AdminModule();
|
adminModule = new AdminModule();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -492,15 +492,24 @@ void PositionModule::sendLostAndFoundText()
|
|||||||
{
|
{
|
||||||
meshtastic_MeshPacket *p = allocDataPacket();
|
meshtastic_MeshPacket *p = allocDataPacket();
|
||||||
p->to = NODENUM_BROADCAST;
|
p->to = NODENUM_BROADCAST;
|
||||||
char *message = new char[60];
|
char message[128];
|
||||||
sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7));
|
int written = snprintf(message, sizeof(message), "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7),
|
||||||
|
(lastGpsLongitude * 1e-7));
|
||||||
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||||
p->want_ack = false;
|
p->want_ack = false;
|
||||||
p->decoded.payload.size = strlen(message);
|
if (written < 0) {
|
||||||
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
// snprintf encoding error — send an empty payload rather than uninitialized bytes.
|
||||||
|
p->decoded.payload.size = 0;
|
||||||
|
} else {
|
||||||
|
// Clamp to buffer capacity (snprintf returns "would-have-written" which can exceed the buffer).
|
||||||
|
const size_t msg_len = std::min(static_cast<size_t>(written), sizeof(message) - 1);
|
||||||
|
p->decoded.payload.size = msg_len;
|
||||||
|
if (msg_len > 0) {
|
||||||
|
memcpy(p->decoded.payload.bytes, message, msg_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||||
delete[] message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision
|
// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision
|
||||||
|
|||||||
@@ -651,7 +651,7 @@ void SerialModule::processWXSerial()
|
|||||||
LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr),
|
LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr),
|
||||||
strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum);
|
strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum);
|
||||||
}
|
}
|
||||||
if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) {
|
if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis) && velCount > 0 && dirCount > 0) {
|
||||||
// calculate averages and send to the mesh
|
// calculate averages and send to the mesh
|
||||||
float velAvg = 1.0 * velSum / velCount;
|
float velAvg = 1.0 * velSum / velCount;
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,23 @@ int32_t StatusMessageModule::runOnce()
|
|||||||
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
{
|
{
|
||||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||||
meshtastic_StatusMessage incomingMessage;
|
meshtastic_StatusMessage incomingMessage = meshtastic_StatusMessage_init_zero;
|
||||||
|
|
||||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
|
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
|
||||||
&incomingMessage)) {
|
&incomingMessage)) {
|
||||||
|
|
||||||
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
||||||
|
|
||||||
|
RecentStatus entry;
|
||||||
|
entry.fromNodeId = mp.from;
|
||||||
|
entry.statusText = incomingMessage.status;
|
||||||
|
|
||||||
|
recentReceived.push_back(std::move(entry));
|
||||||
|
|
||||||
|
// Keep only last MAX_RECENT_STATUSMESSAGES
|
||||||
|
if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) {
|
||||||
|
recentReceived.erase(recentReceived.begin()); // drop oldest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ProcessMessage::CONTINUE;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
#include "SinglePortModule.h"
|
#include "SinglePortModule.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
||||||
{
|
{
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Constructor
|
/** Constructor
|
||||||
* name is for debugging output
|
* name is for debugging output
|
||||||
@@ -19,16 +20,28 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
|
|||||||
this->setInterval(1000 * 12 * 60 * 60);
|
this->setInterval(1000 * 12 * 60 * 60);
|
||||||
}
|
}
|
||||||
// TODO: If we have a string, set the initial delay (15 minutes maybe)
|
// TODO: If we have a string, set the initial delay (15 minutes maybe)
|
||||||
|
|
||||||
|
// Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES
|
||||||
|
recentReceived.reserve(MAX_RECENT_STATUSMESSAGES);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
|
struct RecentStatus {
|
||||||
|
uint32_t fromNodeId; // mp.from
|
||||||
|
std::string statusText; // incomingMessage.status
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/** Called to handle a particular incoming message
|
/** Called to handle a particular incoming message
|
||||||
*/
|
*/
|
||||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
|
||||||
|
std::vector<RecentStatus> recentReceived;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StatusMessageModule *statusMessageModule;
|
extern StatusMessageModule *statusMessageModule;
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
|||||||
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
||||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, p.payload.size);
|
||||||
|
|
||||||
this->packetHistoryTotalCount++;
|
this->packetHistoryTotalCount++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,18 +66,10 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
|
|||||||
#include "Sensor/MCP9808Sensor.h"
|
#include "Sensor/MCP9808Sensor.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<Adafruit_SHT31.h>)
|
|
||||||
#include "Sensor/SHT31Sensor.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if __has_include(<Adafruit_LPS2X.h>)
|
#if __has_include(<Adafruit_LPS2X.h>)
|
||||||
#include "Sensor/LPS22HBSensor.h"
|
#include "Sensor/LPS22HBSensor.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<Adafruit_SHTC3.h>)
|
|
||||||
#include "Sensor/SHTC3Sensor.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
|
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
|
||||||
#include "Sensor/RAK12035Sensor.h"
|
#include "Sensor/RAK12035Sensor.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -94,8 +86,8 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
|
|||||||
#include "Sensor/OPT3001Sensor.h"
|
#include "Sensor/OPT3001Sensor.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<Adafruit_SHT4x.h>)
|
#if __has_include(<SHTSensor.h>)
|
||||||
#include "Sensor/SHT4XSensor.h"
|
#include "Sensor/SHTXXSensor.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
||||||
@@ -155,6 +147,15 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
|||||||
}
|
}
|
||||||
LOG_INFO("Environment Telemetry adding I2C devices...");
|
LOG_INFO("Environment Telemetry adding I2C devices...");
|
||||||
|
|
||||||
|
/*
|
||||||
|
Uncomment the preferences below if you want to use the module
|
||||||
|
without having to configure it from the PythonAPI or WebUI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// moduleConfig.telemetry.environment_measurement_enabled = 1;
|
||||||
|
// moduleConfig.telemetry.environment_screen_enabled = 1;
|
||||||
|
// moduleConfig.telemetry.environment_update_interval = 15;
|
||||||
|
|
||||||
// order by priority of metrics/values (low top, high bottom)
|
// order by priority of metrics/values (low top, high bottom)
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
@@ -202,15 +203,9 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
|||||||
#if __has_include(<Adafruit_MCP9808.h>)
|
#if __has_include(<Adafruit_MCP9808.h>)
|
||||||
addSensor<MCP9808Sensor>(i2cScanner, ScanI2C::DeviceType::MCP9808);
|
addSensor<MCP9808Sensor>(i2cScanner, ScanI2C::DeviceType::MCP9808);
|
||||||
#endif
|
#endif
|
||||||
#if __has_include(<Adafruit_SHT31.h>)
|
|
||||||
addSensor<SHT31Sensor>(i2cScanner, ScanI2C::DeviceType::SHT31);
|
|
||||||
#endif
|
|
||||||
#if __has_include(<Adafruit_LPS2X.h>)
|
#if __has_include(<Adafruit_LPS2X.h>)
|
||||||
addSensor<LPS22HBSensor>(i2cScanner, ScanI2C::DeviceType::LPS22HB);
|
addSensor<LPS22HBSensor>(i2cScanner, ScanI2C::DeviceType::LPS22HB);
|
||||||
#endif
|
#endif
|
||||||
#if __has_include(<Adafruit_SHTC3.h>)
|
|
||||||
addSensor<SHTC3Sensor>(i2cScanner, ScanI2C::DeviceType::SHTC3);
|
|
||||||
#endif
|
|
||||||
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
|
#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1
|
||||||
addSensor<RAK12035Sensor>(i2cScanner, ScanI2C::DeviceType::RAK12035);
|
addSensor<RAK12035Sensor>(i2cScanner, ScanI2C::DeviceType::RAK12035);
|
||||||
#endif
|
#endif
|
||||||
@@ -223,13 +218,9 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
|||||||
#if __has_include(<ClosedCube_OPT3001.h>)
|
#if __has_include(<ClosedCube_OPT3001.h>)
|
||||||
addSensor<OPT3001Sensor>(i2cScanner, ScanI2C::DeviceType::OPT3001);
|
addSensor<OPT3001Sensor>(i2cScanner, ScanI2C::DeviceType::OPT3001);
|
||||||
#endif
|
#endif
|
||||||
#if __has_include(<Adafruit_SHT4x.h>)
|
|
||||||
addSensor<SHT4XSensor>(i2cScanner, ScanI2C::DeviceType::SHT4X);
|
|
||||||
#endif
|
|
||||||
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
||||||
addSensor<MLX90632Sensor>(i2cScanner, ScanI2C::DeviceType::MLX90632);
|
addSensor<MLX90632Sensor>(i2cScanner, ScanI2C::DeviceType::MLX90632);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<Adafruit_BMP3XX.h>)
|
#if __has_include(<Adafruit_BMP3XX.h>)
|
||||||
addSensor<BMP3XXSensor>(i2cScanner, ScanI2C::DeviceType::BMP_3XX);
|
addSensor<BMP3XXSensor>(i2cScanner, ScanI2C::DeviceType::BMP_3XX);
|
||||||
#endif
|
#endif
|
||||||
@@ -245,7 +236,10 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
|||||||
#if __has_include(<BH1750_WE.h>)
|
#if __has_include(<BH1750_WE.h>)
|
||||||
addSensor<BH1750Sensor>(i2cScanner, ScanI2C::DeviceType::BH1750);
|
addSensor<BH1750Sensor>(i2cScanner, ScanI2C::DeviceType::BH1750);
|
||||||
#endif
|
#endif
|
||||||
|
#if __has_include(<SHTSensor.h>)
|
||||||
|
// TODO Can we scan for multiple sensors connected on the same bus?
|
||||||
|
addSensor<SHTXXSensor>(i2cScanner, ScanI2C::DeviceType::SHTXX);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,14 +254,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t result = UINT32_MAX;
|
uint32_t result = UINT32_MAX;
|
||||||
/*
|
|
||||||
Uncomment the preferences below if you want to use the module
|
|
||||||
without having to configure it from the PythonAPI or WebUI.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// moduleConfig.telemetry.environment_measurement_enabled = 1;
|
|
||||||
// moduleConfig.telemetry.environment_screen_enabled = 1;
|
|
||||||
// moduleConfig.telemetry.environment_update_interval = 15;
|
|
||||||
|
|
||||||
if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled ||
|
if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled ||
|
||||||
ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) {
|
ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_SHT31.h>)
|
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
#include "SHT31Sensor.h"
|
|
||||||
#include "TelemetrySensor.h"
|
|
||||||
#include <Adafruit_SHT31.h>
|
|
||||||
|
|
||||||
SHT31Sensor::SHT31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT31, "SHT31") {}
|
|
||||||
|
|
||||||
bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
|
||||||
{
|
|
||||||
LOG_INFO("Init sensor: %s", sensorName);
|
|
||||||
sht31 = Adafruit_SHT31(bus);
|
|
||||||
status = sht31.begin(dev->address.address);
|
|
||||||
initI2CSensor();
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SHT31Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
|
||||||
{
|
|
||||||
measurement->variant.environment_metrics.has_temperature = true;
|
|
||||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
|
||||||
measurement->variant.environment_metrics.temperature = sht31.readTemperature();
|
|
||||||
measurement->variant.environment_metrics.relative_humidity = sht31.readHumidity();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_SHT31.h>)
|
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
#include "TelemetrySensor.h"
|
|
||||||
#include <Adafruit_SHT31.h>
|
|
||||||
|
|
||||||
class SHT31Sensor : public TelemetrySensor
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
Adafruit_SHT31 sht31;
|
|
||||||
|
|
||||||
public:
|
|
||||||
SHT31Sensor();
|
|
||||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
|
||||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_SHT4x.h>)
|
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
#include "SHT4XSensor.h"
|
|
||||||
#include "TelemetrySensor.h"
|
|
||||||
#include <Adafruit_SHT4x.h>
|
|
||||||
|
|
||||||
SHT4XSensor::SHT4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT4X, "SHT4X") {}
|
|
||||||
|
|
||||||
bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
|
||||||
{
|
|
||||||
LOG_INFO("Init sensor: %s", sensorName);
|
|
||||||
|
|
||||||
uint32_t serialNumber = 0;
|
|
||||||
|
|
||||||
status = sht4x.begin(bus);
|
|
||||||
if (!status) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
serialNumber = sht4x.readSerial();
|
|
||||||
if (serialNumber != 0) {
|
|
||||||
LOG_DEBUG("serialNumber : %x", serialNumber);
|
|
||||||
status = 1;
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("Error trying to execute readSerial(): ");
|
|
||||||
status = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
initI2CSensor();
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SHT4XSensor::getMetrics(meshtastic_Telemetry *measurement)
|
|
||||||
{
|
|
||||||
measurement->variant.environment_metrics.has_temperature = true;
|
|
||||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
|
||||||
|
|
||||||
sensors_event_t humidity, temp;
|
|
||||||
sht4x.getEvent(&humidity, &temp);
|
|
||||||
measurement->variant.environment_metrics.temperature = temp.temperature;
|
|
||||||
measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_SHT4x.h>)
|
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
#include "TelemetrySensor.h"
|
|
||||||
#include <Adafruit_SHT4x.h>
|
|
||||||
|
|
||||||
class SHT4XSensor : public TelemetrySensor
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
Adafruit_SHT4x sht4x = Adafruit_SHT4x();
|
|
||||||
|
|
||||||
public:
|
|
||||||
SHT4XSensor();
|
|
||||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
|
||||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_SHTC3.h>)
|
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
#include "SHTC3Sensor.h"
|
|
||||||
#include "TelemetrySensor.h"
|
|
||||||
#include <Adafruit_SHTC3.h>
|
|
||||||
|
|
||||||
SHTC3Sensor::SHTC3Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTC3, "SHTC3") {}
|
|
||||||
|
|
||||||
bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
|
||||||
{
|
|
||||||
LOG_INFO("Init sensor: %s", sensorName);
|
|
||||||
status = shtc3.begin(bus);
|
|
||||||
|
|
||||||
initI2CSensor();
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SHTC3Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
|
||||||
{
|
|
||||||
measurement->variant.environment_metrics.has_temperature = true;
|
|
||||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
|
||||||
|
|
||||||
sensors_event_t humidity, temp;
|
|
||||||
shtc3.getEvent(&humidity, &temp);
|
|
||||||
|
|
||||||
measurement->variant.environment_metrics.temperature = temp.temperature;
|
|
||||||
measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user