Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ff7bc322f | ||
|
|
7adfc3f992 | ||
|
|
ba9cadc14d | ||
|
|
2815c9abc0 | ||
|
|
761ac4e08a | ||
|
|
f8368f5bd2 | ||
|
|
f60d329574 | ||
|
|
897c591ffe | ||
|
|
924411de59 | ||
|
|
d9195944df | ||
|
|
83a98c81f6 | ||
|
|
837637b70c | ||
|
|
e5caf2d52f | ||
|
|
53413ee502 | ||
|
|
04b5f14969 | ||
|
|
7b3f58875a | ||
|
|
b2d980fc25 | ||
|
|
2ed7bba5e7 |
+38
-129
@@ -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 don’t 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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) { \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user