Compare commits

...

36 Commits

Author SHA1 Message Date
Ben Meadors
64116cd0d3 Meshtastic OTA (moar) (#9327)
* Initial commit of combined BLE and WiFi OTA

* Incorporate ota_hash in AdminMessage protobuf

* OTA protobuf changes

* Trunk fmt

* Partition header check for OTA type

* Guards

* Guards

* Derp

* Missed one

---------

Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
2026-01-15 14:36:36 -06:00
Ben Meadors
c8f0295a9c Cleanup 2026-01-15 08:25:38 -06:00
Ben Meadors
3911d5fe15 Fix build with high / low i2c address for OLED 2026-01-15 07:54:33 -06:00
Ben Meadors
59bdb9b097 Merge remote-tracking branch 'origin/develop' 2026-01-15 06:49:05 -06:00
Ben Meadors
b4157bd9bb Heltec V4 TFT metadata (#9325)
* Upgrade trunk (#9323)

Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>

* ICM20948 IMU sleep (#9324)

* Add v4-tft metadata

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-01-15 06:48:41 -06:00
Austin
7e4e772113 Add EByte EoRa-Hub (#9169) 2026-01-15 06:24:10 -06:00
HarukiToreda
82735ca04e ICM20948 IMU sleep (#9324) 2026-01-15 06:23:40 -06:00
github-actions[bot]
e8fbdb4d84 Upgrade trunk (#9323)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-15 06:21:03 -06:00
Ben Meadors
a69e439dc1 Merge branch 'develop' 2026-01-15 06:19:49 -06:00
Ben Meadors
360579926c Trunk fmt 2026-01-15 06:19:18 -06:00
Ben Meadors
ff8316f895 Merge branch 'master' into develop 2026-01-15 06:18:43 -06:00
Jason P
6ee52ca7fa Node Actions Menu Overhaul (#9287)
* Start overhaul and clean up of the Node Actions menu

* Wired up commands - still a lot of work and testing

* Remove old favorites menu

* Remove addFavoritesMenu

* CoPilot to the rescue, wired up some function in both directions

* Clean up CoPilot actions

* Cross out Mute or Ignored in lists, add Save to NodeDB on changes

* Improve strikethrough for columns

* Correct menu wording and adjust vertical divider on Node List

* Code cleanup

* Testing unveiled some issues - fixed with these changes
2026-01-15 16:22:55 +11:00
Thomas Göttgens
233e6acc85 Preliminary Thinknode M4 Support (#8754)
* Preliminary Thinknode M4 Support

* oops

* Fix RF switch TX configuration

* trunk'd

* GPS fix for M4

* Battery handling and LED for M4

* Trunk

* Drop debug warnings

* Make Red LED notification

* Merge cleanup

* Make white LEDs flash during charge

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-01-14 21:36:53 -06:00
Lewis He
5f63f91cbc Added I2C scanner a check for the QMC6310N. (#9305)
* Added support for the new SSD1306 control panel.

* Added QMC6310N inspection to I2C scanner

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-14 20:54:57 -06:00
Ben Meadors
c0afe92a7f Meshtastic unified OTA (#9231)
* Initial commit of combined BLE and WiFi OTA

* Incorporate ota_hash in AdminMessage protobuf

* OTA protobuf changes

* Trunk fmt

---------

Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
2026-01-14 20:54:31 -06:00
Mike Robbins
a6a80b067f Recover long_name, short_name from our own NodeDB entry if device.proto is unreadable (#9248)
* Recover long_name, short_name from our own NodeDB entry if device.proto is unreadable

* NodeDB::loadFromDisk: restore long/short name with memcpy and explicit null termination

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-01-15 11:02:09 +11:00
oscgonfer
64e95554bb Small fix in register size for SHT4X (#9309) 2026-01-15 11:00:42 +11:00
renovate[bot]
6537eeab03 Update pschatzmann_arduino-audio-driver to v0.2.0 (#9272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 11:00:24 +11:00
brad112358
fad315e99d Fix rotary encoder long press (#9039) 2026-01-15 10:59:24 +11:00
renovate[bot]
2d4f1b6bfe Update Adafruit BMP280 to v3 (#9307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 10:47:54 +11:00
oscgonfer
5a81403594 Move PMSA003I to separate class and update AQ telemetry (#7190) 2026-01-14 13:00:08 -06:00
Jonathan Bennett
5d7d1ae7a5 Adds Custom battery curve for thinknode m6 (#9313) 2026-01-14 11:40:35 -06:00
Manuel
940b3e236b fix GPS for T-Watch S3 plus (#9312)
* support T-Watch S3 Plus GPS

* HAS_GPS

* define BUTTON_PIN

* swap GPS pins, USB_MODE=1
2026-01-14 10:01:08 -06:00
vicliu
d1ae131502 T-Deck Pro: speed up eink force refresh (#9303) 2026-01-14 10:00:33 -06:00
Ben Meadors
552df4c88c Supress reboot banner in Reboot OTA 2026-01-14 07:06:40 -06:00
github-actions[bot]
cdbc8f48d4 Update protobufs (#9308)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-14 06:40:10 -06:00
Ben Meadors
919f214e8d Fix OTA partition name matching (#9302) 2026-01-14 06:33:01 -06:00
github-actions[bot]
89a83d00fa Upgrade trunk (#9306)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-14 06:26:31 -06:00
renovate[bot]
5610d4809c Update meshtastic/device-ui digest to 5a870c6 (#9301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 15:59:09 -06:00
github-actions[bot]
dae4061b06 Update protobufs (#9299)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-01-13 05:58:12 -06:00
Mike Robbins
e99853f660 SafeFile: use atomic rename-with-overwrite, rather than non-atomic delete-then-rename (#9296) 2026-01-13 05:57:04 -06:00
github-actions[bot]
3640e35a8b Upgrade trunk (#9297)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-01-13 05:50:40 -06:00
Ben Meadors
782ffdc5cd Merge branch 'develop' 2026-01-13 05:48:19 -06:00
Ben Meadors
6f36f39da9 Fix up T-Beam 1W HW_MODEL 2026-01-13 05:48:14 -06:00
Ben Meadors
ded4f57cb7 Partition name in manifest script (#9294)
* Fix up T-Beam 1W HW_MODEL

* Add part_name for bin files

* app0
2026-01-13 05:47:08 -06:00
HarukiToreda
3a0f3520d1 BaseUI: Autosave Messages (#9269)
* Autosave Messages

* fix

* Add logging, code cleanup, and add save on delete.

* We already save as part of delete messages, no need to do it again

* fix spelling errors

* Updating comment

---------

Co-authored-by: Jason P <applewiz@mac.com>
2026-01-12 19:40:44 -06:00
74 changed files with 1795 additions and 439 deletions

View File

@@ -91,8 +91,8 @@ jobs:
if [[ -f "$manifest" ]]; then if [[ -f "$manifest" ]]; then
echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)" echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)"
# Add OTA entry to files array if not already present # Add OTA entry to files array if not already present
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" \ jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes}] else . end' \ 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \
"$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest"
fi fi
done done

View File

@@ -9,10 +9,10 @@ plugins:
lint: lint:
enabled: enabled:
- checkov@3.2.497 - checkov@3.2.497
- renovate@42.78.2 - renovate@42.81.8
- prettier@3.7.4 - prettier@3.8.0
- trufflehog@3.92.4 - trufflehog@3.92.4
- yamllint@1.37.1 - yamllint@1.38.0
- bandit@1.9.2 - bandit@1.9.2
- trivy@0.68.2 - trivy@0.68.2
- taplo@0.10.0 - taplo@0.10.0

View File

@@ -60,6 +60,14 @@ def manifest_gather(source, target, env):
board_platform = env.BoardConfig().get("platform") board_platform = env.BoardConfig().get("platform")
board_mcu = env.BoardConfig().get("build.mcu").lower() board_mcu = env.BoardConfig().get("build.mcu").lower()
needs_ota_suffix = board_platform == "nordicnrf52" needs_ota_suffix = board_platform == "nordicnrf52"
# Mapping of bin files to their target partition names
# Maps the filename pattern to the partition name where it should be flashed
partition_map = {
f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0)
lfsbin: "spiffs", # filesystem image flashed to spiffs
}
check_paths = [ check_paths = [
progname, progname,
f"{progname}.elf", f"{progname}.elf",
@@ -85,6 +93,9 @@ def manifest_gather(source, target, env):
"md5": f.get_content_hash(), # Returns MD5 hash "md5": f.get_content_hash(), # Returns MD5 hash
"bytes": f.get_size() # Returns file size in bytes "bytes": f.get_size() # Returns file size in bytes
} }
# Add part_name if this file represents a partition that should be flashed
if p in partition_map:
d["part_name"] = partition_map[p]
out.append(d) out.append(d)
print(d) print(d)
manifest_write(out, env) manifest_write(out, env)

View File

@@ -0,0 +1,38 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default.csv",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "CDEBYTE_EoRa-Hub",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.cdebyte.com/products/EoRa-HUB-900TB",
"vendor": "CDEBYTE"
}

53
boards/ThinkNode-M4.json Normal file
View File

@@ -0,0 +1,53 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_thinknode_m4",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M4",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "ELECROW ThinkNode m4",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html",
"vendor": "ELECROW"
}

View File

@@ -9,7 +9,7 @@
"-DBOARD_HAS_PSRAM", "-DBOARD_HAS_PSRAM",
"-DT_WATCH_S3", "-DT_WATCH_S3",
"-DARDUINO_USB_CDC_ON_BOOT=1", "-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0", "-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1", "-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1" "-DARDUINO_EVENT_RUNNING_CORE=1"
], ],

View File

@@ -119,7 +119,7 @@ lib_deps =
[device-ui_base] [device-ui_base]
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/12f8cddc1e2908e1988da21e3500c695668e8d92.zip https://github.com/meshtastic/device-ui/archive/5a870c623a4e9ab7a7abe3d02950536f107d1a31.zip
; Common libs for environmental measurements in telemetry module ; Common libs for environmental measurements in telemetry module
[environmental_base] [environmental_base]
@@ -129,7 +129,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
adafruit/Adafruit Unified Sensor@1.1.15 adafruit/Adafruit Unified Sensor@1.1.15
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
adafruit/Adafruit BMP280 Library@2.6.8 adafruit/Adafruit BMP280 Library@3.0.0
# renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library # renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library
adafruit/Adafruit BMP085 Library@1.2.4 adafruit/Adafruit BMP085 Library@1.2.4
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
@@ -142,8 +142,6 @@ lib_deps =
adafruit/Adafruit INA260 Library@1.5.3 adafruit/Adafruit INA260 Library@1.5.3
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
adafruit/Adafruit INA219@1.2.3 adafruit/Adafruit INA219@1.2.3
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
adafruit/Adafruit PM25 AQI Sensor@2.0.0
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
adafruit/Adafruit MPU6050@2.2.6 adafruit/Adafruit MPU6050@2.2.6
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH

View File

@@ -13,6 +13,11 @@
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) #define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif #endif
// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc)
#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC
#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60)
#endif
// Global message text pool and state // Global message text pool and state
static char *g_messagePool = nullptr; static char *g_messagePool = nullptr;
static size_t g_poolWritePos = 0; static size_t g_poolWritePos = 0;
@@ -102,6 +107,60 @@ void MessageStore::addLiveMessage(const StoredMessage &msg)
pushWithLimit(liveMessages, msg); pushWithLimit(liveMessages, msg);
} }
#if ENABLE_MESSAGE_PERSISTENCE
static bool g_messageStoreHasUnsavedChanges = false;
static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved
static inline uint32_t autosaveIntervalMs()
{
uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC;
if (sec < 60)
sec = 60;
return sec * 1000UL;
}
static inline bool reachedMs(uint32_t now, uint32_t target)
{
return (int32_t)(now - target) >= 0;
}
// Mark new messages in RAM that need to be saved later
static inline void markMessageStoreUnsaved()
{
g_messageStoreHasUnsavedChanges = true;
if (g_lastAutoSaveMs == 0) {
g_lastAutoSaveMs = millis();
}
}
// Called periodically from the main loop in main.cpp
static inline void autosaveTick(MessageStore *store)
{
if (!store)
return;
uint32_t now = millis();
if (g_lastAutoSaveMs == 0) {
g_lastAutoSaveMs = now;
return;
}
if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs()))
return;
// Autosave interval reached, only save if there are unsaved messages.
if (g_messageStoreHasUnsavedChanges) {
LOG_INFO("Autosaving MessageStore to flash");
store->saveToFlash();
} else {
LOG_INFO("Autosave skipped, no changes to save");
g_lastAutoSaveMs = now;
}
}
#endif
// Add from incoming/outgoing packet // Add from incoming/outgoing packet
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{ {
@@ -131,6 +190,11 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
} }
addLiveMessage(sm); addLiveMessage(sm);
#if ENABLE_MESSAGE_PERSISTENCE
markMessageStoreUnsaved();
#endif
return liveMessages.back(); return liveMessages.back();
} }
@@ -155,6 +219,10 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st
sm.ackStatus = AckStatus::NONE; sm.ackStatus = AckStatus::NONE;
addLiveMessage(sm); addLiveMessage(sm);
#if ENABLE_MESSAGE_PERSISTENCE
markMessageStoreUnsaved();
#endif
} }
#if ENABLE_MESSAGE_PERSISTENCE #if ENABLE_MESSAGE_PERSISTENCE
@@ -239,6 +307,10 @@ void MessageStore::saveToFlash()
f.close(); f.close();
#endif #endif
// Reset autosave state after any save
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
} }
void MessageStore::loadFromFlash() void MessageStore::loadFromFlash()
@@ -270,6 +342,9 @@ void MessageStore::loadFromFlash()
f.close(); f.close();
#endif #endif
// Loading messages does not trigger an autosave
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
} }
#else #else
@@ -290,6 +365,11 @@ void MessageStore::clearAllMessages()
f.write(&count, 1); // write "0 messages" f.write(&count, 1); // write "0 messages"
f.close(); f.close();
#endif #endif
#if ENABLE_MESSAGE_PERSISTENCE
g_messageStoreHasUnsavedChanges = false;
g_lastAutoSaveMs = millis();
#endif
} }
// Internal helper: erase first or last message matching a predicate // Internal helper: erase first or last message matching a predicate
@@ -421,6 +501,14 @@ uint16_t MessageStore::storeText(const char *src, size_t len)
return storeTextInPool(src, len); return storeTextInPool(src, len);
} }
#if ENABLE_MESSAGE_PERSISTENCE
void messageStoreAutosaveTick()
{
// Called from the main loop to check autosave timing
autosaveTick(&messageStore);
}
#endif
// Global definition // Global definition
MessageStore messageStore("default"); MessageStore messageStore("default");
#endif #endif

View File

@@ -125,6 +125,11 @@ class MessageStore
std::string filename; // Flash filename for persistence std::string filename; // Flash filename for persistence
}; };
#if ENABLE_MESSAGE_PERSISTENCE
// Called periodically from main loop to trigger time based autosave
void messageStoreAutosaveTick();
#endif
// Global instance (defined in MessageStore.cpp) // Global instance (defined in MessageStore.cpp)
extern MessageStore messageStore; extern MessageStore messageStore;

View File

@@ -476,7 +476,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
} }
#endif #endif
#ifdef EXT_CHRG_DETECT #if defined(ELECROW_ThinkNode_M6)
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
#elif EXT_CHRG_DETECT
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#elif defined(BATTERY_CHARGING_INV) #elif defined(BATTERY_CHARGING_INV)
return !digitalRead(BATTERY_CHARGING_INV); return !digitalRead(BATTERY_CHARGING_INV);
@@ -693,6 +695,8 @@ bool Power::setup()
found = true; found = true;
} else if (lipoChargerInit()) { } else if (lipoChargerInit()) {
found = true; found = true;
} else if (serialBatteryInit()) {
found = true;
} else if (meshSolarInit()) { } else if (meshSolarInit()) {
found = true; found = true;
} else if (analogInit()) { } else if (analogInit()) {
@@ -1569,3 +1573,135 @@ bool Power::meshSolarInit()
return false; return false;
} }
#endif #endif
#ifdef HAS_SERIAL_BATTERY_LEVEL
#include <SoftwareSerial.h>
/**
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
*/
class SerialBatteryLevel : public HasBatteryLevel
{
public:
/**
* Init the I2C meshSolar battery level sensor
*/
bool runOnce()
{
BatterySerial.begin(4800);
return true;
}
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() override { return v_percent; }
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override { return voltage * 1000; }
/**
* return true if there is a battery installed in this unit
*/
virtual bool isBatteryConnect() override
{
// definitely need to gobble up more bytes at once
if (BatterySerial.available() > 5) {
// LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available());
while (BatterySerial.available() > 11) {
BatterySerial.read(); // flush old data
}
// LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available());
int tries = 0;
while (BatterySerial.read() != 0xFE) {
tries++; // wait for start byte
if (tries > 10) {
LOG_WARN("SerialBatteryLevel: no start byte found");
return 1;
}
}
Data[1] = BatterySerial.read();
Data[2] = BatterySerial.read();
Data[3] = BatterySerial.read();
Data[4] = BatterySerial.read();
Data[5] = BatterySerial.read();
if (Data[5] != 0xFD) {
LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]);
return true;
}
v_percent = Data[1];
voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000);
voltage *= 2;
// LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]);
return true;
}
// This function runs first, so use it to grab the latest data from the secondary MCU
return true;
}
/**
* return true if there is an external power source detected
*/
virtual bool isVbusIn() override
{
#if defined(EXT_CHRG_DETECT)
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#endif
return false;
}
virtual bool isCharging() override
{
#ifdef EXT_CHRG_DETECT
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#endif
// by default, we check the battery voltage only
return isVbusIn();
}
private:
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
uint8_t Data[6] = {0};
int v_percent = 0;
float voltage = 0.0;
};
SerialBatteryLevel serialBatteryLevel;
/**
* Init the serial battery level sensor
*/
bool Power::serialBatteryInit()
{
#ifdef EXT_PWR_DETECT
pinMode(EXT_PWR_DETECT, INPUT);
#endif
#ifdef EXT_CHRG_DETECT
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
#endif
bool result = serialBatteryLevel.runOnce();
LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet");
if (!result)
return false;
batteryLevel = &serialBatteryLevel;
return true;
}
#else
/**
* If this device has no serial battery level sensor, don't try to use it.
*/
bool Power::serialBatteryInit()
{
return false;
}
#endif

