Merge branch 'meshtastic:master' into master

This commit is contained in:
Mictronics 2024-03-12 18:58:24 +01:00 committed by GitHub
commit 4427f80da3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 550 additions and 144 deletions

View File

@ -35,6 +35,7 @@ jobs:
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32s2.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32s3.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32c3.ini
- name: Build ESP32
run: bin/build-esp32.sh ${{ inputs.board }}

62
.github/workflows/build_esp32_c3.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Build ESP32-C3
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-esp32-c3:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build base
id: base
uses: ./.github/actions/setup-base
- name: Pull web ui
uses: dsaltares/fetch-gh-release-asset@a40c8b4a0471f9ab81bdf73a010f74cc51476ad4
with:
repo: meshtastic/web
file: build.tar
target: build.tar
token: ${{ secrets.GITHUB_TOKEN }}
- name: Unpack web ui
run: |
tar -xf build.tar -C data/static
rm build.tar
- name: Remove debug flags for release
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32s2.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32s3.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32c3.ini
- name: Build ESP32
run: bin/build-esp32.sh ${{ inputs.board }}
- name: Pull OTA Firmware
uses: dsaltares/fetch-gh-release-asset@a40c8b4a0471f9ab81bdf73a010f74cc51476ad4
with:
repo: meshtastic/firmware-ota
file: firmware-c3.bin
target: release/bleota-c3.bin
token: ${{ secrets.GITHUB_TOKEN }}
- name: Get release version string
shell: bash
run: echo "version=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- name: Store binaries as an artifact
uses: actions/upload-artifact@v3
with:
name: firmware-${{ inputs.board }}-${{ steps.version.outputs.version }}.zip
path: |
release/*.bin
release/*.elf

View File

@ -34,6 +34,7 @@ jobs:
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32s2.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32s3.ini
sed -i '/DDEBUG_HEAP/d' ./arch/esp32/esp32c3.ini
- name: Build ESP32
run: bin/build-esp32.sh ${{ inputs.board }}

View File

@ -67,7 +67,6 @@ jobs:
- board: tlora-v2-1-1_6-tcxo
- board: tlora-v2-1-1_8
- board: tbeam
- board: heltec-ht62-esp32c3-sx1262
- board: heltec-v2_0
- board: heltec-v2_1
- board: tbeam0_7
@ -105,6 +104,16 @@ jobs:
with:
board: ${{ matrix.board }}
build-esp32-c3:
strategy:
fail-fast: false
matrix:
include:
- board: heltec-ht62-esp32c3-sx1262
uses: ./.github/workflows/build_esp32_c3.yml
with:
board: ${{ matrix.board }}
build-nrf52:
strategy:
fail-fast: false
@ -226,6 +235,7 @@ jobs:
[
build-esp32,
build-esp32-s3,
build-esp32-c3,
build-nrf52,
build-raspbian,
build-native,
@ -251,7 +261,7 @@ jobs:
id: version
- name: Move files up
run: mv -b -t ./ ./*tbeam-2*/littlefs*.bin ./*tbeam-2*/bleota.bin ./*tbeam-s3*/bleota-s3.bin ./**/firmware*.bin ./*t-echo*/Meshtastic_nRF52_factory_erase_v2.uf2 ./**/firmware-*.uf2 ./**/firmware-*-ota.zip ./**/*.elf ./*native*/*device-*.sh ./*native*/*device-*.bat ./firmware-raspbian-*/release/meshtasticd_linux_aarch64 ./firmware-raspbian-*/bin/config-dist.yaml
run: mv -b -t ./ ./*tbeam-2*/littlefs*.bin ./*tbeam-2*/bleota.bin ./*tbeam-s3*/bleota-s3.bin ./*esp32c3*/bleota-c3.bin ./**/firmware*.bin ./*t-echo*/Meshtastic_nRF52_factory_erase_v2.uf2 ./**/firmware-*.uf2 ./**/firmware-*-ota.zip ./**/*.elf ./*native*/*device-*.sh ./*native*/*device-*.bat ./firmware-raspbian-*/release/meshtasticd_linux_aarch64 ./firmware-raspbian-*/bin/config-dist.yaml
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v3

View File

