Compare commits

..

8 Commits

Author SHA1 Message Date
Thomas Göttgens
e65ce4d21c ThinkNode G3, ETH support WIP 2026-02-04 14:35:17 +01:00
Mattatat25
538a5f0dfc Add reply bot module with DM-only responses and rate limiting (#9456)
* Implement Meshtastic reply bot module with ping and status features

Adds a reply bot module that listens for /ping, /hello, and /test commands received via direct messages or broadcasts on the primary channel. The module always replies via direct message to the sender only, reporting hop count, RSSI, and SNR. Per-sender cooldowns are enforced to reduce network spam, and the module can be excluded at build time via a compile flag. Updates include the new module source files and required build configuration changes.

* Update ReplyBotModule.cpp

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

* Update src/modules/ReplyBotModule.h

Match the existing MESHTASTIC_EXCLUDE_* guard pattern so the module is excluded by default.

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

* Update src/modules/ReplyBotModule.cpp

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

* Tidying up

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-02-03 20:45:02 -06:00
Vortetty
b7db22055d Inkhud battery icon improvements. (#9513)
* Inkhud battery icon improvements.
Fixes the battery icon draining from the flat side towards the bump, which is backwards from general design language seen on most devices
By request of kr0n05_ on discord, adds the ability to mirror the battery icon which fixes that issue in another way, and is also a common design seen on other devices.

* Remove option for icon mirroring

* Add border + dither to battery to prevent font overlap

* Fix trunk format

* Code cleanup, courtesy of Xaositek.
2026-02-03 20:02:54 -05:00
Eric Sesterhenn
0703e0e6d7 Make sure we always return a value in NodeDB::restorePreferences() (#9516)
In case FScom is not defined there is no return statement. This
moves the return outside of the ifdef to make sure a defined
value is returned.
2026-02-03 06:22:33 -06:00
Jonathan Bennett
f514bc230b Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511) 2026-02-03 00:13:49 -06:00
Jason P
0022148323 Missed in reviews - fixing send bubble (#9505) 2026-02-01 19:10:00 -06:00
Jonathan Bennett
9d06c1bf34 Add StatusMessage module and config overrides (#9351)
* Add StatusMessage module and config overrides

* Trunk

* Don't reboot node simply for a StatusMessage config update
2026-01-31 14:55:51 -06:00
Jonathan Bennett
1d30342c00 Don't ever define PIN_LED or BLE_LED_INVERTED (#9494)
* Don't ever define PIN_LED

* Deprecate BLE_LED_INVERTED
2026-01-31 12:15:06 -06:00
90 changed files with 989 additions and 2217 deletions

View File

@@ -8,18 +8,18 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.500
- renovate@43.4.0
- prettier@3.8.1
- trufflehog@3.93.0
- checkov@3.2.497
- renovate@42.84.2
- prettier@3.8.0
- trufflehog@3.92.5
- yamllint@1.38.0
- bandit@1.9.3
- trivy@0.69.1
- trivy@0.68.2
- taplo@0.10.0
- ruff@0.15.0
- ruff@0.14.13
- isort@7.0.0
- markdownlint@0.47.0
- oxipng@10.1.0
- oxipng@10.0.0
- svgo@4.0.0
- actionlint@1.7.10
- flake8@7.3.0

View File

@@ -156,8 +156,16 @@ IF %BPS_RESET% EQU 1 (
SET "PROGNAME=!FILENAME:.factory.bin=!"
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
@REM Determine OTA filename based on MCU type (unified OTA format)
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
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"
)
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix.

View File

@@ -131,8 +131,14 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
exit 1
fi
# Determine OTA filename based on MCU type (unified OTA format)
OTAFILE="mt-${MCU}-ota.bin"
# 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
# Set SPIFFS filename with "littlefs-" prefix.
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"

View File

@@ -50,6 +50,7 @@ build_flags = -Wno-missing-field-initializers
-DRADIOLIB_EXCLUDE_APRS=1
-DRADIOLIB_EXCLUDE_LORAWAN=1
-DMESHTASTIC_EXCLUDE_DROPZONE=1
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
@@ -66,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/21e484f409cde18d44012caef84c244eb5ca28f3.zip
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.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
@@ -120,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/6c75195e9987b7a49563232234f2f868dd343cae.zip
https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -144,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.8
adafruit/Adafruit MPU6050@2.2.6
# 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
@@ -213,7 +214,6 @@ 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 +240,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
sensirion/Sensirion I2C SCD4x@1.1.0

View File

@@ -89,22 +89,14 @@ class BluetoothStatus : public Status
case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED");
#ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, LOW);
#else
digitalWrite(BLE_LED, HIGH);
#endif
digitalWrite(BLE_LED, LED_STATE_ON);
#endif
break;
case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED");
#ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
break;
}

View File

@@ -13,6 +13,11 @@ extern MemGet memGet;
#define LED_STATE_ON 1
#endif
// WIFI LED
#ifndef WIFI_STATE_ON
#define WIFI_STATE_ON 1
#endif
// -----------------------------------------------------------------------------
// DEBUG
// -----------------------------------------------------------------------------
@@ -147,7 +152,9 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
// Default Bluetooth PIN
#define defaultBLEPin 123456
#if HAS_ETHERNET && !defined(USE_WS5500)
#if HAS_ETHERNET && defined(USE_CH390D)
#include <ESP32_CH390.h>
#elif HAS_ETHERNET && !defined(USE_WS5500)
#include <RAK13800_W5100S.h>
#endif // HAS_ETHERNET

View File

@@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
}
// if it's not HIGH - check the battery
#endif
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
return false;
// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if

View File

@@ -241,7 +241,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
#define SEN5X_ADDR 0x69
// -----------------------------------------------------------------------------
// ACCELEROMETER

View File

@@ -88,8 +88,7 @@ class ScanI2C
BH1750,
DA217,
CHSC6X,
CST226SE,
SEN5X
CST226SE
} DeviceType;
// typedef uint8_t DeviceAddress;

View File

@@ -8,7 +8,6 @@
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
#include "meshUtils.h" // vformat
#endif
bool in_array(uint8_t *array, int size, uint8_t lookfor)
@@ -115,45 +114,6 @@ 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__); \
@@ -608,9 +568,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
case ICM20948_ADDR: // same as BMX160_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
// ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
@@ -621,31 +580,14 @@ 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 {
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;
}
type = MPU6050;
logFoundDevice("MPU6050", (uint8_t)addr.address);
break;
}
break;

View File

@@ -1,31 +0,0 @@
#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;
}

View File

@@ -1,11 +1,41 @@
#ifndef RECLOCK_I2C_
#define RECLOCK_I2C_
#ifdef CAN_RECLOCK_I2C
#include "ScanI2CTwoWire.h"
#include <Wire.h>
#include <stdint.h>
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
{
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

View File

@@ -72,13 +72,11 @@ 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);
@@ -242,12 +240,10 @@ 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);

View File

