mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-07 04:30:04 +10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36af332622 | ||
|
|
eb9fa6f4fe | ||
|
|
c06e152e59 | ||
|
|
8b35fefdc7 | ||
|
|
d15666611b |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 \
|
||||
|
||||
Vendored
+2
-1
@@ -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
|
||||
|
||||
Vendored
+2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
Submodule protobufs updated: d004f503bb...4d5b500df5
+30
-99
@@ -746,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(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -1035,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
|
||||
*
|
||||
@@ -1403,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
|
||||
|
||||
@@ -276,42 +276,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
ui->update();
|
||||
}
|
||||
|
||||
void Screen::showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
|
||||
int maxValueTenths, std::function<void(int)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
if (minValueTenths > maxValueTenths) {
|
||||
int temp = minValueTenths;
|
||||
minValueTenths = maxValueTenths;
|
||||
maxValueTenths = temp;
|
||||
}
|
||||
|
||||
if (initialValueTenths < minValueTenths) {
|
||||
initialValueTenths = minValueTenths;
|
||||
} else if (initialValueTenths > maxValueTenths) {
|
||||
initialValueTenths = maxValueTenths;
|
||||
}
|
||||
|
||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::signed_decimal_picker;
|
||||
NotificationRenderer::signedDecimalValueTenths = static_cast<int16_t>(initialValueTenths);
|
||||
NotificationRenderer::signedDecimalMinTenths = static_cast<int16_t>(minValueTenths);
|
||||
NotificationRenderer::signedDecimalMaxTenths = static_cast<int16_t>(maxValueTenths);
|
||||
NotificationRenderer::signedDecimalIsNegative = (initialValueTenths < 0);
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
@@ -1305,8 +1269,6 @@ void Screen::setFrames(FrameFocus focus)
|
||||
fsi.positions.focusedModule = numframes;
|
||||
if (m && m == waypointModule)
|
||||
fsi.positions.waypoint = numframes;
|
||||
if (m && strcmp(m->getName(), "EnvironmentTelemetry") == 0)
|
||||
fsi.positions.environment = numframes;
|
||||
|
||||
indicatorIcons.push_back(icon_module);
|
||||
numframes++;
|
||||
@@ -2020,8 +1982,6 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
menuHandler::textMessageBaseMenu();
|
||||
}
|
||||
}
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.environment) {
|
||||
menuHandler::environmentTelemetryBaseMenu();
|
||||
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||
namespace graphics
|
||||
{
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, signed_decimal_picker, text_input };
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
const char *message;
|
||||
@@ -312,8 +312,6 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
|
||||
int maxValueTenths, std::function<void(int)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
@@ -709,7 +707,6 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t firstFavorite = 255;
|
||||
uint8_t lastFavorite = 255;
|
||||
uint8_t lora = 255;
|
||||
uint8_t environment = 255;
|
||||
} positions;
|
||||
|
||||
uint8_t frameCount = 0;
|
||||
|
||||
@@ -58,32 +58,6 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
||||
return bannerOptions;
|
||||
}
|
||||
|
||||
constexpr float kTemperatureOffsetMinC = -20.0f;
|
||||
constexpr float kTemperatureOffsetMaxC = 20.0f;
|
||||
constexpr float kTemperatureOffsetDeltaFPerC = 1.8f;
|
||||
|
||||
bool useImperialTemperatureOffsetUnits()
|
||||
{
|
||||
return config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL ||
|
||||
moduleConfig.telemetry.environment_display_fahrenheit;
|
||||
}
|
||||
|
||||
float clampTemperatureOffsetC(float offsetC)
|
||||
{
|
||||
if (offsetC < kTemperatureOffsetMinC) {
|
||||
return kTemperatureOffsetMinC;
|
||||
}
|
||||
if (offsetC > kTemperatureOffsetMaxC) {
|
||||
return kTemperatureOffsetMaxC;
|
||||
}
|
||||
return offsetC;
|
||||
}
|
||||
|
||||
int toTenthsRounded(float value)
|
||||
{
|
||||
return (value >= 0.0f) ? static_cast<int>(value * 10.0f + 0.5f) : static_cast<int>(value * 10.0f - 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||
@@ -1035,52 +1009,6 @@ void menuHandler::textMessageMenu()
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
}
|
||||
|
||||
void menuHandler::environmentTelemetryBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, SetTempOffset };
|
||||
|
||||
static const char *optionsArray[] = {"Back", "Set Temp Offset"};
|
||||
static int optionsEnumArray[] = {Back, SetTempOffset};
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Env Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == SetTempOffset) {
|
||||
menuQueue = EnvironmentTempOffsetPicker;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::environmentTemperatureOffsetPicker()
|
||||
{
|
||||
static char pickerTitle[40];
|
||||
|
||||
const bool useImperial = useImperialTemperatureOffsetUnits();
|
||||
const char displayUnit = useImperial ? 'F' : 'C';
|
||||
const float currentOffsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
|
||||
const float displayOffset = useImperial ? (currentOffsetC * kTemperatureOffsetDeltaFPerC) : currentOffsetC;
|
||||
const float minDisplay = useImperial ? (kTemperatureOffsetMinC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMinC;
|
||||
const float maxDisplay = useImperial ? (kTemperatureOffsetMaxC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMaxC;
|
||||
|
||||
snprintf(pickerTitle, sizeof(pickerTitle), "Set Temp Offset (%c)", displayUnit);
|
||||
screen->showSignedDecimalPicker(pickerTitle, 60000, toTenthsRounded(displayOffset), toTenthsRounded(minDisplay),
|
||||
toTenthsRounded(maxDisplay), [useImperial](int pickedTenths) -> void {
|
||||
float selectedOffset = static_cast<float>(pickedTenths) / 10.0f;
|
||||
if (useImperial) {
|
||||
selectedOffset /= kTemperatureOffsetDeltaFPerC;
|
||||
}
|
||||
|
||||
moduleConfig.telemetry.environment_temperature_offset_c =
|
||||
clampTemperatureOffsetC(selectedOffset);
|
||||
nodeDB->saveToDisk(SEGMENT_MODULECONFIG);
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::textMessageBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
|
||||
@@ -2290,6 +2218,9 @@ void menuHandler::testMenu()
|
||||
static int optionsEnumArray[5] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Number Picker";
|
||||
optionsEnumArray[options++] = NumberPicker;
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
||||
optionsEnumArray[options++] = ShowChirpy;
|
||||
#ifdef HAS_I2S
|
||||
@@ -2303,7 +2234,10 @@ void menuHandler::testMenu()
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == ShowChirpy) {
|
||||
if (selected == NumberPicker) {
|
||||
menuQueue = NumberTest;
|
||||
screen->runNow();
|
||||
} else if (selected == ShowChirpy) {
|
||||
screen->toggleFrameVisibility("chirpy");
|
||||
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||
|
||||
@@ -2319,6 +2253,12 @@ void menuHandler::testMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::numberTest()
|
||||
{
|
||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
||||
}
|
||||
|
||||
void menuHandler::wifiBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Wifi_toggle };
|
||||
@@ -2814,8 +2754,8 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case TestMenu:
|
||||
testMenu();
|
||||
break;
|
||||
case EnvironmentTempOffsetPicker:
|
||||
environmentTemperatureOffsetPicker();
|
||||
case NumberTest:
|
||||
numberTest();
|
||||
break;
|
||||
case WifiToggleMenu:
|
||||
wifiToggleMenu();
|
||||
@@ -2870,4 +2810,4 @@ void menuHandler::saveUIConfig()
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -38,7 +38,7 @@ class menuHandler
|
||||
ManageNodeMenu,
|
||||
RemoveFavorite,
|
||||
TestMenu,
|
||||
EnvironmentTempOffsetPicker,
|
||||
NumberTest,
|
||||
WifiToggleMenu,
|
||||
BluetoothToggleMenu,
|
||||
ScreenOptionsMenu,
|
||||
@@ -78,7 +78,6 @@ class menuHandler
|
||||
static void deleteMessagesMenu();
|
||||
static void homeBaseMenu();
|
||||
static void textMessageBaseMenu();
|
||||
static void environmentTelemetryBaseMenu();
|
||||
static void systemBaseMenu();
|
||||
static void favoriteBaseMenu();
|
||||
static void positionBaseMenu();
|
||||
@@ -102,7 +101,7 @@ class menuHandler
|
||||
static void removeFavoriteMenu();
|
||||
static void traceRouteMenu();
|
||||
static void testMenu();
|
||||
static void environmentTemperatureOffsetPicker();
|
||||
static void numberTest();
|
||||
static void wifiBaseMenu();
|
||||
static void wifiToggleMenu();
|
||||
static void screenOptionsMenu();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#endif
|
||||
#include "main.h"
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#if HAS_TRACKBALL
|
||||
@@ -53,292 +52,16 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
int16_t NotificationRenderer::signedDecimalValueTenths = 0;
|
||||
int16_t NotificationRenderer::signedDecimalMinTenths = -999;
|
||||
int16_t NotificationRenderer::signedDecimalMaxTenths = 999;
|
||||
bool NotificationRenderer::signedDecimalIsNegative = false;
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
struct NumericSlotPickerState {
|
||||
std::vector<uint8_t> digits;
|
||||
bool hasSign = false;
|
||||
bool isNegative = false;
|
||||
uint8_t decimalDigits = 0;
|
||||
int32_t minValue = 0;
|
||||
int32_t maxValue = 0;
|
||||
};
|
||||
|
||||
int32_t maxValueForDigits(uint8_t digitCount)
|
||||
uint32_t pow_of_10(uint32_t n)
|
||||
{
|
||||
int64_t value = 0;
|
||||
for (uint8_t i = 0; i < digitCount; i++) {
|
||||
value = (value * 10) + 9;
|
||||
if (value > std::numeric_limits<int32_t>::max()) {
|
||||
return std::numeric_limits<int32_t>::max();
|
||||
}
|
||||
uint32_t ret = 1;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
ret *= 10;
|
||||
}
|
||||
return static_cast<int32_t>(value);
|
||||
}
|
||||
|
||||
uint8_t pickerSlotCount(const NumericSlotPickerState &state)
|
||||
{
|
||||
return static_cast<uint8_t>(state.digits.size() + (state.hasSign ? 1 : 0));
|
||||
}
|
||||
|
||||
int32_t clampPickerValue(int32_t value, int32_t minValue, int32_t maxValue)
|
||||
{
|
||||
if (value < minValue) {
|
||||
return minValue;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
return maxValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
uint32_t combinePickerDigits(const std::vector<uint8_t> &digits)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
for (const uint8_t digit : digits) {
|
||||
value = (value * 10) + digit;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void splitPickerDigits(uint32_t value, std::vector<uint8_t> &digits)
|
||||
{
|
||||
if (digits.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = static_cast<int>(digits.size()) - 1; i >= 0; i--) {
|
||||
digits[static_cast<size_t>(i)] = static_cast<uint8_t>(value % 10);
|
||||
value /= 10;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t composePickerValue(const NumericSlotPickerState &state)
|
||||
{
|
||||
int64_t value = static_cast<int64_t>(combinePickerDigits(state.digits));
|
||||
if (state.hasSign && state.isNegative && value != 0) {
|
||||
value = -value;
|
||||
}
|
||||
return clampPickerValue(static_cast<int32_t>(value), state.minValue, state.maxValue);
|
||||
}
|
||||
|
||||
void normalizePickerState(NumericSlotPickerState &state)
|
||||
{
|
||||
const bool keepNegativeZero = state.isNegative;
|
||||
const int32_t clampedValue = composePickerValue(state);
|
||||
const uint32_t absValue =
|
||||
(clampedValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(clampedValue)) : static_cast<uint32_t>(clampedValue);
|
||||
splitPickerDigits(absValue, state.digits);
|
||||
|
||||
if (!state.hasSign) {
|
||||
state.isNegative = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (clampedValue < 0) {
|
||||
state.isNegative = true;
|
||||
} else if (clampedValue > 0) {
|
||||
state.isNegative = false;
|
||||
} else {
|
||||
// Keep selected sign for zero so users can pick +/- before entering digits.
|
||||
state.isNegative = keepNegativeZero;
|
||||
}
|
||||
}
|
||||
|
||||
bool isPickerIncrementEvent(uint8_t eventType)
|
||||
{
|
||||
return eventType == INPUT_BROKER_UP || eventType == INPUT_BROKER_ALT_PRESS || eventType == INPUT_BROKER_UP_LONG;
|
||||
}
|
||||
|
||||
bool isPickerDecrementEvent(uint8_t eventType)
|
||||
{
|
||||
return eventType == INPUT_BROKER_DOWN || eventType == INPUT_BROKER_USER_PRESS || eventType == INPUT_BROKER_DOWN_LONG;
|
||||
}
|
||||
|
||||
void applyPickerDelta(NumericSlotPickerState &state, int8_t selectedSlot, int8_t delta)
|
||||
{
|
||||
const uint8_t slotCount = pickerSlotCount(state);
|
||||
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.hasSign && selectedSlot == 0) {
|
||||
state.isNegative = !state.isNegative;
|
||||
normalizePickerState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const int8_t digitOffset = state.hasSign ? 1 : 0;
|
||||
const int8_t digitIndex = selectedSlot - digitOffset;
|
||||
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t &digit = state.digits[static_cast<size_t>(digitIndex)];
|
||||
if (delta > 0) {
|
||||
digit = static_cast<uint8_t>((digit + 1) % 10);
|
||||
} else {
|
||||
digit = static_cast<uint8_t>((digit == 0) ? 9 : (digit - 1));
|
||||
}
|
||||
normalizePickerState(state);
|
||||
}
|
||||
|
||||
bool applyPickerKeypress(NumericSlotPickerState &state, int8_t selectedSlot, char key)
|
||||
{
|
||||
const uint8_t slotCount = pickerSlotCount(state);
|
||||
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.hasSign && selectedSlot == 0 && (key == '+' || key == '-')) {
|
||||
state.isNegative = (key == '-');
|
||||
normalizePickerState(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key < '0' || key > '9') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int8_t digitOffset = state.hasSign ? 1 : 0;
|
||||
const int8_t digitIndex = selectedSlot - digitOffset;
|
||||
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.digits[static_cast<size_t>(digitIndex)] = static_cast<uint8_t>(key - '0');
|
||||
normalizePickerState(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
void parseBannerLines(const char *lineStarts[MAX_LINES + 1], uint16_t &lineCount)
|
||||
{
|
||||
lineCount = 0;
|
||||
char *alertEnd = NotificationRenderer::alertBannerMessage +
|
||||
strnlen(NotificationRenderer::alertBannerMessage, sizeof(NotificationRenderer::alertBannerMessage));
|
||||
lineStarts[lineCount] = NotificationRenderer::alertBannerMessage;
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
if (lineStarts[lineCount + 1][0] == '\n') {
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
}
|
||||
lineCount++;
|
||||
}
|
||||
}
|
||||
|
||||
void buildNumericPickerDisplay(const NumericSlotPickerState &state, std::string &formattedValue,
|
||||
std::vector<int16_t> &slotCharIndexBySlot)
|
||||
{
|
||||
const uint8_t slotCount = pickerSlotCount(state);
|
||||
slotCharIndexBySlot.assign(slotCount, -1);
|
||||
|
||||
formattedValue = " ";
|
||||
formattedValue.reserve(24);
|
||||
int8_t slot = 0;
|
||||
|
||||
if (state.hasSign) {
|
||||
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
|
||||
formattedValue += state.isNegative ? '-' : '+';
|
||||
formattedValue += ' ';
|
||||
}
|
||||
|
||||
const size_t digitCount = state.digits.size();
|
||||
const size_t decimalBreak =
|
||||
(state.decimalDigits > 0 && state.decimalDigits < digitCount) ? (digitCount - state.decimalDigits) : digitCount;
|
||||
for (size_t i = 0; i < digitCount; i++) {
|
||||
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
|
||||
formattedValue += static_cast<char>('0' + state.digits[i]);
|
||||
formattedValue += ' ';
|
||||
|
||||
if (state.decimalDigits > 0 && i + 1 == decimalBreak) {
|
||||
formattedValue += '.';
|
||||
formattedValue += ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Number picker renderer
|
||||
// Reuse by building `formattedValue` as displayed, and mapping each editable slot
|
||||
// to its character index in that string via `slotCharIndexBySlot`.
|
||||
void NumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lineStarts[MAX_LINES + 1], uint16_t lineCount,
|
||||
const std::string &formattedValue, const int16_t *slotCharIndexBySlot, uint8_t slotCount, int8_t selectedSlot)
|
||||
{
|
||||
if (formattedValue.empty() || slotCharIndexBySlot == nullptr || slotCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string spacer(formattedValue.size(), ' ');
|
||||
uint16_t totalLines = lineCount + 3;
|
||||
const char *linePointers[totalLines + 1] = {0};
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
const uint16_t topGuideLineIndex = lineCount;
|
||||
linePointers[lineCount++] = spacer.c_str();
|
||||
const uint16_t valueLineIndex = lineCount;
|
||||
linePointers[lineCount++] = formattedValue.c_str();
|
||||
const uint16_t bottomGuideLineIndex = lineCount;
|
||||
linePointers[lineCount++] = spacer.c_str();
|
||||
|
||||
NotificationRenderer::drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
|
||||
constexpr uint16_t hPadding = 5;
|
||||
constexpr uint16_t vPadding = 2;
|
||||
uint16_t maxWidth = 0;
|
||||
uint16_t lineWidths[MAX_LINES + 3] = {0};
|
||||
for (uint16_t i = 0; i < totalLines; i++) {
|
||||
const uint16_t lineLength = static_cast<uint16_t>(strlen(linePointers[i]));
|
||||
lineWidths[i] = display->getStringWidth(linePointers[i], lineLength, true);
|
||||
if (lineWidths[i] > maxWidth) {
|
||||
maxWidth = lineWidths[i];
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1) {
|
||||
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
|
||||
}
|
||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||
if (totalLines > visibleTotalLines) {
|
||||
boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2;
|
||||
}
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
|
||||
const int selectedSlotClamped = std::max<int>(0, std::min<int>(selectedSlot, static_cast<int>(slotCount) - 1));
|
||||
const int selectedCharIndex = slotCharIndexBySlot[selectedSlotClamped];
|
||||
if (selectedCharIndex < 0 || selectedCharIndex >= static_cast<int>(formattedValue.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
int16_t valueTextX = boxLeft + (boxWidth - lineWidths[valueLineIndex]) / 2;
|
||||
const uint16_t prefixWidth = display->getStringWidth(formattedValue.c_str(), selectedCharIndex, true);
|
||||
const uint16_t slotCharWidth = display->getStringWidth(formattedValue.c_str() + selectedCharIndex, 1, true);
|
||||
const int16_t slotCenterX = valueTextX + static_cast<int16_t>(prefixWidth + (slotCharWidth / 2));
|
||||
|
||||
int16_t topGuideY = boxTop + vPadding + (topGuideLineIndex * effectiveLineHeight);
|
||||
int16_t bottomGuideY = boxTop + vPadding + (bottomGuideLineIndex * effectiveLineHeight);
|
||||
const int16_t triHalfWidth = (currentResolution == ScreenResolution::High) ? 3 : 2;
|
||||
const int16_t guideInsetY = 1;
|
||||
const int16_t triHeight = std::max<int16_t>(2, static_cast<int16_t>((effectiveLineHeight - (guideInsetY * 2) - 1) / 2));
|
||||
const int16_t topBaseY = topGuideY + effectiveLineHeight - guideInsetY - 1;
|
||||
const int16_t topApexY = topBaseY - triHeight;
|
||||
const int16_t bottomBaseY = bottomGuideY + guideInsetY;
|
||||
const int16_t bottomApexY = bottomBaseY + triHeight;
|
||||
|
||||
display->setColor(WHITE);
|
||||
display->fillTriangle(slotCenterX, topApexY, slotCenterX - triHalfWidth, topBaseY, slotCenterX + triHalfWidth, topBaseY);
|
||||
display->fillTriangle(slotCenterX - triHalfWidth, bottomBaseY, slotCenterX + triHalfWidth, bottomBaseY, slotCenterX,
|
||||
bottomApexY);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Used on boot when a certificate is being created
|
||||
@@ -380,10 +103,6 @@ void NotificationRenderer::resetBanner()
|
||||
pauseBanner = false;
|
||||
numDigits = 0;
|
||||
currentNumber = 0;
|
||||
signedDecimalValueTenths = 0;
|
||||
signedDecimalMinTenths = -999;
|
||||
signedDecimalMaxTenths = 999;
|
||||
signedDecimalIsNegative = false;
|
||||
|
||||
nodeDB->pause_sort(false);
|
||||
|
||||
@@ -434,9 +153,6 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
case notificationTypeEnum::number_picker:
|
||||
drawNumberPicker(display, state);
|
||||
break;
|
||||
case notificationTypeEnum::signed_decimal_picker:
|
||||
drawSignedDecimalPicker(display, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,132 +160,83 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
|
||||
{
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
parseBannerLines(lineStarts, lineCount);
|
||||
|
||||
NumericSlotPickerState pickerState;
|
||||
pickerState.hasSign = false;
|
||||
pickerState.decimalDigits = 0;
|
||||
pickerState.minValue = 0;
|
||||
pickerState.maxValue = maxValueForDigits(static_cast<uint8_t>(numDigits));
|
||||
pickerState.digits.assign(numDigits, 0);
|
||||
splitPickerDigits(currentNumber, pickerState.digits);
|
||||
normalizePickerState(pickerState);
|
||||
// Parse lines
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
|
||||
const uint8_t slotCount = pickerSlotCount(pickerState);
|
||||
if (curSelected < 0) {
|
||||
curSelected = 0;
|
||||
} else if (curSelected > static_cast<int8_t>(slotCount)) {
|
||||
curSelected = static_cast<int8_t>(slotCount);
|
||||
// Find lines
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPickerIncrementEvent(inEvent.inputEvent)) {
|
||||
applyPickerDelta(pickerState, curSelected, 1);
|
||||
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
|
||||
applyPickerDelta(pickerState, curSelected, -1);
|
||||
// modulo to extract
|
||||
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
if (this_digit == 9) {
|
||||
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
|
||||
} else {
|
||||
currentNumber += (pow_of_10(numDigits - curSelected - 1));
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
if (this_digit == 0) {
|
||||
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
|
||||
} else {
|
||||
currentNumber -= (pow_of_10(numDigits - curSelected - 1));
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
|
||||
if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit
|
||||
currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1));
|
||||
currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1));
|
||||
curSelected++;
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
curSelected = std::max<int8_t>(0, curSelected - 1);
|
||||
curSelected--;
|
||||
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
||||
alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
currentNumber = static_cast<uint32_t>(std::max<int32_t>(0, composePickerValue(pickerState)));
|
||||
if (curSelected == static_cast<int8_t>(slotCount)) {
|
||||
if (curSelected == static_cast<int8_t>(numDigits)) {
|
||||
alertBannerCallback(currentNumber);
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0') {
|
||||
if (alertBannerMessage[0] == '\0')
|
||||
return;
|
||||
|
||||
uint16_t totalLines = lineCount + 2;
|
||||
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
|
||||
// copy the linestarts to display to the linePointers holder
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
|
||||
std::string formattedValue;
|
||||
std::vector<int16_t> slotCharIndexBySlot;
|
||||
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
|
||||
|
||||
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
parseBannerLines(lineStarts, lineCount);
|
||||
|
||||
NumericSlotPickerState pickerState;
|
||||
pickerState.hasSign = true;
|
||||
pickerState.decimalDigits = 1;
|
||||
pickerState.minValue = signedDecimalMinTenths;
|
||||
pickerState.maxValue = signedDecimalMaxTenths;
|
||||
pickerState.digits.assign(3, 0); // XX.X format
|
||||
|
||||
const int32_t currentValue = static_cast<int32_t>(signedDecimalValueTenths);
|
||||
if (currentValue < 0) {
|
||||
pickerState.isNegative = true;
|
||||
} else if (currentValue > 0) {
|
||||
pickerState.isNegative = false;
|
||||
} else {
|
||||
pickerState.isNegative = signedDecimalIsNegative;
|
||||
}
|
||||
const uint32_t absValue =
|
||||
(currentValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(currentValue)) : static_cast<uint32_t>(currentValue);
|
||||
splitPickerDigits(absValue, pickerState.digits);
|
||||
normalizePickerState(pickerState);
|
||||
|
||||
const uint8_t slotCount = pickerSlotCount(pickerState);
|
||||
if (curSelected < 0) {
|
||||
curSelected = 0;
|
||||
} else if (curSelected > static_cast<int8_t>(slotCount)) {
|
||||
curSelected = static_cast<int8_t>(slotCount);
|
||||
}
|
||||
|
||||
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPickerIncrementEvent(inEvent.inputEvent)) {
|
||||
applyPickerDelta(pickerState, curSelected, 1);
|
||||
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
|
||||
applyPickerDelta(pickerState, curSelected, -1);
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
|
||||
curSelected++;
|
||||
std::string digits = " ";
|
||||
std::string arrowPointer = " ";
|
||||
for (uint16_t i = 0; i < numDigits; i++) {
|
||||
// Modulo minus modulo to return just the current number
|
||||
digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " ";
|
||||
if (curSelected == i) {
|
||||
arrowPointer += "^ ";
|
||||
} else {
|
||||
arrowPointer += "_ ";
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
curSelected = std::max<int8_t>(0, curSelected - 1);
|
||||
}
|
||||
|
||||
signedDecimalValueTenths = static_cast<int16_t>(composePickerValue(pickerState));
|
||||
signedDecimalIsNegative = pickerState.isNegative;
|
||||
if (curSelected == static_cast<int8_t>(slotCount)) {
|
||||
alertBannerCallback(signedDecimalValueTenths);
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
linePointers[lineCount++] = digits.c_str();
|
||||
linePointers[lineCount++] = arrowPointer.c_str();
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string formattedValue;
|
||||
std::vector<int16_t> slotCharIndexBySlot;
|
||||
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
|
||||
|
||||
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
@@ -955,9 +622,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
// Determine if this is a pop-up or a pick list
|
||||
const bool highlightTitleRow =
|
||||
(i == 0) && (alertBannerOptions > 0 || current_notification_type == notificationTypeEnum::signed_decimal_picker);
|
||||
if (highlightTitleRow) {
|
||||
if (alertBannerOptions > 0 && i == 0) {
|
||||
// Pick List
|
||||
display->setColor(WHITE);
|
||||
int background_yOffset = 1;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include "modules/OnScreenKeyboardModule.h"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
@@ -27,10 +26,6 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static int16_t signedDecimalValueTenths;
|
||||
static int16_t signedDecimalMinTenths;
|
||||
static int16_t signedDecimalMaxTenths;
|
||||
static bool signedDecimalIsNegative;
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
@@ -41,7 +36,6 @@ class NotificationRenderer
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,8 +29,7 @@ void TouchScreenImpl1::init()
|
||||
return;
|
||||
#else
|
||||
TouchScreenBase::init(true);
|
||||
if (inputBroker)
|
||||
inputBroker->registerSource(this);
|
||||
inputBroker->registerSource(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+129
@@ -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 =
|
||||
|
||||
@@ -81,7 +81,6 @@ class MeshModule
|
||||
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
||||
meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response);
|
||||
const char *getName() const { return name; }
|
||||
#if HAS_SCREEN
|
||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; }
|
||||
virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -398,8 +398,6 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig {
|
||||
bool device_telemetry_enabled;
|
||||
/* Enable/Disable the air quality telemetry measurement module on-device display */
|
||||
bool air_quality_screen_enabled;
|
||||
/* Temperature offset in Celsius applied to local environment telemetry before it is sent. */
|
||||
float environment_temperature_offset_c;
|
||||
} meshtastic_ModuleConfig_TelemetryConfig;
|
||||
|
||||
/* Canned Messages Module Config */
|
||||
@@ -595,7 +593,7 @@ extern "C" {
|
||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
||||
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""}
|
||||
@@ -614,7 +612,7 @@ extern "C" {
|
||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
||||
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""}
|
||||
@@ -720,7 +718,6 @@ extern "C" {
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_environment_temperature_offset_c_tag 16
|
||||
#define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1
|
||||
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2
|
||||
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3
|
||||
@@ -951,8 +948,7 @@ X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \
|
||||
X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \
|
||||
X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \
|
||||
X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \
|
||||
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, environment_temperature_offset_c, 16)
|
||||
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15)
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL
|
||||
|
||||
@@ -1055,8 +1051,8 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
|
||||
#define meshtastic_ModuleConfig_SerialConfig_size 28
|
||||
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
|
||||
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_size 55
|
||||
#define meshtastic_ModuleConfig_TAKConfig_size 4
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_size 50
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
|
||||
#define meshtastic_ModuleConfig_size 227
|
||||
#define meshtastic_RemoteHardwarePin_size 21
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -138,32 +138,6 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float TEMPERATURE_OFFSET_MIN_C = -20.0f;
|
||||
constexpr float TEMPERATURE_OFFSET_MAX_C = 20.0f;
|
||||
|
||||
float clampTemperatureOffsetC(float offsetC)
|
||||
{
|
||||
if (offsetC < TEMPERATURE_OFFSET_MIN_C)
|
||||
return TEMPERATURE_OFFSET_MIN_C;
|
||||
if (offsetC > TEMPERATURE_OFFSET_MAX_C)
|
||||
return TEMPERATURE_OFFSET_MAX_C;
|
||||
return offsetC;
|
||||
}
|
||||
|
||||
void applyTemperatureOffset(meshtastic_EnvironmentMetrics *metrics)
|
||||
{
|
||||
const float offsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
|
||||
if (offsetC == 0.0f)
|
||||
return;
|
||||
|
||||
if (metrics->has_temperature)
|
||||
metrics->temperature += offsetC;
|
||||
if (metrics->has_soil_temperature)
|
||||
metrics->soil_temperature += offsetC;
|
||||
}
|
||||
} // namespace
|
||||
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
|
||||
|
||||
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
@@ -287,29 +261,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
||||
return disable();
|
||||
}
|
||||
|
||||
auto refreshLocalMeasurementPacket = [this]() {
|
||||
// Keep displaying remote telemetry once we have it.
|
||||
if (lastMeasurementPacket != nullptr && lastMeasurementPacket->from != nodeDB->getNodeNum()) {
|
||||
return;
|
||||
}
|
||||
|
||||
meshtastic_Telemetry local = meshtastic_Telemetry_init_zero;
|
||||
if (!getEnvironmentTelemetry(&local)) {
|
||||
return;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *localPacket = allocDataProtobuf(local);
|
||||
if (localPacket == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastMeasurementPacket != nullptr) {
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
}
|
||||
lastMeasurementPacket = packetPool.allocCopy(*localPacket);
|
||||
packetPool.release(localPacket);
|
||||
};
|
||||
|
||||
if (firstTime) {
|
||||
// This is the first time the OSThread library has called this function, so do some setup
|
||||
firstTime = 0;
|
||||
@@ -339,7 +290,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
||||
result = rak9154Sensor.runOnce();
|
||||
#endif
|
||||
#endif
|
||||
refreshLocalMeasurementPacket();
|
||||
}
|
||||
// it's possible to have this module enabled, only for displaying values on the screen.
|
||||
// therefore, we should only enable the sensor loop if measurement is also enabled
|
||||
@@ -356,7 +306,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
||||
result = delay;
|
||||
}
|
||||
}
|
||||
refreshLocalMeasurementPacket();
|
||||
|
||||
uint32_t lastTelemetry =
|
||||
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
|
||||
@@ -617,9 +566,6 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
|
||||
hasSensor = true;
|
||||
}
|
||||
#endif
|
||||
if (valid)
|
||||
applyTemperatureOffset(&m->variant.environment_metrics);
|
||||
|
||||
return valid && hasSensor;
|
||||
}
|
||||
|
||||
@@ -684,15 +630,11 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
else
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
const bool shouldReplaceDisplayPacket =
|
||||
(lastMeasurementPacket == nullptr || lastMeasurementPacket->from == nodeDB->getNodeNum());
|
||||
if (shouldReplaceDisplayPacket) {
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||
}
|
||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||
if (phoneOnly) {
|
||||
LOG_INFO("Send packet to phone");
|
||||
service->sendToPhone(p);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(¬ifyDeepSleep);
|
||||
touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch);
|
||||
touchScreenImpl1->init();
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
touchBridge.observe(touchScreenImpl1);
|
||||
#endif
|
||||
} else {
|
||||
LOG_ERROR("Failed to find touch controller!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+1
-17
@@ -86,7 +86,7 @@ extern RAK9154Sensor rak9154Sensor;
|
||||
extern XPowersLibInterface *PMU;
|
||||
#endif
|
||||
|
||||
class Power : public concurrency::OSThread
|
||||
class Power : private concurrency::OSThread
|
||||
{
|
||||
|
||||
public:
|
||||
@@ -101,14 +101,6 @@ class Power : public concurrency::OSThread
|
||||
void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; }
|
||||
const uint16_t OCV[11] = {OCV_ARRAY};
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
void attachPowerInterrupts();
|
||||
void detachPowerInterrupts();
|
||||
|
||||
protected:
|
||||
meshtastic::PowerStatus *statusHandler;
|
||||
|
||||
@@ -133,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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-6
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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(¬ifyDeepSleep);
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user