Compare commits

..

5 Commits

Author SHA1 Message Date
Jonathan Bennett
b056c405e6 Add evicted age and accessor in NodeDB 2026-01-18 22:16:20 -06:00
Jason P
49accefd8b Don't Mute DMs just because we mute a channel (#9348)
* Don't Mute DMs just because we mute a channel

* Updated code to consolidate muting
2026-01-18 16:39:23 -05:00
Jason P
02f24b9015 Improve BaseUI Preset Change Flow (#9343)
* Reset Channel Number to 0 on Preset Change

* Add Channel Picker to LoRa Options

* Change Channel to Frequency Slot

* Catch comparison issue

* Reset override_frequency to ensure we correctly move to new Radio Preset

* CoPilot Suggestions
2026-01-18 16:38:46 -05:00
Catalin Patulea
33ae3777a3 toradio, fromradio OPTIONS handler: fix sending proper HTTP response. (#9322)
Before this (missing response):
$ curl -v -X OPTIONS http://meshtastic.local/api/v1/fromradio
* Host meshtastic.local:80 was resolved.
* IPv6: (none)
* IPv4: 192.168.0.19
*   Trying 192.168.0.19:80...
* Connected to meshtastic.local (192.168.0.19) port 80
* using HTTP/1.x
> OPTIONS /api/v1/fromradio HTTP/1.1
> Host: meshtastic.local
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
* Empty reply from server
* shutting down connection #0
curl: (52) Empty reply from server

After this (proper HTTP 204 response):
$ curl -v -X OPTIONS http://meshtastic.local/api/v1/fromradio
* Host meshtastic.local:80 was resolved.
* IPv6: (none)
* IPv4: 192.168.0.19
*   Trying 192.168.0.19:80...
* Connected to meshtastic.local (192.168.0.19) port 80
* using HTTP/1.x
> OPTIONS /api/v1/fromradio HTTP/1.1
> Host: meshtastic.local
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 204 OK
< Content-Type: application/x-protobuf
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET
< X-Protobuf-Schema: https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto
<
* Connection #0 to host meshtastic.local left intact

This is related to https://github.com/meshtastic/firmware/issues/5385.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-18 07:41:24 -06:00
Ted W.
021106dfe5 Add support for setting API port from the config file (#8435)
* Add support for setting API port from the config file

* Update PortduinoGlue.cpp

Fix typo in var identifier

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-17 15:23:16 -06:00
20 changed files with 162 additions and 119 deletions

View File

@@ -13,11 +13,6 @@ extern MemGet memGet;
#define LED_STATE_ON 1
#endif
// WIFI LED
#ifndef WIFI_STATE_ON
#define WIFI_STATE_ON 1
#endif
// -----------------------------------------------------------------------------
// DEBUG
// -----------------------------------------------------------------------------
@@ -152,9 +147,7 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
// Default Bluetooth PIN
#define defaultBLEPin 123456
#if HAS_ETHERNET && defined(USE_CH390D)
#include <ESP32_CH390.h>
#elif HAS_ETHERNET && !defined(USE_WS5500)
#if HAS_ETHERNET && !defined(USE_WS5500)
#include <RAK13800_W5100S.h>
#endif // HAS_ETHERNET

View File

@@ -438,7 +438,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
if (currentResolution == ScreenResolution::UltraLow) {
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
} else {
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num);
}
}
size_t len = strlen(frequencyslot);

View File

@@ -65,12 +65,12 @@ uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
@@ -78,6 +78,8 @@ void menuHandler::loraMenu()
menuHandler::menuQueue = menuHandler::device_role_picker;
} else if (selected == radio_preset_picker) {
menuHandler::menuQueue = menuHandler::radio_preset_picker;
} else if (selected == frequency_slot) {
menuHandler::menuQueue = menuHandler::frequency_slot;
} else if (selected == lora_picker) {
menuHandler::menuQueue = menuHandler::lora_picker;
}
@@ -248,6 +250,113 @@ void menuHandler::DeviceRolePicker()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::FrequencySlotPicker()
{
enum ReplyOptions : int { Back = -1 };
constexpr int MAX_CHANNEL_OPTIONS = 202;
static const char *optionsArray[MAX_CHANNEL_OPTIONS];
static int optionsEnumArray[MAX_CHANNEL_OPTIONS];
static char channelText[MAX_CHANNEL_OPTIONS - 1][12];
int options = 0;
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
optionsArray[options] = "Slot 0 (Auto)";
optionsEnumArray[options++] = 0;
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
double bw = loraConfig.bandwidth;
if (loraConfig.use_preset) {
switch (loraConfig.modem_preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
bw = (myRegion->wideLora) ? 1625.0 : 500;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
bw = (myRegion->wideLora) ? 1625.0 : 500;
break;
default:
bw = (myRegion->wideLora) ? 812.5 : 250;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
bw = (myRegion->wideLora) ? 406.25 : 125;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
bw = (myRegion->wideLora) ? 406.25 : 125;
break;
}
} else {
bw = loraConfig.bandwidth;
if (bw == 31) // This parameter is not an integer
bw = 31.25;
if (bw == 62) // Fix for 62.5Khz bandwidth
bw = 62.5;
if (bw == 200)
bw = 203.125;
if (bw == 400)
bw = 406.25;
if (bw == 800)
bw = 812.5;
if (bw == 1600)
bw = 1625.0;
}
uint32_t numChannels = 0;
if (myRegion) {
numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0)));
} else {
LOG_WARN("Region not set, cannot calculate number of channels");
return;
}
if (numChannels > (uint32_t)(MAX_CHANNEL_OPTIONS - 2))
numChannels = (uint32_t)(MAX_CHANNEL_OPTIONS - 2);
for (uint32_t ch = 1; ch <= numChannels; ch++) {
snprintf(channelText[ch - 1], sizeof(channelText[ch - 1]), "Slot %lu", (unsigned long)ch);
optionsArray[options] = channelText[ch - 1];
optionsEnumArray[options++] = (int)ch;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Frequency Slot";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
// Start highlight on current channel if possible, otherwise on "1"
int initial = (int)config.lora.channel_num + 1;
if (initial < 2 || initial > (int)numChannels + 1)
initial = 1;
bannerOptions.InitialSelected = initial;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
}
config.lora.channel_num = selected;
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::RadioPresetPicker()
{
static const RadioPresetOption presetOptions[] = {
@@ -278,6 +387,8 @@ void menuHandler::RadioPresetPicker()
}
config.lora.modem_preset = option.value;
config.lora.channel_num = 0; // Reset to default channel for the preset
config.lora.override_frequency = 0; // Clear any custom frequency
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
});
@@ -2551,6 +2662,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case radio_preset_picker:
RadioPresetPicker();
break;
case frequency_slot:
FrequencySlotPicker();
break;
case no_timeout_lora_picker:
LoraRegionPicker(0);
break;

