68 Commits
Author SHA1 Message Date
bloqaudio 196d22d7c3 web: keep LLDP lines from piling up in the saved configuration
Save to flash merges the running log into the startup configuration.
Neither its toggle nor its overwrite rules knew "lldp on|off", so every
switch of LLDP added another line: four switches left four lines.
"port <n> lldp permit|block" fell under the rule for "port <n> <speed>",
so setting a port's speed dropped its LLDP setting from the saved
configuration, and the other way round.

"lldp on|off" is now a toggle, and a per-port LLDP setting replaces only
an earlier per-port LLDP setting of that port.
2026-10-16 10:43:08 -05:00
bloqaudio 680a40dab5 web: highlight Save to flash only when there are unsaved changes
The button in the header was always drawn as the primary action, on
every page, so it competed with each page's own buttons, for example
the upload button on the Firmware page. It is now a plain button and
turns primary together with the "unsaved changes" badge.
2026-10-15 00:12:14 -05:00
bloqaudio 97b15f4022 web: give each packet counter of a port its own line
The port tooltip and the port details showed "TX good / bad: 8794 / 0
pkts", which reads as 8794 bad packets. Each counter now has its own
line, labelled as on the Statistics page, and the two strings only that
line used are removed from every language.
2026-10-15 00:11:04 -05:00
bloqaudio d5ccd38dd3 web: right-align the headers of number columns
"table.t th" sets text-align:left and is more specific than ".num", so
the headers of the counter columns on the dashboard and the statistics
page stayed left-aligned above right-aligned values.
2026-10-14 23:48:46 -05:00
bloqaudio 4050555667 port: add a cable test command
"port <port> cabletest" runs the cable diagnostic of the copper PHYs
and reports one line per pair: ok, or open, short, short to another
pair or impedance mismatch with the distance to the fault.

The PHY part follows the cable_test_start and cable_test_get_status
ops of the Linux phy_driver and takes the port number:
rtl_phy_cable_test_start() triggers the test and
rtl_phy_cable_test_get_status() returns true once all four pairs are
done, with a result code and a length per pair. The decode order is
the one of the Linux driver. The same two functions drive the RTL8224
and the internal PHYs of the RTL8372 and RTL8373.

The command only powers the port down and returns. A state machine
called from handle_tick() calls the start op after one second, polls
the status op, stores the result, clears the trigger, also after a
timeout, and restores the control register, so the main loop never
waits. The result is printed when it is ready and "port <port>
cabletest show" prints it again. One test runs at a time.

The result flags are the low byte of the per-pair status word. The
vendor code reads the high byte, which is always zero on the RTL8373.

A link partner that keeps transmitting makes the PHY report an
inter-pair short on every pair, and forcing the speed with
autonegotiation off, as the Linux driver does, does not take the link
down. The port is therefore powered down for a second before the
test. The control register is restored afterwards with an
autonegotiation restart, so speed settings survive and the link
returns.

A test started on a port that is switched off never finishes and
leaves the PHY unusable until the switch is reset, so such a port is
refused, and a test is dropped if the port is switched off during the
wait. SFP ports are refused by the existing check. The external copper
PHYs of the RTL8372 boards are refused because the test has not been
run on one.

The distance is the raw length word minus 150, at 43 counts per 50 cm.
Fitted on open cables of 0.9 m, 2 m and 7.6 m; the pairs of one cable
differ by about 0.2 m. The Linux formula, raw minus 620 at 78 counts
per metre, gives 0, 0 and 2.4 m for these cables.

An unplugged port has no defined result: some pairs report open at
about 0.5 m and others a short or a mismatch at 2 to 3 m.
2026-10-14 12:42:27 -05:00
bloqaudio c2980e42d8 mirror: show the configured monitor port in "mirror status"
"mirror status" printed the local mirroring_port, which is still 0 at
that point, so it named the same port whatever was configured. Print
the port from RTL837x_MIRROR_CTRL instead, which the function has
already read to tell whether mirroring is enabled. send_mirror() decodes
the register the same way for the web interface.
2026-10-12 12:02:02 -05:00
bloqaudio d0374541c5 uip: rate-limit the dropped-fragment log
uip has no reassembly, so every fragment it receives is discarded and
logged. A neighbour that broadcasts fragmented traffic therefore produces a
continuous stream of "ip: fragment dropped." rather than an occasional one,
at roughly thirteen lines a second on a quiet segment, which buries every
other console message.

The message carries real information: a sudden burst of it says something
about the network. What carries none is repeating it per fragment. Report
the count once per five second window instead, which keeps the signal and
ends the flood.