View File

@@ -54,7 +54,7 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size)
} }
/** /**
* Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches * Atomically close the file (overwriting any old version) and readback the contents to confirm the hash matches
* *
* @return false for failure * @return false for failure
*/ */
@@ -73,15 +73,7 @@ bool SafeFile::close()
if (!testReadback()) if (!testReadback())
return false; return false;
{ // Scope for lock // Rename or overwrite (atomic operation)
concurrency::LockGuard g(spiLock);
// brief window of risk here ;-)
if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) {
LOG_ERROR("Can't remove old pref file");
return false;
}
}
String filenameTmp = filename; String filenameTmp = filename;
filenameTmp += ".tmp"; filenameTmp += ".tmp";
if (!renameFile(filenameTmp.c_str(), filename.c_str())) { if (!renameFile(filenameTmp.c_str(), filename.c_str())) {

View File

@@ -172,11 +172,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// OLED & Input // OLED & Input
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#define SSD1306_ADDRESS_L 0x3C // Addr = 0
#define SSD1306_ADDRESS_H 0x3D // Addr = 1
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
#define SSD1306_ADDRESS 0x3D #define SSD1306_ADDRESS SSD1306_ADDRESS_H
#define USE_SH1106 #define USE_SH1106
#else
#define SSD1306_ADDRESS 0x3C
#endif #endif
#define ST7567_ADDRESS 0x3F #define ST7567_ADDRESS 0x3F
@@ -205,7 +206,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define INA_ADDR_WAVESHARE_UPS 0x43 #define INA_ADDR_WAVESHARE_UPS 0x43
#define INA3221_ADDR 0x42 #define INA3221_ADDR 0x42
#define MAX1704X_ADDR 0x36 #define MAX1704X_ADDR 0x36
#define QMC6310_ADDR 0x1C #define QMC6310U_ADDR 0x1C
#define QMI8658_ADDR 0x6B #define QMI8658_ADDR 0x6B
#define QMC5883L_ADDR 0x0D #define QMC5883L_ADDR 0x0D
#define HMC5883L_ADDR 0x1E #define HMC5883L_ADDR 0x1E
@@ -214,7 +215,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define LPS22HB_ADDR_ALT 0x5D #define LPS22HB_ADDR_ALT 0x5D
#define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR 0x44
#define SHT31_4x_ADDR_ALT 0x45 #define SHT31_4x_ADDR_ALT 0x45
#define PMSA0031_ADDR 0x12 #define PMSA003I_ADDR 0x12
#define QMA6100P_ADDR 0x12 #define QMA6100P_ADDR 0x12
#define AHT10_ADDR 0x38 #define AHT10_ADDR 0x38
#define RCWL9620_ADDR 0x57 #define RCWL9620_ADDR 0x57
@@ -480,6 +481,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_EXCLUDE_AUDIO 1 #define MESHTASTIC_EXCLUDE_AUDIO 1
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 #define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 #define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 #define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 #define MESHTASTIC_EXCLUDE_PAXCOUNTER 1

View File

@@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
ScanI2C::FoundDevice ScanI2C::firstAQI() const ScanI2C::FoundDevice ScanI2C::firstAQI() const
{ {
ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; ScanI2C::DeviceType types[] = {PMSA003I, SCD4X};
return firstOfOrNONE(2, types); return firstOfOrNONE(2, types);
} }

View File

@@ -35,11 +35,12 @@ class ScanI2C
SHT4X, SHT4X,
SHTC3, SHTC3,
LPS22HB, LPS22HB,
QMC6310, QMC6310U,
QMC6310N,
QMI8658, QMI8658,
QMC5883L, QMC5883L,
HMC5883L, HMC5883L,
PMSA0031, PMSA003I,
QMA6100P, QMA6100P,
MPU6050, MPU6050,
LIS3DH, LIS3DH,

View File

@@ -63,6 +63,10 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const
if (i2cBus->available()) { if (i2cBus->available()) {
r = i2cBus->read(); r = i2cBus->read();
} }
if (r == 0x80) {
LOG_INFO("QMC6310N found at address 0x%02X", addr.address);
return ScanI2C::DeviceType::QMC6310N;
}
r &= 0x0f; r &= 0x0f;
if (r == 0x08 || r == 0x00) { if (r == 0x08 || r == 0x00) {
@@ -106,7 +110,7 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
if (i2cBus->available()) if (i2cBus->available())
i2cBus->read(); i2cBus->read();
} }
LOG_DEBUG("Register value: 0x%x", value); LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value);
return value; return value;
} }
@@ -175,7 +179,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = NONE; type = NONE;
if (err == 0) { if (err == 0) {
switch (addr.address) { switch (addr.address) {
case SSD1306_ADDRESS: case SSD1306_ADDRESS_H:
case SSD1306_ADDRESS_L:
type = probeOLED(addr); type = probeOLED(addr);
break; break;
@@ -382,11 +387,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
} }
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2); if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
if (registerValue == 0x5449) {
type = OPT3001; type = OPT3001;
logFoundDevice("OPT3001", (uint8_t)addr.address); logFoundDevice("OPT3001", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
0) { // unique SHT4x serial number (6 bytes inc. CRC)
type = SHT4X; type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address); logFoundDevice("SHT4X", (uint8_t)addr.address);
} else { } else {
@@ -412,7 +417,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
case LPS22HB_ADDR_ALT: case LPS22HB_ADDR_ALT:
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
case QMI8658_ADDR: case QMI8658_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID
@@ -442,7 +447,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#ifdef HAS_QMA6100P #ifdef HAS_QMA6100P
SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address)
#else #else
SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
#endif #endif
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);

41
src/detect/reClockI2C.h Normal file
View File

@@ -0,0 +1,41 @@
#ifdef CAN_RECLOCK_I2C
#include "ScanI2CTwoWire.h"
uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus)
{
uint32_t currentClock;
/* See https://github.com/arduino/Arduino/issues/11457
Currently, only ESP32 can getClock()
While all cores can setClock()
https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50
https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60
https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103
For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes)
we need to reclock I2C and set it back to the previous desired speed.
Only for cases where we can know OR predefine the speed, we can do this.
*/
#ifdef ARCH_ESP32
currentClock = i2cBus->getClock();
#elif defined(ARCH_NRF52)
// TODO add getClock function or return a predefined clock speed per variant?
return 0;
#elif defined(ARCH_RP2040)
// TODO add getClock function or return a predefined clock speed per variant
return 0;
#elif defined(ARCH_STM32WL)
// TODO add getClock function or return a predefined clock speed per variant
return 0;
#else
return 0;
#endif
if (currentClock != desiredClock) {
LOG_DEBUG("Changing I2C clock to %u", desiredClock);
i2cBus->setClock(desiredClock);
}
return currentClock;
}
#endif

View File

