feat(mqtt): add link diagnostics and improve network logging

This commit is contained in:
agessaman
2026-09-02 21:37:30 -07:00
parent 50e2b2acba
commit cdde4bf673
11 changed files with 317 additions and 30 deletions
+8
View File
@@ -81,6 +81,7 @@ reboot
```bash
get wifi.ssid
get link.status
get link.diag
get bridge.enabled
get mqtt.rx
get mqtt.tx
@@ -547,6 +548,7 @@ These settings apply across all MQTT slots:
- `get wifi.ssid` - Get WiFi SSID
- `get wifi.pwd` - Get WiFi password
- `get link.status` - Get the selected network medium, connection status, IP, signal when available, and uptime
- `get link.diag` - Explain automatic Ethernet selection using controller initialization, link event, IP, WiFi fallback, route-lock, and reason state
- `get wifi.status` - WiFi-only compatibility alias; reports n/a when another medium is selected
- `get wifi.powersave` - Get WiFi power save mode (none/min/max)
@@ -859,6 +861,12 @@ the radio actually performs in that case.
### Connection Handling
- Automatic reconnection with exponential backoff per slot; a slot that stays down through
the full backoff ladder is retried on a slow periodic probe instead of hammering the broker
- Ethernet-preferred builds wait the full configured boot probe window for an Ethernet IP;
delayed or unavailable PHY carrier reporting does not shorten the DHCP deadline. The boot
selection log includes the measured probe duration.
- Ethernet/WiFi transitions are logged. A lost or changed route stops every started MQTT
client, including one whose disconnect callback arrived first; once a usable route returns,
route-caused backoff is cleared and one immediate reconnect is allowed
- Packets are queued while a slot is disconnected and flushed when it recovers
### Raw Radio Data Capture
+4 -2
View File
@@ -1152,10 +1152,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
MQTTPrefs* obs = _cli.getObserverPrefs();
Serial.printf("Network: probing Ethernet for up to %lums\n",
(unsigned long)NETWORK_ETHERNET_BOOT_WAIT_MS);
const uint32_t ethernet_probe_started_at = millis();
boot_network.bootstrap(obs->wifi_ssid, obs->wifi_password,
NETWORK_ETHERNET_BOOT_WAIT_MS);
Serial.printf("Network: selected %s (%s)\n", boot_network.mediumName(),
boot_network.statusName());
Serial.printf("Network: selected %s (%s) after %lums\n",
boot_network.mediumName(), boot_network.statusName(),
(unsigned long)(millis() - ethernet_probe_started_at));
}
acl.load(_fs, self_id);
+4 -2
View File
@@ -953,10 +953,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
MQTTPrefs* obs = _cli.getObserverPrefs();
Serial.printf("Network: probing Ethernet for up to %lums\n",
(unsigned long)NETWORK_ETHERNET_BOOT_WAIT_MS);
const uint32_t ethernet_probe_started_at = millis();
boot_network.bootstrap(obs->wifi_ssid, obs->wifi_password,
NETWORK_ETHERNET_BOOT_WAIT_MS);
Serial.printf("Network: selected %s (%s)\n", boot_network.mediumName(),
boot_network.statusName());
Serial.printf("Network: selected %s (%s) after %lums\n",
boot_network.mediumName(), boot_network.statusName(),
(unsigned long)(millis() - ethernet_probe_started_at));
}
acl.load(_fs, self_id);
+3
View File
@@ -500,6 +500,9 @@ GETTERS = {
"link.status": lambda c: (
"wifi: connected, IP: 192.168.1.42, RSSI: -58 dBm, Uptime: %dm"
% (int(time.time() - ST.start) // 60)),
"link.diag": lambda c: (
"why:ethernet-not-enabled selected:wifi\n"
"wifi:state:connected ip:192.168.1.42"),
"mqtt.status": lambda c: cli_mqtt_status(c),
"mqtt.presets": lambda c: "\n".join(
"%2d. %s%s" % (i + 1, n, "" if nd == "none" else " (needs %s)" % nd)
+2
View File
@@ -1038,6 +1038,8 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
} else {
strcpy(reply, _mqtt_prefs.wifi_password[0] ? "> ******** (serial only)" : "> (not set)");
}
} else if (strcmp(config, "link.diag") == 0) {
activeNetworkInterface().formatDiagnostics(reply, 160);
} else if (memcmp(config, "link.status", 11) == 0 ||
memcmp(config, "wifi.status", 11) == 0) {
NetworkInterface& network = activeNetworkInterface();
+110 -7
View File
@@ -199,12 +199,31 @@ class WiFiNetworkInterface final : public NetworkInterfaceBase {
bool resolveHost(const char* hostname, IPAddress& address) const override {
return WiFi.hostByName(hostname, address);
}
void formatDiagnostics(char* reply, size_t reply_size) const override {
snprintf(reply, reply_size,
"> why:ethernet-not-enabled selected:wifi\n"
"wifi:state:%s ip:%s",
statusName(),
localIP().toString().c_str());
}
};
#if defined(NETWORK_PREFER_ETHERNET)
class EthernetNetworkInterface final : public NetworkInterfaceBase {
public:
enum class EventState : uint8_t {
None,
Started,
LinkDown,
LinkUp,
GotIp,
Stopped,
};
private:
bool _started = false;
bool _event_registered = false;
std::atomic<uint8_t> _event_state{static_cast<uint8_t>(EventState::None)};
public:
const char* mediumName() const override { return "ethernet"; }
@@ -220,14 +239,30 @@ class EthernetNetworkInterface final : public NetworkInterfaceBase {
if (!_event_registered) {
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t) {
switch (event) {
case ARDUINO_EVENT_ETH_START:
_event_state.store(static_cast<uint8_t>(EventState::Started),
std::memory_order_relaxed);
break;
case ARDUINO_EVENT_ETH_CONNECTED:
_event_state.store(static_cast<uint8_t>(EventState::LinkUp),
std::memory_order_relaxed);
break;
case ARDUINO_EVENT_ETH_GOT_IP:
_event_state.store(static_cast<uint8_t>(EventState::GotIp),
std::memory_order_relaxed);
noteConnected(millis());
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
_event_state.store(static_cast<uint8_t>(EventState::LinkDown),
std::memory_order_relaxed);
// Ethernet has no 802.11 reason code; zero means unavailable.
noteDisconnected(millis(), 0);
_connected_at.store(0, std::memory_order_relaxed);
break;
case ARDUINO_EVENT_ETH_STOP:
_event_state.store(static_cast<uint8_t>(EventState::Stopped),
std::memory_order_relaxed);
break;
default:
break;
}
@@ -282,6 +317,49 @@ class EthernetNetworkInterface final : public NetworkInterfaceBase {
// DNS follows the selected esp_netif even though this entry point is named WiFi.
return WiFi.hostByName(hostname, address);
}
void formatDiagnostics(char* reply, size_t reply_size) const override {
snprintf(reply, reply_size, "> ethernet:%s ip=%s",
statusName(), localIP().toString().c_str());
}
EventState eventState() const {
return static_cast<EventState>(
_event_state.load(std::memory_order_relaxed));
}
bool sampleLink(bool& known) const {
known = false;
if (!_started) return false;
// IEEE 802.3 BMSR link status is latch-low. Read it twice so the second
// value is the current carrier state rather than a remembered link flap.
(void)CH390.readPHY(0x01);
const uint32_t bmsr = CH390.readPHY(0x01) & 0xffffu;
if (bmsr != 0 && bmsr != 0xffffu) {
known = true;
return (bmsr & (1u << 2)) != 0;
}
// A failed/unsupported direct PHY read can still use the driver's events.
const EventState state = eventState();
known = state == EventState::LinkDown || state == EventState::LinkUp ||
state == EventState::GotIp || state == EventState::Stopped;
return state == EventState::LinkUp || state == EventState::GotIp;
}
bool linkUp() const {
bool known = false;
return sampleLink(known);
}
const char* eventName() const {
switch (eventState()) {
case EventState::None: return "none";
case EventState::Started: return "started";
case EventState::LinkDown: return "link-down";
case EventState::LinkUp: return "link-up";
case EventState::GotIp: return "got-ip";
case EventState::Stopped: return "stopped";
}
return "unknown";
}
};
class AutomaticNetworkInterface final : public NetworkInterface {
@@ -385,13 +463,9 @@ class AutomaticNetworkInterface final : public NetworkInterface {
}
const uint32_t started_at = millis();
const uint32_t link_wait_ms = wait_ms < 1500 ? wait_ms : 1500;
while (_ethernet_started && !CH390.linkUp() &&
(uint32_t)(millis() - started_at) < link_wait_ms) {
delay(25);
}
while (_ethernet_started && CH390.linkUp() && !_ethernet.isConnected() &&
(uint32_t)(millis() - started_at) < wait_ms) {
while (NetworkPolicy::ethernetBootProbePending(
_ethernet_started, _ethernet.isConnected(),
(uint32_t)(millis() - started_at), wait_ms)) {
delay(25);
}
@@ -489,6 +563,35 @@ class AutomaticNetworkInterface final : public NetworkInterface {
return _selected != NetworkMedium::None &&
selectedInterface().resolveHost(hostname, address);
}
void formatDiagnostics(char* reply, size_t reply_size) const override {
const bool ethernet_connected = _ethernet.isConnected();
bool ethernet_link_known = false;
bool ethernet_link_up = _ethernet.sampleLink(ethernet_link_known);
ethernet_link_known = ethernet_link_known || ethernet_connected;
ethernet_link_up = ethernet_link_up || ethernet_connected;
const bool switching_locked =
_switch_locks.load(std::memory_order_relaxed) != 0;
const uint32_t now_ms = millis();
const uint32_t ethernet_stable_ms = _ethernet_stable_since == 0
? 0 : (uint32_t)(now_ms - _ethernet_stable_since);
const NetworkDiagnosticReason reason =
NetworkPolicy::automaticDiagnosticReason(
_ethernet_started, ethernet_link_known, ethernet_link_up,
ethernet_connected, _selected, switching_locked,
ethernet_stable_ms);
snprintf(reply, reply_size,
"> why:%s selected:%s lock:%s\n"
"eth:init:%s evt:%s link:%s ip:%s\n"
"wifi:cfg:%s started:%s link:%s",
NetworkPolicy::diagnosticReasonName(reason), mediumName(),
switching_locked ? "yes" : "no",
_ethernet_started ? "ok" : "failed", _ethernet.eventName(),
ethernet_link_known ? (ethernet_link_up ? "up" : "down")
: "unknown",
_ethernet.localIP().toString().c_str(),
wifiConfigured() ? "yes" : "no", _wifi_started ? "yes" : "no",
(_wifi_started && _wifi.isConnected()) ? "up" : "down");
}
unsigned long connectedAtMillis() const override {
return _selected == NetworkMedium::None ? 0 : selectedInterface().connectedAtMillis();
}
+1
View File
@@ -50,6 +50,7 @@ class NetworkInterface {
virtual IPAddress localIP() const = 0;
virtual int rssi() const = 0; // INT_MIN when the selected medium has no RSSI.
virtual bool resolveHost(const char* hostname, IPAddress& address) const = 0;
virtual void formatDiagnostics(char* reply, size_t reply_size) const = 0;
virtual unsigned long connectedAtMillis() const = 0;
virtual uint8_t lastDisconnectReason() const = 0;
+82 -1
View File
@@ -15,20 +15,39 @@ enum class NetworkMedium : uint8_t {
WiFi,
};
enum class NetworkDiagnosticReason : uint8_t {
EthernetActive,
EthernetInitFailed,
EthernetLinkUnknown,
EthernetLinkDown,
EthernetAwaitingIp,
EthernetStabilizing,
SwitchingLocked,
EthernetReady,
};
namespace NetworkPolicy {
struct MQTTTransitionActions {
bool disconnect_slots;
bool stop_started_slots;
bool retry_disconnected_slots_now;
bool reset_reconnect_backoff;
};
static constexpr MQTTTransitionActions mqttActions(NetworkTransition transition) {
return {
transition == NetworkTransition::Down || transition == NetworkTransition::Switched,
transition == NetworkTransition::Up || transition == NetworkTransition::Switched,
transition == NetworkTransition::Up || transition == NetworkTransition::Switched,
};
}
static constexpr const char* mediumName(NetworkMedium medium) {
return medium == NetworkMedium::Ethernet ? "ethernet"
: medium == NetworkMedium::WiFi ? "wifi"
: "none";
}
struct AutomaticSelectionInput {
NetworkMedium selected;
bool ethernet_connected;
@@ -46,6 +65,58 @@ static constexpr uint32_t kEthernetDownGraceMs = 3000;
static constexpr uint32_t kEthernetFailbackStableMs = 10000;
static constexpr uint32_t kNtpRetryMs = 30000;
static inline NetworkDiagnosticReason automaticDiagnosticReason(
bool ethernet_initialized, bool ethernet_link_known,
bool ethernet_link_up, bool ethernet_connected, NetworkMedium selected,
bool switching_locked, uint32_t ethernet_stable_ms) {
if (!ethernet_initialized) {
return NetworkDiagnosticReason::EthernetInitFailed;
}
if (!ethernet_link_known) {
return NetworkDiagnosticReason::EthernetLinkUnknown;
}
if (!ethernet_link_up) {
return NetworkDiagnosticReason::EthernetLinkDown;
}
if (!ethernet_connected) {
return NetworkDiagnosticReason::EthernetAwaitingIp;
}
if (selected == NetworkMedium::Ethernet) {
return NetworkDiagnosticReason::EthernetActive;
}
if (switching_locked) {
return NetworkDiagnosticReason::SwitchingLocked;
}
if (selected == NetworkMedium::WiFi &&
ethernet_stable_ms < kEthernetFailbackStableMs) {
return NetworkDiagnosticReason::EthernetStabilizing;
}
return NetworkDiagnosticReason::EthernetReady;
}
static inline const char* diagnosticReasonName(
NetworkDiagnosticReason reason) {
switch (reason) {
case NetworkDiagnosticReason::EthernetActive:
return "ethernet-active";
case NetworkDiagnosticReason::EthernetInitFailed:
return "ethernet-init-failed";
case NetworkDiagnosticReason::EthernetLinkUnknown:
return "ethernet-link-unknown";
case NetworkDiagnosticReason::EthernetLinkDown:
return "ethernet-link-down";
case NetworkDiagnosticReason::EthernetAwaitingIp:
return "ethernet-awaiting-ip";
case NetworkDiagnosticReason::EthernetStabilizing:
return "ethernet-stabilizing";
case NetworkDiagnosticReason::SwitchingLocked:
return "switching-locked";
case NetworkDiagnosticReason::EthernetReady:
return "ethernet-ready";
}
return "unknown";
}
static constexpr bool ntpPendingAfterConnectivitySample(
bool synced, bool pending, bool was_connected, bool connected) {
return pending || (!synced && connected && !was_connected);
@@ -64,6 +135,16 @@ static constexpr NetworkMedium bootSelection(bool ethernet_connected,
: NetworkMedium::None;
}
// Once the Ethernet controller has initialized, allow the entire boot probe
// window for link negotiation and DHCP. PHY carrier is useful diagnostic data,
// but it must not shorten the advertised deadline when carrier reporting lags.
static constexpr bool ethernetBootProbePending(bool ethernet_initialized,
bool ethernet_connected,
uint32_t elapsed_ms,
uint32_t wait_ms) {
return ethernet_initialized && !ethernet_connected && elapsed_ms < wait_ms;
}
static inline NetworkMedium automaticSelection(
const AutomaticSelectionInput& input) {
if (input.switching_locked) return input.selected;
+44 -14
View File
@@ -1307,16 +1307,7 @@ void MQTTBridge::mqttTaskLoop() {
}
#endif
bool network_just_connected = handleNetworkConnection(now);
if (network_just_connected) {
// The uplink recovered — reset last_reconnect_attempt for disconnected slots so they
// retry immediately rather than waiting up to 5 min for backoff timers to expire.
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_slots[i].enabled && _slots[i].initial_connect_done && !_slots[i].connected) {
_slots[i].last_reconnect_attempt = 0;
}
}
}
handleNetworkConnection(now);
// Schedule once per link-up edge. Failed syncs are owned by the 30-second
// retry below; re-arming here on every loop would make a blocked NTP path
@@ -2745,19 +2736,58 @@ void MQTTBridge::checkConfigurationMismatch() {
}
bool MQTTBridge::handleNetworkConnection(unsigned long now) {
const NetworkMedium previous_medium = _network->medium();
const NetworkTransition transition =
_network->maintain((uint32_t)now, _obs->wifi_power_save);
const NetworkMedium selected_medium = _network->medium();
if (transition == NetworkTransition::Down) {
MQTT_DEBUG_PRINTLN("Network: %s link down", NetworkPolicy::mediumName(previous_medium));
} else if (transition == NetworkTransition::Up) {
MQTT_DEBUG_PRINTLN("Network: %s link up (%s, IP %s)",
NetworkPolicy::mediumName(selected_medium),
_network->statusName(), _network->localIP().toString().c_str());
} else if (transition == NetworkTransition::Switched) {
MQTT_DEBUG_PRINTLN("Network: switched %s -> %s (%s, IP %s)",
NetworkPolicy::mediumName(previous_medium),
NetworkPolicy::mediumName(selected_medium),
_network->statusName(), _network->localIP().toString().c_str());
}
const NetworkPolicy::MQTTTransitionActions actions =
NetworkPolicy::mqttActions(transition);
if (actions.disconnect_slots) {
if (actions.stop_started_slots) {
// Broker ownership stays in the bridge. The physical adapter reports the
// edge; the bridge explicitly closes every slot instead of waiting for
// eventual socket timeouts.
// edge; the bridge explicitly stops every started slot instead of waiting
// for eventual socket timeouts. Do not gate this on slot.connected: the
// ESP-MQTT disconnect callback can clear that flag before the network edge
// reaches this task, but its client task and transport can still be alive.
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_slots[i].client && _slots[i].connected) {
if (_slots[i].client && _slots[i].client->isStarted()) {
MQTT_DEBUG_PRINTLN("MQTT%d stopping for network transition", i + 1);
_slots[i].client->disconnect();
}
_slots[i].connected = false;
_slots[i].connected_at_ms = 0;
}
updateCachedConnectionStatus();
}
if (actions.reset_reconnect_backoff) {
// A usable route is a new connection epoch. Failures earned on the old
// route must not strand the replacement route on the 5-minute rung or at
// the circuit breaker. Preserve JWTs and slot configuration, but give each
// disconnected active slot one immediate, freshly guarded attempt.
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_slots[i].enabled && _slots[i].initial_connect_done && !_slots[i].connected) {
_slots[i].reconnect_backoff = 0;
_slots[i].max_backoff_failures = 0;
_slots[i].circuit_breaker_tripped = false;
_slots[i].last_reconnect_attempt =
now - MQTTConnectionPolicy::reconnectDelayMs(0, static_cast<uint8_t>(i));
}
}
_last_slot_reconnect_ms = now - MQTTConnectionPolicy::kReconnectGuardMs;
}
return actions.retry_disconnected_slots_now;
}
@@ -4,26 +4,36 @@
TEST(NetworkPolicy, MqttDownDisconnectsSlotsWithoutRequestingImmediateRetry) {
const auto actions = NetworkPolicy::mqttActions(NetworkTransition::Down);
EXPECT_TRUE(actions.disconnect_slots);
EXPECT_TRUE(actions.stop_started_slots);
EXPECT_FALSE(actions.retry_disconnected_slots_now);
EXPECT_FALSE(actions.reset_reconnect_backoff);
}
TEST(NetworkPolicy, MqttUpRequestsImmediateRetryWithoutDisconnectingSlots) {
const auto actions = NetworkPolicy::mqttActions(NetworkTransition::Up);
EXPECT_FALSE(actions.disconnect_slots);
EXPECT_FALSE(actions.stop_started_slots);
EXPECT_TRUE(actions.retry_disconnected_slots_now);
EXPECT_TRUE(actions.reset_reconnect_backoff);
}
TEST(NetworkPolicy, NoTransitionHasNoMqttSideEffects) {
const auto actions = NetworkPolicy::mqttActions(NetworkTransition::None);
EXPECT_FALSE(actions.disconnect_slots);
EXPECT_FALSE(actions.stop_started_slots);
EXPECT_FALSE(actions.retry_disconnected_slots_now);
EXPECT_FALSE(actions.reset_reconnect_backoff);
}
TEST(NetworkPolicy, LinkSwitchReconnectsMqttSlotsImmediately) {
const auto actions = NetworkPolicy::mqttActions(NetworkTransition::Switched);
EXPECT_TRUE(actions.disconnect_slots);
EXPECT_TRUE(actions.stop_started_slots);
EXPECT_TRUE(actions.retry_disconnected_slots_now);
EXPECT_TRUE(actions.reset_reconnect_backoff);
}
TEST(NetworkPolicy, MediumNamesAreStableForTransitionLogs) {
EXPECT_STREQ("none", NetworkPolicy::mediumName(NetworkMedium::None));
EXPECT_STREQ("ethernet", NetworkPolicy::mediumName(NetworkMedium::Ethernet));
EXPECT_STREQ("wifi", NetworkPolicy::mediumName(NetworkMedium::WiFi));
}
TEST(NetworkPolicy, BootPrefersEthernetAndOtherwiseUsesConfiguredWifi) {
@@ -32,6 +42,15 @@ TEST(NetworkPolicy, BootPrefersEthernetAndOtherwiseUsesConfiguredWifi) {
EXPECT_EQ(NetworkMedium::None, NetworkPolicy::bootSelection(false, false));
}
TEST(NetworkPolicy, EthernetBootProbeHonorsFullDeadlineUntilConnected) {
EXPECT_TRUE(NetworkPolicy::ethernetBootProbePending(true, false, 0, 8000));
EXPECT_TRUE(NetworkPolicy::ethernetBootProbePending(true, false, 1500, 8000));
EXPECT_TRUE(NetworkPolicy::ethernetBootProbePending(true, false, 7999, 8000));
EXPECT_FALSE(NetworkPolicy::ethernetBootProbePending(true, false, 8000, 8000));
EXPECT_FALSE(NetworkPolicy::ethernetBootProbePending(true, true, 100, 8000));
EXPECT_FALSE(NetworkPolicy::ethernetBootProbePending(false, false, 100, 8000));
}
TEST(NetworkPolicy, EthernetFailureWaitsForGraceAndConnectedWifi) {
NetworkPolicy::AutomaticSelectionInput input = {
NetworkMedium::Ethernet, false, true, true, false, 0,
@@ -57,6 +76,41 @@ TEST(NetworkPolicy, SwitchingLockPinsTheCurrentMedium) {
EXPECT_EQ(NetworkMedium::WiFi, NetworkPolicy::automaticSelection(input));
}
TEST(NetworkPolicy, DiagnosticsIdentifyEthernetFallbackCause) {
using Reason = NetworkDiagnosticReason;
EXPECT_EQ(Reason::EthernetInitFailed,
NetworkPolicy::automaticDiagnosticReason(
false, false, false, false, NetworkMedium::WiFi, false, 0));
EXPECT_EQ(Reason::EthernetLinkUnknown,
NetworkPolicy::automaticDiagnosticReason(
true, false, false, false, NetworkMedium::WiFi, false, 0));
EXPECT_EQ(Reason::EthernetLinkDown,
NetworkPolicy::automaticDiagnosticReason(
true, true, false, false, NetworkMedium::WiFi, false, 0));
EXPECT_EQ(Reason::EthernetAwaitingIp,
NetworkPolicy::automaticDiagnosticReason(
true, true, true, false, NetworkMedium::WiFi, false, 0));
}
TEST(NetworkPolicy, DiagnosticsExplainWhyReadyEthernetHasNotBeenSelected) {
using Reason = NetworkDiagnosticReason;
EXPECT_EQ(Reason::SwitchingLocked,
NetworkPolicy::automaticDiagnosticReason(
true, true, true, true, NetworkMedium::WiFi, true,
NetworkPolicy::kEthernetFailbackStableMs));
EXPECT_EQ(Reason::EthernetStabilizing,
NetworkPolicy::automaticDiagnosticReason(
true, true, true, true, NetworkMedium::WiFi, false,
NetworkPolicy::kEthernetFailbackStableMs - 1));
EXPECT_EQ(Reason::EthernetReady,
NetworkPolicy::automaticDiagnosticReason(
true, true, true, true, NetworkMedium::WiFi, false,
NetworkPolicy::kEthernetFailbackStableMs));
EXPECT_EQ(Reason::EthernetActive,
NetworkPolicy::automaticDiagnosticReason(
true, true, true, true, NetworkMedium::Ethernet, false, 0));
}
TEST(NetworkPolicy, NtpIsScheduledOncePerConnectivityEdge) {
EXPECT_TRUE(NetworkPolicy::ntpPendingAfterConnectivitySample(
false, false, false, true));
+1
View File
@@ -1547,6 +1547,7 @@ var CLI_KEYS=[
["wifi.pwd","WiFi password",0],
["wifi.powersave","WiFi power-save mode",0,"none|min|max"],
["link.status","Selected network, IP, signal and uptime",1],
["link.diag","Ethernet selection and fallback diagnostics",1],
["wifi.status","WiFi connection, IP, RSSI and uptime",1],
["mqtt.origin","Observer name in published messages",0],
["mqtt.iata","IATA region code used in topic paths",0],