feat(network): implement hostname configuration for automatic interfaces
This commit is contained in:
1 parent
88b57db2c6
commit
0c65fc79db
8 files changed
+258
-2
No files matched your search
@@ -4,6 +4,7 @@
|
||||
#include <stdlib.h> // for qsort()
|
||||
#include <helpers/RxReservePacketManager.h>
|
||||
#include <helpers/NetworkInterface.h>
|
||||
#include <helpers/NetworkHostname.h>
|
||||
#if defined(ESP_PLATFORM)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
@@ -1149,6 +1150,11 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
|
||||
NetworkInterface& boot_network = activeNetworkInterface();
|
||||
if (boot_network.isAutomatic()) {
|
||||
char network_hostname[NetworkHostname::kBufferSize];
|
||||
NetworkHostname::build(network_hostname, sizeof(network_hostname),
|
||||
_prefs.node_name, self_id.pub_key, PUB_KEY_SIZE);
|
||||
boot_network.setHostname(network_hostname);
|
||||
Serial.printf("Network: hostname %s\n", network_hostname);
|
||||
MQTTPrefs* obs = _cli.getObserverPrefs();
|
||||
Serial.printf("Network: probing Ethernet for up to %lums\n",
|
||||
(unsigned long)NETWORK_ETHERNET_BOOT_WAIT_MS);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <climits>
|
||||
#include <helpers/RxReservePacketManager.h>
|
||||
#include <helpers/NetworkInterface.h>
|
||||
#include <helpers/NetworkHostname.h>
|
||||
#if defined(ESP_PLATFORM)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
@@ -950,6 +951,11 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
|
||||
NetworkInterface& boot_network = activeNetworkInterface();
|
||||
if (boot_network.isAutomatic()) {
|
||||
char network_hostname[NetworkHostname::kBufferSize];
|
||||
NetworkHostname::build(network_hostname, sizeof(network_hostname),
|
||||
_prefs.node_name, self_id.pub_key, PUB_KEY_SIZE);
|
||||
boot_network.setHostname(network_hostname);
|
||||
Serial.printf("Network: hostname %s\n", network_hostname);
|
||||
MQTTPrefs* obs = _cli.getObserverPrefs();
|
||||
Serial.printf("Network: probing Ethernet for up to %lums\n",
|
||||
(unsigned long)NETWORK_ETHERNET_BOOT_WAIT_MS);
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace NetworkHostname {
|
||||
|
||||
// ESP-IDF documents a 32-byte hostname limit. Keep one byte for the trailing
|
||||
// NUL so the same buffer is safe through Arduino and lwIP APIs.
|
||||
static constexpr size_t kMaxLength = 31;
|
||||
static constexpr size_t kBufferSize = kMaxLength + 1;
|
||||
|
||||
static inline bool isAsciiAlphaNumeric(uint8_t ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= '0' && ch <= '9');
|
||||
}
|
||||
|
||||
static inline char asciiLower(uint8_t ch) {
|
||||
return ch >= 'A' && ch <= 'Z' ? static_cast<char>(ch + ('a' - 'A'))
|
||||
: static_cast<char>(ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DHCP-safe hostname from the MeshCore node name.
|
||||
*
|
||||
* The result is lowercase, starts with "meshcore-", contains only letters,
|
||||
* digits and hyphens, and never exceeds ESP-IDF's practical 31-character
|
||||
* payload limit. If the readable name must be shortened, six hex digits from
|
||||
* the stable node identity are retained to reduce truncation collisions.
|
||||
*/
|
||||
static inline bool build(char* dest, size_t dest_size, const char* node_name,
|
||||
const uint8_t* stable_id, size_t stable_id_size) {
|
||||
if (!dest || dest_size == 0) return false;
|
||||
dest[0] = '\0';
|
||||
|
||||
static constexpr char kPrefix[] = "meshcore-";
|
||||
static constexpr char kFallback[] = "node";
|
||||
static constexpr char kHex[] = "0123456789abcdef";
|
||||
static constexpr size_t kPrefixLength = sizeof(kPrefix) - 1;
|
||||
static constexpr size_t kSuffixLength = 7; // '-' plus six hex digits.
|
||||
|
||||
// NodePrefs::node_name currently holds at most 31 bytes. A larger scratch
|
||||
// buffer keeps this helper safe and independently testable with other input.
|
||||
char slug[64];
|
||||
size_t slug_length = 0;
|
||||
bool separator_pending = false;
|
||||
if (node_name) {
|
||||
for (size_t i = 0; node_name[i] != '\0' && slug_length < sizeof(slug) - 1;
|
||||
++i) {
|
||||
const uint8_t ch = static_cast<uint8_t>(node_name[i]);
|
||||
if (isAsciiAlphaNumeric(ch)) {
|
||||
if (separator_pending && slug_length > 0 &&
|
||||
slug_length < sizeof(slug) - 1) {
|
||||
slug[slug_length++] = '-';
|
||||
}
|
||||
if (slug_length < sizeof(slug) - 1) {
|
||||
slug[slug_length++] = asciiLower(ch);
|
||||
}
|
||||
separator_pending = false;
|
||||
} else if (slug_length > 0) {
|
||||
separator_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (slug_length == 0) {
|
||||
for (size_t i = 0; i < sizeof(kFallback) - 1; ++i) {
|
||||
slug[slug_length++] = kFallback[i];
|
||||
}
|
||||
}
|
||||
slug[slug_length] = '\0';
|
||||
|
||||
size_t max_length = dest_size - 1;
|
||||
if (max_length > kMaxLength) max_length = kMaxLength;
|
||||
if (max_length == 0) return false;
|
||||
|
||||
const bool needs_truncation = kPrefixLength + slug_length > max_length;
|
||||
const bool can_add_identity = needs_truncation && stable_id &&
|
||||
stable_id_size >= 3 && max_length > kPrefixLength + kSuffixLength;
|
||||
const size_t suffix_length = can_add_identity ? kSuffixLength : 0;
|
||||
const size_t prefix_length = kPrefixLength < max_length
|
||||
? kPrefixLength : max_length;
|
||||
size_t slug_budget = max_length - prefix_length - suffix_length;
|
||||
if (slug_budget > slug_length) slug_budget = slug_length;
|
||||
while (slug_budget > 0 && slug[slug_budget - 1] == '-') --slug_budget;
|
||||
|
||||
size_t out = 0;
|
||||
for (size_t i = 0; i < prefix_length; ++i) dest[out++] = kPrefix[i];
|
||||
for (size_t i = 0; i < slug_budget; ++i) dest[out++] = slug[i];
|
||||
if (can_add_identity) {
|
||||
dest[out++] = '-';
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
dest[out++] = kHex[(stable_id[i] >> 4) & 0x0f];
|
||||
dest[out++] = kHex[stable_id[i] & 0x0f];
|
||||
}
|
||||
}
|
||||
dest[out] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace NetworkHostname
|
||||
@@ -77,6 +77,7 @@ class NetworkInterfaceBase : public NetworkInterface {
|
||||
|
||||
class WiFiNetworkInterface final : public NetworkInterfaceBase {
|
||||
bool _event_registered = false;
|
||||
char _hostname[32] = {};
|
||||
char _ssid[33] = {};
|
||||
char _password[65] = {};
|
||||
unsigned long _last_reconnect_attempt = 0;
|
||||
@@ -112,6 +113,11 @@ class WiFiNetworkInterface final : public NetworkInterfaceBase {
|
||||
return wifi_ssid && wifi_ssid[0] != '\0';
|
||||
}
|
||||
|
||||
void setHostname(const char* hostname) override {
|
||||
strncpy(_hostname, hostname ? hostname : "", sizeof(_hostname) - 1);
|
||||
_hostname[sizeof(_hostname) - 1] = '\0';
|
||||
}
|
||||
|
||||
bool begin(const char* wifi_ssid, const char* wifi_password) override {
|
||||
if (!configValid(wifi_ssid)) return false;
|
||||
strncpy(_ssid, wifi_ssid, sizeof(_ssid) - 1);
|
||||
@@ -119,6 +125,9 @@ class WiFiNetworkInterface final : public NetworkInterfaceBase {
|
||||
strncpy(_password, wifi_password ? wifi_password : "", sizeof(_password) - 1);
|
||||
_password[sizeof(_password) - 1] = '\0';
|
||||
|
||||
// Arduino-ESP32 applies this stored value when it creates the STA netif.
|
||||
// It must be set before WiFi.mode()/begin() for the first DHCP exchange.
|
||||
if (_hostname[0] != '\0') WiFi.setHostname(_hostname);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.setAutoConnect(true);
|
||||
@@ -229,6 +238,7 @@ class EthernetNetworkInterface final : public NetworkInterfaceBase {
|
||||
private:
|
||||
bool _started = false;
|
||||
bool _event_registered = false;
|
||||
char _hostname[32] = {};
|
||||
std::atomic<uint8_t> _event_state{static_cast<uint8_t>(EventState::None)};
|
||||
|
||||
public:
|
||||
@@ -240,6 +250,11 @@ class EthernetNetworkInterface final : public NetworkInterfaceBase {
|
||||
int statusCode() const override { return isConnected() ? 1 : 0; }
|
||||
bool configValid(const char*) const override { return true; }
|
||||
|
||||
void setHostname(const char* hostname) override {
|
||||
strncpy(_hostname, hostname ? hostname : "", sizeof(_hostname) - 1);
|
||||
_hostname[sizeof(_hostname) - 1] = '\0';
|
||||
}
|
||||
|
||||
bool begin(const char*, const char*) override {
|
||||
if (_started) return true;
|
||||
// DNS and WiFiClientSecure are transport-neutral sockets in this Arduino
|
||||
@@ -279,7 +294,7 @@ class EthernetNetworkInterface final : public NetworkInterfaceBase {
|
||||
});
|
||||
_event_registered = true;
|
||||
}
|
||||
_started = beginConfiguredCH390();
|
||||
_started = beginConfiguredCH390(_hostname);
|
||||
if (_started && isConnected()) noteConnected(millis());
|
||||
return _started;
|
||||
}
|
||||
@@ -444,6 +459,13 @@ class AutomaticNetworkInterface final : public NetworkInterface {
|
||||
}
|
||||
bool isAutomatic() const override { return true; }
|
||||
|
||||
void setHostname(const char* hostname) override {
|
||||
// Whichever medium wins now or during a later failover presents the same
|
||||
// stable DHCP identity to the LAN.
|
||||
_ethernet.setHostname(hostname);
|
||||
_wifi.setHostname(hostname);
|
||||
}
|
||||
|
||||
bool begin(const char* wifi_ssid, const char* wifi_password) override {
|
||||
rememberWifi(wifi_ssid, wifi_password);
|
||||
if (!_ethernet_started) {
|
||||
|
||||
@@ -29,6 +29,9 @@ class NetworkInterface {
|
||||
virtual const char* statusName() const = 0;
|
||||
virtual int statusCode() const = 0;
|
||||
virtual bool configValid(const char* wifi_ssid) const = 0;
|
||||
// Configure the DHCP hostname before begin()/bootstrap(). Implementations
|
||||
// retain it for any later fallback interface start.
|
||||
virtual void setHostname(const char* hostname) = 0;
|
||||
virtual bool begin(const char* wifi_ssid, const char* wifi_password) = 0;
|
||||
virtual NetworkTransition maintain(uint32_t now_ms, uint8_t wifi_power_save) = 0;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Shared by the companion transport wrapper and the observer network adapter so
|
||||
* pin and static-IP behavior cannot drift between the two paths.
|
||||
*/
|
||||
static inline bool beginConfiguredCH390() {
|
||||
static inline bool beginConfiguredCH390(const char* hostname = nullptr) {
|
||||
ch390_config_t config = CH390_DEFAULT_CONFIG();
|
||||
config.spi_miso_gpio = ETH_MISO_PIN;
|
||||
config.spi_mosi_gpio = ETH_MOSI_PIN;
|
||||
@@ -16,6 +16,28 @@ static inline bool beginConfiguredCH390() {
|
||||
config.int_gpio = ETH_INT_PIN;
|
||||
if (!CH390.begin(config)) return false;
|
||||
|
||||
if (hostname && hostname[0] != '\0') {
|
||||
#if !(defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET))
|
||||
// ESP32-CH390 creates and starts its esp_netif inside begin(), so its
|
||||
// setHostname() cannot be called earlier. Stop the just-created DHCP
|
||||
// client, apply the name to that exact Ethernet netif, then start a fresh
|
||||
// DHCP transaction. This prevents a fast wired link from retaining the
|
||||
// ESP-IDF default hostname for its initial lease.
|
||||
const bool dhcp_stopped = CH390.disableDHCP();
|
||||
const bool hostname_set = CH390.setHostname(hostname);
|
||||
const bool dhcp_started = CH390.enableDHCP();
|
||||
if (!dhcp_stopped || !hostname_set || !dhcp_started) {
|
||||
Serial.printf("Network: could not fully apply Ethernet hostname %s\n",
|
||||
hostname);
|
||||
}
|
||||
#else
|
||||
if (!CH390.setHostname(hostname)) {
|
||||
Serial.printf("Network: could not apply Ethernet hostname %s\n",
|
||||
hostname);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET)
|
||||
IPAddress ip(ETHERNET_STATIC_IP);
|
||||
IPAddress gateway(ETHERNET_STATIC_GATEWAY);
|
||||
|
||||
@@ -30,6 +30,7 @@ does not reflect the GoogleTest count — run the built binary directly
|
||||
| `test_mqtt_topic_router` | `src/helpers/MQTTTopicRouter.h` | complete preset/custom topic-routing contract; MeshRank all types except raw; required identifiers; invalid inputs/slots; exact buffer boundaries |
|
||||
| `test_mqtt_connection_policy` | `src/helpers/MQTTConnectionPolicy.h` | reconnect guard/backoff/stagger and breaker transitions; stable reset; JWT lifetime/renewal policy; exact timing boundaries and 32-bit `millis()` rollover; WiFi current-outage start sticky across STA reconnect attempts |
|
||||
| `test_network_policy` | `src/helpers/NetworkPolicy.h` | transport-neutral MQTT link-transition actions and `start ota` selected-LAN versus forced/fallback SoftAP choice |
|
||||
| `test_network_hostname` | `src/helpers/NetworkHostname.h` | DHCP-safe `meshcore-` hostname generation; invalid-character collapse; empty fallback; exact length boundary; stable identity suffix on truncation |
|
||||
| `test_alert_fault_policy` | `src/helpers/AlertFaultPolicy.h` | WiFi/MQTT fault edge detector; `OutageSnapshot` (down / started_ms / initiating reason) fed to tick and `formatWifiAlert`; reason-8 reconnects change neither duration nor initiating reason; flap between status polls; down at `millis()==0`; packed 64-bit cross-task word; rate-limit floor and first-fire; 5 s poll cadence and `millis()` rollover |
|
||||
| `test_display_viewport` | `src/helpers/ui/DisplayViewport.h`, `src/helpers/ui/DisplayFrameSignature.h` | logical-to-physical portrait mapping; fractional span coverage; fitted-width conversion; preferred/fallback text scaling; stable visible-frame change detection |
|
||||
| `test_mqtt_packet_queue_policy` | `src/helpers/MQTTPacketQueuePolicy.h` | queue-full eviction; stale-disconnect flush; adaptive drain limits; bounded QoS0 retries; exact timing boundaries and 32-bit `millis()` rollover |
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "helpers/NetworkHostname.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const uint8_t kStableId[] = {0xab, 0xcd, 0x01};
|
||||
|
||||
void expectDhcpSafe(const char* hostname) {
|
||||
ASSERT_NE(nullptr, hostname);
|
||||
const size_t length = strlen(hostname);
|
||||
EXPECT_GT(length, 0u);
|
||||
EXPECT_LE(length, NetworkHostname::kMaxLength);
|
||||
EXPECT_NE('-', hostname[0]);
|
||||
EXPECT_NE('-', hostname[length - 1]);
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
const char ch = hostname[i];
|
||||
EXPECT_TRUE((ch >= 'a' && ch <= 'z') ||
|
||||
(ch >= '0' && ch <= '9') || ch == '-')
|
||||
<< "invalid byte at " << i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(NetworkHostname, PrefixesAndLowercasesReadableNodeName) {
|
||||
char hostname[NetworkHostname::kBufferSize];
|
||||
ASSERT_TRUE(NetworkHostname::build(hostname, sizeof(hostname),
|
||||
"Hill Repeater 2", kStableId,
|
||||
sizeof(kStableId)));
|
||||
EXPECT_STREQ("meshcore-hill-repeater-2", hostname);
|
||||
expectDhcpSafe(hostname);
|
||||
}
|
||||
|
||||
TEST(NetworkHostname, CollapsesInvalidRunsAndTrimsEdges) {
|
||||
char hostname[NetworkHostname::kBufferSize];
|
||||
ASSERT_TRUE(NetworkHostname::build(hostname, sizeof(hostname),
|
||||
" Adam's___Hill / Repeater! ",
|
||||
kStableId, sizeof(kStableId)));
|
||||
EXPECT_STREQ("meshcore-adam-s-hill-repeater", hostname);
|
||||
expectDhcpSafe(hostname);
|
||||
}
|
||||
|
||||
TEST(NetworkHostname, UsesDocumentedFallbackForEmptySanitizedName) {
|
||||
char hostname[NetworkHostname::kBufferSize];
|
||||
ASSERT_TRUE(NetworkHostname::build(hostname, sizeof(hostname), "___ !!!",
|
||||
kStableId, sizeof(kStableId)));
|
||||
EXPECT_STREQ("meshcore-node", hostname);
|
||||
expectDhcpSafe(hostname);
|
||||
}
|
||||
|
||||
TEST(NetworkHostname, KeepsExactBoundaryWithoutIdentitySuffix) {
|
||||
char hostname[NetworkHostname::kBufferSize];
|
||||
ASSERT_TRUE(NetworkHostname::build(hostname, sizeof(hostname),
|
||||
"abcdefghijklmnopqrstuv", kStableId,
|
||||
sizeof(kStableId)));
|
||||
EXPECT_STREQ("meshcore-abcdefghijklmnopqrstuv", hostname);
|
||||
EXPECT_EQ(NetworkHostname::kMaxLength, strlen(hostname));
|
||||
}
|
||||
|
||||
TEST(NetworkHostname, TruncatesReadablePartAndAddsStableIdentitySuffix) {
|
||||
char hostname[NetworkHostname::kBufferSize];
|
||||
ASSERT_TRUE(NetworkHostname::build(hostname, sizeof(hostname),
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
|
||||
kStableId, sizeof(kStableId)));
|
||||
EXPECT_STREQ("meshcore-abcdefghijklmno-abcd01", hostname);
|
||||
EXPECT_EQ(NetworkHostname::kMaxLength, strlen(hostname));
|
||||
expectDhcpSafe(hostname);
|
||||
}
|
||||
|
||||
TEST(NetworkHostname, IdentitySuffixSeparatesOtherwiseCollidingNames) {
|
||||
const uint8_t other_id[] = {0x12, 0x34, 0x56};
|
||||
char first[NetworkHostname::kBufferSize];
|
||||
char second[NetworkHostname::kBufferSize];
|
||||
ASSERT_TRUE(NetworkHostname::build(first, sizeof(first),
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
|
||||
kStableId, sizeof(kStableId)));
|
||||
ASSERT_TRUE(NetworkHostname::build(second, sizeof(second),
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
|
||||
other_id, sizeof(other_id)));
|
||||
EXPECT_STRNE(first, second);
|
||||
EXPECT_STREQ("meshcore-abcdefghijklmno-123456", second);
|
||||
}
|
||||
|
||||
TEST(NetworkHostname, RejectsMissingOutputBuffer) {
|
||||
EXPECT_FALSE(NetworkHostname::build(nullptr, 0, "node", kStableId,
|
||||
sizeof(kStableId)));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in new issue
Block a user