Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cba7c75
Fix handling RX/TX for Hetlec v4 FEM
weebl2000 Dec 20, 2025
516a698
Apply same fix for Heltec tracker v2
weebl2000 Dec 21, 2025
9bb5e00
Let SX1262 DIO2 handle the RF switching and hold all pins during sleep
weebl2000 Dec 23, 2025
603b870
Don't hold CPS pin, it's not RTC
weebl2000 Dec 24, 2025
c839e17
Don't touch pin 46 except when transmitting.
weebl2000 Dec 24, 2025
231d16e
Allow negative tx power
weebl2000 Jan 3, 2026
69f1ad3
Save some more power when BLE/WiFi is disabled on the companion radio
weebl2000 Jan 8, 2026
0249860
Allow setting time to the future
weebl2000 Jan 9, 2026
6ac5515
Sync time with GPS every 30 minutes
weebl2000 Jan 9, 2026
3dfbd82
Let ESP32Board.sleep handle the button logic
weebl2000 Jan 9, 2026
af36864
Fix timestamp calculations when RTC clock is corrected backwards
weebl2000 Jan 9, 2026
2688f4e
Backup contacts to tmpFile before saving
weebl2000 Jan 10, 2026
8fc8bf9
Don't write contacts again if auto add isn't on
weebl2000 Jan 10, 2026
cec7988
Add GPS support Heltec Wireless Tracker v1.x
weebl2000 Jan 9, 2026
1044066
Merge remote-tracking branch 'weebl2000/sync-gps-time-30min' into dev
weebl2000 Jan 10, 2026
e97f48d
Merge remote-tracking branch 'weebl2000/powersaving-ble-companion' in…
weebl2000 Jan 10, 2026
a5462d0
Merge remote-tracking branch 'weebl2000/allow-negative-tx' into dev
weebl2000 Jan 10, 2026
c07722a
Merge remote-tracking branch 'weebl2000/robust-contacts' into dev
weebl2000 Jan 10, 2026
f5d9a39
Merge remote-tracking branch 'weebl2000/dont-save-autoadd-off' into dev
weebl2000 Jan 10, 2026
4cc8474
Merge remote-tracking branch 'weebl2000/allow-older-time' into dev
weebl2000 Jan 10, 2026
c550ae7
Merge remote-tracking branch 'weebl2000/heltec_v4_fix_RX_sensitivity'…
weebl2000 Jan 10, 2026
1df64cc
Pass rtc_clock to all MicroNMEALocationProvider instances
weebl2000 Jan 10, 2026
82381ef
Pass rtc_clock to all MicroNMEALocationProvider instances
weebl2000 Jan 10, 2026
88485a4
Merge remote-tracking branch 'weebl2000/pass-rtc_clock-to-locationpro…
weebl2000 Jan 10, 2026
1808f81
Set max power for BLE ESP32
weebl2000 Jan 10, 2026
75fa9a0
Default to LOW for pressed
weebl2000 Jan 10, 2026
f090774
Fixes for button presses
weebl2000 Jan 10, 2026
7986224
Merge branch 'powersaving-ble-companion' into dev
weebl2000 Jan 10, 2026
1232f85
Merge branch 'dev' of github.com:meshcore-dev/MeshCore into dev
weebl2000 Jan 12, 2026
266e489
remove serial debug logging from t3s3 sx1276 companion usb
recrof Jan 12, 2026
77257a3
Merge pull request #1377 from recrof/t3s3-sx1276-fix
liamcottle Jan 12, 2026
cd668a7
Merge branch 'dev' of github.com:meshcore-dev/MeshCore into dev
weebl2000 Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 58 additions & 28 deletions examples/companion_radio/DataStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,42 +267,57 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
}

void DataStore::loadContacts(DataStoreHost* host) {
File file = openRead(_getContactsChannelsFS(), "/contacts3");
if (file) {
bool full = false;
while (!full) {
ContactInfo c;
uint8_t pub_key[32];
uint8_t unused;

bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *)&c.name, 32) == 32);
success = success && (file.read(&c.type, 1) == 1);
success = success && (file.read(&c.flags, 1) == 1);
success = success && (file.read(&unused, 1) == 1);
success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved'
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4);

if (!success) break; // EOF
FILESYSTEM* fs = _getContactsChannelsFS();
File file = openRead(fs, "/contacts3");

// If main file doesn't exist or is empty, try backup
if (!file || file.size() == 0) {
if (file) file.close();
if (fs->exists("/contacts3.bak")) {
MESH_DEBUG_PRINTLN("WARN: contacts3 missing/empty, loading from backup");
file = openRead(fs, "/contacts3.bak");
}
}

