mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-05 00:22:04 +00:00
Compare commits
18 Commits
master
...
baseui_sta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34314d16ac | ||
|
|
89df5ef669 | ||
|
|
6426e2598b | ||
|
|
bfc3eebd54 | ||
|
|
538a5f0dfc | ||
|
|
b7db22055d | ||
|
|
0d57a49b51 | ||
|
|
d8a0b6a737 | ||
|
|
0703e0e6d7 | ||
|
|
f514bc230b | ||
|
|
697dd2b5b2 | ||
|
|
0b8b757fb0 | ||
|
|
62f897eab3 | ||
|
|
523906d031 | ||
|
|
0022148323 | ||
|
|
78f29c0f87 | ||
|
|
9d06c1bf34 | ||
|
|
1d30342c00 |
@@ -50,6 +50,7 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DRADIOLIB_EXCLUDE_APRS=1
|
-DRADIOLIB_EXCLUDE_APRS=1
|
||||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||||
|
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||||
|
|||||||
@@ -89,22 +89,14 @@ class BluetoothStatus : public Status
|
|||||||
case ConnectionState::CONNECTED:
|
case ConnectionState::CONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
#ifdef BLE_LED_INVERTED
|
digitalWrite(BLE_LED, LED_STATE_ON);
|
||||||
digitalWrite(BLE_LED, LOW);
|
|
||||||
#else
|
|
||||||
digitalWrite(BLE_LED, HIGH);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ConnectionState::DISCONNECTED:
|
case ConnectionState::DISCONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
#ifdef BLE_LED_INVERTED
|
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||||
digitalWrite(BLE_LED, HIGH);
|
|
||||||
#else
|
|
||||||
digitalWrite(BLE_LED, LOW);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
// if it's not HIGH - check the battery
|
// if it's not HIGH - check the battery
|
||||||
#endif
|
#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
|
// technically speaking this should work for all(?) NRF52 boards
|
||||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||||
|
|||||||
@@ -1175,7 +1175,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1204,7 +1204,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
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
|
// Focus on a specific frame, in the frame set we just created
|
||||||
switch (focus) {
|
switch (focus) {
|
||||||
|
|||||||
@@ -877,15 +877,15 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
// Send Message (Right side)
|
// Send Message (Right side)
|
||||||
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
|
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
|
||||||
// Top Right Corner
|
// Top Right Corner
|
||||||
display->drawRect(x1, topY, 2, 1);
|
display->drawRect(x1 - 1, topY, 2, 1);
|
||||||
display->drawRect(x1, topY, 1, 2);
|
display->drawRect(x1, topY, 1, 2);
|
||||||
// Bottom Right Corner
|
// Bottom Right Corner
|
||||||
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
|
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
|
||||||
display->drawRect(x1, bottomY - 3, 1, 2);
|
display->drawRect(x1, bottomY - 3, 1, 2);
|
||||||
// Knock the corners off to make a bubble
|
// Knock the corners off to make a bubble
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
|
display->drawRect(x1 - bubbleW + 2, topY - 1, 1, 1);
|
||||||
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
|
display->drawRect(x1 - bubbleW + 2, bottomY - 1, 1, 1);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
} else {
|
} else {
|
||||||
// Received Message (Left Side)
|
// Received Message (Left Side)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#include "CompassRenderer.h"
|
#include "CompassRenderer.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "NodeListRenderer.h"
|
#include "NodeListRenderer.h"
|
||||||
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
|
#include "modules/StatusMessageModule.h"
|
||||||
|
#endif
|
||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
#include "gps/RTC.h" // for getTime() function
|
#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
|
// 1) Choose target candidate (long vs short) only if present
|
||||||
const char *raw = nullptr;
|
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)
|
// 2) Sanitize (empty if raw is null/empty)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
#include "GPSStatus.h"
|
#include "GPSStatus.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "NodeListRenderer.h"
|
#include "NodeListRenderer.h"
|
||||||
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
|
#include "modules/StatusMessageModule.h"
|
||||||
|
#endif
|
||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "gps/GeoCoord.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 *
|
// * 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())
|
if (favoritedNodes.empty())
|
||||||
return;
|
return;
|
||||||
@@ -341,6 +344,57 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
|
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) ===
|
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||||
// If both are present: "Sig: 97% [2hops]"
|
// If both are present: "Sig: 97% [2hops]"
|
||||||
// If only one: show only that one
|
// If only one: show only that one
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class UIRenderer
|
|||||||
// Navigation bar overlay
|
// Navigation bar overlay
|
||||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
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);
|
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
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
#include "./Applet.h"
|
#include "./Applet.h"
|
||||||
@@ -785,6 +787,16 @@ void InkHUD::Applet::drawHeader(std::string text)
|
|||||||
drawPixel(x, 0, BLACK);
|
drawPixel(x, 0, BLACK);
|
||||||
drawPixel(x, headerDivY, BLACK); // Dotted 50%
|
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
|
// Get the height of the standard applet header
|
||||||
|
|||||||
@@ -48,37 +48,27 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
|||||||
|
|
||||||
void InkHUD::BatteryIconApplet::onRender(bool full)
|
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||||
{
|
{
|
||||||
// Fill entire tile
|
// Clear the region beneath the tile, including the border
|
||||||
// - 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
|
|
||||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
// 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"
|
// We do need to do this with the battery though, as it is an "overlay"
|
||||||
fillRect(l, t, w, h, WHITE);
|
fillRect(0, 0, width(), height(), WHITE);
|
||||||
|
|
||||||
// Vertical centerline
|
|
||||||
const int16_t m = t + (h / 2);
|
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Draw battery outline
|
// Draw battery outline
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
// Positive terminal "bump"
|
// 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;
|
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);
|
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||||
|
|
||||||
// Main body of battery
|
// Main body of battery
|
||||||
const int16_t bodyL = bumpL + bumpW;
|
const int16_t bodyL = 1 + bumpW;
|
||||||
const int16_t &bodyT = t;
|
const int16_t &bodyT = 1;
|
||||||
const int16_t &bodyH = h;
|
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
|
||||||
const int16_t bodyW = w - bumpW;
|
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
|
||||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||||
|
|
||||||
// Erase join between bump and body
|
// Erase join between bump and body
|
||||||
@@ -89,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
|
|||||||
// ===================
|
// ===================
|
||||||
|
|
||||||
constexpr int16_t slicePad = 2;
|
constexpr int16_t slicePad = 2;
|
||||||
const int16_t sliceL = bodyL + slicePad;
|
int16_t sliceL = bodyL + slicePad;
|
||||||
const int16_t sliceT = bodyT + slicePad;
|
const int16_t sliceT = bodyT + slicePad;
|
||||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||||
uint16_t sliceW = bodyW - (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);
|
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||||
|
|||||||
@@ -510,10 +510,10 @@ void InkHUD::WindowManager::placeSystemTiles()
|
|||||||
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
|
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
|
||||||
inkhud->getSystemApplet("BatteryIcon")
|
inkhud->getSystemApplet("BatteryIcon")
|
||||||
->getTile()
|
->getTile()
|
||||||
->setRegion(inkhud->width() - batteryIconWidth, // x
|
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
|
||||||
2, // y
|
1, // y
|
||||||
batteryIconWidth, // width
|
batteryIconWidth + 1, // width
|
||||||
batteryIconHeight); // height
|
batteryIconHeight + 2); // height
|
||||||
|
|
||||||
// Note: the tiles of placeholder and menu applets are manipulated specially
|
// Note: the tiles of placeholder and menu applets are manipulated specially
|
||||||
// - menuApplet borrows user tiles
|
// - menuApplet borrows user tiles
|
||||||
|
|||||||
10
src/main.cpp
10
src/main.cpp
@@ -366,11 +366,7 @@ void setup()
|
|||||||
|
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
pinMode(BLE_LED, OUTPUT);
|
pinMode(BLE_LED, OUTPUT);
|
||||||
#ifdef BLE_LED_INVERTED
|
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||||
digitalWrite(BLE_LED, HIGH);
|
|
||||||
#else
|
|
||||||
digitalWrite(BLE_LED, LOW);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
concurrency::hasBeenSetup = true;
|
concurrency::hasBeenSetup = true;
|
||||||
@@ -493,7 +489,9 @@ void setup()
|
|||||||
// The ThinkNodes have their own blink logic
|
// The ThinkNodes have their own blink logic
|
||||||
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
|
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
|
||||||
#else
|
#else
|
||||||
|
|
||||||
ledPeriodic = new Periodic("Blink", ledBlinker);
|
ledPeriodic = new Periodic("Blink", ledBlinker);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
fsInit();
|
fsInit();
|
||||||
@@ -834,7 +832,7 @@ void setup()
|
|||||||
SPI.begin();
|
SPI.begin();
|
||||||
#endif
|
#endif
|
||||||
#else
|
#else
|
||||||
// ESP32
|
// ESP32
|
||||||
#if defined(HW_SPI1_DEVICE)
|
#if defined(HW_SPI1_DEVICE)
|
||||||
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
|
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);
|
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) {
|
if (portduino_config.has_configDisplayMode) {
|
||||||
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.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
|
#endif
|
||||||
}
|
}
|
||||||
@@ -1544,6 +1553,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
|||||||
moduleConfig.has_ambient_lighting = true;
|
moduleConfig.has_ambient_lighting = true;
|
||||||
moduleConfig.has_audio = true;
|
moduleConfig.has_audio = true;
|
||||||
moduleConfig.has_paxcounter = true;
|
moduleConfig.has_paxcounter = true;
|
||||||
|
moduleConfig.has_statusmessage = true;
|
||||||
|
|
||||||
success &=
|
success &=
|
||||||
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
|
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
|
||||||
@@ -2213,8 +2223,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
|
|||||||
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
||||||
// TODO: After more mainline SD card support
|
// TODO: After more mainline SD card support
|
||||||
}
|
}
|
||||||
return success;
|
|
||||||
#endif
|
#endif
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record an error that should be reported via analytics
|
/// Record an error that should be reported via analytics
|
||||||
|
|||||||
@@ -905,10 +905,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
|||||||
|
|
||||||
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &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
|
// 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
|
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
|
||||||
if (!hasOpenEditTransaction &&
|
if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag,
|
||||||
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
|
meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) {
|
||||||
disableBluetooth();
|
disableBluetooth();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1000,8 +1001,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
|||||||
moduleConfig.has_paxcounter = true;
|
moduleConfig.has_paxcounter = true;
|
||||||
moduleConfig.paxcounter = c.payload_variant.paxcounter;
|
moduleConfig.paxcounter = c.payload_variant.paxcounter;
|
||||||
break;
|
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;
|
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.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||||
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||||
break;
|
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.
|
// 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/StatusLEDModule.h"
|
||||||
#include "modules/SystemCommandsModule.h"
|
#include "modules/SystemCommandsModule.h"
|
||||||
#endif
|
#endif
|
||||||
|
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||||
|
#include "ReplyBotModule.h"
|
||||||
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_PKI
|
#if !MESHTASTIC_EXCLUDE_PKI
|
||||||
#include "KeyVerificationModule.h"
|
#include "KeyVerificationModule.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -90,6 +93,9 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||||
#include "modules/DropzoneModule.h"
|
#include "modules/DropzoneModule.h"
|
||||||
#endif
|
#endif
|
||||||
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
|
#include "modules/StatusMessageModule.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(HAS_HARDWARE_WATCHDOG)
|
#if defined(HAS_HARDWARE_WATCHDOG)
|
||||||
#include "watchdog/watchdogThread.h"
|
#include "watchdog/watchdogThread.h"
|
||||||
@@ -109,7 +115,9 @@ void setupModules()
|
|||||||
#if defined(LED_CHARGE) || defined(LED_PAIRING)
|
#if defined(LED_CHARGE) || defined(LED_PAIRING)
|
||||||
statusLEDModule = new StatusLEDModule();
|
statusLEDModule = new StatusLEDModule();
|
||||||
#endif
|
#endif
|
||||||
|
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||||
|
new ReplyBotModule();
|
||||||
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||||
adminModule = new AdminModule();
|
adminModule = new AdminModule();
|
||||||
#endif
|
#endif
|
||||||
@@ -150,6 +158,9 @@ void setupModules()
|
|||||||
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
#if !MESHTASTIC_EXCLUDE_DROPZONE
|
||||||
dropzoneModule = new DropzoneModule();
|
dropzoneModule = new DropzoneModule();
|
||||||
#endif
|
#endif
|
||||||
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
|
statusMessageModule = new StatusMessageModule();
|
||||||
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
|
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
|
||||||
new GenericThreadModule();
|
new GenericThreadModule();
|
||||||
#endif
|
#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;
|
isDeInit = true;
|
||||||
|
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
#ifdef BLE_LED_INVERTED
|
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||||
digitalWrite(BLE_LED, HIGH);
|
|
||||||
#else
|
|
||||||
digitalWrite(BLE_LED, LOW);
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
#ifndef NIMBLE_TWO
|
#ifndef NIMBLE_TWO
|
||||||
NimBLEDevice::deinit();
|
NimBLEDevice::deinit();
|
||||||
|
|||||||
@@ -872,6 +872,7 @@ bool loadConfig(const char *configPath)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (yamlConfig["Config"]) {
|
if (yamlConfig["Config"]) {
|
||||||
|
portduino_config.has_config_overrides = true;
|
||||||
if (yamlConfig["Config"]["DisplayMode"]) {
|
if (yamlConfig["Config"]["DisplayMode"]) {
|
||||||
portduino_config.has_configDisplayMode = true;
|
portduino_config.has_configDisplayMode = true;
|
||||||
if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "TWOCOLOR") {
|
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;
|
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"]) {
|
if (yamlConfig["General"]) {
|
||||||
|
|||||||
@@ -177,8 +177,12 @@ extern struct portduino_config_struct {
|
|||||||
int hostMetrics_channel = 0;
|
int hostMetrics_channel = 0;
|
||||||
|
|
||||||
// config
|
// config
|
||||||
|
bool has_config_overrides = false;
|
||||||
int configDisplayMode = 0;
|
int configDisplayMode = 0;
|
||||||
bool has_configDisplayMode = false;
|
bool has_configDisplayMode = false;
|
||||||
|
std::string statusMessage = "";
|
||||||
|
bool has_statusMessage = false;
|
||||||
|
bool enable_UDP = false;
|
||||||
|
|
||||||
// General
|
// General
|
||||||
std::string mac_address = "";
|
std::string mac_address = "";
|
||||||
@@ -505,21 +509,30 @@ extern struct portduino_config_struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// config
|
// config
|
||||||
if (has_configDisplayMode) {
|
if (has_config_overrides) {
|
||||||
out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
|
out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
|
||||||
switch (configDisplayMode) {
|
if (has_configDisplayMode) {
|
||||||
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
|
|
||||||
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
|
switch (configDisplayMode) {
|
||||||
break;
|
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
|
||||||
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
|
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
|
||||||
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
|
break;
|
||||||
break;
|
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
|
||||||
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
|
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
|
||||||
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
|
break;
|
||||||
break;
|
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
|
||||||
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
|
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
|
||||||
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
|
break;
|
||||||
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
|
out << YAML::EndMap; // Config
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ build_flags =
|
|||||||
-D TLORA_V2_1_16
|
-D TLORA_V2_1_16
|
||||||
-I variants/esp32/tlora_v2_1_16
|
-I variants/esp32/tlora_v2_1_16
|
||||||
-D LORA_TCXO_GPIO=33
|
-D LORA_TCXO_GPIO=33
|
||||||
upload_speed = 115200
|
-ULED_BUILTIN
|
||||||
|
upload_speed = 115200
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
#undef GPS_TX_PIN
|
#undef GPS_TX_PIN
|
||||||
#define NO_GPS 1
|
#define NO_GPS 1
|
||||||
#define HAS_GPS 0
|
#define HAS_GPS 0
|
||||||
#define NO_SCREEN
|
|
||||||
#define HAS_SCREEN 0
|
#define HAS_SCREEN 0
|
||||||
|
|
||||||
// Default SPI1 will be mapped to the display
|
// Default SPI1 will be mapped to the display
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
#define EXT_NOTIFY_OUT 22
|
#define EXT_NOTIFY_OUT 22
|
||||||
#define BUTTON_PIN 0 // 17
|
#define BUTTON_PIN 0 // 17
|
||||||
|
|
||||||
// #define LED_PIN PIN_LED
|
|
||||||
// Board has RGB LED 21
|
// Board has RGB LED 21
|
||||||
#define HAS_NEOPIXEL // Enable the use of neopixels
|
#define HAS_NEOPIXEL // Enable the use of neopixels
|
||||||
#define NEOPIXEL_COUNT 1 // How many neopixels are connected
|
#define NEOPIXEL_COUNT 1 // How many neopixels are connected
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ extern "C" {
|
|||||||
#define PIN_LED2 LED_BLUE
|
#define PIN_LED2 LED_BLUE
|
||||||
#define PIN_LED3 LED_RED
|
#define PIN_LED3 LED_RED
|
||||||
|
|
||||||
#define PIN_LED PIN_LED1
|
|
||||||
|
|
||||||
#define LED_STATE_ON 1 // State when LED is lit
|
#define LED_STATE_ON 1 // State when LED is lit
|
||||||
|
|
||||||
// XIAO Wio-SX1262 Shield User button
|
// XIAO Wio-SX1262 Shield User button
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ extern "C" {
|
|||||||
#define LED_GREEN PIN_LED1
|
#define LED_GREEN PIN_LED1
|
||||||
|
|
||||||
#define BLE_LED LED_BLUE
|
#define BLE_LED LED_BLUE
|
||||||
#define BLE_LED_INVERTED 1
|
|
||||||
#define LED_STATE_ON 0 // State when LED is lit
|
#define LED_STATE_ON 0 // State when LED is lit
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#define ADC_RESOLUTION (12u)
|
#define ADC_RESOLUTION (12u)
|
||||||
|
|
||||||
// LEDs
|
// LEDs
|
||||||
#define PIN_LED (24u)
|
#define LED_PIN (24u)
|
||||||
|
|
||||||
// Serial
|
// Serial
|
||||||
#define PIN_SERIAL1_TX (16u)
|
#define PIN_SERIAL1_TX (16u)
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
#define EXT_NOTIFY_OUT 0xFFFFFFFF
|
#define EXT_NOTIFY_OUT 0xFFFFFFFF
|
||||||
#define BUTTON_PIN 0xFFFFFFFF
|
#define BUTTON_PIN 0xFFFFFFFF
|
||||||
|
|
||||||
#define LED_PIN PIN_LED
|
|
||||||
|
|
||||||
#define USE_RF95 // RFM95/SX127x
|
#define USE_RF95 // RFM95/SX127x
|
||||||
|
|
||||||
#undef LORA_SCK
|
#undef LORA_SCK
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ static const uint8_t A2 = PIN_A2;
|
|||||||
static const uint8_t A3 = PIN_A3;
|
static const uint8_t A3 = PIN_A3;
|
||||||
|
|
||||||
// LEDs
|
// LEDs
|
||||||
#define PIN_LED (23u)
|
#define PIN_LED1 (23u)
|
||||||
#define PIN_LED1 PIN_LED
|
|
||||||
#define LED_NOTIFICATION (24u)
|
#define LED_NOTIFICATION (24u)
|
||||||
|
|
||||||
#define ADC_RESOLUTION 12
|
#define ADC_RESOLUTION 12
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
#define I2C_SDA1 2
|
#define I2C_SDA1 2
|
||||||
#define I2C_SCL1 3
|
#define I2C_SCL1 3
|
||||||
|
|
||||||
#define LED_PIN PIN_LED
|
#define LED_PIN PIN_LED1
|
||||||
#define ledOff(pin) pinMode(pin, INPUT)
|
#define ledOff(pin) pinMode(pin, INPUT)
|
||||||
|
|
||||||
#define BUTTON_PIN 9
|
#define BUTTON_PIN 9
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ static const uint8_t A2 = PIN_A2;
|
|||||||
static const uint8_t A3 = PIN_A3;
|
static const uint8_t A3 = PIN_A3;
|
||||||
|
|
||||||
// LEDs
|
// LEDs
|
||||||
#define PIN_LED (23u)
|
#define PIN_LED1 (23u)
|
||||||
#define PIN_LED1 PIN_LED
|
|
||||||
|
|
||||||
#define ADC_RESOLUTION 12
|
#define ADC_RESOLUTION 12
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#define BUTTON_PIN 2
|
#define BUTTON_PIN 2
|
||||||
#define BUTTON_NEED_PULLUP
|
#define BUTTON_NEED_PULLUP
|
||||||
|
|
||||||
#define LED_PIN PIN_LED
|
#define LED_PIN PIN_LED1
|
||||||
#define ledOff(pin) pinMode(pin, INPUT)
|
#define ledOff(pin) pinMode(pin, INPUT)
|
||||||
|
|
||||||
#undef BATTERY_PIN
|
#undef BATTERY_PIN
|
||||||
|
|||||||
Reference in New Issue
Block a user