mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-05 16:41:52 +00:00
Compare commits
7 Commits
baseui_sta
...
ble-Banner
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b918e6c713 | ||
|
|
1e5a89a57e | ||
|
|
ee071d3b41 | ||
|
|
3ab62c7355 | ||
|
|
591c17a61b | ||
|
|
04f8e7fb8b | ||
|
|
081c3fc034 |
@@ -50,7 +50,6 @@ 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
|
||||
|
||||
@@ -459,8 +459,6 @@ 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
|
||||
|
||||
@@ -168,7 +168,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
@@ -190,7 +190,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker;
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
@@ -214,7 +214,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
@@ -237,7 +237,7 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
@@ -608,7 +608,7 @@ void Screen::setup()
|
||||
static OverlayCallback overlays[] = {
|
||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||
};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 1);
|
||||
|
||||
// Enable UTF-8 to display mapping
|
||||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||||
@@ -1175,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::drawFavoriteNode);
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1202,9 +1202,9 @@ void Screen::setFrames(FrameFocus focus)
|
||||
|
||||
// Add overlays: frame icons and alert banner)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
|
||||
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
||||
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed)
|
||||
|
||||
// Focus on a specific frame, in the frame set we just created
|
||||
switch (focus) {
|
||||
@@ -1630,7 +1630,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
return 0;
|
||||
@@ -1645,7 +1645,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (NotificationRenderer::isOverlayBannerShowing()) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
#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
|
||||
@@ -93,41 +90,8 @@ const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
|
||||
#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;
|
||||
}
|
||||
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,9 +4,6 @@
|
||||
#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"
|
||||
@@ -290,7 +287,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
// **********************
|
||||
// * Favorite Node Info *
|
||||
// **********************
|
||||
void UIRenderer::drawFavoriteNode(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (favoritedNodes.empty())
|
||||
return;
|
||||
@@ -344,57 +341,6 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, const OLEDDisplayUiState
|
||||
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 drawFavoriteNode(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawNodeInfo(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,5 +1,3 @@
|
||||
#include "graphics/niche/InkHUD/Tile.h"
|
||||
#include <cstdint>
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Applet.h"
|
||||
@@ -787,16 +785,6 @@ 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
|
||||
|
||||
@@ -48,27 +48,37 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
{
|
||||
// Clear the region beneath the tile, including the border
|
||||
// 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
|
||||
// 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(0, 0, width(), height(), WHITE);
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
|
||||
// =====================
|
||||
// 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 = 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
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
@@ -79,13 +89,12 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
int16_t sliceL = bodyL + slicePad;
|
||||
const 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
|
||||
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
|
||||
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 - 1, // x
|
||||
1, // y
|
||||
batteryIconWidth + 1, // width
|
||||
batteryIconHeight + 2); // height
|
||||
->setRegion(inkhud->width() - batteryIconWidth, // x
|
||||
2, // y
|
||||
batteryIconWidth, // width
|
||||
batteryIconHeight); // height
|
||||
|
||||
// Note: the tiles of placeholder and menu applets are manipulated specially
|
||||
// - menuApplet borrows user tiles
|
||||
|
||||
@@ -2223,8 +2223,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
|
||||
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
||||
// TODO: After more mainline SD card support
|
||||
}
|
||||
#endif
|
||||
return success;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Record an error that should be reported via analytics
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
#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
|
||||
@@ -115,9 +112,7 @@ 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
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
#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
|
||||
@@ -1,19 +0,0 @@
|
||||
#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
|
||||
@@ -29,23 +29,10 @@ int32_t StatusMessageModule::runOnce()
|
||||
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
meshtastic_StatusMessage incomingMessage = meshtastic_StatusMessage_init_zero;
|
||||
|
||||
meshtastic_StatusMessage incomingMessage;
|
||||
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;
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
#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
|
||||
@@ -20,28 +19,16 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
|
||||
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;
|
||||
|
||||
@@ -573,51 +573,30 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
virtual uint32_t onPassKeyRequest()
|
||||
#endif
|
||||
{
|
||||
uint32_t passkey = config.bluetooth.fixed_pin;
|
||||
uint32_t configuredPasskey = config.bluetooth.fixed_pin;
|
||||
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Use random passkey");
|
||||
// This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits
|
||||
passkey = random(100000, 999999);
|
||||
configuredPasskey = random(100000, 999999);
|
||||
}
|
||||
LOG_INFO("*** Enter passkey %d on the peer side ***", passkey);
|
||||
LOG_INFO("*** Enter passkey %d on the peer side ***", configuredPasskey);
|
||||
|
||||
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
|
||||
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
|
||||
std::string passkey = std::to_string(configuredPasskey);
|
||||
meshtastic::BluetoothStatus newStatus(passkey);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
|
||||
if (screen) {
|
||||
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
snprintf(btPIN, sizeof(btPIN), "%06u", passkey);
|
||||
int x_offset = display->width() / 2;
|
||||
int y_offset = display->height() <= 80 ? 0 : 12;
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
display->setFont(FONT_SMALL);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, "Enter this code");
|
||||
#endif
|
||||
display->setFont(FONT_LARGE);
|
||||
char pin[8];
|
||||
snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, pin);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
char deviceName[64];
|
||||
snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName());
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, deviceName);
|
||||
});
|
||||
std::string ble_message = "Bluetooth\nPIN\n" + passkey.substr(0, 3) + " " + passkey.substr(3, 6);
|
||||
screen->showSimpleBanner(ble_message.c_str(), 30000);
|
||||
}
|
||||
#endif
|
||||
passkeyShowing = true;
|
||||
|
||||
return passkey;
|
||||
return configuredPasskey;
|
||||
}
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
|
||||
@@ -370,34 +370,11 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke
|
||||
meshtastic::BluetoothStatus newStatus(textkey);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN && \
|
||||
!defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
|
||||
#if HAS_SCREEN && !defined(MESHTASTIC_EXCLUDE_SCREEN)
|
||||
if (screen) {
|
||||
screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey);
|
||||
int x_offset = display->width() / 2;
|
||||
int y_offset = display->height() <= 80 ? 0 : 12;
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, "Enter this code");
|
||||
|
||||
display->setFont(FONT_LARGE);
|
||||
String displayPin(btPIN);
|
||||
String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, pin);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
String deviceName = "Name: ";
|
||||
deviceName.concat(getDeviceName());
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, deviceName);
|
||||
});
|
||||
std::string passkey = std::to_string(configuredPasskey);
|
||||
std::string ble_message = "Bluetooth\nPIN\n" + passkey.substr(0, 3) + " " + passkey.substr(3, 6);
|
||||
screen->showSimpleBanner(ble_message.c_str(), 30000);
|
||||
}
|
||||
#endif
|
||||
if (match_request) {
|
||||
|
||||
@@ -7,5 +7,4 @@ build_flags =
|
||||
-D TLORA_V2_1_16
|
||||
-I variants/esp32/tlora_v2_1_16
|
||||
-D LORA_TCXO_GPIO=33
|
||||
-ULED_BUILTIN
|
||||
upload_speed = 115200
|
||||
upload_speed = 115200
|
||||
Reference in New Issue
Block a user