Elecrow ThinkNode M7
The M7 is an ESP32-S3 + LR1110 board with a 100 Mbps RJ45 (WCH CH390 SPI Ethernet
controller) and optional PoE. variants/thinknode_m7/ and boards/thinknode_m7.json
arrived with the upstream 1.17.0 merge, but the board is not in meshcomod's release matrix,
so there is no prebuilt binary and no web-flasher entry (see CI releases).
It has to be built from source.
Environments
*_companion_radio_usb_tcp (meshcomod's USB + TCP + WebSocket companion) does not exist for
this board, so these are upstream's environments.
| Env | Transports | Notes |
|---|---|---|
ThinkNode_M7_companion_radio_ethernet |
USB + Ethernet | The PoE wired node. Companion TCP on port 5000 over the wire, USB for config. |
ThinkNode_M7_companion_radio_ble |
BLE | Ethernet flags are set but unused, since BLE takes precedence. See "Remaining gaps". |
ThinkNode_M7_companion_radio_usb |
USB | Simplest bring-up target. |
ThinkNode_M7_companion_radio_wifi |
Wi-Fi (TCP 5000) | SSID/password hardcoded in the variant's platformio.ini. |
ThinkNode_M7_kiss_modem |
USB | KISS TNC. |
ThinkNode_M7_repeater |
none | Does not build. See "Remaining gaps". |
Building
export FIRMWARE_VERSION=v1.17.0
MERGE_BIN=1 bash build.sh build-firmware ThinkNode_M7_companion_radio_ethernet
MERGE_BIN=1 is needed because build.sh only produces a merged image automatically for env
names in the release matrix, and no M7 env matches. Without it you get the app image only.
| Output | Flash at | Contents |
|---|---|---|
out/<env>-<version>-<sha>-merged.bin |
0x0 |
bootloader + partition table + app |
out/<env>-<version>-<sha>.bin |
0x10000 |
app only, keeps partition table and SPIFFS |
Flash with esptool-js in Chrome, or:
esptool --chip esp32s3 --port <PORT> --baud 921600 write-flash 0x0 out/<file>-merged.bin
The board enumerates as a WCH CH343 bridge (1a86:7522), not Espressif native USB, so <PORT>
is typically /dev/ttyUSB0 on Linux (ch341 driver) or COMx on Windows, not /dev/ttyACM0.
Note also that
boards/thinknode_m7.json still declares hwids of 0x303A:0x1001, which breaks PlatformIO's
automatic port detection. Serial console is 115200. There is no reset button, and the auto-reset
circuit works, so pulse DTR/RTS (or power cycle) to reset.
A merged image spans 0x0 to ~0x131000, which covers NVS at 0x9000 but not SPIFFS at
0x670000. Node preferences live in /prefs.json in SPIFFS, so radio settings survive
reflashing.
Radio region
[arduino_base] in platformio.ini compiles in EU defaults (869.618 MHz / 62.5 kHz / SF8).
These seed the factory defaults only: companion firmware copies them into _prefs on first
boot and uses stored preferences thereafter, and a client app will typically overwrite them
during setup. For the headless roles (repeater, room server, KISS modem) there is no app, so
the compiled values are the operating configuration. Set them explicitly when building those
for a non-EU region.
Hardware validation
Verified on a real ThinkNode M7 (this is the first known instance of meshcomod running on the board):
- Boots, reporting
[BOOT] board okand[BOOT] radio ok, so the LR1110 initializes. - BLE companion pairs from the MeshCore client.
- USB companion connects from the web client.
- Ethernet: link, DHCP lease,
ETH: listening on TCP port: 5000, USB live simultaneously. - Home Assistant connects over the Ethernet TCP transport and reads telemetry.
- Both LEDs work. Green shows the firmware heartbeat, a 20 ms blip every 4 s that lengthens to 200 ms while messages are unread. Blue follows LoRa transmit.
- The compiled radio defaults seed a fresh device. After the store was cleared the node came up on 927.875 MHz, 62.5 kHz, SF7, CR5 with no client involvement.
- DHCP-to-DNS registration works. A rename propagated to DNS with no measurable delay, on the same MAC and the same lease.
Remaining gaps
ThinkNode_M7_repeaterdoes not compile.examples/simple_repeater/UITask.cppusesDisplayDriver::BLUEandDisplayDriver::LIGHT, but thatenum Coloris commented out atsrc/helpers/ui/DisplayDriver.h:20, superseded by theUIColorstatics. This breaks the plain*_repeaterenvs on every board, reproduced identically onStation_G3_ESP32_repeater, so it is not M7-specific and wants its own fix.ThinkNode_M7_companion_radio_blehas unused Ethernet. The env sets bothBLE_PIN_CODEand the CH390 flags, but the companion registers only BLE.MultiSerialInterfacesupports registering BLE, USB and Ethernet together, and wiring that up would give the board every transport at once.examples/companion_radio/ui-tiny/UITask.hstill declares its constructor as takingMultiSerialInterface*, which nothing constructs on nRF52.LilyGo_T-Echo_Card_companion_radio_bleand_usbcarry the same break that this change fixes forui-orig.boards/thinknode_m7.jsonhwidsdo not match the hardware (see above).- The ethernet MAC is locally administered. This platform builds with
CONFIG_ESP32S3_UNIVERSAL_MAC_ADDRESSES=2, so the ESP-IDF derives the ethernet address from the Bluetooth address with the locally administered bit set. From an illustrative base of00:00:5E:00:53:00that gives Bluetooth00:00:5E:00:53:01and ethernet02:00:5E:00:53:01, which differs from the base in both the first and the last octet and carries no vendor OUI. ESPHome on the same board reports00:00:5E:00:53:03, base+3, because it builds with four universal addresses and assigns the ethernet address explicitly withesp_read_mac(ESP_MAC_ETH)andesp_eth_ioctl(ETH_CMD_S_MAC_ADDR)before attaching the netif. Matching that needs the universal address count changed, sinceesp_read_mac(ESP_MAC_ETH)returns the derived value here and writing it back is a no-op. Changing it moves the address of every deployed unit, discarding whatever the current one is bound to, so it is left alone. - A mains powered device reports a battery.
ESP32Board::getBattMilliVolts()returns 0 unlessPIN_VBAT_READis defined, and the M7 defines no such pin because the board is powered over PoE or USB and carries no cell. Zero is indistinguishable from a flat battery, so every client presents the node as critically discharged. The protocol has no way to say a device has no battery, which is where the fix belongs: a device with no cell should report absence rather than zero, and a board should be able to declare that it is externally powered. - The Home Assistant integration exposes battery entities for a device with no battery. It creates Battery Percentage and Battery Voltage from that zero, so the node shows 0 percent with a low-battery icon and will drive any automation or alert keyed on battery level. Until the protocol can express absence, those entities should be omitted or reported unavailable for a device that reports no cell rather than published as a real reading.
- The meshcomod web client mis-parses the device info frame, and the offset is known. Byte 2
of
RESP_CODE_DEVICE_INFOisMAX_CONTACTS / 2, 175, which is invalid UTF-8, and byte 3 isMAX_GROUP_CHANNELS, 40. Those are what the mojibake and the stray bracket are. Bytes 8 to 19 hold the build date and bytes 20 on the manufacturer name, which is why they appear run together. The client starts reading at offset 2 rather than offset 8, skipping the two byte header but neither the two v3+ count bytes nor the four byte BLE pin, and does not stop at the null separators. The same client readsDevice modelandFirmwarecorrectly from that frame, and Home Assistant renders it correctly, so the frame is well formed andStrHelper::strzcpypads every field. The fix belongs in that client. - The same client reports a battery voltage the firmware never sent. It has shown 7.04 V, 2.56 V and 0.000 V for a board with no cell, where Home Assistant renders 0.000 V from the same device. The firmware reports zero, so the value is invented by that client's parsing and the fix belongs with the frame offset above.