@@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
} // namespace
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
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, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 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 == 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;
} 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;
}
};
screen->showOverlayBanner(bannerOptions);
@@ -102,7 +102,7 @@ void menuHandler::OnboardMessage()
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
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::LoraMenu;
menuHandler::menuQueue = menuHandler::lora_Menu;
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::LoraMenu;
menuHandler::menuQueue = menuHandler::lora_Menu;
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::LoraMenu;
menuHandler::menuQueue = menuHandler::lora_Menu;
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::ClockMenu;
menuHandler::menuQueue = menuHandler::clock_menu;
screen->runNow();
} else if (selected == twelve) {
config.display.use_12h_clock = true;
@@ -390,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::function<void
screen->showOverlayBanner(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::ClockMenu;
menuHandler::menuQueue = menuHandler::clock_menu;
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::ClockMenu;
menuHandler::menuQueue = menuHandler::clock_menu;
screen->runNow();
return;
}
@@ -503,13 +503,13 @@ void menuHandler::clockMenu()
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Clock) {
menuHandler::menuQueue = menuHandler::ClockFacePicker;
menuHandler::menuQueue = menuHandler::clock_face_picker;
screen->runNow();
} else if (selected == Time) {
menuHandler::menuQueue = menuHandler::TwelveHourPicker;
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
screen->runNow();
} else if (selected == Timezone) {
menuHandler::menuQueue = menuHandler::TzPicker;
menuHandler::menuQueue = menuHandler::TZ_picker;
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::MessageViewModeMenu;
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
screen->runNow();
// Reply submenu
} else if (selected == ReplyMenu) {
menuHandler::menuQueue = menuHandler::ReplyMenu;
menuHandler::menuQueue = menuHandler::reply_menu;
screen->runNow();
} else if (selected == MuteChannel) {
@@ -589,7 +589,7 @@ void menuHandler::messageResponseMenu()
}
} else if (selected == DeleteMenu) {
menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
menuHandler::menuQueue = menuHandler::delete_messages_menu;
screen->runNow();
#ifdef HAS_I2S
@@ -649,7 +649,7 @@ void menuHandler::replyMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
return;
}
@@ -737,7 +737,7 @@ void menuHandler::deleteMessagesMenu()
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
menuHandler::menuQueue = menuHandler::message_response_menu;
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::MessageResponseMenu;
menuHandler::menuQueue = menuHandler::message_response_menu;
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::ScreenOptionsMenu;
menuHandler::menuQueue = menuHandler::screen_options_menu;
screen->runNow();
} else if (selected == PowerMenu) {
menuHandler::menuQueue = menuHandler::PowerMenu;
menuHandler::menuQueue = menuHandler::power_menu;
screen->runNow();
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::TestMenu;
menuHandler::menuQueue = menuHandler::test_menu;
screen->runNow();
} else if (selected == Bluetooth) {
menuQueue = BluetoothToggleMenu;
menuQueue = bluetooth_toggle_menu;
screen->runNow();
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
} else if (selected == WiFiToggle) {
menuQueue = WifiToggleMenu;
menuQueue = wifi_toggle_menu;
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::RemoveFavorite;
menuHandler::menuQueue = menuHandler::remove_favorite;
screen->runNow();
} else if (selected == TraceRoute) {
if (traceRouteModule) {
@@ -1238,15 +1238,15 @@ void menuHandler::positionBaseMenu()
auto action = static_cast<PositionAction>(option.value);
switch (action) {
case PositionAction::GpsToggle:
menuQueue = GpsToggleMenu;
menuQueue = gps_toggle_menu;
screen->runNow();
break;
case PositionAction::GpsFormat:
menuQueue = GpsFormatMenu;
menuQueue = gps_format_menu;
screen->runNow();
break;
case PositionAction::CompassMenu:
menuQueue = CompassPointNorthMenu;
menuQueue = compass_point_north_menu;
screen->runNow();
break;
case PositionAction::CompassCalibrate:
@@ -1255,15 +1255,15 @@ void menuHandler::positionBaseMenu()
}
break;
case PositionAction::GPSSmartPosition:
menuQueue = GpsSmartPositionMenu;
menuQueue = gps_smart_position_menu;
screen->runNow();
break;
case PositionAction::GPSUpdateInterval:
menuQueue = GpsUpdateIntervalMenu;
menuQueue = gps_update_interval_menu;
screen->runNow();
break;
case PositionAction::GPSPositionBroadcast:
menuQueue = GpsPositionBroadcastMenu;
menuQueue = gps_position_broadcast_menu;
screen->runNow();
break;
}
@@ -1303,13 +1303,13 @@ void menuHandler::nodeListMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NodePicker) {
menuQueue = NodePickerMenu;
menuQueue = NodePicker_menu;
screen->runNow();
} else if (selected == Reset) {
menuQueue = ResetNodeDbMenu;
menuQueue = reset_node_db_menu;
screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
menuHandler::menuQueue = menuHandler::node_name_length_menu;
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 = ManageNodeMenu;
menuQueue = Manage_Node_menu;
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 = NodeBaseMenu;
menuQueue = node_base_menu;
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 = NodeBaseMenu;
menuQueue = node_base_menu;
screen->runNow();
return;
}
@@ -1498,7 +1498,6 @@ 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");
});
@@ -1529,7 +1528,7 @@ void menuHandler::resetNodeDBMenu()
nodeDB->resetNodes(1);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == 0) {
menuQueue = NodeBaseMenu;
menuQueue = node_base_menu;
screen->runNow();
}
};
@@ -1551,7 +1550,7 @@ void menuHandler::compassNorthMenu()
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
[](const CompassOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = PositionBaseMenu;
menuQueue = position_base_menu;
screen->runNow();
return;
}
@@ -1596,7 +1595,7 @@ void menuHandler::GPSToggleMenu()
auto bannerOptions =
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = PositionBaseMenu;
menuQueue = position_base_menu;
screen->runNow();
return;
}
@@ -1661,7 +1660,7 @@ void menuHandler::GPSFormatMenu()
auto onSelection = [](const GPSFormatOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = PositionBaseMenu;
menuQueue = position_base_menu;
screen->runNow();
return;
}
@@ -1716,7 +1715,7 @@ void menuHandler::GPSSmartPositionMenu()
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = PositionBaseMenu;
menuQueue = position_base_menu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_smart_enabled = true;
@@ -1745,7 +1744,7 @@ void menuHandler::GPSUpdateIntervalMenu()
bannerOptions.optionsCount = 16;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = PositionBaseMenu;
menuQueue = position_base_menu;
screen->runNow();
} else if (selected == 1) {
config.position.gps_update_interval = 8;
@@ -1833,7 +1832,7 @@ void menuHandler::GPSPositionBroadcastMenu()
bannerOptions.optionsCount = 17;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
menuQueue = PositionBaseMenu;
menuQueue = position_base_menu;
screen->runNow();
} else if (selected == 1) {
config.position.position_broadcast_secs = 60;
@@ -1916,7 +1915,7 @@ void menuHandler::GPSPositionBroadcastMenu()
#endif
void menuHandler::bluetoothToggleMenu()
void menuHandler::BluetoothToggleMenu()
{
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
BannerOverlayOptions bannerOptions;
@@ -2044,7 +2043,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 = SystemBaseMenu;
menuQueue = system_base_menu;
screen->runNow();
return;
}
@@ -2139,7 +2138,7 @@ void menuHandler::rebootMenu()
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else {
menuQueue = PowerMenu;
menuQueue = power_menu;
screen->runNow();
}
};
@@ -2161,7 +2160,7 @@ void menuHandler::shutdownMenu()
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else {
menuQueue = PowerMenu;
menuQueue = power_menu;
screen->runNow();
}
};
@@ -2222,14 +2221,14 @@ void menuHandler::testMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NumberPicker) {
menuQueue = NumberTest;
menuQueue = number_test;
screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else {
menuQueue = SystemBaseMenu;
menuQueue = system_base_menu;
screen->runNow();
}
};
@@ -2253,7 +2252,7 @@ void menuHandler::wifiBaseMenu()
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Wifi_toggle) {
menuQueue = WifiToggleMenu;
menuQueue = wifi_toggle_menu;
screen->runNow();
}
};
@@ -2302,9 +2301,9 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
static const char *optionsArray[6] = {"Back"};
static int optionsEnumArray[6] = {Back};
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
int options = 1;
// Only show brightness for B&W displays
@@ -2326,9 +2325,6 @@ 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;
@@ -2336,10 +2332,10 @@ void menuHandler::screenOptionsMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Brightness) {
menuHandler::menuQueue = menuHandler::BrightnessPicker;
menuHandler::menuQueue = menuHandler::brightness_picker;
screen->runNow();
} else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
@@ -2347,11 +2343,8 @@ 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 = SystemBaseMenu;
menuQueue = system_base_menu;
screen->runNow();
}
};
@@ -2387,16 +2380,16 @@ void menuHandler::powerMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Reboot) {
menuHandler::menuQueue = menuHandler::RebootMenu;
menuHandler::menuQueue = menuHandler::reboot_menu;
screen->runNow();
} else if (selected == Shutdown) {
menuHandler::menuQueue = menuHandler::ShutdownMenu;
menuHandler::menuQueue = menuHandler::shutdown_menu;
screen->runNow();
} else if (selected == MUI) {
menuHandler::menuQueue = menuHandler::MuiPicker;
menuHandler::menuQueue = menuHandler::mui_picker;
screen->runNow();
} else {
menuQueue = SystemBaseMenu;
menuQueue = system_base_menu;
screen->runNow();
}
};
@@ -2434,7 +2427,7 @@ void menuHandler::keyVerificationFinalPrompt()
}
}
void menuHandler::frameTogglesMenu()
void menuHandler::FrameToggles_menu()
{
enum optionsNumbers {
Finish,
@@ -2578,7 +2571,7 @@ void menuHandler::frameTogglesMenu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::displayUnitsMenu()
void menuHandler::DisplayUnits_menu()
{
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
@@ -2599,34 +2592,7 @@ void menuHandler::displayUnitsMenu()
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
service->reloadConfig(SEGMENT_CONFIG);
} else {
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;
menuHandler::menuQueue = menuHandler::screen_options_menu;
screen->runNow();
}
};
@@ -2635,156 +2601,153 @@ void menuHandler::messageBubblesMenu()
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != MenuNone)
if (menuQueue != menu_none)
test_count = 0;
switch (menuQueue) {
case MenuNone:
case menu_none:
break;
case LoraMenu:
case lora_Menu:
loraMenu();
break;
case LoraPicker:
case lora_picker:
LoraRegionPicker();
break;
case DeviceRolePicker:
deviceRolePicker();
case device_role_picker:
DeviceRolePicker();
break;
case RadioPresetPicker:
radioPresetPicker();
case radio_preset_picker:
RadioPresetPicker();
break;
case FrequencySlot:
case frequency_slot:
FrequencySlotPicker();
break;
case NoTimeoutLoraPicker:
case no_timeout_lora_picker:
LoraRegionPicker(0);
break;
case TzPicker:
case TZ_picker:
TZPicker();
break;
case TwelveHourPicker:
twelveHourPicker();
case twelve_hour_picker:
TwelveHourPicker();
break;
case ClockFacePicker:
clockFacePicker();
case clock_face_picker:
ClockFacePicker();
break;
case ClockMenu:
case clock_menu:
clockMenu();
break;
case SystemBaseMenu:
case system_base_menu:
systemBaseMenu();
break;
case PositionBaseMenu:
case position_base_menu:
positionBaseMenu();
break;
case NodeBaseMenu:
case node_base_menu:
nodeListMenu();
break;
#if !MESHTASTIC_EXCLUDE_GPS
case GpsToggleMenu:
case gps_toggle_menu:
GPSToggleMenu();
break;
case GpsFormatMenu:
case gps_format_menu:
GPSFormatMenu();
break;
case GpsSmartPositionMenu:
case gps_smart_position_menu:
GPSSmartPositionMenu();
break;
case GpsUpdateIntervalMenu:
case gps_update_interval_menu:
GPSUpdateIntervalMenu();
break;
case GpsPositionBroadcastMenu:
case gps_position_broadcast_menu:
GPSPositionBroadcastMenu();
break;
#endif
case CompassPointNorthMenu:
case compass_point_north_menu:
compassNorthMenu();
break;
case ResetNodeDbMenu:
case reset_node_db_menu:
resetNodeDBMenu();
break;
case BuzzerModeMenuPicker:
case buzzermodemenupicker:
BuzzerModeMenu();
break;
case MuiPicker:
case mui_picker:
switchToMUIMenu();
break;
case TftColorMenuPicker:
case tftcolormenupicker:
TFTColorPickerMenu(display);
break;
case BrightnessPicker:
case brightness_picker:
BrightnessPickerMenu();
break;
case NodeNameLengthMenu:
case node_name_length_menu:
nodeNameLengthMenu();
break;
case RebootMenu:
case reboot_menu:
rebootMenu();
break;
case ShutdownMenu:
case shutdown_menu:
shutdownMenu();
break;
case NodePickerMenu:
case NodePicker_menu:
NodePicker();
break;
case ManageNodeMenu:
manageNodeMenu();
case Manage_Node_menu:
ManageNodeMenu();
break;
case RemoveFavorite:
case remove_favorite:
removeFavoriteMenu();
break;
case TraceRouteMenu:
case trace_route_menu:
traceRouteMenu();
break;
case TestMenu:
case test_menu:
testMenu();
break;
case NumberTest:
case number_test:
numberTest();
break;
case WifiToggleMenu:
case wifi_toggle_menu:
wifiToggleMenu();
break;
case KeyVerificationInit:
case key_verification_init:
keyVerificationInitMenu();
break;
case KeyVerificationFinalPrompt:
case key_verification_final_prompt:
keyVerificationFinalPrompt();
break;
case BluetoothToggleMenu:
bluetoothToggleMenu();
case bluetooth_toggle_menu:
BluetoothToggleMenu();
break;
case ScreenOptionsMenu:
case screen_options_menu:
screenOptionsMenu();
break;
case PowerMenu:
case power_menu:
powerMenu();
break;
case FrameToggles:
frameTogglesMenu();
FrameToggles_menu();
break;
case DisplayUnits:
displayUnitsMenu();
DisplayUnits_menu();
break;
case ThrottleMessage:
case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
case MessageResponseMenu:
case message_response_menu:
messageResponseMenu();
break;
case ReplyMenu:
case reply_menu:
replyMenu();
break;
case DeleteMessagesMenu:
case delete_messages_menu:
deleteMessagesMenu();
break;
case MessageViewModeMenu:
case message_viewmode_menu:
messageViewModeMenu();
break;
case MessageBubblesMenu:
messageBubblesMenu();
break;
}
menuQueue = MenuNone;
menuQueue = menu_none;
}
void menuHandler::saveUIConfig()

