mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-04 16:11:54 +00:00
Compare commits
16 Commits
t5-epaper-
...
baseui_sta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6426e2598b | ||
|
|
bfc3eebd54 | ||
|
|
538a5f0dfc | ||
|
|
b7db22055d | ||
|
|
0d57a49b51 | ||
|
|
d8a0b6a737 | ||
|
|
0703e0e6d7 | ||
|
|
f514bc230b | ||
|
|
697dd2b5b2 | ||
|
|
0b8b757fb0 | ||
|
|
62f897eab3 | ||
|
|
523906d031 | ||
|
|
0022148323 | ||
|
|
78f29c0f87 | ||
|
|
9d06c1bf34 | ||
|
|
1d30342c00 |
@@ -156,8 +156,16 @@ IF %BPS_RESET% EQU 1 (
|
||||
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
||||
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
||||
|
||||
@REM Determine OTA filename based on MCU type (unified OTA format)
|
||||
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
|
||||
IF "__!MCU!__" == "__esp32s3__" (
|
||||
@REM We are working with ESP32-S3
|
||||
SET "OTA_FILENAME=bleota-s3.bin"
|
||||
) ELSE IF "__!MCU!__" == "__esp32c3__" (
|
||||
@REM We are working with ESP32-C3
|
||||
SET "OTA_FILENAME=bleota-c3.bin"
|
||||
) ELSE (
|
||||
@REM Everything else
|
||||
SET "OTA_FILENAME=bleota.bin"
|
||||
)
|
||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||
|
||||
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||
|
||||
@@ -131,8 +131,14 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine OTA filename based on MCU type (unified OTA format)
|
||||
OTAFILE="mt-${MCU}-ota.bin"
|
||||
# Determine OTA filename based on MCU type
|
||||
if [ "$MCU" == "esp32s3" ]; then
|
||||
OTAFILE=bleota-s3.bin
|
||||
elif [ "$MCU" == "esp32c3" ]; then
|
||||
OTAFILE=bleota-c3.bin
|
||||
else
|
||||
OTAFILE=bleota.bin
|
||||
fi
|
||||
|
||||
# Set SPIFFS filename with "littlefs-" prefix.
|
||||
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi",
|
||||
"partitions": "default_16MB.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "LilyGo T5-ePaper-S3",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://lilygo.cc/products/t5-e-paper-s3-pro",
|
||||
"vendor": "LILYGO"
|
||||
}
|
||||
@@ -50,6 +50,7 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DRADIOLIB_EXCLUDE_APRS=1
|
||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||
|
||||
@@ -89,22 +89,14 @@ class BluetoothStatus : public Status
|
||||
case ConnectionState::CONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||
#ifdef BLE_LED
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#else
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#endif
|
||||
digitalWrite(BLE_LED, LED_STATE_ON);
|
||||
#endif
|
||||
break;
|
||||
|
||||
case ConnectionState::DISCONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||
#ifdef BLE_LED
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
// if it's not HIGH - check the battery
|
||||
#endif
|
||||
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
||||
return false;
|
||||
|
||||
// technically speaking this should work for all(?) NRF52 boards
|
||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||
|
||||
@@ -72,13 +72,11 @@ RTCSetResult readFromRTC()
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
uint32_t now = millis();
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||
@@ -242,12 +240,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
|
||||
#ifdef USE_EINK
|
||||
#include "EInkDisplay2.h"
|
||||
#include "SPILock.h"
|
||||
#include "main.h"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
|
||||
#ifdef USE_EINK
|
||||
|
||||
#include "GxEPD2_BW.h"
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
#include "EInkParallelDisplay.h"
|
||||
|
||||
#ifdef USE_EINK_PARALLELDISPLAY
|
||||
|
||||
#include "Wire.h"
|
||||
#include "variant.h"
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "FastEPD.h"
|
||||
|
||||
// Thresholds for choosing partial vs full update
|
||||
#ifndef EPD_PARTIAL_THRESHOLD_ROWS
|
||||
#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial
|
||||
#endif
|
||||
#ifndef EPD_FULLSLOW_PERIOD
|
||||
#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh
|
||||
#endif
|
||||
#ifndef EPD_RESPONSIVE_MIN_MS
|
||||
#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates
|
||||
#endif
|
||||
|
||||
EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot)
|
||||
{
|
||||
LOG_INFO("init EInkParallelDisplay");
|
||||
// Set dimensions in OLEDDisplay base class
|
||||
this->geometry = GEOMETRY_RAWMODE;
|
||||
this->displayWidth = width;
|
||||
this->displayHeight = height;
|
||||
|
||||
// Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer
|
||||
uint16_t shortSide = min(width, height);
|
||||
uint16_t longSide = max(width, height);
|
||||
if (shortSide % 8 != 0)
|
||||
shortSide = (shortSide | 7) + 1;
|
||||
|
||||
this->displayBufferSize = longSide * (shortSide / 8);
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// allocate dirty pixel buffer same size as epaper buffers (rowBytes * height)
|
||||
size_t rowBytes = (this->displayWidth + 7) / 8;
|
||||
dirtyPixelsSize = rowBytes * this->displayHeight;
|
||||
dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1);
|
||||
ghostPixelCount = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
EInkParallelDisplay::~EInkParallelDisplay()
|
||||
{
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
if (dirtyPixels) {
|
||||
free(dirtyPixels);
|
||||
dirtyPixels = nullptr;
|
||||
}
|
||||
#endif
|
||||
// If an async full update is running, wait for it to finish
|
||||
if (asyncFullRunning.load()) {
|
||||
// wait a short while for task to finish
|
||||
for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) {
|
||||
delay(50);
|
||||
}
|
||||
if (asyncTaskHandle) {
|
||||
// Let it finish or delete it
|
||||
vTaskDelete(asyncTaskHandle);
|
||||
asyncTaskHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
delete epaper;
|
||||
}
|
||||
|
||||
/*
|
||||
* Called by the OLEDDisplay::init() path.
|
||||
*/
|
||||
bool EInkParallelDisplay::connect()
|
||||
{
|
||||
LOG_INFO("Do EPD init");
|
||||
if (!epaper) {
|
||||
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);
|
||||
epaper->ioPinMode(0, OUTPUT);
|
||||
epaper->ioWrite(0, HIGH);
|
||||
#else
|
||||
#error "unsupported EPD device!"
|
||||
#endif
|
||||
}
|
||||
|
||||
// epaper->setRotation(rotation); // does not work, messes up width/height
|
||||
epaper->setMode(BB_MODE_1BPP);
|
||||
epaper->clearWhite();
|
||||
epaper->fullUpdate(true);
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// After a full/clear the dirty tracking should be reset
|
||||
resetGhostPixelTracking();
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* sendCommand - simple passthrough (not required for epd_driver-based path)
|
||||
*/
|
||||
void EInkParallelDisplay::sendCommand(uint8_t com)
|
||||
{
|
||||
LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com);
|
||||
}
|
||||
|
||||
/*
|
||||
* Start a background task that will perform a blocking fullUpdate(). This lets
|
||||
* display() return quickly while the heavy refresh runs in the background.
|
||||
*/
|
||||
void EInkParallelDisplay::startAsyncFullUpdate(int clearMode)
|
||||
{
|
||||
if (asyncFullRunning.load())
|
||||
return; // already running
|
||||
|
||||
asyncFullRunning.store(true);
|
||||
// pass 'this' as parameter
|
||||
BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t),
|
||||
this, 2, &asyncTaskHandle,
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
0
|
||||
#else
|
||||
1
|
||||
#endif
|
||||
);
|
||||
if (rc != pdPASS) {
|
||||
LOG_WARN("Failed to create async full-update task, falling back to blocking update");
|
||||
epaper->fullUpdate(clearMode, false);
|
||||
epaper->backupPlane();
|
||||
asyncFullRunning.store(false);
|
||||
asyncTaskHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* FreeRTOS task entry: runs the full update and then backs up plane.
|
||||
*/
|
||||
void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters)
|
||||
{
|
||||
EInkParallelDisplay *self = static_cast<EInkParallelDisplay *>(pvParameters);
|
||||
if (!self) {
|
||||
vTaskDelete(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
// choose CLEAR_SLOW occasionally
|
||||
int clearMode = CLEAR_FAST;
|
||||
if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) {
|
||||
clearMode = CLEAR_SLOW;
|
||||
self->fastRefreshCount = 0;
|
||||
} else {
|
||||
// when running async full, treat it as a full so reset fast count
|
||||
self->fastRefreshCount = 0;
|
||||
}
|
||||
|
||||
self->epaper->fullUpdate(clearMode, false);
|
||||
self->epaper->backupPlane();
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// A full refresh clears ghosting state
|
||||
self->resetGhostPixelTracking();
|
||||
#endif
|
||||
|
||||
self->asyncFullRunning.store(false);
|
||||
self->asyncTaskHandle = nullptr;
|
||||
|
||||
// delete this task
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes
|
||||
* buffer used by the FASTEPD library. For performance we write directly into FASTEPD's
|
||||
* currentBuffer() while comparing against previousBuffer() to detect changed rows.
|
||||
* After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according
|
||||
* to a heuristic so only the minimal region is refreshed.
|
||||
*/
|
||||
void EInkParallelDisplay::display(void)
|
||||
{
|
||||
const uint16_t w = this->displayWidth;
|
||||
const uint16_t h = this->displayHeight;
|
||||
|
||||
// Simple rate limiting: avoid very-frequent responsive updates
|
||||
uint32_t nowMs = millis();
|
||||
if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) {
|
||||
LOG_DEBUG("rate-limited, skipping update");
|
||||
return;
|
||||
}
|
||||
|
||||
// bytes per row in epd format (one byte = 8 horizontal pixels)
|
||||
const uint32_t rowBytes = (w + 7) / 8;
|
||||
|
||||
// Get pointers to internal buffers
|
||||
uint8_t *cur = epaper->currentBuffer();
|
||||
uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init
|
||||
|
||||
// Track changed row range while converting
|
||||
int newTop = h; // min changed row (initialized to out-of-range)
|
||||
int newBottom = -1; // max changed row
|
||||
|
||||
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
|
||||
// Track changed byte column range (for clipped fullUpdate fallback)
|
||||
int newLeftByte = (int)rowBytes;
|
||||
int newRightByte = -1;
|
||||
#endif
|
||||
|
||||
// Compute a quick hash of the incoming OLED buffer (so we can skip identical frames)
|
||||
uint32_t imageHash = 0;
|
||||
uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size
|
||||
for (uint32_t bi = 0; bi < bufBytes; ++bi) {
|
||||
imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31);
|
||||
}
|
||||
if (imageHash == previousImageHash) {
|
||||
// LOG_DEBUG("image identical to previous, skipping update");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// reset ghost count for this conversion pass; we'll mark bits that change
|
||||
ghostPixelCount = 0;
|
||||
#endif
|
||||
|
||||
// Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur,
|
||||
// comparing against prev when available to detect changes.
|
||||
for (uint32_t y = 0; y < h; ++y) {
|
||||
const uint32_t base = (y >> 3) * w; // (y/8) * width
|
||||
const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout
|
||||
const uint32_t rowBase = y * rowBytes;
|
||||
|
||||
// process full 8-pixel bytes
|
||||
for (uint32_t xb = 0; xb < rowBytes; ++xb) {
|
||||
uint32_t x0 = xb * 8;
|
||||
// read up to 8 source bytes (vertical-byte per column)
|
||||
uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0;
|
||||
uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0;
|
||||
uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0;
|
||||
uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0;
|
||||
uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0;
|
||||
uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0;
|
||||
uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0;
|
||||
uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0;
|
||||
|
||||
// build output byte: MSB = leftmost pixel
|
||||
uint8_t out = 0;
|
||||
out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00);
|
||||
out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00);
|
||||
out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00);
|
||||
out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00);
|
||||
out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00);
|
||||
out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00);
|
||||
out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00);
|
||||
out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00);
|
||||
|
||||
// handle partial byte at end of row by masking off invalid bits
|
||||
uint8_t mask = 0xFF;
|
||||
uint32_t bitsRemain = (w > x0) ? (w - x0) : 0;
|
||||
if (bitsRemain > 0 && bitsRemain < 8) {
|
||||
mask = (uint8_t)(0xFF << (8 - bitsRemain));
|
||||
out &= mask;
|
||||
}
|
||||
|
||||
// invert to FASTEPD polarity
|
||||
out = (~out) & mask;
|
||||
|
||||
uint32_t pos = rowBase + xb;
|
||||
uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00;
|
||||
// Consider this byte changed if previous buffer differs (or prev is null)
|
||||
bool changed = (prev == nullptr) || (prevVal != out);
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
if (changed && prev)
|
||||
markDirtyBits(prev, pos, mask, out);
|
||||
#endif
|
||||
|
||||
// mark row changed only if the previous buffer differs
|
||||
if (changed) {
|
||||
if (y < (uint32_t)newTop)
|
||||
newTop = y;
|
||||
if ((int)y > newBottom)
|
||||
newBottom = y;
|
||||
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
|
||||
// record changed column bytes
|
||||
if ((int)xb < newLeftByte)
|
||||
newLeftByte = (int)xb;
|
||||
if ((int)xb > newRightByte)
|
||||
newRightByte = (int)xb;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Always write the computed value into the current buffer (avoid leaving stale bytes)
|
||||
cur[pos] = (cur[pos] & ~mask) | out;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing changed, avoid any panel update
|
||||
if (newBottom < 0) {
|
||||
LOG_DEBUG("no pixel changes detected, skipping update (conv)");
|
||||
previousImageHash = imageHash; // still remember that frame
|
||||
return;
|
||||
}
|
||||
|
||||
// Choose partial vs full update using heuristic
|
||||
// Decide if we should force a full update after many fast updates
|
||||
bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD);
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// If ghost pixels exceed limit, force a full update to clear ghosting
|
||||
if (ghostPixelCount > ghostPixelLimit) {
|
||||
LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit);
|
||||
forceFull = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Compute pixel bounds from newTop/newBottom
|
||||
int startRow = (newTop / 8) * 8;
|
||||
int endRow = (newBottom / 8) * 8 + 7;
|
||||
|
||||
LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes);
|
||||
|
||||
if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) {
|
||||
// Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback.
|
||||
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
|
||||
// Workaround for FastEPD partial update bug: use clipped fullUpdate instead
|
||||
// Build a pixel rectangle for a clipped fullUpdate using the changed columns
|
||||
int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0;
|
||||
int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1);
|
||||
|
||||
BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1};
|
||||
// LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h);
|
||||
epaper->fullUpdate(CLEAR_FAST, false, &rect);
|
||||
#else
|
||||
// Use rows for partial update
|
||||
LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow);
|
||||
epaper->partialUpdate(true, startRow, endRow);
|
||||
#endif
|
||||
epaper->backupPlane();
|
||||
fastRefreshCount++;
|
||||
} else {
|
||||
// Full update: run async if possible (startAsyncFullUpdate will fall back to blocking)
|
||||
startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST);
|
||||
}
|
||||
|
||||
lastUpdateMs = millis();
|
||||
previousImageHash = imageHash;
|
||||
|
||||
// Keep same behavior as before
|
||||
lastDrawMsec = millis();
|
||||
}
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount
|
||||
void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out)
|
||||
{
|
||||
// defensive: need dirtyPixels allocated and prevBuf valid
|
||||
if (!dirtyPixels || !prevBuf)
|
||||
return;
|
||||
|
||||
// 'out' is in FASTEPD polarity (1 = black, 0 = white)
|
||||
uint8_t newBlack = out & mask; // bits that will be black now
|
||||
uint8_t newWhite = (~out) & mask; // bits that will be white now
|
||||
|
||||
// previously recorded dirty bits for this byte
|
||||
uint8_t before = dirtyPixels[pos];
|
||||
|
||||
// Ghost bits: bits that were previously marked dirty and are now being driven white
|
||||
uint8_t ghostBits = before & newWhite;
|
||||
if (ghostBits) {
|
||||
ghostPixelCount += __builtin_popcount((unsigned)ghostBits);
|
||||
}
|
||||
|
||||
// Only mark bits dirty when they turn black now (accumulate until a full refresh)
|
||||
uint8_t newlyDirty = newBlack & (~before);
|
||||
if (newlyDirty) {
|
||||
dirtyPixels[pos] |= newlyDirty;
|
||||
}
|
||||
}
|
||||
|
||||
// reset ghost tracking (call after a full refresh)
|
||||
void EInkParallelDisplay::resetGhostPixelTracking()
|
||||
{
|
||||
if (!dirtyPixels)
|
||||
return;
|
||||
memset(dirtyPixels, 0, dirtyPixelsSize);
|
||||
ghostPixelCount = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* forceDisplay: use lastDrawMsec
|
||||
*/
|
||||
bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) {
|
||||
display();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void EInkParallelDisplay::endUpdate()
|
||||
{
|
||||
{
|
||||
// ensure any async full update is started/completed
|
||||
if (asyncFullRunning.load()) {
|
||||
// nothing to do; background task will run and call backupPlane when done
|
||||
} else {
|
||||
epaper->fullUpdate(CLEAR_FAST, false);
|
||||
epaper->backupPlane();
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
resetGhostPixelTracking();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,69 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef USE_EINK_PARALLELDISPLAY
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
class FASTEPD;
|
||||
|
||||
/**
|
||||
* Adapter for E-Ink 8-bit parallel displays (EPD), specifically devices supported by FastEPD library
|
||||
*/
|
||||
class EInkParallelDisplay : public OLEDDisplay
|
||||
{
|
||||
public:
|
||||
enum EpdRotation {
|
||||
EPD_ROT_LANDSCAPE = 0,
|
||||
EPD_ROT_PORTRAIT = 90,
|
||||
EPD_ROT_INVERTED_LANDSCAPE = 180,
|
||||
EPD_ROT_INVERTED_PORTRAIT = 270,
|
||||
};
|
||||
|
||||
EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rotation);
|
||||
virtual ~EInkParallelDisplay();
|
||||
|
||||
// OLEDDisplay virtuals
|
||||
bool connect() override;
|
||||
void sendCommand(uint8_t com) override;
|
||||
int getBufferOffset(void) override { return 0; }
|
||||
|
||||
void display(void) override;
|
||||
bool forceDisplay(uint32_t msecLimit = 1000);
|
||||
void endUpdate();
|
||||
|
||||
protected:
|
||||
uint32_t lastDrawMsec = 0;
|
||||
FASTEPD *epaper;
|
||||
|
||||
private:
|
||||
// Async full-refresh support
|
||||
std::atomic<bool> asyncFullRunning{false};
|
||||
TaskHandle_t asyncTaskHandle = nullptr;
|
||||
void startAsyncFullUpdate(int clearMode);
|
||||
static void asyncFullUpdateTask(void *pvParameters);
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// helpers
|
||||
void resetGhostPixelTracking();
|
||||
void markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out);
|
||||
void countGhostPixelsAndMaybePromote(int &newTop, int &newBottom, bool &forceFull);
|
||||
|
||||
// per-bit dirty buffer (same format as epaper buffers): one bit == one pixel
|
||||
uint8_t *dirtyPixels = nullptr;
|
||||
size_t dirtyPixelsSize = 0;
|
||||
uint32_t ghostPixelCount = 0;
|
||||
uint32_t ghostPixelLimit = EINK_LIMIT_GHOSTING_PX;
|
||||
#endif
|
||||
|
||||
EpdRotation rotation;
|
||||
uint32_t previousImageHash = 0;
|
||||
uint32_t lastUpdateMs = 0;
|
||||
int fastRefreshCount = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -27,7 +27,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "configuration.h"
|
||||
#include "meshUtils.h"
|
||||
#if HAS_SCREEN
|
||||
#include "EInkParallelDisplay.h"
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
@@ -365,14 +364,12 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR)
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY)
|
||||
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
|
||||
dispdev = new EInkDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||||
dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_EINK_PARALLELDISPLAY)
|
||||
dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT);
|
||||
#elif defined(USE_ST7567)
|
||||
dispdev = new ST7567Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -753,11 +750,7 @@ void Screen::forceDisplay(bool forceUiUpdate)
|
||||
}
|
||||
|
||||
// Tell EInk class to update the display
|
||||
#if defined(USE_EINK_PARALLELDISPLAY)
|
||||
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay();
|
||||
#elif defined(USE_EINK)
|
||||
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
|
||||
#endif
|
||||
#else
|
||||
// No delay between UI frame rendering
|
||||
if (forceUiUpdate) {
|
||||
@@ -992,10 +985,8 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
ui->update();
|
||||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||||
|
||||
#if defined(USE_EINK_PARALLELDISPLAY)
|
||||
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay(0);
|
||||
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
|
||||
// Old EInkDisplay class
|
||||
#if !defined(USE_EINK_DYNAMICDISPLAY)
|
||||
static_cast<EInkDisplay *>(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit
|
||||
#endif
|
||||
|
||||
@@ -1007,7 +998,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
#ifdef EINK_HASQUIRK_GHOSTING
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused"
|
||||
#else
|
||||
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
|
||||
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -1184,7 +1175,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,7 +1204,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
|
||||
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed)
|
||||
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
||||
|
||||
// Focus on a specific frame, in the frame set we just created
|
||||
switch (focus) {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "graphics/fonts/OLEDDisplayFontsGR.h"
|
||||
#endif
|
||||
|
||||
#if (defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)) && defined(USE_EINK)
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||
#include "graphics/fonts/EinkDisplayFonts.h"
|
||||
#endif
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \
|
||||
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
// The screen is bigger so use bigger fonts
|
||||
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
|
||||
@@ -106,7 +106,7 @@
|
||||
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
|
||||
#endif
|
||||
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||
#undef FONT_SMALL
|
||||
#undef FONT_MEDIUM
|
||||
#undef FONT_LARGE
|
||||
|
||||
@@ -535,9 +535,6 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
#ifndef T_DECK_PRO
|
||||
barsOffset -= 12;
|
||||
#endif
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
barsOffset += 60;
|
||||
#endif
|
||||
#endif
|
||||
int barX = x + barsOffset;
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
@@ -587,12 +584,11 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap();
|
||||
uint32_t heapTotal = memGet.getHeapSize();
|
||||
|
||||
uint32_t flashUsed = 0, flashTotal = 0;
|
||||
#ifdef ESP32
|
||||
#ifndef T5_S3_EPAPER_PRO
|
||||
uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram();
|
||||
uint32_t psramTotal = memGet.getPsramSize();
|
||||
#endif
|
||||
|
||||
uint32_t flashUsed = 0, flashTotal = 0;
|
||||
#ifdef ESP32
|
||||
flashUsed = FSCom.usedBytes();
|
||||
flashTotal = FSCom.totalBytes();
|
||||
#endif
|
||||
@@ -611,12 +607,10 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
// === Draw memory rows
|
||||
drawUsageRow("Heap:", heapUsed, heapTotal, true);
|
||||
#ifdef ESP32
|
||||
#ifndef T5_S3_EPAPER_PRO
|
||||
if (psramUsed > 0) {
|
||||
line += 1;
|
||||
drawUsageRow("PSRAM:", psramUsed, psramTotal);
|
||||
}
|
||||
#endif
|
||||
if (flashTotal > 0) {
|
||||
line += 1;
|
||||
drawUsageRow("Flash:", flashUsed, flashTotal);
|
||||
|
||||
@@ -1498,7 +1498,6 @@ void menuHandler::nodeNameLengthMenu()
|
||||
|
||||
config.display.use_long_node_name = option.value;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
|
||||
});
|
||||
|
||||
|
||||
@@ -877,15 +877,15 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// Send Message (Right side)
|
||||
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
|
||||
// Top Right Corner
|
||||
display->drawRect(x1, topY, 2, 1);
|
||||
display->drawRect(x1 - 1, topY, 2, 1);
|
||||
display->drawRect(x1, topY, 1, 2);
|
||||
// Bottom Right Corner
|
||||
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
|
||||
display->drawRect(x1, bottomY - 3, 1, 2);
|
||||
// Knock the corners off to make a bubble
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
|
||||
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
|
||||
display->drawRect(x1 - bubbleW + 2, topY - 1, 1, 1);
|
||||
display->drawRect(x1 - bubbleW + 2, bottomY - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
} else {
|
||||
// Received Message (Left Side)
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h" // for getTime() function
|
||||
@@ -90,8 +93,41 @@ const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
// If long-name mode is enabled, and we have a recent status for this node,
|
||||
// prefer "(short_name) statusText" as the raw candidate.
|
||||
std::string composedFromStatus;
|
||||
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||
found = &(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
const char *shortName = node->user.short_name;
|
||||
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
|
||||
composedFromStatus += "(";
|
||||
if (shortName && *shortName) {
|
||||
composedFromStatus += shortName;
|
||||
}
|
||||
composedFromStatus += ") ";
|
||||
composedFromStatus += found->statusText;
|
||||
|
||||
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we didn't compose from status, use normal long/short selection
|
||||
if (!raw) {
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Sanitize (empty if raw is null/empty)
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include "GPSStatus.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -287,7 +290,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
// **********************
|
||||
// * Favorite Node Info *
|
||||
// **********************
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
void UIRenderer::drawFavoriteNode(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (favoritedNodes.empty())
|
||||
return;
|
||||
@@ -341,6 +344,57 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
|
||||
}
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
// === Optional: Last received StatusMessage line for this node ===
|
||||
// Display it directly under the username line (if we have one).
|
||||
if (statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
|
||||
// Search newest-to-oldest
|
||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||
found = &(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
std::string statusLine = std::string(" Status: ") + found->statusText;
|
||||
{
|
||||
const int screenW = display->getWidth();
|
||||
const int ellipseW = display->getStringWidth("...");
|
||||
int w = display->getStringWidth(statusLine.c_str());
|
||||
|
||||
// Only do work if it overflows
|
||||
if (w > screenW) {
|
||||
bool truncated = false;
|
||||
if (ellipseW > screenW) {
|
||||
statusLine.clear();
|
||||
} else {
|
||||
while (!statusLine.empty()) {
|
||||
// remove one char (byte) at a time
|
||||
statusLine.pop_back();
|
||||
truncated = true;
|
||||
|
||||
// Measure candidate with ellipsis appended
|
||||
std::string candidate = statusLine + "...";
|
||||
if (display->getStringWidth(candidate.c_str()) <= screenW) {
|
||||
statusLine = std::move(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (statusLine.empty() && ellipseW <= screenW) {
|
||||
statusLine = "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], statusLine.c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||
// If both are present: "Sig: 97% [2hops]"
|
||||
// If only one: show only that one
|
||||
|
||||
@@ -49,7 +49,7 @@ class UIRenderer
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawFavoriteNode(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "graphics/niche/InkHUD/Tile.h"
|
||||
#include <cstdint>
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Applet.h"
|
||||
@@ -785,6 +787,16 @@ void InkHUD::Applet::drawHeader(std::string text)
|
||||
drawPixel(x, 0, BLACK);
|
||||
drawPixel(x, headerDivY, BLACK); // Dotted 50%
|
||||
}
|
||||
|
||||
// Dither near battery
|
||||
if (settings->optionalFeatures.batteryIcon) {
|
||||
constexpr uint16_t ditherSizePx = 4;
|
||||
Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile();
|
||||
const uint16_t batteryTileLeft = batteryTile->getLeft();
|
||||
const uint16_t batteryTileTop = batteryTile->getTop();
|
||||
const uint16_t batteryTileHeight = batteryTile->getHeight();
|
||||
hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the height of the standard applet header
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
#include <GFX.h> // GFXRoot drawing lib
|
||||
|
||||
#include "mesh/MeshModule.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
|
||||
#include "./AppletFont.h"
|
||||
|
||||
@@ -48,37 +48,27 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
{
|
||||
// Fill entire tile
|
||||
// - size of icon controlled by size of tile
|
||||
int16_t l = 0;
|
||||
int16_t t = 0;
|
||||
uint16_t w = width();
|
||||
int16_t h = height();
|
||||
|
||||
// Clear the region beneath the tile
|
||||
// Clear the region beneath the tile, including the border
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
fillRect(0, 0, width(), height(), WHITE);
|
||||
|
||||
// =====================
|
||||
// Draw battery outline
|
||||
// =====================
|
||||
|
||||
// Positive terminal "bump"
|
||||
const int16_t &bumpL = l;
|
||||
const uint16_t bumpH = h / 2;
|
||||
const int16_t bumpT = m - (bumpH / 2);
|
||||
constexpr uint16_t bumpW = 2;
|
||||
const int16_t &bumpL = 1;
|
||||
const uint16_t bumpH = (height() - 2) / 2;
|
||||
const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2);
|
||||
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||
|
||||
// Main body of battery
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
const int16_t bodyL = 1 + bumpW;
|
||||
const int16_t &bodyT = 1;
|
||||
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
|
||||
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
@@ -89,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
const int16_t sliceL = bodyL + slicePad;
|
||||
int16_t sliceL = bodyL + slicePad;
|
||||
const int16_t sliceT = bodyT + slicePad;
|
||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
|
||||
|
||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
|
||||
@@ -510,10 +510,10 @@ void InkHUD::WindowManager::placeSystemTiles()
|
||||
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
|
||||
inkhud->getSystemApplet("BatteryIcon")
|
||||
->getTile()
|
||||
->setRegion(inkhud->width() - batteryIconWidth, // x
|
||||
2, // y
|
||||
batteryIconWidth, // width
|
||||
batteryIconHeight); // height
|
||||
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
|
||||
1, // y
|
||||
batteryIconWidth + 1, // width
|
||||
batteryIconHeight + 2); // height
|
||||
|
||||
// Note: the tiles of placeholder and menu applets are manipulated specially
|
||||
// - menuApplet borrows user tiles
|
||||
|
||||
72
src/main.cpp
72
src/main.cpp
@@ -366,73 +366,7 @@ 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);
|
||||
#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(T5_S3_EPAPER_PRO)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(BOARD_BL_EN, OUTPUT);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
pinMode(KB_INT, INPUT);
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
|
||||
concurrency::hasBeenSetup = true;
|
||||
@@ -555,7 +489,9 @@ void setup()
|
||||
// The ThinkNodes have their own blink logic
|
||||
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
|
||||
#else
|
||||
|
||||
ledPeriodic = new Periodic("Blink", ledBlinker);
|
||||
|
||||
#endif
|
||||
|
||||
fsInit();
|
||||
@@ -896,7 +832,7 @@ void setup()
|
||||
SPI.begin();
|
||||
#endif
|
||||
#else
|
||||
// ESP32
|
||||
// ESP32
|
||||
#if defined(HW_SPI1_DEVICE)
|
||||
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
|
||||
LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
|
||||
|
||||
@@ -1404,6 +1404,15 @@ void NodeDB::loadFromDisk()
|
||||
if (portduino_config.has_configDisplayMode) {
|
||||
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
|
||||
}
|
||||
if (portduino_config.has_statusMessage) {
|
||||
moduleConfig.has_statusmessage = true;
|
||||
strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(),
|
||||
sizeof(moduleConfig.statusmessage.node_status));
|
||||
moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0';
|
||||
}
|
||||
if (portduino_config.enable_UDP) {
|
||||
config.network.enabled_protocols = true;
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
@@ -1544,6 +1553,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||
moduleConfig.has_ambient_lighting = true;
|
||||
moduleConfig.has_audio = true;
|
||||
moduleConfig.has_paxcounter = true;
|
||||
moduleConfig.has_statusmessage = true;
|
||||
|
||||
success &=
|
||||
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
|
||||
|
||||
@@ -905,10 +905,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
|
||||
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
{
|
||||
bool shouldReboot = true;
|
||||
// If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
|
||||
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
|
||||
if (!hasOpenEditTransaction &&
|
||||
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
|
||||
if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag,
|
||||
meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) {
|
||||
disableBluetooth();
|
||||
}
|
||||
|
||||
@@ -1000,8 +1001,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
moduleConfig.has_paxcounter = true;
|
||||
moduleConfig.paxcounter = c.payload_variant.paxcounter;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_statusmessage_tag:
|
||||
LOG_INFO("Set module config: StatusMessage");
|
||||
moduleConfig.has_statusmessage = true;
|
||||
moduleConfig.statusmessage = c.payload_variant.statusmessage;
|
||||
shouldReboot = false;
|
||||
break;
|
||||
}
|
||||
saveChanges(SEGMENT_MODULECONFIG);
|
||||
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1180,6 +1187,11 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
|
||||
LOG_INFO("Get module config: StatusMessage");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
|
||||
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
|
||||
break;
|
||||
}
|
||||
|
||||
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include "modules/StatusLEDModule.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
#include "ReplyBotModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
#endif
|
||||
@@ -90,6 +93,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||
#include "modules/DropzoneModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
|
||||
#if defined(HAS_HARDWARE_WATCHDOG)
|
||||
#include "watchdog/watchdogThread.h"
|
||||
@@ -109,7 +115,9 @@ void setupModules()
|
||||
#if defined(LED_CHARGE) || defined(LED_PAIRING)
|
||||
statusLEDModule = new StatusLEDModule();
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
new ReplyBotModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminModule = new AdminModule();
|
||||
#endif
|
||||
@@ -150,6 +158,9 @@ void setupModules()
|
||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||
dropzoneModule = new DropzoneModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
statusMessageModule = new StatusMessageModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
|
||||
new GenericThreadModule();
|
||||
#endif
|
||||
|
||||
183
src/modules/ReplyBotModule.cpp
Normal file
183
src/modules/ReplyBotModule.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
/*
|
||||
* ReplyBotModule.cpp
|
||||
*
|
||||
* This module implements a simple reply bot for the Meshtastic firmware. It listens for
|
||||
* specific text commands ("/ping", "/hello" and "/test") delivered either via a direct
|
||||
* message (DM) or a broadcast on the primary channel. When a supported command is
|
||||
* received the bot responds with a short status message that includes the hop count
|
||||
* (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming
|
||||
* the network it enforces a per‑sender cooldown between responses. By default the
|
||||
* module is enabled; define MESHTASTIC_EXCLUDE_REPLYBOT at build time to exclude it
|
||||
* entirely. See the official firmware documentation for guidance on adding modules.
|
||||
*/
|
||||
|
||||
#include "Channels.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ReplyBotModule.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
|
||||
//
|
||||
// Rate limiting data structures
|
||||
//
|
||||
// Each sender is tracked in a small ring buffer. When a message arrives from a
|
||||
// sender we check the last time we responded to them. If the difference is
|
||||
// less than the configured cooldown (different values for DM vs broadcast)
|
||||
// the message is ignored; otherwise we update the last response time and
|
||||
// proceed with replying.
|
||||
|
||||
struct ReplyBotCooldownEntry {
|
||||
uint32_t from = 0;
|
||||
uint32_t lastMs = 0;
|
||||
};
|
||||
|
||||
static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size
|
||||
static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs
|
||||
static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts
|
||||
|
||||
static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS];
|
||||
static uint8_t replybotCooldownIdx = 0;
|
||||
|
||||
// Return true if a reply should be rate‑limited for this sender, updating the
|
||||
// entry table as needed.
|
||||
static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs)
|
||||
{
|
||||
const uint32_t now = millis();
|
||||
for (auto &e : replybotCooldown) {
|
||||
if (e.from == from) {
|
||||
// Found existing entry; check if cooldown expired
|
||||
if ((uint32_t)(now - e.lastMs) < cooldownMs) {
|
||||
return true;
|
||||
}
|
||||
e.lastMs = now;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// No entry found – insert new sender into the ring
|
||||
replybotCooldown[replybotCooldownIdx].from = from;
|
||||
replybotCooldown[replybotCooldownIdx].lastMs = now;
|
||||
replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Constructor – registers a single text port and marks the module promiscuous
|
||||
// so that broadcast messages on the primary channel are visible.
|
||||
ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP)
|
||||
{
|
||||
isPromiscuous = true;
|
||||
}
|
||||
|
||||
void ReplyBotModule::setup()
|
||||
{
|
||||
// In future we may add a protobuf configuration; for now the module is
|
||||
// always enabled when compiled in.
|
||||
}
|
||||
|
||||
// Determine whether we want to process this packet. We only care about
|
||||
// plain text messages addressed to our port.
|
||||
bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
return (p && p->decoded.portnum == ourPortNum);
|
||||
}
|
||||
|
||||
ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Accept only direct messages to us or broadcasts on the Primary channel
|
||||
// (regardless of modem preset: LongFast, MediumFast, etc).
|
||||
|
||||
const uint32_t ourNode = nodeDB->getNodeNum();
|
||||
const bool isDM = (mp.to == ourNode);
|
||||
const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to);
|
||||
if (!isDM && !isPrimaryChannel) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Ignore empty payloads
|
||||
if (mp.decoded.payload.size == 0) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Copy payload into a null‑terminated buffer
|
||||
char buf[260];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
size_t n = mp.decoded.payload.size;
|
||||
if (n > sizeof(buf) - 1)
|
||||
n = sizeof(buf) - 1;
|
||||
memcpy(buf, mp.decoded.payload.bytes, n);
|
||||
|
||||
// React only to supported slash commands
|
||||
if (!isCommand(buf)) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Apply rate limiting per sender depending on DM/broadcast
|
||||
const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS;
|
||||
if (replybotRateLimited(mp.from, cooldownMs)) {
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Compute hop count indicator – if the relay_node is non‑zero we know
|
||||
// there was at least one relay. Some firmware builds support a hop_start
|
||||
// field which could be used for more accurate counts, but here we use
|
||||
// the available relay_node flag only.
|
||||
// int hopsAway = mp.hop_start - mp.hop_limit;
|
||||
int hopsAway = getHopsAway(mp);
|
||||
|
||||
// Normalize RSSI: if positive adjust down by 200 to align with typical values
|
||||
int rssi = mp.rx_rssi;
|
||||
if (rssi > 0) {
|
||||
rssi -= 200;
|
||||
}
|
||||
float snr = mp.rx_snr;
|
||||
|
||||
// Build the reply message and send it back via DM
|
||||
char reply[96];
|
||||
snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr);
|
||||
sendDm(mp, reply);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Check if the message starts with one of the supported commands. Leading
|
||||
// whitespace is skipped and commands must be followed by end‑of‑string or
|
||||
// whitespace.
|
||||
bool ReplyBotModule::isCommand(const char *msg) const
|
||||
{
|
||||
if (!msg)
|
||||
return false;
|
||||
while (*msg == ' ' || *msg == '\t')
|
||||
msg++;
|
||||
auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast<unsigned char>(c)); };
|
||||
if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5]))
|
||||
return true;
|
||||
if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6]))
|
||||
return true;
|
||||
if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5]))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send a direct message back to the originating node.
|
||||
void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text)
|
||||
{
|
||||
if (!text)
|
||||
return;
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->to = rx.from;
|
||||
p->channel = rx.channel;
|
||||
p->want_ack = false;
|
||||
p->decoded.want_response = false;
|
||||
size_t len = strlen(text);
|
||||
if (len > sizeof(p->decoded.payload.bytes)) {
|
||||
len = sizeof(p->decoded.payload.bytes);
|
||||
}
|
||||
p->decoded.payload.size = len;
|
||||
memcpy(p->decoded.payload.bytes, text, len);
|
||||
service->sendToMesh(p);
|
||||
}
|
||||
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
19
src/modules/ReplyBotModule.h
Normal file
19
src/modules/ReplyBotModule.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
#include "SinglePortModule.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
|
||||
class ReplyBotModule : public SinglePortModule
|
||||
{
|
||||
public:
|
||||
ReplyBotModule();
|
||||
void setup() override;
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
protected:
|
||||
bool isCommand(const char *msg) const;
|
||||
void sendDm(const meshtastic_MeshPacket &rx, const char *text);
|
||||
};
|
||||
#endif // MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
54
src/modules/StatusMessageModule.cpp
Normal file
54
src/modules/StatusMessageModule.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
|
||||
#include "StatusMessageModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "ProtobufModule.h"
|
||||
|
||||
StatusMessageModule *statusMessageModule;
|
||||
|
||||
int32_t StatusMessageModule::runOnce()
|
||||
{
|
||||
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
|
||||
// create and send message with the status message set
|
||||
meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero;
|
||||
strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status));
|
||||
ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes),
|
||||
meshtastic_StatusMessage_fields, &ourStatus);
|
||||
p->to = NODENUM_BROADCAST;
|
||||
p->decoded.want_response = false;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
p->channel = 0;
|
||||
service->sendToMesh(p);
|
||||
}
|
||||
|
||||
return 1000 * 12 * 60 * 60;
|
||||
}
|
||||
|
||||
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
meshtastic_StatusMessage incomingMessage = meshtastic_StatusMessage_init_zero;
|
||||
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
|
||||
&incomingMessage)) {
|
||||
|
||||
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
||||
|
||||
RecentStatus entry;
|
||||
entry.fromNodeId = mp.from;
|
||||
entry.statusText = incomingMessage.status;
|
||||
|
||||
recentReceived.push_back(std::move(entry));
|
||||
|
||||
// Keep only last MAX_RECENT_STATUSMESSAGES
|
||||
if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) {
|
||||
recentReceived.erase(recentReceived.begin()); // drop oldest
|
||||
}
|
||||
}
|
||||
}
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
#endif
|
||||
48
src/modules/StatusMessageModule.h
Normal file
48
src/modules/StatusMessageModule.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "SinglePortModule.h"
|
||||
#include "configuration.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
StatusMessageModule()
|
||||
: SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage")
|
||||
{
|
||||
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
|
||||
this->setInterval(2 * 60 * 1000);
|
||||
} else {
|
||||
this->setInterval(1000 * 12 * 60 * 60);
|
||||
}
|
||||
// TODO: If we have a string, set the initial delay (15 minutes maybe)
|
||||
|
||||
// Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES
|
||||
recentReceived.reserve(MAX_RECENT_STATUSMESSAGES);
|
||||
}
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
struct RecentStatus {
|
||||
uint32_t fromNodeId; // mp.from
|
||||
std::string statusText; // incomingMessage.status
|
||||
};
|
||||
|
||||
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
|
||||
std::vector<RecentStatus> recentReceived;
|
||||
};
|
||||
|
||||
extern StatusMessageModule *statusMessageModule;
|
||||
#endif
|
||||
@@ -757,11 +757,7 @@ void NimbleBluetooth::deinit()
|
||||
isDeInit = true;
|
||||
|
||||
#ifdef BLE_LED
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
#ifndef NIMBLE_TWO
|
||||
NimBLEDevice::deinit();
|
||||
|
||||
@@ -24,11 +24,6 @@
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
// Weak empty variant shutdown prep function.
|
||||
// May be redefined by variant files.
|
||||
void variant_shutdown() __attribute__((weak));
|
||||
void variant_shutdown() {}
|
||||
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
void setBluetoothEnable(bool enable)
|
||||
{
|
||||
@@ -254,7 +249,6 @@ void cpuDeepSleep(uint32_t msecToWake)
|
||||
|
||||
#endif // #end ESP32S3_WAKE_TYPE
|
||||
#endif
|
||||
variant_shutdown();
|
||||
|
||||
// We want RTC peripherals to stay on
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef T5_S3_EPAPER_PRO
|
||||
|
||||
#include "TouchDrvGT911.hpp"
|
||||
#include "Wire.h"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
|
||||
TouchDrvGT911 touch;
|
||||
|
||||
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)) {
|
||||
// rotate 90° for landscape
|
||||
*x = raw_y;
|
||||
*y = EPD_WIDTH - 1 - raw_x;
|
||||
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_L, GT911_PIN_SDA, GT911_PIN_SCL)) {
|
||||
touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch);
|
||||
touchScreenImpl1->init();
|
||||
} else {
|
||||
LOG_ERROR("Failed to find touch controller!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
uint16_t getVDDVoltage();
|
||||
|
||||
// Weak empty variant shutdown prep function.
|
||||
// Weak empty variant initialization function.
|
||||
// May be redefined by variant files.
|
||||
void variant_shutdown() __attribute__((weak));
|
||||
void variant_shutdown() {}
|
||||
|
||||
@@ -872,6 +872,7 @@ bool loadConfig(const char *configPath)
|
||||
}
|
||||
|
||||
if (yamlConfig["Config"]) {
|
||||
portduino_config.has_config_overrides = true;
|
||||
if (yamlConfig["Config"]["DisplayMode"]) {
|
||||
portduino_config.has_configDisplayMode = true;
|
||||
if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "TWOCOLOR") {
|
||||
@@ -884,6 +885,13 @@ bool loadConfig(const char *configPath)
|
||||
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
|
||||
}
|
||||
}
|
||||
if (yamlConfig["Config"]["StatusMessage"]) {
|
||||
portduino_config.has_statusMessage = true;
|
||||
portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as<std::string>("");
|
||||
}
|
||||
if ((yamlConfig["Config"]["EnableUDP"]).as<bool>(false)) {
|
||||
portduino_config.enable_UDP = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (yamlConfig["General"]) {
|
||||
|
||||
@@ -177,8 +177,12 @@ extern struct portduino_config_struct {
|
||||
int hostMetrics_channel = 0;
|
||||
|
||||
// config
|
||||
bool has_config_overrides = false;
|
||||
int configDisplayMode = 0;
|
||||
bool has_configDisplayMode = false;
|
||||
std::string statusMessage = "";
|
||||
bool has_statusMessage = false;
|
||||
bool enable_UDP = false;
|
||||
|
||||
// General
|
||||
std::string mac_address = "";
|
||||
@@ -505,21 +509,30 @@ extern struct portduino_config_struct {
|
||||
}
|
||||
|
||||
// config
|
||||
if (has_configDisplayMode) {
|
||||
if (has_config_overrides) {
|
||||
out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
|
||||
switch (configDisplayMode) {
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
|
||||
break;
|
||||
if (has_configDisplayMode) {
|
||||
|
||||
switch (configDisplayMode) {
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
|
||||
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (has_statusMessage) {
|
||||
out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage;
|
||||
}
|
||||
if (enable_UDP) {
|
||||
out << YAML::Key << "EnableUDP" << YAML::Value << true;
|
||||
}
|
||||
|
||||
out << YAML::EndMap; // Config
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#undef GPS_TX_PIN
|
||||
#define NO_GPS 1
|
||||
#define HAS_GPS 0
|
||||
#define NO_SCREEN
|
||||
#define HAS_SCREEN 0
|
||||
|
||||
// Default SPI1 will be mapped to the display
|
||||
|
||||
@@ -34,5 +34,3 @@ lib_deps = ${esp32s3_base.lib_deps}
|
||||
https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip
|
||||
# renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino
|
||||
maxpromer/PCA9557-arduino@1.0.0
|
||||
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
|
||||
lewisxhe/SensorLib@0.3.4
|
||||
@@ -1,17 +1,12 @@
|
||||
#include "variant.h"
|
||||
#include <PCA9557.h>
|
||||
|
||||
PCA9557 io(0x18, &Wire1);
|
||||
PCA9557 io(0x18, &Wire);
|
||||
|
||||
void earlyInitVariant()
|
||||
{
|
||||
Wire1.begin(48, 47);
|
||||
Wire.begin(48, 47);
|
||||
io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
|
||||
io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
|
||||
io.digitalWrite(PCA_PIN_POWER_EN, HIGH);
|
||||
}
|
||||
|
||||
void variant_shutdown()
|
||||
{
|
||||
io.digitalWrite(PCA_PIN_POWER_EN, LOW);
|
||||
}
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
#define I2C_SCL 1
|
||||
#define I2C_SDA 2
|
||||
|
||||
// PCF8563 RTC Module
|
||||
#define PCF8563_RTC 0x51
|
||||
|
||||
// GPS pins
|
||||
#define GPS_SWITH 10
|
||||
#define HAS_GPS 1
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#define EXT_NOTIFY_OUT 22
|
||||
#define BUTTON_PIN 0 // 17
|
||||
|
||||
// #define LED_PIN PIN_LED
|
||||
// Board has RGB LED 21
|
||||
#define HAS_NEOPIXEL // Enable the use of neopixels
|
||||
#define NEOPIXEL_COUNT 1 // How many neopixels are connected
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
|
||||
Most of the Meshtastic firmware uses preprocessor macros throughout the code to support different hardware variants.
|
||||
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 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/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/Heard/HeardApplet.h"
|
||||
#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h"
|
||||
#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h"
|
||||
#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h"
|
||||
|
||||
// Shared NicheGraphics components
|
||||
// --------------------------------
|
||||
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.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
|
||||
// -----------------------------
|
||||
|
||||
// Use E-Ink driver
|
||||
Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
|
||||
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
|
||||
|
||||
// InkHUD
|
||||
// ----------------------------
|
||||
|
||||
InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance();
|
||||
|
||||
// Set the driver
|
||||
inkhud->setDriver(driver);
|
||||
|
||||
// Set how many FAST updates per FULL update
|
||||
// Set how unhealthy additional FAST updates beyond this number are
|
||||
inkhud->setDisplayResilience(7, 1.5);
|
||||
|
||||
// Prepare fonts
|
||||
InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252;
|
||||
InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252;
|
||||
|
||||
// Init settings, and customize defaults
|
||||
inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle?
|
||||
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.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead
|
||||
inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery
|
||||
|
||||
// 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
|
||||
// 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("Basic", new InkHUD::BasicExampleApplet);
|
||||
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
|
||||
|
||||
// Start running InkHUD
|
||||
inkhud->begin();
|
||||
|
||||
// Buttons
|
||||
// --------------------------
|
||||
|
||||
Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component
|
||||
|
||||
// Setup the main user button (0)
|
||||
buttons->setWiring(0, BUTTON_PIN);
|
||||
buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
|
||||
buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); });
|
||||
|
||||
// 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
|
||||
@@ -1,43 +0,0 @@
|
||||
#ifndef Pins_Arduino_h
|
||||
#define Pins_Arduino_h
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define USB_VID 0x303a
|
||||
#define USB_PID 0x1001
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
// The default Wire will be mapped to RTC, Touch, BQ25896, and BQ27220
|
||||
static const uint8_t SDA = 6;
|
||||
static const uint8_t SCL = 5;
|
||||
|
||||
// Default SPI will be mapped to Radio
|
||||
static const uint8_t SS = 46;
|
||||
static const uint8_t MOSI = 17;
|
||||
static const uint8_t MISO = 8;
|
||||
static const uint8_t SCK = 18;
|
||||
|
||||
#define SPI_MOSI (17)
|
||||
#define SPI_SCK (18)
|
||||
#define SPI_MISO (8)
|
||||
#define SPI_CS (16)
|
||||
|
||||
#else // T5_S3_EPAPER_PRO_V2
|
||||
// The default Wire will be mapped to RTC, Touch, PCA9535, BQ25896, and BQ27220
|
||||
static const uint8_t SDA = 39;
|
||||
static const uint8_t SCL = 40;
|
||||
|
||||
// Default SPI will be mapped to Radio
|
||||
static const uint8_t SS = 46;
|
||||
static const uint8_t MOSI = 13;
|
||||
static const uint8_t MISO = 21;
|
||||
static const uint8_t SCK = 14;
|
||||
|
||||
#define SPI_MOSI (13)
|
||||
#define SPI_SCK (14)
|
||||
#define SPI_MISO (21)
|
||||
#define SPI_CS (12)
|
||||
|
||||
#endif
|
||||
|
||||
#endif /* Pins_Arduino_h */
|
||||
@@ -1,58 +0,0 @@
|
||||
[t5s3_epaper_base]
|
||||
extends = esp32s3_base
|
||||
board = t5-epaper-s3
|
||||
board_build.partition = default_16MB.csv
|
||||
board_check = true
|
||||
upload_protocol = esptool
|
||||
build_flags = -fno-strict-aliasing
|
||||
${esp32_base.build_flags}
|
||||
-I variants/esp32s3/t5s3_epaper
|
||||
-D T5_S3_EPAPER_PRO
|
||||
-D USE_EINK
|
||||
-D USE_EINK_PARALLELDISPLAY
|
||||
-D PRIVATE_HW
|
||||
-D TOUCH_THRESHOLD_X=60
|
||||
-D TOUCH_THRESHOLD_Y=40
|
||||
-D TIME_LONG_PRESS=500
|
||||
; -D EINK_LIMIT_GHOSTING_PX=5000
|
||||
-D EPD_FULLSLOW_PERIOD=100
|
||||
-D FAST_EPD_PARTIAL_UPDATE_BUG ; use rect area update instead of partial
|
||||
|
||||
build_src_filter =
|
||||
${esp32s3_base.build_src_filter}
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
lewisxhe/XPowersLib@0.3.1
|
||||
lewisxhe/SensorLib@0.3.1
|
||||
https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
|
||||
https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip
|
||||
|
||||
[env:t5s3_epaper_inkhud]
|
||||
extends = t5s3_epaper_base, inkhud
|
||||
build_flags =
|
||||
${t5s3_epaper_base.build_flags}
|
||||
${inkhud.build_flags}
|
||||
-D SDCARD_USE_SPI1
|
||||
-D T5_S3_EPAPER_PRO_V2
|
||||
build_src_filter =
|
||||
${t5s3_epaper_base.build_src_filter}
|
||||
${inkhud.build_src_filter}
|
||||
lib_deps =
|
||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||
${t5s3_epaper_base.lib_deps}
|
||||
|
||||
|
||||
[env:t5s3-epaper-v1] ; H752
|
||||
extends = t5s3_epaper_base
|
||||
build_flags =
|
||||
${t5s3_epaper_base.build_flags}
|
||||
-D T5_S3_EPAPER_PRO_V1
|
||||
-D GPS_DEFAULT_NOT_PRESENT=1
|
||||
|
||||
[env:t5s3-epaper-v2] ; H752-01
|
||||
extends = t5s3_epaper_base
|
||||
build_flags =
|
||||
${t5s3_epaper_base.build_flags}
|
||||
-D T5_S3_EPAPER_PRO_V2
|
||||
-D SDCARD_USE_SPI1
|
||||
-D GPS_POWER_TOGGLE
|
||||
@@ -1,92 +0,0 @@
|
||||
|
||||
// Display (E-Ink) ED047TC1 - 8bit parallel
|
||||
#define EPD_WIDTH 960
|
||||
#define EPD_HEIGHT 540
|
||||
|
||||
#define CANNED_MESSAGE_MODULE_ENABLE 1
|
||||
#define USE_VIRTUAL_KEYBOARD 1
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
#define BOARD_BL_EN 40
|
||||
#else
|
||||
#define BOARD_BL_EN 11
|
||||
#endif
|
||||
|
||||
#define I2C_SDA SDA
|
||||
#define I2C_SCL SCL
|
||||
|
||||
#define HAS_TOUCHSCREEN 1
|
||||
#define GT911_PIN_SDA SDA
|
||||
#define GT911_PIN_SCL SCL
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
#define GT911_PIN_INT 15
|
||||
#define GT911_PIN_RST 41
|
||||
#else
|
||||
#define GT911_PIN_INT 3
|
||||
#define GT911_PIN_RST 9
|
||||
#endif
|
||||
|
||||
#define PCF85063_RTC 0x51
|
||||
#define HAS_RTC 1
|
||||
#define PCF85063_INT 2
|
||||
|
||||
#define USE_POWERSAVE
|
||||
#define SLEEP_TIME 120
|
||||
|
||||
// GPS
|
||||
#if !defined(T5_S3_EPAPER_PRO_V1)
|
||||
#define GPS_RX_PIN 44
|
||||
#define GPS_TX_PIN 43
|
||||
#endif
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
#define BUTTON_PIN 48
|
||||
#define PIN_BUTTON2 0
|
||||
#define ALT_BUTTON_PIN PIN_BUTTON2
|
||||
#else
|
||||
#define BUTTON_PIN 0
|
||||
#endif
|
||||
|
||||
// SD card
|
||||
#define HAS_SDCARD
|
||||
#define SDCARD_CS SPI_CS
|
||||
#define SD_SPI_FREQUENCY 75000000U
|
||||
|
||||
// battery charger BQ25896
|
||||
#define HAS_PPM 1
|
||||
#define XPOWERS_CHIP_BQ25896
|
||||
|
||||
// battery quality management BQ27220
|
||||
#define HAS_BQ27220 1
|
||||
#define BQ27220_I2C_SDA SDA
|
||||
#define BQ27220_I2C_SCL SCL
|
||||
#define BQ27220_DESIGN_CAPACITY 1500
|
||||
|
||||
// LoRa
|
||||
#define USE_SX1262
|
||||
#define USE_SX1268
|
||||
|
||||
#define LORA_SCK SCK
|
||||
#define LORA_MISO MISO
|
||||
#define LORA_MOSI MOSI
|
||||
#define LORA_CS 46
|
||||
|
||||
#define LORA_DIO0 -1
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
#define LORA_RESET 43
|
||||
#define LORA_DIO1 3 // SX1262 IRQ
|
||||
#define LORA_DIO2 44 // SX1262 BUSY
|
||||
#define LORA_DIO3
|
||||
#else
|
||||
#define LORA_RESET 1
|
||||
#define LORA_DIO1 10 // SX1262 IRQ
|
||||
#define LORA_DIO2 47 // SX1262 BUSY
|
||||
#define LORA_DIO3
|
||||
#endif
|
||||
|
||||
#define SX126X_CS LORA_CS
|
||||
#define SX126X_DIO1 LORA_DIO1
|
||||
#define SX126X_BUSY LORA_DIO2
|
||||
#define SX126X_RESET LORA_RESET
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 2.4
|
||||
@@ -24,5 +24,5 @@ lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM
|
||||
khoih-prog/nRF52_PWM@1.0.1
|
||||
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
|
||||
; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
|
||||
lewisxhe/SensorLib@0.3.4
|
||||
|
||||
@@ -116,6 +116,7 @@ extern "C" {
|
||||
|
||||
// PCF8563 RTC Module
|
||||
#define PCF8563_RTC 0x51
|
||||
#define HAS_RTC 1
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -21,5 +21,5 @@ build_flags = ${nrf52840_base.build_flags}
|
||||
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6>
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
|
||||
; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
|
||||
lewisxhe/SensorLib@0.3.4
|
||||
|
||||
@@ -121,6 +121,7 @@ static const uint8_t A0 = PIN_A0;
|
||||
|
||||
// PCF8563 RTC Module
|
||||
#define PCF8563_RTC 0x51
|
||||
#define HAS_RTC 1
|
||||
|
||||
// SPI
|
||||
#define SPI_INTERFACES_COUNT 1
|
||||
|
||||
@@ -35,8 +35,6 @@ extern "C" {
|
||||
#define PIN_LED2 LED_BLUE
|
||||
#define PIN_LED3 LED_RED
|
||||
|
||||
#define PIN_LED PIN_LED1
|
||||
|
||||
#define LED_STATE_ON 1 // State when LED is lit
|
||||
|
||||
// XIAO Wio-SX1262 Shield User button
|
||||
|
||||
@@ -51,7 +51,6 @@ extern "C" {
|
||||
#define LED_GREEN PIN_LED1
|
||||
|
||||
#define BLE_LED LED_BLUE
|
||||
#define BLE_LED_INVERTED 1
|
||||
#define LED_STATE_ON 0 // State when LED is lit
|
||||
|
||||
// Buttons
|
||||
|
||||
@@ -132,6 +132,9 @@ static const uint8_t A0 = PIN_A0;
|
||||
|
||||
#define HAS_DRV2605 1
|
||||
|
||||
// Battery / ADC already defined above
|
||||
#define HAS_RTC 1
|
||||
|
||||
#define SERIAL_PRINT_PORT 0
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#define ADC_RESOLUTION (12u)
|
||||
|
||||
// LEDs
|
||||
#define PIN_LED (24u)
|
||||
#define LED_PIN (24u)
|
||||
|
||||
// Serial
|
||||
#define PIN_SERIAL1_TX (16u)
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#define EXT_NOTIFY_OUT 0xFFFFFFFF
|
||||
#define BUTTON_PIN 0xFFFFFFFF
|
||||
|
||||
#define LED_PIN PIN_LED
|
||||
|
||||
#define USE_RF95 // RFM95/SX127x
|
||||
|
||||
#undef LORA_SCK
|
||||
|
||||
@@ -23,8 +23,7 @@ static const uint8_t A2 = PIN_A2;
|
||||
static const uint8_t A3 = PIN_A3;
|
||||
|
||||
// LEDs
|
||||
#define PIN_LED (23u)
|
||||
#define PIN_LED1 PIN_LED
|
||||
#define PIN_LED1 (23u)
|
||||
#define LED_NOTIFICATION (24u)
|
||||
|
||||
#define ADC_RESOLUTION 12
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#define I2C_SDA1 2
|
||||
#define I2C_SCL1 3
|
||||
|
||||
#define LED_PIN PIN_LED
|
||||
#define LED_PIN PIN_LED1
|
||||
#define ledOff(pin) pinMode(pin, INPUT)
|
||||
|
||||
#define BUTTON_PIN 9
|
||||
|
||||
@@ -11,8 +11,7 @@ static const uint8_t A2 = PIN_A2;
|
||||
static const uint8_t A3 = PIN_A3;
|
||||
|
||||
// LEDs
|
||||
#define PIN_LED (23u)
|
||||
#define PIN_LED1 PIN_LED
|
||||
#define PIN_LED1 (23u)
|
||||
|
||||
#define ADC_RESOLUTION 12
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#define BUTTON_PIN 2
|
||||
#define BUTTON_NEED_PULLUP
|
||||
|
||||
#define LED_PIN PIN_LED
|
||||
#define LED_PIN PIN_LED1
|
||||
#define ledOff(pin) pinMode(pin, INPUT)
|
||||
|
||||
#undef BATTERY_PIN
|
||||
|
||||
Reference in New Issue
Block a user