c.id = mesh::Identity(pub_key);
if (!host->onContactLoaded(c)) full = true;
}
file.close();
if (file) {
bool full = false;
while (!full) {
ContactInfo c;
uint8_t pub_key[32];
uint8_t unused;

bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *)&c.name, 32) == 32);
success = success && (file.read(&c.type, 1) == 1);
success = success && (file.read(&c.flags, 1) == 1);
success = success && (file.read(&unused, 1) == 1);
success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved'
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4);

if (!success) break; // EOF

c.id = mesh::Identity(pub_key);
if (!host->onContactLoaded(c)) full = true;
}
file.close();
}
}

void DataStore::saveContacts(DataStoreHost* host) {
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
FILESYSTEM* fs = _getContactsChannelsFS();

// Write to temp file first (atomic write pattern)
File file = openWrite(fs, "/contacts3.tmp");
if (file) {
uint32_t idx = 0;
ContactInfo c;
uint8_t unused = 0;
bool write_success = true;

while (host->getContactForSave(idx, c)) {
bool success = (file.write(c.id.pub_key, 32) == 32);
Expand All @@ -318,11 +333,26 @@ void DataStore::saveContacts(DataStoreHost* host) {
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);

if (!success) break; // write failed
if (!success) {
write_success = false;
break; // write failed
}

idx++; // advance to next contact
}
file.flush();
file.close();

if (write_success) {
// Atomic swap: remove old backup, rename current to backup, rename temp to current
fs->remove("/contacts3.bak");
fs->rename("/contacts3", "/contacts3.bak");
fs->rename("/contacts3.tmp", "/contacts3");
} else {
// Write failed, remove incomplete temp file
fs->remove("/contacts3.tmp");
MESH_DEBUG_PRINTLN("ERROR: saveContacts write failed, temp file removed");
}
}
}

Expand Down
15 changes: 7 additions & 8 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(p->path, path, p->path_len);
}

// Don't save if this is a new contact but auto-add is off (contact not added to list)
if (is_new && !isAutoAddEnabled()) {
return;
}
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}

Expand Down Expand Up @@ -778,7 +782,7 @@ void MyMesh::begin(bool has_display) {
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
_prefs.sf = constrain(_prefs.sf, 5, 12);
_prefs.cr = constrain(_prefs.cr, 5, 8);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER);
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours

Expand Down Expand Up @@ -1004,13 +1008,8 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_SET_DEVICE_TIME && len >= 5) {
uint32_t secs;
memcpy(&secs, &cmd_frame[1], 4);
uint32_t curr = getRTCClock()->getCurrentTime();
if (secs >= curr) {
getRTCClock()->setCurrentTime(secs);
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
getRTCClock()->setCurrentTime(secs);
writeOKFrame();
} else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) {
mesh::Packet* pkt;
if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) {
Expand Down
2 changes: 1 addition & 1 deletion examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct NodePrefs { // persisted to file
uint8_t multi_acks;
uint8_t manual_add_contacts;
float bw;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
Expand Down
29 changes: 29 additions & 0 deletions examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
#endif
);

// Power saving timing variables
unsigned long lastActive = 0; // Last time there was activity
unsigned long nextSleepInSecs = 120; // Wait 2 minutes before first sleep
const unsigned long WORK_TIME_SECS = 5; // Stay awake 5 seconds after wake/activity

/* END GLOBAL OBJECTS */

void halt() {
Expand Down Expand Up @@ -219,6 +224,9 @@ void setup() {
#ifdef DISPLAY_CLASS
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
#endif

// Initialize power saving timer
lastActive = millis();
}

void loop() {
Expand All @@ -228,4 +236,25 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();

// Power saving when BLE/WiFi is disabled
// Don't sleep if GPS is enabled - it needs continuous operation to maintain fix
// Note: Disabling BLE/WiFi via UI actually turns off the radio to save power
if (!serial_interface.isEnabled() && !the_mesh.getNodePrefs()->gps_enabled) {
// Check for pending work and update activity timer
if (the_mesh.hasPendingWork()) {
lastActive = millis();
if (nextSleepInSecs < 10) {
nextSleepInSecs += 5; // Extend work time by 5s if still busy
}
}

// Only sleep if enough time has passed since last activity
if (millis() >= lastActive + (nextSleepInSecs * 1000)) {
board.sleep(1800); // Sleep for 30 minutes, wake on LoRa packet, timer, or button
// Just woke up - reset timers
lastActive = millis();
nextSleepInSecs = WORK_TIME_SECS; // Stay awake for 5s after wake
}
}
}
4 changes: 2 additions & 2 deletions examples/companion_radio/ui-new/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ class HomeScreen : public UIScreen {
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i];
if (a->name[0] == 0) continue; // empty slot
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
uint32_t secs = safeElapsedSecs(_rtc->getCurrentTime(), a->recv_timestamp);
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
Expand Down Expand Up @@ -480,7 +480,7 @@ class MsgPreviewScreen : public UIScreen {

auto p = &unread[0];

int secs = _rtc->getCurrentTime() - p->timestamp;
uint32_t secs = safeElapsedSecs(_rtc->getCurrentTime(), p->timestamp);
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
Expand Down
11 changes: 3 additions & 8 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t

// add next neighbour to results
auto neighbour = sorted_neighbours[index + offset];
uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
uint32_t heard_seconds_ago = safeElapsedSecs(getRTCClock()->getCurrentTime(), neighbour->heard_timestamp);
memcpy(&results_buffer[results_offset], neighbour->id.pub_key, pubkey_prefix_length); results_offset += pubkey_prefix_length;
memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4;
memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1;
Expand Down Expand Up @@ -895,7 +895,7 @@ void MyMesh::dumpLogFile() {
}
}

void MyMesh::setTxPower(uint8_t power_dbm) {
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}

Expand Down Expand Up @@ -930,7 +930,7 @@ void MyMesh::formatNeighborsReply(char *reply) {
mesh::Utils::toHex(hex, neighbour->id.pub_key, 4);

// add next neighbour
uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
uint32_t secs_ago = safeElapsedSecs(getRTCClock()->getCurrentTime(), neighbour->heard_timestamp);
sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr);
while (*dp)
dp++; // find end of string
Expand Down Expand Up @@ -1194,8 +1194,3 @@ void MyMesh::loop() {
uptime_millis += now - last_millis;
last_millis = now;
}

// To check if there is pending work
bool MyMesh::hasPendingWork() const {
return _mgr->getOutboundCount(0xFFFFFFFF) > 0;
}
5 changes: 1 addition & 4 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
}

void dumpLogFile() override;
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
Expand Down Expand Up @@ -230,7 +230,4 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
bridge.begin();
}
#endif

