mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-08 18:12:02 +00:00
Compare commits
21 Commits
use-ambien
...
tak-clean
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81bde47cd4 | ||
|
|
370f62a8c9 | ||
|
|
a43ce34143 | ||
|
|
fa5631523e | ||
|
|
7cbab4838c | ||
|
|
53231ae4b1 | ||
|
|
5280caf9d8 | ||
|
|
ba016fd91a | ||
|
|
e2cf401ad3 | ||
|
|
f73d18384d | ||
|
|
11bb2ee84e | ||
|
|
a324c4af10 | ||
|
|
5df5ab2790 | ||
|
|
74ea6206d9 | ||
|
|
b238744445 | ||
|
|
be5f0a9ade | ||
|
|
b008c7a170 | ||
|
|
c8a9cdc148 | ||
|
|
644fa5b54e | ||
|
|
e9d4485bb5 | ||
|
|
004179c045 |
@@ -8,18 +8,18 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.497
|
||||
- renovate@42.84.2
|
||||
- prettier@3.8.0
|
||||
- trufflehog@3.92.5
|
||||
- checkov@3.2.500
|
||||
- renovate@43.4.0
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.93.0
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.3
|
||||
- trivy@0.68.2
|
||||
- trivy@0.69.1
|
||||
- taplo@0.10.0
|
||||
- ruff@0.14.13
|
||||
- ruff@0.15.0
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.47.0
|
||||
- oxipng@10.0.0
|
||||
- oxipng@10.1.0
|
||||
- svgo@4.0.0
|
||||
- actionlint@1.7.10
|
||||
- flake8@7.3.0
|
||||
|
||||
@@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 (
|
||||
SET "PROGNAME=!FILENAME:.factory.bin=!"
|
||||
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
|
||||
|
||||
IF "__!MCU!__" == "__esp32s3__" (
|
||||
@REM We are working with ESP32-S3
|
||||
SET "OTA_FILENAME=bleota-s3.bin"
|
||||
) ELSE IF "__!MCU!__" == "__esp32c3__" (
|
||||
@REM We are working with ESP32-C3
|
||||
SET "OTA_FILENAME=bleota-c3.bin"
|
||||
) ELSE (
|
||||
@REM Everything else
|
||||
SET "OTA_FILENAME=bleota.bin"
|
||||
)
|
||||
@REM Determine OTA filename based on MCU type (unified OTA format)
|
||||
SET "OTA_FILENAME=mt-!MCU!-ota.bin"
|
||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||
|
||||
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||
|
||||
@@ -131,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine OTA filename based on MCU type
|
||||
if [ "$MCU" == "esp32s3" ]; then
|
||||
OTAFILE=bleota-s3.bin
|
||||
elif [ "$MCU" == "esp32c3" ]; then
|
||||
OTAFILE=bleota-c3.bin
|
||||
else
|
||||
OTAFILE=bleota.bin
|
||||
fi
|
||||
# Determine OTA filename based on MCU type (unified OTA format)
|
||||
OTAFILE="mt-${MCU}-ota.bin"
|
||||
|
||||
# Set SPIFFS filename with "littlefs-" prefix.
|
||||
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
|
||||
|
||||
@@ -59,14 +59,8 @@ BuildRequires: pkgconfig(libbsd-overlay)
|
||||
|
||||
Requires: systemd-udev
|
||||
|
||||
# Declare that this package provides the user/group it creates in %pre
|
||||
# Required for Fedora 43+ which tracks users/groups as RPM dependencies
|
||||
Provides: user(%{meshtasticd_user})
|
||||
Provides: group(%{meshtasticd_user})
|
||||
Provides: group(spi)
|
||||
|
||||
%description
|
||||
Meshtastic daemon. Meshtastic is an off-grid
|
||||
Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid
|
||||
text communication platform that uses inexpensive LoRa radios.
|
||||
|
||||
%prep
|
||||
@@ -157,7 +151,6 @@ fi
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%{_bindir}/meshtasticd
|
||||
%{_bindir}/meshtasticd-start.sh
|
||||
%dir %{_localstatedir}/lib/meshtasticd
|
||||
%{_udevrulesdir}/99-meshtasticd-udev.rules
|
||||
%dir %{_sysconfdir}/meshtasticd
|
||||
|
||||
@@ -50,7 +50,6 @@ 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
|
||||
@@ -67,7 +66,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
@@ -121,7 +120,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/63967a4a557d33d56fc5746f9128200dde2d88c5.zip
|
||||
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -145,7 +144,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||
adafruit/Adafruit INA219@1.2.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
||||
adafruit/Adafruit MPU6050@2.2.6
|
||||
adafruit/Adafruit MPU6050@2.2.8
|
||||
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
||||
adafruit/Adafruit LIS3DH@1.3.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
||||
@@ -214,6 +213,7 @@ lib_deps =
|
||||
sensirion/Sensirion Core@0.7.2
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
|
||||
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
[environmental_extra_no_bsec]
|
||||
lib_deps =
|
||||
@@ -240,4 +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
|
||||
|
||||
Submodule protobufs updated: bc63a57f9e...e80cb2e410
@@ -1,21 +1,19 @@
|
||||
#ifndef AMBIENTLIGHTINGTHREAD_H
|
||||
#define AMBIENTLIGHTINGTHREAD_H
|
||||
|
||||
#include "Observer.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
#include <NCP5623.h>
|
||||
#include <graphics/RAKled.h>
|
||||
NCP5623 rgb;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_LP5562
|
||||
#include <graphics/NomadStarLED.h>
|
||||
LP5562 rgbw;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NEOPIXEL
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <graphics/NeoPixel.h>
|
||||
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
||||
#endif
|
||||
|
||||
#ifdef UNPHONE
|
||||
@@ -23,24 +21,10 @@
|
||||
extern unPhone unphone;
|
||||
#endif
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
class AmbientLightingThread : public concurrency::OSThread
|
||||
{
|
||||
friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status.
|
||||
friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications.
|
||||
|
||||
private:
|
||||
#ifdef HAS_NCP5623
|
||||
NCP5623 rgb;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_LP5562
|
||||
LP5562 rgbw;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NEOPIXEL
|
||||
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE);
|
||||
#endif
|
||||
|
||||
public:
|
||||
explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting")
|
||||
{
|
||||
@@ -52,15 +36,14 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
moduleConfig.ambient_lighting.led_state = true;
|
||||
#endif
|
||||
#endif
|
||||
#if AMBIENT_LIGHTING_TEST
|
||||
// define to enable test
|
||||
moduleConfig.ambient_lighting.led_state = true;
|
||||
moduleConfig.ambient_lighting.current = 10;
|
||||
// Uncomment to test module
|
||||
// moduleConfig.ambient_lighting.led_state = true;
|
||||
// moduleConfig.ambient_lighting.current = 10;
|
||||
// Default to a color based on our node number
|
||||
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||
#endif
|
||||
// moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||
// moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||
// moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
_type = type;
|
||||
if (_type == ScanI2C::DeviceType::NONE) {
|
||||
@@ -70,6 +53,11 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_RGB_LED
|
||||
if (!moduleConfig.ambient_lighting.led_state) {
|
||||
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
|
||||
disable();
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("AmbientLighting init");
|
||||
#ifdef HAS_NCP5623
|
||||
if (_type == ScanI2C::NCP5623) {
|
||||
@@ -89,13 +77,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
pixels.clear(); // Set all pixel colors to 'off'
|
||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||
#endif
|
||||
if (!moduleConfig.ambient_lighting.led_state) {
|
||||
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
|
||||
disable();
|
||||
return;
|
||||
}
|
||||
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
setLighting();
|
||||
#endif
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
}
|
||||
@@ -109,8 +91,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) {
|
||||
#endif
|
||||
setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
setLighting();
|
||||
return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
}
|
||||
@@ -167,53 +148,65 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue)
|
||||
void setLighting()
|
||||
{
|
||||
#ifdef HAS_NCP5623
|
||||
rgb.setCurrent(current);
|
||||
rgb.setRed(red);
|
||||
rgb.setGreen(green);
|
||||
rgb.setBlue(blue);
|
||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||
rgb.setCurrent(moduleConfig.ambient_lighting.current);
|
||||
rgb.setRed(moduleConfig.ambient_lighting.red);
|
||||
rgb.setGreen(moduleConfig.ambient_lighting.green);
|
||||
rgb.setBlue(moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d",
|
||||
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
rgbw.setCurrent(current);
|
||||
rgbw.setRed(red);
|
||||
rgbw.setGreen(green);
|
||||
rgbw.setBlue(blue);
|
||||
LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", current, red, green, blue);
|
||||
rgbw.setCurrent(moduleConfig.ambient_lighting.current);
|
||||
rgbw.setRed(moduleConfig.ambient_lighting.red);
|
||||
rgbw.setGreen(moduleConfig.ambient_lighting.green);
|
||||
rgbw.setBlue(moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current,
|
||||
moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
||||
pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||
moduleConfig.ambient_lighting.blue),
|
||||
0, NEOPIXEL_COUNT);
|
||||
|
||||
// RadioMaster Bandit has addressable LED at the two buttons
|
||||
// this allow us to set different lighting for them in variant.h file.
|
||||
#ifdef RADIOMASTER_900_BANDIT
|
||||
#if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX)
|
||||
pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1);
|
||||
#endif
|
||||
#if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX)
|
||||
pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1);
|
||||
#endif
|
||||
#endif
|
||||
pixels.show();
|
||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
||||
// current, red, green, blue);
|
||||
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - red);
|
||||
analogWrite(RGBLED_GREEN, 255 - green);
|
||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
||||
analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green);
|
||||
analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
#elif defined(RGBLED_RED)
|
||||
analogWrite(RGBLED_RED, red);
|
||||
analogWrite(RGBLED_GREEN, green);
|
||||
analogWrite(RGBLED_BLUE, blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red);
|
||||
analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green);
|
||||
analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
#endif
|
||||
#ifdef UNPHONE
|
||||
unphone.rgb(red, green, blue);
|
||||
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue);
|
||||
unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green,
|
||||
moduleConfig.ambient_lighting.blue);
|
||||
LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
#endif // AMBIENTLIGHTINGTHREAD_H
|
||||
|
||||
} // namespace concurrency
|
||||
|
||||
@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
|
||||
case ConnectionState::CONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||
#ifdef BLE_LED
|
||||
digitalWrite(BLE_LED, LED_STATE_ON);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#else
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
|
||||
case ConnectionState::DISCONNECTED:
|
||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||
#ifdef BLE_LED
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
66
src/Led.cpp
Normal file
66
src/Led.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
#include "Led.h"
|
||||
#include "PowerMon.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
|
||||
GpioVirtPin ledForceOn, ledBlink;
|
||||
|
||||
#if defined(LED_PIN)
|
||||
// Most boards have a GPIO for LED control
|
||||
static GpioHwPin ledRawHwPin(LED_PIN);
|
||||
#else
|
||||
static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware
|
||||
#endif
|
||||
|
||||
#if LED_STATE_ON == 0
|
||||
static GpioVirtPin ledHwPin;
|
||||
static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin);
|
||||
#else
|
||||
static GpioPin &ledHwPin = ledRawHwPin;
|
||||
#endif
|
||||
|
||||
#if defined(HAS_PMU)
|
||||
/**
|
||||
* A GPIO controlled by the PMU
|
||||
*/
|
||||
class GpioPmuPin : public GpioPin
|
||||
{
|
||||
public:
|
||||
void set(bool value)
|
||||
{
|
||||
if (pmu_found && PMU) {
|
||||
// blink the axp led
|
||||
PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
||||
}
|
||||
}
|
||||
} ledPmuHwPin;
|
||||
|
||||
// In some cases we need to drive a PMU LED and a normal LED
|
||||
static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin);
|
||||
#else
|
||||
static GpioPin &ledFinalPin = ledHwPin;
|
||||
#endif
|
||||
|
||||
#ifdef USE_POWERMON
|
||||
/**
|
||||
* We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff.
|
||||
*/
|
||||
class MonitoredLedPin : public GpioPin
|
||||
{
|
||||
public:
|
||||
void set(bool value)
|
||||
{
|
||||
if (powerMon) {
|
||||
if (value)
|
||||
powerMon->setState(meshtastic_PowerMon_State_LED_On);
|
||||
else
|
||||
powerMon->clearState(meshtastic_PowerMon_State_LED_On);
|
||||
}
|
||||
ledFinalPin.set(value);
|
||||
}
|
||||
} monitoredLedPin;
|
||||
#else
|
||||
static GpioPin &monitoredLedPin = ledFinalPin;
|
||||
#endif
|
||||
|
||||
static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or);
|
||||
7
src/Led.h
Normal file
7
src/Led.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#include "GpioLogic.h"
|
||||
#include "configuration.h"
|
||||
|
||||
/**
|
||||
* ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main)
|
||||
*/
|
||||
extern GpioVirtPin ledForceOn, ledBlink;
|
||||
101
src/Power.cpp
101
src/Power.cpp
@@ -459,8 +459,6 @@ 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
|
||||
@@ -692,9 +690,7 @@ bool Power::setup()
|
||||
bool found = false;
|
||||
if (axpChipInit()) {
|
||||
found = true;
|
||||
} else if (cw2015Init()) {
|
||||
found = true;
|
||||
} else if (max17048Init()) {
|
||||
} else if (lipoInit()) {
|
||||
found = true;
|
||||
} else if (lipoChargerInit()) {
|
||||
found = true;
|
||||
@@ -1323,7 +1319,7 @@ bool Power::axpChipInit()
|
||||
/**
|
||||
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
||||
*/
|
||||
class MAX17048BatteryLevel : public HasBatteryLevel
|
||||
class LipoBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
private:
|
||||
MAX17048Singleton *max17048 = nullptr;
|
||||
@@ -1371,18 +1367,18 @@ class MAX17048BatteryLevel : public HasBatteryLevel
|
||||
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
|
||||
};
|
||||
|
||||
MAX17048BatteryLevel max17048Level;
|
||||
LipoBatteryLevel lipoLevel;
|
||||
|
||||
/**
|
||||
* Init the Lipo battery level sensor
|
||||
*/
|
||||
bool Power::max17048Init()
|
||||
bool Power::lipoInit()
|
||||
{
|
||||
bool result = max17048Level.runOnce();
|
||||
LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||
bool result = lipoLevel.runOnce();
|
||||
LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||
if (!result)
|
||||
return false;
|
||||
batteryLevel = &max17048Level;
|
||||
batteryLevel = &lipoLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1390,88 +1386,7 @@ bool Power::max17048Init()
|
||||
/**
|
||||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
*/
|
||||
bool Power::max17048Init()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015
|
||||
|
||||
class CW2015BatteryLevel : public AnalogBatteryLevel
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||
*/
|
||||
virtual int getBatteryPercent() override
|
||||
{
|
||||
int data = -1;
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
Wire.write(0x04);
|
||||
if (Wire.endTransmission() == 0) {
|
||||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||||
data = Wire.read();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() override
|
||||
{
|
||||
uint16_t mv = 0;
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
Wire.write(0x02);
|
||||
if (Wire.endTransmission() == 0) {
|
||||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) {
|
||||
mv = Wire.read();
|
||||
mv <<= 8;
|
||||
mv |= Wire.read();
|
||||
// Voltage is read in 305uV units, convert to mV
|
||||
mv = mv * 305 / 1000;
|
||||
}
|
||||
}
|
||||
return mv;
|
||||
}
|
||||
};
|
||||
|
||||
CW2015BatteryLevel cw2015Level;
|
||||
|
||||
/**
|
||||
* Init the CW2015 battery level sensor
|
||||
*/
|
||||
bool Power::cw2015Init()
|
||||
{
|
||||
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
uint8_t getInfo[] = {0x0a, 0x00};
|
||||
Wire.write(getInfo, 2);
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
Wire.beginTransmission(CW2015_ADDR);
|
||||
Wire.write(0x00);
|
||||
bool result = false;
|
||||
if (Wire.endTransmission() == 0) {
|
||||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||||
uint8_t data = Wire.read();
|
||||
LOG_DEBUG("CW2015 init read data: 0x%x", data);
|
||||
if (data == 0x73) {
|
||||
result = true;
|
||||
batteryLevel = &cw2015Level;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
/**
|
||||
* The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
*/
|
||||
bool Power::cw2015Init()
|
||||
bool Power::lipoInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
*/
|
||||
#include "PowerFSM.h"
|
||||
#include "Default.h"
|
||||
#include "Led.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "main.h"
|
||||
#include "modules/StatusLEDModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -103,7 +103,7 @@ static void lsIdle()
|
||||
uint32_t sleepTime = SLEEP_TIME;
|
||||
|
||||
powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep);
|
||||
statusLEDModule->setPowerLED(false);
|
||||
ledBlink.set(false); // Never leave led on while in light sleep
|
||||
esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL);
|
||||
powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep);
|
||||
|
||||
@@ -111,7 +111,7 @@ static void lsIdle()
|
||||
case ESP_SLEEP_WAKEUP_TIMER:
|
||||
// Normal case: timer expired, we should just go back to sleep ASAP
|
||||
|
||||
statusLEDModule->setPowerLED(true);
|
||||
ledBlink.set(true); // briefly turn on led
|
||||
wakeCause2 = doLightSleep(100); // leave led on for 1ms
|
||||
|
||||
secsSlept += sleepTime;
|
||||
@@ -146,7 +146,7 @@ static void lsIdle()
|
||||
}
|
||||
} else {
|
||||
// Time to stop sleeping!
|
||||
statusLEDModule->setPowerLED(false);
|
||||
ledBlink.set(false);
|
||||
LOG_INFO("Reached ls_secs, service loop()");
|
||||
powerFSM.trigger(EVENT_WAKE_TIMER);
|
||||
}
|
||||
|
||||
@@ -233,7 +233,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define NAU7802_ADDR 0x2A
|
||||
#define MAX30102_ADDR 0x57
|
||||
#define SCD4X_ADDR 0x62
|
||||
#define CW2015_ADDR 0x62
|
||||
#define MLX90614_ADDR_DEF 0x5A
|
||||
#define CGRADSENS_ADDR 0x66
|
||||
#define LTR390UV_ADDR 0x53
|
||||
@@ -242,6 +241,7 @@ 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
|
||||
|
||||
@@ -89,7 +89,7 @@ class ScanI2C
|
||||
DA217,
|
||||
CHSC6X,
|
||||
CST226SE,
|
||||
CW2015
|
||||
SEN5X
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#endif
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
||||
#include "meshUtils.h" // vformat
|
||||
|
||||
#endif
|
||||
|
||||
bool in_array(uint8_t *array, int size, uint8_t lookfor)
|
||||
@@ -114,6 +115,45 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
||||
return value;
|
||||
}
|
||||
|
||||
/// for SEN5X detection
|
||||
// Note, this code needs to be called before setting the I2C bus speed
|
||||
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
||||
// detection will not work
|
||||
String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
||||
{
|
||||
uint8_t cmd[] = {0xD0, 0x14};
|
||||
uint8_t response[48] = {0};
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(cmd, 2);
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return "";
|
||||
|
||||
delay(20);
|
||||
if (i2cBus->requestFrom(address, (uint8_t)48) != 48)
|
||||
return "";
|
||||
|
||||
for (int i = 0; i < 48 && i2cBus->available(); ++i) {
|
||||
response[i] = i2cBus->read();
|
||||
}
|
||||
|
||||
char productName[33] = {0};
|
||||
int j = 0;
|
||||
for (int i = 0; i < 48 && j < 32; i += 3) {
|
||||
if (response[i] >= 32 && response[i] <= 126)
|
||||
productName[j++] = response[i];
|
||||
else
|
||||
break;
|
||||
|
||||
if (response[i + 1] >= 32 && response[i + 1] <= 126)
|
||||
productName[j++] = response[i + 1];
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return String(productName);
|
||||
}
|
||||
|
||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||
case ADDR: \
|
||||
logFoundDevice(__VA_ARGS__); \
|
||||
@@ -541,17 +581,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
|
||||
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
|
||||
case SCD4X_ADDR: {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1);
|
||||
if (registerValue == 0x18) {
|
||||
logFoundDevice("CW2015", (uint8_t)addr.address);
|
||||
type = CW2015;
|
||||
} else {
|
||||
logFoundDevice("SCD4X", (uint8_t)addr.address);
|
||||
type = SCD4X;
|
||||
}
|
||||
break;
|
||||
}
|
||||
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
|
||||
#ifdef HAS_TPS65233
|
||||
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
||||
@@ -578,8 +608,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
|
||||
// ICM20948 Register check
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||
#ifdef HAS_ICM20948
|
||||
type = ICM20948;
|
||||
@@ -590,14 +621,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = ICM20948;
|
||||
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
||||
break;
|
||||
} else if (addr.address == BMX160_ADDR) {
|
||||
type = BMX160;
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
type = MPU6050;
|
||||
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
||||
break;
|
||||
String prod = "";
|
||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||
if (prod.startsWith("SEN55")) {
|
||||
type = SEN5X;
|
||||
logFoundDevice("Sensirion SEN55", addr.address);
|
||||
break;
|
||||
} else if (prod.startsWith("SEN54")) {
|
||||
type = SEN5X;
|
||||
logFoundDevice("Sensirion SEN54", addr.address);
|
||||
break;
|
||||
} else if (prod.startsWith("SEN50")) {
|
||||
type = SEN5X;
|
||||
logFoundDevice("Sensirion SEN50", addr.address);
|
||||
break;
|
||||
}
|
||||
if (addr.address == BMX160_ADDR) {
|
||||
type = BMX160;
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
type = MPU6050;
|
||||
logFoundDevice("MPU6050", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
31
src/detect/reClockI2C.cpp
Normal file
31
src/detect/reClockI2C.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "reClockI2C.h"
|
||||
#include "ScanI2CTwoWire.h"
|
||||
|
||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force)
|
||||
{
|
||||
|
||||
uint32_t currentClock = 0;
|
||||
|
||||
/* See https://github.com/arduino/Arduino/issues/11457
|
||||
Currently, only ESP32 can getClock()
|
||||
While all cores can setClock()
|
||||
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
|
||||
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
|
||||
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
|
||||
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
|
||||
we need to reclock I2C and set it back to the previous desired speed.
|
||||
Only for cases where we can know OR predefine the speed, we can do this.
|
||||
*/
|
||||
|
||||
// TODO add getClock function or return a predefined clock speed per variant?
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
currentClock = i2cBus->getClock();
|
||||
#endif
|
||||
|
||||
if ((currentClock != desiredClock) || force) {
|
||||
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
|
||||
i2cBus->setClock(desiredClock);
|
||||
}
|
||||
|
||||
return currentClock;
|
||||
}
|
||||
@@ -1,41 +1,11 @@
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
|
||||
#ifndef RECLOCK_I2C_
|
||||
#define RECLOCK_I2C_
|
||||
|
||||
#include "ScanI2CTwoWire.h"
|
||||
#include <Wire.h>
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
|
||||
{
|
||||
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force);
|
||||
|
||||
uint32_t currentClock;
|
||||
|
||||
/* See https://github.com/arduino/Arduino/issues/11457
|
||||
Currently, only ESP32 can getClock()
|
||||
While all cores can setClock()
|
||||
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
|
||||
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
|
||||
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
|
||||
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
|
||||
we need to reclock I2C and set it back to the previous desired speed.
|
||||
Only for cases where we can know OR predefine the speed, we can do this.
|
||||
*/
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
currentClock = i2cBus->getClock();
|
||||
#elif defined(ARCH_NRF52)
|
||||
// TODO add getClock function or return a predefined clock speed per variant?
|
||||
return 0;
|
||||
#elif defined(ARCH_RP2040)
|
||||
// TODO add getClock function or return a predefined clock speed per variant
|
||||
return 0;
|
||||
#elif defined(ARCH_STM32WL)
|
||||
// TODO add getClock function or return a predefined clock speed per variant
|
||||
return 0;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
if (currentClock != desiredClock) {
|
||||
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
|
||||
i2cBus->setClock(desiredClock);
|
||||
}
|
||||
return currentClock;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -72,11 +72,13 @@ RTCSetResult readFromRTC()
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
uint32_t now = millis();
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||
@@ -240,10 +242,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
if (rtc_found.address == PCF8563_RTC) {
|
||||
SensorPCF8563 rtc;
|
||||
#elif defined(PCF85063_RTC)
|
||||
if (rtc_found.address == PCF85063_RTC) {
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
SensorRtcHelper rtc;
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
|
||||
|
||||
4
src/graphics/NeoPixel.h
Normal file
4
src/graphics/NeoPixel.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifdef HAS_NEOPIXEL
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
extern Adafruit_NeoPixel pixels;
|
||||
#endif
|
||||
5
src/graphics/RAKled.h
Normal file
5
src/graphics/RAKled.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#ifdef HAS_NCP5623
|
||||
#include <NCP5623.h>
|
||||
extern NCP5623 rgb;
|
||||
|
||||
#endif
|
||||
@@ -221,6 +221,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour, minute, second;
|
||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
|
||||
@@ -429,10 +429,6 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
||||
// Show the common "/" pairing next to "?" like on a real keyboard
|
||||
if (key.type == VK_CHAR && key.character == '?') {
|
||||
keyText = "?/";
|
||||
}
|
||||
}
|
||||
|
||||
int textWidth = display->getStringWidth(keyText.c_str());
|
||||
@@ -522,13 +518,9 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
|
||||
char c = key.character;
|
||||
|
||||
// Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard
|
||||
if (isLongPress) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
} else if (c == '?') {
|
||||
c = '/';
|
||||
}
|
||||
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
||||
if (isLongPress && c >= 'a' && c <= 'z') {
|
||||
c = (char)(c - 'a' + 'A');
|
||||
}
|
||||
|
||||
return c;
|
||||
|
||||
@@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
||||
|
||||
} // namespace
|
||||
|
||||
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
|
||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||
uint32_t menuHandler::pickedNodeNum = 0;
|
||||
bool test_enabled = false;
|
||||
uint8_t test_count = 0;
|
||||
@@ -66,7 +66,7 @@ uint8_t test_count = 0;
|
||||
void menuHandler::loraMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
|
||||
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 };
|
||||
enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "LoRa Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -74,14 +74,14 @@ void menuHandler::loraMenu()
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
// No action
|
||||
} else if (selected == device_role_picker) {
|
||||
menuHandler::menuQueue = menuHandler::device_role_picker;
|
||||
} else if (selected == radio_preset_picker) {
|
||||
menuHandler::menuQueue = menuHandler::radio_preset_picker;
|
||||
} else if (selected == frequency_slot) {
|
||||
menuHandler::menuQueue = menuHandler::frequency_slot;
|
||||
} else if (selected == lora_picker) {
|
||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||
} else if (selected == DeviceRolePicker) {
|
||||
menuHandler::menuQueue = menuHandler::DeviceRolePicker;
|
||||
} else if (selected == RadioPresetPicker) {
|
||||
menuHandler::menuQueue = menuHandler::RadioPresetPicker;
|
||||
} else if (selected == FrequencySlot) {
|
||||
menuHandler::menuQueue = menuHandler::FrequencySlot;
|
||||
} else if (selected == LoraPicker) {
|
||||
menuHandler::menuQueue = menuHandler::LoraPicker;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -102,7 +102,7 @@ void menuHandler::OnboardMessage()
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
menuHandler::menuQueue = menuHandler::no_timeout_lora_picker;
|
||||
menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker;
|
||||
screen->runNow();
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::DeviceRolePicker()
|
||||
void menuHandler::deviceRolePicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||
enum optionsNumbers {
|
||||
@@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker()
|
||||
bannerOptions.optionsCount = 5;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
} else if (selected == devicerole_client) {
|
||||
@@ -300,7 +300,7 @@ void menuHandler::FrequencySlotPicker()
|
||||
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -313,7 +313,7 @@ void menuHandler::FrequencySlotPicker()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::RadioPresetPicker()
|
||||
void menuHandler::radioPresetPicker()
|
||||
{
|
||||
static const RadioPresetOption presetOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
@@ -333,7 +333,7 @@ void menuHandler::RadioPresetPicker()
|
||||
auto bannerOptions =
|
||||
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||
menuHandler::menuQueue = menuHandler::LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -352,7 +352,7 @@ void menuHandler::RadioPresetPicker()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::TwelveHourPicker()
|
||||
void menuHandler::twelveHourPicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
|
||||
@@ -362,7 +362,7 @@ void menuHandler::TwelveHourPicker()
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == twelve) {
|
||||
config.display.use_12h_clock = true;
|
||||
@@ -390,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::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::clock_menu;
|
||||
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -456,7 +456,7 @@ void menuHandler::TZPicker()
|
||||
auto bannerOptions = createStaticBannerOptions(
|
||||
"Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||
menuHandler::menuQueue = menuHandler::ClockMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -503,13 +503,13 @@ void menuHandler::clockMenu()
|
||||
bannerOptions.optionsCount = 4;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Clock) {
|
||||
menuHandler::menuQueue = menuHandler::clock_face_picker;
|
||||
menuHandler::menuQueue = menuHandler::ClockFacePicker;
|
||||
screen->runNow();
|
||||
} else if (selected == Time) {
|
||||
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
|
||||
menuHandler::menuQueue = menuHandler::TwelveHourPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == Timezone) {
|
||||
menuHandler::menuQueue = menuHandler::TZ_picker;
|
||||
menuHandler::menuQueue = menuHandler::TzPicker;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -572,12 +572,12 @@ void menuHandler::messageResponseMenu()
|
||||
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
|
||||
|
||||
if (selected == ViewMode) {
|
||||
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageViewModeMenu;
|
||||
screen->runNow();
|
||||
|
||||
// Reply submenu
|
||||
} else if (selected == ReplyMenu) {
|
||||
menuHandler::menuQueue = menuHandler::reply_menu;
|
||||
menuHandler::menuQueue = menuHandler::ReplyMenu;
|
||||
screen->runNow();
|
||||
|
||||
} else if (selected == MuteChannel) {
|
||||
@@ -589,7 +589,7 @@ void menuHandler::messageResponseMenu()
|
||||
}
|
||||
|
||||
} else if (selected == DeleteMenu) {
|
||||
menuHandler::menuQueue = menuHandler::delete_messages_menu;
|
||||
menuHandler::menuQueue = menuHandler::DeleteMessagesMenu;
|
||||
screen->runNow();
|
||||
|
||||
#ifdef HAS_I2S
|
||||
@@ -649,7 +649,7 @@ void menuHandler::replyMenu()
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -737,7 +737,7 @@ void menuHandler::deleteMessagesMenu()
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -901,7 +901,7 @@ void menuHandler::messageViewModeMenu()
|
||||
bannerOptions.bannerCallback = [=](int selected) -> void {
|
||||
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
|
||||
if (selected == -1) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
menuHandler::menuQueue = menuHandler::MessageResponseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == -2) {
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
|
||||
@@ -1083,23 +1083,23 @@ void menuHandler::systemBaseMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Notifications) {
|
||||
menuHandler::menuQueue = menuHandler::buzzermodemenupicker;
|
||||
menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == ScreenOptions) {
|
||||
menuHandler::menuQueue = menuHandler::screen_options_menu;
|
||||
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == PowerMenu) {
|
||||
menuHandler::menuQueue = menuHandler::power_menu;
|
||||
menuHandler::menuQueue = menuHandler::PowerMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Test) {
|
||||
menuHandler::menuQueue = menuHandler::test_menu;
|
||||
menuHandler::menuQueue = menuHandler::TestMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Bluetooth) {
|
||||
menuQueue = bluetooth_toggle_menu;
|
||||
menuQueue = BluetoothToggleMenu;
|
||||
screen->runNow();
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
} else if (selected == WiFiToggle) {
|
||||
menuQueue = wifi_toggle_menu;
|
||||
menuQueue = WifiToggleMenu;
|
||||
screen->runNow();
|
||||
#endif
|
||||
} else if (selected == Back && !test_enabled) {
|
||||
@@ -1177,7 +1177,7 @@ void menuHandler::favoriteBaseMenu()
|
||||
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
||||
screen->handleUIFrameEvent(&evt);
|
||||
} else if (selected == Remove) {
|
||||
menuHandler::menuQueue = menuHandler::remove_favorite;
|
||||
menuHandler::menuQueue = menuHandler::RemoveFavorite;
|
||||
screen->runNow();
|
||||
} else if (selected == TraceRoute) {
|
||||
if (traceRouteModule) {
|
||||
@@ -1238,15 +1238,15 @@ void menuHandler::positionBaseMenu()
|
||||
auto action = static_cast<PositionAction>(option.value);
|
||||
switch (action) {
|
||||
case PositionAction::GpsToggle:
|
||||
menuQueue = gps_toggle_menu;
|
||||
menuQueue = GpsToggleMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::GpsFormat:
|
||||
menuQueue = gps_format_menu;
|
||||
menuQueue = GpsFormatMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::CompassMenu:
|
||||
menuQueue = compass_point_north_menu;
|
||||
menuQueue = CompassPointNorthMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::CompassCalibrate:
|
||||
@@ -1255,15 +1255,15 @@ void menuHandler::positionBaseMenu()
|
||||
}
|
||||
break;
|
||||
case PositionAction::GPSSmartPosition:
|
||||
menuQueue = gps_smart_position_menu;
|
||||
menuQueue = GpsSmartPositionMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::GPSUpdateInterval:
|
||||
menuQueue = gps_update_interval_menu;
|
||||
menuQueue = GpsUpdateIntervalMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
case PositionAction::GPSPositionBroadcast:
|
||||
menuQueue = gps_position_broadcast_menu;
|
||||
menuQueue = GpsPositionBroadcastMenu;
|
||||
screen->runNow();
|
||||
break;
|
||||
}
|
||||
@@ -1303,13 +1303,13 @@ void menuHandler::nodeListMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == NodePicker) {
|
||||
menuQueue = NodePicker_menu;
|
||||
menuQueue = NodePickerMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Reset) {
|
||||
menuQueue = reset_node_db_menu;
|
||||
menuQueue = ResetNodeDbMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == NodeNameLength) {
|
||||
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
||||
menuHandler::menuQueue = menuHandler::NodeNameLengthMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -1330,12 +1330,12 @@ void menuHandler::NodePicker()
|
||||
menuHandler::pickedNodeNum = nodenum;
|
||||
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
|
||||
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
|
||||
menuQueue = Manage_Node_menu;
|
||||
menuQueue = ManageNodeMenu;
|
||||
screen->runNow();
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::ManageNodeMenu()
|
||||
void menuHandler::manageNodeMenu()
|
||||
{
|
||||
// If we don't have a node selected yet, go fast exit
|
||||
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
||||
@@ -1391,7 +1391,7 @@ void menuHandler::ManageNodeMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
menuQueue = node_base_menu;
|
||||
menuQueue = NodeBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1483,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu()
|
||||
auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels,
|
||||
[](const NodeNameOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = node_base_menu;
|
||||
menuQueue = NodeBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1498,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu()
|
||||
|
||||
config.display.use_long_node_name = option.value;
|
||||
saveUIConfig();
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
LOG_INFO("Setting names to %s", option.value ? "long" : "short");
|
||||
});
|
||||
|
||||
@@ -1528,7 +1529,7 @@ void menuHandler::resetNodeDBMenu()
|
||||
nodeDB->resetNodes(1);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
} else if (selected == 0) {
|
||||
menuQueue = node_base_menu;
|
||||
menuQueue = NodeBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -1550,7 +1551,7 @@ void menuHandler::compassNorthMenu()
|
||||
auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels,
|
||||
[](const CompassOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1595,7 +1596,7 @@ void menuHandler::GPSToggleMenu()
|
||||
auto bannerOptions =
|
||||
createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1660,7 +1661,7 @@ void menuHandler::GPSFormatMenu()
|
||||
|
||||
auto onSelection = [](const GPSFormatOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -1715,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu()
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == 1) {
|
||||
config.position.position_broadcast_smart_enabled = true;
|
||||
@@ -1744,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu()
|
||||
bannerOptions.optionsCount = 16;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == 1) {
|
||||
config.position.gps_update_interval = 8;
|
||||
@@ -1832,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu()
|
||||
bannerOptions.optionsCount = 17;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
menuQueue = position_base_menu;
|
||||
menuQueue = PositionBaseMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == 1) {
|
||||
config.position.position_broadcast_secs = 60;
|
||||
@@ -1915,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu()
|
||||
|
||||
#endif
|
||||
|
||||
void menuHandler::BluetoothToggleMenu()
|
||||
void menuHandler::bluetoothToggleMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
@@ -2043,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
|
||||
auto bannerOptions = createStaticBannerOptions(
|
||||
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
@@ -2138,7 +2139,7 @@ void menuHandler::rebootMenu()
|
||||
messageStore.saveToFlash();
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
} else {
|
||||
menuQueue = power_menu;
|
||||
menuQueue = PowerMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2160,7 +2161,7 @@ void menuHandler::shutdownMenu()
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
} else {
|
||||
menuQueue = power_menu;
|
||||
menuQueue = PowerMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2221,14 +2222,14 @@ void menuHandler::testMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == NumberPicker) {
|
||||
menuQueue = number_test;
|
||||
menuQueue = NumberTest;
|
||||
screen->runNow();
|
||||
} else if (selected == ShowChirpy) {
|
||||
screen->toggleFrameVisibility("chirpy");
|
||||
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||
|
||||
} else {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2252,7 +2253,7 @@ void menuHandler::wifiBaseMenu()
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Wifi_toggle) {
|
||||
menuQueue = wifi_toggle_menu;
|
||||
menuQueue = WifiToggleMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2301,9 +2302,9 @@ void menuHandler::screenOptionsMenu()
|
||||
hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
|
||||
static const char *optionsArray[5] = {"Back"};
|
||||
static int optionsEnumArray[5] = {Back};
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
||||
static const char *optionsArray[6] = {"Back"};
|
||||
static int optionsEnumArray[6] = {Back};
|
||||
int options = 1;
|
||||
|
||||
// Only show brightness for B&W displays
|
||||
@@ -2325,6 +2326,9 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsArray[options] = "Display Units";
|
||||
optionsEnumArray[options++] = DisplayUnits;
|
||||
|
||||
optionsArray[options] = "Message Bubbles";
|
||||
optionsEnumArray[options++] = MessageBubbles;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Display Options";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2332,10 +2336,10 @@ void menuHandler::screenOptionsMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Brightness) {
|
||||
menuHandler::menuQueue = menuHandler::brightness_picker;
|
||||
menuHandler::menuQueue = menuHandler::BrightnessPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == ScreenColor) {
|
||||
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
|
||||
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == FrameToggles) {
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
@@ -2343,8 +2347,11 @@ void menuHandler::screenOptionsMenu()
|
||||
} else if (selected == DisplayUnits) {
|
||||
menuHandler::menuQueue = menuHandler::DisplayUnits;
|
||||
screen->runNow();
|
||||
} else if (selected == MessageBubbles) {
|
||||
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
|
||||
screen->runNow();
|
||||
} else {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2380,16 +2387,16 @@ void menuHandler::powerMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Reboot) {
|
||||
menuHandler::menuQueue = menuHandler::reboot_menu;
|
||||
menuHandler::menuQueue = menuHandler::RebootMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Shutdown) {
|
||||
menuHandler::menuQueue = menuHandler::shutdown_menu;
|
||||
menuHandler::menuQueue = menuHandler::ShutdownMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == MUI) {
|
||||
menuHandler::menuQueue = menuHandler::mui_picker;
|
||||
menuHandler::menuQueue = menuHandler::MuiPicker;
|
||||
screen->runNow();
|
||||
} else {
|
||||
menuQueue = system_base_menu;
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2427,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
void menuHandler::FrameToggles_menu()
|
||||
void menuHandler::frameTogglesMenu()
|
||||
{
|
||||
enum optionsNumbers {
|
||||
Finish,
|
||||
@@ -2571,7 +2578,7 @@ void menuHandler::FrameToggles_menu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::DisplayUnits_menu()
|
||||
void menuHandler::displayUnitsMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
|
||||
|
||||
@@ -2592,7 +2599,34 @@ void menuHandler::DisplayUnits_menu()
|
||||
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
} else {
|
||||
menuHandler::menuQueue = menuHandler::screen_options_menu;
|
||||
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::messageBubblesMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, ShowBubbles, HideBubbles };
|
||||
|
||||
static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Message Bubbles";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == ShowBubbles) {
|
||||
config.display.enable_message_bubbles = true;
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
LOG_INFO("Message bubbles enabled");
|
||||
} else if (selected == HideBubbles) {
|
||||
config.display.enable_message_bubbles = false;
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
LOG_INFO("Message bubbles disabled");
|
||||
} else {
|
||||
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -2601,153 +2635,156 @@ void menuHandler::DisplayUnits_menu()
|
||||
|
||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
{
|
||||
if (menuQueue != menu_none)
|
||||
if (menuQueue != MenuNone)
|
||||
test_count = 0;
|
||||
switch (menuQueue) {
|
||||
case menu_none:
|
||||
case MenuNone:
|
||||
break;
|
||||
case lora_Menu:
|
||||
case LoraMenu:
|
||||
loraMenu();
|
||||
break;
|
||||
case lora_picker:
|
||||
case LoraPicker:
|
||||
LoraRegionPicker();
|
||||
break;
|
||||
case device_role_picker:
|
||||
DeviceRolePicker();
|
||||
case DeviceRolePicker:
|
||||
deviceRolePicker();
|
||||
break;
|
||||
case radio_preset_picker:
|
||||
RadioPresetPicker();
|
||||
case RadioPresetPicker:
|
||||
radioPresetPicker();
|
||||
break;
|
||||
case frequency_slot:
|
||||
case FrequencySlot:
|
||||
FrequencySlotPicker();
|
||||
break;
|
||||
case no_timeout_lora_picker:
|
||||
case NoTimeoutLoraPicker:
|
||||
LoraRegionPicker(0);
|
||||
break;
|
||||
case TZ_picker:
|
||||
case TzPicker:
|
||||
TZPicker();
|
||||
break;
|
||||
case twelve_hour_picker:
|
||||
TwelveHourPicker();
|
||||
case TwelveHourPicker:
|
||||
twelveHourPicker();
|
||||
break;
|
||||
case clock_face_picker:
|
||||
ClockFacePicker();
|
||||
case ClockFacePicker:
|
||||
clockFacePicker();
|
||||
break;
|
||||
case clock_menu:
|
||||
case ClockMenu:
|
||||
clockMenu();
|
||||
break;
|
||||
case system_base_menu:
|
||||
case SystemBaseMenu:
|
||||
systemBaseMenu();
|
||||
break;
|
||||
case position_base_menu:
|
||||
case PositionBaseMenu:
|
||||
positionBaseMenu();
|
||||
break;
|
||||
case node_base_menu:
|
||||
case NodeBaseMenu:
|
||||
nodeListMenu();
|
||||
break;
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
case gps_toggle_menu:
|
||||
case GpsToggleMenu:
|
||||
GPSToggleMenu();
|
||||
break;
|
||||
case gps_format_menu:
|
||||
case GpsFormatMenu:
|
||||
GPSFormatMenu();
|
||||
break;
|
||||
case gps_smart_position_menu:
|
||||
case GpsSmartPositionMenu:
|
||||
GPSSmartPositionMenu();
|
||||
break;
|
||||
case gps_update_interval_menu:
|
||||
case GpsUpdateIntervalMenu:
|
||||
GPSUpdateIntervalMenu();
|
||||
break;
|
||||
case gps_position_broadcast_menu:
|
||||
case GpsPositionBroadcastMenu:
|
||||
GPSPositionBroadcastMenu();
|
||||
break;
|
||||
#endif
|
||||
case compass_point_north_menu:
|
||||
case CompassPointNorthMenu:
|
||||
compassNorthMenu();
|
||||
break;
|
||||
case reset_node_db_menu:
|
||||
case ResetNodeDbMenu:
|
||||
resetNodeDBMenu();
|
||||
break;
|
||||
case buzzermodemenupicker:
|
||||
case BuzzerModeMenuPicker:
|
||||
BuzzerModeMenu();
|
||||
break;
|
||||
case mui_picker:
|
||||
case MuiPicker:
|
||||
switchToMUIMenu();
|
||||
break;
|
||||
case tftcolormenupicker:
|
||||
case TftColorMenuPicker:
|
||||
TFTColorPickerMenu(display);
|
||||
break;
|
||||
case brightness_picker:
|
||||
case BrightnessPicker:
|
||||
BrightnessPickerMenu();
|
||||
break;
|
||||
case node_name_length_menu:
|
||||
case NodeNameLengthMenu:
|
||||
nodeNameLengthMenu();
|
||||
break;
|
||||
case reboot_menu:
|
||||
case RebootMenu:
|
||||
rebootMenu();
|
||||
break;
|
||||
case shutdown_menu:
|
||||
case ShutdownMenu:
|
||||
shutdownMenu();
|
||||
break;
|
||||
case NodePicker_menu:
|
||||
case NodePickerMenu:
|
||||
NodePicker();
|
||||
break;
|
||||
case Manage_Node_menu:
|
||||
ManageNodeMenu();
|
||||
case ManageNodeMenu:
|
||||
manageNodeMenu();
|
||||
break;
|
||||
case remove_favorite:
|
||||
case RemoveFavorite:
|
||||
removeFavoriteMenu();
|
||||
break;
|
||||
case trace_route_menu:
|
||||
case TraceRouteMenu:
|
||||
traceRouteMenu();
|
||||
break;
|
||||
case test_menu:
|
||||
case TestMenu:
|
||||
testMenu();
|
||||
break;
|
||||
case number_test:
|
||||
case NumberTest:
|
||||
numberTest();
|
||||
break;
|
||||
case wifi_toggle_menu:
|
||||
case WifiToggleMenu:
|
||||
wifiToggleMenu();
|
||||
break;
|
||||
case key_verification_init:
|
||||
case KeyVerificationInit:
|
||||
keyVerificationInitMenu();
|
||||
break;
|
||||
case key_verification_final_prompt:
|
||||
case KeyVerificationFinalPrompt:
|
||||
keyVerificationFinalPrompt();
|
||||
break;
|
||||
case bluetooth_toggle_menu:
|
||||
BluetoothToggleMenu();
|
||||
case BluetoothToggleMenu:
|
||||
bluetoothToggleMenu();
|
||||
break;
|
||||
case screen_options_menu:
|
||||
case ScreenOptionsMenu:
|
||||
screenOptionsMenu();
|
||||
break;
|
||||
case power_menu:
|
||||
case PowerMenu:
|
||||
powerMenu();
|
||||
break;
|
||||
case FrameToggles:
|
||||
FrameToggles_menu();
|
||||
frameTogglesMenu();
|
||||
break;
|
||||
case DisplayUnits:
|
||||
DisplayUnits_menu();
|
||||
displayUnitsMenu();
|
||||
break;
|
||||
case throttle_message:
|
||||
case ThrottleMessage:
|
||||
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
||||
break;
|
||||
case message_response_menu:
|
||||
case MessageResponseMenu:
|
||||
messageResponseMenu();
|
||||
break;
|
||||
case reply_menu:
|
||||
case ReplyMenu:
|
||||
replyMenu();
|
||||
break;
|
||||
case delete_messages_menu:
|
||||
case DeleteMessagesMenu:
|
||||
deleteMessagesMenu();
|
||||
break;
|
||||
case message_viewmode_menu:
|
||||
case MessageViewModeMenu:
|
||||
messageViewModeMenu();
|
||||
break;
|
||||
case MessageBubblesMenu:
|
||||
messageBubblesMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = menu_none;
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
|
||||
void menuHandler::saveUIConfig()
|
||||
|
||||
@@ -8,53 +8,54 @@ class menuHandler
|
||||
{
|
||||
public:
|
||||
enum screenMenus {
|
||||
menu_none,
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
radio_preset_picker,
|
||||
frequency_slot,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
clock_face_picker,
|
||||
clock_menu,
|
||||
position_base_menu,
|
||||
node_base_menu,
|
||||
gps_toggle_menu,
|
||||
gps_format_menu,
|
||||
gps_smart_position_menu,
|
||||
gps_update_interval_menu,
|
||||
gps_position_broadcast_menu,
|
||||
compass_point_north_menu,
|
||||
reset_node_db_menu,
|
||||
buzzermodemenupicker,
|
||||
mui_picker,
|
||||
tftcolormenupicker,
|
||||
brightness_picker,
|
||||
reboot_menu,
|
||||
shutdown_menu,
|
||||
NodePicker_menu,
|
||||
Manage_Node_menu,
|
||||
remove_favorite,
|
||||
test_menu,
|
||||
number_test,
|
||||
wifi_toggle_menu,
|
||||
bluetooth_toggle_menu,
|
||||
screen_options_menu,
|
||||
power_menu,
|
||||
system_base_menu,
|
||||
key_verification_init,
|
||||
key_verification_final_prompt,
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
message_response_menu,
|
||||
message_viewmode_menu,
|
||||
reply_menu,
|
||||
delete_messages_menu,
|
||||
node_name_length_menu,
|
||||
MenuNone,
|
||||
LoraMenu,
|
||||
LoraPicker,
|
||||
DeviceRolePicker,
|
||||
RadioPresetPicker,
|
||||
FrequencySlot,
|
||||
NoTimeoutLoraPicker,
|
||||
TzPicker,
|
||||
TwelveHourPicker,
|
||||
ClockFacePicker,
|
||||
ClockMenu,
|
||||
PositionBaseMenu,
|
||||
NodeBaseMenu,
|
||||
GpsToggleMenu,
|
||||
GpsFormatMenu,
|
||||
GpsSmartPositionMenu,
|
||||
GpsUpdateIntervalMenu,
|
||||
GpsPositionBroadcastMenu,
|
||||
CompassPointNorthMenu,
|
||||
ResetNodeDbMenu,
|
||||
BuzzerModeMenuPicker,
|
||||
MuiPicker,
|
||||
TftColorMenuPicker,
|
||||
BrightnessPicker,
|
||||
RebootMenu,
|
||||
ShutdownMenu,
|
||||
NodePickerMenu,
|
||||
ManageNodeMenu,
|
||||
RemoveFavorite,
|
||||
TestMenu,
|
||||
NumberTest,
|
||||
WifiToggleMenu,
|
||||
BluetoothToggleMenu,
|
||||
ScreenOptionsMenu,
|
||||
PowerMenu,
|
||||
SystemBaseMenu,
|
||||
KeyVerificationInit,
|
||||
KeyVerificationFinalPrompt,
|
||||
TraceRouteMenu,
|
||||
ThrottleMessage,
|
||||
MessageResponseMenu,
|
||||
MessageViewModeMenu,
|
||||
ReplyMenu,
|
||||
DeleteMessagesMenu,
|
||||
NodeNameLengthMenu,
|
||||
FrameToggles,
|
||||
DisplayUnits
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
@@ -62,15 +63,15 @@ class menuHandler
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
static void loraMenu();
|
||||
static void DeviceRolePicker();
|
||||
static void RadioPresetPicker();
|
||||
static void deviceRolePicker();
|
||||
static void radioPresetPicker();
|
||||
static void FrequencySlotPicker();
|
||||
static void handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<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();
|
||||
@@ -95,7 +96,7 @@ class menuHandler
|
||||
static void rebootMenu();
|
||||
static void shutdownMenu();
|
||||
static void NodePicker();
|
||||
static void ManageNodeMenu();
|
||||
static void manageNodeMenu();
|
||||
static void addFavoriteMenu();
|
||||
static void removeFavoriteMenu();
|
||||
static void traceRouteMenu();
|
||||
@@ -106,15 +107,16 @@ class menuHandler
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void nodeNameLengthMenu();
|
||||
static void FrameToggles_menu();
|
||||
static void DisplayUnits_menu();
|
||||
static void frameTogglesMenu();
|
||||
static void displayUnitsMenu();
|
||||
static void messageBubblesMenu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
static void keyVerificationInitMenu();
|
||||
static void keyVerificationFinalPrompt();
|
||||
static void BluetoothToggleMenu();
|
||||
static void bluetoothToggleMenu();
|
||||
};
|
||||
|
||||
/* Generic Menu Options designations */
|
||||
|
||||
@@ -527,8 +527,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
constexpr int BUBBLE_MIN_W = 24;
|
||||
constexpr int BUBBLE_TEXT_INDENT = 2;
|
||||
|
||||
// Check if bubbles are enabled
|
||||
const bool showBubbles = config.display.enable_message_bubbles;
|
||||
const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN;
|
||||
|
||||
// Derived widths
|
||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2);
|
||||
const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0);
|
||||
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
|
||||
|
||||
// Title string depending on mode
|
||||
@@ -796,114 +800,105 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
}
|
||||
}
|
||||
|
||||
// Draw bubbles
|
||||
for (size_t bi = 0; bi < blocks.size(); ++bi) {
|
||||
const auto &b = blocks[bi];
|
||||
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
|
||||
continue;
|
||||
// Draw bubbles (only if enabled)
|
||||
if (showBubbles) {
|
||||
for (size_t bi = 0; bi < blocks.size(); ++bi) {
|
||||
const auto &b = blocks[bi];
|
||||
if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end)
|
||||
continue;
|
||||
|
||||
int visualTop = lineTop[b.start];
|
||||
int visualTop = lineTop[b.start];
|
||||
|
||||
int topY;
|
||||
if (isHeader[b.start]) {
|
||||
// Header start
|
||||
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
|
||||
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
|
||||
} else {
|
||||
// Body start
|
||||
bool thisLineHasEmote = false;
|
||||
for (int e = 0; e < numEmotes; ++e) {
|
||||
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
||||
thisLineHasEmote = true;
|
||||
break;
|
||||
int topY;
|
||||
if (isHeader[b.start]) {
|
||||
// Header start
|
||||
constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2
|
||||
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
|
||||
} else {
|
||||
// Body start
|
||||
bool thisLineHasEmote = false;
|
||||
for (int e = 0; e < numEmotes; ++e) {
|
||||
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
||||
thisLineHasEmote = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (thisLineHasEmote) {
|
||||
constexpr int EMOTE_PADDING_ABOVE = 4;
|
||||
visualTop -= EMOTE_PADDING_ABOVE;
|
||||
}
|
||||
topY = visualTop - BUBBLE_PAD_Y;
|
||||
}
|
||||
if (thisLineHasEmote) {
|
||||
constexpr int EMOTE_PADDING_ABOVE = 4;
|
||||
visualTop -= EMOTE_PADDING_ABOVE;
|
||||
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||
|
||||
if (bi + 1 < blocks.size()) {
|
||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||
int nextTop = lineTop[nextHeaderIndex];
|
||||
int maxBottom = nextTop - 1 - bubbleGapY;
|
||||
if (bottomY > maxBottom)
|
||||
bottomY = maxBottom;
|
||||
}
|
||||
topY = visualTop - BUBBLE_PAD_Y;
|
||||
}
|
||||
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||
|
||||
if (bi + 1 < blocks.size()) {
|
||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||
int nextTop = lineTop[nextHeaderIndex];
|
||||
int maxBottom = nextTop - 1 - bubbleGapY;
|
||||
if (bottomY > maxBottom)
|
||||
bottomY = maxBottom;
|
||||
}
|
||||
if (bottomY <= topY + 2)
|
||||
continue;
|
||||
|
||||
if (bottomY <= topY + 2)
|
||||
continue;
|
||||
if (bottomY < contentTop || topY > contentBottom - 1)
|
||||
continue;
|
||||
|
||||
if (bottomY < contentTop || topY > contentBottom - 1)
|
||||
continue;
|
||||
int maxLineW = 0;
|
||||
|
||||
int maxLineW = 0;
|
||||
|
||||
for (size_t i = b.start; i <= b.end; ++i) {
|
||||
int w = 0;
|
||||
if (isHeader[i]) {
|
||||
w = display->getStringWidth(cachedLines[i].c_str());
|
||||
if (b.mine)
|
||||
w += 12; // room for ACK/NACK/relay mark
|
||||
} else {
|
||||
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||
for (size_t i = b.start; i <= b.end; ++i) {
|
||||
int w = 0;
|
||||
if (isHeader[i]) {
|
||||
w = display->getStringWidth(cachedLines[i].c_str());
|
||||
if (b.mine)
|
||||
w += 12; // room for ACK/NACK/relay mark
|
||||
} else {
|
||||
w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||
}
|
||||
if (w > maxLineW)
|
||||
maxLineW = w;
|
||||
}
|
||||
if (w > maxLineW)
|
||||
maxLineW = w;
|
||||
}
|
||||
|
||||
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2));
|
||||
int bubbleH = (bottomY - topY) + 1;
|
||||
int bubbleX = 0;
|
||||
if (b.mine) {
|
||||
bubbleX = rightEdge - bubbleW;
|
||||
} else {
|
||||
bubbleX = x;
|
||||
}
|
||||
if (bubbleX < x)
|
||||
bubbleX = x;
|
||||
if (bubbleX + bubbleW > rightEdge)
|
||||
bubbleW = std::max(1, rightEdge - bubbleX);
|
||||
|
||||
if (bubbleW > 1 && bubbleH > 1) {
|
||||
int x1 = bubbleX + bubbleW - 1;
|
||||
int y1 = topY + bubbleH - 1;
|
||||
|
||||
int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2));
|
||||
int bubbleH = (bottomY - topY) + 1;
|
||||
int bubbleX = 0;
|
||||
if (b.mine) {
|
||||
// Send Message (Right side)
|
||||
display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH);
|
||||
// Top Right Corner
|
||||
display->drawRect(x1 - 1, topY, 2, 1);
|
||||
display->drawRect(x1, topY, 1, 2);
|
||||
// Bottom Right Corner
|
||||
display->drawRect(x1 - 1, bottomY - 2, 2, 1);
|
||||
display->drawRect(x1, bottomY - 3, 1, 2);
|
||||
// Knock the corners off to make a bubble
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(x1 - bubbleW + 2, topY - 1, 1, 1);
|
||||
display->drawRect(x1 - bubbleW + 2, bottomY - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
bubbleX = rightEdge - bubbleW;
|
||||
} else {
|
||||
// Received Message (Left Side)
|
||||
display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH);
|
||||
// Top Left Corner
|
||||
display->drawRect(bubbleX + 1, topY + 1, 2, 1);
|
||||
display->drawRect(bubbleX + 1, topY + 1, 1, 2);
|
||||
// Bottom Left Corner
|
||||
display->drawRect(bubbleX + 1, bottomY - 1, 2, 1);
|
||||
display->drawRect(bubbleX + 1, bottomY - 2, 1, 2);
|
||||
// Knock the corners off to make a bubble
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(bubbleX + bubbleW, topY, 1, 1);
|
||||
display->drawRect(bubbleX + bubbleW, bottomY, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
bubbleX = x;
|
||||
}
|
||||
if (bubbleX < x)
|
||||
bubbleX = x;
|
||||
if (bubbleX + bubbleW > rightEdge)
|
||||
bubbleW = std::max(1, rightEdge - bubbleX);
|
||||
|
||||
// Draw rounded rectangle bubble
|
||||
if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) {
|
||||
const int r = BUBBLE_RADIUS;
|
||||
const int bx = bubbleX;
|
||||
const int by = topY;
|
||||
const int bw = bubbleW;
|
||||
const int bh = bubbleH;
|
||||
|
||||
// Draw the 4 corner arcs using drawCircleQuads
|
||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||
|
||||
// Draw the 4 edges between corners
|
||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||
} else if (bubbleW > 1 && bubbleH > 1) {
|
||||
// Fallback to simple rectangle for very small bubbles
|
||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end if (showBubbles)
|
||||
|
||||
// Render visible lines
|
||||
int lineY = yOffset;
|
||||
@@ -916,11 +911,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
int headerX;
|
||||
if (isMine[i]) {
|
||||
// push header left to avoid overlap with scrollbar
|
||||
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT;
|
||||
headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0);
|
||||
if (headerX < LEFT_MARGIN)
|
||||
headerX = LEFT_MARGIN;
|
||||
} else {
|
||||
headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT;
|
||||
headerX = x + textIndent;
|
||||
}
|
||||
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
||||
|
||||
@@ -960,14 +955,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
if (isMine[i]) {
|
||||
// Calculate actual rendered width including emotes
|
||||
int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes);
|
||||
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT;
|
||||
int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0);
|
||||
if (rightX < LEFT_MARGIN)
|
||||
rightX = LEFT_MARGIN;
|
||||
|
||||
drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
|
||||
} else {
|
||||
drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes,
|
||||
numEmotes);
|
||||
drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#include "graphics/niche/InkHUD/Tile.h"
|
||||
#include <cstdint>
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./Applet.h"
|
||||
@@ -787,16 +785,6 @@ 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
|
||||
|
||||
@@ -48,27 +48,37 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
{
|
||||
// Clear the region beneath the tile, including the border
|
||||
// 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
|
||||
// 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(0, 0, width(), height(), WHITE);
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
|
||||
// =====================
|
||||
// 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 = 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
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
@@ -79,13 +89,12 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
int16_t sliceL = bodyL + slicePad;
|
||||
const 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
|
||||
sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
|
||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
|
||||
@@ -510,10 +510,10 @@ void InkHUD::WindowManager::placeSystemTiles()
|
||||
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
|
||||
inkhud->getSystemApplet("BatteryIcon")
|
||||
->getTile()
|
||||
->setRegion(inkhud->width() - batteryIconWidth - 1, // x
|
||||
1, // y
|
||||
batteryIconWidth + 1, // width
|
||||
batteryIconHeight + 2); // height
|
||||
->setRegion(inkhud->width() - batteryIconWidth, // x
|
||||
2, // y
|
||||
batteryIconWidth, // width
|
||||
batteryIconHeight); // height
|
||||
|
||||
// Note: the tiles of placeholder and menu applets are manipulated specially
|
||||
// - menuApplet borrows user tiles
|
||||
|
||||
95
src/main.cpp
95
src/main.cpp
@@ -7,13 +7,13 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "PowerMon.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "ReliableRouter.h"
|
||||
#include "airtime.h"
|
||||
#include "buzz.h"
|
||||
#include "power/PowerHAL.h"
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "Led.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "Throttle.h"
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <Wire.h>
|
||||
#endif
|
||||
#include "detect/einkScan.h"
|
||||
#include "graphics/RAKled.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "main.h"
|
||||
#include "mesh/generated/meshtastic/config.pb.h"
|
||||
@@ -192,8 +193,6 @@ bool kb_found = false;
|
||||
// global bool to record that on-screen keyboard (OSK) is present
|
||||
bool osk_found = false;
|
||||
|
||||
unsigned long last_listen = 0;
|
||||
|
||||
// The I2C address of the RTC Module (if found)
|
||||
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
|
||||
// The I2C address of the Accelerometer (if found)
|
||||
@@ -243,10 +242,28 @@ const char *getDeviceName()
|
||||
return name;
|
||||
}
|
||||
|
||||
// TODO remove from main.cpp
|
||||
static int32_t ledBlinker()
|
||||
{
|
||||
// Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if
|
||||
// config.device.led_heartbeat_disabled is changed
|
||||
if (config.device.led_heartbeat_disabled)
|
||||
return 1000;
|
||||
|
||||
static bool ledOn;
|
||||
ledOn ^= 1;
|
||||
|
||||
ledBlink.set(ledOn);
|
||||
|
||||
// have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that
|
||||
return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000);
|
||||
}
|
||||
|
||||
uint32_t timeLastPowered = 0;
|
||||
|
||||
static Periodic *ledPeriodic;
|
||||
static OSThread *powerFSMthread;
|
||||
OSThread *ambientLightingThread;
|
||||
static OSThread *ambientLightingThread;
|
||||
|
||||
RadioInterface *rIf = NULL;
|
||||
#ifdef ARCH_PORTDUINO
|
||||
@@ -282,16 +299,21 @@ void earlyInitVariant() {}
|
||||
// blink user led in 3 flashes sequence to indicate what is happening
|
||||
void waitUntilPowerLevelSafe()
|
||||
{
|
||||
|
||||
#ifdef LED_PIN
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
#endif
|
||||
|
||||
while (powerHAL_isPowerLevelSafe() == false) {
|
||||
|
||||
#ifdef LED_POWER
|
||||
#ifdef LED_PIN
|
||||
|
||||
// 3x: blink for 300 ms, pause for 300 ms
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||||
digitalWrite(LED_PIN, LED_STATE_ON);
|
||||
delay(300);
|
||||
digitalWrite(LED_POWER, LED_STATE_OFF);
|
||||
digitalWrite(LED_PIN, LED_STATE_OFF);
|
||||
delay(300);
|
||||
}
|
||||
#endif
|
||||
@@ -315,11 +337,6 @@ void setup()
|
||||
// initialize power HAL layer as early as possible
|
||||
powerHAL_init();
|
||||
|
||||
#ifdef LED_POWER
|
||||
pinMode(LED_POWER, OUTPUT);
|
||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||||
#endif
|
||||
|
||||
// prevent booting if device is in power failure mode
|
||||
// boot sequence will follow when battery level raises to safe mode
|
||||
waitUntilPowerLevelSafe();
|
||||
@@ -332,6 +349,11 @@ void setup()
|
||||
digitalWrite(PIN_POWER_EN, HIGH);
|
||||
#endif
|
||||
|
||||
#ifdef LED_POWER
|
||||
pinMode(LED_POWER, OUTPUT);
|
||||
digitalWrite(LED_POWER, LED_STATE_ON);
|
||||
#endif
|
||||
|
||||
#ifdef LED_NOTIFICATION
|
||||
pinMode(LED_NOTIFICATION, OUTPUT);
|
||||
digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON);
|
||||
@@ -344,7 +366,11 @@ void setup()
|
||||
|
||||
#ifdef BLE_LED
|
||||
pinMode(BLE_LED, OUTPUT);
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
concurrency::hasBeenSetup = true;
|
||||
@@ -462,6 +488,14 @@ void setup()
|
||||
|
||||
OSThread::setup();
|
||||
|
||||
// TODO make this ifdef based on defined pins and move from main.cpp
|
||||
#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
|
||||
// The ThinkNodes have their own blink logic
|
||||
// ledPeriodic = new Periodic("Blink", elecrowLedBlinker);
|
||||
#else
|
||||
ledPeriodic = new Periodic("Blink", ledBlinker);
|
||||
#endif
|
||||
|
||||
fsInit();
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
@@ -679,13 +713,19 @@ 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
|
||||
setupSDCard();
|
||||
#endif
|
||||
|
||||
// LED init
|
||||
|
||||
#ifdef LED_PIN
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now
|
||||
#endif
|
||||
|
||||
// Hello
|
||||
printInfo();
|
||||
#ifdef BUILD_EPOCH
|
||||
@@ -793,7 +833,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);
|
||||
@@ -888,6 +928,13 @@ void setup()
|
||||
service = new MeshService();
|
||||
service->init();
|
||||
|
||||
// Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it
|
||||
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
|
||||
#ifndef HAS_PHYSICAL_KEYBOARD
|
||||
osk_found = true;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Now that the mesh service is created, create any modules
|
||||
setupModules();
|
||||
|
||||
@@ -919,6 +966,12 @@ void setup()
|
||||
setupNicheGraphics();
|
||||
#endif
|
||||
|
||||
#ifdef LED_PIN
|
||||
// Turn LED off after boot, if heartbeat by config
|
||||
if (config.device.led_heartbeat_disabled)
|
||||
digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
|
||||
#endif
|
||||
|
||||
// Do this after service.init (because that clears error_code)
|
||||
#ifdef HAS_PMU
|
||||
if (!pmu_found)
|
||||
@@ -972,12 +1025,6 @@ void setup()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
|
||||
#ifndef HAS_PHYSICAL_KEYBOARD
|
||||
osk_found = true;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
// Start web server thread.
|
||||
webServerThread = new WebServerThread();
|
||||
@@ -1121,12 +1168,6 @@ void loop()
|
||||
#endif
|
||||
power->powerCommandsCheck();
|
||||
|
||||
if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) &&
|
||||
!(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) {
|
||||
RadioLibInterface::instance->startReceive();
|
||||
LOG_DEBUG("attempting AGC reset");
|
||||
}
|
||||
|
||||
#ifdef DEBUG_STACK
|
||||
static uint32_t lastPrint = 0;
|
||||
if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) {
|
||||
|
||||
@@ -33,7 +33,6 @@ extern ScanI2C::DeviceAddress cardkb_found;
|
||||
extern uint8_t kb_model;
|
||||
extern bool kb_found;
|
||||
extern bool osk_found;
|
||||
extern unsigned long last_listen;
|
||||
extern ScanI2C::DeviceAddress rtc_found;
|
||||
extern ScanI2C::DeviceAddress accelerometer_found;
|
||||
extern ScanI2C::FoundDevice rgb_found;
|
||||
|
||||
@@ -574,6 +574,10 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
|
||||
#endif
|
||||
|
||||
#if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200)
|
||||
config.display.enable_message_bubbles = true;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_DEVICE_ROLE
|
||||
// Restrict ROUTER*, LOST AND FOUND roles for security reasons
|
||||
if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER,
|
||||
@@ -1404,15 +1408,6 @@ 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
|
||||
}
|
||||
@@ -1553,7 +1548,6 @@ 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);
|
||||
|
||||
@@ -514,8 +514,6 @@ void RadioLibInterface::handleReceiveInterrupt()
|
||||
|
||||
void RadioLibInterface::startReceive()
|
||||
{
|
||||
// Note the updated timestamp, to avoid unneeded AGC resets
|
||||
last_listen = millis();
|
||||
isReceiving = true;
|
||||
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);
|
||||
}
|
||||
|
||||
@@ -505,6 +505,8 @@ typedef struct _meshtastic_Config_DisplayConfig {
|
||||
/* If false (default), the device will use short names for various display screens.
|
||||
If true, node names will show in long format */
|
||||
bool use_long_node_name;
|
||||
/* If true, the device will display message bubbles on screen. */
|
||||
bool enable_message_bubbles;
|
||||
} meshtastic_Config_DisplayConfig;
|
||||
|
||||
/* Lora Config */
|
||||
@@ -732,7 +734,7 @@ extern "C" {
|
||||
#define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
|
||||
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0}
|
||||
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
|
||||
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||
@@ -743,7 +745,7 @@ extern "C" {
|
||||
#define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
|
||||
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0}
|
||||
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
|
||||
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||
@@ -811,6 +813,7 @@ extern "C" {
|
||||
#define meshtastic_Config_DisplayConfig_compass_orientation_tag 11
|
||||
#define meshtastic_Config_DisplayConfig_use_12h_clock_tag 12
|
||||
#define meshtastic_Config_DisplayConfig_use_long_node_name_tag 13
|
||||
#define meshtastic_Config_DisplayConfig_enable_message_bubbles_tag 14
|
||||
#define meshtastic_Config_LoRaConfig_use_preset_tag 1
|
||||
#define meshtastic_Config_LoRaConfig_modem_preset_tag 2
|
||||
#define meshtastic_Config_LoRaConfig_bandwidth_tag 3
|
||||
@@ -957,7 +960,8 @@ X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \
|
||||
X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \
|
||||
X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) \
|
||||
X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) \
|
||||
X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13)
|
||||
X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) \
|
||||
X(a, STATIC, SINGULAR, BOOL, enable_message_bubbles, 14)
|
||||
#define meshtastic_Config_DisplayConfig_CALLBACK NULL
|
||||
#define meshtastic_Config_DisplayConfig_DEFAULT NULL
|
||||
|
||||
@@ -1035,7 +1039,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
|
||||
#define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size
|
||||
#define meshtastic_Config_BluetoothConfig_size 10
|
||||
#define meshtastic_Config_DeviceConfig_size 100
|
||||
#define meshtastic_Config_DisplayConfig_size 34
|
||||
#define meshtastic_Config_DisplayConfig_size 36
|
||||
#define meshtastic_Config_LoRaConfig_size 85
|
||||
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
|
||||
#define meshtastic_Config_NetworkConfig_size 204
|
||||
|
||||
@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
||||
#define meshtastic_BackupPreferences_size 2362
|
||||
#define meshtastic_BackupPreferences_size 2364
|
||||
#define meshtastic_ChannelFile_size 718
|
||||
#define meshtastic_DeviceState_size 1737
|
||||
#define meshtastic_NodeInfoLite_size 196
|
||||
|
||||
@@ -193,7 +193,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
|
||||
#define meshtastic_LocalConfig_size 749
|
||||
#define meshtastic_LocalConfig_size 751
|
||||
#define meshtastic_LocalModuleConfig_size 758
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#if HAS_WIFI
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#endif
|
||||
#include "Led.h"
|
||||
#include "SPILock.h"
|
||||
#include "power.h"
|
||||
#include "serialization/JSON.h"
|
||||
@@ -91,6 +92,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
|
||||
ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);
|
||||
|
||||
ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks);
|
||||
ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED);
|
||||
ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport);
|
||||
ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes);
|
||||
ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic);
|
||||
@@ -108,6 +110,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
|
||||
secureServer->registerNode(nodeRestart);
|
||||
secureServer->registerNode(nodeFormUpload);
|
||||
secureServer->registerNode(nodeJsonScanNetworks);
|
||||
secureServer->registerNode(nodeJsonBlinkLED);
|
||||
secureServer->registerNode(nodeJsonFsBrowseStatic);
|
||||
secureServer->registerNode(nodeJsonDelete);
|
||||
secureServer->registerNode(nodeJsonReport);
|
||||
@@ -130,6 +133,7 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
|
||||
insecureServer->registerNode(nodeRestart);
|
||||
insecureServer->registerNode(nodeFormUpload);
|
||||
insecureServer->registerNode(nodeJsonScanNetworks);
|
||||
insecureServer->registerNode(nodeJsonBlinkLED);
|
||||
insecureServer->registerNode(nodeJsonFsBrowseStatic);
|
||||
insecureServer->registerNode(nodeJsonDelete);
|
||||
insecureServer->registerNode(nodeJsonReport);
|
||||
@@ -900,6 +904,45 @@ void handleRestart(HTTPRequest *req, HTTPResponse *res)
|
||||
webServerThread->requestRestart = (millis() / 1000) + 5;
|
||||
}
|
||||
|
||||
void handleBlinkLED(HTTPRequest *req, HTTPResponse *res)
|
||||
{
|
||||
res->setHeader("Content-Type", "application/json");
|
||||
res->setHeader("Access-Control-Allow-Origin", "*");
|
||||
res->setHeader("Access-Control-Allow-Methods", "POST");
|
||||
|
||||
ResourceParameters *params = req->getParams();
|
||||
std::string blink_target;
|
||||
|
||||
if (!params->getQueryParameter("blink_target", blink_target)) {
|
||||
// if no blink_target was supplied in the URL parameters of the
|
||||
// POST request, then assume we should blink the LED
|
||||
blink_target = "LED";
|
||||
}
|
||||
|
||||
if (blink_target == "LED") {
|
||||
uint8_t count = 10;
|
||||
while (count > 0) {
|
||||
ledBlink.set(true);
|
||||
delay(50);
|
||||
ledBlink.set(false);
|
||||
delay(50);
|
||||
count = count - 1;
|
||||
}
|
||||
} else {
|
||||
#if HAS_SCREEN
|
||||
if (screen)
|
||||
screen->blink();
|
||||
#endif
|
||||
}
|
||||
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
}
|
||||
|
||||
void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
||||
{
|
||||
res->setHeader("Content-Type", "application/json");
|
||||
|
||||
@@ -11,6 +11,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res);
|
||||
void handleScanNetworks(HTTPRequest *req, HTTPResponse *res);
|
||||
void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res);
|
||||
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res);
|
||||
void handleBlinkLED(HTTPRequest *req, HTTPResponse *res);
|
||||
void handleReport(HTTPRequest *req, HTTPResponse *res);
|
||||
void handleNodes(HTTPRequest *req, HTTPResponse *res);
|
||||
void handleUpdateFs(HTTPRequest *req, HTTPResponse *res);
|
||||
|
||||
@@ -22,10 +22,14 @@
|
||||
class UdpMulticastHandler final
|
||||
{
|
||||
public:
|
||||
UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); }
|
||||
UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); }
|
||||
|
||||
void start()
|
||||
{
|
||||
if (isRunning) {
|
||||
LOG_DEBUG("UDP multicast already running");
|
||||
return;
|
||||
}
|
||||
if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) {
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
|
||||
LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3],
|
||||
@@ -34,13 +38,29 @@ class UdpMulticastHandler final
|
||||
LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str());
|
||||
#endif
|
||||
udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); });
|
||||
isRunning = true;
|
||||
} else {
|
||||
LOG_DEBUG("Failed to listen on UDP");
|
||||
}
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Stopping UDP multicast");
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
|
||||
udp.close();
|
||||
#endif
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
void onReceive(AsyncUDPPacket packet)
|
||||
{
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
size_t packetLength = packet.length();
|
||||
#if defined(ARCH_NRF52)
|
||||
IPAddress ip = packet.remoteIP();
|
||||
@@ -67,7 +87,7 @@ class UdpMulticastHandler final
|
||||
|
||||
bool onSend(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
if (!mp || !udp) {
|
||||
if (!isRunning || !mp || !udp) {
|
||||
return false;
|
||||
}
|
||||
#if defined(ARCH_NRF52)
|
||||
@@ -92,5 +112,6 @@ class UdpMulticastHandler final
|
||||
private:
|
||||
IPAddress udpIpAddress;
|
||||
AsyncUDP udp;
|
||||
bool isRunning;
|
||||
};
|
||||
#endif // HAS_UDP_MULTICAST
|
||||
@@ -391,6 +391,11 @@ static void WiFiEvent(WiFiEvent_t event)
|
||||
LOG_INFO("Disconnected from WiFi access point");
|
||||
#ifdef WIFI_LED
|
||||
digitalWrite(WIFI_LED, LOW);
|
||||
#endif
|
||||
#if HAS_UDP_MULTICAST
|
||||
if (udpHandler) {
|
||||
udpHandler->stop();
|
||||
}
|
||||
#endif
|
||||
if (!isReconnecting) {
|
||||
WiFi.disconnect(false, true);
|
||||
@@ -417,6 +422,11 @@ static void WiFiEvent(WiFiEvent_t event)
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
|
||||
LOG_INFO("Lost IP address and IP address is reset to 0");
|
||||
#if HAS_UDP_MULTICAST
|
||||
if (udpHandler) {
|
||||
udpHandler->stop();
|
||||
}
|
||||
#endif
|
||||
if (!isReconnecting) {
|
||||
WiFi.disconnect(false, true);
|
||||
syslog.disable();
|
||||
|
||||
@@ -106,4 +106,15 @@ const std::string vformat(const char *const zcFormat, ...)
|
||||
std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs);
|
||||
va_end(vaArgs);
|
||||
return std::string(zc.data(), iLen);
|
||||
}
|
||||
|
||||
size_t pb_string_length(const char *str, size_t max_len)
|
||||
{
|
||||
size_t len = 0;
|
||||
for (size_t i = 0; i < max_len; i++) {
|
||||
if (str[i] != '\0') {
|
||||
len = i + 1;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
@@ -35,4 +35,7 @@ bool isOneOf(int item, int count, ...);
|
||||
|
||||
const std::string vformat(const char *const zcFormat, ...);
|
||||
|
||||
// Get actual string length for nanopb char array fields.
|
||||
size_t pb_string_length(const char *str, size_t max_len);
|
||||
|
||||
#define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__)
|
||||
|
||||
@@ -643,6 +643,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
accelerometerThread->enabled = true;
|
||||
accelerometerThread->start();
|
||||
}
|
||||
#endif
|
||||
#ifdef LED_PIN
|
||||
// Turn LED off if heartbeat by config
|
||||
if (c.payload_variant.device.led_heartbeat_disabled) {
|
||||
digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON);
|
||||
}
|
||||
#endif
|
||||
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
|
||||
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
|
||||
@@ -899,11 +905,10 @@ 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, meshtastic_ModuleConfig_statusmessage_tag)) {
|
||||
if (!hasOpenEditTransaction &&
|
||||
!IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) {
|
||||
disableBluetooth();
|
||||
}
|
||||
|
||||
@@ -995,14 +1000,8 @@ 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, shouldReboot);
|
||||
saveChanges(SEGMENT_MODULECONFIG);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1181,11 +1180,6 @@ 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.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "mesh/compression/unishox2.h"
|
||||
#include "meshUtils.h"
|
||||
#include "meshtastic/atak.pb.h"
|
||||
|
||||
AtakPluginModule *atakPluginModule;
|
||||
@@ -70,16 +71,17 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
auto compressed = cloneTAKPacketData(t);
|
||||
compressed.is_compressed = true;
|
||||
if (t->has_contact) {
|
||||
auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign,
|
||||
sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_compress_lines(
|
||||
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
|
||||
compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Compressed callsign: %d bytes", length);
|
||||
length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
|
||||
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1,
|
||||
USX_PSET_DFLT, NULL);
|
||||
length = unishox2_compress_lines(
|
||||
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
|
||||
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -87,9 +89,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
LOG_DEBUG("Compressed device_callsign: %d bytes", length);
|
||||
}
|
||||
if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
|
||||
auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
|
||||
compressed.payload_variant.chat.message,
|
||||
sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_compress_lines(
|
||||
t->payload_variant.chat.message,
|
||||
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
|
||||
compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
|
||||
NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -98,9 +102,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to) {
|
||||
compressed.payload_variant.chat.has_to = true;
|
||||
length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
|
||||
compressed.payload_variant.chat.to,
|
||||
sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_compress_lines(
|
||||
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
|
||||
compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -110,9 +114,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to_callsign) {
|
||||
compressed.payload_variant.chat.has_to_callsign = true;
|
||||
length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
|
||||
compressed.payload_variant.chat.to_callsign,
|
||||
sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_compress_lines(
|
||||
t->payload_variant.chat.to_callsign,
|
||||
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
|
||||
compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1,
|
||||
USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
|
||||
return;
|
||||
@@ -134,18 +140,18 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
auto uncompressed = cloneTAKPacketData(t);
|
||||
uncompressed.is_compressed = false;
|
||||
if (t->has_contact) {
|
||||
auto length =
|
||||
unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign,
|
||||
sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_decompress_lines(
|
||||
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
|
||||
uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow contact.callsign. Bailing out");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("Decompressed callsign: %d bytes", length);
|
||||
|
||||
length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign),
|
||||
uncompressed.contact.device_callsign,
|
||||
sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_decompress_lines(
|
||||
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
|
||||
uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
|
||||
return;
|
||||
@@ -153,9 +159,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
LOG_DEBUG("Decompressed device_callsign: %d bytes", length);
|
||||
}
|
||||
if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) {
|
||||
auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message),
|
||||
uncompressed.payload_variant.chat.message,
|
||||
sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL);
|
||||
auto length = unishox2_decompress_lines(
|
||||
t->payload_variant.chat.message,
|
||||
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
|
||||
uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
|
||||
NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.message. Bailing out");
|
||||
return;
|
||||
@@ -164,9 +172,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to) {
|
||||
uncompressed.payload_variant.chat.has_to = true;
|
||||
length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to),
|
||||
uncompressed.payload_variant.chat.to,
|
||||
sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_decompress_lines(
|
||||
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
|
||||
uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.to. Bailing out");
|
||||
return;
|
||||
@@ -176,10 +184,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast
|
||||
|
||||
if (t->payload_variant.chat.has_to_callsign) {
|
||||
uncompressed.payload_variant.chat.has_to_callsign = true;
|
||||
length =
|
||||
unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign),
|
||||
uncompressed.payload_variant.chat.to_callsign,
|
||||
sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL);
|
||||
length = unishox2_decompress_lines(
|
||||
t->payload_variant.chat.to_callsign,
|
||||
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
|
||||
uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1,
|
||||
USX_PSET_DFLT, NULL);
|
||||
if (length < 0) {
|
||||
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
|
||||
return;
|
||||
|
||||
@@ -24,8 +24,24 @@
|
||||
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
#include <graphics/RAKled.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAS_LP5562
|
||||
#include <graphics/NomadStarLED.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NEOPIXEL
|
||||
#include <graphics/NeoPixel.h>
|
||||
#endif
|
||||
|
||||
#ifdef UNPHONE
|
||||
#include "unPhone.h"
|
||||
extern unPhone unphone;
|
||||
#endif
|
||||
|
||||
#if defined(HAS_RGB_LED)
|
||||
#include "AmbientLightingThread.h"
|
||||
uint8_t red = 0;
|
||||
uint8_t green = 0;
|
||||
uint8_t blue = 0;
|
||||
@@ -107,6 +123,32 @@ int32_t ExternalNotificationModule::runOnce()
|
||||
green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7
|
||||
blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7
|
||||
white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0;
|
||||
#ifdef HAS_NCP5623
|
||||
if (rgb_found.type == ScanI2C::NCP5623) {
|
||||
rgb.setColor(red, green, blue);
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
if (rgb_found.type == ScanI2C::LP5562) {
|
||||
rgbw.setColor(red, green, blue, white);
|
||||
}
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
|
||||
analogWrite(RGBLED_GREEN, 255 - green);
|
||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||
#elif defined(RGBLED_RED)
|
||||
analogWrite(RGBLED_RED, red);
|
||||
analogWrite(RGBLED_GREEN, green);
|
||||
analogWrite(RGBLED_BLUE, blue);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
||||
pixels.show();
|
||||
#endif
|
||||
#ifdef UNPHONE
|
||||
unphone.rgb(red, green, blue);
|
||||
#endif
|
||||
if (ascending) { // fade in
|
||||
brightnessIndex++;
|
||||
if (brightnessIndex == (sizeof(brightnessValues) - 1)) {
|
||||
@@ -213,9 +255,34 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
|
||||
blue = 0;
|
||||
white = 0;
|
||||
}
|
||||
ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue);
|
||||
#endif
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
if (rgb_found.type == ScanI2C::NCP5623) {
|
||||
rgb.setColor(red, green, blue);
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
if (rgb_found.type == ScanI2C::LP5562) {
|
||||
rgbw.setColor(red, green, blue, white);
|
||||
}
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic
|
||||
analogWrite(RGBLED_GREEN, 255 - green);
|
||||
analogWrite(RGBLED_BLUE, 255 - blue);
|
||||
#elif defined(RGBLED_RED)
|
||||
analogWrite(RGBLED_RED, red);
|
||||
analogWrite(RGBLED_GREEN, green);
|
||||
analogWrite(RGBLED_BLUE, blue);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT);
|
||||
pixels.show();
|
||||
#endif
|
||||
#ifdef UNPHONE
|
||||
unphone.rgb(red, green, blue);
|
||||
#endif
|
||||
#ifdef HAS_DRV2605
|
||||
if (on) {
|
||||
drv.go();
|
||||
@@ -340,6 +407,33 @@ ExternalNotificationModule::ExternalNotificationModule()
|
||||
LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio);
|
||||
}
|
||||
}
|
||||
#ifdef HAS_NCP5623
|
||||
if (rgb_found.type == ScanI2C::NCP5623) {
|
||||
rgb.begin();
|
||||
rgb.setCurrent(10);
|
||||
}
|
||||
#endif
|
||||
#ifdef HAS_LP5562
|
||||
if (rgb_found.type == ScanI2C::LP5562) {
|
||||
rgbw.begin();
|
||||
rgbw.setCurrent(20);
|
||||
}
|
||||
#endif
|
||||
#ifdef RGBLED_RED
|
||||
pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins
|
||||
pinMode(RGBLED_GREEN, OUTPUT);
|
||||
pinMode(RGBLED_BLUE, OUTPUT);
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed
|
||||
analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off
|
||||
analogWrite(RGBLED_BLUE, 255);
|
||||
#endif
|
||||
#ifdef HAS_NEOPIXEL
|
||||
pixels.begin(); // Initialise the pixel(s)
|
||||
pixels.clear(); // Set all pixel colors to 'off'
|
||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||
#endif
|
||||
} else {
|
||||
LOG_INFO("External Notification Module Disabled");
|
||||
disable();
|
||||
|
||||
@@ -5,11 +5,6 @@
|
||||
#include "configuration.h"
|
||||
#include "input/InputBroker.h"
|
||||
|
||||
#ifdef HAS_RGB_LED
|
||||
#include "AmbientLightingThread.h"
|
||||
extern AmbientLightingThread *ambientLightingThread;
|
||||
#endif
|
||||
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
#include <NonBlockingRtttl.h>
|
||||
#else
|
||||
|
||||
@@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
// generate nonce
|
||||
updateState();
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;)
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||
return false;
|
||||
}
|
||||
currentNonce = random();
|
||||
@@ -259,7 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_USER;
|
||||
IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);)
|
||||
IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "buzz/BuzzerFeedbackThread.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#endif
|
||||
#include "modules/StatusLEDModule.h"
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
#include "ReplyBotModule.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
@@ -93,9 +90,6 @@
|
||||
#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"
|
||||
@@ -112,10 +106,10 @@ void setupModules()
|
||||
buzzerFeedbackThread = new BuzzerFeedbackThread();
|
||||
}
|
||||
#endif
|
||||
#if defined(LED_CHARGE) || defined(LED_PAIRING)
|
||||
statusLEDModule = new StatusLEDModule();
|
||||
#if !MESHTASTIC_EXCLUDE_REPLYBOT
|
||||
new ReplyBotModule();
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminModule = new AdminModule();
|
||||
#endif
|
||||
@@ -156,9 +150,6 @@ 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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "PowerStressModule.h"
|
||||
#include "Led.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
@@ -77,12 +78,10 @@ int32_t PowerStressModule::runOnce()
|
||||
|
||||
switch (p.cmd) {
|
||||
case meshtastic_PowerStressMessage_Opcode_LED_ON:
|
||||
// FIXME - implement
|
||||
// ledForceOn.set(true);
|
||||
ledForceOn.set(true);
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_LED_OFF:
|
||||
// FIXME - implement
|
||||
// ledForceOn.set(false);
|
||||
ledForceOn.set(false);
|
||||
break;
|
||||
case meshtastic_PowerStressMessage_Opcode_GPS_ON:
|
||||
// FIXME - implement
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
#include "configuration.h"
|
||||
#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 per‑sender 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 "Channels.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ReplyBotModule.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 rate‑limited 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 null‑terminated 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 non‑zero 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 end‑of‑string 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
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
#include "configuration.h"
|
||||
#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
|
||||
@@ -13,10 +13,8 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule")
|
||||
{
|
||||
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
|
||||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
if (inputBroker)
|
||||
inputObserver.observe(inputBroker);
|
||||
#endif
|
||||
}
|
||||
|
||||
int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
|
||||
@@ -64,22 +62,19 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
|
||||
int StatusLEDModule::handleInputEvent(const InputEvent *event)
|
||||
{
|
||||
lastUserbuttonTime = millis();
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
int32_t StatusLEDModule::runOnce()
|
||||
{
|
||||
my_interval = 1000;
|
||||
|
||||
if (power_state == charging) {
|
||||
#ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING
|
||||
CHARGE_LED_state = !CHARGE_LED_state;
|
||||
#endif
|
||||
} else if (power_state == charged) {
|
||||
CHARGE_LED_state = LED_STATE_ON;
|
||||
} else if (power_state == critical) {
|
||||
@@ -99,15 +94,7 @@ int32_t StatusLEDModule::runOnce()
|
||||
}
|
||||
|
||||
} else {
|
||||
if (doing_fast_blink) {
|
||||
CHARGE_LED_state = LED_STATE_OFF;
|
||||
doing_fast_blink = false;
|
||||
my_interval = 999;
|
||||
} else {
|
||||
CHARGE_LED_state = LED_STATE_ON;
|
||||
doing_fast_blink = true;
|
||||
my_interval = 1;
|
||||
}
|
||||
CHARGE_LED_state = LED_STATE_OFF;
|
||||
}
|
||||
|
||||
if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) {
|
||||
@@ -125,11 +112,6 @@ int32_t StatusLEDModule::runOnce()
|
||||
PAIRING_LED_state = LED_STATE_ON;
|
||||
}
|
||||
|
||||
// Override if disabled in config
|
||||
if (config.device.led_heartbeat_disabled) {
|
||||
CHARGE_LED_state = LED_STATE_OFF;
|
||||
}
|
||||
#ifdef Battery_LED_1
|
||||
bool chargeIndicatorLED1 = LED_STATE_OFF;
|
||||
bool chargeIndicatorLED2 = LED_STATE_OFF;
|
||||
bool chargeIndicatorLED3 = LED_STATE_OFF;
|
||||
@@ -144,38 +126,14 @@ int32_t StatusLEDModule::runOnce()
|
||||
if (powerStatus && powerStatus->getBatteryChargePercent() >= 75)
|
||||
chargeIndicatorLED4 = LED_STATE_ON;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(HAS_PMU)
|
||||
if (pmu_found && PMU) {
|
||||
// blink the axp led
|
||||
PMU->setChargingLedMode(CHARGE_LED_state ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PCA_LED_POWER
|
||||
io.digitalWrite(PCA_LED_POWER, CHARGE_LED_state);
|
||||
#endif
|
||||
#ifdef PCA_LED_ENABLE
|
||||
io.digitalWrite(PCA_LED_ENABLE, CHARGE_LED_state);
|
||||
#endif
|
||||
#ifdef LED_POWER
|
||||
digitalWrite(LED_POWER, CHARGE_LED_state);
|
||||
#ifdef LED_CHARGE
|
||||
digitalWrite(LED_CHARGE, CHARGE_LED_state);
|
||||
#endif
|
||||
#ifdef LED_PAIRING
|
||||
digitalWrite(LED_PAIRING, PAIRING_LED_state);
|
||||
#endif
|
||||
|
||||
#ifdef RGB_LED_POWER
|
||||
if (!config.device.led_heartbeat_disabled) {
|
||||
if (CHARGE_LED_state == LED_STATE_ON) {
|
||||
ambientLightingThread->setLighting(10, 255, 0, 0);
|
||||
} else {
|
||||
ambientLightingThread->setLighting(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Battery_LED_1
|
||||
digitalWrite(Battery_LED_1, chargeIndicatorLED1);
|
||||
#endif
|
||||
@@ -191,43 +149,3 @@ int32_t StatusLEDModule::runOnce()
|
||||
|
||||
return (my_interval);
|
||||
}
|
||||
|
||||
void StatusLEDModule::setPowerLED(bool LEDon)
|
||||
{
|
||||
|
||||
#if defined(HAS_PMU)
|
||||
if (pmu_found && PMU) {
|
||||
// blink the axp led
|
||||
PMU->setChargingLedMode(LEDon ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF);
|
||||
}
|
||||
#endif
|
||||
if (LEDon)
|
||||
LEDon = LED_STATE_ON;
|
||||
else
|
||||
LEDon = LED_STATE_OFF;
|
||||
#ifdef PCA_LED_POWER
|
||||
io.digitalWrite(PCA_LED_POWER, LEDon);
|
||||
#endif
|
||||
#ifdef PCA_LED_ENABLE
|
||||
io.digitalWrite(PCA_LED_ENABLE, LEDon);
|
||||
#endif
|
||||
#ifdef LED_POWER
|
||||
digitalWrite(LED_POWER, LEDon);
|
||||
#endif
|
||||
#ifdef LED_PAIRING
|
||||
digitalWrite(LED_PAIRING, LEDon);
|
||||
#endif
|
||||
|
||||
#ifdef Battery_LED_1
|
||||
digitalWrite(Battery_LED_1, LEDon);
|
||||
#endif
|
||||
#ifdef Battery_LED_2
|
||||
digitalWrite(Battery_LED_2, LEDon);
|
||||
#endif
|
||||
#ifdef Battery_LED_3
|
||||
digitalWrite(Battery_LED_3, LEDon);
|
||||
#endif
|
||||
#ifdef Battery_LED_4
|
||||
digitalWrite(Battery_LED_4, LEDon);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -5,14 +5,10 @@
|
||||
#include "PowerStatus.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "input/InputBroker.h"
|
||||
#endif
|
||||
|
||||
class StatusLEDModule : private concurrency::OSThread
|
||||
{
|
||||
bool slowTrack = false;
|
||||
@@ -21,11 +17,8 @@ class StatusLEDModule : private concurrency::OSThread
|
||||
StatusLEDModule();
|
||||
|
||||
int handleStatusUpdate(const meshtastic::Status *);
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
int handleInputEvent(const InputEvent *arg);
|
||||
#endif
|
||||
|
||||
void setPowerLED(bool);
|
||||
int handleInputEvent(const InputEvent *arg);
|
||||
|
||||
protected:
|
||||
unsigned int my_interval = 1000; // interval in millisconds
|
||||
@@ -35,10 +28,8 @@ class StatusLEDModule : private concurrency::OSThread
|
||||
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
|
||||
CallbackObserver<StatusLEDModule, const meshtastic::Status *> powerStatusObserver =
|
||||
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
CallbackObserver<StatusLEDModule, const InputEvent *> inputObserver =
|
||||
CallbackObserver<StatusLEDModule, const InputEvent *>(this, &StatusLEDModule::handleInputEvent);
|
||||
#endif
|
||||
|
||||
private:
|
||||
bool CHARGE_LED_state = LED_STATE_OFF;
|
||||
@@ -59,7 +50,3 @@ class StatusLEDModule : private concurrency::OSThread
|
||||
};
|
||||
|
||||
extern StatusLEDModule *statusLEDModule;
|
||||
#ifdef RGB_LED_POWER
|
||||
#include "AmbientLightingThread.h"
|
||||
extern AmbientLightingThread *ambientLightingThread;
|
||||
#endif
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#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
|
||||
@@ -1,35 +0,0 @@
|
||||
#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
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "Sensor/AddI2CSensorTemplate.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
@@ -20,7 +19,9 @@
|
||||
#include <Throttle.h>
|
||||
|
||||
// Sensors
|
||||
#include "Sensor/AddI2CSensorTemplate.h"
|
||||
#include "Sensor/PMSA003ISensor.h"
|
||||
#include "Sensor/SEN5XSensor.h"
|
||||
|
||||
void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
{
|
||||
@@ -42,6 +43,7 @@ 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()
|
||||
@@ -85,10 +87,27 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
}
|
||||
|
||||
// Wake up the sensors that need it
|
||||
LOG_INFO("Waking up sensors");
|
||||
LOG_INFO("Waking up sensors...");
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
if (!sensor->isActive()) {
|
||||
return sensor->wakeUp();
|
||||
if (!sensor->canSleep()) {
|
||||
LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName);
|
||||
} else if (((lastSentToMesh == 0) ||
|
||||
!Throttle::isWithinTimespanMs(lastSentToMesh - sensor->wakeUpTimeMs(),
|
||||
Default::getConfiguredOrDefaultMsScaled(
|
||||
moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil()) {
|
||||
if (!sensor->isActive()) {
|
||||
LOG_DEBUG("Waking up: %s", sensor->sensorName);
|
||||
return sensor->wakeUp();
|
||||
} else {
|
||||
int32_t pendingForReadyMs = sensor->pendingForReadyMs();
|
||||
LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs);
|
||||
if (pendingForReadyMs) {
|
||||
return pendingForReadyMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +128,18 @@ int32_t AirQualityTelemetryModule::runOnce()
|
||||
}
|
||||
|
||||
// Send to sleep sensors that consume power
|
||||
LOG_INFO("Sending sensors to sleep");
|
||||
LOG_DEBUG("Sending sensors to sleep");
|
||||
for (TelemetrySensor *sensor : sensors) {
|
||||
sensor->sleep();
|
||||
if (sensor->isActive() && sensor->canSleep()) {
|
||||
if (sensor->wakeUpTimeMs() < Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs,
|
||||
numOnlineNodes)) {
|
||||
LOG_DEBUG("Disabling %s until next period", sensor->sensorName);
|
||||
sensor->sleep();
|
||||
} else {
|
||||
LOG_DEBUG("Sensor stays enabled due to warm up period");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return min(sendToPhoneIntervalMs, result);
|
||||
@@ -158,8 +186,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
const auto &m = telemetry.variant.air_quality_metrics;
|
||||
|
||||
// Check if any telemetry field has valid data
|
||||
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental ||
|
||||
m.has_pm25_environmental || m.has_pm100_environmental;
|
||||
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard;
|
||||
|
||||
if (!hasAny) {
|
||||
display->drawString(x, currentY, "No Telemetry");
|
||||
@@ -225,9 +252,10 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
|
||||
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
|
||||
t->variant.air_quality_metrics.pm100_standard);
|
||||
|
||||
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
|
||||
t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
|
||||
t->variant.air_quality_metrics.pm100_environmental);
|
||||
// TODO - Decide what to do with these
|
||||
// LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
|
||||
// t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
|
||||
// t->variant.air_quality_metrics.pm100_environmental);
|
||||
#endif
|
||||
// release previous packet before occupying a new spot
|
||||
if (lastMeasurementPacket != nullptr)
|
||||
@@ -247,10 +275,8 @@ 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_INFO("Reading AQ sensors");
|
||||
LOG_DEBUG("Reading %s", sensor->sensorName);
|
||||
valid = valid && sensor->getMetrics(m);
|
||||
hasSensor = true;
|
||||
}
|
||||
@@ -291,12 +317,14 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
|
||||
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
|
||||
m.time = getTime();
|
||||
|
||||
if (getAirQualityTelemetry(&m)) {
|
||||
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \
|
||||
pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
|
||||
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard,
|
||||
m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental,
|
||||
m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental);
|
||||
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);
|
||||
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(m);
|
||||
p->to = dest;
|
||||
@@ -331,6 +359,20 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
LOG_DEBUG("Start next execution in 5s, then sleep");
|
||||
setIntervalFromNow(FIVE_SECONDS_MS);
|
||||
}
|
||||
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
|
||||
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
|
||||
notification->level = meshtastic_LogRecord_Level_INFO;
|
||||
notification->time = getValidTime(RTCQualityFromNet);
|
||||
sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
|
||||
Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
|
||||
default_telemetry_broadcast_interval_secs) /
|
||||
1000U);
|
||||
service->sendClientNotification(notification);
|
||||
sleepOnNextExecution = true;
|
||||
LOG_DEBUG("Start next execution in 5s, then sleep");
|
||||
setIntervalFromNow(FIVE_SECONDS_MS);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,26 +21,29 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
_bus = bus;
|
||||
_address = dev->address.address;
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
|
||||
if (!currentClock) {
|
||||
LOG_WARN("PMSA003I can't be used at this clock speed");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#ifdef PMSA003I_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* PMSA003I_I2C_CLOCK_SPEED */
|
||||
|
||||
_bus->beginTransmission(_address);
|
||||
if (_bus->endTransmission() != 0) {
|
||||
LOG_WARN("PMSA003I not found on I2C at 0x12");
|
||||
LOG_WARN("%s not found on I2C at 0x12", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus);
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
status = 1;
|
||||
LOG_INFO("PMSA003I Enabled");
|
||||
LOG_INFO("%s Enabled", sensorName);
|
||||
|
||||
initI2CSensor();
|
||||
return true;
|
||||
@@ -49,30 +52,37 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
if (!isActive()) {
|
||||
LOG_WARN("PMSA003I is not active");
|
||||
LOG_WARN("Can't get metrics. %s is not active", sensorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
|
||||
#endif
|
||||
#ifdef PMSA003I_I2C_CLOCK_SPEED
|
||||
#ifdef CAN_RECLOCK_I2C
|
||||
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false);
|
||||
#elif !HAS_SCREEN
|
||||
reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true);
|
||||
#else
|
||||
LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName);
|
||||
return false;
|
||||
#endif /* CAN_RECLOCK_I2C */
|
||||
#endif /* PMSA003I_I2C_CLOCK_SPEED */
|
||||
|
||||
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
|
||||
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
|
||||
LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available());
|
||||
LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available());
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus);
|
||||
#endif
|
||||
|
||||
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
|
||||
buffer[i] = _bus->read();
|
||||
}
|
||||
|
||||
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
|
||||
reClockI2C(currentClock, _bus, false);
|
||||
#endif
|
||||
|
||||
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
|
||||
LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
|
||||
LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -86,7 +96,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
|
||||
|
||||
if (computedChecksum != receivedChecksum) {
|
||||
LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
|
||||
LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -136,20 +146,58 @@ bool PMSA003ISensor::isActive()
|
||||
return state == State::ACTIVE;
|
||||
}
|
||||
|
||||
int32_t PMSA003ISensor::wakeUpTimeMs()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
return PMSA003I_WARMUP_MS;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t PMSA003ISensor::pendingForReadyMs()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
|
||||
uint32_t now;
|
||||
now = getTime();
|
||||
uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000;
|
||||
LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted);
|
||||
|
||||
if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) {
|
||||
LOG_INFO("%s: not enough time passed since starting measurement", sensorName);
|
||||
return PMSA003I_WARMUP_MS - sincePmMeasureStarted;
|
||||
}
|
||||
return 0;
|
||||
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool PMSA003ISensor::canSleep()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
void PMSA003ISensor::sleep()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
|
||||
state = State::IDLE;
|
||||
pmMeasureStarted = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t PMSA003ISensor::wakeUp()
|
||||
{
|
||||
#ifdef PMSA003I_ENABLE_PIN
|
||||
LOG_INFO("Waking up PMSA003I");
|
||||
LOG_INFO("Waking up %s", sensorName);
|
||||
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
|
||||
state = State::ACTIVE;
|
||||
pmMeasureStarted = getTime();
|
||||
|
||||
return PMSA003I_WARMUP_MS;
|
||||
#endif
|
||||
// No need to wait for warmup if already active
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
#define PMSA003I_I2C_CLOCK_SPEED 100000
|
||||
@@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor
|
||||
virtual bool isActive() override;
|
||||
virtual void sleep() override;
|
||||
virtual uint32_t wakeUp() override;
|
||||
virtual bool canSleep() override;
|
||||
virtual int32_t wakeUpTimeMs() override;
|
||||
virtual int32_t pendingForReadyMs() override;
|
||||
|
||||
private:
|
||||
enum class State { IDLE, ACTIVE };
|
||||
@@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor
|
||||
|
||||
uint16_t computedChecksum = 0;
|
||||
uint16_t receivedChecksum = 0;
|
||||
uint32_t pmMeasureStarted = 0;
|
||||
|
||||
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
|
||||
TwoWire *_bus{};
|
||||
|
||||
957
src/modules/Telemetry/Sensor/SEN5XSensor.cpp
Normal file
957
src/modules/Telemetry/Sensor/SEN5XSensor.cpp
Normal file
@@ -0,0 +1,957 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../detect/reClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "FSCommon.h"
|
||||
#include "SEN5XSensor.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include <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, ¬hing, 0);
|
||||
}
|
||||
|
||||
bool SEN5XSensor::sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber)
|
||||
{
|
||||
// At least we need two bytes for the command
|
||||
uint8_t bufferSize = 2;
|
||||
|
||||
// Add space for CRC bytes (one every two bytes)
|
||||
if (byteNumber > 0)
|
||||
bufferSize += byteNumber + (byteNumber / 2);
|
||||
|
||||
uint8_t toSend[bufferSize];
|
||||
uint8_t i = 0;
|
||||
toSend[i++] = static_cast<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
|
||||
170
src/modules/Telemetry/Sensor/SEN5XSensor.h
Normal file
170
src/modules/Telemetry/Sensor/SEN5XSensor.h
Normal file
@@ -0,0 +1,170 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "Wire.h"
|
||||
|
||||
// Warm up times for SEN5X from the datasheet
|
||||
#ifndef SEN5X_WARMUP_MS_1
|
||||
#define SEN5X_WARMUP_MS_1 15000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_WARMUP_MS_2
|
||||
#define SEN5X_WARMUP_MS_2 30000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_POLL_INTERVAL
|
||||
#define SEN5X_POLL_INTERVAL 1000
|
||||
#endif
|
||||
|
||||
#ifndef SEN5X_I2C_CLOCK_SPEED
|
||||
#define SEN5X_I2C_CLOCK_SPEED 100000
|
||||
#endif
|
||||
|
||||
/*
|
||||
Time after which the sensor can go to sleep, as the warmup period has passed
|
||||
and the VOCs sensor will is allowed to stop (although needs to recover the state
|
||||
each time)
|
||||
*/
|
||||
#ifndef SEN5X_VOC_STATE_WARMUP_S
|
||||
/* Note for Testing 5' is enough
|
||||
Sensirion recommends 1h
|
||||
This can be bypassed completely if switching to low-power RHT/Gas mode and setting
|
||||
SEN5X_VOC_STATE_WARMUP_S 0
|
||||
*/
|
||||
#define SEN5X_VOC_STATE_WARMUP_S 3600
|
||||
#endif
|
||||
|
||||
#define ONE_WEEK_IN_SECONDS 604800
|
||||
|
||||
struct _SEN5XMeasurements {
|
||||
uint16_t pM1p0;
|
||||
uint16_t pM2p5;
|
||||
uint16_t pM4p0;
|
||||
uint16_t pM10p0;
|
||||
uint32_t pN0p5;
|
||||
uint32_t pN1p0;
|
||||
uint32_t pN2p5;
|
||||
uint32_t pN4p0;
|
||||
uint32_t pN10p0;
|
||||
float tSize;
|
||||
float humidity;
|
||||
float temperature;
|
||||
float vocIndex;
|
||||
float noxIndex;
|
||||
};
|
||||
|
||||
class SEN5XSensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
TwoWire *_bus{};
|
||||
uint8_t _address{};
|
||||
|
||||
bool getVersion();
|
||||
float firmwareVer = -1;
|
||||
float hardwareVer = -1;
|
||||
float protocolVer = -1;
|
||||
bool findModel();
|
||||
|
||||
// Commands
|
||||
#define SEN5X_RESET 0xD304
|
||||
#define SEN5X_GET_PRODUCT_NAME 0xD014
|
||||
#define SEN5X_GET_FIRMWARE_VERSION 0xD100
|
||||
#define SEN5X_START_MEASUREMENT 0x0021
|
||||
#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037
|
||||
#define SEN5X_STOP_MEASUREMENT 0x0104
|
||||
#define SEN5X_READ_DATA_READY 0x0202
|
||||
#define SEN5X_START_FAN_CLEANING 0x5607
|
||||
#define SEN5X_RW_VOCS_STATE 0x6181
|
||||
|
||||
#define SEN5X_READ_VALUES 0x03C4
|
||||
#define SEN5X_READ_RAW_VALUES 0x03D2
|
||||
#define SEN5X_READ_PM_VALUES 0x0413
|
||||
|
||||
#define SEN5X_VOC_VALID_TIME 600
|
||||
#define SEN5X_VOC_VALID_DATE 1514764800
|
||||
|
||||
enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 };
|
||||
SEN5Xmodel model = SEN5X_UNKNOWN;
|
||||
|
||||
enum SEN5XState {
|
||||
SEN5X_OFF,
|
||||
SEN5X_IDLE,
|
||||
SEN5X_RHTGAS_ONLY,
|
||||
SEN5X_MEASUREMENT,
|
||||
SEN5X_MEASUREMENT_2,
|
||||
SEN5X_CLEANING,
|
||||
SEN5X_NOT_DETECTED
|
||||
};
|
||||
SEN5XState state = SEN5X_OFF;
|
||||
// Flag to work on one-shot (read and sleep), or continuous mode
|
||||
bool oneShotMode = true;
|
||||
void setMode(bool setOneShot);
|
||||
bool vocStateValid();
|
||||
/* Sensirion recommends taking a reading after 15 seconds,
|
||||
if the Particle number reading is over 100#/cm3 the reading is OK,
|
||||
but if it is lower wait until 30 seconds and take it again.
|
||||
See: https://sensirion.com/resource/application_note/low_power_mode/sen5x
|
||||
*/
|
||||
#define SEN5X_PN4P0_CONC_THD 100
|
||||
|
||||
bool sendCommand(uint16_t command);
|
||||
bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0);
|
||||
uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber); // Return number of bytes received
|
||||
uint8_t sen5xCRC(uint8_t *buffer);
|
||||
bool startCleaning();
|
||||
uint8_t getMeasurements();
|
||||
// bool readRawValues();
|
||||
bool readPNValues(bool cumulative);
|
||||
bool readValues();
|
||||
|
||||
uint32_t pmMeasureStarted = 0;
|
||||
uint32_t rhtGasMeasureStarted = 0;
|
||||
uint32_t lastDataPoll = 0;
|
||||
_SEN5XMeasurements sen5xmeasurement{};
|
||||
|
||||
bool idle(bool checkState = true);
|
||||
|
||||
protected:
|
||||
// Store status of the sensor in this file
|
||||
const char *sen5XStateFileName = "/prefs/sen5X.dat";
|
||||
meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero;
|
||||
|
||||
bool loadState();
|
||||
bool saveState();
|
||||
|
||||
// Cleaning State
|
||||
uint32_t lastCleaning = 0;
|
||||
bool lastCleaningValid = false;
|
||||
|
||||
// VOC State
|
||||
#define SEN5X_VOC_STATE_BUFFER_SIZE 8
|
||||
uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{};
|
||||
uint32_t vocTime = 0;
|
||||
bool vocValid = false;
|
||||
|
||||
bool vocStateFromSensor();
|
||||
bool vocStateToSensor();
|
||||
bool vocStateStable();
|
||||
bool vocStateRecent(uint32_t now);
|
||||
|
||||
public:
|
||||
SEN5XSensor();
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
|
||||
virtual bool isActive() override;
|
||||
virtual void sleep() override;
|
||||
virtual uint32_t wakeUp() override;
|
||||
virtual bool canSleep() override { return true; }
|
||||
virtual int32_t wakeUpTimeMs() override;
|
||||
virtual int32_t pendingForReadyMs() override;
|
||||
|
||||
AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request,
|
||||
meshtastic_AdminMessage *response) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -26,7 +26,6 @@ class TelemetrySensor
|
||||
this->status = 0;
|
||||
}
|
||||
|
||||
const char *sensorName;
|
||||
meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET;
|
||||
unsigned status;
|
||||
bool initialized = false;
|
||||
@@ -56,13 +55,18 @@ class TelemetrySensor
|
||||
return AdminMessageHandleResult::NOT_HANDLED;
|
||||
}
|
||||
|
||||
const char *sensorName;
|
||||
// TODO: delete after migration
|
||||
bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; }
|
||||
|
||||
// Functions to sleep / wakeup sensors that support it
|
||||
// These functions can save power consumption in cases like AQ
|
||||
virtual void sleep(){};
|
||||
virtual uint32_t wakeUp() { return 0; }
|
||||
// Return active by default, override per sensor
|
||||
virtual bool isActive() { return true; }
|
||||
virtual bool isActive() { return true; } // Return true by default, override per sensor
|
||||
virtual bool canSleep() { return false; } // Return false by default, override per sensor
|
||||
virtual int32_t wakeUpTimeMs() { return 0; }
|
||||
virtual int32_t pendingForReadyMs() { return 0; }
|
||||
|
||||
#if WIRE_INTERFACES_COUNT > 1
|
||||
// Set to true if Implementation only works first I2C port (Wire)
|
||||
|
||||
@@ -757,7 +757,11 @@ void NimbleBluetooth::deinit()
|
||||
isDeInit = true;
|
||||
|
||||
#ifdef BLE_LED
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
#ifndef NIMBLE_TWO
|
||||
NimBLEDevice::deinit();
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
#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)
|
||||
{
|
||||
@@ -249,6 +254,7 @@ void cpuDeepSleep(uint32_t msecToWake)
|
||||
|
||||
#endif // #end ESP32S3_WAKE_TYPE
|
||||
#endif
|
||||
variant_shutdown();
|
||||
|
||||
// We want RTC peripherals to stay on
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
@@ -36,6 +36,13 @@ bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t p
|
||||
return udp.endPacket();
|
||||
}
|
||||
|
||||
void AsyncUDP::close()
|
||||
{
|
||||
udp.stop();
|
||||
localPort = 0;
|
||||
_onPacket = nullptr;
|
||||
}
|
||||
|
||||
// AsyncUDPPacket
|
||||
AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort())
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ class AsyncUDP : public Print, private concurrency::OSThread
|
||||
|
||||
bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64);
|
||||
bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port);
|
||||
void close();
|
||||
|
||||
size_t write(uint8_t b) override;
|
||||
size_t write(const uint8_t *data, size_t len) override;
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef PIN_LED1
|
||||
#define LED_POWER PIN_LED1 // LED1 on nrf52840-DK
|
||||
#define LED_PIN PIN_LED1 // LED1 on nrf52840-DK
|
||||
#endif
|
||||
|
||||
#ifdef PIN_BUTTON1
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
uint16_t getVDDVoltage();
|
||||
|
||||
// Weak empty variant initialization function.
|
||||
// Weak empty variant shutdown prep function.
|
||||
// May be redefined by variant files.
|
||||
void variant_shutdown() __attribute__((weak));
|
||||
void variant_shutdown() {}
|
||||
|
||||
@@ -872,7 +872,6 @@ 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") {
|
||||
@@ -885,13 +884,6 @@ 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"]) {
|
||||
|
||||
@@ -177,12 +177,8 @@ 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 = "";
|
||||
@@ -509,30 +505,21 @@ extern struct portduino_config_struct {
|
||||
}
|
||||
|
||||
// config
|
||||
if (has_config_overrides) {
|
||||
if (has_configDisplayMode) {
|
||||
out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
out << YAML::EndMap; // Config
|
||||
|
||||
@@ -103,10 +103,8 @@ class Power : private concurrency::OSThread
|
||||
bool axpChipInit();
|
||||
/// Setup a simple ADC input based battery sensor
|
||||
bool analogInit();
|
||||
/// Setup cw2015 battery level sensor
|
||||
bool cw2015Init();
|
||||
/// Setup a 17048 battery level sensor
|
||||
bool max17048Init();
|
||||
/// Setup a Lipo battery level sensor
|
||||
bool lipoInit();
|
||||
/// Setup a Lipo charger
|
||||
bool lipoChargerInit();
|
||||
/// Setup a meshSolar battery sensor
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#endif
|
||||
|
||||
#include "Default.h"
|
||||
#include "Led.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
@@ -12,7 +13,6 @@
|
||||
#include "detect/LoRaRadioType.h"
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
#include "modules/StatusLEDModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -268,7 +268,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
||||
digitalWrite(PIN_WD_EN, LOW);
|
||||
#endif
|
||||
#endif
|
||||
statusLEDModule->setPowerLED(false);
|
||||
ledBlink.set(false);
|
||||
|
||||
#ifdef RESET_OLED
|
||||
digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power
|
||||
#endif
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
|
||||
#include "TestUtil.h"
|
||||
|
||||
#if defined(ARDUINO)
|
||||
#include <Arduino.h>
|
||||
#else
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#endif
|
||||
|
||||
void initializeTestEnvironment()
|
||||
{
|
||||
concurrency::hasBeenSetup = true;
|
||||
@@ -15,4 +22,13 @@ void initializeTestEnvironment()
|
||||
perhapsSetRTC(RTCQualityNTP, &tv);
|
||||
#endif
|
||||
concurrency::OSThread::setup();
|
||||
}
|
||||
|
||||
void testDelay(unsigned long ms)
|
||||
{
|
||||
#if defined(ARDUINO)
|
||||
::delay(ms);
|
||||
#else
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
#endif
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
// Initialize testing environment.
|
||||
void initializeTestEnvironment();
|
||||
void initializeTestEnvironment();
|
||||
|
||||
// Portable delay for tests (Arduino or host).
|
||||
void testDelay(unsigned long ms);
|
||||
216
test/test_atak/test_main.cpp
Normal file
216
test/test_atak/test_main.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#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() {}
|
||||
@@ -16,4 +16,4 @@ upload_speed = 460800
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
|
||||
adafruit/Adafruit NeoPixel@1.15.2
|
||||
adafruit/Adafruit NeoPixel@1.15.3
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#define LORA_CS 5
|
||||
#define RF95_FAN_EN 17
|
||||
|
||||
// This is a LED_WS2812 not a standard LED
|
||||
// #define LED_PIN 16 // This is a LED_WS2812 not a standard LED
|
||||
#define HAS_NEOPIXEL // Enable the use of neopixels
|
||||
#define NEOPIXEL_COUNT 1 // How many neopixels are connected
|
||||
#define NEOPIXEL_DATA 16 // gpio pin used to send data to the neopixels
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#define LORA_DIO2
|
||||
#define LORA_DIO3
|
||||
|
||||
#define LED_POWER 16 // green - blue is at 17
|
||||
#define LED_PIN 16 // green - blue is at 17
|
||||
|
||||
#define BUTTON_PIN 25
|
||||
#define BUTTON_NEED_PULLUP
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#define SX126X_TXEN RADIOLIB_NC
|
||||
#define SX126X_RXEN RADIOLIB_NC
|
||||
|
||||
// Status
|
||||
// #define LED_PIN 1
|
||||
// External notification
|
||||
// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the
|
||||
// app/preferences
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
#define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client
|
||||
|
||||
// LEDs
|
||||
#define LED_POWER 13 // Tx LED
|
||||
#define USER_LED 2 // Rx LED
|
||||
#define LED_PIN 13 // Tx LED
|
||||
#define USER_LED 2 // Rx LED
|
||||
|
||||
// Buzzer
|
||||
#define PIN_BUZZER 33
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k)
|
||||
#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards
|
||||
#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975).
|
||||
#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets)
|
||||
#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets)
|
||||
|
||||
// Radio
|
||||
#define USE_SX1262 // E22-900M30S uses SX1262
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k)
|
||||
#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards
|
||||
#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975).
|
||||
#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets)
|
||||
#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets)
|
||||
|
||||
#define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module
|
||||
#define LORA_RESET 23 // RST for SX1276, and for SX1262/SX1268
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// HACKBOX LoRa IO Kit
|
||||
// Uses a ESP-32-WROOM and a RA-01SH (SX1262) LoRa Board
|
||||
|
||||
#define LED_POWER 2 // LED
|
||||
#define LED_PIN 2 // LED
|
||||
#define LED_STATE_ON 1 // State when LED is lit
|
||||
|
||||
#define HAS_SCREEN 0
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller
|
||||
|
||||
#define LED_POWER 25 // If defined we will blink this LED
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#define BUTTON_PIN 0 // If defined, this will be used for user button presses
|
||||
|
||||
#define USE_RF95
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller
|
||||
|
||||
#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost
|
||||
#define LED_POWER 25 // If defined we will blink this LED
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#define BUTTON_PIN 0 // If defined, this will be used for user button presses
|
||||
|
||||
#define USE_RF95
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller
|
||||
|
||||
#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost
|
||||
#define LED_POWER 25 // If defined we will blink this LED
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#define BUTTON_PIN 0 // If defined, this will be used for user button presses
|
||||
|
||||
#define USE_RF95
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#undef GPS_TX_PIN
|
||||
|
||||
// Green / Lora = PIN 22 / GPIO2, Yellow / Wifi = PIN 23 / GPIO0, Blue / BLE = PIN 25 / GPIO16
|
||||
#define LED_POWER 22
|
||||
#define LED_PIN 22
|
||||
#define WIFI_LED 23
|
||||
#define BLE_LED 25
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#define I2C_SCL SCL
|
||||
#define I2C_SDA SDA
|
||||
|
||||
#define LED_POWER LED
|
||||
#define LED_PIN LED
|
||||
|
||||
// active low, powers the Battery reader, but no lora antenna boost (?)
|
||||
// #define VEXT_ENABLE Vext
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
// Green LED
|
||||
#define LED_STATE_ON 1 // State when LED is lit
|
||||
#define LED_POWER 10
|
||||
#define LED_PIN 10
|
||||
|
||||
// PCF8563 RTC Module
|
||||
#define PCF8563_RTC 0x51
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
/*
|
||||
LED PIN setup.
|
||||
*/
|
||||
#define LED_POWER 15
|
||||
#define LED_PIN 15
|
||||
|
||||
/*
|
||||
Five way button when using ADC.
|
||||
|
||||
@@ -43,7 +43,7 @@ static const uint8_t SCK = 33;
|
||||
#undef GPS_TX_PIN
|
||||
#define GPS_TX_PIN (TX1)
|
||||
|
||||
#define LED_POWER LED_BLUE
|
||||
#define LED_PIN LED_BLUE
|
||||
|
||||
#define PIN_VBAT WB_A0
|
||||
#define BATTERY_PIN PIN_VBAT
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module.
|
||||
|
||||
#define LED_STATE_ON 0 // State when LED is lit
|
||||
#define LED_POWER 4 // Newer tbeams (1.1) have an extra led on GPIO4
|
||||
#define LED_PIN 4 // Newer tbeams (1.1) have an extra led on GPIO4
|
||||
|
||||
// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if
|
||||
// not found then probe for SX1262
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
#undef EXT_NOTIFY_OUT
|
||||
#undef LED_STATE_ON
|
||||
#undef LED_POWER
|
||||
#undef LED_PIN
|
||||
|
||||
#define HAS_CST226SE 1
|
||||
#define HAS_TOUCHSCREEN 1
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost
|
||||
#define VEXT_ON_VALUE LOW
|
||||
#define LED_POWER 2 // If defined we will blink this LED
|
||||
#define LED_PIN 2 // If defined we will blink this LED
|
||||
#define BUTTON_PIN 0 // If defined, this will be used for user button presses
|
||||
#define BUTTON_NEED_PULLUP
|
||||
#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#define RESET_OLED 16 // If defined, this pin will be used to reset the display controller
|
||||
|
||||
#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost
|
||||
#define LED_POWER 25 // If defined we will blink this LED
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#define BUTTON_PIN 36
|
||||
#define BUTTON_NEED_PULLUP
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#define I2C_SCL 22
|
||||
|
||||
#define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost
|
||||
#define LED_POWER 25 // If defined we will blink this LED
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#define BUTTON_PIN \
|
||||
0 // If defined, this will be used for user button presses, if your board doesn't have a physical switch, you can wire one
|
||||
// between this pin and ground
|
||||
|
||||
@@ -22,4 +22,4 @@ build_flags =
|
||||
${env:tlora-v2-1-1_6.build_flags}
|
||||
-DBUTTON_PIN=0
|
||||
-DPIN_BUZZER=25
|
||||
-DLED_POWER=-1
|
||||
-DLED_PIN=-1
|
||||
@@ -8,10 +8,10 @@
|
||||
#define I2C_SDA 21 // I2C pins for this board
|
||||
#define I2C_SCL 22
|
||||
|
||||
#if defined(LED_POWER) && LED_POWER == -1
|
||||
#undef LED_POWER
|
||||
#if defined(LED_PIN) && LED_PIN == -1
|
||||
#undef LED_PIN
|
||||
#else
|
||||
#define LED_POWER 25 // If defined we will blink this LED
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#endif
|
||||
|
||||
#define USE_RF95
|
||||
|
||||
@@ -7,5 +7,4 @@ build_flags =
|
||||
-D TLORA_V2_1_16
|
||||
-I variants/esp32/tlora_v2_1_16
|
||||
-D LORA_TCXO_GPIO=33
|
||||
-ULED_BUILTIN
|
||||
upload_speed = 115200
|
||||
upload_speed = 115200
|
||||
@@ -6,7 +6,7 @@
|
||||
#define I2C_SDA 21 // I2C pins for this board
|
||||
#define I2C_SCL 22
|
||||
|
||||
#define LED_POWER 25 // If defined we will blink this LED
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#define BUTTON_PIN 12 // If defined, this will be used for user button presses,
|
||||
|
||||
#define BUTTON_NEED_PULLUP
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#define GPS_RX_PIN 9
|
||||
#define GPS_TX_PIN 10
|
||||
|
||||
#define LED_POWER 13 // 13 red, 2 blue, 15 red
|
||||
#define LED_PIN 13 // 13 red, 2 blue, 15 red
|
||||
|
||||
#define BUTTON_PIN 0
|
||||
#define BUTTON_NEED_PULLUP
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user