Compare commits

...
39 Commits
Author SHA1 Message Date
HarukiToreda 6457a7ab9e Merge branch 'develop' into Freetext 2026-02-23 20:55:58 -05:00
oscgonfer 417ca86c30 Split module includes for AQ module (#9711) 2026-02-22 21:07:32 -06:00
renovate[bot] 7df37f8254 chore(deps): update radiolib to v7.6.0 (#9695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 18:53:33 +01:00
Wessel 9383d0bcfc Apply SX1262 register 0x8B5 patch for improved GC1109 RX sensitivity (#9571)
* Apply SX1262 register 0x8B5 patch for improved GC1109 RX sensitivity

Sets the LSB of undocumented SX1262 register 0x8B5 on Heltec V4 and
Wireless Tracker V2 boards with the GC1109 FEM. This patch was
recommended by Heltec/Semtech and tested in MeshCore PR #1398, where
it significantly reduced packet loss on the Heltec V4.

* Use higher level function

* Add .venv/ to .gitignore
2026-02-21 09:08:14 -06:00
Mike RobbinsandBen Meadors 8feb34e7a8 RadioLib edge-triggered interrupts robustness (#9658)
* Fix potential race condition (read, ISR write, clear) in NotifiedWorkerThread

* Check for missed edge-triggered interrupts race condition between startReceive and enableInterrupt

* Occasionally poll to catch missed RX_DONE interrupts. (RadioLibInterface::pollMissedIrqs)

* Simplify RadioLibInterface::checkRxDoneIrqFlag()

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-21 06:14:04 -06:00
David Remba a5523b04ef Fix/rak3401 button (#9668) 2026-02-21 09:15:22 +01:00
Wessel af518fbd2b Hold GC1109 FEM power during deep sleep for LNA RX wake (#9572)
* Hold GC1109 PA_POWER and PA_EN during deep sleep for LNA RX wake

Use rtc_gpio_hold_en to latch PA_POWER (LDO) and PA_EN (CSD) HIGH
during deep sleep so the GC1109 LNA remains powered for wake-on-packet
RX. Previously these pins used weak pull-ups which could lose state.

On deep sleep wake, skip these pins in the blanket RTC hold release and
instead release them in SX126xInterface::init() after GPIO registers are
set HIGH first, avoiding a power glitch on the GC1109.

Trade-off: ~6.5mA additional deep sleep current for significantly
improved wake-on-packet RX sensitivity (~17dB).

Reference: https://github.com/meshcore-dev/MeshCore/pull/1600

* Add LDO startup delay before GC1109 chip enable

TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot,
wait 1ms for VBAT to stabilise before driving CSD/CPS, per GC1109
power-on sequence requirement. On deep sleep wake the LDO is held on
via RTC latch so no delay is needed.
2026-02-20 16:12:54 -06:00
Ben Meadors 8c37d6923b Add #include "RadioLibInterface.h" 2026-02-20 12:00:30 -06:00
Ben Meadors 299ef95f25 Revert "Add agc reset attempt (#8163)" (#9702)
This reverts commit ac611c4b62.
2026-02-20 11:25:28 -06:00
840b29860f skip header items when enabling the InkHUD menu cursor (#9552)
* onNavUp() sets the menu cursor to the last item if not shown

* skip headers when showing the menu cursor in onNavUp() and onNavDown()

* skip headers when showing the menu cursor in onButtonShortPress()

* brace cursor incrementing

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-02-20 06:12:22 -06:00
HarukiToreda c27ad2349b Suppress Navigation bar for Freetext frames 2026-02-19 21:49:43 -05:00
HarukiToreda e206f14f37 Emote Picker Refactoring 2026-02-19 18:26:52 -05:00
HarukiToreda 1e7228f667 Grid View Emotes 2026-02-19 17:35:58 -05:00
HarukiToreda f6bf33d61d Optimization 2026-02-19 17:09:28 -05:00
HarukiToreda 951b812693 Emote support on "e" press 2026-02-19 14:44:41 -05:00
HarukiToreda 2632122495 Rounded selections and hidden navigation bar 2026-02-19 13:48:51 -05:00
HarukiToreda 96e9041943 CannedMessageModule.cpp refactoring 2026-02-19 13:06:33 -05:00
HarukiToreda 2b03d012f2 Text Prediction 2026-02-19 02:51:52 -05:00
Max 86986d6337 ULED_BUILTIN for 9m2ibr_aprs_lora_tracker (#9685) 2026-02-19 10:20:07 +08:00
Thomas Göttgens 178c3114d9 Fake IAQ values on Non-BSEC2 platforms like Platformio and the original ESP32 (#9663)
* BSEC2 Replacement
- add approximation for IAQ to non-BSEC2 platforms.
- Re-add this sensor to ESP32 targets, and refactor env_extra includes.
- Fix C++ 11 compatibility
* Check for gas resistance 0
2026-02-17 22:16:56 +01:00
Vortetty 3ca68e7788 InkHUD: Allow non-system applets to subscribe to input events (#9514)
* Allow inkhud user applets to consume inputs with opt-in system
Adds a way for applets to subscribe to input events while keeping it off
by default to preserve compatibility and expected behaviours. Adds
example for use as well.
* Add check for nullptr on getActiveApplet uses
* Remove redundant includes
* Move subscribedInputs to protected
* More consistent naming scheme
2026-02-17 11:03:26 +01:00
Thomas Göttgens a4ba312579 zip a few gitrefs down (#9672) 2026-02-17 09:53:20 +01:00
Thomas Göttgens a227fd7028 #9623 resolved a local shadow of next_key by converting it to int. (#9665) 2026-02-16 19:39:43 +01:00
Thomas GöttgensandCopilot 6b44b5786e fix detection of SCD30 by checking if the size of the return from a 2 byte register read is correct (#9664)
* fix detection of SCD30 by checking if thee size of the return from a 2 byte register read is correct
fix signedness warning in PMSA003 sensor code.

* Add alternate path for LPS22HB

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

* check EndTransmission for errors and compare returned length to expected value

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 10:07:07 -06:00
Thomas Göttgens 5feba46b53 our firmware action is too clever
Update pio_target and add pio_opts for checks.
2026-02-16 16:39:31 +01:00
Wessel 0cbf53b7a7 fix: respect DontMqttMeBro flag regardless of channel PSK (#9626)
The previous PSK check was broken from its introduction in #4643 —
memcmp was used in boolean context without comparing to 0, inverting
the condition. Since no one noticed for over a year, the PSK-based
filtering provided no practical value. Simplifying to always respect
the sender's preference is both more correct and easier to reason about.
2026-02-16 13:49:58 +01:00
57268bf4ea Feat/add scd30 (#9609)
* Merge develop into SCD30

* Add SCD30 class

* Fix logging and admin commands

* Minor cleanup and logging improvements

* Minor formatting issue

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

* Improvements on setTemperature

* Fix casting float-uint16_t
* Pass 100 for resetting temperature offset

* Fix issues pointed out by copilot

* Add quick reboot to set interval quicker on scd30

* Change saveState to only happen after boot and minor log changes

* Fix missing semicolon in one shot mode log

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-16 05:38:36 -06:00
Thomas Göttgens 56fd9c7813 fix a lot of low level cppcheck warnings (#9623)
* simplify the observer pattern, since all the called functions are const getters.
* use arduino macro over std: for numerical values and refactor local variables in drawScrollbar()
* oh, so Cppcheck actually complained about const pointers not being const.
* slowly getting out of ifdef hell
* fix inkHUD warnings as well
* last 2 check warnings
* git checks should fail on low defects from now on
2026-02-16 12:28:07 +01:00
HarukiToreda 32db70037d InkHUD: Favorite Map Applet (#9654) 2026-02-16 10:35:27 +01:00
Clive Blackledge 778823e623 Add USB_MODE=1 for Station G2 (#9660) 2026-02-16 10:16:05 +01:00
Jorropo 545826d319 replace delete in AudioThread.h with std::unique_ptr (#9651)
Is part of the unique_ptr modernization effort.
2026-02-15 06:49:16 -06:00
Jorropo dbef1de286 workaround NCP5623 and LP5562 I2C builds (#9652)
Theses two appear to be buggy on r1-neo and nomadstar meteor pro, they rely on Wire.h being included previously to their import.

Idk why other platforms using the same smart LEDs are working while theses ones don't.

This should make CI green on the dev branch.
2026-02-15 00:32:19 +01:00
Jorropo b0bd3df226 replace delete in CryptoEngine.{cpp,h} with std::unique_ptr (#9649)
Is part of the unique_ptr modernization effort.
2026-02-14 22:44:36 +01:00
Jorropo 167bcf2863 fix typo in PIN_GPS_SWITCH (#9648)
Wasn't caught by CI.
2026-02-14 22:12:31 +01:00
Jorropo ff485d5ff8 replace delete in RadioInterface.cpp with std::unique_ptr (#9645)
Is part of the unique_ptr modernization effort.
2026-02-14 21:39:26 +01:00
Eric BarchandJorropo 184e4ddd83 Undefine LED_BUILTIN for Heltec v2 variant (#9647)
* Undefine LED_BUILTIN for Heltec v2 variant

* Undefine LED_BUILTIN for Heltec v2.1 variant

---------

Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
2026-02-14 21:30:16 +01:00
Jorropo aed2c7ca9b replace delete in EInkDynamicDisplay.{cpp,h} with std::unique_ptr (#9643)
Is part of the unique_ptr modernization effort.
2026-02-14 19:56:53 +01:00
Jorropo e1f9ccd337 replace delete in RedirectablePrint.cpp with std::unique_ptr (#9642)
Is part of the unique_ptr modernization effort.
2026-02-14 19:32:16 +01:00
Jorropo df14561560 convert GPS global and some new in gps.cpp to unique_ptr (#9628)
Trying this to see if anything bad happen if I were to replace most raw pointers with unique_ptr.

I didn't used std::make_unique since it is only supported in C++14 and onwards but until we update esp32 to arduino 3.x the ESP32 xtensa chips use a C++11 std.
2026-02-14 19:12:59 +01:00
134 changed files with 4248 additions and 2055 deletions
+1
View File
@@ -96,6 +96,7 @@ jobs:
pio_platform: ${{ matrix.check.platform }}
pio_env: ${{ matrix.check.board }}
pio_target: check
pio_opts: --fail-on-defect=low
build:
needs: [setup, version]
+1
View File
@@ -33,6 +33,7 @@ __pycache__
*~
venv/
.venv/
release/
.vscode/extensions.json
/compile_commands.json
+1 -1
View File
@@ -8,7 +8,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0"
],
+18 -34
View File
@@ -117,7 +117,7 @@ lib_deps =
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.5.0
jgromes/RadioLib@7.6.0
[device-ui_base]
lib_deps =
@@ -184,8 +184,8 @@ lib_deps =
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
wollewald/BH1750_WE@1.1.10
; (not included in native / portduino)
[environmental_extra]
; Common environmental sensor libraries (not included in native / portduino)
[environmental_extra_common]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
@@ -205,10 +205,6 @@ lib_deps =
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
@@ -217,33 +213,21 @@ lib_deps =
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
sensirion/Sensirion I2C SCD30@1.0.0
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
[environmental_extra]
lib_deps =
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
boschsensortec/BME68x Sensor Library@1.3.40408
; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
adafruit/Adafruit BMP3XX Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
adafruit/Adafruit SHTC3 Library@1.0.2
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
adafruit/Adafruit SHT31 Library@2.2.2
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
adafruit/Adafruit VEML7700 Library@2.1.6
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
adafruit/Adafruit SHT4x Library@1.0.5
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.3
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
sensirion/Sensirion I2C SFA3x@1.0.0
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
adafruit/Adafruit BME680 Library@^2.0.5
+2
View File
@@ -7,6 +7,8 @@
#include "sleep.h"
#ifdef HAS_NCP5623
#include <Wire.h>
#include <NCP5623.h>
#endif
+10 -13
View File
@@ -4,6 +4,7 @@
#include "configuration.h"
#include "main.h"
#include "sleep.h"
#include <memory>
#ifdef HAS_I2S
#include <AudioFileSourcePROGMEM.h>
@@ -29,9 +30,9 @@ class AudioThread : public concurrency::OSThread
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
setCPUFast(true);
rtttlFile = new AudioFileSourcePROGMEM(data, len);
i2sRtttl = new AudioGeneratorRTTTL();
i2sRtttl->begin(rtttlFile, audioOut);
rtttlFile = std::unique_ptr<AudioFileSourcePROGMEM>(new AudioFileSourcePROGMEM(data, len));
i2sRtttl = std::unique_ptr<AudioGeneratorRTTTL>(new AudioGeneratorRTTTL());
i2sRtttl->begin(rtttlFile.get(), audioOut.get());
}
// Also handles actually playing the RTTTL, needs to be called in loop
@@ -47,12 +48,10 @@ class AudioThread : public concurrency::OSThread
{
if (i2sRtttl != nullptr) {
i2sRtttl->stop();
delete i2sRtttl;
i2sRtttl = nullptr;
}
if (rtttlFile != nullptr) {
delete rtttlFile;
rtttlFile = nullptr;
}
@@ -66,16 +65,14 @@ class AudioThread : public concurrency::OSThread
{
if (i2sRtttl != nullptr) {
i2sRtttl->stop();
delete i2sRtttl;
i2sRtttl = nullptr;
}
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
ESP8266SAM *sam = new ESP8266SAM;
sam->Say(audioOut, text);
delete sam;
auto sam = std::unique_ptr<ESP8266SAM>(new ESP8266SAM);
sam->Say(audioOut.get(), text);
setCPUFast(false);
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, LOW);
@@ -96,15 +93,15 @@ class AudioThread : public concurrency::OSThread
private:
void initOutput()
{
audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S);
audioOut = std::unique_ptr<AudioOutputI2S>(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S));
audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK);
audioOut->SetGain(0.2);
};
AudioGeneratorRTTTL *i2sRtttl = nullptr;
AudioOutputI2S *audioOut = nullptr;
std::unique_ptr<AudioGeneratorRTTTL> i2sRtttl = nullptr;
std::unique_ptr<AudioOutputI2S> audioOut = nullptr;
AudioFileSourcePROGMEM *rtttlFile = nullptr;
std::unique_ptr<AudioFileSourcePROGMEM> rtttlFile = nullptr;
};
#endif
+5 -3
View File
@@ -704,11 +704,11 @@ bool Power::setup()
found = true;
} else if (analogInit()) {
found = true;
}
} else {
#ifdef NRF_APM
found = true;
found = true;
#endif
}
#ifdef EXT_PWR_DETECT
attachInterrupt(
EXT_PWR_DETECT,
@@ -846,8 +846,10 @@ void Power::readPowerStatus()
if (batteryLevel) {
hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse;
#ifndef NRF_APM
usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse;
isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse;
#endif
if (hasBattery) {
batteryVoltageMv = batteryLevel->getBattVoltage();
// If the AXP192 returns a valid battery percentage, use it
+18 -29
View File
@@ -227,34 +227,21 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
#endif
if (isBleConnected) {
char *message;
size_t initialLen;
size_t len;
initialLen = strlen(format);
message = new char[initialLen + 1];
len = vsnprintf(message, initialLen + 1, format, arg);
if (len > initialLen) {
delete[] message;
message = new char[len + 1];
vsnprintf(message, len + 1, format, arg);
}
auto thread = concurrency::OSThread::currentThread;
meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero;
logRecord.level = getLogLevel(logLevel);
strcpy(logRecord.message, message);
vsprintf(logRecord.message, format, arg);
if (thread)
strcpy(logRecord.source, thread->ThreadName.c_str());
logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true);
uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size];
size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
#ifdef ARCH_ESP32
nimbleBluetooth->sendLog(buffer, size);
nimbleBluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF52)
nrf52Bluetooth->sendLog(buffer, size);
nrf52Bluetooth->sendLog(buffer.get(), size);
#endif
delete[] message;
delete[] buffer;
}
}
#else
@@ -292,8 +279,8 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
// append \n to format
size_t len = strlen(format);
char *newFormat = new char[len + 2];
strcpy(newFormat, format);
auto newFormat = std::unique_ptr<char[]>(new char[len + 2]);
strcpy(newFormat.get(), format);
newFormat[len] = '\n';
newFormat[len + 1] = '\0';
@@ -310,23 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
va_end(arg);
}
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
delete[] newFormat;
return;
}
}
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
delete[] newFormat;
return;
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
delete[] newFormat;
return;
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
delete[] newFormat;
return;
}
#endif
if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
delete[] newFormat;
return;
}
@@ -338,11 +320,19 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
#endif
va_list arg;
va_list arg_copy;
va_start(arg, format);
log_to_serial(logLevel, newFormat, arg);
log_to_syslog(logLevel, newFormat, arg);
log_to_ble(logLevel, newFormat, arg);
va_copy(arg_copy, arg);
log_to_serial(logLevel, newFormat.get(), arg_copy);
va_end(arg_copy);
va_copy(arg_copy, arg);
log_to_syslog(logLevel, newFormat.get(), arg_copy);
va_end(arg_copy);
log_to_ble(logLevel, newFormat.get(), arg);
va_end(arg);
#ifdef HAS_FREE_RTOS
@@ -352,7 +342,6 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
#endif
}
delete[] newFormat;
return;
}
+4 -2
View File
@@ -76,8 +76,10 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit
void NotifiedWorkerThread::checkNotification()
{
auto n = notification;
notification = 0; // clear notification
// Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification
// after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes
// in.)
auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe
if (n) {
onNotify(n);
}
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "OSThread.h"
#include <atomic>
namespace concurrency
{
@@ -13,7 +14,7 @@ class NotifiedWorkerThread : public OSThread
/**
* The notification that was most recently used to wake the thread. Read from runOnce()
*/
uint32_t notification = 0;
std::atomic<uint32_t> notification{0};
public:
NotifiedWorkerThread(const char *name) : OSThread(name) {}
+1
View File
@@ -244,6 +244,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
#define SEN5X_ADDR 0x69
#define SCD30_ADDR 0x61
// -----------------------------------------------------------------------------
// ACCELEROMETER
+2 -1
View File
@@ -91,7 +91,8 @@ class ScanI2C
CST226SE,
SEN5X,
SFA30,
CW2015
CW2015,
SCD30
} DeviceType;
// typedef uint8_t DeviceAddress;
+30 -5
View File
@@ -117,6 +117,25 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const
{
TwoWire *i2cBus = fetchI2CBus(addr);
i2cBus->beginTransmission(addr.address);
if (command > 0xFF) {
i2cBus->write((uint8_t)(command >> 8));
}
i2cBus->write((uint8_t)(command & 0xFF));
if (i2cBus->endTransmission() != 0) {
return false;
}
delay(20);
uint8_t received = i2cBus->requestFrom(addr.address, expectedLength);
bool match = (received == expectedLength);
while (i2cBus->available())
i2cBus->read();
return match;
}
/// for SEN5X detection
// Note, this code needs to be called before setting the I2C bus speed
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
@@ -432,8 +451,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
type = OPT3001;
logFoundDevice("OPT3001", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
0) { // unique SHT4x serial number (6 bytes inc. CRC)
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address);
} else {
@@ -458,13 +476,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
case LPS22HB_ADDR_ALT:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
if (registerValue != 0) {
// SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response
if (i2cCommandResponseLength(addr, 0xD060, 48)) {
type = SFA30;
logFoundDevice("SFA30", (uint8_t)addr.address);
break;
}
// TODO - What happens with these two?
// Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1)
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
if (registerValue == 0xB1) {
type = LPS22HB;
logFoundDevice("LPS22HB", (uint8_t)addr.address);
}
break;
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
@@ -557,6 +581,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address);
case CST328_ADDR:
// Do we have the CST328 or the CST226SE
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
+2
View File
@@ -55,6 +55,8 @@ class ScanI2CTwoWire : public ScanI2C
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const;
bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const;
DeviceType probeOLED(ScanI2C::DeviceAddress) const;
static void logFoundDevice(const char *device, uint8_t address);
+10 -12
View File
@@ -52,7 +52,7 @@ SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT;
HardwareSerial *GPS::_serial_gps = nullptr;
#endif
GPS *gps = nullptr;
std::unique_ptr<GPS> gps = nullptr;
static GPSUpdateScheduling scheduling;
@@ -127,7 +127,7 @@ static int32_t gpsSwitch()
return 1000;
}
static concurrency::Periodic *gpsPeriodic;
static std::unique_ptr<concurrency::Periodic> gpsPeriodic;
#endif
static void UBXChecksum(uint8_t *message, size_t length)
@@ -1485,7 +1485,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
if (bufferSize > 2048)
bufferSize = 2048;
char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
auto response = std::unique_ptr<char[]>(new char[bufferSize]); // Dynamically allocate based on baud rate
uint16_t responseLen = 0;
unsigned long start = millis();
while (millis() - start < timeout) {
@@ -1501,19 +1501,18 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
// check if we can see our chips
for (const auto &chipInfo : responseMap) {
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) {
#ifdef GPS_DEBUG
LOG_DEBUG(response);
LOG_DEBUG(response.get());
#endif
LOG_INFO("%s detected", chipInfo.chipName.c_str());
delete[] response; // Cleanup before return
return chipInfo.driver;
}
}
}
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
#ifdef GPS_DEBUG
LOG_DEBUG(response);
LOG_DEBUG(response.get());
#endif
// Reset the response buffer for the next potential message
responseLen = 0;
@@ -1522,13 +1521,12 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
}
}
#ifdef GPS_DEBUG
LOG_DEBUG(response);
LOG_DEBUG(response.get());
#endif
delete[] response; // Cleanup before return
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
}
GPS *GPS::createGps()
std::unique_ptr<GPS> GPS::createGps()
{
int8_t _rx_gpio = config.position.rx_gpio;
int8_t _tx_gpio = config.position.tx_gpio;
@@ -1553,7 +1551,7 @@ GPS *GPS::createGps()
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
return nullptr;
GPS *new_gps = new GPS;
auto new_gps = std::unique_ptr<GPS>(new GPS());
new_gps->rx_gpio = _rx_gpio;
new_gps->tx_gpio = _tx_gpio;
@@ -1581,7 +1579,7 @@ GPS *GPS::createGps()
#ifdef PIN_GPS_SWITCH
// toggle GPS via external GPIO switch
pinMode(PIN_GPS_SWITCH, INPUT);
gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch);
gpsPeriodic = std::unique_ptr<concurrency::Periodic>(new concurrency::Periodic("GPSSwitch", gpsSwitch));
#endif
// Currently disabled per issue #525 (TinyGPS++ crash bug)
+4 -2
View File
@@ -2,6 +2,8 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_GPS
#include <memory>
#include "GPSStatus.h"
#include "GpioLogic.h"
#include "Observer.h"
@@ -118,7 +120,7 @@ class GPS : private concurrency::OSThread
// Creates an instance of the GPS class.
// Returns the new instance or null if the GPS is not present.
static GPS *createGps();
static std::unique_ptr<GPS> createGps();
// Wake the GPS hardware - ready for an update
void up();
@@ -256,5 +258,5 @@ class GPS : private concurrency::OSThread
uint8_t fixeddelayCtr = 0;
};
extern GPS *gps;
extern std::unique_ptr<GPS> gps;
#endif // Exclude GPS
+1 -1
View File
@@ -312,7 +312,7 @@ const char *RtcName(RTCQuality quality)
* @param t The time to potentially set the RTC to.
* @return True if the RTC was set to the provided time, false otherwise.
*/
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t)
{
/* Convert to unix time
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970
+1 -1
View File
@@ -41,7 +41,7 @@ extern uint32_t lastSetFromPhoneNtpOrGps;
/// If we haven't yet set our RTC this boot, set it from a GPS derived time
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false);
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t);
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t);
/// Return a string name for the quality
const char *RtcName(RTCQuality quality);
+3 -3
View File
@@ -10,7 +10,7 @@ EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDI
{
// If tracking ghost pixels, grab memory
#ifdef EINK_LIMIT_GHOSTING_PX
dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros
dirtyPixels = std::unique_ptr<uint8_t[]>(new uint8_t[EInkDisplay::displayBufferSize]()); // Init with zeros
#endif
}
@@ -19,7 +19,7 @@ EInkDynamicDisplay::~EInkDynamicDisplay()
{
// If we were tracking ghost pixels, free the memory
#ifdef EINK_LIMIT_GHOSTING_PX
delete[] dirtyPixels;
dirtyPixels = nullptr;
#endif
}
@@ -454,7 +454,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting()
void EInkDynamicDisplay::resetGhostPixelTracking()
{
// Copy the current frame into dirtyPixels[] from the display buffer
memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize);
memcpy(dirtyPixels.get(), EInkDisplay::buffer, EInkDisplay::displayBufferSize);
}
#endif // EINK_LIMIT_GHOSTING_PX
+6 -5
View File
@@ -1,6 +1,7 @@
#pragma once
#include "configuration.h"
#include <memory>
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
@@ -116,11 +117,11 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
// Optional - track ghosting, pixel by pixel
// May 2024: no longer used by any display. Kept for possible future use.
#ifdef EINK_LIMIT_GHOSTING_PX
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
std::unique_ptr<uint8_t[]> dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
#endif
// Conditional - async full refresh - only with modified meshtastic/GxEPD2
+2
View File
@@ -1,4 +1,6 @@
#ifdef HAS_LP5562
#include <Wire.h>
#include <LP5562.h>
extern LP5562 rgbw;
+11 -12
View File
@@ -539,7 +539,7 @@ void menuHandler::messageResponseMenu()
// If viewing ALL chats, hide “Mute Chat”
if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) {
const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex();
auto &chan = channels.getByIndex(chIndex);
const auto &chan = channels.getByIndex(chIndex);
optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel";
optionsEnumArray[options++] = MuteChannel;
@@ -831,7 +831,7 @@ void menuHandler::messageViewModeMenu()
// Gather unique peers
auto dms = messageStore.getDirectMessages();
std::vector<uint32_t> uniquePeers;
for (auto &m : dms) {
for (const auto &m : dms) {
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
@@ -1397,7 +1397,7 @@ void menuHandler::manageNodeMenu()
}
if (selected == Favorite) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
@@ -2292,14 +2292,13 @@ void menuHandler::wifiToggleMenu()
void menuHandler::screenOptionsMenu()
{
// Check if brightness is supported
bool hasSupportBrightness = false;
#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
hasSupportBrightness = true;
#endif
#if defined(T_DECK)
// TDeck Doesn't seem to support brightness at all, at least not reliably
hasSupportBrightness = false;
bool hasSupportBrightness = false;
#elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
bool hasSupportBrightness = true;
#else
bool hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
@@ -2444,7 +2443,7 @@ void menuHandler::frameTogglesMenu()
nodelist_hopsignal,
nodelist_distance,
nodelist_bearings,
gps,
gps_position,
lora,
clock,
show_favorites,
@@ -2482,7 +2481,7 @@ void menuHandler::frameTogglesMenu()
#endif
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps;
optionsEnumArray[options++] = gps_position;
#endif
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
@@ -2545,7 +2544,7 @@ void menuHandler::frameTogglesMenu()
screen->toggleFrameVisibility("nodelist_bearings");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == gps) {
} else if (selected == gps_position) {
screen->toggleFrameVisibility("gps");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
+10 -13
View File
@@ -171,7 +171,7 @@ unsigned long getModeCycleIntervalMs()
int calculateMaxScroll(int totalEntries, int visibleRows)
{
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
return max(0, (totalEntries - 1) / (visibleRows * 2));
}
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
@@ -187,13 +187,12 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
if (totalEntries <= visibleNodeRows * columns)
return;
int scrollbarX = display->getWidth() - 2;
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
int perPage = visibleNodeRows * columns;
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
int thumbHeight = max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) /
max(1, max(0, (totalEntries - 1) / (visibleNodeRows * columns)));
int scrollbarX = display->getWidth() - 2;
for (int i = 0; i < thumbHeight; i++) {
display->setPixel(scrollbarX, thumbY + i);
}
@@ -556,13 +555,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
int maxScroll = 0;
if (perPage > 0) {
maxScroll = std::max(0, (totalEntries - 1) / perPage);
maxScroll = max(0, (totalEntries - 1) / perPage);
}
if (scrollIndex > maxScroll)
scrollIndex = maxScroll;
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int endIndex = min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int yOffset = 0;
int col = 0;
int lastNodeY = y;
@@ -580,7 +579,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
if (extras)
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
yOffset += rowYOffset;
shownCount++;
rowCount++;
@@ -613,13 +612,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
if (millis() - popupTime < POPUP_DURATION_MS) {
popupTotal = totalEntries;
int perPage = visibleNodeRows * totalColumns;
popupStart = startIndex + 1;
popupEnd = std::min(startIndex + perPage, totalEntries);
popupEnd = min(startIndex + perPage, totalEntries);
popupPage = (scrollIndex + 1);
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
popupMaxPage = max(1, (totalEntries + perPage - 1) / perPage);
char buf[32];
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
+9 -1
View File
@@ -12,6 +12,7 @@
#include "graphics/TimeFormatters.h"
#include "graphics/images.h"
#include "main.h"
#include "modules/CannedMessageModule.h"
#include "target_specific.h"
#include <OLEDDisplay.h>
#include <RTC.h>
@@ -288,7 +289,8 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
// **********************
// * Favorite Node Info *
// **********************
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
if (favoritedNodes.empty())
return;
@@ -1388,6 +1390,7 @@ static int8_t lastFrameIndex = -1;
static uint32_t lastFrameChangeTime = 0;
constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
{
int currentFrame = state->currentFrame;
@@ -1420,6 +1423,11 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
// Suppress nav overlay entirely
if (cannedMessageModule && cannedMessageModule->isFreeTextUIActive()) {
return;
}
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
+1 -1
View File
@@ -49,7 +49,7 @@ class UIRenderer
// Navigation bar overlay
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
@@ -42,7 +42,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
{
// Contingency only
// - pin wasn't set
if (pin != (uint8_t)-1) {
if (pin != static_cast<uint8_t>(-1)) {
off();
pinMode(pin, INPUT); // High impedance - unnecessary?
} else
@@ -55,7 +55,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
void LatchingBacklight::peek()
{
assert(pin != (uint8_t)-1);
assert(pin != static_cast<uint8_t>(-1));
digitalWrite(pin, logicActive); // On
on = true;
latched = false;
@@ -67,7 +67,7 @@ void LatchingBacklight::peek()
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
void LatchingBacklight::latch()
{
assert(pin != (uint8_t)-1);
assert(pin != static_cast<uint8_t>(-1));
// Blink if moving from peek to latch
// Indicates to user that the transition has taken place
@@ -89,7 +89,7 @@ void LatchingBacklight::latch()
// Suitable for ending both peek and latch
void LatchingBacklight::off()
{
assert(pin != (uint8_t)-1);
assert(pin != static_cast<uint8_t>(-1));
digitalWrite(pin, !logicActive); // Off
on = false;
latched = false;
@@ -40,7 +40,7 @@ class LatchingBacklight
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
uint8_t pin = (uint8_t)-1;
uint8_t pin = static_cast<uint8_t>(-1);
bool logicActive = HIGH; // Is light active HIGH or active LOW
bool on = false; // Is light on (either peek or latched)
+30 -15
View File
@@ -34,7 +34,7 @@ void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
{
// Only render pixels if they fall within user's cropped region
if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight))
assignedTile->handleAppletPixel(x, y, (Color)color);
assignedTile->handleAppletPixel(x, y, static_cast<Color>(color));
}
// Link our applet to a tile
@@ -144,6 +144,21 @@ void InkHUD::Applet::resetDrawingSpace()
setFont(fontSmall);
}
// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it
void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured)
{
if (captured)
subscribedInputs |= input;
else
subscribedInputs &= ~input;
}
// Checks if a specific input is enabled for this applet and should be sent to it
bool InkHUD::Applet::isInputSubscribed(InputMask input)
{
return (subscribedInputs & input) == input;
}
// Tell InkHUD::Renderer that we want to render now
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
// When an applet decides it has heard something important, and wants to redraw, it calls this method
@@ -312,7 +327,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalA
}
// Print text, specifying the position of any edge / corner of the textbox
void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va)
void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va)
{
printAt(x, y, text.c_str(), ha, va);
}
@@ -334,7 +349,7 @@ InkHUD::AppletFont InkHUD::Applet::getFont()
// Parse any text which might have "special characters"
// Re-encodes UTF-8 characters to match our 8-bit encoded fonts
std::string InkHUD::Applet::parse(std::string text)
std::string InkHUD::Applet::parse(const std::string &text)
{
return getFont().decodeUTF8(text);
}
@@ -361,10 +376,10 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
}
// Determine if all characters of a string are printable using the current font
bool InkHUD::Applet::isPrintable(std::string text)
bool InkHUD::Applet::isPrintable(const std::string &text)
{
// Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled
for (char &c : text) {
for (const char &c : text) {
if (c == '\x1A')
return false;
}
@@ -387,7 +402,7 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text)
// Gets rendered width of a string
// Wrapper for getTextBounds
uint16_t InkHUD::Applet::getTextWidth(std::string text)
uint16_t InkHUD::Applet::getTextWidth(const std::string &text)
{
return getTextWidth(text.c_str());
}
@@ -435,7 +450,7 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
// Print text, with word wrapping
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text)
{
// Place the AdafruitGFX cursor to suit our "top" coord
setCursor(left, top + getFont().heightAboveCursor());
@@ -492,15 +507,15 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std
// Todo: rewrite making use of AdafruitGFX native text wrapping
char cstr[] = {0, 0};
int16_t l, t;
uint16_t w, h;
int16_t bx, by;
uint16_t bw, bh;
for (uint16_t c = 0; c < word.length(); c++) {
// Shove next char into a c string
cstr[0] = word[c];
getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h);
getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh);
// Manual newline, if next character will spill beyond screen edge
if ((l + w) > left + width)
if ((bx + bw) > left + width)
setCursor(left, getCursorY() + getFont().lineHeight());
// Print next character
@@ -519,7 +534,7 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std
// Simulate running printWrapped, to determine how tall the block of text will be.
// This is a wasteful way of handling things. Maybe some way to optimize in future?
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text)
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text)
{
// Cache the current crop region
int16_t cL = cropLeft;
@@ -649,7 +664,7 @@ uint16_t InkHUD::Applet::getActiveNodeCount()
// For each node in db
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Check if heard recently, and not our own node
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
@@ -702,7 +717,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters)
}
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY)
{
// How many times to draw along x axis
int16_t xStart;
@@ -770,7 +785,7 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n)
│ │
└───────────────────────────────┘
*/
void InkHUD::Applet::drawHeader(std::string text)
void InkHUD::Applet::drawHeader(const std::string &text)
{
// Y position for divider
// - between header text and messages
+31 -8
View File
@@ -89,6 +89,9 @@ class Applet : public GFX
virtual void onForeground() {}
virtual void onBackground() {}
virtual void onShutdown() {}
// Input Events
virtual void onButtonShortPress() {}
virtual void onButtonLongPress() {}
virtual void onExitShort() {}
@@ -100,6 +103,18 @@ class Applet : public GFX
virtual void onFreeText(char c) {}
virtual void onFreeTextDone() {}
virtual void onFreeTextCancel() {}
// List of inputs which can be subscribed to
enum InputMask { // | No Joystick | With Joystick |
BUTTON_SHORT = 1, // | Button Click | Joystick Center Click |
BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold |
EXIT_SHORT = 4, // | no-op | Back Button Click |
EXIT_LONG = 8, // | no-op | Back Button Hold |
NAV_UP = 16, // | no-op | Joystick Up |
NAV_DOWN = 32, // | no-op | Joystick Down |
NAV_LEFT = 64, // | no-op | Joystick Left |
NAV_RIGHT = 128 // | no-op | Joystick Right |
};
bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded.
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
@@ -121,20 +136,28 @@ class Applet : public GFX
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
void resetCrop(); // Removes setCrop()
// User Input Handling
uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs
void setInputsSubscribed(uint8_t input,
bool captured); // Set if an input should be handled by applet or not, this should not be
// overloaded. Can take multiple inputs at once if you OR/`|` them together
// Text
void setFont(AppletFont f);
AppletFont getFont();
uint16_t getTextWidth(std::string text);
uint16_t getTextWidth(const std::string &text);
uint16_t getTextWidth(const char *text);
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX,
uint8_t thicknessY); // Faux bold
void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
void drawHeader(std::string text); // Draw the standard applet header
void drawHeader(const std::string &text); // Draw the standard applet header
// Meshtastic Logo
@@ -150,9 +173,9 @@ class Applet : public GFX
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
std::string parse(std::string text); // Handle text which might contain special chars
std::string parse(const std::string &text); // Handle text which might contain special chars
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
bool isPrintable(std::string); // Check for characters which the font can't print
bool isPrintable(const std::string &text); // Check for characters which the font can't print
// Convenient references
+7 -7
View File
@@ -39,11 +39,11 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
// Caution: signed and unsigned types
int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset;
if (glyphAscender > 0)
this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender);
this->ascenderHeight = max(this->ascenderHeight, static_cast<uint8_t>(glyphAscender));
int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset;
if (glyphDescender > 0)
this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender);
this->descenderHeight = max(this->descenderHeight, static_cast<uint8_t>(glyphDescender));
}
// Apply any manual padding to grow or shrink the line size
@@ -52,7 +52,7 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
descenderHeight += paddingBottom;
// Find how far the cursor advances when we "print" a space character
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
spaceCharWidth = gfxFont->glyph[static_cast<uint8_t>(' ') - gfxFont->first].xAdvance;
}
/*
@@ -98,7 +98,7 @@ uint8_t InkHUD::AppletFont::widthBetweenWords()
// Convert a unicode char from set of UTF-8 bytes to UTF-32
// Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value
uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8)
{
uint32_t utf32 = 0;
@@ -132,7 +132,7 @@ uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
// Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII
// Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars
std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded)
{
// Final processed output
std::string decoded;
@@ -141,7 +141,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
std::string utf8Char;
uint8_t utf8CharSize = 0;
for (char &c : encoded) {
for (const char &c : encoded) {
// If first byte
if (utf8Char.empty()) {
@@ -178,7 +178,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
// Re-encode a single UTF-8 character to extended ASCII
// Target encoding depends on the font
char InkHUD::AppletFont::applyEncoding(std::string utf8)
char InkHUD::AppletFont::applyEncoding(const std::string &utf8)
{
// ##################################################### Syntactic Sugar #####################################################
#define REMAP(in, out) \
+6 -5
View File
@@ -30,20 +30,21 @@ class AppletFont
};
AppletFont();
AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0);
explicit AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0,
int8_t paddingBottom = 0);
uint8_t lineHeight();
uint8_t heightAboveCursor();
uint8_t heightBelowCursor();
uint8_t widthBetweenWords(); // Width of the space character
std::string decodeUTF8(std::string encoded);
std::string decodeUTF8(const std::string &encoded);
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
const GFXfont *gfxFont = nullptr; // Default value: in-built AdafruitGFX font
private:
uint32_t toUtf32(std::string utf8);
char applyEncoding(std::string utf8);
uint32_t toUtf32(const std::string &utf8);
char applyEncoding(const std::string &utf8);
uint8_t height = 8; // Default value: in-built AdafruitGFX font
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
@@ -81,25 +81,25 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
uint8_t InkHUD::NodeListApplet::maxCards()
{
// Cache result. Shouldn't change during execution
static uint8_t cards = 0;
static uint8_t maxCardCount = 0;
if (!cards) {
if (!maxCardCount) {
const uint16_t height = Tile::maxDisplayDimension();
// Use a loop instead of arithmetic, because it's easier for my brain to follow
// Add cards one by one, until the latest card extends below screen
uint16_t y = cardH; // First card: no margin above
cards = 1;
maxCardCount = 1;
while (y < height) {
y += cardMarginH;
y += cardH;
cards++;
maxCardCount++;
}
}
return cards;
return maxCardCount;
}
// Draw, using info which derived applet placed into NodeListApplet::cards for us
@@ -137,12 +137,12 @@ void InkHUD::NodeListApplet::onRender(bool full)
// Gather info
// ========================================
NodeNum &nodeNum = card->nodeNum;
const NodeNum &nodeNum = card->nodeNum;
SignalStrength &signal = card->signal;
std::string longName; // handled below
std::string shortName; // handled below
std::string distance; // handled below;
uint8_t &hopsAway = card->hopsAway;
const uint8_t &hopsAway = card->hopsAway;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
@@ -0,0 +1,79 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./UserAppletInputExample.h"
using namespace NicheGraphics;
void InkHUD::UserAppletInputExampleApplet::onActivate()
{
setGrabbed(false);
}
void InkHUD::UserAppletInputExampleApplet::onRender(bool full)
{
drawHeader("Input Example");
uint16_t headerHeight = getHeaderHeight();
std::string buttonName;
if (settings->joystick.enabled)
buttonName = "joystick center button";
else
buttonName = "user button";
std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls";
if (!isGrabbed)
additional = " | Control is released, long press " + buttonName + " to grab controls";
printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional);
}
void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed)
{
isGrabbed = grabbed;
setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT,
grabbed); // Enables/disables grabbing all inputs
setInputsSubscribed(BUTTON_LONG, true); // Always grab this input
}
void InkHUD::UserAppletInputExampleApplet::onButtonShortPress()
{
lastInput = "BUTTON_SHORT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onButtonLongPress()
{
lastInput = "BUTTON_LONG";
setGrabbed(!isGrabbed);
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onExitShort()
{
lastInput = "EXIT_SHORT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onExitLong()
{
lastInput = "EXIT_LONG";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavUp()
{
lastInput = "NAV_UP";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavDown()
{
lastInput = "NAV_DOWN";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavLeft()
{
lastInput = "NAV_LEFT";
requestUpdate();
}
void InkHUD::UserAppletInputExampleApplet::onNavRight()
{
lastInput = "NAV_RIGHT";
requestUpdate();
}
#endif
@@ -0,0 +1,36 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applet.h"
namespace NicheGraphics::InkHUD
{
class UserAppletInputExampleApplet : public Applet
{
public:
void onActivate() override;
void onRender(bool full) override;
void onButtonShortPress() override;
void onButtonLongPress() override;
void onExitShort() override;
void onExitLong() override;
void onNavUp() override;
void onNavDown() override;
void onNavLeft() override;
void onNavRight() override;
private:
std::string lastInput = "None";
bool isGrabbed = false;
void setGrabbed(bool grabbed);
};
} // namespace NicheGraphics::InkHUD
#endif
@@ -29,10 +29,10 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
// If we get a different type of status, something has gone weird elsewhere
assert(status->getStatusType() == STATUS_TYPE_POWER);
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
const meshtastic::PowerStatus *pwrStatus = (const meshtastic::PowerStatus *)status;
// Get the new state of charge %, and round to the nearest 10%
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
uint8_t newSocRounded = ((pwrStatus->getBatteryChargePercent() + 5) / 10) * 10;
// If rounded value has changed, trigger a display update
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
@@ -1176,7 +1176,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("Back", previousPage));
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
meshtastic_Channel &ch = channels.getByIndex(i);
const meshtastic_Channel &ch = channels.getByIndex(i);
if (!ch.has_settings)
continue;
@@ -1252,7 +1252,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
case NODE_CONFIG_CHANNEL_PRECISION: {
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
items.push_back(MenuItem("Back", previousPage));
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
const meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
break;
@@ -1524,7 +1524,15 @@ void InkHUD::MenuApplet::onButtonShortPress()
if (!settings->joystick.enabled) {
if (!cursorShown) {
cursorShown = true;
// Select the first item that isn't a header
cursor = 0;
while (cursor < items.size() && items.at(cursor).isHeader) {
cursor++;
}
if (cursor >= items.size()) {
cursorShown = false;
cursor = 0;
}
} else {
do {
cursor = (cursor + 1) % items.size();
@@ -1576,7 +1584,15 @@ void InkHUD::MenuApplet::onNavUp()
if (!cursorShown) {
cursorShown = true;
cursor = 0;
// Select the last item that isn't a header
cursor = items.size() - 1;
while (items.at(cursor).isHeader) {
if (cursor == 0) {
cursorShown = false;
break;
}
cursor--;
}
} else {
do {
if (cursor == 0)
@@ -1597,7 +1613,15 @@ void InkHUD::MenuApplet::onNavDown()
if (!cursorShown) {
cursorShown = true;
// Select the first item that isn't a header
cursor = 0;
while (cursor < items.size() && items.at(cursor).isHeader) {
cursor++;
}
if (cursor >= items.size()) {
cursorShown = false;
cursor = 0;
}
} else {
do {
cursor = (cursor + 1) % items.size();
@@ -1759,7 +1783,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
// Get the channel, and check if it's enabled
meshtastic_Channel &channel = channels.getByIndex(i);
const meshtastic_Channel &channel = channels.getByIndex(i);
if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED)
continue;
@@ -1829,7 +1853,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
items.push_back(MenuItem("Exit", MenuPage::EXIT));
}
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text)
{
setFont(fontSmall);
uint16_t wrapMaxH = 0;
@@ -55,7 +55,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
std::string text); // Draw input field for free text
const std::string &text); // Draw input field for free text
uint16_t getSystemInfoPanelHeight();
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
uint16_t *height = nullptr); // Info panel at top of root menu
@@ -228,17 +228,17 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
// Pick source of message
MessageStore::Message *message =
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
const MessageStore::Message *message =
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
// Find info about the sender
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
// Leading tag (channel vs. DM)
text += isBroadcast ? "From:" : "DM: ";
text += msgIsBroadcast ? "From:" : "DM: ";
// Sender id
if (node && node->has_user)
@@ -252,7 +252,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text.clear();
// Leading tag (channel vs. DM)
text += isBroadcast ? "Msg from " : "DM from ";
text += msgIsBroadcast ? "Msg from " : "DM from ";
// Sender id
if (node && node->has_user)
@@ -55,12 +55,12 @@ int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *sta
// We'll mimic that behavior, just to keep in line with the other Statuses,
// even though I'm not sure what the original reason for jumping through these extra hoops was.
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
const auto *btStatus = static_cast<const meshtastic::BluetoothStatus *>(status);
// When pairing begins
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
if (btStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
// Store the passkey for rendering
passkey = bluetoothStatus->getPasskey();
passkey = btStatus->getPasskey();
// Show pairing screen
bringToForeground();
@@ -0,0 +1,111 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./FavoritesMapApplet.h"
#include "NodeDB.h"
using namespace NicheGraphics;
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
{
// Keep our own node available as map anchor/center; all others must be favorited.
return node && (node->num == nodeDB->getNodeNum() || node->is_favorite);
}
void InkHUD::FavoritesMapApplet::onRender(bool full)
{
// Custom empty state text for favorites-only map.
if (!enoughMarkers()) {
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Favorite node position", CENTER, MIDDLE);
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
return;
}
// Draw the usual map applet first.
MapApplet::onRender(full);
// Draw our latest "node of interest" as a special marker.
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers())
drawLabeledMarker(node);
}
// Determine if we need to redraw the map, when we receive a new position packet.
ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshPacket &mp)
{
// If applet is not active, we shouldn't be handling any data.
if (!isActive())
return ProcessMessage::CONTINUE;
// Try decode a position from the packet.
bool hasPosition = false;
float lat;
float lng;
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
meshtastic_Position position = meshtastic_Position_init_default;
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
if (position.has_latitude_i && position.has_longitude_i // Actually has position
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
{
hasPosition = true;
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
lng = position.longitude_i * 1e-7;
}
}
}
// Skip if we didn't get a valid position.
if (!hasPosition)
return ProcessMessage::CONTINUE;
const int8_t hopsAway = getHopsAway(mp);
const bool hasHopsAway = hopsAway >= 0;
// Determine if the position packet would change anything on-screen.
bool somethingChanged = false;
// If our own position.
if (isFromUs(&mp)) {
// Ignore tiny local movement to reduce update spam.
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
somethingChanged = true;
ourLastLat = lat;
ourLastLng = lng;
}
} else {
// For non-local packets, this applet only reacts to favorited nodes.
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
if (!sender || !sender->is_favorite)
return ProcessMessage::CONTINUE;
// Check if this position is from someone different than our previous position packet.
if (mp.from != lastFrom) {
somethingChanged = true;
lastFrom = mp.from;
lastLat = lat;
lastLng = lng;
lastHopsAway = hopsAway;
}
// Same sender: check if position changed.
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
somethingChanged = true;
lastLat = lat;
lastLng = lng;
}
// Same sender, same position: check if hops changed.
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
somethingChanged = true;
lastHopsAway = hopsAway;
}
}
if (somethingChanged) {
requestAutoshow();
requestUpdate();
}
return ProcessMessage::CONTINUE;
}
#endif
@@ -0,0 +1,44 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
Plots position of favorited nodes from DB, with North facing up.
Scaled to fit the most distant node.
Size of marker represents hops away.
The favorite node which most recently sent a position will be labeled.
*/
#pragma once
#include "configuration.h"
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
#include "SinglePortModule.h"
namespace NicheGraphics::InkHUD
{
class FavoritesMapApplet : public MapApplet, public SinglePortModule
{
public:
FavoritesMapApplet() : SinglePortModule("FavoritesMapApplet", meshtastic_PortNum_POSITION_APP) {}
void onRender(bool full) override;
protected:
bool shouldDrawNode(meshtastic_NodeInfoLite *node) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
NodeNum lastFrom = 0; // Sender of most recent favorited (non-local) position packet
float lastLat = 0.0;
float lastLng = 0.0;
float lastHopsAway = 0;
float ourLastLat = 0.0; // Info about most recent local position
float ourLastLng = 0.0;
};
} // namespace NicheGraphics::InkHUD
#endif
@@ -69,9 +69,10 @@ void InkHUD::HeardApplet::populateFromNodeDB()
}
// Sort the collection by age
std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool {
return (top->last_heard > bottom->last_heard);
});
std::sort(ordered.begin(), ordered.end(),
[](const meshtastic_NodeInfoLite *top, const meshtastic_NodeInfoLite *bottom) -> bool {
return (top->last_heard > bottom->last_heard);
});
// Keep the most recent entries only
// Just enough to fill the screen
@@ -69,7 +69,7 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full)
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
// Grab data for message
MessageStore::Message &m = store->messages.at(i);
const MessageStore::Message &m = store->messages.at(i);
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
+61 -12
View File
@@ -59,10 +59,16 @@ void InkHUD::Events::onButtonShort()
if (consumer) {
consumer->onButtonShortPress();
} else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
if (!settings->joystick.enabled)
inkhud->nextApplet();
else
inkhud->openMenu();
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT))
userConsumer->onButtonShortPress();
else {
if (!settings->joystick.enabled)
inkhud->nextApplet();
else
inkhud->openMenu();
}
}
}
@@ -84,8 +90,14 @@ void InkHUD::Events::onButtonLong()
// If no system applet is handling input, default behavior instead is to open the menu
if (consumer)
consumer->onButtonLongPress();
else
inkhud->openMenu();
else {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG))
userConsumer->onButtonLongPress();
else
inkhud->openMenu();
}
}
void InkHUD::Events::onExitShort()
@@ -110,8 +122,14 @@ void InkHUD::Events::onExitShort()
// If no system applet is handling input, default behavior instead is change tiles
if (consumer)
consumer->onExitShort();
else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
inkhud->nextTile();
else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT))
userConsumer->onExitShort();
else
inkhud->nextTile();
}
}
}
@@ -133,6 +151,13 @@ void InkHUD::Events::onExitLong()
if (consumer)
consumer->onExitLong();
else {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG))
userConsumer->onExitLong();
// Nothing uses exit long yet
}
}
}
@@ -157,6 +182,12 @@ void InkHUD::Events::onNavUp()
if (consumer)
consumer->onNavUp();
else if (!dismissedExt) {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP))
userConsumer->onNavUp();
}
}
}
@@ -181,6 +212,12 @@ void InkHUD::Events::onNavDown()
if (consumer)
consumer->onNavDown();
else if (!dismissedExt) {
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN))
userConsumer->onNavDown();
}
}
}
@@ -206,8 +243,14 @@ void InkHUD::Events::onNavLeft()
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavLeft();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->prevApplet();
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT))
userConsumer->onNavLeft();
else
inkhud->prevApplet();
}
}
}
@@ -233,8 +276,14 @@ void InkHUD::Events::onNavRight()
// If no system applet is handling input, default behavior instead is to cycle applets
if (consumer)
consumer->onNavRight();
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
inkhud->nextApplet();
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
Applet *userConsumer = inkhud->getActiveApplet();
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT))
userConsumer->onNavRight();
else
inkhud->nextApplet();
}
}
}
+6
View File
@@ -210,6 +210,12 @@ void InkHUD::InkHUD::prevApplet()
windowManager->prevApplet();
}
// Returns the currently active applet
InkHUD::Applet *InkHUD::InkHUD::getActiveApplet()
{
return windowManager->getActiveApplet();
}
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::InkHUD::openMenu()
+1
View File
@@ -74,6 +74,7 @@ class InkHUD
void nextApplet();
void prevApplet();
NicheGraphics::InkHUD::Applet *getActiveApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
+15 -13
View File
@@ -12,7 +12,7 @@ using namespace NicheGraphics;
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
InkHUD::MessageStore::MessageStore(std::string label)
InkHUD::MessageStore::MessageStore(const std::string &label)
{
filename = "";
filename += "/NicheGraphics";
@@ -50,12 +50,13 @@ void InkHUD::MessageStore::saveToFlash()
// For each message
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
Message &m = messages.at(i);
f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes
f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte
f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i), min(MAX_MESSAGE_SIZE, m.text.size()),
m.text.c_str());
}
// Release firmware's SPI lock, because SafeFile::close needs it
@@ -111,17 +112,17 @@ void InkHUD::MessageStore::loadFromFlash()
// First byte: how many messages are in the flash store
uint8_t flashMessageCount = 0;
f.readBytes((char *)&flashMessageCount, 1);
LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount);
f.readBytes(reinterpret_cast<char *>(&flashMessageCount), 1);
LOG_DEBUG("Messages available: %u", static_cast<uint32_t>(flashMessageCount));
// For each message
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
Message m;
// Read meta data (fixed width)
f.readBytes((char *)&m.timestamp, sizeof(m.timestamp));
f.readBytes((char *)&m.sender, sizeof(m.sender));
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
f.readBytes(reinterpret_cast<char *>(&m.timestamp), sizeof(m.timestamp));
f.readBytes(reinterpret_cast<char *>(&m.sender), sizeof(m.sender));
f.readBytes(reinterpret_cast<char *>(&m.channelIndex), sizeof(m.channelIndex));
// Read characters until we find a null term
char c;
@@ -136,7 +137,8 @@ void InkHUD::MessageStore::loadFromFlash()
// Store in RAM
messages.push_back(m);
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str());
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast<uint32_t>(i), m.timestamp, m.sender,
m.text.c_str());
}
f.close();
+1 -1
View File
@@ -31,7 +31,7 @@ class MessageStore
};
MessageStore() = delete;
explicit MessageStore(std::string label); // Label determines filename in flash
explicit MessageStore(const std::string &label); // Label determines filename in flash
void saveToFlash();
void loadFromFlash();
@@ -8,5 +8,5 @@ build_flags =
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps =
# TODO renovate
https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
# renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root
https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX
+13 -13
View File
@@ -269,42 +269,42 @@ void InkHUD::Renderer::clearTile(Tile *t)
// Rotate the tile dimensions
int16_t left = 0;
int16_t top = 0;
uint16_t width = 0;
uint16_t height = 0;
uint16_t tileW = 0;
uint16_t tileH = 0;
switch (settings->rotation) {
case 0:
left = t->getLeft();
top = t->getTop();
width = t->getWidth();
height = t->getHeight();
tileW = t->getWidth();
tileH = t->getHeight();
break;
case 1:
left = driver->width - (t->getTop() + t->getHeight());
top = t->getLeft();
width = t->getHeight();
height = t->getWidth();
tileW = t->getHeight();
tileH = t->getWidth();
break;
case 2:
left = driver->width - (t->getLeft() + t->getWidth());
top = driver->height - (t->getTop() + t->getHeight());
width = t->getWidth();
height = t->getHeight();
tileW = t->getWidth();
tileH = t->getHeight();
break;
case 3:
left = t->getTop();
top = driver->height - (t->getLeft() + t->getWidth());
width = t->getHeight();
height = t->getWidth();
tileW = t->getHeight();
tileH = t->getWidth();
break;
}
// Calculate the bounds to clear
uint16_t xStart = (left < 0) ? 0 : left;
uint16_t yStart = (top < 0) ? 0 : top;
if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0)
if (xStart >= driver->width || yStart >= driver->height || left + tileW < 0 || top + tileH < 0)
return; // the box is completely off the screen
uint16_t xEnd = left + width;
uint16_t yEnd = top + height;
uint16_t xEnd = left + tileW;
uint16_t yEnd = top + tileH;
if (xEnd > driver->width)
xEnd = driver->width;
if (yEnd > driver->height)
+7 -1
View File
@@ -273,6 +273,12 @@ void InkHUD::WindowManager::prevApplet()
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// Returns active applet
NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet()
{
return userTiles.at(settings->userTiles.focused)->getAssignedApplet();
}
// Rotate the display image by 90 degrees
void InkHUD::WindowManager::rotate()
{
@@ -396,7 +402,7 @@ void InkHUD::WindowManager::autoshow()
{
// Don't perform autoshow if a system applet has exclusive use of the display right now
// Note: lockRequests prevents autoshow attempting to hide menuApplet
for (SystemApplet *sa : inkhud->systemApplets) {
for (const SystemApplet *sa : inkhud->systemApplets) {
if (sa->lockRendering || sa->lockRequests)
return;
}
@@ -29,6 +29,7 @@ class WindowManager
void nextTile();
void prevTile();
Applet *getActiveApplet();
void openMenu();
void openAlignStick();
void openKeyboard();
@@ -146,7 +146,7 @@ void CannedMessageStore::handleGet(meshtastic_AdminMessage *response)
std::string merged;
if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0
merged.reserve(201);
for (std::string &s : messages) {
for (const std::string &s : messages) {
merged += s;
merged += '|';
}
+4 -5
View File
@@ -106,8 +106,8 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
{}};
HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
reset();
}
@@ -147,7 +147,6 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
@@ -187,8 +186,8 @@ void HackadayCommunicatorKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -18,8 +18,8 @@ class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+3 -3
View File
@@ -91,7 +91,7 @@ MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(null
{
// LOG_DEBUG("MPR121 @ %02x", m_addr);
state = Init;
last_key = -1;
last_key = UINT8_MAX;
last_tap = 0L;
char_idx = 0;
queue = "";
@@ -359,8 +359,8 @@ void MPR121Keyboard::released()
return;
}
// would clear longpress callback... later.
if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = -1;
if (last_key >= _NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = UINT8_MAX;
state = Idle;
return;
}
+1 -1
View File
@@ -14,7 +14,7 @@ class MPR121Keyboard
MPR121States state;
int8_t last_key;
uint8_t last_key;
uint32_t last_tap;
uint8_t char_idx;
+5 -6
View File
@@ -43,8 +43,8 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
};
TCA8418Keyboard::TCA8418Keyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0),
should_backspace(false)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0),
tap_interval(0), should_backspace(false)
{
}
@@ -63,7 +63,6 @@ void TCA8418Keyboard::pressed(uint8_t key)
if (state == Init || state == Busy) {
return;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -72,7 +71,7 @@ void TCA8418Keyboard::pressed(uint8_t key)
}
// Compute key index based on dynamic row/column
next_key = row * _TCA8418_COLS + col;
next_key = (uint8_t)(row * _TCA8418_COLS + col);
// LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key);
@@ -106,8 +105,8 @@ void TCA8418Keyboard::released()
return;
}
if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -14,8 +14,8 @@ class TCA8418Keyboard : public TCA8418KeyboardBase
void pressed(uint8_t key) override;
void released(void) override;
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+4 -5
View File
@@ -62,8 +62,8 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = {
};
TDeckProKeyboard::TDeckProKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
}
@@ -101,7 +101,6 @@ void TDeckProKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -142,8 +141,8 @@ void TDeckProKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -19,8 +19,8 @@ class TDeckProKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+4 -5
View File
@@ -65,8 +65,8 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'},
{' ', 0x00, Key::BL_TOGGLE}};
TLoraPagerKeyboard::TLoraPagerKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
{
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
@@ -129,7 +129,6 @@ void TLoraPagerKeyboard::pressed(uint8_t key)
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
@@ -170,8 +169,8 @@ void TLoraPagerKeyboard::released()
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
if (last_key >= _TCA8418_NUM_KEYS) {
last_key = UINT8_MAX;
state = Idle;
return;
}
+2 -2
View File
@@ -21,8 +21,8 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint8_t last_key;
uint8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
+18 -24
View File
@@ -192,8 +192,6 @@ bool kb_found = false;
// global bool to record that on-screen keyboard (OSK) is present
bool osk_found = false;
unsigned long last_listen = 0;
// The I2C address of the RTC Module (if found)
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Accelerometer (if found)
@@ -249,9 +247,7 @@ static OSThread *powerFSMthread;
OSThread *ambientLightingThread;
RadioInterface *rIf = NULL;
#ifdef ARCH_PORTDUINO
RadioLibHal *RadioLibHAL = NULL;
#endif
/**
* Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep.
@@ -725,17 +721,15 @@ void setup()
playStartMelody();
#if HAS_SCREEN
// fixed screen override?
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
screen_model = config.display.oled;
// fixed screen override?
#if defined(USE_SH1107)
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128
screen_geometry = GEOMETRY_128_128;
#endif
#if defined(USE_SH1107_128_64)
#elif defined(USE_SH1107_128_64)
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
#else
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
screen_model = config.display.oled;
#endif
#endif
@@ -954,7 +948,7 @@ void setup()
#endif
#endif
initLoRa();
auto rIf = initLoRa();
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
@@ -1003,12 +997,12 @@ void setup()
if (!rIf)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO);
else {
router->addInterface(rIf);
// Log bit rate to debug output
LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) /
(float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) *
1000);
router->addInterface(std::move(rIf));
}
// This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values
@@ -1125,10 +1119,12 @@ void loop()
#endif
power->powerCommandsCheck();
if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) &&
!(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) {
RadioLibInterface::instance->startReceive();
LOG_DEBUG("attempting AGC reset");
if (RadioLibInterface::instance != nullptr) {
static uint32_t lastRadioMissedIrqPoll;
if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) {
lastRadioMissedIrqPoll = millis();
RadioLibInterface::instance->pollMissedIrqs();
}
}
#ifdef DEBUG_STACK
@@ -1150,10 +1146,7 @@ void loop()
}
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
LOG_ERROR("LoRa in error detected, attempting to recover");
if (rIf != nullptr) {
delete rIf;
rIf = nullptr;
}
router->addInterface(nullptr);
if (portduino_config.lora_spi_dev == "ch341") {
if (ch341Hal != nullptr) {
delete ch341Hal;
@@ -1169,8 +1162,9 @@ void loop()
exit(EXIT_FAILURE);
}
}
if (initLoRa()) {
router->addInterface(rIf);
auto rIf = initLoRa();
if (rIf) {
router->addInterface(std::move(rIf));
portduino_status.LoRa_in_error = false;
} else {
LOG_WARN("Reconfigure failed, rebooting");
-1
View File
@@ -33,7 +33,6 @@ extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model;
extern bool kb_found;
extern bool osk_found;
extern unsigned long last_listen;
extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::FoundDevice rgb_found;
+5 -6
View File
@@ -1,6 +1,7 @@
#include "CryptoEngine.h"
// #include "NodeDB.h"
#include "architecture.h"
#include <memory>
#if !(MESHTASTIC_EXCLUDE_PKI)
#include "NodeDB.h"
@@ -169,10 +170,9 @@ void CryptoEngine::hash(uint8_t *bytes, size_t numBytes)
void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len)
{
delete aes;
aes = nullptr;
if (key_len != 0) {
aes = new AESSmall256();
aes = std::unique_ptr<AESSmall256>(new AESSmall256());
aes->setKey(key_bytes, key_len);
}
}
@@ -231,12 +231,11 @@ void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes
// Generic implementation of AES-CTR encryption.
void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes)
{
delete ctr;
ctr = nullptr;
std::unique_ptr<CTRCommon> ctr;
if (_key.length == 16)
ctr = new CTR<AES128>();
ctr = std::unique_ptr<CTRCommon>(new CTR<AES128>());
else
ctr = new CTR<AES256>();
ctr = std::unique_ptr<CTRCommon>(new CTR<AES256>());
ctr->setKey(_key.bytes, _key.length);
static uint8_t scratch[MAX_BLOCKSIZE];
memcpy(scratch, bytes, numBytes);
+2 -2
View File
@@ -5,6 +5,7 @@
#include "configuration.h"
#include "mesh-pb-constants.h"
#include <Arduino.h>
#include <memory>
extern concurrency::Lock *cryptLock;
@@ -48,7 +49,7 @@ class CryptoEngine
virtual void aesSetKey(const uint8_t *key, size_t key_len);
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
AESSmall256 *aes = NULL;
std::unique_ptr<AESSmall256> aes = nullptr;
#endif
@@ -77,7 +78,6 @@ class CryptoEngine
/** Our per packet nonce */
uint8_t nonce[16] = {0};
CryptoKey key = {};
CTRCommon *ctr = NULL;
#if !(MESHTASTIC_EXCLUDE_PKI)
uint8_t shared_key[32] = {0};
uint8_t private_key[32] = {0};
+1
View File
@@ -263,6 +263,7 @@ template <typename T> void LR11x0Interface<T>::startReceive()
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
+7 -5
View File
@@ -814,26 +814,28 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.has_store_forward = true;
moduleConfig.has_telemetry = true;
moduleConfig.has_external_notification = true;
#if defined(PIN_BUZZER)
#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION)
moduleConfig.external_notification.enabled = true;
#endif
#if defined(PIN_BUZZER)
moduleConfig.external_notification.output_buzzer = PIN_BUZZER;
moduleConfig.external_notification.use_pwm = true;
moduleConfig.external_notification.alert_message_buzzer = true;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
#endif
#if defined(PIN_VIBRATION)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output_vibra = PIN_VIBRATION;
moduleConfig.external_notification.alert_message_vibra = true;
moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2;
#endif
#if defined(LED_NOTIFICATION)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = LED_NOTIFICATION;
moduleConfig.external_notification.active = LED_STATE_ON;
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
#endif
#if defined(PIN_VIBRATION)
moduleConfig.external_notification.nag_timeout = 2;
#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION)
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
#endif
+2 -2
View File
@@ -439,7 +439,7 @@ void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, cons
}
// Getters and setters for hop limit fields packed in hop_limit
inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r)
inline uint8_t PacketHistory::getHighestHopLimit(const PacketRecord &r)
{
return r.hop_limit & HOP_LIMIT_HIGHEST_MASK;
}
@@ -449,7 +449,7 @@ inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit)
r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK);
}
inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r)
inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r)
{
return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT;
}
+2 -2
View File
@@ -43,9 +43,9 @@ class PacketHistory
* @return true if node was indeed a relayer, false if not */
bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr);
uint8_t getHighestHopLimit(PacketRecord &r);
uint8_t getHighestHopLimit(const PacketRecord &r);
void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit);
uint8_t getOurTxHopLimit(PacketRecord &r);
uint8_t getOurTxHopLimit(const PacketRecord &r);
void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);
PacketHistory(const PacketHistory &); // non construction-copyable
+1
View File
@@ -301,6 +301,7 @@ void RF95Interface::startReceive()
// Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
}
bool RF95Interface::isChannelActive()
+55 -63
View File
@@ -227,23 +227,19 @@ static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1];
// Global LoRa radio type
LoRaRadioType radioType = NO_RADIO;
extern RadioInterface *rIf;
extern RadioLibHal *RadioLibHAL;
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
extern SPIClass SPI1;
#endif
bool initLoRa()
std::unique_ptr<RadioInterface> initLoRa()
{
if (rIf != nullptr) {
delete rIf;
rIf = nullptr;
}
std::unique_ptr<RadioInterface> rIf = nullptr;
#if ARCH_PORTDUINO
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
#else
SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
SPISettings loraSpiSettings(4000000, MSBFIRST, SPI_MODE0);
#endif
#ifdef ARCH_PORTDUINO
@@ -252,26 +248,26 @@ bool initLoRa()
RADIOLIB_PIN_TYPE busy) {
switch (portduino_config.lora_module) {
case use_rf95:
return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new RF95Interface(hal, cs, irq, rst, busy));
case use_sx1262:
return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new SX1262Interface(hal, cs, irq, rst, busy));
case use_sx1268:
return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new SX1268Interface(hal, cs, irq, rst, busy));
case use_sx1280:
return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new SX1280Interface(hal, cs, irq, rst, busy));
case use_lr1110:
return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new LR1110Interface(hal, cs, irq, rst, busy));
case use_lr1120:
return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new LR1120Interface(hal, cs, irq, rst, busy));
case use_lr1121:
return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new LR1121Interface(hal, cs, irq, rst, busy));
case use_llcc68:
return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
return std::unique_ptr<RadioInterface>(new LLCC68Interface(hal, cs, irq, rst, busy));
case use_simradio:
return (RadioInterface *)new SimRadio;
return std::unique_ptr<RadioInterface>(new SimRadio);
default:
assert(0); // shouldn't happen
return (RadioInterface *)nullptr;
return std::unique_ptr<RadioInterface>(nullptr);
}
};
@@ -284,7 +280,7 @@ bool initLoRa()
delete RadioLibHAL;
RadioLibHAL = nullptr;
}
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
RadioLibHAL = new LockingArduinoHal(SPI, loraSpiSettings);
}
rIf =
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
@@ -292,27 +288,28 @@ bool initLoRa()
if (!rIf->init()) {
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
delete rIf;
rIf = NULL;
rIf = nullptr;
exit(EXIT_FAILURE);
} else {
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
}
#elif defined(HW_SPI1_DEVICE)
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings);
RadioLibHAL = loraHal;
#else // HW_SPI1_DEVICE
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings);
RadioLibHAL = loraHal;
#endif
// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init)
#if defined(USE_STM32WLx)
if (!rIf) {
rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
rIf = std::unique_ptr<STM32WLE5JCInterface>(
new STM32WLE5JCInterface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
if (!rIf->init()) {
LOG_WARN("No STM32WL radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("STM32WL init success");
radioType = STM32WLx_RADIO;
@@ -322,11 +319,10 @@ bool initLoRa()
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
rIf = std::unique_ptr<RF95Interface>(new RF95Interface(loraHal, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1));
if (!rIf->init()) {
LOG_WARN("No RF95 radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("RF95 init success");
radioType = RF95_RADIO;
@@ -336,17 +332,17 @@ bool initLoRa()
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
auto sxIf =
std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
#ifdef SX126X_DIO3_TCXO_VOLTAGE
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
#endif
if (!sxIf->init()) {
LOG_WARN("No SX1262 radio");
delete sxIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("SX1262 init success");
rIf = sxIf;
rIf = std::move(sxIf);
radioType = SX1262_RADIO;
}
}
@@ -355,26 +351,25 @@ bool initLoRa()
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// try using the specified TCXO voltage
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
auto sxIf =
std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
if (!sxIf->init()) {
LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
delete sxIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
rIf = sxIf;
rIf = std::move(sxIf);
radioType = SX1262_RADIO;
}
}
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
rIf = std::unique_ptr<SX1262Interface>(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
if (!rIf->init()) {
LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
radioType = SX1262_RADIO;
@@ -386,25 +381,24 @@ bool initLoRa()
#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL)
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// try using the specified TCXO voltage
auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
auto sxIf =
std::unique_ptr<SX1268Interface>(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
if (!sxIf->init()) {
LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
delete sxIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
rIf = sxIf;
rIf = std::move(sxIf);
radioType = SX1268_RADIO;
}
}
#endif
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
rIf = std::unique_ptr<SX1268Interface>(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
if (!rIf->init()) {
LOG_WARN("No SX1268 radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("SX1268 init success");
radioType = SX1268_RADIO;
@@ -414,11 +408,10 @@ bool initLoRa()
#if defined(USE_LLCC68)
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
rIf = std::unique_ptr<LLCC68Interface>(new LLCC68Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY));
if (!rIf->init()) {
LOG_WARN("No LLCC68 radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("LLCC68 init success");
radioType = LLCC68_RADIO;
@@ -428,11 +421,11 @@ bool initLoRa()
#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN);
rIf = std::unique_ptr<LR1110Interface>(
new LR1110Interface(loraHal, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN));
if (!rIf->init()) {
LOG_WARN("No LR1110 radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("LR1110 init success");
radioType = LR1110_RADIO;
@@ -442,11 +435,11 @@ bool initLoRa()
#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1
if (!rIf) {
rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN);
rIf = std::unique_ptr<LR1120Interface>(
new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN));
if (!rIf->init()) {
LOG_WARN("No LR1120 radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("LR1120 init success");
radioType = LR1120_RADIO;
@@ -456,11 +449,11 @@ bool initLoRa()
#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1
if (!rIf) {
rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN);
rIf = std::unique_ptr<LR1121Interface>(
new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN));
if (!rIf->init()) {
LOG_WARN("No LR1121 radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("LR1121 init success");
radioType = LR1121_RADIO;
@@ -470,11 +463,10 @@ bool initLoRa()
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
if (!rIf) {
rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY);
rIf = std::unique_ptr<SX1280Interface>(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY));
if (!rIf->init()) {
LOG_WARN("No SX1280 radio");
delete rIf;
rIf = NULL;
rIf = nullptr;
} else {
LOG_INFO("SX1280 init success");
radioType = SX1280_RADIO;
@@ -496,7 +488,7 @@ bool initLoRa()
rebootAtMsec = millis() + 5000;
}
}
return rIf != nullptr;
return rIf;
}
void initRegion()
+2 -1
View File
@@ -6,6 +6,7 @@
#include "PointerQueue.h"
#include "airtime.h"
#include "error.h"
#include <memory>
// Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file.
typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig;
@@ -279,7 +280,7 @@ class RadioInterface
}
};
bool initLoRa();
std::unique_ptr<RadioInterface> initLoRa();
/// Debug printing for packets
void printPacket(const char *prefix, const meshtastic_MeshPacket *p);
+16 -2
View File
@@ -517,12 +517,26 @@ void RadioLibInterface::handleReceiveInterrupt()
void RadioLibInterface::startReceive()
{
// Note the updated timestamp, to avoid unneeded AGC resets
last_listen = millis();
isReceiving = true;
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);
}
void RadioLibInterface::pollMissedIrqs()
{
// RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges.
if (isReceiving) {
checkRxDoneIrqFlag();
}
}
void RadioLibInterface::checkRxDoneIrqFlag()
{
if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) {
LOG_WARN("caught missed RX_DONE");
notify(ISR_RX, true);
}
}
void RadioLibInterface::configHardwareForSend()
{
powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn);
+7
View File
@@ -112,6 +112,11 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
*/
virtual void enableInterrupt(void (*)()) = 0;
/**
* Poll as a backup to catch missed edge-triggered interrupts.
*/
void pollMissedIrqs();
/**
* Debugging counts
*/
@@ -264,4 +269,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
*/
bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override;
void checkRxDoneIrqFlag();
};
+3 -2
View File
@@ -8,6 +8,7 @@
#include "PointerQueue.h"
#include "RadioInterface.h"
#include "concurrency/OSThread.h"
#include <memory>
/**
* A mesh aware router that supports multiple interfaces.
@@ -20,7 +21,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
PointerQueue<meshtastic_MeshPacket> fromRadioQueue;
protected:
RadioInterface *iface = NULL;
std::unique_ptr<RadioInterface> iface = nullptr;
public:
/**
@@ -32,7 +33,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
/**
* Currently we only allow one interface, that may change in the future
*/
void addInterface(RadioInterface *_iface) { iface = _iface; }
void addInterface(std::unique_ptr<RadioInterface> _iface) { iface = std::move(_iface); }
/**
* do idle processing
+35
View File
@@ -6,6 +6,10 @@
#ifdef ARCH_PORTDUINO
#include "PortduinoGlue.h"
#endif
#if defined(USE_GC1109_PA) && defined(ARCH_ESP32)
#include <driver/rtc_io.h>
#include <esp_sleep.h>
#endif
#include "Throttle.h"
@@ -55,14 +59,33 @@ template <typename T> bool SX126xInterface<T>::init()
#if defined(USE_GC1109_PA)
// GC1109 FEM chip initialization
// See variant.h for full pin mapping and control logic documentation
//
// On deep sleep wake, PA_POWER and PA_EN are held HIGH by RTC latch (set in
// enableLoraInterrupt). We configure GPIO registers before releasing the hold
// so the pad transitions atomically from held-HIGH to register-HIGH with no
// power glitch. On cold boot the hold_dis is a harmless no-op.
// VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on)
pinMode(LORA_PA_POWER, OUTPUT);
digitalWrite(LORA_PA_POWER, HIGH);
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER);
// TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot, wait
// for VBAT to stabilise before driving CSD/CPS, per GC1109 requirement:
// "VBAT must be prior to CSD/CPS/CTX for the power on sequence"
// On deep sleep wake the LDO was held on via RTC latch, so no delay needed.
#if defined(ARCH_ESP32)
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) {
delayMicroseconds(1000);
}
#else
delayMicroseconds(1000);
#endif
// CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX
pinMode(LORA_PA_EN, OUTPUT);
digitalWrite(LORA_PA_EN, HIGH);
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_EN);
// CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care)
// Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH
@@ -171,6 +194,17 @@ template <typename T> bool SX126xInterface<T>::init()
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result);
}
#ifdef USE_GC1109_PA
// Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity
// on boards with the GC1109 FEM. Sets bit 0 of register 0x8B5.
// Reference: https://github.com/meshcore-dev/MeshCore/pull/1398
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) {
LOG_INFO("Applied SX1262 register 0x8B5 patch for GC1109 RX improvement");
} else {
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for GC1109");
}
#endif
#if 0
// Read/write a register we are not using (only used for FSK mode) to test SPI comms
uint8_t crcLSB = 0;
@@ -328,6 +362,7 @@ template <typename T> void SX126xInterface<T>::startReceive()
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
+1
View File
@@ -271,6 +271,7 @@ template <typename T> void SX128xInterface<T>::startReceive()
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
+2 -2
View File
@@ -27,7 +27,7 @@ int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen)
/**
* Read any rx chars from the link and call handleRecStream
*/
int32_t StreamAPI::readStream(char *buf, uint16_t bufLen)
int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen)
{
if (bufLen < 1) {
// Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time
@@ -56,7 +56,7 @@ void StreamAPI::writeStream()
}
}
int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen)
int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen)
{
uint16_t index = 0;
while (bufLen > index) { // Currently we never want to block
+2 -2
View File
@@ -57,8 +57,8 @@ class StreamAPI : public PhoneAPI
* Read any rx chars from the link and call handleToRadio
*/
int32_t readStream();
int32_t readStream(char *buf, uint16_t bufLen);
int32_t handleRecStream(char *buf, uint16_t bufLen);
int32_t readStream(const char *buf, uint16_t bufLen);
int32_t handleRecStream(const char *buf, uint16_t bufLen);
/**
* call getFromRadio() and deliver encapsulated packets to the Stream
+1 -1
View File
@@ -33,7 +33,7 @@ static int constant_time_compare(const void *a_, const void *b_, size_t len)
d |= (a[i] ^ b[i]);
}
/* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */
return (1 & ((d - 1) >> 8)) - 1;
return (1 & (((unsigned int)d - 1) >> 8)) - 1;
}
static void WPA_PUT_BE16(uint8_t *a, uint16_t val)
+1 -1
View File
@@ -17,7 +17,7 @@ class PacketAPI : public PhoneAPI, public concurrency::OSThread
virtual int32_t runOnce();
protected:
PacketAPI(PacketServer *_server);
explicit PacketAPI(PacketServer *_server);
// Check the current underlying physical queue to see if the client is fetching packets
bool checkIsConnected() override;
+1 -1
View File
@@ -623,7 +623,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
}
// Helper lambda to create JSON array and clean up memory properly
auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * {
JSONArray tempArray;
for (int i = 0; i < count; i++) {
tempArray.push_back(new JSONValue((int)logArray[i]));
File diff suppressed because it is too large Load Diff
+31
View File
@@ -2,6 +2,8 @@
#if HAS_SCREEN
#include "ProtobufModule.h"
#include "input/InputBroker.h"
#include <utility>
#include <vector>
// ============================
// Enums & Defines
@@ -72,6 +74,11 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
bool shouldDraw();
bool hasMessages();
void resetSearch();
// Treat both freetext compose and emote picker as "text compose UI" for global overlays.
bool isFreeTextUIActive() const
{
return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER;
}
void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
bool isCharInputAllowed() const;
@@ -148,6 +155,20 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
unsigned long lastUpdateMillis = 0;
String searchQuery;
String freetext;
static constexpr uint8_t maxFreeTextCompletions = 4;
String freeTextCompletion;
String freeTextCompletions[maxFreeTextCompletions];
uint8_t freeTextCompletionCount = 0;
uint8_t freeTextCompletionIndex = 0;
bool freeTextCompletionSuppressed = false;
// Cached wrapped token lines for freetext rendering (rebuilt only when content/width changes).
String freeTextLayoutCacheText;
int freeTextLayoutCacheWidth = -1;
std::vector<std::vector<std::pair<bool, String>>> freeTextLayoutCacheLines;
// When true, the next emote picker selection replaces this typed prefix span.
bool freeTextEmoteReplacePending = false;
unsigned int freeTextEmoteReplaceStart = 0;
unsigned int freeTextEmoteReplaceLength = 0;
// === Message Storage ===
char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
@@ -190,6 +211,16 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect);
bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect);
bool handleFreeTextInput(const InputEvent *event);
bool tryStartFreeTextFromInactive(const InputEvent *event);
void clearComposeState();
int32_t runFreeTextState(UIFrameEvent &e);
String getFreeTextPrefix() const;
void updateFreeTextCompletion();
bool cycleFreeTextCompletion(int8_t step);
bool acceptFreeTextCompletion(bool appendSpace);
void drawFreeTextCompletionRow(OLEDDisplay *display, int16_t x, int16_t rowY, const String &completionPrefix);
void drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, char *buffer);
void drawCannedMessageListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, char *buffer);
#if defined(USE_VIRTUAL_KEYBOARD)
Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0},
+999
View File
@@ -0,0 +1,999 @@
#include "configuration.h"
#if HAS_SCREEN
#include "CannedMessageModule.h"
#include "Channels.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "buzz.h"
#include "gps/RTC.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/NotificationRenderer.h"
#include "graphics/emotes.h"
#include "modules/FreeTextModule.h"
#include <algorithm>
#include <cstring>
extern MessageStore messageStore;
extern bool osk_found;
extern NodeNum lastDest;
extern uint8_t lastChannel;
extern bool lastDestSet;
namespace graphics
{
extern int bannerSignalBars;
}
// Tracks whether destination-picker cancel/select should return to canned list vs freetext.
static bool returnToCannedList = false;
namespace
{
// ASCII lowercase helper keeps filtering deterministic and avoids locale overhead.
char toLowerAscii(char c)
{
if (c >= 'A' && c <= 'Z') {
return static_cast<char>(c + ('a' - 'A'));
}
return c;
}
// Case-insensitive substring search without temporary String allocations.
bool containsCaseInsensitiveAscii(const char *text, const char *needle)
{
if (!text || !needle || !needle[0]) {
return true;
}
const size_t textLen = strlen(text);
const size_t needleLen = strlen(needle);
if (needleLen > textLen) {
return false;
}
for (size_t i = 0; i + needleLen <= textLen; ++i) {
bool matched = true;
for (size_t j = 0; j < needleLen; ++j) {
if (toLowerAscii(text[i + j]) != toLowerAscii(needle[j])) {
matched = false;
break;
}
}
if (matched) {
return true;
}
}
return false;
}
// Truncate text to fit width and append ellipsis when clipping is required.
void truncateWithEllipsisToWidth(OLEDDisplay *display, char *text, size_t textSize, int maxWidth)
{
if (!display || !text || textSize == 0) {
return;
}
if (maxWidth <= 0) {
text[0] = '\0';
return;
}
if (display->getStringWidth(text) <= maxWidth) {
return;
}
if (textSize < 4) {
text[0] = '\0';
return;
}
const size_t originalLen = strlen(text);
size_t low = 0;
size_t high = std::min(originalLen, textSize - 4);
char candidate[64];
candidate[0] = '\0';
// Binary search shortest-fitting clipped token with ellipsis.
while (low <= high) {
const size_t mid = low + ((high - low) / 2);
if (mid >= sizeof(candidate)) {
break;
}
memcpy(candidate, text, mid);
candidate[mid] = '\0';
strcat(candidate, "...");
if (display->getStringWidth(candidate) <= maxWidth) {
low = mid + 1;
} else {
if (mid == 0) {
break;
}
high = mid - 1;
}
}
const size_t keep = (low == 0) ? 0 : (low - 1);
const size_t clipped = std::min(keep, textSize - 4);
text[clipped] = '\0';
strcat(text, "...");
}
// Find the emote descriptor for a token label.
const graphics::Emote *findEmoteByLabel(const String &label)
{
for (int i = 0; i < graphics::numEmotes; ++i) {
if (label == graphics::emotes[i].label) {
return &graphics::emotes[i];
}
}
return nullptr;
}
} // namespace
// Reset destination search state and keep the previous selection roughly centered.
void CannedMessageModule::resetSearch()
{
int previousDestIndex = destIndex;
searchQuery = "";
updateDestinationSelectionList();
// Adjust scrollIndex so previousDestIndex is still visible
int totalEntries = activeChannelIndices.size() + filteredNodes.size();
this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL;
if (this->visibleRows < 1)
this->visibleRows = 1;
int maxScrollIndex = std::max(0, totalEntries - visibleRows);
scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex);
lastUpdateMillis = millis();
requestFocus();
}
// Rebuild searchable destination entries (channels + eligible nodes).
void CannedMessageModule::updateDestinationSelectionList()
{
static size_t lastNumMeshNodes = 0;
static String lastSearchQuery = "";
size_t numMeshNodes = nodeDB->getNumMeshNodes();
bool nodesChanged = (numMeshNodes != lastNumMeshNodes);
lastNumMeshNodes = numMeshNodes;
// Early exit if nothing changed
if (searchQuery == lastSearchQuery && !nodesChanged)
return;
lastSearchQuery = searchQuery;
needsUpdate = false;
this->filteredNodes.clear();
this->activeChannelIndices.clear();
NodeNum myNodeNum = nodeDB->getNodeNum();
const bool hasSearchFilter = searchQuery.length() > 0;
const char *searchText = searchQuery.c_str();
// Preallocate space to reduce reallocation
this->filteredNodes.reserve(numMeshNodes);
for (size_t i = 0; i < numMeshNodes; ++i) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32)
continue;
const char *nodeName = node->user.long_name;
if (!hasSearchFilter) {
this->filteredNodes.push_back({node, sinceLastSeen(node)});
} else {
// Compare in place to avoid building lower-cased temporary Strings per node.
if (containsCaseInsensitiveAscii(nodeName, searchText)) {
this->filteredNodes.push_back({node, sinceLastSeen(node)});
}
}
}
// Populate unique active channels without temporary String lists.
for (uint8_t i = 0; i < channels.getNumChannels(); ++i) {
const char *name = channels.getName(i);
if (!name || !name[0]) {
continue;
}
bool alreadyIncluded = false;
for (uint8_t existingIndex : this->activeChannelIndices) {
const char *existingName = channels.getName(existingIndex);
if (existingName && strcmp(existingName, name) == 0) {
alreadyIncluded = true;
break;
}
}
if (!alreadyIncluded) {
this->activeChannelIndices.push_back(i);
}
}
scrollIndex = 0; // Show first result at the top
destIndex = 0; // Highlight the first entry
if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
LOG_INFO("Nodes changed, forcing UI refresh.");
screen->forceDisplay();
}
}
// Handle destination picker input: type-to-filter, navigation, select, and cancel.
int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect)
{
// Override isDown and isSelect ONLY for destination selector behavior
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
if (event->inputEvent == INPUT_BROKER_USER_PRESS) {
isDown = true;
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
isSelect = true;
}
}
if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT &&
event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) {
this->searchQuery += (char)event->kbchar;
needsUpdate = true;
if ((millis() - lastFilterUpdate) > filterDebounceMs) {
runOnce(); // update filter immediately
lastFilterUpdate = millis();
}
return 1;
}
size_t numMeshNodes = filteredNodes.size();
int totalEntries = numMeshNodes + activeChannelIndices.size();
int columns = 1;
int totalRows = totalEntries;
int maxScrollIndex = std::max(0, totalRows - visibleRows);
scrollIndex = clamp(scrollIndex, 0, maxScrollIndex);
// Handle backspace
if (event->inputEvent == INPUT_BROKER_BACK) {
if (searchQuery.length() > 0) {
searchQuery.remove(searchQuery.length() - 1);
needsUpdate = true;
runOnce();
}
if (searchQuery.length() == 0) {
resetSearch();
needsUpdate = false;
}
return 1;
}
if (isUp) {
if (destIndex > 0) {
destIndex--;
} else if (totalEntries > 0) {
destIndex = totalEntries - 1;
}
if ((destIndex / columns) < scrollIndex)
scrollIndex = destIndex / columns;
else if ((destIndex / columns) >= (scrollIndex + visibleRows))
scrollIndex = (destIndex / columns) - visibleRows + 1;
screen->forceDisplay(true);
return 1;
}
if (isDown) {
if (destIndex + 1 < totalEntries) {
destIndex++;
} else if (totalEntries > 0) {
destIndex = 0;
scrollIndex = 0;
}
if ((destIndex / columns) >= (scrollIndex + visibleRows))
scrollIndex = (destIndex / columns) - visibleRows + 1;
screen->forceDisplay(true);
return 1;
}
// SELECT
if (isSelect) {
if (destIndex < static_cast<int>(activeChannelIndices.size())) {
dest = NODENUM_BROADCAST;
channel = activeChannelIndices[destIndex];
lastDest = dest;
lastChannel = channel;
lastDestSet = true;
} else {
int nodeIndex = destIndex - static_cast<int>(activeChannelIndices.size());
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(filteredNodes.size())) {
const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node;
if (selectedNode) {
dest = selectedNode->num;
channel = selectedNode->channel;
// Already saves here, but for clarity, also:
lastDest = dest;
lastChannel = channel;
lastDestSet = true;
}
}
}
runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT;
returnToCannedList = false;
screen->forceDisplay(true);
return 1;
}
// CANCEL
if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) {
runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT;
returnToCannedList = false;
searchQuery = "";
// UIFrameEvent e;
// e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
// notifyObservers(&e);
screen->forceDisplay(true);
return 1;
}
return 0;
}
// Handle canned-message list input including destination/freetext/exit actions.
bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect)
{
// Override isDown and isSelect ONLY for canned message list behavior
if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
if (event->inputEvent == INPUT_BROKER_USER_PRESS) {
isDown = true;
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
isSelect = true;
}
}
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
return false;
// Handle Cancel key: go inactive, clear UI state
if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE &&
(event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) {
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
freetext = "";
cursor = 0;
payload = 0;
currentMessageIndex = -1;
// Notify UI that we want to redraw/close this screen
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
screen->forceDisplay();
return true;
}
bool handled = false;
// Handle up/down navigation
if (isUp && messagesCount > 0) {
runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP;
handled = true;
} else if (isDown && messagesCount > 0) {
runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
handled = true;
} else if (isSelect) {
const char *current = messages[currentMessageIndex];
// [Select Destination] triggers destination selection UI
if (strcmp(current, "[Select Destination]") == 0) {
returnToCannedList = true;
runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
destIndex = 0;
scrollIndex = 0;
updateDestinationSelectionList(); // Make sure list is fresh
screen->forceDisplay();
return true;
}
// [Exit] returns to the main/inactive screen
if (strcmp(current, "[Exit]") == 0) {
// Set runState to inactive so we return to main UI
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
currentMessageIndex = -1;
// Notify UI to regenerate frame set and redraw
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
screen->forceDisplay();
return true;
}
// [Free Text] triggers the free text input (virtual keyboard)
#if defined(USE_VIRTUAL_KEYBOARD)
if (strcmp(current, "[-- Free Text --]") == 0) {
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
requestFocus();
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
notifyObservers(&e);
return true;
}
#else
if (strcmp(current, "[-- Free Text --]") == 0) {
if (osk_found && screen) {
char headerBuffer[64];
if (this->dest == NODENUM_BROADCAST) {
snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel));
} else {
snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest));
}
screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) {
if (!text.empty()) {
this->freetext = text.c_str();
this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
currentMessageIndex = -1;
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->notifyObservers(&e);
screen->forceDisplay();
setIntervalFromNow(500);
return;
} else {
// Don't delete virtual keyboard immediately - it might still be executing
// Instead, just clear the callback and reset banner to stop input processing
graphics::NotificationRenderer::textInputCallback = nullptr;
graphics::NotificationRenderer::resetBanner();
// Return to inactive state
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
this->currentMessageIndex = -1;
this->freetext = "";
this->cursor = 0;
// Force display update to show normal screen
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
this->notifyObservers(&e);
screen->forceDisplay();
// Schedule cleanup for next loop iteration to ensure safe deletion
setIntervalFromNow(50);
return;
}
});
return true;
}
}
#endif
// Normal canned message selection
if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
} else {
#if CANNED_MESSAGE_ADD_CONFIRMATION
const int savedIndex = currentMessageIndex;
graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() {
this->currentMessageIndex = savedIndex;
this->payload = this->runState;
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
this->setIntervalFromNow(0);
});
#else
payload = runState;
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
#endif
// Do not immediately set runState; wait for confirmation
handled = true;
}
}
if (handled) {
requestFocus();
if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT)
setIntervalFromNow(0);
else
runOnce();
}
return handled;
}
// Build and send a text packet, persist it locally, and switch to text-message view.
void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies)
{
lastDest = dest;
lastChannel = channel;
lastDestSet = true;
meshtastic_MeshPacket *p = allocDataPacket();
p->to = dest;
p->channel = channel;
p->want_ack = true;
p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num
this->lastSentNode = dest;
this->incoming = dest;
// Manually find the node by number to check PKI capability
meshtastic_NodeInfoLite *node = nullptr;
size_t numMeshNodes = nodeDB->getNumMeshNodes();
for (size_t i = 0; i < numMeshNodes; ++i) {
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num == dest) {
node = n;
break;
}
}
NodeNum myNodeNum = nodeDB->getNodeNum();
if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) {
p->pki_encrypted = true;
p->channel = 0; // force PKI
}
// Track this packet’s request ID for matching ACKs
this->lastRequestId = p->id;
// Copy payload
p->decoded.payload.size = strlen(message);
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) {
p->decoded.payload.bytes[p->decoded.payload.size++] = 7;
p->decoded.payload.bytes[p->decoded.payload.size] = '\0';
}
this->waitingForAck = true;
// Send to mesh (PKI-encrypted if conditions above matched)
service->sendToMesh(p, RX_SRC_LOCAL, true);
// Show banner immediately
if (screen) {
graphics::BannerOverlayOptions opts;
opts.message = "Sending...";
opts.durationMs = 2000;
screen->showOverlayBanner(opts);
}
// Save outgoing message
StoredMessage sm;
// Always use our local time, consistent with other paths
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000;
sm.isBootRelative = (nowSecs == 0);
sm.sender = nodeDB->getNodeNum(); // us
sm.channelIndex = channel;
size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1);
sm.textOffset = MessageStore::storeText(message, len);
sm.textLength = len;
// Classify broadcast vs DM
if (dest == NODENUM_BROADCAST) {
sm.dest = NODENUM_BROADCAST;
sm.type = MessageType::BROADCAST;
} else {
sm.dest = dest;
sm.type = MessageType::DM_TO_US;
// Only add as favorite if our role is NOT CLIENT_BASE
if (config.device.role != 12) {
LOG_INFO("Proactively adding %x as favorite node", dest);
nodeDB->set_favorite(true, dest);
} else {
LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest);
}
}
sm.ackStatus = AckStatus::NONE;
messageStore.addLiveMessage(std::move(sm));
// Auto-switch thread view on outgoing message
if (sm.type == MessageType::BROADCAST) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex);
} else {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest);
}
playComboTune();
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
this->payload = wantReplies ? 1 : 0;
requestFocus();
// Tell Screen to switch to TextMessage frame via UIFrameEvent
UIFrameEvent e;
e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
notifyObservers(&e);
}
// Draw the destination picker list with highlight, truncation, and scrollbar.
void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
requestFocus();
display->setColor(WHITE); // Always draw cleanly
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
// Header
int titleY = 2;
String titleText = "Select Destination";
titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]";
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(display->getWidth() / 2, titleY, titleText);
display->setTextAlignment(TEXT_ALIGN_LEFT);
// List Items
int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4);
int numActiveChannels = this->activeChannelIndices.size();
int totalEntries = numActiveChannels + this->filteredNodes.size();
int columns = 1;
this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4);
if (this->visibleRows < 1)
this->visibleRows = 1;
// Clamp scrolling
if (scrollIndex > totalEntries / columns)
scrollIndex = totalEntries / columns;
if (scrollIndex < 0)
scrollIndex = 0;
for (int row = 0; row < visibleRows; row++) {
int itemIndex = scrollIndex + row;
if (itemIndex >= totalEntries)
break;
int xOffset = 0;
int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset;
char entryText[64] = "";
// Draw Channels First
if (itemIndex < numActiveChannels) {
uint8_t channelIndex = this->activeChannelIndices[itemIndex];
snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex));
}
// Then Draw Nodes
else {
int nodeIndex = itemIndex - numActiveChannels;
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
if (node && node->user.long_name) {
strncpy(entryText, node->user.long_name, sizeof(entryText) - 1);
entryText[sizeof(entryText) - 1] = '\0';
}
int availWidth = display->getWidth() -
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
((node && node->is_favorite) ? 10 : 0);
if (availWidth < 0)
availWidth = 0;
truncateWithEllipsisToWidth(display, entryText, sizeof(entryText), availWidth);
// Prepend "* " if this is a favorite
if (node && node->is_favorite) {
size_t len = strlen(entryText);
if (len + 2 < sizeof(entryText)) {
memmove(entryText + 2, entryText, len + 1);
entryText[0] = '*';
entryText[1] = ' ';
}
}
if (node) {
if (display->getWidth() <= 64) {
snprintf(entryText, sizeof(entryText), "%s", node->user.short_name);
}
}
}
}
if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0)
strcpy(entryText, "?");
// Highlight background (if selected)
if (itemIndex == destIndex) {
int scrollPadding = 8; // Reserve space for scrollbar
display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5);
display->setColor(BLACK);
}
// Draw entry text
display->drawString(xOffset + 2, yOffset, entryText);
display->setColor(WHITE);
// Draw key icon (after highlight)
/*
if (itemIndex >= numActiveChannels) {
int nodeIndex = itemIndex - numActiveChannels;
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
if (node && hasKeyForNode(node)) {
int iconX = display->getWidth() - key_symbol_width - 15;
int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2;
if (itemIndex == destIndex) {
display->setColor(INVERSE);
} else {
display->setColor(WHITE);
}
display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol);
}
}
}
*/
}
// Scrollbar
if (totalEntries > visibleRows) {
int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4);
int totalScrollable = totalEntries;
int scrollTrackX = display->getWidth() - 6;
display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight);
int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable;
int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable;
display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight);
}
}
// Draw the canned messages list with selection highlight and emote-aware row layout.
void CannedMessageModule::drawCannedMessageListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y,
char *buffer)
{
(void)state;
if (this->messagesCount <= 0) {
return;
}
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
// Precompute per-row heights based on emotes (centered if present).
const int baseRowSpacing = FONT_HEIGHT_SMALL - 4;
int topMsg;
int _visibleRows;
// Draw header (To: ...).
drawHeader(display, x, y, buffer);
// Shift message list upward by 3 pixels to reduce spacing between header and first message.
const int listYOffset = y + FONT_HEIGHT_SMALL - 3;
_visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing;
// Figure out which messages are visible.
topMsg =
(messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1) ? currentMessageIndex - _visibleRows + 2 : 0;
int countRows = std::min(messagesCount, _visibleRows);
// Draw all message rows with multi-emote support.
int yCursor = listYOffset;
for (int vis = 0; vis < countRows; vis++) {
int msgIdx = topMsg + vis;
int lineY = yCursor;
const char *msg = getMessageByIndex(msgIdx);
bool _highlight = (msgIdx == currentMessageIndex);
// Multi-emote tokenization.
std::vector<std::pair<bool, String>> tokens = freeTextModule::tokenizeMessageWithEmotes(msg);
int maxEmoteHeight = 0;
for (const auto &token : tokens) {
if (!token.first) {
continue;
}
const graphics::Emote *emote = findEmoteByLabel(token.second);
if (emote && emote->height > maxEmoteHeight) {
maxEmoteHeight = emote->height;
}
}
const int rowHeight = std::max(baseRowSpacing, maxEmoteHeight + 2);
// Vertically center based on rowHeight.
int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2;
#ifdef USE_EINK
int nextX = x + (_highlight ? 12 : 0);
if (_highlight)
display->drawString(x + 0, lineY + textYOffset, ">");
#else
int scrollPadding = 8;
if (_highlight) {
display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight);
display->setColor(BLACK);
}
int nextX = x + (_highlight ? 2 : 0);
#endif
// Draw all tokens left to right.
for (const auto &token : tokens) {
if (token.first) {
// Emote rendering centralized in helper.
freeTextModule::renderEmote(display, nextX, lineY, rowHeight, token.second);
} else {
// Text.
display->drawString(nextX, lineY + textYOffset, token.second);
nextX += display->getStringWidth(token.second);
}
}
#ifndef USE_EINK
if (_highlight)
display->setColor(WHITE);
#endif
yCursor += rowHeight;
}
// Scrollbar.
if (messagesCount > _visibleRows) {
int scrollHeight = display->getHeight() - listYOffset;
int scrollTrackX = display->getWidth() - 6;
display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight);
int barHeight = (scrollHeight * _visibleRows) / messagesCount;
int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount;
display->fillRect(scrollTrackX, scrollPos, 4, barHeight);
}
}
// Map modem preset to a rough SNR quality threshold used for banner grading.
static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset)
{
switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
return -6.0f;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
return -5.5f;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
return -4.5f;
default:
return -6.0f;
}
}
// Convert RSSI/SNR into user-facing quality label and 1-5 signal bars.
static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars)
{
// 5-bar logic: strength inside Good/Fair/Bad category
if (snr > snrLimit && rssi > -10) {
bars = 5; // very strong good
return "Good";
} else if (snr > snrLimit && rssi > -20) {
bars = 4; // normal good
return "Good";
} else if (snr > 0 && rssi > -50) {
bars = 3; // weaker good (on edge of fair)
return "Good";
} else if (snr > -10 && rssi > -100) {
bars = 2; // fair
return "Fair";
} else {
bars = 1; // bad
return "Bad";
}
}
// Process routing ACK/NACK for the most recent outbound message and show status banner.
ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{
// Only process routing ACK/NACK packets that are responses to our own outbound
if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() &&
mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet
{
if (mp.decoded.request_id != 0) {
// Decode the routing response
meshtastic_Routing decoded = meshtastic_Routing_init_default;
pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded);
// Determine ACK/NACK status
bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE);
bool isFromDest = (mp.from == this->lastSentNode);
bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST);
// Identify the responding node
if (wasBroadcast && mp.from != nodeDB->getNodeNum()) {
this->incoming = mp.from; // relayed by another node
} else {
this->incoming = this->lastSentNode; // direct reply
}
// Final ACK/NACK logic
if (wasBroadcast) {
// Any ACK counts for broadcast
this->ack = isAck;
waitingForAck = false;
} else if (isFromDest) {
// Only ACK from destination counts as final
this->ack = isAck;
waitingForAck = false;
} else if (isAck) {
// Relay ACK → mark as RELAYED, still no final ACK
this->ack = false;
waitingForAck = false;
} else {
// Explicit failure
this->ack = false;
waitingForAck = false;
}
// Update last sent StoredMessage with ACK/NACK/RELAYED result
if (!messageStore.getMessages().empty()) {
StoredMessage &last = const_cast<StoredMessage &>(messageStore.getMessages().back());
if (last.sender == nodeDB->getNodeNum()) { // only update our own messages
if (wasBroadcast && isAck) {
last.ackStatus = AckStatus::ACKED;
} else if (isFromDest && isAck) {
last.ackStatus = AckStatus::ACKED;
} else if (!isFromDest && isAck) {
last.ackStatus = AckStatus::RELAYED;
} else {
last.ackStatus = AckStatus::NACKED;
}
}
}
// Capture radio metrics
this->lastRxRssi = mp.rx_rssi;
this->lastRxSnr = mp.rx_snr;
// Show overlay banner
if (screen) {
auto *display = screen->getDisplayDevice();
graphics::BannerOverlayOptions opts;
static char buf[128];
const char *channelName = channels.getName(this->channel);
const char *src = getNodeName(this->incoming);
char nodeName[48];
strncpy(nodeName, src, sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0';
int availWidth =
display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30);
if (availWidth < 0)
availWidth = 0;
truncateWithEllipsisToWidth(display, nodeName, sizeof(nodeName), availWidth);
// Calculate signal quality and bars based on preset, SNR, and RSSI
float snrLimit = getSnrLimit(config.lora.modem_preset);
int bars = 0;
const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars);
if (this->ack) {
if (this->lastSentNode == NODENUM_BROADCAST) {
snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s",
(channelName && channelName[0]) ? channelName : "unknown", qualityLabel);
} else {
snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s",
(nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel);
}
} else if (isAck && !isFromDest) {
// Relay ACK banner
snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s",
(nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel);
} else {
if (this->lastSentNode == NODENUM_BROADCAST) {
snprintf(buf, sizeof(buf), "Message failed to\n#%s",
(channelName && channelName[0]) ? channelName : "unknown");
} else {
snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown");
}
}
opts.message = buf;
opts.durationMs = 3000;
graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw
screen->showOverlayBanner(opts); // this triggers drawNotificationBox()
}
}
}
return ProcessMessage::CONTINUE;
}
#endif
+1 -1
View File
@@ -367,7 +367,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
}
}
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex());
// If we receive a broadcast message, apply channel mute setting
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#if HAS_SCREEN
#include "configuration.h"
#include <utility>
#include <vector>
class OLEDDisplay;
namespace freeTextModule
{
std::vector<std::pair<bool, String>> tokenizeMessageWithEmotes(const char *msg);
void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label);
} // namespace freeTextModule
#endif
+5 -1
View File
@@ -56,11 +56,15 @@
#endif
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "main.h"
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/EnvironmentTelemetry.h"
#include "modules/Telemetry/HealthTelemetry.h"
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
#endif
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "main.h"
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/Sensor/TelemetrySensor.h"
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
#include "modules/Telemetry/PowerTelemetry.h"
#endif
+9 -14
View File
@@ -23,7 +23,6 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
{
switch (arg->getStatusType()) {
case STATUS_TYPE_POWER: {
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg;
if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) {
power_state = charging;
if (powerStatus->getBatteryChargePercent() >= 100) {
@@ -39,7 +38,6 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
break;
}
case STATUS_TYPE_BLUETOOTH: {
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg;
switch (bluetoothStatus->getConnectionState()) {
case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: {
ble_state = unpaired;
@@ -199,33 +197,30 @@ void StatusLEDModule::setPowerLED(bool LEDon)
PMU->setChargingLedMode(LEDon ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
}
#endif
if (LEDon)
LEDon = LED_STATE_ON;
else
LEDon = LED_STATE_OFF;
uint8_t ledState = LEDon ? LED_STATE_ON : LED_STATE_OFF;
#ifdef PCA_LED_POWER
io.digitalWrite(PCA_LED_POWER, LEDon);
io.digitalWrite(PCA_LED_POWER, ledState);
#endif
#ifdef PCA_LED_ENABLE
io.digitalWrite(PCA_LED_ENABLE, LEDon);
io.digitalWrite(PCA_LED_ENABLE, ledState);
#endif
#ifdef LED_POWER
digitalWrite(LED_POWER, LEDon);
digitalWrite(LED_POWER, ledState);
#endif
#ifdef LED_PAIRING
digitalWrite(LED_PAIRING, LEDon);
digitalWrite(LED_PAIRING, ledState);
#endif
#ifdef Battery_LED_1
digitalWrite(Battery_LED_1, LEDon);
digitalWrite(Battery_LED_1, ledState);
#endif
#ifdef Battery_LED_2
digitalWrite(Battery_LED_2, LEDon);
digitalWrite(Battery_LED_2, ledState);
#endif
#ifdef Battery_LED_3
digitalWrite(Battery_LED_3, LEDon);
digitalWrite(Battery_LED_3, ledState);
#endif
#ifdef Battery_LED_4
digitalWrite(Battery_LED_4, LEDon);
digitalWrite(Battery_LED_4, ledState);
#endif
}
+1 -3
View File
@@ -131,9 +131,7 @@ void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to)
uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time)
{
uint32_t count = 0;
if (lastRequest.find(dest) == lastRequest.end()) {
lastRequest.emplace(dest, 0);
}
lastRequest.emplace(dest, 0);
for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) {
if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) {
// Client is only interested in packets not from itself and only in broadcast packets or packets towards it.
@@ -29,6 +29,9 @@
#if __has_include(<SensirionI2cSfa3x.h>)
#include "Sensor/SFA30Sensor.h"
#endif
#if __has_include(<SensirionI2cScd30.h>)
#include "Sensor/SCD30Sensor.h"
#endif
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -57,6 +60,9 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
#if __has_include(<SensirionI2cSfa3x.h>)
addSensor<SFA30Sensor>(i2cScanner, ScanI2C::DeviceType::SFA30);
#endif
#if __has_include(<SensirionI2cScd30.h>)
addSensor<SCD30Sensor>(i2cScanner, ScanI2C::DeviceType::SCD30);
#endif
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -8,7 +8,7 @@
static std::forward_list<TelemetrySensor *> sensors;
template <typename T> void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type)
template <typename T> void addSensor(const ScanI2C *i2cScanner, ScanI2C::DeviceType type)
{
ScanI2C::FoundDevice dev = i2cScanner->find(type);
if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) {
+25 -1
View File
@@ -8,6 +8,10 @@
#include "SPILock.h"
#include "TelemetrySensor.h"
#if __has_include(<Adafruit_BME680.h>)
#include <cmath>
#endif
BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {}
#if __has_include(<bsec2.h>)
@@ -96,8 +100,28 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
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;
float gasRaw = bme680->readGas();
measurement->variant.environment_metrics.gas_resistance = gasRaw / 1000.0;
// IAQ approximation: humidity-compensated logarithmic mapping of gas resistance
// Gas sensor resistance drops with humidity; compensate to a 40% RH reference baseline
// Map compensated gas resistance (Ohms) to IAQ 0-500 using log-linear interpolation
// Clean air reference ~400 kOhm, polluted reference ~5 kOhm
if (gasRaw > 0.0f && !isfinite(gasRaw)) {
static constexpr float LOG_UPPER = 12.899219f; // log(400k)
static constexpr float LOG_RANGE_INV = 1.0f / (12.899219f - 8.517193f); // 1 / (log(400k) - log(5k))
measurement->variant.environment_metrics.has_iaq = true;
measurement->variant.environment_metrics.iaq = (uint16_t)(fminf(
fmaxf(((LOG_UPPER -
logf(fmaxf(gasRaw * expf(0.035f * (measurement->variant.environment_metrics.relative_humidity - 40.0f)),
1.0f))) *
LOG_RANGE_INV) *
500.0f,
0.0f),
500.0f));
}
#endif
return true;
}
+1 -1
View File
@@ -28,7 +28,7 @@ class BME680Sensor : public TelemetrySensor
#else
using BME680Ptr = std::unique_ptr<Adafruit_BME680>;
static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique<Adafruit_BME680>(bus); }
static BME680Ptr makeBME680(TwoWire *bus) { return BME680Ptr(new Adafruit_BME680(bus)); }
BME680Ptr bme680;
#endif
@@ -67,7 +67,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
#endif /* CAN_RECLOCK_I2C */
#endif /* PMSA003I_I2C_CLOCK_SPEED */
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
_bus->requestFrom(_address, (uint8_t)PMSA003I_FRAME_LENGTH);
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
return false;
@@ -86,7 +86,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
return false;
}
auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; };
auto read16 = [](const uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; };
computedChecksum = 0;
@@ -0,0 +1,511 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd30.h>)
#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "SCD30Sensor.h"
#define SCD30_NO_ERROR 0
SCD30Sensor::SCD30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD30, "SCD30") {}
bool SCD30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
LOG_INFO("Init sensor: %s", sensorName);
_bus = bus;
_address = dev->address.address;
#ifdef SCD30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD30_I2C_CLOCK_SPEED */
scd30.begin(*_bus, _address);
if (!startMeasurement()) {
LOG_ERROR("%s: Failed to start periodic measurement", sensorName);
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
if (!getASC(ascActive)) {
LOG_WARN("%s: Could not determine ASC state", sensorName);
}
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
if (state == SCD30_MEASUREMENT) {
status = 1;
} else {
status = 0;
}
initI2CSensor();
return true;
}
bool SCD30Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
float co2, temperature, humidity;
#ifdef SCD30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return false;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD30_I2C_CLOCK_SPEED */
if (scd30.readMeasurementData(co2, temperature, humidity) != SCD30_NO_ERROR) {
LOG_ERROR("SCD30: Failed to read measurement data.");
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return false;
}
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
if (co2 == 0) {
LOG_ERROR("SCD30: Invalid CO₂ reading.");
return false;
}
measurement->variant.air_quality_metrics.has_co2 = true;
measurement->variant.air_quality_metrics.has_co2_temperature = true;
measurement->variant.air_quality_metrics.has_co2_humidity = true;
measurement->variant.air_quality_metrics.co2 = (uint32_t)co2;
measurement->variant.air_quality_metrics.co2_temperature = temperature;
measurement->variant.air_quality_metrics.co2_humidity = humidity;
LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum=%.2f", sensorName, (uint32_t)co2, temperature, humidity);
return true;
}
bool SCD30Sensor::setMeasurementInterval(uint16_t measInterval)
{
uint16_t error;
LOG_INFO("%s: setting measurement interval at %us", sensorName, measInterval);
error = scd30.setMeasurementInterval(measInterval);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to set measurement interval. Error code: %u", sensorName, error);
return false;
}
// Restart measuring so we don't need to wait the current interval to finish
// (useful when you come from very long intervals)
scd30.stopPeriodicMeasurement();
scd30.startPeriodicMeasurement(0);
getMeasurementInterval(measurementInterval);
return true;
}
bool SCD30Sensor::getMeasurementInterval(uint16_t &measInterval)
{
uint16_t error;
LOG_INFO("%s: getting measurement interval", sensorName);
error = scd30.getMeasurementInterval(measInterval);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to get measurement interval. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: measurement interval is %us", sensorName, measInterval);
return true;
}
/**
* @brief Start measurement mode
* @note This function should not change the clock
*/
bool SCD30Sensor::startMeasurement()
{
uint16_t error;
if (state == SCD30_MEASUREMENT) {
LOG_DEBUG("%s: Already in measurement mode", sensorName);
return true;
}
error = scd30.startPeriodicMeasurement(0);
if (error == SCD30_NO_ERROR) {
LOG_INFO("%s: Started measurement mode", sensorName);
state = SCD30_MEASUREMENT;
return true;
} else {
LOG_ERROR("%s: Couldn't start measurement mode", sensorName);
return false;
}
}
/**
* @brief Stop measurement mode
* @note This function should not change the clock
*/
bool SCD30Sensor::stopMeasurement()
{
uint16_t error;
error = scd30.stopPeriodicMeasurement();
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to stop measurement", sensorName);
return false;
}
state = SCD30_IDLE;
return true;
}
bool SCD30Sensor::performFRC(uint16_t targetCO2)
{
uint16_t error;
LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName);
LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2);
error = scd30.forceRecalibration((uint16_t)targetCO2);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName);
return false;
}
LOG_INFO("%s: FRC Correction successful.", sensorName);
return true;
}
bool SCD30Sensor::setASC(bool ascEnabled)
{
uint16_t error;
LOG_INFO("%s: %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling");
error = scd30.activateAutoCalibration((uint16_t)ascEnabled);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to send command.", sensorName);
return false;
}
if (!getASC(ascActive)) {
LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
return false;
}
return true;
}
bool SCD30Sensor::getASC(uint16_t &_ascActive)
{
uint16_t error;
// LOG_INFO("%s: Getting ASC", sensorName);
error = scd30.getAutoCalibrationStatus(_ascActive);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to send command.", sensorName);
return false;
}
LOG_INFO("%s: ASC is %s", sensorName, _ascActive ? "enabled" : "disabled");
return true;
}
/**
* @brief Set the temperature reference. Unit ℃.
*
* The on-board RH/T sensor is influenced by thermal self-heating of SCD30
* and other electrical components. Design-in alters the thermal properties
* of SCD30 such that temperature and humidity offsets may occur when
* operating the sensor in end-customer devices. Compensation of those
* effects is achievable by writing the temperature offset found in
* continuous operation of the device into the sensor. Temperature offset
* value is saved in non-volatile memory. The last set value will be used
* for temperature offset compensation after repowering.
*
* @param[in] tempReference
* @note this function is certainly confusing and it's not recommended
*/
bool SCD30Sensor::setTemperature(float tempReference)
{
uint16_t error;
uint16_t updatedTempOffset;
float tempOffset;
uint16_t _tempOffset;
float co2;
float temperature;
float humidity;
if (tempReference == 100) {
// Requesting the value of 100 will restore the temperature offset
LOG_INFO("%s: Setting reference temperature at 0degC", sensorName);
_tempOffset = 0;
} else {
LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference);
error = scd30.readMeasurementData(co2, temperature, humidity);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature);
tempOffset = (temperature - tempReference);
if (tempOffset < 0) {
LOG_ERROR("%s temperature offset is only positive", sensorName);
return false;
}
tempOffset *= 100;
_tempOffset = static_cast<uint16_t>(tempOffset);
}
LOG_INFO("%s: Setting temperature offset: %u (*100)", sensorName, _tempOffset);
error = scd30.setTemperatureOffset(_tempOffset);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error);
return false;
}
scd30.getTemperatureOffset(updatedTempOffset);
LOG_INFO("%s: Updated sensor temperature offset: %u (*100)", sensorName, updatedTempOffset);
return true;
}
bool SCD30Sensor::setAltitude(uint16_t altitude)
{
uint16_t error;
LOG_INFO("%s: setting altitude at %um", sensorName, altitude);
error = scd30.setAltitudeCompensation(altitude);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
return false;
}
uint16_t newAltitude;
getAltitude(newAltitude);
return true;
}
bool SCD30Sensor::getAltitude(uint16_t &altitude)
{
uint16_t error;
// LOG_INFO("%s: Getting altitude", sensorName);
error = scd30.getAltitudeCompensation(altitude);
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude);
return true;
}
bool SCD30Sensor::softReset()
{
uint16_t error;
LOG_INFO("%s: Requesting soft reset", sensorName);
error = scd30.softReset();
if (error != SCD30_NO_ERROR) {
LOG_ERROR("%s: Unable to do soft reset. Error code: %u", sensorName, error);
return false;
}
LOG_INFO("%s: soft reset successful", sensorName);
return true;
}
/**
* @brief Check if sensor is in measurement mode
*/
bool SCD30Sensor::isActive()
{
return state == SCD30_MEASUREMENT;
}
/**
* @brief Start measurement mode
* @note Not used in admin comands, getMetrics or init, can change clock.
*/
uint32_t SCD30Sensor::wakeUp()
{
#ifdef SCD30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return 0;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD30_I2C_CLOCK_SPEED */
startMeasurement();
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return 0;
}
/**
* @brief Stop measurement mode
* @note Not used in admin comands, getMetrics or init, can change clock.
*/
void SCD30Sensor::sleep()
{
#ifdef SCD30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD30_I2C_CLOCK_SPEED */
stopMeasurement();
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
}
bool SCD30Sensor::canSleep()
{
return false;
}
int32_t SCD30Sensor::wakeUpTimeMs()
{
return 0;
}
int32_t SCD30Sensor::pendingForReadyMs()
{
return 0;
}
AdminMessageHandleResult SCD30Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result;
#ifdef SCD30_I2C_CLOCK_SPEED
#ifdef CAN_RECLOCK_I2C
uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false);
#elif !HAS_SCREEN
reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true);
#else
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
return AdminMessageHandleResult::NOT_HANDLED;
#endif /* CAN_RECLOCK_I2C */
#endif /* SCD30_I2C_CLOCK_SPEED */
switch (request->which_payload_variant) {
case meshtastic_AdminMessage_sensor_config_tag:
// Check for ASC-FRC request first
if (!request->sensor_config.has_scd30_config) {
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
if (request->sensor_config.scd30_config.has_soft_reset) {
LOG_DEBUG("%s: Requested soft reset", sensorName);
this->softReset();
} else {
if (request->sensor_config.scd30_config.has_set_asc) {
this->setASC(request->sensor_config.scd30_config.set_asc);
if (request->sensor_config.scd30_config.set_asc == false) {
LOG_DEBUG("%s: Request for FRC", sensorName);
if (request->sensor_config.scd30_config.has_set_target_co2_conc) {
this->performFRC(request->sensor_config.scd30_config.set_target_co2_conc);
} else {
// FRC requested but no target CO2 provided
LOG_ERROR("%s: target CO2 not provided", sensorName);
result = AdminMessageHandleResult::NOT_HANDLED;
break;
}
}
}
// Check for temperature offset
// NOTE: this requires to have a sensor working on stable environment
// And to make it between readings
if (request->sensor_config.scd30_config.has_set_temperature) {
this->setTemperature(request->sensor_config.scd30_config.set_temperature);
}
// Check for altitude
if (request->sensor_config.scd30_config.has_set_altitude) {
this->setAltitude(request->sensor_config.scd30_config.set_altitude);
}
// Check for set measuremen interval
if (request->sensor_config.scd30_config.has_set_measurement_interval) {
this->setMeasurementInterval(request->sensor_config.scd30_config.set_measurement_interval);
}
}
result = AdminMessageHandleResult::HANDLED;
break;
default:
result = AdminMessageHandleResult::NOT_HANDLED;
}
#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
#endif
return result;
}
#endif
@@ -0,0 +1,53 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include(<SensirionI2cScd30.h>)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <SensirionI2cScd30.h>
#define SCD30_I2C_CLOCK_SPEED 100000
class SCD30Sensor : public TelemetrySensor
{
private:
SensirionI2cScd30 scd30;
TwoWire *_bus{};
uint8_t _address{};
bool performFRC(uint16_t targetCO2);
bool setASC(bool ascEnabled);
bool getASC(uint16_t &ascEnabled);
bool setTemperature(float tempReference);
bool getAltitude(uint16_t &altitude);
bool setAltitude(uint16_t altitude);
bool softReset(); //
bool setMeasurementInterval(uint16_t measInterval);
bool getMeasurementInterval(uint16_t &measInterval);
bool startMeasurement();
bool stopMeasurement();
// Parameters
uint16_t ascActive = 1;
uint16_t measurementInterval = 2;
public:
SCD30Sensor();
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
enum SCD30State { SCD30_OFF, SCD30_IDLE, SCD30_MEASUREMENT };
SCD30State state = SCD30_OFF;
virtual bool isActive() override;
virtual void sleep() override; // Stops measurement (measurement -> idle)
virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement)
virtual bool canSleep() override;
virtual int32_t wakeUpTimeMs() override;
virtual int32_t pendingForReadyMs() override;
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response) override;
};
#endif

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