@@ -896,14 +896,11 @@ void GPS::writePinEN(bool on)
void GPS::writePinStandby(bool standby) void GPS::writePinStandby(bool standby)
{ {
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones #ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones
bool val;
// Determine the new value for the pin if (standby)
// Normally: active HIGH for awake val = GPS_STANDBY_ACTIVE;
#ifdef PIN_GPS_STANDBY_INVERTED else
bool val = standby; val = !GPS_STANDBY_ACTIVE;
#else
bool val = !standby;
#endif
// Write and log // Write and log
pinMode(PIN_GPS_STANDBY, OUTPUT); pinMode(PIN_GPS_STANDBY, OUTPUT);

View File

@@ -16,6 +16,11 @@
#define GPS_EN_ACTIVE 1 #define GPS_EN_ACTIVE 1
#endif #endif
// Allow defining the polarity of the STANDBY output. default is LOW for standby
#ifndef GPS_STANDBY_ACTIVE
#define GPS_STANDBY_ACTIVE LOW
#endif
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL; static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000; static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;

View File

@@ -9,6 +9,15 @@
#include "GxEPD2Multi.h" #include "GxEPD2Multi.h"
#endif #endif
// Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing.
#ifndef EINK_FORCE_DISPLAY_THROTTLE_MS
#if defined(T_DECK_PRO)
#define EINK_FORCE_DISPLAY_THROTTLE_MS 200
#else
#define EINK_FORCE_DISPLAY_THROTTLE_MS 1000
#endif
#endif
/** /**
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
* *
@@ -42,7 +51,7 @@ class EInkDisplay : public OLEDDisplay
* *
* @return true if we did draw the screen * @return true if we did draw the screen
*/ */
virtual bool forceDisplay(uint32_t msecLimit = 1000); virtual bool forceDisplay(uint32_t msecLimit = EINK_FORCE_DISPLAY_THROTTLE_MS);
/** /**
* Run any code needed to complete an update, after the physical refresh has completed. * Run any code needed to complete an update, after the physical refresh has completed.

View File

@@ -825,7 +825,7 @@ int32_t Screen::runOnce()
#endif #endif
} }
#endif #endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) {
showSimpleBanner("Rebooting...", 0); showSimpleBanner("Rebooting...", 0);
} }

View File

@@ -59,6 +59,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
} // namespace } // namespace
menuHandler::screenMenus menuHandler::menuQueue = menu_none; menuHandler::screenMenus menuHandler::menuQueue = menu_none;
uint32_t menuHandler::pickedNodeNum = 0;
bool test_enabled = false; bool test_enabled = false;
uint8_t test_count = 0; uint8_t test_count = 0;
@@ -449,7 +450,7 @@ void menuHandler::clockMenu()
} }
void menuHandler::messageResponseMenu() void menuHandler::messageResponseMenu()
{ {
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, MuteChannel, Aloud, enumEnd }; enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd };
static const char *optionsArray[enumEnd]; static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd]; static int optionsEnumArray[enumEnd];
@@ -479,7 +480,7 @@ void menuHandler::messageResponseMenu()
// Delete submenu // Delete submenu
optionsArray[options] = "Delete"; optionsArray[options] = "Delete";
optionsEnumArray[options++] = 900; optionsEnumArray[options++] = DeleteMenu;
#ifdef HAS_I2S #ifdef HAS_I2S
optionsArray[options] = "Read Aloud"; optionsArray[options] = "Read Aloud";
@@ -520,34 +521,10 @@ void menuHandler::messageResponseMenu()
nodeDB->saveToDisk(); nodeDB->saveToDisk();
} }
// Delete submenu } else if (selected == DeleteMenu) {
} else if (selected == 900) {
menuHandler::menuQueue = menuHandler::delete_messages_menu; menuHandler::menuQueue = menuHandler::delete_messages_menu;
screen->runNow(); screen->runNow();
// Delete oldest FIRST (only change)
} else if (selected == DeleteOldest) {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
// Global oldest
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
// Oldest in current channel
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
// Oldest in current DM
messageStore.deleteOldestMessageWithPeer(peer);
}
// Delete all messages
} else if (selected == DeleteAll) {
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
#ifdef HAS_I2S #ifdef HAS_I2S
} else if (selected == Aloud) { } else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
@@ -716,7 +693,6 @@ void menuHandler::deleteMessagesMenu()
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteOldestMessageWithPeer(peer); messageStore.deleteOldestMessageWithPeer(peer);
} }
return; return;
} }
@@ -729,7 +705,6 @@ void menuHandler::deleteMessagesMenu()
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteAllMessagesWithPeer(peer); messageStore.deleteAllMessagesWithPeer(peer);
} }
return; return;
} }
}; };
@@ -1239,20 +1214,13 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu() void menuHandler::nodeListMenu()
{ {
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"}; static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back}; static int optionsEnumArray[enumEnd] = {Back};
int options = 1; int options = 1;
optionsArray[options] = "Add Favorite"; optionsArray[options] = "Node Actions / Settings";
optionsEnumArray[options++] = Favorite; optionsEnumArray[options++] = NodePicker;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = Verify;
}
if (currentResolution != ScreenResolution::UltraLow) { if (currentResolution != ScreenResolution::UltraLow) {
optionsArray[options] = "Show Long/Short Name"; optionsArray[options] = "Show Long/Short Name";
@@ -1267,18 +1235,12 @@ void menuHandler::nodeListMenu()
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) { if (selected == NodePicker) {
menuQueue = add_favorite; menuQueue = NodePicker_menu;
screen->runNow();
} else if (selected == Verify) {
menuQueue = key_verification_init;
screen->runNow(); screen->runNow();
} else if (selected == Reset) { } else if (selected == Reset) {
menuQueue = reset_node_db_menu; menuQueue = reset_node_db_menu;
screen->runNow(); screen->runNow();
} else if (selected == TraceRoute) {
menuQueue = trace_route_menu;
screen->runNow();
} else if (selected == NodeNameLength) { } else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu; menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow(); screen->runNow();
@@ -1287,6 +1249,159 @@ void menuHandler::nodeListMenu()
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::NodePicker()
{
const char *NODE_PICKER_TITLE;
if (currentResolution == ScreenResolution::UltraLow) {
NODE_PICKER_TITLE = "Pick Node";
} else {
NODE_PICKER_TITLE = "Pick A Node";
}
screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void {
LOG_INFO("Nodenum: %u", nodenum);
// Store the selection so the Manage Node menu knows which node to operate on
menuHandler::pickedNodeNum = nodenum;
// Keep UI favorite context in sync (used elsewhere for some node-based actions)
graphics::UIRenderer::currentFavoriteNodeNum = nodenum;
menuQueue = Manage_Node_menu;
screen->runNow();
});
}
void menuHandler::ManageNodeMenu()
{
// If we don't have a node selected yet, go fast exit
auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!node) {
return;
}
enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
if (node->is_favorite) {
optionsArray[options] = "Unfavorite";
} else {
optionsArray[options] = "Favorite";
}
optionsEnumArray[options++] = Favorite;
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
if (isMuted) {
optionsArray[options] = "Unmute Notifications";
} else {
optionsArray[options] = "Mute Notifications";
}
optionsEnumArray[options++] = Mute;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = KeyVerification;
if (node->is_ignored) {
optionsArray[options] = "Unignore Node";
} else {
optionsArray[options] = "Ignore Node";
}
optionsEnumArray[options++] = Ignore;
BannerOverlayOptions bannerOptions;
std::string title = "";
if (node->has_user && node->user.long_name && node->user.long_name[0]) {
title += sanitizeString(node->user.long_name).substr(0, 15);
} else {
char buf[20];
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
title += buf;
}
bannerOptions.message = title.c_str();
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuQueue = node_base_menu;
screen->runNow();
return;
}
if (selected == Favorite) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
if (n->is_favorite) {
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
} else {
LOG_INFO("Adding node %08X to favorites", menuHandler::pickedNodeNum);
nodeDB->set_favorite(true, menuHandler::pickedNodeNum);
}
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
return;
}
if (selected == Mute) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) {
n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK;
LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum);
} else {
n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK;
LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum);
}
nodeDB->notifyObservers(true);
nodeDB->saveToDisk();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
return;
}
if (selected == TraceRoute) {
LOG_INFO("Starting traceroute to %08X", menuHandler::pickedNodeNum);
if (traceRouteModule) {
traceRouteModule->startTraceRoute(menuHandler::pickedNodeNum);
}
return;
}
if (selected == KeyVerification) {
LOG_INFO("Initiating key verification with %08X", menuHandler::pickedNodeNum);
if (keyVerificationModule) {
keyVerificationModule->sendInitialRequest(menuHandler::pickedNodeNum);
}
return;
}
if (selected == Ignore) {
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
if (!n) {
return;
}
if (n->is_ignored) {
n->is_ignored = false;
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
} else {
n->is_ignored = true;
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
}
nodeDB->notifyObservers(true);
nodeDB->saveToDisk();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::nodeNameLengthMenu() void menuHandler::nodeNameLengthMenu()
{ {
static const NodeNameOption nodeNameOptions[] = { static const NodeNameOption nodeNameOptions[] = {
@@ -1315,6 +1430,7 @@ void menuHandler::nodeNameLengthMenu()
} }
config.display.use_long_node_name = option.value; config.display.use_long_node_name = option.value;
saveUIConfig();
LOG_INFO("Setting names to %s", option.value ? "long" : "short"); LOG_INFO("Setting names to %s", option.value ? "long" : "short");
}); });
@@ -1984,21 +2100,6 @@ void menuHandler::shutdownMenu()
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::addFavoriteMenu()
{
const char *NODE_PICKER_TITLE;
if (currentResolution == ScreenResolution::UltraLow) {
NODE_PICKER_TITLE = "Node Favorite";
} else {
NODE_PICKER_TITLE = "Node To Favorite";
}
screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void {
LOG_WARN("Nodenum: %u", nodenum);
nodeDB->set_favorite(true, nodenum);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
});
}
void menuHandler::removeFavoriteMenu() void menuHandler::removeFavoriteMenu()
{ {
@@ -2273,7 +2374,8 @@ void menuHandler::FrameToggles_menu()
lora, lora,
clock, clock,
show_favorites, show_favorites,
show_telemetry, show_env_telemetry,
show_aq_telemetry,
show_power, show_power,
enumEnd enumEnd
}; };
@@ -2318,8 +2420,11 @@ void menuHandler::FrameToggles_menu()
optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
optionsEnumArray[options++] = show_favorites; optionsEnumArray[options++] = show_favorites;
optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry";
optionsEnumArray[options++] = show_telemetry; optionsEnumArray[options++] = show_env_telemetry;
optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry";
optionsEnumArray[options++] = show_aq_telemetry;
optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power";
optionsEnumArray[options++] = show_power; optionsEnumArray[options++] = show_power;
@@ -2382,10 +2487,14 @@ void menuHandler::FrameToggles_menu()
screen->toggleFrameVisibility("show_favorites"); screen->toggleFrameVisibility("show_favorites");
menuHandler::menuQueue = menuHandler::FrameToggles; menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow(); screen->runNow();
} else if (selected == show_telemetry) { } else if (selected == show_env_telemetry) {
moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles; menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow(); screen->runNow();
} else if (selected == show_aq_telemetry) {
moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_power) { } else if (selected == show_power) {
moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles; menuHandler::menuQueue = menuHandler::FrameToggles;
@@ -2510,8 +2619,11 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case shutdown_menu: case shutdown_menu:
shutdownMenu(); shutdownMenu();
break; break;
case add_favorite: case NodePicker_menu:
addFavoriteMenu(); NodePicker();
break;
case Manage_Node_menu:
ManageNodeMenu();
break; break;
case remove_favorite: case remove_favorite:
removeFavoriteMenu(); removeFavoriteMenu();

View File

@@ -33,7 +33,8 @@ class menuHandler
brightness_picker, brightness_picker,
reboot_menu, reboot_menu,
shutdown_menu, shutdown_menu,
add_favorite, NodePicker_menu,
Manage_Node_menu,
remove_favorite, remove_favorite,
test_menu, test_menu,
number_test, number_test,
@@ -55,6 +56,7 @@ class menuHandler
DisplayUnits DisplayUnits
}; };
static screenMenus menuQueue; static screenMenus menuQueue;
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
static void OnboardMessage(); static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000); static void LoraRegionPicker(uint32_t duration = 30000);
@@ -90,6 +92,8 @@ class menuHandler
static void BrightnessPickerMenu(); static void BrightnessPickerMenu();
static void rebootMenu(); static void rebootMenu();
static void shutdownMenu(); static void shutdownMenu();
static void NodePicker();
static void ManageNodeMenu();
static void addFavoriteMenu(); static void addFavoriteMenu();
static void removeFavoriteMenu(); static void removeFavoriteMenu();
static void traceRouteMenu(); static void traceRouteMenu();
@@ -149,6 +153,7 @@ using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>; using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
using NodeNameOption = MenuOption<bool>; using NodeNameOption = MenuOption<bool>;
using PositionMenuOption = MenuOption<int>; using PositionMenuOption = MenuOption<int>;
using ManageNodeOption = MenuOption<int>;
using ClockFaceOption = MenuOption<bool>; using ClockFaceOption = MenuOption<bool>;
} // namespace graphics } // namespace graphics

View File

@@ -176,6 +176,7 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{ {
x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x;
for (int y = yStart; y <= yEnd; y += 2) { for (int y = yStart; y <= yEnd; y += 2) {
display->setPixel(x, y); display->setPixel(x, y);
} }
@@ -205,9 +206,11 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25;
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char timeStr[10]; char timeStr[10];
uint32_t seconds = sinceLastSeen(node); uint32_t seconds = sinceLastSeen(node);
@@ -234,6 +237,13 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
int rightEdge = x + columnWidth - timeOffset; int rightEdge = x + columnWidth - timeOffset;
if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time
@@ -253,6 +263,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
int barsXOffset = columnWidth - barsOffset; int barsXOffset = columnWidth - barsOffset;
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
@@ -265,6 +276,13 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
// Draw signal strength bars // Draw signal strength bars
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
@@ -298,6 +316,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char distStr[10] = ""; char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -358,6 +377,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
if (strlen(distStr) > 0) { if (strlen(distStr) > 0) {
int offset = (currentResolution == ScreenResolution::High) int offset = (currentResolution == ScreenResolution::High)
@@ -392,6 +418,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
@@ -403,6 +430,13 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) {
if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else {
display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6);
}
}
} }
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,

View File

@@ -93,6 +93,8 @@ int32_t RotaryEncoderInterruptBase::runOnce()
if (!pressDetected) { if (!pressDetected) {
this->action = ROTARY_ACTION_NONE; this->action = ROTARY_ACTION_NONE;
} else if (now - pressStartTime < LONG_PRESS_DURATION) {
return (20); // keep checking for long/short until time expires
} }
return INT32_MAX; return INT32_MAX;

View File

@@ -38,6 +38,9 @@
#include "target_specific.h" #include "target_specific.h"
#include <memory> #include <memory>
#include <utility> #include <utility>
#if HAS_SCREEN
#include "MessageStore.h"
#endif
#ifdef ELECROW_ThinkNode_M5 #ifdef ELECROW_ThinkNode_M5
PCA9557 io(0x18, &Wire); PCA9557 io(0x18, &Wire);
@@ -102,6 +105,43 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr;
#include <string> #include <string>
#endif #endif
#ifdef ARCH_ESP32
#ifdef DEBUG_PARTITION_TABLE
#include "esp_partition.h"
void printPartitionTable()
{
printf("\n--- Partition Table ---\n");
// Print Column Headers
printf("| %-16s | %-4s | %-7s | %-10s | %-10s |\n", "Label", "Type", "Subtype", "Offset", "Size");
printf("|------------------|------|---------|------------|------------|\n");
// Create an iterator to find ALL partitions (Type ANY, Subtype ANY)
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
// Loop through the iterator
if (it != NULL) {
do {
const esp_partition_t *part = esp_partition_get(it);
// Print details: Label, Type (Hex), Subtype (Hex), Offset (Hex), Size (Hex)
printf("| %-16s | 0x%02x | 0x%02x | 0x%08x | 0x%08x |\n", part->label, part->type, part->subtype, part->address,
part->size);
// Move to next partition
it = esp_partition_next(it);
} while (it != NULL);
// Release the iterator memory
esp_partition_iterator_release(it);
} else {
printf("No partitions found.\n");
}
printf("-----------------------\n");
}
#endif // DEBUG_PARTITION_TABLE
#endif // ARCH_ESP32
#if HAS_BUTTON || defined(ARCH_PORTDUINO) #if HAS_BUTTON || defined(ARCH_PORTDUINO)
#include "input/ButtonThread.h" #include "input/ButtonThread.h"
@@ -571,6 +611,7 @@ void setup()
Wire.setSCL(I2C_SCL); Wire.setSCL(I2C_SCL);
Wire.begin(); Wire.begin();
#elif defined(I2C_SDA) && !defined(ARCH_RP2040) #elif defined(I2C_SDA) && !defined(ARCH_RP2040)
LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL);
Wire.begin(I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL);
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
if (portduino_config.i2cdev != "") { if (portduino_config.i2cdev != "") {
@@ -644,7 +685,11 @@ void setup()
sensor_detected = true; sensor_detected = true;
#endif #endif
} }
#ifdef ARCH_ESP32
#ifdef DEBUG_PARTITION_TABLE
printPartitionTable();
#endif
#endif // ARCH_ESP32
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
// Don't init display if we don't have one or we are waking headless due to a timer event // Don't init display if we don't have one or we are waking headless due to a timer event
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) { if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) {
@@ -755,11 +800,12 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310);
// TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N
// scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
@@ -1538,8 +1584,9 @@ void setup()
} }
#endif #endif
uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes)
uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client)
bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for OTA handoff)
// If a thread does something that might need for it to be rescheduled ASAP it can set this flag // If a thread does something that might need for it to be rescheduled ASAP it can set this flag
// This will suppress the current delay and instead try to run ASAP. // This will suppress the current delay and instead try to run ASAP.
@@ -1652,6 +1699,9 @@ void loop()
if (dispdev) if (dispdev)
static_cast<TFTDisplay *>(dispdev)->sdlLoop(); static_cast<TFTDisplay *>(dispdev)->sdlLoop();
} }
#endif
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
messageStoreAutosaveTick();
#endif #endif
long delayMsec = mainController.runOrDelay(); long delayMsec = mainController.runOrDelay();

View File

@@ -81,6 +81,7 @@ extern uint32_t timeLastPowered;
extern uint32_t rebootAtMsec; extern uint32_t rebootAtMsec;
extern uint32_t shutdownAtMsec; extern uint32_t shutdownAtMsec;
extern bool suppressRebootBanner;
extern uint32_t serialSinceMsec; extern uint32_t serialSinceMsec;

View File

@@ -53,7 +53,7 @@
#endif #endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
#include <WiFiOTA.h> #include <MeshtasticOTA.h>
#endif #endif
NodeDB *nodeDB = nullptr; NodeDB *nodeDB = nullptr;
@@ -756,8 +756,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.display.compass_orientation = COMPASS_ORIENTATION; config.display.compass_orientation = COMPASS_ORIENTATION;
#endif #endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
if (WiFiOTA::isUpdated()) { if (MeshtasticOTA::isUpdated()) {
WiFiOTA::recoverConfig(&config.network); MeshtasticOTA::recoverConfig(&config.network);
} }
#endif #endif
@@ -823,7 +823,7 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.nag_timeout = 2; moduleConfig.external_notification.nag_timeout = 2;
#endif #endif
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ #if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \
defined(ELECROW_ThinkNode_M6) defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6)
// Default to PIN_LED2 for external notification output (LED color depends on device variant) // Default to PIN_LED2 for external notification output (LED color depends on device variant)
moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = PIN_LED2; moduleConfig.external_notification.output = PIN_LED2;
@@ -1264,6 +1264,23 @@ void NodeDB::loadFromDisk()
if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) {
LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version);
installDefaultDeviceState(); installDefaultDeviceState();
// Attempt recovery of owner fields from our own NodeDB entry if available.
meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum());
if (us && us->has_user) {
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
us->num);
memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name));
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name));
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
owner.is_licensed = us->user.is_licensed;
owner.has_is_unmessagable = us->user.has_is_unmessagable;
owner.is_unmessagable = us->user.is_unmessagable;
// Save the recovered owner to device state on disk
saveToDisk(SEGMENT_DEVICESTATE);
}
} else { } else {
LOG_INFO("Loaded saved devicestate version %d", devicestate.version); LOG_INFO("Loaded saved devicestate version %d", devicestate.version);
} }

View File

@@ -66,7 +66,7 @@ typedef enum _meshtastic_Config_DeviceConfig_Role {
but should not be given priority over other routers in order to avoid unnecessaraily but should not be given priority over other routers in order to avoid unnecessaraily
consuming hops. */ consuming hops. */
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11,
/* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. /* Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */

View File

@@ -298,6 +298,8 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_MESHSTICK_1262 = 121, meshtastic_HardwareModel_MESHSTICK_1262 = 121,
/* LilyGo T-Beam 1W */ /* LilyGo T-Beam 1W */
meshtastic_HardwareModel_TBEAM_1_WATT = 122, meshtastic_HardwareModel_TBEAM_1_WATT = 122,
/* LilyGo T5 S3 ePaper Pro (V1 and V2) */
meshtastic_HardwareModel_T5_S3_EPAPER_PRO = 123,
/* ------------------------------------------------------------------------------------------------------------------------------------------ /* ------------------------------------------------------------------------------------------------------------------------------------------
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
------------------------------------------------------------------------------------------------------------------------------------------ */ ------------------------------------------------------------------------------------------------------------------------------------------ */

View File

@@ -9,11 +9,8 @@
#include "meshUtils.h" #include "meshUtils.h"
#include <FSCommon.h> #include <FSCommon.h>
#include <ctype.h> // for better whitespace handling #include <ctype.h> // for better whitespace handling
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH
#include "BleOta.h"
#endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
#include "WiFiOTA.h" #include "MeshtasticOTA.h"
#endif #endif
#include "Router.h" #include "Router.h"
#include "configuration.h" #include "configuration.h"
@@ -236,26 +233,51 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
reboot(r->reboot_seconds); reboot(r->reboot_seconds);
break; break;
} }
case meshtastic_AdminMessage_reboot_ota_seconds_tag: { case meshtastic_AdminMessage_ota_request_tag: {
int32_t s = r->reboot_ota_seconds;
#if defined(ARCH_ESP32) #if defined(ARCH_ESP32)
#if !MESHTASTIC_EXCLUDE_BLUETOOTH LOG_INFO("OTA Requested");
if (!BleOta::getOtaAppVersion().isEmpty()) {
if (r->ota_request.ota_hash.size != 32) {
suppressRebootBanner = true;
sendWarningAndLog("Cannot start OTA: Invalid `ota_hash` provided.");
break;
}
meshtastic_OTAMode mode = r->ota_request.reboot_ota_mode;
const char *mode_name = (mode == METHOD_OTA_BLE ? "BLE" : "WiFi");
// Check that we have an OTA partition
const esp_partition_t *part = MeshtasticOTA::getAppPartition();
if (part == NULL) {
suppressRebootBanner = true;
sendWarningAndLog("Cannot start OTA: Cannot find OTA Loader partition.");
break;
}
static esp_app_desc_t app_desc;
if (!MeshtasticOTA::getAppDesc(part, &app_desc)) {
suppressRebootBanner = true;
sendWarningAndLog("Cannot start OTA: Device does have a valid OTA Loader.");
break;
}
if (!MeshtasticOTA::checkOTACapability(&app_desc, mode)) {
suppressRebootBanner = true;
sendWarningAndLog("OTA Loader does not support %s", mode_name);
break;
}
if (MeshtasticOTA::trySwitchToOTA()) {
suppressRebootBanner = true;
if (screen) if (screen)
screen->startFirmwareUpdateScreen(); screen->startFirmwareUpdateScreen();
BleOta::switchToOtaApp(); MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes);
LOG_INFO("Rebooting to BLE OTA"); sendWarningAndLog("Rebooting to %s OTA", mode_name);
} else {
sendWarningAndLog("Unable to switch to the OTA partition.");
} }
#endif #endif
#if !MESHTASTIC_EXCLUDE_WIFI int s = 1; // Reboot in 1 second, hard coded
if (WiFiOTA::trySwitchToOTA()) {
if (screen)
screen->startFirmwareUpdateScreen();
WiFiOTA::saveConfig(&config.network);
LOG_INFO("Rebooting to WiFi OTA");
}
#endif
#endif
LOG_INFO("Reboot in %d seconds", s); LOG_INFO("Reboot in %d seconds", s);
rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000);
break; break;
@@ -1474,15 +1496,43 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent
#endif #endif
} }
void AdminModule::sendWarning(const char *message) void AdminModule::sendWarning(const char *format, ...)
{ {
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
if (!cn)
return;
cn->level = meshtastic_LogRecord_Level_WARNING; cn->level = meshtastic_LogRecord_Level_WARNING;
cn->time = getValidTime(RTCQualityFromNet); cn->time = getValidTime(RTCQualityFromNet);
strncpy(cn->message, message, sizeof(cn->message));
va_list args;
va_start(args, format);
// Format the arguments directly into the notification object
vsnprintf(cn->message, sizeof(cn->message), format, args);
va_end(args);
service->sendClientNotification(cn); service->sendClientNotification(cn);
} }
void AdminModule::sendWarningAndLog(const char *format, ...)
{
// We need a temporary buffer to hold the formatted text so we can log it
// Using 250 bytes as a safe upper limit for typical text notifications
char buf[250];
va_list args;
va_start(args, format);
vsnprintf(buf, sizeof(buf), format, args);
va_end(args);
LOG_WARN(buf);
// 2. Call sendWarning
// SECURITY NOTE: We pass "%s", buf instead of just 'buf'.
// If 'buf' contained a % symbol (e.g. "Battery 50%"), passing it directly
// would crash sendWarning. "%s" treats it purely as text.
sendWarning("%s", buf);
}
void disableBluetooth() void disableBluetooth()
{ {
#if HAS_BLUETOOTH #if HAS_BLUETOOTH

View File

@@ -1,7 +1,9 @@
#include <sys/types.h>
#pragma once #pragma once
#ifdef ESP_PLATFORM
#include <esp_ota_ops.h>
#endif
#include "ProtobufModule.h" #include "ProtobufModule.h"
#include <sys/types.h>
#if HAS_WIFI #if HAS_WIFI
#include "mesh/wifi/WiFiAPClient.h" #include "mesh/wifi/WiFiAPClient.h"
#endif #endif
@@ -71,7 +73,8 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
bool messageIsResponse(const meshtastic_AdminMessage *r); bool messageIsResponse(const meshtastic_AdminMessage *r);
bool messageIsRequest(const meshtastic_AdminMessage *r); bool messageIsRequest(const meshtastic_AdminMessage *r);
void sendWarning(const char *message); void sendWarning(const char *format, ...) __attribute__((format(printf, 2, 3)));
void sendWarningAndLog(const char *format, ...) __attribute__((format(printf, 2, 3)));
}; };
static constexpr const char *licensedModeMessage = static constexpr const char *licensedModeMessage =

