Compare commits

..

9 Commits

Author SHA1 Message Date
Ben Meadors
857c16faa2 Guard portduino 2026-02-03 20:36:55 -06:00
Ben Meadors
3aa0cbd26a Add close method to AsyncUDP and improve UDP multicast handler lifecycle management 2026-02-03 19:44:53 -06:00
Ben Meadors
17ccd6f921 Merge branch 'master' into udp-cleanup 2026-02-03 19:44:21 -06:00
Jason P
b008c7a170 Fix config.display.use_long_node_name not saving (#9522) 2026-02-03 12:21:51 -06:00
Ben Meadors
a08e7f07ff Implement UDP multicast handler start/stop to ensure proper lifecycle 2026-02-03 08:58:10 -06:00
Eric Sesterhenn
c8a9cdc148 Make sure we always return a value in NodeDB::restorePreferences() (#9516)
In case FScom is not defined there is no return statement. This
moves the return outside of the ifdef to make sure a defined
value is returned.
2026-02-03 06:23:49 -06:00
Jonathan Bennett
644fa5b54e Power off control pin on Thinknode m5 during deepsleep and add RTC (#9510)
* Power off control pin on Thinknode m5 during deepsleep

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Finish HAS_RTC cleanup

* Add RTC for Thinknode M5

* Don't double-init Wire

* Specify the RTC chip directly rather than use SensorRtcHelper.
Saves a bit of flash, and avoid mis-detection

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-03 06:07:33 -06:00
Ben Meadors
e9d4485bb5 Merge remote-tracking branch 'origin/develop' 2026-01-30 13:56:11 -06:00
Ben Meadors
004179c045 Fix OTA filename determination to use unified format for ESP32 (#9488) 2026-01-30 13:46:11 -06:00
48 changed files with 153 additions and 934 deletions

View File

@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
SET "PROGNAME=!FILENAME:.factory.bin=!"
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
IF "__!MCU!__" == "__esp32s3__" (
@REM We are working with ESP32-S3
SET "OTA_FILENAME=bleota-s3.bin"
) ELSE IF "__!MCU!__" == "__esp32c3__" (
@REM We are working with ESP32-C3
SET "OTA_FILENAME=bleota-c3.bin"
) ELSE (
@REM Everything else
SET "OTA_FILENAME=bleota.bin"
)
@REM Determine OTA filename based on MCU type (unified OTA format)
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix.

View File

@@ -131,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
exit 1
fi
# Determine OTA filename based on MCU type
if [ "$MCU" == "esp32s3" ]; then
OTAFILE=bleota-s3.bin
elif [ "$MCU" == "esp32c3" ]; then
OTAFILE=bleota-c3.bin
else
OTAFILE=bleota.bin
fi
# Determine OTA filename based on MCU type (unified OTA format)
OTAFILE="mt-${MCU}-ota.bin"
# Set SPIFFS filename with "littlefs-" prefix.
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"

View File

@@ -50,8 +50,6 @@ build_flags = -Wno-missing-field-initializers
-DRADIOLIB_EXCLUDE_APRS=1
-DRADIOLIB_EXCLUDE_LORAWAN=1
-DMESHTASTIC_EXCLUDE_DROPZONE=1
-DMESHTASTIC_EXCLUDE_ZPS=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

View File

@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED");
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_ON);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, LOW);
#else
digitalWrite(BLE_LED, HIGH);
#endif
#endif
break;
case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED");
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_OFF);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif
break;
}

View File

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

View File

@@ -72,11 +72,13 @@ RTCSetResult readFromRTC()
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
SensorPCF85063 rtc;
#endif
uint32_t now = millis();
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
@@ -240,10 +242,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
SensorPCF85063 rtc;
#endif
SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);

View File

@@ -1498,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu()
config.display.use_long_node_name = option.value;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
});

View File