View File

@@ -8,54 +8,53 @@ class menuHandler
{
public:
enum screenMenus {
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,
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,
FrameToggles,
DisplayUnits,
MessageBubblesMenu
DisplayUnits
};
static screenMenus menuQueue;
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
@@ -63,15 +62,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<void()> 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();
@@ -96,7 +95,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();
@@ -107,16 +106,15 @@ class menuHandler
static void screenOptionsMenu();
static void powerMenu();
static void nodeNameLengthMenu();
static void frameTogglesMenu();
static void displayUnitsMenu();
static void messageBubblesMenu();
static void FrameToggles_menu();
static void DisplayUnits_menu();
static void textMessageMenu();
private:
static void saveUIConfig();
static void keyVerificationInitMenu();
static void keyVerificationFinalPrompt();
static void bluetoothToggleMenu();
static void BluetoothToggleMenu();
};
/* Generic Menu Options designations */

View File

@@ -527,12 +527,8 @@ 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 - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
// Title string depending on mode
@@ -800,105 +796,114 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
}
}
// 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;
// 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;
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;
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;
}
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 < contentTop || topY > contentBottom - 1)
continue;
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 {
// 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;
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
}
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
int bottomY = visualBottom + BUBBLE_PAD_Y;
if (w > maxLineW)
maxLineW = w;
}
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;
}
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 (bottomY <= topY + 2)
continue;
if (bubbleW > 1 && bubbleH > 1) {
int x1 = bubbleX + bubbleW - 1;
int y1 = topY + bubbleH - 1;
if (bottomY < contentTop || topY > contentBottom - 1)
continue;
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);
}
if (w > maxLineW)
maxLineW = w;
}
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
int bubbleH = (bottomY - topY) + 1;
int bubbleX = 0;
if (b.mine) {
bubbleX = rightEdge - bubbleW;
// 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);
} else {
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);
// 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);
}
}
} // end if (showBubbles)
}
// Render visible lines
int lineY = yOffset;
@@ -911,11 +916,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 - (showBubbles ? textIndent : 0);
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT;
if (headerX < LEFT_MARGIN)
headerX = LEFT_MARGIN;
} else {
headerX = x + textIndent;
headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT;
}
display->drawString(headerX, lineY, cachedLines[i].c_str());
@@ -955,13 +960,14 @@ 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 - (showBubbles ? textIndent : 0);
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT;
if (rightX < LEFT_MARGIN)
rightX = LEFT_MARGIN;
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else {
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes,
numEmotes);
}
}
}

