diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml
index 9d563a39a..36154affa 100644
--- a/.trunk/trunk.yaml
+++ b/.trunk/trunk.yaml
@@ -8,18 +8,18 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- - checkov@3.2.497
- - renovate@42.84.2
- - prettier@3.8.0
- - trufflehog@3.92.5
+ - checkov@3.2.500
+ - renovate@43.4.0
+ - prettier@3.8.1
+ - trufflehog@3.93.0
- yamllint@1.38.0
- bandit@1.9.3
- - trivy@0.68.2
+ - trivy@0.69.1
- taplo@0.10.0
- - ruff@0.14.13
+ - ruff@0.15.0
- isort@7.0.0
- markdownlint@0.47.0
- - oxipng@10.0.0
+ - oxipng@10.1.0
- svgo@4.0.0
- actionlint@1.7.10
- flake8@7.3.0
diff --git a/bin/device-install.bat b/bin/device-install.bat
index c200a3201..69469d581 100755
--- a/bin/device-install.bat
+++ b/bin/device-install.bat
@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
SET "PROGNAME=!FILENAME:.factory.bin=!"
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
-IF "__!MCU!__" == "__esp32s3__" (
- @REM We are working with ESP32-S3
- SET "OTA_FILENAME=bleota-s3.bin"
-) ELSE IF "__!MCU!__" == "__esp32c3__" (
- @REM We are working with ESP32-C3
- SET "OTA_FILENAME=bleota-c3.bin"
-) ELSE (
- @REM Everything else
- SET "OTA_FILENAME=bleota.bin"
-)
+@REM Determine OTA filename based on MCU type (unified OTA format)
+SET "OTA_FILENAME=mt-!MCU!-ota.bin"
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix.
diff --git a/bin/device-install.sh b/bin/device-install.sh
index 49427524e..52a848c38 100755
--- a/bin/device-install.sh
+++ b/bin/device-install.sh
@@ -131,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
exit 1
fi
- # Determine OTA filename based on MCU type
- if [ "$MCU" == "esp32s3" ]; then
- OTAFILE=bleota-s3.bin
- elif [ "$MCU" == "esp32c3" ]; then
- OTAFILE=bleota-c3.bin
- else
- OTAFILE=bleota.bin
- fi
+ # Determine OTA filename based on MCU type (unified OTA format)
+ OTAFILE="mt-${MCU}-ota.bin"
# Set SPIFFS filename with "littlefs-" prefix.
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
diff --git a/platformio.ini b/platformio.ini
index 291915d3c..b7123b865 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -67,7 +67,7 @@ monitor_speed = 115200
monitor_filters = direct
lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
- https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip
+ https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
@@ -121,7 +121,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
- https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip
+ https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -145,7 +145,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
adafruit/Adafruit INA219@1.2.3
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
- adafruit/Adafruit MPU6050@2.2.6
+ adafruit/Adafruit MPU6050@2.2.8
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
adafruit/Adafruit LIS3DH@1.3.0
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
@@ -214,6 +214,7 @@ lib_deps =
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0
+
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
[environmental_extra_no_bsec]
lib_deps =
@@ -240,4 +241,4 @@ lib_deps =
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.2
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
- sensirion/Sensirion I2C SCD4x@1.1.0
\ No newline at end of file
+ sensirion/Sensirion I2C SCD4x@1.1.0
diff --git a/protobufs b/protobufs
index bc63a57f9..e80cb2e41 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit bc63a57f9e5dba8a7c90ee0bd4a9840862d61f6d
+Subproject commit e80cb2e410d8054f5da67ba6767613c4336b6c88
diff --git a/src/configuration.h b/src/configuration.h
index 7b7c603ba..5d8d764c7 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -242,6 +242,7 @@ along with this program. If not, see .
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
+#define SEN5X_ADDR 0x69
// -----------------------------------------------------------------------------
// ACCELEROMETER
diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp
index 4795d2abc..bf01a0365 100644
--- a/src/detect/ScanI2C.cpp
+++ b/src/detect/ScanI2C.cpp
@@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
ScanI2C::FoundDevice ScanI2C::firstAQI() const
{
- ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
+ ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X};
return firstOfOrNONE(2, types);
}
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index 0edd5e7a7..528159661 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -89,7 +89,8 @@ class ScanI2C
DA217,
CHSC6X,
CST226SE,
- CW2015
+ CW2015,
+ SEN5X
} DeviceType;
// typedef uint8_t DeviceAddress;
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index 445e9342c..e5e69dca7 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -8,6 +8,7 @@
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#include "meshUtils.h" // vformat
+
#endif
bool in_array(uint8_t *array, int size, uint8_t lookfor)
@@ -114,6 +115,45 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
return value;
}
+/// 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
+// detection will not work
+String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
+{
+ uint8_t cmd[] = {0xD0, 0x14};
+ uint8_t response[48] = {0};
+
+ i2cBus->beginTransmission(address);
+ i2cBus->write(cmd, 2);
+ if (i2cBus->endTransmission() != 0)
+ return "";
+
+ delay(20);
+ if (i2cBus->requestFrom(address, (uint8_t)48) != 48)
+ return "";
+
+ for (int i = 0; i < 48 && i2cBus->available(); ++i) {
+ response[i] = i2cBus->read();
+ }
+
+ char productName[33] = {0};
+ int j = 0;
+ for (int i = 0; i < 48 && j < 32; i += 3) {
+ if (response[i] >= 32 && response[i] <= 126)
+ productName[j++] = response[i];
+ else
+ break;
+
+ if (response[i + 1] >= 32 && response[i + 1] <= 126)
+ productName[j++] = response[i + 1];
+ else
+ break;
+ }
+
+ return String(productName);
+}
+
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
case ADDR: \
logFoundDevice(__VA_ARGS__); \
@@ -578,8 +618,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
- case ICM20948_ADDR: // same as BMX160_ADDR
+ case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
+ // ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
@@ -590,14 +631,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
- } else if (addr.address == BMX160_ADDR) {
- type = BMX160;
- logFoundDevice("BMX160", (uint8_t)addr.address);
- break;
} else {
- type = MPU6050;
- logFoundDevice("MPU6050", (uint8_t)addr.address);
- break;
+ String prod = "";
+ prod = readSEN5xProductName(i2cBus, addr.address);
+ if (prod.startsWith("SEN55")) {
+ type = SEN5X;
+ logFoundDevice("Sensirion SEN55", addr.address);
+ break;
+ } else if (prod.startsWith("SEN54")) {
+ type = SEN5X;
+ logFoundDevice("Sensirion SEN54", addr.address);
+ break;
+ } else if (prod.startsWith("SEN50")) {
+ type = SEN5X;
+ logFoundDevice("Sensirion SEN50", addr.address);
+ break;
+ }
+ if (addr.address == BMX160_ADDR) {
+ type = BMX160;
+ logFoundDevice("BMX160", (uint8_t)addr.address);
+ break;
+ } else {
+ type = MPU6050;
+ logFoundDevice("MPU6050", (uint8_t)addr.address);
+ break;
+ }
}
break;
diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp
new file mode 100644
index 000000000..60cd3c808
--- /dev/null
+++ b/src/detect/reClockI2C.cpp
@@ -0,0 +1,31 @@
+#include "reClockI2C.h"
+#include "ScanI2CTwoWire.h"
+
+uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force)
+{
+
+ uint32_t currentClock = 0;
+
+ /* See https://github.com/arduino/Arduino/issues/11457
+ Currently, only ESP32 can getClock()
+ While all cores can setClock()
+ https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
+ https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
+ https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
+ For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
+ we need to reclock I2C and set it back to the previous desired speed.
+ Only for cases where we can know OR predefine the speed, we can do this.
+ */
+
+// TODO add getClock function or return a predefined clock speed per variant?
+#ifdef CAN_RECLOCK_I2C
+ currentClock = i2cBus->getClock();
+#endif
+
+ if ((currentClock != desiredClock) || force) {
+ LOG_DEBUG("Changing I2C clock to %u", desiredClock);
+ i2cBus->setClock(desiredClock);
+ }
+
+ return currentClock;
+}
diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h
index 689e88d6f..9c53efc4f 100644
--- a/src/detect/reClockI2C.h
+++ b/src/detect/reClockI2C.h
@@ -1,41 +1,11 @@
-#ifdef CAN_RECLOCK_I2C
+
+#ifndef RECLOCK_I2C_
+#define RECLOCK_I2C_
+
#include "ScanI2CTwoWire.h"
+#include
+#include
-uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
-{
+uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
- uint32_t currentClock;
-
- /* See https://github.com/arduino/Arduino/issues/11457
- Currently, only ESP32 can getClock()
- While all cores can setClock()
- https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
- https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
- https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
- For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
- we need to reclock I2C and set it back to the previous desired speed.
- Only for cases where we can know OR predefine the speed, we can do this.
- */
-
-#ifdef ARCH_ESP32
- currentClock = i2cBus->getClock();
-#elif defined(ARCH_NRF52)
- // TODO add getClock function or return a predefined clock speed per variant?
- return 0;
-#elif defined(ARCH_RP2040)
- // TODO add getClock function or return a predefined clock speed per variant
- return 0;
-#elif defined(ARCH_STM32WL)
- // TODO add getClock function or return a predefined clock speed per variant
- return 0;
-#else
- return 0;
-#endif
-
- if (currentClock != desiredClock) {
- LOG_DEBUG("Changing I2C clock to %u", desiredClock);
- i2cBus->setClock(desiredClock);
- }
- return currentClock;
-}
#endif
diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index 3bca6f6ec..5e31de950 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -72,11 +72,13 @@ RTCSetResult readFromRTC()
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
+ SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
+ SensorPCF85063 rtc;
+
#endif
uint32_t now = millis();
- SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
@@ -240,10 +242,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
+ SensorPCF8563 rtc;
#elif defined(PCF85063_RTC)
if (rtc_found.address == PCF85063_RTC) {
+ SensorPCF85063 rtc;
+
#endif
- SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp
index 6d29e9f7f..195da09f9 100644
--- a/src/graphics/draw/MenuHandler.cpp
+++ b/src/graphics/draw/MenuHandler.cpp
@@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
} // namespace
-menuHandler::screenMenus menuHandler::menuQueue = menu_none;
+menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
uint32_t menuHandler::pickedNodeNum = 0;
bool test_enabled = false;
uint8_t test_count = 0;
@@ -66,7 +66,7 @@ uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
- enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 };
+ enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -74,14 +74,14 @@ void menuHandler::loraMenu()
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
- } else if (selected == device_role_picker) {
- menuHandler::menuQueue = menuHandler::device_role_picker;
- } else if (selected == radio_preset_picker) {
- menuHandler::menuQueue = menuHandler::radio_preset_picker;
- } else if (selected == frequency_slot) {
- menuHandler::menuQueue = menuHandler::frequency_slot;
- } else if (selected == lora_picker) {
- menuHandler::menuQueue = menuHandler::lora_picker;
+ } else if (selected == DeviceRolePicker) {
+ menuHandler::menuQueue = menuHandler::DeviceRolePicker;
+ } else if (selected == RadioPresetPicker) {
+ menuHandler::menuQueue = menuHandler::RadioPresetPicker;
+ } else if (selected == FrequencySlot) {
+ menuHandler::menuQueue = menuHandler::FrequencySlot;
+ } else if (selected == LoraPicker) {
+ menuHandler::menuQueue = menuHandler::LoraPicker;
}
};
screen->showOverlayBanner(bannerOptions);
@@ -102,7 +102,7 @@ void menuHandler::OnboardMessage()
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
- menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
+ menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
screen->runNow();
};
screen->showOverlayBanner(bannerOptions);
@@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
screen->showOverlayBanner(bannerOptions);
}
-void menuHandler::DeviceRolePicker()
+void menuHandler::deviceRolePicker()
{
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
enum optionsNumbers {
@@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker()
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
- menuHandler::menuQueue = menuHandler::lora_Menu;
+ menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
} else if (selected == devicerole_client) {
@@ -300,7 +300,7 @@ void menuHandler::FrequencySlotPicker()
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
- menuHandler::menuQueue = menuHandler::lora_Menu;
+ menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
@@ -313,7 +313,7 @@ void menuHandler::FrequencySlotPicker()
screen->showOverlayBanner(bannerOptions);
}
-void menuHandler::RadioPresetPicker()
+void menuHandler::radioPresetPicker()
{
static const RadioPresetOption presetOptions[] = {
{"Back", OptionsAction::Back},
@@ -333,7 +333,7 @@ void menuHandler::RadioPresetPicker()
auto bannerOptions =
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuHandler::menuQueue = menuHandler::lora_Menu;
+ menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
@@ -352,7 +352,7 @@ void menuHandler::RadioPresetPicker()
screen->showOverlayBanner(bannerOptions);
}
-void menuHandler::TwelveHourPicker()
+void menuHandler::twelveHourPicker()
{
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
@@ -362,7 +362,7 @@ void menuHandler::TwelveHourPicker()
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
- menuHandler::menuQueue = menuHandler::clock_menu;
+ menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
} else if (selected == twelve) {
config.display.use_12h_clock = true;
@@ -390,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::functionshowOverlayBanner(confirmBanner);
}
-void menuHandler::ClockFacePicker()
+void menuHandler::clockFacePicker()
{
static const ClockFaceOption clockFaceOptions[] = {
{"Back", OptionsAction::Back},
@@ -404,7 +404,7 @@ void menuHandler::ClockFacePicker()
auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels,
[](const ClockFaceOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuHandler::menuQueue = menuHandler::clock_menu;
+ menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
return;
}
@@ -456,7 +456,7 @@ void menuHandler::TZPicker()
auto bannerOptions = createStaticBannerOptions(
"Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuHandler::menuQueue = menuHandler::clock_menu;
+ menuHandler::menuQueue = menuHandler::ClockMenu;
screen->runNow();
return;
}
@@ -503,13 +503,13 @@ void menuHandler::clockMenu()
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Clock) {
- menuHandler::menuQueue = menuHandler::clock_face_picker;
+ menuHandler::menuQueue = menuHandler::ClockFacePicker;
screen->runNow();
} else if (selected == Time) {
- menuHandler::menuQueue = menuHandler::twelve_hour_picker;
+ menuHandler::menuQueue = menuHandler::TwelveHourPicker;
screen->runNow();
} else if (selected == Timezone) {
- menuHandler::menuQueue = menuHandler::TZ_picker;
+ menuHandler::menuQueue = menuHandler::TzPicker;
screen->runNow();
}
};
@@ -572,12 +572,12 @@ void menuHandler::messageResponseMenu()
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
if (selected == ViewMode) {
- menuHandler::menuQueue = menuHandler::message_viewmode_menu;
+ menuHandler::menuQueue = menuHandler::MessageViewModeMenu;
screen->runNow();
// Reply submenu
} else if (selected == ReplyMenu) {
- menuHandler::menuQueue = menuHandler::reply_menu;
+ menuHandler::menuQueue = menuHandler::ReplyMenu;
screen->runNow();
} else if (selected == MuteChannel) {
@@ -589,7 +589,7 @@ void menuHandler::messageResponseMenu()
}
} else if (selected == DeleteMenu) {
- menuHandler::menuQueue = menuHandler::delete_messages_menu;
+ menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
screen->runNow();
#ifdef HAS_I2S
@@ -649,7 +649,7 @@ void menuHandler::replyMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
- menuHandler::menuQueue = menuHandler::message_response_menu;
+ menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
return;
}
@@ -737,7 +737,7 @@ void menuHandler::deleteMessagesMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
- menuHandler::menuQueue = menuHandler::message_response_menu;
+ menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
return;
}
@@ -901,7 +901,7 @@ void menuHandler::messageViewModeMenu()
bannerOptions.bannerCallback = [=](int selected) -> void {
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
if (selected == -1) {
- menuHandler::menuQueue = menuHandler::message_response_menu;
+ menuHandler::menuQueue = menuHandler::MessageResponseMenu;
screen->runNow();
} else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
@@ -1083,23 +1083,23 @@ void menuHandler::systemBaseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Notifications) {
- menuHandler::menuQueue = menuHandler::buzzermodemenupicker;
+ menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker;
screen->runNow();
} else if (selected == ScreenOptions) {
- menuHandler::menuQueue = menuHandler::screen_options_menu;
+ menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
} else if (selected == PowerMenu) {
- menuHandler::menuQueue = menuHandler::power_menu;
+ menuHandler::menuQueue = menuHandler::PowerMenu;
screen->runNow();
} else if (selected == Test) {
- menuHandler::menuQueue = menuHandler::test_menu;
+ menuHandler::menuQueue = menuHandler::TestMenu;
screen->runNow();
} else if (selected == Bluetooth) {
- menuQueue = bluetooth_toggle_menu;
+ menuQueue = BluetoothToggleMenu;
screen->runNow();
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
} else if (selected == WiFiToggle) {
- menuQueue = wifi_toggle_menu;
+ menuQueue = WifiToggleMenu;
screen->runNow();
#endif
} else if (selected == Back && !test_enabled) {
@@ -1177,7 +1177,7 @@ void menuHandler::favoriteBaseMenu()
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
screen->handleUIFrameEvent(&evt);
} else if (selected == Remove) {
- menuHandler::menuQueue = menuHandler::remove_favorite;
+ menuHandler::menuQueue = menuHandler::RemoveFavorite;
screen->runNow();
} else if (selected == TraceRoute) {
if (traceRouteModule) {
@@ -1238,15 +1238,15 @@ void menuHandler::positionBaseMenu()
auto action = static_cast(option.value);
switch (action) {
case PositionAction::GpsToggle:
- menuQueue = gps_toggle_menu;
+ menuQueue = GpsToggleMenu;
screen->runNow();
break;
case PositionAction::GpsFormat:
- menuQueue = gps_format_menu;
+ menuQueue = GpsFormatMenu;
screen->runNow();
break;
case PositionAction::CompassMenu:
- menuQueue = compass_point_north_menu;
+ menuQueue = CompassPointNorthMenu;
screen->runNow();
break;
case PositionAction::CompassCalibrate:
@@ -1255,15 +1255,15 @@ void menuHandler::positionBaseMenu()
}
break;
case PositionAction::GPSSmartPosition:
- menuQueue = gps_smart_position_menu;
+ menuQueue = GpsSmartPositionMenu;
screen->runNow();
break;
case PositionAction::GPSUpdateInterval:
- menuQueue = gps_update_interval_menu;
+ menuQueue = GpsUpdateIntervalMenu;
screen->runNow();
break;
case PositionAction::GPSPositionBroadcast:
- menuQueue = gps_position_broadcast_menu;
+ menuQueue = GpsPositionBroadcastMenu;
screen->runNow();
break;
}
@@ -1303,13 +1303,13 @@ void menuHandler::nodeListMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NodePicker) {
- menuQueue = NodePicker_menu;
+ menuQueue = NodePickerMenu;
screen->runNow();
} else if (selected == Reset) {
- menuQueue = reset_node_db_menu;
+ menuQueue = ResetNodeDbMenu;
screen->runNow();
} else if (selected == NodeNameLength) {
- menuHandler::menuQueue = menuHandler::node_name_length_menu;
+ menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
screen->runNow();
}
};
@@ -1330,12 +1330,12 @@ void menuHandler::NodePicker()
menuHandler::pickedNodeNum = nodenum;
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
- menuQueue = Manage_Node_menu;
+ menuQueue = ManageNodeMenu;
screen->runNow();
});
}
-void menuHandler::ManageNodeMenu()
+void menuHandler::manageNodeMenu()
{
// If we don't have a node selected yet, go fast exit
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
@@ -1391,7 +1391,7 @@ void menuHandler::ManageNodeMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
- menuQueue = node_base_menu;
+ menuQueue = NodeBaseMenu;
screen->runNow();
return;
}
@@ -1483,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu()
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
[](const NodeNameOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuQueue = node_base_menu;
+ menuQueue = NodeBaseMenu;
screen->runNow();
return;
}
@@ -1498,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu()
config.display.use_long_node_name = option.value;
saveUIConfig();
+ service->reloadConfig(SEGMENT_CONFIG);
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
});
@@ -1528,7 +1529,7 @@ void menuHandler::resetNodeDBMenu()
nodeDB->resetNodes(1);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == 0) {
- menuQueue = node_base_menu;
+ menuQueue = NodeBaseMenu;
screen->runNow();
}
};
@@ -1550,7 +1551,7 @@ void menuHandler::compassNorthMenu()
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
[](const CompassOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuQueue = position_base_menu;
+ menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1595,7 +1596,7 @@ void menuHandler::GPSToggleMenu()
auto bannerOptions =
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuQueue = position_base_menu;
+ menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1660,7 +1661,7 @@ void menuHandler::GPSFormatMenu()
auto onSelection = [](const GPSFormatOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuQueue = position_base_menu;
+ menuQueue = PositionBaseMenu;
screen->runNow();
return;
}
@@ -1715,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu()
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
- menuQueue = position_base_menu;
+ menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_smart_enabled = true;
@@ -1744,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu()
bannerOptions.optionsCount = 16;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
- menuQueue = position_base_menu;
+ menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.gps_update_interval = 8;
@@ -1832,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu()
bannerOptions.optionsCount = 17;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
- menuQueue = position_base_menu;
+ menuQueue = PositionBaseMenu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_secs = 60;
@@ -1915,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu()
#endif
-void menuHandler::BluetoothToggleMenu()
+void menuHandler::bluetoothToggleMenu()
{
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
BannerOverlayOptions bannerOptions;
@@ -2043,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
auto bannerOptions = createStaticBannerOptions(
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
- menuQueue = system_base_menu;
+ menuQueue = SystemBaseMenu;
screen->runNow();
return;
}
@@ -2138,7 +2139,7 @@ void menuHandler::rebootMenu()
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else {
- menuQueue = power_menu;
+ menuQueue = PowerMenu;
screen->runNow();
}
};
@@ -2160,7 +2161,7 @@ void menuHandler::shutdownMenu()
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else {
- menuQueue = power_menu;
+ menuQueue = PowerMenu;
screen->runNow();
}
};
@@ -2221,14 +2222,14 @@ void menuHandler::testMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NumberPicker) {
- menuQueue = number_test;
+ menuQueue = NumberTest;
screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else {
- menuQueue = system_base_menu;
+ menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2252,7 +2253,7 @@ void menuHandler::wifiBaseMenu()
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Wifi_toggle) {
- menuQueue = wifi_toggle_menu;
+ menuQueue = WifiToggleMenu;
screen->runNow();
}
};
@@ -2301,9 +2302,9 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false;
#endif
- enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
- static const char *optionsArray[5] = {"Back"};
- static int optionsEnumArray[5] = {Back};
+ enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
+ static const char *optionsArray[6] = {"Back"};
+ static int optionsEnumArray[6] = {Back};
int options = 1;
// Only show brightness for B&W displays
@@ -2325,6 +2326,9 @@ void menuHandler::screenOptionsMenu()
optionsArray[options] = "Display Units";
optionsEnumArray[options++] = DisplayUnits;
+ optionsArray[options] = "Message Bubbles";
+ optionsEnumArray[options++] = MessageBubbles;
+
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Display Options";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -2332,10 +2336,10 @@ void menuHandler::screenOptionsMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Brightness) {
- menuHandler::menuQueue = menuHandler::brightness_picker;
+ menuHandler::menuQueue = menuHandler::BrightnessPicker;
screen->runNow();
} else if (selected == ScreenColor) {
- menuHandler::menuQueue = menuHandler::tftcolormenupicker;
+ menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
@@ -2343,8 +2347,11 @@ void menuHandler::screenOptionsMenu()
} else if (selected == DisplayUnits) {
menuHandler::menuQueue = menuHandler::DisplayUnits;
screen->runNow();
+ } else if (selected == MessageBubbles) {
+ menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
+ screen->runNow();
} else {
- menuQueue = system_base_menu;
+ menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2380,16 +2387,16 @@ void menuHandler::powerMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Reboot) {
- menuHandler::menuQueue = menuHandler::reboot_menu;
+ menuHandler::menuQueue = menuHandler::RebootMenu;
screen->runNow();
} else if (selected == Shutdown) {
- menuHandler::menuQueue = menuHandler::shutdown_menu;
+ menuHandler::menuQueue = menuHandler::ShutdownMenu;
screen->runNow();
} else if (selected == MUI) {
- menuHandler::menuQueue = menuHandler::mui_picker;
+ menuHandler::menuQueue = menuHandler::MuiPicker;
screen->runNow();
} else {
- menuQueue = system_base_menu;
+ menuQueue = SystemBaseMenu;
screen->runNow();
}
};
@@ -2427,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt()
}
}
-void menuHandler::FrameToggles_menu()
+void menuHandler::frameTogglesMenu()
{
enum optionsNumbers {
Finish,
@@ -2571,7 +2578,7 @@ void menuHandler::FrameToggles_menu()
screen->showOverlayBanner(bannerOptions);
}
-void menuHandler::DisplayUnits_menu()
+void menuHandler::displayUnitsMenu()
{
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
@@ -2592,7 +2599,34 @@ void menuHandler::DisplayUnits_menu()
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
service->reloadConfig(SEGMENT_CONFIG);
} else {
- menuHandler::menuQueue = menuHandler::screen_options_menu;
+ menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
+ screen->runNow();
+ }
+ };
+ screen->showOverlayBanner(bannerOptions);
+}
+
+void menuHandler::messageBubblesMenu()
+{
+ enum optionsNumbers { Back, ShowBubbles, HideBubbles };
+
+ static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"};
+ BannerOverlayOptions bannerOptions;
+ bannerOptions.message = "Message Bubbles";
+ bannerOptions.optionsArrayPtr = optionsArray;
+ bannerOptions.optionsCount = 3;
+ bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2;
+ bannerOptions.bannerCallback = [](int selected) -> void {
+ if (selected == ShowBubbles) {
+ config.display.enable_message_bubbles = true;
+ service->reloadConfig(SEGMENT_CONFIG);
+ LOG_INFO("Message bubbles enabled");
+ } else if (selected == HideBubbles) {
+ config.display.enable_message_bubbles = false;
+ service->reloadConfig(SEGMENT_CONFIG);
+ LOG_INFO("Message bubbles disabled");
+ } else {
+ menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
}
};
@@ -2601,153 +2635,156 @@ void menuHandler::DisplayUnits_menu()
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
- if (menuQueue != menu_none)
+ if (menuQueue != MenuNone)
test_count = 0;
switch (menuQueue) {
- case menu_none:
+ case MenuNone:
break;
- case lora_Menu:
+ case LoraMenu:
loraMenu();
break;
- case lora_picker:
+ case LoraPicker:
LoraRegionPicker();
break;
- case device_role_picker:
- DeviceRolePicker();
+ case DeviceRolePicker:
+ deviceRolePicker();
break;
- case radio_preset_picker:
- RadioPresetPicker();
+ case RadioPresetPicker:
+ radioPresetPicker();
break;
- case frequency_slot:
+ case FrequencySlot:
FrequencySlotPicker();
break;
- case no_timeout_lora_picker:
+ case NoTimeoutLoraPicker:
LoraRegionPicker(0);
break;
- case TZ_picker:
+ case TzPicker:
TZPicker();
break;
- case twelve_hour_picker:
- TwelveHourPicker();
+ case TwelveHourPicker:
+ twelveHourPicker();
break;
- case clock_face_picker:
- ClockFacePicker();
+ case ClockFacePicker:
+ clockFacePicker();
break;
- case clock_menu:
+ case ClockMenu:
clockMenu();
break;
- case system_base_menu:
+ case SystemBaseMenu:
systemBaseMenu();
break;
- case position_base_menu:
+ case PositionBaseMenu:
positionBaseMenu();
break;
- case node_base_menu:
+ case NodeBaseMenu:
nodeListMenu();
break;
#if !MESHTASTIC_EXCLUDE_GPS
- case gps_toggle_menu:
+ case GpsToggleMenu:
GPSToggleMenu();
break;
- case gps_format_menu:
+ case GpsFormatMenu:
GPSFormatMenu();
break;
- case gps_smart_position_menu:
+ case GpsSmartPositionMenu:
GPSSmartPositionMenu();
break;
- case gps_update_interval_menu:
+ case GpsUpdateIntervalMenu:
GPSUpdateIntervalMenu();
break;
- case gps_position_broadcast_menu:
+ case GpsPositionBroadcastMenu:
GPSPositionBroadcastMenu();
break;
#endif
- case compass_point_north_menu:
+ case CompassPointNorthMenu:
compassNorthMenu();
break;
- case reset_node_db_menu:
+ case ResetNodeDbMenu:
resetNodeDBMenu();
break;
- case buzzermodemenupicker:
+ case BuzzerModeMenuPicker:
BuzzerModeMenu();
break;
- case mui_picker:
+ case MuiPicker:
switchToMUIMenu();
break;
- case tftcolormenupicker:
+ case TftColorMenuPicker:
TFTColorPickerMenu(display);
break;
- case brightness_picker:
+ case BrightnessPicker:
BrightnessPickerMenu();
break;
- case node_name_length_menu:
+ case NodeNameLengthMenu:
nodeNameLengthMenu();
break;
- case reboot_menu:
+ case RebootMenu:
rebootMenu();
break;
- case shutdown_menu:
+ case ShutdownMenu:
shutdownMenu();
break;
- case NodePicker_menu:
+ case NodePickerMenu:
NodePicker();
break;
- case Manage_Node_menu:
- ManageNodeMenu();
+ case ManageNodeMenu:
+ manageNodeMenu();
break;
- case remove_favorite:
+ case RemoveFavorite:
removeFavoriteMenu();
break;
- case trace_route_menu:
+ case TraceRouteMenu:
traceRouteMenu();
break;
- case test_menu:
+ case TestMenu:
testMenu();
break;
- case number_test:
+ case NumberTest:
numberTest();
break;
- case wifi_toggle_menu:
+ case WifiToggleMenu:
wifiToggleMenu();
break;
- case key_verification_init:
+ case KeyVerificationInit:
keyVerificationInitMenu();
break;
- case key_verification_final_prompt:
+ case KeyVerificationFinalPrompt:
keyVerificationFinalPrompt();
break;
- case bluetooth_toggle_menu:
- BluetoothToggleMenu();
+ case BluetoothToggleMenu:
+ bluetoothToggleMenu();
break;
- case screen_options_menu:
+ case ScreenOptionsMenu:
screenOptionsMenu();
break;
- case power_menu:
+ case PowerMenu:
powerMenu();
break;
case FrameToggles:
- FrameToggles_menu();
+ frameTogglesMenu();
break;
case DisplayUnits:
- DisplayUnits_menu();
+ displayUnitsMenu();
break;
- case throttle_message:
+ case ThrottleMessage:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
- case message_response_menu:
+ case MessageResponseMenu:
messageResponseMenu();
break;
- case reply_menu:
+ case ReplyMenu:
replyMenu();
break;
- case delete_messages_menu:
+ case DeleteMessagesMenu:
deleteMessagesMenu();
break;
- case message_viewmode_menu:
+ case MessageViewModeMenu:
messageViewModeMenu();
break;
+ case MessageBubblesMenu:
+ messageBubblesMenu();
+ break;
}
- menuQueue = menu_none;
+ menuQueue = MenuNone;
}
void menuHandler::saveUIConfig()
diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h
index 1b964678b..4a0360412 100644
--- a/src/graphics/draw/MenuHandler.h
+++ b/src/graphics/draw/MenuHandler.h
@@ -8,53 +8,54 @@ class menuHandler
{
public:
enum screenMenus {
- menu_none,
- lora_Menu,
- lora_picker,
- device_role_picker,
- radio_preset_picker,
- frequency_slot,
- no_timeout_lora_picker,
- TZ_picker,
- twelve_hour_picker,
- clock_face_picker,
- clock_menu,
- position_base_menu,
- node_base_menu,
- gps_toggle_menu,
- gps_format_menu,
- gps_smart_position_menu,
- gps_update_interval_menu,
- gps_position_broadcast_menu,
- compass_point_north_menu,
- reset_node_db_menu,
- buzzermodemenupicker,
- mui_picker,
- tftcolormenupicker,
- brightness_picker,
- reboot_menu,
- shutdown_menu,
- NodePicker_menu,
- Manage_Node_menu,
- remove_favorite,
- test_menu,
- number_test,
- wifi_toggle_menu,
- bluetooth_toggle_menu,
- screen_options_menu,
- power_menu,
- system_base_menu,
- key_verification_init,
- key_verification_final_prompt,
- trace_route_menu,
- throttle_message,
- message_response_menu,
- message_viewmode_menu,
- reply_menu,
- delete_messages_menu,
- node_name_length_menu,
+ MenuNone,
+ LoraMenu,
+ LoraPicker,
+ DeviceRolePicker,
+ RadioPresetPicker,
+ FrequencySlot,
+ NoTimeoutLoraPicker,
+ TzPicker,
+ TwelveHourPicker,
+ ClockFacePicker,
+ ClockMenu,
+ PositionBaseMenu,
+ NodeBaseMenu,
+ GpsToggleMenu,
+ GpsFormatMenu,
+ GpsSmartPositionMenu,
+ GpsUpdateIntervalMenu,
+ GpsPositionBroadcastMenu,
+ CompassPointNorthMenu,
+ ResetNodeDbMenu,
+ BuzzerModeMenuPicker,
+ MuiPicker,
+ TftColorMenuPicker,
+ BrightnessPicker,
+ RebootMenu,
+ ShutdownMenu,
+ NodePickerMenu,
+ ManageNodeMenu,
+ RemoveFavorite,
+ TestMenu,
+ NumberTest,
+ WifiToggleMenu,
+ BluetoothToggleMenu,
+ ScreenOptionsMenu,
+ PowerMenu,
+ SystemBaseMenu,
+ KeyVerificationInit,
+ KeyVerificationFinalPrompt,
+ TraceRouteMenu,
+ ThrottleMessage,
+ MessageResponseMenu,
+ MessageViewModeMenu,
+ ReplyMenu,
+ DeleteMessagesMenu,
+ NodeNameLengthMenu,
FrameToggles,
- DisplayUnits
+ DisplayUnits,
+ MessageBubblesMenu
};
static screenMenus menuQueue;
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
@@ -62,15 +63,15 @@ class menuHandler
static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000);
static void loraMenu();
- static void DeviceRolePicker();
- static void RadioPresetPicker();
+ static void deviceRolePicker();
+ static void radioPresetPicker();
static void FrequencySlotPicker();
static void handleMenuSwitch(OLEDDisplay *display);
static void showConfirmationBanner(const char *message, std::function onConfirm);
static void clockMenu();
static void TZPicker();
- static void TwelveHourPicker();
- static void ClockFacePicker();
+ static void twelveHourPicker();
+ static void clockFacePicker();
static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
@@ -95,7 +96,7 @@ class menuHandler
static void rebootMenu();
static void shutdownMenu();
static void NodePicker();
- static void ManageNodeMenu();
+ static void manageNodeMenu();
static void addFavoriteMenu();
static void removeFavoriteMenu();
static void traceRouteMenu();
@@ -106,15 +107,16 @@ class menuHandler
static void screenOptionsMenu();
static void powerMenu();
static void nodeNameLengthMenu();
- static void FrameToggles_menu();
- static void DisplayUnits_menu();
+ static void frameTogglesMenu();
+ static void displayUnitsMenu();
+ static void messageBubblesMenu();
static void textMessageMenu();
private:
static void saveUIConfig();
static void keyVerificationInitMenu();
static void keyVerificationFinalPrompt();
- static void BluetoothToggleMenu();
+ static void bluetoothToggleMenu();
};
/* Generic Menu Options designations */
diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp
index 01fdbb966..79d8b1ccd 100644
--- a/src/graphics/draw/MessageRenderer.cpp
+++ b/src/graphics/draw/MessageRenderer.cpp
@@ -527,8 +527,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
constexpr int BUBBLE_MIN_W = 24;
constexpr int BUBBLE_TEXT_INDENT = 2;
+ // Check if bubbles are enabled
+ const bool showBubbles = config.display.enable_message_bubbles;
+ const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
+
// Derived widths
- const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
+ const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
// Title string depending on mode
@@ -796,114 +800,105 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
}
}
- // Draw bubbles
- for (size_t bi = 0; bi < blocks.size(); ++bi) {
- const auto &b = blocks[bi];
- if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
- continue;
+ // Draw bubbles (only if enabled)
+ if (showBubbles) {
+ for (size_t bi = 0; bi < blocks.size(); ++bi) {
+ const auto &b = blocks[bi];
+ if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
+ continue;
- int visualTop = lineTop[b.start];
+ int visualTop = lineTop[b.start];
- int topY;
- if (isHeader[b.start]) {
- // Header start
- constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
- topY = visualTop - BUBBLE_PAD_TOP_HEADER;
- } else {
- // Body start
- bool thisLineHasEmote = false;
- for (int e = 0; e < numEmotes; ++e) {
- if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
- thisLineHasEmote = true;
- break;
+ int topY;
+ if (isHeader[b.start]) {
+ // Header start
+ constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
+ topY = visualTop - BUBBLE_PAD_TOP_HEADER;
+ } else {
+ // Body start
+ bool thisLineHasEmote = false;
+ for (int e = 0; e < numEmotes; ++e) {
+ if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
+ thisLineHasEmote = true;
+ break;
+ }
}
+ if (thisLineHasEmote) {
+ constexpr int EMOTE_PADDING_ABOVE = 4;
+ visualTop -= EMOTE_PADDING_ABOVE;
+ }
+ topY = visualTop - BUBBLE_PAD_Y;
}
- if (thisLineHasEmote) {
- constexpr int EMOTE_PADDING_ABOVE = 4;
- visualTop -= EMOTE_PADDING_ABOVE;
+ int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
+ int bottomY = visualBottom + BUBBLE_PAD_Y;
+
+ if (bi + 1 < blocks.size()) {
+ int nextHeaderIndex = (int)blocks[bi + 1].start;
+ int nextTop = lineTop[nextHeaderIndex];
+ int maxBottom = nextTop - 1 - bubbleGapY;
+ if (bottomY > maxBottom)
+ bottomY = maxBottom;
}
- topY = visualTop - BUBBLE_PAD_Y;
- }
- int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
- int bottomY = visualBottom + BUBBLE_PAD_Y;
- if (bi + 1 < blocks.size()) {
- int nextHeaderIndex = (int)blocks[bi + 1].start;
- int nextTop = lineTop[nextHeaderIndex];
- int maxBottom = nextTop - 1 - bubbleGapY;
- if (bottomY > maxBottom)
- bottomY = maxBottom;
- }
+ if (bottomY <= topY + 2)
+ continue;
- if (bottomY <= topY + 2)
- continue;
+ if (bottomY < contentTop || topY > contentBottom - 1)
+ continue;
- if (bottomY < contentTop || topY > contentBottom - 1)
- continue;
+ int maxLineW = 0;
- int maxLineW = 0;
-
- for (size_t i = b.start; i <= b.end; ++i) {
- int w = 0;
- if (isHeader[i]) {
- w = display->getStringWidth(cachedLines[i].c_str());
- if (b.mine)
- w += 12; // room for ACK/NACK/relay mark
- } else {
- w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
+ for (size_t i = b.start; i <= b.end; ++i) {
+ int w = 0;
+ if (isHeader[i]) {
+ w = display->getStringWidth(cachedLines[i].c_str());
+ if (b.mine)
+ w += 12; // room for ACK/NACK/relay mark
+ } else {
+ w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
+ }
+ if (w > maxLineW)
+ maxLineW = w;
}
- if (w > maxLineW)
- maxLineW = w;
- }
-
- int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2));
- int bubbleH = (bottomY - topY) + 1;
- int bubbleX = 0;
- if (b.mine) {
- bubbleX = rightEdge - bubbleW;
- } else {
- bubbleX = x;
- }
- if (bubbleX < x)
- bubbleX = x;
- if (bubbleX + bubbleW > rightEdge)
- bubbleW = std::max(1, rightEdge - bubbleX);
-
- if (bubbleW > 1 && bubbleH > 1) {
- int x1 = bubbleX + bubbleW - 1;
- int y1 = topY + bubbleH - 1;
+ int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
+ int bubbleH = (bottomY - topY) + 1;
+ int bubbleX = 0;
if (b.mine) {
- // Send Message (Right side)
- display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
- // Top Right Corner
- display->drawRect(x1 - 1, topY, 2, 1);
- display->drawRect(x1, topY, 1, 2);
- // Bottom Right Corner
- display->drawRect(x1 - 1, bottomY - 2, 2, 1);
- display->drawRect(x1, bottomY - 3, 1, 2);
- // Knock the corners off to make a bubble
- display->setColor(BLACK);
- display->drawRect(x1 - bubbleW + 2, topY - 1, 1, 1);
- display->drawRect(x1 - bubbleW + 2, bottomY - 1, 1, 1);
- display->setColor(WHITE);
+ bubbleX = rightEdge - bubbleW;
} else {
- // Received Message (Left Side)
- display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
- // Top Left Corner
- display->drawRect(bubbleX + 1, topY + 1, 2, 1);
- display->drawRect(bubbleX + 1, topY + 1, 1, 2);
- // Bottom Left Corner
- display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
- display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
- // Knock the corners off to make a bubble
- display->setColor(BLACK);
- display->drawRect(bubbleX + bubbleW, topY, 1, 1);
- display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
- display->setColor(WHITE);
+ bubbleX = x;
+ }
+ if (bubbleX < x)
+ bubbleX = x;
+ if (bubbleX + bubbleW > rightEdge)
+ bubbleW = std::max(1, rightEdge - bubbleX);
+
+ // Draw rounded rectangle bubble
+ if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
+ const int r = BUBBLE_RADIUS;
+ const int bx = bubbleX;
+ const int by = topY;
+ const int bw = bubbleW;
+ const int bh = bubbleH;
+
+ // Draw the 4 corner arcs using drawCircleQuads
+ display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
+ display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
+ display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
+ display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
+
+ // Draw the 4 edges between corners
+ display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
+ display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
+ display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
+ display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
+ } else if (bubbleW > 1 && bubbleH > 1) {
+ // Fallback to simple rectangle for very small bubbles
+ display->drawRect(bubbleX, topY, bubbleW, bubbleH);
}
}
- }
+ } // end if (showBubbles)
// Render visible lines
int lineY = yOffset;
@@ -916,11 +911,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int headerX;
if (isMine[i]) {
// push header left to avoid overlap with scrollbar
- headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT;
+ headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0);
if (headerX < LEFT_MARGIN)
headerX = LEFT_MARGIN;
} else {
- headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT;
+ headerX = x + textIndent;
}
display->drawString(headerX, lineY, cachedLines[i].c_str());
@@ -960,14 +955,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
if (isMine[i]) {
// Calculate actual rendered width including emotes
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
- int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT;
+ int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0);
if (rightX < LEFT_MARGIN)
rightX = LEFT_MARGIN;
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else {
- drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes,
- numEmotes);
+ drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
}
}
}
diff --git a/src/main.cpp b/src/main.cpp
index 20efb955c..44885813e 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -680,7 +680,6 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
-
#endif
#ifdef HAS_SDCARD
@@ -889,6 +888,13 @@ void setup()
service = new MeshService();
service->init();
+ // Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it
+#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
+#ifndef HAS_PHYSICAL_KEYBOARD
+ osk_found = true;
+#endif
+#endif
+
// Now that the mesh service is created, create any modules
setupModules();
@@ -973,12 +979,6 @@ void setup()
#endif
#endif
-#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
-#ifndef HAS_PHYSICAL_KEYBOARD
- osk_found = true;
-#endif
-#endif
-
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
// Start web server thread.
webServerThread = new WebServerThread();
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index f23d6490b..563c18d6c 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -574,6 +574,10 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
#endif
+#if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200)
+ config.display.enable_message_bubbles = true;
+#endif
+
#ifdef USERPREFS_CONFIG_DEVICE_ROLE
// Restrict ROUTER*, LOST AND FOUND roles for security reasons
if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER,
diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h
index d93f6fafa..c669de6a8 100644
--- a/src/mesh/generated/meshtastic/config.pb.h
+++ b/src/mesh/generated/meshtastic/config.pb.h
@@ -505,6 +505,8 @@ typedef struct _meshtastic_Config_DisplayConfig {
/* If false (default), the device will use short names for various display screens.
If true, node names will show in long format */
bool use_long_node_name;
+ /* If true, the device will display message bubbles on screen. */
+ bool enable_message_bubbles;
} meshtastic_Config_DisplayConfig;
/* Lora Config */
@@ -732,7 +734,7 @@ extern "C" {
#define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
-#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0}
+#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
@@ -743,7 +745,7 @@ extern "C" {
#define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
-#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0}
+#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
@@ -811,6 +813,7 @@ extern "C" {
#define meshtastic_Config_DisplayConfig_compass_orientation_tag 11
#define meshtastic_Config_DisplayConfig_use_12h_clock_tag 12
#define meshtastic_Config_DisplayConfig_use_long_node_name_tag 13
+#define meshtastic_Config_DisplayConfig_enable_message_bubbles_tag 14
#define meshtastic_Config_LoRaConfig_use_preset_tag 1
#define meshtastic_Config_LoRaConfig_modem_preset_tag 2
#define meshtastic_Config_LoRaConfig_bandwidth_tag 3
@@ -957,7 +960,8 @@ X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \
X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \
X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) \
X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) \
-X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13)
+X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) \
+X(a, STATIC, SINGULAR, BOOL, enable_message_bubbles, 14)
#define meshtastic_Config_DisplayConfig_CALLBACK NULL
#define meshtastic_Config_DisplayConfig_DEFAULT NULL
@@ -1035,7 +1039,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
#define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size
#define meshtastic_Config_BluetoothConfig_size 10
#define meshtastic_Config_DeviceConfig_size 100
-#define meshtastic_Config_DisplayConfig_size 34
+#define meshtastic_Config_DisplayConfig_size 36
#define meshtastic_Config_LoRaConfig_size 85
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
#define meshtastic_Config_NetworkConfig_size 204
diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h
index 57e7df8fc..2aaa12da5 100644
--- a/src/mesh/generated/meshtastic/deviceonly.pb.h
+++ b/src/mesh/generated/meshtastic/deviceonly.pb.h
@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
/* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
-#define meshtastic_BackupPreferences_size 2362
+#define meshtastic_BackupPreferences_size 2364
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196
diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h
index f11b13419..b2b71733c 100644
--- a/src/mesh/generated/meshtastic/localonly.pb.h
+++ b/src/mesh/generated/meshtastic/localonly.pb.h
@@ -193,7 +193,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
-#define meshtastic_LocalConfig_size 749
+#define meshtastic_LocalConfig_size 751
#define meshtastic_LocalModuleConfig_size 758
#ifdef __cplusplus
diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h
index 2df8686a3..d79b63ceb 100644
--- a/src/mesh/udp/UdpMulticastHandler.h
+++ b/src/mesh/udp/UdpMulticastHandler.h
@@ -22,10 +22,14 @@
class UdpMulticastHandler final
{
public:
- UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); }
+ UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); }
void start()
{
+ if (isRunning) {
+ LOG_DEBUG("UDP multicast already running");
+ return;
+ }
if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) {
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3],
@@ -34,13 +38,29 @@ class UdpMulticastHandler final
LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str());
#endif
udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); });
+ isRunning = true;
} else {
LOG_DEBUG("Failed to listen on UDP");
}
}
+ void stop()
+ {
+ if (!isRunning) {
+ return;
+ }
+ LOG_DEBUG("Stopping UDP multicast");
+#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
+ udp.close();
+#endif
+ isRunning = false;
+ }
+
void onReceive(AsyncUDPPacket packet)
{
+ if (!isRunning) {
+ return;
+ }
size_t packetLength = packet.length();
#if defined(ARCH_NRF52)
IPAddress ip = packet.remoteIP();
@@ -67,7 +87,7 @@ class UdpMulticastHandler final
bool onSend(const meshtastic_MeshPacket *mp)
{
- if (!mp || !udp) {
+ if (!isRunning || !mp || !udp) {
return false;
}
#if defined(ARCH_NRF52)
@@ -92,5 +112,6 @@ class UdpMulticastHandler final
private:
IPAddress udpIpAddress;
AsyncUDP udp;
+ bool isRunning;
};
#endif // HAS_UDP_MULTICAST
\ No newline at end of file
diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp
index a95dfa58f..5d05c7fc6 100644
--- a/src/mesh/wifi/WiFiAPClient.cpp
+++ b/src/mesh/wifi/WiFiAPClient.cpp
@@ -391,6 +391,11 @@ static void WiFiEvent(WiFiEvent_t event)
LOG_INFO("Disconnected from WiFi access point");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, LOW);
+#endif
+#if HAS_UDP_MULTICAST
+ if (udpHandler) {
+ udpHandler->stop();
+ }
#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
@@ -417,6 +422,11 @@ static void WiFiEvent(WiFiEvent_t event)
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
LOG_INFO("Lost IP address and IP address is reset to 0");
+#if HAS_UDP_MULTICAST
+ if (udpHandler) {
+ udpHandler->stop();
+ }
+#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
syslog.disable();
diff --git a/src/meshUtils.cpp b/src/meshUtils.cpp
index ea2ba641b..1a4497101 100644
--- a/src/meshUtils.cpp
+++ b/src/meshUtils.cpp
@@ -106,4 +106,15 @@ const std::string vformat(const char *const zcFormat, ...)
std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs);
va_end(vaArgs);
return std::string(zc.data(), iLen);
+}
+
+size_t pb_string_length(const char *str, size_t max_len)
+{
+ size_t len = 0;
+ for (size_t i = 0; i < max_len; i++) {
+ if (str[i] != '\0') {
+ len = i + 1;
+ }
+ }
+ return len;
}
\ No newline at end of file
diff --git a/src/meshUtils.h b/src/meshUtils.h
index 9fcf6f8a8..67446f91f 100644
--- a/src/meshUtils.h
+++ b/src/meshUtils.h
@@ -35,4 +35,7 @@ bool isOneOf(int item, int count, ...);
const std::string vformat(const char *const zcFormat, ...);
+// Get actual string length for nanopb char array fields.
+size_t pb_string_length(const char *str, size_t max_len);
+
#define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__)
diff --git a/src/modules/AtakPluginModule.cpp b/src/modules/AtakPluginModule.cpp
index a51ef54c3..bddb6276b 100644
--- a/src/modules/AtakPluginModule.cpp
+++ b/src/modules/AtakPluginModule.cpp
@@ -6,6 +6,7 @@
#include "configuration.h"
#include "main.h"
#include "mesh/compression/unishox2.h"
+#include "meshUtils.h"
#include "meshtastic/atak.pb.h"
AtakPluginModule *atakPluginModule;
@@ -70,16 +71,17 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
auto compressed = cloneTAKPacketData(t);
compressed.is_compressed = true;
if (t->has_contact) {
- auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign,
- sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
+ auto length = unishox2_compress_lines(
+ t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
+ compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed callsign: %d bytes", length);
- length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
- compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1,
- USX_PSET_DFLT, NULL);
+ length = unishox2_compress_lines(
+ t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
+ compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
return;
@@ -87,9 +89,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
LOG_DEBUG("Compressed device_callsign: %d bytes", length);
}
if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
- auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
- compressed.payload_variant.chat.message,
- sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
+ auto length = unishox2_compress_lines(
+ t->payload_variant.chat.message,
+ pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
+ compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
+ NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
return;
@@ -98,9 +102,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to) {
compressed.payload_variant.chat.has_to = true;
- length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
- compressed.payload_variant.chat.to,
- sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
+ length = unishox2_compress_lines(
+ t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
+ compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
return;
@@ -110,9 +114,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to_callsign) {
compressed.payload_variant.chat.has_to_callsign = true;
- length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
- compressed.payload_variant.chat.to_callsign,
- sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
+ length = unishox2_compress_lines(
+ t->payload_variant.chat.to_callsign,
+ pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
+ compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1,
+ USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
return;
@@ -134,18 +140,18 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
auto uncompressed = cloneTAKPacketData(t);
uncompressed.is_compressed = false;
if (t->has_contact) {
- auto length =
- unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign,
- sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
+ auto length = unishox2_decompress_lines(
+ t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
+ uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.callsign. Bailing out");
return;
}
LOG_DEBUG("Decompressed callsign: %d bytes", length);
- length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
- uncompressed.contact.device_callsign,
- sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
+ length = unishox2_decompress_lines(
+ t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
+ uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
return;
@@ -153,9 +159,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
LOG_DEBUG("Decompressed device_callsign: %d bytes", length);
}
if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) {
- auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
- uncompressed.payload_variant.chat.message,
- sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
+ auto length = unishox2_decompress_lines(
+ t->payload_variant.chat.message,
+ pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
+ uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
+ NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.message. Bailing out");
return;
@@ -164,9 +172,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to) {
uncompressed.payload_variant.chat.has_to = true;
- length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
- uncompressed.payload_variant.chat.to,
- sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
+ length = unishox2_decompress_lines(
+ t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
+ uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to. Bailing out");
return;
@@ -176,10 +184,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
if (t->payload_variant.chat.has_to_callsign) {
uncompressed.payload_variant.chat.has_to_callsign = true;
- length =
- unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
- uncompressed.payload_variant.chat.to_callsign,
- sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
+ length = unishox2_decompress_lines(
+ t->payload_variant.chat.to_callsign,
+ pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
+ uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1,
+ USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
return;
diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp
index 3b8225763..6d0255d53 100644
--- a/src/modules/KeyVerificationModule.cpp
+++ b/src/modules/KeyVerificationModule.cpp
@@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
// generate nonce
updateState();
if (currentState != KEY_VERIFICATION_IDLE) {
- IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;)
+ IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
return false;
}
currentNonce = random();
@@ -259,7 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
p->priority = meshtastic_MeshPacket_Priority_HIGH;
service->sendToMesh(p, RX_SRC_LOCAL, true);
currentState = KEY_VERIFICATION_SENDER_AWAITING_USER;
- IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);)
+ IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);)
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING;
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp
index 01f5da2c6..aac398eb9 100644
--- a/src/modules/Telemetry/AirQualityTelemetry.cpp
+++ b/src/modules/Telemetry/AirQualityTelemetry.cpp
@@ -10,7 +10,6 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
-#include "Sensor/AddI2CSensorTemplate.h"
#include "UnitConversions.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
@@ -20,7 +19,12 @@
#include
// Sensors
+#include "Sensor/AddI2CSensorTemplate.h"
#include "Sensor/PMSA003ISensor.h"
+#include "Sensor/SEN5XSensor.h"
+#if __has_include()
+#include "Sensor/SCD4XSensor.h"
+#endif
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -42,6 +46,10 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
// order by priority of metrics/values (low top, high bottom)
addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I);
+ addSensor(i2cScanner, ScanI2C::DeviceType::SEN5X);
+#if __has_include()
+ addSensor(i2cScanner, ScanI2C::DeviceType::SCD4X);
+#endif
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -85,10 +93,27 @@ int32_t AirQualityTelemetryModule::runOnce()
}
// Wake up the sensors that need it
- LOG_INFO("Waking up sensors");
+ LOG_INFO("Waking up sensors...");
for (TelemetrySensor *sensor : sensors) {
- if (!sensor->isActive()) {
- return sensor->wakeUp();
+ if (!sensor->canSleep()) {
+ LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
+ } else if (((lastSentToMesh == 0) ||
+ !Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(),
+ Default::getConfiguredOrDefaultMsScaled(
+ moduleConfig.telemetry.air_quality_interval,
+ default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
+ airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
+ airTime->isTxAllowedAirUtil()) {
+ if (!sensor->isActive()) {
+ LOG_DEBUG("Waking up: %s", sensor->sensorName);
+ return sensor->wakeUp();
+ } else {
+ int32_t pendingForReadyMs = sensor->pendingForReadyMs();
+ LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs);
+ if (pendingForReadyMs) {
+ return pendingForReadyMs;
+ }
+ }
}
}
@@ -109,9 +134,18 @@ int32_t AirQualityTelemetryModule::runOnce()
}
// Send to sleep sensors that consume power
- LOG_INFO("Sending sensors to sleep");
+ LOG_DEBUG("Sending sensors to sleep");
for (TelemetrySensor *sensor : sensors) {
- sensor->sleep();
+ if (sensor->isActive() && sensor->canSleep()) {
+ if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
+ default_telemetry_broadcast_interval_secs,
+ numOnlineNodes)) {
+ LOG_DEBUG("Disabling %s until next period", sensor->sensorName);
+ sensor->sleep();
+ } else {
+ LOG_DEBUG("Sensor stays enabled due to warm up period");
+ }
+ }
}
}
return min(sendToPhoneIntervalMs, result);
@@ -158,8 +192,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
const auto &m = telemetry.variant.air_quality_metrics;
// Check if any telemetry field has valid data
- bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental ||
- m.has_pm25_environmental || m.has_pm100_environmental;
+ bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_co2;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
@@ -186,6 +219,8 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3");
if (m.has_pm100_standard)
entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3");
+ if (m.has_co2)
+ entries.push_back("CO2: " + String(m.co2) + "ppm");
// === Show first available metric on top-right of first line ===
if (!entries.empty()) {
@@ -225,9 +260,13 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard);
- LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
- t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
- t->variant.air_quality_metrics.pm100_environmental);
+ // TODO - Decide what to do with these
+ // LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
+ // t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
+ // t->variant.air_quality_metrics.pm100_environmental);
+
+ LOG_INFO(" | CO2=%i, CO2_T=%f, CO2_H=%f", t->variant.air_quality_metrics.co2,
+ t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity);
#endif
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@@ -241,17 +280,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
{
- bool valid = true;
+ // Note: this is different to the case in EnvironmentTelemetryModule
+ // There, if any sensor fails to read - valid = false.
+ bool valid = false;
bool hasSensor = false;
m->time = getTime();
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
- // TODO - Should we check for sensor state here?
- // If a sensor is sleeping, we should know and check to wake it up
+ bool sensor_get = false;
for (TelemetrySensor *sensor : sensors) {
- LOG_INFO("Reading AQ sensors");
- valid = valid && sensor->getMetrics(m);
+ LOG_DEBUG("Reading %s", sensor->sensorName);
+ // Note - this function doesn't get properly called if within a conditional
+ sensor_get = sensor->getMetrics(m);
+ valid = valid || sensor_get;
hasSensor = true;
}
@@ -291,12 +333,30 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m.time = getTime();
+
if (getAirQualityTelemetry(&m)) {
- LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \
- pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
- m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard,
- m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental,
- m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental);
+
+ bool hasAnyPM =
+ m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard ||
+ m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental ||
+ m.variant.air_quality_metrics.has_pm25_environmental || m.variant.air_quality_metrics.has_pm100_environmental;
+
+ if (hasAnyPM) {
+ LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard,
+ m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard);
+ if (m.variant.air_quality_metrics.has_pm10_environmental)
+ LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
+ m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental,
+ m.variant.air_quality_metrics.pm100_environmental);
+ }
+
+ bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature ||
+ m.variant.air_quality_metrics.has_co2_humidity;
+
+ if (hasAnyCO2) {
+ LOG_INFO("Send: co2=%i, co2_t=%f, co2_rh=%f", m.variant.air_quality_metrics.co2,
+ m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity);
+ }
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
@@ -331,6 +391,20 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
+
+ if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
+ meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
+ notification->level = meshtastic_LogRecord_Level_INFO;
+ notification->time = getValidTime(RTCQualityFromNet);
+ sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
+ Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
+ default_telemetry_broadcast_interval_secs) /
+ 1000U);
+ service->sendClientNotification(notification);
+ sleepOnNextExecution = true;
+ LOG_DEBUG("Start next execution in 5s, then sleep");
+ setIntervalFromNow(FIVE_SECONDS_MS);
+ }
}
return true;
}
diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
index 2225a4d87..bc067c04c 100644
--- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
+++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
@@ -21,26 +21,29 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
_bus = bus;
_address = dev->address.address;
-#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
- if (!currentClock) {
- LOG_WARN("PMSA003I can't be used at this clock speed");
- return false;
- }
-#endif
+#ifdef PMSA003I_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(PMSA003I_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 /* PMSA003I_I2C_CLOCK_SPEED */
_bus->beginTransmission(_address);
if (_bus->endTransmission() != 0) {
- LOG_WARN("PMSA003I not found on I2C at 0x12");
+ LOG_WARN("%s not found on I2C at 0x12", sensorName);
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- reClockI2C(currentClock, _bus);
+ reClockI2C(currentClock, _bus, false);
#endif
status = 1;
- LOG_INFO("PMSA003I Enabled");
+ LOG_INFO("%s Enabled", sensorName);
initI2CSensor();
return true;
@@ -49,30 +52,37 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
if (!isActive()) {
- LOG_WARN("PMSA003I is not active");
+ LOG_WARN("Can't get metrics. %s is not active", sensorName);
return false;
}
-#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
-#endif
+#ifdef PMSA003I_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(PMSA003I_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 /* PMSA003I_I2C_CLOCK_SPEED */
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
- LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available());
+ LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
return false;
}
-#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
- reClockI2C(currentClock, _bus);
-#endif
-
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
buffer[i] = _bus->read();
}
+#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
- LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
+ LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]);
return false;
}
@@ -86,7 +96,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
if (computedChecksum != receivedChecksum) {
- LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
+ LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum);
return false;
}
@@ -136,20 +146,58 @@ bool PMSA003ISensor::isActive()
return state == State::ACTIVE;
}
+int32_t PMSA003ISensor::wakeUpTimeMs()
+{
+#ifdef PMSA003I_ENABLE_PIN
+ return PMSA003I_WARMUP_MS;
+#endif
+ return 0;
+}
+
+int32_t PMSA003ISensor::pendingForReadyMs()
+{
+#ifdef PMSA003I_ENABLE_PIN
+
+ uint32_t now;
+ now = getTime();
+ uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
+ LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted);
+
+ if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) {
+ LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
+ return PMSA003I_WARMUP_MS - sincePmMeasureStarted;
+ }
+ return 0;
+
+#endif
+ return 0;
+}
+
+bool PMSA003ISensor::canSleep()
+{
+#ifdef PMSA003I_ENABLE_PIN
+ return true;
+#endif
+ return false;
+}
+
void PMSA003ISensor::sleep()
{
#ifdef PMSA003I_ENABLE_PIN
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
+ pmMeasureStarted = 0;
#endif
}
uint32_t PMSA003ISensor::wakeUp()
{
#ifdef PMSA003I_ENABLE_PIN
- LOG_INFO("Waking up PMSA003I");
+ LOG_INFO("Waking up %s", sensorName);
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
+ pmMeasureStarted = getTime();
+
return PMSA003I_WARMUP_MS;
#endif
// No need to wait for warmup if already active
diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h
index 09b43d620..3fe96888d 100644
--- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h
+++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h
@@ -3,6 +3,7 @@
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "RTC.h"
#include "TelemetrySensor.h"
#define PMSA003I_I2C_CLOCK_SPEED 100000
@@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor
virtual bool isActive() override;
virtual void sleep() override;
virtual uint32_t wakeUp() override;
+ virtual bool canSleep() override;
+ virtual int32_t wakeUpTimeMs() override;
+ virtual int32_t pendingForReadyMs() override;
private:
enum class State { IDLE, ACTIVE };
@@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor
uint16_t computedChecksum = 0;
uint16_t receivedChecksum = 0;
+ uint32_t pmMeasureStarted = 0;
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
TwoWire *_bus{};
diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp
new file mode 100644
index 000000000..4f6e28b4b
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp
@@ -0,0 +1,893 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include()
+
+#include "../detect/reClockI2C.h"
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "SCD4XSensor.h"
+
+#define SCD4X_NO_ERROR 0
+
+SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {}
+
+bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
+{
+ LOG_INFO("Init sensor: %s", sensorName);
+
+ _bus = bus;
+ _address = dev->address.address;
+
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_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 /* SCD4X_I2C_CLOCK_SPEED */
+
+ scd4x.begin(*_bus, _address);
+
+ // From SCD4X library
+ delay(30);
+
+ // Stop periodic measurement
+ if (!stopMeasurement()) {
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ return false;
+ }
+
+ // Get sensor variant
+ scd4x.getSensorVariant(sensorVariant);
+
+ if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41) {
+ LOG_INFO("%s: Found SCD41", sensorName);
+ if (!powerUp()) {
+ LOG_ERROR("%s: Error trying to execute powerUp()", sensorName);
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ return false;
+ }
+ }
+
+ if (!getASC(ascActive)) {
+ LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ return false;
+ }
+
+ // Start measurement in selected power mode (low power by default)
+ if (!startMeasurement()) {
+ LOG_ERROR("%s: Couldn't start measurement", sensorName);
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ return false;
+ }
+
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ if (state == SCD4X_MEASUREMENT) {
+ status = 1;
+ } else {
+ status = 0;
+ }
+
+ initI2CSensor();
+
+ return true;
+}
+
+bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+
+ if (state != SCD4X_MEASUREMENT) {
+ LOG_ERROR("%s: Not in measurement mode", sensorName);
+ return false;
+ }
+
+ uint16_t co2, error;
+ float temperature, humidity;
+
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_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 /* SCD4X_I2C_CLOCK_SPEED */
+
+ bool dataReady;
+ error = scd4x.getDataReadyStatus(dataReady);
+ if (!dataReady) {
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ LOG_ERROR("SCD4X: Data is not ready");
+ return false;
+ }
+
+ error = scd4x.readMeasurement(co2, temperature, humidity);
+
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ LOG_DEBUG("%s readings: %u ppm, %.2f degC, %.2f %rh", sensorName, co2, temperature, humidity);
+ if (error != SCD4X_NO_ERROR) {
+ LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error);
+ if (co2 == 0) {
+ LOG_ERROR("%s: Skipping invalid measurement.", sensorName);
+ }
+ return false;
+ } else {
+ measurement->variant.air_quality_metrics.has_co2_temperature = true;
+ measurement->variant.air_quality_metrics.has_co2_humidity = true;
+ measurement->variant.air_quality_metrics.has_co2 = true;
+ measurement->variant.air_quality_metrics.co2_temperature = temperature;
+ measurement->variant.air_quality_metrics.co2_humidity = humidity;
+ measurement->variant.air_quality_metrics.co2 = co2;
+ return true;
+ }
+}
+
+/**
+ * @brief Perform a forced recalibration (FRC) of the COâ‚‚ concentration.
+ *
+ * From Sensirion SCD4X I2C Library
+ *
+ * 1. Operate the SCD4x in the operation mode later used for normal sensor
+ * operation (e.g. periodic measurement) for at least 3 minutes in an
+ * environment with a homogenous and constant CO2 concentration. The sensor
+ * must be operated at the voltage desired for the application when
+ * performing the FRC sequence. 2. Issue the stop_periodic_measurement
+ * command. 3. Issue the perform_forced_recalibration command.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::performFRC(uint32_t targetCO2)
+{
+ uint16_t error, frcCorr;
+
+ LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2);
+ error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr);
+
+ // SCD4X Sensirion datasheet
+ delay(400);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName);
+ return false;
+ }
+
+ if (frcCorr == 0xFFFF) {
+ LOG_ERROR("%s: Error while performing forced recalibration.", sensorName);
+ return false;
+ }
+
+ LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr - 0x8000));
+
+ return true;
+}
+
+/**
+ * @brief Start measurement mode
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::startMeasurement()
+{
+ uint16_t error;
+
+ if (state == SCD4X_MEASUREMENT) {
+ LOG_DEBUG("%s: Already in measurement mode", sensorName);
+ return true;
+ }
+
+ if (lowPower) {
+ error = scd4x.startLowPowerPeriodicMeasurement();
+ } else {
+ error = scd4x.startPeriodicMeasurement();
+ }
+
+ if (error == SCD4X_NO_ERROR) {
+ LOG_INFO("%s: Started measurement mode", sensorName);
+ if (lowPower) {
+ LOG_INFO("%s: Low power mode", sensorName);
+ } else {
+ LOG_INFO("%s: Normal power mode", sensorName);
+ }
+
+ state = SCD4X_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 SCD4XSensor::stopMeasurement()
+{
+ uint16_t error;
+
+ error = scd4x.stopPeriodicMeasurement();
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to set idle mode on SCD4X.", sensorName);
+ return false;
+ }
+
+ state = SCD4X_IDLE;
+ co2MeasureStarted = 0;
+ return true;
+}
+
+/**
+ * @brief Set power mode
+ * Pass true to set low power mode
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::setPowerMode(bool _lowPower)
+{
+ lowPower = _lowPower;
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ if (lowPower) {
+ LOG_DEBUG("%s: Set low power mode", sensorName);
+ } else {
+ LOG_DEBUG("%s: Set normal power mode", sensorName);
+ }
+
+ return true;
+}
+
+/**
+ * @brief Check the current mode (ASC or FRC)
+ * From Sensirion SCD4X I2C Library
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::getASC(uint16_t &_ascActive)
+{
+ uint16_t error;
+ LOG_INFO("%s: Getting ASC", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+ error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to send command.", sensorName);
+ return false;
+ }
+
+ if (_ascActive) {
+ LOG_INFO("%s: ASC is enabled", sensorName);
+ } else {
+ LOG_INFO("%s: FRC is enabled", sensorName);
+ }
+
+ return true;
+}
+
+/**
+ * @brief Enable or disable automatic self calibration (ASC).
+ *
+ * From Sensirion SCD4X I2C Library
+ *
+ * Sets the current state (enabled / disabled) of the ASC. By default, ASC
+ * is enabled.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::setASC(bool ascEnabled)
+{
+ uint16_t error;
+
+ if (ascEnabled) {
+ LOG_INFO("%s: Enabling ASC", sensorName);
+ } else {
+ LOG_INFO("%s: Disabling ASC", sensorName);
+ }
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to send command.", sensorName);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
+ return false;
+ }
+
+ if (!getASC(ascActive)) {
+ LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName);
+ return false;
+ }
+
+ if (ascActive) {
+ LOG_INFO("%s: ASC is enabled", sensorName);
+ } else {
+ LOG_INFO("%s: ASC is disabled", sensorName);
+ }
+
+ return true;
+}
+
+/**
+ * @brief Set the value of ASC baseline target in ppm.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * Sets the value of the ASC baseline target, i.e. the COâ‚‚ concentration in
+ * ppm which the ASC algorithm will assume as lower-bound background to
+ * which the SCD4x is exposed to regularly within one ASC period of
+ * operation. To save the setting to the EEPROM, the persist_settings
+ * command must be issued subsequently. The factory default value is 400
+ * ppm.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::setASCBaseline(uint32_t targetCO2)
+{
+ // TODO - Remove?
+ // Available in library, but not described in datasheet.
+ uint16_t error;
+ LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2);
+
+ getASC(ascActive);
+ if (!ascActive) {
+ LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName);
+ return false;
+ }
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to send command.", sensorName);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to make settings persistent.", sensorName);
+ return false;
+ }
+
+ LOG_INFO("%s: Setting ASC baseline successful", sensorName);
+
+ return true;
+}
+
+/**
+ * @brief Set the temperature compensation reference.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * Setting the temperature offset of the SCD4x inside the customer device
+ * allows the user to optimize the RH and T output signal.
+ * By default, the temperature offset is set to 4 °C. To save
+ * the setting to the EEPROM, the persist_settings command may be issued.
+ * Equation (1) details how the characteristic temperature offset can be
+ * calculated using the current temperature output of the sensor (TSCD4x), a
+ * reference temperature value (TReference), and the previous temperature
+ * offset (Toffset_pervious) obtained using the get_temperature_offset_raw
+ * command:
+ *
+ * Toffset_actual = TSCD4x - TReference + Toffset_pervious.
+ *
+ * Recommended temperature offset values are between 0 °C and 20 °C. The
+ * temperature offset does not impact the accuracy of the CO2 output.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::setTemperature(float tempReference)
+{
+ uint16_t error;
+ float prevTempOffset;
+ float updatedTempOffset;
+ float tempOffset;
+ bool dataReady;
+ uint16_t co2;
+ float temperature;
+ float humidity;
+
+ LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference);
+
+ error = scd4x.getDataReadyStatus(dataReady);
+ if (!dataReady) {
+ LOG_ERROR("%s: Data is not ready", sensorName);
+ return false;
+ }
+
+ error = scd4x.readMeasurement(co2, temperature, humidity);
+ if (error != SCD4X_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);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.getTemperatureOffset(prevTempOffset);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error);
+ return false;
+ }
+ LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset);
+
+ tempOffset = temperature - tempReference + prevTempOffset;
+
+ LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset);
+ error = scd4x.setTemperatureOffset(tempOffset);
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ scd4x.getTemperatureOffset(updatedTempOffset);
+ LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset);
+
+ return true;
+}
+
+/**
+ * @brief Get the sensor altitude.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * Altitude in meters above sea level can be set after device installation.
+ * Valid value between 0 and 3000m. This overrides pressure offset.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::getAltitude(uint16_t &altitude)
+{
+ uint16_t error;
+ LOG_INFO("%s: Requesting sensor altitude", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.getSensorAltitude(altitude);
+
+ if (error != SCD4X_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;
+}
+
+/**
+ * @brief Get the ambient pressure around the sensor.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * Gets the ambient pressure in Pa.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure)
+{
+ uint16_t error;
+ LOG_INFO("%s: Requesting sensor ambient pressure", sensorName);
+
+ error = scd4x.getAmbientPressure(ambientPressure);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error);
+ return false;
+ }
+ LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure);
+
+ return true;
+}
+
+/**
+ * @brief Set the sensor altitude.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * Altitude in meters above sea level can be set after device installation.
+ * Valid value between 0 and 3000m. This overrides pressure offset.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::setAltitude(uint32_t altitude)
+{
+ uint16_t error;
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.setSensorAltitude(altitude);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @brief Set the ambient pressure around the sensor.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * The set_ambient_pressure command can be sent during periodic measurements
+ * to enable continuous pressure compensation. Note that setting an ambient
+ * pressure overrides any pressure compensation based on a previously set
+ * sensor altitude. Use of this command is highly recommended for
+ * applications experiencing significant ambient pressure changes to ensure
+ * sensor accuracy. Valid input values are between 70000 - 120000 Pa. The
+ * default value is 101300 Pa.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure)
+{
+ uint16_t error;
+
+ error = scd4x.setAmbientPressure(ambientPressure);
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ // Sensirion doesn't indicate if this is necessary. We send it anyway
+ error = scd4x.persistSettings();
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @brief Perform factory reset to erase the settings stored in the EEPROM.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * The perform_factory_reset command resets all configuration settings
+ * stored in the EEPROM and erases the FRC and ASC algorithm history.
+ * @note This function should not change the clock
+ */
+bool SCD4XSensor::factoryReset()
+{
+ uint16_t error;
+
+ LOG_INFO("%s: Requesting factory reset", sensorName);
+
+ if (!stopMeasurement()) {
+ return false;
+ }
+
+ error = scd4x.performFactoryReset();
+
+ if (error != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error);
+ return false;
+ }
+
+ LOG_INFO("%s: Factory reset successful", sensorName);
+
+ return true;
+}
+
+/**
+ * @brief Put the sensor into sleep mode from idle mode.
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * Put the sensor from idle to sleep to reduce power consumption. Can be
+ * used to power down when operating the sensor in power-cycled single shot
+ * mode.
+ * @note This command is only available in idle mode. Only for SCD41.
+ */
+bool SCD4XSensor::powerDown()
+{
+ LOG_INFO("%s: Trying to send sensor to sleep", sensorName);
+
+ if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) {
+ LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring");
+ return true;
+ }
+
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_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 /* SCD4X_I2C_CLOCK_SPEED */
+
+ if (!stopMeasurement()) {
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ return false;
+ }
+
+ if (scd4x.powerDown() != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Error trying to execute sleep()", sensorName);
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ return false;
+ }
+
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ state = SCD4X_OFF;
+ return true;
+}
+
+/**
+ * @brief Wake up sensor from sleep mode to idle mode (powerUp)
+ *
+ * From Sensirion SCD4X I2C Library.
+ *
+ * Wake up the sensor from sleep mode into idle mode. Note that the SCD4x
+ * does not acknowledge the wake_up command. The sensor's idle state after
+ * wake up can be verified by reading out the serial number.
+ * @note This command is only available for SCD41.
+ * @note This function can't change clock (used in init)
+ */
+bool SCD4XSensor::powerUp()
+{
+ LOG_INFO("%s: Waking up", sensorName);
+
+ if (scd4x.wakeUp() != SCD4X_NO_ERROR) {
+ LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName);
+ return false;
+ }
+
+ state = SCD4X_IDLE;
+
+ return true;
+}
+
+/**
+ * @brief Check if sensor is in measurement mode
+ */
+bool SCD4XSensor::isActive()
+{
+ return state == SCD4X_MEASUREMENT;
+}
+
+/**
+ * @brief Start measurement mode
+ * @note Not used in admin comands, getMetrics or init, can change clock.
+ */
+uint32_t SCD4XSensor::wakeUp()
+{
+
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_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 /* SCD4X_I2C_CLOCK_SPEED */
+
+ if (startMeasurement()) {
+ co2MeasureStarted = getTime();
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+ return SCD4X_WARMUP_MS;
+ }
+
+#if defined(SCD4X_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 SCD4XSensor::sleep()
+{
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_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 /* SCD4X_I2C_CLOCK_SPEED */
+
+ stopMeasurement();
+
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+}
+
+/**
+ * @brief Can sleep function
+ *
+ * Power consumption is very low on lowPower mode, modify this function if
+ * you still want to override this behaviour. Otherwise, sleep is disabled
+ * routinely in low power mode
+ */
+bool SCD4XSensor::canSleep()
+{
+ return lowPower ? false : true;
+}
+
+int32_t SCD4XSensor::wakeUpTimeMs()
+{
+ return SCD4X_WARMUP_MS;
+}
+
+int32_t SCD4XSensor::pendingForReadyMs()
+{
+ uint32_t now;
+ now = getTime();
+ uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000;
+ LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted);
+
+ if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) {
+ LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
+ return SCD4X_WARMUP_MS - sinceCO2MeasureStarted;
+ }
+ return 0;
+}
+
+AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
+ meshtastic_AdminMessage *response)
+{
+ AdminMessageHandleResult result;
+
+#ifdef SCD4X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SCD4X_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 /* SCD4X_I2C_CLOCK_SPEED */
+
+ // TODO: potentially add selftest command?
+ switch (request->which_payload_variant) {
+ case meshtastic_AdminMessage_sensor_config_tag:
+ // Check for ASC-FRC request first
+ if (!request->sensor_config.has_scd4x_config) {
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ break;
+ }
+
+ if (request->sensor_config.scd4x_config.has_factory_reset) {
+ LOG_DEBUG("%s: Requested factory reset", sensorName);
+ this->factoryReset();
+ } else {
+
+ if (request->sensor_config.scd4x_config.has_set_asc) {
+ this->setASC(request->sensor_config.scd4x_config.set_asc);
+ if (request->sensor_config.scd4x_config.set_asc == false) {
+ LOG_DEBUG("%s: Request for FRC", sensorName);
+ if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
+ this->performFRC(request->sensor_config.scd4x_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;
+ }
+ } else {
+ LOG_DEBUG("%s: Request for ASC", sensorName);
+ if (request->sensor_config.scd4x_config.has_set_target_co2_conc) {
+ LOG_DEBUG("%s: Request has target CO2", sensorName);
+ // TODO - Remove? see setASCBaseline function
+ this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc);
+ } else {
+ LOG_DEBUG("%s: Request doesn't have target CO2", sensorName);
+ }
+ }
+ }
+
+ // 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.scd4x_config.has_set_temperature) {
+ this->setTemperature(request->sensor_config.scd4x_config.set_temperature);
+ }
+
+ // Check for altitude or pressure offset
+ if (request->sensor_config.scd4x_config.has_set_altitude) {
+ this->setAltitude(request->sensor_config.scd4x_config.set_altitude);
+ } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) {
+ this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure);
+ }
+
+ // Check for low power mode
+ // NOTE: to switch from one mode to another do:
+ // setPowerMode -> startMeasurement
+ if (request->sensor_config.scd4x_config.has_set_power_mode) {
+ this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode);
+ }
+ }
+
+ // Start measurement mode
+ this->startMeasurement();
+
+ result = AdminMessageHandleResult::HANDLED;
+ break;
+
+ default:
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ }
+
+#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ return result;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h
new file mode 100644
index 000000000..1ed86a183
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h
@@ -0,0 +1,63 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "RTC.h"
+#include "TelemetrySensor.h"
+#include
+
+// Max speed 400kHz
+#define SCD4X_I2C_CLOCK_SPEED 100000
+#define SCD4X_WARMUP_MS 5000
+
+class SCD4XSensor : public TelemetrySensor
+{
+ private:
+ SensirionI2cScd4x scd4x;
+ TwoWire *_bus{};
+ uint8_t _address{};
+
+ bool performFRC(uint32_t targetCO2);
+ bool setASCBaseline(uint32_t targetCO2);
+ bool getASC(uint16_t &ascEnabled);
+ bool setASC(bool ascEnabled);
+ bool setTemperature(float tempReference);
+ bool getAltitude(uint16_t &altitude);
+ bool setAltitude(uint32_t altitude);
+ bool getAmbientPressure(uint32_t &ambientPressure);
+ bool setAmbientPressure(uint32_t ambientPressure);
+ bool factoryReset();
+ bool setPowerMode(bool _lowPower);
+ bool startMeasurement();
+ bool stopMeasurement();
+
+ uint16_t ascActive = 1;
+ // low power measurement mode (on sensirion side). Disables sleep mode
+ // Improvement and testing needed for timings
+ bool lowPower = true;
+ uint32_t co2MeasureStarted = 0;
+
+ public:
+ SCD4XSensor();
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+ virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
+
+ enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT };
+ SCD4XState state = SCD4X_OFF;
+ SCD4xSensorVariant sensorVariant{};
+
+ virtual bool isActive() override;
+
+ virtual void sleep() override; // Stops measurement (measurement -> idle)
+ virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement)
+ bool powerDown(); // Powers down sensor (idle -> power-off)
+ bool powerUp(); // Powers the sensor (power-off -> idle)
+ 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
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp
new file mode 100644
index 000000000..fc20ce1f2
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp
@@ -0,0 +1,957 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
+
+#include "../detect/reClockI2C.h"
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "FSCommon.h"
+#include "SEN5XSensor.h"
+#include "SPILock.h"
+#include "SafeFile.h"
+#include "TelemetrySensor.h"
+#include // FLT_MAX
+#include
+#include
+
+SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {}
+
+bool SEN5XSensor::getVersion()
+{
+ if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)) {
+ LOG_ERROR("SEN5X: Error sending version command");
+ return false;
+ }
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t versionBuffer[12];
+ size_t charNumber = readBuffer(&versionBuffer[0], 3);
+ if (charNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting data ready flag value");
+ return false;
+ }
+
+ firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10);
+ hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10);
+ protocolVer = versionBuffer[5] + (versionBuffer[6] / 10);
+
+ LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer);
+ LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer);
+ LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer);
+
+ return true;
+}
+
+bool SEN5XSensor::findModel()
+{
+ if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) {
+ LOG_ERROR("SEN5X: Error asking for product name");
+ return false;
+ }
+ delay(50); // From Sensirion Datasheet
+
+ const uint8_t nameSize = 48;
+ uint8_t name[nameSize];
+ size_t charNumber = readBuffer(&name[0], nameSize);
+ if (charNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting device name");
+ return false;
+ }
+
+ // We only check the last character that defines the model SEN5X
+ switch (name[4]) {
+ case 48:
+ model = SEN50;
+ LOG_INFO("SEN5X: found sensor model SEN50");
+ break;
+ case 52:
+ model = SEN54;
+ LOG_INFO("SEN5X: found sensor model SEN54");
+ break;
+ case 53:
+ model = SEN55;
+ LOG_INFO("SEN5X: found sensor model SEN55");
+ break;
+ }
+
+ return true;
+}
+
+bool SEN5XSensor::sendCommand(uint16_t command)
+{
+ uint8_t nothing;
+ return sendCommand(command, ¬hing, 0);
+}
+
+bool SEN5XSensor::sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber)
+{
+ // At least we need two bytes for the command
+ uint8_t bufferSize = 2;
+
+ // Add space for CRC bytes (one every two bytes)
+ if (byteNumber > 0)
+ bufferSize += byteNumber + (byteNumber / 2);
+
+ uint8_t toSend[bufferSize];
+ uint8_t i = 0;
+ toSend[i++] = static_cast((command & 0xFF00) >> 8);
+ toSend[i++] = static_cast((command & 0x00FF) >> 0);
+
+ // Prepare buffer with CRC every third byte
+ uint8_t bi = 0;
+ if (byteNumber > 0) {
+ while (bi < byteNumber) {
+ toSend[i++] = buffer[bi++];
+ toSend[i++] = buffer[bi++];
+ uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]);
+ toSend[i++] = calcCRC;
+ }
+ }
+
+#ifdef SEN5X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SEN5X_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 /* SEN5X_I2C_CLOCK_SPEED */
+
+ // Transmit the data
+ // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize);
+ // Note: this delay is necessary to allow for long-buffers
+ delay(20);
+ _bus->beginTransmission(_address);
+ size_t writtenBytes = _bus->write(toSend, bufferSize);
+ uint8_t i2c_error = _bus->endTransmission();
+
+#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ if (writtenBytes != bufferSize) {
+ LOG_ERROR("SEN5X: Error writting on I2C bus");
+ return false;
+ }
+
+ if (i2c_error != 0) {
+ LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error);
+ return false;
+ }
+ return true;
+}
+
+uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber)
+{
+#ifdef SEN5X_I2C_CLOCK_SPEED
+#ifdef CAN_RECLOCK_I2C
+ uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false);
+#elif !HAS_SCREEN
+ reClockI2C(SEN5X_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 /* SEN5X_I2C_CLOCK_SPEED */
+
+ size_t readBytes = _bus->requestFrom(_address, byteNumber);
+ if (readBytes != byteNumber) {
+ LOG_ERROR("SEN5X: Error reading I2C bus");
+ return 0;
+ }
+
+ uint8_t i = 0;
+ uint8_t receivedBytes = 0;
+ while (readBytes > 0) {
+ buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments.
+ buffer[i++] = _bus->read();
+ uint8_t recvCRC = _bus->read();
+ uint8_t calcCRC = sen5xCRC(&buffer[i - 2]);
+ if (recvCRC != calcCRC) {
+ LOG_ERROR("SEN5X: Checksum error while receiving msg");
+ return 0;
+ }
+ readBytes -= 3;
+ receivedBytes += 2;
+ }
+#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
+ reClockI2C(currentClock, _bus, false);
+#endif
+
+ return receivedBytes;
+}
+
+uint8_t SEN5XSensor::sen5xCRC(uint8_t *buffer)
+{
+ // This code is based on Sensirion's own implementation
+ // https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp
+ uint8_t crc = 0xff;
+
+ for (uint8_t i = 0; i < 2; i++) {
+
+ crc ^= buffer[i];
+
+ for (uint8_t bit = 8; bit > 0; bit--) {
+ if (crc & 0x80)
+ crc = (crc << 1) ^ 0x31;
+ else
+ crc = (crc << 1);
+ }
+ }
+
+ return crc;
+}
+
+void SEN5XSensor::sleep()
+{
+ // TODO Check this works
+ idle(true);
+}
+
+bool SEN5XSensor::idle(bool checkState)
+{
+ // From the datasheet:
+ // By default, the VOC algorithm resets its state to initial
+ // values each time a measurement is started,
+ // even if the measurement was stopped only for a short
+ // time. So, the VOC index output value needs a long time
+ // until it is stable again. This can be avoided by
+ // restoring the previously memorized algorithm state before
+ // starting the measure mode
+
+ if (checkState) {
+ // If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle
+ if (model != SEN50) {
+ // Get VOC state before going to idle mode
+ vocValid = false;
+ if (vocStateFromSensor()) {
+ vocValid = vocStateValid();
+ // Check if we have time, and store it
+ uint32_t now; // If time is RTCQualityNone, it will return zero
+ now = getValidTime(RTCQuality::RTCQualityDevice);
+ if (now) {
+ // Check if state is valid (non-zero)
+ vocTime = now;
+ }
+ }
+
+ if (vocStateStable() && vocValid) {
+ saveState();
+ } else {
+ LOG_INFO("SEN5X: Not stopping measurement, vocState is not stable yet!");
+ return true;
+ }
+ }
+ }
+
+ if (!oneShotMode) {
+ LOG_INFO("SEN5X: Not stopping measurement, continuous mode!");
+ return true;
+ }
+
+ // Switch to low-power based on the model
+ if (model == SEN50) {
+ if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error stopping measurement");
+ return false;
+ }
+ state = SEN5X_IDLE;
+ LOG_INFO("SEN5X: Stop measurement mode");
+ } else {
+ if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) {
+ LOG_ERROR("SEN5X: Error switching to RHT/Gas measurement");
+ return false;
+ }
+ state = SEN5X_RHTGAS_ONLY;
+ LOG_INFO("SEN5X: Switch to RHT/Gas only measurement mode");
+ }
+
+ delay(200); // From Sensirion Datasheet
+ pmMeasureStarted = 0;
+ return true;
+}
+
+bool SEN5XSensor::vocStateRecent(uint32_t now)
+{
+ if (now) {
+ uint32_t passed = now - vocTime; // in seconds
+
+ // Check if state is recent, less than 10 minutes (600 seconds)
+ if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool SEN5XSensor::vocStateValid()
+{
+ if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && !vocState[4] && !vocState[5] && !vocState[6] &&
+ !vocState[7]) {
+ LOG_DEBUG("SEN5X: VOC state is all 0, invalid");
+ return false;
+ } else {
+ LOG_DEBUG("SEN5X: VOC state is valid");
+ return true;
+ }
+}
+
+bool SEN5XSensor::vocStateToSensor()
+{
+ if (model == SEN50) {
+ return true;
+ }
+
+ if (!vocStateValid()) {
+ LOG_INFO("SEN5X: VOC state is invalid, not sending");
+ return true;
+ }
+
+ if (!sendCommand(SEN5X_STOP_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error stoping measurement");
+ return false;
+ }
+ delay(200); // From Sensirion Datasheet
+
+ LOG_DEBUG("SEN5X: Sending VOC state to sensor");
+ LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], vocState[3], vocState[4], vocState[5],
+ vocState[6], vocState[7]);
+
+ // Note: send command already takes into account the CRC
+ // buffer size increment needed
+ if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)) {
+ LOG_ERROR("SEN5X: Error sending VOC's state command'");
+ return false;
+ }
+
+ return true;
+}
+
+bool SEN5XSensor::vocStateFromSensor()
+{
+ if (model == SEN50) {
+ return true;
+ }
+
+ LOG_INFO("SEN5X: Getting VOC state from sensor");
+ // Ask VOCs state from the sensor
+ if (!sendCommand(SEN5X_RW_VOCS_STATE)) {
+ LOG_ERROR("SEN5X: Error sending VOC's state command'");
+ return false;
+ }
+
+ delay(20); // From Sensirion Datasheet
+
+ // Retrieve the data
+ // Allocate buffer to account for CRC
+ size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2));
+ delay(20); // From Sensirion Datasheet
+
+ if (receivedNumber == 0) {
+ LOG_DEBUG("SEN5X: Error getting VOC's state");
+ return false;
+ }
+
+ // Print the state (if debug is on)
+ LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2],
+ vocState[3], vocState[4], vocState[5], vocState[6], vocState[7]);
+
+ return true;
+}
+
+bool SEN5XSensor::loadState()
+{
+#ifdef FSCom
+ spiLock->lock();
+ auto file = FSCom.open(sen5XStateFileName, FILE_O_READ);
+ bool okay = false;
+ if (file) {
+ LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName);
+ pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size};
+
+ if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
+ LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
+ } else {
+ lastCleaning = sen5xstate.last_cleaning_time;
+ lastCleaningValid = sen5xstate.last_cleaning_valid;
+ oneShotMode = sen5xstate.one_shot_mode;
+
+ if (model != SEN50) {
+ vocTime = sen5xstate.voc_state_time;
+ vocValid = sen5xstate.voc_state_valid;
+ // Unpack state
+ vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56);
+ vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48);
+ vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40);
+ vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32);
+ vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24);
+ vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16);
+ vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8);
+ vocState[0] = (uint8_t)sen5xstate.voc_state_array;
+ }
+
+ // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning);
+ // LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid);
+ // LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false");
+ // LOG_DEBUG("Loaded vocTime %u", vocTime);
+ // LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]",
+ // vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]);
+ // LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in");
+
+ okay = true;
+ }
+ file.close();
+ } else {
+ LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName);
+ }
+ spiLock->unlock();
+ return okay;
+#else
+ LOG_ERROR("SEN5X: ERROR - Filesystem not implemented");
+#endif
+}
+
+bool SEN5XSensor::saveState()
+{
+#ifdef FSCom
+ auto file = SafeFile(sen5XStateFileName);
+
+ sen5xstate.last_cleaning_time = lastCleaning;
+ sen5xstate.last_cleaning_valid = lastCleaningValid;
+ sen5xstate.one_shot_mode = oneShotMode;
+
+ if (model != SEN50) {
+ sen5xstate.has_voc_state_time = true;
+ sen5xstate.has_voc_state_valid = true;
+ sen5xstate.has_voc_state_array = true;
+
+ sen5xstate.voc_state_time = vocTime;
+ sen5xstate.voc_state_valid = vocValid;
+ // Unpack state (8 bytes)
+ sen5xstate.voc_state_array = (((uint64_t)vocState[7]) << 56) | ((uint64_t)vocState[6] << 48) |
+ ((uint64_t)vocState[5] << 40) | ((uint64_t)vocState[4] << 32) |
+ ((uint64_t)vocState[3] << 24) | ((uint64_t)vocState[2] << 16) |
+ ((uint64_t)vocState[1] << 8) | ((uint64_t)vocState[0]);
+ }
+
+ bool okay = false;
+
+ LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName);
+ pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_SEN5XState_size};
+
+ if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) {
+ LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
+ } else {
+ okay = true;
+ }
+
+ okay &= file.close();
+
+ if (okay)
+ LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName);
+
+ return okay;
+#else
+ LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName);
+#endif
+}
+
+bool SEN5XSensor::isActive()
+{
+ return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2;
+}
+
+uint32_t SEN5XSensor::wakeUp()
+{
+
+ LOG_DEBUG("SEN5X: Waking up sensor");
+
+ if (!sendCommand(SEN5X_START_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error starting measurement");
+ // TODO - what should this return?? Something actually on the default interval?
+ return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+ }
+ delay(50); // From Sensirion Datasheet
+
+ // TODO - This is currently "problematic"
+ // If time is updated in between reads, there is no way to
+ // keep track of how long it has passed
+ pmMeasureStarted = getTime();
+ state = SEN5X_MEASUREMENT;
+ if (state == SEN5X_MEASUREMENT)
+ LOG_INFO("SEN5X: Started measurement mode");
+ return SEN5X_WARMUP_MS_1;
+}
+
+bool SEN5XSensor::vocStateStable()
+{
+ uint32_t now;
+ now = getTime();
+ uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted);
+ LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted);
+ return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S;
+}
+
+bool SEN5XSensor::startCleaning()
+{
+ // Note: we only should enter here if we have a valid RTC with at least
+ // RTCQuality::RTCQualityDevice
+ state = SEN5X_CLEANING;
+
+ // Note that cleaning command can only be run when the sensor is in measurement mode
+ if (!sendCommand(SEN5X_START_MEASUREMENT)) {
+ LOG_ERROR("SEN5X: Error starting measurment mode");
+ return false;
+ }
+ delay(50); // From Sensirion Datasheet
+
+ if (!sendCommand(SEN5X_START_FAN_CLEANING)) {
+ LOG_ERROR("SEN5X: Error starting fan cleaning");
+ return false;
+ }
+ delay(20); // From Sensirion Datasheet
+
+ // This message will be always printed so the user knows the device it's not hung
+ LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds...");
+
+ uint16_t started = millis();
+ while (millis() - started < 10500) {
+ delay(500);
+ }
+ LOG_INFO("SEN5X: Cleaning done!!");
+
+ // Save timestamp in flash so we know when a week has passed
+ uint32_t now;
+ now = getValidTime(RTCQuality::RTCQualityDevice);
+ // If time is not RTCQualityNone, it will return non-zero
+ lastCleaning = now;
+ lastCleaningValid = true;
+ saveState();
+
+ idle();
+ return true;
+}
+
+bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
+{
+ state = SEN5X_NOT_DETECTED;
+ LOG_INFO("Init sensor: %s", sensorName);
+
+ _bus = bus;
+ _address = dev->address.address;
+
+ delay(50); // without this there is an error on the deviceReset function
+
+ if (!sendCommand(SEN5X_RESET)) {
+ LOG_ERROR("SEN5X: Error reseting device");
+ return false;
+ }
+ delay(200); // From Sensirion Datasheet
+
+ if (!findModel()) {
+ LOG_ERROR("SEN5X: error finding sensor model");
+ return false;
+ }
+
+ // Check the firmware version
+ if (!getVersion())
+ return false;
+ if (firmwareVer < 2) {
+ LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation");
+ return false;
+ }
+ delay(200); // From Sensirion Datasheet
+
+ // Detection succeeded
+ state = SEN5X_IDLE;
+ status = 1;
+
+ // Load state
+ loadState();
+
+ // Check if it is time to do a cleaning
+ uint32_t now;
+ int32_t passed;
+ now = getValidTime(RTCQuality::RTCQualityDevice);
+
+ // If time is not RTCQualityNone, it will return non-zero
+ if (now) {
+ if (lastCleaningValid) {
+
+ passed = now - lastCleaning; // in seconds
+
+ if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) {
+ // If current date greater than 01/01/2018 (validity check)
+ LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed,
+ lastCleaning);
+ startCleaning();
+ } else {
+ LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning);
+ }
+ } else {
+ // We assume the device has just been updated or it is new,
+ // so no need to trigger a cleaning.
+ // Just save the timestamp to do a cleaning one week from now.
+ // Otherwise, we will never trigger cleaning in some cases
+ lastCleaning = now;
+ lastCleaningValid = true;
+ LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning);
+ saveState();
+ }
+
+ if (model != SEN50) {
+ if (!vocValid) {
+ LOG_INFO("SEN5X: No valid VOC's state found");
+ } else {
+ // Check if state is recent
+ if (vocStateRecent(now)) {
+ // If current date greater than 01/01/2018 (validity check)
+ // Send it to the sensor
+ LOG_INFO("SEN5X: VOC state is valid and recent");
+ vocStateToSensor();
+ } else {
+ LOG_INFO("SEN5X: VOC state is too old or date is invalid");
+ LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now);
+ }
+ }
+ }
+ } else {
+ // TODO - Should this actually ignore? We could end up never cleaning...
+ LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved state. Trying again later");
+ }
+
+ idle(false);
+ rhtGasMeasureStarted = now;
+
+ initI2CSensor();
+ return true;
+}
+
+bool SEN5XSensor::readValues()
+{
+ if (!sendCommand(SEN5X_READ_VALUES)) {
+ LOG_ERROR("SEN5X: Error sending read command");
+ return false;
+ }
+ LOG_DEBUG("SEN5X: Reading PM Values");
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t dataBuffer[16];
+ size_t receivedNumber = readBuffer(&dataBuffer[0], 24);
+ if (receivedNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting values");
+ return false;
+ }
+
+ // Get the integers
+ uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]);
+ uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]);
+ uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]);
+ uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]);
+
+ int16_t int_humidity = static_cast((dataBuffer[8] << 8) | dataBuffer[9]);
+ int16_t int_temperature = static_cast((dataBuffer[10] << 8) | dataBuffer[11]);
+ int16_t int_vocIndex = static_cast((dataBuffer[12] << 8) | dataBuffer[13]);
+ int16_t int_noxIndex = static_cast((dataBuffer[14] << 8) | dataBuffer[15]);
+
+ // Convert values based on Sensirion Arduino lib
+ sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX;
+ sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX;
+ sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX;
+ sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX;
+ sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX;
+ sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX;
+ sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX;
+ sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX;
+
+ LOG_DEBUG("Got: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5,
+ sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0);
+
+ if (model != SEN50) {
+ LOG_DEBUG("Got: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sen5xmeasurement.humidity, sen5xmeasurement.temperature,
+ sen5xmeasurement.vocIndex);
+ }
+
+ if (model == SEN55) {
+ LOG_DEBUG("Got: noxIndex=%.2f", sen5xmeasurement.noxIndex);
+ }
+
+ return true;
+}
+
+bool SEN5XSensor::readPNValues(bool cumulative)
+{
+ if (!sendCommand(SEN5X_READ_PM_VALUES)) {
+ LOG_ERROR("SEN5X: Error sending read command");
+ return false;
+ }
+
+ LOG_DEBUG("SEN5X: Reading PN Values");
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t dataBuffer[20];
+ size_t receivedNumber = readBuffer(&dataBuffer[0], 30);
+ if (receivedNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting PN values");
+ return false;
+ }
+
+ // Get the integers
+ // uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]);
+ // uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]);
+ // uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]);
+ // uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]);
+ uint16_t uint_pN0p5 = static_cast((dataBuffer[8] << 8) | dataBuffer[9]);
+ uint16_t uint_pN1p0 = static_cast((dataBuffer[10] << 8) | dataBuffer[11]);
+ uint16_t uint_pN2p5 = static_cast((dataBuffer[12] << 8) | dataBuffer[13]);
+ uint16_t uint_pN4p0 = static_cast((dataBuffer[14] << 8) | dataBuffer[15]);
+ uint16_t uint_pN10p0 = static_cast((dataBuffer[16] << 8) | dataBuffer[17]);
+ uint16_t uint_tSize = static_cast((dataBuffer[18] << 8) | dataBuffer[19]);
+
+ // Convert values based on Sensirion Arduino lib
+ // Multiply by 100 for converting from #/cm3 to #/0.1l for PN values
+ sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100 : UINT32_MAX;
+ sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX;
+
+ // Remove accumuluative values:
+ // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85
+ if (!cumulative) {
+ sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0;
+ sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5;
+ sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0;
+ sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5;
+ }
+
+ LOG_DEBUG("Got: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sen5xmeasurement.pN0p5,
+ sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, sen5xmeasurement.pN10p0,
+ sen5xmeasurement.tSize);
+
+ return true;
+}
+
+uint8_t SEN5XSensor::getMeasurements()
+{
+ uint32_t now;
+ now = getTime();
+
+ // Try to get new data
+ if (!sendCommand(SEN5X_READ_DATA_READY)) {
+ LOG_ERROR("SEN5X: Error sending command data ready flag");
+ return 2;
+ }
+ delay(20); // From Sensirion Datasheet
+
+ uint8_t dataReadyBuffer[3];
+ size_t charNumber = readBuffer(&dataReadyBuffer[0], 3);
+ if (charNumber == 0) {
+ LOG_ERROR("SEN5X: Error getting device version value");
+ return 2;
+ }
+
+ bool dataReady = dataReadyBuffer[1];
+ uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000;
+ // Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL
+ if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) {
+ LOG_INFO("SEN5X: Data is not ready");
+ return 1;
+ }
+
+ if (!readValues()) {
+ LOG_ERROR("SEN5X: Error getting readings");
+ return 2;
+ }
+
+ if (!readPNValues(false)) {
+ LOG_ERROR("SEN5X: Error getting PN readings");
+ return 2;
+ }
+
+ lastDataPoll = now;
+
+ return 0;
+}
+
+int32_t SEN5XSensor::wakeUpTimeMs()
+{
+ return SEN5X_WARMUP_MS_2;
+}
+
+int32_t SEN5XSensor::pendingForReadyMs()
+{
+ uint32_t now;
+ now = getTime();
+ uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
+ LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted);
+
+ switch (state) {
+ case SEN5X_MEASUREMENT: {
+
+ if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) {
+ LOG_INFO("SEN5X: not enough time passed since starting measurement");
+ return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted;
+ }
+
+ if (!pmMeasureStarted) {
+ pmMeasureStarted = now;
+ }
+
+ // Get PN values to check if we are above or below threshold
+ readPNValues(true);
+ lastDataPoll = now;
+
+ // If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later
+ if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
+ LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period");
+ state = SEN5X_MEASUREMENT_2;
+ // Report how many seconds are pending to cover the first warm up period
+ return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
+ }
+ return 0;
+ }
+ case SEN5X_MEASUREMENT_2: {
+ if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) {
+ // Report how many seconds are pending to cover the first warm up period
+ return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted;
+ }
+ return 0;
+ }
+ default: {
+ return -1;
+ }
+ }
+}
+
+bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+ LOG_INFO("SEN5X: Attempting to get metrics");
+ if (!isActive()) {
+ LOG_INFO("SEN5X: not in measurement mode");
+ return false;
+ }
+
+ uint8_t response;
+ response = getMeasurements();
+
+ if (response == 0) {
+ if (sen5xmeasurement.pM1p0 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.has_pm10_standard = true;
+ measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0;
+ }
+ if (sen5xmeasurement.pM2p5 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.has_pm25_standard = true;
+ measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5;
+ }
+ if (sen5xmeasurement.pM4p0 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.has_pm40_standard = true;
+ measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0;
+ }
+ if (sen5xmeasurement.pM10p0 != UINT16_MAX) {
+ measurement->variant.air_quality_metrics.has_pm100_standard = true;
+ measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0;
+ }
+ if (sen5xmeasurement.pN0p5 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.has_particles_05um = true;
+ measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5;
+ }
+ if (sen5xmeasurement.pN1p0 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.has_particles_10um = true;
+ measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0;
+ }
+ if (sen5xmeasurement.pN2p5 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.has_particles_25um = true;
+ measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5;
+ }
+ if (sen5xmeasurement.pN4p0 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.has_particles_40um = true;
+ measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0;
+ }
+ if (sen5xmeasurement.pN10p0 != UINT32_MAX) {
+ measurement->variant.air_quality_metrics.has_particles_100um = true;
+ measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0;
+ }
+ if (sen5xmeasurement.tSize != FLT_MAX) {
+ measurement->variant.air_quality_metrics.has_particles_tps = true;
+ measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize;
+ }
+
+ if (model != SEN50) {
+ if (sen5xmeasurement.humidity != FLT_MAX) {
+ measurement->variant.air_quality_metrics.has_pm_humidity = true;
+ measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity;
+ }
+ if (sen5xmeasurement.temperature != FLT_MAX) {
+ measurement->variant.air_quality_metrics.has_pm_temperature = true;
+ measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature;
+ }
+ if (sen5xmeasurement.noxIndex != FLT_MAX) {
+ measurement->variant.air_quality_metrics.has_pm_voc_idx = true;
+ measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex;
+ }
+ }
+
+ if (model == SEN55) {
+ if (sen5xmeasurement.noxIndex != FLT_MAX) {
+ measurement->variant.air_quality_metrics.has_pm_nox_idx = true;
+ measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex;
+ }
+ }
+
+ return true;
+ } else if (response == 1) {
+ // TODO return because data was not ready yet
+ // Should this return false?
+ idle();
+ return false;
+ } else if (response == 2) {
+ // Return with error for non-existing data
+ idle();
+ return false;
+ }
+
+ return true;
+}
+
+void SEN5XSensor::setMode(bool setOneShot)
+{
+ oneShotMode = setOneShot;
+}
+
+AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
+ meshtastic_AdminMessage *response)
+{
+ AdminMessageHandleResult result;
+ result = AdminMessageHandleResult::NOT_HANDLED;
+
+ switch (request->which_payload_variant) {
+ case meshtastic_AdminMessage_sensor_config_tag:
+ if (!request->sensor_config.has_sen5x_config) {
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ break;
+ }
+
+ // TODO - Add admin command to set temperature offset
+ // Check for temperature offset
+ // if (request->sensor_config.sen5x_config.has_set_temperature) {
+ // this->setTemperature(request->sensor_config.sen5x_config.set_temperature);
+ // }
+
+ // Check for one-shot/continuous mode request
+ if (request->sensor_config.sen5x_config.has_set_one_shot_mode) {
+ this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode);
+ }
+
+ result = AdminMessageHandleResult::HANDLED;
+ break;
+
+ default:
+ result = AdminMessageHandleResult::NOT_HANDLED;
+ }
+
+ return result;
+}
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h
new file mode 100644
index 000000000..46f8c70e9
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h
@@ -0,0 +1,170 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "RTC.h"
+#include "TelemetrySensor.h"
+#include "Wire.h"
+
+// Warm up times for SEN5X from the datasheet
+#ifndef SEN5X_WARMUP_MS_1
+#define SEN5X_WARMUP_MS_1 15000
+#endif
+
+#ifndef SEN5X_WARMUP_MS_2
+#define SEN5X_WARMUP_MS_2 30000
+#endif
+
+#ifndef SEN5X_POLL_INTERVAL
+#define SEN5X_POLL_INTERVAL 1000
+#endif
+
+#ifndef SEN5X_I2C_CLOCK_SPEED
+#define SEN5X_I2C_CLOCK_SPEED 100000
+#endif
+
+/*
+Time after which the sensor can go to sleep, as the warmup period has passed
+and the VOCs sensor will is allowed to stop (although needs to recover the state
+each time)
+*/
+#ifndef SEN5X_VOC_STATE_WARMUP_S
+/* Note for Testing 5' is enough
+Sensirion recommends 1h
+This can be bypassed completely if switching to low-power RHT/Gas mode and setting
+SEN5X_VOC_STATE_WARMUP_S 0
+*/
+#define SEN5X_VOC_STATE_WARMUP_S 3600
+#endif
+
+#define ONE_WEEK_IN_SECONDS 604800
+
+struct _SEN5XMeasurements {
+ uint16_t pM1p0;
+ uint16_t pM2p5;
+ uint16_t pM4p0;
+ uint16_t pM10p0;
+ uint32_t pN0p5;
+ uint32_t pN1p0;
+ uint32_t pN2p5;
+ uint32_t pN4p0;
+ uint32_t pN10p0;
+ float tSize;
+ float humidity;
+ float temperature;
+ float vocIndex;
+ float noxIndex;
+};
+
+class SEN5XSensor : public TelemetrySensor
+{
+ private:
+ TwoWire *_bus{};
+ uint8_t _address{};
+
+ bool getVersion();
+ float firmwareVer = -1;
+ float hardwareVer = -1;
+ float protocolVer = -1;
+ bool findModel();
+
+// Commands
+#define SEN5X_RESET 0xD304
+#define SEN5X_GET_PRODUCT_NAME 0xD014
+#define SEN5X_GET_FIRMWARE_VERSION 0xD100
+#define SEN5X_START_MEASUREMENT 0x0021
+#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
+#define SEN5X_STOP_MEASUREMENT 0x0104
+#define SEN5X_READ_DATA_READY 0x0202
+#define SEN5X_START_FAN_CLEANING 0x5607
+#define SEN5X_RW_VOCS_STATE 0x6181
+
+#define SEN5X_READ_VALUES 0x03C4
+#define SEN5X_READ_RAW_VALUES 0x03D2
+#define SEN5X_READ_PM_VALUES 0x0413
+
+#define SEN5X_VOC_VALID_TIME 600
+#define SEN5X_VOC_VALID_DATE 1514764800
+
+ enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 };
+ SEN5Xmodel model = SEN5X_UNKNOWN;
+
+ enum SEN5XState {
+ SEN5X_OFF,
+ SEN5X_IDLE,
+ SEN5X_RHTGAS_ONLY,
+ SEN5X_MEASUREMENT,
+ SEN5X_MEASUREMENT_2,
+ SEN5X_CLEANING,
+ SEN5X_NOT_DETECTED
+ };
+ SEN5XState state = SEN5X_OFF;
+ // Flag to work on one-shot (read and sleep), or continuous mode
+ bool oneShotMode = true;
+ void setMode(bool setOneShot);
+ bool vocStateValid();
+/* Sensirion recommends taking a reading after 15 seconds,
+if the Particle number reading is over 100#/cm3 the reading is OK,
+but if it is lower wait until 30 seconds and take it again.
+See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
+*/
+#define SEN5X_PN4P0_CONC_THD 100
+
+ bool sendCommand(uint16_t command);
+ bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0);
+ uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber); // Return number of bytes received
+ uint8_t sen5xCRC(uint8_t *buffer);
+ bool startCleaning();
+ uint8_t getMeasurements();
+ // bool readRawValues();
+ bool readPNValues(bool cumulative);
+ bool readValues();
+
+ uint32_t pmMeasureStarted = 0;
+ uint32_t rhtGasMeasureStarted = 0;
+ uint32_t lastDataPoll = 0;
+ _SEN5XMeasurements sen5xmeasurement{};
+
+ bool idle(bool checkState = true);
+
+ protected:
+ // Store status of the sensor in this file
+ const char *sen5XStateFileName = "/prefs/sen5X.dat";
+ meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero;
+
+ bool loadState();
+ bool saveState();
+
+ // Cleaning State
+ uint32_t lastCleaning = 0;
+ bool lastCleaningValid = false;
+
+// VOC State
+#define SEN5X_VOC_STATE_BUFFER_SIZE 8
+ uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{};
+ uint32_t vocTime = 0;
+ bool vocValid = false;
+
+ bool vocStateFromSensor();
+ bool vocStateToSensor();
+ bool vocStateStable();
+ bool vocStateRecent(uint32_t now);
+
+ public:
+ SEN5XSensor();
+ virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
+ virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+
+ virtual bool isActive() override;
+ virtual void sleep() override;
+ virtual uint32_t wakeUp() override;
+ virtual bool canSleep() override { return true; }
+ virtual int32_t wakeUpTimeMs() override;
+ virtual int32_t pendingForReadyMs() override;
+
+ AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
+ meshtastic_AdminMessage *response) override;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h
index af51ddfad..6c53bbd72 100644
--- a/src/modules/Telemetry/Sensor/TelemetrySensor.h
+++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h
@@ -26,7 +26,6 @@ class TelemetrySensor
this->status = 0;
}
- const char *sensorName;
meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET;
unsigned status;
bool initialized = false;
@@ -56,13 +55,18 @@ class TelemetrySensor
return AdminMessageHandleResult::NOT_HANDLED;
}
+ const char *sensorName;
// TODO: delete after migration
bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; }
+
// Functions to sleep / wakeup sensors that support it
+ // These functions can save power consumption in cases like AQ
virtual void sleep(){};
virtual uint32_t wakeUp() { return 0; }
- // Return active by default, override per sensor
- virtual bool isActive() { return true; }
+ virtual bool isActive() { return true; } // Return true by default, override per sensor
+ virtual bool canSleep() { return false; } // Return false by default, override per sensor
+ virtual int32_t wakeUpTimeMs() { return 0; }
+ virtual int32_t pendingForReadyMs() { return 0; }
#if WIRE_INTERFACES_COUNT > 1
// Set to true if Implementation only works first I2C port (Wire)
diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp
index 6667acf5c..171911e5e 100644
--- a/src/platform/esp32/main-esp32.cpp
+++ b/src/platform/esp32/main-esp32.cpp
@@ -24,6 +24,11 @@
#include
#include
+// Weak empty variant shutdown prep function.
+// May be redefined by variant files.
+void variant_shutdown() __attribute__((weak));
+void variant_shutdown() {}
+
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
void setBluetoothEnable(bool enable)
{
@@ -249,6 +254,7 @@ void cpuDeepSleep(uint32_t msecToWake)
#endif // #end ESP32S3_WAKE_TYPE
#endif
+ variant_shutdown();
// We want RTC peripherals to stay on
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
diff --git a/src/platform/nrf52/AsyncUDP.cpp b/src/platform/nrf52/AsyncUDP.cpp
index 836fb1307..d119ffddb 100644
--- a/src/platform/nrf52/AsyncUDP.cpp
+++ b/src/platform/nrf52/AsyncUDP.cpp
@@ -36,6 +36,13 @@ bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t p
return udp.endPacket();
}
+void AsyncUDP::close()
+{
+ udp.stop();
+ localPort = 0;
+ _onPacket = nullptr;
+}
+
// AsyncUDPPacket
AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort())
{
diff --git a/src/platform/nrf52/AsyncUDP.h b/src/platform/nrf52/AsyncUDP.h
index e2b406ba9..718277309 100644
--- a/src/platform/nrf52/AsyncUDP.h
+++ b/src/platform/nrf52/AsyncUDP.h
@@ -22,6 +22,7 @@ class AsyncUDP : public Print, private concurrency::OSThread
bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64);
bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port);
+ void close();
size_t write(uint8_t b) override;
size_t write(const uint8_t *data, size_t len) override;
diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp
index 0376a1dad..c705521ad 100644
--- a/src/platform/nrf52/main-nrf52.cpp
+++ b/src/platform/nrf52/main-nrf52.cpp
@@ -46,7 +46,7 @@
uint16_t getVDDVoltage();
-// Weak empty variant initialization function.
+// Weak empty variant shutdown prep function.
// May be redefined by variant files.
void variant_shutdown() __attribute__((weak));
void variant_shutdown() {}
diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp
index a12972cb0..042bc3763 100644
--- a/src/serialization/MeshPacketSerializer.cpp
+++ b/src/serialization/MeshPacketSerializer.cpp
@@ -149,17 +149,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
}
- if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
- msgPayload["pm10_e"] =
- new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
+ if (decoded->variant.air_quality_metrics.has_co2) {
+ msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2);
}
- if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
- msgPayload["pm25_e"] =
- new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
+ if (decoded->variant.air_quality_metrics.has_co2_temperature) {
+ msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature);
}
- if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
- msgPayload["pm100_e"] =
- new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
+ if (decoded->variant.air_quality_metrics.has_co2_humidity) {
+ msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity);
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp
index 41f505b94..a0ad4e4b9 100644
--- a/src/serialization/MeshPacketSerializer_nRF52.cpp
+++ b/src/serialization/MeshPacketSerializer_nRF52.cpp
@@ -120,14 +120,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard;
}
- if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
- jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental;
+ if (decoded->variant.air_quality_metrics.has_co2) {
+ jsonObj["payload"]["co2"] = (unsigned int)decoded->variant.air_quality_metrics.co2;
}
- if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
- jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental;
+ if (decoded->variant.air_quality_metrics.has_co2_temperature) {
+ jsonObj["payload"]["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature;
}
- if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
- jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental;
+ if (decoded->variant.air_quality_metrics.has_co2_humidity) {
+ jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity;
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
diff --git a/test/TestUtil.cpp b/test/TestUtil.cpp
index b470b8ce8..a8262238f 100644
--- a/test/TestUtil.cpp
+++ b/test/TestUtil.cpp
@@ -4,6 +4,13 @@
#include "TestUtil.h"
+#if defined(ARDUINO)
+#include
+#else
+#include
+#include
+#endif
+
void initializeTestEnvironment()
{
concurrency::hasBeenSetup = true;
@@ -15,4 +22,13 @@ void initializeTestEnvironment()
perhapsSetRTC(RTCQualityNTP, &tv);
#endif
concurrency::OSThread::setup();
+}
+
+void testDelay(unsigned long ms)
+{
+#if defined(ARDUINO)
+ ::delay(ms);
+#else
+ std::this_thread::sleep_for(std::chrono::milliseconds(ms));
+#endif
}
\ No newline at end of file
diff --git a/test/TestUtil.h b/test/TestUtil.h
index ce021e459..fb634184b 100644
--- a/test/TestUtil.h
+++ b/test/TestUtil.h
@@ -1,4 +1,7 @@
#pragma once
// Initialize testing environment.
-void initializeTestEnvironment();
\ No newline at end of file
+void initializeTestEnvironment();
+
+// Portable delay for tests (Arduino or host).
+void testDelay(unsigned long ms);
\ No newline at end of file
diff --git a/test/test_atak/test_main.cpp b/test/test_atak/test_main.cpp
new file mode 100644
index 000000000..84078b300
--- /dev/null
+++ b/test/test_atak/test_main.cpp
@@ -0,0 +1,216 @@
+#include
+#include
+
+#include "TestUtil.h"
+#include "meshUtils.h"
+
+void setUp(void)
+{
+ // set stuff up here
+}
+
+void tearDown(void)
+{
+ // clean stuff up here
+}
+
+/**
+ * Test normal string without embedded nulls
+ * Should behave the same as strlen() for regular strings
+ */
+void test_normal_string(void)
+{
+ char test_str[32] = "Hello World";
+ size_t expected = 11; // strlen("Hello World")
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(expected, result);
+}
+
+/**
+ * Test empty string
+ * Should return 0 for empty string
+ */
+void test_empty_string(void)
+{
+ char test_str[32] = "";
+ size_t expected = 0;
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(expected, result);
+}
+
+/**
+ * Test string with only trailing nulls
+ * Common case - string followed by null padding
+ */
+void test_trailing_nulls(void)
+{
+ char test_str[32] = {0};
+ strcpy(test_str, "Test");
+ // test_str is now: "Test\0\0\0\0..." (4 chars + 28 nulls)
+ size_t expected = 4;
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(expected, result);
+}
+
+/**
+ * Test string with embedded null byte
+ * This is the critical bug case - strlen() would truncate at first null
+ */
+void test_embedded_null(void)
+{
+ char test_str[32] = {0};
+ // Create string "ABC\0XYZ" (embedded null after C)
+ test_str[0] = 'A';
+ test_str[1] = 'B';
+ test_str[2] = 'C';
+ test_str[3] = '\0'; // embedded null
+ test_str[4] = 'X';
+ test_str[5] = 'Y';
+ test_str[6] = 'Z';
+ // Rest is already null from initialization
+
+ // strlen would return 3, but pb_string_length should return 7
+ size_t strlen_result = strlen(test_str);
+ size_t pb_result = pb_string_length(test_str, sizeof(test_str));
+
+ TEST_ASSERT_EQUAL_size_t(3, strlen_result); // strlen stops at first null
+ TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds last non-null
+}
+
+/**
+ * Test Android UID with embedded null bytes
+ * Real-world case from bug report: ANDROID-e7e455b40002429d
+ * The "00" in the UID represents 0x00 bytes that were truncating the string
+ */
+void test_android_uid_pattern(void)
+{
+ char test_str[32] = {0};
+ // Simulate "ANDROID-e7e455b4" + 0x00 + 0x00 + "2429d"
+ const char part1[] = "ANDROID-e7e455b4";
+ strcpy(test_str, part1);
+ size_t pos = strlen(part1);
+ test_str[pos] = '\0'; // embedded null
+ test_str[pos + 1] = '\0'; // another embedded null
+ strcpy(test_str + pos + 2, "2429d");
+
+ // The full UID should be 24 characters
+ size_t strlen_result = strlen(test_str);
+ size_t pb_result = pb_string_length(test_str, sizeof(test_str));
+
+ TEST_ASSERT_EQUAL_size_t(16, strlen_result); // strlen truncates to "ANDROID-e7e455b4"
+ TEST_ASSERT_EQUAL_size_t(23, pb_result); // pb_string_length gets full length
+}
+
+/**
+ * Test string with multiple embedded nulls
+ * Edge case with several null bytes scattered through the string
+ */
+void test_multiple_embedded_nulls(void)
+{
+ char test_str[32] = {0};
+ // Create "A\0B\0C\0D" (3 embedded nulls)
+ test_str[0] = 'A';
+ test_str[1] = '\0';
+ test_str[2] = 'B';
+ test_str[3] = '\0';
+ test_str[4] = 'C';
+ test_str[5] = '\0';
+ test_str[6] = 'D';
+
+ size_t strlen_result = strlen(test_str);
+ size_t pb_result = pb_string_length(test_str, sizeof(test_str));
+
+ TEST_ASSERT_EQUAL_size_t(1, strlen_result); // strlen stops at first null
+ TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds all chars
+}
+
+/**
+ * Test buffer completely filled with non-null characters
+ * Edge case where string uses entire buffer
+ */
+void test_full_buffer(void)
+{
+ char test_str[8];
+ // Fill entire buffer with 'X'
+ memset(test_str, 'X', sizeof(test_str));
+
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(8, result);
+}
+
+/**
+ * Test buffer with all nulls
+ * Should return 0
+ */
+void test_all_nulls(void)
+{
+ char test_str[32] = {0};
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(0, result);
+}
+
+/**
+ * Test single character followed by nulls
+ * Minimal non-empty case
+ */
+void test_single_char(void)
+{
+ char test_str[32] = {0};
+ test_str[0] = 'X';
+
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(1, result);
+}
+
+/**
+ * Test callsign field typical size
+ * Test with typical ATAK callsign field size (64 bytes)
+ */
+void test_callsign_field_size(void)
+{
+ char test_str[64] = {0};
+ strcpy(test_str, "CALLSIGN-123");
+
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(12, result);
+}
+
+/**
+ * Test with data at end of buffer
+ * String with embedded null and data at very end
+ */
+void test_data_at_buffer_end(void)
+{
+ char test_str[10] = {0};
+ test_str[0] = 'A';
+ test_str[1] = '\0';
+ test_str[8] = 'Z'; // Data near end
+ test_str[9] = 'X'; // Data at end
+
+ size_t result = pb_string_length(test_str, sizeof(test_str));
+ TEST_ASSERT_EQUAL_size_t(10, result); // Should find the 'X' at position 9
+}
+
+void setup()
+{
+ // NOTE!!! Wait for >2 secs
+ // if board doesn't support software reset via Serial.DTR/RTS
+ testDelay(10);
+ testDelay(2000);
+
+ UNITY_BEGIN();
+ RUN_TEST(test_normal_string);
+ RUN_TEST(test_empty_string);
+ RUN_TEST(test_trailing_nulls);
+ RUN_TEST(test_embedded_null);
+ RUN_TEST(test_android_uid_pattern);
+ RUN_TEST(test_multiple_embedded_nulls);
+ RUN_TEST(test_full_buffer);
+ RUN_TEST(test_all_nulls);
+ RUN_TEST(test_single_char);
+ RUN_TEST(test_callsign_field_size);
+ RUN_TEST(test_data_at_buffer_end);
+ exit(UNITY_END());
+}
+
+void loop() {}
diff --git a/variants/esp32/betafpv_2400_tx_micro/platformio.ini b/variants/esp32/betafpv_2400_tx_micro/platformio.ini
index 77a1f7043..f3d488f43 100644
--- a/variants/esp32/betafpv_2400_tx_micro/platformio.ini
+++ b/variants/esp32/betafpv_2400_tx_micro/platformio.ini
@@ -16,4 +16,4 @@ upload_speed = 460800
lib_deps =
${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini
index af1535f59..7393c291f 100644
--- a/variants/esp32/m5stack_coreink/platformio.ini
+++ b/variants/esp32/m5stack_coreink/platformio.ini
@@ -19,7 +19,7 @@ build_flags =
lib_deps =
${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4
lib_ignore =
diff --git a/variants/esp32c3/heltec_hru_3601/platformio.ini b/variants/esp32c3/heltec_hru_3601/platformio.ini
index 8200b6e87..7cf60fc08 100644
--- a/variants/esp32c3/heltec_hru_3601/platformio.ini
+++ b/variants/esp32c3/heltec_hru_3601/platformio.ini
@@ -7,4 +7,4 @@ build_flags =
-I variants/esp32c3/heltec_hru_3601
lib_deps = ${esp32c3_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini
index ed26598d2..b14fc83f2 100644
--- a/variants/esp32c6/m5stack_unitc6l/platformio.ini
+++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini
@@ -24,7 +24,7 @@ build_unflags =
lib_deps =
${esp32c6_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
h2zero/NimBLE-Arduino@2.3.7
build_flags =
diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini
index ee51018d4..92d4bd519 100644
--- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini
+++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini
@@ -34,3 +34,5 @@ lib_deps = ${esp32s3_base.lib_deps}
https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip
# renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino
maxpromer/PCA9557-arduino@1.0.0
+ # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
+ lewisxhe/SensorLib@0.3.4
\ No newline at end of file
diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp
index 82b218cf9..917341560 100644
--- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp
+++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp
@@ -1,11 +1,11 @@
#include "variant.h"
#include
-PCA9557 io(0x18, &Wire);
+PCA9557 io(0x18, &Wire1);
void earlyInitVariant()
{
- Wire.begin(48, 47);
+ Wire1.begin(48, 47);
io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
io.pinMode(PCA_LED_POWER, OUTPUT);
@@ -16,3 +16,8 @@ void earlyInitVariant()
io.digitalWrite(PCA_LED_USER, LOW);
io.digitalWrite(PCA_LED_ENABLE, LOW);
}
+
+void variant_shutdown()
+{
+ io.digitalWrite(PCA_PIN_POWER_EN, LOW);
+}
diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h
index 4f6e415a2..223f60264 100644
--- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h
+++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h
@@ -32,6 +32,9 @@
#define I2C_SCL 1
#define I2C_SDA 2
+// PCF8563 RTC Module
+#define PCF8563_RTC 0x51
+
// GPS pins
#define GPS_SWITH 10
#define HAS_GPS 1
diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini
index f44f26006..69f3a4af8 100644
--- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini
+++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini
@@ -11,9 +11,9 @@ upload_speed = 921600
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
build_unflags =
${esp32s3_base.build_unflags}
-DARDUINO_USB_MODE=1
diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini
index 4f759d1e6..529d9182e 100644
--- a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini
+++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini
@@ -11,7 +11,7 @@ upload_speed = 921600
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
build_unflags =
${esp32s3_base.build_unflags}
-DARDUINO_USB_MODE=1
diff --git a/variants/esp32s3/esp32-s3-pico/platformio.ini b/variants/esp32s3/esp32-s3-pico/platformio.ini
index db0c038e6..f24c84aa7 100644
--- a/variants/esp32s3/esp32-s3-pico/platformio.ini
+++ b/variants/esp32s3/esp32-s3-pico/platformio.ini
@@ -23,6 +23,6 @@ build_flags = ${esp32s3_base.build_flags}
lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
diff --git a/variants/esp32s3/heltec_sensor_hub/platformio.ini b/variants/esp32s3/heltec_sensor_hub/platformio.ini
index 3f93c7ad2..3d8fe932d 100644
--- a/variants/esp32s3/heltec_sensor_hub/platformio.ini
+++ b/variants/esp32s3/heltec_sensor_hub/platformio.ini
@@ -11,4 +11,4 @@ build_flags =
lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini
index 5ba82d045..29960f280 100644
--- a/variants/esp32s3/t-deck-pro/platformio.ini
+++ b/variants/esp32s3/t-deck-pro/platformio.ini
@@ -33,7 +33,7 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
# renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main
https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip
# renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328
diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini
index 924dfa74f..045454e8e 100644
--- a/variants/esp32s3/unphone/platformio.ini
+++ b/variants/esp32s3/unphone/platformio.ini
@@ -41,7 +41,7 @@ lib_deps = ${esp32s3_base.lib_deps}
# TODO renovate
https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
- adafruit/Adafruit NeoPixel@1.15.2
+ adafruit/Adafruit NeoPixel@1.15.3
[env:unphone-tft]
board_level = extra
diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini
index 093c3732d..e202715b0 100644
--- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini
+++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini
@@ -12,5 +12,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_
lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
debug_tool = jlink
diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini
index 7a87c2a21..b6956c0f1 100644
--- a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini
+++ b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini
@@ -24,5 +24,5 @@ lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM
khoih-prog/nRF52_PWM@1.0.1
- ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
+ # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4
diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h
index 7766a171e..fa127ae3e 100644
--- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h
+++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h
@@ -115,7 +115,6 @@ extern "C" {
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
-#define HAS_RTC 1
#ifdef __cplusplus
}
diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini
index 329111ce6..fa09a4463 100644
--- a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini
+++ b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini
@@ -21,5 +21,5 @@ build_flags = ${nrf52840_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6>
lib_deps =
${nrf52840_base.lib_deps}
- ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
+ # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4
diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h
index bdc52a530..2ebb79031 100644
--- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h
+++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h
@@ -121,7 +121,6 @@ static const uint8_t A0 = PIN_A0;
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
-#define HAS_RTC 1
// SPI
#define SPI_INTERFACES_COUNT 1
diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini
index 5951a37a3..afc8e4b2a 100644
--- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini
+++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini
@@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS0
lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
upload_protocol = nrfutil
;upload_port = /dev/ttyACM1
diff --git a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini
index ecb630bb7..3410859b1 100644
--- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini
+++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini
@@ -15,6 +15,6 @@ lib_deps =
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
debug_tool = jlink
;upload_port = /dev/ttyACM4
\ No newline at end of file
diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini
index d93f179c2..5e5b4b665 100644
--- a/variants/nrf52840/TWC_mesh_v4/platformio.ini
+++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini
@@ -9,5 +9,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mes
lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
debug_tool = jlink
diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini
index f0da832cb..ba48cf2a2 100644
--- a/variants/nrf52840/rak4631_epaper/platformio.ini
+++ b/variants/nrf52840/rak4631_epaper/platformio.ini
@@ -15,7 +15,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631
lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
# renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028
melopero/Melopero RV3028@1.2.0
# renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library
diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini
index 112ddfc29..714737c9d 100644
--- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini
+++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini
@@ -17,7 +17,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631
lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
- zinggjm/GxEPD2@1.6.6
+ zinggjm/GxEPD2@1.6.7
# renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028
melopero/Melopero RV3028@1.2.0
# renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library
diff --git a/variants/nrf52840/t-echo-plus/variant.h b/variants/nrf52840/t-echo-plus/variant.h
index 4038ce6ef..7ebdf48c0 100644
--- a/variants/nrf52840/t-echo-plus/variant.h
+++ b/variants/nrf52840/t-echo-plus/variant.h
@@ -132,9 +132,6 @@ static const uint8_t A0 = PIN_A0;
#define HAS_DRV2605 1
-// Battery / ADC already defined above
-#define HAS_RTC 1
-
#define SERIAL_PRINT_PORT 0
#ifdef __cplusplus
diff --git a/variants/stm32/wio-e5/platformio.ini b/variants/stm32/wio-e5/platformio.ini
index 311cade58..c8dbb2b72 100644
--- a/variants/stm32/wio-e5/platformio.ini
+++ b/variants/stm32/wio-e5/platformio.ini
@@ -17,5 +17,6 @@ build_flags =
-DPIN_SERIAL2_RX=PA3
-DHAS_GPS=1
-DGPS_SERIAL_PORT=Serial2
+ -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
upload_port = stlink