mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-15 07:51:19 +10:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5026bc5f84 | ||
|
|
30db82ea50 | ||
|
|
45ccb2ce1f | ||
|
|
7347091055 | ||
|
|
55f15076ca | ||
|
|
b6a5171357 | ||
|
|
5e76f637bb | ||
|
|
85f8179aaf | ||
|
|
439b87b860 | ||
|
|
d47301defc | ||
|
|
04b819a7b5 | ||
|
|
8e653122c7 | ||
|
|
5ea3d143da | ||
|
|
56c897e826 | ||
|
|
2cc13a1132 | ||
|
|
031f332ec1 | ||
|
|
48747ee43d | ||
|
|
7c27f4e2df | ||
|
|
92c0133ef9 | ||
|
|
6171ad8c14 | ||
|
|
d8b11f0b14 | ||
|
|
a710841723 | ||
|
|
fb1de111d7 | ||
|
|
2b5daf2438 | ||
|
|
9361b85f47 | ||
|
|
68383c8bd5 | ||
|
|
76dea77929 | ||
|
|
d7ba178bf1 | ||
|
|
5f836cdf3b | ||
|
|
a8a5315460 | ||
|
|
d8e4389da2 | ||
|
|
25febfdeee | ||
|
|
f5be09c123 | ||
|
|
23321c4588 | ||
|
|
e1f5043489 | ||
|
|
5d9a2564e4 | ||
|
|
84ce1ea147 | ||
|
|
eba74fa6e2 |
@@ -34,7 +34,6 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|||||||
libxcb-xkb-dev \
|
libxcb-xkb-dev \
|
||||||
libxkbcommon-dev \
|
libxkbcommon-dev \
|
||||||
libinput-dev \
|
libinput-dev \
|
||||||
libnotify-dev \
|
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN pipx install platformio
|
RUN pipx install platformio
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: mui
|
id: mui
|
||||||
attributes:
|
attributes:
|
||||||
label: Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?
|
label: Is this bug report about any UI (https://meshtastic.org/docs/configuration/device-uis/) component firmware?
|
||||||
options:
|
options:
|
||||||
- label: Meshtastic UI aka MUI colorTFT
|
- label: Meshtastic UI aka MUI
|
||||||
- label: InkHUD ePaper
|
- label: InkHUD
|
||||||
- label: OLED slide UI on any display
|
- label: BaseUI
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ runs:
|
|||||||
- name: Install libs needed for native build
|
- name: Install libs needed for native build
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev libnotify-dev
|
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||||
|
|||||||
@@ -4,14 +4,9 @@ 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
|
||||||
@@ -20,18 +15,32 @@ on:
|
|||||||
- rp2040
|
- rp2040
|
||||||
- rp2350
|
- rp2350
|
||||||
- stm32
|
- stm32
|
||||||
description: Choose an arch to limit the search, or 'all' to search all architectures.
|
target:
|
||||||
default: all
|
type: string
|
||||||
|
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:
|
||||||
- all
|
- esp32
|
||||||
|
- 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
|
||||||
@@ -42,37 +51,14 @@ 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)
|
||||||
if [ "$BUILDTARGET" = "" ]; then
|
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
|
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
|
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY
|
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
|
echo "Targets:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "## 🎯 The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY
|
echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $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 != '' }}
|
||||||
@@ -92,12 +78,12 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
|
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
|
||||||
needs: [version, find-targets]
|
needs: [version]
|
||||||
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: ${{ needs.find-targets.outputs.arch }}
|
platform: ${{ inputs.arch }}
|
||||||
|
|
||||||
gather-artifacts:
|
gather-artifacts:
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
+3
-3
@@ -9,9 +9,9 @@ plugins:
|
|||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.524
|
- checkov@3.2.524
|
||||||
- renovate@43.132.1
|
- renovate@43.141.0
|
||||||
- prettier@3.8.3
|
- prettier@3.8.3
|
||||||
- trufflehog@3.94.3
|
- 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
|
||||||
@@ -19,7 +19,7 @@ lint:
|
|||||||
- ruff@0.15.11
|
- ruff@0.15.11
|
||||||
- isort@8.0.1
|
- isort@8.0.1
|
||||||
- markdownlint@0.48.0
|
- markdownlint@0.48.0
|
||||||
- oxipng@10.1.0
|
- oxipng@10.1.1
|
||||||
- svgo@4.0.1
|
- svgo@4.0.1
|
||||||
- actionlint@1.7.12
|
- actionlint@1.7.12
|
||||||
- flake8@7.3.0
|
- flake8@7.3.0
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ 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 \
|
||||||
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 libnotify-dev \
|
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& pip install --no-cache-dir -U platformio \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
&& mkdir /tmp/firmware
|
&& mkdir /tmp/firmware
|
||||||
@@ -53,7 +53,7 @@ USER root
|
|||||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||||
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 libnotify4 \
|
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
|
|||||||
g++ git ca-certificates pkg-config \
|
g++ git ca-certificates pkg-config \
|
||||||
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 libnotify-dev \
|
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& pip install --no-cache-dir platformio==6.1.19 \
|
&& pip install --no-cache-dir platformio==6.1.19 \
|
||||||
&& useradd --create-home --shell /usr/sbin/nologin meshtastic
|
&& useradd --create-home --shell /usr/sbin/nologin meshtastic
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ 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 \
|
||||||
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 libnotify-dev \
|
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& pip install --no-cache-dir -U platformio \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
&& mkdir /tmp/firmware
|
&& mkdir /tmp/firmware
|
||||||
@@ -42,7 +42,7 @@ USER root
|
|||||||
|
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
||||||
i2c-tools libuv libx11 libinput libxkbcommon sdl2 libnotify \
|
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
Executable
+118
@@ -0,0 +1,118 @@
|
|||||||
|
#!/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
|
||||||
Vendored
+1
-2
@@ -27,8 +27,7 @@ Build-Depends: debhelper-compat (= 13),
|
|||||||
libinput-dev,
|
libinput-dev,
|
||||||
libxkbcommon-x11-dev,
|
libxkbcommon-x11-dev,
|
||||||
libsqlite3-dev,
|
libsqlite3-dev,
|
||||||
libsdl2-dev,
|
libsdl2-dev
|
||||||
libnotify-dev
|
|
||||||
Standards-Version: 4.6.2
|
Standards-Version: 4.6.2
|
||||||
Homepage: https://github.com/meshtastic/firmware
|
Homepage: https://github.com/meshtastic/firmware
|
||||||
Rules-Requires-Root: no
|
Rules-Requires-Root: no
|
||||||
|
|||||||
Vendored
-2
@@ -7,5 +7,3 @@ bin/meshtasticd.service lib/systemd/system
|
|||||||
bin/meshtasticd-start.sh usr/bin
|
bin/meshtasticd-start.sh usr/bin
|
||||||
|
|
||||||
web/* usr/share/meshtasticd/web
|
web/* usr/share/meshtasticd/web
|
||||||
|
|
||||||
bin/org.meshtastic.meshtasticd.svg usr/share/icons/hicolor/scalable/apps
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ BuildRequires: pkgconfig(x11)
|
|||||||
BuildRequires: pkgconfig(libinput)
|
BuildRequires: pkgconfig(libinput)
|
||||||
BuildRequires: pkgconfig(xkbcommon-x11)
|
BuildRequires: pkgconfig(xkbcommon-x11)
|
||||||
BuildRequires: pkgconfig(sdl2)
|
BuildRequires: pkgconfig(sdl2)
|
||||||
BuildRequires: pkgconfig(libnotify)
|
|
||||||
|
|
||||||
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
|
||||||
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
|
||||||
@@ -113,10 +112,6 @@ cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web
|
|||||||
# Install default SSL storage directory (for web)
|
# Install default SSL storage directory (for web)
|
||||||
mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl
|
mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl
|
||||||
|
|
||||||
# Install desktop icon
|
|
||||||
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
|
|
||||||
install -m 0644 bin/org.meshtastic.meshtasticd.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/org.meshtastic.meshtasticd.svg
|
|
||||||
|
|
||||||
%pre
|
%pre
|
||||||
# create spi group (for udev rules)
|
# create spi group (for udev rules)
|
||||||
getent group spi > /dev/null || groupadd -r spi
|
getent group spi > /dev/null || groupadd -r spi
|
||||||
@@ -176,7 +171,6 @@ fi
|
|||||||
%dir %{_datadir}/meshtasticd/web
|
%dir %{_datadir}/meshtasticd/web
|
||||||
%{_datadir}/meshtasticd/web/*
|
%{_datadir}/meshtasticd/web/*
|
||||||
%dir %{_sysconfdir}/meshtasticd/ssl
|
%dir %{_sysconfdir}/meshtasticd/ssl
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/org.meshtastic.meshtasticd.svg
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
%autochangelog
|
%autochangelog
|
||||||
|
|||||||
+3
-3
@@ -29,6 +29,7 @@ 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
|
||||||
@@ -56,6 +57,7 @@ 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
|
||||||
@@ -66,7 +68,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/21e484f409cde18d44012caef84c244eb5ca28f3.zip
|
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/6bfd1f135e1ebe37afd6050bb4b9964cea3fcfda.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
|
||||||
@@ -225,8 +227,6 @@ 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: 4d5b500df5...249a80855a
@@ -4,8 +4,7 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
|||||||
bool usePreset)
|
bool usePreset)
|
||||||
{
|
{
|
||||||
|
|
||||||
// If use_preset is false, always return "Custom" — callers such as RadioInterface and Channels
|
// If use_preset is false, always return "Custom"
|
||||||
// rely on this being a stable literal for channel-name hashing and default-channel detection.
|
|
||||||
if (!usePreset) {
|
if (!usePreset) {
|
||||||
return "Custom";
|
return "Custom";
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-64
@@ -40,22 +40,6 @@
|
|||||||
#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"
|
||||||
@@ -344,17 +328,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
float scaled = 0;
|
float scaled = 0;
|
||||||
|
|
||||||
battery_adcEnable();
|
battery_adcEnable();
|
||||||
#ifdef ARCH_STM32WL
|
#ifdef ARCH_ESP32 // ADC block for espressif platforms
|
||||||
// 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
|
||||||
@@ -552,11 +530,6 @@ 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)
|
||||||
|
|
||||||
@@ -666,9 +639,7 @@ bool Power::analogInit()
|
|||||||
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ARCH_STM32WL
|
#ifdef ARCH_ESP32 // ESP32 needs special analog stuff
|
||||||
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;
|
||||||
@@ -678,7 +649,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
|
||||||
@@ -708,7 +679,7 @@ bool Power::analogInit()
|
|||||||
|
|
||||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||||
|
|
||||||
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32WL)
|
#ifndef ARCH_ESP32
|
||||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -746,37 +717,17 @@ bool Power::setup()
|
|||||||
found = true;
|
found = true;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#ifdef EXT_PWR_DETECT
|
attachPowerInterrupts();
|
||||||
attachInterrupt(
|
|
||||||
EXT_PWR_DETECT,
|
|
||||||
[]() {
|
|
||||||
power->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
},
|
|
||||||
CHANGE);
|
|
||||||
#endif
|
|
||||||
#ifdef BATTERY_CHARGING_INV
|
|
||||||
attachInterrupt(
|
|
||||||
BATTERY_CHARGING_INV,
|
|
||||||
[]() {
|
|
||||||
power->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
},
|
|
||||||
CHANGE);
|
|
||||||
#endif
|
|
||||||
#ifdef EXT_CHRG_DETECT
|
|
||||||
attachInterrupt(
|
|
||||||
EXT_CHRG_DETECT,
|
|
||||||
[]() {
|
|
||||||
power->setIntervalFromNow(0);
|
|
||||||
runASAP = true;
|
|
||||||
BaseType_t higherWake = 0;
|
|
||||||
},
|
|
||||||
CHANGE);
|
|
||||||
#endif
|
|
||||||
enabled = found;
|
enabled = found;
|
||||||
low_voltage_counter = 0;
|
low_voltage_counter = 0;
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
// Register callbacks for before and after lightsleep
|
||||||
|
// Used to detach and reattach interrupts
|
||||||
|
lsObserver.observe(¬ifyLightSleep);
|
||||||
|
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||||
|
#endif
|
||||||
|
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,6 +1006,97 @@ int32_t Power::runOnce()
|
|||||||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
|
||||||
|
// Detach our class' interrupts before lightsleep
|
||||||
|
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||||
|
int Power::beforeLightSleep(void *unused)
|
||||||
|
{
|
||||||
|
LOG_WARN("Detaching power interrupts for sleep");
|
||||||
|
detachPowerInterrupts();
|
||||||
|
return 0; // Indicates success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconfigure our interrupts
|
||||||
|
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||||
|
int Power::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||||
|
{
|
||||||
|
attachPowerInterrupts();
|
||||||
|
return 0; // Indicates success
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Attach (or re-attach) hardware interrupts for power management
|
||||||
|
* Public method. Used outside class when waking from MCU sleep
|
||||||
|
*/
|
||||||
|
void Power::attachPowerInterrupts()
|
||||||
|
{
|
||||||
|
#ifdef EXT_PWR_DETECT
|
||||||
|
attachInterrupt(
|
||||||
|
EXT_PWR_DETECT,
|
||||||
|
[]() {
|
||||||
|
power->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
},
|
||||||
|
CHANGE);
|
||||||
|
#endif
|
||||||
|
#ifdef BATTERY_CHARGING_INV
|
||||||
|
attachInterrupt(
|
||||||
|
BATTERY_CHARGING_INV,
|
||||||
|
[]() {
|
||||||
|
power->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
},
|
||||||
|
CHANGE);
|
||||||
|
#endif
|
||||||
|
#ifdef EXT_CHRG_DETECT
|
||||||
|
attachInterrupt(
|
||||||
|
EXT_CHRG_DETECT,
|
||||||
|
[]() {
|
||||||
|
power->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
},
|
||||||
|
CHANGE);
|
||||||
|
#endif
|
||||||
|
#ifdef PMU_IRQ
|
||||||
|
if (PMU) {
|
||||||
|
attachInterrupt(
|
||||||
|
PMU_IRQ,
|
||||||
|
[] {
|
||||||
|
pmu_irq = true;
|
||||||
|
power->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
},
|
||||||
|
FALLING);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Detach the "normal" button interrupts.
|
||||||
|
* Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep
|
||||||
|
*/
|
||||||
|
void Power::detachPowerInterrupts()
|
||||||
|
{
|
||||||
|
#ifdef EXT_PWR_DETECT
|
||||||
|
detachInterrupt(EXT_PWR_DETECT);
|
||||||
|
#endif
|
||||||
|
#ifdef BATTERY_CHARGING_INV
|
||||||
|
detachInterrupt(BATTERY_CHARGING_INV);
|
||||||
|
#endif
|
||||||
|
#ifdef EXT_CHRG_DETECT
|
||||||
|
detachInterrupt(EXT_CHRG_DETECT);
|
||||||
|
#endif
|
||||||
|
#ifdef PMU_IRQ
|
||||||
|
if (PMU) {
|
||||||
|
detachInterrupt(PMU_IRQ);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init the power manager chip
|
* Init the power manager chip
|
||||||
*
|
*
|
||||||
@@ -1332,8 +1374,6 @@ bool Power::axpChipInit()
|
|||||||
}
|
}
|
||||||
|
|
||||||
pinMode(PMU_IRQ, INPUT);
|
pinMode(PMU_IRQ, INPUT);
|
||||||
attachInterrupt(
|
|
||||||
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
|
||||||
|
|
||||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
||||||
// because it occurs repeatedly while there is no battery also it could cause
|
// because it occurs repeatedly while there is no battery also it could cause
|
||||||
|
|||||||
@@ -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.%03u ", hour, min, sec, millis() / 1000, millis() % 1000);
|
::printf("| %02d:%02d:%02d %u ", hour, min, sec, 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.%03u ", millis() / 1000, millis() % 1000);
|
::printf("| ??:??:?? %u ", millis() / 1000);
|
||||||
#else
|
#else
|
||||||
printf("%s ", logLevel);
|
printf("%s ", logLevel);
|
||||||
if (color) {
|
if (color) {
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ 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)
|
||||||
|
|||||||
+3
-9
@@ -78,11 +78,6 @@ 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
|
||||||
@@ -231,7 +226,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 // same as SHT2X
|
#define INA_ADDR 0x40
|
||||||
#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
|
||||||
@@ -244,8 +239,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 SHTXX_ADDR 0x44
|
#define SHT31_4x_ADDR 0x44
|
||||||
#define SHTXX_ADDR_ALT 0x45
|
#define SHT31_4x_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
|
||||||
@@ -515,7 +510,6 @@ 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,9 +136,7 @@ bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint1
|
|||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
/// for SEN5X detection
|
||||||
// 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
|
||||||
@@ -176,46 +174,6 @@ 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: \
|
||||||
@@ -413,7 +371,7 @@ 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: // Same as SHT2X
|
case INA_ADDR:
|
||||||
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);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||||
@@ -429,12 +387,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
logFoundDevice("INA260", (uint8_t)addr.address);
|
logFoundDevice("INA260", (uint8_t)addr.address);
|
||||||
type = INA260;
|
type = INA260;
|
||||||
}
|
}
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
} else { // Assume INA219 if INA260 ID is not found
|
||||||
} else if (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;
|
||||||
}
|
}
|
||||||
@@ -495,19 +448,22 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SHTXX_ADDR: // same as OPT3001_ADDR_ALT
|
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
||||||
case SHTXX_ADDR_ALT: // same as OPT3001_ADDR
|
case SHT31_4x_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 { // SHTXX
|
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
|
||||||
type = SHTXX;
|
type = SHT4X;
|
||||||
logFoundDevice("SHTXX", (uint8_t)addr.address);
|
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||||
|
} else {
|
||||||
|
type = SHT31;
|
||||||
|
logFoundDevice("SHT31", (uint8_t)addr.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTXX, "SHTXX", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (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);
|
||||||
@@ -743,7 +699,6 @@ 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")) {
|
||||||
@@ -759,7 +714,6 @@ 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,14 +103,6 @@ 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,20 +70,25 @@ 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
|
||||||
else if (elapsedSearchMs() > maxSearchMs)
|
if (elapsed > maxSearchMs)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Otherwise, not too long yet!
|
// Otherwise, not too long yet!
|
||||||
else
|
return false;
|
||||||
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
|
||||||
|
|||||||
+7
-99
@@ -60,7 +60,6 @@ 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"
|
||||||
@@ -99,7 +98,6 @@ 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
|
||||||
@@ -137,60 +135,6 @@ 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
|
||||||
// ==============================
|
// ==============================
|
||||||
@@ -328,25 +272,10 @@ 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 = -1.0f;
|
static float b;
|
||||||
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) {
|
||||||
// Need at least two position points before we can infer heading.
|
// just prepare for next time
|
||||||
oldLat = lat;
|
oldLat = lat;
|
||||||
oldLon = lon;
|
oldLon = lon;
|
||||||
|
|
||||||
@@ -354,20 +283,12 @@ 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, keep previous heading (invalid until first real movement)
|
if (d < 10) // haven't moved enough, just keep current bearing
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -1002,22 +923,9 @@ 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.
|
||||||
|
|
||||||
uint32_t desiredFramerate = IDLE_FRAMERATE;
|
if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) {
|
||||||
#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 = desiredFramerate;
|
targetFramerate = IDLE_FRAMERATE;
|
||||||
|
|
||||||
ui->setTargetFPS(targetFramerate);
|
ui->setTargetFPS(targetFramerate);
|
||||||
forceDisplay();
|
forceDisplay();
|
||||||
@@ -1289,7 +1197,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::drawFavoriteNode);
|
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,7 +1226,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 drawFavoriteNode to pick a new node (because our list just changed)
|
prevFrame = -1; // Force drawNodeInfo 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,11 +330,15 @@ 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(float heading);
|
void setHeading(long _heading)
|
||||||
|
{
|
||||||
|
hasCompass = true;
|
||||||
|
compassHeading = fmod(_heading, 360);
|
||||||
|
}
|
||||||
|
|
||||||
bool hasHeading() { return hasCompass; }
|
bool hasHeading() { return hasCompass; }
|
||||||
|
|
||||||
float getHeading() { return compassHeading; }
|
long 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; }
|
||||||
@@ -788,4 +792,4 @@ extern std::vector<std::string> functionSymbol;
|
|||||||
extern std::string functionSymbolString;
|
extern std::string functionSymbolString;
|
||||||
extern graphics::Screen *screen;
|
extern graphics::Screen *screen;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -1254,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;
|
|
||||||
|
|
||||||
// Step 3a: copy rest of the pixels in this row into the pixel line buffer,
|
// Quickly write out the first changed pixel (saves another array lookup)
|
||||||
// while also recording the last pixel in the row that needs updating.
|
linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
|
||||||
// Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set.
|
x_LastPixelUpdate = x_FirstPixelUpdate;
|
||||||
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;
|
||||||
|
|
||||||
@@ -1274,14 +1274,6 @@ 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,6 +1,10 @@
|
|||||||
#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>
|
||||||
@@ -17,8 +21,8 @@ struct Point {
|
|||||||
|
|
||||||
void rotate(float angle)
|
void rotate(float angle)
|
||||||
{
|
{
|
||||||
float cos_a = cosf(angle);
|
float cos_a = cos(angle);
|
||||||
float sin_a = sinf(angle);
|
float sin_a = sin(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;
|
||||||
@@ -47,30 +51,21 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
|||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
radius += 4;
|
radius += 4;
|
||||||
}
|
}
|
||||||
float northX = 0.0f;
|
Point north(0, -radius);
|
||||||
float northY = -radius;
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
|
north.rotate(-myHeading);
|
||||||
const float c = cosf(-myHeading);
|
north.translate(compassX, compassY);
|
||||||
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(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||||
} else {
|
} else {
|
||||||
display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||||
}
|
}
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->drawString(northX, northY - 3, "N");
|
display->drawString(north.x, north.y - 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)
|
||||||
@@ -118,46 +113,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
float estimatedHeading(double lat, double lon)
|
||||||
{
|
{
|
||||||
headingRadian = 0.0f;
|
// Simple magnetic declination estimation
|
||||||
|
// This is a very basic implementation - the original might be more sophisticated
|
||||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING)
|
return 0.0f; // Return 0 for now, indicating no heading available
|
||||||
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)
|
||||||
@@ -177,4 +137,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
|||||||
|
|
||||||
} // namespace CompassRenderer
|
} // namespace CompassRenderer
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
#endif
|
#endif
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#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>
|
||||||
|
|
||||||
@@ -24,9 +25,7 @@ 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
|
||||||
bool getHeadingRadians(double lat, double lon, float &headingRadian);
|
float estimatedHeading(double lat, double lon);
|
||||||
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,16 +408,7 @@ 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 ===
|
||||||
// For custom modem settings show the actual parameters; for presets use the preset name.
|
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
||||||
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;
|
||||||
@@ -425,7 +416,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, modeStr);
|
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textWidth = display->getStringWidth(regionradiopreset);
|
textWidth = display->getStringWidth(regionradiopreset);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
#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"
|
||||||
@@ -26,7 +25,6 @@
|
|||||||
#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>
|
||||||
|
|
||||||
@@ -161,22 +159,31 @@ 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 (crypto) {
|
if (!owner.is_licensed) {
|
||||||
crypto->ensurePkiKeys(config.security, owner);
|
bool keygenSuccess = false;
|
||||||
|
if (config.security.private_key.size == 32) {
|
||||||
|
// public key is derived from private, so this will always have the same result.
|
||||||
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
LOG_INFO("Generate new PKI keys");
|
||||||
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
if (keygenSuccess) {
|
||||||
|
config.security.public_key.size = 32;
|
||||||
|
config.security.private_key.size = 32;
|
||||||
|
owner.public_key.size = 32;
|
||||||
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
config.lora.tx_enabled = true;
|
config.lora.tx_enabled = true;
|
||||||
@@ -192,6 +199,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
}
|
}
|
||||||
|
|
||||||
service->reloadConfig(changes);
|
service->reloadConfig(changes);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
bannerOptions.durationMs = duration;
|
bannerOptions.durationMs = duration;
|
||||||
@@ -257,24 +265,13 @@ 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) {
|
||||||
// Match RadioInterface::applyModemConfig(): include padding, add spacing in numerator, and use round()
|
numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0)));
|
||||||
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;
|
||||||
@@ -310,6 +307,7 @@ 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);
|
||||||
@@ -348,6 +346,7 @@ 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,9 +3,6 @@
|
|||||||
#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
|
||||||
@@ -95,41 +92,8 @@ 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) {
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||||
// 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.
|
||||||
@@ -409,13 +373,14 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *distanceLabel = (strlen(distStr) > 0) ? distStr : "?";
|
if (strlen(distStr) > 0) {
|
||||||
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(distanceLabel);
|
int textWidth = display->getStringWidth(distStr);
|
||||||
display->drawString(rightEdge - textWidth, y, distanceLabel);
|
display->drawString(rightEdge - textWidth, y, distStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -466,8 +431,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||||
float myHeadingRadian, double userLat, double userLon)
|
double userLat, double userLon)
|
||||||
{
|
{
|
||||||
if (!nodeDB->hasValidPosition(node))
|
if (!nodeDB->hasValidPosition(node))
|
||||||
return;
|
return;
|
||||||
@@ -481,11 +446,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 relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
float bearingToNode = RAD_TO_DEG * bearing;
|
||||||
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
||||||
// 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, relativeBearingDeg);
|
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing);
|
||||||
/*
|
/*
|
||||||
float angle = relativeBearing * DEG_TO_RAD;
|
float angle = relativeBearing * DEG_TO_RAD;
|
||||||
float halfSize = size / 2.0;
|
float halfSize = size / 2.0;
|
||||||
@@ -515,27 +480,12 @@ 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 headingRadian, double lat, double lon)
|
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, 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;
|
||||||
@@ -620,7 +570,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, headingRadian, lat, lon);
|
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||||
|
|
||||||
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||||
yOffset += rowYOffset;
|
yOffset += rowYOffset;
|
||||||
@@ -815,13 +765,9 @@ 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 headingRadian = 0.0f;
|
float heading = 0;
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -833,12 +779,21 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
|||||||
lastSwitchTime = now;
|
lastSwitchTime = now;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!CompassRenderer::getHeadingRadians(lat, lon, headingRadian)) {
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, lat, lon);
|
#if HAS_GPS
|
||||||
return;
|
if (screen->hasHeading()) {
|
||||||
}
|
heading = screen->getHeading(); // degrees
|
||||||
|
validHeading = true;
|
||||||
|
} else {
|
||||||
|
heading = screen->estimatedHeading(lat, lon);
|
||||||
|
validHeading = !isnan(heading);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon);
|
if (!validHeading)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, 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 headingRadian = 0, double lat = 0,
|
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 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,
|
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||||
float myHeadingRadian, double userLat, double userLon);
|
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);
|
||||||
|
|||||||
+103
-184
@@ -5,9 +5,6 @@
|
|||||||
#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"
|
||||||
@@ -41,15 +38,6 @@ 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();
|
||||||
@@ -302,7 +290,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::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
if (favoritedNodes.empty())
|
if (favoritedNodes.empty())
|
||||||
return;
|
return;
|
||||||
@@ -354,57 +342,6 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
|
||||||
// === 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 and Hops (combined on one line, if available) ===
|
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||||
char signalHopsStr[32] = "";
|
char signalHopsStr[32] = "";
|
||||||
bool haveSignal = false;
|
bool haveSignal = false;
|
||||||
@@ -701,54 +638,51 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
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) {
|
||||||
if (showCompass || statusLine1) {
|
bool showCompass = false;
|
||||||
|
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);
|
||||||
if (showCompass) {
|
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||||
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
||||||
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
|
||||||
if (showCompass || statusLine1) {
|
bool showCompass = false;
|
||||||
|
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;
|
||||||
@@ -759,8 +693,8 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
#else
|
#else
|
||||||
const int navBarHeight = 0;
|
const int navBarHeight = 0;
|
||||||
#endif
|
#endif
|
||||||
// --------- END PATCH FOR EINK NAV BAR -----------
|
|
||||||
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||||
|
// --------- END PATCH FOR EINK NAV BAR -----------
|
||||||
|
|
||||||
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
||||||
return;
|
return;
|
||||||
@@ -774,13 +708,25 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
int compassX = x + SCREEN_WIDTH / 2;
|
int compassX = x + SCREEN_WIDTH / 2;
|
||||||
int compassY = yBelowContent + availableHeight / 2;
|
int compassY = yBelowContent + availableHeight / 2;
|
||||||
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
const auto &op = ourNode->position;
|
||||||
if (showCompass) {
|
float myHeading = 0;
|
||||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
||||||
} else {
|
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
// else show nothing
|
// else show nothing
|
||||||
}
|
}
|
||||||
@@ -1216,7 +1162,6 @@ 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
|
||||||
@@ -1231,12 +1176,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, textPos[line]);
|
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
||||||
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
|
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
|
||||||
display->drawString(x + 11 + xOffset, textPos[line++], displayLine);
|
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||||
} else {
|
} else {
|
||||||
// Onboard GPS
|
// Onboard GPS
|
||||||
UIRenderer::drawGps(display, 0, textPos[line++], gpsStatus);
|
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.display.heading_bold = origBold;
|
config.display.heading_bold = origBold;
|
||||||
@@ -1245,36 +1190,18 @@ 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()));
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
// === Determine Compass Heading ===
|
||||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
float heading = 0;
|
||||||
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;
|
||||||
const char *statusLine1 = nullptr;
|
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||||
const char *statusLine2 = nullptr;
|
validHeading = true;
|
||||||
if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) {
|
} else {
|
||||||
double headingLat = 0.0;
|
if (screen->hasHeading()) {
|
||||||
double headingLon = 0.0;
|
heading = radians(screen->getHeading());
|
||||||
if (hasLiveGpsFix) {
|
validHeading = true;
|
||||||
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 {
|
||||||
statusLine1 = "No";
|
heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
|
||||||
statusLine2 = "Fix";
|
validHeading = !isnan(heading);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1292,18 +1219,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, textPos[line++], uptimeStr);
|
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
|
||||||
} else {
|
} else {
|
||||||
display->drawString(0, textPos[line++], "Last: ?");
|
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Third Row: Line 1 GPS Info ===
|
// === Third Row: Line 1 GPS Info ===
|
||||||
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line1");
|
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[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, textPos[line++], gpsStatus, "line2");
|
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Final Row: Altitude ===
|
// === Final Row: Altitude ===
|
||||||
@@ -1314,14 +1241,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, textPos[line++], altitudeLine);
|
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
|
||||||
}
|
}
|
||||||
#if !defined(M5STACK_UNITC6L)
|
#if !defined(M5STACK_UNITC6L)
|
||||||
// === Draw Compass ===
|
// === Draw Compass if heading is valid ===
|
||||||
if (validHeading || statusLine1) {
|
if (validHeading) {
|
||||||
// --- 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 = textPos[1];
|
const int16_t topY = getTextPositions(display)[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;
|
||||||
|
|
||||||
@@ -1334,33 +1261,29 @@ 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 = textPos[5] + FONT_HEIGHT_SMALL + 2;
|
int yBelowContent = getTextPositions(display)[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)
|
||||||
@@ -1381,29 +1304,25 @@ 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 drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawNodeInfo(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);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
NicheGraphics parallel E-Ink driver for the LilyGo T5-S3-ePaper-Pro (ED047TC1).
|
||||||
|
|
||||||
|
InkHUD buffer format : 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||||
|
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||||
|
|
||||||
|
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
|
||||||
|
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
|
||||||
|
960×540 FastEPD buffer so content clears the panel's inactive edge border.
|
||||||
|
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
|
||||||
|
// #define EINK_EDGE_LINES
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
#ifdef T5_S3_EPAPER_PRO
|
||||||
|
|
||||||
|
#include "./ED047TC1.h"
|
||||||
|
|
||||||
|
#include "FastEPD.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics::Drivers;
|
||||||
|
|
||||||
|
void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||||
|
{
|
||||||
|
// Parallel display — SPI parameters are not used
|
||||||
|
(void)spi;
|
||||||
|
(void)pin_dc;
|
||||||
|
(void)pin_cs;
|
||||||
|
(void)pin_busy;
|
||||||
|
(void)pin_rst;
|
||||||
|
|
||||||
|
epaper = new FASTEPD;
|
||||||
|
|
||||||
|
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||||
|
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||||
|
#elif defined(T5_S3_EPAPER_PRO_V2)
|
||||||
|
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
|
||||||
|
// Initialize all PCA9535 port-0 pins as outputs / HIGH
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
epaper->ioPinMode(i, OUTPUT);
|
||||||
|
epaper->ioWrite(i, HIGH);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
epaper->setMode(BB_MODE_1BPP);
|
||||||
|
epaper->clearWhite();
|
||||||
|
epaper->fullUpdate(true); // Blocking initial clear
|
||||||
|
}
|
||||||
|
|
||||||
|
void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||||
|
{
|
||||||
|
if (!epaper)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// InkHUD renders into a DISPLAY_WIDTH × DISPLAY_HEIGHT safe-area buffer.
|
||||||
|
// We need to place that into the centre of the physical 960×540 FastEPD buffer,
|
||||||
|
// leaving blank margins at every edge to avoid the panel's inactive border.
|
||||||
|
const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (118)
|
||||||
|
const uint32_t dstRowBytes = (960 + 7) / 8; // bytes per row in physical buffer (120)
|
||||||
|
const uint32_t dstTotalRows = 540;
|
||||||
|
|
||||||
|
uint8_t *cur = epaper->currentBuffer();
|
||||||
|
|
||||||
|
// Fill physical buffer with white (0xFF = white in FastEPD 1bpp)
|
||||||
|
memset(cur, 0xFF, dstRowBytes * dstTotalRows);
|
||||||
|
|
||||||
|
// Copy each InkHUD row into the physical buffer with horizontal + vertical offsets
|
||||||
|
for (uint32_t row = 0; row < DISPLAY_HEIGHT; row++) {
|
||||||
|
const uint8_t *srcRow = imageData + row * srcRowBytes;
|
||||||
|
uint8_t *dstRow = cur + (row + V_OFFSET_TOP) * dstRowBytes + H_OFFSET_BYTES;
|
||||||
|
memcpy(dstRow, srcRow, srcRowBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef EINK_EDGE_LINES
|
||||||
|
// Draw a 1px black box at the exact boundary of the safe area within the
|
||||||
|
// physical buffer. If the margins are correct, all 4 lines should be
|
||||||
|
// fully visible and right at the edge of the usable display area.
|
||||||
|
|
||||||
|
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
|
||||||
|
|
||||||
|
const uint32_t safeX = H_OFFSET_BYTES * 8;
|
||||||
|
const uint32_t safeY = V_OFFSET_TOP;
|
||||||
|
const uint32_t safeW = DISPLAY_WIDTH;
|
||||||
|
const uint32_t safeH = DISPLAY_HEIGHT;
|
||||||
|
|
||||||
|
// Top edge: horizontal line at safeY
|
||||||
|
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||||
|
setPixelBlack(col, safeY);
|
||||||
|
|
||||||
|
// Bottom edge: horizontal line at safeY + safeH - 1
|
||||||
|
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||||
|
setPixelBlack(col, safeY + safeH - 1);
|
||||||
|
|
||||||
|
// Left edge: vertical line at safeX
|
||||||
|
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||||
|
setPixelBlack(safeX, row);
|
||||||
|
|
||||||
|
// Right edge: vertical line at safeX + safeW - 1
|
||||||
|
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||||
|
setPixelBlack(safeX + safeW - 1, row);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (type == FULL) {
|
||||||
|
epaper->fullUpdate(CLEAR_SLOW, false);
|
||||||
|
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
|
||||||
|
} else {
|
||||||
|
// FAST: true partial update — compares pCurrent vs pPrevious and only applies
|
||||||
|
// the update waveform to rows that actually changed. Unchanged rows get a neutral
|
||||||
|
// signal (no visible effect). partialUpdate() updates pPrevious internally.
|
||||||
|
epaper->partialUpdate(false, 0, dstTotalRows - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // T5_S3_EPAPER_PRO
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
E-Ink display driver adapter
|
||||||
|
- ED047TC1 (via FastEPD library)
|
||||||
|
- Manufacturer: E Ink / used in LilyGo T5-E-Paper-S3-Pro
|
||||||
|
- Size: 4.7 inch
|
||||||
|
- Physical resolution: 960px x 540px
|
||||||
|
- Interface: 8-bit parallel (NOT SPI)
|
||||||
|
|
||||||
|
Unlike the other NicheGraphics EInk drivers, this one drives a parallel e-paper
|
||||||
|
panel via the FastEPD library. SPI parameters passed to begin() are ignored.
|
||||||
|
|
||||||
|
The ED047TC1 panel has an inactive pixel border on all four edges (~4–8 physical
|
||||||
|
pixels). DISPLAY_WIDTH / DISPLAY_HEIGHT expose a reduced "safe area" to InkHUD so
|
||||||
|
that content is never drawn into this dead zone. The update() method copies the
|
||||||
|
InkHUD frame buffer into the centre of the larger physical 960×540 buffer, using
|
||||||
|
H_OFFSET_BYTES (horizontal, whole bytes = 8 pixels per byte),
|
||||||
|
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
|
||||||
|
|
||||||
|
Changing these constants shifts content inward from each physical edge:
|
||||||
|
H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944)
|
||||||
|
V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom)
|
||||||
|
V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "./EInk.h"
|
||||||
|
|
||||||
|
// Forward declare to avoid pulling FastEPD into all translation units
|
||||||
|
class FASTEPD;
|
||||||
|
|
||||||
|
namespace NicheGraphics::Drivers
|
||||||
|
{
|
||||||
|
|
||||||
|
class ED047TC1 : public EInk
|
||||||
|
{
|
||||||
|
// Safe-area dimensions exposed to InkHUD (physical panel is 960×540).
|
||||||
|
//
|
||||||
|
// The ED047TC1 has an inactive pixel border on all physical edges.
|
||||||
|
// The physical buffer coordinates do NOT directly match the visual orientation
|
||||||
|
// due to FastEPD's portrait scan direction and InkHUD's rotation=3 (270° CW):
|
||||||
|
//
|
||||||
|
// Physical buffer Visual on device (rotation=3)
|
||||||
|
// ───────────────── ──────────────────────────────
|
||||||
|
// Physical LEFT cols → Visual TOP edge
|
||||||
|
// Physical RIGHT cols → Visual BOTTOM edge
|
||||||
|
// Physical TOP rows → Visual RIGHT edge
|
||||||
|
// Physical BOTTOM rows → Visual LEFT edge
|
||||||
|
//
|
||||||
|
// Offset constants shift the InkHUD safe-area away from each physical dead zone:
|
||||||
|
// H_OFFSET_BYTES : whole bytes from physical left (8px per byte, affects visual TOP)
|
||||||
|
// Physical right margin = 960 − H_OFFSET_BYTES×8 − DISPLAY_WIDTH (affects visual BOTTOM)
|
||||||
|
// V_OFFSET_TOP : pixel rows from physical top (affects visual RIGHT)
|
||||||
|
// V_OFFSET_BOTTOM: pixel rows from physical bottom (affects visual LEFT)
|
||||||
|
//
|
||||||
|
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
|
||||||
|
|
||||||
|
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px)
|
||||||
|
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px)
|
||||||
|
|
||||||
|
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
|
||||||
|
// visual BOTTOM: 960−8−944=8px physical right margin
|
||||||
|
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
|
||||||
|
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
|
||||||
|
|
||||||
|
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
|
||||||
|
|
||||||
|
public:
|
||||||
|
ED047TC1() : EInk(DISPLAY_WIDTH, DISPLAY_HEIGHT, supported) {}
|
||||||
|
|
||||||
|
// EInk interface — SPI params are not used for this parallel display
|
||||||
|
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override;
|
||||||
|
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool isUpdateDone() override { return true; } // FastEPD updates are blocking
|
||||||
|
|
||||||
|
private:
|
||||||
|
FASTEPD *epaper = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace NicheGraphics::Drivers
|
||||||
|
|
||||||
|
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -88,8 +88,12 @@ class AppletFont
|
|||||||
|
|
||||||
// Greek
|
// Greek
|
||||||
#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h"
|
#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h"
|
||||||
|
#include "graphics/niche/Fonts/FreeSans18pt_Win1253.h"
|
||||||
|
#include "graphics/niche/Fonts/FreeSans24pt_Win1253.h"
|
||||||
#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h"
|
#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h"
|
||||||
#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h"
|
#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h"
|
||||||
|
#define FREESANS_24PT_WIN1253 InkHUD::AppletFont(FreeSans24pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -5, 3)
|
||||||
|
#define FREESANS_18PT_WIN1253 InkHUD::AppletFont(FreeSans18pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -4, 2)
|
||||||
#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1)
|
#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1)
|
||||||
#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1)
|
#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1)
|
||||||
#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2)
|
#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2)
|
||||||
|
|||||||
@@ -177,8 +177,24 @@ 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 (crypto) {
|
if (!owner.is_licensed) {
|
||||||
crypto->ensurePkiKeys(config.security, owner);
|
bool keygenSuccess = false;
|
||||||
|
|
||||||
|
if (config.security.private_key.size == 32) {
|
||||||
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keygenSuccess) {
|
||||||
|
config.security.public_key.size = 32;
|
||||||
|
config.security.private_key.size = 32;
|
||||||
|
owner.public_key.size = 32;
|
||||||
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,10 @@ int32_t ButtonThread::runOnce()
|
|||||||
leadUpSequenceActive = false;
|
leadUpSequenceActive = false;
|
||||||
resetLeadUpSequence();
|
resetLeadUpSequence();
|
||||||
}
|
}
|
||||||
|
#ifdef INPUT_DEBUG
|
||||||
|
if (buttonCurrentlyPressed)
|
||||||
|
LOG_WARN("Button held for %u ms", millis() - buttonPressStartTime);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Progressive lead-up sound system
|
// Progressive lead-up sound system
|
||||||
if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) {
|
if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) {
|
||||||
@@ -311,7 +315,8 @@ int32_t ButtonThread::runOnce()
|
|||||||
void ButtonThread::attachButtonInterrupts()
|
void ButtonThread::attachButtonInterrupts()
|
||||||
{
|
{
|
||||||
// Interrupt for user button, during normal use. Improves responsiveness.
|
// Interrupt for user button, during normal use. Improves responsiveness.
|
||||||
attachInterrupt(_pinNum, _intRoutine, CHANGE);
|
if (_intRoutine != nullptr)
|
||||||
|
attachInterrupt(_pinNum, _intRoutine, CHANGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -320,7 +325,8 @@ void ButtonThread::attachButtonInterrupts()
|
|||||||
*/
|
*/
|
||||||
void ButtonThread::detachButtonInterrupts()
|
void ButtonThread::detachButtonInterrupts()
|
||||||
{
|
{
|
||||||
detachInterrupt(_pinNum);
|
if (_intRoutine != nullptr)
|
||||||
|
detachInterrupt(_pinNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ void TouchScreenImpl1::init()
|
|||||||
return;
|
return;
|
||||||
#else
|
#else
|
||||||
TouchScreenBase::init(true);
|
TouchScreenBase::init(true);
|
||||||
inputBroker->registerSource(this);
|
if (inputBroker)
|
||||||
|
inputBroker->registerSource(this);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-129
@@ -340,138 +340,9 @@ void setup()
|
|||||||
|
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
pinMode(BLE_LED, OUTPUT);
|
pinMode(BLE_LED, OUTPUT);
|
||||||
#ifdef BLE_LED_INVERTED
|
|
||||||
digitalWrite(BLE_LED, HIGH);
|
|
||||||
#else
|
|
||||||
digitalWrite(BLE_LED, LOW);
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(T_DECK)
|
|
||||||
// GPIO10 manages all peripheral power supplies
|
|
||||||
// Turn on peripheral power immediately after MUC starts.
|
|
||||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
|
||||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
|
||||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
|
||||||
pinMode(KB_POWERON, OUTPUT);
|
|
||||||
digitalWrite(KB_POWERON, HIGH);
|
|
||||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
|
||||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
|
||||||
// e.g. when detecting the SD card
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(TFT_CS, OUTPUT);
|
|
||||||
digitalWrite(TFT_CS, HIGH);
|
|
||||||
delay(100);
|
|
||||||
#elif defined(T_DECK_PRO)
|
|
||||||
pinMode(LORA_EN, OUTPUT);
|
|
||||||
digitalWrite(LORA_EN, HIGH);
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(PIN_EINK_CS, OUTPUT);
|
|
||||||
digitalWrite(PIN_EINK_CS, HIGH);
|
|
||||||
#if PIN_EINK_RES >= 0
|
|
||||||
pinMode(PIN_EINK_RES, OUTPUT);
|
|
||||||
digitalWrite(PIN_EINK_RES, HIGH);
|
|
||||||
#endif
|
|
||||||
pinMode(CST328_PIN_RST, OUTPUT);
|
|
||||||
digitalWrite(CST328_PIN_RST, HIGH);
|
|
||||||
#elif defined(T_LORA_PAGER)
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(TFT_CS, OUTPUT);
|
|
||||||
digitalWrite(TFT_CS, HIGH);
|
|
||||||
pinMode(KB_INT, INPUT_PULLUP);
|
|
||||||
// io expander
|
|
||||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
|
||||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
|
||||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
|
||||||
#elif defined(HACKADAY_COMMUNICATOR)
|
|
||||||
pinMode(KB_INT, INPUT);
|
|
||||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(T_DECK)
|
|
||||||
// GPIO10 manages all peripheral power supplies
|
|
||||||
// Turn on peripheral power immediately after MUC starts.
|
|
||||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
|
||||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
|
||||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
|
||||||
pinMode(KB_POWERON, OUTPUT);
|
|
||||||
digitalWrite(KB_POWERON, HIGH);
|
|
||||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
|
||||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
|
||||||
// e.g. when detecting the SD card
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(TFT_CS, OUTPUT);
|
|
||||||
digitalWrite(TFT_CS, HIGH);
|
|
||||||
delay(100);
|
|
||||||
#elif defined(T_DECK_PRO)
|
|
||||||
pinMode(LORA_EN, OUTPUT);
|
|
||||||
digitalWrite(LORA_EN, HIGH);
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(PIN_EINK_CS, OUTPUT);
|
|
||||||
digitalWrite(PIN_EINK_CS, HIGH);
|
|
||||||
#if PIN_EINK_RES >= 0
|
|
||||||
pinMode(PIN_EINK_RES, OUTPUT);
|
|
||||||
digitalWrite(PIN_EINK_RES, HIGH);
|
|
||||||
#endif
|
|
||||||
pinMode(CST328_PIN_RST, OUTPUT);
|
|
||||||
digitalWrite(CST328_PIN_RST, HIGH);
|
|
||||||
#elif defined(T_LORA_PAGER)
|
|
||||||
pinMode(LORA_CS, OUTPUT);
|
|
||||||
digitalWrite(LORA_CS, HIGH);
|
|
||||||
pinMode(SDCARD_CS, OUTPUT);
|
|
||||||
digitalWrite(SDCARD_CS, HIGH);
|
|
||||||
pinMode(TFT_CS, OUTPUT);
|
|
||||||
digitalWrite(TFT_CS, HIGH);
|
|
||||||
pinMode(KB_INT, INPUT_PULLUP);
|
|
||||||
// io expander
|
|
||||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
|
||||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
|
||||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
|
||||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
|
||||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
|
||||||
#elif defined(HACKADAY_COMMUNICATOR)
|
|
||||||
pinMode(KB_INT, INPUT);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
concurrency::hasBeenSetup = true;
|
concurrency::hasBeenSetup = true;
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
meshtastic_Config_DisplayConfig_OledType screen_model =
|
meshtastic_Config_DisplayConfig_OledType screen_model =
|
||||||
|
|||||||
+5
-23
@@ -10,26 +10,8 @@
|
|||||||
#include "memGet.h"
|
#include "memGet.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
|
|
||||||
#include <malloc.h>
|
|
||||||
#include <unistd.h> // sbrk
|
|
||||||
|
|
||||||
#ifdef ARCH_STM32WL
|
#ifdef ARCH_STM32WL
|
||||||
// Returns the uncommitted sbrk headroom: addressable space between the current heap
|
#include <malloc.h>
|
||||||
// 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;
|
||||||
@@ -46,9 +28,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(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
#elif defined(ARCH_STM32WL)
|
||||||
struct mallinfo m = mallinfo();
|
struct mallinfo m = mallinfo();
|
||||||
return m.fordblks + sbrkHeadroom(); // Free space within arena + uncommitted sbrk headroom
|
return m.fordblks; // Total free space (bytes)
|
||||||
#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;
|
||||||
@@ -67,9 +49,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(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
#elif defined(ARCH_STM32WL)
|
||||||
struct mallinfo m = mallinfo();
|
struct mallinfo m = mallinfo();
|
||||||
return m.arena + sbrkHeadroom(); // Non-mmapped space allocated + uncommitted sbrk headroom
|
return m.arena; // Non-mmapped space allocated (bytes)
|
||||||
#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,33 +71,6 @@ 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,7 +36,6 @@ 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);
|
||||||
|
|||||||
@@ -30,11 +30,6 @@
|
|||||||
#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
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ 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) {
|
||||||
// Intentionally silent: this path runs during portduinoSetup() before the
|
#ifndef PIO_UNIT_TESTING
|
||||||
// console/SerialConsole is initialized, so LOG_* here would dereference a null pointer.
|
LOG_ERROR("No radio instance available to provide entropy");
|
||||||
|
#endif
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ template <typename T> bool LR11x0Interface<T>::init()
|
|||||||
|
|
||||||
RadioLibInterface::init();
|
RadioLibInterface::init();
|
||||||
|
|
||||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
limitPower(LR1110_MAX_POWER);
|
||||||
limitPower(LR1120_MAX_POWER);
|
|
||||||
} else {
|
if ((power > LR1120_MAX_POWER) &&
|
||||||
limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range
|
(config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if 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
|
||||||
@@ -175,12 +177,6 @@ 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);
|
||||||
|
|
||||||
@@ -188,14 +184,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;
|
||||||
|
|||||||
+3
-37
@@ -6,52 +6,18 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "detect/LoRaRadioType.h"
|
#include "detect/LoRaRadioType.h"
|
||||||
|
|
||||||
// Sentinel marking the end of a modem preset array
|
|
||||||
static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
|
||||||
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
|
|
||||||
|
|
||||||
// Region profile: bundles the preset list with regulatory parameters shared across regions
|
|
||||||
struct RegionProfile {
|
|
||||||
const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default
|
|
||||||
float spacing; // gaps between radio channels
|
|
||||||
float padding; // padding at each side of the "operating channel"
|
|
||||||
bool audioPermitted;
|
|
||||||
bool licensedOnly; // a region profile for licensed operators only
|
|
||||||
int8_t textThrottle; // throttle for text - future expansion
|
|
||||||
int8_t positionThrottle; // throttle for location data - future expansion
|
|
||||||
int8_t telemetryThrottle; // throttle for telemetry - future expansion
|
|
||||||
uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place
|
|
||||||
};
|
|
||||||
|
|
||||||
extern const RegionProfile PROFILE_STD;
|
|
||||||
extern const RegionProfile PROFILE_EU868;
|
|
||||||
extern const RegionProfile PROFILE_UNDEF;
|
|
||||||
// extern const RegionProfile PROFILE_LITE;
|
|
||||||
// extern const RegionProfile PROFILE_NARROW;
|
|
||||||
// extern const RegionProfile PROFILE_HAM;
|
|
||||||
|
|
||||||
// Map from old region names to new region enums
|
// Map from old region names to new region enums
|
||||||
struct RegionInfo {
|
struct RegionInfo {
|
||||||
meshtastic_Config_LoRaConfig_RegionCode code;
|
meshtastic_Config_LoRaConfig_RegionCode code;
|
||||||
float freqStart;
|
float freqStart;
|
||||||
float freqEnd;
|
float freqEnd;
|
||||||
float dutyCycle; // modified by getEffectiveDutyCycle
|
float dutyCycle;
|
||||||
|
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,9 +4,6 @@
|
|||||||
#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() {}
|
||||||
@@ -129,28 +126,15 @@ 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)
|
||||||
{
|
{
|
||||||
// Check if traffic management wants to exhaust this packet's hops
|
if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) {
|
||||||
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);
|
||||||
|
|
||||||
// If exhausting hops, force hop_limit = 0 regardless of other logic
|
// Use shared logic to determine if hop_limit should be decremented
|
||||||
if (exhaustHops) {
|
if (shouldDecrementHopLimit(p)) {
|
||||||
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");
|
||||||
|
|||||||
+1
-40
@@ -863,10 +863,6 @@ void NodeDB::installDefaultModuleConfig()
|
|||||||
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#if ARCH_PORTDUINO
|
|
||||||
moduleConfig.external_notification.enabled = true;
|
|
||||||
moduleConfig.external_notification.alert_message = true;
|
|
||||||
#endif
|
|
||||||
#ifdef NANO_G2_ULTRA
|
#ifdef NANO_G2_ULTRA
|
||||||
moduleConfig.external_notification.enabled = true;
|
moduleConfig.external_notification.enabled = true;
|
||||||
moduleConfig.external_notification.alert_message = true;
|
moduleConfig.external_notification.alert_message = true;
|
||||||
@@ -1310,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::clampConfigLora(config.lora);
|
RadioInterface::bootstrapLoRaConfigFromPreset(config.lora);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED
|
#if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED
|
||||||
@@ -1661,25 +1657,6 @@ 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
|
||||||
@@ -1717,22 +1694,6 @@ 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,27 +114,6 @@ 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,
|
||||||
|
|||||||
@@ -465,11 +465,6 @@ 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;
|
|
||||||
default:
|
default:
|
||||||
LOG_ERROR("Unknown module config type %d", config_state);
|
LOG_ERROR("Unknown module config type %d", config_state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,7 +240,8 @@ 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);
|
||||||
|
|
||||||
limitPower(RF95_MAX_POWER);
|
if (power > RF95_MAX_POWER) // This chip has lower power limits than some
|
||||||
|
power = RF95_MAX_POWER;
|
||||||
|
|
||||||
#ifdef USE_RF95_RFO
|
#ifdef USE_RF95_RFO
|
||||||
err = lora->setOutputPower(power, true);
|
err = lora->setOutputPower(power, true);
|
||||||
|
|||||||
+132
-312
@@ -21,7 +21,6 @@
|
|||||||
#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"
|
||||||
@@ -33,32 +32,10 @@
|
|||||||
#include "STM32WLE5JCInterface.h"
|
#include "STM32WLE5JCInterface.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
|
#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \
|
||||||
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, power_limit, frequency_switching, \
|
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \
|
||||||
wide_lora, &profile_ptr, #name \
|
frequency_switching, wide_lora, #name \
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegionInfo regions[] = {
|
const RegionInfo regions[] = {
|
||||||
@@ -66,7 +43,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, 30, false, false, PROFILE_STD),
|
RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
|
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
|
||||||
@@ -74,7 +51,8 @@ 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, 10, false, false, PROFILE_STD),
|
RDEF(EU_433, 433.0f, 434.0f, 10, 0, 10, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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/
|
||||||
@@ -89,33 +67,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, 27, false, false, PROFILE_EU868),
|
RDEF(EU_868, 869.4f, 869.65f, 10, 0, 27, false, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 19, false, false, PROFILE_STD),
|
RDEF(CN, 470.0f, 510.0f, 100, 0, 19, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 13, false, false, PROFILE_STD),
|
RDEF(JP, 920.5f, 923.5f, 100, 0, 13, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 30, false, false, PROFILE_STD),
|
RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 14, false, false, PROFILE_STD),
|
RDEF(ANZ_433, 433.05f, 434.79f, 100, 0, 14, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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
|
||||||
@@ -123,13 +101,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, 20, false, false, PROFILE_STD),
|
RDEF(RU, 868.7f, 869.2f, 100, 0, 20, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 23, false, false, PROFILE_STD),
|
RDEF(KR, 920.0f, 923.0f, 100, 0, 23, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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.
|
||||||
@@ -137,40 +115,44 @@ 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, 27, false, false, PROFILE_STD),
|
RDEF(TW, 920.0f, 925.0f, 100, 0, 27, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 30, false, false, PROFILE_STD),
|
RDEF(IN, 865.0f, 867.0f, 100, 0, 30, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 36, false, false, PROFILE_STD),
|
RDEF(NZ_865, 864.0f, 868.0f, 100, 0, 36, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 27, false, false, PROFILE_STD),
|
RDEF(TH, 920.0f, 925.0f, 10, 0, 27, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD),
|
RDEF(UA_868, 868.0f, 868.6f, 1, 0, 14, true, false, false),
|
||||||
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, 20, false, false, PROFILE_STD),
|
RDEF(MY_433, 433.0f, 435.0f, 100, 0, 20, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Malaysia
|
Malaysia
|
||||||
@@ -179,14 +161,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, 27, true, false, PROFILE_STD),
|
RDEF(MY_919, 919.0f, 924.0f, 100, 0, 27, true, true, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 20, false, false, PROFILE_STD),
|
RDEF(SG_923, 917.0f, 925.0f, 100, 0, 20, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Philippines
|
Philippines
|
||||||
@@ -196,9 +178,8 @@ 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, 10, false, false, PROFILE_STD),
|
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_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD),
|
RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false),
|
||||||
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD),
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Kazakhstan
|
Kazakhstan
|
||||||
@@ -206,38 +187,37 @@ 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, 10, false, false, PROFILE_STD),
|
RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false),
|
||||||
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
|
RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 30, false, false, PROFILE_STD),
|
RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 30, false, false, PROFILE_STD),
|
RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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, 10, false, true, PROFILE_STD),
|
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 0, 10, true, false, true),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This needs to be last. Same as US.
|
This needs to be last. Same as US.
|
||||||
*/
|
*/
|
||||||
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF)
|
RDEF(UNSET, 902.0f, 928.0f, 100, 0, 30, true, false, false)
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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];
|
||||||
|
|
||||||
@@ -523,14 +503,45 @@ void initRegion()
|
|||||||
myRegion = r;
|
myRegion = r;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig)
|
||||||
{
|
{
|
||||||
|
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 != code; r++)
|
for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; 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;
|
||||||
@@ -740,7 +751,7 @@ void RadioInterface::saveFreq(float freq)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save our frequency slot (aka channel) for later reuse.
|
* Save our channel for later reuse.
|
||||||
*/
|
*/
|
||||||
void RadioInterface::saveChannelNum(uint32_t channel_num)
|
void RadioInterface::saveChannelNum(uint32_t channel_num)
|
||||||
{
|
{
|
||||||
@@ -763,304 +774,113 @@ 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;
|
||||||
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
bool validConfig = false; // We need to check for a valid configuration
|
||||||
myRegion = newRegion;
|
while (!validConfig) {
|
||||||
|
if (loraConfig.use_preset) {
|
||||||
if (loraConfig.use_preset) {
|
modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr);
|
||||||
if (!validateConfigLora(loraConfig)) {
|
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) {
|
||||||
loraConfig.modem_preset = newRegion->getDefaultPreset();
|
cr = loraConfig.coding_rate;
|
||||||
}
|
LOG_INFO("Using custom Coding Rate %u", cr);
|
||||||
uint8_t newcr;
|
}
|
||||||
modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr);
|
} else {
|
||||||
// If custom CR is being used already, check if the new preset is higher
|
sf = loraConfig.spread_factor;
|
||||||
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;
|
||||||
LOG_INFO("Using custom Coding Rate %u", cr);
|
bw = bwCodeToKHz(loraConfig.bandwidth);
|
||||||
} else {
|
|
||||||
cr = newcr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else { // if not using preset, then just use the custom settings
|
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
|
||||||
if (validateConfigLora(loraConfig)) {
|
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 {
|
||||||
LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults",
|
validConfig = true;
|
||||||
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 > newRegion->powerLimit) && !devicestate.owner.is_licensed))
|
if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed))
|
||||||
power = newRegion->powerLimit;
|
power = myRegion->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 newRegion defaults
|
power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion 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
|
||||||
|
|
||||||
uint32_t channel_num;
|
// Calculate the number of channels
|
||||||
float freq;
|
uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000)));
|
||||||
|
|
||||||
// Calculate number of frequency slots (aka Channels):
|
// If user has manually specified a channel num, then use that, otherwise generate one by hashing the name
|
||||||
// spacing = gap between channels (0 for continuous spectrum) and at the beginning of the band
|
const char *channelName = channels.getName(channels.getPrimaryIndex());
|
||||||
// padding = gap at the beginning and end of the channel (0 for no padding)
|
// channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1)
|
||||||
float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (bw / 1000); // in MHz
|
uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels;
|
||||||
uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth);
|
|
||||||
|
|
||||||
// Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one.
|
// Check if we use the default frequency slot
|
||||||
// Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to
|
RadioInterface::uses_default_frequency_slot =
|
||||||
// (numFreqSlots - 1).
|
channel_num ==
|
||||||
uint32_t presetNameHashSlot =
|
hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels;
|
||||||
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", newRegion->name, channelName, loraConfig.modem_preset,
|
LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset,
|
||||||
channel_num, power);
|
channel_num, power);
|
||||||
LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd,
|
LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd,
|
||||||
newRegion->freqEnd - newRegion->freqStart);
|
myRegion->freqEnd - myRegion->freqStart);
|
||||||
LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw);
|
LOG_INFO("numChannels: %d x %.3fkHz", numChannels, 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;
|
||||||
@@ -1174,4 +994,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
|
|||||||
|
|
||||||
sendingPacket = p;
|
sendingPacket = p;
|
||||||
return p->encrypted.size + sizeof(PacketHeader);
|
return p->encrypted.size + sizeof(PacketHeader);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,20 +92,15 @@ class RadioInterface
|
|||||||
uint8_t sf = 9;
|
uint8_t sf = 9;
|
||||||
uint8_t cr = 5;
|
uint8_t cr = 5;
|
||||||
|
|
||||||
static constexpr uint8_t NUM_SYM_CAD =
|
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
|
||||||
2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48
|
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
|
||||||
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
|
||||||
static constexpr uint16_t preambleLengthDefault =
|
uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast
|
||||||
16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
|
const uint32_t PROCESSING_TIME_MSEC =
|
||||||
static constexpr uint16_t wideLoraPreambleLengthDefault = 12; // 12 is default for wide Lora
|
4500; // time to construct, process and construct a packet again (empirically determined)
|
||||||
uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast
|
const uint8_t CWmin = 3; // minimum CWsize
|
||||||
static constexpr uint32_t PROCESSING_TIME_MSEC =
|
const uint8_t CWmax = 8; // maximum CWsize
|
||||||
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;
|
||||||
@@ -132,7 +127,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); // maybe superseded?
|
static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
||||||
@@ -239,20 +234,6 @@ 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()
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
#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
|
||||||
@@ -98,20 +95,6 @@ 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)
|
||||||
@@ -875,12 +858,6 @@ 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);
|
||||||
|
|||||||
@@ -245,21 +245,14 @@ 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);
|
||||||
|
|
||||||
limitPower(SX126X_MAX_POWER);
|
if (power > SX126X_MAX_POWER) // This chip has lower power limits than some
|
||||||
// Make sure we reach the minimum power supported to turn the chip on (-9dBm)
|
power = SX126X_MAX_POWER;
|
||||||
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;
|
||||||
@@ -455,6 +448,15 @@ template <typename T> void SX126xInterface<T>::resetAGC()
|
|||||||
// RX boosted gain mode
|
// RX boosted gain mode
|
||||||
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||||
|
|
||||||
|
// Re-apply the undocumented 0x8B5 RX sensitivity patch that was set in init().
|
||||||
|
// The CALIBRATE_ALL (0x7F) command above clears bit 0 of register 0x8B5, which
|
||||||
|
// silently removes the RX sensitivity improvement introduced in #9571 / #9777.
|
||||||
|
// Without this re-apply, every SX1262 node loses its RX boost ~60s after boot
|
||||||
|
// and never recovers until reboot. See empirical evidence in the PR description.
|
||||||
|
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) {
|
||||||
|
LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch");
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Resume receiving
|
// 6. Resume receiving
|
||||||
startReceive();
|
startReceive();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ 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);
|
||||||
|
|
||||||
limitPower(SX128X_MAX_POWER);
|
if (power > SX128X_MAX_POWER) // This chip has lower power limits than some
|
||||||
|
power = SX128X_MAX_POWER;
|
||||||
|
|
||||||
err = lora.setOutputPower(power);
|
err = lora.setOutputPower(power);
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
if (err != RADIOLIB_ERR_NONE)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#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)
|
||||||
{
|
{
|
||||||
@@ -81,7 +82,11 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
|||||||
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
|
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
|
||||||
|
|
||||||
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';
|
||||||
|
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';
|
||||||
|
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;
|
||||||
@@ -99,7 +104,11 @@ 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';
|
||||||
|
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';
|
||||||
|
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;
|
||||||
|
|||||||
@@ -285,7 +285,18 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
|
|||||||
/* Nepal 865MHz */
|
/* Nepal 865MHz */
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25,
|
meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25,
|
||||||
/* Brazil 902MHz */
|
/* Brazil 902MHz */
|
||||||
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26
|
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26,
|
||||||
|
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
|
||||||
|
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
|
||||||
|
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
|
||||||
|
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
|
||||||
@@ -315,7 +326,24 @@ 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 {
|
||||||
@@ -702,12 +730,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_BR_902
|
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_EU_N_868
|
||||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
|
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868+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_LONG_TURBO
|
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW
|
||||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1))
|
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW+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
|
||||||
|
|||||||
@@ -315,6 +315,8 @@ 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.
|
||||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -69,22 +69,6 @@ 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);
|
||||||
@@ -106,4 +90,4 @@ bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count);
|
|||||||
*/
|
*/
|
||||||
bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count);
|
bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count);
|
||||||
|
|
||||||
#define is_in_repeated(name, n) is_in_helper(n, name, name##_count)
|
#define is_in_repeated(name, n) is_in_helper(n, name, name##_count)
|
||||||
@@ -117,4 +117,93 @@ 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
|
||||||
|
buf[bufSize - 1] = '\0';
|
||||||
|
|
||||||
|
bool replaced = false;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,10 @@ 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)
|
||||||
{
|
{
|
||||||
|
|||||||
+102
-124
@@ -24,9 +24,7 @@
|
|||||||
|
|
||||||
#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"
|
||||||
@@ -199,35 +197,10 @@ 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");
|
||||||
@@ -480,7 +453,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) || defined(ARCH_STM32WL)
|
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
|
||||||
enterDfuMode();
|
enterDfuMode();
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
@@ -626,10 +599,14 @@ 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());
|
||||||
|
|
||||||
@@ -653,7 +630,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||||
{
|
{
|
||||||
auto changes = SEGMENT_CONFIG;
|
auto changes = SEGMENT_CONFIG;
|
||||||
auto existingRole = config.device.role;
|
auto existingRole = config.device.role;
|
||||||
@@ -797,57 +774,18 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
|||||||
validatedLora.spread_factor = LORA_SF_DEFAULT;
|
validatedLora.spread_factor = LORA_SF_DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're setting a new region, check the region is valid and then init the region or discard the change
|
// If no lora radio parameters change, don't need to reboot
|
||||||
if (validatedLora.region != myRegion->code) {
|
if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region &&
|
||||||
// Region has changed so check whether it is valid for e.g. licensing conditions and if the lora config is valid
|
oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth &&
|
||||||
if (RadioInterface::validateConfigRegion(validatedLora) && RadioInterface::validateConfigLora(validatedLora)) {
|
oldLoraConfig.spread_factor == validatedLora.spread_factor &&
|
||||||
// If we're setting region for the first time, init the region and regenerate the keys
|
oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power &&
|
||||||
if (isRegionUnset && validatedLora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
oldLoraConfig.frequency_offset == validatedLora.frequency_offset &&
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
oldLoraConfig.override_frequency == validatedLora.override_frequency &&
|
||||||
if (crypto) {
|
oldLoraConfig.channel_num == validatedLora.channel_num &&
|
||||||
crypto->ensurePkiKeys(config.security, owner);
|
oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) {
|
||||||
}
|
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) {
|
||||||
@@ -863,21 +801,63 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
|||||||
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(validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED);
|
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED);
|
||||||
} else if (validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT) {
|
} else if (config.lora.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");
|
||||||
validatedLora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT;
|
config.lora.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();
|
||||||
|
|
||||||
config.lora = validatedLora; // Finally, return the validated config back to the main config
|
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;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case meshtastic_Config_bluetooth_tag:
|
case meshtastic_Config_bluetooth_tag:
|
||||||
@@ -925,10 +905,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
|||||||
}
|
}
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -1034,11 +1014,6 @@ 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;
|
||||||
@@ -1153,85 +1128,78 @@ 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:
|
||||||
configName = "MQTT";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Serial";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "External Notification";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Store & Forward";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Range Test";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Telemetry";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Canned Message";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Audio";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Remote Hardware";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Neighbor Info";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Detection Sensor";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Ambient Lighting";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "Paxcounter";
|
LOG_INFO("Get module config: 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:
|
||||||
configName = "StatusMessage";
|
LOG_INFO("Get module config: 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
|
||||||
@@ -1414,23 +1382,33 @@ 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
|
||||||
const char *fieldsToCheck[] = {p.call_sign, p.short_name};
|
if (*p.call_sign) {
|
||||||
const char *fieldNames[] = {"call_sign", "short_name"};
|
const char *start = p.call_sign;
|
||||||
for (int i = 0; i < 2; i++) {
|
// Skip all whitespace
|
||||||
if (*fieldsToCheck[i]) {
|
while (*start && isspace((unsigned char)*start))
|
||||||
const char *start = fieldsToCheck[i];
|
start++;
|
||||||
while (*start && isspace((unsigned char)*start))
|
if (*start == '\0') {
|
||||||
start++;
|
LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character");
|
||||||
if (*start == '\0') {
|
return;
|
||||||
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,11 +60,7 @@ 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);
|
||||||
|
|||||||
@@ -24,10 +24,6 @@
|
|||||||
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
#if HAS_LIBNOTIFY
|
|
||||||
#include <libnotify/notify.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(HAS_RGB_LED)
|
#if defined(HAS_RGB_LED)
|
||||||
#include "AmbientLightingThread.h"
|
#include "AmbientLightingThread.h"
|
||||||
uint8_t red = 0;
|
uint8_t red = 0;
|
||||||
@@ -412,9 +408,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
|||||||
if (genericShouldAlert) {
|
if (genericShouldAlert) {
|
||||||
LOG_INFO("externalNotificationModule - Generic alert");
|
LOG_INFO("externalNotificationModule - Generic alert");
|
||||||
setExternalState(0, true);
|
setExternalState(0, true);
|
||||||
#if HAS_LIBNOTIFY
|
|
||||||
portduinoNotify(mp);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vibraShouldAlert) {
|
if (vibraShouldAlert) {
|
||||||
@@ -528,40 +521,3 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event)
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if HAS_LIBNOTIFY
|
|
||||||
void ExternalNotificationModule::portduinoNotify(const meshtastic_MeshPacket &mp)
|
|
||||||
{
|
|
||||||
std::string senderName;
|
|
||||||
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
|
||||||
if (sender && sender->has_user) {
|
|
||||||
if (sender->user.long_name[0] != '\0') {
|
|
||||||
senderName = sender->user.long_name;
|
|
||||||
} else {
|
|
||||||
senderName = sender->user.short_name;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
senderName = std::to_string(mp.from);
|
|
||||||
}
|
|
||||||
std::string notificationSummary = "From: " + senderName;
|
|
||||||
std::string notificationBody = std::string((char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
|
|
||||||
|
|
||||||
if (!notify_is_initted()) {
|
|
||||||
if (!notify_init("Meshtasticd")) {
|
|
||||||
LOG_WARN("Failed to initialize libnotify");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NotifyNotification *notification =
|
|
||||||
notify_notification_new(notificationSummary.c_str(), notificationBody.c_str(), "org.meshtastic.meshtasticd");
|
|
||||||
if (notification) {
|
|
||||||
GError *error = nullptr;
|
|
||||||
if (!notify_notification_show(notification, &error)) {
|
|
||||||
LOG_WARN("Failed to show notification: %s", error ? error->message : "unknown error");
|
|
||||||
if (error)
|
|
||||||
g_error_free(error);
|
|
||||||
}
|
|
||||||
g_object_unref(G_OBJECT(notification));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -77,10 +77,6 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency:
|
|||||||
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
||||||
meshtastic_AdminMessage *request,
|
meshtastic_AdminMessage *request,
|
||||||
meshtastic_AdminMessage *response) override;
|
meshtastic_AdminMessage *response) override;
|
||||||
|
|
||||||
#if HAS_LIBNOTIFY
|
|
||||||
void portduinoNotify(const meshtastic_MeshPacket &mp);
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern ExternalNotificationModule *externalNotificationModule;
|
extern ExternalNotificationModule *externalNotificationModule;
|
||||||
@@ -38,9 +38,6 @@
|
|||||||
#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"
|
||||||
@@ -123,14 +120,6 @@ 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
|
||||||
|
|||||||
@@ -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) && velCount > 0 && dirCount > 0) {
|
if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) {
|
||||||
// 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,23 +29,10 @@ 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_init_zero;
|
meshtastic_StatusMessage incomingMessage;
|
||||||
|
|
||||||
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,11 +2,10 @@
|
|||||||
#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
|
||||||
@@ -20,28 +19,16 @@ 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;
|
||||||
|
|||||||
@@ -66,10 +66,18 @@ 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
|
||||||
@@ -86,8 +94,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(<SHTSensor.h>)
|
#if __has_include(<Adafruit_SHT4x.h>)
|
||||||
#include "Sensor/SHTXXSensor.h"
|
#include "Sensor/SHT4XSensor.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
#if __has_include(<SparkFun_MLX90632_Arduino_Library.h>)
|
||||||
@@ -147,15 +155,6 @@ 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
|
||||||
@@ -203,9 +202,15 @@ 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
|
||||||
@@ -218,9 +223,13 @@ 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
|
||||||
@@ -236,10 +245,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +260,14 @@ 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)) {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<Adafruit_SHTC3.h>)
|
||||||
|
|
||||||
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
|
#include "TelemetrySensor.h"
|
||||||
|
#include <Adafruit_SHTC3.h>
|
||||||
|
|
||||||
|
class SHTC3Sensor : public TelemetrySensor
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
Adafruit_SHTC3 shtc3 = Adafruit_SHTC3();
|
||||||
|
|
||||||
|
public:
|
||||||
|
SHTC3Sensor();
|
||||||
|
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||||
|
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SHTSensor.h>)
|
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
#include "SHTXXSensor.h"
|
|
||||||
#include "TelemetrySensor.h"
|
|
||||||
#include <SHTSensor.h>
|
|
||||||
|
|
||||||
SHTXXSensor::SHTXXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTXX, "SHTXX") {}
|
|
||||||
|
|
||||||
void SHTXXSensor::getSensorVariant(SHTSensor::SHTSensorType sensorType)
|
|
||||||
{
|
|
||||||
switch (sensorType) {
|
|
||||||
case SHTSensor::SHTSensorType::SHT2X:
|
|
||||||
sensorVariant = "SHT2x";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SHTSensor::SHTSensorType::SHT3X:
|
|
||||||
case SHTSensor::SHTSensorType::SHT85:
|
|
||||||
sensorVariant = "SHT3x/SHT85";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SHTSensor::SHTSensorType::SHT3X_ALT:
|
|
||||||
sensorVariant = "SHT3x";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SHTSensor::SHTSensorType::SHTW1:
|
|
||||||
case SHTSensor::SHTSensorType::SHTW2:
|
|
||||||
case SHTSensor::SHTSensorType::SHTC1:
|
|
||||||
case SHTSensor::SHTSensorType::SHTC3:
|
|
||||||
sensorVariant = "SHTC1/SHTC3/SHTW1/SHTW2";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SHTSensor::SHTSensorType::SHT4X:
|
|
||||||
sensorVariant = "SHT4x";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sensorVariant = "Unknown";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SHTXXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
|
||||||
{
|
|
||||||
LOG_INFO("Init sensor: %s", sensorName);
|
|
||||||
|
|
||||||
_bus = bus;
|
|
||||||
_address = dev->address.address;
|
|
||||||
|
|
||||||
if (sht.init(*_bus)) {
|
|
||||||
LOG_INFO("%s: init(): success", sensorName);
|
|
||||||
getSensorVariant(sht.mSensorType);
|
|
||||||
LOG_INFO("%s Sensor detected: %s on 0x%x", sensorName, sensorVariant, _address);
|
|
||||||
status = 1;
|
|
||||||
} else {
|
|
||||||
LOG_ERROR("%s: init(): failed", sensorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
initI2CSensor();
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accuracy setting of measurement.
|
|
||||||
* Not all sensors support changing the sampling accuracy (only SHT3X and SHT4X)
|
|
||||||
* SHTAccuracy:
|
|
||||||
* - SHT_ACCURACY_HIGH: Highest repeatability at the cost of slower measurement
|
|
||||||
* - SHT_ACCURACY_MEDIUM: Balanced repeatability and speed of measurement
|
|
||||||
* - SHT_ACCURACY_LOW: Fastest measurement but lowest repeatability
|
|
||||||
*/
|
|
||||||
bool SHTXXSensor::setAccuracy(SHTSensor::SHTAccuracy newAccuracy)
|
|
||||||
{
|
|
||||||
// Only SHT3X-family (including alternates) and SHT4X support changing accuracy
|
|
||||||
if (sht.mSensorType != SHTSensor::SHTSensorType::SHT3X && sht.mSensorType != SHTSensor::SHTSensorType::SHT3X_ALT &&
|
|
||||||
sht.mSensorType != SHTSensor::SHTSensorType::SHT85 && sht.mSensorType != SHTSensor::SHTSensorType::SHT4X) {
|
|
||||||
LOG_WARN("%s doesn't support accuracy setting", sensorVariant);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LOG_INFO("%s: setting new accuracy setting", sensorVariant);
|
|
||||||
accuracy = newAccuracy;
|
|
||||||
return sht.setAccuracy(accuracy);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SHTXXSensor::getMetrics(meshtastic_Telemetry *measurement)
|
|
||||||
{
|
|
||||||
if (sht.readSample()) {
|
|
||||||
measurement->variant.environment_metrics.has_temperature = true;
|
|
||||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
|
||||||
measurement->variant.environment_metrics.temperature = sht.getTemperature();
|
|
||||||
measurement->variant.environment_metrics.relative_humidity = sht.getHumidity();
|
|
||||||
|
|
||||||
LOG_INFO("%s (%s): Got: temp:%fdegC, hum:%f%%rh", sensorName, sensorVariant,
|
|
||||||
measurement->variant.environment_metrics.temperature,
|
|
||||||
measurement->variant.environment_metrics.relative_humidity);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
LOG_ERROR("%s (%s): read sample failed", sensorName, sensorVariant);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AdminMessageHandleResult SHTXXSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
|
||||||
meshtastic_AdminMessage *response)
|
|
||||||
{
|
|
||||||
AdminMessageHandleResult result;
|
|
||||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
|
||||||
|
|
||||||
switch (request->which_payload_variant) {
|
|
||||||
case meshtastic_AdminMessage_sensor_config_tag:
|
|
||||||
if (!request->sensor_config.has_shtxx_config) {
|
|
||||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for sensor accuracy setting
|
|
||||||
if (request->sensor_config.shtxx_config.has_set_accuracy) {
|
|
||||||
SHTSensor::SHTAccuracy newAccuracy;
|
|
||||||
if (request->sensor_config.shtxx_config.set_accuracy == 0) {
|
|
||||||
newAccuracy = SHTSensor::SHT_ACCURACY_LOW;
|
|
||||||
} else if (request->sensor_config.shtxx_config.set_accuracy == 1) {
|
|
||||||
newAccuracy = SHTSensor::SHT_ACCURACY_MEDIUM;
|
|
||||||
} else if (request->sensor_config.shtxx_config.set_accuracy == 2) {
|
|
||||||
newAccuracy = SHTSensor::SHT_ACCURACY_HIGH;
|
|
||||||
} else {
|
|
||||||
LOG_ERROR("%s: incorrect accuracy setting", sensorName);
|
|
||||||
result = AdminMessageHandleResult::HANDLED;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this->setAccuracy(newAccuracy);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = AdminMessageHandleResult::HANDLED;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
result = AdminMessageHandleResult::NOT_HANDLED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<SHTSensor.h>)
|
|
||||||
|
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
#include "TelemetrySensor.h"
|
|
||||||
#include <SHTSensor.h>
|
|
||||||
|
|
||||||
class SHTXXSensor : public TelemetrySensor
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
SHTSensor sht;
|
|
||||||
TwoWire *_bus{};
|
|
||||||
uint8_t _address{};
|
|
||||||
SHTSensor::SHTAccuracy accuracy{};
|
|
||||||
bool setAccuracy(SHTSensor::SHTAccuracy newAccuracy);
|
|
||||||
|
|
||||||
public:
|
|
||||||
SHTXXSensor();
|
|
||||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
|
||||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
|
||||||
void getSensorVariant(SHTSensor::SHTSensorType);
|
|
||||||
const char *sensorVariant{};
|
|
||||||
|
|
||||||
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
|
||||||
meshtastic_AdminMessage *response) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,434 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "MeshModule.h"
|
|
||||||
#include "concurrency/Lock.h"
|
|
||||||
#include "concurrency/OSThread.h"
|
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
|
||||||
#include "mesh/generated/meshtastic/telemetry.pb.h"
|
|
||||||
|
|
||||||
#if HAS_TRAFFIC_MANAGEMENT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TrafficManagementModule - Packet inspection and traffic shaping for mesh networks.
|
|
||||||
*
|
|
||||||
* This module provides:
|
|
||||||
* - Position deduplication (drop redundant position broadcasts)
|
|
||||||
* - Per-node rate limiting (throttle chatty nodes)
|
|
||||||
* - Unknown packet filtering (drop undecoded packets from repeat offenders)
|
|
||||||
* - NodeInfo direct response (answer queries from cache to reduce mesh chatter)
|
|
||||||
* - Local-only telemetry/position (exhaust hop_limit for local broadcasts)
|
|
||||||
* - Router hop preservation (maintain hop_limit for router-to-router traffic)
|
|
||||||
*
|
|
||||||
* Memory Optimization:
|
|
||||||
* Uses a unified cache with cuckoo hashing for O(1) lookups and 56% memory reduction
|
|
||||||
* compared to separate per-feature caches. Timestamps are stored as 8-bit relative
|
|
||||||
* offsets from a rolling epoch to further reduce memory footprint.
|
|
||||||
*/
|
|
||||||
class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TrafficManagementModule();
|
|
||||||
~TrafficManagementModule();
|
|
||||||
|
|
||||||
// Singleton — no copying or moving
|
|
||||||
TrafficManagementModule(const TrafficManagementModule &) = delete;
|
|
||||||
TrafficManagementModule &operator=(const TrafficManagementModule &) = delete;
|
|
||||||
|
|
||||||
meshtastic_TrafficManagementStats getStats() const;
|
|
||||||
void resetStats();
|
|
||||||
void recordRouterHopPreserved();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this packet should have its hops exhausted.
|
|
||||||
* Called from perhapsRebroadcast() to force hop_limit = 0 regardless of
|
|
||||||
* router_preserve_hops or favorite node logic.
|
|
||||||
*/
|
|
||||||
bool shouldExhaustHops(const meshtastic_MeshPacket &mp) const
|
|
||||||
{
|
|
||||||
return exhaustRequested && exhaustRequestedFrom == getFrom(&mp) && exhaustRequestedId == mp.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
|
||||||
bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
|
|
||||||
void alterReceived(meshtastic_MeshPacket &mp) override;
|
|
||||||
int32_t runOnce() override;
|
|
||||||
// Protected so test shims can force epoch rollover behavior.
|
|
||||||
void resetEpoch(uint32_t nowMs);
|
|
||||||
|
|
||||||
private:
|
|
||||||
// =========================================================================
|
|
||||||
// Unified Cache Entry (10 bytes) - Same for ALL platforms
|
|
||||||
// =========================================================================
|
|
||||||
//
|
|
||||||
// A single compact structure used across ESP32, NRF52, and all other platforms.
|
|
||||||
// Memory: 10 bytes × 2048 entries = 20KB
|
|
||||||
//
|
|
||||||
// Position Fingerprinting:
|
|
||||||
// Instead of storing full coordinates (8 bytes) or a computed hash,
|
|
||||||
// we store an 8-bit fingerprint derived deterministically from the
|
|
||||||
// truncated lat/lon. This extracts the lower 4 significant bits from
|
|
||||||
// each coordinate: fingerprint = (lat_low4 << 4) | lon_low4
|
|
||||||
//
|
|
||||||
// Benefits over hash:
|
|
||||||
// - Adjacent grid cells have sequential fingerprints (no collision)
|
|
||||||
// - Two positions only collide if 16+ grid cells apart in BOTH dimensions
|
|
||||||
// - Deterministic: same input always produces same output
|
|
||||||
//
|
|
||||||
// Adaptive Timestamp Resolution:
|
|
||||||
// All timestamps use 8-bit values with adaptive resolution calculated
|
|
||||||
// from config at startup. Resolution = max(60, min(339, interval/2)).
|
|
||||||
// - Min 60 seconds ensures reasonable precision
|
|
||||||
// - Max 339 seconds allows ~24 hour range (255 * 339 = 86445 sec)
|
|
||||||
// - interval/2 ensures at least 2 ticks per configured interval
|
|
||||||
//
|
|
||||||
// Layout:
|
|
||||||
// [0-3] node - NodeNum (4 bytes)
|
|
||||||
// [4] pos_fingerprint - 4 bits lat + 4 bits lon (1 byte)
|
|
||||||
// [5] rate_count - Packets in current window (1 byte)
|
|
||||||
// [6] unknown_count - Unknown packets count (1 byte)
|
|
||||||
// [7] pos_time - Position timestamp (1 byte, adaptive resolution)
|
|
||||||
// [8] rate_time - Rate window start (1 byte, adaptive resolution)
|
|
||||||
// [9] unknown_time - Unknown tracking start (1 byte, adaptive resolution)
|
|
||||||
//
|
|
||||||
struct __attribute__((packed)) UnifiedCacheEntry {
|
|
||||||
NodeNum node; // 4 bytes - Node identifier (0 = empty slot)
|
|
||||||
uint8_t pos_fingerprint; // 1 byte - Lower 4 bits of lat + lon
|
|
||||||
uint8_t rate_count; // 1 byte - Packet count (saturates at 255)
|
|
||||||
uint8_t unknown_count; // 1 byte - Unknown packet count (saturates at 255)
|
|
||||||
uint8_t pos_time; // 1 byte - Position timestamp (adaptive resolution)
|
|
||||||
uint8_t rate_time; // 1 byte - Rate window start (adaptive resolution)
|
|
||||||
uint8_t unknown_time; // 1 byte - Unknown tracking start (adaptive resolution)
|
|
||||||
};
|
|
||||||
static_assert(sizeof(UnifiedCacheEntry) == 10, "UnifiedCacheEntry should be 10 bytes");
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Cuckoo Hash Table Implementation
|
|
||||||
// =========================================================================
|
|
||||||
//
|
|
||||||
// Cuckoo hashing provides O(1) worst-case lookup time using two hash functions.
|
|
||||||
// Each key can be in one of two possible locations (h1 or h2). On collision,
|
|
||||||
// the existing entry is "kicked" to its alternate location.
|
|
||||||
//
|
|
||||||
// Benefits over linear scan:
|
|
||||||
// - O(1) lookup vs O(n) - critical at packet processing rates
|
|
||||||
// - O(1) insertion (amortized) with simple eviction on cycles
|
|
||||||
// - ~95% load factor achievable
|
|
||||||
//
|
|
||||||
// Cache size rounds to power-of-2 for fast modulo via bitmask.
|
|
||||||
// TRAFFIC_MANAGEMENT_CACHE_SIZE=2000 → cacheSize()=2048
|
|
||||||
//
|
|
||||||
static constexpr uint16_t cacheSize();
|
|
||||||
static constexpr uint16_t cacheMask();
|
|
||||||
|
|
||||||
// Hash functions for cuckoo hashing
|
|
||||||
inline uint16_t cuckooHash1(NodeNum node) const { return node & cacheMask(); }
|
|
||||||
inline uint16_t cuckooHash2(NodeNum node) const { return ((node * 2654435769u) >> (32 - cuckooHashBits())) & cacheMask(); }
|
|
||||||
static constexpr uint8_t cuckooHashBits();
|
|
||||||
|
|
||||||
// NodeInfo cache configuration (PSRAM path):
|
|
||||||
// - Payload lives in PSRAM
|
|
||||||
// - DRAM keeps packed 12-bit tags with 4-way bucketed cuckoo hashing
|
|
||||||
// (Fan et al., CoNEXT 2014). Tag value 0 is reserved as "empty".
|
|
||||||
static constexpr uint16_t kNodeInfoIndexMetadataBudgetBytes = 3072; // 3KB DRAM tag store
|
|
||||||
static constexpr uint8_t kNodeInfoTargetOccupancyPercent = 95;
|
|
||||||
static constexpr uint8_t kNodeInfoBucketSize = 4;
|
|
||||||
static constexpr uint8_t kNodeInfoTagBits = 12;
|
|
||||||
static constexpr uint16_t kNodeInfoTagMask = static_cast<uint16_t>((1u << kNodeInfoTagBits) - 1u);
|
|
||||||
static constexpr uint16_t kNodeInfoIndexSlotsRaw =
|
|
||||||
static_cast<uint16_t>((kNodeInfoIndexMetadataBudgetBytes * 8u) / kNodeInfoTagBits);
|
|
||||||
static constexpr uint16_t kNodeInfoIndexSlots =
|
|
||||||
static_cast<uint16_t>(kNodeInfoIndexSlotsRaw - (kNodeInfoIndexSlotsRaw % kNodeInfoBucketSize));
|
|
||||||
static constexpr uint16_t kNodeInfoTargetEntries =
|
|
||||||
static_cast<uint16_t>((kNodeInfoIndexSlots * kNodeInfoTargetOccupancyPercent) / 100u);
|
|
||||||
static_assert((kNodeInfoIndexSlots % kNodeInfoBucketSize) == 0, "NodeInfo slot count must align to bucket size");
|
|
||||||
static_assert(kNodeInfoTargetEntries < (1u << kNodeInfoTagBits), "NodeInfo tag bits must encode payload index");
|
|
||||||
|
|
||||||
static constexpr uint16_t nodeInfoTargetEntries();
|
|
||||||
static constexpr uint16_t nodeInfoIndexMetadataBudgetBytes();
|
|
||||||
static constexpr uint8_t nodeInfoTargetOccupancyPercent();
|
|
||||||
static constexpr uint8_t nodeInfoBucketSize();
|
|
||||||
static constexpr uint8_t nodeInfoTagBits();
|
|
||||||
static constexpr uint16_t nodeInfoTagMask();
|
|
||||||
static constexpr uint16_t nodeInfoIndexSlots();
|
|
||||||
static constexpr uint16_t nodeInfoBucketCount();
|
|
||||||
static constexpr uint16_t nodeInfoBucketMask();
|
|
||||||
static constexpr uint8_t nodeInfoBucketHashBits();
|
|
||||||
inline uint16_t nodeInfoHash1(NodeNum node) const { return node & nodeInfoBucketMask(); }
|
|
||||||
inline uint16_t nodeInfoHash2(NodeNum node) const
|
|
||||||
{
|
|
||||||
return ((node * 2246822519u) >> (32 - nodeInfoBucketHashBits())) & nodeInfoBucketMask();
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Adaptive Timestamp Resolution
|
|
||||||
// =========================================================================
|
|
||||||
//
|
|
||||||
// All timestamps use 8-bit values with adaptive resolution calculated from
|
|
||||||
// config at startup. This allows ~24 hour range while maintaining precision.
|
|
||||||
//
|
|
||||||
// Resolution formula: max(60, min(339, interval/2))
|
|
||||||
// - 60 sec minimum ensures reasonable precision
|
|
||||||
// - 339 sec maximum allows 24 hour range (255 * 339 ≈ 86400 sec)
|
|
||||||
// - interval/2 ensures at least 2 ticks per configured interval
|
|
||||||
//
|
|
||||||
// Since config changes require reboot, resolution is calculated once.
|
|
||||||
//
|
|
||||||
uint32_t cacheEpochMs = 0;
|
|
||||||
uint16_t posTimeResolution = 60; // Seconds per tick for position
|
|
||||||
uint16_t rateTimeResolution = 60; // Seconds per tick for rate limiting
|
|
||||||
uint16_t unknownTimeResolution = 60; // Seconds per tick for unknown tracking
|
|
||||||
|
|
||||||
// Calculate resolution from configured interval (called once at startup)
|
|
||||||
static uint16_t calcTimeResolution(uint32_t intervalSecs)
|
|
||||||
{
|
|
||||||
// Resolution = interval/2 to ensure at least 2 ticks per interval
|
|
||||||
// Clamped to [60, 339] for min precision and max 24h range
|
|
||||||
uint32_t res = (intervalSecs > 0) ? (intervalSecs / 2) : 60;
|
|
||||||
if (res < 60)
|
|
||||||
res = 60;
|
|
||||||
if (res > 339)
|
|
||||||
res = 339;
|
|
||||||
return static_cast<uint16_t>(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to/from 8-bit relative timestamps with given resolution
|
|
||||||
uint8_t toRelativeTime(uint32_t nowMs, uint16_t resolutionSecs) const
|
|
||||||
{
|
|
||||||
uint32_t ticks = (nowMs - cacheEpochMs) / (resolutionSecs * 1000UL);
|
|
||||||
return (ticks > UINT8_MAX) ? UINT8_MAX : static_cast<uint8_t>(ticks);
|
|
||||||
}
|
|
||||||
uint32_t fromRelativeTime(uint8_t ticks, uint16_t resolutionSecs) const
|
|
||||||
{
|
|
||||||
return cacheEpochMs + (static_cast<uint32_t>(ticks) * resolutionSecs * 1000UL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convenience wrappers for each timestamp type
|
|
||||||
uint8_t toRelativePosTime(uint32_t nowMs) const { return toRelativeTime(nowMs, posTimeResolution); }
|
|
||||||
uint32_t fromRelativePosTime(uint8_t t) const { return fromRelativeTime(t, posTimeResolution); }
|
|
||||||
|
|
||||||
uint8_t toRelativeRateTime(uint32_t nowMs) const { return toRelativeTime(nowMs, rateTimeResolution); }
|
|
||||||
uint32_t fromRelativeRateTime(uint8_t t) const { return fromRelativeTime(t, rateTimeResolution); }
|
|
||||||
|
|
||||||
uint8_t toRelativeUnknownTime(uint32_t nowMs) const { return toRelativeTime(nowMs, unknownTimeResolution); }
|
|
||||||
uint32_t fromRelativeUnknownTime(uint8_t t) const { return fromRelativeTime(t, unknownTimeResolution); }
|
|
||||||
|
|
||||||
// Epoch reset when any timestamp approaches overflow
|
|
||||||
// With max resolution of 339 sec, 200 ticks = ~19 hours (safe margin for 24h max)
|
|
||||||
bool needsEpochReset(uint32_t nowMs) const
|
|
||||||
{
|
|
||||||
uint16_t maxRes = posTimeResolution;
|
|
||||||
if (rateTimeResolution > maxRes)
|
|
||||||
maxRes = rateTimeResolution;
|
|
||||||
if (unknownTimeResolution > maxRes)
|
|
||||||
maxRes = unknownTimeResolution;
|
|
||||||
return (nowMs - cacheEpochMs) > (200UL * maxRes * 1000UL);
|
|
||||||
}
|
|
||||||
// =========================================================================
|
|
||||||
// Position Fingerprint
|
|
||||||
// =========================================================================
|
|
||||||
//
|
|
||||||
// Computes 8-bit fingerprint from truncated lat/lon coordinates.
|
|
||||||
// Extracts lower 4 significant bits from each coordinate.
|
|
||||||
//
|
|
||||||
// fingerprint = (lat_low4 << 4) | lon_low4
|
|
||||||
//
|
|
||||||
// Unlike a hash, adjacent grid cells have sequential fingerprints,
|
|
||||||
// so nearby positions never collide. Collisions only occur for
|
|
||||||
// positions 16+ grid cells apart in both dimensions.
|
|
||||||
//
|
|
||||||
// Guards: If precision < 4 bits, uses min(precision, 4) bits.
|
|
||||||
//
|
|
||||||
static uint8_t computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision);
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Cache Storage
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
mutable concurrency::Lock cacheLock; // Protects all cache access
|
|
||||||
UnifiedCacheEntry *cache = nullptr; // Cuckoo hash table (unified for all platforms)
|
|
||||||
bool cacheFromPsram = false; // Tracks allocator for correct deallocation
|
|
||||||
|
|
||||||
struct NodeInfoPayloadEntry {
|
|
||||||
// Node identifier associated with this payload slot.
|
|
||||||
// 0 means the slot is currently unused.
|
|
||||||
NodeNum node;
|
|
||||||
|
|
||||||
// Cached NODEINFO_APP payload body. This is separate from NodeDB and is only
|
|
||||||
// used by the PSRAM-backed direct-response path in this module.
|
|
||||||
meshtastic_User user;
|
|
||||||
|
|
||||||
// Extra response metadata captured from the latest observed NODEINFO_APP
|
|
||||||
// packet for this node. shouldRespondToNodeInfo() uses this metadata when
|
|
||||||
// building spoofed replies for requesting clients.
|
|
||||||
|
|
||||||
// Last local uptime tick (millis) when this entry was refreshed.
|
|
||||||
uint32_t lastObservedMs;
|
|
||||||
|
|
||||||
// Last RTC/packet timestamp (seconds) observed for this NodeInfo frame.
|
|
||||||
// If unavailable in packet, remains 0.
|
|
||||||
uint32_t lastObservedRxTime;
|
|
||||||
|
|
||||||
// Channel where we most recently heard this node's NodeInfo.
|
|
||||||
uint8_t sourceChannel;
|
|
||||||
|
|
||||||
// Cached decoded bitfield metadata from the source packet.
|
|
||||||
// We preserve non-OK_TO_MQTT bits in direct replies when available.
|
|
||||||
bool hasDecodedBitfield;
|
|
||||||
uint8_t decodedBitfield;
|
|
||||||
};
|
|
||||||
|
|
||||||
NodeInfoPayloadEntry *nodeInfoPayload = nullptr; // NodeInfo payloads in PSRAM
|
|
||||||
bool nodeInfoPayloadFromPsram = false; // Tracks allocator for correct deallocation
|
|
||||||
uint8_t *nodeInfoIndex = nullptr; // Packed 12-bit NodeInfo tags in DRAM
|
|
||||||
uint16_t nodeInfoAllocHint = 0;
|
|
||||||
uint16_t nodeInfoEvictCursor = 0;
|
|
||||||
|
|
||||||
meshtastic_TrafficManagementStats stats;
|
|
||||||
|
|
||||||
// Flag set during alterReceived() when packet should be exhausted.
|
|
||||||
// Checked by perhapsRebroadcast() to force hop_limit = 0 only for the
|
|
||||||
// matching packet key (from + id). Reset at start of handleReceived().
|
|
||||||
bool exhaustRequested = false;
|
|
||||||
NodeNum exhaustRequestedFrom = 0;
|
|
||||||
PacketId exhaustRequestedId = 0;
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Cache Operations
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
// Find or create entry for node using cuckoo hashing
|
|
||||||
// Returns nullptr if cache is full and eviction fails
|
|
||||||
UnifiedCacheEntry *findOrCreateEntry(NodeNum node, bool *isNew);
|
|
||||||
|
|
||||||
// Find existing entry (no creation)
|
|
||||||
UnifiedCacheEntry *findEntry(NodeNum node);
|
|
||||||
|
|
||||||
// NodeInfo cache operations (bucketed cuckoo index + PSRAM payloads)
|
|
||||||
const NodeInfoPayloadEntry *findNodeInfoEntry(NodeNum node) const;
|
|
||||||
NodeInfoPayloadEntry *findOrCreateNodeInfoEntry(NodeNum node, bool *usedEmptySlot);
|
|
||||||
uint16_t findNodeInfoPayloadIndex(NodeNum node) const;
|
|
||||||
bool removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex);
|
|
||||||
uint16_t allocateNodeInfoPayloadSlot();
|
|
||||||
uint16_t evictNodeInfoPayloadSlot();
|
|
||||||
bool tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag);
|
|
||||||
uint16_t encodeNodeInfoTag(uint16_t payloadIndex) const;
|
|
||||||
uint16_t decodeNodeInfoPayloadIndex(uint16_t tag) const;
|
|
||||||
uint16_t getNodeInfoTag(uint16_t slot) const;
|
|
||||||
void setNodeInfoTag(uint16_t slot, uint16_t tag);
|
|
||||||
uint16_t countNodeInfoEntriesLocked() const;
|
|
||||||
void cacheNodeInfoPacket(const meshtastic_MeshPacket &mp);
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Traffic Management Logic
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
bool shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs);
|
|
||||||
bool shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse);
|
|
||||||
bool isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const;
|
|
||||||
bool isRateLimited(NodeNum from, uint32_t nowMs);
|
|
||||||
bool shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs);
|
|
||||||
|
|
||||||
void logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const;
|
|
||||||
void incrementStat(uint32_t *field);
|
|
||||||
};
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Compile-time Cache Size Calculations
|
|
||||||
// =========================================================================
|
|
||||||
//
|
|
||||||
// Round TRAFFIC_MANAGEMENT_CACHE_SIZE up to next power of 2 for efficient
|
|
||||||
// cuckoo hash indexing (allows bitmask instead of modulo).
|
|
||||||
//
|
|
||||||
// These use C++11-compatible constexpr (single return statement).
|
|
||||||
//
|
|
||||||
|
|
||||||
namespace detail
|
|
||||||
{
|
|
||||||
// Helper: round up to next power of 2 using bit manipulation
|
|
||||||
constexpr uint16_t nextPow2(uint16_t n)
|
|
||||||
{
|
|
||||||
return n == 0 ? 0 : (((n - 1) | ((n - 1) >> 1) | ((n - 1) >> 2) | ((n - 1) >> 4) | ((n - 1) >> 8)) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: floor(log2(n)) for n >= 0, C++11-compatible constexpr.
|
|
||||||
constexpr uint8_t log2Floor(uint16_t n)
|
|
||||||
{
|
|
||||||
return n <= 1 ? 0 : static_cast<uint8_t>(1 + log2Floor(static_cast<uint16_t>(n >> 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: ceil(log2(n)) for n >= 1, C++11-compatible constexpr.
|
|
||||||
constexpr uint8_t log2Ceil(uint16_t n)
|
|
||||||
{
|
|
||||||
return n <= 1 ? 0 : static_cast<uint8_t>(1 + log2Floor(static_cast<uint16_t>(n - 1)));
|
|
||||||
}
|
|
||||||
} // namespace detail
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::cacheSize()
|
|
||||||
{
|
|
||||||
return detail::nextPow2(TRAFFIC_MANAGEMENT_CACHE_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::cacheMask()
|
|
||||||
{
|
|
||||||
return cacheSize() > 0 ? cacheSize() - 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t TrafficManagementModule::cuckooHashBits()
|
|
||||||
{
|
|
||||||
return detail::log2Floor(cacheSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::nodeInfoTargetEntries()
|
|
||||||
{
|
|
||||||
return kNodeInfoTargetEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::nodeInfoIndexMetadataBudgetBytes()
|
|
||||||
{
|
|
||||||
return kNodeInfoIndexMetadataBudgetBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t TrafficManagementModule::nodeInfoTargetOccupancyPercent()
|
|
||||||
{
|
|
||||||
return kNodeInfoTargetOccupancyPercent;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t TrafficManagementModule::nodeInfoBucketSize()
|
|
||||||
{
|
|
||||||
return kNodeInfoBucketSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t TrafficManagementModule::nodeInfoTagBits()
|
|
||||||
{
|
|
||||||
return kNodeInfoTagBits;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::nodeInfoTagMask()
|
|
||||||
{
|
|
||||||
return kNodeInfoTagMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::nodeInfoIndexSlots()
|
|
||||||
{
|
|
||||||
return kNodeInfoIndexSlots;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::nodeInfoBucketCount()
|
|
||||||
{
|
|
||||||
return static_cast<uint16_t>(nodeInfoIndexSlots() / nodeInfoBucketSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint16_t TrafficManagementModule::nodeInfoBucketMask()
|
|
||||||
{
|
|
||||||
return nodeInfoBucketCount() > 0 ? nodeInfoBucketCount() - 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr uint8_t TrafficManagementModule::nodeInfoBucketHashBits()
|
|
||||||
{
|
|
||||||
return detail::log2Floor(nodeInfoBucketCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
extern TrafficManagementModule *trafficManagementModule;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -15,6 +15,15 @@
|
|||||||
|
|
||||||
WaypointModule *waypointModule;
|
WaypointModule *waypointModule;
|
||||||
|
|
||||||
|
static inline float degToRad(float deg)
|
||||||
|
{
|
||||||
|
return deg * PI / 180.0f;
|
||||||
|
}
|
||||||
|
static inline float radToDeg(float rad)
|
||||||
|
{
|
||||||
|
return rad * 180.0f / PI;
|
||||||
|
}
|
||||||
|
|
||||||
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
{
|
{
|
||||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||||
@@ -82,7 +91,9 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
|||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
const int *textPos = graphics::getTextPositions(display);
|
|
||||||
|
const int w = display->getWidth();
|
||||||
|
const int h = display->getHeight();
|
||||||
|
|
||||||
// Decode the waypoint
|
// Decode the waypoint
|
||||||
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
|
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
|
||||||
@@ -97,118 +108,71 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
|||||||
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
|
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
|
||||||
|
|
||||||
// Will contain distance information, passed as a field to drawColumns
|
// Will contain distance information, passed as a field to drawColumns
|
||||||
char distStr[20] = "";
|
char distStr[20];
|
||||||
|
|
||||||
// Get our node, to use our own position
|
// Get our node, to use our own position
|
||||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
|
||||||
// Match compass sizing/placement to favorite node screen logic.
|
// Dimensions / co-ordinates for the compass/circle
|
||||||
const int w = display->getWidth();
|
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
|
||||||
int16_t compassRadius = 8;
|
const int16_t compassX = x + w - (compassDiam / 2) - 5;
|
||||||
int16_t compassX = x + w - compassRadius - 8;
|
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
|
||||||
int16_t compassY = y + display->getHeight() / 2;
|
? y + h / 2
|
||||||
|
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
|
||||||
|
|
||||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
// If our node has a position:
|
||||||
const int16_t topY = textPos[1];
|
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
|
||||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
|
|
||||||
const int16_t usableHeight = bottomY - topY - 5;
|
|
||||||
compassRadius = usableHeight / 2;
|
|
||||||
if (compassRadius < 8)
|
|
||||||
compassRadius = 8;
|
|
||||||
compassX = x + SCREEN_WIDTH - compassRadius - 8;
|
|
||||||
compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
|
||||||
} else {
|
|
||||||
// Waypoint content uses rows 1..4, so place the compass below that block.
|
|
||||||
const int yBelowContent = textPos[4] + FONT_HEIGHT_SMALL + 2;
|
|
||||||
const int margin = 4;
|
|
||||||
#if defined(USE_EINK)
|
|
||||||
const int iconSize = (graphics::currentResolution == graphics::ScreenResolution::High) ? 16 : 8;
|
|
||||||
const int navBarHeight = iconSize + 6;
|
|
||||||
#else
|
|
||||||
const int navBarHeight = 0;
|
|
||||||
#endif
|
|
||||||
const int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
|
||||||
if (availableHeight > 0) {
|
|
||||||
compassRadius = availableHeight / 2;
|
|
||||||
if (compassRadius < 8)
|
|
||||||
compassRadius = 8;
|
|
||||||
if (compassRadius * 2 > SCREEN_WIDTH - 16)
|
|
||||||
compassRadius = (SCREEN_WIDTH - 16) / 2;
|
|
||||||
if (compassRadius < 8)
|
|
||||||
compassRadius = 8;
|
|
||||||
compassX = x + SCREEN_WIDTH / 2;
|
|
||||||
compassY = yBelowContent + availableHeight / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const uint16_t compassDiam = compassRadius * 2;
|
|
||||||
|
|
||||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
|
||||||
const char *statusLine1 = nullptr;
|
|
||||||
const char *statusLine2 = nullptr;
|
|
||||||
|
|
||||||
// Distance only needs our own position fix; compass/bearing additionally needs heading.
|
|
||||||
if (hasOwnPositionFix) {
|
|
||||||
const meshtastic_PositionLite &op = ourNode->position;
|
const meshtastic_PositionLite &op = ourNode->position;
|
||||||
const float d =
|
float myHeading;
|
||||||
GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||||
|
myHeading = 0;
|
||||||
|
} else {
|
||||||
|
if (screen->hasHeading())
|
||||||
|
myHeading = degToRad(screen->getHeading());
|
||||||
|
else
|
||||||
|
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||||
|
}
|
||||||
|
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2));
|
||||||
|
|
||||||
// Always show distance once we have an own-position fix, even without heading.
|
// Compass bearing to waypoint
|
||||||
|
float bearingToOther =
|
||||||
|
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
|
||||||
|
// If the top of the compass is a static north then bearingToOther can be drawn on the compass directly
|
||||||
|
// If the top of the compass is not a static north we need adjust bearingToOther based on heading
|
||||||
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
|
||||||
|
bearingToOther -= myHeading;
|
||||||
|
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||||
|
|
||||||
|
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
|
||||||
|
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
|
||||||
|
|
||||||
|
// Distance to Waypoint
|
||||||
|
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||||
float feet = d * METERS_TO_FEET;
|
float feet = d * METERS_TO_FEET;
|
||||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft" : "%.1fmi",
|
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
||||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET);
|
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
||||||
} else {
|
} else {
|
||||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm" : "%.1fkm", d < 2000 ? d : d / 1000);
|
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
||||||
|
bearingToOtherDegrees);
|
||||||
}
|
}
|
||||||
|
|
||||||
float myHeading = 0.0f;
|
|
||||||
const bool hasHeading =
|
|
||||||
graphics::CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
|
||||||
if (hasHeading) {
|
|
||||||
// Draw compass circle
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
|
||||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
|
||||||
|
|
||||||
// Compass bearing to waypoint
|
|
||||||
float bearingToOther =
|
|
||||||
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
|
|
||||||
bearingToOther = graphics::CompassRenderer::adjustBearingForCompassMode(bearingToOther, myHeading);
|
|
||||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
|
||||||
|
|
||||||
const float bearingToOtherDegrees = graphics::CompassRenderer::radiansToDegrees360(bearingToOther);
|
|
||||||
|
|
||||||
// Distance to waypoint with relative bearing when heading is available.
|
|
||||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
|
||||||
float feet = d * METERS_TO_FEET;
|
|
||||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
|
||||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
|
||||||
} else {
|
|
||||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
|
||||||
bearingToOtherDegrees);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
statusLine1 = "No";
|
|
||||||
statusLine2 = "Heading";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No own fix yet, so compass/bearing data would be misleading.
|
|
||||||
statusLine1 = "No";
|
|
||||||
statusLine2 = "Fix";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusLine1) {
|
else {
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
||||||
display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1);
|
// ? in the distance field
|
||||||
display->drawString(compassX, compassY, statusLine2);
|
snprintf(distStr, sizeof(distStr), "? %s ?°",
|
||||||
|
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw compass circle
|
||||||
|
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
|
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
|
||||||
display->drawString(0, textPos[line++], lastStr);
|
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
|
||||||
display->drawString(0, textPos[line++], wp.name);
|
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
|
||||||
display->drawString(0, textPos[line++], wp.description);
|
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
|
||||||
if (distStr[0])
|
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
|
||||||
display->drawString(0, textPos[line++], distStr);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_
|
|||||||
// moduleConfig.audio.i2s_sck = 14;
|
// moduleConfig.audio.i2s_sck = 14;
|
||||||
// moduleConfig.audio.ptt_pin = 39;
|
// moduleConfig.audio.ptt_pin = 39;
|
||||||
|
|
||||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) {
|
if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) {
|
||||||
LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1);
|
LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1);
|
||||||
codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1);
|
codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1);
|
||||||
memcpy(tx_header.magic, c2_magic, sizeof(c2_magic));
|
memcpy(tx_header.magic, c2_magic, sizeof(c2_magic));
|
||||||
@@ -143,7 +143,7 @@ void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
|||||||
|
|
||||||
int32_t AudioModule::runOnce()
|
int32_t AudioModule::runOnce()
|
||||||
{
|
{
|
||||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) {
|
if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) {
|
||||||
esp_err_t res;
|
esp_err_t res;
|
||||||
if (firstTime) {
|
if (firstTime) {
|
||||||
// Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC
|
// Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC
|
||||||
@@ -270,7 +270,7 @@ void AudioModule::sendPayload(NodeNum dest, bool wantReplies)
|
|||||||
|
|
||||||
ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp)
|
ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
{
|
{
|
||||||
if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) {
|
if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) {
|
||||||
auto &p = mp.decoded;
|
auto &p = mp.decoded;
|
||||||
if (!isFromUs(&mp)) {
|
if (!isFromUs(&mp)) {
|
||||||
memcpy(rx_encode_frame, p.payload.bytes, p.payload.size);
|
memcpy(rx_encode_frame, p.payload.bytes, p.payload.size);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
extern graphics::Screen *screen;
|
extern graphics::Screen *screen;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Flag when an interrupt has been detected
|
||||||
|
volatile static bool BMM150_IRQ = false;
|
||||||
|
|
||||||
BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
|
BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
|
||||||
|
|
||||||
bool BMM150Sensor::init()
|
bool BMM150Sensor::init()
|
||||||
@@ -20,7 +23,24 @@ int32_t BMM150Sensor::runOnce()
|
|||||||
{
|
{
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||||
float heading = sensor->getCompassDegree();
|
float heading = sensor->getCompassDegree();
|
||||||
heading = applyCompassOrientation(heading);
|
|
||||||
|
switch (config.display.compass_orientation) {
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||||
|
heading += 90;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||||
|
heading += 180;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||||
|
heading += 270;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->setHeading(heading);
|
screen->setHeading(heading);
|
||||||
#endif
|
#endif
|
||||||
@@ -70,4 +90,4 @@ bool BMM150Singleton::init(ScanI2C::FoundDevice device)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -16,7 +16,6 @@ bool BMX160Sensor::init()
|
|||||||
if (sensor.begin()) {
|
if (sensor.begin()) {
|
||||||
// set output data rate
|
// set output data rate
|
||||||
sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ);
|
sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ);
|
||||||
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
|
||||||
LOG_DEBUG("BMX160 init ok");
|
LOG_DEBUG("BMX160 init ok");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -34,12 +33,42 @@ int32_t BMX160Sensor::runOnce()
|
|||||||
sensor.getAllData(&magAccel, NULL, &gAccel);
|
sensor.getAllData(&magAccel, NULL, &gAccel);
|
||||||
|
|
||||||
if (doCalibration) {
|
if (doCalibration) {
|
||||||
beginCalibrationDisplay(showingScreen);
|
|
||||||
updateCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
if (!showingScreen) {
|
||||||
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
|
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||||
lowestZ);
|
showingScreen = true;
|
||||||
|
if (screen)
|
||||||
|
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (magAccel.x > highestX)
|
||||||
|
highestX = magAccel.x;
|
||||||
|
if (magAccel.x < lowestX)
|
||||||
|
lowestX = magAccel.x;
|
||||||
|
if (magAccel.y > highestY)
|
||||||
|
highestY = magAccel.y;
|
||||||
|
if (magAccel.y < lowestY)
|
||||||
|
lowestY = magAccel.y;
|
||||||
|
if (magAccel.z > highestZ)
|
||||||
|
highestZ = magAccel.z;
|
||||||
|
if (magAccel.z < lowestZ)
|
||||||
|
lowestZ = magAccel.z;
|
||||||
|
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now > endCalibrationAt) {
|
||||||
|
doCalibration = false;
|
||||||
|
endCalibrationAt = 0;
|
||||||
|
showingScreen = false;
|
||||||
|
if (screen)
|
||||||
|
screen->endAlert();
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX,
|
||||||
|
// lowestY, highestY, lowestZ, highestZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int highestRealX = highestX - (highestX + lowestX) / 2;
|
||||||
|
|
||||||
magAccel.x -= (highestX + lowestX) / 2;
|
magAccel.x -= (highestX + lowestX) / 2;
|
||||||
magAccel.y -= (highestY + lowestY) / 2;
|
magAccel.y -= (highestY + lowestY) / 2;
|
||||||
magAccel.z -= (highestZ + lowestZ) / 2;
|
magAccel.z -= (highestZ + lowestZ) / 2;
|
||||||
@@ -59,7 +88,23 @@ int32_t BMX160Sensor::runOnce()
|
|||||||
|
|
||||||
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
||||||
|
|
||||||
heading = applyCompassOrientation(heading);
|
switch (config.display.compass_orientation) {
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||||
|
heading += 90;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||||
|
heading += 180;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||||
|
heading += 270;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->setHeading(heading);
|
screen->setHeading(heading);
|
||||||
#endif
|
#endif
|
||||||
@@ -74,8 +119,15 @@ void BMX160Sensor::calibrate(uint16_t forSeconds)
|
|||||||
sBmx160SensorData_t gAccel;
|
sBmx160SensorData_t gAccel;
|
||||||
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||||
sensor.getAllData(&magAccel, NULL, &gAccel);
|
sensor.getAllData(&magAccel, NULL, &gAccel);
|
||||||
seedCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
highestX = magAccel.x, lowestX = magAccel.x;
|
||||||
startCalibrationWindow(forSeconds);
|
highestY = magAccel.y, lowestY = magAccel.y;
|
||||||
|
highestZ = magAccel.z, lowestZ = magAccel.z;
|
||||||
|
|
||||||
|
doCalibration = true;
|
||||||
|
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||||
|
endCalibrationAt = millis() + calibrateFor;
|
||||||
|
if (screen)
|
||||||
|
screen->setEndCalibration(endCalibrationAt);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class BMX160Sensor : public MotionSensor
|
|||||||
private:
|
private:
|
||||||
RAK_BMX160 sensor;
|
RAK_BMX160 sensor;
|
||||||
bool showingScreen = false;
|
bool showingScreen = false;
|
||||||
static constexpr const char *compassCalibrationFileName = "/prefs/compass_bmx160.dat";
|
|
||||||
float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -40,4 +39,4 @@ class BMX160Sensor : public MotionSensor
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -26,11 +26,7 @@ bool ICM20948Sensor::init()
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Enable simple Wake on Motion
|
// Enable simple Wake on Motion
|
||||||
bool wakeOnMotionOk = sensor->setWakeOnMotion();
|
return sensor->setWakeOnMotion();
|
||||||
if (wakeOnMotionOk) {
|
|
||||||
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
|
||||||
}
|
|
||||||
return wakeOnMotionOk;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ICM_20948_INT_PIN
|
#ifdef ICM_20948_INT_PIN
|
||||||
@@ -51,8 +47,7 @@ int32_t ICM20948Sensor::runOnce()
|
|||||||
int32_t ICM20948Sensor::runOnce()
|
int32_t ICM20948Sensor::runOnce()
|
||||||
{
|
{
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||||
if (screen && !doCalibration && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion &&
|
if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) {
|
||||||
!config.device.double_tap_as_button_press) {
|
|
||||||
if (!isAsleep) {
|
if (!isAsleep) {
|
||||||
LOG_DEBUG("sleeping IMU");
|
LOG_DEBUG("sleeping IMU");
|
||||||
sensor->sleep(true);
|
sensor->sleep(true);
|
||||||
@@ -74,10 +69,38 @@ int32_t ICM20948Sensor::runOnce()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (doCalibration) {
|
if (doCalibration) {
|
||||||
beginCalibrationDisplay(showingScreen);
|
|
||||||
updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
if (!showingScreen) {
|
||||||
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
|
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||||
lowestZ);
|
showingScreen = true;
|
||||||
|
if (screen)
|
||||||
|
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (magX > highestX)
|
||||||
|
highestX = magX;
|
||||||
|
if (magX < lowestX)
|
||||||
|
lowestX = magX;
|
||||||
|
if (magY > highestY)
|
||||||
|
highestY = magY;
|
||||||
|
if (magY < lowestY)
|
||||||
|
lowestY = magY;
|
||||||
|
if (magZ > highestZ)
|
||||||
|
highestZ = magZ;
|
||||||
|
if (magZ < lowestZ)
|
||||||
|
lowestZ = magZ;
|
||||||
|
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now > endCalibrationAt) {
|
||||||
|
doCalibration = false;
|
||||||
|
endCalibrationAt = 0;
|
||||||
|
showingScreen = false;
|
||||||
|
if (screen)
|
||||||
|
screen->endAlert();
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX,
|
||||||
|
// lowestY, highestY, lowestZ, highestZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
magX -= (highestX + lowestX) / 2;
|
magX -= (highestX + lowestX) / 2;
|
||||||
@@ -99,7 +122,23 @@ int32_t ICM20948Sensor::runOnce()
|
|||||||
|
|
||||||
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
||||||
|
|
||||||
heading = applyCompassOrientation(heading);
|
switch (config.display.compass_orientation) {
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||||
|
heading += 90;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||||
|
heading += 180;
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||||
|
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||||
|
heading += 270;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->setHeading(heading);
|
screen->setHeading(heading);
|
||||||
#endif
|
#endif
|
||||||
@@ -130,16 +169,26 @@ int32_t ICM20948Sensor::runOnce()
|
|||||||
void ICM20948Sensor::calibrate(uint16_t forSeconds)
|
void ICM20948Sensor::calibrate(uint16_t forSeconds)
|
||||||
{
|
{
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||||
LOG_DEBUG("ICM20948 cal start %is", forSeconds);
|
LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f",
|
||||||
|
highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||||
|
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||||
if (sensor->dataReady()) {
|
if (sensor->dataReady()) {
|
||||||
sensor->getAGMT();
|
sensor->getAGMT();
|
||||||
seedCalibrationExtrema(sensor->agmt.mag.axes.x, sensor->agmt.mag.axes.y, sensor->agmt.mag.axes.z, highestX, lowestX,
|
highestX = sensor->agmt.mag.axes.x;
|
||||||
highestY, lowestY, highestZ, lowestZ);
|
lowestX = sensor->agmt.mag.axes.x;
|
||||||
|
highestY = sensor->agmt.mag.axes.y;
|
||||||
|
lowestY = sensor->agmt.mag.axes.y;
|
||||||
|
highestZ = sensor->agmt.mag.axes.z;
|
||||||
|
lowestZ = sensor->agmt.mag.axes.z;
|
||||||
} else {
|
} else {
|
||||||
seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
startCalibrationWindow(forSeconds);
|
doCalibration = true;
|
||||||
|
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||||
|
endCalibrationAt = millis() + calibrateFor;
|
||||||
|
if (screen)
|
||||||
|
screen->setEndCalibration(endCalibrationAt);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@@ -265,6 +314,11 @@ bool ICM20948Singleton::setWakeOnMotion()
|
|||||||
status = intEnableWOM(true);
|
status = intEnableWOM(true);
|
||||||
LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString());
|
LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString());
|
||||||
return status == ICM_20948_Stat_Ok;
|
return status == ICM_20948_Stat_Ok;
|
||||||
|
|
||||||
|
// Clear any current interrupts
|
||||||
|
ICM20948_IRQ = false;
|
||||||
|
clearInterrupts();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ class ICM20948Sensor : public MotionSensor
|
|||||||
ICM20948Singleton *sensor = nullptr;
|
ICM20948Singleton *sensor = nullptr;
|
||||||
bool showingScreen = false;
|
bool showingScreen = false;
|
||||||
bool isAsleep = false;
|
bool isAsleep = false;
|
||||||
static constexpr const char *compassCalibrationFileName = "/prefs/compass_icm20948.dat";
|
|
||||||
#ifdef MUZI_BASE
|
#ifdef MUZI_BASE
|
||||||
float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000,
|
float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000,
|
||||||
lowestZ = 98.000000;
|
lowestZ = 98.000000;
|
||||||
@@ -104,4 +103,4 @@ class ICM20948Sensor : public MotionSensor
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+14
-245
@@ -1,37 +1,10 @@
|
|||||||
#include "MotionSensor.h"
|
#include "MotionSensor.h"
|
||||||
#include "FSCommon.h"
|
|
||||||
#include "SPILock.h"
|
|
||||||
#include "SafeFile.h"
|
|
||||||
#include "graphics/draw/CompassRenderer.h"
|
#include "graphics/draw/CompassRenderer.h"
|
||||||
|
|
||||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
|
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
|
||||||
|
|
||||||
char timeRemainingBuffer[12];
|
char timeRemainingBuffer[12];
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr uint32_t COMPASS_CALIBRATION_MAGIC = 0x4D43414CL; // "MCAL"
|
|
||||||
constexpr uint16_t COMPASS_CALIBRATION_VERSION = 1;
|
|
||||||
|
|
||||||
struct CompassCalibrationRecord {
|
|
||||||
uint32_t magic;
|
|
||||||
uint16_t version;
|
|
||||||
uint16_t reserved;
|
|
||||||
float highestX;
|
|
||||||
float lowestX;
|
|
||||||
float highestY;
|
|
||||||
float lowestY;
|
|
||||||
float highestZ;
|
|
||||||
float lowestZ;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool isRangeValid(float highest, float lowest)
|
|
||||||
{
|
|
||||||
// NaN/Inf guard without pulling in extra math helpers.
|
|
||||||
return (highest == highest) && (lowest == lowest) && (highest > lowest);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// screen is defined in main.cpp
|
// screen is defined in main.cpp
|
||||||
extern graphics::Screen *screen;
|
extern graphics::Screen *screen;
|
||||||
|
|
||||||
@@ -59,237 +32,33 @@ ScanI2C::I2CPort MotionSensor::devicePort()
|
|||||||
return device.address.port;
|
return device.address.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MotionSensor::saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY,
|
|
||||||
float highestZ, float lowestZ)
|
|
||||||
{
|
|
||||||
#ifdef FSCom
|
|
||||||
if (!isRangeValid(highestX, lowestX) || !isRangeValid(highestY, lowestY) || !isRangeValid(highestZ, lowestZ)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
FSCom.mkdir("/prefs");
|
|
||||||
CompassCalibrationRecord record = {
|
|
||||||
COMPASS_CALIBRATION_MAGIC, COMPASS_CALIBRATION_VERSION, 0, highestX, lowestX, highestY, lowestY, highestZ, lowestZ};
|
|
||||||
|
|
||||||
auto file = SafeFile(filePath, true);
|
|
||||||
const size_t written = file.write(reinterpret_cast<const uint8_t *>(&record), sizeof(record));
|
|
||||||
return (written == sizeof(record)) && file.close();
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MotionSensor::loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY,
|
|
||||||
float &lowestY, float &highestZ, float &lowestZ)
|
|
||||||
{
|
|
||||||
#ifdef FSCom
|
|
||||||
CompassCalibrationRecord record = {};
|
|
||||||
size_t bytesRead = 0;
|
|
||||||
|
|
||||||
spiLock->lock();
|
|
||||||
auto file = FSCom.open(filePath, FILE_O_READ);
|
|
||||||
if (!file) {
|
|
||||||
spiLock->unlock();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bytesRead = file.read(reinterpret_cast<uint8_t *>(&record), sizeof(record));
|
|
||||||
file.close();
|
|
||||||
spiLock->unlock();
|
|
||||||
|
|
||||||
const bool headerValid = (bytesRead == sizeof(record)) && (record.magic == COMPASS_CALIBRATION_MAGIC) &&
|
|
||||||
(record.version == COMPASS_CALIBRATION_VERSION) && (record.reserved == 0U);
|
|
||||||
const bool rangeValid = isRangeValid(record.highestX, record.lowestX) && isRangeValid(record.highestY, record.lowestY) &&
|
|
||||||
isRangeValid(record.highestZ, record.lowestZ);
|
|
||||||
if (!headerValid || !rangeValid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
highestX = record.highestX;
|
|
||||||
lowestX = record.lowestX;
|
|
||||||
highestY = record.highestY;
|
|
||||||
lowestY = record.lowestY;
|
|
||||||
highestZ = record.highestZ;
|
|
||||||
lowestZ = record.lowestZ;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void MotionSensor::beginCalibrationDisplay(bool &showingScreen)
|
|
||||||
{
|
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
|
||||||
if (!showingScreen) {
|
|
||||||
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
|
||||||
showingScreen = true;
|
|
||||||
if (screen)
|
|
||||||
screen->startAlert((FrameCallback)drawFrameCalibration);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
(void)showingScreen;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void MotionSensor::finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX,
|
|
||||||
float highestY, float lowestY, float highestZ, float lowestZ)
|
|
||||||
{
|
|
||||||
const uint32_t now = millis();
|
|
||||||
if ((int32_t)(now - endCalibrationAt) < 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
doCalibration = false;
|
|
||||||
endCalibrationAt = 0;
|
|
||||||
showingScreen = false;
|
|
||||||
saveMagnetometerCalibration(filePath, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
|
||||||
|
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
|
||||||
if (screen) {
|
|
||||||
screen->setEndCalibration(0);
|
|
||||||
screen->endAlert();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void MotionSensor::startCalibrationWindow(uint16_t forSeconds)
|
|
||||||
{
|
|
||||||
doCalibration = true;
|
|
||||||
const uint32_t calibrateFor = static_cast<uint32_t>(forSeconds) * 1000U;
|
|
||||||
endCalibrationAt = millis() + calibrateFor;
|
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
|
||||||
if (screen)
|
|
||||||
screen->setEndCalibration(endCalibrationAt);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void MotionSensor::seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
|
||||||
float &lowestY, float &highestZ, float &lowestZ)
|
|
||||||
{
|
|
||||||
highestX = lowestX = x;
|
|
||||||
highestY = lowestY = y;
|
|
||||||
highestZ = lowestZ = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MotionSensor::updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
|
||||||
float &lowestY, float &highestZ, float &lowestZ)
|
|
||||||
{
|
|
||||||
if (x > highestX)
|
|
||||||
highestX = x;
|
|
||||||
if (x < lowestX)
|
|
||||||
lowestX = x;
|
|
||||||
if (y > highestY)
|
|
||||||
highestY = y;
|
|
||||||
if (y < lowestY)
|
|
||||||
lowestY = y;
|
|
||||||
if (z > highestZ)
|
|
||||||
highestZ = z;
|
|
||||||
if (z < lowestZ)
|
|
||||||
lowestZ = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
float MotionSensor::applyCompassOrientation(float heading)
|
|
||||||
{
|
|
||||||
switch (config.display.compass_orientation) {
|
|
||||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
|
||||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
|
||||||
return heading + 90;
|
|
||||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
|
||||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
|
||||||
return heading + 180;
|
|
||||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
|
||||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
|
||||||
return heading + 270;
|
|
||||||
default:
|
|
||||||
return heading;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||||
void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
if (screen == nullptr)
|
if (screen == nullptr)
|
||||||
return;
|
return;
|
||||||
|
// int x_offset = display->width() / 2;
|
||||||
|
// int y_offset = display->height() <= 80 ? 0 : 32;
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_MEDIUM);
|
||||||
|
display->drawString(x, y, "Calibrating\nCompass");
|
||||||
|
|
||||||
const int16_t width = display->getWidth();
|
uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000;
|
||||||
const int16_t height = display->getHeight();
|
sprintf(timeRemainingBuffer, "( %02d )", timeRemaining);
|
||||||
const bool compactLayout = (height <= 80);
|
display->setFont(FONT_SMALL);
|
||||||
const int16_t margin = 4;
|
display->drawString(x, y + 40, timeRemainingBuffer);
|
||||||
|
|
||||||
const uint32_t now = millis();
|
|
||||||
const uint32_t endCalibrationAt = screen->getEndCalibration();
|
|
||||||
uint32_t timeRemaining = 0;
|
|
||||||
if (endCalibrationAt > now) {
|
|
||||||
timeRemaining = (endCalibrationAt - now + 999) / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t compassX = 0, compassY = 0;
|
int16_t compassX = 0, compassY = 0;
|
||||||
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(width, height);
|
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
|
||||||
const int16_t compassRadius = compassDiam / 2;
|
|
||||||
|
|
||||||
// coordinates for the center of the compass/circle
|
// coordinates for the center of the compass/circle
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||||
compassX = x + width - compassRadius - margin;
|
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||||
compassY = y + height / 2;
|
compassY = y + display->getHeight() / 2;
|
||||||
} else {
|
} else {
|
||||||
compassX = x + width - compassRadius - margin;
|
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||||
compassY = y + FONT_HEIGHT_SMALL + (height - FONT_HEIGHT_SMALL) / 2;
|
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int16_t textLeft = x + 1;
|
|
||||||
const int16_t textRight = compassX - compassRadius - margin;
|
|
||||||
const int16_t textWidth = textRight - textLeft;
|
|
||||||
int16_t lineY = y;
|
|
||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
||||||
if (textWidth > 12) {
|
|
||||||
const char *title = "Cal";
|
|
||||||
const char *line1 = "Figure-8";
|
|
||||||
const char *line2 = "Rotate axes";
|
|
||||||
const char *line3 = "Away from metal";
|
|
||||||
|
|
||||||
display->setFont(FONT_SMALL);
|
|
||||||
if (!compactLayout && display->getStringWidth("Compass Calibration") <= textWidth) {
|
|
||||||
display->setFont(FONT_MEDIUM);
|
|
||||||
title = "Compass Calibration";
|
|
||||||
line1 = "Move in figure-8";
|
|
||||||
line2 = "Rotate all axes";
|
|
||||||
line3 = "Keep from metal";
|
|
||||||
display->drawString(textLeft, lineY, title);
|
|
||||||
lineY += FONT_HEIGHT_MEDIUM;
|
|
||||||
display->setFont(FONT_SMALL);
|
|
||||||
} else if (display->getStringWidth("Compass Cal") <= textWidth) {
|
|
||||||
title = "Compass Cal";
|
|
||||||
if (textWidth >= display->getStringWidth("Move in figure-8")) {
|
|
||||||
line1 = "Move in figure-8";
|
|
||||||
line2 = "Rotate all axes";
|
|
||||||
line3 = "Keep from metal";
|
|
||||||
}
|
|
||||||
display->drawString(textLeft, lineY, title);
|
|
||||||
lineY += FONT_HEIGHT_SMALL;
|
|
||||||
} else {
|
|
||||||
display->drawString(textLeft, lineY, title);
|
|
||||||
lineY += FONT_HEIGHT_SMALL;
|
|
||||||
}
|
|
||||||
|
|
||||||
display->drawString(textLeft, lineY, line1);
|
|
||||||
lineY += FONT_HEIGHT_SMALL;
|
|
||||||
display->drawString(textLeft, lineY, line2);
|
|
||||||
lineY += FONT_HEIGHT_SMALL;
|
|
||||||
if (!compactLayout || textWidth >= display->getStringWidth(line3)) {
|
|
||||||
display->drawString(textLeft, lineY, line3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textWidth >= display->getStringWidth("000s left")) {
|
|
||||||
snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus left", (unsigned long)timeRemaining);
|
|
||||||
} else {
|
|
||||||
snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus", (unsigned long)timeRemaining);
|
|
||||||
}
|
|
||||||
display->setFont(FONT_SMALL);
|
|
||||||
if (textWidth > 12) {
|
|
||||||
display->drawString(textLeft, y + height - FONT_HEIGHT_SMALL - 1, timeRemainingBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
display->drawCircle(compassX, compassY, compassDiam / 2);
|
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2));
|
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#ifndef _MOTION_SENSOR_H_
|
#ifndef _MOTION_SENSOR_H_
|
||||||
#define _MOTION_SENSOR_H_
|
#define _MOTION_SENSOR_H_
|
||||||
|
|
||||||
#define MOTION_SENSOR_CHECK_INTERVAL_MS 50
|
#define MOTION_SENSOR_CHECK_INTERVAL_MS 100
|
||||||
#define MOTION_SENSOR_CLICK_THRESHOLD 40
|
#define MOTION_SENSOR_CLICK_THRESHOLD 40
|
||||||
|
|
||||||
#include "../configuration.h"
|
#include "../configuration.h"
|
||||||
@@ -54,20 +54,6 @@ class MotionSensor
|
|||||||
static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY,
|
|
||||||
float highestZ, float lowestZ);
|
|
||||||
bool loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY, float &lowestY,
|
|
||||||
float &highestZ, float &lowestZ);
|
|
||||||
void beginCalibrationDisplay(bool &showingScreen);
|
|
||||||
void finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX, float highestY,
|
|
||||||
float lowestY, float highestZ, float lowestZ);
|
|
||||||
void startCalibrationWindow(uint16_t forSeconds);
|
|
||||||
static void seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
|
||||||
float &lowestY, float &highestZ, float &lowestZ);
|
|
||||||
static void updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
|
||||||
float &lowestY, float &highestZ, float &lowestZ);
|
|
||||||
static float applyCompassOrientation(float heading);
|
|
||||||
|
|
||||||
ScanI2C::FoundDevice device;
|
ScanI2C::FoundDevice device;
|
||||||
|
|
||||||
// Do calibration if true
|
// Do calibration if true
|
||||||
@@ -77,4 +63,4 @@ class MotionSensor
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+2
-2
@@ -322,8 +322,8 @@ bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &cli
|
|||||||
pubSub.setClient(client);
|
pubSub.setClient(client);
|
||||||
pubSub.setServer(config.serverAddr.c_str(), config.serverPort);
|
pubSub.setServer(config.serverAddr.c_str(), config.serverPort);
|
||||||
|
|
||||||
LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: ***", config.serverAddr.c_str(),
|
LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(),
|
||||||
config.serverPort, config.mqttUsername);
|
config.serverPort, config.mqttUsername, config.mqttPassword);
|
||||||
|
|
||||||
// Generate node ID from nodenum for client identification
|
// Generate node ID from nodenum for client identification
|
||||||
std::string nodeId = nodeDB->getNodeId();
|
std::string nodeId = nodeDB->getNodeId();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "NRF52Bluetooth.h"
|
#include "NRF52Bluetooth.h"
|
||||||
#include "BLEDfuSecure.h"
|
#include "BLEDfuSecure.h"
|
||||||
#include "BluetoothCommon.h"
|
#include "BluetoothCommon.h"
|
||||||
|
#include "HardwareRNG.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -272,9 +273,13 @@ void NRF52Bluetooth::setup()
|
|||||||
Bluefruit.setTxPower(NRF52_BLE_TX_POWER);
|
Bluefruit.setTxPower(NRF52_BLE_TX_POWER);
|
||||||
#endif
|
#endif
|
||||||
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||||
configuredPasskey = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN
|
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN) {
|
||||||
? config.bluetooth.fixed_pin
|
configuredPasskey = config.bluetooth.fixed_pin;
|
||||||
: random(100000, 999999);
|
} else {
|
||||||
|
uint32_t hwrand = 0;
|
||||||
|
HardwareRNG::fill(reinterpret_cast<uint8_t *>(&hwrand), sizeof(hwrand));
|
||||||
|
configuredPasskey = hwrand % 900000u + 100000u;
|
||||||
|
}
|
||||||
auto pinString = std::to_string(configuredPasskey);
|
auto pinString = std::to_string(configuredPasskey);
|
||||||
LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey);
|
LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey);
|
||||||
Bluefruit.Security.setPIN(pinString.c_str());
|
Bluefruit.Security.setPIN(pinString.c_str());
|
||||||
|
|||||||
@@ -182,10 +182,6 @@ void portduinoSetup()
|
|||||||
// Force stdout to be line buffered
|
// Force stdout to be line buffered
|
||||||
setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer));
|
setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer));
|
||||||
|
|
||||||
// We do this super early so that we can log from the rest of the init code
|
|
||||||
concurrency::hasBeenSetup = true;
|
|
||||||
consoleInit();
|
|
||||||
|
|
||||||
if (portduino_config.force_simradio == true) {
|
if (portduino_config.force_simradio == true) {
|
||||||
portduino_config.lora_module = use_simradio;
|
portduino_config.lora_module = use_simradio;
|
||||||
} else if (configPath != nullptr) {
|
} else if (configPath != nullptr) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
.globl HardFault_Handler
|
|
||||||
.syntax unified
|
|
||||||
.thumb
|
|
||||||
|
|
||||||
.type HardFault_Handler, %function
|
|
||||||
HardFault_Handler:
|
|
||||||
tst lr, #4
|
|
||||||
ite eq
|
|
||||||
mrseq r0, msp
|
|
||||||
mrsne r0, psp
|
|
||||||
b HardFault_Handler_C
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user