Compare commits

..

9 Commits

Author SHA1 Message Date
Ben Meadors
c10a0c9c81 Fix OTA partition name matching 2026-01-13 18:50:55 -06:00
renovate[bot]
5610d4809c Update meshtastic/device-ui digest to 5a870c6 (#9301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 15:59:09 -06:00
github-actions[bot]
dae4061b06 Update protobufs (#9299)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-13 05:58:12 -06:00
Mike Robbins
e99853f660 SafeFile: use atomic rename-with-overwrite, rather than non-atomic delete-then-rename (#9296) 2026-01-13 05:57:04 -06:00
github-actions[bot]
3640e35a8b Upgrade trunk (#9297)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-13 05:50:40 -06:00
Ben Meadors
782ffdc5cd Merge branch 'develop' 2026-01-13 05:48:19 -06:00
Ben Meadors
6f36f39da9 Fix up T-Beam 1W HW_MODEL 2026-01-13 05:48:14 -06:00
Ben Meadors
ded4f57cb7 Partition name in manifest script (#9294)
* Fix up T-Beam 1W HW_MODEL

* Add part_name for bin files

* app0
2026-01-13 05:47:08 -06:00
HarukiToreda
3a0f3520d1 BaseUI: Autosave Messages (#9269)
* Autosave Messages

* fix

* Add logging, code cleanup, and add save on delete.

* We already save as part of delete messages, no need to do it again

* fix spelling errors

* Updating comment

---------

Co-authored-by: Jason P <applewiz@mac.com>
2026-01-12 19:40:44 -06:00
20 changed files with 168 additions and 91 deletions

View File

@@ -91,8 +91,8 @@ jobs:
if [[ -f "$manifest" ]]; then
echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)"
# Add OTA entry to files array if not already present
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" \
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes}] else . end' \
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \
"$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest"
fi
done

View File

@@ -9,10 +9,10 @@ plugins:
lint:
enabled:
- checkov@3.2.497
- renovate@42.78.2
- renovate@42.80.1
- prettier@3.7.4
- trufflehog@3.92.4
- yamllint@1.37.1
- yamllint@1.38.0
- bandit@1.9.2
- trivy@0.68.2
- taplo@0.10.0

View File

@@ -60,6 +60,14 @@ def manifest_gather(source, target, env):
board_platform = env.BoardConfig().get("platform")
board_mcu = env.BoardConfig().get("build.mcu").lower()
needs_ota_suffix = board_platform == "nordicnrf52"
# Mapping of bin files to their target partition names
# Maps the filename pattern to the partition name where it should be flashed
partition_map = {
f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0)
lfsbin: "spiffs", # filesystem image flashed to spiffs
}
check_paths = [
progname,
f"{progname}.elf",
@@ -85,6 +93,9 @@ def manifest_gather(source, target, env):
"md5": f.get_content_hash(), # Returns MD5 hash
"bytes": f.get_size() # Returns file size in bytes
}
# Add part_name if this file represents a partition that should be flashed
if p in partition_map:
d["part_name"] = partition_map[p]
out.append(d)
print(d)
manifest_write(out, env)

View File

@@ -119,7 +119,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/14dc991a4b57066e0a83599be1a87ff08e5e5308.zip
https://github.com/meshtastic/device-ui/archive/5a870c623a4e9ab7a7abe3d02950536f107d1a31.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]

View File

@@ -13,6 +13,11 @@
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc)
#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC
#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60)
#endif
// Global message text pool and state
static char *g_messagePool = nullptr;
static size_t g_poolWritePos = 0;
@@ -102,6 +107,60 @@ void MessageStore::addLiveMessage(const StoredMessage &msg)
pushWithLimit(liveMessages, msg);
}
#if ENABLE_MESSAGE_PERSISTENCE
static bool g_messageStoreHasUnsavedChanges = false;
static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved
static inline uint32_t autosaveIntervalMs()
{
uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC;
if (sec < 60)
sec = 60;
return sec * 1000UL;
}
static inline bool reachedMs(uint32_t now, uint32_t target)
{
return (int32_t)(now - target) >= 0;
}
// Mark new messages in RAM that need to be saved later
static inline void markMessageStoreUnsaved()
{
g_messageStoreHasUnsavedChanges = true;
if (g_lastAutoSaveMs == 0) {
g_lastAutoSaveMs = millis();
}
}
// Called periodically from the main loop in main.cpp
static inline void autosaveTick(MessageStore *store)
{
if (!store)
return;
uint32_t now = millis();
if (g_lastAutoSaveMs == 0) {
g_lastAutoSaveMs = now;
return;
}
if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs()))
return;
// Autosave interval reached, only save if there are unsaved messages.
if (g_messageStoreHasUnsavedChanges) {
LOG_INFO("Autosaving MessageStore to flash");
store->saveToFlash();
} else {
LOG_INFO("Autosave skipped, no changes to save");
g_lastAutoSaveMs = now;
}
}
#endif
// Add from incoming/outgoing packet
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{
@@ -131,6 +190,11 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
}
addLiveMessage(sm);
#if ENABLE_MESSAGE_PERSISTENCE
markMessageStoreUnsaved();
#endif
return liveMessages.back();
}
@@ -155,6 +219,10 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
sm.ackStatus = AckStatus::NONE;
addLiveMessage(sm);
#if ENABLE_MESSAGE_PERSISTENCE
markMessageStoreUnsaved();
#endif
}
#if ENABLE_MESSAGE_PERSISTENCE
@@ -239,6 +307,10 @@ void MessageStore::saveToFlash()
f.close();
#endif
// Reset autosave state after any save
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
}
void MessageStore::loadFromFlash()
@@ -270,6 +342,9 @@ void MessageStore::loadFromFlash()
f.close();
#endif
// Loading messages does not trigger an autosave
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
}
#else
@@ -290,6 +365,11 @@ void MessageStore::clearAllMessages()
f.write(&count, 1); // write "0 messages"
f.close();
#endif
#if ENABLE_MESSAGE_PERSISTENCE
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
#endif
}
// Internal helper: erase first or last message matching a predicate
@@ -421,6 +501,14 @@ uint16_t MessageStore::storeText(const char *src, size_t len)
return storeTextInPool(src, len);
}
#if ENABLE_MESSAGE_PERSISTENCE
void messageStoreAutosaveTick()
{
// Called from the main loop to check autosave timing
autosaveTick(&messageStore);
}
#endif
// Global definition
MessageStore messageStore("default");
#endif
#endif