@ -31,9 +31,13 @@ IF EXIST %FILENAME% IF x%FILENAME:update=%==x%FILENAME% (
%PYTHON% -m esptool --baud 115200 erase_flash
%PYTHON% -m esptool --baud 115200 write_flash 0x00 %FILENAME%
@REM Account for S3 board's different OTA partition
@REM Account for S3 and C3 board's different OTA partition
IF x%FILENAME:s3=%==x%FILENAME% IF x%FILENAME:v3=%==x%FILENAME% IF x%FILENAME:t-deck=%==x%FILENAME% IF x%FILENAME:wireless-paper=%==x%FILENAME% IF x%FILENAME:wireless-tracker=%==x%FILENAME% (
%PYTHON% -m esptool --baud 115200 write_flash 0x260000 bleota.bin
IF x%FILENAME:esp32c3=%==x%FILENAME% (
%PYTHON% -m esptool --baud 115200 write_flash 0x260000 bleota.bin
) else (
%PYTHON% -m esptool --baud 115200 write_flash 0x260000 bleota-c3.bin
)
) else (
%PYTHON% -m esptool --baud 115200 write_flash 0x260000 bleota-s3.bin
)

View File

@ -1,12 +1,12 @@
#!/bin/sh
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
set -e
# Usage info
show_help() {
cat << EOF
cat <<EOF
Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME]
Flash image file to device, but first erasing and writing system information"
@ -18,44 +18,50 @@ Flash image file to device, but first erasing and writing system information"
EOF
}
while getopts ":hp:P:f:" opt; do
case "${opt}" in
h)
show_help
exit 0
;;
p) export ESPTOOL_PORT=${OPTARG}
;;
P) PYTHON=${OPTARG}
;;
f) FILENAME=${OPTARG}
;;
*)
echo "Invalid flag."
show_help >&2
exit 1
;;
esac
case "${opt}" in
h)
show_help
exit 0
;;
p)
export ESPTOOL_PORT=${OPTARG}
;;
P)
PYTHON=${OPTARG}
;;
f)
FILENAME=${OPTARG}
;;
*)
echo "Invalid flag."
show_help >&2
exit 1
;;
esac
done
shift "$((OPTIND-1))"
shift "$((OPTIND - 1))"
[ -z "$FILENAME" -a -n "$1" ] && {
FILENAME=$1
shift
FILENAME=$1
shift
}
if [ -f "${FILENAME}" ] && [ ! -z "${FILENAME##*"update"*}" ]; then
if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
"$PYTHON" -m esptool erase_flash
"$PYTHON" -m esptool write_flash 0x00 ${FILENAME}
"$PYTHON" -m esptool erase_flash
"$PYTHON" -m esptool write_flash 0x00 ${FILENAME}
# Account for S3 board's different OTA partition
if [ ! -z "${FILENAME##*"s3"*}" ] && [ ! -z "${FILENAME##*"-v3"*}" ] && [ ! -z "${FILENAME##*"t-deck"*}" ] && [ ! -z "${FILENAME##*"wireless-paper"*}" ] && [ ! -z "${FILENAME##*"wireless-tracker"*}" ]; then
"$PYTHON" -m esptool write_flash 0x260000 bleota.bin
if [ -n "${FILENAME##*"s3"*}" ] && [ -n "${FILENAME##*"-v3"*}" ] && [ -n "${FILENAME##*"t-deck"*}" ] && [ -n "${FILENAME##*"wireless-paper"*}" ] && [ -n "${FILENAME##*"wireless-tracker"*}" ]; then
if [ -n "${FILENAME##*"esp32c3"*}" ]; then
"$PYTHON" -m esptool write_flash 0x260000 bleota.bin
else
"$PYTHON" -m esptool write_flash 0x260000 bleota-c3.bin
fi
else
"$PYTHON" -m esptool write_flash 0x260000 bleota-s3.bin
"$PYTHON" -m esptool write_flash 0x260000 bleota-s3.bin
fi
"$PYTHON" -m esptool write_flash 0x300000 littlefs-*.bin
"$PYTHON" -m esptool write_flash 0x300000 littlefs-*.bin
else
show_help

View File

@ -103,12 +103,21 @@ class AccelerometerThread : public concurrency::OSThread
#endif
struct bma423_axes_remap remap_data;
#ifdef T_WATCH_S3
remap_data.x_axis = 1;
remap_data.x_axis_sign = 0;
remap_data.y_axis = 0;
remap_data.y_axis_sign = 0;
remap_data.z_axis = 2;
remap_data.z_axis_sign = 1;
#else
remap_data.x_axis = 0;
remap_data.x_axis_sign = 1;
remap_data.y_axis = 1;
remap_data.y_axis_sign = 0;
remap_data.z_axis = 2;
remap_data.z_axis_sign = 1;
#endif
// Need to raise the wrist function, need to set the correct axis
bmaSensor.setRemapAxes(&remap_data);
// sensor.enableFeature(BMA423_STEP_CNTR, true);
@ -171,4 +180,4 @@ class AccelerometerThread : public concurrency::OSThread
Adafruit_LIS3DH lis;
};
} // namespace concurrency
} // namespace concurrency

View File

@ -54,6 +54,19 @@ static const adc_atten_t atten = ADC_ATTENUATION;
#endif
#endif // BATTERY_PIN && ARCH_ESP32
#ifdef EXT_CHRG_DETECT
#ifndef EXT_CHRG_DETECT_MODE
static const uint8_t ext_chrg_detect_mode = INPUT;
#else
static const uint8_t ext_chrg_detect_mode = EXT_CHRG_DETECT_MODE;
#endif
#ifndef EXT_CHRG_DETECT_VALUE
static const uint8_t ext_chrg_detect_value = HIGH;
#else
static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE;
#endif
#endif
#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
INA260Sensor ina260Sensor;
INA219Sensor ina219Sensor;
@ -322,7 +335,14 @@ class AnalogBatteryLevel : public HasBatteryLevel
/// Assume charging if we have a battery and external power is connected.
/// we can't be smart enough to say 'full'?
virtual bool isCharging() override { return isBatteryConnect() && isVbusIn(); }
virtual bool isCharging() override
{
#ifdef EXT_CHRG_DETECT
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#else
return isBatteryConnect() && isVbusIn();
#endif
}
private:
/// If we see a battery voltage higher than physics allows - assume charger is pumping
@ -389,6 +409,9 @@ bool Power::analogInit()
#ifdef EXT_PWR_DETECT
pinMode(EXT_PWR_DETECT, INPUT);
#endif
#ifdef EXT_CHRG_DETECT
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
#endif
#ifdef BATTERY_PIN
LOG_DEBUG("Using analog input %d for battery level\n", BATTERY_PIN);

View File

@ -239,4 +239,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1
#define MESHTASTIC_EXCLUDE_TRACEROUTE 1
#define MESHTASTIC_EXCLUDE_WAYPOINT 1
#define MESHTASTIC_EXCLUDE_INPUTBROKER 1
#define MESHTASTIC_EXCLUDE_SERIAL 1
#endif

View File