View File

@@ -13,6 +13,7 @@ class menuHandler
lora_picker,
device_role_picker,
radio_preset_picker,
frequency_slot,
no_timeout_lora_picker,
TZ_picker,
twelve_hour_picker,
@@ -63,6 +64,7 @@ class menuHandler
static void loraMenu();
static void DeviceRolePicker();
static void RadioPresetPicker();
static void FrequencySlotPicker();
static void handleMenuSwitch(OLEDDisplay *display);
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
static void clockMenu();

View File

@@ -337,7 +337,7 @@ void setup()
#ifdef WIFI_LED
pinMode(WIFI_LED, OUTPUT);
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
digitalWrite(WIFI_LED, LOW);
#endif
#ifdef BLE_LED

View File

@@ -25,7 +25,7 @@ template class LR11x0Interface<LR1121>;
template class SX126xInterface<STM32WLx>;
#endif
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#if HAS_ETHERNET && !defined(USE_WS5500)
#include "api/ethServerAPI.h"
template class ServerAPI<EthernetClient>;
template class APIServerPort<ethServerAPI, EthernetServer>;

View File

@@ -2029,6 +2029,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
}
// if we found a "boring" node, evict it
if (oldestBoringIndex != -1) {
evictedAge = sinceLastSeen(&meshNodes->at(oldestBoringIndex));
oldestIndex = oldestBoringIndex;
}
@@ -2040,6 +2041,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
(numMeshNodes)--;
}
}
LOG_WARN("Evicted node %u secs old", evictedAge);
// add the node at the end
lite = &meshNodes->at((numMeshNodes)++);

View File