View File

@@ -125,6 +125,11 @@ class MessageStore
std::string filename; // Flash filename for persistence
};
#if ENABLE_MESSAGE_PERSISTENCE
// Called periodically from main loop to trigger time based autosave
void messageStoreAutosaveTick();
#endif
// Global instance (defined in MessageStore.cpp)
extern MessageStore messageStore;

View File

@@ -54,7 +54,7 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size)
}
/**
* Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches
* Atomically close the file (overwriting any old version) and readback the contents to confirm the hash matches
*
* @return false for failure
*/
@@ -73,15 +73,7 @@ bool SafeFile::close()
if (!testReadback())
return false;
{ // Scope for lock
concurrency::LockGuard g(spiLock);
// brief window of risk here ;-)
if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) {
LOG_ERROR("Can't remove old pref file");
return false;
}
}
// Rename or overwrite (atomic operation)
String filenameTmp = filename;
filenameTmp += ".tmp";
if (!renameFile(filenameTmp.c_str(), filename.c_str())) {

View File

@@ -269,9 +269,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define TCA9535_ADDR 0x20
#define TCA9555_ADDR 0x26
// used for display brightness control
#define STC8H1K28_ADDR 0x30
// -----------------------------------------------------------------------------
// Touchscreen
// -----------------------------------------------------------------------------

View File

@@ -62,7 +62,6 @@ class ScanI2C
NAU7802,
FT6336U,
STK8BAXX,
STC8H1K28,
ICM20948,
SCD4X,
MAX30102,

View File

@@ -238,8 +238,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#endif
#ifdef HAS_LP5562
SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address);
#else
SCAN_SIMPLE_CASE(STC8H1K28_ADDR, LP5562, "STC8H1K28", (uint8_t)addr.address);
#endif
case XPOWERS_AXP192_AXP2101_ADDRESS:
// Do we have the axp2101/192 or the TCA8418

View File

@@ -449,7 +449,7 @@ void menuHandler::clockMenu()
}
void menuHandler::messageResponseMenu()
{
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, MuteChannel, Aloud, enumEnd };
enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
@@ -479,7 +479,7 @@ void menuHandler::messageResponseMenu()
// Delete submenu
optionsArray[options] = "Delete";
optionsEnumArray[options++] = 900;
optionsEnumArray[options++] = DeleteMenu;
#ifdef HAS_I2S
optionsArray[options] = "Read Aloud";
@@ -520,34 +520,10 @@ void menuHandler::messageResponseMenu()
nodeDB->saveToDisk();
}
// Delete submenu
} else if (selected == 900) {
} else if (selected == DeleteMenu) {
menuHandler::menuQueue = menuHandler::delete_messages_menu;
screen->runNow();
// Delete oldest FIRST (only change)
} else if (selected == DeleteOldest) {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
// Global oldest
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
// Oldest in current channel
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
// Oldest in current DM
messageStore.deleteOldestMessageWithPeer(peer);
}
// Delete all messages
} else if (selected == DeleteAll) {
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
#ifdef HAS_I2S
} else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
@@ -716,7 +692,6 @@ void menuHandler::deleteMessagesMenu()
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteOldestMessageWithPeer(peer);
}
return;
}
@@ -729,7 +704,6 @@ void menuHandler::deleteMessagesMenu()
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteAllMessagesWithPeer(peer);
}
return;
}
};

View File

