Skip to content

Add Elecrow ThinkNode M4 variant (nRF52840 + LR1110) - #2682

Open
M0JSX wants to merge 278 commits into
meshcore-dev:mainfrom
M0JSX:feature/thinknode-m4
Open

M0JSX wants to merge 278 commits into
meshcore-dev:mainfrom
M0JSX:feature/thinknode-m4

Conversation

@M0JSX

@M0JSX M0JSX commented Jun 4, 2026 •

Copy link
Copy Markdown

Adds MeshCore companion BLE, repeater, and room server firmware support for the Elecrow ThinkNode M4 — a power bank device with integrated LoRa communication, GPS, and nRF52840 + Semtech LR1110.
Hardware:

MCU: Nordic nRF52840
Radio: Semtech LR1110 (868/915 MHz)
GPS: Quectel L76K
Battery: 2x18650 parallel (~7000mAh)
Bootloader: UF2, SoftDevice S140 v6.1.1

Environments:

ThinkNode_M4_companion_ble — BLE companion radio (PIN: 123456)
ThinkNode_M4_repeater — standalone repeater
ThinkNode_M4_room_server — standalone room server

Features implemented:

BLE advertising, pairing and secure connection
LoRa messaging via LR1110
GPS via L76K (enable/disable via settings)
Battery percentage and voltage via power bank serial chip (UART at 4800 baud, pins RX=30, TX=5)
Battery gauge LEDs — show level on button press for 10 seconds
LoRa TX/RX blue LED flash
Power-on red LED sequence — 3 quick flashes then steady during init
Sleep on 3s long press, wake on button press
Double press sends location advert

Key implementation notes:

LED_BLUE must be set to -1 — without this Bluefruit calls digitalWrite(41) continuously during BLE advertising, causing random hard fault crashes
Serial2 instead of SoftwareSerial — SoftwareSerial interrupt conflicts with the SoftDevice S140 causing random crashes; hardware UART (NRF_UARTE1) on pins 30/5 is stable
SoftDevice v6.1.1 required — must match the factory bootloader; v7 causes RADIOLIB_ERR_CHIP_NOT_FOUND
RadioLib must be pinned to 7.6.0 — v7.7.1 returns RADIOLIB_ERR_SPI_CMD_INVALID (-706) with the LR1110 firmware on this hardware
Power enable pins must be driven before radio SPI init: PIN_POWER_EN (11) HIGH and VEXT_ENABLE (32) LOW
RF switch table uses only DIO5/DIO6 with 2 values, matching Meshtastic's rfswitch.h for this board
PIN_SERIAL2_RX=30 / PIN_SERIAL2_TX=5 must be defined in variant.h to enable hardware Serial2 for the battery management chip
board.loop() is called from ThinkNodeM4SensorManager::loop() since the main loop only calls sensors.loop()
lib/nrf52/s140_nrf52_6.1.1_API must be copied from the PlatformIO framework packages before building:

bash cp -r ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/nordic/softdevice/s140_nrf52_6.1.1_API lib/nrf52/
Pin mapping (from Meshtastic variants/nrf52840/ELECROW-ThinkNode-M4/variant.h):

SPI: MISO=8, MOSI=7, SCK=6, NSS=27
LR1110: RESET=40 (P1.08), IRQ=12, BUSY=26
GPS: EN=43 (P1.11, active LOW), RESET=3, STANDBY=28, RX=46 (P1.14), TX=44 (P1.12)
Battery LEDs: 15, 17, 34 (P1.02), 36 (P1.04)
Battery serial: RX=30, TX=5, 4800 baud
Power: PIN_POWER_EN=11 (HIGH=on), VEXT_ENABLE=32/P1.00 (LOW=on)
Button: 4, LED_HEARTBEAT=41 (P1.09, red), LED_PAIRING=13 (blue)

Known limitations:

Battery percentage from serial chip may differ slightly from app-displayed value — the power bank chip reports per-cell voltage; the app uses its own voltage-to-percentage curve
GPS LED breathing/pulse effect not yet implemented

Tested on: Elecrow ThinkNode M4 (LMM14004D1), EU 868MHz — BLE advertising, pairing, LoRa messaging, battery display, LED indicators and sleep/wake all confirmed working.

@khagele

khagele commented Jul 11, 2026

Copy link
Copy Markdown

Tested this branch on real hardware (Elecrow ThinkNode M4, ThinkNode_M4_companion_ble env) after recovering the device from a bricked bootloader/missing SoftDevice state. Build succeeds cleanly and boots fine.

Worth noting: I initially tried a different community fork's ThinkNode M4 implementation, which had two issues this PR's implementation doesn't have:

  • VEXT_ENABLE (P1.00) was never driven low, so the shared peripheral power rail stayed off — this PR's ThinkNodeM4Board::begin() correctly enables it.
  • GPS used a 2-pin (EN/RESET) generic MicroNMEALocationProvider with an unmanaged STANDBY pin left asserted, so the L76K never woke up. This PR's dedicated ThinkNodeM4SensorManager::start_gps()/stop_gps() correctly drives all three pins (EN/RESET/STANDBY) and GPS is detected/working.

Confirmed working after flashing this PR's build: BLE pairing, LoRa, GPS detection, and the powerbank's onboard flashlight (which depends on the VEXT rail).

F4FPR and others added 28 commits August 14, 2026 00:42
RadioLibWrapper::setTxPower() called setOutputPower() and nothing else. On
LR2021 that writes the PA config and TxParams, which are standby-only commands,
and it never re-arms the receiver.