@@ -877,15 +877,15 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// Send Message (Right side)
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
// Top Right Corner
display->drawRect(x1 - 1, topY, 2, 1);
display->drawRect(x1, topY, 2, 1);
display->drawRect(x1, topY, 1, 2);
// Bottom Right Corner
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
display->drawRect(x1, bottomY - 3, 1, 2);
// Knock the corners off to make a bubble
display->setColor(BLACK);
display->drawRect(x1 - bubbleW + 2, topY - 1, 1, 1);
display->drawRect(x1 - bubbleW + 2, bottomY - 1, 1, 1);
display->drawRect(x1 - bubbleW, topY - 1, 1, 1);
display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1);
display->setColor(WHITE);
} else {
// Received Message (Left Side)

View File

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

View File

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

View File

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

View File

@@ -366,7 +366,11 @@ void setup()
#ifdef BLE_LED
pinMode(BLE_LED, OUTPUT);
digitalWrite(BLE_LED, LED_STATE_OFF);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif
concurrency::hasBeenSetup = true;
@@ -489,9 +493,7 @@ void setup()
// The ThinkNodes have their own blink logic
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
#else
ledPeriodic = new Periodic("Blink", ledBlinker);
#endif
fsInit();
@@ -832,7 +834,7 @@ void setup()
SPI.begin();
#endif
#else
// ESP32
// ESP32
#if defined(HW_SPI1_DEVICE)
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);

View File

@@ -1404,15 +1404,6 @@ void NodeDB::loadFromDisk()
if (portduino_config.has_configDisplayMode) {
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
}
if (portduino_config.has_statusMessage) {
moduleConfig.has_statusmessage = true;
strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(),
sizeof(moduleConfig.statusmessage.node_status));
moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0';
}
if (portduino_config.enable_UDP) {
config.network.enabled_protocols = true;
}
#endif
}
@@ -1553,7 +1544,6 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
moduleConfig.has_ambient_lighting = true;
moduleConfig.has_audio = true;
moduleConfig.has_paxcounter = true;
moduleConfig.has_statusmessage = true;
success &=
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);

View File

@@ -22,10 +22,14 @@
class UdpMulticastHandler final
{
public:
UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); }
UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); }
void start()
{
if (isRunning) {
LOG_DEBUG("UDP multicast already running");
return;
}
if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) {
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3],
@@ -34,13 +38,29 @@ class UdpMulticastHandler final
LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str());
#endif
udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); });
isRunning = true;
} else {
LOG_DEBUG("Failed to listen on UDP");
}
}
void stop()
{
if (!isRunning) {
return;
}
LOG_DEBUG("Stopping UDP multicast");
#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
udp.close();
#endif
isRunning = false;
}
void onReceive(AsyncUDPPacket packet)
{
if (!isRunning) {
return;
}
size_t packetLength = packet.length();
#if defined(ARCH_NRF52)
IPAddress ip = packet.remoteIP();
@@ -67,7 +87,7 @@ class UdpMulticastHandler final
bool onSend(const meshtastic_MeshPacket *mp)
{
if (!mp || !udp) {
if (!isRunning || !mp || !udp) {
return false;
}
#if defined(ARCH_NRF52)
@@ -92,5 +112,6 @@ class UdpMulticastHandler final
private:
IPAddress udpIpAddress;
AsyncUDP udp;
bool isRunning;
};
#endif // HAS_UDP_MULTICAST

View File

@@ -391,6 +391,11 @@ static void WiFiEvent(WiFiEvent_t event)
LOG_INFO("Disconnected from WiFi access point");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, LOW);
#endif
#if HAS_UDP_MULTICAST
if (udpHandler) {
udpHandler->stop();
}
#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
@@ -417,6 +422,11 @@ static void WiFiEvent(WiFiEvent_t event)
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
LOG_INFO("Lost IP address and IP address is reset to 0");
#if HAS_UDP_MULTICAST
if (udpHandler) {
udpHandler->stop();
}
#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
syslog.disable();

View File

@@ -905,11 +905,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
{
bool shouldReboot = true;
// If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag,
meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) {
if (!hasOpenEditTransaction &&
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
disableBluetooth();
}
@@ -1001,14 +1000,8 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
moduleConfig.has_paxcounter = true;
moduleConfig.paxcounter = c.payload_variant.paxcounter;
break;
case meshtastic_ModuleConfig_statusmessage_tag:
LOG_INFO("Set module config: StatusMessage");
moduleConfig.has_statusmessage = true;
moduleConfig.statusmessage = c.payload_variant.statusmessage;
shouldReboot = false;
break;
}
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
saveChanges(SEGMENT_MODULECONFIG);
return true;
}
@@ -1187,11 +1180,6 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
break;
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
LOG_INFO("Get module config: StatusMessage");
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
break;
}
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.

View File

