Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
acac4af
FAQ: Fixed instructions on wiping and resetting nrf52 devices
recrof Aug 23, 2026
0679dbe
Merge pull request #3282 from recrof/patch-3
liamcottle Aug 24, 2026
7b1c97c
fix(lr2021): setTxPower() could leave the receiver down until reboot
fkallay1 Aug 15, 2026
94b9ea8
trimmed comment
oltaco Aug 17, 2026
946e5a4
repeater: set `loop.detect` to `minimal` as default for new installs
recrof Aug 18, 2026
69efe75
fix: move MeshadventurerBoard.h to correct location
recrof Aug 18, 2026
23930b9
thinknode_m8: initial support
fdlamotte Aug 8, 2026
8a2806d
gxepddisplay: better handling of full refresh (no faint partial after…
fdlamotte Aug 16, 2026
654f1ac
M8: preview messages up to 164 cars
fdlamotte Aug 17, 2026
7427313
gxepd: do full refresh before screen off ... so wakes quicker ;)
fdlamotte Aug 17, 2026
421cb19
updated gcc toolchain for nrf52 boards
liamcottle Aug 19, 2026
7ee4395
Fix for CMD_SEND_RAW_DATA and multi-byte paths
ripplebiz Aug 18, 2026
d3a82a7
fix for 3-byte paths passed to CMD_SEND_RAW_DATA
liamcottle Aug 19, 2026
1489343
Add charging indicator to companion_radio battery icon
hansimgamr Jul 28, 2026
a6636f5
Show plug at >=95% instead of exactly 100%
hansimgamr Jul 28, 2026
cd3b416
Skip empty channel slots in searchChannelsByHash
dl9sau Aug 19, 2026
d958e9a
Fixes BLE exceeding flash size
DominikTyrala Aug 17, 2026
3530ad9
fix(sensors): claim an I2C address after a successful init
agessaman Aug 17, 2026
7ac0ac8
fix(sensors): probe BMP/BME at both 0x76 and 0x77
agessaman Aug 17, 2026
9ce01ab
Duplicate MAX_CLIENTS definition
entr0p1 Aug 17, 2026
0442701
add support for rak12500 and rak12501 gps on rak3401
liamcottle Aug 22, 2026
660d07c
add UI_NO_HIBERNATE build flag to disable hibernate screen on wio tra…
liamcottle Aug 24, 2026
0881b8f
don't toggle io pins on rak3401 when probing gps
liamcottle Aug 23, 2026
d5f5ccc
Update patch for 1.17.1
netprince17 Aug 24, 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
53 changes: 53 additions & 0 deletions boards/thinknode_m8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_thinknode_m8",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M8",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "elecrow thinknode m8",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "",
"vendor": "ELECROW"
}
43 changes: 24 additions & 19 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,23 +691,28 @@ You can get the epoch time on <https://www.epochconverter.com> and use it to set
### 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.
**A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.

### 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?
**A:**

1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
- For RAK, click the reset button **TWICE**
- For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device **TWICE**
- For Heltec T114, click the reset button **TWICE** (the bottom button)
- For Xiao nRF52, click the reset button once. If that doesn't work, quickly double-click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader))
2. A new folder will appear on your computer's desktop
3. Download the `flash_erase*.uf2` file for your device on <https://flasher.meshcore.io>
- RAK WisBlock and Heltec T114: `Flash_erase-nRF32_softdevice_v6.uf2`
- Seeed Studio Xiao nRF52 WIO: `Flash_erase-nRF52_softdevice_v7.uf2`
4. drag and drop the uf2 file for your device to the root of the new folder
5. Wait for the copy to complete. You might get an error dialog, you can ignore it
6. Go to <https://flasher.meshcore.io>, click `Console` and select the serial port for your connected device
7. In the console, press enter. Your flash should now be erased
8. You may now flash the latest MeshCore firmware onto your device
### 6.7. Q: My RAK/T1000-E/xiao_nRF52/... device seems to be corrupted, how do I wipe it clean to start fresh?
**A: If you're able to connect to the device from your MeshCore app**
1. Navigate to Gear icon(Settings) on the upper right corner
2. Click on `Export Settings` button, choose `Select All` and Confirm. This will save your Node configuration
3. Now choose `Factory Reset` option
4. Confirm that you want to reset your device
5. Go to Bluetooth System settings and remove pairing of the device, so you can connect again
6. Connect your device in the app and enter the PIN
7. Navigate to Gear icon(Settings) on the upper right corner
8. Click on `Import Settings` and choose the file from 2., click on `Select All` and Confirm
9. Find `Reboot` button on of the Settings list