View File

@@ -252,9 +252,9 @@ void setupModules()
(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) {
new EnvironmentTelemetryModule(); new EnvironmentTelemetryModule();
} }
#if __has_include("Adafruit_PM25AQI.h") #if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && if (moduleConfig.has_telemetry &&
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) {
new AirQualityTelemetryModule(); new AirQualityTelemetryModule();
} }
#endif #endif

View File

@@ -64,8 +64,9 @@ SerialModule *serialModule;
SerialModuleRadio *serialModuleRadio; SerialModuleRadio *serialModuleRadio;
#if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \ #if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \
defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || \ defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M5) || \
defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE)
SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial")
{ {
api_type = TYPE_SERIAL; api_type = TYPE_SERIAL;
@@ -205,8 +206,9 @@ int32_t SerialModule::runOnce()
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
} }
#elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ #elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \
!defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \
!defined(MUZI_BASE) !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE)
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
#ifdef ARCH_RP2040 #ifdef ARCH_RP2040
Serial2.setFIFOSize(RX_BUFFER); Serial2.setFIFOSize(RX_BUFFER);
@@ -263,7 +265,8 @@ int32_t SerialModule::runOnce()
} }
#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \
!defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \
!defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE)
else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
processWXSerial(); processWXSerial();
@@ -539,7 +542,10 @@ void SerialModule::processWXSerial()
{ {
#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \
!defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \
!defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) !defined(ELECROW_ThinkNode_M3) && \
!defined(ELECROW_ThinkNode_M4) && \
!defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE)
static unsigned int lastAveraged = 0; static unsigned int lastAveraged = 0;
static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
static double dir_sum_sin = 0; static double dir_sum_sin = 0;