@@ -90,13 +90,6 @@
#if !MESHTASTIC_EXCLUDE_DROPZONE
#include "modules/DropzoneModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
#include "modules/StatusMessageModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_ZPS
#include "modules/esp32/ZPSModule.h"
#endif
#if defined(HAS_HARDWARE_WATCHDOG)
#include "watchdog/watchdogThread.h"
@@ -157,12 +150,6 @@ void setupModules()
#if !MESHTASTIC_EXCLUDE_DROPZONE
dropzoneModule = new DropzoneModule();
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
statusMessageModule = new StatusMessageModule();
#endif
#if !MESHTASTIC_EXCLUDE_ZPS
zpsModule = new ZPSModule();
#endif
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
new GenericThreadModule();
#endif

View File

@@ -1,183 +0,0 @@
#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 "ReplyBotModule.h"
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "configuration.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,18 +0,0 @@
#pragma once
#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

View File

@@ -1,41 +0,0 @@
#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;
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);
}
}
return ProcessMessage::CONTINUE;
}
#endif

View File

@@ -1,35 +0,0 @@
#pragma once
#if !MESHTASTIC_EXCLUDE_STATUS
#include "SinglePortModule.h"
#include "configuration.h"
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)
}
virtual int32_t runOnce() override;
protected:
/** Called to handle a particular incoming message
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
private:
};
extern StatusMessageModule *statusMessageModule;
#endif

View File

@@ -1,419 +0,0 @@
/*
* ZPS - Zero-GPS Positioning System for standalone Meshtastic devices
* - experimental tools for estimating own position without a GPS -
*
* Copyright 2021 all rights reserved by https://github.com/a-f-G-U-C
* Released under GPL v3 (see LICENSE file for details)
*/
#include "ZPSModule.h"
#include "Default.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "NodeStatus.h"
#include "Router.h"
#include "configuration.h"
#include "gps/RTC.h"
#include <WiFi.h>
#if !defined(MESHTASTIC_EXCLUDE_BLUETOOTH)
#include "NimBLEDevice.h"
#define BLE_MAX_REC 15
#define BLE_NO_RESULTS -1 // Indicates a BLE scan is in progress
uint8_t bleCounter = 0; // used internally by the ble scanner
uint64_t bleResult[BLE_MAX_REC + 1];
int bleResSize = BLE_NO_RESULTS;
uint64_t scanStart = 0;
ZPSModule *zpsModule;
// Mini BLE scanner, NIMBLE based and modelled loosely after the Wifi scanner
static int ble_scan(uint32_t duration, bool passive = true, bool dedup = true);
// ZPSModule::ZPSModule()
// : ProtobufModule("ZPS", ZPS_PORTNUM, Position_fields), concurrency::OSThread("ZPSModule")
ZPSModule::ZPSModule() : SinglePortModule("ZPS", ZPS_PORTNUM), concurrency::OSThread("ZPSModule")
{
setIntervalFromNow(ZPS_STARTUP_DELAY); // Delay startup by 10 seconds, no need to race :)
wantBSS = true;
wantBLE = true;
WiFi.mode(WIFI_STA);
WiFi.disconnect();
WiFi.scanNetworks(true, true); // nonblock, showhidden
scanState = SCAN_BSS_RUN;
}
ProcessMessage ZPSModule::handleReceived(const meshtastic_MeshPacket &mp)
{
meshtastic_Position pos = meshtastic_Position_init_default;
auto &pd = mp.decoded;
uint8_t nRecs = pd.payload.size >> 3;
LOG_DEBUG("handleReceived %s 0x%0x->0x%0x, id=0x%x, port=%d, len=%d, rec=%d\n", name, mp.from, mp.to, mp.id, pd.portnum,
pd.payload.size, nRecs);
if (nRecs > ZPS_DATAPKT_MAXITEMS)
nRecs = ZPS_DATAPKT_MAXITEMS;
memcpy(&netData, pd.payload.bytes, nRecs << 3);
// Currently we are unable to act as a position server, so we're
// not interested in broadcasts (this will change later)
if (mp.to != nodeDB->getNodeNum()) {
// Message is not for us, won't process
return ProcessMessage::CONTINUE;
}
#ifdef ZPS_EXTRAVERBOSE
for (int i = 0; i < nRecs; i++) {
LOG_DEBUG("ZPS[%d]: %08x"
"%08x\n",
i, (uint32_t)(netData[i] >> 32), (uint32_t)netData[i]);
}
#endif
if ((netData[0] & 0x800000000000) && (nRecs >= 2)) {
// message contains a position
pos.PDOP = (netData[0] >> 40) & 0x7f;
pos.timestamp = netData[0] & 0xffffffff;
// second int64 encodes lat and lon
pos.longitude_i = (int32_t)(netData[1] & 0xffffffff);
pos.latitude_i = (int32_t)((netData[1] >> 32) & 0xffffffff);
// FIXME should be conditional, to ensure we don't overwrite a good GPS fix!
LOG_DEBUG("ZPS lat/lon/dop/pts %d/%d/%d/%d\n", pos.latitude_i, pos.longitude_i, pos.PDOP, pos.timestamp);
// Some required fields
pos.time = getTime();
pos.location_source = meshtastic_Position_LocSource_LOC_EXTERNAL;
// don't update position if my gps fix is valid
if (nodeDB->hasValidPosition(nodeDB->getMeshNode(nodeDB->getNodeNum()))) {
LOG_DEBUG("ZPSModule::handleReceived: ignoring position update, GPS is valid\n");
return ProcessMessage::CONTINUE;
}
nodeDB->updatePosition(nodeDB->getNodeNum(), pos);
} else {
// nothing we can do - for now
return ProcessMessage::CONTINUE;
}
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
meshtastic_MeshPacket *ZPSModule::allocReply()
{
meshtastic_MeshPacket *p = allocDataPacket();
p->decoded.payload.size = (netRecs + 2) << 3; // actually can be only +1 if no GPS data
LOG_DEBUG("Allocating dataPacket for %d items, %d bytes\n", netRecs, p->decoded.payload.size);
memcpy(p->decoded.payload.bytes, &netData, p->decoded.payload.size);
return (p);
}
void ZPSModule::sendDataPacket(NodeNum dest, bool wantReplies)
{
// cancel any not yet sent (now stale) position packets
if (prevPacketId)
service->cancelSending(prevPacketId);
meshtastic_MeshPacket *p = allocReply();
p->to = dest;
p->decoded.portnum = meshtastic_PortNum_ZPS_APP;
p->decoded.want_response = wantReplies;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
prevPacketId = p->id;
service->sendToMesh(p, RX_SRC_LOCAL);
}
int32_t ZPSModule::runOnce()
{
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
assert(node);
// LOG_DEBUG("ZPSModule::runOnce() START, scanState: %d\n", (int) scanState);
int numWifi = 0;
if (scanState == SCAN_BSS_RUN) {
// check completion status of any running Wifi scan
numWifi = WiFi.scanComplete();
if (numWifi >= 0) {
// scan is complete
LOG_DEBUG("%d BSS found\n", numWifi);
LOG_DEBUG("BSS scan done in %d millis\n", millis() - scanStart);
if (wantBSS && haveBSS) {
// old data exists, overwrite it
netRecs = 0;
haveBSS = haveBLE = false;
}
for (int i = 0; i < numWifi; i++) {
// pack each Wifi network record into a 64-bit int
uint64_t netBytes = encodeBSS(WiFi.BSSID(i), WiFi.channel(i), abs(WiFi.RSSI(i)));
if (wantBSS) {
// load into outbound array if needed
outBufAdd(netBytes);
haveBSS = true;
}
#ifdef ZPS_EXTRAVERBOSE
LOG_DEBUG("BSS[%02d]: %08x"
"%08x\n",
i, (uint32_t)(netBytes >> 32), (uint32_t)netBytes);
#endif
}
WiFi.scanDelete();
scanState = SCAN_BSS_DONE;
#ifdef ZPS_EXTRAVERBOSE
} else if (numWifi == -1) {
// LOG_DEBUG("BSS scan in-progress\n");
} else {
LOG_DEBUG("BSS scan state=%d\n", numWifi);
#endif
}
}
if ((scanState == SCAN_BLE_RUN) && (bleResSize >= 0)) {
// completion status checked above (bleResSize >= 0)
LOG_DEBUG("BLE scan done in %d millis\n", millis() - scanStart);
scanState = SCAN_BLE_DONE;
if (wantBLE && haveBLE) {
// old data exists, overwrite it
netRecs = 0;
haveBSS = haveBLE = false;
}
for (int i = 0; i < bleResSize; i++) {
// load data into output array if needed
if (wantBLE) {
outBufAdd(bleResult[i]);
haveBLE = true;
}
#ifdef ZPS_EXTRAVERBOSE
LOG_DEBUG("BLE[%d]: %08x"
"%08x\n",
i, (uint32_t)(bleResult[i] >> 32), (uint32_t)bleResult[i]);
#endif
}
// Reset the counter once we're done with the dataset
bleResSize = BLE_NO_RESULTS;
}
// Are we finished assembling that packet? Then send it out
if ((wantBSS == haveBSS) && (wantBLE == haveBLE) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil() &&
(lastSend == 0 || millis() - lastSend >= Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs,
default_broadcast_interval_secs,
nodeStatus->getNumOnline()))) {
haveBSS = haveBLE = false;
sendDataPacket(NODENUM_BROADCAST, false); // no replies
lastSend = millis();
netRecs = 0; // reset packet
}
/*
* State machine transitions
*
* FIXME could be managed better, for example: check if we require
* each type of scan (wantBSS/wantBLE), and if not, don't start it!
*/
if (scanState == SCAN_BLE_DONE) {
// BLE done, transition to BSS scanning
scanStart = millis();
LOG_DEBUG("BSS scan start t=%d\n", scanStart);
if (WiFi.scanNetworks(true, true) == WIFI_SCAN_RUNNING) // nonblock, showhidden
scanState = SCAN_BSS_RUN;
} else if (scanState == SCAN_BSS_DONE) {
// BSS done, transition to BLE scanning
scanStart = millis();
LOG_DEBUG("BLE scan start t=%d\n", scanStart);
if (ble_scan(ZPS_BLE_SCANTIME) == 0)
scanState = SCAN_BLE_RUN;
}
// LOG_DEBUG("ZPSModule::runOnce() DONE, scanState=%d\n", scanState);
if ((scanState == SCAN_BSS_RUN) || (scanState == SCAN_BLE_RUN)) {
return 1000; // scan in progress, re-check soon
}
return 5000;
}
uint64_t encodeBSS(uint8_t *bssid, uint8_t chan, uint8_t absRSSI)
{
uint64_t netBytes = absRSSI & 0xff;
netBytes <<= 8;
netBytes |= (chan & 0xff);
for (uint8_t b = 0; b < 6; b++) {
netBytes <<= 8;
netBytes |= bssid[b];
}
return netBytes;
}
uint64_t encodeBLE(uint8_t *addr, uint8_t absRSSI)
{
uint64_t netBytes = absRSSI & 0xff;
netBytes <<= 8;
netBytes |= 0xff; // "channel" byte reserved in BLE records
for (uint8_t b = 0; b < 6; b++) {
netBytes <<= 8;
netBytes |= addr[5 - b] & 0xff;
}
return netBytes;
}
/**
* Event handler
*/
static int ble_gap_event(struct ble_gap_event *event, void *arg)
{
// Adverts matching certain patterns are useless for positioning purposes
// (ephemeral MAC etc), so try excluding them if possible
//
// TODO: Expand the list of reject patterns for BLE adverts.
// There are likely more than 10 patterns to test and reject, including most Apple devices and others.
//
// TODO: Implement full packet search for reject patterns (use memmem() or similar),
// not just at the beginning (currently uses memcmp()).
const uint8_t rejPat[] = {0x1e, 0xff, 0x06, 0x00, 0x01}; // one of many
struct ble_hs_adv_fields fields;
int rc;
int i = 0;
uint64_t netBytes = 0;
switch (event->type) {
case BLE_GAP_EVENT_DISC:
// called once for every BLE advert received
rc = ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data);
if (rc != 0)
return 0;
if (bleResSize != BLE_NO_RESULTS)
// as far as we know, we're not in the middle of a BLE scan!
LOG_DEBUG("Unexpected BLE_GAP_EVENT_DISC!\n");
#ifdef ZPS_EXTRAVERBOSE
// Dump the advertisement packet
DEBUG_PORT.hexDump("DEBUG", (unsigned char *)event->disc.data, event->disc.length_data);
#endif
// Reject beacons known to be unreliable (ephemeral etc)
if (memcmp(event->disc.data, rejPat, sizeof(rejPat)) == 0) {
LOG_DEBUG("(BLE item filtered by pattern)\n");
return 0; // Processing-wise, it's still a success
}
//
// STORE THE RESULTS IN A SORTED LIST
//
// first, pack each BLE item reading into a 64-bit int
netBytes = encodeBLE(event->disc.addr.val, abs(event->disc.rssi));
// SOME DUPLICATES SURVIVE through filter_duplicates = 1, catch them here
// Duplicate filtering is now handled in the sorting loop below,
// but right now we write for clarity not optimization
for (i = 0; i < bleCounter; i++) {
if ((bleResult[i] & 0xffffffffffff) == (netBytes & 0xffffffffffff)) {
LOG_DEBUG("(BLE duplicate filtered)\n");
return 0;
}
}
#ifdef ZPS_EXTRAVERBOSE
// redundant extraverbosity, but I need it for duplicate hunting
LOG_DEBUG("BL_[%02d]: %08x"
"%08x\n",
bleCounter, (uint32_t)(netBytes >> 32), (uint32_t)netBytes);
#endif
// then insert item into a list (up to BLE_MAX_REC records), sorted by RSSI
for (i = 0; i < bleCounter; i++) {
// find first element greater than ours, that will be our insertion point
if (bleResult[i] > netBytes)
break;
}
// any other records move down one position to vacate res[i]
for (int j = bleCounter; j > i; j--)
bleResult[j] = bleResult[j - 1];
// write new element at insertion point
bleResult[i] = netBytes;
// advance tail of list, but not beyond limit
if (bleCounter < BLE_MAX_REC)
bleCounter++;
return 0; // SUCCESS
case BLE_GAP_EVENT_DISC_COMPLETE:
LOG_DEBUG("EVENT_DISC_COMPLETE in %d millis\n", (millis() - scanStart));
LOG_DEBUG("%d BLE found\n", bleCounter);
bleResSize = bleCounter;
bleCounter = 0; // reset counter
return 0; // SUCCESS
default:
return 0; // SUCCESS
}
}
/**
* Initiates the GAP general discovery procedure (non-blocking)
*/
static int ble_scan(uint32_t duration, bool passive, bool dedup)
{
uint8_t own_addr_type;
struct ble_gap_disc_params disc_params;
int rc;
// Figure out address type to use
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
LOG_DEBUG("error determining address type; rc=%d\n", rc);
return rc;
}
// Scanning parameters, these are mostly default
disc_params.itvl = 0;
disc_params.window = 0;
disc_params.filter_policy = 0;
disc_params.limited = 0;
// These two params are the more interesting ones
disc_params.filter_duplicates = dedup; // self-explanatory
disc_params.passive = passive; // passive uses less power
// Start scanning process (non-blocking) and return
rc = ble_gap_disc(own_addr_type, duration, &disc_params, ble_gap_event, NULL);
if (rc != 0) {
LOG_DEBUG("error initiating GAP discovery; rc=%d\n", rc);
}
return rc;
}
#endif // MESHTASTIC_EXCLUDE_BLUETOOTH

