Compare commits

...
Author SHA1 Message Date
Ben MeadorsandCopilot c7b7c3f90d Update src/graphics/draw/MessageRenderer.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-13 11:31:19 -05:00
Ben Meadors fc2316b292 Refine message history limits for resource-constrained builds and cap cached lines to prevent heap overflow 2026-04-13 11:06:30 -05:00
Ben Meadors 263ed1d0ce Optimize message frame allocation to prevent excessive memory usage 2026-04-13 09:14:56 -05:00
Ben Meadors 7d10602f6a Set MESSAGE_HISTORY_LIMIT to 10 for original ESP32 to optimize RAM usage 2026-04-13 09:14:22 -05:00
Ben MeadorsandCopilot 6f0e3c5b68 Update src/graphics/draw/MessageRenderer.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-13 09:13:02 -05:00
Ben Meadors d05232b8b2 Fix heap blowout on TBeams 2026-04-13 08:49:33 -05:00
Thomas Göttgens e42ff3590c fix last cppcheck issue (#10154) 2026-04-13 15:48:30 +02:00
Ben Meadors 7527233130 Enhance release notes generation with commit range comparison 2026-04-13 06:43:11 -05:00
197226365b fix(native): implement BinarySemaphorePosix with proper pthread synchronization (#9895)
* fix(native): implement BinarySemaphorePosix with proper pthread synchronization

The BinarySemaphorePosix class (used on all Linux/portduino/native builds)
had stub implementations: give() was a no-op and take() just called
delay(msec) and returned false. This broke the cooperative thread scheduler
on native platforms — threads could not wake the main loop, radio RX
interrupts were missed, and telemetry never transmitted over the mesh.

Replace the stubs with a proper binary semaphore using pthread_mutex_t +
pthread_cond_t + bool signaled:

- take(msec): pthread_cond_timedwait with CLOCK_REALTIME timeout, consumes
  signal atomically (binary semaphore semantics)
- give(): sets signaled=true, signals condition variable
- giveFromISR(): delegates to give(), sets pxHigherPriorityTaskWoken

Tested on Raspberry Pi 3 Model B (ARM64, Debian Bookworm) with Adafruit
LoRa Radio Bonnet (SX1276). Before fix: no radio TX/RX, no telemetry on
mesh. After fix: bidirectional LoRa, MQTT gateway, telemetry all working.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ARCH_PORTDUINO

* Refactor BinarySemaphorePosix header for ARCH_PORTDUINO

* Change preprocessor directive from ifndef to ifdef

* Gate new Semaphore code to Portduino and fix STM compilation

* Binary Semaphore Posix better error handling

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-04-13 06:31:38 -05:00
7 changed files with 173 additions and 37 deletions
+4 -2
View File
@@ -301,10 +301,12 @@ jobs:
id: release_notes id: release_notes
run: | run: |
chmod +x ./bin/generate_release_notes.py chmod +x ./bin/generate_release_notes.py
NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }}) NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD 2>release_notes.log)
echo "notes<<EOF" >> $GITHUB_OUTPUT echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
echo "### Release note range" >> $GITHUB_STEP_SUMMARY
cat release_notes.log >> $GITHUB_STEP_SUMMARY
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -466,7 +468,7 @@ jobs:
- name: Generate release notes - name: Generate release notes
run: | run: |
chmod +x ./bin/generate_release_notes.py chmod +x ./bin/generate_release_notes.py
./bin/generate_release_notes.py ${{ needs.version.outputs.long }} > ./publish/release_notes.md ./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD > ./publish/release_notes.md
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+53 -28
View File
@@ -1,25 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """Generate release notes from the actual release commit range."""
Generate release notes from merged PRs on develop and master branches.
Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections.
"""
import subprocess import argparse
import re
import json import json
import re
import subprocess
import sys import sys
from datetime import datetime
def get_last_release_tag(): def get_last_release_tag(compare_ref, exclude_tag=None):
"""Get the most recent release tag.""" """Get the most recent version tag merged into compare_ref."""
result = subprocess.run( result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"], ["git", "tag", "--merged", compare_ref, "--sort=-version:refname", "v*"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True, check=True,
) )
return result.stdout.strip()
for line in result.stdout.splitlines():
candidate = line.strip()
if not candidate:
continue
if exclude_tag and candidate == exclude_tag:
continue
return candidate
raise subprocess.CalledProcessError(result.returncode, result.args, output=result.stdout, stderr=result.stderr)
def get_tag_date(tag): def get_tag_date(tag):
@@ -33,18 +39,18 @@ def get_tag_date(tag):
return result.stdout.strip() return result.stdout.strip()
def get_merged_prs_since_tag(tag, branch): def get_merged_prs_in_range(tag, compare_ref):
"""Get all merged PRs since the given tag on the specified branch.""" """Get all merged PRs in the git range between tag and compare_ref."""
# Get commits since tag on the branch - look for PR numbers in parentheses
result = subprocess.run( result = subprocess.run(
[ [
"git", "git",
"log", "log",
f"{tag}..origin/{branch}", f"{tag}..{compare_ref}",
"--oneline", "--oneline",
], ],
capture_output=True, capture_output=True,
text=True, text=True,
check=True,
) )
prs = [] prs = []
@@ -65,6 +71,25 @@ def get_merged_prs_since_tag(tag, branch):
return prs return prs
def parse_args():
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(
description="Generate release notes from the actual release commit range."
)
parser.add_argument("new_version", help="Version that will be tagged for this release")
parser.add_argument(
"--base-tag",
dest="base_tag",
help="Existing version tag to diff from. Defaults to the latest version tag merged into the compare ref.",
)
parser.add_argument(
"--compare-ref",
default="HEAD",
help="Git ref to diff to. Defaults to HEAD.",
)
return parser.parse_args()
def get_pr_details(pr_number): def get_pr_details(pr_number):
"""Get PR details from GitHub API via gh CLI.""" """Get PR details from GitHub API via gh CLI."""
try: try:
@@ -268,28 +293,28 @@ def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"):
def main(): def main():
if len(sys.argv) < 2: args = parse_args()
print("Usage: generate_release_notes.py <new_version>", file=sys.stderr) new_version = args.new_version
sys.exit(1) compare_ref = args.compare_ref
current_tag = f"v{new_version}"
new_version = sys.argv[1]
# Get last release tag # Get last release tag
try: try:
last_tag = get_last_release_tag() last_tag = args.base_tag or get_last_release_tag(compare_ref, exclude_tag=current_tag)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print("Error: Could not find last release tag", file=sys.stderr) print("Error: Could not find last release tag", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Collect PRs from both branches print(
all_pr_numbers = set() f"Resolved release note range: {last_tag}..{compare_ref}",
file=sys.stderr,
)
for branch in ["develop", "master"]:
try: try:
prs = get_merged_prs_since_tag(last_tag, branch) all_pr_numbers = set(get_merged_prs_in_range(last_tag, compare_ref))
all_pr_numbers.update(prs) except subprocess.CalledProcessError as e:
except Exception as e: print(f"Error: Could not get PRs for range {last_tag}..{compare_ref}: {e}", file=sys.stderr)
print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr) sys.exit(1)
# Get details for all PRs # Get details for all PRs
enhancements = [] enhancements = []
+7
View File
@@ -21,8 +21,15 @@
// How many messages are stored (RAM + flash). // How many messages are stored (RAM + flash).
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage. // Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
#ifndef MESSAGE_HISTORY_LIMIT #ifndef MESSAGE_HISTORY_LIMIT
#if defined(ARCH_ESP32) && \
!(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2))
// Baseline ESP32 (non-PSRAM variants) has limited heap; reduce message history on resource-constrained builds.
// Override with -DMESSAGE_HISTORY_LIMIT=N if needed.
#define MESSAGE_HISTORY_LIMIT 10
#else
#define MESSAGE_HISTORY_LIMIT 20 #define MESSAGE_HISTORY_LIMIT 20
#endif #endif
#endif
// Internal alias used everywhere in code – do NOT redefine elsewhere. // Internal alias used everywhere in code – do NOT redefine elsewhere.
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT #define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
+76
View File
@@ -1,10 +1,85 @@
#include "concurrency/BinarySemaphorePosix.h" #include "concurrency/BinarySemaphorePosix.h"
#include "configuration.h" #include "configuration.h"
#include <errno.h>
#include <sys/time.h>
#ifndef HAS_FREE_RTOS #ifndef HAS_FREE_RTOS
namespace concurrency namespace concurrency
{ {
#ifdef ARCH_PORTDUINO
BinarySemaphorePosix::BinarySemaphorePosix()
{
if (pthread_mutex_init(&mutex, NULL) != 0) {
throw std::runtime_error("pthread_mutex_init failed");
}
if (pthread_cond_init(&cond, NULL) != 0) {
pthread_mutex_destroy(&mutex);
throw std::runtime_error("pthread_cond_init failed");
}
signaled = false;
}
BinarySemaphorePosix::~BinarySemaphorePosix()
{
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
/**
* Returns false if we timed out
*/
bool BinarySemaphorePosix::take(uint32_t msec)
{
pthread_mutex_lock(&mutex);
if (!signaled) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += msec / 1000;
ts.tv_nsec += (msec % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) {
ts.tv_sec += 1;
ts.tv_nsec -= 1000000000L;
}
while (!signaled) {
int rc = pthread_cond_timedwait(&cond, &mutex, &ts);
if (rc == ETIMEDOUT)
break;
if (rc != 0) {
// Some other error occurred
pthread_mutex_unlock(&mutex);
throw std::runtime_error("pthread_cond_timedwait failed: " + std::to_string(rc));
}
}
}
bool wasSignaled = signaled;
signaled = false; // consume the signal (binary semaphore)
pthread_mutex_unlock(&mutex);
return wasSignaled;
}
void BinarySemaphorePosix::give()
{
pthread_mutex_lock(&mutex);
signaled = true;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken)
{
give();
if (pxHigherPriorityTaskWoken)
*pxHigherPriorityTaskWoken = true;
}
#else
BinarySemaphorePosix::BinarySemaphorePosix() {} BinarySemaphorePosix::BinarySemaphorePosix() {}
@@ -22,6 +97,7 @@ bool BinarySemaphorePosix::take(uint32_t msec)
void BinarySemaphorePosix::give() {} void BinarySemaphorePosix::give() {}
IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) {} IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) {}
#endif
} // namespace concurrency } // namespace concurrency
+10 -1
View File
@@ -2,6 +2,10 @@
#include "../freertosinc.h" #include "../freertosinc.h"
#ifdef ARCH_PORTDUINO
#include <pthread.h>
#endif
namespace concurrency namespace concurrency
{ {
@@ -9,7 +13,12 @@ namespace concurrency
class BinarySemaphorePosix class BinarySemaphorePosix
{ {
// SemaphoreHandle_t semaphore;
#ifdef ARCH_PORTDUINO
pthread_mutex_t mutex;
pthread_cond_t cond;
bool signaled;
#endif
public: public:
BinarySemaphorePosix(); BinarySemaphorePosix();
+20 -2
View File
@@ -422,6 +422,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
std::vector<bool> isMine; // track alignment std::vector<bool> isMine; // track alignment
std::vector<bool> isHeader; // track header lines std::vector<bool> isHeader; // track header lines
std::vector<AckStatus> ackForLine; std::vector<AckStatus> ackForLine;
// Hard limit on total cached lines to prevent unbounded growth from a single long message.
// Reserve to the actual cache cap up front, because a single message can expand to many more
// wrapped display lines than a small per-message estimate would predict. For a display
// rendering only ~5-30 lines at a time, caching more than this limit wastes heap. Stop
// appending once we reach MAX_CACHED_LINES to prevent a single message from blowing out the
// heap.
constexpr size_t MAX_CACHED_LINES = 100U; // ~5-6KB for std::string overhead on 32-bit (if each ~50-60 bytes avg)
allLines.reserve(MAX_CACHED_LINES);
isMine.reserve(MAX_CACHED_LINES);
isHeader.reserve(MAX_CACHED_LINES);
ackForLine.reserve(MAX_CACHED_LINES);
for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) {
const auto &m = *it; const auto &m = *it;
@@ -565,16 +576,23 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int wrapWidth = mine ? rightTextWidth : leftTextWidth; int wrapWidth = mine ? rightTextWidth : leftTextWidth;
std::vector<std::string> wrapped = generateLines(display, "", msgText, wrapWidth); std::vector<std::string> wrapped = generateLines(display, "", msgText, wrapWidth);
// Per-message wrap-line limit: even if wrapping produces many lines, cap them to prevent
// a single long message from consuming most or all of the cache.
constexpr size_t MAX_WRAPPED_LINES_PER_MSG = 20U;
size_t wrappedCount = 0;
for (auto &ln : wrapped) { for (auto &ln : wrapped) {
allLines.push_back(ln); if (allLines.size() >= MAX_CACHED_LINES || wrappedCount >= MAX_WRAPPED_LINES_PER_MSG)
break; // Cache limit or per-message limit reached; stop adding lines from this message
allLines.emplace_back(std::move(ln));
isMine.push_back(mine); isMine.push_back(mine);
isHeader.push_back(false); isHeader.push_back(false);
ackForLine.push_back(AckStatus::NONE); ackForLine.push_back(AckStatus::NONE);
++wrappedCount;
} }
} }
// Cache lines and heights // Cache lines and heights
cachedLines = allLines; cachedLines.swap(allLines);
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine); std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine);
-1
View File
@@ -121,7 +121,6 @@ void CardputerKeyboard::pressed(uint8_t key)
modifierFlag = 0; modifierFlag = 0;
} }
uint8_t next_key = 0;
int row = (key - 1) / 10; int row = (key - 1) / 10;
int col = (key - 1) % 10; int col = (key - 1) % 10;