Compare commits

..
Author SHA1 Message Date
Jonathan BennettandGitHub 0ff7bc322f Merge branch 'develop' into power-cleanup 2026-04-24 10:50:14 -05:00
Jonathan BennettandGitHub 7adfc3f992 Remove incorrect LED_STATE_ON definition for t-beam-s3 (#10280)
Fixes #9912  and #10170
2026-04-24 15:17:33 +10:00
ba9cadc14d Fix INA226 detection for non-TI compatible chip (Silergy) (#10247)
* Fix INA226 detection for non-TI compatible chip (Silergy)

* Removed extra I2C transaction + 20ms delay on every scan of address 0x40 (including real SHT2x sensors).

Changes suggested by Copilot

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Apply formatting (trunk fmt)

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-24 13:09:37 +10:00
Jonathan Bennett 2815c9abc0 Re-add isVbusIn() override (Thanks CoPilot!) 2026-04-23 21:13:23 -05:00
Jonathan Bennett 761ac4e08a Drop the LongPressIrq detection 2026-04-23 21:12:59 -05:00
Jonathan Bennett f8368f5bd2 Enable PMU IRQ for t-beam-s3, and power button as cancel. 2026-04-23 20:30:25 -05:00
Jonathan BennettandGitHub f60d329574 Merge branch 'develop' into power-cleanup 2026-04-23 19:58:13 -05:00
897c591ffe Update src/power.h marking pmu_irq volatile
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 19:56:22 -05:00
924411de59 T-Watch S3 Power button managment (#9855)
* PMU interrupt pin defined in t-watch s3

* Implement button control on T-Watch S3

Added interrupt handling for the Power/Corona button on T-Watch S3, I use it to control screen state.

* Reducing labels

* Reducing labels

* Updated the comment

* ISR is now IRAM-safe

Updated interrupt management not to cause random crashes.

* Trunk

* Simplify and use INPUT_BROKER_CANCEL

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-04-23 19:53:59 -05:00
d9195944df PositionModule::sendLostAndFoundText: use stack buffer, eliminate heap alloc (#10251)
* PositionModule::sendLostAndFoundText: use stack buffer, eliminate heap alloc

The lost-and-found message was built with an unnecessary heap allocation:

    char *message = new char[60];
    sprintf(message, "..."...);
    ...
    delete[] message;

Two problems:

1. **Buffer too small.** The format string expands with two %f (IEEE 754
   doubles), which `sprintf` prints with full precision — easily 15+
   digits each plus separators — so the actual rendered string can run
   40-50 characters before even considering the emoji (4 UTF-8 bytes)
   and the embedded BEL. A pathological lat/lon can overflow 60 bytes
   and corrupt heap metadata. Unbounded `sprintf` with no size check.

2. **Heap churn in a GPS callback.** This function is called from the
   position-update path which is already heap-sensitive. An infrequent
   60-byte transient alloc isn't catastrophic, but stack is trivially
   available here and removes the failure mode entirely.

Fix: replace with a 128-byte stack buffer and `snprintf` bounded by
`sizeof(message)`. Drop the matching `delete[]` since there's nothing
to delete.

Behavior is identical on the happy path; the overflow case now
truncates safely instead of scribbling over heap.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* PositionModule.cpp: add trailing newline for clang-format

* Address Copilot review: cleaner snprintf size handling

Review feedback from @Copilot on PR #10251: the ternary-plus-static-cast
form mixed signed/unsigned types (int written vs. pb_size_t payload.size
vs. size_t sizeof(message)) and was harder to read than necessary.

Cleaner form:
  const size_t msg_len = std::min(static_cast<size_t>(written), sizeof(message) - 1);
  p->decoded.payload.size = msg_len;

Same behaviour (clamp to buffer-minus-NUL) with one explicit cast and
a size_t variable that names the meaning. Handles the encoding-error
path (written < 0) separately so no bad values leak into payload.size.

* Trunk

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-23 18:20:07 -05:00
83a98c81f6 Hash table index for O(1) packet history lookups (#9499)
* Use hash table for O(1) lookup of recently seen packets

* Eliminate a packet lookup during deduplication

* Infinite loop checks for find and remove

* Consolidate conditional compilation

* Exclude hash table from minimal build

* Additional comment on hash table capacity

* Unit tests for packet history changes

* Update incorrect comment about size clamp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Const

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-04-23 18:04:34 -05:00
Jonathan BennettandGitHub 837637b70c Only enable wakeup via EXT_CHRG_DETECT if we shut down due to low power (#10263) 2026-04-23 15:37:35 -05:00
Jonathan Bennett e5caf2d52f Leftover fix from local merge 2026-04-23 14:59:25 -05:00
Jonathan BennettandGitHub 53413ee502 Merge branch 'develop' into power-cleanup 2026-04-23 14:57:32 -05:00
Jonathan Bennett 04b5f14969 Begin power.cpp/h cleanup 2026-04-23 14:45:20 -05:00
Valentin V. BartenevandGitHub 7b3f58875a Fix example comment in airtime.h (#10275)
Looks like a copy'n'paste typo from the previous line.
It definitely meant to be RX_ALL_LOG according to comment.
2026-04-23 14:44:39 -05:00
Andrew YongandGitHub b2d980fc25 feat(Power): support EXT_PWR_DETECT_MODE & EXT_PWR_DETECT_VALUE, simplify EXT_PWR_DETECT (#10140)
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <me@ndoo.sg>
2026-04-23 14:32:17 -05:00
2ed7bba5e7 fix(Power): refactor EXT_CHRG_DETECT to compile-time macros (#10191)
Mirror the EXT_PWR_DETECT pattern: replace runtime static variables
(ext_chrg_detect_mode, ext_chrg_detect_value) with compile-time macros.
Auto-infer EXT_CHRG_DETECT_VALUE from EXT_CHRG_DETECT_MODE when the mode
is INPUT_PULLUP (→ LOW) or INPUT_PULLDOWN (→ HIGH); default to HIGH.

This fixes inverted polarity on variants that define EXT_CHRG_DETECT_MODE
INPUT_PULLUP without an explicit EXT_CHRG_DETECT_VALUE (e.g. russell):
previously the runtime default of HIGH caused isCharging() to return the
opposite of the correct value. With auto-inference the correct LOW active
level is now derived at compile time.

Remove the now-redundant EXT_CHRG_DETECT_VALUE HIGH from ELECROW-ThinkNode-M4
variant.h since HIGH is the inferred default.


Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>

Signed-off-by: Andrew Yong <noreply@example.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-04-23 13:24:05 -05:00
27 changed files with 1315 additions and 769 deletions
+38 -129
View File
@@ -68,11 +68,7 @@
#define ETH ETH2
#endif // HAS_ETHERNET
#endif
#ifndef DELAY_FOREVER
#define DELAY_FOREVER portMAX_DELAY
#endif
#endif // MQTT
#if defined(BATTERY_PIN) && defined(ARCH_ESP32)
@@ -94,19 +90,6 @@ static const adc_atten_t atten = ADC_ATTENUATION;
#endif
#endif // BATTERY_PIN && ARCH_ESP32
#ifdef EXT_CHRG_DETECT
#ifndef EXT_CHRG_DETECT_MODE
static const uint8_t ext_chrg_detect_mode = INPUT;
#else
static const uint8_t ext_chrg_detect_mode = EXT_CHRG_DETECT_MODE;
#endif
#ifndef EXT_CHRG_DETECT_VALUE
static const uint8_t ext_chrg_detect_value = HIGH;
#else
static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE;
#endif
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#if __has_include(<Adafruit_INA219.h>)
INA219Sensor ina219Sensor;
@@ -145,7 +128,7 @@ MAX17048Sensor max17048Sensor;
NullSensor max17048Sensor;
#endif
#endif
#endif
#endif // !MESHTASTIC_EXCLUDE_I2C
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT
RAK9154Sensor rak9154Sensor;
@@ -163,46 +146,12 @@ XPowersPPM *PPM = NULL;
#ifdef HAS_PMU
XPowersLibInterface *PMU = NULL;
#else
// Copy of the base class defined in axp20x.h.
// I'd rather not include axp20x.h as it brings Wire dependency.
class HasBatteryLevel
{
public:
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() { return -1; }
/**
* The raw voltage of the battery or NAN if unknown
*/
virtual uint16_t getBattVoltage() { return 0; }
/**
* return true if there is a battery installed in this unit
*/
virtual bool isBatteryConnect() { return false; }
virtual bool isVbusIn() { return false; }
virtual bool isCharging() { return false; }
};
#endif
bool pmu_irq = false;
Power *power;
using namespace meshtastic;
// NRF52 has AREF_VOLTAGE defined in architecture.h but
// make sure it's included. If something is wrong with NRF52
// definition - compilation will fail on missing definition
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
#define AREF_VOLTAGE 3.3
#endif
/**
* If this board has a battery level sensor, set this to a valid implementation
*/
@@ -245,7 +194,7 @@ static void battery_adcDisable()
#endif
}
#endif
#endif // BATTERY_PIN
/**
* A simple battery level sensor that assumes the battery voltage is attached
@@ -304,7 +253,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
}
/**
* The raw voltage of the batteryin millivolts or NAN if unknown
* The raw voltage of the battery in millivolts or NAN if unknown
*/
virtual uint16_t getBattVoltage() override
{
@@ -321,16 +270,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
}
#endif
#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER 2.0
#endif
#ifndef BATTERY_SENSE_SAMPLES
#define BATTERY_SENSE_SAMPLES \
15 // Set the number of samples, it has an effect of increasing sensitivity in
// complex electromagnetic environment.
#endif
#ifdef BATTERY_PIN
// Override variant or default ADC_MULTIPLIER if we have the override pref
float operativeAdcMultiplier =
@@ -469,28 +408,14 @@ class AnalogBatteryLevel : public HasBatteryLevel
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
#endif
/// If we see a battery voltage higher than physics allows - assume charger is
/// pumping in power On some boards we don't have the power management chip
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
/// source
// Detect if an external power source is connected if we dont have a PMIC;
// Firstly prefer EXT_PWR_DETECT GPIO if available,
// secondly try an nRF52-specific routine on some variants,
// lastly provide a fallback to indicate external power when fully charged.
virtual bool isVbusIn() override
{
#ifdef EXT_PWR_DETECT
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
// if external powered that pin will be pulled down
if (digitalRead(EXT_PWR_DETECT) == LOW) {
return true;
}
// if it's not LOW - check the battery
#else
// if external powered that pin will be pulled up
if (digitalRead(EXT_PWR_DETECT) == HIGH) {
return true;
}
// if it's not HIGH - check the battery
#endif
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
return false;
return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE;
// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if
@@ -511,9 +436,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
}
#endif
#if defined(ELECROW_ThinkNode_M6)
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || isVbusIn();
#elif EXT_CHRG_DETECT
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
#elif defined(BATTERY_CHARGING_INV)
return !digitalRead(BATTERY_CHARGING_INV);
#else
@@ -646,14 +571,10 @@ Power::Power() : OSThread("Power")
bool Power::analogInit()
{
#ifdef EXT_PWR_DETECT
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
pinMode(EXT_PWR_DETECT, INPUT_PULLUP);
#else
pinMode(EXT_PWR_DETECT, INPUT);
#endif
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
#endif
#ifdef EXT_CHRG_DETECT
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
#endif
#ifdef BATTERY_PIN
@@ -1003,6 +924,14 @@ int32_t Power::runOnce()
powerFSM.trigger(EVENT_POWER_CONNECTED);
}
#ifdef PMU_POWER_BUTTON_IS_CANCEL
// cancel action also turns the screen on and off.
if (PMU->isPekeyShortPressIrq()) {
LOG_INFO("Input: Corona Button Click");
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
}
#endif
/*
Other things we could check if we cared...
@@ -1019,13 +948,6 @@ int32_t Power::runOnce()
LOG_DEBUG("Battery removed");
}
*/
#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3?
if (PMU->isPekeyLongPressIrq()) {
LOG_DEBUG("PEK long button press");
if (screen)
screen->setOn(false);
}
#endif
PMU->clearIrqStatus();
}
@@ -1094,8 +1016,8 @@ void Power::attachPowerInterrupts()
if (PMU) {
attachInterrupt(
PMU_IRQ,
[] {
pmu_irq = true;
[]() {
power->pmu_irq = true;
power->setIntervalFromNow(0);
runASAP = true;
},
@@ -1397,19 +1319,16 @@ bool Power::axpChipInit()
uint64_t pmuIrqMask = 0;
if (PMU->getChipModel() == XPOWERS_AXP192) {
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
}
pinMode(PMU_IRQ, INPUT);
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
// because it occurs repeatedly while there is no battery also it could cause
// inadvertent waking from light sleep just because the battery filled we
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
// don't have anything hooked to vbus
// We wake on IRQ, so only enable the IRQs that we care about.
// we want USB plug and unplug to update the screen and LED status,
// and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off).
PMU->enableIRQ(pmuIrqMask);
PMU->clearIrqStatus();
@@ -1717,7 +1636,7 @@ bool Power::lipoChargerInit()
return true;
}
#else
#else // HAS_PPM
/**
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
*/
@@ -1725,7 +1644,7 @@ bool Power::lipoChargerInit()
{
return false;
}
#endif
#endif // HAS_PPM
#ifdef HELTEC_MESH_SOLAR
#include "meshSolarApp.h"
@@ -1787,7 +1706,7 @@ bool Power::meshSolarInit()
return true;
}
#else
#else // HELTEC_MESH_SOLAR
/**
* The meshSolar battery level sensor is unavailable - default to
* AnalogBatteryLevel
@@ -1796,7 +1715,7 @@ bool Power::meshSolarInit()
{
return false;
}
#endif
#endif // HELTEC_MESH_SOLAR
#ifdef HAS_SERIAL_BATTERY_LEVEL
#include <SoftwareSerial.h>
@@ -1804,7 +1723,7 @@ bool Power::meshSolarInit()
/**
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
*/
class SerialBatteryLevel : public HasBatteryLevel
class SerialBatteryLevel : public AnalogBatteryLevel
{
public:
@@ -1875,22 +1794,12 @@ class SerialBatteryLevel : public HasBatteryLevel
{
#if defined(EXT_CHRG_DETECT)
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
#endif
return false;
}
virtual bool isCharging() override
{
#ifdef EXT_CHRG_DETECT
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#endif
// by default, we check the battery voltage only
return isVbusIn();
}
private:
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
uint8_t Data[6] = {0};
@@ -1906,10 +1815,10 @@ SerialBatteryLevel serialBatteryLevel;
bool Power::serialBatteryInit()
{
#ifdef EXT_PWR_DETECT
pinMode(EXT_PWR_DETECT, INPUT);
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
#endif
#ifdef EXT_CHRG_DETECT
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
#endif
bool result = serialBatteryLevel.runOnce();
@@ -1920,7 +1829,7 @@ bool Power::serialBatteryInit()
return true;
}
#else
#else // HAS_SERIAL_BATTERY_LEVEL
/**
* If this device has no serial battery level sensor, don't try to use it.
*/
@@ -1928,4 +1837,4 @@ bool Power::serialBatteryInit()
{
return false;
}
#endif
#endif // HAS_SERIAL_BATTERY_LEVEL
+2 -2
View File
@@ -19,8 +19,8 @@
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel.
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including
other lora radios.
TX_LOG + RX_ALL_LOG = Total air time for a particular meshtastic channel, including
other lora radios.
RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel.
*/
+1
View File
@@ -499,6 +499,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_EXCLUDE_PKI 1
#define MESHTASTIC_EXCLUDE_POWER_FSM 1
#define MESHTASTIC_EXCLUDE_TZ 1
#define MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH 1
#endif
// Turn off all optional modules
+25 -10
View File
@@ -415,30 +415,45 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#if !defined(M5STACK_UNITC6L)
case INA_ADDR: // Same as SHT2X
case INA_ADDR_ALTERNATE:
case INA_ADDR_WAVESHARE_UPS:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
LOG_DEBUG("Register MFG_UID: 0x%x", registerValue);
if (registerValue == 0x5449) {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
LOG_DEBUG("Register DIE_UID: 0x%x", registerValue);
case INA_ADDR_WAVESHARE_UPS: {
uint16_t mfg = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
if (registerValue == 0x2260) {
LOG_DEBUG("Register MFG_UID: 0x%x", mfg);
// Only read DIE_UID for vendors we recognize as INA-compatible to avoid
// an extra I2C transaction + delay on other devices sharing this address.
if (mfg == 0x5449 || mfg == 0x190F) {
uint16_t die = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
LOG_DEBUG("Register DIE_UID: 0x%x", die);
// TI INA226 or fully compatible clones (e.g. TPA626)
if (mfg == 0x5449 && die == 0x2260) {
logFoundDevice("INA226", (uint8_t)addr.address);
type = INA226;
} else {
}
// Silergy SQ52201 (INA226-compatible with different IDs)
else if (mfg == 0x190F && die == 0x0000) {
logFoundDevice("INA226 (SQ52201)", (uint8_t)addr.address);
type = INA226;
}
// TI INA260
else if (mfg == 0x5449) {
logFoundDevice("INA260", (uint8_t)addr.address);
type = INA260;
}
}
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
} else if (detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) {
if (type == NONE && detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) {
logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address);
type = SHTXX;
}
#endif
} else { // Assume INA219 if none of the above ones are found
else { // Assume INA219 if none of the above ones are found
logFoundDevice("INA219", (uint8_t)addr.address);
type = INA219;
}
break;
}
case INA3221_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue);
-40
View File
@@ -276,42 +276,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
ui->update();
}
void Screen::showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
int maxValueTenths, std::function<void(int)> bannerCallback)
{
#ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif
if (minValueTenths > maxValueTenths) {
int temp = minValueTenths;
minValueTenths = maxValueTenths;
maxValueTenths = temp;
}
if (initialValueTenths < minValueTenths) {
initialValueTenths = minValueTenths;
} else if (initialValueTenths > maxValueTenths) {
initialValueTenths = maxValueTenths;
}
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::curSelected = 0;
NotificationRenderer::current_notification_type = notificationTypeEnum::signed_decimal_picker;
NotificationRenderer::signedDecimalValueTenths = static_cast<int16_t>(initialValueTenths);
NotificationRenderer::signedDecimalMinTenths = static_cast<int16_t>(minValueTenths);
NotificationRenderer::signedDecimalMaxTenths = static_cast<int16_t>(maxValueTenths);
NotificationRenderer::signedDecimalIsNegative = (initialValueTenths < 0);
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
}
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback)
{
@@ -1305,8 +1269,6 @@ void Screen::setFrames(FrameFocus focus)
fsi.positions.focusedModule = numframes;
if (m && m == waypointModule)
fsi.positions.waypoint = numframes;
if (m && strcmp(m->getName(), "EnvironmentTelemetry") == 0)
fsi.positions.environment = numframes;
indicatorIcons.push_back(icon_module);
numframes++;
@@ -2020,8 +1982,6 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::textMessageBaseMenu();
}
}
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.environment) {
menuHandler::environmentTelemetryBaseMenu();
} else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
+1 -4
View File
@@ -12,7 +12,7 @@
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
namespace graphics
{
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, signed_decimal_picker, text_input };
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
struct BannerOverlayOptions {
const char *message;
@@ -312,8 +312,6 @@ class Screen : public concurrency::OSThread
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
void showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
int maxValueTenths, std::function<void(int)> bannerCallback);
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback);
@@ -709,7 +707,6 @@ class Screen : public concurrency::OSThread
uint8_t firstFavorite = 255;
uint8_t lastFavorite = 255;
uint8_t lora = 255;
uint8_t environment = 255;
} positions;
uint8_t frameCount = 0;
+16 -76
View File
@@ -58,32 +58,6 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
return bannerOptions;
}
constexpr float kTemperatureOffsetMinC = -20.0f;
constexpr float kTemperatureOffsetMaxC = 20.0f;
constexpr float kTemperatureOffsetDeltaFPerC = 1.8f;
bool useImperialTemperatureOffsetUnits()
{
return config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL ||
moduleConfig.telemetry.environment_display_fahrenheit;
}
float clampTemperatureOffsetC(float offsetC)
{
if (offsetC < kTemperatureOffsetMinC) {
return kTemperatureOffsetMinC;
}
if (offsetC > kTemperatureOffsetMaxC) {
return kTemperatureOffsetMaxC;
}
return offsetC;
}
int toTenthsRounded(float value)
{
return (value >= 0.0f) ? static_cast<int>(value * 10.0f + 0.5f) : static_cast<int>(value * 10.0f - 0.5f);
}
} // namespace
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
@@ -1035,52 +1009,6 @@ void menuHandler::textMessageMenu()
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
}
void menuHandler::environmentTelemetryBaseMenu()
{
enum optionsNumbers { Back, SetTempOffset };
static const char *optionsArray[] = {"Back", "Set Temp Offset"};
static int optionsEnumArray[] = {Back, SetTempOffset};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Env Actions";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == SetTempOffset) {
menuQueue = EnvironmentTempOffsetPicker;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::environmentTemperatureOffsetPicker()
{
static char pickerTitle[40];
const bool useImperial = useImperialTemperatureOffsetUnits();
const char displayUnit = useImperial ? 'F' : 'C';
const float currentOffsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
const float displayOffset = useImperial ? (currentOffsetC * kTemperatureOffsetDeltaFPerC) : currentOffsetC;
const float minDisplay = useImperial ? (kTemperatureOffsetMinC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMinC;
const float maxDisplay = useImperial ? (kTemperatureOffsetMaxC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMaxC;
snprintf(pickerTitle, sizeof(pickerTitle), "Set Temp Offset (%c)", displayUnit);
screen->showSignedDecimalPicker(pickerTitle, 60000, toTenthsRounded(displayOffset), toTenthsRounded(minDisplay),
toTenthsRounded(maxDisplay), [useImperial](int pickedTenths) -> void {
float selectedOffset = static_cast<float>(pickedTenths) / 10.0f;
if (useImperial) {
selectedOffset /= kTemperatureOffsetDeltaFPerC;
}
moduleConfig.telemetry.environment_temperature_offset_c =
clampTemperatureOffsetC(selectedOffset);
nodeDB->saveToDisk(SEGMENT_MODULECONFIG);
});
}
void menuHandler::textMessageBaseMenu()
{
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
@@ -2290,6 +2218,9 @@ void menuHandler::testMenu()
static int optionsEnumArray[5] = {Back};
int options = 1;
optionsArray[options] = "Number Picker";
optionsEnumArray[options++] = NumberPicker;
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
optionsEnumArray[options++] = ShowChirpy;
#ifdef HAS_I2S
@@ -2303,7 +2234,10 @@ void menuHandler::testMenu()
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == ShowChirpy) {
if (selected == NumberPicker) {
menuQueue = NumberTest;
screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
@@ -2319,6 +2253,12 @@ void menuHandler::testMenu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::numberTest()
{
screen->showNumberPicker("Pick a number\n ", 30000, 4,
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
}
void menuHandler::wifiBaseMenu()
{
enum optionsNumbers { Back, Wifi_toggle };
@@ -2814,8 +2754,8 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case TestMenu:
testMenu();
break;
case EnvironmentTempOffsetPicker:
environmentTemperatureOffsetPicker();
case NumberTest:
numberTest();
break;
case WifiToggleMenu:
wifiToggleMenu();
@@ -2870,4 +2810,4 @@ void menuHandler::saveUIConfig()
} // namespace graphics
#endif
#endif
+2 -3
View File
@@ -38,7 +38,7 @@ class menuHandler
ManageNodeMenu,
RemoveFavorite,
TestMenu,
EnvironmentTempOffsetPicker,
NumberTest,
WifiToggleMenu,
BluetoothToggleMenu,
ScreenOptionsMenu,
@@ -78,7 +78,6 @@ class menuHandler
static void deleteMessagesMenu();
static void homeBaseMenu();
static void textMessageBaseMenu();
static void environmentTelemetryBaseMenu();
static void systemBaseMenu();
static void favoriteBaseMenu();
static void positionBaseMenu();
@@ -102,7 +101,7 @@ class menuHandler
static void removeFavoriteMenu();
static void traceRouteMenu();
static void testMenu();
static void environmentTemperatureOffsetPicker();
static void numberTest();
static void wifiBaseMenu();
static void wifiToggleMenu();
static void screenOptionsMenu();
+61 -396
View File
@@ -15,7 +15,6 @@
#endif
#include "main.h"
#include <algorithm>
#include <limits>
#include <string>
#include <vector>
#if HAS_TRACKBALL
@@ -53,292 +52,16 @@ bool NotificationRenderer::pauseBanner = false;
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
uint32_t NotificationRenderer::numDigits = 0;
uint32_t NotificationRenderer::currentNumber = 0;
int16_t NotificationRenderer::signedDecimalValueTenths = 0;
int16_t NotificationRenderer::signedDecimalMinTenths = -999;
int16_t NotificationRenderer::signedDecimalMaxTenths = 999;
bool NotificationRenderer::signedDecimalIsNegative = false;
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
struct NumericSlotPickerState {
std::vector<uint8_t> digits;
bool hasSign = false;
bool isNegative = false;
uint8_t decimalDigits = 0;
int32_t minValue = 0;
int32_t maxValue = 0;
};
int32_t maxValueForDigits(uint8_t digitCount)
uint32_t pow_of_10(uint32_t n)
{
int64_t value = 0;
for (uint8_t i = 0; i < digitCount; i++) {
value = (value * 10) + 9;
if (value > std::numeric_limits<int32_t>::max()) {
return std::numeric_limits<int32_t>::max();
}
uint32_t ret = 1;
for (uint32_t i = 0; i < n; i++) {
ret *= 10;
}
return static_cast<int32_t>(value);
}
uint8_t pickerSlotCount(const NumericSlotPickerState &state)
{
return static_cast<uint8_t>(state.digits.size() + (state.hasSign ? 1 : 0));
}
int32_t clampPickerValue(int32_t value, int32_t minValue, int32_t maxValue)
{
if (value < minValue) {
return minValue;
}
if (value > maxValue) {
return maxValue;
}
return value;
}
uint32_t combinePickerDigits(const std::vector<uint8_t> &digits)
{
uint32_t value = 0;
for (const uint8_t digit : digits) {
value = (value * 10) + digit;
}
return value;
}
void splitPickerDigits(uint32_t value, std::vector<uint8_t> &digits)
{
if (digits.empty()) {
return;
}
for (int i = static_cast<int>(digits.size()) - 1; i >= 0; i--) {
digits[static_cast<size_t>(i)] = static_cast<uint8_t>(value % 10);
value /= 10;
}
}
int32_t composePickerValue(const NumericSlotPickerState &state)
{
int64_t value = static_cast<int64_t>(combinePickerDigits(state.digits));
if (state.hasSign && state.isNegative && value != 0) {
value = -value;
}
return clampPickerValue(static_cast<int32_t>(value), state.minValue, state.maxValue);
}
void normalizePickerState(NumericSlotPickerState &state)
{
const bool keepNegativeZero = state.isNegative;
const int32_t clampedValue = composePickerValue(state);
const uint32_t absValue =
(clampedValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(clampedValue)) : static_cast<uint32_t>(clampedValue);
splitPickerDigits(absValue, state.digits);
if (!state.hasSign) {
state.isNegative = false;
return;
}
if (clampedValue < 0) {
state.isNegative = true;
} else if (clampedValue > 0) {
state.isNegative = false;
} else {
// Keep selected sign for zero so users can pick +/- before entering digits.
state.isNegative = keepNegativeZero;
}
}
bool isPickerIncrementEvent(uint8_t eventType)
{
return eventType == INPUT_BROKER_UP || eventType == INPUT_BROKER_ALT_PRESS || eventType == INPUT_BROKER_UP_LONG;
}
bool isPickerDecrementEvent(uint8_t eventType)
{
return eventType == INPUT_BROKER_DOWN || eventType == INPUT_BROKER_USER_PRESS || eventType == INPUT_BROKER_DOWN_LONG;
}
void applyPickerDelta(NumericSlotPickerState &state, int8_t selectedSlot, int8_t delta)
{
const uint8_t slotCount = pickerSlotCount(state);
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
return;
}
if (state.hasSign && selectedSlot == 0) {
state.isNegative = !state.isNegative;
normalizePickerState(state);
return;
}
const int8_t digitOffset = state.hasSign ? 1 : 0;
const int8_t digitIndex = selectedSlot - digitOffset;
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
return;
}
uint8_t &digit = state.digits[static_cast<size_t>(digitIndex)];
if (delta > 0) {
digit = static_cast<uint8_t>((digit + 1) % 10);
} else {
digit = static_cast<uint8_t>((digit == 0) ? 9 : (digit - 1));
}
normalizePickerState(state);
}
bool applyPickerKeypress(NumericSlotPickerState &state, int8_t selectedSlot, char key)
{
const uint8_t slotCount = pickerSlotCount(state);
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
return false;
}
if (state.hasSign && selectedSlot == 0 && (key == '+' || key == '-')) {
state.isNegative = (key == '-');
normalizePickerState(state);
return true;
}
if (key < '0' || key > '9') {
return false;
}
const int8_t digitOffset = state.hasSign ? 1 : 0;
const int8_t digitIndex = selectedSlot - digitOffset;
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
return false;
}
state.digits[static_cast<size_t>(digitIndex)] = static_cast<uint8_t>(key - '0');
normalizePickerState(state);
return true;
}
void parseBannerLines(const char *lineStarts[MAX_LINES + 1], uint16_t &lineCount)
{
lineCount = 0;
char *alertEnd = NotificationRenderer::alertBannerMessage +
strnlen(NotificationRenderer::alertBannerMessage, sizeof(NotificationRenderer::alertBannerMessage));
lineStarts[lineCount] = NotificationRenderer::alertBannerMessage;
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
if (lineStarts[lineCount + 1][0] == '\n') {
lineStarts[lineCount + 1] += 1;
}
lineCount++;
}
}
void buildNumericPickerDisplay(const NumericSlotPickerState &state, std::string &formattedValue,
std::vector<int16_t> &slotCharIndexBySlot)
{
const uint8_t slotCount = pickerSlotCount(state);
slotCharIndexBySlot.assign(slotCount, -1);
formattedValue = " ";
formattedValue.reserve(24);
int8_t slot = 0;
if (state.hasSign) {
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
formattedValue += state.isNegative ? '-' : '+';
formattedValue += ' ';
}
const size_t digitCount = state.digits.size();
const size_t decimalBreak =
(state.decimalDigits > 0 && state.decimalDigits < digitCount) ? (digitCount - state.decimalDigits) : digitCount;
for (size_t i = 0; i < digitCount; i++) {
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
formattedValue += static_cast<char>('0' + state.digits[i]);
formattedValue += ' ';
if (state.decimalDigits > 0 && i + 1 == decimalBreak) {
formattedValue += '.';
formattedValue += ' ';
}
}
}
// Number picker renderer
// Reuse by building `formattedValue` as displayed, and mapping each editable slot
// to its character index in that string via `slotCharIndexBySlot`.
void NumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lineStarts[MAX_LINES + 1], uint16_t lineCount,
const std::string &formattedValue, const int16_t *slotCharIndexBySlot, uint8_t slotCount, int8_t selectedSlot)
{
if (formattedValue.empty() || slotCharIndexBySlot == nullptr || slotCount == 0) {
return;
}
std::string spacer(formattedValue.size(), ' ');
uint16_t totalLines = lineCount + 3;
const char *linePointers[totalLines + 1] = {0};
for (uint16_t i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
const uint16_t topGuideLineIndex = lineCount;
linePointers[lineCount++] = spacer.c_str();
const uint16_t valueLineIndex = lineCount;
linePointers[lineCount++] = formattedValue.c_str();
const uint16_t bottomGuideLineIndex = lineCount;
linePointers[lineCount++] = spacer.c_str();
NotificationRenderer::drawNotificationBox(display, state, linePointers, totalLines, 0);
constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2;
uint16_t maxWidth = 0;
uint16_t lineWidths[MAX_LINES + 3] = {0};
for (uint16_t i = 0; i < totalLines; i++) {
const uint16_t lineLength = static_cast<uint16_t>(strlen(linePointers[i]));
lineWidths[i] = display->getStringWidth(linePointers[i], lineLength, true);
if (lineWidths[i] > maxWidth) {
maxWidth = lineWidths[i];
}
}
uint16_t boxWidth = hPadding * 2 + maxWidth;
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1) {
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
}
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines) {
boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2;
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
const int selectedSlotClamped = std::max<int>(0, std::min<int>(selectedSlot, static_cast<int>(slotCount) - 1));
const int selectedCharIndex = slotCharIndexBySlot[selectedSlotClamped];
if (selectedCharIndex < 0 || selectedCharIndex >= static_cast<int>(formattedValue.size())) {
return;
}
int16_t valueTextX = boxLeft + (boxWidth - lineWidths[valueLineIndex]) / 2;
const uint16_t prefixWidth = display->getStringWidth(formattedValue.c_str(), selectedCharIndex, true);
const uint16_t slotCharWidth = display->getStringWidth(formattedValue.c_str() + selectedCharIndex, 1, true);
const int16_t slotCenterX = valueTextX + static_cast<int16_t>(prefixWidth + (slotCharWidth / 2));
int16_t topGuideY = boxTop + vPadding + (topGuideLineIndex * effectiveLineHeight);
int16_t bottomGuideY = boxTop + vPadding + (bottomGuideLineIndex * effectiveLineHeight);
const int16_t triHalfWidth = (currentResolution == ScreenResolution::High) ? 3 : 2;
const int16_t guideInsetY = 1;
const int16_t triHeight = std::max<int16_t>(2, static_cast<int16_t>((effectiveLineHeight - (guideInsetY * 2) - 1) / 2));
const int16_t topBaseY = topGuideY + effectiveLineHeight - guideInsetY - 1;
const int16_t topApexY = topBaseY - triHeight;
const int16_t bottomBaseY = bottomGuideY + guideInsetY;
const int16_t bottomApexY = bottomBaseY + triHeight;
display->setColor(WHITE);
display->fillTriangle(slotCenterX, topApexY, slotCenterX - triHalfWidth, topBaseY, slotCenterX + triHalfWidth, topBaseY);
display->fillTriangle(slotCenterX - triHalfWidth, bottomBaseY, slotCenterX + triHalfWidth, bottomBaseY, slotCenterX,
bottomApexY);
return ret;
}
// Used on boot when a certificate is being created
@@ -380,10 +103,6 @@ void NotificationRenderer::resetBanner()
pauseBanner = false;
numDigits = 0;
currentNumber = 0;
signedDecimalValueTenths = 0;
signedDecimalMinTenths = -999;
signedDecimalMaxTenths = 999;
signedDecimalIsNegative = false;
nodeDB->pause_sort(false);
@@ -434,9 +153,6 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
case notificationTypeEnum::number_picker:
drawNumberPicker(display, state);
break;
case notificationTypeEnum::signed_decimal_picker:
drawSignedDecimalPicker(display, state);
break;
}
}
@@ -444,132 +160,83 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
{
const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0;
parseBannerLines(lineStarts, lineCount);
NumericSlotPickerState pickerState;
pickerState.hasSign = false;
pickerState.decimalDigits = 0;
pickerState.minValue = 0;
pickerState.maxValue = maxValueForDigits(static_cast<uint8_t>(numDigits));
pickerState.digits.assign(numDigits, 0);
splitPickerDigits(currentNumber, pickerState.digits);
normalizePickerState(pickerState);
// Parse lines
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
lineStarts[lineCount] = alertBannerMessage;
const uint8_t slotCount = pickerSlotCount(pickerState);
if (curSelected < 0) {
curSelected = 0;
} else if (curSelected > static_cast<int8_t>(slotCount)) {
curSelected = static_cast<int8_t>(slotCount);
// Find lines
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1;
lineCount++;
}
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
resetBanner();
return;
}
if (isPickerIncrementEvent(inEvent.inputEvent)) {
applyPickerDelta(pickerState, curSelected, 1);
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
applyPickerDelta(pickerState, curSelected, -1);
// modulo to extract
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
// Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
if (this_digit == 9) {
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
} else {
currentNumber += (pow_of_10(numDigits - curSelected - 1));
}
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
if (this_digit == 0) {
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
} else {
currentNumber -= (pow_of_10(numDigits - curSelected - 1));
}
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit
currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1));
currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1));
curSelected++;
}
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
curSelected = std::max<int8_t>(0, curSelected - 1);
curSelected--;
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
alertBannerUntil != 0) {
resetBanner();
return;
}
currentNumber = static_cast<uint32_t>(std::max<int32_t>(0, composePickerValue(pickerState)));
if (curSelected == static_cast<int8_t>(slotCount)) {
if (curSelected == static_cast<int8_t>(numDigits)) {
alertBannerCallback(currentNumber);
resetBanner();
return;
}
inEvent.inputEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0') {
if (alertBannerMessage[0] == '\0')
return;
uint16_t totalLines = lineCount + 2;
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
// copy the linestarts to display to the linePointers holder
for (uint16_t i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
std::string formattedValue;
std::vector<int16_t> slotCharIndexBySlot;
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
}
void NotificationRenderer::drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
{
const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0;
parseBannerLines(lineStarts, lineCount);
NumericSlotPickerState pickerState;
pickerState.hasSign = true;
pickerState.decimalDigits = 1;
pickerState.minValue = signedDecimalMinTenths;
pickerState.maxValue = signedDecimalMaxTenths;
pickerState.digits.assign(3, 0); // XX.X format
const int32_t currentValue = static_cast<int32_t>(signedDecimalValueTenths);
if (currentValue < 0) {
pickerState.isNegative = true;
} else if (currentValue > 0) {
pickerState.isNegative = false;
} else {
pickerState.isNegative = signedDecimalIsNegative;
}
const uint32_t absValue =
(currentValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(currentValue)) : static_cast<uint32_t>(currentValue);
splitPickerDigits(absValue, pickerState.digits);
normalizePickerState(pickerState);
const uint8_t slotCount = pickerSlotCount(pickerState);
if (curSelected < 0) {
curSelected = 0;
} else if (curSelected > static_cast<int8_t>(slotCount)) {
curSelected = static_cast<int8_t>(slotCount);
}
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
resetBanner();
return;
}
if (isPickerIncrementEvent(inEvent.inputEvent)) {
applyPickerDelta(pickerState, curSelected, 1);
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
applyPickerDelta(pickerState, curSelected, -1);
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
curSelected++;
std::string digits = " ";
std::string arrowPointer = " ";
for (uint16_t i = 0; i < numDigits; i++) {
// Modulo minus modulo to return just the current number
digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " ";
if (curSelected == i) {
arrowPointer += "^ ";
} else {
arrowPointer += "_ ";
}
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
curSelected = std::max<int8_t>(0, curSelected - 1);
}
signedDecimalValueTenths = static_cast<int16_t>(composePickerValue(pickerState));
signedDecimalIsNegative = pickerState.isNegative;
if (curSelected == static_cast<int8_t>(slotCount)) {
alertBannerCallback(signedDecimalValueTenths);
resetBanner();
return;
}
linePointers[lineCount++] = digits.c_str();
linePointers[lineCount++] = arrowPointer.c_str();
inEvent.inputEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0') {
return;
}
std::string formattedValue;
std::vector<int16_t> slotCharIndexBySlot;
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
drawNotificationBox(display, state, linePointers, totalLines, 0);
}
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
@@ -955,9 +622,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
strncpy(lineBuffer, lines[i], lineLengths[i]);
lineBuffer[lineLengths[i]] = '\0';
// Determine if this is a pop-up or a pick list
const bool highlightTitleRow =
(i == 0) && (alertBannerOptions > 0 || current_notification_type == notificationTypeEnum::signed_decimal_picker);
if (highlightTitleRow) {
if (alertBannerOptions > 0 && i == 0) {
// Pick List
display->setColor(WHITE);
int background_yOffset = 1;
-6
View File
@@ -5,7 +5,6 @@
#include "graphics/Screen.h"
#include "graphics/VirtualKeyboard.h"
#include "modules/OnScreenKeyboardModule.h"
#include <cstdint>
#include <functional>
#include <string>
#define MAX_LINES 5
@@ -27,10 +26,6 @@ class NotificationRenderer
static std::function<void(int)> alertBannerCallback;
static uint32_t numDigits;
static uint32_t currentNumber;
static int16_t signedDecimalValueTenths;
static int16_t signedDecimalMinTenths;
static int16_t signedDecimalMaxTenths;
static bool signedDecimalIsNegative;
static VirtualKeyboard *virtualKeyboard;
static std::function<void(const std::string &)> textInputCallback;
@@ -41,7 +36,6 @@ class NotificationRenderer
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
-1
View File
@@ -81,7 +81,6 @@ class MeshModule
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response);
const char *getName() const { return name; }
#if HAS_SCREEN
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; }
virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset
+5 -2
View File
@@ -101,9 +101,12 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
if (origTx) {
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
// directly from the destination
bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to);
// Single lookup for both relayer checks on the same (request_id, to) pair
bool wasAlreadyRelayer = false;
bool weWereSoleRelayer = false;
bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer);
bool weWereRelayer = false;
checkRelayers(p->relay_node, ourRelayID, p->decoded.request_id, p->to, &wasAlreadyRelayer, &weWereRelayer,
&weWereSoleRelayer);
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
if (origTx->next_hop != p->relay_node) { // Not already set
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
+169 -13
View File
@@ -1,6 +1,7 @@
#include "PacketHistory.h"
#include "configuration.h"
#include "mesh-pb-constants.h"
#include "meshUtils.h"
#ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
@@ -23,6 +24,14 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
size = PACKETHISTORY_MAX; // Use default size if invalid
}
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Ensure capacity fits in uint16_t hash index (HASH_EMPTY = 0xFFFF is the sentinel)
if (size >= HASH_EMPTY) {
LOG_WARN("Packet History - Clamping size %d to %d (hash index limit)", size, HASH_EMPTY - 1);
size = HASH_EMPTY - 1;
}
#endif
// Allocate memory for the recent packets array
recentPacketsCapacity = size;
recentPackets = new PacketRecord[recentPacketsCapacity];
@@ -35,6 +44,20 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
// Initialize the recent packets array to zero
memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity);
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Allocate hash index with load factor <= 0.5 for short probe chains
hashCapacity = nextPowerOf2(recentPacketsCapacity * 2);
hashMask = hashCapacity - 1;
hashIndex = new uint16_t[hashCapacity];
if (!hashIndex) {
LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity);
hashCapacity = 0;
hashMask = 0;
return;
}
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
#endif
}
PacketHistory::~PacketHistory()
@@ -42,6 +65,12 @@ PacketHistory::~PacketHistory()
recentPacketsCapacity = 0;
delete[] recentPackets;
recentPackets = NULL;
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
delete[] hashIndex;
hashIndex = NULL;
hashCapacity = 0;
hashMask = 0;
#endif
}
/** Update recentPackets and return true if we have already seen this packet */
@@ -194,7 +223,78 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
return seenRecently;
}
/** Find a packet record in history.
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Hash function for (sender, id) pairs. Uses xor-shift mixing for good distribution.
uint32_t PacketHistory::hashSlot(NodeNum sender, PacketId id) const
{
uint32_t h = sender ^ (id * 0x9E3779B9); // Fibonacci hashing constant
h ^= h >> 16;
h *= 0x45d9f3b;
h ^= h >> 16;
return h & hashMask;
}
void PacketHistory::hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx)
{
if (!hashIndex)
return;
uint32_t bucket = hashSlot(sender, id);
// Guard against infinite loop if hash table is corrupted (no HASH_EMPTY slots)
for (uint32_t i = 0; i < hashCapacity; i++) {
if (hashIndex[bucket] == HASH_EMPTY) {
hashIndex[bucket] = slotIdx;
return;
}
bucket = (bucket + 1) & hashMask;
}
LOG_ERROR("Packet History - hashInsert: table full or corrupted, rebuilding");
hashRebuild();
}
void PacketHistory::hashRemove(NodeNum sender, PacketId id)
{
if (!hashIndex)
return;
uint32_t bucket = hashSlot(sender, id);
for (uint32_t i = 0; i < hashCapacity; i++) {
if (hashIndex[bucket] == HASH_EMPTY)
return;
uint16_t idx = hashIndex[bucket];
if (idx < recentPacketsCapacity && recentPackets[idx].sender == sender && recentPackets[idx].id == id) {
// Found it — delete and re-insert subsequent entries to maintain probe chain integrity
hashIndex[bucket] = HASH_EMPTY;
uint32_t next = (bucket + 1) & hashMask;
for (uint32_t j = 0; j < hashCapacity; j++) {
if (hashIndex[next] == HASH_EMPTY)
break;
uint16_t displaced = hashIndex[next];
hashIndex[next] = HASH_EMPTY;
if (displaced < recentPacketsCapacity) {
const auto &rec = recentPackets[displaced];
hashInsert(rec.sender, rec.id, displaced);
}
next = (next + 1) & hashMask;
}
return;
}
bucket = (bucket + 1) & hashMask;
}
}
void PacketHistory::hashRebuild()
{
if (!hashIndex)
return;
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity);
for (uint32_t i = 0; i < recentPacketsCapacity; i++) {
if (recentPackets[i].rxTimeMsec != 0)
hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i);
}
}
#endif
/** Find a packet record in history using the hash index for O(1) average lookup.
* Falls back to linear scan if hash index is unavailable.
* @return pointer to PacketRecord if found, NULL if not found */
PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
{
@@ -205,23 +305,40 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
return NULL;
}
PacketRecord *it = NULL;
for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
if (it->id == id && it->sender == sender) {
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Use hash index for O(1) lookup when available
if (hashIndex) {
uint32_t bucket = hashSlot(sender, id);
for (uint32_t i = 0; i < hashCapacity; i++) {
if (hashIndex[bucket] == HASH_EMPTY)
break;
uint16_t idx = hashIndex[bucket];
if (idx < recentPacketsCapacity && recentPackets[idx].id == id && recentPackets[idx].sender == sender) {
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender,
it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec),
it - recentPackets, recentPacketsCapacity);
LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d",
recentPackets[idx].sender, recentPackets[idx].id, recentPackets[idx].next_hop,
recentPackets[idx].relayed_by[0], recentPackets[idx].relayed_by[1], recentPackets[idx].relayed_by[2],
millis() - (recentPackets[idx].rxTimeMsec), idx, recentPacketsCapacity);
#endif
// only the first match is returned, so be careful not to create duplicate entries
return it; // Return pointer to the found record
return &recentPackets[idx];
}
bucket = (bucket + 1) & hashMask;
}
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id);
#endif
return NULL;
}
#endif
// Linear scan (sole path when hash excluded, fallback when hash allocation failed)
for (PacketRecord *it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
if (it->id == id && it->sender == sender) {
return it;
}
}
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id);
#endif
return NULL; // Not found
return NULL;
}
/** Insert/Replace oldest PacketRecord in recentPackets. */
@@ -327,8 +444,22 @@ void PacketHistory::insert(const PacketRecord &r)
return; // Return early if we can't update the history
}
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Maintain hash index: remove old entry if evicting a different packet, then insert new entry
bool isMatchingSlot = (tu->id == r.id && tu->sender == r.sender);
if (!isMatchingSlot && tu->rxTimeMsec != 0) {
hashRemove(tu->sender, tu->id);
}
*tu = r; // store the packet
if (!isMatchingSlot) {
hashInsert(r.sender, r.id, (uint16_t)(tu - recentPackets));
}
#else
*tu = r; // store the packet
#endif
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER",
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
@@ -396,6 +527,31 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, boo
return found;
}
// Check two relayers against the same packet record with a single find() call,
// avoiding redundant O(N) lookups when both are checked for the same (id, sender) pair.
void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
bool *r2WasSole)
{
*r1Result = false;
*r2Result = false;
if (r2WasSole)
*r2WasSole = false;
if (!initOk()) {
LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED!");
return;
}
const PacketRecord *found = find(sender, id);
if (!found)
return;
if (relayer1 != 0)
*r1Result = wasRelayer(relayer1, *found);
if (relayer2 != 0)
*r2Result = wasRelayer(relayer2, *found, r2WasSole);
}
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
{
+26
View File
@@ -28,6 +28,22 @@ class PacketHistory
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat.
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan.
// Maps (sender, id) -> index into recentPackets[]. Uses linear probing with a load factor <= 0.5.
// The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity),
// and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot).
static constexpr uint16_t HASH_EMPTY = 0xFFFF;
uint16_t *hashIndex = NULL;
uint32_t hashCapacity = 0; // Always a power of 2
uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing
uint32_t hashSlot(NodeNum sender, PacketId id) const;
void hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx);
void hashRemove(NodeNum sender, PacketId id);
void hashRebuild();
#endif
/** Find a packet record in history.
* @param sender NodeNum
* @param id PacketId
@@ -70,6 +86,16 @@ class PacketHistory
* @return true if node was indeed a relayer, false if not */
bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr);
/**
* Check two relayers against the same packet record with a single lookup.
* Avoids redundant find() calls when checking multiple relayers for the same (id, sender) pair.
* @param r1Result set to true if relayer1 was a relayer
* @param r2Result set to true if relayer2 was a relayer
* @param r2WasSole if not nullptr, set to true if relayer2 was the sole relayer
*/
void checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
bool *r2WasSole = nullptr);
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
@@ -398,8 +398,6 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig {
bool device_telemetry_enabled;
/* Enable/Disable the air quality telemetry measurement module on-device display */
bool air_quality_screen_enabled;
/* Temperature offset in Celsius applied to local environment telemetry before it is sent. */
float environment_temperature_offset_c;
} meshtastic_ModuleConfig_TelemetryConfig;
/* Canned Messages Module Config */
@@ -595,7 +593,7 @@ extern "C" {
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0}
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""}
@@ -614,7 +612,7 @@ extern "C" {
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0}
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0}
#define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""}
@@ -720,7 +718,6 @@ extern "C" {
#define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13
#define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14
#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15
#define meshtastic_ModuleConfig_TelemetryConfig_environment_temperature_offset_c_tag 16
#define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3
@@ -951,8 +948,7 @@ X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \
X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \
X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \
X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15) \
X(a, STATIC, SINGULAR, FLOAT, environment_temperature_offset_c, 16)
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15)
#define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL
#define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL
@@ -1055,8 +1051,8 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
#define meshtastic_ModuleConfig_SerialConfig_size 28
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
#define meshtastic_ModuleConfig_TelemetryConfig_size 55
#define meshtastic_ModuleConfig_TAKConfig_size 4
#define meshtastic_ModuleConfig_TelemetryConfig_size 50
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
#define meshtastic_ModuleConfig_size 227
#define meshtastic_RemoteHardwarePin_size 21
+18
View File
@@ -11,6 +11,24 @@ template <class T> constexpr const T &clamp(const T &v, const T &lo, const T &hi
return (v < lo) ? lo : (hi < v) ? hi : v;
}
/// Return the smallest power of 2 >= n (undefined for n > 2^31)
static inline uint32_t nextPowerOf2(uint32_t n)
{
if (n <= 1)
return 1;
#if defined(__GNUC__)
return 1U << (32 - __builtin_clz(n - 1));
#else
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n + 1;
#endif
}
#if HAS_SCREEN
#define IF_SCREEN(X) \
if (screen) { \
+15 -6
View File
@@ -492,15 +492,24 @@ void PositionModule::sendLostAndFoundText()
{
meshtastic_MeshPacket *p = allocDataPacket();
p->to = NODENUM_BROADCAST;
char *message = new char[60];
sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7));
char message[128];
int written = snprintf(message, sizeof(message), "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7),
(lastGpsLongitude * 1e-7));
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
p->want_ack = false;
p->decoded.payload.size = strlen(message);
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
if (written < 0) {
// snprintf encoding error — send an empty payload rather than uninitialized bytes.
p->decoded.payload.size = 0;
} else {
// Clamp to buffer capacity (snprintf returns "would-have-written" which can exceed the buffer).
const size_t msg_len = std::min(static_cast<size_t>(written), sizeof(message) - 1);
p->decoded.payload.size = msg_len;
if (msg_len > 0) {
memcpy(p->decoded.payload.bytes, message, msg_len);
}
}
service->sendToMesh(p, RX_SRC_LOCAL, true);
delete[] message;
}
// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision
@@ -580,4 +589,4 @@ void PositionModule::handleNewPosition()
}
}
#endif
#endif
+4 -62
View File
@@ -138,32 +138,6 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
namespace
{
constexpr float TEMPERATURE_OFFSET_MIN_C = -20.0f;
constexpr float TEMPERATURE_OFFSET_MAX_C = 20.0f;
float clampTemperatureOffsetC(float offsetC)
{
if (offsetC < TEMPERATURE_OFFSET_MIN_C)
return TEMPERATURE_OFFSET_MIN_C;
if (offsetC > TEMPERATURE_OFFSET_MAX_C)
return TEMPERATURE_OFFSET_MAX_C;
return offsetC;
}
void applyTemperatureOffset(meshtastic_EnvironmentMetrics *metrics)
{
const float offsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
if (offsetC == 0.0f)
return;
if (metrics->has_temperature)
metrics->temperature += offsetC;
if (metrics->has_soil_temperature)
metrics->soil_temperature += offsetC;
}
} // namespace
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
@@ -287,29 +261,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
return disable();
}
auto refreshLocalMeasurementPacket = [this]() {
// Keep displaying remote telemetry once we have it.
if (lastMeasurementPacket != nullptr && lastMeasurementPacket->from != nodeDB->getNodeNum()) {
return;
}
meshtastic_Telemetry local = meshtastic_Telemetry_init_zero;
if (!getEnvironmentTelemetry(&local)) {
return;
}
meshtastic_MeshPacket *localPacket = allocDataProtobuf(local);
if (localPacket == nullptr) {
return;
}
if (lastMeasurementPacket != nullptr) {
packetPool.release(lastMeasurementPacket);
}
lastMeasurementPacket = packetPool.allocCopy(*localPacket);
packetPool.release(localPacket);
};
if (firstTime) {
// This is the first time the OSThread library has called this function, so do some setup
firstTime = 0;
@@ -339,7 +290,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = rak9154Sensor.runOnce();
#endif
#endif
refreshLocalMeasurementPacket();
}
// it's possible to have this module enabled, only for displaying values on the screen.
// therefore, we should only enable the sensor loop if measurement is also enabled
@@ -356,7 +306,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = delay;
}
}
refreshLocalMeasurementPacket();
uint32_t lastTelemetry =
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
@@ -617,9 +566,6 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
hasSensor = true;
}
#endif
if (valid)
applyTemperatureOffset(&m->variant.environment_metrics);
return valid && hasSensor;
}
@@ -684,15 +630,11 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
else
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
const bool shouldReplaceDisplayPacket =
(lastMeasurementPacket == nullptr || lastMeasurementPacket->from == nodeDB->getNodeNum());
if (shouldReplaceDisplayPacket) {
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
packetPool.release(lastMeasurementPacket);
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
packetPool.release(lastMeasurementPacket);
lastMeasurementPacket = packetPool.allocCopy(*p);
}
lastMeasurementPacket = packetPool.allocCopy(*p);
if (phoneOnly) {
LOG_INFO("Send packet to phone");
service->sendToPhone(p);
+76
View File
@@ -28,6 +28,54 @@
#define NUM_CELLS 1
#endif
// Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
#ifndef BATTERY_SENSE_SAMPLES
#define BATTERY_SENSE_SAMPLES 15
#endif
#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER 2.0
#endif
#ifdef EXT_PWR_DETECT
#ifndef EXT_PWR_DETECT_MODE
#define EXT_PWR_DETECT_MODE INPUT
// If using internal pull resistors, we can infer EXT_PWR_DETECT_VALUE
#elif EXT_PWR_DETECT_MODE == INPUT_PULLUP
#define EXT_PWR_DETECT_VALUE LOW
#elif EXT_PWR_DETECT_MODE == INPUT_PULLDOWN
#define EXT_PWR_DETECT_VALUE HIGH
#endif
#ifndef EXT_PWR_DETECT_VALUE
#define EXT_PWR_DETECT_VALUE HIGH
#endif
#endif
#ifdef EXT_CHRG_DETECT
#ifndef EXT_CHRG_DETECT_MODE
#define EXT_CHRG_DETECT_MODE INPUT
// If using internal pull resistors, we can infer EXT_CHRG_DETECT_VALUE
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLUP
#define EXT_CHRG_DETECT_VALUE LOW
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLDOWN
#define EXT_CHRG_DETECT_VALUE HIGH
#endif
#ifndef EXT_CHRG_DETECT_VALUE
#define EXT_CHRG_DETECT_VALUE HIGH
#endif
#endif
#ifndef DELAY_FOREVER
#define DELAY_FOREVER portMAX_DELAY
#endif
// NRF52 has AREF_VOLTAGE defined in architecture.h but
// make sure it's included. If something is wrong with NRF52
// definition - compilation will fail on missing definition
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
#define AREF_VOLTAGE 3.3
#endif
#ifdef BAT_MEASURE_ADC_UNIT
extern RTC_NOINIT_ATTR uint64_t RTC_reg_b;
#include "soc/sens_reg.h" // needed for adc pin reset
@@ -86,6 +134,31 @@ extern RAK9154Sensor rak9154Sensor;
extern XPowersLibInterface *PMU;
#endif
#ifndef HAS_PMU
// Copy of the base class defined in axp20x.h, to prevent an automatic wire.h dependency
class HasBatteryLevel
{
public:
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() { return -1; }
/**
* The raw voltage of the battery or NAN if unknown
*/
virtual uint16_t getBattVoltage() { return 0; }
/**
* return true if there is a battery installed in this unit
*/
virtual bool isBatteryConnect() { return false; }
virtual bool isVbusIn() { return false; }
virtual bool isCharging() { return false; }
};
#endif
class Power : public concurrency::OSThread
{
@@ -100,6 +173,7 @@ class Power : public concurrency::OSThread
virtual int32_t runOnce() override;
void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; }
const uint16_t OCV[11] = {OCV_ARRAY};
bool isLowBattery() { return low_voltage_counter >= 10; };
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused);
@@ -109,6 +183,8 @@ class Power : public concurrency::OSThread
void attachPowerInterrupts();
void detachPowerInterrupts();
volatile bool pmu_irq = false;
protected:
meshtastic::PowerStatus *statusHandler;
+834
View File
@@ -0,0 +1,834 @@
/*
* Unit tests for PacketHistory — the packet deduplication engine
* used by the mesh routing stack.
*
* PacketHistory maintains a fixed-size array of PacketRecords with an
* optional hash table for O(1) lookup. It tracks which nodes relayed
* each packet, supports LRU-style eviction, and detects fallback-to-
* flooding and hop-limit upgrades.
*/
#include "PacketHistory.h"
#include "TestUtil.h"
#include <unity.h>
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
static constexpr uint32_t OUR_NODE_NUM = 0xDEAD1234;
static constexpr uint8_t OUR_RELAY_ID = 0x34; // getLastByteOfNodeNum(OUR_NODE_NUM)
static constexpr uint32_t SMALL_CAPACITY = 8;
// ---------------------------------------------------------------------------
// Per-test state
// ---------------------------------------------------------------------------
static PacketHistory *ph = nullptr;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static meshtastic_MeshPacket makePacket(uint32_t from, uint32_t id, uint8_t hop_limit = 3,
uint8_t next_hop = NO_NEXT_HOP_PREFERENCE, uint8_t relay_node = 0)
{
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
p.from = from;
p.id = id;
p.hop_limit = hop_limit;
p.next_hop = next_hop;
p.relay_node = relay_node;
return p;
}
// ---------------------------------------------------------------------------
// setUp / tearDown — called before and after every test
// ---------------------------------------------------------------------------
void setUp(void)
{
myNodeInfo.my_node_num = OUR_NODE_NUM;
ph = new PacketHistory(SMALL_CAPACITY);
}
void tearDown(void)
{
delete ph;
ph = nullptr;
}
// ===========================================================================
// Group 1 — Initialization
// ===========================================================================
void test_init_valid_size(void)
{
PacketHistory h(8);
TEST_ASSERT_TRUE(h.initOk());
}
void test_init_minimum_size(void)
{
PacketHistory h(4);
TEST_ASSERT_TRUE(h.initOk());
}
void test_init_too_small_falls_back(void)
{
// Sizes < 4 or > PACKETHISTORY_MAX are clamped to PACKETHISTORY_MAX inside the constructor
PacketHistory h(2);
TEST_ASSERT_TRUE(h.initOk());
}
// ===========================================================================
// Group 2 — Basic Deduplication
// ===========================================================================
void test_first_packet_not_seen(void)
{
auto p = makePacket(0x1111, 100);
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p));
}
void test_same_packet_seen_twice(void)
{
auto p = makePacket(0x1111, 100);
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // first time
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p)); // duplicate
}
void test_different_id_not_confused(void)
{
auto p1 = makePacket(0x1111, 100);
auto p2 = makePacket(0x1111, 200);
ph->wasSeenRecently(&p1);
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2));
}
void test_different_sender_not_confused(void)
{
auto p1 = makePacket(0x1111, 100);
auto p2 = makePacket(0x2222, 100);
ph->wasSeenRecently(&p1);
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2));
}
void test_withUpdate_false_no_insert(void)
{
auto p = makePacket(0x1111, 100);
// First call with withUpdate=false: should not store
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/false));
// Second call with withUpdate=true: still not found because first didn't store
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true));
}
void test_withUpdate_true_inserts(void)
{
auto p = makePacket(0x1111, 100);
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true));
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, /*withUpdate=*/false)); // found without inserting again
}
// ===========================================================================
// Group 3 — LRU Eviction
// ===========================================================================
void test_fill_capacity_all_found(void)
{
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
auto p = makePacket(0xAAAA, i);
ph->wasSeenRecently(&p);
}
// All 8 should be found
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
auto p = makePacket(0xAAAA, i);
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
}
}
void test_eviction_oldest_replaced(void)
{
// Fill all 8 slots
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
auto p = makePacket(0xAAAA, i);
ph->wasSeenRecently(&p);
}
// Advance time so the eviction logic can distinguish "oldest" from "newest".
// insert() uses (now_millis - rxTimeMsec) > OldtrxTimeMsec with strict >, so
// entries with identical timestamps all have age 0 and none gets selected.
delay(1);
// Insert a 9th packet — should evict the oldest
auto p9 = makePacket(0xAAAA, 9);
ph->wasSeenRecently(&p9);
// The 9th should be found
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p9, false));
// At least one of the originals should have been evicted
int evicted = 0;
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
auto p = makePacket(0xAAAA, i);
if (!ph->wasSeenRecently(&p, false))
evicted++;
}
TEST_ASSERT_TRUE(evicted > 0);
}
void test_matching_slot_reused(void)
{
// Insert packet, then re-insert same (sender, id) — should reuse slot, not evict others
auto p1 = makePacket(0xAAAA, 1);
auto p2 = makePacket(0xBBBB, 2);
ph->wasSeenRecently(&p1);
ph->wasSeenRecently(&p2);
// Re-observe p1 (triggers merge path)
ph->wasSeenRecently(&p1);
// Both should still be present
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p1, false));
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
}
void test_free_slot_preferred(void)
{
// Insert 4 packets into capacity-8 history — next insert should use a free slot, not evict
for (uint32_t i = 1; i <= 4; i++) {
auto p = makePacket(0xAAAA, i);
ph->wasSeenRecently(&p);
}
auto p5 = makePacket(0xAAAA, 5);
ph->wasSeenRecently(&p5);
// All 5 should be present (no eviction needed)
for (uint32_t i = 1; i <= 5; i++) {
auto p = makePacket(0xAAAA, i);
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
}
}
void test_evict_all_old_packets(void)
{
// Fill with packets 1..8
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
auto p = makePacket(0xAAAA, i);
ph->wasSeenRecently(&p);
}
// Advance time so the replacement batch can evict the originals
delay(1);
// Replace all with packets 101..108
for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) {
auto p = makePacket(0xBBBB, i);
ph->wasSeenRecently(&p);
}
// None of the originals should be found
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
auto p = makePacket(0xAAAA, i);
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, false));
}
// All new ones should be found
for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) {
auto p = makePacket(0xBBBB, i);
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
}
}
// ===========================================================================
// Group 4 — Relayer Tracking
// ===========================================================================
void test_wasRelayer_true(void)
{
// Non-us relay_nodes only enter relayed_by[] through the "heard-back" merge path:
// we must have relayed first, then observe the packet return at hop_limit-1.
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Heard-back from 0xCC at hop_limit=2 (ourTxHopLimit-1) triggers the merge
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC);
ph->wasSeenRecently(&p2);
TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111));
}
void test_wasRelayer_false(void)
{
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA);
ph->wasSeenRecently(&p);
// 0xCC was never a relayer
TEST_ASSERT_FALSE(ph->wasRelayer(0xCC, 100, 0x1111));
}
void test_wasRelayer_zero_returns_false(void)
{
auto p = makePacket(0x1111, 100);
ph->wasSeenRecently(&p);
TEST_ASSERT_FALSE(ph->wasRelayer(0, 100, 0x1111));
}
void test_wasRelayer_not_found(void)
{
// Packet not in history at all
TEST_ASSERT_FALSE(ph->wasRelayer(0xAA, 999, 0x9999));
}
void test_wasRelayer_wasSole_true(void)
{
// relay_node = ourRelayID → relayed_by[0] = ourRelayID
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p);
bool wasSole = false;
bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole);
TEST_ASSERT_TRUE(result);
TEST_ASSERT_TRUE(wasSole);
}
void test_wasRelayer_wasSole_false(void)
{
// First observation: we relay
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Second observation: different relayer adds to record
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
ph->wasSeenRecently(&p2);
bool wasSole = true;
bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole);
TEST_ASSERT_TRUE(result);
TEST_ASSERT_FALSE(wasSole);
}
void test_wasRelayer_all_six_slots(void)
{
// First observation: we relay with hop_limit=3 (fills slot 0, ourTxHopLimit=3)
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p);
// Each heard-back must satisfy: hop_limit == ourTxHopLimit OR ourTxHopLimit-1.
// Using hop_limit=2 (ourTxHopLimit-1) for all, which triggers the heard-back
// merge path each time. Each new relay_node pushes to slot 0 and shifts existing
// relayers right, eventually filling all NUM_RELAYERS(6) slots.
uint8_t relayers[] = {0x11, 0x22, 0x33, 0x44, 0x55};
for (int i = 0; i < 5; i++) {
auto pn = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, relayers[i]);
ph->wasSeenRecently(&pn);
}
// All 6 should be detected
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
for (int i = 0; i < 5; i++) {
TEST_ASSERT_TRUE(ph->wasRelayer(relayers[i], 100, 0x1111));
}
}
// ===========================================================================
// Group 5 — removeRelayer
// ===========================================================================
void test_removeRelayer_removes(void)
{
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111);
TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
}
void test_removeRelayer_compacts(void)
{
// We relay first
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Second relayer
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
ph->wasSeenRecently(&p2);
// Remove us, 0xBB should still be found
ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111);
TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111));
}
void test_removeRelayer_nonexistent_safe(void)
{
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p);
// Removing a relayer that doesn't exist should not crash
ph->removeRelayer(0xFF, 100, 0x1111);
// Original should still be there
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
}
void test_removeRelayer_packet_not_found_safe(void)
{
// Packet not in history — should not crash
ph->removeRelayer(0xAA, 999, 0x9999);
}
// ===========================================================================
// Group 6 — checkRelayers
// ===========================================================================
void test_checkRelayers_both_found(void)
{
// We relay first
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Second relayer
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
ph->wasSeenRecently(&p2);
bool r1 = false, r2 = false;
ph->checkRelayers(OUR_RELAY_ID, 0xBB, 100, 0x1111, &r1, &r2);
TEST_ASSERT_TRUE(r1);
TEST_ASSERT_TRUE(r2);
}
void test_checkRelayers_one_found(void)
{
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p);
bool r1 = false, r2 = false;
ph->checkRelayers(OUR_RELAY_ID, 0xCC, 100, 0x1111, &r1, &r2);
TEST_ASSERT_TRUE(r1);
TEST_ASSERT_FALSE(r2);
}
void test_checkRelayers_r2WasSole(void)
{
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p);
bool r1 = false, r2 = false, r2Sole = false;
// relayer1=0xCC (not found), relayer2=OUR_RELAY_ID (sole relayer)
ph->checkRelayers(0xCC, OUR_RELAY_ID, 100, 0x1111, &r1, &r2, &r2Sole);
TEST_ASSERT_FALSE(r1);
TEST_ASSERT_TRUE(r2);
TEST_ASSERT_TRUE(r2Sole);
}
// ===========================================================================
// Group 7 — wasSeenRecently Merge Logic
// ===========================================================================
void test_merge_preserves_original_next_hop(void)
{
// First observation with next_hop=0x55
auto p1 = makePacket(0x1111, 100, 3, 0x55, 0xAA);
ph->wasSeenRecently(&p1);
// Re-observation with different next_hop
auto p2 = makePacket(0x1111, 100, 2, 0x77, 0xBB);
ph->wasSeenRecently(&p2);
// The stored next_hop should still be 0x55 (the original)
// We verify via weWereNextHop: if we set original next_hop to ourRelayID, it should detect it
auto p3 = makePacket(0x1111, 200, 3, OUR_RELAY_ID, 0xAA);
ph->wasSeenRecently(&p3);
auto p4 = makePacket(0x1111, 200, 2, 0x99, 0xBB);
bool weWereNextHop = false;
ph->wasSeenRecently(&p4, true, nullptr, &weWereNextHop);
TEST_ASSERT_TRUE(weWereNextHop);
}
void test_merge_preserves_highest_hop_limit(void)
{
// First observation with hop_limit=5
auto p1 = makePacket(0x1111, 100, 5);
ph->wasSeenRecently(&p1);
// Re-observation with hop_limit=2 (lower)
auto p2 = makePacket(0x1111, 100, 2);
ph->wasSeenRecently(&p2);
// Third observation with hop_limit=3 should not trigger upgrade (highest was 5)
bool wasUpgraded = true;
auto p3 = makePacket(0x1111, 100, 3);
ph->wasSeenRecently(&p3, true, nullptr, nullptr, &wasUpgraded);
TEST_ASSERT_FALSE(wasUpgraded);
}
void test_merge_no_duplicate_relayers(void)
{
// Observe with relayer 0xAA (stored via relay_node, but only slot 0 for ourRelayID)
// We need to use ourRelayID for the first observation to get it into slot 0
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Re-observe with same relay_node=ourRelayID — should not create duplicates
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p2);
// ourRelayID should appear exactly once — wasSole should still be true
bool wasSole = false;
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole));
TEST_ASSERT_TRUE(wasSole);
}
void test_merge_adds_new_relayer(void)
{
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
ph->wasSeenRecently(&p2);
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111));
}
void test_merge_we_relay_sets_slot_zero(void)
{
// When relay_node == ourRelayID, relayed_by[0] should be set to ourRelayID
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p);
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
}
void test_merge_heard_back_stores_relay_node(void)
{
// First: we relay (hop_limit=3)
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Second: we hear the packet back with hop_limit=2 (one less), from relay_node=0xCC
// This triggers the "heard back" logic: weWereRelayer && hop_limit == ourTxHopLimit-1
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC);
ph->wasSeenRecently(&p2);
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111));
}
// ===========================================================================
// Group 8 — Fallback-to-Flooding Detection
// ===========================================================================
void test_fallback_detected(void)
{
// The fallback condition requires wasRelayer(relay_node) && !wasRelayer(ourRelayID).
// Non-us relayers only enter relayed_by[] via the heard-back merge path, which
// also stores ourRelayID. So we must removeRelayer(ourRelayID) to satisfy both.
//
// Scenario: we relay a directed packet, hear it back from 0xAA, then the router
// removes us from the relayer list. Later the sender falls back to flooding.
// Step 1: We relay (directed to next_hop=0x55)
auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Step 2: Heard-back from 0xAA at hop_limit-1 → stores 0xAA in relayed_by
auto p2 = makePacket(0x1111, 100, 2, 0x55, 0xAA);
ph->wasSeenRecently(&p2);
// Step 3: Router removes us from the relayer list
ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111);
// Step 4: Sender falls back to flooding — same packet, NO_NEXT_HOP_PREFERENCE, from 0xAA
auto p3 = makePacket(0x1111, 100, 1, NO_NEXT_HOP_PREFERENCE, 0xAA);
bool wasFallback = false;
ph->wasSeenRecently(&p3, true, &wasFallback);
TEST_ASSERT_TRUE(wasFallback);
}
void test_fallback_not_when_we_relayed(void)
{
// First observation: directed, we relayed it
auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID);
ph->wasSeenRecently(&p1);
// Second observation: fallback to flooding from same relayer (us)
// But since we already relayed, wasFallback should be false
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
bool wasFallback = false;
ph->wasSeenRecently(&p2, true, &wasFallback);
TEST_ASSERT_FALSE(wasFallback);
}
void test_fallback_not_on_first_observation(void)
{
// First time seen — can't be a fallback
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA);
bool wasFallback = false;
ph->wasSeenRecently(&p, true, &wasFallback);
TEST_ASSERT_FALSE(wasFallback);
}
// ===========================================================================
// Group 9 — Next-Hop and Upgrade Detection
// ===========================================================================
void test_weWereNextHop_true(void)
{
// Packet directed to us (next_hop = ourRelayID)
auto p1 = makePacket(0x1111, 100, 3, OUR_RELAY_ID, 0xAA);
ph->wasSeenRecently(&p1);
// Re-observe: check if we were the original next_hop
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
bool weWereNextHop = false;
ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop);
TEST_ASSERT_TRUE(weWereNextHop);
}
void test_weWereNextHop_false(void)
{
// Packet directed to someone else
auto p1 = makePacket(0x1111, 100, 3, 0x99, 0xAA);
ph->wasSeenRecently(&p1);
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
bool weWereNextHop = false;
ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop);
TEST_ASSERT_FALSE(weWereNextHop);
}
void test_wasUpgraded_true(void)
{
// First observation with hop_limit=3 → stored as highestHopLimit bits 0-2 = 3
auto p1 = makePacket(0x1111, 100, 3);
ph->wasSeenRecently(&p1);
// Re-observation with hop_limit=5
// The upgrade check on line 122 compares the raw packed byte found->hop_limit against p->hop_limit.
// found->hop_limit has highestHopLimit=3 in bits 0-2 (and possibly ourTxHopLimit in bits 3-5).
// So the packed byte value is 3 (or more if ourTxHopLimit was set), and p->hop_limit is 5.
// Since 3 < 5 (with no ourTxHopLimit set), this should detect an upgrade.
auto p2 = makePacket(0x1111, 100, 5);
bool wasUpgraded = false;
ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded);
TEST_ASSERT_TRUE(wasUpgraded);
}
void test_wasUpgraded_false(void)
{
auto p1 = makePacket(0x1111, 100, 5);
ph->wasSeenRecently(&p1);
// Same or lower hop_limit
auto p2 = makePacket(0x1111, 100, 3);
bool wasUpgraded = false;
ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded);
TEST_ASSERT_FALSE(wasUpgraded);
}
// ===========================================================================
// Group 10 — Edge Cases
// ===========================================================================
void test_packet_id_zero_not_stored(void)
{
auto p = makePacket(0x1111, 0);
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p));
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // still not found
}
void test_sender_zero_substituted(void)
{
// from=0 means "from us" — getFrom() substitutes nodeDB->getNodeNum()
auto p = makePacket(0, 100);
ph->wasSeenRecently(&p);
// Should be stored under our node num, not 0
auto p2 = makePacket(OUR_NODE_NUM, 100);
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
}
void test_uninitialized_wasSeenRecently(void)
{
// Simulate uninitialized state — create a PacketHistory that looks uninitialized
// We can't easily make allocation fail, but we can test the initOk guard with a destructed one
PacketHistory h(4);
TEST_ASSERT_TRUE(h.initOk()); // sanity check
h.~PacketHistory();
auto p = makePacket(0x1111, 100);
TEST_ASSERT_FALSE(h.wasSeenRecently(&p));
// Reconstruct in place to allow proper destruction
new (&h) PacketHistory(4);
}
void test_uninitialized_wasRelayer(void)
{
PacketHistory h(4);
h.~PacketHistory();
TEST_ASSERT_FALSE(h.wasRelayer(0xAA, 100, 0x1111));
new (&h) PacketHistory(4);
}
void test_multiple_instances_independent(void)
{
PacketHistory h2(SMALL_CAPACITY);
auto p = makePacket(0x1111, 100);
ph->wasSeenRecently(&p);
// h2 should NOT find it
TEST_ASSERT_FALSE(h2.wasSeenRecently(&p, false));
// ph should still find it
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
}
// ===========================================================================
// Group 11 — Hash Table Stress
// ===========================================================================
void test_many_packets_no_false_negatives(void)
{
PacketHistory big(64);
for (uint32_t i = 1; i <= 64; i++) {
auto p = makePacket(0xAAAA, i);
big.wasSeenRecently(&p);
}
for (uint32_t i = 1; i <= 64; i++) {
auto p = makePacket(0xAAAA, i);
TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "False negative in hash table");
}
}
void test_many_packets_no_false_positives(void)
{
PacketHistory big(64);
for (uint32_t i = 1; i <= 64; i++) {
auto p = makePacket(0xAAAA, i);
big.wasSeenRecently(&p);
}
// IDs 65..128 were never inserted
for (uint32_t i = 65; i <= 128; i++) {
auto p = makePacket(0xAAAA, i);
TEST_ASSERT_FALSE_MESSAGE(big.wasSeenRecently(&p, false), "False positive in hash table");
}
}
void test_churn_correctness(void)
{
// Insert 3x capacity to force heavy eviction.
// Advance time between each generation so eviction can distinguish old from new.
PacketHistory big(32);
uint32_t capacity = 32;
uint32_t generations = 3;
for (uint32_t gen = 0; gen < generations; gen++) {
if (gen > 0)
delay(1); // Ensure new generation has a newer timestamp than the old
for (uint32_t i = 1; i <= capacity; i++) {
auto p = makePacket(0xAAAA, gen * capacity + i);
big.wasSeenRecently(&p);
}
}
uint32_t total = capacity * generations;
// Only the most recent 32 should be present (due to LRU eviction)
for (uint32_t i = total - 31; i <= total; i++) {
auto p = makePacket(0xAAAA, i);
TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "Recent packet lost after churn");
}
// Older packets should be gone
int found = 0;
for (uint32_t i = 1; i <= total - capacity; i++) {
auto p = makePacket(0xAAAA, i);
if (big.wasSeenRecently(&p, false))
found++;
}
TEST_ASSERT_EQUAL_INT_MESSAGE(0, found, "Evicted packets should not be found");
}
// ===========================================================================
// Test runner
// ===========================================================================
void setup()
{
delay(10);
delay(2000);
initializeTestEnvironment();
UNITY_BEGIN();
// Group 1 — Initialization
RUN_TEST(test_init_valid_size);
RUN_TEST(test_init_minimum_size);
RUN_TEST(test_init_too_small_falls_back);
// Group 2 — Basic Deduplication
RUN_TEST(test_first_packet_not_seen);
RUN_TEST(test_same_packet_seen_twice);
RUN_TEST(test_different_id_not_confused);
RUN_TEST(test_different_sender_not_confused);
RUN_TEST(test_withUpdate_false_no_insert);
RUN_TEST(test_withUpdate_true_inserts);
// Group 3 — LRU Eviction
RUN_TEST(test_fill_capacity_all_found);
RUN_TEST(test_eviction_oldest_replaced);
RUN_TEST(test_matching_slot_reused);
RUN_TEST(test_free_slot_preferred);
RUN_TEST(test_evict_all_old_packets);
// Group 4 — Relayer Tracking
RUN_TEST(test_wasRelayer_true);
RUN_TEST(test_wasRelayer_false);
RUN_TEST(test_wasRelayer_zero_returns_false);
RUN_TEST(test_wasRelayer_not_found);
RUN_TEST(test_wasRelayer_wasSole_true);
RUN_TEST(test_wasRelayer_wasSole_false);
RUN_TEST(test_wasRelayer_all_six_slots);
// Group 5 — removeRelayer
RUN_TEST(test_removeRelayer_removes);
RUN_TEST(test_removeRelayer_compacts);
RUN_TEST(test_removeRelayer_nonexistent_safe);
RUN_TEST(test_removeRelayer_packet_not_found_safe);
// Group 6 — checkRelayers
RUN_TEST(test_checkRelayers_both_found);
RUN_TEST(test_checkRelayers_one_found);
RUN_TEST(test_checkRelayers_r2WasSole);
// Group 7 — Merge Logic
RUN_TEST(test_merge_preserves_original_next_hop);
RUN_TEST(test_merge_preserves_highest_hop_limit);
RUN_TEST(test_merge_no_duplicate_relayers);
RUN_TEST(test_merge_adds_new_relayer);
RUN_TEST(test_merge_we_relay_sets_slot_zero);
RUN_TEST(test_merge_heard_back_stores_relay_node);
// Group 8 — Fallback-to-Flooding Detection
RUN_TEST(test_fallback_detected);
RUN_TEST(test_fallback_not_when_we_relayed);
RUN_TEST(test_fallback_not_on_first_observation);
// Group 9 — Next-Hop and Upgrade Detection
RUN_TEST(test_weWereNextHop_true);
RUN_TEST(test_weWereNextHop_false);
RUN_TEST(test_wasUpgraded_true);
RUN_TEST(test_wasUpgraded_false);
// Group 10 — Edge Cases
RUN_TEST(test_packet_id_zero_not_stored);
RUN_TEST(test_sender_zero_substituted);
RUN_TEST(test_uninitialized_wasSeenRecently);
RUN_TEST(test_uninitialized_wasRelayer);
RUN_TEST(test_multiple_instances_independent);
// Group 11 — Hash Table Stress
RUN_TEST(test_many_packets_no_false_negatives);
RUN_TEST(test_many_packets_no_false_positives);
RUN_TEST(test_churn_correctness);
exit(UNITY_END());
}
void loop() {}
@@ -1,6 +1,7 @@
#define LED_POWER 33
#define LED_POWER2 34
#define EXT_PWR_DETECT 35
#define EXT_PWR_DETECT_MODE INPUT_PULLUP
#define BUTTON_PIN 18
#define BUTTON_ACTIVE_LOW false
@@ -1,4 +1,5 @@
#define EXT_PWR_DETECT 20
#define EXT_PWR_DETECT_MODE INPUT_PULLUP
#define BUTTON_PIN 17
#define BUTTON_ACTIVE_LOW false
+3
View File
@@ -42,6 +42,7 @@
#define DAC_I2S_MCLK -1
#define HAS_AXP2101
#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen)
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
@@ -60,6 +61,8 @@
#define BUTTON_PIN 0 // only for Plus version
#define PMU_IRQ 21 // Interrupt pin for the PMU
#define USE_SX1262
#define USE_SX1268
+4 -6
View File
@@ -9,8 +9,6 @@
#define BUTTON_PIN 0 // The middle button GPIO on the T-Beam S3
// #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module.
#define LED_STATE_ON 0 // State when LED is lit
// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if
// not found then probe for SX1262
#define USE_SX1262
@@ -48,9 +46,9 @@
#define LR11X0_DIO_AS_RF_SWITCH
#endif
// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts
// and waking from light sleep
// #define PMU_IRQ 40
// Voiding warrenties, we're gonna try the IRQ
#define PMU_IRQ 40
#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen)
#define HAS_AXP2101
// PCF8563 RTC Module
@@ -76,4 +74,4 @@
// has 32768 Hz crystal
#define HAS_32768HZ 1
#define USE_SH1106
#define USE_SH1106
@@ -85,7 +85,6 @@ static const uint8_t A0 = PIN_A0;
// charger status
#define EXT_CHRG_DETECT (32 + 6)
#define EXT_CHRG_DETECT_VALUE HIGH
// SPI
#define SPI_INTERFACES_COUNT 1
@@ -20,6 +20,7 @@
#include "variant.h"
#include "nrf.h"
#include "power.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
@@ -65,7 +66,11 @@ void variant_shutdown()
nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW;
nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1);
nrf_gpio_cfg_input(EXT_CHRG_DETECT, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input
nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_LOW;
nrf_gpio_cfg_sense_set(EXT_CHRG_DETECT, sense2);
// If we are sleeping because of low battery, wake up when the solar charger detects power.
// But if the user intentionally put us to sleep with the button, don't wake up just because the lights are on
if (power->isLowBattery()) {
nrf_gpio_cfg_input(EXT_CHRG_DETECT, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input
nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_LOW;
nrf_gpio_cfg_sense_set(EXT_CHRG_DETECT, sense2);
}
}
@@ -138,7 +138,7 @@ static const uint8_t A0 = PIN_A0;
#define HAS_SOLAR
#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3450
#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3490
#ifdef __cplusplus
}