mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-16 06:47:52 +00:00
Compare commits
36 Commits
crowpanelV
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64116cd0d3 | ||
|
|
c8f0295a9c | ||
|
|
3911d5fe15 | ||
|
|
59bdb9b097 | ||
|
|
b4157bd9bb | ||
|
|
7e4e772113 | ||
|
|
82735ca04e | ||
|
|
e8fbdb4d84 | ||
|
|
a69e439dc1 | ||
|
|
360579926c | ||
|
|
ff8316f895 | ||
|
|
6ee52ca7fa | ||
|
|
233e6acc85 | ||
|
|
5f63f91cbc | ||
|
|
c0afe92a7f | ||
|
|
a6a80b067f | ||
|
|
64e95554bb | ||
|
|
6537eeab03 | ||
|
|
fad315e99d | ||
|
|
2d4f1b6bfe | ||
|
|
5a81403594 | ||
|
|
5d7d1ae7a5 | ||
|
|
940b3e236b | ||
|
|
d1ae131502 | ||
|
|
552df4c88c | ||
|
|
cdbc8f48d4 | ||
|
|
919f214e8d | ||
|
|
89a83d00fa | ||
|
|
5610d4809c | ||
|
|
dae4061b06 | ||
|
|
e99853f660 | ||
|
|
3640e35a8b | ||
|
|
782ffdc5cd | ||
|
|
6f36f39da9 | ||
|
|
ded4f57cb7 | ||
|
|
3a0f3520d1 |
4
.github/workflows/build_firmware.yml
vendored
4
.github/workflows/build_firmware.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
38
boards/CDEBYTE_EoRa-Hub.json
Normal file
38
boards/CDEBYTE_EoRa-Hub.json
Normal 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
53
boards/ThinkNode-M4.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Submodule protobufs updated: 61219de748...4b9f104a18
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
138
src/Power.cpp
138
src/Power.cpp
@@ -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
|
||||||
|
|||||||
@@ -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())) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
41
src/detect/reClockI2C.h
Normal 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
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
60
src/main.cpp
60
src/main.cpp
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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.
|
||||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 fan’s 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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
34
src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h
Normal file
34
src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h
Normal 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
|
||||||
158
src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
Normal file
158
src/modules/Telemetry/Sensor/PMSA003ISensor.cpp
Normal 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
|
||||||
35
src/modules/Telemetry/Sensor/PMSA003ISensor.h
Normal file
35
src/modules/Telemetry/Sensor/PMSA003ISensor.h
Normal 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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
26
src/platform/esp32/MeshtasticOTA.h
Normal file
26
src/platform/esp32/MeshtasticOTA.h
Normal 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
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
28
variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h
Normal file
28
variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h
Normal 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 */
|
||||||
8
variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini
Normal file
8
variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini
Normal 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
|
||||||
19
variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h
Normal file
19
variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h
Normal 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,
|
||||||
|
};
|
||||||
50
variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h
Normal file
50
variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
15
variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini
Normal file
15
variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini
Normal 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
|
||||||
11
variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h
Normal file
11
variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h
Normal 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,
|
||||||
|
};
|
||||||
51
variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp
Normal file
51
variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp
Normal 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);
|
||||||
|
}
|
||||||
142
variants/nrf52840/ELECROW-ThinkNode-M4/variant.h
Normal file
142
variants/nrf52840/ELECROW-ThinkNode-M4/variant.h
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user