**B: You're not able to connect to the App**
1. Connect USB cable to your device
2. Go to https://flasher.meshcore.io
3. Search for your device in the list
4. Choose `Companion Bluetooth`
5. Press `Enter DFU mode` button, choose your USB device
6. Press `Erase Flash` button, choose the device again and wait until it completes
7. Press `Flash!` button and choose the USB device last time
8. The device is erased and newest firmware is installed
9. You might need to remove the pairing in Bluetooth System Settings in order to re-pair the app again.

Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on <https://flasher.meshcore.io>

Expand All @@ -716,9 +721,9 @@ Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If y

`NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.`

Allow the browser user on it:
Allow user access on your USB port:

`# setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0`
`sudo setfacl -m u:$USER:rw /dev/ttyUSB0`

---

Expand Down
22 changes: 13 additions & 9 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1511,16 +1511,20 @@ void MyMesh::handleCmdFrame(size_t len) {
#endif
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
int i = 1;
int8_t path_len = cmd_frame[i++];
if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload
uint8_t *path = &cmd_frame[i];
i += path_len;
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
writeOKFrame();
uint8_t path_len = cmd_frame[i++];
if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) {
uint8_t path[MAX_PATH_SIZE];
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
if (i + 4 > len) { // min payload 4 bytes
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
}
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet)
Expand Down
77 changes: 70 additions & 7 deletions examples/companion_radio/ui-new/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#include <time.h>
#ifdef WIFI_SSID
#include <WiFi.h>
#endif

#ifndef UI_TZ_OFFSET
#define UI_TZ_OFFSET 0
#endif

#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#endif
Expand All @@ -23,7 +28,7 @@
#define UI_RECENT_LIST_SIZE 4
#endif

#if UI_HAS_JOYSTICK
#if UI_HAS_JOYSTICK || UI_HAS_ROTARY_INPUT
#define PRESS_LABEL "press Enter"
#else
#define PRESS_LABEL "long press"
Expand Down Expand Up @@ -97,7 +102,9 @@ class HomeScreen : public UIScreen {
#if UI_SENSORS_PAGE == 1
SENSORS,
#endif
#ifndef UI_NO_HIBERNATE
SHUTDOWN,
#endif
Count // keep as last
};

Expand Down Expand Up @@ -141,11 +148,24 @@ class HomeScreen : public UIScreen {
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);

// show muted icon if buzzer is muted
// while charging, show a bolt (or a plug once full) just left of the battery,
// keeping the fill bar itself clean and uninterrupted
bool charging = board.isExternalPowered();
if (charging) {
// There's no charge-complete signal on most boards, so "full" is a high
// voltage band rather than an exact 100% (a real pack rarely reads 4.2V).
const int BATT_FULL_PCT = 95;
const uint8_t* symbol = (batteryPercentage >= BATT_FULL_PCT) ? plug_icon : charging_icon;
display.setColor(UIColor::title_txt);
display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8);
}

// show muted icon if buzzer is muted (shifted further left when the charging
// icon already occupies the slot immediately left of the battery)
#ifdef PIN_BUZZER
if (_task->isBuzzerQuiet()) {
display.setColor(UIColor::warning_txt);
display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
display.drawXbm(iconX - (charging ? 18 : 9), iconY + 1, muted_icon, 8, 8);
}
#endif
}
Expand Down Expand Up @@ -224,7 +244,19 @@ class HomeScreen : public UIScreen {
display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.drawTextCentered(display.width() / 2, 22, tmp);


#ifdef UI_SHOW_CLOCK
display.setTextSize(3);
uint32_t now = _rtc->getCurrentTime();
int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ...
now += (int32_t)tz * 3600;
DateTime dt (now);
sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute());
display.drawTextCentered(display.width() / 2, 60, tmp);
display.setTextSize(1);
sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year());
display.drawTextCentered(display.width() / 2, 80, tmp);
#endif
#ifdef WIFI_SSID
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
Expand All @@ -234,13 +266,21 @@ class HomeScreen : public UIScreen {
if (_task->hasConnection()) {
display.setColor(UIColor::warning_txt);
display.setTextSize(1);
#ifdef UI_SHOW_CLOCK
display.drawTextCentered(display.width() / 2, 110, "< Connected >");
#else
display.drawTextCentered(display.width() / 2, 43, "< Connected >");

#endif
} else if (the_mesh.getBLEPin() != 0) { // BT pin
display.setColor(UIColor::warning_txt);
display.setTextSize(2);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
#ifdef UI_SHOW_CLOCK
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 110, tmp);
#else
display.setTextSize(2);
display.drawTextCentered(display.width() / 2, 43, tmp);
#endif
}
} else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
Expand Down Expand Up @@ -421,6 +461,7 @@ class HomeScreen : public UIScreen {
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
else sensors_scroll_offset = 0;
#endif
#ifndef UI_NO_HIBERNATE
} else if (_page == HomePage::SHUTDOWN) {
display.setColor(UIColor::corp_blue);
display.setTextSize(1);
Expand All @@ -432,6 +473,7 @@ class HomeScreen : public UIScreen {
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
}
#endif
}
return 5000; // next render after 5000 ms
}
Expand Down Expand Up @@ -478,22 +520,28 @@ class HomeScreen : public UIScreen {
return true;
}
#endif
#ifndef UI_NO_HIBERNATE
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
_shutdown_init = true; // need to wait for button to be released
return true;
}
#endif
return false;
}
};