View File

@@ -13,6 +13,8 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule")
{ {
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
powerStatusObserver.observe(&powerStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus);
if (inputBroker)
inputObserver.observe(inputBroker);
} }
int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
@@ -60,6 +62,12 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg)
return 0; return 0;
}; };
int StatusLEDModule::handleInputEvent(const InputEvent *event)
{
lastUserbuttonTime = millis();
return 0;
}
int32_t StatusLEDModule::runOnce() int32_t StatusLEDModule::runOnce()
{ {
my_interval = 1000; my_interval = 1000;
@@ -103,6 +111,21 @@ int32_t StatusLEDModule::runOnce()
PAIRING_LED_state = LED_STATE_ON; PAIRING_LED_state = LED_STATE_ON;
} }
bool chargeIndicatorLED1 = LED_STATE_OFF;
bool chargeIndicatorLED2 = LED_STATE_OFF;
bool chargeIndicatorLED3 = LED_STATE_OFF;
bool chargeIndicatorLED4 = LED_STATE_OFF;
if (lastUserbuttonTime + 10 * 1000 > millis() || CHARGE_LED_state == LED_STATE_ON) {
// should this be off at very low percentages?
chargeIndicatorLED1 = LED_STATE_ON;
if (powerStatus && powerStatus->getBatteryChargePercent() >= 25)
chargeIndicatorLED2 = LED_STATE_ON;
if (powerStatus && powerStatus->getBatteryChargePercent() >= 50)
chargeIndicatorLED3 = LED_STATE_ON;
if (powerStatus && powerStatus->getBatteryChargePercent() >= 75)
chargeIndicatorLED4 = LED_STATE_ON;
}
#ifdef LED_CHARGE #ifdef LED_CHARGE
digitalWrite(LED_CHARGE, CHARGE_LED_state); digitalWrite(LED_CHARGE, CHARGE_LED_state);
#endif #endif
@@ -111,5 +134,18 @@ int32_t StatusLEDModule::runOnce()
digitalWrite(LED_PAIRING, PAIRING_LED_state); digitalWrite(LED_PAIRING, PAIRING_LED_state);
#endif #endif
#ifdef Battery_LED_1
digitalWrite(Battery_LED_1, chargeIndicatorLED1);
#endif
#ifdef Battery_LED_2
digitalWrite(Battery_LED_2, chargeIndicatorLED2);
#endif
#ifdef Battery_LED_3
digitalWrite(Battery_LED_3, chargeIndicatorLED3);
#endif
#ifdef Battery_LED_4
digitalWrite(Battery_LED_4, chargeIndicatorLED4);
#endif
return (my_interval); return (my_interval);
} }

View File

@@ -5,6 +5,7 @@
#include "PowerStatus.h" #include "PowerStatus.h"
#include "concurrency/OSThread.h" #include "concurrency/OSThread.h"
#include "configuration.h" #include "configuration.h"
#include "input/InputBroker.h"
#include <Arduino.h> #include <Arduino.h>
#include <functional> #include <functional>
@@ -17,6 +18,8 @@ class StatusLEDModule : private concurrency::OSThread
int handleStatusUpdate(const meshtastic::Status *); int handleStatusUpdate(const meshtastic::Status *);
int handleInputEvent(const InputEvent *arg);
protected: protected:
unsigned int my_interval = 1000; // interval in millisconds unsigned int my_interval = 1000; // interval in millisconds
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
@@ -25,12 +28,15 @@ class StatusLEDModule : private concurrency::OSThread
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
CallbackObserver<StatusLEDModule, const meshtastic::Status *> powerStatusObserver = CallbackObserver<StatusLEDModule, const meshtastic::Status *> powerStatusObserver =
CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver<StatusLEDModule, const meshtastic::Status *>(this, &StatusLEDModule::handleStatusUpdate);
CallbackObserver<StatusLEDModule, const InputEvent *> inputObserver =
CallbackObserver<StatusLEDModule, const InputEvent *>(this, &StatusLEDModule::handleInputEvent);
private: private:
bool CHARGE_LED_state = LED_STATE_OFF; bool CHARGE_LED_state = LED_STATE_OFF;
bool PAIRING_LED_state = LED_STATE_OFF; bool PAIRING_LED_state = LED_STATE_OFF;
uint32_t PAIRING_LED_starttime = 0; uint32_t PAIRING_LED_starttime = 0;
uint32_t lastUserbuttonTime = 0;
uint32_t POWER_LED_starttime = 0; uint32_t POWER_LED_starttime = 0;
bool doing_fast_blink = false; bool doing_fast_blink = false;

View File