@ -71,28 +71,24 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
}
}
// Trigger the refresh in GxEPD2
LOG_DEBUG("Updating E-Paper... ");
#if false
// Currently unused; rescued from commented-out line during a refactor
// Use a meaningful macro here if variant doesn't want fast refresh
// Full update mode (slow)
adafruitDisplay->display(false)
#else
// Fast update mode
adafruitDisplay->nextPage();
#endif
#ifndef EINK_NO_HIBERNATE // Only hibernate if controller IC will preserve image memory
// Put screen to sleep to save power (possibly not necessary because we already did poweroff inside of display)
adafruitDisplay->hibernate();
#endif
// End the update process
endUpdate();
LOG_DEBUG("done\n");
return true;
}
// End the update process - virtual method, overriden in derived class
void EInkDisplay::endUpdate()
{
// Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep)
adafruitDisplay->hibernate();
}
// Write the buffer to the display memory
void EInkDisplay::display(void)
{
@ -188,6 +184,7 @@ bool EInkDisplay::connect()
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
adafruitDisplay->clearScreen(); // Clearing now, so the boot logo will draw nice and smoothe (fast refresh)
}
#elif defined(PCA10059)
{

View File

@ -45,6 +45,13 @@ class EInkDisplay : public OLEDDisplay
*/
virtual bool forceDisplay(uint32_t msecLimit = 1000);
/**
* Run any code needed to complete an update, after the physical refresh has completed.
* Split from forceDisplay(), to enable async refresh in derived EInkDynamicDisplay class.
*
*/
virtual void endUpdate();
/**
* shim to make the abstraction happy
*

View File

@ -25,19 +25,19 @@ EInkDynamicDisplay::~EInkDynamicDisplay()
// Screen requests a BACKGROUND frame
void EInkDynamicDisplay::display()
{
setFrameFlag(BACKGROUND);
addFrameFlag(BACKGROUND);
update();
}
// Screen requests a RESPONSIVE frame
bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit)
{
setFrameFlag(RESPONSIVE);
addFrameFlag(RESPONSIVE);
return update(); // (Unutilized) Base class promises to return true if update ran
}
// Add flag for the next frame
void EInkDynamicDisplay::setFrameFlag(frameFlagTypes flag)
void EInkDynamicDisplay::addFrameFlag(frameFlagTypes flag)
{
// OR the new flag into the existing flags
this->frameFlags = (frameFlagTypes)(this->frameFlags | flag);
@ -94,38 +94,75 @@ void EInkDynamicDisplay::adjustRefreshCounters()
// Trigger the display update by calling base class
bool EInkDynamicDisplay::update()
{
// Detemine the refresh mode to use, and start the update
bool refreshApproved = determineMode();
if (refreshApproved)
if (refreshApproved) {
EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system
return refreshApproved; // (Unutilized) Base class promises to return true if update ran
storeAndReset(); // Store the result of this loop for next time. Note: call *before* endOrDetach()
endOrDetach(); // endUpdate() right now, or set the async refresh flag (if FULL and HAS_EINK_ASYNCFULL)
} else
storeAndReset(); // No update, no post-update code, just store the results
return refreshApproved; // (Unutilized) Base class promises to return true if update ran
}
// Figure out who runs the post-update code
void EInkDynamicDisplay::endOrDetach()
{
// If the GxEPD2 version reports that it has the async modifications
#ifdef HAS_EINK_ASYNCFULL
if (previousRefresh == FULL) {
asyncRefreshRunning = true; // Set the flag - picked up at start of determineMode(), next loop.
if (previousFrameFlags & BLOCKING)
awaitRefresh();
else
LOG_DEBUG("Async full-refresh begins\n");
}
// Fast Refresh
else if (previousRefresh == FAST)
EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves.
// Fallback - If using an unmodified version of GxEPD2 for some reason
#else
if (previousRefresh == FULL || previousRefresh == FAST) { // If refresh wasn't skipped (on unspecified..)
LOG_WARN(
"GxEPD2 version has not been modified to support async refresh; using fallback behavior. Please update lib_deps in "
"variant's platformio.ini file\n");
EInkDisplay::endUpdate();
}
#endif
}
// Assess situation, pick a refresh type
bool EInkDynamicDisplay::determineMode()
{
checkWasFlooded();
checkInitialized();
checkForPromotion();
#if defined(HAS_EINK_ASYNCFULL)
checkAsyncFullRefresh();
#endif
checkRateLimiting();
// If too soon for a new time, abort here
if (refresh == SKIPPED) {
storeAndReset();
// If too soon for a new frame, or display busy, abort early
if (refresh == SKIPPED)
return false; // No refresh
}
// -- New frame is due --
resetRateLimiting(); // Once determineMode() ends, will have to wait again
hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check
LOG_DEBUG("EInkDynamicDisplay: "); // Begin log entry
LOG_DEBUG("determineMode(): "); // Begin log entry
// Once mode determined, any remaining checks will bypass
checkCosmetic();
checkDemandingFast();
checkFrameMatchesPrevious();
checkConsecutiveFastRefreshes();
#ifdef EINK_LIMIT_GHOSTING_PX
checkExcessiveGhosting();
#endif
checkFrameMatchesPrevious();
checkFastRequested();
if (refresh == UNSPECIFIED)
@ -142,22 +179,49 @@ bool EInkDynamicDisplay::determineMode()
#endif
// Return - call a refresh or not?
if (refresh == SKIPPED) {
storeAndReset();
if (refresh == SKIPPED)
return false; // Don't trigger a refresh
} else {
storeAndReset();
else
return true; // Do trigger a refresh
}
// Is this the very first frame?
void EInkDynamicDisplay::checkInitialized()
{
if (!initialized) {
// Undo GxEPD2_BW::partialWindow(), if set by developer in EInkDisplay::connect()
configForFullRefresh();
// Clear any existing image, so we can draw logo with fast-refresh, but also to set GxEPD2_EPD::_initial_write
adafruitDisplay->clearScreen();
LOG_DEBUG("initialized, ");
initialized = true;
// Use a fast-refresh for the next frame; no skipping or else blank screen when waking from deep sleep
addFrameFlag(DEMAND_FAST);
}
}
// Did RESPONSIVE frames previously exceed the rate-limit for fast refresh?
void EInkDynamicDisplay::checkWasFlooded()
// Was a frame skipped (rate, display busy) that should have been a FAST refresh?
void EInkDynamicDisplay::checkForPromotion()
{
if (previousReason == EXCEEDED_RATELIMIT_FAST) {
// If so, allow a BACKGROUND frame to draw as RESPONSIVE
// Because we DID want a RESPONSIVE frame last time, we just didn't get it
setFrameFlag(RESPONSIVE);
// If a frame was skipped (rate, display busy), then promote a BACKGROUND frame
// Because we DID want a RESPONSIVE/COSMETIC/DEMAND_FULL frame last time, we just didn't get it
switch (previousReason) {
case ASYNC_REFRESH_BLOCKED_DEMANDFAST:
addFrameFlag(DEMAND_FAST);
break;
case ASYNC_REFRESH_BLOCKED_COSMETIC:
addFrameFlag(COSMETIC);
break;
case ASYNC_REFRESH_BLOCKED_RESPONSIVE:
case EXCEEDED_RATELIMIT_FAST:
addFrameFlag(RESPONSIVE);
break;
default:
break;
}
}
@ -204,7 +268,7 @@ void EInkDynamicDisplay::checkCosmetic()
if (frameFlags & COSMETIC) {
refresh = FULL;
reason = FLAGGED_COSMETIC;
LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC\n");
LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x\n", frameFlags);
}
}
@ -219,22 +283,7 @@ void EInkDynamicDisplay::checkDemandingFast()
if (frameFlags & DEMAND_FAST) {
refresh = FAST;
reason = FLAGGED_DEMAND_FAST;
LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST\n");
}
}
// Have too many fast-refreshes occured consecutively, since last full refresh?
void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
// If too many FAST refreshes consecutively - force a FULL refresh
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
refresh = FULL;
reason = EXCEEDED_LIMIT_FASTREFRESH;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH\n");
LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x\n", frameFlags);
}
}
@ -254,7 +303,7 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious()
if (frameFlags == BACKGROUND && fastRefreshCount > 0) {
refresh = FULL;
reason = REDRAW_WITH_FULL;
LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL\n");
LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x\n", frameFlags);
return;
}
#endif
@ -262,7 +311,22 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious()
// Not redrawn, not COSMETIC, not DEMAND_FAST
refresh = SKIPPED;
reason = FRAME_MATCHED_PREVIOUS;
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS\n");
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x\n", frameFlags);
}
// Have too many fast-refreshes occured consecutively, since last full refresh?
void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
// If too many FAST refreshes consecutively - force a FULL refresh
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
refresh = FULL;
reason = EXCEEDED_LIMIT_FASTREFRESH;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x\n", frameFlags);
}
}
// No objections, we can perform fast-refresh, if desired
@ -276,7 +340,8 @@ void EInkDynamicDisplay::checkFastRequested()
// If we want BACKGROUND to use fast. (FULL only when a limit is hit)
refresh = FAST;
reason = BACKGROUND_USES_FAST;
LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu\n", fastRefreshCount);
LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x\n", fastRefreshCount,
frameFlags);
#else
// If we do want to use FULL for BACKGROUND updates
refresh = FULL;
@ -289,7 +354,7 @@ void EInkDynamicDisplay::checkFastRequested()
if (frameFlags & RESPONSIVE) {
refresh = FAST;
reason = NO_OBJECTIONS;
LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu\n", fastRefreshCount);
LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x\n", fastRefreshCount, frameFlags);
}
}
@ -313,6 +378,7 @@ void EInkDynamicDisplay::hashImage()
// Store the results of determineMode() for future use, and reset for next call
void EInkDynamicDisplay::storeAndReset()
{
previousFrameFlags = frameFlags;
previousRefresh = refresh;
previousReason = reason;
@ -369,7 +435,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting()
if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) {
refresh = FULL;
reason = EXCEEDED_GHOSTINGLIMIT;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT\n");
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x\n", frameFlags);
}
}
@ -381,4 +447,54 @@ void EInkDynamicDisplay::resetGhostPixelTracking()
}
#endif // EINK_LIMIT_GHOSTING_PX
#ifdef HAS_EINK_ASYNCFULL
// Check the status of an "async full-refresh", and run the finish-up code if the hardware is ready
void EInkDynamicDisplay::checkAsyncFullRefresh()
{
// No refresh taking place, continue with determineMode()
if (!asyncRefreshRunning)
return;
// Full refresh still running
if (adafruitDisplay->epd2.isBusy()) {
// No refresh
refresh = SKIPPED;
// Set the reason, marking what type of frame we're skipping
if (frameFlags & DEMAND_FAST)
reason = ASYNC_REFRESH_BLOCKED_DEMANDFAST;
else if (frameFlags & COSMETIC)
reason = ASYNC_REFRESH_BLOCKED_COSMETIC;
else if (frameFlags & RESPONSIVE)
reason = ASYNC_REFRESH_BLOCKED_RESPONSIVE;
else
reason = ASYNC_REFRESH_BLOCKED_BACKGROUND;
return;
}
// If we asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done
adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code
EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override)
asyncRefreshRunning = false; // Unset the flag
LOG_DEBUG("Async full-refresh complete\n");
// Note: this code only works because of a modification to meshtastic/GxEPD2.
// It is only equipped to intercept calls to nextPage()
}
// Hold control while an async refresh runs
void EInkDynamicDisplay::awaitRefresh()
{
// Continually poll the BUSY pin
while (adafruitDisplay->epd2.isBusy())
yield();
// End the full-refresh process
adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code
EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override)
asyncRefreshRunning = false; // Unset the flag
}
#endif // HAS_EINK_ASYNCFULL
#endif // USE_EINK_DYNAMICDISPLAY

View File

@ -28,8 +28,9 @@ class EInkDynamicDisplay : public EInkDisplay
RESPONSIVE = (1 << 1), // For frames via forceDisplay()
COSMETIC = (1 << 2), // For splashes
DEMAND_FAST = (1 << 3), // Special case only
BLOCKING = (1 << 4), // Modifier - block while refresh runs
};
void setFrameFlag(frameFlagTypes flag);
void addFrameFlag(frameFlagTypes flag);
// Set the correct frame flag, then call universal "update()" method
void display() override;
@ -44,6 +45,10 @@ class EInkDynamicDisplay : public EInkDisplay
};
enum reasonTypes : uint8_t { // How was the decision reached
NO_OBJECTIONS,
ASYNC_REFRESH_BLOCKED_DEMANDFAST,
ASYNC_REFRESH_BLOCKED_COSMETIC,
ASYNC_REFRESH_BLOCKED_RESPONSIVE,
ASYNC_REFRESH_BLOCKED_BACKGROUND,
EXCEEDED_RATELIMIT_FAST,
EXCEEDED_RATELIMIT_FULL,
FLAGGED_COSMETIC,
@ -62,14 +67,16 @@ class EInkDynamicDisplay : public EInkDisplay
void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type
void adjustRefreshCounters(); // Update fastRefreshCount
bool update(); // Trigger the display update - determine mode, then call base class
void endOrDetach(); // Run the post-update code, or delegate it off to checkAsyncFullRefresh()
// Checks as part of determineMode()
void checkWasFlooded(); // Was the previous frame skipped for exceeding EINK_LIMIT_RATE_RESPONSIVE_SEC?
void checkInitialized(); // Is this the very first frame?
void checkForPromotion(); // Was a frame skipped (rate, display busy) that should have been a FAST refresh?
void checkRateLimiting(); // Is this frame too soon?
void checkCosmetic(); // Was the COSMETIC flag set?
void checkDemandingFast(); // Was the DEMAND_FAST flag set?
void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively?
void checkFrameMatchesPrevious(); // Does the new frame match the existing display image?
void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively?
void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND?
void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting
@ -77,14 +84,16 @@ class EInkDynamicDisplay : public EInkDisplay
void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call
// What we are determining for this frame
frameFlagTypes frameFlags = BACKGROUND; // Frame type(s) - determineMode() input
frameFlagTypes frameFlags = BACKGROUND; // Frame characteristics - determineMode() input
refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output
reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used
// What happened last time determineMode() ran
refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome
reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason
frameFlagTypes previousFrameFlags = BACKGROUND; // (Previous) Frame flags
refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome
reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason
bool initialized = false; // Have we drawn at least one frame yet?
uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting)
uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed!
uint32_t previousImageHash = 0; // Hash of the previous update's frame
@ -99,6 +108,20 @@ class EInkDynamicDisplay : public EInkDisplay
uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
#endif
// Conditional - async full refresh - only with modified meshtastic/GxEPD2
#if defined(HAS_EINK_ASYNCFULL)
void checkAsyncFullRefresh(); // Check the status of "async full-refresh"; run the post-update code if the hardware is ready
void awaitRefresh(); // Hold control while an async refresh runs
void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay()
bool asyncRefreshRunning = false; // Flag, checked by checkAsyncFullRefresh()
#endif
};
// Tidier calls to addFrameFlag() from outside class
#define EINK_ADD_FRAMEFLAG(display, flag) static_cast<EInkDynamicDisplay *>(display)->addFrameFlag(EInkDynamicDisplay::flag)
#else // !USE_EINK_DYNAMICDISPLAY
// Dummy-macro, removes the need for include guards
#define EINK_ADD_FRAMEFLAG(display, flag)
#endif

View File

@ -260,6 +260,10 @@ static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i
/// Used on eink displays while in deep sleep
static void drawSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Next frame should use full-refresh, and block while running, else device will sleep before async callback
EINK_ADD_FRAMEFLAG(display, COSMETIC);
EINK_ADD_FRAMEFLAG(display, BLOCKING);
drawIconScreen("Sleeping...", display, state, x, y);
}
#endif
@ -1170,6 +1174,7 @@ int32_t Screen::runOnce()
break;
case Cmd::STOP_BLUETOOTH_PIN_SCREEN:
case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
setFrames();
break;
case Cmd::PRINT:
@ -1350,6 +1355,7 @@ void Screen::handleStartBluetoothPinScreen(uint32_t pin)
{
LOG_DEBUG("showing bluetooth screen\n");
showingNormalScreen = false;
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame
static FrameCallback frames[] = {drawFrameBluetooth};
snprintf(btPIN, sizeof(btPIN), "%06u", pin);
@ -1367,6 +1373,7 @@ void Screen::handleShutdownScreen()
{
LOG_DEBUG("showing shutdown screen\n");
showingNormalScreen = false;
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame
auto frame = [](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
drawFrameText(display, state, x, y, "Shutting down...");
@ -1380,6 +1387,7 @@ void Screen::handleRebootScreen()
{
LOG_DEBUG("showing reboot screen\n");
showingNormalScreen = false;
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame
auto frame = [](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
drawFrameText(display, state, x, y, "Rebooting...");
@ -1392,6 +1400,7 @@ void Screen::handleStartFirmwareUpdateScreen()
{
LOG_DEBUG("showing firmware screen\n");
showingNormalScreen = false;
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame
static FrameCallback frames[] = {drawFrameFirmware};
setFrameImmediateDraw(frames);

View File

@ -2,6 +2,7 @@
#include "CryptoEngine.h"
#include "DisplayFormatters.h"
#include "NodeDB.h"
#include "RadioInterface.h"
#include "configuration.h"
#include <assert.h>
@ -254,6 +255,25 @@ const char *Channels::getName(size_t chIndex)
return channelName;
}
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
if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency)
return false;
// Check if any of the channels are using the default name and PSK
for (size_t i = 0; i < getNumChannels(); i++) {
const auto &ch = getByIndex(i);
if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) {
const char *name = getName(i);
const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
// Check if the name is the default derived from the modem preset
if (strcmp(name, presetName) == 0)
return true;
}
}
return false;
}
/**
* Generate a short suffix used to disambiguate channels that might have the same "name" entered by the human but different PSKs.
* The ideas is that the PSK changing should be visible to the user so that they see they probably messed up and that's why they

View File

@ -102,6 +102,9 @@ class Channels
*/
int16_t setActiveByIndex(ChannelIndex channelIndex);
// Returns true if we can be reached via a channel with the default settings given a region and modem preset
bool hasDefaultChannel();
private:
/** Given a channel index, change to use the crypto key specified by that index
*

View File

@ -744,14 +744,17 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p)
#define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline
size_t NodeDB::getNumOnlineMeshNodes()
size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
{
size_t numseen = 0;
// FIXME this implementation is kinda expensive
for (int i = 0; i < *numMeshNodes; i++)
for (int i = 0; i < *numMeshNodes; i++) {
if (localOnly && meshNodes[i].via_mqtt)
continue;
if (sinceLastSeen(&meshNodes[i]) < NUM_ONLINE_SECS)
numseen++;
}
return numseen;
}

View File

@ -108,8 +108,10 @@ class NodeDB
// get channel channel index we heard a nodeNum on, defaults to 0 if not found
uint8_t getMeshNodeChannel(NodeNum n);
/// Return the number of nodes we've heard from recently (within the last 2 hrs?)
size_t getNumOnlineMeshNodes();
/* Return the number of nodes we've heard from recently (within the last 2 hrs?)
* @param localOnly if true, ignore nodes heard via MQTT
*/
size_t getNumOnlineMeshNodes(bool localOnly = false);
void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(), removeNodeByNum(uint nodeNum);

View File

@ -1,5 +1,6 @@
#include "RadioInterface.h"
#include "Channels.h"
#include "DisplayFormatters.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
@ -143,6 +144,7 @@ const RegionInfo regions[] = {
};
const RegionInfo *myRegion;
bool RadioInterface::uses_default_frequency_slot = true;
static uint8_t bytes[MAX_RHPACKETLEN];
@ -486,6 +488,10 @@ void RadioInterface::applyModemConfig()
// channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1)
int channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels;
// Check if we use the default frequency slot
RadioInterface::uses_default_frequency_slot =
channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false)) % numChannels;
// Old frequency selection formula
// float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num);