@@ -307,7 +307,10 @@ class NodeDB
newStatus.notifyObservers(&status);
}
uint32_t get_evicted_age() { return evictedAge; }
private:
uint32_t evictedAge = UINT32_MAX;
bool duplicateWarned = false;
bool localPositionUpdatedSinceBoot = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash

View File

@@ -1,7 +1,7 @@
#include "configuration.h"
#include <Arduino.h>
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#if HAS_ETHERNET && !defined(USE_WS5500)
#include "ethServerAPI.h"

View File

@@ -1,7 +1,7 @@
#pragma once
#include "ServerAPI.h"
#if !defined(USE_WS5500) && !defined(USE_CH390D)
#ifndef USE_WS5500
#include <RAK13800_W5100S.h>
/**

View File

@@ -173,7 +173,7 @@ void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res)
if (req->getMethod() == "OPTIONS") {
res->setStatusCode(204); // Success with no content
// res->print(""); @todo remove
res->print("");
return;
}
@@ -223,7 +223,7 @@ void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res)
if (req->getMethod() == "OPTIONS") {
res->setStatusCode(204); // Success with no content
// res->print(""); @todo remove
res->print("");
return;
}

View File

@@ -384,13 +384,13 @@ static void WiFiEvent(WiFiEvent_t event)
#endif
}
#ifdef WIFI_LED
digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON);
digitalWrite(WIFI_LED, HIGH);
#endif
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
LOG_INFO("Disconnected from WiFi access point");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
digitalWrite(WIFI_LED, LOW);
#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
@@ -442,13 +442,13 @@ static void WiFiEvent(WiFiEvent_t event)
case ARDUINO_EVENT_WIFI_AP_START:
LOG_INFO("WiFi access point started");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON);
digitalWrite(WIFI_LED, HIGH);
#endif
break;
case ARDUINO_EVENT_WIFI_AP_STOP:
LOG_INFO("WiFi access point stopped");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
digitalWrite(WIFI_LED, LOW);
#endif
break;
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:

View File

@@ -1238,7 +1238,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
}
#endif
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#if HAS_ETHERNET && !defined(USE_WS5500)
conn.has_ethernet = true;
conn.ethernet.has_status = true;
if (Ethernet.linkStatus() == LinkON) {

View File

@@ -460,12 +460,15 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
}
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
bool mutedNode = false;
if (sender) {
mutedNode = (sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK);
}
meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex());
// If we receive a broadcast message, apply channel mute setting
// If we receive a direct message and the receipent is us, apply DM mute setting
// Else we just handle it as not muted.
const bool directToUs = !isBroadcast(mp.to) && isToUs(&mp);
bool is_muted = directToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0))
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
if (moduleConfig.external_notification.alert_bell) {
if (containsBell) {
LOG_INFO("externalNotificationModule - Notification Bell");
@@ -516,8 +519,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
}
}
if (moduleConfig.external_notification.alert_message && !mutedNode &&
(!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) {
if (moduleConfig.external_notification.alert_message && !is_muted) {
LOG_INFO("externalNotificationModule - Notification Module");
isNagging = true;
setExternalState(0, true);
@@ -528,8 +530,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
}
}
if (moduleConfig.external_notification.alert_message_vibra && !mutedNode &&
(!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) {
if (moduleConfig.external_notification.alert_message_vibra && !is_muted) {
LOG_INFO("externalNotificationModule - Notification Module (Vibra)");
isNagging = true;
setExternalState(1, true);
@@ -540,8 +541,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
}
}
if (moduleConfig.external_notification.alert_message_buzzer && !mutedNode &&
(!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) {
if (moduleConfig.external_notification.alert_message_buzzer && !is_muted) {
LOG_INFO("externalNotificationModule - Notification Module (Buzzer)");
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(!isBroadcast(mp.to) && isToUs(&mp))) {

View File

@@ -15,7 +15,7 @@
#include <WiFiClientSecure.h>
#endif
#endif
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#if HAS_ETHERNET && !defined(USE_WS5500)
#include <EthernetClient.h>
#endif

View File

@@ -55,6 +55,7 @@ void cpuDeepSleep(uint32_t msecs)
void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel");
int TCPPort = SERVER_API_DEFAULT_PORT;
bool checkConfigPort = true;
static error_t parse_opt(int key, char *arg, struct argp_state *state)
{
@@ -63,6 +64,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
if (sscanf(arg, "%d", &TCPPort) < 1)
return ARGP_ERR_UNKNOWN;
else
checkConfigPort = false;
printf("Using config file %d\n", TCPPort);
break;
case 'c':
@@ -870,6 +872,14 @@ bool loadConfig(const char *configPath)
std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl;
exit(EXIT_FAILURE);
}
if (checkConfigPort) {
portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as<int>(-1);
if (portduino_config.api_port != -1 &&
portduino_config.api_port > 1023 &&
portduino_config.api_port < 65536) {
TCPPort = (portduino_config.api_port);
}
}
portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as<std::string>("");
if (portduino_config.mac_address != "") {
portduino_config.mac_address_explicit = true;

View File

@@ -175,6 +175,7 @@ extern struct portduino_config_struct {
std::string mac_address = "";
bool mac_address_explicit = false;
std::string mac_address_source = "";
int api_port = -1;
std::string config_directory = "";
std::string available_directory = "/etc/meshtasticd/available.d/";
int maxtophone = 100;
@@ -508,6 +509,8 @@ extern struct portduino_config_struct {
out << YAML::Key << "General" << YAML::Value << YAML::BeginMap;
if (config_directory != "")
out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory;
if (api_port != -1)
out << YAML::Key << "TCPPort" << YAML::Value << api_port;
if (mac_address_explicit)
out << YAML::Key << "MACAddress" << YAML::Value << mac_address;
if (mac_address_source != "")
@@ -519,4 +522,4 @@ extern struct portduino_config_struct {
out << YAML::EndMap; // General
return out.c_str();
}
} portduino_config;
} portduino_config;

View File

@@ -1,26 +0,0 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
// The default Wire will be mapped to PMU and RTC
static const uint8_t SDA = 17;
static const uint8_t SCL = 18;
// Default SPI will be mapped to Radio
static const uint8_t SS = 39;
static const uint8_t MOSI = 40;
static const uint8_t MISO = 41;
static const uint8_t SCK = 42;
// #define SPI_MOSI (11)
// #define SPI_SCK (10)
// #define SPI_MISO (9)
// #define SPI_CS (12)
// #define SDCARD_CS SPI_CS
#endif /* Pins_Arduino_h */

