diff --git a/data-src/index.js b/data-src/index.js
index c1b65c3..6dd60fd 100644
--- a/data-src/index.js
+++ b/data-src/index.js
@@ -1750,7 +1750,8 @@ class Wifi {
{ val: 1, label: 'WT32-ETH01 - Wireless Tag', clk: 0, ct: 0, addr: 1, pwr: 16, mdc: 23, mdio: 18 }
];
ethClockModes = [{ val: 0, label: 'GPIO0 IN' }, { val: 1, label: 'GPIO0 OUT' }, { val: 2, label: 'GPIO16 OUT' }, { val: 3, label: 'GPIO17 OUT' }];
- ethPhyTypes = [{ val: 0, label: 'LAN8720' }, { val: 1, label: 'TLK110' }, { val: 2, label: 'RTL8201' }, { val: 3, label: 'DP83848' }, { val: 4, label: 'DM9051' }, { val: 5, label: 'KZ8081' }];
+ // The ids the firmware stores (eth_phy_id_t in EthPhy.h). 4 is the DM9051, an SPI chip the ESP32's MAC can't drive.
+ ethPhyTypes = [{ val: 0, label: 'LAN8720' }, { val: 1, label: 'TLK110' }, { val: 2, label: 'RTL8201' }, { val: 3, label: 'DP83848' }, { val: 5, label: 'KSZ8041' }, { val: 6, label: 'KSZ8081' }];
init() {
document.getElementById("divNetworkStrength").innerHTML = this.displaySignal(-100);
if (this.initialized) return;
@@ -1811,8 +1812,11 @@ class Wifi {
ui.serviceError(err);
}
else {
- document.getElementById('cbHardwired').checked = settings.connType >= 2;
- document.getElementById('cbFallbackWireless').checked = settings.connType === 3;
+ // Firmware older than hasEthernet always built Ethernet in.
+ let hasEthernet = settings.hasEthernet !== false;
+ document.getElementById('divHardwired').style.display = hasEthernet ? '' : 'none';
+ document.getElementById('cbHardwired').checked = hasEthernet && settings.connType >= 2;
+ document.getElementById('cbFallbackWireless').checked = hasEthernet && settings.connType === 3;
ui.toElement(pnl, settings);
// The device never returns the stored passphrase; leave the field blank to keep it.
this.savedSsid = settings.wifi.ssid;
diff --git a/src/ConfigFile.cpp b/src/ConfigFile.cpp
index 468b73f..2d606e4 100644
--- a/src/ConfigFile.cpp
+++ b/src/ConfigFile.cpp
@@ -621,7 +621,7 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename,
if(opts.network) {
settings.IP.save();
settings.WIFI.save();
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
settings.Ethernet.save();
#endif
}
@@ -679,9 +679,9 @@ bool ShadeConfigFile::readNetRecord(restore_options_t &opts) {
if(opts.network) {
if(strncmp(settings.serverId, this->header.serverId, sizeof(settings.serverId)) == 0) {
ESP_LOGI(TAG, "Restoring Ethernet adapter settings");
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
settings.Ethernet.boardType = this->readUInt8(1);
- settings.Ethernet.phyType = static_cast
(this->readUInt8(0));
+ settings.Ethernet.phyType = this->readUInt8(0);
settings.Ethernet.CLKMode = static_cast(this->readUInt8(0));
settings.Ethernet.phyAddress = this->readInt8(1);
settings.Ethernet.PWRPin = this->readInt8(1);
@@ -1048,9 +1048,9 @@ bool ShadeConfigFile::writeNetRecord() {
this->writeBool(settings.MQTT.pubDisco);
this->writeVarString(settings.MQTT.rootTopic);
this->writeVarString(settings.MQTT.discoTopic);
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
this->writeUInt8(settings.Ethernet.boardType);
- this->writeUInt8(static_cast(settings.Ethernet.phyType));
+ this->writeUInt8(settings.Ethernet.phyType);
this->writeUInt8(static_cast(settings.Ethernet.CLKMode));
this->writeInt8(settings.Ethernet.phyAddress);
this->writeInt8(settings.Ethernet.PWRPin);
diff --git a/src/ConfigSettings.cpp b/src/ConfigSettings.cpp
index f0745a4..2d6c851 100644
--- a/src/ConfigSettings.cpp
+++ b/src/ConfigSettings.cpp
@@ -181,7 +181,7 @@ bool ConfigSettings::begin() {
this->Security.begin();
this->IP.begin();
this->WIFI.begin();
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
this->Ethernet.begin();
#endif
this->NTP.begin();
@@ -255,7 +255,7 @@ void ConfigSettings::print() {
ESP_LOGD(TAG, "Connection Type: %u", (unsigned int) this->connType);
this->NTP.print();
if(this->connType == conn_types_t::wifi || this->connType == conn_types_t::unset) this->WIFI.print();
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
if(this->connType == conn_types_t::ethernet || this->connType == conn_types_t::ethernetpref) this->Ethernet.print();
#endif
}
@@ -601,7 +601,7 @@ bool WifiSettings::ssidExists(const char *ssid) {
}
return false;
}
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
EthernetSettings::EthernetSettings() {}
bool EthernetSettings::begin() {
this->load();
@@ -611,7 +611,7 @@ bool EthernetSettings::fromJSON(JsonObject &obj) {
if(!obj["boardType"].isNull()) this->boardType = obj["boardType"];
if(!obj["phyAddress"].isNull()) this->phyAddress = obj["phyAddress"];
if(!obj["CLKMode"].isNull()) this->CLKMode = static_cast(obj["CLKMode"]);
- if(!obj["phyType"].isNull()) this->phyType = static_cast(obj["phyType"]);
+ if(!obj["phyType"].isNull()) this->phyType = obj["phyType"];
if(!obj["PWRPin"].isNull()) this->PWRPin = obj["PWRPin"];
if(!obj["MDCPin"].isNull()) this->MDCPin = obj["MDCPin"];
if(!obj["MDIOPin"].isNull()) this->MDIOPin = obj["MDIOPin"];
@@ -621,7 +621,7 @@ bool EthernetSettings::toJSON(JsonObject &obj) {
obj["boardType"] = this->boardType;
obj["phyAddress"] = this->phyAddress;
obj["CLKMode"] = static_cast(this->CLKMode);
- obj["phyType"] = static_cast(this->phyType);
+ obj["phyType"] = this->phyType;
obj["PWRPin"] = this->PWRPin;
obj["MDCPin"] = this->MDCPin;
obj["MDIOPin"] = this->MDIOPin;
@@ -642,7 +642,7 @@ bool EthernetSettings::save() {
pref.clear();
pref.putChar("boardType", this->boardType);
pref.putChar("phyAddress", this->phyAddress);
- pref.putChar("phyType", static_cast(this->phyType));
+ pref.putChar("phyType", this->phyType);
pref.putChar("CLKMode", static_cast(this->CLKMode));
pref.putChar("PWRPin", this->PWRPin);
pref.putChar("MDCPin", this->MDCPin);
@@ -654,7 +654,7 @@ bool EthernetSettings::load() {
Preferences pref;
pref.begin("ETH");
this->boardType = pref.getChar("boardType", this->boardType);
- this->phyType = static_cast(pref.getChar("phyType", ETH_PHY_LAN8720));
+ this->phyType = pref.getChar("phyType", this->phyType);
this->CLKMode = static_cast(pref.getChar("CLKMode", ETH_CLOCK_GPIO0_IN));
this->phyAddress = pref.getChar("phyAddress", this->phyAddress);
this->PWRPin = pref.getChar("PWRPin", this->PWRPin);
@@ -667,7 +667,7 @@ void EthernetSettings::print() {
ESP_LOGD(TAG, "Ethernet Settings Board:%d PHYType:%d CLK:%d ADDR:%d PWR:%d MDC:%d MDIO:%d",
this->boardType, this->phyType, this->CLKMode, this->phyAddress, this->PWRPin, this->MDCPin, this->MDIOPin);
}
-#endif // CONFIG_IDF_TARGET_ESP32C6
+#endif // HAS_ETHERNET
void ConfigSettings::printAvailHeap() {
ESP_LOGD(TAG, "Max Heap: %u", (unsigned int)ESP.getMaxAllocHeap());
ESP_LOGD(TAG, "Free Heap: %u", (unsigned int)ESP.getFreeHeap());
diff --git a/src/ConfigSettings.h b/src/ConfigSettings.h
index 5af161c..7bcff67 100644
--- a/src/ConfigSettings.h
+++ b/src/ConfigSettings.h
@@ -1,7 +1,5 @@
#include
-#ifndef CONFIG_IDF_TARGET_ESP32C6
-#include
-#endif
+#include "EthPhy.h"
#ifndef configsettings_h
#define configsettings_h
#include "WResp.h"
@@ -94,17 +92,18 @@ class WifiSettings: BaseSettings {
void print();
};
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
class EthernetSettings: BaseSettings {
public:
EthernetSettings();
uint8_t boardType = 0; // These board types are enumerated in the ui and used to set the chip settings.
- eth_phy_type_t phyType = ETH_PHY_LAN8720;
+ uint8_t phyType = static_cast(eth_phy_id_t::LAN8720); // An eth_phy_id_t, see EthPhy.h.
eth_clock_mode_t CLKMode = ETH_CLOCK_GPIO0_IN;
- int8_t phyAddress = ETH_PHY_ADDR;
- int8_t PWRPin = ETH_PHY_POWER;
- int8_t MDCPin = ETH_PHY_MDC;
- int8_t MDIOPin = ETH_PHY_MDIO;
+ // The LAN8720 wiring Arduino 2.x defaulted to. 3.x has no defaults.
+ int8_t phyAddress = 0;
+ int8_t PWRPin = -1;
+ int8_t MDCPin = 23;
+ int8_t MDIOPin = 18;
bool begin();
bool fromJSON(JsonObject &obj);
@@ -188,7 +187,7 @@ class ConfigSettings: BaseSettings {
uint8_t status;
IPSettings IP;
WifiSettings WIFI;
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
EthernetSettings Ethernet;
#endif
NTPSettings NTP;
diff --git a/src/ESPNetwork.cpp b/src/ESPNetwork.cpp
index e91c2a9..5125a9c 100644
--- a/src/ESPNetwork.cpp
+++ b/src/ESPNetwork.cpp
@@ -1,6 +1,3 @@
-#ifndef CONFIG_IDF_TARGET_ESP32C6
-#include
-#endif
#include
#include
#include
@@ -65,11 +62,16 @@ conn_types_t ESPNetwork::preferredConnType() {
case conn_types_t::unset:
case conn_types_t::ap:
return conn_types_t::ap;
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
case conn_types_t::ethernetpref:
return settings.WIFI.ssid[0] != '\0' && (!ETH.linkUp() && this->ethStarted) ? conn_types_t::wifi : conn_types_t::ethernet;
case conn_types_t::ethernet:
return ETH.linkUp() || !this->ethStarted ? conn_types_t::ethernet : conn_types_t::ap;
+#else
+ // Stored by a backup or an older UI on a chip without Ethernet.
+ case conn_types_t::ethernetpref:
+ case conn_types_t::ethernet:
+ return settings.WIFI.ssid[0] != '\0' ? conn_types_t::wifi : conn_types_t::ap;
#endif
default:
return settings.connType;
@@ -191,12 +193,12 @@ void ESPNetwork::emitSockets() {
}
}
void ESPNetwork::emitSockets(uint8_t num) {
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
if(this->connType == conn_types_t::ethernet) {
JsonSockEvent *json = sockEmit.beginEmit("ethernet");
json->beginObject();
json->addElem("connected", this->connected());
- json->addElem("speed", ETH.linkSpeed());
+ json->addElem("speed", (uint32_t)ETH.linkSpeed());
json->addElem("fullduplex", ETH.fullDuplex());
json->endObject();
sockEmit.endEmit(num);
@@ -254,7 +256,7 @@ void ESPNetwork::setConnected(conn_types_t connType) {
this->channel = WiFi.channel();
this->connectAttempts++;
}
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
else if(this->connType == conn_types_t::ethernet) {
if(this->softAPOpened) {
ESP_LOGI(TAG, "Disconnecting from SoftAP");
@@ -265,7 +267,7 @@ void ESPNetwork::setConnected(conn_types_t connType) {
this->_connecting = false;
this->wifiFallback = false;
}
-#endif // CONFIG_IDF_TARGET_ESP32C6
+#endif // HAS_ETHERNET
// NET: Begin this in the startup.
//sockEmit.begin();
safe_wdt_reset();
@@ -281,7 +283,7 @@ void ESPNetwork::setConnected(conn_types_t connType) {
settings.IP.dns2 = WiFi.dnsIP(1);
}
}
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
else {
ESP_LOGI(TAG, "Successfully Connected to Ethernet!!! %s%s %dMbps", ETH.localIP().toString().c_str(), ETH.fullDuplex() ? " FULL DUPLEX" : "", ETH.linkSpeed());
if(settings.IP.dhcp) {
@@ -295,7 +297,7 @@ void ESPNetwork::setConnected(conn_types_t connType) {
JsonSockEvent *json = sockEmit.beginEmit("ethernet");
json->beginObject();
json->addElem("connected", this->connected());
- json->addElem("speed", ETH.linkSpeed());
+ json->addElem("speed", (uint32_t)ETH.linkSpeed());
json->addElem("fullduplex", ETH.fullDuplex());
json->endObject();
sockEmit.endEmit();
@@ -307,7 +309,7 @@ void ESPNetwork::setConnected(conn_types_t connType) {
if(this->connType == conn_types_t::wifi) {
ESP_LOGI(TAG, "Reconnected after %.3fsec IP: %s %s CH:%d (%d dBm) Disconnected %d times", 1.0 * (millis() - this->connectStart)/1000, WiFi.localIP().toString().c_str(), this->mac.c_str(), this->channel, this->strength, this->connectAttempts - 1);
}
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
else {
ESP_LOGI(TAG, "Reconnected after %.3fsec IP: %s%s %dMbps Disconnected %d times", 1.0 * (millis() - this->connectStart)/1000, ETH.localIP().toString().c_str(), ETH.fullDuplex() ? " FULL DUPLEX" : "", ETH.linkSpeed(), this->connectAttempts - 1);
}
@@ -355,7 +357,7 @@ void ESPNetwork::setConnected(conn_types_t connType) {
settings.printAvailHeap();
this->needsBroadcast = true;
}
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
bool ESPNetwork::connectWired() {
if(ETH.linkUp()) {
// If the ethernet link is re-established then we need to shut down wifi.
@@ -389,7 +391,13 @@ bool ESPNetwork::connectWired() {
else
ETH.setHostname("ESPSomfy-RTS");
ESP_LOGD(TAG, "Set hostname to: %s", ETH.getHostname());
- if(!ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode)) {
+ eth_phy_type_t phyType;
+ bool begun = false;
+ if(!ethPhyType(settings.Ethernet.phyType, phyType))
+ ESP_LOGE(TAG, "Ethernet PHY type %u is not supported", (unsigned int)settings.Ethernet.phyType);
+ else
+ begun = ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, phyType, settings.Ethernet.CLKMode);
+ if(!begun) {
ESP_LOGE(TAG, "Ethernet Begin failed");
this->ethStarted = false;
if(settings.connType == conn_types_t::ethernetpref) {
@@ -412,10 +420,10 @@ bool ESPNetwork::connectWired() {
this->connectStart = millis();
return true;
}
-#endif // CONFIG_IDF_TARGET_ESP32C6
+#endif // HAS_ETHERNET
void ESPNetwork::updateHostname() {
if(settings.hostname[0] != '\0' && this->connected()) {
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
if(this->connType == conn_types_t::ethernet &&
strcmp(settings.hostname, ETH.getHostname()) != 0) {
ESP_LOGD(TAG, "Updating host name to %s...", settings.hostname);
@@ -511,7 +519,7 @@ bool ESPNetwork::connect(conn_types_t ctype) {
esp_task_wdt_reset();
if(this->connecting()) return true;
if(this->disconnectTime == 0) this->disconnectTime = millis();
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
if(ctype == conn_types_t::ethernet && this->connType != conn_types_t::ethernet) {
// Here we need to call the connect to ethernet.
this->connectWired();
@@ -588,7 +596,7 @@ bool ESPNetwork::connected() {
if(this->connecting()) return false;
else if(this->connType == conn_types_t::unset) return false;
else if(this->connType == conn_types_t::wifi) return WiFi.status() == WL_CONNECTED;
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
else if(this->connType == conn_types_t::ethernet) return ETH.linkUp();
#endif
else return this->connType != conn_types_t::unset;
@@ -635,7 +643,7 @@ void ESPNetwork::networkEvent(WiFiEvent_t event) {
net.setConnected(conn_types_t::wifi);
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP: ESP_LOGW(TAG, "Lost IP address and IP address is reset to 0"); break;
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
case ARDUINO_EVENT_ETH_GOT_IP:
// If the Wifi is connected then drop that connection
if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true);
@@ -680,7 +688,7 @@ void ESPNetwork::networkEvent(WiFiEvent_t event) {
net.softAPOpened = false;
break;
default:
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
if(event > ARDUINO_EVENT_ETH_START)
ESP_LOGW(TAG, "(evt) Unknown Ethernet Event %d", event);
#endif
diff --git a/src/EthPhy.h b/src/EthPhy.h
new file mode 100644
index 0000000..6b2ec46
--- /dev/null
+++ b/src/EthPhy.h
@@ -0,0 +1,45 @@
+#ifndef ETH_PHY_H
+#define ETH_PHY_H
+#include
+#if __has_include()
+#include
+#endif
+
+// Ethernet needs the RMII MAC, which only the original ESP32 has. The C3, S3
+// and C6 build without it.
+#if defined(CONFIG_ETH_USE_ESP32_EMAC) && CONFIG_ETH_USE_ESP32_EMAC
+#define HAS_ETHERNET 1
+#else
+#define HAS_ETHERNET 0
+#endif
+
+// PHY chip ids as stored in NVS and backups and sent to the web UI. They are
+// Arduino 2.x's eth_phy_type_t values, frozen: Arduino 3.x renumbered that
+// enum, so an id is translated by name when Ethernet starts, never cast.
+enum class eth_phy_id_t : uint8_t {
+ LAN8720 = 0,
+ TLK110 = 1,
+ RTL8201 = 2,
+ DP83848 = 3,
+ DM9051 = 4, // An SPI chip. The RMII MAC can't drive it.
+ KSZ8041 = 5,
+ KSZ8081 = 6
+};
+
+#if HAS_ETHERNET
+#include
+// Sets type to the framework's PHY for a stored id. False for an id the RMII
+// MAC can't drive.
+inline bool ethPhyType(uint8_t id, eth_phy_type_t &type) {
+ switch(static_cast(id)) {
+ case eth_phy_id_t::LAN8720: type = ETH_PHY_LAN8720; return true;
+ case eth_phy_id_t::TLK110: type = ETH_PHY_TLK110; return true;
+ case eth_phy_id_t::RTL8201: type = ETH_PHY_RTL8201; return true;
+ case eth_phy_id_t::DP83848: type = ETH_PHY_DP83848; return true;
+ case eth_phy_id_t::KSZ8041: type = ETH_PHY_KSZ8041; return true;
+ case eth_phy_id_t::KSZ8081: type = ETH_PHY_KSZ8081; return true;
+ default: return false;
+ }
+}
+#endif
+#endif
diff --git a/src/SSDP.cpp b/src/SSDP.cpp
index 7dd4a62..c7ef2c7 100644
--- a/src/SSDP.cpp
+++ b/src/SSDP.cpp
@@ -382,22 +382,11 @@ void SSDPClass::_parsePacket(ssdp_packet_t *pkt, AsyncUDPPacket &p) {
}
IPAddress SSDPClass::localIP()
{
- // Make sure we don't get a null IPAddress.
-#ifdef CONFIG_IDF_TARGET_ESP32C6
- return WiFi.localIP();
-#else
- tcpip_adapter_ip_info_t ip;
- if (WiFi.getMode() == WIFI_STA) {
- if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip)) {
- return IPAddress();
- }
- } else if (WiFi.getMode() == WIFI_OFF) {
- if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip)) {
- return IPAddress();
- }
- }
- return IPAddress(ip.ip.addr);
+#if HAS_ETHERNET
+ // Wi-Fi is switched off while Ethernet is connected.
+ if (WiFi.getMode() == WIFI_OFF) return ETH.localIP();
#endif
+ return WiFi.localIP();
}
void SSDPClass::_sendResponse(IPAddress addr, uint16_t port, UPNPDeviceType *d, const char *st, response_types_t responseType) {
char buffer[1460];
diff --git a/src/SomfyJson.cpp b/src/SomfyJson.cpp
index fe2902a..af6a53f 100644
--- a/src/SomfyJson.cpp
+++ b/src/SomfyJson.cpp
@@ -50,7 +50,7 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) {
pin_users_t radio, ethernet, others;
if(somfy.transceiver.config.enabled)
radio.uses = [](uint8_t pin, void *) { return somfy.transceiver.usesPin(pin); };
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref)
ethernet.uses = [](uint8_t pin, void *) { return settings.Ethernet.usesPin(pin); };
#endif
diff --git a/src/entryPoints/WebUiRoutes.cpp b/src/entryPoints/WebUiRoutes.cpp
index fc0ba55..9d7559f 100644
--- a/src/entryPoints/WebUiRoutes.cpp
+++ b/src/entryPoints/WebUiRoutes.cpp
@@ -802,7 +802,7 @@ void registerWebUiRoutes(AsyncWebServer &server) {
JsonObject objEth = obj["ethernet"];
if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref)
reboot = true;
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
settings.Ethernet.fromJSON(objEth);
settings.Ethernet.save();
#endif
@@ -886,8 +886,10 @@ void registerWebUiRoutes(AsyncWebServer &server) {
JsonObject obj = doc.to();
settings.toJSON(obj);
obj["fwVersion"] = settings.fwVersion.name;
+ // The web UI hides the Ethernet settings when the chip has no MAC.
+ obj["hasEthernet"] = HAS_ETHERNET == 1;
JsonObject eth = obj.createNestedObject("ethernet");
-#ifndef CONFIG_IDF_TARGET_ESP32C6
+#if HAS_ETHERNET
settings.Ethernet.toJSON(eth);
#endif
JsonObject wifi = obj.createNestedObject("wifi");
diff --git a/test/stubs/ETH.h b/test/stubs/ETH.h
new file mode 100644
index 0000000..c974957
--- /dev/null
+++ b/test/stubs/ETH.h
@@ -0,0 +1,16 @@
+// Stand-in for the Arduino core's ETH.h in the native test env. Uses the
+// Arduino 3.x numbering, which put ETH_PHY_GENERIC first, so a stored PHY id
+// that is cast instead of translated fails the tests.
+#ifndef eth_stub_h
+#define eth_stub_h
+typedef enum {
+ ETH_PHY_GENERIC,
+ ETH_PHY_LAN8720,
+ ETH_PHY_TLK110,
+ ETH_PHY_RTL8201,
+ ETH_PHY_DP83848,
+ ETH_PHY_KSZ8041,
+ ETH_PHY_KSZ8081,
+ ETH_PHY_MAX
+} eth_phy_type_t;
+#endif
diff --git a/test/test_ethphy/test_ethphy.cpp b/test/test_ethphy/test_ethphy.cpp
new file mode 100644
index 0000000..08228d2
--- /dev/null
+++ b/test/test_ethphy/test_ethphy.cpp
@@ -0,0 +1,63 @@
+#include
+// As on the original ESP32, so the translation is built.
+#define CONFIG_ETH_USE_ESP32_EMAC 1
+#include "EthPhy.h"
+
+void setUp() {}
+void tearDown() {}
+
+static eth_phy_type_t translate(eth_phy_id_t id) {
+ eth_phy_type_t type = ETH_PHY_MAX;
+ TEST_ASSERT_TRUE(ethPhyType(static_cast(id), type));
+ return type;
+}
+
+/*********************************************************************
+ * Stored ids
+ ********************************************************************/
+static void test_ids_match_stored_values() {
+ // NVS, backups and the web UI hold these numbers. Changing one moves
+ // every existing Ethernet board to another chip.
+ TEST_ASSERT_EQUAL_UINT8(0, static_cast(eth_phy_id_t::LAN8720));
+ TEST_ASSERT_EQUAL_UINT8(1, static_cast(eth_phy_id_t::TLK110));
+ TEST_ASSERT_EQUAL_UINT8(2, static_cast(eth_phy_id_t::RTL8201));
+ TEST_ASSERT_EQUAL_UINT8(3, static_cast(eth_phy_id_t::DP83848));
+ TEST_ASSERT_EQUAL_UINT8(4, static_cast(eth_phy_id_t::DM9051));
+ TEST_ASSERT_EQUAL_UINT8(5, static_cast(eth_phy_id_t::KSZ8041));
+ TEST_ASSERT_EQUAL_UINT8(6, static_cast(eth_phy_id_t::KSZ8081));
+}
+
+/*********************************************************************
+ * ethPhyType
+ ********************************************************************/
+static void test_translates_each_rmii_chip_by_name() {
+ TEST_ASSERT_EQUAL(ETH_PHY_LAN8720, translate(eth_phy_id_t::LAN8720));
+ TEST_ASSERT_EQUAL(ETH_PHY_TLK110, translate(eth_phy_id_t::TLK110));
+ TEST_ASSERT_EQUAL(ETH_PHY_RTL8201, translate(eth_phy_id_t::RTL8201));
+ TEST_ASSERT_EQUAL(ETH_PHY_DP83848, translate(eth_phy_id_t::DP83848));
+ TEST_ASSERT_EQUAL(ETH_PHY_KSZ8041, translate(eth_phy_id_t::KSZ8041));
+ TEST_ASSERT_EQUAL(ETH_PHY_KSZ8081, translate(eth_phy_id_t::KSZ8081));
+}
+static void test_lan8720_is_not_the_framework_zero() {
+ // The default board stores 0. Arduino 3.x's 0 is ETH_PHY_GENERIC.
+ TEST_ASSERT_NOT_EQUAL(0, translate(eth_phy_id_t::LAN8720));
+}
+static void test_rejects_dm9051() {
+ eth_phy_type_t type = ETH_PHY_MAX;
+ TEST_ASSERT_FALSE(ethPhyType(static_cast(eth_phy_id_t::DM9051), type));
+}
+static void test_rejects_unknown_ids() {
+ eth_phy_type_t type = ETH_PHY_MAX;
+ TEST_ASSERT_FALSE(ethPhyType(7, type));
+ TEST_ASSERT_FALSE(ethPhyType(255, type));
+}
+
+int main(int, char **) {
+ UNITY_BEGIN();
+ RUN_TEST(test_ids_match_stored_values);
+ RUN_TEST(test_translates_each_rmii_chip_by_name);
+ RUN_TEST(test_lan8720_is_not_the_framework_zero);
+ RUN_TEST(test_rejects_dm9051);
+ RUN_TEST(test_rejects_unknown_ids);
+ return UNITY_END();
+}