View File

@ -175,6 +175,9 @@ class RadioInterface
/// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers
virtual bool isIRQPending() { return false; }
// Whether we use the default frequency slot given our LoRa config (region and modem preset)
static bool uses_default_frequency_slot;
protected:
int8_t power = 17; // Set by applyModemConfig()

View File

@ -167,8 +167,6 @@ bool ReliableRouter::stopRetransmission(GlobalPacketId key)
auto old = findPendingPacket(key);
if (old) {
auto p = old->packet;
auto numErased = pending.erase(key);
assert(numErased == 1);
/* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue
to avoid canceling a transmission if it was ACKed super fast via MQTT */
if (old->numRetransmissions < NUM_RETRANSMISSIONS - 1) {
@ -177,6 +175,8 @@ bool ReliableRouter::stopRetransmission(GlobalPacketId key)
// now free the pooled copy for retransmission too
packetPool.release(p);
}
auto numErased = pending.erase(key);
assert(numErased == 1);
return true;
} else
return false;

View File

@ -39,11 +39,11 @@
#if HAS_TELEMETRY
#include "modules/Telemetry/DeviceTelemetry.h"
#endif
#if HAS_SENSOR && !EXCLUDE_ENVIRONMENTAL_SENSOR
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "modules/Telemetry/AirQualityTelemetry.h"
#include "modules/Telemetry/EnvironmentTelemetry.h"
#endif
#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) && !EXCLUDE_POWER_TELEMETRY
#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
#include "modules/Telemetry/PowerTelemetry.h"
#endif
#ifdef ARCH_ESP32
@ -138,13 +138,13 @@ void setupModules()
#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO)
new DeviceTelemetryModule();
#endif
#if HAS_SENSOR && !EXCLUDE_ENVIRONMENTAL_SENSOR
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
new EnvironmentTelemetryModule();
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
new AirQualityTelemetryModule();
}
#endif
#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) && !EXCLUDE_POWER_TELEMETRY
#if HAS_TELEMETRY && !defined(ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY
new PowerTelemetryModule();
#endif
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \

View File

@ -114,7 +114,7 @@ void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies)
p->to = dest;
p->decoded.want_response = wantReplies;
p->hop_limit = 0;
p->want_ack = true;
p->want_ack = false;
packetSequence++;

View File

@ -112,7 +112,7 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
else
p->priority = meshtastic_MeshPacket_Priority_MIN;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)

View File

@ -91,7 +91,7 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
p->to = dest;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_MIN;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
nodeDB.updateTelemetry(nodeDB.getNodeNum(), telemetry, RX_SRC_LOCAL);
if (phoneOnly) {

View File

@ -253,7 +253,7 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
else
p->priority = meshtastic_MeshPacket_Priority_MIN;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
packetPool.release(lastMeasurementPacket);

View File

@ -195,7 +195,7 @@ bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR)
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
else
p->priority = meshtastic_MeshPacket_Priority_MIN;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// release previous packet before occupying a new spot
if (lastMeasurementPacket != nullptr)
packetPool.release(lastMeasurementPacket);

View File

@ -12,7 +12,8 @@ int32_t SHT31Sensor::runOnce()
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
status = sht31.begin();
sht31 = Adafruit_SHT31(nodeTelemetrySensorsMap[sensorType].second);
status = sht31.begin(nodeTelemetrySensorsMap[sensorType].first);
return initI2CSensor();
}