@@ -1,6 +1,6 @@
#include "configuration.h" #include "configuration.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "AirQualityTelemetry.h" #include "AirQualityTelemetry.h"
@@ -10,27 +10,54 @@
#include "PowerFSM.h" #include "PowerFSM.h"
#include "RTC.h" #include "RTC.h"
#include "Router.h" #include "Router.h"
#include "detect/ScanI2CTwoWire.h" #include "Sensor/AddI2CSensorTemplate.h"
#include "UnitConversions.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "main.h" #include "main.h"
#include "sleep.h"
#include <Throttle.h> #include <Throttle.h>
#ifndef PMSA003I_WARMUP_MS // Sensors
// from the PMSA003I datasheet: #include "Sensor/PMSA003ISensor.h"
// "Stable data should be got at least 30 seconds after the sensor wakeup
// from the sleep mode because of the fans performance."
#define PMSA003I_WARMUP_MS 30000
#endif
int32_t AirQualityTelemetryModule::runOnce() void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{ {
if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) {
return;
}
LOG_INFO("Air Quality Telemetry adding I2C devices...");
/* /*
Uncomment the preferences below if you want to use the module Uncomment the preferences below if you want to use the module
without having to configure it from the PythonAPI or WebUI. without having to configure it from the PythonAPI or WebUI.
Note: this was previously on runOnce, which didnt take effect
as other modules already had already been initialized (screen)
*/ */
// moduleConfig.telemetry.air_quality_enabled = 1; // moduleConfig.telemetry.air_quality_enabled = 1;
// moduleConfig.telemetry.air_quality_screen_enabled = 1;
// moduleConfig.telemetry.air_quality_interval = 15;
if (!(moduleConfig.telemetry.air_quality_enabled)) { // order by priority of metrics/values (low top, high bottom)
addSensor<PMSA003ISensor>(i2cScanner, ScanI2C::DeviceType::PMSA003I);
}
int32_t AirQualityTelemetryModule::runOnce()
{
if (sleepOnNextExecution == true) {
sleepOnNextExecution = false;
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs);
LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs);
doDeepSleep(nightyNightMs, true, false);
}
uint32_t result = UINT32_MAX;
if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled ||
AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) {
// If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it
return disable(); return disable();
} }
@@ -42,82 +69,152 @@ int32_t AirQualityTelemetryModule::runOnce()
if (moduleConfig.telemetry.air_quality_enabled) { if (moduleConfig.telemetry.air_quality_enabled) {
LOG_INFO("Air quality Telemetry: init"); LOG_INFO("Air quality Telemetry: init");
#ifdef PMSA003I_ENABLE_PIN // check if we have at least one sensor
// put the sensor to sleep on startup if (!sensors.empty()) {
pinMode(PMSA003I_ENABLE_PIN, OUTPUT); result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
#endif /* PMSA003I_ENABLE_PIN */
if (!aqi.begin_I2C()) {
#ifndef I2C_NO_RESCAN
LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan");
// rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty.
uint8_t i2caddr_scan[] = {PMSA0031_ADDR};
uint8_t i2caddr_asize = 1;
auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire());
#if defined(I2C_SDA1)
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize);
#endif
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize);
auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031);
if (found.type != ScanI2C::DeviceType::NONE) {
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address;
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
i2cScanner->fetchI2CBus(found.address);
return setStartDelay();
}
#endif
return disable();
} }
return setStartDelay();
} }
return disable();
// it's possible to have this module enabled, only for displaying values on the screen.
// therefore, we should only enable the sensor loop if measurement is also enabled
return result == UINT32_MAX ? disable() : setStartDelay();
} else { } else {
// if we somehow got to a second run of this module with measurement disabled, then just wait forever // if we somehow got to a second run of this module with measurement disabled, then just wait forever
if (!moduleConfig.telemetry.air_quality_enabled) if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) {
return disable();
switch (state) {
#ifdef PMSA003I_ENABLE_PIN
case State::IDLE:
// sensor is in standby; fire it up and sleep
LOG_DEBUG("runOnce(): state = idle");
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
return PMSA003I_WARMUP_MS;
#endif /* PMSA003I_ENABLE_PIN */
case State::ACTIVE:
// sensor is already warmed up; grab telemetry and send it
LOG_DEBUG("runOnce(): state = active");
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
} else if (service->isToPhoneQueueEmpty()) {
// Just send to phone when it's not our time to send to mesh yet
// Only send while queue is empty (phone assumed connected)
sendTelemetry(NODENUM_BROADCAST, true);
}
#ifdef PMSA003I_ENABLE_PIN
// put sensor back to sleep
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
#endif /* PMSA003I_ENABLE_PIN */
return sendToPhoneIntervalMs;
default:
return disable(); return disable();
} }
// Wake up the sensors that need it
LOG_INFO("Waking up sensors");
for (TelemetrySensor *sensor : sensors) {
if (!sensor->isActive()) {
return sensor->wakeUp();
}
}
if (((lastSentToMesh == 0) ||
!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled(
moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs, numOnlineNodes))) &&
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
airTime->isTxAllowedAirUtil()) {
sendTelemetry();
lastSentToMesh = millis();
} else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
(service->isToPhoneQueueEmpty())) {
// Just send to phone when it's not our time to send to mesh yet
// Only send while queue is empty (phone assumed connected)
sendTelemetry(NODENUM_BROADCAST, true);
lastSentToPhone = millis();
}
// Send to sleep sensors that consume power
LOG_INFO("Sending sensors to sleep");
for (TelemetrySensor *sensor : sensors) {
sensor->sleep();
}
} }
return min(sendToPhoneIntervalMs, result);
} }
bool AirQualityTelemetryModule::wantUIFrame()
{
return moduleConfig.telemetry.air_quality_screen_enabled;
}
#if HAS_SCREEN
void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// === Setup display ===
display->clear();
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
int line = 1;
// === Set Title
const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Air Quality" : "AQ.";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
// === Row spacing setup ===
const int rowHeight = FONT_HEIGHT_SMALL - 4;
int currentY = graphics::getTextPositions(display)[line++];
// === Show "No Telemetry" if no data available ===
if (!lastMeasurementPacket) {
display->drawString(x, currentY, "No Telemetry");
return;
}
// Decode the telemetry message from the latest received packet
const meshtastic_Data &p = lastMeasurementPacket->decoded;
meshtastic_Telemetry telemetry;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) {
display->drawString(x, currentY, "No Telemetry");
return;
}
const auto &m = telemetry.variant.air_quality_metrics;
// Check if any telemetry field has valid data
bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental ||
m.has_pm25_environmental || m.has_pm100_environmental;
if (!hasAny) {
display->drawString(x, currentY, "No Telemetry");
return;
}
// === First line: Show sender name + time since received (left), and first metric (right) ===
const char *sender = getSenderShortName(*lastMeasurementPacket);
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
String agoStr = (agoSecs > 864000) ? "?"
: (agoSecs > 3600) ? String(agoSecs / 3600) + "h"
: (agoSecs > 60) ? String(agoSecs / 60) + "m"
: String(agoSecs) + "s";
String leftStr = String(sender) + " (" + agoStr + ")";
display->drawString(x, currentY, leftStr); // Left side: who and when
// === Collect sensor readings as label strings (no icons) ===
std::vector<String> entries;
if (m.has_pm10_standard)
entries.push_back("PM1: " + String(m.pm10_standard) + "ug/m3");
if (m.has_pm25_standard)
entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3");
if (m.has_pm100_standard)
entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3");
// === Show first available metric on top-right of first line ===
if (!entries.empty()) {
String valueStr = entries.front();
int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr);
display->drawString(rightX, currentY, valueStr);
entries.erase(entries.begin()); // Remove from queue
}
// === Advance to next line for remaining telemetry entries ===
currentY += rowHeight;
// === Draw remaining entries in 2-column format (left and right) ===
for (size_t i = 0; i < entries.size(); i += 2) {
// Left column
display->drawString(x, currentY, entries[i]);
// Right column if it exists
if (i + 1 < entries.size()) {
int rightX = SCREEN_WIDTH / 2;
display->drawString(rightX, currentY, entries[i + 1]);
}
currentY += rowHeight;
}
graphics::drawCommonFooter(display, x, y);
}
#endif
bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
{ {
if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
@@ -144,35 +241,21 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
{ {
if (!aqi.read(&data)) { bool valid = true;
LOG_WARN("Skip send measurements. Could not read AQIn"); bool hasSensor = false;
return false;
}
m->time = getTime(); m->time = getTime();
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics.has_pm10_standard = true; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
m->variant.air_quality_metrics.pm10_standard = data.pm10_standard;
m->variant.air_quality_metrics.has_pm25_standard = true;
m->variant.air_quality_metrics.pm25_standard = data.pm25_standard;
m->variant.air_quality_metrics.has_pm100_standard = true;
m->variant.air_quality_metrics.pm100_standard = data.pm100_standard;
m->variant.air_quality_metrics.has_pm10_environmental = true; // TODO - Should we check for sensor state here?
m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; // If a sensor is sleeping, we should know and check to wake it up
m->variant.air_quality_metrics.has_pm25_environmental = true; for (TelemetrySensor *sensor : sensors) {
m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; LOG_INFO("Reading AQ sensors");
m->variant.air_quality_metrics.has_pm100_environmental = true; valid = valid && sensor->getMetrics(m);
m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; hasSensor = true;
}
LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, return valid && hasSensor;
m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard);
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental,
m->variant.air_quality_metrics.pm100_environmental);
return true;
} }
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
@@ -206,7 +289,15 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
{ {
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m.time = getTime();
if (getAirQualityTelemetry(&m)) { if (getAirQualityTelemetry(&m)) {
LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \
pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u",
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard,
m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental,
m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental);
meshtastic_MeshPacket *p = allocDataProtobuf(m); meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest; p->to = dest;
p->decoded.want_response = false; p->decoded.want_response = false;
@@ -221,16 +312,44 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
lastMeasurementPacket = packetPool.allocCopy(*p); lastMeasurementPacket = packetPool.allocCopy(*p);
if (phoneOnly) { if (phoneOnly) {
LOG_INFO("Send packet to phone"); LOG_INFO("Sending packet to phone");
service->sendToPhone(p); service->sendToPhone(p);
} else { } else {
LOG_INFO("Send packet to mesh"); LOG_INFO("Sending packet to mesh");
service->sendToMesh(p, RX_SRC_LOCAL, true); service->sendToMesh(p, RX_SRC_LOCAL, true);
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) {
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
notification->level = meshtastic_LogRecord_Level_INFO;
notification->time = getValidTime(RTCQualityFromNet);
sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment",
Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval,
default_telemetry_broadcast_interval_secs) /
1000U);
service->sendClientNotification(notification);
sleepOnNextExecution = true;
LOG_DEBUG("Start next execution in 5s, then sleep");
setIntervalFromNow(FIVE_SECONDS_MS);
}
} }
return true; return true;
} }
return false; return false;
} }
AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
meshtastic_AdminMessage *request,
meshtastic_AdminMessage *response)
{
AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED;
for (TelemetrySensor *sensor : sensors) {
result = sensor->handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
return result;
}
#endif #endif

View File

@@ -1,14 +1,23 @@
#include "configuration.h" #include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#pragma once #pragma once
#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE
#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0
#endif
#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Adafruit_PM25AQI.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "ProtobufModule.h" #include "ProtobufModule.h"
#include "detect/ScanI2CConsumer.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry> class AirQualityTelemetryModule : private concurrency::OSThread,
public ScanI2CConsumer,
public ProtobufModule<meshtastic_Telemetry>
{ {
CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *> nodeStatusObserver = CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *> nodeStatusObserver =
CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *>(this, CallbackObserver<AirQualityTelemetryModule, const meshtastic::Status *>(this,
@@ -16,22 +25,19 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf
public: public:
AirQualityTelemetryModule() AirQualityTelemetryModule()
: concurrency::OSThread("AirQualityTelemetry"), : concurrency::OSThread("AirQualityTelemetry"), ScanI2CConsumer(),
ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
{ {
lastMeasurementPacket = nullptr; lastMeasurementPacket = nullptr;
setIntervalFromNow(10 * 1000);
aqi = Adafruit_PM25AQI();
nodeStatusObserver.observe(&nodeStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus);
setIntervalFromNow(10 * 1000);
#ifdef PMSA003I_ENABLE_PIN
// the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking
// a reading
state = State::IDLE;
#else
state = State::ACTIVE;
#endif
} }
virtual bool wantUIFrame() override;
#if !HAS_SCREEN
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
#else
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
#endif
protected: protected:
/** Called to handle a particular incoming message /** Called to handle a particular incoming message
@@ -49,19 +55,17 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf
*/ */
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
private: virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
enum State { meshtastic_AdminMessage *request,
IDLE = 0, meshtastic_AdminMessage *response) override;
ACTIVE = 1, void i2cScanFinished(ScanI2C *i2cScanner);
};
State state; private:
Adafruit_PM25AQI aqi;
PM25_AQI_Data data = {0};
bool firstTime = true; bool firstTime = true;
meshtastic_MeshPacket *lastMeasurementPacket; meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0; uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
}; };
#endif #endif

View File

@@ -141,37 +141,10 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
#include "Sensor/AddI2CSensorTemplate.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include <Throttle.h> #include <Throttle.h>
#include <forward_list>
static std::forward_list<TelemetrySensor *> sensors;
template <typename T> void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type)
{
ScanI2C::FoundDevice dev = i2cScanner->find(type);
if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) {
TelemetrySensor *sensor = new T();
#if WIRE_INTERFACES_COUNT > 1
TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address);
if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) {
// This sensor only works on Wire (Wire1 is not supported)
delete sensor;
return;
}
#else
TwoWire *bus = &Wire;
#endif
if (sensor->initDevice(bus, &dev)) {
sensors.push_front(sensor);
return;
}
// destroy sensor
delete sensor;
}
}
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
{ {
if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) {
@@ -642,8 +615,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature,
m.variant.environment_metrics.soil_moisture); m.variant.environment_metrics.soil_moisture);
sensor_read_error_count = 0;
meshtastic_MeshPacket *p = allocDataProtobuf(m); meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest; p->to = dest;
p->decoded.want_response = false; p->decoded.want_response = false;

View File

@@ -67,7 +67,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread,
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0; uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0; uint32_t lastSentToPhone = 0;
uint32_t sensor_read_error_count = 0;
}; };
#endif #endif

View File

@@ -0,0 +1,34 @@
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "TelemetrySensor.h"
#include "detect/ScanI2C.h"
#include "detect/ScanI2CTwoWire.h"
#include <Wire.h>
#include <forward_list>
static std::forward_list<TelemetrySensor *> sensors;
template <typename T> void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type)
{
ScanI2C::FoundDevice dev = i2cScanner->find(type);
if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) {
TelemetrySensor *sensor = new T();
#if WIRE_INTERFACES_COUNT > 1
TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address);
if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) {
// This sensor only works on Wire (Wire1 is not supported)
delete sensor;
return;
}
#else
TwoWire *bus = &Wire;
#endif
if (sensor->initDevice(bus, &dev)) {
sensors.push_front(sensor);
return;
}
// destroy sensor
delete sensor;
}
}
#endif

View File

@@ -0,0 +1,158 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../detect/reClockI2C.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "PMSA003ISensor.h"
#include "TelemetrySensor.h"
#include <Wire.h>
PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {}
bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
{
LOG_INFO("Init sensor: %s", sensorName);
#ifdef PMSA003I_ENABLE_PIN
pinMode(PMSA003I_ENABLE_PIN, OUTPUT);
#endif
_bus = bus;
_address = dev->address.address;
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
if (!currentClock) {
LOG_WARN("PMSA003I can't be used at this clock speed");
return false;
}
#endif
_bus->beginTransmission(_address);
if (_bus->endTransmission() != 0) {
LOG_WARN("PMSA003I not found on I2C at 0x12");
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus);
#endif
status = 1;
LOG_INFO("PMSA003I Enabled");
initI2CSensor();
return true;
}
bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement)
{
if (!isActive()) {
LOG_WARN("PMSA003I is not active");
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus);
#endif
_bus->requestFrom(_address, PMSA003I_FRAME_LENGTH);
if (_bus->available() < PMSA003I_FRAME_LENGTH) {
LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available());
return false;
}
#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C)
reClockI2C(currentClock, _bus);
#endif
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) {
buffer[i] = _bus->read();
}
if (buffer[0] != 0x42 || buffer[1] != 0x4D) {
LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]);
return false;
}
auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; };
computedChecksum = 0;
for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) {
computedChecksum += buffer[i];
}
receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2);
if (computedChecksum != receivedChecksum) {
LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum);
return false;
}
measurement->variant.air_quality_metrics.has_pm10_standard = true;
measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4);
measurement->variant.air_quality_metrics.has_pm25_standard = true;
measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6);
measurement->variant.air_quality_metrics.has_pm100_standard = true;
measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8);
// TODO - Add admin command to remove environmental metrics to save protobuf space
measurement->variant.air_quality_metrics.has_pm10_environmental = true;
measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10);
measurement->variant.air_quality_metrics.has_pm25_environmental = true;
measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12);
measurement->variant.air_quality_metrics.has_pm100_environmental = true;
measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14);
// TODO - Add admin command to remove PN to save protobuf space
measurement->variant.air_quality_metrics.has_particles_03um = true;
measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16);
measurement->variant.air_quality_metrics.has_particles_05um = true;
measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18);
measurement->variant.air_quality_metrics.has_particles_10um = true;
measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20);
measurement->variant.air_quality_metrics.has_particles_25um = true;
measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22);
measurement->variant.air_quality_metrics.has_particles_50um = true;
measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24);
measurement->variant.air_quality_metrics.has_particles_100um = true;
measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26);
return true;
}
bool PMSA003ISensor::isActive()
{
return state == State::ACTIVE;
}
void PMSA003ISensor::sleep()
{
#ifdef PMSA003I_ENABLE_PIN
digitalWrite(PMSA003I_ENABLE_PIN, LOW);
state = State::IDLE;
#endif
}
uint32_t PMSA003ISensor::wakeUp()
{
#ifdef PMSA003I_ENABLE_PIN
LOG_INFO("Waking up PMSA003I");
digitalWrite(PMSA003I_ENABLE_PIN, HIGH);
state = State::ACTIVE;
return PMSA003I_WARMUP_MS;
#endif
// No need to wait for warmup if already active
return 0;
}
#endif

View File

