mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-21 10:42:35 +10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f65f3e5645 | ||
|
|
56c897e826 | ||
|
|
2cc13a1132 | ||
|
|
031f332ec1 | ||
|
|
c78abc2073 |
@@ -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:
|
||||||
|
|||||||
@@ -86,13 +86,7 @@ jobs:
|
|||||||
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
||||||
|
|
||||||
- name: PlatformIO Tests
|
- name: PlatformIO Tests
|
||||||
run: |
|
run: platformio test -e coverage -v --junit-output-path testreport.xml
|
||||||
set -o pipefail
|
|
||||||
# Filter out SKIPPED summary rows for hardware variants that can't run on the
|
|
||||||
# native host. They flood the log and make it harder to spot real failures.
|
|
||||||
# The JUnit XML is written directly to testreport.xml before the pipe, so
|
|
||||||
# the test artifact is unaffected.
|
|
||||||
platformio test -e coverage -v --junit-output-path testreport.xml 2>&1 | grep -v "[[:space:]]SKIPPED$"
|
|
||||||
|
|
||||||
- name: Save test results
|
- name: Save test results
|
||||||
if: always() # run this step even if previous step failed
|
if: always() # run this step even if previous step failed
|
||||||
|
|||||||
@@ -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
|
|
||||||
+2
-2
@@ -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
|
||||||
@@ -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: d004f503bb...4d5b500df5
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-133
@@ -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,17 +717,37 @@ bool Power::setup()
|
|||||||
found = true;
|
found = true;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
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
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,97 +1026,6 @@ 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
|
||||||
*
|
*
|
||||||
@@ -1403,6 +1303,8 @@ 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)
|
||||||
|
|||||||
+7
-139
@@ -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
|
||||||
// ==============================
|
// ==============================
|
||||||
@@ -276,42 +220,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
|||||||
ui->update();
|
ui->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
|
|
||||||
int maxValueTenths, std::function<void(int)> bannerCallback)
|
|
||||||
{
|
|
||||||
#ifdef USE_EINK
|
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
|
||||||
#endif
|
|
||||||
if (minValueTenths > maxValueTenths) {
|
|
||||||
int temp = minValueTenths;
|
|
||||||
minValueTenths = maxValueTenths;
|
|
||||||
maxValueTenths = temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialValueTenths < minValueTenths) {
|
|
||||||
initialValueTenths = minValueTenths;
|
|
||||||
} else if (initialValueTenths > maxValueTenths) {
|
|
||||||
initialValueTenths = maxValueTenths;
|
|
||||||
}
|
|
||||||
|
|
||||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
|
||||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
|
||||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
|
||||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
|
||||||
NotificationRenderer::pauseBanner = false;
|
|
||||||
NotificationRenderer::curSelected = 0;
|
|
||||||
NotificationRenderer::current_notification_type = notificationTypeEnum::signed_decimal_picker;
|
|
||||||
NotificationRenderer::signedDecimalValueTenths = static_cast<int16_t>(initialValueTenths);
|
|
||||||
NotificationRenderer::signedDecimalMinTenths = static_cast<int16_t>(minValueTenths);
|
|
||||||
NotificationRenderer::signedDecimalMaxTenths = static_cast<int16_t>(maxValueTenths);
|
|
||||||
NotificationRenderer::signedDecimalIsNegative = (initialValueTenths < 0);
|
|
||||||
|
|
||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
|
||||||
ui->setTargetFPS(60);
|
|
||||||
ui->update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
std::function<void(const std::string &)> textCallback)
|
std::function<void(const std::string &)> textCallback)
|
||||||
{
|
{
|
||||||
@@ -364,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;
|
||||||
|
|
||||||
@@ -390,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;
|
||||||
}
|
}
|
||||||
@@ -1038,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();
|
||||||
@@ -1305,8 +1177,6 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
fsi.positions.focusedModule = numframes;
|
fsi.positions.focusedModule = numframes;
|
||||||
if (m && m == waypointModule)
|
if (m && m == waypointModule)
|
||||||
fsi.positions.waypoint = numframes;
|
fsi.positions.waypoint = numframes;
|
||||||
if (m && strcmp(m->getName(), "EnvironmentTelemetry") == 0)
|
|
||||||
fsi.positions.environment = numframes;
|
|
||||||
|
|
||||||
indicatorIcons.push_back(icon_module);
|
indicatorIcons.push_back(icon_module);
|
||||||
numframes++;
|
numframes++;
|
||||||
@@ -1327,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1356,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) {
|
||||||
@@ -2020,8 +1890,6 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
menuHandler::textMessageBaseMenu();
|
menuHandler::textMessageBaseMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.environment) {
|
|
||||||
menuHandler::environmentTelemetryBaseMenu();
|
|
||||||
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
||||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, signed_decimal_picker, text_input };
|
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||||
|
|
||||||
struct BannerOverlayOptions {
|
struct BannerOverlayOptions {
|
||||||
const char *message;
|
const char *message;
|
||||||
@@ -312,8 +312,6 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||||
void showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
|
|
||||||
int maxValueTenths, std::function<void(int)> bannerCallback);
|
|
||||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
std::function<void(const std::string &)> textCallback);
|
std::function<void(const std::string &)> textCallback);
|
||||||
|
|
||||||
@@ -332,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; }
|
||||||
@@ -709,7 +711,6 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t firstFavorite = 255;
|
uint8_t firstFavorite = 255;
|
||||||
uint8_t lastFavorite = 255;
|
uint8_t lastFavorite = 255;
|
||||||
uint8_t lora = 255;
|
uint8_t lora = 255;
|
||||||
uint8_t environment = 255;
|
|
||||||
} positions;
|
} positions;
|
||||||
|
|
||||||
uint8_t frameCount = 0;
|
uint8_t frameCount = 0;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -58,32 +56,6 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
|||||||
return bannerOptions;
|
return bannerOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr float kTemperatureOffsetMinC = -20.0f;
|
|
||||||
constexpr float kTemperatureOffsetMaxC = 20.0f;
|
|
||||||
constexpr float kTemperatureOffsetDeltaFPerC = 1.8f;
|
|
||||||
|
|
||||||
bool useImperialTemperatureOffsetUnits()
|
|
||||||
{
|
|
||||||
return config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL ||
|
|
||||||
moduleConfig.telemetry.environment_display_fahrenheit;
|
|
||||||
}
|
|
||||||
|
|
||||||
float clampTemperatureOffsetC(float offsetC)
|
|
||||||
{
|
|
||||||
if (offsetC < kTemperatureOffsetMinC) {
|
|
||||||
return kTemperatureOffsetMinC;
|
|
||||||
}
|
|
||||||
if (offsetC > kTemperatureOffsetMaxC) {
|
|
||||||
return kTemperatureOffsetMaxC;
|
|
||||||
}
|
|
||||||
return offsetC;
|
|
||||||
}
|
|
||||||
|
|
||||||
int toTenthsRounded(float value)
|
|
||||||
{
|
|
||||||
return (value >= 0.0f) ? static_cast<int>(value * 10.0f + 0.5f) : static_cast<int>(value * 10.0f - 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||||
@@ -187,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;
|
||||||
@@ -218,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;
|
||||||
@@ -283,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;
|
||||||
@@ -336,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);
|
||||||
@@ -374,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);
|
||||||
@@ -1035,52 +1008,6 @@ void menuHandler::textMessageMenu()
|
|||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::environmentTelemetryBaseMenu()
|
|
||||||
{
|
|
||||||
enum optionsNumbers { Back, SetTempOffset };
|
|
||||||
|
|
||||||
static const char *optionsArray[] = {"Back", "Set Temp Offset"};
|
|
||||||
static int optionsEnumArray[] = {Back, SetTempOffset};
|
|
||||||
|
|
||||||
BannerOverlayOptions bannerOptions;
|
|
||||||
bannerOptions.message = "Env Actions";
|
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
|
||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
|
||||||
bannerOptions.optionsCount = 2;
|
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
|
||||||
if (selected == SetTempOffset) {
|
|
||||||
menuQueue = EnvironmentTempOffsetPicker;
|
|
||||||
screen->runNow();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
screen->showOverlayBanner(bannerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
void menuHandler::environmentTemperatureOffsetPicker()
|
|
||||||
{
|
|
||||||
static char pickerTitle[40];
|
|
||||||
|
|
||||||
const bool useImperial = useImperialTemperatureOffsetUnits();
|
|
||||||
const char displayUnit = useImperial ? 'F' : 'C';
|
|
||||||
const float currentOffsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
|
|
||||||
const float displayOffset = useImperial ? (currentOffsetC * kTemperatureOffsetDeltaFPerC) : currentOffsetC;
|
|
||||||
const float minDisplay = useImperial ? (kTemperatureOffsetMinC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMinC;
|
|
||||||
const float maxDisplay = useImperial ? (kTemperatureOffsetMaxC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMaxC;
|
|
||||||
|
|
||||||
snprintf(pickerTitle, sizeof(pickerTitle), "Set Temp Offset (%c)", displayUnit);
|
|
||||||
screen->showSignedDecimalPicker(pickerTitle, 60000, toTenthsRounded(displayOffset), toTenthsRounded(minDisplay),
|
|
||||||
toTenthsRounded(maxDisplay), [useImperial](int pickedTenths) -> void {
|
|
||||||
float selectedOffset = static_cast<float>(pickedTenths) / 10.0f;
|
|
||||||
if (useImperial) {
|
|
||||||
selectedOffset /= kTemperatureOffsetDeltaFPerC;
|
|
||||||
}
|
|
||||||
|
|
||||||
moduleConfig.telemetry.environment_temperature_offset_c =
|
|
||||||
clampTemperatureOffsetC(selectedOffset);
|
|
||||||
nodeDB->saveToDisk(SEGMENT_MODULECONFIG);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void menuHandler::textMessageBaseMenu()
|
void menuHandler::textMessageBaseMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
|
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
|
||||||
@@ -2290,6 +2217,9 @@ void menuHandler::testMenu()
|
|||||||
static int optionsEnumArray[5] = {Back};
|
static int optionsEnumArray[5] = {Back};
|
||||||
int options = 1;
|
int options = 1;
|
||||||
|
|
||||||
|
optionsArray[options] = "Number Picker";
|
||||||
|
optionsEnumArray[options++] = NumberPicker;
|
||||||
|
|
||||||
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
||||||
optionsEnumArray[options++] = ShowChirpy;
|
optionsEnumArray[options++] = ShowChirpy;
|
||||||
#ifdef HAS_I2S
|
#ifdef HAS_I2S
|
||||||
@@ -2303,7 +2233,10 @@ void menuHandler::testMenu()
|
|||||||
bannerOptions.optionsCount = options;
|
bannerOptions.optionsCount = options;
|
||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == ShowChirpy) {
|
if (selected == NumberPicker) {
|
||||||
|
menuQueue = NumberTest;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == ShowChirpy) {
|
||||||
screen->toggleFrameVisibility("chirpy");
|
screen->toggleFrameVisibility("chirpy");
|
||||||
screen->setFrames(Screen::FOCUS_SYSTEM);
|
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||||
|
|
||||||
@@ -2319,6 +2252,12 @@ void menuHandler::testMenu()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void menuHandler::numberTest()
|
||||||
|
{
|
||||||
|
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
||||||
|
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::wifiBaseMenu()
|
void menuHandler::wifiBaseMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers { Back, Wifi_toggle };
|
enum optionsNumbers { Back, Wifi_toggle };
|
||||||
@@ -2814,8 +2753,8 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case TestMenu:
|
case TestMenu:
|
||||||
testMenu();
|
testMenu();
|
||||||
break;
|
break;
|
||||||
case EnvironmentTempOffsetPicker:
|
case NumberTest:
|
||||||
environmentTemperatureOffsetPicker();
|
numberTest();
|
||||||
break;
|
break;
|
||||||
case WifiToggleMenu:
|
case WifiToggleMenu:
|
||||||
wifiToggleMenu();
|
wifiToggleMenu();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class menuHandler
|
|||||||
ManageNodeMenu,
|
ManageNodeMenu,
|
||||||
RemoveFavorite,
|
RemoveFavorite,
|
||||||
TestMenu,
|
TestMenu,
|
||||||
EnvironmentTempOffsetPicker,
|
NumberTest,
|
||||||
WifiToggleMenu,
|
WifiToggleMenu,
|
||||||
BluetoothToggleMenu,
|
BluetoothToggleMenu,
|
||||||
ScreenOptionsMenu,
|
ScreenOptionsMenu,
|
||||||
@@ -78,7 +78,6 @@ class menuHandler
|
|||||||
static void deleteMessagesMenu();
|
static void deleteMessagesMenu();
|
||||||
static void homeBaseMenu();
|
static void homeBaseMenu();
|
||||||
static void textMessageBaseMenu();
|
static void textMessageBaseMenu();
|
||||||
static void environmentTelemetryBaseMenu();
|
|
||||||
static void systemBaseMenu();
|
static void systemBaseMenu();
|
||||||
static void favoriteBaseMenu();
|
static void favoriteBaseMenu();
|
||||||
static void positionBaseMenu();
|
static void positionBaseMenu();
|
||||||
@@ -102,7 +101,7 @@ class menuHandler
|
|||||||
static void removeFavoriteMenu();
|
static void removeFavoriteMenu();
|
||||||
static void traceRouteMenu();
|
static void traceRouteMenu();
|
||||||
static void testMenu();
|
static void testMenu();
|
||||||
static void environmentTemperatureOffsetPicker();
|
static void numberTest();
|
||||||
static void wifiBaseMenu();
|
static void wifiBaseMenu();
|
||||||
static void wifiToggleMenu();
|
static void wifiToggleMenu();
|
||||||
static void screenOptionsMenu();
|
static void screenOptionsMenu();
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -275,12 +239,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
|
|
||||||
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
||||||
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||||
constexpr int kBarCount = 4;
|
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||||
constexpr int kBarWidth = 2;
|
|
||||||
constexpr int kBarGap = 1;
|
|
||||||
|
|
||||||
int barsXOffset = columnWidth - barsOffset;
|
int barsXOffset = columnWidth - barsOffset;
|
||||||
int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth;
|
|
||||||
|
|
||||||
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
||||||
char nodeName[96];
|
char nodeName[96];
|
||||||
@@ -307,35 +268,28 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
// Draw signal strength bars
|
||||||
|
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||||
|
int barWidth = 2;
|
||||||
|
int barStartX = x + barsXOffset;
|
||||||
|
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||||
|
|
||||||
// Show signal only for direct neighbors (0 hops)
|
for (int b = 0; b < 4; b++) {
|
||||||
if (isZeroHop) {
|
if (b < bars) {
|
||||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
int height = (b * 2);
|
||||||
int barStartX = x + barsXOffset;
|
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
|
||||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
|
||||||
|
|
||||||
for (int b = 0; b < kBarCount; b++) {
|
|
||||||
if (b < bars) {
|
|
||||||
int height = (b * 2);
|
|
||||||
display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw hop count + hop icon
|
// Draw hop count
|
||||||
if (node->has_hops_away && node->hops_away > 0) {
|
char hopStr[6] = "";
|
||||||
char hopCount[6];
|
if (node->has_hops_away && node->hops_away > 0)
|
||||||
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
|
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
|
||||||
|
|
||||||
const int hopCountWidth = display->getStringWidth(hopCount);
|
if (hopStr[0] != '\0') {
|
||||||
const int gap = 1;
|
int rightEdge = x + columnWidth - hopOffset;
|
||||||
const int totalWidth = hopCountWidth + gap + hop_width;
|
int textWidth = display->getStringWidth(hopStr);
|
||||||
const int hopX = barsRightEdge - totalWidth;
|
display->drawString(rightEdge - textWidth, y, hopStr);
|
||||||
const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
|
||||||
|
|
||||||
display->drawString(hopX, y, hopCount);
|
|
||||||
display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,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)
|
||||||
@@ -476,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;
|
||||||
@@ -491,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;
|
||||||
@@ -525,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;
|
||||||
@@ -630,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;
|
||||||
@@ -825,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);
|
||||||
|
|
||||||
@@ -843,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);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <limits>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#if HAS_TRACKBALL
|
#if HAS_TRACKBALL
|
||||||
@@ -53,292 +52,16 @@ bool NotificationRenderer::pauseBanner = false;
|
|||||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||||
uint32_t NotificationRenderer::numDigits = 0;
|
uint32_t NotificationRenderer::numDigits = 0;
|
||||||
uint32_t NotificationRenderer::currentNumber = 0;
|
uint32_t NotificationRenderer::currentNumber = 0;
|
||||||
int16_t NotificationRenderer::signedDecimalValueTenths = 0;
|
|
||||||
int16_t NotificationRenderer::signedDecimalMinTenths = -999;
|
|
||||||
int16_t NotificationRenderer::signedDecimalMaxTenths = 999;
|
|
||||||
bool NotificationRenderer::signedDecimalIsNegative = false;
|
|
||||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||||
|
|
||||||
struct NumericSlotPickerState {
|
uint32_t pow_of_10(uint32_t n)
|
||||||
std::vector<uint8_t> digits;
|
|
||||||
bool hasSign = false;
|
|
||||||
bool isNegative = false;
|
|
||||||
uint8_t decimalDigits = 0;
|
|
||||||
int32_t minValue = 0;
|
|
||||||
int32_t maxValue = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
int32_t maxValueForDigits(uint8_t digitCount)
|
|
||||||
{
|
{
|
||||||
int64_t value = 0;
|
uint32_t ret = 1;
|
||||||
for (uint8_t i = 0; i < digitCount; i++) {
|
for (uint32_t i = 0; i < n; i++) {
|
||||||
value = (value * 10) + 9;
|
ret *= 10;
|
||||||
if (value > std::numeric_limits<int32_t>::max()) {
|
|
||||||
return std::numeric_limits<int32_t>::max();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return static_cast<int32_t>(value);
|
return ret;
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t pickerSlotCount(const NumericSlotPickerState &state)
|
|
||||||
{
|
|
||||||
return static_cast<uint8_t>(state.digits.size() + (state.hasSign ? 1 : 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t clampPickerValue(int32_t value, int32_t minValue, int32_t maxValue)
|
|
||||||
{
|
|
||||||
if (value < minValue) {
|
|
||||||
return minValue;
|
|
||||||
}
|
|
||||||
if (value > maxValue) {
|
|
||||||
return maxValue;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t combinePickerDigits(const std::vector<uint8_t> &digits)
|
|
||||||
{
|
|
||||||
uint32_t value = 0;
|
|
||||||
for (const uint8_t digit : digits) {
|
|
||||||
value = (value * 10) + digit;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void splitPickerDigits(uint32_t value, std::vector<uint8_t> &digits)
|
|
||||||
{
|
|
||||||
if (digits.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = static_cast<int>(digits.size()) - 1; i >= 0; i--) {
|
|
||||||
digits[static_cast<size_t>(i)] = static_cast<uint8_t>(value % 10);
|
|
||||||
value /= 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t composePickerValue(const NumericSlotPickerState &state)
|
|
||||||
{
|
|
||||||
int64_t value = static_cast<int64_t>(combinePickerDigits(state.digits));
|
|
||||||
if (state.hasSign && state.isNegative && value != 0) {
|
|
||||||
value = -value;
|
|
||||||
}
|
|
||||||
return clampPickerValue(static_cast<int32_t>(value), state.minValue, state.maxValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
void normalizePickerState(NumericSlotPickerState &state)
|
|
||||||
{
|
|
||||||
const bool keepNegativeZero = state.isNegative;
|
|
||||||
const int32_t clampedValue = composePickerValue(state);
|
|
||||||
const uint32_t absValue =
|
|
||||||
(clampedValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(clampedValue)) : static_cast<uint32_t>(clampedValue);
|
|
||||||
splitPickerDigits(absValue, state.digits);
|
|
||||||
|
|
||||||
if (!state.hasSign) {
|
|
||||||
state.isNegative = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clampedValue < 0) {
|
|
||||||
state.isNegative = true;
|
|
||||||
} else if (clampedValue > 0) {
|
|
||||||
state.isNegative = false;
|
|
||||||
} else {
|
|
||||||
// Keep selected sign for zero so users can pick +/- before entering digits.
|
|
||||||
state.isNegative = keepNegativeZero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isPickerIncrementEvent(uint8_t eventType)
|
|
||||||
{
|
|
||||||
return eventType == INPUT_BROKER_UP || eventType == INPUT_BROKER_ALT_PRESS || eventType == INPUT_BROKER_UP_LONG;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isPickerDecrementEvent(uint8_t eventType)
|
|
||||||
{
|
|
||||||
return eventType == INPUT_BROKER_DOWN || eventType == INPUT_BROKER_USER_PRESS || eventType == INPUT_BROKER_DOWN_LONG;
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyPickerDelta(NumericSlotPickerState &state, int8_t selectedSlot, int8_t delta)
|
|
||||||
{
|
|
||||||
const uint8_t slotCount = pickerSlotCount(state);
|
|
||||||
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.hasSign && selectedSlot == 0) {
|
|
||||||
state.isNegative = !state.isNegative;
|
|
||||||
normalizePickerState(state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int8_t digitOffset = state.hasSign ? 1 : 0;
|
|
||||||
const int8_t digitIndex = selectedSlot - digitOffset;
|
|
||||||
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t &digit = state.digits[static_cast<size_t>(digitIndex)];
|
|
||||||
if (delta > 0) {
|
|
||||||
digit = static_cast<uint8_t>((digit + 1) % 10);
|
|
||||||
} else {
|
|
||||||
digit = static_cast<uint8_t>((digit == 0) ? 9 : (digit - 1));
|
|
||||||
}
|
|
||||||
normalizePickerState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool applyPickerKeypress(NumericSlotPickerState &state, int8_t selectedSlot, char key)
|
|
||||||
{
|
|
||||||
const uint8_t slotCount = pickerSlotCount(state);
|
|
||||||
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.hasSign && selectedSlot == 0 && (key == '+' || key == '-')) {
|
|
||||||
state.isNegative = (key == '-');
|
|
||||||
normalizePickerState(state);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key < '0' || key > '9') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int8_t digitOffset = state.hasSign ? 1 : 0;
|
|
||||||
const int8_t digitIndex = selectedSlot - digitOffset;
|
|
||||||
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.digits[static_cast<size_t>(digitIndex)] = static_cast<uint8_t>(key - '0');
|
|
||||||
normalizePickerState(state);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void parseBannerLines(const char *lineStarts[MAX_LINES + 1], uint16_t &lineCount)
|
|
||||||
{
|
|
||||||
lineCount = 0;
|
|
||||||
char *alertEnd = NotificationRenderer::alertBannerMessage +
|
|
||||||
strnlen(NotificationRenderer::alertBannerMessage, sizeof(NotificationRenderer::alertBannerMessage));
|
|
||||||
lineStarts[lineCount] = NotificationRenderer::alertBannerMessage;
|
|
||||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
|
||||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
|
||||||
if (lineStarts[lineCount + 1][0] == '\n') {
|
|
||||||
lineStarts[lineCount + 1] += 1;
|
|
||||||
}
|
|
||||||
lineCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void buildNumericPickerDisplay(const NumericSlotPickerState &state, std::string &formattedValue,
|
|
||||||
std::vector<int16_t> &slotCharIndexBySlot)
|
|
||||||
{
|
|
||||||
const uint8_t slotCount = pickerSlotCount(state);
|
|
||||||
slotCharIndexBySlot.assign(slotCount, -1);
|
|
||||||
|
|
||||||
formattedValue = " ";
|
|
||||||
formattedValue.reserve(24);
|
|
||||||
int8_t slot = 0;
|
|
||||||
|
|
||||||
if (state.hasSign) {
|
|
||||||
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
|
|
||||||
formattedValue += state.isNegative ? '-' : '+';
|
|
||||||
formattedValue += ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t digitCount = state.digits.size();
|
|
||||||
const size_t decimalBreak =
|
|
||||||
(state.decimalDigits > 0 && state.decimalDigits < digitCount) ? (digitCount - state.decimalDigits) : digitCount;
|
|
||||||
for (size_t i = 0; i < digitCount; i++) {
|
|
||||||
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
|
|
||||||
formattedValue += static_cast<char>('0' + state.digits[i]);
|
|
||||||
formattedValue += ' ';
|
|
||||||
|
|
||||||
if (state.decimalDigits > 0 && i + 1 == decimalBreak) {
|
|
||||||
formattedValue += '.';
|
|
||||||
formattedValue += ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number picker renderer
|
|
||||||
// Reuse by building `formattedValue` as displayed, and mapping each editable slot
|
|
||||||
// to its character index in that string via `slotCharIndexBySlot`.
|
|
||||||
void NumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lineStarts[MAX_LINES + 1], uint16_t lineCount,
|
|
||||||
const std::string &formattedValue, const int16_t *slotCharIndexBySlot, uint8_t slotCount, int8_t selectedSlot)
|
|
||||||
{
|
|
||||||
if (formattedValue.empty() || slotCharIndexBySlot == nullptr || slotCount == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string spacer(formattedValue.size(), ' ');
|
|
||||||
uint16_t totalLines = lineCount + 3;
|
|
||||||
const char *linePointers[totalLines + 1] = {0};
|
|
||||||
for (uint16_t i = 0; i < lineCount; i++) {
|
|
||||||
linePointers[i] = lineStarts[i];
|
|
||||||
}
|
|
||||||
const uint16_t topGuideLineIndex = lineCount;
|
|
||||||
linePointers[lineCount++] = spacer.c_str();
|
|
||||||
const uint16_t valueLineIndex = lineCount;
|
|
||||||
linePointers[lineCount++] = formattedValue.c_str();
|
|
||||||
const uint16_t bottomGuideLineIndex = lineCount;
|
|
||||||
linePointers[lineCount++] = spacer.c_str();
|
|
||||||
|
|
||||||
NotificationRenderer::drawNotificationBox(display, state, linePointers, totalLines, 0);
|
|
||||||
|
|
||||||
constexpr uint16_t hPadding = 5;
|
|
||||||
constexpr uint16_t vPadding = 2;
|
|
||||||
uint16_t maxWidth = 0;
|
|
||||||
uint16_t lineWidths[MAX_LINES + 3] = {0};
|
|
||||||
for (uint16_t i = 0; i < totalLines; i++) {
|
|
||||||
const uint16_t lineLength = static_cast<uint16_t>(strlen(linePointers[i]));
|
|
||||||
lineWidths[i] = display->getStringWidth(linePointers[i], lineLength, true);
|
|
||||||
if (lineWidths[i] > maxWidth) {
|
|
||||||
maxWidth = lineWidths[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
|
||||||
uint16_t screenHeight = display->height();
|
|
||||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
|
||||||
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
|
||||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
|
||||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
|
||||||
if (visibleTotalLines == 1) {
|
|
||||||
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
|
|
||||||
}
|
|
||||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
|
||||||
if (totalLines > visibleTotalLines) {
|
|
||||||
boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2;
|
|
||||||
}
|
|
||||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
|
||||||
|
|
||||||
const int selectedSlotClamped = std::max<int>(0, std::min<int>(selectedSlot, static_cast<int>(slotCount) - 1));
|
|
||||||
const int selectedCharIndex = slotCharIndexBySlot[selectedSlotClamped];
|
|
||||||
if (selectedCharIndex < 0 || selectedCharIndex >= static_cast<int>(formattedValue.size())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t valueTextX = boxLeft + (boxWidth - lineWidths[valueLineIndex]) / 2;
|
|
||||||
const uint16_t prefixWidth = display->getStringWidth(formattedValue.c_str(), selectedCharIndex, true);
|
|
||||||
const uint16_t slotCharWidth = display->getStringWidth(formattedValue.c_str() + selectedCharIndex, 1, true);
|
|
||||||
const int16_t slotCenterX = valueTextX + static_cast<int16_t>(prefixWidth + (slotCharWidth / 2));
|
|
||||||
|
|
||||||
int16_t topGuideY = boxTop + vPadding + (topGuideLineIndex * effectiveLineHeight);
|
|
||||||
int16_t bottomGuideY = boxTop + vPadding + (bottomGuideLineIndex * effectiveLineHeight);
|
|
||||||
const int16_t triHalfWidth = (currentResolution == ScreenResolution::High) ? 3 : 2;
|
|
||||||
const int16_t guideInsetY = 1;
|
|
||||||
const int16_t triHeight = std::max<int16_t>(2, static_cast<int16_t>((effectiveLineHeight - (guideInsetY * 2) - 1) / 2));
|
|
||||||
const int16_t topBaseY = topGuideY + effectiveLineHeight - guideInsetY - 1;
|
|
||||||
const int16_t topApexY = topBaseY - triHeight;
|
|
||||||
const int16_t bottomBaseY = bottomGuideY + guideInsetY;
|
|
||||||
const int16_t bottomApexY = bottomBaseY + triHeight;
|
|
||||||
|
|
||||||
display->setColor(WHITE);
|
|
||||||
display->fillTriangle(slotCenterX, topApexY, slotCenterX - triHalfWidth, topBaseY, slotCenterX + triHalfWidth, topBaseY);
|
|
||||||
display->fillTriangle(slotCenterX - triHalfWidth, bottomBaseY, slotCenterX + triHalfWidth, bottomBaseY, slotCenterX,
|
|
||||||
bottomApexY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used on boot when a certificate is being created
|
// Used on boot when a certificate is being created
|
||||||
@@ -380,10 +103,6 @@ void NotificationRenderer::resetBanner()
|
|||||||
pauseBanner = false;
|
pauseBanner = false;
|
||||||
numDigits = 0;
|
numDigits = 0;
|
||||||
currentNumber = 0;
|
currentNumber = 0;
|
||||||
signedDecimalValueTenths = 0;
|
|
||||||
signedDecimalMinTenths = -999;
|
|
||||||
signedDecimalMaxTenths = 999;
|
|
||||||
signedDecimalIsNegative = false;
|
|
||||||
|
|
||||||
nodeDB->pause_sort(false);
|
nodeDB->pause_sort(false);
|
||||||
|
|
||||||
@@ -434,9 +153,6 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
|||||||
case notificationTypeEnum::number_picker:
|
case notificationTypeEnum::number_picker:
|
||||||
drawNumberPicker(display, state);
|
drawNumberPicker(display, state);
|
||||||
break;
|
break;
|
||||||
case notificationTypeEnum::signed_decimal_picker:
|
|
||||||
drawSignedDecimalPicker(display, state);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,132 +160,83 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
|
|||||||
{
|
{
|
||||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||||
uint16_t lineCount = 0;
|
uint16_t lineCount = 0;
|
||||||
parseBannerLines(lineStarts, lineCount);
|
|
||||||
|
|
||||||
NumericSlotPickerState pickerState;
|
// Parse lines
|
||||||
pickerState.hasSign = false;
|
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||||
pickerState.decimalDigits = 0;
|
lineStarts[lineCount] = alertBannerMessage;
|
||||||
pickerState.minValue = 0;
|
|
||||||
pickerState.maxValue = maxValueForDigits(static_cast<uint8_t>(numDigits));
|
|
||||||
pickerState.digits.assign(numDigits, 0);
|
|
||||||
splitPickerDigits(currentNumber, pickerState.digits);
|
|
||||||
normalizePickerState(pickerState);
|
|
||||||
|
|
||||||
const uint8_t slotCount = pickerSlotCount(pickerState);
|
// Find lines
|
||||||
if (curSelected < 0) {
|
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||||
curSelected = 0;
|
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||||
} else if (curSelected > static_cast<int8_t>(slotCount)) {
|
if (lineStarts[lineCount + 1][0] == '\n')
|
||||||
curSelected = static_cast<int8_t>(slotCount);
|
lineStarts[lineCount + 1] += 1;
|
||||||
|
lineCount++;
|
||||||
}
|
}
|
||||||
|
// modulo to extract
|
||||||
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
|
||||||
resetBanner();
|
// Handle input
|
||||||
return;
|
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||||
}
|
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||||
|
if (this_digit == 9) {
|
||||||
if (isPickerIncrementEvent(inEvent.inputEvent)) {
|
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
|
||||||
applyPickerDelta(pickerState, curSelected, 1);
|
} else {
|
||||||
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
|
currentNumber += (pow_of_10(numDigits - curSelected - 1));
|
||||||
applyPickerDelta(pickerState, curSelected, -1);
|
}
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||||
|
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||||
|
if (this_digit == 0) {
|
||||||
|
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
|
||||||
|
} else {
|
||||||
|
currentNumber -= (pow_of_10(numDigits - curSelected - 1));
|
||||||
|
}
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||||
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
|
if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit
|
||||||
|
currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1));
|
||||||
|
currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1));
|
||||||
curSelected++;
|
curSelected++;
|
||||||
}
|
}
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||||
curSelected++;
|
curSelected++;
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||||
curSelected = std::max<int8_t>(0, curSelected - 1);
|
curSelected--;
|
||||||
|
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
||||||
|
alertBannerUntil != 0) {
|
||||||
|
resetBanner();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (curSelected == static_cast<int8_t>(numDigits)) {
|
||||||
currentNumber = static_cast<uint32_t>(std::max<int32_t>(0, composePickerValue(pickerState)));
|
|
||||||
if (curSelected == static_cast<int8_t>(slotCount)) {
|
|
||||||
alertBannerCallback(currentNumber);
|
alertBannerCallback(currentNumber);
|
||||||
resetBanner();
|
resetBanner();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||||
if (alertBannerMessage[0] == '\0') {
|
if (alertBannerMessage[0] == '\0')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
uint16_t totalLines = lineCount + 2;
|
||||||
|
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||||
|
|
||||||
|
// copy the linestarts to display to the linePointers holder
|
||||||
|
for (uint16_t i = 0; i < lineCount; i++) {
|
||||||
|
linePointers[i] = lineStarts[i];
|
||||||
}
|
}
|
||||||
|
std::string digits = " ";
|
||||||
std::string formattedValue;
|
std::string arrowPointer = " ";
|
||||||
std::vector<int16_t> slotCharIndexBySlot;
|
for (uint16_t i = 0; i < numDigits; i++) {
|
||||||
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
|
// Modulo minus modulo to return just the current number
|
||||||
|
digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " ";
|
||||||
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
|
if (curSelected == i) {
|
||||||
}
|
arrowPointer += "^ ";
|
||||||
|
} else {
|
||||||
void NotificationRenderer::drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
arrowPointer += "_ ";
|
||||||
{
|
|
||||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
|
||||||
uint16_t lineCount = 0;
|
|
||||||
parseBannerLines(lineStarts, lineCount);
|
|
||||||
|
|
||||||
NumericSlotPickerState pickerState;
|
|
||||||
pickerState.hasSign = true;
|
|
||||||
pickerState.decimalDigits = 1;
|
|
||||||
pickerState.minValue = signedDecimalMinTenths;
|
|
||||||
pickerState.maxValue = signedDecimalMaxTenths;
|
|
||||||
pickerState.digits.assign(3, 0); // XX.X format
|
|
||||||
|
|
||||||
const int32_t currentValue = static_cast<int32_t>(signedDecimalValueTenths);
|
|
||||||
if (currentValue < 0) {
|
|
||||||
pickerState.isNegative = true;
|
|
||||||
} else if (currentValue > 0) {
|
|
||||||
pickerState.isNegative = false;
|
|
||||||
} else {
|
|
||||||
pickerState.isNegative = signedDecimalIsNegative;
|
|
||||||
}
|
|
||||||
const uint32_t absValue =
|
|
||||||
(currentValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(currentValue)) : static_cast<uint32_t>(currentValue);
|
|
||||||
splitPickerDigits(absValue, pickerState.digits);
|
|
||||||
normalizePickerState(pickerState);
|
|
||||||
|
|
||||||
const uint8_t slotCount = pickerSlotCount(pickerState);
|
|
||||||
if (curSelected < 0) {
|
|
||||||
curSelected = 0;
|
|
||||||
} else if (curSelected > static_cast<int8_t>(slotCount)) {
|
|
||||||
curSelected = static_cast<int8_t>(slotCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
|
||||||
resetBanner();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPickerIncrementEvent(inEvent.inputEvent)) {
|
|
||||||
applyPickerDelta(pickerState, curSelected, 1);
|
|
||||||
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
|
|
||||||
applyPickerDelta(pickerState, curSelected, -1);
|
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
|
||||||
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
|
|
||||||
curSelected++;
|
|
||||||
}
|
}
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
|
||||||
curSelected++;
|
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
|
||||||
curSelected = std::max<int8_t>(0, curSelected - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signedDecimalValueTenths = static_cast<int16_t>(composePickerValue(pickerState));
|
linePointers[lineCount++] = digits.c_str();
|
||||||
signedDecimalIsNegative = pickerState.isNegative;
|
linePointers[lineCount++] = arrowPointer.c_str();
|
||||||
if (curSelected == static_cast<int8_t>(slotCount)) {
|
|
||||||
alertBannerCallback(signedDecimalValueTenths);
|
|
||||||
resetBanner();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||||
if (alertBannerMessage[0] == '\0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string formattedValue;
|
|
||||||
std::vector<int16_t> slotCharIndexBySlot;
|
|
||||||
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
|
|
||||||
|
|
||||||
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
@@ -955,9 +622,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
|||||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||||
lineBuffer[lineLengths[i]] = '\0';
|
lineBuffer[lineLengths[i]] = '\0';
|
||||||
// Determine if this is a pop-up or a pick list
|
// Determine if this is a pop-up or a pick list
|
||||||
const bool highlightTitleRow =
|
if (alertBannerOptions > 0 && i == 0) {
|
||||||
(i == 0) && (alertBannerOptions > 0 || current_notification_type == notificationTypeEnum::signed_decimal_picker);
|
|
||||||
if (highlightTitleRow) {
|
|
||||||
// Pick List
|
// Pick List
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
int background_yOffset = 1;
|
int background_yOffset = 1;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/VirtualKeyboard.h"
|
#include "graphics/VirtualKeyboard.h"
|
||||||
#include "modules/OnScreenKeyboardModule.h"
|
#include "modules/OnScreenKeyboardModule.h"
|
||||||
#include <cstdint>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#define MAX_LINES 5
|
#define MAX_LINES 5
|
||||||
@@ -27,10 +26,6 @@ class NotificationRenderer
|
|||||||
static std::function<void(int)> alertBannerCallback;
|
static std::function<void(int)> alertBannerCallback;
|
||||||
static uint32_t numDigits;
|
static uint32_t numDigits;
|
||||||
static uint32_t currentNumber;
|
static uint32_t currentNumber;
|
||||||
static int16_t signedDecimalValueTenths;
|
|
||||||
static int16_t signedDecimalMinTenths;
|
|
||||||
static int16_t signedDecimalMaxTenths;
|
|
||||||
static bool signedDecimalIsNegative;
|
|
||||||
static VirtualKeyboard *virtualKeyboard;
|
static VirtualKeyboard *virtualKeyboard;
|
||||||
static std::function<void(const std::string &)> textInputCallback;
|
static std::function<void(const std::string &)> textInputCallback;
|
||||||
|
|
||||||
@@ -41,7 +36,6 @@ class NotificationRenderer
|
|||||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
|
||||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||||
|
|||||||
+177
-226
@@ -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,61 +342,10 @@ 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
|
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||||
// === Optional: Last received StatusMessage line for this node ===
|
char signalHopsStr[32] = "";
|
||||||
// Display it directly under the username line (if we have one).
|
|
||||||
if (statusMessageModule) {
|
|
||||||
const auto &recent = statusMessageModule->getRecentReceived();
|
|
||||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
|
||||||
|
|
||||||
// Search newest-to-oldest
|
|
||||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
|
||||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
|
||||||
found = &(*it);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
std::string statusLine = std::string(" Status: ") + found->statusText;
|
|
||||||
{
|
|
||||||
const int screenW = display->getWidth();
|
|
||||||
const int ellipseW = display->getStringWidth("...");
|
|
||||||
int w = display->getStringWidth(statusLine.c_str());
|
|
||||||
|
|
||||||
// Only do work if it overflows
|
|
||||||
if (w > screenW) {
|
|
||||||
bool truncated = false;
|
|
||||||
if (ellipseW > screenW) {
|
|
||||||
statusLine.clear();
|
|
||||||
} else {
|
|
||||||
while (!statusLine.empty()) {
|
|
||||||
// remove one char (byte) at a time
|
|
||||||
statusLine.pop_back();
|
|
||||||
truncated = true;
|
|
||||||
|
|
||||||
// Measure candidate with ellipsis appended
|
|
||||||
std::string candidate = statusLine + "...";
|
|
||||||
if (display->getStringWidth(candidate.c_str()) <= screenW) {
|
|
||||||
statusLine = std::move(candidate);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (statusLine.empty() && ellipseW <= screenW) {
|
|
||||||
statusLine = "...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
display->drawString(x, getTextPositions(display)[line++], statusLine.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// === 2. Signal/Hops line (if available) ===
|
|
||||||
bool haveSignal = false;
|
bool haveSignal = false;
|
||||||
int bars = 0;
|
int bars = 0;
|
||||||
const char *qualityLabel = nullptr;
|
|
||||||
|
|
||||||
// Helper to get SNR limit based on modem preset
|
// Helper to get SNR limit based on modem preset
|
||||||
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
|
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
|
||||||
@@ -429,52 +366,81 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate signal grade using modem preset and SNR only
|
||||||
|
float snrLimit = getSnrLimit(config.lora.modem_preset);
|
||||||
|
float snr = node->snr;
|
||||||
|
|
||||||
|
// Determine signal quality label and bars using SNR-only grading
|
||||||
|
const char *qualityLabel = nullptr;
|
||||||
|
|
||||||
|
if (snr > snrLimit + 10) {
|
||||||
|
qualityLabel = "Good";
|
||||||
|
bars = 4;
|
||||||
|
} else if (snr > snrLimit + 6) {
|
||||||
|
qualityLabel = "Good";
|
||||||
|
bars = 3;
|
||||||
|
} else if (snr > snrLimit + 2) {
|
||||||
|
qualityLabel = "Good";
|
||||||
|
bars = 2;
|
||||||
|
} else if (snr > snrLimit - 4) {
|
||||||
|
qualityLabel = "Fair";
|
||||||
|
bars = 1;
|
||||||
|
} else {
|
||||||
|
qualityLabel = "Bad";
|
||||||
|
bars = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Add extra spacing on the left if we have an API connection to account for the common footer icons
|
// Add extra spacing on the left if we have an API connection to account for the common footer icons
|
||||||
const char *leftSideSpacing =
|
const char *leftSideSpacing =
|
||||||
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
|
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
|
||||||
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
|
||||||
|
|
||||||
// Signal text/bars are only for direct (zero-hop) nodes with valid SNR.
|
// --- Build the Signal/Hops line ---
|
||||||
if (isZeroHop) {
|
// Only show signal if we have valid SNR
|
||||||
float snr = node->snr;
|
if (snr > -100 && snr != 0) {
|
||||||
if (snr > -100 && snr != 0) {
|
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
|
||||||
float snrLimit = getSnrLimit(config.lora.modem_preset);
|
haveSignal = true;
|
||||||
// Determine signal quality label and bars using SNR-only grading.
|
}
|
||||||
if (snr > snrLimit + 10) {
|
|
||||||
qualityLabel = "Good";
|
|
||||||
bars = 4;
|
|
||||||
} else if (snr > snrLimit + 6) {
|
|
||||||
qualityLabel = "Good";
|
|
||||||
bars = 3;
|
|
||||||
} else if (snr > snrLimit + 2) {
|
|
||||||
qualityLabel = "Good";
|
|
||||||
bars = 2;
|
|
||||||
} else if (snr > snrLimit - 4) {
|
|
||||||
qualityLabel = "Fair";
|
|
||||||
bars = 1;
|
|
||||||
} else {
|
|
||||||
qualityLabel = "Bad";
|
|
||||||
bars = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
haveSignal = true;
|
if (node->hops_away > 0) {
|
||||||
|
size_t len = strlen(signalHopsStr);
|
||||||
|
if (haveSignal) {
|
||||||
|
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
|
||||||
|
} else {
|
||||||
|
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool showHops = node->has_hops_away && node->hops_away > 0;
|
if (signalHopsStr[0]) {
|
||||||
|
|
||||||
if (haveSignal || showHops) {
|
|
||||||
int yPos = getTextPositions(display)[line++];
|
int yPos = getTextPositions(display)[line++];
|
||||||
int curX = x + display->getStringWidth(leftSideSpacing);
|
int curX = x;
|
||||||
|
|
||||||
// Draw signal quality text for zero-hop nodes when present.
|
// Split combined string into signal text and hop suffix
|
||||||
if (haveSignal && qualityLabel) {
|
char sigPart[20] = "";
|
||||||
char signalLabel[20];
|
const char *hopPart = nullptr;
|
||||||
snprintf(signalLabel, sizeof(signalLabel), "Sig:%s", qualityLabel);
|
|
||||||
display->drawString(curX, yPos, signalLabel);
|
char *bracket = strchr(signalHopsStr, '[');
|
||||||
curX += display->getStringWidth(signalLabel) + 4;
|
if (bracket) {
|
||||||
|
size_t n = (size_t)(bracket - signalHopsStr);
|
||||||
|
if (n >= sizeof(sigPart))
|
||||||
|
n = sizeof(sigPart) - 1;
|
||||||
|
memcpy(sigPart, signalHopsStr, n);
|
||||||
|
sigPart[n] = '\0';
|
||||||
|
|
||||||
|
// Trim trailing spaces
|
||||||
|
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
|
||||||
|
sigPart[strlen(sigPart) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
hopPart = bracket; // "[n Hop(s)]"
|
||||||
|
} else {
|
||||||
|
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
|
||||||
|
sigPart[sizeof(sigPart) - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw signal quality text
|
||||||
|
display->drawString(curX, yPos, sigPart);
|
||||||
|
curX += display->getStringWidth(sigPart) + 4;
|
||||||
|
|
||||||
// Draw signal bars (skip on UltraLow, text only)
|
// Draw signal bars (skip on UltraLow, text only)
|
||||||
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
|
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
|
||||||
const int kMaxBars = 4;
|
const int kMaxBars = 4;
|
||||||
@@ -512,12 +478,12 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
|
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw hops for non-zero-hop nodes as: number + hop icon.
|
// Draw hops AFTER the bars as: [ number + hop icon ]
|
||||||
// This path is mutually exclusive with the zero-hop signal-bars path above.
|
if (hopPart && node->hops_away > 0) {
|
||||||
if (showHops) {
|
|
||||||
// hop label
|
// open bracket
|
||||||
display->drawString(curX, yPos, "Hop:");
|
display->drawString(curX, yPos, "[");
|
||||||
curX += display->getStringWidth("Hop:") + 2;
|
curX += display->getStringWidth("[") + 1;
|
||||||
|
|
||||||
// hop count
|
// hop count
|
||||||
char hopCount[6];
|
char hopCount[6];
|
||||||
@@ -529,6 +495,9 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||||
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
|
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
|
||||||
curX += hop_width + 1;
|
curX += hop_width + 1;
|
||||||
|
|
||||||
|
// closing bracket
|
||||||
|
display->drawString(curX, yPos, "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,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;
|
||||||
@@ -727,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;
|
||||||
@@ -742,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
|
||||||
}
|
}
|
||||||
@@ -1184,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
|
||||||
@@ -1199,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;
|
||||||
@@ -1213,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1260,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 ===
|
||||||
@@ -1282,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;
|
||||||
|
|
||||||
@@ -1302,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)
|
||||||
@@ -1349,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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+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;
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ class MeshModule
|
|||||||
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
||||||
meshtastic_AdminMessage *request,
|
meshtastic_AdminMessage *request,
|
||||||
meshtastic_AdminMessage *response);
|
meshtastic_AdminMessage *response);
|
||||||
const char *getName() const { return name; }
|
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; }
|
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; }
|
||||||
virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset
|
virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset
|
||||||
|
|||||||
+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
-36
@@ -1306,7 +1306,7 @@ void NodeDB::loadFromDisk()
|
|||||||
// Coerce LoRa config fields derived from presets while bootstrapping.
|
// Coerce LoRa config fields derived from presets while bootstrapping.
|
||||||
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
|
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
|
||||||
if (config.has_lora && config.lora.use_preset) {
|
if (config.has_lora && config.lora.use_preset) {
|
||||||
RadioInterface::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
|
||||||
@@ -1657,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
|
||||||
@@ -1713,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,
|
||||||
|
|||||||
+1
-11
@@ -465,18 +465,8 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||||
fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter;
|
fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||||
break;
|
break;
|
||||||
case meshtastic_ModuleConfig_traffic_management_tag:
|
|
||||||
LOG_DEBUG("Send module config: traffic management");
|
|
||||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag;
|
|
||||||
fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management;
|
|
||||||
break;
|
|
||||||
case meshtastic_ModuleConfig_tak_tag:
|
|
||||||
LOG_DEBUG("Send module config: tak");
|
|
||||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_tak_tag;
|
|
||||||
fromRadioScratch.moduleConfig.payload_variant.tak = moduleConfig.tak;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
LOG_DEBUG("Unhandled module config type %d", config_state);
|
LOG_ERROR("Unknown module config type %d", config_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
config_state++;
|
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);
|
||||||
|
|||||||
+131
-311
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -46,16 +46,6 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioLibInterface::~RadioLibInterface()
|
|
||||||
{
|
|
||||||
// If the static `instance` pointer still references us, clear it.
|
|
||||||
// A later successful init() may have replaced `instance` with a newer
|
|
||||||
// interface — don't clobber that case.
|
|
||||||
if (instance == this) {
|
|
||||||
instance = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
// ESP32 doesn't use that flag
|
// ESP32 doesn't use that flag
|
||||||
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
||||||
|
|||||||
@@ -136,13 +136,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
|||||||
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||||
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
|
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the static `instance` pointer if it still points at us, so callers
|
|
||||||
* that check `RadioLibInterface::instance != nullptr` don't dereference a
|
|
||||||
* freed object after a failed init() + unique_ptr reset.
|
|
||||||
*/
|
|
||||||
virtual ~RadioLibInterface();
|
|
||||||
|
|
||||||
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
|
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-25
@@ -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)
|
||||||
@@ -499,9 +482,9 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
meshtastic_Data decodedtmp;
|
meshtastic_Data decodedtmp;
|
||||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||||
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
|
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
|
||||||
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
|
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
|
||||||
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||||
LOG_DEBUG("Invalid portnum (bad psk?)");
|
LOG_ERROR("Invalid portnum (bad psk?)!");
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
|
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
|
||||||
LOG_WARN("Rejecting legacy DM");
|
LOG_WARN("Rejecting legacy DM");
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -82,8 +83,10 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
|||||||
|
|
||||||
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
||||||
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
||||||
|
sanitizeUtf8(lite.long_name, sizeof(lite.long_name));
|
||||||
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
||||||
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
||||||
|
sanitizeUtf8(lite.short_name, sizeof(lite.short_name));
|
||||||
lite.hw_model = user.hw_model;
|
lite.hw_model = user.hw_model;
|
||||||
lite.role = user.role;
|
lite.role = user.role;
|
||||||
lite.is_licensed = user.is_licensed;
|
lite.is_licensed = user.is_licensed;
|
||||||
@@ -102,8 +105,10 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User
|
|||||||
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
||||||
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
||||||
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
||||||
|
sanitizeUtf8(user.long_name, sizeof(user.long_name));
|
||||||
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
||||||
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
||||||
|
sanitizeUtf8(user.short_name, sizeof(user.short_name));
|
||||||
user.hw_model = lite.hw_model;
|
user.hw_model = lite.hw_model;
|
||||||
user.role = lite.role;
|
user.role = lite.role;
|
||||||
user.is_licensed = lite.is_licensed;
|
user.is_licensed = lite.is_licensed;
|
||||||
|
|||||||
@@ -285,11 +285,7 @@ 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
|
|
||||||
} meshtastic_Config_LoRaConfig_RegionCode;
|
} meshtastic_Config_LoRaConfig_RegionCode;
|
||||||
|
|
||||||
/* Standard predefined channel settings
|
/* Standard predefined channel settings
|
||||||
@@ -706,8 +702,8 @@ 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_ITU23_2M
|
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_BR_902
|
||||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M+1))
|
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+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_LONG_TURBO
|
||||||
|
|||||||
@@ -398,8 +398,6 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig {
|
|||||||
bool device_telemetry_enabled;
|
bool device_telemetry_enabled;
|
||||||
/* Enable/Disable the air quality telemetry measurement module on-device display */
|
/* Enable/Disable the air quality telemetry measurement module on-device display */
|
||||||
bool air_quality_screen_enabled;
|
bool air_quality_screen_enabled;
|
||||||
/* Temperature offset in Celsius applied to local environment telemetry before it is sent. */
|
|
||||||
float environment_temperature_offset_c;
|
|
||||||
} meshtastic_ModuleConfig_TelemetryConfig;
|
} meshtastic_ModuleConfig_TelemetryConfig;
|
||||||
|
|
||||||
/* Canned Messages Module Config */
|
/* Canned Messages Module Config */
|
||||||
@@ -595,7 +593,7 @@ extern "C" {
|
|||||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
#define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
||||||
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""}
|
#define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""}
|
||||||
@@ -614,7 +612,7 @@ extern "C" {
|
|||||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
#define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
||||||
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""}
|
#define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""}
|
||||||
@@ -720,7 +718,6 @@ extern "C" {
|
|||||||
#define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13
|
#define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14
|
#define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15
|
#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_environment_temperature_offset_c_tag 16
|
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1
|
#define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2
|
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3
|
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3
|
||||||
@@ -951,8 +948,7 @@ X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \
|
|||||||
X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \
|
X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \
|
X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \
|
X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15) \
|
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15)
|
||||||
X(a, STATIC, SINGULAR, FLOAT, environment_temperature_offset_c, 16)
|
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL
|
#define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL
|
#define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL
|
||||||
|
|
||||||
@@ -1055,8 +1051,8 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
|
|||||||
#define meshtastic_ModuleConfig_SerialConfig_size 28
|
#define meshtastic_ModuleConfig_SerialConfig_size 28
|
||||||
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
|
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
|
||||||
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
|
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_size 55
|
|
||||||
#define meshtastic_ModuleConfig_TAKConfig_size 4
|
#define meshtastic_ModuleConfig_TAKConfig_size 4
|
||||||
|
#define meshtastic_ModuleConfig_TelemetryConfig_size 50
|
||||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
|
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
|
||||||
#define meshtastic_ModuleConfig_size 227
|
#define meshtastic_ModuleConfig_size 227
|
||||||
#define meshtastic_RemoteHardwarePin_size 21
|
#define meshtastic_RemoteHardwarePin_size 21
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -118,3 +118,92 @@ 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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ int32_t StatusLEDModule::runOnce()
|
|||||||
#ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING
|
#ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING
|
||||||
CHARGE_LED_state = !CHARGE_LED_state;
|
CHARGE_LED_state = !CHARGE_LED_state;
|
||||||
#endif
|
#endif
|
||||||
} else if (power_state == charged) {
|
|
||||||
CHARGE_LED_state = LED_STATE_ON;
|
|
||||||
} else if (power_state == critical) {
|
} else if (power_state == critical) {
|
||||||
if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) {
|
if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) {
|
||||||
doing_fast_blink = true;
|
doing_fast_blink = true;
|
||||||
@@ -96,7 +94,7 @@ int32_t StatusLEDModule::runOnce()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (power_state != charging && power_state != charged && !doing_fast_blink) {
|
if (power_state != charging && !doing_fast_blink) {
|
||||||
if (CHARGE_LED_state == LED_STATE_ON) {
|
if (CHARGE_LED_state == LED_STATE_ON) {
|
||||||
CHARGE_LED_state = LED_STATE_OFF;
|
CHARGE_LED_state = LED_STATE_OFF;
|
||||||
my_interval = 999;
|
my_interval = 999;
|
||||||
@@ -145,7 +143,7 @@ int32_t StatusLEDModule::runOnce()
|
|||||||
#if defined(HAS_PMU)
|
#if defined(HAS_PMU)
|
||||||
if (pmu_found && PMU) {
|
if (pmu_found && PMU) {
|
||||||
// blink the axp led
|
// blink the axp led
|
||||||
PMU->setChargingLedMode(CHARGE_LED_state ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
PMU->setChargingLedMode((CHARGE_LED_state == LED_STATE_ON) ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
|||||||
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
||||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, p.payload.size);
|
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||||
|
|
||||||
this->packetHistoryTotalCount++;
|
this->packetHistoryTotalCount++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>)
|
||||||
@@ -138,32 +146,6 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
|
|||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr float TEMPERATURE_OFFSET_MIN_C = -20.0f;
|
|
||||||
constexpr float TEMPERATURE_OFFSET_MAX_C = 20.0f;
|
|
||||||
|
|
||||||
float clampTemperatureOffsetC(float offsetC)
|
|
||||||
{
|
|
||||||
if (offsetC < TEMPERATURE_OFFSET_MIN_C)
|
|
||||||
return TEMPERATURE_OFFSET_MIN_C;
|
|
||||||
if (offsetC > TEMPERATURE_OFFSET_MAX_C)
|
|
||||||
return TEMPERATURE_OFFSET_MAX_C;
|
|
||||||
return offsetC;
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyTemperatureOffset(meshtastic_EnvironmentMetrics *metrics)
|
|
||||||
{
|
|
||||||
const float offsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
|
|
||||||
if (offsetC == 0.0f)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (metrics->has_temperature)
|
|
||||||
metrics->temperature += offsetC;
|
|
||||||
if (metrics->has_soil_temperature)
|
|
||||||
metrics->soil_temperature += offsetC;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
|
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
|
||||||
|
|
||||||
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||||
@@ -173,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
|
||||||
@@ -229,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
|
||||||
@@ -244,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
|
||||||
@@ -262,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,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)) {
|
||||||
@@ -287,29 +275,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
return disable();
|
return disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto refreshLocalMeasurementPacket = [this]() {
|
|
||||||
// Keep displaying remote telemetry once we have it.
|
|
||||||
if (lastMeasurementPacket != nullptr && lastMeasurementPacket->from != nodeDB->getNodeNum()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
meshtastic_Telemetry local = meshtastic_Telemetry_init_zero;
|
|
||||||
if (!getEnvironmentTelemetry(&local)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
meshtastic_MeshPacket *localPacket = allocDataProtobuf(local);
|
|
||||||
if (localPacket == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastMeasurementPacket != nullptr) {
|
|
||||||
packetPool.release(lastMeasurementPacket);
|
|
||||||
}
|
|
||||||
lastMeasurementPacket = packetPool.allocCopy(*localPacket);
|
|
||||||
packetPool.release(localPacket);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (firstTime) {
|
if (firstTime) {
|
||||||
// This is the first time the OSThread library has called this function, so do some setup
|
// This is the first time the OSThread library has called this function, so do some setup
|
||||||
firstTime = 0;
|
firstTime = 0;
|
||||||
@@ -339,7 +304,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
result = rak9154Sensor.runOnce();
|
result = rak9154Sensor.runOnce();
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
refreshLocalMeasurementPacket();
|
|
||||||
}
|
}
|
||||||
// it's possible to have this module enabled, only for displaying values on the screen.
|
// it's possible to have this module enabled, only for displaying values on the screen.
|
||||||
// therefore, we should only enable the sensor loop if measurement is also enabled
|
// therefore, we should only enable the sensor loop if measurement is also enabled
|
||||||
@@ -356,7 +320,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
result = delay;
|
result = delay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshLocalMeasurementPacket();
|
|
||||||
|
|
||||||
uint32_t lastTelemetry =
|
uint32_t lastTelemetry =
|
||||||
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
|
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
|
||||||
@@ -617,9 +580,6 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
|
|||||||
hasSensor = true;
|
hasSensor = true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (valid)
|
|
||||||
applyTemperatureOffset(&m->variant.environment_metrics);
|
|
||||||
|
|
||||||
return valid && hasSensor;
|
return valid && hasSensor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,15 +644,11 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
|||||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||||
else
|
else
|
||||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
const bool shouldReplaceDisplayPacket =
|
// release previous packet before occupying a new spot
|
||||||
(lastMeasurementPacket == nullptr || lastMeasurementPacket->from == nodeDB->getNodeNum());
|
if (lastMeasurementPacket != nullptr)
|
||||||
if (shouldReplaceDisplayPacket) {
|
packetPool.release(lastMeasurementPacket);
|
||||||
// release previous packet before occupying a new spot
|
|
||||||
if (lastMeasurementPacket != nullptr)
|
|
||||||
packetPool.release(lastMeasurementPacket);
|
|
||||||
|
|
||||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||||
}
|
|
||||||
if (phoneOnly) {
|
if (phoneOnly) {
|
||||||
LOG_INFO("Send packet to phone");
|
LOG_INFO("Send packet to phone");
|
||||||
service->sendToPhone(p);
|
service->sendToPhone(p);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+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();
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
|||||||
/**
|
/**
|
||||||
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
|
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
|
||||||
*/
|
*/
|
||||||
virtual void onNowHasData(uint32_t fromRadioNum) override
|
virtual void onNowHasData(uint32_t fromRadioNum)
|
||||||
{
|
{
|
||||||
PhoneAPI::onNowHasData(fromRadioNum);
|
PhoneAPI::onNowHasData(fromRadioNum);
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check the current underlying physical link to see if the client is currently connected
|
/// Check the current underlying physical link to see if the client is currently connected
|
||||||
virtual bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; }
|
virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||||
|
|
||||||
void requestHighThroughputConnection(uint16_t conn_handle)
|
void requestHighThroughputConnection(uint16_t conn_handle)
|
||||||
{
|
{
|
||||||
@@ -412,9 +412,9 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
|
|||||||
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||||
{
|
{
|
||||||
#ifdef NIMBLE_TWO
|
#ifdef NIMBLE_TWO
|
||||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
|
||||||
#else
|
#else
|
||||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic) override
|
virtual void onWrite(NimBLECharacteristic *pCharacteristic)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
@@ -464,9 +464,9 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
|||||||
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||||
{
|
{
|
||||||
#ifdef NIMBLE_TWO
|
#ifdef NIMBLE_TWO
|
||||||
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
|
||||||
#else
|
#else
|
||||||
virtual void onRead(NimBLECharacteristic *pCharacteristic) override
|
virtual void onRead(NimBLECharacteristic *pCharacteristic)
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||||
@@ -582,9 +582,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
|||||||
private:
|
private:
|
||||||
NimbleBluetooth *ble;
|
NimbleBluetooth *ble;
|
||||||
|
|
||||||
virtual uint32_t onPassKeyDisplay() override
|
virtual uint32_t onPassKeyDisplay()
|
||||||
#else
|
#else
|
||||||
virtual uint32_t onPassKeyRequest() override
|
virtual uint32_t onPassKeyRequest()
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
uint32_t passkey = config.bluetooth.fixed_pin;
|
uint32_t passkey = config.bluetooth.fixed_pin;
|
||||||
@@ -635,9 +635,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef NIMBLE_TWO
|
#ifdef NIMBLE_TWO
|
||||||
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) override
|
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo)
|
||||||
#else
|
#else
|
||||||
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) override
|
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc)
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
LOG_INFO("BLE authentication complete");
|
LOG_INFO("BLE authentication complete");
|
||||||
@@ -655,7 +655,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef NIMBLE_TWO
|
#ifdef NIMBLE_TWO
|
||||||
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override
|
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
|
||||||
{
|
{
|
||||||
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
|
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
|
||||||
|
|
||||||
@@ -683,11 +683,11 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef NIMBLE_TWO
|
#ifdef NIMBLE_TWO
|
||||||
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override
|
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
|
||||||
{
|
{
|
||||||
LOG_INFO("BLE disconnect reason: %d", reason);
|
LOG_INFO("BLE disconnect reason: %d", reason);
|
||||||
#else
|
#else
|
||||||
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) override
|
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
|
||||||
{
|
{
|
||||||
LOG_INFO("BLE disconnect");
|
LOG_INFO("BLE disconnect");
|
||||||
#endif
|
#endif
|
||||||
@@ -989,4 +989,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearNVS()
|
||||||
|
{
|
||||||
|
NimBLEDevice::deleteAllBonds();
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
ESP.restart();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ class NimbleBluetooth : BluetoothApi
|
|||||||
};
|
};
|
||||||
|
|
||||||
void setBluetoothEnable(bool enable);
|
void setBluetoothEnable(bool enable);
|
||||||
|
void clearNVS();
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#ifdef T5_S3_EPAPER_PRO
|
|
||||||
|
|
||||||
#include "Observer.h"
|
|
||||||
#include "TouchDrvGT911.hpp"
|
|
||||||
#include "Wire.h"
|
|
||||||
#include "input/InputBroker.h"
|
|
||||||
#include "input/TouchScreenImpl1.h"
|
|
||||||
#include "sleep.h"
|
|
||||||
|
|
||||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
||||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
|
||||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
|
||||||
|
|
||||||
// Bridges touch events from TouchScreenImpl1 directly into InkHUD,
|
|
||||||
// bypassing the InputBroker (which is excluded in InkHUD builds).
|
|
||||||
// Routing mirrors the mini-epaper-s3 two-way rocker pattern:
|
|
||||||
// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system applet has focus (e.g. menu)
|
|
||||||
// - Nav up/down: navUp/navDown always (menu scroll)
|
|
||||||
// - Tap: shortpress (cycle applets / confirm in menu)
|
|
||||||
// - Long press: longpress (open menu / back)
|
|
||||||
class TouchInkHUDBridge : public Observer<const InputEvent *>
|
|
||||||
{
|
|
||||||
int onNotify(const InputEvent *e) override
|
|
||||||
{
|
|
||||||
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
|
||||||
|
|
||||||
// Keep alignment in sync with the current rotation so that visual-frame gestures
|
|
||||||
// always pass through nav functions without remapping: (rotation + alignment) % 4 == 0.
|
|
||||||
inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4;
|
|
||||||
|
|
||||||
// Check whether a system applet (e.g. menu) is currently handling input
|
|
||||||
bool systemHandlingInput = false;
|
|
||||||
for (NicheGraphics::InkHUD::SystemApplet *sa : inkhud->systemApplets) {
|
|
||||||
if (sa->handleInput) {
|
|
||||||
systemHandlingInput = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (e->inputEvent) {
|
|
||||||
case INPUT_BROKER_USER_PRESS:
|
|
||||||
inkhud->shortpress();
|
|
||||||
break;
|
|
||||||
case INPUT_BROKER_SELECT:
|
|
||||||
inkhud->longpress();
|
|
||||||
break;
|
|
||||||
case INPUT_BROKER_LEFT:
|
|
||||||
if (systemHandlingInput)
|
|
||||||
inkhud->navUp();
|
|
||||||
else
|
|
||||||
inkhud->prevApplet();
|
|
||||||
break;
|
|
||||||
case INPUT_BROKER_RIGHT:
|
|
||||||
if (systemHandlingInput)
|
|
||||||
inkhud->navDown();
|
|
||||||
else
|
|
||||||
inkhud->nextApplet();
|
|
||||||
break;
|
|
||||||
case INPUT_BROKER_UP:
|
|
||||||
inkhud->navUp();
|
|
||||||
break;
|
|
||||||
case INPUT_BROKER_DOWN:
|
|
||||||
inkhud->navDown();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static TouchInkHUDBridge touchBridge;
|
|
||||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
||||||
|
|
||||||
TouchDrvGT911 touch;
|
|
||||||
|
|
||||||
// Commands the GT911 into standby before the Wire bus is torn down.
|
|
||||||
// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still available here.
|
|
||||||
struct TouchDeepSleepObserver {
|
|
||||||
int onDeepSleep(void *)
|
|
||||||
{
|
|
||||||
touch.sleep();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
CallbackObserver<TouchDeepSleepObserver, void *> observer{this, &TouchDeepSleepObserver::onDeepSleep};
|
|
||||||
} static touchDeepSleepObserver;
|
|
||||||
|
|
||||||
bool readTouch(int16_t *x, int16_t *y)
|
|
||||||
{
|
|
||||||
if (!digitalRead(GT911_PIN_INT)) {
|
|
||||||
int16_t raw_x;
|
|
||||||
int16_t raw_y;
|
|
||||||
if (touch.getPoint(&raw_x, &raw_y)) {
|
|
||||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
||||||
// Transform raw GT911 axes to visual-frame coordinates for the current display rotation.
|
|
||||||
// rotation=3 is the physical identity (device's default orientation).
|
|
||||||
switch (NicheGraphics::InkHUD::InkHUD::getInstance()->persistence->settings.rotation) {
|
|
||||||
default:
|
|
||||||
case 3:
|
|
||||||
*x = raw_x;
|
|
||||||
*y = raw_y;
|
|
||||||
break; // identity
|
|
||||||
case 2:
|
|
||||||
*x = (EPD_WIDTH - 1) - raw_y;
|
|
||||||
*y = raw_x;
|
|
||||||
break; // 90° CW tilt
|
|
||||||
case 1:
|
|
||||||
*x = (EPD_HEIGHT - 1) - raw_x;
|
|
||||||
*y = (EPD_WIDTH - 1) - raw_y;
|
|
||||||
break; // 180° flip
|
|
||||||
case 0:
|
|
||||||
*x = raw_y;
|
|
||||||
*y = (EPD_HEIGHT - 1) - raw_x;
|
|
||||||
break; // 90° CCW tilt
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
*x = raw_x;
|
|
||||||
*y = raw_y;
|
|
||||||
#endif
|
|
||||||
LOG_DEBUG("touched(%d/%d)", *x, *y);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// T5-S3-ePaper Pro specific (late-) init
|
|
||||||
void lateInitVariant(void)
|
|
||||||
{
|
|
||||||
touch.setPins(GT911_PIN_RST, GT911_PIN_INT);
|
|
||||||
if (touch.begin(Wire, GT911_SLAVE_ADDRESS_H, GT911_PIN_SDA, GT911_PIN_SCL)) {
|
|
||||||
touchDeepSleepObserver.observer.observe(¬ifyDeepSleep);
|
|
||||||
touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch);
|
|
||||||
touchScreenImpl1->init();
|
|
||||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
|
||||||
touchBridge.observe(touchScreenImpl1);
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
LOG_ERROR("Failed to find touch controller!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -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) {
|
||||||
@@ -654,9 +650,7 @@ void portduinoSetup()
|
|||||||
if (verboseEnabled && portduino_config.logoutputlevel != level_trace) {
|
if (verboseEnabled && portduino_config.logoutputlevel != level_trace) {
|
||||||
portduino_config.logoutputlevel = level_debug;
|
portduino_config.logoutputlevel = level_debug;
|
||||||
}
|
}
|
||||||
if (portduino_config.lora_spi_dev != "") {
|
|
||||||
portduinoSetOptions({.realHardware = true});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,60 +1,8 @@
|
|||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stm32wle5xx.h>
|
#include <stm32wle5xx.h>
|
||||||
#include <stm32wlxx_hal.h>
|
#include <stm32wlxx_hal.h>
|
||||||
|
|
||||||
// ─── Bootloader redirect ──────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Why .noinit + constructor instead of TAMP backup registers:
|
|
||||||
//
|
|
||||||
// The STM32duino startup sequence initialises clocks which may call
|
|
||||||
// __HAL_RCC_BACKUPRESET_FORCE/RELEASE when configuring the LSE oscillator,
|
|
||||||
// wiping the entire backup domain (including TAMP->BKP0R) before setup()
|
|
||||||
// ever runs. The backup-register approach therefore cannot reliably survive
|
|
||||||
// a soft reset in this toolchain.
|
|
||||||
//
|
|
||||||
// Solution: store the magic in a .noinit SRAM variable.
|
|
||||||
// - NVIC_SystemReset() does NOT clear SRAM.
|
|
||||||
// - The linker script skips zero-init for .noinit sections.
|
|
||||||
// - __attribute__((constructor)) fires before main()/HAL_Init(), so we can
|
|
||||||
// intercept and jump before anything disturbs peripheral state.
|
|
||||||
|
|
||||||
#define BOOTLOADER_MAGIC 0xD00DB007UL
|
|
||||||
#define SYS_MEM_BASE 0x1FFF0000UL
|
|
||||||
|
|
||||||
// Placed in .noinit — not zeroed at startup, survives NVIC_SystemReset().
|
|
||||||
__attribute__((section(".noinit"), used)) volatile uint32_t g_bootloaderMagic;
|
|
||||||
|
|
||||||
// Fires before main() / HAL_Init(). Must use only core Cortex-M registers.
|
|
||||||
__attribute__((constructor(101), used)) static void earlyBootCheck(void)
|
|
||||||
{
|
|
||||||
if (g_bootloaderMagic != BOOTLOADER_MAGIC)
|
|
||||||
return;
|
|
||||||
g_bootloaderMagic = 0;
|
|
||||||
|
|
||||||
SysTick->CTRL = 0;
|
|
||||||
SysTick->LOAD = 0;
|
|
||||||
SysTick->VAL = 0;
|
|
||||||
for (int i = 0; i < 8; i++) {
|
|
||||||
NVIC->ICER[i] = 0xFFFFFFFF;
|
|
||||||
NVIC->ICPR[i] = 0xFFFFFFFF;
|
|
||||||
}
|
|
||||||
__DSB();
|
|
||||||
__ISB();
|
|
||||||
SCB->VTOR = SYS_MEM_BASE;
|
|
||||||
__set_MSP(*(volatile uint32_t *)SYS_MEM_BASE);
|
|
||||||
((void (*)(void))(*(volatile uint32_t *)(SYS_MEM_BASE + 4)))();
|
|
||||||
while (1)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
void enterDfuMode()
|
|
||||||
{
|
|
||||||
g_bootloaderMagic = BOOTLOADER_MAGIC;
|
|
||||||
HAL_NVIC_SystemReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setBluetoothEnable(bool enable) {}
|
void setBluetoothEnable(bool enable) {}
|
||||||
|
|
||||||
void playStartMelody() {}
|
void playStartMelody() {}
|
||||||
@@ -106,98 +54,3 @@ extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Taken from https://interrupt.memfault.com/blog/cortex-m-hardfault-debug
|
|
||||||
typedef struct __attribute__((packed)) ContextStateFrame {
|
|
||||||
uint32_t r0;
|
|
||||||
uint32_t r1;
|
|
||||||
uint32_t r2;
|
|
||||||
uint32_t r3;
|
|
||||||
uint32_t r12;
|
|
||||||
uint32_t lr;
|
|
||||||
uint32_t return_address;
|
|
||||||
uint32_t xpsr;
|
|
||||||
} sContextStateFrame;
|
|
||||||
|
|
||||||
// NOTE: If you are using CMSIS, the registers can also be
|
|
||||||
// accessed through CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk
|
|
||||||
#define HALT_IF_DEBUGGING() \
|
|
||||||
do { \
|
|
||||||
if ((*(volatile uint32_t *)0xE000EDF0) & (1 << 0)) { \
|
|
||||||
__asm("bkpt 1"); \
|
|
||||||
} \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
static char hardfault_message_buffer[256];
|
|
||||||
|
|
||||||
// printf directly using srcwrapper's debug UART function.
|
|
||||||
static void debug_printf(const char *format, ...)
|
|
||||||
{
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
int length = vsnprintf(hardfault_message_buffer, sizeof(hardfault_message_buffer), format, args);
|
|
||||||
va_end(args);
|
|
||||||
|
|
||||||
if (length < 0)
|
|
||||||
return;
|
|
||||||
uart_debug_write((uint8_t *)hardfault_message_buffer, min((unsigned int)length, sizeof(hardfault_message_buffer) - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// N picked by guessing
|
|
||||||
#define DOT_TIME 1200000
|
|
||||||
static void dot()
|
|
||||||
{
|
|
||||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
|
||||||
for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */
|
|
||||||
}
|
|
||||||
digitalWrite(LED_POWER, LED_STATE_OFF);
|
|
||||||
for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void dash()
|
|
||||||
{
|
|
||||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
|
||||||
for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */
|
|
||||||
}
|
|
||||||
digitalWrite(LED_POWER, LED_STATE_OFF);
|
|
||||||
for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void space()
|
|
||||||
{
|
|
||||||
for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable optimizations for this function so "frame" argument
|
|
||||||
// does not get optimized away
|
|
||||||
extern "C" __attribute__((optimize("O0"))) void HardFault_Handler_C(sContextStateFrame *frame)
|
|
||||||
{
|
|
||||||
debug_printf("HardFault!\r\n");
|
|
||||||
debug_printf("r0: %08x\r\n", frame->r0);
|
|
||||||
debug_printf("r1: %08x\r\n", frame->r1);
|
|
||||||
debug_printf("r2: %08x\r\n", frame->r2);
|
|
||||||
debug_printf("r3: %08x\r\n", frame->r3);
|
|
||||||
debug_printf("r12: %08x\r\n", frame->r12);
|
|
||||||
debug_printf("lr: %08x\r\n", frame->lr);
|
|
||||||
debug_printf("pc[return address]: %08x\r\n", frame->return_address);
|
|
||||||
debug_printf("xpsr: %08x\r\n", frame->xpsr);
|
|
||||||
|
|
||||||
HALT_IF_DEBUGGING();
|
|
||||||
|
|
||||||
// blink SOS forever
|
|
||||||
while (1) {
|
|
||||||
dot();
|
|
||||||
dot();
|
|
||||||
dot();
|
|
||||||
dash();
|
|
||||||
dash();
|
|
||||||
dash();
|
|
||||||
dot();
|
|
||||||
dot();
|
|
||||||
dot();
|
|
||||||
space();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-22
@@ -15,13 +15,8 @@
|
|||||||
|
|
||||||
// Device specific curves go in variant.h
|
// Device specific curves go in variant.h
|
||||||
#ifndef OCV_ARRAY
|
#ifndef OCV_ARRAY
|
||||||
#if defined(ARCH_STM32WL) && BATTERY_PIN == AVBAT
|
|
||||||
// STM32 VDD/VBAT absolute maximum is 4V so use an LFP curve
|
|
||||||
#define OCV_ARRAY 3650, 3400, 3340, 3320, 3300, 3280, 3270, 3260, 3240, 3200, 2500
|
|
||||||
#else
|
|
||||||
#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100
|
#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
|
|
||||||
/*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/
|
/*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/
|
||||||
#ifndef NUM_CELLS
|
#ifndef NUM_CELLS
|
||||||
@@ -86,7 +81,7 @@ extern RAK9154Sensor rak9154Sensor;
|
|||||||
extern XPowersLibInterface *PMU;
|
extern XPowersLibInterface *PMU;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class Power : public concurrency::OSThread
|
class Power : private concurrency::OSThread
|
||||||
{
|
{
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -101,14 +96,6 @@ class Power : public concurrency::OSThread
|
|||||||
void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; }
|
void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; }
|
||||||
const uint16_t OCV[11] = {OCV_ARRAY};
|
const uint16_t OCV[11] = {OCV_ARRAY};
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
|
||||||
int beforeLightSleep(void *unused);
|
|
||||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void attachPowerInterrupts();
|
|
||||||
void detachPowerInterrupts();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
meshtastic::PowerStatus *statusHandler;
|
meshtastic::PowerStatus *statusHandler;
|
||||||
|
|
||||||
@@ -133,14 +120,6 @@ class Power : public concurrency::OSThread
|
|||||||
// open circuit voltage lookup table
|
// open circuit voltage lookup table
|
||||||
uint8_t low_voltage_counter;
|
uint8_t low_voltage_counter;
|
||||||
uint32_t lastLogTime = 0;
|
uint32_t lastLogTime = 0;
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
|
||||||
// Get notified when lightsleep begins and ends
|
|
||||||
CallbackObserver<Power, void *> lsObserver = CallbackObserver<Power, void *>(this, &Power::beforeLightSleep);
|
|
||||||
CallbackObserver<Power, esp_sleep_wakeup_cause_t> lsEndObserver =
|
|
||||||
CallbackObserver<Power, esp_sleep_wakeup_cause_t>(this, &Power::afterLightSleep);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef DEBUG_HEAP
|
#ifdef DEBUG_HEAP
|
||||||
uint32_t lastheap;
|
uint32_t lastheap;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+6
-5
@@ -497,12 +497,13 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
|
|||||||
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||||
notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
|
notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
|
||||||
|
|
||||||
|
#ifdef BUTTON_PIN
|
||||||
if (cause == ESP_SLEEP_WAKEUP_GPIO) {
|
if (cause == ESP_SLEEP_WAKEUP_GPIO) {
|
||||||
LOG_INFO("Exit light sleep gpio");
|
LOG_INFO("Exit light sleep gpio: btn=%d",
|
||||||
// If we woke because of a GPIO, it's possible power needs to run to handle.
|
!digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
|
||||||
power->setIntervalFromNow(0);
|
} else
|
||||||
runASAP = true;
|
#endif
|
||||||
} else {
|
{
|
||||||
LOG_INFO("Exit light sleep cause: %d", cause);
|
LOG_INFO("Exit light sleep cause: %d", cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,814 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for the radio configuration validation and clamping functions
|
|
||||||
* introduced in the radio_interface_cherrypick branch.
|
|
||||||
*
|
|
||||||
* Targets:
|
|
||||||
* 1. getRegion()
|
|
||||||
* 2. RadioInterface::validateConfigRegion()
|
|
||||||
* 3. RadioInterface::validateConfigLora()
|
|
||||||
* 4. RadioInterface::clampConfigLora()
|
|
||||||
* 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF)
|
|
||||||
* 6. Channel spacing calculation (placeholder for future protobuf changes)
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "MeshRadio.h"
|
|
||||||
#include "MeshService.h"
|
|
||||||
#include "NodeDB.h"
|
|
||||||
#include "RadioInterface.h"
|
|
||||||
#include "TestUtil.h"
|
|
||||||
#include "modules/AdminModule.h"
|
|
||||||
#include <unity.h>
|
|
||||||
|
|
||||||
#include "meshtastic/config.pb.h"
|
|
||||||
|
|
||||||
class MockMeshService : public MeshService
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); }
|
|
||||||
};
|
|
||||||
|
|
||||||
static MockMeshService *mockMeshService;
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// getRegion() tests
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
|
|
||||||
|
|
||||||
static void test_getRegion_returnsCorrectRegion_US()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
||||||
TEST_ASSERT_NOT_NULL(r);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code);
|
|
||||||
TEST_ASSERT_EQUAL_STRING("US", r->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_getRegion_returnsCorrectRegion_EU868()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
TEST_ASSERT_NOT_NULL(r);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code);
|
|
||||||
TEST_ASSERT_EQUAL_STRING("EU_868", r->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_getRegion_returnsCorrectRegion_LORA24()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
||||||
TEST_ASSERT_NOT_NULL(r);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code);
|
|
||||||
TEST_ASSERT_TRUE(r->wideLora);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_getRegion_unsetCodeReturnsUnsetEntry()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
|
||||||
TEST_ASSERT_NOT_NULL(r);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
||||||
TEST_ASSERT_EQUAL_STRING("UNSET", r->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_getRegion_unknownCodeFallsToUnset()
|
|
||||||
{
|
|
||||||
// A code not in the table should iterate to the UNSET sentinel
|
|
||||||
const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255);
|
|
||||||
TEST_ASSERT_NOT_NULL(r);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// validateConfigRegion() tests
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
static void test_validateConfigRegion_validRegionReturnsTrue()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
|
|
||||||
// Ensure owner is not licensed (should not matter for non-licensed-only regions)
|
|
||||||
devicestate.owner.is_licensed = false;
|
|
||||||
|
|
||||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigRegion_unsetRegionReturnsTrue()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
||||||
|
|
||||||
devicestate.owner.is_licensed = false;
|
|
||||||
|
|
||||||
// UNSET region has licensedOnly=false, so should pass
|
|
||||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Shadow tables for testing (preset lists → profiles → regions → lookup)
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
// A minimal preset list with only one entry
|
|
||||||
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = {
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
|
|
||||||
MODEM_PRESET_END,
|
|
||||||
};
|
|
||||||
|
|
||||||
// A preset list that includes all turbo variants only
|
|
||||||
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = {
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
|
||||||
MODEM_PRESET_END,
|
|
||||||
};
|
|
||||||
|
|
||||||
// A restricted list simulating a hypothetical tight-regulation region
|
|
||||||
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = {
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
|
||||||
MODEM_PRESET_END,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mirrors PROFILE_STD but with non-zero spacing/padding for testing
|
|
||||||
static const RegionProfile TEST_PROFILE_SPACED = {
|
|
||||||
TEST_PRESETS_SINGLE,
|
|
||||||
/* spacing */ 0.025f,
|
|
||||||
/* padding */ 0.010f,
|
|
||||||
/* audioPermitted */ true,
|
|
||||||
/* licensedOnly */ false,
|
|
||||||
/* textThrottle */ 0,
|
|
||||||
/* positionThrottle */ 0,
|
|
||||||
/* telemetryThrottle */ 0,
|
|
||||||
/* overrideSlot */ 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// A licensed-only profile for testing access control
|
|
||||||
static const RegionProfile TEST_PROFILE_LICENSED = {
|
|
||||||
TEST_PRESETS_RESTRICTED,
|
|
||||||
/* spacing */ 0.0f,
|
|
||||||
/* padding */ 0.0f,
|
|
||||||
/* audioPermitted */ false,
|
|
||||||
/* licensedOnly */ true,
|
|
||||||
/* textThrottle */ 5,
|
|
||||||
/* positionThrottle */ 10,
|
|
||||||
/* telemetryThrottle */ 10,
|
|
||||||
/* overrideSlot */ 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Turbo-only profile
|
|
||||||
static const RegionProfile TEST_PROFILE_TURBO = {
|
|
||||||
TEST_PRESETS_TURBO_ONLY,
|
|
||||||
/* spacing */ 0.0f,
|
|
||||||
/* padding */ 0.0f,
|
|
||||||
/* audioPermitted */ true,
|
|
||||||
/* licensedOnly */ false,
|
|
||||||
/* textThrottle */ 0,
|
|
||||||
/* positionThrottle */ 0,
|
|
||||||
/* telemetryThrottle */ 0,
|
|
||||||
/* overrideSlot */ 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const RegionInfo testRegions[] = {
|
|
||||||
// A wide US-like region with spacing + padding
|
|
||||||
{meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_US_SPACED"},
|
|
||||||
|
|
||||||
// A narrow band simulating tight EU regulation
|
|
||||||
{meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED,
|
|
||||||
"TEST_EU_LICENSED"},
|
|
||||||
|
|
||||||
// A wide-LoRa region with turbo-only presets
|
|
||||||
{meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO,
|
|
||||||
"TEST_LORA24_TURBO"},
|
|
||||||
|
|
||||||
// Sentinel — must be last
|
|
||||||
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_UNSET"},
|
|
||||||
};
|
|
||||||
|
|
||||||
static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
|
||||||
{
|
|
||||||
const RegionInfo *r = testRegions;
|
|
||||||
while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
||||||
if (r->code == code)
|
|
||||||
return r;
|
|
||||||
r++;
|
|
||||||
}
|
|
||||||
return r; // Returns the UNSET sentinel
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Shadow table tests
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
static void test_shadowTable_spacedProfileHasNonZeroSpacing()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
||||||
TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name);
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing);
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_shadowTable_licensedProfileFlagsCorrect()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
TEST_ASSERT_TRUE(r->profile->licensedOnly);
|
|
||||||
TEST_ASSERT_FALSE(r->profile->audioPermitted);
|
|
||||||
TEST_ASSERT_EQUAL(3, r->profile->overrideSlot);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_shadowTable_presetCountMatchesExpected()
|
|
||||||
{
|
|
||||||
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
||||||
TEST_ASSERT_EQUAL(1, spaced->getNumPresets());
|
|
||||||
|
|
||||||
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
TEST_ASSERT_EQUAL(2, licensed->getNumPresets());
|
|
||||||
|
|
||||||
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
||||||
TEST_ASSERT_EQUAL(2, turbo->getNumPresets());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_shadowTable_defaultPresetIsFirstInList()
|
|
||||||
{
|
|
||||||
const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset());
|
|
||||||
|
|
||||||
const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset());
|
|
||||||
|
|
||||||
const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_shadowTable_channelSpacingWithPadding()
|
|
||||||
{
|
|
||||||
// Verify channel count when spacing + padding are non-zero
|
|
||||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
||||||
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
|
||||||
float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f);
|
|
||||||
|
|
||||||
// spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250
|
|
||||||
// channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing);
|
|
||||||
|
|
||||||
uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f);
|
|
||||||
// (928 - 902 + 0.025) / 0.295 = 88.2 → 88
|
|
||||||
TEST_ASSERT_EQUAL_UINT32(88, numChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_shadowTable_turboOnlyOnWideLora()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
||||||
TEST_ASSERT_TRUE(r->wideLora);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset());
|
|
||||||
|
|
||||||
// Verify wide-LoRa bandwidth for SHORT_TURBO
|
|
||||||
float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_shadowTable_unknownCodeFallsToSentinel()
|
|
||||||
{
|
|
||||||
const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200);
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
|
|
||||||
TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// validateConfigLora() tests
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
static void test_validateConfigLora_validPresetForUS()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
||||||
|
|
||||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_allStdPresetsValidForUS()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = stdPresets[i];
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_turboPresetsInvalidForEU868()
|
|
||||||
{
|
|
||||||
// EU_868 has PRESETS_EU_868 which excludes SHORT_TURBO and LONG_TURBO
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
|
||||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868");
|
|
||||||
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO;
|
|
||||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_validPresetsForEU868()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = {
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) {
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = eu868Presets[i];
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_customBandwidthTooWideForEU868()
|
|
||||||
{
|
|
||||||
// EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz
|
|
||||||
// A 500 kHz custom BW should be rejected
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
cfg.use_preset = false;
|
|
||||||
cfg.bandwidth = 500;
|
|
||||||
cfg.spread_factor = 11;
|
|
||||||
cfg.coding_rate = 5;
|
|
||||||
|
|
||||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_customBandwidthFitsUS()
|
|
||||||
{
|
|
||||||
// US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
cfg.use_preset = false;
|
|
||||||
cfg.bandwidth = 250;
|
|
||||||
cfg.spread_factor = 11;
|
|
||||||
cfg.coding_rate = 5;
|
|
||||||
|
|
||||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_customBandwidthFitsEU868()
|
|
||||||
{
|
|
||||||
// EU_868 spans 250 kHz, 125 kHz BW should fit
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
cfg.use_preset = false;
|
|
||||||
cfg.bandwidth = 125;
|
|
||||||
cfg.spread_factor = 12;
|
|
||||||
cfg.coding_rate = 8;
|
|
||||||
|
|
||||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_bogusPresetRejected()
|
|
||||||
{
|
|
||||||
// A fabricated preset value not in any list should be rejected
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
||||||
|
|
||||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast()
|
|
||||||
{
|
|
||||||
// UNSET uses PROFILE_UNDEF which has only LONG_FAST
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_FAST should be valid for UNSET");
|
|
||||||
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
||||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_FAST should be invalid for UNSET");
|
|
||||||
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
|
||||||
TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for UNSET");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_validateConfigLora_allPresetsValidForLORA24()
|
|
||||||
{
|
|
||||||
// LORA_24 uses PROFILE_STD (9 presets) with wideLora=true
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = stdPresets[i];
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// clampConfigLora() tests
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
static void test_clampConfigLora_invalidPresetClampedToDefault()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_clampConfigLora_validPresetUnchanged()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_clampConfigLora_customBwTooWideClampedToDefaultBw()
|
|
||||||
{
|
|
||||||
// EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW.
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
cfg.use_preset = false;
|
|
||||||
cfg.bandwidth = 500;
|
|
||||||
cfg.spread_factor = 11;
|
|
||||||
cfg.coding_rate = 5;
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora);
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_clampConfigLora_customBwValidLeftUnchanged()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
cfg.use_preset = false;
|
|
||||||
cfg.bandwidth = 125;
|
|
||||||
cfg.spread_factor = 12;
|
|
||||||
cfg.coding_rate = 8;
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast()
|
|
||||||
{
|
|
||||||
// UNSET uses PROFILE_UNDEF with only LONG_FAST; any other preset should clamp to it
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault()
|
|
||||||
{
|
|
||||||
// LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD)
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
|
|
||||||
TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// RegionInfo preset list integrity tests
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
static void test_presetsStd_hasNineEntries()
|
|
||||||
{
|
|
||||||
// PROFILE_STD should have exactly 9 presets
|
|
||||||
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
||||||
TEST_ASSERT_EQUAL(9, us->getNumPresets());
|
|
||||||
TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_presetsEU868_hasSevenEntries()
|
|
||||||
{
|
|
||||||
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
TEST_ASSERT_EQUAL(7, eu->getNumPresets());
|
|
||||||
TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_presetsUndef_hasOneEntry()
|
|
||||||
{
|
|
||||||
const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
|
||||||
TEST_ASSERT_EQUAL(1, unset->getNumPresets());
|
|
||||||
TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_defaultPresetIsInAvailablePresets()
|
|
||||||
{
|
|
||||||
// For every region, the defaultPreset must appear in its own availablePresets list
|
|
||||||
const RegionInfo *r = regions;
|
|
||||||
while (true) {
|
|
||||||
bool found = false;
|
|
||||||
for (size_t i = 0; i < r->getNumPresets(); i++) {
|
|
||||||
if (r->getAvailablePresets()[i] == r->getDefaultPreset()) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
char msg[80];
|
|
||||||
snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name);
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(found, msg);
|
|
||||||
|
|
||||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
||||||
break; // UNSET is the sentinel, stop after it
|
|
||||||
r++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_regionFieldsAreSane()
|
|
||||||
{
|
|
||||||
// Basic sanity check: all regions have freqEnd > freqStart and a non-null name
|
|
||||||
const RegionInfo *r = regions;
|
|
||||||
while (true) {
|
|
||||||
char msg[80];
|
|
||||||
snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name);
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg);
|
|
||||||
TEST_ASSERT_NOT_NULL(r->name);
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0");
|
|
||||||
TEST_ASSERT_NOT_NULL(r->getAvailablePresets());
|
|
||||||
|
|
||||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
||||||
break;
|
|
||||||
r++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_onlyLORA24HasWideLora()
|
|
||||||
{
|
|
||||||
// Verify that LORA_24 is the only region with wideLora=true
|
|
||||||
const RegionInfo *r = regions;
|
|
||||||
while (true) {
|
|
||||||
char msg[80];
|
|
||||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) {
|
|
||||||
snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name);
|
|
||||||
TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg);
|
|
||||||
} else {
|
|
||||||
snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name);
|
|
||||||
TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
|
||||||
break;
|
|
||||||
r++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Channel spacing calculation (placeholder for future protobuf updates)
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
static void test_channelSpacingCalculation_US_LONG_FAST()
|
|
||||||
{
|
|
||||||
// Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000)
|
|
||||||
// US: spacing=0, padding=0
|
|
||||||
// LONG_FAST on non-wide region: bw=250 kHz
|
|
||||||
// channelSpacing = 0 + 0 + 0.250 = 0.250 MHz
|
|
||||||
// numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104
|
|
||||||
const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
||||||
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
|
|
||||||
float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f);
|
|
||||||
uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f);
|
|
||||||
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
|
||||||
TEST_ASSERT_EQUAL_UINT32(104, numChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_channelSpacingCalculation_EU868_LONG_FAST()
|
|
||||||
{
|
|
||||||
// EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0
|
|
||||||
// LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz
|
|
||||||
// numChannels = round((0.25 + 0) / 0.250) = 1
|
|
||||||
const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora);
|
|
||||||
float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f);
|
|
||||||
uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f);
|
|
||||||
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
|
|
||||||
TEST_ASSERT_EQUAL_UINT32(1, numChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder: when protobuf region definitions include non-zero padding/spacing,
|
|
||||||
// add tests here to verify the channel count and frequency calculations.
|
|
||||||
static void test_channelSpacingCalculation_placeholder()
|
|
||||||
{
|
|
||||||
// TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values,
|
|
||||||
// verify:
|
|
||||||
// - Channel count matches expected value for each (region, preset) pair
|
|
||||||
// - First channel frequency = freqStart + (bw/2000) + padding
|
|
||||||
// - Nth channel frequency = first + (n * channelSpacing)
|
|
||||||
// - overrideSlot, when non-zero, forces the channel_num
|
|
||||||
TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// handleSetConfig fromOthers dispatch tests
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
class AdminModuleTestShim : public AdminModule
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using AdminModule::handleSetConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
static AdminModuleTestShim *testAdmin;
|
|
||||||
|
|
||||||
static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset preset)
|
|
||||||
{
|
|
||||||
meshtastic_Config c = meshtastic_Config_init_zero;
|
|
||||||
c.which_payload_variant = meshtastic_Config_lora_tag;
|
|
||||||
c.payload_variant.lora.region = region;
|
|
||||||
c.payload_variant.lora.use_preset = usePreset;
|
|
||||||
c.payload_variant.lora.modem_preset = preset;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_handleSetConfig_fromOthers_invalidPresetRejected()
|
|
||||||
{
|
|
||||||
// Set up a known-good baseline in the global config
|
|
||||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
config.lora.use_preset = true;
|
|
||||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
||||||
initRegion();
|
|
||||||
|
|
||||||
// Build an admin set_config with an invalid preset for EU_868
|
|
||||||
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
|
||||||
|
|
||||||
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
||||||
|
|
||||||
// fromOthers=true: invalid preset should be rejected, old preset preserved
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_handleSetConfig_fromLocal_invalidPresetClamped()
|
|
||||||
{
|
|
||||||
// Set up a known-good baseline
|
|
||||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
config.lora.use_preset = true;
|
|
||||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
||||||
initRegion();
|
|
||||||
|
|
||||||
// Build an admin set_config with an invalid preset for EU_868
|
|
||||||
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
|
|
||||||
|
|
||||||
testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)
|
|
||||||
|
|
||||||
// fromOthers=false: invalid preset should be clamped to the region's default
|
|
||||||
const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
|
|
||||||
TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_handleSetConfig_fromOthers_validPresetAccepted()
|
|
||||||
{
|
|
||||||
// Set up baseline
|
|
||||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
config.lora.use_preset = true;
|
|
||||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
||||||
initRegion();
|
|
||||||
|
|
||||||
// Build an admin set_config with a valid preset for EU_868
|
|
||||||
meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
|
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);
|
|
||||||
|
|
||||||
testAdmin->handleSetConfig(c, true); // fromOthers = true
|
|
||||||
|
|
||||||
// Valid preset should be accepted regardless of fromOthers
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Test runner
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
void setUp(void)
|
|
||||||
{
|
|
||||||
mockMeshService = new MockMeshService();
|
|
||||||
service = mockMeshService;
|
|
||||||
testAdmin = new AdminModuleTestShim();
|
|
||||||
}
|
|
||||||
void tearDown(void)
|
|
||||||
{
|
|
||||||
service = nullptr;
|
|
||||||
delete mockMeshService;
|
|
||||||
mockMeshService = nullptr;
|
|
||||||
delete testAdmin;
|
|
||||||
testAdmin = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
delay(10);
|
|
||||||
delay(2000);
|
|
||||||
|
|
||||||
initializeTestEnvironment();
|
|
||||||
|
|
||||||
UNITY_BEGIN();
|
|
||||||
|
|
||||||
// getRegion()
|
|
||||||
RUN_TEST(test_getRegion_returnsCorrectRegion_US);
|
|
||||||
RUN_TEST(test_getRegion_returnsCorrectRegion_EU868);
|
|
||||||
RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24);
|
|
||||||
RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry);
|
|
||||||
RUN_TEST(test_getRegion_unknownCodeFallsToUnset);
|
|
||||||
|
|
||||||
// validateConfigRegion()
|
|
||||||
RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue);
|
|
||||||
RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue);
|
|
||||||
|
|
||||||
// Shadow table tests
|
|
||||||
RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing);
|
|
||||||
RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect);
|
|
||||||
RUN_TEST(test_shadowTable_presetCountMatchesExpected);
|
|
||||||
RUN_TEST(test_shadowTable_defaultPresetIsFirstInList);
|
|
||||||
RUN_TEST(test_shadowTable_channelSpacingWithPadding);
|
|
||||||
RUN_TEST(test_shadowTable_turboOnlyOnWideLora);
|
|
||||||
RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel);
|
|
||||||
|
|
||||||
// validateConfigLora()
|
|
||||||
RUN_TEST(test_validateConfigLora_validPresetForUS);
|
|
||||||
RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS);
|
|
||||||
RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868);
|
|
||||||
RUN_TEST(test_validateConfigLora_validPresetsForEU868);
|
|
||||||
RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868);
|
|
||||||
RUN_TEST(test_validateConfigLora_customBandwidthFitsUS);
|
|
||||||
RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868);
|
|
||||||
RUN_TEST(test_validateConfigLora_bogusPresetRejected);
|
|
||||||
RUN_TEST(test_validateConfigLora_unsetRegionOnlyAcceptsLongFast);
|
|
||||||
RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24);
|
|
||||||
|
|
||||||
// clampConfigLora()
|
|
||||||
RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
|
|
||||||
RUN_TEST(test_clampConfigLora_validPresetUnchanged);
|
|
||||||
RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw);
|
|
||||||
RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged);
|
|
||||||
RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast);
|
|
||||||
RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault);
|
|
||||||
|
|
||||||
// RegionInfo preset list integrity
|
|
||||||
RUN_TEST(test_presetsStd_hasNineEntries);
|
|
||||||
RUN_TEST(test_presetsEU868_hasSevenEntries);
|
|
||||||
RUN_TEST(test_presetsUndef_hasOneEntry);
|
|
||||||
RUN_TEST(test_defaultPresetIsInAvailablePresets);
|
|
||||||
RUN_TEST(test_regionFieldsAreSane);
|
|
||||||
RUN_TEST(test_onlyLORA24HasWideLora);
|
|
||||||
|
|
||||||
// Channel spacing (current + placeholder)
|
|
||||||
RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST);
|
|
||||||
RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST);
|
|
||||||
RUN_TEST(test_channelSpacingCalculation_placeholder);
|
|
||||||
|
|
||||||
// handleSetConfig fromOthers dispatch
|
|
||||||
RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected);
|
|
||||||
RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped);
|
|
||||||
RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted);
|
|
||||||
|
|
||||||
exit(UNITY_END());
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {}
|
|
||||||
+22
-151
@@ -1,36 +1,10 @@
|
|||||||
#include "MeshRadio.h"
|
#include "MeshRadio.h"
|
||||||
#include "MeshService.h"
|
|
||||||
#include "RadioInterface.h"
|
#include "RadioInterface.h"
|
||||||
#include "TestUtil.h"
|
#include "TestUtil.h"
|
||||||
#include <unity.h>
|
#include <unity.h>
|
||||||
|
|
||||||
#include "meshtastic/config.pb.h"
|
#include "meshtastic/config.pb.h"
|
||||||
|
|
||||||
class MockMeshService : public MeshService
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); }
|
|
||||||
};
|
|
||||||
|
|
||||||
static MockMeshService *mockMeshService;
|
|
||||||
|
|
||||||
// Test shim to expose protected radio parameters set by applyModemConfig()
|
|
||||||
class TestableRadioInterface : public RadioInterface
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TestableRadioInterface() : RadioInterface() {}
|
|
||||||
uint8_t getCr() const { return cr; }
|
|
||||||
uint8_t getSf() const { return sf; }
|
|
||||||
float getBw() const { return bw; }
|
|
||||||
|
|
||||||
// Override reconfigure to call the base which invokes applyModemConfig()
|
|
||||||
bool reconfigure() override { return RadioInterface::reconfigure(); }
|
|
||||||
|
|
||||||
// Stubs for pure virtual methods required by RadioInterface
|
|
||||||
uint32_t getPacketTime(uint32_t, bool) override { return 0; }
|
|
||||||
ErrorCode send(meshtastic_MeshPacket *p) override { return ERRNO_OK; }
|
|
||||||
};
|
|
||||||
|
|
||||||
static void test_bwCodeToKHz_specialMappings()
|
static void test_bwCodeToKHz_specialMappings()
|
||||||
{
|
{
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31));
|
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31));
|
||||||
@@ -47,7 +21,7 @@ static void test_bwCodeToKHz_passthrough()
|
|||||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250));
|
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_validateConfigLora_noopWhenUsePresetFalse()
|
static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse()
|
||||||
{
|
{
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||||
cfg.use_preset = false;
|
cfg.use_preset = false;
|
||||||
@@ -56,152 +30,55 @@ static void test_validateConfigLora_noopWhenUsePresetFalse()
|
|||||||
cfg.bandwidth = 123;
|
cfg.bandwidth = 123;
|
||||||
cfg.spread_factor = 8;
|
cfg.spread_factor = 8;
|
||||||
|
|
||||||
RadioInterface::validateConfigLora(cfg);
|
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth);
|
TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth);
|
||||||
TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor);
|
TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor);
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_validateConfigLora_validPreset_nonWideRegion()
|
static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion()
|
||||||
{
|
{
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||||
cfg.use_preset = true;
|
cfg.use_preset = true;
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||||
|
|
||||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_validateConfigLora_validPreset_wideRegion()
|
static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion()
|
||||||
{
|
{
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||||
cfg.use_preset = true;
|
cfg.use_preset = true;
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||||
|
|
||||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
|
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_validateConfigLora_rejectsInvalidPresetForRegion()
|
static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan()
|
||||||
{
|
{
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||||
cfg.use_preset = true;
|
cfg.use_preset = true;
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
||||||
|
|
||||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
|
RadioInterface::bootstrapLoRaConfigFromPreset(cfg);
|
||||||
}
|
|
||||||
|
|
||||||
static void test_clampConfigLora_invalidPresetClampedToDefault()
|
|
||||||
{
|
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_clampConfigLora_validPresetUnchanged()
|
void setUp(void) {}
|
||||||
{
|
void tearDown(void) {}
|
||||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
cfg.use_preset = true;
|
|
||||||
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
||||||
|
|
||||||
RadioInterface::clampConfigLora(cfg);
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// applyModemConfig() coding rate tests (via reconfigure)
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
static TestableRadioInterface *testRadio;
|
|
||||||
|
|
||||||
// After fresh flash: coding_rate=0, use_preset=true, modem_preset=LONG_FAST
|
|
||||||
// CR should come from the preset (5 for LONG_FAST), not from the zero default.
|
|
||||||
static void test_applyModemConfig_freshFlashCodingRateNotZero()
|
|
||||||
{
|
|
||||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
config.lora.use_preset = true;
|
|
||||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
||||||
// coding_rate is 0 (default after init_zero, same as fresh flash)
|
|
||||||
|
|
||||||
testRadio->reconfigure();
|
|
||||||
|
|
||||||
// LONG_FAST preset has cr=5; must never be 0
|
|
||||||
TEST_ASSERT_EQUAL_UINT8(5, testRadio->getCr());
|
|
||||||
TEST_ASSERT_EQUAL_UINT8(11, testRadio->getSf());
|
|
||||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 250.0f, testRadio->getBw());
|
|
||||||
}
|
|
||||||
|
|
||||||
// When coding_rate matches the preset exactly, should still use the preset value
|
|
||||||
static void test_applyModemConfig_codingRateMatchesPreset()
|
|
||||||
{
|
|
||||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
config.lora.use_preset = true;
|
|
||||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
|
|
||||||
config.lora.coding_rate = 8; // LONG_SLOW default is cr=8
|
|
||||||
|
|
||||||
testRadio->reconfigure();
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom CR higher than preset should be used
|
|
||||||
static void test_applyModemConfig_customCodingRateHigherThanPreset()
|
|
||||||
{
|
|
||||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
config.lora.use_preset = true;
|
|
||||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
||||||
config.lora.coding_rate = 7; // LONG_FAST preset has cr=5, 7 > 5
|
|
||||||
|
|
||||||
testRadio->reconfigure();
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT8(7, testRadio->getCr());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom CR lower than preset: preset wins (higher is more robust)
|
|
||||||
static void test_applyModemConfig_customCodingRateLowerThanPreset()
|
|
||||||
{
|
|
||||||
config.lora = meshtastic_Config_LoRaConfig_init_zero;
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
config.lora.use_preset = true;
|
|
||||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
|
|
||||||
config.lora.coding_rate = 5; // LONG_SLOW preset has cr=8, 5 < 8
|
|
||||||
|
|
||||||
testRadio->reconfigure();
|
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr());
|
|
||||||
}
|
|
||||||
|
|
||||||
void setUp(void)
|
|
||||||
{
|
|
||||||
mockMeshService = new MockMeshService();
|
|
||||||
service = mockMeshService;
|
|
||||||
|
|
||||||
// RadioInterface computes slotTimeMsec during construction and expects myRegion to be valid.
|
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
|
||||||
initRegion();
|
|
||||||
|
|
||||||
testRadio = new TestableRadioInterface();
|
|
||||||
}
|
|
||||||
void tearDown(void)
|
|
||||||
{
|
|
||||||
delete testRadio;
|
|
||||||
testRadio = nullptr;
|
|
||||||
service = nullptr;
|
|
||||||
delete mockMeshService;
|
|
||||||
mockMeshService = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
@@ -213,16 +90,10 @@ void setup()
|
|||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_bwCodeToKHz_specialMappings);
|
RUN_TEST(test_bwCodeToKHz_specialMappings);
|
||||||
RUN_TEST(test_bwCodeToKHz_passthrough);
|
RUN_TEST(test_bwCodeToKHz_passthrough);
|
||||||
RUN_TEST(test_validateConfigLora_noopWhenUsePresetFalse);
|
RUN_TEST(test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse);
|
||||||
RUN_TEST(test_validateConfigLora_validPreset_nonWideRegion);
|
RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion);
|
||||||
RUN_TEST(test_validateConfigLora_validPreset_wideRegion);
|
RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion);
|
||||||
RUN_TEST(test_validateConfigLora_rejectsInvalidPresetForRegion);
|
RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan);
|
||||||
RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
|
|
||||||
RUN_TEST(test_clampConfigLora_validPresetUnchanged);
|
|
||||||
RUN_TEST(test_applyModemConfig_freshFlashCodingRateNotZero);
|
|
||||||
RUN_TEST(test_applyModemConfig_codingRateMatchesPreset);
|
|
||||||
RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset);
|
|
||||||
RUN_TEST(test_applyModemConfig_customCodingRateLowerThanPreset);
|
|
||||||
exit(UNITY_END());
|
exit(UNITY_END());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,195 @@
|
|||||||
|
#include "meshUtils.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
void setUp(void) {}
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
// --- Valid UTF-8 should pass through unchanged ---
|
||||||
|
|
||||||
|
void test_ascii_unchanged()
|
||||||
|
{
|
||||||
|
char buf[32] = "Hello World";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Hello World", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_valid_2byte_unchanged()
|
||||||
|
{
|
||||||
|
// "café" — é is C3 A9
|
||||||
|
char buf[16] = "caf\xC3\xA9";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("caf\xC3\xA9", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_valid_3byte_unchanged()
|
||||||
|
{
|
||||||
|
// "€" is E2 82 AC
|
||||||
|
char buf[16] = "\xE2\x82\xAC";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("\xE2\x82\xAC", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_valid_4byte_emoji_unchanged()
|
||||||
|
{
|
||||||
|
// 🌙 is F0 9F 8C 99
|
||||||
|
char buf[16] = "\xF0\x9F\x8C\x99";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("\xF0\x9F\x8C\x99", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_valid_mixed_unchanged()
|
||||||
|
{
|
||||||
|
// "Hi 🌙!" — mix of ASCII and 4-byte
|
||||||
|
char buf[16] = "Hi \xF0\x9F\x8C\x99!";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Hi \xF0\x9F\x8C\x99!", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_empty_string()
|
||||||
|
{
|
||||||
|
char buf[8] = "";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Invalid sequences observed in the wild ---
|
||||||
|
|
||||||
|
void test_truncated_4byte_at_end()
|
||||||
|
{
|
||||||
|
// Name with valid emoji 🌙 followed by a truncated 4-byte sequence + ASCII
|
||||||
|
char buf[32] = "Lunar Tower \xF0\x9F\x8C\x99\xF0\x9F\x97"
|
||||||
|
"4";
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
// The 🌙 should be preserved; F0 9F 97 is an incomplete 4-byte sequence,
|
||||||
|
// '4' (0x34) is not a valid continuation byte
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Lunar Tower \xF0\x9F\x8C\x99???4", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_lone_lead_bytes_without_continuations()
|
||||||
|
{
|
||||||
|
// Mixed ASCII with stray multibyte lead bytes (E1, F3) lacking proper continuations
|
||||||
|
char buf[32] = "Mesht\xE1\xF3tic 37e2";
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
// E1 expects 2 continuation bytes, but F3 is not a continuation → E1 replaced
|
||||||
|
// F3 expects 3 continuation bytes, 't','i','c' are not continuations → F3 replaced
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Mesht??tic 37e2", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Edge cases ---
|
||||||
|
|
||||||
|
void test_bare_continuation_byte()
|
||||||
|
{
|
||||||
|
// 0x80 alone is invalid (continuation byte with no lead)
|
||||||
|
char buf[8] = "\x80";
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("?", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_overlong_2byte()
|
||||||
|
{
|
||||||
|
// C0 AF is an overlong encoding of U+002F '/'
|
||||||
|
char buf[8] = "\xC0\xAF";
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
// C0 is a 2-byte lead, AF is valid continuation, but codepoint 0x2F < 0x80 → overlong
|
||||||
|
// C0 replaced, AF (now bare continuation) also replaced
|
||||||
|
TEST_ASSERT_EQUAL_STRING("??", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_surrogate_half()
|
||||||
|
{
|
||||||
|
// ED A0 80 encodes U+D800 (surrogate half — invalid in UTF-8)
|
||||||
|
char buf[8] = "\xED\xA0\x80";
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("???", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_5byte_sequence_rejected()
|
||||||
|
{
|
||||||
|
// F8 80 80 80 80 — 5-byte sequence, not valid UTF-8
|
||||||
|
char buf[8] = "\xF8\x80\x80\x80\x80";
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
// F8 is invalid lead (>= 0xF8), each 0x80 is bare continuation
|
||||||
|
TEST_ASSERT_EQUAL_STRING("?????", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_truncated_3byte_at_buffer_end()
|
||||||
|
{
|
||||||
|
// Buffer is exactly 4 bytes: E2 82 then forced null at [3]
|
||||||
|
char buf[4];
|
||||||
|
buf[0] = '\xE2';
|
||||||
|
buf[1] = '\x82';
|
||||||
|
buf[2] = '\0'; // String ends before the 3-byte sequence completes
|
||||||
|
buf[3] = '\0';
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("??", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_null_termination_enforced()
|
||||||
|
{
|
||||||
|
// Fill buffer completely with no null terminator
|
||||||
|
char buf[5];
|
||||||
|
memset(buf, 'A', sizeof(buf));
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
// Should be null-terminated and content preserved (all ASCII)
|
||||||
|
TEST_ASSERT_EQUAL_STRING("AAAA", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_null_buffer()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(nullptr, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_zero_size()
|
||||||
|
{
|
||||||
|
char buf[4] = "Hi";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, 0));
|
||||||
|
// Buffer should be untouched
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Hi", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_valid_max_codepoint()
|
||||||
|
{
|
||||||
|
// U+10FFFF = F4 8F BF BF (maximum valid Unicode codepoint)
|
||||||
|
char buf[8] = "\xF4\x8F\xBF\xBF";
|
||||||
|
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("\xF4\x8F\xBF\xBF", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_above_max_codepoint()
|
||||||
|
{
|
||||||
|
// U+110000 = F4 90 80 80 (just above maximum valid Unicode)
|
||||||
|
char buf[8] = "\xF4\x90\x80\x80";
|
||||||
|
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
// Valid UTF-8 passthrough
|
||||||
|
RUN_TEST(test_ascii_unchanged);
|
||||||
|
RUN_TEST(test_valid_2byte_unchanged);
|
||||||
|
RUN_TEST(test_valid_3byte_unchanged);
|
||||||
|
RUN_TEST(test_valid_4byte_emoji_unchanged);
|
||||||
|
RUN_TEST(test_valid_mixed_unchanged);
|
||||||
|
RUN_TEST(test_empty_string);
|
||||||
|
|
||||||
|
// Invalid sequences observed in the wild
|
||||||
|
RUN_TEST(test_truncated_4byte_at_end);
|
||||||
|
RUN_TEST(test_lone_lead_bytes_without_continuations);
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
RUN_TEST(test_bare_continuation_byte);
|
||||||
|
RUN_TEST(test_overlong_2byte);
|
||||||
|
RUN_TEST(test_surrogate_half);
|
||||||
|
RUN_TEST(test_5byte_sequence_rejected);
|
||||||
|
RUN_TEST(test_truncated_3byte_at_buffer_end);
|
||||||
|
RUN_TEST(test_null_termination_enforced);
|
||||||
|
RUN_TEST(test_null_buffer);
|
||||||
|
RUN_TEST(test_zero_size);
|
||||||
|
RUN_TEST(test_valid_max_codepoint);
|
||||||
|
RUN_TEST(test_above_max_codepoint);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ build_flags =
|
|||||||
-Wall
|
-Wall
|
||||||
-Wextra
|
-Wextra
|
||||||
-Isrc/platform/esp32
|
-Isrc/platform/esp32
|
||||||
|
-include mbedtls/error.h
|
||||||
-std=gnu++17
|
-std=gnu++17
|
||||||
-DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG
|
-DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG
|
||||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ build_flags =
|
|||||||
-D BOARD_HAS_PSRAM
|
-D BOARD_HAS_PSRAM
|
||||||
-D RADIOLIB_EXCLUDE_LR11X0=1
|
-D RADIOLIB_EXCLUDE_LR11X0=1
|
||||||
-D RADIOLIB_EXCLUDE_SX128X=1
|
-D RADIOLIB_EXCLUDE_SX128X=1
|
||||||
-D RADIOLIB_EXCLUDE_LR2021=1
|
|
||||||
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
||||||
-D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1
|
-D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1
|
||||||
-D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
|
-D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
|
||||||
|
|||||||
@@ -29,14 +29,6 @@
|
|||||||
#define SX126X_DIO2_AS_RF_SWITCH
|
#define SX126X_DIO2_AS_RF_SWITCH
|
||||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||||
|
|
||||||
// Enable Traffic Management Module for Heltec V4
|
|
||||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
|
||||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
|
||||||
#endif
|
|
||||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
|
||||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ---- GC1109 RF FRONT END CONFIGURATION ----
|
// ---- GC1109 RF FRONT END CONFIGURATION ----
|
||||||
// The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA
|
// The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA
|
||||||
// RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna
|
// RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user