View File

@@ -1,3 +1,5 @@
#include "graphics/niche/InkHUD/Tile.h"
#include <cstdint>
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./Applet.h"
@@ -785,6 +787,16 @@ void InkHUD::Applet::drawHeader(std::string text)
drawPixel(x, 0, BLACK);
drawPixel(x, headerDivY, BLACK); // Dotted 50%
}
// Dither near battery
if (settings->optionalFeatures.batteryIcon) {
constexpr uint16_t ditherSizePx = 4;
Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile();
const uint16_t batteryTileLeft = batteryTile->getLeft();
const uint16_t batteryTileTop = batteryTile->getTop();
const uint16_t batteryTileHeight = batteryTile->getHeight();
hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE);
}
}
// Get the height of the standard applet header

View File

@@ -48,37 +48,27 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
void InkHUD::BatteryIconApplet::onRender(bool full)
{
// Fill entire tile
// - size of icon controlled by size of tile
int16_t l = 0;
int16_t t = 0;
uint16_t w = width();
int16_t h = height();
// Clear the region beneath the tile
// Clear the region beneath the tile, including the border
// Most applets are drawing onto an empty frame buffer and don't need to do this
// We do need to do this with the battery though, as it is an "overlay"
fillRect(l, t, w, h, WHITE);
// Vertical centerline
const int16_t m = t + (h / 2);
fillRect(0, 0, width(), height(), WHITE);
// =====================
// Draw battery outline
// =====================
// Positive terminal "bump"
const int16_t &bumpL = l;
const uint16_t bumpH = h / 2;
const int16_t bumpT = m - (bumpH / 2);
constexpr uint16_t bumpW = 2;
const int16_t &bumpL = 1;
const uint16_t bumpH = (height() - 2) / 2;
const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2);
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
// Main body of battery
const int16_t bodyL = bumpL + bumpW;
const int16_t &bodyT = t;
const int16_t &bodyH = h;
const int16_t bodyW = w - bumpW;
const int16_t bodyL = 1 + bumpW;
const int16_t &bodyT = 1;
const int16_t &bodyH = height() - 2; // Handle top/bottom padding
const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
// Erase join between bump and body
@@ -89,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
// ===================
constexpr int16_t slicePad = 2;
const int16_t sliceL = bodyL + slicePad;
int16_t sliceL = bodyL + slicePad;
const int16_t sliceT = bodyT + slicePad;
const uint16_t sliceH = bodyH - (slicePad * 2);
uint16_t sliceW = bodyW - (slicePad * 2);
sliceW = (sliceW * socRounded) / 100; // Apply percentage
sliceW = (sliceW * socRounded) / 100; // Apply percentage
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);

View File

@@ -510,10 +510,10 @@ void InkHUD::WindowManager::placeSystemTiles()
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
inkhud->getSystemApplet("BatteryIcon")
->getTile()
->setRegion(inkhud->width() - batteryIconWidth, // x
2, // y
batteryIconWidth, // width
batteryIconHeight); // height
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
1, // y
batteryIconWidth + 1, // width
batteryIconHeight + 2); // height
// Note: the tiles of placeholder and menu applets are manipulated specially
// - menuApplet borrows user tiles

View File

@@ -361,16 +361,12 @@ void setup()
#ifdef WIFI_LED
pinMode(WIFI_LED, OUTPUT);
digitalWrite(WIFI_LED, LOW);
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
#endif
#ifdef BLE_LED
pinMode(BLE_LED, OUTPUT);
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
concurrency::hasBeenSetup = true;
@@ -493,7 +489,9 @@ void setup()
// The ThinkNodes have their own blink logic
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
#else
ledPeriodic = new Periodic("Blink", ledBlinker);
#endif
fsInit();
@@ -713,6 +711,7 @@ 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
@@ -833,7 +832,7 @@ void setup()
SPI.begin();
#endif
#else
// ESP32
// ESP32
#if defined(HW_SPI1_DEVICE)
SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
@@ -928,13 +927,6 @@ 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();
@@ -1025,6 +1017,12 @@ 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();

View File

@@ -25,7 +25,7 @@ template class LR11x0Interface<LR1121>;
template class SX126xInterface<STM32WLx>;
#endif
#if HAS_ETHERNET && !defined(USE_WS5500)
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#include "api/ethServerAPI.h"
template class ServerAPI<EthernetClient>;
template class APIServerPort<ethServerAPI, EthernetServer>;

View File

@@ -574,10 +574,6 @@ 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,
@@ -1408,6 +1404,15 @@ void NodeDB::loadFromDisk()
if (portduino_config.has_configDisplayMode) {
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
}
if (portduino_config.has_statusMessage) {
moduleConfig.has_statusmessage = true;
strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(),
sizeof(moduleConfig.statusmessage.node_status));
moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0';
}
if (portduino_config.enable_UDP) {
config.network.enabled_protocols = true;
}
#endif
}
@@ -1548,6 +1553,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
moduleConfig.has_ambient_lighting = true;
moduleConfig.has_audio = true;
moduleConfig.has_paxcounter = true;
moduleConfig.has_statusmessage = true;
success &=
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);

View File

@@ -90,9 +90,9 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
bool seenRecently = (found != NULL); // If found -> the packet was seen recently
// Check for hop_limit upgrade scenario
if (seenRecently && wasUpgraded && getHighestHopLimit(*found) < p->hop_limit) {
LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id,
getHighestHopLimit(*found), p->hop_limit);
if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) {
LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit,
p->hop_limit);
*wasUpgraded = true;
} else if (wasUpgraded) {
*wasUpgraded = false; // Initialize to false if not an upgrade

View File

@@ -1,7 +1,7 @@
#include "configuration.h"
#include <Arduino.h>
#if HAS_ETHERNET && !defined(USE_WS5500)
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#include "ethServerAPI.h"

View File

@@ -1,7 +1,7 @@
#pragma once
#include "ServerAPI.h"
#ifndef USE_WS5500
#if !defined(USE_WS5500) && !defined(USE_CH390D)
#include <RAK13800_W5100S.h>
/**

View File

@@ -505,8 +505,6 @@ 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 */
@@ -734,7 +732,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, 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_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}
@@ -745,7 +743,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, 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_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}
@@ -813,7 +811,6 @@ 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
@@ -960,8 +957,7 @@ 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, enable_message_bubbles, 14)
X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13)
#define meshtastic_Config_DisplayConfig_CALLBACK NULL
#define meshtastic_Config_DisplayConfig_DEFAULT NULL
@@ -1039,7 +1035,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 36
#define meshtastic_Config_DisplayConfig_size 34
#define meshtastic_Config_LoRaConfig_size 85
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
#define meshtastic_Config_NetworkConfig_size 204

