stp: vendor-style per-port config + status (path cost, p2p, designated info)

Bring the Spanning Tree page in line with a typical managed switch's
per-port panel. Configuration gains the full-range path cost (raw
0..200000000, 0 = auto, replacing the old 1000x-scaled byte), a
point-to-point admin control (auto/on/off), and the priority is now a
0..240 step-16 dropdown. A new status table shows, per port, the Port
State, Role, Designated Bridge / Port ID / Cost (learned from received
BPDUs, kept per port and aged via the BPDU age), Operational Edge and
Operational Point-to-Point.

The designated fields fall back to presenting this switch as the
segment's designated bridge when no fresh BPDU has been heard (so a
quiet port shows our own bridge-id, as the vendor UIs do). /stp.json
carries the packed hex fields plus our own MAC for that fallback.

Space: reclaim BANK2 for the above by moving rtl837x_pins to HOME and
compacting leds_dump into a register-address table (~800B); bandwidth
returns to BANK1. No BANK3 - hardware-verified that PSBANK > 2 crashes
this SoC at boot (a bricked unit and an SPI-programmer recovery earlier
today); a warning to that effect is now in rtl837x_lldp.c.

Hardware-verified: cost 200000000 and p2p off round-trip through the CLI
and JSON, the status table populates correctly with STP enabled (all
ports Forwarding/Designated, oper-edge and oper-p2p True), LACP 3f/3f
and the LAN unaffected.