#ifndef UI_MSG_PREVIEW_SIZE
#define UI_MSG_PREVIEW_SIZE 78
#endif

class MsgPreviewScreen : public UIScreen {
UITask* _task;
mesh::RTCClock* _rtc;

struct MsgEntry {
uint32_t timestamp;
char origin[62];
char msg[78];
char msg[UI_MSG_PREVIEW_SIZE];
};
#define MAX_UNREAD_MSGS 32
int num_unread;
Expand Down Expand Up @@ -720,6 +768,9 @@ void UITask::shutdown(bool restart){
if (restart) {
_board->reboot();
} else {
display.forceFullRefresh();
display.clear();
display.endFrame();
// Power off board including radio, display, GPS and components
_board->powerOff();
}
Expand Down Expand Up @@ -760,6 +811,17 @@ void UITask::loop() {
}
#elif defined(PIN_USER_BTN)
int ev = user_btn.check();
#ifdef UI_HAS_NAV_INPUT
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_ENTER);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
display.turnOff();
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_SELECT);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#else
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
Expand All @@ -769,6 +831,7 @@ void UITask::loop() {
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#endif
#endif
#if defined(UI_HAS_ROTARY_INPUT)
RotaryInputEvent rotaryEv = rotary_input.poll();
Expand Down
10 changes: 10 additions & 0 deletions examples/companion_radio/ui-new/icons.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = {

static const uint8_t muted_icon[] = {
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
};

// small lightning bolt, 8x8px, shown next to the battery icon while charging
static const uint8_t charging_icon[] = {
0x18, 0x30, 0x60, 0xFC, 0x18, 0x30, 0x60, 0xC0
};

// small power plug, 8x8px, shown next to the battery icon once fully charged
static const uint8_t plug_icon[] = {
0x24, 0x24, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x18
};
11 changes: 8 additions & 3 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
int results_offset = 0;
uint8_t results_buffer[130];
for(int index = 0; index < count && index + offset < neighbours_count; index++){

// stop if we can't fit another entry in results
int entry_size = pubkey_prefix_length + 4 + 1;
if(results_offset + entry_size > sizeof(results_buffer)){
Expand Down Expand Up @@ -434,7 +434,8 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood()
&& mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped, _prefs.flood_max_advert)) {
&& mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped, _prefs.flood_max_advert,
_prefs.flood_max_request, _prefs.flood_max_anon_request, _prefs.flood_max_response)) {
return false;
}
if (packet->isRouteFlood() && recv_pkt_region == NULL) {
Expand Down Expand Up @@ -905,8 +906,12 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max = 64;
_prefs.flood_max_unscoped = 64;
_prefs.flood_max_advert = 8;
_prefs.flood_max_request = 64;
_prefs.flood_max_anon_request = 64;
_prefs.flood_max_response = 64;
_prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
_prefs.loop_detect = LOOP_DETECT_MINIMAL;

// bridge defaults
_prefs.bridge_enabled = 1; // enabled
Expand Down Expand Up @@ -1170,7 +1175,7 @@ void MyMesh::formatRadioStatsReply(char *reply) {
}

void MyMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
}

Expand Down
4 changes: 0 additions & 4 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ struct RepeaterStats {
uint32_t n_recv_errors;
};

#ifndef MAX_CLIENTS
#define MAX_CLIENTS 32
#endif

struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
Expand Down
Loading