feat: Implement W5500 SPI ethernet support (#286)

* feat: implement feature gate & w5500 SPI ethernet

* feat: eth conn restart handling, wifi-ap connection type, BLE sensor
gate

* fix: bring eth down when disabled

* fix: disable WiFi scan when ethernet is active

* feat: refactor uplink types into separate classes

* fix: compare enum, not string

* chore: remove dead code

* fix: remove out of scope persistNow(), move w5500 to networksettings,
add base for future ethernet implementations

* fix: keep wifi armed while reconnecting, reset IP when moving from
static to DHCP, refactor wifi loop to use return

* fix: clear out connectionType string for enum
This commit is contained in:
Elvin Luff authored and GitHub committed 2026-11-11 08:31:36 +03:00
1 parent dfbd297f90
commit f9a814ca11
25 files changed
+1801 -450

No files matched your search

+15 -2
View File
@@ -50,15 +50,19 @@
<th scope="row" data-i18n>network.params.mac</th>
<td><b id="network-mac"></b></td>
</tr>
<tr>
<th scope="row" data-i18n>network.params.connection</th>
<td><b id="network-connection"></b></td>
</tr>
<tr>
<th scope="row" data-i18n>network.wifi.connected</th>
<td><i id="network-connected"></i></td>
</tr>
<tr>
<tr id="network-ssid-row">
<th scope="row" data-i18n>network.wifi.ssid</th>
<td><b id="network-ssid"></b></td>
</tr>
<tr>
<tr id="network-signal-row">
<th scope="row" data-i18n>network.wifi.signal</th>
<td><b id="network-signal"></b> %</td>
</tr>
@@ -192,6 +196,7 @@
setValue('#network-hostname', result.network.hostname);
setValue('#network-mac', result.network.mac);
setValue('#network-connection', i18n(`network.params.connectionType.${result.network.connectionType}`));
setStatus(
'#network-connected',
result.network.connected ? "success" : "error",
@@ -199,6 +204,14 @@
);
setValue('#network-ssid', result.network.ssid);
setValue('#network-signal', result.network.signalQuality);
// SSID and signal strength only describe the uplink when WiFi carries it; with
// ethernet up the radio is parked and both values are meaningless
const wifiUplink = result.network.connectionType === "wifi";
document.getElementById("network-ssid-row")?.classList.toggle("hidden", !wifiUplink);
document.getElementById("network-signal-row")?.classList.toggle("hidden", !wifiUplink);
setValue('#network-ip', result.network.ip);
setValue('#network-subnet', result.network.subnet);
setValue('#network-gateway', result.network.gateway);
+115 -6
View File
@@ -79,6 +79,7 @@
</div>
</article>
<article>
<div>
<hgroup>
@@ -100,6 +101,7 @@
</table>
</div>
<p id="scan-unavailable" class="hidden" data-i18n>network.scan.unavailable</p>
<button type="submit" data-i18n>button.refresh</button>
</form>
@@ -162,6 +164,62 @@
</form>
</div>
</article>
<article id="ethernet-section" class="hidden">
<div>
<hgroup>
<h3 data-i18n>network.section.ethernet</h3>
<p></p>
</hgroup>
<div id="ethernet-settings-busy" aria-busy="true"></div>
<form action="/api/network/settings" id="ethernet-settings" class="hidden">
<label>
<input type="checkbox" name="ethernet[enabled]" value="true">
<span data-i18n>network.ethernet.enable</span>
</label>
<small id="ethernet-not-configured" class="hidden" data-i18n>network.ethernet.notConfigured</small>
<div class="grid">
<label>
<span data-i18n>network.ethernet.clk</span>
<input type="number" inputmode="numeric" name="ethernet[pins][clk]" min="0" max="254" step="1">
</label>
<label>
<span data-i18n>network.ethernet.mosi</span>
<input type="number" inputmode="numeric" name="ethernet[pins][mosi]" min="0" max="254" step="1">
</label>
<label>
<span data-i18n>network.ethernet.miso</span>
<input type="number" inputmode="numeric" name="ethernet[pins][miso]" min="0" max="254" step="1">
</label>
</div>
<div class="grid">
<label>
<span data-i18n>network.ethernet.cs</span>
<input type="number" inputmode="numeric" name="ethernet[pins][cs]" min="0" max="254" step="1">
</label>
<label>
<span data-i18n>network.ethernet.irq</span>
<input type="number" inputmode="numeric" name="ethernet[pins][irq]" min="0" max="254" step="1">
</label>
<label>
<span data-i18n>network.ethernet.rst</span>
<input type="number" inputmode="numeric" name="ethernet[pins][rst]" min="0" max="254" step="1">
</label>
</div>
<small data-i18n>network.ethernet.note</small>
<button type="submit" data-i18n>button.save</button>
</form>
</div>
</article>
</main>
<footer class="container">
@@ -190,6 +248,29 @@
setInputValue("[name='staticConfig[dns]']", data.staticConfig.dns);
setBusy('#network-settings-busy', '#network-settings', false);
const ethernet = data.ethernet || {};
const ethernetConfigured = !!ethernet.configured;
const ethernetPins = ethernet.pins || {};
const pinValue = (value) => (value ?? 255) < 255 ? value : '';
setInputValue("[name='ethernet[pins][clk]']", pinValue(ethernetPins.clk));
setInputValue("[name='ethernet[pins][mosi]']", pinValue(ethernetPins.mosi));
setInputValue("[name='ethernet[pins][miso]']", pinValue(ethernetPins.miso));
setInputValue("[name='ethernet[pins][cs]']", pinValue(ethernetPins.cs));
setInputValue("[name='ethernet[pins][irq]']", pinValue(ethernetPins.irq));
setInputValue("[name='ethernet[pins][rst]']", pinValue(ethernetPins.rst));
setCheckboxValue("[name='ethernet[enabled]']", ethernetConfigured ? !!ethernet.enabled : false);
const ethernetCheckbox = document.querySelector("[name='ethernet[enabled]']");
if (ethernetCheckbox) {
ethernetCheckbox.disabled = !ethernetConfigured;
}
document.getElementById('ethernet-not-configured')?.classList.toggle('hidden', ethernetConfigured);
setBusy('#ethernet-settings-busy', '#ethernet-settings', false);
setInputValue("[name='sta[ssid]']", data.sta.ssid);
setInputValue("[name='sta[password]']", data.sta.password);
setInputValue("[name='sta[channel]']", data.sta.channel);
@@ -201,21 +282,49 @@
setBusy('#ap-settings-busy', '#ap-settings', false);
};
const applyCapabilities = (info) => {
// pages and firmware ship together: the section stays hidden unless the build says it can do ethernet
const supported = info?.features?.ethernet === true;
document.getElementById('ethernet-section')?.classList.toggle('hidden', !supported);
return supported;
};
try {
const response = await fetch("/api/network/settings", {
cache: "no-cache",
credentials: "include"
});
const [response, infoResponse] = await Promise.all([
fetch("/api/network/settings", {
cache: "no-cache",
credentials: "include"
}),
fetch("/api/info", {
cache: "no-cache",
credentials: "include"
})
]);
if (!response.ok) {
throw new Error('Response not valid');
}
const info = infoResponse.ok ? await infoResponse.json() : null;
const ethernetSupported = applyCapabilities(info);
const result = await response.json();
fillData(result);
setupForm('#network-settings', fillData, ['hostname']);
setupNetworkScanForm('#network-scan', '#networks');
// scanning needs the radio, which stays parked while ethernet carries the uplink
const radioParked = info?.network?.connectionType === 'ethernet';
setupNetworkScanForm('#network-scan', '#networks', !radioParked);
document.querySelector('#network-scan button[type="submit"]').disabled = radioParked;
document.querySelector('#scan-unavailable').classList.toggle('hidden', !radioParked);
if (ethernetSupported) {
setupForm('#ethernet-settings', fillData);
}
setupForm('#sta-settings', fillData, ['sta.ssid', 'sta.password']);
setupForm('#ap-settings', fillData, ['ap.ssid', 'ap.password']);
+35 -5
View File
@@ -38,6 +38,8 @@
<p></p>
</hgroup>
<small id="ble-not-supported" class="hidden" data-i18n>settings.note.bleDevice</small>
<details id="template" class="sensor hidden" data-id="" data-preloaded="0">
<summary><b>#<span class="pos"></span>: <span class="name"></span></b></summary>
@@ -117,7 +119,7 @@
<option value="50" data-i18n>sensors.types.ntcTemp</option>
<option value="51" data-i18n>sensors.types.dallasTemp</option>
<option value="52" data-i18n>sensors.types.bluetooth</option>
<option value="52" data-requires="ble" disabled data-i18n>sensors.types.bluetooth</option>
<option value="253" data-i18n>sensors.types.heatSetpointTemp</option>
<option value="254" data-i18n>sensors.types.manual</option>
<option value="255" data-i18n>sensors.types.notConfigured</option>
@@ -206,16 +208,38 @@
const container = document.querySelector("article");
const templateNode = container.querySelector("#template");
try {
const response = await fetch("/api/sensors", {
cache: "no-cache",
credentials: "include"
let bleSupported = false;
const applyCapabilities = (info, root) => {
// pages and firmware ship together: BLE stays disabled unless the build says it has the stack
bleSupported = info?.features?.ble === true;
root.querySelectorAll('[data-requires="ble"]').forEach((option) => {
option.disabled = !bleSupported;
});
document.getElementById("ble-not-supported")?.classList.toggle("hidden", bleSupported);
};
try {
const [response, infoResponse] = await Promise.all([
fetch("/api/sensors", {
cache: "no-cache",
credentials: "include"
}),
fetch("/api/info", {
cache: "no-cache",
credentials: "include"
})
]);
if (!response.ok) {
throw new Error("Response not valid");
}
const info = infoResponse.ok ? await infoResponse.json() : null;
applyCapabilities(info, templateNode);
const result = await response.json();
for (const sensorId in result) {
const sensorNode = templateNode.cloneNode(true);
@@ -287,6 +311,12 @@
// ble
case 52:
if (!bleSupported) {
parentGpio.classList.add("hidden");
parentAddress.classList.add("hidden");
break;
}
parentGpio.classList.add("hidden");
parentAddress.classList.remove("hidden");
address.setAttribute("pattern", "([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}");