View File

@ -5,7 +5,7 @@
class SHT31Sensor : public TelemetrySensor
{
private:
Adafruit_SHT31 sht31 = Adafruit_SHT31();
Adafruit_SHT31 sht31;
protected:
virtual void setup() override;

View File

@ -50,7 +50,7 @@ bool PaxcounterModule::sendInfo(NodeNum dest)
meshtastic_MeshPacket *p = allocDataProtobuf(pl);
p->to = dest;
p->decoded.want_response = false;
p->priority = meshtastic_MeshPacket_Priority_MIN;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
service.sendToMesh(p, RX_SRC_LOCAL, true);

View File

@ -255,7 +255,7 @@ void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForw
p->to = dest;
p->priority = meshtastic_MeshPacket_Priority_MIN;
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// FIXME - Determine if the delayed packet is broadcast or delayed. For now, assume
// everything is broadcast.
@ -334,7 +334,7 @@ ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &m
LOG_INFO("*** S&F - Busy. Try again shortly.\n");
meshtastic_MeshPacket *pr = allocReply();
pr->to = getFrom(&mp);
pr->priority = meshtastic_MeshPacket_Priority_MIN;
pr->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
pr->want_ack = false;
pr->decoded.want_response = false;
pr->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;

View File

@ -77,8 +77,7 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length)
if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) {
memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length());
p->decoded.payload.size = jsonPayloadStr.length();
meshtastic_MeshPacket *packet = packetPool.allocCopy(*p);
service.sendToMesh(packet, RX_SRC_LOCAL);
service.sendToMesh(p, RX_SRC_LOCAL);
} else {
LOG_WARN("Received MQTT json payload too long, dropping\n");
}
@ -186,10 +185,17 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE)
statusTopic = moduleConfig.mqtt.root + statusTopic;
cryptTopic = moduleConfig.mqtt.root + cryptTopic;
jsonTopic = moduleConfig.mqtt.root + jsonTopic;
mapTopic = moduleConfig.mqtt.root + mapTopic;
} else {
statusTopic = "msh" + statusTopic;
cryptTopic = "msh" + cryptTopic;
jsonTopic = "msh" + jsonTopic;
mapTopic = "msh" + mapTopic;
}
if (moduleConfig.mqtt.map_reporting_enabled && moduleConfig.mqtt.has_map_report_settings) {
map_position_precision = moduleConfig.mqtt.map_report_settings.position_precision;
map_publish_interval_secs = moduleConfig.mqtt.map_report_settings.publish_interval_secs;
}
#ifdef HAS_NETWORKING
@ -365,27 +371,30 @@ void MQTT::sendSubscriptions()
bool MQTT::wantsLink() const
{
bool hasChannel = false;
bool hasChannelorMapReport = false;
if (moduleConfig.mqtt.enabled) {
// No need for link if no channel needed it
size_t numChan = channels.getNumChannels();
for (size_t i = 0; i < numChan; i++) {
const auto &ch = channels.getByIndex(i);
if (ch.settings.uplink_enabled || ch.settings.downlink_enabled) {
hasChannel = true;
break;
hasChannelorMapReport = moduleConfig.mqtt.map_reporting_enabled;
if (!hasChannelorMapReport) {
// No need for link if no channel needed it
size_t numChan = channels.getNumChannels();
for (size_t i = 0; i < numChan; i++) {
const auto &ch = channels.getByIndex(i);
if (ch.settings.uplink_enabled || ch.settings.downlink_enabled) {
hasChannelorMapReport = true;
break;
}
}
}
}
if (hasChannel && moduleConfig.mqtt.proxy_to_client_enabled)
if (hasChannelorMapReport && moduleConfig.mqtt.proxy_to_client_enabled)
return true;
#if HAS_WIFI
return hasChannel && WiFi.isConnected();
return hasChannelorMapReport && WiFi.isConnected();
#endif
#if HAS_ETHERNET
return hasChannel && Ethernet.linkStatus() == LinkON;
return hasChannelorMapReport && Ethernet.linkStatus() == LinkON;
#endif
return false;
}
@ -397,6 +406,8 @@ int32_t MQTT::runOnce()
bool wantConnection = wantsLink();
perhapsReportToMap();
// If connected poll rapidly, otherwise only occasionally check for a wifi connection change and ability to contact server
if (moduleConfig.mqtt.proxy_to_client_enabled) {
publishQueuedMessages();
@ -536,6 +547,78 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp, const meshtastic_MeshPacket &
}
}
void MQTT::perhapsReportToMap()
{
if (!moduleConfig.mqtt.map_reporting_enabled || !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly()))
return;
if (millis() - last_report_to_map < map_publish_interval_secs * 1000) {
return;
} else {
if (map_position_precision == 0 || (localPosition.latitude_i == 0 && localPosition.longitude_i == 0)) {
LOG_WARN("MQTT Map reporting is enabled, but precision is 0 or no position available.\n");
return;
}
// Allocate ServiceEnvelope and fill it
meshtastic_ServiceEnvelope *se = mqttPool.allocZeroed();
se->channel_id = (char *)channels.getGlobalId(channels.getPrimaryIndex()); // Use primary channel as the channel_id
se->gateway_id = owner.id;
// Allocate MeshPacket and fill it
meshtastic_MeshPacket *mp = packetPool.allocZeroed();
mp->which_payload_variant = meshtastic_MeshPacket_decoded_tag;
mp->from = nodeDB.getNodeNum();
mp->to = NODENUM_BROADCAST;
mp->decoded.portnum = meshtastic_PortNum_MAP_REPORT_APP;
// Fill MapReport message
meshtastic_MapReport mapReport = meshtastic_MapReport_init_default;
memcpy(mapReport.long_name, owner.long_name, sizeof(owner.long_name));
memcpy(mapReport.short_name, owner.short_name, sizeof(owner.short_name));
mapReport.role = config.device.role;
mapReport.hw_model = owner.hw_model;
strncpy(mapReport.firmware_version, optstr(APP_VERSION), sizeof(mapReport.firmware_version));
mapReport.region = config.lora.region;
mapReport.modem_preset = config.lora.modem_preset;
mapReport.has_default_channel = channels.hasDefaultChannel();
// Set position with precision (same as in PositionModule)
if (map_position_precision < 32 && map_position_precision > 0) {
mapReport.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - map_position_precision));
mapReport.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - map_position_precision));
mapReport.latitude_i += (1 << (31 - map_position_precision));
mapReport.longitude_i += (1 << (31 - map_position_precision));
} else {
mapReport.latitude_i = localPosition.latitude_i;
mapReport.longitude_i = localPosition.longitude_i;
}
mapReport.altitude = localPosition.altitude;
mapReport.position_precision = map_position_precision;
mapReport.num_online_local_nodes = nodeDB.getNumOnlineMeshNodes(true);
// Encode MapReport message and set it to MeshPacket in ServiceEnvelope
mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes),
&meshtastic_MapReport_msg, &mapReport);
se->packet = mp;
// FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets
static uint8_t bytes[meshtastic_MeshPacket_size + 64];
size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, se);
LOG_INFO("MQTT Publish map report to %s\n", mapTopic.c_str());
publish(mapTopic.c_str(), bytes, numBytes, false);
// Release the allocated memory for ServiceEnvelope and MeshPacket
mqttPool.release(se);
packetPool.release(mp);
// Update the last report time
last_report_to_map = millis();
}
}
// converts a downstream packet into a json message
std::string MQTT::meshPacketToJson(meshtastic_MeshPacket *mp)
{
@ -832,4 +915,4 @@ bool MQTT::isValidJsonEnvelope(JSONObject &json)
(json["from"]->AsNumber() == nodeDB.getNodeNum()) && // only accept message if the "from" is us
(json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type
(json.find("payload") != json.end()); // should have a payload
}
}

