Skip to content

Commit e7c02da

Browse files
thebenternCopilot
andcommitted
Merge remote-tracking branch 'origin/master' into develop
Co-authored-by: Copilot <copilot@github.com>
2 parents 7421953 + 7347091 commit e7c02da

14 files changed

Lines changed: 475 additions & 14 deletions

File tree

.trunk/trunk.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins:
99
lint:
1010
enabled:
1111
- checkov@3.2.524
12-
- renovate@43.139.6
12+
- renovate@43.141.0
1313
- prettier@3.8.3
1414
- trufflehog@3.95.2
1515
- yamllint@1.38.0

bin/show-unmerged-prs.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/bin/bash
2+
3+
# Script to show commits in develop that are not in master
4+
# with their associated PR info and commit hashes
5+
#
6+
# Usage:
7+
# ./show-unmerged-prs.sh # Show all unmerged commits
8+
# ./show-unmerged-prs.sh --bugfix # Show only bugfix-labeled PRs
9+
10+
set -e
11+
12+
REPO="firmware"
13+
OWNER="meshtastic"
14+
BASE_BRANCH="master"
15+
HEAD_BRANCH="develop"
16+
LIMIT=100
17+
FILTER_LABEL=""
18+
19+
# Parse arguments
20+
for arg in "$@"; do
21+
case $arg in
22+
--bugfix)
23+
FILTER_LABEL="bugfix"
24+
shift
25+
;;
26+
--feature)
27+
FILTER_LABEL="feature"
28+
shift
29+
;;
30+
--help)
31+
echo "Usage: $0 [OPTIONS]"
32+
echo "Options:"
33+
echo " --bugfix Show only PRs labeled with 'bugfix'"
34+
echo " --feature Show only PRs labeled with 'feature'"
35+
echo " --help Show this help message"
36+
exit 0
37+
;;
38+
esac
39+
done
40+
41+
if [ -n "$FILTER_LABEL" ]; then
42+
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH (filtered by label: $FILTER_LABEL)..."
43+
else
44+
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH..."
45+
fi
46+
echo ""
47+
48+
# Check if gh CLI is available
49+
if ! command -v gh &> /dev/null; then
50+
echo "ERROR: GitHub CLI (gh) not found. Please install it first."
51+
echo "Visit: https://cli.github.com/"
52+
exit 1
53+
fi
54+
55+
# Get commits in develop that are not in master
56+
# For each commit, try to find associated PR
57+
git fetch origin develop master 2>/dev/null || true
58+
59+
# Use git to get the list of commits
60+
commits=$(git log --pretty=format:"%H|%s" origin/master..origin/develop | head -n $LIMIT)
61+
62+
count=0
63+
displayed=0
64+
echo "Commits in $HEAD_BRANCH not in $BASE_BRANCH:"
65+
echo "=============================================="
66+
echo ""
67+
68+
while IFS='|' read -r hash subject; do
69+
((count++))
70+
71+
# Try to find the PR for this commit
72+
# Extract PR number, title, description, and labels
73+
pr_response=$(gh api -X GET "/repos/$OWNER/$REPO/commits/$hash/pulls" \
74+
-H "Accept: application/vnd.github.v3+json" 2>/dev/null | \
75+
jq -r '.[0] | "\(.number)|\(.title)|\(.body // "No description")|\(.labels | map(.name) | join(","))"' 2>/dev/null || echo "||||")
76+
77+
if [ -z "$pr_response" ] || [ "$pr_response" = "||||" ]; then
78+
# If no PR found, skip if filter is active, otherwise show the commit
79+
if [ -z "$FILTER_LABEL" ]; then
80+
((displayed++))
81+
echo "[$displayed] Commit: $hash"
82+
echo " Subject: $subject"
83+
echo " PR: Not found in GitHub"
84+
echo ""
85+
fi
86+
else
87+
IFS='|' read -r pr_num pr_title pr_desc pr_labels <<< "$pr_response"
88+
89+
# Check if filter matches
90+
if [ -n "$FILTER_LABEL" ]; then
91+
# Only show if the label is in the labels list
92+
if ! echo "$pr_labels" | grep -q "$FILTER_LABEL"; then
93+
continue
94+
fi
95+
fi
96+
97+
((displayed++))
98+
echo "[$displayed] PR #$pr_num - $pr_title"
99+
echo " Commit: $hash"
100+
if [ -n "$pr_desc" ] && [ "$pr_desc" != "No description" ]; then
101+
# Truncate description to 200 chars
102+
desc_short="${pr_desc:0:200}"
103+
[ ${#pr_desc} -gt 200 ] && desc_short+="..."
104+
echo " Description: $desc_short"
105+
fi
106+
if [ -n "$pr_labels" ] && [ "$pr_labels" != "" ]; then
107+
echo " Labels: $pr_labels"
108+
fi
109+
echo ""
110+
fi
111+
done <<< "$commits"
112+
113+
echo ""
114+
if [ -n "$FILTER_LABEL" ]; then
115+
echo "Done. Showing $displayed PRs with label '$FILTER_LABEL' from $displayed commits checked."
116+
else
117+
echo "Done. Showing $displayed commits from $HEAD_BRANCH not in $BASE_BRANCH."
118+
fi

platformio.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ build_flags = -Wno-missing-field-initializers
2929
-DUSE_THREAD_NAMES
3030
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
3131
-DPB_ENABLE_MALLOC=1
32+
-DPB_VALIDATE_UTF8=1
3233
-DRADIOLIB_EXCLUDE_CC1101=1
3334
-DRADIOLIB_EXCLUDE_NRF24=1
3435
-DRADIOLIB_EXCLUDE_RF69=1
@@ -66,7 +67,7 @@ monitor_speed = 115200
6667
monitor_filters = direct
6768
lib_deps =
6869
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
69-
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
70+
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/6bfd1f135e1ebe37afd6050bb4b9964cea3fcfda.zip
7071
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
7172
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
7273
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master

src/mesh/HardwareRNG.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
4848
// and return false so callers know no extra mixing occurred.
4949
RadioLibInterface *radio = RadioLibInterface::instance;
5050
if (!radio) {
51-
// Intentionally silent: this path runs during portduinoSetup() before the
52-
// console/SerialConsole is initialized, so LOG_* here would dereference a null pointer.
51+
// This path can run during portduinoSetup() before the console is initialized.
52+
#ifndef PIO_UNIT_TESTING
53+
LOG_ERROR("No radio instance available to provide entropy");
54+
#endif
5355
return false;
5456
}
5557

src/mesh/TypeConversions.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "TypeConversions.h"
22
#include "mesh/generated/meshtastic/deviceonly.pb.h"
33
#include "mesh/generated/meshtastic/mesh.pb.h"
4+
#include "meshUtils.h"
45

56
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
67
{
@@ -82,8 +83,10 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
8283

8384
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
8485
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
86+
sanitizeUtf8(lite.long_name, sizeof(lite.long_name));
8587
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
8688
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
89+
sanitizeUtf8(lite.short_name, sizeof(lite.short_name));
8790
lite.hw_model = user.hw_model;
8891
lite.role = user.role;
8992
lite.is_licensed = user.is_licensed;
@@ -102,8 +105,10 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User
102105
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
103106
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
104107
user.long_name[sizeof(user.long_name) - 1] = '\0';
108+
sanitizeUtf8(user.long_name, sizeof(user.long_name));
105109
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
106110
user.short_name[sizeof(user.short_name) - 1] = '\0';
111+
sanitizeUtf8(user.short_name, sizeof(user.short_name));
107112
user.hw_model = lite.hw_model;
108113
user.role = lite.role;
109114
user.is_licensed = lite.is_licensed;

src/mesh/generated/meshtastic/config.pb.h

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,14 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
289289
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
290290
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
291291
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
292-
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28
292+
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28,
293+
/* EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
294+
meshtastic_Config_LoRaConfig_RegionCode_EU_866 = 29,
295+
/* EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
296+
meshtastic_Config_LoRaConfig_RegionCode_EU_874 = 30,
297+
meshtastic_Config_LoRaConfig_RegionCode_EU_917 = 31,
298+
/* EU 868MHz band, with narrow presets */
299+
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32
293300
} meshtastic_Config_LoRaConfig_RegionCode;
294301

295302
/* Standard predefined channel settings
@@ -319,7 +326,24 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
319326
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8,
320327
/* Long Range - Turbo
321328
This preset performs similarly to LongFast, but with 500Khz bandwidth. */
322-
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9
329+
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9,
330+
/* Lite Fast
331+
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
332+
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC. */
333+
meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST = 10,
334+
/* Lite Slow
335+
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
336+
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC. */
337+
meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW = 11,
338+
/* Narrow Fast
339+
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
340+
Comparable link budget to SHORT_SLOW, but with half the data rate.
341+
Intended to avoid interference with other devices. */
342+
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST = 12,
343+
/* Narrow Slow
344+
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
345+
Comparable link budget and data rate to LONG_FAST. */
346+
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13
323347
} meshtastic_Config_LoRaConfig_ModemPreset;
324348

325349
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
@@ -706,12 +730,12 @@ extern "C" {
706730
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
707731

708732
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
709-
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M
710-
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M+1))
733+
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_EU_N_868
734+
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868+1))
711735

