From 7ec0286d694f347e792ea56fd3fc7bf1864beaaa Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 22 Jul 2026 09:44:47 +0200 Subject: [PATCH] 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) --- html/config.js | 5 ++-- html/stp.html | 10 +++++-- html/stp.js | 72 +++++++++++++++++++++++++++++------------------ httpd/page_impl.c | 57 +++++++++++++++++++++++++++++++++---- rtl837x_pins.c | 2 -- rtl837x_stp.c | 29 +++++++++++++++++-- rtl837x_stp.h | 10 ++++++- 7 files changed, 144 insertions(+), 41 deletions(-) diff --git a/html/config.js b/html/config.js index 9b37f81..556bf40 100644 --- a/html/config.js +++ b/html/config.js @@ -27,10 +27,11 @@ const conf_cmds = [ /^stp\s+version\s+(rstp|stp)$/, /^stp\s+port\s+\d{1,2}\s+(on|off)$/, /^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/, - /^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,3}$/, + /^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,9}$/, /^stp\s+port\s+\d{1,2}\s+prio\s+\d{1,3}$/, /^stp\s+port\s+\d{1,2}\s+guard\s+(none|bpdu|root)$/, /^stp\s+port\s+\d{1,2}\s+filter\s+(on|off)$/, + /^stp\s+port\s+\d{1,2}\s+p2p\s+(auto|on|off)$/, /^igmp\s+(on|off)$/, /^mtu\s+\d{1,2}\s+\d+$/, /^bw\s+(in|out)\s+\d{1,2}\s+\S+$/, @@ -55,7 +56,7 @@ const conf_overwrite = [ /^laghash\b/, /^isolate\s+\d{1,2}\b/, /^stp\s+(prio|hello|maxage|fwd|txhold|version|failsafe)\b/, - /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter)\b/, + /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter|p2p)\b/, /^igmp\b/, /^mtu\s+\d{1,2}\b/, /^bw\s+(in|out)\s+\d{1,2}\b/, diff --git a/html/stp.html b/html/stp.html index 15815be..5bdbeb5 100644 --- a/html/stp.html +++ b/html/stp.html @@ -29,10 +29,16 @@

Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.

-

Ports

+

Port configuration

- + + +
PortStateRoleSTPEdgeCost [k]PriorityGuardFilterPortStatePath Cost
(0 = Auto)
PriorityEdge PortBPDU FilterGuardPoint-to-Point
+

Port status

