feat: populate MyNodeInfo.device_id on all platforms (#10995)

* feat: populate MyNodeInfo.device_id on all platforms

RP2040/RP2350 use the 64-bit pico unique board id, STM32WL the 96-bit
silicon UID, ESP32-S2 joins the existing OPTIONAL_UNIQUE_ID efuse branch,
and everything else (classic ESP32 in particular) falls back to a
deterministic factory-MAC-derived id, resolving the long-standing FIXME.
Portduino keeps the config-supplied id preferred and now uses the MAC
fallback when the config omits one.

No proto or persistence changes; the id is re-read from silicon each
boot and PhoneAPI still zeroes it for unauthenticated clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: declare zero_mac const to satisfy cppcheck

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address review feedback on device_id derivation

Clear any disk-loaded device_id before the silicon derivation so a failed
read leaves it unset rather than stale (Copilot), and size the portduino
config copy with sizeof instead of a literal 16 (CodeRabbit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: extract device_id generation into per-arch getDeviceId()

Per review feedback on #10995: move the platform-specific
MyNodeInfo.device_id derivation out of the #if/#elif ladder in
NodeDB.cpp into a getDeviceId() interface (target_specific.h)
implemented per-architecture alongside each platform's getMacAddr():

  - esp32:     efuse OPTIONAL_UNIQUE_ID (C3/S2/S3/C6); classic ESP32 -> MAC
  - nrf52:     FICR DEVICEID + DEVICEADDR
  - nrf54l15:  FICR->INFO.DEVICEID + DEVICEADDR (NRF_FICR-guarded, MAC fallback)
  - rp2xx0:    pico_get_unique_board_id()
  - stm32wl:   HAL_GetUIDw0/1/2()
  - portduino: config-supplied id preferred, else MAC

The shared MAC-derived fallback moves to meshUtils as
getMacAddrDeviceId(). NodeDB.cpp now zero-inits the field and makes a
single getDeviceId() call, dropping ~65 lines of platform boilerplate
plus the esp_efuse/pico/stm32 includes that came with it. No behavior
change: device_id is still re-read from silicon each boot and never
persisted.

Builds green: native-macos, tbeam, heltec-v3, rak4631, rak11310, rak3172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address review of device_id refactor

- getMacAddrDeviceId(): zero-init the mac[6] buffer. getMacAddr() can return
  without writing (e.g. Portduino with no MAC source), so the old uninitialized
  buffer let stack garbage pass the all-zeros guard and become device_id. The
  pre-refactor code relied on the zero-initialized static ourMacAddr; restore
  that guarantee.
- nrf54l15 getDeviceId(): drop the `#if defined(NRF_FICR)` guard and read FICR
  unconditionally (as the pre-refactor NodeDB code did). The guarded #else fell
  back to getMacAddr()'s hard-coded placeholder MAC, which would give every unit
  an identical device_id; a missing NRF_FICR should be a loud compile error.
- NodeDB.cpp: `#include "target_specific.h"` instead of hand-copied externs for
  getMacAddr/getDeviceId; retire the stale FIXME. Same for meshUtils.cpp (whose
  extern comment wrongly claimed the TU was Arduino-free).
- Delete the orphaned commented-out device_id hex-dump block in NodeDB.cpp.
- Trim/de-duplicate the getDeviceId contract comments (single-sourced in
  target_specific.h).

Builds green: native-macos, tbeam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: trim device_id comments to the repo's 1-2 line guideline

Addresses CodeRabbit review nitpick on #10995: shorten the getDeviceId()
(target_specific.h), getMacAddrDeviceId() (meshUtils.h), device-id refresh
(NodeDB.cpp), and nrf54l15 getDeviceId comments to two lines each, per the
"one or two lines maximum" coding guideline. Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-07-13 16:28:25 -05:00
committed by GitHub
co-authored by GitHub Claude Fable 5
parent fe288a70e4
commit 269b974e96
10 changed files with 106 additions and 53 deletions
+8 -52
View File
@@ -27,6 +27,7 @@
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
#include "meshUtils.h"
#include "modules/NeighborInfoModule.h"
#include "target_specific.h"
#if HAS_VARIABLE_HOPS
#include "modules/HopScalingModule.h"
#endif
@@ -50,11 +51,7 @@
#include "SPILock.h"
#include "modules/StoreForwardModule.h"
#include <Preferences.h>
#include <esp_efuse.h>
#include <esp_efuse_table.h>
#include <nvs_flash.h>
#include <soc/efuse_reg.h>
#include <soc/soc.h>
#endif
#ifdef ARCH_PORTDUINO
@@ -372,8 +369,7 @@ void NodeDB::disarmNodeDatabaseDecodeTargets()
*/
uint32_t radioGeneration;
// FIXME - move this somewhere else
extern void getMacAddr(uint8_t *dmac);
// getMacAddr() and getDeviceId() are the per-architecture hooks declared in target_specific.h.
/**
*
@@ -410,53 +406,13 @@ NodeDB::NodeDB()
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
int saveWhat = 0;
// Get device unique id
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
uint32_t unique_id[4];
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8);
if (err == ESP_OK) {
memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id));
myNodeInfo.device_id.size = 16;
} else {
LOG_WARN("Failed to read unique id from efuse");
// Re-read the device id from silicon each boot via the per-arch getDeviceId(); clear the
// disk-loaded value first so a failed/empty derivation leaves it unset rather than stale.
myNodeInfo.device_id.size = 0;
memset(myNodeInfo.device_id.bytes, 0, sizeof(myNodeInfo.device_id.bytes));
if (getDeviceId(myNodeInfo.device_id.bytes)) {
myNodeInfo.device_id.size = sizeof(myNodeInfo.device_id.bytes);
}
#elif defined(ARCH_NRF54L15)
// nRF54L15: DEVICEID is under FICR->INFO sub-struct (not top-level as on nRF52)
uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0];
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
myNodeInfo.device_id.size = 16;
#elif defined(ARCH_NRF52)
// Nordic applies a FIPS compliant Random ID to each chip at the factory
// We concatenate the device address to the Random ID to create a unique ID for now
// This will likely utilize a crypto module in the future
uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0];
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
myNodeInfo.device_id.size = 16;
// Uncomment below to print the device id
#elif ARCH_PORTDUINO
if (portduino_config.has_device_id) {
memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16);
myNodeInfo.device_id.size = 16;
}
#else
// FIXME - implement for other platforms
#endif
// if (myNodeInfo.device_id.size == 16) {
// std::string deviceIdHex;
// for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) {
// char buf[3];
// snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]);
// deviceIdHex += buf;
// }
// LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str());
// }
// likewise - we always want the app requirements to come from the running appload
myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00
+15
View File
@@ -1,4 +1,5 @@
#include "meshUtils.h"
#include "target_specific.h"
#include <string.h>
/*
@@ -79,6 +80,20 @@ bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes)
return true;
}
bool getMacAddrDeviceId(uint8_t *deviceId)
{
// Zero-initialized: getMacAddr() may return without writing (e.g. Portduino with no MAC
// source), and we want that no-write case to hit the all-zeros guard below, not read garbage.
uint8_t mac[6] = {0};
getMacAddr(mac);
if (memfll(mac, 0, sizeof(mac))) {
LOG_WARN("MAC is all zeros, leaving device_id unset");
return false;
}
memcpy(deviceId, mac, sizeof(mac));
return true;
}
bool isOneOf(int item, int count, ...)
{
va_list args;
+4
View File
@@ -49,6 +49,10 @@ void printBytes(const char *label, const uint8_t *p, size_t numbytes);
// is the memory region filled with a single character?
bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes);
// getDeviceId() fallback (see target_specific.h): copies the 6-byte factory MAC, or returns false
// on an all-zero MAC so two blank devices don't collide on an all-zero id.
bool getMacAddrDeviceId(uint8_t *deviceId);
bool isOneOf(int item, int count, ...);
const std::string vformat(const char *const zcFormat, ...);
+22
View File
@@ -27,6 +27,8 @@
#include "target_specific.h"
#include <Preferences.h>
#include <driver/rtc_io.h>
#include <esp_efuse.h>
#include <esp_efuse_table.h>
#include <nvs.h>
#include <nvs_flash.h>
@@ -158,6 +160,26 @@ void getMacAddr(uint8_t *dmac)
#endif
}
bool getDeviceId(uint8_t *deviceId)
{
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || \
defined(CONFIG_IDF_TARGET_ESP32C6)
// ESP32 factory burns a 128-bit unique id in efuse for S2+ and C3+ series (used for HMACs
// in the esp-rainmaker AIOT platform); a good stable hardware anchor for us too.
uint32_t unique_id[4];
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8);
if (err != ESP_OK) {
LOG_WARN("Failed to read unique id from efuse");
return false;
}
memcpy(deviceId, unique_id, sizeof(unique_id));
return true;
#else
// Classic ESP32 has no OPTIONAL_UNIQUE_ID efuse: fall back to the factory-burned MAC.
return getMacAddrDeviceId(deviceId);
#endif
}
#if HAS_32768HZ
#define CALIBRATE_ONE(cali_clk) calibrate_one(cali_clk, #cali_clk)
+11
View File
@@ -186,6 +186,17 @@ void getMacAddr(uint8_t *dmac)
dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack
}
bool getDeviceId(uint8_t *deviceId)
{
// Nordic burns a FIPS-compliant random id into each chip at the factory. We concatenate
// the device address to that random id to form the 16-byte hardware identifier.
uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0];
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
memcpy(deviceId, &device_id_start, sizeof(device_id_start));
memcpy(deviceId + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
return true;
}
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
void setBluetoothEnable(bool enable)
{
+12
View File
@@ -12,6 +12,7 @@
#include <SPI.h>
#include <Wire.h>
#include <assert.h>
#include <cstring>
#include <stdio.h>
#include "NodeDB.h"
@@ -97,6 +98,17 @@ void getMacAddr(uint8_t *dmac)
#endif
}
bool getDeviceId(uint8_t *deviceId)
{
// nRF54L15: DEVICEID under the FICR->INFO sub-struct. Read unconditionally so a future build
// lacking NRF_FICR fails loudly rather than silently sharing getMacAddr()'s placeholder MAC.
uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0];
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
memcpy(deviceId, &device_id_start, sizeof(device_id_start));
memcpy(deviceId + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
return true;
}
// ── Bluetooth ─────────────────────────────────────────────────────────────────
void setBluetoothEnable(bool enable)
+10
View File
@@ -201,6 +201,16 @@ void getMacAddr(uint8_t *dmac)
}
}
bool getDeviceId(uint8_t *deviceId)
{
if (portduino_config.has_device_id) {
memcpy(deviceId, portduino_config.device_id, sizeof(portduino_config.device_id));
return true;
}
// Config-supplied id stays preferred: host NIC/BT MACs can be unstable (docker, multi-NIC).
return getMacAddrDeviceId(deviceId);
}
std::string cleanupNameForAutoconf(std::string name)
{
// Convert spaces -> dashes, lowercase
+10
View File
@@ -1,6 +1,7 @@
#include "HardwareRNG.h"
#include "configuration.h"
#include "hardware/xosc.h"
#include <cstring>
#include <hardware/clocks.h>
#include <hardware/pll.h>
#include <hardware/watchdog.h>
@@ -98,6 +99,15 @@ void getMacAddr(uint8_t *dmac)
dmac[0] = src.id[2];
}
bool getDeviceId(uint8_t *deviceId)
{
// RP2040/RP2350: 64-bit unique board id (flash serial / OTP) in bytes 0-7 (rest stay zero).
pico_unique_board_id_t board_id;
pico_get_unique_board_id(&board_id);
memcpy(deviceId, board_id.id, sizeof(board_id.id));
return true;
}
void rp2040Setup()
{
if (watchdog_caused_reboot()) {
+9
View File
@@ -1,5 +1,6 @@
#include "RTC.h"
#include "configuration.h"
#include <cstring>
#include <stdarg.h>
#include <stm32wle5xx.h>
#include <stm32wlxx_hal.h>
@@ -77,6 +78,14 @@ void getMacAddr(uint8_t *dmac)
dmac[0] = (uint8_t)(uid2 >> 8);
}
bool getDeviceId(uint8_t *deviceId)
{
// STM32WL: 96-bit factory silicon UID (words w0..w2, little-endian) in bytes 0-11 (rest stay zero).
uint32_t uid[3] = {HAL_GetUIDw0(), HAL_GetUIDw1(), HAL_GetUIDw2()};
memcpy(deviceId, uid, sizeof(uid));
return true;
}
void cpuDeepSleep(uint32_t msecToWake) {}
// Hacks to force more code and data out.
+5 -1
View File
@@ -7,4 +7,8 @@
// Enable/disable bluetooth.
void setBluetoothEnable(bool enable);
void getMacAddr(uint8_t *dmac);
void getMacAddr(uint8_t *dmac);
// Fill deviceId (a caller-zeroed 16-byte buffer) with a stable silicon/factory id; return true, or
// false (buffer untouched) if none exists here. Writes only leading bytes. Per-arch: src/platform/<arch>/.
bool getDeviceId(uint8_t *deviceId);