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.