View File

@@ -1,86 +0,0 @@
#pragma once
#include "SinglePortModule.h"
#include "concurrency/OSThread.h"
#include "gps/RTC.h"
#define ZPS_PORTNUM meshtastic_PortNum_ZPS_APP
#define ZPS_DATAPKT_MAXITEMS 20 // max number of records to pack in an outbound packet (~10)
#define ZPS_STARTUP_DELAY 10000 // Module startup delay in millis
// Duration of a BLE scan in millis.
// We want this number to be SLIGHTLY UNDER an integer number of seconds,
// to be able to catch the result as fresh as possible on a 1-second polling loop
#define ZPS_BLE_SCANTIME 2900 // millis
enum SCANSTATE { SCAN_NONE, SCAN_BSS_RUN, SCAN_BSS_DONE, SCAN_BLE_RUN, SCAN_BLE_DONE };
/*
* Data packing "compression" functions
* Ingest a WiFi BSSID, channel and RSSI (or BLE address and RSSI)
* and encode them into a packed uint64
*/
uint64_t encodeBSS(uint8_t *bssid, uint8_t chan, uint8_t absRSSI);
uint64_t encodeBLE(uint8_t *addr, uint8_t absRSSI);
class ZPSModule : public SinglePortModule, private concurrency::OSThread
{
/// The id of the last packet we sent, to allow us to cancel it if we make something fresher
PacketId prevPacketId = 0;
/// We limit our broadcasts to a max rate
uint32_t lastSend = 0;
bool wantBSS = true;
bool haveBSS = false;
bool wantBLE = true;
bool haveBLE = false;
public:
/** Constructor
* name is for debugging output
*/
ZPSModule();
/**
* Send our radio environment data into the mesh
*/
void sendDataPacket(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
protected:
/** Called to handle a particular incoming message
@return true if you've guaranteed you've handled this message and no other handlers should be considered for it
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp);
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked
* so that subclasses can (optionally) send a response back to the original sender. */
virtual meshtastic_MeshPacket *allocReply();
/** Does our periodic broadcast */
virtual int32_t runOnce();
private:
// outbound data packet staging buffer and record counter
uint64_t netData[ZPS_DATAPKT_MAXITEMS + 2] = {0};
uint8_t netRecs = 0;
// mini state machine to alternate between BSS(Wifi) and BLE scanning
SCANSTATE scanState = SCAN_NONE;
inline void outBufAdd(uint64_t netBytes)
{
// If this is the first record, initialize the header with the current time and reset the record count.
if (!netRecs) {
netData[0] = getTime();
netData[1] = 0;
}
// push to buffer and update counter
if (netRecs < ZPS_DATAPKT_MAXITEMS)
netData[2 + (netRecs++)] = netBytes;
}
};
extern ZPSModule *zpsModule;

View File

@@ -757,7 +757,11 @@ void NimbleBluetooth::deinit()
isDeInit = true;
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_OFF);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
#endif
#ifndef NIMBLE_TWO
NimBLEDevice::deinit();