@@ -0,0 +1,35 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#define PMSA003I_I2C_CLOCK_SPEED 100000
#define PMSA003I_FRAME_LENGTH 32
#define PMSA003I_WARMUP_MS 30000
class PMSA003ISensor : public TelemetrySensor
{
public:
PMSA003ISensor();
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
virtual bool isActive() override;
virtual void sleep() override;
virtual uint32_t wakeUp() override;
private:
enum class State { IDLE, ACTIVE };
State state = State::ACTIVE;
uint16_t computedChecksum = 0;
uint16_t receivedChecksum = 0;
uint8_t buffer[PMSA003I_FRAME_LENGTH]{};
TwoWire *_bus{};
uint8_t _address{};
};
#endif

View File

@@ -1,6 +1,6 @@
#include "configuration.h" #include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "NodeDB.h" #include "NodeDB.h"

View File

@@ -58,6 +58,11 @@ class TelemetrySensor
// TODO: delete after migration // TODO: delete after migration
bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; }
// Functions to sleep / wakeup sensors that support it
virtual void sleep(){};
virtual uint32_t wakeUp() { return 0; }
// Return active by default, override per sensor
virtual bool isActive() { return true; }
#if WIRE_INTERFACES_COUNT > 1 #if WIRE_INTERFACES_COUNT > 1
// Set to true if Implementation only works first I2C port (Wire) // Set to true if Implementation only works first I2C port (Wire)
@@ -65,6 +70,7 @@ class TelemetrySensor
#endif #endif
virtual int32_t runOnce() { return INT32_MAX; } virtual int32_t runOnce() { return INT32_MAX; }
virtual bool isInitialized() { return initialized; } virtual bool isInitialized() { return initialized; }
// TODO: is this used?
virtual bool isRunning() { return status > 0; } virtual bool isRunning() { return status > 0; }
virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0;

View File

@@ -47,7 +47,6 @@ int32_t ICM20948Sensor::runOnce()
int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce()
{ {
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
#if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze
if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) {
if (!isAsleep) { if (!isAsleep) {
LOG_DEBUG("sleeping IMU"); LOG_DEBUG("sleeping IMU");
@@ -60,7 +59,6 @@ int32_t ICM20948Sensor::runOnce()
sensor->sleep(false); sensor->sleep(false);
isAsleep = false; isAsleep = false;
} }
#endif
float magX = 0, magY = 0, magZ = 0; float magX = 0, magY = 0, magZ = 0;
if (sensor->dataReady()) { if (sensor->dataReady()) {

View File

@@ -82,8 +82,8 @@ class ICM20948Sensor : public MotionSensor
private: private:
ICM20948Singleton *sensor = nullptr; ICM20948Singleton *sensor = nullptr;
bool showingScreen = false; bool showingScreen = false;
#ifdef MUZI_BASE
bool isAsleep = false; bool isAsleep = false;
#ifdef MUZI_BASE
float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000,
lowestZ = 98.000000; lowestZ = 98.000000;
#else #else

View File

@@ -1,46 +0,0 @@
#include "BleOta.h"
#include "Arduino.h"
#include <esp_ota_ops.h>
static const String MESHTASTIC_OTA_APP_PROJECT_NAME("Meshtastic-OTA");
const esp_partition_t *BleOta::findEspOtaAppPartition()
{
const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr);
esp_app_desc_t app_desc;
esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) {
part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr);
ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
}
if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) {
return part;
} else {
return nullptr;
}
}
String BleOta::getOtaAppVersion()
{
const esp_partition_t *part = findEspOtaAppPartition();
esp_app_desc_t app_desc;
esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc));
String version;
if (ret == ESP_OK) {
version = app_desc.version;
}
return version;
}
bool BleOta::switchToOtaApp()
{
bool success = false;
const esp_partition_t *part = findEspOtaAppPartition();
if (part) {
success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK);
}
return success;
}

View File

@@ -1,20 +0,0 @@
#ifndef BLEOTA_H
#define BLEOTA_H
#include <Arduino.h>
#include <functional>
class BleOta
{
public:
explicit BleOta(){};
static String getOtaAppVersion();
static bool switchToOtaApp();
private:
String mUserAgent;
static const esp_partition_t *findEspOtaAppPartition();
};
#endif // BLEOTA_H

View File

@@ -1,13 +1,17 @@
#include "WiFiOTA.h" #include "MeshtasticOTA.h"
#include "configuration.h" #include "configuration.h"
#ifdef ESP_PLATFORM
#include <Preferences.h> #include <Preferences.h>
#include <esp_ota_ops.h> #include <esp_ota_ops.h>
#endif
namespace WiFiOTA namespace MeshtasticOTA
{ {
static const char *nvsNamespace = "ota-wifi"; static const char *nvsNamespace = "MeshtasticOTA";
static const char *appProjectName = "OTA-WiFi"; static const char *combinedAppProjectName = "MeshtasticOTA";
static const char *bleOnlyAppProjectName = "MeshtasticOTA-BLE";
static const char *wifiOnlyAppProjectName = "MeshtasticOTA-WiFi";
static bool updated = false; static bool updated = false;
@@ -43,12 +47,14 @@ void recoverConfig(meshtastic_Config_NetworkConfig *network)
strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk));
} }
void saveConfig(meshtastic_Config_NetworkConfig *network) void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash)
{ {
LOG_INFO("Saving WiFi settings for upcoming OTA update"); LOG_INFO("Saving WiFi settings for upcoming OTA update");
Preferences prefs; Preferences prefs;
prefs.begin(nvsNamespace); prefs.begin(nvsNamespace);
prefs.putUChar("method", method);
prefs.putBytes("ota_hash", ota_hash, 32);
prefs.putString("ssid", network->wifi_ssid); prefs.putString("ssid", network->wifi_ssid);
prefs.putString("psk", network->wifi_psk); prefs.putString("psk", network->wifi_psk);
prefs.putBool("updated", false); prefs.putBool("updated", false);
@@ -62,21 +68,48 @@ const esp_partition_t *getAppPartition()
bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc)
{ {
if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) {
return false; LOG_INFO("esp_ota_get_partition_description failed");
if (strcmp(app_desc->project_name, appProjectName) != 0)
return false; return false;
}
return true; return true;
} }
bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method)
{
// Combined loader supports all (both) transports, BLE and WiFi
if (strcmp(app_desc->project_name, combinedAppProjectName) == 0) {
LOG_INFO("OTA partition contains combined BLE/WiFi OTA Loader");
return true;
}
if (method == METHOD_OTA_BLE && strcmp(app_desc->project_name, bleOnlyAppProjectName) == 0) {
LOG_INFO("OTA partition contains BLE-only OTA Loader");
return true;
}
if (method == METHOD_OTA_WIFI && strcmp(app_desc->project_name, wifiOnlyAppProjectName) == 0) {
LOG_INFO("OTA partition contains WiFi-only OTA Loader");
return true;
}
LOG_INFO("OTA partition does not contain a known OTA loader");
return false;
}
bool trySwitchToOTA() bool trySwitchToOTA()
{ {
const esp_partition_t *part = getAppPartition(); const esp_partition_t *part = getAppPartition();
esp_app_desc_t app_desc;
if (!getAppDesc(part, &app_desc)) if (part == NULL) {
LOG_WARN("Unable to get app partition in preparation of OTA reboot");
return false; return false;
if (esp_ota_set_boot_partition(part) != ESP_OK) }
uint8_t result = esp_ota_set_boot_partition(part);
// Partition and app checks should now be done in the AdminModule before this is called
if (result != ESP_OK) {
LOG_WARN("Unable to switch to OTA partiton. (Reason %d)", result);
return false; return false;
}
return true; return true;
} }
@@ -89,4 +122,4 @@ const char *getVersion()
return app_desc.version; return app_desc.version;
} }
} // namespace WiFiOTA } // namespace MeshtasticOTA

View File

@@ -0,0 +1,26 @@
#ifndef MESHTASTICOTA_H
#define MESHTASTICOTA_H
#include "mesh-pb-constants.h"
#include <Arduino.h>
#ifdef ESP_PLATFORM
#include <esp_ota_ops.h>
#endif
#define METHOD_OTA_BLE 1
#define METHOD_OTA_WIFI 2
namespace MeshtasticOTA
{
void initialize();
bool isUpdated();
const esp_partition_t *getAppPartition();
bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc);
bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method);
void recoverConfig(meshtastic_Config_NetworkConfig *network);
void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash);
bool trySwitchToOTA();
const char *getVersion();
} // namespace MeshtasticOTA
#endif // MESHTASTICOTA_H

View File

@@ -1,18 +0,0 @@
#ifndef WIFIOTA_H
#define WIFIOTA_H
#include "mesh-pb-constants.h"
#include <Arduino.h>
namespace WiFiOTA
{
void initialize();
bool isUpdated();
void recoverConfig(meshtastic_Config_NetworkConfig *network);
void saveConfig(meshtastic_Config_NetworkConfig *network);
bool trySwitchToOTA();
const char *getVersion();
} // namespace WiFiOTA
#endif // WIFIOTA_H

View File

@@ -195,6 +195,8 @@
#define HW_VENDOR meshtastic_HardwareModel_LINK_32 #define HW_VENDOR meshtastic_HardwareModel_LINK_32
#elif defined(T_DECK_PRO) #elif defined(T_DECK_PRO)
#define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
#elif defined(T_BEAM_1W)
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT
#elif defined(T_LORA_PAGER) #elif defined(T_LORA_PAGER)
#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
#elif defined(HELTEC_V4) #elif defined(HELTEC_V4)

View File

@@ -5,11 +5,10 @@
#include "main.h" #include "main.h"
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
#include "BleOta.h"
#include "nimble/NimbleBluetooth.h" #include "nimble/NimbleBluetooth.h"
#endif #endif
#include <WiFiOTA.h> #include <MeshtasticOTA.h>
#if HAS_WIFI #if HAS_WIFI
#include "mesh/wifi/WiFiAPClient.h" #include "mesh/wifi/WiFiAPClient.h"
@@ -144,22 +143,14 @@ void esp32Setup()
preferences.putUInt("hwVendor", HW_VENDOR); preferences.putUInt("hwVendor", HW_VENDOR);
preferences.end(); preferences.end();
LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); LOG_DEBUG("Number of Device Reboots: %d", rebootCounter);
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
String BLEOTA = BleOta::getOtaAppVersion();
if (BLEOTA.isEmpty()) {
LOG_INFO("No BLE OTA firmware available");
} else {
LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str());
}
#endif
#if !MESHTASTIC_EXCLUDE_WIFI #if !MESHTASTIC_EXCLUDE_WIFI
String version = WiFiOTA::getVersion(); String version = MeshtasticOTA::getVersion();
if (version.isEmpty()) { if (version.isEmpty()) {
LOG_INFO("No WiFi OTA firmware available"); LOG_INFO("MeshtasticOTA firmware not available");
} else { } else {
LOG_INFO("WiFi OTA firmware version %s", version.c_str()); LOG_INFO("MeshtasticOTA firmware version %s", version.c_str());
} }
WiFiOTA::initialize(); MeshtasticOTA::initialize();
#endif #endif
// enableModemSleep(); // enableModemSleep();

View File

@@ -74,6 +74,8 @@
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M3 #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M3
#elif defined(ELECROW_ThinkNode_M6) #elif defined(ELECROW_ThinkNode_M6)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6 #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6
#elif defined(ELECROW_ThinkNode_M4)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4
#elif defined(NANO_G2_ULTRA) #elif defined(NANO_G2_ULTRA)
#define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA #define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA
#elif defined(CANARYONE) #elif defined(CANARYONE)

View File

@@ -121,6 +121,8 @@ class Power : private concurrency::OSThread
bool lipoChargerInit(); bool lipoChargerInit();
/// Setup a meshSolar battery sensor /// Setup a meshSolar battery sensor
bool meshSolarInit(); bool meshSolarInit();
/// Setup a serial battery sensor
bool serialBatteryInit();
private: private:
void shutdown(); void shutdown();

View File

@@ -49,6 +49,7 @@ build_flags =
-DLIBPAX_BLE -DLIBPAX_BLE
-DHAS_UDP_MULTICAST=1 -DHAS_UDP_MULTICAST=1
;-DDEBUG_HEAP ;-DDEBUG_HEAP
-DCAN_RECLOCK_I2C
lib_deps = lib_deps =
${arduino_base.lib_deps} ${arduino_base.lib_deps}

View File

@@ -1,9 +1,9 @@
[env:heltec-wireless-bridge] [env:heltec-wireless-bridge]
;build_type = debug ; to make it possible to step through our jtag debugger ;build_type = debug ; to make it possible to step through our jtag debugger
extends = esp32_base extends = esp32_base
board_level = extra board_level = extra
board = heltec_wifi_lora_32 board = heltec_wifi_lora_32
build_flags = build_flags =
${esp32_base.build_flags} ${esp32_base.build_flags}
-I variants/esp32/heltec_wireless_bridge -I variants/esp32/heltec_wireless_bridge
-D HELTEC_WIRELESS_BRIDGE -D HELTEC_WIRELESS_BRIDGE
@@ -13,6 +13,7 @@ build_flags =
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1
-D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
-D MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
-D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1
-D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1
-D MESHTASTIC_EXCLUDE_GPS=1 -D MESHTASTIC_EXCLUDE_GPS=1