@@ -38,6 +38,9 @@
#include "target_specific.h"
#include <memory>
#include <utility>
#if HAS_SCREEN
#include "MessageStore.h"
#endif
#ifdef ELECROW_ThinkNode_M5
PCA9557 io(0x18, &Wire);
@@ -1652,6 +1655,9 @@ void loop()
if (dispdev)
static_cast<TFTDisplay *>(dispdev)->sdlLoop();
}
#endif
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
messageStoreAutosaveTick();
#endif
long delayMsec = mainController.runOrDelay();

View File

@@ -66,7 +66,7 @@ typedef enum _meshtastic_Config_DeviceConfig_Role {
but should not be given priority over other routers in order to avoid unnecessaraily
consuming hops. */
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11,
/* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
/* Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */

View File

@@ -1,31 +1,53 @@
#include "BleOta.h"
#include "Arduino.h"
#include <cctype>
#include <esp_ota_ops.h>
#include <string>
static const String MESHTASTIC_OTA_APP_PROJECT_NAME("Meshtastic-OTA");
static bool isMeshtasticOtaProject(const esp_app_desc_t &desc)
{
std::string name(desc.project_name);
return name.find("Meshtastic") != std::string::npos && name.find("OTA") != std::string::npos;
}
const esp_partition_t *BleOta::findEspOtaAppPartition()
{
const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr);
esp_app_desc_t app_desc;
esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
esp_err_t ret = ESP_ERR_INVALID_ARG;
if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) {
part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr);
// Try standard OTA slots first (app0 / app1)
const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr);
if (part) {
ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
}
if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) {
return part;
} else {
return nullptr;
if (!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc)) {
part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr);
if (part) {
ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
}
}
// Fallback: look by partition label "app1" in case table uses custom labels
if ((!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc))) {
part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, "app1");
if (part) {
ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
}
}
if (part && ret == ESP_OK && isMeshtasticOtaProject(app_desc)) {
return part;
}
return nullptr;
}
String BleOta::getOtaAppVersion()
{
const esp_partition_t *part = findEspOtaAppPartition();
if (!part) {
return String();
}
esp_app_desc_t app_desc;
esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
String version;

View File

@@ -195,6 +195,8 @@
#define HW_VENDOR meshtastic_HardwareModel_LINK_32
#elif defined(T_DECK_PRO)
#define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
#elif defined(T_BEAM_1W)
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT
#elif defined(T_LORA_PAGER)
#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
#elif defined(HELTEC_V4)

View File

@@ -164,6 +164,3 @@ build_flags =
${crowpanel_large_esp32s3_base.build_flags}
-D VIEW_320x240
-D DISPLAY_SIZE=800x480 ; landscape mode
build_src_filter =
${esp32s3_base.build_src_filter}
+<../variants/esp32s3/elecrow_panel>

View File

@@ -1,21 +0,0 @@
// meshtastic/firmware/variants/elecrow_panel/variant.cpp
#include "variant.h"
#include "Arduino.h"
#include "Wire.h"
bool elecrow_v2 = false; // false = v1, true = v2
extern "C" {
void initVariant()
{
Wire.begin(I2C_SDA, I2C_SCL, 100000);
delay(50);
Wire.beginTransmission(0x30);
if (Wire.endTransmission() == 0) {
elecrow_v2 = true;
}
Wire.end();
}
}

View File

@@ -1,8 +1,6 @@
#define I2C_SDA 15
#define I2C_SCL 16
extern bool elecrow_v2; // false = v1, true = v2
#if CROW_SELECT == 1
#define WAKE_ON_TOUCH
#define SCREEN_TOUCH_INT 47
@@ -19,7 +17,7 @@ extern bool elecrow_v2; // false = v1, true = v2
#define DAC_I2S_DOUT 12
#define DAC_I2S_MCLK 8 // don't use GPIO0 because it's assigned to LoRa or button
#else
#define PIN_BUZZER (elecrow_v2 ? 0 : 8)
#define PIN_BUZZER 8
#endif
// GPS via UART1 connector
@@ -74,7 +72,7 @@ extern bool elecrow_v2; // false = v1, true = v2
#define SENSOR_POWER_ON LOW
#else
// 4.3", 5.0", 7.0"
#define LORA_CS (elecrow_v2 ? 8 : 0)
#define LORA_CS 0
#define LORA_SCK 5
#define LORA_MISO 4
#define LORA_MOSI 6

View File

@@ -1,5 +1,14 @@
; LilyGo T-Beam-1W (1 Watt LoRa with external PA)
[env:t-beam-1w]
custom_meshtastic_hw_model = 122
custom_meshtastic_hw_model_slug = TBEAM_1_WATT
custom_meshtastic_architecture = esp32s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = LILYGO T-Beam 1W
custom_meshtastic_images = tbeam-1w.svg
custom_meshtastic_tags = LilyGo
extends = esp32s3_base
board = t-beam-1w
board_build.partitions = default_8MB.csv