Merge branch 'develop' into sfpp

This commit is contained in:
Jonathan Bennett
2026-02-27 17:32:30 -06:00
committed by GitHub
129 changed files with 1904 additions and 259 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
with:
pio_platform: ${{ matrix.check.platform }}
pio_env: ${{ matrix.check.board }}
pio_target: check --fail-on-defect=low
pio_target: check
build:
needs: [setup, version]
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Stale PR+Issues
uses: actions/stale@v10.1.1
uses: actions/stale@v10.2.0
with:
days-before-stale: 45
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
+1
View File
@@ -33,6 +33,7 @@ __pycache__
*~
venv/
.venv/
release/
.vscode/extensions.json
/compile_commands.json
+6 -6
View File
@@ -8,20 +8,20 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.501
- renovate@43.10.3
- checkov@3.2.506
- renovate@43.31.9
- prettier@3.8.1
- trufflehog@3.93.3
- trufflehog@3.93.4
- yamllint@1.38.0
- bandit@1.9.3
- trivy@0.69.1
- taplo@0.10.0
- ruff@0.15.1
- isort@7.0.0
- ruff@0.15.2
- isort@8.0.0
- markdownlint@0.47.0
- oxipng@10.1.0
- svgo@4.0.0
- actionlint@1.7.10
- actionlint@1.7.11
- flake8@7.3.0
- hadolint@2.14.0
- shfmt@3.6.0
+26
View File
@@ -0,0 +1,26 @@
# Lightweight container for running native PlatformIO tests on non-Linux hosts
FROM python:3.14-slim-trixie
ENV DEBIAN_FRONTEND=noninteractive
ENV PIP_ROOT_USER_ACTION=ignore
# hadolint ignore=DL3008
RUN apt-get update && apt-get install --no-install-recommends -y \
g++ git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir platformio==6.1.19 \
&& useradd --create-home --shell /usr/sbin/nologin meshtastic
WORKDIR /firmware
RUN chown -R meshtastic:meshtastic /firmware
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD platformio --version || exit 1
USER meshtastic
# Run tests by default; override with docker run args for specific filters
CMD ["platformio", "test", "-e", "coverage", "-v"]
+1 -1
View File
@@ -23,4 +23,4 @@ for BOARD in $BOARDS; do
CHECK="${CHECK} -e ${BOARD}"
done
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high
@@ -9,3 +9,5 @@ Lora:
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
GPIO_DETECT_PA: 13
TX_GAIN_LORA: [8, 8, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
+16
View File
@@ -0,0 +1,16 @@
# For use with Armbian luckfox-lyra-ultra-w
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 1 Watt Lyra Ultra hat
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 10
IRQ: 5
Busy: 11
Reset: 9
RXen: 14
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
spiSpeed: 2000000
+17
View File
@@ -0,0 +1,17 @@
# For use with Armbian luckfox-lyra-ultra-w
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 2 Watt Lyra Ultra hat
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
SX126X_MAX_POWER: 8
CS: 10
IRQ: 5
Busy: 11
Reset: 9
RXen: 14
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
spiSpeed: 2000000
@@ -0,0 +1,25 @@
# For use with Armbian luckfox-lyra // luckfox-lyra-plus
# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config
# Waveshare LoRa HAT for Raspberry Pi Pico
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
spidev: spidev0.0
CS: # GPIO0_B5
pin: 13
gpiochip: 0
line: 13
IRQ: # GPIO1_C2
pin: 50
gpiochip: 1
line: 18
Busy: # GPIO0_B4
pin: 12
gpiochip: 0
line: 12
Reset: # GPIO0_A2
pin: 2
gpiochip: 0
line: 2
+1 -2
View File
@@ -13,8 +13,7 @@ Lora:
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
+1 -2
View File
@@ -13,8 +13,7 @@ Lora:
# USB_Serialnum: 12345678
SX126X_MAX_POWER: 22
# Reduce output power to improve EMI
NUM_PA_POINTS: 22
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
# Note: This module integrates an additional PA to achieve higher output power.
# The 'power' parameter here does not represent the actual RF output.
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts).
#
# Usage:
# ./bin/test-native-docker.sh # run all native tests
# ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter
# ./bin/test-native-docker.sh --rebuild # force rebuild the image
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
IMAGE_NAME="meshtastic-native-test"
REBUILD=false
EXTRA_ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--rebuild" ]]; then
REBUILD=true
else
EXTRA_ARGS+=("$arg")
fi
done
if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
echo "Building test image (first run may take a few minutes)..."
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR"
fi
# Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI)
sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/'
# Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through.
if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then
CMD=("platformio" "test" "-e" "coverage" "-v")
else
CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}")
fi
exec docker run --rm \
-v "$ROOT_DIR:/src:ro" \
"$IMAGE_NAME" \
bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}"
+40
View File
@@ -0,0 +1,40 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_ESP32S3_DEV",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo Mini-Epaper-S3 (4 MB Flash, 2MB PSRAM)",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.lilygo.cc",
"vendor": "LilyGo"
}
+16 -36
View File
@@ -117,7 +117,7 @@ lib_deps =
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.5.0
jgromes/RadioLib@7.6.0
[device-ui_base]
lib_deps =
@@ -184,8 +184,8 @@ lib_deps =
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
wollewald/BH1750_WE@1.1.10
; (not included in native / portduino)
[environmental_extra]
; Common environmental sensor libraries (not included in native / portduino)
[environmental_extra_common]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
@@ -205,10 +205,6 @@ lib_deps =
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
@@ -220,34 +216,18 @@ lib_deps =
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
sensirion/Sensirion I2C SCD30@1.0.0
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
[environmental_extra]
lib_deps =
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
adafruit/Adafruit SHTC3 Library@1.0.2
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
adafruit/Adafruit SHT31 Library@2.2.2
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
adafruit/Adafruit VEML7700 Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
adafruit/Adafruit SHT4x Library@1.0.5
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.3
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
sensirion/Sensirion I2C SCD30@1.0.0
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
adafruit/Adafruit BME680 Library@^2.0.5
+1 -3
View File
@@ -51,9 +51,7 @@ class AudioThread : public concurrency::OSThread
i2sRtttl = nullptr;
}
if (rtttlFile != nullptr) {
rtttlFile = nullptr;
}
rtttlFile = nullptr;
setCPUFast(false);
#ifdef T_LORA_PAGER
+2
View File
@@ -35,6 +35,8 @@ void consoleInit()
#if defined(SERIAL_HAS_ON_RECEIVE)
// onReceive does only exist for HardwareSerial not for USB CDC serial
Port.onReceive([sc]() { sc->rxInt(); });
#else
(void)sc;
#endif
DEBUG_PORT.rpInit(); // Simply sets up semaphore
}
+4 -2
View File
@@ -76,8 +76,10 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit
void NotifiedWorkerThread::checkNotification()
{
auto n = notification;
notification = 0; // clear notification
// Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification
// after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes
// in.)
auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe
if (n) {
onNotify(n);
}
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "OSThread.h"
#include <atomic>
namespace concurrency
{
@@ -13,7 +14,7 @@ class NotifiedWorkerThread : public OSThread
/**
* The notification that was most recently used to wake the thread. Read from runOnce()
*/
uint32_t notification = 0;
std::atomic<uint32_t> notification{0};
public:
NotifiedWorkerThread(const char *name) : OSThread(name) {}
+11 -6
View File
@@ -1,24 +1,29 @@
#pragma once
#include <functional>
#include <utility>
#include "concurrency/OSThread.h"
namespace concurrency
{
/**
* @brief Periodically invoke a callback. This just provides C-style callback conventions
* rather than a virtual function - FIXME, remove?
* @brief Periodically invoke a callback.
* Supports both legacy function pointers and modern callables.
*/
class Periodic : public OSThread
{
int32_t (*callback)();
public:
// callback returns the period for the next callback invocation (or 0 if we should no longer be called)
Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {}
Periodic(const char *name, int32_t (*cb)()) : OSThread(name), callback(cb) {}
Periodic(const char *name, std::function<int32_t()> cb) : OSThread(name), callback(std::move(cb)) {}
protected:
int32_t runOnce() override { return callback(); }
int32_t runOnce() override { return callback ? callback() : 0; }
private:
std::function<int32_t()> callback;
};
} // namespace concurrency
+29 -5
View File
@@ -117,6 +117,25 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const
{
TwoWire *i2cBus = fetchI2CBus(addr);
i2cBus->beginTransmission(addr.address);
if (command > 0xFF) {
i2cBus->write((uint8_t)(command >> 8));
}
i2cBus->write((uint8_t)(command & 0xFF));
if (i2cBus->endTransmission() != 0) {
return false;
}
delay(20);
uint8_t received = i2cBus->requestFrom(addr.address, expectedLength);
bool match = (received == expectedLength);
while (i2cBus->available())
i2cBus->read();
return match;
}
/// for SEN5X detection
// 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
@@ -432,8 +451,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
type = OPT3001;
logFoundDevice("OPT3001", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
0) { // unique SHT4x serial number (6 bytes inc. CRC)
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address);
} else {
@@ -458,13 +476,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
case LPS22HB_ADDR_ALT:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
if (registerValue != 0) {
// SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response
if (i2cCommandResponseLength(addr, 0xD060, 48)) {
type = SFA30;
logFoundDevice("SFA30", (uint8_t)addr.address);
break;
}
// TODO - What happens with these two?
// Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1)
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
if (registerValue == 0xB1) {
type = LPS22HB;
logFoundDevice("LPS22HB", (uint8_t)addr.address);
}
break;
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
+2
View File
@@ -55,6 +55,8 @@ class ScanI2CTwoWire : public ScanI2C
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const;
bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const;
DeviceType probeOLED(ScanI2C::DeviceAddress) const;
static void logFoundDevice(const char *device, uint8_t address);
+11
View File
@@ -259,6 +259,17 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(MINI_EPAPER_S3)
spi1 = new SPIClass(HSPI);
spi1->begin(PIN_SPI1_SCK, PIN_SPI1_MISO, PIN_SPI1_MOSI, PIN_EINK_CS);
// Create GxEPD2 objects
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(1);
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
// Detect display model, before starting SPI
+2 -1
View File
@@ -93,7 +93,8 @@ class EInkDisplay : public OLEDDisplay
SPIClass *hspi = NULL;
#endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) || \
defined(MINI_EPAPER_S3)
SPIClass *spi1 = NULL;
#endif
+4
View File
@@ -875,6 +875,10 @@ int32_t Screen::runOnce()
break;
case Cmd::STOP_ALERT_FRAME:
NotificationRenderer::pauseBanner = false;
// Return from one-off alert mode back to regular frames.
if (!showingNormalScreen && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
setFrames();
}
break;
case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
+2 -2
View File
@@ -1209,8 +1209,8 @@ void TFTDisplay::display(bool fromBlank)
bool somethingChanged = false;
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8);
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
colorTftMesh = __builtin_bswap16(TFT_MESH);
colorTftBlack = __builtin_bswap16(TFT_BLACK);
y = 0;
while (y < displayHeight) {
+15
View File
@@ -144,6 +144,21 @@ void InkHUD::Applet::resetDrawingSpace()
setFont(fontSmall);
}
// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it
void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured)
{
if (captured)
subscribedInputs |= input;
else
subscribedInputs &= ~input;
}
// Checks if a specific input is enabled for this applet and should be sent to it
bool InkHUD::Applet::isInputSubscribed(InputMask input)
{
return (subscribedInputs & input) == input;
}
// Tell InkHUD::Renderer that we want to render now
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
// When an applet decides it has heard something important, and wants to redraw, it calls this method
+22
View File
@@ -89,6 +89,9 @@ class Applet : public GFX
virtual void onForeground() {}
virtual void onBackground() {}
virtual void onShutdown() {}
// Input Events
virtual void onButtonShortPress() {}
virtual void onButtonLongPress() {}
virtual void onExitShort() {}
@@ -100,6 +103,18 @@ class Applet : public GFX
virtual void onFreeText(char c) {}
virtual void onFreeTextDone() {}
virtual void onFreeTextCancel() {}
// List of inputs which can be subscribed to
enum InputMask { // | No Joystick | With Joystick |
BUTTON_SHORT = 1, // | Button Click | Joystick Center Click |
BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold |
EXIT_SHORT = 4, // | no-op | Back Button Click |
EXIT_LONG = 8, // | no-op | Back Button Hold |
NAV_UP = 16, // | no-op | Joystick Up |
NAV_DOWN = 32, // | no-op | Joystick Down |
NAV_LEFT = 64, // | no-op | Joystick Left |
NAV_RIGHT = 128 // | no-op | Joystick Right |
};
bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded.
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
@@ -121,6 +136,13 @@ class Applet : public GFX
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
void resetCrop(); // Removes setCrop()
// User Input Handling
uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs
void setInputsSubscribed(uint8_t input,
bool captured); // Set if an input should be handled by applet or not, this should not be
// overloaded. Can take multiple inputs at once if you OR/`|` them together
// Text
void setFont(AppletFont f);
@@ -0,0 +1,79 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./UserAppletInputExample.h"
using namespace NicheGraphics;
void InkHUD::UserAppletInputExampleApplet::onActivate()
{
setGrabbed(false);
}
void InkHUD::UserAppletInputExampleApplet::onRender(bool full)
{
drawHeader("Input Example");
uint16_t headerHeight = getHeaderHeight();
std::string buttonName;
if (settings->joystick.enabled)
buttonName = "joystick center button";
else
buttonName = "user button";
std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls";
if (!isGrabbed)
additional = " | Control is released, long press " + buttonName + " to grab controls";
printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional);
}
void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed)
{
isGrabbed = grabbed;
setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT,
grabbed); // Enables/disables grabbing all inputs
setInputsSubscribed(BUTTON_LONG, true); // Always grab this input
}
void InkHUD::UserAppletInputExampleApplet::onButtonShortPress()
{
lastInput = "BUTTON_SHORT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onButtonLongPress()
{
lastInput = "BUTTON_LONG";
setGrabbed(!isGrabbed);
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onExitShort()
{
lastInput = "EXIT_SHORT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onExitLong()
{
lastInput = "EXIT_LONG";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavUp()
{
lastInput = "NAV_UP";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavDown()
{
lastInput = "NAV_DOWN";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavLeft()
{
lastInput = "NAV_LEFT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavRight()
{
lastInput = "NAV_RIGHT";
requestUpdate();
}
#endif
@@ -0,0 +1,36 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class UserAppletInputExampleApplet : public Applet
{
public:
void onActivate() override;
void onRender(bool full) override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onExitShort() override;
void onExitLong() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
private:
std::string lastInput = "None";
bool isGrabbed = false;
void setGrabbed(bool grabbed);
};
} // namespace NicheGraphics::InkHUD
#endif
@@ -1524,7 +1524,15 @@ void InkHUD::MenuApplet::onButtonShortPress()
if (!settings->joystick.enabled) {
if (!cursorShown) {
cursorShown = true;
// Select the first item that isn't a header
cursor = 0;
while (cursor < items.size() && items.at(cursor).isHeader) {
cursor++;
}
if (cursor >= items.size()) {
cursorShown = false;
cursor = 0;
}
} else {
do {
cursor = (cursor + 1) % items.size();
@@ -1576,7 +1584,15 @@ void InkHUD::MenuApplet::onNavUp()
if (!cursorShown) {
cursorShown = true;
cursor = 0;
// Select the last item that isn't a header
cursor = items.size() - 1;
while (items.at(cursor).isHeader) {
if (cursor == 0) {
cursorShown = false;
break;
}
cursor--;
}
} else {
do {
if (cursor == 0)
@@ -1597,7 +1613,15 @@ void InkHUD::MenuApplet::onNavDown()
if (!cursorShown) {
cursorShown = true;
// Select the first item that isn't a header
cursor = 0;
while (cursor < items.size() && items.at(cursor).isHeader) {
cursor++;
}
if (cursor >= items.size()) {
cursorShown = false;
cursor = 0;
}
} else {
do {
cursor = (cursor + 1) % items.size();
@@ -73,7 +73,7 @@ ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshP
}
} else {
// For non-local packets, this applet only reacts to favorited nodes.
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
if (!sender || !sender->is_favorite)
return ProcessMessage::CONTINUE;
+61 -12
View File
@@ -59,10 +59,16 @@ void InkHUD::Events::onButtonShort()
if (consumer) {
consumer->onButtonShortPress();
} else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
if (!settings->joystick.enabled)
inkhud->nextApplet();
else
inkhud->openMenu();
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT))
userConsumer->onButtonShortPress();
else {
if (!settings->joystick.enabled)
inkhud->nextApplet();
else
inkhud->openMenu();
}
}
}
@@ -84,8 +90,14 @@ void InkHUD::Events::onButtonLong()
// If no system applet is handling input, default behavior instead is to open the menu
if (consumer)
consumer->onButtonLongPress();
else
inkhud->openMenu();
else {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG))
userConsumer->onButtonLongPress();
else
inkhud->openMenu();
}
}
void InkHUD::Events::onExitShort()
@@ -110,8 +122,14 @@ void InkHUD::Events::onExitShort()
// If no system applet is handling input, default behavior instead is change tiles
if (consumer)
consumer->onExitShort();
else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
inkhud->nextTile();
else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT))
userConsumer->onExitShort();
else
inkhud->nextTile();
}
}
}
@@ -133,6 +151,13 @@ void InkHUD::Events::onExitLong()
if (consumer)
consumer->onExitLong();
else {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG))
userConsumer->onExitLong();
// Nothing uses exit long yet
}
}
}
@@ -157,6 +182,12 @@ void InkHUD::Events::onNavUp()
if (consumer)
consumer->onNavUp();
else if (!dismissedExt) {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP))
userConsumer->onNavUp();
}
}
}
@@ -181,6 +212,12 @@ void InkHUD::Events::onNavDown()
if (consumer)
consumer->onNavDown();
else if (!dismissedExt) {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN))
userConsumer->onNavDown();
}
}
}
@@ -206,8 +243,14 @@ void InkHUD::Events::onNavLeft()
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavLeft();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->prevApplet();
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT))
userConsumer->onNavLeft();
else
inkhud->prevApplet();
}
}
}
@@ -233,8 +276,14 @@ void InkHUD::Events::onNavRight()
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavRight();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->nextApplet();
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT))
userConsumer->onNavRight();
else
inkhud->nextApplet();
}
}
}
+6
View File
@@ -210,6 +210,12 @@ void InkHUD::InkHUD::prevApplet()
windowManager->prevApplet();
}
// Returns the currently active applet
InkHUD::Applet *InkHUD::InkHUD::getActiveApplet()
{
return windowManager->getActiveApplet();
}
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::InkHUD::openMenu()
+1
View File
@@ -74,6 +74,7 @@ class InkHUD
void nextApplet();
void prevApplet();
NicheGraphics::InkHUD::Applet *getActiveApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
@@ -8,5 +8,5 @@ build_flags =
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps =
# TODO renovate
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
# renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root
https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX
@@ -273,6 +273,12 @@ void InkHUD::WindowManager::prevApplet()
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// Returns active applet
NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet()
{
return userTiles.at(settings->userTiles.focused)->getAssignedApplet();
}
// Rotate the display image by 90 degrees
void InkHUD::WindowManager::rotate()
{
@@ -29,6 +29,7 @@ class WindowManager
void nextTile();
void prevTile();
Applet *getActiveApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
+2 -1
View File
@@ -276,7 +276,8 @@ int32_t ButtonThread::runOnce()
case BUTTON_EVENT_LONG_RELEASED: {
LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime);
if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE &&
// Require press started after boot holdoff to avoid phantom shutdown from floating pins
if (millis() > 30000 && buttonPressStartTime > 30000 && _longLongPress != INPUT_BROKER_NONE &&
(millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) {
evt.inputEvent = _longLongPress;
this->notifyObservers(&evt);
+4 -5
View File
@@ -106,8 +106,8 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
{}};
HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
reset();
}
@@ -147,7 +147,6 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
@@ -187,8 +186,8 @@ void HackadayCommunicatorKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -18,8 +18,8 @@ class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+16 -1
View File
@@ -100,13 +100,28 @@ void InputBroker::processInputEventQueue()
int InputBroker::handleInputEvent(const InputEvent *event)
{
powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release
#if HAS_SCREEN
bool screenWasOff = false;
if (screen) {
screenWasOff = !screen->isScreenOn();
}
#endif
powerFSM.trigger(EVENT_INPUT);
if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule &&
moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) {
externalNotificationModule->stopNow();
// If this turns off a notification, don't further process the event
return 0;
}
#if HAS_SCREEN
if (screen && screenWasOff) {
// If the screen was off, it is in the process of turning on, and we just drop the event
return 0;
}
#endif
this->notifyObservers(event);
return 0;
}
+3 -3
View File
@@ -91,7 +91,7 @@ MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(null
{
// LOG_DEBUG("MPR121 @ %02x", m_addr);
state = Init;
last_key = -1;
last_key = UINT8_MAX;
last_tap = 0L;
char_idx = 0;
queue = "";
@@ -359,8 +359,8 @@ void MPR121Keyboard::released()
return;
}
// would clear longpress callback... later.
if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = -1;
if (last_key >= _NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = UINT8_MAX;
state = Idle;
return;
}
+1 -1
View File
@@ -14,7 +14,7 @@ class MPR121Keyboard
MPR121States state;
int8_t last_key;
uint8_t last_key;
uint32_t last_tap;
uint8_t char_idx;
+5 -5
View File
@@ -43,8 +43,8 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
};
TCA8418Keyboard::TCA8418Keyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0),
should_backspace(false)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0),
tap_interval(0), should_backspace(false)
{
}
@@ -71,7 +71,7 @@ void TCA8418Keyboard::pressed(uint8_t key)
}
// Compute key index based on dynamic row/column
next_key = (int8_t)(row * _TCA8418_COLS + col);
next_key = (uint8_t)(row * _TCA8418_COLS + col);
// LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key);
@@ -105,8 +105,8 @@ void TCA8418Keyboard::released()
return;
}
if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -14,8 +14,8 @@ class TCA8418Keyboard : public TCA8418KeyboardBase
void pressed(uint8_t key) override;
void released(void) override;
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+4 -5
View File
@@ -62,8 +62,8 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = {
};
TDeckProKeyboard::TDeckProKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
}
@@ -101,7 +101,6 @@ void TDeckProKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -142,8 +141,8 @@ void TDeckProKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -19,8 +19,8 @@ class TDeckProKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+4 -5
View File
@@ -65,8 +65,8 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'},
{' ', 0x00, Key::BL_TOGGLE}};
TLoraPagerKeyboard::TLoraPagerKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
@@ -129,7 +129,6 @@ void TLoraPagerKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -170,8 +169,8 @@ void TLoraPagerKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -21,8 +21,8 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+17 -7
View File
@@ -9,6 +9,7 @@
#include "PowerMon.h"
#include "RadioLibInterface.h"
#include "ReliableRouter.h"
#include "TransmitHistory.h"
#include "airtime.h"
#include "buzz.h"
#include "power/PowerHAL.h"
@@ -192,8 +193,6 @@ bool kb_found = false;
// global bool to record that on-screen keyboard (OSK) is present
bool osk_found = false;
unsigned long last_listen = 0;
// The I2C address of the RTC Module (if found)
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Accelerometer (if found)
@@ -248,7 +247,6 @@ uint32_t timeLastPowered = 0;
static OSThread *powerFSMthread;
OSThread *ambientLightingThread;
RadioInterface *rIf = NULL;
RadioLibHal *RadioLibHAL = NULL;
/**
@@ -706,6 +704,9 @@ void setup()
// We do this as early as possible because this loads preferences from flash
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
nodeDB = new NodeDB;
// Initialize transmit history to persist broadcast throttle timers across reboots
TransmitHistory::getInstance()->loadFromDisk();
#if HAS_TFT
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
tftSetup();
@@ -1121,10 +1122,19 @@ void loop()
#endif
power->powerCommandsCheck();
if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) &&
!(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) {
RadioLibInterface::instance->startReceive();
LOG_DEBUG("attempting AGC reset");
if (RadioLibInterface::instance != nullptr) {
static uint32_t lastRadioMissedIrqPoll;
if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) {
lastRadioMissedIrqPoll = millis();
RadioLibInterface::instance->pollMissedIrqs();
}
// Periodic AGC reset — warm sleep + recalibrate to prevent stuck AGC gain
static uint32_t lastAgcReset;
if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) {
lastAgcReset = millis();
RadioLibInterface::instance->resetAGC();
}
}
#ifdef DEBUG_STACK
-1
View File
@@ -33,7 +33,6 @@ extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model;
extern bool kb_found;
extern bool osk_found;
extern unsigned long last_listen;
extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::FoundDevice rgb_found;
+33
View File
@@ -263,6 +263,7 @@ template <typename T> void LR11x0Interface<T>::startReceive()
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
@@ -298,6 +299,38 @@ template <typename T> bool LR11x0Interface<T>::isActivelyReceiving()
RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
}
#ifdef LR11X0_AGC_RESET
template <typename T> void LR11x0Interface<T>::resetAGC()
{
// Safety: don't reset mid-packet
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
return;
LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)");
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
lora.sleep(true, 0);
// 2. Wake to RC standby for stable calibration
lora.standby(RADIOLIB_LR11X0_STANDBY_RC, true);
// 3. Calibrate all blocks (PLL, ADC, image, RC oscillators)
// calibrate() is protected on LR11x0, so use raw SPI (same as internal implementation)
uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL;
module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true);
// 4. Re-calibrate image rejection for actual operating frequency
// Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions.
lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f);
// 5. Re-apply RX boosted gain mode
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
// 6. Resume receiving
startReceive();
}
#endif
template <typename T> bool LR11x0Interface<T>::sleep()
{
// \todo Display actual typename of the adapter, not just `LR11x0`
+4
View File
@@ -27,6 +27,10 @@ template <class T> class LR11x0Interface : public RadioLibInterface
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
#ifdef LR11X0_AGC_RESET
void resetAGC() override;
#endif
protected:
/**
* Specific module instance
+12
View File
@@ -106,6 +106,18 @@ class MeshModule
/* We allow modules to ignore a request without sending an error if they have a specific reason for it. */
bool ignoreRequest = false;
/**
* Check if the current request is a multi-hop broadcast. Modules should call this in allocReply()
* and return NULL to prevent reply storms from broadcast requests that have already been relayed.
*/
bool isMultiHopBroadcastRequest()
{
if (currentRequest && isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) {
return true;
}
return false;
}
/** If a bound channel name is set, we will only accept received packets that come in on that channel.
* A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface
* are allowed on any channel (this lets the local user do anything).
+5 -6
View File
@@ -143,9 +143,8 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_
digitalWrite(rst, HIGH);
delay(10);
uint32_t ID = 0;
ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst);
ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice
readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst);
uint32_t ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice
return ID;
}
@@ -322,9 +321,9 @@ NodeDB::NodeDB()
// Uncomment below to always enable UDP broadcasts
// config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST;
// If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum value
// of 30 minutes or more
if (channels.isDefaultChannel(channels.getPrimaryIndex())) {
// If we are setup to broadcast on any default channel slot (with default frequency slot semantics),
// ensure that the telemetry intervals are coerced to the minimum value of 30 minutes or more.
if (channels.hasDefaultChannel()) {
LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults");
moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue(
moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs);
+1
View File
@@ -301,6 +301,7 @@ void RF95Interface::startReceive()
// Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
}
bool RF95Interface::isChannelActive()
+21 -2
View File
@@ -517,12 +517,31 @@ void RadioLibInterface::handleReceiveInterrupt()
void RadioLibInterface::startReceive()
{
// Note the updated timestamp, to avoid unneeded AGC resets
last_listen = millis();
isReceiving = true;
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);
}
void RadioLibInterface::pollMissedIrqs()
{
// RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges.
if (isReceiving) {
checkRxDoneIrqFlag();
}
}
void RadioLibInterface::resetAGC()
{
// Base implementation: no-op. Override in chip-specific subclasses.
}
void RadioLibInterface::checkRxDoneIrqFlag()
{
if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
LOG_WARN("caught missed RX_DONE");
notify(ISR_RX, true);
}
}
void RadioLibInterface::configHardwareForSend()
{
powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn);
+16
View File
@@ -19,6 +19,8 @@
// In addition to the default Rx flags, we need the PREAMBLE_DETECTED flag to detect whether we are actively receiving
#define MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS (RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1 << RADIOLIB_IRQ_PREAMBLE_DETECTED))
#define AGC_RESET_INTERVAL_MS (60 * 1000) // 60 seconds
/**
* We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access
*/
@@ -112,6 +114,18 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
*/
virtual void enableInterrupt(void (*)()) = 0;
/**
* Poll as a backup to catch missed edge-triggered interrupts.
*/
void pollMissedIrqs();
/**
* Reset AGC by power-cycling the analog frontend.
* Subclasses override with chip-specific calibration sequences.
* Safe to call periodically — skips if currently sending or receiving.
*/
virtual void resetAGC();
/**
* Debugging counts
*/
@@ -264,4 +278,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
*/
bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override;
void checkRxDoneIrqFlag();
};
+90
View File
@@ -6,6 +6,10 @@
#ifdef ARCH_PORTDUINO
#include "PortduinoGlue.h"
#endif
#if defined(USE_GC1109_PA) && defined(ARCH_ESP32)
#include <driver/rtc_io.h>
#include <esp_sleep.h>
#endif
#include "Throttle.h"
@@ -55,14 +59,33 @@ template <typename T> bool SX126xInterface<T>::init()
#if defined(USE_GC1109_PA)
// GC1109 FEM chip initialization
// See variant.h for full pin mapping and control logic documentation
//
// On deep sleep wake, PA_POWER and PA_EN are held HIGH by RTC latch (set in
// enableLoraInterrupt). We configure GPIO registers before releasing the hold
// so the pad transitions atomically from held-HIGH to register-HIGH with no
// power glitch. On cold boot the hold_dis is a harmless no-op.
// VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on)
pinMode(LORA_PA_POWER, OUTPUT);
digitalWrite(LORA_PA_POWER, HIGH);
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER);
// TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot, wait
// for VBAT to stabilise before driving CSD/CPS, per GC1109 requirement:
// "VBAT must be prior to CSD/CPS/CTX for the power on sequence"
// On deep sleep wake the LDO was held on via RTC latch, so no delay needed.
#if defined(ARCH_ESP32)
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) {
delayMicroseconds(1000);
}
#else
delayMicroseconds(1000);
#endif
// CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX
pinMode(LORA_PA_EN, OUTPUT);
digitalWrite(LORA_PA_EN, HIGH);
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_EN);
// CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care)
// Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH
@@ -171,6 +194,17 @@ template <typename T> bool SX126xInterface<T>::init()
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result);
}
#ifdef USE_GC1109_PA
// Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity
// on boards with the GC1109 FEM. Sets bit 0 of register 0x8B5.
// Reference: https://github.com/meshcore-dev/MeshCore/pull/1398
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) {
LOG_INFO("Applied SX1262 register 0x8B5 patch for GC1109 RX improvement");
} else {
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for GC1109");
}
#endif
#if 0
// Read/write a register we are not using (only used for FSK mode) to test SPI comms
uint8_t crcLSB = 0;
@@ -328,6 +362,7 @@ template <typename T> void SX126xInterface<T>::startReceive()
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
@@ -399,6 +434,61 @@ template <typename T> bool SX126xInterface<T>::sleep()
return true;
}
template <typename T> void SX126xInterface<T>::resetAGC()
{
// Safety: don't reset mid-packet
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
return;
LOG_DEBUG("SX126x AGC reset: warm sleep + Calibrate(0x7F)");
// 1. Warm sleep — powers down the entire analog frontend, resetting AGC state.
// A plain standby→startReceive cycle does NOT reset the AGC.
lora.sleep(true);
// 2. Wake to RC standby for stable calibration
lora.standby(RADIOLIB_SX126X_STANDBY_RC, true);
// 3. Calibrate all blocks (ADC, PLL, image, RC oscillators)
uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL;
module.SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false);
// 4. Wait for calibration to complete (BUSY pin goes low)
module.hal->delay(5);
uint32_t start = millis();
while (module.hal->digitalRead(module.getGpio())) {
if (millis() - start > 50)
break;
module.hal->yield();
}
if (module.hal->digitalRead(module.getGpio())) {
LOG_WARN("SX126x AGC reset: calibration did not complete within 50ms");
startReceive();
return;
}
// 5. Re-calibrate image rejection for actual operating frequency
// Calibrate(0x7F) defaults to 902-928 MHz which is wrong for other regions.
lora.calibrateImage(getFreq());
// Re-apply settings that calibration may have reset
// DIO2 as RF switch
#ifdef SX126X_DIO2_AS_RF_SWITCH
lora.setDio2AsRfSwitch(true);
#elif defined(ARCH_PORTDUINO)
if (portduino_config.dio2_as_rf_switch)
lora.setDio2AsRfSwitch(true);
#endif
// RX boosted gain mode
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
// 6. Resume receiving
startReceive();
}
/** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */
template <typename T> void SX126xInterface<T>::setTransmitEnable(bool txon)
{
+2
View File
@@ -28,6 +28,8 @@ template <class T> class SX126xInterface : public RadioLibInterface
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
void resetAGC() override;
void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; }
protected:
+1
View File
@@ -271,6 +271,7 @@ template <typename T> void SX128xInterface<T>::startReceive()
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
+202
View File
@@ -0,0 +1,202 @@
#include "TransmitHistory.h"
#include "FSCommon.h"
#include "RTC.h"
#include "SPILock.h"
#include <Throttle.h>
#ifdef FSCom
TransmitHistory *transmitHistory = nullptr;
TransmitHistory *TransmitHistory::getInstance()
{
if (!transmitHistory) {
transmitHistory = new TransmitHistory();
}
return transmitHistory;
}
void TransmitHistory::loadFromDisk()
{
spiLock->lock();
auto file = FSCom.open(FILENAME, FILE_O_READ);
if (file) {
FileHeader header{};
if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC &&
header.version == VERSION && header.count <= MAX_ENTRIES) {
for (uint8_t i = 0; i < header.count; i++) {
Entry entry{};
if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry)) {
if (entry.epochSeconds > 0) {
history[entry.key] = entry.epochSeconds;
// Seed in-memory millis so throttle works even without RTC/GPS.
// Treating stored entries as "just sent" is safe — worst case the
// node waits one full interval before its first broadcast.
lastMillis[entry.key] = millis();
}
}
}
LOG_INFO("TransmitHistory: loaded %u entries from disk", header.count);
} else {
LOG_WARN("TransmitHistory: invalid file header, starting fresh");
}
file.close();
} else {
LOG_INFO("TransmitHistory: no history file found, starting fresh");
}
spiLock->unlock();
dirty = false;
}
void TransmitHistory::setLastSentToMesh(uint16_t key)
{
lastMillis[key] = millis();
uint32_t now = getTime();
if (now >= 2) {
history[key] = now;
dirty = true;
// Don't flush to disk on every transmit — flash has limited write endurance.
// The in-memory lastMillis map handles throttle during normal operation.
// Disk is flushed: before deep sleep (sleep.cpp) and periodically here,
// throttled to at most once per 5 minutes. Always save the first time
// after boot so a crash-reboot loop can't avoid persisting.
if (lastDiskSave == 0 || !Throttle::isWithinTimespanMs(lastDiskSave, SAVE_INTERVAL_MS)) {
if (saveToDisk()) {
lastDiskSave = millis();
}
}
}
}
uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const
{
auto it = history.find(key);
if (it != history.end()) {
return it->second;
}
return 0;
}
uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const
{
// Prefer runtime millis value (accurate within this boot)
auto mit = lastMillis.find(key);
if (mit != lastMillis.end()) {
return mit->second;
}
// Fall back to epoch conversion (loaded from disk after reboot)
uint32_t storedEpoch = getLastSentToMeshEpoch(key);
if (storedEpoch == 0) {
return 0; // No stored time — module has never sent
}
uint32_t now = getTime();
if (now < 2) {
// No valid RTC time yet — can't convert to millis. Return 0 so throttle doesn't block.
return 0;
}
if (storedEpoch > now) {
// Stored time is in the future (clock went backwards?) — treat as stale
return 0;
}
uint32_t secondsAgo = now - storedEpoch;
uint32_t msAgo = secondsAgo * 1000;
// Guard against overflow: if the transmit was very long ago, just return 0 (won't throttle)
if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) {
return 0;
}
// Convert to a millis()-relative timestamp: millis() - msAgo
// This gives a value that, when passed to Throttle::isWithinTimespanMs(value, interval),
// correctly reports whether the transmit was within interval ms.
return millis() - msAgo;
}
bool TransmitHistory::saveToDisk()
{
if (!dirty) {
return true;
}
spiLock->lock();
FSCom.mkdir("/prefs");
// Remove old file first
if (FSCom.exists(FILENAME)) {
FSCom.remove(FILENAME);
}
auto file = FSCom.open(FILENAME, FILE_O_WRITE);
if (file) {
FileHeader header{};
header.magic = MAGIC;
header.version = VERSION;
header.count = (uint8_t)min((size_t)MAX_ENTRIES, history.size());
file.write((uint8_t *)&header, sizeof(header));
uint8_t written = 0;
for (auto &pair : history) {
if (written >= MAX_ENTRIES)
break;
Entry entry{};
entry.key = pair.first;
entry.epochSeconds = pair.second;
file.write((uint8_t *)&entry, sizeof(entry));
written++;
}
file.flush();
file.close();
LOG_DEBUG("TransmitHistory: saved %u entries to disk", written);
dirty = false;
spiLock->unlock();
return true;
} else {
LOG_WARN("TransmitHistory: failed to open file for writing");
}
spiLock->unlock();
return false;
}
#else
// No filesystem available — provide stub with in-memory tracking
TransmitHistory *transmitHistory = nullptr;
TransmitHistory *TransmitHistory::getInstance()
{
if (!transmitHistory) {
transmitHistory = new TransmitHistory();
}
return transmitHistory;
}
void TransmitHistory::loadFromDisk() {}
void TransmitHistory::setLastSentToMesh(uint16_t key)
{
lastMillis[key] = millis();
}
uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const
{
return 0;
}
uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const
{
auto mit = lastMillis.find(key);
return (mit != lastMillis.end()) ? mit->second : 0;
}
bool TransmitHistory::saveToDisk()
{
return true;
}
#endif
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include "configuration.h"
#include <Arduino.h>
#include <map>
/**
* TransmitHistory persists the last broadcast transmit time (epoch seconds) per portnum
* to the filesystem so that throttle checks survive reboots/crashes.
*
* On boot, modules call getLastSentToMeshMillis() to recover a millis()-relative timestamp
* from the stored epoch time, which plugs directly into existing throttle logic.
*
* On every broadcast transmit, modules call setLastSentToMesh() which updates the
* in-memory cache and flushes to disk.
*
* Keys are meshtastic_PortNum values (one entry per portnum).
*/
#include "mesh/generated/meshtastic/portnums.pb.h"
class TransmitHistory
{
public:
static TransmitHistory *getInstance();
/**
* Load persisted transmit times from disk. Call once during init after filesystem is ready.
*/
void loadFromDisk();
/**
* Record that a broadcast was sent for the given key right now.
* Stores epoch seconds and flushes to disk.
*/
void setLastSentToMesh(uint16_t key);
/**
* Get the last transmit epoch seconds for a given key, or 0 if unknown.
*/
uint32_t getLastSentToMeshEpoch(uint16_t key) const;
/**
* Convert a stored epoch timestamp into a millis()-relative timestamp suitable
* for use with Throttle::isWithinTimespanMs().
*
* Returns 0 if no valid time is stored or if the stored time is in the future
* (which shouldn't happen but guards against clock weirdness).
*
* Example: if the stored epoch is 300 seconds ago, and millis() is currently 10000,
* this returns 10000 - 300000 (wrapped appropriately for uint32_t arithmetic).
*/
uint32_t getLastSentToMeshMillis(uint16_t key) const;
/**
* Flush dirty entries to disk. Called periodically or on demand.
*
* @return true if the data is persisted (or there was nothing to write), false on write/open failure.
*/
bool saveToDisk();
private:
TransmitHistory() = default;
static constexpr const char *FILENAME = "/prefs/transmit_history.dat";
static constexpr uint32_t MAGIC = 0x54485354; // "THST"
static constexpr uint8_t VERSION = 1;
static constexpr uint8_t MAX_ENTRIES = 16;
static constexpr uint32_t SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
struct __attribute__((packed)) Entry {
uint16_t key;
uint32_t epochSeconds;
};
struct __attribute__((packed)) FileHeader {
uint32_t magic;
uint8_t version;
uint8_t count;
};
std::map<uint16_t, uint32_t> history; // key -> epoch seconds (for disk persistence)
std::map<uint16_t, uint32_t> lastMillis; // key -> millis() value (for runtime throttle)
bool dirty = false;
uint32_t lastDiskSave = 0; // millis() of last disk flush
};
extern TransmitHistory *transmitHistory;
+5 -3
View File
@@ -81,7 +81,9 @@ typedef enum _meshtastic_AdminMessage_ModuleConfigType {
/* TODO: REPLACE */
meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13,
/* Traffic management module config */
meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG = 14
meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG = 14,
/* TAK module config */
meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG = 15
} meshtastic_AdminMessage_ModuleConfigType;
typedef enum _meshtastic_AdminMessage_BackupLocation {
@@ -395,8 +397,8 @@ extern "C" {
#define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1))
#define _meshtastic_AdminMessage_ModuleConfigType_MIN meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG
#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG
#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG+1))
#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG
#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG+1))
#define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH
#define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD
@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
/* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
#define meshtastic_BackupPreferences_size 2419
#define meshtastic_BackupPreferences_size 2426
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196
+10 -4
View File
@@ -93,6 +93,9 @@ typedef struct _meshtastic_LocalModuleConfig {
/* The part of the config that is specific to the Traffic Management module */
bool has_traffic_management;
meshtastic_ModuleConfig_TrafficManagementConfig traffic_management;
/* TAK Config */
bool has_tak;
meshtastic_ModuleConfig_TAKConfig tak;
} meshtastic_LocalModuleConfig;
@@ -102,9 +105,9 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_LocalConfig_init_default {false, meshtastic_Config_DeviceConfig_init_default, false, meshtastic_Config_PositionConfig_init_default, false, meshtastic_Config_PowerConfig_init_default, false, meshtastic_Config_NetworkConfig_init_default, false, meshtastic_Config_DisplayConfig_init_default, false, meshtastic_Config_LoRaConfig_init_default, false, meshtastic_Config_BluetoothConfig_init_default, 0, false, meshtastic_Config_SecurityConfig_init_default}
#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_default}
#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_default, false, meshtastic_ModuleConfig_TAKConfig_init_default}
#define meshtastic_LocalConfig_init_zero {false, meshtastic_Config_DeviceConfig_init_zero, false, meshtastic_Config_PositionConfig_init_zero, false, meshtastic_Config_PowerConfig_init_zero, false, meshtastic_Config_NetworkConfig_init_zero, false, meshtastic_Config_DisplayConfig_init_zero, false, meshtastic_Config_LoRaConfig_init_zero, false, meshtastic_Config_BluetoothConfig_init_zero, 0, false, meshtastic_Config_SecurityConfig_init_zero}
#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_zero}
#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_zero, false, meshtastic_ModuleConfig_TAKConfig_init_zero}
/* Field tags (for use in manual encoding/decoding) */
#define meshtastic_LocalConfig_device_tag 1
@@ -132,6 +135,7 @@ extern "C" {
#define meshtastic_LocalModuleConfig_paxcounter_tag 14
#define meshtastic_LocalModuleConfig_statusmessage_tag 15
#define meshtastic_LocalModuleConfig_traffic_management_tag 16
#define meshtastic_LocalModuleConfig_tak_tag 17
/* Struct field encoding specification for nanopb */
#define meshtastic_LocalConfig_FIELDLIST(X, a) \
@@ -171,7 +175,8 @@ X(a, STATIC, OPTIONAL, MESSAGE, ambient_lighting, 12) \
X(a, STATIC, OPTIONAL, MESSAGE, detection_sensor, 13) \
X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) \
X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) \
X(a, STATIC, OPTIONAL, MESSAGE, traffic_management, 16)
X(a, STATIC, OPTIONAL, MESSAGE, traffic_management, 16) \
X(a, STATIC, OPTIONAL, MESSAGE, tak, 17)
#define meshtastic_LocalModuleConfig_CALLBACK NULL
#define meshtastic_LocalModuleConfig_DEFAULT NULL
#define meshtastic_LocalModuleConfig_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig
@@ -189,6 +194,7 @@ X(a, STATIC, OPTIONAL, MESSAGE, traffic_management, 16)
#define meshtastic_LocalModuleConfig_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig
#define meshtastic_LocalModuleConfig_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig
#define meshtastic_LocalModuleConfig_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig
#define meshtastic_LocalModuleConfig_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig
extern const pb_msgdesc_t meshtastic_LocalConfig_msg;
extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
@@ -200,7 +206,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
#define meshtastic_LocalConfig_size 751
#define meshtastic_LocalModuleConfig_size 813
#define meshtastic_LocalModuleConfig_size 820
#ifdef __cplusplus
} /* extern "C" */
+4
View File
@@ -300,6 +300,10 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_TBEAM_1_WATT = 122,
/* LilyGo T5 S3 ePaper Pro (V1 and V2) */
meshtastic_HardwareModel_T5_S3_EPAPER_PRO = 123,
/* LilyGo T-Beam BPF (144-148Mhz) */
meshtastic_HardwareModel_TBEAM_BPF = 124,
/* LilyGo T-Mini E-paper S3 Kit */
meshtastic_HardwareModel_MINI_EPAPER_S3 = 125,
/* ------------------------------------------------------------------------------------------------------------------------------------------
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
------------------------------------------------------------------------------------------------------------------------------------------ */
@@ -57,6 +57,9 @@ PB_BIND(meshtastic_ModuleConfig_AmbientLightingConfig, meshtastic_ModuleConfig_A
PB_BIND(meshtastic_ModuleConfig_StatusMessageConfig, meshtastic_ModuleConfig_StatusMessageConfig, AUTO)
PB_BIND(meshtastic_ModuleConfig_TAKConfig, meshtastic_ModuleConfig_TAKConfig, AUTO)
PB_BIND(meshtastic_RemoteHardwarePin, meshtastic_RemoteHardwarePin, AUTO)
@@ -4,6 +4,7 @@
#ifndef PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED
#define PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED
#include <pb.h>
#include "meshtastic/atak.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
@@ -448,6 +449,16 @@ typedef struct _meshtastic_ModuleConfig_StatusMessageConfig {
char node_status[80];
} meshtastic_ModuleConfig_StatusMessageConfig;
/* TAK team/role configuration */
typedef struct _meshtastic_ModuleConfig_TAKConfig {
/* Team color.
Default Unspecifed_Color -> firmware uses Cyan */
meshtastic_Team team;
/* Member role.
Default Unspecifed -> firmware uses TeamMember */
meshtastic_MemberRole role;
} meshtastic_ModuleConfig_TAKConfig;
/* A GPIO pin definition for remote hardware module */
typedef struct _meshtastic_RemoteHardwarePin {
/* GPIO Pin number (must match Arduino) */
@@ -503,6 +514,8 @@ typedef struct _meshtastic_ModuleConfig {
meshtastic_ModuleConfig_StatusMessageConfig statusmessage;
/* Traffic management module config for mesh network optimization */
meshtastic_ModuleConfig_TrafficManagementConfig traffic_management;
/* TAK team/role configuration for TAK_TRACKER */
meshtastic_ModuleConfig_TAKConfig tak;
} payload_variant;
} meshtastic_ModuleConfig;
@@ -560,6 +573,9 @@ extern "C" {
#define meshtastic_ModuleConfig_TAKConfig_team_ENUMTYPE meshtastic_Team
#define meshtastic_ModuleConfig_TAKConfig_role_ENUMTYPE meshtastic_MemberRole
#define meshtastic_RemoteHardwarePin_type_ENUMTYPE meshtastic_RemoteHardwarePinType
@@ -581,6 +597,7 @@ extern "C" {
#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_StatusMessageConfig_init_default {""}
#define meshtastic_ModuleConfig_TAKConfig_init_default {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN}
#define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN}
#define meshtastic_ModuleConfig_init_zero {0, {meshtastic_ModuleConfig_MQTTConfig_init_zero}}
#define meshtastic_ModuleConfig_MQTTConfig_init_zero {0, "", "", "", 0, 0, 0, "", 0, 0, false, meshtastic_ModuleConfig_MapReportSettings_init_zero}
@@ -599,6 +616,7 @@ extern "C" {
#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_StatusMessageConfig_init_zero {""}
#define meshtastic_ModuleConfig_TAKConfig_init_zero {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN}
#define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN}
/* Field tags (for use in manual encoding/decoding) */
@@ -717,6 +735,8 @@ extern "C" {
#define meshtastic_ModuleConfig_AmbientLightingConfig_green_tag 4
#define meshtastic_ModuleConfig_AmbientLightingConfig_blue_tag 5
#define meshtastic_ModuleConfig_StatusMessageConfig_node_status_tag 1
#define meshtastic_ModuleConfig_TAKConfig_team_tag 1
#define meshtastic_ModuleConfig_TAKConfig_role_tag 2
#define meshtastic_RemoteHardwarePin_gpio_pin_tag 1
#define meshtastic_RemoteHardwarePin_name_tag 2
#define meshtastic_RemoteHardwarePin_type_tag 3
@@ -738,6 +758,7 @@ extern "C" {
#define meshtastic_ModuleConfig_paxcounter_tag 13
#define meshtastic_ModuleConfig_statusmessage_tag 14
#define meshtastic_ModuleConfig_traffic_management_tag 15
#define meshtastic_ModuleConfig_tak_tag 16
/* Struct field encoding specification for nanopb */
#define meshtastic_ModuleConfig_FIELDLIST(X, a) \
@@ -755,7 +776,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ambient_lighting,payload_var
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,detection_sensor,payload_variant.detection_sensor), 12) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.paxcounter), 13) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,traffic_management,payload_variant.traffic_management), 15)
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,traffic_management,payload_variant.traffic_management), 15) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,tak,payload_variant.tak), 16)
#define meshtastic_ModuleConfig_CALLBACK NULL
#define meshtastic_ModuleConfig_DEFAULT NULL
#define meshtastic_ModuleConfig_payload_variant_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig
@@ -773,6 +795,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,traffic_management,payload_v
#define meshtastic_ModuleConfig_payload_variant_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig
#define meshtastic_ModuleConfig_payload_variant_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig
#define meshtastic_ModuleConfig_payload_variant_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig
#define meshtastic_ModuleConfig_payload_variant_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig
#define meshtastic_ModuleConfig_MQTTConfig_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, enabled, 1) \
@@ -958,6 +981,12 @@ X(a, STATIC, SINGULAR, STRING, node_status, 1)
#define meshtastic_ModuleConfig_StatusMessageConfig_CALLBACK NULL
#define meshtastic_ModuleConfig_StatusMessageConfig_DEFAULT NULL
#define meshtastic_ModuleConfig_TAKConfig_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, team, 1) \
X(a, STATIC, SINGULAR, UENUM, role, 2)
#define meshtastic_ModuleConfig_TAKConfig_CALLBACK NULL
#define meshtastic_ModuleConfig_TAKConfig_DEFAULT NULL
#define meshtastic_RemoteHardwarePin_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, gpio_pin, 1) \
X(a, STATIC, SINGULAR, STRING, name, 2) \
@@ -982,6 +1011,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_TelemetryConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_CannedMessageConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_AmbientLightingConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_StatusMessageConfig_msg;
extern const pb_msgdesc_t meshtastic_ModuleConfig_TAKConfig_msg;
extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
@@ -1002,6 +1032,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
#define meshtastic_ModuleConfig_CannedMessageConfig_fields &meshtastic_ModuleConfig_CannedMessageConfig_msg
#define meshtastic_ModuleConfig_AmbientLightingConfig_fields &meshtastic_ModuleConfig_AmbientLightingConfig_msg
#define meshtastic_ModuleConfig_StatusMessageConfig_fields &meshtastic_ModuleConfig_StatusMessageConfig_msg
#define meshtastic_ModuleConfig_TAKConfig_fields &meshtastic_ModuleConfig_TAKConfig_msg
#define meshtastic_RemoteHardwarePin_fields &meshtastic_RemoteHardwarePin_msg
/* Maximum encoded size of messages (where known) */
@@ -1020,6 +1051,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
#define meshtastic_ModuleConfig_SerialConfig_size 28
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
#define meshtastic_ModuleConfig_TAKConfig_size 4
#define meshtastic_ModuleConfig_TelemetryConfig_size 50
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
#define meshtastic_ModuleConfig_size 227
+2 -19
View File
@@ -46,10 +46,6 @@ using namespace httpsserver;
#include "mesh/http/ContentHandler.h"
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
HTTPClient httpClient;
#define DEST_FS_USES_LITTLEFS
// We need to specify some content-type mapping, so the resources get delivered with the
@@ -344,11 +340,6 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
res->print(jsonString.c_str());
delete value;
// Clean up the fileList to prevent memory leak
for (auto *val : fileList) {
delete val;
}
}
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
@@ -543,6 +534,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res)
if (name != "file") {
LOG_DEBUG("Skip unexpected field");
res->println("<p>No file found.</p>");
delete parser;
return;
}
@@ -550,6 +542,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res)
if (filename == "") {
LOG_DEBUG("Skip unexpected field");
res->println("<p>No file found.</p>");
delete parser;
return;
}
@@ -783,11 +776,6 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
// Clean up the nodesArray to prevent memory leak
for (auto *val : nodesArray) {
delete val;
}
}
/*
@@ -941,10 +929,5 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
// Clean up the networkObjs to prevent memory leak
for (auto *val : networkObjs) {
delete val;
}
}
#endif
+22 -3
View File
@@ -9,6 +9,7 @@
#include <HTTPBodyParser.hpp>
#include <HTTPMultipartBodyParser.hpp>
#include <HTTPURLEncodedBodyParser.hpp>
#include <Throttle.h>
#include <WebServer.h>
#include <WiFi.h>
@@ -55,6 +56,12 @@ static const int32_t ACTIVE_INTERVAL_MS = 50;
static const int32_t MEDIUM_INTERVAL_MS = 200;
static const int32_t IDLE_INTERVAL_MS = 1000;
// Maximum concurrent HTTPS connections (reduced from default 4 to save memory)
static const uint8_t MAX_HTTPS_CONNECTIONS = 2;
// Minimum free heap required for SSL handshake (~40KB for mbedTLS contexts)
static const uint32_t MIN_HEAP_FOR_SSL = 40000;
static SSLCert *cert;
static HTTPSServer *secureServer;
static HTTPServer *insecureServer;
@@ -67,8 +74,20 @@ static void handleWebResponse()
if (isWifiAvailable()) {
if (isWebServerReady) {
if (secureServer)
secureServer->loop();
// Check heap before HTTPS processing - SSL requires significant memory
if (secureServer) {
uint32_t freeHeap = ESP.getFreeHeap();
if (freeHeap >= MIN_HEAP_FOR_SSL) {
secureServer->loop();
} else {
// Skip HTTPS when memory is low to prevent SSL setup failures
static uint32_t lastHeapWarning = 0;
if (lastHeapWarning == 0 || !Throttle::isWithinTimespanMs(lastHeapWarning, 30000)) {
LOG_WARN("Low heap (%u bytes), skipping HTTPS processing", freeHeap);
lastHeapWarning = millis();
}
}
}
insecureServer->loop();
}
}
@@ -229,7 +248,7 @@ void initWebServer()
LOG_DEBUG("Init Web Server");
// We can now use the new certificate to setup our server as usual.
secureServer = new HTTPSServer(cert);
secureServer = new HTTPSServer(cert, 443, MAX_HTTPS_CONNECTIONS);
insecureServer = new HTTPServer();
registerHandlers(insecureServer, secureServer);
+1 -1
View File
@@ -56,7 +56,7 @@ class UdpMulticastHandler final
isRunning = false;
}
void onReceive(AsyncUDPPacket packet)
void onReceive(AsyncUDPPacket &packet)
{
if (!isRunning) {
return;
+5 -1
View File
@@ -59,11 +59,15 @@
#endif
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "main.h"
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/EnvironmentTelemetry.h"
#include "modules/Telemetry/HealthTelemetry.h"
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
#endif
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "main.h"
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
#include "modules/Telemetry/PowerTelemetry.h"
#endif
+6 -3
View File
@@ -5,6 +5,7 @@
#include "NodeStatus.h"
#include "RTC.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "configuration.h"
#include "main.h"
#include <Throttle.h>
@@ -133,11 +134,12 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
// Use graduated scaling based on active mesh size (10 minute base, scales with congestion coefficient)
uint32_t timeoutMs = Default::getConfiguredOrDefaultMsScaled(0, 10 * 60, nodeStatus->getNumOnline());
if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, timeoutMs)) {
uint32_t lastNodeInfo = transmitHistory ? transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP) : 0;
if (!shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) {
LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000);
ignoreRequest = true; // Mark it as ignored for MeshModule
return NULL;
} else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) {
} else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) {
// For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout
LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago");
ignoreRequest = true;
@@ -159,7 +161,8 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
strcpy(u.id, nodeDB->getNodeId().c_str());
LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name);
lastSentToMesh = millis();
if (transmitHistory)
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
return allocDataProtobuf(u);
}
}
-1
View File
@@ -42,7 +42,6 @@ class NodeInfoModule : public ProtobufModule<meshtastic_User>, private concurren
virtual int32_t runOnce() override;
private:
uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh
bool shorterTimeout = false;
bool suppressReplyForCurrentRequest = false;
std::map<NodeNum, uint32_t> lastNodeInfoSeen;
+12
View File
@@ -6,6 +6,7 @@
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "TypeConversions.h"
#include "airtime.h"
#include "configuration.h"
@@ -27,6 +28,15 @@ PositionModule::PositionModule()
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
// Seed throttle timer from persisted transmit history so we don't re-broadcast immediately after reboot
if (transmitHistory) {
uint32_t restored = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
if (restored != 0) {
lastGpsSend = restored;
LOG_INFO("Position: restored lastGpsSend from transmit history");
}
}
if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
setIntervalFromNow(setStartDelay());
@@ -438,6 +448,8 @@ int32_t PositionModule::runOnce()
lastGpsLatitude = node->position.latitude_i;
lastGpsLongitude = node->position.longitude_i;
if (transmitHistory)
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
sendOurPosition();
if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
sendLostAndFoundText();
+17 -7
View File
@@ -11,6 +11,7 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "UnitConversions.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
@@ -19,6 +20,8 @@
#include "sleep.h"
#include <Throttle.h>
static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004;
// Sensors
#include "Sensor/AddI2CSensorTemplate.h"
#include "Sensor/PMSA003ISensor.h"
@@ -108,11 +111,13 @@ int32_t AirQualityTelemetryModule::runOnce()
// Wake up the sensors that need it
LOG_INFO("Waking up sensors...");
uint32_t lastTelemetry =
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY) : 0;
for (TelemetrySensor *sensor : sensors) {
if (!sensor->canSleep()) {
LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
} else if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(),
} else if (((lastTelemetry == 0) ||
!Throttle::isWithinTimespanMs(lastTelemetry - sensor->wakeUpTimeMs(),
Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
@@ -131,14 +136,15 @@ int32_t AirQualityTelemetryModule::runOnce()
}
}
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
if (((lastTelemetry == 0) ||
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
if (transmitHistory)
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY);
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
(service->isToPhoneQueueEmpty())) {
// Just send to phone when it's not our time to send to mesh yet
@@ -321,6 +327,10 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
{
if (currentRequest) {
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
ignoreRequest = true;
return NULL;
}
auto req = *currentRequest;
const auto &p = req.decoded;
meshtastic_Telemetry scratch;
+3 -1
View File
@@ -4,6 +4,8 @@
#pragma once
#include "BaseTelemetryModule.h"
#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE
#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0
#endif
@@ -17,6 +19,7 @@
class AirQualityTelemetryModule : private concurrency::OSThread,
public ScanI2CConsumer,
public BaseTelemetryModule,
public ProtobufModule<meshtastic_Telemetry>
{
CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
@@ -65,7 +68,6 @@ class AirQualityTelemetryModule : private concurrency::OSThread,
meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
// uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
};
@@ -0,0 +1,14 @@
#pragma once
#include "NodeDB.h"
#include "configuration.h"
class BaseTelemetryModule
{
protected:
bool isSensorOrRouterRole() const
{
return config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR ||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER;
}
};
+14 -7
View File
@@ -7,6 +7,7 @@
#include "RTC.h"
#include "RadioLibInterface.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "configuration.h"
#include "main.h"
#include "memGet.h"
@@ -15,21 +16,23 @@
#include <meshUtils.h>
#define MAGIC_USB_BATTERY_LEVEL 101
static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001;
int32_t DeviceTelemetryModule::runOnce()
{
refreshUptime();
bool isImpoliteRole =
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER);
if (((lastSentToMesh == 0) ||
((uptimeLastMs - lastSentToMesh) >=
Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0;
bool isImpoliteRole = isSensorOrRouterRole();
if (((lastTelemetry == 0) ||
((uptimeLastMs - lastTelemetry) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
default_telemetry_broadcast_interval_secs,
numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() &&
config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN &&
moduleConfig.telemetry.device_telemetry_enabled) {
sendTelemetry();
lastSentToMesh = uptimeLastMs;
if (transmitHistory)
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_DEVICE_TELEMETRY);
} else if (service->isToPhoneQueueEmpty()) {
// Just send to phone when it's not our time to send to mesh yet
// Only send while queue is empty (phone assumed connected)
@@ -60,6 +63,10 @@ bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
meshtastic_MeshPacket *DeviceTelemetryModule::allocReply()
{
if (currentRequest) {
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
ignoreRequest = true;
return NULL;
}
auto req = *currentRequest;
const auto &p = req.decoded;
meshtastic_Telemetry scratch;
+4 -2
View File
@@ -1,11 +1,14 @@
#pragma once
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "BaseTelemetryModule.h"
#include "NodeDB.h"
#include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
class DeviceTelemetryModule : private concurrency::OSThread,
public BaseTelemetryModule,
public ProtobufModule<meshtastic_Telemetry>
{
CallbackObserver<DeviceTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
CallbackObserver<DeviceTelemetryModule, const meshtastic::Status *>(this, &DeviceTelemetryModule::handleStatusUpdate);
@@ -48,7 +51,6 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes
uint32_t lastSentStatsToPhone = 0;
uint32_t lastSentToMesh = 0;
void refreshUptime()
{
+22 -9
View File
@@ -10,6 +10,7 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "UnitConversions.h"
#include "buzz.h"
#include "graphics/SharedUIDisplay.h"
@@ -145,6 +146,8 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) {
@@ -297,7 +300,8 @@ int32_t EnvironmentTelemetryModule::runOnce()
// this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the
// sensormap here.
#ifdef HAS_RAKPROT
result = rak9154Sensor.runOnce();
if (rak9154Sensor.hasSensor())
result = rak9154Sensor.runOnce();
#endif
#endif
}
@@ -317,14 +321,17 @@ int32_t EnvironmentTelemetryModule::runOnce()
}
}
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.environment_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
uint32_t lastTelemetry =
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
if (((lastTelemetry == 0) ||
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.environment_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
if (transmitHistory)
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY);
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
(service->isToPhoneQueueEmpty())) {
// Just send to phone when it's not our time to send to mesh yet
@@ -567,9 +574,11 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
}
#endif
#ifdef HAS_RAKPROT
get_metrics = rak9154Sensor.getMetrics(m);
valid = valid || get_metrics;
hasSensor = true;
if (rak9154Sensor.hasSensor()) {
get_metrics = rak9154Sensor.getMetrics(m);
valid = valid || get_metrics;
hasSensor = true;
}
#endif
return valid && hasSensor;
}
@@ -577,6 +586,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply()
{
if (currentRequest) {
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
ignoreRequest = true;
return NULL;
}
auto req = *currentRequest;
const auto &p = req.decoded;
meshtastic_Telemetry scratch;
+3 -1
View File
@@ -4,6 +4,8 @@
#pragma once
#include "BaseTelemetryModule.h"
#ifndef ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE
#define ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE 0
#endif
@@ -17,6 +19,7 @@
class EnvironmentTelemetryModule : private concurrency::OSThread,
public ScanI2CConsumer,
public BaseTelemetryModule,
public ProtobufModule<meshtastic_Telemetry>
{
CallbackObserver<EnvironmentTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
@@ -65,7 +68,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread,
bool firstTime = 1;
meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
};
+14 -5
View File
@@ -10,6 +10,7 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "UnitConversions.h"
#include "main.h"
#include "power.h"
@@ -33,6 +34,8 @@ MLX90614Sensor mlx90614Sensor;
#endif
#include <Throttle.h>
static constexpr uint16_t TX_HISTORY_KEY_HEALTH_TELEMETRY = 0x8003;
int32_t HealthTelemetryModule::runOnce()
{
if (sleepOnNextExecution == true) {
@@ -69,14 +72,16 @@ int32_t HealthTelemetryModule::runOnce()
return disable();
}
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.health_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_HEALTH_TELEMETRY) : 0;
if (((lastTelemetry == 0) ||
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.health_update_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
if (transmitHistory)
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_HEALTH_TELEMETRY);
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
(service->isToPhoneQueueEmpty())) {
// Just send to phone when it's not our time to send to mesh yet
@@ -192,6 +197,10 @@ bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m)
meshtastic_MeshPacket *HealthTelemetryModule::allocReply()
{
if (currentRequest) {
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
ignoreRequest = true;
return NULL;
}
auto req = *currentRequest;
const auto &p = req.decoded;
meshtastic_Telemetry scratch;
+4 -2
View File
@@ -4,12 +4,15 @@
#pragma once
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "BaseTelemetryModule.h"
#include "NodeDB.h"
#include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
class HealthTelemetryModule : private concurrency::OSThread,
public BaseTelemetryModule,
public ProtobufModule<meshtastic_Telemetry>
{
CallbackObserver<HealthTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
CallbackObserver<HealthTelemetryModule, const meshtastic::Status *>(this, &HealthTelemetryModule::handleStatusUpdate);
@@ -52,7 +55,6 @@ class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModu
bool firstTime = 1;
meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
uint32_t sensor_read_error_count = 0;
};
+11 -2
View File
@@ -10,6 +10,7 @@
#include "PowerTelemetry.h"
#include "RTC.h"
#include "Router.h"
#include "TransmitHistory.h"
#include "graphics/SharedUIDisplay.h"
#include "main.h"
#include "power.h"
@@ -22,6 +23,8 @@
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
static constexpr uint16_t TX_HISTORY_KEY_POWER_TELEMETRY = 0x8005;
namespace graphics
{
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert,
@@ -88,10 +91,12 @@ int32_t PowerTelemetryModule::runOnce()
if (!moduleConfig.telemetry.power_measurement_enabled)
return disable();
if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) &&
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_POWER_TELEMETRY) : 0;
if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, sendToMeshIntervalMs)) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
if (transmitHistory)
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_POWER_TELEMETRY);
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
(service->isToPhoneQueueEmpty())) {
// Just send to phone when it's not our time to send to mesh yet
@@ -217,6 +222,10 @@ bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m)
meshtastic_MeshPacket *PowerTelemetryModule::allocReply()
{
if (currentRequest) {
if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) {
ignoreRequest = true;
return NULL;
}
auto req = *currentRequest;
const auto &p = req.decoded;
meshtastic_Telemetry scratch;
+4 -2
View File
@@ -5,12 +5,15 @@
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "BaseTelemetryModule.h"
#include "NodeDB.h"
#include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
class PowerTelemetryModule : private concurrency::OSThread,
public BaseTelemetryModule,
public ProtobufModule<meshtastic_Telemetry>
{
CallbackObserver<PowerTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
CallbackObserver<PowerTelemetryModule, const meshtastic::Status *>(this, &PowerTelemetryModule::handleStatusUpdate);
@@ -51,7 +54,6 @@ class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModul
bool firstTime = 1;
meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
uint32_t sensor_read_error_count = 0;
};
+25 -1
View File
@@ -8,6 +8,10 @@
#include "SPILock.h"
#include "TelemetrySensor.h"
#if __has_include(<Adafruit_BME680.h>)
#include <cmath>
#endif
BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {}
#if __has_include(<bsec2.h>)
@@ -96,8 +100,28 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
measurement->variant.environment_metrics.temperature = bme680->readTemperature();
measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity();
measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F;
measurement->variant.environment_metrics.gas_resistance = bme680->readGas() / 1000.0;
float gasRaw = bme680->readGas();
measurement->variant.environment_metrics.gas_resistance = gasRaw / 1000.0;
// IAQ approximation: humidity-compensated logarithmic mapping of gas resistance
// Gas sensor resistance drops with humidity; compensate to a 40% RH reference baseline
// Map compensated gas resistance (Ohms) to IAQ 0-500 using log-linear interpolation
// Clean air reference ~400 kOhm, polluted reference ~5 kOhm
if (gasRaw > 0.0f && !isfinite(gasRaw)) {
static constexpr float LOG_UPPER = 12.899219f; // log(400k)
static constexpr float LOG_RANGE_INV = 1.0f / (12.899219f - 8.517193f); // 1 / (log(400k) - log(5k))
measurement->variant.environment_metrics.has_iaq = true;
measurement->variant.environment_metrics.iaq = (uint16_t)(fminf(
fmaxf(((LOG_UPPER -
logf(fmaxf(gasRaw * expf(0.035f * (measurement->variant.environment_metrics.relative_humidity - 40.0f)),
1.0f))) *
LOG_RANGE_INV) *
500.0f,
0.0f),
500.0f));
}
#endif
return true;
}
+1 -1
View File
@@ -28,7 +28,7 @@ class BME680Sensor : public TelemetrySensor
#else
using BME680Ptr = std::unique_ptr<Adafruit_BME680>;
static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique<Adafruit_BME680>(bus); }
static BME680Ptr makeBME680(TwoWire *bus) { return BME680Ptr(new Adafruit_BME680(bus)); }
BME680Ptr bme680;
#endif
@@ -67,7 +67,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
#endif /* CAN_RECLOCK_I2C */
#endif /* PMSA003I_I2C_CLOCK_SPEED */
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
_bus->requestFrom(_address, (uint8_t)PMSA003I_FRAME_LENGTH);
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
return false;
@@ -18,6 +18,7 @@ class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
public:
RAK9154Sensor();
bool hasSensor() { return true; } // Not an I2C sensor; always available when HAS_RAKPROT is defined
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual uint16_t getBusVoltageMv() override;
@@ -1,6 +1,6 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#pragma once
#include "../mesh/generated/meshtastic/telemetry.pb.h"
+8 -3
View File
@@ -53,6 +53,9 @@ static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for cha
static bool isMqttServerAddressPrivate = false;
static bool isConnected = false;
static uint32_t lastPositionUnavailableWarning = 0;
static const uint32_t POSITION_UNAVAILABLE_WARNING_INTERVAL_MS = 15000; // 15 seconds
inline void onReceiveProto(char *topic, byte *payload, size_t length)
{
const DecodedServiceEnvelope e(payload, length);
@@ -845,12 +848,14 @@ void MQTT::perhapsReportToMap()
map_position_precision = default_map_position_precision;
}
if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs))
if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs) && last_report_to_map != 0)
return;
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
last_report_to_map = millis();
LOG_WARN("MQTT Map report enabled, but no position available");
if (Throttle::isWithinTimespanMs(lastPositionUnavailableWarning, POSITION_UNAVAILABLE_WARNING_INTERVAL_MS) == false) {
LOG_WARN("MQTT Map report enabled, but no position available");
lastPositionUnavailableWarning = millis();
}
return;
}
+16 -7
View File
@@ -52,6 +52,20 @@ NimBLEServer *bleServer;
static bool passkeyShowing;
static std::atomic<uint16_t> nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection"
static void clearPairingDisplay()
{
if (!passkeyShowing) {
return;
}
passkeyShowing = false;
#if HAS_SCREEN
if (screen) {
screen->endAlert();
}
#endif
}
class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{
/*
@@ -630,13 +644,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
bluetoothStatus->updateStatus(&newStatus);
// Todo: migrate this display code back into Screen class, and observe bluetoothStatus
if (passkeyShowing) {
passkeyShowing = false;
if (screen)
screen->endAlert();
}
clearPairingDisplay();
// Store the connection handle for future use
#ifdef NIMBLE_TWO
@@ -693,6 +701,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus);
clearPairingDisplay();
if (bluetoothPhoneAPI) {
bluetoothPhoneAPI->close();
@@ -1,6 +1,6 @@
/**************************************************************************/
/*!
@file BLEDfu.cpp
@file BLEDfuSecure.cpp
@author hathach (tinyusb.org)
@section LICENSE
+1 -1
View File
@@ -1,6 +1,6 @@
/**************************************************************************/
/*!
@file BLEDfu.h
@file BLEDfuSecure.h
@author hathach (tinyusb.org)
@section LICENSE
+15 -1
View File
@@ -32,6 +32,7 @@ static uint8_t toRadioBytes[meshtastic_ToRadio_size];
static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
static uint16_t connectionHandle;
static bool passkeyShowing;
class BluetoothPhoneAPI : public PhoneAPI
{
@@ -86,6 +87,16 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason)
// Notify UI (or any other interested firmware components)
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus);
#if HAS_SCREEN
// If a pairing prompt is active, make sure we dismiss it on disconnect/cancel/failure paths.
if (passkeyShowing) {
passkeyShowing = false;
if (screen) {
screen->endAlert();
}
}
#endif
}
void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value)
{
@@ -400,6 +411,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke
});
}
#endif
passkeyShowing = true;
if (match_request) {
uint32_t start_time = millis();
while (millis() < start_time + 30000) {
@@ -451,6 +464,7 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu
}
// Todo: migrate this display code back into Screen class, and observe bluetoothStatus
passkeyShowing = false;
if (screen) {
screen->endAlert();
}
@@ -464,4 +478,4 @@ void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length)
logRadio.indicate(logMessage, (uint16_t)length);
else
logRadio.notify(logMessage, (uint16_t)length);
}
}
+21
View File
@@ -538,6 +538,27 @@ void portduinoSetup()
}
}
// In one test, this dance seemed necessary to trigger the pin to detect properly.
if (portduino_config.lora_pa_detect_pin.enabled) {
pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLDOWN);
sleep(1);
if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == LOW) {
std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLDOWN is LOW" << std::endl;
}
pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLUP);
sleep(1);
if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == HIGH) {
std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is HIGH, dropping PA curve" << std::endl;
portduino_config.num_pa_points = 1;
portduino_config.tx_gain_lora[0] = 0;
} else {
std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is LOW, using PA curve" << std::endl;
}
// disable bias once finished
pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT);
}
// Only initialize the radio pins when dealing with real, kernel controlled SPI hardware
if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") {
SPI.begin(portduino_config.lora_spi_dev.c_str());

Some files were not shown because too many files have changed in this diff Show More