View File

@@ -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 2364
#define meshtastic_BackupPreferences_size 2362
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196

View File

@@ -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 751
#define meshtastic_LocalConfig_size 749
#define meshtastic_LocalModuleConfig_size 758
#ifdef __cplusplus

View File

@@ -22,14 +22,10 @@
class UdpMulticastHandler final
{
public:
UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); }
UdpMulticastHandler() { 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],
@@ -38,29 +34,13 @@ 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();
@@ -87,7 +67,7 @@ class UdpMulticastHandler final
bool onSend(const meshtastic_MeshPacket *mp)
{
if (!isRunning || !mp || !udp) {
if (!mp || !udp) {
return false;
}
#if defined(ARCH_NRF52)
@@ -112,6 +92,5 @@ class UdpMulticastHandler final
private:
IPAddress udpIpAddress;
AsyncUDP udp;
bool isRunning;
};
#endif // HAS_UDP_MULTICAST

View File

@@ -384,18 +384,13 @@ static void WiFiEvent(WiFiEvent_t event)
#endif
}
#ifdef WIFI_LED
digitalWrite(WIFI_LED, HIGH);
digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON);
#endif
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
LOG_INFO("Disconnected from WiFi access point");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, LOW);
#endif
#if HAS_UDP_MULTICAST
if (udpHandler) {
udpHandler->stop();
}
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
#endif
if (!isReconnecting) {
WiFi.disconnect(false, true);
@@ -422,11 +417,6 @@ 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();
@@ -452,13 +442,13 @@ static void WiFiEvent(WiFiEvent_t event)
case ARDUINO_EVENT_WIFI_AP_START:
LOG_INFO("WiFi access point started");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, HIGH);
digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON);
#endif
break;
case ARDUINO_EVENT_WIFI_AP_STOP:
LOG_INFO("WiFi access point stopped");
#ifdef WIFI_LED
digitalWrite(WIFI_LED, LOW);
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
#endif
break;
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:

View File

@@ -106,15 +106,4 @@ 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;
}

View File

@@ -35,7 +35,4 @@ 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__)

View File

@@ -905,10 +905,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
{
bool shouldReboot = true;
// If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
if (!hasOpenEditTransaction &&
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag,
meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) {
disableBluetooth();
}
@@ -1000,8 +1001,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
moduleConfig.has_paxcounter = true;
moduleConfig.paxcounter = c.payload_variant.paxcounter;
break;
case meshtastic_ModuleConfig_statusmessage_tag:
LOG_INFO("Set module config: StatusMessage");
moduleConfig.has_statusmessage = true;
moduleConfig.statusmessage = c.payload_variant.statusmessage;
shouldReboot = false;
break;
}
saveChanges(SEGMENT_MODULECONFIG);
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
return true;
}
@@ -1180,6 +1187,11 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
break;
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
LOG_INFO("Get module config: StatusMessage");
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
break;
}
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.
@@ -1262,7 +1274,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
}
#endif
#if HAS_ETHERNET && !defined(USE_WS5500)
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
conn.has_ethernet = true;
conn.ethernet.has_status = true;
if (Ethernet.linkStatus() == LinkON) {

View File

@@ -6,7 +6,6 @@
#include "configuration.h"
#include "main.h"
#include "mesh/compression/unishox2.h"
#include "meshUtils.h"
#include "meshtastic/atak.pb.h"
AtakPluginModule *atakPluginModule;
@@ -71,17 +70,16 @@ 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, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
auto length = unishox2_compress_lines(t->contact.callsign, strlen(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, 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);
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);
if (length < 0) {
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
return;
@@ -89,11 +87,9 @@ 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,
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);
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);
if (length < 0) {
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
return;
@@ -102,9 +98,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, 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);
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);
if (length < 0) {
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
return;
@@ -114,11 +110,9 @@ 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,
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);
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);
if (length < 0) {
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
return;
@@ -140,18 +134,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, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
auto length =
unishox2_decompress_lines(t->contact.callsign, strlen(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, 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);
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);
if (length < 0) {
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
return;
@@ -159,11 +153,9 @@ 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,
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);
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);
if (length < 0) {
LOG_WARN("Decompress overflow chat.message. Bailing out");
return;
@@ -172,9 +164,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, 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);
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);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to. Bailing out");
return;
@@ -184,11 +176,10 @@ 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,
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);
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);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
return;

View File

@@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
// generate nonce
updateState();
if (currentState != KEY_VERIFICATION_IDLE) {
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;)
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::KeyVerificationFinalPrompt);)
IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);)
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_WARNING;
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);

View File

@@ -90,6 +90,9 @@
#if !MESHTASTIC_EXCLUDE_DROPZONE
#include "modules/DropzoneModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
#include "modules/StatusMessageModule.h"
#endif
#if defined(HAS_HARDWARE_WATCHDOG)
#include "watchdog/watchdogThread.h"
@@ -150,6 +153,9 @@ void setupModules()
#if !MESHTASTIC_EXCLUDE_DROPZONE
dropzoneModule = new DropzoneModule();
#endif
#if !MESHTASTIC_EXCLUDE_STATUS
statusMessageModule = new StatusMessageModule();
#endif
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
new GenericThreadModule();
#endif

View File