712736
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
713-
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
714-
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1))
737+
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW
738+
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW+1))
715739

716740
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
717741
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT

src/mesh/generated/meshtastic/mesh.pb.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ typedef enum _meshtastic_HardwareModel {
315315
meshtastic_HardwareModel_THINKNODE_M7 = 129,
316316
meshtastic_HardwareModel_THINKNODE_M8 = 130,
317317
meshtastic_HardwareModel_THINKNODE_M9 = 131,
318+
/* The Heltec-V4-R8 uses an ESP32S3R8 chip, plus an SX1262. */
319+
meshtastic_HardwareModel_HELTEC_V4_R8 = 132,
318320
/* ------------------------------------------------------------------------------------------------------------------------------------------
319321
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
320322
------------------------------------------------------------------------------------------------------------------------------------------ */

src/meshUtils.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,93 @@ size_t pb_string_length(const char *str, size_t max_len)
117117
}
118118
}
119119
return len;
120+
}
121+
122+
bool sanitizeUtf8(char *buf, size_t bufSize)
123+
{
124+
if (!buf || bufSize == 0)
125+
return false;
126+
127+
// Ensure null-terminated within buffer
128+
buf[bufSize - 1] = '\0';
129+
130+
bool replaced = false;
131+
size_t i = 0;
132+
size_t len = strlen(buf);
133+
134+
while (i < len) {
135+
uint8_t b = (uint8_t)buf[i];
136+
137+
// Determine expected sequence length from lead byte
138+
size_t seqLen;
139+
uint32_t minCodepoint;
140+
if (b <= 0x7F) {
141+
// ASCII — valid single byte
142+
i++;
143+
continue;
144+
} else if ((b & 0xE0) == 0xC0) {
145+
seqLen = 2;
146+
minCodepoint = 0x80; // Reject overlong
147+
} else if ((b & 0xF0) == 0xE0) {
148+
seqLen = 3;
149+
minCodepoint = 0x800;
150+
} else if ((b & 0xF8) == 0xF0) {
151+
seqLen = 4;
152+
minCodepoint = 0x10000;
153+
} else {
154+
// Invalid lead byte (0x80-0xBF or 0xF8+)
155+
buf[i] = '?';
156+
replaced = true;
157+
i++;
158+
continue;
159+
}
160+
161+
// Check that we have enough bytes remaining
162+
if (i + seqLen > len) {
163+
// Truncated sequence at end of string — replace remaining bytes
164+
for (size_t j = i; j < len; j++) {
165+
buf[j] = '?';
166+
}
167+
replaced = true;
168+
break;
169+
}
170+
171+
// Validate continuation bytes (must be 10xxxxxx)
172+
bool valid = true;
173+
for (size_t j = 1; j < seqLen; j++) {
174+
if (((uint8_t)buf[i + j] & 0xC0) != 0x80) {
175+
valid = false;
176+
break;
177+
}
178+
}
179+
180+
if (valid) {
181+
// Decode codepoint to check for overlong encodings and surrogates
182+
uint32_t cp = 0;
183+
if (seqLen == 2)
184+
cp = b & 0x1F;
185+
else if (seqLen == 3)
186+
cp = b & 0x0F;
187+
else
188+
cp = b & 0x07;
189+
for (size_t j = 1; j < seqLen; j++)
190+
cp = (cp << 6) | ((uint8_t)buf[i + j] & 0x3F);
191+
192+
if (cp < minCodepoint || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
193+
// Overlong encoding, out of Unicode range, or surrogate half
194+
valid = false;
195+
}
196+
}
197+
198+
if (valid) {
199+
i += seqLen;
200+
} else {
201+
// Replace only the lead byte; continuation bytes will be caught on next iteration
202+
buf[i] = '?';
203+
replaced = true;
204+
i++;
205+
}
206+
}
207+
208+
return replaced;
120209
}

src/meshUtils.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ const std::string vformat(const char *const zcFormat, ...);
5656
// Get actual string length for nanopb char array fields.
5757
size_t pb_string_length(const char *str, size_t max_len);
5858

59+
// Sanitize a fixed-size char buffer in-place by replacing invalid UTF-8 sequences with '?'.
60+
// Ensures the result is null-terminated within bufSize. Returns true if any bytes were replaced.
61+
bool sanitizeUtf8(char *buf, size_t bufSize);
62+
5963
/// Calculate 2^n without calling pow() - used for spreading factor and other calculations
6064
inline uint32_t pow_of_2(uint32_t n)
6165
{

0 commit comments

Comments
 (0)