mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-03 22:06:52 -04:00
Merge branch 'develop' into pioarduino
This commit is contained in:
8
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
8
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
@@ -75,11 +75,11 @@ body:
|
||||
- type: checkboxes
|
||||
id: mui
|
||||
attributes:
|
||||
label: Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?
|
||||
label: Is this bug report about any UI (https://meshtastic.org/docs/configuration/device-uis/) component firmware?
|
||||
options:
|
||||
- label: Meshtastic UI aka MUI colorTFT
|
||||
- label: InkHUD ePaper
|
||||
- label: OLED slide UI on any display
|
||||
- label: Meshtastic UI aka MUI
|
||||
- label: InkHUD
|
||||
- label: BaseUI
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
|
||||
@@ -8,18 +8,18 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.517
|
||||
- renovate@43.110.9
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.94.3
|
||||
- checkov@3.2.524
|
||||
- renovate@43.139.6
|
||||
- prettier@3.8.3
|
||||
- trufflehog@3.95.2
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.4
|
||||
- trivy@0.69.3
|
||||
- trivy@0.70.0
|
||||
- taplo@0.10.0
|
||||
- ruff@0.15.9
|
||||
- ruff@0.15.11
|
||||
- isort@8.0.1
|
||||
- markdownlint@0.48.0
|
||||
- oxipng@10.1.0
|
||||
- oxipng@10.1.1
|
||||
- svgo@4.0.1
|
||||
- actionlint@1.7.12
|
||||
- flake8@7.3.0
|
||||
|
||||
211
src/Power.cpp
211
src/Power.cpp
@@ -165,16 +165,31 @@ static bool initAdcCalibration()
|
||||
|
||||
#endif // BATTERY_PIN && ARCH_ESP32
|
||||
|
||||
#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
|
||||
static const uint8_t ext_chrg_detect_mode = INPUT;
|
||||
#else
|
||||
static const uint8_t ext_chrg_detect_mode = 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
|
||||
static const uint8_t ext_chrg_detect_value = HIGH;
|
||||
#else
|
||||
static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE;
|
||||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -523,28 +538,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
|
||||
@@ -565,9 +566,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
|
||||
@@ -700,14 +701,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
|
||||
@@ -789,37 +786,17 @@ bool Power::setup()
|
||||
found = true;
|
||||
#endif
|
||||
}
|
||||
#ifdef EXT_PWR_DETECT
|
||||
attachInterrupt(
|
||||
EXT_PWR_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef BATTERY_CHARGING_INV
|
||||
attachInterrupt(
|
||||
BATTERY_CHARGING_INV,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
attachInterrupt(
|
||||
EXT_CHRG_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
attachPowerInterrupts();
|
||||
enabled = found;
|
||||
low_voltage_counter = 0;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
// Used to detach and reattach interrupts
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -1066,6 +1043,17 @@ int32_t Power::runOnce()
|
||||
powerFSM.trigger(EVENT_POWER_CONNECTED);
|
||||
}
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
/*
|
||||
In the T-Watch S3 this code fragment reacts to the short press of the button by switching the
|
||||
display 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...
|
||||
|
||||
@@ -1098,6 +1086,97 @@ int32_t Power::runOnce()
|
||||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int Power::beforeLightSleep(void *unused)
|
||||
{
|
||||
LOG_WARN("Detaching power interrupts for sleep");
|
||||
detachPowerInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int Power::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
attachPowerInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Attach (or re-attach) hardware interrupts for power management
|
||||
* Public method. Used outside class when waking from MCU sleep
|
||||
*/
|
||||
void Power::attachPowerInterrupts()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
attachInterrupt(
|
||||
EXT_PWR_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef BATTERY_CHARGING_INV
|
||||
attachInterrupt(
|
||||
BATTERY_CHARGING_INV,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
attachInterrupt(
|
||||
EXT_CHRG_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef PMU_IRQ
|
||||
if (PMU) {
|
||||
attachInterrupt(
|
||||
PMU_IRQ,
|
||||
[] {
|
||||
pmu_irq = true;
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
FALLING);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* Detach the "normal" button interrupts.
|
||||
* Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep
|
||||
*/
|
||||
void Power::detachPowerInterrupts()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
detachInterrupt(EXT_PWR_DETECT);
|
||||
#endif
|
||||
#ifdef BATTERY_CHARGING_INV
|
||||
detachInterrupt(BATTERY_CHARGING_INV);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
detachInterrupt(EXT_CHRG_DETECT);
|
||||
#endif
|
||||
#ifdef PMU_IRQ
|
||||
if (PMU) {
|
||||
detachInterrupt(PMU_IRQ);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the power manager chip
|
||||
*
|
||||
@@ -1375,8 +1454,6 @@ bool Power::axpChipInit()
|
||||
}
|
||||
|
||||
pinMode(PMU_IRQ, INPUT);
|
||||
attachInterrupt(
|
||||
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
||||
|
||||
// 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
|
||||
@@ -1849,7 +1926,7 @@ 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;
|
||||
@@ -1858,7 +1935,7 @@ class SerialBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
|
||||
#endif
|
||||
// by default, we check the battery voltage only
|
||||
@@ -1880,10 +1957,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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
129
src/main.cpp
129
src/main.cpp
@@ -340,138 +340,9 @@ void setup()
|
||||
|
||||
#ifdef BLE_LED
|
||||
pinMode(BLE_LED, OUTPUT);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// GPIO10 manages all peripheral power supplies
|
||||
// Turn on peripheral power immediately after MUC starts.
|
||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
||||
pinMode(KB_POWERON, OUTPUT);
|
||||
digitalWrite(KB_POWERON, HIGH);
|
||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
||||
// e.g. when detecting the SD card
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
delay(100);
|
||||
#elif defined(T_DECK_PRO)
|
||||
pinMode(LORA_EN, OUTPUT);
|
||||
digitalWrite(LORA_EN, HIGH);
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
#if PIN_EINK_RES >= 0
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
#endif
|
||||
pinMode(CST328_PIN_RST, OUTPUT);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
#elif defined(T_LORA_PAGER)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
pinMode(KB_INT, INPUT_PULLUP);
|
||||
// io expander
|
||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
pinMode(KB_INT, INPUT);
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// GPIO10 manages all peripheral power supplies
|
||||
// Turn on peripheral power immediately after MUC starts.
|
||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
||||
pinMode(KB_POWERON, OUTPUT);
|
||||
digitalWrite(KB_POWERON, HIGH);
|
||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
||||
// e.g. when detecting the SD card
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
delay(100);
|
||||
#elif defined(T_DECK_PRO)
|
||||
pinMode(LORA_EN, OUTPUT);
|
||||
digitalWrite(LORA_EN, HIGH);
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
#if PIN_EINK_RES >= 0
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
#endif
|
||||
pinMode(CST328_PIN_RST, OUTPUT);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
#elif defined(T_LORA_PAGER)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
pinMode(KB_INT, INPUT_PULLUP);
|
||||
// io expander
|
||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
pinMode(KB_INT, INPUT);
|
||||
#endif
|
||||
|
||||
concurrency::hasBeenSetup = true;
|
||||
#if HAS_SCREEN
|
||||
meshtastic_Config_DisplayConfig_OledType screen_model =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -470,8 +470,13 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_tak_tag:
|
||||
LOG_DEBUG("Send module config: tak");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_tak_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.tak = moduleConfig.tak;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("Unknown module config type %d", config_state);
|
||||
LOG_DEBUG("Unhandled module config type %d", config_state);
|
||||
}
|
||||
|
||||
config_state++;
|
||||
|
||||
@@ -46,6 +46,16 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
|
||||
#endif
|
||||
}
|
||||
|
||||
RadioLibInterface::~RadioLibInterface()
|
||||
{
|
||||
// If the static `instance` pointer still references us, clear it.
|
||||
// A later successful init() may have replaced `instance` with a newer
|
||||
// interface — don't clobber that case.
|
||||
if (instance == this) {
|
||||
instance = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// ESP32 doesn't use that flag
|
||||
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
||||
|
||||
@@ -136,6 +136,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
|
||||
|
||||
/**
|
||||
* Clear the static `instance` pointer if it still points at us, so callers
|
||||
* that check `RadioLibInterface::instance != nullptr` don't dereference a
|
||||
* freed object after a failed init() + unique_ptr reset.
|
||||
*/
|
||||
virtual ~RadioLibInterface();
|
||||
|
||||
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
|
||||
|
||||
/**
|
||||
|
||||
@@ -499,9 +499,9 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
meshtastic_Data decodedtmp;
|
||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
|
||||
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
|
||||
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
|
||||
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||
LOG_ERROR("Invalid portnum (bad psk?)!");
|
||||
LOG_DEBUG("Invalid portnum (bad psk?)");
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
|
||||
LOG_WARN("Rejecting legacy DM");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -206,7 +206,7 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
||||
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
||||
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
||||
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, p.payload.size);
|
||||
|
||||
this->packetHistoryTotalCount++;
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
/**
|
||||
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
|
||||
*/
|
||||
virtual void onNowHasData(uint32_t fromRadioNum)
|
||||
virtual void onNowHasData(uint32_t fromRadioNum) override
|
||||
{
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
|
||||
@@ -337,7 +337,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
}
|
||||
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||
virtual bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||
|
||||
void requestHighThroughputConnection(uint16_t conn_handle)
|
||||
{
|
||||
|
||||
@@ -19,5 +19,4 @@ class NimbleBluetooth : BluetoothApi
|
||||
void setupService();
|
||||
};
|
||||
|
||||
void setBluetoothEnable(bool enable);
|
||||
void clearNVS();
|
||||
void setBluetoothEnable(bool enable);
|
||||
19
src/power.h
19
src/power.h
@@ -83,7 +83,7 @@ extern RAK9154Sensor rak9154Sensor;
|
||||
extern XPowersLibInterface *PMU;
|
||||
#endif
|
||||
|
||||
class Power : private concurrency::OSThread
|
||||
class Power : public concurrency::OSThread
|
||||
{
|
||||
|
||||
public:
|
||||
@@ -97,6 +97,15 @@ class Power : private 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);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
void attachPowerInterrupts();
|
||||
void detachPowerInterrupts();
|
||||
|
||||
protected:
|
||||
meshtastic::PowerStatus *statusHandler;
|
||||
@@ -122,6 +131,14 @@ class Power : private concurrency::OSThread
|
||||
// open circuit voltage lookup table
|
||||
uint8_t low_voltage_counter;
|
||||
uint32_t lastLogTime = 0;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<Power, void *> lsObserver = CallbackObserver<Power, void *>(this, &Power::beforeLightSleep);
|
||||
CallbackObserver<Power, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<Power, esp_sleep_wakeup_cause_t>(this, &Power::afterLightSleep);
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_HEAP
|
||||
uint32_t lastheap;
|
||||
#endif
|
||||
|
||||
@@ -513,13 +513,12 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
|
||||
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||
notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
|
||||
|
||||
#ifdef BUTTON_PIN
|
||||
if (cause == ESP_SLEEP_WAKEUP_GPIO) {
|
||||
LOG_INFO("Exit light sleep gpio: btn=%d",
|
||||
!digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
LOG_INFO("Exit light sleep gpio");
|
||||
// If we woke because of a GPIO, it's possible power needs to run to handle.
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
} else {
|
||||
LOG_INFO("Exit light sleep cause: %d", cause);
|
||||
}
|
||||
|
||||
|
||||
834
test/test_packet_history/test_main.cpp
Normal file
834
test/test_packet_history/test_main.cpp
Normal file
@@ -0,0 +1,834 @@
|
||||
/*
|
||||
* Unit tests for PacketHistory — the packet deduplication engine
|
||||
* used by the mesh routing stack.
|
||||
*
|
||||
* PacketHistory maintains a fixed-size array of PacketRecords with an
|
||||
* optional hash table for O(1) lookup. It tracks which nodes relayed
|
||||
* each packet, supports LRU-style eviction, and detects fallback-to-
|
||||
* flooding and hop-limit upgrades.
|
||||
*/
|
||||
|
||||
#include "PacketHistory.h"
|
||||
|
||||
#include "TestUtil.h"
|
||||
#include <unity.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
static constexpr uint32_t OUR_NODE_NUM = 0xDEAD1234;
|
||||
static constexpr uint8_t OUR_RELAY_ID = 0x34; // getLastByteOfNodeNum(OUR_NODE_NUM)
|
||||
static constexpr uint32_t SMALL_CAPACITY = 8;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-test state
|
||||
// ---------------------------------------------------------------------------
|
||||
static PacketHistory *ph = nullptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
static meshtastic_MeshPacket makePacket(uint32_t from, uint32_t id, uint8_t hop_limit = 3,
|
||||
uint8_t next_hop = NO_NEXT_HOP_PREFERENCE, uint8_t relay_node = 0)
|
||||
{
|
||||
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
||||
p.from = from;
|
||||
p.id = id;
|
||||
p.hop_limit = hop_limit;
|
||||
p.next_hop = next_hop;
|
||||
p.relay_node = relay_node;
|
||||
return p;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setUp / tearDown — called before and after every test
|
||||
// ---------------------------------------------------------------------------
|
||||
void setUp(void)
|
||||
{
|
||||
myNodeInfo.my_node_num = OUR_NODE_NUM;
|
||||
ph = new PacketHistory(SMALL_CAPACITY);
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
delete ph;
|
||||
ph = nullptr;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 1 — Initialization
|
||||
// ===========================================================================
|
||||
|
||||
void test_init_valid_size(void)
|
||||
{
|
||||
PacketHistory h(8);
|
||||
TEST_ASSERT_TRUE(h.initOk());
|
||||
}
|
||||
|
||||
void test_init_minimum_size(void)
|
||||
{
|
||||
PacketHistory h(4);
|
||||
TEST_ASSERT_TRUE(h.initOk());
|
||||
}
|
||||
|
||||
void test_init_too_small_falls_back(void)
|
||||
{
|
||||
// Sizes < 4 or > PACKETHISTORY_MAX are clamped to PACKETHISTORY_MAX inside the constructor
|
||||
PacketHistory h(2);
|
||||
TEST_ASSERT_TRUE(h.initOk());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 2 — Basic Deduplication
|
||||
// ===========================================================================
|
||||
|
||||
void test_first_packet_not_seen(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100);
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p));
|
||||
}
|
||||
|
||||
void test_same_packet_seen_twice(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100);
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // first time
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p)); // duplicate
|
||||
}
|
||||
|
||||
void test_different_id_not_confused(void)
|
||||
{
|
||||
auto p1 = makePacket(0x1111, 100);
|
||||
auto p2 = makePacket(0x1111, 200);
|
||||
ph->wasSeenRecently(&p1);
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2));
|
||||
}
|
||||
|
||||
void test_different_sender_not_confused(void)
|
||||
{
|
||||
auto p1 = makePacket(0x1111, 100);
|
||||
auto p2 = makePacket(0x2222, 100);
|
||||
ph->wasSeenRecently(&p1);
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p2));
|
||||
}
|
||||
|
||||
void test_withUpdate_false_no_insert(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100);
|
||||
// First call with withUpdate=false: should not store
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/false));
|
||||
// Second call with withUpdate=true: still not found because first didn't store
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true));
|
||||
}
|
||||
|
||||
void test_withUpdate_true_inserts(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100);
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, /*withUpdate=*/true));
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, /*withUpdate=*/false)); // found without inserting again
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 3 — LRU Eviction
|
||||
// ===========================================================================
|
||||
|
||||
void test_fill_capacity_all_found(void)
|
||||
{
|
||||
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
ph->wasSeenRecently(&p);
|
||||
}
|
||||
// All 8 should be found
|
||||
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
|
||||
}
|
||||
}
|
||||
|
||||
void test_eviction_oldest_replaced(void)
|
||||
{
|
||||
// Fill all 8 slots
|
||||
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
ph->wasSeenRecently(&p);
|
||||
}
|
||||
|
||||
// Advance time so the eviction logic can distinguish "oldest" from "newest".
|
||||
// insert() uses (now_millis - rxTimeMsec) > OldtrxTimeMsec with strict >, so
|
||||
// entries with identical timestamps all have age 0 and none gets selected.
|
||||
delay(1);
|
||||
|
||||
// Insert a 9th packet — should evict the oldest
|
||||
auto p9 = makePacket(0xAAAA, 9);
|
||||
ph->wasSeenRecently(&p9);
|
||||
|
||||
// The 9th should be found
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p9, false));
|
||||
|
||||
// At least one of the originals should have been evicted
|
||||
int evicted = 0;
|
||||
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
if (!ph->wasSeenRecently(&p, false))
|
||||
evicted++;
|
||||
}
|
||||
TEST_ASSERT_TRUE(evicted > 0);
|
||||
}
|
||||
|
||||
void test_matching_slot_reused(void)
|
||||
{
|
||||
// Insert packet, then re-insert same (sender, id) — should reuse slot, not evict others
|
||||
auto p1 = makePacket(0xAAAA, 1);
|
||||
auto p2 = makePacket(0xBBBB, 2);
|
||||
ph->wasSeenRecently(&p1);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
// Re-observe p1 (triggers merge path)
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Both should still be present
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p1, false));
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
|
||||
}
|
||||
|
||||
void test_free_slot_preferred(void)
|
||||
{
|
||||
// Insert 4 packets into capacity-8 history — next insert should use a free slot, not evict
|
||||
for (uint32_t i = 1; i <= 4; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
ph->wasSeenRecently(&p);
|
||||
}
|
||||
auto p5 = makePacket(0xAAAA, 5);
|
||||
ph->wasSeenRecently(&p5);
|
||||
|
||||
// All 5 should be present (no eviction needed)
|
||||
for (uint32_t i = 1; i <= 5; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
|
||||
}
|
||||
}
|
||||
|
||||
void test_evict_all_old_packets(void)
|
||||
{
|
||||
// Fill with packets 1..8
|
||||
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
ph->wasSeenRecently(&p);
|
||||
}
|
||||
|
||||
// Advance time so the replacement batch can evict the originals
|
||||
delay(1);
|
||||
|
||||
// Replace all with packets 101..108
|
||||
for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xBBBB, i);
|
||||
ph->wasSeenRecently(&p);
|
||||
}
|
||||
// None of the originals should be found
|
||||
for (uint32_t i = 1; i <= SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p, false));
|
||||
}
|
||||
// All new ones should be found
|
||||
for (uint32_t i = 101; i <= 100 + SMALL_CAPACITY; i++) {
|
||||
auto p = makePacket(0xBBBB, i);
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 4 — Relayer Tracking
|
||||
// ===========================================================================
|
||||
|
||||
void test_wasRelayer_true(void)
|
||||
{
|
||||
// Non-us relay_nodes only enter relayed_by[] through the "heard-back" merge path:
|
||||
// we must have relayed first, then observe the packet return at hop_limit-1.
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Heard-back from 0xCC at hop_limit=2 (ourTxHopLimit-1) triggers the merge
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_wasRelayer_false(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA);
|
||||
ph->wasSeenRecently(&p);
|
||||
// 0xCC was never a relayer
|
||||
TEST_ASSERT_FALSE(ph->wasRelayer(0xCC, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_wasRelayer_zero_returns_false(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100);
|
||||
ph->wasSeenRecently(&p);
|
||||
TEST_ASSERT_FALSE(ph->wasRelayer(0, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_wasRelayer_not_found(void)
|
||||
{
|
||||
// Packet not in history at all
|
||||
TEST_ASSERT_FALSE(ph->wasRelayer(0xAA, 999, 0x9999));
|
||||
}
|
||||
|
||||
void test_wasRelayer_wasSole_true(void)
|
||||
{
|
||||
// relay_node = ourRelayID → relayed_by[0] = ourRelayID
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p);
|
||||
|
||||
bool wasSole = false;
|
||||
bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole);
|
||||
TEST_ASSERT_TRUE(result);
|
||||
TEST_ASSERT_TRUE(wasSole);
|
||||
}
|
||||
|
||||
void test_wasRelayer_wasSole_false(void)
|
||||
{
|
||||
// First observation: we relay
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Second observation: different relayer adds to record
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
bool wasSole = true;
|
||||
bool result = ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole);
|
||||
TEST_ASSERT_TRUE(result);
|
||||
TEST_ASSERT_FALSE(wasSole);
|
||||
}
|
||||
|
||||
void test_wasRelayer_all_six_slots(void)
|
||||
{
|
||||
// First observation: we relay with hop_limit=3 (fills slot 0, ourTxHopLimit=3)
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p);
|
||||
|
||||
// Each heard-back must satisfy: hop_limit == ourTxHopLimit OR ourTxHopLimit-1.
|
||||
// Using hop_limit=2 (ourTxHopLimit-1) for all, which triggers the heard-back
|
||||
// merge path each time. Each new relay_node pushes to slot 0 and shifts existing
|
||||
// relayers right, eventually filling all NUM_RELAYERS(6) slots.
|
||||
uint8_t relayers[] = {0x11, 0x22, 0x33, 0x44, 0x55};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
auto pn = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, relayers[i]);
|
||||
ph->wasSeenRecently(&pn);
|
||||
}
|
||||
|
||||
// All 6 should be detected
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
for (int i = 0; i < 5; i++) {
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(relayers[i], 100, 0x1111));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 5 — removeRelayer
|
||||
// ===========================================================================
|
||||
|
||||
void test_removeRelayer_removes(void)
|
||||
{
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
|
||||
ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111);
|
||||
TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_removeRelayer_compacts(void)
|
||||
{
|
||||
// We relay first
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
// Second relayer
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
// Remove us, 0xBB should still be found
|
||||
ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111);
|
||||
TEST_ASSERT_FALSE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_removeRelayer_nonexistent_safe(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p);
|
||||
// Removing a relayer that doesn't exist should not crash
|
||||
ph->removeRelayer(0xFF, 100, 0x1111);
|
||||
// Original should still be there
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_removeRelayer_packet_not_found_safe(void)
|
||||
{
|
||||
// Packet not in history — should not crash
|
||||
ph->removeRelayer(0xAA, 999, 0x9999);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 6 — checkRelayers
|
||||
// ===========================================================================
|
||||
|
||||
void test_checkRelayers_both_found(void)
|
||||
{
|
||||
// We relay first
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
// Second relayer
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
bool r1 = false, r2 = false;
|
||||
ph->checkRelayers(OUR_RELAY_ID, 0xBB, 100, 0x1111, &r1, &r2);
|
||||
TEST_ASSERT_TRUE(r1);
|
||||
TEST_ASSERT_TRUE(r2);
|
||||
}
|
||||
|
||||
void test_checkRelayers_one_found(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p);
|
||||
|
||||
bool r1 = false, r2 = false;
|
||||
ph->checkRelayers(OUR_RELAY_ID, 0xCC, 100, 0x1111, &r1, &r2);
|
||||
TEST_ASSERT_TRUE(r1);
|
||||
TEST_ASSERT_FALSE(r2);
|
||||
}
|
||||
|
||||
void test_checkRelayers_r2WasSole(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p);
|
||||
|
||||
bool r1 = false, r2 = false, r2Sole = false;
|
||||
// relayer1=0xCC (not found), relayer2=OUR_RELAY_ID (sole relayer)
|
||||
ph->checkRelayers(0xCC, OUR_RELAY_ID, 100, 0x1111, &r1, &r2, &r2Sole);
|
||||
TEST_ASSERT_FALSE(r1);
|
||||
TEST_ASSERT_TRUE(r2);
|
||||
TEST_ASSERT_TRUE(r2Sole);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 7 — wasSeenRecently Merge Logic
|
||||
// ===========================================================================
|
||||
|
||||
void test_merge_preserves_original_next_hop(void)
|
||||
{
|
||||
// First observation with next_hop=0x55
|
||||
auto p1 = makePacket(0x1111, 100, 3, 0x55, 0xAA);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Re-observation with different next_hop
|
||||
auto p2 = makePacket(0x1111, 100, 2, 0x77, 0xBB);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
// The stored next_hop should still be 0x55 (the original)
|
||||
// We verify via weWereNextHop: if we set original next_hop to ourRelayID, it should detect it
|
||||
auto p3 = makePacket(0x1111, 200, 3, OUR_RELAY_ID, 0xAA);
|
||||
ph->wasSeenRecently(&p3);
|
||||
auto p4 = makePacket(0x1111, 200, 2, 0x99, 0xBB);
|
||||
bool weWereNextHop = false;
|
||||
ph->wasSeenRecently(&p4, true, nullptr, &weWereNextHop);
|
||||
TEST_ASSERT_TRUE(weWereNextHop);
|
||||
}
|
||||
|
||||
void test_merge_preserves_highest_hop_limit(void)
|
||||
{
|
||||
// First observation with hop_limit=5
|
||||
auto p1 = makePacket(0x1111, 100, 5);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Re-observation with hop_limit=2 (lower)
|
||||
auto p2 = makePacket(0x1111, 100, 2);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
// Third observation with hop_limit=3 should not trigger upgrade (highest was 5)
|
||||
bool wasUpgraded = true;
|
||||
auto p3 = makePacket(0x1111, 100, 3);
|
||||
ph->wasSeenRecently(&p3, true, nullptr, nullptr, &wasUpgraded);
|
||||
TEST_ASSERT_FALSE(wasUpgraded);
|
||||
}
|
||||
|
||||
void test_merge_no_duplicate_relayers(void)
|
||||
{
|
||||
// Observe with relayer 0xAA (stored via relay_node, but only slot 0 for ourRelayID)
|
||||
// We need to use ourRelayID for the first observation to get it into slot 0
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Re-observe with same relay_node=ourRelayID — should not create duplicates
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
// ourRelayID should appear exactly once — wasSole should still be true
|
||||
bool wasSole = false;
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111, &wasSole));
|
||||
TEST_ASSERT_TRUE(wasSole);
|
||||
}
|
||||
|
||||
void test_merge_adds_new_relayer(void)
|
||||
{
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(0xBB, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_merge_we_relay_sets_slot_zero(void)
|
||||
{
|
||||
// When relay_node == ourRelayID, relayed_by[0] should be set to ourRelayID
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p);
|
||||
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
}
|
||||
|
||||
void test_merge_heard_back_stores_relay_node(void)
|
||||
{
|
||||
// First: we relay (hop_limit=3)
|
||||
auto p1 = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Second: we hear the packet back with hop_limit=2 (one less), from relay_node=0xCC
|
||||
// This triggers the "heard back" logic: weWereRelayer && hop_limit == ourTxHopLimit-1
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xCC);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(OUR_RELAY_ID, 100, 0x1111));
|
||||
TEST_ASSERT_TRUE(ph->wasRelayer(0xCC, 100, 0x1111));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 8 — Fallback-to-Flooding Detection
|
||||
// ===========================================================================
|
||||
|
||||
void test_fallback_detected(void)
|
||||
{
|
||||
// The fallback condition requires wasRelayer(relay_node) && !wasRelayer(ourRelayID).
|
||||
// Non-us relayers only enter relayed_by[] via the heard-back merge path, which
|
||||
// also stores ourRelayID. So we must removeRelayer(ourRelayID) to satisfy both.
|
||||
//
|
||||
// Scenario: we relay a directed packet, hear it back from 0xAA, then the router
|
||||
// removes us from the relayer list. Later the sender falls back to flooding.
|
||||
|
||||
// Step 1: We relay (directed to next_hop=0x55)
|
||||
auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Step 2: Heard-back from 0xAA at hop_limit-1 → stores 0xAA in relayed_by
|
||||
auto p2 = makePacket(0x1111, 100, 2, 0x55, 0xAA);
|
||||
ph->wasSeenRecently(&p2);
|
||||
|
||||
// Step 3: Router removes us from the relayer list
|
||||
ph->removeRelayer(OUR_RELAY_ID, 100, 0x1111);
|
||||
|
||||
// Step 4: Sender falls back to flooding — same packet, NO_NEXT_HOP_PREFERENCE, from 0xAA
|
||||
auto p3 = makePacket(0x1111, 100, 1, NO_NEXT_HOP_PREFERENCE, 0xAA);
|
||||
bool wasFallback = false;
|
||||
ph->wasSeenRecently(&p3, true, &wasFallback);
|
||||
TEST_ASSERT_TRUE(wasFallback);
|
||||
}
|
||||
|
||||
void test_fallback_not_when_we_relayed(void)
|
||||
{
|
||||
// First observation: directed, we relayed it
|
||||
auto p1 = makePacket(0x1111, 100, 3, 0x55, OUR_RELAY_ID);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Second observation: fallback to flooding from same relayer (us)
|
||||
// But since we already relayed, wasFallback should be false
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, OUR_RELAY_ID);
|
||||
bool wasFallback = false;
|
||||
ph->wasSeenRecently(&p2, true, &wasFallback);
|
||||
TEST_ASSERT_FALSE(wasFallback);
|
||||
}
|
||||
|
||||
void test_fallback_not_on_first_observation(void)
|
||||
{
|
||||
// First time seen — can't be a fallback
|
||||
auto p = makePacket(0x1111, 100, 3, NO_NEXT_HOP_PREFERENCE, 0xAA);
|
||||
bool wasFallback = false;
|
||||
ph->wasSeenRecently(&p, true, &wasFallback);
|
||||
TEST_ASSERT_FALSE(wasFallback);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 9 — Next-Hop and Upgrade Detection
|
||||
// ===========================================================================
|
||||
|
||||
void test_weWereNextHop_true(void)
|
||||
{
|
||||
// Packet directed to us (next_hop = ourRelayID)
|
||||
auto p1 = makePacket(0x1111, 100, 3, OUR_RELAY_ID, 0xAA);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Re-observe: check if we were the original next_hop
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
|
||||
bool weWereNextHop = false;
|
||||
ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop);
|
||||
TEST_ASSERT_TRUE(weWereNextHop);
|
||||
}
|
||||
|
||||
void test_weWereNextHop_false(void)
|
||||
{
|
||||
// Packet directed to someone else
|
||||
auto p1 = makePacket(0x1111, 100, 3, 0x99, 0xAA);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
auto p2 = makePacket(0x1111, 100, 2, NO_NEXT_HOP_PREFERENCE, 0xBB);
|
||||
bool weWereNextHop = false;
|
||||
ph->wasSeenRecently(&p2, true, nullptr, &weWereNextHop);
|
||||
TEST_ASSERT_FALSE(weWereNextHop);
|
||||
}
|
||||
|
||||
void test_wasUpgraded_true(void)
|
||||
{
|
||||
// First observation with hop_limit=3 → stored as highestHopLimit bits 0-2 = 3
|
||||
auto p1 = makePacket(0x1111, 100, 3);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Re-observation with hop_limit=5
|
||||
// The upgrade check on line 122 compares the raw packed byte found->hop_limit against p->hop_limit.
|
||||
// found->hop_limit has highestHopLimit=3 in bits 0-2 (and possibly ourTxHopLimit in bits 3-5).
|
||||
// So the packed byte value is 3 (or more if ourTxHopLimit was set), and p->hop_limit is 5.
|
||||
// Since 3 < 5 (with no ourTxHopLimit set), this should detect an upgrade.
|
||||
auto p2 = makePacket(0x1111, 100, 5);
|
||||
bool wasUpgraded = false;
|
||||
ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded);
|
||||
TEST_ASSERT_TRUE(wasUpgraded);
|
||||
}
|
||||
|
||||
void test_wasUpgraded_false(void)
|
||||
{
|
||||
auto p1 = makePacket(0x1111, 100, 5);
|
||||
ph->wasSeenRecently(&p1);
|
||||
|
||||
// Same or lower hop_limit
|
||||
auto p2 = makePacket(0x1111, 100, 3);
|
||||
bool wasUpgraded = false;
|
||||
ph->wasSeenRecently(&p2, true, nullptr, nullptr, &wasUpgraded);
|
||||
TEST_ASSERT_FALSE(wasUpgraded);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 10 — Edge Cases
|
||||
// ===========================================================================
|
||||
|
||||
void test_packet_id_zero_not_stored(void)
|
||||
{
|
||||
auto p = makePacket(0x1111, 0);
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p));
|
||||
TEST_ASSERT_FALSE(ph->wasSeenRecently(&p)); // still not found
|
||||
}
|
||||
|
||||
void test_sender_zero_substituted(void)
|
||||
{
|
||||
// from=0 means "from us" — getFrom() substitutes nodeDB->getNodeNum()
|
||||
auto p = makePacket(0, 100);
|
||||
ph->wasSeenRecently(&p);
|
||||
|
||||
// Should be stored under our node num, not 0
|
||||
auto p2 = makePacket(OUR_NODE_NUM, 100);
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
|
||||
}
|
||||
|
||||
void test_uninitialized_wasSeenRecently(void)
|
||||
{
|
||||
// Simulate uninitialized state — create a PacketHistory that looks uninitialized
|
||||
// We can't easily make allocation fail, but we can test the initOk guard with a destructed one
|
||||
PacketHistory h(4);
|
||||
TEST_ASSERT_TRUE(h.initOk()); // sanity check
|
||||
h.~PacketHistory();
|
||||
|
||||
auto p = makePacket(0x1111, 100);
|
||||
TEST_ASSERT_FALSE(h.wasSeenRecently(&p));
|
||||
|
||||
// Reconstruct in place to allow proper destruction
|
||||
new (&h) PacketHistory(4);
|
||||
}
|
||||
|
||||
void test_uninitialized_wasRelayer(void)
|
||||
{
|
||||
PacketHistory h(4);
|
||||
h.~PacketHistory();
|
||||
|
||||
TEST_ASSERT_FALSE(h.wasRelayer(0xAA, 100, 0x1111));
|
||||
|
||||
new (&h) PacketHistory(4);
|
||||
}
|
||||
|
||||
void test_multiple_instances_independent(void)
|
||||
{
|
||||
PacketHistory h2(SMALL_CAPACITY);
|
||||
|
||||
auto p = makePacket(0x1111, 100);
|
||||
ph->wasSeenRecently(&p);
|
||||
|
||||
// h2 should NOT find it
|
||||
TEST_ASSERT_FALSE(h2.wasSeenRecently(&p, false));
|
||||
|
||||
// ph should still find it
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p, false));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Group 11 — Hash Table Stress
|
||||
// ===========================================================================
|
||||
|
||||
void test_many_packets_no_false_negatives(void)
|
||||
{
|
||||
PacketHistory big(64);
|
||||
for (uint32_t i = 1; i <= 64; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
big.wasSeenRecently(&p);
|
||||
}
|
||||
for (uint32_t i = 1; i <= 64; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "False negative in hash table");
|
||||
}
|
||||
}
|
||||
|
||||
void test_many_packets_no_false_positives(void)
|
||||
{
|
||||
PacketHistory big(64);
|
||||
for (uint32_t i = 1; i <= 64; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
big.wasSeenRecently(&p);
|
||||
}
|
||||
// IDs 65..128 were never inserted
|
||||
for (uint32_t i = 65; i <= 128; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
TEST_ASSERT_FALSE_MESSAGE(big.wasSeenRecently(&p, false), "False positive in hash table");
|
||||
}
|
||||
}
|
||||
|
||||
void test_churn_correctness(void)
|
||||
{
|
||||
// Insert 3x capacity to force heavy eviction.
|
||||
// Advance time between each generation so eviction can distinguish old from new.
|
||||
PacketHistory big(32);
|
||||
uint32_t capacity = 32;
|
||||
uint32_t generations = 3;
|
||||
|
||||
for (uint32_t gen = 0; gen < generations; gen++) {
|
||||
if (gen > 0)
|
||||
delay(1); // Ensure new generation has a newer timestamp than the old
|
||||
for (uint32_t i = 1; i <= capacity; i++) {
|
||||
auto p = makePacket(0xAAAA, gen * capacity + i);
|
||||
big.wasSeenRecently(&p);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t total = capacity * generations;
|
||||
|
||||
// Only the most recent 32 should be present (due to LRU eviction)
|
||||
for (uint32_t i = total - 31; i <= total; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
TEST_ASSERT_TRUE_MESSAGE(big.wasSeenRecently(&p, false), "Recent packet lost after churn");
|
||||
}
|
||||
// Older packets should be gone
|
||||
int found = 0;
|
||||
for (uint32_t i = 1; i <= total - capacity; i++) {
|
||||
auto p = makePacket(0xAAAA, i);
|
||||
if (big.wasSeenRecently(&p, false))
|
||||
found++;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, found, "Evicted packets should not be found");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Test runner
|
||||
// ===========================================================================
|
||||
|
||||
void setup()
|
||||
{
|
||||
delay(10);
|
||||
delay(2000);
|
||||
|
||||
initializeTestEnvironment();
|
||||
UNITY_BEGIN();
|
||||
|
||||
// Group 1 — Initialization
|
||||
RUN_TEST(test_init_valid_size);
|
||||
RUN_TEST(test_init_minimum_size);
|
||||
RUN_TEST(test_init_too_small_falls_back);
|
||||
|
||||
// Group 2 — Basic Deduplication
|
||||
RUN_TEST(test_first_packet_not_seen);
|
||||
RUN_TEST(test_same_packet_seen_twice);
|
||||
RUN_TEST(test_different_id_not_confused);
|
||||
RUN_TEST(test_different_sender_not_confused);
|
||||
RUN_TEST(test_withUpdate_false_no_insert);
|
||||
RUN_TEST(test_withUpdate_true_inserts);
|
||||
|
||||
// Group 3 — LRU Eviction
|
||||
RUN_TEST(test_fill_capacity_all_found);
|
||||
RUN_TEST(test_eviction_oldest_replaced);
|
||||
RUN_TEST(test_matching_slot_reused);
|
||||
RUN_TEST(test_free_slot_preferred);
|
||||
RUN_TEST(test_evict_all_old_packets);
|
||||
|
||||
// Group 4 — Relayer Tracking
|
||||
RUN_TEST(test_wasRelayer_true);
|
||||
RUN_TEST(test_wasRelayer_false);
|
||||
RUN_TEST(test_wasRelayer_zero_returns_false);
|
||||
RUN_TEST(test_wasRelayer_not_found);
|
||||
RUN_TEST(test_wasRelayer_wasSole_true);
|
||||
RUN_TEST(test_wasRelayer_wasSole_false);
|
||||
RUN_TEST(test_wasRelayer_all_six_slots);
|
||||
|
||||
// Group 5 — removeRelayer
|
||||
RUN_TEST(test_removeRelayer_removes);
|
||||
RUN_TEST(test_removeRelayer_compacts);
|
||||
RUN_TEST(test_removeRelayer_nonexistent_safe);
|
||||
RUN_TEST(test_removeRelayer_packet_not_found_safe);
|
||||
|
||||
// Group 6 — checkRelayers
|
||||
RUN_TEST(test_checkRelayers_both_found);
|
||||
RUN_TEST(test_checkRelayers_one_found);
|
||||
RUN_TEST(test_checkRelayers_r2WasSole);
|
||||
|
||||
// Group 7 — Merge Logic
|
||||
RUN_TEST(test_merge_preserves_original_next_hop);
|
||||
RUN_TEST(test_merge_preserves_highest_hop_limit);
|
||||
RUN_TEST(test_merge_no_duplicate_relayers);
|
||||
RUN_TEST(test_merge_adds_new_relayer);
|
||||
RUN_TEST(test_merge_we_relay_sets_slot_zero);
|
||||
RUN_TEST(test_merge_heard_back_stores_relay_node);
|
||||
|
||||
// Group 8 — Fallback-to-Flooding Detection
|
||||
RUN_TEST(test_fallback_detected);
|
||||
RUN_TEST(test_fallback_not_when_we_relayed);
|
||||
RUN_TEST(test_fallback_not_on_first_observation);
|
||||
|
||||
// Group 9 — Next-Hop and Upgrade Detection
|
||||
RUN_TEST(test_weWereNextHop_true);
|
||||
RUN_TEST(test_weWereNextHop_false);
|
||||
RUN_TEST(test_wasUpgraded_true);
|
||||
RUN_TEST(test_wasUpgraded_false);
|
||||
|
||||
// Group 10 — Edge Cases
|
||||
RUN_TEST(test_packet_id_zero_not_stored);
|
||||
RUN_TEST(test_sender_zero_substituted);
|
||||
RUN_TEST(test_uninitialized_wasSeenRecently);
|
||||
RUN_TEST(test_uninitialized_wasRelayer);
|
||||
RUN_TEST(test_multiple_instances_independent);
|
||||
|
||||
// Group 11 — Hash Table Stress
|
||||
RUN_TEST(test_many_packets_no_false_negatives);
|
||||
RUN_TEST(test_many_packets_no_false_positives);
|
||||
RUN_TEST(test_churn_correctness);
|
||||
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -19,7 +19,7 @@ build_flags =
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
|
||||
lewisxhe/SensorLib@0.3.4
|
||||
lib_ignore =
|
||||
|
||||
@@ -11,7 +11,10 @@ upload_speed = 921600
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
build_unflags =
|
||||
${esp32s3_base.build_unflags}
|
||||
-DARDUINO_USB_MODE=1
|
||||
build_flags =
|
||||
${esp32s3_base.build_flags}
|
||||
-D PRIVATE_HW
|
||||
|
||||
@@ -23,4 +23,4 @@ build_flags = ${esp32s3_base.build_flags}
|
||||
|
||||
lib_deps = ${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@ build_flags =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
# renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main
|
||||
https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip
|
||||
# renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328
|
||||
|
||||
@@ -34,7 +34,7 @@ build_flags =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
# renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main
|
||||
https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip
|
||||
# renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
|
||||
#define BUTTON_PIN 0 // only for Plus version
|
||||
|
||||
#define PMU_IRQ 21 // Interrupt pin for the PMU
|
||||
|
||||
#define USE_SX1262
|
||||
#define USE_SX1268
|
||||
|
||||
|
||||
@@ -1,6 +1,130 @@
|
||||
#include "variant.h"
|
||||
#include "Arduino.h"
|
||||
#include "pins_arduino.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef T5_S3_EPAPER_PRO
|
||||
|
||||
#include "Observer.h"
|
||||
#include "TouchDrvGT911.hpp"
|
||||
#include "Wire.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
// Bridges touch events from TouchScreenImpl1 directly into InkHUD,
|
||||
// bypassing the InputBroker (which is excluded in InkHUD builds).
|
||||
// Routing mirrors the mini-epaper-s3 two-way rocker pattern:
|
||||
// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system applet has focus (e.g. menu)
|
||||
// - Nav up/down: navUp/navDown always (menu scroll)
|
||||
// - Tap: shortpress (cycle applets / confirm in menu)
|
||||
// - Long press: longpress (open menu / back)
|
||||
class TouchInkHUDBridge : public Observer<const InputEvent *>
|
||||
{
|
||||
int onNotify(const InputEvent *e) override
|
||||
{
|
||||
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
||||
|
||||
// Keep alignment in sync with the current rotation so that visual-frame gestures
|
||||
// always pass through nav functions without remapping: (rotation + alignment) % 4 == 0.
|
||||
inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4;
|
||||
|
||||
// Check whether a system applet (e.g. menu) is currently handling input
|
||||
bool systemHandlingInput = false;
|
||||
for (NicheGraphics::InkHUD::SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
systemHandlingInput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (e->inputEvent) {
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
inkhud->shortpress();
|
||||
break;
|
||||
case INPUT_BROKER_SELECT:
|
||||
inkhud->longpress();
|
||||
break;
|
||||
case INPUT_BROKER_LEFT:
|
||||
if (systemHandlingInput)
|
||||
inkhud->navUp();
|
||||
else
|
||||
inkhud->prevApplet();
|
||||
break;
|
||||
case INPUT_BROKER_RIGHT:
|
||||
if (systemHandlingInput)
|
||||
inkhud->navDown();
|
||||
else
|
||||
inkhud->nextApplet();
|
||||
break;
|
||||
case INPUT_BROKER_UP:
|
||||
inkhud->navUp();
|
||||
break;
|
||||
case INPUT_BROKER_DOWN:
|
||||
inkhud->navDown();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
static TouchInkHUDBridge touchBridge;
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
TouchDrvGT911 touch;
|
||||
|
||||
// Commands the GT911 into standby before the Wire bus is torn down.
|
||||
// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still available here.
|
||||
struct TouchDeepSleepObserver {
|
||||
int onDeepSleep(void *)
|
||||
{
|
||||
touch.sleep();
|
||||
return 0;
|
||||
}
|
||||
CallbackObserver<TouchDeepSleepObserver, void *> observer{this, &TouchDeepSleepObserver::onDeepSleep};
|
||||
} static touchDeepSleepObserver;
|
||||
|
||||
bool readTouch(int16_t *x, int16_t *y)
|
||||
{
|
||||
if (!digitalRead(GT911_PIN_INT)) {
|
||||
int16_t raw_x;
|
||||
int16_t raw_y;
|
||||
if (touch.getPoint(&raw_x, &raw_y)) {
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// Transform raw GT911 axes to visual-frame coordinates for the current display rotation.
|
||||
// rotation=3 is the physical identity (device's default orientation).
|
||||
switch (NicheGraphics::InkHUD::InkHUD::getInstance()->persistence->settings.rotation) {
|
||||
default:
|
||||
case 3:
|
||||
*x = raw_x;
|
||||
*y = raw_y;
|
||||
break; // identity
|
||||
case 2:
|
||||
*x = (EPD_WIDTH - 1) - raw_y;
|
||||
*y = raw_x;
|
||||
break; // 90° CW tilt
|
||||
case 1:
|
||||
*x = (EPD_HEIGHT - 1) - raw_x;
|
||||
*y = (EPD_WIDTH - 1) - raw_y;
|
||||
break; // 180° flip
|
||||
case 0:
|
||||
*x = raw_y;
|
||||
*y = (EPD_HEIGHT - 1) - raw_x;
|
||||
break; // 90° CCW tilt
|
||||
}
|
||||
#else
|
||||
*x = raw_x;
|
||||
*y = raw_y;
|
||||
#endif
|
||||
LOG_DEBUG("touched(%d/%d)", *x, *y);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void earlyInitVariant()
|
||||
{
|
||||
@@ -37,3 +161,19 @@ void variant_shutdown()
|
||||
// Ensure frontlight is off during deep sleep
|
||||
digitalWrite(BOARD_BL_EN, LOW);
|
||||
}
|
||||
|
||||
void lateInitVariant()
|
||||
{
|
||||
touch.setPins(GT911_PIN_RST, GT911_PIN_INT);
|
||||
if (touch.begin(Wire, GT911_SLAVE_ADDRESS_H, GT911_PIN_SDA, GT911_PIN_SCL)) {
|
||||
touchDeepSleepObserver.observer.observe(¬ifyDeepSleep);
|
||||
touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch);
|
||||
touchScreenImpl1->init();
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
touchBridge.observe(touchScreenImpl1);
|
||||
#endif
|
||||
} else {
|
||||
LOG_ERROR("Failed to find touch controller!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
@@ -76,4 +74,4 @@
|
||||
// has 32768 Hz crystal
|
||||
#define HAS_32768HZ 1
|
||||
|
||||
#define USE_SH1106
|
||||
#define USE_SH1106
|
||||
|
||||
@@ -12,5 +12,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
debug_tool = jlink
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS0
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
|
||||
upload_protocol = nrfutil
|
||||
;upload_port = /dev/ttyACM1
|
||||
|
||||
@@ -15,6 +15,6 @@ lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
|
||||
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
debug_tool = jlink
|
||||
;upload_port = /dev/ttyACM4
|
||||
@@ -9,5 +9,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mes
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
debug_tool = jlink
|
||||
|
||||
@@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
# renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028
|
||||
melopero/Melopero RV3028@1.2.0
|
||||
# renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library
|
||||
|
||||
@@ -18,7 +18,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
|
||||
zinggjm/GxEPD2@1.6.8
|
||||
zinggjm/GxEPD2@1.6.9
|
||||
# renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028
|
||||
melopero/Melopero RV3028@1.2.0
|
||||
# renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library
|
||||
|
||||
Reference in New Issue
Block a user