Compare commits

...

43 Commits

Author SHA1 Message Date
Ben Meadors
dd5e0b74ba Added adaptive coding rate support and unit tests 2026-01-07 08:32:13 -06:00
santosvivos
9f5170a0bc Add LilyGO T-Beam 1W support (#8967)
* Add LilyGO T-Beam 1W support
- Add board definition and variant files for ESP32-S3 based T-Beam 1W
- Add RF95_FAN_EN support to SX126xInterface for PA cooling fan
- Add SX126X_PA_RAMP_US for configurable PA ramp time (800us for 1W PA)
- Configure RF switch: DIO2 for PA, GPIO 21 for LNA control

* Set TX_GAIN_LORA to 10dB per PR feedback (offset for 1W PA)

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-06 06:23:28 -06:00
Ben Meadors
e648e26c17 Merge pull request #9191 from meshtastic/bme-native
BME680 on Native
2026-01-05 20:55:44 -06:00
Jonathan Bennett
1669a027e6 BME680 on Native
Co-authored-by: juanjin-dev <juanjin.dev@gmail.com>
2026-01-05 19:33:41 -06:00
Ben Meadors
105d657359 Merge pull request #9189 from vidplace7/actions-feature-branches 2026-01-05 16:52:57 -06:00
Austin Lane
37ab800500 Actions: CI for feature/ branches
...and pioarduino
2026-01-05 17:44:07 -05:00
Sergey Galkin
0c553c40d4 Fix zero in sp02 and Heart Rate on screen (#9174)
Fixed zero in sp02 and Heart Rate in HealthTelemetry screen
2026-01-05 07:57:49 +11:00
Iris
17b075a11c added tcxo definition to mesh-tab (#8604) 2026-01-04 05:57:50 -06:00
Valentyn Diduryk
25bdefecb2 Fixed shouldFilterReceived function to check prev relay accoding to the function definition (#9168) 2026-01-04 05:22:26 -06:00
Jorropo
beb268ff25 Revert "add a .clang-format file (#9154)" (#9172)
I thought git would be smart enough to understand all the whitespace changes but even with all the flags I know to make it ignore theses it still blows up if there are identical changes on both sides.

I have a solution but it require creating a new commit at the merge base for each conflicting PR and merging it into develop.

I don't think blowing up all PRs is worth for now, maybe if we can coordinate this for V3 let's say.

This reverts commit 0d11331d18.
2026-01-04 05:15:53 -06:00
Jorropo
0d11331d18 add a .clang-format file (#9154) 2026-01-03 14:19:24 -06:00
Tom Fifield
abab6ce815 Fix link formatting in welcome message (#9163) 2026-01-03 06:00:23 -06:00
brad112358
52907e4c44 Faster rotary encoder events (#9146)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-02 20:22:40 -06:00
Jonathan Bennett
f63dadd19e Add custom coding rate configuration for LoRa (#9155) 2026-01-02 16:23:01 -06:00
Ben Meadors
9313d465f6 I think this is supposed to be extra 2026-01-02 15:58:54 -06:00
Jason P
004746683e Refactored some of the system menus to the new DRY method (Redux) (#9152)
* Refactored some of the system menus to the new DRY method

* Fix menu name from Position to GPS
2026-01-02 15:34:25 -06:00
github-actions[bot]
caceaf424a Automated version bumps (#9030)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-02 06:56:02 -06:00
Ben Meadors
75144d2028 Update security policy to reflect new stage 2026-01-02 06:42:28 -06:00
Ben Meadors
27b522b55a Merge branch 'master' into develop 2026-01-01 18:25:18 -06:00
renovate[bot]
11b5f1a4fe chore(deps): update dorny/test-reporter action to v2.4.0 (#9135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-01 18:04:13 -06:00
renovate[bot]
f9c9350f45 chore(deps): update meshtastic/device-ui digest to a8e2f94 (#9140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-01 18:03:27 -06:00
Jonathan Bennett
a5b2d4a9d5 Add null check for p_encrypted before MQTT publish (#9136)
* Add null check for p_encrypted before MQTT publish

A user on BayMesh observed a strange crash in MQTT::onSend that seemed to be a null pointer dereference of this value.

* Trunk
2026-01-01 13:53:36 -06:00
Ben Meadors
7fb95841e4 Apparently I marked board level extra on the wrong tbeam target 2026-01-01 08:25:33 -06:00
renovate[bot]
eaab8f04b5 chore(deps): update meshtastic/device-ui digest to 940ba85 (#9129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-01 10:58:56 +11:00
Eric Severance
9058ccecf9 Calculate hops correctly even when hop_start==0 (#9120)
* Calculate hops correctly even when hop_start==0.

* Use the same type (int8_t) in the loop, avoiding signed/unsigned mismatches.

* Clarify defaultIfUnknown is returned for encrypted packets.
2025-12-30 19:03:51 -06:00
Ben Meadors
1b83501ee2 Revert "Upgrade all esp32 targets to NimBLE 2.X (#9003)" (#9125)
This reverts commit 40f1f91c0d.
2025-12-30 17:23:50 -06:00
github-actions[bot]
ac571d5dd2 Upgrade trunk (#9121)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-30 07:10:36 -06:00
renovate[bot]
ef30fd850d Update meshtastic/device-ui digest to 7656d49 (#9111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 19:09:44 +01:00
renovate[bot]
b9a0015149 chore(deps): update meshtastic/device-ui digest to d234bd9 (#9108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 06:50:12 -06:00
github-actions[bot]
9673cfb0b2 Upgrade trunk (#9106)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-29 06:03:03 -06:00
renovate[bot]
757f7b68d6 Update meshtastic/device-ui digest to caff403 (#9104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 13:35:31 +11:00
Jason P
5510dae8d3 Implement HAS_PHYSICAL_KEYBOARD for devices with physical keyboards (#9071)
- Implement HAS_PHYSICAL_KEYBOARD for devices with physical keyboards
- Add HAS_PHYSICAL_KEYBOARD to variant.h for:
  - TDeck
  - TLora Pager
  - TDeck Pro
2025-12-27 06:53:55 -06:00
Tom
52fd362720 Fix gps pin defs for various NRF variants. (#9034)
* fix on nrf52_promicro

* try fix for GPS issue

* fix GPS pin assignment in variant.h

* cleared up some comments and confirmed pinouts from schematics

---------

Co-authored-by: macvenez <macvenez@gmail.com>
2025-12-27 06:50:07 -06:00
github-actions[bot]
33e1f58f6e Upgrade trunk (#9076)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-26 17:45:57 -06:00
Jonathan Bennett
9dc7ef612e In autoconf, don't probe Wire unless i2c device is set (#9081)
Found another bit of code that crashes my desktop, by probing the wrong i2c bus.
2025-12-26 14:33:17 -06:00
github-actions[bot]
b2c82bdc41 Upgrade trunk (#9072)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-25 06:34:38 -06:00
Jonathan Bennett
54a928f47f M6 shutdown and LEDs work (#9065)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-12-24 07:48:14 -06:00
github-actions[bot]
33f18659c8 Upgrade trunk (#9067)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-24 05:20:22 -06:00
github-actions[bot]
3a7093a973 Upgrade trunk (#9047)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-12-23 18:55:54 -06:00
renovate[bot]
a4f6f4515a Update meshtastic-esp8266-oled-ssd1306 digest to b34c681 (#9062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 18:55:37 -06:00
Jonathan Bennett
d609d05698 In statusLEDModule, also detect isCharging (#9050) 2025-12-23 07:48:55 -06:00
Ben Meadors
83c6161ac6 Revert "Automated version bumps (#9025)"
This reverts commit 1021d967da.
2025-12-20 14:10:02 -06:00
Ben Meadors
d93d68d31e Fix -ota.zip in manifest and build output 2025-12-20 14:09:05 -06:00
49 changed files with 1313 additions and 477 deletions

View File

@@ -22,7 +22,7 @@ jobs:
### @{fc-author}, Welcome to Meshtastic! :wave: ### @{fc-author}, Welcome to Meshtastic! :wave:
Thanks for opening your first issue. If it's helpful, an easy way Thanks for opening your first issue. If it's helpful, an easy way
to get logs is the "Open Serial Monitor" button on the (Web Flasher](https://flasher.meshtastic.org). to get logs is the "Open Serial Monitor" button on the [Web Flasher](https://flasher.meshtastic.org).
If you have ideas for features, note that we often debate big ideas If you have ideas for features, note that we often debate big ideas
in the [discussions tab](https://github.com/meshtastic/firmware/discussions/categories/ideas) in the [discussions tab](https://github.com/meshtastic/firmware/discussions/categories/ideas)

View File

@@ -8,7 +8,9 @@ on:
branches: branches:
- master - master
- develop - develop
- pioarduino # Remove when merged // use `feature/` in the future.
- event/* - event/*
- feature/*
paths-ignore: paths-ignore:
- "**.md" - "**.md"
- version.properties - version.properties
@@ -18,7 +20,9 @@ on:
branches: branches:
- master - master
- develop - develop
- pioarduino # Remove when merged // use `feature/` in the future.
- event/* - event/*
- feature/*
paths-ignore: paths-ignore:
- "**.md" - "**.md"
#- "**.yml" #- "**.yml"

View File

@@ -143,7 +143,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Test Report - name: Test Report
uses: dorny/test-reporter@v2.3.0 uses: dorny/test-reporter@v2.4.0
with: with:
name: PlatformIO Tests name: PlatformIO Tests
path: testreport.xml path: testreport.xml

View File

@@ -8,8 +8,8 @@ plugins:
uri: https://github.com/trunk-io/plugins uri: https://github.com/trunk-io/plugins
lint: lint:
enabled: enabled:
- checkov@3.2.495 - checkov@3.2.496
- renovate@42.66.8 - renovate@42.66.14
- prettier@3.7.4 - prettier@3.7.4
- trufflehog@3.92.4 - trufflehog@3.92.4
- yamllint@1.37.1 - yamllint@1.37.1

43
Dockerfile.test Normal file
View File

@@ -0,0 +1,43 @@
# Minimal container to run PlatformIO native/portduino tests
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3 \
python3-pip \
git \
build-essential \
cmake \
pkg-config \
libssl-dev \
libncurses5 \
libsdl2-dev \
libx11-dev \
libxext-dev \
libusb-1.0-0-dev \
libyaml-cpp-dev \
libuv1-dev \
libgpiod-dev \
libbluetooth-dev \
libulfius-dev \
liborcania-dev \
libmicrohttpd-dev \
libjansson-dev \
libgnutls28-dev \
libcurl4-gnutls-dev \
libi2c-dev \
openssl \
lsb-release \
cppcheck \
uuid-dev \
zlib1g-dev \
libbsd-dev \
gdb \
&& pip3 install --no-cache-dir platformio \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
CMD ["bash"]

View File

@@ -4,8 +4,8 @@
| Firmware Version | Supported | | Firmware Version | Supported |
| ---------------- | ------------------ | | ---------------- | ------------------ |
| 2.6.x | :white_check_mark: | | 2.7.x | :white_check_mark: |
| <= 2.5.x | :x: | | <= 2.6.x | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@@ -87,6 +87,9 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="2.7.18" date="2026-01-02">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18</url>
</release>
<release version="2.7.17" date="2025-11-28"> <release version="2.7.17" date="2025-11-28">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17</url>
</release> </release>

50
boards/t-beam-1w.json Normal file
View File

@@ -0,0 +1,50 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DLILYGO_TBEAM_1W",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
[
"0x303A",
"0x1001"
]
],
"mcu": "esp32s3",
"variant": "t-beam-1w"
},
"connectivity": [
"wifi",
"bluetooth",
"lora"
],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino"
],
"name": "LilyGo TBeam-1W",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "http://www.lilygo.cn/",
"vendor": "LilyGo"
}

6
debian/changelog vendored
View File

@@ -1,3 +1,9 @@
meshtasticd (2.7.18.0) unstable; urgency=medium
* Version 2.7.18
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Fri, 02 Jan 2026 12:45:36 +0000
meshtasticd (2.7.17.0) unstable; urgency=medium meshtasticd (2.7.17.0) unstable; urgency=medium
* Version 2.7.17 * Version 2.7.17

View File

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

View File

@@ -24,6 +24,9 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
switch (event->inputEvent) { switch (event->inputEvent) {
case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_USER_PRESS:
case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_ALT_PRESS:
playClick(); // Low delay feedback
break;
case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT:
case INPUT_BROKER_SELECT_LONG: case INPUT_BROKER_SELECT_LONG:
playBeep(); // Confirmation feedback playBeep(); // Confirmation feedback
@@ -58,4 +61,4 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
} }
return 0; // Allow other handlers to process the event return 0; // Allow other handlers to process the event
} }

View File

@@ -113,7 +113,14 @@ void playShutdownMelody()
void playChirp() void playChirp()
{ {
// A short, friendly "chirp" sound for key presses // A short, friendly "chirp" sound for key presses
ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
void playClick()
{
// A very short "click" sound with minimum delay; ideal for rotary encoder events
ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3
playTones(melody, sizeof(melody) / sizeof(ToneDuration)); playTones(melody, sizeof(melody) / sizeof(ToneDuration));
} }

View File

@@ -9,6 +9,7 @@ void playGPSDisableBeep();
void playComboTune(); void playComboTune();
void playBoop(); void playBoop();
void playChirp(); void playChirp();
void playClick();
void playLongPressLeadUp(); void playLongPressLeadUp();
bool playNextLeadUpNote(); // Play the next note in the lead-up sequence bool playNextLeadUpNote(); // Play the next note in the lead-up sequence
void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning

View File

@@ -444,6 +444,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif #endif
#endif #endif
// BME680 BSEC2 support detection
#if !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
#if defined(RAK_4631) || defined(TBEAM_V10)
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 1
#define MESHTASTIC_BME680_HEADER <bsec2.h>
#else
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 0
#define MESHTASTIC_BME680_HEADER <Adafruit_BME680.h>
#endif // defined(RAK_4631)
#endif // !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Global switches to turn off features for a minimized build // Global switches to turn off features for a minimized build
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View File

@@ -107,50 +107,60 @@ void menuHandler::OnboardMessage()
void menuHandler::LoraRegionPicker(uint32_t duration) void menuHandler::LoraRegionPicker(uint32_t duration)
{ {
static const char *optionsArray[] = {"Back", static const LoraRegionOption regionOptions[] = {
"US", {"Back", OptionsAction::Back},
"EU_433", {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
"EU_868", {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433},
"CN", {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868},
"JP", {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN},
"ANZ", {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP},
"KR", {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ},
"TW", {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR},
"RU", {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW},
"IN", {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU},
"NZ_865", {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN},
"TH", {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865},
"LORA_24", {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH},
"UA_433", {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24},
"UA_868", {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433},
"MY_433", {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868},
"MY_" {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433},
"919", {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919},
"SG_" {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923},
"923", {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433},
"PH_433", {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868},
"PH_868", {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915},
"PH_915", {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433},
"ANZ_433", {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433},
"KZ_433", {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863},
"KZ_863", {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865},
"NP_865", {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902},
"BR_902"}; };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Set the LoRa region"; constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
static std::array<const char *, regionCount> regionLabels{};
const char *bannerMessage = "Set the LoRa region";
if (currentResolution == ScreenResolution::UltraLow) { if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "LoRa Region"; bannerMessage = "LoRa Region";
} }
bannerOptions.durationMs = duration;
bannerOptions.optionsArrayPtr = optionsArray; auto bannerOptions =
bannerOptions.optionsCount = 27; createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void {
bannerOptions.InitialSelected = 0; if (!option.hasValue) {
bannerOptions.bannerCallback = [](int selected) -> void { return;
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { }
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
auto selectedRegion = option.value;
if (config.lora.region == selectedRegion) {
return;
}
config.lora.region = selectedRegion;
auto changes = SEGMENT_CONFIG; auto changes = SEGMENT_CONFIG;
// This is needed as we wait til picking the LoRa region to generate keys for the first time. // FIXME: This should be a method consolidated with the same logic in the admin message as well
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
if (!owner.is_licensed) { if (!owner.is_licensed) {
bool keygenSuccess = false; bool keygenSuccess = false;
@@ -187,8 +197,19 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
service->reloadConfig(changes); service->reloadConfig(changes);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
});
bannerOptions.durationMs = duration;
int initialSelection = 0;
for (size_t i = 0; i < regionCount; ++i) {
if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) {
initialSelection = static_cast<int>(i);
break;
} }
}; }
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
@@ -303,102 +324,100 @@ void menuHandler::showConfirmationBanner(const char *message, std::function<void
void menuHandler::ClockFacePicker() void menuHandler::ClockFacePicker()
{ {
static const char *optionsArray[] = {"Back", "Digital", "Analog"}; static const ClockFaceOption clockFaceOptions[] = {
enum optionsNumbers { Back = 0, Digital = 1, Analog = 2 }; {"Back", OptionsAction::Back},
BannerOverlayOptions bannerOptions; {"Digital", OptionsAction::Select, false},
bannerOptions.message = "Which Face?"; {"Analog", OptionsAction::Select, true},
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
screen->runNow();
} else if (selected == Digital) {
uiconfig.is_clockface_analog = false;
saveUIConfig();
screen->setFrames(Screen::FOCUS_CLOCK);
} else {
uiconfig.is_clockface_analog = true;
saveUIConfig();
screen->setFrames(Screen::FOCUS_CLOCK);
}
}; };
constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]);
static std::array<const char *, clockFaceCount> clockFaceLabels{};
auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
[](const ClockFaceOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::clock_menu;
screen->runNow();
return;
}
if (!option.hasValue) {
return;
}
if (uiconfig.is_clockface_analog == option.value) {
return;
}
uiconfig.is_clockface_analog = option.value;
saveUIConfig();
screen->setFrames(Screen::FOCUS_CLOCK);
});
bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::TZPicker() void menuHandler::TZPicker()
{ {
static const char *optionsArray[] = {"Back", static const TimezoneOption timezoneOptions[] = {
"US/Hawaii", {"Back", OptionsAction::Back},
"US/Alaska", {"US/Hawaii", OptionsAction::Select, "HST10"},
"US/Pacific", {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"},
"US/Arizona", {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"},
"US/Mountain", {"US/Arizona", OptionsAction::Select, "MST7"},
"US/Central", {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"},
"US/Eastern", {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"},
"BR/Brasilia", {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"},
"UTC", {"BR/Brasilia", OptionsAction::Select, "BRT3"},
"EU/Western", {"UTC", OptionsAction::Select, "UTC0"},
"EU/" {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"},
"Central", {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"},
"EU/Eastern", {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"},
"Asia/Kolkata", {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"},
"Asia/Hong_Kong", {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"},
"AU/AWST", {"AU/AWST", OptionsAction::Select, "AWST-8"},
"AU/ACST", {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"},
"AU/AEST", {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"},
"Pacific/NZ"}; {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"},
BannerOverlayOptions bannerOptions; };
bannerOptions.message = "Pick Timezone";
bannerOptions.optionsArrayPtr = optionsArray; constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]);
bannerOptions.optionsCount = 19; static std::array<const char *, timezoneCount> timezoneLabels{};
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) { auto bannerOptions = createStaticBannerOptions(
menuHandler::menuQueue = menuHandler::clock_menu; "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
screen->runNow(); if (option.action == OptionsAction::Back) {
} else if (selected == 1) { // Hawaii menuHandler::menuQueue = menuHandler::clock_menu;
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); screen->runNow();
} else if (selected == 2) { // Alaska return;
strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); }
} else if (selected == 3) { // Pacific
strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); if (!option.hasValue) {
} else if (selected == 4) { // Arizona return;
strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef)); }
} else if (selected == 5) { // Mountain
strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) {
} else if (selected == 6) { // Central return;
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); }
} else if (selected == 7) { // Eastern
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef));
} else if (selected == 8) { // Brazil config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0';
strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef));
} else if (selected == 9) { // UTC
strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef));
} else if (selected == 10) { // EU/Western
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
} else if (selected == 11) { // EU/Central
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
} else if (selected == 12) { // EU/Eastern
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
} else if (selected == 13) { // Asia/Kolkata
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
} else if (selected == 14) { // China
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
} else if (selected == 15) { // AU/AWST
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
} else if (selected == 16) { // AU/ACST
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
} else if (selected == 17) { // AU/AEST
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
} else if (selected == 18) { // NZ
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
}
if (selected != 0) {
setenv("TZ", config.device.tzdef, 1); setenv("TZ", config.device.tzdef, 1);
service->reloadConfig(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG);
});
int initialSelection = 0;
for (size_t i = 0; i < timezoneCount; ++i) {
if (timezoneOptions[i].hasValue &&
strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) {
initialSelection = static_cast<int>(i);
break;
} }
}; }
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
@@ -458,10 +477,9 @@ void menuHandler::messageResponseMenu()
#endif #endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Message Action";
if (currentResolution == ScreenResolution::UltraLow) { if (currentResolution == ScreenResolution::UltraLow) {
bannerOptions.message = "Message"; bannerOptions.message = "Message";
} else {
bannerOptions.message = "Message Action";
} }
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -910,8 +928,12 @@ void menuHandler::homeBaseMenu()
} else if (selected == Sleep) { } else if (selected == Sleep) {
screen->setOn(false); screen->setOn(false);
} else if (selected == Position) { } else if (selected == Position) {
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SEND_PING, .kbchar = 0, .touchX = 0, .touchY = 0}; service->refreshLocalMeshNode();
inputBroker->injectInputEvent(&event); if (service->trySendPosition(NODENUM_BROADCAST, true)) {
IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000));
} else {
IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000));
}
} else if (selected == Preset) { } else if (selected == Preset) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (selected == Freetext) { } else if (selected == Freetext) {
@@ -1108,57 +1130,92 @@ void menuHandler::favoriteBaseMenu()
void menuHandler::positionBaseMenu() void menuHandler::positionBaseMenu()
{ {
enum optionsNumbers { enum class PositionAction {
Back, GpsToggle,
GPSToggle, GpsFormat,
GPSFormat,
CompassMenu, CompassMenu,
CompassCalibrate, CompassCalibrate,
GPSSmartPosition, GPSSmartPosition,
GPSUpdateInterval, GPSUpdateInterval,
GPSPositionBroadcast, GPSPositionBroadcast
enumEnd
}; };
static const char *optionsArray[enumEnd] = { static const PositionMenuOption baseOptions[] = {
"Back", "On/Off Toggle", "Format", "Smart Position", "Update Interval", "Broadcast Interval", "Compass"}; {"Back", OptionsAction::Back},
static int optionsEnumArray[enumEnd] = { {"On/Off Toggle", OptionsAction::Select, static_cast<int>(PositionAction::GpsToggle)},
Back, GPSToggle, GPSFormat, GPSSmartPosition, GPSUpdateInterval, GPSPositionBroadcast, CompassMenu}; {"Format", OptionsAction::Select, static_cast<int>(PositionAction::GpsFormat)},
int options = 7; {"Smart Position", OptionsAction::Select, static_cast<int>(PositionAction::GPSSmartPosition)},
{"Update Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSUpdateInterval)},
{"Broadcast Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSPositionBroadcast)},
{"Compass", OptionsAction::Select, static_cast<int>(PositionAction::CompassMenu)},
};
if (accelerometerThread) { static const PositionMenuOption calibrateOptions[] = {
optionsArray[options] = "Compass Calibrate"; {"Back", OptionsAction::Back},
optionsEnumArray[options++] = CompassCalibrate; {"On/Off Toggle", OptionsAction::Select, static_cast<int>(PositionAction::GpsToggle)},
} {"Format", OptionsAction::Select, static_cast<int>(PositionAction::GpsFormat)},
{"Smart Position", OptionsAction::Select, static_cast<int>(PositionAction::GPSSmartPosition)},
{"Update Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSUpdateInterval)},
{"Broadcast Interval", OptionsAction::Select, static_cast<int>(PositionAction::GPSPositionBroadcast)},
{"Compass", OptionsAction::Select, static_cast<int>(PositionAction::CompassMenu)},
{"Compass Calibrate", OptionsAction::Select, static_cast<int>(PositionAction::CompassCalibrate)},
};
BannerOverlayOptions bannerOptions; constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]);
bannerOptions.message = "GPS Action"; constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]);
bannerOptions.optionsArrayPtr = optionsArray; static std::array<const char *, baseCount> baseLabels{};
bannerOptions.optionsEnumPtr = optionsEnumArray; static std::array<const char *, calibrateCount> calibrateLabels{};
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void { auto onSelection = [](const PositionMenuOption &option, int) -> void {
if (selected == GPSToggle) { if (option.action == OptionsAction::Back) {
return;
}
if (!option.hasValue) {
return;
}
auto action = static_cast<PositionAction>(option.value);
switch (action) {
case PositionAction::GpsToggle:
menuQueue = gps_toggle_menu; menuQueue = gps_toggle_menu;
screen->runNow(); screen->runNow();
} else if (selected == GPSFormat) { break;
case PositionAction::GpsFormat:
menuQueue = gps_format_menu; menuQueue = gps_format_menu;
screen->runNow(); screen->runNow();
} else if (selected == CompassMenu) { break;
case PositionAction::CompassMenu:
menuQueue = compass_point_north_menu; menuQueue = compass_point_north_menu;
screen->runNow(); screen->runNow();
} else if (selected == CompassCalibrate) { break;
accelerometerThread->calibrate(30); case PositionAction::CompassCalibrate:
} else if (selected == GPSSmartPosition) { if (accelerometerThread) {
accelerometerThread->calibrate(30);
}
break;
case PositionAction::GPSSmartPosition:
menuQueue = gps_smart_position_menu; menuQueue = gps_smart_position_menu;
screen->runNow(); screen->runNow();
} else if (selected == GPSUpdateInterval) { break;
case PositionAction::GPSUpdateInterval:
menuQueue = gps_update_interval_menu; menuQueue = gps_update_interval_menu;
screen->runNow(); screen->runNow();
} else if (selected == GPSPositionBroadcast) { break;
case PositionAction::GPSPositionBroadcast:
menuQueue = gps_position_broadcast_menu; menuQueue = gps_position_broadcast_menu;
screen->runNow(); screen->runNow();
break;
} }
}; };
BannerOverlayOptions bannerOptions;
if (accelerometerThread) {
bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection);
} else {
bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection);
}
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
@@ -1214,27 +1271,38 @@ void menuHandler::nodeListMenu()
void menuHandler::nodeNameLengthMenu() void menuHandler::nodeNameLengthMenu()
{ {
enum OptionsNumbers { Back, Long, Short }; static const NodeNameOption nodeNameOptions[] = {
static const char *optionsArray[] = {"Back", "Long", "Short"}; {"Back", OptionsAction::Back},
BannerOverlayOptions bannerOptions; {"Long", OptionsAction::Select, true},
bannerOptions.message = "Node Name Length"; {"Short", OptionsAction::Select, false},
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Long) {
// Set names to long
LOG_INFO("Setting names to long");
config.display.use_long_node_name = true;
} else if (selected == Short) {
// Set names to short
LOG_INFO("Setting names to short");
config.display.use_long_node_name = false;
} else if (selected == Back) {
menuQueue = node_base_menu;
screen->runNow();
}
}; };
bannerOptions.InitialSelected = config.display.use_long_node_name == true ? 1 : 2;
constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]);
static std::array<const char *, nodeNameCount> nodeNameLabels{};
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
[](const NodeNameOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = node_base_menu;
screen->runNow();
return;
}
if (!option.hasValue) {
return;
}
if (config.display.use_long_node_name == option.value) {
return;
}
config.display.use_long_node_name = option.value;
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
});
int initialSelection = config.display.use_long_node_name ? 1 : 2;
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
@@ -1268,119 +1336,169 @@ void menuHandler::resetNodeDBMenu()
void menuHandler::compassNorthMenu() void menuHandler::compassNorthMenu()
{ {
enum optionsNumbers { Back, Dynamic, Fixed, Freeze }; static const CompassOption compassOptions[] = {
static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; {"Back", OptionsAction::Back},
BannerOverlayOptions bannerOptions; {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC},
bannerOptions.message = "North Directions?"; {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING},
bannerOptions.optionsArrayPtr = optionsArray; {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING},
bannerOptions.optionsCount = 4;
bannerOptions.InitialSelected = uiconfig.compass_mode + 1;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Dynamic) {
if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) {
uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC;
saveUIConfig();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
} else if (selected == Fixed) {
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING;
saveUIConfig();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
} else if (selected == Freeze) {
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING;
saveUIConfig();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
} else if (selected == Back) {
menuQueue = position_base_menu;
screen->runNow();
}
}; };
constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]);
static std::array<const char *, compassCount> compassLabels{};
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
[](const CompassOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
screen->runNow();
return;
}
if (!option.hasValue) {
return;
}
if (uiconfig.compass_mode == option.value) {
return;
}
uiconfig.compass_mode = option.value;
saveUIConfig();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
});
int initialSelection = 0;
for (size_t i = 0; i < compassCount; ++i) {
if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) {
initialSelection = static_cast<int>(i);
break;
}
}
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
#if !MESHTASTIC_EXCLUDE_GPS #if !MESHTASTIC_EXCLUDE_GPS
void menuHandler::GPSToggleMenu() void menuHandler::GPSToggleMenu()
{ {
static const GPSToggleOption gpsToggleOptions[] = {
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; {"Back", OptionsAction::Back},
BannerOverlayOptions bannerOptions; {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED},
bannerOptions.message = "Toggle GPS"; {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED},
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
playGPSEnableBeep();
gps->enable();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 2) {
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
playGPSDisableBeep();
gps->disable();
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuQueue = position_base_menu;
screen->runNow();
}
}; };
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]);
static std::array<const char *, toggleCount> toggleLabels{};
auto bannerOptions =
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = position_base_menu;
screen->runNow();
return;
}
if (!option.hasValue) {
return;
}
if (config.position.gps_mode == option.value) {
return;
}
config.position.gps_mode = option.value;
if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
playGPSEnableBeep();
gps->enable();
} else {
playGPSDisableBeep();
gps->disable();
}
service->reloadConfig(SEGMENT_CONFIG);
});
int initialSelection = 0;
for (size_t i = 0; i < toggleCount; ++i) {
if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) {
initialSelection = static_cast<int>(i);
break;
}
}
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::GPSFormatMenu() void menuHandler::GPSFormatMenu()
{ {
static const GPSFormatOption formatOptionsHigh[] = {
{"Back", OptionsAction::Back},
{"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC},
{"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS},
{"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM},
{"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS},
{"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC},
{"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR},
{"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS},
};
static const char *optionsArray[] = {"Back", static const GPSFormatOption formatOptionsLow[] = {
(currentResolution == ScreenResolution::High) ? "Decimal Degrees" : "DEC", {"Back", OptionsAction::Back},
(currentResolution == ScreenResolution::High) ? "Degrees Minutes Seconds" : "DMS", {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC},
(currentResolution == ScreenResolution::High) ? "Universal Transverse Mercator" : "UTM", {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS},
(currentResolution == ScreenResolution::High) ? "Military Grid Reference System" {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM},
: "MGRS", {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS},
(currentResolution == ScreenResolution::High) ? "Open Location Code" : "OLC", {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC},
(currentResolution == ScreenResolution::High) ? "Ordnance Survey Grid Ref" : "OSGR", {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR},
(currentResolution == ScreenResolution::High) ? "Maidenhead Locator" : "MLS"}; {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS},
BannerOverlayOptions bannerOptions; };
bannerOptions.message = "GPS Format";
bannerOptions.optionsArrayPtr = optionsArray; constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]);
bannerOptions.optionsCount = 8; static std::array<const char *, formatCount> formatLabelsHigh{};
bannerOptions.bannerCallback = [](int selected) -> void { static std::array<const char *, formatCount> formatLabelsLow{};
if (selected == 1) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC; auto onSelection = [](const GPSFormatOption &option, int) -> void {
saveUIConfig(); if (option.action == OptionsAction::Back) {
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 2) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 3) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 4) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 5) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 6) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 7) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuQueue = position_base_menu; menuQueue = position_base_menu;
screen->runNow(); screen->runNow();
return;
} }
if (!option.hasValue) {
return;
}
if (uiconfig.gps_format == option.value) {
return;
}
uiconfig.gps_format = option.value;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
}; };
bannerOptions.InitialSelected = uiconfig.gps_format + 1;
BannerOverlayOptions bannerOptions;
int initialSelection = 0;
if (currentResolution == ScreenResolution::High) {
bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection);
for (size_t i = 0; i < formatCount; ++i) {
if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) {
initialSelection = static_cast<int>(i);
break;
}
}
} else {
bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection);
for (size_t i = 0; i < formatCount; ++i) {
if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) {
initialSelection = static_cast<int>(i);
break;
}
}
}
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
@@ -1701,100 +1819,63 @@ void menuHandler::switchToMUIMenu()
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
{ {
static const char *optionsArray[] = { static const ScreenColorOption colorOptions[] = {
"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Blue", "Teal", "Cyan", "Ice", "Pink", {"Back", OptionsAction::Back},
"White", "Gray"}; {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)},
BannerOverlayOptions bannerOptions; {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)},
bannerOptions.message = "Select Screen Color"; {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)},
bannerOptions.optionsArrayPtr = optionsArray; {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)},
bannerOptions.optionsCount = 14; {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)},
bannerOptions.bannerCallback = [display](int selected) -> void { {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)},
{"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)},
{"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)},
{"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)},
{"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)},
{"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)},
{"White", OptionsAction::Select, ScreenColor(255, 255, 255)},
{"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)},
};
constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]);
static std::array<const char *, colorCount> colorLabels{};
auto bannerOptions = createStaticBannerOptions(
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = system_base_menu;
screen->runNow();
return;
}
if (!option.hasValue) {
return;
}
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
HAS_TFT || defined(HACKADAY_COMMUNICATOR) HAS_TFT || defined(HACKADAY_COMMUNICATOR)
uint8_t TFT_MESH_r = 0; const ScreenColor &color = option.value;
uint8_t TFT_MESH_g = 0; if (color.useVariant) {
uint8_t TFT_MESH_b = 0; LOG_INFO("Setting color to system default or defined variant");
if (selected == 1) { } else {
LOG_INFO("Setting color to system default or defined variant"); LOG_INFO("Setting color to %s", option.label);
// Given just before we set all these to zero, we will allow this to go through }
} else if (selected == 2) {
LOG_INFO("Setting color to Meshtastic Green"); uint8_t r = color.r;
TFT_MESH_r = 103; uint8_t g = color.g;
TFT_MESH_g = 234; uint8_t b = color.b;
TFT_MESH_b = 148;
} else if (selected == 3) {
LOG_INFO("Setting color to Yellow");
TFT_MESH_r = 255;
TFT_MESH_g = 255;
TFT_MESH_b = 128;
} else if (selected == 4) {
LOG_INFO("Setting color to Red");
TFT_MESH_r = 255;
TFT_MESH_g = 64;
TFT_MESH_b = 64;
} else if (selected == 5) {
LOG_INFO("Setting color to Orange");
TFT_MESH_r = 255;
TFT_MESH_g = 160;
TFT_MESH_b = 20;
} else if (selected == 6) {
LOG_INFO("Setting color to Purple");
TFT_MESH_r = 204;
TFT_MESH_g = 153;
TFT_MESH_b = 255;
} else if (selected == 7) {
LOG_INFO("Setting color to Blue");
TFT_MESH_r = 0;
TFT_MESH_g = 0;
TFT_MESH_b = 255;
} else if (selected == 8) {
LOG_INFO("Setting color to Teal");
TFT_MESH_r = 16;
TFT_MESH_g = 102;
TFT_MESH_b = 102;
} else if (selected == 9) {
LOG_INFO("Setting color to Cyan");
TFT_MESH_r = 0;
TFT_MESH_g = 255;
TFT_MESH_b = 255;
} else if (selected == 10) {
LOG_INFO("Setting color to Ice");
TFT_MESH_r = 173;
TFT_MESH_g = 216;
TFT_MESH_b = 230;
} else if (selected == 11) {
LOG_INFO("Setting color to Pink");
TFT_MESH_r = 255;
TFT_MESH_g = 105;
TFT_MESH_b = 180;
} else if (selected == 12) {
LOG_INFO("Setting color to White");
TFT_MESH_r = 255;
TFT_MESH_g = 255;
TFT_MESH_b = 255;
} else if (selected == 13) {
LOG_INFO("Setting color to Gray");
TFT_MESH_r = 128;
TFT_MESH_g = 128;
TFT_MESH_b = 128;
} else {
menuQueue = system_base_menu;
screen->runNow();
}
if (selected != 0) {
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
display->setColor(WHITE); display->setColor(WHITE);
if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
#ifdef TFT_MESH_OVERRIDE #ifdef TFT_MESH_OVERRIDE
TFT_MESH = TFT_MESH_OVERRIDE; TFT_MESH = TFT_MESH_OVERRIDE;
#else #else
TFT_MESH = COLOR565(0x67, 0xEA, 0x94); TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
#endif #endif
} else { } else {
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); TFT_MESH = COLOR565(r, g, b);
} }
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
@@ -1802,16 +1883,40 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
#endif #endif
screen->setFrames(graphics::Screen::FOCUS_SYSTEM); screen->setFrames(graphics::Screen::FOCUS_SYSTEM);
if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
uiconfig.screen_rgb_color = 0; uiconfig.screen_rgb_color = 0;
} else { } else {
uiconfig.screen_rgb_color = (TFT_MESH_r << 16) | (TFT_MESH_g << 8) | TFT_MESH_b; uiconfig.screen_rgb_color =
(static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
} }
LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color);
saveUIConfig(); saveUIConfig();
}
#endif #endif
}; });
int initialSelection = 0;
if (uiconfig.screen_rgb_color == 0) {
initialSelection = 1;
} else {
uint32_t currentColor = uiconfig.screen_rgb_color;
for (size_t i = 0; i < colorCount; ++i) {
if (!colorOptions[i].hasValue) {
continue;
}
const ScreenColor &color = colorOptions[i].value;
if (color.useVariant) {
continue;
}
uint32_t encoded =
(static_cast<uint32_t>(color.r) << 16) | (static_cast<uint32_t>(color.g) << 8) | static_cast<uint32_t>(color.b);
if (encoded == currentColor) {
initialSelection = static_cast<int>(i);
break;
}
}
}
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }

View File

@@ -128,7 +128,28 @@ template <typename T> struct MenuOption {
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
}; };
struct ScreenColor {
uint8_t r;
uint8_t g;
uint8_t b;
bool useVariant;
ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
{
}
};
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>; using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
using LoraRegionOption = MenuOption<meshtastic_Config_LoRaConfig_RegionCode>;
using TimezoneOption = MenuOption<const char *>;
using CompassOption = MenuOption<meshtastic_CompassMode>;
using ScreenColorOption = MenuOption<ScreenColor>;
using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
using NodeNameOption = MenuOption<bool>;
using PositionMenuOption = MenuOption<int>;
using ClockFaceOption = MenuOption<bool>;
} // namespace graphics } // namespace graphics
#endif #endif

View File

@@ -238,6 +238,12 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
// call to startRetransmission. // call to startRetransmission.
packetPool.release(p); packetPool.release(p);
#ifdef USE_ADAPTIVE_CODING_RATE
if (iface) {
iface->clearAdaptiveCodingRateState(getFrom(p), p->id);
}
#endif
return true; return true;
} else } else
return false; return false;

View File

@@ -520,6 +520,10 @@ void RadioInterface::applyModemConfig()
sf = 12; sf = 12;
break; break;
} }
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) {
cr = loraConfig.coding_rate;
LOG_INFO("Using custom Coding Rate %u", cr);
}
} else { } else {
sf = loraConfig.spread_factor; sf = loraConfig.spread_factor;
cr = loraConfig.coding_rate; cr = loraConfig.coding_rate;
@@ -614,6 +618,13 @@ void RadioInterface::applyModemConfig()
slotTimeMsec = computeSlotTimeMsec(); slotTimeMsec = computeSlotTimeMsec();
preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw);
#ifdef USE_ADAPTIVE_CODING_RATE
if (adaptiveCrOverride >= 5 && adaptiveCrOverride <= 8 && cr != adaptiveCrOverride) {
cr = adaptiveCrOverride;
LOG_DEBUG("Adaptive coding rate override set to %u", cr);
}
#endif
LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset);
LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset, LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset,
channel_num, power); channel_num, power);
@@ -726,3 +737,70 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
sendingPacket = p; sendingPacket = p;
return p->encrypted.size + sizeof(PacketHeader); return p->encrypted.size + sizeof(PacketHeader);
} }
#ifdef USE_ADAPTIVE_CODING_RATE
uint8_t RadioInterface::computeAdaptiveCodingRate(uint8_t attempt) const
{
if (attempt <= 1) {
return 5; // Attempt 1: 4/5
}
if (attempt == 2) {
return 7; // Attempt 2: 4/7
}
return 8; // Attempt 3+: 4/8
}
uint64_t RadioInterface::adaptiveKey(NodeNum from, PacketId id) const
{
return (static_cast<uint64_t>(from) << 32) | id;
}
void RadioInterface::pruneAdaptiveAttempts(uint32_t now)
{
const uint32_t expiryMsec = 5 * 60 * 1000UL; // drop state after 5 minutes
for (auto it = adaptiveAttempts.begin(); it != adaptiveAttempts.end();) {
if (now - it->second.lastUseMsec > expiryMsec) {
it = adaptiveAttempts.erase(it);
} else {
++it;
}
}
}
uint8_t RadioInterface::recordAdaptiveAttempt(const meshtastic_MeshPacket *p)
{
const uint32_t now = millis();
pruneAdaptiveAttempts(now);
const uint64_t key = adaptiveKey(getFrom(p), p->id);
auto &state = adaptiveAttempts[key];
if (state.attempts < UINT8_MAX) {
state.attempts++;
}
state.lastUseMsec = now;
return state.attempts;
}
bool RadioInterface::applyAdaptiveCodingRate(const meshtastic_MeshPacket *p)
{
const uint8_t attempt = recordAdaptiveAttempt(p);
const uint8_t desiredCr = computeAdaptiveCodingRate(attempt);
if (desiredCr < 5 || desiredCr > 8) {
return false;
}
adaptiveCrOverride = desiredCr;
if (cr != desiredCr) {
cr = desiredCr;
reconfigure();
return true;
}
return false;
}
void RadioInterface::clearAdaptiveCodingRateState(NodeNum from, PacketId id)
{
adaptiveAttempts.erase(adaptiveKey(from, id));
}
#endif

View File

@@ -6,6 +6,9 @@
#include "PointerQueue.h" #include "PointerQueue.h"
#include "airtime.h" #include "airtime.h"
#include "error.h" #include "error.h"
#ifdef USE_ADAPTIVE_CODING_RATE
#include <unordered_map>
#endif
#define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission #define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission
@@ -220,6 +223,11 @@ class RadioInterface
// Whether we use the default frequency slot given our LoRa config (region and modem preset) // Whether we use the default frequency slot given our LoRa config (region and modem preset)
static bool uses_default_frequency_slot; static bool uses_default_frequency_slot;
#ifdef USE_ADAPTIVE_CODING_RATE
/** Clear adaptive coding rate tracking for a completed packet id */
void clearAdaptiveCodingRateState(NodeNum from, PacketId id);
#endif
protected: protected:
int8_t power = 17; // Set by applyModemConfig() int8_t power = 17; // Set by applyModemConfig()
@@ -250,6 +258,20 @@ class RadioInterface
*/ */
virtual void saveChannelNum(uint32_t savedChannelNum); virtual void saveChannelNum(uint32_t savedChannelNum);
#ifdef USE_ADAPTIVE_CODING_RATE
bool applyAdaptiveCodingRate(const meshtastic_MeshPacket *p);
struct AdaptiveAttemptState {
uint8_t attempts = 0;
uint32_t lastUseMsec = 0;
};
std::unordered_map<uint64_t, AdaptiveAttemptState> adaptiveAttempts;
uint8_t adaptiveCrOverride = 0;
uint8_t recordAdaptiveAttempt(const meshtastic_MeshPacket *p);
uint8_t computeAdaptiveCodingRate(uint8_t attempt) const;
void pruneAdaptiveAttempts(uint32_t now);
uint64_t adaptiveKey(NodeNum from, PacketId id) const;
#endif
private: private:
/** /**
* Convert our modemConfig enum into wf, sf, etc... * Convert our modemConfig enum into wf, sf, etc...

View File

@@ -540,6 +540,9 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
packetPool.release(txp); packetPool.release(txp);
return false; return false;
} else { } else {
#ifdef USE_ADAPTIVE_CODING_RATE
applyAdaptiveCodingRate(txp);
#endif
configHardwareForSend(); // must be after setStandby configHardwareForSend(); // must be after setStandby
size_t numbytes = beginSending(txp); size_t numbytes = beginSending(txp);

View File

@@ -113,7 +113,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
// Check 3: role check (moderate cost - multiple comparisons) // Check 3: role check (moderate cost - multiple comparisons)
if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER,
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
continue; continue;
} }
@@ -745,15 +745,19 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
MeshModule::callModules(*p, src); MeshModule::callModules(*p, src);
#if !MESHTASTIC_EXCLUDE_MQTT #if !MESHTASTIC_EXCLUDE_MQTT
// Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not to if (p_encrypted == nullptr) {
// us (because we would be able to decrypt it) LOG_WARN("p_encrypted is null, skipping MQTT publish");
if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && } else {
!isBroadcast(p->to) && !isToUs(p)) // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not
p_encrypted->pki_encrypted = true; // to us (because we would be able to decrypt it)
// After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 &&
if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && !isBroadcast(p->to) && !isToUs(p))
!isFromUs(p) && mqtt) p_encrypted->pki_encrypted = true;
mqtt->onSend(*p_encrypted, *p, p->channel); // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet
if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled &&
!isFromUs(p) && mqtt)
mqtt->onSend(*p_encrypted, *p, p->channel);
}
#endif #endif
} }

View File

@@ -62,6 +62,11 @@ template <typename T> bool SX126xInterface<T>::init()
digitalWrite(LORA_PA_TX_EN, LOW); digitalWrite(LORA_PA_TX_EN, LOW);
#endif #endif
#ifdef RF95_FAN_EN
digitalWrite(RF95_FAN_EN, HIGH);
pinMode(RF95_FAN_EN, OUTPUT);
#endif
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) {
@@ -85,6 +90,13 @@ template <typename T> bool SX126xInterface<T>::init()
power = -9; power = -9;
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO);
#ifdef SX126X_PA_RAMP_US
// Set custom PA ramp time for boards requiring longer stabilization (e.g., T-Beam 1W needs >800us)
if (res == RADIOLIB_ERR_NONE) {
lora.setPaRampTime(SX126X_PA_RAMP_US);
}
#endif
// \todo Display actual typename of the adapter, not just `SX126x` // \todo Display actual typename of the adapter, not just `SX126x`
LOG_INFO("SX126x init result %d", res); LOG_INFO("SX126x init result %d", res);
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)

View File

@@ -53,7 +53,7 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
#include "Sensor/LTR390UVSensor.h" #include "Sensor/LTR390UVSensor.h"
#endif #endif
#if __has_include(<bsec2.h>) #if __has_include(MESHTASTIC_BME680_HEADER)
#include "Sensor/BME680Sensor.h" #include "Sensor/BME680Sensor.h"
#endif #endif
@@ -214,7 +214,7 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
#if __has_include(<Adafruit_LTR390.h>) #if __has_include(<Adafruit_LTR390.h>)
addSensor<LTR390UVSensor>(i2cScanner, ScanI2C::DeviceType::LTR390UV); addSensor<LTR390UVSensor>(i2cScanner, ScanI2C::DeviceType::LTR390UV);
#endif #endif
#if __has_include(<bsec2.h>) #if __has_include(MESHTASTIC_BME680_HEADER)
addSensor<BME680Sensor>(i2cScanner, ScanI2C::DeviceType::BME_680); addSensor<BME680Sensor>(i2cScanner, ScanI2C::DeviceType::BME_680);
#endif #endif
#if __has_include(<Adafruit_BMP280.h>) #if __has_include(<Adafruit_BMP280.h>)

View File

@@ -136,12 +136,12 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *
display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr);
if (lastMeasurement.variant.health_metrics.has_heart_bpm) { if (lastMeasurement.variant.health_metrics.has_heart_bpm) {
char heartStr[32]; char heartStr[32];
snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); snprintf(heartStr, sizeof(heartStr), "Heart Rate: %u bpm", lastMeasurement.variant.health_metrics.heart_bpm);
display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr);
} }
if (lastMeasurement.variant.health_metrics.has_spO2) { if (lastMeasurement.variant.health_metrics.has_spO2) {
char spo2Str[32]; char spo2Str[32];
snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); snprintf(spo2Str, sizeof(spo2Str), "spO2: %u %%", lastMeasurement.variant.health_metrics.spO2);
display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str);
} }
} }

View File

@@ -1,6 +1,6 @@
#include "configuration.h" #include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<bsec2.h>) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(MESHTASTIC_BME680_HEADER)
#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "BME680Sensor.h" #include "BME680Sensor.h"
@@ -10,6 +10,7 @@
BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {}
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
int32_t BME680Sensor::runOnce() int32_t BME680Sensor::runOnce()
{ {
if (!bme680.run()) { if (!bme680.run()) {
@@ -17,10 +18,13 @@ int32_t BME680Sensor::runOnce()
} }
return 35; return 35;
} }
#endif // defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{ {
status = 0; status = 0;
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
if (!bme680.begin(dev->address.address, *bus)) if (!bme680.begin(dev->address.address, *bus))
checkStatus("begin"); checkStatus("begin");
@@ -42,12 +46,25 @@ bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
if (status == 0) if (status == 0)
LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status);
#else
bme680 = makeBME680(bus);
if (!bme680->begin(dev->address.address)) {
LOG_ERROR("Init sensor: %s failed at begin()", sensorName);
return status;
}
status = 1;
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
initI2CSensor(); initI2CSensor();
return status; return status;
} }
bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
{ {
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0) if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0)
return false; return false;
@@ -65,9 +82,27 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
// Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms) // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms)
measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal; measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal;
updateState(); updateState();
#else
if (!bme680->performReading()) {
LOG_ERROR("BME680Sensor::getMetrics: performReading failed");
return false;
}
measurement->variant.environment_metrics.has_temperature = true;
measurement->variant.environment_metrics.has_relative_humidity = true;
measurement->variant.environment_metrics.has_barometric_pressure = true;
measurement->variant.environment_metrics.has_gas_resistance = true;
measurement->variant.environment_metrics.temperature = bme680->readTemperature();
measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity();
measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F;
measurement->variant.environment_metrics.gas_resistance = bme680->readGas() / 1000.0;
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
return true; return true;
} }
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
void BME680Sensor::loadState() void BME680Sensor::loadState()
{ {
#ifdef FSCom #ifdef FSCom
@@ -144,5 +179,6 @@ void BME680Sensor::checkStatus(const char *functionName)
else if (bme680.sensor.status > BME68X_OK) else if (bme680.sensor.status > BME68X_OK)
LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status);
} }
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
#endif #endif

View File

@@ -1,23 +1,40 @@
#include "configuration.h" #include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<bsec2.h>) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(MESHTASTIC_BME680_HEADER)
#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h" #include "TelemetrySensor.h"
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
#include <bme68xLibrary.h>
#include <bsec2.h> #include <bsec2.h>
#else
#include <Adafruit_BME680.h>
#include <memory>
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
#define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis() #define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis()
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
const uint8_t bsec_config[] = { const uint8_t bsec_config[] = {
#include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt" #include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt"
}; };
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
class BME680Sensor : public TelemetrySensor class BME680Sensor : public TelemetrySensor
{ {
private: private:
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
Bsec2 bme680; Bsec2 bme680;
#else
using BME680Ptr = std::unique_ptr<Adafruit_BME680>;
static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique<Adafruit_BME680>(bus); }
BME680Ptr bme680;
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
protected: protected:
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
const char *bsecConfigFileName = "/prefs/bsec.dat"; const char *bsecConfigFileName = "/prefs/bsec.dat";
uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0};
uint8_t accuracy = 0; uint8_t accuracy = 0;
@@ -34,10 +51,13 @@ class BME680Sensor : public TelemetrySensor
void loadState(); void loadState();
void updateState(); void updateState();
void checkStatus(const char *functionName); void checkStatus(const char *functionName);
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
public: public:
BME680Sensor(); BME680Sensor();
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
}; };

View File

@@ -14,11 +14,11 @@
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
#ifdef NIMBLE_TWO
#include "NimBLEAdvertising.h" #include "NimBLEAdvertising.h"
#ifdef CONFIG_BT_NIMBLE_EXT_ADV
#include "NimBLEExtAdvertising.h" #include "NimBLEExtAdvertising.h"
#endif
#include "PowerStatus.h" #include "PowerStatus.h"
#endif
#if defined(CONFIG_NIMBLE_CPP_IDF) #if defined(CONFIG_NIMBLE_CPP_IDF)
#include "host/ble_gap.h" #include "host/ble_gap.h"
@@ -26,12 +26,15 @@
#include "nimble/nimble/host/include/host/ble_gap.h" #include "nimble/nimble/host/include/host/ble_gap.h"
#endif #endif
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
namespace namespace
{ {
constexpr uint16_t kPreferredBleMtu = 517; constexpr uint16_t kPreferredBleMtu = 517;
constexpr uint16_t kPreferredBleTxOctets = 251; constexpr uint16_t kPreferredBleTxOctets = 251;
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8; constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
} // namespace } // namespace
#endif
// Debugging options: careful, they slow things down quite a bit! // Debugging options: careful, they slow things down quite a bit!
// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration // #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
@@ -310,9 +313,11 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{ {
PhoneAPI::onNowHasData(fromRadioNum); PhoneAPI::onNowHasData(fromRadioNum);
#ifdef DEBUG_NIMBLE_NOTIFY
int currentNotifyCount = notifyCount.fetch_add(1); int currentNotifyCount = notifyCount.fetch_add(1);
uint8_t cc = bleServer->getConnectedCount(); uint8_t cc = bleServer->getConnectedCount();
#ifdef DEBUG_NIMBLE_NOTIFY
// This logging slows things down when there are lots of packets going to the phone, like initial connection: // 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); LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc);
#endif #endif
@@ -321,7 +326,13 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
put_le32(val, fromRadioNum); put_le32(val, fromRadioNum);
fromNumCharacteristic->setValue(val, sizeof(val)); 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); 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 /// Check the current underlying physical link to see if the client is currently connected
@@ -386,7 +397,12 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
{ {
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &) override #ifdef NIMBLE_TWO
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
#else
virtual void onWrite(NimBLECharacteristic *pCharacteristic)
#endif
{ {
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. // 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. // Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls.
@@ -433,7 +449,11 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
{ {
void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &) override #ifdef NIMBLE_TWO
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
#else
virtual void onRead(NimBLECharacteristic *pCharacteristic)
#endif
{ {
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
@@ -541,27 +561,32 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
class NimbleBluetoothServerCallback : public NimBLEServerCallbacks class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
{ {
#ifdef NIMBLE_TWO
public: public:
explicit NimbleBluetoothServerCallback(NimbleBluetooth *ble) : ble(ble) {} NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; }
private: private:
NimbleBluetooth *ble; NimbleBluetooth *ble;
uint32_t onPassKeyDisplay() override virtual uint32_t onPassKeyDisplay()
#else
virtual uint32_t onPassKeyRequest()
#endif
{ {
uint32_t passkey = config.bluetooth.fixed_pin; uint32_t passkey = config.bluetooth.fixed_pin;
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
LOG_INFO("Use random passkey"); LOG_INFO("Use random passkey");
// This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits
passkey = random(100000, 999999); passkey = random(100000, 999999);
} }
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey); LOG_INFO("*** Enter passkey %d on the peer side ***", passkey);
powerFSM.trigger(EVENT_BLUETOOTH_PAIR); powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
bluetoothStatus->updateStatus(&newStatus); bluetoothStatus->updateStatus(&newStatus);
#if HAS_SCREEN #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
if (screen) { if (screen) {
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
char btPIN[16] = "888888"; char btPIN[16] = "888888";
@@ -590,29 +615,39 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
}); });
} }
#endif #endif
passkeyShowing = true; passkeyShowing = true;
return passkey; return passkey;
} }
void onAuthenticationComplete(NimBLEConnInfo &connInfo) override #ifdef NIMBLE_TWO
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo)
#else
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc)
#endif
{ {
LOG_INFO("BLE authentication complete"); LOG_INFO("BLE authentication complete");
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
bluetoothStatus->updateStatus(&newStatus); bluetoothStatus->updateStatus(&newStatus);
// Todo: migrate this display code back into Screen class, and observe bluetoothStatus
if (passkeyShowing) { if (passkeyShowing) {
passkeyShowing = false; passkeyShowing = false;
if (screen) { if (screen)
screen->endAlert(); screen->endAlert();
}
} }
// Store the connection handle for future use
#ifdef NIMBLE_TWO
nimbleBluetoothConnHandle = connInfo.getConnHandle(); nimbleBluetoothConnHandle = connInfo.getConnHandle();
#else
nimbleBluetoothConnHandle = desc->conn_handle;
#endif
} }
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override #ifdef NIMBLE_TWO
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
{ {
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
@@ -637,12 +672,21 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu);
pServer->updateConnParams(connHandle, 6, 12, 0, 200); pServer->updateConnParams(connHandle, 6, 12, 0, 200);
} }
#endif
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override #ifdef NIMBLE_TWO
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
{ {
LOG_INFO("BLE disconnect reason: %d", reason); LOG_INFO("BLE disconnect reason: %d", reason);
#else
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
{
LOG_INFO("BLE disconnect");
#endif
#ifdef NIMBLE_TWO
if (ble->isDeInit) if (ble->isDeInit)
return; return;
#endif
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus); bluetoothStatus->updateStatus(&newStatus);
@@ -666,69 +710,35 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
bluetoothPhoneAPI->writeCount = 0; bluetoothPhoneAPI->writeCount = 0;
} }
// Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
memset(lastToRadio, 0, sizeof(lastToRadio)); memset(lastToRadio, 0, sizeof(lastToRadio));
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection"
#ifdef NIMBLE_TWO
// Restart Advertising
ble->startAdvertising(); 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 NimbleBluetoothToRadioCallback *toRadioCallbacks;
static NimbleBluetoothFromRadioCallback *fromRadioCallbacks; static NimbleBluetoothFromRadioCallback *fromRadioCallbacks;
void NimbleBluetooth::startAdvertising()
{
#if defined(CONFIG_BT_NIMBLE_EXT_ADV)
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);
if (powerStatus->getHasBattery() == 1) {
pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f));
}
NimBLEAdvertisementData scan;
scan.setName(getDeviceName());
pAdvertising->setScanResponseData(scan);
pAdvertising->enableScanResponse(true);
if (!pAdvertising->start(0)) {
LOG_ERROR("BLE failed to start advertising");
}
#endif
LOG_DEBUG("BLE Advertising started");
}
void NimbleBluetooth::shutdown() void NimbleBluetooth::shutdown()
{ {
// No measurable power saving for ESP32 during light-sleep(?)
#ifndef ARCH_ESP32 #ifndef ARCH_ESP32
// Shutdown bluetooth for minimum power draw
LOG_INFO("Disable bluetooth"); LOG_INFO("Disable bluetooth");
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->reset(); pAdvertising->reset();
@@ -736,6 +746,7 @@ void NimbleBluetooth::shutdown()
#endif #endif
} }
// Proper shutdown for ESP32. Needs reboot to reverse.
void NimbleBluetooth::deinit() void NimbleBluetooth::deinit()
{ {
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
@@ -749,17 +760,21 @@ void NimbleBluetooth::deinit()
digitalWrite(BLE_LED, LOW); digitalWrite(BLE_LED, LOW);
#endif #endif
#endif #endif
#ifndef NIMBLE_TWO
NimBLEDevice::deinit();
#endif
#endif #endif
} }
// Has initial setup been completed
bool NimbleBluetooth::isActive() bool NimbleBluetooth::isActive()
{ {
return bleServer != nullptr; return bleServer;
} }
bool NimbleBluetooth::isConnected() bool NimbleBluetooth::isConnected()
{ {
return bleServer && bleServer->getConnectedCount() > 0; return bleServer->getConnectedCount() > 0;
} }
int NimbleBluetooth::getRssi() int NimbleBluetooth::getRssi()
@@ -803,7 +818,7 @@ void NimbleBluetooth::setup()
LOG_INFO("Init the NimBLE bluetooth module"); LOG_INFO("Init the NimBLE bluetooth module");
NimBLEDevice::init(getDeviceName()); NimBLEDevice::init(getDeviceName());
NimBLEDevice::setPower(9); NimBLEDevice::setPower(ESP_PWR_LVL_P9);
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu);
@@ -836,7 +851,11 @@ void NimbleBluetooth::setup()
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
} }
bleServer = NimBLEDevice::createServer(); bleServer = NimBLEDevice::createServer();
auto *serverCallbacks = new NimbleBluetoothServerCallback(this); #ifdef NIMBLE_TWO
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this);
#else
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback();
#endif
bleServer->setCallbacks(serverCallbacks, true); bleServer->setCallbacks(serverCallbacks, true);
setupService(); setupService();
startAdvertising(); startAdvertising();
@@ -881,7 +900,11 @@ void NimbleBluetooth::setupService()
NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is 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) BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
(uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1); (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1);
#ifdef NIMBLE_TWO
NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904();
#else
NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904);
#endif
batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8);
batteryLevelDescriptor->setNamespace(1); batteryLevelDescriptor->setNamespace(1);
batteryLevelDescriptor->setUnit(0x27ad); batteryLevelDescriptor->setUnit(0x27ad);
@@ -889,12 +912,54 @@ void NimbleBluetooth::setupService()
batteryService->start(); 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 /// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level) void updateBatteryLevel(uint8_t level)
{ {
if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) {
BatteryCharacteristic->setValue(&level, 1); BatteryCharacteristic->setValue(&level, 1);
#ifdef NIMBLE_TWO
BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE);
#else
BatteryCharacteristic->notify();
#endif
} }
} }
@@ -909,7 +974,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
if (!bleServer || !isConnected() || length > 512) { if (!bleServer || !isConnected() || length > 512) {
return; return;
} }
#ifdef NIMBLE_TWO
logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE);
#else
logRadioCharacteristic->notify(logMessage, length, true);
#endif
} }
void clearNVS() void clearNVS()

View File

@@ -12,11 +12,16 @@ class NimbleBluetooth : BluetoothApi
bool isConnected(); bool isConnected();
int getRssi(); int getRssi();
void sendLog(const uint8_t *logMessage, size_t length); void sendLog(const uint8_t *logMessage, size_t length);
#if defined(NIMBLE_TWO)
void startAdvertising(); void startAdvertising();
#endif
bool isDeInit = false; bool isDeInit = false;
private: private:
void setupService(); void setupService();
#if !defined(NIMBLE_TWO)
void startAdvertising();
#endif
}; };
void setBluetoothEnable(bool enable); void setBluetoothEnable(bool enable);

View File

@@ -1,6 +1,8 @@
#include "SerialConsole.h" #include "SerialConsole.h"
#include "concurrency/OSThread.h" #include "concurrency/OSThread.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "mesh/MeshRadio.h"
#include "mesh/NodeDB.h"
#include "TestUtil.h" #include "TestUtil.h"
@@ -14,5 +16,19 @@ void initializeTestEnvironment()
tv.tv_usec = 0; tv.tv_usec = 0;
perhapsSetRTC(RTCQualityNTP, &tv); perhapsSetRTC(RTCQualityNTP, &tv);
#endif #endif
concurrency::OSThread::setup();
}
void initializeTestEnvironmentMinimal()
{
// Only satisfy OSThread assertions; skip SerialConsole and platform-specific setup
concurrency::hasBeenSetup = true;
// Ensure region/config globals are sane before any RadioInterface instances compute slot timing
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
config.lora.use_preset = true;
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
initRegion();
concurrency::OSThread::setup(); concurrency::OSThread::setup();
} }

View File

@@ -1,4 +1,7 @@
#pragma once #pragma once
// Initialize testing environment. // Initialize testing environment.
void initializeTestEnvironment(); void initializeTestEnvironment();
// Minimal init without creating SerialConsole or portduino peripherals (useful for lightweight logic tests)
void initializeTestEnvironmentMinimal();

View File

@@ -0,0 +1,164 @@
#include <Arduino.h>
#include <unity.h>
#include "TestUtil.h"
// Ensure adaptive coding rate logic is available during tests
#ifndef USE_ADAPTIVE_CODING_RATE
#define USE_ADAPTIVE_CODING_RATE 1
#endif
#include "mesh/RadioInterface.h"
class TestRadio : public RadioInterface
{
public:
bool applyForTest(const meshtastic_MeshPacket *p) { return applyAdaptiveCodingRate(p); }
uint8_t getAttempts(NodeNum from, PacketId id)
{
#ifdef USE_ADAPTIVE_CODING_RATE
auto it = adaptiveAttempts.find(adaptiveKey(from, id));
return (it == adaptiveAttempts.end()) ? 0 : it->second.attempts;
#else
(void)from;
(void)id;
return 0;
#endif
}
#ifdef USE_ADAPTIVE_CODING_RATE
void setAdaptiveState(NodeNum from, PacketId id, uint8_t attempts, uint32_t lastUse)
{
adaptiveAttempts[adaptiveKey(from, id)] = {attempts, lastUse};
}
#endif
uint8_t currentCr() const { return cr; }
void setCrForTest(uint8_t value) { cr = value; }
ErrorCode send(meshtastic_MeshPacket *p) override
{
packetPool.release(p);
return ERRNO_OK;
}
uint32_t getPacketTime(uint32_t /*totalPacketLen*/, bool /*received*/ = false) override { return 0; }
bool reconfigure() override
{
reconfigureCount++;
lastCr = cr;
return true;
}
uint32_t reconfigureCount = 0;
uint8_t lastCr = 0;
};
void test_attempt_progression()
{
TestRadio radio;
meshtastic_MeshPacket packet = {};
packet.from = 0xABCDEF01;
packet.id = 0x1;
TEST_ASSERT_FALSE(radio.applyForTest(&packet));
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(packet.from, packet.id));
TEST_ASSERT_EQUAL_UINT8(5, radio.currentCr());
TEST_ASSERT_EQUAL_UINT32(0, radio.reconfigureCount);
TEST_ASSERT_TRUE(radio.applyForTest(&packet));
TEST_ASSERT_EQUAL_UINT8(2, radio.getAttempts(packet.from, packet.id));
TEST_ASSERT_EQUAL_UINT8(7, radio.currentCr());
TEST_ASSERT_EQUAL_UINT32(1, radio.reconfigureCount);
TEST_ASSERT_EQUAL_UINT8(7, radio.lastCr);
TEST_ASSERT_TRUE(radio.applyForTest(&packet));
TEST_ASSERT_EQUAL_UINT8(3, radio.getAttempts(packet.from, packet.id));
TEST_ASSERT_EQUAL_UINT8(8, radio.currentCr());
TEST_ASSERT_EQUAL_UINT32(2, radio.reconfigureCount);
TEST_ASSERT_EQUAL_UINT8(8, radio.lastCr);
}
void test_attempts_are_per_packet()
{
TestRadio radio;
meshtastic_MeshPacket first = {};
first.from = 0x1001;
first.id = 0xA;
meshtastic_MeshPacket second = {};
second.from = 0x1001;
second.id = 0xB;
radio.applyForTest(&first);
radio.applyForTest(&second);
radio.applyForTest(&first);
TEST_ASSERT_EQUAL_UINT8(2, radio.getAttempts(first.from, first.id));
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(second.from, second.id));
TEST_ASSERT_EQUAL_UINT8(7, radio.currentCr());
}
void test_clear_resets_attempts_and_rate()
{
TestRadio radio;
meshtastic_MeshPacket packet = {};
packet.from = 0xCAFE;
packet.id = 0x55;
radio.applyForTest(&packet);
radio.applyForTest(&packet);
radio.applyForTest(&packet);
radio.reconfigureCount = 0;
radio.setCrForTest(8);
radio.clearAdaptiveCodingRateState(packet.from, packet.id);
TEST_ASSERT_TRUE(radio.applyForTest(&packet));
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(packet.from, packet.id));
TEST_ASSERT_EQUAL_UINT8(5, radio.currentCr());
TEST_ASSERT_EQUAL_UINT32(1, radio.reconfigureCount);
}
void test_prunes_expired_state()
{
TestRadio radio;
meshtastic_MeshPacket packet = {};
packet.from = 0xBEEF;
packet.id = 0x99;
radio.applyForTest(&packet);
#ifdef USE_ADAPTIVE_CODING_RATE
const uint32_t now = millis();
radio.setAdaptiveState(packet.from, packet.id, 3, now - (5 * 60 * 1000UL + 50));
#endif
radio.reconfigureCount = 0;
radio.setCrForTest(5);
TEST_ASSERT_FALSE(radio.applyForTest(&packet));
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(packet.from, packet.id));
TEST_ASSERT_EQUAL_UINT32(0, radio.reconfigureCount);
}
void setup()
{
printf("AdaptiveCodingRate test setup start\n");
fflush(stdout);
// Use minimal init to avoid pulling in SerialConsole/portduino peripherals for these logic-only tests
initializeTestEnvironmentMinimal();
printf("AdaptiveCodingRate test init done\n");
fflush(stdout);
UNITY_BEGIN();
RUN_TEST(test_attempt_progression);
RUN_TEST(test_attempts_are_per_packet);
RUN_TEST(test_clear_resets_attempts_and_rate);
RUN_TEST(test_prunes_expired_state);
UNITY_END();
}
void loop()
{
delay(1000);
}

View File

@@ -38,7 +38,6 @@ build_flags =
-DAXP_DEBUG_PORT=Serial -DAXP_DEBUG_PORT=Serial
-DCONFIG_BT_NIMBLE_ENABLED -DCONFIG_BT_NIMBLE_ENABLED
-DCONFIG_BT_NIMBLE_MAX_BONDS=6 # default is 3 -DCONFIG_BT_NIMBLE_MAX_BONDS=6 # default is 3
-DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED
-DCONFIG_NIMBLE_CPP_LOG_LEVEL=2 -DCONFIG_NIMBLE_CPP_LOG_LEVEL=2
-DCONFIG_BT_NIMBLE_MAX_CCCDS=20 -DCONFIG_BT_NIMBLE_MAX_CCCDS=20
-DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192 -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
@@ -61,11 +60,11 @@ lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master
https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
h2zero/NimBLE-Arduino@2.3.7 h2zero/NimBLE-Arduino@^1.4.3
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
lewisxhe/XPowersLib@0.3.2 https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto

View File

@@ -5,4 +5,4 @@ extends = esp32_common
custom_esp32_kind = esp32 custom_esp32_kind = esp32
build_flags = build_flags =
${esp32_common.build_flags} ${esp32_common.build_flags}

View File

@@ -2,7 +2,7 @@
[env:tbeam] [env:tbeam]
extends = esp32_base extends = esp32_base
board = ttgo-t-beam board = ttgo-t-beam
board_level = extra
board_check = true board_check = true
build_flags = ${esp32_base.build_flags} build_flags = ${esp32_base.build_flags}
-D TBEAM_V10 -D TBEAM_V10
@@ -13,7 +13,7 @@ upload_speed = 921600
[env:tbeam-displayshield] [env:tbeam-displayshield]
extends = env:tbeam extends = env:tbeam
board_level = extra
build_flags = build_flags =
${env:tbeam.build_flags} ${env:tbeam.build_flags}
-D USE_ST7796 -D USE_ST7796

View File

@@ -4,8 +4,3 @@ custom_esp32_kind = esp32c3
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_c3_exception_decoder monitor_filters = esp32_c3_exception_decoder
build_flags =
${esp32_common.build_flags}
-DCONFIG_BT_NIMBLE_EXT_ADV=1
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2

View File

@@ -26,6 +26,7 @@ build_flags =
-D HAS_BLUETOOTH=1 -D HAS_BLUETOOTH=1
-DCONFIG_BT_NIMBLE_EXT_ADV=1 -DCONFIG_BT_NIMBLE_EXT_ADV=1
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2 -DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
-D NIMBLE_TWO
monitor_speed=115200 monitor_speed=115200
lib_ignore = lib_ignore =
NonBlockingRTTTL NonBlockingRTTTL

View File

@@ -3,8 +3,3 @@ extends = esp32_common
custom_esp32_kind = esp32s3 custom_esp32_kind = esp32s3
monitor_speed = 115200 monitor_speed = 115200
build_flags =
${esp32_common.build_flags}
-DCONFIG_BT_NIMBLE_EXT_ADV=1
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2

View File

@@ -55,5 +55,6 @@
#define SX126X_RESET 14 #define SX126X_RESET 14
#define SX126X_RXEN 47 #define SX126X_RXEN 47
#define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin #define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#endif #endif

View File

@@ -13,8 +13,7 @@ build_flags =
[env:rak3112] [env:rak3112]
extends = esp32s3_base extends = esp32s3_base
board = wiscore_rak3312 board = wiscore_rak3312
board_level = pr board_level = extra
board_check = true
upload_protocol = esptool upload_protocol = esptool
build_flags = build_flags =

View File

@@ -0,0 +1,25 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
static const uint8_t TX = 43;
static const uint8_t RX = 44;
// I2C for OLED and sensors
static const uint8_t SDA = 8;
static const uint8_t SCL = 9;
// Default SPI mapped to Radio/SD
static const uint8_t SS = 15; // LoRa CS
static const uint8_t MOSI = 11;
static const uint8_t MISO = 12;
static const uint8_t SCK = 13;
// SD Card CS
#define SDCARD_CS 10
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,14 @@
; LilyGo T-Beam-1W (1 Watt LoRa with external PA)
[env:t-beam-1w]
extends = esp32s3_base
board = t-beam-1w
board_build.partitions = default_8MB.csv
board_check = true
lib_deps =
${esp32s3_base.lib_deps}
build_flags =
${esp32s3_base.build_flags}
-I variants/esp32s3/t-beam-1w
-D T_BEAM_1W

View File

@@ -0,0 +1,97 @@
// LilyGo T-Beam-1W variant.h
// Configuration based on LilyGO utilities.h and RF documentation
// I2C for OLED display (SH1106 at 0x3C)
#define I2C_SDA 8
#define I2C_SCL 9
// GPS - Quectel L76K
#define GPS_RX_PIN 5
#define GPS_TX_PIN 6
#define GPS_1PPS_PIN 7
#define GPS_WAKEUP_PIN 16 // GPS_EN_PIN in LilyGO code
#define HAS_GPS 1
#define GPS_BAUDRATE 9600
// Buttons
#define BUTTON_PIN 0 // BUTTON 1
#define BUTTON_PIN_ALT 17 // BUTTON 2
// SPI (shared by LoRa and SD)
#define SPI_MOSI 11
#define SPI_SCK 13
#define SPI_MISO 12
#define SPI_CS 10
// SD Card
#define HAS_SDCARD
#define SDCARD_USE_SPI1
#define SDCARD_CS SPI_CS
// LoRa Radio - SX1262 with 1W PA
#define USE_SX1262
#define LORA_SCK SPI_SCK
#define LORA_MISO SPI_MISO
#define LORA_MOSI SPI_MOSI
#define LORA_CS 15
#define LORA_RESET 3
#define LORA_DIO1 1
#define LORA_BUSY 38
// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()!
// GPIO 40 powers the SX1262 + PA module via LDO
#define SX126X_POWER_EN 40
// TX power offset for external PA (0 = no offset, full SX1262 power)
#define TX_GAIN_LORA 10
#ifdef USE_SX1262
#define SX126X_CS LORA_CS
#define SX126X_DIO1 LORA_DIO1
#define SX126X_BUSY LORA_BUSY
#define SX126X_RESET LORA_RESET
// RF switching configuration for 1W PA module
// DIO2 controls PA (via SX126X_DIO2_AS_RF_SWITCH)
// CTRL PIN (GPIO 21) controls LNA - must be HIGH during RX
// Truth table: DIO2=1,CTRL=0 → TX (PA on, LNA off)
// DIO2=0,CTRL=1 → RX (PA off, LNA on)
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_RXEN 21 // LNA enable - HIGH during RX
// TCXO voltage - required for radio init
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define SX126X_MAX_POWER 22
#endif
// LED
#define LED_PIN 18
#define LED_STATE_ON 1 // HIGH = ON
// Battery ADC
#define BATTERY_PIN 4
#define ADC_CHANNEL ADC1_GPIO4_CHANNEL
#define BATTERY_SENSE_SAMPLES 30
#define ADC_MULTIPLIER 2.9333
// NTC temperature sensor
#define NTC_PIN 14
// Fan control
#define FAN_CTRL_PIN 41
// Meshtastic standard fan control pin macro
#define RF95_FAN_EN FAN_CTRL_PIN
// PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us)
// Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U
#define SX126X_PA_RAMP_US 0x05
// Display - SH1106 OLED (128x64)
#define USE_SH1106
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
// 32768 Hz crystal present
#define HAS_32768HZ 1

View File

@@ -34,6 +34,8 @@ lib_deps =
adafruit/Adafruit seesaw Library@1.7.9 adafruit/Adafruit seesaw Library@1.7.9
# renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
adafruit/Adafruit BME680 Library@^2.0.5
build_flags = build_flags =
${arduino_base.build_flags} ${arduino_base.build_flags}

View File

@@ -2,6 +2,9 @@
extends = portduino_base extends = portduino_base
build_flags = ${portduino_base.build_flags} -I variants/native/portduino build_flags = ${portduino_base.build_flags} -I variants/native/portduino
-I /usr/include -I /usr/include
-I /opt/homebrew/include
-L /opt/homebrew/lib -largp
-DUSE_ADAPTIVE_CODING_RATE
board = cross_platform board = cross_platform
board_level = extra board_level = extra
lib_deps = lib_deps =
@@ -9,6 +12,11 @@ lib_deps =
# renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028
melopero/Melopero RV3028@1.2.0 melopero/Melopero RV3028@1.2.0
; Disable LovyanGFX for native test builds to avoid missing macOS system headers
lib_ignore =
${portduino_base.lib_ignore}
LovyanGFX
build_src_filter = ${portduino_base.build_src_filter} build_src_filter = ${portduino_base.build_src_filter}
[env:native] [env:native]

View File

@@ -7,6 +7,7 @@ build_flags =
${nrf52840_base.build_flags} ${nrf52840_base.build_flags}
-Ivariants/nrf52840/ELECROW-ThinkNode-M3 -Ivariants/nrf52840/ELECROW-ThinkNode-M3
-DELECROW_ThinkNode_M3 -DELECROW_ThinkNode_M3
-DUSE_ADAPTIVE_CODING_RATE
-DGPS_POWER_TOGGLE -DGPS_POWER_TOGGLE
-D CONFIG_NFCT_PINS_AS_GPIOS=1 -D CONFIG_NFCT_PINS_AS_GPIOS=1
-L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard"

View File

@@ -9,6 +9,7 @@ build_flags = ${nrf52840_base.build_flags}
-Ivariants/nrf52840/heltec_mesh_pocket -Ivariants/nrf52840/heltec_mesh_pocket
-DHELTEC_MESH_POCKET -DHELTEC_MESH_POCKET
-DHELTEC_MESH_POCKET_BATTERY_5000 -DHELTEC_MESH_POCKET_BATTERY_5000
-DUSE_ADAPTIVE_CODING_RATE
-DUSE_EINK -DUSE_EINK
-DEINK_DISPLAY_MODEL=GxEPD2_213_B74 -DEINK_DISPLAY_MODEL=GxEPD2_213_B74
-DEINK_WIDTH=250 -DEINK_WIDTH=250
@@ -38,6 +39,7 @@ build_flags =
-I variants/nrf52840/heltec_mesh_pocket -I variants/nrf52840/heltec_mesh_pocket
-D HELTEC_MESH_POCKET -D HELTEC_MESH_POCKET
-D HELTEC_MESH_POCKET_BATTERY_5000 -D HELTEC_MESH_POCKET_BATTERY_5000
-DUSE_ADAPTIVE_CODING_RATE
lib_deps = lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}
@@ -54,6 +56,7 @@ build_flags = ${nrf52840_base.build_flags}
-Ivariants/nrf52840/heltec_mesh_pocket -Ivariants/nrf52840/heltec_mesh_pocket
-DHELTEC_MESH_POCKET -DHELTEC_MESH_POCKET
-DHELTEC_MESH_POCKET_BATTERY_10000 -DHELTEC_MESH_POCKET_BATTERY_10000
-DUSE_ADAPTIVE_CODING_RATE
-DUSE_EINK -DUSE_EINK
-DEINK_DISPLAY_MODEL=GxEPD2_213_B74 -DEINK_DISPLAY_MODEL=GxEPD2_213_B74
-DEINK_WIDTH=250 -DEINK_WIDTH=250
@@ -83,6 +86,7 @@ build_flags =
-I variants/nrf52840/heltec_mesh_pocket -I variants/nrf52840/heltec_mesh_pocket
-D HELTEC_MESH_POCKET -D HELTEC_MESH_POCKET
-D HELTEC_MESH_POCKET_BATTERY_10000 -D HELTEC_MESH_POCKET_BATTERY_10000
-DUSE_ADAPTIVE_CODING_RATE
lib_deps = lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${nrf52840_base.lib_deps} ${nrf52840_base.lib_deps}

View File

@@ -7,6 +7,7 @@ build_flags = ${nrf52840_base.build_flags}
-I variants/nrf52840/rak_wismeshtag -I variants/nrf52840/rak_wismeshtag
-D WISMESH_TAG -D WISMESH_TAG
-D RAK_4631 -D RAK_4631
-DUSE_ADAPTIVE_CODING_RATE
-DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX128X=1
-DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_SX127X=1
-DRADIOLIB_EXCLUDE_LR11X0=1 -DRADIOLIB_EXCLUDE_LR11X0=1

View File

@@ -7,6 +7,7 @@ build_flags = ${nrf52840_base.build_flags}
-Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice
-Isrc/platform/nrf52/softdevice/nrf52 -Isrc/platform/nrf52/softdevice/nrf52
-DTRACKER_T1000_E -DTRACKER_T1000_E
-DUSE_ADAPTIVE_CODING_RATE
-DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1
-DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-DMESHTASTIC_EXCLUDE_SCREEN=1 -DMESHTASTIC_EXCLUDE_SCREEN=1

View File

@@ -1,4 +1,4 @@
[VERSION] [VERSION]
major = 2 major = 2
minor = 7 minor = 7
build = 17 build = 18