+ + +
PortPort StateRoleDesignated BridgeDesignated Port IDDesignated CostOper. EdgeOper. P2P
diff --git a/html/stp.js b/html/stp.js index c6cd124..1af2230 100644 --- a/html/stp.js +++ b/html/stp.js @@ -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"); diff --git a/httpd/page_impl.c b/httpd/page_impl.c index c5fad8f..3d65dbb 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -535,7 +535,31 @@ void send_lag(void) * 0 Dis 1 Blk 2 Lrn 3 Fwd), an approximated role, and the per-port config * (enabled, edge admin/auto/oper, cost/1000, prio, guard, filter, tripped). */ __xdata uint8_t stp_we_root; -__xdata uint8_t pi_i, pi_j; /* shared loop iterators (DSEG relief) */ +__xdata uint8_t pi_i, pi_j, pi_j2; /* shared loop iterators (DSEG relief) */ + +/* Parameter relays in xdata: keeps these helpers off the IRAM overlay */ +static __xdata uint32_t pi_u32; +static __xdata uint8_t pi_prio, pi_ext; +static __xdata uint8_t * __xdata pi_mac; + +static void u32hex_html(void) +{ + /* byte access instead of uint32 shifts: sdcc/mcs51 expands each + * 32-bit shift into a large helper sequence. Little-endian layout. */ + __xdata uint8_t *b = (__xdata uint8_t *)&pi_u32; + byte_to_html(b[3]); + byte_to_html(b[2]); + byte_to_html(b[1]); + byte_to_html(b[0]); +} + +static void bridge_to_html(void) +{ + byte_to_html(pi_prio); + byte_to_html(pi_ext); + for (pi_j2 = 0; pi_j2 < 6; pi_j2++) + byte_to_html(pi_mac[pi_j2]); +} void send_stp(void) { @@ -566,6 +590,9 @@ void send_stp(void) slen += strtox(outbuf + slen, "\",\"rootMac\":\""); for (pi_j = 0; pi_j < 6; pi_j++) byte_to_html(root_bridge.mac[pi_j]); + slen += strtox(outbuf + slen, "\",\"myMac\":\""); + for (pi_j = 0; pi_j < 6; pi_j++) + byte_to_html(uip_ethaddr.addr[pi_j]); slen += strtox(outbuf + slen, "\",\"cost\":\""); byte_to_html(root_bridge_cost >> 24); byte_to_html(root_bridge_cost >> 16); @@ -599,11 +626,31 @@ void send_stp(void) itoa_html(3); slen += strtox(outbuf + slen, ",\"f\":"); itoa_html(stp_pflags[pi_i]); - slen += strtox(outbuf + slen, ",\"cost\":"); - itoa_html(stp_pcost[pi_i] / 1000); - slen += strtox(outbuf + slen, ",\"prio\":"); + /* path cost (raw hex, full 0..200M range), priority, p2p */ + slen += strtox(outbuf + slen, ",\"pc\":\""); + pi_u32 = stp_pcost[pi_i]; u32hex_html(); + slen += strtox(outbuf + slen, "\",\"prio\":"); itoa_html(stp_pprio[pi_i]); - slen += strtox(outbuf + slen, "},"); + slen += strtox(outbuf + slen, ",\"p2\":"); + itoa_html(stp_pp2p[pi_i]); + /* designated info: a freshly heard BPDU wins, else we are the + * segment's designated bridge and report our own values */ + stp_we_root = stp_dbridge[pi_i].mac[5] && stp_bpdu_age[pi_i] < (uint16_t)stp_maxage_s * 64; + slen += strtox(outbuf + slen, ",\"db\":\""); + if (stp_we_root) { + pi_prio = stp_dbridge[pi_i].prio; pi_ext = stp_dbridge[pi_i].ext; + pi_mac = stp_dbridge[pi_i].mac; + } else { + pi_prio = stp_prio; pi_ext = 0; + pi_mac = uip_ethaddr.addr; + } + bridge_to_html(); + slen += strtox(outbuf + slen, "\",\"dp\":\""); + byte_to_html(stp_we_root ? (stp_dpid[pi_i] >> 8) : stp_pprio[pi_i]); + byte_to_html(stp_we_root ? stp_dpid[pi_i] : (pi_i + 1)); + slen += strtox(outbuf + slen, "\",\"dc\":\""); + pi_u32 = stp_we_root ? stp_dcost[pi_i] : root_bridge_cost; u32hex_html(); + slen += strtox(outbuf + slen, "\"},"); } slen -= 1; // remove comma slen += strtox(outbuf + slen, "]}"); diff --git a/rtl837x_pins.c b/rtl837x_pins.c index 90015b7..e340c26 100644 --- a/rtl837x_pins.c +++ b/rtl837x_pins.c @@ -2,8 +2,6 @@ #include "rtl837x_common.h" #include "rtl837x_regs.h" -#pragma codeseg BANK2 -#pragma constseg BANK2 uint8_t i2c_bus_from_sda_pin(uint8_t sda_pin) __banked { switch (sda_pin) { diff --git a/rtl837x_stp.c b/rtl837x_stp.c index a07e435..13980b8 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -74,6 +74,11 @@ extern volatile __xdata uint8_t mgmt_alive; /* set by httpd on any request */ __xdata uint8_t stp_pflags[10]; __xdata uint32_t stp_pcost[10]; __xdata uint8_t stp_pprio[10]; +__xdata uint8_t stp_pp2p[10]; + +__xdata struct bridge stp_dbridge[10]; +__xdata uint16_t stp_dpid[10]; +__xdata uint32_t stp_dcost[10]; /* ---- Status / runtime ---- */ __xdata struct bridge root_bridge; @@ -600,9 +605,29 @@ void stp_parse(void) __banked __reentrant else if (!cmd_compare(4, "off")) goto err; } else if (cmd_compare(3, "cost")) { - if (atoi_byte(&stp_scratch, cmd_words_b[4])) + /* raw 802.1D value, 0..200000000; 0 = auto (speed-based) */ + stp_cost_scratch = 0; + { + __xdata uint8_t *cp = &cmd_buffer[cmd_words_b[4]]; + if (*cp < '0' || *cp > '9') + goto err; + while (*cp >= '0' && *cp <= '9') { + stp_cost_scratch = stp_cost_scratch * 10 + (*cp - '0'); + cp++; + } + } + if (stp_cost_scratch > 200000000UL) + goto err; + stp_pcost[port] = stp_cost_scratch; + } else if (cmd_compare(3, "p2p")) { + if (cmd_compare(4, "auto")) + stp_pp2p[port] = 0; + else if (cmd_compare(4, "on")) + stp_pp2p[port] = 1; + else if (cmd_compare(4, "off")) + stp_pp2p[port] = 2; + else goto err; - stp_pcost[port] = (uint32_t)stp_scratch * 1000; } else if (cmd_compare(3, "prio")) { if (atoi_byte(&stp_scratch, cmd_words_b[4])) goto err; diff --git a/rtl837x_stp.h b/rtl837x_stp.h index bad0d03..56aea9d 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -39,7 +39,15 @@ extern __xdata uint8_t stp_failsafe_tripped; /* max BPDUs per port per second ( extern __xdata uint8_t stp_pflags[10]; extern __xdata uint32_t stp_pcost[10]; /* path cost; 0 = auto (20000) */ -extern __xdata uint8_t stp_pprio[10]; /* port priority (default 0x80) */ +extern __xdata uint8_t stp_pprio[10]; +extern __xdata uint8_t stp_pp2p[10]; /* admin point-to-point: 0 auto, 1 on, 2 off */ + +/* Last-heard designated info per port (from received BPDUs); consult + * stp_bpdu_age to decide whether it is still current. */ +extern __xdata struct bridge stp_dbridge[10]; +extern __xdata uint16_t stp_dpid[10]; +extern __xdata uint32_t stp_dcost[10]; +extern __xdata uint16_t stp_bpdu_age[10]; /* ticks since a BPDU was heard */ /* port priority (default 0x80) */ /* ---- Status, exposed read-only for the web UI (send_stp) ---- */ extern __xdata struct bridge root_bridge;