Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e03a9579c | ||
|
|
59dd837c0e | ||
|
|
472809b827 | ||
|
|
d7cbac0156 | ||
|
|
bc241481b1 | ||
|
|
ae62affeec | ||
|
|
fa89db17bb | ||
|
|
8e6525c4aa | ||
|
|
7aa004c1eb | ||
|
|
8e72b16693 | ||
|
|
3c68c2eed9 | ||
|
|
a43cd75bd3 | ||
|
|
41cbb45cdc | ||
|
|
434225eb01 | ||
|
|
b603ed0bbf | ||
|
|
245b45fff3 | ||
|
|
9e3a9c370d | ||
|
|
b93239bb0a | ||
|
|
fcfe329091 | ||
|
|
04545c7ba2 | ||
|
|
c4fdf374e1 | ||
|
|
3501f620a3 | ||
|
|
c51c01607d | ||
|
|
d8d92b7b71 |
Vendored
+2
-1
@@ -10,5 +10,6 @@
|
||||
},
|
||||
"[powershell]": {
|
||||
"editor.defaultFormatter": "ms-vscode.powershell"
|
||||
}
|
||||
},
|
||||
"idf.currentSetup": "C:/Espressif/frameworks/esp-idf-v5.3.1/"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/NebraHat
|
||||
# Use for 1 watt hat
|
||||
Meta:
|
||||
name: NebraHat 1W
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Nebra SX1262 Pi Hat - 1W
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# CS: 8 # Newer version of MeshtasticD do not need this? If issues uncomment this line
|
||||
IRQ: 22
|
||||
Busy: 4
|
||||
Reset: 18
|
||||
RXen: 25
|
||||
I2C:
|
||||
I2CDevice: /dev/i2c-1
|
||||
@@ -0,0 +1,20 @@
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/NebraHat
|
||||
# Use for 2 watt hat
|
||||
Meta:
|
||||
name: NebraHat 2W
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Nebra SX1262 Pi Hat - 2W
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 8
|
||||
# CS: 8 # Newer version of MeshtasticD do not need this? If issues uncomment this line
|
||||
IRQ: 22
|
||||
Busy: 4
|
||||
Reset: 18
|
||||
RXen: 25
|
||||
I2C:
|
||||
I2CDevice: /dev/i2c-1
|
||||
@@ -18,4 +18,4 @@ Lora:
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: 8
|
||||
# CS: 8
|
||||
|
||||
@@ -18,4 +18,4 @@ Lora:
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
# CS: 7
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/ZebraHAT
|
||||
# Use for 1 watt hat
|
||||
Meta:
|
||||
name: ZebraHat 1W
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Zebra SX1262 Pi Hat - 1W
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 18
|
||||
CS: 24
|
||||
IRQ: 22
|
||||
Busy: 27
|
||||
Reset: 17
|
||||
I2C:
|
||||
I2CDevice: /dev/i2c-1
|
||||
@@ -0,0 +1,20 @@
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/ZebraHAT
|
||||
# Use for 2 watt hat
|
||||
Meta:
|
||||
name: ZebraHat 2W
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Zebra SX1262 Pi Hat - 2W
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 8
|
||||
CS: 24
|
||||
IRQ: 22
|
||||
Busy: 27
|
||||
Reset: 17
|
||||
RXen: 25
|
||||
I2C:
|
||||
I2CDevice: /dev/i2c-1
|
||||
@@ -29,6 +29,8 @@ Lora:
|
||||
- pin: 50 # GPIO1_C2 (physical 16)
|
||||
gpiochip: 1
|
||||
line: 18
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_A7 (SPI1_CSN1, physical 26)
|
||||
# pin: 7
|
||||
|
||||
@@ -29,6 +29,8 @@ Lora:
|
||||
- pin: 50 # GPIO1_C2 (physical 16)
|
||||
gpiochip: 1
|
||||
line: 18
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_A7 (SPI1_CSN1, physical 26)
|
||||
# pin: 7
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Station G3 + BQ35LORA900V1M Primary Slot
|
||||
# Board Doc: https://wiki.bqvoy.com/en/devkits/station-g3
|
||||
Meta:
|
||||
name: B&Q Station G3 + BQ35LORA900V1M Primary Slot
|
||||
support: official
|
||||
compatible:
|
||||
- luckfox-lyra-zero-w # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO0_A5 (physical 15)
|
||||
pin: 5
|
||||
gpiochip: 0
|
||||
line: 5
|
||||
Reset: # GPIO1_D1 (physical 36)
|
||||
pin: 57
|
||||
gpiochip: 1
|
||||
line: 25
|
||||
Busy: # GPIO0_B4 (physical 18)
|
||||
pin: 12
|
||||
gpiochip: 0
|
||||
line: 12
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: # GPIO0_B2 (physical 24)
|
||||
# pin: 10
|
||||
# gpiochip: 0
|
||||
# line: 10
|
||||
@@ -29,6 +29,8 @@ Lora:
|
||||
- pin: 13 # GPIO0_B5
|
||||
gpiochip: 0
|
||||
line: 13
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_B1
|
||||
# pin: 9
|
||||
|
||||
@@ -29,6 +29,8 @@ Lora:
|
||||
- pin: 13 # GPIO0_B5
|
||||
gpiochip: 0
|
||||
line: 13
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_B1
|
||||
# pin: 9
|
||||
|
||||
@@ -31,6 +31,8 @@ Lora:
|
||||
- pin: 103 # GPIO3_A7 (physical 16)
|
||||
gpiochip: 3
|
||||
line: 7
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_B7 (SPI0_CSN1, physical 26)
|
||||
# pin: 15
|
||||
|
||||
@@ -31,6 +31,8 @@ Lora:
|
||||
- pin: 103 # GPIO3_A7 (physical 16)
|
||||
gpiochip: 3
|
||||
line: 7
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_B7 (SPI0_CSN1, physical 26)
|
||||
# pin: 15
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Station G3 + BQ35LORA900V1M Primary Slot
|
||||
# Board Doc: https://wiki.bqvoy.com/en/devkits/station-g3
|
||||
Meta:
|
||||
name: B&Q Station G3 + BQ35LORA900V1M Primary Slot
|
||||
support: official
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: 22
|
||||
Reset: 16
|
||||
Busy: 24
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
#CS: 8
|
||||
@@ -1,3 +1,5 @@
|
||||
# This config works with all revisions of the Meshtoad USB radio.
|
||||
|
||||
Meta:
|
||||
name: meshtoad-e22
|
||||
support: official
|
||||
|
||||
+25
-29
@@ -219,41 +219,37 @@ with open(jsonLoc) as f:
|
||||
jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE)
|
||||
userPrefs = json.loads(jsonStr)
|
||||
|
||||
pref_flags = []
|
||||
# Pre-process the userPrefs
|
||||
for pref in userPrefs:
|
||||
if userPrefs[pref].startswith("{"):
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
elif userPrefs[pref].lstrip("-").replace(".", "").isdigit():
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
elif userPrefs[pref] == "true" or userPrefs[pref] == "false":
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
elif userPrefs[pref].startswith("meshtastic_"):
|
||||
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
|
||||
# If the value is a string, we need to wrap it in quotes
|
||||
else:
|
||||
pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "")
|
||||
|
||||
# General options that are passed to the C and C++ compilers
|
||||
# Calculate unix epoch for current day (midnight)
|
||||
current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
build_epoch = int(current_date.timestamp())
|
||||
|
||||
flags = [
|
||||
"-DAPP_VERSION=" + verObj["long"],
|
||||
"-DAPP_VERSION_SHORT=" + verObj["short"],
|
||||
"-DAPP_ENV=" + env.get("PIOENV"),
|
||||
"-DAPP_REPO=" + repo_owner,
|
||||
"-DBUILD_EPOCH=" + str(build_epoch),
|
||||
] + pref_flags
|
||||
cpp_defines = [
|
||||
("APP_VERSION", verObj["long"]),
|
||||
("APP_VERSION_SHORT", verObj["short"]),
|
||||
("APP_ENV", env.get("PIOENV")),
|
||||
("APP_REPO", repo_owner),
|
||||
("BUILD_EPOCH", str(build_epoch)),
|
||||
]
|
||||
|
||||
print("Using flags:")
|
||||
for flag in flags:
|
||||
print(flag)
|
||||
# Process userPrefs into CPPDEFINES
|
||||
for pref in userPrefs:
|
||||
val = userPrefs[pref]
|
||||
if val.startswith("{"):
|
||||
cpp_defines.append((pref, val))
|
||||
elif val.lstrip("-").replace(".", "").isdigit():
|
||||
cpp_defines.append((pref, val))
|
||||
elif val == "true" or val == "false":
|
||||
cpp_defines.append((pref, val))
|
||||
elif val.startswith("meshtastic_"):
|
||||
cpp_defines.append((pref, val))
|
||||
else:
|
||||
cpp_defines.append((pref, env.StringifyMacro(val)))
|
||||
|
||||
projenv.Append(
|
||||
CCFLAGS=flags,
|
||||
)
|
||||
print("Using CPPDEFINES:")
|
||||
for d in cpp_defines:
|
||||
print(f" -D{d[0]}={d[1]}")
|
||||
|
||||
projenv.Append(CPPDEFINES=cpp_defines)
|
||||
|
||||
for lb in env.GetLibBuilders():
|
||||
if lb.name == "meshtastic-device-ui":
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_T_IMPULSE_PLUS -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [["0x239A", "0x8029"]],
|
||||
"usb_product": "T-Impulse-Plus-nRF52840",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "t-impulse-plus",
|
||||
"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"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Lilygo T-Impulse-Plus-nRF52840",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"require_upload_port": true,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink",
|
||||
"cmsis-dap",
|
||||
"blackmagic"
|
||||
]
|
||||
},
|
||||
"url": "https://www.lilygo.cc/",
|
||||
"vendor": "Lilygo"
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_qspi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "t-watch-ultra"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "LilyGo T-Watch Ultra",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.lilygo.cc/en-pl/products/t-watch-ultra",
|
||||
"vendor": "LilyGo"
|
||||
}
|
||||
Vendored
+1
-1
@@ -33,5 +33,5 @@ if [[ -n $GPG_KEY_ID ]]; then
|
||||
debuild -S -nc -k"$GPG_KEY_ID"
|
||||
else
|
||||
# Build the source deb without signing (forks)
|
||||
debuild -S -nc
|
||||
debuild -S -nc -us -uc
|
||||
fi
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@ extra_configs =
|
||||
description = Meshtastic
|
||||
|
||||
[env]
|
||||
test_build_src = true
|
||||
; test_build_src = true ; disabled for faster incremental builds
|
||||
extra_scripts =
|
||||
pre:bin/platformio-pre.py
|
||||
bin/platformio-custom.py
|
||||
@@ -62,6 +62,7 @@ build_flags = -Wno-missing-field-initializers
|
||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||
#-D OLED_PL=1
|
||||
#-D OLED_ZH=1 ; uncomment for compile-time Chinese font (runtime switching works without this on TFT)
|
||||
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
||||
#-D DEBUG_LOOP_TIMING=1 ; uncomment to add main loop timing logs
|
||||
|
||||
|
||||
+1
-1
Submodule protobufs updated: f16a13e433...03314e6395
+118
-31
@@ -88,6 +88,9 @@ bool renameFile(const char *pathFrom, const char *pathTo)
|
||||
#endif
|
||||
}
|
||||
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
@@ -119,6 +122,93 @@ bool fsFormat()
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef FSCom
|
||||
namespace
|
||||
{
|
||||
bool pathEndsWithDot(const char *path)
|
||||
{
|
||||
if (!path)
|
||||
return false;
|
||||
|
||||
size_t length = strlen(path);
|
||||
return length > 0 && path[length - 1] == '.';
|
||||
}
|
||||
|
||||
bool copyFilePath(char *dest, size_t destSize, const char *path, bool *wasLimited)
|
||||
{
|
||||
if (!path || destSize == 0) {
|
||||
if (wasLimited)
|
||||
*wasLimited = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlcpy(dest, path, destSize) >= destSize) {
|
||||
if (wasLimited)
|
||||
*wasLimited = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void collectFiles(const char *dirname, uint8_t levels, size_t maxCount, std::vector<meshtastic_FileInfo> &filenames,
|
||||
bool *wasLimited)
|
||||
{
|
||||
if (!dirname)
|
||||
return;
|
||||
|
||||
File root = FSCom.open(dirname, FILE_O_READ);
|
||||
if (!root)
|
||||
return;
|
||||
if (!root.isDirectory()) {
|
||||
root.close();
|
||||
return;
|
||||
}
|
||||
|
||||
File file = root.openNextFile();
|
||||
// file.name()[0] check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395)
|
||||
while (file && file.name()[0]) {
|
||||
if (filenames.size() >= maxCount) {
|
||||
if (wasLimited)
|
||||
*wasLimited = true;
|
||||
file.close();
|
||||
break;
|
||||
}
|
||||
const char *fileName = file.name();
|
||||
if (file.isDirectory() && !pathEndsWithDot(fileName)) {
|
||||
char pathBuffer[sizeof(((meshtastic_FileInfo *)nullptr)->file_name)] = {};
|
||||
#ifdef ARCH_ESP32
|
||||
const char *subDirPath = file.path();
|
||||
#else
|
||||
const char *subDirPath = fileName;
|
||||
#endif
|
||||
bool hasSubDirPath = copyFilePath(pathBuffer, sizeof(pathBuffer), subDirPath, wasLimited);
|
||||
file.close();
|
||||
|
||||
if (levels && hasSubDirPath) {
|
||||
collectFiles(pathBuffer, levels - 1, maxCount, filenames, wasLimited);
|
||||
} else if (wasLimited) {
|
||||
*wasLimited = true;
|
||||
}
|
||||
} else {
|
||||
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
||||
#ifdef ARCH_ESP32
|
||||
bool hasFilePath = copyFilePath(fileInfo.file_name, sizeof(fileInfo.file_name), file.path(), wasLimited);
|
||||
#else
|
||||
bool hasFilePath = copyFilePath(fileInfo.file_name, sizeof(fileInfo.file_name), file.name(), wasLimited);
|
||||
#endif
|
||||
if (hasFilePath && !pathEndsWithDot(fileInfo.file_name)) {
|
||||
filenames.push_back(fileInfo);
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
file = root.openNextFile();
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Get the list of files in a directory.
|
||||
*
|
||||
@@ -127,43 +217,40 @@ bool fsFormat()
|
||||
*
|
||||
* @param dirname The name of the directory.
|
||||
* @param levels The number of levels of subdirectories to list.
|
||||
* @return A vector of strings containing the full path of each file in the directory.
|
||||
* @param maxCount The maximum number of files to collect before truncating the walk.
|
||||
* @param wasLimited Optional out-param, set to true if the listing was truncated (by maxCount or low memory).
|
||||
* @return A vector of meshtastic_FileInfo for each file in the directory.
|
||||
*/
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, size_t maxCount, bool *wasLimited)
|
||||
{
|
||||
std::vector<meshtastic_FileInfo> filenames = {};
|
||||
if (wasLimited)
|
||||
*wasLimited = false;
|
||||
#ifdef FSCom
|
||||
File root = FSCom.open(dirname, FILE_O_READ);
|
||||
if (!root)
|
||||
return filenames;
|
||||
if (!root.isDirectory())
|
||||
return filenames;
|
||||
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
#ifdef ARCH_ESP32
|
||||
const char *filepath = file.path();
|
||||
#else
|
||||
const char *filepath = file.name();
|
||||
#endif
|
||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||
if (levels) {
|
||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(filepath, levels - 1);
|
||||
filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end());
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
||||
strncpy(fileInfo.file_name, filepath, sizeof(fileInfo.file_name) - 1);
|
||||
fileInfo.file_name[sizeof(fileInfo.file_name) - 1] = '\0';
|
||||
if (!String(fileInfo.file_name).endsWith(".")) {
|
||||
filenames.push_back(fileInfo);
|
||||
}
|
||||
file.close();
|
||||
#if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
|
||||
size_t reservedCount = maxCount;
|
||||
while (reservedCount > 0) {
|
||||
try {
|
||||
filenames.reserve(reservedCount);
|
||||
break;
|
||||
} catch (const std::bad_alloc &) {
|
||||
reservedCount /= 2;
|
||||
} catch (const std::length_error &) {
|
||||
reservedCount /= 2;
|
||||
}
|
||||
file = root.openNextFile();
|
||||
}
|
||||
root.close();
|
||||
if (reservedCount == 0) {
|
||||
if (wasLimited)
|
||||
*wasLimited = true;
|
||||
return filenames;
|
||||
}
|
||||
if (reservedCount < maxCount) {
|
||||
if (wasLimited)
|
||||
*wasLimited = true;
|
||||
maxCount = reservedCount;
|
||||
}
|
||||
#endif
|
||||
collectFiles(dirname, levels, maxCount, filenames, wasLimited);
|
||||
#endif
|
||||
return filenames;
|
||||
}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ void fsListFiles();
|
||||
bool copyFile(const char *from, const char *to);
|
||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||
bool fsFormat();
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr);
|
||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||
void rmDir(const char *dirname);
|
||||
void setupSDCard();
|
||||
+17
-46
@@ -24,6 +24,7 @@
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "power/PowerHAL.h"
|
||||
#include "power/SGM41562.h"
|
||||
#include "sleep.h"
|
||||
#ifdef ARCH_ESP32
|
||||
// #include <driver/adc.h>
|
||||
@@ -545,6 +546,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
// lastly provide a fallback to indicate external power when fully charged.
|
||||
virtual bool isVbusIn() override
|
||||
{
|
||||
#ifdef HAS_SGM41562
|
||||
if (sgm41562 && sgm41562->refresh())
|
||||
return sgm41562->isInputPowerGood();
|
||||
#endif
|
||||
#ifdef EXT_PWR_DETECT
|
||||
return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE;
|
||||
|
||||
@@ -561,6 +566,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
/// we can't be smart enough to say 'full'?
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
#ifdef HAS_SGM41562
|
||||
if (sgm41562 && sgm41562->refresh())
|
||||
return sgm41562->isCharging();
|
||||
#endif
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU)
|
||||
if (hasRAK()) {
|
||||
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
|
||||
@@ -767,6 +776,12 @@ bool Power::analogInit()
|
||||
*/
|
||||
bool Power::setup()
|
||||
{
|
||||
#ifdef HAS_SGM41562
|
||||
// Initialize the charger early so AnalogBatteryLevel can read charging
|
||||
// state from it. The charger does not provide battery voltage / percent —
|
||||
// those still come from the platform ADC via analogInit() below.
|
||||
initSGM41562(SGM41562_WIRE);
|
||||
#endif
|
||||
bool found = false;
|
||||
if (axpChipInit()) {
|
||||
found = true;
|
||||
@@ -1063,10 +1078,8 @@ int32_t Power::runOnce()
|
||||
// cancel action also turns the screen on and off.
|
||||
if (PMU->isPekeyShortPressIrq()) {
|
||||
LOG_INFO("Input: Corona Button Click");
|
||||
if (inputBroker) {
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
#endif
|
||||
/*
|
||||
@@ -1369,48 +1382,6 @@ bool Power::axpChipInit()
|
||||
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
|
||||
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
|
||||
PMU->disablePowerOutput(XPOWERS_VBACKUP);
|
||||
} else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA) {
|
||||
PMU->clearIrqStatus();
|
||||
|
||||
// Turn off the PMU charging indicator light, no physical connection
|
||||
PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); // NO LED
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); // SD Card
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO1);
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); // Display
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); // LoRa
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO3);
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 1800); // Sensor
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); // GPS
|
||||
PMU->enablePowerOutput(XPOWERS_BLDO1);
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); // Speaker
|
||||
PMU->enablePowerOutput(XPOWERS_BLDO2);
|
||||
|
||||
PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); // RTC Button battery
|
||||
PMU->enablePowerOutput(XPOWERS_VBACKUP);
|
||||
|
||||
// PMU->enablePowerOutput(XPOWERS_DLDO1); // NFC
|
||||
|
||||
// UNUSED POWER CHANNEL
|
||||
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
||||
PMU->disablePowerOutput(XPOWERS_DCDC3);
|
||||
PMU->disablePowerOutput(XPOWERS_DCDC4);
|
||||
PMU->disablePowerOutput(XPOWERS_DCDC5);
|
||||
PMU->disablePowerOutput(XPOWERS_CPULDO);
|
||||
|
||||
// Enable Measure
|
||||
PMU->enableBattDetection();
|
||||
PMU->enableVbusVoltageMeasure();
|
||||
PMU->enableBattVoltageMeasure();
|
||||
PMU->enableSystemVoltageMeasure();
|
||||
PMU->enableTemperatureMeasure();
|
||||
}
|
||||
|
||||
// disable all axp chip interrupt
|
||||
|
||||
@@ -37,9 +37,9 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948,
|
||||
QMA6100P, BHI260AP, BMM150, BMI270, ICM42607P, ISM330DHCX};
|
||||
return firstOfOrNONE(13, types);
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
|
||||
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P, ISM330DHCX};
|
||||
return firstOfOrNONE(12, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
|
||||
|
||||
+2
-2
@@ -1279,8 +1279,8 @@ void GPS::setPowerPMU(bool on)
|
||||
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) {
|
||||
// t-beam-s3-core GNSS power channel
|
||||
on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4);
|
||||
} else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
|
||||
// t-watch-ultra / t-watch-s3-plus GNSS power channel
|
||||
} else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
|
||||
// t-watch-s3-plus GNSS power channel
|
||||
on ? PMU->enablePowerOutput(XPOWERS_BLDO1) : PMU->disablePowerOutput(XPOWERS_BLDO1);
|
||||
}
|
||||
} else if (model == XPOWERS_AXP192) {
|
||||
|
||||
+87
-12
@@ -31,13 +31,63 @@ static uint32_t
|
||||
timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds to that time
|
||||
static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only updated once on initial lock
|
||||
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
// Test seam: unit tests can inject a fake system clock (e.g. the uptime seconds that
|
||||
// gettimeofday() returns on boards without a real RTC, like RP2040) and force readFromRTC()
|
||||
// down the no-hardware-RTC fallback even when a hardware-RTC branch is compiled in.
|
||||
static bool hasMockSystemTime = false;
|
||||
static bool forceSystemTimeFallback = false;
|
||||
static struct timeval mockSystemTime = {};
|
||||
#endif
|
||||
|
||||
// Reads the platform system clock (or the injected mock during unit tests). Used only by the
|
||||
// no-hardware-RTC fallback below, so it may be unused on builds with a hardware RTC.
|
||||
[[maybe_unused]] static bool readSystemTime(struct timeval *tv)
|
||||
{
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
if (hasMockSystemTime) {
|
||||
*tv = mockSystemTime;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return gettimeofday(tv, NULL) == 0;
|
||||
}
|
||||
|
||||
// Seeds the clock from the system time on boards without a hardware RTC. gettimeofday() can
|
||||
// return uptime rather than wall-clock time there (e.g. RP2040), so only adopt it when we have
|
||||
// nothing better yet -- never clobber a higher-quality GPS/NTP/phone source (issue #9828).
|
||||
[[maybe_unused]] static RTCSetResult readFromSystemTimeFallback()
|
||||
{
|
||||
struct timeval tv;
|
||||
if (readSystemTime(&tv)) {
|
||||
uint32_t now = millis();
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
LOG_DEBUG("Seed time from system clock: %lu", (unsigned long)printableEpoch);
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
} else {
|
||||
LOG_DEBUG("Ignore system clock fallback (%lu); current RTC quality is %s", (unsigned long)printableEpoch,
|
||||
RtcName(currentQuality));
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
return RTCSetResultNotSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current date and time from the RTC module and updates the system time.
|
||||
* @return True if the RTC was successfully read and the system time was updated, false otherwise.
|
||||
* Reads date/time from the RTC module (or system-time fallback) and seeds internal timekeeping.
|
||||
* @return RTCSetResultSuccess if a time source was read successfully (even if an existing higher-quality time is retained).
|
||||
*/
|
||||
RTCSetResult readFromRTC()
|
||||
{
|
||||
struct timeval tv; /* btw settimeofday() is helpful here too*/
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
if (forceSystemTimeFallback) {
|
||||
return readFromSystemTimeFallback();
|
||||
}
|
||||
#endif
|
||||
|
||||
[[maybe_unused]] struct timeval tv; /* btw settimeofday() is helpful here too*/
|
||||
#ifdef RV3028_RTC
|
||||
if (rtc_found.address == RV3028_RTC) {
|
||||
uint32_t now = millis();
|
||||
@@ -162,14 +212,7 @@ RTCSetResult readFromRTC()
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (!gettimeofday(&tv, NULL)) {
|
||||
uint32_t now = millis();
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
LOG_DEBUG("Read RTC time as %ld", printableEpoch);
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
return readFromSystemTimeFallback();
|
||||
#endif
|
||||
return RTCSetResultNotSet;
|
||||
}
|
||||
@@ -292,7 +335,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
LOG_WARN("Failed to set time for RX8130CE");
|
||||
}
|
||||
}
|
||||
#elif defined(ARCH_ESP32)
|
||||
#elif defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
settimeofday(tv, NULL);
|
||||
#endif
|
||||
|
||||
@@ -423,6 +466,38 @@ void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot)
|
||||
lastSetFromPhoneNtpOrGps = 0;
|
||||
lastTimeValidationWarning = 0;
|
||||
}
|
||||
|
||||
void clearRTCSystemTimeForTests()
|
||||
{
|
||||
hasMockSystemTime = false;
|
||||
mockSystemTime = {};
|
||||
}
|
||||
|
||||
void setRTCSystemTimeForTests(const struct timeval *tv)
|
||||
{
|
||||
if (tv == NULL) {
|
||||
clearRTCSystemTimeForTests();
|
||||
return;
|
||||
}
|
||||
mockSystemTime = *tv;
|
||||
hasMockSystemTime = true;
|
||||
}
|
||||
|
||||
void setReadFromRTCUseSystemTimeForTests(bool enabled)
|
||||
{
|
||||
forceSystemTimeFallback = enabled;
|
||||
}
|
||||
|
||||
void resetRTCStateForTests()
|
||||
{
|
||||
currentQuality = RTCQualityNone;
|
||||
timeStartMsec = 0;
|
||||
zeroOffsetSecs = 0;
|
||||
lastSetFromPhoneNtpOrGps = 0;
|
||||
lastTimeValidationWarning = 0;
|
||||
setReadFromRTCUseSystemTimeForTests(false);
|
||||
clearRTCSystemTimeForTests();
|
||||
}
|
||||
#endif
|
||||
|
||||
time_t gm_mktime(const struct tm *tm)
|
||||
|
||||
@@ -56,6 +56,10 @@ RTCSetResult readFromRTC();
|
||||
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot);
|
||||
void resetRTCStateForTests();
|
||||
void setRTCSystemTimeForTests(const struct timeval *tv);
|
||||
void clearRTCSystemTimeForTests();
|
||||
void setReadFromRTCUseSystemTimeForTests(bool enabled);
|
||||
#endif
|
||||
|
||||
time_t gm_mktime(const struct tm *tm);
|
||||
|
||||
+292
-13
@@ -39,6 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "draw/NotificationRenderer.h"
|
||||
#include "draw/UIRenderer.h"
|
||||
#include "graphics/fonts/OLEDDisplayFontsZH.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "security/LockdownDisplay.h"
|
||||
@@ -60,6 +61,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "graphics/l10n/Strings.h"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
@@ -140,10 +142,11 @@ static void drawLockdownLockScreenIntoBuffer(OLEDDisplay *display)
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_LARGE);
|
||||
display->drawString(w / 2, h / 2 - FONT_HEIGHT_LARGE, "LOCKED");
|
||||
display->drawString(w / 2, h / 2 - FONT_HEIGHT_LARGE, meshtastic::l10n::getString(meshtastic::l10n::StringKey::LOCKED));
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
char status[32] = "Connect to unlock";
|
||||
char status[32];
|
||||
strncpy(status, meshtastic::l10n::getString(meshtastic::l10n::StringKey::ConnectToUnlock), 31);
|
||||
if (powerStatus && powerStatus->getHasBattery()) {
|
||||
int pct = powerStatus->getBatteryChargePercent();
|
||||
snprintf(status, sizeof(status), "Battery %d%%", pct);
|
||||
@@ -512,8 +515,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
LOG_INFO("SSD1306 init success");
|
||||
}
|
||||
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(CO5300_CS) || \
|
||||
defined(HACKADAY_COMMUNICATOR)
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR)
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY)
|
||||
@@ -587,12 +589,12 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
if (on) {
|
||||
LOG_INFO("Turn on screen");
|
||||
powerMon->setState(meshtastic_PowerMon_State_Screen_On);
|
||||
#if defined(T_WATCH_S3) || defined(T_WATCH_ULTRA)
|
||||
#ifdef T_WATCH_S3
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||||
#endif
|
||||
|
||||
// some screens seem to need a kick in the pants to turn back on
|
||||
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV) || defined(TFT_RESET_AFTER_SLEEP)
|
||||
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV)
|
||||
dispdev->init();
|
||||
dispdev->setBrightness(brightness);
|
||||
dispdev->flipScreenVertically();
|
||||
@@ -717,7 +719,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(T_WATCH_S3) // on T_WATCH_ULTRA, powering down this pin seems to goober the i2c bus.
|
||||
#ifdef T_WATCH_S3
|
||||
PMU->disablePowerOutput(XPOWERS_ALDO2);
|
||||
#endif
|
||||
enabled = false;
|
||||
@@ -732,7 +734,10 @@ void Screen::setup()
|
||||
// Enable display rendering
|
||||
useDisplay = true;
|
||||
|
||||
// Load saved brightness from UI config
|
||||
// Set the runtime display language from persisted config
|
||||
meshtastic::l10n::currentDisplayLanguage = uiconfig.language;
|
||||
|
||||
// Load brightness from UI config
|
||||
// For OLED displays (SSD1306), default brightness is 255 if not set
|
||||
if (uiconfig.screen_brightness == 0) {
|
||||
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
@@ -820,6 +825,9 @@ void Screen::setup()
|
||||
// Enable UTF-8 to display mapping
|
||||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||||
|
||||
// Load CJK (Chinese) font if language requires it
|
||||
loadCJKFontIfNeeded();
|
||||
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
logo_timeout *= 2; // Give more time for branded boot logos
|
||||
#endif
|
||||
@@ -829,7 +837,7 @@ void Screen::setup()
|
||||
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
||||
#ifdef ARCH_ESP32
|
||||
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1)
|
||||
graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming...");
|
||||
graphics::UIRenderer::drawFrameText(display, state, x, y, meshtastic::l10n::getString(meshtastic::l10n::StringKey::Resuming));
|
||||
else
|
||||
#endif
|
||||
{
|
||||
@@ -849,8 +857,7 @@ void Screen::setup()
|
||||
#else
|
||||
if (!config.display.flip_screen) {
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(CO5300_CS) || \
|
||||
defined(HACKADAY_COMMUNICATOR)
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR)
|
||||
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
|
||||
#elif defined(USE_ST7789)
|
||||
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
|
||||
@@ -888,7 +895,7 @@ void Screen::setup()
|
||||
touchScreenImpl1->init();
|
||||
}
|
||||
}
|
||||
#elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE && !VARIANT_TOUCHSCREEN
|
||||
#elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE
|
||||
touchScreenImpl1 =
|
||||
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
||||
touchScreenImpl1->init();
|
||||
@@ -987,6 +994,13 @@ int32_t Screen::runOnce()
|
||||
return RUN_SAME;
|
||||
}
|
||||
|
||||
// Detect language changes from remote config (phone app)
|
||||
if (uiconfig.language != meshtastic::l10n::currentDisplayLanguage) {
|
||||
meshtastic::l10n::currentDisplayLanguage = uiconfig.language;
|
||||
loadCJKFontIfNeeded();
|
||||
setFrames(FOCUS_PRESERVE);
|
||||
}
|
||||
|
||||
if (displayHeight == 0) {
|
||||
displayHeight = dispdev->getHeight();
|
||||
}
|
||||
@@ -1054,7 +1068,7 @@ int32_t Screen::runOnce()
|
||||
}
|
||||
#endif
|
||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) {
|
||||
showSimpleBanner("Rebooting...", 0);
|
||||
showSimpleBanner(meshtastic::l10n::getString(meshtastic::l10n::StringKey::Rebooting), 0);
|
||||
}
|
||||
|
||||
// Process incoming commands.
|
||||
@@ -2212,6 +2226,271 @@ bool Screen::isOverlayBannerShowing()
|
||||
return NotificationRenderer::isOverlayBannerShowing();
|
||||
}
|
||||
|
||||
char Screen::customFontTableLookup(const uint8_t ch)
|
||||
{
|
||||
static uint8_t LASTCHAR;
|
||||
static bool SKIPREST;
|
||||
static int8_t cjkRemaining = 0;
|
||||
|
||||
if (cjkRemaining > 0) {
|
||||
cjkRemaining--;
|
||||
LASTCHAR = ch;
|
||||
return (uint8_t)0;
|
||||
}
|
||||
|
||||
if (ch < 128) {
|
||||
LASTCHAR = 0;
|
||||
SKIPREST = false;
|
||||
cjkRemaining = 0;
|
||||
return ch;
|
||||
}
|
||||
|
||||
uint8_t last = LASTCHAR;
|
||||
LASTCHAR = ch;
|
||||
|
||||
switch (last) {
|
||||
case 0xC2: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)ch;
|
||||
}
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
}
|
||||
|
||||
if (ch == 0xC2 || ch == 0xC3)
|
||||
return (uint8_t)0;
|
||||
|
||||
#if defined(OLED_PL)
|
||||
switch (last) {
|
||||
case 0xC3: {
|
||||
if (ch == 147)
|
||||
return (uint8_t)(ch);
|
||||
else if (ch == 179)
|
||||
return (uint8_t)(148);
|
||||
else
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
break;
|
||||
}
|
||||
case 0xC4: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch);
|
||||
}
|
||||
case 0xC5: {
|
||||
SKIPREST = false;
|
||||
if (ch == 132)
|
||||
return (uint8_t)(136);
|
||||
else if (ch == 186)
|
||||
return (uint8_t)(137);
|
||||
else
|
||||
return (uint8_t)(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5)
|
||||
return (uint8_t)0;
|
||||
#endif
|
||||
|
||||
#if defined(OLED_UA) || defined(OLED_RU)
|
||||
switch (last) {
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
case 0xD0: {
|
||||
SKIPREST = false;
|
||||
if (ch == 132)
|
||||
return (uint8_t)(170);
|
||||
if (ch == 134)
|
||||
return (uint8_t)(178);
|
||||
if (ch == 135)
|
||||
return (uint8_t)(175);
|
||||
if (ch == 129)
|
||||
return (uint8_t)(168);
|
||||
if (ch > 143 && ch < 192)
|
||||
return (uint8_t)(ch + 48);
|
||||
break;
|
||||
}
|
||||
case 0xD1: {
|
||||
SKIPREST = false;
|
||||
if (ch == 148)
|
||||
return (uint8_t)(186);
|
||||
if (ch == 150)
|
||||
return (uint8_t)(179);
|
||||
if (ch == 151)
|
||||
return (uint8_t)(191);
|
||||
if (ch == 145)
|
||||
return (uint8_t)(184);
|
||||
if (ch > 127 && ch < 144)
|
||||
return (uint8_t)(ch + 112);
|
||||
break;
|
||||
}
|
||||
case 0xD2: {
|
||||
SKIPREST = false;
|
||||
if (ch == 144)
|
||||
return (uint8_t)(165);
|
||||
if (ch == 145)
|
||||
return (uint8_t)(180);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0x82 || ch == 0xD0 || ch == 0xD1)
|
||||
return (uint8_t)0;
|
||||
#endif
|
||||
|
||||
#if defined(OLED_CS)
|
||||
switch (last) {
|
||||
case 0xC2: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)ch;
|
||||
}
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
case 0xC4: {
|
||||
SKIPREST = false;
|
||||
if (ch == 140)
|
||||
return (uint8_t)(129);
|
||||
if (ch == 141)
|
||||
return (uint8_t)(138);
|
||||
if (ch == 142)
|
||||
return (uint8_t)(130);
|
||||
if (ch == 143)
|
||||
return (uint8_t)(139);
|
||||
if (ch == 154)
|
||||
return (uint8_t)(131);
|
||||
if (ch == 155)
|
||||
return (uint8_t)(140);
|
||||
if (ch == 185)
|
||||
return (uint8_t)(147);
|
||||
if (ch == 186)
|
||||
return (uint8_t)(148);
|
||||
if (ch == 189)
|
||||
return (uint8_t)(149);
|
||||
if (ch == 190)
|
||||
return (uint8_t)(150);
|
||||
break;
|
||||
}
|
||||
case 0xC5: {
|
||||
SKIPREST = false;
|
||||
if (ch == 135)
|
||||
return (uint8_t)(132);
|
||||
if (ch == 136)
|
||||
return (uint8_t)(141);
|
||||
if (ch == 152)
|
||||
return (uint8_t)(133);
|
||||
if (ch == 153)
|
||||
return (uint8_t)(142);
|
||||
if (ch == 160)
|
||||
return (uint8_t)(134);
|
||||
if (ch == 161)
|
||||
return (uint8_t)(143);
|
||||
if (ch == 164)
|
||||
return (uint8_t)(135);
|
||||
if (ch == 165)
|
||||
return (uint8_t)(144);
|
||||
if (ch == 174)
|
||||
return (uint8_t)(136);
|
||||
if (ch == 175)
|
||||
return (uint8_t)(145);
|
||||
if (ch == 189)
|
||||
return (uint8_t)(137);
|
||||
if (ch == 190)
|
||||
return (uint8_t)(146);
|
||||
if (ch == 148)
|
||||
return (uint8_t)(151);
|
||||
if (ch == 149)
|
||||
return (uint8_t)(152);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5)
|
||||
return (uint8_t)0;
|
||||
#endif
|
||||
|
||||
#if defined(OLED_GR)
|
||||
switch (last) {
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
case 0xCE: {
|
||||
SKIPREST = false;
|
||||
if (ch >= 145 && ch <= 161)
|
||||
return (uint8_t)(ch + 48);
|
||||
else if (ch >= 163 && ch <= 169)
|
||||
return (uint8_t)(ch + 48);
|
||||
else if (ch >= 177 && ch <= 193)
|
||||
return (uint8_t)(ch + 48);
|
||||
break;
|
||||
}
|
||||
case 0xCF: {
|
||||
SKIPREST = false;
|
||||
if (ch >= 130 && ch <= 137)
|
||||
return (uint8_t)(ch + 112);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xCE || ch == 0xCF)
|
||||
return (uint8_t)0;
|
||||
#endif
|
||||
|
||||
if (ch >= 0xE2 && ch <= 0xEF) {
|
||||
cjkRemaining = 2;
|
||||
return (uint8_t)0;
|
||||
}
|
||||
|
||||
if (SKIPREST)
|
||||
return (uint8_t)0;
|
||||
SKIPREST = true;
|
||||
|
||||
return (uint8_t)191;
|
||||
}
|
||||
|
||||
void Screen::drawTextL10n(int16_t x, int16_t y, const char *text, OLEDDISPLAY_TEXT_ALIGNMENT alignment)
|
||||
{
|
||||
if (!dispdev || !text)
|
||||
return;
|
||||
|
||||
#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \
|
||||
defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014)
|
||||
if (meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_SIMPLIFIED_CHINESE ||
|
||||
meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_TRADITIONAL_CHINESE) {
|
||||
TFTDisplay *tftDisp = static_cast<TFTDisplay *>(dispdev);
|
||||
if (tftDisp->hasCJKFont()) {
|
||||
tftDisp->drawStringCJK(x, y, text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
dispdev->setTextAlignment(alignment);
|
||||
dispdev->drawString(x, y, text);
|
||||
}
|
||||
|
||||
void Screen::setFontL10n(const uint8_t *fontData)
|
||||
{
|
||||
if (!dispdev)
|
||||
return;
|
||||
dispdev->setFont(fontData);
|
||||
}
|
||||
|
||||
void Screen::loadCJKFontIfNeeded()
|
||||
{
|
||||
#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \
|
||||
defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014)
|
||||
if (meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_SIMPLIFIED_CHINESE ||
|
||||
meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_TRADITIONAL_CHINESE) {
|
||||
TFTDisplay *tftDisp = static_cast<TFTDisplay *>(dispdev);
|
||||
if (!tftDisp->hasCJKFont()) {
|
||||
tftDisp->loadCJKFont(FontZH_CN_16, FontZH_CN_16_size);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#else
|
||||
|
||||
+5
-250
@@ -271,9 +271,6 @@ class Screen : public concurrency::OSThread
|
||||
// FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class
|
||||
char ourId[5];
|
||||
|
||||
// if we have a step counter, this stores the number of steps.
|
||||
uint32_t steps = 0;
|
||||
|
||||
/// Initializes the UI, turns on the display, starts showing boot screen.
|
||||
//
|
||||
// Not thread safe - must be called before any other methods are called.
|
||||
@@ -369,255 +366,13 @@ class Screen : public concurrency::OSThread
|
||||
enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP});
|
||||
}
|
||||
|
||||
/// Overrides the default utf8 character conversion, to replace empty space with question marks
|
||||
static char customFontTableLookup(const uint8_t ch)
|
||||
{
|
||||
// UTF-8 to font table index converter
|
||||
// Code from http://playground.arduino.cc/Main/Utf8ascii
|
||||
static uint8_t LASTCHAR;
|
||||
static bool SKIPREST; // Only display a single unconvertable-character symbol per sequence of unconvertable characters
|
||||
static char customFontTableLookup(const uint8_t ch);
|
||||
|
||||
if (ch < 128) { // Standard ASCII-set 0..0x7F handling
|
||||
LASTCHAR = 0;
|
||||
SKIPREST = false;
|
||||
return ch;
|
||||
}
|
||||
void drawTextL10n(int16_t x, int16_t y, const char *text, OLEDDISPLAY_TEXT_ALIGNMENT alignment = TEXT_ALIGN_LEFT);
|
||||
void setFontL10n(const uint8_t *fontData);
|
||||
|
||||
uint8_t last = LASTCHAR; // get last char
|
||||
LASTCHAR = ch;
|
||||
|
||||
switch (last) {
|
||||
case 0xC2: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)ch;
|
||||
}
|
||||
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
}
|
||||
|
||||
// We want to strip out prefix chars for two-byte char formats
|
||||
if (ch == 0xC2 || ch == 0xC3)
|
||||
return (uint8_t)0;
|
||||
|
||||
#if defined(OLED_PL)
|
||||
|
||||
switch (last) {
|
||||
case 0xC3: {
|
||||
|
||||
if (ch == 147)
|
||||
return (uint8_t)(ch); // Ó
|
||||
else if (ch == 179)
|
||||
return (uint8_t)(148); // ó
|
||||
else
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xC4: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch);
|
||||
}
|
||||
|
||||
case 0xC5: {
|
||||
SKIPREST = false;
|
||||
if (ch == 132)
|
||||
return (uint8_t)(136); // ń
|
||||
else if (ch == 186)
|
||||
return (uint8_t)(137); // ź
|
||||
else
|
||||
return (uint8_t)(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We want to strip out prefix chars for two-byte char formats
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5)
|
||||
return (uint8_t)0;
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(OLED_UA) || defined(OLED_RU)
|
||||
|
||||
switch (last) {
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
// map UTF-8 cyrillic chars to it Windows-1251 (CP-1251) ASCII codes
|
||||
// note: in this case we must use compatible font - provided ArialMT_Plain_10/16/24 by 'ThingPulse/esp8266-oled-ssd1306'
|
||||
// library have empty chars for non-latin ASCII symbols
|
||||
case 0xD0: {
|
||||
SKIPREST = false;
|
||||
if (ch == 132)
|
||||
return (uint8_t)(170); // Є
|
||||
if (ch == 134)
|
||||
return (uint8_t)(178); // І
|
||||
if (ch == 135)
|
||||
return (uint8_t)(175); // Ї
|
||||
if (ch == 129)
|
||||
return (uint8_t)(168); // Ё
|
||||
if (ch > 143 && ch < 192)
|
||||
return (uint8_t)(ch + 48);
|
||||
break;
|
||||
}
|
||||
case 0xD1: {
|
||||
SKIPREST = false;
|
||||
if (ch == 148)
|
||||
return (uint8_t)(186); // є
|
||||
if (ch == 150)
|
||||
return (uint8_t)(179); // і
|
||||
if (ch == 151)
|
||||
return (uint8_t)(191); // ї
|
||||
if (ch == 145)
|
||||
return (uint8_t)(184); // ё
|
||||
if (ch > 127 && ch < 144)
|
||||
return (uint8_t)(ch + 112);
|
||||
break;
|
||||
}
|
||||
case 0xD2: {
|
||||
SKIPREST = false;
|
||||
if (ch == 144)
|
||||
return (uint8_t)(165); // Ґ
|
||||
if (ch == 145)
|
||||
return (uint8_t)(180); // ґ
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We want to strip out prefix chars for two-byte char formats
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0x82 || ch == 0xD0 || ch == 0xD1)
|
||||
return (uint8_t)0;
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(OLED_CS)
|
||||
|
||||
switch (last) {
|
||||
case 0xC2: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)ch;
|
||||
}
|
||||
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
|
||||
case 0xC4: {
|
||||
SKIPREST = false;
|
||||
if (ch == 140)
|
||||
return (uint8_t)(129); // Č
|
||||
if (ch == 141)
|
||||
return (uint8_t)(138); // č
|
||||
if (ch == 142)
|
||||
return (uint8_t)(130); // Ď
|
||||
if (ch == 143)
|
||||
return (uint8_t)(139); // ď
|
||||
if (ch == 154)
|
||||
return (uint8_t)(131); // Ě
|
||||
if (ch == 155)
|
||||
return (uint8_t)(140); // ě
|
||||
// Slovak specific glyphs
|
||||
if (ch == 185)
|
||||
return (uint8_t)(147); // Ĺ
|
||||
if (ch == 186)
|
||||
return (uint8_t)(148); // ĺ
|
||||
if (ch == 189)
|
||||
return (uint8_t)(149); // Ľ
|
||||
if (ch == 190)
|
||||
return (uint8_t)(150); // ľ
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xC5: {
|
||||
SKIPREST = false;
|
||||
if (ch == 135)
|
||||
return (uint8_t)(132); // Ň
|
||||
if (ch == 136)
|
||||
return (uint8_t)(141); // ň
|
||||
if (ch == 152)
|
||||
return (uint8_t)(133); // Ř
|
||||
if (ch == 153)
|
||||
return (uint8_t)(142); // ř
|
||||
if (ch == 160)
|
||||
return (uint8_t)(134); // Š
|
||||
if (ch == 161)
|
||||
return (uint8_t)(143); // š
|
||||
if (ch == 164)
|
||||
return (uint8_t)(135); // Ť
|
||||
if (ch == 165)
|
||||
return (uint8_t)(144); // ť
|
||||
if (ch == 174)
|
||||
return (uint8_t)(136); // Ů
|
||||
if (ch == 175)
|
||||
return (uint8_t)(145); // ů
|
||||
if (ch == 189)
|
||||
return (uint8_t)(137); // Ž
|
||||
if (ch == 190)
|
||||
return (uint8_t)(146); // ž
|
||||
// Slovak specific glyphs
|
||||
if (ch == 148)
|
||||
return (uint8_t)(151); // Ŕ
|
||||
if (ch == 149)
|
||||
return (uint8_t)(152); // ŕ
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We want to strip out prefix chars for two-byte char formats
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5)
|
||||
return (uint8_t)0;
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(OLED_GR)
|
||||
|
||||
switch (last) {
|
||||
case 0xC3: {
|
||||
SKIPREST = false;
|
||||
return (uint8_t)(ch | 0xC0);
|
||||
}
|
||||
// Map UTF-8 Greek chars to Windows-1253 (CP-1253) ASCII codes
|
||||
case 0xCE: {
|
||||
SKIPREST = false;
|
||||
// Uppercase Greek: Α-Ρ (U+0391-U+03A1) -> CP-1253 193-209
|
||||
if (ch >= 145 && ch <= 161)
|
||||
return (uint8_t)(ch + 48);
|
||||
// Uppercase Greek: Σ-Ω (U+03A3-U+03A9) -> CP-1253 211-217
|
||||
else if (ch >= 163 && ch <= 169)
|
||||
return (uint8_t)(ch + 48);
|
||||
// Lowercase Greek: α-ρ (U+03B1-U+03C1) -> CP-1253 225-241
|
||||
else if (ch >= 177 && ch <= 193)
|
||||
return (uint8_t)(ch + 48);
|
||||
break;
|
||||
}
|
||||
case 0xCF: {
|
||||
SKIPREST = false;
|
||||
// Lowercase Greek: ς-ω (U+03C2-U+03C9) -> CP-1253 242-249
|
||||
if (ch >= 130 && ch <= 137)
|
||||
return (uint8_t)(ch + 112);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We want to strip out prefix chars for two-byte Greek char formats
|
||||
if (ch == 0xC2 || ch == 0xC3 || ch == 0xCE || ch == 0xCF)
|
||||
return (uint8_t)0;
|
||||
|
||||
#endif
|
||||
|
||||
// If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the
|
||||
// rest of it
|
||||
if (SKIPREST)
|
||||
return (uint8_t)0;
|
||||
SKIPREST = true;
|
||||
|
||||
return (uint8_t)191; // otherwise: return ¿ if character can't be converted (note that the font map we're using doesn't
|
||||
// stick to standard EASCII codes)
|
||||
}
|
||||
// Load CJK font if language requires it (called from setup & on language change)
|
||||
void loadCJKFontIfNeeded();
|
||||
|
||||
/// Returns a handle to the DebugInfo screen.
|
||||
//
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#include "graphics/fonts/OLEDDisplayFontsGR.h"
|
||||
#endif
|
||||
|
||||
#ifdef OLED_ZH
|
||||
#include "graphics/fonts/OLEDDisplayFontsZH.h"
|
||||
#endif
|
||||
|
||||
#if (defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)) && defined(USE_EINK)
|
||||
#include "graphics/fonts/EinkDisplayFonts.h"
|
||||
#endif
|
||||
@@ -100,7 +104,7 @@
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
// The screen is bigger so use bigger fonts
|
||||
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
|
||||
|
||||
+36
-141
@@ -16,92 +16,6 @@
|
||||
extern SX1509 gpioExtender;
|
||||
#endif
|
||||
|
||||
#ifdef TFT_MESH_OVERRIDE
|
||||
uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
|
||||
#else
|
||||
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
||||
#endif
|
||||
|
||||
#if defined(CO5300_CS)
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for AMOLED driver chip
|
||||
class LGFX : public lgfx::LGFX_Device
|
||||
{
|
||||
lgfx::Panel_CO5300 _panel_instance;
|
||||
lgfx::Bus_SPI _bus_instance;
|
||||
|
||||
public:
|
||||
LGFX(void)
|
||||
{
|
||||
{
|
||||
auto cfg = _bus_instance.config();
|
||||
|
||||
// configure SPI
|
||||
cfg.spi_host = CO5300_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST
|
||||
cfg.spi_mode = SPI_MODE0;
|
||||
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
|
||||
// 80MHz by an integer)
|
||||
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
|
||||
cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin
|
||||
cfg.use_lock = true; // Set to true to use transaction locking
|
||||
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
|
||||
// SPI_DMA_CH_AUTO=auto setting)
|
||||
cfg.pin_sclk = CO5300_SCK; // Set SPI SCLK pin number
|
||||
cfg.pin_io0 = CO5300_IO0;
|
||||
cfg.pin_io1 = CO5300_IO1;
|
||||
cfg.pin_io2 = CO5300_IO2;
|
||||
cfg.pin_io3 = CO5300_IO3;
|
||||
|
||||
_bus_instance.config(cfg); // applies the set value to the bus.
|
||||
_panel_instance.setBus(&_bus_instance); // set the bus on the panel.
|
||||
}
|
||||
|
||||
{ // Set the display panel control.
|
||||
auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
|
||||
|
||||
cfg.pin_cs = CO5300_CS; // Pin number where CS is connected (-1 = disable)
|
||||
cfg.pin_rst = CO5300_RESET; // Pin number where RST is connected (-1 = disable)
|
||||
cfg.panel_width = TFT_WIDTH; // actual displayable width
|
||||
cfg.panel_height = TFT_HEIGHT; // actual displayable height
|
||||
cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is upside down)
|
||||
cfg.offset_x = TFT_OFFSET_X;
|
||||
cfg.offset_y = TFT_OFFSET_Y;
|
||||
cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout
|
||||
cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read
|
||||
cfg.readable = true; // Set to true if data can be read
|
||||
cfg.invert = false; // Set to true if the light/darkness of the panel is reversed
|
||||
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
|
||||
cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units
|
||||
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
|
||||
|
||||
// Set the following only when the display is shifted with a driver with a variable number of pixels
|
||||
cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
|
||||
cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
|
||||
_panel_instance.config(cfg);
|
||||
}
|
||||
|
||||
setPanel(&_panel_instance);
|
||||
}
|
||||
|
||||
bool init()
|
||||
{
|
||||
#ifdef CO5300_RESET
|
||||
LOG_DEBUG("LGFX_Panel_CO5300::init()");
|
||||
lgfx::pinMode(CO5300_RESET, lgfx::pin_mode_t::output);
|
||||
lgfx::gpio_hi(CO5300_RESET);
|
||||
delay(20);
|
||||
lgfx::gpio_lo(CO5300_RESET);
|
||||
delay(30);
|
||||
lgfx::gpio_hi(CO5300_RESET);
|
||||
delay(20);
|
||||
#endif
|
||||
return lgfx::LGFX_Device::init();
|
||||
}
|
||||
};
|
||||
|
||||
static LGFX *tft = nullptr;
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ST7735S)
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||
|
||||
@@ -1448,7 +1362,6 @@ void TFTDisplay::display(bool fromBlank)
|
||||
x_LastPixelUpdate = displayWidth - 1;
|
||||
}
|
||||
|
||||
int y_offset = 0;
|
||||
// Step 3: Copy only the changed span into the pixel line buffer.
|
||||
for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
@@ -1463,58 +1376,13 @@ void TFTDisplay::display(bool fromBlank)
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(CO5300_CS)
|
||||
uint8_t lines_updated = 2;
|
||||
if (y % 2 == 0) {
|
||||
y_byteIndex = ((y + 1) / 8) * displayWidth;
|
||||
y_byteMask = (1 << ((y + 1) & 7));
|
||||
uint32_t bufferIndex = 1;
|
||||
for (x = x_FirstPixelUpdate; x < x_LastPixelUpdate; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions) {
|
||||
linePixelBuffer[bufferIndex++ + x_LastPixelUpdate] = graphics::resolveTFTColorPixel(
|
||||
static_cast<int16_t>(x), static_cast<int16_t>(y), isset, colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
linePixelBuffer[bufferIndex++ + x_LastPixelUpdate] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
#else
|
||||
linePixelBuffer[bufferIndex++ + x_LastPixelUpdate] = isset ? colorTftWhite : colorTftBlack;
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
y_offset = -1;
|
||||
memcpy(&linePixelBuffer[x_LastPixelUpdate + 1], &linePixelBuffer[x_FirstPixelUpdate],
|
||||
2 * (x_LastPixelUpdate - x_FirstPixelUpdate + 1));
|
||||
y_byteIndex = ((y - 1) / 8) * displayWidth;
|
||||
y_byteMask = (1 << ((y - 1) & 7));
|
||||
uint32_t bufferIndex = 0;
|
||||
for (x = x_FirstPixelUpdate; x < x_LastPixelUpdate; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions) {
|
||||
linePixelBuffer[bufferIndex++ + x_FirstPixelUpdate] = graphics::resolveTFTColorPixel(
|
||||
static_cast<int16_t>(x), static_cast<int16_t>(y), isset, colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
linePixelBuffer[bufferIndex++ + x_FirstPixelUpdate] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
#else
|
||||
linePixelBuffer[bufferIndex++ + x_FirstPixelUpdate] = isset ? colorTftWhite : colorTftBlack;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#else
|
||||
uint8_t lines_updated = 1;
|
||||
#endif
|
||||
|
||||
#if defined(HACKADAY_COMMUNICATOR)
|
||||
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
|
||||
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
|
||||
#else
|
||||
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
|
||||
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
|
||||
tft->pushImage(x_FirstPixelUpdate, y + y_offset, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), lines_updated,
|
||||
tft->pushImage(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
#endif
|
||||
somethingChanged = true;
|
||||
@@ -1582,7 +1450,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
// handle display on/off directly
|
||||
switch (com) {
|
||||
case DISPLAYON: {
|
||||
LOG_DEBUG("Display on");
|
||||
// LOG_DEBUG("Display on");
|
||||
backlightEnable->set(true);
|
||||
#if ARCH_PORTDUINO
|
||||
display(true);
|
||||
@@ -1610,7 +1478,7 @@ void TFTDisplay::sendCommand(uint8_t com)
|
||||
break;
|
||||
}
|
||||
case DISPLAYOFF: {
|
||||
LOG_DEBUG("Display off");
|
||||
// LOG_DEBUG("Display off");
|
||||
backlightEnable->set(false);
|
||||
#if ARCH_PORTDUINO
|
||||
tft->clear();
|
||||
@@ -1713,8 +1581,8 @@ bool TFTDisplay::connect()
|
||||
tft = new LGFX;
|
||||
#endif
|
||||
|
||||
LOG_INFO("Power to TFT Backlight");
|
||||
backlightEnable->set(true);
|
||||
LOG_INFO("Power to TFT Backlight");
|
||||
|
||||
#ifdef UNPHONE
|
||||
unphone.backlight(true); // using unPhone library
|
||||
@@ -1742,7 +1610,7 @@ bool TFTDisplay::connect()
|
||||
tft->setRotation(1); // T-Deck has the TFT in landscape
|
||||
#elif defined(T_WATCH_S3)
|
||||
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
||||
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) || defined(T_WATCH_ULTRA)
|
||||
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER)
|
||||
tft->setRotation(0); // use config.yaml to set rotation
|
||||
#else
|
||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||
@@ -1750,11 +1618,7 @@ bool TFTDisplay::connect()
|
||||
tft->fillScreen(getThemeDefaultOffColor());
|
||||
|
||||
if (this->linePixelBuffer == NULL) {
|
||||
#if defined(CO5300_CS)
|
||||
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * 2);
|
||||
#else
|
||||
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
|
||||
#endif
|
||||
|
||||
if (!this->linePixelBuffer) {
|
||||
LOG_ERROR("Not enough memory to create TFT line buffer\n");
|
||||
@@ -1772,4 +1636,35 @@ bool TFTDisplay::connect()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TFTDisplay::loadCJKFont(const uint8_t *vlwData, uint32_t vlwDataLen)
|
||||
{
|
||||
if (!tft || !vlwData || vlwDataLen == 0)
|
||||
return false;
|
||||
cjkFontLoaded = tft->loadFont(vlwData);
|
||||
if (cjkFontLoaded) {
|
||||
LOG_INFO("CJK font loaded (%u bytes)", vlwDataLen);
|
||||
} else {
|
||||
LOG_WARN("Failed to load CJK font");
|
||||
}
|
||||
return cjkFontLoaded;
|
||||
}
|
||||
|
||||
void TFTDisplay::drawStringCJK(int16_t x, int16_t y, const char *text)
|
||||
{
|
||||
if (!tft || !text || !cjkFontLoaded)
|
||||
return;
|
||||
concurrency::LockGuard g(spiLock);
|
||||
uint16_t fgColor = getThemeDefaultOnColor();
|
||||
tft->setTextColor(fgColor, getThemeDefaultOffColor());
|
||||
tft->setCursor(x, y);
|
||||
tft->print(text);
|
||||
}
|
||||
|
||||
int16_t TFTDisplay::getStringWidthCJK(const char *text)
|
||||
{
|
||||
if (!tft || !text || !cjkFontLoaded)
|
||||
return 0;
|
||||
return tft->textWidth(text);
|
||||
}
|
||||
|
||||
#endif // USE_TFTDISPLAY
|
||||
|
||||
@@ -38,6 +38,12 @@ class TFTDisplay : public OLEDDisplay
|
||||
// Functions for changing display brightness
|
||||
void setDisplayBrightness(uint8_t);
|
||||
|
||||
// CJK (Chinese/Japanese/Korean) text rendering via LovyanGFX
|
||||
bool loadCJKFont(const uint8_t *vlwData, uint32_t vlwDataLen);
|
||||
void drawStringCJK(int16_t x, int16_t y, const char *text);
|
||||
int16_t getStringWidthCJK(const char *text);
|
||||
bool hasCJKFont() const { return cjkFontLoaded; }
|
||||
|
||||
/**
|
||||
* shim to make the abstraction happy
|
||||
*
|
||||
@@ -64,4 +70,5 @@ class TFTDisplay : public OLEDDisplay
|
||||
|
||||
uint16_t *linePixelBuffer = nullptr;
|
||||
uint16_t *repaintChunkBuffer = nullptr;
|
||||
bool cjkFontLoaded = false;
|
||||
};
|
||||
|
||||
@@ -99,7 +99,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
||||
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
|
||||
defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \
|
||||
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
|
||||
8, imgQuestionL1);
|
||||
@@ -112,7 +112,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
||||
} else {
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
|
||||
defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
|
||||
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
|
||||
8, imgSFL1);
|
||||
@@ -128,7 +128,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
||||
// TODO: Raspberry Pi supports more than just the one screen size
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
|
||||
defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \
|
||||
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
|
||||
imgInfoL1);
|
||||
|
||||
+288
-132
@@ -13,6 +13,7 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/l10n/Strings.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
@@ -35,6 +36,9 @@
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
using namespace meshtastic::l10n;
|
||||
using meshtastic::l10n::StringKey;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -132,10 +136,15 @@ uint8_t test_count = 0;
|
||||
|
||||
void menuHandler::loraMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
|
||||
static const char *optionsArray[5];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = getString(StringKey::DeviceRole);
|
||||
optionsArray[2] = getString(StringKey::RadioPreset);
|
||||
optionsArray[3] = getString(StringKey::FrequencySlot);
|
||||
optionsArray[4] = getString(StringKey::LoRaRegion);
|
||||
enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "LoRa Actions";
|
||||
bannerOptions.message = getString(StringKey::LoRaActions);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 5;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -156,7 +165,9 @@ void menuHandler::loraMenu()
|
||||
|
||||
void menuHandler::OnboardMessage()
|
||||
{
|
||||
static const char *optionsArray[] = {"OK", "Got it!"};
|
||||
static const char *optionsArray[2];
|
||||
optionsArray[0] = getString(StringKey::OK);
|
||||
optionsArray[1] = getString(StringKey::GotIt);
|
||||
enum optionsNumbers { OK, got };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if HAS_TFT
|
||||
@@ -220,7 +231,7 @@ static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool
|
||||
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{
|
||||
static const LoraRegionOption regionOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
|
||||
{"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433},
|
||||
{"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868},
|
||||
@@ -261,7 +272,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
|
||||
const char *bannerMessage = "Set the LoRa region";
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
bannerMessage = "LoRa Region";
|
||||
bannerMessage = getString(StringKey::LoRaRegion);
|
||||
}
|
||||
|
||||
auto bannerOptions =
|
||||
@@ -353,7 +364,12 @@ void menuHandler::licensedToNormalConfirmMenu()
|
||||
|
||||
void menuHandler::deviceRolePicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||
static const char *optionsArray[5];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = getString(StringKey::Client);
|
||||
optionsArray[2] = getString(StringKey::ClientMute);
|
||||
optionsArray[3] = getString(StringKey::LostAndFound);
|
||||
optionsArray[4] = getString(StringKey::Tracker);
|
||||
enum optionsNumbers {
|
||||
Back = 0,
|
||||
devicerole_client = 1,
|
||||
@@ -362,7 +378,7 @@ void menuHandler::deviceRolePicker()
|
||||
devicerole_tracker = 4
|
||||
};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Device Role";
|
||||
bannerOptions.message = getString(StringKey::DeviceRole);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 5;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -394,9 +410,9 @@ void menuHandler::FrequencySlotPicker()
|
||||
static int optionsEnumArray[MAX_CHANNEL_OPTIONS];
|
||||
static char channelText[MAX_CHANNEL_OPTIONS - 1][12];
|
||||
int options = 0;
|
||||
optionsArray[options] = "Back";
|
||||
optionsArray[options] = getString(StringKey::Back);
|
||||
optionsEnumArray[options++] = Back;
|
||||
optionsArray[options] = "Slot 0 (Auto)";
|
||||
optionsArray[options] = getString(StringKey::Slot0);
|
||||
optionsEnumArray[options++] = 0;
|
||||
|
||||
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
||||
@@ -433,7 +449,7 @@ void menuHandler::FrequencySlotPicker()
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Frequency Slot";
|
||||
bannerOptions.message = getString(StringKey::FrequencySlot);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
@@ -469,7 +485,7 @@ static BannerOverlayOptions buildRegionPresetBanner()
|
||||
static char presetLabelBuf[MAX_PRESET_OPTIONS][12]; // scratch space for name copies
|
||||
int count = 0;
|
||||
|
||||
optionsArray[count] = "Back";
|
||||
optionsArray[count] = getString(StringKey::Back);
|
||||
optionsEnumArray[count++] = -1;
|
||||
|
||||
if (myRegion && myRegion->profile) {
|
||||
@@ -493,7 +509,7 @@ static BannerOverlayOptions buildRegionPresetBanner()
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Radio Preset";
|
||||
bannerOptions.message = getString(StringKey::RadioPreset);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = static_cast<uint8_t>(count);
|
||||
@@ -520,10 +536,13 @@ void menuHandler::radioPresetPicker()
|
||||
|
||||
void menuHandler::twelveHourPicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = getString(StringKey::Hour12);
|
||||
optionsArray[2] = getString(StringKey::Hour24);
|
||||
enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Time Format";
|
||||
bannerOptions.message = getString(StringKey::TimeFormat);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -559,7 +578,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::function<void
|
||||
void menuHandler::clockFacePicker()
|
||||
{
|
||||
static const ClockFaceOption clockFaceOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"Digital", OptionsAction::Select, false},
|
||||
{"Analog", OptionsAction::Select, true},
|
||||
};
|
||||
@@ -595,7 +614,7 @@ void menuHandler::clockFacePicker()
|
||||
void menuHandler::TZPicker()
|
||||
{
|
||||
static const TimezoneOption timezoneOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"US/Hawaii", OptionsAction::Select, "HST10"},
|
||||
{"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"},
|
||||
{"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"},
|
||||
@@ -658,13 +677,20 @@ void menuHandler::TZPicker()
|
||||
void menuHandler::clockMenu()
|
||||
{
|
||||
#if defined(OLED_TINY)
|
||||
static const char *optionsArray[] = {"Back", "Time Format", "Timezone"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = getString(StringKey::TimeFormat);
|
||||
optionsArray[2] = getString(StringKey::Timezone);
|
||||
#else
|
||||
static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
|
||||
static const char *optionsArray[4];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = getString(StringKey::ClockFace);
|
||||
optionsArray[2] = getString(StringKey::TimeFormat);
|
||||
optionsArray[3] = getString(StringKey::Timezone);
|
||||
#endif
|
||||
enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Clock Action";
|
||||
bannerOptions.message = getString(StringKey::ClockAction);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 4;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -692,14 +718,14 @@ void menuHandler::messageResponseMenu()
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
int threadChannel = graphics::MessageRenderer::getThreadChannel();
|
||||
|
||||
optionsArray[options] = "Back";
|
||||
optionsArray[options] = getString(StringKey::Back);
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
// New Reply submenu (replaces Preset and Freetext directly in this menu)
|
||||
optionsArray[options] = "Reply";
|
||||
optionsArray[options] = getString(StringKey::Reply);
|
||||
optionsEnumArray[options++] = ReplyMenu;
|
||||
|
||||
optionsArray[options] = "View Chats";
|
||||
optionsArray[options] = getString(StringKey::ViewChats);
|
||||
optionsEnumArray[options++] = ViewMode;
|
||||
|
||||
// If viewing ALL chats, hide “Mute Chat”
|
||||
@@ -707,21 +733,21 @@ void menuHandler::messageResponseMenu()
|
||||
const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex();
|
||||
const auto &chan = channels.getByIndex(chIndex);
|
||||
|
||||
optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel";
|
||||
optionsArray[options] = chan.settings.module_settings.is_muted ? getString(StringKey::UnmuteChannel) : getString(StringKey::MuteChannel);
|
||||
optionsEnumArray[options++] = MuteChannel;
|
||||
}
|
||||
|
||||
// Delete submenu
|
||||
optionsArray[options] = "Delete";
|
||||
optionsArray[options] = getString(StringKey::Delete);
|
||||
optionsEnumArray[options++] = DeleteMenu;
|
||||
|
||||
#ifdef HAS_I2S
|
||||
optionsArray[options] = "Read Aloud";
|
||||
optionsArray[options] = getString(StringKey::ReadAloud);
|
||||
optionsEnumArray[options++] = Aloud;
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Message Action";
|
||||
bannerOptions.message = getString(StringKey::MessageAction);
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
bannerOptions.message = "Message";
|
||||
}
|
||||
@@ -781,16 +807,16 @@ void menuHandler::replyMenu()
|
||||
int options = 0;
|
||||
|
||||
// Back
|
||||
optionsArray[options] = "Back";
|
||||
optionsArray[options] = getString(StringKey::Back);
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
// Preset reply
|
||||
optionsArray[options] = "With Preset";
|
||||
optionsArray[options] = getString(StringKey::WithPreset);
|
||||
optionsEnumArray[options++] = ReplyPreset;
|
||||
|
||||
// Freetext reply (only when keyboard exists)
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "With Freetext";
|
||||
optionsArray[options] = getString(StringKey::WithFreetext);
|
||||
optionsEnumArray[options++] = ReplyFreetext;
|
||||
}
|
||||
|
||||
@@ -861,21 +887,21 @@ void menuHandler::deleteMessagesMenu()
|
||||
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
|
||||
optionsArray[options] = "Back";
|
||||
optionsArray[options] = getString(StringKey::Back);
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
optionsArray[options] = "Delete Oldest";
|
||||
optionsArray[options] = getString(StringKey::DeleteOldest);
|
||||
optionsEnumArray[options++] = DeleteOldest;
|
||||
|
||||
// If viewing ALL chats → hide “Delete This Chat”
|
||||
if (mode != graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
optionsArray[options] = "Delete This Chat";
|
||||
optionsArray[options] = getString(StringKey::DeleteThisChat);
|
||||
optionsEnumArray[options++] = DeleteThis;
|
||||
}
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
optionsArray[options] = "Delete All";
|
||||
optionsArray[options] = getString(StringKey::DeleteAll);
|
||||
} else {
|
||||
optionsArray[options] = "Delete All Chats";
|
||||
optionsArray[options] = getString(StringKey::DeleteAllChats);
|
||||
}
|
||||
optionsEnumArray[options++] = DeleteAll;
|
||||
|
||||
@@ -945,7 +971,7 @@ void menuHandler::messageViewModeMenu()
|
||||
ids.clear();
|
||||
idToPeer.clear();
|
||||
|
||||
labels.push_back("Back");
|
||||
labels.push_back(getString(StringKey::Back));
|
||||
ids.push_back(-1);
|
||||
labels.push_back("View All Chats");
|
||||
ids.push_back(-2);
|
||||
@@ -1076,30 +1102,31 @@ void menuHandler::homeBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static const char *optionsArray[enumEnd];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
if (moduleConfig.external_notification.enabled && externalNotificationModule &&
|
||||
config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) {
|
||||
if (!externalNotificationModule->getMute()) {
|
||||
optionsArray[options] = "Temporarily Mute";
|
||||
optionsArray[options] = getString(StringKey::TemporarilyMute);
|
||||
} else {
|
||||
optionsArray[options] = "Unmute";
|
||||
optionsArray[options] = getString(StringKey::Unmute);
|
||||
}
|
||||
optionsEnumArray[options++] = Mute;
|
||||
}
|
||||
#if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
|
||||
optionsArray[options] = "Toggle Backlight";
|
||||
optionsArray[options] = getString(StringKey::ToggleBacklight);
|
||||
optionsEnumArray[options++] = Backlight;
|
||||
#else
|
||||
optionsArray[options] = "Sleep Screen";
|
||||
optionsArray[options] = getString(StringKey::SleepScreen);
|
||||
optionsEnumArray[options++] = Sleep;
|
||||
#endif
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
optionsArray[options] = "Send Position";
|
||||
optionsArray[options] = getString(StringKey::SendPosition);
|
||||
} else {
|
||||
optionsArray[options] = "Send Node Info";
|
||||
optionsArray[options] = getString(StringKey::SendNodeInfo);
|
||||
}
|
||||
optionsEnumArray[options++] = Position;
|
||||
|
||||
@@ -1165,18 +1192,19 @@ void menuHandler::textMessageBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static const char *optionsArray[enumEnd];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
optionsArray[options] = getString(StringKey::NewPresetMsg);
|
||||
optionsEnumArray[options++] = Preset;
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsArray[options] = getString(StringKey::NewFreetextMsg);
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Message Action";
|
||||
bannerOptions.message = getString(StringKey::MessageAction);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
@@ -1193,36 +1221,37 @@ void menuHandler::textMessageBaseMenu()
|
||||
void menuHandler::systemBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd };
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static const char *optionsArray[enumEnd];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Notifications";
|
||||
optionsArray[options] = getString(StringKey::Notifications);
|
||||
optionsEnumArray[options++] = Notifications;
|
||||
|
||||
optionsArray[options] = "Display Options";
|
||||
optionsArray[options] = getString(StringKey::DisplayOptions);
|
||||
optionsEnumArray[options++] = ScreenOptions;
|
||||
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
optionsArray[options] = "Bluetooth";
|
||||
optionsArray[options] = getString(StringKey::Bluetooth);
|
||||
} else {
|
||||
optionsArray[options] = "Bluetooth Toggle";
|
||||
optionsArray[options] = getString(StringKey::BluetoothToggle);
|
||||
}
|
||||
optionsEnumArray[options++] = Bluetooth;
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
optionsArray[options] = "WiFi Toggle";
|
||||
optionsArray[options] = getString(StringKey::WifiToggle);
|
||||
optionsEnumArray[options++] = WiFiToggle;
|
||||
#endif
|
||||
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
optionsArray[options] = "Power";
|
||||
optionsArray[options] = getString(StringKey::Power);
|
||||
} else {
|
||||
optionsArray[options] = "Reboot/Shutdown";
|
||||
optionsArray[options] = getString(StringKey::RebootShutdown);
|
||||
}
|
||||
optionsEnumArray[options++] = PowerMenu;
|
||||
|
||||
if (test_enabled) {
|
||||
optionsArray[options] = "Test Menu";
|
||||
optionsArray[options] = getString(StringKey::TestMenu);
|
||||
optionsEnumArray[options++] = Test;
|
||||
}
|
||||
|
||||
@@ -1269,7 +1298,8 @@ void menuHandler::favoriteBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static const char *optionsArray[enumEnd];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
@@ -1283,26 +1313,26 @@ void menuHandler::favoriteBaseMenu()
|
||||
}
|
||||
}
|
||||
if (hasConversation) {
|
||||
optionsArray[options] = "Go To Chat";
|
||||
optionsArray[options] = getString(StringKey::GoToChat);
|
||||
optionsEnumArray[options++] = GoToChat;
|
||||
}
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
optionsArray[options] = "New Preset";
|
||||
optionsArray[options] = getString(StringKey::NewPreset);
|
||||
} else {
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
optionsArray[options] = getString(StringKey::NewPresetMsg);
|
||||
}
|
||||
optionsEnumArray[options++] = Preset;
|
||||
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsArray[options] = getString(StringKey::NewFreetextMsg);
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
|
||||
if (currentResolution != ScreenResolution::UltraLow) {
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsArray[options] = getString(StringKey::TraceRoute);
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
}
|
||||
optionsArray[options] = "Remove Favorite";
|
||||
optionsArray[options] = getString(StringKey::RemoveFavorite);
|
||||
optionsEnumArray[options++] = Remove;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
@@ -1354,7 +1384,7 @@ void menuHandler::positionBaseMenu()
|
||||
};
|
||||
|
||||
static const PositionMenuOption baseOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"On/Off Toggle", OptionsAction::Select, static_cast<int>(PositionAction::GpsToggle)},
|
||||
{"Format", OptionsAction::Select, static_cast<int>(PositionAction::GpsFormat)},
|
||||
{"Smart Position", OptionsAction::Select, static_cast<int>(PositionAction::GPSSmartPosition)},
|
||||
@@ -1364,7 +1394,7 @@ void menuHandler::positionBaseMenu()
|
||||
};
|
||||
|
||||
static const PositionMenuOption calibrateOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"On/Off Toggle", OptionsAction::Select, static_cast<int>(PositionAction::GpsToggle)},
|
||||
{"Format", OptionsAction::Select, static_cast<int>(PositionAction::GpsFormat)},
|
||||
{"Smart Position", OptionsAction::Select, static_cast<int>(PositionAction::GPSSmartPosition)},
|
||||
@@ -1448,18 +1478,19 @@ void menuHandler::positionBaseMenu()
|
||||
void menuHandler::nodeListMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static const char *optionsArray[enumEnd];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Node Actions / Settings";
|
||||
optionsArray[options] = getString(StringKey::NodeActionsSettings);
|
||||
optionsEnumArray[options++] = NodePicker;
|
||||
|
||||
if (currentResolution != ScreenResolution::UltraLow) {
|
||||
optionsArray[options] = "Show Long/Short Name";
|
||||
optionsArray[options] = getString(StringKey::ShowNameLength);
|
||||
optionsEnumArray[options++] = NodeNameLength;
|
||||
}
|
||||
optionsArray[options] = "Reset NodeDB";
|
||||
optionsArray[options] = getString(StringKey::NodeDbReset);
|
||||
optionsEnumArray[options++] = Reset;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
@@ -1509,35 +1540,36 @@ void menuHandler::manageNodeMenu()
|
||||
return;
|
||||
}
|
||||
enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd };
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static const char *optionsArray[enumEnd];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
optionsArray[options] = "Unfavorite";
|
||||
optionsArray[options] = getString(StringKey::RemoveFavorite);
|
||||
} else {
|
||||
optionsArray[options] = "Favorite";
|
||||
optionsArray[options] = getString(StringKey::Favorite);
|
||||
}
|
||||
optionsEnumArray[options++] = Favorite;
|
||||
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
if (isMuted) {
|
||||
optionsArray[options] = "Unmute Notifications";
|
||||
optionsArray[options] = getString(StringKey::UnmuteNotifications);
|
||||
} else {
|
||||
optionsArray[options] = "Mute Notifications";
|
||||
optionsArray[options] = getString(StringKey::MuteNotifications);
|
||||
}
|
||||
optionsEnumArray[options++] = Mute;
|
||||
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsArray[options] = getString(StringKey::TraceRoute);
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
|
||||
optionsArray[options] = "Key Verification";
|
||||
optionsArray[options] = getString(StringKey::KeyVerification);
|
||||
optionsEnumArray[options++] = KeyVerification;
|
||||
|
||||
if (nodeInfoLiteIsIgnored(node)) {
|
||||
optionsArray[options] = "Unignore Node";
|
||||
optionsArray[options] = getString(StringKey::UnignoreNode);
|
||||
} else {
|
||||
optionsArray[options] = "Ignore Node";
|
||||
optionsArray[options] = getString(StringKey::IgnoreNode);
|
||||
}
|
||||
optionsEnumArray[options++] = Ignore;
|
||||
|
||||
@@ -1643,7 +1675,7 @@ void menuHandler::manageNodeMenu()
|
||||
void menuHandler::nodeNameLengthMenu()
|
||||
{
|
||||
static const NodeNameOption nodeNameOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"Long", OptionsAction::Select, true},
|
||||
{"Short", OptionsAction::Select, false},
|
||||
};
|
||||
@@ -1681,7 +1713,10 @@ void menuHandler::nodeNameLengthMenu()
|
||||
|
||||
void menuHandler::resetNodeDBMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Reset All";
|
||||
optionsArray[2] = "Preserve Favorites";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Confirm Reset NodeDB";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -1710,7 +1745,7 @@ void menuHandler::resetNodeDBMenu()
|
||||
void menuHandler::compassNorthMenu()
|
||||
{
|
||||
static const CompassOption compassOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC},
|
||||
{"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING},
|
||||
{"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING},
|
||||
@@ -1756,7 +1791,7 @@ void menuHandler::compassNorthMenu()
|
||||
void menuHandler::GPSToggleMenu()
|
||||
{
|
||||
static const GPSToggleOption gpsToggleOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED},
|
||||
{"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED},
|
||||
};
|
||||
@@ -1805,7 +1840,7 @@ void menuHandler::GPSToggleMenu()
|
||||
void menuHandler::GPSFormatMenu()
|
||||
{
|
||||
static const GPSFormatOption formatOptionsHigh[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC},
|
||||
{"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS},
|
||||
{"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM},
|
||||
@@ -1816,7 +1851,7 @@ void menuHandler::GPSFormatMenu()
|
||||
};
|
||||
|
||||
static const GPSFormatOption formatOptionsLow[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{getString(StringKey::Back), OptionsAction::Back},
|
||||
{"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC},
|
||||
{"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS},
|
||||
{"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM},
|
||||
@@ -1854,7 +1889,7 @@ void menuHandler::GPSFormatMenu()
|
||||
int initialSelection = 0;
|
||||
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection);
|
||||
bannerOptions = createStaticBannerOptions(getString(StringKey::GPSFormat), formatOptionsHigh, formatLabelsHigh, onSelection);
|
||||
for (size_t i = 0; i < formatCount; ++i) {
|
||||
if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) {
|
||||
initialSelection = static_cast<int>(i);
|
||||
@@ -1862,7 +1897,7 @@ void menuHandler::GPSFormatMenu()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection);
|
||||
bannerOptions = createStaticBannerOptions(getString(StringKey::GPSFormat), formatOptionsLow, formatLabelsLow, onSelection);
|
||||
for (size_t i = 0; i < formatCount; ++i) {
|
||||
if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) {
|
||||
initialSelection = static_cast<int>(i);
|
||||
@@ -1877,7 +1912,10 @@ void menuHandler::GPSFormatMenu()
|
||||
|
||||
void menuHandler::GPSSmartPositionMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Enabled";
|
||||
optionsArray[2] = "Disabled";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Toggle Smart Position";
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
@@ -1907,9 +1945,23 @@ void menuHandler::GPSSmartPositionMenu()
|
||||
|
||||
void menuHandler::GPSUpdateIntervalMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds",
|
||||
"2 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour",
|
||||
"6 hours", "12 hours", "24 hours", "At Boot Only"};
|
||||
static const char *optionsArray[16];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "8 seconds";
|
||||
optionsArray[2] = "20 seconds";
|
||||
optionsArray[3] = "40 seconds";
|
||||
optionsArray[4] = "1 minute";
|
||||
optionsArray[5] = "80 seconds";
|
||||
optionsArray[6] = "2 minutes";
|
||||
optionsArray[7] = "5 minutes";
|
||||
optionsArray[8] = "10 minutes";
|
||||
optionsArray[9] = "15 minutes";
|
||||
optionsArray[10] = "30 minutes";
|
||||
optionsArray[11] = "1 hour";
|
||||
optionsArray[12] = "6 hours";
|
||||
optionsArray[13] = "12 hours";
|
||||
optionsArray[14] = "24 hours";
|
||||
optionsArray[15] = "At Boot Only";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Update Interval";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -1995,9 +2047,24 @@ void menuHandler::GPSUpdateIntervalMenu()
|
||||
|
||||
void menuHandler::GPSPositionBroadcastMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour",
|
||||
"2 hours", "3 hours", "4 hours", "5 hours", "6 hours", "12 hours",
|
||||
"18 hours", "24 hours", "36 hours", "48 hours", "72 hours"};
|
||||
static const char *optionsArray[17];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "1 minute";
|
||||
optionsArray[2] = "90 seconds";
|
||||
optionsArray[3] = "5 minutes";
|
||||
optionsArray[4] = "15 minutes";
|
||||
optionsArray[5] = "1 hour";
|
||||
optionsArray[6] = "2 hours";
|
||||
optionsArray[7] = "3 hours";
|
||||
optionsArray[8] = "4 hours";
|
||||
optionsArray[9] = "5 hours";
|
||||
optionsArray[10] = "6 hours";
|
||||
optionsArray[11] = "12 hours";
|
||||
optionsArray[12] = "18 hours";
|
||||
optionsArray[13] = "24 hours";
|
||||
optionsArray[14] = "36 hours";
|
||||
optionsArray[15] = "48 hours";
|
||||
optionsArray[16] = "72 hours";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Broadcast Interval";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2089,11 +2156,14 @@ void menuHandler::GPSPositionBroadcastMenu()
|
||||
|
||||
void menuHandler::bluetoothToggleMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Enabled";
|
||||
optionsArray[2] = "Disabled";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Toggle Bluetooth";
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
bannerOptions.message = "Bluetooth";
|
||||
bannerOptions.message = getString(StringKey::Bluetooth);
|
||||
}
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
@@ -2111,7 +2181,12 @@ void menuHandler::bluetoothToggleMenu()
|
||||
|
||||
void menuHandler::BuzzerModeMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"};
|
||||
static const char *optionsArray[5];
|
||||
optionsArray[0] = "All Enabled";
|
||||
optionsArray[1] = "All Disabled";
|
||||
optionsArray[2] = getString(StringKey::Notifications);
|
||||
optionsArray[3] = "System Only";
|
||||
optionsArray[4] = "DMs Only";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Notification Sounds";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2126,7 +2201,11 @@ void menuHandler::BuzzerModeMenu()
|
||||
|
||||
void menuHandler::BrightnessPickerMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Low", "Medium", "High"};
|
||||
static const char *optionsArray[4];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Low";
|
||||
optionsArray[2] = "Medium";
|
||||
optionsArray[3] = "High";
|
||||
|
||||
// Get current brightness level to set initial selection
|
||||
int currentSelection = 1; // Default to Medium
|
||||
@@ -2139,7 +2218,7 @@ void menuHandler::BrightnessPickerMenu()
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Brightness";
|
||||
bannerOptions.message = getString(StringKey::Brightness);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 4;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -2151,7 +2230,7 @@ void menuHandler::BrightnessPickerMenu()
|
||||
uiconfig.screen_brightness = 255;
|
||||
}
|
||||
|
||||
if (selected != 0) { // Not "Back"
|
||||
if (selected != 0) { // Not getString(StringKey::Back)
|
||||
// Apply brightness immediately
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
|
||||
// For HELTEC devices, use analogWrite to control backlight
|
||||
@@ -2174,7 +2253,9 @@ void menuHandler::BrightnessPickerMenu()
|
||||
|
||||
void menuHandler::switchToMUIMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"No", "Yes"};
|
||||
static const char *optionsArray[2];
|
||||
optionsArray[0] = "No";
|
||||
optionsArray[1] = "Yes";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Switch to MUI?";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2192,11 +2273,13 @@ void menuHandler::switchToMUIMenu()
|
||||
|
||||
void menuHandler::rebootMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||
static const char *optionsArray[2];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Confirm";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Reboot Device?";
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
bannerOptions.message = "Reboot";
|
||||
bannerOptions.message = getString(StringKey::Reboot);
|
||||
}
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
@@ -2216,11 +2299,13 @@ void menuHandler::rebootMenu()
|
||||
|
||||
void menuHandler::shutdownMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||
static const char *optionsArray[2];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Confirm";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Shutdown Device?";
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
bannerOptions.message = "Shutdown";
|
||||
bannerOptions.message = getString(StringKey::Shutdown);
|
||||
}
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
@@ -2239,7 +2324,9 @@ void menuHandler::shutdownMenu()
|
||||
void menuHandler::removeFavoriteMenu()
|
||||
{
|
||||
|
||||
static const char *optionsArray[] = {"Back", "Yes"};
|
||||
static const char *optionsArray[2];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Yes";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
std::string message = "Unfavorite This Node?\n";
|
||||
const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
@@ -2273,17 +2360,18 @@ void menuHandler::testMenu()
|
||||
{
|
||||
|
||||
enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce };
|
||||
static const char *optionsArray[5] = {"Back"};
|
||||
static const char *optionsArray[5];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[5] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Number Picker";
|
||||
optionsArray[options] = getString(StringKey::NumberPicker);
|
||||
optionsEnumArray[options++] = NumberPicker;
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
||||
optionsEnumArray[options++] = ShowChirpy;
|
||||
#ifdef HAS_I2S
|
||||
optionsArray[options] = "Test Announce";
|
||||
optionsArray[options] = getString(StringKey::TestAnnounce);
|
||||
optionsEnumArray[options++] = TestAnnounce;
|
||||
#endif
|
||||
|
||||
@@ -2322,7 +2410,9 @@ void menuHandler::wifiBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Wifi_toggle };
|
||||
|
||||
static const char *optionsArray[] = {"Back", "WiFi Toggle"};
|
||||
static const char *optionsArray[2];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = getString(StringKey::WifiToggle);
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "WiFi Menu";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2340,7 +2430,10 @@ void menuHandler::wifiToggleMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Wifi_disable, Wifi_enable };
|
||||
|
||||
static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "WiFi Disabled";
|
||||
optionsArray[2] = "WiFi Enabled";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "WiFi Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2377,33 +2470,40 @@ void menuHandler::screenOptionsMenu()
|
||||
bool hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme };
|
||||
static const char *optionsArray[7] = {"Back"};
|
||||
static int optionsEnumArray[7] = {Back};
|
||||
enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme, LangPicker };
|
||||
static const char *optionsArray[8];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[8] = {Back};
|
||||
int options = 1;
|
||||
|
||||
// Only show brightness for B&W displays
|
||||
if (hasSupportBrightness) {
|
||||
optionsArray[options] = "Brightness";
|
||||
optionsArray[options] = getString(StringKey::Brightness);
|
||||
optionsEnumArray[options++] = Brightness;
|
||||
}
|
||||
|
||||
optionsArray[options] = "Frame Visibility";
|
||||
optionsArray[options] = getString(StringKey::FrameVisibility);
|
||||
optionsEnumArray[options++] = FrameToggles;
|
||||
|
||||
optionsArray[options] = "Display Units";
|
||||
optionsArray[options] = getString(StringKey::DisplayUnits);
|
||||
optionsEnumArray[options++] = DisplayUnits;
|
||||
|
||||
optionsArray[options] = "Message Bubbles";
|
||||
optionsArray[options] = getString(StringKey::MessageBubbles);
|
||||
optionsEnumArray[options++] = MessageBubbles;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
optionsArray[options] = "Theme";
|
||||
optionsArray[options] = getString(StringKey::Theme);
|
||||
optionsEnumArray[options++] = Theme;
|
||||
#endif
|
||||
|
||||
#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \
|
||||
defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014)
|
||||
optionsArray[options] = getString(StringKey::Language);
|
||||
optionsEnumArray[options++] = LangPicker;
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Display Options";
|
||||
bannerOptions.message = getString(StringKey::DisplayOptions);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
@@ -2423,6 +2523,9 @@ void menuHandler::screenOptionsMenu()
|
||||
} else if (selected == Theme) {
|
||||
menuHandler::menuQueue = menuHandler::ThemeMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == LangPicker) {
|
||||
menuHandler::menuQueue = menuHandler::LanguagePicker;
|
||||
screen->runNow();
|
||||
} else {
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
@@ -2435,25 +2538,26 @@ void menuHandler::powerMenu()
|
||||
{
|
||||
|
||||
enum optionsNumbers { Back, Reboot, Shutdown, MUI };
|
||||
static const char *optionsArray[4] = {"Back"};
|
||||
static const char *optionsArray[4];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[4] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Reboot";
|
||||
optionsArray[options] = getString(StringKey::Reboot);
|
||||
optionsEnumArray[options++] = Reboot;
|
||||
|
||||
optionsArray[options] = "Shutdown";
|
||||
optionsArray[options] = getString(StringKey::Shutdown);
|
||||
optionsEnumArray[options++] = Shutdown;
|
||||
|
||||
#if HAS_TFT
|
||||
optionsArray[options] = "Switch to MUI";
|
||||
optionsArray[options] = getString(StringKey::SwitchToMUI);
|
||||
optionsEnumArray[options++] = MUI;
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Reboot / Shutdown";
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
bannerOptions.message = "Power";
|
||||
bannerOptions.message = getString(StringKey::Power);
|
||||
}
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
@@ -2490,7 +2594,9 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet
|
||||
|
||||
if (screen) {
|
||||
static const char *optionsArray[] = {"Reject", "Accept"};
|
||||
static const char *optionsArray[2];
|
||||
optionsArray[0] = "Reject";
|
||||
optionsArray[1] = "Accept";
|
||||
graphics::BannerOverlayOptions options;
|
||||
options.message = message;
|
||||
options.durationMs = 30000;
|
||||
@@ -2526,7 +2632,8 @@ void menuHandler::frameTogglesMenu()
|
||||
show_power,
|
||||
enumEnd
|
||||
};
|
||||
static const char *optionsArray[enumEnd] = {"Finish"};
|
||||
static const char *optionsArray[enumEnd];
|
||||
optionsArray[0] = "Finish";
|
||||
static int optionsEnumArray[enumEnd] = {Finish};
|
||||
int options = 1;
|
||||
|
||||
@@ -2655,7 +2762,10 @@ void menuHandler::displayUnitsMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
|
||||
|
||||
static const char *optionsArray[] = {"Back", "Metric", "Imperial"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Metric";
|
||||
optionsArray[2] = "Imperial";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = " Select display units";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2683,9 +2793,12 @@ void menuHandler::messageBubblesMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, ShowBubbles, HideBubbles };
|
||||
|
||||
static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"};
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
optionsArray[1] = "Show Bubbles";
|
||||
optionsArray[2] = "Hide Bubbles";
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Message Bubbles";
|
||||
bannerOptions.message = getString(StringKey::MessageBubbles);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2;
|
||||
@@ -2710,11 +2823,12 @@ void menuHandler::themeMenu()
|
||||
{
|
||||
// Build menu dynamically from the theme table.
|
||||
// Only visible themes appear!
|
||||
// Slot budget: 1 for "Back" + up to kMaxThemesInMenu visible themes.
|
||||
// Slot budget: 1 for getString(StringKey::Back) + up to kMaxThemesInMenu visible themes.
|
||||
// Bump kMaxThemesInMenu if you add more themes than will fit here.
|
||||
constexpr size_t kMaxThemesInMenu = 15;
|
||||
const size_t visibleCount = getVisibleThemeCount();
|
||||
static const char *optionsArray[kMaxThemesInMenu + 1] = {"Back"};
|
||||
static const char *optionsArray[kMaxThemesInMenu + 1];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
const size_t shownCount = (visibleCount < kMaxThemesInMenu) ? visibleCount : kMaxThemesInMenu;
|
||||
const int options = static_cast<int>(shownCount) + 1; // +1 for Back
|
||||
|
||||
@@ -2723,12 +2837,12 @@ void menuHandler::themeMenu()
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Theme";
|
||||
bannerOptions.message = getString(StringKey::Theme);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
|
||||
// Highlight the currently active theme (visible index + 1 for the Back
|
||||
// offset). If the active theme is hidden, leave selection on "Back".
|
||||
// offset). If the active theme is hidden, leave selection on getString(StringKey::Back).
|
||||
const size_t activeVisible = getActiveVisibleThemeIndex();
|
||||
bannerOptions.InitialSelected = (activeVisible == SIZE_MAX) ? 0 : static_cast<int>(activeVisible) + 1;
|
||||
|
||||
@@ -2903,6 +3017,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case ThemeMenu:
|
||||
themeMenu();
|
||||
break;
|
||||
case LanguagePicker:
|
||||
languagePickerMenu();
|
||||
break;
|
||||
case HamModeConfirm:
|
||||
hamModeConfirmMenu();
|
||||
break;
|
||||
@@ -2913,6 +3030,45 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
|
||||
void menuHandler::languagePickerMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, English, Chinese };
|
||||
static const char *optionsArray[3];
|
||||
optionsArray[0] = getString(StringKey::Back);
|
||||
static int optionsEnumArray[3] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = getString(StringKey::English);
|
||||
optionsEnumArray[options++] = English;
|
||||
|
||||
optionsArray[options] = getString(StringKey::Chinese);
|
||||
optionsEnumArray[options++] = Chinese;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = getString(StringKey::Language);
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == English) {
|
||||
uiconfig.language = meshtastic_Language_ENGLISH;
|
||||
meshtastic::l10n::currentDisplayLanguage = meshtastic_Language_ENGLISH;
|
||||
saveUIConfig();
|
||||
} else if (selected == Chinese) {
|
||||
uiconfig.language = meshtastic_Language_SIMPLIFIED_CHINESE;
|
||||
meshtastic::l10n::currentDisplayLanguage = meshtastic_Language_SIMPLIFIED_CHINESE;
|
||||
saveUIConfig();
|
||||
#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \
|
||||
defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014)
|
||||
screen->loadCJKFontIfNeeded();
|
||||
#endif
|
||||
}
|
||||
menuQueue = ScreenOptionsMenu;
|
||||
screen->runNow();
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::saveUIConfig()
|
||||
{
|
||||
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig);
|
||||
|
||||
@@ -56,6 +56,7 @@ class menuHandler
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu,
|
||||
ThemeMenu,
|
||||
LanguagePicker,
|
||||
HamModeConfirm,
|
||||
LicensedToNormalConfirm
|
||||
};
|
||||
@@ -107,6 +108,7 @@ class menuHandler
|
||||
static void wifiBaseMenu();
|
||||
static void wifiToggleMenu();
|
||||
static void screenOptionsMenu();
|
||||
static void languagePickerMenu();
|
||||
static void powerMenu();
|
||||
static void nodeNameLengthMenu();
|
||||
static void frameTogglesMenu();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/l10n/Strings.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "main.h"
|
||||
@@ -75,6 +76,15 @@ void scrollDown()
|
||||
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||
{
|
||||
#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \
|
||||
defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014) || defined(USE_ST7789) || \
|
||||
defined(USE_ST7796)
|
||||
if (meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_SIMPLIFIED_CHINESE ||
|
||||
meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_TRADITIONAL_CHINESE) {
|
||||
screen->drawTextL10n(x, y, line.c_str());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, FONT_HEIGHT_SMALL, emotes, emoteCount);
|
||||
}
|
||||
|
||||
@@ -482,7 +492,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// Still in ALL mode and no messages at all → show placeholder
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
didReset = false;
|
||||
const char *messageString = "No messages";
|
||||
const char *messageString = meshtastic::l10n::getString(meshtastic::l10n::StringKey::NoMessages);
|
||||
int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2);
|
||||
display->drawString(center_text, getTextPositions(display)[2], messageString);
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/images.h"
|
||||
#include "graphics/l10n/Strings.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#if HAS_BUTTON
|
||||
@@ -39,6 +41,27 @@ extern std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
extern bool hasUnreadMessage;
|
||||
|
||||
namespace
|
||||
{
|
||||
inline bool isCJKLanguage()
|
||||
{
|
||||
return meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_SIMPLIFIED_CHINESE ||
|
||||
meshtastic::l10n::currentDisplayLanguage == meshtastic_Language_TRADITIONAL_CHINESE;
|
||||
}
|
||||
|
||||
inline void drawBannerText(OLEDDisplay *display, int16_t x, int16_t y, const char *text)
|
||||
{
|
||||
#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \
|
||||
defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(RAK14014)
|
||||
if (isCJKLanguage() && ::screen) {
|
||||
::screen->drawTextL10n(x, y, text);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
display->drawString(x, y, text);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
int bannerSignalBars = -1;
|
||||
@@ -184,7 +207,7 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawString(64 + x, y, "Creating SSL certificate");
|
||||
display->drawString(64 + x, y, meshtastic::l10n::getString(meshtastic::l10n::StringKey::CreatingSSLCert));
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
yield();
|
||||
@@ -193,9 +216,9 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
if ((millis() / 1000) % 2) {
|
||||
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . .");
|
||||
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, meshtastic::l10n::getString(meshtastic::l10n::StringKey::PleaseWait));
|
||||
} else {
|
||||
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ");
|
||||
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, meshtastic::l10n::getString(meshtastic::l10n::StringKey::PleaseWait));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,9 +942,12 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->setColor(BLACK);
|
||||
int yOffset = 3;
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
UIRenderer::drawStringWithEmotes(display, textX, lineY - yOffset, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (isCJKLanguage())
|
||||
drawBannerText(display, textX, lineY - yOffset, lineBuffer);
|
||||
else
|
||||
UIRenderer::drawStringWithEmotes(display, textX, lineY - yOffset, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
} else {
|
||||
display->drawString(textX, lineY - yOffset, lineBuffer);
|
||||
drawBannerText(display, textX, lineY - yOffset, lineBuffer);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
lineY += (thisLineHeight - 2 - background_yOffset);
|
||||
@@ -943,9 +969,12 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
|
||||
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (isCJKLanguage())
|
||||
drawBannerText(display, groupStartX, lineY, lineBuffer);
|
||||
else
|
||||
UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
} else {
|
||||
display->drawString(groupStartX, lineY, lineBuffer);
|
||||
drawBannerText(display, groupStartX, lineY, lineBuffer);
|
||||
}
|
||||
|
||||
int baseX = groupStartX + textWidth + gap;
|
||||
@@ -977,9 +1006,12 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
}
|
||||
} else {
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (isCJKLanguage())
|
||||
drawBannerText(display, textX, lineY, lineBuffer);
|
||||
else
|
||||
UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
} else {
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
drawBannerText(display, textX, lineY, lineBuffer);
|
||||
}
|
||||
}
|
||||
lineY += thisLineHeight;
|
||||
@@ -1009,24 +1041,24 @@ void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisp
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
|
||||
char tempBuf[24];
|
||||
snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code);
|
||||
char tempBuf[40];
|
||||
snprintf(tempBuf, sizeof(tempBuf), "%s #%d", meshtastic::l10n::getString(meshtastic::l10n::StringKey::CriticalFault), error_code);
|
||||
display->drawString(0 + x, 0 + y, tempBuf);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org");
|
||||
display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, meshtastic::l10n::getString(meshtastic::l10n::StringKey::ForHelpVisit));
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(64 + x, y, "Updating");
|
||||
display->drawString(64 + x, y, meshtastic::l10n::getString(meshtastic::l10n::StringKey::Updating));
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(),
|
||||
"Please be patient and do not power off.");
|
||||
meshtastic::l10n::getString(meshtastic::l10n::StringKey::PleaseBePatient));
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
|
||||
@@ -719,7 +719,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
|
||||
defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
|
||||
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
@@ -1304,13 +1304,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1,
|
||||
false);
|
||||
}
|
||||
#ifdef SHOW_STEP_COUNTER
|
||||
std::string stepsLine = "Steps: " + std::to_string(screen->steps);
|
||||
textWidth = UIRenderer::measureStringWithEmotes(display, stepsLine.c_str());
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], stepsLine.c_str(), FONT_HEIGHT_SMALL, 1,
|
||||
false);
|
||||
#endif
|
||||
#endif
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Chinese font data (VLW format) for OLEDDisplay-based TFT rendering via LovyanGFX.
|
||||
//
|
||||
// HOW TO GENERATE FONT DATA:
|
||||
//
|
||||
// Option 1: Adafruit GFX fontconvert (simpler, fewer glyphs)
|
||||
// brew install adafruitnrf52-gfx-fontconvert # macOS
|
||||
// fontconvert NotoSansSC-Regular.ttf 16 > zh_cn_16.h
|
||||
// The output is a GFXfont struct. LovyanGFX can use this via ft_gfx type.
|
||||
//
|
||||
// Option 2: LovyanGFX VLW format (recommended, more features)
|
||||
// Use the LovyanGFX font converter script to generate a .vlw file from TTF
|
||||
// Then embed the .vlw binary as a byte array using xxd:
|
||||
// xxd -i zh_font_16.vlw > zh_font_16_data.h
|
||||
//
|
||||
// Option 3: Use BDF fonts (free, text-based)
|
||||
// WenQuanYi bitmap fonts: http://wenq.org/
|
||||
// Convert BDF to VLW using LovyanGFX tools
|
||||
//
|
||||
// FONT SIZE GUIDELINES:
|
||||
// - 16px: Minimum readable Chinese on 320x240 TFT (~20 chars per line)
|
||||
// - 20px: Better readability, ~16 chars per line
|
||||
// - 24px: Title/header text, ~13 chars per line
|
||||
//
|
||||
// CHARACTER SET RECOMMENDATIONS:
|
||||
// - GB2312 Level 1: 3755 commonly used simplified Chinese characters
|
||||
// - Plus ASCII 0x20-0x7E for punctuation and Latin characters
|
||||
//
|
||||
// PLACEHOLDER DATA - Replace with generated font binary:
|
||||
|
||||
#include "OLEDDisplayFontsZH.h"
|
||||
|
||||
// Placeholder font data.
|
||||
// Replace with actual generated VLW/Gfx font binary from a Chinese TTF file.
|
||||
// The array below is intentionally minimal - it won't render any characters
|
||||
// until real font data is provided.
|
||||
const uint8_t FontZH_CN_16[] = {
|
||||
// VLW font file magic header would go here
|
||||
// Format: 0x00 0x00 ...
|
||||
// Replace with output from: xxd -i your_chinese_font.vlw
|
||||
};
|
||||
const uint32_t FontZH_CN_16_size = sizeof(FontZH_CN_16);
|
||||
|
||||
const uint8_t FontZH_TW_16[] = {
|
||||
// Traditional Chinese VLW font data placeholder
|
||||
};
|
||||
const uint32_t FontZH_TW_16_size = sizeof(FontZH_TW_16);
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Chinese Simplified font - VLW format (to be generated from TTF)
|
||||
//
|
||||
// HOW TO GENERATE:
|
||||
// 1. Download NotoSansSC-Regular.ttf (or any open-source Chinese font)
|
||||
// 2. Use Adafruit fontconvert tool:
|
||||
// ./fontconvert NotoSansSC-Regular.ttf 16 > zh_cn_16_pt.h
|
||||
// 3. Copy the resulting binary data (GFXfont struct bytes) into the array below
|
||||
//
|
||||
// For LovyanGFX VLW font format, use:
|
||||
// - The binary VLW file can be embedded as a const uint8_t[] array
|
||||
// - Loaded with tft->loadFont(zh_font_data_array)
|
||||
|
||||
extern const uint8_t FontZH_CN_16[];
|
||||
extern const uint32_t FontZH_CN_16_size;
|
||||
|
||||
// Chinese Traditional font (VLW format)
|
||||
extern const uint8_t FontZH_TW_16[];
|
||||
extern const uint32_t FontZH_TW_16_size;
|
||||
@@ -28,7 +28,7 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff};
|
||||
const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "Strings.h"
|
||||
#include "Strings_EN.h"
|
||||
#include "Strings_ZH.h"
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
namespace l10n
|
||||
{
|
||||
|
||||
meshtastic_Language currentDisplayLanguage = meshtastic_Language_ENGLISH;
|
||||
|
||||
const char *getString(StringKey key, meshtastic_Language lang)
|
||||
{
|
||||
if (lang == meshtastic_Language_SIMPLIFIED_CHINESE || lang == meshtastic_Language_TRADITIONAL_CHINESE)
|
||||
return getStringZH(key);
|
||||
return getStringEN(key);
|
||||
}
|
||||
|
||||
} // namespace l10n
|
||||
} // namespace meshtastic
|
||||
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
|
||||
#include "mesh/generated/meshtastic/device_ui.pb.h"
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
namespace l10n
|
||||
{
|
||||
|
||||
enum class StringKey : uint8_t {
|
||||
Back,
|
||||
OK,
|
||||
GotIt,
|
||||
WelcomeToMeshtastic,
|
||||
SwipeToNavigate,
|
||||
ClickToNavigate,
|
||||
UseSelectButton,
|
||||
DeviceRole,
|
||||
RadioPreset,
|
||||
FrequencySlot,
|
||||
LoRaRegion,
|
||||
Client,
|
||||
ClientMute,
|
||||
LostAndFound,
|
||||
Tracker,
|
||||
Hour12,
|
||||
Hour24,
|
||||
ClockAction,
|
||||
ClockFace,
|
||||
TimeFormat,
|
||||
Timezone,
|
||||
MessageAction,
|
||||
Reply,
|
||||
ViewChats,
|
||||
UnmuteChannel,
|
||||
MuteChannel,
|
||||
Delete,
|
||||
ReadAloud,
|
||||
Brightness,
|
||||
FrameVisibility,
|
||||
DisplayUnits,
|
||||
MessageBubbles,
|
||||
Theme,
|
||||
Reboot,
|
||||
Shutdown,
|
||||
SwitchToMUI,
|
||||
DisplayOptions,
|
||||
Language,
|
||||
Chinese,
|
||||
English,
|
||||
NoGPS,
|
||||
GPSoff,
|
||||
GPSnotPresent,
|
||||
GPSisDisabled,
|
||||
Node,
|
||||
Hop,
|
||||
USB,
|
||||
BT_off,
|
||||
WiFi,
|
||||
WiFiNotConnected,
|
||||
WiFiConnected,
|
||||
SSID,
|
||||
LoRaInfo,
|
||||
LoRa,
|
||||
Role,
|
||||
Freq,
|
||||
ChUtil,
|
||||
System,
|
||||
Heap,
|
||||
PSRAM,
|
||||
Flash,
|
||||
SD,
|
||||
Ver,
|
||||
Up,
|
||||
ClientApp,
|
||||
NoConnected,
|
||||
LOCKED,
|
||||
ConnectToUnlock,
|
||||
Battery,
|
||||
Resuming,
|
||||
LastHeard,
|
||||
HopsSig,
|
||||
HopsSignal,
|
||||
Nodes,
|
||||
Distance,
|
||||
Bearings,
|
||||
CreatingSSLCert,
|
||||
PleaseWait,
|
||||
CriticalFault,
|
||||
ForHelpVisit,
|
||||
Updating,
|
||||
PleaseBePatient,
|
||||
RebootInProgress,
|
||||
PowerMenu,
|
||||
SystemMenu,
|
||||
ScreenOptions,
|
||||
WifiToggle,
|
||||
BluetoothToggle,
|
||||
NodeDbReset,
|
||||
ManageNode,
|
||||
RemoveFavorite,
|
||||
AddFavorite,
|
||||
TestMenu,
|
||||
TraceRoute,
|
||||
ThrottleMessage,
|
||||
MessageViewMode,
|
||||
DeleteMessages,
|
||||
NodeNameLength,
|
||||
GPSMenu,
|
||||
GPSToggle,
|
||||
GPSFormat,
|
||||
GPSSmartPosition,
|
||||
GPSUpdateInterval,
|
||||
GPSPositionBroadcast,
|
||||
CompassNorth,
|
||||
BuzzerMode,
|
||||
HamModeConfirm,
|
||||
LicensedToNormal,
|
||||
KeyVerification,
|
||||
FinalPrompt,
|
||||
RegionUnset,
|
||||
SelectRegionMessage,
|
||||
Rebooting,
|
||||
LoRaActions,
|
||||
Slot0,
|
||||
WithPreset,
|
||||
WithFreetext,
|
||||
DeleteOldest,
|
||||
DeleteThisChat,
|
||||
DeleteAll,
|
||||
DeleteAllChats,
|
||||
TemporarilyMute,
|
||||
Unmute,
|
||||
ToggleBacklight,
|
||||
SleepScreen,
|
||||
SendPosition,
|
||||
SendNodeInfo,
|
||||
NewPresetMsg,
|
||||
NewFreetextMsg,
|
||||
Notifications,
|
||||
Power,
|
||||
RebootShutdown,
|
||||
GoToChat,
|
||||
NewPreset,
|
||||
Favorite,
|
||||
UnmuteNotifications,
|
||||
MuteNotifications,
|
||||
UnignoreNode,
|
||||
IgnoreNode,
|
||||
NumberPicker,
|
||||
TestAnnounce,
|
||||
NodeActionsSettings,
|
||||
ShowNameLength,
|
||||
Bluetooth,
|
||||
NoMessages,
|
||||
COUNT
|
||||
};
|
||||
|
||||
extern meshtastic_Language currentDisplayLanguage;
|
||||
|
||||
const char *getString(StringKey key, meshtastic_Language lang);
|
||||
|
||||
inline const char *getString(StringKey key)
|
||||
{
|
||||
extern meshtastic_Language currentDisplayLanguage;
|
||||
return getString(key, currentDisplayLanguage);
|
||||
}
|
||||
|
||||
} // namespace l10n
|
||||
} // namespace meshtastic
|
||||
@@ -0,0 +1,456 @@
|
||||
#pragma once
|
||||
|
||||
#include "Strings.h"
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
namespace l10n
|
||||
{
|
||||
|
||||
// English string table
|
||||
// The strings are stored as const arrays to save RAM on constrained devices.
|
||||
// Each locale file defines the same set of strings in its own language.
|
||||
|
||||
static const char STR_BACK_EN[] = "Back";
|
||||
static const char STR_OK_EN[] = "OK";
|
||||
static const char STR_GOT_IT_EN[] = "Got it!";
|
||||
static const char STR_WELCOME_TO_MESHTASTIC_EN[] = "Welcome to Meshtastic!";
|
||||
static const char STR_SWIPE_TO_NAVIGATE_EN[] = "Swipe to navigate.";
|
||||
static const char STR_CLICK_TO_NAVIGATE_EN[] = "Click to navigate.";
|
||||
static const char STR_USE_SELECT_BUTTON_EN[] = "Use the Select button.";
|
||||
static const char STR_DEVICE_ROLE_EN[] = "Device Role";
|
||||
static const char STR_RADIO_PRESET_EN[] = "Radio Preset";
|
||||
static const char STR_FREQUENCY_SLOT_EN[] = "Frequency Slot";
|
||||
static const char STR_LORA_REGION_EN[] = "LoRa Region";
|
||||
static const char STR_CLIENT_EN[] = "Client";
|
||||
static const char STR_CLIENT_MUTE_EN[] = "Client Mute";
|
||||
static const char STR_LOST_AND_FOUND_EN[] = "Lost and Found";
|
||||
static const char STR_TRACKER_EN[] = "Tracker";
|
||||
static const char STR_12HOUR_EN[] = "12-hour";
|
||||
static const char STR_24HOUR_EN[] = "24-hour";
|
||||
static const char STR_CLOCK_ACTION_EN[] = "Clock Action";
|
||||
static const char STR_CLOCK_FACE_EN[] = "Clock Face";
|
||||
static const char STR_TIME_FORMAT_EN[] = "Time Format";
|
||||
static const char STR_TIMEZONE_EN[] = "Timezone";
|
||||
static const char STR_MESSAGE_ACTION_EN[] = "Message Action";
|
||||
static const char STR_REPLY_EN[] = "Reply";
|
||||
static const char STR_VIEW_CHATS_EN[] = "View Chats";
|
||||
static const char STR_UNMUTE_CHANNEL_EN[] = "Unmute Channel";
|
||||
static const char STR_MUTE_CHANNEL_EN[] = "Mute Channel";
|
||||
static const char STR_DELETE_EN[] = "Delete";
|
||||
static const char STR_READ_ALOUD_EN[] = "Read Aloud";
|
||||
static const char STR_BRIGHTNESS_EN[] = "Brightness";
|
||||
static const char STR_FRAME_VISIBILITY_EN[] = "Frame Visibility";
|
||||
static const char STR_DISPLAY_UNITS_EN[] = "Display Units";
|
||||
static const char STR_MESSAGE_BUBBLES_EN[] = "Message Bubbles";
|
||||
static const char STR_THEME_EN[] = "Theme";
|
||||
static const char STR_REBOOT_EN[] = "Reboot";
|
||||
static const char STR_SHUTDOWN_EN[] = "Shutdown";
|
||||
static const char STR_SWITCH_TO_MUI_EN[] = "Switch to MUI";
|
||||
static const char STR_DISPLAY_OPTIONS_EN[] = "Display Options";
|
||||
static const char STR_LANGUAGE_EN[] = "Language";
|
||||
static const char STR_CHINESE_EN[] = "Chinese";
|
||||
static const char STR_ENGLISH_EN[] = "English";
|
||||
static const char STR_NO_GPS_EN[] = "No GPS";
|
||||
static const char STR_GPS_OFF_EN[] = "GPS off";
|
||||
static const char STR_GPS_NOT_PRESENT_EN[] = "GPS not present";
|
||||
static const char STR_GPS_IS_DISABLED_EN[] = "GPS is disabled";
|
||||
static const char STR_NODE_EN[] = "Node";
|
||||
static const char STR_HOP_EN[] = "Hop:";
|
||||
static const char STR_USB_EN[] = "USB";
|
||||
static const char STR_BT_OFF_EN[] = "BT off";
|
||||
static const char STR_WIFI_EN[] = "WiFi";
|
||||
static const char STR_WIFI_NOT_CONNECTED_EN[] = "WiFi: Not Connected";
|
||||
static const char STR_WIFI_CONNECTED_EN[] = "WiFi: Connected";
|
||||
static const char STR_SSID_EN[] = "SSID:";
|
||||
static const char STR_LORA_INFO_EN[] = "LoRa Info";
|
||||
static const char STR_LORA_EN[] = "LoRa";
|
||||
static const char STR_ROLE_EN[] = "Role:";
|
||||
static const char STR_FREQ_EN[] = "Freq:";
|
||||
static const char STR_CHUTIL_EN[] = "ChUtil";
|
||||
static const char STR_SYSTEM_EN[] = "System";
|
||||
static const char STR_HEAP_EN[] = "Heap:";
|
||||
static const char STR_PSRAM_EN[] = "PSRAM:";
|
||||
static const char STR_FLASH_EN[] = "Flash:";
|
||||
static const char STR_SD_EN[] = "SD:";
|
||||
static const char STR_VER_EN[] = "Ver:";
|
||||
static const char STR_UP_EN[] = "Up:";
|
||||
static const char STR_CLIENT_APP_EN[] = "Client";
|
||||
static const char STR_NO_CONNECTED_EN[] = "No Connected";
|
||||
static const char STR_LOCKED_EN[] = "LOCKED";
|
||||
static const char STR_CONNECT_TO_UNLOCK_EN[] = "Connect to unlock";
|
||||
static const char STR_BATTERY_EN[] = "Battery";
|
||||
static const char STR_RESUMING_EN[] = "Resuming...";
|
||||
static const char STR_LAST_HEARD_EN[] = "Last Heard";
|
||||
static const char STR_HOPS_SIG_EN[] = "Hops/Sig";
|
||||
static const char STR_HOPS_SIGNAL_EN[] = "Hops/Signal";
|
||||
static const char STR_NODES_EN[] = "Nodes";
|
||||
static const char STR_DISTANCE_EN[] = "Distance";
|
||||
static const char STR_BEARINGS_EN[] = "Bearings";
|
||||
static const char STR_CREATING_SSL_EN[] = "Creating SSL certificate";
|
||||
static const char STR_PLEASE_WAIT_EN[] = "Please wait . . .";
|
||||
static const char STR_CRITICAL_FAULT_EN[] = "Critical fault";
|
||||
static const char STR_FOR_HELP_VISIT_EN[] = "For help, please visit";
|
||||
static const char STR_UPDATING_EN[] = "Updating";
|
||||
static const char STR_PLEASE_BE_PATIENT_EN[] = "Please be patient and do not power off.";
|
||||
static const char STR_REBOOT_IN_PROGRESS_EN[] = "Reboot in progress...";
|
||||
static const char STR_POWER_MENU_EN[] = "Power";
|
||||
static const char STR_SYSTEM_MENU_EN[] = "System";
|
||||
static const char STR_SCREEN_OPTIONS_EN[] = "Screen";
|
||||
static const char STR_WIFI_TOGGLE_EN[] = "WiFi";
|
||||
static const char STR_BLUETOOTH_TOGGLE_EN[] = "Bluetooth";
|
||||
static const char STR_NODE_DB_RESET_EN[] = "Reset NodeDB";
|
||||
static const char STR_MANAGE_NODE_EN[] = "Manage Node";
|
||||
static const char STR_REMOVE_FAVORITE_EN[] = "Remove Favorite";
|
||||
static const char STR_ADD_FAVORITE_EN[] = "Add Favorite";
|
||||
static const char STR_TEST_MENU_EN[] = "Test";
|
||||
static const char STR_TRACE_ROUTE_EN[] = "Trace Route";
|
||||
static const char STR_THROTTLE_MSG_EN[] = "Message Throttled";
|
||||
static const char STR_MSG_VIEW_MODE_EN[] = "View Mode";
|
||||
static const char STR_DELETE_MESSAGES_EN[] = "Delete Messages";
|
||||
static const char STR_NODE_NAME_LENGTH_EN[] = "Name Length";
|
||||
static const char STR_GPS_MENU_EN[] = "GPS";
|
||||
static const char STR_GPS_TOGGLE_EN[] = "GPS Toggle";
|
||||
static const char STR_GPS_FORMAT_EN[] = "GPS Format";
|
||||
static const char STR_GPS_SMART_POS_EN[] = "Smart Position";
|
||||
static const char STR_GPS_UPDATE_INTERVAL_EN[] = "Update Interval";
|
||||
static const char STR_GPS_POS_BCAST_EN[] = "Position Broadcast";
|
||||
static const char STR_COMPASS_NORTH_EN[] = "Compass North";
|
||||
static const char STR_BUZZER_MODE_EN[] = "Buzzer";
|
||||
static const char STR_HAM_MODE_EN[] = "Ham Mode";
|
||||
static const char STR_LICENSED_NORMAL_EN[] = "Normal Mode";
|
||||
static const char STR_KEY_VERIFY_EN[] = "Verify Key";
|
||||
static const char STR_REGION_UNSET_EN[] = "Select a Region";
|
||||
static const char STR_SELECT_REGION_EN[] = "Please select your LoRa region:";
|
||||
static const char STR_REBOOTING_EN[] = "Rebooting...";
|
||||
static const char STR_LORA_ACTIONS_EN[] = "LoRa Actions";
|
||||
static const char STR_SLOT0_EN[] = "Slot 0 (Auto)";
|
||||
static const char STR_WITH_PRESET_EN[] = "With Preset";
|
||||
static const char STR_WITH_FREETEXT_EN[] = "With Freetext";
|
||||
static const char STR_DELETE_OLDEST_EN[] = "Delete Oldest";
|
||||
static const char STR_DELETE_THIS_CHAT_EN[] = "Delete This Chat";
|
||||
static const char STR_DELETE_ALL_EN[] = "Delete All";
|
||||
static const char STR_DELETE_ALL_CHATS_EN[] = "Delete All Chats";
|
||||
static const char STR_TEMPORARILY_MUTE_EN[] = "Temporarily Mute";
|
||||
static const char STR_UNMUTE_EN[] = "Unmute";
|
||||
static const char STR_TOGGLE_BACKLIGHT_EN[] = "Toggle Backlight";
|
||||
static const char STR_SLEEP_SCREEN_EN[] = "Sleep Screen";
|
||||
static const char STR_SEND_POSITION_EN[] = "Send Position";
|
||||
static const char STR_SEND_NODE_INFO_EN[] = "Send Node Info";
|
||||
static const char STR_NEW_PRESET_MSG_EN[] = "New Preset Msg";
|
||||
static const char STR_NEW_FREETEXT_MSG_EN[] = "New Freetext Msg";
|
||||
static const char STR_NOTIFICATIONS_EN[] = "Notifications";
|
||||
static const char STR_POWER_EN[] = "Power";
|
||||
static const char STR_REBOOT_SHUTDOWN_EN[] = "Reboot/Shutdown";
|
||||
static const char STR_GO_TO_CHAT_EN[] = "Go To Chat";
|
||||
static const char STR_NEW_PRESET_EN[] = "New Preset";
|
||||
static const char STR_FAVORITE_EN[] = "Favorite";
|
||||
static const char STR_UNMUTE_NOTIFICATIONS_EN[] = "Unmute Notifications";
|
||||
static const char STR_MUTE_NOTIFICATIONS_EN[] = "Mute Notifications";
|
||||
static const char STR_UNIGNORE_NODE_EN[] = "Unignore Node";
|
||||
static const char STR_IGNORE_NODE_EN[] = "Ignore Node";
|
||||
static const char STR_NUMBER_PICKER_EN[] = "Number Picker";
|
||||
static const char STR_TEST_ANNOUNCE_EN[] = "Test Announce";
|
||||
static const char STR_NODE_ACTIONS_EN[] = "Node Actions / Settings";
|
||||
static const char STR_SHOW_NAME_LENGTH_EN[] = "Show Long/Short Name";
|
||||
static const char STR_BLUETOOTH_EN[] = "Bluetooth";
|
||||
static const char STR_NO_MESSAGES_EN[] = "No messages";
|
||||
|
||||
inline const char *getStringEN(StringKey key)
|
||||
{
|
||||
switch (key) {
|
||||
case StringKey::Back:
|
||||
return STR_BACK_EN;
|
||||
case StringKey::OK:
|
||||
return STR_OK_EN;
|
||||
case StringKey::GotIt:
|
||||
return STR_GOT_IT_EN;
|
||||
case StringKey::WelcomeToMeshtastic:
|
||||
return STR_WELCOME_TO_MESHTASTIC_EN;
|
||||
case StringKey::SwipeToNavigate:
|
||||
return STR_SWIPE_TO_NAVIGATE_EN;
|
||||
case StringKey::ClickToNavigate:
|
||||
return STR_CLICK_TO_NAVIGATE_EN;
|
||||
case StringKey::UseSelectButton:
|
||||
return STR_USE_SELECT_BUTTON_EN;
|
||||
case StringKey::DeviceRole:
|
||||
return STR_DEVICE_ROLE_EN;
|
||||
case StringKey::RadioPreset:
|
||||
return STR_RADIO_PRESET_EN;
|
||||
case StringKey::FrequencySlot:
|
||||
return STR_FREQUENCY_SLOT_EN;
|
||||
case StringKey::LoRaRegion:
|
||||
return STR_LORA_REGION_EN;
|
||||
case StringKey::Client:
|
||||
return STR_CLIENT_EN;
|
||||
case StringKey::ClientMute:
|
||||
return STR_CLIENT_MUTE_EN;
|
||||
case StringKey::LostAndFound:
|
||||
return STR_LOST_AND_FOUND_EN;
|
||||
case StringKey::Tracker:
|
||||
return STR_TRACKER_EN;
|
||||
case StringKey::Hour12:
|
||||
return STR_12HOUR_EN;
|
||||
case StringKey::Hour24:
|
||||
return STR_24HOUR_EN;
|
||||
case StringKey::ClockAction:
|
||||
return STR_CLOCK_ACTION_EN;
|
||||
case StringKey::ClockFace:
|
||||
return STR_CLOCK_FACE_EN;
|
||||
case StringKey::TimeFormat:
|
||||
return STR_TIME_FORMAT_EN;
|
||||
case StringKey::Timezone:
|
||||
return STR_TIMEZONE_EN;
|
||||
case StringKey::MessageAction:
|
||||
return STR_MESSAGE_ACTION_EN;
|
||||
case StringKey::Reply:
|
||||
return STR_REPLY_EN;
|
||||
case StringKey::ViewChats:
|
||||
return STR_VIEW_CHATS_EN;
|
||||
case StringKey::UnmuteChannel:
|
||||
return STR_UNMUTE_CHANNEL_EN;
|
||||
case StringKey::MuteChannel:
|
||||
return STR_MUTE_CHANNEL_EN;
|
||||
case StringKey::Delete:
|
||||
return STR_DELETE_EN;
|
||||
case StringKey::ReadAloud:
|
||||
return STR_READ_ALOUD_EN;
|
||||
case StringKey::Brightness:
|
||||
return STR_BRIGHTNESS_EN;
|
||||
case StringKey::FrameVisibility:
|
||||
return STR_FRAME_VISIBILITY_EN;
|
||||
case StringKey::DisplayUnits:
|
||||
return STR_DISPLAY_UNITS_EN;
|
||||
case StringKey::MessageBubbles:
|
||||
return STR_MESSAGE_BUBBLES_EN;
|
||||
case StringKey::Theme:
|
||||
return STR_THEME_EN;
|
||||
case StringKey::Reboot:
|
||||
return STR_REBOOT_EN;
|
||||
case StringKey::Shutdown:
|
||||
return STR_SHUTDOWN_EN;
|
||||
case StringKey::SwitchToMUI:
|
||||
return STR_SWITCH_TO_MUI_EN;
|
||||
case StringKey::DisplayOptions:
|
||||
return STR_DISPLAY_OPTIONS_EN;
|
||||
case StringKey::Language:
|
||||
return STR_LANGUAGE_EN;
|
||||
case StringKey::Chinese:
|
||||
return STR_CHINESE_EN;
|
||||
case StringKey::English:
|
||||
return STR_ENGLISH_EN;
|
||||
case StringKey::NoGPS:
|
||||
return STR_NO_GPS_EN;
|
||||
case StringKey::GPSoff:
|
||||
return STR_GPS_OFF_EN;
|
||||
case StringKey::GPSnotPresent:
|
||||
return STR_GPS_NOT_PRESENT_EN;
|
||||
case StringKey::GPSisDisabled:
|
||||
return STR_GPS_IS_DISABLED_EN;
|
||||
case StringKey::Node:
|
||||
return STR_NODE_EN;
|
||||
case StringKey::Hop:
|
||||
return STR_HOP_EN;
|
||||
case StringKey::USB:
|
||||
return STR_USB_EN;
|
||||
case StringKey::BT_off:
|
||||
return STR_BT_OFF_EN;
|
||||
case StringKey::WiFi:
|
||||
return STR_WIFI_EN;
|
||||
case StringKey::WiFiNotConnected:
|
||||
return STR_WIFI_NOT_CONNECTED_EN;
|
||||
case StringKey::WiFiConnected:
|
||||
return STR_WIFI_CONNECTED_EN;
|
||||
case StringKey::SSID:
|
||||
return STR_SSID_EN;
|
||||
case StringKey::LoRaInfo:
|
||||
return STR_LORA_INFO_EN;
|
||||
case StringKey::LoRa:
|
||||
return STR_LORA_EN;
|
||||
case StringKey::Role:
|
||||
return STR_ROLE_EN;
|
||||
case StringKey::Freq:
|
||||
return STR_FREQ_EN;
|
||||
case StringKey::ChUtil:
|
||||
return STR_CHUTIL_EN;
|
||||
case StringKey::System:
|
||||
return STR_SYSTEM_EN;
|
||||
case StringKey::Heap:
|
||||
return STR_HEAP_EN;
|
||||
case StringKey::PSRAM:
|
||||
return STR_PSRAM_EN;
|
||||
case StringKey::Flash:
|
||||
return STR_FLASH_EN;
|
||||
case StringKey::SD:
|
||||
return STR_SD_EN;
|
||||
case StringKey::Ver:
|
||||
return STR_VER_EN;
|
||||
case StringKey::Up:
|
||||
return STR_UP_EN;
|
||||
case StringKey::ClientApp:
|
||||
return STR_CLIENT_APP_EN;
|
||||
case StringKey::NoConnected:
|
||||
return STR_NO_CONNECTED_EN;
|
||||
case StringKey::LOCKED:
|
||||
return STR_LOCKED_EN;
|
||||
case StringKey::ConnectToUnlock:
|
||||
return STR_CONNECT_TO_UNLOCK_EN;
|
||||
case StringKey::Battery:
|
||||
return STR_BATTERY_EN;
|
||||
case StringKey::Resuming:
|
||||
return STR_RESUMING_EN;
|
||||
case StringKey::LastHeard:
|
||||
return STR_LAST_HEARD_EN;
|
||||
case StringKey::HopsSig:
|
||||
return STR_HOPS_SIG_EN;
|
||||
case StringKey::HopsSignal:
|
||||
return STR_HOPS_SIGNAL_EN;
|
||||
case StringKey::Nodes:
|
||||
return STR_NODES_EN;
|
||||
case StringKey::Distance:
|
||||
return STR_DISTANCE_EN;
|
||||
case StringKey::Bearings:
|
||||
return STR_BEARINGS_EN;
|
||||
case StringKey::CreatingSSLCert:
|
||||
return STR_CREATING_SSL_EN;
|
||||
case StringKey::PleaseWait:
|
||||
return STR_PLEASE_WAIT_EN;
|
||||
case StringKey::CriticalFault:
|
||||
return STR_CRITICAL_FAULT_EN;
|
||||
case StringKey::ForHelpVisit:
|
||||
return STR_FOR_HELP_VISIT_EN;
|
||||
case StringKey::Updating:
|
||||
return STR_UPDATING_EN;
|
||||
case StringKey::PleaseBePatient:
|
||||
return STR_PLEASE_BE_PATIENT_EN;
|
||||
case StringKey::RebootInProgress:
|
||||
return STR_REBOOT_IN_PROGRESS_EN;
|
||||
case StringKey::PowerMenu:
|
||||
return STR_POWER_MENU_EN;
|
||||
case StringKey::SystemMenu:
|
||||
return STR_SYSTEM_MENU_EN;
|
||||
case StringKey::ScreenOptions:
|
||||
return STR_SCREEN_OPTIONS_EN;
|
||||
case StringKey::WifiToggle:
|
||||
return STR_WIFI_TOGGLE_EN;
|
||||
case StringKey::BluetoothToggle:
|
||||
return STR_BLUETOOTH_TOGGLE_EN;
|
||||
case StringKey::NodeDbReset:
|
||||
return STR_NODE_DB_RESET_EN;
|
||||
case StringKey::ManageNode:
|
||||
return STR_MANAGE_NODE_EN;
|
||||
case StringKey::RemoveFavorite:
|
||||
return STR_REMOVE_FAVORITE_EN;
|
||||
case StringKey::AddFavorite:
|
||||
return STR_ADD_FAVORITE_EN;
|
||||
case StringKey::TestMenu:
|
||||
return STR_TEST_MENU_EN;
|
||||
case StringKey::TraceRoute:
|
||||
return STR_TRACE_ROUTE_EN;
|
||||
case StringKey::ThrottleMessage:
|
||||
return STR_THROTTLE_MSG_EN;
|
||||
case StringKey::MessageViewMode:
|
||||
return STR_MSG_VIEW_MODE_EN;
|
||||
case StringKey::DeleteMessages:
|
||||
return STR_DELETE_MESSAGES_EN;
|
||||
case StringKey::NodeNameLength:
|
||||
return STR_NODE_NAME_LENGTH_EN;
|
||||
case StringKey::GPSMenu:
|
||||
return STR_GPS_MENU_EN;
|
||||
case StringKey::GPSToggle:
|
||||
return STR_GPS_TOGGLE_EN;
|
||||
case StringKey::GPSFormat:
|
||||
return STR_GPS_FORMAT_EN;
|
||||
case StringKey::GPSSmartPosition:
|
||||
return STR_GPS_SMART_POS_EN;
|
||||
case StringKey::GPSUpdateInterval:
|
||||
return STR_GPS_UPDATE_INTERVAL_EN;
|
||||
case StringKey::GPSPositionBroadcast:
|
||||
return STR_GPS_POS_BCAST_EN;
|
||||
case StringKey::CompassNorth:
|
||||
return STR_COMPASS_NORTH_EN;
|
||||
case StringKey::BuzzerMode:
|
||||
return STR_BUZZER_MODE_EN;
|
||||
case StringKey::HamModeConfirm:
|
||||
return STR_HAM_MODE_EN;
|
||||
case StringKey::LicensedToNormal:
|
||||
return STR_LICENSED_NORMAL_EN;
|
||||
case StringKey::KeyVerification:
|
||||
return STR_KEY_VERIFY_EN;
|
||||
case StringKey::RegionUnset:
|
||||
return STR_REGION_UNSET_EN;
|
||||
case StringKey::SelectRegionMessage:
|
||||
return STR_SELECT_REGION_EN;
|
||||
case StringKey::Rebooting:
|
||||
return STR_REBOOTING_EN;
|
||||
case StringKey::LoRaActions:
|
||||
return STR_LORA_ACTIONS_EN;
|
||||
case StringKey::Slot0:
|
||||
return STR_SLOT0_EN;
|
||||
case StringKey::WithPreset:
|
||||
return STR_WITH_PRESET_EN;
|
||||
case StringKey::WithFreetext:
|
||||
return STR_WITH_FREETEXT_EN;
|
||||
case StringKey::DeleteOldest:
|
||||
return STR_DELETE_OLDEST_EN;
|
||||
case StringKey::DeleteThisChat:
|
||||
return STR_DELETE_THIS_CHAT_EN;
|
||||
case StringKey::DeleteAll:
|
||||
return STR_DELETE_ALL_EN;
|
||||
case StringKey::DeleteAllChats:
|
||||
return STR_DELETE_ALL_CHATS_EN;
|
||||
case StringKey::TemporarilyMute:
|
||||
return STR_TEMPORARILY_MUTE_EN;
|
||||
case StringKey::Unmute:
|
||||
return STR_UNMUTE_EN;
|
||||
case StringKey::ToggleBacklight:
|
||||
return STR_TOGGLE_BACKLIGHT_EN;
|
||||
case StringKey::SleepScreen:
|
||||
return STR_SLEEP_SCREEN_EN;
|
||||
case StringKey::SendPosition:
|
||||
return STR_SEND_POSITION_EN;
|
||||
case StringKey::SendNodeInfo:
|
||||
return STR_SEND_NODE_INFO_EN;
|
||||
case StringKey::NewPresetMsg:
|
||||
return STR_NEW_PRESET_MSG_EN;
|
||||
case StringKey::NewFreetextMsg:
|
||||
return STR_NEW_FREETEXT_MSG_EN;
|
||||
case StringKey::Notifications:
|
||||
return STR_NOTIFICATIONS_EN;
|
||||
case StringKey::Power:
|
||||
return STR_POWER_EN;
|
||||
case StringKey::RebootShutdown:
|
||||
return STR_REBOOT_SHUTDOWN_EN;
|
||||
case StringKey::GoToChat:
|
||||
return STR_GO_TO_CHAT_EN;
|
||||
case StringKey::NewPreset:
|
||||
return STR_NEW_PRESET_EN;
|
||||
case StringKey::Favorite:
|
||||
return STR_FAVORITE_EN;
|
||||
case StringKey::UnmuteNotifications:
|
||||
return STR_UNMUTE_NOTIFICATIONS_EN;
|
||||
case StringKey::MuteNotifications:
|
||||
return STR_MUTE_NOTIFICATIONS_EN;
|
||||
case StringKey::UnignoreNode:
|
||||
return STR_UNIGNORE_NODE_EN;
|
||||
case StringKey::IgnoreNode:
|
||||
return STR_IGNORE_NODE_EN;
|
||||
case StringKey::NumberPicker:
|
||||
return STR_NUMBER_PICKER_EN;
|
||||
case StringKey::TestAnnounce:
|
||||
return STR_TEST_ANNOUNCE_EN;
|
||||
case StringKey::NodeActionsSettings:
|
||||
return STR_NODE_ACTIONS_EN;
|
||||
case StringKey::ShowNameLength:
|
||||
return STR_SHOW_NAME_LENGTH_EN;
|
||||
case StringKey::Bluetooth:
|
||||
return STR_BLUETOOTH_EN;
|
||||
case StringKey::NoMessages:
|
||||
return STR_NO_MESSAGES_EN;
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace l10n
|
||||
} // namespace meshtastic
|
||||
@@ -0,0 +1,452 @@
|
||||
#pragma once
|
||||
|
||||
#include "Strings.h"
|
||||
|
||||
namespace meshtastic
|
||||
{
|
||||
namespace l10n
|
||||
{
|
||||
|
||||
static const char STR_BACK_ZH[] = "\xe8\xbf\x94\xe5\x9b\x9e";
|
||||
static const char STR_OK_ZH[] = "\xe7\xa1\xae\xe5\xae\x9a";
|
||||
static const char STR_GOT_IT_ZH[] = "\xe7\x9f\xa5\xe9\x81\x93\xe4\xba\x86";
|
||||
static const char STR_WELCOME_TO_MESHTASTIC_ZH[] = "\xe6\xac\xa2\xe8\xbf\x8e\xe4\xbd\xbf\xe7\x94\xa8 Meshtastic\xef\xbc\x81";
|
||||
static const char STR_SWIPE_TO_NAVIGATE_ZH[] = "\xe6\xbb\x91\xe5\x8a\xa8\xe4\xbb\xa5\xe5\xaf\xbc\xe8\x88\xaa\xe3\x80\x82";
|
||||
static const char STR_CLICK_TO_NAVIGATE_ZH[] = "\xe7\x82\xb9\xe5\x87\xbb\xe4\xbb\xa5\xe5\xaf\xbc\xe8\x88\xaa\xe3\x80\x82";
|
||||
static const char STR_USE_SELECT_BUTTON_ZH[] = "\xe4\xbd\xbf\xe7\x94\xa8\xe9\x80\x89\xe6\x8b\xa9\xe6\x8c\x89\xe9\x92\xae\xe3\x80\x82";
|
||||
static const char STR_DEVICE_ROLE_ZH[] = "\xe8\xae\xbe\xe5\xa4\x87\xe8\xa7\x92\xe8\x89\xb2";
|
||||
static const char STR_RADIO_PRESET_ZH[] = "\xe6\x97\xa0\xe7\xba\xbf\xe9\xa2\x84\xe8\xae\xbe";
|
||||
static const char STR_FREQUENCY_SLOT_ZH[] = "\xe9\xa2\x91\xe7\x8e\x87\xe6\xa7\xbd\xe4\xbd\x8d";
|
||||
static const char STR_LORA_REGION_ZH[] = "LoRa \xe5\x9c\xb0\xe5\x8c\xba";
|
||||
static const char STR_CLIENT_ZH[] = "\xe5\xae\xa2\xe6\x88\xb7\xe7\xab\xaf";
|
||||
static const char STR_CLIENT_MUTE_ZH[] = "\xe9\x9d\x99\xe9\x9f\xb3\xe5\xae\xa2\xe6\x88\xb7\xe7\xab\xaf";
|
||||
static const char STR_LOST_AND_FOUND_ZH[] = "\xe5\xa4\xb1\xe7\x89\xa9\xe6\x8b\x9b\xe9\xa2\x86";
|
||||
static const char STR_TRACKER_ZH[] = "\xe8\xbf\xbd\xe8\xb8\xaa\xe5\x99\xa8";
|
||||
static const char STR_12HOUR_ZH[] = "12 \xe5\xb0\x8f\xe6\x97\xb6\xe5\x88\xb6";
|
||||
static const char STR_24HOUR_ZH[] = "24 \xe5\xb0\x8f\xe6\x97\xb6\xe5\x88\xb6";
|
||||
static const char STR_CLOCK_ACTION_ZH[] = "\xe6\x97\xb6\xe9\x92\x9f\xe6\x93\x8d\xe4\xbd\x9c";
|
||||
static const char STR_CLOCK_FACE_ZH[] = "\xe6\x97\xb6\xe9\x92\x9f\xe6\xa0\xb7\xe5\xbc\x8f";
|
||||
static const char STR_TIME_FORMAT_ZH[] = "\xe6\x97\xb6\xe9\x97\xb4\xe6\xa0\xbc\xe5\xbc\x8f";
|
||||
static const char STR_TIMEZONE_ZH[] = "\xe6\x97\xb6\xe5\x8c\xba";
|
||||
static const char STR_MESSAGE_ACTION_ZH[] = "\xe6\xb6\x88\xe6\x81\xaf\xe6\x93\x8d\xe4\xbd\x9c";
|
||||
static const char STR_REPLY_ZH[] = "\xe5\x9b\x9e\xe5\xa4\x8d";
|
||||
static const char STR_VIEW_CHATS_ZH[] = "\xe6\x9f\xa5\xe7\x9c\x8b\xe8\x81\x8a\xe5\xa4\xa9";
|
||||
static const char STR_UNMUTE_CHANNEL_ZH[] = "\xe5\x8f\x96\xe6\xb6\x88\xe9\x9d\x99\xe9\x9f\xb3";
|
||||
static const char STR_MUTE_CHANNEL_ZH[] = "\xe9\x9d\x99\xe9\x9f\xb3\xe9\xa2\x91\xe9\x81\x93";
|
||||
static const char STR_DELETE_ZH[] = "\xe5\x88\xa0\xe9\x99\xa4";
|
||||
static const char STR_READ_ALOUD_ZH[] = "\xe5\xa4\xa7\xe5\xa3\xb0\xe6\x9c\x97\xe8\xaf\xbb";
|
||||
static const char STR_BRIGHTNESS_ZH[] = "\xe4\xba\xae\xe5\xba\xa6";
|
||||
static const char STR_FRAME_VISIBILITY_ZH[] = "\xe9\xa1\xb5\xe9\x9d\xa2\xe5\x8f\xaf\xe8\xa7\x81\xe6\x80\xa7";
|
||||
static const char STR_DISPLAY_UNITS_ZH[] = "\xe6\x98\xbe\xe7\xa4\xba\xe5\x8d\x95\xe4\xbd\x8d";
|
||||
static const char STR_MESSAGE_BUBBLES_ZH[] = "\xe6\xb6\x88\xe6\x81\xaf\xe6\xb0\x94\xe6\xb3\xa1";
|
||||
static const char STR_THEME_ZH[] = "\xe4\xb8\xbb\xe9\xa2\x98";
|
||||
static const char STR_REBOOT_ZH[] = "\xe9\x87\x8d\xe5\x90\xaf";
|
||||
static const char STR_SHUTDOWN_ZH[] = "\xe5\x85\xb3\xe6\x9c\xba";
|
||||
static const char STR_SWITCH_TO_MUI_ZH[] = "\xe5\x88\x87\xe6\x8d\xa2\xe5\x88\xb0 MUI";
|
||||
static const char STR_DISPLAY_OPTIONS_ZH[] = "\xe6\x98\xbe\xe7\xa4\xba\xe9\x80\x89\xe9\xa1\xb9";
|
||||
static const char STR_LANGUAGE_ZH[] = "\xe8\xaf\xad\xe8\xa8\x80";
|
||||
static const char STR_CHINESE_ZH[] = "\xe4\xb8\xad\xe6\x96\x87";
|
||||
static const char STR_ENGLISH_ZH[] = "\xe8\x8b\xb1\xe6\x96\x87";
|
||||
static const char STR_NO_GPS_ZH[] = "\xe6\x97\xa0 GPS";
|
||||
static const char STR_GPS_OFF_ZH[] = "GPS \xe5\xb7\xb2\xe5\x85\xb3\xe9\x97\xad";
|
||||
static const char STR_GPS_NOT_PRESENT_ZH[] = "GPS \xe4\xb8\x8d\xe5\xad\x98\xe5\x9c\xa8";
|
||||
static const char STR_GPS_IS_DISABLED_ZH[] = "GPS \xe5\xb7\xb2\xe7\xa6\x81\xe7\x94\xa8";
|
||||
static const char STR_NODE_ZH[] = "\xe8\x8a\x82\xe7\x82\xb9";
|
||||
static const char STR_HOP_ZH[] = "\xe8\xb7\xb3\xe6\x95\xb0:";
|
||||
static const char STR_USB_ZH[] = "USB";
|
||||
static const char STR_BT_OFF_ZH[] = "\xe8\x93\x9d\xe7\x89\x99\xe5\xb7\xb2\xe5\x85\xb3";
|
||||
static const char STR_WIFI_ZH[] = "WiFi";
|
||||
static const char STR_WIFI_NOT_CONNECTED_ZH[] = "WiFi: \xe6\x9c\xaa\xe8\xbf\x9e\xe6\x8e\xa5";
|
||||
static const char STR_WIFI_CONNECTED_ZH[] = "WiFi: \xe5\xb7\xb2\xe8\xbf\x9e\xe6\x8e\xa5";
|
||||
static const char STR_SSID_ZH[] = "SSID:";
|
||||
static const char STR_LORA_INFO_ZH[] = "LoRa \xe4\xbf\xa1\xe6\x81\xaf";
|
||||
static const char STR_LORA_ZH[] = "LoRa";
|
||||
static const char STR_ROLE_ZH[] = "\xe8\xa7\x92\xe8\x89\xb2:";
|
||||
static const char STR_FREQ_ZH[] = "\xe9\xa2\x91\xe7\x8e\x87:";
|
||||
static const char STR_CHUTIL_ZH[] = "\xe4\xbf\xa1\xe9\x81\x93\xe5\x88\xa9\xe7\x94\xa8";
|
||||
static const char STR_SYSTEM_ZH[] = "\xe7\xb3\xbb\xe7\xbb\x9f";
|
||||
static const char STR_HEAP_ZH[] = "\xe5\xa0\x86:";
|
||||
static const char STR_PSRAM_ZH[] = "PSRAM:";
|
||||
static const char STR_FLASH_ZH[] = "Flash:";
|
||||
static const char STR_SD_ZH[] = "SD:";
|
||||
static const char STR_VER_ZH[] = "\xe7\x89\x88\xe6\x9c\xac:";
|
||||
static const char STR_UP_ZH[] = "\xe8\xbf\x90\xe8\xa1\x8c:";
|
||||
static const char STR_CLIENT_APP_ZH[] = "\xe5\xae\xa2\xe6\x88\xb7\xe7\xab\xaf";
|
||||
static const char STR_NO_CONNECTED_ZH[] = "\xe6\x9c\xaa\xe8\xbf\x9e\xe6\x8e\xa5";
|
||||
static const char STR_LOCKED_ZH[] = "\xe5\xb7\xb2\xe9\x94\x81\xe5\xae\x9a";
|
||||
static const char STR_CONNECT_TO_UNLOCK_ZH[] = "\xe8\xbf\x9e\xe6\x8e\xa5\xe4\xbb\xa5\xe8\xa7\xa3\xe9\x94\x81";
|
||||
static const char STR_BATTERY_ZH[] = "\xe7\x94\xb5\xe6\xb1\xa0";
|
||||
static const char STR_RESUMING_ZH[] = "\xe6\xad\xa3\xe5\x9c\xa8\xe6\x81\xa2\xe5\xa4\x8d...";
|
||||
static const char STR_LAST_HEARD_ZH[] = "\xe6\x9c\x80\xe5\x90\x8e\xe5\x90\xac\xe5\x88\xb0";
|
||||
static const char STR_HOPS_SIG_ZH[] = "\xe8\xb7\xb3\xe6\x95\xb0/\xe4\xbf\xa1\xe5\x8f\xb7";
|
||||
static const char STR_HOPS_SIGNAL_ZH[] = "\xe8\xb7\xb3\xe6\x95\xb0/\xe4\xbf\xa1\xe5\x8f\xb7";
|
||||
static const char STR_NODES_ZH[] = "\xe8\x8a\x82\xe7\x82\xb9";
|
||||
static const char STR_DISTANCE_ZH[] = "\xe8\xb7\x9d\xe7\xa6\xbb";
|
||||
static const char STR_BEARINGS_ZH[] = "\xe6\x96\xb9\xe4\xbd\x8d";
|
||||
static const char STR_CREATING_SSL_ZH[] = "\xe6\xad\xa3\xe5\x9c\xa8\xe5\x88\x9b\xe5\xbb\xba SSL \xe8\xaf\x81\xe4\xb9\xa6";
|
||||
static const char STR_PLEASE_WAIT_ZH[] = "\xe8\xaf\xb7\xe7\xa8\x8d\xe5\x80\x99 . . .";
|
||||
static const char STR_CRITICAL_FAULT_ZH[] = "\xe4\xb8\xa5\xe9\x87\x8d\xe6\x95\x85\xe9\x9a\x9c";
|
||||
static const char STR_FOR_HELP_VISIT_ZH[] = "\xe8\xaf\xb7\xe8\xae\xbf\xe9\x97\xae\x4e\xe5\xb8\xae\xe5\x8a\xa9";
|
||||
static const char STR_UPDATING_ZH[] = "\xe6\xad\xa3\xe5\x9c\xa8\xe6\x9b\xb4\xe6\x96\xb0";
|
||||
static const char STR_PLEASE_BE_PATIENT_ZH[] = "\xe8\xaf\xb7\xe8\x80\x90\xe5\xbf\x83\xe7\xad\x89\xe5\xbe\x85\xef\xbc\x8c\xe4\xb8\x8d\xe8\xa6\x81\xe5\x85\xb3\xe9\x97\xad\xe7\x94\xb5\xe6\xba\x90\xe3\x80\x82";
|
||||
static const char STR_REBOOT_IN_PROGRESS_ZH[] = "\xe6\xad\xa3\xe5\x9c\xa8\xe9\x87\x8d\xe5\x90\xaf...";
|
||||
static const char STR_POWER_MENU_ZH[] = "\xe7\x94\xb5\xe6\xba\x90";
|
||||
static const char STR_SYSTEM_MENU_ZH[] = "\xe7\xb3\xbb\xe7\xbb\x9f";
|
||||
static const char STR_SCREEN_OPTIONS_ZH[] = "\xe5\xb1\x8f\xe5\xb9\x95";
|
||||
static const char STR_WIFI_TOGGLE_ZH[] = "WiFi";
|
||||
static const char STR_BLUETOOTH_TOGGLE_ZH[] = "\xe8\x93\x9d\xe7\x89\x99";
|
||||
static const char STR_NODE_DB_RESET_ZH[] = "\xe9\x87\x8d\xe7\xbd\xae\xe8\x8a\x82\xe7\x82\xb9\xe5\xba\x93";
|
||||
static const char STR_MANAGE_NODE_ZH[] = "\xe7\xae\xa1\xe7\x90\x86\xe8\x8a\x82\xe7\x82\xb9";
|
||||
static const char STR_REMOVE_FAVORITE_ZH[] = "\xe5\x8f\x96\xe6\xb6\x88\xe6\x94\xb6\xe8\x97\x8f";
|
||||
static const char STR_ADD_FAVORITE_ZH[] = "\xe6\xb7\xbb\xe5\x8a\xa0\xe6\x94\xb6\xe8\x97\x8f";
|
||||
static const char STR_TEST_MENU_ZH[] = "\xe6\xb5\x8b\xe8\xaf\x95";
|
||||
static const char STR_TRACE_ROUTE_ZH[] = "\xe8\xb7\xaf\xe7\x94\xb1\xe8\xbf\xbd\xe8\xb8\xaa";
|
||||
static const char STR_THROTTLE_MSG_ZH[] = "\xe6\xb6\x88\xe6\x81\xaf\xe5\xb7\xb2\xe9\x99\x90\xe6\xb5\x81";
|
||||
static const char STR_MSG_VIEW_MODE_ZH[] = "\xe6\x9f\xa5\xe7\x9c\x8b\xe6\xa8\xa1\xe5\xbc\x8f";
|
||||
static const char STR_DELETE_MESSAGES_ZH[] = "\xe5\x88\xa0\xe9\x99\xa4\xe6\xb6\x88\xe6\x81\xaf";
|
||||
static const char STR_NODE_NAME_LENGTH_ZH[] = "\xe5\x90\x8d\xe7\xa7\xb0\xe9\x95\xbf\xe5\xba\xa6";
|
||||
static const char STR_GPS_MENU_ZH[] = "GPS";
|
||||
static const char STR_GPS_TOGGLE_ZH[] = "GPS \xe5\xbc\x80\xe5\x85\xb3";
|
||||
static const char STR_GPS_FORMAT_ZH[] = "GPS \xe6\xa0\xbc\xe5\xbc\x8f";
|
||||
static const char STR_GPS_SMART_POS_ZH[] = "\xe6\x99\xba\xe8\x83\xbd\xe5\xae\x9a\xe4\xbd\x8d";
|
||||
static const char STR_GPS_UPDATE_INTERVAL_ZH[] = "\xe6\x9b\xb4\xe6\x96\xb0\xe9\x97\xb4\xe9\x9a\x94";
|
||||
static const char STR_GPS_POS_BCAST_ZH[] = "\xe4\xbd\x8d\xe7\xbd\xae\xe5\xb9\xbf\xe6\x92\xad";
|
||||
static const char STR_COMPASS_NORTH_ZH[] = "\xe6\x8c\x87\xe5\x8d\x97\xe9\x92\x88";
|
||||
static const char STR_BUZZER_MODE_ZH[] = "\xe8\x9c\x82\xe9\xb8\xa3\xe5\x99\xa8";
|
||||
static const char STR_HAM_MODE_ZH[] = "Ham \xe6\xa8\xa1\xe5\xbc\x8f";
|
||||
static const char STR_LICENSED_NORMAL_ZH[] = "\xe6\x99\xae\xe9\x80\x9a\xe6\xa8\xa1\xe5\xbc\x8f";
|
||||
static const char STR_KEY_VERIFY_ZH[] = "\xe9\xaa\x8c\xe8\xaf\x81\xe5\xaf\x86\xe9\x92\xa5";
|
||||
static const char STR_REGION_UNSET_ZH[] = "\xe8\xaf\xb7\xe9\x80\x89\xe6\x8b\xa9\xe5\x9c\xb0\xe5\x8c\xba";
|
||||
static const char STR_SELECT_REGION_ZH[] = "\xe8\xaf\xb7\xe9\x80\x89\xe6\x8b\xa9\xe6\x82\xa8\xe7\x9a\x84 LoRa \xe5\x8c\xba\xe5\x9f\x9f:";
|
||||
static const char STR_REBOOTING_ZH[] = "\xe6\xad\xa3\xe5\x9c\xa8\xe9\x87\x8d\xe5\x90\xaf...";
|
||||
static const char STR_LORA_ACTIONS_ZH[] = "LoRa \xe6\x93\x8d\xe4\xbd\x9c";
|
||||
static const char STR_SLOT0_ZH[] = "\xe6\xa7\xbd\xe4\xbd\x8d 0 (\xe8\x87\xaa\xe5\x8a\xa8)";
|
||||
static const char STR_WITH_PRESET_ZH[] = "\xe9\xa2\x84\xe8\xae\xbe\xe6\xb6\x88\xe6\x81\xaf";
|
||||
static const char STR_WITH_FREETEXT_ZH[] = "\xe8\x87\xaa\xe5\xae\x9a\xe4\xb9\x89";
|
||||
static const char STR_DELETE_OLDEST_ZH[] = "\xe5\x88\xa0\xe9\x99\xa4\xe6\x9c\x80\xe6\x97\xa7";
|
||||
static const char STR_DELETE_THIS_CHAT_ZH[] = "\xe5\x88\xa0\xe9\x99\xa4\xe6\xad\xa4\xe8\x81\x8a\xe5\xa4\xa9";
|
||||
static const char STR_DELETE_ALL_ZH[] = "\xe5\x88\xa0\xe9\x99\xa4\xe5\x85\xa8\xe9\x83\xa8";
|
||||
static const char STR_DELETE_ALL_CHATS_ZH[] = "\xe5\x88\xa0\xe9\x99\xa4\xe6\x89\x80\xe6\x9c\x89\xe8\x81\x8a\xe5\xa4\xa9";
|
||||
static const char STR_TEMPORARILY_MUTE_ZH[] = "\xe4\xb8\xb4\xe6\x97\xb6\xe9\x9d\x99\xe9\x9f\xb3";
|
||||
static const char STR_UNMUTE_ZH[] = "\xe5\x8f\x96\xe6\xb6\x88\xe9\x9d\x99\xe9\x9f\xb3";
|
||||
static const char STR_TOGGLE_BACKLIGHT_ZH[] = "\xe8\x83\x8c\xe5\x85\x89\xe5\xbc\x80\xe5\x85\xb3";
|
||||
static const char STR_SLEEP_SCREEN_ZH[] = "\xe4\xbc\x91\xe7\x9c\xa0\xe5\xb1\x8f\xe5\xb9\x95";
|
||||
static const char STR_SEND_POSITION_ZH[] = "\xe5\x8f\x91\xe9\x80\x81\xe4\xbd\x8d\xe7\xbd\xae";
|
||||
static const char STR_SEND_NODE_INFO_ZH[] = "\xe5\x8f\x91\xe9\x80\x81\xe8\x8a\x82\xe7\x82\xb9\xe4\xbf\xa1\xe6\x81\xaf";
|
||||
static const char STR_NEW_PRESET_MSG_ZH[] = "\xe6\x96\xb0\xe9\xa2\x84\xe8\xae\xbe\xe6\xb6\x88\xe6\x81\xaf";
|
||||
static const char STR_NEW_FREETEXT_MSG_ZH[] = "\xe6\x96\xb0\xe8\x87\xaa\xe5\xae\x9a\xe4\xb9\x89\xe6\xb6\x88\xe6\x81\xaf";
|
||||
static const char STR_NOTIFICATIONS_ZH[] = "\xe9\x80\x9a\xe7\x9f\xa5";
|
||||
static const char STR_POWER_ZH[] = "\xe7\x94\xb5\xe6\xba\x90";
|
||||
static const char STR_REBOOT_SHUTDOWN_ZH[] = "\xe9\x87\x8d\xe5\x90\xaf/\xe5\x85\xb3\xe6\x9c\xba";
|
||||
static const char STR_GO_TO_CHAT_ZH[] = "\xe8\xbf\x9b\xe5\x85\xa5\xe8\x81\x8a\xe5\xa4\xa9";
|
||||
static const char STR_NEW_PRESET_ZH[] = "\xe6\x96\xb0\xe9\xa2\x84\xe8\xae\xbe";
|
||||
static const char STR_FAVORITE_ZH[] = "\xe6\x94\xb6\xe8\x97\x8f";
|
||||
static const char STR_UNMUTE_NOTIFICATIONS_ZH[] = "\xe5\x8f\x96\xe6\xb6\x88\xe9\x80\x9a\xe7\x9f\xa5\xe9\x9d\x99\xe9\x9f\xb3";
|
||||
static const char STR_MUTE_NOTIFICATIONS_ZH[] = "\xe9\x80\x9a\xe7\x9f\xa5\xe9\x9d\x99\xe9\x9f\xb3";
|
||||
static const char STR_UNIGNORE_NODE_ZH[] = "\xe5\x8f\x96\xe6\xb6\x88\xe5\xbf\xbd\xe7\x95\xa5";
|
||||
static const char STR_IGNORE_NODE_ZH[] = "\xe5\xbf\xbd\xe7\x95\xa5\xe8\x8a\x82\xe7\x82\xb9";
|
||||
static const char STR_NUMBER_PICKER_ZH[] = "\xe6\x95\xb0\xe5\xad\x97\xe9\x80\x89\xe6\x8b\xa9";
|
||||
static const char STR_TEST_ANNOUNCE_ZH[] = "\xe6\xb5\x8b\xe8\xaf\x95\xe5\x85\xac\xe5\x91\x8a";
|
||||
static const char STR_NODE_ACTIONS_ZH[] = "\xe8\x8a\x82\xe7\x82\xb9\xe6\x93\x8d\xe4\xbd\x9c/\xe8\xae\xbe\xe7\xbd\xae";
|
||||
static const char STR_SHOW_NAME_LENGTH_ZH[] = "\xe6\x98\xbe\xe7\xa4\xba\xe9\x95\xbf/\xe7\x9f\xad\xe5\x90\x8d";
|
||||
static const char STR_BLUETOOTH_ZH[] = "\xe8\x93\x9d\xe7\x89\x99";
|
||||
static const char STR_NO_MESSAGES_ZH[] = "\xe6\x97\xa0\xe6\xb6\x88\xe6\x81\xaf";
|
||||
|
||||
inline const char *getStringZH(StringKey key)
|
||||
{
|
||||
switch (key) {
|
||||
case StringKey::Back:
|
||||
return STR_BACK_ZH;
|
||||
case StringKey::OK:
|
||||
return STR_OK_ZH;
|
||||
case StringKey::GotIt:
|
||||
return STR_GOT_IT_ZH;
|
||||
case StringKey::WelcomeToMeshtastic:
|
||||
return STR_WELCOME_TO_MESHTASTIC_ZH;
|
||||
case StringKey::SwipeToNavigate:
|
||||
return STR_SWIPE_TO_NAVIGATE_ZH;
|
||||
case StringKey::ClickToNavigate:
|
||||
return STR_CLICK_TO_NAVIGATE_ZH;
|
||||
case StringKey::UseSelectButton:
|
||||
return STR_USE_SELECT_BUTTON_ZH;
|
||||
case StringKey::DeviceRole:
|
||||
return STR_DEVICE_ROLE_ZH;
|
||||
case StringKey::RadioPreset:
|
||||
return STR_RADIO_PRESET_ZH;
|
||||
case StringKey::FrequencySlot:
|
||||
return STR_FREQUENCY_SLOT_ZH;
|
||||
case StringKey::LoRaRegion:
|
||||
return STR_LORA_REGION_ZH;
|
||||
case StringKey::Client:
|
||||
return STR_CLIENT_ZH;
|
||||
case StringKey::ClientMute:
|
||||
return STR_CLIENT_MUTE_ZH;
|
||||
case StringKey::LostAndFound:
|
||||
return STR_LOST_AND_FOUND_ZH;
|
||||
case StringKey::Tracker:
|
||||
return STR_TRACKER_ZH;
|
||||
case StringKey::Hour12:
|
||||
return STR_12HOUR_ZH;
|
||||
case StringKey::Hour24:
|
||||
return STR_24HOUR_ZH;
|
||||
case StringKey::ClockAction:
|
||||
return STR_CLOCK_ACTION_ZH;
|
||||
case StringKey::ClockFace:
|
||||
return STR_CLOCK_FACE_ZH;
|
||||
case StringKey::TimeFormat:
|
||||
return STR_TIME_FORMAT_ZH;
|
||||
case StringKey::Timezone:
|
||||
return STR_TIMEZONE_ZH;
|
||||
case StringKey::MessageAction:
|
||||
return STR_MESSAGE_ACTION_ZH;
|
||||
case StringKey::Reply:
|
||||
return STR_REPLY_ZH;
|
||||
case StringKey::ViewChats:
|
||||
return STR_VIEW_CHATS_ZH;
|
||||
case StringKey::UnmuteChannel:
|
||||
return STR_UNMUTE_CHANNEL_ZH;
|
||||
case StringKey::MuteChannel:
|
||||
return STR_MUTE_CHANNEL_ZH;
|
||||
case StringKey::Delete:
|
||||
return STR_DELETE_ZH;
|
||||
case StringKey::ReadAloud:
|
||||
return STR_READ_ALOUD_ZH;
|
||||
case StringKey::Brightness:
|
||||
return STR_BRIGHTNESS_ZH;
|
||||
case StringKey::FrameVisibility:
|
||||
return STR_FRAME_VISIBILITY_ZH;
|
||||
case StringKey::DisplayUnits:
|
||||
return STR_DISPLAY_UNITS_ZH;
|
||||
case StringKey::MessageBubbles:
|
||||
return STR_MESSAGE_BUBBLES_ZH;
|
||||
case StringKey::Theme:
|
||||
return STR_THEME_ZH;
|
||||
case StringKey::Reboot:
|
||||
return STR_REBOOT_ZH;
|
||||
case StringKey::Shutdown:
|
||||
return STR_SHUTDOWN_ZH;
|
||||
case StringKey::SwitchToMUI:
|
||||
return STR_SWITCH_TO_MUI_ZH;
|
||||
case StringKey::DisplayOptions:
|
||||
return STR_DISPLAY_OPTIONS_ZH;
|
||||
case StringKey::Language:
|
||||
return STR_LANGUAGE_ZH;
|
||||
case StringKey::Chinese:
|
||||
return STR_CHINESE_ZH;
|
||||
case StringKey::English:
|
||||
return STR_ENGLISH_ZH;
|
||||
case StringKey::NoGPS:
|
||||
return STR_NO_GPS_ZH;
|
||||
case StringKey::GPSoff:
|
||||
return STR_GPS_OFF_ZH;
|
||||
case StringKey::GPSnotPresent:
|
||||
return STR_GPS_NOT_PRESENT_ZH;
|
||||
case StringKey::GPSisDisabled:
|
||||
return STR_GPS_IS_DISABLED_ZH;
|
||||
case StringKey::Node:
|
||||
return STR_NODE_ZH;
|
||||
case StringKey::Hop:
|
||||
return STR_HOP_ZH;
|
||||
case StringKey::USB:
|
||||
return STR_USB_ZH;
|
||||
case StringKey::BT_off:
|
||||
return STR_BT_OFF_ZH;
|
||||
case StringKey::WiFi:
|
||||
return STR_WIFI_ZH;
|
||||
case StringKey::WiFiNotConnected:
|
||||
return STR_WIFI_NOT_CONNECTED_ZH;
|
||||
case StringKey::WiFiConnected:
|
||||
return STR_WIFI_CONNECTED_ZH;
|
||||
case StringKey::SSID:
|
||||
return STR_SSID_ZH;
|
||||
case StringKey::LoRaInfo:
|
||||
return STR_LORA_INFO_ZH;
|
||||
case StringKey::LoRa:
|
||||
return STR_LORA_ZH;
|
||||
case StringKey::Role:
|
||||
return STR_ROLE_ZH;
|
||||
case StringKey::Freq:
|
||||
return STR_FREQ_ZH;
|
||||
case StringKey::ChUtil:
|
||||
return STR_CHUTIL_ZH;
|
||||
case StringKey::System:
|
||||
return STR_SYSTEM_ZH;
|
||||
case StringKey::Heap:
|
||||
return STR_HEAP_ZH;
|
||||
case StringKey::PSRAM:
|
||||
return STR_PSRAM_ZH;
|
||||
case StringKey::Flash:
|
||||
return STR_FLASH_ZH;
|
||||
case StringKey::SD:
|
||||
return STR_SD_ZH;
|
||||
case StringKey::Ver:
|
||||
return STR_VER_ZH;
|
||||
case StringKey::Up:
|
||||
return STR_UP_ZH;
|
||||
case StringKey::ClientApp:
|
||||
return STR_CLIENT_APP_ZH;
|
||||
case StringKey::NoConnected:
|
||||
return STR_NO_CONNECTED_ZH;
|
||||
case StringKey::LOCKED:
|
||||
return STR_LOCKED_ZH;
|
||||
case StringKey::ConnectToUnlock:
|
||||
return STR_CONNECT_TO_UNLOCK_ZH;
|
||||
case StringKey::Battery:
|
||||
return STR_BATTERY_ZH;
|
||||
case StringKey::Resuming:
|
||||
return STR_RESUMING_ZH;
|
||||
case StringKey::LastHeard:
|
||||
return STR_LAST_HEARD_ZH;
|
||||
case StringKey::HopsSig:
|
||||
return STR_HOPS_SIG_ZH;
|
||||
case StringKey::HopsSignal:
|
||||
return STR_HOPS_SIGNAL_ZH;
|
||||
case StringKey::Nodes:
|
||||
return STR_NODES_ZH;
|
||||
case StringKey::Distance:
|
||||
return STR_DISTANCE_ZH;
|
||||
case StringKey::Bearings:
|
||||
return STR_BEARINGS_ZH;
|
||||
case StringKey::CreatingSSLCert:
|
||||
return STR_CREATING_SSL_ZH;
|
||||
case StringKey::PleaseWait:
|
||||
return STR_PLEASE_WAIT_ZH;
|
||||
case StringKey::CriticalFault:
|
||||
return STR_CRITICAL_FAULT_ZH;
|
||||
case StringKey::ForHelpVisit:
|
||||
return STR_FOR_HELP_VISIT_ZH;
|
||||
case StringKey::Updating:
|
||||
return STR_UPDATING_ZH;
|
||||
case StringKey::PleaseBePatient:
|
||||
return STR_PLEASE_BE_PATIENT_ZH;
|
||||
case StringKey::RebootInProgress:
|
||||
return STR_REBOOT_IN_PROGRESS_ZH;
|
||||
case StringKey::PowerMenu:
|
||||
return STR_POWER_MENU_ZH;
|
||||
case StringKey::SystemMenu:
|
||||
return STR_SYSTEM_MENU_ZH;
|
||||
case StringKey::ScreenOptions:
|
||||
return STR_SCREEN_OPTIONS_ZH;
|
||||
case StringKey::WifiToggle:
|
||||
return STR_WIFI_TOGGLE_ZH;
|
||||
case StringKey::BluetoothToggle:
|
||||
return STR_BLUETOOTH_TOGGLE_ZH;
|
||||
case StringKey::NodeDbReset:
|
||||
return STR_NODE_DB_RESET_ZH;
|
||||
case StringKey::ManageNode:
|
||||
return STR_MANAGE_NODE_ZH;
|
||||
case StringKey::RemoveFavorite:
|
||||
return STR_REMOVE_FAVORITE_ZH;
|
||||
case StringKey::AddFavorite:
|
||||
return STR_ADD_FAVORITE_ZH;
|
||||
case StringKey::TestMenu:
|
||||
return STR_TEST_MENU_ZH;
|
||||
case StringKey::TraceRoute:
|
||||
return STR_TRACE_ROUTE_ZH;
|
||||
case StringKey::ThrottleMessage:
|
||||
return STR_THROTTLE_MSG_ZH;
|
||||
case StringKey::MessageViewMode:
|
||||
return STR_MSG_VIEW_MODE_ZH;
|
||||
case StringKey::DeleteMessages:
|
||||
return STR_DELETE_MESSAGES_ZH;
|
||||
case StringKey::NodeNameLength:
|
||||
return STR_NODE_NAME_LENGTH_ZH;
|
||||
case StringKey::GPSMenu:
|
||||
return STR_GPS_MENU_ZH;
|
||||
case StringKey::GPSToggle:
|
||||
return STR_GPS_TOGGLE_ZH;
|
||||
case StringKey::GPSFormat:
|
||||
return STR_GPS_FORMAT_ZH;
|
||||
case StringKey::GPSSmartPosition:
|
||||
return STR_GPS_SMART_POS_ZH;
|
||||
case StringKey::GPSUpdateInterval:
|
||||
return STR_GPS_UPDATE_INTERVAL_ZH;
|
||||
case StringKey::GPSPositionBroadcast:
|
||||
return STR_GPS_POS_BCAST_ZH;
|
||||
case StringKey::CompassNorth:
|
||||
return STR_COMPASS_NORTH_ZH;
|
||||
case StringKey::BuzzerMode:
|
||||
return STR_BUZZER_MODE_ZH;
|
||||
case StringKey::HamModeConfirm:
|
||||
return STR_HAM_MODE_ZH;
|
||||
case StringKey::LicensedToNormal:
|
||||
return STR_LICENSED_NORMAL_ZH;
|
||||
case StringKey::KeyVerification:
|
||||
return STR_KEY_VERIFY_ZH;
|
||||
case StringKey::RegionUnset:
|
||||
return STR_REGION_UNSET_ZH;
|
||||
case StringKey::SelectRegionMessage:
|
||||
return STR_SELECT_REGION_ZH;
|
||||
case StringKey::Rebooting:
|
||||
return STR_REBOOTING_ZH;
|
||||
case StringKey::LoRaActions:
|
||||
return STR_LORA_ACTIONS_ZH;
|
||||
case StringKey::Slot0:
|
||||
return STR_SLOT0_ZH;
|
||||
case StringKey::WithPreset:
|
||||
return STR_WITH_PRESET_ZH;
|
||||
case StringKey::WithFreetext:
|
||||
return STR_WITH_FREETEXT_ZH;
|
||||
case StringKey::DeleteOldest:
|
||||
return STR_DELETE_OLDEST_ZH;
|
||||
case StringKey::DeleteThisChat:
|
||||
return STR_DELETE_THIS_CHAT_ZH;
|
||||
case StringKey::DeleteAll:
|
||||
return STR_DELETE_ALL_ZH;
|
||||
case StringKey::DeleteAllChats:
|
||||
return STR_DELETE_ALL_CHATS_ZH;
|
||||
case StringKey::TemporarilyMute:
|
||||
return STR_TEMPORARILY_MUTE_ZH;
|
||||
case StringKey::Unmute:
|
||||
return STR_UNMUTE_ZH;
|
||||
case StringKey::ToggleBacklight:
|
||||
return STR_TOGGLE_BACKLIGHT_ZH;
|
||||
case StringKey::SleepScreen:
|
||||
return STR_SLEEP_SCREEN_ZH;
|
||||
case StringKey::SendPosition:
|
||||
return STR_SEND_POSITION_ZH;
|
||||
case StringKey::SendNodeInfo:
|
||||
return STR_SEND_NODE_INFO_ZH;
|
||||
case StringKey::NewPresetMsg:
|
||||
return STR_NEW_PRESET_MSG_ZH;
|
||||
case StringKey::NewFreetextMsg:
|
||||
return STR_NEW_FREETEXT_MSG_ZH;
|
||||
case StringKey::Notifications:
|
||||
return STR_NOTIFICATIONS_ZH;
|
||||
case StringKey::Power:
|
||||
return STR_POWER_ZH;
|
||||
case StringKey::RebootShutdown:
|
||||
return STR_REBOOT_SHUTDOWN_ZH;
|
||||
case StringKey::GoToChat:
|
||||
return STR_GO_TO_CHAT_ZH;
|
||||
case StringKey::NewPreset:
|
||||
return STR_NEW_PRESET_ZH;
|
||||
case StringKey::Favorite:
|
||||
return STR_FAVORITE_ZH;
|
||||
case StringKey::UnmuteNotifications:
|
||||
return STR_UNMUTE_NOTIFICATIONS_ZH;
|
||||
case StringKey::MuteNotifications:
|
||||
return STR_MUTE_NOTIFICATIONS_ZH;
|
||||
case StringKey::UnignoreNode:
|
||||
return STR_UNIGNORE_NODE_ZH;
|
||||
case StringKey::IgnoreNode:
|
||||
return STR_IGNORE_NODE_ZH;
|
||||
case StringKey::NumberPicker:
|
||||
return STR_NUMBER_PICKER_ZH;
|
||||
case StringKey::TestAnnounce:
|
||||
return STR_TEST_ANNOUNCE_ZH;
|
||||
case StringKey::NodeActionsSettings:
|
||||
return STR_NODE_ACTIONS_ZH;
|
||||
case StringKey::ShowNameLength:
|
||||
return STR_SHOW_NAME_LENGTH_ZH;
|
||||
case StringKey::Bluetooth:
|
||||
return STR_BLUETOOTH_ZH;
|
||||
case StringKey::NoMessages:
|
||||
return STR_NO_MESSAGES_ZH;
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace l10n
|
||||
} // namespace meshtastic
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "HapticFeedback.h"
|
||||
|
||||
#ifdef HAPTIC_FEEDBACK_PIN
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef HAPTIC_FEEDBACK_ACTIVE_LOW
|
||||
#define HAPTIC_FEEDBACK_ON_STATE LOW
|
||||
#define HAPTIC_FEEDBACK_OFF_STATE HIGH
|
||||
#else
|
||||
#define HAPTIC_FEEDBACK_ON_STATE HIGH
|
||||
#define HAPTIC_FEEDBACK_OFF_STATE LOW
|
||||
#endif
|
||||
|
||||
HapticFeedback *hapticFeedback = nullptr;
|
||||
|
||||
void initHapticFeedback()
|
||||
{
|
||||
if (!hapticFeedback)
|
||||
hapticFeedback = new HapticFeedback();
|
||||
}
|
||||
|
||||
HapticFeedback::HapticFeedback() : concurrency::OSThread("Haptic")
|
||||
{
|
||||
pinMode(HAPTIC_FEEDBACK_PIN, OUTPUT);
|
||||
digitalWrite(HAPTIC_FEEDBACK_PIN, HAPTIC_FEEDBACK_OFF_STATE);
|
||||
}
|
||||
|
||||
void HapticFeedback::motorWrite(bool on)
|
||||
{
|
||||
digitalWrite(HAPTIC_FEEDBACK_PIN, on ? HAPTIC_FEEDBACK_ON_STATE : HAPTIC_FEEDBACK_OFF_STATE);
|
||||
}
|
||||
|
||||
void HapticFeedback::pulse(uint16_t durationMs)
|
||||
{
|
||||
motorWrite(true);
|
||||
pulseOffAt = millis() + durationMs;
|
||||
if (pulseOffAt == 0) // 0 is the "no pulse" sentinel
|
||||
pulseOffAt = 1;
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
void HapticFeedback::armDelayedPulse(uint16_t delayMs, uint16_t durationMs)
|
||||
{
|
||||
delayedPulseAt = millis() + delayMs;
|
||||
if (delayedPulseAt == 0)
|
||||
delayedPulseAt = 1;
|
||||
delayedPulseDuration = durationMs;
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
void HapticFeedback::cancelDelayedPulse()
|
||||
{
|
||||
delayedPulseAt = 0;
|
||||
}
|
||||
|
||||
void HapticFeedback::scheduleNext()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint32_t next = 0;
|
||||
if (pulseOffAt != 0)
|
||||
next = pulseOffAt;
|
||||
if (delayedPulseAt != 0 && (next == 0 || (int32_t)(delayedPulseAt - next) < 0))
|
||||
next = delayedPulseAt;
|
||||
if (next == 0)
|
||||
return;
|
||||
int32_t delay = (int32_t)(next - now);
|
||||
setIntervalFromNow(delay > 0 ? (unsigned long)delay : 0);
|
||||
}
|
||||
|
||||
int32_t HapticFeedback::runOnce()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
|
||||
if (pulseOffAt != 0 && (int32_t)(now - pulseOffAt) >= 0) {
|
||||
motorWrite(false);
|
||||
pulseOffAt = 0;
|
||||
}
|
||||
|
||||
if (delayedPulseAt != 0 && (int32_t)(now - delayedPulseAt) >= 0) {
|
||||
uint16_t dur = delayedPulseDuration;
|
||||
delayedPulseAt = 0;
|
||||
pulse(dur);
|
||||
}
|
||||
|
||||
uint32_t next = 0;
|
||||
if (pulseOffAt != 0)
|
||||
next = pulseOffAt;
|
||||
if (delayedPulseAt != 0 && (next == 0 || (int32_t)(delayedPulseAt - next) < 0))
|
||||
next = delayedPulseAt;
|
||||
if (next == 0)
|
||||
return 60 * 1000;
|
||||
int32_t delay = (int32_t)(next - now);
|
||||
return delay > 0 ? delay : 0;
|
||||
}
|
||||
|
||||
#endif // HAPTIC_FEEDBACK_PIN
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef HAPTIC_FEEDBACK_PIN
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// Non-blocking pulses on a GPIO vibration motor. HAPTIC_FEEDBACK_ACTIVE_LOW inverts polarity.
|
||||
class HapticFeedback : public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
HapticFeedback();
|
||||
void pulse(uint16_t durationMs = 30);
|
||||
void armDelayedPulse(uint16_t delayMs, uint16_t durationMs = 30);
|
||||
void cancelDelayedPulse();
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
uint32_t pulseOffAt = 0;
|
||||
uint32_t delayedPulseAt = 0;
|
||||
uint16_t delayedPulseDuration = 0;
|
||||
|
||||
void motorWrite(bool on);
|
||||
// Reschedule to the soonest pending event so later arms don't clobber earlier wakes.
|
||||
void scheduleNext();
|
||||
};
|
||||
|
||||
extern HapticFeedback *hapticFeedback;
|
||||
void initHapticFeedback();
|
||||
|
||||
#endif // HAPTIC_FEEDBACK_PIN
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "PowerFSM.h" // needed for event trigger
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "input/HapticFeedback.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#ifdef MESHTASTIC_LOCKDOWN
|
||||
#include "security/LockdownDisplay.h"
|
||||
@@ -256,6 +257,16 @@ void InputBroker::Init()
|
||||
}
|
||||
touchBacklightActive = false;
|
||||
};
|
||||
#endif
|
||||
#if defined(HAPTIC_FEEDBACK_PIN)
|
||||
// Blip on touch, second blip when long-press fires (500 ms = touchConfig.longPressTime default).
|
||||
touchConfig.suppressLeadUpSound = true;
|
||||
initHapticFeedback();
|
||||
touchConfig.onPress = []() {
|
||||
hapticFeedback->pulse(80);
|
||||
hapticFeedback->armDelayedPulse(500, 80);
|
||||
};
|
||||
touchConfig.onRelease = []() { hapticFeedback->cancelDelayedPulse(); };
|
||||
#endif
|
||||
TouchButtonThread->initButton(touchConfig);
|
||||
#endif
|
||||
|
||||
@@ -66,14 +66,8 @@ void TouchScreenBase::init(bool hasTouch)
|
||||
|
||||
int32_t TouchScreenBase::runOnce()
|
||||
{
|
||||
uint32_t nowMs = millis();
|
||||
if (nowMs - _lastRun < 20) { // suppress too fast consecutive runOnce() executions
|
||||
return 20;
|
||||
}
|
||||
_lastRun = nowMs;
|
||||
TouchEvent e;
|
||||
e.touchEvent = static_cast<char>(TOUCH_ACTION_NONE);
|
||||
this->setInterval(TOUCH_POLL_INTERVAL_IDLE);
|
||||
const bool fastTapMode = fastTapModeEnabled();
|
||||
const bool allowLongPress = longPressEnabled();
|
||||
|
||||
@@ -192,7 +186,7 @@ int32_t TouchScreenBase::runOnce()
|
||||
|
||||
void TouchScreenBase::hapticFeedback()
|
||||
{
|
||||
#if defined(T_WATCH_S3) || defined(T_WATCH_ULTRA)
|
||||
#ifdef T_WATCH_S3
|
||||
drv.setWaveform(0, 75);
|
||||
drv.setWaveform(1, 0); // end waveform
|
||||
drv.go();
|
||||
|
||||
@@ -53,7 +53,6 @@ class TouchScreenBase : public Observable<const InputEvent *>, public concurrenc
|
||||
time_t _start; // for LONG_PRESS
|
||||
uint32_t _lastTouchSeenMs; // helps suppress brief touch-controller dropouts
|
||||
bool _tapped; // for DOUBLE_TAP
|
||||
uint32_t _lastRun = 0; // helps suppress too fast consecutive runOnce() executions
|
||||
|
||||
const char *_originName;
|
||||
};
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
#include "InputBroker.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "sleep.h"
|
||||
#include <cstring>
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -18,27 +16,6 @@
|
||||
|
||||
TouchScreenImpl1 *touchScreenImpl1;
|
||||
|
||||
// Hardware-interrupt wake on the touch IRQ line. Scoped to T-Watch Ultra: other touch boards
|
||||
// either drive this pin differently (RAK14014 already owns this interrupt in TFTDisplay) or
|
||||
// route it through an IO expander, which can't be used with attachInterrupt().
|
||||
#if defined(SCREEN_TOUCH_INT) && defined(T_WATCH_ULTRA)
|
||||
#define TOUCHSCREEN_HAS_INTERRUPT 1
|
||||
|
||||
// The touch controller pulls SCREEN_TOUCH_INT low when a new touch begins. Wake the polling
|
||||
// thread immediately so the touch is handled without waiting for the next idle poll. The
|
||||
// periodic poll in TouchScreenBase::runOnce() remains as a fallback. Mirrors the button
|
||||
// interrupt handling in ButtonThread/InputBroker.
|
||||
static void touchScreenInterruptHandler()
|
||||
{
|
||||
if (touchScreenImpl1) {
|
||||
touchScreenImpl1->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *))
|
||||
: TouchScreenBase("touchscreen1", width, height), _getTouch(getTouch)
|
||||
{
|
||||
@@ -50,7 +27,6 @@ void TouchScreenImpl1::init()
|
||||
if (portduino_config.touchscreenModule) {
|
||||
TouchScreenBase::init(true);
|
||||
inputBroker->registerSource(this);
|
||||
attachTouchInterrupt();
|
||||
} else {
|
||||
TouchScreenBase::init(false);
|
||||
}
|
||||
@@ -61,46 +37,9 @@ void TouchScreenImpl1::init()
|
||||
TouchScreenBase::init(true);
|
||||
if (inputBroker)
|
||||
inputBroker->registerSource(this);
|
||||
attachTouchInterrupt();
|
||||
#endif
|
||||
|
||||
#if defined(TOUCHSCREEN_HAS_INTERRUPT) && defined(ARCH_ESP32)
|
||||
// Detach/reattach our interrupt around light sleep, so sleep.cpp can configure the touch
|
||||
// pin as a wake source without our handler interfering.
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Attach the touch-controller IRQ so a new touch wakes the polling thread immediately.
|
||||
// No-op on boards without a usable touch interrupt line.
|
||||
void TouchScreenImpl1::attachTouchInterrupt()
|
||||
{
|
||||
#ifdef TOUCHSCREEN_HAS_INTERRUPT
|
||||
pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP);
|
||||
attachInterrupt(SCREEN_TOUCH_INT, touchScreenInterruptHandler, FALLING);
|
||||
LOG_INFO("TouchScreen interrupt attached on pin %d", SCREEN_TOUCH_INT);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Detach our interrupt before light sleep; sleep.cpp configures its own wake-on-touch.
|
||||
int TouchScreenImpl1::beforeLightSleep(void *unused)
|
||||
{
|
||||
#ifdef TOUCHSCREEN_HAS_INTERRUPT
|
||||
detachInterrupt(SCREEN_TOUCH_INT);
|
||||
#endif
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reattach our interrupt after waking from light sleep.
|
||||
int TouchScreenImpl1::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
attachTouchInterrupt();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
#endif
|
||||
|
||||
bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y)
|
||||
{
|
||||
return _getTouch(&x, &y);
|
||||
|
||||
@@ -13,23 +13,7 @@ class TouchScreenImpl1 : public TouchScreenBase
|
||||
bool fastTapModeEnabled() const override;
|
||||
bool longPressEnabled() const override;
|
||||
|
||||
// Attach/detach a hardware interrupt on the touch IRQ pin (SCREEN_TOUCH_INT) so a new touch
|
||||
// wakes the polling thread immediately. No-op on boards without a usable touch interrupt line.
|
||||
void attachTouchInterrupt();
|
||||
|
||||
bool (*_getTouch)(int16_t *, int16_t *);
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Detach the touch interrupt before light sleep (so sleep.cpp can own the wake config),
|
||||
// and reattach it afterwards. Mirrors ButtonThread's interrupt handling.
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
|
||||
CallbackObserver<TouchScreenImpl1, void *> lsObserver =
|
||||
CallbackObserver<TouchScreenImpl1, void *>(this, &TouchScreenImpl1::beforeLightSleep);
|
||||
CallbackObserver<TouchScreenImpl1, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<TouchScreenImpl1, esp_sleep_wakeup_cause_t>(this, &TouchScreenImpl1::afterLightSleep);
|
||||
#endif
|
||||
};
|
||||
|
||||
extern TouchScreenImpl1 *touchScreenImpl1;
|
||||
|
||||
+18
-18
@@ -290,9 +290,6 @@ __attribute__((weak, noinline)) bool loopCanSleep()
|
||||
void lateInitVariant() __attribute__((weak));
|
||||
void lateInitVariant() {}
|
||||
|
||||
// NOTE: earlyInitVariant() runs before consoleInit(), so the logging subsystem isn't set
|
||||
// up yet. Calling a LOG_* macro here CRASHES the device -- it is not a silent no-op. Do
|
||||
// not use them in earlyInitVariant(); defer any logging to lateInitVariant() or later.
|
||||
void earlyInitVariant() __attribute__((weak));
|
||||
void earlyInitVariant() {}
|
||||
|
||||
@@ -919,7 +916,7 @@ void setup()
|
||||
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_SPISSD1306) || defined(USE_ST7796) || defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR)
|
||||
defined(USE_SPISSD1306) || defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
|
||||
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
|
||||
@@ -1045,7 +1042,7 @@ void setup()
|
||||
// the current region name)
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(USE_SPISSD1306) || defined(CO5300_CS) || defined(HACKADAY_COMMUNICATOR)
|
||||
defined(USE_ST7796) || defined(USE_SPISSD1306) || defined(HACKADAY_COMMUNICATOR)
|
||||
if (screen)
|
||||
screen->setup();
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
@@ -1074,19 +1071,6 @@ void setup()
|
||||
digitalWrite(RF95_FAN_EN, LOW ^ 0);
|
||||
#endif
|
||||
|
||||
#ifndef ARCH_PORTDUINO
|
||||
|
||||
// Initialize Wifi
|
||||
#if HAS_WIFI
|
||||
initWifi();
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET
|
||||
// Initialize Ethernet
|
||||
initEthernet();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
// Start web server thread.
|
||||
webServerThread = new WebServerThread();
|
||||
@@ -1124,6 +1108,22 @@ void setup()
|
||||
setCPUFast(false); // 80MHz is fine for our slow peripherals
|
||||
#endif
|
||||
|
||||
#ifndef ARCH_PORTDUINO
|
||||
// Force BT init before WiFi so NimBLE gets clean heap
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
setBluetoothEnable(true);
|
||||
#endif
|
||||
// Initialize Wifi after PowerFSM so BT gets clean heap during init
|
||||
#if HAS_WIFI
|
||||
initWifi();
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET
|
||||
// Initialize Ethernet
|
||||
initEthernet();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap());
|
||||
LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram());
|
||||
|
||||
@@ -440,6 +440,24 @@ bool Channels::usesPublicKey(ChannelIndex chIndex)
|
||||
return (psk.size == sizeof(defaultpsk) && memcmp(psk.bytes, defaultpsk, sizeof(defaultpsk) - 1) == 0);
|
||||
}
|
||||
|
||||
bool Channels::isWellKnownChannel(ChannelIndex chIndex)
|
||||
{
|
||||
const auto &ch = getByIndex(chIndex);
|
||||
// Absent (unencrypted) or single-byte PSK — all the well-known key indexes
|
||||
if (ch.settings.psk.size > 1)
|
||||
return false;
|
||||
|
||||
const char *name = getName(chIndex);
|
||||
for (int p = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; p <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; p++) {
|
||||
const char *presetName =
|
||||
DisplayFormatters::getModemPresetDisplayName(static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(p), false, true);
|
||||
// Presets without a display name fall through to "Invalid" — never a match
|
||||
if (strcmp(presetName, "Invalid") != 0 && strcmp(name, presetName) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Channels::hasDefaultChannel()
|
||||
{
|
||||
// If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel
|
||||
|
||||
@@ -88,6 +88,12 @@ class Channels
|
||||
|
||||
// Returns true if this channel's effective key is publicly decryptable (open or well-known/default PSK).
|
||||
bool usesPublicKey(ChannelIndex chIndex);
|
||||
// Returns true if the channel is "well known": its PSK is absent or a
|
||||
// single-byte well-known key index, AND its name is any modem-preset
|
||||
// display name (e.g. a channel named "LongFast" counts even while the
|
||||
// radio runs MediumFast). Broader than isDefaultChannel, which only
|
||||
// matches the current preset's name and PSK byte 1.
|
||||
bool isWellKnownChannel(ChannelIndex chIndex);
|
||||
|
||||
// Returns true if we can be reached via a channel with the default settings given a region and modem preset
|
||||
bool hasDefaultChannel();
|
||||
|
||||
+11
-2
@@ -18,6 +18,9 @@
|
||||
#define default_telemetry_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60)
|
||||
#define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60)
|
||||
#define default_broadcast_smart_minimum_interval_secs 5 * 60
|
||||
// Floor for our own position broadcasts when stationary (unchanged beyond the broadcast
|
||||
// precision) or fixed_position: identical positions get deduped by traffic management anyway.
|
||||
#define default_position_stationary_broadcast_secs (12 * 60 * 60)
|
||||
#define min_default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60)
|
||||
#define min_default_broadcast_smart_minimum_interval_secs 5 * 60
|
||||
#define default_wait_bluetooth_secs IF_ROUTER(1, 60)
|
||||
@@ -34,8 +37,14 @@
|
||||
enum class TrafficType { POSITION, TELEMETRY };
|
||||
|
||||
// Traffic management defaults
|
||||
#define default_traffic_mgmt_position_precision_bits 19 // ~90m grid cells (±45m)
|
||||
#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions
|
||||
#define default_traffic_mgmt_position_precision_bits 19 // ~90m grid cells (±45m)
|
||||
#define default_traffic_mgmt_position_min_interval_secs (11 * 60 * 60) // 11 hours between identical positions
|
||||
// Role cap: tracker-role origins may refresh a duplicate position this often (vs the 11h default).
|
||||
#define default_traffic_mgmt_tracker_position_min_interval_secs (60 * 60) // 1 hour
|
||||
// Role cap: lost-and-found origins may refresh a duplicate position this often, so a lost
|
||||
// device updates frequently without flooding. (Quantised to the dedup tick: ~2 ticks.)
|
||||
// Unlike before, lost-and-found is NOT exempt from the relayed precision clamp.
|
||||
#define default_traffic_mgmt_lost_and_found_position_min_interval_secs (15 * 60) // 15 minutes
|
||||
|
||||
// Hop scaling defaults
|
||||
#define default_hop_scaling_min_target_nodes 40 // walk threshold: first hop reaching this cumulative count
|
||||
|
||||
+55
-6
@@ -66,6 +66,10 @@
|
||||
#include <utility/bonding.h>
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_RP2040
|
||||
#include <hardware/watchdog.h>
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
||||
#include <MeshtasticOTA.h>
|
||||
#endif
|
||||
@@ -590,6 +594,16 @@ NodeDB::NodeDB()
|
||||
moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs;
|
||||
}
|
||||
|
||||
// If a fixed position is configured, restore the persisted position into localPosition at boot.
|
||||
// This keeps position broadcasts / MQTT map reports working after reboot on GPS-less nodes.
|
||||
if (config.position.fixed_position) {
|
||||
meshtastic_PositionLite fixedPos;
|
||||
if (copyNodePosition(getNodeNum(), fixedPos) && (fixedPos.latitude_i != 0 || fixedPos.longitude_i != 0)) {
|
||||
setLocalPosition(TypeConversions::ConvertToPosition(fixedPos));
|
||||
LOG_INFO("Restored fixed position to localPosition: lat=%d lon=%d", fixedPos.latitude_i, fixedPos.longitude_i);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the neighbor info update interval is coerced to the minimum
|
||||
moduleConfig.neighbor_info.update_interval =
|
||||
Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs);
|
||||
@@ -1006,7 +1020,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
|
||||
|
||||
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
|
||||
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(T_WATCH_ULTRA) || defined(HELTEC_V4_R8_TFT)) && \
|
||||
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)) && \
|
||||
HAS_TFT
|
||||
// switch BT off by default; use TFT programming mode or hotkey to enable
|
||||
config.bluetooth.enabled = false;
|
||||
@@ -1092,7 +1106,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
config.display.wake_on_tap_or_motion = true;
|
||||
#endif
|
||||
|
||||
#if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) || defined(T_WATCH_ULTRA)
|
||||
#if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
|
||||
config.display.screen_on_secs = 30;
|
||||
config.display.wake_on_tap_or_motion = true;
|
||||
#endif
|
||||
@@ -1155,8 +1169,12 @@ static void installTrafficManagementDefaults(meshtastic_LocalModuleConfig &mc)
|
||||
{
|
||||
mc.has_traffic_management = true;
|
||||
mc.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero;
|
||||
mc.traffic_management.enabled = true;
|
||||
mc.traffic_management.position_dedup_enabled = true;
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
// Position dedup ships enabled at the 11-hour default window on all supported targets.
|
||||
// STM32WL is excluded at compile time (HAS_TRAFFIC_MANAGEMENT=0 in mesh-pb-constants.h).
|
||||
// Set position_min_interval_secs=0 at runtime to disable dedup.
|
||||
mc.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
|
||||
#endif
|
||||
}
|
||||
|
||||
void NodeDB::installDefaultModuleConfig()
|
||||
@@ -2668,6 +2686,11 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
||||
nodeDatabase.status.clear();
|
||||
nodeDatabase.status.shrink_to_fit();
|
||||
#if WARM_NODE_COUNT > 0
|
||||
#ifdef ARCH_RP2040
|
||||
// nodes.proto + warm.dat are written back-to-back without the loop running between them;
|
||||
// reset the 8s HW watchdog so the second write gets a full budget (issue #10746).
|
||||
watchdog_update();
|
||||
#endif
|
||||
// Same cadence as the node DB; failure is logged but must not propagate —
|
||||
// a false return from here would trigger saveToDisk()'s fsFormat() path.
|
||||
warmStore.saveIfDirty();
|
||||
@@ -2821,6 +2844,10 @@ HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p)
|
||||
return HopStartStatus::INVALID;
|
||||
|
||||
if (p.hop_start == 0) {
|
||||
// hop_start == hop_limit == 0: intentional zero-hop broadcast (e.g. beacon). Valid by definition —
|
||||
// the packet was never meant to travel any hops, so no hop_start ambiguity applies.
|
||||
if (p.hop_limit == 0)
|
||||
return HopStartStatus::VALID;
|
||||
// Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a
|
||||
// bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware
|
||||
// version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as
|
||||
@@ -3161,8 +3188,14 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
mp.via_mqtt); // Store if we received this packet via MQTT
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
// Only sample packets that arrived over LoRa.
|
||||
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && hopScalingModule) {
|
||||
// Only sample genuine RF-origin packets. The transport check excludes packets received
|
||||
// directly from the broker (TRANSPORT_MQTT), but an MQTT-origin packet rebroadcast onto
|
||||
// LoRa by a gateway arrives as TRANSPORT_LORA with via_mqtt set — count those would
|
||||
// inflate the local mesh-size estimate with non-RF nodes (and they usually carry
|
||||
// hop_start==0, landing in the hop-0 bucket that pulls the recommendation lowest), so
|
||||
// exclude via_mqtt too.
|
||||
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && !mp.via_mqtt &&
|
||||
hopScalingModule) {
|
||||
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
|
||||
hopScalingModule->samplePacketForHistogram(mp.from, hopCount);
|
||||
}
|
||||
@@ -3440,6 +3473,20 @@ bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
|
||||
return false;
|
||||
}
|
||||
|
||||
meshtastic_Config_DeviceConfig_Role NodeDB::getNodeRole(NodeNum n)
|
||||
{
|
||||
const meshtastic_NodeInfoLite *info = getMeshNode(n);
|
||||
if (nodeInfoLiteHasUser(info))
|
||||
return info->role;
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Hot-store miss: fall back to the role the warm tier cached at eviction.
|
||||
uint8_t role = 0, prot = 0;
|
||||
if (warmStore.lookupMeta(n, role, prot))
|
||||
return static_cast<meshtastic_Config_DeviceConfig_Role>(role);
|
||||
#endif
|
||||
return meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
}
|
||||
|
||||
/// Find a node in our DB, create an empty NodeInfo if missing
|
||||
meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
{
|
||||
@@ -3662,6 +3709,8 @@ bool NodeDB::createNewIdentity()
|
||||
myNodeInfo.my_node_num = newNodeNum;
|
||||
|
||||
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
|
||||
if (!info)
|
||||
return false;
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, owner);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -341,6 +341,11 @@ class NodeDB
|
||||
/// tier. Returns false if we don't know a key for n.
|
||||
bool copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out);
|
||||
|
||||
/// Resolve a node's device role — hot store (with user) first, then the role
|
||||
/// cached in the warm tier, else CLIENT. Lets role-aware policy keep firing for
|
||||
/// nodes that have aged out of the hot store.
|
||||
meshtastic_Config_DeviceConfig_Role getNodeRole(NodeNum n);
|
||||
|
||||
/// last_heard of a hot-store node, or 0 if absent. Plain scan of meshNodes
|
||||
/// with no allocation side effects (unlike getOrCreateMeshNode).
|
||||
uint32_t hotNodeLastHeard(NodeNum n) const;
|
||||
|
||||
+31
-8
@@ -40,6 +40,17 @@
|
||||
#include "Throttle.h"
|
||||
#include <RTC.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint8_t FILES_MANIFEST_LEVELS = 3;
|
||||
constexpr size_t FILES_MANIFEST_MAX_COUNT = 64;
|
||||
|
||||
void releaseFilesManifest(std::vector<meshtastic_FileInfo> &filesManifest)
|
||||
{
|
||||
std::vector<meshtastic_FileInfo>().swap(filesManifest);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Flag to indicate a heartbeat was received and we should send queue status
|
||||
bool heartbeatReceived = false;
|
||||
|
||||
@@ -298,18 +309,31 @@ void PhoneAPI::handleStartConfig()
|
||||
state = STATE_SEND_MY_INFO;
|
||||
}
|
||||
pauseBluetoothLogging = true;
|
||||
spiLock->lock();
|
||||
#if defined(MESHTASTIC_EXCLUDE_FILES_MANIFEST)
|
||||
// Skip the recursive FS walk. Used by platforms whose Zephyr LittleFS
|
||||
// backend can't safely traverse a deep tree (e.g. nRF54L15) and platforms
|
||||
// that don't support OTA browsing — the manifest is only consumed by
|
||||
// companion apps for those flows.
|
||||
filesManifest.clear();
|
||||
releaseFilesManifest(filesManifest);
|
||||
#else
|
||||
filesManifest = getFiles("/", 10);
|
||||
// Manifest is never read on the node-info-only path (STATE_SEND_FILEMANIFEST
|
||||
// short-circuits to sendConfigComplete), so skip the SPI lock + FS walk.
|
||||
if (config_nonce != SPECIAL_NONCE_ONLY_NODES) {
|
||||
bool filesManifestLimited = false;
|
||||
{
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
filesManifest = getFiles("/", FILES_MANIFEST_LEVELS, FILES_MANIFEST_MAX_COUNT, &filesManifestLimited);
|
||||
}
|
||||
if (filesManifestLimited) {
|
||||
LOG_WARN("Got %zu files in manifest (limited to %zu entries/depth %u)", filesManifest.size(),
|
||||
FILES_MANIFEST_MAX_COUNT, static_cast<unsigned>(FILES_MANIFEST_LEVELS));
|
||||
} else {
|
||||
LOG_DEBUG("Got %zu files in manifest", filesManifest.size());
|
||||
}
|
||||
} else {
|
||||
releaseFilesManifest(filesManifest);
|
||||
}
|
||||
#endif
|
||||
spiLock->unlock();
|
||||
LOG_DEBUG("Got %d files in manifest", filesManifest.size());
|
||||
|
||||
LOG_INFO("Start API client config millis=%u", millis());
|
||||
// Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue.
|
||||
@@ -377,8 +401,7 @@ void PhoneAPI::close()
|
||||
replayPhase = REPLAY_PHASE_IDLE;
|
||||
}
|
||||
packetForPhone = NULL;
|
||||
filesManifest.clear();
|
||||
filesManifest.shrink_to_fit();
|
||||
releaseFilesManifest(filesManifest);
|
||||
lastPortNumToRadio.clear();
|
||||
fromRadioNum = 0;
|
||||
config_nonce = 0;
|
||||
@@ -943,7 +966,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// ONLY_NODES variants skip the manifest.
|
||||
if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
config_state = 0;
|
||||
filesManifest.clear();
|
||||
releaseFilesManifest(filesManifest);
|
||||
// Skip to complete packet
|
||||
sendConfigComplete();
|
||||
} else {
|
||||
|
||||
@@ -28,8 +28,11 @@ uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
return precision;
|
||||
}
|
||||
|
||||
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
|
||||
int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
|
||||
{
|
||||
if (precision == 0 || precision >= 32)
|
||||
return coordinate;
|
||||
|
||||
uint32_t coordinateBits = static_cast<uint32_t>(coordinate);
|
||||
uint32_t truncated = coordinateBits & (UINT32_MAX << (32 - precision));
|
||||
|
||||
@@ -39,6 +42,11 @@ static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
|
||||
return static_cast<int32_t>(truncated);
|
||||
}
|
||||
|
||||
int32_t truncateCoordinate(int32_t coordinate, uint8_t precision)
|
||||
{
|
||||
return truncateCoordinate(coordinate, static_cast<uint32_t>(precision));
|
||||
}
|
||||
|
||||
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision)
|
||||
{
|
||||
if (precision == 0) {
|
||||
|
||||
@@ -15,6 +15,12 @@ uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
|
||||
|
||||
// Configured precision, clamped to MAX_POSITION_PRECISION_PUBLIC_KEY when the channel's effective key is publicly decryptable.
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
|
||||
|
||||
// Truncate a single latitude_i/longitude_i to `precision` significant bits, centered in the
|
||||
// resulting grid cell (stable under GPS jitter). precision 0 or >=32 returns the value unchanged.
|
||||
// The return is the coordinate (int32_t); the uint8_t overload only narrows the precision arg.
|
||||
int32_t truncateCoordinate(int32_t coordinate, uint32_t precision);
|
||||
int32_t truncateCoordinate(int32_t coordinate, uint8_t precision);
|
||||
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
|
||||
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
|
||||
bool applyPositionPrecisionForChannel(meshtastic_MeshPacket &packet, uint8_t channelIndex);
|
||||
|
||||
+6
-13
@@ -100,19 +100,12 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
||||
return true;
|
||||
}
|
||||
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
// When router_preserve_hops is enabled, preserve hops for decoded packets that are not
|
||||
// position or telemetry (those have their own exhaust_hop controls).
|
||||
if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled &&
|
||||
moduleConfig.traffic_management.router_preserve_hops && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum != meshtastic_PortNum_POSITION_APP && p->decoded.portnum != meshtastic_PortNum_TELEMETRY_APP) {
|
||||
LOG_DEBUG("Router hop preserved: port=%d from=0x%08x (traffic_management)", p->decoded.portnum, getFrom(p));
|
||||
if (trafficManagementModule) {
|
||||
trafficManagementModule->recordRouterHopPreserved();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
// router_preserve_hops: not suitable right now — removed from config until
|
||||
// the right heuristics for when to preserve vs. exhaust hops are established.
|
||||
// #if HAS_TRAFFIC_MANAGEMENT
|
||||
// if (moduleConfig.has_traffic_management &&
|
||||
// moduleConfig.traffic_management.router_preserve_hops && ...) { ... }
|
||||
// #endif
|
||||
|
||||
// For subsequent hops, preserve hop_limit only when the previous relay is UNAMBIGUOUSLY a favorite
|
||||
// router. The relay_node byte is just the last byte of a 32-bit node number, so on a dense mesh it
|
||||
|
||||
+30
-27
@@ -52,7 +52,8 @@ void StreamAPI::writeStream()
|
||||
do {
|
||||
// Send every packet we can
|
||||
len = getFromRadio(txBuf + HEADER_LEN);
|
||||
emitTxBuffer(len);
|
||||
if (len != 0 && !emitTxBuffer(len))
|
||||
break;
|
||||
} while (len);
|
||||
}
|
||||
}
|
||||
@@ -169,21 +170,36 @@ int32_t StreamAPI::readStream()
|
||||
/**
|
||||
* Send the current txBuffer over our stream
|
||||
*/
|
||||
void StreamAPI::emitTxBuffer(size_t len)
|
||||
bool StreamAPI::writeFrame(uint8_t *buf, size_t len)
|
||||
{
|
||||
if (len != 0) {
|
||||
txBuf[0] = START1;
|
||||
txBuf[1] = START2;
|
||||
txBuf[2] = (len >> 8) & 0xff;
|
||||
txBuf[3] = len & 0xff;
|
||||
if (len == 0 || !canWrite)
|
||||
return false;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
// Serialize stream writes against `emitLogRecord` so a LOG_ firing
|
||||
// mid-packet-emission can't interleave bytes on the wire.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
stream->write(txBuf, totalLen);
|
||||
buf[0] = START1;
|
||||
buf[1] = START2;
|
||||
buf[2] = (len >> 8) & 0xff;
|
||||
buf[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
// Serialize write-readiness checks, writes and write-failure handling
|
||||
// against concurrent stream writes/close.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
if (!canWriteFrame(totalLen))
|
||||
return false;
|
||||
|
||||
size_t written = stream->write(buf, totalLen);
|
||||
if (written == totalLen) {
|
||||
stream->flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
onFrameWriteFailed(totalLen, written);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool StreamAPI::emitTxBuffer(size_t len)
|
||||
{
|
||||
return writeFrame(txBuf, len);
|
||||
}
|
||||
|
||||
void StreamAPI::emitRebooted()
|
||||
@@ -221,20 +237,7 @@ void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src,
|
||||
|
||||
size_t len =
|
||||
pb_encode_to_bytes(txBufLog + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratchLog);
|
||||
if (len != 0) {
|
||||
txBufLog[0] = START1;
|
||||
txBufLog[1] = START2;
|
||||
txBufLog[2] = (len >> 8) & 0xff;
|
||||
txBufLog[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
// Serialize stream writes against `emitTxBuffer` so a packet
|
||||
// emission in flight on another task doesn't interleave bytes
|
||||
// with this log record.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
stream->write(txBufLog, totalLen);
|
||||
stream->flush();
|
||||
}
|
||||
writeFrame(txBufLog, len);
|
||||
}
|
||||
|
||||
/// Hookable to find out when connection changes
|
||||
@@ -249,4 +252,4 @@ void StreamAPI::onConnectionChanged(bool connected)
|
||||
// received a packet in a while
|
||||
powerFSM.trigger(EVENT_SERIAL_DISCONNECTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ class StreamAPI : public PhoneAPI
|
||||
/**
|
||||
* Send the current txBuffer over our stream
|
||||
*/
|
||||
void emitTxBuffer(size_t len);
|
||||
bool emitTxBuffer(size_t len);
|
||||
|
||||
/// Are we allowed to write packets to our output stream (subclasses can turn this off - i.e. SerialConsole)
|
||||
bool canWrite = true;
|
||||
@@ -91,7 +91,12 @@ class StreamAPI : public PhoneAPI
|
||||
/// Low level function to emit a protobuf encapsulated log record
|
||||
void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg);
|
||||
|
||||
virtual bool canWriteFrame(size_t frameLen) { return true; }
|
||||
virtual void onFrameWriteFailed(size_t frameLen, size_t writtenLen) {}
|
||||
|
||||
private:
|
||||
bool writeFrame(uint8_t *buf, size_t len);
|
||||
|
||||
/// Dedicated scratch + tx buffer for LogRecord emission.
|
||||
///
|
||||
/// The main packet emission path (`writeStream` -> `getFromRadio` ->
|
||||
@@ -113,4 +118,4 @@ class StreamAPI : public PhoneAPI
|
||||
meshtastic_FromRadio fromRadioScratchLog = {};
|
||||
uint8_t txBufLog[MAX_STREAM_BUF_SIZE] = {0};
|
||||
concurrency::Lock streamLock;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,12 +22,38 @@ template <typename T> void ServerAPI<T>::close()
|
||||
StreamAPI::close();
|
||||
}
|
||||
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
/// Check the current underlying physical link to see if the client is currently
|
||||
/// connected
|
||||
template <typename T> bool ServerAPI<T>::checkIsConnected()
|
||||
{
|
||||
return client.connected();
|
||||
}
|
||||
|
||||
template <typename T> bool ServerAPI<T>::canWriteFrame(size_t)
|
||||
{
|
||||
// Only a dropped link is a reason to refuse a write up front. A full transmit
|
||||
// buffer (availableForWrite() == 0) is normal backpressure, not a dead socket,
|
||||
// so we must not close the connection on it. A genuinely failed write is
|
||||
// detected after the fact in onFrameWriteFailed().
|
||||
if (!client.connected()) {
|
||||
canWrite = false;
|
||||
enabled = false;
|
||||
LOG_WARN("TCP client disconnected before write, closing API service");
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> void ServerAPI<T>::onFrameWriteFailed(size_t frameLen, size_t writtenLen)
|
||||
{
|
||||
canWrite = false;
|
||||
enabled = false;
|
||||
LOG_WARN("TCP client write short (%lu/%lu bytes), closing API service", (unsigned long)writtenLen, (unsigned long)frameLen);
|
||||
close();
|
||||
}
|
||||
|
||||
template <class T> int32_t ServerAPI<T>::runOnce()
|
||||
{
|
||||
if (client.connected()) {
|
||||
@@ -95,4 +121,4 @@ template <class T, class U> int32_t APIServerPort<T, U>::runOnce()
|
||||
waitTime = 100;
|
||||
#endif
|
||||
return 100; // only check occasionally for incoming connections
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ template <class T> class ServerAPI : public StreamAPI, private concurrency::OSTh
|
||||
/// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the board to
|
||||
/// stay in the POWERED state to prevent disabling wifi)
|
||||
virtual void onConnectionChanged(bool connected) override {}
|
||||
virtual bool canWriteFrame(size_t frameLen) override;
|
||||
virtual void onFrameWriteFailed(size_t frameLen, size_t writtenLen) override;
|
||||
|
||||
virtual int32_t runOnce() override; // Check for dropped client connections
|
||||
};
|
||||
|
||||
@@ -452,7 +452,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
||||
#define meshtastic_BackupPreferences_size 2432
|
||||
#define meshtastic_BackupPreferences_size 2410
|
||||
#define meshtastic_ChannelFile_size 718
|
||||
#define meshtastic_DeviceState_size 1944
|
||||
#define meshtastic_NodeEnvironmentEntry_size 170
|
||||
|
||||
@@ -206,7 +206,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
|
||||
#define meshtastic_LocalConfig_size 757
|
||||
#define meshtastic_LocalModuleConfig_size 820
|
||||
#define meshtastic_LocalModuleConfig_size 798
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -232,34 +232,23 @@ typedef struct _meshtastic_ModuleConfig_PaxcounterConfig {
|
||||
/* Config for the Traffic Management module.
|
||||
Provides packet inspection and traffic shaping to help reduce channel utilization */
|
||||
typedef struct _meshtastic_ModuleConfig_TrafficManagementConfig {
|
||||
/* Master enable for traffic management module */
|
||||
bool enabled;
|
||||
/* Enable position deduplication to drop redundant position broadcasts */
|
||||
bool position_dedup_enabled;
|
||||
/* Number of bits of precision for position deduplication (0-32) */
|
||||
uint32_t position_precision_bits;
|
||||
/* Minimum interval in seconds between position updates from the same node */
|
||||
/* Minimum interval in seconds between position updates from the same node.
|
||||
A non-zero value implicitly enables the suppression window; 0 disables it. */
|
||||
uint32_t position_min_interval_secs;
|
||||
/* Enable direct response to NodeInfo requests from local cache */
|
||||
bool nodeinfo_direct_response;
|
||||
/* Minimum hop distance from requestor before responding to NodeInfo requests */
|
||||
/* Maximum hop distance from the requestor at which direct NodeInfo responses
|
||||
are served from the local cache. A non-zero value implicitly enables direct
|
||||
response; 0 disables it. */
|
||||
uint32_t nodeinfo_direct_response_max_hops;
|
||||
/* Enable per-node rate limiting to throttle chatty nodes */
|
||||
bool rate_limit_enabled;
|
||||
/* Time window in seconds for rate limiting calculations */
|
||||
/* Time window in seconds for per-node rate limiting.
|
||||
A non-zero value implicitly enables rate limiting; 0 disables it. */
|
||||
uint32_t rate_limit_window_secs;
|
||||
/* Maximum packets allowed per node within the rate limit window */
|
||||
/* Maximum packets allowed per node within the rate limit window.
|
||||
A non-zero value implicitly enables rate limiting; 0 disables it. */
|
||||
uint32_t rate_limit_max_packets;
|
||||
/* Enable dropping of unknown/undecryptable packets per rate_limit_window_secs */
|
||||
bool drop_unknown_enabled;
|
||||
/* Number of unknown packets before dropping from a node */
|
||||
/* Maximum unknown/undecryptable packets per rate window before the source
|
||||
is dropped. A non-zero value implicitly enables unknown-packet filtering;
|
||||
0 disables it. */
|
||||
uint32_t unknown_packet_threshold;
|
||||
/* Set hop_limit to 0 for relayed telemetry broadcasts (own packets unaffected) */
|
||||
bool exhaust_hop_telemetry;
|
||||
/* Set hop_limit to 0 for relayed position broadcasts (own packets unaffected) */
|
||||
bool exhaust_hop_position;
|
||||
/* Preserve hop_limit for router-to-router traffic */
|
||||
bool router_preserve_hops;
|
||||
} meshtastic_ModuleConfig_TrafficManagementConfig;
|
||||
|
||||
/* Serial Config */
|
||||
@@ -588,7 +577,7 @@ extern "C" {
|
||||
#define meshtastic_ModuleConfig_DetectionSensorConfig_init_default {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0}
|
||||
#define meshtastic_ModuleConfig_AudioConfig_init_default {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_PaxcounterConfig_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_init_default {0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0}
|
||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
|
||||
@@ -607,7 +596,7 @@ extern "C" {
|
||||
#define meshtastic_ModuleConfig_DetectionSensorConfig_init_zero {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0}
|
||||
#define meshtastic_ModuleConfig_AudioConfig_init_zero {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_PaxcounterConfig_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_init_zero {0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0}
|
||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
|
||||
@@ -656,20 +645,11 @@ extern "C" {
|
||||
#define meshtastic_ModuleConfig_PaxcounterConfig_paxcounter_update_interval_tag 2
|
||||
#define meshtastic_ModuleConfig_PaxcounterConfig_wifi_threshold_tag 3
|
||||
#define meshtastic_ModuleConfig_PaxcounterConfig_ble_threshold_tag 4
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_enabled_tag 1
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_position_dedup_enabled_tag 2
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_position_precision_bits_tag 3
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_position_min_interval_secs_tag 4
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_tag 5
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_max_hops_tag 6
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_enabled_tag 7
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_window_secs_tag 8
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_max_packets_tag 9
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_drop_unknown_enabled_tag 10
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_unknown_packet_threshold_tag 11
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_telemetry_tag 12
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_position_tag 13
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_router_preserve_hops_tag 14
|
||||
#define meshtastic_ModuleConfig_SerialConfig_enabled_tag 1
|
||||
#define meshtastic_ModuleConfig_SerialConfig_echo_tag 2
|
||||
#define meshtastic_ModuleConfig_SerialConfig_rxd_tag 3
|
||||
@@ -867,20 +847,11 @@ X(a, STATIC, SINGULAR, INT32, ble_threshold, 4)
|
||||
#define meshtastic_ModuleConfig_PaxcounterConfig_DEFAULT NULL
|
||||
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, enabled, 1) \
|
||||
X(a, STATIC, SINGULAR, BOOL, position_dedup_enabled, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, position_precision_bits, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, position_min_interval_secs, 4) \
|
||||
X(a, STATIC, SINGULAR, BOOL, nodeinfo_direct_response, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, nodeinfo_direct_response_max_hops, 6) \
|
||||
X(a, STATIC, SINGULAR, BOOL, rate_limit_enabled, 7) \
|
||||
X(a, STATIC, SINGULAR, UINT32, rate_limit_window_secs, 8) \
|
||||
X(a, STATIC, SINGULAR, UINT32, rate_limit_max_packets, 9) \
|
||||
X(a, STATIC, SINGULAR, BOOL, drop_unknown_enabled, 10) \
|
||||
X(a, STATIC, SINGULAR, UINT32, unknown_packet_threshold, 11) \
|
||||
X(a, STATIC, SINGULAR, BOOL, exhaust_hop_telemetry, 12) \
|
||||
X(a, STATIC, SINGULAR, BOOL, exhaust_hop_position, 13) \
|
||||
X(a, STATIC, SINGULAR, BOOL, router_preserve_hops, 14)
|
||||
X(a, STATIC, SINGULAR, UINT32, unknown_packet_threshold, 11)
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_CALLBACK NULL
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_DEFAULT NULL
|
||||
|
||||
@@ -1053,7 +1024,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
|
||||
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
|
||||
#define meshtastic_ModuleConfig_TAKConfig_size 4
|
||||
#define meshtastic_ModuleConfig_TelemetryConfig_size 50
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
|
||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 30
|
||||
#define meshtastic_ModuleConfig_size 227
|
||||
#define meshtastic_RemoteHardwarePin_size 21
|
||||
|
||||
|
||||
@@ -99,23 +99,20 @@ static inline int get_max_num_nodes()
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
#define MAX_NUM_NODES 250 // native host: no flash/RAM constraint; match the ESP32-S3 top tier
|
||||
#else
|
||||
#define MAX_NUM_NODES 120 // nRF52840 (28 KB LittleFS) and generic ESP32
|
||||
#define MAX_NUM_NODES 120 // nRF52840 and generic ESP32 (inc. ESP32C3 etc.)
|
||||
#endif // platform
|
||||
#endif // MAX_NUM_NODES
|
||||
|
||||
/// Per-map cap (position/telemetry/environment/status): only the freshest
|
||||
/// MAX_SATELLITE_NODES nodes keep satellite payloads, the rest just the
|
||||
/// NodeInfoLite header. RAM-bound: the four maps live in internal SRAM (not
|
||||
/// PSRAM). PSRAM-equipped ESP32-S3 (and native) keep the full 250; other ESP32
|
||||
/// (no-PSRAM, incl. S3) get 80 -- ~32 KB worst case, affordable now the warm
|
||||
/// tier is trimmed; nRF52840 and other tight parts stay at 40.
|
||||
/// NodeInfoLite header. RAM-bound (the maps are internal-SRAM, not PSRAM), so
|
||||
/// flash-rich hosts get a cap >= their hot store (satellites for every node, as
|
||||
/// before the cap existed) while constrained parts stay at 40.
|
||||
#ifndef MAX_SATELLITE_NODES
|
||||
#if defined(ARCH_PORTDUINO) || (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM))
|
||||
#define MAX_SATELLITE_NODES 250 // native / PSRAM-equipped ESP32-S3
|
||||
#elif defined(ARCH_ESP32)
|
||||
#define MAX_SATELLITE_NODES 80 // no-PSRAM ESP32 (incl. ESP32-S3)
|
||||
#if (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)) || defined(ARCH_PORTDUINO)
|
||||
#define MAX_SATELLITE_NODES 250
|
||||
#else
|
||||
#define MAX_SATELLITE_NODES 40 // nRF52840 (28 KB LittleFS) and other constrained parts
|
||||
#define MAX_SATELLITE_NODES 40 // nRF52840, generic ESP32, and ESP32-S3 without PSRAM
|
||||
#endif // platform
|
||||
#endif // MAX_SATELLITE_NODES
|
||||
|
||||
@@ -130,14 +127,21 @@ static inline int get_max_num_nodes()
|
||||
// architecture.h via configuration.h) isn't defined this early in every include
|
||||
// chain. Backed by the raw-flash ring below LittleFS — see WarmNodeStore.h.
|
||||
#define WARM_NODE_COUNT 200
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)
|
||||
#define WARM_NODE_COUNT 2000 // ESP32-S3 with PSRAM (external); warm.dat ~80 KB
|
||||
#elif (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)) || defined(ARCH_PORTDUINO)
|
||||
#define WARM_NODE_COUNT 2000 // PSRAM-equipped ESP32-S3 / native host; warm cache in PSRAM (~80 KB)
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
#define WARM_NODE_COUNT 150 // 512 KB+ SRAM, no PSRAM (S3/C6/P4): ~6 KB heap (#10705)
|
||||
#elif defined(ARCH_ESP32)
|
||||
#define WARM_NODE_COUNT 100 // classic ESP32 (520 KB) / S2 (320 KB) / C3 (400 KB): tightest free heap w/ BLE+WiFi, ~4 KB (#10705)
|
||||
#elif defined(ARCH_RP2040)
|
||||
#define WARM_NODE_COUNT 150 // RP2040 (264 KB) / RP2350 (520 KB): bounded so warm.dat write fits the 8s watchdog (#10746)
|
||||
#else
|
||||
// generic ESP32 and no-PSRAM ESP32-S3: ~12.5 KB in internal heap (calloc fallback in
|
||||
// WarmNodeStore), leaving room for the BLE controller. PSRAM-equipped S3 takes the 2000 case above.
|
||||
#define WARM_NODE_COUNT 320
|
||||
#endif // platform
|
||||
#endif // WARM_NODE_COUNT
|
||||
// nRF52840 is handled explicitly above (200, raw-flash ring). Any other nRF52 (non-XXAA) and any
|
||||
// future non-ESP32/non-RP LittleFS part fall through to this 320 default — flag for review if such a
|
||||
// RAM-constrained nRF52 target is ever added.
|
||||
#define WARM_NODE_COUNT 320 // other LittleFS-backed parts (e.g. non-nRF52840 nRF52)
|
||||
#endif // platform
|
||||
#endif // WARM_NODE_COUNT
|
||||
|
||||
/// Max number of channels allowed
|
||||
#define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0]))
|
||||
@@ -157,18 +161,28 @@ static inline int get_max_num_nodes()
|
||||
#ifdef ARCH_STM32WL
|
||||
#define HAS_VARIABLE_HOPS 0
|
||||
#endif
|
||||
|
||||
#ifndef HAS_VARIABLE_HOPS
|
||||
#define HAS_VARIABLE_HOPS 1
|
||||
#endif
|
||||
|
||||
// Cache size for traffic management (number of nodes to track)
|
||||
// Can be overridden per-variant based on available memory
|
||||
// Can be overridden per-variant by defining before this header is included.
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000
|
||||
#else
|
||||
#if !HAS_TRAFFIC_MANAGEMENT
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 0
|
||||
#endif // HAS_TRAFFIC_MANAGEMENT
|
||||
#elif (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)) || defined(ARCH_PORTDUINO)
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 // PSRAM-equipped ESP32-S3 / native host
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 500 // 512 KB+ SRAM, no PSRAM (S3/C6/P4): ~5 KB heap (#10705)
|
||||
#elif defined(ARCH_ESP32)
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 400 // classic ESP32 / S2 / C3: tightest free heap, ~4 KB (#10705)
|
||||
#else
|
||||
// nRF52 (incl. nRF52840) and RP2040/RP2350 fall through here — there is no nRF/RP branch above,
|
||||
// by design. These parts have no ESP32-style WiFi+BLE coexistence eating the heap, so the larger
|
||||
// 1000-entry (~10 KB) cache fits: nRF52840 is BLE-only on 256 KB RAM; RP2040/RP2350 have 264/520 KB.
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000 // nRF52 / RP2040 / RP2350 / other non-ESP32
|
||||
#endif
|
||||
#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
|
||||
/// helper function for encoding a record as a protobuf, any failures to encode are fatal and we will panic
|
||||
|
||||
@@ -481,6 +481,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||
} else if (mp.from == 0) { // local request from the phone — tell the user why it didn't take
|
||||
sendWarning(NodeDB::PROTECTED_CAP_WARN_FMT, "favorite", r->set_favorite_node, MAX_NUM_NODES - 2);
|
||||
} else {
|
||||
LOG_WARN("Remote set_favorite_node for 0x%x refused: protected-node cap", r->set_favorite_node);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -508,6 +510,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
} else if (mp.from == 0) { // local request from the phone — tell the user why it didn't take
|
||||
sendWarning(NodeDB::PROTECTED_CAP_WARN_FMT, "ignore", r->set_ignored_node, MAX_NUM_NODES - 2);
|
||||
} else {
|
||||
LOG_WARN("Remote set_ignored_node for 0x%x refused: protected-node cap", r->set_ignored_node);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -128,8 +128,7 @@ void setupModules()
|
||||
#endif
|
||||
|
||||
#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT
|
||||
// Instantiate only when enabled to avoid extra memory use and background work.
|
||||
if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled) {
|
||||
if (moduleConfig.has_traffic_management) {
|
||||
trafficManagementModule = new TrafficManagementModule();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -70,7 +70,7 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
||||
if (config.position.fixed_position) {
|
||||
LOG_DEBUG("Ignore incoming position update from myself except for time, because position.fixed_position is true");
|
||||
|
||||
#if defined(T_WATCH_S3) || defined(T_WATCH_ULTRA)
|
||||
#ifdef T_WATCH_S3
|
||||
// Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the
|
||||
// client device here
|
||||
if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) {
|
||||
@@ -432,6 +432,42 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha
|
||||
|
||||
#define RUNONCE_INTERVAL 5000;
|
||||
|
||||
bool PositionModule::positionUnchangedSinceLastSend(const meshtastic_PositionLite &selfPos, bool useConfiguredPrecision)
|
||||
{
|
||||
if (lastGpsLatitude == 0 && lastGpsLongitude == 0)
|
||||
return false; // no prior broadcast to compare against
|
||||
|
||||
// Broadcast channel = the one sendOurPosition() would pick (first with non-zero on-wire
|
||||
// precision). Default nodes gauge movement at that on-wire (public-clamped) resolution;
|
||||
// trackers use their own configured (unclamped) precision so finer moves still count.
|
||||
uint32_t precisionBits = 0;
|
||||
for (uint8_t ch = 0; ch < 8; ch++) {
|
||||
if (getPositionPrecisionForChannel(ch) == 0)
|
||||
continue;
|
||||
precisionBits =
|
||||
useConfiguredPrecision ? getPositionPrecisionForChannel(channels.getByIndex(ch)) : getPositionPrecisionForChannel(ch);
|
||||
break;
|
||||
}
|
||||
|
||||
return positionWithinPrecisionCell(selfPos.latitude_i, selfPos.longitude_i, lastGpsLatitude, lastGpsLongitude, precisionBits);
|
||||
}
|
||||
|
||||
bool PositionModule::positionWithinPrecisionCell(int32_t aLat, int32_t aLon, int32_t bLat, int32_t bLon, uint32_t precision)
|
||||
{
|
||||
if (precision == 0 || precision >= 32)
|
||||
return false; // sharing disabled or full precision: no coarse cell to hold within
|
||||
|
||||
return truncateCoordinate(aLat, precision) == truncateCoordinate(bLat, precision) &&
|
||||
truncateCoordinate(aLon, precision) == truncateCoordinate(bLon, precision);
|
||||
}
|
||||
|
||||
uint32_t PositionModule::effectiveBroadcastIntervalMs(uint32_t configuredIntervalMs, bool stationary, uint32_t stationaryFloorMs)
|
||||
{
|
||||
if (stationary && stationaryFloorMs > configuredIntervalMs)
|
||||
return stationaryFloorMs;
|
||||
return configuredIntervalMs;
|
||||
}
|
||||
|
||||
int32_t PositionModule::runOnce()
|
||||
{
|
||||
if (sleepOnNextExecution == true) {
|
||||
@@ -458,7 +494,23 @@ int32_t PositionModule::runOnce()
|
||||
|
||||
bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot();
|
||||
|
||||
if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) {
|
||||
// Hold to the 12h floor when fixed_position (every role: pinning yourself forfeits the
|
||||
// exception) or when stationary. A real move still goes out early via smart-broadcast below.
|
||||
// Not-fixed exceptions: lost-and-found broadcasts freely; trackers judge movement at their
|
||||
// own (unclamped) precision rather than the on-wire one (useConfiguredPrecision).
|
||||
const auto role = config.device.role;
|
||||
bool stationary = config.position.fixed_position;
|
||||
if (!stationary && role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && nodeDB->hasValidPosition(node)) {
|
||||
const bool isTracker =
|
||||
IS_ONE_OF(role, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_TAK_TRACKER);
|
||||
meshtastic_PositionLite selfPos;
|
||||
if (nodeDB->copyNodePosition(node->num, selfPos))
|
||||
stationary = positionUnchangedSinceLastSend(selfPos, /*useConfiguredPrecision=*/isTracker);
|
||||
}
|
||||
uint32_t effectiveIntervalMs =
|
||||
effectiveBroadcastIntervalMs(intervalMs, stationary, (uint32_t)default_position_stationary_broadcast_secs * 1000UL);
|
||||
|
||||
if (lastGpsSend == 0 || msSinceLastSend >= effectiveIntervalMs) {
|
||||
if (waitingForFreshPosition) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Skip initial position send; no fresh position since boot");
|
||||
|
||||
@@ -38,6 +38,14 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
|
||||
|
||||
void handleNewPosition();
|
||||
|
||||
// Pure broadcast-policy helpers, split out so they're unit-testable without the module.
|
||||
// True when two coordinates truncate to the same precision cell (so a re-broadcast would be a
|
||||
// duplicate). precision 0 or >=32 returns false: no coarse cell to hold within, never suppress.
|
||||
static bool positionWithinPrecisionCell(int32_t aLat, int32_t aLon, int32_t bLat, int32_t bLon, uint32_t precision);
|
||||
// Effective min interval: stationary positions are held to stationaryFloorMs (when that is the
|
||||
// longer of the two); otherwise the normal configured interval.
|
||||
static uint32_t effectiveBroadcastIntervalMs(uint32_t configuredIntervalMs, bool stationary, uint32_t stationaryFloorMs);
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
|
||||
@@ -57,6 +65,12 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
|
||||
private:
|
||||
meshtastic_MeshPacket *allocPositionPacket();
|
||||
struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition);
|
||||
// True when our position is unchanged since the last broadcast: it truncates to the same
|
||||
// precision grid cell, so re-sending would be a duplicate that traffic management dedups
|
||||
// downstream anyway. Used to hold stationary broadcasts to a 12h floor. useConfiguredPrecision
|
||||
// gauges movement at our own configured (unclamped) precision rather than the on-wire
|
||||
// (public-clamped) precision — trackers report finer movement.
|
||||
bool positionUnchangedSinceLastSend(const meshtastic_PositionLite &selfPos, bool useConfiguredPrecision);
|
||||
meshtastic_MeshPacket *allocAtakPli();
|
||||
void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false);
|
||||
uint32_t precision;
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
|
||||
#include "Channels.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "Router.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "airtime.h"
|
||||
@@ -13,6 +15,7 @@
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
#include <Arduino.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#define TM_LOG_DEBUG(fmt, ...) LOG_DEBUG("[TM] " fmt, ##__VA_ARGS__)
|
||||
@@ -27,7 +30,6 @@ namespace
|
||||
{
|
||||
|
||||
constexpr uint32_t kMaintenanceIntervalMs = 60 * 1000UL; // Cache cleanup interval
|
||||
constexpr uint32_t kUnknownResetMs = 60 * 1000UL; // Unknown packet window
|
||||
|
||||
// NodeInfo direct response: enforced maximum hops by device role
|
||||
// Both use maxHops logic (respond when hopsAway <= threshold)
|
||||
@@ -47,6 +49,17 @@ uint32_t secsToMs(uint32_t secs)
|
||||
return static_cast<uint32_t>(ms);
|
||||
}
|
||||
|
||||
// Advertised role of the originating node (from NodeDB), or CLIENT (no exception) if unknown.
|
||||
// Position filtering grants two role exceptions: trackers may refresh duplicates hourly, and
|
||||
// lost-and-found is throttled only to the shortest dedup window. Both are still subject to
|
||||
// the channel-precision ceiling in alterReceived().
|
||||
meshtastic_Config_DeviceConfig_Role originRole(NodeNum from)
|
||||
{
|
||||
// Resolve via NodeDB: hot store (with user) → warm-tier cached role → CLIENT. The
|
||||
// warm fallback keeps role exceptions firing for trackers/etc. aged out of the hot store.
|
||||
return nodeDB ? nodeDB->getNodeRole(from) : meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp precision to a valid dedup range.
|
||||
* Invalid values use the module default precision.
|
||||
@@ -64,57 +77,6 @@ uint8_t sanitizePositionPrecision(uint8_t precision)
|
||||
return 32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a timestamp is within a time window.
|
||||
* Handles wrap-around correctly using unsigned subtraction.
|
||||
*/
|
||||
bool isWithinWindow(uint32_t nowMs, uint32_t startMs, uint32_t intervalMs)
|
||||
{
|
||||
if (intervalMs == 0 || startMs == 0)
|
||||
return false;
|
||||
return (nowMs - startMs) < intervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide an 8-bit relative timestamp back by a wall-clock slab during epoch rebase.
|
||||
*
|
||||
* Entries older than the slab clamp to 0 (then reclaimed by the maintenance sweep);
|
||||
* live entries keep their reconstructed age minus a sub-tick remainder. Each field
|
||||
* slides by its own resolution's worth of ticks, so a single slab covers all three.
|
||||
*/
|
||||
inline void slideRelativeTime(uint8_t &ticks, uint32_t slabMs, uint16_t resolutionSecs)
|
||||
{
|
||||
if (ticks == 0 || resolutionSecs == 0)
|
||||
return;
|
||||
uint32_t dec = slabMs / (static_cast<uint32_t>(resolutionSecs) * 1000UL);
|
||||
ticks = (ticks > dec) ? static_cast<uint8_t>(ticks - dec) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate lat/lon to specified precision for position deduplication.
|
||||
*
|
||||
* The truncation works by masking off lower bits and rounding to the center
|
||||
* of the resulting grid cell. This creates a stable truncated value even
|
||||
* when GPS jitter causes small coordinate changes.
|
||||
*
|
||||
* @param value Raw latitude_i or longitude_i from position
|
||||
* @param precision Number of significant bits to keep (0-32)
|
||||
* @return Truncated and centered coordinate value
|
||||
*/
|
||||
int32_t truncateLatLon(int32_t value, uint8_t precision)
|
||||
{
|
||||
if (precision == 0 || precision >= 32)
|
||||
return value;
|
||||
|
||||
// Create mask to zero out lower bits
|
||||
uint32_t mask = UINT32_MAX << (32 - precision);
|
||||
uint32_t truncated = static_cast<uint32_t>(value) & mask;
|
||||
|
||||
// Add half the truncation step to center in the grid cell
|
||||
truncated += (1u << (31 - precision));
|
||||
return static_cast<int32_t>(truncated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saturating increment for uint8_t counters.
|
||||
* Prevents overflow by capping at UINT8_MAX (255).
|
||||
@@ -176,23 +138,10 @@ TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManageme
|
||||
encryptedOk = true; // Can process encrypted packets
|
||||
stats = meshtastic_TrafficManagementStats_init_zero;
|
||||
|
||||
// Initialize rolling epoch for relative timestamps
|
||||
cacheEpochMs = millis();
|
||||
|
||||
// Calculate adaptive time resolutions from config (config changes require reboot)
|
||||
// Resolution = max(60, min(339, interval/2)) for ~24 hour range with good precision
|
||||
posTimeResolution = calcTimeResolution(Default::getConfiguredOrDefault(
|
||||
moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs));
|
||||
rateTimeResolution = calcTimeResolution(moduleConfig.traffic_management.rate_limit_window_secs);
|
||||
unknownTimeResolution = calcTimeResolution(kUnknownResetMs / 1000); // ~5 min default
|
||||
|
||||
const auto &cfg = moduleConfig.traffic_management;
|
||||
TM_LOG_INFO("Enabled: pos_dedup=%d nodeinfo_resp=%d rate_limit=%d drop_unknown=%d exhaust_telem=%d exhaust_pos=%d "
|
||||
"preserve_hops=%d",
|
||||
cfg.position_dedup_enabled, cfg.nodeinfo_direct_response, cfg.rate_limit_enabled, cfg.drop_unknown_enabled,
|
||||
cfg.exhaust_hop_telemetry, cfg.exhaust_hop_position, cfg.router_preserve_hops);
|
||||
TM_LOG_DEBUG("Time resolutions: pos=%us, rate=%us, unknown=%us", posTimeResolution, rateTimeResolution,
|
||||
unknownTimeResolution);
|
||||
TM_LOG_INFO("Config: nodeinfo_max_hops=%u rate_window=%us rate_max=%u unknown_thresh=%u pos_interval=%us",
|
||||
cfg.nodeinfo_direct_response_max_hops, cfg.rate_limit_window_secs, cfg.rate_limit_max_packets,
|
||||
cfg.unknown_packet_threshold, cfg.position_min_interval_secs);
|
||||
|
||||
// Allocate unified cache (10 bytes/entry for all platforms)
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
@@ -278,9 +227,9 @@ void TrafficManagementModule::resetStats()
|
||||
|
||||
void TrafficManagementModule::recordRouterHopPreserved()
|
||||
{
|
||||
if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled)
|
||||
return;
|
||||
incrementStat(&stats.router_hops_preserved);
|
||||
// router_preserve_hops: not suitable right now — removed from config until
|
||||
// the right heuristic for when to preserve vs. exhaust is clearer.
|
||||
(void)stats.router_hops_preserved;
|
||||
}
|
||||
|
||||
void TrafficManagementModule::incrementStat(uint32_t *field)
|
||||
@@ -313,6 +262,18 @@ TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findEntry(N
|
||||
#endif
|
||||
}
|
||||
|
||||
int TrafficManagementModule::peekCachedRole(NodeNum node)
|
||||
{
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0
|
||||
(void)node;
|
||||
return -1;
|
||||
#else
|
||||
concurrency::LockGuard guard(&cacheLock);
|
||||
const UnifiedCacheEntry *entry = findEntry(node);
|
||||
return entry ? static_cast<int>(entry->getCachedRole()) : -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create an entry for the given node.
|
||||
*
|
||||
@@ -326,6 +287,51 @@ TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findEntry(N
|
||||
* @param isNew Set to true if a new entry was created
|
||||
* @return Pointer to entry, or nullptr if the cache is unavailable
|
||||
*/
|
||||
// Sender-role resolution for the position hot path. The tier-3 cache is authoritative
|
||||
// here and is kept fresh by updateCachedRoleFromNodeInfo() — i.e. updated at the same
|
||||
// time NodeDB learns a role, not re-derived on every packet. We only fall back to a
|
||||
// NodeDB scan (tiers 1+2) the first time we start tracking a node, to seed the cache so
|
||||
// a resident special-role node is correct from its very first position. Thereafter the
|
||||
// read is O(1) and survives the node aging out of both NodeDB stores.
|
||||
meshtastic_Config_DeviceConfig_Role TrafficManagementModule::resolveSenderRole(NodeNum from, UnifiedCacheEntry *entry, bool isNew)
|
||||
{
|
||||
if (!entry)
|
||||
return originRole(from);
|
||||
if (isNew) {
|
||||
// First time tracking this node: seed tier 3 from NodeDB (hot → warm). Stores
|
||||
// CLIENT (0) too, which simply reads back as "no exception".
|
||||
const meshtastic_Config_DeviceConfig_Role role = originRole(from);
|
||||
entry->setCachedRole(static_cast<uint8_t>(std::min(15, static_cast<int>(role))));
|
||||
return role;
|
||||
}
|
||||
// Established entry: trust the cached role (refreshed on NodeInfo). No NodeDB scan.
|
||||
return static_cast<meshtastic_Config_DeviceConfig_Role>(entry->getCachedRole());
|
||||
}
|
||||
|
||||
// Refresh the tier-3 role cache from an observed NodeInfo — the same event that updates
|
||||
// NodeDB's role — so role changes (including demotion back to CLIENT) are picked up
|
||||
// without scanning NodeDB on the position hot path. Role is read straight from the
|
||||
// packet's User payload (authoritative regardless of module ordering). Only updates nodes
|
||||
// we already track (findEntry, no create) so NodeInfo from non-position nodes can't pollute
|
||||
// the cache; the role rides along with the node's existing position/rate/unknown state.
|
||||
void TrafficManagementModule::updateCachedRoleFromNodeInfo(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
if (mp.decoded.payload.size == 0)
|
||||
return;
|
||||
meshtastic_User user = meshtastic_User_init_zero;
|
||||
if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &user))
|
||||
return;
|
||||
|
||||
concurrency::LockGuard guard(&cacheLock);
|
||||
UnifiedCacheEntry *entry = findEntry(getFrom(&mp));
|
||||
if (entry)
|
||||
entry->setCachedRole(static_cast<uint8_t>(std::min(15, static_cast<int>(user.role))));
|
||||
#else
|
||||
(void)mp;
|
||||
#endif
|
||||
}
|
||||
|
||||
TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findOrCreateEntry(NodeNum node, bool *isNew)
|
||||
{
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0
|
||||
@@ -341,7 +347,7 @@ TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findOrCreat
|
||||
|
||||
UnifiedCacheEntry *empty = nullptr;
|
||||
UnifiedCacheEntry *victim = nullptr;
|
||||
bool victimHasHop = true;
|
||||
bool leastPreferredVictim = true;
|
||||
uint8_t victimRecency = UINT8_MAX;
|
||||
|
||||
for (uint16_t i = 0; i < cacheSize(); i++) {
|
||||
@@ -355,15 +361,28 @@ TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findOrCreat
|
||||
}
|
||||
if (empty)
|
||||
continue; // an empty slot beats any victim; stop scoring
|
||||
const bool hasHop = e.next_hop != 0;
|
||||
uint8_t recency = e.pos_time;
|
||||
if (e.rate_time > recency)
|
||||
recency = e.rate_time;
|
||||
if (e.unknown_time > recency)
|
||||
recency = e.unknown_time;
|
||||
if (!victim || (hasHop == victimHasHop ? recency < victimRecency : !hasHop)) {
|
||||
// "Preferred" entries are evicted last: a confirmed next-hop hint (routing overflow
|
||||
// store) or a cached special (non-CLIENT) role (tracker / lost-and-found / router).
|
||||
// Both are the long-tail state this cache exists to retain.
|
||||
const bool preferred = e.next_hop != 0 || e.getCachedRole() != meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
// Age in pos-ticks (8-bit modular, wraps correctly). Entries with no
|
||||
// pos state (pos_time==0) score as maximally old (age=currentPosTick()).
|
||||
const uint8_t nowPosTick = currentPosTick();
|
||||
const uint8_t posAge = static_cast<uint8_t>(nowPosTick - e.pos_time);
|
||||
// Blend in rate/unknown ages scaled to pos-tick units (coarser = conservative).
|
||||
const uint8_t rateAgePosScale =
|
||||
static_cast<uint8_t>(static_cast<uint8_t>((currentRateTick() - e.getRateTime()) & 0x0F) * 5 / 3);
|
||||
const uint8_t unknownAgePosScale =
|
||||
static_cast<uint8_t>(static_cast<uint8_t>((currentUnknownTick() - e.getUnknownTime()) & 0x0F) / 6);
|
||||
uint8_t recencyAge = posAge;
|
||||
if (e.getRateCount() != 0 && rateAgePosScale > recencyAge)
|
||||
recencyAge = rateAgePosScale;
|
||||
if (e.getUnknownCount() != 0 && unknownAgePosScale > recencyAge)
|
||||
recencyAge = unknownAgePosScale;
|
||||
const uint8_t recency = static_cast<uint8_t>(UINT8_MAX - recencyAge);
|
||||
if (!victim || (preferred == leastPreferredVictim ? recency < victimRecency : !preferred)) {
|
||||
victim = &e;
|
||||
victimHasHop = hasHop;
|
||||
leastPreferredVictim = preferred;
|
||||
victimRecency = recency;
|
||||
}
|
||||
}
|
||||
@@ -417,7 +436,7 @@ TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findOrCr
|
||||
NodeInfoPayloadEntry *empty = nullptr;
|
||||
NodeInfoPayloadEntry *lru = nullptr;
|
||||
uint32_t lruAge = 0;
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = clockMs();
|
||||
|
||||
for (uint16_t i = 0; i < nodeInfoTargetEntries(); i++) {
|
||||
NodeInfoPayloadEntry &e = nodeInfoPayload[i];
|
||||
@@ -493,7 +512,7 @@ void TrafficManagementModule::cacheNodeInfoPacket(const meshtastic_MeshPacket &m
|
||||
// richer context than "just the user protobuf" when PSRAM is present.
|
||||
// This path is intentionally independent from NodeInfoModule/NodeDB.
|
||||
entry->user = user;
|
||||
entry->lastObservedMs = millis();
|
||||
entry->lastObservedMs = clockMs();
|
||||
entry->lastObservedRxTime = mp.rx_time;
|
||||
entry->sourceChannel = mp.channel;
|
||||
entry->hasDecodedBitfield = mp.decoded.has_bitfield;
|
||||
@@ -570,11 +589,11 @@ void TrafficManagementModule::clearNextHop(NodeNum dest)
|
||||
#endif
|
||||
}
|
||||
|
||||
void TrafficManagementModule::preloadNextHopsFromNodeDB()
|
||||
bool TrafficManagementModule::preloadNextHopsFromNodeDB()
|
||||
{
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
if (!cache || !nodeDB)
|
||||
return;
|
||||
return false; // prerequisites not ready yet — caller should retry on a later pass
|
||||
|
||||
uint16_t seeded = 0;
|
||||
concurrency::LockGuard guard(&cacheLock);
|
||||
@@ -594,6 +613,9 @@ void TrafficManagementModule::preloadNextHopsFromNodeDB()
|
||||
}
|
||||
|
||||
TM_LOG_INFO("Preloaded %u next-hop hints from NodeDB", static_cast<unsigned>(seeded));
|
||||
return true;
|
||||
#else
|
||||
return true; // nothing to preload on a cache-less build; don't keep retrying
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -601,59 +623,11 @@ void TrafficManagementModule::preloadNextHopsFromNodeDB()
|
||||
// Epoch Management
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Reset the timestamp epoch when relative offsets approach overflow.
|
||||
*
|
||||
* Called when epoch age exceeds ~19 hours (approaching 8-bit minute overflow).
|
||||
* Invalidates all cached per-node traffic state.
|
||||
*/
|
||||
void TrafficManagementModule::resetEpoch(uint32_t nowMs)
|
||||
void TrafficManagementModule::flushCache()
|
||||
{
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
TM_LOG_DEBUG("Resetting cache epoch");
|
||||
cacheEpochMs = nowMs;
|
||||
|
||||
// Full flush avoids stale dedup identity/counters surviving epoch rollover.
|
||||
TM_LOG_DEBUG("Flushing cache");
|
||||
memset(cache, 0, static_cast<size_t>(cacheSize()) * sizeof(UnifiedCacheEntry));
|
||||
#else
|
||||
(void)nowMs;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Sliding-epoch rebase — preserve cached state past the 8-bit timestamp horizon.
|
||||
*
|
||||
* Instead of flushing the whole cache when offsets approach overflow, advance the
|
||||
* epoch by a fixed slab and shift every live entry's relative timestamps back by
|
||||
* the same wall-clock amount. A valid entry's window is only a handful of ticks
|
||||
* wide (TTL auto-scales with resolution), so live entries comfortably survive;
|
||||
* already-expired entries clamp to 0 and are reclaimed by the maintenance sweep in
|
||||
* the same locked pass. Reconstructed absolute time is preserved (minus a sub-tick
|
||||
* remainder), so in-flight TTL checks remain correct across the rebase.
|
||||
*
|
||||
* Caller must hold cacheLock.
|
||||
*/
|
||||
void TrafficManagementModule::rebaseEpoch(uint32_t nowMs)
|
||||
{
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
(void)nowMs;
|
||||
|
||||
// Slab stays well below the 200-tick reset threshold so a single rebase drops
|
||||
// the offset back into range (~200 -> ~72 ticks) while live entries survive.
|
||||
const uint32_t slabMs = 128UL * maxResolution() * 1000UL;
|
||||
cacheEpochMs += slabMs;
|
||||
|
||||
TM_LOG_DEBUG("Rebasing cache epoch by %lus", static_cast<unsigned long>(slabMs / 1000UL));
|
||||
|
||||
for (uint16_t i = 0; i < cacheSize(); i++) {
|
||||
if (cache[i].node == 0)
|
||||
continue;
|
||||
slideRelativeTime(cache[i].pos_time, slabMs, posTimeResolution);
|
||||
slideRelativeTime(cache[i].rate_time, slabMs, rateTimeResolution);
|
||||
slideRelativeTime(cache[i].unknown_time, slabMs, unknownTimeResolution);
|
||||
}
|
||||
#else
|
||||
(void)nowMs;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -697,7 +671,12 @@ uint8_t TrafficManagementModule::computePositionFingerprint(int32_t lat_truncate
|
||||
uint8_t latBits = (static_cast<uint32_t>(lat_truncated) >> shift) & ((1u << bitsToTake) - 1);
|
||||
uint8_t lonBits = (static_cast<uint32_t>(lon_truncated) >> shift) & ((1u << bitsToTake) - 1);
|
||||
|
||||
return static_cast<uint8_t>((latBits << 4) | lonBits);
|
||||
const uint8_t fp = static_cast<uint8_t>((latBits << 4) | lonBits);
|
||||
// 0 is the "no position seen" sentinel for pos_fingerprint, so a real position that happens to
|
||||
// hash to 0 must not collide with it (otherwise its duplicates would never dedup). Remap 0 -> 0xFF,
|
||||
// mirroring NodeDB::getLastByteOfNodeNum()'s 0 -> 0xFF idiom. Cost: the 0x00 bucket merges into
|
||||
// 0xFF (one extra collision in 256 — negligible; the fingerprint already collides every 16 cells).
|
||||
return fp ? fp : 0xFF;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -712,7 +691,7 @@ uint8_t TrafficManagementModule::computePositionFingerprint(int32_t lat_truncate
|
||||
// force hop_limit=0 on the rebroadcast copy, allowing one final relay hop.
|
||||
ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled)
|
||||
if (!moduleConfig.has_traffic_management)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
ignoreRequest = false;
|
||||
@@ -722,7 +701,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
incrementStat(&stats.packets_inspected);
|
||||
|
||||
const auto &cfg = moduleConfig.traffic_management;
|
||||
const uint32_t nowMs = millis();
|
||||
const uint32_t nowMs = TrafficManagementModule::clockMs();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Undecoded Packet Handling
|
||||
@@ -731,7 +710,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
// a misbehaving node. Track and optionally drop repeat offenders.
|
||||
|
||||
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
||||
if (cfg.drop_unknown_enabled && cfg.unknown_packet_threshold > 0) {
|
||||
if (cfg.unknown_packet_threshold > 0) {
|
||||
if (shouldDropUnknown(&mp, nowMs)) {
|
||||
logAction("drop", &mp, "unknown");
|
||||
incrementStat(&stats.unknown_packet_drops);
|
||||
@@ -742,9 +721,12 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Learn NodeInfo payloads into the dedicated PSRAM cache.
|
||||
if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP)
|
||||
// Learn NodeInfo payloads into the dedicated PSRAM cache, and refresh the tier-3
|
||||
// role cache for any node we already track (keeps the dedup role exception current).
|
||||
if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) {
|
||||
cacheNodeInfoPacket(mp);
|
||||
updateCachedRoleFromNodeInfo(mp);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// NodeInfo Direct Response
|
||||
@@ -754,8 +736,8 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
// STOP prevents the request from being rebroadcast toward the target node,
|
||||
// and our cached response is sent back to the requestor with hop_limit=0.
|
||||
|
||||
if (cfg.nodeinfo_direct_response && mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP && mp.decoded.want_response &&
|
||||
!isBroadcast(mp.to) && !isToUs(&mp) && !isFromUs(&mp)) {
|
||||
if (cfg.nodeinfo_direct_response_max_hops > 0 && mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP &&
|
||||
mp.decoded.want_response && !isBroadcast(mp.to) && !isToUs(&mp) && !isFromUs(&mp)) {
|
||||
if (shouldRespondToNodeInfo(&mp, true)) {
|
||||
meshtastic_User requester = meshtastic_User_init_zero;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) {
|
||||
@@ -776,7 +758,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
// GPS jitter within the configured precision.
|
||||
|
||||
if (!isFromUs(&mp) && !isToUs(&mp)) {
|
||||
if (cfg.position_dedup_enabled && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
|
||||
if (channels.isWellKnownChannel(mp.channel) && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
|
||||
meshtastic_Position pos = meshtastic_Position_init_zero;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &pos)) {
|
||||
if (shouldDropPosition(&mp, &pos, nowMs)) {
|
||||
@@ -794,7 +776,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
// Throttle nodes sending too many packets within a time window.
|
||||
// Excludes routing and admin packets which are essential for mesh operation.
|
||||
|
||||
if (cfg.rate_limit_enabled && cfg.rate_limit_window_secs > 0 && cfg.rate_limit_max_packets > 0) {
|
||||
if (cfg.rate_limit_window_secs > 0 && cfg.rate_limit_max_packets > 0) {
|
||||
if (mp.decoded.portnum != meshtastic_PortNum_ROUTING_APP && mp.decoded.portnum != meshtastic_PortNum_ADMIN_APP) {
|
||||
if (isRateLimited(mp.from, nowMs)) {
|
||||
logAction("drop", &mp, "rate-limit");
|
||||
@@ -811,7 +793,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
|
||||
void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled)
|
||||
if (!moduleConfig.has_traffic_management)
|
||||
return;
|
||||
|
||||
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag)
|
||||
@@ -820,40 +802,45 @@ void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp)
|
||||
if (isFromUs(&mp))
|
||||
return;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Relayed Broadcast Hop Exhaustion
|
||||
// -------------------------------------------------------------------------
|
||||
// For relayed telemetry or position broadcasts from other nodes, optionally
|
||||
// set hop_limit=0 so they don't propagate further through the mesh.
|
||||
// exhaust_hop_telemetry / exhaust_hop_position / router_preserve_hops:
|
||||
// not suitable right now — the right heuristics for when to exhaust or
|
||||
// preserve hops need more field data before we expose them as config knobs.
|
||||
// exhaustRequested stays false; perhapsRebroadcast() behaves normally.
|
||||
|
||||
const auto &cfg = moduleConfig.traffic_management;
|
||||
const bool isTelemetry = mp.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP;
|
||||
const bool isPosition = mp.decoded.portnum == meshtastic_PortNum_POSITION_APP;
|
||||
// Only exhaust telemetry hops when channel is actually congested, mirroring the same
|
||||
// airtime checks that gate self-generated telemetry in the telemetry modules.
|
||||
const bool channelBusy = airTime && (!airTime->isTxAllowedChannelUtil(true) || !airTime->isTxAllowedAirUtil());
|
||||
const bool shouldExhaust =
|
||||
((channelBusy && isTelemetry && cfg.exhaust_hop_telemetry) || (isPosition && cfg.exhaust_hop_position));
|
||||
|
||||
if (!shouldExhaust || !isBroadcast(mp.to))
|
||||
return;
|
||||
|
||||
if (mp.hop_limit > 0) {
|
||||
const char *reason = isTelemetry ? "exhaust-hop-telemetry" : "exhaust-hop-position";
|
||||
logAction("exhaust", &mp, reason);
|
||||
// Adjust hop_start so downstream nodes compute correct hopsAway (hop_start - hop_limit).
|
||||
// Without this, hop_limit=0 with original hop_start would show inflated hopsAway.
|
||||
mp.hop_start = mp.hop_start - mp.hop_limit + 1;
|
||||
mp.hop_limit = 0;
|
||||
// Signal perhapsRebroadcast() to allow one final relay with hop_limit=0.
|
||||
// Without this flag, perhapsRebroadcast() would skip the packet since hop_limit==0.
|
||||
// The packet-scoped flag is checked in NextHopRouter::perhapsRebroadcast()
|
||||
// and forces tosend->hop_limit=0, ensuring no further propagation beyond the
|
||||
// next node.
|
||||
exhaustRequested = true;
|
||||
exhaustRequestedFrom = getFrom(&mp);
|
||||
exhaustRequestedId = mp.id;
|
||||
incrementStat(&stats.hop_exhausted_packets);
|
||||
// -------------------------------------------------------------------------
|
||||
// Relayed Position Precision Clamp
|
||||
// -------------------------------------------------------------------------
|
||||
// Clamp relayed position broadcasts to the channel's configured precision
|
||||
// ceiling. Guards against forwarding more-precise coordinates than the
|
||||
// channel is intended to carry (e.g. a LongFast channel set to 13-bit /
|
||||
// ~1.5 km). chanPrec==0 means position sharing is disabled on the channel;
|
||||
// skip — not our job to zero positions on relay.
|
||||
// Ham mode (owner.is_licensed) is exempt. Lost-and-found is NOT exempt — its relayed
|
||||
// positions get the same precision clamp as any node.
|
||||
// Compile USERPREFS_TMM_APPLY_TO_PRIVATE_CHANNELS to extend to private channels.
|
||||
if (!owner.is_licensed && isPosition && isBroadcast(mp.to)) {
|
||||
#ifdef USERPREFS_TMM_APPLY_TO_PRIVATE_CHANNELS
|
||||
const bool shouldClamp = true;
|
||||
#else
|
||||
const bool shouldClamp = channels.isWellKnownChannel(mp.channel);
|
||||
#endif
|
||||
if (shouldClamp) {
|
||||
const uint32_t chanPrec = getPositionPrecisionForChannel(mp.channel);
|
||||
if (chanPrec > 0) {
|
||||
meshtastic_Position pos = meshtastic_Position_init_default;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &pos)) {
|
||||
const uint32_t packetPrec = pos.precision_bits > 0 ? pos.precision_bits : 32u;
|
||||
if (packetPrec > chanPrec) {
|
||||
applyPositionPrecision(pos, chanPrec);
|
||||
mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes),
|
||||
&meshtastic_Position_msg, &pos);
|
||||
logAction("clamp", &mp, "precision");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,53 +850,58 @@ void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp)
|
||||
|
||||
int32_t TrafficManagementModule::runOnce()
|
||||
{
|
||||
if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled)
|
||||
if (!moduleConfig.has_traffic_management)
|
||||
return INT32_MAX;
|
||||
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
const uint32_t nowMs = millis();
|
||||
const uint32_t nowMs = TrafficManagementModule::clockMs();
|
||||
|
||||
// Warm-start the next-hop cache from persisted NodeInfoLite hints once nodeDB
|
||||
// is populated. Done here (not in the constructor) so nodeDB has finished
|
||||
// loading. Takes its own lock, so call before acquiring the sweep guard below.
|
||||
if (!nextHopPreloaded) {
|
||||
preloadNextHopsFromNodeDB();
|
||||
// Only latch the one-shot guard once the preload actually ran; if nodeDB wasn't
|
||||
// ready yet, retry on the next maintenance pass instead of skipping it forever.
|
||||
if (!nextHopPreloaded && preloadNextHopsFromNodeDB())
|
||||
nextHopPreloaded = true;
|
||||
}
|
||||
|
||||
// Calculate TTLs for cache expiration
|
||||
// Free-running tick counters (no epoch needed).
|
||||
// TTL expressed in ticks:
|
||||
// pos: 4× position_min_interval_secs (clamped to 255 ticks @ 6 min/tick)
|
||||
// rate: 2× rate_limit_window_secs (clamped to 15 ticks @ 5 min/tick; only relevant when rate limits are configured)
|
||||
// unknown: fixed 12 ticks @ 1 min/tick (only relevant when unknown_packet_threshold > 0)
|
||||
const uint32_t positionIntervalMs = secsToMs(Default::getConfiguredOrDefault(
|
||||
moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs));
|
||||
const uint32_t positionTtlMs = positionIntervalMs * 4;
|
||||
const uint8_t posTtlTicks =
|
||||
static_cast<uint8_t>(std::min(static_cast<uint32_t>(255), (positionIntervalMs * 4) / kPosTimeTickMs));
|
||||
|
||||
const uint32_t rateIntervalMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs);
|
||||
const uint32_t rateTtlMs = (rateIntervalMs > 0) ? rateIntervalMs * 2 : (10 * 60 * 1000UL);
|
||||
const uint32_t rateWindowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs);
|
||||
const uint8_t rateTtlTicks = static_cast<uint8_t>(
|
||||
std::min(static_cast<uint32_t>(15), (rateWindowMs > 0 ? rateWindowMs * 2 : 24 * kRateTimeTickMs) / kRateTimeTickMs));
|
||||
|
||||
const uint32_t unknownTtlMs = kUnknownResetMs * 5;
|
||||
// unknown: fixed 12-tick TTL (12 min — 4 ticks past the 5-min default window)
|
||||
const uint8_t unknownTtlTicks = 12;
|
||||
|
||||
const uint8_t nowPosTick = currentPosTick();
|
||||
const uint8_t nowRateTick = currentRateTick();
|
||||
const uint8_t nowUnknownTick = currentUnknownTick();
|
||||
|
||||
// Sweep cache and clear expired entries
|
||||
uint16_t activeEntries = 0;
|
||||
uint16_t expiredEntries = 0;
|
||||
const uint32_t sweepStartMs = millis();
|
||||
const uint32_t sweepStartMs = TrafficManagementModule::clockMs();
|
||||
|
||||
const auto &cfg = moduleConfig.traffic_management;
|
||||
concurrency::LockGuard guard(&cacheLock);
|
||||
|
||||
// Slide the epoch instead of flushing when offsets approach 8-bit overflow.
|
||||
// Rebase preserves live entries; only already-expired ones clamp to 0 and are
|
||||
// reclaimed by the sweep below in this same locked pass.
|
||||
if (needsEpochReset(nowMs))
|
||||
rebaseEpoch(nowMs);
|
||||
|
||||
for (uint16_t i = 0; i < cacheSize(); i++) {
|
||||
if (cache[i].node == 0)
|
||||
continue;
|
||||
|
||||
bool anyValid = false;
|
||||
|
||||
// Check and clear expired position data
|
||||
if (cache[i].pos_time != 0) {
|
||||
uint32_t posTimeMs = fromRelativePosTime(cache[i].pos_time);
|
||||
if (!isWithinWindow(nowMs, posTimeMs, positionTtlMs)) {
|
||||
// Check and clear expired position data (presence: pos_fingerprint != 0)
|
||||
if (cache[i].pos_fingerprint != 0) {
|
||||
if (static_cast<uint8_t>(nowPosTick - cache[i].pos_time) >= posTtlTicks) {
|
||||
cache[i].pos_fingerprint = 0;
|
||||
cache[i].pos_time = 0;
|
||||
} else {
|
||||
@@ -917,31 +909,33 @@ int32_t TrafficManagementModule::runOnce()
|
||||
}
|
||||
}
|
||||
|
||||
// Check and clear expired rate limit data
|
||||
if (cache[i].rate_time != 0) {
|
||||
uint32_t rateTimeMs = fromRelativeRateTime(cache[i].rate_time);
|
||||
if (!isWithinWindow(nowMs, rateTimeMs, rateTtlMs)) {
|
||||
cache[i].rate_count = 0;
|
||||
cache[i].rate_time = 0;
|
||||
// Check and clear expired rate limit data (presence: getRateCount() != 0)
|
||||
if (cache[i].getRateCount() != 0) {
|
||||
if ((static_cast<uint8_t>(nowRateTick - cache[i].getRateTime()) & 0x0F) >= rateTtlTicks) {
|
||||
cache[i].setRateCount(0);
|
||||
cache[i].setRateTime(0);
|
||||
} else {
|
||||
anyValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check and clear expired unknown tracking data
|
||||
if (cache[i].unknown_time != 0) {
|
||||
uint32_t unknownTimeMs = fromRelativeUnknownTime(cache[i].unknown_time);
|
||||
if (!isWithinWindow(nowMs, unknownTimeMs, unknownTtlMs)) {
|
||||
cache[i].unknown_count = 0;
|
||||
cache[i].unknown_time = 0;
|
||||
// Check and clear expired unknown tracking data (presence: getUnknownCount() != 0)
|
||||
if (cache[i].getUnknownCount() != 0) {
|
||||
if ((static_cast<uint8_t>(nowUnknownTick - cache[i].getUnknownTime()) & 0x0F) >= unknownTtlTicks) {
|
||||
cache[i].setUnknownCount(0);
|
||||
cache[i].setUnknownTime(0);
|
||||
} else {
|
||||
anyValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// A confirmed next-hop hint has no TTL of its own and keeps the slot alive,
|
||||
// so an aged-out routing hint outlives the dedup/rate/unknown state.
|
||||
if (cache[i].next_hop != 0)
|
||||
// Two fields have no TTL of their own and pin the slot, so they outlive the
|
||||
// dedup/rate/unknown state:
|
||||
// - a confirmed next-hop hint (the routing overflow store), and
|
||||
// - a cached special (non-CLIENT) role, so a tracker / lost-and-found / router
|
||||
// keeps its dedup-window exception across quiet periods rather than reverting
|
||||
// to CLIENT the moment its timed state expires.
|
||||
if (cache[i].next_hop != 0 || cache[i].getCachedRole() != meshtastic_Config_DeviceConfig_Role_CLIENT)
|
||||
anyValid = true;
|
||||
|
||||
// If all data expired, free the slot entirely
|
||||
@@ -955,7 +949,7 @@ int32_t TrafficManagementModule::runOnce()
|
||||
|
||||
TM_LOG_DEBUG("Maintenance: %u active, %u expired, %u/%u slots, %lums elapsed", activeEntries, expiredEntries,
|
||||
static_cast<unsigned>(activeEntries), static_cast<unsigned>(cacheSize()),
|
||||
static_cast<unsigned long>(millis() - sweepStartMs));
|
||||
static_cast<unsigned long>(TrafficManagementModule::clockMs() - sweepStartMs));
|
||||
|
||||
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
||||
if (nodeInfoPayload) {
|
||||
@@ -984,18 +978,21 @@ bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p,
|
||||
if (!pos->has_latitude_i || !pos->has_longitude_i)
|
||||
return false;
|
||||
|
||||
uint8_t precision = Default::getConfiguredOrDefault(moduleConfig.traffic_management.position_precision_bits,
|
||||
default_traffic_mgmt_position_precision_bits);
|
||||
precision = sanitizePositionPrecision(precision);
|
||||
// Precision is driven by the channel's own position_precision ceiling — the same
|
||||
// grid the channel uses for broadcast. Falls back to the firmware default (19-bit,
|
||||
// ~90m cells) when the channel has no precision configured (chanPrec == 0).
|
||||
const uint32_t chanPrec = getPositionPrecisionForChannel(p->channel);
|
||||
uint8_t precision = sanitizePositionPrecision(
|
||||
chanPrec > 0 ? static_cast<uint8_t>(chanPrec) : static_cast<uint8_t>(default_traffic_mgmt_position_precision_bits));
|
||||
|
||||
const int32_t lat_truncated = truncateLatLon(pos->latitude_i, precision);
|
||||
const int32_t lon_truncated = truncateLatLon(pos->longitude_i, precision);
|
||||
const int32_t lat_truncated = truncateCoordinate(pos->latitude_i, precision);
|
||||
const int32_t lon_truncated = truncateCoordinate(pos->longitude_i, precision);
|
||||
const uint8_t fingerprint = computePositionFingerprint(lat_truncated, lon_truncated, precision);
|
||||
// Drop gate uses the RAW configured interval: 0 means "dedup disabled" (the
|
||||
// contract documented below). The 12h default is only for resolution/TTL
|
||||
// sizing (constructor / runOnce), not for deciding whether to drop — feeding
|
||||
// the default here would silently turn the 0-disables-dedup contract off.
|
||||
const uint32_t minIntervalMs = secsToMs(moduleConfig.traffic_management.position_min_interval_secs);
|
||||
uint32_t minIntervalMs = secsToMs(moduleConfig.traffic_management.position_min_interval_secs);
|
||||
|
||||
bool isNew = false;
|
||||
concurrency::LockGuard guard(&cacheLock);
|
||||
@@ -1003,19 +1000,46 @@ bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p,
|
||||
if (!entry)
|
||||
return false;
|
||||
|
||||
// Compare fingerprint and check time window
|
||||
// When minIntervalMs == 0, deduplication is disabled (withinInterval = false means never drop)
|
||||
const bool hasPositionState = !isNew && entry->pos_time != 0;
|
||||
// Role exceptions keyed on the originating node's advertised role, resolved across
|
||||
// all three tiers (hot store → warm store → TMM live cache). The position path is
|
||||
// the one place that needs sender-role, and it also keeps tier 3 warm so the
|
||||
// exception survives the node aging out of both NodeDB stores — important in the
|
||||
// common dedup-only config, where isRateLimited()'s role write never runs.
|
||||
const meshtastic_Config_DeviceConfig_Role role = resolveSenderRole(p->from, entry, isNew);
|
||||
if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
||||
// Lost-and-found may refresh a duplicate position at most every ~15 min (cap, never
|
||||
// lengthens; quantised to ~2 dedup ticks). Only when dedup is active — never tighten
|
||||
// past an operator who disabled it (0).
|
||||
const uint32_t lostFoundCapMs = secsToMs(default_traffic_mgmt_lost_and_found_position_min_interval_secs);
|
||||
if (minIntervalMs != 0 && minIntervalMs > lostFoundCapMs)
|
||||
minIntervalMs = lostFoundCapMs;
|
||||
} else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER || role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
||||
// Trackers may refresh a duplicate position as often as hourly (cap, never lengthens).
|
||||
const uint32_t trackerCapMs = secsToMs(default_traffic_mgmt_tracker_position_min_interval_secs);
|
||||
if (minIntervalMs > trackerCapMs)
|
||||
minIntervalMs = trackerCapMs;
|
||||
}
|
||||
|
||||
// Compare fingerprint and check time window.
|
||||
// When minIntervalMs == 0, deduplication is disabled (withinInterval = false means never drop).
|
||||
// Presence: pos_fingerprint != 0; computePositionFingerprint() remaps 0 -> 0xFF so zero means unseen.
|
||||
const bool hasPositionState = !isNew && entry->pos_fingerprint != 0;
|
||||
const bool samePosition = hasPositionState && entry->pos_fingerprint == fingerprint;
|
||||
const uint8_t nowPosTick = currentPosTick();
|
||||
// Clamp to [1, 255]: intervals shorter than one tick still dedup within the same tick.
|
||||
const uint8_t windowTicks =
|
||||
(minIntervalMs == 0) ? 0
|
||||
: static_cast<uint8_t>(std::min(static_cast<uint32_t>(UINT8_MAX),
|
||||
std::max(static_cast<uint32_t>(1), minIntervalMs / kPosTimeTickMs)));
|
||||
const bool withinInterval =
|
||||
hasPositionState && (minIntervalMs != 0) && isWithinWindow(nowMs, fromRelativePosTime(entry->pos_time), minIntervalMs);
|
||||
hasPositionState && (windowTicks != 0) && (static_cast<uint8_t>(nowPosTick - entry->pos_time) < windowTicks);
|
||||
|
||||
TM_LOG_DEBUG("Position dedup 0x%08x: fp=0x%02x prev=0x%02x same=%d within=%d new=%d", p->from, fingerprint,
|
||||
entry->pos_fingerprint, samePosition, withinInterval, isNew);
|
||||
|
||||
// Update cache entry
|
||||
// Update cache entry (raw tick; 0 is a valid tick value)
|
||||
entry->pos_fingerprint = fingerprint;
|
||||
entry->pos_time = toRelativePosTime(nowMs);
|
||||
entry->pos_time = nowPosTick;
|
||||
|
||||
// Drop only if same position AND within the minimum interval
|
||||
return samePosition && withinInterval;
|
||||
@@ -1104,7 +1128,7 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke
|
||||
reply->decoded.bitfield |= BITFIELD_OK_TO_MQTT_MASK;
|
||||
|
||||
if (hasCachedUser && cachedLastObservedMs != 0) {
|
||||
uint32_t ageMs = millis() - cachedLastObservedMs;
|
||||
uint32_t ageMs = clockMs() - cachedLastObservedMs;
|
||||
TM_LOG_DEBUG("NodeInfo PSRAM hit node=0x%08x age=%lu ms src_ch=%u req_ch=%u rx_time=%lu", p->to,
|
||||
static_cast<unsigned long>(ageMs), static_cast<unsigned>(cachedSourceChannel),
|
||||
static_cast<unsigned>(p->channel), static_cast<unsigned long>(cachedLastObservedRxTime));
|
||||
@@ -1169,25 +1193,32 @@ bool TrafficManagementModule::isRateLimited(NodeNum from, uint32_t nowMs)
|
||||
if (!entry)
|
||||
return false;
|
||||
|
||||
// Check if window has expired
|
||||
if (isNew || !isWithinWindow(nowMs, fromRelativeRateTime(entry->rate_time), windowMs)) {
|
||||
entry->rate_time = toRelativeRateTime(nowMs);
|
||||
entry->rate_count = 1;
|
||||
// Window ticks: clamp to [1,15] so zero windowMs (config error) opens a new window.
|
||||
const uint8_t windowTicks = static_cast<uint8_t>(std::min(static_cast<uint32_t>(15), windowMs / kRateTimeTickMs));
|
||||
const uint8_t nowRateTick = currentRateTick();
|
||||
const bool windowExpired =
|
||||
isNew || entry->getRateCount() == 0 ||
|
||||
((static_cast<uint8_t>(nowRateTick - entry->getRateTime()) & 0x0F) >= std::max(static_cast<uint8_t>(1), windowTicks));
|
||||
if (windowExpired) {
|
||||
entry->setRateTime(nowRateTick);
|
||||
entry->setRateCount(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increment counter (saturates at 255)
|
||||
saturatingIncrement(entry->rate_count);
|
||||
// Increment counter, saturating at 63 (6-bit field max).
|
||||
const uint8_t cur = entry->getRateCount();
|
||||
if (cur < 0x3F)
|
||||
entry->setRateCount(static_cast<uint8_t>(cur + 1));
|
||||
|
||||
// Check against threshold (uint8_t max is 255, but config is uint32_t)
|
||||
// Threshold capped at 60 so a saturated reading (63) always exceeds it.
|
||||
uint32_t threshold = moduleConfig.traffic_management.rate_limit_max_packets;
|
||||
if (threshold > 255)
|
||||
threshold = 255;
|
||||
if (threshold > 60)
|
||||
threshold = 60;
|
||||
|
||||
bool limited = entry->rate_count > threshold;
|
||||
if (limited || entry->rate_count == threshold) {
|
||||
TM_LOG_DEBUG("Rate limit 0x%08x: count=%u threshold=%u -> %s", from, entry->rate_count, threshold,
|
||||
limited ? "DROP" : "at-limit");
|
||||
const uint8_t count = entry->getRateCount();
|
||||
bool limited = count > threshold;
|
||||
if (limited || count == threshold) {
|
||||
TM_LOG_DEBUG("Rate limit 0x%08x: count=%u threshold=%u -> %s", from, count, threshold, limited ? "DROP" : "at-limit");
|
||||
}
|
||||
return limited;
|
||||
#endif
|
||||
@@ -1200,12 +1231,11 @@ bool TrafficManagementModule::shouldDropUnknown(const meshtastic_MeshPacket *p,
|
||||
(void)nowMs;
|
||||
return false;
|
||||
#else
|
||||
if (!moduleConfig.traffic_management.drop_unknown_enabled || moduleConfig.traffic_management.unknown_packet_threshold == 0)
|
||||
if (moduleConfig.traffic_management.unknown_packet_threshold == 0)
|
||||
return false;
|
||||
|
||||
uint32_t windowMs = kUnknownResetMs;
|
||||
if (moduleConfig.traffic_management.rate_limit_window_secs > 0)
|
||||
windowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs);
|
||||
// Fixed 5-tick (5 min) unknown window; capped at 12 ticks (12 min max).
|
||||
static constexpr uint8_t kUnknownWindowTicks = 5;
|
||||
|
||||
bool isNew = false;
|
||||
concurrency::LockGuard guard(&cacheLock);
|
||||
@@ -1213,25 +1243,31 @@ bool TrafficManagementModule::shouldDropUnknown(const meshtastic_MeshPacket *p,
|
||||
if (!entry)
|
||||
return false;
|
||||
|
||||
// Check if window has expired
|
||||
if (isNew || !isWithinWindow(nowMs, fromRelativeUnknownTime(entry->unknown_time), windowMs)) {
|
||||
entry->unknown_time = toRelativeUnknownTime(nowMs);
|
||||
entry->unknown_count = 0;
|
||||
// Check if window has expired (presence: getUnknownCount() != 0)
|
||||
const uint8_t nowUnknownTick = currentUnknownTick();
|
||||
const bool windowExpired = isNew || entry->getUnknownCount() == 0 ||
|
||||
((static_cast<uint8_t>(nowUnknownTick - entry->getUnknownTime()) & 0x0F) >= kUnknownWindowTicks);
|
||||
if (windowExpired) {
|
||||
entry->setUnknownTime(nowUnknownTick);
|
||||
entry->setUnknownCount(0);
|
||||
}
|
||||
|
||||
// Increment counter (saturates at 255). Same saturation handling as
|
||||
// isRateLimited: without it, a clamped threshold of 255 can never fire.
|
||||
const bool alreadySaturated = (entry->unknown_count == UINT8_MAX);
|
||||
saturatingIncrement(entry->unknown_count);
|
||||
// Increment counter, saturating at 63 (6-bit field max). With threshold
|
||||
// capped at 60, a saturated reading always exceeds the limit — no special
|
||||
// already-saturated edge case needed.
|
||||
const uint8_t cur = entry->getUnknownCount();
|
||||
if (cur < 0x3F)
|
||||
entry->setUnknownCount(static_cast<uint8_t>(cur + 1));
|
||||
|
||||
// Check against threshold
|
||||
// Threshold capped at 60 so a saturated reading (63) always exceeds it.
|
||||
uint32_t threshold = moduleConfig.traffic_management.unknown_packet_threshold;
|
||||
if (threshold > 255)
|
||||
threshold = 255;
|
||||
if (threshold > 60)
|
||||
threshold = 60;
|
||||
|
||||
bool drop = entry->unknown_count > threshold || (alreadySaturated && threshold == 255);
|
||||
if (drop || entry->unknown_count == threshold) {
|
||||
TM_LOG_DEBUG("Unknown packets 0x%08x: count=%u threshold=%u -> %s", p->from, entry->unknown_count, threshold,
|
||||
const uint8_t count = entry->getUnknownCount();
|
||||
bool drop = count > threshold;
|
||||
if (drop || count == threshold) {
|
||||
TM_LOG_DEBUG("Unknown packets 0x%08x: count=%u threshold=%u -> %s", p->from, count, threshold,
|
||||
drop ? "DROP" : "at-limit");
|
||||
}
|
||||
return drop;
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
* Memory Optimization:
|
||||
* Uses one flat unified cache (plain array, linear scan) shared by all
|
||||
* per-node features instead of separate per-feature caches. Timestamps are
|
||||
* stored as 8-bit relative offsets from a rolling epoch to further reduce
|
||||
* memory footprint. LoRa packet rates are low enough that an O(n) scan of
|
||||
* ~1000 11-byte entries is negligible next to packet processing.
|
||||
* stored as free-running modular tick counters (pos: 8-bit 360 s/tick;
|
||||
* rate+unknown: paired 4-bit nibbles in one byte) for a 10-byte entry.
|
||||
* LoRa packet rates are low enough that an O(n) scan of ~1000 entries is
|
||||
* negligible next to packet processing.
|
||||
*/
|
||||
class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
{
|
||||
@@ -54,7 +55,9 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
// Warm-start the next-hop cache from persisted NodeInfoLite hints so confirmed
|
||||
// hops survive later hot-store (NodeDB) eviction. Idempotent; runs once after
|
||||
// nodeDB is populated (lazily on first maintenance pass).
|
||||
void preloadNextHopsFromNodeDB();
|
||||
// @return true if it actually ran (prereqs met / nothing to do); false if
|
||||
// prerequisites (cache, nodeDB) weren't ready yet, so the caller should retry.
|
||||
bool preloadNextHopsFromNodeDB();
|
||||
|
||||
/**
|
||||
* Check if this packet should have its hops exhausted.
|
||||
@@ -66,81 +69,90 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
return exhaustRequested && exhaustRequestedFrom == getFrom(&mp) && exhaustRequestedId == mp.id;
|
||||
}
|
||||
|
||||
// Injectable monotonic clock (ms). All TMM time reads go through clockMs() so unit tests can
|
||||
// advance a virtual timebase instead of sleeping real seconds across the 6 min/360 s tick.
|
||||
// Mirrors HopScalingModule::s_testNowMs. Writable from tests as TrafficManagementModule::s_testNowMs;
|
||||
// ignored in production (clockMs() returns millis()).
|
||||
inline static uint32_t s_testNowMs = 0;
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
static uint32_t clockMs() { return s_testNowMs; }
|
||||
#else
|
||||
static uint32_t clockMs() { return millis(); }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
|
||||
void alterReceived(meshtastic_MeshPacket &mp) override;
|
||||
int32_t runOnce() override;
|
||||
// Protected so test shims can force epoch rollover behavior.
|
||||
void resetEpoch(uint32_t nowMs);
|
||||
// Sliding-epoch rebase: advance the epoch and shift live entries back by the
|
||||
// same wall-clock amount instead of flushing, so cached state survives past the
|
||||
// ~19h horizon. Caller must hold cacheLock.
|
||||
void rebaseEpoch(uint32_t nowMs);
|
||||
// Protected so test shims can flush per-node traffic state.
|
||||
void flushCache();
|
||||
// Introspection for tests: the cached device role for a node, or -1 if the node has
|
||||
// no cache entry (distinguishes "not tracked / evicted" from CLIENT == 0).
|
||||
int peekCachedRole(NodeNum node);
|
||||
|
||||
private:
|
||||
// =========================================================================
|
||||
// Unified Cache Entry (11 bytes) - Same for ALL platforms
|
||||
// Unified Cache Entry (10 bytes) - Same for ALL platforms
|
||||
// =========================================================================
|
||||
//
|
||||
// A single compact structure used across ESP32, NRF52, and all other platforms.
|
||||
// Memory: 11 bytes × TRAFFIC_MANAGEMENT_CACHE_SIZE entries (default 1000 = 11KB)
|
||||
//
|
||||
// Position Fingerprinting:
|
||||
// Instead of storing full coordinates (8 bytes) or a computed hash,
|
||||
// we store an 8-bit fingerprint derived deterministically from the
|
||||
// truncated lat/lon. This extracts the lower 4 significant bits from
|
||||
// each coordinate: fingerprint = (lat_low4 << 4) | lon_low4
|
||||
//
|
||||
// Benefits over hash:
|
||||
// - Adjacent grid cells have sequential fingerprints (no collision)
|
||||
// - Two positions only collide if 16+ grid cells apart in BOTH dimensions
|
||||
// - Deterministic: same input always produces same output
|
||||
//
|
||||
// Adaptive Timestamp Resolution:
|
||||
// All timestamps use 8-bit values with adaptive resolution calculated
|
||||
// from config at startup. Resolution = max(60, min(339, interval/2)).
|
||||
// - Min 60 seconds ensures reasonable precision
|
||||
// - Max 339 seconds allows ~24 hour range (255 * 339 = 86445 sec)
|
||||
// - interval/2 ensures at least 2 ticks per configured interval
|
||||
//
|
||||
// Layout:
|
||||
// [0-3] node - NodeNum (4 bytes)
|
||||
// [4] pos_fingerprint - 4 bits lat + 4 bits lon (1 byte)
|
||||
// [5] rate_count - Packets in current window (1 byte)
|
||||
// [6] unknown_count - Unknown packets count (1 byte)
|
||||
// [7] pos_time - Position timestamp (1 byte, adaptive resolution)
|
||||
// [8] rate_time - Rate window start (1 byte, adaptive resolution)
|
||||
// [9] unknown_time - Unknown tracking start (1 byte, adaptive resolution)
|
||||
// [10] next_hop - Last-byte relay to reach `node` (1 byte, 0 = none)
|
||||
// [0-3] node - NodeNum (4 bytes, 0 = empty slot)
|
||||
// [4] pos_fingerprint - 4 bits lat + 4 bits lon (0 = no position seen)
|
||||
// [5] rate_count - [7:6] role[3:2] | [5:0] packets in rate window (0 = no window active)
|
||||
// [6] unknown_count - [7:6] role[1:0] | [5:0] unknown packets in window (0 = no window active)
|
||||
// [7] pos_time - Position tick (uint8, free-running 360 s/tick)
|
||||
// [8] rate_unknown_time - [7:4] rate nibble (300 s/tick) | [3:0] unknown nibble (60 s/tick)
|
||||
// [9] next_hop - Last-byte relay to reach `node` (0 = none)
|
||||
//
|
||||
// next_hop semantics:
|
||||
// A routing hint: the last byte of the NodeNum to use as next hop to reach
|
||||
// `node`. Written ONLY from NextHopRouter's ACK-confirmed decision (a
|
||||
// bidirectionally-verified relay), never inferred one-way from relayed
|
||||
// traffic. The TMM cache acts as an overflow store for confirmed next-hops
|
||||
// that have aged out of the hot NodeDB (NodeInfoLite). Unlike the other
|
||||
// fields it has no TTL of its own — it keeps its slot alive (see runOnce)
|
||||
// and is refreshed only on the next confirmed exchange.
|
||||
// The 4-bit device role (bits [7:6] of rate_count paired with [7:6] of unknown_count)
|
||||
// caches the sender's meshtastic_Config_DeviceConfig_Role as a third fallback after the
|
||||
// hot store and warm store, for nodes evicted from both. Read/written via
|
||||
// resolveSenderRole(). Max encodable value is 15.
|
||||
//
|
||||
// Presence sentinels (no epoch, no +1 offset needed):
|
||||
// pos active: pos_fingerprint != 0
|
||||
// rate active: getRateCount() != 0 (low 6 bits only)
|
||||
// unknown active: getUnknownCount() != 0 (low 6 bits only)
|
||||
//
|
||||
// next_hop: routing hint written only from ACK-confirmed NextHopRouter decisions.
|
||||
// No TTL — keeps the slot alive across maintenance sweeps.
|
||||
//
|
||||
#if _meshtastic_Config_DeviceConfig_Role_MAX > 15
|
||||
#warning "Device role enum max exceeds 15 — TMM 4-bit role cache (rate_count[7:6]/unknown_count[7:6]) will truncate new values"
|
||||
#endif
|
||||
struct __attribute__((packed)) UnifiedCacheEntry {
|
||||
NodeNum node; // 4 bytes - Node identifier (0 = empty slot)
|
||||
uint8_t pos_fingerprint; // 1 byte - Lower 4 bits of lat + lon
|
||||
uint8_t rate_count; // 1 byte - Packet count (saturates at 255)
|
||||
uint8_t unknown_count; // 1 byte - Unknown packet count (saturates at 255)
|
||||
uint8_t pos_time; // 1 byte - Position timestamp (adaptive resolution)
|
||||
uint8_t rate_time; // 1 byte - Rate window start (adaptive resolution)
|
||||
uint8_t unknown_time; // 1 byte - Unknown tracking start (adaptive resolution)
|
||||
uint8_t next_hop; // 1 byte - Last-byte relay to reach `node` (0 = none). See note below.
|
||||
NodeNum node;
|
||||
uint8_t pos_fingerprint;
|
||||
uint8_t rate_count; // [7:6] = role[3:2], [5:0] = count (max 63)
|
||||
uint8_t unknown_count; // [7:6] = role[1:0], [5:0] = count (max 63)
|
||||
uint8_t pos_time;
|
||||
uint8_t rate_unknown_time;
|
||||
uint8_t next_hop;
|
||||
|
||||
uint8_t getRateCount() const { return rate_count & 0x3F; }
|
||||
void setRateCount(uint8_t c) { rate_count = static_cast<uint8_t>((rate_count & 0xC0) | (c & 0x3F)); }
|
||||
uint8_t getUnknownCount() const { return unknown_count & 0x3F; }
|
||||
void setUnknownCount(uint8_t c) { unknown_count = static_cast<uint8_t>((unknown_count & 0xC0) | (c & 0x3F)); }
|
||||
uint8_t getCachedRole() const { return static_cast<uint8_t>(((rate_count >> 6) << 2) | (unknown_count >> 6)); }
|
||||
void setCachedRole(uint8_t role)
|
||||
{
|
||||
rate_count = static_cast<uint8_t>((rate_count & 0x3F) | ((role >> 2) << 6));
|
||||
unknown_count = static_cast<uint8_t>((unknown_count & 0x3F) | ((role & 0x03) << 6));
|
||||
}
|
||||
uint8_t getRateTime() const { return (rate_unknown_time >> 4) & 0x0F; }
|
||||
uint8_t getUnknownTime() const { return rate_unknown_time & 0x0F; }
|
||||
void setRateTime(uint8_t t) { rate_unknown_time = static_cast<uint8_t>((rate_unknown_time & 0x0F) | ((t & 0x0F) << 4)); }
|
||||
void setUnknownTime(uint8_t t) { rate_unknown_time = static_cast<uint8_t>((rate_unknown_time & 0xF0) | (t & 0x0F)); }
|
||||
};
|
||||
static_assert(sizeof(UnifiedCacheEntry) == 11, "UnifiedCacheEntry should be 11 bytes");
|
||||
static_assert(sizeof(UnifiedCacheEntry) == 10, "UnifiedCacheEntry should be 10 bytes");
|
||||
|
||||
// =========================================================================
|
||||
// Flat unified cache
|
||||
// =========================================================================
|
||||
//
|
||||
// Plain array, linear scan (same idiom as WarmNodeStore). A lookup walks at
|
||||
// most cacheSize() × 11 B — microseconds at LoRa packet rates, not worth a
|
||||
// most cacheSize() × 10 B — microseconds at LoRa packet rates, not worth a
|
||||
// hash table. Insertion on a full cache evicts the stalest entry,
|
||||
// preferring entries without a next_hop hint (those are the long-tail
|
||||
// routing state this cache exists to keep).
|
||||
@@ -154,82 +166,29 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
static constexpr uint16_t nodeInfoTargetEntries() { return kNodeInfoCacheEntries; }
|
||||
|
||||
// =========================================================================
|
||||
// Adaptive Timestamp Resolution
|
||||
// Free-Running Tick Counters
|
||||
// =========================================================================
|
||||
//
|
||||
// All timestamps use 8-bit values with adaptive resolution calculated from
|
||||
// config at startup. This allows ~24 hour range while maintaining precision.
|
||||
// Timestamps are stored as free-running modular tick counters derived from
|
||||
// millis(). No epoch anchor needed: modular subtraction gives correct age
|
||||
// as long as the true age stays below the counter period.
|
||||
//
|
||||
// Resolution formula: max(60, min(339, interval/2))
|
||||
// - 60 sec minimum ensures reasonable precision
|
||||
// - 339 sec maximum allows 24 hour range (255 * 339 ≈ 86400 sec)
|
||||
// - interval/2 ensures at least 2 ticks per configured interval
|
||||
// pos_time : uint8 (256 ticks × 360 s = 25.6 h period; max window 12 h = 120 ticks)
|
||||
// rate_time : nibble (16 ticks × 300 s = 80 min period; max window 1 h = 12 ticks)
|
||||
// unknown_time: nibble (16 ticks × 60 s = 16 min period; max window 12 min = 12 ticks)
|
||||
//
|
||||
// Since config changes require reboot, resolution is calculated once.
|
||||
// Presence sentinels (no +1 offset needed; count fields serve as guards):
|
||||
// pos active: pos_fingerprint != 0 (0 is reserved sentinel; computePositionFingerprint() remaps computed-0 → 0xFF)
|
||||
// rate active: getRateCount() != 0 (low 6 bits; high 2 bits are cached role)
|
||||
// unknown active: getUnknownCount() != 0
|
||||
//
|
||||
uint32_t cacheEpochMs = 0;
|
||||
uint16_t posTimeResolution = 60; // Seconds per tick for position
|
||||
uint16_t rateTimeResolution = 60; // Seconds per tick for rate limiting
|
||||
uint16_t unknownTimeResolution = 60; // Seconds per tick for unknown tracking
|
||||
static constexpr uint32_t kPosTimeTickMs = 360'000UL; // 6 min/tick
|
||||
static constexpr uint32_t kRateTimeTickMs = 300'000UL; // 5 min/tick
|
||||
static constexpr uint32_t kUnknownTimeTickMs = 60'000UL; // 1 min/tick
|
||||
|
||||
// Calculate resolution from configured interval (called once at startup)
|
||||
static uint16_t calcTimeResolution(uint32_t intervalSecs)
|
||||
{
|
||||
// Resolution = interval/2 to ensure at least 2 ticks per interval
|
||||
// Clamped to [60, 339] for min precision and max 24h range
|
||||
uint32_t res = (intervalSecs > 0) ? (intervalSecs / 2) : 60;
|
||||
if (res < 60)
|
||||
res = 60;
|
||||
if (res > 339)
|
||||
res = 339;
|
||||
return static_cast<uint16_t>(res);
|
||||
}
|
||||
|
||||
// Convert to/from 8-bit relative timestamps with given resolution.
|
||||
//
|
||||
// All stored timestamps carry a uniform +1 "presence" offset: a value of 0 is
|
||||
// reserved for "no timestamp recorded" (which is also the zero-initialized
|
||||
// state), and stored values 1..255 encode raw ticks 0..254. This keeps the
|
||||
// 0-means-empty sentinel consistent with memset/calloc zeroing across every
|
||||
// sub-store, so the maintenance sweep's `_time != 0` presence checks are
|
||||
// unambiguous (a timestamp recorded in the first tick after the epoch is no
|
||||
// longer mistaken for an empty slot). The offset is applied here and removed
|
||||
// on read, so it cancels out in all window math.
|
||||
uint8_t toRelativeTime(uint32_t nowMs, uint16_t resolutionSecs) const
|
||||
{
|
||||
uint32_t ticks = (nowMs - cacheEpochMs) / (resolutionSecs * 1000UL);
|
||||
return (ticks >= UINT8_MAX) ? UINT8_MAX : static_cast<uint8_t>(ticks + 1);
|
||||
}
|
||||
uint32_t fromRelativeTime(uint8_t ticks, uint16_t resolutionSecs) const
|
||||
{
|
||||
return (ticks == 0) ? cacheEpochMs : cacheEpochMs + (static_cast<uint32_t>(ticks - 1) * resolutionSecs * 1000UL);
|
||||
}
|
||||
|
||||
// Convenience wrappers for each timestamp type (the +1 presence offset lives
|
||||
// in the shared converters above, so these are plain pass-throughs).
|
||||
uint8_t toRelativePosTime(uint32_t nowMs) const { return toRelativeTime(nowMs, posTimeResolution); }
|
||||
uint32_t fromRelativePosTime(uint8_t t) const { return fromRelativeTime(t, posTimeResolution); }
|
||||
|
||||
uint8_t toRelativeRateTime(uint32_t nowMs) const { return toRelativeTime(nowMs, rateTimeResolution); }
|
||||
uint32_t fromRelativeRateTime(uint8_t t) const { return fromRelativeTime(t, rateTimeResolution); }
|
||||
|
||||
uint8_t toRelativeUnknownTime(uint32_t nowMs) const { return toRelativeTime(nowMs, unknownTimeResolution); }
|
||||
uint32_t fromRelativeUnknownTime(uint8_t t) const { return fromRelativeTime(t, unknownTimeResolution); }
|
||||
|
||||
// Coarsest of the per-feature resolutions (seconds per tick).
|
||||
uint16_t maxResolution() const
|
||||
{
|
||||
uint16_t maxRes = posTimeResolution;
|
||||
if (rateTimeResolution > maxRes)
|
||||
maxRes = rateTimeResolution;
|
||||
if (unknownTimeResolution > maxRes)
|
||||
maxRes = unknownTimeResolution;
|
||||
return maxRes;
|
||||
}
|
||||
|
||||
// True when relative offsets approach 8-bit overflow.
|
||||
// With max resolution of 339 sec, 200 ticks = ~19 hours (safe margin for 24h max).
|
||||
bool needsEpochReset(uint32_t nowMs) const { return (nowMs - cacheEpochMs) > (200UL * maxResolution() * 1000UL); }
|
||||
static uint8_t currentPosTick() { return static_cast<uint8_t>(clockMs() / kPosTimeTickMs); }
|
||||
static uint8_t currentRateTick() { return static_cast<uint8_t>((clockMs() / kRateTimeTickMs) & 0x0F); }
|
||||
static uint8_t currentUnknownTick() { return static_cast<uint8_t>((clockMs() / kUnknownTimeTickMs) & 0x0F); }
|
||||
// =========================================================================
|
||||
// Position Fingerprint
|
||||
// =========================================================================
|
||||
@@ -309,6 +268,21 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
// Find existing entry (no creation)
|
||||
UnifiedCacheEntry *findEntry(NodeNum node);
|
||||
|
||||
// Resolve a sender's advertised device role for the position hot path. The tier-3
|
||||
// cache (this entry's getCachedRole) is authoritative and is kept fresh by
|
||||
// updateCachedRoleFromNodeInfo() — updated when NodeDB learns a role, not re-derived
|
||||
// per packet. Only on first tracking (isNew) do we scan NodeDB (hot store → warm
|
||||
// store, via getNodeRole) to seed the cache, so a resident special-role node is
|
||||
// correct from its first position; after that the read is O(1) and survives the node
|
||||
// aging out of both NodeDB stores. Caller must hold cacheLock; entry may be null
|
||||
// (→ NodeDB scan only).
|
||||
meshtastic_Config_DeviceConfig_Role resolveSenderRole(NodeNum from, UnifiedCacheEntry *entry, bool isNew);
|
||||
|
||||
// Refresh the tier-3 role cache from an observed NodeInfo (the same event that updates
|
||||
// NodeDB's role). Reads role from the packet's User payload; updates only nodes already
|
||||
// tracked (no entry creation). Takes cacheLock.
|
||||
void updateCachedRoleFromNodeInfo(const meshtastic_MeshPacket &mp);
|
||||
|
||||
// NodeInfo cache operations (flat PSRAM payload array, linear scan)
|
||||
const NodeInfoPayloadEntry *findNodeInfoEntry(NodeNum node) const;
|
||||
NodeInfoPayloadEntry *findOrCreateNodeInfoEntry(NodeNum node, bool *usedEmptySlot);
|
||||
|
||||
@@ -27,9 +27,6 @@
|
||||
#ifdef HAS_STK8XXX
|
||||
#include "STK8XXXSensor.h"
|
||||
#endif
|
||||
#ifdef HAS_BHI260AP
|
||||
#include "BHI260APSensor.h"
|
||||
#endif
|
||||
|
||||
extern ScanI2C::DeviceAddress accelerometer_found;
|
||||
|
||||
@@ -142,11 +139,6 @@ class AccelerometerThread : public concurrency::OSThread
|
||||
case ScanI2C::DeviceType::QMA6100P:
|
||||
sensor = new QMA6100PSensor(device);
|
||||
break;
|
||||
#endif
|
||||
#ifdef HAS_BHI260AP
|
||||
case ScanI2C::DeviceType::BHI260AP:
|
||||
sensor = new BHI260APSensor(device);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
disable();
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#include "BHI260APSensor.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BHI260AP) && __has_include(<SensorBHI260AP.hpp>)
|
||||
#define BOSCH_BHI260_KLIO
|
||||
#define USING_DATA_HELPER
|
||||
|
||||
#include <BoschFirmware.h>
|
||||
BHI260APSensor::BHI260APSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
|
||||
// https://github.com/lewisxhe/SensorLib/blob/master/examples/Sensors/IMU/BHI260AP_InterruptSettings/BHI260AP_InterruptSettings.ino
|
||||
|
||||
bool BHI260APSensor::init()
|
||||
{
|
||||
LOG_WARN("Initializing BHI260AP sensor %u", deviceAddress());
|
||||
sensor.setFirmware(bosch_firmware_image, bosch_firmware_size, bosch_firmware_type);
|
||||
sensor.setBootFromFlash(bosch_firmware_type);
|
||||
if (sensor.begin(Wire, deviceAddress())) {
|
||||
sensor.setRemapAxes(SensorBHI260AP::TOP_LAYER_BOTTOM_RIGHT_CORNER);
|
||||
BoschSensorInfo info = sensor.getSensorInfo();
|
||||
|
||||
LOG_INFO("Product ID : %02x\n", info.product_id);
|
||||
LOG_INFO("Kernel version : %04u\n", info.kernel_version);
|
||||
LOG_INFO("User version : %04u\n", info.user_version);
|
||||
LOG_INFO("ROM version : %04u\n", info.rom_version);
|
||||
LOG_INFO("Power state : %s\n", (info.host_status & BHY2_HST_POWER_STATE) ? "sleeping" : "active");
|
||||
LOG_INFO("Host interface : %s\n", (info.host_status & BHY2_HST_HOST_PROTOCOL) ? "SPI" : "I2C");
|
||||
LOG_INFO("Feature status : 0x%02x\n", info.feat_status);
|
||||
|
||||
stepCounter = new SensorStepCounter(sensor);
|
||||
// stepDetector = new SensorStepDetector(sensor);
|
||||
|
||||
// sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE);
|
||||
// sensor.enableAccelerometer();
|
||||
// sensor.configInterrupt();
|
||||
|
||||
#ifdef BHI260AP_INT
|
||||
pinMode(BHI260AP_INT, INPUT);
|
||||
attachInterrupt(
|
||||
BHI260AP_INT,
|
||||
[] {
|
||||
// Set interrupt to set irq value to true
|
||||
},
|
||||
RISING); // Select the interrupt mode according to the actual circuit
|
||||
#endif
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
// Need to raise the wrist function, need to set the correct axis
|
||||
sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER);
|
||||
#else
|
||||
// sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER);
|
||||
#endif
|
||||
|
||||
// stepDetector->enable(1.0, 0);
|
||||
stepCounter->enable(1.0, 0);
|
||||
LOG_DEBUG("BHI260AP init ok");
|
||||
return true;
|
||||
}
|
||||
LOG_DEBUG("BHI260AP init failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t BHI260APSensor::runOnce()
|
||||
{
|
||||
sensor.update();
|
||||
if (stepCounter->hasUpdated()) {
|
||||
steps = stepCounter->getStepCount();
|
||||
LOG_WARN("Step count updated: %u", steps);
|
||||
if (screen)
|
||||
screen->steps = steps;
|
||||
}
|
||||
// LOG_WARN("Step count: %u", stepCounter->getStepCount());
|
||||
// if (sensor.readIrqStatus()) {
|
||||
// if (sensor.isTilt() || sensor.isDoubleTap()) {
|
||||
// wakeScreen();
|
||||
// return 500;
|
||||
// }
|
||||
//}
|
||||
return 1000;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
#ifndef _BHI260AP_SENSOR_H_
|
||||
#define _BHI260AP_SENSOR_H_
|
||||
|
||||
#include "MotionSensor.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BHI260AP) && __has_include(<SensorBHI260AP.hpp>)
|
||||
|
||||
// Sensor lib
|
||||
#include <SensorBHI260AP.hpp>
|
||||
#include <Wire.h>
|
||||
#include <bosch/BoschSensorDataHelper.hpp>
|
||||
|
||||
class BHI260APSensor : public MotionSensor
|
||||
{
|
||||
private:
|
||||
SensorBHI260AP sensor;
|
||||
volatile bool BHI_IRQ = false;
|
||||
SensorStepCounter *stepCounter;
|
||||
SensorStepDetector *stepDetector;
|
||||
uint32_t steps = 0;
|
||||
|
||||
public:
|
||||
explicit BHI260APSensor(ScanI2C::FoundDevice foundDevice);
|
||||
virtual bool init() override;
|
||||
virtual int32_t runOnce() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -206,8 +206,6 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L
|
||||
#elif defined(HELTEC_WIRELESS_TRACKER_V2)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2
|
||||
#elif defined(T_WATCH_ULTRA)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_WATCH_ULTRA
|
||||
#elif defined(M5STACK_CARDPUTER_ADV)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV
|
||||
#else
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Workaround for the IDF 5.5 manual esp_flash read regression on t-watch-ultra.
|
||||
//
|
||||
// On this board (Winbond W25Q128JW, ef:8018), the IDF 5.5 *direct* flash read path
|
||||
// (esp_flash_read / esp_partition_read) returns 0x00 for data that is physically
|
||||
// correct on flash. We proved the *memory-mapped* (cache) read returns the right
|
||||
// data, writes work, and it's not read-mode/HPM/timing-tuning/PSRAM. So route every
|
||||
// esp_partition_read through esp_partition_mmap + memcpy, which uses the working
|
||||
// cache path. Activated by `-Wl,--wrap=esp_partition_read` (t-watch-ultra only).
|
||||
#if defined(T_WATCH_ULTRA)
|
||||
|
||||
#include "esp_partition.h"
|
||||
#include <string.h>
|
||||
|
||||
extern esp_err_t __real_esp_partition_read(const esp_partition_t *partition, size_t src_offset, void *dst, size_t size);
|
||||
|
||||
esp_err_t __wrap_esp_partition_read(const esp_partition_t *partition, size_t src_offset, void *dst, size_t size)
|
||||
{
|
||||
if (partition == NULL || dst == NULL)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
if (size == 0)
|
||||
return ESP_OK;
|
||||
|
||||
// mmap requires a 64KB-aligned start; map the containing page span and copy
|
||||
// out from the requested offset.
|
||||
const size_t PAGE = 0x10000;
|
||||
size_t aligned = src_offset & ~(PAGE - 1);
|
||||
size_t delta = src_offset - aligned;
|
||||
|
||||
const void *ptr = NULL;
|
||||
esp_partition_mmap_handle_t handle;
|
||||
esp_err_t err = esp_partition_mmap(partition, aligned, delta + size, ESP_PARTITION_MMAP_DATA, &ptr, &handle);
|
||||
if (err != ESP_OK) {
|
||||
// Encrypted partitions / regions mmap can't serve: fall back to the real
|
||||
// read (may be wrong on this board, but better than failing the call).
|
||||
return __real_esp_partition_read(partition, src_offset, dst, size);
|
||||
}
|
||||
memcpy(dst, (const uint8_t *)ptr + delta, size);
|
||||
esp_partition_munmap(handle);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#endif // T_WATCH_ULTRA
|
||||
@@ -1,23 +0,0 @@
|
||||
// Free up some precious space in the iram0_0_seg memory segment
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <esp_attr.h>
|
||||
#include <esp_flash.h>
|
||||
#include <spi_flash_chip_driver.h>
|
||||
|
||||
#define IRAM_SECTION section(".iram1.stub")
|
||||
|
||||
IRAM_ATTR esp_err_t stub_probe(esp_flash_t *chip, uint32_t flash_id)
|
||||
{
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
const spi_flash_chip_t stub_flash_chip __attribute__((IRAM_SECTION)) = {
|
||||
.name = "stub",
|
||||
.probe = stub_probe,
|
||||
};
|
||||
|
||||
extern const spi_flash_chip_t __wrap_esp_flash_chip_gd __attribute__((IRAM_SECTION, alias("stub_flash_chip")));
|
||||
extern const spi_flash_chip_t __wrap_esp_flash_chip_issi __attribute__((IRAM_SECTION, alias("stub_flash_chip")));
|
||||
extern const spi_flash_chip_t __wrap_esp_flash_chip_winbond __attribute__((IRAM_SECTION, alias("stub_flash_chip")));
|
||||
@@ -33,13 +33,7 @@ void variant_shutdown() {}
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
void setBluetoothEnable(bool enable)
|
||||
{
|
||||
#if defined(USE_WS5500) || defined(USE_CH390D)
|
||||
if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false))
|
||||
#elif HAS_WIFI
|
||||
if (!isWifiAvailable() && config.bluetooth.enabled == true)
|
||||
#else
|
||||
if (config.bluetooth.enabled == true)
|
||||
#endif
|
||||
{
|
||||
if (!nimbleBluetooth) {
|
||||
nimbleBluetooth = new NimbleBluetooth();
|
||||
@@ -48,9 +42,6 @@ void setBluetoothEnable(bool enable)
|
||||
powerMon->setState(meshtastic_PowerMon_State_BT_On);
|
||||
nimbleBluetooth->setup();
|
||||
}
|
||||
// For ESP32, no way to recover from bluetooth shutdown without reboot
|
||||
// BLE advertising automatically stops when MCU enters light-sleep(?)
|
||||
// For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse
|
||||
}
|
||||
}
|
||||
#else
|
||||
|
||||
@@ -7,8 +7,6 @@ This directory tree is designed to solve two problems.
|
||||
|
||||
So we are borrowing the initVariant() ideas here (by using weak gcc references). You can now define earlyInitVariant() and lateInitVariant() if your board needs them. earlyInitVariant() runs at the beginning of setup() directly after waitUntilPowerLevelSafe(); while lateInitVariant() runs after the LoRa radio is initialized.
|
||||
|
||||
**Important:** earlyInitVariant() runs _before_ consoleInit(), so the logging subsystem isn't set up yet. Calling a `LOG_*` macro there **crashes the device** — it is not a silent no-op. Never use `LOG_*` in earlyInitVariant(); defer any logging to lateInitVariant() or later.
|
||||
|
||||
If you'd like a board specific variant to be run, add the variant.cpp file to an appropriately named
|
||||
subdirectory and check for \_VARIANT_boardname in the cpp file (so that your code is only built for your board).
|
||||
You'll need to define \_VARIANT_boardname in your corresponding variant.h file.
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef T_WATCH_ULTRA
|
||||
|
||||
// Board-specific init lives here (rather than in variants/esp32s3/t-watch-ultra/variant.cpp)
|
||||
// so that PlatformIO's library dependency finder can resolve headers such as
|
||||
// input/TouchScreenImpl1.h (which transitively pulls in the ArduinoThread "Thread.h"),
|
||||
// ExtensionIOXL9555.hpp and TouchDrvCSTXXX.hpp. Files compiled from outside src/ only get
|
||||
// include paths for libraries they reference directly, so the transitive Thread.h include
|
||||
// is not found there. See src/platform/extra_variants/README.md.
|
||||
|
||||
#include "TouchDrvCSTXXX.hpp"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
#include <ExtensionIOXL9555.hpp>
|
||||
#include <Wire.h>
|
||||
|
||||
static ExtensionIOXL9555 io;
|
||||
static TouchDrvCST92xx touchDrv;
|
||||
|
||||
void earlyInitVariant()
|
||||
{
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(DISP_CS, OUTPUT);
|
||||
digitalWrite(DISP_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(NFC_CS, OUTPUT);
|
||||
digitalWrite(NFC_CS, HIGH);
|
||||
pinMode(I2C_SDA, INPUT_PULLUP);
|
||||
pinMode(I2C_SCL, INPUT_PULLUP);
|
||||
|
||||
if (io.begin(Wire, XL9555_SLAVE_ADDRESS0)) {
|
||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
||||
delay(1);
|
||||
io.pinMode(EXPANDS_DISP_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DISP_EN, HIGH);
|
||||
delay(1);
|
||||
io.pinMode(EXPANDS_TOUCH_RST, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_TOUCH_RST, LOW);
|
||||
delay(20);
|
||||
io.digitalWrite(EXPANDS_TOUCH_RST, HIGH);
|
||||
delay(60);
|
||||
io.pinMode(EXPANDS_LORA_RF_SW, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_LORA_RF_SW, HIGH); // set RF switch to built-in LoRa antenna
|
||||
// io.pinMode(EXPANDS_SD_DET, INPUT);
|
||||
}
|
||||
// NOTE: deliberately no LOG_* on the io.begin() failure path. earlyInitVariant() runs
|
||||
// before consoleInit(), where calling a LOG_* macro crashes the device (see
|
||||
// extra_variants/README.md). On failure the EXPANDS_* pins stay on their defaults.
|
||||
}
|
||||
|
||||
static bool readTouch(int16_t *x, int16_t *y)
|
||||
{
|
||||
int16_t x_array[1], y_array[1];
|
||||
uint8_t touched = touchDrv.getPoint(x_array, y_array, 1);
|
||||
if (touched > 0) {
|
||||
*x = (x_array[0]);
|
||||
*y = (y_array[0]);
|
||||
// Check bounds
|
||||
if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
return true; // Valid touch detected
|
||||
}
|
||||
return false; // No valid touch data
|
||||
}
|
||||
|
||||
void lateInitVariant()
|
||||
{
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP);
|
||||
touchDrv.setPins(-1, SCREEN_TOUCH_INT);
|
||||
if (touchDrv.begin(Wire, TOUCH_SLAVE_ADDRESS, -1, -1)) {
|
||||
touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch);
|
||||
touchScreenImpl1->init();
|
||||
} else {
|
||||
LOG_ERROR("failed to initialize CST92xx");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -89,6 +89,8 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_CARD
|
||||
#elif defined(TTGO_T_ECHO_PLUS)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_PLUS
|
||||
#elif defined(T_IMPULSE_PLUS)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_IMPULSE_PLUS
|
||||
#elif defined(ELECROW_ThinkNode_M1)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1
|
||||
#elif defined(ELECROW_ThinkNode_M3)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "SGM41562.h"
|
||||
|
||||
#ifdef HAS_SGM41562
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
SGM41562 *sgm41562 = nullptr;
|
||||
|
||||
bool initSGM41562(TwoWire &wire)
|
||||
{
|
||||
if (sgm41562)
|
||||
return true;
|
||||
sgm41562 = new SGM41562();
|
||||
if (!sgm41562->begin(wire)) {
|
||||
delete sgm41562;
|
||||
sgm41562 = nullptr;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SGM41562::readReg(uint8_t reg, uint8_t &value)
|
||||
{
|
||||
wire_->beginTransmission(address_);
|
||||
wire_->write(reg);
|
||||
if (wire_->endTransmission(false) != 0)
|
||||
return false;
|
||||
if (wire_->requestFrom((int)address_, 1) != 1)
|
||||
return false;
|
||||
value = wire_->read();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SGM41562::writeReg(uint8_t reg, uint8_t value)
|
||||
{
|
||||
wire_->beginTransmission(address_);
|
||||
wire_->write(reg);
|
||||
wire_->write(value);
|
||||
return wire_->endTransmission() == 0;
|
||||
}
|
||||
|
||||
bool SGM41562::updateReg(uint8_t reg, uint8_t mask, uint8_t value)
|
||||
{
|
||||
uint8_t cur;
|
||||
if (!readReg(reg, cur))
|
||||
return false;
|
||||
cur = (cur & ~mask) | (value & mask);
|
||||
return writeReg(reg, cur);
|
||||
}
|
||||
|
||||
bool SGM41562::begin(TwoWire &wire, uint8_t address)
|
||||
{
|
||||
wire_ = &wire;
|
||||
address_ = address;
|
||||
|
||||
uint8_t id;
|
||||
if (!readReg(REG_DEVICE_ID, id)) {
|
||||
LOG_WARN("SGM41562: I2C read failed at 0x%02X", address_);
|
||||
return false;
|
||||
}
|
||||
if (id != DEVICE_ID_EXPECTED) {
|
||||
LOG_WARN("SGM41562: unexpected device ID 0x%02X (expected 0x%02X)", id, DEVICE_ID_EXPECTED);
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("SGM41562: detected at 0x%02X (id 0x%02X)", address_, id);
|
||||
|
||||
// Mirror the vendor reference init sequence: PCB OTP off, NTC off,
|
||||
// watchdog off, charger enabled. These match LilyGo's stock firmware
|
||||
// for the T-Impulse Plus.
|
||||
delay(120);
|
||||
writeReg(REG_SYS_VOLTAGE_REG, 0xB7);
|
||||
writeReg(REG_MISC_OP_CONTROL, 0x40);
|
||||
writeReg(REG_CHARGE_TERM_TIMER, 0x1A);
|
||||
writeReg(REG_POWER_ON_CFG, 0xA4);
|
||||
|
||||
return refresh();
|
||||
}
|
||||
|
||||
bool SGM41562::refresh()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
if (lastRefreshMs_ != 0 && (now - lastRefreshMs_) < 250)
|
||||
return true; // cached
|
||||
lastRefreshMs_ = now == 0 ? 1 : now;
|
||||
|
||||
uint8_t status, fault;
|
||||
if (!readReg(REG_SYSTEM_STATUS, status))
|
||||
return false;
|
||||
if (!readReg(REG_FAULT, fault))
|
||||
return false;
|
||||
|
||||
chargeStatus_ = static_cast<ChargeStatus>((status >> SYS_STATUS_CHRG_SHIFT) & SYS_STATUS_CHRG_MASK);
|
||||
inputPowerGood_ = (status & SYS_STATUS_PG) != 0;
|
||||
thermalReg_ = (status & SYS_STATUS_THERM_REG) != 0;
|
||||
faultMask_ = fault & 0x3F; // bits [7:6] are enter_ship_time config, not faults
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SGM41562::setChargeEnable(bool enable)
|
||||
{
|
||||
return updateReg(REG_POWER_ON_CFG, POWER_ON_CFG_CHG_DISABLE, enable ? 0x00 : POWER_ON_CFG_CHG_DISABLE);
|
||||
}
|
||||
|
||||
bool SGM41562::setShippingModeEnable(bool enable)
|
||||
{
|
||||
return updateReg(REG_MISC_OP_CONTROL, MISC_OP_SHIPPING_MODE, enable ? MISC_OP_SHIPPING_MODE : 0x00);
|
||||
}
|
||||
|
||||
#endif // HAS_SGM41562
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef HAS_SGM41562
|
||||
|
||||
#include <Wire.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// SG Micro SGM41562 — single-cell Li-ion buck charger, I²C-controlled, no
|
||||
// fuel gauge. This driver exposes status (charging / input good / fault),
|
||||
// charge enable, and shipping-mode control. Battery voltage/percent still
|
||||
// come from the platform ADC path; the charger is plumbed in as a
|
||||
// side-channel for isCharging()/isVbusIn() in AnalogBatteryLevel.
|
||||
//
|
||||
// Reference: SGM41562 datasheet (Cmd map + bit fields cross-verified against
|
||||
// LilyGo's `Cpp_Bus_Driver::Sgm41562xx` driver, which is what their vendor
|
||||
// example for this board uses).
|
||||
|
||||
#ifndef SGM41562_ADDR
|
||||
#define SGM41562_ADDR 0x03 // Per datasheet — unusual but correct
|
||||
#endif
|
||||
|
||||
#ifndef SGM41562_WIRE
|
||||
#define SGM41562_WIRE Wire1 // Most boards put the PMU on the secondary bus
|
||||
#endif
|
||||
|
||||
class SGM41562
|
||||
{
|
||||
public:
|
||||
enum class ChargeStatus : uint8_t {
|
||||
NotCharging = 0b00,
|
||||
Precharge = 0b01,
|
||||
FastCharge = 0b10,
|
||||
ChargeDone = 0b11,
|
||||
};
|
||||
|
||||
bool begin(TwoWire &wire, uint8_t address = SGM41562_ADDR);
|
||||
|
||||
// Re-read the system status + fault registers. Throttled internally to
|
||||
// at most one I²C transaction per 250 ms — call as often as you like.
|
||||
bool refresh();
|
||||
|
||||
// Status — cached from the most recent refresh().
|
||||
ChargeStatus chargeStatus() const { return chargeStatus_; }
|
||||
bool isCharging() const { return chargeStatus_ == ChargeStatus::Precharge || chargeStatus_ == ChargeStatus::FastCharge; }
|
||||
bool isChargeDone() const { return chargeStatus_ == ChargeStatus::ChargeDone; }
|
||||
bool isInputPowerGood() const { return inputPowerGood_; }
|
||||
bool isThermalRegulation() const { return thermalReg_; }
|
||||
uint8_t faultMask() const { return faultMask_; }
|
||||
|
||||
// Control.
|
||||
bool setChargeEnable(bool enable);
|
||||
bool setShippingModeEnable(bool enable);
|
||||
|
||||
private:
|
||||
TwoWire *wire_ = nullptr;
|
||||
uint8_t address_ = SGM41562_ADDR;
|
||||
uint32_t lastRefreshMs_ = 0;
|
||||
|
||||
ChargeStatus chargeStatus_ = ChargeStatus::NotCharging;
|
||||
bool inputPowerGood_ = false;
|
||||
bool thermalReg_ = false;
|
||||
uint8_t faultMask_ = 0;
|
||||
|
||||
bool readReg(uint8_t reg, uint8_t &value);
|
||||
bool writeReg(uint8_t reg, uint8_t value);
|
||||
bool updateReg(uint8_t reg, uint8_t mask, uint8_t value);
|
||||
|
||||
// SGM41562 register addresses
|
||||
static constexpr uint8_t REG_INPUT_SOURCE = 0x00;
|
||||
static constexpr uint8_t REG_POWER_ON_CFG = 0x01;
|
||||
static constexpr uint8_t REG_CHARGE_CURRENT = 0x02;
|
||||
static constexpr uint8_t REG_DISCHARGE_TERM_CURRENT = 0x03;
|
||||
static constexpr uint8_t REG_CHARGE_VOLTAGE = 0x04;
|
||||
static constexpr uint8_t REG_CHARGE_TERM_TIMER = 0x05;
|
||||
static constexpr uint8_t REG_MISC_OP_CONTROL = 0x06;
|
||||
static constexpr uint8_t REG_SYS_VOLTAGE_REG = 0x07;
|
||||
static constexpr uint8_t REG_SYSTEM_STATUS = 0x08;
|
||||
static constexpr uint8_t REG_FAULT = 0x09;
|
||||
static constexpr uint8_t REG_I2C_ADDR_MISC = 0x0A;
|
||||
static constexpr uint8_t REG_DEVICE_ID = 0x0B;
|
||||
|
||||
// Bit positions in REG_POWER_ON_CFG.
|
||||
static constexpr uint8_t POWER_ON_CFG_CHG_DISABLE = 0x08; // bit 3: 1 = charging disabled
|
||||
// Bit positions in REG_MISC_OP_CONTROL.
|
||||
static constexpr uint8_t MISC_OP_SHIPPING_MODE = 0x20; // bit 5: 1 = enter shipping mode
|
||||
// Bit positions in REG_SYSTEM_STATUS.
|
||||
static constexpr uint8_t SYS_STATUS_CHRG_SHIFT = 3;
|
||||
static constexpr uint8_t SYS_STATUS_CHRG_MASK = 0x03;
|
||||
static constexpr uint8_t SYS_STATUS_PG = 0x02; // bit 1: input power good
|
||||
static constexpr uint8_t SYS_STATUS_THERM_REG = 0x01; // bit 0: thermal regulation
|
||||
|
||||
static constexpr uint8_t DEVICE_ID_EXPECTED = 0x04;
|
||||
};
|
||||
|
||||
extern SGM41562 *sgm41562;
|
||||
|
||||
// Lazy-instantiate the global on the supplied wire. Returns true on success.
|
||||
bool initSGM41562(TwoWire &wire);
|
||||
|
||||
#endif // HAS_SGM41562
|
||||
+1
-1
@@ -361,7 +361,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
||||
// t-beam v1.2 radio power channel
|
||||
PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel
|
||||
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE ||
|
||||
HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3 || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA) {
|
||||
HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
|
||||
PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel
|
||||
}
|
||||
} else if (model == XPOWERS_AXP192) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "TestUtil.h"
|
||||
#include "modules/PositionModule.h"
|
||||
#include <unity.h>
|
||||
|
||||
// These exercise PositionModule's pure broadcast-policy helpers (stationary detection and the
|
||||
// interval floor). They take plain values, so no device globals or fake clock are needed.
|
||||
|
||||
// Coordinates sharing the top `precision` bits land in the same grid cell.
|
||||
static void test_withinPrecisionCell_jitterStaysInCell()
|
||||
{
|
||||
// At precision 16 the top 16 bits define the cell; the low 16 bits are GPS jitter.
|
||||
TEST_ASSERT_TRUE(PositionModule::positionWithinPrecisionCell(0x12340000, 0x22340000, 0x1234ABCD, 0x2234EF01, 16));
|
||||
}
|
||||
|
||||
static void test_withinPrecisionCell_movingLatLeavesCell()
|
||||
{
|
||||
TEST_ASSERT_FALSE(PositionModule::positionWithinPrecisionCell(0x12340000, 0x22340000, 0x12350000, 0x22340000, 16));
|
||||
}
|
||||
|
||||
static void test_withinPrecisionCell_movingLonLeavesCell()
|
||||
{
|
||||
TEST_ASSERT_FALSE(PositionModule::positionWithinPrecisionCell(0x12340000, 0x22340000, 0x12340000, 0x22350000, 16));
|
||||
}
|
||||
|
||||
// precision 0 means position sharing is off — never treat as stationary/suppressible.
|
||||
static void test_withinPrecisionCell_zeroPrecisionNeverSuppresses()
|
||||
{
|
||||
TEST_ASSERT_FALSE(PositionModule::positionWithinPrecisionCell(0x12340000, 0x22340000, 0x12340000, 0x22340000, 0));
|
||||
}
|
||||
|
||||
// Full precision (>=32): any difference matters, and identical full-precision coords still aren't
|
||||
// "stationary" because there's no coarse cell to hold within.
|
||||
static void test_withinPrecisionCell_fullPrecisionNeverSuppresses()
|
||||
{
|
||||
TEST_ASSERT_FALSE(PositionModule::positionWithinPrecisionCell(0x12340000, 0x22340000, 0x12340000, 0x22340000, 32));
|
||||
}
|
||||
|
||||
static void test_effectiveInterval_stationaryRaisesToFloor()
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(43200000U, PositionModule::effectiveBroadcastIntervalMs(60000U, true, 43200000U));
|
||||
}
|
||||
|
||||
static void test_effectiveInterval_movingKeepsConfigured()
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(60000U, PositionModule::effectiveBroadcastIntervalMs(60000U, false, 43200000U));
|
||||
}
|
||||
|
||||
// A configured interval already longer than the floor is never shortened.
|
||||
static void test_effectiveInterval_longConfiguredWinsOverFloor()
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(50000000U, PositionModule::effectiveBroadcastIntervalMs(50000000U, true, 43200000U));
|
||||
}
|
||||
|
||||
static void test_effectiveInterval_zeroFloorIsNoOp()
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(60000U, PositionModule::effectiveBroadcastIntervalMs(60000U, true, 0U));
|
||||
}
|
||||
|
||||
void setUp(void) {}
|
||||
|
||||
void tearDown(void) {}
|
||||
|
||||
extern "C" {
|
||||
void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_withinPrecisionCell_jitterStaysInCell);
|
||||
RUN_TEST(test_withinPrecisionCell_movingLatLeavesCell);
|
||||
RUN_TEST(test_withinPrecisionCell_movingLonLeavesCell);
|
||||
RUN_TEST(test_withinPrecisionCell_zeroPrecisionNeverSuppresses);
|
||||
RUN_TEST(test_withinPrecisionCell_fullPrecisionNeverSuppresses);
|
||||
RUN_TEST(test_effectiveInterval_stationaryRaisesToFloor);
|
||||
RUN_TEST(test_effectiveInterval_movingKeepsConfigured);
|
||||
RUN_TEST(test_effectiveInterval_longConfiguredWinsOverFloor);
|
||||
RUN_TEST(test_effectiveInterval_zeroFloorIsNoOp);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "TestUtil.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unity.h>
|
||||
|
||||
// Regression coverage for issue #9828: on boards without a hardware RTC (e.g. RP2040),
|
||||
// gettimeofday() can return uptime seconds rather than wall-clock time. A later readFromRTC()
|
||||
// must not overwrite a higher-quality network/GPS time with that value, but it should still seed
|
||||
// the clock when nothing better exists yet.
|
||||
//
|
||||
// The native test build compiles the RV3028 hardware-RTC branch (variants/native/portduino
|
||||
// defines RV3028_RTC), so these tests use setReadFromRTCUseSystemTimeForTests() to force the
|
||||
// no-hardware-RTC fallback path and setRTCSystemTimeForTests() to inject a deterministic clock.
|
||||
|
||||
static const uint32_t kAllowedDriftSeconds = 2;
|
||||
static const time_t kUptimeSeconds = 21; // what gettimeofday() returns on RP2040 without a real clock
|
||||
|
||||
// A clearly-valid wall-clock epoch, safely inside any BUILD_EPOCH validity window.
|
||||
static time_t makeValidEpoch()
|
||||
{
|
||||
return time(NULL) + SEC_PER_DAY;
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
resetRTCStateForTests();
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
resetRTCStateForTests();
|
||||
}
|
||||
|
||||
// A higher-quality network time must survive a later system-time read that only knows uptime.
|
||||
static void test_readFromRTC_preserves_better_network_time(void)
|
||||
{
|
||||
const time_t networkEpoch = makeValidEpoch();
|
||||
struct timeval networkTime;
|
||||
networkTime.tv_sec = networkEpoch;
|
||||
networkTime.tv_usec = 0;
|
||||
TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, perhapsSetRTC(RTCQualityFromNet, &networkTime));
|
||||
|
||||
// Simulate a later readFromRTC() falling back to a system clock that only knows uptime.
|
||||
struct timeval uptime;
|
||||
uptime.tv_sec = kUptimeSeconds;
|
||||
uptime.tv_usec = 0;
|
||||
setRTCSystemTimeForTests(&uptime);
|
||||
setReadFromRTCUseSystemTimeForTests(true);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC());
|
||||
TEST_ASSERT_EQUAL_INT(RTCQualityFromNet, getRTCQuality());
|
||||
TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)networkEpoch, getValidTime(RTCQualityFromNet));
|
||||
}
|
||||
|
||||
// Before any higher-quality source exists, the fallback should still seed the clock.
|
||||
static void test_readFromRTC_initializes_time_when_no_better_source(void)
|
||||
{
|
||||
const time_t systemEpoch = makeValidEpoch();
|
||||
struct timeval systemTime;
|
||||
systemTime.tv_sec = systemEpoch;
|
||||
systemTime.tv_usec = 0;
|
||||
setRTCSystemTimeForTests(&systemTime);
|
||||
setReadFromRTCUseSystemTimeForTests(true);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC());
|
||||
TEST_ASSERT_EQUAL_INT(RTCQualityNone, getRTCQuality());
|
||||
TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)systemEpoch, getTime());
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
delay(10);
|
||||
initializeTestEnvironment();
|
||||
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_readFromRTC_preserves_better_network_time);
|
||||
RUN_TEST(test_readFromRTC_initializes_time_when_no_better_source);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "airtime.h"
|
||||
#include "mesh/CryptoEngine.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/MeshService.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
#include "mesh/Router.h"
|
||||
@@ -78,6 +79,13 @@ class MockNodeDB : public NodeDB
|
||||
// Role the TMM should see for the cached node (sender-role-aware throttles).
|
||||
void setCachedNodeRole(meshtastic_Config_DeviceConfig_Role role) { cachedNode.role = role; }
|
||||
|
||||
// Direct mutable access to the cached node for fine-grained bitfield manipulation in tests.
|
||||
meshtastic_NodeInfoLite &cachedNodeForTest()
|
||||
{
|
||||
hasCachedNode = true;
|
||||
return cachedNode;
|
||||
}
|
||||
|
||||
// Seed a node into the hot-store buffer at index 1 (index 0 is reserved for
|
||||
// "self"). Respects the fixed-buffer invariant: `meshNodes` is a buffer of
|
||||
// MAX_NUM_NODES slots with `numMeshNodes` as the logical count — we grow the
|
||||
@@ -149,8 +157,9 @@ class TrafficManagementModuleTestShim : public TrafficManagementModule
|
||||
{
|
||||
public:
|
||||
using TrafficManagementModule::alterReceived;
|
||||
using TrafficManagementModule::flushCache;
|
||||
using TrafficManagementModule::handleReceived;
|
||||
using TrafficManagementModule::resetEpoch;
|
||||
using TrafficManagementModule::peekCachedRole;
|
||||
using TrafficManagementModule::runOnce;
|
||||
|
||||
bool ignoreRequestFlag() const { return ignoreRequest; }
|
||||
@@ -163,7 +172,6 @@ static void resetTrafficConfig()
|
||||
moduleConfig = meshtastic_LocalModuleConfig_init_zero;
|
||||
moduleConfig.has_traffic_management = true;
|
||||
moduleConfig.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero;
|
||||
moduleConfig.traffic_management.enabled = true;
|
||||
|
||||
config = meshtastic_LocalConfig_init_zero;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
@@ -179,6 +187,10 @@ static void resetTrafficConfig()
|
||||
mockNodeDB->resetNodes();
|
||||
mockNodeDB->clearCachedNode();
|
||||
nodeDB = mockNodeDB;
|
||||
|
||||
// Virtual clock base (1 h in, so tick subtraction never underflows). Tests advance time by
|
||||
// bumping TrafficManagementModule::s_testNowMs instead of sleeping real seconds across a tick.
|
||||
TrafficManagementModule::s_testNowMs = 3600000;
|
||||
}
|
||||
|
||||
static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to = NODENUM_BROADCAST)
|
||||
@@ -261,6 +273,16 @@ static void installWellKnownPrimaryChannel()
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
}
|
||||
|
||||
// Install the well-known primary channel AND set a specific position_precision so
|
||||
// shouldDropPosition() uses that precision ceiling rather than the default fallback.
|
||||
// precision=0 means "no channel ceiling" and falls back to the firmware default (19 bits).
|
||||
static void installWellKnownPrimaryChannelWithPrecision(uint32_t precision)
|
||||
{
|
||||
installWellKnownPrimaryChannel();
|
||||
channelFile.channels[0].settings.has_module_settings = true;
|
||||
channelFile.channels[0].settings.module_settings.position_precision = precision;
|
||||
}
|
||||
|
||||
static meshtastic_MeshPacket makeNodeInfoPacket(NodeNum from, const char *longName, const char *shortName)
|
||||
{
|
||||
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, from, NODENUM_BROADCAST);
|
||||
@@ -275,6 +297,21 @@ static meshtastic_MeshPacket makeNodeInfoPacket(NodeNum from, const char *longNa
|
||||
return packet;
|
||||
}
|
||||
|
||||
static meshtastic_MeshPacket makeNodeInfoPacketWithRole(NodeNum from, meshtastic_Config_DeviceConfig_Role role)
|
||||
{
|
||||
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, from, NODENUM_BROADCAST);
|
||||
|
||||
meshtastic_User user = meshtastic_User_init_zero;
|
||||
snprintf(user.id, sizeof(user.id), "!%08x", from);
|
||||
strncpy(user.long_name, "rolenode", sizeof(user.long_name) - 1);
|
||||
strncpy(user.short_name, "rn", sizeof(user.short_name) - 1);
|
||||
user.role = role;
|
||||
|
||||
packet.decoded.payload.size =
|
||||
pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_User_msg, &user);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the module is a no-op when traffic management is disabled.
|
||||
* Important so config toggles cannot accidentally change routing behavior.
|
||||
@@ -300,7 +337,6 @@ static void test_tm_moduleDisabled_doesNothing(void)
|
||||
*/
|
||||
static void test_tm_unknownPackets_dropOnNPlusOne(void)
|
||||
{
|
||||
moduleConfig.traffic_management.drop_unknown_enabled = true;
|
||||
moduleConfig.traffic_management.unknown_packet_threshold = 2;
|
||||
TrafficManagementModuleTestShim module;
|
||||
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode);
|
||||
@@ -324,9 +360,8 @@ static void test_tm_unknownPackets_dropOnNPlusOne(void)
|
||||
*/
|
||||
static void test_tm_positionDedup_dropsDuplicateWithinWindow(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
@@ -349,9 +384,8 @@ static void test_tm_positionDedup_dropsDuplicateWithinWindow(void)
|
||||
*/
|
||||
static void test_tm_positionDedup_allowsMovedPosition(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
@@ -372,7 +406,6 @@ static void test_tm_positionDedup_allowsMovedPosition(void)
|
||||
*/
|
||||
static void test_tm_rateLimit_dropsOnlyAfterThreshold(void)
|
||||
{
|
||||
moduleConfig.traffic_management.rate_limit_enabled = true;
|
||||
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
||||
moduleConfig.traffic_management.rate_limit_max_packets = 3;
|
||||
TrafficManagementModuleTestShim module;
|
||||
@@ -397,12 +430,10 @@ static void test_tm_rateLimit_dropsOnlyAfterThreshold(void)
|
||||
*/
|
||||
static void test_tm_fromUs_bypassesPositionAndRateFilters(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
moduleConfig.traffic_management.rate_limit_enabled = true;
|
||||
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
||||
moduleConfig.traffic_management.rate_limit_max_packets = 1;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket positionPacket = makePositionPacket(kLocalNode, 374221234, -1220845678);
|
||||
@@ -428,12 +459,10 @@ static void test_tm_fromUs_bypassesPositionAndRateFilters(void)
|
||||
*/
|
||||
static void test_tm_localDestination_bypassesTransitFilters(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
moduleConfig.traffic_management.rate_limit_enabled = true;
|
||||
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
||||
moduleConfig.traffic_management.rate_limit_max_packets = 1;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket position1 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode);
|
||||
@@ -461,7 +490,6 @@ static void test_tm_localDestination_bypassesTransitFilters(void)
|
||||
*/
|
||||
static void test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops(void)
|
||||
{
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER;
|
||||
mockNodeDB->setCachedNode(kTargetNode);
|
||||
@@ -486,7 +514,6 @@ static void test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops(void)
|
||||
*/
|
||||
static void test_tm_nodeinfo_directResponse_respondsFromCache(void)
|
||||
{
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
config.lora.config_ok_to_mqtt = true;
|
||||
@@ -532,7 +559,6 @@ static void test_tm_nodeinfo_directResponse_respondsFromCache(void)
|
||||
*/
|
||||
static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
|
||||
{
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
mockNodeDB->setCachedNode(kTargetNode);
|
||||
@@ -568,7 +594,6 @@ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
|
||||
*/
|
||||
static void test_tm_nodeinfo_clientClamp_skipsWhenNotDirect(void)
|
||||
{
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
mockNodeDB->setCachedNode(kTargetNode);
|
||||
@@ -595,7 +620,6 @@ static void test_tm_nodeinfo_clientClamp_skipsWhenNotDirect(void)
|
||||
*/
|
||||
static void test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips(void)
|
||||
{
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
mockNodeDB->clearCachedNode();
|
||||
@@ -629,7 +653,6 @@ static void test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips(void)
|
||||
*/
|
||||
static void test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield(void)
|
||||
{
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
config.lora.config_ok_to_mqtt = true;
|
||||
@@ -683,7 +706,6 @@ static void test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfie
|
||||
*/
|
||||
static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(void)
|
||||
{
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response = true;
|
||||
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
mockNodeDB->setCachedNode(kTargetNode);
|
||||
@@ -711,15 +733,13 @@ static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(voi
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Verify relayed telemetry broadcasts are hop-exhausted when enabled AND the
|
||||
* channel is congested (telemetry exhaustion is gated on channel utilization,
|
||||
* unlike position exhaustion).
|
||||
* Important to prevent further mesh propagation while still allowing one relay step.
|
||||
* Verify relayed telemetry broadcasts are NOT hop-exhausted.
|
||||
* exhaust_hop_telemetry / exhaust_hop_position have been removed from the config
|
||||
* as "not suitable right now" — alterReceived must leave hop_limit unchanged.
|
||||
*/
|
||||
static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void)
|
||||
static void test_tm_alterReceived_telemetryBroadcast_hopLimitUnchanged(void)
|
||||
{
|
||||
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
||||
ScopedBusyAirTime busyChannel;
|
||||
ScopedBusyAirTime busyChannel; // congestion present but exhaust is disabled
|
||||
TrafficManagementModuleTestShim module;
|
||||
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST);
|
||||
packet.hop_start = 5;
|
||||
@@ -728,20 +748,19 @@ static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void)
|
||||
module.alterReceived(packet);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, packet.hop_start);
|
||||
TEST_ASSERT_TRUE(module.shouldExhaustHops(packet));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, packet.hop_limit); // unchanged
|
||||
TEST_ASSERT_EQUAL_UINT8(5, packet.hop_start); // unchanged
|
||||
TEST_ASSERT_FALSE(module.shouldExhaustHops(packet));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify hop exhaustion skips unicast and local-origin packets.
|
||||
* Important to avoid mutating traffic that should retain normal forwarding behavior.
|
||||
* Verify alterReceived does not modify unicast or local-origin packets.
|
||||
* The precision clamp (the only active alterReceived path) only fires for
|
||||
* broadcast position packets from remote nodes — these should be untouched.
|
||||
*/
|
||||
static void test_tm_alterReceived_skipsLocalAndUnicast(void)
|
||||
{
|
||||
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
||||
ScopedBusyAirTime busyChannel; // congestion satisfied, so only the skip conditions are under test
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket unicast = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kTargetNode);
|
||||
@@ -768,9 +787,9 @@ static void test_tm_alterReceived_skipsLocalAndUnicast(void)
|
||||
*/
|
||||
static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 1;
|
||||
// 360 s = 1 pos-tick (kPosTimeTickMs); advance the virtual clock past one tick period.
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 360;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
@@ -779,7 +798,7 @@ static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void)
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(second);
|
||||
testDelay(1200);
|
||||
TrafficManagementModule::s_testNowMs += 360001; // advance past one 6-min pos-tick (virtual clock)
|
||||
ProcessMessage r3 = module.handleReceived(third);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
@@ -795,9 +814,9 @@ static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void)
|
||||
*/
|
||||
static void test_tm_positionDedup_intervalZero_neverDrops(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
// position_min_interval_secs=0 disables the drop gate (shouldDropPosition returns false for any packet).
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 0;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
@@ -818,9 +837,9 @@ static void test_tm_positionDedup_intervalZero_neverDrops(void)
|
||||
*/
|
||||
static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 99;
|
||||
// Channel precision=99 is out of range; sanitizePositionPrecision falls back to default (19 bits).
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(99);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
@@ -836,18 +855,21 @@ static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify precision=32 does not collapse all positions to one fingerprint.
|
||||
* Important to prevent false duplicate drops at the full-precision boundary.
|
||||
* Verify the dedup fingerprint does not collapse positions that are distinct at the
|
||||
* channel's *effective* precision. Dedup only runs on well-known (public) channels,
|
||||
* where precision is capped at MAX_POSITION_PRECISION_PUBLIC_KEY (15) regardless of the
|
||||
* channel's configured value — so the requested 32 is clamped to 15. Positions must
|
||||
* therefore differ in the top 15 bits (>= 2^17 raw units) to read as distinct; here
|
||||
* they differ by 2^18, well clear of the precision-15 grid, so neither is dropped.
|
||||
*/
|
||||
static void test_tm_positionDedup_precision32_allowsDistinctPositions(void)
|
||||
static void test_tm_positionDedup_distinctAtClampedChannelPrecision(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 32;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(32); // clamped to 15 on a public channel
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677);
|
||||
meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234 + (1 << 18), -1220845678 + (1 << 18));
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(second);
|
||||
@@ -859,17 +881,15 @@ static void test_tm_positionDedup_precision32_allowsDistinctPositions(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify precision=0 falls back to the default precision (same contract as
|
||||
* >32: getConfiguredOrDefault + sanitizePositionPrecision treat 0 as unset).
|
||||
* Important so invalid config does not collapse all positions into one
|
||||
* fingerprint — positions in different default-precision grid cells must
|
||||
* still be distinct.
|
||||
* Verify channel precision=0 (no channel ceiling set) falls back to the firmware
|
||||
* default precision (19 bits / ~90 m cells). Positions more than one default grid
|
||||
* cell apart must remain distinct, not collapse into one fingerprint.
|
||||
*/
|
||||
static void test_tm_positionDedup_precisionZero_allowsDistinctPositions(void)
|
||||
static void test_tm_positionDedup_precisionZero_channelFallsBackToDefault(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 0;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
// precision=0 in the channel → getPositionPrecisionForChannel returns 0 → falls back to default 19 bits.
|
||||
installWellKnownPrimaryChannelWithPrecision(0);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
@@ -888,20 +908,19 @@ static void test_tm_positionDedup_precisionZero_allowsDistinctPositions(void)
|
||||
* Verify epoch reset invalidates stale position identity for dedup.
|
||||
* Important so reset paths cannot leak prior packet identity into new windows.
|
||||
*/
|
||||
static void test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset(void)
|
||||
static void test_tm_positionDedup_cacheFlush_doesNotDropFirstPacketAfterFlush(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterReset = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterFlush = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
module.resetEpoch(millis());
|
||||
ProcessMessage r2 = module.handleReceived(afterReset);
|
||||
module.flushCache();
|
||||
ProcessMessage r2 = module.handleReceived(afterFlush);
|
||||
ProcessMessage r3 = module.handleReceived(duplicate);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
@@ -917,12 +936,10 @@ static void test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset(vo
|
||||
*/
|
||||
static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_dedup_enabled = true;
|
||||
moduleConfig.traffic_management.position_precision_bits = 16;
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
moduleConfig.traffic_management.rate_limit_enabled = true;
|
||||
moduleConfig.traffic_management.rate_limit_window_secs = 60;
|
||||
moduleConfig.traffic_management.rate_limit_max_packets = 10;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode);
|
||||
@@ -946,15 +963,15 @@ static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero
|
||||
*/
|
||||
static void test_tm_rateLimit_resetsAfterWindowExpires(void)
|
||||
{
|
||||
moduleConfig.traffic_management.rate_limit_enabled = true;
|
||||
moduleConfig.traffic_management.rate_limit_window_secs = 1;
|
||||
// 300 s = 1 rate-tick (kRateTimeTickMs); advance the virtual clock past one tick period.
|
||||
moduleConfig.traffic_management.rate_limit_window_secs = 300;
|
||||
moduleConfig.traffic_management.rate_limit_max_packets = 1;
|
||||
TrafficManagementModuleTestShim module;
|
||||
meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(packet);
|
||||
ProcessMessage r2 = module.handleReceived(packet);
|
||||
testDelay(1200);
|
||||
TrafficManagementModule::s_testNowMs += 300001; // advance past one 5-min rate-tick (virtual clock)
|
||||
ProcessMessage r3 = module.handleReceived(packet);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
@@ -969,15 +986,13 @@ static void test_tm_rateLimit_resetsAfterWindowExpires(void)
|
||||
*/
|
||||
static void test_tm_unknownPackets_resetAfterWindowExpires(void)
|
||||
{
|
||||
moduleConfig.traffic_management.drop_unknown_enabled = true;
|
||||
moduleConfig.traffic_management.unknown_packet_threshold = 1;
|
||||
moduleConfig.traffic_management.rate_limit_window_secs = 1;
|
||||
TrafficManagementModuleTestShim module;
|
||||
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(packet);
|
||||
ProcessMessage r2 = module.handleReceived(packet);
|
||||
testDelay(1200);
|
||||
TrafficManagementModule::s_testNowMs += 300001; // advance past 5 unknown-ticks (5 × 60s) (virtual clock)
|
||||
ProcessMessage r3 = module.handleReceived(packet);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
@@ -988,17 +1003,17 @@ static void test_tm_unknownPackets_resetAfterWindowExpires(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify unknown threshold values above 255 clamp to the counter ceiling.
|
||||
* Important to align config semantics with saturating counter storage.
|
||||
* Verify unknown threshold values above the implementation cap (60) are clamped.
|
||||
* The counter is a 6-bit field (saturates at 63); threshold is capped at 60 so a
|
||||
* saturated reading always exceeds it. A config value of 300 should behave as 60.
|
||||
*/
|
||||
static void test_tm_unknownPackets_thresholdAbove255_clamps(void)
|
||||
{
|
||||
moduleConfig.traffic_management.drop_unknown_enabled = true;
|
||||
moduleConfig.traffic_management.unknown_packet_threshold = 300;
|
||||
TrafficManagementModuleTestShim module;
|
||||
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode);
|
||||
|
||||
for (int i = 0; i < 255; i++) {
|
||||
for (int i = 0; i < 60; i++) {
|
||||
ProcessMessage result = module.handleReceived(packet);
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
||||
}
|
||||
@@ -1010,14 +1025,11 @@ static void test_tm_unknownPackets_thresholdAbove255_clamps(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify relayed position broadcasts can also be hop-exhausted — under the
|
||||
* same pressure gate as telemetry (here: channel congestion).
|
||||
* Important because telemetry and position use separate exhaust flags.
|
||||
* Verify relayed position broadcasts are NOT hop-exhausted.
|
||||
* exhaust_hop_position has been removed — alterReceived must leave hop_limit unchanged.
|
||||
*/
|
||||
static void test_tm_alterReceived_exhaustsRelayedPositionBroadcast(void)
|
||||
static void test_tm_alterReceived_positionBroadcast_hopLimitUnchanged(void)
|
||||
{
|
||||
moduleConfig.traffic_management.exhaust_hop_position = true;
|
||||
ScopedBusyAirTime busyChannel;
|
||||
TrafficManagementModuleTestShim module;
|
||||
meshtastic_MeshPacket packet = makePositionPacket(kRemoteNode, 374221234, -1220845678, NODENUM_BROADCAST);
|
||||
packet.hop_start = 5;
|
||||
@@ -1026,19 +1038,17 @@ static void test_tm_alterReceived_exhaustsRelayedPositionBroadcast(void)
|
||||
module.alterReceived(packet);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, packet.hop_start);
|
||||
TEST_ASSERT_TRUE(module.shouldExhaustHops(packet));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, packet.hop_limit); // unchanged
|
||||
TEST_ASSERT_EQUAL_UINT8(5, packet.hop_start); // unchanged
|
||||
TEST_ASSERT_FALSE(module.shouldExhaustHops(packet));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets);
|
||||
}
|
||||
/**
|
||||
* Verify hop exhaustion ignores undecoded/encrypted packets.
|
||||
* Verify alterReceived ignores undecoded/encrypted packets.
|
||||
* Important so we never mutate packets that were not decoded by this module.
|
||||
*/
|
||||
static void test_tm_alterReceived_skipsUndecodedPackets(void)
|
||||
{
|
||||
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
||||
ScopedBusyAirTime busyChannel; // congestion satisfied, so only the undecoded skip is under test
|
||||
TrafficManagementModuleTestShim module;
|
||||
meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode, NODENUM_BROADCAST);
|
||||
packet.hop_start = 5;
|
||||
@@ -1054,20 +1064,19 @@ static void test_tm_alterReceived_skipsUndecodedPackets(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify exhaustRequested is per-packet and resets on next handleReceived().
|
||||
* Important so a prior packet cannot leak hop-exhaust state into later packets.
|
||||
* Verify shouldExhaustHops() always returns false — exhaust_hop_* features are
|
||||
* removed, so the exhaustRequested flag is never set.
|
||||
* Guards against accidental re-enablement without updating the flag logic.
|
||||
*/
|
||||
static void test_tm_alterReceived_resetExhaustFlagOnNextPacket(void)
|
||||
static void test_tm_alterReceived_exhaustFlagAlwaysFalse(void)
|
||||
{
|
||||
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
||||
ScopedBusyAirTime busyChannel; // telemetry exhaust only fires under congestion
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST);
|
||||
telemetry.hop_start = 5;
|
||||
telemetry.hop_limit = 3;
|
||||
module.alterReceived(telemetry);
|
||||
TEST_ASSERT_TRUE(module.shouldExhaustHops(telemetry));
|
||||
TEST_ASSERT_FALSE(module.shouldExhaustHops(telemetry));
|
||||
|
||||
meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode);
|
||||
ProcessMessage result = module.handleReceived(text);
|
||||
@@ -1075,42 +1084,40 @@ static void test_tm_alterReceived_resetExhaustFlagOnNextPacket(void)
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(result));
|
||||
TEST_ASSERT_FALSE(module.shouldExhaustHops(telemetry));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify exhaust requests are packet-scoped (from + id).
|
||||
* Important so stale state from one packet cannot influence unrelated packets
|
||||
* that pass through duplicate/rebroadcast paths before handleReceived().
|
||||
* Verify shouldExhaustHops() returns false for any packet regardless of from/id.
|
||||
* Since exhaust is removed, the from+id scope check is moot — this guards that
|
||||
* the always-false invariant holds across multiple distinct packets.
|
||||
*/
|
||||
static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void)
|
||||
{
|
||||
moduleConfig.traffic_management.exhaust_hop_telemetry = true;
|
||||
ScopedBusyAirTime busyChannel; // telemetry exhaust only fires under congestion
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket exhausted = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST);
|
||||
exhausted.id = 0x1010;
|
||||
exhausted.hop_start = 5;
|
||||
exhausted.hop_limit = 3;
|
||||
module.alterReceived(exhausted);
|
||||
meshtastic_MeshPacket p1 = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST);
|
||||
p1.id = 0x1010;
|
||||
p1.hop_start = 5;
|
||||
p1.hop_limit = 3;
|
||||
module.alterReceived(p1);
|
||||
|
||||
meshtastic_MeshPacket unrelated = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kTargetNode, NODENUM_BROADCAST);
|
||||
unrelated.id = 0x2020;
|
||||
unrelated.hop_start = 4;
|
||||
unrelated.hop_limit = 0;
|
||||
meshtastic_MeshPacket p2 = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kTargetNode, NODENUM_BROADCAST);
|
||||
p2.id = 0x2020;
|
||||
p2.hop_start = 4;
|
||||
p2.hop_limit = 0;
|
||||
|
||||
TEST_ASSERT_TRUE(module.shouldExhaustHops(exhausted));
|
||||
TEST_ASSERT_FALSE(module.shouldExhaustHops(unrelated));
|
||||
TEST_ASSERT_FALSE(module.shouldExhaustHops(p1));
|
||||
TEST_ASSERT_FALSE(module.shouldExhaustHops(p2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify runOnce() returns sleep-forever interval when module is disabled.
|
||||
* Important to ensure the maintenance thread is effectively inert when off.
|
||||
* Verify runOnce() returns sleep-forever interval when has_traffic_management is false.
|
||||
* TMM has no runtime enable flag — the presence bit is the only runtime gate.
|
||||
*/
|
||||
static void test_tm_runOnce_disabledReturnsMaxInterval(void)
|
||||
{
|
||||
moduleConfig.traffic_management.enabled = false;
|
||||
moduleConfig.has_traffic_management = false;
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
int32_t interval = module.runOnce();
|
||||
@@ -1218,6 +1225,381 @@ static void test_tm_nextHop_keptAliveAcrossMaintenanceSweep(void)
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(0x42, module.getNextHopHint(kTargetNode));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Role exceptions: TRACKER and LOST_AND_FOUND
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Verify TRACKER role caps the dedup window at 1 hour.
|
||||
* A duplicate position that would normally be blocked for 11 h (default) must
|
||||
* be forwarded once the 1-hour tracker cap expires.
|
||||
*/
|
||||
static void test_tm_trackerRole_capsDedupWindowAtOneHour(void)
|
||||
{
|
||||
// Operator interval is 11 h — longer than the tracker cap.
|
||||
moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_TRACKER);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterCap = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(dup); // still within 1-hour cap
|
||||
// Advance past 1 hour (tracker cap = 3600 s; pos-tick = 360 s → 10 ticks).
|
||||
TrafficManagementModule::s_testNowMs += (default_traffic_mgmt_tracker_position_min_interval_secs * 1000UL) + 1;
|
||||
ProcessMessage r3 = module.handleReceived(afterCap);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify TAK_TRACKER role receives the same 1-hour dedup cap as TRACKER.
|
||||
* Both roles share the same exception branch; this guards against future divergence.
|
||||
*/
|
||||
static void test_tm_takTrackerRole_capsDedupWindowAtOneHour(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_TAK_TRACKER);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterCap = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(dup);
|
||||
TrafficManagementModule::s_testNowMs += (default_traffic_mgmt_tracker_position_min_interval_secs * 1000UL) + 1;
|
||||
ProcessMessage r3 = module.handleReceived(afterCap);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the tracker role exception survives the node being evicted from BOTH the
|
||||
* hot and warm NodeDB stores — the TMM unified cache is the third fallback. The
|
||||
* role is cached on the entry while NodeDB still knows the node; once NodeDB
|
||||
* forgets it (getNodeRole → CLIENT), the cached role must keep the 1-hour cap
|
||||
* applied instead of reverting to the 11-hour default interval.
|
||||
*/
|
||||
static void test_tm_trackerRole_survivesNodeDbEvictionViaCachedRole(void)
|
||||
{
|
||||
// Operator interval is 11 h — longer than the tracker cap.
|
||||
moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_TRACKER);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterCap = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
// First packet: NodeDB knows the role; it is cached onto the TMM entry.
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
|
||||
// Node ages out of NodeDB entirely (hot + warm). getNodeRole now returns CLIENT.
|
||||
mockNodeDB->clearCachedNode();
|
||||
|
||||
ProcessMessage r2 = module.handleReceived(dup); // within 1-hour cap — still drop
|
||||
// Advance past the tracker cap (3600 s) but stay well under the 11-hour default.
|
||||
// Without the cached-role fallback this would still be inside the 11-hour window
|
||||
// (CLIENT → no exception) and wrongly drop; with it, the 1-hour cap lets it pass.
|
||||
TrafficManagementModule::s_testNowMs += (default_traffic_mgmt_tracker_position_min_interval_secs * 1000UL) + 1;
|
||||
ProcessMessage r3 = module.handleReceived(afterCap);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a role change observed via NodeInfo updates the cached role (write-time update),
|
||||
* so an exception is dropped when a node is demoted from TRACKER back to CLIENT. The role
|
||||
* is read from the NodeInfo's User payload — the same event that updates NodeDB — not by
|
||||
* re-scanning NodeDB on the position path.
|
||||
*/
|
||||
static void test_tm_roleChange_viaNodeInfo_dropsTrackerException(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_TRACKER);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
// First position seeds the TMM entry with TRACKER (from NodeDB on isNew).
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
|
||||
// The node demotes to CLIENT and announces it. The NodeInfo refresh must overwrite
|
||||
// the cached TRACKER role with CLIENT — even though the packet payload, not NodeDB,
|
||||
// is the source of truth here (NodeDB role left stale on purpose to prove the path).
|
||||
meshtastic_MeshPacket info = makeNodeInfoPacketWithRole(kRemoteNode, meshtastic_Config_DeviceConfig_Role_CLIENT);
|
||||
module.handleReceived(info);
|
||||
|
||||
// Past the 1-hour tracker cap but within the 11-hour CLIENT interval. With the stale
|
||||
// TRACKER role this would pass; after the demotion it must drop (full interval applies).
|
||||
TrafficManagementModule::s_testNowMs += (default_traffic_mgmt_tracker_position_min_interval_secs * 1000UL) + 1;
|
||||
meshtastic_MeshPacket afterCap = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
ProcessMessage r2 = module.handleReceived(afterCap);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a cached special (non-CLIENT) role pins the TMM entry through the maintenance
|
||||
* sweep: once the node's position state has expired and been cleared, the entry — and its
|
||||
* role — must still survive (role has no TTL), the same way a confirmed next-hop hint does.
|
||||
*/
|
||||
static void test_tm_specialRole_pinsEntryThroughSweep(void)
|
||||
{
|
||||
// Short position interval → short pos TTL so the sweep clears pos state quickly.
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_TRACKER);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
module.handleReceived(makePositionPacket(kRemoteNode, 374221234, -1220845678));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(meshtastic_Config_DeviceConfig_Role_TRACKER), module.peekCachedRole(kRemoteNode));
|
||||
|
||||
// Advance well past the position TTL, then sweep: pos state is cleared but the role
|
||||
// (no TTL) must keep the entry alive. Without the role pin the entry would be reclaimed
|
||||
// and peekCachedRole would return -1.
|
||||
TrafficManagementModule::s_testNowMs += 60UL * 60UL * 1000UL; // 1 h
|
||||
module.runOnce();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(meshtastic_Config_DeviceConfig_Role_TRACKER), module.peekCachedRole(kRemoteNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify special-role entries are evicted last. A tracker that is the OLDEST entry must
|
||||
* survive cache pressure that evicts many newer CLIENT entries, because a cached
|
||||
* special role marks the entry "preferred" (like a next-hop hint) in victim selection.
|
||||
*/
|
||||
static void test_tm_specialRole_evictedLastUnderPressure(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
// Oldest entry: a tracker. Seed its role from NodeDB on first position.
|
||||
const NodeNum tracker = 0xAA0000FF;
|
||||
mockNodeDB->setCachedNode(tracker);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_TRACKER);
|
||||
module.handleReceived(makePositionPacket(tracker, 100, 200));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(meshtastic_Config_DeviceConfig_Role_TRACKER), module.peekCachedRole(tracker));
|
||||
|
||||
mockNodeDB->clearCachedNode(); // every subsequent filler resolves to CLIENT (unprotected)
|
||||
|
||||
// Fill past capacity with newer CLIENT nodes, forcing many evictions. The tracker is
|
||||
// the stalest entry but is "preferred", so an unprotected client must always be the
|
||||
// victim instead — the tracker must never be evicted.
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(TRAFFIC_MANAGEMENT_CACHE_SIZE) + 50; i++) {
|
||||
const NodeNum filler = 0x01000000u + i;
|
||||
module.handleReceived(makePositionPacket(filler, 300 + static_cast<int>(i), 400 + static_cast<int>(i)));
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(meshtastic_Config_DeviceConfig_Role_TRACKER), module.peekCachedRole(tracker));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the tracker cap never lengthens an operator-configured interval shorter
|
||||
* than the cap default. The cap is a ceiling, not a floor.
|
||||
*/
|
||||
static void test_tm_trackerRole_doesNotLengthenShorterOperatorInterval(void)
|
||||
{
|
||||
// Operator set 5-minute interval — shorter than the 1-hour tracker cap.
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_TRACKER);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterShortInterval = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(dup);
|
||||
// The 300 s operator interval rounds to 1 pos-tick (360 s) — dedup is tick-granular.
|
||||
// Advance past one full tick to verify the window is 1 tick (not the 10-tick tracker cap).
|
||||
TrafficManagementModule::s_testNowMs += 360'000UL + 1;
|
||||
ProcessMessage r3 = module.handleReceived(afterShortInterval);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify LOST_AND_FOUND role caps duplicate-position dedup at ~15 min (2 pos-ticks),
|
||||
* not the old one-tick fast-announce. A configured 11-hour interval is shortened to the
|
||||
* 15-min cap; a duplicate one tick later still drops, but one past the 2-tick cap passes.
|
||||
*/
|
||||
static void test_tm_lostAndFoundRole_capsDedupAtFifteenMinutes(void)
|
||||
{
|
||||
// Long interval that would normally suppress duplicates for 11 h.
|
||||
moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterOneTick = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterCap = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(dup); // same tick — drop
|
||||
// One pos-tick later: STILL within the 15-min (~2-tick) cap, unlike the old 1-tick exception.
|
||||
// (360 s = kPosTimeTickMs, kept as a literal because the constant is private to the module.)
|
||||
TrafficManagementModule::s_testNowMs += 360'000UL + 1;
|
||||
ProcessMessage r3 = module.handleReceived(afterOneTick); // still drop
|
||||
// Jump past the full cap (>= 2 ticks since the last packet): now it passes.
|
||||
TrafficManagementModule::s_testNowMs += 2 * 360'000UL;
|
||||
ProcessMessage r4 = module.handleReceived(afterCap);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r3));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r4));
|
||||
TEST_ASSERT_EQUAL_UINT32(2, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a node with unknown role (absent from NodeDB) is not granted any role
|
||||
* exception: the full configured interval applies, so a duplicate inside that
|
||||
* window is dropped exactly as for an ordinary CLIENT.
|
||||
*/
|
||||
static void test_tm_unknownRole_noDbEntry_appliesFullInterval(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
// No cached node — getMeshNode returns nullptr for kRemoteNode.
|
||||
mockNodeDB->clearCachedNode();
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket afterShort = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(dup); // within 300-s window — must drop
|
||||
// The 300 s operator interval rounds to 1 pos-tick (360 s) — dedup is tick-granular.
|
||||
// Advance past one full tick to confirm the packet passes without any tracker exception.
|
||||
TrafficManagementModule::s_testNowMs += 360'000UL + 1;
|
||||
ProcessMessage r3 = module.handleReceived(afterShort);
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r3));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a node present in NodeDB but without user info (HAS_USER bit clear)
|
||||
* is not granted a role exception: it falls back to CLIENT and the full
|
||||
* configured interval applies.
|
||||
*/
|
||||
static void test_tm_unknownRole_noUserBit_appliesFullInterval(void)
|
||||
{
|
||||
moduleConfig.traffic_management.position_min_interval_secs = 300;
|
||||
installWellKnownPrimaryChannelWithPrecision(16);
|
||||
|
||||
// Node is in NodeDB but the HAS_USER bit is NOT set — role must be ignored.
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
// Clear the HAS_USER bit that setCachedNode sets, leaving role at CLIENT default.
|
||||
mockNodeDB->cachedNodeForTest().bitfield &= ~NODEINFO_BITFIELD_HAS_USER_MASK;
|
||||
mockNodeDB->cachedNodeForTest().role = meshtastic_Config_DeviceConfig_Role_TRACKER;
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
meshtastic_MeshPacket dup = makePositionPacket(kRemoteNode, 374221234, -1220845678);
|
||||
|
||||
ProcessMessage r1 = module.handleReceived(first);
|
||||
ProcessMessage r2 = module.handleReceived(dup); // within 300-s window — must drop
|
||||
meshtastic_TrafficManagementStats stats = module.getStats();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::CONTINUE), static_cast<int>(r1));
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(r2));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a LOST_AND_FOUND origin now GETS the relayed precision clamp — the
|
||||
* anti-dox exemption was removed, so a relayed position more precise than the
|
||||
* channel setting is clamped down to the channel ceiling like any other node's.
|
||||
*/
|
||||
static void test_tm_lostAndFoundRole_getsAlterReceivedPrecisionClamp(void)
|
||||
{
|
||||
// Set channel precision ceiling to 13 bits. Must be <= MAX_POSITION_PRECISION_PUBLIC_KEY
|
||||
// (15) — well-known channels have a public PSK (size==1), so getPositionPrecisionForChannel
|
||||
// clamps any value above 15 via usesPublicKey().
|
||||
installWellKnownPrimaryChannelWithPrecision(13);
|
||||
|
||||
mockNodeDB->setCachedNode(kRemoteNode);
|
||||
mockNodeDB->setCachedNodeRole(meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND);
|
||||
|
||||
TrafficManagementModuleTestShim module;
|
||||
|
||||
// Full-precision packet — 32 bits — exceeds the channel cap.
|
||||
const uint32_t fullPrecision = 32;
|
||||
meshtastic_MeshPacket packet = makePositionPacketWithPrecision(kRemoteNode, 374221234, -1220845678, fullPrecision);
|
||||
packet.hop_start = 3;
|
||||
packet.hop_limit = 2; // relayed (hop_limit < hop_start) so clamp logic applies
|
||||
|
||||
module.alterReceived(packet);
|
||||
|
||||
meshtastic_Position out;
|
||||
TEST_ASSERT_TRUE(decodePositionPayload(packet, out));
|
||||
// Clamped to channel ceiling (13 bits) — lost-and-found is no longer exempt.
|
||||
// Note: precision must be <= MAX_POSITION_PRECISION_PUBLIC_KEY (15); well-known
|
||||
// channels always have a public PSK so getPositionPrecisionForChannel caps at 15.
|
||||
TEST_ASSERT_EQUAL_UINT32(13, out.precision_bits);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void setUp(void)
|
||||
@@ -1254,21 +1636,21 @@ TM_TEST_ENTRY void setup()
|
||||
RUN_TEST(test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield);
|
||||
RUN_TEST(test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb);
|
||||
#endif
|
||||
RUN_TEST(test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast);
|
||||
RUN_TEST(test_tm_alterReceived_telemetryBroadcast_hopLimitUnchanged);
|
||||
RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast);
|
||||
RUN_TEST(test_tm_positionDedup_allowsDuplicateAfterIntervalExpires);
|
||||
RUN_TEST(test_tm_positionDedup_intervalZero_neverDrops);
|
||||
RUN_TEST(test_tm_positionDedup_precisionAbove32_usesDefaultPrecision);
|
||||
RUN_TEST(test_tm_positionDedup_precision32_allowsDistinctPositions);
|
||||
RUN_TEST(test_tm_positionDedup_precisionZero_allowsDistinctPositions);
|
||||
RUN_TEST(test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset);
|
||||
RUN_TEST(test_tm_positionDedup_distinctAtClampedChannelPrecision);
|
||||
RUN_TEST(test_tm_positionDedup_precisionZero_channelFallsBackToDefault);
|
||||
RUN_TEST(test_tm_positionDedup_cacheFlush_doesNotDropFirstPacketAfterFlush);
|
||||
RUN_TEST(test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero);
|
||||
RUN_TEST(test_tm_rateLimit_resetsAfterWindowExpires);
|
||||
RUN_TEST(test_tm_unknownPackets_resetAfterWindowExpires);
|
||||
RUN_TEST(test_tm_unknownPackets_thresholdAbove255_clamps);
|
||||
RUN_TEST(test_tm_alterReceived_exhaustsRelayedPositionBroadcast);
|
||||
RUN_TEST(test_tm_alterReceived_positionBroadcast_hopLimitUnchanged);
|
||||
RUN_TEST(test_tm_alterReceived_skipsUndecodedPackets);
|
||||
RUN_TEST(test_tm_alterReceived_resetExhaustFlagOnNextPacket);
|
||||
RUN_TEST(test_tm_alterReceived_exhaustFlagAlwaysFalse);
|
||||
RUN_TEST(test_tm_alterReceived_exhaustFlag_isPacketScoped);
|
||||
RUN_TEST(test_tm_runOnce_disabledReturnsMaxInterval);
|
||||
RUN_TEST(test_tm_runOnce_enabledReturnsMaintenanceInterval);
|
||||
@@ -1276,6 +1658,17 @@ TM_TEST_ENTRY void setup()
|
||||
RUN_TEST(test_tm_nextHop_servedAfterNodeDbRoll);
|
||||
RUN_TEST(test_tm_nextHop_preloadDoesNotClobberLearned);
|
||||
RUN_TEST(test_tm_nextHop_keptAliveAcrossMaintenanceSweep);
|
||||
RUN_TEST(test_tm_trackerRole_capsDedupWindowAtOneHour);
|
||||
RUN_TEST(test_tm_takTrackerRole_capsDedupWindowAtOneHour);
|
||||
RUN_TEST(test_tm_trackerRole_survivesNodeDbEvictionViaCachedRole);
|
||||
RUN_TEST(test_tm_roleChange_viaNodeInfo_dropsTrackerException);
|
||||
RUN_TEST(test_tm_specialRole_pinsEntryThroughSweep);
|
||||
RUN_TEST(test_tm_specialRole_evictedLastUnderPressure);
|
||||
RUN_TEST(test_tm_trackerRole_doesNotLengthenShorterOperatorInterval);
|
||||
RUN_TEST(test_tm_lostAndFoundRole_capsDedupAtFifteenMinutes);
|
||||
RUN_TEST(test_tm_lostAndFoundRole_getsAlterReceivedPrecisionClamp);
|
||||
RUN_TEST(test_tm_unknownRole_noDbEntry_appliesFullInterval);
|
||||
RUN_TEST(test_tm_unknownRole_noUserBit_appliesFullInterval);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
// "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN",
|
||||
// "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, and LOST AND FOUND roles are restricted.
|
||||
// "USERPREFS_EVENT_MODE": "1",
|
||||
// "USERPREFS_TMM_APPLY_TO_PRIVATE_CHANNELS": "1", // Extend TMM position dedup and precision clamping to private/custom-key channels (default: well-known channels only)
|
||||
// "USERPREFS_FIRMWARE_EDITION": "meshtastic_FirmwareEdition_BURNING_MAN",
|
||||
// "USERPREFS_FIXED_BLUETOOTH": "121212",
|
||||
// "USERPREFS_FIXED_GPS": "",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef Pins_Arduino_h
|
||||
#define Pins_Arduino_h
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
static const uint8_t TX = 21;
|
||||
static const uint8_t RX = 20;
|
||||
|
||||
static const uint8_t SDA = 12;
|
||||
static const uint8_t SCL = 13;
|
||||
|
||||
static const uint8_t SS = 8;
|
||||
static const uint8_t MOSI = 7;
|
||||
static const uint8_t MISO = 6;
|
||||
static const uint8_t SCK = 10;
|
||||
|
||||
static const uint8_t A0 = 0;
|
||||
static const uint8_t A1 = 1;
|
||||
static const uint8_t A2 = 2;
|
||||
static const uint8_t A3 = 3;
|
||||
static const uint8_t A4 = 4;
|
||||
static const uint8_t A5 = 5;
|
||||
|
||||
#endif /* Pins_Arduino_h */
|
||||
@@ -0,0 +1,29 @@
|
||||
; ESP32 C3 Super Mini Development Board
|
||||
; https://www.espboards.dev/esp32/esp32-c3-super-mini/
|
||||
[env:esp32c3_moonshine]
|
||||
|
||||
extends = esp32c3_base
|
||||
board = esp32-c3-devkitm-1
|
||||
board_level = pr
|
||||
build_flags =
|
||||
${esp32c3_base.build_flags}
|
||||
-D PRIVATE_HW
|
||||
-I variants/esp32c3/diy/esp32c3_moonshine
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
monitor_speed = 115200
|
||||
upload_protocol = esptool
|
||||
;upload_port = /dev/ttyUSB0
|
||||
upload_speed = 921600
|
||||
|
||||
|
||||
; ------------------- Flash 核心配置 -------------------
|
||||
; 1. Flash 容量(根据硬件实际值改,常见 4MB/8MB)
|
||||
board_build.flash_size = 4MB
|
||||
; 2. Flash 模式(ESP32-C3 推荐 dio)
|
||||
board_build.flash_mode = dio
|
||||
; 3. Flash 时钟频率(ESP32-C3 推荐 80MHz)
|
||||
board_build.flash_freq = 80MHz
|
||||
|
||||
; (可选)上传时的 Flash 配置(和上面保持一致即可)
|
||||
board_upload.flash_size = 4MB
|
||||
@@ -0,0 +1,63 @@
|
||||
#define BUTTON_PIN 9
|
||||
|
||||
#define HAS_SCREEN 0
|
||||
|
||||
#define HAS_I2C 0
|
||||
#define WIRE_INTERFACES_COUNT (0)
|
||||
#define I2C_SDA 12
|
||||
#define I2C_SCL 13
|
||||
|
||||
|
||||
#define BATTERY_PIN 1
|
||||
#define ADC_CHANNEL ADC_CHANNEL_1
|
||||
#define ADC_MULTIPLIER 2.0f
|
||||
|
||||
#define LED_POWER 0 // LED
|
||||
#define LED_STATE_ON 1 // State when LED is lit
|
||||
|
||||
|
||||
#define HAS_GPS 0
|
||||
#undef GPS_RX_PIN
|
||||
#undef GPS_TX_PIN
|
||||
|
||||
//#define E220_400M30S
|
||||
//#define E220_400M33S
|
||||
#define E22_400M33S
|
||||
|
||||
#ifdef E220_400M30S
|
||||
#define SETTING_MAX_POWER 30
|
||||
#define TX_GAIN_LORA 8
|
||||
#define SX126X_MAX_POWER 22
|
||||
#endif
|
||||
|
||||
#ifdef E22_400M33S
|
||||
#define SETTING_MAX_POWER 22
|
||||
#define NUM_PA_POINTS 1
|
||||
#define TX_GAIN_LORA 0
|
||||
#define SX126X_MAX_POWER 22
|
||||
//#define HAS_LORA_FEM 1
|
||||
#endif
|
||||
|
||||
#define USE_LLCC68
|
||||
#define USE_SX1262
|
||||
#define USE_SX1268
|
||||
|
||||
#define LORA_SCK 10
|
||||
#define LORA_MISO 6
|
||||
#define LORA_MOSI 7
|
||||
#define LORA_CS 8
|
||||
#define LORA_DIO0 RADIOLIB_NC
|
||||
#define LORA_RESET 5
|
||||
#define LORA_DIO1 3
|
||||
#define LORA_DIO2 RADIOLIB_NC
|
||||
#define LORA_BUSY 4
|
||||
|
||||
#define SX126X_RXEN 2
|
||||
#define SX126X_CS LORA_CS
|
||||
#define SX126X_DIO1 LORA_DIO1
|
||||
#define SX126X_BUSY LORA_BUSY
|
||||
#define SX126X_RESET LORA_RESET
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
#define TCXO_OPTIONAL
|
||||
@@ -33,9 +33,6 @@
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
||||
#endif
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
|
||||
#endif
|
||||
|
||||
// ---- GC1109 RF FRONT END CONFIGURATION ----
|
||||
// The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA
|
||||
|
||||
@@ -31,9 +31,6 @@
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
||||
#endif
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
|
||||
#endif
|
||||
|
||||
// ---- KCT8103L RF FRONT END CONFIGURATION ----
|
||||
// The Heltec V4.3 uses a KCT8103L FEM chip with integrated PA and LNA
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user