View File

@@ -24,6 +24,11 @@
#include <nvs.h>
#include <nvs_flash.h>
// Weak empty variant shutdown prep function.
// May be redefined by variant files.
void variant_shutdown() __attribute__((weak));
void variant_shutdown() {}
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
void setBluetoothEnable(bool enable)
{
@@ -249,6 +254,7 @@ void cpuDeepSleep(uint32_t msecToWake)
#endif // #end ESP32S3_WAKE_TYPE
#endif
variant_shutdown();
// We want RTC peripherals to stay on
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);

View File

@@ -36,6 +36,13 @@ bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t p
return udp.endPacket();
}
void AsyncUDP::close()
{
udp.stop();
localPort = 0;
_onPacket = nullptr;
}
// AsyncUDPPacket
AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort())
{

View File

@@ -22,6 +22,7 @@ class AsyncUDP : public Print, private concurrency::OSThread
bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64);
bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port);
void close();
size_t write(uint8_t b) override;
size_t write(const uint8_t *data, size_t len) override;

View File

@@ -46,7 +46,7 @@
uint16_t getVDDVoltage();
// Weak empty variant initialization function.
// Weak empty variant shutdown prep function.
// May be redefined by variant files.
void variant_shutdown() __attribute__((weak));
void variant_shutdown() {}