View File

@@ -1,22 +0,0 @@
[env:thinknode_g3]
extends = esp32s3_base
board = ESP32-S3-WROOM-1-N4
board_build.psram_type = opi
build_flags =
${esp32s3_base.build_flags}
-D ELECROW_ThinkNode_G3
-D HAS_UDP_MULTICAST=1
-D BOARD_HAS_PSRAM
-D PRIVATE_HW
# -D CONFIG_ETH_ENABLED=1
# -D CONFIG_ETH_USE_ESP32_EMAC=1
-I variants/esp32s3/ELECROW-ThinkNode-G3
-mfix-esp32-psram-cache-issue
lib_ignore =
Ethernet
lib_deps =
${esp32s3_base.lib_deps}
# file://../ESP32-CH390/ESP32-CH390-1.0.1.tar.gz

View File

@@ -1,36 +0,0 @@
#define HAS_GPS 0
#define HAS_WIRE 0
#define I2C_NO_RESCAN
#define WIFI_LED 5
#define WIFI_STATE_ON 0
#define LED_PIN 6 // The blue LORA LED
#define LED_STATE_ON 0
#define BUTTON_PIN 4 // the external user button of the device, BOOT and RESET are not accessible without opening it up.
#define USE_SX1262
#define LORA_SCK 42
#define LORA_MISO 41
#define LORA_MOSI 40
#define LORA_CS 39
#define LORA_RESET 21
#define SX126X_CS LORA_CS
#define SX126X_DIO1 15
#define SX126X_BUSY 47
#define SX126X_RESET LORA_RESET
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define PIN_POWER_EN 45
// #define HAS_ETHERNET 1
// #define USE_CH390D 1 // this driver uses the same stack as the ESP32 Wifi driver
// #define ETH_MISO_PIN 47
// #define ETH_MOSI_PIN 21
// #define ETH_SCLK_PIN 48
// #define ETH_CS_PIN 45
// #define ETH_INT_PIN 14
// #define ETH_RST_PIN -1
// #define ETH_ADDR 1