(cherry picked from commit 2ec62072f061dc9e78bc821ba1c297cb6819e206)
This commit is contained in:
d00f
2026-08-04 03:28:07 +02:00
parent 6fcb8ef11f
commit 7ec0286d69
7 changed files with 144 additions and 41 deletions
+45 -27
View File
@@ -1,16 +1,7 @@
/* Spanning Tree page: full RSTP configuration + live status.
*
* Every control applies IMMEDIATELY on change (POST /cmd "stp ...") - there is
* no per-row Apply. The refresh (2 s) repopulates controls from /stp.json;
* a global dirty flag suppresses that between a change and its confirmation
* so the refresh never reverts an edit in flight (same lesson as the LAG page).
*/
// STP port states as encoded in the ASIC's MSTP register (2 bits per port)
const STP_STATES = ["Disabled", "Blocking", "Learning", "Forwarding"];
const STP_ROLES = ["-", "Root", "Designated", "Alternate"];
// stp_pflags bits (keep in sync with rtl837x_stp.h)
const PF_ENABLED = 1, PF_ADMEDGE = 2, PF_AUTOEDGE = 4, PF_BPDUGUARD = 8,
PF_ROOTGUARD = 16, PF_FILTER = 32, PF_OPEREDGE = 64, PF_TRIPPED = 128;
@@ -49,38 +40,59 @@ function num(id, min, max, onch) {
function buildPortsTable(ports) {
const tbl = document.getElementById("stpPortsTbl");
const stat = document.getElementById("stpStatTbl");
for (const p of ports) {
const tr = tbl.insertRow();
tr.insertCell().textContent = p.p; // Port
tr.insertCell().id = "st_" + p.p; // State
tr.insertCell().id = "role_" + p.p; // Role
tr.insertCell().appendChild(sel("en_" + p.p,
[["on","on"],["off","off"]],
[["on","Enable"],["off","Disable"]],
e => stpCmd("stp port " + p.p + " " + e.target.value)));
const pc = num("cost_" + p.p, 0, 200000000,
e => stpCmd("stp port " + p.p + " cost " + e.target.value));
pc.style.width = "7em";
pc.title = "0 - 200000000 (0 = Auto)";
tr.insertCell().appendChild(pc);
const pr = sel("prio_" + p.p, [],
e => stpCmd("stp port " + p.p + " prio " + e.target.value));
for (let v = 0; v <= 240; v += 16) {
const o = document.createElement("option");
o.value = v; o.textContent = v + (v === 128 ? " (default)" : "");
pr.appendChild(o);
}
tr.insertCell().appendChild(pr);
tr.insertCell().appendChild(sel("edge_" + p.p,
[["auto","auto"],["on","edge"],["off","off"]],
[["auto","Auto"],["on","Enable"],["off","Disable"]],
e => stpCmd("stp port " + p.p + " edge " + e.target.value)));
tr.insertCell().appendChild(num("cost_" + p.p, 0, 255,
e => stpCmd("stp port " + p.p + " cost " + e.target.value)));
tr.insertCell().appendChild(num("prio_" + p.p, 0, 240,
e => stpCmd("stp port " + p.p + " prio " + e.target.value)));
tr.insertCell().appendChild(sel("guard_" + p.p,
[["none","none"],["bpdu","BPDU"],["root","Root"]],
e => stpCmd("stp port " + p.p + " guard " + e.target.value)));
tr.insertCell().appendChild(sel("filt_" + p.p,
[["off","off"],["on","on"]],
[["off","Disable"],["on","Enable"]],
e => stpCmd("stp port " + p.p + " filter " + e.target.value)));
tr.insertCell().appendChild(sel("guard_" + p.p,
[["none","None"],["bpdu","BPDU"],["root","Root"]],
e => stpCmd("stp port " + p.p + " guard " + e.target.value)));
tr.insertCell().appendChild(sel("p2p_" + p.p,
[["auto","Auto"],["on","Enable"],["off","Disable"]],
e => stpCmd("stp port " + p.p + " p2p " + e.target.value)));
const sr = stat.insertRow();
sr.insertCell().textContent = p.p;
for (const id of ["st","role","db","dp","dc","oe","op"])
sr.insertCell().id = id + "_" + p.p;
}
stpRows = ports.length;
}
function fmtBridgeId(h) {
if (!h || h.length < 16) return "";
const prio = parseInt(h.slice(0, 4), 16);
const mac = h.slice(4).replace(/(..)(?=.)/g, "$1:");
return prio + "-" + mac.toUpperCase();
}
function fetchStp() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
// textContent throughout: rootMac comes from received BPDUs
// (remote-controlled), never render it as HTML
if (!stpRows)
buildPortsTable(s.ports);
document.getElementById("stpStat").textContent = s.fsT
@@ -92,13 +104,19 @@ function fetchStp() {
+ " via port " + s.rootPort + " — path cost: 0x" + s.cost
+ " — topology changes: " + parseInt(s.tc, 16))
: "";
// live status columns always refresh
for (const p of s.ports) {
const trip = (p.f & PF_TRIPPED) ? " (guard!)" : "";
document.getElementById("st_" + p.p).textContent =
s.on ? STP_STATES[p.st] + trip : "-";
document.getElementById("role_" + p.p).textContent =
s.on ? STP_ROLES[p.role] + ((p.f & PF_OPEREDGE) ? " edge" : "") : "-";
s.on ? STP_ROLES[p.role] : "-";
document.getElementById("db_" + p.p).textContent = s.on ? fmtBridgeId(p.db) : "-";
document.getElementById("dp_" + p.p).textContent =
s.on ? (parseInt(p.dp.slice(0, 2), 16) + "-" + parseInt(p.dp.slice(2), 16)) : "-";
document.getElementById("dc_" + p.p).textContent = s.on ? parseInt(p.dc, 16) : "-";
document.getElementById("oe_" + p.p).textContent =
s.on ? ((p.f & PF_OPEREDGE) ? "True" : "False") : "-";
document.getElementById("op_" + p.p).textContent = s.on ? (p.p2 == 2 ? "False" : "True") : "-";
}
if (stpDirty) // an edit is in flight - do not revert controls
return;
@@ -114,8 +132,9 @@ function fetchStp() {
document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off";
document.getElementById("edge_" + p.p).value =
(p.f & PF_ADMEDGE) ? "on" : ((p.f & PF_AUTOEDGE) ? "auto" : "off");
document.getElementById("cost_" + p.p).value = p.cost;
document.getElementById("cost_" + p.p).value = parseInt(p.pc, 16);
document.getElementById("prio_" + p.p).value = p.prio;
document.getElementById("p2p_" + p.p).value = ["auto","on","off"][p.p2];
document.getElementById("guard_" + p.p).value =
(p.f & PF_BPDUGUARD) ? "bpdu" : ((p.f & PF_ROOTGUARD) ? "root" : "none");
document.getElementById("filt_" + p.p).value = (p.f & PF_FILTER) ? "on" : "off";
@@ -132,7 +151,6 @@ async function stpSub() {
}
window.addEventListener("load", function() {
// bridge priority: 0-15 (x4096)
const bp = document.getElementById("bPrio");
for (let i = 0; i < 16; i++) {
const o = document.createElement("option");