Merge develop into XEdDSA-merge

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-19 12:11:18 +00:00
committed by GitHub
co-authored by thebentern
198 changed files with 6284 additions and 685 deletions
+4
View File
@@ -47,6 +47,10 @@ data/boot/logo.*
managed_components/*
arduino-lib-builder*
dependencies.lock
# JLink / RTT debug artifacts (nRF SoCs)
flash.jlink
rtt_*.txt
idf_component.yml
CMakeLists.txt
/sdkconfig.*
+4 -4
View File
@@ -4,11 +4,11 @@ cli:
plugins:
sources:
- id: trunk
ref: v1.9.0
ref: v1.10.0
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.528
- checkov@3.2.529
- renovate@43.150.0
- prettier@3.8.3
- trufflehog@3.95.3
@@ -16,7 +16,7 @@ lint:
- bandit@1.9.4
- trivy@0.70.0
- taplo@0.10.0
- ruff@0.15.12
- ruff@0.15.13
- isort@8.0.1
- markdownlint@0.48.0
- oxipng@10.1.1
@@ -26,7 +26,7 @@ lint:
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- black@26.3.1
- black@26.5.1
- git-diff-check
- gitleaks@8.30.1
- clang-format@16.0.3
+1 -1
View File
@@ -38,4 +38,4 @@ cp bin/device-install.* $OUTDIR/
cp bin/device-update.* $OUTDIR/
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
+8 -4
View File
@@ -293,9 +293,12 @@ if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
board_arch = infer_architecture(env.BoardConfig())
should_skip_manifest = board_arch is None
# For host/native envs, avoid depending on 'buildprog' (some targets don't define it)
mtjson_deps = [] if should_skip_manifest else ["buildprog"]
if not should_skip_manifest and platform.name == "espressif32":
# Most platforms can generate the manifest as part of the default 'buildprog' target.
# Typically this passes success/failure properly.
mtjson_deps = ["buildprog"]
if platform.name == "espressif32":
# On ESP32, we need to explicitly depend upon the binary to prevent fake-success upon failure.
mtjson_deps = ["$BUILD_DIR/${PROGNAME}.bin"]
# Build littlefs image as part of mtjson target
# Equivalent to `pio run -t buildfs`
target_lfs = env.DataToBin(
@@ -309,7 +312,8 @@ if should_skip_manifest:
env.AddCustomTarget(
name="mtjson",
dependencies=mtjson_deps,
# For host/native envs, avoid depending on 'buildprog' (some targets don't define it)
dependencies=[],
actions=[skip_manifest],
title="Meshtastic Manifest (skipped)",
description="mtjson generation is skipped for native environments",
+1 -1
View File
@@ -7,7 +7,7 @@
"extra_flags": [
"-D CDEBYTE_EORA_S3",
"-D ARDUINO_USB_CDC_ON_BOOT=1",
"-D ARDUINO_USB_MODE=0",
"-D ARDUINO_USB_MODE=1",
"-D ARDUINO_RUNNING_CORE=1",
"-D ARDUINO_EVENT_RUNNING_CORE=1",
"-D BOARD_HAS_PSRAM"
+1 -1
View File
@@ -6,7 +6,7 @@
"core": "esp32",
"extra_flags": [
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1",
"-DBOARD_HAS_PSRAM"
+1 -1
View File
@@ -8,7 +8,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -9,7 +9,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -9,7 +9,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -9,7 +9,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -8,7 +8,7 @@
"extra_flags": [
"-DHELTEC_WIRELESS_TRACKER",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -7,7 +7,7 @@
"core": "esp32",
"extra_flags": [
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+26
View File
@@ -0,0 +1,26 @@
{
"build": {
"cpu": "cortex-m33",
"f_cpu": "128000000L",
"mcu": "nrf54l15",
"zephyr": {
"variant": "nrf54l15dk/nrf54l15/cpuapp"
}
},
"connectivity": ["bluetooth"],
"debug": {
"default_tools": ["jlink"],
"jlink_device": "nRF54L15_M33",
"svd_path": "nrf54l15.svd"
},
"frameworks": ["zephyr"],
"name": "Nordic nRF54L15-DK (PCA10156)",
"upload": {
"maximum_ram_size": 262144,
"maximum_size": 1572864,
"protocol": "jlink",
"protocols": ["jlink"]
},
"url": "https://www.nordicsemi.com/Products/nRF54L15",
"vendor": "Nordic Semiconductor"
}
+1 -1
View File
@@ -8,7 +8,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0"
],
+1 -1
View File
@@ -9,7 +9,7 @@
"-DBOARD_HAS_PSRAM",
"-DLILYGO_TBEAM_1W",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -8,7 +8,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -8,7 +8,7 @@
"-DBOARD_HAS_PSRAM",
"-DLILYGO_TBEAM_S3_CORE",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+1 -1
View File
@@ -7,7 +7,7 @@
"extra_flags": [
"-DLILYGO_T3S3_V1",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1",
"-DBOARD_HAS_PSRAM"
+1 -1
View File
@@ -10,7 +10,7 @@
"-DBOARD_HAS_PSRAM",
"-DUNPHONE_SPIN=9",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
+7
View File
@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x640000,
app1, app, ota_1, 0x650000,0x640000,
spiffs, data, spiffs, 0xc90000,0x360000,
coredump, data, coredump,0xFF0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x640000
5 app1 app ota_1 0x650000 0x640000
6 spiffs data spiffs 0xc90000 0x360000
7 coredump data coredump 0xFF0000 0x10000
+7
View File
@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x330000,
app1, app, ota_1, 0x340000,0x330000,
spiffs, data, spiffs, 0x670000,0x180000,
coredump, data, coredump,0x7F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x330000
5 app1 app ota_1 0x340000 0x330000
6 spiffs data spiffs 0x670000 0x180000
7 coredump data coredump 0x7F0000 0x10000
+3 -14
View File
@@ -70,17 +70,6 @@ def esp32_create_combined_bin(source, target, env):
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
esp32_kind = env.GetProjectOption("custom_esp32_kind")
if esp32_kind == "esp32":
# Free up some IRAM by removing auxiliary SPI flash chip drivers.
# Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
env.Append(
LINKFLAGS=[
"-Wl,--wrap=esp_flash_chip_gd",
"-Wl,--wrap=esp_flash_chip_issi",
"-Wl,--wrap=esp_flash_chip_winbond",
]
)
else:
# For newer ESP32 targets, using newlib nano works better.
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
# Enable Newlib Nano formatting to save space
# ...but allow printf float support (compromise)
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
# force linker response file instead of command line arguments
Import("env")
def wrap_with_tempfile(command_key):
command = env.get(command_key)
if not command or not isinstance(command, str):
return
if "TEMPFILE(" in command:
return
env.Replace(**{command_key: "${TEMPFILE('%s')}" % command})
# Force SCons to spill long commands into response files on this target.
env.Replace(MAXLINELENGTH=8192)
for key in ("LINKCOM", "CXXLINKCOM", "SHLINKCOM", "SHCXXLINKCOM"):
wrap_with_tempfile(key)
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
#
# post:extra_scripts/nrf54l15_linker.py
#
# Fix for Zephyr two-pass link on nRF54L15:
# platformio-build.py registers env.Depends("$PROG_PATH", final_ld_script) but
# the SCons dependency chain is broken (final_ld_script Command never runs).
# This script adds a PreAction on the final firmware binary that runs the gcc
# preprocessing command directly (extracted from build.ninja) to generate
# zephyr/linker.cmd before the link step.
#
# PlatformIO bundles an old Ninja that can't handle multi-output depslog rules,
# so we parse the COMMAND line from build.ninja and run just the gcc -E part,
# skipping the cmake_transform_depfile step (only needed for Ninja deps tracking).
import os
import re
import subprocess
Import("env")
if env.get("PIOENV") != "nrf54l15dk":
pass # Only for the nrf54l15dk environment
else:
def _extract_gcc_command(ninja_build):
"""Parse build.ninja to find the gcc -E command that generates linker.cmd.
The rule format depends on the host:
Windows (CMake's RunCMake wraps every command):
COMMAND = cmd.exe /C "cd /D DIR && arm-none-eabi-gcc.exe ... -o linker.cmd && cmake.exe -E cmake_transform_depfile ..."
POSIX (Linux/macOS — no wrapper):
COMMAND = cd DIR && arm-none-eabi-gcc ... -o linker.cmd && cmake -E cmake_transform_depfile ...
Returns (gcc_cmd_string, cwd_path) or raises RuntimeError.
"""
in_rule = False
with open(ninja_build, "r", encoding="utf-8", errors="replace") as f:
for line in f:
# Detect start of the linker.cmd custom command rule
if not in_rule:
if "build zephyr/linker.cmd" in line and "CUSTOM_COMMAND" in line:
in_rule = True
continue
stripped = line.strip()
if not stripped.startswith("COMMAND = "):
continue
command_val = stripped[len("COMMAND = ") :]
# On Windows the value is wrapped in `cmd.exe /C "..."` — strip
# the wrapper. On POSIX hosts the inner sequence is the value
# itself (no quoting layer).
m = re.search(r'/C\s+"(.*)"\s*$', command_val)
inner = m.group(1) if m else command_val
parts = inner.split(" && ")
cwd = None
gcc_cmd = None
for part in parts:
part = part.strip()
if part.startswith("cd /D "): # Windows form
cwd = part[len("cd /D ") :]
elif part.startswith("cd "): # POSIX form
cwd = part[len("cd ") :]
elif "arm-none-eabi-gcc" in part:
gcc_cmd = part
if not gcc_cmd:
raise RuntimeError(
"nRF54L15 linker fix: arm-none-eabi-gcc command not found in:\n%s"
% inner[:400]
)
return gcc_cmd, cwd
raise RuntimeError(
"nRF54L15 linker fix: 'build zephyr/linker.cmd' rule not found in build.ninja"
)
def _generate_linker_cmd(target, source, env):
"""Generate zephyr/linker.cmd via direct gcc invocation before the final link."""
build_dir = env.subst("$BUILD_DIR")
zephyr_dir = os.path.join(build_dir, "zephyr")
linker_cmd = os.path.join(zephyr_dir, "linker.cmd")
if os.path.exists(linker_cmd):
return # Already present — nothing to do
ninja_build = os.path.join(build_dir, "build.ninja")
if not os.path.exists(ninja_build):
raise RuntimeError(
"nRF54L15 linker fix: build.ninja not found at %s\n"
"Run a full build first so CMake generates the Ninja files."
% ninja_build
)
gcc_cmd, cwd = _extract_gcc_command(ninja_build)
run_cwd = cwd if cwd else zephyr_dir
print(
"==> nRF54L15: Generating zephyr/linker.cmd (LINKER_ZEPHYR_FINAL) via GCC"
)
# gcc_cmd comes verbatim from our own build.ninja (never user input) and
# contains Windows-style paths with spaces that cannot be safely argv-split
# with shlex, so we run it via the platform shell. nosec/nosemgrep below
# acknowledge this deliberate, scoped use of shell=True.
result = subprocess.run( # nosec B602
gcc_cmd,
shell=True, # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
cwd=run_cwd,
capture_output=True,
text=True,
)
if result.returncode != 0:
print("GCC stdout:", result.stdout[:2000])
print("GCC stderr:", result.stderr[:2000])
raise RuntimeError(
"nRF54L15 linker fix: GCC failed to generate linker.cmd (rc=%d)"
% result.returncode
)
if not os.path.exists(linker_cmd):
raise RuntimeError(
"nRF54L15 linker fix: GCC returned 0 but linker.cmd was not created at %s"
% linker_cmd
)
print("==> linker.cmd generated successfully")
# Use PIOMAINPROG (set by ZephyrBuildProgram) to get the exact SCons node
prog = env.get("PIOMAINPROG")
if prog:
env.AddPreAction(prog, _generate_linker_cmd)
else:
print(
"[nrf54l15_linker] WARNING: PIOMAINPROG not set, falling back to $PROG_PATH"
)
env.AddPreAction(env.subst("$PROG_PATH"), _generate_linker_cmd)
+2 -1
View File
@@ -2,7 +2,7 @@
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = tbeam
default_envs = heltec-v3
extra_configs =
variants/*/*.ini
@@ -17,6 +17,7 @@ test_build_src = true
extra_scripts =
pre:bin/platformio-pre.py
bin/platformio-custom.py
post:extra_scripts/nrf54l15_linker.py
; note: we add src to our include search path so that lmic_project_config can override
; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile
; of code is a heap corruption bug!
+2 -3
View File
@@ -158,9 +158,8 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
#include <RAK13800_W5100S.h>
#endif // HAS_ETHERNET
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
#if HAS_WIFI
+1 -1
View File
@@ -277,7 +277,7 @@ void fsInit()
*/
void setupSDCard()
{
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI)
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) && !defined(HAS_SD_MMC)
concurrency::LockGuard g(spiLock);
SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) {
+8
View File
@@ -48,6 +48,14 @@ using namespace STM32_LittleFS_Namespace;
using namespace Adafruit_LittleFS_Namespace;
#endif
#if defined(ARCH_NRF54L15)
// nRF54L15 — Zephyr LittleFS on 36 KB storage_partition (internal RRAM)
#include "InternalFileSystem.h"
#define FSCom InternalFS
#define FSBegin() FSCom.begin()
using namespace Adafruit_LittleFS_Namespace;
#endif
void fsInit();
void fsListFiles();
bool copyFile(const char *from, const char *to);
+126 -83
View File
@@ -24,6 +24,13 @@
#include "meshUtils.h"
#include "power/PowerHAL.h"
#include "sleep.h"
#ifdef ARCH_ESP32
// #include <driver/adc.h>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_cali_scheme.h>
#include <esp_adc/adc_oneshot.h>
#include <esp_err.h>
#endif
#if defined(ARCH_PORTDUINO)
#include "api/WiFiServerAPI.h"
@@ -63,9 +70,8 @@
#include <WiFi.h>
#endif
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
#endif
@@ -77,21 +83,86 @@
#if defined(BATTERY_PIN) && defined(ARCH_ESP32)
#ifndef BAT_MEASURE_ADC_UNIT // ADC1 is default
static const adc1_channel_t adc_channel = ADC_CHANNEL;
static const adc_channel_t adc_channel = ADC_CHANNEL;
static const adc_unit_t unit = ADC_UNIT_1;
#else // ADC2
static const adc2_channel_t adc_channel = ADC_CHANNEL;
#else // ADC2
static const adc_channel_t adc_channel = ADC_CHANNEL;
static const adc_unit_t unit = ADC_UNIT_2;
RTC_NOINIT_ATTR uint64_t RTC_reg_b;
#endif // BAT_MEASURE_ADC_UNIT
esp_adc_cal_characteristics_t *adc_characs = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t));
static adc_oneshot_unit_handle_t adc_handle = nullptr;
static adc_cali_handle_t adc_cali_handle = nullptr;
static bool adc_calibrated = false;
#ifndef ADC_ATTENUATION
static const adc_atten_t atten = ADC_ATTEN_DB_12;
#else
static const adc_atten_t atten = ADC_ATTENUATION;
#endif
#ifdef ADC_BITWIDTH
static const adc_bitwidth_t adc_width = ADC_BITWIDTH;
#else
static const adc_bitwidth_t adc_width = ADC_BITWIDTH_DEFAULT;
#endif
static int adcBitWidthToBits(adc_bitwidth_t width)
{
switch (width) {
case ADC_BITWIDTH_9:
return 9;
case ADC_BITWIDTH_10:
return 10;
case ADC_BITWIDTH_11:
return 11;
case ADC_BITWIDTH_12:
return 12;
#ifdef ADC_BITWIDTH_13
case ADC_BITWIDTH_13:
return 13;
#endif
default:
return 12;
}
}
static bool initAdcCalibration()
{
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = unit,
.atten = atten,
.bitwidth = adc_width,
};
esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle);
if (ret == ESP_OK) {
LOG_INFO("ADC calibration: curve fitting enabled");
return true;
}
if (ret != ESP_ERR_NOT_SUPPORTED) {
LOG_WARN("ADC calibration: curve fitting failed: %s", esp_err_to_name(ret));
}
#endif
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
adc_cali_line_fitting_config_t cali_config = {
.unit_id = unit,
.atten = atten,
.bitwidth = adc_width,
.default_vref = DEFAULT_VREF,
};
esp_err_t ret = adc_cali_create_scheme_line_fitting(&cali_config, &adc_cali_handle);
if (ret == ESP_OK) {
LOG_INFO("ADC calibration: line fitting enabled");
return true;
}
if (ret != ESP_ERR_NOT_SUPPORTED) {
LOG_WARN("ADC calibration: line fitting failed: %s", esp_err_to_name(ret));
}
#endif
LOG_INFO("ADC calibration not supported; using approximate scaling");
return false;
}
#endif // BATTERY_PIN && ARCH_ESP32
#ifdef EXT_PWR_DETECT
@@ -367,8 +438,20 @@ class AnalogBatteryLevel : public HasBatteryLevel
scaled *= operativeAdcMultiplier;
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
raw = espAdcRead();
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
scaled *= operativeAdcMultiplier;
int voltage_mv = 0;
if (adc_calibrated && adc_cali_handle) {
if (adc_cali_raw_to_voltage(adc_cali_handle, raw, &voltage_mv) != ESP_OK) {
LOG_WARN("ADC calibration read failed; using raw value");
voltage_mv = 0;
}
}
if (voltage_mv == 0) {
// Fallback approximate conversion without calibration
const int bits = adcBitWidthToBits(adc_width);
const float max_code = powf(2.0f, bits) - 1.0f;
voltage_mv = (int)((raw / max_code) * DEFAULT_VREF);
}
scaled = voltage_mv * operativeAdcMultiplier;
#else // block for all other platforms
#ifdef ARCH_NRF52
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
@@ -410,51 +493,22 @@ class AnalogBatteryLevel : public HasBatteryLevel
uint32_t raw = 0;
uint8_t raw_c = 0; // raw reading counter
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
if (!adc_handle) {
LOG_ERROR("ADC oneshot handle not initialized");
return 0;
}
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
int val_ = adc1_get_raw(adc_channel);
if (val_ >= 0) { // save only valid readings
raw += val_;
int val = 0;
esp_err_t err = adc_oneshot_read(adc_handle, adc_channel, &val);
if (err == ESP_OK) {
raw += val;
raw_c++;
}
// delayMicroseconds(100);
}
#else // ADC2
#ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3
// ADC2 wifi bug workaround not required, breaks compile
// On ESP32S3, ADC2 can take turns with Wifi (?)
int32_t adc_buf;
esp_err_t read_result;
// Multiple samples
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
adc_buf = 0;
read_result = -1;
read_result = adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf);
if (read_result == ESP_OK) {
raw += adc_buf;
raw_c++; // Count valid samples
} else {
LOG_DEBUG("An attempt to sample ADC2 failed");
LOG_DEBUG("ADC read failed: %s", esp_err_to_name(err));
}
}
#else // Other ESP32
int32_t adc_buf = 0;
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
// ADC2 wifi bug workaround, see
// https://github.com/espressif/arduino-esp32/issues/102
WRITE_PERI_REG(SENS_SAR_READ_CTRL2_REG, RTC_reg_b);
SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV);
adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf);
raw += adc_buf;
raw_c++;
}
#endif // BAT_MEASURE_ADC_UNIT
#endif // End BAT_MEASURE_ADC_UNIT
return (raw / (raw_c < 1 ? 1 : raw_c));
}
#endif
@@ -666,42 +720,31 @@ bool Power::analogInit()
#ifdef ARCH_STM32WL
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = unit,
};
#ifndef ADC_WIDTH // max resolution by default
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
#else
static const adc_bits_width_t width = ADC_WIDTH;
#endif
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
adc1_config_width(width);
adc1_config_channel_atten(adc_channel, atten);
#else // ADC2
adc2_config_channel_atten(adc_channel, atten);
#ifndef CONFIG_IDF_TARGET_ESP32S3
// ADC2 wifi bug workaround
// Not required with ESP32S3, breaks compile
RTC_reg_b = READ_PERI_REG(SENS_SAR_READ_CTRL2_REG);
#endif
#endif
// calibrate ADC
esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_characs);
// show ADC characterization base
if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) {
LOG_INFO("ADC config based on Two Point values stored in eFuse");
} else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
LOG_INFO("ADC config based on reference voltage stored in eFuse");
if (!adc_handle) {
esp_err_t err = adc_oneshot_new_unit(&init_config, &adc_handle);
if (err != ESP_OK) {
LOG_ERROR("ADC oneshot init failed: %s", esp_err_to_name(err));
return false;
}
}
#ifdef CONFIG_IDF_TARGET_ESP32S3
// ESP32S3
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
LOG_INFO("ADC config based on Two Point values and fitting curve "
"coefficients stored in eFuse");
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = atten,
.bitwidth = adc_width,
};
esp_err_t err = adc_oneshot_config_channel(adc_handle, adc_channel, &chan_cfg);
if (err != ESP_OK) {
LOG_ERROR("ADC channel config failed: %s", esp_err_to_name(err));
return false;
}
#endif
else {
LOG_INFO("ADC config based on default reference voltage");
}
#endif // ARCH_ESP32
adc_calibrated = initAdcCalibration();
#endif // ARCH_ESP32
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
+4
View File
@@ -225,6 +225,8 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected();
#elif defined(ARCH_NRF52)
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
#elif defined(ARCH_NRF54L15)
isBleConnected = nrf54l15Bluetooth != nullptr && nrf54l15Bluetooth->isConnected();
#endif
if (isBleConnected) {
auto thread = concurrency::OSThread::currentThread;
@@ -241,6 +243,8 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
nimbleBluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF52)
nrf52Bluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF54L15)
nrf54l15Bluetooth->sendLog(buffer.get(), size);
#endif
}
}
+4
View File
@@ -561,5 +561,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_SCREEN 0
#endif
#ifndef USE_ETHERNET_DEFAULT
#define USE_ETHERNET_DEFAULT 0
#endif
#include "DebugConfiguration.h"
#include "RF95Configuration.h"
+8 -3
View File
@@ -425,6 +425,11 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
#elif defined(USE_SSD1306)
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#if defined(OLED_Y_OFFSET_PAGES)
// Panels whose active window does not start at GDDRAM row 0 (e.g. 72x40
// modules on pages 3..7) need a fixed vertical page shift on every write.
static_cast<SSD1306Wire *>(dispdev)->setYOffset(OLED_Y_OFFSET_PAGES);
#endif
#elif defined(USE_SPISSD1306)
dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48);
if (!dispdev->init()) {
@@ -910,7 +915,7 @@ int32_t Screen::runOnce()
#ifndef DISABLE_WELCOME_UNSET
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
menuHandler::LoraRegionPicker();
#else
menuHandler::OnboardMessage();
@@ -1147,7 +1152,7 @@ void Screen::setFrames(FrameFocus focus)
#if defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) {
fsi.positions.clock = numframes;
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
#else
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
@@ -1606,7 +1611,7 @@ void Screen::showFrame(FrameDirection direction)
void Screen::setFastFramerate()
{
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
dispdev->clear();
#if GRAPHICS_TFT_COLORING_ENABLED
prepareFrameColorRegions();
+1 -1
View File
@@ -106,7 +106,7 @@
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
#define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#elif defined(M5STACK_UNITC6L)
#elif defined(OLED_TINY)
#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13
#define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13
#define FONT_LARGE FONT_SMALL_LOCAL // Height: 13
+7 -1
View File
@@ -31,6 +31,12 @@ ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenw
return ScreenResolution::UltraLow;
}
#ifdef DISPLAY_FORCE_SMALL_FONTS
if (screenwidth <= 160 && screenheight <= 80) {
return ScreenResolution::Low;
}
#endif
// Standard OLED screens
if (screenwidth > 128 && screenheight <= 64) {
return ScreenResolution::Low;
@@ -240,7 +246,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int batteryX = 1;
int batteryY = HEADER_OFFSET_Y + 1;
#if !defined(M5STACK_UNITC6L)
#if !defined(OLED_TINY)
// === Battery Icons ===
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
batteryX += 1;
+2 -2
View File
@@ -460,7 +460,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
#if !defined(M5STACK_UNITC6L)
#if !defined(OLED_TINY)
// === Fifth Row: Channel Utilization ===
const char *chUtil = "ChUtil:";
char chUtilPercentage[10];
@@ -592,7 +592,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
// Label
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->drawString(labelX, getTextPositions(display)[line], label);
#if !defined(M5STACK_UNITC6L)
#if !defined(OLED_TINY)
// Bar
int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2;
display->setColor(WHITE);
+1 -1
View File
@@ -555,7 +555,7 @@ void menuHandler::TZPicker()
void menuHandler::clockMenu()
{
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
static const char *optionsArray[] = {"Back", "Time Format", "Timezone"};
#else
static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
+4 -4
View File
@@ -26,7 +26,7 @@ extern bool haveGlyphs(const char *str);
// Global screen instance
extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
static uint32_t lastSwitchTime = 0;
#endif
namespace graphics
@@ -788,7 +788,7 @@ void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state
unsigned long now = millis();
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
display->clear();
if (now - lastSwitchTime >= 3000) {
display->display();
@@ -824,7 +824,7 @@ void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *st
unsigned long now = millis();
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
display->clear();
if (now - lastSwitchTime >= 3000) {
display->display();
@@ -894,7 +894,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
double lat = DegD(ourSelfPos.latitude_i);
double lon = DegD(ourSelfPos.longitude_i);
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
display->clear();
uint32_t now = millis();
if (now - lastSwitchTime >= 2000) {
+1 -1
View File
@@ -728,7 +728,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
}
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1;
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
if (visibleTotalLines == 1) {
boxTop += 25;
}
+6 -6
View File
@@ -25,7 +25,7 @@
// External variables
extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
static uint32_t lastSwitchTime = 0;
#endif
namespace graphics
@@ -753,7 +753,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
if (!node || node->num == nodeDB->getNodeNum() || !nodeInfoLiteIsFavorite(node))
return;
display->clear();
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
uint32_t now = millis();
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
{
@@ -959,7 +959,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
if (seenStr[0]) {
display->drawString(x, getTextPositions(display)[line++], seenStr);
}
#if !defined(M5STACK_UNITC6L)
#if !defined(OLED_TINY)
// === 4. Uptime (only show if metric is present) ===
char uptimeStr[32] = "";
meshtastic_DeviceMetrics nodeMetrics;
@@ -1172,7 +1172,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
}
#endif
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
line += 1;
// === Node Identity ===
@@ -1456,7 +1456,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
// needs to be drawn relative to x and y
// draw centered icon left to right and centered above the one line of app text
#if defined(M5STACK_UNITC6L)
#if defined(OLED_TINY)
display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits);
if (gBootSplashBoldPass) {
display->drawXbm(x + (SCREEN_WIDTH - 50) / 2 + 1, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits);
@@ -1666,7 +1666,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
}
display->drawString(x, textPos[line++], altitudeLine);
}
#if !defined(M5STACK_UNITC6L)
#if !defined(OLED_TINY)
// === Draw Compass ===
if (validHeading || statusLine1) {
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
+1 -1
View File
@@ -318,7 +318,7 @@ const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
#define connection_icon_height 5
const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36};
#ifdef M5STACK_UNITC6L
#ifdef OLED_TINY
#include "img/icon_small.xbm"
#else
#include "img/icon.xbm"
+5 -4
View File
@@ -53,10 +53,11 @@ void InkHUD::MessageStore::saveToFlash()
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i), min(MAX_MESSAGE_SIZE, m.text.size()),
m.text.c_str());
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()),
min((size_t)MAX_MESSAGE_SIZE, m.text.size())); // Write message text
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i),
min((size_t)MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
}
// Release firmware's SPI lock, because SafeFile::close needs it
+2
View File
@@ -13,6 +13,8 @@
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
// REVISIT esp_adc_cal.h
// "legacy adc calibration driver is deprecated, please migrate to use esp_adc/adc_cali.h and esp_adc/adc_cali_scheme.h"
#include <esp_adc_cal.h>
#include <soc/adc_channel.h>
+2 -2
View File
@@ -68,7 +68,7 @@ TLoraPagerKeyboard::TLoraPagerKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
#else
ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
@@ -108,7 +108,7 @@ void TLoraPagerKeyboard::setBacklight(bool on)
uint32_t _brightness = 0;
if (on)
_brightness = brightness;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
ledcWrite(KB_BL_PIN, _brightness);
#else
ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness);
+20
View File
@@ -2,6 +2,9 @@
#if !MESHTASTIC_EXCLUDE_GPS
#include "GPS.h"
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "input/InputBroker.h"
#endif
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
@@ -59,6 +62,12 @@ NimbleBluetooth *nimbleBluetooth = nullptr;
NRF52Bluetooth *nrf52Bluetooth = nullptr;
#endif
#ifdef ARCH_NRF54L15
void nrf54l15Setup();
void nrf54l15Loop();
NRF54L15Bluetooth *nrf54l15Bluetooth = nullptr;
#endif
#if HAS_WIFI || defined(USE_WS5500) || defined(USE_CH390D)
#include "mesh/api/WiFiServerAPI.h"
#include "mesh/wifi/WiFiAPClient.h"
@@ -696,6 +705,9 @@ void setup()
#ifdef ARCH_NRF52
nrf52Setup();
#endif
#ifdef ARCH_NRF54L15
nrf54l15Setup();
#endif
#ifdef ARCH_RP2040
rp2040Setup();
@@ -741,6 +753,11 @@ void setup()
}
}
#endif
#ifdef OLED_GEOMETRY_OVERRIDE
// Per-variant geometry (e.g. 72x40 micro-OLEDs). Takes precedence over the
// default GEOMETRY_128_64 set at the top of setup().
screen_geometry = OLED_GEOMETRY_OVERRIDE;
#endif
#endif
#if !MESHTASTIC_EXCLUDE_I2C
@@ -1126,6 +1143,9 @@ void loop()
#endif
#ifdef ARCH_NRF52
nrf52Loop();
#endif
#ifdef ARCH_NRF54L15
nrf54l15Loop();
#endif
power->powerCommandsCheck();
+4
View File
@@ -19,6 +19,10 @@ extern NimbleBluetooth *nimbleBluetooth;
#include "NRF52Bluetooth.h"
extern NRF52Bluetooth *nrf52Bluetooth;
#endif
#ifdef ARCH_NRF54L15
#include "NRF54L15Bluetooth.h"
extern NRF54L15Bluetooth *nrf54l15Bluetooth;
#endif
#if !MESHTASTIC_EXCLUDE_I2C
#include "detect/ScanI2CTwoWire.h"
#endif
+17
View File
@@ -93,6 +93,23 @@ void Channels::initDefaultLoraConfig()
#ifdef USERPREFS_LORACONFIG_CHANNEL_NUM
loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM;
#endif
// Apply any hardcoded USERPREFS overrides for custom modem config (e.g. region-locked boards)
#ifdef USERPREFS_LORACONFIG_USE_PRESET
loraConfig.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
#endif
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
loraConfig.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
#endif
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
loraConfig.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
#endif
#ifdef USERPREFS_LORACONFIG_CODING_RATE
loraConfig.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
#endif
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
loraConfig.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
#endif
}
bool Channels::ensureLicensedOperation()
+7 -3
View File
@@ -334,7 +334,9 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p)
if (toPhoneQueue.enqueue(p, 0) == false) {
LOG_CRIT("Failed to queue a packet into toPhoneQueue!");
abort();
releaseToPool(p);
fromNum++; // notify observers so phone can resync
return;
}
fromNum++;
}
@@ -351,7 +353,8 @@ void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage
if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) {
LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!");
abort();
releaseMqttClientProxyMessageToPool(m);
return;
}
fromNum++;
}
@@ -383,7 +386,8 @@ void MeshService::sendClientNotification(meshtastic_ClientNotification *n)
if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) {
LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!");
abort();
releaseClientNotificationToPool(n);
return;
}
fromNum++;
}
+60 -2
View File
@@ -393,6 +393,13 @@ NodeDB::NodeDB()
} else {
LOG_WARN("Failed to read unique id from efuse");
}
#elif defined(ARCH_NRF54L15)
// nRF54L15: DEVICEID is under FICR->INFO sub-struct (not top-level as on nRF52)
uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0];
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
myNodeInfo.device_id.size = 16;
#elif defined(ARCH_NRF52)
// Nordic applies a FIPS compliant Random ID to each chip at the factory
// We concatenate the device address to the Random ID to create a unique ID for now
@@ -811,6 +818,23 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET;
#else
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
#endif
#ifdef USERPREFS_LORACONFIG_USE_PRESET
config.lora.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
#else
config.lora.use_preset = true;
#endif
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
config.lora.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
#endif
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
config.lora.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
#endif
#ifdef USERPREFS_LORACONFIG_CODING_RATE
config.lora.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
#endif
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
#endif
config.lora.hop_limit = HOP_RELIABLE;
#ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT
@@ -946,6 +970,10 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED;
#endif
#if USE_ETHERNET_DEFAULT
config.network.eth_enabled = true;
#endif
#ifdef USERPREFS_NETWORK_WIFI_SSID
strncpy(config.network.wifi_ssid, USERPREFS_NETWORK_WIFI_SSID, sizeof(config.network.wifi_ssid));
#endif
@@ -1028,7 +1056,8 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.has_store_forward = true;
moduleConfig.has_telemetry = true;
moduleConfig.has_external_notification = true;
#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) || defined(PCA_LED_NOTIFICATION)
#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) || defined(PCA_LED_NOTIFICATION) || \
defined(NEOPIXEL_STATUS_NOTIFICATION_PIN)
moduleConfig.external_notification.enabled = true;
#endif
#if defined(PIN_BUZZER)
@@ -1049,7 +1078,7 @@ void NodeDB::installDefaultModuleConfig()
#endif
#if defined(PIN_VIBRATION)
moduleConfig.external_notification.nag_timeout = 2;
#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION)
#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) || defined(NEOPIXEL_STATUS_NOTIFICATION_PIN)
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
#endif
@@ -1202,7 +1231,14 @@ void NodeDB::initModuleConfigIntervals()
#else
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
#endif
#ifdef USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
moduleConfig.telemetry.environment_update_interval = USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL;
#else
moduleConfig.telemetry.environment_update_interval = 0;
#endif
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
#endif
moduleConfig.telemetry.air_quality_interval = 0;
moduleConfig.telemetry.power_update_interval = 0;
moduleConfig.telemetry.health_update_interval = 0;
@@ -1711,6 +1747,28 @@ void NodeDB::loadFromDisk()
config.lora.tx_enabled = false;
#endif
// Always-apply LoRa overrides: applied after loading saved config so they
// take effect even when NVS already has a valid config (e.g. region-locked
// dev boards with no BLE/serial to set the region at runtime).
#ifdef USERPREFS_CONFIG_LORA_REGION
config.lora.region = USERPREFS_CONFIG_LORA_REGION;
#endif
#ifdef USERPREFS_LORACONFIG_USE_PRESET
config.lora.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
#endif
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
config.lora.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
#endif
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
config.lora.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
#endif
#ifdef USERPREFS_LORACONFIG_CODING_RATE
config.lora.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
#endif
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
#endif
if (backupSecurity.private_key.size > 0) {
LOG_DEBUG("Restoring backup of security config");
config.security = backupSecurity;
+8
View File
@@ -71,7 +71,15 @@ void PhoneAPI::handleStartConfig()
}
pauseBluetoothLogging = true;
spiLock->lock();
#if defined(MESHTASTIC_EXCLUDE_FILES_MANIFEST)
// Skip the recursive FS walk. Used by platforms whose Zephyr LittleFS
// backend can't safely traverse a deep tree (e.g. nRF54L15) and platforms
// that don't support OTA browsing — the manifest is only consumed by
// companion apps for those flows.
filesManifest.clear();
#else
filesManifest = getFiles("/", 10);
#endif
spiLock->unlock();
LOG_DEBUG("Got %d files in manifest", filesManifest.size());
+17 -10
View File
@@ -46,16 +46,6 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
#endif
}
RadioLibInterface::~RadioLibInterface()
{
// If the static `instance` pointer still references us, clear it.
// A later successful init() may have replaced `instance` with a newer
// interface — don't clobber that case.
if (instance == this) {
instance = nullptr;
}
}
#ifdef ARCH_ESP32
// ESP32 doesn't use that flag
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
@@ -434,6 +424,9 @@ void RadioLibInterface::completeSending()
// that can take a long time
auto p = sendingPacket;
sendingPacket = NULL;
#ifdef LED_LORA
digitalWrite(LED_LORA, LED_STATE_OFF);
#endif
if (p) {
// Packet has been sent, count it toward our TX airtime utilization.
@@ -555,6 +548,9 @@ void RadioLibInterface::pollMissedIrqs()
if (isReceiving) {
checkRxDoneIrqFlag();
}
if (sendingPacket) {
checkTxDoneIrqFlag();
}
}
void RadioLibInterface::resetAGC()
@@ -570,6 +566,14 @@ void RadioLibInterface::checkRxDoneIrqFlag()
}
}
void RadioLibInterface::checkTxDoneIrqFlag()
{
if (iface->checkIrq(RADIOLIB_IRQ_TX_DONE)) {
LOG_WARN("caught missed TX_DONE");
notify(ISR_TX, true);
}
}
void RadioLibInterface::configHardwareForSend()
{
powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn);
@@ -611,6 +615,9 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
enableInterrupt(isrTxLevel0);
lastTxStart = millis();
printPacket("Started Tx", txp);
#ifdef LED_LORA
digitalWrite(LED_LORA, LED_STATE_ON);
#endif
}
return res == RADIOLIB_ERR_NONE;
+8 -7
View File
@@ -104,6 +104,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
*/
static RadioLibInterface *instance;
/** Clear instance on destruction so stale pointer checks in loop() are safe */
virtual ~RadioLibInterface()
{
if (instance == this)
instance = nullptr;
}
/**
* Glue functions called from ISR land
*/
@@ -136,13 +143,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
/**
* Clear the static `instance` pointer if it still points at us, so callers
* that check `RadioLibInterface::instance != nullptr` don't dereference a
* freed object after a failed init() + unique_ptr reset.
*/
virtual ~RadioLibInterface();
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
/**
@@ -293,4 +293,5 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override;
void checkRxDoneIrqFlag();
void checkTxDoneIrqFlag();
};
+2 -3
View File
@@ -3,9 +3,8 @@
#include "ServerAPI.h"
#include <WiFi.h>
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
/**
+5 -3
View File
@@ -198,7 +198,9 @@ typedef enum _meshtastic_Config_DisplayConfig_OledType {
/* Can not be auto detected but set by proto. Used for 128x64 screens */
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3,
/* Can not be auto detected but set by proto. Used for 128x128 screens */
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 = 4
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 = 4,
/* Can not be auto detected but set by proto. Used for 64x128 rotated screens */
meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_ROTATED = 5
} meshtastic_Config_DisplayConfig_OledType;
typedef enum _meshtastic_Config_DisplayConfig_DisplayMode {
@@ -720,8 +722,8 @@ extern "C" {
#define _meshtastic_Config_DisplayConfig_DisplayUnits_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayUnits)(meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL+1))
#define _meshtastic_Config_DisplayConfig_OledType_MIN meshtastic_Config_DisplayConfig_OledType_OLED_AUTO
#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128
#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128+1))
#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_ROTATED
#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_ROTATED+1))
#define _meshtastic_Config_DisplayConfig_DisplayMode_MIN meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT
#define _meshtastic_Config_DisplayConfig_DisplayMode_MAX meshtastic_Config_DisplayConfig_DisplayMode_COLOR
+4
View File
@@ -321,6 +321,10 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_HELTEC_MESH_NODE_T1 = 133,
/* B&Q Consulting Station G3: TBD */
meshtastic_HardwareModel_STATION_G3 = 134,
/* Lilygo T-Impulse-Plus */
meshtastic_HardwareModel_T_IMPULSE_PLUS = 135,
/* Lilygo T-Echo Card */
meshtastic_HardwareModel_T_ECHO_CARD = 136,
/* ------------------------------------------------------------------------------------------------------------------------------------------
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.
------------------------------------------------------------------------------------------------------------------------------------------ */
@@ -144,7 +144,7 @@ typedef struct _meshtastic_ModuleConfig_MQTTConfig {
(the default official mqtt.meshtastic.org server can handle encrypted packets)
Decrypted packets may be useful for external systems that want to consume meshtastic packets */
bool encryption_enabled;
/* Whether to send / consume json packets on MQTT */
/* Deprecated: JSON packet support on MQTT was removed, and this field is ignored. */
bool json_enabled;
/* If true, we attempt to establish a secure connection using TLS */
bool tls_enabled;
+2 -3
View File
@@ -13,9 +13,8 @@
#include <WebServer.h>
#include <WiFi.h>
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
#ifdef ARCH_ESP32
+2 -3
View File
@@ -12,9 +12,8 @@
#include <AsyncUDP.h>
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
#define UDP_MULTICAST_DEFAUL_PORT 4403 // Default port for UDP multicast is same as TCP api server
+2 -3
View File
@@ -10,9 +10,8 @@
#include "target_specific.h"
#include <WiFi.h>
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
#if HAS_ETHERNET && defined(USE_CH390D)
+2 -3
View File
@@ -9,9 +9,8 @@
#include <WiFi.h>
#endif
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
extern bool needReconnect;
+10 -2
View File
@@ -262,7 +262,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
break;
}
case meshtastic_AdminMessage_ota_request_tag: {
#if defined(ARCH_ESP32)
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
LOG_INFO("OTA Requested");
if (r->ota_request.ota_hash.size != 32) {
@@ -666,7 +666,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
bool requiresReboot = true;
switch (c.which_payload_variant) {
case meshtastic_Config_device_tag:
case meshtastic_Config_device_tag: {
LOG_INFO("Set config: Device");
config.has_device = true;
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
@@ -720,6 +720,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
}
#endif
break;
} // case meshtastic_Config_device_tag
case meshtastic_Config_position_tag:
LOG_INFO("Set config: Position");
config.has_position = true;
@@ -1337,6 +1338,10 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
if (config.bluetooth.enabled && nrf52Bluetooth) {
conn.bluetooth.is_connected = nrf52Bluetooth->isConnected();
}
#elif defined(ARCH_NRF54L15)
if (config.bluetooth.enabled && nrf54l15Bluetooth) {
conn.bluetooth.is_connected = nrf54l15Bluetooth->isConnected();
}
#endif
#endif
conn.has_serial = true; // No serial-less devices
@@ -1597,6 +1602,9 @@ void disableBluetooth()
#elif defined(ARCH_NRF52)
if (nrf52Bluetooth)
nrf52Bluetooth->shutdown();
#elif defined(ARCH_NRF54L15)
if (nrf54l15Bluetooth)
nrf54l15Bluetooth->shutdown();
#endif
#endif
}
@@ -206,6 +206,10 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
#ifdef PCA_LED_NOTIFICATION
io.digitalWrite(PCA_LED_NOTIFICATION, on);
#endif
#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN
notificationPixel.setPixelColor(0, on ? NEOPIXEL_STATUS_NOTIFICATION_COLOR : 0);
notificationPixel.show();
#endif
break;
}
@@ -324,6 +328,12 @@ ExternalNotificationModule::ExternalNotificationModule()
LOG_INFO("Use Pin %i in digital mode", output);
pinMode(output, OUTPUT);
}
#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN
LOG_INFO("Use WS2812 on GPIO %d as notification LED", NEOPIXEL_STATUS_NOTIFICATION_PIN);
notificationPixel.begin();
notificationPixel.clear();
notificationPixel.show();
#endif
setExternalState(0, false);
externalTurnedOn[0] = 0;
if (moduleConfig.external_notification.output_vibra) {
+17
View File
@@ -10,6 +10,19 @@
extern AmbientLightingThread *ambientLightingThread;
#endif
// Drive a single WS2812 as the notification LED (M1/M2-style LED_NOTIFICATION
// but addressable). A variant defines NEOPIXEL_STATUS_NOTIFICATION_PIN to
// enable. Colour defaults to green but can be overridden.
#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN
#include <Adafruit_NeoPixel.h>
#ifndef NEOPIXEL_STATUS_TYPE
#define NEOPIXEL_STATUS_TYPE (NEO_GRB + NEO_KHZ800)
#endif
#ifndef NEOPIXEL_STATUS_NOTIFICATION_COLOR
#define NEOPIXEL_STATUS_NOTIFICATION_COLOR 0x00FF00 // green
#endif
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
#include <NonBlockingRtttl.h>
#else
@@ -38,6 +51,10 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency:
CallbackObserver<ExternalNotificationModule, const InputEvent *>(this, &ExternalNotificationModule::handleInputEvent);
uint32_t output = 0;
#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN
Adafruit_NeoPixel notificationPixel = Adafruit_NeoPixel(1, NEOPIXEL_STATUS_NOTIFICATION_PIN, NEOPIXEL_STATUS_TYPE);
#endif
public:
ExternalNotificationModule();
+33
View File
@@ -17,8 +17,29 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule")
if (inputBroker)
inputObserver.observe(inputBroker);
#endif
#ifdef NEOPIXEL_STATUS_POWER_PIN
powerPixel.begin();
powerPixel.clear();
powerPixel.show();
#endif
#ifdef NEOPIXEL_STATUS_PAIRING_PIN
pairingPixel.begin();
pairingPixel.clear();
pairingPixel.show();
#endif
}
// Helper: write a 1-pixel NeoPixel strand to `color` when stateOn, else clear.
// Kept as a static inline here (rather than a member) so it compiles out
// completely when no NeoPixel status pins are defined.
#if defined(NEOPIXEL_STATUS_POWER_PIN) || defined(NEOPIXEL_STATUS_PAIRING_PIN)
static inline void writeStatusPixel(Adafruit_NeoPixel &pixel, uint32_t color, bool stateOn)
{
pixel.setPixelColor(0, stateOn ? color : 0);
pixel.show();
}
#endif
int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
{
switch (arg->getStatusType()) {
@@ -176,6 +197,12 @@ int32_t StatusLEDModule::runOnce()
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, PAIRING_LED_state);
#endif
#ifdef NEOPIXEL_STATUS_POWER_PIN
writeStatusPixel(powerPixel, NEOPIXEL_STATUS_POWER_COLOR, CHARGE_LED_state == LED_STATE_ON);
#endif
#ifdef NEOPIXEL_STATUS_PAIRING_PIN
writeStatusPixel(pairingPixel, NEOPIXEL_STATUS_PAIRING_COLOR, PAIRING_LED_state == LED_STATE_ON);
#endif
#ifdef RGB_LED_POWER
if (!config.device.led_heartbeat_disabled) {
@@ -225,6 +252,12 @@ void StatusLEDModule::setPowerLED(bool LEDon)
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, ledState);
#endif
#ifdef NEOPIXEL_STATUS_POWER_PIN
writeStatusPixel(powerPixel, NEOPIXEL_STATUS_POWER_COLOR, LEDon);
#endif
#ifdef NEOPIXEL_STATUS_PAIRING_PIN
writeStatusPixel(pairingPixel, NEOPIXEL_STATUS_PAIRING_COLOR, LEDon);
#endif
#ifdef Battery_LED_1
digitalWrite(Battery_LED_1, ledState);
+26
View File
@@ -13,6 +13,25 @@
#include "input/InputBroker.h"
#endif
// WS2812/NeoPixel status-LED support. A variant may define
// NEOPIXEL_STATUS_POWER_PIN (required to enable the power/charge pixel)
// NEOPIXEL_STATUS_POWER_COLOR (optional, default red 0xFF0000)
// NEOPIXEL_STATUS_PAIRING_PIN / _COLOR (default blue 0x0000FF)
// Each pixel is a standalone 1-LED strand on its own GPIO — this mirrors how
// boards like the LilyGo T-Echo-Card expose three independent WS2812s.
#if defined(NEOPIXEL_STATUS_POWER_PIN) || defined(NEOPIXEL_STATUS_PAIRING_PIN)
#include <Adafruit_NeoPixel.h>
#ifndef NEOPIXEL_STATUS_TYPE
#define NEOPIXEL_STATUS_TYPE (NEO_GRB + NEO_KHZ800)
#endif
#ifndef NEOPIXEL_STATUS_POWER_COLOR
#define NEOPIXEL_STATUS_POWER_COLOR 0xFF0000 // red
#endif
#ifndef NEOPIXEL_STATUS_PAIRING_COLOR
#define NEOPIXEL_STATUS_PAIRING_COLOR 0x0000FF // blue
#endif
#endif
class StatusLEDModule : private concurrency::OSThread
{
bool slowTrack = false;
@@ -27,6 +46,13 @@ class StatusLEDModule : private concurrency::OSThread
void setPowerLED(bool);
#ifdef NEOPIXEL_STATUS_POWER_PIN
Adafruit_NeoPixel powerPixel = Adafruit_NeoPixel(1, NEOPIXEL_STATUS_POWER_PIN, NEOPIXEL_STATUS_TYPE);
#endif
#ifdef NEOPIXEL_STATUS_PAIRING_PIN
Adafruit_NeoPixel pairingPixel = Adafruit_NeoPixel(1, NEOPIXEL_STATUS_PAIRING_PIN, NEOPIXEL_STATUS_TYPE);
#endif
protected:
unsigned int my_interval = 1000; // interval in millisconds
virtual int32_t runOnce() override;
+2 -2
View File
@@ -90,8 +90,8 @@ int32_t PaxcounterModule::runOnce()
configuration.blecounter = 1;
configuration.blescantime = 0; // infinite
configuration.wificounter = 1;
configuration.wifi_channel_map = WIFI_CHANNEL_ALL;
configuration.wifi_channel_switch_interval = 50;
// configuration.wifi_channel_map = WIFI_CHANNEL_ALL;
// configuration.wifi_channel_switch_interval = 50;
configuration.wifi_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.wifi_threshold, -80);
configuration.ble_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.ble_threshold, -80);
libpax_update_config(&configuration);
+2 -6
View File
@@ -19,12 +19,8 @@
#include "mesh/wifi/WiFiAPClient.h"
#include <WiFi.h>
#endif
#if HAS_ETHERNET && defined(USE_WS5500)
#include <ETHClass2.h>
#define ETH ETH2
#elif HAS_ETHERNET && defined(USE_CH390D)
#include "ESP32_CH390.h"
#define ETH CH390
#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET
#include "Default.h"
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
+189 -248
View File
@@ -10,23 +10,18 @@
#include "mesh/PhoneAPI.h"
#include "mesh/mesh-pb-constants.h"
#include "sleep.h"
#include <NimBLEDevice.h>
#include <BLE2904.h>
#include <BLEAdvertising.h>
#include <BLEDevice.h>
#include <BLESecurity.h>
#include <BLEUtils.h>
#include <atomic>
#include <mutex>
#ifdef NIMBLE_TWO
#include "NimBLEAdvertising.h"
#include "NimBLEExtAdvertising.h"
#include "PowerStatus.h"
#endif
#if defined(CONFIG_NIMBLE_CPP_IDF)
#include "host/ble_gap.h"
#else
#include "nimble/nimble/host/include/host/ble_gap.h"
#endif
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
#include "host/ble_store.h"
namespace
{
@@ -34,7 +29,6 @@ constexpr uint16_t kPreferredBleMtu = 517;
constexpr uint16_t kPreferredBleTxOctets = 251;
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
} // namespace
#endif
// Debugging options: careful, they slow things down quite a bit!
// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
@@ -44,10 +38,10 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
#define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3
#define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3
NimBLECharacteristic *fromNumCharacteristic;
NimBLECharacteristic *BatteryCharacteristic;
NimBLECharacteristic *logRadioCharacteristic;
NimBLEServer *bleServer;
BLECharacteristic *fromNumCharacteristic;
BLECharacteristic *BatteryCharacteristic;
BLECharacteristic *logRadioCharacteristic;
BLEServer *bleServer;
static bool passkeyShowing;
static std::atomic<uint16_t> nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection"
@@ -118,7 +112,8 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
- Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management.
NOTIFY IS BROKEN:
- Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible.
- Adding BLECharacteristic::PROPERTY_NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards
compatible.
ZERO-SIZE READS:
- Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we
@@ -139,7 +134,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
std::mutex fromPhoneMutex;
std::atomic<size_t> fromPhoneQueueSize{0};
// We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
std::array<NimBLEAttValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
std::array<BLEValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
/* Packets to phone (BLE onRead callback) */
std::mutex toPhoneMutex;
@@ -301,7 +296,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load());
// Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop.
NimBLEAttValue val;
BLEValue val;
{ // scope for fromPhoneMutex mutex
std::lock_guard<std::mutex> guard(fromPhoneMutex);
val = fromPhoneQueue[0];
@@ -316,7 +311,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
fromPhoneQueueSize--;
}
handleToRadio(val.data(), val.length());
handleToRadio(val.getData(), val.getLength());
}
}
@@ -328,9 +323,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
PhoneAPI::onNowHasData(fromRadioNum);
#ifdef DEBUG_NIMBLE_NOTIFY
int currentNotifyCount = notifyCount.fetch_add(1);
uint8_t cc = bleServer->getConnectedCount();
// This logging slows things down when there are lots of packets going to the phone, like initial connection:
LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc);
@@ -340,13 +333,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
put_le32(val, fromRadioNum);
fromNumCharacteristic->setValue(val, sizeof(val));
#ifdef NIMBLE_TWO
// NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be
// notify().
fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE);
#else
fromNumCharacteristic->notify();
#endif
}
/// Check the current underlying physical link to see if the client is currently connected
@@ -409,14 +396,9 @@ static BluetoothPhoneAPI *bluetoothPhoneAPI;
// Last ToRadio value received from the phone
static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
class NimbleBluetoothToRadioCallback : public BLECharacteristicCallbacks
{
#ifdef NIMBLE_TWO
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
#else
virtual void onWrite(NimBLECharacteristic *pCharacteristic) override
#endif
void onWrite(BLECharacteristic *pCharacteristic) override
{
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
// Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls.
@@ -428,15 +410,17 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis);
#endif
auto val = pCharacteristic->getValue();
// Create a BLEValue and populate it with the received data
BLEValue val;
val.setValue(pCharacteristic->getData(), pCharacteristic->getLength());
if (memcmp(lastToRadio, val.data(), val.length()) != 0) {
if (memcmp(lastToRadio, val.getData(), val.getLength()) != 0) {
if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) {
// Note: the comparison above is safe without a mutex because we are the only method that *increases*
// fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.)
memcpy(lastToRadio, val.data(), val.length());
memcpy(lastToRadio, val.getData(), val.getLength());
{ // scope for fromPhoneMutex mutex
{ // scope for fromPhoneMutex mutexv, pCharacteristic->getLen
// Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible.
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
@@ -450,24 +434,21 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
int finishMillis = millis();
LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount,
finishMillis - startMillis, val.length());
finishMillis - startMillis, val.getLength());
#endif
} else {
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length());
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount,
val.getLength());
}
} else {
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length());
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.getLength());
}
}
};
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
class NimbleBluetoothFromRadioCallback : public BLECharacteristicCallbacks
{
#ifdef NIMBLE_TWO
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
#else
virtual void onRead(NimBLECharacteristic *pCharacteristic) override
#endif
void onRead(BLECharacteristic *pCharacteristic) override
{
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
@@ -573,51 +554,46 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
}
};
class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
class NimbleBluetoothSecurityCallback : public BLESecurityCallbacks
{
#ifdef NIMBLE_TWO
public:
NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; }
private:
NimbleBluetooth *ble;
virtual uint32_t onPassKeyDisplay() override
#else
virtual uint32_t onPassKeyRequest() override
#endif
void onPassKeyNotify(uint32_t passkey) override
{
uint32_t configuredPasskey = config.bluetooth.fixed_pin;
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
LOG_INFO("Use random passkey");
// This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits
configuredPasskey = random(100000, 999999);
}
LOG_INFO("*** Enter passkey %d on the peer side ***", configuredPasskey);
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey);
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
std::string passkey = std::to_string(configuredPasskey);
meshtastic::BluetoothStatus newStatus(passkey);
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
bluetoothStatus->updateStatus(&newStatus);
#if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
#if HAS_SCREEN
if (screen) {
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
char btPIN[16] = "888888";
snprintf(btPIN, sizeof(btPIN), "%06u", passkey);
int x_offset = display->width() / 2;
int y_offset = display->height() <= 80 ? 0 : 12;
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_MEDIUM);
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
#if !defined(OLED_TINY)
display->setFont(FONT_SMALL);
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
display->drawString(x_offset + x, y_offset + y, "Enter this code");
#endif
display->setFont(FONT_LARGE);
char pin[8];
snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3);
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
display->drawString(x_offset + x, y_offset + y, pin);
std::string ble_message = "Bluetooth\nPIN\n[M]" + passkey.substr(0, 3) + " " + passkey.substr(3, 6);
screen->showSimpleBanner(ble_message.c_str(), 30000);
display->setFont(FONT_SMALL);
char deviceName[64];
snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName());
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
display->drawString(x_offset + x, y_offset + y, deviceName);
});
}
#endif
passkeyShowing = true;
return configuredPasskey;
}
#ifdef NIMBLE_TWO
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) override
#else
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) override
#endif
void onAuthenticationComplete(ble_gap_conn_desc *desc) override
{
LOG_INFO("BLE authentication complete");
@@ -625,58 +601,47 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
bluetoothStatus->updateStatus(&newStatus);
clearPairingDisplay();
// Store the connection handle for future use
#ifdef NIMBLE_TWO
nimbleBluetoothConnHandle = connInfo.getConnHandle();
#else
nimbleBluetoothConnHandle = desc->conn_handle;
#endif
}
};
#ifdef NIMBLE_TWO
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override
class NimbleBluetoothServerCallback : public BLEServerCallbacks
{
public:
explicit NimbleBluetoothServerCallback(NimbleBluetooth *ble) : ble(ble) {}
private:
NimbleBluetooth *ble;
void onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
{
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
BLEAddress peer_addr(desc->peer_id_addr);
LOG_INFO("BLE incoming connection %s", peer_addr.toString().c_str());
const uint16_t connHandle = connInfo.getConnHandle();
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
int phyResult =
ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY);
if (phyResult == 0) {
LOG_INFO("BLE conn %u requested 2M PHY", connHandle);
} else {
LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult);
}
#endif
const uint16_t connHandle = desc->conn_handle;
// With Google Pixel 8 Android devices, this causes ESP32 device crash
// when phone reconnects. Disable this to make progress on the
// Arduino v3 migration while we investigate the Android compatibility
// issue.
#if 0
int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
if (dataLenResult == 0) {
LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets);
} else {
LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult);
}
#endif
LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu);
LOG_INFO("BLE conn %u peer MTU %u (target %u)", connHandle, pServer->getPeerMTU(connHandle), kPreferredBleMtu);
pServer->updateConnParams(connHandle, 6, 12, 0, 200);
}
#endif
#ifdef NIMBLE_TWO
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override
void onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
{
LOG_INFO("BLE disconnect reason: %d", reason);
#else
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) override
{
LOG_INFO("BLE disconnect");
#endif
#ifdef NIMBLE_TWO
LOG_INFO("BLE disconnected");
if (ble->isDeInit)
return;
#else
if (nimbleBluetooth && nimbleBluetooth->isDeInit)
return;
#endif
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus);
@@ -701,43 +666,51 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
bluetoothPhoneAPI->writeCount = 0;
}
// Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
memset(lastToRadio, 0, sizeof(lastToRadio));
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection"
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE;
#ifdef NIMBLE_TWO
// Restart Advertising
ble->startAdvertising();
#else
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
if (!pAdvertising->start(0)) {
if (pAdvertising->isAdvertising()) {
LOG_DEBUG("BLE advertising already running");
} else {
LOG_ERROR("BLE failed to restart advertising");
}
}
#endif
}
};
static NimbleBluetoothToRadioCallback *toRadioCallbacks;
static NimbleBluetoothFromRadioCallback *fromRadioCallbacks;
void NimbleBluetooth::startAdvertising()
{
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->stop();
pAdvertising->reset();
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
// if (powerStatus->getHasBattery() == 1) {
// pAdvertising->addServiceUUID(BLEUUID((uint16_t)0x180f));
// }
BLEAdvertisementData scan = BLEAdvertisementData();
scan.setName(getDeviceName());
pAdvertising->setScanResponseData(scan);
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
pAdvertising->setMaxPreferred(0x12);
if (!pAdvertising->start(0)) {
LOG_ERROR("BLE failed to start advertising");
} else {
LOG_DEBUG("BLE Advertising started");
}
}
void NimbleBluetooth::shutdown()
{
// No measurable power saving for ESP32 during light-sleep(?)
#ifndef ARCH_ESP32
// Shutdown bluetooth for minimum power draw
LOG_INFO("Disable bluetooth");
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->reset();
pAdvertising->stop();
#endif
}
// Proper shutdown for ESP32. Needs reboot to reverse.
void NimbleBluetooth::deinit()
{
#ifdef ARCH_ESP32
@@ -747,21 +720,19 @@ void NimbleBluetooth::deinit()
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
#ifndef NIMBLE_TWO
NimBLEDevice::deinit();
#endif
BLEDevice::deinit(true);
#endif
}
// Has initial setup been completed
bool NimbleBluetooth::isActive()
{
return bleServer;
return bleServer != nullptr;
}
bool NimbleBluetooth::isConnected()
{
return bleServer->getConnectedCount() > 0;
return bleServer && bleServer->getConnectedCount() > 0;
}
int NimbleBluetooth::getRssi()
@@ -774,9 +745,9 @@ int NimbleBluetooth::getRssi()
uint16_t connHandle = nimbleBluetoothConnHandle.load();
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
const auto peers = bleServer->getPeerDevices();
const auto peers = bleServer->getPeerDevices(true);
if (!peers.empty()) {
connHandle = peers.front();
connHandle = peers.begin()->first;
nimbleBluetoothConnHandle = connHandle;
}
}
@@ -804,74 +775,84 @@ void NimbleBluetooth::setup()
LOG_INFO("Init the NimBLE bluetooth module");
NimBLEDevice::init(getDeviceName());
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
BLEDevice::init(getDeviceName());
BLEDevice::setPower(ESP_PWR_LVL_P9);
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu);
int mtuResult = BLEDevice::setMTU(kPreferredBleMtu);
if (mtuResult == 0) {
LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu);
} else {
LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult);
}
int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK);
if (phyResult == 0) {
LOG_INFO("BLE default PHY preference set to 2M");
} else {
LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult);
}
int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs);
if (dataLenResult == 0) {
LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets);
} else {
LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs,
dataLenResult);
}
#endif
BLESecurity *pSecurity = new BLESecurity();
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
pSecurity->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC);
NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
// Set IO capability to DisplayOnly for MITM authentication
pSecurity->setCapability(ESP_IO_CAP_OUT);
// Set the passkey
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
LOG_INFO("Use random passkey");
pSecurity->setPassKey(false); // generate a random passkey
} else {
LOG_INFO("Use fixed passkey");
pSecurity->setPassKey(true, config.bluetooth.fixed_pin);
}
// Enable authorization requirements:
// - bonding: true (for persistent storage of the keys)
// - MITM: true (enables Man-In-The-Middle protection for password prompts)
// - secure connection: true (enables secure connection for encryption)
pSecurity->setAuthenticationMode(true, true, true);
} else {
// No IO capability for no PIN mode
pSecurity->setCapability(ESP_IO_CAP_NONE);
// No PIN mode: no MITM protection
pSecurity->setAuthenticationMode(true, false, false);
}
bleServer = NimBLEDevice::createServer();
#ifdef NIMBLE_TWO
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this);
#else
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback();
#endif
bleServer->setCallbacks(serverCallbacks, true);
// Set the security callbacks
BLEDevice::setSecurityCallbacks(new NimbleBluetoothSecurityCallback());
bleServer = BLEDevice::createServer();
// BLEDevice::createServer calls ble_svc_gap_init, which resets the device
// name to default, so set it again.
int nameRc = ble_svc_gap_device_name_set(BLEDevice::getDeviceName().c_str());
if (nameRc != 0) {
LOG_ERROR("ble_svc_gap_device_name_set: rc=%d %s", nameRc, BLEUtils::returnCodeToString(nameRc));
}
bleServer->setCallbacks(new NimbleBluetoothServerCallback(this));
setupService();
startAdvertising();
}
void NimbleBluetooth::setupService()
{
NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID);
NimBLECharacteristic *ToRadioCharacteristic;
NimBLECharacteristic *FromRadioCharacteristic;
BLEService *bleService = bleServer->createService(MESH_SERVICE_UUID);
BLECharacteristic *ToRadioCharacteristic;
BLECharacteristic *FromRadioCharacteristic;
// Define the characteristics that the app is looking for
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE);
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE);
// Allow notifications so phones can stream FromRadio without polling.
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ);
fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ);
logRadioCharacteristic =
bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U);
} else {
ToRadioCharacteristic = bleService->createCharacteristic(
TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC);
FromRadioCharacteristic = bleService->createCharacteristic(
FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ);
fromNumCharacteristic =
bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ |
NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
bleService->createCharacteristic(FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ);
logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_READ);
} else {
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_WRITE_AUTHEN |
BLECharacteristic::PROPERTY_WRITE_ENC);
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN |
BLECharacteristic::PROPERTY_READ_ENC);
fromNumCharacteristic = bleService->createCharacteristic(
FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
logRadioCharacteristic = bleService->createCharacteristic(
LOGRADIO_UUID,
NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U);
LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
}
bluetoothPhoneAPI = new BluetoothPhoneAPI();
@@ -884,76 +865,31 @@ void NimbleBluetooth::setupService()
bleService->start();
// Setup the battery service
NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
(uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1);
#ifdef NIMBLE_TWO
NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904();
#else
NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904);
#endif
batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8);
BLEService *batteryService = bleServer->createService(BLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
BLE2904 *batteryLevelDescriptor = new BLE2904();
batteryLevelDescriptor->setFormat(BLE2904::FORMAT_UINT8);
batteryLevelDescriptor->setNamespace(1);
batteryLevelDescriptor->setUnit(0x27ad);
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
(uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
BatteryCharacteristic->addDescriptor(batteryLevelDescriptor);
batteryService->start();
}
void NimbleBluetooth::startAdvertising()
{
#ifdef NIMBLE_TWO
NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
NimBLEExtAdvertisement legacyAdvertising;
legacyAdvertising.setLegacyAdvertising(true);
legacyAdvertising.setScannable(true);
legacyAdvertising.setConnectable(true);
legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN);
if (powerStatus->getHasBattery() == 1) {
legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f));
}
legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID));
legacyAdvertising.setMinInterval(500);
legacyAdvertising.setMaxInterval(1000);
NimBLEExtAdvertisement legacyScanResponse;
legacyScanResponse.setLegacyAdvertising(true);
legacyScanResponse.setConnectable(true);
legacyScanResponse.setName(getDeviceName());
if (!pAdvertising->setInstanceData(0, legacyAdvertising)) {
LOG_ERROR("BLE failed to set legacyAdvertising");
} else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) {
LOG_ERROR("BLE failed to set legacyScanResponse");
} else if (!pAdvertising->start(0, 0, 0)) {
LOG_ERROR("BLE failed to start legacyAdvertising");
}
#else
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->reset();
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
pAdvertising->start(0);
#endif
}
/// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level)
{
if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) {
BatteryCharacteristic->setValue(&level, 1);
#ifdef NIMBLE_TWO
BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE);
#else
BatteryCharacteristic->notify();
#endif
}
}
void NimbleBluetooth::clearBonds()
{
LOG_INFO("Clearing bluetooth bonds!");
NimBLEDevice::deleteAllBonds();
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
}
void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
@@ -961,11 +897,16 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
if (!bleServer || !isConnected() || length > 512) {
return;
}
#ifdef NIMBLE_TWO
logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE);
#else
logRadioCharacteristic->notify(logMessage, length, true);
#endif
logRadioCharacteristic->setValue(logMessage, length);
logRadioCharacteristic->notify();
}
void clearNVS()
{
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
#ifdef ARCH_ESP32
ESP.restart();
#endif
}
#endif
-5
View File
@@ -12,16 +12,11 @@ class NimbleBluetooth : BluetoothApi
bool isConnected();
int getRssi();
void sendLog(const uint8_t *logMessage, size_t length);
#if defined(NIMBLE_TWO)
void startAdvertising();
#endif
bool isDeInit = false;
private:
void setupService();
#if !defined(NIMBLE_TWO)
void startAdvertising();
#endif
};
void setBluetoothEnable(bool enable);
+42
View File
@@ -0,0 +1,42 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_attr.h"
#ifdef ESP32_FORCE_IRAM_MEMSET
/*
* T-Beam/classic ESP32 boot workaround
* ------------------------------------
* During early flash operations the ESP32 disables cache, but some IRAM flash
* code paths still reach libc memcpy/memset. If those resolve to flash-resident
* implementations, startup can panic with cache-disabled access errors.
*
* We wrap memcpy/memset for the T-Beam environment. Fast path uses the
* normal libc routines when cache is enabled; slow path uses IRAM-safe byte
* loops when cache is disabled.
*/
extern void *__real_memcpy(void *dst, const void *src, size_t n);
static inline bool IRAM_ATTR cache_is_enabled(void)
{
return (*(volatile uint32_t *)0x3FF00040u) & (1u << 3);
}
extern void *IRAM_ATTR __wrap_memcpy(void *dst, const void *src, size_t n)
{
if (cache_is_enabled()) {
return __real_memcpy(dst, src, n);
}
uint8_t *d = (uint8_t *)dst;
const uint8_t *s = (const uint8_t *)src;
while (n--) {
*d++ = *s++;
}
return dst;
}
#endif
+42
View File
@@ -0,0 +1,42 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_attr.h"
#ifdef ESP32_FORCE_IRAM_MEMSET
/*
* T-Beam/classic ESP32 boot workaround
* ------------------------------------
* During early flash operations the ESP32 disables cache, but some IRAM flash
* code paths still reach libc memcpy/memset. If those resolve to flash-resident
* implementations, startup can panic with cache-disabled access errors.
*
* We wrap memcpy/memset for the T-Beam environment. Fast path uses the
* normal libc routines when cache is enabled; slow path uses IRAM-safe byte
* loops when cache is disabled.
*/
extern void *__real_memset(void *dst, int c, size_t n);
static inline bool IRAM_ATTR cache_is_enabled(void)
{
return (*(volatile uint32_t *)0x3FF00040u) & (1u << 3);
}
extern void *IRAM_ATTR __wrap_memset(void *dst, int c, size_t n)
{
if (cache_is_enabled()) {
return __real_memset(dst, c, n);
}
uint8_t *ptr = (uint8_t *)dst;
const uint8_t fill = (uint8_t)c;
while (n--) {
*ptr++ = fill;
}
return dst;
}
#endif
@@ -0,0 +1,11 @@
/* Arduino fix: catch esp_event's orphaned .text.handler_execute section and align to 4 bytes */
SECTIONS
{
.text.handler_execute ALIGN(4) :
{
KEEP(*(.text.handler_execute))
KEEP(*(.text.handler_execute.*))
. = ALIGN(4);
}
}
INSERT AFTER .flash.text;
+21 -6
View File
@@ -165,17 +165,30 @@ void esp32Setup()
// #define APP_WATCHDOG_SECS 45
#define APP_WATCHDOG_SECS 90
#ifdef CONFIG_IDF_TARGET_ESP32C6
esp_task_wdt_config_t *wdt_config = (esp_task_wdt_config_t *)malloc(sizeof(esp_task_wdt_config_t));
wdt_config->timeout_ms = APP_WATCHDOG_SECS * 1000;
wdt_config->trigger_panic = true;
res = esp_task_wdt_init(wdt_config);
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
const esp_task_wdt_config_t wdt_config = {
.timeout_ms = APP_WATCHDOG_SECS * 1000,
.idle_core_mask = (1U << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1U,
.trigger_panic = true,
};
res = esp_task_wdt_init(&wdt_config);
if (res == ESP_ERR_INVALID_STATE) {
LOG_WARN("Task watchdog already initialized, reconfiguring existing instance");
res = esp_task_wdt_reconfigure(&wdt_config);
}
assert(res == ESP_OK);
#else
res = esp_task_wdt_init(APP_WATCHDOG_SECS, true);
if (res == ESP_ERR_INVALID_STATE) {
LOG_WARN("Task watchdog already initialized, reusing existing instance");
res = ESP_OK;
}
assert(res == ESP_OK);
#endif
res = esp_task_wdt_add(NULL);
res = esp_task_wdt_status(NULL);
if (res == ESP_ERR_NOT_FOUND) {
res = esp_task_wdt_add(NULL);
}
assert(res == ESP_OK);
#if HAS_32768HZ
@@ -258,8 +271,10 @@ void cpuDeepSleep(uint32_t msecToWake)
#endif
variant_shutdown();
#if SOC_PM_SUPPORT_RTC_PERIPH_PD
// We want RTC peripherals to stay on
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
#endif
esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs
esp_deep_sleep_start(); // TBD mA sleep current (battery)
@@ -3,14 +3,63 @@
#ifdef M5STACK_CARDPUTER_ADV
#include "AudioBoard.h"
#include <Wire.h>
DriverPins PinsAudioBoardES8311;
AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311);
// PI4IOE5V6408 on the optional Cap LoRa-1262 (and Cap LoRa868).
#define PI4IO_ADDR 0x43
#define PI4IO_REG_IO_DIR 0x03
#define PI4IO_REG_OUT_SET 0x05
#define PI4IO_REG_OUT_H_IM 0x07
static TwoWire *findLoraCapBus()
{
TwoWire *candidates[] = {&Wire1, &Wire};
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); ++i) {
candidates[i]->beginTransmission(PI4IO_ADDR);
if (candidates[i]->endTransmission() == 0) {
return candidates[i];
}
}
return nullptr;
}
static bool pi4ioWrite(TwoWire *bus, uint8_t reg, uint8_t val)
{
bus->beginTransmission(PI4IO_ADDR);
bus->write(reg);
bus->write(val);
uint8_t status = bus->endTransmission();
if (status != 0) {
LOG_DEBUG("PI4IO write reg=0x%02x val=0x%02x failed, I2C status=%u", reg, val, status);
return false;
}
return true;
}
static void initLoraCap()
{
TwoWire *bus = findLoraCapBus();
if (!bus) {
LOG_ERROR("Cap LoRa-1262 not found");
return;
}
bool ok = pi4ioWrite(bus, PI4IO_REG_IO_DIR, 0b00000001);
ok = ok && pi4ioWrite(bus, PI4IO_REG_OUT_H_IM, 0b00000000);
ok = ok && pi4ioWrite(bus, PI4IO_REG_OUT_SET, 0b00000001);
if (!ok) {
LOG_ERROR("Antenna switch init failed");
}
}
// M5stack Cardputer ADV specific init
void lateInitVariant()
{
initLoraCap();
// AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug);
// I2C: function, scl, sda
PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire);
+824
View File
@@ -0,0 +1,824 @@
/**
* Arduino.h — Zephyr compatibility shim for nRF54L15
*
* Provides the Arduino API surface expected by Meshtastic, backed by
* Zephyr primitives. Only the subset actually used by Meshtastic is
* implemented; the rest compiles as no-ops / stubs for now.
*
* Phase 2: compile only. Real GPIO / SPI / Wire implementations follow
* in Phase 3 once the build is clean.
*/
#pragma once
#ifndef Arduino_h
#define Arduino_h
// ── C standard headers ───────────────────────────────────────────────────────
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp, strncasecmp */
// ── Zephyr kernel ────────────────────────────────────────────────────────────
#include <zephyr/kernel.h>
#include <zephyr/sys/reboot.h>
// ── Basic Arduino types ──────────────────────────────────────────────────────
typedef bool boolean;
typedef uint8_t byte;
typedef uint16_t word;
// ── Pin / digital constants ──────────────────────────────────────────────────
#define INPUT 0u
#define OUTPUT 1u
#define INPUT_PULLUP 2u
#define INPUT_PULLDOWN 3u
#define OUTPUT_OPENDRAIN 4u
#define HIGH 1u
#define LOW 0u
#define CHANGE 1
#define FALLING 2
#define RISING 3
#ifndef LED_BUILTIN
#define LED_BUILTIN -1
#endif
// ── Math / trig constants ────────────────────────────────────────────────────
#ifndef PI
#define PI 3.14159265358979323846
#endif
#define HALF_PI 1.57079632679489661923
#define TWO_PI 6.28318530717958647693
#define DEG_TO_RAD 0.01745329251994329576
#define RAD_TO_DEG 57.2957795130823208767
#define EULER 2.71828182845904523536
// ── Bit utilities ────────────────────────────────────────────────────────────
#define bitRead(v, b) (((v) >> (b)) & 1)
#define bitSet(v, b) ((v) |= (1UL << (b)))
#define bitClear(v, b) ((v) &= ~(1UL << (b)))
#define bitToggle(v, b) ((v) ^= (1UL << (b)))
#define bitWrite(v, b, x) ((x) ? bitSet(v, b) : bitClear(v, b))
#define bit(b) (1UL << (b))
#define lowByte(w) ((uint8_t)((w)&0xff))
#define highByte(w) ((uint8_t)((w) >> 8))
// word(h,l) — only define if not already defined (conflicts with typedef above)
#undef word
#define word(h, l) ((uint16_t)(((h) << 8) | (l)))
// ── UART config constants ─────────────────────────────────────────────────────
#define SERIAL_8N1 0x800001cu
#define SERIAL_8N2 0x8000001eu
#define SERIAL_8E1 0x8000001eu
#define SERIAL_7E1 0x8000001cu
// ── Integer order ────────────────────────────────────────────────────────────
// Adafruit BusIO's SPIDevice.h has `typedef BitOrder BusIOBitOrder;` which
// requires BitOrder to be a *type*, not a macro. Mirror the ArduinoCore-API
// enum definition rather than #defines.
enum BitOrder : uint8_t {
LSBFIRST = 0,
MSBFIRST = 1,
};
// ── pgmspace compatibility (no-ops on Cortex-M) ──────────────────────────────
#define PROGMEM
#define PSTR(s) (s)
#define F(s) (s)
#define pgm_read_byte(addr) (*((const uint8_t *)(addr)))
#define pgm_read_word(addr) (*((const uint16_t *)(addr)))
#define pgm_read_dword(addr) (*((const uint32_t *)(addr)))
#define pgm_read_float(addr) (*((const float *)(addr)))
#define pgm_read_ptr(addr) (*((const void **)(addr)))
#define strlen_P(s) strlen(s)
#define strcpy_P(d, s) strcpy(d, s)
#define strncpy_P(d, s, n) strncpy(d, s, n)
#define strcmp_P(a, b) strcmp(a, b)
#define memcpy_P(d, s, n) memcpy(d, s, n)
#define sprintf_P sprintf
typedef const char *PGM_P;
typedef const char *PGM_VOID_P;
// ── Arduino numeric base constants (used by Print, RadioLib, etc.) ───────────
#define DEC 10
#define HEX 16
#define OCT 8
#define BIN 2
// ── ulong / uint typedef (used by RadioLibInterface, etc.) ───────────────────
typedef unsigned long ulong;
typedef unsigned int uint;
// ── Interrupt stubs ──────────────────────────────────────────────────────────
static inline void interrupts() {}
static inline void noInterrupts() {}
#define digitalPinToInterrupt(p) (p)
// ── portMAX_DELAY — freertosinc.h also defines this; let it win ──────────────
// We intentionally do NOT define portMAX_DELAY here. freertosinc.h defines
// it for the FreeRTOS / Meshtastic threading layer and must not be overridden.
// ── Timing & system functions — declared with C linkage ──────────────────────
// buzz.cpp and others forward-declare delay() as extern "C"; keep linkage
// consistent by wrapping in extern "C" here.
#ifdef __cplusplus
extern "C" {
#endif
void NVIC_SystemReset(void);
uint32_t millis(void);
uint32_t micros(void);
void delay(uint32_t ms);
void delayMicroseconds(uint32_t us);
void yield(void);
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
#include <cctype>
#include <cstdarg>
// ── C++ STL — include BEFORE defining any min/max helpers ───────────────────
// Include algorithm first so its min/max templates are in scope.
// We must NOT define min/max as function-like macros: the C++ STL uses
// 3-argument versions (min(a,b,comp)) that the preprocessor would treat as
// calling a 2-arg macro with 3 args.
#include <algorithm>
// Bring 2-arg std::min / std::max into the global namespace as unqualified
// names so that Arduino code calling min(a,b) continues to compile.
// (Arduino convention; kept minimal to avoid surprises.)
#undef min
#undef max
using std::max;
using std::min;
// ── Arduino math helpers (macros safe for mixed-type / C calls) ──────────────
#ifndef abs
#define abs(x) ((x) >= 0 ? (x) : -(x))
#endif
#define constrain(x, l, h) ((x) < (l) ? (l) : ((x) > (h) ? (h) : (x)))
#define round(x) ((x) >= 0 ? (long)((x) + 0.5) : (long)((x)-0.5))
#define radians(d) ((d)*DEG_TO_RAD)
#define degrees(r) ((r)*RAD_TO_DEG)
#define sq(x) ((x) * (x))
// ── Random ───────────────────────────────────────────────────────────────────
static inline void randomSeed(unsigned long seed)
{
srand((unsigned int)seed);
}
static inline long random(void)
{
return (long)rand();
}
static inline long random(long bound)
{
return bound > 0 ? (rand() % bound) : 0;
}
static inline long random(long lo, long hi)
{
return hi > lo ? lo + rand() % (hi - lo) : lo;
}
// ── GPIO — real Zephyr implementation (Phase 3) ──────────────────────────────
// Implemented in nrf54l15_arduino.cpp using Zephyr GPIO/SPI APIs.
// Pin numbering: P0.n = n, P1.n = 16+n, P2.n = 32+n
void pinMode(uint32_t pin, uint32_t mode);
void digitalWrite(uint32_t pin, uint32_t value);
int digitalRead(uint32_t pin);
static inline void digitalToggle(uint32_t pin)
{
digitalWrite(pin, !digitalRead(pin));
}
static inline uint32_t analogRead(uint32_t)
{
return 0;
}
static inline void analogWrite(uint32_t, uint32_t) {}
static inline void analogReadResolution(int) {}
static inline void analogWriteResolution(int) {}
// ── __WFI — provided by CMSIS core_cm33.h; do NOT redefine here ─────────────
// ── __FlashStringHelper — Arduino PROGMEM string class (no-op on Cortex-M) ──
class __FlashStringHelper;
// ── attachInterrupt / detachInterrupt — real Zephyr GPIO interrupt impl ──────
typedef void (*voidFuncPtr)(void);
void attachInterrupt(uint32_t pin, voidFuncPtr cb, int mode);
void detachInterrupt(uint32_t pin);
// ── Forward declaration of String (needed by Print / Stream) ─────────────────
class String;
// ── Print base class ─────────────────────────────────────────────────────────
class Print
{
public:
virtual size_t write(uint8_t c) = 0;
virtual size_t write(const uint8_t *buf, size_t n)
{
size_t written = 0;
while (n--)
written += write(*buf++);
return written;
}
size_t write(const char *s) { return s ? write((const uint8_t *)s, strlen(s)) : 0; }
size_t write(const char *s, size_t n) { return write((const uint8_t *)s, n); }
size_t print(const char *s) { return s ? write((const uint8_t *)s, strlen(s)) : 0; }
int printf(const char *fmt, ...) __attribute__((format(printf, 2, 3)));
size_t print(char c) { return write((uint8_t)c); }
size_t print(const String &s);
size_t print(unsigned char n, int base = 10);
size_t print(int n, int base = 10);
size_t print(long n, int base = 10);
size_t print(unsigned int n, int base = 10);
size_t print(unsigned long n, int base = 10);
size_t print(float n, int digits = 2);
size_t print(double n, int digits = 2);
size_t print(bool b) { return print(b ? "true" : "false"); }
size_t println() { return write((uint8_t)'\n'); }
size_t println(const char *s)
{
size_t r = print(s);
return r + println();
}
size_t println(char c)
{
size_t r = print(c);
return r + println();
}
size_t println(const String &s);
size_t println(int n, int base = 10)
{
size_t r = print(n, base);
return r + println();
}
size_t println(long n, int base = 10)
{
size_t r = print(n, base);
return r + println();
}
size_t println(unsigned long n, int base = 10)
{
size_t r = print(n, base);
return r + println();
}
size_t println(unsigned int n, int base = 10)
{
size_t r = print(n, base);
return r + println();
}
size_t println(float n, int d = 2)
{
size_t r = print(n, d);
return r + println();
}
size_t println(double n, int d = 2)
{
size_t r = print(n, d);
return r + println();
}
size_t println(bool b)
{
size_t r = print(b);
return r + println();
}
virtual void flush() {}
};
// ── Stream base class ────────────────────────────────────────────────────────
class Stream : public Print
{
public:
virtual int available() = 0;
virtual int read() = 0;
virtual int peek() = 0;
virtual void setTimeout(unsigned long) {}
virtual bool find(const char *) { return false; }
String readString();
String readStringUntil(char terminator);
};
// ── Minimal Arduino String class (backed by a char buffer) ───────────────────
class String
{
public:
String() : _buf(nullptr), _len(0), _cap(0) {}
// Implicit conversion is part of the Arduino String contract, used pervasively across the codebase.
// cppcheck-suppress noExplicitConstructor
String(const char *cstr) : _buf(nullptr), _len(0), _cap(0)
{
if (cstr)
assign(cstr, strlen(cstr));
}
// cppcheck-suppress noExplicitConstructor
String(const String &s) : _buf(nullptr), _len(0), _cap(0) { assign(s._buf ? s._buf : "", s._len); }
// cppcheck-suppress noExplicitConstructor
String(char c) : _buf(nullptr), _len(0), _cap(0)
{
const char tmp[2] = {c, 0};
assign(tmp, 1);
}
// cppcheck-suppress noExplicitConstructor
String(int n) : _buf(nullptr), _len(0), _cap(0)
{
char tmp[16];
snprintf(tmp, 16, "%d", n);
assign(tmp, strlen(tmp));
}
// cppcheck-suppress noExplicitConstructor
String(unsigned int n) : _buf(nullptr), _len(0), _cap(0)
{
char tmp[16];
snprintf(tmp, 16, "%u", n);
assign(tmp, strlen(tmp));
}
// cppcheck-suppress noExplicitConstructor
String(long n) : _buf(nullptr), _len(0), _cap(0)
{
char tmp[24];
snprintf(tmp, 24, "%ld", n);
assign(tmp, strlen(tmp));
}
// cppcheck-suppress noExplicitConstructor
String(unsigned long n) : _buf(nullptr), _len(0), _cap(0)
{
char tmp[24];
snprintf(tmp, 24, "%lu", n);
assign(tmp, strlen(tmp));
}
// cppcheck-suppress noExplicitConstructor
String(float n, int d = 2) : _buf(nullptr), _len(0), _cap(0)
{
char tmp[32];
snprintf(tmp, 32, "%.*f", d, n);
assign(tmp, strlen(tmp));
}
// cppcheck-suppress noExplicitConstructor
String(double n, int d = 2) : _buf(nullptr), _len(0), _cap(0)
{
char tmp[32];
snprintf(tmp, 32, "%.*f", d, (double)n);
assign(tmp, strlen(tmp));
}
~String() { free(_buf); }
String &operator=(const String &s)
{
assign(s._buf ? s._buf : "", s._len);
return *this;
}
String &operator=(const char *s)
{
assign(s ? s : "", s ? strlen(s) : 0);
return *this;
}
String &operator=(char c)
{
const char tmp[2] = {c, 0};
assign(tmp, 1);
return *this;
}
String &operator+=(const String &s)
{
concat(s._buf ? s._buf : "", s._len);
return *this;
}
String &operator+=(const char *s)
{
if (s)
concat(s, strlen(s));
return *this;
}
String &operator+=(char c)
{
concat(&c, 1);
return *this;
}
String &operator+=(int n) { return *this += String(n); }
String &operator+=(unsigned long n) { return *this += String(n); }
String operator+(const String &rhs) const
{
String r(*this);
r += rhs;
return r;
}
String operator+(const char *rhs) const
{
String r(*this);
r += rhs;
return r;
}
String operator+(char rhs) const
{
String r(*this);
r += rhs;
return r;
}
bool operator==(const String &s) const { return _len == s._len && (_len == 0 || strcmp(_buf, s._buf) == 0); }
bool operator==(const char *s) const { return s && strcmp(c_str(), s) == 0; }
bool operator!=(const String &s) const { return !(*this == s); }
bool operator!=(const char *s) const { return !(*this == s); }
bool operator<(const String &s) const { return strcmp(c_str(), s.c_str()) < 0; }
bool operator>(const String &s) const { return strcmp(c_str(), s.c_str()) > 0; }
char operator[](unsigned int i) const { return (_buf && i < _len) ? _buf[i] : 0; }
char &operator[](unsigned int i)
{
static char dummy = 0;
return (_buf && i < _len) ? _buf[i] : dummy;
}
const char *c_str() const { return _buf ? _buf : ""; }
unsigned int length() const { return _len; }
bool isEmpty() const { return _len == 0; }
bool equals(const String &s) const { return *this == s; }
bool equals(const char *s) const { return *this == s; }
bool equalsIgnoreCase(const String &s) const
{
if (_len != s._len)
return false;
for (unsigned i = 0; i < _len; i++)
if (std::tolower(_buf[i]) != std::tolower(s._buf[i]))
return false;
return true;
}
bool startsWith(const String &pfx) const
{
if (pfx._len > _len)
return false;
return strncmp(c_str(), pfx.c_str(), pfx._len) == 0;
}
bool startsWith(const char *pfx) const
{
if (!pfx)
return false;
size_t pl = strlen(pfx);
return pl <= _len && strncmp(c_str(), pfx, pl) == 0;
}
bool endsWith(const String &sfx) const
{
if (sfx._len > _len)
return false;
return strcmp(c_str() + _len - sfx._len, sfx.c_str()) == 0;
}
int indexOf(char c, unsigned from = 0) const
{
if (!_buf)
return -1;
const char *p = strchr(_buf + from, c);
return p ? (int)(p - _buf) : -1;
}
int indexOf(const String &s, unsigned from = 0) const
{
if (!_buf)
return -1;
const char *p = strstr(_buf + from, s.c_str());
return p ? (int)(p - _buf) : -1;
}
int lastIndexOf(char c) const
{
if (!_buf)
return -1;
const char *p = strrchr(_buf, c);
return p ? (int)(p - _buf) : -1;
}
String substring(unsigned beginIndex) const
{
if (!_buf || beginIndex >= _len)
return String();
return String(_buf + beginIndex);
}
String substring(unsigned beginIndex, unsigned endIndex) const
{
if (!_buf || beginIndex >= _len)
return String();
if (endIndex > _len)
endIndex = _len;
if (endIndex <= beginIndex)
return String();
String r;
r.assign(_buf + beginIndex, endIndex - beginIndex);
return r;
}
void toUpperCase()
{
if (_buf)
for (unsigned i = 0; i < _len; i++)
_buf[i] = (char)std::toupper(_buf[i]);
}
void toLowerCase()
{
if (_buf)
for (unsigned i = 0; i < _len; i++)
_buf[i] = (char)std::tolower(_buf[i]);
}
void trim()
{
if (!_buf || _len == 0)
return;
unsigned s = 0;
while (s < _len && std::isspace(_buf[s]))
s++;
unsigned e = _len;
while (e > s && std::isspace(_buf[e - 1]))
e--;
if (s > 0 || e < _len) {
memmove(_buf, _buf + s, e - s);
_len = e - s;
_buf[_len] = 0;
}
}
void replace(char from, char to)
{
if (_buf)
for (unsigned i = 0; i < _len; i++)
if (_buf[i] == from)
_buf[i] = to;
}
void replace(const String &from, const String &to);
bool remove(unsigned index, unsigned count = 1)
{
if (!_buf || index >= _len)
return false;
if (index + count > _len)
count = _len - index;
memmove(_buf + index, _buf + index + count, _len - index - count + 1);
_len -= count;
return true;
}
void clear()
{
_len = 0;
if (_buf)
_buf[0] = 0;
}
char charAt(unsigned i) const { return (*this)[i]; }
void setCharAt(unsigned i, char c)
{
if (_buf && i < _len)
_buf[i] = c;
}
void toCharArray(char *buf, unsigned int bufsize, unsigned int index = 0) const
{
if (!buf || bufsize == 0)
return;
unsigned int avail = (_buf && _len > index) ? (_len - index) : 0;
unsigned int copy = avail < bufsize - 1 ? avail : bufsize - 1;
if (copy > 0)
memcpy(buf, _buf + index, copy);
buf[copy] = '\0';
}
void concat(const String &s) { *this += s; }
void concat(const char *s) { *this += s; }
long toInt() const { return _buf ? atol(_buf) : 0; }
float toFloat() const { return _buf ? (float)atof(_buf) : 0.0f; }
double toDouble() const { return _buf ? atof(_buf) : 0.0; }
private:
char *_buf;
unsigned int _len;
unsigned int _cap;
void assign(const char *s, unsigned int n)
{
if (n >= _cap)
reserve(n + 1);
if (_buf) {
memcpy(_buf, s, n);
_buf[n] = 0;
_len = n;
}
}
void concat(const char *s, unsigned int n)
{
if (!s || n == 0)
return;
unsigned newlen = _len + n;
if (newlen >= _cap)
reserve(newlen + 1);
if (_buf) {
memcpy(_buf + _len, s, n);
_len = newlen;
_buf[_len] = 0;
}
}
void reserve(unsigned int n)
{
char *b = (char *)realloc(_buf, n);
if (b) {
_buf = b;
_cap = n;
}
}
};
inline String operator+(const char *lhs, const String &rhs)
{
return String(lhs) + rhs;
}
inline String operator+(char lhs, const String &rhs)
{
return String(lhs) + rhs;
}
// ── Print inline definitions that need String ────────────────────────────────
inline size_t Print::print(const String &s)
{
return write((const uint8_t *)s.c_str(), s.length());
}
inline size_t Print::println(const String &s)
{
size_t r = print(s);
return r + println();
}
// ── Stream inline definitions that need String ───────────────────────────────
inline String Stream::readString()
{
return String();
}
inline String Stream::readStringUntil(char)
{
return String();
}
// ── HardwareSerial ───────────────────────────────────────────────────────────
class HardwareSerial : public Stream
{
public:
void begin(unsigned long) {}
void begin(unsigned long, uint16_t) {}
void end() {}
void setPins(int rx, int tx) {}
void setPinout(int tx, int rx) {}
void setFIFOSize(size_t) {}
void setRxBufferSize(size_t) {}
void begin(unsigned long baud, uint32_t config, int8_t rx = -1, int8_t tx = -1, bool invert = false) {}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
size_t write(uint8_t c) override;
size_t write(const uint8_t *buf, size_t n) override;
using Print::write; // un-hide base class write(const char*)
size_t readBytes(uint8_t *buf, size_t len) { return 0; }
size_t readBytes(char *buf, size_t len) { return 0; }
operator bool() const { return true; }
void flush() override {}
String readString() { return String(); }
String readStringUntil(char) { return String(); }
};
// Uart — nRF52 BSP alias for HardwareSerial (used by GPS.h when ARCH_NRF52)
typedef HardwareSerial Uart;
extern HardwareSerial Serial;
extern HardwareSerial Serial1;
extern HardwareSerial Serial2;
// ── map() utility ────────────────────────────────────────────────────────────
static inline long map(long x, long in_min, long in_max, long out_min, long out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
// ── shiftIn / shiftOut stubs ─────────────────────────────────────────────────
static inline uint8_t shiftIn(uint8_t, uint8_t, uint8_t)
{
return 0;
}
static inline void shiftOut(uint8_t, uint8_t, uint8_t, uint8_t) {}
// ── tone / noTone stubs ──────────────────────────────────────────────────────
static inline void tone(uint8_t, unsigned int, unsigned long = 0) {}
static inline void noTone(uint8_t) {}
// ── pulseIn stub ─────────────────────────────────────────────────────────────
static inline unsigned long pulseIn(uint8_t, uint8_t, unsigned long = 1000000UL)
{
return 0;
}
// ── strdup / stpcpy — POSIX extensions not in Zephyr newlib ─────────────────
#ifndef strdup
static inline char *strdup(const char *s)
{
size_t n = strlen(s) + 1;
char *d = (char *)malloc(n);
if (d)
memcpy(d, s, n);
return d;
}
#endif
#ifndef stpcpy
static inline char *stpcpy(char *dst, const char *src)
{
while ((*dst++ = *src++) != '\0') {
}
return dst - 1;
}
#endif
// ── strnstr — BSD extension not in Zephyr libc; defined in meshUtils.cpp ─────
// Declare here so callers (GPS.cpp etc.) don't need ARCH_PORTDUINO.
#ifndef STRNSTR
#define STRNSTR
char *strnstr(const char *s, const char *find, size_t slen);
#endif
// ── strlcpy — BSD extension; implementation in nrf54l15_arduino.cpp ──────────
#ifndef HAVE_STRLCPY
#define HAVE_STRLCPY
#ifdef __cplusplus
extern "C" {
#endif
size_t strlcpy(char *dst, const char *src, size_t size);
#ifdef __cplusplus
}
#endif
#endif
// ── setenv / getenv / tzset — Zephyr stubs for timezone support ──────────────
#include <stdlib.h>
static inline int setenv(const char *, const char *, int)
{
return 0;
}
static inline void tzset(void) {}
// ── dbgHeapFree / dbgHeapTotal — nRF52 BSP heap diagnostics ─────────────────
// Used by memGet.cpp when ARCH_NRF52 is defined. Return 0 for Phase 2.
static inline uint32_t dbgHeapFree(void)
{
return 0;
}
static inline uint32_t dbgHeapTotal(void)
{
return 0;
}
// ── WCharacter helpers ───────────────────────────────────────────────────────
static inline bool isAlpha(char c)
{
return std::isalpha((unsigned char)c) != 0;
}
static inline bool isAlphaNumeric(char c)
{
return std::isalnum((unsigned char)c) != 0;
}
static inline bool isDigit(char c)
{
return std::isdigit((unsigned char)c) != 0;
}
static inline bool isSpace(char c)
{
return std::isspace((unsigned char)c) != 0;
}
static inline bool isUpperCase(char c)
{
return std::isupper((unsigned char)c) != 0;
}
static inline bool isLowerCase(char c)
{
return std::islower((unsigned char)c) != 0;
}
static inline char toUpperCase(char c)
{
return (char)std::toupper((unsigned char)c);
}
static inline char toLowerCase(char c)
{
return (char)std::tolower((unsigned char)c);
}
#else /* C only */
#ifndef min
#define min(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef max
#define max(a, b) ((a) > (b) ? (a) : (b))
#endif
#ifndef abs
#define abs(x) ((x) >= 0 ? (x) : -(x))
#endif
#define constrain(x, l, h) ((x) < (l) ? (l) : ((x) > (h) ? (h) : (x)))
#define round(x) ((x) >= 0 ? (long)((x) + 0.5) : (long)((x)-0.5))
#endif /* __cplusplus */
#endif /* Arduino_h */
+34
View File
@@ -0,0 +1,34 @@
// IPAddress.h — stub for nRF54L15/Zephyr
// MQTT.cpp includes this for IPv4 address representation.
// Phase 2: compile-only stub.
#pragma once
#include <stdint.h>
class IPAddress
{
public:
IPAddress() : _addr(0) {}
explicit IPAddress(uint32_t addr) : _addr(addr) {}
IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
: _addr(((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24))
{
}
uint8_t operator[](int i) const { return reinterpret_cast<const uint8_t *>(&_addr)[i]; }
operator uint32_t() const { return _addr; }
bool operator==(const IPAddress &o) const { return _addr == o._addr; }
bool operator!=(const IPAddress &o) const { return _addr != o._addr; }
bool fromString(const char *addr)
{
unsigned a, b, c, d;
if (sscanf(addr, "%u.%u.%u.%u", &a, &b, &c, &d) == 4 && a <= 255 && b <= 255 && c <= 255 && d <= 255) {
_addr = a | (b << 8) | (c << 16) | (d << 24);
return true;
}
return false;
}
private:
uint32_t _addr;
};
@@ -0,0 +1,274 @@
// InternalFileSystem.cpp — Zephyr LittleFS backend for nRF54L15
//
// Implements Adafruit_LittleFS_Namespace used by FSCommon.h/cpp.
// Storage: 36 KB storage_partition in nRF54L15 internal RRAM (defined in
// zephyr/dts/nordic/nrf54l15_partition.dtsi, included by the board DTS).
#include "InternalFileSystem.h"
#include "configuration.h"
#include <zephyr/fs/fs.h>
#include <zephyr/fs/littlefs.h>
#include <zephyr/storage/flash_map.h>
using namespace Adafruit_LittleFS_Namespace;
// ── LittleFS mount ────────────────────────────────────────────────────────
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(nrf54l15_lfs_data);
static struct fs_mount_t _lfs_mnt = {
.type = FS_LITTLEFS,
.mnt_point = NRF54L15_FS_MOUNT,
.fs_data = &nrf54l15_lfs_data,
.storage_dev = (void *)(uintptr_t)FIXED_PARTITION_ID(storage_partition),
.flags = 0,
};
// ── Global singleton ──────────────────────────────────────────────────────
Adafruit_LittleFS_Namespace::InternalFileSystem InternalFS;
// ── Path helpers ──────────────────────────────────────────────────────────
void InternalFileSystem::toabs(const char *rel, char *abs, size_t abssz)
{
// Root "/" maps to the mount point itself (no trailing slash)
if (rel[0] == '/' && rel[1] == '\0') {
strncpy(abs, NRF54L15_FS_MOUNT, abssz - 1);
abs[abssz - 1] = '\0';
} else if (rel[0] == '/') {
snprintf(abs, abssz, "%s%s", NRF54L15_FS_MOUNT, rel);
} else {
snprintf(abs, abssz, "%s/%s", NRF54L15_FS_MOUNT, rel);
}
}
// Strip mount-point prefix to get the FS-root-relative path ("/prefs/...").
static void torel(const char *abs, char *rel, size_t relsz)
{
const char *mp = NRF54L15_FS_MOUNT;
size_t mplen = strlen(mp);
if (strncmp(abs, mp, mplen) == 0) {
const char *suffix = abs + mplen;
if (suffix[0] == '\0') {
strncpy(rel, "/", relsz);
} else {
strncpy(rel, suffix, relsz - 1);
rel[relsz - 1] = '\0';
}
} else {
strncpy(rel, abs, relsz - 1);
rel[relsz - 1] = '\0';
}
}
// ── InternalFileSystem methods ────────────────────────────────────────────
bool InternalFileSystem::begin()
{
if (_mounted)
return true;
int rc = fs_mount(&_lfs_mnt);
if (rc == 0) {
_mounted = true;
return true;
}
// Mount failed: attempt to format (creates a fresh LittleFS)
LOG_WARN("LittleFS mount failed (%d), formatting storage partition...", rc);
int fmt_rc = fs_mkfs(FS_LITTLEFS, (uintptr_t)FIXED_PARTITION_ID(storage_partition), NULL, 0);
if (fmt_rc != 0) {
LOG_ERROR("LittleFS format failed (%d)", fmt_rc);
return false;
}
rc = fs_mount(&_lfs_mnt);
if (rc == 0) {
_mounted = true;
return true;
}
LOG_ERROR("LittleFS mount failed after format (%d)", rc);
return false;
}
File InternalFileSystem::open(const char *path, const char *mode)
{
if (!_mounted)
return File();
char abs[NRF54L15_FS_PATHLEN];
toabs(path, abs, sizeof(abs));
auto s = std::make_shared<NRF54L15FileState>();
if (!s)
return File();
strncpy(s->fullpath, abs, sizeof(s->fullpath) - 1);
torel(abs, s->relpath, sizeof(s->relpath));
// Check whether the path is a directory
struct fs_dirent entry;
int stat_rc = fs_stat(abs, &entry);
if (stat_rc == 0 && entry.type == FS_DIR_ENTRY_DIR) {
s->is_dir = true;
if (fs_opendir(&s->dir, abs) == 0) {
s->valid = true;
return File(s);
}
return File();
}
// Open as a regular file
fs_mode_t flags;
if (strcmp(mode, FILE_O_WRITE) == 0) {
// Truncate on write — unlink first to ensure a clean start
fs_unlink(abs);
flags = FS_O_WRITE | FS_O_CREATE;
} else {
flags = FS_O_READ;
}
if (fs_open(&s->file, abs, flags) == 0) {
s->is_dir = false;
s->valid = true;
return File(s);
}
return File();
}
bool InternalFileSystem::exists(const char *path)
{
if (!_mounted)
return false;
char abs[NRF54L15_FS_PATHLEN];
toabs(path, abs, sizeof(abs));
struct fs_dirent entry;
return fs_stat(abs, &entry) == 0;
}
bool InternalFileSystem::remove(const char *path)
{
if (!_mounted)
return false;
char abs[NRF54L15_FS_PATHLEN];
toabs(path, abs, sizeof(abs));
return fs_unlink(abs) == 0;
}
bool InternalFileSystem::rename(const char *from, const char *to)
{
if (!_mounted)
return false;
char absfrom[NRF54L15_FS_PATHLEN], absto[NRF54L15_FS_PATHLEN];
toabs(from, absfrom, sizeof(absfrom));
toabs(to, absto, sizeof(absto));
return fs_rename(absfrom, absto) == 0;
}
bool InternalFileSystem::mkdir(const char *path)
{
if (!_mounted)
return false;
char abs[NRF54L15_FS_PATHLEN];
toabs(path, abs, sizeof(abs));
int rc = fs_mkdir(abs);
return rc == 0 || rc == -EEXIST;
}
bool InternalFileSystem::rmdir(const char *path)
{
if (!_mounted)
return false;
char abs[NRF54L15_FS_PATHLEN];
toabs(path, abs, sizeof(abs));
return fs_unlink(abs) == 0;
}
bool InternalFileSystem::rmdir_r(const char *path)
{
if (!_mounted)
return false;
char abs[NRF54L15_FS_PATHLEN];
toabs(path, abs, sizeof(abs));
struct fs_dir_t dir;
fs_dir_t_init(&dir);
if (fs_opendir(&dir, abs) != 0) {
// Not a directory — try to delete as file
return fs_unlink(abs) == 0;
}
struct fs_dirent entry;
char child[NRF54L15_FS_PATHLEN];
while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') {
snprintf(child, sizeof(child), "%s/%s", abs, entry.name);
if (entry.type == FS_DIR_ENTRY_DIR) {
// Recurse: pass the absolute path stripped of mount prefix
char childrel[NRF54L15_FS_PATHLEN];
torel(child, childrel, sizeof(childrel));
rmdir_r(childrel);
} else {
fs_unlink(child);
}
}
fs_closedir(&dir);
return fs_unlink(abs) == 0;
}
bool InternalFileSystem::format()
{
if (_mounted) {
fs_unmount(&_lfs_mnt);
_mounted = false;
}
int rc = fs_mkfs(FS_LITTLEFS, (uintptr_t)FIXED_PARTITION_ID(storage_partition), NULL, 0);
if (rc != 0) {
LOG_ERROR("LittleFS format failed (%d)", rc);
return false;
}
return begin();
}
// ── File::openNextFile ────────────────────────────────────────────────────
// Defined here because it accesses Zephyr fs_readdir/fs_open APIs.
File File::openNextFile()
{
if (!_s || !_s->valid || !_s->is_dir)
return File();
struct fs_dirent entry;
if (fs_readdir(&_s->dir, &entry) != 0)
return File();
if (entry.name[0] == '\0')
return File(); // end of directory
char childabs[NRF54L15_FS_PATHLEN];
snprintf(childabs, sizeof(childabs), "%s/%s", _s->fullpath, entry.name);
auto s = std::make_shared<NRF54L15FileState>();
if (!s)
return File();
strncpy(s->fullpath, childabs, sizeof(s->fullpath) - 1);
torel(childabs, s->relpath, sizeof(s->relpath));
if (entry.type == FS_DIR_ENTRY_DIR) {
s->is_dir = true;
if (fs_opendir(&s->dir, childabs) == 0) {
s->valid = true;
return File(s);
}
} else {
s->is_dir = false;
if (fs_open(&s->file, childabs, FS_O_READ) == 0) {
s->valid = true;
return File(s);
}
}
return File();
}
+212
View File
@@ -0,0 +1,212 @@
// InternalFileSystem.h — Zephyr LittleFS backend for nRF54L15
//
// Implements the Adafruit InternalFileSystem API subset used by Meshtastic,
// backed by Zephyr's fs/littlefs on the storage_partition of the nRF54L15's
// internal RRAM. Partition size is taken from the DTS at compile time via
// FIXED_PARTITION_SIZE(storage_partition) — the DK overlay currently maps
// ~700 KB into slot1 (see zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay).
//
// Mount point: /lfs
// All paths passed to open/exists/mkdir etc. are relative to the FS root
// (e.g. "/prefs/config.proto") and are prepended with "/lfs" internally.
//
// File objects are copyable via std::shared_ptr<NRF54L15FileState>.
// The underlying Zephyr handle is closed when the last copy is destroyed.
#pragma once
#include <memory>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <zephyr/fs/fs.h>
#include <zephyr/storage/flash_map.h>
#ifndef FILE_O_READ
#define FILE_O_READ "r"
#define FILE_O_WRITE "w"
#endif
#define NRF54L15_FS_MOUNT "/lfs"
#define NRF54L15_FS_PATHLEN 256
namespace Adafruit_LittleFS_Namespace
{
class InternalFileSystem; // forward
// ── Internal file/dir state ───────────────────────────────────────────────
struct NRF54L15FileState {
bool valid = false;
bool is_dir = false;
// Absolute Zephyr path, e.g. "/lfs/prefs/config.proto"
char fullpath[NRF54L15_FS_PATHLEN] = {0};
// Path from FS root, e.g. "/prefs/config.proto" (returned by name())
char relpath[NRF54L15_FS_PATHLEN] = {0};
struct fs_file_t file;
struct fs_dir_t dir;
NRF54L15FileState()
{
fs_file_t_init(&file);
fs_dir_t_init(&dir);
}
~NRF54L15FileState()
{
if (valid) {
if (is_dir)
fs_closedir(&dir);
else
fs_close(&file);
valid = false;
}
}
};
// ── File ─────────────────────────────────────────────────────────────────
class File
{
public:
File() = default;
explicit File(InternalFileSystem &) {} // nRF52 compat constructor
explicit operator bool() const { return _s && _s->valid; }
int read(void *buf, uint16_t nbyte)
{
if (!_s || !_s->valid || _s->is_dir)
return -1;
ssize_t n = fs_read(&_s->file, buf, nbyte);
return n < 0 ? -1 : (int)n;
}
int read()
{
uint8_t b;
return read(&b, 1) == 1 ? (int)b : -1;
}
size_t write(const uint8_t *buf, size_t len)
{
if (!_s || !_s->valid || _s->is_dir)
return 0;
ssize_t n = fs_write(&_s->file, buf, len);
return n < 0 ? 0 : (size_t)n;
}
size_t write(uint8_t b) { return write(&b, 1); }
void flush()
{
if (_s && _s->valid && !_s->is_dir)
fs_sync(&_s->file);
}
void close() { _s.reset(); }
size_t size()
{
if (!_s || !_s->valid || _s->is_dir)
return 0;
struct fs_dirent entry;
if (fs_stat(_s->fullpath, &entry) == 0)
return (size_t)entry.size;
return 0;
}
bool isDirectory() { return _s && _s->valid && _s->is_dir; }
// Returns path from FS root, e.g. "/prefs/config.proto"
const char *name() { return _s ? _s->relpath : ""; }
// Returns the next entry in a directory. Modifies the dir stream in _s.
File openNextFile();
void rewindDirectory()
{
if (!_s || !_s->valid || !_s->is_dir)
return;
// Zephyr has no rewinddir(); close + reopen the same handle. Skipping
// the close would leak the LittleFS dir state and the next openNextFile
// could return stale entries on some Zephyr versions.
fs_closedir(&_s->dir);
fs_dir_t_init(&_s->dir);
if (fs_opendir(&_s->dir, _s->fullpath) != 0) {
_s->valid = false;
}
}
bool seek(uint32_t pos)
{
if (!_s || !_s->valid || _s->is_dir)
return false;
return fs_seek(&_s->file, (off_t)pos, FS_SEEK_SET) == 0;
}
int available()
{
if (!_s || !_s->valid || _s->is_dir)
return 0;
off_t pos = fs_tell(&_s->file);
if (pos < 0)
return 0;
struct fs_dirent entry;
if (fs_stat(_s->fullpath, &entry) != 0)
return 0;
long rem = (long)entry.size - (long)pos;
return rem > 0 ? (int)rem : 0;
}
int peek() { return -1; }
// Internal: constructed by InternalFileSystem and openNextFile()
explicit File(std::shared_ptr<NRF54L15FileState> s) : _s(std::move(s)) {}
private:
std::shared_ptr<NRF54L15FileState> _s;
};
// ── InternalFileSystem ────────────────────────────────────────────────────
class InternalFileSystem
{
public:
bool begin();
File open(const char *path, const char *mode);
bool exists(const char *path);
bool remove(const char *path);
bool rename(const char *from, const char *to);
bool mkdir(const char *path);
bool rmdir(const char *path);
bool rmdir_r(const char *path); // recursive delete (used by FSCommon rmDir)
uint32_t usedBytes()
{
struct fs_statvfs st = {};
if (fs_statvfs(NRF54L15_FS_MOUNT, &st) != 0)
return 0;
// Zephyr returns block counts; convert to bytes. f_frsize is the
// fundamental fragment size (LittleFS reports it equal to the block
// size). used = (total - free) * frag_size.
if (st.f_blocks <= st.f_bfree)
return 0;
return (uint32_t)((st.f_blocks - st.f_bfree) * st.f_frsize);
}
uint32_t totalBytes() { return (uint32_t)FIXED_PARTITION_SIZE(storage_partition); }
bool format();
// Convert a FS-root-relative path to an absolute Zephyr path.
static void toabs(const char *rel, char *abs, size_t abssz);
private:
bool _mounted = false;
};
} // namespace Adafruit_LittleFS_Namespace
extern Adafruit_LittleFS_Namespace::InternalFileSystem InternalFS;
+18
View File
@@ -0,0 +1,18 @@
// NRF52Bluetooth.h — stub for nRF54L15/Zephyr
// main.h includes this when ARCH_NRF52 is defined.
// Bluetooth is excluded (MESHTASTIC_EXCLUDE_BLUETOOTH=1); this satisfies the
// include chain without pulling in the nRF52 Bluefruit SDK.
#pragma once
class NRF52Bluetooth
{
public:
void setup() {}
void shutdown() {}
void startDisabled() {}
void resumeAdvertising() {}
void clearBonds() {}
bool isConnected() { return false; }
int getRssi() { return 0; }
void sendLog(const uint8_t *, size_t) {}
};
+805
View File
@@ -0,0 +1,805 @@
// NRF54L15Bluetooth.cpp — Zephyr BLE GATT peripheral for Meshtastic nRF54L15
//
// GATT profile (identical UUIDs to the nRF52 / NimBLE implementations):
// Service: 6ba1b218-15a8-461f-9fa8-5dcae273eafd
// fromNum: ed9da18c-a800-4f66-a670-aa7547e34453 READ | NOTIFY
// fromRadio: 2c55e69e-4993-11ed-b878-0242ac120002 READ
// toRadio: f75c76d2-129e-4dad-a1dd-7866124401e7 WRITE
// logRadio: 5a3d6e49-06e6-4423-9944-e9de8cdf9547 READ | NOTIFY | INDICATE
//
// Threading model:
// - BT RX thread: connected_cb / disconnected_cb / GATT read_/write_
// callbacks
// - Meshtastic OSThread scheduler (cooperative, main thread):
// BleDeferredThread
// polls pendingToRadio and runs the zombie-connection watchdog every 100 ms
// - PhoneAPI::onNowHasData: sends fromNum notify synchronously from whichever
// thread pushed the packet (bt_gatt_notify is thread-safe in Zephyr)
// - active_conn protected by ble_mutex where needed
#include "NRF54L15Bluetooth.h"
#include "BluetoothCommon.h"
#include "BluetoothStatus.h"
#include "PowerFSM.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include "main.h"
#include "mesh/PhoneAPI.h"
#include "mesh/mesh-pb-constants.h"
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/reboot.h>
// ── UUID definitions (little-endian per Bluetooth spec)
// ─────────────────────── Syntax: replace hyphens with commas, prefix 0x —
// matches BT_UUID_128_ENCODE doc.
#define MESH_SVC_UUID_VAL BT_UUID_128_ENCODE(0x6ba1b218, 0x15a8, 0x461f, 0x9fa8, 0x5dcae273eafd)
#define FROMNUM_UUID_VAL BT_UUID_128_ENCODE(0xed9da18c, 0xa800, 0x4f66, 0xa670, 0xaa7547e34453)
#define FROMRADIO_UUID_VAL BT_UUID_128_ENCODE(0x2c55e69e, 0x4993, 0x11ed, 0xb878, 0x0242ac120002)
#define TORADIO_UUID_VAL BT_UUID_128_ENCODE(0xf75c76d2, 0x129e, 0x4dad, 0xa1dd, 0x7866124401e7)
#define LOGRADIO_UUID_VAL BT_UUID_128_ENCODE(0x5a3d6e49, 0x06e6, 0x4423, 0x9944, 0xe9de8cdf9547)
static const struct bt_uuid_128 mesh_svc_uuid = BT_UUID_INIT_128(MESH_SVC_UUID_VAL);
static const struct bt_uuid_128 fromnum_uuid = BT_UUID_INIT_128(FROMNUM_UUID_VAL);
static const struct bt_uuid_128 fromradio_uuid = BT_UUID_INIT_128(FROMRADIO_UUID_VAL);
static const struct bt_uuid_128 toradio_uuid = BT_UUID_INIT_128(TORADIO_UUID_VAL);
static const struct bt_uuid_128 logradio_uuid = BT_UUID_INIT_128(LOGRADIO_UUID_VAL);
// ── Module state ─────────────────────────────────────────────────────────────
static struct bt_conn *active_conn = nullptr;
static K_MUTEX_DEFINE(ble_mutex);
// Take a reference to active_conn under ble_mutex. Returns nullptr if there is
// no active connection. Caller MUST bt_conn_unref() when done.
//
// Reading `active_conn` outside this lock races with disconnected_cb which can
// unref + null it on the BT RX thread — touching the freed pointer (even just
// to bt_conn_ref it) is a use-after-free.
static struct bt_conn *acquire_active_conn()
{
struct bt_conn *conn = nullptr;
k_mutex_lock(&ble_mutex, K_FOREVER);
if (active_conn) {
conn = bt_conn_ref(active_conn);
}
k_mutex_unlock(&ble_mutex);
return conn;
}
static bool bt_initialized = false; // bt_enable() called at most once
static bool ble_enabled = false; // set by setup(), cleared by shutdown()
// Forward declarations — BT_GATT_SERVICE_DEFINE(mesh_svc, ...) is below, but
// read_fromradio() (defined earlier) needs to reference the service to notify
// on fromNum after each non-empty read.
#define FROMNUM_ATTR_IDX 2
#define LOGRADIO_ATTR_IDX 9
extern const struct bt_gatt_service_static mesh_svc;
static void start_advertising(); // forward declaration (defined in advertising
// section below)
// Work item for advertising restart after disconnect.
//
// disconnected_cb runs on the BT RX thread (the same thread that processes
// HCI Command Complete events). Calling bt_le_adv_start() →
// bt_hci_cmd_send_sync() directly from that thread deadlocks: the thread blocks
// on k_sem_take waiting for Command Complete, but it is the very thread that
// would process it. After 10 s the host panics with "Controller unresponsive,
// opcode 0x2006 timeout".
//
// Fix: submit a k_work item. The system workqueue runs bt_adv_restart_work_fn
// on its own thread → no deadlock.
static struct k_work adv_restart_work;
static void adv_restart_work_fn(struct k_work *work)
{
if (ble_enabled) {
start_advertising();
}
}
// CCC state: 0=off, BT_GATT_CCC_NOTIFY=notify, BT_GATT_CCC_INDICATE=indicate
static uint16_t fromnum_ccc_val = 0;
static uint16_t logradio_ccc_val = 0;
// Scratch buffers — only one BLE operation at a time
static uint8_t fromRadioBytes[meshtastic_FromRadio_size];
static size_t fromRadioLen = 0;
static uint8_t toRadioBytes[meshtastic_ToRadio_size];
static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
static uint32_t fromNumValue = 0;
// Deferred ToRadio processing
//
// write_toradio() runs on the BT RX workqueue thread (6 KB stack). Calling
// phoneAPI->handleToRadio() directly triggers handleStartConfig →
// getFiles("/", 10) → nanopb encode, which overflows the stack on the exact
// "Client wants config" write. Instead we copy the payload into a pending
// buffer under a mutex and let BleDeferredThread (running on the Meshtastic
// OSThread scheduler, 24 KB stack) do the actual call outside the lock.
//
// The mutex makes the producer/consumer handoff race-free — producer may
// overwrite a pending buffer the consumer hasn't read yet (dropped packet),
// but partial reads / torn writes are impossible.
K_MUTEX_DEFINE(pendingToRadioMutex);
static uint8_t pendingToRadioBuf[MAX_TO_FROM_RADIO_SIZE];
static size_t pendingToRadioLen = 0;
static bool pendingToRadio = false;
// Zombie-connection watchdog state.
//
// The nRF54L15 Zephyr 4.2.1 SW-LL occasionally fails to forward an
// LE Disconnection Complete event to the host: when iOS tears down the link
// (either explicitly by the user or via supervision timeout), the LL layer
// drops the connection but disconnected_cb never fires, active_conn stays
// non-null and advertising never restarts — the device vanishes from scans
// until power cycle. Track the connected timestamp and the last time we
// observed ATT traffic; a long ATT idle on an "active" connection means we
// are zombied. A cold reboot is the only path that reliably recovers (any
// bt_hci_cmd_send_sync after this state, e.g. bt_le_adv_start or
// bt_conn_disconnect, hangs in k_sem_take and later panics with "Controller
// unresponsive, opcode 0x2006 timeout").
static uint32_t connect_time_ms = 0;
static uint32_t last_att_time_ms = 0;
// ── BluetoothPhoneAPI
// ─────────────────────────────────────────────────────────
class BluetoothPhoneAPI : public PhoneAPI
{
virtual void onNowHasData(uint32_t fromRadioNum) override;
virtual bool checkIsConnected() override;
public:
BluetoothPhoneAPI() { api_type = TYPE_BLE; }
};
static BluetoothPhoneAPI *phoneAPI = nullptr;
// ── CCC change callbacks
// ──────────────────────────────────────────────────────
static void fromnum_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
fromnum_ccc_val = value;
LOG_INFO("BLE fromNum CCC: %u", value);
}
static void logradio_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
logradio_ccc_val = value;
LOG_INFO("BLE logRadio CCC: %u", value);
}
// ── GATT attribute callbacks
// ──────────────────────────────────────────────────
static ssize_t read_fromnum(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
{
LOG_INFO("GATT read_fromnum: fromNum=%u offset=%u", fromNumValue, offset);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &fromNumValue, sizeof(fromNumValue));
}
static ssize_t read_fromradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
{
if (offset == 0) {
// First chunk: pull the next packet from the queue.
// Subsequent chunks (offset > 0) are ATT_READ_BLOB continuations of the
// same value and must reuse fromRadioBytes untouched.
fromRadioLen = phoneAPI ? phoneAPI->getFromRadio(fromRadioBytes) : 0;
LOG_DEBUG("GATT read_fromradio len=%u", (unsigned)fromRadioLen);
}
last_att_time_ms = k_uptime_get_32();
return bt_gatt_attr_read(conn, attr, buf, len, offset, fromRadioBytes, fromRadioLen);
}
static ssize_t read_logradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
{
// logRadio is write-only from the device side (notify/indicate).
// Return an empty read so GATT discovery doesn't fail with NOT_PERMITTED.
return bt_gatt_attr_read(conn, attr, buf, len, offset, NULL, 0);
}
static ssize_t write_toradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len,
uint16_t offset, uint8_t flags)
{
// Writes >MTU-3 arrive here with offset=0 and
// flags=BT_GATT_WRITE_FLAG_EXECUTE after Zephyr reassembles the ATT Prepare
// Write fragments (CONFIG_BT_ATT_PREPARE_COUNT>0). Single writes arrive with
// flags=0.
LOG_DEBUG("GATT write_toradio len=%u flags=0x%x", len, flags);
if (offset != 0) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
// Reject any write that won't fit in the dedup buffer (lastToRadio) or the
// pending handoff buffer (pendingToRadioBuf), both sized
// MAX_TO_FROM_RADIO_SIZE. Returning success while silently dropping a
// payload would let the phone believe a config write was applied.
if (len > sizeof(toRadioBytes) || len > MAX_TO_FROM_RADIO_SIZE) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
// Deduplicate — drop packet if identical to the last one we processed
if (memcmp(lastToRadio, buf, len) != 0) {
memcpy(lastToRadio, buf, len);
if (len < MAX_TO_FROM_RADIO_SIZE) {
memset(lastToRadio + len, 0, MAX_TO_FROM_RADIO_SIZE - len);
}
// Defer handleToRadio() to BleDeferredThread (24 KB stack).
// Running it here on bt_workq (6 KB) overflows during handleStartConfig.
// Always overwrite pending — we already dedup'd above via lastToRadio,
// so any new write here is genuinely new data that must be delivered.
k_mutex_lock(&pendingToRadioMutex, K_FOREVER);
memcpy(pendingToRadioBuf, buf, len);
pendingToRadioLen = len;
pendingToRadio = true;
k_mutex_unlock(&pendingToRadioMutex);
}
last_att_time_ms = k_uptime_get_32();
return (ssize_t)len;
}
// ── GATT service definition (static, linked at compile time)
// ──────────────────
//
// Attribute indices (0-based):
// [0] Primary Service declaration
// [1] fromNum characteristic declaration
// [2] fromNum value ← notify target (FROMNUM_ATTR_IDX)
// [3] fromNum CCC descriptor
// [4] fromRadio characteristic declaration
// [5] fromRadio value
// [6] toRadio characteristic declaration
// [7] toRadio value
// [8] logRadio characteristic declaration
// [9] logRadio value ← notify target (LOGRADIO_ATTR_IDX)
// [10] logRadio CCC descriptor
// All user characteristics require authenticated encryption (MITM passkey)
// before the client can read/write. This mirrors the nrf52
// SECMODE_ENC_WITH_MITM service permission. The stack returns "Insufficient
// Authentication" on the first access attempt, prompting the client to pair
// with the configured PIN.
#define MESH_PERM_READ (BT_GATT_PERM_READ | BT_GATT_PERM_READ_AUTHEN)
#define MESH_PERM_WRITE (BT_GATT_PERM_WRITE | BT_GATT_PERM_WRITE_AUTHEN)
BT_GATT_SERVICE_DEFINE(mesh_svc, BT_GATT_PRIMARY_SERVICE(&mesh_svc_uuid.uuid),
// fromNum: READ | NOTIFY — packet-counter triggers phone to read fromRadio
BT_GATT_CHARACTERISTIC(&fromnum_uuid.uuid, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, MESH_PERM_READ,
read_fromnum, NULL, &fromNumValue),
BT_GATT_CCC(fromnum_ccc_changed, MESH_PERM_READ | MESH_PERM_WRITE),
// fromRadio: READ — phone polls this after receiving a fromNum notification
BT_GATT_CHARACTERISTIC(&fromradio_uuid.uuid, BT_GATT_CHRC_READ, MESH_PERM_READ, read_fromradio, NULL,
NULL),
// toRadio: WRITE — phone sends protobuf packets to the device
BT_GATT_CHARACTERISTIC(&toradio_uuid.uuid, BT_GATT_CHRC_WRITE, MESH_PERM_WRITE, NULL, write_toradio, NULL),
// logRadio: READ | NOTIFY | INDICATE — log stream to phone when connected
BT_GATT_CHARACTERISTIC(&logradio_uuid.uuid,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_INDICATE, MESH_PERM_READ,
read_logradio, NULL, NULL),
BT_GATT_CCC(logradio_ccc_changed, MESH_PERM_READ | MESH_PERM_WRITE), );
// ── Advertising
// ───────────────────────────────────────────────────────────────
//
// Use legacy advertising (bt_le_adv_start / HCI 0x2006 path).
//
// History: we previously used bt_le_ext_adv_create (true extended advertising)
// because bt_le_adv_start() with CONFIG_BT_EXT_ADV=y was translated internally
// to the extended HCI path with LEGACY-bit (0x2036), which produced
// non-connectable PDUs on the nRF54L15 SW-LL. The true extended path
// (0x203x, AUX_ADV_IND) was connectable but caused two problems:
// 1. iOS CoreBluetooth does not reliably complete GATT after connecting via
// extended advertising (zero ATT PDUs observed in all test sessions).
// 2. After each connection the controller auto-stops the advertising set, and
// the subsequent bt_le_ext_adv_delete() sends LE Remove Advertising Set
// (0x203c) which times out → kernel oops at hci_core.c:506.
//
// With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI commands — the same
// path Nordic NCS uses in all nRF54L15 examples (peripheral_uart,
// peripheral_lbs) and which is universally iOS-compatible. The legacy data
// payload is 31 bytes:
// FLAGS (3B) + UUID128 (18B) = 21B in adv; NAME in scan-response (17B).
static void start_advertising()
{
// IMPORTANT: BT_DATA_BYTES() uses C99 compound literals that GCC C++ treats
// as temporaries; with -Os the compiler may elide writes, leaving stack
// uninitialized. Use static const arrays for stable data (flags, UUID)
// and a runtime pointer for the dynamic device name.
static const uint8_t adv_flags_val[] = {BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR};
static const uint8_t adv_uuid128_val[] = {MESH_SVC_UUID_VAL};
const char *name = bt_get_name();
size_t full_name_len = strlen(name);
// Legacy scan-response payload is 31 bytes total. Each AD entry costs 2
// bytes (length + type), leaving 29 bytes for the name. With
// CONFIG_BT_DEVICE_NAME_MAX=32 the name can exceed that — truncate and
// mark as SHORTENED so bt_le_adv_start() doesn't reject the payload.
constexpr size_t LEGACY_SCAN_RSP_NAME_MAX = 31 - 2;
bool name_shortened = full_name_len > LEGACY_SCAN_RSP_NAME_MAX;
uint8_t name_len = (uint8_t)(name_shortened ? LEGACY_SCAN_RSP_NAME_MAX : full_name_len);
// Primary advertising data: FLAGS + Meshtastic service UUID128 (21 bytes
// total)
struct bt_data ad[] = {
{BT_DATA_FLAGS, sizeof(adv_flags_val), adv_flags_val},
{BT_DATA_UUID128_ALL, sizeof(adv_uuid128_val), adv_uuid128_val},
};
// Scan response: device name (discovered after scan request)
struct bt_data sd[] = {
{(uint8_t)(name_shortened ? BT_DATA_NAME_SHORTENED : BT_DATA_NAME_COMPLETE), name_len, (const uint8_t *)name},
};
// BT_LE_ADV_OPT_CONN = connectable legacy ADV_IND + stops after first
// connection (replaces deprecated
// CONNECTABLE|ONE_TIME in Zephyr 4.2.1;
// BT_LE_ADV_OPT_CONN = BIT(0)|BIT(1))
// BT_LE_ADV_OPT_USE_IDENTITY = use static random identity address (stable
// across reboots) Advertising restart after disconnect is via
// adv_restart_work (system workqueue) so calling bt_le_adv_start() from the
// BT RX thread context is avoided.
int err = bt_le_adv_start(BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_USE_IDENTITY, BT_GAP_ADV_FAST_INT_MIN_2,
BT_GAP_ADV_FAST_INT_MAX_2, NULL),
ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (err == -EALREADY) {
return;
}
if (err) {
LOG_WARN("BLE adv start failed: %d", err);
} else {
LOG_INFO("BLE advertising as '%s'", bt_get_name());
}
}
static void stop_advertising()
{
bt_le_adv_stop();
}
// ── Connection callbacks
// ──────────────────────────────────────────────────────
static void connected_cb(struct bt_conn *conn, uint8_t err)
{
if (err) {
LOG_WARN("BLE connection failed, err=0x%02x", err);
return;
}
k_mutex_lock(&ble_mutex, K_FOREVER);
active_conn = bt_conn_ref(conn);
k_mutex_unlock(&ble_mutex);
memset(lastToRadio, 0, sizeof(lastToRadio));
connect_time_ms = k_uptime_get_32();
last_att_time_ms = connect_time_ms;
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INFO("BLE connected: %s", addr);
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
bluetoothStatus->updateStatus(&newStatus);
// nRF54L15-DK has no screen — cannot display a PIN to the user.
// Requesting BT_SECURITY_L2 causes the OS to show a pairing dialog that
// the user dismisses, triggering disconnect + advertising restart failure.
// Skip security negotiation; the Meshtastic app works over plain GATT.
// (Security can be re-enabled once a display or NFC OOB path is available.)
}
static void disconnected_cb(struct bt_conn *conn, uint8_t reason)
{
LOG_INFO("BLE disconnected, reason=0x%02x", reason);
k_mutex_lock(&ble_mutex, K_FOREVER);
if (active_conn) {
bt_conn_unref(active_conn);
active_conn = nullptr;
}
k_mutex_unlock(&ble_mutex);
fromnum_ccc_val = 0;
logradio_ccc_val = 0;
connect_time_ms = 0;
last_att_time_ms = 0;
if (phoneAPI) {
phoneAPI->close();
}
memset(lastToRadio, 0, sizeof(lastToRadio));
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus);
// Schedule advertising restart via work queue — NOT from this callback
// directly. disconnected_cb runs on the BT RX thread; calling
// bt_le_adv_start() here would deadlock (see adv_restart_work comment above).
if (ble_enabled) {
k_work_submit(&adv_restart_work);
}
}
#if defined(CONFIG_BT_SMP)
static void security_changed_cb(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
{
if (err == BT_SECURITY_ERR_PIN_OR_KEY_MISSING) {
// Phone has a stale bond (device was wiped/reflashed). Unpair the stale
// entry so the phone re-pairs cleanly on the next connection attempt.
LOG_WARN("BLE stale bond detected (key missing) — unpairing");
bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL);
} else if (err) {
LOG_WARN("BLE security change failed: level=%d err=%d", (int)level, (int)err);
} else {
LOG_INFO("BLE security level %d established", (int)level);
}
}
#endif /* CONFIG_BT_SMP */
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected_cb,
.disconnected = disconnected_cb,
#if defined(CONFIG_BT_SMP)
.security_changed = security_changed_cb,
#endif
};
// ── Pairing / auth callbacks
// ──────────────────────────────────────────────────
#if defined(CONFIG_BT_SMP)
static uint32_t configuredPasskey;
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
char passkey_str[7];
snprintf(passkey_str, sizeof(passkey_str), "%06u", passkey);
configuredPasskey = passkey;
LOG_INFO("BLE pairing PIN: %s", passkey_str);
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
std::string textkey(passkey_str);
meshtastic::BluetoothStatus pairingStatus(textkey);
bluetoothStatus->updateStatus(&pairingStatus);
}
static void auth_cancel(struct bt_conn *conn)
{
LOG_WARN("BLE pairing cancelled");
}
static struct bt_conn_auth_cb auth_cb = {
.passkey_display = auth_passkey_display,
.passkey_entry = NULL,
.cancel = auth_cancel,
};
static void pairing_complete_cb(struct bt_conn *conn, bool bonded)
{
LOG_INFO("BLE pairing complete, bonded=%d", (int)bonded);
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
bluetoothStatus->updateStatus(&newStatus);
}
static void pairing_failed_cb(struct bt_conn *conn, enum bt_security_err reason)
{
LOG_WARN("BLE pairing failed, reason=%d", (int)reason);
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus);
}
static struct bt_conn_auth_info_cb auth_info_cb = {
.pairing_complete = pairing_complete_cb,
.pairing_failed = pairing_failed_cb,
};
#endif /* CONFIG_BT_SMP */
// ── BluetoothPhoneAPI methods
// ─────────────────────────────────────────────────
void BluetoothPhoneAPI::onNowHasData(uint32_t fromRadioNum)
{
PhoneAPI::onNowHasData(fromRadioNum);
fromNumValue = fromRadioNum;
if (!(fromnum_ccc_val & BT_GATT_CCC_NOTIFY))
return;
// active_conn may be torn down on another thread while we're dispatching
// this notify — acquire under ble_mutex so disconnected_cb can't free the
// conn between the null check and bt_conn_ref.
struct bt_conn *conn = acquire_active_conn();
if (!conn)
return;
bt_gatt_notify(conn, &mesh_svc.attrs[FROMNUM_ATTR_IDX], &fromNumValue, sizeof(fromNumValue));
bt_conn_unref(conn);
}
bool BluetoothPhoneAPI::checkIsConnected()
{
return active_conn != nullptr;
}
// ── Deferred ToRadio processor + zombie-connection watchdog ──────────────────
//
// write_toradio() runs on the BT RX workqueue thread (CONFIG_BT_RX_STACK_SIZE)
// and cannot execute phoneAPI->handleToRadio() directly: handleStartConfig
// recurses through nanopb encode + state machine init and overflows the RX
// stack. This thread runs on the Meshtastic OSThread scheduler (24 KB stack),
// picks up the pending ToRadio buffer flagged by write_toradio(), and calls
// handleToRadio() with plenty of headroom.
//
// Real-time fromNum notifications are sent synchronously from
// BluetoothPhoneAPI::onNowHasData() (called by PhoneAPI when new data is
// queued).
//
// Zombie-connection detection has two tiers:
//
// (1) Liveness probe. After IDLE_BEFORE_PROBE_MS without ATT traffic, send
// a bt_gatt_notify to fromNum every PROBE_INTERVAL_MS. If the
// controller replies -ENOTCONN the LL link is definitely dead but the
// host didn't forward LE Disconnection Complete → reboot. We avoid
// probing during normal activity so iOS isn't woken up unnecessarily
// (each probe wakes iOS → triggers a zero-byte FromRadio drain).
//
// (2) Hard watchdog. Absolute HARD_WATCHDOG_MS ceiling on ATT idle as a
// fallback if probes somehow don't detect the zombie.
class BleDeferredThread : public concurrency::OSThread
{
static constexpr uint32_t IDLE_BEFORE_PROBE_MS = 30000; // 30 s: start probing
static constexpr uint32_t PROBE_INTERVAL_MS = 5000; // 5 s: between probes
static constexpr uint32_t HARD_WATCHDOG_MS = 60000; // 1 min: last resort
uint32_t last_probe_ms = 0;
public:
BleDeferredThread() : concurrency::OSThread("BleDeferred") {}
protected:
int32_t runOnce() override
{
// Snapshot the pending ToRadio buffer under the mutex, then release
// the lock before calling into handleToRadio (which can be slow and
// must not block the BT RX thread producer).
uint8_t buf[MAX_TO_FROM_RADIO_SIZE];
size_t n = 0;
bool have_pending = false;
k_mutex_lock(&pendingToRadioMutex, K_FOREVER);
if (pendingToRadio) {
memcpy(buf, pendingToRadioBuf, pendingToRadioLen);
n = pendingToRadioLen;
pendingToRadio = false;
have_pending = true;
}
k_mutex_unlock(&pendingToRadioMutex);
if (have_pending && phoneAPI) {
phoneAPI->handleToRadio(buf, n);
}
// Take a reference to active_conn so it can't be freed underneath us
// if disconnected_cb fires on another thread while we're dispatching.
struct bt_conn *conn = acquire_active_conn();
if (!conn || connect_time_ms == 0) {
if (conn)
bt_conn_unref(conn);
last_probe_ms = 0;
return 100;
}
uint32_t now = k_uptime_get_32();
uint32_t att_idle = now - last_att_time_ms;
// Liveness probe — only when ATT has been quiet for a while.
if (att_idle > IDLE_BEFORE_PROBE_MS && (now - last_probe_ms) >= PROBE_INTERVAL_MS &&
(fromnum_ccc_val & BT_GATT_CCC_NOTIFY)) {
last_probe_ms = now;
int err = bt_gatt_notify(conn, &mesh_svc.attrs[FROMNUM_ATTR_IDX], &fromNumValue, sizeof(fromNumValue));
if (err == -ENOTCONN) {
LOG_WARN("BLE zombie (probe ENOTCONN); rebooting");
bt_conn_unref(conn);
k_sleep(K_MSEC(50)); // flush log
sys_reboot(SYS_REBOOT_COLD);
}
}
bt_conn_unref(conn);
// Hard ceiling — last-resort reboot if probes miss the zombie.
if (att_idle > HARD_WATCHDOG_MS && (now - connect_time_ms) > HARD_WATCHDOG_MS) {
LOG_WARN("BLE zombie (hard watchdog %us); rebooting", HARD_WATCHDOG_MS / 1000);
k_sleep(K_MSEC(50));
sys_reboot(SYS_REBOOT_COLD);
}
return 100;
}
};
static BleDeferredThread *bleDeferredThread = nullptr;
// ── BT stack pre-initializer (call from main thread before OSThreads start) ──
//
// bt_enable() requires substantially more stack than a Meshtastic OSThread
// (PowerFSMThread) provides — calling it there causes a stack overflow.
// Call this from nrf54l15Setup() (main Zephyr thread, CONFIG_MAIN_STACK_SIZE)
// so that by the time NRF54L15Bluetooth::setup() runs from PowerFSMThread,
// bt_initialized is already true and bt_enable() is skipped.
void nrf54l15_bt_preinit()
{
if (!bt_initialized) {
int err = bt_enable(NULL);
if (err) {
LOG_ERROR("BLE pre-init failed: %d", err);
return;
}
bt_initialized = true;
LOG_INFO("BLE stack pre-initialized on main thread");
// Phase 7: load bonding keys from LittleFS (/lfs/bt_settings).
// LittleFS is already mounted by fsInit() before nrf54l15Setup() runs.
// On first boot the file doesn't exist — settings_load() returns 0 (OK).
// On subsequent boots, previously bonded peers are restored so the
// phone can reconnect without re-pairing.
err = settings_load();
if (err) {
LOG_WARN("settings_load failed: %d (OK on first boot)", err);
} else {
LOG_INFO("BT settings loaded from /lfs/bt_settings");
}
}
}
// ── NRF54L15Bluetooth public methods ─────────────────────────────────────────
// Shared init: idempotent setup of work item, OSThread, auth callbacks,
// bt_enable, and device name. Leaves advertising control to the caller.
static bool nrf54l15_bt_init_common()
{
k_work_init(&adv_restart_work, adv_restart_work_fn);
if (!bleDeferredThread) {
bleDeferredThread = new BleDeferredThread();
}
if (!phoneAPI) {
phoneAPI = new BluetoothPhoneAPI();
}
#if defined(CONFIG_BT_SMP)
// NO_PIN is unsupported on this platform: the mesh GATT permissions are
// declared with BT_GATT_PERM_*_AUTHEN, prj.conf sets
// CONFIG_BT_SMP_ENFORCE_MITM=y, and the build pulls in the SMP/passkey path.
// If a user requested NO_PIN we'd register no auth callbacks → no display
// path for the passkey → every GATT access returns BT_ATT_ERR_AUTHENTICATION
// and the link is unusable. Fall back to RANDOM_PIN behavior with a warning
// instead of leaving BLE silently broken.
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
LOG_WARN("BLE: NO_PIN not supported on nRF54L15-DK (MITM-only build); "
"treating as RANDOM_PIN");
}
bt_conn_auth_cb_register(&auth_cb);
bt_conn_auth_info_cb_register(&auth_info_cb);
// FIXED_PIN — register the configured passkey so the mobile app prompts
// the user for that specific number instead of a random display-only PIN.
// RANDOM_PIN (and clamped NO_PIN) keeps the default behavior: Zephyr
// generates a fresh passkey on each pairing attempt and fires
// auth_passkey_display with it.
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN) {
configuredPasskey = config.bluetooth.fixed_pin;
int rc = bt_passkey_set(configuredPasskey);
if (rc) {
LOG_WARN("bt_passkey_set(%u) failed: %d", configuredPasskey, rc);
} else {
LOG_INFO("BLE fixed PIN: %06u", configuredPasskey);
}
} else {
bt_passkey_set(BT_PASSKEY_INVALID); // random per-pair
}
#endif /* CONFIG_BT_SMP */
if (!bt_initialized) {
int err = bt_enable(NULL);
if (err) {
LOG_ERROR("BLE enable failed: %d", err);
return false;
}
bt_initialized = true;
LOG_INFO("BLE stack enabled");
}
bt_set_name(getDeviceName());
return true;
}
void NRF54L15Bluetooth::setup()
{
LOG_INFO("NRF54L15Bluetooth::setup()");
if (!nrf54l15_bt_init_common()) {
return;
}
ble_enabled = true;
start_advertising();
}
void NRF54L15Bluetooth::shutdown()
{
LOG_INFO("NRF54L15Bluetooth::shutdown()");
ble_enabled = false;
stop_advertising();
struct bt_conn *conn = acquire_active_conn();
if (conn) {
bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
bt_conn_unref(conn);
}
}
void NRF54L15Bluetooth::startDisabled()
{
// Initialize BT stack but leave advertising off until resumeAdvertising().
if (!nrf54l15_bt_init_common()) {
return;
}
ble_enabled = false;
LOG_INFO("BLE initialized, advertising stopped (startDisabled)");
}
void NRF54L15Bluetooth::resumeAdvertising()
{
ble_enabled = true;
start_advertising();
}
void NRF54L15Bluetooth::clearBonds()
{
LOG_INFO("BLE clear bonds");
bt_unpair(BT_ID_DEFAULT, BT_ADDR_LE_ANY);
}
bool NRF54L15Bluetooth::isConnected()
{
return active_conn != nullptr;
}
int NRF54L15Bluetooth::getRssi()
{
return 0; // TODO: Zephyr has no direct bt_conn_get_rssi; use HCI RSSI read
// command
}
void NRF54L15Bluetooth::sendLog(const uint8_t *logMessage, size_t length)
{
if (length > 512 || logradio_ccc_val == 0) {
return;
}
// Acquire a reference under ble_mutex so disconnected_cb can't free the
// connection between the null check and bt_gatt_notify.
struct bt_conn *conn = acquire_active_conn();
if (!conn) {
return;
}
// Send as notify regardless of whether client subscribed to NOTIFY or
// INDICATE — bt_gatt_indicate() requires a params struct with a callback;
// notify is simpler and the app accepts both. Change to indicate if
// compatibility issues arise.
bt_gatt_notify(conn, &mesh_svc.attrs[LOGRADIO_ATTR_IDX], logMessage, (uint16_t)length);
bt_conn_unref(conn);
}
+29
View File
@@ -0,0 +1,29 @@
// NRF54L15Bluetooth.h — Zephyr BLE backend for nRF54L15
//
// Implements the same interface as NRF52Bluetooth (same method names and
// signatures) so main.cpp and AdminModule can use nrf52Bluetooth pointer
// without knowing the underlying implementation.
//
// GATT profile is identical to the nRF52 implementation:
// Service: MESH_SERVICE_UUID
// toRadio: TORADIO_UUID (WRITE)
// fromRadio: FROMRADIO_UUID (READ)
// fromNum: FROMNUM_UUID (READ | NOTIFY)
// logRadio: LOGRADIO_UUID (READ | NOTIFY | INDICATE)
#pragma once
#include "BluetoothCommon.h"
class NRF54L15Bluetooth : public BluetoothApi
{
public:
void setup();
void shutdown();
void startDisabled();
void resumeAdvertising();
void clearBonds();
bool isConnected();
int getRssi();
void sendLog(const uint8_t *logMessage, size_t length);
};
+17
View File
@@ -0,0 +1,17 @@
// Nrf52SaadcLock.h — stub for nRF54L15/Zephyr
// Power.cpp includes this when ARCH_NRF52 is defined.
// Phase 2: compile-only stub.
#pragma once
#ifdef ARCH_NRF52
#include "concurrency/Lock.h"
namespace concurrency
{
/** Shared mutex for SAADC configuration and reads (VDD + battery analog path).
* On nRF54L15 ADC is handled differently; this is a compile-only stub. */
extern Lock *nrf52SaadcLock;
} // namespace concurrency
#endif
+4
View File
@@ -0,0 +1,4 @@
// Print.h — shim for nRF54L15/Zephyr
// Meshtastic includes <Print.h> separately; redirect to our Arduino.h shim.
#pragma once
#include "Arduino.h"
+62
View File
@@ -0,0 +1,62 @@
/**
* SPI.h — Arduino SPI shim for Zephyr/nRF54L15
*
* Provides the Arduino SPIClass interface backed by Zephyr's SPI API. The
* backing controller is SPIM00 (HP domain, 3.0 V); the implementation in
* nrf54l15_arduino.cpp binds to DEVICE_DT_GET(DT_NODELABEL(spi00)) and the
* bus is configured in zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay.
* RadioLib uses ArduinoHal which calls transfer() byte-by-byte.
*
* CS pin is handled by RadioLib via digitalWrite() — hardware CS is not used.
*/
#pragma once
#include "Arduino.h"
#include <stdint.h>
#define SPI_MODE0 0
#define SPI_MODE1 1
#define SPI_MODE2 2
#define SPI_MODE3 3
struct SPISettings {
uint32_t clock;
uint8_t bitOrder;
uint8_t dataMode;
// Arduino API allows `SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0))` — implicit form is intentional.
// cppcheck-suppress noExplicitConstructor
SPISettings(uint32_t clock = 4000000, uint8_t bitOrder = MSBFIRST, uint8_t dataMode = SPI_MODE0)
: clock(clock), bitOrder(bitOrder), dataMode(dataMode)
{
}
};
class SPIClass
{
public:
void begin() {}
void begin(uint8_t sck, uint8_t miso, uint8_t mosi, uint8_t ss = 0xFF) {}
void end() {}
void beginTransaction(SPISettings) {}
void endTransaction() {}
void setBitOrder(uint8_t order) {}
void setDataMode(uint8_t mode) {}
void setClockDivider(uint8_t div) {}
void setFrequency(uint32_t freq) {}
// Real Zephyr SPI implementation — defined in nrf54l15_arduino.cpp
uint8_t transfer(uint8_t data);
uint16_t transfer16(uint16_t data);
void transfer(void *buf, size_t count);
void transferBytes(const uint8_t *tx, uint8_t *rx, uint32_t count);
uint8_t transfer(uint8_t tx, uint8_t *rx, uint32_t count)
{
transferBytes(&tx, rx, count);
return rx ? rx[0] : 0;
}
};
extern SPIClass SPI;
extern SPIClass SPI1;
+5
View File
@@ -0,0 +1,5 @@
// Stream.h — shim for nRF54L15/Zephyr
// StreamAPI.h and other Meshtastic headers include <Stream.h>.
// Redirect to our Arduino.h shim which defines the Stream base class.
#pragma once
#include "Arduino.h"
+4
View File
@@ -0,0 +1,4 @@
// Tone.h — shim for nRF54L15/Zephyr
// Tone functions are stubbed in Arduino.h; this header satisfies direct includes.
#pragma once
#include "Arduino.h"
+5
View File
@@ -0,0 +1,5 @@
// WProgram.h — shim for nRF54L15/Zephyr
// ArduinoThread (and other legacy Arduino libs) include <WProgram.h>.
// Redirect to our Arduino.h shim.
#pragma once
#include "Arduino.h"
+219
View File
@@ -0,0 +1,219 @@
// Wire.cpp — Arduino TwoWire backed by Zephyr i2c30 (TWIM30 hardware).
//
// The pinctrl + clock-frequency are configured in
// zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay. Runtime begin()/setClock()
// are best-effort: setClock() goes through i2c_configure() to actually change
// the bus speed; begin() just verifies the device is ready.
#include "Wire.h"
#include "configuration.h"
#include <errno.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/kernel.h>
// Resolve the i2c30 node at compile time. If the overlay has not enabled
// i2c30, this evaluates to a NULL device pointer and every call short-circuits
// to a NACK return code — matching the prior compile-only stub behavior.
#define I2C_NODE DT_NODELABEL(i2c30)
static const struct device *getI2CDevice()
{
#if DT_NODE_HAS_STATUS(I2C_NODE, okay)
static const struct device *const dev = DEVICE_DT_GET(I2C_NODE);
return dev;
#else
return nullptr;
#endif
}
// Wire/Wire1 instances are defined in nrf54l15_arduino.cpp alongside the
// other Arduino singletons (Serial, SPI, …).
TwoWire::TwoWire() : txAddr(0), txLen(0), txBuf{}, rxLen(0), rxPos(0), rxBuf{} {}
void TwoWire::begin()
{
const struct device *dev = getI2CDevice();
if (dev == nullptr) {
LOG_WARN("Wire.begin(): i2c30 not enabled in DT overlay");
return;
}
if (!device_is_ready(dev)) {
LOG_WARN("Wire.begin(): i2c30 device not ready");
return;
}
LOG_INFO("Wire.begin(): i2c30 ready");
}
void TwoWire::begin(uint8_t /*sda*/, uint8_t /*scl*/)
{
// SDA/SCL fixed by overlay pinctrl — pin args ignored.
begin();
}
void TwoWire::begin(int /*sda*/, int /*scl*/, uint32_t freq)
{
begin();
if (freq) {
setClock(freq);
}
}
void TwoWire::end()
{
// No-op: Zephyr i2c devices stay initialized for the lifetime of the
// application. Runtime PM (zephyr,pm-device-runtime-auto in the DT)
// handles low-power transitions when idle.
}
void TwoWire::setClock(uint32_t freq)
{
const struct device *dev = getI2CDevice();
if (dev == nullptr) {
return;
}
uint32_t speed;
if (freq >= 1000000U) {
speed = I2C_SPEED_FAST_PLUS; // 1 MHz
} else if (freq >= 400000U) {
speed = I2C_SPEED_FAST; // 400 kHz
} else {
speed = I2C_SPEED_STANDARD; // 100 kHz
}
uint32_t cfg = I2C_MODE_CONTROLLER | I2C_SPEED_SET(speed);
int rc = i2c_configure(dev, cfg);
if (rc) {
LOG_WARN("Wire.setClock(%u) failed: %d", (unsigned)freq, rc);
}
}
void TwoWire::beginTransmission(uint8_t addr)
{
txAddr = addr;
txLen = 0;
}
size_t TwoWire::write(uint8_t data)
{
if (txLen >= WIRE_BUFFER_LENGTH) {
return 0; // overflow — endTransmission() will return 1
}
txBuf[txLen++] = data;
return 1;
}
size_t TwoWire::write(const uint8_t *data, size_t n)
{
size_t written = 0;
for (size_t i = 0; i < n; i++) {
if (write(data[i]) == 0) {
break;
}
written++;
}
return written;
}
uint8_t TwoWire::endTransmission(bool /*stop*/)
{
// Arduino return codes:
// 0 = success
// 1 = data-too-long (overflow caught in write())
// 2 = NACK on address
// 3 = NACK on data
// 4 = other error
// 5 = timeout
if (txLen > WIRE_BUFFER_LENGTH) {
return 1;
}
const struct device *dev = getI2CDevice();
if (dev == nullptr || !device_is_ready(dev)) {
return 4;
}
int rc = i2c_write(dev, txBuf, txLen, txAddr);
txLen = 0;
if (rc == 0) {
return 0;
}
if (rc == -EIO) {
return 2; // address NACK is the most common -EIO source on nrf-twim
}
if (rc == -ETIMEDOUT) {
return 5;
}
return 4;
}
uint8_t TwoWire::requestFrom(uint8_t addr, uint8_t quantity, bool /*stop*/)
{
rxLen = 0;
rxPos = 0;
if (quantity == 0) {
return 0;
}
if (quantity > WIRE_BUFFER_LENGTH) {
quantity = WIRE_BUFFER_LENGTH;
}
const struct device *dev = getI2CDevice();
if (dev == nullptr || !device_is_ready(dev)) {
return 0;
}
// If there is a pending TX (driver wrote register address via write()
// without an explicit endTransmission()), use i2c_write_read so the
// repeated-start path matches the typical "set register pointer then
// read N bytes" sensor protocol.
int rc;
if (txLen > 0) {
rc = i2c_write_read(dev, addr, txBuf, txLen, rxBuf, quantity);
txLen = 0;
} else {
rc = i2c_read(dev, rxBuf, quantity, addr);
}
if (rc) {
return 0;
}
rxLen = quantity;
return quantity;
}
int TwoWire::available()
{
return rxLen - rxPos;
}
int TwoWire::read()
{
if (rxPos >= rxLen) {
return -1;
}
return rxBuf[rxPos++];
}
int TwoWire::peek()
{
if (rxPos >= rxLen) {
return -1;
}
return rxBuf[rxPos];
}
size_t TwoWire::readBytes(uint8_t *buf, size_t len)
{
size_t n = 0;
while (n < len) {
int b = read();
if (b < 0) {
break;
}
buf[n++] = (uint8_t)b;
}
return n;
}
TwoWire::operator bool() const
{
return getI2CDevice() != nullptr;
}
+83
View File
@@ -0,0 +1,83 @@
/**
* Wire.h — Arduino TwoWire (I2C) shim for Zephyr / nRF54L15.
*
* Bus binding: the Zephyr device tree alias `i2c30` (TWIM30 hardware
* peripheral, HP domain, 3.0 V) is resolved at compile time via
* DEVICE_DT_GET in Wire.cpp. SDA/SCL pins are configured in the board
* overlay via pinctrl — `begin(sda, scl)` overloads are accepted for
* Arduino API compatibility but the pin arguments are ignored.
*
* Buffer sizes are sized for the worst-case I2C consumer we plan to use
* (NXP SE050 secure element, ~256-byte T=1 frames). BMP280 / INA228 /
* SHT40 / INA3221 read in single-digit bytes and fit trivially.
*/
#pragma once
#include "Arduino.h"
#include <stddef.h>
#include <stdint.h>
#ifndef WIRE_BUFFER_LENGTH
#define WIRE_BUFFER_LENGTH 256
#endif
class TwoWire
{
public:
TwoWire();
// ── Bus lifecycle ─────────────────────────────────────────────────
// begin() variants — pin arguments are accepted for API compatibility
// but ignored: SDA/SCL are fixed by the Zephyr overlay pinctrl. freq
// is also fixed by overlay clock-frequency (use setClock() at runtime).
void begin();
void begin(uint8_t sda, uint8_t scl);
void begin(int sda, int scl, uint32_t freq);
void end();
void setClock(uint32_t freq);
void setClockStretchLimit(uint32_t) {} // no-op on TWIM hardware
// ── Master write ─────────────────────────────────────────────────
void beginTransmission(uint8_t addr);
void beginTransmission(int addr) { beginTransmission((uint8_t)addr); }
// Return codes (Arduino convention):
// 0 = success, 1 = data-too-long, 2 = NACK on addr, 3 = NACK on data,
// 4 = other error, 5 = timeout.
uint8_t endTransmission(bool stop = true);
uint8_t endTransmission(uint8_t stop) { return endTransmission(stop != 0); }
size_t write(uint8_t data);
size_t write(const uint8_t *data, size_t n);
// ── Master read ──────────────────────────────────────────────────
uint8_t requestFrom(uint8_t addr, uint8_t quantity, bool stop = true);
uint8_t requestFrom(uint8_t addr, uint8_t quantity, uint8_t stop) { return requestFrom(addr, quantity, stop != 0); }
uint8_t requestFrom(int addr, int quantity, int stop = 1) { return requestFrom((uint8_t)addr, (uint8_t)quantity, stop != 0); }
int available();
int read();
int peek();
size_t readBytes(uint8_t *buf, size_t len);
size_t readBytes(char *buf, size_t len) { return readBytes((uint8_t *)buf, len); }
void flush() {}
// Slave callbacks unsupported — peripheral-only stub.
void onReceive(void (*)(int)) {}
void onRequest(void (*)(void)) {}
operator bool() const;
private:
uint8_t txAddr;
uint16_t txLen;
uint8_t txBuf[WIRE_BUFFER_LENGTH];
uint16_t rxLen;
uint16_t rxPos;
uint8_t rxBuf[WIRE_BUFFER_LENGTH];
};
extern TwoWire Wire;
extern TwoWire Wire1; // alias to Wire — only one I2C bus on this board
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#define ARCH_NRF54L15
//
// Feature flags for nRF54L15.
//
// The HAS_* macros below are Meshtastic's compile-time feature gate: every
// optional subsystem (BLE, screen, I2C, GPS, buttons, telemetry, sensors,
// radio, CPU shutdown, ...) is wrapped in `#if HAS_FOO` so a given board
// only pays for the features it actually ships. On memory-tight MCUs this
// is not cosmetic — it's the difference between a binary that fits in
// flash and one that doesn't, and between a build that links and one that
// drags in drivers for hardware the board doesn't have. Defaulting to 0
// here (rather than inheriting nRF52 defaults) is deliberate: the
// nRF54L15-DK is a bare dev kit with no screen, no I2C sensors, no GPS,
// no user buttons — so every HAS_* flag starts off and gets flipped on
// explicitly by variants that add that hardware.
//
// Feature flags are also the cleanest way to absorb platform divergence
// without sprinkling `#ifdef ARCH_NRF54L15` across shared code. Anywhere
// a subsystem can be conditionally compiled via HAS_*, prefer that over
// per-arch guards: it keeps the core code arch-agnostic, makes it trivial
// to bring up the next board (flip the flags, don't patch call sites),
// and keeps the "does this platform support X?" question answerable by
// reading one file instead of grepping the tree. BLE in particular is
// deferred to Phase 2 on this port — the nRF54L15 uses MPSL/Zephyr BLE
// APIs rather than the Adafruit SoftDevice stack used by nRF52840 — so
// while HAS_BLUETOOTH defaults to 1, the actual implementation lives in
// NRF54L15Bluetooth.cpp behind its own Zephyr Kconfig gates.
//
#ifndef HAS_BLUETOOTH
#define HAS_BLUETOOTH 1
#endif
#ifndef HAS_SCREEN
#define HAS_SCREEN 0
#endif
#ifndef HAS_WIRE
#define HAS_WIRE 0
#endif
#ifndef HAS_GPS
#define HAS_GPS 0
#endif
#ifndef HAS_BUTTON
#define HAS_BUTTON 0
#endif
#ifndef HAS_TELEMETRY
#define HAS_TELEMETRY 0
#endif
#ifndef HAS_SENSOR
#define HAS_SENSOR 0
#endif
#ifndef HAS_RADIO
#define HAS_RADIO 1
#endif
#ifndef HAS_CPU_SHUTDOWN
#define HAS_CPU_SHUTDOWN 0
#endif
// ADC reference — nRF54L15 SAADC uses VDD/4 internal ref by default
#ifndef AREF_VOLTAGE
#define AREF_VOLTAGE 3.6
#endif
#ifndef BATTERY_SENSE_RESOLUTION_BITS
#define BATTERY_SENSE_RESOLUTION_BITS 12
#endif
//
// HW_VENDOR — maps build-time define to HardwareModel enum.
// PRIVATE_HW (255): the protobuf HardwareModel enum reserves DK / DIY boards
// without an SKU under this value; the nRF54L15-DK doesn't get a dedicated
// enum number. Variant manifest matches via custom_meshtastic_hw_model = 255.
//
#ifdef NRF54L15_DK
#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW
#else
#define HW_VENDOR meshtastic_HardwareModel_UNSET
#endif
+19
View File
@@ -0,0 +1,19 @@
// bluefruit.h — stub for nRF54L15/Zephyr
// NodeDB.cpp includes this when ARCH_NRF52 is defined.
// Bluetooth is excluded (MESHTASTIC_EXCLUDE_BLUETOOTH=1); this satisfies
// the include chain without pulling in the Adafruit Bluefruit SDK.
#pragma once
struct BLEPeripheral {
void clearBonds() {}
};
struct BLECentral {
void clearBonds() {}
};
struct BlueFruitClass {
BLEPeripheral Periph;
BLECentral Central;
};
extern BlueFruitClass Bluefruit;
+210
View File
@@ -0,0 +1,210 @@
/*
* main-nrf54l15.cpp — Platform entry points for Nordic nRF54L15
*
* Adapted from src/platform/nrf52/main-nrf52.cpp.
* SoftDevice, Adafruit BLE, and nRFCrypto are NOT available on nRF54L15.
* Phase 2 will add proper BLE via Zephyr MPSL APIs.
*
* TODO items are marked with "TODO(nrf54l15):"
*/
#include "configuration.h"
#include <SPI.h>
#include <Wire.h>
#include <assert.h>
#include <stdio.h>
#include "NodeDB.h"
#include "PowerMon.h"
#include "Router.h"
#include "error.h"
#include "main.h"
#include "mesh/MeshService.h"
#include "meshUtils.h"
#include "power.h"
#include <power/PowerHAL.h>
// ── Watchdog ──────────────────────────────────────────────────────────────
// TODO(nrf54l15): nRF54L15 has a WDT peripheral but nrfx_wdt driver support
// may differ depending on the Zephyr SDK version. Enable once confirmed.
#define APP_WATCHDOG_SECS 90
static bool watchdog_running = false;
static inline void watchdog_feed() {} // TODO(nrf54l15): replace with real WDT feed
// ── Weak variant hooks ────────────────────────────────────────────────────
void variant_shutdown() __attribute__((weak));
void variant_shutdown() {}
void variant_nrf54l15LoopHook(void) __attribute__((weak));
void variant_nrf54l15LoopHook(void) {}
// ── PowerHAL ─────────────────────────────────────────────────────────────
bool powerHAL_isVBUSConnected()
{
// TODO(nrf54l15): nRF54L15 has a USB POWER peripheral — read USBREGSTATUS
return false;
}
bool powerHAL_isPowerLevelSafe()
{
// TODO(nrf54l15): implement SAADC VDD measurement similar to nRF52
return true;
}
void powerHAL_platformInit()
{
// TODO(nrf54l15): configure POF comparator and analog reference if needed
}
// ── Utilities ─────────────────────────────────────────────────────────────
bool loopCanSleep()
{
return !Serial;
}
void updateBatteryLevel(uint8_t level)
{
(void)level;
}
void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr)
{
LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr);
NVIC_SystemReset();
}
void getMacAddr(uint8_t *dmac)
{
// TODO(nrf54l15): verify FICR register layout for nRF54L15.
// nRF52840 uses NRF_FICR->DEVICEADDR[0/1]; nRF54L15 Zephyr HAL may differ.
#if defined(NRF_FICR)
const uint8_t *src = (const uint8_t *)NRF_FICR->DEVICEADDR;
dmac[5] = src[0];
dmac[4] = src[1];
dmac[3] = src[2];
dmac[2] = src[3];
dmac[1] = src[4];
dmac[0] = src[5] | 0xc0;
#else
// Fallback: fixed placeholder until Zephyr FICR path is confirmed
dmac[0] = 0xC2;
dmac[1] = 0xA7;
dmac[2] = 0x54;
dmac[3] = 0x15;
dmac[4] = 0x00;
dmac[5] = 0x01;
#endif
}
// ── Bluetooth ─────────────────────────────────────────────────────────────────
void setBluetoothEnable(bool enable)
{
if (enable) {
static bool initialized = false;
if (!initialized) {
nrf54l15Bluetooth = new NRF54L15Bluetooth();
nrf54l15Bluetooth->startDisabled();
initialized = true;
}
if (nrf54l15Bluetooth) {
nrf54l15Bluetooth->resumeAdvertising();
}
} else {
if (nrf54l15Bluetooth) {
nrf54l15Bluetooth->shutdown();
}
}
}
void clearBonds()
{
if (!nrf54l15Bluetooth) {
nrf54l15Bluetooth = new NRF54L15Bluetooth();
nrf54l15Bluetooth->setup();
}
nrf54l15Bluetooth->clearBonds();
}
void enterDfuMode()
{
// TODO(nrf54l15): nRF54L15 uses nRF Connect DFU (MCUboot/SUIT).
// Trigger via Zephyr boot_request_upgrade() or similar.
NVIC_SystemReset();
}
// ── printf via RTT ────────────────────────────────────────────────────────
// TODO(nrf54l15): SEGGER_RTT may not be available with Zephyr; use printk()
// or a USB CDC console instead. Remove this override if it conflicts.
#ifdef SEGGER_RTT_PRINTF
int printf(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
auto res = SEGGER_RTT_vprintf(0, fmt, &args);
va_end(args);
return res;
}
#endif
// ── Deep sleep ────────────────────────────────────────────────────────────
void cpuDeepSleep(uint32_t msecToWake)
{
#if HAS_WIRE
Wire.end();
#endif
SPI.end();
if (Serial)
Serial.end();
variant_shutdown();
// TODO(nrf54l15): use Zephyr pm_system_suspend() or WFI for proper low-power
if (msecToWake != portMAX_DELAY) {
delay(msecToWake);
NVIC_SystemReset();
} else {
// System off equivalent — halt
while (1) {
__WFI();
}
}
}
// ── Setup / Loop ──────────────────────────────────────────────────────────
// Forward declaration — defined in NRF54L15Bluetooth.cpp
void nrf54l15_bt_preinit();
void nrf54l15Setup()
{
// nRF54L15 power peripheral layout differs from nRF52; RESETREAS not present here.
// TODO(Phase 3): use zephyr/drivers/hwinfo.h hwinfo_get_reset_cause()
LOG_DEBUG("Reset reason: (nRF54L15 power peripheral differs from nRF52, skipped)");
// TODO(nrf54l15): init SAADC, watchdog, and random seed via nrfx or Zephyr
// For now seed with a fixed value; replace with hardware entropy source.
#if defined(NRF_FICR)
randomSeed(analogRead(0) ^ (uint32_t)NRF_FICR->DEVICEADDR[0]);
#else
randomSeed(analogRead(0));
#endif
// Pre-initialize BT stack here on the main thread (CONFIG_MAIN_STACK_SIZE=8192).
// bt_enable() overflows the smaller PowerFSMThread stack when called later.
// NRF54L15Bluetooth::setup() checks bt_initialized and skips bt_enable() if true.
nrf54l15_bt_preinit();
}
void nrf54l15Loop()
{
// First-call gate for the future WDT init — body will hold real init code, not just the bookkeeping flag.
// cppcheck-suppress duplicateConditionalAssign
if (!watchdog_running) {
// TODO(nrf54l15): enable WDT here
watchdog_running = true;
}
watchdog_feed();
variant_nrf54l15LoopHook();
}
+557
View File
@@ -0,0 +1,557 @@
/**
* nrf54l15_arduino.cpp — Arduino shim implementations for Zephyr/nRF54L15
*
* Provides concrete implementations for Print, HardwareSerial, GPIO, SPI,
* and String methods declared in Arduino.h / SPI.h.
*
* Phase 3: real GPIO via Zephyr GPIO API and real SPI via Zephyr SPI API.
* Pin numbering convention: P0.n = n, P1.n = 16+n, P2.n = 32+n.
*/
#include "Arduino.h"
#include "SPI.h"
#include "Wire.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/kernel.h>
// ── Bluefruit singleton stub (satisfies NodeDB.cpp ARCH_NRF52 path) ──────────
#include "bluefruit.h"
BlueFruitClass Bluefruit;
// ── _fini stub — ARM newlib's __libc_fini_array references _fini, but ────────
// Zephyr startup doesn't provide it. Provide a weak no-op so the linker
// is satisfied when C++ global dtors or atexit() pull in __libc_fini_array.
extern "C" void __attribute__((weak)) _fini(void) {}
// ── SPI / Wire singletons ─────────────────────────────────────────────────────
SPIClass SPI;
SPIClass SPI1;
TwoWire Wire;
TwoWire Wire1;
// ── HardwareSerial singletons ────────────────────────────────────────────────
HardwareSerial Serial;
HardwareSerial Serial1;
HardwareSerial Serial2;
// ── Timing functions — C linkage to match extern "C" declarations ────────────
extern "C" uint32_t millis(void)
{
return (uint32_t)k_uptime_get_32();
}
extern "C" uint32_t micros(void)
{
return (uint32_t)(k_uptime_get() * 1000ULL);
}
extern "C" void delay(uint32_t ms)
{
k_sleep(K_MSEC(ms));
}
extern "C" void delayMicroseconds(uint32_t us)
{
k_sleep(K_USEC(us));
}
extern "C" void yield(void)
{
k_yield();
}
// ── NVIC_SystemReset — wraps __NVIC_SystemReset from CMSIS core_cm33.h ───────
// core_cm33.h has #define NVIC_SystemReset __NVIC_SystemReset, so undef it
// before defining our own implementation to prevent macro expansion collision.
#pragma push_macro("NVIC_SystemReset")
#undef NVIC_SystemReset
extern "C" void NVIC_SystemReset(void)
{
sys_reboot(SYS_REBOOT_COLD);
}
#pragma pop_macro("NVIC_SystemReset")
// ── HardwareSerial::write ─────────────────────────────────────────────────────
size_t HardwareSerial::write(uint8_t c)
{
// TODO(nrf54l15 Phase 3): route through Zephyr UART / USB-CDC console
// For now use printk so we at least get something over RTT/UART0
printk("%c", (char)c);
return 1;
}
size_t HardwareSerial::write(const uint8_t *buf, size_t n)
{
for (size_t i = 0; i < n; i++)
printk("%c", (char)buf[i]);
return n;
}
// ── Print::printf ─────────────────────────────────────────────────────────────
int Print::printf(const char *fmt, ...)
{
char buf[256];
va_list args;
va_start(args, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
if (n > 0)
write((const uint8_t *)buf, (size_t)(n < (int)sizeof(buf) ? n : (int)sizeof(buf) - 1));
return n;
}
// ── strlcpy — BSD extension not in Zephyr newlib ────────────────────────────
extern "C" size_t strlcpy(char *dst, const char *src, size_t size)
{
size_t len = strlen(src);
if (size > 0) {
size_t copy = len < size - 1 ? len : size - 1;
memcpy(dst, src, copy);
dst[copy] = '\0';
}
return len;
}
// ── Print numeric helpers ─────────────────────────────────────────────────────
static size_t printNumber(Print &p, unsigned long n, uint8_t base)
{
if (base == 0)
return p.write((uint8_t)n);
char buf[8 * sizeof(long) + 1];
char *end = buf + sizeof(buf) - 1;
*end = '\0';
if (n == 0) {
*--end = '0';
} else {
while (n > 0) {
unsigned long remainder = n % base;
*--end = (char)(remainder < 10 ? '0' + remainder : 'A' + remainder - 10);
n /= base;
}
}
return p.write((const uint8_t *)end, strlen(end));
}
static size_t printFloat(Print &p, double number, uint8_t digits)
{
if (isnan(number))
return p.print("nan");
if (isinf(number))
return p.print("inf");
if (number > 4294967040.0 || number < -4294967040.0)
return p.print("ovf");
size_t n = 0;
if (number < 0.0) {
n += p.write('-');
number = -number;
}
// Round
double rounding = 0.5;
for (uint8_t i = 0; i < digits; i++)
rounding /= 10.0;
number += rounding;
unsigned long int_part = (unsigned long)number;
double remainder = number - (double)int_part;
n += printNumber(p, int_part, 10);
if (digits > 0) {
n += p.write('.');
for (uint8_t i = 0; i < digits; i++) {
remainder *= 10.0;
unsigned int d = (unsigned int)remainder;
n += p.write('0' + d);
remainder -= d;
}
}
return n;
}
size_t Print::print(unsigned char n, int base)
{
return printNumber(*this, n, base);
}
size_t Print::print(int n, int base)
{
if (base == 10 && n < 0) {
size_t r = write('-');
return r + printNumber(*this, (unsigned long)(-n), base);
}
return printNumber(*this, (unsigned long)n, base);
}
size_t Print::print(long n, int base)
{
if (base == 10 && n < 0) {
size_t r = write('-');
return r + printNumber(*this, (unsigned long)(-n), base);
}
return printNumber(*this, (unsigned long)n, base);
}
size_t Print::print(unsigned int n, int base)
{
return printNumber(*this, n, base);
}
size_t Print::print(unsigned long n, int base)
{
return printNumber(*this, n, base);
}
size_t Print::print(float n, int d)
{
return printFloat(*this, n, d);
}
size_t Print::print(double n, int d)
{
return printFloat(*this, n, d);
}
// ── String::replace(String, String) ─────────────────────────────────────────
void String::replace(const String &from, const String &to)
{
if (from.isEmpty() || !_buf)
return;
// Simple O(n²) replace — fine for typical Meshtastic string lengths
String result;
const char *p = _buf;
while (*p) {
if (strncmp(p, from.c_str(), from.length()) == 0) {
result += to;
p += from.length();
} else {
result += *p++;
}
}
*this = result;
}
// ═════════════════════════════════════════════════════════════════════════════
// GPIO — Real Zephyr implementation (Phase 3)
// Pin mapping: P0.n = n (0-15), P1.n = 16+n (16-31), P2.n = 32+n (32-47)
// ═════════════════════════════════════════════════════════════════════════════
static const struct device *_gpio_dev_for_pin(uint32_t pin, gpio_pin_t *zpin)
{
if (pin < 16) {
*zpin = (gpio_pin_t)pin;
return DEVICE_DT_GET(DT_NODELABEL(gpio0));
} else if (pin < 32) {
*zpin = (gpio_pin_t)(pin - 16);
return DEVICE_DT_GET(DT_NODELABEL(gpio1));
} else {
*zpin = (gpio_pin_t)(pin - 32);
return DEVICE_DT_GET(DT_NODELABEL(gpio2));
}
}
void pinMode(uint32_t pin, uint32_t mode)
{
gpio_pin_t zpin;
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
if (!device_is_ready(dev))
return;
gpio_flags_t flags;
switch (mode) {
case OUTPUT:
flags = GPIO_OUTPUT_INACTIVE;
break;
case INPUT_PULLUP:
flags = GPIO_INPUT | GPIO_PULL_UP;
break;
case INPUT_PULLDOWN:
flags = GPIO_INPUT | GPIO_PULL_DOWN;
break;
default:
flags = GPIO_INPUT;
break;
}
gpio_pin_configure(dev, zpin, flags);
}
// Bring-up diagnostics for the SX1262 wiring path. Off by default — enable by
// adding `-DNRF54L15_GPIO_DEBUG` to platformio.ini build_flags. Useful when
// validating CS/NRESET toggles after a wiring change, diagnosing a "stuck HIGH"
// BUSY before the first NRESET pulse, or tracing BUSY transitions during early
// boot. In normal operation these traces are noise (they bypass LOG level
// controls and print on every GPIO touch), so they are gated at compile time.
#ifdef NRF54L15_GPIO_DEBUG
#define GPIO_LOG_MAX 20
static uint32_t _gpio_log_count = 0;
#endif
void digitalWrite(uint32_t pin, uint32_t value)
{
#ifdef NRF54L15_GPIO_DEBUG
// Before the very first NRESET pulse, snapshot BUSY state.
// If BUSY is already HIGH here, the chip never completed power-on calibration.
if (pin == 32 && value == 0) {
static bool _first_nreset = true;
if (_first_nreset) {
_first_nreset = false;
const struct device *bdev = DEVICE_DT_GET(DT_NODELABEL(gpio2));
if (device_is_ready(bdev)) {
gpio_pin_configure(bdev, 3, GPIO_INPUT); // P2.03 = BUSY
int busy_before = gpio_pin_get(bdev, 3);
printk("[nrf54l15] BUSY before first NRESET = %d%s\n", busy_before,
busy_before ? " ← STUCK HIGH (chip damaged?)" : " ← LOW (chip OK)");
}
}
}
#endif
gpio_pin_t zpin;
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
if (!device_is_ready(dev)) {
// Genuine hardware/DTS misconfiguration — keep this regardless of the
// GPIO_DEBUG gate so it surfaces in production builds too.
printk("[GPIO] pin%u dev NOT READY\n", (unsigned)pin);
return;
}
gpio_pin_set(dev, zpin, (int)value);
#ifdef NRF54L15_GPIO_DEBUG
if ((pin == 37 || pin == 32) && _gpio_log_count < GPIO_LOG_MAX) {
// Read back the pin state to confirm it actually changed
int actual = gpio_pin_get(dev, zpin);
printk("[GPIO] pin%u → %u (read-back=%d)\n", (unsigned)pin, (unsigned)value, actual);
_gpio_log_count++;
}
#endif
}
int digitalRead(uint32_t pin)
{
gpio_pin_t zpin;
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
if (!device_is_ready(dev))
return 0;
int v = gpio_pin_get(dev, zpin);
#ifdef NRF54L15_GPIO_DEBUG
// Log BUSY pin (35=P2.03) state changes + periodic updates for 10 seconds
if (pin == 35) {
static uint32_t busy_log_count = 0;
static int last_busy = -1;
static uint32_t first_read_ms = 0;
if (first_read_ms == 0)
first_read_ms = k_uptime_get_32();
uint32_t elapsed_ms = k_uptime_get_32() - first_read_ms;
// Always log state changes
if (v != last_busy) {
printk("[BUSY] %ums: state changed %d → %d\n", (unsigned)elapsed_ms, last_busy, v);
last_busy = v;
}
// Also log every 500ms for first 10 seconds so we can see timeline
if (elapsed_ms < 10000 && busy_log_count < 20 && (elapsed_ms / 500) > (busy_log_count)) {
printk("[BUSY] %ums: pin=%d (periodic)\n", (unsigned)elapsed_ms, v);
busy_log_count = (elapsed_ms / 500) + 1;
}
}
#endif
return v;
}
// ─── attachInterrupt — supports up to NRF54L15_MAX_IRQS pins ────────────────
#define NRF54L15_MAX_IRQS 8
struct _PinIrq {
struct gpio_callback cb;
voidFuncPtr user_cb;
const struct device *dev;
gpio_pin_t zpin;
bool used;
};
static _PinIrq _irq_table[NRF54L15_MAX_IRQS];
static void _gpio_irq_dispatch(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
_PinIrq *irq = CONTAINER_OF(cb, _PinIrq, cb);
if (irq->user_cb)
irq->user_cb();
}
void attachInterrupt(uint32_t pin, voidFuncPtr cb, int mode)
{
gpio_pin_t zpin;
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
if (!device_is_ready(dev))
return;
// Find a free slot (or reuse existing registration for same pin)
_PinIrq *slot = nullptr;
for (int i = 0; i < NRF54L15_MAX_IRQS; i++) {
if (_irq_table[i].used && _irq_table[i].dev == dev && _irq_table[i].zpin == zpin) {
// Re-register: remove old callback first
gpio_remove_callback(dev, &_irq_table[i].cb);
slot = &_irq_table[i];
break;
}
if (!slot && !_irq_table[i].used)
slot = &_irq_table[i];
}
if (!slot)
return; // table full
gpio_flags_t irq_flags;
switch (mode) {
case RISING:
irq_flags = GPIO_INT_EDGE_RISING;
break;
case FALLING:
irq_flags = GPIO_INT_EDGE_FALLING;
break;
default:
irq_flags = GPIO_INT_EDGE_BOTH;
break;
}
slot->user_cb = cb;
slot->dev = dev;
slot->zpin = zpin;
slot->used = true;
gpio_pin_configure(dev, zpin, GPIO_INPUT);
gpio_init_callback(&slot->cb, _gpio_irq_dispatch, BIT(zpin));
gpio_add_callback(dev, &slot->cb);
gpio_pin_interrupt_configure(dev, zpin, irq_flags);
}
void detachInterrupt(uint32_t pin)
{
gpio_pin_t zpin;
const struct device *dev = _gpio_dev_for_pin(pin, &zpin);
for (int i = 0; i < NRF54L15_MAX_IRQS; i++) {
if (_irq_table[i].used && _irq_table[i].dev == dev && _irq_table[i].zpin == zpin) {
gpio_pin_interrupt_configure(dev, zpin, GPIO_INT_DISABLE);
gpio_remove_callback(dev, &_irq_table[i].cb);
_irq_table[i].used = false;
break;
}
}
}
// ═════════════════════════════════════════════════════════════════════════════
// SPI — Real Zephyr implementation using SPIM00 (HP domain, 3.0V)
// CS is handled by RadioLib via digitalWrite() — hardware CS not used.
// Mode 0 (CPOL=0, CPHA=0), MSB first.
// ═════════════════════════════════════════════════════════════════════════════
// Use SPIM00 (HP domain, 3.0V) — SPIM20 is 1.8V LP domain, incompatible with SX1262.
// Lazy-init: DEVICE_DT_GET in global scope fails when the extern symbol is
// not visible in this translation unit. Use a function-local static instead.
static const struct device *_spi00(void)
{
static const struct device *dev = nullptr;
if (!dev) {
dev = DEVICE_DT_GET(DT_NODELABEL(spi00));
if (!device_is_ready(dev)) {
printk("[nrf54l15] spi00 NOT READY\n");
dev = nullptr;
} else {
printk("[nrf54l15] spi00 ready\n");
}
}
return dev;
}
// SPI config: Mode 0, MSB first, no hardware CS (RadioLib does it manually)
//
// SPIM00 base clock = 128 MHz (nRF54L15 default when NRF_CONFIG_CPU_FREQ_MHZ
// is not set; SystemInit() applies 128 MHz). Hardware prescaler must be EVEN
// and in [4, 126] (SPIM00_PRESCALER_DIVISOR_RANGE_MIN/MAX from MDK).
// 1 MHz → prescaler = 128 > 126 → NRFX_ERROR_INVALID_PARAM → -EIO on every
// transfer. Minimum valid frequency is 2 MHz (prescaler = 64).
static const struct spi_config _spi00_cfg = {
.frequency = 2000000U, // 2 MHz — minimum valid for SPIM00 at 128 MHz base
.operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_TRANSFER_MSB,
.slave = 0,
.cs = {}, // CS = NULL → RadioLib handles CS via GPIO
};
// Static DMA buffers — stack-allocated bufs on nRF54L15 may not be reachable
// by SPIM20 EasyDMA. Static placement in .bss/.data is always in Global SRAM.
// rx_byte is pre-filled with 0xAA before every transfer so we can distinguish:
// 0xAA → DMA never wrote (EasyDMA can't reach the buffer)
// 0x00 → MISO actively driven LOW (chip in reset / bus fight)
// 0xFF → MISO floating HIGH
// other → real chip response
static uint8_t _spi_tx_byte __attribute__((aligned(4)));
static uint8_t _spi_rx_byte __attribute__((aligned(4)));
// Dump the first SPI_DUMP_N byte exchanges so we can see what MISO returns.
#define SPI_DUMP_N 30
static uint32_t _spi_dump_count = 0;
uint8_t SPIClass::transfer(uint8_t data)
{
const struct device *dev = _spi00();
if (!dev)
return 0xFF;
_spi_tx_byte = data;
_spi_rx_byte = 0xAA; // sentinel: if DMA doesn't write, we return 0xAA
struct spi_buf tx_buf = {.buf = &_spi_tx_byte, .len = 1};
struct spi_buf rx_buf = {.buf = &_spi_rx_byte, .len = 1};
struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1};
static uint32_t spi_err_count = 0;
int ret = spi_transceive(dev, &_spi00_cfg, &tx_set, &rx_set);
if (ret != 0 && spi_err_count++ < 3)
printk("[SPI] err=%d tx=0x%02x\n", ret, data);
if (_spi_dump_count < SPI_DUMP_N) {
printk("[SPI] #%u tx=0x%02x rx=0x%02x\n", (unsigned)_spi_dump_count, data, _spi_rx_byte);
_spi_dump_count++;
}
return _spi_rx_byte;
}
// Static DMA-safe buffers for transfer16 — same EasyDMA reachability concern
// applies as for the byte path: stack buffers from a caller thread may sit in
// per-thread RAM regions that EasyDMA cannot reach.
static uint8_t _spi_tx16[2] __attribute__((aligned(4)));
static uint8_t _spi_rx16[2] __attribute__((aligned(4)));
uint16_t SPIClass::transfer16(uint16_t data)
{
const struct device *dev = _spi00();
if (!dev)
return 0xFFFF;
_spi_tx16[0] = (uint8_t)(data >> 8);
_spi_tx16[1] = (uint8_t)(data & 0xFF);
_spi_rx16[0] = 0xAA;
_spi_rx16[1] = 0xAA;
struct spi_buf tx_buf = {.buf = _spi_tx16, .len = 2};
struct spi_buf rx_buf = {.buf = _spi_rx16, .len = 2};
struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1};
spi_transceive(dev, &_spi00_cfg, &tx_set, &rx_set);
return ((uint16_t)_spi_rx16[0] << 8) | _spi_rx16[1];
}
void SPIClass::transferBytes(const uint8_t *tx, uint8_t *rx, uint32_t count)
{
if (!count)
return;
const struct device *dev = _spi00();
if (!dev)
return;
// Zephyr requires non-const buf pointer; cast is safe for tx-only direction
struct spi_buf tx_buf = {.buf = const_cast<uint8_t *>(tx), .len = count};
struct spi_buf rx_buf = {.buf = rx, .len = count};
struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
struct spi_buf_set rx_set = {.buffers = rx_buf.buf ? &rx_buf : nullptr, .count = rx_buf.buf ? 1U : 0U};
spi_transceive(dev, &_spi00_cfg, &tx_set, rx ? &rx_set : nullptr);
}
void SPIClass::transfer(void *buf, size_t count)
{
if (!count || !buf)
return;
transferBytes(reinterpret_cast<const uint8_t *>(buf), reinterpret_cast<uint8_t *>(buf), (uint32_t)count);
}
+121
View File
@@ -0,0 +1,121 @@
/*
* nrf54l15_main.cpp — Zephyr entry point for Meshtastic nRF54L15 port
*
* Zephyr calls main() instead of Arduino's setup()/loop().
* This file provides the main() that bootstraps the Arduino-style
* Meshtastic application loop.
*/
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/fatal.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/reboot.h>
// Forward declarations from src/main.cpp
void setup();
void loop();
// ── Crash info saved to noinit RAM (survives soft reset) ─────────────────────
// Zephyr's arch_esf does not expose the faulting SP directly; we capture PSP
// at entry to the fatal handler (the exception-basic frame lives there) and
// store xPSR alongside PC/LR for context.
struct crash_info {
uint32_t magic;
uint32_t reason;
uint32_t pc;
uint32_t psp; // stack pointer captured at fault entry
uint32_t xpsr; // saved program status (flags + exception number)
uint32_t lr;
uint32_t cfsr; // Configurable Fault Status Register
};
static struct crash_info saved_crash __attribute__((section(".noinit")));
#define CRASH_MAGIC 0xDEADBEEF
// Override Zephyr's weak fatal handler to save crash info, then cold-reboot so
// main() can report the saved record on the next boot. We don't rely on
// CONFIG_RESET_ON_FATAL_ERROR (default off → k_fatal_halt would spin forever)
// — we issue sys_reboot() ourselves after flushing logs.
extern "C" void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf)
{
saved_crash.magic = CRASH_MAGIC;
saved_crash.reason = reason;
// Capture the faulting thread's stack pointer before we start using the
// handler's own stack for logging.
uint32_t psp_at_entry;
__asm__ volatile("mrs %0, psp" : "=r"(psp_at_entry));
saved_crash.psp = psp_at_entry;
if (esf) {
saved_crash.pc = esf->basic.pc;
saved_crash.xpsr = esf->basic.xpsr;
saved_crash.lr = esf->basic.lr;
}
// Read Cortex-M33 SCB CFSR
saved_crash.cfsr = *((volatile uint32_t *)0xE000ED28U);
printk("[nrf54l15] FATAL reason=%u pc=0x%08x lr=0x%08x cfsr=0x%08x\n", reason, saved_crash.pc, saved_crash.lr,
saved_crash.cfsr);
// Walk the failing thread's stack and print any word that looks like a
// Thumb code address (0x1000 — flash end, with the Thumb-mode low bit set).
// The Cortex-M exception frame at PSP holds r0,r1,r2,r3,r12,lr,pc,xpsr
// (8 words); deeper words are the caller's saved frame, which gives a
// crude but useful poor-man's backtrace when CONFIG_DEBUG_COREDUMP is off.
// Found the BLE-init bad_alloc → abort() chain (heap exhaustion under
// CONFIG_BT_BUF_ACL_RX_SIZE=251) when the fault dump alone showed only
// abort itself. Cheap (~150 B of code) and silent until a fault.
uint32_t psp;
__asm__ volatile("mrs %0, psp" : "=r"(psp));
printk("[nrf54l15] PSP=0x%08x — stack walk:\n", psp);
// Validate PSP before dereferencing. Real faults frequently leave PSP
// pointing at corrupted/unmapped memory, and walking it blindly triggers a
// second fault inside this handler. Restrict to nRF54L15 SRAM (256 KB at
// 0x20000000) with 4-byte alignment, and clamp the walk so we never read
// past the end of RAM.
const uintptr_t SRAM_START = 0x20000000UL;
const uintptr_t SRAM_END = 0x20040000UL;
if (psp < SRAM_START || psp >= SRAM_END || (psp & 0x3U) != 0) {
printk("[nrf54l15] PSP out of SRAM range or unaligned, skipping walk\n");
} else {
const uint32_t *sp = (const uint32_t *)psp;
int max_words = (int)((SRAM_END - psp) / sizeof(uint32_t));
if (max_words > 96)
max_words = 96;
for (int i = 0; i < max_words; i++) {
uint32_t v = sp[i];
if (v >= 0x00001000 && v < 0x00080000 && (v & 1)) {
printk("[nrf54l15] sp[%d]=0x%08x (code)\n", i, v);
}
}
}
// Give the RTT/printk backend a chance to drain before we reset, otherwise
// the crash log line above is lost and the next boot's "Prev crash" line is
// the only forensic evidence we get.
k_busy_wait(50000); // 50 ms
sys_reboot(SYS_REBOOT_COLD);
// Unreachable; k_fatal_halt as a defensive backstop in case sys_reboot
// returns (it shouldn't).
k_fatal_halt(reason);
}
int main(void)
{
uint32_t reset_cause = 0;
hwinfo_get_reset_cause(&reset_cause);
hwinfo_clear_reset_cause();
printk("[nrf54l15] Reset cause: 0x%08x\n", reset_cause);
if (saved_crash.magic == CRASH_MAGIC) {
printk("[nrf54l15] Prev crash: reason=%u pc=0x%08x lr=0x%08x psp=0x%08x xpsr=0x%08x cfsr=0x%08x\n", saved_crash.reason,
saved_crash.pc, saved_crash.lr, saved_crash.psp, saved_crash.xpsr, saved_crash.cfsr);
saved_crash.magic = 0;
}
printk("[nrf54l15] A: main() entry\n");
printk("[nrf54l15] B: calling setup()\n");
setup();
printk("[nrf54l15] C: setup() returned\n");
while (true) {
loop();
}
return 0;
}
+11
View File
@@ -0,0 +1,11 @@
// utility/bonding.h — stub for nRF54L15/Zephyr
// NodeDB.cpp includes this when ARCH_NRF52 is defined.
// Bluetooth is excluded; this stub satisfies the include chain.
#pragma once
// BLE role constants (from Bluefruit SDK)
#define BLE_GAP_ROLE_PERIPH 0x01
#define BLE_GAP_ROLE_CENTRAL 0x02
// Stub for bond_print_list()
static inline void bond_print_list(uint8_t) {}
+4 -7
View File
@@ -4,8 +4,10 @@
#include "configuration.h"
#ifdef ARCH_ESP32
// "legacy adc calibration driver is deprecated, please migrate to use esp_adc/adc_cali.h and esp_adc/adc_cali_scheme.h
#include <esp_adc_cal.h>
// #include <driver/adc.h>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_cali_scheme.h>
#include <esp_adc/adc_oneshot.h>
#include <soc/adc_channel.h>
#endif
@@ -28,11 +30,6 @@
#define NUM_CELLS 1
#endif
#ifdef BAT_MEASURE_ADC_UNIT
extern RTC_NOINIT_ATTR uint64_t RTC_reg_b;
#include "soc/sens_reg.h" // needed for adc pin reset
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "modules/Telemetry/Sensor/nullSensor.h"
#if __has_include(<Adafruit_INA219.h>)
+30 -12
View File
@@ -18,8 +18,6 @@
#include "target_specific.h"
#ifdef ARCH_ESP32
// "esp_pm_config_esp32_t is deprecated, please include esp_pm.h and use esp_pm_config_t instead"
#include "esp32/pm.h"
#include "esp_pm.h"
#if HAS_WIFI
#include "mesh/wifi/WiFiAPClient.h"
@@ -146,15 +144,31 @@ void initDeepSleep()
// If we booted because our timer ran out or the user pressed reset, send those as fake events
RESET_REASON hwReason = rtc_get_reset_reason(0);
#ifdef CONFIG_IDF_TARGET_ESP32P4
if (hwReason == BROWN_OUT_RESET)
reason = "brownout";
else if (hwReason == HP_CORE_HP_WDT_RESET)
reason = "taskWatchdog";
else if (hwReason == HP_CORE_LP_WDT_RESET)
reason = "intWatchdog";
else if (hwReason == CHIP_LP_WDT_RESET)
reason = "chipWatchdog";
else if (hwReason == SUPER_WDT_RESET)
reason = "superWatchdog";
else if (hwReason == HP_SYS_HP_WDT_RESET)
reason = "systemWatchdog";
else if (hwReason == HP_SYS_LP_WDT_RESET)
reason = "systemLowPowerWatchdog";
#else
if (hwReason == RTCWDT_BROWN_OUT_RESET)
reason = "brownout";
if (hwReason == TG0WDT_SYS_RESET)
else if (hwReason == RTCWDT_RTC_RESET)
reason = "rtcWatchdog";
else if (hwReason == TG0WDT_SYS_RESET)
reason = "taskWatchdog";
if (hwReason == TG1WDT_SYS_RESET)
else if (hwReason == TG1WDT_SYS_RESET)
reason = "intWatchdog";
#endif
LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason);
#endif
@@ -397,8 +411,10 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
// NOTE! ESP docs say we must disable bluetooth and wifi before light sleep
#if SOC_PM_SUPPORT_RTC_PERIPH_PD
// We want RTC peripherals to stay on
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
#endif
#if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP)
gpio_pullup_en((gpio_num_t)BUTTON_PIN);
@@ -532,11 +548,7 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
*/
void enableModemSleep()
{
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
static esp_pm_config_t esp32_config; // filled with zeros because bss
#else
static esp_pm_config_esp32_t esp32_config; // filled with zeros because bss
#endif
#if CONFIG_IDF_TARGET_ESP32S3
esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ;
#elif CONFIG_IDF_TARGET_ESP32S2
@@ -545,6 +557,12 @@ void enableModemSleep()
esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ;
#elif CONFIG_IDF_TARGET_ESP32C3
esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ;
#elif CONFIG_IDF_TARGET_ESP32P4
#if CONFIG_ESP32P4_REV_MIN_FULL < 300
esp32_config.max_freq_mhz = 360;
#else
esp32_config.max_freq_mhz = 400;
#endif
#else
esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ;
#endif
@@ -562,8 +580,8 @@ bool shouldLoraWake(uint32_t msecToWake)
void enableLoraInterrupt()
{
esp_err_t res;
#if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC)
esp_err_t res;
res = gpio_pulldown_en((gpio_num_t)LORA_DIO1);
if (res != ESP_OK) {
LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res);
+2
View File
@@ -34,6 +34,8 @@
// "USERPREFS_CONFIG_GPS_UPDATE_INTERVAL": "600",
// "USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL": "1800",
// "USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL": "900", // Device telemetry update interval in seconds
// "USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL": "900", // Environment telemetry update interval in seconds
// "USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED": "1", // Force BMP280/sensor reads + LoRa broadcast on first boot
// "USERPREFS_LORACONFIG_CHANNEL_NUM": "31",
// "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST",
// "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }",
+1 -1
View File
@@ -70,7 +70,7 @@
// Battery
#define BATTERY_PIN 34 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
#define ADC_CHANNEL ADC1_GPIO34_CHANNEL
#define ADC_CHANNEL ADC_CHANNEL_6
#define ADC_ATTENUATION \
ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32
// ESP32-S2/C3/S3 are different
@@ -30,7 +30,7 @@
// Battery sense
#define BATTERY_PIN 35
#define ADC_MULTIPLIER 2.01 // 100k + 100k, and add 1% tolerance
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
#define ADC_CHANNEL ADC_CHANNEL_7
#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION
// SPI

Some files were not shown because too many files have changed in this diff Show More