Add an implementation in 8051 assembler for the generation of a
ChaCha20 keystream and an encryption function using the keystream
in C.
A test function using the example from RFC7539 is provided in
chacha20_test.
Keys, nonce, and block counter and the pointers to the input plaintext,
its length and the output pointer are provided via a struct chacha20,
which also holds the working state of the ChaCha20 algorithm
Move port_bit() to rtl837x_common.h as PORT_BIT(), a uint16_t shift,
and MAC_ADDR_LEN from rtl837x_lldp.h next to it, and use MAC_ADDR_LEN
for every address in the LACP code instead of a literal 6. Define
lacp_mux_update() before its first caller and drop the forward
declaration, declare variables at the top of their blocks, put the
operator at the end of a continued line, drop the field value comments
of the LACPDU struct, and leave lacp_port_lag to lacp_init() instead of
initialising it in the test environment.
The trap is described with STP, so the LACP page only says that it is
used. The LACP sections leave link_aggregation.md, which now points to
lacp.md, and the timing section gives the values in seconds and the
802.3ad requirements without the test history.
The comments repeated the documentation, described tests or explained
the code to its author. What stays describes the LACPDU structures. The
timer constants are decimal, in units of the LACP timer.
The LACP test now compiles with the shared harness flags and
sdcc_shim.h against the firmware headers, so the copies under
test/lacp/shim are gone. test_port_tables already covers the trunk hash
seed that test/port_hash checked against its own register copies.
lacp_in() declares its locals at the top, which the stricter host build
needs.
The port and group of lacp_in(), the member mask passed to
lacp_lag_set() and the compiler temporaries both functions spilled took
six bytes of DSEG, and with them six bytes of stack. The locals and the
mask live in xdata now, the partner fields go through the scratch word,
and lacp_lag_set() records the new group of a port before it releases
the old one. One byte is left: the group number passed in.
lacp_in() always leaves uip_len at 0, so the tcpip_output() after it
never ran. The LAG page names the active members by front-panel port
instead of a logical mask. The partner key is not part of selection;
say so under Known limits.
Listing by next-address also returned the reserved-multicast entries STP
writes, shown as port 1 with a delete button, while l2_delete() still
looks up by next-unicast and so removed the unicast entry after that
index instead. The change was there to show the LACP steering entries;
delivery by trap writes none, so it goes.
Both paths wrote the trunk registers directly and left the protocol
running on the group, so "lag show" said disabled while the partner kept
both links in one aggregate, and re-sending the LACP line did not repair
it because the same-ports shortcut returns early. Each path now releases
the group from LACP first; for a group that never ran LACP that is a
no-op.
The receive machine armed current_while from the partner's TIMEOUT bit,
so against a partner on the slow rate a port whose LACPDUs stopped with
the link still up stayed in the trunk for the long timeout, two minutes.
802.3ad 43.4.12 starts the timer from the actor's own LACP_Timeout, and
we always advertise the short one: the partner sends every second and is
let go after the short timeout.
Sandbox T23 plays a slow-rate partner that goes quiet and fails on the
code before this.
Dynamic link aggregation on top of the static LAGG, for RTL8372/8373:
receive, selection and mux state machines (coupled mux, one aggregator
per group), with "lacp show" to look inside. Each of the four trunk
groups runs its own aggregator on the candidate ports assigned with
"lag <n> lacp <ports>"; the engine starts with the first such group and
stops with the last.
LACPDUs reach the CPU through the reserved-multicast trap, so delivery
needs no L2 entry and nothing written per VLAN or per PVID. The trap
acts on the address before the VLAN lookup and before the ingress
filters, so a LACPDU also arrives on a port that admits tagged frames
only and on a port STP holds in blocking, neither of which a forwarded
one reaches. A trapped frame carries no VLAN tag,
so lacp_in() takes the offset of the PDU body from TRAP_RX_BODY(). The
action is set with trap_rma_set(), which leaves the flags sharing that
register alone, and "lacp off" puts the slow-protocols drop default
back. doc/lacp.md and doc/link_aggregation.md record what was measured,
including why the trap looked like a dead end at first.
The web UI gets a Static/LACP mode per group on the LAG page with live
status, and the startup-config validator knows the per-group syntax. A
host-side sandbox for the state machines runs in the test harness
together with the LAG hash check. It drives both forms of an incoming
LACPDU, so the body offset of a trapped one is covered as well.
The L2 table page iterated with the "next unicast" read method, so
static multicast entries such as an IGMP group were invisible. It now
iterates with "next address", skips L3 entries (their layout carries IP
addresses where a MAC is expected), and reports the lowest member of a
multicast entry's port mask, so such an entry shows up as a static
entry on that port.
A port taken out of LACP forgets its partner, so "lacp show" after
"lacp off" no longer reports one. Applying the same candidate set to a
group again, as the LAG page does on every Apply, leaves the trunk
alone instead of tearing it down for a second or two. lacp_in() checks
the frame length and ethertype before reading a LACPDU, so a runt or a
foreign frame on the slow-protocols address cannot leave stale buffer
bytes as a partner. The sandbox covers these three in T20-T22. On the
LAG page, LACP mode with no ports checked sends "lag N lacp off", and
the startup-config validator wants at least one port after "lacp".
A reserved-multicast trap goes to the port named in EXT_CPU_CTRL, which
names no port out of reset, so trapped frames were dropped at ingress.
trap_init() points it at the CPU port once at startup, and
trap_rma_set() changes only the action field of an RMA register.
A trapped frame arrives without the VLAN tag a forwarded one gets, so
TRAP_RX_BODY() gives the offset of the payload for both forms.
STP now traps its address while it runs instead of steering it with a
static L2 entry per VLAN, so turning it on no longer walks the VLAN
table and a pvid change no longer needs stp off and on. With STP off
the flooding entries are restored as before.
A trapped BPDU passes the tagged-only ingress filter that drops a
forwarded one, so the stp_setup() warning about such ports goes.
Opcode 0x48 outputs 8 dummy clocks between the address and the first data
byte. flash_configure_sio() sets DUMMYCYCLES to 0 and flash_read_security()
never raises it, so the controller captures the dummy slot as the first of
its four data bytes while the address still advances by four. The printed
output therefore shows 0xff in place of every fourth byte and skips one
content byte per transfer. flash_read_uid() already sets DUMMYCYCLES for
opcode 0x4b, which needs the same allowance.
The answer to a lookup arrives after the command has returned, so the
console of the web interface never shows it. Plain "dns" now prints the
name and the address, or whether it is still pending or failed.
Needed to increase buffer size for this to send app.js
The logical port status is what is returned in the array, for
non-sequential systems we need to map through physToLog rather
than just subtracting 1
physToLog is 1 based! not 0; also fixing up typecasts
Reimplementing using an array of objects to represent ports.
The previous approach of a bitmask of logical ports, was just too
simple on devices such as the SWGT024_V2_0_MANAGED which has
gaps in the port assignments.
timing with ticks
Fixing up the log_to_phys_port assignment
2000 is about every 10 seconds, so scaling up to once every 30ish
Removing the attempt to block lldp frames,
This does not work however the hard work has been done for us in
the #514 so we can borrow that work later on
boot_to_html outputs 1 and 0 not true and false, making the sim match
the real implementation.
lldp.json has a hanging , in the array; so overwriting it
Moving the global on/off onto the LLDP page
Building a table of LLDP status from the S ports. Later on, if
we wish to implement an lldp receiver we can display the remote
details here
Adding support to the cmd parser for the lldp commands
to each port once every 30 seconds.
* Additionally fixes the on-ward passing of LLDP frames through
the switch
The ports on the case are marked 1-8 so this should reflect that
rather than being 0-based
We can send the hostname as well, this can be dynamically allocated
Let's send the model as the system description as well
igmp_setup(), which `igmp off` also calls, enabled the lookup of IPv4
multicast in the L2 table. Groups learned while IGMP was on therefore
kept going only to the ports that had reported them, and with reports
no longer trapped nothing updated those entries until a reboot.
The lookup is now enabled in igmp_enable() and disabled in igmp_setup(),
so with IGMP off all IP multicast is flooded again.
The switch keeps flooding 224.0.0.0/24 with IGMP on, but a report for a
group in that block still made an entry for it, and the entry limited
the group to the ports that reported it. A host announcing mDNS on
224.0.0.251 thereby cut mDNS off from every other port. Such reports
are now ignored, as RFC 4541 asks.
leds_dump() printed the pin mux, the LED enables and every LED set on each
boot, from BANK2. It now lives in its own file in BANK3, where there is
room, and runs only on the new "leds" command, next to gpio and sds, so it
stays at hand for bringing up a new board without costing BANK2 or boot
output.
A matrix of the ports, one row per ingress port, showing which ports a
frame entering there may leave through. The port number of a row turns
red when the port is not fully open, and the cell for the port itself is
marked since it is always allowed. Apply sends one "isolate" command per
changed row, and Allow all ticks the whole matrix.
The switch side is a new /isolation.json, which reports each port's
allowance mask read back from the hardware. The page is an Isolation
section of the single-page UI.
494320b removed strcpy() from rtl837x_flash.c but left its declaration
in rtl837x_common.h. Since then the linker has taken strcpy from the
SDCC library instead, which expects the destination as a generic pointer
in DPL, DPH and B. The callers pass an __xdata pointer and leave B as it
is, so the copy lands wherever B happens to point. boot.c only gets away
with it because B is zero there; a call with B set, as from the ChaCha20
test in #207, writes into internal RAM and stops the 8051.
With several switches open, every tab was called "Switch". The
dashboard tab now shows the hostname, and the login page shows the
hostname seen at the last login on that address (kept in localStorage,
which is per switch), or the address itself before the first login.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The firmware page accepted any valid image, also one built for a
different board whose SFP, LED and button pins differ. Every image
carries its machine name as a NUL-terminated string, the same one the
switch reports as hw_ver; when the selected image lacks it, the page
now warns below the check result and in the confirmation dialog. The
upload stays possible on purpose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The V3.2 board has the same component placement as V3.1 and works with
its machine definition: RJ45 links at 2.5G, LEDs, SFP detection with DOM
readings at 10G, factory MAC at 0x1FC000 and the reset button. The new
target only reports "V3.2" as the machine name.
Installed from the original firmware V100.9.25 through its web
interface with the OEM installer image. Reading the flash in-circuit with
a SOIC-8 clip gave the chip ID EF FF FF, as on a V3.1 board.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #494. Storm control (#455) was merged after it and brings
three objects in code space without const: the extern declaration of
machine in rtl837x_storm.c and the name tables storm_types and
storm_names. SDCC 4.6.0 reports them as warnings 356 and 357, which
fail the build with CI=1 (--Werror). SDCC 4.5, which the CI uses, does
not report them.
The declarations now take the form used in the rest of the tree, as in
mime_strings in httpd.c. The images for KP_9000_6XHML_X2 and
KP_9000_9XHML_X_V3_1 are byte-identical before and after.
Adds a German dictionary to LANG in app.js (all 317 keys, placeholders
unchanged), the language selector entry in index.html and the login
strings in login.html. Uses Swiss spelling (ss instead of ß), which
reads correctly for all German speakers.
Checked against a mock of the httpd endpoints: every section renders
with the German strings; no layout changes compared to English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only frames to 01:00:5e:00:00:16 reached the IGMP handler, so v1/v2
reports, which go to the group address, were never seen. The handler
also took the group from the first v3 group record for every type.
Pass all IPv4 multicast to the handler, which takes the IGMP header
from the IP header length, the group from the IGMP header for v1/v2
reports and v2 leaves and from the first group record for v3, and
returns 0 for anything that is not IGMP so it goes on to the IP stack.
The message and record types have names now.
Storm control used meters 0 to 39, the same meters the ACL hands out,
so a storm limit and an acl meter could overwrite each other. It now
uses 24 + port * 4 + type, meters 24 to 63.
parse_storm() is no longer reentrant and uses plain locals, looks the
type up in a while loop and returns straight from the one-line cases.
storm_show() uses plain locals too.
The documentation says how IGMP decides between mcast and umcast, how
to get from a meter index to its limit, where the IFG bit of a meter
is, and what an ingress bandwidth limit on the same port does to the
storm limit.
The switch has no battery backed clock, only a seconds counter that starts
at zero on every boot. An SNTP client keeps UTC as the time of the last
answer plus the seconds counted since: "ntp server <name|ip>", "ntp interval
<minutes>", "ntp timezone <+hh:mm>", "ntp dst off|eu|us", "ntp on|off",
"ntp" and "time". A server given by name goes through the DNS resolver, an
answer from an unsynchronised server is ignored, and a failed attempt is
repeated after 30 seconds.
The local time adds the time zone offset and, while the European or United
States rule says so, one hour. The date is split with a shift and subtract
division and a walk over the years, so no 32-bit division from the library
ends up in the common area.
The System page gets the NTP settings and shows the local time.
A small resolver for IPv4 address records, so that clients such as NTP can
be given a host name: "dns server <ip> [<ip>]", "dns lookup <name>" and
"dns". Without a configured server it uses the one DHCP handed out. A query
goes to the servers in turn every two seconds and gives up after six tries;
answers with compressed names and CNAME records ahead of the address are
understood. A name that already is a dotted address is taken without a
query.
The resolver sits in BANK3, the command line in BANK2 next to the helpers it
uses. The System page shows and sets the servers.
The image rule no longer passes -p, so fileadder fell through to printing
html_data.h and html_data.c on stdout on every image build. The listing is
only useful when fileadder is run without an image, to see what it would
generate.
The .bin rule passes -p html_data to fileadder, which rewrites
html_data.c and html_data.h after the objects that include them have
already been compiled and linked. The next make therefore rebuilds
html_data.c, httpd.c and page_impl.c even when nothing changed, and
so does the one after that. The rule only needs to embed the files
into the image; the header comes from the html_data.c rule above.
Spotted by plaes in #358, which cannot be merged as it stands.
With this and the previous commit, a second make after a full build
compiles nothing, and the image is byte for byte the same as before.
Two things kept the same source from producing the same image.
fileadder walked the input directory with readdir(), which returns entries
in whatever order the filesystem hands them over. The same tree therefore
embedded the files in a different order on a different filesystem, and the
image differed byte for byte. scandir() with alphasort() fixes the order.
version.h was listed in .PHONY, which makes it permanently out of date and
propagates that to everything including it, so a plain build recompiled the
four largest translation units even when nothing had changed. Dropping it
from .PHONY alone would go too far the other way: with no prerequisites the
recipe would never run again and the version string would freeze at its
first value. A FORCE prerequisite keeps the recipe running every time, and
the generated header is only moved into place when its content actually
differs, so the timestamp survives an unchanged rebuild.
Measured on this tree: with FORCE the version follows GIT_VERSION and an
unchanged rebuild leaves version.h untouched; without it the header keeps
the value from the first build.
Restore the sy_igmp and sy_services_note lines to their original
indentation so they do not show up as changed in the diff; the new
session timeout strings move to their own lines. Also add the es
strings, which were missing after the merge with main.
test_port_tables and test_page_json link httpd/page_impl.c unmodified,
which now references session_timeout. Define it in support.c next to
the other firmware globals so the tests link again on current main.
The server timeout is an inactivity timeout: scan_header() refreshes
last_session_use on every authenticated request. Max-Age counts from
login instead, and the cookie is only set there, so the browser deletes
it session_timeout seconds after logging in however busy the session
is. Leave expiry to the server, which already handles it.
The five-digit itoa16_html() stays: information.json must print
timeouts from 10000 to 65535 correctly.
rtl837x_port.c prints "called" and "done" around mirroring, PVID, VLAN
and L2 setup, and dhcp.c around discover, request, start and stop. They
land on the console on every boot and config replay, and in the log
once syslog is running. The VLAN line keeps the VLAN, members and
tagged values, only without the function name in front.
On the KP_9000_6XHML_X2 this takes 359 bytes out of BANK1, 88 out of
BANK2 and 24 out of BANK3. BANK1 goes from 431 to 790 bytes free.
dhcp_addopt_request_ip() and dhcp_addopt_server_id() both end with a
copy of four bytes of our MAC to the current position, without moving
the position on. The next option always starts there, so those bytes
are written over before the packet goes out and never reach the wire.
The packet stays the same without the copy, and BANK3 is 102 bytes
smaller on the KP_9000_6XHML_X2.
ip_opt() copies four bytes whatever the option length says, so a
subnet, router, DNS, server id or broadcast option shorter than four
bytes stores the start of the next option as the address. long_opt()
does not look at the length at all and always moves on by six bytes, so
a lease, renewal or rebind option of another length puts the parser out
of step for the rest of the packet.
Both now read the length. An address or time shorter than four bytes is
skipped, a longer one is taken from its first four bytes and stepped
over whole, and a skipped time leaves the previous value alone.
Found in the code, not on the wire; no server here sends such options.
BANK3 is 47 bytes smaller on the KP_9000_6XHML_X2.
The main loop drains the NIC only after an RX interrupt, at most
RX_BUDGET frames, then clears the status and waits for the next one.
When frames are left and the buffer reaches the RX stop threshold, the
NIC takes no more frames and raises no further interrupt, so reception
stops for good.
Check the buffer after clearing the status and keep draining while it
holds data; the interrupt is re-enabled only once it is empty.
rtl8224_enable() waits 2.5 s after releasing the RTL8224 reset before
anything talks to the PHY. On an SWTGW218AS the chip id already reads
back 0x8224 at the first poll, about 265 ms after the release.
Remember when the reset was released and, once SMI is up, poll the chip
id until it answers, giving up after the same 2.5 s as before. The PHY
writes that follow read back the same as before.
The reset pulse before it goes from 0.5 s to 20 ms, twice the 10 ms the
related RTL8372 and RTL8221B need. The pulse does reset the chip: the
MDI reverse register (MMD 30, 0x0a90) still reads its power-on value
0x00f3 just before the init writes 0x00fc, as it does with 0.5 s.
Together the console banner comes 3 s sooner.
The SerDes write loop in phy_config_8224() waited on the busy bit with
no limit, so a PHY that never answers stopped the boot before the
network came up. Bound it and report a timeout.
The name branch read the fourth word without checking that the line had
one, and the tokenizer does not reset unused word slots, so "port 5 name"
took the name from wherever the previous command's fourth word had been:
"port 5 name hello" followed by "port 5 name" gave "hello" again, and
after a longer line it gave the tail of that line. The web UI never sends
the form, the console can. With no argument the name is now empty, which
also gives the console a way to remove a name.
The receive interrupt was wired up but never used: EX1 was enabled, the
handler re-enabled itself and did nothing, and the mask bit for it in the
NIC stayed clear, so idle() read the receive buffer register on every
pass to find out whether a packet had arrived, and slept until the next
tick when none had. A packet that arrived during that sleep waited for
the tick: 2.2 ms on average, 5 ms at worst, for every frame the CPU
handles.
Now the handler latches rx_irq and masks itself, and idle() sleeps
unless a receive, a link change, a console command or the tick is
pending. On rx_irq the loop drains the buffer, clears the status bit and
re-enables EX1, in that order: the NIC holds the line as long as the
status bit is set, and the bit does not clear while the buffer still
holds a frame, so a frame that lands between the last read and EX1 = 1
raises the interrupt again rather than getting lost. The other order,
clearing before draining, left the line asserted and produced an
interrupt on every pass. The mask is written after nic_setup(), which
resets it.
Measured on an RTL8373 board with the same counters in both images,
clocked by the tick counter, traffic from a host behind an LACP trunk:
```
polling interrupt
idle, register ops/s 788 79
25 pps, RTT avg/max 2.19/4.96 0.44/0.75 ms
500 pps, RTT avg/max 2.78/5.06 0.39/5.40 ms
500 pps, loss 0.08% 0.08%
```
The handler compiles to clr EX1 / setb rx_irq / reti.
The timer interrupt now raises a tick flag too, so idle() no longer works
out TICKS_DUE twice per pass, and cmd_available moves from xdata to a
bit. The four wake-up flags sit in the low bits of one byte at 0x20, so
the sleep test is a single read of that byte. tx_buf_empty deliberately
stays out: it is 1 when there is nothing to do, so including it would
keep the sleep test from ever firing.
idle() ran everything on every pass: the seconds block, the SFP pin
test, the TCP periodic timer behind a gate of its own, the STP timers
behind theirs. The timed work moves into handle_tick(), which idle()
calls once per system tick from a single gate; handle_tx() loses its
private one.
handle_sfp() ran on every pass as well, two GPIO reads per pass to learn
that nothing changed; it now runs every ten ticks. A module inserted is
still seen within 50 ms, and the read itself has been off the loop since
the wake-up wait moved to the tick.
No change in behaviour otherwise: the link, receive and console paths
stay where they were. SWTGW218AS: code +45 B, xdata +1 B, stack
unchanged.
The health checkpoints follow the work they measure: the seconds block,
SFP, TX and STP phases are taken inside handle_tick(), the link, RX and
command phases in idle().
The link-change interrupt was set up at boot (mode 0x5f84, mask 0x5f34)
but never enabled: the handler did nothing and did not clear the event in
the switch, so EX0 stayed commented out and idle() read the two link
registers on every pass instead, 400 reads a second on a quiet switch.
The handler now only latches a flag and masks itself; the register access
macros build a write in the shared SFR data bytes, so an interrupt that
touched the switch in the middle of a write from the main loop would
corrupt it. idle() clears the event register (write-one-to-clear, 0x3ff
for the ten ports), re-enables EX0 and then reads the link state, in that
order, so a change that lands during the read raises a new edge. The flag
is also set once a second from the seconds block, so a board whose INT0
line turns out not to be wired keeps detecting link changes, a second
late at worst. Boot drops the events pending from bringing the links up
before the mask is set.
Measured on an RTL8373 switch before this change: 0x5f88 held 0x3fd, nine
ports had reported a change nobody read. With the change a counter in the
loop went 2 -> 3 -> 4 on "port 5 off" / "port 5 on" and stayed at 4 over
three reads without events, so the event is delivered, cleared and
re-armed, and there is no flood.
SWTGW218AS: code +57 B, xdata 0, stack 151 -> 150 B; the flag is a bit
variable. The handler compiles to clr EX0 / setb link_irq / reti.
idle() also keeps in a bit what it saw in NIC_RX_BUFF_DATA when deciding
whether to sleep, and the first iteration of the receive loop uses it
instead of reading the register again a moment later. Between the two
only the link check and handle_sfp() run, and neither can make a queued
packet disappear. Every later iteration reads the register as before, so
the budget loop behaves as it did. This part was not measured on
hardware: one register read per pass needs a throughput run to show up.
SWTGW218AS: BANK1 and BANK2 +6 bytes each, BANK3 unchanged.
stp_timers() ran every fourth pass of the main loop and stp_clock counted
passes, which only measures time while a pass is one tick long. It never
quite was: a pass is as long as the packets it handles, and #482 now takes
up to four of them, so under an IPv4 stream a pass is about 16 ms and STP
time runs at a third of its rate, with hellos 6 s apart and a root aged out
after a minute. Since #483 the loop also stops sleeping while packets wait,
so under a flood of cheap frames passes come faster than ticks and STP time
runs fast instead, shortening the forward delay in exactly the situation a
loop produces. uip_periodic() had the same problem from the other side: it
ran once per pass, so the retransmit timer sped up with the loop.
Both now go by the tick counter the timer interrupt keeps. The STP block
catches up on ticks a long pass missed, at most eight steps at a time, and
handle_tx() runs its periodics once per tick at most. One byte of xdata
more, 48 bytes of code.
A host test drives the gate with one pass every 5 ms, every 16 ms and
every 0.6 ms for ten seconds each; the STP clock lands on 500 steps in
all three, where counting passes gave 156 and 4166. A pass of 1.3 s loses
time rather than firing 65 steps at once, and the arithmetic survives the
tick counter wrapping.
The advertisement written for a forced speed of 1G and above is 0x1001,
while auto and the forced 10M and 100M modes all carry bit 10, the PAUSE
capability. Forcing a port to 1g or faster therefore takes pause off the
link: the advertisement narrows and the far end has nothing left to
agree to.
Measured on a SWTGW218AS through phyget and physet, on a port with no
link so nothing was disturbed. Auto reads 0x15e1, port 1 1g reads
0x1001, port 1 auto reads 0x15e1 again, and writing 0x1401 by hand
sticks. That bit 10 is the pause capability follows from a second
reading: two ports advertise the same 0x15e1 from this side, and pause
is active only against the partner whose own advertisement carries the
bit.
Reported by feelfree69 in #489.
The .d files track headers, so adding or removing a flag on a tree that
was already built leaves every object that did not change for some other
reason compiled with the old flags, and the image ends up a mix of the
two.
How that shows depends on the flag. DIAG=1 fails the link on undefined
symbols, which at least stops the build. An opt-in whose hooks expand to
empty macros loses the calls instead, so the link succeeds and the
feature quietly measures nothing.
Objects now depend on a stamp holding CC_FLAGS, rewritten only when the
flags differ, so an unchanged flag set still does not force a rebuild.
Two per-port MAC status registers that nothing in the firmware reads:
0x6408 says which end is master on a BASE-T link, 0x63FC and 0x6400 say
whether pause was negotiated in each direction. port <n> show prints
both at the end.
The read sits in rtl837x_port.c rather than phy_show(), because these
are MAC registers and phy_show() is skipped for SFP ports. Master/slave
keys off machine.is_sfp[] so it stays off the fibre ports, where the
notion does not apply.
Bit N of each register is chip port N, which is not how
RTL837X_REG_LINKS_STS indexes its bytes a few lines above in the same
header.
edc469f made max_upload count, which bounds how many bytes a request may
carry. Nothing bounds where those bytes land. The two are not the same
check: the first watches the request, the second watches the flash address.
uptr walks forward one page at a time from FIRMWARE_UPLOAD_START and has no
ceiling. A body whose closing boundary never arrives keeps writing page after
page past the staging region, and on a flash exactly twice FIRMWARE_UPLOAD_START
the address wraps onto the running image at zero, so a bad upload takes the
working firmware with it. That commit's own message notes the related hazard:
flash_write_bytes() carries the address in three bytes and drops what does not
fit.
Bound it where uptr actually moves: refuse the next page once it would leave
the area check_and_flash_update_image() reads back, and answer 400. A partial
image left behind is harmless - boot rejects it on CRC and erases it.
This rebase drops the part of the original patch that removed max_upload;
the length limit and the address limit now sit side by side.
uip-neighbor.c is the IPv6 neighbour cache. Its only caller is the neighbour
solicitation handler in uip_process(), which sits inside #if UIP_CONF_IPV6,
so on an IPv4 build the module is linked without being reachable.
Guard it on the same macro as its caller.
Worth recording separately: the module does not currently compile with
UIP_CONF_IPV6 set. uip_ipaddr_cmpx() is defined only in the non-IPv6 half of
uip.h, and uip_neighbor_add() calls it, so an IPv6 build stops with an
implicit declaration. That is true before this change as well, and is left
alone here.
On SWTGW218AS, on top of the forwarding change, the stack gains a further 12
bytes to 172 available and BANK1 drops another 714 bytes to 46080.
uip-fw.c forwards packets between several network interfaces. Nothing in
this firmware calls it: the only references to uip_fw_output() outside the
module are commented out in uip-split.c. It is compiled and linked all the
same, and its register pressure lands in direct RAM, which on SWTGW218AS is
already reported as having no spare space.
Add UIP_CONF_FORWARDING alongside the existing UIP_CONF_BROADCAST and build
the module only when it is set. A device that forwards turns it on and gets
the module unchanged; a single-interface device stops paying for it.
On SWTGW218AS the stack gains 9 bytes, from 151 available to 160, and BANK1
drops 1200 bytes, from 47994 to 46794.
Add support for the F7008-2.5 board (PCB F7008-2.5GPOE-V1.2), an
RTL8373N + RTL8224N design with eight 2.5GBASE-T ports and one SFP+ cage.
The LED pads are not wired in port order, so the generic DEFAULT_8C_1SFP
target lights the wrong lamp on this board. The pad to lamp map was
measured on the hardware and is documented in doc/devices/F7008-2.5.md.
The green lamp marks a 2.5G link, the yellow lamp blinks while data flows.
The counters, the phase hooks in idle() and the "health" command are now
behind HEALTH, set from the command line with HEALTH=1. Without it the
hooks are empty macros and the command does not exist, so a default build
carries none of it: on SWTGW218AS that is 1065 bytes of bank 2, 101 of the
common segment and 28 of xdata, plus seven banked calls per main-loop pass.
It is a developer's tool. Reading the dump takes knowing the main loop, and
a user who needs it for a report can build with the flag on, which is how
the kernel treats its lock and latency instrumentation as well.
The tcp slot dump printed the slot index behind `if (UIP_CONNS > 1)`, and
with one connection configured that is a constant-false condition around
live code: sdcc reported two "unreachable code" warnings for the body and
one "conditional flow changed by optimizer" for the loop that could only
run once.
Print the single slot directly and state the assumption as a build-time
check, so a build that raises UIP_CONNS fails loudly here instead of
quietly labelling every line as slot zero. Builds warning-free again.
What each line of the dump means, how two dumps give rates, and the
two things worth knowing before trusting the numbers: the command
inflates its own cmd phase counters by the cost of printing, and the
untouched figure is a high-water mark, not a live level.
Every probe of a misbehaving switch so far measured it from the
outside. This puts the witnesses inside.
Each idle() pass increments a loop counter and stamps a checkpoint
after every phase: link polling, SFP, RX, TX, STP and the command
dispatch. A phase spanning two or more system ticks counts as slow and
its worst time is kept, in 5 ms units. handle_rx counts frames, so the
dump separates a flooded CPU from a stalled loop.
At boot the free stack area is painted, preserving the interrupt state
around the painting, and the dump reports how much of the pattern
survives next to the live stack pointer. The TCP connection table is
printed whole: state, timers, retransmissions, ports and peer, next to
the httpd transfer state, so a permanently occupied slot names its
occupant. With more than one slot configured every slot is listed.
Everything is raw hex; two dumps a known time apart give the rates.
The implementation sits in cmd_parser next to the other command
handlers and needs no storage beyond its counters.
handle_sfp() called sfp_module_read() as soon as it saw a module appear, and
that path opened with delay(100). delay() spins on PCON until the tick counter
runs down, so the main loop stood still for 100 ticks - half a second at
SYS_TICK_HZ 200. Nothing else ran in that window: no STP, no LACP, no packet
handling. A diagnostic build measured the sfp phase at up to 107 ticks, the
longest stall of any phase in the loop.
Remember when the module showed up and let the loop continue. The I2C read
happens on a later pass, once the wake-up period has elapsed. Pulling a module
cancels a pending read, and a failed read still restores the detect bit, so the
existing retry-on-the-next-poll behaviour is unchanged.
The comparison is written out rather than using a helper so that this does not
depend on anything outside the current tree. The pending flag is an array
indexed by slot, like the other per-slot state in this file.
Cost on SWTGW218AS, measured against the branch point: BANK3 11173 -> 11264,
CSEG, BANK1 and BANK2 unchanged, plus 4 bytes of xdata.
Some generic 1.25gbps DAC cables were seen to have a NUL *after* the
space padding in the model field. This caused the normalization to miss
the space padding in the string. The vendor field did not have the
issue.
Note that this seems to be against spec per SFF-8472, which mentions
only space padding or *all* NUL, not a combination of both. In the case
of all NUL, we'll waste a few cycles with no-op writes.
Building with SDCC 4.6.0 emits tons of warnings e.g.:
cmd_parser.c:31: warning 356: object in read-only code space should be const
cmd_parser.c:36: warning 357: pointer to object in read-only code space should be pointer to const
These of course become fatal when building with --Werror.
Comparing resultant binaries, it seems that a single string is shifted
(deduplication?) but the numbers emitted at the end of the build are
otherwise identical.
idle() went to sleep before it looked at the receive buffer, so a packet that
arrived just after the loop dozed off waited for the next tick before anything
touched it. Every TCP round-trip paid that toll, which is why the web UI felt
sluggish even though no phase of the loop was anywhere near busy.
Check the receive buffer first and only sleep when it is empty. The read takes
its result straight from SFR_DATA_U16 rather than going through reg_read_m()
and the sfr_data[] copy, which is what the rest of the tree already does.
Measured on SWTGW218AS with this change alone:
web UI, median of 15 requests for the main page 88 ms -> 76 ms
CPU port under a one-way UDP stream 159 -> 230 packets/s
The loop still sleeps normally when there is nothing to do, so this does not
turn the core into a busy-wait: loops/ticks stays at 0.99 on an idle switch,
and rises above 1.0 only while packets are actually queued.
Costs 12 bytes of code and no extra XDATA.
handle_rx() picked up exactly one packet per iteration of the main loop. Since
the loop sleeps until the next tick, that capped the CPU port at roughly one
packet per tick no matter how little work it had to do. A burst of broadcast,
ARP or IGMP traffic aimed at the CPU queued up behind that and pushed out the
STP and LACP timers.
Drain up to RX_BUDGET packets in one pass instead. The budget matters: without
one a sustained flood would spin here and starve everything else in the loop.
The two register reads in the loop take their result straight from
SFR_DATA_U16 rather than going through reg_read_m() and the sfr_data[] copy,
which is what the rest of the tree already does.
Measured on SWTGW218AS with a one-way UDP stream, counting the packets the CPU
actually processed, with this change alone:
before 159 packets/s
RX_BUDGET 4 245 packets/s, loop iteration ~16 ms
RX_BUDGET 8 298 packets/s, loop iteration ~26 ms
Throughput is already most of the way there at 4, so the larger budget mostly
buys a longer worst-case delay for the other phases. STP stayed responsive in
both cases.
Costs 71 bytes of code and no extra XDATA.
All the possibilities are handled, or otherwise a compile-time macro
I2CBUS_VALID ensures invalid options are rejected with a compiler error.
Also fix a compiler warning.
Use 0x7 as error value for an invalid I2C SDA/SCL-pin value.
The macro `I2CBUS_VALID` checks for this value.
When a invalid pin is ues, the compiler errors out with:
"machine.c:<line>: error 176: sizeof applied to an incomplete type",
Error message is not ideal but at-least points to the right location in
the code.
We can reduce the i2c-settings to just one byte, and this new const value
can directly be used by the I2C-peripheral without additional computation.
This saves 1-xdata load and a lot of bit-shifting.
I see a codesize savings of +200 bytes.
The TODO next to the disabled TCP checksum code asks which bit of the RX tag
carries the result. It is bit 2 of byte 1: the hardware sets it when the L4
checksum is bad.
That answers the question for UDP as well, which was not being checked either
because UIP_CONF_UDP_CHECKSUMS is 0. Both paths now read the flag instead of
running a checksum over the payload on the 8051.
A zero UDP checksum, which is optional in IPv4, is not flagged by the hardware,
so DHCP keeps working and no extra condition is needed.
rx_headers lives in rtlplayground.c, which the host tests do not link, so
test_httpd_tx carries the storage itself the way it already carries uip_buf.
A zeroed descriptor reads as a good checksum, so the existing scenarios are
unaffected.
A new scenario sets the flag on a request and expects it counted as a checksum
error and left unanswered, then sends the same request with the flag clear and
expects it served. Built against uip.c without this change, those first two
checks fail and the two control checks still pass, so the scenario tells the
two versions apart rather than merely exercising the code.
Costs 156 bytes of code on SWTGW218AS and no extra XDATA.
The speed selector on the Ports tab was filled with its options and never
given a value, so it read "auto" on every load whatever the port was set
to. Forcing a port to 1G and reloading showed "auto" in the selector
beside a link badge reading 1G.
The advertisement bits ports.json already reports say what the port is
set to: phy_set_speed() writes the same three registers the JSON reads
back, and every forced speed leaves exactly one of the six bits standing
while auto leaves the lot. Map those to the selector, and leave it alone
while it has focus, as the MTU field does.
Values with no entry in the table, such as the 100M setting that
advertises both duplex modes, leave the selector untouched rather than
claim a setting the list cannot express.
port_eee_enable() and port_eee_disable() report the port with
print_byte(), which prints the internal index in hex, so turning EEE off
on port 5 answers "EEE off for 04". port_eee_status() in the same file
already uses print_phys_port(), and the status listing it produces counts
from one, so the two halves of the same command disagreed.
Use print_phys_port() in all three messages.
max_upload was armed once when the upload began and never reduced, so
`max_upload - uip_len > 0` was an unsigned expression that only went
false when a segment arrived whose length matched the limit exactly. The
write pointer climbed from the staging address with nothing to stop it,
while flash_write_bytes() carries the address in three bytes and drops
whatever does not fit.
Subtract each segment as it is taken and compare before subtracting, which
is what the comment there described. The limit was only ever armed for
firmware uploads; configuration uploads keep the bound their own buffer
already imposes.
The multipart branch for /config tests authenticated before it touches
anything; the branch for /upload does not, and the 401 for it is only
raised further down at the dispatch. By then the request has already set
config_upload, uptr, verify_crc, max_upload and pre_acc, and announced a
firmware upload on the console.
Those five are globals while the transfer state they steer is per
connection, and the stack is built for ten connections, so a request that
is about to be rejected can still retarget an upload another client is in
the middle of: a configuration upload continues as a firmware one and its
remaining parts are written to the staging area instead of the
configuration sector.
Apply the same test the neighbouring branch uses, before the assignments.
udp_callbacks() offers every UDP connection event to syslog_callback(),
which decides whether the event is its own by comparing the local port
against state.syslog_conn->lport. The pointer is null until syslog is
started: syslog_init() clears it at boot and syslog_stop() clears it
again, so with syslog off, which is the default, every poll dereferences
null and reads whatever lives at the bottom of xdata.
Should that word ever match the port being offered, the send path runs on
another application's connection, overwrites uip_appdata and drops the
log line it was holding.
Test the pointer first.
leds_setup() walks all four LED sets and writes the high-speed nibble of
each into one of two registers, LED3_0_SET1 for sets 0 and 1 and
LED3_0_SET3 for sets 2 and 3. The second branch reuses the index
expression of the first without rebasing it, so sets 2 and 3 address
sfr_data[-1], [-2], [-3] and [-4].
Two things follow. Sets 2 and 3 never receive their nibble, because the
read-modify-write pair writes back exactly what it read. And four bytes
below sfr_data are overwritten on every boot of every machine: the loop
runs over all four sets whatever the board declares. On this build the
linker places sfr_data directly after the serial receive ring, so the
writes land in the tail of that ring.
leds_dump() reads the same two registers back and already rebases with
(set - 2); use the same form here.
The EEE toggle posted `eee <port> <on|off>`, the reverse of the
`eee [on|off|status] [port] [100m|1g|2g5]` the parser and the
documentation describe, so the switch answered with its usage line and
the checkbox sprang back. The multi-page UI this replaced sent `eee on`
with no port at all, which toggled every port instead of one.
Moving the port to the end breaks the stored-configuration merge, which
builds its drop key as a line prefix: with the port trailing, a per-port
line matches the bare eee entry and replaces every eee line in the
configuration rather than the one for that port. Give the command its own
case beside the bandwidth one, so a line with a port replaces only that
port and a line without one clears them all, and accept the optional
speed word the command has always taken.
Reported by tofurky in issue #459.
parse_eee() sniffs word 2 for a trailing 'g' or 'm' to tell a speed from
a port number, and advances idx past the digits to do so. The port branch
then hands that advanced idx to cmd_parse_port_separator(), which expects
the first character of the number and finds the separator instead, so it
returns 0 and the command is rejected. Every other caller passes
cmd_words_b[].
The result is that no eee command carrying a port number works, in any
argument order, and the stored per-port lines a saved configuration
replays are silently dropped.
Reported by tofurky in issue #459, along with this fix.
The row sits between Hardware and Flash. The firmware sends the formatted string
rather than the raw register, since the System table prints its values as they
arrive.
Costs 186 bytes in BANK1.
The chip has a pair of thermal meters and nothing was reading them. This adds a
`temp` command that prints the current reading next to the one taken at power-on.
The result register at 0x340 splits a 19 bit value across two fields: bits 18:3
live in the low 16 bits, bits 2:0 in bits 18:16, with the sign at bit 18, degrees
in 17:10 and a fraction below. The nice part of that layout is that the degrees
fall out of a shift of the low half, so the conversion stays 16 bit.
The meter comes out of reset latched: the register keeps whatever was measured
when the board powered up and then never moves. Four samples 40 s apart came back
identical down to the raw ADC, and 0x340 matched the power-on register at 0x350.
Clearing bit 2 of the control register at 0x338 during init is what makes it live.
Measured on an SWTGW218AS: 58.7 to 59.5 C while running, with the power-on
register still showing the 49.2 C it started from. For what it is worth, the
scale lines up three ways: the threshold register reads -40 and +125, the slope
from warm-up works out at 4450 LSB/C, and the slope between the two meters at
4388 LSB/C.
We only read the meter that is up after boot. The other sits in reset with no
coefficients, so it would just repeat its power-on value.
Costs 231 bytes in BANK2.
The helper landed between that comment and the function it describes, so the
comment now reads as if it belonged to name_char(). Moving the helper up puts
the two back together. The generated code is unchanged.
gcc flags the else whose only statement is dbg_string(), because that macro
expands to nothing unless DEBUG is on, leaving the branch with an empty body.
The line is older than this branch; the bench is simply the first thing that
compiles httpd.c with a host compiler and -Wall -Wextra.
Turn warnings into errors in the test Makefile too, so the next one is caught
where it appears instead of scrolling past. It is the only warning the four
test binaries produce, so nothing else has to change.
The replay takes a line of at most 126 bytes, so a longer one is skipped
on boot. Neither the upload path nor the editor looked at line length,
so the file the user saved was not the configuration the switch would
come up with.
Reject such a body in config_take(), before the sector is erased, which
the upload path already maps to 400. The editor points at the offending
line and its length, and measures it in bytes rather than characters, so
a name written in a non-ASCII script is counted the way the switch
counts it.
execute_config() replays the saved configuration line by line into
cmd_buffer. A line that does not fit, or one cmd_tokenize() rejects,
ended the replay right there: everything below it was silently left
unapplied, while the file itself and the editor showing it stayed
complete. On a switch here, a 130-byte line placed after the last real
setting was enough to lose every line that followed it on the next boot.
Report the offending line and carry on with the next one. The skip flag
lives across the flash page refill, so a long line that crosses a page
boundary is not resumed halfway and run as a truncated command.
Drives the unmodified httpd.c and uip.c through a handshake and a GET
from a client that moves its receive window, and compares the bytes that
reach the wire with the file they are meant to carry. The served file
holds a position-dependent pattern, so a repeat or a gap is reported at
its offset rather than as content that differs somewhere.
The first scenario holds the window still and must pass: it is the check
that the bench itself is sound, before the two moving-window scenarios
are believed.
uip_mss() is the limit on the next segment, not a record of the last
one: uIP recomputes it from the window the peer advertises in every
incoming segment, before the application is called. The ACK path
subtracted it from slen and added it to o_idx, and the retransmit path
handed it to uip_send(), so whenever the window moved between a segment
going out and its ACK coming back, the file being served lost or
repeated the difference. A window shrinking from 1460 to 600 between the
two repeats 860 bytes under fresh sequence numbers, which a browser
writes into the file; a window opening the same way skips 860.
Remember the length in tx_send() and use it in both places. The
retransmit path then repeats exactly the segment uIP still counts as
outstanding, which is what uip_connr->len makes it put on the wire.
uip_appdata is void *, so p - uip_appdata subtracts pointers of
different types. SDCC accepts it, C does not, and no host compiler will
build the file for a unit test. Cast to the type the byte count is meant
in; the generated firmware code is unchanged.
The vendor, model and serial strings are filtered while the EEPROM read
fills the buffers, so the page copies them out as they are. NUL still
ends a string, so a field shorter than its slot reads the same as before.
The three loops in send_status() become one call each. strcpy() cannot
return the length: rtl837x_common.h hides that prototype under
RTLP_HOST_TEST so host builds take the C library version, and a call
that adds its result to slen does not compile there. A small static
helper does the copy instead, and bank 1 drops by 96 bytes.
The console, POST /cmd and the saved config all go through the command
parser, so one filter there covers every way a name can be set. The
hostname already did this inline; port and VLAN names now do it too.
The SFP strings still need escaping on the way out: they come from the
module's EEPROM, not from a command.
A port named NAS"1 made status.json unparsable, so the page stopped
loading, and the name survives a reboot in the saved config. VLAN names
and the SFP strings have the same problem.
They now go through a helper that replaces ", \ and non-printable bytes
with a dot, the way the hostname is already sanitized on ingest. The
length does not change, so the size checks still hold.
The helper also covers the 0xa0 byte that send_sfp_info() skipped as a
special case, so that check is gone.
Compiling these two files for the host turns up warnings that SDCC does
not print: four places mix & with | and two mix && with ||, and l2_delete
keeps a counter it never reads. The parentheses only say what the
compiler already does, so the generated code is unchanged.
counter_to_html() reads the SFRs directly for the zero test, which saves
14 bytes of bank 1.
page_impl.c and rtl837x_port.c are linked together over the simulated
engine, so /vlan.json, /vlanlist and /l2.json are checked against what
vlan_create, pvid and the L2 table hold, through the same bytes the switch
would. The L2 walk is driven the way the web page drives it, page after
page until an index repeats, which is also where the repeat of the first
entry is pinned down as the wrap marker the page relies on. The counter
test fails on the code before the previous commit.
send_status wrote the high and the low word of a packet counter one after
the other, each without its leading zeros. That reads fine until a port
passes four billion packets; from then on a low word of 5 prints as "0x15"
next to a high word of 1, and the rates the page derives from the
difference between two readings jump around. send_counters already padded
the low word, so both now share one helper that pads it only when the high
word is set, which also drops the leading "0" the counters page used to
print in front of every small value.
The switch is reached through four register functions and the SFR_DATA
registers, so a flat register file behind them is enough to run the table
code on the host. A few addresses are live: writing TBL_CTRL with EXECUTE
performs the VLAN or L2 operation at once and clears the bit, STAT_GET
fetches a counter, L2_TBL_FLUSH_CTRL drops entries. Nothing ever reports
busy, so a polling loop runs once and the tests say nothing about what
happens when hardware stalls; that question was settled elsewhere.
The header states the VLAN and L2 entry layouts on its own, so a test can
hold what rtl837x_port.c writes against a second reading of the same bits
rather than against itself. The first suite does that for the VLAN entry,
the PVID register two ports share, the static multicast and management
entries, the per-port flush and the trunk registers. Two stub headers stand
in for the ones a firmware build generates, so the harness needs no SDCC
build first.
Two things kept every register-driven module out of the host harness. The
SFR keywords were unknown, so rtl837x_sfr.h did not compile at all, and once
it did, each translation unit would have defined its own copy of the
registers. They map to extern declarations now; a mock emits the storage
once by re-including the header with the keywords redefined.
The other was memset. rtl837x_common.h hides the firmware's own prototypes
under RTLP_HOST_TEST so host builds use the C library, but nothing declared
the library versions, and the modules do call them. The shim pulls in
string.h ahead of everything, where nothing can clash with it.
The messages said Error: themselves, or usage:, or nothing at all.
cmd_error() prints the prefix now, so every rejected command answers the
same way and the strings only carry what is specific to them.
Two messages that started with Error or refusing are reworded so the
prefix does not read twice. The syslog usage text becomes one string
instead of three calls.
The session cookie carried neither Expires nor Max-Age, so browsers that
clean up session cookies aggressively (LibreWolf, for one) may drop it
while the session is still valid. Send Max-Age=session_timeout so the
declared lifetime matches the timeout already enforced by the firmware.
itoa16_html() only converted four digits, so extend it to five to print
timeouts up to 65535. That also fixes information.json, which used the
same helper and reported wrong values from 10000 s up.
Adds a numeric input next to the syslog settings to read and set the
session timeout without knowing the command. The current value is
reported in information.json, so the field also shows the default the
firmware was built with.
Costs 86 bytes of code (BANK1) and nothing in RAM.
run_cmd_body() answers POST /cmd with 400 when err_status is set, but
only the tokenizer set it. Every "Error:", "Invalid ..." and "Unknown
command" from the parsers went back as 200, so the web page reported the
command as applied, and save_cmd put the rejected line into the command
history, from where Save to flash wrote it into the startup config.
Set ERR_INVALID_ARGUMENT on every path that rejects a command: the err:
labels of the parsers, the parsers that print their own error, the
stp parser and the unknown command fallback. Warnings that still apply
the command, such as a full VLAN name table, are left alone.
Record a command in the history only when it was accepted, and let
execute_commands() report the first failure of a multi-line body, since
cmd_tokenize() clears err_status again for every following line.
Save to flash ran the stored config through the same filter as the
command log, so stored lines the page does not know were dropped. The
stored lines are now carried over as they are and only the log is
filtered.
ingress only changes the ports it lists, but the merge replaced every
stored ingress line. It now merges per port; "ingress t|u|a" still
replaces everything, and per-port settings an all-ports line already
overrode are not revived.
Write from the config editor no longer clears the command log or the
unsaved marker, since the edited text does not hold the changes made on
other pages; only Save, which merges them, clears the log.
The command log grows from 1 kB to 2 kB, which still fits the 2500 byte
output buffer, and send_cmd_log() stops at the end of that buffer.
The workflow only ran on push, so a pull request opened from a fork
never got a build, and neither did the host-side unit tests that run in
the same job.
handle_post() takes the request path from byte 6 of the segment it is
given. For the first segment of a POST that is the path, but for a later
segment of a multipart upload it is whatever part of the file happens to
sit there. The cmd and login handlers were matched before the multipart
state was checked, so a segment of a config or firmware upload with
"cmd " at that offset ran the rest of the segment as CLI commands, and
one with "login " there aborted the upload with 400.
Check for an upload in progress, and for new upload and config requests,
before looking at cmd and login. A new POST cannot match both sets of
words, so nothing changes for requests that are not uploads.
session_id is cleared at boot and only a successful login fills it in,
while the uptime counter and last_session_use both start at 0. For the
first SESSION_TIMEOUT seconds after a start, a request carrying an empty
"Cookie: session=" therefore passes is_word_x(session, session_id) and
is treated as logged in, which is enough for /cmd, /config and /upload.
Only look at the cookie once a session has been handed out.
flash_read_security() ends its loop the same way flash_read_bulk() did
before the previous commit: only through comparisons with 1, 2, 3 and 4. A length of
zero matches none of them, the following subtraction wraps on uint16_t
and the loop keeps printing until the counter comes back down to 4.
No caller passes zero today, since the only one is the CLI with a
constant length of 40, but the bound belongs in the function rather
than in whoever calls it.
flash_read_bulk() ends its copy loop only through the comparisons
len == 1|2|3|4, so a length of zero matches none of them. The
following len -= 4 wraps on uint16_t to 65532 and reaches 4 only
after 16384 iterations, writing 65536 bytes to the destination and
overwriting the whole XDATA space.
send_config() gets there with valid_len 0 whenever the stored
configuration begins with a NUL byte, which is the state left by
uploading an empty configuration file. Every later request for the
configuration page then clears the management VLAN and the STP
flag and replaces the host address, while the switch keeps running
and keeps forwarding traffic.
The session timeout was a compile-time constant (200 s), so changing it
meant rebuilding the firmware. Make it a variable that keeps the same
default and add a "session <seconds>" command, so it can be set from
the console or from the startup configuration like the other settings.
Measured on KP_9000_9XHML_X_V3_1: ROM +180 bytes (BANK1 +17, BANK2
+161) and xdata +2 bytes; the internal RAM map is unchanged.
The harness builds with the gcc the workflow already installs and runs
in under a second, so it goes before the SDCC steps and fails the build
first when a host-visible regression lands.
The firmware counts every authenticated request as session activity, and
the page polls the status every 2.5 s, so an open tab never let the
session expire. Track the last mouse, key, touch or wheel event and treat
the page as hidden once ten minutes have passed without one: the pollers
keep ticking but send nothing, the session times out in the firmware as
it did before, and the next input resumes polling, which meets the 401
and returns to the login page.
Building the firmware headers on the host showed two warnings from
machine.h: `typedef struct machine { ... };` and the same for
machine_runtime declare no alias, so the keyword only draws "useless
storage class specifier in empty declaration". Every user says
`struct machine`, so drop the keyword.
-Wno-builtin-declaration-mismatch is a gcc switch and clang reports it
as an unknown option on every build; add it only when the compiler is
not clang.
Compile selected firmware translation units unmodified under host gcc with
AddressSanitizer and UBSan, force-including a shim header that erases the
SDCC 8051 keywords, and link them against a mocked serial ring and console.
A first suite covers the CLI line editor: line entry, backspace editing,
and both long-input overflow cases (length cap and ring-pointer stall).
The only firmware-source change is one #ifndef RTLP_HOST_TEST block in
rtl837x_common.h grouping the libc-named prototypes (sleep/memcpy/memset/
strlen/strcpy/strcmp) so host builds take them from the C library instead;
on-target builds are unaffected.
Run with: make -C test
BANK2 outgrew its 48 KB window on PCB_SWTG018AS_V2_1_0 (49182 bytes, 30
over) once the spanning tree code learnt about link aggregation, and the
image builder refuses to link. Only KP_9000_6XHML_X2 is built in CI, so
the overflow was not caught there.
dhcp.c is self-contained: its three exports are __banked, the rest of
the module is only reached from inside it, it holds no __code pointers,
and its uip calls already crossed into BANK1. Moving the pragma is the
whole change. BANK2 49182 -> 43691 on PCB_SWTG018AS_V2_1_0, BANK3 5709
-> 11206, BANK1 +6 for the trampolines; the generated assembly shows no
plain lcall leaving the module's bank.
The firmware lists a LAG as its own entity in /stp.json (`lag` and `mbr`,
`p` 0) and drops the member ports from the list; its settings take
`stp lag <n>`. Give the group its own row in both spanning tree tables,
named with its members, key the cells by entity rather than port number,
and rebuild the tables whenever the entity set or a membership changes so
a group created on the LAG tab appears without a reload. The `stp lag`
commands are recognised for the unsaved-changes badge and the startup
config merge alongside `stp port`.
The rename of the entity index turned `stp port <n>` into `stp ent <n>`
in the parser only. doc/stp.md, the web UI and every saved startup
config still spell it `stp port`, so per-port settings were rejected on
the console and silently dropped when the startup config was replayed at
boot. Restore the keyword; `stp lag <n>` is unchanged.
A dark receiver reports 0 mW, which printed as -Infinity dBm. The DOM
value has a resolution of 0.1 uW, so -40 dBm is the lowest reading the
module can express; clamp there.
The header carried the save button, the language selector and the theme
selector on every section, although the latter two are set once. They
now live in a Display card in the System section; the header keeps the
title, the unsaved-changes badge and Save to flash.
The forwarding database is read a page at a time; leaving the MAC table
or ports section let the walk carry on through the request queue. Give
the walk a generation number that the leave hooks bump, and stop the
walk at the next page when it is stale.
The VLAN table is one /vlanlist plus one /vlan.json per VLAN. A failed
per-VLAN request rejected the chain, so every VLAN after it was missing
from the table, indistinguishable from VLANs that do not exist. Keep the
row with the id and name from the list and a ? in the port columns.
Save to flash showed the merged startup config read-only, so a stray line
had to be fixed afterwards in the System section. The preview is now a
textarea: the size and unknown-line notes follow the edits, the write
button is disabled above 2048 bytes, and what is written is the edited
text.
The default hostname (RTLPlayground- plus six hex digits) overflows the
200 px sidebar; clip it with an ellipsis and keep the full name in the
title. The MTU and Mbit/s fields were 70 px, which leaves no room for five
digits next to a number spinner. The main column stops at 1400 px instead
of 1100 px so the dashboard cards sit side by side on a wide screen.
The old dashboard showed a hover tooltip per port with the link state and,
for SFP ports, the module and its DOM readings, and rewrote it on every
poll. The single-page app replaced it with a click-to-open dialog that was
a snapshot: the values did not change while it was open.
Build the rows once per port and use them for both: the dialog is
re-rendered on every status poll while it is open, and each port in the
strip carries the same rows as its title.
The firmware emits every per-port array in logical port order, which on a
board with a remapped port table (SWGT024: phys_to_log = {4,5,6,7,8,3,...})
is 6, 1, 2, 3, 4, 5. The old pages iterated over physical port numbers and
looked each entry up; the single-page app rendered the arrays as received,
so the ports, spanning tree, EEE and bandwidth tables and the dashboard
strip came out in logical order, and the strip put the SFP marker and the
detail view on the wrong port.
Sort every array by portNum on receipt so index i is port i+1 throughout.
The pages become one index.html holding every section, one app.js and a
self-contained login.html. The layout is a sidebar with light, dark and
Selenized themes (or follow the OS), a live dashboard with per-port
packet rates, and confirmation dialogs in front of the actions that can
lock the operator out.
Behaviour changes against the previous pages:
- every request goes through one queue that waits for the previous
response BODY rather than its headers: the firmware shares a single
output buffer across connections and delimits responses by closing,
so a second request in flight corrupts the first
- "Save to flash" merges the command log into the startup configuration
with the grammar the console accepts, shows the result before writing,
verifies the sector by reading it back and clears the command log only
after that; the header shows an "unsaved changes" badge while the
running configuration differs from flash
- the console section shows the output the firmware returns for a
command, including the parser's explanation on a rejected one
- the firmware update section checks size, bank header and CRC16 of the
image in the browser before uploading, shows upload progress and waits
for the switch to come back after the reset
- the ports table shows the learned MAC of a port when there is exactly
one and the count otherwise; the MAC table sorts per column, filters,
and names the LAG a learned address sits behind
- the spanning tree section sends only the settings that changed on
Apply instead of one command per control event
- link aggregation groups are numbered 1-4 like the command line, and
applying an empty group clears it
The translation table is inline in app.js with English, Japanese and
Chinese. The language selector sits in the page header and reloads the
page so that dynamically built tables are rebuilt in the new language.
Only login.html and the favicon are served before authentication; the
stylesheet, the port images and the old script bundle no longer exist.
The simulator's allowlist follows the firmware's.
The session timer was refreshed only when a page from the file table was
served, so a client that loads its pages once and then talks to the JSON
endpoints alone is logged out SESSION_TIMEOUT seconds after the load,
however busy it is. A firmware upload started after that point is answered
with 401 instead of a verdict. Refresh the timer wherever the session
cookie validates, which covers every authenticated request, and drop the
file-serving refresh that this makes redundant.
The 8051 in the RTL837x has 48 kB of XDATA, 0x0000 to 0xBFFF. Writing
a byte and reading it back from the same address works all the way up
to 0xBFFF on a SWTGW218AS, while at 0xC000 the byte comes back as 0x00.
Without --xram-size the linker assumes 16 MB, so running past the end
would still build cleanly and only show up at run time.
The value is 49151 rather than 49152. The linker check adds up the
sizes of the XDATA areas, and XDATA allocation starts at 0x0001, so a
limit of 49152 would still let the last byte land on 0xC000.
boot.c calls it from BANK3 and only rtlplayground.c had a prototype, so
the compiler saw an implicit declaration. The call itself was fine, the
function lives in the common area, but the warning was real. Move the
prototype into the shared header.
Reported by vDorst.
sfp.c takes handle_sfp, sfp_module_read, sfp_apply_quirks, sfp_get_info,
sfp_read_field, sfp_print_info, setup_sfp_gpio, the quirk table and the module
state that goes with them. Everything exported is __banked: handle_sfp() and
sfp_print_info() are reached from cmd_parser.c in BANK2, the rest from the
common area, and both are bank boundaries.
The quirk table needed checking rather than assuming. Its entries are __code
pointers, and a __code pointer carries no bank, so passing one to a function in
a different bank hands it an offset read through the wrong window. That is safe
here because strcmp() sits in the common segment at 0x51f and is reached with a
plain lcall, so the bank register still says 3 while it runs. Verified in the
built image: the table at 0x35580 holds 0x563c and 0x5644, which resolve to
"QSFPTEK" and "QT-SFP+-T" inside BANK3.
Common segment 13393 -> 8432 bytes, leaving 7492 free rather than 2531. BANK3
holds 5710, BANK1 is unchanged and BANK2 grows by 31 for the trampolines.
All 34 machines in machine.h build and link, and the same walk over the
generated assembly finds no plain lcall crossing a bank boundary.
The common segment is the scarce one: sixteen kilobytes shared by every bank,
and before this it had about 2.5 kB left. Most of what moves here runs once at
startup and does not have to sit in it. The two SerDes helpers are the
exception: the link-change handler and the SFP polling both reach sds_config()
while the switch is running, so they are not startup-only. None of it is
reached from an interrupt.
boot.c takes setup_i2c, init_smi, nic_setup, rtl8373_revision, rtl8224_enable,
early_boot_handle_button, set_hostname_default and the two SerDes helpers.
BANK3 is enabled with the linker flag; nothing else in the build needed
changing.
Everything moved is declared __banked, since every caller is outside BANK3:
main() and friends run from the common area and rtl837x_init.c calls
sds_config() and sds_config_mac() from BANK2. The calls the moved code makes
outward were already banked, so that side needed no work.
Common segment 13393 -> 10137 bytes, leaving 5787 free rather than 2531.
BANK3 holds 3732. All 34 machines in machine.h build and link. Walking
the generated assembly for plain lcalls that cross a bank boundary finds none
among 2808 calls; those build cleanly and only fail at run time, so it seemed
worth checking by machine rather than by eye.
IPBUF is laid out for frames we send, with the 12 byte TX descriptor in
front of the Ethernet header. A received frame starts with the Ethernet
header at uip_buf[0] and carries the RTL and VLAN tags after the
addresses, so IPBUF->ethhdr.src read the port mask and both VLAN fields
as a MAC. Every IP packet from the local network then replaced the
sender's ARP entry with that, and our replies went to an address nobody
has. The IP source address sits at the same offset in both layouts,
which is why the rest looked right.
Use BUF, the receive view uip_arp_arpin() already works from.
parse_mac() now writes to the global mac_parse_result[6] and takes only the
index, like the other parsers (ip[4]). This drops the local __xdata buffer
and the now-unused hexval() helper.
Build (KP_9000_9XHML_X_V3_1): ROM 110302 -> 110193 bytes, XSEG and internal
RAM unchanged.
The state literals were spread over eighteen places in the file, sixteen
of them older than this branch, so naming only the new ones would have
left it written half one way and half the other.
Naming them separates two roles the literal hid: 0b11 is the forwarding
state in a call and a two bit field mask in a read-modify-write, hence
both STP_ST_FORWARDING and STP_ST_MASK. The comment above stp_state_set()
said what the defines now say, so it goes.
stp_parse() holds an entity index, not a port. cmd_parse_port() has
already mapped the front panel digit through machine.phys_to_log_port[],
and the aggregates continue that same logical range above the CPU port,
so call the variable ent. Reading the two branches of the if no longer
means following the parser two functions down.
No change to the generated code: 5331 instructions before and after.
parse_mac now accepts only a strict aa:bb:cc:dd:ee:ff (or '-' separators)
form: each octet is exactly two hex digits, the 5 separators are a single
consistent character, and nothing is allowed after the 6th octet. This
rejects the malformed values the reviewer flagged (repeated separators,
missing digits, trailing characters). A new inline hexval() helper does the
digit-to-value conversion and can be reused by future commands.
print_mac and print_ip are marked __banked to match cmd_parser(), which
lives in codeseg BANK2 and is itself __banked, so callers across the code
bank work correctly. cmd_parser.h is updated with the new prototypes and
the __banked qualifier on print_ip.
Verified with make MACHINE=KP_9000_9XHML_X_V3_1: the build links cleanly
and the internal-RAM (DSEG) layout is unchanged against main; XSEG is
unchanged and ROM grows by 184 bytes. A host-side test confirms parse_mac
rejects the two malformed inputs from the review.
The web UI became one index.html plus one main.js while this branch was
open, so the STP page changes had no html/stp.js and html/config.js left to
live in. They are re-applied here against the new structure.
A row now carries two derived fields: the name the CLI expects for it
("port N" or "lag N") and the key its controls are keyed by (the port
number or "LN"). A group's row is labelled with its front panel members,
which /stp.json reports in the mbr field.
conf_cmds and conf_overwrite accept the "stp lag N ..." forms so the
startup config keeps them.
Review of #397 pointed at the bare 10, which shows up in fifteen places. It
is the width of the port field in the MSTP register, so it is the CPU port
index plus one and it gets a name.
The web page counted to ten as well when listing a group's members. It uses
numPorts now, like the rest of that page does.
The aggregation section of the doc no longer names the OEM firmware or calls
a group a trunk, and it says plainly that the hardware keeps no per-group
state, so the firmware writes the members itself.
Generated code is unchanged: the only difference in the assembly is the
version string.
Spanning tree so far saw only physical ports. Two ports aggregated towards
the same neighbour look to the protocol like two parallel paths, so it
blocks one of them and cuts the group in half.
Give each of the four groups an STP entity of its own, numbered above the
physical ports. A member port answers to its group: BPDUs arriving on it
are accounted to the entity, the entity's state is written to every member
together, and its BPDUs carry the state of a live member. Ports outside
any group keep behaving exactly as before.
Membership comes from the aggregation tables once a second; when it
changed, the entity's state is reapplied to the new set of members. The
CLI takes lag <1-4> wherever it takes port <n>, the web UI shows a group
as one row, and the saved config keeps the lag form of the commands.
The lookup tables live in BANK1, so the routine switches the code bank to
reach them with movc and never switches it back. That is the contract the
__naked declaration already implied: same bank or common segment only.
The name now says it.
crc16_bank1 is the routine unchanged. crc16 becomes a __banked wrapper in
BANK1, so a caller from another bank has its own bank put back by
__sdcc_banked_ret. The two existing call sites take the direct path and
pay nothing.
A VLAN name is copied into vlan_names with nothing checking vlan_ptr
against VLAN_NAMES_SIZE. The only limit is the 128 byte command line, so
enough named VLANs walk straight off the end of a 1 kB table.
What sits immediately behind it makes this worse than a plain overflow: at
0x048A comes vlan_ptr itself, the index driving the write, then port_names
and cmd_buffer. Once the index is corrupted the following writes land
wherever it now points, and the configuration is replayed at boot, so a
config file that overflows does it again on every start.
Measure the name first and refuse it if the three id digits, the name, its
separating space and the terminator would not fit. The check goes before
vlan_name_remove() so a name that does not fit cannot take the existing one
with it, and the VLAN still gets its ports: dropping the whole line would
lose port membership during a config replay, which is worse than losing a
name.
The copy walks a pointer instead of indexing vlan_names[vlan_ptr++] each time,
and the word offset comes out of cmd_words_b once. Writing the bound as
VLAN_NAMES_SIZE - 5 folds the constant at compile time instead of adding it on
every call. BANK2 ends up 25 bytes smaller than it was before the check
existed.
The RTL tag carries a 4-bit port number for received frames, and the two
places that turn it into a mask bit take the whole byte:
entry.pmask |= (1L << (IGMP_I->rtl_tag.pmask >> 8));
The comment beside it already says only the low four bits count, but nothing
takes them. rtl837x_stp.c:424 does the same extraction and does mask:
stp_scratch = ((uint8_t)HTONS(STP_I->rtl_tag.pmask)) & 0x0f;
With a count above 15 the result is not a bit at all. SDCC narrows the 1L to
16 bits and emits the shift as a djnz loop, so a count of 200 spins the loop
200 times and leaves zero behind: the join adds no port and the leave clears
none, neither saying anything. It also spends those iterations inside packet
handling.
Mask to four bits and shift a uint16_t, which is the width the surrounding
code works in. Six bytes of BANK1.
Worth saying that this does not make snooping work on its own. struct
igmp_pkt has no vlan_tag field while struct eth_in describes the same frame
with one, so every IP and IGMP field below rtl_tag is read four bytes early.
The 100M branch of phy_set_speed() was missing an else. A port set to 100M
half wrote 0x1481, walked into the next if, found duplex was not 1, took its
else and overwrote the register with 0x1581 straight away, so it advertised
100BASE-TX half and full both. That is what asking for both is meant to do,
and forcing half is something people do to match a partner that cannot
negotiate, so the link comes up looking fine and misbehaves under load.
Bit 7 is 100BASE-TX half and bit 8 is full, which makes 0x1481, 0x1501 and
0x1581 half, full and both, the same shape as 0x1421, 0x1441 and 0x1461 in
the 10M branch above.
Both branches now choose the advertisement into a variable and write it once,
and they compare against the PHY_DUPLEX_* names instead of 0, 1 and whatever
is left. Same values reach the PHY; BANK2 loses 92 bytes.
The 100M branch of phy_set_speed() is missing an else. A port set to 100M
half writes 0x1481, falls into the next if, finds duplex is not 1 and takes
its else, and immediately overwrites the register with 0x1581. The port
ends up advertising 100BASE-TX half and full, which is what "both" is
supposed to mean.
The 10M branch directly above has the chain the 100M one meant to have, and
the values line up with it: bit 7 is 100BASE-TX half, bit 8 is full, so
0x1481, 0x1501 and 0x1581 are half, full and both, exactly as 0x1421,
0x1441 and 0x1461 are for 10M.
Forcing half duplex is something people do to match a link partner that
cannot negotiate, so quietly advertising full as well is the wrong way for
this to fail.
Not tried on hardware: every port on the switch here has a link up, and
setting the speed restarts negotiation.
isr_ext2() ends with PCON |= 1, putting the core to sleep from inside an
interrupt handler.
The DW8051 leaves idle when any enabled interrupt is activated, so the core
does not stay down for long. It naps until the next enabled interrupt request
turns up, with the 200 Hz system tick putting a ceiling of 5 ms on that, and
then carries on.
The sleep buys nothing either way, because the instruction after it is the
return. While it lasts the handler holds the priority latch, so everything at
its level or below waits, and that includes the tick, the console and the NIC
receive interrupt.
The handler still clears its flag, which is what it has to do. The bit numbers
beside the two masks were wrong: 0xef clears bit 4 and 0xdf clears bit 5,
not 7 and 6.
isr_ext2() ends with PCON |= 1, putting the core to sleep from inside an
interrupt handler.
The DW8051 leaves idle when any enabled interrupt is activated, so the core
does not stay down for long. It naps until the next enabled interrupt request
turns up, with the 200 Hz system tick putting a ceiling of 5 ms on that, and
then carries on.
The sleep buys nothing either way, because the instruction after it is the
return. While it lasts the handler holds the priority latch, so everything at
its level or below waits, and that includes the tick, the console and the NIC
receive interrupt.
The handler still clears its flag, which is what it has to do. The two wrong
comments next to the masks go as well: 0xef clears bit 4 and 0xdf clears bit 5,
not 7 and 6, and the masks already say that without help.
Add a `mac <aa:bb:cc:dd:ee:ff>` console command (and a bare `mac` to show
the current address) that overrides the management MAC at runtime. The
address is validated (unicast, globally administered, non-zero OUI) and
the static L2 management entry is re-registered so the change applies
without a reboot. Because it is a normal command, a line in the startup
config persists it across boots: execute_config() runs it before the final
static L2 entry and the derived hostname are set up.
Also point the keepLink KP-9000-9XHML-X V3.1 machine definition at the
factory MAC area (0x1FC000, same location other machines use) so a valid
factory MAC is picked up at boot instead of falling back to a locally
administered address.
The MAC parsing/printing lives in parse_mac()/print_mac() helpers next to
parse_ip()/print_ip() so future commands can reuse them, and a memcmp()
helper joins the project's own memcpy()/memset() (no libc is linked).
isr_ext2() ends with PCON |= 1. On the DW8051 any enabled interrupt request
ends idle, so the handler sleeps until the next request arrives, the system
tick at the latest, and only then returns. Everything at the same priority
waits with it, and the instruction after the idle is RETI, so the sleep buys
nothing.
The handler still clears its flag, which is what it has to do. The other four
uses of PCON |= 1 are in task context and are left alone.
A file larger than the output buffer is sent in two phases: the first
buffer goes out in MSS-sized pieces, with o_idx walking through it, and
what is left is then streamed from flash a chunk at a time into outbuf
from offset 0.
The continuation branch never reset o_idx, so it kept the offset the first
phase had left it at, while uip_rexmit() retransmits from outbuf + o_idx.
A lost continuation segment was therefore retransmitted as the bytes 1460
further into the buffer, which the client accepts as the missing data, and
with o_idx + slen past the 2500-byte buffer it also read whatever xdata
follows outbuf.
Reproduced on a SWTGW218AS by dropping 15% of the switch's TCP segments:
seven of eight fetches of a 21 kB static came back the right length and
the wrong content, each first differing at a fixed phase of the 1460-byte
chunk. With o_idx reset the retransmission repeats the chunk it sent.
The o_idx += slen right after slen = 0 went too: it added nothing.
(cherry picked from commit 789d29423288f89411a41673e26ce1854b296c3d)
uip_arp_ipin() is an empty macro with its body under #if 0, both inherited
from the uIP import and untouched since. That leaves uip_arp_update()
reachable only from uip_arp_arpin(), so an entry is refreshed only by ARP
addressed to us and ages out after UIP_ARP_MAXAGE no matter how much traffic
passed over it.
When it goes, the next transmission to that host hits the miss path of
uip_arp_out(), which overwrites the packet in uip_buf with an ARP request and
returns. For TCP that costs a retransmission. For UDP nothing retransmits, and
handle_tx() sends the UDP connections through uip_arp_out() too, so the
datagram is lost.
The body could not be enabled as written: uip_len -= sizeof(struct uip_eth_hdr)
is a leftover from when UIP_LLH_LEN was 0. Here it is ETHER_HEADER_SIZE +
RTL_FRAME_DESC_SIZE and uip.c reads the IP header at uip_buf[UIP_LLH_LEN], so
uip_len has to keep the link layer in it. The rest of the function is sound.
replaceCalls() ended with strlen(&buffer[pos]), so the length recorded for a
file was the distance to its first 0x00 byte rather than the file size. The
byte just past the content happens to be zero today, which is the only reason
the numbers come out right.
Nothing in html/ contains a 0x00 today, so this is latent, but it is a trap
waiting for the first gzipped or binary asset: gzip puts a zero in the fourth
byte of its header. A gzipped style.css lands in the index as 3 bytes, and
since addr advances by that same value the next file is placed 3 bytes in and
overwrites what came before it. Two gzipped files in html/ are enough to see
it: both come out as 3 bytes and the second sits 1356 bytes inside the first.
Pass the length in and return the adjusted one. The substitution shortens
#{function} to #{NNN}, so the delta is known without measuring. The loops are
bounded by the length as well, which also covers -n, where nothing had put a
terminator in the buffer for strlen to find in the first place.
No output change: the image built from an unmodified html/ is byte for byte
the same before and after, and html_data.h is identical.
Option 58 is optional, and dhcp_state.renewal is only ever written by
parse_opts(), so a server that leaves it out keeps it at zero. The ACK
handler then sets dhcp_timer to zero, and the callback treats a zero timer
in LEASING as time to renew, so it sends a REQUEST on the next pass.
That REQUEST does set a 30 second timeout, but the ACK that answers it
comes back through the same path and zeroes the timer again, so the two
sides trade REQUEST and ACK as fast as the round trip allows, with the
console printing the whole lease each time round.
RFC 2131 has the client renew at half the lease when the server does not
say, so use that, and fall back to a fixed value if there is no lease
either. Both only fill in a value that is missing: a server that asks for a
short renewal still gets exactly what it asked for, which a blanket lower
bound would have overridden.
Not reproduced here, that needs a server configured without the option.
write_char() masked writeptr and never looked at readptr, so once more
than LOGBUF_SIZE bytes were written between two drains the ring wrapped
over content that had not been sent yet.
That is not a corner case. The ring is emptied once per main loop pass,
while a single command runs to completion inside one pass, and a full L2
listing prints well over twice the 512 byte buffer. On hardware the whole
1227 byte dump came out as one 174 byte datagram whose first line began in
the middle of a MAC address.
Drop the character when the ring is full instead of overwriting, so what
has been logged stays intact and it is the tail that is lost. A full ring
also flags a line, otherwise a burst without a newline would leave the
reader waiting on line_available forever and syslog would stop for good.
dhcp_start() never looked at whether DHCP was already running, and "ip dhcp"
calls it without stopping first, so a second start took a fresh slot from
uip_udp_conns and left the previous one bound.
The leak is the smaller half of it. dhcp_callback() decides whether a call
is for it by comparing the local port, not the connection, and every
leftover socket still holds port 68, so each one runs the state machine on
every poll. The timers live in one shared dhcp_state, so ticks count down
once per socket: two starts and the retransmit and renewal timing runs at
twice the intended rate.
Stop the old one first. dhcp_stop() already does exactly that, and the
state test keeps it away from a connection that was never created.
uip_init() does not set uip_conn, and httpd_init() runs right after it, so
the assignment went through a pointer nothing had assigned yet.
It has stayed invisible because the startup code does clear xdata: uip_conn
is NULL rather than garbage, appstate sits 28 bytes into struct uip_conn, and
the byte therefore lands at xdata 0x1C. With the current map that is inside
the command line buffer, at an offset nothing reads while the line is empty,
and cmd_editor_init() runs later anyway.
Nothing is broken today, but it is a write to an address that has nothing to
do with the state being set, and it moves the moment the struct grows or the
map shifts. Address the connection directly.
Leaving SFP ports out of the table is deliberate for now, and being able to
force a module's speed belongs in a feature of its own rather than arriving
inside a fix, so that half goes again.
What remains is the off-by-one: createPortTable() read pState[i] where the
array is filled by portNum - 1, so every row started out showing the next
port's link speed until the first refresh corrected it. The debug line above
it goes too; it printed a third index, pState[i-2].
The id was built and checked with HTONS(), which casts to 16 bits, so the
top half of the 32-bit transaction id was thrown away and the rest byte
swapped. The id is opaque: the client only has to receive its own value
back, so there is no byte order to honour and no reason to narrow it.
Assign and compare the field directly, so the full 32-bit id round-trips.
Fold the "group with no ports" case into the argument-count guard
(cmd_words_len < 3) instead of a separate check, and accept only "d" for
delete, dropping the longer "delete" spelling. Update the serial-console
docs to match.
The EX0..EX3 handlers each emitted a single debug character through
write_char(), which busy-waits on the serial TX-empty flag that is set by
the serial interrupt. If one of these handlers runs while a byte is being
transmitted, it waits for a flag that the equal-or-lower-priority serial
interrupt cannot deliver, hanging the handler with the system tick stopped.
The characters were only ever debug markers, so drop them; the handlers now
just acknowledge the interrupt. No interrupt service routine should call the
blocking console path.
uip_arp_timer() was defined but never called, so ARP entries were only
refreshed on new learning and only removed under table pressure. A cached
entry for a peer whose MAC later changes (a gateway failover, a replaced
device) is then used until it is evicted, and traffic to anything reached
through it is sent to the old MAC and lost, while on-link traffic is
unaffected.
uIP expects the timer roughly every ten seconds; drive it from the
once-per-second housekeeping in idle().
The guard read `if (!DHCP_P->tid == HTONS(...))`. Unary `!` binds tighter
than `==`, so this evaluated `(!tid) == HTONS(id)`, i.e. `(tid ? 0 : 1) ==
HTONS(id)`, which is false for any non-zero id and never returns. The reply
was therefore accepted regardless of its transaction id, so a broadcast
reply meant for another client on the segment was parsed as ours.
Compare the field directly. The id is stored network-order in the packet
and host-order in the state, matching the HTONS() used when the request is
built, so `DHCP_P->tid != HTONS(dhcp_state.transaction_id)` is the intended
test.
The port table skipped every SFP port outright, so on a SWTGW218AS the
uplink simply was not there. Of the seven columns only Set Speed is
meaningless for a module: the name, the link speed and the devices behind
the port all are worth showing, and the name in particular was accepted on
another page and then never displayed anywhere. The MTU table further down
the same page has always drawn the port, so the page disagreed with itself.
Give an SFP port its row and leave the three control cells empty. Whether
speed and disable should work for a module is a separate question and
untested, so this does not offer a button for it.
The link state was read one port too far. main.js fills pState by
portNum - 1, updatePortTable() reads pState[i-1], and createPortTable()
read pState[i], so every row started out showing the next port's link
speed until the first refresh corrected it. On the last copper port that
meant showing the uplink's speed. Feeding the real status.json of a nine
port switch through both versions:
expected 7 7 7 1 7 1 7 7 6
before 7 7 1 7 1 7 7 6
after 7 7 7 1 7 1 7 7 6
Dropping the skip also removes a trap rather than papering over it.
updatePortTable() addresses rows as tbl.rows[i], which only lined up with
port i because the SFP port happens to be last on this board. With a
module in the middle every row after it would have been off by one.
A device on an aggregate is learned against whichever member the frame came in
on, and the hash moves that around: the same address changes port every time
the flow mix changes. The table was telling the truth and there was no way to
read it, since nothing on the page or the console says those two ports are one
link.
port_lag_of() already exists and already knows the answer, so both views ask
it. The console prints LAG1 in place of the member, and /l2.json carries a
"lag" field beside the port, 0 when the port stands alone, which is the shape
/stp.json already uses.
Costs 151 bytes of BANK1.
Two small things about the same command.
The usage line offered "lag <1-4> [port]...", which leaves out "lag show" even
though that is the only way to see the groups, so the one subcommand a reader
most needs is the one the error does not mention.
And a group number with no ports after it quietly wiped the group. That is a
reasonable thing to want and an unreasonable thing to do by accident, as it is
one fat-fingered return away from a bare "lag 1". It now takes "lag 1 delete"
or "lag 1 d", and a bare group number prints the usage instead.
Nothing changes for a saved configuration: "lag 1 5 6" is what gets written and
what gets replayed, and neither the old bare form nor the new delete matches
the pattern that decides what is persisted.
The LAG page draws four sections headed LAG 1 to LAG 4 and wires their buttons
to lagSub(0) through lagSub(3), which then sends the index straight through as
the group number. So the first button asks for "lag 0", which the parser
rejects, and the page shows nothing at all because the error only goes to the
console. Every other button is off by one: the section headed LAG 2 creates
group 1.
The index stays 0-based for the element ids and only the command gets the
1-based number the CLI documents.
* uip: time out an ESTABLISHED connection whose peer went silent
uIP has no keepalive and no idle timeout for an ESTABLISHED connection, so
a peer that vanishes without FIN or RST holds it open forever. A crashed
client does it, and so does a SYN caught while the switch is still
booting. With UIP_CONF_MAX_CONNECTIONS = 1 that wedges the single slot:
the web UI stops answering, port 80 no longer even SYN-ACKs, and only a
power cycle recovers it. Every other TCP state already has an exit path
(UIP_MAXRTX, UIP_TIME_WAIT_TIMEOUT); idle ESTABLISHED was the one hole.
Close it inside uIP rather than in httpd, so any future TCP service is
covered too. The retransmission timer is unused while nothing is in
flight, so the idle count lives in it: a 1 Hz strobe derived from the
uip_periodic() rate ages it, any arriving segment clears it, and past the
limit the connection is aborted through the same path as UIP_MAXRTX. Two
new options in uipopt.h, default off; this project enables 30 s.
httpd also joins uip_timedout() to its cleanup: it only handled closed
and aborted, so both this timeout and the existing UIP_MAXRTX one would
have left the transfer state stuck on a dead connection.
* uip: keep the tuning values out of the prose
The comment named the timeout and the tick rate, which go stale the
moment either constant is edited. It now says why an idle connection
is aged out and leaves the numbers to the defines under it.
The /cmd path carried its own copy of the plain-text response header,
which was the same string page_impl.c already keeps in
HTTP_RESPONCE_TXT, minus one difference: the shared one never said
Connection: close.
This httpd closes after every response, so a reply that does not
advertise it invites the browser to reuse the socket and lose the next
request. HTTP_RESPONCE_JSON has always said so; the text one was the
odd one out, and send_config() and send_cmd_log() were answering
without it.
The copy is gone and the shared string carries the header. The check
for a command that printed nothing now compares against the length
strtox() returned instead of sizeof() on the macro.
The firmware now refuses to delete the switch's own entry, so the
button on the CPU row can only fail; leave it out. When a delete
succeeds, remove the entry from the cached table and repaint instead
of leaving the stale row until the next refresh.
Test the busy flag and compare the entry's MAC against the SFR data
registers directly instead of going through the sfr_data copy, and
rewrite TBL_DATA_0 in place rather than reassembling all four bytes.
Smaller and faster on the deepest httpd path.
Suggested-by: vDorst
The status line built the dotted quad by hand, which is what
print_ip() from cmd_parser.c already does. Reusing it drops seven
calls to one and takes 48 bytes off BANK2.
The prototype is not __banked because cmd_parser.c and syslog.c are
both in BANK2; a plain call is what the compiler needs here. Should
either file move to another bank, this has to become __banked with it.
The L2 page's Delete button removes any table entry by index, including
the entry for the switch's own MAC on the CPU port. Without that entry
unicast to the management address is no longer delivered to the CPU, so
management goes dark until the CPU happens to transmit again and the
entry is relearned. Compare the entry's MAC against uip_ethaddr before
rewriting it and answer result 0 for the switch's own address, as for a
missing entry. The compare is inline: this handler sits at the bottom of
the uIP call chain, where every extra call level costs internal RAM.
The CPU's own MAC exists in the L2 table only as a learned entry,
created whenever the CPU transmits, and it ages out like any other
dynamic entry. Unknown unicast is not flooded to the CPU port, so on an
idle switch (no web session, no management traffic) the entry expires
after the aging time and frames for the management IP no longer reach
the CPU: pings die until the next CPU transmission (an ARP reply)
re-teaches the entry, then die again. Real management sessions keep the
entry alive continuously, which is why this is only visible on idle
switches.
Install a static entry for the management MAC on the CPU port after the
startup config has been replayed, so it lands in the final management
VLAN, and move it when the management VLAN is changed at runtime.
Entry mechanics, established on hardware: a table write with read-method
0 hashes the key in TBL_DATA_IN and places the entry itself (a search
returns index 0 on a miss, there is no software-visible insertion slot).
The age field in the third data word must be non-zero for the entry to
be visible to lookup at all, and the static flag (bit 0 of byte 1 of
that word) stops the aging engine from decrementing it: with the flag a
full-age entry survives untouched where an identical unflagged entry
ages to invisible within the aging period. With no management VLAN
configured the entry lives in VLAN 1.
fetch() does not throw on 4xx, so a request that came back 401 after the
session had timed out reached the same line as a successful one: the
window showed the prompt and nothing under it, and the input was cleared
as if the command had run. The session lasts 200 seconds, so leaving the
tab open is enough to hit it.
A 401 now goes to the login page instead. The transcript also gets a
trailing newline when the answer lacks one, so the next prompt does not
run into the last line of output, and it is capped so a long session
does not keep growing the node.
Three ways the answer could mislead the caller, all in the paths that
matter most when something is wrong.
The capture sat in write_char(), so anything printed through
print_string_no_syslog() escaped it. That is exactly the replies of the
syslog commands, which deliberately avoid the syslog path: "syslog on"
answered with the bare address and no sentence around it, "syslog off"
answered with nothing at all. Moving the capture one level down, into
write_char_no_syslog(), catches both paths and costs nothing, since
write_char() reaches it anyway.
On a parse error send_bad_request() rewrote the buffer from the start,
throwing away the very text that explains the error and replacing it
with the 400 HTML page. Keeping the captured text and restating only the
status keeps the parser's own message; "400 NO" is as long as "200 OK",
so the header does not have to be rebuilt.
An overlong listing was cut at the buffer end with nothing to show for
it, so a clipped answer read as a short one. It now ends in a marker,
and the reserved room is accounted for in the guard.
While here: a command that configures something prints nothing, and an
empty body reads the same as "nothing ran", so a successful silent
command now answers "OK".
No internal RAM and no xdata; 174 bytes of BANK1, which has 7356 left.
The console tab posts to /cmd and throws the response away, so the page stayed
silent whether the command worked, was refused or was just misspelt. Now that
/cmd answers with the command's output, the page keeps a running transcript:
each command echoed after a prompt, then whatever it printed.
The box is a pre element filled through textContent, so nothing the device
prints is taken for markup. It scrolls to the newest line and the input clears
after a send.
POST /cmd runs the command and answers with an empty 200, so a caller cannot
tell "done" from "nothing happened", and whatever the command prints only
reaches the serial console. Reading a register over the network needs a second
channel just to see the answer.
write_char() is the one funnel every console helper goes through, and it already
tees into the syslog buffer. A third sink, live only while the httpd runs a
command that arrived over the network, puts the same bytes into the response
behind the header. Serial and syslog keep getting their copy.
The capture stops at the end of the response buffer, so a long listing is
truncated rather than overrunning it.
BANK1 grows 39 bytes and the common area 52. One byte of xdata, BANK2 unchanged.
login.html asks for i18n.js, but the file is not on the short list a
client may have before it authenticates, so the request is answered with
a redirect back to the login page and the strings stay as the English
that is baked into the markup.
The file carries only the interface text, the same text the page already
ships untranslated, so serving it before login gives nothing away.
The enabled flag is a flag, so say bool. The header length no longer needs
to live in xdata: it was put there when a local would not link, and there
is room for it here.
The overview names the row after the JSON key, so the syslog row said
"syslog_server_ip" while carrying only an address. It now says
"syslog_server" and carries the port with it.
A POST /cmd whose body arrives in a separate TCP segment from the headers
executed nothing: handle_post() read the body from the segment that
carried the request line, found it empty, and answered 200 OK. Python's
urllib and requests both write headers and body separately, so every
scripted command from those clients became a silent no-op. POST /login had
the same hole. The multipart endpoints were fixed earlier; this covers the
plain ones.
When the first segment holds fewer body bytes than Content-Length
announces, the bytes accumulate in config_buf (idle outside multipart
uploads) and the connection waits in TSTATE_POSTBODY; once the announced
length is in, the endpoint runs against the buffer. The /cmd and /login
executions move into run_cmd_body() and run_login_body() so both paths
share them; the 200 OK builder they duplicated becomes send_ok(), and
run_login_body() checks the pwd= prefix instead of skipping four bytes
blindly.
The announced length is the contract on both paths. A body that arrives
with the headers is terminated at Content-Length before it runs, so
pipelined bytes after it are not executed as commands the way the old
code did. A request without a usable Content-Length answers 411: without
one there is no way to know when the body has arrived, and the old 200
OK for an empty body is the bug this fixes. Bodies announced at or above
the buffer size answer 400 before anything is buffered.
The wait state has a deadline. uIP is built with a single connection,
and an ESTABLISHED connection with nothing in flight never times out on
its own, so headers followed by silence (a script interrupted, a link
dropped mid-request, or someone holding the socket on purpose) would
otherwise keep the slot until reboot; POST /login reaches the wait
before authentication. The poll handler aborts the connection when the
body has made no progress for five seconds, measured on the 200 Hz
system tick rather than on poll count, which runs faster under interrupt
load.
Record the announced body length alongside Content-Type and the session
cookie. The value goes through parse_short(), so an announcement that does
not fit sixteen bits saturates at 0xffff rather than wrapping to something
the size checks would accept, and a missing or unparseable header reads as 0.
Nothing consumes it yet; the plain POST endpoints take it up next.
The scan stepped over the NUL that ends the received data before breaking,
so on a request whose headers were cut short it returned a pointer one past
the data. handle_post() then tested that byte for the end of the header,
reading whatever an earlier packet had left there instead of the NUL the
appcall wrote, and could go on to run an endpoint against the stale bytes.
Break before advancing so the returned pointer sits on the NUL and the
caller's check sees it.
The digit loop accumulated into a uint16_t without a bound, so a query
such as /vlan.json?vid=65540 read as VLAN 4 and every consumer saw a small,
valid-looking number for an out-of-range one. Clamp the result at 0xffff
once another digit would overflow (6552 * 10 + 9 is the last value that
fits). The consumers already reject or mask 0xffff: vlan_get() refuses
anything from 4095 up, send_l2() masks the index to the table size, and
l2_delete() masks the high byte.
scan_header() found Content-Type and Cookie with is_word(), which compares
bytes exactly and requires a separator after the pattern. Field names are
case-insensitive (RFC 7230 section 3.2), and the whitespace after the colon
is optional, so "content-type: multipart/..." and "Content-Type:multipart/..."
were both treated as absent. The value pointers were then fixed offsets that
assumed exactly one space.
Add header_value(), which matches a lower-case name anchored at the line
start, folds the request bytes to lower case as it compares, and returns
the start of the value past any blanks, so a call site no longer adds the
name length by hand. Cookie scanning reuses the returned pointer, and the
end-of-header test becomes the strstart() the file already has.
The tail of handle_post() interleaved the config and firmware upload
paths behind one config_upload conditional. Give each path its own
function, handle_config_fragment() and handle_firmware_fragment(), and
reduce handle_post() to routing.
Both handlers keep their own copy of the buffer-and-bounds-check
prologue: with SDCC a shared helper costs 2 bytes of direct RAM for
the fragment pointer (a spill slot when the parameter lives in xdata,
a DSEG home when it does not), and direct RAM is fully allocated. The
memory layout is unchanged from the pre-split code.
cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last and cfg_bl are used only
by config_take(), so move them out of file scope and into the function.
The five cursors stay static: SDCC register-caches automatic xdata
locals and spills them through DSEG slots, which costs 6 bytes of direct
RAM this build has no room for; static keeps the access pattern of the
old globals and the memory layout is unchanged. cfg_bl produces no spill
slot at the current register pressure, so it is a plain local. The
mechanism is recorded in issue #386.
handle_post() reused cfg_pos and cfg_end as scratch for unrelated
values; those uses get their own named locals, frag_len and
payload_start.
The pointer is masked when it advances over the command text, then
incremented once more for the newline without masking, so it can come to
rest one past the end of the ring.
Both readers walk from the pointer with their own index masked every
step, so an index that sits outside the ring is never reached and the
walk does not end. `history` on the console spins there, and so does
`/cmd_log`, which the web page fetches whenever settings are saved: it
fills the transmit buffer and keeps going past it.
This PCB is used in RY-8GT-1SX. The UART has swapped Rx and 3.3V VCC as
documented in `doc/devices/FG-8GT-1SX.md` so I imagine there would be
a revision later, after the current '2622' batch, like the RY-4GT-2SX /
FG-4GT-2SX_V2.0 pair from the same vendor.
The member mask is filled with a loop that stops one short, so the
highest front panel port never gets its bit and stays isolated from the
port that was just opened up. Every other loop over the port range in
the tree treats machine.max_port as the last port rather than the one
past it.
An empty slot used to read back 0xa0 per byte and the filter dropped it,
so the field came out empty. Block reads report the failure instead and
leave the buffer alone, and these two callers ignored that and printed
what the last successful read had left there.
Say nothing when there is nothing to read, which is what the other eight
callers of sfp_read_block() already do.
The port argument went through its own digit test and machine table
lookup; cmd_parse_port_separator() does both and also checks the port
exists on this board, which the open-coded test did not.
A port priority was masked to its top nibble, so "prio 17" quietly became
16. The bridge priority next to it refuses what it cannot represent, and
now the port priority does too.
Both only ever served stp_fdb_update(), so they belong there. Internal RAM
has room for them on this branch and on the one that carries the
aggregation module too.
stp_enabled is a flag, so say bool. cmpBytes() returns a comparison
result, so say int8_t. The three busy waits this branch adds read the
status straight out of the SFR instead of copying four bytes to xdata
first.
A port held out of forwarding because a loop was seen on it stops hearing
the frames that justified the hold, so its BPDU age climbs. The auto edge
branch lives inside the same countdown as the hold, reads that age, and
promotes the port back to forwarding after three seconds. The loop opens,
a BPDU comes back, the block is applied again, and the two take turns.
Measured on a looped pair present when the tree came up: twenty cycles in
one window and fifteen in the next, each block followed by a promotion,
still going when the capture ended.
The countdown alone cannot tell a loop hold from an ordinary listen, so
record the hold and leave the port alone while it stands.
make clean left tools/output in place, so the host tools were never
rebuilt after a clean. SUBDIRSCLEAN, which builds the per-directory
clean target names for exactly that purpose, was defined but nothing
consumed it. Give clean and distclean those targets as prerequisites
and add the rule that runs the subdirectory's own clean, which
tools/Makefile already provides.
tcpip_output() decides whether to splice in the 802.1Q tag, and that
decision also moves the frame: the header is shifted forward over its
padding, so a tagged frame starts at uip_buf with the q_frame layout,
while an untagged one keeps the padding and the nonq_frame layout.
nic_tx_packet() took that decision a second time, from management_vlan
alone. That agreed while the sender suppressed the management VLAN around
the transmission, but not since the tag is skipped per frame for a
CPU-tagged one: the descriptor then still sits behind the padding while
the transfer is set up for the shifted layout. The frame goes out four
bytes early and its length is read from the offset where tx_seq and
chksum_flags live, so a sixty byte BPDU is sent as around 1800 bytes of
whatever follows it. Looped back it is no longer a BPDU, the port hears
nothing, auto edge promotes it and the loop stays open.
Record the decision where it is taken and let the transfer follow it.
A variable rather than an argument because internal RAM is full once the
aggregation module shares the image: the overlay area ends at 0x7f, and
an argument or any temporary for the condition no longer fits.
The hand rolled loop was here because atoi_short() reported "no digits"
but not overflow, so 65540 would have wrapped to 4 and opened a
connection to a port nobody asked for. #326 gave it the same bound this
loop was carrying, so the reason is gone.
Zero still covers all three ways to be wrong, except that "no digits"
and overflow now come back as a zero return and port zero as a zero
value, so both are tested.
Three xdata scratch variables go with the loop.
The loop tested the digit range and the overflow bound with separate ifs,
on the strength of a comment saying the joined form makes sdcc park a
boolean in the bit area, and that an image carrying LACP and STP as well
is one bit short of needing a third byte of it. Measured on this branch
that is not what happens: the joined form links, the bit segment is
unchanged at fourteen bits overall and nine in cmd_parser, and BANK2
comes out ten bytes smaller.
The claim was about an image this branch does not build, and no branch
currently carries LACP, STP and this parser at once, so there is nothing
left to check it against. A justification nobody can verify, for a shape
nobody would otherwise write, is worse than no comment at all.
Behaviour is unchanged. The two forms agree on every input from 0 to
99999 and on the edges: empty, non digits, trailing junk, leading zeros,
65535 and 65536.
The note about atoi_short() stays, because that one still reports "no
digits" without reporting overflow. #326 replaces it with a version that
does both, and this should move onto it once that lands.
Saving the configuration drops the syslog server address whenever the log
of executed commands also contains "syslog on", which it almost always
does, since you have to enable the thing after pointing it somewhere.
parseConf() deduplicates by taking the matched text of a conf_overwrite
entry and removing every stored line that starts with it. For "syslog on"
the entry that matches is the catch-all ^syslog\b, so the matched text is
just "syslog" and the filter removes anything beginning with "syslog ",
including the "syslog ip ..." line saved a moment earlier. The address then
falls back to 0.0.0.0 on the next boot.
It fails quietly: nothing is reported, the running switch keeps logging
until it is restarted, and the loss looks like the firmware forgetting a
setting rather than the page dropping it.
Narrow the entry to the commands it is meant to collapse, so the address and
the port keep their own entries and are no longer collateral.
That alone would stop "syslog on" and "syslog off" from replacing each other,
because the list drops lines starting with the text it matched and "syslog off"
does not start with "syslog on". A separate list names the stem of such a pair
and collapses the two, which is what the catch-all used to do by accident.
The port was frozen at 514 in the call that opens the connection, so a
collector listening anywhere else was simply out of reach, and plenty do,
because 514 is privileged and containerised collectors usually get a high
port instead. Working around it meant putting a proxy in front, which then
rewrites the source address and hides which device actually sent the line.
Add `syslog port <n>`. The default stays 514, so nothing changes for an
existing configuration. Setting it tears the connection down and builds a
new one, the same dance the address already does, because uip_udp_new()
freezes the port into the connection.
The value is parsed here rather than through atoi_short(), which reports
"no digits" but not overflow: 65540 would quietly wrap to 4 and we would
open a connection to a port nobody asked for. Checking before each multiply
keeps it in 16-bit arithmetic. Zero then covers all three ways to be wrong -
no digits, past 65535, or an explicit "0", which is not a port either.
Whitelist the new command in config.js as well, next to `syslog ip`, or the
line is dropped without a word when the configuration is saved and the
setting quietly fails to survive a reboot. The dedup entry has to precede
the general `^syslog\b` one, otherwise it would deduplicate against
`syslog on`.
itoa_short() is new: itoa() only handles a byte and print_short() prints
hex, so there was no way to show a port as a decimal number. Kept separate
from itoa() rather than widening it, because every existing caller passes a
byte and would start paying for 16-bit divisions it does not need.
The scratch variables for the parser live in xdata. Locals there would take
internal RAM, and this image has none left: data ends at 0x77, the overlay
occupies 0x78-0x7f and the stack starts at 0x80. Measured: no module's DSEG
grew.
RFC 3164 has a hostname between the priority and the message text, and we
were sending nothing there. A receiver still has to fill that field, so it
takes the first word of the text instead, so every line arrives attributed to
"STP:", or "REGGET:", or whatever the log happens to start with. The sender
is then impossible to select on: the name changes with the content, and
anything in front of us on the path replaces the source address with its
own.
Observed on a collector behind a UDP proxy: the same switch showed up as two
different hosts depending on which subsystem had logged last.
Send the configured hostname. Skipped when it is empty, so we never emit a
lone separator.
The header length lives in xdata rather than on the stack. syslog_callback()
is __banked, so a local there cannot be overlaid, and while the variable
itself is only two bytes it costs more than that: it takes a register, which
pushes log_start out of one and into internal RAM. Those two bytes are
enough to stop an image carrying LACP and STP from linking at all.
Both conflicts are the register keyword removal meeting a change of ours,
so each takes main's signature and keeps what the branch was saying:
nic_rx_packet() still returns a bool, and port_ingress_filter_get() stays
declared.
main has since put its own copy of the STP module in BANK2, so the pragma
this branch carried is now duplicated. The one with main's comment stays.
Generate wrong address for `a4` location.
Both on read from and write to a __sfr32 variable.
Workaround:
Use two __sfr16 instead of one __sfr32.
We still get some optimalizations / better code gen.
The preamble scan cursor and the buffered-length parameter carry no
state between calls, so they do not need static xdata slots. As plain
locals the compiler places both in registers, trimming 64 bytes of
BANK1 code and two bytes of xdata.
The offsets shared by config_take() stay static: direct data space is
fully allocated on machines like the SWTGW218AS, so plain locals there
add overlay bytes that no longer link, and xdata-class locals spill
three temporaries into direct space while growing the code by roughly
120 bytes. Document pre_acc, whose accumulation across TCP segments is
why it must remain global.
A firmware upload previously ended in a silent connection close, leaving
the client unable to distinguish a verified upload from a failed one.
Send an explicit 200/400 verdict with the CRC result, with
Content-Length so the browser completes the response before the reset,
and only reset the chip once the verdict has been fully ACKed.
The unconditional close after a config upload is gone since the
buffered config path answers with its own response, so drop the
now-unreachable close hack from the streaming path.
A firmware image cannot be buffered whole, so the upload is streamed to
flash, and stream_upload() already resumes across TCP segments. The
multipart preamble did not: handle_post() walked the part headers from the
start of whichever segment it held, so a client that split inside the
octet-stream part header lost its place, never started streaming, and the
request hung. Firefox splits exactly there, right after filename=".
Buffer the multipart body only until the octet-stream part header is
complete, locate the payload, then stream from that point; later segments
stream as before. The header reuses the configuration buffer, which is idle
during a firmware upload, so no extra memory is needed.
This PCB appears only slightly different to the already supported and
documented PCB-SWTG115AS-V2.0. It works without any changes to the
machine definition.
While at it, also fix a typo (8272 instead of 8372), and include the
existing unused pictures on the device page.
cmd_parser.c conflicted twice: main rewrote the pvid command around the
new port-separator and atoi helpers, next to the line where this branch
delegates "stp" to stp_parse(). Both belong, so the STP delegation keeps
main's pvid body.
The linker caught what the merge could not see: main changed atoi_byte()
to return the digit count and leave the value in atoi_results_u8, where
it used to return non-zero on failure and write through a pointer. Note
the sense is inverted, so the three calls in rtl837x_stp.c are adjusted
rather than just re-arranged.
This branch moved rtl837x_pins to the common window in eaff953 to reclaim
BANK2 for the per-port status work, which was free at the time because the
module was only the I2C and GPIO pin helpers.
main has since put the SFP EEPROM transfer there, so the module now weighs
934 bytes and the common window is 156 bytes short. BANK2 has room for it
again, and that is where main keeps it.
Common segment +934 bytes, BANK2 -934.
Only httpd.c conflicted: main added the buffered configuration upload
next to the pointers this branch had moved into xdata to free internal
RAM. Both belong, so the new globals sit above declarations that keep
their storage class.
All entry points are already __banked and none of the code runs in
interrupt context, so the module can leave the resident bank. This
relieves pressure on bank 0, which no longer fits a machine with a
custom init table.
Removing the CPU-port support, As stated in #334, it is not needed to
manual add the CPU-port to any command. Because the CPU-port should be
added automatilly when CPU-port is needed.
Refactor cmd_parse_port() because port number is only 1 byte.
This simplifies the parsing code.
Refactor cmd_parse_port_separator() to ensure the return size is correct.
cmd_parse_port_separator() parse the full port number and checks that the
number end with a NUL of space.
Now this function can also be used to parse the last argument number
because this ends with a NUL.
Also refactor code.
Measured against the generic leds_setup() output on the board: eight of
the 21 stock values are identical to what leds_setup() computes from
the machine's led_mux table, and eight more revert to the generic
values with no change in LED behaviour in any tested link state (copper
2.5G, SFP 2.5G, SFP 10G). Keep the five with a measurable effect: 6528
selects blue over green at 10G, 6540/6548 carry the SFP and copper LED
set behaviour, 65dc enables the LED outputs, and PIN_MUX_0 routes the
blue pin.
Verified from a clean boot on a SWTG018AS-V2.1.0 board: copper solid
green with activity blink at 2.5G on two ports, SFP green at 2.5G,
SFP blue at 10G, LEDs off on link down.
machine_custom_init() runs once at boot, but its code and any tables it
uses were compiled into machine.c and so into the common bank. On the
SWTG018AS-V2.1.0 variant, whose init carries a 21-entry LED register
table, that overflows bank 0 by 0x66 bytes and main no longer links for
MACHINE_PCB_SWTG018AS_V2_1_0; any machine whose init grows can hit the
same wall. SDCC segment pragmas apply file-wide, so the hooks move to a
new machine_init.c compiled into BANK2, and the prototype becomes
banked. machine_check also compiles the new file per machine so the
hooks keep CI syntax coverage.
The I2C controller transfers up to sixteen bytes per transaction and
page_impl.c already used that for sfp_send_data(), while sfp_read_reg()
asked for one byte and every caller looped. Reading a module therefore
cost one address phase per byte: 87 transactions when a module is
inserted, 52 for the sfp command, 36 for the vendor block in status.json.
sfp_read_block() replaces sfp_read_reg() and the callers that already
wanted a run of registers ask for it once: the vendor fields as three
16 byte pages, the diagnostics as one transfer, rate and encoding
together. That drops the three paths above to 8, 6 and 3 transactions,
and sfp_send_data() loses its copy of the transfer.
The vendor loops now run over 16..63 rather than 20..59 so the page base
is a multiple of sixteen and the index into the buffer is a single AND.
The four extra bytes at each end are read and discarded. The diagnostics
read asks for 16 bytes rather than the 15 it uses, because 16 is a width
the shipped firmware already exercises and 15 is not.
The device address, the bus selection and the start bit go into the
control register in one write now that the memory address is written
first, so a transfer touches that register once instead of three times.
The register reads take their result from the SFRs directly rather than
through the sfr_data mirror. The result is a bool and the destination is
sfp_buf, so a caller that cares about a failed transfer looks at the
return value instead of a flag.
Every caller gives up on the first failed read rather than carrying a
flag to the end, which is why the module read moved out of handle_sfp
into a function of its own. A module whose read fails is left marked as
absent, so the next poll retries it instead of configuring the SerDes
from bytes that never arrived.
BANK1 -194 bytes, BANK2 +382, common segment +44, xdata +15 for the
buffer, and one byte more of internal RAM free than before the series.
Built for all 25 machine definitions on sdcc 4.5.0; the tightest common
segment is 98 bytes free on SWTG024AS_V2_0, against 54 before this
series.
CONFIG_UPLOAD_BUF at 2560 capped a configuration upload at about 2.2K
while the config sector holds 4K. Size the buffer as CONFIG_LEN plus
room for the multipart framing so the whole sector is usable.
config_take() wrote the payload without checking it against the sector:
safe before only because the buffer could not hold an oversized one.
Reject a payload that does not fit CONFIG_LEN, terminator included,
instead of writing past the erased sector.
send_config() streams a configuration larger than the TCP output buffer
through the cont_addr/cont_len continuation, but set cont_addr to the
offset within the config instead of a flash address, so the tail was read
from code space. The file server sets cont_addr absolute; do the same by
adding CONFIG_START.
The bug was unreachable while CONFIG_UPLOAD_BUF capped uploads below
TCP_OUTBUF_SIZE, but a stored config near the full sector exposes it.
sfp_read_reg() sat in rtlplayground.c, so it occupied the common 16 KB
window that every bank shares, even though nothing outside the SFP paths
calls it. That window is the tightest resource in the image:
SWTG024AS_V2_0 and SWTG024AS_A_2_0_1_5C_1SFP had 54 bytes left in it.
rtl837x_pins.c is already in BANK2 and already holds the I2C bus helpers
this function calls, so the transfer belongs there. The function moves
verbatim and becomes __banked; the prototype in rtl837x_common.h says so,
which is what keeps the callers in BANK1 and BANK2 honest.
No behaviour change. The common segment gains 200 bytes on every machine:
159 to 359 free on SWTGW218AS, 54 to 254 on the two variants above.
BANK1 +6 bytes, BANK2 +336. Built on sdcc 4.5.0.
Saving the configuration works in Chrome and fails in Firefox, and the
difference is only how each browser splits the request. The handler
erased the config sector as soon as the request started and then parsed
the multipart body one TCP segment at a time, which requires every
boundary and every part header to fall inside a single segment. Firefox
splits inside a part header, so the parser lost its place and the
erased sector was left holding a truncated body or nothing at all. A
single-burst scripted post lost the whole body the same way.
The configuration is limited to two kilobytes, so the whole request body
now accumulates in xdata and is parsed only after the closing boundary
has arrived. The parts are walked in one pass, the part carrying a
filename is written to a freshly erased sector, and the client receives
a 200 instead of the previous silent close. No segmentation can confuse
this, since the parser only ever sees a complete body.
Locating the closing boundary first also bounds the walk over the parts,
since none can lie beyond it, so the length of the buffer is no longer
the bound and the test for the two trailing dashes is unnecessary.
The walk matches at offsets inside a buffer that is not terminated, so
neither existing helper fits: strcmp() goes on to compare the byte after
the match and is_word_x() demands a separator there. Add strstart() and
strstart_x() for that case, and use strlen_x() for the boundary length.
The firmware upload path still streams, since a megabyte cannot be
buffered, and is untouched.
The bounded waits were silent: on timeout the code went straight back to
its caller, and handle_rx() then read a frame the DMA may never have
delivered, which is worse than waiting longer.
Each of the three transfers now says so on the console, the two RX ones
report failure to handle_rx(), and handle_rx() acknowledges the packet
and gives up on it rather than parsing whatever is in the buffer. The
guard variable moved to the top of its function, so the block that held
it and its indentation are gone.
The comment said the ASIC never completes a TX when the egress port is in a
non-forwarding MSTP state. The guard is worth keeping either way, since an
unbounded spin in the DMA wait takes the whole main loop down, but the
mechanism is more than the evidence supports and the DMA into the TX ring
has no business knowing the egress port at all.
The variable lives in rtlplayground.c, so it is declared in rtl837x_common.h
with the others there, and it follows the naming of the rest.
The header carried comments on the externs that the definitions in
rtl837x_stp.c repeat, sometimes differently, which is one place too many to
keep in step. What only the header had, the value ranges and what the
designated arrays hold, moved to the definitions; the rest is gone. Function
declarations lost their comments too.
The status printer only prints, so its running commentary went. A define
replaces the bare 33 in stp_in(), and the note on the loop check is down to
what applies at that line.
A blocked port does pass a received BPDU up to the CPU. The evidence is in
logicog's capture of a looped pair: the port the loop check had already
blocked kept reporting a BPDU age of zero seconds across dumps taken more
than a forward delay apart, and that counter is only cleared in stp_in().
That is also what makes the loop latch work, since the port that stays
forwarding has to go on hearing the blocked one.
The tag flag comment said the source address is not learned on the egress
port. doc/CpuPort.md defines it as not learning the source address from the
frame at all, which is the narrower claim to make. The dangling heading
above it described a field that is documented there too.
802.1D puts the port's own address in the source field and the bridge
address only in the Bridge Identifier. We used the bridge address for
both, and on this hardware that costs the management path.
Measured on a SWTGW218AS: the ASIC learns the source address of a frame
addressed to 01:80:c2:00:00:00, and the bridge's own address is not
exempt. A BPDU that leaves a blocked port and comes back on a forwarding
one therefore moves the management address off the CPU port, and frames
for it are then sent down that port instead of to the CPU. Traffic
between other stations is unaffected, which is what makes it look like
the CPU port has been blocked.
The derived address keeps the bridge address and sets the locally
administered bit, so it differs from the bridge address in the first
octet for any globally assigned OUI, with the port number in the low
nibble of the last octet. Nothing here reads the source address of a
received BPDU; the loop check compares the Bridge Identifier.
Declaring port at the top of stp_in() and stp_parse() does the same job
without a block that is not indented like one. The static xdata copy in
stp_in() went with it, it was only ever written.
The argument count check in stp_parse() that lost its comment guards
against cmd_compare(4, ..) reading a stale word from the previous command
line, because cmd_words_b is not cleared between commands.
tcpip_output() splices the 802.1Q tag in right behind the source address,
which is exactly where the ASIC expects the RTL tag of a frame the CPU
addressed to a port itself. The tag then ends up behind the VLAN tag, the
ASIC does not find it, and the frame goes out flooded with the 0x8899
header still on it instead of being sent to the port that was asked for.
Whether a frame is CPU-tagged is a property of the frame, so decide it
here from the ether-type rather than having every sender of such a frame
clear management_vlan around its tcpip_output() call. stp_cnf_send() did
that, and no longer has to.
The comments they replace are gone with them. Two things the comments
carried that the names do not:
Accepting version >= 2 rather than == 2 is deliberate. 802.1D-2004 14.4
has an RSTP bridge accept a higher Protocol Version and treat it as RST,
and MSTP sends version 3 type 2 with a prefix identical to an RST BPDU
for exactly that reason, so insisting on == 2 would make us blind to
every MST bridge on the segment.
In the TCN branch stp_cnf_send() transmits by itself, so uip_len is
cleared afterwards to keep handle_rx() from sending the frame twice.
Worth keeping out of the code but on record: RTL_TAG_KEEP is deliberately
not set here. On an LLC/802.3 frame the ASIC drops the frame outright with
that flag, while on ethertype frames such as LACP it works fine.
The port number comes out of a received BPDU, so the range check is
there to keep a forged frame from naming a port this module does not
manage - including the CPU port, which would cost us the management
path. Nothing outside min_port..max_port would ever release the block
either, because stp_timers() only counts down the ports it walks.
The three xdata bytes and the reused stp_we_root scratch are gone; the
compiler needs a register for an index either way. Frees four bytes of
xdata and 29 bytes of code.
The tree structure already crossed the switch by regeneration, but the
topology-change information did not: a received TC flag was ignored and
a TCN only acknowledged, so bridges behind this one kept stale entries
until normal aging.
A TC flag in a received BPDU now flushes the other non-edge ports once
and arms the transmit window our BPDUs already copy the flag from,
refreshed to hello+1 seconds by every further flagged frame so it ends
one hello after the neighbour stops, without shortening the longer
window a local change arms. A TCN is acknowledged as before and then
treated like a local change on that port.
The table could say a port was designated and had heard nothing, which is
two different situations wearing the same face: either we are not
announcing on that segment, or we are and nobody is answering. Telling them
apart needed a capture on the far side.
port state role edge tx bpdu
05 fwd desg yes 2a 255
01 block desg no 2a 21
03 fwd root no 00 0
The tx column counts BPDUs actually handed to the hardware, so it moves
only past the enable, filter and tx hold checks in stp_cnf_send(). A
designated port has to show it climbing once per hello time. The root port
never does, because we do not announce back towards the root, so a
neighbour that has taken us as root falls silent in both directions on that
link and the two columns together say exactly that rather than looking like
a fault.
The counter is a byte and wraps at 256. It is meant to be watched moving,
not summed, and it starts again when STP is enabled.
67 bytes of BANK2 and 10 of xdata, nothing in BANK1 or internal RAM. Built
for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
The table printed the ASIC's raw two bit state, a 1 or a 2 for the role and
a 1 or a 0 for the edge flag, so reading it meant having the source open
next to the console. The columns carry the words now:
port state role edge bpdu
05 fwd desg yes 255
01 block desg no 21
02 learn desg no 5
03 fwd root no 0
They come from fixed width tables indexed by the same values as before, so
nothing about how any of the three is derived changes, and the columns line
up under the header without a formatter.
The role column still only tells the root port from everything else,
because that is all the state machine tracks. A port sitting in blocking
because a better BPDU arrived on it reads as designated here. Naming the
column makes that visible rather than introducing it.
154 bytes of BANK2, nothing in BANK1, xdata or internal RAM. Built for
SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
The status output named state, role and edge, none of which separates a
port nobody is speaking (R)STP to from a port whose BPDUs we are
dropping. Both look the same: forwarding, designated, edge, and the tree
rooted at ourselves. stp_in() leaves on eight different conditions, from
a short frame through an unexpected LLC header to a disabled port, and
none of them says anything.
stp_bpdu_age was already maintained for the ageing rules, so this only
prints it, in seconds and capped at 255. A column that counts up means
nothing is arriving; a column that stays near zero means frames are
arriving and any disagreement about the tree is ours.
Eighty two bytes of BANK2 and two of xdata, most of it the sixteen bit
divide by the tick rate. It comes out of a branch that gives back three
hundred and twenty eight, so it is affordable, and printing raw ticks to
save it would put the reader back to converting in their head.
A Bridge Identifier is two priority octets followed by the MAC, compared
as one unsigned number. The test here read the first priority octet and
then went straight to the MAC, so the system ID extension in between was
never looked at and two bridges differing only in it were ranked by MAC
instead. The field is stored, sent and printed, just not compared.
Ordinary single instance RSTP leaves the extension zero on both sides,
which is why this has not shown up. Where it is not zero the ranking is
simply wrong: same priority octet, extension 0x0a against 0x00, and the
worse bridge wins if its MAC happens to be lower.
cmpMAC becomes cmpBytes with a length, since the identifier is eight
contiguous bytes in both the packet overlay and root_bridge, and the
loop was already doing the right thing for six of them. sdcc lays the
struct out with no padding, checked, so the eight byte compare is the
standard's rule written directly.
802.1D has a port leave the edge state when a BPDU arrives on it. Here
the flag was only ever cleared by the loop latch, root guard, a link
coming back, "stp on", "stp off" and the edge command itself, so a port
that auto-edged during the three seconds of silence after link-up kept
the flag for as long as it stayed up, whatever the neighbour sent.
Two things read that flag. The status page prints it, so a port talking
to a bridge reported edge 1 and there was no way to tell from the output
whether a BPDU had ever arrived. More quietly, stp_topology_change()
returns early for an edge port, which is right for a real one and wrong
for this: a topology change on such a port was neither counted nor
propagated, and port_l2_forget_port() never ran, so what was learned
behind it stayed in the table.
Only the flag is cleared. The port is not pushed back through the listen
period, which would take a working link out of forwarding for a forward
delay the first time a neighbour speaks.
"stp status" has always worked, but the line printed on a bad command
listed only on and off, so the one subcommand that shows what the bridge
thinks was the one you had to already know about.
The window could be armed from the serial console but only ever disarmed
by an HTTP request. save_cmd, which gates arming, is cleared only while
execute_config() replays the startup config, so every interactive command
armed it wherever it was typed, while mgmt_alive, which disarms it, was
written in exactly one place, on HTTP traffic. An operator working
entirely on the serial console therefore lost STP 180 seconds after
enabling it however much they typed, which is what makes the mechanism
impossible to test from a console.
The documentation described the behaviour that was intended rather than
the one that was built, and in both directions: it said a command on the
serial console also confirms, and it said a reboot with STP in the
startup config disables it again three minutes later. Neither held. The
replay path never armed the window at all.
Repairing the asymmetry would have kept a mechanism whose premise is
contested anyway. A watchdog that switches the protection off in response
to silence adds a second failure mode on top of the first: where the
network is misconfigured and STP is the thing holding a storm back,
restoring forwarding removes the last reason management still answers.
Gone with it: the stp failsafe command, the fs and fsT fields of
/stp.json, the input and the tripped banner on the Spanning Tree page,
the two persistence patterns in config.js, the documentation section, and
mgmt_alive itself, which had no other reader.
550 bytes back, 145 of BANK1 and 405 of BANK2, and five of xdata, which
is the four counters and mgmt_alive and nothing else. Built for
SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
Your console session shows the shape of this better than I could have. STP found
a loop, blocked the port, unblocked the other side of the pair, and then the
failsafe turned STP off. It had been disarmed by your first console command and
re-armed by the loop detection itself, so a mechanism that exists to protect
against a lockout ended up removing loop protection while a loop was physically
present. That is the part that did not make sense, and it wasn't the console.
Two changes, both narrowing.
Loop detection and root guard no longer arm the window. Those are the protocol
doing its job on evidence off the wire. Nothing an operator did needs undoing
there, and nobody is waiting to confirm anything.
Typing on the serial console no longer disarms it. The failsafe asks one
question, whether the operator can still reach management over the network, and
serial activity doesn't answer it. It proves somebody is standing at the box,
which is the one case where a lockout doesn't matter, and it took the safety net
away from a remote operator on behalf of someone not using it. HTTP activity
still confirms, because that is the path being measured, and the console in the
web interface counts for the same reason.
What is left arms on stp on, stp port N on and stp failsafe, each of them an
operator choosing something whose outcome the protocol then decides.
Gives back 61 bytes of BANK2, 31 of the common area and a byte of xdata.
Two things from the review, both about the console being where you end up when
the tree is not what you expected.
The six messages that name a port were printing the internal index. On a board
whose map is not the identity that is a different number from the one written
next to the socket, which is worse than no number at all. They go through
machine.log_to_phys_port now, in a small helper that also swallows the newline
each of them repeated.
"stp status" prints the bridge and root IDs, the root port and path cost, the
topology change count, the failsafe setting, and a line per port with state,
role and operational edge. Everything it shows is state the module already
keeps, apart from the port states, which come from one read of MSTP_STATES.
660 bytes of BANK2, which leaves 3366 free. No internal RAM, no xdata.
The report on the PR is that it will not link for KP_9000_6XHML_X2, with
"?ASlink-Error-Could not get N consecutive bytes in internal RAM for area OSEG"
five times over. It builds here on sdcc 4.2.0 and 4.5.0 for that same machine
and the same commit, so something in the toolchain differs, but the pressure it
is complaining about is mine and it costs little to give back.
stp_in() is __banked, so its temporaries get exclusive DSEG instead of
overlaying with anything else. Recording the designated bridge put four more
live values across a memcpy in the middle of it and the register allocator
answered with five spill locations. The module went from 5 bytes of DSEG to 12,
and from 17 sloc references to 49.
Moving that block into a __reentrant helper puts its temporaries on the stack
instead. The module now claims no DSEG at all, 5 bytes better than before the
recording was added, and the image sits at 95 bytes of DSEG against 101 on main.
It costs 170 bytes of BANK2, where there is room.
The failsafe is a commit confirm window for an interactive change: turn STP on,
and if management goes quiet for stp_failsafe_s seconds the switch undoes it.
The three places that arm it sit in the command parser, and execute_config()
drives that same parser at boot, so a saved "stp on" arms the window too. A
switch that reboots with nobody watching then turns its own STP back off.
Measured on a SWTGW218AS with "stp failsafe 180" in the saved config: cold boot,
no HTTP and no console for four minutes, and "STP failsafe: disabling" arrives
on time, with stp.json reporting on:0 and fsT:1.
execute_config() already clears save_cmd while it replays and sets it again at
the end, so the three parser sites can just test it. The two on the protocol
side, the loop latch and the root guard, stay unconditional. They react to what
arrived on the wire, which is the case the failsafe exists for, and they only
run once the replay is long finished.
BANK2 grows 24 bytes. Nothing else moves.
stp_dbridge, stp_dpid and stp_dcost were declared and read by the status page,
but nothing ever wrote them, so they stayed zero for the life of the firmware.
The page's validity test then always failed, and the Designated Bridge,
Designated Port ID and Designated Cost columns reported our own values on every
port, including the root port where the answer is the upstream neighbour. The
three arrays reserved 140 bytes of xdata and never used any of it.
They are filled now, right after the loop check, so a frame that came back from
one of our own ports is not mistaken for a neighbour.
The validity test moves from the last byte of the stored MAC to the stored Port
ID. A Port ID is 1-based on the wire and cannot be zero, while a neighbour whose
MAC happens to end in 0x00 would have failed the old test.
The root path cost byte swap happens once now, and the root port branch reuses
the value instead of repeating the shifts.
BANK2 grows 148 bytes, BANK1 loses 9, and xdata does not move.
Three things from the page feedback.
The rows came out in logical order while carrying the physical port number,
so on the six-port boards the first row is labelled 5. Sorting the rows by
that number in JS puts every board back into front-panel order. I walked all
25 machine definitions and each one now yields a clean 1..N.
The Designated Bridge column is hard to read without knowing this switch's
own bridge ID, so the status line shows it in the same priority and MAC shape
as the cells use. On the test switch that reads 61440-06:05:16:1E:F9:24 and
matches the Designated Bridge of every locally designated port, which is the
comparison that was missing. The root bridge and the path cost now use the
same formatting as the columns instead of raw hex.
Enabling STP printed nothing until the next poll, and because the ports start
blocked, management can stay quiet for the whole listening and learning
period, so the page had no chance to say anything later. It now writes what
is about to happen before the command goes out, and how long the ports need.
Page data only. Both banks, xdata and the common bank are unchanged.
The review asked for these to come out and the reasoning holds. A guard that
gives up mid-transaction lets the code carry on with whatever the engine left
behind, and that is what cost me an SPI clip twice. An unbounded wait on a
wedged engine still hangs, but it hangs in a known place instead of writing
garbage into the L2 table.
BANK1 loses 45 bytes and xdata one. BANK2 and the common bank do not move.
Review asked for this across the other commits too. Gone are the blocks
that restate what doc/stp.md already says, the ones that explain what an
embedded programmer already knows, and one that had gone stale inside this
very branch: the CLI summary above stp_parse still described "cost <0-255>
(x1000)" while the parser has taken the raw 0 to 200000000 for some time,
and it never learned about p2p or trk at all. A usage list next to the
parser is the kind of thing that rots first, so it is out rather than
updated.
The review flagged one comment saying a variable is in xdata because the
internal RAM overlay is full, on the grounds that it may stop being true.
Four more of the same kind were in these files and are out as well, one of
them pointing at a file that does not exist in this branch at all. The
declarations still say __xdata, which is the part a reader needs.
Also out: the note on why three helpers are __reentrant, which was really
a paragraph about two bytes of DSEG, and the measurement story behind the
tick divider, which belongs with the other timer numbers in doc/stp.md.
One comment was not stale but simply wrong. "max BPDUs per port per second"
sat on stp_failsafe_tripped, having slid down two lines when the two
failsafe variables were inserted above it. It describes stp_txhold and is
back there now.
Short factual labels stay: they sit next to the magic number they explain
and the codebase uses them throughout. The generated code is byte for byte
what it was before this commit, both banks and xdata unchanged.
Review asked for this directly: the hardware layout above port_l2mc_set()
would be better as documentation than as a comment, keeping only the two
lines that say what the function does.
doc/l2.md gains a section on static multicast entries, why delivery uses
the forward action rather than the trap, and the SMI layout of the entry.
doc/CpuPort.md gains the layout of the tag's flags and pmask words, with
the byte order trap that cost an afternoon: writing the flags constant raw
instead of through HTONS puts 0x0020 on the wire as 0x2000, which is EFID
rather than LEARN_DIS, and the ASIC then leaves the 0x8899 header on the
frame.
The comments those paragraphs came from are replaced by a pointer to the
file that now holds them.
The note said a blocked port drops frames the CPU injects into it and so
cannot send BPDUs of its own. Hardware says otherwise, and it matters,
because that sentence is the reason one would go looking for a way to let
control frames out of a blocked port when there is nothing to fix there.
Held a two port group in blocking and watched from the neighbour. Our
BPDUs kept leaving it, 27 of them with a largest gap of 2.00 s, which is
the hello interval with nothing missed. Pings across the same port stopped
dead for 11.63 s in one unbroken gap, so data really is held. In the same
window the neighbour sent 74 frames with a largest gap of 1.04 s while our
receive counter for them moved by 2, and the port stayed in its trunk
throughout, so nothing in the aggregation code was discarding them.
The failsafe used to watch management traffic for as long as STP ran, so
three minutes of nobody looking at the web UI took the tree down on any
quiet network. That made a standing STP config impractical, which is the
problem raised in the review of the original PR.
Enabling STP arms a window of stp_failsafe_s seconds. One HTTP request
inside it confirms that management survived the new tree and disarms the
watchdog; a silent window disables STP and restores forwarding. Both
outcomes print to the console and the syslog.
The window re-arms on any later event that newly takes a port out of
forwarding: a port rejoining via "stp port N on", root guard firing, the
loop latch. Those were covered by the old always-on surveillance and a
disarmed window would have left them able to cut management off for good.
If management traffic keeps flowing past the new block, the next request
confirms straight away, which is the correct verdict, the block did not
cut it. The arming deliberately does not refresh an already armed window:
root guard can re-fire on every hello, and refreshing the countdown on
each one would keep a cut-off window from ever expiring. A stable network
with nothing newly blocked never re-arms, which is the reviewed-for
behaviour.
The request or console command that causes the arming never counts as its
own confirmation: mgmt_alive is cleared when a command arms, and the
console hook only disarms when the window predates the command. Without
that, enabling from the web UI or the console would confirm the window
before the new tree had any chance to cut management off.
A command on the serial console confirms like HTTP does. An operator at
the console has out-of-band access that no tree can cut, so the automatic
restore only takes STP away from someone equipped to deal with the
situation. The hook sits on the interactive console path only, identified
by cmd_available, so neither the config replay at boot nor HTTP commands
pass through it.
After a confirmation STP runs unsupervised until something new blocks.
Headless installs where nobody will confirm should set stp failsafe 0;
doc/stp.md says so.
Costs two bytes of XDATA, the armed flag and the console-path snapshot;
stp.rel and rtlplayground.rel keep their segment sizes.
A port set to admit tagged frames only will never see a BPDU, because
delivery rides the forward action and the ingress pipeline drops untagged
frames before the L2 lookup. The failure is silent and looks like a dead
receive path: the port turns edge after three seconds, the bridge elects
itself root, and nothing hints at the ingress setting. Diagnosing exactly
that cost most of a day on a live switch, with the neighbour provably
transmitting the whole time.
stp_setup() now prints one line per affected port, so the hint lands at
"stp on" and at every config replay on boot. The check runs in its own
loop after the MSTP write: port_ingress_filter_get() reads a register
into sfr_data, which the state-building loop above is still using. The
port number in the message is physical, matching what the ingress
command takes.
doc/stp.md explains why this can happen here and not on a normal bridge,
where BPDUs are consumed before any VLAN classification.
stp.rel stays at DSEG 5 with no OSEG and the image at 10498 bytes of
XDATA.
Every RST BPDU we sent carried flags 0x3c - designated, learning, forwarding -
whatever the port was actually doing. A blocked port kept announcing itself as
forwarding, and the root port would have called itself designated. Nothing on
this bench acted on it, but it is a lie in the protocol frame and the kind
that surfaces in somebody else's mixed network.
Derive the flags instead: the root port reports the root role, every other
transmitting port is designated (alternates do not transmit at all), and the
learning and forwarding bits mirror the ASIC state, so a listening port now
sends 0x0c. TC and TCA stay dynamic as before. Legacy Config BPDUs are
unchanged - their flags only ever carried TC and TCA.
Costs nothing in internal RAM; the state comes from the register scratch that
is already there.
Two of our own ports on one segment blocked each other in turn instead
of settling. The guard on the loop path only acted when port_timers[]
had already run out, so a BPDU arriving while the port was blocked did
nothing: the timer expired, the port went forwarding, the loop reopened
and the pair started over. The comment above the code claimed the
opposite - "if the loop persists the BPDUs keep arriving and the port
stays blocked" - but nothing implemented it.
Measured on a SWTGW218AS with a patch cord between two free ports: both
ports blocked, both returned to forwarding one forward delay later, and
the topology-change counter reached 0x51 in 5.5 minutes - 15.6 changes
per minute for as long as the cable was in.
Let the better Port ID decide for both. That port is forwarding by
construction, so it goes on hearing the loop and re-arms the other
port's timer on every BPDU, which is what turns the block into a latch;
the held port only has to keep transmitting, which the send path already
allows in any MSTP state. Nothing here depends on a blocked port still
receiving - that was never established. Having one writer also removes a
race: while both ends decided for themselves, the winner's re-arm could
land in the loser's port_timers[] first, the loser read it as "already
blocked" and skipped its own state change, and the loop stayed open.
802.1D compares the priority before the number and stp_cnf_send() puts
stp_pprio[] on the wire next to it, so compare that first - otherwise
"stp port N prio" would quietly not influence which end of a looped pair
keeps forwarding.
The port number arrives in a frame and our bridge MAC is public in every
BPDU we send, so bound it to the ports this module manages before
indexing anything. Outside that range nothing would release the block
either: stp_timers() walks min_port..max_port and skips ports that are
not STP-enabled, so their port_timers[] never counts down.
Equal Port IDs mean the frame came back on the port it left - a loop
further out, behind an unmanaged switch. There is no pair to choose
from, so that port holds itself down; since it can only re-arm while it
is receiving, that case stays the forward-delay pulse it was before
rather than becoming a real latch.
The work sits in a __reentrant helper on purpose, like the two functions
above it: parameters and locals then live on the stack. Inlined into
stp_in(), which is __banked and whose temporaries cannot be overlaid,
the same code costs two more bytes of DSEG - enough to stop an image
that also carries LACP from linking at all.
Verified on hardware: with the loop in place for 1 h 36 min exactly one
port blocked, the other kept forwarding, and the topology-change counter
moved four times in total - three of them the link event and the
promotion in the first minute.
The wording read as a claim about the CPU interface in general, which is
wrong and misleading: the 8051 sits behind an ordinary port of the internal
switch and is an ordinary member of a forwarding mask - which is exactly
what this implementation relies on.
Say what is actually broken instead: the trap action, a separate mechanism
whose destination is an external CPU port these boards do not populate.
Record the measurements behind it, including the widened CPU_PMSK and both
external-CPU destinations, and add the ACL trap result - a rule matching the
group intercepts frames but does not deliver them either, which is a second,
independent path to the same conclusion.
Reported-by: vDorst
We only recognised RST BPDUs when the Protocol Version Identifier was
exactly 2, which silently drops every MST BPDU: 802.1s uses version 3
with type 2 and a prefix deliberately laid out to be identical to an RST
BPDU, precisely so that an RSTP bridge can parse it.
802.1D-2004 14.4 spells the rule out - a bridge shall accept a version
identifier of 2 or greater and treat the BPDU as RST, ignoring anything
beyond what it understands. Compare with >= instead of ==. The receive
path already length-checks before touching the body and only reads the
fields common to both formats, so a longer MST body needs no other care.
The two fields are deliberately asymmetric: the Protocol Identifier must
be exactly zero (it is a sanity check), while the version is an extension
point that has to tolerate the future.
The state machine never looked at link state, so a port whose cable was
pulled stayed in forwarding: it kept being announced, kept its learned
entries, and the most ordinary topology change there is went unnoticed.
Observed on hardware - a non-edge port with the link administratively
down still reported forwarding and left the topology-change counter at
zero for the whole observation window.
Sample the carrier bitmap once per second, alongside the tx-budget refill
(the 50 Hz tick has no business doing register reads). On carrier loss put
the port back to blocking and run the normal topology-change path, which
flushes just that port's entries. On carrier return re-run the listen
period rather than forwarding immediately - the segment may have been
rewired while we were down - and clear the operational edge flag so a port
that was auto-edged has to earn it again.
stp_setup() seeds the bitmap from the hardware so enabling STP does not
report every already-down port as a fresh topology change.
Covers enabling it, the bridge and per-port settings, how BPDUs are
delivered on this hardware (RMA forward constrained by a CPU-only static
L2 entry, since trap-to-CPU targets an external CPU these boards do not
have), the timer base, and the management failsafe - enabling STP over
the network can block the very port the management VLAN rides on, so
that part is spelled out rather than left to be discovered.
"stp port N edge off" cleared only the admin and auto flags, not the
operational one - and that is the flag the engine actually consults: it
exempts the port from topology changes and lets it skip the listen
period. A port therefore stayed an edge port until the next "stp off" /
"stp on", silently ignoring the new setting. Clear it with the others,
and mark an admin edge operational right away, as stp_setup() does.
The timers assumed stp_timers() runs at 64 Hz. It does not: the main loop
idles on the 200 Hz system tick and calls us every fourth pass, i.e.
50 Hz. Measured on hardware - with hello configured to 2 s the BPDUs left
the port 2.560 s apart, exactly the 28 % overshoot the wrong constant
implies, and every other timer (forward delay, max age, tx-hold refill)
was stretched the same way.
Move the constant to the header with the arithmetic spelled out, and use
it in the status page too, which had the 64 hardcoded and therefore aged
the same counters differently than the engine.
The Topology Change Acknowledgment is staged in a one-shot variable and
consumed when the BPDU is built - but stp_cnf_send() can return before
that, when the port is filtered/tripped or its tx-hold budget for this
second is spent. The flag then survived and was OR-ed into the next BPDU
this switch sent, on whatever port that happened to be. Clear it with
the frame it belonged to.
A port entering forwarding, or being blocked because its own BPDU came
back, changes where MAC addresses live - but the counter was bumped and
nothing else happened: our forwarding table kept the stale entries and
the neighbours were never told.
Flush the affected port's dynamic entries (bounded single-port flush)
and set the Topology Change flag in our BPDUs for max age + forward
delay, so neighbours age their tables out as well. Edge ports are
exempt: a host coming or going is not a topology change.
port_l2_forget() flushes the whole table and polls the flush engine
without a bound. A topology change only needs to age out the port that
changed, and the STP tick cannot afford an unbounded poll: add
port_l2_forget_port() with a single-port mask and the same bounded wait
the static-entry helper uses.
Two protocol-correctness fixes on the information we advertise:
Only designated ports announce periodically. The root port is where our
root information arrives; sending it back there feeds the upstream
bridge its own data and makes us look like a competing designated
bridge on that segment.
Relay the message age instead of always claiming zero. A bridge
increments the received age by one second per hop, so downstream
neighbours can age the information out; advertising 0 forever made our
BPDUs look eternally fresh no matter how stale the root information was.
Age stays 0 while we are the root ourselves.
Accept legacy Topology Change Notification BPDUs (v0, type 0x80): reply
on the ingress port with a Config BPDU carrying Topology Change
Acknowledgment so the sender stops repeating, and count the change.
Also stop reading fields past the end of short frames: require the
header through bpdu_type (33 bytes with the CPU/VLAN prefix) before
classifying, and the full 35-byte body before the election logic -
truncated or fuzzed BPDUs are dropped instead of parsed as garbage.
An RST BPDU body is 36 bytes: the Config-BPDU fields plus a trailing
version-1 length octet (zero - there is no version-1 information).
Ours was 35 - strict 802.1w parsers treat such a BPDU as malformed and
drop it. Add the field, keep legacy Config BPDUs at 35 bytes, and set
the 802.3 length accordingly (0x27 with LLC for RST, 0x26 for Config).
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)
Enabling STP on a bridge whose management rides an in-band VLAN can cut
off that very management - and not only by our own blocking: on this
network the upstream TP-Link Easy Smart switch's "loop prevention"
reacted to our BPDU hellos by blocking ITS port towards us while our
ASIC was all-forwarding, isolating the whole segment until a power
cycle. Recoverable only by going quiet.
Add a commit-confirm watchdog: while STP is enabled, any HTTP request
re-arms a countdown ("stp failsafe <seconds>", default 180, 0 disables);
if management stays silent for the whole window, STP disables itself,
which also stops BPDU TX so a neighbour's loop protection can release
its block. The web UI polls /stp.json every 2 s, so an open browser
naturally keeps the watchdog re-armed. The trip is reported via
/stp.json (fs, fsT) and as a warning on the Spanning Tree page.
Deliberately not conditioned on our own MSTP port states - the incident
above proves the uplink can be dead while every local port forwards.
Also bound the NIC DMA busy-waits (nic_tx_packet, nic_rx_header,
nic_rx_packet): an unbounded spin on SFR_NIC_CTRL freezes the entire
main loop (timers, HTTP, ARP) if the ASIC ever fails to consume a
transfer; give up after ~65k polls and drop the frame instead.
Hardware-verified end to end: with priority 15 against a live RSTP
bridge the uplink died 6 s after "stp on" and the network recovered BY
ITSELF 66 s later (trip at 45 s + neighbour release), fsT=1, LACP and
LAN intact. Telemetry via syslog-to-edge-port host confirmed the full
chain: countdown 44->4, trip, hello TX stopping at the trip.
(cherry picked from commit 1fa9775156fd6d7ebfdda2382f73430b86601230)
Two TX bugs meant our BPDUs NEVER left the switch as valid STP frames -
on the wire they appeared as ethertype 0x8899 (the raw Realtek CPU tag)
and were flooded to all ports instead of directed. Every earlier root
election was a solo act: no other bridge ever saw us. Both are the same
bug classes fixed for LACP earlier:
- rtl_tag.flags was written raw (0x0020); like every other tag field it
must go through HTONS, otherwise the bits land in the wrong byte
(0x2000 = EFID), the ASIC fails to parse the tag and floods the frame
with the 0x8899 header still attached.
- With a management VLAN set, tcpip_output() splices an 802.1Q tag after
the SA, again shifting the CPU tag out of the parsed position. BPDUs are
link-local and must egress untagged: suppress the VLAN insert per frame,
exactly as lacp_send() does.
Hardware note discovered while fixing this: RTL_TAG_KEEP on an LLC/802.3
(length-field) frame makes the ASIC drop it entirely - the same flag works
fine on ethertype frames (LACP). So BPDUs use LEARN_DIS only.
Verified on the wire (tcpdump on the peer): clean "802.3 ... LLC, dsap STP
0x42 ... Rapid STP, bridge-id 8000.<our mac>" at the hello interval, sent
directed (no flood), management HTTP unaffected, LAN at 0% loss throughout.
(cherry picked from commit 4a41a292a9ab88d4fb05a8481ad28f8ffcfd9bc4)
"stp port 7 edge" (no value) passed the cmd_words_len < 4 check and then
cmd_compare(4, ...) read a stale word left over from the PREVIOUS command
line - cmd_words_b is not cleared between commands - so the sub-command
could randomly match whatever was typed before. Require 5 words for every
per-port sub-command that carries an argument (everything except on/off).
(cherry picked from commit 1210f4f9257b14c31ad653fc7616ef403a494d28)
Implements the standard 802.1D-2004/802.1w configuration surface:
Bridge: priority (0-15 x4096), hello time, max age, forward delay,
force-version (RSTP v2 / STP-compatible v0 Config BPDUs), tx hold
count (per-port per-second BPDU budget).
Per port: enable, admin edge (forwarding immediately - no listen gap),
auto edge (forwarding after 3 s of BPDU silence; DEFAULT, so
host-facing ports no longer take the full forward delay),
path cost (0=auto/20000), port priority, BPDU guard (port disabled
on BPDU receipt), root guard (never accept a better root on the
port), BPDU filter (no BPDUs in or out).
Engine additions: root max-age expiry (reclaim the tree when the root goes
silent), root path cost accounting (rx cost + root-port cost, advertised in
our BPDUs), loop detection (our own BPDU coming back blocks the port for a
listen period), topology-change counter, approximated per-port roles
(Root/Designated/Alternate) for diagnostics.
CLI: "stp prio|hello|maxage|fwd|txhold|version ..." and
"stp port <n> on|off|edge|cost|prio|guard|filter ..." (stp_parse, delegated
from cmd_parser); all forms accepted by the startup-config validator so the
whole configuration persists. /stp.json now reports config + status; the
Spanning Tree page exposes everything with immediate-apply controls and live
state/role columns (edit-in-flight guard against the 2 s refresh).
8051 memory: the module moves to code BANK2; internal-RAM pressure from
cross-bank calls resolved by xdata loop iterators/scratch, __reentrant on
the small helpers, and moving httpd's header-pointer globals to xdata.
Verified on hardware (SWTGW218AS): defaults land per standard; priority and
hello change live; admin-edge ports (the LACP bond uplinks) keep the LAN at
0% loss THROUGH "stp on"; auto-edge ports forward after 3 s; a port that
heard real BPDUs (a VM bridge behind physical port 6) correctly declined
auto-edge, sat out the full listen period and became Designated; tc counts
promotions; we win the root election at priority 16384 vs 32768.
(cherry picked from commit 09a34dc6acdc81ab9cab0727d2f4a59c68131a3e)
Add a Spanning Tree page: an on/off toggle driving the existing "stp"
command over /cmd, and a live status section fed by a new /stp.json
endpoint - the elected root bridge (priority + MAC), our path cost,
whether we are the root, and the per-port STP state read live from the
ASIC's MSTP register (same 2-bit encoding stp_setup() writes). Ports are
reported by their physical numbers.
Recovered-from: 3132319, 9365c86
The "do not block the CPU port" mask 0x0f covers bits 3:0 of MSTP_STATES
byte 1, which is ports 8 AND 9 - so stp_setup unconditionally forced
port 8 (a real front port, the SFP uplink on SWTGW218AS) into forwarding
and it could never be blocked. The CPU port alone is bits 3:2 = 0x0c.
"stp on" put every port into blocking (stp_setup, port_timers = "10 s") but
nothing ever counted those timers down: stp_timers() only sent hello BPDUs.
On a network with no other (R)STP bridge - i.e. nobody sends us BPDUs - every
port therefore stayed blocking FOREVER and enabling STP took the whole
network down until "stp off".
- stp_timers(): count port_timers down; when a port's listen period expires
with no better root heard, promote it to forwarding in MSTP_STATES (we are
the designated bridge on that port).
- Calibrate the tick constants to the real stp_timers() rate (~64 Hz: main
loop ~256 Hz / (STP_TICK_DIVIDER+1)): TIME_HELLO 0x200->0x80 is an actual
2 s hello, port_timers 0xa00->0x280 an actual 10 s listen period. Measured
before the fix, ports converged only after ~40 s.
- Move struct bridge into rtl837x_stp.h and export root_bridge/-_cost for
the web UI status endpoint.
Verified on hardware: "stp on" -> ports report Blocking, after the 10 s
listen period all ports promote to Forwarding and LAN connectivity returns;
"stp off" restores forwarding immediately. We elect ourselves root (weRoot)
with no other bridge present.
(cherry picked from commit 8537a15ca254b2122272b20bec7a66426e86df4b)
The always-mapped common area is nearly full (349 bytes free before this
change), and the STP state machine that follows does not fit there. Move
the module to BANK2 next to the other protocol code; its public entry
points are already __banked, and cmpMAC/stp_cnf_send have no callers
outside the file.
The rtl_tag `flags` word (LEARN_DIS, KEEP) and the `pmask` ALLOW-bit
semantics are properties of the RTL8_4 CPU tag, not of any one protocol:
STP injects BPDUs with LEARN_DIS set and LACP emits slow-protocol frames
the same way. Define them once in the shared header, with the HTONS
byte-order caveat documented, so every feature that hand-builds a CPU
tag frame uses the same constants.
(cherry picked from commit 7f905b3e90f9bc5df586a5138723d97edf3d6aaf)
With STP enabled the switch is a participating bridge, so BPDUs must be
consumed, not relayed - yet the reserved group 01:80:C2:00:00:00 was
flooded across the VLAN just like any multicast, leaking every BPDU to
all ports (the same defect class as the LACPDU flood addressed in the
LACP branch, PR #299).
On stp on, write a CPU-only static L2 multicast entry for the BPDU group
per VLAN: BPDUs can arrive VLAN-tagged and classify into the tag's VID,
so cover every VLAN present in the VLAN table plus every port's PVID for
the untagged case.
On stp off the same entries are retargeted to all ports + CPU, restoring
the previous flood behaviour: an unmanaged switch is expected to be
transparent to BPDUs so the surrounding spanning tree can span through
it, and dropping them instead would partition that topology.
Note: with STP enabled the ports start out blocking, which also stops
egress of CPU-originated LACPDUs, so an active LACP aggregate drops
until the ports reach forwarding - a pre-existing interaction, not
changed here.
port_l2mc_set() writes a static L2 multicast entry for a reserved group
01:80:C2:00:00:<last> in a given VLAN with a given member portmask.
Slow-protocol frames must reach the management CPU without being flooded
to other ports, but the RMA "trap" action cannot deliver to the internal
NIC on this hardware - its destination is an external CPU attached to a
physical port. The working alternative is to keep the RMA action at
"forward" and constrain the egress with a static entry: the forward
lookup then hits the entry's member mask instead of the VLAN flood mask.
Hardware-verified on a SWTGW218AS in both directions: a mask without the
CPU bit stops delivery to the CPU, a CPU-only mask delivers with no port
egress. Lookups are IVL, so callers add one entry per VID they care
about; rewriting the same MAC+VID replaces the entry in place.
Used by the BPDU containment in the next commit; the pending LACP branch
adopts it for 01:80:C2:00:00:02 the same way.
The single-digit arm of the ingress parser guards with p - '1' > 9,
which no digit can satisfy: the largest, '9', gives 8. The digit that
needed rejecting is '0', which gives -1 and indexes one byte before
phys_to_log_port, so "ingress 0 t" reads out of bounds and applies the
ingress mode to whatever port number that byte happens to contain.
Ports are 1-based, so reject anything below '1'; values above '9' are
already excluded by the isnumber check before this.
The MAC table listing tests bit 0 of byte 2 of the third table data
word for the static flag, but the flag lives in bit 0 of byte 1: an
entry written with byte 1 bit 0 set survives the aging engine
indefinitely where an identical entry without it ages out, and reads
back with exactly that bit set through both the address and the
next-entry read methods. Byte 2 of that word reads zero for learned and
static entries alike, so every entry has always been listed as learned
and a static entry has never been visible as such in the table listing.
Adds a Connected devices column to the Port Configuration table, fed by
the shared walkL2() helper. A port with a single MAC behind it shows the
MAC directly; several are shown as a count with the full list in the
cell tooltip, since picking one of many to display would suggest it was
"the" device. The walk restarts 15 seconds after each completion and
starts 3 seconds after page load to stay clear of the initial status and
MTU requests. A walk that ends without ok keeps the previous cells,
since an incomplete table would blank ports that have devices behind
them. Translations for the three languages included.
Resolve the conflicts against the 41 upstream commits since 52692d0:
- Makefile: keep the upstream parallel-build fixes (grouped html_data
target with order-only deps, create_build_dir, crtbank, deterministic
BUILD_DATE, atomic version.h write) and keep the minify + gzip
pipeline (-z, html_min, .css/.ico in the html file list) on top.
- html: l2.js stays removed; port the upstream L2 fixes into the
single-page main.js (walkL2() with empty-reply and 4096-entry cap
handling, 3 retries on error). The L2 poll now re-schedules itself
after each walk and stops when the section is hidden.
- httpd/httpd.c merged cleanly (gzip Content-Encoding alongside the
upstream GET walk bound and counters.json port check).
- doc/webui-compression.md: describe the upstream version.h write.
Verified: KP_9000_6XHML_X2 and PCB_K0402WS_V3 build sequentially and
with make -j8; all 8 embedded files decompress byte-identically;
httpd_sim serves main.js/index.html and the L2 JSON endpoint.
VLAN membership, PVID, egress tagging, isolation and MTU are all per
physical port in this ASIC, so nothing stopped a member of a working
aggregation group being given a different PVID from its peers. The group
then forwards asymmetrically depending on which member the hash picks,
and no part of the firmware says a word about it.
port_lag_of() answers which group a port belongs to, reading the
membership through the shared reader rather than a fourth private copy.
port_pvid_set() expands to the whole group when the port it is given is
a member, and ports outside a group keep the path they had.
This is the second and third of the three steps set out in #347. I said
there that the lookup would come when something needed it, which had it
the wrong way round: nothing in the tree asks which group a port is in,
so the lookup only earns its place alongside a caller. PVID is the
smallest such caller, and the rest of the per port settings can follow
the same shape once this one is agreed.
Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
sfp_read_reg() waited for the transfer to finish and then read the output
register whatever the outcome, so an address nothing acknowledged came back
as an ordinary byte and no caller could tell it apart from data. The vendor
SDK looks at bit 1 of the control register for exactly this, and we did
not.
A failure now sets sfp_i2c_fail and the read returns 0xff, which is already
the value sfp_apply_quirks() reads as either a failed transfer or a voltage
the spec does not allow, so that test starts being true when it should be.
The insertion path and the sfp command clear the flag first and say so
afterwards, rather than presenting the bytes as though they came from the
module.
What this deliberately does not do is act on the failure. Skipping
sds_config() when the rate read failed is the obvious next step, but a
module that raises the bit spuriously would then never be configured at
all, which is worse than what happens today, and I have no way to judge how
often the bit is right. That decision belongs with someone holding the
board.
It also leaves the other half of the rewrite alone, reading and writing up
to sixteen bytes per transaction. doc/sfp.md describes only the single byte
path and does not name a length field, and guessing at a register I cannot
test is how the last attempt at this function went wrong.
40 bytes of the common segment, 51 of BANK2 and 1 of xdata, nothing in
BANK1 or internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc
4.5.0. Not tested on hardware: shorting the clock line, as in #342, should
now print the failure line instead of a plausible looking byte.
The SWTGW218AS label covers more than one PCB. On boards with the
SWTG018AS-V2.1.0 silkscreen the SFP module-detect is GPIO38 (no LOS pin
wired) and the LED block is wired differently, including a bi-color SFP
LED (green up to 2.5G, blue at 10G) that the LED-set encoding cannot
express. With the existing MACHINE_SWTGW218AS definition these boards
never detect an SFP module, so an SFP-uplinked switch comes up with no
working uplink.
Add the variant as its own machine define named by the PCB marking,
leaving MACHINE_SWTGW218AS unchanged for boards that match its wiring.
The LED register values are taken from the stock firmware; the PIN_MUX_0
write routes the blue-LED pin to the LED controller, without which no
LED register value can drive it. PIN_MUX_1/2 stay untouched so SFP
detect and i2c remain GPIOs.
index.html now contains all pages as sections (overview, ports, stat,
vlan, l2, mirror, lag, eee, bandwidth, system, update); the sidebar
switches between them via the URL hash (#/ports, #/vlan, ...).
login.html stays separate as the authentication gate.
A section's initialisation runs when the section is shown, its polling
intervals run only while it is visible, and re-entering a section
refetches the data; the checkbox builders (lag/vlan/mirror) and the L2
scanner were made idempotent so re-initialising cannot duplicate rows.
tools/minify.py: recognise regex literals - an escaped slash before the
closing delimiter was previously stripped as a // comment.
Measured (make MACHINE=KP_9000_6XHML_X2): 8 embedded files, 109,814 ->
97,730 -> 25,302 bytes gzip (23.0 %); the data block ends at 0x462d6
(~86 KB freed). On a real PCB-K0402WS-V3: cold load ~450 ms, section
switches ~60-160 ms with no asset re-downloads, all sections render
without JavaScript errors.
All 17 page scripts (i18n, shared helpers, navigation and the per-page
scripts) are merged into one html/main.js that every page loads as its
only script, so the browser fetches it once and gzip can compress
across all of it.
The page-specific initialisation is guarded by the presence of the
matching page elements (it used to be selected implicitly by which
script a page loaded); the sidebar fill and the stat popup wiring moved
into DOMContentLoaded handlers because the bundle loads in <head>,
before the body exists.
tools/httpd_sim.h: BUFFER_SIZE 24000 -> 131072 so the simulator can
serve the larger bundle (the firmware is unaffected, it serves the
gzip'd bundle of ~17 KB).
Measured (make MACHINE=KP_9000_6XHML_X2): the JS drops from 23,302 to
16,948 bytes gzip; the whole Web UI from 34,784 to 28,200 bytes (25.3 %
of the raw 111,280); the data block ends at 0x46e28. Verified with a
headless browser against httpd_sim and against a real PCB-K0402WS-V3:
all pages render, the sidebar and the port strip are drawn, no
JavaScript errors.
Two build-time steps shrink the embedded Web UI (html/) from 113,015 to
34,784 bytes (30.8 %):
- tools/minify.py (new): string-literal-safe minifier that strips
comments and indentation; comments are pure overhead on the wire, so
the transfer size drops already before gzip (113,015 -> 101,498).
Binary files (.ico) are copied through unchanged. The output goes to
output/html_min/, the new fileadder input.
- fileadder -z: gzip-compresses every added file (zlib gzip format,
Z_BEST_COMPRESSION); the generated f_data entries carry a gzip flag.
tools/Makefile links the tool against -lz.
- httpd: adds "Content-Encoding: gzip" for flagged entries; the 8051
only copies compressed bytes, decompression happens in the browser.
The JSON API stays uncompressed.
Fixes required to make the sizes deterministic:
- fileadder terminated files at data_read + 1, so the strlen()-based
size computation depended on uninitialised buffer content; the NUL
now lands directly after the content.
- version.h was .PHONY, so every build rewrote it while parallel builds
could read a half-written copy.
- the file list was unscoped and missed .css/.ico; it is now scoped to
html/ and covers all embedded types.
Measured with make MACHINE=KP_9000_6XHML_X2: data block end moves from
0x5b977 to 0x487e0 (~78 KB freed on a 512 KB image). Verified with
MACHINE=PCB_K0402WS_V3 and on real hardware (PCB-K0402WS-V3).
- All 4 2.5GBASE-T RJ45 ports work at 10/100/1000/2500 Mbps
- The SFP+ port supports 1G, 2.5G and 10G modules
- LEDs work with the same indiciations as the OEM firmware (use KP_9000_6XHML_X2 in machine.h if building yourself or the corresponding pre-compiled binary)
- LEDs work with the same indiciations as the OEM firmware (use KP_9000_6XHML_X2_V1_1 in machine.h if building yourself or the corresponding pre-compiled binary)
- untested due to missing Hardware: SFP+ ports equipped with 1G or 2.5G SFPs.
The `POE-2G080110GS` is an unmanaged 8x2.5G RJ45 and 1xSFP+ PoE switch sold by Mokerlink. There is also a `POE-2G080110GSM` managed version of the switch which may be similar but this has not been verified on actual hardware.
> This device operates on mains voltage, which can be a dangerous or potentially lethal hazard. Even after the device is unplugged, high-voltage capacitors can remain charged afterward and may still shock you. Do not attempt to open/disassemble a mains powered device unless you understand how to do so safely. Also, opening the device may void your warranty.
## Notes
The stock firmware blinks both the 2.5G and 1G LEDs on activity, despite the front of the device indicating that each LED should be independent. The `POE_2G080110GS` machine config behaves as the front of the device indicates (and what seems more logical) -- each port LED turns on and blinks independently.
The PCB has five unpopulated through-holes near the SoC, with a white rectangle surrouding them, labeled as `J18` and a triangle points to the square shaped first pin.
This is where the "expected" UART header should be soldered at, with redundant 3V3 VCC, but also with missing 0Ω resistors between TX/RX and SoC, so simply soldering a header would not work. One need also add the missing resistors or solder the pads together, while making sure not connecting unrelated pads.
Numbered from the triangle, the header pinout is:
| Position | Signal | GPIO | Status |
|----------|--------|--------|-------------|
| 1 | GND | GND | Internal |
| 2 | TX | GPIO31 | Unconnected |
| 3 | RX | GPIO32 | Unconnected |
| 4 | 3V3 | - | Internal |
| 5 | 3V3 | - | Internal |
There're four resistor pads near the header.
| Resistor ID | SoC Side | Header Side |
|-------------|-------------|-------------|
| R1240 | TX / GPIO31 | Pin 2 |
| R1243 | 3V3 | - |
| R1242 | 3V3 | Pin 3 |
| R1241 | RX / GPIO32 | - |
To get TX working, solder a 0Ω resistor between the pads for R1240 or solder them together.
For RX However, the designer certainly made a mistake, as the SoC-side lines to the resistor expected for Rx (R1242) and 3V3 (R1241) are swapped. For RX to work, the R1241 SoC side and R1242 header side shall be connected, so either:
- Solder: R1241 SoC side -> R1241 header side -> R1242 header side
- Jump wire: R1241 SoC side -> R1242 header side
Be sure not to bring R1242 SoC side to the connection as that would wire Rx to 3v3.
A complete working serial header should look like following on this PCB:
The 8 copper ports support PoE powered by a DC 52V 2.5A input. Power management is handled by two HS104PTI chipsets over I2C (GPIO47_I2C_SDA0, GPIO46_I2C_SCL0):
* Chip 1 (I2C address 0x1A): 2.5G Ports 1–4
* Chip 2 (I2C address 0x2A): 2.5G Ports 5–8
Note: The current implementation does not yet support PoE management. Consequently, PoE currently operates in standalone mode without software control.
**Note:** These devices are **RTL8372N-based** with **LAN ports mounted upside-down on the PCB**, requiring a custom configuration. The `MACHINE_ONT_S207CW_62TS_SE` definition uses a **custom configuration** with:
*Photos courtesy of [andreas5232](https://github.com/andreas5232) from [emesix/ONT-S207CW-62TS-SE#1](https://github.com/emesix/ONT-S207CW-62TS-SE/issues/1)*
## Hardware Specification
| Feature | Value |
|---|---|
| **CPU** | RTL8372N |
| **Flash** | GD25Q128E (16MB) |
| **RAM** | Integrated in RTL8372N |
| **RJ45 Ports** | 4x 2.5GBase-T (Ports 1-4) |
| **SFP+ Ports** | 2x 10G (Ports 5-6) |
| **Console** | UART (115200 baud for RTLPlayground, 9600 baud for stock) |
- **LED Wiring:** RJ45 Ports 1-2 and SFP Port 6 have reversed LED pins (orange/green swapped)
- **LED Sets:** Set 0 for RJ45 Ports 3-4, Set 1 unused, Set 2 for RJ45 Ports 1-2, Set 3 for SFP Ports 5-6
## Notes
- ONT-S207CW-62TS-SE, Binardat 2G06-04210GSM (managed), and Binardat 2G06-04210GS (unmanaged) are **RTL8372N-based** with **LAN ports mounted upside-down on the PCB**
- Web UI shows correct physical port order (1-2-3-4 for RJ45, 5-6 for SFP)
- Loader Mode always remains accessible at `192.168.10.247` for recovery
- Serial console: 9600 baud (stock) → 115200 baud (RTLPlayground)
## References
- [GitHub Issue: emesix/ONT-S207CW-62TS-SE#1](https://github.com/emesix/ONT-S207CW-62TS-SE/issues/1) - Original photos and discussion
- [PR: logicog/RTLPlayground#465](https://github.com/logicog/RTLPlayground/pull/465) - Feedback and improvements
@@ -36,7 +48,7 @@ Port 5 RJ45 is interfaced through a RTL8221B IC.
| RJ45 | 3, 4, 5, 6, 7 | 1-5 |
| SFP | 8 | 6 |
## J4
### J4
* Location: SFP connector `J4`.
* Connected to: 10GMAC number 8, SerDes 1.
@@ -48,10 +60,11 @@ Port 5 RJ45 is interfaced through a RTL8221B IC.
|5| MODDEF1 – SCL | GPIO40 | I2C SCL |
|6| MODDEF0 – PRESENT | GPIO30 | Detect |
|8| LOS | GPIO37 | RX Loss of Signal |
### Notes
#### Notes
* Not all signals were mapped mechanically, hence they've been left out of documentation.
## T3, Slave Interface
### T3, Slave Interface
This connector goes to U4 `I2C EEPROM` and U10 `SPI FLASH` (mappings identical to SWTG024AS).
@@ -65,11 +78,12 @@ For detailed Slave Interface functionality and protocol information, see [T3 doc
|4| VCC |
|5| 33R -> U10-P2 | SPI-DO/D1 |
|6| U10-P1 | SPI-CS |
### Notes
#### Notes
* 1 pin is square shaped.
## T5, Serial Console
### T5, Serial Console
|`T5` pin|GPIO|Signal|
|---|---|---|
@@ -77,10 +91,11 @@ For detailed Slave Interface functionality and protocol information, see [T3 doc
| 2 | GND | |
| 3 | GPIO32 | U0RXD (Input) |
| 4 | 3V3 | |
### Notes
#### Notes
* 1 pin is square shaped.
## T8
### T8
|`T8` pin|GPIO|Signal|
|---|---|---|
@@ -91,18 +106,19 @@ For detailed Slave Interface functionality and protocol information, see [T3 doc
| 5 | GPIO47 | |
| 6 | GPIO49 | |
### Notes
#### Notes
* 1 pin is square shaped.
* Mapping unverified but assumed the same as [LIANGUO SWTG024AS](SWTG024AS.md#t8).
# Reset Circuit
## Reset Circuit
| Function | GPIO |
|---|---|
| Reset button | GPIO54 |
### Notes
* Circuit is active-low
# GPIO
## GPIO
Note: T3/U4/U10-related signal annotations below are copied from [LIANGUO SWTG024AS T3 section](SWTG024AS.md#t3-slave-interface) as well as T8 port from [LIANGUO SWTG024AS T8 section](SWTG024AS.md#t8). They should be treated as assumed identical for ZX-SWTGW215AS as it has not been 100% confirmed true at the moment.
@@ -141,7 +157,7 @@ Note: T3/U4/U10-related signal annotations below are copied from [LIANGUO SWTG02
@@ -162,3 +178,6 @@ Note: T3/U4/U10-related signal annotations below are copied from [LIANGUO SWTG02
While [SWTG024AS.md](SWTG024AS.md) can be used as a general reference for hardware concepts and interface specifications, this device should not be assumed to be identical beside the difference implicitely highlighted below. Not all information has been validated for compatibility with the SWTG215AS. Consult the SWTG024AS documentation with caution and verify any critical details against this device's.
## Flashing
For devices with Winbond 25Q16JVSIQ, flashing is possible for example using flashprog with device "W25Q16.V". Note that you should not overwrite the whole flash to keep device-specific data (like the MAC address at 0x1fc000). Also, creating a backup of the entire falsh before is of course a very good idea.
all translations are stored in a single JavaScript dictionary embedded in the firmware.
No server-side changes are needed.
The firmware uses a client-side i18n approach: all translations are stored
in one JavaScript dictionary embedded in the firmware. No server-side
changes are needed.
## Architecture
All translation logic lives in`html/i18n.js`. The file contains:
All translation logic lives at the top of`html/app.js`:
- A `LANG` object with one sub-object per language (`en`, `ja`, ...)
- Language auto-detection (browser language → `localStorage` override)
-`t(key)` — look up a translated string
- `setLang(lang)` — switch language and update the page
- `applyTranslation(el)` — apply translation to one DOM element
- A `LANG` object with one sub-object per language (`en`, `ja`, `zh`)
- Language auto-detection (browser language, overridden by the
`rtl_lang` key in `localStorage`, which the language selector in the
System section writes)
- `t(key, vars)` looks up a translated string; `{name}` placeholders in
the string are replaced from `vars`, e.g. `t("v_del_q", {n: vid})`
- `i18nApply()` applies the dictionary to the static markup once at load
Translation keys are **flat strings** (no nesting). The English keys in `LANG.en` also serve as the fallback when a key is missing in another language.
Translation keys are flat strings. The English entry is the fallback when
a key is missing in another language, and the key itself is the fallback
when it is missing everywhere.
Changing the language stores the choice and reloads the page, so every
dynamically built table is rebuilt in the new language.
`login.html` is served before authentication and does not load `app.js`;
it carries its own five strings per language in an inline table.
## How to Add a New Language
### 1. Add a dictionary entry in `html/i18n.js`
### 1. Add a dictionary entry in `html/app.js`
Append a new sub-object to the `LANG` object. Every key from `LANG.en`must be present:
Append a sub-object to `LANG`. Every key from `LANG.en`should be present:
```js
var LANG = {
en:{
nav_overview: 'Overview',
nav_port_config: 'Port Configuration',
// ... all keys for English
},
ja: {
nav_overview: '概要',
nav_port_config: 'ポート設定',
// ... all keys for Japanese
},
LANGCODE: { // ← add your language here
nav_overview: '...',
nav_port_config: '...',
// ... translate every key
},
var LANG={
en:{
nav_dash:"Dashboard",
...
},
LANGCODE:{ // add your language here
nav_dash:"...",
...
}
};
```
### 2. Add the language to the navigation sidebar
### 2. Add the language to the selector in `html/index.html`
In `html/navigation.js`, add an `<option>` to the language selector:
The `linkS` array in `html/main.js` maps numeric link states to display strings. The first two entries (`speed_disabled`, `speed_down`) use `t()` for translation; the remaining entries are static literals (they are the same in all languages):
function linkText(idx) { var v = linkS[idx]; return typeof v === 'function' ? v() : v; }
```
Always use `linkText(idx)` (not `linkS[idx]`) to read these values.
Strings that are identical in every language (`10M`, `2.5G`, `MAC`,
`VLAN`, `CPU`, `RSTP`) are literals, not dictionary entries.
## Size Considerations
- `html/i18n.js` is embedded in the firmware filesystem (~14 KB for two languages)
- Each new language adds roughly the same number of bytes as the English dictionary (~3–4 KB)
- The firmware binary is padded to 512 KiB, so a few extra KB do not change the flash footprint
- Values that are identical in all languages should be inlined as literals rather than added to the dictionary (e.g., `"10M"`, `"2.5G"`, `"MAC"`, `"VLAN"`, `"CPU"`)
- Each language adds roughly 3 KB to `app.js` before compression; the
build gzips the file, so the flash cost of a language is closer to 1 KB
- The largest embedded file must stay below 64 KB after compression
(`uint16_t` length in the file table); `app.js` with three languages
is about 29 KB compressed
## Build
No special flags are needed. The`html/`directory is embedded by `fileadder` during the build.
## Script Load Order
`i18n.js` must be loaded after `main.js` (which defines `t()`'s dependencies like `LANG`) but before any page-specific JS that calls `t()`:
```html
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<script src="/eee.js"></script> <!-- uses t() -->
```
The `navigation.js` script is loaded last (bottom of `<body>`).
No special flags are needed. `html/`is minified and embedded by the
For KP-9000-6XH-X2 / KP-9000-6XHML-X2 / Mokerlink 2G040210GSM devices, select
the machine target by PCB revision. The ML/non-ML or managed/unmanaged label
alone does not identify the wiring.
Other device based on RTL8272/3 that may work are described here: [Up-N-Atoms 2.5 GBit RTL Switch hacking guide](https://github.com/up-n-atom/SWTG118AS)
port_mtu_heading:'Configure Maximum Frame Size (MTU) forwarded at Port',
port_auto:'Auto',
port_2500m:'2500MBit/Full',
port_1000m:'1000MBit/Full',
port_100m_f:'100MBit/Full',
port_100m_h:'100MBit/Half',
port_10m_f:'10MBit/Full',
port_10m_h:'10MBit/Half',
port_apply:'Apply',
stat_title:'FreeSwitchOS Port Statistics',
stat_heading:'Port Statistics',
stat_detailed:'Detailed Port Statistics',
stat_close:'Close',
stat_col_port:'Port',
stat_col_name:'Name',
stat_col_link:'link',
stat_col_tx_good:'TX Good',
stat_col_tx_bad:'TX Bad',
stat_col_rx_good:'RX Good',
stat_col_rx_bad:'RX Bad',
stat_col_all:'All Counters',
stat_counter:'Counter',
stat_value:'Value',
stat_show:'Show',
vlan_title:'FreeSwitchOS VLAN Configuration',
vlan_heading:'VLAN Configuration',
vlan_select:'VLAN Select:',
vlan_choose:'— VLAN Choose —',
vlan_id:'VLAN ID:',
vlan_get_config:'Get Configuration',
vlan_name:'VLAN Name:',
vlan_tagged:'Tagged Ports',
vlan_untagged:'Untagged Ports',
vlan_select_all:'Select all',
vlan_pvid:'Use as default VLAN for incoming traffic (PVID)',
vlan_update:'Update / Create',
vlan_configured:'Configured VLANs',
vlan_col_name:'Name',
vlan_col_member:'Member Ports',
vlan_col_tagged:'Tagged Ports',
vlan_col_untagged:'Untagged Ports',
vlan_col_pvid:'PVID Ports',
vlan_col_delete:'Delete',
vlan_set_id_first:'Set VLAN ID first',
vlan_delete_confirm:'Delete VLAN ',
lag_title:'Link Aggregation Configuration',
lag_heading:'Link Aggregation Groups Configuration',
lag_update:'Update / Create',
mirror_title:'Mirror Configuration',
mirror_heading:'Mirror Configuration',
mirror_enabled:'Enabled:',
mirror_port:'Mirroring Port:',
mirror_tx:'Mirrored Ports (TX)',
mirror_rx:'Mirrored Ports (RX)',
mirror_update:'Update / Create',
mirror_disable:'Disable Mirroring',
mirror_set_port_first:'Set Mirroring Port first',
mirror_select_ports:'Select Mirrored Ports',
eee_title:'EEE Configuration',
eee_heading:'EEE Status',
eee_advertising:'Advertising',
eee_partner:'Link-Partner advertises',
eee_port:'Port',
eee_active:'Active?',
eee_enable:'Enable EEE',
eee_disable:'Disable EEE',
eee_on:'ON',
eee_off:'OFF',
l2_title:'FreeSwitchOS L2 Configuration',
l2_heading:'L2 Configuration',
l2_col_port:'Port',
l2_col_type:'Type',
l2_col_remove:'Remove Entry',
l2_shown:'Shown:',
l2_delete:'Delete',
l2_static:'static',
l2_learned:'learned',
bw_title:'Ingress and Egress Bandwidth',
bw_heading:'Ingress and Egress Bandwidth',
bw_ingress:'Ingress',
bw_egress:'Egress',
bw_col_port:'Port',
bw_col_limit:'Limit',
bw_col_bandwidth:'Bandwidth [kBit/s]',
bw_col_flow:'Flow Control',
bw_col_apply:'Apply',
bw_unlimited:'UNLIMITED',
sys_title:'System Settings',
sys_tab_system:'System',
sys_tab_advanced:'Advanced',
sys_tab_console:'Console',
sys_heading:'System Settings',
sys_ip:'IP address:',
sys_model:'Model:',
sys_hostname:'Hostname:',
sys_apply:'Apply',
sys_netmask:'Netmask:',
sys_gateway:'Gateway:',
sys_language:'Language:',
sys_mgmt_vlan:'Management VLAN:',
sys_mgmt_untagged:'untagged',
sys_mgmt_confirm:'Move switch management to VLAN ',
sys_mgmt_warn:'The switch will start tagging its own traffic with that VLAN. If the port you are connected through does not carry it, this page becomes unreachable and the setting can only be undone over the console. Continue?',
sys_ip_note:'When updating the above settings, remember to point your browser to the new IP afterwards:',
sys_update:'Update Settings',
sys_save_label:'Save all current settings to Flash:',
sys_save:'Save Settings to Flash',
sys_advanced:'Advanced Settings',
sys_startup_config:'Startup configuration:',
sys_startup_warn:'Be careful when saving the directly edited startup configuration, you can lock yourself out:',
sys_clear_config:'Clear Startup Config',
sys_save_startup:'Save Startup Settings to Flash',
sys_reset:'Reset Switch',
sys_console:'Console Command',
sys_enter_cmd:'Enter command:',
sys_send_cmd:'Send Command',
sys_console_warn:'Be careful when entering console commands, you can lock yourself out!',
sys_invalid_ip:'Invalid ip:',
sys_reset_confirm:'Are you sure you want to reset the switch?',
sys_resetting:'Switch is resetting. Please wait and refresh the page.',
login_title:'RTL Switch Login',
login_heading:'RTL Switch Login',
login_wrong:'Wrong password!',
login_password:'Password',
login_login:'Login',
index_title:'FreeSwitchOS Main Page',
index_heading:'Switch Configuration',
index_settings:'Settings',
update_title:'Firmware update',
update_heading:'Firmware Update',
update_instruction:'Choose a firmware update file to upload:',
update_upload:'Upload File',
common_port:'Port ',
common_pkts:' pkts',
},
ja:{
nav_overview:'概要',
nav_port_config:'ポート設定',
nav_port_stat:'ポート統計',
nav_l2:'L2 設定',
nav_mirror:'ミラーリング',
nav_lag:'リンクアグリゲーション',
nav_eee:'EEE',
nav_bandwidth:'帯域制限',
nav_system:'システム設定',
nav_fw_update:'ファームウェア更新',
port_name:'名前',
port_status:'状態',
port_not_enabled:'無効',
port_link_speed:'リンク速度',
port_vendor:'ベンダー',
port_model:'モデル',
port_serial:'シリアル',
port_temp:'温度',
port_vcc:'電圧',
port_tx_fault:'TX 障害',
port_tx_disabled:'TX 無効',
port_tx_bias:'TX バイアス',
port_tx_power:'TX 電力',
port_rx_power:'RX 電力',
port_rx_los:'RX 信号ロス',
speed_disabled:'無効',
speed_down:'リンクダウン',
port_title:'FreeSwitchOS ポート設定',
port_heading:'ポート設定',
port_col_port:'ポート',
port_col_name:'名前',
port_col_speed:'現在のリンク速度',
port_col_set_speed:'速度設定',
port_col_disabled:'無効',
port_col_apply:'適用',
port_mtu_heading:'ポートの最大フレームサイズ (MTU) 設定',
port_auto:'自動',
port_2500m:'2500Mbps/全二重',
port_1000m:'1000Mbps/全二重',
port_100m_f:'100Mbps/全二重',
port_100m_h:'100Mbps/半二重',
port_10m_f:'10Mbps/全二重',
port_10m_h:'10Mbps/半二重',
port_apply:'適用',
stat_title:'FreeSwitchOS ポート統計',
stat_heading:'ポート統計',
stat_detailed:'詳細ポート統計',
stat_close:'閉じる',
stat_col_port:'ポート',
stat_col_name:'名前',
stat_col_link:'リンク',
stat_col_tx_good:'TX 正常',
stat_col_tx_bad:'TX 異常',
stat_col_rx_good:'RX 正常',
stat_col_rx_bad:'RX 異常',
stat_col_all:'全カウンタ',
stat_counter:'カウンタ',
stat_value:'値',
stat_show:'表示',
vlan_title:'FreeSwitchOS VLAN 設定',
vlan_heading:'VLAN 設定',
vlan_select:'VLAN 選択:',
vlan_choose:'— VLAN 選択 —',
vlan_id:'VLAN ID:',
vlan_get_config:'設定取得',
vlan_name:'VLAN 名:',
vlan_tagged:'タグ付きポート',
vlan_untagged:'タグ無しポート',
vlan_select_all:'すべて選択',
vlan_pvid:'受信トラフィックのデフォルト VLAN (PVID)',
vlan_update:'更新 / 作成',
vlan_configured:'設定済み VLAN',
vlan_col_name:'名前',
vlan_col_member:'メンバーポート',
vlan_col_tagged:'タグ付きポート',
vlan_col_untagged:'タグ無しポート',
vlan_col_pvid:'PVID ポート',
vlan_col_delete:'削除',
vlan_set_id_first:'VLAN ID を先に設定してください',
vlan_delete_confirm:'VLAN 削除 ',
lag_title:'リンクアグリゲーション設定',
lag_heading:'リンクアグリゲーショングループ設定',
lag_update:'更新 / 作成',
mirror_title:'ミラーリング設定',
mirror_heading:'ミラーリング設定',
mirror_enabled:'有効:',
mirror_port:'ミラーポート:',
mirror_tx:'ミラー元ポート (TX)',
mirror_rx:'ミラー元ポート (RX)',
mirror_update:'更新 / 作成',
mirror_disable:'ミラーリング無効化',
mirror_set_port_first:'ミラーポートを先に設定してください',
mirror_select_ports:'ミラー元ポートを選択してください',
eee_title:'EEE 設定',
eee_heading:'EEE 状態',
eee_advertising:'EEE アドバタイジング',
eee_partner:'リンクパートナー広告',
eee_port:'ポート',
eee_active:'有効?',
eee_enable:'EEE 有効化',
eee_disable:'EEE 無効化',
eee_on:'オン',
eee_off:'オフ',
l2_title:'FreeSwitchOS L2 設定',
l2_heading:'L2 設定',
l2_col_port:'ポート',
l2_col_type:'タイプ',
l2_col_remove:'エントリ削除',
l2_shown:'Shown:',
l2_delete:'削除',
l2_static:'静的',
l2_learned:'学習',
bw_title:'入力/出力帯域制限',
bw_heading:'入力/出力帯域制限',
bw_ingress:'入力',
bw_egress:'出力',
bw_col_port:'ポート',
bw_col_limit:'制限',
bw_col_bandwidth:'帯域 [kbps]',
bw_col_flow:'フロー制御',
bw_col_apply:'適用',
bw_unlimited:'制限無し',
sys_title:'システム設定',
sys_tab_system:'システム',
sys_tab_advanced:'詳細設定',
sys_tab_console:'コンソール',
sys_heading:'システム設定',
sys_ip:'IP アドレス:',
sys_model:'モデル:',
sys_hostname:'ホスト名:',
sys_apply:'適用',
sys_netmask:'ネットマスク:',
sys_gateway:'ゲートウェイ:',
sys_language:'言語:',
sys_mgmt_vlan:'Management VLAN:',
sys_mgmt_untagged:'untagged',
sys_mgmt_confirm:'Move switch management to VLAN ',
sys_mgmt_warn:'The switch will start tagging its own traffic with that VLAN. If the port you are connected through does not carry it, this page becomes unreachable and the setting can only be undone over the console. Continue?',
sys_ip_note:'上記設定を変更した場合は、ブラウザで新しい IP にアクセスしてください:',
sys_mgmt_confirm:'Move switch management to VLAN ',
sys_mgmt_warn:'The switch will start tagging its own traffic with that VLAN. If the port you are connected through does not carry it, this page becomes unreachable and the setting can only be undone over the console. Continue?',
<pclass="small mut"style="margin-top:8px"data-i18n="stp_prio_note">Lower priority wins the root election (default 8 = 32768). Settings apply immediately; use Save to flash to keep them.</p>
<pclass="small mut"style="margin-top:8px"data-i18n="stp_port_note">Edge ports forward immediately and cause no topology change; Auto treats a port as edge after 3 s without BPDUs, and any received BPDU revokes it. Path cost 0 = automatic.</p>
<pclass="small mut"style="margin-top:6px"data-i18n="v_legend">U = untagged member, T = tagged member, - = not a member. PVID assigns this VLAN to untagged ingress traffic.</p>
</div>
<divclass="card"><h2><spandata-i18n="v_ingress">Ingress filtering</span><spanclass="hint"data-i18n="v_ingress_h">write-only: the state is not readable from the switch</span></h2>
<pclass="small mut"style="margin-top:10px"data-i18n="lag_note">A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.</p>
<pclass="small mut"style="margin-top:8px"data-i18n="e_note">Advertised and link-partner flags per speed: 100M, 1G, 2.5G. SFP ports do not support EEE.</p>
<divclass="card"><h2><spandata-i18n="sc_title">Storm control</span><spanclass="hint"data-i18n="sc_h">per port and traffic type, empty = off</span></h2>
<pclass="small mut"style="margin-bottom:12px"data-i18n="fw_intro">Upload an RTLPlayground image (512 KiB .bin). The image is staged to flash and CRC-checked; on success the switch reboots itself, re-verifies the image and applies it. The startup configuration is preserved.</p>
Loaded 100 of 281 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.