View File

@@ -10,6 +10,8 @@ custom_meshtastic_tags = M5Stack
extends = esp32c6_base extends = esp32c6_base
board = esp32-c6-devkitc-1 board = esp32-c6-devkitc-1
board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
;OpenOCD flash method ;OpenOCD flash method
;upload_protocol = esp-builtin ;upload_protocol = esp-builtin
;Normal method ;Normal method

View File

@@ -0,0 +1,28 @@
// Need this file for ESP32-S3
// No need to modify this file, changes to pins imported from variant.h
// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#include <variant.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
// Serial
static const uint8_t TX = UART_TX;
static const uint8_t RX = UART_RX;
// Default SPI will be mapped to Radio
static const uint8_t SS = LORA_CS;
static const uint8_t SCK = LORA_SCK;
static const uint8_t MOSI = LORA_MOSI;
static const uint8_t MISO = LORA_MISO;
// The default Wire will be mapped to PMU and RTC
static const uint8_t SCL = I2C_SCL;
static const uint8_t SDA = I2C_SDA;
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,8 @@
[env:CDEBYTE_EoRa-Hub]
extends = esp32s3_base
board = CDEBYTE_EoRa-Hub
board_level = extra
build_flags =
${esp32s3_base.build_flags}
-D PRIVATE_HW
-I variants/esp32s3/CDEBYTE_EoRa-Hub

View File

@@ -0,0 +1,19 @@
#include "RadioLib.h"
// This is rewritten to match the requirements of the E80-900M2213S
// The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix.
// See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin"
// RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0
// DIO5 -> RFSW0_V1
// DIO6 -> RFSW1_V2
// DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible.
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
// mode DIO5 DIO6 DIO7
{LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}},
{LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}},
{LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}},
{LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE,
};

View File

@@ -0,0 +1,50 @@
// EByte EoRA-Hub
// Uses E80 (LR1121) LoRa module
#define LED_PIN 35
// Button - user interface
#define BUTTON_PIN 0 // BOOT button
#define BATTERY_PIN 1
#define ADC_CHANNEL ADC1_GPIO1_CHANNEL
#define ADC_MULTIPLIER 103.0 // Calibrated value
#define ADC_ATTENUATION ADC_ATTEN_DB_0
#define ADC_CTRL 37
#define ADC_CTRL_ENABLED LOW
// Display - OLED connected via I2C by the default hardware configuration
#define HAS_SCREEN 1
#define USE_SSD1306
#define I2C_SCL 17
#define I2C_SDA 18
// UART - The 1mm JST SH connector closest to the USB-C port
#define UART_TX 43
#define UART_RX 44
// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. There are no
// pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently untested
#define I2C_SCL1 21
#define I2C_SDA1 10
// Radio
#define USE_LR1121
#define LORA_SCK 9
#define LORA_MOSI 10
#define LORA_MISO 11
#define LORA_RESET 12
#define LORA_CS 8
#define LORA_DIO9 13
// LR1121
#define LR1121_IRQ_PIN 14
#define LR1121_NRESET_PIN LORA_RESET
#define LR1121_BUSY_PIN LORA_DIO9
#define LR1121_SPI_NSS_PIN LORA_CS
#define LR1121_SPI_SCK_PIN LORA_SCK
#define LR1121_SPI_MOSI_PIN LORA_MOSI
#define LR1121_SPI_MISO_PIN LORA_MISO
#define LR11X0_DIO3_TCXO_VOLTAGE 1.8
#define LR11X0_DIO_AS_RF_SWITCH

View File

@@ -34,6 +34,17 @@ build_flags =
-D I2C_SCL1=3 -D I2C_SCL1=3
[env:heltec-v4-tft] [env:heltec-v4-tft]
custom_meshtastic_hw_model = 110
custom_meshtastic_hw_model_slug = HELTEC_V4
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = Heltec V4 TFT
custom_meshtastic_images = heltec_v4.svg
custom_meshtastic_tags = Heltec
custom_meshtastic_requires_dfu = true
custom_meshtastic_partition_scheme = 16MB
extends = heltec_v4_base extends = heltec_v4_base
build_flags = build_flags =
${heltec_v4_base.build_flags} ;-Os ${heltec_v4_base.build_flags} ;-Os

View File

@@ -1,5 +1,14 @@
; LilyGo T-Beam-1W (1 Watt LoRa with external PA) ; LilyGo T-Beam-1W (1 Watt LoRa with external PA)
[env:t-beam-1w] [env:t-beam-1w]
custom_meshtastic_hw_model = 122
custom_meshtastic_hw_model_slug = TBEAM_1_WATT
custom_meshtastic_architecture = esp32s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = LILYGO T-Beam 1W
custom_meshtastic_images = tbeam-1w.svg
custom_meshtastic_tags = LilyGo
extends = esp32s3_base extends = esp32s3_base
board = t-beam-1w board = t-beam-1w
board_build.partitions = default_8MB.csv board_build.partitions = default_8MB.csv

View File

@@ -53,10 +53,13 @@
#define HAS_BMA423 1 #define HAS_BMA423 1
#define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor #define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor
#define HAS_GPS 1
#define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_DEFAULT_NOT_PRESENT 1
#define GPS_BAUDRATE 38400 #define GPS_BAUDRATE 38400
#define GPS_RX_PIN 42 #define GPS_RX_PIN 41
#define GPS_TX_PIN 41 #define GPS_TX_PIN 42
#define BUTTON_PIN 0 // only for Plus version
#define USE_SX1262 #define USE_SX1262
#define USE_SX1268 #define USE_SX1268

View File

@@ -41,7 +41,7 @@ lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.3 lewisxhe/SensorLib@0.3.3
# renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver
https://github.com/pschatzmann/arduino-audio-driver/archive/v0.1.3.zip https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.0.zip
# TODO renovate # TODO renovate
https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
# TODO renovate # TODO renovate

View File

@@ -0,0 +1,15 @@
; ThinkNode M4 - Powerbank nrf52840/LR1110 by Elecrow
[env:thinknode_m4]
extends = nrf52840_base
board = ThinkNode-M4
board_check = true
debug_tool = jlink
build_flags = ${nrf52840_base.build_flags}
-Ivariants/nrf52840/ELECROW-ThinkNode-M4
-DELECROW_ThinkNode_M4
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4>
lib_deps =
${nrf52840_base.lib_deps}
lewisxhe/PCF8563_Library@^1.0.1

View File

@@ -0,0 +1,11 @@
#include "RadioLib.h"
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
// mode DIO5 DIO6
{LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}},
{LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}},
{LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}},
{LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE,
};

View File

@@ -0,0 +1,51 @@
/*
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
Copyright (c) 2016 Sandeep Mistry All right reserved.
Copyright (c) 2018, Adafruit Industries (adafruit.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "variant.h"
#include "nrf.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const uint32_t g_ADigitalPinMap[] = {
// P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
// P1
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
void initVariant()
{
pinMode(PIN_LED2, OUTPUT);
ledOff(PIN_LED2);
pinMode(LED_PAIRING, OUTPUT);
ledOff(LED_PAIRING);
pinMode(Battery_LED_1, OUTPUT);
ledOff(Battery_LED_1);
pinMode(Battery_LED_2, OUTPUT);
ledOff(Battery_LED_2);
pinMode(Battery_LED_3, OUTPUT);
ledOff(Battery_LED_3);
pinMode(Battery_LED_4, OUTPUT);
ledOff(Battery_LED_4);
}

View File

@@ -0,0 +1,142 @@
/*
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
Copyright (c) 2016 Sandeep Mistry All right reserved.
Copyright (c) 2018, Adafruit Industries (adafruit.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _VARIANT_ELECROW_THINKNODE_M4_
#define _VARIANT_ELECROW_THINKNODE_M4_
/** Master clock frequency */
#define VARIANT_MCK (64000000ul)
#define USE_LFXO
/*----------------------------------------------------------------------------
* Headers
*----------------------------------------------------------------------------*/
#include "WVariant.h"
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
// LEDs
#define LED_BUILTIN -1
#define LED_BLUE -1
#define PIN_LED2 (32 + 9)
#define LED_PAIRING (13)
#define Battery_LED_1 (15)
#define Battery_LED_2 (17)
#define Battery_LED_3 (32 + 2)
#define Battery_LED_4 (32 + 4)
#define LED_STATE_ON 1
// Button
#define PIN_BUTTON1 (4)
// Battery ADC
#define PIN_A0 (2)
#define BATTERY_PIN PIN_A0
#define BATTERY_SENSE_SAMPLES 30
#define ADC_RESOLUTION 14
#define BATTERY_SENSE_RESOLUTION_BITS 12
#define BATTERY_SENSE_RESOLUTION 4096.0
#define ADC_MULTIPLIER (2.00F)
#undef AREF_VOLTAGE
#define AREF_VOLTAGE 3.0
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
#define HAS_SERIAL_BATTERY_LEVEL 1
#define SERIAL_BATTERY_RX 30
#define SERIAL_BATTERY_TX 5
static const uint8_t A0 = PIN_A0;
#define PIN_NFC1 (9)
#define PIN_NFC2 (10)
// I2C
#define WIRE_INTERFACES_COUNT 1
#define PIN_WIRE_SDA (23)
#define PIN_WIRE_SCL (25)
// actually the LORA Radio
#define PIN_POWER_EN (11)
// charger status
#define EXT_CHRG_DETECT (32 + 6)
#define EXT_CHRG_DETECT_VALUE HIGH
// SPI
#define SPI_INTERFACES_COUNT 1
#define PIN_SPI_MISO (8)
#define PIN_SPI_MOSI (7)
#define PIN_SPI_SCK (6)
#define LORA_RESET (32 + 8)
#define LORA_DIO1 (12)
#define LORA_DIO2 (26)
#define LORA_SCK PIN_SPI_SCK
#define LORA_MISO PIN_SPI_MISO
#define LORA_MOSI PIN_SPI_MOSI
#define LORA_CS (27)
#define USE_LR1110
#define LR1110_IRQ_PIN LORA_DIO1
#define LR1110_NRESET_PIN LORA_RESET
#define LR1110_BUSY_PIN LORA_DIO2
#define LR1110_SPI_NSS_PIN LORA_CS
#define LR1110_SPI_SCK_PIN LORA_SCK
#define LR1110_SPI_MOSI_PIN LORA_MOSI
#define LR1110_SPI_MISO_PIN LORA_MISO
#define LR11X0_DIO3_TCXO_VOLTAGE 1.6
#define LR11X0_DIO_AS_RF_SWITCH
// Peripherals on I2C bus. Active Low
#define VEXT_ENABLE (32)
#define VEXT_ON_VALUE LOW
// GPS L76K
#define HAS_GPS 1
#define GPS_L76K
#define GPS_BAUDRATE 9600
#define PIN_GPS_EN (32 + 11)
#define GPS_EN_ACTIVE LOW
#define PIN_GPS_RESET (3)
#define GPS_RESET_MODE HIGH
#define PIN_GPS_STANDBY (28)
#define GPS_STANDBY_ACTIVE HIGH
#define GPS_TX_PIN (32 + 12)
#define GPS_RX_PIN (32 + 14)
#define GPS_THREAD_INTERVAL 50
#define PIN_SERIAL1_RX GPS_RX_PIN
#define PIN_SERIAL1_TX GPS_TX_PIN
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -134,12 +134,14 @@ static const uint8_t A0 = PIN_A0;
#define BATTERY_SENSE_RESOLUTION_BITS 12 #define BATTERY_SENSE_RESOLUTION_BITS 12
#define BATTERY_SENSE_RESOLUTION 4096.0 #define BATTERY_SENSE_RESOLUTION 4096.0
#undef AREF_VOLTAGE #undef AREF_VOLTAGE
#define AREF_VOLTAGE 3.0 #define AREF_VOLTAGE 2.4
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define VBAT_AR_INTERNAL AR_INTERNAL_2_4
#define ADC_MULTIPLIER (1.75F) #define ADC_MULTIPLIER (1.75F)
#define HAS_SOLAR #define HAS_SOLAR
#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3450
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -13,6 +13,7 @@ build_flags =
-DPIN_SERIAL1_RX=PB7 -DPIN_SERIAL1_RX=PB7
-DPIN_SERIAL1_TX=PB6 -DPIN_SERIAL1_TX=PB6
-DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
-DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_I2C=1
-DMESHTASTIC_EXCLUDE_GPS=1 -DMESHTASTIC_EXCLUDE_GPS=1

View File

@@ -12,6 +12,7 @@ build_flags =
-DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SDA=PA11
-DPIN_WIRE_SCL=PA12 -DPIN_WIRE_SCL=PA12
-DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
-DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_I2C=1
-DMESHTASTIC_EXCLUDE_GPS=1 -DMESHTASTIC_EXCLUDE_GPS=1