@@ -0,0 +1,183 @@
#if !MESHTASTIC_EXCLUDE_REPLYBOT
/*
* ReplyBotModule.cpp
*
* This module implements a simple reply bot for the Meshtastic firmware. It listens for
* specific text commands ("/ping", "/hello" and "/test") delivered either via a direct
* message (DM) or a broadcast on the primary channel. When a supported command is
* received the bot responds with a short status message that includes the hop count
* (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming
* the network it enforces a persender cooldown between responses. By default the
* module is enabled; define MESHTASTIC_EXCLUDE_REPLYBOT at build time to exclude it
* entirely. See the official firmware documentation for guidance on adding modules.
*/
#include "ReplyBotModule.h"
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "configuration.h"
#include "mesh/MeshTypes.h"
#include <Arduino.h>
#include <cctype>
#include <cstring>
//
// Rate limiting data structures
//
// Each sender is tracked in a small ring buffer. When a message arrives from a
// sender we check the last time we responded to them. If the difference is
// less than the configured cooldown (different values for DM vs broadcast)
// the message is ignored; otherwise we update the last response time and
// proceed with replying.
struct ReplyBotCooldownEntry {
uint32_t from = 0;
uint32_t lastMs = 0;
};
static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size
static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs
static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts
static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS];
static uint8_t replybotCooldownIdx = 0;
// Return true if a reply should be ratelimited for this sender, updating the
// entry table as needed.
static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs)
{
const uint32_t now = millis();
for (auto &e : replybotCooldown) {
if (e.from == from) {
// Found existing entry; check if cooldown expired
if ((uint32_t)(now - e.lastMs) < cooldownMs) {
return true;
}
e.lastMs = now;
return false;
}
}
// No entry found insert new sender into the ring
replybotCooldown[replybotCooldownIdx].from = from;
replybotCooldown[replybotCooldownIdx].lastMs = now;
replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS;
return false;
}
// Constructor registers a single text port and marks the module promiscuous
// so that broadcast messages on the primary channel are visible.
ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP)
{
isPromiscuous = true;
}
void ReplyBotModule::setup()
{
// In future we may add a protobuf configuration; for now the module is
// always enabled when compiled in.
}
// Determine whether we want to process this packet. We only care about
// plain text messages addressed to our port.
bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p)
{
return (p && p->decoded.portnum == ourPortNum);
}
ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp)
{
// Accept only direct messages to us or broadcasts on the Primary channel
// (regardless of modem preset: LongFast, MediumFast, etc).
const uint32_t ourNode = nodeDB->getNodeNum();
const bool isDM = (mp.to == ourNode);
const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to);
if (!isDM && !isPrimaryChannel) {
return ProcessMessage::CONTINUE;
}
// Ignore empty payloads
if (mp.decoded.payload.size == 0) {
return ProcessMessage::CONTINUE;
}
// Copy payload into a nullterminated buffer
char buf[260];
memset(buf, 0, sizeof(buf));
size_t n = mp.decoded.payload.size;
if (n > sizeof(buf) - 1)
n = sizeof(buf) - 1;
memcpy(buf, mp.decoded.payload.bytes, n);
// React only to supported slash commands
if (!isCommand(buf)) {
return ProcessMessage::CONTINUE;
}
// Apply rate limiting per sender depending on DM/broadcast
const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS;
if (replybotRateLimited(mp.from, cooldownMs)) {
return ProcessMessage::CONTINUE;
}
// Compute hop count indicator if the relay_node is nonzero we know
// there was at least one relay. Some firmware builds support a hop_start
// field which could be used for more accurate counts, but here we use
// the available relay_node flag only.
// int hopsAway = mp.hop_start - mp.hop_limit;
int hopsAway = getHopsAway(mp);
// Normalize RSSI: if positive adjust down by 200 to align with typical values
int rssi = mp.rx_rssi;
if (rssi > 0) {
rssi -= 200;
}
float snr = mp.rx_snr;
// Build the reply message and send it back via DM
char reply[96];
snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr);
sendDm(mp, reply);
return ProcessMessage::CONTINUE;
}
// Check if the message starts with one of the supported commands. Leading
// whitespace is skipped and commands must be followed by endofstring or
// whitespace.
bool ReplyBotModule::isCommand(const char *msg) const
{
if (!msg)
return false;
while (*msg == ' ' || *msg == '\t')
msg++;
auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast<unsigned char>(c)); };
if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5]))
return true;
if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6]))
return true;
if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5]))
return true;
return false;
}
// Send a direct message back to the originating node.
void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text)
{
if (!text)
return;
meshtastic_MeshPacket *p = allocDataPacket();
p->to = rx.from;
p->channel = rx.channel;
p->want_ack = false;
p->decoded.want_response = false;
size_t len = strlen(text);
if (len > sizeof(p->decoded.payload.bytes)) {
len = sizeof(p->decoded.payload.bytes);
}
p->decoded.payload.size = len;
memcpy(p->decoded.payload.bytes, text, len);
service->sendToMesh(p);
}
#endif // MESHTASTIC_EXCLUDE_REPLYBOT

View File

@@ -0,0 +1,18 @@
#pragma once
#if !MESHTASTIC_EXCLUDE_REPLYBOT
#include "SinglePortModule.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
class ReplyBotModule : public SinglePortModule
{
public:
ReplyBotModule();
void setup() override;
bool wantPacket(const meshtastic_MeshPacket *p) override;
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
protected:
bool isCommand(const char *msg) const;
void sendDm(const meshtastic_MeshPacket &rx, const char *text);
};
#endif // MESHTASTIC_EXCLUDE_REPLYBOT

View File

@@ -0,0 +1,41 @@
#if !MESHTASTIC_EXCLUDE_STATUS
#include "StatusMessageModule.h"
#include "MeshService.h"
#include "ProtobufModule.h"
StatusMessageModule *statusMessageModule;
int32_t StatusMessageModule::runOnce()
{
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
// create and send message with the status message set
meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero;
strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status));
ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination
meshtastic_MeshPacket *p = allocDataPacket();
p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes),
meshtastic_StatusMessage_fields, &ourStatus);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
p->channel = 0;
service->sendToMesh(p);
}
return 1000 * 12 * 60 * 60;
}
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
meshtastic_StatusMessage incomingMessage;
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
&incomingMessage)) {
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
}
}
return ProcessMessage::CONTINUE;
}
#endif

View File

@@ -0,0 +1,35 @@
#pragma once
#if !MESHTASTIC_EXCLUDE_STATUS
#include "SinglePortModule.h"
#include "configuration.h"
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
{
public:
/** Constructor
* name is for debugging output
*/
StatusMessageModule()
: SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage")
{
if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') {
this->setInterval(2 * 60 * 1000);
} else {
this->setInterval(1000 * 12 * 60 * 60);
}
// TODO: If we have a string, set the initial delay (15 minutes maybe)
}
virtual int32_t runOnce() override;
protected:
/** Called to handle a particular incoming message
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
private:
};
extern StatusMessageModule *statusMessageModule;
#endif

View File

@@ -10,6 +10,7 @@
#include "PowerFSM.h"
#include "RTC.h"
#include "Router.h"
#include "Sensor/AddI2CSensorTemplate.h"
#include "UnitConversions.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
@@ -19,9 +20,7 @@
#include <Throttle.h>
// Sensors
#include "Sensor/AddI2CSensorTemplate.h"
#include "Sensor/PMSA003ISensor.h"
#include "Sensor/SEN5XSensor.h"
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{
@@ -43,7 +42,6 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
// order by priority of metrics/values (low top, high bottom)
addSensor<PMSA003ISensor>(i2cScanner, ScanI2C::DeviceType::PMSA003I);
addSensor<SEN5XSensor>(i2cScanner, ScanI2C::DeviceType::SEN5X);
}
int32_t AirQualityTelemetryModule::runOnce()
@@ -87,27 +85,10 @@ 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->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;
}
}
if (!sensor->isActive()) {
return sensor->wakeUp();
}
}
@@ -128,18 +109,9 @@ int32_t AirQualityTelemetryModule::runOnce()
}
// Send to sleep sensors that consume power
LOG_DEBUG("Sending sensors to sleep");
LOG_INFO("Sending sensors to sleep");
for (TelemetrySensor *sensor : sensors) {
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");
}
}
sensor->sleep();
}
}
return min(sendToPhoneIntervalMs, result);
@@ -186,7 +158,8 @@ 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;
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;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
@@ -252,10 +225,9 @@ 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);
// 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(" | 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);
#endif
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
@@ -275,8 +247,10 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
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
for (TelemetrySensor *sensor : sensors) {
LOG_DEBUG("Reading %s", sensor->sensorName);
LOG_INFO("Reading AQ sensors");
valid = valid && sensor->getMetrics(m);
hasSensor = true;
}
@@ -317,14 +291,12 @@ 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", 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);
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);
meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest;
@@ -359,20 +331,6 @@ 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;
}

View File

@@ -21,29 +21,26 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
_bus = bus;
_address = dev->address.address;
#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 */
#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
_bus->beginTransmission(_address);
if (_bus->endTransmission() != 0) {
LOG_WARN("%s not found on I2C at 0x12", sensorName);
LOG_WARN("PMSA003I not found on I2C at 0x12");
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus, false);
reClockI2C(currentClock, _bus);
#endif
status = 1;
LOG_INFO("%s Enabled", sensorName);
LOG_INFO("PMSA003I Enabled");
initI2CSensor();
return true;
@@ -52,37 +49,30 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
if (!isActive()) {
LOG_WARN("Can't get metrics. %s is not active", sensorName);
LOG_WARN("PMSA003I is not active");
return false;
}
#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 */
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
#endif
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _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("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]);
LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
return false;
}
@@ -96,7 +86,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
if (computedChecksum != receivedChecksum) {
LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum);
LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
return false;
}
@@ -146,58 +136,20 @@ 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 %s", sensorName);
LOG_INFO("Waking up PMSA003I");
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
pmMeasureStarted = getTime();
return PMSA003I_WARMUP_MS;
#endif
// No need to wait for warmup if already active

