Merge remote-tracking branch 'origin/develop'
This commit is contained in:
@@ -96,7 +96,6 @@ jobs:
|
|||||||
pio_platform: ${{ matrix.check.platform }}
|
pio_platform: ${{ matrix.check.platform }}
|
||||||
pio_env: ${{ matrix.check.board }}
|
pio_env: ${{ matrix.check.board }}
|
||||||
pio_target: check
|
pio_target: check
|
||||||
pio_opts: --fail-on-defect=low
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [setup, version]
|
needs: [setup, version]
|
||||||
|
|||||||
@@ -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
@@ -23,4 +23,4 @@ for BOARD in $BOARDS; do
|
|||||||
CHECK="${CHECK} -e ${BOARD}"
|
CHECK="${CHECK} -e ${BOARD}"
|
||||||
done
|
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
|
DIO3_TCXO_VOLTAGE: true
|
||||||
DIO2_AS_RF_SWITCH: true
|
DIO2_AS_RF_SWITCH: true
|
||||||
spidev: spidev0.0
|
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]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -13,8 +13,7 @@ Lora:
|
|||||||
# USB_Serialnum: 12345678
|
# USB_Serialnum: 12345678
|
||||||
SX126X_MAX_POWER: 22
|
SX126X_MAX_POWER: 22
|
||||||
# Reduce output power to improve EMI
|
# 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.
|
# Note: This module integrates an additional PA to achieve higher output power.
|
||||||
# The 'power' parameter here does not represent the actual RF output.
|
# 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).
|
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ Lora:
|
|||||||
# USB_Serialnum: 12345678
|
# USB_Serialnum: 12345678
|
||||||
SX126X_MAX_POWER: 22
|
SX126X_MAX_POWER: 22
|
||||||
# Reduce output power to improve EMI
|
# 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.
|
# Note: This module integrates an additional PA to achieve higher output power.
|
||||||
# The 'power' parameter here does not represent the actual RF output.
|
# 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).
|
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||||
|
|||||||
Executable
+44
@@ -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[*]}"
|
||||||
+1
-3
@@ -51,9 +51,7 @@ class AudioThread : public concurrency::OSThread
|
|||||||
i2sRtttl = nullptr;
|
i2sRtttl = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rtttlFile != nullptr) {
|
rtttlFile = nullptr;
|
||||||
rtttlFile = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCPUFast(false);
|
setCPUFast(false);
|
||||||
#ifdef T_LORA_PAGER
|
#ifdef T_LORA_PAGER
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ void consoleInit()
|
|||||||
#if defined(SERIAL_HAS_ON_RECEIVE)
|
#if defined(SERIAL_HAS_ON_RECEIVE)
|
||||||
// onReceive does only exist for HardwareSerial not for USB CDC serial
|
// onReceive does only exist for HardwareSerial not for USB CDC serial
|
||||||
Port.onReceive([sc]() { sc->rxInt(); });
|
Port.onReceive([sc]() { sc->rxInt(); });
|
||||||
|
#else
|
||||||
|
(void)sc;
|
||||||
#endif
|
#endif
|
||||||
DEBUG_PORT.rpInit(); // Simply sets up semaphore
|
DEBUG_PORT.rpInit(); // Simply sets up semaphore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1209,8 +1209,8 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
bool somethingChanged = false;
|
bool somethingChanged = false;
|
||||||
|
|
||||||
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
|
// 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);
|
colorTftMesh = __builtin_bswap16(TFT_MESH);
|
||||||
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
|
colorTftBlack = __builtin_bswap16(TFT_BLACK);
|
||||||
|
|
||||||
y = 0;
|
y = 0;
|
||||||
while (y < displayHeight) {
|
while (y < displayHeight) {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshP
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For non-local packets, this applet only reacts to favorited nodes.
|
// 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)
|
if (!sender || !sender->is_favorite)
|
||||||
return ProcessMessage::CONTINUE;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,8 @@ int32_t ButtonThread::runOnce()
|
|||||||
case BUTTON_EVENT_LONG_RELEASED: {
|
case BUTTON_EVENT_LONG_RELEASED: {
|
||||||
|
|
||||||
LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime);
|
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) {
|
(millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) {
|
||||||
evt.inputEvent = _longLongPress;
|
evt.inputEvent = _longLongPress;
|
||||||
this->notifyObservers(&evt);
|
this->notifyObservers(&evt);
|
||||||
|
|||||||
+11
-1
@@ -9,6 +9,7 @@
|
|||||||
#include "PowerMon.h"
|
#include "PowerMon.h"
|
||||||
#include "RadioLibInterface.h"
|
#include "RadioLibInterface.h"
|
||||||
#include "ReliableRouter.h"
|
#include "ReliableRouter.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
#include "power/PowerHAL.h"
|
#include "power/PowerHAL.h"
|
||||||
@@ -246,7 +247,6 @@ uint32_t timeLastPowered = 0;
|
|||||||
static OSThread *powerFSMthread;
|
static OSThread *powerFSMthread;
|
||||||
OSThread *ambientLightingThread;
|
OSThread *ambientLightingThread;
|
||||||
|
|
||||||
RadioInterface *rIf = NULL;
|
|
||||||
RadioLibHal *RadioLibHAL = NULL;
|
RadioLibHal *RadioLibHAL = NULL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -704,6 +704,9 @@ void setup()
|
|||||||
// We do this as early as possible because this loads preferences from flash
|
// 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
|
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
|
||||||
nodeDB = new NodeDB;
|
nodeDB = new NodeDB;
|
||||||
|
|
||||||
|
// Initialize transmit history to persist broadcast throttle timers across reboots
|
||||||
|
TransmitHistory::getInstance()->loadFromDisk();
|
||||||
#if HAS_TFT
|
#if HAS_TFT
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
tftSetup();
|
tftSetup();
|
||||||
@@ -1125,6 +1128,13 @@ void loop()
|
|||||||
lastRadioMissedIrqPoll = millis();
|
lastRadioMissedIrqPoll = millis();
|
||||||
RadioLibInterface::instance->pollMissedIrqs();
|
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
|
#ifdef DEBUG_STACK
|
||||||
|
|||||||
@@ -299,6 +299,38 @@ template <typename T> bool LR11x0Interface<T>::isActivelyReceiving()
|
|||||||
RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
|
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()
|
template <typename T> bool LR11x0Interface<T>::sleep()
|
||||||
{
|
{
|
||||||
// \todo Display actual typename of the adapter, not just `LR11x0`
|
// \todo Display actual typename of the adapter, not just `LR11x0`
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ template <class T> class LR11x0Interface : public RadioLibInterface
|
|||||||
|
|
||||||
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
|
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
|
||||||
|
|
||||||
|
#ifdef LR11X0_AGC_RESET
|
||||||
|
void resetAGC() override;
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/**
|
/**
|
||||||
* Specific module instance
|
* Specific module instance
|
||||||
|
|||||||
+2
-3
@@ -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);
|
digitalWrite(rst, HIGH);
|
||||||
delay(10);
|
delay(10);
|
||||||
|
|
||||||
uint32_t ID = 0;
|
readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst);
|
||||||
ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst);
|
uint32_t ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice
|
||||||
ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice
|
|
||||||
return ID;
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -529,6 +529,11 @@ void RadioLibInterface::pollMissedIrqs()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RadioLibInterface::resetAGC()
|
||||||
|
{
|
||||||
|
// Base implementation: no-op. Override in chip-specific subclasses.
|
||||||
|
}
|
||||||
|
|
||||||
void RadioLibInterface::checkRxDoneIrqFlag()
|
void RadioLibInterface::checkRxDoneIrqFlag()
|
||||||
{
|
{
|
||||||
if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
|
if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
// In addition to the default Rx flags, we need the PREAMBLE_DETECTED flag to detect whether we are actively receiving
|
// 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 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
|
* We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +119,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
|||||||
*/
|
*/
|
||||||
void pollMissedIrqs();
|
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
|
* Debugging counts
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -434,6 +434,61 @@ template <typename T> bool SX126xInterface<T>::sleep()
|
|||||||
return true;
|
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) */
|
/** 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)
|
template <typename T> void SX126xInterface<T>::setTransmitEnable(bool txon)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ template <class T> class SX126xInterface : public RadioLibInterface
|
|||||||
|
|
||||||
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
|
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
|
||||||
|
|
||||||
|
void resetAGC() override;
|
||||||
|
|
||||||
void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; }
|
void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
@@ -56,7 +56,7 @@ class UdpMulticastHandler final
|
|||||||
isRunning = false;
|
isRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onReceive(AsyncUDPPacket packet)
|
void onReceive(AsyncUDPPacket &packet)
|
||||||
{
|
{
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -56,11 +56,15 @@
|
|||||||
#endif
|
#endif
|
||||||
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "modules/Telemetry/AirQualityTelemetry.h"
|
|
||||||
#include "modules/Telemetry/EnvironmentTelemetry.h"
|
#include "modules/Telemetry/EnvironmentTelemetry.h"
|
||||||
#include "modules/Telemetry/HealthTelemetry.h"
|
#include "modules/Telemetry/HealthTelemetry.h"
|
||||||
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
|
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
|
||||||
#endif
|
#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
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
|
||||||
#include "modules/Telemetry/PowerTelemetry.h"
|
#include "modules/Telemetry/PowerTelemetry.h"
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "NodeStatus.h"
|
#include "NodeStatus.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <Throttle.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)
|
// 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());
|
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);
|
LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000);
|
||||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||||
return NULL;
|
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
|
// 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");
|
LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago");
|
||||||
ignoreRequest = true;
|
ignoreRequest = true;
|
||||||
@@ -159,7 +161,8 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
|
|||||||
strcpy(u.id, nodeDB->getNodeId().c_str());
|
strcpy(u.id, nodeDB->getNodeId().c_str());
|
||||||
|
|
||||||
LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name);
|
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);
|
return allocDataProtobuf(u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class NodeInfoModule : public ProtobufModule<meshtastic_User>, private concurren
|
|||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh
|
|
||||||
bool shorterTimeout = false;
|
bool shorterTimeout = false;
|
||||||
bool suppressReplyForCurrentRequest = false;
|
bool suppressReplyForCurrentRequest = false;
|
||||||
std::map<NodeNum, uint32_t> lastNodeInfoSeen;
|
std::map<NodeNum, uint32_t> lastNodeInfoSeen;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "TypeConversions.h"
|
#include "TypeConversions.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "configuration.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
|
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
|
||||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
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 &&
|
if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
|
||||||
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
||||||
setIntervalFromNow(setStartDelay());
|
setIntervalFromNow(setStartDelay());
|
||||||
@@ -438,6 +448,8 @@ int32_t PositionModule::runOnce()
|
|||||||
lastGpsLatitude = node->position.latitude_i;
|
lastGpsLatitude = node->position.latitude_i;
|
||||||
lastGpsLongitude = node->position.longitude_i;
|
lastGpsLongitude = node->position.longitude_i;
|
||||||
|
|
||||||
|
if (transmitHistory)
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||||
sendOurPosition();
|
sendOurPosition();
|
||||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
||||||
sendLostAndFoundText();
|
sendLostAndFoundText();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "UnitConversions.h"
|
#include "UnitConversions.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
|
static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004;
|
||||||
|
|
||||||
// Sensors
|
// Sensors
|
||||||
#include "Sensor/AddI2CSensorTemplate.h"
|
#include "Sensor/AddI2CSensorTemplate.h"
|
||||||
#include "Sensor/PMSA003ISensor.h"
|
#include "Sensor/PMSA003ISensor.h"
|
||||||
@@ -108,11 +111,13 @@ int32_t AirQualityTelemetryModule::runOnce()
|
|||||||
|
|
||||||
// Wake up the sensors that need it
|
// Wake up the sensors that need it
|
||||||
LOG_INFO("Waking up sensors...");
|
LOG_INFO("Waking up sensors...");
|
||||||
|
uint32_t lastTelemetry =
|
||||||
|
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY) : 0;
|
||||||
for (TelemetrySensor *sensor : sensors) {
|
for (TelemetrySensor *sensor : sensors) {
|
||||||
if (!sensor->canSleep()) {
|
if (!sensor->canSleep()) {
|
||||||
LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
|
LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
|
||||||
} else if (((lastSentToMesh == 0) ||
|
} else if (((lastTelemetry == 0) ||
|
||||||
!Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(),
|
!Throttle::isWithinTimespanMs(lastTelemetry - sensor->wakeUpTimeMs(),
|
||||||
Default::getConfiguredOrDefaultMsScaled(
|
Default::getConfiguredOrDefaultMsScaled(
|
||||||
moduleConfig.telemetry.air_quality_interval,
|
moduleConfig.telemetry.air_quality_interval,
|
||||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||||
@@ -131,14 +136,15 @@ int32_t AirQualityTelemetryModule::runOnce()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (((lastSentToMesh == 0) ||
|
if (((lastTelemetry == 0) ||
|
||||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
|
||||||
moduleConfig.telemetry.air_quality_interval,
|
moduleConfig.telemetry.air_quality_interval,
|
||||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||||
airTime->isTxAllowedAirUtil()) {
|
airTime->isTxAllowedAirUtil()) {
|
||||||
sendTelemetry();
|
sendTelemetry();
|
||||||
lastSentToMesh = millis();
|
if (transmitHistory)
|
||||||
|
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY);
|
||||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||||
(service->isToPhoneQueueEmpty())) {
|
(service->isToPhoneQueueEmpty())) {
|
||||||
// Just send to phone when it's not our time to send to mesh yet
|
// Just send to phone when it's not our time to send to mesh yet
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class AirQualityTelemetryModule : private concurrency::OSThread,
|
|||||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||||
// uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute
|
// uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute
|
||||||
uint32_t lastSentToMesh = 0;
|
|
||||||
uint32_t lastSentToPhone = 0;
|
uint32_t lastSentToPhone = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "RadioLibInterface.h"
|
#include "RadioLibInterface.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "memGet.h"
|
#include "memGet.h"
|
||||||
@@ -15,20 +16,23 @@
|
|||||||
#include <meshUtils.h>
|
#include <meshUtils.h>
|
||||||
|
|
||||||
#define MAGIC_USB_BATTERY_LEVEL 101
|
#define MAGIC_USB_BATTERY_LEVEL 101
|
||||||
|
static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001;
|
||||||
|
|
||||||
int32_t DeviceTelemetryModule::runOnce()
|
int32_t DeviceTelemetryModule::runOnce()
|
||||||
{
|
{
|
||||||
refreshUptime();
|
refreshUptime();
|
||||||
|
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0;
|
||||||
bool isImpoliteRole = isSensorOrRouterRole();
|
bool isImpoliteRole = isSensorOrRouterRole();
|
||||||
if (((lastSentToMesh == 0) ||
|
if (((lastTelemetry == 0) ||
|
||||||
((uptimeLastMs - lastSentToMesh) >=
|
((uptimeLastMs - lastTelemetry) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
|
||||||
Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval,
|
default_telemetry_broadcast_interval_secs,
|
||||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
numOnlineNodes))) &&
|
||||||
airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() &&
|
airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() &&
|
||||||
config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN &&
|
config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN &&
|
||||||
moduleConfig.telemetry.device_telemetry_enabled) {
|
moduleConfig.telemetry.device_telemetry_enabled) {
|
||||||
sendTelemetry();
|
sendTelemetry();
|
||||||
lastSentToMesh = uptimeLastMs;
|
if (transmitHistory)
|
||||||
|
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_DEVICE_TELEMETRY);
|
||||||
} else if (service->isToPhoneQueueEmpty()) {
|
} else if (service->isToPhoneQueueEmpty()) {
|
||||||
// Just send to phone when it's not our time to send to mesh yet
|
// Just send to phone when it's not our time to send to mesh yet
|
||||||
// Only send while queue is empty (phone assumed connected)
|
// Only send while queue is empty (phone assumed connected)
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ class DeviceTelemetryModule : private concurrency::OSThread,
|
|||||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
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 sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes
|
||||||
uint32_t lastSentStatsToPhone = 0;
|
uint32_t lastSentStatsToPhone = 0;
|
||||||
uint32_t lastSentToMesh = 0;
|
|
||||||
|
|
||||||
void refreshUptime()
|
void refreshUptime()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "UnitConversions.h"
|
#include "UnitConversions.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
#include "graphics/SharedUIDisplay.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 "graphics/ScreenFonts.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
|
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
|
||||||
|
|
||||||
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||||
{
|
{
|
||||||
if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) {
|
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
|
// 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.
|
// sensormap here.
|
||||||
#ifdef HAS_RAKPROT
|
#ifdef HAS_RAKPROT
|
||||||
result = rak9154Sensor.runOnce();
|
if (rak9154Sensor.hasSensor())
|
||||||
|
result = rak9154Sensor.runOnce();
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -317,14 +321,17 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (((lastSentToMesh == 0) ||
|
uint32_t lastTelemetry =
|
||||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
|
||||||
moduleConfig.telemetry.environment_update_interval,
|
if (((lastTelemetry == 0) ||
|
||||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
!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->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||||
airTime->isTxAllowedAirUtil()) {
|
airTime->isTxAllowedAirUtil()) {
|
||||||
sendTelemetry();
|
sendTelemetry();
|
||||||
lastSentToMesh = millis();
|
if (transmitHistory)
|
||||||
|
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY);
|
||||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||||
(service->isToPhoneQueueEmpty())) {
|
(service->isToPhoneQueueEmpty())) {
|
||||||
// Just send to phone when it's not our time to send to mesh yet
|
// 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
|
#endif
|
||||||
#ifdef HAS_RAKPROT
|
#ifdef HAS_RAKPROT
|
||||||
get_metrics = rak9154Sensor.getMetrics(m);
|
if (rak9154Sensor.hasSensor()) {
|
||||||
valid = valid || get_metrics;
|
get_metrics = rak9154Sensor.getMetrics(m);
|
||||||
hasSensor = true;
|
valid = valid || get_metrics;
|
||||||
|
hasSensor = true;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
return valid && hasSensor;
|
return valid && hasSensor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread,
|
|||||||
bool firstTime = 1;
|
bool firstTime = 1;
|
||||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||||
uint32_t lastSentToMesh = 0;
|
|
||||||
uint32_t lastSentToPhone = 0;
|
uint32_t lastSentToPhone = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "UnitConversions.h"
|
#include "UnitConversions.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
@@ -33,6 +34,8 @@ MLX90614Sensor mlx90614Sensor;
|
|||||||
#endif
|
#endif
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
|
static constexpr uint16_t TX_HISTORY_KEY_HEALTH_TELEMETRY = 0x8003;
|
||||||
|
|
||||||
int32_t HealthTelemetryModule::runOnce()
|
int32_t HealthTelemetryModule::runOnce()
|
||||||
{
|
{
|
||||||
if (sleepOnNextExecution == true) {
|
if (sleepOnNextExecution == true) {
|
||||||
@@ -69,14 +72,16 @@ int32_t HealthTelemetryModule::runOnce()
|
|||||||
return disable();
|
return disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (((lastSentToMesh == 0) ||
|
uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_HEALTH_TELEMETRY) : 0;
|
||||||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
|
if (((lastTelemetry == 0) ||
|
||||||
moduleConfig.telemetry.health_update_interval,
|
!Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled(
|
||||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
moduleConfig.telemetry.health_update_interval,
|
||||||
|
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||||
airTime->isTxAllowedAirUtil()) {
|
airTime->isTxAllowedAirUtil()) {
|
||||||
sendTelemetry();
|
sendTelemetry();
|
||||||
lastSentToMesh = millis();
|
if (transmitHistory)
|
||||||
|
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_HEALTH_TELEMETRY);
|
||||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||||
(service->isToPhoneQueueEmpty())) {
|
(service->isToPhoneQueueEmpty())) {
|
||||||
// Just send to phone when it's not our time to send to mesh yet
|
// Just send to phone when it's not our time to send to mesh yet
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class HealthTelemetryModule : private concurrency::OSThread,
|
|||||||
bool firstTime = 1;
|
bool firstTime = 1;
|
||||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||||
uint32_t lastSentToMesh = 0;
|
|
||||||
uint32_t lastSentToPhone = 0;
|
uint32_t lastSentToPhone = 0;
|
||||||
uint32_t sensor_read_error_count = 0;
|
uint32_t sensor_read_error_count = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "PowerTelemetry.h"
|
#include "PowerTelemetry.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
|
static constexpr uint16_t TX_HISTORY_KEY_POWER_TELEMETRY = 0x8005;
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert,
|
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)
|
if (!moduleConfig.telemetry.power_measurement_enabled)
|
||||||
return disable();
|
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()) {
|
airTime->isTxAllowedAirUtil()) {
|
||||||
sendTelemetry();
|
sendTelemetry();
|
||||||
lastSentToMesh = millis();
|
if (transmitHistory)
|
||||||
|
transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_POWER_TELEMETRY);
|
||||||
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
|
||||||
(service->isToPhoneQueueEmpty())) {
|
(service->isToPhoneQueueEmpty())) {
|
||||||
// Just send to phone when it's not our time to send to mesh yet
|
// Just send to phone when it's not our time to send to mesh yet
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class PowerTelemetryModule : private concurrency::OSThread,
|
|||||||
bool firstTime = 1;
|
bool firstTime = 1;
|
||||||
meshtastic_MeshPacket *lastMeasurementPacket;
|
meshtastic_MeshPacket *lastMeasurementPacket;
|
||||||
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
|
||||||
uint32_t lastSentToMesh = 0;
|
|
||||||
uint32_t lastSentToPhone = 0;
|
uint32_t lastSentToPhone = 0;
|
||||||
uint32_t sensor_read_error_count = 0;
|
uint32_t sensor_read_error_count = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
RAK9154Sensor();
|
RAK9154Sensor();
|
||||||
|
bool hasSensor() { return true; } // Not an I2C sensor; always available when HAS_RAKPROT is defined
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||||
virtual uint16_t getBusVoltageMv() override;
|
virtual uint16_t getBusVoltageMv() override;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**************************************************************************/
|
/**************************************************************************/
|
||||||
/*!
|
/*!
|
||||||
@file BLEDfu.cpp
|
@file BLEDfuSecure.cpp
|
||||||
@author hathach (tinyusb.org)
|
@author hathach (tinyusb.org)
|
||||||
|
|
||||||
@section LICENSE
|
@section LICENSE
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**************************************************************************/
|
/**************************************************************************/
|
||||||
/*!
|
/*!
|
||||||
@file BLEDfu.h
|
@file BLEDfuSecure.h
|
||||||
@author hathach (tinyusb.org)
|
@author hathach (tinyusb.org)
|
||||||
|
|
||||||
@section LICENSE
|
@section LICENSE
|
||||||
|
|||||||
@@ -537,6 +537,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
|
// 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") {
|
if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") {
|
||||||
SPI.begin(portduino_config.lora_spi_dev.c_str());
|
SPI.begin(portduino_config.lora_spi_dev.c_str());
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ extern struct portduino_config_struct {
|
|||||||
pinMapping lora_txen_pin = {"Lora", "TXen"};
|
pinMapping lora_txen_pin = {"Lora", "TXen"};
|
||||||
pinMapping lora_rxen_pin = {"Lora", "RXen"};
|
pinMapping lora_rxen_pin = {"Lora", "RXen"};
|
||||||
pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"};
|
pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"};
|
||||||
|
pinMapping lora_pa_detect_pin = {"Lora", "GPIO_DETECT_PA"};
|
||||||
std::vector<pinMapping> extra_pins = {};
|
std::vector<pinMapping> extra_pins = {};
|
||||||
|
|
||||||
// GPS
|
// GPS
|
||||||
@@ -194,13 +195,14 @@ extern struct portduino_config_struct {
|
|||||||
int maxtophone = 100;
|
int maxtophone = 100;
|
||||||
int MaxNodes = 200;
|
int MaxNodes = 200;
|
||||||
|
|
||||||
pinMapping *all_pins[20] = {&lora_cs_pin,
|
pinMapping *all_pins[21] = {&lora_cs_pin,
|
||||||
&lora_irq_pin,
|
&lora_irq_pin,
|
||||||
&lora_busy_pin,
|
&lora_busy_pin,
|
||||||
&lora_reset_pin,
|
&lora_reset_pin,
|
||||||
&lora_txen_pin,
|
&lora_txen_pin,
|
||||||
&lora_rxen_pin,
|
&lora_rxen_pin,
|
||||||
&lora_sx126x_ant_sw_pin,
|
&lora_sx126x_ant_sw_pin,
|
||||||
|
&lora_pa_detect_pin,
|
||||||
&displayDC,
|
&displayDC,
|
||||||
&displayCS,
|
&displayCS,
|
||||||
&displayBacklight,
|
&displayBacklight,
|
||||||
@@ -255,18 +257,23 @@ extern struct portduino_config_struct {
|
|||||||
out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << tx_gain_lora[0];
|
out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << tx_gain_lora[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch;
|
if (dio2_as_rf_switch)
|
||||||
|
out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch;
|
||||||
if (dio3_tcxo_voltage != 0)
|
if (dio3_tcxo_voltage != 0)
|
||||||
out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000;
|
out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000;
|
||||||
if (lora_usb_pid != 0x5512)
|
if (lora_usb_pid != 0x5512)
|
||||||
out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid;
|
out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid;
|
||||||
if (lora_usb_vid != 0x1A86)
|
if (lora_usb_vid != 0x1A86)
|
||||||
out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid;
|
out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid;
|
||||||
if (lora_spi_dev != "")
|
if (lora_spi_dev != "" && !(lora_spi_dev == "/dev/spidev0.0" && lora_module == use_autoconf)) {
|
||||||
|
if (lora_spi_dev.find("/dev/") != std::string::npos)
|
||||||
|
lora_spi_dev = lora_spi_dev.substr(5);
|
||||||
out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev;
|
out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev;
|
||||||
|
}
|
||||||
if (lora_usb_serial_num != "")
|
if (lora_usb_serial_num != "")
|
||||||
out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num;
|
out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num;
|
||||||
out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed;
|
if (spiSpeed != 2000000)
|
||||||
|
out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed;
|
||||||
if (rfswitch_dio_pins[0] != RADIOLIB_NC) {
|
if (rfswitch_dio_pins[0] != RADIOLIB_NC) {
|
||||||
out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap;
|
out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerMon.h"
|
#include "PowerMon.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
#include "detect/LoRaRadioType.h"
|
#include "detect/LoRaRadioType.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -245,6 +246,10 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
|||||||
nodeDB->saveToDisk();
|
nodeDB->saveToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist broadcast transmit times so throttle survives reboot
|
||||||
|
if (transmitHistory)
|
||||||
|
transmitHistory->saveToDisk();
|
||||||
|
|
||||||
#ifdef PIN_POWER_EN
|
#ifdef PIN_POWER_EN
|
||||||
digitalWrite(PIN_POWER_EN, LOW);
|
digitalWrite(PIN_POWER_EN, LOW);
|
||||||
pinMode(PIN_POWER_EN, INPUT); // power off peripherals
|
pinMode(PIN_POWER_EN, INPUT); // power off peripherals
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#include "TestUtil.h"
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
static void test_placeholder()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
initializeTestEnvironment();
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_placeholder);
|
||||||
|
exit(UNITY_END());
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
|
}
|
||||||
@@ -334,7 +334,7 @@ void setUp(void)
|
|||||||
owner = meshtastic_User{.id = "!12345678"};
|
owner = meshtastic_User{.id = "!12345678"};
|
||||||
myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic
|
myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic
|
||||||
localPosition =
|
localPosition =
|
||||||
meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7};
|
meshtastic_Position{.has_latitude_i = true, .latitude_i = 700000000, .has_longitude_i = true, .longitude_i = 300000000};
|
||||||
|
|
||||||
router = mockRouter = new MockRouter();
|
router = mockRouter = new MockRouter();
|
||||||
service = mockMeshService = new MockMeshService();
|
service = mockMeshService = new MockMeshService();
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
#include "TestUtil.h"
|
||||||
|
#include "TransmitHistory.h"
|
||||||
|
#include <Throttle.h>
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
// Reset the singleton between tests
|
||||||
|
static void resetTransmitHistory()
|
||||||
|
{
|
||||||
|
if (transmitHistory) {
|
||||||
|
delete transmitHistory;
|
||||||
|
transmitHistory = nullptr;
|
||||||
|
}
|
||||||
|
transmitHistory = TransmitHistory::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
resetTransmitHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
static void test_setLastSentToMesh_stores_millis()
|
||||||
|
{
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
|
||||||
|
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
TEST_ASSERT_NOT_EQUAL(0, result);
|
||||||
|
|
||||||
|
// The stored millis value should be very close to current millis()
|
||||||
|
uint32_t diff = millis() - result;
|
||||||
|
TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_set_overwrites_previous_value()
|
||||||
|
{
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
|
||||||
|
testDelay(50);
|
||||||
|
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
|
||||||
|
// The second value should be newer (larger millis)
|
||||||
|
TEST_ASSERT_GREATER_THAN(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Throttle integration ---
|
||||||
|
|
||||||
|
static void test_throttle_blocks_within_interval()
|
||||||
|
{
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
|
||||||
|
// Should be within a 10-minute interval (just set it)
|
||||||
|
bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
|
||||||
|
TEST_ASSERT_TRUE(withinInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_throttle_allows_after_interval()
|
||||||
|
{
|
||||||
|
// Unknown key returns 0 — throttle should NOT block
|
||||||
|
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, lastMs);
|
||||||
|
|
||||||
|
// When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending
|
||||||
|
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
|
||||||
|
TEST_ASSERT_TRUE(shouldSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_throttle_blocks_after_set_then_zero_does_not()
|
||||||
|
{
|
||||||
|
// Set it — now throttle should block
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000);
|
||||||
|
TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval)
|
||||||
|
|
||||||
|
// Different key — should allow
|
||||||
|
uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
||||||
|
bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000);
|
||||||
|
TEST_ASSERT_TRUE(otherShouldSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Multiple keys ---
|
||||||
|
|
||||||
|
static void test_multiple_keys_stored_independently()
|
||||||
|
{
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
testDelay(20);
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||||
|
uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
||||||
|
testDelay(20);
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
|
||||||
|
uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
|
||||||
|
uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
|
||||||
|
|
||||||
|
// All should be non-zero
|
||||||
|
TEST_ASSERT_NOT_EQUAL(0, nodeInfo);
|
||||||
|
TEST_ASSERT_NOT_EQUAL(0, position);
|
||||||
|
TEST_ASSERT_NOT_EQUAL(0, telemetry);
|
||||||
|
|
||||||
|
// Updating other keys should not overwrite earlier key timestamps
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(positionInitial, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Singleton ---
|
||||||
|
|
||||||
|
static void test_getInstance_returns_same_instance()
|
||||||
|
{
|
||||||
|
TransmitHistory *a = TransmitHistory::getInstance();
|
||||||
|
TransmitHistory *b = TransmitHistory::getInstance();
|
||||||
|
TEST_ASSERT_EQUAL_PTR(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_getInstance_creates_global()
|
||||||
|
{
|
||||||
|
if (transmitHistory) {
|
||||||
|
delete transmitHistory;
|
||||||
|
transmitHistory = nullptr;
|
||||||
|
}
|
||||||
|
TEST_ASSERT_NULL(transmitHistory);
|
||||||
|
|
||||||
|
TransmitHistory::getInstance();
|
||||||
|
TEST_ASSERT_NOT_NULL(transmitHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Persistence round-trip (loadFromDisk / saveToDisk) ---
|
||||||
|
|
||||||
|
static void test_save_and_load_round_trip()
|
||||||
|
{
|
||||||
|
// Set some values
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
testDelay(10);
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||||
|
|
||||||
|
uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
|
||||||
|
|
||||||
|
// Force save
|
||||||
|
transmitHistory->saveToDisk();
|
||||||
|
|
||||||
|
// Reset and reload
|
||||||
|
delete transmitHistory;
|
||||||
|
transmitHistory = nullptr;
|
||||||
|
transmitHistory = TransmitHistory::getInstance();
|
||||||
|
transmitHistory->loadFromDisk();
|
||||||
|
|
||||||
|
// Epoch values should be restored (if RTC was available when set)
|
||||||
|
uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition);
|
||||||
|
|
||||||
|
// After loadFromDisk, millis should be seeded (non-zero) for stored entries
|
||||||
|
uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
if (restoredNodeInfo > 0) {
|
||||||
|
// If epoch was stored, millis should be seeded from load
|
||||||
|
TEST_ASSERT_NOT_EQUAL(0, restoredMillis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Boot without RTC scenario ---
|
||||||
|
|
||||||
|
static void test_load_seeds_millis_even_without_rtc()
|
||||||
|
{
|
||||||
|
// This tests the critical crash-reboot scenario:
|
||||||
|
// After loadFromDisk(), even if getTime() returns 0 (no RTC),
|
||||||
|
// lastMillis should be seeded so throttle blocks immediate re-broadcast.
|
||||||
|
|
||||||
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
transmitHistory->saveToDisk();
|
||||||
|
|
||||||
|
// Simulate reboot: destroy and recreate
|
||||||
|
delete transmitHistory;
|
||||||
|
transmitHistory = nullptr;
|
||||||
|
transmitHistory = TransmitHistory::getInstance();
|
||||||
|
transmitHistory->loadFromDisk();
|
||||||
|
|
||||||
|
// The key insight: after load, getLastSentToMeshMillis should return non-zero
|
||||||
|
// because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry.
|
||||||
|
// This ensures throttle works even without RTC.
|
||||||
|
uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
|
||||||
|
uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
|
||||||
|
if (epoch > 0) {
|
||||||
|
// Data was persisted — millis must be seeded
|
||||||
|
TEST_ASSERT_NOT_EQUAL(0, result);
|
||||||
|
|
||||||
|
// And it should cause throttle to block (treating as "just sent")
|
||||||
|
bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000);
|
||||||
|
TEST_ASSERT_TRUE(withinInterval);
|
||||||
|
}
|
||||||
|
// If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore.
|
||||||
|
// This is expected on platforms without RTC during the very first boot.
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
initializeTestEnvironment();
|
||||||
|
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_setLastSentToMesh_stores_millis);
|
||||||
|
RUN_TEST(test_set_overwrites_previous_value);
|
||||||
|
|
||||||
|
RUN_TEST(test_throttle_blocks_within_interval);
|
||||||
|
RUN_TEST(test_throttle_allows_after_interval);
|
||||||
|
RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not);
|
||||||
|
|
||||||
|
RUN_TEST(test_multiple_keys_stored_independently);
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
RUN_TEST(test_getInstance_returns_same_instance);
|
||||||
|
RUN_TEST(test_getInstance_creates_global);
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
RUN_TEST(test_save_and_load_round_trip);
|
||||||
|
RUN_TEST(test_load_seeds_millis_even_without_rtc);
|
||||||
|
|
||||||
|
exit(UNITY_END());
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
@@ -52,12 +52,6 @@ extern "C" {
|
|||||||
|
|
||||||
#define LED_STATE_ON 1 // State when LED is litted
|
#define LED_STATE_ON 1 // State when LED is litted
|
||||||
|
|
||||||
/*
|
|
||||||
* Buttons
|
|
||||||
*/
|
|
||||||
#define PIN_BUTTON1 (9)
|
|
||||||
#define BUTTON_NEED_PULLUP
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Analog pins
|
* Analog pins
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user