// To check if there is pending work
bool hasPendingWork() const;
};
8 changes: 7 additions & 1 deletion examples/simple_repeater/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
#include <Arduino.h>
#include <helpers/CommonCLI.h>

// Default button polarity: Active-LOW (pressed = LOW)
// Override with -D USER_BTN_PRESSED=HIGH in platformio.ini for rare active-high devices.
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif

#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds

Expand Down Expand Up @@ -85,7 +91,7 @@ void UITask::loop() {
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == LOW) { // pressed?
if (btnState == USER_BTN_PRESSED) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ void MyMesh::dumpLogFile() {
}
}

void MyMesh::setTxPower(uint8_t power_dbm) {
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}

Expand Down
2 changes: 1 addition & 1 deletion examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
}

void dumpLogFile() override;
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;

void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
Expand Down
8 changes: 7 additions & 1 deletion examples/simple_room_server/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
#include <Arduino.h>
#include <helpers/CommonCLI.h>

// Default button polarity: Active-LOW (pressed = LOW)
// Override with -D USER_BTN_PRESSED=HIGH in platformio.ini for rare active-high devices.
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif

#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds

Expand Down Expand Up @@ -85,7 +91,7 @@ void UITask::loop() {
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == LOW) { // pressed?
if (btnState == USER_BTN_PRESSED) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
Expand Down
13 changes: 4 additions & 9 deletions examples/simple_secure_chat/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct NodePrefs { // persisted to file
char node_name[32];
double node_lat, node_lon;
float freq;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t unused[3];
};

Expand Down Expand Up @@ -158,13 +158,8 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
}

void setClock(uint32_t timestamp) {
uint32_t curr = getRTCClock()->getCurrentTime();
if (timestamp > curr) {
getRTCClock()->setCurrentTime(timestamp);
Serial.println(" (OK - clock set!)");
} else {
Serial.println(" (ERR: clock cannot go backwards)");
}
getRTCClock()->setCurrentTime(timestamp);
Serial.println(" (OK - clock set!)");
}

void importCard(const char* command) {
Expand Down Expand Up @@ -290,7 +285,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
}

float getFreqPref() const { return _prefs.freq; }
uint8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
int8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }

void begin(FILESYSTEM& fs) {
_fs = &fs;
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ void SensorMesh::updateFloodAdvertTimer() {
}
}

void SensorMesh::setTxPower(uint8_t power_dbm) {
void SensorMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}

Expand Down
2 changes: 1 addition & 1 deletion examples/simple_sensor/SensorMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
void setLoggingOn(bool enable) override { }
void eraseLogFile() override { }
void dumpLogFile() override { }
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
Expand Down
Loading