This only affects LR2021 boards. recvRaw() has:

    #if defined(USE_LR2021)
      state = STATE_RX;    // LR2021 stays in Rx after readData
    #else
      state = STATE_IDLE;  // need another startReceive()
    #endif

so on SX126x the next recvRaw() re-arms Rx anyway and the write is harmless,
while on LR2021 startReceive() is never called again on its own. A 'set tx'
issued while the radio was listening could therefore stop reception until the
next reboot. Observed a few times on an LR2021 repeater; recovery required a
power cycle.

Dispatcher's stuck-radio check does not catch it: isInRecvMode() reports the
wrapper's own `state` flag rather than the chip's actual mode, so it still
believes the radio is in Rx, and it only raises ERR_EVENT_STARTRX_TIMEOUT
without attempting recovery.

Fix: on LR2021, drop to standby via idle() before writing the PA config and let
checkRecv() re-arm Rx - the same pattern resetAGC() and
applySideDetectorConfig() already use. Other radios are left untouched.

Affects meshtracker_x1 and meshnology_w12 (both USE_LR2021).

Tested on hardware with an LR2021 repeater: six consecutive 'set tx' changes
(15/18/21/14/19/14), after which the radio kept receiving and forwarding
traffic (rawrx/rxpkts increasing, no missed IRQs). Noise-floor sampling also
kept updating, which only happens while state == STATE_RX.
MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win.

This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value).

Changes:
- Remove MAX_CLIENTS from repeater MyMesh.h
- Increase MAX_CLIENTS limit in ClientACL.h to 32
…power-rx-stall

Fix LR2021 occasionally stopping reception after a TX power change
Several table entries share an address and not every driver verifies a
chip ID: INA226::begin() only checks that the address ACKs, so an SHT4x
at 0x44 was also registered as an INA226 and reported junk current on a
second channel. Mark the address consumed once a driver initializes it
so later entries cannot re-claim the same device.
Grove and other Bosch modules strap SDO high, so the 0x76-only table
never initialized them. Add an alternate-address entry per Bosch sensor;
the bus scan still gates every probe, and all four drivers verify a chip
ID before claiming an address.

Each sensor type has a single static driver instance, so skip an entry
whose query is already active: the alternate address is a fallback, not
a second device.
…minimal

repeater: set `loop.detect` to `minimal` as default for new installs
…-relocate

fix: move MeshadventurerBoard.h to variants/meshadventurer
thinknode_m8: initial support

adds some functionalities to ui_task and epddisplay:

epd display, 
- supports screen off, and clear screen
- supports forcing full refresh every N partial refreshes (off by default, configured to 60 for M8)

ui_task
- UI_SHOW_CLOCK flag to display clock on first screen
- rotary button can act like joystick (with regards to Enter key), defaults to old config for existing nodes
- screen off on long press when using rotary button (very handy for the watch)
Update GCC toolchain for nRF52 boards to fix builds on Raspberry Pi
…t-fix

Fix for CMD_SEND_RAW_DATA and multi-byte paths
liamcottle and others added 30 commits October 7, 2026 12:16
Use consistent `_OTA` suffix for nRF52 OTA BLE names
…allocation

Add Nelos allocation for GroupData
ESP32-S3FH4R2 (4MB flash, 2MB PSRAM, native USB-CDC) with an LR1121.
Pins, the DIO5/DIO6 RF switch table and the 1.8 V TCXO follow the HF
schematic and Waveshare's examples. Envs: repeater, room server,
companion USB/BLE and KISS modem, all on min_spiffs so roles share one
partition layout.

Only RFO_HP_LF is wired (R4 on RFO_LP_LF is not fitted), so the new
opt-in LR1121_FORCE_HP_PA flag keeps RadioLib on the HP PA at <= 14 dBm,
where it would otherwise select the unconnected LP PA.
- Clip requested power to the active PA's range instead of failing. A
  stored 22 dBm was rejected at 2.4 GHz (HF PA max 13 dBm), and since
  setTxPower ignores errors the sub-GHz PA stayed configured.
- Re-apply the requested power after every successful retune: PA choice
  depends on the band, and set radio / tempradio only call setParams.
Treat transport code 0 the same as un-scoped.
…p32s3-lr1121

feat(variants): add Waveshare ESP32-S3-LR1121
…band-power

LR1121: keep TX power valid across sub-GHz and 2.4 GHz
…peater

Room: "set repeat" CLI now deprecated
…n-ble-wifi

Add WiFi support to Heltec v3 and v4 BLE variants
Disable NRF52 undervoltage bootlock feature
added dynamic wifi to: heltec wsl3, v4r8, wireless paper; rak 3112; seeed xiao s3 (wio)
Housekeeping: removed unnecessary space
Fix: tz_offset should allow for half hour timezone offsets
…ditional-display

Remove BLE screen when BLE not available
Upstream has since merged its own ThinkNode M4 port (0a15c48, 8bf072a,
b578775). Resolve all add/add conflicts in favour of upstream, drop the
vendored s140 6.1.1 headers (not needed by the upstream build), and keep
root platformio.ini as on dev.
…110 init retry

Carried over from the original feature/thinknode-m4 work onto the upstream port:
- Press the user button to show charge on the 4 battery LEDs (1/25/50/75%),
  auto-off after 10 s
- Three red flashes on power-up
- Retry radio.begin() once if the LR1110 misses the first SPI command on a
  cold start

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.