View File

@@ -872,7 +872,6 @@ bool loadConfig(const char *configPath)
}
if (yamlConfig["Config"]) {
portduino_config.has_config_overrides = true;
if (yamlConfig["Config"]["DisplayMode"]) {
portduino_config.has_configDisplayMode = true;
if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "TWOCOLOR") {
@@ -885,13 +884,6 @@ bool loadConfig(const char *configPath)
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
}
}
if (yamlConfig["Config"]["StatusMessage"]) {
portduino_config.has_statusMessage = true;
portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as<std::string>("");
}
if ((yamlConfig["Config"]["EnableUDP"]).as<bool>(false)) {
portduino_config.enable_UDP = true;
}
}
if (yamlConfig["General"]) {

View File

@@ -177,12 +177,8 @@ extern struct portduino_config_struct {
int hostMetrics_channel = 0;
// config
bool has_config_overrides = false;
int configDisplayMode = 0;
bool has_configDisplayMode = false;
std::string statusMessage = "";
bool has_statusMessage = false;
bool enable_UDP = false;
// General
std::string mac_address = "";
@@ -509,30 +505,21 @@ extern struct portduino_config_struct {
}
// config
if (has_config_overrides) {
if (has_configDisplayMode) {
out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
if (has_configDisplayMode) {
switch (configDisplayMode) {
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
break;
}
}
if (has_statusMessage) {
out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage;
}
if (enable_UDP) {
out << YAML::Key << "EnableUDP" << YAML::Value << true;
switch (configDisplayMode) {
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
break;
}
out << YAML::EndMap; // Config

View File

@@ -26,6 +26,7 @@
#undef GPS_TX_PIN
#define NO_GPS 1
#define HAS_GPS 0
#define NO_SCREEN
#define HAS_SCREEN 0
// Default SPI1 will be mapped to the display

View File

@@ -34,3 +34,5 @@ lib_deps = ${esp32s3_base.lib_deps}
https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip
# renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino
maxpromer/PCA9557-arduino@1.0.0
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4

View File

@@ -1,12 +1,17 @@
#include "variant.h"
#include <PCA9557.h>
PCA9557 io(0x18, &Wire);
PCA9557 io(0x18, &Wire1);
void earlyInitVariant()
{
Wire.begin(48, 47);
Wire1.begin(48, 47);
io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
io.digitalWrite(PCA_PIN_POWER_EN, HIGH);
}
void variant_shutdown()
{
io.digitalWrite(PCA_PIN_POWER_EN, LOW);
}

View File

@@ -30,6 +30,9 @@
#define I2C_SCL 1
#define I2C_SDA 2
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
// GPS pins
#define GPS_SWITH 10
#define HAS_GPS 1

View File

@@ -8,6 +8,7 @@
#define EXT_NOTIFY_OUT 22
#define BUTTON_PIN 0 // 17
// #define LED_PIN PIN_LED
// Board has RGB LED 21
#define HAS_NEOPIXEL // Enable the use of neopixels
#define NEOPIXEL_COUNT 1 // How many neopixels are connected

View File

@@ -24,5 +24,5 @@ lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM
khoih-prog/nRF52_PWM@1.0.1
; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4

View File

@@ -116,7 +116,6 @@ extern "C" {
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
#define HAS_RTC 1
#ifdef __cplusplus
}

View File

@@ -21,5 +21,5 @@ build_flags = ${nrf52840_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6>
lib_deps =
${nrf52840_base.lib_deps}
; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4

View File

@@ -121,7 +121,6 @@ static const uint8_t A0 = PIN_A0;
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
#define HAS_RTC 1
// SPI
#define SPI_INTERFACES_COUNT 1

View File

@@ -35,6 +35,8 @@ extern "C" {
#define PIN_LED2 LED_BLUE
#define PIN_LED3 LED_RED
#define PIN_LED PIN_LED1
#define LED_STATE_ON 1 // State when LED is lit
// XIAO Wio-SX1262 Shield User button

View File

@@ -51,6 +51,7 @@ extern "C" {
#define LED_GREEN PIN_LED1
#define BLE_LED LED_BLUE
#define BLE_LED_INVERTED 1
#define LED_STATE_ON 0 // State when LED is lit
// Buttons

View File

@@ -132,9 +132,6 @@ static const uint8_t A0 = PIN_A0;
#define HAS_DRV2605 1
// Battery / ADC already defined above
#define HAS_RTC 1
#define SERIAL_PRINT_PORT 0
#ifdef __cplusplus

View File

@@ -7,7 +7,7 @@
#define ADC_RESOLUTION (12u)
// LEDs
#define LED_PIN (24u)
#define PIN_LED (24u)
// Serial
#define PIN_SERIAL1_TX (16u)

View File

@@ -5,6 +5,8 @@
#define EXT_NOTIFY_OUT 0xFFFFFFFF
#define BUTTON_PIN 0xFFFFFFFF
#define LED_PIN PIN_LED
#define USE_RF95 // RFM95/SX127x
#undef LORA_SCK

View File

@@ -23,7 +23,8 @@ static const uint8_t A2 = PIN_A2;
static const uint8_t A3 = PIN_A3;
// LEDs
#define PIN_LED1 (23u)
#define PIN_LED (23u)
#define PIN_LED1 PIN_LED
#define LED_NOTIFICATION (24u)
#define ADC_RESOLUTION 12

View File

@@ -10,7 +10,7 @@
#define I2C_SDA1 2
#define I2C_SCL1 3
#define LED_PIN PIN_LED1
#define LED_PIN PIN_LED
#define ledOff(pin) pinMode(pin, INPUT)
#define BUTTON_PIN 9

View File

@@ -11,7 +11,8 @@ static const uint8_t A2 = PIN_A2;
static const uint8_t A3 = PIN_A3;
// LEDs
#define PIN_LED1 (23u)
#define PIN_LED (23u)
#define PIN_LED1 PIN_LED
#define ADC_RESOLUTION 12

View File

@@ -5,7 +5,7 @@
#define BUTTON_PIN 2
#define BUTTON_NEED_PULLUP
#define LED_PIN PIN_LED1
#define LED_PIN PIN_LED
#define ledOff(pin) pinMode(pin, INPUT)
#undef BATTERY_PIN