Compare commits

..
Author SHA1 Message Date
Austin Lane 36af332622 Add libnotify to setup-native (GitHub Actions) 2026-04-20 19:13:41 -04:00
Austin Lane eb9fa6f4fe Notifications: Use libnotify
Use libnotify for meshtasticd desktop notifications.

Add "HAS_LIBNOTIFY" macro to guard for builds where libnotify is expected.
This is not included in buildroot/openwrt builds. (no libnotify except when extra repos are added).

Install desktop icon in the correct location on Debian and Fedora packages.
Update dependencies in packaging and dockerfiles.
2026-04-20 19:03:19 -04:00
Jonathan Bennett c06e152e59 Add files via upload 2026-04-20 13:46:17 -05:00
Jonathan Bennett 8b35fefdc7 Merge branch 'develop' into meshtasticd-notifications 2026-04-20 12:56:56 -05:00
Jonathan Bennett d15666611b First attempt at sending message notifications on Linux 2026-04-20 12:55:31 -05:00
72 changed files with 521 additions and 5863 deletions
+1
View File
@@ -34,6 +34,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
libxcb-xkb-dev \
libxkbcommon-dev \
libinput-dev \
libnotify-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN pipx install platformio
+4 -4
View File
@@ -75,11 +75,11 @@ body:
- type: checkboxes
id: mui
attributes:
label: Is this bug report about any UI (https://meshtastic.org/docs/configuration/device-uis/) component firmware?
label: Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?
options:
- label: Meshtastic UI aka MUI
- label: InkHUD
- label: BaseUI
- label: Meshtastic UI aka MUI colorTFT
- label: InkHUD ePaper
- label: OLED slide UI on any display
- type: input
id: version
+1 -1
View File
@@ -11,4 +11,4 @@ runs:
- name: Install libs needed for native build
shell: bash
run: |
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev libnotify-dev
+1 -7
View File
@@ -86,13 +86,7 @@ jobs:
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
- name: PlatformIO Tests
run: |
set -o pipefail
# Filter out SKIPPED summary rows for hardware variants that can't run on the
# native host. They flood the log and make it harder to spot real failures.
# The JUnit XML is written directly to testreport.xml before the pipe, so
# the test artifact is unaffected.
platformio test -e coverage -v --junit-output-path testreport.xml 2>&1 | grep -v "[[:space:]]SKIPPED$"
run: platformio test -e coverage -v --junit-output-path testreport.xml
- name: Save test results
if: always() # run this step even if previous step failed
+3 -3
View File
@@ -9,9 +9,9 @@ plugins:
lint:
enabled:
- checkov@3.2.524
- renovate@43.139.6
- renovate@43.132.1
- prettier@3.8.3
- trufflehog@3.95.2
- trufflehog@3.94.3
- yamllint@1.38.0
- bandit@1.9.4
- trivy@0.70.0
@@ -19,7 +19,7 @@ lint:
- ruff@0.15.11
- isort@8.0.1
- markdownlint@0.48.0
- oxipng@10.1.1
- oxipng@10.1.0
- svgo@4.0.1
- actionlint@1.7.12
- flake8@7.3.0
+2 -2
View File
@@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
curl wget g++ zip git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev libnotify-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware
@@ -53,7 +53,7 @@ USER root
RUN apt-get update && apt-get --no-install-recommends -y install \
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
liborcania2.3 libulfius2.7t64 libssl3t64 \
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 libnotify4 \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \
+1 -1
View File
@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
g++ git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev libnotify-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir platformio==6.1.19 \
&& useradd --create-home --shell /usr/sbin/nologin meshtastic
+2 -2
View File
@@ -11,7 +11,7 @@ RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev libnotify-dev \
&& rm -rf /var/cache/apk/* \
&& pip install --no-cache-dir -U platformio \
&& mkdir /tmp/firmware
@@ -42,7 +42,7 @@ USER root
RUN apk --no-cache add \
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
i2c-tools libuv libx11 libinput libxkbcommon sdl2 libnotify \
&& rm -rf /var/cache/apk/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d \
+2 -1
View File
@@ -27,7 +27,8 @@ Build-Depends: debhelper-compat (= 13),
libinput-dev,
libxkbcommon-x11-dev,
libsqlite3-dev,
libsdl2-dev
libsdl2-dev,
libnotify-dev
Standards-Version: 4.6.2
Homepage: https://github.com/meshtastic/firmware
Rules-Requires-Root: no
+2
View File
@@ -7,3 +7,5 @@ bin/meshtasticd.service lib/systemd/system
bin/meshtasticd-start.sh usr/bin
web/* usr/share/meshtasticd/web
bin/org.meshtastic.meshtasticd.svg usr/share/icons/hicolor/scalable/apps
+6
View File
@@ -50,6 +50,7 @@ BuildRequires: pkgconfig(x11)
BuildRequires: pkgconfig(libinput)
BuildRequires: pkgconfig(xkbcommon-x11)
BuildRequires: pkgconfig(sdl2)
BuildRequires: pkgconfig(libnotify)
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
@@ -112,6 +113,10 @@ cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web
# Install default SSL storage directory (for web)
mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl
# Install desktop icon
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
install -m 0644 bin/org.meshtastic.meshtasticd.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/org.meshtastic.meshtasticd.svg
%pre
# create spi group (for udev rules)
getent group spi > /dev/null || groupadd -r spi
@@ -171,6 +176,7 @@ fi
%dir %{_datadir}/meshtasticd/web
%{_datadir}/meshtasticd/web/*
%dir %{_sysconfdir}/meshtasticd/ssl
%{_datadir}/icons/hicolor/scalable/apps/org.meshtastic.meshtasticd.svg
%changelog
%autochangelog
+67 -133
View File
@@ -94,31 +94,16 @@ static const adc_atten_t atten = ADC_ATTENUATION;
#endif
#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
#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
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
#define EXT_CHRG_DETECT_VALUE HIGH
static const uint8_t ext_chrg_detect_value = HIGH;
#else
static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE;
#endif
#endif
@@ -484,14 +469,28 @@ class AnalogBatteryLevel : public HasBatteryLevel
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
#endif
// 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.
/// 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
virtual bool isVbusIn() override
{
#ifdef EXT_PWR_DETECT
return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE;
#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;
// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if
@@ -512,9 +511,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
@@ -647,10 +646,14 @@ Power::Power() : OSThread("Power")
bool Power::analogInit()
{
#ifdef EXT_PWR_DETECT
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
pinMode(EXT_PWR_DETECT, INPUT_PULLUP);
#else
pinMode(EXT_PWR_DETECT, INPUT);
#endif
#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
@@ -743,17 +746,37 @@ bool Power::setup()
found = true;
#endif
}
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
enabled = found;
low_voltage_counter = 0;
#ifdef ARCH_ESP32
// Register callbacks for before and after lightsleep
// Used to detach and reattach interrupts
lsObserver.observe(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
#endif
return found;
}
@@ -1032,97 +1055,6 @@ 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
*
@@ -1400,6 +1332,8 @@ 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
@@ -1872,7 +1806,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;
@@ -1881,7 +1815,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
@@ -1903,10 +1837,10 @@ SerialBatteryLevel serialBatteryLevel;
bool Power::serialBatteryInit()
{
#ifdef EXT_PWR_DETECT
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
pinMode(EXT_PWR_DETECT, INPUT);
#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();
+2 -2
View File
@@ -19,8 +19,8 @@
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel.
TX_LOG + RX_ALL_LOG = Total air time for a particular meshtastic channel, including
other lora radios.
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including
other lora radios.
RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel.
*/
-1
View File
@@ -499,7 +499,6 @@ 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
+18 -28
View File
@@ -275,12 +275,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
constexpr int kBarCount = 4;
constexpr int kBarWidth = 2;
constexpr int kBarGap = 1;
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset;
int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth;
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
char nodeName[96];
@@ -307,35 +304,28 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
}
}
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
// Draw signal strength bars
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
int barWidth = 2;
int barStartX = x + barsXOffset;
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
// Show signal only for direct neighbors (0 hops)
if (isZeroHop) {
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
int barStartX = x + barsXOffset;
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
for (int b = 0; b < kBarCount; b++) {
if (b < bars) {
int height = (b * 2);
display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height);
}
for (int b = 0; b < 4; b++) {
if (b < bars) {
int height = (b * 2);
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
}
}
// Draw hop count + hop icon
if (node->has_hops_away && node->hops_away > 0) {
char hopCount[6];
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
// Draw hop count
char hopStr[6] = "";
if (node->has_hops_away && node->hops_away > 0)
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
const int hopCountWidth = display->getStringWidth(hopCount);
const int gap = 1;
const int totalWidth = hopCountWidth + gap + hop_width;
const int hopX = barsRightEdge - totalWidth;
const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2;
display->drawString(hopX, y, hopCount);
display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop);
if (hopStr[0] != '\0') {
int rightEdge = x + columnWidth - hopOffset;
int textWidth = display->getStringWidth(hopStr);
display->drawString(rightEdge - textWidth, y, hopStr);
}
}
+74 -42
View File
@@ -405,10 +405,10 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
}
#endif
// === 2. Signal/Hops line (if available) ===
// === 2. Signal and Hops (combined on one line, if available) ===
char signalHopsStr[32] = "";
bool haveSignal = false;
int bars = 0;
const char *qualityLabel = nullptr;
// Helper to get SNR limit based on modem preset
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
@@ -429,52 +429,81 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
}
};
// Calculate signal grade using modem preset and SNR only
float snrLimit = getSnrLimit(config.lora.modem_preset);
float snr = node->snr;
// Determine signal quality label and bars using SNR-only grading
const char *qualityLabel = nullptr;
if (snr > snrLimit + 10) {
qualityLabel = "Good";
bars = 4;
} else if (snr > snrLimit + 6) {
qualityLabel = "Good";
bars = 3;
} else if (snr > snrLimit + 2) {
qualityLabel = "Good";
bars = 2;
} else if (snr > snrLimit - 4) {
qualityLabel = "Fair";
bars = 1;
} else {
qualityLabel = "Bad";
bars = 1;
}
// Add extra spacing on the left if we have an API connection to account for the common footer icons
const char *leftSideSpacing =
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
// Signal text/bars are only for direct (zero-hop) nodes with valid SNR.
if (isZeroHop) {
float snr = node->snr;
if (snr > -100 && snr != 0) {
float snrLimit = getSnrLimit(config.lora.modem_preset);
// Determine signal quality label and bars using SNR-only grading.
if (snr > snrLimit + 10) {
qualityLabel = "Good";
bars = 4;
} else if (snr > snrLimit + 6) {
qualityLabel = "Good";
bars = 3;
} else if (snr > snrLimit + 2) {
qualityLabel = "Good";
bars = 2;
} else if (snr > snrLimit - 4) {
qualityLabel = "Fair";
bars = 1;
} else {
qualityLabel = "Bad";
bars = 1;
}
// --- Build the Signal/Hops line ---
// Only show signal if we have valid SNR
if (snr > -100 && snr != 0) {
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
haveSignal = true;
}
haveSignal = true;
if (node->hops_away > 0) {
size_t len = strlen(signalHopsStr);
if (haveSignal) {
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
} else {
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
}
}
const bool showHops = node->has_hops_away && node->hops_away > 0;
if (haveSignal || showHops) {
if (signalHopsStr[0]) {
int yPos = getTextPositions(display)[line++];
int curX = x + display->getStringWidth(leftSideSpacing);
int curX = x;
// Draw signal quality text for zero-hop nodes when present.
if (haveSignal && qualityLabel) {
char signalLabel[20];
snprintf(signalLabel, sizeof(signalLabel), "Sig:%s", qualityLabel);
display->drawString(curX, yPos, signalLabel);
curX += display->getStringWidth(signalLabel) + 4;
// Split combined string into signal text and hop suffix
char sigPart[20] = "";
const char *hopPart = nullptr;
char *bracket = strchr(signalHopsStr, '[');
if (bracket) {
size_t n = (size_t)(bracket - signalHopsStr);
if (n >= sizeof(sigPart))
n = sizeof(sigPart) - 1;
memcpy(sigPart, signalHopsStr, n);
sigPart[n] = '\0';
// Trim trailing spaces
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
sigPart[strlen(sigPart) - 1] = '\0';
}
hopPart = bracket; // "[n Hop(s)]"
} else {
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
sigPart[sizeof(sigPart) - 1] = '\0';
}
// Draw signal quality text
display->drawString(curX, yPos, sigPart);
curX += display->getStringWidth(sigPart) + 4;
// Draw signal bars (skip on UltraLow, text only)
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
const int kMaxBars = 4;
@@ -512,12 +541,12 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
}
// Draw hops for non-zero-hop nodes as: number + hop icon.
// This path is mutually exclusive with the zero-hop signal-bars path above.
if (showHops) {
// hop label
display->drawString(curX, yPos, "Hop:");
curX += display->getStringWidth("Hop:") + 2;
// Draw hops AFTER the bars as: [ number + hop icon ]
if (hopPart && node->hops_away > 0) {
// open bracket
display->drawString(curX, yPos, "[");
curX += display->getStringWidth("[") + 1;
// hop count
char hopCount[6];
@@ -529,6 +558,9 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
curX += hop_width + 1;
// closing bracket
display->drawString(curX, yPos, "]");
}
}
@@ -1,122 +0,0 @@
/*
NicheGraphics parallel E-Ink driver for the LilyGo T5-S3-ePaper-Pro (ED047TC1).
InkHUD buffer format : 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
960×540 FastEPD buffer so content clears the panel's inactive edge border.
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
*/
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
// #define EINK_EDGE_LINES
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#ifdef T5_S3_EPAPER_PRO
#include "./ED047TC1.h"
#include "FastEPD.h"
#include "configuration.h"
using namespace NicheGraphics::Drivers;
void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
{
// Parallel display — SPI parameters are not used
(void)spi;
(void)pin_dc;
(void)pin_cs;
(void)pin_busy;
(void)pin_rst;
epaper = new FASTEPD;
#if defined(T5_S3_EPAPER_PRO_V1)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
#elif defined(T5_S3_EPAPER_PRO_V2)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
// Initialize all PCA9535 port-0 pins as outputs / HIGH
for (int i = 0; i < 8; i++) {
epaper->ioPinMode(i, OUTPUT);
epaper->ioWrite(i, HIGH);
}
#else
#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
#endif
epaper->setMode(BB_MODE_1BPP);
epaper->clearWhite();
epaper->fullUpdate(true); // Blocking initial clear
}
void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
{
if (!epaper)
return;
// InkHUD renders into a DISPLAY_WIDTH × DISPLAY_HEIGHT safe-area buffer.
// We need to place that into the centre of the physical 960×540 FastEPD buffer,
// leaving blank margins at every edge to avoid the panel's inactive border.
const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (118)
const uint32_t dstRowBytes = (960 + 7) / 8; // bytes per row in physical buffer (120)
const uint32_t dstTotalRows = 540;
uint8_t *cur = epaper->currentBuffer();
// Fill physical buffer with white (0xFF = white in FastEPD 1bpp)
memset(cur, 0xFF, dstRowBytes * dstTotalRows);
// Copy each InkHUD row into the physical buffer with horizontal + vertical offsets
for (uint32_t row = 0; row < DISPLAY_HEIGHT; row++) {
const uint8_t *srcRow = imageData + row * srcRowBytes;
uint8_t *dstRow = cur + (row + V_OFFSET_TOP) * dstRowBytes + H_OFFSET_BYTES;
memcpy(dstRow, srcRow, srcRowBytes);
}
#ifdef EINK_EDGE_LINES
// Draw a 1px black box at the exact boundary of the safe area within the
// physical buffer. If the margins are correct, all 4 lines should be
// fully visible and right at the edge of the usable display area.
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
const uint32_t safeX = H_OFFSET_BYTES * 8;
const uint32_t safeY = V_OFFSET_TOP;
const uint32_t safeW = DISPLAY_WIDTH;
const uint32_t safeH = DISPLAY_HEIGHT;
// Top edge: horizontal line at safeY
for (uint32_t col = safeX; col < safeX + safeW; col++)
setPixelBlack(col, safeY);
// Bottom edge: horizontal line at safeY + safeH - 1
for (uint32_t col = safeX; col < safeX + safeW; col++)
setPixelBlack(col, safeY + safeH - 1);
// Left edge: vertical line at safeX
for (uint32_t row = safeY; row < safeY + safeH; row++)
setPixelBlack(safeX, row);
// Right edge: vertical line at safeX + safeW - 1
for (uint32_t row = safeY; row < safeY + safeH; row++)
setPixelBlack(safeX + safeW - 1, row);
#endif
if (type == FULL) {
epaper->fullUpdate(CLEAR_SLOW, false);
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
} else {
// FAST: true partial update — compares pCurrent vs pPrevious and only applies
// the update waveform to rows that actually changed. Unchanged rows get a neutral
// signal (no visible effect). partialUpdate() updates pPrevious internally.
epaper->partialUpdate(false, 0, dstTotalRows - 1);
}
}
#endif // T5_S3_EPAPER_PRO
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
@@ -1,90 +0,0 @@
/*
E-Ink display driver adapter
- ED047TC1 (via FastEPD library)
- Manufacturer: E Ink / used in LilyGo T5-E-Paper-S3-Pro
- Size: 4.7 inch
- Physical resolution: 960px x 540px
- Interface: 8-bit parallel (NOT SPI)
Unlike the other NicheGraphics EInk drivers, this one drives a parallel e-paper
panel via the FastEPD library. SPI parameters passed to begin() are ignored.
The ED047TC1 panel has an inactive pixel border on all four edges (~4–8 physical
pixels). DISPLAY_WIDTH / DISPLAY_HEIGHT expose a reduced "safe area" to InkHUD so
that content is never drawn into this dead zone. The update() method copies the
InkHUD frame buffer into the centre of the larger physical 960×540 buffer, using
H_OFFSET_BYTES (horizontal, whole bytes = 8 pixels per byte),
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
Changing these constants shifts content inward from each physical edge:
H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944)
V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom)
V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523)
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./EInk.h"
// Forward declare to avoid pulling FastEPD into all translation units
class FASTEPD;
namespace NicheGraphics::Drivers
{
class ED047TC1 : public EInk
{
// Safe-area dimensions exposed to InkHUD (physical panel is 960×540).
//
// The ED047TC1 has an inactive pixel border on all physical edges.
// The physical buffer coordinates do NOT directly match the visual orientation
// due to FastEPD's portrait scan direction and InkHUD's rotation=3 (270° CW):
//
// Physical buffer Visual on device (rotation=3)
// ───────────────── ──────────────────────────────
// Physical LEFT cols → Visual TOP edge
// Physical RIGHT cols → Visual BOTTOM edge
// Physical TOP rows → Visual RIGHT edge
// Physical BOTTOM rows → Visual LEFT edge
//
// Offset constants shift the InkHUD safe-area away from each physical dead zone:
// H_OFFSET_BYTES : whole bytes from physical left (8px per byte, affects visual TOP)
// Physical right margin = 960 − H_OFFSET_BYTES×8 − DISPLAY_WIDTH (affects visual BOTTOM)
// V_OFFSET_TOP : pixel rows from physical top (affects visual RIGHT)
// V_OFFSET_BOTTOM: pixel rows from physical bottom (affects visual LEFT)
//
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px)
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px)
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
// visual BOTTOM: 960−8−944=8px physical right margin
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
public:
ED047TC1() : EInk(DISPLAY_WIDTH, DISPLAY_HEIGHT, supported) {}
// EInk interface — SPI params are not used for this parallel display
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override;
void update(uint8_t *imageData, UpdateTypes type) override;
protected:
bool isUpdateDone() override { return true; } // FastEPD updates are blocking
private:
FASTEPD *epaper = nullptr;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-4
View File
@@ -88,12 +88,8 @@ class AppletFont
// Greek
#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans18pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans24pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h"
#define FREESANS_24PT_WIN1253 InkHUD::AppletFont(FreeSans24pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -5, 3)
#define FREESANS_18PT_WIN1253 InkHUD::AppletFont(FreeSans18pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -4, 2)
#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1)
#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1)
#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2)
+2 -8
View File
@@ -143,10 +143,6 @@ int32_t ButtonThread::runOnce()
leadUpSequenceActive = false;
resetLeadUpSequence();
}
#ifdef INPUT_DEBUG
if (buttonCurrentlyPressed)
LOG_WARN("Button held for %u ms", millis() - buttonPressStartTime);
#endif
// Progressive lead-up sound system
if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) {
@@ -315,8 +311,7 @@ int32_t ButtonThread::runOnce()
void ButtonThread::attachButtonInterrupts()
{
// Interrupt for user button, during normal use. Improves responsiveness.
if (_intRoutine != nullptr)
attachInterrupt(_pinNum, _intRoutine, CHANGE);
attachInterrupt(_pinNum, _intRoutine, CHANGE);
}
/*
@@ -325,8 +320,7 @@ void ButtonThread::attachButtonInterrupts()
*/
void ButtonThread::detachButtonInterrupts()
{
if (_intRoutine != nullptr)
detachInterrupt(_pinNum);
detachInterrupt(_pinNum);
}
#ifdef ARCH_ESP32
+1 -2
View File
@@ -29,8 +29,7 @@ void TouchScreenImpl1::init()
return;
#else
TouchScreenBase::init(true);
if (inputBroker)
inputBroker->registerSource(this);
inputBroker->registerSource(this);
#endif
}
+129
View File
@@ -340,9 +340,138 @@ 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 =
+2 -5
View File
@@ -101,12 +101,9 @@ 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
// Single lookup for both relayer checks on the same (request_id, to) pair
bool wasAlreadyRelayer = false;
bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to);
bool weWereSoleRelayer = false;
bool weWereRelayer = false;
checkRelayers(p->relay_node, ourRelayID, p->decoded.request_id, p->to, &wasAlreadyRelayer, &weWereRelayer,
&weWereSoleRelayer);
bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &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,
+4
View File
@@ -863,6 +863,10 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
#endif
#endif
#if ARCH_PORTDUINO
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.alert_message = true;
#endif
#ifdef NANO_G2_ULTRA
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.alert_message = true;
+14 -170
View File
@@ -1,7 +1,6 @@
#include "PacketHistory.h"
#include "configuration.h"
#include "mesh-pb-constants.h"
#include "meshUtils.h"
#ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
@@ -24,14 +23,6 @@ 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];
@@ -44,20 +35,6 @@ 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()
@@ -65,12 +42,6 @@ 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 */
@@ -223,78 +194,7 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
return seenRecently;
}
#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.
/** Find a packet record in history.
* @return pointer to PacketRecord if found, NULL if not found */
PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
{
@@ -305,40 +205,23 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
return NULL;
}
#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",
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
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) {
PacketRecord *it = NULL;
for (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 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);
#endif
// only the first match is returned, so be careful not to create duplicate entries
return it; // Return pointer to the found record
}
}
return NULL;
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id);
#endif
return NULL; // Not found
}
/** Insert/Replace oldest PacketRecord in recentPackets. */
@@ -444,22 +327,8 @@ 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],
@@ -527,31 +396,6 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, boo
return found;
}
// Check two relayers against the same packet record with a single find() call,
// avoiding redundant O(N) lookups when both are checked for the same (id, sender) pair.
void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
bool *r2WasSole)
{
*r1Result = false;
*r2Result = false;
if (r2WasSole)
*r2WasSole = false;
if (!initOk()) {
LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED!");
return;
}
const PacketRecord *found = find(sender, id);
if (!found)
return;
if (relayer1 != 0)
*r1Result = wasRelayer(relayer1, *found);
if (relayer2 != 0)
*r2Result = wasRelayer(relayer2, *found, r2WasSole);
}
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
{
-26
View File
@@ -28,22 +28,6 @@ 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
@@ -86,16 +70,6 @@ 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);
+1 -6
View File
@@ -470,13 +470,8 @@ 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_DEBUG("Unhandled module config type %d", config_state);
LOG_ERROR("Unknown module config type %d", config_state);
}
config_state++;
-10
View File
@@ -46,16 +46,6 @@ 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()
-7
View File
@@ -136,13 +136,6 @@ 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;
/**
+2 -2
View File
@@ -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_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_DEBUG("Invalid portnum (bad psk?)");
LOG_ERROR("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");
-9
View File
@@ -455,15 +455,6 @@ template <typename T> void SX126xInterface<T>::resetAGC()
// RX boosted gain mode
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
// Re-apply the undocumented 0x8B5 RX sensitivity patch that was set in init().
// The CALIBRATE_ALL (0x7F) command above clears bit 0 of register 0x8B5, which
// silently removes the RX sensitivity improvement introduced in #9571 / #9777.
// Without this re-apply, every SX1262 node loses its RX boost ~60s after boot
// and never recovers until reboot. See empirical evidence in the PR description.
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) {
LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch");
}
// 6. Resume receiving
startReceive();
}
-4
View File
@@ -81,9 +81,7 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
lite.hw_model = user.hw_model;
lite.role = user.role;
lite.is_licensed = user.is_licensed;
@@ -101,9 +99,7 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
user.long_name[sizeof(user.long_name) - 1] = '\0';
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
user.short_name[sizeof(user.short_name) - 1] = '\0';
user.hw_model = lite.hw_model;
user.role = lite.role;
user.is_licensed = lite.is_licensed;
+3 -7
View File
@@ -285,11 +285,7 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
/* Nepal 865MHz */
meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25,
/* Brazil 902MHz */
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26,
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26
} meshtastic_Config_LoRaConfig_RegionCode;
/* Standard predefined channel settings
@@ -706,8 +702,8 @@ extern "C" {
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M+1))
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_BR_902
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
-18
View File
@@ -11,24 +11,6 @@ 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) { \
@@ -24,6 +24,10 @@
#include "mesh/generated/meshtastic/rtttl.pb.h"
#include <Arduino.h>
#if HAS_LIBNOTIFY
#include <libnotify/notify.h>
#endif
#if defined(HAS_RGB_LED)
#include "AmbientLightingThread.h"
uint8_t red = 0;
@@ -408,6 +412,9 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
if (genericShouldAlert) {
LOG_INFO("externalNotificationModule - Generic alert");
setExternalState(0, true);
#if HAS_LIBNOTIFY
portduinoNotify(mp);
#endif
}
if (vibraShouldAlert) {
@@ -521,3 +528,40 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event)
}
return 0;
}
#if HAS_LIBNOTIFY
void ExternalNotificationModule::portduinoNotify(const meshtastic_MeshPacket &mp)
{
std::string senderName;
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
if (sender && sender->has_user) {
if (sender->user.long_name[0] != '\0') {
senderName = sender->user.long_name;
} else {
senderName = sender->user.short_name;
}
} else {
senderName = std::to_string(mp.from);
}
std::string notificationSummary = "From: " + senderName;
std::string notificationBody = std::string((char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
if (!notify_is_initted()) {
if (!notify_init("Meshtasticd")) {
LOG_WARN("Failed to initialize libnotify");
return;
}
}
NotifyNotification *notification =
notify_notification_new(notificationSummary.c_str(), notificationBody.c_str(), "org.meshtastic.meshtasticd");
if (notification) {
GError *error = nullptr;
if (!notify_notification_show(notification, &error)) {
LOG_WARN("Failed to show notification: %s", error ? error->message : "unknown error");
if (error)
g_error_free(error);
}
g_object_unref(G_OBJECT(notification));
}
}
#endif
+4
View File
@@ -77,6 +77,10 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency:
virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
#if HAS_LIBNOTIFY
void portduinoNotify(const meshtastic_MeshPacket &mp);
#endif
};
extern ExternalNotificationModule *externalNotificationModule;
+6 -15
View File
@@ -492,24 +492,15 @@ void PositionModule::sendLostAndFoundText()
{
meshtastic_MeshPacket *p = allocDataPacket();
p->to = NODENUM_BROADCAST;
char message[128];
int written = snprintf(message, sizeof(message), "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7),
(lastGpsLongitude * 1e-7));
char *message = new char[60];
sprintf(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;
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);
}
}
p->decoded.payload.size = strlen(message);
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
service->sendToMesh(p, RX_SRC_LOCAL, true);
delete[] message;
}
// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision
@@ -589,4 +580,4 @@ void PositionModule::handleNewPosition()
}
}
#endif
#endif
+1 -1
View File
@@ -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, p.payload.size);
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
this->packetHistoryTotalCount++;
}
+20 -13
View File
@@ -323,7 +323,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) override
virtual void onNowHasData(uint32_t fromRadioNum)
{
PhoneAPI::onNowHasData(fromRadioNum);
@@ -350,7 +350,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() override { return bleServer && bleServer->getConnectedCount() > 0; }
virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; }
void requestHighThroughputConnection(uint16_t conn_handle)
{
@@ -412,9 +412,9 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
{
#ifdef NIMBLE_TWO
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
#else
virtual void onWrite(NimBLECharacteristic *pCharacteristic) override
virtual void onWrite(NimBLECharacteristic *pCharacteristic)
#endif
{
@@ -464,9 +464,9 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
{
#ifdef NIMBLE_TWO
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
#else
virtual void onRead(NimBLECharacteristic *pCharacteristic) override
virtual void onRead(NimBLECharacteristic *pCharacteristic)
#endif
{
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
@@ -582,9 +582,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
private:
NimbleBluetooth *ble;
virtual uint32_t onPassKeyDisplay() override
virtual uint32_t onPassKeyDisplay()
#else
virtual uint32_t onPassKeyRequest() override
virtual uint32_t onPassKeyRequest()
#endif
{
uint32_t passkey = config.bluetooth.fixed_pin;
@@ -635,9 +635,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
}
#ifdef NIMBLE_TWO
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) override
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo)
#else
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) override
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc)
#endif
{
LOG_INFO("BLE authentication complete");
@@ -655,7 +655,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
}
#ifdef NIMBLE_TWO
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
{
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
@@ -683,11 +683,11 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
#endif
#ifdef NIMBLE_TWO
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
{
LOG_INFO("BLE disconnect reason: %d", reason);
#else
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) override
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
{
LOG_INFO("BLE disconnect");
#endif
@@ -989,4 +989,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
#endif
}
void clearNVS()
{
NimBLEDevice::deleteAllBonds();
#ifdef ARCH_ESP32
ESP.restart();
#endif
}
#endif
+2 -1
View File
@@ -24,4 +24,5 @@ class NimbleBluetooth : BluetoothApi
#endif
};
void setBluetoothEnable(bool enable);
void setBluetoothEnable(bool enable);
void clearNVS();
@@ -1,144 +0,0 @@
#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;
}
// T5-S3-ePaper Pro specific (late-) init
void lateInitVariant(void)
{
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(&notifyDeepSleep);
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
+1 -18
View File
@@ -86,7 +86,7 @@ extern RAK9154Sensor rak9154Sensor;
extern XPowersLibInterface *PMU;
#endif
class Power : public concurrency::OSThread
class Power : private concurrency::OSThread
{
public:
@@ -100,15 +100,6 @@ 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);
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
#endif
void attachPowerInterrupts();
void detachPowerInterrupts();
protected:
meshtastic::PowerStatus *statusHandler;
@@ -134,14 +125,6 @@ class Power : public 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
+6 -5
View File
@@ -497,12 +497,13 @@ 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");
// 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 gpio: btn=%d",
!digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
} else
#endif
{
LOG_INFO("Exit light sleep cause: %d", cause);
}
-834
View File
@@ -1,834 +0,0 @@
/*
* 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.9
zinggjm/GxEPD2@1.6.8
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4
lib_ignore =
@@ -11,7 +11,7 @@ upload_speed = 921600
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.9
zinggjm/GxEPD2@1.6.8
build_unflags =
${esp32s3_base.build_unflags}
-DARDUINO_USB_MODE=1
@@ -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.9
zinggjm/GxEPD2@1.6.8
@@ -1,7 +1,6 @@
#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,5 +1,4 @@
#define EXT_PWR_DETECT 20
#define EXT_PWR_DETECT_MODE INPUT_PULLUP
#define BUTTON_PIN 17
#define BUTTON_ACTIVE_LOW false
@@ -10,6 +10,9 @@ build_flags =
-D M5STACK_CARDPUTER_ADV
-D ARDUINO_USB_CDC_ON_BOOT=1
-I variants/esp32s3/m5stack_cardputer_adv
build_src_filter =
${esp32s3_base.build_src_filter}
+<../variants/esp32s3/m5stack_cardputer_adv>
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
@@ -1,8 +1,5 @@
#include "configuration.h"
#ifdef M5STACK_CARDPUTER_ADV
#include "AudioBoard.h"
#include "configuration.h"
DriverPins PinsAudioBoardES8311;
AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311);
@@ -41,5 +38,3 @@ void lateInitVariant()
es8311_write_reg(0x32, 0xBF); // DAC volume (0dB)
es8311_write_reg(0x37, 0x08); // EQ bypass
}
#endif
@@ -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.9
zinggjm/GxEPD2@1.6.8
# 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
+1 -1
View File
@@ -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.9
zinggjm/GxEPD2@1.6.8
# 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
+1 -1
View File
@@ -39,7 +39,7 @@
#define DAC_I2S_BCK 48
#define DAC_I2S_WS 15
#define DAC_I2S_DOUT 46
#define DAC_I2S_MCLK -1
#define DAC_I2S_MCLK 0
#define HAS_AXP2101
+47 -42
View File
@@ -6,27 +6,26 @@ NicheGraphics attempts a different approach:
Per-device config takes place in this setupNicheGraphics() method
(And a small amount in platformio.ini)
This file sets up InkHUD for the LilyGo T5-E-Paper-S3-Pro.
The board uses a 4.7" ED047TC1 parallel e-paper display (960×540, 8-bit parallel interface).
This is driven via the FastEPD library through the NicheGraphics ED047TC1 driver adapter.
This file sets up InkHUD for Heltec VM-E290.
Different NicheGraphics UIs and different hardware variants will each have their own setup procedure.
*/
#pragma once
#include "configuration.h"
#include "mesh/MeshModule.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
// InkHUD-specific components
// ---------------------------
#include "graphics/niche/InkHUD/InkHUD.h"
// #include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/WindowManager.h"
// Applets
#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h"
#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h"
#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h"
#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h"
#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h"
#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h"
@@ -35,20 +34,26 @@ This is driven via the FastEPD library through the NicheGraphics ED047TC1 driver
// Shared NicheGraphics components
// --------------------------------
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
#include "graphics/niche/Drivers/EInk/ED047TC1.h"
#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h"
#include "graphics/niche/Inputs/TwoButton.h"
void setupNicheGraphics()
{
using namespace NicheGraphics;
// SPI
// -----------------------------
// Display is connected to HSPI
SPIClass *hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
// E-Ink Driver
// -----------------------------
// The ED047TC1 is a parallel display — no SPI bus setup needed.
// begin() args are part of the EInk interface but are ignored for parallel displays.
Drivers::EInk *driver = new Drivers::ED047TC1;
driver->begin(nullptr, 0, 0, 0);
// Use E-Ink driver
Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
// InkHUD
// ----------------------------
@@ -62,57 +67,57 @@ void setupNicheGraphics()
// Set how unhealthy additional FAST updates beyond this number are
inkhud->setDisplayResilience(7, 1.5);
// Prepare fonts — use larger sizes to suit the 4.7" screen at ~234 DPI
InkHUD::Applet::fontLarge = FREESANS_24PT_WIN1253;
InkHUD::Applet::fontMedium = FREESANS_18PT_WIN1253;
InkHUD::Applet::fontSmall = FREESANS_12PT_WIN1253;
// Prepare fonts
InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252;
InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252;
// Customize default settings
// Init settings, and customize defaults
inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle?
inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise
inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise
inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
inkhud->persistence->settings.optionalFeatures.batteryIcon = true;
inkhud->persistence->settings.optionalMenuItems.backlight = true;
inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead
inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery
// Alignment must cancel rotation for visual-frame touch input: (rotation + alignment) % 4 == 0.
inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4;
// Setup backlight
// Note: AUX button behavior configured further down
Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance();
backlight->setPin(PIN_EINK_EN);
// Pick applets
// Note: order of applets determines priority of "auto-show" feature
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, false, false); // Not Active, not autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet, true, true); // Activated, Autoshown
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, true); // Activated, Autoshown
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1), false, false); // Not Active, not autoshown
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false); // Activated, not autoshown
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false); // Activated, not autoshown
// Optional arguments for defaults:
// - is activated?
// - is autoshown?
// - is foreground on a specific tile (index)?
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet);
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // Not Active, not autoshown
// Backlight
// ----------------------------
Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance();
backlight->setPin(BOARD_BL_EN); // GPIO11 on V2
// inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet);
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
// Start running InkHUD
inkhud->begin();
// Touch navigation requires joystick mode — enforce post-begin so flash cannot override.
inkhud->persistence->settings.joystick.enabled = true;
inkhud->persistence->settings.joystick.aligned = true;
// Buttons
// --------------------------
Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component
// Setup the main user button (boot button, GPIO 0)
// Setup the main user button (0)
buttons->setWiring(0, BUTTON_PIN);
buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); });
buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); });
buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); });
// No dedicated aux button on this board
// Setup the aux button (1)
// Bonus feature of VME290
buttons->setWiring(1, BUTTON_PIN_SECONDARY);
buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); });
buttons->start();
}
#endif
#endif
+1 -1
View File
@@ -5,7 +5,7 @@ board_build.partition = default_16MB.csv
board_check = true
upload_protocol = esptool
build_flags = -fno-strict-aliasing
${esp32s3_base.build_flags}
${esp32_base.build_flags}
-I variants/esp32s3/t5s3_epaper
-D T5_S3_EPAPER_PRO
-D USE_EINK
+7 -139
View File
@@ -2,123 +2,21 @@
#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
// rotate 90° for landscape
*x = raw_y;
*y = EPD_WIDTH - 1 - raw_x;
LOG_DEBUG("touched(%d/%d)", *x, *y);
return true;
}
@@ -133,47 +31,17 @@ void earlyInitVariant()
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(BOARD_BL_EN, OUTPUT);
// Program GT911 touch controller to I2C address 0x14 (GT911_SLAVE_ADDRESS_H) before
// the I2C bus scan runs. GPIO3 (INT) defaults LOW on ESP32-S3 cold boot, which would
// leave the GT911 at 0x5D (GT911_SLAVE_ADDRESS_L) — the same address as the SFA30
// air quality sensor — causing a false-positive SFA30 detection during the I2C scan.
//
// GT911 datasheet §4.3 "Address Selection":
// Pull INT HIGH before releasing RST → device latches address 0x14 (SLAVE_ADDRESS_H)
// Pull INT LOW before releasing RST → device latches address 0x5D (SLAVE_ADDRESS_L)
// Minimum RST assert time: 100 µs; minimum startup time after RST deassert: 5 ms.
//
// lateInitVariant() calls touch.begin() which repeats this sequence internally while
// also performing full I2C initialisation; the double-reset is harmless.
pinMode(GT911_PIN_RST, OUTPUT);
digitalWrite(GT911_PIN_RST, LOW);
pinMode(GT911_PIN_INT, OUTPUT);
digitalWrite(GT911_PIN_INT, HIGH); // HIGH → latch address 0x14
delay(1); // > 100 µs
digitalWrite(GT911_PIN_RST, HIGH);
delay(10); // > 5 ms startup
pinMode(GT911_PIN_INT, INPUT); // release INT for interrupt use
}
void variant_shutdown()
{
// Ensure frontlight is off during deep sleep
digitalWrite(BOARD_BL_EN, LOW);
}
void lateInitVariant()
// T5-S3-ePaper Pro specific (late-) init
void lateInitVariant(void)
{
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(&notifyDeepSleep);
if (touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) {
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
#endif
+2 -2
View File
@@ -26,9 +26,9 @@
#define GT911_PIN_RST 9
#endif
#define PCF8563_RTC 0x51
#define PCF85063_RTC 0x51
#define HAS_RTC 1
#define PCF8563_INT 2
#define PCF85063_INT 2
#define USE_POWERSAVE
#define SLEEP_TIME 120
+3 -1
View File
@@ -9,6 +9,8 @@
#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
@@ -74,4 +76,4 @@
// has 32768 Hz crystal
#define HAS_32768HZ 1
#define USE_SH1106
#define USE_SH1106
+11 -4
View File
@@ -2,6 +2,8 @@
extends = portduino_base
build_flags = ${portduino_base.build_flags} -I variants/native/portduino
-I /usr/include
!pkg-config --cflags --libs libnotify
-D HAS_LIBNOTIFY=1
board = cross_platform
board_level = extra
lib_deps =
@@ -27,7 +29,8 @@ build_type = release
lib_deps =
${native_base.lib_deps}
${device-ui_base.lib_deps}
build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections
build_flags = ${native_base.build_flags}
-Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections
-D RAM_SIZE=16384
-D USE_X11=1
-D HAS_TFT=1
@@ -55,7 +58,8 @@ build_type = release
lib_deps =
${native_base.lib_deps}
${device-ui_base.lib_deps}
build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections
build_flags = ${native_base.build_flags}
-Os -ffunction-sections -fdata-sections -Wl,--gc-sections
-D RAM_SIZE=8192
-D USE_FRAMEBUFFER=1
-D LV_COLOR_DEPTH=32
@@ -84,7 +88,8 @@ build_type = debug
lib_deps =
${native_base.lib_deps}
${device-ui_base.lib_deps}
build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon
build_flags = ${native_base.build_flags}
-O0 -fsanitize=address -lX11 -linput -lxkbcommon
-D DEBUG_HEAP
-D RAM_SIZE=16384
-D USE_X11=1
@@ -112,7 +117,9 @@ build_src_filter = ${env:native-tft.build_src_filter}
[env:coverage]
extends = env:native
build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:native.build_flags}
build_flags =
-lgcov --coverage -fprofile-abs-path -fsanitize=address
${env:native.build_flags}
; https://docs.platformio.org/en/latest/projectconf/sections/env/options/test/test_testing_command.html
test_testing_command =
${platformio.build_dir}/${this.__env__}/meshtasticd
@@ -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.9
zinggjm/GxEPD2@1.6.8
debug_tool = jlink
@@ -85,6 +85,7 @@ 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,7 +20,6 @@
#include "variant.h"
#include "nrf.h"
#include "power.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
@@ -66,11 +65,7 @@ void variant_shutdown()
nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW;
nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1);
// 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);
}
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, 3490
#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3450
#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.9
zinggjm/GxEPD2@1.6.8
; 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.9
zinggjm/GxEPD2@1.6.8
debug_tool = jlink
;upload_port = /dev/ttyACM4
+1 -1
View File
@@ -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.9
zinggjm/GxEPD2@1.6.8
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.9
zinggjm/GxEPD2@1.6.8
# 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.9
zinggjm/GxEPD2@1.6.8
# 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