View File

@ -79,11 +79,18 @@ class MQTT : private concurrency::OSThread
virtual int32_t runOnce() override;
private:
std::string statusTopic = "/2/stat/";
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID
/** return true if we have a channel that wants uplink/downlink
*/
std::string statusTopic = "/2/stat/"; // For "online"/"offline" message
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID
std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages
// For map reporting (only applies when enabled)
uint32_t last_report_to_map = 0;
uint32_t map_position_precision = 32; // default to full precision
uint32_t map_publish_interval_secs = 60 * 15; // default to 15 minutes
/** return true if we have a channel that wants uplink/downlink or map reporting is enabled
*/
bool wantsLink() const;
/** Tell the server what subscriptions we want (based on channels.downlink_enabled)
@ -102,6 +109,9 @@ class MQTT : private concurrency::OSThread
void publishStatus();
void publishQueuedMessages();
// Check if we should report unencrypted information about our node for consumption by a map
void perhapsReportToMap();
// returns true if this is a valid JSON envelope which we accept on downlink
bool isValidJsonEnvelope(JSONObject &json);

View File

@ -16,7 +16,7 @@ build_flags =
-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
lib_deps =
${esp32s3_base.lib_deps}
https://github.com/meshtastic/GxEPD2
https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a
adafruit/Adafruit BusIO@^1.13.2
lewisxhe/PCF8563_Library@^1.0.1
upload_speed = 115200

View File

@ -5,7 +5,6 @@
#define I2C_SCL SCL
#define USE_EINK
#define EINK_NO_HIBERNATE
/*
* eink display pins

View File

@ -16,7 +16,7 @@ build_flags =
;-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
lib_deps =
${esp32s3_base.lib_deps}
https://github.com/meshtastic/GxEPD2/
https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a
adafruit/Adafruit BusIO@^1.13.2
lewisxhe/PCF8563_Library@^1.0.1
upload_speed = 115200

View File

@ -11,10 +11,16 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/t-echo
-DEINK_DISPLAY_MODEL=GxEPD2_154_D67
-DEINK_WIDTH=200
-DEINK_HEIGHT=200
-DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk
-DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted
-DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates
-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
-DEINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated
-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/t-echo>
lib_deps =
${nrf52840_base.lib_deps}
https://github.com/meshtastic/GxEPD2#afce87a97dda1ac31d8a28dc8fa7c6f55dc96a61
https://github.com/meshtastic/GxEPD2#55f618961db45a23eff0233546430f1e5a80f63a
adafruit/Adafruit BusIO@^1.13.2
lewisxhe/PCF8563_Library@^1.0.1
;upload_protocol = fs

View File

@ -1,4 +1,4 @@
[VERSION]
major = 2
minor = 3
build = 0
build = 1