The count is printed through the no_syslog primitives, as the original
message was, so the flood cannot move from the console into the log ring.
itoa() writes through write_char(), which also feeds the ring, so it gets
a no_syslog twin beside it rather than a private copy of its digits.
2026-09-25 08:20:27 -05:00
bloqaudio 675faf0471 uip: build the IPv6 neighbour cache only for IPv6 builds
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.
2026-09-22 09:23:13 -05:00
bloqaudio 175ca9464a uip: build the forwarding module only when configured
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.
2026-09-22 09:23:13 -05:00
bloqaudio 93003c8cb8 web: show the speed a port is set to, not always auto
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.
2026-09-18 15:13:41 -05:00
bloqaudio fd058ea74d port: name the port an eee command acted on the way the user wrote it
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.
2026-09-18 14:59:47 -05:00
bloqaudio edc469fe29 httpd: make the firmware upload length limit count
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.
2026-09-18 14:01:03 -05:00
bloqaudio a71c5359a5 httpd: refuse an unauthenticated firmware upload before it changes state
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.
2026-09-18 14:00:51 -05:00
bloqaudio 9e3863cc4e syslog: do not read the connection before there is one
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.
2026-09-18 13:59:59 -05:00
bloqaudio d22ec04b5b leds: index the second LED register from the set it belongs to
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.
2026-09-18 13:59:50 -05:00
bloqaudio 0a65536a7f web: send an eee command in the documented argument order
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.
2026-09-18 13:57:07 -05:00
bloqaudio 0631c813e2 cmd: parse the port of an eee command from the start of the word
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.
2026-09-18 13:56:53 -05:00
bloqaudio f65416f0d1 ci: run the host-side unit tests on every push
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.
2026-09-14 15:04:19 -05:00
bloqaudio ffde4eaf60 web: stop polling when the page has had no input for ten minutes
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.
2026-09-14 14:06:41 -05:00
bloqaudio c05d83132b test: drop the unused typedef keyword and make the gcc-only warning switch conditional
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.
2026-09-14 14:05:33 -05:00
bloqaudio dcd3cd5a98 test: add host-side unit-test harness, covering cmd_editor
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
2026-09-14 14:02:35 -05:00
bloqaudio 690092d564 Move DHCP out of BANK2 into BANK3
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.
2026-09-14 08:29:39 -05:00
bloqaudio 115ea09ebb web: address a link aggregation group as one spanning tree entity
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`.
2026-09-14 08:14:53 -05:00
bloqaudio f3e1a393b5 stp: accept the documented port keyword again
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.
2026-09-14 08:05:57 -05:00
bloqaudio 206c72b5ef web: floor the optical power at -40 dBm
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.
2026-09-14 08:03:36 -05:00
bloqaudio 472811cc9a doc: add the single-page UI to the changelog 2026-09-14 08:03:36 -05:00
bloqaudio 80552816b9 web: move the language and theme selectors to the System section
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.
2026-09-14 08:03:36 -05:00
bloqaudio c1caf6f569 web: stop the MAC table walk when its section is left
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.
2026-09-14 08:03:36 -05:00
bloqaudio ddad65fe90 web: keep a VLAN's row when its details cannot be read
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.
2026-09-14 08:03:36 -05:00
bloqaudio ff7b517491 web: let the merged config be edited before it is written
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.
2026-09-14 08:03:36 -05:00
bloqaudio 65417433eb web: fit the hostname in the sidebar, widen the number fields and the page
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.
2026-09-14 08:03:36 -05:00
bloqaudio b7621fad31 web: keep the port detail view live and summarise each port in a tooltip
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.
2026-09-14 08:03:36 -05:00
bloqaudio 82f78cb8f2 web: render the per-port tables in physical port order
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.
2026-09-14 08:03:36 -05:00
bloqaudio 75f2ade1a1 doc: describe the web UI files and translation table after the rewrite 2026-09-14 08:03:36 -05:00
bloqaudio 3e91b05a42 web: replace the multi-page UI with a themed single-page app
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.
2026-09-14 08:03:36 -05:00
bloqaudio ec1cb4106d httpd: count every authenticated request as session activity
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.
2026-09-14 08:03:35 -05:00
bloqaudio f0e9953081 web: hide the L2 delete button for the CPU entry and drop a deleted row
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.
2026-09-03 14:33:07 -05:00
bloqaudio 1c9bf10b87 httpd: read the table registers straight from the SFRs in l2_delete()
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
2026-09-03 14:33:07 -05:00
bloqaudio dd7f9f7c7b httpd: refuse to delete the switch's own L2 entry
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.
2026-09-03 10:33:53 -05:00
bloqaudio 3b8dba6e87 port: pin the management MAC to the CPU port with a static L2 entry
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.
2026-09-02 21:35:51 -05:00
bloqaudio f5e4fa5134 httpd: buffer a plain POST body that arrives after the headers
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.
2026-09-01 14:55:56 -05:00
bloqaudio 28020e8186 httpd: parse Content-Length in scan_header()
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.
2026-09-01 14:52:09 -05:00
bloqaudio cbcf67a693 httpd: keep scan_header() on the terminator of a truncated header
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.
2026-09-01 14:52:09 -05:00
bloqaudio d6bf46595a httpd: saturate parse_short() instead of wrapping
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.
2026-09-01 14:52:09 -05:00
bloqaudio 2499d116a3 httpd: match header field names case-insensitively
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.
2026-09-01 14:52:09 -05:00
bloqaudio 005209f97a httpd: split the upload fragment handlers out of handle_post()
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.
2026-09-01 09:59:52 -05:00
bloqaudio f0f142fbe0 httpd: track buffered upload bytes in one accumulator 2026-09-01 09:59:52 -05:00
bloqaudio daf9d8965a httpd: scope the multipart parse cursors to their functions
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.
2026-09-01 09:59:52 -05:00
bloqaudio 6be5693385 Makefile: have clean and distclean recurse into the tool directory
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.
2026-08-30 21:49:56 -05:00
bloqaudio e826180d06 httpd: pass stream_upload() state through an xdata struct 2026-08-30 04:09:30 -05:00
bloqaudio b63117ba6c httpd: scan the upload preamble with a register cursor
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.
2026-08-30 04:09:30 -05:00
bloqaudio be7796a22c httpd: send a real verdict for firmware uploads
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.
2026-08-30 04:09:30 -05:00
bloqaudio 1bd4a8d3cc httpd: buffer a firmware upload's part header before streaming
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.
2026-08-30 04:09:30 -05:00
bloqaudio 801150431b stp: host the spanning tree module in BANK2
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.
2026-08-25 10:05:19 -05:00
bloqaudio 1a743b3889 leds: reduce the SWTG018AS-V2.1.0 custom init to the effective registers
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.
2026-08-24 23:44:36 -05:00
bloqaudio 425283b774 machine: host the per-machine custom init hooks in BANK2
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.
2026-08-24 23:36:17 -05:00
bloqaudio cefe48fe51 httpd: size the config upload buffer for a full config sector
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.
2026-08-24 15:45:25 -05:00
bloqaudio e8e7fcbe7f httpd: send the config tail from the config sector, not a bare offset
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.
2026-08-24 15:45:25 -05:00
bloqaudio 772e9dc526 httpd: parse a configuration upload once the whole request has arrived
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.
2026-08-23 18:11:46 -05:00
bloqaudio a8d3b7d39a cmd_parser: reject port 0 in the ingress command
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.
2026-08-17 17:19:46 -05:00
bloqaudio a3d1a35e2f httpd: read the static flag of an L2 entry from the byte that holds it
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.
2026-08-17 12:17:42 -05:00
bloqaudio 8c3df643d0 ports: show the devices connected behind each port
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.
2026-08-17 10:49:39 -05:00
bloqaudio 0998b7381f machine: add PCB SWTG018AS-V2.1.0 variant of the SWTGW218AS
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.
2026-08-13 16:19:25 -05:00
bloqaudio 0ed45e0710 cmd_editor: fix cmd_buffer overflow and serial-ring skip on long input
Typing or pasting a line of 128 characters or more into the CLI hangs the
editor and overruns cmd_buffer[128]: the length check did not reserve room
for the terminating NUL written on Enter, and on a full buffer the character
was retried via continue without advancing the serial-ring read pointer at
the bottom of the loop, so input processing never caught up again.

Cap the line at CMD_BUF_SIZE-1 and, when full, drop the character but fall
through to consume the ring byte instead of spinning on it. The functional
change is three lines; the rest of the diff is re-indentation of the
insert-and-echo block (git diff -w shows the minimal form).
2026-08-11 10:26:06 -05:00
bloqaudio 3853a2701d machine: read a factory MAC from flash via mac_flash_offset
Adds an optional per-board mac_flash_offset. When non-zero, RTLPlayground
reads a 6-byte MAC from that flash address and uses it if it is a valid
globally-administered unicast address; otherwise it falls back to the
generated locally-administered MAC. Offset 0 (default for every board)
preserves current behaviour.

Enable it for the SWTGW218AS, whose stock firmware keeps the factory MAC
in nvcfg at 0x1FC000. RTLPlayground images and web upgrades only touch
flash below that region, so the factory MAC survives flashing.
2026-08-11 02:32:31 -05:00
bloqaudio 00a5f81871 dhcp: announce the hostname via option 12
Send the configured hostname on discover and request so the DHCP server
can register the device (e.g. in local DNS). Skipped while the hostname
is still empty, i.e. before set_hostname_default() has run.
2026-08-11 02:03:11 -05:00
bloqaudio 6b190eb9f3 makefile: derive BUILD_DATE deterministically
Honor SOURCE_DATE_EPOCH, falling back to the HEAD commit date and then
to wall-clock when git is absent. Same-commit builds become
byte-identical (BUILD_DATE is baked into the image and covered by the
trailing CRC), which makes it possible to verify that a binary matches
a source tree.
2026-08-10 15:55:08 -05:00
bloqaudio fae16dabf5 rtl837x_igmp: cast &entry to __xdata pointer in memset()
SDCC rejects memset() on the generic &entry pointer (error 78,
incompatible types). The ipmc/l2mc table entries are __xdata; pass an
explicit (__xdata uint8_t *) so the call resolves.
2026-08-10 15:54:08 -05:00