Compare commits

..

11 Commits

Author SHA1 Message Date
Jonathan Bennett
577499f126 Use Throttle function 2026-02-01 22:53:34 -06:00
Jonathan Bennett
09eac5e692 Update src/main.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-01 22:38:44 -06:00
Jonathan Bennett
e9c551a419 AGC reset don't crash, don't naively call 2026-02-01 22:20:00 -06:00
Jonathan Bennett
39bac0db01 Merge branch 'develop' into agc-reset 2026-02-01 21:27:22 -06:00
Jonathan Bennett
e7306448be Trunk 2026-02-01 16:19:00 -06:00
Jonathan Bennett
51f9e537de Add radioLibInterface include 2026-02-01 16:01:23 -06:00
Jonathan Bennett
715918d5ac Merge branch 'develop' into agc-reset 2026-02-01 14:26:05 -06:00
Jonathan Bennett
a0e2b06970 Merge branch 'develop' into agc-reset 2026-01-28 16:17:28 -06:00
Jonathan Bennett
bc702b18d8 Merge branch 'develop' into agc-reset 2025-12-17 19:54:07 -06:00
Jonathan Bennett
5e2d8eac6a Merge branch 'develop' into agc-reset 2025-10-14 09:04:28 -05:00
Jonathan Bennett
971543fab3 Add agc reset attempt 2025-09-29 12:11:48 -05:00
12 changed files with 39 additions and 240 deletions

View File

@@ -50,7 +50,6 @@ 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

View File

@@ -459,8 +459,6 @@ 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

View File

@@ -1,5 +1,3 @@
#include "graphics/niche/InkHUD/Tile.h"
#include <cstdint>
#ifdef MESHTASTIC_INCLUDE_INKHUD #ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Applet.h" #include "./Applet.h"
@@ -787,16 +785,6 @@ 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

View File

@@ -48,27 +48,37 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
void InkHUD::BatteryIconApplet::onRender(bool full) 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 // 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(0, 0, width(), height(), WHITE); fillRect(l, t, w, h, 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 = 1 + bumpW; const int16_t bodyL = bumpL + bumpW;
const int16_t &bodyT = 1; const int16_t &bodyT = t;
const int16_t &bodyH = height() - 2; // Handle top/bottom padding const int16_t &bodyH = h;
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad const int16_t bodyW = w - bumpW;
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
// Erase join between bump and body // Erase join between bump and body
@@ -79,13 +89,12 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
// =================== // ===================
constexpr int16_t slicePad = 2; constexpr int16_t slicePad = 2;
int16_t sliceL = bodyL + slicePad; const 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);

View File

@@ -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 - 1, // x ->setRegion(inkhud->width() - batteryIconWidth, // x
1, // y 2, // y
batteryIconWidth + 1, // width batteryIconWidth, // width
batteryIconHeight + 2); // height batteryIconHeight); // 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

View File

@@ -7,6 +7,7 @@
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "PowerMon.h" #include "PowerMon.h"
#include "RadioLibInterface.h"
#include "ReliableRouter.h" #include "ReliableRouter.h"
#include "airtime.h" #include "airtime.h"
#include "buzz.h" #include "buzz.h"
@@ -193,6 +194,8 @@ bool kb_found = false;
// global bool to record that on-screen keyboard (OSK) is present // global bool to record that on-screen keyboard (OSK) is present
bool osk_found = false; bool osk_found = false;
unsigned long last_listen = 0;
// The I2C address of the RTC Module (if found) // The I2C address of the RTC Module (if found)
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Accelerometer (if found) // The I2C address of the Accelerometer (if found)
@@ -1166,6 +1169,12 @@ void loop()
#endif #endif
power->powerCommandsCheck(); power->powerCommandsCheck();
if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) &&
!(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) {
RadioLibInterface::instance->startReceive();
LOG_DEBUG("attempting AGC reset");
}
#ifdef DEBUG_STACK #ifdef DEBUG_STACK
static uint32_t lastPrint = 0; static uint32_t lastPrint = 0;
if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) {

View File

@@ -33,6 +33,7 @@ extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model; extern uint8_t kb_model;
extern bool kb_found; extern bool kb_found;
extern bool osk_found; extern bool osk_found;
extern unsigned long last_listen;
extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::FoundDevice rgb_found; extern ScanI2C::FoundDevice rgb_found;

View File

@@ -2223,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
} }
#endif
return success; return success;
#endif
} }
/// Record an error that should be reported via analytics /// Record an error that should be reported via analytics

View File

@@ -514,6 +514,8 @@ void RadioLibInterface::handleReceiveInterrupt()
void RadioLibInterface::startReceive() void RadioLibInterface::startReceive()
{ {
// Note the updated timestamp, to avoid unneeded AGC resets
last_listen = millis();
isReceiving = true; isReceiving = true;
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);
} }

View File

@@ -4,9 +4,6 @@
#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
@@ -115,9 +112,7 @@ 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

View File

@@ -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 persender 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 ratelimited 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 nullterminated 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 nonzero 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 endofstring 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

View File

@@ -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