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