View File

@@ -3,7 +3,6 @@
#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
@@ -20,9 +19,6 @@ 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 };
@@ -30,7 +26,6 @@ class PMSA003ISensor : public TelemetrySensor
uint16_t computedChecksum = 0;
uint16_t receivedChecksum = 0;
uint32_t pmMeasureStarted = 0;
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
TwoWire *_bus{};

View File

@@ -1,957 +0,0 @@
#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 <float.h> // FLT_MAX
#include <pb_decode.h>
#include <pb_encode.h>
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, &nothing, 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<uint8_t>((command & 0xFF00) >> 8);
toSend[i++] = static_cast<uint8_t>((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<Print *>(&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<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
int16_t int_humidity = static_cast<int16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
int16_t int_temperature = static_cast<int16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
int16_t int_vocIndex = static_cast<int16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
int16_t int_noxIndex = static_cast<int16_t>((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<uint16_t>((dataBuffer[0] << 8) | dataBuffer[1]);
// uint16_t uint_pM2p5 = static_cast<uint16_t>((dataBuffer[2] << 8) | dataBuffer[3]);
// uint16_t uint_pM4p0 = static_cast<uint16_t>((dataBuffer[4] << 8) | dataBuffer[5]);
// uint16_t uint_pM10p0 = static_cast<uint16_t>((dataBuffer[6] << 8) | dataBuffer[7]);
uint16_t uint_pN0p5 = static_cast<uint16_t>((dataBuffer[8] << 8) | dataBuffer[9]);
uint16_t uint_pN1p0 = static_cast<uint16_t>((dataBuffer[10] << 8) | dataBuffer[11]);
uint16_t uint_pN2p5 = static_cast<uint16_t>((dataBuffer[12] << 8) | dataBuffer[13]);
uint16_t uint_pN4p0 = static_cast<uint16_t>((dataBuffer[14] << 8) | dataBuffer[15]);
uint16_t uint_pN10p0 = static_cast<uint16_t>((dataBuffer[16] << 8) | dataBuffer[17]);
uint16_t uint_tSize = static_cast<uint16_t>((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

View File

@@ -1,170 +0,0 @@
#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

View File

@@ -26,6 +26,7 @@ class TelemetrySensor
this->status = 0;
}
const char *sensorName;
meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET;
unsigned status;
bool initialized = false;
@@ -55,18 +56,13 @@ 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; }
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; }
// Return active by default, override per sensor
virtual bool isActive() { return true; }
#if WIRE_INTERFACES_COUNT > 1
// Set to true if Implementation only works first I2C port (Wire)

View File

@@ -15,7 +15,7 @@
#include <WiFiClientSecure.h>
#endif
#endif
#if HAS_ETHERNET && !defined(USE_WS5500)
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#include <EthernetClient.h>
#endif

View File

@@ -757,11 +757,7 @@ void NimbleBluetooth::deinit()
isDeInit = true;
#ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW);
#endif
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
#ifndef NIMBLE_TWO
NimBLEDevice::deinit();

View File

@@ -24,11 +24,6 @@
#include <nvs.h>
#include <nvs_flash.h>
// 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)
{
@@ -254,7 +249,6 @@ 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);

View File

@@ -36,13 +36,6 @@ 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())
{

View File

@@ -22,7 +22,6 @@ 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;

View File

@@ -46,7 +46,7 @@
uint16_t getVDDVoltage();
// Weak empty variant shutdown prep function.
// Weak empty variant initialization function.
// May be redefined by variant files.
void variant_shutdown() __attribute__((weak));
void variant_shutdown() {}

View File

@@ -872,6 +872,7 @@ bool loadConfig(const char *configPath)
}
if (yamlConfig["Config"]) {
portduino_config.has_config_overrides = true;
if (yamlConfig["Config"]["DisplayMode"]) {
portduino_config.has_configDisplayMode = true;
if ((yamlConfig["Config"]["DisplayMode"]).as<std::string>("") == "TWOCOLOR") {
@@ -884,6 +885,13 @@ bool loadConfig(const char *configPath)
portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
}
}
if (yamlConfig["Config"]["StatusMessage"]) {
portduino_config.has_statusMessage = true;
portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as<std::string>("");
}
if ((yamlConfig["Config"]["EnableUDP"]).as<bool>(false)) {
portduino_config.enable_UDP = true;
}
}
if (yamlConfig["General"]) {

View File

@@ -177,8 +177,12 @@ extern struct portduino_config_struct {
int hostMetrics_channel = 0;
// config
bool has_config_overrides = false;
int configDisplayMode = 0;
bool has_configDisplayMode = false;
std::string statusMessage = "";
bool has_statusMessage = false;
bool enable_UDP = false;
// General
std::string mac_address = "";
@@ -505,21 +509,30 @@ extern struct portduino_config_struct {
}
// config
if (has_configDisplayMode) {
if (has_config_overrides) {
out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
switch (configDisplayMode) {
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
break;
if (has_configDisplayMode) {
switch (configDisplayMode) {
case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
break;
case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
break;
}
}
if (has_statusMessage) {
out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage;
}
if (enable_UDP) {
out << YAML::Key << "EnableUDP" << YAML::Value << true;
}
out << YAML::EndMap; // Config

View File

@@ -149,18 +149,18 @@ 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_pm25_environmental) {
// msgPayload["pm25_e"] =
// new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
// }
// 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_pm10_environmental) {
msgPayload["pm10_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
}
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_pm100_environmental) {
msgPayload["pm100_e"] =
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);

View File

@@ -120,15 +120,15 @@ 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_pm25_environmental) {
// jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental;
// }
// 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_pm10_environmental) {
jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental;
}
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_pm100_environmental) {
jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental;
}
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
if (decoded->variant.power_metrics.has_ch1_voltage) {
jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;

View File

@@ -4,13 +4,6 @@
#include "TestUtil.h"
#if defined(ARDUINO)
#include <Arduino.h>
#else
#include <chrono>
#include <thread>
#endif
void initializeTestEnvironment()
{
concurrency::hasBeenSetup = true;
@@ -22,13 +15,4 @@ 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
}

View File

@@ -1,7 +1,4 @@
#pragma once
// Initialize testing environment.
void initializeTestEnvironment();
// Portable delay for tests (Arduino or host).
void testDelay(unsigned long ms);
void initializeTestEnvironment();

View File

@@ -1,216 +0,0 @@
#include <string.h>
#include <unity.h>
#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() {}

View File

@@ -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.3
adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -26,7 +26,6 @@
#undef GPS_TX_PIN
#define NO_GPS 1
#define HAS_GPS 0
#define NO_SCREEN
#define HAS_SCREEN 0
// Default SPI1 will be mapped to the display

View File

@@ -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.3
adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -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.3
adafruit/Adafruit NeoPixel@1.15.2
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
h2zero/NimBLE-Arduino@2.3.7
build_flags =

View File

@@ -0,0 +1,26 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
// The default Wire will be mapped to PMU and RTC
static const uint8_t SDA = 17;
static const uint8_t SCL = 18;
// Default SPI will be mapped to Radio
static const uint8_t SS = 39;
static const uint8_t MOSI = 40;
static const uint8_t MISO = 41;
static const uint8_t SCK = 42;
// #define SPI_MOSI (11)
// #define SPI_SCK (10)
// #define SPI_MISO (9)
// #define SPI_CS (12)
// #define SDCARD_CS SPI_CS
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,22 @@
[env:thinknode_g3]
extends = esp32s3_base
board = ESP32-S3-WROOM-1-N4
board_build.psram_type = opi
build_flags =
${esp32s3_base.build_flags}
-D ELECROW_ThinkNode_G3
-D HAS_UDP_MULTICAST=1
-D BOARD_HAS_PSRAM
-D PRIVATE_HW
# -D CONFIG_ETH_ENABLED=1
# -D CONFIG_ETH_USE_ESP32_EMAC=1
-I variants/esp32s3/ELECROW-ThinkNode-G3
-mfix-esp32-psram-cache-issue
lib_ignore =
Ethernet
lib_deps =
${esp32s3_base.lib_deps}
# file://../ESP32-CH390/ESP32-CH390-1.0.1.tar.gz

View File

@@ -0,0 +1,36 @@
#define HAS_GPS 0
#define HAS_WIRE 0
#define I2C_NO_RESCAN
#define WIFI_LED 5
#define WIFI_STATE_ON 0
#define LED_PIN 6 // The blue LORA LED
#define LED_STATE_ON 0
#define BUTTON_PIN 4 // the external user button of the device, BOOT and RESET are not accessible without opening it up.
#define USE_SX1262
#define LORA_SCK 42
#define LORA_MISO 41
#define LORA_MOSI 40
#define LORA_CS 39
#define LORA_RESET 21
#define SX126X_CS LORA_CS
#define SX126X_DIO1 15
#define SX126X_BUSY 47
#define SX126X_RESET LORA_RESET
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define PIN_POWER_EN 45
// #define HAS_ETHERNET 1
// #define USE_CH390D 1 // this driver uses the same stack as the ESP32 Wifi driver
// #define ETH_MISO_PIN 47
// #define ETH_MOSI_PIN 21
// #define ETH_SCLK_PIN 48
// #define ETH_CS_PIN 45
// #define ETH_INT_PIN 14
// #define ETH_RST_PIN -1
// #define ETH_ADDR 1

View File

@@ -34,5 +34,3 @@ 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

View File

@@ -1,17 +1,12 @@
#include "variant.h"
#include <PCA9557.h>
PCA9557 io(0x18, &Wire1);
PCA9557 io(0x18, &Wire);
void earlyInitVariant()
{
Wire1.begin(48, 47);
Wire.begin(48, 47);
io.pinMode(PCA_PIN_EINK_EN, OUTPUT);
io.pinMode(PCA_PIN_POWER_EN, OUTPUT);
io.digitalWrite(PCA_PIN_POWER_EN, HIGH);
}
void variant_shutdown()
{
io.digitalWrite(PCA_PIN_POWER_EN, LOW);
}

View File

@@ -30,9 +30,6 @@
#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

View File

@@ -13,7 +13,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.6
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.3
adafruit/Adafruit NeoPixel@1.15.2
build_unflags =
${esp32s3_base.build_unflags}
-DARDUINO_USB_MODE=1

View File

@@ -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.3
adafruit/Adafruit NeoPixel@1.15.2
build_unflags =
${esp32s3_base.build_unflags}
-DARDUINO_USB_MODE=1

View File

@@ -25,4 +25,4 @@ lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2
zinggjm/GxEPD2@1.6.6
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
adafruit/Adafruit NeoPixel@1.15.3
adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -8,7 +8,6 @@
#define EXT_NOTIFY_OUT 22
#define BUTTON_PIN 0 // 17
// #define LED_PIN PIN_LED
// Board has RGB LED 21
#define HAS_NEOPIXEL // Enable the use of neopixels
#define NEOPIXEL_COUNT 1 // How many neopixels are connected

View File

@@ -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.3
adafruit/Adafruit NeoPixel@1.15.2

View File

@@ -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.3
adafruit/Adafruit NeoPixel@1.15.2
[env:unphone-tft]
board_level = extra

View File

@@ -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

View File

@@ -116,6 +116,7 @@ extern "C" {
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
#define HAS_RTC 1
#ifdef __cplusplus
}

View File

@@ -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

View File

@@ -121,6 +121,7 @@ static const uint8_t A0 = PIN_A0;
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
#define HAS_RTC 1
// SPI
#define SPI_INTERFACES_COUNT 1

View File

@@ -35,8 +35,6 @@ extern "C" {
#define PIN_LED2 LED_BLUE
#define PIN_LED3 LED_RED
#define PIN_LED PIN_LED1
#define LED_STATE_ON 1 // State when LED is lit
// XIAO Wio-SX1262 Shield User button

View File

@@ -51,7 +51,6 @@ extern "C" {
#define LED_GREEN PIN_LED1
#define BLE_LED LED_BLUE
#define BLE_LED_INVERTED 1
#define LED_STATE_ON 0 // State when LED is lit
// Buttons

View File

@@ -132,6 +132,9 @@ 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

View File

@@ -7,7 +7,7 @@
#define ADC_RESOLUTION (12u)
// LEDs
#define PIN_LED (24u)
#define LED_PIN (24u)
// Serial
#define PIN_SERIAL1_TX (16u)

View File

@@ -5,8 +5,6 @@
#define EXT_NOTIFY_OUT 0xFFFFFFFF
#define BUTTON_PIN 0xFFFFFFFF
#define LED_PIN PIN_LED
#define USE_RF95 // RFM95/SX127x
#undef LORA_SCK

View File

@@ -23,8 +23,7 @@ static const uint8_t A2 = PIN_A2;
static const uint8_t A3 = PIN_A3;
// LEDs
#define PIN_LED (23u)
#define PIN_LED1 PIN_LED
#define PIN_LED1 (23u)
#define LED_NOTIFICATION (24u)
#define ADC_RESOLUTION 12

View File

@@ -10,7 +10,7 @@
#define I2C_SDA1 2
#define I2C_SCL1 3
#define LED_PIN PIN_LED
#define LED_PIN PIN_LED1
#define ledOff(pin) pinMode(pin, INPUT)
#define BUTTON_PIN 9

View File

@@ -11,8 +11,7 @@ static const uint8_t A2 = PIN_A2;
static const uint8_t A3 = PIN_A3;
// LEDs
#define PIN_LED (23u)
#define PIN_LED1 PIN_LED
#define PIN_LED1 (23u)
#define ADC_RESOLUTION 12

View File

@@ -5,7 +5,7 @@
#define BUTTON_PIN 2
#define BUTTON_NEED_PULLUP
#define LED_PIN PIN_LED
#define LED_PIN PIN_LED1
#define ledOff(pin) pinMode(pin, INPUT)
#undef BATTERY_PIN

View File

@@ -17,6 +17,5 @@ build_flags =
-DPIN_SERIAL2_RX=PA3
-DHAS_GPS=1
-DGPS_SERIAL_PORT=Serial2
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
upload_port = stlink