338 Commits
Author SHA1 Message Date
René van Dorst 4c8e0ce975 Merge pull request #375 from bloqaudio/fix/post-body-split
httpd: buffer a plain POST body that arrives after the headers
2026-09-02 08:59:56 +02:00
René van Dorst 71344825c7 Merge pull request #382 from DrDoof/fix/sfp-empty-slot-json
sfp: stop the web pages printing the previous slot's bytes
2026-09-02 08:25:42 +02: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
René van Dorst 74a1c6831a Merge pull request #335 from bloqaudio/feat/ports-connected-devices
ports: show the devices connected behind each port
2026-09-01 18:02:22 +00:00
René van Dorst a79c483f94 Merge pull request #385 from 7Ji/fg_8gt_1sx
Add FG-8GT-1SX
2026-09-01 17:35:54 +00:00
René van Dorst 75aa23468c Merge pull request #374 from bloqaudio/upload-cleanup
httpd: clean up the upload path in handle_post()
2026-09-01 17:30:56 +00:00
logicog 89b4cf83ce Merge pull request #314 from DrDoof/feat/stp-v2
stp: Spanning Tree support for the RTL837x switches
2026-09-01 18:52:20 +02: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
René van Dorst 58b12c4aa6 Merge pull request #384 from DrDoof/fix/cmd-history-ring
cmd: keep the history pointer inside the ring
2026-09-01 09:22:12 +02:00
d00f 4ce3e33447 cmd: keep the history pointer inside the ring
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.
2026-09-01 09:02:28 +02:00
Guoxin Pu ebd27e4763 Add FG-8GT-1SX
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.
2026-09-01 15:01:04 +08:00
René van Dorst e60a09fb7e Merge pull request #383 from DrDoof/fix/isolate-off-top-port
cmd: let "isolate off" put the top port back in the member list
2026-09-01 08:23:05 +02:00
d00f 828963b586 cmd: let "isolate off" put the top port back in the member list
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.
2026-09-01 04:11:30 +02:00
d00f 944f4ce6f0 sfp: stop the web pages printing the previous slot's bytes
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.
2026-08-31 23:26:37 +02:00
d00f 3a3967960f stp: take the review notes on the port and priority arguments
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.
2026-08-31 22:55:41 +02:00
d00f e38abf27c9 Merge upstream/main into the Spanning Tree branch 2026-08-31 22:48:14 +02:00
d00f 05ded88f85 stp: keep the FDB update's counters to the function that uses them
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.
2026-08-31 22:18:28 +02:00
logicog dc22c4908b Merge pull request #376 from bloqaudio/fix/make-clean-tools
Makefile: have clean and distclean recurse into the tool directory
2026-08-31 21:58:58 +02:00
d00f c0d9bf7f0d stp: take the review notes on types and register reads
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.
2026-08-31 21:50:19 +02:00
René van Dorst 1947356ec5 Merge pull request #368 from vDorst/fix_all_compiler_warnings
Disable all `__sfr32` SDCC bug 4070 and fix all compiler warnings.
2026-08-31 19:47:06 +00:00
René van Dorst 2fa4686b33 fix review points. 2026-08-31 21:00:37 +02:00
d00f bd06f7f5e5 stp: do not let auto edge undo a loop block
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.
2026-08-31 14:26:05 +02:00
René van Dorst b6d2d7c072 Merge pull request #351 from DrDoof/feat/lag-interface
port: let a pvid name an aggregation group
2026-08-31 13:37:14 +02:00
René van Dorst 3c72f0e1b6 Merge pull request #372 from vDorst/fix_macros
Fix define macros, was missing the do {} while (0) wrapping
2026-08-31 13:07:29 +02: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
d00f 8e9bb56a29 nic: send a frame with the layout it actually has
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.
2026-08-31 02:52:00 +02:00
d00f 7f0e7fdc82 Merge upstream/main into the Spanning Tree branch
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.
2026-08-30 21:58:54 +02:00
René van Dorst f187261085 Fix all compiler warnings.
- Many times we need help the compiler to reason about variable type or
define.
2026-08-30 21:41:30 +02:00
René van Dorst aba69cd640 DHCP: Added workarounds for SDCC bug 4070 2026-08-30 21:37:37 +02:00
René van Dorst b6dcb75130 Disable all __sfr32 due to bug SDCC 4070 and add workarounds.
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.
2026-08-30 21:36:03 +02:00
René van Dorst 4e04735a5f Fix define macros, was missing the do {} while (0) wrapping 2026-08-30 19:35:37 +02:00
René van Dorst fc3c686564 Merge pull request #359 from bloqaudio/fix/upload-preamble-split
httpd: buffer a firmware upload's part header before streaming
2026-08-30 16:48:53 +00: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
René van Dorst 1db30f22de Merge pull request #330 from vDorst/SRAM-easy-changes
Sram easy changes
2026-08-29 18:33:29 +00:00
logicog 4c70e654d1 Merge pull request #363 from vDorst/refactor_remove_keyword_register
Remove register keyword from all the function arguments.
2026-08-29 11:56:22 +02:00
feelfree69 3c24f2bdea Merge pull request #260 from donbernhardo/fix-kp-9000-6xhml-x2-led-mux
Add KP-9000 6XH and 6XHML PCB-revisions to Machine targets
2026-08-29 10:16:16 +02:00
donbernhardo 3e610392a9 Merge remote-tracking branch 'origin/main' into fix-kp-9000-6xhml-x2-led-mux
# Conflicts:
#	machine.c
2026-08-28 23:01:38 +02:00
René van Dorst 146771f5ca Merge pull request #362 from bloqaudio/fix/v210-bank0
leds: move the SWTG018AS-V2.1.0 custom LED init to BANK2
2026-08-27 16:32:09 +02:00
René van Dorst bf0bea1edd Remove register keyword from all the function arguments.
Adds no value.
2026-08-26 21:52:31 +02:00
René van Dorst 907ad08830 uip: uip_arp_update() put more arguments in xdata
Saves 7-bytes.
2026-08-26 20:51:32 +02:00
René van Dorst 7cf0115182 httpd: scan_header() put argument on xdata
Saves 2 bytes
2026-08-26 20:51:32 +02:00
René van Dorst adfbead438 syslog: syslog_callback() put local variables on xdata.
Save atleast 2 bytes.
2026-08-26 20:51:32 +02:00
René van Dorst 301774040b uip: place some pointer in xdata.
Saves 2 bytes.
2026-08-26 20:51:32 +02:00
René van Dorst 683037f410 httpd: rename gen_random_bytes to gen_random_hex_chars
This reflexs the function better.
Moving `byte` arguments to __xdata which saves 1 SRAM byte.
2026-08-26 20:51:27 +02:00
René van Dorst 57b820c4e8 httpd: mark pointer variable as __xdata.
Otherwise it is put on SRAM location.

Saves 6 SRAM bytes
2026-08-26 20:49:58 +02:00
d00f 75b223223c Merge upstream/main into the Spanning Tree branch
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.
2026-08-26 16:29:39 +02:00
d00f 9a6e2b3e01 pins: put the module back in BANK2
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.
2026-08-26 01:05:40 +02:00
d00f 6e394300c8 Merge upstream/main into the Spanning Tree branch
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.
2026-08-25 23:28:09 +02: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
logicog 45d0aac118 Merge pull request #326 from vDorst/SRAM-cmd-parse-port-helper
Add/refactor helpers to parse IP, numbers and port arguments
2026-08-25 12:49:00 +02:00
René van Dorst ee05cd4000 Fix: send_counters(), port-argument have to be physical port number.
Not the physical port index!
2026-08-25 07:55:17 +02:00
René van Dorst 74013bb14f CMD: Treat port zero as CPU-port 2026-08-25 07:55:17 +02:00
René van Dorst beb14deba5 httpd: send_counter(): Validate phys_port_idx and better error handling 2026-08-25 07:55:17 +02:00
René van Dorst 1212def799 CMD: fixes many comments 2026-08-25 07:55:17 +02:00
René van Dorst 95b3bde822 Rename print_port() to print_phys_port() 2026-08-25 07:55:17 +02:00
René van Dorst f511453f79 CMD: refactor code 2026-08-25 07:55:17 +02:00
René van Dorst 8653216047 CMD: Fix some comments 2026-08-25 07:55:17 +02:00
René van Dorst e62e0fc8bd CMD: Isolate allow the CPU-port as a member. 2026-08-25 07:55:17 +02:00
René van Dorst 2ff418f80f Replace '\0' to NUL to make it more clear that it is a NUL-terminated string. 2026-08-25 07:55:06 +02:00
René van Dorst 5d42e8843e Rename cmd_is_space_or_null to cmd_is_space_or_nul 2026-08-25 07:54:15 +02:00
René van Dorst 3c183f73f8 Added print_port() helper to print physical port number. 2026-08-25 07:54:15 +02:00
René van Dorst 3776f12f5c Fix and refactor parse_bw() 2026-08-25 07:54:15 +02:00
René van Dorst 5ddec9710a CMD: Fix parse_lag_hash() 2026-08-25 07:54:15 +02:00
René van Dorst f627559005 fix CMD: pvid: remove atoi_results_u8 from check 2026-08-25 07:54:15 +02:00
René van Dorst dca92d7f45 cmd_parse_port_separator() cmd_parse_port() remove CPU-port support
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.
2026-08-25 07:54:15 +02:00
René van Dorst 149dc8537f Rename cmd_parse_port_space() to cmd_parse_port_separator()
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.
2026-08-25 07:54:15 +02:00
René van Dorst 42f7b4fba1 Fix parse_isolate(), ensure spaces between arguments 2026-08-25 07:54:15 +02:00
René van Dorst 17642699d4 Improve parse_ingress() 2026-08-25 07:54:15 +02:00
René van Dorst 724fd1e061 Added print_ip() to print IPv4 addresses. 2026-08-25 07:54:15 +02:00
René van Dorst 3eb2dfe0c9 Change parse_ip() 2026-08-25 07:54:15 +02:00
René van Dorst 73df006d39 Replace all manual port parsing with cmd_parse_port_space() or cmd_parse_port().
Saves no SRAM but around 1k code size
2026-08-25 07:54:15 +02:00
René van Dorst 175f3bb185 Add cmd_parse_port() and cmd_parse_port_space() helper.
A lot of places we manual parse and translate the port.
These helper functions will do that for us.
2026-08-25 07:54:15 +02:00
René van Dorst 57061161b6 Refactor code to make use of new atoi_byte() 2026-08-25 07:54:15 +02:00
René van Dorst 80f97a50e0 Refactor atoi_byte() same as atoi_short() 2026-08-25 07:54:15 +02:00
René van Dorst c265a20d83 Refactor the code for the new atoi_short()
Free-up 1 SRAM-byte
2026-08-25 07:54:15 +02:00
René van Dorst 4937618684 Change atoi_short() so it returns number of bytes consumed. 2026-08-25 07:54:15 +02: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
logicog f582576b10 Merge pull request #345 from DrDoof/fix/sfp-i2c-error
sfp: notice when an I2C read fails
2026-08-25 05:13:47 +02:00
feelfree69 16b0f207ed Merge pull request #317 from bloqaudio/pr/swtgw218as-sfp-led
machine: add PCB SWTG018AS-V2.1.0 variant of the SWTGW218AS
2026-08-25 02:22:21 +02:00
René van Dorst fe2bcc7f2d Merge pull request #356 from bloqaudio/fix/config-post-race
httpd: buffer a configuration upload before touching the flash
2026-08-24 23:35:19 +00:00
d00f a0628c7df5 sfp: read the EEPROM in blocks instead of a byte at a time
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.
2026-08-25 01:09:20 +02: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
donbernhardo 8a28b4fc88 Merge upstream main and resolve conflicts 2026-08-24 10:51:27 +02:00
d00f 9a96c80af2 sfp: move the I2C transfer to the banked pins module
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.
2026-08-24 02:34:39 +02: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
logicog 025f72c876 Merge pull request #258 from bennydiamond/newline-on-uip-serial-print
Automatically print newline on serial interface
2026-08-22 19:14:45 +02:00
d00f 62f80f0ad2 nic: report a transfer that does not complete instead of carrying on
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.
2026-08-19 16:20:34 +02:00
d00f 803d9c1242 nic: do not claim a cause for the bounded TX wait
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.
2026-08-18 23:32:33 +02:00
d00f ec069a5bcc stp: rename stpEnabled and take the comments out of the header
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.
2026-08-18 23:32:25 +02:00
d00f 81d11c246c stp: correct what the blocking state does
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.
2026-08-18 23:31:06 +02:00
d00f 85d1d7520a stp: send BPDUs with a per port source address
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.
2026-08-18 23:31:05 +02:00
d00f 810db48a00 stp: drop the bare scope blocks around the port variable
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.
2026-08-18 23:31:02 +02:00
d00f f9d3dec50f Do not insert the management VLAN tag into a CPU-tagged frame
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.
2026-08-18 23:31:00 +02:00
d00f 236eac610d stp: name the BPDU version, type and flag constants
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.
2026-08-18 23:30:53 +02:00
d00f 9df9eece3f stp: drop the comment on the CPU tag flags
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.
2026-08-18 23:30:52 +02:00
d00f 3cd9131795 stp: drop the inline comments in stp_loop_hold_peer
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.
2026-08-18 23:30:50 +02:00
d00f 50b68f908e stp: drop the owner comments on the externs 2026-08-18 23:30:49 +02:00
d00f f1f3d521b5 stp: drop the comment on the rtl837x_port.h include 2026-08-18 23:30:46 +02:00
d00f addca1cb26 httpd: use local variables for the loop indexes in send_stp
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.
2026-08-18 23:30:46 +02:00
d00f cbd2a8c080 httpd: drop the tick rate comment on the ticks extern 2026-08-18 23:30:45 +02:00
d00f 78b3971782 stp: pass a received topology change through the switch
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.
2026-08-18 23:30:44 +02:00
d00f 201c7e333d httpd: drop the byte-access justification from u32hex_html
Byte access instead of 32-bit shifts is how this codebase works
everywhere, so the comment explained a house rule at one call site.
2026-08-18 23:30:40 +02:00
d00f 2c26a4f389 stp: count the BPDUs each port has sent
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.
2026-08-18 23:30:38 +02:00
d00f 266c95e4d7 stp: name the port state, role and edge in stp status
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.
2026-08-18 23:30:37 +02:00
d00f 38b19820e4 stp: show how long ago each port last heard a BPDU
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.
2026-08-18 23:30:37 +02:00
d00f 988bddacf7 stp: compare the whole Bridge Identifier, not the priority byte and the MAC
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.
2026-08-18 23:30:35 +02:00
d00f 4264856109 stp: stop treating a port as an edge once it hears a BPDU
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.
2026-08-18 23:30:34 +02:00
d00f 35d26cb08f stp: name the status subcommand in the usage line
"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.
2026-08-18 23:30:32 +02:00
d00f a9af466702 stp: drop the management failsafe
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.
2026-08-18 23:30:25 +02:00
d00f 024c8cef49 stp: arm the management failsafe only where an operator asked for it
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.
2026-08-18 23:29:18 +02:00
d00f 64906ad135 stp: name front panel ports on the console, and add "stp status"
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.
2026-08-18 23:29:17 +02:00
d00f 4d7a2e28c7 stp: keep the designated bridge recording out of internal RAM
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.
2026-08-18 23:29:16 +02:00
d00f a6c5f558bf stp: do not arm the management failsafe while the config replays
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.
2026-08-18 23:29:16 +02:00
d00f 5a6b854c5d stp: record the designated bridge, port and cost from received BPDUs
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.
2026-08-18 23:29:15 +02:00
d00f d31980eb9e stp: sort the port rows, show this switch's bridge ID, react to enabling
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.
2026-08-18 23:29:14 +02:00
d00f eb4a6ac275 stp: drop the bounds on the L2 flush and L2MC table waits
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.
2026-08-18 23:29:13 +02:00
d00f 1ba1224644 stp: trim the comments, and put one back on the variable it describes
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.
2026-08-18 23:29:11 +02:00
d00f 4a78b2dc8c doc: move the L2 multicast and tag word details out of the code
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.
2026-08-18 23:29:10 +02:00
d00f 0160b430f4 doc: correct what a blocked port does with frames
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.
2026-08-18 23:29:09 +02:00
d00f c72d36af36 stp: turn the management failsafe into a one-shot window
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.
2026-08-18 23:29:08 +02:00
d00f 7ebb420e7d stp: warn when an enabled port cannot receive BPDUs
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.
2026-08-18 23:28:39 +02:00
d00f 2cf60e177b stp: derive the BPDU flags from the port state
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.
2026-08-18 23:28:38 +02:00
d00f 0ce5fb4af3 stp: latch the block on a looped port
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.
2026-08-18 23:28:37 +02:00
d00f 4b5bf09c83 doc: separate the trap action from CPU-port delivery
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
2026-08-18 23:28:37 +02:00
d00f be520d6716 stp: accept BPDUs with a protocol version above 2
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.
2026-08-18 23:28:35 +02:00
d00f f3c1b7f3ac stp: react to a port losing carrier
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.
2026-08-18 23:28:34 +02:00
d00f 7a790b32b1 README: STP is no longer missing
Point at doc/stp.md instead, with the caveat that the implementation is a
simplified one and that enabling it remotely deserves a read first.
2026-08-18 23:28:34 +02:00
d00f 293a196c91 doc: describe the Spanning Tree support
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.
2026-08-18 23:28:33 +02:00
d00f 6b4d2b9cfa stp: apply an edge-port change immediately
"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.
2026-08-18 23:28:33 +02:00
d00f 3c4fb679bd stp: correct the timer tick rate (50 Hz, measured)
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.
2026-08-18 23:28:32 +02:00
d00f f1f69e26fd stp: do not carry a dropped BPDU's flags over to the next one
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.
2026-08-18 23:28:31 +02:00
d00f b51ed71bb1 stp: announce topology changes and flush the port that changed
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.
2026-08-18 23:28:30 +02:00
d00f 65a41eaddd port: add a bounded single-port L2 flush
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.
2026-08-18 23:28:30 +02:00
d00f 5361c85d30 stp: keep the root port quiet and relay the message age
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.
2026-08-18 23:28:29 +02:00
d00f a31be093e7 stp: answer TCN BPDUs and validate the received BPDU length
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.
2026-08-18 23:28:28 +02:00
d00f f39eefa90c stp: carry the version-1 length field in RST BPDUs
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).
2026-08-18 23:28:27 +02:00
d00f eaff9537ef stp: vendor-style per-port config + status (path cost, p2p, designated info)
Bring the Spanning Tree page in line with a typical managed switch's
per-port panel. Configuration gains the full-range path cost (raw
0..200000000, 0 = auto, replacing the old 1000x-scaled byte), a
point-to-point admin control (auto/on/off), and the priority is now a
0..240 step-16 dropdown. A new status table shows, per port, the Port
State, Role, Designated Bridge / Port ID / Cost (learned from received
BPDUs, kept per port and aged via the BPDU age), Operational Edge and
Operational Point-to-Point.

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

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

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

(cherry picked from commit 2ec62072f061dc9e78bc821ba1c297cb6819e206)
2026-08-18 23:28:27 +02:00
d00f 2110cf128a stp: management failsafe (commit-confirm) + bounded NIC waits
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)
2026-08-18 23:28:26 +02:00
d00f 267e371d3c stp: BPDUs finally reach the wire (CPU tag flags + management VLAN)
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)
2026-08-18 23:28:25 +02:00
d00f 9702f8e4c4 stp: reject port sub-commands with a missing argument
"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)
2026-08-18 23:28:24 +02:00
d00f 524c37d7c7 stp: full RSTP configuration (bridge + per-port), CLI + GUI + persistence
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)
2026-08-18 23:28:23 +02:00
d00f b5387016fa gui: Spanning Tree page (config + live status)
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
2026-08-18 23:27:09 +02:00
d00f 5c881da4fe stp: stop forcing the SFP port to forwarding in the CPU-port mask
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.
2026-08-18 23:27:08 +02:00
d00f 0df699659a stp: actually promote ports out of blocking; calibrate timers
"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)
2026-08-18 23:27:08 +02:00
d00f 032305ad3a stp: move the STP module to code bank 2
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.
2026-08-18 23:27:07 +02:00
d00f b16c7d9723 common: define the RTL frame-tag flag bits shared by STP and LACP
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)
2026-08-18 23:27:06 +02:00
d00f 6e1f199571 stp: contain BPDUs to the CPU while STP runs
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.
2026-08-18 23:27:06 +02:00
d00f 5329193987 port: add a helper to steer a link-local group via a static L2 entry
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.
2026-08-18 23:27:05 +02:00
logicog 52cf759bec Merge pull request #355 from bloqaudio/fix/l2-static-display
httpd: read the static flag of an L2 entry from the byte that holds it
2026-08-18 19:44:05 +02:00
René van Dorst cbbc6160f4 Merge pull request #357 from bloqaudio/fix/ingress-port-bound
cmd_parser: reject port 0 in the ingress command
2026-08-18 09:05:06 +02: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
René van Dorst 80095ba617 Merge pull request #354 from DrDoof/fix/isolate-cpu-port
port: reject the CPU port in isolate instead of refusing it silently
2026-08-17 18:31:25 +00: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
d00f 47b3ee60b6 port: reject the CPU port in isolate instead of refusing it silently
parse_isolate() accepted a two digit port and mapped it to logical port 9,
the CPU port, while port_isolate() and port_isolation_get() both refuse
anything above machine.max_port. Setting the isolation of the CPU port was
therefore declined without a word and reading it always answered no
members, whatever the hardware held.

Bound the port to the front panel, so the command says what it does. The
digit is checked before it indexes phys_to_log_port[], which a non numeric
argument used to read past.
2026-08-17 10:53:17 +02:00
logicog 59d20ed9b6 Merge pull request #339 from plaes/parallel-build
Fix parallel build
2026-08-17 08:11:25 +02:00
logicog f41ed2b943 Merge pull request #332 from DrDoof/fix/igmp-enable-gate
igmp: only hand reports to the CPU while snooping is on
2026-08-17 07:58:18 +02:00
d00f 56fa96c494 igmp: only hand reports to the CPU while snooping is on
handle_rx() dispatched to igmp_packet_handler() on the destination
address alone, so an ordinary IGMPv3 report off the wire reached the
handler and could write a table entry whether or not anyone had asked
for snooping. The STP branch right above it is gated on stpEnabled;
this brings the IGMP branch in line.

Snooping state lived only in the per-port registers, and the receive
path cannot afford to read one per packet, so the flag shadows it:
igmp_enable() sets it, igmp_setup() clears it, and igmp_setup() runs
from both the boot path and "igmp off".

While here, igmp off becomes an explicit subcommand instead of the
fall-through, and an unrecognised igmp subcommand prints the usage
line rather than silently turning snooping off.

Six bytes of BANK1 and one of xdata, no internal RAM.
2026-08-16 23:42:46 +02:00
René van Dorst 6bfcbd2f9a Merge pull request #349 from DrDoof/fix/lag-cmd-bounds
lag: number the groups from one and bound what the command is given
2026-08-16 06:48:28 +00:00
d00f fa7895ad62 doc: the aggregation example used group zero, which no longer parses
The command now numbers groups the way 'lag show' prints them, so the
walkthrough would have failed at its first step.
2026-08-16 04:10:03 +02:00
d00f d405dd7776 port: let a pvid name an aggregation group
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.
2026-08-16 01:52:03 +02:00
d00f 1ed8b131bc httpd: keep the send_l2 flags in bit memory
The two flags added with the JSON fix sit in data, where internal RAM is
full enough that this branch stopped linking for some toolchains. __bit
puts them in the bit area instead and hands three bytes back to the
stack: SSEG goes from 131 to 134 on SWTGW218AS.

Patch by vDorst on the pull request.
2026-08-16 01:23:56 +02:00
d00f 9ada6adad7 lag: number the groups from one and bound what the command is given
lag show has always printed the groups as 1 to 4 while lag <n> took the
number literally, so typing what you saw configured the group beside it.
Both lag and lag hash count from one now, matching how ports are numbered
everywhere else, and reject anything outside 1 to 4. Subtracting '1' makes
0 wrap well past three, so one test covers both ends.

The port argument indexed machine.phys_to_log_port, which holds nine
entries, before it was checked, and a two digit argument reaches 109. It is
bounded before the table is touched rather than after.

port_lag_members_set() and port_lag_hash_set() complained about a group out
of range and then wrote the registers anyway, past the four the groups
occupy. They return instead.

lag hash also read cmd_words_b[1] without checking a word was there, and
now shares the error path parse_lag() already had.
2026-08-16 01:08:30 +02:00
René van Dorst 719c6db228 Merge pull request #344 from DrDoof/feat/walk-l2
html: move the L2 table walk into a shared walkL2() helper
2026-08-15 21:38:43 +00:00
d00f a96fdfe10c html: move the L2 table walk into a shared walkL2() helper
Both the L2 page and the ports page in #335 need to page through /l2.json
and decode the same fields, and the second copy arrived carrying the two
bugs the first one had only just been fixed for. Rather than keep two
copies in step by hand, the transport and the decoding move to main.js,
which every page already loads, and each page keeps only what is its own.

walkL2(onDone) pages through the table once, parses idx and vlan out of
hex, maps the port to a physical number or to 'CPU', and calls
onDone(entries, ok). It stops on a wrapped index, an empty page or 4096
entries, all of which set ok. A page that comes back as anything other than
200, or with a body JSON.parse rejects, is asked for again at the same
index up to three times; only once those run out does the walk end with ok
clear, so a caller can tell a finished table from a partial one. l2.js
keeps the s and l to label mapping, since that needs the page's own
translations, redraws only when ok is set, and restarts the walk from its
callback either way.

Two things change while moving:

The next request goes out from the previous reply rather than from a
setInterval that fires whether or not the last one came back. The httpd
serves one connection at a time, so a timer that outruns the responses only
queues work it cannot use.

A walk that reaches 4096 entries hands over what it collected. Before it
threw the entries away and cleared its own interval, which left the page
unable to refresh again until it was reloaded.

The retry is not a new idea, it is the old behaviour written down. The
previous code ignored anything that was not a 200 and let the interval ask
for the same index again, so a blip never disturbed the table on screen.
Dropping that on the way to a chained walk would have made every timeout
redraw the page with a truncated table, which at one connection at a time
is not a rare event.

Driven with a scripted server in node, running the helper itself rather
than a copy of it: an empty table gives 0 entries in 1 request; three pages
ending in a repeated index give 61 entries in 3 requests, asking for 0, 30
and 60; an empty page ends the walk after 2; a 500 and a malformed body are
each retried at the same index and then complete normally, asking 0, 30, 30
and 31; three failures in a row end the walk with ok clear and the 30
entries already collected; 4096 entries in one page end it with ok set; the
CPU port decodes to 'CPU'; vlan and idx come back as numbers.

main.js grows by 1331 bytes and l2.js loses 1039, so 292 bytes of flash.
Worth stating where they land: main.js is loaded by every page, so pages
that never walk the table now carry the helper too. That is the cost of
having the decoding exist exactly once, which is the point of the move.
2026-08-15 23:16:29 +02:00
d00f 1d1e33f4d5 sfp: notice when an I2C read fails
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.
2026-08-15 23:14:53 +02:00
d00f 3fda9ccd86 html: handle an empty MAC table reply and fix the entry cap
l2.js reads the last index of a reply to know where the next page starts.
With the firmware side of this branch an empty table answers [], and
s[s.length-1] then throws on undefined. It used to answer commas with
nothing between them and throw in JSON.parse instead, so this is the same
case reaching a different line rather than a new one. An empty reply now renders what
has been collected and starts the next pass from zero.

The 4096 entry cap compared the array against the number instead of its
length, so it never fired: an empty array and a 5000 element one both
compare false. Comparing the length restores what the check was for.
2026-08-15 23:14:49 +02:00
d00f 3be6667789 httpd: emit valid JSON from send_l2
The MAC table listing wrote its separator once per iteration rather than
once per object. An entry the table engine reports as invalid produces no
object, so it contributed a bare comma, and two in a row give ",," which
JSON.parse rejects. The whole table then fails to load, not just the row
that was missing. The separator now goes before each object and the
closing bracket after the loop, which is the shape send_vlanlist already
uses further down the file.

The next index for an invalid entry was computed as h | low + 1, and the
addition binds tighter than the or. That agrees with (h | low) + 1 except
when the low byte reads 0xff and bit 8 of the index is already set, eight
of the 4096 combinations. There the result is the start of the current
block of 256 rather than the start of the next one, so the walk repeats a
block it has already covered. Reading the index once after the branch
rather than once in each arm removes the second copy of that expression
along with the bug.

The VLAN now comes first in each object. It is taken from the same
L2_DATA_OUT_B read that decides whether the entry is valid, which saves
reading that register a second time. The page addresses the fields by
name, so the order they arrive in does not matter to it.

A bound check on the output buffer goes in for consistency with
send_vlanlist. Thirty entries of at most 74 bytes plus the brackets fit in
the 2500 byte buffer with 179 to spare, so nothing changes today, but the
margin was nowhere stated and L2_MAX_TRANSFER is a tunable.

5 bytes of BANK1, nothing in BANK2, xdata or internal RAM. Built for SWTGW218AS
and KP_9000_6XHML_X2 on sdcc 4.5.0.
2026-08-15 23:14:48 +02:00
René van Dorst 5103d1c168 Merge pull request #338 from DrDoof/fix/lag-hash-default
port: make the trunk hash default reachable again, and per group
2026-08-15 21:01:09 +00:00
René van Dorst ce85576882 Merge pull request #346 from DrDoof/fix/crtstart-home
Rename crtstart.asm to crtbank.asm
put the bank switching helpers in HOME-code location
2026-08-15 20:33:54 +00:00
d00f 59c60504e7 crtbank: put the bank switching helpers in HOME
__sdcc_banked_call and __sdcc_banked_ret were assembled into GSFINAL, which
sits in the startup path: GSINIT ends exactly where GSFINAL begins, so the
processor falls into it rather than being sent there. It works today only
because this object comes after every C object on the link line, so the
LJMP to __sdcc_program_startup is laid down first and the helpers land
behind it. Reordering that line, or moving main() into another module,
would put the helper body at the fallthrough address instead, and the board
would not come up out of a build that reports nothing wrong.

SDCC's own crtbank.asm declares the area order and then puts both symbols
in HOME, so the file takes that name and that preamble as well.

GSFINAL now holds the three byte jump and nothing else. Of 401 symbols 17
change address, every one in the startup region, and between the reset
vector and 0x0094 not a byte differs, so no interrupt vector is disturbed.

Run on a SWTGW218AS: it came back after about 42 seconds reporting the new
build, with its stored configuration byte identical and every link at the
speed it had before.
2026-08-15 22:18:13 +02:00
d00f a3c586ef38 port: make the trunk hash default reachable again, and per group
Wrapping REG_SET restored the guard in front of the hash default, and
that exposed three things about the line it guards.

The test was against zero. The register does not read zero: it comes out
of reset holding source port number plus both MAC fields, both IP fields
and the L4 source port, which the header now names LAG_HASH_RESET.
Measured on an SWTGW218AS, where all four groups read 0x3f after a cold
boot and a value written before a power cycle is gone afterwards. With
the guard working and the test unreachable, the default would never be
installed, where before it was installed on every call. Testing against
the reset value restores the intent, and zero is still accepted in case
another device does reset that way.

The write went to the base address while the read that decides it used
the group offset, so a group other than zero was tested and group zero
was written. Both ends use the offset now.

The range check printed a complaint and carried on. It returns, which
matters more now that the hash write also uses the group number to build
an address.

Thirty bytes of BANK1.
2026-08-15 22:01:08 +02:00
René van Dorst 8bc530d3d0 Merge pull request #347 from DrDoof/feat/lag-members-get
port: read a trunk's members through one function
2026-08-15 19:23:50 +00:00
d00f f2c6ac01d9 port: read a trunk's members through one function
The member mask of an aggregation group is decoded by hand in two places,
the lag command and the JSON behind the aggregation page, and every branch
that touches trunks adds another copy.

port_lag_members_get() sits next to port_lag_members_set() and both readers
call it. It answers from the hardware, so it covers a group configured with
lag and one a protocol brought up, without either having to say so.

It reads through reg_read() rather than reg_read_m(), so sfr_data is left
alone. Neither caller looked at it afterwards; both read the hash register
next.
2026-08-15 20:55:56 +02:00
René van Dorst 94d9f2c8e8 Merge pull request #341 from DrDoof/fix/counters-port-index
httpd: check the port index /counters.json is given
2026-08-15 07:52:54 +00:00
d00f 0503e7952d httpd: check the port index /counters.json is given
The handler took one raw character of the request line and passed it to
send_counters(), which uses it to index machine.phys_to_log_port. That
array has nine entries and the character is whatever the client sent, so
the read ran up to 246 entries past the end and the result went on to
STAT_GET as a port number. is_word() accepts any request whose name is
followed by a question mark, so nothing constrained the byte to a digit.

Bounding it where it is read keeps the check beside the assumption it
protects and needs nothing from the machine description. sdcc leaves
plain char unsigned and the subtraction wraps in eight bits, so a byte
below '0' comes out above 200 and one upper test covers both ends:
exactly '0' to '8' now reach send_counters. The compiled test is
add a,#0xf7 followed by jnc, which I read back out of the assembly rather
than assuming.

Out of range answers 400 by the path the other malformed requests already
take, rather than an empty array. An empty array would have been worse
than useless here, since the statistics page calls BigInt on the first
element before it looks at the length. The page asks only for index zero
to the port count minus one, so nothing that answered before stops
answering, and a non-200 reply makes its handler do nothing at all.

11 bytes of BANK1, nothing in the common segment, BANK2, xdata or
internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
2026-08-15 00:49:21 +02:00
René van Dorst 787d593996 Merge pull request #337 from DrDoof/fix/igmp-dup-loop
igmp: drop the duplicate port configuration loop
2026-08-14 22:12:33 +00:00
René van Dorst db2a541e5f Merge pull request #331 from DrDoof/fix/reg-macros-braces
regs: wrap REG_SET and REG_WRITE, and fix what that uncovers
2026-08-14 21:29:03 +00:00
Priit Laes 11be13fe53 build: Fix double generation of html_data
Make supports grouped target which runs once for all listed targets.
2026-08-14 11:16:05 +03:00
Priit Laes 65f8afb908 build: Remove undefined html variable
HTML target references $(html) which was never defined, which
causes find to run through the entire source tree.
2026-08-14 11:16:05 +03:00
Priit Laes 0126b159bf build: Make create_build_dir PHONY and add order-only deps
Mark create_build_dir as PHONY so directory creation is never skipped.
Also add html_data.h as order-only prerequisite to the .c pattern rule,
fixing another the race where httpd/httpd.c and httpd/page_impl.c are
compiled before the generated header exists.
2026-08-14 11:14:59 +03:00
Priit Laes 31024388ba build: Fix tools dependency for parallel builds
Replace file-path prerequisite tools/output/fileadder with order-only
dependency on the tools PHONY target.

Also add tools as order-only dependency to the final .bin target which
invokes all of the tools.
2026-08-14 11:14:29 +03:00
Priit Laes 9f2d6b72b7 build: Fix version.h race condition in parallel builds 2026-08-14 11:13:17 +03:00
d00f 5c5dcb5209 igmp: drop the duplicate port configuration loop
igmp_setup() writes the per-port IGMP configuration twice, once with the
value spelled out and once with the same number assembled from the
constants: IGMP_MAX_GROUP | IGMP_PROTOCOL_ENABLE | IGMP_FLOOD is exactly
0x00ff7c15. The second loop carries the comment block explaining the bit
layout, so the literal one is the one to drop.

Today the cost is one redundant register write, because REG_SET is not a
single statement and the unbraced loop body only ever reaches index
machine.max_port + 1. Once the macro is wrapped it becomes one redundant
write per port on every boot and on every "igmp off", which is what makes
this worth removing rather than leaving.

Fifty nine bytes of BANK1 on SWTGW218AS, nothing anywhere else.
2026-08-14 07:39:24 +02:00
d00f be6d47a3ab regs: wrap REG_SET and REG_WRITE in do { } while (0)
Both macros expand to a run of statements joined by backslashes with
nothing around them, so as the unbraced body of an if or a for only the
first assignment belongs to that body. The other three and the
reg_write() call sit after it and run once, unconditionally, with
whatever the loop counter ended on. Wrapping each macro into a single
statement is what every call site already assumes it to be.

This hands the compiler no new room around the SFR writes, which is worth
showing rather than asserting. Building the whole image before and after
and comparing the generated assembly module by module, with label
numbering, block scope suffixes and the version string normalised away,
three modules differ: rtl837x_igmp, rtl837x_port and rtl837x_leds. Ten
modules call these macros, so the other seven come out identical, and so
does everything else in the image.

Those three differ because they hold the five unbraced uses. Two of them
want a fix rather than only the brace, and that is left to the commits
that follow.

Six bytes of BANK1 on SWTGW218AS, nothing anywhere else.
2026-08-14 07:34:42 +02:00
d00fandd00f 4ff009dbfc uip: cap TCP MSS to 1460 to survive jumbo-MTU clients (#298)
* uip: parenthesise UIP_LLH_LEN

The macro expands to a bare sum, so wherever it is subtracted the second term
gets added instead. UIP_TCP_MSS - and with it UIP_RECEIVE_WINDOW - therefore
comes out 24 bytes above the buffer's real capacity. UIP_APPDATA_SIZE and
UIP_REASS_BUFSIZE are wrong the same way, though neither is reachable today.

The additions, uip_buf[UIP_LLH_LEN] and friends, were right by luck.

* uip: keep the advertised MSS below the buffer edge

Deriving the MSS straight from the buffer size makes the switch advertise
exactly the segment that fills uip_buf to its last byte, and a peer that takes
it literally corrupts every large upload: the firmware image arrives fully
acknowledged, with no retransmissions on the wire, yet the CRC over the
streamed body never matches and the flash write is abandoned.

Isolated by changing nothing but the segment size, same buffer and same file:
1490-byte segments fail four times out of four, 1460-byte segments succeed,
745-byte segments succeed. Linux halves its segments against a window this
small, so only macOS on a jumbo link ever produces a full-size segment - which
is why the failure hides so well.

Where exactly the full segment breaks the stream is not pinned down yet; until
it is, the advertised MSS stays a step below the edge.

* uip: size the buffer to the largest frame the CPU port accepts

UIP_TCP_MSS derives from UIP_CONF_BUFFER_SIZE, and the buffer was large enough
for frames the hardware will never deliver, so the switch advertised a segment
size no peer could usefully reach. A client on a jumbo-MTU link took it at its
word and the oversized replies went nowhere.

Size the buffer to the ingress limit instead. ICMP bypasses MSS and so probes
the hardware directly: on a SWTGW218AS a 1502-byte payload is answered and 1503
never arrives, which puts the largest frame the NIC hands us at 1556 bytes of
uip_buf. UIP_TCP_MSS then derives to 1490, the same edge measured over TCP.

Frames above the limit are dropped by the NIC rather than written to the
buffer - an 8 kB ping leaves the switch untouched - so nothing overruns it.
Frees 644 bytes of XDATA.

* uip: trim these comments, one of which had stopped being true

The note above UIP_CONF_BUFFER_SIZE claimed the MSS derives from it as 1490.
It does not: the commit that follows pins the MSS at 1460 on purpose, a step
below that ceiling, because a segment filling the buffer to its last byte
corrupts large uploads. Left as it was, the file argued with itself.

Both blocks are shorter now. What justifies the numbers stays, which is the
ICMP measurement behind 1556 and the four-out-of-four failure behind 1460.
What went is the storytelling around them, which belongs in this thread rather
than in a config header.

* uip: derive the MSS from the buffer again, minus explicit headroom

The review asked why the buffer size and the MSS are both set by hand when one
used to follow from the other. They answer different questions, but the gap
between them is a number in its own right, so it gets a name now:
UIP_CONF_BUFFER_EXTRA, and UIP_TCP_MSS goes back to being derived.

The headroom is where the measurement lives. A segment that fills uip_buf to
its last byte corrupts large uploads: with nothing but the segment size
changing, 1490 fails four times out of four and 1460 succeeds. With the buffer
sized to the frame the NIC accepts, an extra of 30 lands on 1460.

Deriving it the other way round does not work. Sizing the buffer from a 1460
byte MSS gives 1526, which is 30 bytes under the frame the NIC actually
delivers. A 1502 byte ICMP payload occupies 1556 bytes of uip_buf and is
answered today, and it would stop fitting.

The generated image is byte for byte the same as the one with 1460 written out,
so the expression lands on the value that was measured.

---------

Co-authored-by: d00f <tokyusho@chatik.pl>
2026-08-14 06:42:36 +02: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
René van Dorst 0c339e9123 Merge pull request #328 from DrDoof/fix/httpd-get-path-walk
httpd: stop the GET request line walk at the end of the buffer
2026-08-12 09:28:22 +02:00
René van Dorst 75151e0b00 Merge pull request #319 from bloqaudio/pr/cmd-editor-overflow
cmd_editor: fix cmd_buffer overflow and CLI hang on long input
2026-08-12 09:26:17 +02:00
d00f 43845b911b httpd: stop the GET request line walk at the end of the buffer
The POST path tests for a NUL before it looks at a byte. The GET path did not,
and is_separator() counts only space, tab, question mark and equals, so a request
line carrying none of those walks past the end of uip_buf and writes its
terminator into whatever xdata it happens to stop on.

Everything that is not a POST reaches that walk. The pointer advances past the
method before anything checks that the method was GET, so a TLS record sent to
port 80 by a browser trying https first is enough on its own, as is a port
scanner or a malformed line. The stop is wherever the first space, tab, question
mark or equals turns up in memory, which is why the symptoms are erratic.

Two bytes of BANK1. BANK2 and xdata do not move.
2026-08-12 03:22:01 +02: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
logicog 5e30f8e6c0 Merge pull request #318 from bloqaudio/pr/mac-from-flash
machine: optionally read the factory MAC from flash
2026-08-11 14:32:32 +02:00
logicog 7f3c91ac0c Merge pull request #321 from bloqaudio/pr/dhcp-hostname-opt12
dhcp: announce the hostname via option 12
2026-08-11 14:30:21 +02: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
logicog 53d865059d Merge pull request #320 from bloqaudio/pr/reproducible-build
makefile: derive BUILD_DATE deterministically
2026-08-11 08:50:05 +02:00
logicog b90aedd8f2 Merge pull request #316 from bloqaudio/pr/igmp-memset-cast
rtl837x_igmp: cast &entry to __xdata pointer in memset()
2026-08-11 08:43:28 +02: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
René van Dorst 52692d05b1 Merge pull request #313 from DrDoof/fix/vlan-id-range
Reject VLAN IDs and numeric arguments the hardware cannot take
2026-08-08 20:51:43 +00:00
René van Dorst d364bd69e4 Merge pull request #306 from DrDoof/mgmt-vlan-gui
system: show and set the management VLAN in System Settings
2026-08-08 20:44:07 +00:00
René van Dorst 70ab7ff769 Merge pull request #307 from DrDoof/l2-sort-filter
l2: fix the CPU/SFP port label, add sorting and filtering
2026-08-08 20:29:50 +00:00
d00f e30976be76 httpd: move the management VLAN into /vlanlist
Review feedback on #306. The value used to ride in /information.json and
the picker fetched /vlanlist separately, so the page needed both requests
to mean anything. /vlanlist now answers {"mgmt":N,"vlan":[...]} and the
picker reads both from the one response. Both consumers in vlan.js were
taught the new shape, and /information.json no longer carries mgmt_vlan.

The truncation guard now reserves 141 bytes instead of 139: the closing
grew to two bytes with the wrapping object, and the comma in front of a
non-first entry was never counted, so the worst case could land one byte
past outbuf even before this change. char_to_html() does not check.

The comments added on this branch are gone as well, style.css and
system.js both, since these files are served byte for byte.

page_impl.rel stays at DSEG 5, OSEG 0, BSEG 3 and the image reports the
same 10183 bytes of XDATA before and after.
2026-08-08 18:26:49 +02:00
d00f 1a55a134c7 system: pick the management VLAN from System Settings
`vlan <id> mgmt` existed on the CLI, but nothing reported which VLAN
currently carries management, so the setting was invisible from the web UI -
the only way to find out was to read the startup config.

Report it in information.json and offer it where the other switch-wide
addressing settings live, between Gateway and Language, as a picker filled
from the configured VLANs. When management is untagged there is no VLAN to
select, so that state shows as a disabled entry rather than inventing an id
the switch would reject.

Confirm before applying, and say what will happen rather than echoing the
action back: the switch starts tagging its own frames, and if the port you
came in on does not carry that VLAN the page becomes unreachable and the way
back is the console. Cancelling puts the picker back where it was.
2026-08-08 18:26:49 +02:00
d00f 23f0ba995c css: align read-only values with the input fields
The System Settings rows all pair a label with a form control, except Model,
which is a bare span. The inputs carry padding and a left margin, so their
text starts about 32px further in than the model name did - close enough to
look like a mistake and far enough to see it.

Give read-only values a class with the same metrics instead of styling the
model specifically; any other value shown without a control gets the
alignment for free.
2026-08-08 18:26:15 +02:00
d00f 22a09bd06a l2: drop the comments and define the column list once
Review feedback on #307: these files are served from flash byte for byte,
so comments ride along on every page load. The three added in this branch
are gone, 357 bytes across l2.js and style.css. The column list existed in
three copies inside renderL2() and is now a single const.
2026-08-08 17:55:27 +02:00
d00f 930a08ec52 cmd: validate the port argument of mtu and pvid
Both handlers turned the first character of the port word into an index
with no check at all. "mtu 0 100" computes '0' - '1' = 255, reads far past
the end of phys_to_log_port[9], and writes the size to whatever register
0x1250 plus that garbage points at. "pvid 0 2" walks the same path into
port_pvid_set(). lag and vlan already validate their port arguments; these
two just did not.

The port now has to be a single digit 1 to 9, which is exactly the range
the mapping table holds. A second digit or a stray letter falls to the
usage message.

"mtu show" also gained the return it was missing: after printing the table
it fell through, derived a port from the word "show" and printed the
garbage byte before the length check stopped it.
2026-08-08 17:46:24 +02:00
d00f 1701d4dc53 cmd: refuse a management VLAN that cannot exist
"vlan 5000 mgmt" parked the management interface on a VLAN the table
cannot hold, which quietly cuts management off. IDs above 4094 now fall
through to the usage message. 0 still switches the management VLAN off,
which is the documented way to disable it.
2026-08-08 17:38:10 +02:00
d00f 1e19a9abe2 cmd: bound the PVID and say something when it is refused
"pvid 1 5000" packed 5000 into the 12-bit PVID field and truncated on the
way, so the port ended up with a PVID nobody chose. 0 and anything above
4094 are refused now, matching what the VLAN table can hold. A failed
parse used to be dropped without a word; both cases print the usage line.
2026-08-08 17:38:10 +02:00
d00f 1098e73337 cmd: stop "mtu" from acting on a value it failed to parse
The handler threw away atoi_short()'s return and leant on the range test
alone. The range test cannot tell a failed parse from a small number, so
"mtu 1 99999" stopped at the partial 9999 and went through as a number
nobody typed. With the parse result checked, a failure is rejected with
the same message as an out-of-range value.
2026-08-08 17:38:10 +02:00
d00f 99b0fc4b32 cmd: give mtu a lower bound as well as an upper one
"mtu 1 0" was accepted. The chip takes it verbatim, the port reports 0x0000
back, and it stops passing frames: on a live 2.5G LAG member the LACPDU
receives moved by 4 in fifteen seconds against 21 on the sibling port, and
the partner went expired. Restoring the size brought both back.

Nothing shorter than a minimum Ethernet frame is a usable maximum, so the
range is now 64 to 16383. The upper end is unchanged and still comes from
the width of the field the value is written into.

This also covers most of #312 by accident: "mtu 1 abc" leaves the parse
result at 0 and now gets rejected on the bound rather than reaching the
register. It does not cover all of it. The handler still ignores what
atoi_short() returns, so "mtu 1 99999" stops on a partial 9999 and goes
through as a number nobody typed.

The GUI is not affected either way, it offers a fixed list of sizes.
2026-08-08 17:30:25 +02:00
d00f 10d472d8a6 vlan: reject VLAN IDs the table cannot hold
vlan_create() and vlan_delete() wrote the ID straight into the table index
register. vlan_get() has refused anything >= 0xfff for a while, so reads were
guarded and writes were not: "vlan 4095 1 2" built an entry that no read path
can see, and IDs above that either miss the table or alias onto another VLAN.

Both writers now enforce the range vlan_get() already did, and parse_vlan()
rejects the same values with the usage message so the CLI says why. The check
sits after the "vlan 0 mgmt" branch, which legitimately takes 0 to switch the
management VLAN off.

Costs nothing in RAM: rtl837x_port.rel stays at DSEG 0, OSEG 5, and the image
still reports 10207 bytes of XDATA in use.
2026-08-08 17:13:28 +02:00
d00f 6254f44001 cmd: reject out-of-range numeric arguments instead of wrapping
atoi_short() accumulated into a uint16_t without checking, so "vlan 65540"
wrapped to 4 and edited VLAN 4 instead of failing. atoi_byte() had the same
hole with "300" landing on 44. Both now refuse the digit that would push the
value past its type, before it lands.

The partial result is deliberately left alone rather than zeroed. Zeroing
would give the function one tidy rule, every failure leaves 0, but a caller
that ignores the return would then write that 0, and 0 is not a harmless
number everywhere. Set as a port MTU it stops the port taking frames: I put
0 on a live 2.5G LAG member and its LACPDU receives moved by 4 in fifteen
seconds against 21 on the sibling port, with the partner going expired.
Putting the size back recovered both. A wrong number does less damage than
that, and the real fix belongs in the callers that ignore the return anyway.

The test sits inside the loop rather than after it, so no wider accumulator
is needed and the parser stays off the internal RAM budget.

056a30a on the branch in #303 fixes atoi_byte a different way, by widening
the accumulator. Whichever lands first, the other hunk should go.
2026-08-08 17:13:08 +02:00
René van Dorst c435838376 Merge pull request #308 from plaes/horaco-zx-sg4t2
docs: Add Horaco XZ-SG4T2 skeleton documentation and images
2026-08-08 12:08:27 +00:00
René van Dorst 3f7db7e430 Merge pull request #297 from DrDoof/hostname
system: configurable hostname + show model on the System page
2026-08-07 18:10:34 +00:00
René van Dorst f98fe32d0c Merge pull request #309 from plaes/http-security-fixes
httpd: Fix buffer offerflow in scan_header
2026-08-07 10:08:11 +00:00
Priit Laes d2e8ee0e41 httpd: Fix buffer offerflow in scan_header
Fixes #300
2026-08-06 15:24:09 +03:00
Priit Laes a9a455e447 docs: Add Puya P25D40SH flash chip to list of known supported 2026-08-06 10:57:49 +03:00
Priit Laes 3f26bf7348 docs: Use relative paths 2026-08-06 10:57:49 +03:00
Priit Laes ac302d4a67 Add barebones docs for already supported Horaco ZX-SG4T2 / WTG024AS-A-V2.0.1_4C_2SFP 2026-08-06 10:47:35 +03:00
Priit Laes 409bc58e17 docs: Add HC-SWTGW215AS to supported devices index 2026-08-06 10:13:29 +03:00
Priit Laes fbcec2cb5a docs: Fix capitalization/typos in company names 2026-08-06 10:06:24 +03:00
d00f ccb90ed16f l2: refresh the type column when a row is reused
The paint loop reuses existing rows and rewrites port, MAC, VLAN and the
delete button, but never the type cell - only the insert path set it. With a
fixed row order that stayed invisible, since a row usually landed back where
it was. Sorting moves rows, so every other column followed the data while
type kept the previous row's value, which made sorting by type look broken
when the sort itself was correct.
2026-08-05 21:42:54 +02:00
d00f 4389cfcd8b l2: show which column sorts and in which direction
Clicking a heading sorted the table but nothing said so afterwards - the
only feedback was the rows moving, which is no help when the sort key is a
column you are not looking at.

Give every sortable heading a permanent marker: a neutral double arrow when
it is not the sort key, up or down when it is. The marker sits in its own
span so the i18n pass, which replaces the heading text, does not wipe it.
2026-08-05 21:35:54 +02:00
d00f 5e901fb5c2 l2: sort and filter the forwarding table from its header
The table lists every learned and static entry in one flat block, which is
fine with a handful and unusable with a few hundred: finding out where one
MAC sits, or what a port has learned, meant reading the whole thing.

Make the column headings sort and give each one a filter box. Filters are
substring matches combined with AND, so "port 8 + static" is two keystrokes.
A counter above the table shows matched out of total, so a filter that hides
everything is obvious rather than looking like an empty table.

Sorting keeps the CPU entry from comparing as a number - it sorts last
instead of landing between ports 8 and 9, where a string-vs-number compare
would otherwise put it.

The filter inputs live inside the existing header cells rather than in a
second row: the paint loop addresses data rows as rows[i+1], and a second
header row would have shifted every one of them.
2026-08-05 21:21:03 +02:00
d00f 2974e4b66a l2: stop labelling the SFP port as CPU
The port column was mapped from logical to physical numbering first and the
CPU label applied afterwards, testing for port 9. On an 8+1 board the SFP
port maps to physical 9 as well, so every entry learned on the SFP was shown
as CPU - on this switch that was 26 of 30 entries.

Label the CPU while the number is still logical, before the mapping, so the
two cannot collide.
2026-08-05 21:21:03 +02:00
d00f 108ac9b8fc system: derive the default hostname after the startup config
Move the MAC-derived default name out of main() into its own function and
call it after execute_config(), returning early when the config already
set a name - a configured switch then does no work for it at all.

The body deliberately has no local variables. Locals here - counters and
pointers alike - land in the 8051's internal-RAM overlay, and on an image
with LACP and STP both enabled that overlay is exhausted: a loop makes the
linker fail with "Could not get 8 consecutive bytes in internal RAM for
area OSEG". Moving the code into its own function does not help, since the
overlay is shared across the whole image, and hoisting the locals to xdata
does not either, because itohex() is inline and brings its own frame. This
only shows up in an integrated build; the branch on its own links fine.

Suggested-by: vDorst
2026-08-05 16:48:36 +02:00
René van Dorst de3eca26c3 Merge pull request #301 from zytstudio/MACHINE_FG_4GT_2SX_V2_0
Add FG-4GT-2SX_V2.0
2026-08-05 08:53:02 +00:00
ZYT 8164166465 Fix mistake in comments 2026-08-05 11:15:48 +08:00
ZYT 87fe1e77ae Merge branch 'main' into MACHINE_FG_4GT_2SX_V2_0 2026-08-05 11:09:53 +08:00
ZYT a753f6f02e Add led colors and remove an unnecessary photo 2026-08-05 11:07:51 +08:00
René van Dorst c86d4b31f2 Merge pull request #304 from plaes/makefile
makefile: Make CC and ASM overridable via command line
2026-08-04 19:50:31 +00:00
Priit Laes 3c89b22207 makefile: Use origin check for CC variable
Make defines defaults for commonly used variables, therefore
we need to check first how the variable was defined. If `default`
value was used, override it with our own default.
2026-08-04 21:55:28 +03:00
Priit Laes bebe79c4c9 makefile: Fix possible shell injections in makefile 2026-08-04 18:53:23 +03:00
Priit Laes adb0b54691 makefile: Reorder sources to immediately fail on invalid MACHINE
Also group and reorder source files and move each of these to
separate line.
2026-08-04 17:57:53 +03:00
Priit Laes b99436543a makefile: Make CC and ASM overridable via command line
This makes it easier to run make without docker within
distros without editing Makefile. For example Fedora:
`ASM=sdcc-sdas8051 CC=sdcc-sdcc make`
2026-08-04 13:17:53 +03:00
d00f 1194823cd5 system: drop the defensive checks around the System page fields
The hostname and model fields were populated through element-existence
checks and empty-string fallbacks that the other fields on the page do
without. Assign them the same way.

Suggested-by: vDorst
2026-08-04 05:43:05 +02:00
d00f 94716ceb5d system: derive the default hostname from the MAC address
Every switch came up as "RTLPlayground", so several of them on one
network were indistinguishable until someone configured a name. Append
the last three MAC octets (e.g. RTLPlayground-1ef924) - unique in
practice, still recognisable, and any "hostname ..." line in the startup
config overrides it as before.

Suggested-by: plaes
2026-08-04 05:43:05 +02:00
d00f 13120127df cmd: make "hostname" report the name and reject stray arguments
Typing "hostname" on its own cleared the name: with no argument the copy
loop never ran and the terminating NUL landed at index 0. Report the
current name instead, accept exactly one argument to set it, and reject
anything longer - a name with spaces tokenizes into several words, and
silently keeping only the first one is worse than an error. Walk the
buffer with a pointer, which the compiler codes better than indexing.

Suggested-by: vDorst
2026-08-04 05:43:05 +02:00
ZYT 46037cd54d Add FG-4GT-2SX_V2.0 2026-08-02 06:46:52 +08:00
d00f e6cd362a1e system: show the hardware model on the System page
machine.machine_name is already reported in /information.json as hw_ver;
surface it read-only on the System Settings page so the exact build
target is visible in the UI, without adding a redundant JSON field.
2026-07-25 13:21:13 +02:00
d00f f21b3a32bd system: configurable hostname (device identity)
Add a device hostname settable from the CLI (`hostname <text>`) and the
System Settings page. The value is sanitized on ingest to JSON-safe
printable ASCII (<=23 chars), stored in a shared __xdata buffer, seeded
to "RTLPlayground" at boot, persisted through the startup-config, and
reported in /information.json. It lives in the common header so other
modules can advertise it (LLDP uses it as the System Name TLV).
2026-07-25 13:07:09 +02:00
logicog 35941dd19f Merge pull request #291 from DrDoof/fix/webui-login-cookies
httpd: fix browser login (cookie parsed by name, Connection: close, self-contained login page)
2026-07-25 11:09:19 +02:00
d00f 0f257d38df httpd: shorten comments per review
Keep only the non-obvious bits; the rationale for each change already
lives in the respective commit messages.
2026-07-25 10:39:32 +02:00
logicog 912e68d6eb Merge pull request #295 from sempr/main
Fix rate range for SDS_10GR return value
2026-07-25 08:21:53 +02:00
Sempr c473646389 Fix rate range for SDS_10GR return value
Broaden range of SFP+ rates mapped to SDS_10GR
A commonly used SFP+ form-factor 10GBASE-LR (Hisense LTF1303-BH+) reports 0x62(	BR Nominal: 9800MBd) at
power on and then switches to 0x64 after booting up.

0x62 -> 9.8gbps
0x63 -> 9.9gbps
0x64 -> 10gbps
0x6f -> 11.1gbps
0x70 -> 11.2gbps

These are outside of the previous 0x63-0x6f range which prevented the link from ever being brought up. So I changed the range to 0x62-0x6f
2026-07-24 16:02:20 +08:00
logicog 51cf40d040 Merge pull request #290 from eraiza0816/feat/dockerfile
add: docker use prerequisites
2026-07-24 07:58:12 +02:00
d00f 3fd9cdfa7e httpd: parse the session cookie by name, not fixed offset
Root cause of the "browser login always bounces back with Wrong password!
while curl works": scan_header() read the session id from a fixed offset
into the Cookie header (p + 17), assuming "session=" is the first and only
cookie. Browsers keep stale cookies for a long time - e.g. an "admin" cookie
left over from this switch's VENDOR firmware - so the header can arrive as
"Cookie: admin=..; session=..", the fixed offset then points into the admin
value, authentication silently fails and every page bounces to login although
the password had been accepted. curl sends only "session=", which is why
command-line tests passed while a real browser (with that stale cookie) failed.

- scan_header(): scan the Cookie header for the actual "session=" key
  (matched as "session" - is_word() requires a separator after the pattern
  and '=' is on its list, the first value byte is not).
- is_word_x(): accept ';' as a terminating separator so the session value
  also matches when it is not the last cookie in the header.

Verified on hardware end-to-end in a real browser WITH the stale "admin"
cookie present: login -> index.html, all pages and JSON endpoints work.
2026-07-21 06:48:18 +02:00
d00f 3189820ba0 httpd: send Connection: close (single-connection uIP mitigation)
Real root cause of the "web login fails from a browser but works from curl":
the uIP httpd is built with UIP_CONF_MAX_CONNECTIONS = 1 and uses global
response state (outbuf/slen/session), i.e. it serves exactly one TCP connection
at a time and closes it after each response - but never advertises that via
the Connection header. A browser's HTTP/1.1 client therefore assumes the
connection may be persistent and can park it in its keep-alive pool for reuse;
a later request sent on that pooled connection hits one the server has already
closed, and a POST (unlike a GET) is never retried by the browser, so it can
be silently lost this way.

This adds "Connection: close" to every response so the browser does not pool
and reuse a connection the server is about to drop. On its own this did not
fully explain the reported login failures - the actual authentication bug is
fixed in the next commit (the Cookie header parsed at a fixed offset) - but it
is correct behaviour for a server that only ever handles one connection, and
removes one source of dropped requests.
2026-07-21 06:47:44 +02:00
d00f ed74ec1e97 httpd/login: complete CSP + password autocomplete hint
Two hygiene fixes for the web UI, prompted by a login that appeared to fail
under privacy shields (Brave Shields / NoScript-family extensions):

- httpd: replace the partial "style-src 'self' 'unsafe-inline'" CSP with a
  complete, first-party policy (default-src 'self'; script-src 'self'
  'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
  connect-src 'self'; form-action 'self'). Everything the UI needs is
  same-origin; the explicit policy stops shields injecting their own
  restrictive report-only probes (the noisy script-src-elem 'none' console
  spam) and passes a strict-CSP audit. Verified: no CSP violations in-browser.
- login.html: add autocomplete="current-password" so password managers
  recognise the field (they showed "unknown password" without it).

NOTE: these do NOT bypass a browser's LAN-device protection (NoScript "lan" /
Brave Shields), which strips the POST body of requests to a LAN address and is
why the login can fail in-browser while the same credentials work over curl.
That is a deliberate browser security feature; the user must allow the site in
their shields to log in. The backend password (default 1234) is unchanged and
correct.
2026-07-21 06:47:10 +02:00
Your Name dd95a05487 add: docker use prerequisites 2026-07-19 19:49:30 +09:00
logicog 481c02c740 Merge pull request #289 from eraiza0816/fix/httpd-sim-const-warning
fix: httpd-sim-const-warning
2026-07-19 09:49:08 +02:00
Your Name 0a31a65477 fix: httpd_sim 2026-07-19 16:44:21 +09:00
Lynn-Becky 2d176b7ccc add SWTG024AS-A-V2.0.1 with 5 RJ45 port (#282)
* add MACHINE_SWTG024AS_A_2_0_1_5_RJ45

* fix(machine): update SFP port LED color descriptions for PCB-SWTG024AS V2.0

* add(doc): create a simple documentation for SWTG024AS-A-V2.0.1_5_RJ45

* refactor(machine): update machine definition
2026-07-18 06:20:47 +02:00
logicog a1fac63710 Merge pull request #286 from eraiza0816/feature/language-settings
add language selection to system settings
2026-07-18 06:18:33 +02:00
logicog 5c7f82e2fe Merge pull request #285 from eraiza0816/feat/dockerfile
feat: add Dockerfile for development environment
2026-07-18 05:58:10 +02:00
Your Name e765d17c2b feat: add language selection to system settings
Add a language selector to the System Settings page supporting English,
Japanese, and Chinese. The i18n dictionary already contained zh
translations but lacked the UI to switch languages.

Changes:
- html/i18n.js: add sys_language key to en/ja/zh
- html/system.html: add language selector dropdown
- html/system.js: add changeLang() and selector initialization
- tools/httpd_sim.c: fix cookie parsing for multi-cookie headers;
  fix Set-Cookie response (missing Path=/, missing \r\n\r\n);
  add SO_REUSEADDR for faster port reuse
2026-07-15 01:35:10 +09:00
Your Name 8fb9afb955 feat: add Dockerfile for development environment
- Debian 13 (trixie) based with sdcc 4.5.0 from apt
- Includes gcc, make, xxd, python3, libjson-c-dev, golang-go
- Add .dockerignore to exclude build artifacts
- Add Docker usage section to README (collapsible)
2026-07-14 02:10:55 +09:00
logicog e96780fadb Merge pull request #280 from tofurky/ddm_broken_module
Add SFP quirk for devices that misreport DDM capability
2026-07-13 09:57:35 +02:00
Matt Merhar 2221f0fa32 Add SFP quirk for devices that misreport DDM capability
On a QSFPTEK QT-SFP+-T (RTL8261C) 10GBase-T module, the diag type field
(92) comes back as 0x00, but there's actually some statistics available
like temperature. Other metrics may be hard-coded values.

Add a basic struct that allows matching on vendor and/or model, using a
bitfield to allow multiple quirks for a given SFP module. Only
SFP_QUIRK_DDM is implemented.

For modules matching SFP_QUIRK_DDM, attempt an I2C read of the MSB of
module voltage during probe if DDM is "unsupported" - if it's not 0xff,
override the reported options so we can pull the diagnostic data.

To allow simpler comparisons, convert the ASCII fields (vendor,
model, serial) from space-padded to standard NULL-terminated strings.

strcmp() is moved from httpd.c to rtlplayground.c alongside other string
functions and shared between them.

The __reentrant keyword is used for the new functions to avoid using up
additional OSEG space. This allocates the variables on the stack, which
is OK for this particular code path.

The JSON assembly in send_status() is slightly modified to treat the
sfp_module_* data as standard NULL-terminated strings, and a repeated
subtraction was moved into a uint8_t to declutter the code.
2026-07-13 01:31:29 -04:00
logicog f86b8f32de Merge pull request #283 from Lynn-Becky/translate/chinese
Translate to Chinese
2026-07-13 07:15:52 +02:00
logicog 9389db9e9b Merge pull request #284 from eraiza0816/feature-translate
fix: rename local variable to avoid shadowing global translation
2026-07-13 07:15:29 +02:00
Your Name 519c3a8df3 fix: rename local variable to avoid shadowing global translation function 2026-07-10 13:09:41 +09:00
Lynn-Becky 64422367d5 feat(translate):translate to Chinese 2026-07-08 20:40:00 +08:00
logicog dc2b03e331 Merge pull request #279 from eraiza0816/translate-japanese
Support i18n & translate japanese
2026-07-07 19:28:19 +02:00
Your Name fcf1b3c310 Fix: Remove the language-specific display logic. 2026-07-05 02:31:37 +09:00
Your Name 1dd6156a1f add: how to support i18n 2026-07-05 01:09:50 +09:00
Your Name e75a4c2a95 support i18n & translate japanese 2026-07-05 00:51:27 +09:00
René van Dorst 05d8c36afe Merge pull request #278 from eraiza0816/update-docs-SWTG024AS
update document: SKS3200M-4GPY2XF flash size
2026-07-04 12:47:04 +00:00
René van Dorst b6c9eaf00f Merge pull request #275 from riker77/mokerlink-2G040210GSM
Add Mokerlink 2G040210GSM (2M-PCB43-V1.1)
2026-07-04 12:45:47 +00:00
Your Name ecf5f47960 update SKS3200M-4GPY2XF flash size 2026-07-04 15:52:25 +09:00
donbernhardo 457f117216 Add PCB43 V1.1 and symmetric KP-9000 6XH targets 2026-06-29 20:59:28 +02:00
Ron ebf6ed7526 Update 2M-PCB43-V1.1.md
Add hint to use KP_9000_6XHML_X2 as a compatible machine definition.
2026-06-29 20:30:07 +02:00
riker77 96f2c860fc Add Mokerlink 2G040210GSM (2M-PCB43-V1.1) 2026-06-29 17:57:09 +02:00
donbernhardo dd52e8e85e Discard incorrect KP-9000 V1.2 LED mux override 2026-06-29 14:12:31 +02:00
donbernhardo ecf8c999b1 Merge main into KP-9000 PCB revision split 2026-06-29 14:00:22 +02:00
donbernhardo 64a1d252cd Split KP-9000 6XH targets by PCB revision 2026-06-29 13:54:47 +02:00
feelfree69 b80d46955c Merge pull request #274 from Lynn-Becky/feat/PCB-SWTG024AS-V2.0
fix(machine): move SWTG024AS-specific SDS0 setup to machine.c
2026-06-28 21:32:53 +02:00
Lynn-Becky 0a18c64dcc fix(machine): move SWTG024AS-specific SDS0 setup to machine.c 2026-06-28 22:47:33 +08:00
René van Dorst b53c56e7f6 Merge pull request #273 from Lynn-Becky/feat/PCB-SWTG024AS-V2.0
fix(machine): SWTG024AS V2.0 fix 5th RJ45 port
2026-06-27 19:50:05 +00:00
Lynn-Becky 17d41b53b7 update doc 2026-06-28 00:56:50 +08:00
Lynn-Becky bd98cb0312 fix(machine): SWTG024AS V2.0 fix 5th RJ45 port 2026-06-28 00:48:05 +08:00
René van Dorst 5566d71522 Merge pull request #272 from Lynn-Becky/feat/PCB-SWTG024AS-V2.0
fix(SWTG024AS-V2.0): correct SFP port GPIO assignments
2026-06-27 13:33:29 +00:00
Lynn-Becky 5e1396cc42 fix(machine): correct SFP port GPIO assignments 2026-06-27 21:16:47 +08:00
René van Dorst 7fe0678c92 Merge pull request #271 from th0m4sek/main
Add reset button gpio to SWTG018AS-A V2.0
2026-06-25 18:33:18 +00:00
th0m4sek 7ec25a39a2 Add brands information for SWTG018AS-A V2.0
Added brands section with details for Ampcom and Horaco.
2026-06-25 20:23:54 +02:00
th0m4sek 98e3cde2f5 Update reset_pin on SWTG018AS-A V2.0 2026-06-25 20:16:56 +02:00
René van Dorst d90c02f951 Merge pull request #268 from th0m4sek/main
Table with supported devices
2026-06-23 17:40:51 +00:00
th0m4sek bbb92c86db Fix formatting issue in supported_devices.md 2026-06-23 09:16:05 +02:00
René van Dorst 1635236a05 Merge pull request #266 from hlyi/pr-tool
Tools for detecting GPIO mapping and extracting LED configure
2026-06-23 06:28:38 +00:00
th0m4sek 5e3a280b5c Update supported devices list with new entries 2026-06-23 07:26:35 +02:00
HL Yi a56e2a419b sleep-interval default value is reduced to 1.
i2c-bus is required argument
replace --bus with --i2c-bus for i2c_dump_rtl_regs.py for better consistency
format lint clean up
2026-06-22 18:02:34 -05:00
th0m4sek 9c01227572 Fix typo in device manufacturer name from 'Haraco' to 'Horaco' 2026-06-22 23:21:13 +02:00
th0m4sek 6d37a47481 Rename Horaco SWTGW215AS to HC-SWTGW218AS 2026-06-22 23:18:42 +02:00
th0m4sek 87b9e59cdb Update ZX-SWTG124AS entry with new data 2026-06-22 22:47:02 +02:00
th0m4sek 0e86b84efa Add Ports column to supported devices list
Updated the supported devices table to include Ports column and adjusted device details.
2026-06-22 22:45:20 +02:00
René van Dorst bd595b7f94 Merge pull request #263 from Lynn-Becky/feat/PCB-SWTG024AS-V2.0
Add PCB-SWTG024AS V2.0 (Unmanaged)
2026-06-22 19:27:38 +00:00
Lynn-Becky 06d4a3504a Add LED status comments,try to fix 5th rj45 2026-06-23 00:42:31 +08:00
Lynn-Becky d68534e833 update PCB-SWTG024AS-V2.0 to SWTG024AS-V2.0 2026-06-22 23:51:57 +08:00
Lynn-Becky e0356bd751 Add SWTG024AS-V2.0 doc and photo 2026-06-22 22:24:46 +08:00
HL Yi 4b770a8abc Fixed document based upon vDorst
Expand ignore IO list to the GPIOs related to SYS_LED, UART, SMI, and SPI
2026-06-22 07:08:19 -05:00
th0m4sek 709436ca13 Update supported devices list
Added new device 'Keeplink KP-9000-6XH-X2' and removed duplicate 'Horaco SWTGW215AS' entry.
2026-06-22 12:08:52 +02:00
th0m4sek 28856ef495 Revise supported devices documentation
Updated the supported devices list with new entries and details.
2026-06-22 12:07:30 +02:00
th0m4sek 113bb654aa Add links to PCB documentation for supported devices
Updated device entries with links to detailed PCB documentation.
2026-06-22 11:14:04 +02:00
th0m4sek efc1883685 Table with supported devices
Added a table listing supported hardware devices with details.
2026-06-22 10:38:40 +02:00
feelfree69 72921ba3be Merge pull request #267 from hlyi/pr-doc
add SKS3200-8E1X to SWTGW218AS.md doc
2026-06-22 08:00:46 +02:00
HL Yi 70be673fca updates based upon vDorst's feedback.
change i2c_read_rtl_gpio sleep tiemr to 1s
update README.md
Sorted port speed
2026-06-22 00:18:32 -05:00
HL Yi bf5205258e add SKS3200-8E1X to SWTGW218AS.md doc 2026-06-21 17:24:02 -05:00
René van Dorst 98ddb0bb29 Merge pull request #253 from Erdnusschokolade/fix/vlan-mgmt-filter
Fix conf_overwrite filter removing vlan mgmt entries
2026-06-21 18:52:59 +00:00
HL Yi d152f89549 remove redundant code 2026-06-21 13:22:54 -05:00
HL Yi c61e94527e add clarification for firmware requirement 2026-06-21 13:21:06 -05:00
HL Yi f76ccaf3d8 add disclaim that tool tested in linux 2026-06-21 13:08:58 -05:00
HL Yi ebc6cd06ad add utilities for led and gpio dump and detection 2026-06-21 13:04:04 -05:00
Lynn-Becky 017aaf71d6 Add PCB-SWTG024AS V2.0 machine target 2026-06-21 01:15:34 +08:00
Erdnusschokolade 4eaf16f96d Drop dead exact-match check in vlan delete filter
A bare "vlan N" line never matches conf_cmds (a VLAN entry always carries
a member port or the mgmt/name keyword), so it is never stored. The
c !== "vlan N" comparison could therefore never match an entry; the
startsWith(prefix) check alone covers all stored vlan lines.
2026-06-20 08:13:43 +02:00
Erdnusschokolade 718745e7b5 Reduce stale management VLAN entries on config save
The firmware has a single management VLAN (one management_vlan variable),
so a stored config should never carry more than one "vlan N mgmt" line.
When parsing a mgmt command, drop any previously stored mgmt entry so
repeatedly changing the management VLAN no longer accumulates stale lines.
2026-06-20 08:00:54 +02:00
Erdnusschokolade 06ee1ce38e Persist port speed settings in config parser
parseConf() only recognized "port N name" in conf_cmds, so "port N <speed>"
lines were treated as unknown commands and dropped on save - configured port
speeds never persisted.

Add the speed command (incl. optional half/full duplex suffix) to conf_cmds,
add a "port N" entry to conf_overwrite so re-saving a speed replaces the
previous value, and guard the per-port name entry so changing a port's speed
no longer wipes its configured name.
2026-06-18 14:49:56 +02:00
donbernhardo d3e8698e40 Fix KP-9000-6XHML-X2 LED mux mapping 2026-06-17 21:16:21 +02:00
René van Dorst f122402bdd Merge pull request #259 from logicog/rtl8221b_100m_support
Add support for 100M on RTL8221B ports
2026-06-15 17:54:03 +00:00
logicog 842d68af97 Add support for 100M on RTL8221B ports 2026-06-15 18:40:03 +02:00
logicog bfd1ce9183 Merge pull request #209 from hlyi/pr
Add Steamemo IG204 V1 support
2026-06-12 17:35:17 +02:00
feelfree69 4ed6575a87 Merge branch 'main' into pr 2026-06-10 08:40:35 +02:00
feelfree69 1b1c656404 Merge pull request #215 from logicog/2x10g
Add  ZX310S-4T2XT device support
2026-06-10 08:21:48 +02:00
bennydiamond 7a7ffd3bf8 Automatically print newline on serial interface
For some print_string
2026-06-07 14:52:56 -04:00
HL Yi e7f70a724b add doc for STEAMEMO_IG204_V1 2026-06-05 20:05:26 -05:00
HL Yi d7a7bdac7e add support of STEAMEMO_IG204_V1 2026-06-05 18:32:22 -05:00
Erdnusschokolade 592b903305 Fix conf_overwrite filter removing vlan mgmt entries
The vlan/mgmt lookahead split (commit dcc60c3) correctly separated the
patterns so that `vlan N mgmt` and `vlan N <members>` match different
conf_overwrite entries. But the filter that applies the overwrite still
used `item.startsWith(matchStr + " ")` with matchStr = "vlan N", which
also matches "vlan N mgmt" — so the management entry was removed whenever
the VLAN's membership definition was re-saved.

Example: with both `vlan 44 management 2t 4t 5 6t` and `vlan 44 mgmt`
in the config, changing the membership dropped `vlan 44 mgmt` entirely.

Added a `!item.endsWith(" mgmt")` guard so management entries survive the
filter. No reordering is needed: `vlan N mgmt` only sets the global
management_vlan variable (cmd_parser.c) and does not depend on the VLAN
table entry existing, so its position in the config is irrelevant.
2026-06-02 22:52:06 +02:00
105 changed files with 5792 additions and 1258 deletions
+19
View File
@@ -0,0 +1,19 @@
.git/
.gitignore
.gitattributes
.github/
output/
installer/output/
*.bin
html_data.c
html_data.h
*.o
*.rel
*.lst
*.sym
*.asm
*.ihx
*.img
*.map
*.mem
*.lk
+19
View File
@@ -0,0 +1,19 @@
FROM debian:13-slim
RUN apt-get update && apt-get install -y \
make \
gcc \
sdcc \
xxd \
python3 \
libjson-c-dev \
golang-go \
git \
&& rm -rf /var/lib/apt/lists/*
# git safe.directory for mounted repos (Makefile uses git describe)
RUN git config --global --add safe.directory /workspace
WORKDIR /workspace
CMD ["bash"]
+70 -23
View File
@@ -4,9 +4,11 @@ DEFAULT_CONFIG_LOCATION = 454656
CONFIG_LOCATION = 458752
HTML_LOCATION = 262144
ifeq ($(origin CC),default)
CC = sdcc
endif
CC_FLAGS = -mmcs51 -I. -Ihttpd -Iuip
ASM = sdas8051
ASM ?= sdas8051
AFLAGS= -plosgff
SUBDIRS := tools
@@ -30,58 +32,102 @@ endif
VERSION_EXTENSION = v$(VERSION)-$(GIT_VERSION)
FILENAME_EXTENSION = $(VERSION_EXTENSION)-$(MACHINE)
# Deterministic build date: honor SOURCE_DATE_EPOCH, else the HEAD commit date,
# else wall-clock (no-git fallback). Keeps same-commit builds byte-identical
# (BUILD_DATE is baked into the image and covered by the trailing CRC).
SOURCE_DATE_EPOCH ?= $(shell git show -s --format=%ct HEAD 2>/dev/null)
ifeq ($(SOURCE_DATE_EPOCH),)
BUILD_DATE := $(shell date +"%Y-%m-%d %H:%M:%S")
else
BUILD_DATE := $(shell date -u -d @$(SOURCE_DATE_EPOCH) +"%Y-%m-%d %H:%M:%S" 2>/dev/null \
|| date -u -r $(SOURCE_DATE_EPOCH) +"%Y-%m-%d %H:%M:%S")
endif
all: create_build_dir $(VERSION_HEADER) $(SUBDIRS) $(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin
create_build_dir:
mkdir -p $(BUILDDIR)
mkdir -p $(BUILDDIR)/uip
mkdir -p $(BUILDDIR)/httpd
mkdir -p "$(BUILDDIR)"
mkdir -p "$(BUILDDIR)/uip"
mkdir -p "$(BUILDDIR)/httpd"
# Keep machine.c in first position to fail immediately on invalid $MACHINE value
SRCS = \
machine.c \
machine_init.c \
cmd_editor.c \
cmd_parser.c \
dhcp.c \
html_data.c \
rtlplayground.c \
syslog.c \
udp_apps.c
# RTL837x
SRCS += \
rtl837x_bandwidth.c \
rtl837x_flash.c \
rtl837x_igmp.c \
rtl837x_init.c \
rtl837x_leds.c \
rtl837x_phy.c \
rtl837x_pins.c\
rtl837x_port.c \
rtl837x_stp.c
SRCS += \
httpd/httpd.c \
httpd/page_impl.c
SRCS += \
uip/timer.c \
uip/uip.c \
uip/uiplib.c \
uip/uip_arp.c \
uip/uip-fw.c \
uip/uip-neighbor.c \
uip/uip-split.c
SRCS = rtlplayground.c rtl837x_flash.c rtl837x_leds.c rtl837x_phy.c rtl837x_port.c cmd_parser.c html_data.c rtl837x_igmp.c
SRCS += rtl837x_stp.c rtl837x_pins.c dhcp.c machine.c cmd_editor.c rtl837x_bandwidth.c rtl837x_init.c syslog.c
SRCS += uip/timer.c uip/uip.c uip/uip_arp.c uip/uiplib.c uip/uip-fw.c uip/uip-neighbor.c uip/uip-split.c udp_apps.c
SRCS += httpd/httpd.c httpd/page_impl.c
OBJS = ${SRCS:%.c=$(BUILDDIR)/%.rel}
DEPS := ${SRCS:%.c=$(BUILDDIR)/%.d}
HTML := $(shell find $(html) -name '*.js' -or -name '*.html' -or -name '*.svg')
HTML := $(shell find html -name '*.js' -or -name '*.html' -or -name '*.svg')
html_data.c html_data.h: $(HTML) tools/output/fileadder
html_data.c html_data.h &: $(HTML) | tools
tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -d html -p html_data
$(VERSION_HEADER):
@echo "#ifndef VERSION_H" > $(VERSION_HEADER)
@echo "#define VERSION_H" >> $(VERSION_HEADER)
@echo "#define VERSION_SW \"$(VERSION_EXTENSION)\"" >> $(VERSION_HEADER)
@echo "#define BUILD_DATE \"$(shell date +"%Y-%m-%d %H:%M:%S")\"" >> $(VERSION_HEADER)
@echo "#endif" >> $(VERSION_HEADER)
@printf '%s\n' "#ifndef VERSION_H" "#define VERSION_H" \
"#define VERSION_SW \"$(VERSION_EXTENSION)\"" \
"#define BUILD_DATE \"$(BUILD_DATE)\"" \
"#endif" > $(VERSION_HEADER)
httpd: html_data.h
$(SUBDIRS):
$(MAKE) -C $@
clean:
clean: $(SUBDIRSCLEAN)
-rm -f html_data.c html_data.h $(VERSION_HEADER)
-if [ -d $(BUILDDIR) ]; then find $(BUILDDIR) -type f ! -name "*.bin" -delete; fi
distclean:
distclean: $(SUBDIRSCLEAN)
-rm -f html_data.c html_data.h $(VERSION_HEADER)
-rm -rf $(BUILDDIR)
$(BUILDDIR)/%.rel: %.c
$(SUBDIRSCLEAN):
$(MAKE) -C $(@:clean=) clean
$(BUILDDIR)/%.rel: %.c | create_build_dir html_data.h
$(CC) -MMD $(CC_FLAGS) -o $@ -c $<
$(BUILDDIR)/%.rel: %.asm
$(BUILDDIR)/%.rel: %.asm | create_build_dir
${ASM} ${AFLAGS} -o $@ $<
# mv -f $(addprefix $(basename $^), .lst .rel .sym) .
$(BUILDDIR)/rtlplayground.ihx: $(OBJS) $(BUILDDIR)/crtstart.rel $(BUILDDIR)/crc16.rel
$(BUILDDIR)/rtlplayground.ihx: $(OBJS) $(BUILDDIR)/crtbank.rel $(BUILDDIR)/crc16.rel
$(CC) $(CC_FLAGS) -Wl-bHOME=0x00000 -Wl-bBANK1=0x14000 -Wl-bBANK2=0x24000 -Wl-r -o $@ $^
$(BUILDDIR)/rtlplayground.img: $(BUILDDIR)/rtlplayground.ihx
objcopy --input-target=ihex -O binary $< $@
$(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.img
$(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.img | tools
if [ -e $@ ]; then rm $@; fi
tools/output/imagebuilder -i $^ $@
tools/output/fileadder -a $(DEFAULT_CONFIG_LOCATION) -s $(IMAGESIZE) -d config.txt $@
@@ -90,16 +136,17 @@ $(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.i
tools/output/crc_calculator -u $@
ln -sf $(MACHINE)/rtlplayground-$(FILENAME_EXTENSION).bin output/rtlplayground.bin
.PHONY: clean all $(SUBDIRS) $(VERSION_HEADER)
.PHONY: clean distclean all $(SUBDIRS) $(SUBDIRSCLEAN) $(VERSION_HEADER) create_build_dir
.PHONY:
machine_check:
@mkdir -p $(BUILDDIR)/tmp
@set -eo pipefail; \
for MACHINE in `grep -e ' MACHINE_' machine.c | sed -e 's%^.* MACHINE_%%' -e 's%[ ]*//.*$$%%' | sort -u`; \
for MACHINE in `grep -E '^[[:space:]]*(//[[:space:]]*)?#define MACHINE_' machine.h | sed -E 's%^[[:space:]]*(//[[:space:]]*)?#define MACHINE_%%' | awk '{print $$1}' | sort -u`; \
do \
echo "Checking $${MACHINE}"; \
$(CC) $(CC_FLAGS) -DMACHINE_$${MACHINE} -MMD -o $(BUILDDIR)/tmp/machine_check -c machine.c; \
$(CC) $(CC_FLAGS) -DMACHINE_$${MACHINE} -MMD -o $(BUILDDIR)/tmp/machine_check -c machine_init.c; \
done
@rm -rf $(BUILDDIR)/tmp
+51 -2
View File
@@ -28,8 +28,10 @@ only the following features are provided:
<img width="1420" height="623" alt="GUI" src="doc/images/gui.png" />
While the firmware provides already considerable improvements over the original managed firmware,
the firmware still lacks support for STP and the proprietary loop prevention
protocols as well as DHCP. If you need these features, do not install the playground on your managed
the firmware still lacks support for the proprietary loop prevention
protocols as well as DHCP. Spanning Tree is available (see doc/stp.md), but is
a simplified implementation - read that document before enabling it on a
switch you administer over the network. If you need these features, do not install the playground on your managed
devices. In any case, installation is strongly discouraged unless you can at least make
a backup of the original flash content via a SOIC clamp such as also used for BIOS
backups and can re-install that firmware in case something is wrong. For this no soldering
@@ -60,6 +62,53 @@ still has an older version of sdcc, but you will need sdcc version 4.5 for the c
sudo apt install make gcc sdcc xxd python-is-python3 libjson-c-dev
```
<details>
<summary>If using Docker (click to expand)</summary>
### Prerequisites
Install Docker for your platform:
- **Linux (Debian/Ubuntu)**: `sudo apt install docker.io` then `sudo usermod -aG docker $USER` (log out and back in)
- **Linux (other distros)**: Follow the [Docker Engine install guide](https://docs.docker.com/engine/install/)
- **Windows**: Install [Docker Desktop for Windows](https://docs.docker.com/desktop/setup/install/windows-install/)
- **macOS**: Install [Docker Desktop for Mac](https://docs.docker.com/desktop/setup/install/mac-install/)
### Usage
A Dockerfile is provided for a reproducible build environment:
```
docker build -t rtlplayground-dev .
```
Build the firmware (replace MACHINE with your target, e.g. `DEFAULT_8C_1SFP`):
```
docker run --rm -v $(pwd):/workspace rtlplayground-dev make MACHINE=DEFAULT_8C_1SFP
```
The resulting `.bin` file appears in `output/` on your host.
Build host tools only:
```
docker run --rm -v $(pwd):/workspace rtlplayground-dev make -C tools
```
Run the web-interface simulator locally:
```
docker run --rm -p 8080:8080 -v $(pwd):/workspace rtlplayground-dev \
tools/output/httpd_sim /workspace/html
```
Edit `machine.h` or `config.txt` on your host, then re-run `make` — the
source directory is mounted into the container, so changes take effect
immediately. To build for a different machine, pass `MACHINE=...`.
</details>
## (1) Compiling for direct chip flashing AND upgrading an existing RTLPlayground running device
Edit machine.h with an editor like vi or nano. Select the correct machine the firmware should build for.
+6 -3
View File
@@ -40,8 +40,10 @@ void cmd_edit(void) __banked
{
while (l != sbuf_ptr) {
if (sbuf[l] >= ' ' && sbuf[l] < 127) { // A printable character, copy to command line
if (cmd_line_len >= CMD_BUF_SIZE)
continue;
// Reserve one byte for the terminating NUL written on Enter. When the
// line is full, drop the character but still fall through to advance the
// serial-ring read pointer below; a 'continue' here would spin forever.
if (cmd_line_len < CMD_BUF_SIZE - 1) {
write_char(sbuf[l]);
// Shift buffer to right
for (uint8_t i = cmd_line_len; i > cursor; i--)
@@ -55,6 +57,7 @@ void cmd_edit(void) __banked
// Move backwards
for (uint8_t i = cursor; i < cmd_line_len; i++)
write_char('\010'); // BS works like cursor-left
}
} else if (sbuf[l] == '\033') { // ESC-Sequence
// Wait until we have at least 3 characters including the ESC character in the serial buffer
if (((sbuf_ptr + SBUF_SIZE - l) & SBUF_MASK) < 3)
@@ -190,7 +193,7 @@ void cmd_edit(void) __banked
// Check whether return was pressed:
if (sbuf[l] == '\n' || sbuf[l] == '\r') {
write_char('\n');
cmd_buffer[cmd_line_len] = '\0';
cmd_buffer[cmd_line_len] = NUL;
// write_char('>'); print_string_x(cmd_buffer); write_char('<');
// If there is a command we print the prompt after execution
// otherwise immediately because there is nothing to execute
+444 -292
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
.area HOME (CODE)
.area GSINIT0 (CODE)
.area GSINIT1 (CODE)
.area GSINIT2 (CODE)
.area GSINIT3 (CODE)
.area GSINIT4 (CODE)
.area GSINIT5 (CODE)
.area GSINIT (CODE)
.area GSFINAL (CODE)
.area CSEG (CODE)
.area HOME (CODE)
__sdcc_banked_call::
push _PSBANK
xch a,r0
push a
mov a,r1
push a
mov a,r2
anl a,#0x1f
mov _PSBANK, a
xch a, r0
ret
__sdcc_banked_ret::
pop _PSBANK
ret
-17
View File
@@ -1,17 +0,0 @@
.area GSFINAL (CODE)
__sdcc_banked_call::
push _PSBANK
xch a,r0
push a
mov a,r1
push a
mov a,r2
anl a,#0x1f
mov _PSBANK, a
xch a, r0
ret
__sdcc_banked_ret::
pop _PSBANK
ret
+23 -1
View File
@@ -41,6 +41,7 @@ __xdata uip_ipaddr_t server;
#define DHCP_REBIND_LEN 4
#define DHCP_CLIENT_ID 61
#define DHCP_CLIENT_ID_LEN 7
#define DHCP_HOSTNAME 12
#define DHCP_REQUEST_IP 50
#define DHCP_REQUEST_IP_LEN 4
#define DHCP_PARAMS 55
@@ -116,6 +117,20 @@ void dhcp_addopt_client_id(void)
}
void dhcp_addopt_hostname(void)
{
uint8_t len = 0;
while (hostname[len])
len++;
if (!len)
return;
DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_HOSTNAME;
DHCP_OPT[dhcp_state.opt_ptr++] = len;
memcpy(&DHCP_OPT[dhcp_state.opt_ptr], hostname, len);
dhcp_state.opt_ptr += len;
}
void dhcp_addopt_request_ip(void)
{
DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_REQUEST_IP;
@@ -152,6 +167,7 @@ void dhcp_send_discover(void)
dhcp_addopt_client_id();
dhcp_addopt_request_ip();
dhcp_addopt_hostname();
DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_PARAMS;
DHCP_OPT[dhcp_state.opt_ptr++] = 3;
@@ -188,6 +204,7 @@ void dhcp_send_request(void)
dhcp_addopt_client_id();
dhcp_addopt_request_ip();
dhcp_addopt_server_id();
dhcp_addopt_hostname();
DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_PARAMS;
DHCP_OPT[dhcp_state.opt_ptr++] = 3;
@@ -334,7 +351,12 @@ void dhcp_start(void) __banked
return;
}
get_random_32();
dhcp_state.transaction_id = SFR_DATA_U32;
// Workaround SDCC bug 4070: dhcp_state.transaction_id = SFR_DATA_U32;
__xdata uint8_t * tid = &dhcp_state.transaction_id;
*tid++ = SFR_DATA_24;
*tid++ = SFR_DATA_16;
*tid++ = SFR_DATA_8;
*tid = SFR_DATA_0;
dhcp_state.state = DHCP_START;
print_string("dhcp_start done\n");
}
+30
View File
@@ -64,3 +64,33 @@ Writing 0x1 to register 0x7850 will transmit the frame. The Ethernet frame
checksum and the TCP checksum are automatically calculated (offloaded) by the
ASIC before transmitting on the wire.
## The RTL tag words
The frame header uses the Realtek Remote Control Protocol (RRCP) format or
the like.
The `flags` word:
```
bit15 EFID_EN | 14:12 EFID | 11 PRI_EN | 10:8 PRI |
bit7 KEEP | 6 VSEL | 5 LEARN_DIS | 4:0 VIDX
```
All fields are in network byte order.
* `EFID_EN`, `EFID`: look the destination up under this filtering ID
instead of the port's own
* `PRI_EN`, `PRI`: force the given priority on the frame
* `KEEP`: keep the 802.1Q tagging of the frame exactly as injected,
bypassing the egress tagging rules of the port
* `VSEL`, `VIDX`: classify the frame into the VLAN at this index of the
VLAN table
* `LEARN_DIS`: do not learn the source address from this frame
The `pmask` word: bit 15 is `ALLOW`, bits 14 to 0 are a port mask.
* `ALLOW` clear: the mask is the egress set, the frame goes to exactly
the ports given
* `ALLOW` set: the ASIC looks the destination up as usual and the mask
only limits which ports the result may use
+55
View File
@@ -0,0 +1,55 @@
# 2G040210GSM
The following is a documentation for the managed switch marked as `2G040210GSM`
and sold by Mokerlink.
### Label specifications
- **Name**: 4-port 2.5G Web Managed Switch
- **Ports**:
- 4 × RJ45: 10/100/1000/2500 Mbps
- 2 × SFP+: 1000 / 2500 / 10000 Mbps
- **Power**: 12V DC, 1A barrel connector
### What works
The device is fully supported:
- 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_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.
### Hardware overview
Front
<img src="photos/2M-PCB43-V1.1-managed/2M-PCB43-V1.1-front.jpeg" width="300" />
Label
<img src="photos/2M-PCB43-V1.1-managed/2M-PCB43-V1.1-label.jpeg" width="300" />
### PCB overview
**Board markings**
- Top silkscreen: 2M-PCB43-V1.1
Top side
<img src="photos/2M-PCB43-V1.1-managed/2M-PCB43-V1.1-top.jpeg" width="300" />
Bottom
<img src="photos/2M-PCB43-V1.1-managed/2M-PCB43-V1.1-bottom.jpeg" width="300" />
### J1, serial console
| `J8` pin | Signal |
| -------- | ----------- |
| 1 | RX (Input) |
| 2 | TX (Output) |
| 3 | GND |
| 4 | 3V3 |
## Power supply
Input power is delivered via barell plug, `12V 1A` adapter was provided.
+39
View File
@@ -0,0 +1,39 @@
# FG-4GT-2SX_V2.0
Following is documentation for unmanaged switch marked as `FG-4GT-2SX_V2.0`.
Original software is running UART on 9600 baud rate.
## Brands
* Ruiying RY-4GT-2SX
<img src="photos/FG-4GT-2SX_V2.0/RY-4GT-2SX_label.jpg" width="300" />
## What works
- All four 2.5GBASE-T RJ45 ports at 10/100/1000/2500 Mbps
- Both SFP ports supporting 1G, 2.5G and 10G modules
- LEDs
## PCB overview
**Board markings**
- Top silkscreen: FG-4GT-2SX_V2.0
Front panel
<img src="photos/FG-4GT-2SX_V2.0/chassis-front.jpg" width="300" />
Top side
<img src="photos/FG-4GT-2SX_V2.0/PCB-top.jpg" width="300" />
Bottom
<img src="photos/FG-4GT-2SX_V2.0/PCB-bottom.jpg" width="300" />
## Power supply
Input power is delivered via barell plug, `12V 1A` adapter was provided.
+79
View File
@@ -0,0 +1,79 @@
# FG-8GT-1SX
Following is documentation for unmanaged switch marked as `FG-8GT-1SX`.
Original software is running UART on 9600 baud rate.
Using SPI clamp in-board is the only method for initial installation.
### Brands
| Brand | Type | Managed | PCB | Flash | Chip RTL |
|---------|------------|---------|------------|-------------|---------------|
| Ruiying | RY-8GT-1SX | No | FG-8GT-1SX | GD25Q80ESIG | 8373N + 8224N |
### What works
- All eight 2.5GBASE-T RJ45 ports at 10/100/1000/2500 Mbps
- SFP port with 1G/2.5G/10G modules
- LEDs
### PCB overview
**Board markings**
- Top silkscreen: FG-8GT-1SX
Top side:
<img src="photos/FG-8GT-1SX/PCB-top.jpg" width="600" />
Bottom:
<img src="photos/FG-8GT-1SX/PCB-bottom.jpg" width="600" />
### Serial console
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:
<img src="photos/FG-8GT-1SX/Serial.png" width="600" />
- **Settings**: 115200 baud / 8N1 / 3.3V TTL
- Connect a USB-TTL adapter: adapter GND → pin 1, RX → pin 2, TX → pin 3
### Power supply
Input power is delivered via barell plug, `12V 1A` adapter was provided.
+41 -5
View File
@@ -1,17 +1,36 @@
# Keeplink KP-9000-6XH-X2
# Keeplink KP-9000-6XH-X2 / KP-9000-6XHML-X2
Following is documentation for unmanaged switch marked as `KP-9000-6XH-X2`.
Following is documentation for switches marked as `KP-9000-6XH-X2` or
`KP-9000-6XHML-X2`.
Using SPI clamp in-board is the only method for initial installation.
### Label specifications
- **Name**: 4X 2.5G RJ45 Port + 2 X 10G SFP+ Port
- **Model**: KP-9000-6XH-X2
- **Model**: KP-9000-6XH-X2 / KP-9000-6XHML-X2
- **Ports**:
- 4 × RJ45: 10/100/1000/2500 Mbps
- 2 × SFP+: 1000 / 2500 / 10000 Mbps
### Machine target
These devices exist with different PCB revisions. Select the machine target by
the PCB silkscreen, not by the label on the case. The `6XH` / `6XHML`
distinction appears to be a stock firmware, SKU or label difference, not a
reliable indicator of PCB wiring. The PCB revision defines the hardware layout.
| PCB silkscreen | Known labels / devices | Recommended machine target | Equivalent target | Legacy target |
| --- | --- | --- | --- | --- |
| `2M-PCB43-V1.1` | Mokerlink 2G040210GSM web-managed 4+2 switch | `MACHINE_KP_9000_6XHML_X2_V1_1` | `MACHINE_KP_9000_6XH_X2_V1_1` | `MACHINE_KP_9000_6XHML_X2` |
| `2M-PCB43-V1.2` | `KP-9000-6XHML-X2` | `MACHINE_KP_9000_6XHML_X2_V1_2` | `MACHINE_KP_9000_6XH_X2_V1_2` | `MACHINE_KP_9000_6XHML_X2` |
| `2M-PCB43-V2.1` | `KP-9000-6XH-X2`, `KP-9000-6XHML-X2` | `MACHINE_KP_9000_6XH_X2_V2_1` or `MACHINE_KP_9000_6XHML_X2_V2_1` | same V2.1 layout | `MACHINE_KP_9000_6XH_X2` |
The V1.1 and V1.2 boards currently use the same V1.x GPIO, SFP, port and LED
layout. The V2.1 board uses a different V2.1 layout with custom LED muxing.
Legacy targets are preserved for compatibility with already-tested devices, but
new builds should prefer the explicit PCB-revision targets.
### What works
- All four 2.5GBASE-T RJ45 ports at 10/100/1000/2500 Mbps
@@ -19,7 +38,7 @@ Using SPI clamp in-board is the only method for initial installation.
- LEDs
- untested due to missing Hardware: SFP+ ports equipped with 1G or 2.5G SFPs.
### Hardware overview
### Hardware overview: 2M-PCB43-V2.1
Front side:
@@ -44,6 +63,24 @@ Bottom
<img src="photos/2M-PCB43-V2.1-unmanaged/2M-PCB43-V2.1-bottom.jpg" width="600" />
### Hardware overview: 2M-PCB43-V1.1 / V1.2
The V1.1 board has been reported in the Mokerlink 2G040210GSM web-managed 4+2
switch. The V1.2 board has been seen in managed `KP-9000-6XHML-X2` devices.
Both use the same V1.x layout.
Label:
<img src="photos/KP-9000-6XHML-X2/label.jpg" width="600" />
Top side:
<img src="photos/KP-9000-6XHML-X2/pcb_top.jpg" width="600" />
Bottom:
<img src="photos/KP-9000-6XHML-X2/pcb_bottom.jpg" width="600" />
## Reset Button
There's an unpopulated Reset button on the front left side of the PCB.
@@ -54,4 +91,3 @@ The front case has already the hole in the metal case, you just have to punch a
## Power supply
Input power is delivered via barell plug, `12V 1A` adapter was provided.
+43
View File
@@ -0,0 +1,43 @@
# Steamemo IG204-V1
Following is documentation for unmanaged switch marked as `IG204-V1`.
Using SPI clamp in-board is the only method for initial installation.
### Label specifications
- **Name**: 2.5G Ethernet Switch
- **Model**: IG204 V1
- **Ports**:
- 4 × RJ45: 10/100/1000/2500 Mbps
- 2 × SFP: 1000 / 2500 / 10000 Mbps
### What works (expected from label + similar devices)
- Four 2.5GBASE-T RJ45 ports at 10/100/1000/2500 Mbps
- Two SFP ports supporting 1G, 2.5G and 10G modules
- LEDs
### PCB overview
**Board markings**
- Top silkscreen: PB-2131
Top side
<img src="photos/STEAMEMO_IG204_V1/PCB-top.jpg" width="600" />
### Connectors
### T7, serial console
| `T7` pin | Signal |
| -------- | ----------- |
| 1 | TX (Output) |
| 2 | GND |
| 3 | RX (Input) |
| 4 | 3V3 |
### Power supply
Input power is delivered via barell plug, `12V 1A` adapter was provided.
+6
View File
@@ -1,4 +1,10 @@
# SWTG018AS-A V2.0
## Brands
| Brand | Type |Managed| PCB | Flash | Chip RTL |
|--------|------------------|-------|------------------|-----------------|---------------|
| Ampcom | SWTG018AS-A V2.0 | No | SWTG018AS-A V2.0 | 2MB | 8273N + 8224N |
| Horaco | HC-SWTGW218AS-A | Yes | SWTG018AS-A V2.0 | 2MB(25Q16JVSIQ) | 8273N + 8224N |
The following is a documentation for the unmanaged switch marked as `SWTG018AS-A V2.0`.
It is e.g. sold under the Ampcom brand, but no branh-markings are found on the device.
+43
View File
@@ -0,0 +1,43 @@
### SWTG024AS-A-V2.0.1_4C_2SFP
It is highly similar to SWTG024AS-V2.0, with the only difference being the GPIO configuration for the SFP port.
## Brands
|Brand|Type|Managed|PCB|Flash|Chip RTL|
|---|---|---|---|---|---|
| Horaco | ZX-SG4T2 | | PCB-SWTG024AS-A-V2.0.1_19650 | P25D40SH | ??? |
### Label specifications
- **Name**:
- **Ports**:
- 4 × RJ45: 10/100/1000/2500 Mbps
- 2 × SFP+: 1000 / 2500 / 10000 Mbps
<img src="photos/SWTG024AS-A-V2_0_1_19650/horaco-zx-sg4t2-label.jpg" width="300" />
### What works
The device is fully supported:
- ALL 2.5GBASE-T RJ45 ports work at 10/100/1000/2500 Mbps
- The SFP+ port supports 1G, 2.5G and 10G modules
### PCB overview
**Board markings**
- Top silkscreen: PCB-SWTG024AS-A-V2.0.1_19650
Top side
<img src="photos/SWTG024AS-A-V2_0_1_19650/horaco-zx-sg4t2-pcb-top.jpg" width="300" />
Bottom
<img src="photos/SWTG024AS-A-V2_0_1_19650/horaco-zx-sg4t2-pcb-bottom.jpg" width="300" />
### J1, serial console
| `J1` pin | Signal |
| -------- | ----------- |
| 1 | 3V3 |
| 2 | GND |
| 3 | RX (Input) |
| 4 | TX (Output) |
+44
View File
@@ -0,0 +1,44 @@
### SWTG024AS-A-V2.0.1_5C_1SFP
It is highly similar to SWTG024AS-V2.0, with the only difference being the GPIO configuration for the SFP port.
## Brands
|Brand|Type|Managed|PCB|Flash|Chip RTL|
|---|---|---|---|---|---|
| Horaco | HC-SWTGW215AS | | PCB-SWTG024AS-A-V2.0.1_19650 | W25Q16JV | 8272N |
### Label specifications
- **Name**:
- **Ports**:
- 5 × RJ45: 10/100/1000/2500 Mbps
- 1 × SFP+: 1000 / 2500 / 10000 Mbps
<!-- <img src="" width="300" /> -->
### What works
The device is fully supported:
- ALL 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
### PCB overview
**Board markings**
- Top silkscreen: PCB-SWTG024AS-A-V2.0.1_19650
Top side
<!-- <img src="" width="300" /> -->
Bottom
<!-- <img src="" width="300" /> -->
### J1, serial console
| `J1` pin | Signal |
| -------- | ----------- |
| 1 | 3V3 |
| 2 | GND |
| 3 | RX (Input) |
| 4 | TX (Output) |
+49
View File
@@ -0,0 +1,49 @@
### SWTG024AS-V2.0
## Brands
|Brand|Type|Managed|PCB|Flash|Chip RTL|
|---|---|---|---|---|---|
| hongyavision | LG-SG5T1 | No | PCB-SWTG024AS-V2.0_16895 | 25Q40 | 8272 |
### Label specifications
- **Name**:
- **Ports**:
- 5 × RJ45: 10/100/1000/2500 Mbps
- 1 × SFP+: 1000 / 2500 / 10000 Mbps
- **Power**: 12V DC, 1A 5525 connector
<img src="photos/SWTG024AS-V2.0/label.jpg" width="300" />
### What works
The device is fully supported:
- ALL 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
- Online update does not work with 512KiB flash.
### PCB overview
**Board markings**
- Top silkscreen: PCB-SWTG024AS-V2.0
Top side
<img src="photos/SWTG024AS-V2.0/pcb_top.jpg" width="300" />
Bottom
<img src="photos/SWTG024AS-V2.0/pcb_bottom.jpg" width="300" />
### J1, serial console
| `J1` pin | Signal |
| -------- | ----------- |
| 1 | GND |
| 2 | RX (Input) |
| 3 | TX (Output) |
Note,`R52``R53`may not be installed.You need to bridge them using either solder or resistors.
## Power supply
Input power is delivered via barell plug, `12V 1A` adapter was provided.
+2 -2
View File
@@ -9,8 +9,8 @@ Also the RJ45 connectors can be all plastic/non-shielded or with metal shielding
|Brand|Type|Managed|PCB|PCB Label|Flash|Chip RTL|
|---|---|---|---|---|---|---|
| LIANGUO |SWTG024AS |No| SWTG024AS-v2.0-17452 | CM-23-11-2336 023-17453| 512 KiB | 8272 |
| Haraco |ZX-SWTG124AS | Yes | SWTG024AS-v2.0 | ??? | ??? | 8272 |
| Xikestore |SKS3200M-4GPY2XF | Yes | SWTG024AS-v1.0 | CM-23-08-2043 023-16721 | ??? | 8272 |
| Horaco |ZX-SWTG124AS | Yes | SWTG024AS-v2.0 | ??? | ??? | 8272 |
| Xikestore |SKS3200M-4GPY2XF | Yes | SWTG024AS-v1.0 | CM-23-08-2043 023-16721 | 2048 KiB | 8272 |
| Sodola | SL-SWTG124AS-D | Yes | SWTG024AS-v2.0-17452 | ??? | 2048 KiB | 8272 |
## PCB
+1
View File
@@ -6,6 +6,7 @@
| Mokerlink | ZX-SWTGW218AS | Yes| SWTG118AS-V2.0-16029 | 2MB (FM25Q16A)| 8273N + 8224N |
| Sodola | | | | | |
| Horaco | | | | | |
| XikeStor | SKS3200-8E1X | Yes | SWTG118AS-V2.1-17462 | 2MB (25Q16JVSIQ) | |
## Photos
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 MiB

+33
View File
@@ -65,3 +65,36 @@ ASIC and flushing the table in order to quickly forget the learned entries.
3c:18:a0:7e:11:00 0x0001 learned 5
1c:2a:a3:23:00:02 0x0001 learned 7
```
## Static multicast entries
Slow-protocol frames such as LACPDUs and STP BPDUs have to reach the CPU
without being flooded to the other ports. No bridge relays these frames:
their addresses are in the set that 802.1D-2004 clause 7.12.6 forbids a
bridge to forward, and what travels the network is the information, with
every bridge regenerating BPDUs of its own on its designated ports. The reserved-multicast *trap* action
cannot do that on this hardware, because its destination is an external CPU
attached to a physical port, which these boards do not populate. The protocol
modules therefore leave the reserved-multicast action at *forward* and constrain
the egress with a static L2 multicast entry instead: the lookup hits the entry's
own port mask rather than the VLAN flood mask. Verified on a SWTGW218AS both
ways, with the CPU bit cleared, where delivery stops, and with the CPU bit alone,
where nothing egresses.
`port_l2mc_set()` writes one such entry. The SMI layout is the L2 multicast
variant of the table entry:
```
DATA_IN_A = MAC bytes 5..2 -> c2 00 00 <mac_last>
DATA_IN_B = MAC[1..0] | vid<<16 | IVL<<29 | pmask[1:0]<<30
DATA_IN_C = pmask[9:2]
```
Lookups are IVL, so an entry made for VID 0 is never matched and a caller adds
one entry per PVID in use. The write goes through the table access register
with the table selector set to the L2 lookup table, `TBL_L2_UNICAST` in the
code, a name that despite appearances covers the multicast entries as well.
The hardware hashes MAC and VID to pick the bucket slot by itself.
Writing the same MAC and VID again replaces the entry rather than adding a
second one, so a caller can retarget the mask at will, for instance back to all
ports to restore flooding.
+1 -1
View File
@@ -92,7 +92,7 @@ The following shows the network configuration
On _both_ switches create a LAG with ports 1 and 2 inside and the default hash algorithm which takes
source and destination ports into account, e.g. just use the default:
```
> lag 0 1 2
> lag 1 1 2
```
+2 -1
View File
@@ -37,12 +37,13 @@ This list is incomplete.
| Brand | Partnumber |
| ---------- |----------- |
| GigaDevice | GD25Q32E |
| Fundan | FM25Q16A |
| Puya | P25D40SH |
| Winbond | W25Q16JV |
| Winbond | W25Q32FV |
| Winbond | W25Q32JV |
| Winbond | W25Q16JL |
| Winbond | W25Q16DV |
| Winbond | W25Q80DV |
| Fundan | FM25Q16A |
*NOTE*: Part numbers are incomplete. Part numbers may contain additional information such as package, temperature specifications, and even the number of devices on a reel. So always check the datasheet so that you have the right orderable partnumber.
+155
View File
@@ -0,0 +1,155 @@
# Spanning Tree (STP / RSTP)
The switch can take part in a spanning tree (IEEE 802.1D / 802.1w) so that
redundant links between bridges are blocked instead of forming a loop. The
implementation elects a root bridge from the BPDUs it receives, promotes ports
to forwarding once their listen period expires, ages the root out when it goes
silent, and blocks a port on which it sees its own BPDU.
STP can be enabled and controlled via the web interface or the command line,
as follows:
## Quick start
```
stp on # start participating
stp off # stop, all ports back to forwarding
```
Live status is on the Spanning Tree page of the web UI (or `/stp.json`),
and on the serial console via `stp status`.
With no other bridge around, the switch elects itself root and every port ends
up forwarding — you can leave it on safely. Put the settings in the startup
config to make them survive a reboot:
```
stp prio 15
stp port 1 edge on
stp on
```
## Hardware background
BPDUs are addressed to `01:80:C2:00:00:00`, a reserved link-local group. The
ASIC's Reserved-Multicast action for that address decides what happens to the
frame.
Forwarding to the CPU port works normally: the 8051 sits behind an ordinary
port of the internal switch and is an ordinary member of a forwarding mask.
The *trap* action does not deliver to it. Its destination is an external CPU
attached to a physical port (`cpuTag_externalCpuPort_set`, `EXT_CPU_CTRL` in
the vendor SDK), which these boards do not populate. The ACL trap and
redirect actions do not deliver to the 8051 either.
Delivery therefore uses the *forward* action, constrained to the CPU port
by a static L2 multicast entry (`port_l2mc_set()`), one per VLAN in use:
* while STP runs, the entry's member mask is the CPU port only — BPDUs reach
the CPU and are not flooded to other ports, as a participating bridge
requires;
* with STP off, the same entries are retargeted to all ports, restoring the
transparency an unmanaged switch is expected to have, so a surrounding
spanning tree can span *through* this device.
A BPDU delivered this way is an ordinary frame to the port's ingress logic
and passes through its acceptable-frame-type filter. BPDUs are untagged, so a
port set to admit tagged frames only (`ingress <port>t`) never delivers one
to the CPU. `stp_setup()` prints a warning for every STP-enabled port in that
state.
Port states live in `RTL837X_MSTP_STATES (0x5310)`, two bits per port:
`00` disabled, `01` blocking, `10` learning, `11` forwarding. A port in
blocking forwards nothing between ports, but it still sends what the CPU
hands it and still passes a received BPDU up to the CPU, which is what lets
loop detection go on working on a port it has already blocked.
## Timers
`stp_timers()` runs at 50 Hz (the main loop idles on the 200 Hz system tick and
STP is called every fourth pass), which is what `STP_HZ` in `rtl837x_stp.h`
encodes. All configured values are in seconds:
| setting | default | range |
|---|---|---|
| `stp hello <n>` | 2 | 110 |
| `stp maxage <n>` | 20 | 640 |
| `stp fwd <n>` | 15 | 430 |
| `stp txhold <n>` | 6 | 110 |
A port entering the tree spends `fwd` seconds in blocking before it forwards
(an edge port skips the wait). Root information is discarded after `maxage`
seconds without a BPDU, and the switch then reclaims the root role.
## Topology changes
A change on a local non-edge port (the link coming or going, a port promoted
to forwarding) flushes the addresses learned on it and sets the TC flag in
our BPDUs for `maxage + fwd` seconds. A TC flag received in a BPDU is passed
on: the switch flushes the other non-edge ports once and keeps the flag in
its own BPDUs until one hello after the last flagged frame, so the
notification crosses the switch instead of dying at it. A legacy TCN is
acknowledged with TCA and then treated like a local change.
## Bridge settings
```
stp prio <0-15> # bridge priority = n * 4096, default 8 (32768)
stp version rstp|stp # RST BPDUs (default) or legacy Config BPDUs
stp hello|maxage|fwd|txhold <seconds>
```
The bridge with the lowest priority wins the root election; ties are broken by
the MAC address. If you do not want this switch to become the root of an
existing network, give it a worse priority than the current root — `stp prio 15`
(61440) is the usual "never me" value.
## Per-port settings
```
stp port <1-9> on|off # take part in STP, or stay plain forwarding
stp port <1-9> edge on|off|auto # host-facing port handling (default: auto)
stp port <1-9> cost <0-200000000> # path cost, 0 = automatic (20000)
stp port <1-9> prio <0-240> # port priority, steps of 16
stp port <1-9> guard none|bpdu|root
stp port <1-9> filter on|off # neither send nor accept BPDUs
stp port <1-9> p2p auto|on|off
```
**edge** — an edge port forwards immediately and does not trigger a
topology change when its link comes and goes; `auto` promotes a port to edge
after three seconds without a BPDU, and demotes it as soon as one arrives. Use
`edge on` for ports where only hosts are attached.
**guard**`bpdu` disables a port as soon as a BPDU arrives on it (a host port
should never see one); `root` keeps a port from ever becoming the path to the
root, which protects an existing topology from a newly attached bridge that
claims a better priority.
**filter** — the port neither sends nor accepts BPDUs. Useful when the device
on the far side reacts badly to them (some unmanaged switches with loop
prevention cut the link) but you still want STP on the rest of the ports.
## Status
The Spanning Tree page shows the elected root (priority and MAC), the path cost
to it, the root port, the topology-change counter and, per port, the live state
read from the ASIC together with the configured options. The same data is
available as JSON:
```
GET /stp.json
```
The `stp status` command prints the same view on the serial console.
## Limitations
* One spanning-tree instance; no MSTP, no per-VLAN trees.
* No proposal/agreement handshake — an RST-capable neighbour will still
converge, but through the timers rather than the fast transition.
* Port roles are approximated: the root port and designated ports are
distinguished, alternate/backup are not.
* Topology changes propagate away from the root only: nothing is announced
on the root port (no TCN and no BPDUs at all), so bridges upstream rely on
their own detection.
+135
View File
@@ -0,0 +1,135 @@
# Supporting Multiple Languages in the Web UI
The firmware uses a client-side i18n approach
all translations are stored in a single JavaScript dictionary embedded in the firmware.
No server-side changes are needed.
## Architecture
All translation logic lives in `html/i18n.js`. The file contains:
- 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
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.
## How to Add a New Language
### 1. Add a dictionary entry in `html/i18n.js`
Append a new sub-object to the `LANG` object. Every key from `LANG.en` must 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
},
};
```
### 2. Add the language to the navigation sidebar
In `html/navigation.js`, add an `<option>` to the language selector:
```js
+ "<option value='en'>English</option><option value='ja'>日本語</option>"
```
Replace with:
```js
+ "<option value='en'>English</option><option value='ja'>日本語</option><option value='LANGCODE'>Native Name</option>"
```
### 3. Verify auto-detection
The language detection code in `i18n.js` reads `navigator.language` and normalises it to the first two characters:
```js
var browser = (navigator.language || navigator.userLanguage || 'en').substring(0, 2);
return LANG[browser] ? browser : 'en';
```
If the two-letter code matches a key in `LANG`, it will be auto-selected. No changes needed here.
## Two Translation Mechanisms
### (A) `data-i18n` attribute (declarative — for HTML)
Add `data-i18n="key_name"` to any HTML element. The English text goes in the element content as a fallback:
```html
<h1 data-i18n="port_heading">Port Configuration</h1>
<input type="button" data-i18n="port_apply" value="Apply">
<option data-i18n="port_auto">Auto</option>
<title data-i18n="port_title">Port Configuration</title>
```
On page load, `applyTranslation()` sets:
- `el.value` for `<input type="submit|button">`
- `el.textContent` for `<option>`, `<title>`
- `el.innerHTML` for everything else
### (B) `t('key')` call (imperative — for JavaScript strings)
When generating HTML or text in JavaScript, wrap translatable strings with `t()`:
```js
td.appendChild(document.createTextNode(t('common_port') + i));
td.innerHTML = t('port_auto');
iHTML += "<tr><td>" + t('port_vendor') + "</td></tr>";
```
### Special Case: Link Speed Display
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):
```js
const linkS = [
function(){return t('speed_disabled')},
function(){return t('speed_down')},
"10M", "100M", "1000M", "500M", "10G", "2.5G", "5G"
];
function linkText(idx) { var v = linkS[idx]; return typeof v === 'function' ? v() : v; }
```
Always use `linkText(idx)` (not `linkS[idx]`) to read these values.
## 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 (~34 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"`)
## 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>`).
+35 -12
View File
@@ -1,18 +1,41 @@
# Supported Hardware
The following devices have been tested and are fully working:
- Horaco ZX_SG4T2
- keepLINK kp-9000-6hx-x2 (RTL8372: 4x 2.5GBit + 2x 10GBit SFP+)
- keepLINK KP-9000-6XHML-X2, same as above, but Managed
- keepLINK kp-9000-6hx-x (RTL8372 + RTL8221B 2.5GBit PHY: 5 x 2.5GBit + 1x 10GBit SFP+)
- keepLINK kp-9000-9xh-x-eu (1 x RTL8373 + RTL8224: 8x 2.5GBit + 1x 10GBit SFP+)
- Lianguo LG-SWTGW218AS (RTL8373 + RTL8224 PHY: 8x 2.5GBit + 1x 10GBit SFP+)
- No-Name ZX-SWTGW215AS, managed version of kp-9000-6hx-x, ordered on
AliExpress as keepLINK 5+1 port managed
- TrendNet TEG-S562 (RTL8372: 4x 2.5GBit + 2x 10GBit SFP+)
- FNS-1200P (RTL8372: 4x 2.5GBit + 2x 10GBit SFP+)
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)
| Brand | Type | Managed | PCB | Flash | Ports |
|----------|-----------------|---------|---------------------------------------------------------------------------|-------|-------|
| Ampcom | WAM902-SWTG018AS| No | [SWTG018AS-A V2.0](devices/SWTG018AS_A_V2_0.md) | | 8 + 1 |
| Davuaz | Da-K6501W | No | [PCB-K0501W-V2.0](devices/K0501W_V2_0.md) | | 5 + 1 |
| FOXNEO | FNS-1200P | No | [PCB-K0402W-U13-V2.0](devices/FNS-1200P.md) | 2M | 4 + 2 |
| Hisource | Hi-K0402WS | No | [PCB-K0402WS-V3.0](devices/PCB-K0402WS-V3.0.md) | | 4 + 2 |
| Hisource | Hi-K0801WS | No | [PCB-KO801W-V2.0](devices/HI-K0801WS.md) | | 8 + 1 |
| hongyavision | LG-SG5T1 | No | [PCB-SWTG024AS-V2.0_16895](devices/SWTG024AS-V2.0.md) | 0.5M | 5 + 1 |
| Horaco | HC-SWTGW215AS | Yes | [SWTG024AS-A-V2.0.1_19650 5C 1SFP](devices/SWTG024AS-A-V2.0.1_5C_1SFP.md) | ? | 5 + 1 |
| Horaco | HC-SWTGW218AS | Yes | [SWTG018AS-A V2.0](devices/SWTG018AS_A_V2_0.md) | | 8 + 1 |
| Horaco | ZX310S-4T2XH | Yes | [PCB-SL310S-4T1T1X-V1.0.1-24107](devices/ZX310S-4T2XH.md) | 2M | 5 + 1 |
| Horaco | ZX310S-4T2XT | Yes | [PCB-SL310S-4T2XT-V1.0.0-22273](devices/ZX310S-4T2XT.md) | 2M | 6 |
| Horaco | ZX-SG4T2 | No | [SWTG024AS-A-V2.0.1_19650_4C_2SFP](devices/SWTG024AS-A-V2.0.1_4C_2SFP.md) | 0.5M | 4 + 2 |
| Horaco | ZX-SWTG124AS | Yes | [SWTG024AS-v2.0](devices/SWTG024AS.md) | | 4 + 2 |
| Keeplink | KP-9000-6XH-X2 / KP-9000-6XHML-X2 | No/Yes | [2M-PCB43-V1.2 / V2.1](devices/KP-9000-6XH-X2.md) | | 4 + 2 |
| keepLINK | KP-9000-9XHML-X | Yes | [2M-PCB23-V2.2](devices/2M-PCB23-V2_2.md) | 2M | 8 + 1 |
| keepLINK | KP-9000-9XHML-X | Yes | [2M-PCB23-V3.1](devices/2M-PCB23-V3_1.md) | 2M | 8 + 1 |
| LIANGUO | SWTG024AS | No | [SWTG024AS-v2.0-17452](devices/SWTG024AS.md) | 0.5M | 4 + 2 |
| Lianguo | ZX-SWTGW215AS | Yes | [PCB-SWTG115AS-V2.0](devices/SWTGW215AS.md) | 2M | 5 + 1 |
| Mokerlink| 2G040210GSM | Yes | [2M-PCB43-V1.1](devices/2M-PCB43-V1.1.md) | | 4 + 2 |
| Mokerlink| ZX-SWTGW218AS | Yes | [SWTG118AS-V2.0-16029](devices/SWTGW218AS.md) | 2M | 8 + 1 |
| Ruiying | RY-4GT-2SX | No | [FG-4GT-2SX_V2.0](devices/FG-4GT-2SX_V2.0.md) | 4M | 4 + 2 |
| Ruiying | RY-8GT-1SX | No | [FG-8GT-1SX](devices/FG-8GT-1SX.md) | 1M | 8 + 1 |
| Sodola | SL-SWTG124AS-D | Yes | [SWTG024AS-v2.0-17452](devices/SWTG024AS.md) | 2M | 4 + 2 |
| Steamemo | IG204-V1 | No | [PB-2131](devices/STEAMEMO_IG204_V1.md) | | 4 + 2 |
| TrendNet | TEG-S562 | No | [TEG-S563/EU H/W: V1.0R](devices/TEG-S562.md) | 2M | 4 + 2 |
| Xikestore| SKS3200M-4GPY2XF| Yes | [SWTG024AS-v1.0](devices/SWTG024AS.md) | | 4 + 2 |
| XikeStor | SKS3200-8E1X | Yes | [SWTG118AS-V2.1-17462](devices/SWTGW218AS.md) | 2M | 8 + 1 |
| Ztyuav | Z-QWYT0402 | No | [PCB-K0402WS-V3.0](devices/PCB-K0402WS-V3.0.md) | | 4 + 2 |
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)
Many of the RTL8272/3 devices come in versions with PoE support. The RTLPlayground usually also
works on these, however, no support for configuring PoE is provided, simply because these
+5 -4
View File
@@ -1,17 +1,18 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>Ingress and Egress Bandwidth</title>
<title data-i18n="bw_title">Ingress and Egress Bandwidth</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>Ingress and Egress Bandwidth</h1>
<h1 data-i18n="bw_heading">Ingress and Egress Bandwidth</h1>
<table id="bwtable">
<tr> <th> </th> <th colspan="3"> Ingress </th> <th colspan="2">Egress</th> <th></th></tr>
<tr> <th>Port</th> <th>Limit</th> <th>Bandwidth [kBit/s]</th> <th>Flow Control</th> <th>Limit</th> <th>Bandwidth [kBit/s]</th> <th>Apply</th></tr>
<tr> <th> </th> <th colspan="3" data-i18n="bw_ingress"> Ingress </th> <th colspan="2" data-i18n="bw_egress">Egress</th> <th></th></tr>
<tr> <th data-i18n="bw_col_port">Port</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_flow">Flow Control</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_apply">Apply</th></tr>
</table>
<script src="/bandwidth.js"></script>
</div>
+8 -8
View File
@@ -6,18 +6,18 @@ function createBW() {
console.log("CREATING TABLE ", tbl.rows.length);
for (let i = 2; i < 2 + numPorts; i++) {
const tr = tbl.insertRow();
let td = tr.insertCell(); td.appendChild(document.createTextNode(`Port ${i-1}`));
let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + (i-1)));
td = tr.insertCell();
td.innerHTML = limit.replaceAll("limit_port", "ilimit_port_" + i).replace("exec()", "iClicked(" + i + ")");
td = tr.insertCell();
td.innerHTML = 'UNLIMITED';
td.innerHTML = t('bw_unlimited');
td = tr.insertCell();
td.innerHTML = limit.replaceAll("limit_port", "fc_port_" + i).replace("exec()", "document.getElementById('bwapply_" + i + "').disabled=false;");
td = tr.insertCell();
td.innerHTML = limit.replaceAll("limit_port", "elimit_port_" + i).replace("exec()", "eClicked(" + i + ")");
td = tr.insertCell();
td.innerHTML = 'UNLIMITED';
var button = '<button type="button" id="bwapply_' + i + '" style="margin: 0 0 0 24px" onclick="applyBandwidth(' + i + ');">Apply</button>';
td.innerHTML = t('bw_unlimited');
var button = '<button type="button" id="bwapply_' + i + '" style="margin: 0 0 0 24px" onclick="applyBandwidth(' + i + ');">' + t('bw_col_apply') + '</button>';
td = tr.insertCell();
td.innerHTML = button;
document.getElementById("bwapply_" + i).disabled = true;
@@ -31,7 +31,7 @@ function iClicked(i)
var tbl = document.getElementById('bwtable');
var tr = tbl.rows[i];
if (!document.getElementById("ilimit_port_" + i).checked) {
tr.cells[2].innerHTML = "UNLIMITED";
tr.cells[2].innerHTML = t('bw_unlimited');
document.getElementById("fc_port_" + i).disabled = true;
document.getElementById("fc_port_" + i).checked = true;
} else {
@@ -47,7 +47,7 @@ function eClicked(i)
var tbl = document.getElementById('bwtable');
var tr = tbl.rows[i];
if (!document.getElementById("elimit_port_" + i).checked) {
tr.cells[5].innerHTML = "UNLIMITED";
tr.cells[5].innerHTML = t('bw_unlimited');
} else {
tr.cells[5].innerHTML = '<input id="ebw_' + i + iLayout + i + ')" value="0"/>';
}
@@ -110,12 +110,12 @@ function getBW() {
document.getElementById("ilimit_port_" + (n+1)).checked = p.iLimited;
document.getElementById("elimit_port_" + (n+1)).checked = p.eLimited;
if (!p.iLimited) {
tr.cells[2].innerHTML = "UNLIMITED";
tr.cells[2].innerHTML = t('bw_unlimited');
} else {
tr.cells[2].innerHTML = '<input id="ibw_' + (n+1) + iLayout + (n+1) + ')" value="' + iBW +'"/>';
}
if (!p.eLimited) {
tr.cells[5].innerHTML = "UNLIMITED";
tr.cells[5].innerHTML = t('bw_unlimited');
} else {
tr.cells[5].innerHTML = '<input id="ebw_' + (n+1) + iLayout + (n+1) + ')" value="' + eBW +'"/>';
}
+20 -4
View File
@@ -14,6 +14,7 @@ const conf_cmds = [
/^pvid\s+\d{1,2}\s+\d{1,4}$/,
/^ingress(\s+\d{1,2}[tua])+$/,
/^ingress\s+[tua]$/,
/^port\s+\d{1,2}\s+(10m|100m|1g|2g5|5g|10g|auto|on|off)(\s+(half|full))?$/,
/^port\s+\d{1,2}\s+name\s+\S+$/,
/^eee(\s+\d{1,2})?\s+(on|off)$/,
/^mirror(\s+\d{1,2})(\s+\d{1,2}[tr]?)+$/,
@@ -21,9 +22,19 @@ const conf_cmds = [
/^laghash\s+\d(\s+\w+)+$/,
/^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/,
/^stp\s+(on|off)$/,
/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/,
/^stp\s+version\s+(rstp|stp)$/,
/^stp\s+port\s+\d{1,2}\s+(on|off)$/,
/^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/,
/^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,9}$/,
/^stp\s+port\s+\d{1,2}\s+prio\s+\d{1,3}$/,
/^stp\s+port\s+\d{1,2}\s+guard\s+(none|bpdu|root)$/,
/^stp\s+port\s+\d{1,2}\s+filter\s+(on|off)$/,
/^stp\s+port\s+\d{1,2}\s+p2p\s+(auto|on|off)$/,
/^igmp\s+(on|off)$/,
/^mtu\s+\d{1,2}\s+\d+$/,
/^bw\s+(in|out)\s+\d{1,2}\s+\S+$/,
/^hostname\s+.{1,23}$/,
];
const conf_overwrite = [
/^ip\b/,
@@ -36,6 +47,7 @@ const conf_overwrite = [
/^vlan\s+\d{1,4}(?!\s+mgmt\b)/,
/^pvid\s+\d{1,2}\b/,
/^ingress\b/,
/^port\s+\d{1,2}(?!\s+name\b)/,
/^port\s+\d{1,2}\s+name\b/,
/^eee\s+\d{1,2}\b/,
/^eee\b/,
@@ -43,10 +55,12 @@ const conf_overwrite = [
/^lag\s+\d+\b/,
/^laghash\b/,
/^isolate\s+\d{1,2}\b/,
/^stp\b/,
/^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/,
/^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter|p2p)\b/,
/^igmp\b/,
/^mtu\s+\d{1,2}\b/,
/^bw\s+(in|out)\s+\d{1,2}\b/,
/^hostname\b/,
];
function parseConf(s){
@@ -57,8 +71,7 @@ function parseConf(s){
const deleteMatch = line.match(/^vlan\s+(\d{1,4})\s+d$/);
if (deleteMatch) {
const prefix = "vlan " + deleteMatch[1] + " ";
configuration = configuration.filter(c =>
c !== "vlan " + deleteMatch[1] && !c.startsWith(prefix));
configuration = configuration.filter(c => !c.startsWith(prefix));
continue;
}
console.log(l + ' --> ' + line);
@@ -71,10 +84,13 @@ function parseConf(s){
let m = line.match(x);
let matchStr = m[0];
configuration = configuration.filter(item =>
!(item === matchStr || item.startsWith(matchStr + " ")));
!(item === matchStr || (item.startsWith(matchStr + " ") && !item.endsWith(" mgmt") && !item.startsWith(matchStr + " name "))));
break;
}
}
// Only one management VLAN can be active, so drop any previous mgmt entry
if (/^vlan\s+\d{1,4}\s+mgmt$/.test(line))
configuration = configuration.filter(item => !/^vlan\s+\d{1,4}\s+mgmt$/.test(item));
configuration.push(line);
}
console.log("Configuration now:");
+7 -6
View File
@@ -1,21 +1,22 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>EEE Configuration</title>
<title data-i18n="eee_title">EEE Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>EEE Status</h1>
<h1 data-i18n="eee_heading">EEE Status</h1>
<table id="eeetable">
<tr> <th> </th> <th colspan="3"> Advertising </th> <th colspan="3">Link-Partner advertises</th> <th></th></tr>
<tr> <th>Port</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th>Active?</th></tr>
<tr> <th> </th> <th colspan="3" data-i18n="eee_advertising"> Advertising </th> <th colspan="3" data-i18n="eee_partner">Link-Partner advertises</th> <th></th></tr>
<tr> <th data-i18n="eee_port">Port</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th data-i18n="eee_active">Active?</th></tr>
</table>
<div>
<input style="width:20%;" class="action" id="eee_enable" onclick="eeeSub(0, 1);" type="button" value="Enable EEE">
<input style="width:20%;" class="action" id="eee_enable" onclick="eeeSub(0, 0);" type="button" value="Disable EEE">
<input style="width:20%;" class="action" id="eee_enable" onclick="eeeSub(0, 1);" type="button" data-i18n="eee_enable" value="Enable EEE">
<input style="width:20%;" class="action" id="eee_disable" onclick="eeeSub(0, 0);" type="button" data-i18n="eee_disable" value="Disable EEE">
</div>
<script src="/eee.js"></script>
<script src="/eee_sub.js"></script>
+3 -3
View File
@@ -5,7 +5,7 @@ function createEEE() {
for (let i = 2; i < 2 + numPorts; i++) {
console.log("Table row: " + i + "pState: " + pState[i-2]);
const tr = tbl.insertRow();
let td = tr.insertCell(); td.appendChild(document.createTextNode(`Port ${i-1}`));
let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + (i-1)));
for (let j = 0; j < 7; j++) {
td = tr.insertCell(); td.appendChild(document.createTextNode(" "));
}
@@ -28,8 +28,8 @@ function getEEE() {
let tr = tbl.rows[n+1];
if (!p.isSFP) {
let eee = parseInt(p.eee,2); let lp = parseInt(p.eee_lp,2);
tr.cells[1].innerHTML = `${eee&4?"ON":"OFF"}`; tr.cells[2].innerHTML = `${eee&2?"ON":"OFF"}`; tr.cells[3].innerHTML = `${eee&1?"ON":"OFF"}`;
tr.cells[4].innerHTML = `${lp&4?"ON":"OFF"}`; tr.cells[5].innerHTML = `${lp&2?"ON":"OFF"}`; tr.cells[6].innerHTML = `${lp&1?"ON":"OFF"}`;
tr.cells[1].innerHTML = `${eee&4?t('eee_on'):t('eee_off')}`; tr.cells[2].innerHTML = `${eee&2?t('eee_on'):t('eee_off')}`; tr.cells[3].innerHTML = `${eee&1?t('eee_on'):t('eee_off')}`;
tr.cells[4].innerHTML = `${lp&4?t('eee_on'):t('eee_off')}`; tr.cells[5].innerHTML = `${lp&2?t('eee_on'):t('eee_off')}`; tr.cells[6].innerHTML = `${lp&1?t('eee_on'):t('eee_off')}`;
tr.cells[7].innerHTML = `${p.active}`;
tr.classList.toggle('disabled', pState[i-2] < 0); tr.classList.toggle('isNOK', !p.active); tr.classList.toggle('isOK', p.active);
}
+609
View File
@@ -0,0 +1,609 @@
var LANG = {
en: {
nav_overview: 'Overview',
nav_port_config: 'Port Configuration',
nav_port_stat: 'Port Statistics',
nav_l2: 'L2 Configuration',
nav_mirror: 'Mirroring',
nav_lag: 'Link Aggregation',
nav_eee: 'EEE',
nav_bandwidth: 'Bandwidth Limits',
nav_system: 'System Settings',
nav_fw_update: 'Firmware Update',
port_name: 'Name',
port_status: 'Status',
port_not_enabled: 'Not enabled.',
port_link_speed: 'Link speed',
port_vendor: 'Vendor',
port_model: 'Model',
port_serial: 'Serial',
port_temp: 'Temp',
port_vcc: 'Vcc',
port_tx_fault: 'TX-Fault',
port_tx_disabled: 'TX-Disabled',
port_tx_bias: 'TX-Bias',
port_tx_power: 'TX-Power',
port_rx_power: 'RX-Power',
port_rx_los: 'RX-LOS',
speed_disabled: 'Disabled',
speed_down: 'Down',
port_title: 'FreeSwitchOS Port Configuration',
port_heading: 'Port Configuration',
port_col_port: 'Port',
port_col_name: 'Name',
port_col_speed: 'Current Link Speed',
port_col_set_speed: 'Set Speed',
port_col_disabled: 'Disabled',
port_col_devices: 'Connected devices',
port_devices: 'devices',
port_col_apply: 'Apply',
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_devices: '接続デバイス',
port_devices: 'デバイス',
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_update: '設定更新',
sys_save_label: '現在の設定をフラッシュに保存:',
sys_save: '設定をフラッシュに保存',
sys_advanced: '詳細設定',
sys_startup_config: '起動設定:',
sys_startup_warn: '起動設定を直接編集する際は注意してください。ロックアウトされる可能性があります:',
sys_clear_config: '起動設定クリア',
sys_save_startup: '起動設定をフラッシュに保存',
sys_reset: 'スイッチ再起動',
sys_console: 'コンソールコマンド',
sys_enter_cmd: 'コマンド入力:',
sys_send_cmd: 'コマンド送信',
sys_console_warn: 'コンソールコマンドは注意して入力してください。ロックアウトされる可能性があります!',
sys_invalid_ip: '無効な IP: ',
sys_reset_confirm: 'スイッチを再起動してもよろしいですか?',
sys_resetting: 'スイッチを再起動中です。しばらく待ってからページをリロードしてください。',
login_title: 'RTL スイッチ ログイン',
login_heading: 'RTL スイッチ ログイン',
login_wrong: 'パスワードが違います!',
login_password: 'パスワード',
login_login: 'ログイン',
index_title: 'FreeSwitchOS メインページ',
index_heading: 'スイッチ設定',
index_settings: '設定',
update_title: 'ファームウェア更新',
update_heading: 'ファームウェア更新',
update_instruction: 'アップロードするファームウェアファイルを選択:',
update_upload: 'ファイルをアップロード',
common_port: 'ポート ',
common_pkts: ' pkts',
},
zh: {
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_devices: '已连接设备',
port_devices: '台设备',
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: 'Tagged 端口',
vlan_untagged: 'Untagged 端口',
vlan_select_all: '全选',
vlan_pvid: '作为入方向流量的默认 VLAN (PVID)',
vlan_update: '更新 / 创建',
vlan_configured: '已配置 VLAN',
vlan_col_name: '名称',
vlan_col_member: '成员端口',
vlan_col_tagged: 'Tagged 端口',
vlan_col_untagged: '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_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: '带宽 [kbit/s]',
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_update: '更新设置',
sys_save_label: '将当前全部设置保存到 Flash:',
sys_save: '保存设置到 Flash',
sys_advanced: '高级设置',
sys_startup_config: '启动配置:',
sys_startup_warn: '直接编辑启动配置时请谨慎,错误配置可能导致无法访问设备:',
sys_clear_config: '清除启动配置',
sys_save_startup: '保存启动配置到 Flash',
sys_reset: '重启交换机',
sys_console: '控制台命令',
sys_enter_cmd: '输入命令:',
sys_send_cmd: '发送命令',
sys_console_warn: '输入控制台命令时请谨慎,错误命令可能导致无法访问设备!',
sys_invalid_ip: '无效 IP: ',
sys_reset_confirm: '确定要重启交换机吗?',
sys_resetting: '交换机正在重启。请稍候并刷新页面。',
login_title: 'RTL 交换机登录',
login_heading: 'RTL 交换机登录',
login_wrong: '密码错误!',
login_password: '密码',
login_login: '登录',
index_title: 'FreeSwitchOS 主页',
index_heading: '交换机配置',
index_settings: '设置',
update_title: '固件升级',
update_heading: '固件升级',
update_instruction: '请选择要上传的固件文件:',
update_upload: '上传文件',
common_port: '端口 ',
common_pkts: ' 个包',
}
};
var rtlLang = (function() {
var saved = localStorage.getItem('rtl_lang');
if (saved && LANG[saved]) return saved;
var browser = (navigator.language || navigator.userLanguage || 'en').substring(0, 2);
return LANG[browser] ? browser : 'en';
})();
function t(key) {
return LANG[rtlLang][key] || LANG['en'][key] || key;
}
function setLang(lang) {
if (LANG[lang]) {
localStorage.setItem('rtl_lang', lang);
rtlLang = lang;
document.querySelectorAll('[data-i18n]').forEach(function(el) {
applyTranslation(el);
});
}
}
function applyTranslation(el) {
var key = el.getAttribute('data-i18n');
if (!key) return;
if (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button')) {
el.value = t(key);
} else if (el.tagName === 'OPTION') {
el.textContent = t(key);
} else if (el.tagName === 'TITLE') {
el.textContent = t(key);
} else {
el.innerHTML = t(key);
}
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-i18n]').forEach(function(el) {
applyTranslation(el);
});
});
+4 -3
View File
@@ -2,6 +2,7 @@
<html>
<script src="/main.js"></script>
<script src="/main_info.js"></script>
<script src="/i18n.js"></script>
<script>
window.addEventListener("load", function() {
update( () => {
@@ -10,17 +11,17 @@
});
</script>
<link rel="stylesheet" href="style.css">
<title>FreeSwitchOS Main Page</title>
<title data-i18n="index_title">FreeSwitchOS Main Page</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>Switch Configuration</h1>
<h1 data-i18n="index_heading">Switch Configuration</h1>
<table id="infoTable">
<tr>
<th colspan="2">Settings</th>
<th colspan="2" data-i18n="index_settings">Settings</th>
</tr>
<tbody>
</tbody>
+14 -3
View File
@@ -1,16 +1,27 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>FreeSwitchOS L2 Configuration</title>
<title data-i18n="l2_title">FreeSwitchOS L2 Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>L2 Configuration</h1>
<h1 data-i18n="l2_heading">L2 Configuration</h1>
<p><span data-i18n="l2_shown">Shown:</span> <span id="l2count">-</span></p>
<table id="l2table">
<tr> <th>Port</th> <th>MAC</th> <th>VLAN</th> <th>Type</th> <th>Remove Entry</th></tr>
<tr>
<th><span class="l2sort" onclick="l2SortBy('port')"><span data-i18n="l2_col_port">Port</span><span id="l2a_port" class="l2arrow"></span></span><br>
<input id="l2f_port" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('mac')">MAC<span id="l2a_mac" class="l2arrow"></span></span><br>
<input id="l2f_mac" class="l2filter" oninput="l2FilterChanged()" size="14"></th>
<th><span class="l2sort" onclick="l2SortBy('vlan')">VLAN<span id="l2a_vlan" class="l2arrow"></span></span><br>
<input id="l2f_vlan" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('type')"><span data-i18n="l2_col_type">Type</span><span id="l2a_type" class="l2arrow"></span></span><br>
<input id="l2f_type" class="l2filter" oninput="l2FilterChanged()" size="8"></th>
<th data-i18n="l2_col_remove">Remove Entry</th></tr>
<script src="/l2.js"></script>
</table>
</div>
+72 -54
View File
@@ -1,7 +1,3 @@
var l2GetInterval;
var l2Entries = [];
var l2CurrentEntry = 0;
function fillStats() {
var tbl = document.getElementById('statstable');
if (!numPorts)
@@ -9,22 +5,22 @@ function fillStats() {
if (tbl.rows.length > 1) {
for (let i = 0; i < numPorts; i++) {
console.log("Table Update row: " + i + " state " + pState[i] + " is " + linkS[pState[i] +1]);
tbl.rows[i+1].cells[1].innerHTML = `${linkS[pState[i]+1]}`;
tbl.rows[i+1].cells[2].innerHTML = `${txG[i]} pkts`;
tbl.rows[i+1].cells[3].innerHTML = `${txB[i]} pkts`;
tbl.rows[i+1].cells[4].innerHTML = `${rxG[i]} pkts`;
tbl.rows[i+1].cells[5].innerHTML = `${rxB[i]} pkts`;
tbl.rows[i+1].cells[1].innerHTML = linkText(pState[i]+1);
tbl.rows[i+1].cells[2].innerHTML = `${txG[i]}` + t('common_pkts');
tbl.rows[i+1].cells[3].innerHTML = `${txB[i]}` + t('common_pkts');
tbl.rows[i+1].cells[4].innerHTML = `${rxG[i]}` + t('common_pkts');
tbl.rows[i+1].cells[5].innerHTML = `${rxB[i]}` + t('common_pkts');
}
} else {
for (let i = 0; i < numPorts; i++) {
console.log("Table row: " + i);
const tr = tbl.insertRow();
let td = tr.insertCell(); td.appendChild(document.createTextNode(`Port ${i+1}`));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${linkS[pState[i]+1]}`));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${txG[i]} pkts`));
td = tr.insertCell();td.appendChild(document.createTextNode(`${txB[i]} pkts`));
td = tr.insertCell();td.appendChild(document.createTextNode(`${rxG[i]} pkts`));
td = tr.insertCell();td.appendChild(document.createTextNode(`${rxB[i]} pkts`));
let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + (i+1)));
td = tr.insertCell(); td.appendChild(document.createTextNode(linkText(pState[i]+1)));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${txG[i]}` + t('common_pkts')));
td = tr.insertCell();td.appendChild(document.createTextNode(`${txB[i]}` + t('common_pkts')));
td = tr.insertCell();td.appendChild(document.createTextNode(`${rxG[i]}` + t('common_pkts')));
td = tr.insertCell();td.appendChild(document.createTextNode(`${rxB[i]}` + t('common_pkts')));
}
}
}
@@ -64,6 +60,51 @@ function delL2(idx) {
xhttp.timeout = 1500; xhttp.send();
}
var l2All = [];
const l2Cols = ['port', 'mac', 'vlan', 'type'];
var l2SortCol = 'port';
var l2SortDir = 1;
function l2Key(e, col) {
if (col === 'port') return e.port === 'CPU' ? Number.MAX_SAFE_INTEGER : Number(e.port);
if (col === 'vlan') return Number(e.vlan);
return String(e[col]).toLowerCase();
}
function l2SortBy(col) {
l2SortDir = (col === l2SortCol) ? -l2SortDir : 1;
l2SortCol = col;
renderL2();
}
function l2FilterChanged() { renderL2(); }
function renderL2() {
var tbl = document.getElementById('l2table');
if (!tbl) return;
var f = {};
l2Cols.forEach(function(c) {
var el = document.getElementById('l2f_' + c);
f[c] = el ? el.value.trim().toLowerCase() : '';
});
var rows = l2All.filter(function(e) {
return l2Cols.every(function(c) {
return !f[c] || String(e[c]).toLowerCase().indexOf(f[c]) !== -1;
});
});
rows.sort(function(a, b) {
var x = l2Key(a, l2SortCol), y = l2Key(b, l2SortCol);
return (x < y ? -1 : x > y ? 1 : 0) * l2SortDir;
});
l2Cols.forEach(function(c) {
var a = document.getElementById('l2a_' + c);
if (a) a.textContent = (c === l2SortCol) ? (l2SortDir > 0 ? ' \u25b2' : ' \u25bc') : ' \u21c5';
});
paintL2(tbl, rows);
var cnt = document.getElementById('l2count');
if (cnt) cnt.textContent = rows.length + ' / ' + l2All.length;
}
function fillL2(s)
{
var tbl = document.getElementById('l2table');
@@ -71,7 +112,12 @@ function fillL2(s)
return;
s.sort(l2CMP);
s = uniq(s);
var s = s.map(function(e) { e.port = e.port != 9 ? e.port : "CPU"; return e; });
l2All = s;
renderL2();
}
function paintL2(tbl, s)
{
console.log("L2: ", JSON.stringify(s));
for (let i = 0; i < s.length; i++) {
var e = s[i];
@@ -80,64 +126,36 @@ function fillL2(s)
tbl.rows[i+1].cells[0].innerHTML = `${e.port}`;
tbl.rows[i+1].cells[1].innerHTML = `${e.mac}`;
tbl.rows[i+1].cells[2].innerHTML = `${e.vlan}`;
tbl.rows[i+1].cells[4].innerHTML = '<button type="button" onclick="delL2(' + e.idx + ');">Delete</button>';
tbl.rows[i+1].cells[3].innerHTML = `${e.type}`;
tbl.rows[i+1].cells[4].innerHTML = '<button type="button" onclick="delL2(' + e.idx + ');">' + t('l2_delete') + '</button>';
} else {
const tr = tbl.insertRow();
let td = tr.insertCell(); td.innerHTML = `${e.port}`;
td = tr.insertCell(); td.innerHTML = `${e.mac}`;
td = tr.insertCell(); td.innerHTML = `${e.vlan}`;
td = tr.insertCell(); td.innerHTML = `${e.type}`;
td = tr.insertCell(); td.innerHTML = '<button type="button" onclick="delL2(' + e.idx + ');">Delete</button>';
td = tr.insertCell(); td.innerHTML = '<button type="button" onclick="delL2(' + e.idx + ');">' + t('l2_delete') + '</button>';
}
}
for (let i = tbl.rows.length - 1; i > s.length; i--)
tbl.deleteRow(i);
l2Entries = [];
}
function getL2() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var s = JSON.parse(xhttp.responseText);
var s = s.map(function(e) {
e.vlan = parseInt(e.vlan, 16);
e.idx = parseInt(e.idx, 16);
e.type = e.type == "s" ? "static" : "learned";
e.port = e.port == 9 ? 9 : logToPhysPort[e.port];
return e;
walkL2(function(entries, ok) {
if (ok) {
for (var i = 0; i < entries.length; i++)
entries[i].type = entries[i].type == "s" ? t('l2_static') : t('l2_learned');
fillL2(entries);
}
setTimeout(getL2, 1000);
});
l2Entries.push(...s);
if (l2Entries >= 4096) {
l2Entries = [];
l2CurrentEntry = 0;
clearInterval(l2GetInterval);
return;
}
var w = 0;
for (var i = l2Entries.length-1; i > 0; i--) {
if (l2Entries[0].idx == l2Entries[i].idx) {
w = 1;
break;
}
}
if (w) {
l2CurrentEntry = 0;
fillL2(l2Entries);
} else {
l2CurrentEntry = s[s.length-1].idx + 1;
}
}
};
xhttp.open("GET", "/l2.json?idx=" + l2CurrentEntry, true);
xhttp.timeout = 1500; sendXHTTP(xhttp);
}
window.addEventListener("load", function() {
update( () => {
getL2();
const interval = setInterval(update, 2000);
l2GetInterval = setInterval(getL2, 1000);
});;
});
+7 -6
View File
@@ -1,24 +1,25 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>Link Aggregation Configuration</title>
<title data-i18n="lag_title">Link Aggregation Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>Link Aggregation Groups Configuration</h1>
<h2>LAG 1 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub0" onclick="lagSub(0);" type="button" value="Update / Create"></h2>
<h1 data-i18n="lag_heading">Link Aggregation Groups Configuration</h1>
<h2>LAG 1 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub0" onclick="lagSub(0);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG0"></div>
<br />
<h2>LAG 2 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub1" onclick="lagSub(1);" type="button" value="Update / Create"></h2>
<h2>LAG 2 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub1" onclick="lagSub(1);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG1"></div>
<br />
<h2>LAG 3 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub2" onclick="lagSub(2);" type="button" value="Update / Create"></h2>
<h2>LAG 3 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub2" onclick="lagSub(2);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG2"></div>
<br />
<h2>LAG 4 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub3" onclick="lagSub(3);" type="button" value="Update / Create"></h2>
<h2>LAG 4 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub3" onclick="lagSub(3);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG3"></div>
<script src="/lag.js"></script>
</div>
+7 -7
View File
@@ -1,30 +1,30 @@
<!DOCTYPE html>
<html>
<title>RTL Switch Login</title>
<title data-i18n="login_title">RTL Switch Login</title>
<link rel="stylesheet" href="style.css">
<script src="/i18n.js"></script>
<script>
function removeNote() {
document.getElementById("incorrect").innerHTML = "";
}
window.addEventListener("load", function() {
if (document.referrer.endsWith("login.html"))
document.getElementById("incorrect").innerHTML = "Wrong password!";
document.getElementById("incorrect").innerHTML = t('login_wrong');
});
</script>
</head>
<body class="login_page">
<div class = "center">
<h1> RTL Switch Login</h1>
<h1 data-i18n="login_heading"> RTL Switch Login</h1>
<form method="post" action="login">
<div class="txt_field">
<input name="pwd" type="password" onclick="removeNote()" required />
<input name="pwd" type="password" autocomplete="current-password" onclick="removeNote()" required />
<span></span>
<label>Password</label>
<label data-i18n="login_password">Password</label>
</div>
<input type="submit" value="Login"/>
<input type="submit" data-i18n="login_login" value="Login"/>
<h3 id="incorrect" style="margin-top: 5em;"></h3>
</form>
</body>
</html>
+80 -16
View File
@@ -2,11 +2,12 @@ var txG = new BigInt64Array(10);
var txB = new BigInt64Array(10);
var rxG = new BigInt64Array(10);
var rxB = new BigInt64Array(10);
const linkS = ["Disabled", "Down", "10M", "100M", "1000M", "500M", "10G", "2.5G", "5G"];
const linkS = [function(){return t('speed_disabled')}, function(){return t('speed_down')}, "10M", "100M", "1000M", "500M", "10G", "2.5G", "5G"];
var pState = new Int8Array(10);
var pIsSFP = new Int8Array(10);
var pAdvertised = new Int8Array(10);
var numPorts = 0;
function linkText(idx) { var v = linkS[idx]; return typeof v === 'function' ? v() : v; }
var logToPhysPort = new Int8Array(10);
var physToLogPort = new Int8Array(10);
var portNames = new Array(10);
@@ -21,7 +22,7 @@ function drawPorts() {
d.classList.add('tooltip');
const s = document.createElement("span");
s.classList.add("tooltiptext");
s.innerHTML = "Tooltip text";
s.innerHTML = t('common_port');
s.id="tt_" + (i+1);
const l = document.createElement("object");
d.appendChild(l);
@@ -152,13 +153,13 @@ function update(callback) {
continue;
const portName = p.name || portNames[p.logPort] || '';
var iHTML = "<table border=\"0\" class=\"tt_table\">";
if (portName) iHTML += "<tr><td align=\"left\">Name</td><td>:</td><td>" + portName + "</td></tr>";
if (portName) iHTML += "<tr><td align=\"left\">" + t('port_name') + "</td><td>:</td><td>" + portName + "</td></tr>";
if (p.enabled == 0) {
pState[n] = -1;
bgs[0].style.fill = "red";
leds[0].style.fill = "black"; leds[1].style.fill = "black";
psvg.style.opacity = 0.4;
iHTML += "<tr><td align=\"left\">Status</td><td>:</td><td>Not enabled.</td></tr>";
iHTML += "<tr><td align=\"left\">" + t('port_status') + "</td><td>:</td><td>" + t('port_not_enabled') + "</td></tr>";
iHTML += "</table>";
tt.innerHTML = iHTML;
} else {
@@ -174,31 +175,31 @@ function update(callback) {
leds[0].style.fill = "black"; leds[1].style.fill = "black";
psvg.style.opacity = 0.4
}
iHTML += "<tr><td align=\"left\">Link speed</td><td>:</td><td>" + linkS[p.link + 1] + "</td></tr>";
iHTML += "<tr><td align=\"left\">" + t('port_link_speed') + "</td><td>:</td><td>" + linkText(p.link + 1) + "</td></tr>";
if (p.isSFP) {
pAdvertised[n] = 0;
const hasExtendedStatus = p.sfp_options & 0x40;
iHTML += "<tr><td>Vendor</td><td>:</td><td>" + p.sfp_vendor + "</td></tr>";
iHTML += "<tr><td>Model</td><td>:</td><td>" + p.sfp_model + "</td></tr>";
iHTML += "<tr><td>Serial</td><td>:</td><td>" + p.sfp_serial + "</td></tr>";
iHTML += "<tr><td>" + t('port_vendor') + "</td><td>:</td><td>" + p.sfp_vendor + "</td></tr>";
iHTML += "<tr><td>" + t('port_model') + "</td><td>:</td><td>" + p.sfp_model + "</td></tr>";
iHTML += "<tr><td>" + t('port_serial') + "</td><td>:</td><td>" + p.sfp_serial + "</td></tr>";
if (hasExtendedStatus) {
let txPower = decodeSfpTxPower(p.sfp_txpower, p.sfp_txpower_cal);
let txPowerdBm = convertPowerTodBm(txPower);
let rxPower = decodeSfpRxPower(p.sfp_rxpower, p.sfp_rxpower_cal);
let rxPowerdBm = convertPowerTodBm(rxPower);
iHTML += "<tr><td>Temp</td><td>:</td><td>" + decodeSfpTemp(p.sfp_temp, p.sfp_temp_cal).toFixed(2) + "&#8239;&#8451;</td></tr>";
iHTML += "<tr><td>Vcc</td><td>:</td><td>" + decodeSfpVcc(p.sfp_vcc, p.sfp_vcc_cal).toFixed(2) + "&#8239;V</td></tr>";
iHTML += "<tr><td>TX-Fault</td><td>:</td><td>" + (Boolean(Number(p.sfp_state) & 0x4)) + "</td></tr>";
iHTML += "<tr><td>TX-Disabled</td><td>:</td><td>" + (Boolean(Number(p.sfp_state) & 0x80)) + "</td></tr>";
iHTML += "<tr><td>TX-Bias</td><td>:</td><td>" + decodeSfpTxBias(p.sfp_txbias, p.sfp_txbias_cal).toFixed(1) + "&#8239;mA</td></tr>";
iHTML += "<tr><td>TX-Power</td><td>:</td><td>" + txPower.toFixed(3) + "&#8239;mW / " + txPowerdBm.toFixed(2) + "&#8239;dBm</td></tr>";
iHTML += "<tr><td>RX-Power</td><td>:</td><td>" + rxPower.toFixed(3) + "&#8239;mW / " + rxPowerdBm.toFixed(2) + "&#8239;dBm</td></tr>";
iHTML += "<tr><td>" + t('port_temp') + "</td><td>:</td><td>" + decodeSfpTemp(p.sfp_temp, p.sfp_temp_cal).toFixed(2) + "&#8239;&#8451;</td></tr>";
iHTML += "<tr><td>" + t('port_vcc') + "</td><td>:</td><td>" + decodeSfpVcc(p.sfp_vcc, p.sfp_vcc_cal).toFixed(2) + "&#8239;V</td></tr>";
iHTML += "<tr><td>" + t('port_tx_fault') + "</td><td>:</td><td>" + (Boolean(Number(p.sfp_state) & 0x4)) + "</td></tr>";
iHTML += "<tr><td>" + t('port_tx_disabled') + "</td><td>:</td><td>" + (Boolean(Number(p.sfp_state) & 0x80)) + "</td></tr>";
iHTML += "<tr><td>" + t('port_tx_bias') + "</td><td>:</td><td>" + decodeSfpTxBias(p.sfp_txbias, p.sfp_txbias_cal).toFixed(1) + "&#8239;mA</td></tr>";
iHTML += "<tr><td>" + t('port_tx_power') + "</td><td>:</td><td>" + txPower.toFixed(3) + "&#8239;mW / " + txPowerdBm.toFixed(2) + "&#8239;dBm</td></tr>";
iHTML += "<tr><td>" + t('port_rx_power') + "</td><td>:</td><td>" + rxPower.toFixed(3) + "&#8239;mW / " + rxPowerdBm.toFixed(2) + "&#8239;dBm</td></tr>";
}
// Not all devices & modules have LOS pin...
const rx_los_pin = p.sfp_los !== null ? Boolean(Number(p.sfp_los)) : null;
const rx_los_module = hasExtendedStatus ? Boolean(Number(p.sfp_state) & 0x2) : null;
if (rx_los_module !== null || rx_los_pin !== null) {
iHTML += `<tr><td>RX-LOS</td><td>:</td><td>${rxLosHTML(rx_los_pin, rx_los_module)}</td></tr>`;
iHTML += `<tr><td>` + t('port_rx_los') + `</td><td>:</td><td>${rxLosHTML(rx_los_pin, rx_los_module)}</td></tr>`;
}
} else {
pAdvertised[n] = parseInt(p.adv, 2);
@@ -284,3 +285,66 @@ function sendXHTTP(x)
currentRequests.push(x);
}
function walkL2(onDone)
{
var entries = [];
var idx = 0;
var tries = 0;
function retry() {
if (++tries < 3) {
setTimeout(page, 1000);
return;
}
onDone(entries, false);
}
function page() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState != 4)
return;
if (this.status != 200) {
retry();
return;
}
var s;
try {
s = JSON.parse(xhttp.responseText);
} catch (err) {
retry();
return;
}
tries = 0;
s = s.map(function(e) {
e.vlan = parseInt(e.vlan, 16);
e.idx = parseInt(e.idx, 16);
e.port = e.port == 9 ? 'CPU' : logToPhysPort[e.port];
return e;
});
if (!s.length) {
onDone(entries, true);
return;
}
entries.push(...s);
for (var i = entries.length - 1; i > 0; i--) {
if (entries[0].idx == entries[i].idx) {
onDone(entries, true);
return;
}
}
if (entries.length >= 4096) {
onDone(entries, true);
return;
}
idx = s[s.length - 1].idx + 1;
setTimeout(page, 1000);
};
xhttp.open("GET", "/l2.json?idx=" + idx, true);
xhttp.timeout = 1500;
sendXHTTP(xhttp);
}
page();
}
+9 -8
View File
@@ -1,23 +1,24 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>Mirror Configuration</title>
<title data-i18n="mirror_title">Mirror Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>Mirror Configuration</h1>
<label class="tswitch">Enabled: <input id="me" type="checkbox"></label><br/>
<label for="mp">Mirroring Port:</label> <input type="number" id="mp" name="mp" min="1" max="9"/>
<h2>Mirrored Ports (TX)</h2>
<h1 data-i18n="mirror_heading">Mirror Configuration</h1>
<label class="tswitch"><span data-i18n="mirror_enabled">Enabled:</span> <input id="me" type="checkbox"></label><br/>
<label for="mp"><span data-i18n="mirror_port">Mirroring Port:</span></label> <input type="number" id="mp" name="mp" min="1" max="9"/>
<h2 data-i18n="mirror_tx">Mirrored Ports (TX)</h2>
<div id="mPortsTX"></div>
<br />
<h2>Mirrored Ports (RX)</h2>
<h2 data-i18n="mirror_rx">Mirrored Ports (RX)</h2>
<div id="mPortsRX"></div>
<br/> <input style="width:15%;" class="action" id="mirror_sub" onclick="mirrorSub();" type="button" value="Update / Create">
<input style="width:15%;" class="action" id="mirror_del" onclick="mirrorDel();" type="button" value="Disable Mirroring">
<br/> <input style="width:15%;" class="action" id="mirror_sub" onclick="mirrorSub();" type="button" data-i18n="mirror_update" value="Update / Create">
<input style="width:15%;" class="action" id="mirror_del" onclick="mirrorDel();" type="button" data-i18n="mirror_disable" value="Disable Mirroring">
<script src="/mirror.js"></script>
<script src="/mirror_sub.js"></script>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@ async function mirrorSub() {
var cmd = "mirror ";
var mp=document.getElementById('mp').value
if (!mp) {
alert("Set Mirroring Port first");
alert(t('mirror_set_port_first'));
return;
}
document.getElementById(mirrors[0]+mp).checked=false;document.getElementById(mirrors[1]+mp).checked=false;
@@ -16,7 +16,7 @@ async function mirrorSub() {
cmd = cmd + ` ${i}r`;
}
if (cmd.length < 10) {
alert("Select Mirrored Ports");
alert(t('mirror_select_ports'));
return;
}
try {
+20 -11
View File
@@ -1,12 +1,21 @@
document.getElementById('sidebar').innerHTML =
"<ul><li><a href='index.html'>Overview</a></li>"
+ "<li><a href='ports.html'>Port Configuration</a></li>"
+ "<li><a href='stat.html'>Port Statistics</a></li>"
+ "<li><a href='vlan.html'>VLAN</a></li>"
+ "<li><a href='l2.html'>L2 Configuration</a></li>"
+ "<li><a href='mirror.html'>Mirroring</a></li>"
+ "<li><a href='lag.html'>Link Aggregation</a></li>"
+ "<li><a href='eee.html'>EEE</a></li>"
+ "<li><a href='bandwidth.html'>Bandwidth Limits</a></li>"
+ "<li><a href='system.html'>System Settings</a></li>"
+ "<li><a href='update.html'>Firmware Update</a></li></ul>";
"<ul><li><a href='index.html' data-i18n='nav_overview'>Overview</a></li>"
+ "<li><a href='ports.html' data-i18n='nav_port_config'>Port Configuration</a></li>"
+ "<li><a href='stat.html' data-i18n='nav_port_stat'>Port Statistics</a></li>"
+ "<li><a href='vlan.html' >VLAN</a></li>"
+ "<li><a href='l2.html' data-i18n='nav_l2'>L2 Configuration</a></li>"
+ "<li><a href='stp.html'>Spanning Tree</a></li>"
+ "<li><a href='mirror.html' data-i18n='nav_mirror'>Mirroring</a></li>"
+ "<li><a href='lag.html' data-i18n='nav_lag'>Link Aggregation</a></li>"
+ "<li><a href='eee.html' data-i18n='nav_eee'>EEE</a></li>"
+ "<li><a href='bandwidth.html' data-i18n='nav_bandwidth'>Bandwidth Limits</a></li>"
+ "<li><a href='system.html' data-i18n='nav_system'>System Settings</a></li>"
+ "<li><a href='update.html' data-i18n='nav_fw_update'>Firmware Update</a></li></ul>";
document.addEventListener('DOMContentLoaded', function() {
var links = document.querySelectorAll('#sidebar a[data-i18n]');
links.forEach(function(el) {
var key = el.getAttribute('data-i18n');
if (key) el.textContent = t(key);
});
});
+5 -4
View File
@@ -1,19 +1,20 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>FreeSwitchOS Port Configuration</title>
<title data-i18n="port_title">FreeSwitchOS Port Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>Port Configuration</h1>
<h1 data-i18n="port_heading">Port Configuration</h1>
<form id="vform" action="/vlan.html">
<table id="speedtable">
<tr> <th>Port</th> <th>Name</th> <th>Current Link Speed</th><th>Set Speed</th><th>Disabled</th><th>Apply</th></tr>
<tr> <th data-i18n="port_col_port">Port</th> <th data-i18n="port_col_name">Name</th> <th data-i18n="port_col_speed">Current Link Speed</th><th data-i18n="port_col_devices">Connected devices</th><th data-i18n="port_col_set_speed">Set Speed</th><th data-i18n="port_col_disabled">Disabled</th><th data-i18n="port_col_apply">Apply</th></tr>
</table>
<h2 style="margin-top:3em">Configure Maximum Frame Size (MTU) forwarded at Port</h2>
<h2 style="margin-top:3em" data-i18n="port_mtu_heading">Configure Maximum Frame Size (MTU) forwarded at Port</h2>
<table id="mtutable" style="margin-top:1em">
</table>
<script src="/ports.js"></script>
+54 -12
View File
@@ -4,13 +4,13 @@ function createPortTable() {
var tbl = document.getElementById('speedtable');
if (tbl.rows.length <= 2 && numPorts) {
const sSelect = '<select name="speed_sel" id="speed_sel">'
+ '<option value="auto">Auto</option>'
+ '<option value="2g5">2500MBit/Full</option>'
+ '<option value="1g">1000MBit/Full</option>'
+ '<option value="100m full">100MBit/Full</option>'
+ '<option value="100m half">100MBit/Half</option>'
+ '<option value="10m full">10MBit/Full</option>'
+ '<option value="10m half">10MBit/Half</option>'
+ '<option value="auto">' + t('port_auto') + '</option>'
+ '<option value="2g5">' + t('port_2500m') + '</option>'
+ '<option value="1g">' + t('port_1000m') + '</option>'
+ '<option value="100m full">' + t('port_100m_f') + '</option>'
+ '<option value="100m half">' + t('port_100m_h') + '</option>'
+ '<option value="10m full">' + t('port_10m_f') + '</option>'
+ '<option value="10m half">' + t('port_10m_h') + '</option>'
+ '</select>';
const dSwitch = '<input type="checkbox" id="disable_port" onchange="portOnOff();">'
for (let i = 1; i <= numPorts; i++) {
@@ -18,14 +18,15 @@ function createPortTable() {
continue;
console.log("Table row: " + i + "pState: " + pState[i-2]);
const tr = tbl.insertRow();
let td = tr.insertCell(); td.appendChild(document.createTextNode(`Port ${i}`));
let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + i));
let portName = portNames[physToLogPort[i-1]] || '';
td = tr.insertCell(); td.appendChild(document.createTextNode(portName));
td = tr.insertCell(); td.innerHTML = linkS[pState[i] + 1];
td = tr.insertCell(); td.innerHTML = linkText(pState[i] + 1);
tr.insertCell(); // filled by devRender()
td = tr.insertCell(); td.innerHTML = sSelect.replaceAll("speed_sel", "speed_sel_" + i);
td = tr.insertCell(); td.innerHTML = dSwitch.replaceAll("disable_port", "disable_port_" + i)
.replace("portOnOff()", "portOnOff(" + i + ")");
var button = '<button type="button" style="margin: 0 0 0 24px" onclick="applySpeed(' + i + ');">Apply</button>';
var button = '<button type="button" style="margin: 0 0 0 24px" onclick="applySpeed(' + i + ');">' + t('port_apply') + '</button>';
td = tr.insertCell();
td.innerHTML = button;
}
@@ -55,7 +56,7 @@ function createPortTable() {
tr = tbl.insertRow();
for (let i = 1; i <= numPorts; i++) {
let td = tr.insertCell();
td.innerHTML = '<button type="button" style="margin: 0 0 0 24px" onclick="applyMTU(' + i + ');">Apply</button>';
td.innerHTML = '<button type="button" style="margin: 0 0 0 24px" onclick="applyMTU(' + i + ');">' + t('port_apply') + '</button>';
}
}
}
@@ -68,7 +69,7 @@ function updatePortTable() {
for (let i = 1; i <= numPorts ; i++) {
if (pIsSFP[i-1])
continue;
tbl.rows[i].cells[2].innerHTML = `${linkS[pState[i-1]+1]}`;
tbl.rows[i].cells[2].innerHTML = linkText(pState[i-1]+1);
if (!clicked[i] && pState[i - 1] < 0) {
document.getElementById('speed_sel_' + i).disabled = true;
document.getElementById('disable_port_' + i).checked = true;
@@ -117,6 +118,46 @@ async function applyMTU(port) {
}
}
function devRender(entries) {
var tbl = document.getElementById('speedtable');
if (tbl.rows.length <= 2 || !numPorts)
return;
var perPort = {};
for (var i = 0; i < entries.length; i++) {
var e = entries[i];
if (e.port == 'CPU')
continue;
if (!perPort[e.port])
perPort[e.port] = [];
if (perPort[e.port].indexOf(e.mac) < 0)
perPort[e.port].push(e.mac);
}
for (let i = 1; i <= numPorts; i++) {
if (pIsSFP[i-1])
continue;
var cell = tbl.rows[i].cells[3];
var macs = perPort[i] || [];
if (macs.length == 1) {
cell.textContent = macs[0];
cell.title = '';
} else if (macs.length > 1) {
cell.textContent = macs.length + ' ' + t('port_devices');
cell.title = macs.join('\n');
} else {
cell.textContent = '';
cell.title = '';
}
}
}
function devWalk() {
walkL2(function(entries, ok) {
if (ok)
devRender(entries);
setTimeout(devWalk, 15000);
});
}
function getMTUs() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
@@ -143,6 +184,7 @@ window.addEventListener("load", function() {
createPortTable();
updatePortTable();
getMTUs()
setTimeout(devWalk, 3000);
const interval = setInterval(update, 2000);
const updatePortTableInterval = setInterval(updatePortTable, 1000);
});
+6 -5
View File
@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>FreeSwitchOS Port Statistics</title>
<title data-i18n="stat_title">FreeSwitchOS Port Statistics</title>
<style>
.popup {
display: none;
@@ -31,14 +32,14 @@
<div id="ports"></div>
<div id="popup" class="popup">
<div class="popup-content">
<h2>Detailed Port Statistics</h2>
<h2 data-i18n="stat_detailed">Detailed Port Statistics</h2>
<div id="popup_text"></div>
<button id="closePopup" class="action">Close</button>
<button id="closePopup" class="action" data-i18n="stat_close">Close</button>
</div>
</div>
<h1>Port Statistics</h1>
<h1 data-i18n="stat_heading">Port Statistics</h1>
<table id="statstable">
<tr> <th>Port</th> <th>Name</th> <th>link</th> <th>TX Good</th> <th>TX Bad</th> <th>RX Good</th> <th>RX Bad</th> <th> All Counters </th></tr>
<tr> <th data-i18n="stat_col_port">Port</th> <th data-i18n="stat_col_name">Name</th> <th data-i18n="stat_col_link">link</th> <th data-i18n="stat_col_tx_good">TX Good</th> <th data-i18n="stat_col_tx_bad">TX Bad</th> <th data-i18n="stat_col_rx_good">RX Good</th> <th data-i18n="stat_col_rx_bad">RX Bad</th> <th data-i18n="stat_col_all"> All Counters </th></tr>
<script src="/stat.js"></script>
</table>
</div>
+28 -28
View File
@@ -109,44 +109,44 @@ const mib_counters = [
function getCounters(port) {
var xhttp = new XMLHttpRequest();
const popup = document.getElementById('popup');
xhttp.onreadystatechange = function() {
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("Counters: ", JSON.stringify(s));
const ptext = document.getElementById('popup_text');
var t = "<table style='width:100%'> <tr> <th>Counter</th> <th>Value</th> <th>Counter</th> <th>Value</th></tr> <tr>";
var tableHtml = "<table style='width:100%'> <tr> <th>" + t('stat_counter') + "</th> <th>" + t('stat_value') + "</th> <th>" + t('stat_counter') + "</th> <th>" + t('stat_value') + "</th></tr> <tr>";
console.log("Counter 0: ", BigInt(s[0]).toString(), " length: ", s.length);
var c = 0;
for (i = 0; i < mib_counters.length; i += 4) {
console.log(i, " ", mib_counters[i], ": ", mib_counters[i+1]);
console.log(i, " ", mib_counters[i], ": ", mib_counters[i + 1]);
if (mib_counters[i] == "" && mib_counters[i + 1] == 8) {
console.log("c " + i + ": continue");
continue;
}
var count = BigInt(s[i/4]);
if (mib_counters[i+1] == 8) {
t += "<td>" + mib_counters[i] + "</td><td>" + count.toString() + "</td>";
var count = BigInt(s[i / 4]);
if (mib_counters[i + 1] == 8) {
tableHtml += "<td>" + mib_counters[i] + "</td><td>" + count.toString() + "</td>";
c += 1;
} else if (mib_counters[i+1] == 4) {
} else if (mib_counters[i + 1] == 4) {
if (mib_counters[i] != "") {
t += "<td>" + mib_counters[i] + "</td><td>" + (count >> 32n).toString() + "</td>";
tableHtml += "<td>" + mib_counters[i] + "</td><td>" + (count >> 32n).toString() + "</td>";
c += 1;
}
if (c == 2) {
t += "</tr> <tr>";
tableHtml += "</tr> <tr>";
c = 0;
}
if (mib_counters[i+2] != "") {
t += "<td>" + mib_counters[i+2] + "</td><td>" + (count & 4294967295n).toString() + "</td>";
if (mib_counters[i + 2] != "") {
tableHtml += "<td>" + mib_counters[i + 2] + "</td><td>" + (count & 4294967295n).toString() + "</td>";
c += 1;
}
}
if (c == 2) {
t += "</tr> <tr>";
tableHtml += "</tr> <tr>";
c = 0;
}
}
ptext.innerHTML = t + "</tr></table>";
ptext.innerHTML = tableHtml + "</tr></table>";
popup.style.display = 'flex';
}
};
@@ -161,26 +161,26 @@ function fillStats() {
return;
if (tbl.rows.length > 1) {
for (let i = 0; i < numPorts; i++) {
console.log("Table Update row: " + i + " state " + pState[i] + " is " + linkS[pState[i] +1]);
tbl.rows[i+1].cells[2].innerHTML = `${linkS[pState[i]+1]}`;
tbl.rows[i+1].cells[3].innerHTML = `${txG[i]} pkts`;
tbl.rows[i+1].cells[4].innerHTML = `${txB[i]} pkts`;
tbl.rows[i+1].cells[5].innerHTML = `${rxG[i]} pkts`;
tbl.rows[i+1].cells[6].innerHTML = `${rxB[i]} pkts`;
console.log("Table Update row: " + i + " state " + pState[i] + " is " + linkS[pState[i] + 1]);
tbl.rows[i + 1].cells[2].innerHTML = linkText(pState[i] + 1);
tbl.rows[i + 1].cells[3].innerHTML = `${txG[i]}` + t('common_pkts');
tbl.rows[i + 1].cells[4].innerHTML = `${txB[i]}` + t('common_pkts');
tbl.rows[i + 1].cells[5].innerHTML = `${rxG[i]}` + t('common_pkts');
tbl.rows[i + 1].cells[6].innerHTML = `${rxB[i]}` + t('common_pkts');
}
} else {
for (let i = 0; i < numPorts; i++) {
console.log("Table row: " + i);
const tr = tbl.insertRow();
let td = tr.insertCell(); td.appendChild(document.createTextNode(`Port ${i+1}`));
let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + (i + 1)));
let portName = portNames[physToLogPort[i]] || '';
td = tr.insertCell(); td.appendChild(document.createTextNode(portName));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${linkS[pState[i]+1]}`));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${txG[i]} pkts`));
td = tr.insertCell();td.appendChild(document.createTextNode(`${txB[i]} pkts`));
td = tr.insertCell();td.appendChild(document.createTextNode(`${rxG[i]} pkts`));
td = tr.insertCell();td.appendChild(document.createTextNode(`${rxB[i]} pkts`));
var button = '<button type="button" style="margin: 0 0 0 24px" onclick="getCounters(' + i + ');">Show</button>';
td = tr.insertCell(); td.appendChild(document.createTextNode(linkText(pState[i] + 1)));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${txG[i]}` + t('common_pkts')));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${txB[i]}` + t('common_pkts')));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${rxG[i]}` + t('common_pkts')));
td = tr.insertCell(); td.appendChild(document.createTextNode(`${rxB[i]}` + t('common_pkts')));
var button = '<button type="button" style="margin: 0 0 0 24px" onclick="getCounters(' + (i + 1) + ');">' + t('stat_show') + '</button>';
td = tr.insertCell(); td.innerHTML = button;
}
}
@@ -197,8 +197,8 @@ window.addEventListener('click', (event) => {
}
});
window.addEventListener("load", function() {
update( () => {
window.addEventListener("load", function () {
update(() => {
update();
fillStats();
const stat = setInterval(fillStats, 1000);
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>FreeSwitchOS Spanning Tree</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>Spanning Tree (RSTP)</h1>
<h2>Mode: <select id="stpMode"><option value="off">Disabled</option><option value="on">Enabled</option></select>
<input style="width:15%;margin-left: 3em;" class="action" id="stp_sub" onclick="stpSub();" type="button" value="Apply"></h2>
<div id="stpStat" style="font-family:monospace;"></div>
<h2>Bridge settings</h2>
<table id="stpBridge">
<tr>
<th>Priority</th><th>Version</th><th>Hello [s]</th><th>Max age [s]</th><th>Fwd delay [s]</th><th>Tx hold</th>
</tr>
<tr>
<td><select id="bPrio"></select></td>
<td><select id="bVer"><option value="rstp">RSTP</option><option value="stp">STP compat</option></select></td>
<td><input id="bHello" type="number" min="1" max="10" style="width:4em"></td>
<td><input id="bMaxage" type="number" min="6" max="40" style="width:4em"></td>
<td><input id="bFwd" type="number" min="4" max="30" style="width:4em"></td>
<td><input id="bTxhold" type="number" min="1" max="10" style="width:4em"></td>
</tr>
</table>
<p style="font-size:small">Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.</p>
<h2>Port configuration</h2>
<table id="stpPortsTbl">
<tr>
<th>Port</th><th>State</th><th>Path Cost<br><span style="font-weight:normal;font-size:small">(0 = Auto)</span></th><th>Priority</th><th>Edge Port</th><th>BPDU Filter</th><th>Guard</th><th>Point-to-Point</th>
</tr>
</table>
<h2>Port status</h2>
<table id="stpStatTbl">
<tr>
<th>Port</th><th>Port State</th><th>Role</th><th>Designated Bridge</th><th>Designated Port ID</th><th>Designated Cost</th><th>Oper. Edge</th><th>Oper. P2P</th>
</tr>
</table>
<script src="/stp.js"></script>
</div>
</body>
<script src="/navigation.js"></script>
</html>
+187
View File
@@ -0,0 +1,187 @@
const STP_STATES = ["Disabled", "Blocking", "Learning", "Forwarding"];
const STP_ROLES = ["-", "Root", "Designated", "Alternate"];
const PF_ENABLED = 1, PF_ADMEDGE = 2, PF_AUTOEDGE = 4, PF_BPDUGUARD = 8,
PF_ROOTGUARD = 16, PF_FILTER = 32, PF_OPEREDGE = 64, PF_TRIPPED = 128;
var stpDirty = false;
var stpRows = 0; // ports table built?
async function stpCmd(cmd) {
stpDirty = true;
try {
await fetch('/cmd', { method: 'POST', body: cmd });
} catch(err) {
console.error(`Error: ${err}`);
}
stpDirty = false;
fetchStp();
}
function sel(id, opts, onch) {
const s = document.createElement("select");
s.id = id;
for (const [v, label] of opts) {
const o = document.createElement("option");
o.value = v; o.textContent = label;
s.appendChild(o);
}
s.addEventListener("change", onch);
return s;
}
function num(id, min, max, onch) {
const n = document.createElement("input");
n.type = "number"; n.id = id; n.min = min; n.max = max; n.style.width = "4em";
n.addEventListener("change", onch);
return n;
}
function buildPortsTable(ports) {
const tbl = document.getElementById("stpPortsTbl");
const stat = document.getElementById("stpStatTbl");
for (const p of [...ports].sort((a, b) => a.p - b.p)) {
const tr = tbl.insertRow();
tr.insertCell().textContent = p.p; // Port
tr.insertCell().appendChild(sel("en_" + p.p,
[["on","Enable"],["off","Disable"]],
e => stpCmd("stp port " + p.p + " " + e.target.value)));
const pc = num("cost_" + p.p, 0, 200000000,
e => stpCmd("stp port " + p.p + " cost " + e.target.value));
pc.style.width = "7em";
pc.title = "0 - 200000000 (0 = Auto)";
tr.insertCell().appendChild(pc);
const pr = sel("prio_" + p.p, [],
e => stpCmd("stp port " + p.p + " prio " + e.target.value));
for (let v = 0; v <= 240; v += 16) {
const o = document.createElement("option");
o.value = v; o.textContent = v + (v === 128 ? " (default)" : "");
pr.appendChild(o);
}
tr.insertCell().appendChild(pr);
tr.insertCell().appendChild(sel("edge_" + p.p,
[["auto","Auto"],["on","Enable"],["off","Disable"]],
e => stpCmd("stp port " + p.p + " edge " + e.target.value)));
tr.insertCell().appendChild(sel("filt_" + p.p,
[["off","Disable"],["on","Enable"]],
e => stpCmd("stp port " + p.p + " filter " + e.target.value)));
tr.insertCell().appendChild(sel("guard_" + p.p,
[["none","None"],["bpdu","BPDU"],["root","Root"]],
e => stpCmd("stp port " + p.p + " guard " + e.target.value)));
tr.insertCell().appendChild(sel("p2p_" + p.p,
[["auto","Auto"],["on","Enable"],["off","Disable"]],
e => stpCmd("stp port " + p.p + " p2p " + e.target.value)));
const sr = stat.insertRow();
sr.insertCell().textContent = p.p;
for (const id of ["st","role","db","dp","dc","oe","op"])
sr.insertCell().id = id + "_" + p.p;
}
stpRows = ports.length;
}
function bridgeSelf(s) {
return fmtBridgeId((s.prio * 4096).toString(16).padStart(4, "0") + s.myMac);
}
function fmtBridgeId(h) {
if (!h || h.length < 16) return "";
const prio = parseInt(h.slice(0, 4), 16);
const mac = h.slice(4).replace(/(..)(?=.)/g, "$1:");
return prio + "-" + mac.toUpperCase();
}
function fetchStp() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
if (!stpRows)
buildPortsTable(s.ports);
document.getElementById("stpStat").textContent = s.on
? (s.weRoot
? "This switch (" + bridgeSelf(s) + ") is the root bridge — topology changes: "
+ parseInt(s.tc, 16)
: "This switch: " + bridgeSelf(s)
+ " — root bridge: " + fmtBridgeId(s.rootPrio + s.rootMac)
+ " via port " + s.rootPort + " — path cost: " + parseInt(s.cost, 16)
+ " — topology changes: " + parseInt(s.tc, 16))
: "";
for (const p of s.ports) {
const trip = (p.f & PF_TRIPPED) ? " (guard!)" : "";
document.getElementById("st_" + p.p).textContent =
s.on ? STP_STATES[p.st] + trip : "-";
document.getElementById("role_" + p.p).textContent =
s.on ? STP_ROLES[p.role] : "-";
document.getElementById("db_" + p.p).textContent = s.on ? fmtBridgeId(p.db) : "-";
document.getElementById("dp_" + p.p).textContent =
s.on ? (parseInt(p.dp.slice(0, 2), 16) + "-" + parseInt(p.dp.slice(2), 16)) : "-";
document.getElementById("dc_" + p.p).textContent = s.on ? parseInt(p.dc, 16) : "-";
document.getElementById("oe_" + p.p).textContent =
s.on ? ((p.f & PF_OPEREDGE) ? "True" : "False") : "-";
document.getElementById("op_" + p.p).textContent = s.on ? (p.p2 == 2 ? "False" : "True") : "-";
}
if (stpDirty) // an edit is in flight - do not revert controls
return;
document.getElementById("stpMode").value = s.on ? "on" : "off";
document.getElementById("bPrio").value = s.prio;
document.getElementById("bVer").value = s.rstp ? "rstp" : "stp";
document.getElementById("bHello").value = s.hello;
document.getElementById("bMaxage").value = s.maxage;
document.getElementById("bFwd").value = s.fwd;
document.getElementById("bTxhold").value = s.txhold;
for (const p of s.ports) {
document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off";
document.getElementById("edge_" + p.p).value =
(p.f & PF_ADMEDGE) ? "on" : ((p.f & PF_AUTOEDGE) ? "auto" : "off");
document.getElementById("cost_" + p.p).value = parseInt(p.pc, 16);
document.getElementById("prio_" + p.p).value = p.prio;
document.getElementById("p2p_" + p.p).value = ["auto","on","off"][p.p2];
document.getElementById("guard_" + p.p).value =
(p.f & PF_BPDUGUARD) ? "bpdu" : ((p.f & PF_ROOTGUARD) ? "root" : "none");
document.getElementById("filt_" + p.p).value = (p.f & PF_FILTER) ? "on" : "off";
}
}
};
xhttp.open("GET", `/stp.json`, true);
sendXHTTP(xhttp);
}
async function stpSub() {
const on = document.getElementById("stpMode").value === "on";
document.getElementById("stpStat").textContent = on
? "Enabling STP. The ports start blocked and take up to "
+ (2 * document.getElementById("bFwd").value)
+ " s to reach forwarding, and this page can stay silent until they do."
: "Disabling STP.";
await stpCmd(on ? "stp on" : "stp off");
}
window.addEventListener("load", function() {
const bp = document.getElementById("bPrio");
for (let i = 0; i < 16; i++) {
const o = document.createElement("option");
o.value = i; o.textContent = (i * 4096) + (i === 8 ? " (default)" : "");
bp.appendChild(o);
}
bp.addEventListener("change", e => stpCmd("stp prio " + e.target.value));
document.getElementById("bVer")
.addEventListener("change", e => stpCmd("stp version " + e.target.value));
document.getElementById("bHello")
.addEventListener("change", e => stpCmd("stp hello " + e.target.value));
document.getElementById("bMaxage")
.addEventListener("change", e => stpCmd("stp maxage " + e.target.value));
document.getElementById("bFwd")
.addEventListener("change", e => stpCmd("stp fwd " + e.target.value));
document.getElementById("bTxhold")
.addEventListener("change", e => stpCmd("stp txhold " + e.target.value));
document.getElementById("stpMode")
.addEventListener("change", () => { stpDirty = true; });
update( () => {
fetchStp();
const interval = setInterval(update, 2000);
const stpInt = setInterval(fetchStp, 2000);
});
});
+6
View File
@@ -82,6 +82,7 @@ object, img {
.isNOK{ color: #900;}
.isOK{ color: #090;}
.ip{padding:8px 16px;margin-bottom: 1em;margin-left: 1em}
.rotext{display:inline-block;padding:8px 16px;margin-bottom: 1em;margin-left: 1em}
.row {display: flex;}
.rcol {flex: 90%;}
.lcol {flex: 10%;}
@@ -164,3 +165,8 @@ margin: 30px 0;
select { text-align-last: right; font-family: monospace}
option { direction: rtl; font-family: sans-serif}
#vlanTable td { text-align: left; }
.l2sort{cursor:pointer;user-select:none}
.l2sort:hover{text-decoration:underline}
.l2arrow{opacity:0.55;font-size:0.85em}
.l2filter{width:100%;box-sizing:border-box;font-weight:normal;font-size:0.9em}
+46 -23
View File
@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>System Settings</title>
<title data-i18n="sys_title">System Settings</title>
<style>
.tab-bar { display: flex; border-bottom: 2px solid #226; margin-bottom: 0; margin-left: 16%; padding: 1px 16px; padding-bottom: 0; }
.tab-btn { padding: 10px 20px; background-color: #ddf; border: none; cursor: pointer; font-size: 1em; border-radius: 8px 8px 0 0; margin-right: 4px; }
@@ -14,57 +15,79 @@
</head>
<body>
<div class="tab-bar">
<button class="tab-btn active" onclick="openTab(event, 'system-tab')">System</button>
<button class="tab-btn" onclick="openTab(event, 'advanced-tab')">Advanced</button>
<button class="tab-btn" onclick="openTab(event, 'console-tab')">Console</button>
<button class="tab-btn active" onclick="openTab(event, 'system-tab')" data-i18n="sys_tab_system">System</button>
<button class="tab-btn" onclick="openTab(event, 'advanced-tab')" data-i18n="sys_tab_advanced">Advanced</button>
<button class="tab-btn" onclick="openTab(event, 'console-tab')" data-i18n="sys_tab_console">Console</button>
</div>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<div id="system-tab" class="tab-content active">
<h1>System Settings</h1>
<h1 data-i18n="sys_heading">System Settings</h1>
<div class="row">
<div class="lcol"> <label for="ip">IP address:</label></div>
<div class="lcol"> <label for="hostname" data-i18n="sys_hostname">Hostname:</label></div>
<div class="rcol"> <input id="hostname" type="text" maxlength="23" size="20"/>
<button onclick="hostSub()" data-i18n="sys_apply">Apply</button></div>
</div>
<div class="row">
<div class="lcol"> <label data-i18n="sys_model">Model:</label></div>
<div class="rcol"><span id="model" class="rotext"></span></div>
</div>
<div class="row">
<div class="lcol"> <label for="ip" data-i18n="sys_ip">IP address:</label></div>
<div class="rcol"> <input id="ip" class="ip" type="text" minlength="7" maxlength="15" size="15"/></div>
</div>
<div class="row">
<div class="lcol"> <label for="netmask">Netmask:</label></div>
<div class="lcol"> <label for="netmask" data-i18n="sys_netmask">Netmask:</label></div>
<div class="rcol"><input id="netmask" class="ip" type="text" minlength="7" maxlength="15" size="15"/></div>
</div>
<div class="row">
<div class="lcol"> <label for="gw">Gateway:</label></div>
<div class="lcol"> <label for="gw" data-i18n="sys_gateway">Gateway:</label></div>
<div class="rcol"><input id="gw" class="ip" type="text" minlength="7" maxlength="15" size="15"/></div>
</div>
<div class="row">
<div class="lcol"> <label for="mgmtvlan" data-i18n="sys_mgmt_vlan">Management VLAN:</label></div>
<div class="rcol"><select id="mgmtvlan" class="ip" onchange="mgmtVlanChanged()"></select></div>
</div>
<div class="row">
<div class="lcol"> <label data-i18n="sys_language">Language:</label></div>
<div class="rcol">
<select id="lang-select" onchange="changeLang()">
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="zh">中文</option>
</select>
</div>
</div>
<br/>
When updating the above settings, remember to point your browser to the new IP afterwards:<br/>
<input style="width:40%;" class="action" id="ip_sub" onclick="ipSub();" type="button" value="Update Settings"><br/>
<span data-i18n="sys_ip_note">When updating the above settings, remember to point your browser to the new IP afterwards:</span><br/>
<input style="width:40%;" class="action" id="ip_sub" onclick="ipSub();" type="button" data-i18n="sys_update" value="Update Settings"><br/>
<br/>
Save all current settings to Flash:<br/>
<input style="width:40%;" class="action" id="flash_sub" onclick="flashSave();" type="button" value="Save Settings to Flash">
<span data-i18n="sys_save_label">Save all current settings to Flash:</span><br/>
<input style="width:40%;" class="action" id="flash_sub" onclick="flashSave();" type="button" data-i18n="sys_save" value="Save Settings to Flash">
</div>
<div id="advanced-tab" class="tab-content">
<h1>Advanced Settings</h1>
<div class="lcol"> <label for="config_display">Startup configuration:</label></div>
<h1 data-i18n="sys_advanced">Advanced Settings</h1>
<div class="lcol"> <label for="config_display" data-i18n="sys_startup_config">Startup configuration:</label></div>
<textarea id="config_display" rows="8" cols="60"></textarea>
<br/><br/>
Be careful when saving the directly edited startup configuration, you can lock yourself out:<br/>
<input style="width:40%;" class="action" id="clear_config" onclick="clearConfig();" type="button" value="Clear Startup Config">
<span data-i18n="sys_startup_warn">Be careful when saving the directly edited startup configuration, you can lock yourself out:</span><br/>
<input style="width:40%;" class="action" id="clear_config" onclick="clearConfig();" type="button" data-i18n="sys_clear_config" value="Clear Startup Config">
<br/>
<input style="width:40%;" class="action" id="flash_startup_sub" onclick="flashStartupSave();" type="button" value="Save Startup Settings to Flash">
<input style="width:40%;" class="action" id="flash_startup_sub" onclick="flashStartupSave();" type="button" data-i18n="sys_save_startup" value="Save Startup Settings to Flash">
<br/>
<input style="width:40%;" class="action" id="switch_reset" onclick="resetSwitch();" type="button" value="Reset Switch">
<input style="width:40%;" class="action" id="switch_reset" onclick="resetSwitch();" type="button" data-i18n="sys_reset" value="Reset Switch">
</div>
<div id="console-tab" class="tab-content">
<h1>Console Command</h1>
<label for="console_command">Enter command:</label>
<h1 data-i18n="sys_console">Console Command</h1>
<label for="console_command" data-i18n="sys_enter_cmd">Enter command:</label>
<input type="text" id="console_cmd" name="console_cmd" style="width:40%;">
<input style="width:20%;" class="action" id="cmd_sub" onclick="cmdSub();" type="button" value="Send Command"><br/>
<input style="width:20%;" class="action" id="cmd_sub" onclick="cmdSub();" type="button" data-i18n="sys_send_cmd" value="Send Command"><br/>
<br/><br/>
Be careful when entering console commands, you can lock yourself out!<br/>
<span data-i18n="sys_console_warn">Be careful when entering console commands, you can lock yourself out!</span><br/>
</div>
+61 -3
View File
@@ -2,9 +2,14 @@ var systemInterval = Number();
var isSaving = false;
const ips = ["ip", "netmask", "gw"];
function changeLang() {
var lang = document.getElementById('lang-select').value;
setLang(lang);
}
function checkIp(ip) {
const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipv4.test(ip)) {alert(`Invalid ip:${ip}`); return false };
if (!ipv4.test(ip)) {alert(t('sys_invalid_ip') + ip); return false };
return true;
}
@@ -43,6 +48,14 @@ async function cmdSub() {
}
async function hostSub() {
const h = document.getElementById("hostname").value;
try { await fetch('/cmd', { method: 'POST', body: "hostname " + h }); }
catch(err) { console.error(`Error: ${err}`); }
fetchIP();
}
async function sendConfig(c) {
if (isSaving) return;
isSaving = true;
@@ -118,6 +131,9 @@ function fetchIP() {
document.getElementById("ip").value=s.ip_address;
document.getElementById("netmask").value=s.ip_netmask;
document.getElementById("gw").value=s.ip_gateway;
document.getElementById("hostname").value=s.hostname;
document.getElementById("model").textContent=s.hw_ver;
loadMgmtVlan();
clearInterval(systemInterval);
// Fetch and populate the config textbox
fetchConfig().then((configText) => {
@@ -136,15 +152,57 @@ function fetchIP() {
}
function resetSwitch() {
if (!confirm('Are you sure you want to reset the switch?')) {
if (!confirm(t('sys_reset_confirm'))) {
return;
}
fetch('/reset', { method: 'GET' }).catch(() => {});
setTimeout(() => {
alert('Switch is resetting. Please wait and refresh the page.');
alert(t('sys_resetting'));
}, 3000);
}
window.addEventListener("load", function() {
var langSel = document.getElementById('lang-select');
if (langSel) langSel.value = rtlLang;
systemInterval = setInterval(fetchIP, 1000);
});
var mgmtVlanCurrent = 0;
function loadMgmtVlan() {
var sel = document.getElementById('mgmtvlan');
if (!sel) return;
fetch('/vlanlist').then(function(r) { return r.json(); }).then(function(d) {
var cur = d.mgmt || 0;
var list = d.vlan || [];
mgmtVlanCurrent = cur;
sel.innerHTML = '';
if (!cur) {
var none = document.createElement('option');
none.value = 0; none.disabled = true;
none.textContent = t('sys_mgmt_untagged');
sel.appendChild(none);
}
for (var i = 0; i < list.length; i++) {
var o = document.createElement('option');
o.value = list[i].id;
o.textContent = list[i].name ? (list[i].id + ' (' + list[i].name + ')') : list[i].id;
sel.appendChild(o);
}
sel.value = cur;
}).catch(function(err) { console.error('VLAN list failed:', err); });
}
function mgmtVlanChanged() {
var sel = document.getElementById('mgmtvlan');
var id = parseInt(sel.value, 10);
if (!id || id === mgmtVlanCurrent) return;
if (!confirm(t('sys_mgmt_confirm') + id + '.\n\n' + t('sys_mgmt_warn'))) {
sel.value = mgmtVlanCurrent;
return;
}
fetch('/cmd', { method: 'POST', body: 'vlan ' + id + ' mgmt' })
.then(function() { mgmtVlanCurrent = id; })
.catch(function(err) { console.error('Set management VLAN failed:', err); sel.value = mgmtVlanCurrent; });
}
+5 -4
View File
@@ -1,18 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>Firmware update</title>
<title data-i18n="update_title">Firmware update</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;width:40%;">
<h1>Firmware Update</h1>
<h1 data-i18n="update_heading">Firmware Update</h1>
<form enctype="multipart/form-data" action="/upload" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
Choose a firmware update file to upload: <br/> <br/>
<span data-i18n="update_instruction">Choose a firmware update file to upload:</span> <br/> <br/>
<input name="uploadedfile" type="file" accept=".bin" /><br />
<input style="margin-top:3em" type="submit" value="Upload File" />
<input style="margin-top:3em" type="submit" data-i18n="update_upload" value="Upload File" />
</form>
<script src="/navigation.js"></script>
</body>
+22 -21
View File
@@ -1,52 +1,53 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title>FreeSwitchOS VLAN Configuration</title>
<title data-i18n="vlan_title">FreeSwitchOS VLAN Configuration</title>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<h1>VLAN Configuration</h1>
<h1 data-i18n="vlan_heading">VLAN Configuration</h1>
<form id="vform" action="/vlan.html">
<div>
<label for="vlanSelect">VLAN auswählen:</label>
<label for="vlanSelect" data-i18n="vlan_select">VLAN Select:</label>
<select id="vlanSelect" style="margin: 0 0 0 8px">
<option value="" disabled selected>— VLAN wählen</option>
<option value="" disabled selected data-i18n="vlan_choose">— VLAN Choose</option>
</select>
</div>
<br/>
<div>
<label for="vid">VLAN ID:</label>
<label for="vid" data-i18n="vlan_id">VLAN ID:</label>
<input type="number" min="1" max="4094" id="vid" name="vid">
<button type="button" style="margin: 0 0 0 24px" onclick="fetchVLAN();">Get Configuration</button>
<button type="button" style="margin: 0 0 0 24px" onclick="fetchVLAN();" data-i18n="vlan_get_config">Get Configuration</button>
</div>
<br/><br/>
<label for="vname">VLAN Name:</label>
<label for="vname" data-i18n="vlan_name">VLAN Name:</label>
<input type="text" id="vname" name="vname"><br><br>
<br/>
<h2>Tagged Ports</h2>
<div id="tPorts"><button type="button" style="transform: translateY(-100%);margin: 0 50px 0 0" onclick="utClicked(true);">Select all</button></div>
<h2>Untagged Ports</h2>
<div id="uPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="utClicked(false);">Select all</button> </div>
<h2>Use as default VLAN for incoming traffic (PVID)</h2>
<div id="pPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="pvClicked(true);">Select all</button> </div>
<h2 data-i18n="vlan_tagged">Tagged Ports</h2>
<div id="tPorts"><button type="button" style="transform: translateY(-100%);margin: 0 50px 0 0" onclick="utClicked(true);" data-i18n="vlan_select_all">Select all</button></div>
<h2 data-i18n="vlan_untagged">Untagged Ports</h2>
<div id="uPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="utClicked(false);" data-i18n="vlan_select_all">Select all</button> </div>
<h2 data-i18n="vlan_pvid">Use as default VLAN for incoming traffic (PVID)</h2>
<div id="pPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="pvClicked(true);" data-i18n="vlan_select_all">Select all</button> </div>
<script src="/vlan.js"></script>
<br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" value="Update / Create">
<br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" data-i18n="vlan_update" value="Update / Create">
<script src="/vlan_sub.js"></script>
</form>
<h2>Configured VLANs</h2>
<h2 data-i18n="vlan_configured">Configured VLANs</h2>
<table id="vlanTable" style="width:90%">
<thead>
<tr>
<th>VLAN</th>
<th>Name</th>
<th>Member Ports</th>
<th>Tagged Ports</th>
<th>Untagged Ports</th>
<th>PVID Ports</th>
<th>Delete</th>
<th data-i18n="vlan_col_name">Name</th>
<th data-i18n="vlan_col_member">Member Ports</th>
<th data-i18n="vlan_col_tagged">Tagged Ports</th>
<th data-i18n="vlan_col_untagged">Untagged Ports</th>
<th data-i18n="vlan_col_pvid">PVID Ports</th>
<th data-i18n="vlan_col_delete">Delete</th>
</tr>
</thead>
<tbody id="vlanTableBody">
+4 -4
View File
@@ -76,7 +76,7 @@ function fetchVLAN() {
};
var v=document.getElementById('vid').value
if (!v) {
alert("Set VLAN ID first");
alert(t('vlan_set_id_first'));
return;
}
xhttp.open("GET", `/vlan.json?vid=${v}`, true);
@@ -110,7 +110,7 @@ async function loadVlanTable() {
var resp;
try { resp = await fetch('/vlanlist'); } catch(e) { return; }
if (!resp.ok) return;
var vlans = await resp.json();
var vlans = (await resp.json()).vlan || [];
for (var i = 0; i < vlans.length; i++) {
var v = vlans[i];
var vresp;
@@ -161,7 +161,7 @@ async function loadVlanTable() {
}
function deleteVlan(id) {
if (!confirm('Delete VLAN ' + id + '?')) return;
if (!confirm(t('vlan_delete_confirm') + id + '?')) return;
fetch('/cmd', { method: 'POST', body: 'vlan ' + id + ' d' })
.then(function() { refreshVlanViews(); })
.catch(function(err) { console.error('Delete failed:', err); });
@@ -181,7 +181,7 @@ function loadVlanList() {
sel.style.display = 'none';
return;
}
var vlans = JSON.parse(this.responseText);
var vlans = JSON.parse(this.responseText).vlan || [];
if (!vlans.length) {
sel.style.display = 'none';
return;
+1 -1
View File
@@ -3,7 +3,7 @@ async function vlanSub() {
var cmd = "vlan ";
var v=document.getElementById('vid').value
if (!v) {
alert("Set VLAN ID first");
alert(t('vlan_set_id_first'));
return;
}
cmd = cmd + v;
+428 -131
View File
@@ -20,6 +20,7 @@
#pragma constseg BANK1
extern volatile __xdata uint8_t sfr_data[4];
extern volatile __xdata uint32_t ticks;
extern __code uint8_t * __code hex;
extern __code struct f_data f_data[];
extern __code char * __code mime_strings[];
@@ -41,8 +42,19 @@ __xdata uint32_t cont_addr;
// HTTP header properties
__xdata uint8_t boundary[72];
__xdata uint8_t *content_type = 0;
__xdata uint8_t *session = 0;
// a client may split the request anywhere, including inside a boundary or a
// part header, so a configuration upload is parsed only once it is complete;
// sized for a full config sector plus the multipart framing around it
#define CONFIG_UPLOAD_BUF (CONFIG_LEN + 384)
__xdata uint8_t config_upload;
__xdata uint8_t config_buf[CONFIG_UPLOAD_BUF];
// bytes buffered in config_buf so far (config body, or a firmware part
// header); accumulates across TCP segments
__xdata uint16_t pre_acc;
__xdata uint8_t * __xdata content_type = 0;
__xdata uint8_t * __xdata session = 0;
__xdata uint16_t content_length;
// Global variables holding POST state
__xdata uint16_t bindex; // Current index into the boundary
@@ -50,11 +62,20 @@ __xdata uint8_t verify_crc;
__xdata uint32_t max_upload;
__xdata uint16_t short_parsed;
#define POSTBODY_CMD 1
#define POSTBODY_LOGIN 2
#define POSTBODY_TIMEOUT (5 * SYS_TICK_HZ)
__xdata uint8_t postbody_endpoint;
__xdata uint16_t postbody_start;
__xdata char passwd[21];
// Set when a verified firmware upload awaits its response ACK, after
// which the chip resets to apply the staged image
__xdata uint8_t fw_reset_pending;
__xdata char session_id[SESSION_ID_LENGTH + 1];
__xdata uint8_t authenticated;
__xdata uint32_t now;
__xdata uint8_t *timeptr;
__xdata uint8_t * __xdata timeptr;
__xdata uint32_t last_session_use;
#define TSTATE_NONE 0
@@ -63,6 +84,7 @@ __xdata uint32_t last_session_use;
#define TSTATE_CLOSED 3
#define TSTATE_POST 4
#define TSTATE_MULTIPART 5
#define TSTATE_POSTBODY 6
extern __xdata uint16_t crc_value;
__xdata uint16_t crc_final;
@@ -77,10 +99,12 @@ inline uint8_t is_separator(uint8_t c)
void httpd_init(void) __banked
{
config_upload = 0; // xdata is not zeroed by the startup code
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
// Start listening to port 80
uip_listen(HTONS(80));
s->tstate = TSTATE_CLOSED;
fw_reset_pending = 0; // xdata is not zeroed by the startup code
}
@@ -101,21 +125,6 @@ uint8_t find_entry(__xdata uint8_t *e)
}
char strcmp(__xdata uint8_t *c, __code uint8_t * __xdata d)
{
uint8_t i = 0;
while (d[i] && (d[i] == c[i]))
i++;
if (c[i] < d[i])
return -1;
else if (c[i] > d[i])
return 1;
return 0;
}
bool is_word(__xdata uint8_t *xdata_str_p, __code uint8_t * __xdata code_str_p)
{
uint8_t u, c;
@@ -124,8 +133,8 @@ bool is_word(__xdata uint8_t *xdata_str_p, __code uint8_t * __xdata code_str_p)
u = *xdata_str_p++;
c = *code_str_p++;
if (c == '\0') {
if (u != '\0' && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r')
if (c == NUL) {
if (u != NUL && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r')
return false;
return true;
}
@@ -137,6 +146,24 @@ bool is_word(__xdata uint8_t *xdata_str_p, __code uint8_t * __xdata code_str_p)
}
/* name must be lower-case, starting with the '\n' of the previous line's end */
__xdata uint8_t *header_value(__xdata uint8_t *p, __code uint8_t *name)
{
uint8_t u, c;
while ((c = *name++)) {
u = *p++;
if (u >= 'A' && u <= 'Z')
u += 'a' - 'A';
if (u != c)
return 0;
}
while (*p == ' ' || *p == '\t')
p++;
return p;
}
bool is_url_word_x(__xdata uint8_t *uri_str_p, __xdata uint8_t *src_str_p)
{
uint8_t u, s;
@@ -145,8 +172,8 @@ bool is_url_word_x(__xdata uint8_t *uri_str_p, __xdata uint8_t *src_str_p)
u = *uri_str_p++;
s = *src_str_p++;
if (s == '\0') {
if (u != '\0' && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r')
if (s == NUL) {
if (u != NUL && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r')
return false;
return true;
}
@@ -180,7 +207,7 @@ bool is_url_word_x(__xdata uint8_t *uri_str_p, __xdata uint8_t *src_str_p)
}
bool is_word_x(__xdata uint8_t *lhs_str_p, __xdata uint8_t *rhs_str_p)
bool is_word_x(__xdata uint8_t * lhs_str_p, __xdata uint8_t * rhs_str_p)
{
uint8_t u, c;
@@ -188,8 +215,9 @@ bool is_word_x(__xdata uint8_t *lhs_str_p, __xdata uint8_t *rhs_str_p)
u = *lhs_str_p++;
c = *rhs_str_p++;
if (c == '\0') {
if (u != '\0' && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r')
if (c == NUL) {
/* ';' separates cookies in a Cookie header, so it ends a value too. */
if (u != NUL && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r' && u != ';')
return false;
return true;
}
@@ -211,6 +239,9 @@ uint8_t parse_short(__xdata uint8_t *p)
c = *p++ - '0';
if (c > 9) { break; }
err = 0;
if (short_parsed > 6552)
short_parsed = 0xffff;
else
short_parsed = (short_parsed * 10) + c;
}
return err;
@@ -219,62 +250,82 @@ uint8_t parse_short(__xdata uint8_t *p)
void send_not_found(void)
{
slen = strtox(outbuf, "HTTP/1.1 404 Not found\r\nContent-Type: text/html\r\n\r\n" \
slen = strtox(outbuf, "HTTP/1.1 404 Not found\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n" \
"<!DOCTYPE HTML PUBLIC>\n<title>404 Not Found</title>\n<h1>Not Found</h1>\n");
}
void send_bad_request(void)
{
slen = strtox(outbuf, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\n\r\n" \
slen = strtox(outbuf, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n" \
"<!DOCTYPE HTML PUBLIC>\n<title>400 Bad Request</title>\n<h1>Bad Request</h1>\n");
}
void send_to_login(void)
{
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\n" \
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nConnection: close\r\n" \
"Location: login.html\r\n\r\n");
}
void send_unauthorized(void)
{
slen = strtox(outbuf, "HTTP/1.1 401 Unauthorized\r\n\r\n");
slen = strtox(outbuf, "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
}
__xdata uint8_t *skip_boundary(__xdata uint8_t *p)
void send_length_required(void)
{
while (*p) {
if (is_word_x(p, boundary))
return p + strlen_x(boundary);
p++;
}
return p;
slen = strtox(outbuf, "HTTP/1.1 411 Length Required\r\nConnection: close\r\n\r\n");
}
__xdata uint8_t *scan_header(__xdata uint8_t *p)
void send_ok(void)
{
slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n");
}
__xdata uint8_t *scan_header(__xdata uint8_t * __xdata p)
{
__xdata uint8_t *v;
content_type = 0;
content_length = 0;
session = 0;
authenticated = 0;
while (*p != '\r' || *(p + 1) != '\n' || *(p + 2) != '\r' || *(p + 3) != '\n') {
while (!strstart(p, "\r\n\r\n")) {
dbg_char(*p);
if (!*p++)
if (!*p)
break;
if (is_word(p, "\nContent-Type:"))
content_type = p + 15;
else if (is_word(p, "\nCookie:"))
session = p + 17;
p++;
if ((v = header_value(p, "\ncontent-type:")))
content_type = v;
else if ((v = header_value(p, "\ncontent-length:"))) {
parse_short(v);
content_length = short_parsed;
} else if ((v = header_value(p, "\ncookie:"))) {
/* Scan for the "session" key: the header may hold several
* cookies in any order. Match "session" not "session=" -
* is_word() requires a separator after the match and '=' is
* one, so this also rejects a longer key like "sessionx". */
while (*v && *v != '\r' && *v != '\n') {
if (is_word(v, "session")) {
session = v + 8; /* past "session=" */
break;
}
v++;
}
}
}
if (content_type && is_word(content_type, "multipart/form-data; boundary")) {
dbg_string("\nFound multipart\n");
content_type += 30;
uint8_t i = 0;
while (content_type[i] != '\r' && content_type[i] != '\n') {
while (i < (sizeof(boundary) - 5) &&
content_type[i] != '\r' && content_type[i] != '\n') {
boundary[i + 4] = content_type[i];
i++;
}
@@ -301,10 +352,12 @@ __xdata uint8_t *scan_header(__xdata uint8_t *p)
return p;
}
void gen_random_bytes(__xdata uint8_t *b, uint8_t bytes)
/*
* Generate random HEX-chars at the buffer location.
*/
void gen_random_hex_chars(__xdata uint8_t * b, __xdata uint8_t bytes)
{
__xdata uint8_t i = 0;
uint8_t i = 0;
while (bytes) {
if (!i)
get_random_32();
@@ -316,21 +369,110 @@ void gen_random_bytes(__xdata uint8_t *b, uint8_t bytes)
}
/* 0: body incomplete, 1: configuration stored, 2: malformed */
static uint8_t config_take(void)
{
// #386: needs static, otherwise it still lands in SRAM/DSEG
static __xdata uint16_t cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last;
__xdata uint8_t cfg_bl;
cfg_bl = strlen_x(boundary);
// the body is complete once the closing boundary has arrived
cfg_last = 0;
while (1) {
if (cfg_last + cfg_bl + 1 >= pre_acc)
return 0;
if (strstart_x(&config_buf[cfg_last], boundary)
&& strstart(&config_buf[cfg_last + cfg_bl], "--"))
break;
cfg_last++;
}
// every part lies ahead of the closing boundary, so it bounds the walk
cfg_pos = 0;
while (cfg_pos < cfg_last) {
if (!strstart_x(&config_buf[cfg_pos], boundary)) {
cfg_pos++;
continue;
}
cfg_hdr = cfg_pos + cfg_bl;
cfg_body = cfg_hdr;
while (1) {
if (cfg_body + 3 >= cfg_last)
return 2;
if (strstart(&config_buf[cfg_body], "\r\n\r\n"))
break;
cfg_body++;
}
cfg_end = cfg_body;
cfg_body += 4;
// reaching cfg_last is a match: the last part ends at the closing boundary
while (cfg_end < cfg_last && !strstart_x(&config_buf[cfg_end], boundary))
cfg_end++;
while (cfg_hdr + 8 < cfg_body) {
// the part carrying a filename holds the configuration
if (strstart(&config_buf[cfg_hdr], "filename")) {
// the payload plus its terminator must fit the sector
if (cfg_end - cfg_body + 1 > CONFIG_LEN)
return 2;
config_buf[cfg_end] = 0;
flash_region.addr = CONFIG_START;
flash_sector_erase();
flash_region.addr = CONFIG_START;
flash_region.len = cfg_end - cfg_body + 1;
flash_write_bytes(config_buf + cfg_body);
return 1;
}
cfg_hdr++;
}
cfg_pos = cfg_end;
}
return 2;
}
// unlike scan_header(), keeps no auth state, so it may run on every buffered segment
static uint16_t preamble_payload_start(uint16_t n)
{
uint16_t pos;
for (pos = 0; pos + 24 <= n; pos++) {
if (strstart(&config_buf[pos], "application/octet-stream"))
break;
}
if (pos + 24 > n)
return 0;
pos += 24;
while (pos + 3 < n && !strstart(&config_buf[pos], "\r\n\r\n"))
pos++;
if (pos + 3 >= n)
return 0;
return pos + 4;
}
// Source window for stream_upload(); filled by the caller before the call
__xdata struct {
__xdata uint8_t *p;
uint16_t bptr;
uint16_t plen;
} upload_settings;
/*
* Reads post data from the http stream and writes it into flash memory
* Input: the current position in the TCP buffer (uip_appdata)
* Input: upload_settings, set by the caller
* Returns 1: More data to read, 0: Upload complete, all parts reads
*/
uint8_t stream_upload(uint16_t bptr)
uint8_t stream_upload(void)
{
__xdata uint8_t *p = uip_appdata;
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
dbg_string("Stream_upload called: ");
dbg_short(bptr); dbg_char('\n');
dbg_short(upload_settings.bptr); dbg_char('\n');
do {
if (bptr >= uip_len) {
if (upload_settings.bptr >= upload_settings.plen) {
s->tstate = TSTATE_POST;
return 1;
}
@@ -343,17 +485,23 @@ uint8_t stream_upload(uint16_t bptr)
flash_write_bytes(flash_buf);
uptr += write_len;
write_len = 0;
// TODO: This is a bit premature, what about a nice web-page saying the device will reset???
if (verify_crc) {
dbg_string("CRC16: "); dbg_short(crc_final); dbg_char('\n');
// Both bodies are 33 bytes; Content-Length lets the
// browser complete the response without waiting for
// the connection close (which a reset would swallow)
if (crc_final == 0xb001) {
print_string("Checksum OK.\nUpload to flash done, will reset!\n");
// close connection to avoid retries by browser
uip_close();
reset_chip();
slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nContent-Length: 33\r\n"
"Content-Type: text/plain\r\n\r\n"
"OK: checksum verified, rebooting\n");
// Reset once the response is fully ACKed
fw_reset_pending = 1;
} else {
print_string("Checksum incorrect! Aborting.\n");
uip_close();
slen = strtox(outbuf, "HTTP/1.1 400 Bad Request\r\nContent-Length: 33\r\n"
"Content-Type: text/plain\r\n\r\n"
"NO: checksum failed, not applied\n");
}
}
// Make sure there is a 0 at the end of the uploaded data
@@ -361,18 +509,15 @@ uint8_t stream_upload(uint16_t bptr)
flash_region.addr = uptr;
flash_region.len = 1;
flash_write_bytes(flash_buf);
if (bptr >= uip_len)
if (upload_settings.bptr >= upload_settings.plen)
return 0;
if(!verify_crc)
//ugly hack to signal connection finished after config upload.
uip_close();
return 1;
}
if (p[bptr] == boundary[bindex]) {
if (upload_settings.p[upload_settings.bptr] == boundary[bindex]) {
if (!bindex)
crc_final = crc_value;
crc16(p + bptr);
bptr++;
crc16(upload_settings.p + upload_settings.bptr);
upload_settings.bptr++;
bindex++;
} else {
if (bindex) {
@@ -380,8 +525,8 @@ uint8_t stream_upload(uint16_t bptr)
write_len += bindex;
bindex = 0;
}
crc16(p + bptr);
flash_buf[write_len++] = p[bptr++];
crc16(upload_settings.p + upload_settings.bptr);
flash_buf[write_len++] = upload_settings.p[upload_settings.bptr++];
if (write_len >= FLASH_PAGE_SIZE) {
dbg_string("len: "); dbg_short(write_len); dbg_char(' ');
dbg_string("CRC16: "); dbg_short(crc_value); dbg_char('\n');
@@ -406,12 +551,167 @@ uint8_t stream_upload(uint16_t bptr)
}
static void handle_config_fragment(__xdata uint8_t *p)
{
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
__xdata uint16_t frag_len;
uint8_t taken;
frag_len = uip_len - (p - uip_appdata);
if (pre_acc + frag_len >= CONFIG_UPLOAD_BUF) {
print_string("Configuration too large, aborting.\n");
config_upload = 0;
s->tstate = TSTATE_NONE;
send_bad_request();
return;
}
memcpy(config_buf + pre_acc, p, frag_len);
pre_acc += frag_len;
taken = config_take();
if (!taken) {
s->tstate = TSTATE_MULTIPART;
return;
}
config_upload = 0;
s->tstate = TSTATE_NONE;
if (taken == 2) {
send_bad_request();
return;
}
send_ok();
}
static void handle_firmware_fragment(__xdata uint8_t *p)
{
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
__xdata uint16_t frag_len, payload_start;
frag_len = uip_len - (p - uip_appdata);
if (pre_acc + frag_len >= CONFIG_UPLOAD_BUF) {
print_string("Firmware upload header too large, aborting.\n");
config_upload = 0;
s->tstate = TSTATE_NONE;
send_bad_request();
return;
}
memcpy(config_buf + pre_acc, p, frag_len);
pre_acc += frag_len;
payload_start = preamble_payload_start(pre_acc);
if (!payload_start) {
s->tstate = TSTATE_MULTIPART;
return;
}
dbg_string("Have content octets\n");
flash_init(0); // Re-initialize flash for non-DIO operation, otherwise flashing fails
set_sys_led_state(SYS_LED_FAST);
crc_value = 0;
bindex = 0;
write_len = 0;
// A verdict is only built once the upload part completes;
// clear any stale response so the completion check in the
// appcall POST branch cannot send leftovers
slen = 0;
upload_settings.p = config_buf;
upload_settings.bptr = payload_start;
upload_settings.plen = pre_acc;
stream_upload();
dbg_string("Done reading first fragment\n");
}
static void run_cmd_body(__xdata uint8_t *body)
{
execute_commands(body);
if (err_status != ERR_OK) {
send_bad_request();
return;
}
send_ok();
}
static void run_login_body(__xdata uint8_t *body)
{
if (strstart(body, "pwd=") && is_url_word_x(body + 4, passwd)) {
dbg_string("Password accepted!\n");
read_reg_timer(&last_session_use);
gen_random_hex_chars(session_id, SESSION_ID_LENGTH);
session_id[SESSION_ID_LENGTH] = NUL;
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nConnection: close\r\nLocation: index.html\r\n" \
"Set-Cookie: session=");
for (uint8_t i = 0; i < SESSION_ID_LENGTH; i++)
outbuf[slen++] = session_id[i];
slen += strtox(outbuf + slen, "; SameSite=Strict\r\n\r\n");
} else {
dbg_string("Password invalid!\n");
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nConnection: close\r\nLocation: login.html\r\n\r\n");
}
}
static uint8_t post_body_take(__xdata uint8_t *p)
{
uint16_t have;
if (!content_length) {
send_length_required();
return 0;
}
if (content_length >= CONFIG_UPLOAD_BUF) {
send_bad_request();
return 0;
}
have = uip_len - (p - uip_appdata);
if (have >= content_length) {
p[content_length] = NUL;
return 1;
}
memcpy(config_buf, p, have);
pre_acc = have;
postbody_start = ticks;
uip_conn->appstate.tstate = TSTATE_POSTBODY;
return 0;
}
static void post_body_continue(void)
{
uint16_t take;
// no header scan runs while the body is pending: content_length is this request's
take = content_length - pre_acc;
if (take > uip_len)
take = uip_len;
memcpy(config_buf + pre_acc, uip_appdata, take);
pre_acc += take;
if (pre_acc < content_length) {
postbody_start = ticks;
return;
}
config_buf[pre_acc] = NUL;
uip_conn->appstate.tstate = TSTATE_NONE;
if (postbody_endpoint == POSTBODY_CMD)
run_cmd_body(config_buf);
else
run_login_body(config_buf);
}
void handle_post(void)
{
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
__xdata uint8_t *p = uip_appdata;
__xdata uint8_t *request_path = p + 6;
if (s->tstate == TSTATE_POSTBODY) {
post_body_continue();
return;
}
// Was the multipart header sent in multiple packets?
if (s->tstate != TSTATE_MULTIPART) {
dbg_string("Is POST\n");
@@ -419,10 +719,10 @@ void handle_post(void)
// Find end of request path
while (*p && !is_separator(*p))
p++;
*p++ = '\0';
*p++ = NUL;
// Find end of request header
boundary[0] ='\0';
boundary[0] =NUL;
p = scan_header(p);
dbg_string("Boundary: >"); dbg_string_x(boundary); dbg_string("<\n");
if (!*p || !content_type) {
@@ -438,20 +738,20 @@ void handle_post(void)
return;
}
print_string("Firmware upload started.");
config_upload = 0;
uptr = FIRMWARE_UPLOAD_START;
verify_crc = 1;
max_upload = 1024576;
pre_acc = 0;
} else if (is_word(request_path, "config")) {
if (!authenticated) {
send_unauthorized();
return;
}
dbg_string("Configuration upload, erasing config mem!\n");
uptr = CONFIG_START;
dbg_string("Configuration upload\n");
verify_crc = 0;
max_upload = 2048;
flash_region.addr = CONFIG_START;
flash_sector_erase();
config_upload = 1;
pre_acc = 0;
}
// Check for other POST requests, which are not multipart, below
} else {
@@ -464,11 +764,11 @@ void handle_post(void)
send_unauthorized();
return;
}
execute_commands(p);
if (err_status != ERR_OK) {
send_bad_request();
postbody_endpoint = POSTBODY_CMD;
if (!post_body_take(p))
return;
run_cmd_body(p);
return;
}
} else if (is_word(request_path, "login")) {
dbg_string("POST login\n");
@@ -478,21 +778,11 @@ void handle_post(void)
return;
}
p += 8; // Read also over "pwd="
if (is_url_word_x(p, passwd)) {
dbg_string("Password accepted!\n");
read_reg_timer(&last_session_use);
gen_random_bytes(session_id, SESSION_ID_LENGTH);
session_id[SESSION_ID_LENGTH] = '\0';
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nLocation: index.html\r\n" \
"Set-Cookie: session=");
for (register uint8_t i = 0; i < SESSION_ID_LENGTH; i++)
outbuf[slen++] = session_id[i];
slen += strtox(outbuf + slen, "; SameSite=Strict\r\n\r\n");
} else {
dbg_string("Password invalid!\n");
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nLocation: login.html\r\n\r\n");
}
p += 4;
postbody_endpoint = POSTBODY_LOGIN;
if (!post_body_take(p))
return;
run_login_body(p);
return;
} else if (s->tstate == TSTATE_MULTIPART || is_word(request_path, "upload") || is_word(request_path, "config")) {
dbg_string("POST upload/config request\n");
@@ -505,42 +795,15 @@ void handle_post(void)
send_bad_request();
return;
}
// We skip the intial parts as part of the header
do {
p = skip_boundary(p);
if (!*p) {
s->tstate = TSTATE_MULTIPART;
if (config_upload)
handle_config_fragment(p);
else
handle_firmware_fragment(p);
return;
}
p = scan_header(p);
if (!*p)
goto bad_request;
if (!content_type) // We are waiting for the part with the octet stream
continue;
} while (!is_word(content_type, "application/octet-stream"));
dbg_string("Have content octets\n");
p += 4; // Skip \r\n\r\n sequence at end of preamble of part
flash_init(0); // Re-initialize flash for non-DIO operation, otherwise flashing fails
set_sys_led_state(SYS_LED_FAST);
crc_value = 0;
bindex = 0;
write_len = 0;
stream_upload(p - uip_appdata);
dbg_string("Done reading first fragment\n");
return;
} else {
send_not_found();
return;
}
slen = strtox(outbuf, "HTTP/1.1 200 OK\r\n\r\n");
return;
bad_request:
send_bad_request();
return;
}
@@ -571,6 +834,11 @@ void httpd_appcall(void)
dbg_string("Closing because everything has been transmitted\n");
uip_close();
s->tstate = TSTATE_CLOSED;
} else if (s->tstate == TSTATE_POSTBODY
&& (uint16_t)ticks - postbody_start > POSTBODY_TIMEOUT) {
dbg_string("Body never arrived\n");
uip_abort();
s->tstate = TSTATE_CLOSED;
}
} else if (uip_acked() && s->tstate == TSTATE_TX) {
dbg_string("ACK\n");
@@ -604,11 +872,23 @@ void httpd_appcall(void)
cont_len -= slen;
cont_addr += slen;
s->tstate = TSTATE_TX;
} else if (fw_reset_pending) {
// The upload verdict has been fully ACKed by the client;
// now it is safe to reset and apply the staged image
print_string("Resetting to apply update\n");
reset_chip();
}
} else if (uip_newdata() && s->tstate == TSTATE_POST) {
// Check here maxupload by subtracting uip_len and close socekt if fails!
if (max_upload - uip_len > 0) {
stream_upload(0);
upload_settings.p = uip_appdata;
upload_settings.bptr = 0;
upload_settings.plen = uip_len;
stream_upload();
// A completed part with a built verdict must go out
// through the normal TX path
if (s->tstate == TSTATE_NONE && slen)
goto do_send;
write_char('.');
} else {
send_bad_request();
@@ -626,24 +906,31 @@ void httpd_appcall(void)
dbg_char('\n');
#endif
p = uip_appdata;
if (is_word(p, "POST") || s->tstate == TSTATE_MULTIPART) {
if (is_word(p, "POST") || s->tstate == TSTATE_MULTIPART
|| s->tstate == TSTATE_POSTBODY) {
handle_post();
// If this is an ongoing post stream, then wait for the next packet
if (s->tstate == TSTATE_POST || s->tstate == TSTATE_MULTIPART) {
if (s->tstate == TSTATE_POST || s->tstate == TSTATE_MULTIPART
|| s->tstate == TSTATE_POSTBODY) {
uip_len = 0;
return;
}
goto do_send;
}
if (is_word(p, "GET"))
// We only expect a GET request here.
if (!is_word(p, "GET")) {
send_bad_request();
goto do_send;
}
dbg_string("GET request ");
p += 4;
scan_header(p);
__xdata uint8_t *q = p;
while (!is_separator(*p))
while (*p && !is_separator(*p))
p++;
*p = '\0';
*p = NUL;
dbg_string_x(q);
dbg_char('\n');
@@ -665,7 +952,9 @@ void httpd_appcall(void)
parse_short(q + 15);
send_vlan(short_parsed);
} else if (is_word(q, "/counters.json")) {
send_counters(q[20]-'0');
uint8_t cport = q[20] - '0';
if (send_counters(cport))
send_bad_request();
} else if (is_word(q, "/eee.json")) {
send_eee();
} else if (is_word(q, "/bandwidth.json")) {
@@ -682,6 +971,8 @@ void httpd_appcall(void)
send_mtu();
} else if (is_word(q, "/lag.json")) {
send_lag();
} else if (is_word(q, "/stp.json")) {
send_stp();
} else if (is_word(q, "/vlanlist")) {
send_vlanlist();
} else if (is_word(q, "/config")) {
@@ -714,7 +1005,13 @@ void httpd_appcall(void)
slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nContent-Type: ");
slen += strtox(outbuf + slen, mime_strings[f_data[entry].mime]);
slen += strtox(outbuf + slen, "; charset=UTF-8\r\nCache-Control: max-age=60, must-revalidate\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: style-src 'self' 'unsafe-inline'\r\n\r\n");
/* 'unsafe-inline' is needed for the inline onclick handlers
* and the inline <script> on login.html. Connection: close is
* required because this httpd closes the connection after every
* response; without advertising it a browser reuses the socket
* from its keep-alive pool and the next request hits the already
* closed connection (a POST is then dropped without a retry). */
slen += strtox(outbuf + slen, "; charset=UTF-8\r\nCache-Control: max-age=60, must-revalidate\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; form-action 'self'\r\n\r\n");
len_left = f_data[entry].len;
if (len_left > (TCP_OUTBUF_SIZE - slen)) {
+212 -90
View File
@@ -12,6 +12,7 @@
#include "phy.h"
#include "version.h"
#include "machine.h"
#include "rtl837x_stp.h"
#include "page_impl.h"
#include "syslog.h"
@@ -26,6 +27,7 @@
extern __code const struct machine machine;
extern __xdata uint8_t outbuf[TCP_OUTBUF_SIZE];
extern __xdata uint16_t slen;
extern __xdata uint16_t management_vlan;
extern __xdata uint16_t cont_len;
extern __xdata uint32_t cont_addr;
extern __code uint8_t * __code hex;
@@ -45,7 +47,7 @@ extern __xdata char sfp_module_model[2][17];
extern __xdata char sfp_module_serial[2][17];
extern __xdata uint8_t sfp_options[2];
__code uint8_t * __code HTTP_RESPONCE_JSON = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
__code uint8_t * __code HTTP_RESPONCE_JSON = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n";
__code uint8_t * __code HTTP_RESPONCE_TXT = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n";
// Convert uint8_t to ascii HEX char push on html-buffer.
@@ -156,14 +158,14 @@ void sfr_data_to_html(void)
}
void reg_to_html(register uint16_t reg)
void reg_to_html(uint16_t reg)
{
reg_read_m(reg);
sfr_data_to_html();
}
void reg_to_html_long(register uint16_t reg)
void reg_to_html_long(uint16_t reg)
{
reg_read_m(reg);
byte_to_html(sfr_data[0]);
@@ -176,10 +178,12 @@ void reg_to_html_long(register uint16_t reg)
void send_sfp_info(uint8_t sfp)
{
// This loops over the Vendor-name, Vendor OUI, Vendor PN and Vendor rev ASCII fields
for (uint8_t i = 20; i < 60; i++) {
if (i >= 36 && i < 40) // Skip Non-ASCII codes
for (uint8_t i = 16; i < 64; i++) {
if (!(i & 0xf) && !sfp_read_block(sfp, i, 16))
return;
if (i < 20 || i >= 60 || (i >= 36 && i < 40)) // Skip Non-ASCII codes
continue;
uint8_t c = sfp_read_reg(sfp, i);
uint8_t c = sfp_buf[i & 0xf];
if (c && c != 0xa0) // a0 is the byte read from a non-existant I2C EEPROM
char_to_html(c);
}
@@ -192,32 +196,11 @@ void sfp_send_data(uint8_t slot, uint8_t reg, uint8_t len)
if (len > 16)
return;
if (reg & 0x80) { // Configure SFP readings address (0x51) as I2C device address
reg &= 0x7f;
REG_WRITE(RTL837X_REG_I2C_CTRL, 0x00, 0x1 << (I2C_MEM_ADDR_WIDTH-16) | (len - 1) & 0xf, 0x51 >> 5, (0x51 << 3) & 0xff);
} else {
REG_WRITE(RTL837X_REG_I2C_CTRL, 0x00, 0x1 << (I2C_MEM_ADDR_WIDTH-16) | (len - 1) & 0xf, 0x50 >> 5, (0x50 << 3) & 0xff);
}
if (!sfp_read_block(slot, reg, len))
return;
reg_read_m(RTL837X_REG_I2C_CTRL);
sfr_mask_data(1, 0xfc, i2c_bus_from_scl_pin(machine.sfp_port[slot].i2c.scl) << 5 | i2c_bus_from_sda_pin(machine.sfp_port[slot].i2c.sda) << 2);
reg_write_m(RTL837X_REG_I2C_CTRL);
REG_WRITE(RTL837X_REG_I2C_IN, 0, 0, 0, reg);
// Execute I2C Read
reg_bit_set(RTL837X_REG_I2C_CTRL, 0);
// Wait for execution to finish
do {
reg_read_m(RTL837X_REG_I2C_CTRL);
} while (sfr_data[3] & 0x1);
for (uint8_t i = 0; i < len; i++) {
if (!(i & 0x3))
reg_read_m(RTL837X_REG_I2C_OUT + i);
byte_to_html(sfr_data[3 - (i & 0x3)]);
}
for (uint8_t i = 0; i < len; i++)
byte_to_html(sfp_buf[i]);
}
@@ -252,6 +235,12 @@ void send_basic_info(void)
byte_to_html(uip_ethaddr.addr[3]); char_to_html(':');
byte_to_html(uip_ethaddr.addr[4]); char_to_html(':');
byte_to_html(uip_ethaddr.addr[5]);
slen += strtox(outbuf + slen, "\",\"hostname\":\"");
{
__xdata char *hp = hostname; /* sanitized on ingest, emit verbatim */
while (*hp)
char_to_html(*hp++);
}
slen += strtox(outbuf + slen, "\",\"sw_ver\":\"");
slen += strtox(outbuf + slen, VERSION_SW);
slen += strtox(outbuf + slen, "\",\"build_date\":\"");
@@ -302,17 +291,26 @@ void send_vlan(uint16_t vlan)
slen += strtox(outbuf + slen, "\"}");
}
void send_counters(char port)
/* Send counters
* Only accepts physical port 1..9.
* Returns an error if the port physical don't exists.
*/
bool send_counters(uint8_t phys_port)
{
dbg_string("send_counters called: "); dbg_byte(port); dbg_char('\n');
uint8_t phys_port_idx = phys_port - 1;
if (phys_port_idx > 8)
goto err;
uint8_t log_port = machine.phys_to_log_port[phys_port_idx];
if (log_port == 0)
goto err;
dbg_string("send_counters called: "); dbg_byte(phys_port_idx); dbg_char('\n');
slen = strtox(outbuf, HTTP_RESPONCE_JSON);
dbg_string("sending counters\n");
dbg_byte(port);
uint8_t i = machine.phys_to_log_port[port];
slen += strtox(outbuf + slen, "[");
dbg_string("sending counters\n"); dbg_byte(phys_port_idx);
char_to_html('[');
for (uint8_t counter = 0; counter < 0x37; counter++) {
STAT_GET(counter, i);
STAT_GET(counter, log_port);
slen += strtox(outbuf + slen, "\"0x");
reg_to_html(RTL837X_STAT_V_HIGH);
reg_to_html_long(RTL837X_STAT_V_LOW);
@@ -321,6 +319,12 @@ void send_counters(char port)
char_to_html(',');
}
char_to_html(']');
return false;
err:
dbg_string("Error: counters: phy_port_idx don't exists\n");
return true;
}
@@ -349,6 +353,7 @@ void send_l2(uint16_t idx)
*/
__xdata uint16_t entry = idx & 0xfff;
__xdata uint16_t first_entry = 0xffff; // An illegal entry index
__bit first = true;
char_to_html('[');
while (1) {
entries_left--;
@@ -362,9 +367,22 @@ void send_l2(uint16_t idx)
} while (sfr_data[3] & TBL_EXECUTE);
reg_read_m(RTL837x_L2_DATA_OUT_B);
if ((sfr_data[0] & 0x20)) { // Check entry is valid
__bit valid = (sfr_data[0] & 0x20) != 0;
if (valid) {
/* separator + 74-byte worst-case entry + closing "]" */
if (slen + 76 > TCP_OUTBUF_SIZE)
break;
if (!first)
char_to_html(',');
first = false;
// VLAN, taken from the read above instead of reading the register twice
slen += strtox(outbuf + slen, "{\"vlan\":\"");
charhex_to_html(sfr_data[0] & 0x0f);
byte_to_html(sfr_data[1]);
// MAC
slen += strtox(outbuf + slen, "{\"mac\":\"");
slen += strtox(outbuf + slen, "\",\"mac\":\"");
byte_to_html(sfr_data[2]); char_to_html(':');
byte_to_html(sfr_data[3]); char_to_html(':');
port = (sfr_data[0] >> 6) & 0x3;
@@ -374,47 +392,35 @@ void send_l2(uint16_t idx)
byte_to_html(sfr_data[2]); char_to_html(':');
byte_to_html(sfr_data[3]);
// VLAN
slen += strtox(outbuf + slen, "\",\"vlan\":\"");
reg_read_m(RTL837x_L2_DATA_OUT_B);
charhex_to_html(sfr_data[0] & 0x0f);
byte_to_html(sfr_data[1]);
// type
reg_read_m(RTL837x_L2_DATA_OUT_C);
if (sfr_data[2] & 0x1)
if (sfr_data[1] & 0x1)
slen += strtox(outbuf + slen, "\",\"type\":\"s\",\"port\":");
else
slen += strtox(outbuf + slen, "\",\"type\":\"l\",\"port\":");
port |= (sfr_data[3] & 0x3) << 2;
itoa_html(port);
}
// Index
reg_read_m(RTL837x_TBL_DATA_0);
entry = (((uint16_t)sfr_data[2] & 0x0f) << 8) | sfr_data[3];
if (valid) {
slen += strtox(outbuf + slen, ",\"idx\":\"");
byte_to_html(entry >> 8);
byte_to_html(entry);
char_to_html('"');
char_to_html('}');
}
entry += 1; // We want the next entry following after the current entry
} else {
reg_read_m(RTL837x_TBL_DATA_0);
entry = (((uint16_t)sfr_data[2] & 0x0f) << 8) | sfr_data[3] + 1;
}
if (first_entry == 0xffff) {
char_to_html(',');
if (first_entry == 0xffff)
first_entry = entry;
} else {
if (first_entry == entry || !entries_left) {
char_to_html(']');
else if (first_entry == entry || !entries_left)
break;
} else {
char_to_html(',');
}
}
}
char_to_html(']');
}
@@ -511,8 +517,7 @@ void send_lag(void)
slen += strtox(outbuf + slen, "{\"lagNum\":");
itoa_html(l);
slen += strtox(outbuf + slen, ",\"members\":\"");
reg_read_m(RTL837X_TRK_MBR_CTRL_BASE + (l << 2));
uint16_t ports = ((uint16_t)sfr_data[2] << 8) | sfr_data[3];
uint16_t ports = port_lag_members_get(l);
for (uint8_t i = 0; i < 16; i++) {
bool_to_html(!!(ports & 0x8000));
ports <<= 1;
@@ -527,6 +532,119 @@ void send_lag(void)
}
static __xdata uint32_t pi_u32;
static __xdata uint8_t pi_prio, pi_ext;
static __xdata uint8_t * __xdata pi_mac;
static void u32hex_html(void)
{
__xdata uint8_t *b = (__xdata uint8_t *)&pi_u32;
byte_to_html(b[3]);
byte_to_html(b[2]);
byte_to_html(b[1]);
byte_to_html(b[0]);
}
static void bridge_to_html(void)
{
byte_to_html(pi_prio);
byte_to_html(pi_ext);
for (uint8_t i = 0; i < 6; i++)
byte_to_html(pi_mac[i]);
}
void send_stp(void)
{
uint8_t i, j, st, dsg;
dbg_string("send_stp called\n");
slen = strtox(outbuf, HTTP_RESPONCE_JSON);
slen += strtox(outbuf + slen, "{\"on\":");
bool_to_html(stp_enabled);
slen += strtox(outbuf + slen, ",\"rstp\":");
bool_to_html(stp_rstp);
slen += strtox(outbuf + slen, ",\"prio\":");
itoa_html(stp_prio >> 4);
slen += strtox(outbuf + slen, ",\"hello\":");
itoa_html(stp_hello_s);
slen += strtox(outbuf + slen, ",\"maxage\":");
itoa_html(stp_maxage_s);
slen += strtox(outbuf + slen, ",\"fwd\":");
itoa_html(stp_fwddelay_s);
slen += strtox(outbuf + slen, ",\"txhold\":");
itoa_html(stp_txhold);
slen += strtox(outbuf + slen, ",\"rootPrio\":\"");
byte_to_html(root_bridge.prio);
byte_to_html(root_bridge.ext);
slen += strtox(outbuf + slen, "\",\"rootMac\":\"");
for (j = 0; j < 6; j++)
byte_to_html(root_bridge.mac[j]);
slen += strtox(outbuf + slen, "\",\"myMac\":\"");
for (j = 0; j < 6; j++)
byte_to_html(uip_ethaddr.addr[j]);
slen += strtox(outbuf + slen, "\",\"cost\":\"");
byte_to_html(root_bridge_cost >> 24);
byte_to_html(root_bridge_cost >> 16);
byte_to_html(root_bridge_cost >> 8);
byte_to_html(root_bridge_cost);
slen += strtox(outbuf + slen, "\",\"weRoot\":");
bool_to_html(stp_root_port == 0xff ? 1 : 0);
slen += strtox(outbuf + slen, ",\"rootPort\":");
itoa_html(stp_root_port == 0xff ? 0 : machine.log_to_phys_port[stp_root_port]);
slen += strtox(outbuf + slen, ",\"tc\":\"");
byte_to_html(stp_tc_count >> 8);
byte_to_html(stp_tc_count);
slen += strtox(outbuf + slen, "\",\"ports\":[");
reg_read_m(RTL837X_MSTP_STATES);
for (i = machine.min_port; i <= machine.max_port; i++) {
slen += strtox(outbuf + slen, "{\"p\":");
itoa_html(machine.log_to_phys_port[i]);
slen += strtox(outbuf + slen, ",\"st\":");
st = (sfr_data[3 - (i >> 2)] >> ((i << 1) & 0x7)) & 0x3;
itoa_html(st);
slen += strtox(outbuf + slen, ",\"role\":");
if (!(stp_pflags[i] & STP_PF_ENABLED) || (stp_pflags[i] & STP_PF_TRIPPED))
itoa_html(0);
else if (i == stp_root_port)
itoa_html(1);
else if (st == 3)
itoa_html(2);
else
itoa_html(3);
slen += strtox(outbuf + slen, ",\"f\":");
itoa_html(stp_pflags[i]);
slen += strtox(outbuf + slen, ",\"pc\":\"");
pi_u32 = stp_pcost[i]; u32hex_html();
slen += strtox(outbuf + slen, "\",\"prio\":");
itoa_html(stp_pprio[i]);
slen += strtox(outbuf + slen, ",\"p2\":");
itoa_html(stp_pp2p[i]);
dsg = stp_dpid[i] && stp_bpdu_age[i] < (uint16_t)stp_maxage_s * STP_HZ;
slen += strtox(outbuf + slen, ",\"db\":\"");
if (dsg) {
pi_prio = stp_dbridge[i].prio; pi_ext = stp_dbridge[i].ext;
pi_mac = stp_dbridge[i].mac;
} else {
pi_prio = stp_prio; pi_ext = 0;
pi_mac = uip_ethaddr.addr;
}
bridge_to_html();
slen += strtox(outbuf + slen, "\",\"dp\":\"");
byte_to_html(dsg ? (stp_dpid[i] >> 8) : stp_pprio[i]);
byte_to_html(dsg ? stp_dpid[i] : (i + 1));
slen += strtox(outbuf + slen, "\",\"dc\":\"");
pi_u32 = dsg ? stp_dcost[i] : root_bridge_cost; u32hex_html();
slen += strtox(outbuf + slen, "\"},");
}
slen -= 1; // remove comma
slen += strtox(outbuf + slen, "]}");
}
void send_eee(void)
{
dbg_string("send_eee called\nsending EEE status\n");
@@ -660,52 +778,53 @@ void send_status(void)
slen += strtox(outbuf + slen, "\"");
if (machine.is_sfp[i]) {
uint8_t sfp = machine.is_sfp[i] - 1;
slen += strtox(outbuf + slen, ",\"isSFP\":1,\"enabled\":");
if (!(sfp_pins_last & (0x1 << ((machine.is_sfp[i] - 1) << 2)))) {
if (!(sfp_pins_last & (0x1 << (sfp << 2)))) {
bool_to_html(1);
slen += strtox(outbuf + slen,",\"sfp_options\":\"0x");
byte_to_html(sfp_options[machine.is_sfp[i]-1]);
if (sfp_options[machine.is_sfp[i]-1] & 0x40) {
byte_to_html(sfp_options[sfp]);
if (sfp_options[sfp] & 0x40) {
slen += strtox(outbuf + slen,"\",\"sfp_temp\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 224, 2);
sfp_send_data(sfp, 224, 2);
slen += strtox(outbuf + slen,"\",\"sfp_vcc\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 226, 2);
sfp_send_data(sfp, 226, 2);
slen += strtox(outbuf + slen,"\",\"sfp_txbias\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 228, 2);
sfp_send_data(sfp, 228, 2);
slen += strtox(outbuf + slen,"\",\"sfp_txpower\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 230, 2);
sfp_send_data(sfp, 230, 2);
slen += strtox(outbuf + slen,"\",\"sfp_rxpower\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 232, 2);
if (sfp_options[machine.is_sfp[i]-1] & 0x10) {
sfp_send_data(sfp, 232, 2);
if (sfp_options[sfp] & 0x10) {
slen += strtox(outbuf + slen,"\",\"sfp_temp_cal\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 212, 4);
sfp_send_data(sfp, 212, 4);
slen += strtox(outbuf + slen,"\",\"sfp_vcc_cal\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 216, 4);
sfp_send_data(sfp, 216, 4);
slen += strtox(outbuf + slen,"\",\"sfp_txbias_cal\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 204, 4);
sfp_send_data(sfp, 204, 4);
slen += strtox(outbuf + slen,"\",\"sfp_txpower_cal\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 208, 4);
sfp_send_data(sfp, 208, 4);
slen += strtox(outbuf + slen,"\",\"sfp_rxpower_cal\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 184, 16);
sfp_send_data(machine.is_sfp[i] - 1, 200, 4);
sfp_send_data(sfp, 184, 16);
sfp_send_data(sfp, 200, 4);
}
slen += strtox(outbuf + slen,"\",\"sfp_state\":\"0x");
sfp_send_data(machine.is_sfp[i] - 1, 238, 1);
sfp_send_data(sfp, 238, 1);
}
slen += strtox(outbuf + slen,"\",\"sfp_vendor\":\"");
for (register uint8_t s = 0; s < 16; s++)
outbuf[slen++] = sfp_module_vendor[machine.is_sfp[i]-1][s];
for (uint8_t s = 0; s < 16 && sfp_module_vendor[sfp][s]; s++)
outbuf[slen++] = sfp_module_vendor[sfp][s];
slen += strtox(outbuf + slen,"\",\"sfp_model\":\"");
for (register uint8_t s = 0; s < 16; s++)
outbuf[slen++] = sfp_module_model[machine.is_sfp[i]-1][s];
for (uint8_t s = 0; s < 16 && sfp_module_model[sfp][s]; s++)
outbuf[slen++] = sfp_module_model[sfp][s];
slen += strtox(outbuf + slen,"\",\"sfp_serial\":\"");
for (register uint8_t s = 0; s < 16; s++)
outbuf[slen++] = sfp_module_serial[machine.is_sfp[i]-1][s];
for (uint8_t s = 0; s < 16 && sfp_module_serial[sfp][s]; s++)
outbuf[slen++] = sfp_module_serial[sfp][s];
slen += strtox(outbuf + slen,"\",\"sfp_los\":");
if (machine.sfp_port[machine.is_sfp[i]-1].pin_los == GPIO_NA) {
if (machine.sfp_port[sfp].pin_los == GPIO_NA) {
slen += strtox(outbuf + slen,"null");
} else {
bool_to_html(sfp_pins_last & (0x2 << (((machine.is_sfp[i]-1) << 2))));
bool_to_html(sfp_pins_last & (0x2 << (sfp << 2)));
}
} else {
bool_to_html(0);
@@ -813,7 +932,7 @@ found_end:
if (valid_len > (TCP_OUTBUF_SIZE - slen)) {
cont_len = valid_len - (TCP_OUTBUF_SIZE - slen);
valid_len = TCP_OUTBUF_SIZE - slen;
cont_addr = valid_len;
cont_addr = CONFIG_START + valid_len;
}
flash_region.addr = CONFIG_START;
@@ -850,7 +969,9 @@ void send_vlanlist(void)
uint8_t first = 1;
slen = strtox(outbuf, HTTP_RESPONCE_JSON);
char_to_html('[');
slen += strtox(outbuf + slen, "{\"mgmt\":");
itoa16_html(management_vlan);
slen += strtox(outbuf + slen, ",\"vlan\":[");
for (i = 1; i < 4095; i++) {
if (vlan_get(i) < 0)
@@ -858,7 +979,7 @@ void send_vlanlist(void)
if (!(sfr_data[0] & 0x02)) /* bit 1: VLAN table entry valid */
continue;
if (slen + 139 > TCP_OUTBUF_SIZE) /* 138 bytes worst-case entry + 1 byte for closing ']' */
if (slen + 141 > TCP_OUTBUF_SIZE) /* comma + 138-byte worst-case entry + closing "]}" */
break;
if (!first)
@@ -880,4 +1001,5 @@ void send_vlanlist(void)
}
char_to_html(']');
char_to_html('}');
}
+4 -1
View File
@@ -1,7 +1,9 @@
#ifndef __PAGE_IMPL_H__
#define __PAGE_IMPL_H__
void send_counters(char port);
#include <stdbool.h>
bool send_counters(uint8_t phys_port);
void send_status(void);
void send_vlan(uint16_t vlan);
void send_basic_info(void);
@@ -14,6 +16,7 @@ void send_mtu(void);
void send_config(void);
void send_cmd_log(void);
void send_lag(void);
void send_stp(void);
void send_vlanlist(void);
/* Convert only the lower nibble to ascii HEX char.
+1 -1
View File
@@ -213,7 +213,7 @@ uint8_t flash_read_status(void)
* Reads bulk data of length len from the flash memory starging at address src
* and writes the data into a buffer pointed to by dst in XMEM
*/
void flash_read_bulk(register __xdata uint8_t *dst, __xdata uint32_t src, register uint16_t len)
void flash_read_bulk(__xdata uint8_t *dst, __xdata uint32_t src, uint16_t len)
{
short status;
do {
+310 -58
View File
@@ -5,9 +5,23 @@
#include "rtl837x_regs.h"
#include "rtl837x_common.h"
#ifdef MACHINE_KP_9000_6XHML_X2
#if defined(MACHINE_KP_9000_6XHML_X2) || \
defined(MACHINE_KP_9000_6XH_X2_V1_1) || \
defined(MACHINE_KP_9000_6XHML_X2_V1_1) || \
defined(MACHINE_KP_9000_6XH_X2_V1_2) || \
defined(MACHINE_KP_9000_6XHML_X2_V1_2)
__code const struct machine machine = {
#if defined(MACHINE_KP_9000_6XH_X2_V1_1)
.machine_name = "keepLink KP-9000-6XH V1.1",
#elif defined(MACHINE_KP_9000_6XHML_X2_V1_1)
.machine_name = "keepLink KP-9000-6XHML V1.1",
#elif defined(MACHINE_KP_9000_6XH_X2_V1_2)
.machine_name = "keepLink KP-9000-6XH V1.2",
#elif defined(MACHINE_KP_9000_6XHML_X2_V1_2)
.machine_name = "keepLink KP-9000-6XHML V1.2",
#else
.machine_name = "keepLink KP-9000-6XHML-X2",
#endif
.isRTL8373 = 0,
.min_port = 3,
.max_port = 8,
@@ -42,8 +56,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_KP_9000_6XH_X
__code const struct machine machine = {
.machine_name = "keepLink KP-9000-6XH-X",
@@ -72,11 +84,15 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_KP_9000_6XH_X2
#elif defined(MACHINE_KP_9000_6XH_X2) || defined(MACHINE_KP_9000_6XH_X2_V2_1) || defined(MACHINE_KP_9000_6XHML_X2_V2_1)
__code const struct machine machine = {
#if defined(MACHINE_KP_9000_6XHML_X2_V2_1)
.machine_name = "keepLink KP-9000-6XHML V2.1",
#elif defined(MACHINE_KP_9000_6XH_X2_V2_1)
.machine_name = "keepLink KP-9000-6XH-X2 V2.1",
#else
.machine_name = "keepLink KP-9000-6XH-X2",
#endif
.isRTL8373 = 0,
.min_port = 3,
.max_port = 8,
@@ -122,10 +138,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) {
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
}
#elif defined MACHINE_KP_9000_9XH_X_EU
__code const struct machine machine = {
.machine_name = "keepLink KP-9000-9XH-X-EU",
@@ -151,8 +163,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_KP_9000_9XHML_X_V2_2
__code const struct machine machine = {
.machine_name = "keepLink KP-9000-9XHML-X V2.2",
@@ -204,8 +214,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_KP_9000_9XHML_X_V3_1
__code const struct machine machine = {
.machine_name = "keepLink KP-9000-9XHML-X V3.1",
@@ -241,8 +249,6 @@ __code const struct machine machine = {
0x1a, 0x19, 0x1d, 0x1e, 0x1c, 0x1d, 0x20, 0x21},
};
void machine_custom_init(void) { }
#elif defined MACHINE_SWGT024_V2_0_MANAGED
__code const struct machine machine = {
.machine_name = "SWGT024 V2.0 Managed",
@@ -284,8 +290,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_SWGT024_V2_0_UNMANAGED
__code const struct machine machine = {
.machine_name = "SWGT024 V2.0 Unmanaged",
@@ -327,8 +331,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_SWTG018AS_A_V_2_0
__code const struct machine machine = {
.machine_name = "SWTG018AS-A V2.0",
@@ -344,7 +346,7 @@ __code const struct machine machine = {
.sfp_port[0].pin_tx_disable = GPIO_NA,
.sfp_port[0].sds = 1,
.sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 },
.reset_pin = GPIO_NA,
.reset_pin = GPIO48_I2C_SCL1,
.high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 },
.port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1},
.led_sets = {
@@ -368,8 +370,6 @@ __code const struct machine machine = {
0x1d, 0x20, 0x21 },
};
void machine_custom_init(void) { }
#elif defined MACHINE_HG0402XG_V1_1
__code const struct machine machine = {
.machine_name = "HG0402XG V1.1",
@@ -408,13 +408,12 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_SWTGW218AS
__code const struct machine machine = {
.machine_name = "SWTGW218AS 8+1 Managed Switch",
.isRTL8373 = 1,
.mac_flash_offset = 0x1FC000,
.min_port = 0,
.max_port = 8,
.n_sfp = 1,
@@ -442,7 +441,48 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_PCB_SWTG018AS_V2_1_0 // Sold as Sodola SL902 / Horaco "SWTGW218AS"; the SWTGW218AS label also covers other PCBs with different SFP and LED wiring (see MACHINE_SWTGW218AS)
__code const struct machine machine = {
.machine_name = "SWTGW218AS (SWTG018AS-V2.1.0)",
.isRTL8373 = 1,
.mac_flash_offset = 0x1FC000,
.min_port = 0,
.max_port = 8,
.n_sfp = 1,
.log_to_phys_port = {1, 2, 3, 4, 5, 6, 7, 8, 9},
.phys_to_log_port = {0, 1, 2, 3, 4, 5, 6, 7, 8},
.is_sfp = {0, 0, 0, 0, 0, 0, 0, 0, 1},
.sfp_port[0].pin_detect = GPIO38, // pulled low on module insert
.sfp_port[0].pin_los = GPIO_NA, // no LOS pin wired
.sfp_port[0].pin_tx_disable = GPIO_NA,
.sfp_port[0].sds = 1,
.sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 },
.reset_pin = GPIO54_ACL_BIT2_EN,
.high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 },
.port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1},
// LED wiring matches the SWTG018AS-A V2.0 (same PCB family)
.led_sets = {
{ /* RJ45: First LED, yellow, second LED: green */
LEDS_2G5 | LEDS_LINK,
LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
0,
0,
}, { /* SFP set (superseded by the raw register override in machine_custom_init) */
LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT | LEDS_10G,
0,
0,
0,
}},
.led_mux_custom = 1,
.led_mux = { 0x00, 0x01, 0x04, 0x05, 0x08, // 65e0
0x09, 0x0c, 0x09, 0x0d, 0x10, // 65e4
0x11, 0x0e, 0x14, 0x11, 0x12, // 65e8
0x15, 0x15, 0x16, 0x18, 0x19, // 65ec
0x1a, 0x19, 0x1d, 0x1e, 0x1c, // 65f0
0x1d, 0x20, 0x21 },
};
#elif defined MACHINE_LIANGUO_ZX_SWTGW215AS // Has PCB branded PCB-SWTG115AS-V2.0 but is labeled and reports as a ZX-SWTGW215AS, seems to be identical to the "real" ZX-SWTGW215AS except for the LEDs
__code const struct machine machine = {
.machine_name = "Lianguo ZX-SWTGW215AS",
@@ -475,8 +515,6 @@ __code const struct machine machine = {
.led_mux_custom = 0,
};
void machine_custom_init(void) { }
#elif defined MACHINE_DEFAULT_8C_1SFP
__code const struct machine machine = {
.machine_name = "8+1 SFP Port Switch",
@@ -502,8 +540,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_TRENDNET_TEG_S562
__code const struct machine machine = {
.machine_name = "Trendnet TEG-S562",
@@ -543,8 +579,6 @@ __code const struct machine machine = {
};
void machine_custom_init(void) { }
#elif defined(MACHINE_PCB_K0402WS_V3) || defined(MACHINE_HI_K0402WS) // Sold as a variety of devices, see doc/
__code const struct machine machine = {
.machine_name = "PCB-K0402WS-V3.0",
@@ -591,10 +625,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) {
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
}
#elif defined MACHINE_K0501W_V2_0
__code const struct machine machine = {
.machine_name = "K0501W V2.0",
@@ -629,8 +659,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_ZX310S_4T2XH
__code const struct machine machine = {
.machine_name = "ZX310S-4T2XH",
@@ -674,7 +702,55 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_STEAMEMO_IG204_V1
__code const struct machine machine = {
.machine_name = "Steamemo IG204 V1",
.isRTL8373 = 0,
.min_port = 3,
.max_port = 8,
.n_sfp = 2,
.log_to_phys_port = {0, 0, 0, 6, 1, 2, 3, 4, 5},
.phys_to_log_port = {4, 5, 6, 7, 8, 3, 0, 0, 0},
.is_sfp = {0, 0, 0, 2, 0, 0, 0, 0, 1},
// Left SFP port (5)
// LED pin 9
.sfp_port[0].pin_detect = GPIO30_ACL_BIT3_EN,
.sfp_port[0].pin_los = GPIO37,
.sfp_port[0].pin_tx_disable = GPIO_NA,
.sfp_port[0].sds = 1,
.sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 },
// Right SFP port (6)
// LED pin 24
.sfp_port[1].pin_detect = GPIO50_I2C_SCL2_UART1_TX,
.sfp_port[1].pin_los = GPIO51_I2C_SDA2_UART1_RX,
.sfp_port[1].pin_tx_disable = GPIO_NA,
.sfp_port[1].sds = 0,
.sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 },
.reset_pin = GPIO_NA,
.high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 },
.port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1},
.led_sets = {
{
// RJ45 Amber LED (left)
LEDS_2G5 | LEDS_LINK,
// RJ45 Green LED (Right)
LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
0,
0,
},
{
// SFP LED
LEDS_10G | LEDS_5G | LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_LINK | LEDS_ACT,
0, // unused
0, // unused
0
},
},
.led_mux_custom = 1,
.led_mux = {
0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x0f, 0x20, 0x0d, 0x0e, 0x10, 0x11, 0x12, 0x14, 0x15, 0x16, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0c, 0x21, 0x22, 0x23
},
};
#elif defined MACHINE_HI_K0801WS
__code const struct machine machine = {
@@ -725,8 +801,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) { }
#elif defined MACHINE_FNS1200P
__code const struct machine machine = {
.machine_name = "FNS-1200P",
@@ -782,11 +856,6 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void)
{
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
}
#elif defined MACHINE_PCB_SWTG024AS_A_2_0_1
__code const struct machine machine = {
@@ -836,13 +905,91 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void)
{
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
reg_bit_set(RTL837X_REG_LED_MODE, 17);
reg_bit_clear(RTL837X_REG_LED_MODE, 9);
reg_bit_clear(RTL837X_REG_LED_MODE, 7);
}
#elif defined MACHINE_SWTG024AS_A_2_0_1_5C_1SFP
__code const struct machine machine = {
.machine_name = "SWTG024AS-A-V2.0.1-5C-1SFP",
.isRTL8373 = 0,
.min_port = 3,
.max_port = 8,
.n_sfp = 1,
.log_to_phys_port = {0, 0, 0, 5, 1, 2, 3, 4, 6},
.phys_to_log_port = {4, 5, 6, 7, 3, 8, 0, 0, 0},
.is_sfp = {0, 0, 0, 0, 0, 0, 0, 0, 1},
.sfp_port[0].pin_detect = GPIO38,
.sfp_port[0].pin_los = GPIO_NA,
.sfp_port[0].pin_tx_disable = GPIO_NA,
.sfp_port[0].sds = 1,
.sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 },
.reset_pin = GPIO_NA,
.high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_27 | LED_28_SYS | LED_29 },
.port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1},
/* Ports 1-5 RJ45 use set 0, port 9 SFP uses set 1
* Ports 1-5: Green: 2.5GBit, Amber: 10/100/1000MBit
* SFP-port: Blue: 10GBit, Green: 100MBit-2.5GBit
*/
.led_sets = {
{
LEDS_2G5 | LEDS_LINK | LEDS_ACT,
LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
LEDS_DUPLEX,
LEDS_2G5 | LEDS_LINK | LEDS_ACT
},
{
LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_LINK | LEDS_ACT,
LEDS_10G | LEDS_LINK | LEDS_ACT,
LEDS_2G5 | LEDS_LINK,
LEDS_COL | LEDS_DUPLEX
},
},
.led_mux_custom = 1,
.led_mux = {
0x00,0x01,0x04,0x05,0x08,0x09,0x0c,0x3f,0x0d,0x10,0x11,0x0e,0x14,0x11,0x12,0x15,0x15,0x16,0x18,0x19,0x1a,0x19,0x1d,0x1e,0x1c,0x1d,0x20,0x21
},
};
#elif defined MACHINE_SWTG024AS_V2_0
__code const struct machine machine = {
.machine_name = "SWTG024AS-V2.0",
.isRTL8373 = 0,
.min_port = 3,
.max_port = 8,
.n_sfp = 1,
.log_to_phys_port = {0, 0, 0, 5, 1, 2, 3, 4, 6},
.phys_to_log_port = {4, 5, 6, 7, 3, 8, 0, 0, 0},
.is_sfp = {0, 0, 0, 0, 0, 0, 0, 0, 1},
.sfp_port[0].pin_detect = GPIO30_ACL_BIT3_EN,
.sfp_port[0].pin_los = GPIO37,
.sfp_port[0].pin_tx_disable = GPIO_NA,
.sfp_port[0].sds = 1,
.sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 },
.reset_pin = GPIO_NA,
.high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_27 | LED_28_SYS | LED_29 },
.port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1},
/* Ports 1-5 RJ45 use set 0, port 9 SFP uses set 1
* Ports 1-5: Green: 2.5GBit, Amber: 10/100/1000MBit
* SFP-port: Blue: 10GBit, Amber: 100MBit-2.5GBit
*/
.led_sets = {
{
LEDS_2G5 | LEDS_LINK | LEDS_ACT,
LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
LEDS_DUPLEX,
LEDS_2G5 | LEDS_LINK | LEDS_ACT
},
{
LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_LINK | LEDS_ACT,
LEDS_10G | LEDS_LINK | LEDS_ACT,
LEDS_2G5 | LEDS_LINK,
LEDS_COL | LEDS_DUPLEX
},
},
.led_mux_custom = 1,
.led_mux = {
0x00,0x01,0x04,0x05,0x08,0x09,0x0c,0x3f,0x0d,0x10,0x11,0x0e,0x14,0x11,0x12,0x15,0x15,0x16,0x18,0x19,0x1a,0x19,0x1d,0x1e,0x1c,0x1d,0x20,0x21
},
};
#elif defined MACHINE_ZX310S_4T2XT
__code const struct machine machine = {
@@ -881,11 +1028,116 @@ __code const struct machine machine = {
},
};
void machine_custom_init(void) {
// For this device, the reset value of RTL837X_PIN_MUX_0 is 0x30000000,
// which would disables all LEDS, enable them manually:
REG_SET(RTL837X_PIN_MUX_0, 0x30db68bf);
}
#elif defined MACHINE_FG_4GT_2SX_V2_0
__code const struct machine machine = {
.machine_name = "FG-4GT-2SX_V2.0",
.isRTL8373 = 0,
.min_port = 3,
.max_port = 8,
.n_sfp = 2,
.log_to_phys_port = {0, 0, 0, 6, 1, 2, 3, 4, 5},
.phys_to_log_port = {4, 5, 6, 7, 8, 3, 0, 0, 0},
.is_sfp = {0, 0, 0, 2, 0, 0, 0, 0, 1},
// Left SFP port
.sfp_port[0].pin_detect = GPIO38,
.sfp_port[0].pin_los = GPIO_NA,
.sfp_port[0].sds = 1,
.sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 },
// Right SFP port
.sfp_port[1].pin_detect = GPIO37,
.sfp_port[1].pin_los = GPIO_NA,
.sfp_port[1].sds = 0,
.sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 },
.reset_pin = GPIO_NA,
.high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 },
.port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1},
/* Ports 1-4 RJ45 use set 0, port 5-6 SFP uses set 1
* Ports 1-4: Green: 2.5GBit, Amber: 10/100/1000MBit
* Ports 5-6: Green: 100MBit-10GBit
*/
.led_sets = {
{
LEDS_2G5 | LEDS_LINK | LEDS_ACT,
LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
0,
LEDS_2G5 | LEDS_LINK | LEDS_ACT
},
{
LEDS_10G | LEDS_5G | LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
LEDS_10G | LEDS_LINK,
0,
LEDS_COL | LEDS_DUPLEX
},
{
LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
LEDS_2G5 | LEDS_1G | LEDS_LINK,
LEDS_5G | LEDS_2G5 | LEDS_LINK | LEDS_ACT,
LEDS_10G | LEDS_LINK | LEDS_ACT
},
{
LEDS_TX,
LEDS_RX,
LEDS_10G | LEDS_TWO_PAIR_5G | LEDS_5G | LEDS_TWO_PAIR_2G5 |
LEDS_2G5 | LEDS_TWO_PAIR_1G | LEDS_1G | LEDS_500M | LEDS_100M | LEDS_10M | LEDS_ACT,
LEDS_10G | LEDS_TWO_PAIR_5G | LEDS_5G | LEDS_TWO_PAIR_2G5 |
LEDS_2G5 | LEDS_TWO_PAIR_1G | LEDS_1G | LEDS_500M | LEDS_100M | LEDS_10M | LEDS_LINK
},
},
.led_mux_custom = 1,
.led_mux = {
0x0c, 0x0d, 0x0e, 0x10, 0x11, 0x12, 0x14, 0x3f, 0x15, 0x16,
0x18, 0x0e, 0x19, 0x11, 0x12, 0x1a, 0x15, 0x16, 0x1c, 0x19,
0x1a, 0x1d, 0x1d, 0x1e, 0x1e, 0x20, 0x21, 0x22
},
};
#elif defined MACHINE_FG_8GT_1SX
__code const struct machine machine = {
.machine_name = "FG-8GT-1SX",
.isRTL8373 = 1,
.min_port = 0,
.max_port = 8,
.n_sfp = 1,
.log_to_phys_port = {1, 2, 3, 4, 5, 6, 7, 8, 9},
.phys_to_log_port = {0, 1, 2, 3, 4, 5, 6, 7, 8},
.is_sfp = {0, 0, 0, 0, 0, 0, 0, 0, 1},
.sfp_port[0].pin_detect = GPIO38,
.sfp_port[0].pin_los = GPIO_NA,
.sfp_port[0].pin_tx_disable = GPIO_NA,
.sfp_port[0].sds = 1,
.sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 },
.reset_pin = GPIO_NA,
.high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 },
.port_led_set = {0, 0, 0, 0, 0, 0, 0, 0, 1},
.led_sets = {
/*
Ports 1-8: Left Amber 10/100/1000MBit (Vendor no 10M, but we add it)
Right Green 2.5GBit
SFP-port: Any speed
*/
{
LEDS_2G5 | LEDS_LINK | LEDS_ACT,
LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
0,
0,
},
{
LEDS_10G | LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT,
0,
0,
0,
},
},
.led_mux_custom = 1,
.led_mux = {
0x00, 0x01, 0x04, 0x05, 0x08, 0x09, 0x0c, 0x09, 0x0d, 0x10,
0x11, 0x0e, 0x14, 0x11, 0x12, 0x15, 0x15, 0x16, 0x18, 0x19,
0x1a, 0x19, 0x1d, 0x1e, 0x1c, 0x1d, 0x20, 0x21,
},
};
#else
#error "Please select a machine type in machine.h"
+19 -2
View File
@@ -6,9 +6,19 @@
/*
* Select your machine type below
*/
// Legacy KP-9000 4+2 targets. Prefer the PCB-revision-specific targets below.
// #define MACHINE_KP_9000_6XHML_X2
// #define MACHINE_KP_9000_6XH_X
// #define MACHINE_KP_9000_6XH_X2
// KP-9000 4+2 targets by PCB silkscreen revision.
// #define MACHINE_KP_9000_6XH_X2_V1_1
// #define MACHINE_KP_9000_6XHML_X2_V1_1
// #define MACHINE_KP_9000_6XH_X2_V1_2
// #define MACHINE_KP_9000_6XHML_X2_V1_2
// #define MACHINE_KP_9000_6XH_X2_V2_1
// #define MACHINE_KP_9000_6XHML_X2_V2_1
// #define MACHINE_KP_9000_6XH_X
// #define MACHINE_KP_9000_9XH_X_EU
// #define MACHINE_KP_9000_9XHML_X_V2_2
// #define MACHINE_KP_9000_9XHML_X_V3_1
@@ -18,15 +28,21 @@
// #define MACHINE_HG0402XG_V1_1
// #define MACHINE_SWTG018AS_A_V_2_0
// #define MACHINE_SWTGW218AS
// #define MACHINE_PCB_SWTG018AS_V2_1_0
// #define MACHINE_PCB_K0402WS_V3
// #define MACHINE_K0501W_V2_0
// #define MACHINE_LIANGUO_ZX_SWTGW215AS
// #define MACHINE_ZX310S_4T2XH
// #define MACHINE_ZX310S_4T2XT
// #define MACHINE_STEAMEMO_IG204_V1
// #define MACHINE_DEFAULT_8C_1SFP
// #define MACHINE_HI_K0801WS
// #define MACHINE_FNS1200P
// #define MACHINE_PCB_SWTG024AS_A_2_0_1
// #define MACHINE_SWTG024AS_A_2_0_1_5C_1SFP
// #define MACHINE_SWTG024AS_V2_0
// #define MACHINE_FG_4GT_2SX_V2_0
// #define MACHINE_FG_8GT_1SX
typedef struct {
// GPIO pins for SDA/SCL
@@ -81,6 +97,7 @@ typedef struct machine {
uint32_t led_sets[4][4];
uint8_t led_mux_custom;
uint8_t led_mux[28];
uint32_t mac_flash_offset;
};
typedef struct machine_runtime
@@ -89,6 +106,6 @@ typedef struct machine_runtime
uint8_t isN : 1;
};
void machine_custom_init(void);
void machine_custom_init(void) __banked;
#endif
+122
View File
@@ -0,0 +1,122 @@
/*
* Per-machine one-shot boot hooks, hosted in BANK2 so board-specific
* tables and code do not consume the common bank.
*/
#include <stdint.h>
#include "machine.h"
#include "rtl837x_pins.h"
#include "rtl837x_leds.h"
#include "rtl837x_sfr.h"
#include "rtl837x_regs.h"
#include "rtl837x_common.h"
#pragma codeseg BANK2
#pragma constseg BANK2
#if defined(MACHINE_KP_9000_6XH_X2) || \
defined(MACHINE_KP_9000_6XH_X2_V2_1) || \
defined(MACHINE_KP_9000_6XHML_X2_V2_1)
void machine_custom_init(void) __banked
{
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
}
#elif defined MACHINE_PCB_SWTG018AS_V2_1_0
// Stock-firmware values for what the LED-set encoding cannot express: the
// bi-color SFP LED (blue pin at 10G) and the PIN_MUX_0 routing of that pin
// to the LED controller. Runs after leds_setup(), which covers the rest.
static __code const struct { uint16_t reg; uint32_t val; } custom_init_regs[] = {
{ RTL837X_REG_LED3_0_SET1, 0x00100000UL },
{ RTL837X_REG_LED1_0_SET1, 0x01400155UL },
{ RTL837X_REG_LED1_0_SET0, 0x01740141UL },
{ RTL837X_REG_LED_GLB_IO_EN, 0x7f24977fUL },
{ RTL837X_PIN_MUX_0, 0x20db6880UL },
};
void machine_custom_init(void) __banked
{
uint8_t i;
// REG_SET is a multi-statement macro without a do-while wrapper: braces required
for (i = 0; i < sizeof(custom_init_regs) / sizeof(custom_init_regs[0]); i++) {
REG_SET(custom_init_regs[i].reg, custom_init_regs[i].val);
}
}
#elif defined(MACHINE_PCB_K0402WS_V3) || defined(MACHINE_HI_K0402WS)
void machine_custom_init(void) __banked
{
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
}
#elif defined MACHINE_FNS1200P
void machine_custom_init(void) __banked
{
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
}
#elif defined MACHINE_PCB_SWTG024AS_A_2_0_1
void machine_custom_init(void) __banked
{
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
reg_bit_set(RTL837X_REG_LED_MODE, 17);
reg_bit_clear(RTL837X_REG_LED_MODE, 9);
reg_bit_clear(RTL837X_REG_LED_MODE, 7);
}
#elif defined MACHINE_SWTG024AS_A_2_0_1_5C_1SFP
void machine_custom_init(void) __banked
{
uint16_t pval;
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
reg_bit_set(RTL837X_REG_LED_MODE, 17);
reg_bit_clear(RTL837X_REG_LED_MODE, 9);
reg_bit_clear(RTL837X_REG_LED_MODE, 7);
// OEM firmware sets these companion SDS0 polarity bits for the RTL8221B.
sds_read(0, 0, 0);
pval = SFR_DATA_U16;
sds_write_v(0, 0, 0, pval | 0x100);
sds_read(0, 6, 2);
pval = SFR_DATA_U16;
sds_write_v(0, 6, 2, pval | 0x4000);
}
#elif defined MACHINE_SWTG024AS_V2_0
void machine_custom_init(void) __banked
{
uint16_t pval;
reg_bit_set(RTL837X_REG_LED_GLB_IO_EN, 6);
reg_bit_set(RTL837X_REG_LED_MODE, 17);
reg_bit_clear(RTL837X_REG_LED_MODE, 9);
reg_bit_clear(RTL837X_REG_LED_MODE, 7);
// OEM firmware sets these companion SDS0 polarity bits for the RTL8221B.
sds_read(0, 0, 0);
pval = SFR_DATA_U16;
sds_write_v(0, 0, 0, pval | 0x100);
sds_read(0, 6, 2);
pval = SFR_DATA_U16;
sds_write_v(0, 6, 2, pval | 0x4000);
}
#elif defined MACHINE_ZX310S_4T2XT
void machine_custom_init(void) __banked
{
// For this device, the reset value of RTL837X_PIN_MUX_0 is 0x30000000,
// which would disables all LEDS, enable them manually:
REG_SET(RTL837X_PIN_MUX_0, 0x30db68bf);
}
#elif defined MACHINE_FG_4GT_2SX_V2_0
void machine_custom_init(void) __banked
{
REG_SET(RTL837X_REG_LED_GLB_IO_EN, 0x7624155b);
}
#else
void machine_custom_init(void) __banked { }
#endif
+25 -8
View File
@@ -8,6 +8,7 @@
#define SYS_TICK_HZ 200
#define CPU_PORT 9
#define NUL '\0'
// Define Port-masks for 9-port devices and 6-port devices
#define PMASK_9 0x1ff
@@ -71,6 +72,9 @@ struct vlan_tag {
#define VLAN_TAG_SIZE (sizeof (struct vlan_tag))
#define RTL_FRAME_TAG_ID 0x8899
#define RTL_FRAME_TAG_VERSION 0x04
/* Bits of the tag's `flags` word, see doc/CpuPort.md. */
#define RTL_TAG_LEARN_DIS 0x0020 /* do not learn the source address from this frame */
#define RTL_TAG_KEEP 0x0080 /* keep the frame's 802.1Q tag format as injected */
// For TX, an 8 byte (plus 4 byte padding when when VLAN is enabled)
// header describing the frame to be moved to the Asic is used
@@ -115,11 +119,19 @@ struct flash_region_t {
extern __xdata char port_names[9][PORT_NAME_SIZE];
extern __xdata bool stp_enabled;
/* System hostname (device identity). Set via `hostname <text>` and the System
* Settings page, reported in /information.json. Other modules (e.g. LLDP, which
* advertises it as the System Name TLV) read it from here. */
extern __xdata char hostname[24];
extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE+2];
extern __xdata struct uip_eth_addr uip_ethaddr;
// Headers for calls in the common code area (HOME/BANK0)
void print_string_no_syslog(__code char *p);
void print_string_newline_no_syslog(__code char *p);
void print_string(__code char *p);
void print_string_x(__xdata char *p);
void print_long(uint32_t a);
@@ -129,6 +141,7 @@ void itoa(uint8_t v);
void print_sfr_data(void);
void print_phy_data(void);
void print_cmd_prompt(void);
void print_phys_port(uint8_t port);
void phy_write_mask(uint16_t phy_mask, uint8_t dev_id, uint16_t reg, uint16_t v);
void phy_write(uint8_t phy_id, uint8_t dev_id, uint16_t reg, uint16_t v);
void phy_read(uint8_t phy_id, uint8_t dev_id, uint16_t reg);
@@ -144,7 +157,8 @@ void sleep(uint16_t t);
void write_char_no_syslog(char c);
void write_char(char c);
void print_reg(uint16_t reg);
uint8_t sfp_read_reg(uint8_t slot, uint8_t reg);
bool sfp_read_block(uint8_t slot, uint8_t reg, uint8_t len) __banked __reentrant;
extern __xdata uint8_t sfp_buf[16];
void reg_bit_set(uint16_t reg_addr, char bit);
void reg_bit_clear(uint16_t reg_addr, char bit);
uint8_t reg_bit_test(uint16_t reg_addr, char bit);
@@ -152,17 +166,20 @@ void sfr_mask_data(uint8_t n, uint8_t mask, uint8_t set);
void sfr_set_zero(void);
void reset_chip(void);
void memcpy(__xdata void * __xdata dst, __xdata const void * __xdata src, uint16_t len);
void memcpyc(register __xdata uint8_t *dst, register __code uint8_t *src, register uint16_t len);
void memset(register __xdata uint8_t *dst, register __xdata uint8_t v, register uint8_t len);
uint16_t strlen(register __code const char *s);
uint16_t strlen_x(register __xdata const char *s);
uint16_t strtox(register __xdata uint8_t *dst, register __code const char *s);
uint16_t strcpy(register __xdata uint8_t *dst, register const char *s);
void memcpyc(__xdata uint8_t *dst, __code uint8_t *src, uint16_t len);
void memset(__xdata uint8_t *dst, __xdata uint8_t v, uint8_t len);
uint16_t strlen(__code const char *s);
uint16_t strlen_x(__xdata const char *s);
uint16_t strtox(__xdata uint8_t *dst, __code const char *s);
uint16_t strcpy(__xdata uint8_t *dst, const char *s);
char strcmp(__xdata const uint8_t *a, __code const uint8_t *b);
bool strstart(__xdata const uint8_t *a, __code const uint8_t *b);
bool strstart_x(__xdata const uint8_t *a, __xdata const uint8_t *b);
void tcpip_output(void);
uint8_t read_flash(uint8_t bank, __code uint8_t *addr);
void get_random_32(void);
void read_reg_timer(__xdata uint32_t * tmr);
void sfp_print_info(uint8_t sfp);
bool sfp_print_info(uint8_t sfp);
bool gpio_pin_test(uint8_t pin);
void set_sys_led_state(uint8_t state);
void sds_read(uint8_t sds_id, uint8_t page, uint8_t reg);
+10 -9
View File
@@ -16,6 +16,7 @@
#include "machine.h"
extern __code struct machine machine;
extern __xdata uint8_t igmpEnabled;
#include "uip.h"
@@ -85,22 +86,21 @@ void igmp_setup(void) __banked
{
uint8_t i;
print_string("igmp_setup called\n");
igmpEnabled = 0;
// For now, forward all unkown IP-MC pkts (2 bits per port. 00: flood via floodmask, 01: drop, 10: trap, 11: to rport)
REG_SET(RTL837X_IPV4_PORT_MC_LM_ACT, LOOKUP_MISS_FLOOD);
REG_SET(RTL837X_IPV6_PORT_MC_LM_ACT, LOOKUP_MISS_FLOOD);
// Define ports where unknown MC addresses are flooded to:
REG_SET(RTL837X_IPV4_UNKN_MC_FLD_PMSK, machine_detected.isRTL8373? PMASK_9: PMASK_6);
REG_SET(RTL837X_IPV6_UNKN_MC_FLD_PMSK, machine_detected.isRTL8373? PMASK_9: PMASK_6);
uint16_t mask = PMASK_6;
if (machine_detected.isRTL8373)
mask = PMASK_9;
REG_SET(RTL837X_IPV4_UNKN_MC_FLD_PMSK, mask);
REG_SET(RTL837X_IPV6_UNKN_MC_FLD_PMSK, mask);
// Enable lookup of IPv4 MC addresses in table
reg_bit_set(RTL837X_L2_CTRL, L2_CTRL_LUT_IPMC_HASH);
// Configure per-port IGMP configuration, bits 0-10 enable MC protocol snooping,
// bits 16-24 configure max MC group used by that port. For now all protocols are flooded (01)
for (i = machine.min_port; i <= machine.max_port; i++)
REG_SET(RTL837X_IGMP_PORT_CFG + (i << 2), 0x00ff7c15);
/* Configure per-port IGMP operations when protocol messages are received
* bits 0-9 enable MC protocol snooping
* bit 10: Enable dynamic router port learning
@@ -135,6 +135,7 @@ void igmp_setup(void) __banked
void igmp_enable(void) __banked
{
print_string("igmp_enable called\n");
igmpEnabled = 1;
// Configure trapping of unhandled IGMP protocol packets to CPU
REG_SET(RTL837X_IGMP_TRAP_CFG, IGMP_CPU_PORT | IGMP_TRAP_PRIORITY);
@@ -223,7 +224,7 @@ void igmp_packet_handler(void) __banked
#endif
#ifdef IPMC_USES_L3MC
memset(&entry, 0, sizeof(struct ipmc_table_entry));
memset((__xdata uint8_t *)&entry, 0, sizeof(struct ipmc_table_entry));
// For IPv4 MC, the Source-IP is 0.0.0.0
entry.sip[0] = 0x00; entry.sip[1] = 0x00; entry.sip[2] = 0x00; entry.sip[3] = 0x00;
// For IPv4 MC, the Destination-IP is the IPv4 MC address
@@ -235,7 +236,7 @@ void igmp_packet_handler(void) __banked
* yy = MC_IP[2]
* zz = MC_IP[3]
*/
memset(&entry, 0, sizeof(struct l2mc_table_entry));
memset((__xdata uint8_t *)&entry, 0, sizeof(struct l2mc_table_entry));
entry.mac[0] = 0x01; entry.mac[1] = 0x00; entry.mac[2] = 0x5e;
entry.mac[3] = IGMP_I->mc_ip[1] & 0x7f; entry.mac[4] = IGMP_I->mc_ip[2]; entry.mac[5] = IGMP_I->mc_ip[3];
entry.vlan = 1; //TODO: Get this out of the packet and compare with VLAN table!
+13 -11
View File
@@ -262,7 +262,7 @@ void phy_set_speed(void) __banked
{
uint16_t v;
print_string("Setting port "); write_char(machine.log_to_phys_port[phy_settings.port] + '0');
print_string("Setting port "); print_phys_port(phy_settings.port);
if (machine.n_10g && phy_settings.port == 3)
phy_settings.is10g_port = 1;
if (machine.n_10g == 2 && phy_settings.port == 8)
@@ -381,7 +381,7 @@ void phy_set_duplex(void) __banked
{
uint16_t v;
print_string("Setting port "); write_char(machine.log_to_phys_port[phy_settings.port] + '0');
print_string("Setting port "); print_phys_port(phy_settings.port);
if (phy_settings.duplex)
print_string(" to full duplex");
else
@@ -468,9 +468,13 @@ void phy_show(uint8_t port) __banked
phy_read(port, PHY_MMD_PMAPMD, 0);
v = SFR_DATA_U16;
print_string("\nForced speed: "); print_short(v); write_char('\n');
uint8_t s1 = ((v & 0x40) ? 0x2 : 0x0) | ((v & 0x2000) ? 0x1 : 0x0);
uint8_t s2 = (v >> 2) & 0xf;
switch(s1) {
uint8_t s1 = 0x00;
if ((uint8_t)v & 0x40)
s1 = 0x2;
if (v & 0x2000)
s1 |= 0x1;
uint8_t s2 = ((uint8_t)v >> 2) & 0xf;
switch(s1 & 0x3) {
case 0:
print_string("10M\n");
break;
@@ -495,8 +499,6 @@ void phy_show(uint8_t port) __banked
print_string("Unknown\n");
}
break;
default:
print_string("Unknown\n");
}
phy_read(port, PHY_MMD31, PHY_MMD31_FEDCR);
v = SFR_DATA_U16;
@@ -582,7 +584,7 @@ void phy_reset(uint8_t port) __banked
// Reading only reads the lower 16-bit part of the 32-bit register.
// When also needing read the upper 16-bits, use register address + 1.
// Readed values it return via sfr-data.
void inline rtl8224_read_reg_u16(uint16_t reg) __banked
void rtl8224_read_reg_u16(uint16_t reg) __banked
{
// void phy_read(uint8_t phy_id, uint8_t dev_id, uint16_t reg)
// phy_read(RTL8224_PHY_ID, PHY_MMD30, reg);
@@ -590,7 +592,7 @@ void inline rtl8224_read_reg_u16(uint16_t reg) __banked
SFR_SMI_REG_U16 = reg; // c2, c2
SFR_SMI_PHY = RTL8224_PHY_ID; // a5
SFR_SMI_DEV = PHY_MMD30 << 3 | 2; // c4
SFR_SMI_DEV = (uint8_t)PHY_MMD30 << 3 | 2; // c4
SFR_EXEC_GO = SFR_EXEC_READ_SMI;
do {
@@ -601,7 +603,7 @@ void inline rtl8224_read_reg_u16(uint16_t reg) __banked
// Registers names are the same as on the RTL837x.
// Writing only the lower 16-bit part of the 32-bit register.
// When also needing to write the upper 16-bits, use register address + 1.
void inline rtl8224_write_reg_u16(uint16_t reg, uint16_t val) __banked
void rtl8224_write_reg_u16(uint16_t reg, uint16_t val) __banked
{
SFR_DATA_U16 = val; // SFR_A6, SFR_A7
SFR_SMI_REG_U16 = reg; // SFR_C2, SFR_C3
@@ -633,7 +635,7 @@ void inline rtl8224_write_reg_u16(uint16_t reg, uint16_t val) __banked
// Write to the RTL8224 SDS registers.
void rtl8224_sds_write(uint16_t sds_cmd, uint16_t value) __banked
void rtl8224_sds_write(uint16_t sds_cmd, __xdata uint16_t value) __banked
{
// Wait for command bit is cleared
do {
+5 -3
View File
@@ -28,14 +28,16 @@ void phy_show(uint8_t port) __banked;
void phy_reset(uint8_t port) __banked;
void rtl8224_read_reg_u16(uint16_t reg) __banked;
void rtl8224_write_reg_u16(uint16_t reg, uint16_t val) __banked;
void rtl8224_sds_write(uint16_t sds_cmd, uint16_t val) __banked;
void rtl8224_sds_write(uint16_t sds_cmd, __xdata uint16_t val) __banked;
void phy_config_8261(uint8_t phy, uint8_t sds) __banked;
#define RTL8224_SDS_WRITE(sds_id, page, reg, v) uint16_t _sdscmd = (uint16_t)(sds_id & 0x01) | (1 << 14) | (1 << 15); \
#define RTL8224_SDS_WRITE(sds_id, page, reg, v) do { \
uint16_t _sdscmd = (uint16_t)(sds_id & 0x01) | (1 << 14) | (1 << 15); \
_sdscmd |= (page & 0x3F) << 1; \
_sdscmd |= ((uint16_t)(reg & 0x1f)) << 7; \
print_string("CMD: "); print_short(_sdscmd); \
write_char('-'); print_short(v); \
rtl8224_sds_write(_sdscmd, v);
rtl8224_sds_write(_sdscmd, v); \
} while (0)
#endif
+58
View File
@@ -1,6 +1,11 @@
#include "rtl837x_pins.h"
#include "rtl837x_common.h"
#include "rtl837x_sfr.h"
#include "rtl837x_regs.h"
#include "machine.h"
extern __code const struct machine machine;
extern __xdata uint8_t sfr_data[4];
#pragma codeseg BANK2
#pragma constseg BANK2
@@ -121,3 +126,56 @@ void gpio_output_setup(uint8_t pin, __xdata uint8_t initial_val) __banked{
reg_bit_set(gpio_direction_reg(pin), (pin % 32));
}
/*
* Read up to 16 consecutive registers of the EEPROM via I2C into sfp_buf
*/
bool sfp_read_block(uint8_t slot, uint8_t reg, uint8_t len) __banked __reentrant
{
uint8_t dev;
uint8_t val;
len--;
if (len > 15)
return false;
dev = (reg & 0x80) ? 0x51 : 0x50; // 0x51 holds the diagnostics, 0x50 the module data
reg &= 0x7f;
REG_WRITE(RTL837X_REG_I2C_IN, 0, 0, 0, reg);
REG_WRITE(RTL837X_REG_I2C_CTRL, 0x00,
0x1 << (I2C_MEM_ADDR_WIDTH - 16) | len,
(dev >> 5) | i2c_bus_from_scl_pin(machine.sfp_port[slot].i2c.scl) << 5
| i2c_bus_from_sda_pin(machine.sfp_port[slot].i2c.sda) << 2,
((dev << 3) & 0xff) | 0x1);
do {
reg_read(RTL837X_REG_I2C_CTRL);
} while (SFR_DATA_0 & 0x1);
if (SFR_DATA_0 & 0x2)
return false;
for (uint8_t i = 0; i <= len; i++) {
switch (i & 0x3) {
case 0:
reg_read(RTL837X_REG_I2C_OUT + i);
val = SFR_DATA_0;
break;
case 1:
val = SFR_DATA_8;
break;
case 2:
val = SFR_DATA_16;
break;
default:
val = SFR_DATA_24;
break;
}
sfp_buf[i] = val;
}
return true;
}
+118 -38
View File
@@ -30,7 +30,7 @@ __xdata uint32_t l2_head;
__xdata struct vlan_settings vlan_settings;
void port_mirror_set(register uint8_t port, __xdata uint16_t rx_pmask, __xdata uint16_t tx_pmask) __banked
void port_mirror_set(uint8_t port, __xdata uint16_t rx_pmask, __xdata uint16_t tx_pmask) __banked
{
print_string("\nport_mirror_set called \n");
print_string("Mirroring port: "); print_byte(port); print_string(" with rx-mask: ");
@@ -87,10 +87,9 @@ vlan_ingress_mode_t port_ingress_filter_get(__xdata uint8_t port) __banked
/*
* Define a Primary VLAN ID for a port
*/
void port_pvid_set(uint8_t port, __xdata uint16_t pvid) __banked
static void port_pvid_write(uint8_t port, __xdata uint16_t pvid)
{
// r4e1c:00001001 R4e1c-000017d0 r6738:00000000 R6738-00000000 (no filtering)
print_string("\nport_pvid_set called \n");
uint16_t reg = RTL837x_PVID_BASE_REG + ((port >> 1) << 2);
reg_read_m(reg);
@@ -101,6 +100,22 @@ void port_pvid_set(uint8_t port, __xdata uint16_t pvid) __banked
}
}
void port_pvid_set(uint8_t port, __xdata uint16_t pvid) __banked
{
uint8_t lag = port_lag_of(port);
print_string("\nport_pvid_set called \n");
if (lag == PORT_LAG_NONE) {
port_pvid_write(port, pvid);
return;
}
uint16_t members = port_lag_members_get(lag);
for (uint8_t i = 0; i < 10; i++)
if ((members >> i) & 1)
port_pvid_write(i, pvid);
}
uint16_t port_pvid_get(uint8_t port) __banked
{
uint16_t reg = RTL837x_PVID_BASE_REG + ((port >> 1) << 2);
@@ -115,6 +130,9 @@ uint16_t port_pvid_get(uint8_t port) __banked
void vlan_delete(uint16_t vlan) __banked
{
if (!vlan || vlan >= 0xfff)
return;
print_string("\nvlan_delete called \n"); print_short(vlan);
vlan_name_remove(vlan);
REG_WRITE(RTL837x_TBL_DATA_IN_A, 0, 0, 0, 0);
@@ -159,7 +177,7 @@ void vlan_name_remove(uint16_t vlan) __banked
* Reads VLAN information from VLAN table
* Returns data in sfr_data
*/
int8_t vlan_get(register uint16_t vlan) __banked
int8_t vlan_get(uint16_t vlan) __banked
{
if (vlan >= 0xfff) // VLAN 4095 is special
return -1;
@@ -174,7 +192,7 @@ int8_t vlan_get(register uint16_t vlan) __banked
}
__xdata uint16_t vlan_name(register uint16_t vlan) __banked
__xdata uint16_t vlan_name(uint16_t vlan) __banked
{
__xdata int16_t i = 0;
__xdata uint8_t begin = 1;
@@ -197,6 +215,11 @@ __xdata uint16_t vlan_name(register uint16_t vlan) __banked
*/
void vlan_create(void) __banked
{
if (!vlan_settings.vlan || vlan_settings.vlan >= 0xfff) {
print_string("\nInvalid VLAN: "); print_short(vlan_settings.vlan); write_char('\n');
return;
}
// For now, the CPU-port is always a tagged member:
vlan_settings.members |= 0x0200; // Set 10th bit
vlan_settings.tagged |= 0x0200;
@@ -306,6 +329,20 @@ void vlan_setup(void) __banked
}
/*
* Forget the dynamic L2 entries learned on one port.
*/
void port_l2_forget_port(uint8_t port) __banked
{
REG_SET(RTL837x_L2_TBL_FLUSH_CNF, 0x0); /* port-based, dynamic entries */
REG_SET(RTL837x_L2_TBL_FLUSH_CTRL, L2_TBL_FLUSH_EXEC | (((uint16_t)1) << port));
do {
reg_read(RTL837x_L2_TBL_FLUSH_CTRL);
} while (SFR_DATA_16);
}
/*
* Forget all dynamic L2 learned entries
*/
@@ -317,7 +354,10 @@ uint8_t port_l2_forget(void) __banked
REG_SET(RTL837x_L2_TBL_FLUSH_CNF, 0x0);
// Flush L2 table for all ports by setting the ports and the flush-exec bit (bit 16)
REG_SET(RTL837x_L2_TBL_FLUSH_CTRL, L2_TBL_FLUSH_EXEC | (machine_detected.isRTL8373 ? PMASK_9 : PMASK_6));
uint16_t mask = PMASK_6;
if (machine_detected.isRTL8373)
mask = PMASK_9;
REG_SET(RTL837x_L2_TBL_FLUSH_CTRL, L2_TBL_FLUSH_EXEC | mask);
// Wait for flush completed
do {
@@ -382,10 +422,7 @@ void port_l2_learned(void) __banked
print_string("\tlearned\t");
port |= (sfr_data[3] & 0x3) << 2;
if (port < 9)
write_char(machine.log_to_phys_port[port] + '0');
else
print_string("CPU");
print_phys_port(port);
}
entry++;
@@ -394,6 +431,27 @@ void port_l2_learned(void) __banked
}
/*
* Static L2 multicast entry for the link-local group 01:80:C2:00:00:<mac_last>
* in VLAN `vid`, with member portmask `pmask` (bit 9 = CPU port).
*/
void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked
{
do {
reg_read(RTL837X_TBL_CTRL);
} while (SFR_DATA_0 & TBL_EXECUTE);
REG_WRITE(RTL837x_TBL_DATA_IN_A, 0xc2, 0x00, 0x00, mac_last);
REG_WRITE(RTL837x_TBL_DATA_IN_B, 0x20 | (vid >> 8) | ((pmask & 0x3) << 6), vid, 0x01, 0x80);
REG_WRITE(RTL837x_TBL_DATA_IN_C, 0, 0, 0, pmask >> 2);
REG_WRITE(RTL837X_TBL_CTRL, 0, 0, TBL_L2_UNICAST, TBL_WRITE | TBL_EXECUTE);
do {
reg_read(RTL837X_TBL_CTRL);
} while (SFR_DATA_0 & TBL_EXECUTE);
}
/*
* Basic L2 configuration such as time to forget an entry
*/
@@ -405,12 +463,14 @@ void port_l2_setup(void) __banked
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
// Limit the number of automatically learned MAC-Entries per port to 0x1040
uint16_t reg = RTL837X_L2_LRN_PORT_CONSTRAINT + (i << 2);
REG_SET(reg, 0x00001040);
uint8_t idx = (i << 2);
REG_SET(RTL837X_L2_LRN_PORT_CONSTRAINT + idx, 0x00001040);
// All ports may communicate with each other and CPU-Port
reg = RTL837X_PORT_ISOLATION_BASE + (i << 2);
REG_SET(reg, PMASK_CPU | (machine_detected.isRTL8373? PMASK_9 : PMASK_6));
uint16_t mask = PMASK_CPU | PMASK_6;
if (machine_detected.isRTL8373)
mask = PMASK_CPU | PMASK_9;
REG_SET(RTL837X_PORT_ISOLATION_BASE + idx, mask);
}
// When maximim entries learned, then simply flood the packet
reg_bit_set(RTL837X_L2_LRN_PORT_CONSTRT_ACT, 0);
@@ -423,7 +483,7 @@ void port_stats_print(void) __banked
{
print_string("\nPort\tState\tLink\tTxGood\t\tTxBad\t\tRxGood\t\tRxBad\n");
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
write_char('0' + machine.log_to_phys_port[i]); write_char('\t');
print_phys_port(i); write_char('\t');
if (!machine.is_sfp[i]) {
phy_read(i, PHY_MMD31, 0xa610);
@@ -500,14 +560,14 @@ void port_stats_print(void) __banked
}
void port_isolate(register uint8_t port, __xdata uint16_t pmask) __banked
void port_isolate(uint8_t port, __xdata uint16_t pmask) __banked
{
if (port <= machine.max_port)
REG_SET(RTL837X_PORT_ISOLATION_BASE + (port << 2), pmask);
}
uint16_t port_isolation_get(register uint8_t port) __banked
uint16_t port_isolation_get(uint8_t port) __banked
{
if (port > machine.max_port)
return 0;
@@ -598,7 +658,7 @@ void port_eee_disable(uint8_t port) __banked
void port_eee_status(uint8_t port) __banked
{
print_string("Port: "); write_char('0' + machine.log_to_phys_port[port]);
print_string("Port: "); print_phys_port(port);
print_string(": ");
if (machine.is_sfp[port]) {
print_string("SFP\n");
@@ -725,6 +785,27 @@ void port_rldp_on(__xdata uint16_t p_ms)
}
/*
* Reads the member port bitmask of a Link Aggregation Group.
* The groups have numbers 0-3; bit n is set when logical port n is a member.
* The bitmask reflects what the hardware holds, so it covers groups set up
* statically and groups a protocol brought up, without either having to say so.
*/
uint16_t port_lag_members_get(uint8_t lag) __banked
{
reg_read(RTL837X_TRK_MBR_CTRL_BASE + (lag << 2));
return ((uint16_t)SFR_DATA_8 << 8) | SFR_DATA_0;
}
uint8_t port_lag_of(uint8_t port) __banked
{
for (uint8_t lag = 0; lag < 4; lag++)
if ((port_lag_members_get(lag) >> port) & 1)
return lag;
return PORT_LAG_NONE;
}
/*
* Configure LAGs
* Sets the members via port bitmask of a given Link Aggregation Group
@@ -736,11 +817,14 @@ void port_lag_members_set(__xdata uint8_t lag, __xdata uint16_t members) __banke
{
print_string("port_lag_members_set, lag: "); print_byte(lag); print_string(", members: "); print_short(members);
write_char('\n');
if (lag > 3)
print_string("Link aggregation group must be 0-3!\n");
if (lag > 3) {
print_string("Link aggregation group out of range\n");
return;
}
reg_read_m(RTL837X_TRK_HASH_CTRL_BASE + (lag << 2));
if (!(sfr_data[0] | sfr_data [1] | sfr_data [2] | sfr_data [3]))
REG_SET(RTL837X_TRK_HASH_CTRL_BASE, LAG_HASH_DEFAULT);
if (!(sfr_data[0] | sfr_data[1] | sfr_data[2])
&& (sfr_data[3] == LAG_HASH_RESET || sfr_data[3] == 0))
REG_SET(RTL837X_TRK_HASH_CTRL_BASE + (lag << 2), LAG_HASH_DEFAULT);
REG_WRITE(RTL837X_TRK_MBR_CTRL_BASE + (lag << 2), 0, 0, members >> 8, members & 0xff);
}
@@ -753,8 +837,10 @@ void port_lag_hash_set(__xdata uint8_t lag, __xdata uint8_t hash_bits) __banked
{
print_string("port_lag_hash_set, lag: "); print_byte(lag); print_string(", hash: "); print_byte(hash_bits);
write_char('\n');
if (lag > 3)
print_string("Link aggregation group must be 0-3!\n");
if (lag > 3) {
print_string("Link aggregation group out of range\n");
return;
}
REG_WRITE(RTL837X_TRK_HASH_CTRL_BASE + (lag << 2), 0, 0, 0, hash_bits);
}
@@ -775,16 +861,6 @@ void print_port_ingress_filter_mode(vlan_ingress_mode_t mode) __banked
}
}
static void print_phys_port(uint8_t port) __banked
{
if (port >= machine.min_port && port <= machine.max_port)
write_char(machine.log_to_phys_port[port] + '0');
else if (port == 9)
write_char('9');
else
write_char('?');
}
void print_vlan_ingress_port(uint8_t log_port) __banked
{
print_phys_port(log_port);write_char('\t');
@@ -813,19 +889,23 @@ void vlan_dump(void) __banked
/** Set the ingress VLAN filtering */
bool port_ingress_vlan_filter_set(__xdata uint8_t port, __xdata bool enabled) __banked
bool port_ingress_vlan_filter_set(uint8_t port, __xdata bool enabled) __banked
{
if (port < machine.min_port || port > machine.max_port && port != 9) {
if (port < machine.min_port || port > machine.max_port && port != CPU_PORT) {
return false;
}
if (enabled)
reg_bit_set(RTL837X_VLAN_PORT_IGR_FLTR, port);
else
reg_bit_clear(RTL837X_VLAN_PORT_IGR_FLTR, port);
return true;
}
/** Get the ingress VLAN filtering status */
bool port_ingress_vlan_filter_get(__xdata uint8_t port) __banked
bool port_ingress_vlan_filter_get(uint8_t port) __banked
{
if (port < machine.min_port || port > machine.max_port && port != 9) {
if (port < machine.min_port || port > machine.max_port && port != CPU_PORT) {
return false;
}
+16 -9
View File
@@ -8,11 +8,12 @@
#define STAT_COUNTER_RX_PKTS 47
#define STAT_COUNTER_ERR_PKTS 48
#define STAT_GET(cnt, port) \
#define STAT_GET(cnt, port) do { \
REG_WRITE(RTL837X_STAT_GET, 0x00, 0x00, cnt >> 3, (cnt << 5) | (port << 1) | 1); \
do { \
reg_read_m(RTL837X_STAT_GET); \
} while (sfr_data[3] & 0x1);
} while (sfr_data[3] & 0x1); \
} while (0)
// Possible values for ingress filter type
typedef enum {
@@ -48,19 +49,24 @@ extern __xdata struct vlan_settings vlan_settings;
uint8_t port_l2_forget(void) __banked;
void port_l2_learned(void) __banked;
void port_stats_print(void) __banked;
int8_t vlan_get(register uint16_t vlan) __banked;
__xdata uint16_t vlan_name(register uint16_t vlan) __banked;
int8_t vlan_get(uint16_t vlan) __banked;
__xdata uint16_t vlan_name(uint16_t vlan) __banked;
void vlan_name_remove(uint16_t vlan) __banked;
void vlan_setup(void) __banked;
void port_pvid_set(uint8_t port, __xdata uint16_t pvid) __banked;
uint16_t port_pvid_get(uint8_t port) __banked;
void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked;
void port_l2_forget_port(uint8_t port) __banked;
void vlan_create(void) __banked;
void vlan_delete(uint16_t vlan) __banked;
void vlan_dump(void) __banked;
void port_mirror_set(register uint8_t port, __xdata uint16_t rx_pmask, __xdata uint16_t tx_pmask) __banked;
void port_mirror_set(uint8_t port, __xdata uint16_t rx_pmask, __xdata uint16_t tx_pmask) __banked;
void port_mirror_del(void) __banked;
bool port_ingress_filter(__xdata uint8_t port, __xdata vlan_ingress_mode_t type) __banked;
void port_l2_setup(void) __banked;
uint16_t port_lag_members_get(uint8_t lag) __banked;
#define PORT_LAG_NONE 0xff
uint8_t port_lag_of(uint8_t port) __banked;
void port_lag_members_set(__xdata uint8_t lag, __xdata uint16_t members) __banked;
void port_lag_hash_set(__xdata uint8_t lag, __xdata uint8_t hash) __banked;
void port_eee_enable_all(__xdata uint8_t speed) __banked;
@@ -70,9 +76,10 @@ void port_eee_enable(__xdata uint8_t port, __xdata uint8_t speed) __banked;
void port_eee_disable(uint8_t port) __banked;
void port_eee_status(uint8_t port) __banked;
void print_port_ingress_filter_mode(vlan_ingress_mode_t mode) __banked;
bool port_ingress_vlan_filter_set(__xdata uint8_t port, __xdata bool enabled) __banked;
bool port_ingress_vlan_filter_get(__xdata uint8_t port) __banked;
void port_isolate(register uint8_t port, __xdata uint16_t pmask) __banked;
uint16_t port_isolation_get(register uint8_t port) __banked;
bool port_ingress_vlan_filter_set(uint8_t port, __xdata bool enabled) __banked;
bool port_ingress_vlan_filter_get(uint8_t port) __banked;
vlan_ingress_mode_t port_ingress_filter_get(__xdata uint8_t port) __banked;
void port_isolate(uint8_t port, __xdata uint16_t pmask) __banked;
uint16_t port_isolation_get(uint8_t port) __banked;
#endif
+18 -8
View File
@@ -232,6 +232,8 @@
#define LAG_HASH_L4_SPORT 0x20
#define LAG_HASH_L4_DPORT 0x40
#define LAG_HASH_DEFAULT (LAG_HASH_L2_SMAC | LAG_HASH_L2_DMAC | LAG_HASH_L3_SIP | LAG_HASH_L3_DIP | LAG_HASH_L4_SPORT | LAG_HASH_L4_DPORT)
#define LAG_HASH_RESET (LAG_HASH_SOURCE_PORT_NUMBER | LAG_HASH_L2_SMAC | LAG_HASH_L2_DMAC \
| LAG_HASH_L3_SIP | LAG_HASH_L3_DIP | LAG_HASH_L4_SPORT)
/*
* Port isolation
@@ -305,32 +307,40 @@
#ifdef REGDBG
#define REG_SET(r, v) SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \
#define REG_SET(r, v) do { \
SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \
SFR_DATA_16 = (((uint32_t)v) >> 16) & 0xff; \
SFR_DATA_8 = (((uint16_t)v) >> 8 & 0xff); \
SFR_DATA_0 = (v) & 0xff; \
reg_write(r); \
write_char('R'); print_byte(r >> 8); print_byte(r); write_char('-'); \
print_byte(((v) >> 24) & 0xff); print_byte((v) >> 16 & 0xff); print_byte((v) >> 8 & 0xff); print_byte( (v) & 0xff); write_char(' ');
print_byte(((v) >> 24) & 0xff); print_byte((v) >> 16 & 0xff); print_byte((v) >> 8 & 0xff); print_byte( (v) & 0xff); write_char(' '); \
} while (0)
#define REG_WRITE(r, v24, v16, v8, v0) SFR_DATA_24 = (v24); \
#define REG_WRITE(r, v24, v16, v8, v0) do { \
SFR_DATA_24 = (v24); \
SFR_DATA_16 = (v16); \
SFR_DATA_8 = (v8); \
SFR_DATA_0 = (v0); \
reg_write(r); \
write_char('R'); print_byte(r>>8); print_byte(r); write_char('-'); print_byte(v24); print_byte(v16); print_byte(v8); print_byte(v0); write_char(' ');
write_char('R'); print_byte(r>>8); print_byte(r); write_char('-'); print_byte(v24); print_byte(v16); print_byte(v8); print_byte(v0); write_char(' '); \
} while (0)
#else
#define REG_SET(r, v) SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \
#define REG_SET(r, v) do { \
SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \
SFR_DATA_16 = (((uint32_t)v) >> 16) & 0xff; \
SFR_DATA_8 = (((uint16_t)v) >> 8 & 0xff); \
SFR_DATA_0 = (v) & 0xff; \
reg_write(r);
reg_write(r); \
} while (0)
#define REG_WRITE(r, v24, v16, v8, v0) SFR_DATA_24 = (v24); \
#define REG_WRITE(r, v24, v16, v8, v0) do { \
SFR_DATA_24 = (v24); \
SFR_DATA_16 = (v16); \
SFR_DATA_8 = (v8); \
SFR_DATA_0 = (v0); \
reg_write(r);
reg_write(r); \
} while (0)
#endif
#endif
+7 -2
View File
@@ -5,8 +5,13 @@ __sfr16 __at(0xa2a3) SFR_REG_ADDR_U16;
__sfr __at(0xa2) SFR_REG_ADDRH;
__sfr __at(0xa3) SFR_REG_ADDRL;
__sfr16 __at(0xa6a7) SFR_DATA_U16;
__sfr32 __at(0xa4a5a6a7) SFR_DATA_U32;
__sfr32 __at(0xa7a6a5a4) SFR_DATA_U32LE;
// Disabling until the SDCC bug #4070 is fixed.
// Generate wrong address for `a4` location.
// Both read from and write to `SFR_DATA_U32`.
// __sfr32 __at(0xa4a5a6a7) SFR_DATA_U32;
// __sfr32 __at(0xa7a6a5a4) SFR_DATA_U32LE;
// This is the upper part of the U32 as a workaround for SDCC bug #4070.
__sfr16 __at(0xa4a5) SFR_DATA_U16_UPPER;
__sfr __at(0xa4) SFR_DATA_24;
__sfr __at(0xa5) SFR_DATA_16;
__sfr __at(0xa6) SFR_DATA_8;
+783 -92
View File
@@ -11,29 +11,84 @@
#include "rtl837x_sfr.h"
#include "rtl837x_regs.h"
#include "rtl837x_stp.h"
#include "rtl837x_port.h"
#include "uip.h"
#include "machine.h"
// All entry points are __banked and nothing here runs from an interrupt,
// so the module does not need to stay in the resident bank
#pragma codeseg BANK2
#pragma constseg BANK2
extern __code struct machine machine;
extern __xdata uint8_t sfr_data[4];
extern __xdata struct machine_runtime machine_detected;
extern __xdata struct uip_eth_addr uip_ethaddr;
extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2];
struct bridge {
uint8_t prio;
uint8_t ext;
uint8_t mac[6];
};
extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE];
extern __xdata uint8_t cmd_words_len;
extern __xdata uint8_t cmd_words_b[15];
extern __xdata char save_cmd; /* 0 while execute_config() replays the saved config */
uint8_t cmd_compare(uint8_t start, __code uint8_t * cmd);
uint8_t atoi_byte(uint8_t idx);
uint8_t cmd_parse_port_separator(uint8_t idx);
extern __xdata uint8_t atoi_results_u8;
/* ---- Configuration ---- */
__xdata uint8_t stp_prio; /* bridge priority high byte (0x80 = 32768) */
__xdata uint8_t stp_hello_s; /* 1-10 s */
__xdata uint8_t stp_maxage_s; /* 6-40 s */
__xdata uint8_t stp_fwddelay_s; /* 4-30 s, also our listen period */
__xdata uint8_t stp_rstp; /* 1 = RST BPDUs, 0 = legacy Config BPDUs */
__xdata uint8_t stp_txhold; /* BPDUs per port per second */
__xdata uint8_t stp_pflags[10];
__xdata uint32_t stp_pcost[10]; /* 0 = auto */
__xdata uint8_t stp_pprio[10];
__xdata uint8_t stp_pp2p[10]; /* admin point-to-point: 0 auto, 1 on, 2 off */
/* Designated bridge, port and cost last heard on the port; stp_bpdu_age tells
* whether they are still current.
*/
__xdata struct bridge stp_dbridge[10];
__xdata uint16_t stp_dpid[10];
__xdata uint32_t stp_dcost[10];
/* ---- Status / runtime ---- */
__xdata struct bridge root_bridge;
__xdata uint32_t root_bridge_cost;
__xdata uint32_t root_bridge_cost; /* our cost to the root (rx cost + root port cost) */
__xdata uint8_t stp_root_port; /* 0xff = we are the root */
__xdata uint16_t stp_tc_count;
__xdata uint16_t stp_scratch16; /* scratch for status printing only */
__xdata uint8_t port_types[10];
__xdata uint16_t port_timers[10];
__xdata uint16_t port_hello[10];
__xdata uint16_t port_timers[10]; /* listen-period countdown (0 = not listening) */
__xdata uint16_t port_hello[10]; /* hello TX countdown */
__xdata uint16_t stp_bpdu_age[10]; /* ticks since last BPDU seen on port (saturating) */
__xdata uint8_t stp_loop_held[10]; /* port is out of forwarding because a loop was seen on it */
__xdata uint8_t stp_tx_budget[10]; /* tx hold: BPDUs left in the current second */
__xdata uint8_t stp_tx_count[10]; /* BPDUs actually put on the wire, wraps at 256 */
__xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */
__xdata uint16_t stp_link_prev; /* carrier bitmap as of the last check */
__xdata uint16_t stp_link_now;
__xdata uint8_t stp_scratch;
__xdata uint8_t stp_tx_flags_extra; /* one-shot flags OR-ed into the next BPDU (TCA) */
__xdata uint16_t stp_rxlen; /* received frame length, saved before uip_len is consumed */
__xdata uint8_t stp_msg_age; /* message age of the root info we hold, seconds */
__xdata uint16_t stp_tc_while; /* ticks left to set the TC flag in our BPDUs */
__xdata uint8_t stp_i;
__xdata uint32_t stp_cost_scratch;
__xdata uint8_t stp_loop_peer; /* the other own port seen on a looped segment */
#define STP_EDGE_DELAY (3 * STP_HZ) /* auto-edge: forward after 3 s without BPDU */
#define AUTO_COST 20000UL /* path cost used when stp_pcost == 0 (1G default) */
#define PCOST(i) (stp_pcost[i] ? stp_pcost[i] : AUTO_COST)
struct stp_pkt {
uint8_t stp_addr[6];
@@ -56,6 +111,7 @@ struct stp_pkt {
uint16_t age_max;
uint16_t hello;
uint16_t fwd_delay;
uint8_t version1_length; /* RST BPDU only: length of the (empty) v1 part */
};
struct stp_pkt_in {
@@ -80,18 +136,126 @@ struct stp_pkt_in {
uint16_t age_max;
uint16_t hello;
uint16_t fwd_delay;
uint8_t version1_length; /* RST BPDU only: length of the (empty) v1 part */
};
#define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE])
#define STP_I ((__xdata struct stp_pkt_in *)&uip_buf[0])
#define FLAG_PROPOSAL 0x02
#define P_DESIGNATED ((STP_I->flags & 0x0c) == 0x0c)
#define P_PROPOSAL (STP_I->flags & FLAG_PROPOSAL)
#define BPDU_VER_STP 0x00
#define BPDU_VER_RSTP 0x02
signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2)
#define BPDU_TYPE_CONFIG 0x00
#define BPDU_TYPE_RST 0x02
#define BPDU_TYPE_TCN 0x80
#define BPDU_LEN_CONFIG 0x26 // LLC and a 35 byte body
#define BPDU_LEN_RST 0x27 // LLC and a 36 byte body
#define BPDU_LEN_MIN_HEADER 33 // addresses through bpdu_type
#define BPDU_FLAG_TC 0x01
#define BPDU_FLAG_LEARNING 0x10
#define BPDU_FLAG_FORWARDING 0x20
#define BPDU_FLAG_TCACK 0x80
#define BPDU_ROLE_ROOT (0b10 << 2)
#define BPDU_ROLE_DESIGNATED (0b11 << 2)
/* Console messages name the port on the front panel, not the internal index. */
static void print_port_nl(uint8_t port) __reentrant
{
for (uint8_t i = 0; i < 6; i++) {
print_byte(machine.log_to_phys_port[port]);
write_char('\n');
}
static void print_bridge_id(uint8_t prio, uint8_t ext, __xdata uint8_t *mac) __reentrant
{
print_byte(prio); print_byte(ext); write_char('/');
for (stp_i = 0; stp_i < 6; stp_i++)
print_byte(mac[stp_i]);
}
/* Fixed width columns so the rows line up under the header without a
* formatter. The state indices are the ASIC's own two bits, in the order
* stp_state_set() writes them. */
static __code const char stp_state_txt[] = "off blocklearnfwd ";
static __code const char stp_role_txt[] = "desgroot";
static __code const char stp_edge_txt[] = "no yes ";
static void print_field(__code const char *txt, uint8_t idx, uint8_t width) __reentrant
{
txt += idx * width;
while (width--)
write_char(*txt++);
}
static void stp_status(void)
{
if (!stp_enabled) {
print_string("STP off\n");
return;
}
print_string(stp_rstp ? "STP on, RSTP\n" : "STP on, STP\n");
print_string("bridge ");
print_bridge_id(stp_prio, 0, uip_ethaddr.addr);
print_string("\nroot ");
print_bridge_id(root_bridge.prio, root_bridge.ext, root_bridge.mac);
if (stp_root_port == 0xff) {
print_string(" (this switch)\n");
} else {
print_string(" port ");
print_byte(machine.log_to_phys_port[stp_root_port]);
print_string(" cost ");
print_long(root_bridge_cost);
write_char('\n');
}
print_string("changes ");
print_short(stp_tc_count);
write_char('\n');
print_string("port state role edge tx bpdu\n");
reg_read_m(RTL837X_MSTP_STATES);
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) {
write_char(' ');
print_byte(machine.log_to_phys_port[stp_i]);
print_string(" ");
print_field(stp_state_txt, (sfr_data[3 - (stp_i >> 2)] >> ((stp_i << 1) & 0x7)) & 0x3, 5);
write_char(' ');
print_field(stp_role_txt, stp_i == stp_root_port ? 1 : 0, 4);
write_char(' ');
print_field(stp_edge_txt, stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0, 4);
write_char(' ');
print_byte(stp_tx_count[stp_i]);
write_char(' ');
stp_scratch16 = stp_bpdu_age[stp_i] / STP_HZ;
itoa(stp_scratch16 > 255 ? 255 : (uint8_t)stp_scratch16);
write_char('\n');
}
}
static void stp_record_designated(uint8_t port) __reentrant
{
stp_dbridge[port].prio = STP_I->bridge.prio;
stp_dbridge[port].ext = STP_I->bridge.ext;
memcpy(stp_dbridge[port].mac, STP_I->bridge.mac, 6);
stp_dpid[port] = ((uint16_t)STP_I->port_prio << 8) | STP_I->port_id;
stp_cost_scratch = STP_I->root_path_cost;
stp_dcost[port] = ((stp_cost_scratch & 0xff) << 24)
| ((stp_cost_scratch & 0xff00) << 8)
| ((stp_cost_scratch >> 8) & 0xff00)
| (stp_cost_scratch >> 24);
}
/* Lexicographic compare of n bytes. A MAC is 6 of them; a Bridge Identifier
* is 8, the two priority octets ahead of the MAC, compared as one unsigned
* number per 802.1D. */
int8_t cmpBytes(__xdata uint8_t *m1, __xdata uint8_t *m2, uint8_t n) __reentrant
{
for (uint8_t i = 0; i < n; i++) {
if (m1[i] == m2[i])
continue;
if (m1[i] < m2[i])
@@ -102,103 +266,448 @@ signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2)
}
void stp_in(void) __banked
/* Write one port's 2-bit state into the ASIC's MSTP register.
* 00 disable, 01 blocking, 10 learning, 11 forwarding. */
static void stp_state_set(uint8_t port, uint8_t state) __reentrant
{
// By default we do not send anything out
uip_len = 0;
// MSTPSTP_I_STATES 0x5310
// reg_read_m(RTL837X_MSTP_STATES);
print_string("Check BPDU... \n");
for (uint8_t i = 0; i < 80; i++) {
print_byte(uip_buf[i]);
write_char(' ');
}
write_char('\n');
print_byte(STP_I->dsap);
print_byte(STP_I->ssap);
print_byte(STP_I->ctrl);
write_char('\n');
// Make sure this is the type of RSTP packet we are interested in:
if (!(STP_I->dsap == 0x42 && STP_I->ssap == 0x42 && STP_I->ctrl == 0x03))
return;
print_string("Checking RSTP\n");
if (STP_I->proto)
return;
// write_char('A'); print_byte(STP_I->version); write_char('\n');
if (STP_I->version != 2)
return;
// write_char('B'); print_byte(STP_I->bpdu_type); write_char('\n');
if (STP_I->bpdu_type != 2)
return;
// write_char('\n');
// print_string("Flags: "); print_byte(STP_I->flags); write_char('\n');
print_string("Check new Root\n");
if (STP_I->root.prio < root_bridge.prio
|| ((STP_I->root.prio == root_bridge.prio) && cmpMAC(STP_I->root.mac, root_bridge.mac) < 0)) {
print_string("Updating Root bridge\n");
root_bridge.prio = STP_I->root.prio;
memcpy(root_bridge.mac, STP_I->root.mac, 6);
}
reg_read_m(RTL837X_MSTP_STATES);
stp_scratch = 3 - (port >> 2);
sfr_data[stp_scratch] &= ~(uint8_t)(0b11 << ((port << 1) & 0x7));
sfr_data[stp_scratch] |= (uint8_t)(state << ((port << 1) & 0x7));
reg_write_m(RTL837X_MSTP_STATES);
}
void stp_cnf_send(uint8_t port)
/* Signal a topology change. Edge ports are exempt. */
static void stp_topology_change(uint8_t port) __reentrant
{
if (stp_pflags[port] & STP_PF_OPEREDGE)
return;
stp_tc_count++;
stp_tc_while = ((uint16_t)stp_maxage_s + stp_fwddelay_s) * STP_HZ;
port_l2_forget_port(port);
}
/* Hold one port out of forwarding because a loop was seen on it, and keep
* holding it for as long as the caller keeps saying so. The caller is the
* port that won the Port ID compare (see stp_in) - a different port than
* the one held, except when the frame came back on the port it left.
*/
static void stp_loop_hold_peer(uint8_t port) __reentrant
{
if (port < machine.min_port || port > machine.max_port)
return;
if (!(stp_pflags[port] & STP_PF_ENABLED))
return;
if (stp_pflags[port] & STP_PF_TRIPPED)
return;
if (!port_timers[port]) {
print_string("STP: loop detected, blocking port ");
print_port_nl(port);
stp_state_set(port, 0b01);
stp_pflags[port] &= ~STP_PF_OPEREDGE;
stp_topology_change(port);
}
stp_loop_held[port] = 1;
port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ;
}
/* Take the bridge back as root of its own tree (initial state / root aged out) */
static void stp_claim_root(void)
{
root_bridge.prio = stp_prio;
root_bridge.ext = 0x00;
memcpy(root_bridge.mac, uip_ethaddr.addr, 6);
root_bridge_cost = 0;
stp_root_port = 0xff;
stp_msg_age = 0;
}
void stp_cnf_send(uint8_t port) __reentrant
{
/* A one-shot flag (TCA) belongs to the BPDU we were asked to send: drop
* it with the frame, or it would surface on an unrelated port later. */
if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & (STP_PF_FILTER | STP_PF_TRIPPED))) {
stp_tx_flags_extra = 0;
return;
}
if (!stp_tx_budget[port]) { /* tx hold count exhausted for this second */
stp_tx_flags_extra = 0;
return;
}
stp_tx_budget[port]--;
stp_tx_count[port]++;
STP_O->stp_addr[0] = 0x01; STP_O->stp_addr[1] = 0x80; STP_O->stp_addr[2] = 0xc2;
STP_O->stp_addr[3] = STP_O->stp_addr[4] = STP_O->stp_addr[5] = 0x00;
STP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID);
STP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION;
STP_O->rtl_tag.reason = 0x00;
STP_O->rtl_tag.flags = 0x0020; // Disable L2 learning
STP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS);
STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << port);
STP_O->msg_len = HTONS(0x27);
STP_O->dsap = 0x42;
STP_O->ssap = 0x42;
STP_O->ctrl = 0x03;
STP_O->proto = 0x0000;
STP_O->version = 0x02; // RSTP
STP_O->bpdu_type = 0x00; // Config
STP_O->flags = 0x81;
if (stp_rstp) {
STP_O->msg_len = HTONS(BPDU_LEN_RST);
STP_O->version = BPDU_VER_RSTP;
STP_O->bpdu_type = BPDU_TYPE_RST;
reg_read_m(RTL837X_MSTP_STATES);
STP_O->flags = port == stp_root_port ? BPDU_ROLE_ROOT : BPDU_ROLE_DESIGNATED;
if (((sfr_data[3 - (port >> 2)] >> ((port << 1) & 0x7)) & 0b11) == 0b11)
STP_O->flags |= BPDU_FLAG_LEARNING | BPDU_FLAG_FORWARDING;
} else {
STP_O->msg_len = HTONS(BPDU_LEN_CONFIG);
STP_O->version = BPDU_VER_STP;
STP_O->bpdu_type = BPDU_TYPE_CONFIG;
STP_O->flags = 0x00;
}
if (stp_tc_while)
STP_O->flags |= BPDU_FLAG_TC;
STP_O->flags |= stp_tx_flags_extra;
stp_tx_flags_extra = 0;
memcpy(STP_O->src_addr, uip_ethaddr.addr, 6);
STP_O->src_addr[0] |= 0x02;
STP_O->src_addr[5] = (uip_ethaddr.addr[5] & 0xf0) | port;
memcpy(STP_O->root.mac, root_bridge.mac, 6);
memcpy(STP_O->bridge.mac, uip_ethaddr.addr, 6);
STP_O->root.prio = root_bridge.prio;
STP_O->root.ext = 0x00;
STP_O->root_path_cost = 0x00000000;
STP_O->root.ext = root_bridge.ext;
/* Our root path cost, big-endian (0 while we are the root ourselves) */
STP_O->root_path_cost = ((root_bridge_cost & 0xff) << 24)
| ((root_bridge_cost & 0xff00) << 8)
| ((root_bridge_cost >> 8) & 0xff00)
| (root_bridge_cost >> 24);
STP_O->bridge.prio = 0x80;
STP_O->bridge.prio = stp_prio;
STP_O->bridge.ext = 0x00;
STP_O->port_prio = 0x80;
STP_O->port_id = port;
STP_O->age = 0x00; // FIXME: This only works because we do not use HTONS and the values are in 1/256 seconds
STP_O->age_max = 20;
STP_O->hello = 2;
STP_O->fwd_delay = 0x0f;
STP_O->port_prio = stp_pprio[port];
STP_O->port_id = port + 1;
/* Message age, incremented by one second per bridge we relay through.
* The timer fields are in 1/256 s on the wire, and sdcc stores uint16
* little-endian, so assigning the plain second count lands the value in
* the high (seconds) octet - see age_max/hello/fwd_delay below. */
STP_O->age = (stp_root_port == 0xff) ? 0 : (uint16_t)(stp_msg_age + 1);
STP_O->age_max = stp_maxage_s;
STP_O->hello = stp_hello_s;
STP_O->fwd_delay = stp_fwddelay_s;
STP_O->version1_length = 0; /* RST BPDU: no version-1 information */
// uip_len = 0x27 + sizeof(struct rtl_tag);
uip_len = sizeof(struct stp_pkt);
uip_len = stp_rstp ? sizeof(struct stp_pkt) : sizeof(struct stp_pkt) - 1;
tcpip_output();
}
void stp_in(void) __banked
{
uint8_t port;
if (uip_len < BPDU_LEN_MIN_HEADER) {
uip_len = 0;
return;
}
stp_rxlen = uip_len;
// By default we do not send anything out
uip_len = 0;
/* Ingress port: low nibble of the CPU tag's pmask on RX */
stp_scratch = ((uint8_t)HTONS(STP_I->rtl_tag.pmask)) & 0x0f;
if (stp_scratch < machine.min_port || stp_scratch > machine.max_port)
return;
port = stp_scratch;
// Make sure this is the type of (R)STP packet we are interested in:
if (!(STP_I->dsap == 0x42 && STP_I->ssap == 0x42 && STP_I->ctrl == 0x03))
return;
if (STP_I->proto)
return;
if (!((STP_I->version >= BPDU_VER_RSTP && STP_I->bpdu_type == BPDU_TYPE_RST)
|| (STP_I->version == BPDU_VER_STP
&& (STP_I->bpdu_type == BPDU_TYPE_CONFIG
|| STP_I->bpdu_type == BPDU_TYPE_TCN))))
return;
if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & STP_PF_FILTER))
return;
/* BPDU guard: an edge-facing port must never see a BPDU - shut it down. */
if (stp_pflags[port] & STP_PF_BPDUGUARD) {
print_string("STP: BPDU guard tripped, disabling port ");
print_port_nl(port);
stp_pflags[port] |= STP_PF_TRIPPED;
stp_state_set(port, 0b00);
stp_tc_count++;
return;
}
stp_bpdu_age[port] = 0;
/* A port that hears a BPDU is not an edge port, whatever it decided
* during the silence after the link came up. Only the flag is dropped:
* the port keeps whatever forwarding state the rules below give it,
* rather than being pushed back through the listen period, which would
* black-hole a working link for a forward delay on the first BPDU. The
* flag matters beyond the status page, since stp_topology_change()
* exempts edge ports and so would go on skipping the counter and the
* L2 flush for a port that has a bridge behind it. */
stp_pflags[port] &= ~STP_PF_OPEREDGE;
if (STP_I->bpdu_type == BPDU_TYPE_TCN) {
stp_tx_flags_extra = BPDU_FLAG_TCACK;
stp_cnf_send(port);
uip_len = 0;
stp_topology_change(port);
return;
}
/* Everything below reads the full Config/RST body. */
if (stp_rxlen < 64)
return;
/* Our own BPDU coming back: two of our ports sit on one segment. Only
* the one with the worse Port ID stops forwarding, and only the other
* one writes that state, so the two never race each other.
*/
if (cmpBytes(STP_I->bridge.mac, uip_ethaddr.addr, 6) == 0) {
/* Equal means the frame came back on the port it left: a loop
* further out, behind an unmanaged switch. There is no pair to
* pick from, so that port holds itself down - and since it can
* only re-arm while it is receiving, that case degrades to the
* forward-delay pulse we had before rather than a real latch.
* The peer's number is validated by the callee, not here. */
stp_loop_peer = STP_I->port_id; /* 1-based, as we send it */
if (!stp_loop_peer)
return;
stp_loop_peer--;
/* A Port ID is (priority, number) and priority is compared
* first - stp_cnf_send() puts stp_pprio[] on the wire next to
* the number, so "stp port N prio" has to be able to decide
* which end of a looped pair keeps forwarding. Comparing the
* number alone would quietly ignore it. */
if (STP_I->port_prio != stp_pprio[port]) {
if (STP_I->port_prio < stp_pprio[port])
return; /* peer is better: it decides */
} else if (stp_loop_peer < port) {
return;
}
stp_loop_hold_peer(stp_loop_peer);
return;
}
/* Topology Change in transit. The flag arms a short window that our
* own BPDUs copy downstream (the TX side already sends TC while
* stp_tc_while runs) and that each further flagged BPDU refreshes, so
* it expires one hello after the neighbour stops - without shortening
* the long window a local change may have armed. The flush runs once,
* on the arming edge: everything learned on the other non-edge ports
* may sit behind the moved link and must be relearned. */
if (STP_I->flags & BPDU_FLAG_TC) {
if (!stp_tc_while) {
uint8_t i;
stp_tc_count++;
for (i = machine.min_port; i <= machine.max_port; i++)
if (i != port && (stp_pflags[i] & STP_PF_ENABLED)
&& !(stp_pflags[i] & STP_PF_OPEREDGE))
port_l2_forget_port(i);
}
if (stp_tc_while < ((uint16_t)stp_hello_s + 1) * STP_HZ)
stp_tc_while = ((uint16_t)stp_hello_s + 1) * STP_HZ;
}
stp_record_designated(port);
/* Better root than the one we know? The identifier is priority, system
* ID extension and MAC in that order: comparing the priority byte and
* then jumping to the MAC skipped the twelve bits in between, so two
* bridges differing only in the extension were ranked by MAC. */
if (cmpBytes((__xdata uint8_t *)&STP_I->root, (__xdata uint8_t *)&root_bridge, 8) < 0) {
/* Root guard: this port must never become our path to the root. */
if (stp_pflags[port] & STP_PF_ROOTGUARD) {
print_string("STP: root guard blocking port ");
print_port_nl(port);
stp_state_set(port, 0b01);
port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ;
stp_pflags[port] &= ~STP_PF_OPEREDGE;
return;
}
print_string("Updating Root bridge\n");
root_bridge.prio = STP_I->root.prio;
root_bridge.ext = STP_I->root.ext;
memcpy(root_bridge.mac, STP_I->root.mac, 6);
stp_root_port = port;
stp_tc_count++;
}
/* Refresh our cost to the root when the update comes in on the root port */
if (port == stp_root_port) {
/* Age of the information we now hold (see the TX note on the wire
* format); saturate rather than wrap on absurd input. */
stp_msg_age = (STP_I->age > 254) ? 254 : (uint8_t)STP_I->age;
root_bridge_cost = stp_dcost[port] + PCOST(port);
}
}
void stp_timers(void) __banked
{
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
port_hello[i]--;
if (!port_hello[i]) {
port_hello[i] = TIME_HELLO;
print_string("STP_HELLO port ");
print_byte(i); write_char('\n');
stp_cnf_send(i);
/* Refill the per-port tx budgets once per second (tx hold count) */
if (++stp_sec_tick >= STP_HZ) {
stp_sec_tick = 0;
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++)
stp_tx_budget[stp_i] = stp_txhold;
/* Link supervision. Without this the state machine never learns
* that a port lost carrier: it keeps the port in forwarding, keeps
* announcing on it, and never flushes what was learned behind it -
* yet losing a link is the most ordinary topology change there is.
* Once per second is soon enough, and it keeps register reads out
* of the 50 Hz tick. */
reg_read_m(RTL837X_REG_LINKS_STS);
stp_link_now = (uint16_t)sfr_data[1] | ((uint16_t)sfr_data[2] << 8);
if (stp_link_now != stp_link_prev) {
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) {
if (!(stp_pflags[stp_i] & STP_PF_ENABLED))
continue;
if (!((stp_link_now ^ stp_link_prev) >> stp_i & 1))
continue;
/* Either way the port must stop forwarding first. */
stp_state_set(stp_i, 0b01);
if ((stp_link_now >> stp_i) & 1) {
/* Carrier back: re-run the listen period rather than
* forwarding straight away - the segment may have been
* rewired while we were down. Auto edge still applies. */
port_timers[stp_i] = (uint16_t)stp_fwddelay_s * STP_HZ;
stp_pflags[stp_i] &= ~STP_PF_OPEREDGE;
stp_bpdu_age[stp_i] = 0;
} else {
port_timers[stp_i] = 0;
print_string("STP: link down, port blocking ");
print_port_nl(stp_i);
stp_topology_change(stp_i);
}
}
stp_link_prev = stp_link_now;
}
}
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) {
if (!(stp_pflags[stp_i] & STP_PF_ENABLED))
continue;
if (stp_bpdu_age[stp_i] < 0xffff)
stp_bpdu_age[stp_i]++;
/* Periodic hello */
if (port_hello[stp_i])
port_hello[stp_i]--;
if (!port_hello[stp_i]) {
port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ;
/* Only designated ports announce periodically: the root port is
* where our own root information comes FROM, and echoing it back
* there just feeds the upstream bridge its own data (and looks
* like a competing designated bridge on that segment). */
if (stp_i != stp_root_port)
stp_cnf_send(stp_i);
}
/* Promote a port out of blocking once its listen period expires
* with no reason to stay blocked (no better root heard: we are
* the designated bridge on that port). */
if (port_timers[stp_i]) {
if (!--port_timers[stp_i]) {
stp_loop_held[stp_i] = 0;
stp_state_set(stp_i, 0b11);
print_string("STP: port forwarding ");
print_port_nl(stp_i);
stp_topology_change(stp_i);
} else if ((stp_pflags[stp_i] & STP_PF_AUTOEDGE)
&& !stp_loop_held[stp_i]
&& stp_bpdu_age[stp_i] > STP_EDGE_DELAY) {
/* Auto edge: nothing talks (R)STP on this port - it is
* host-facing, go to forwarding without the full wait. */
port_timers[stp_i] = 0;
stp_pflags[stp_i] |= STP_PF_OPEREDGE;
stp_state_set(stp_i, 0b11);
print_string("STP: edge port forwarding ");
print_port_nl(stp_i);
}
}
}
if (stp_tc_while)
stp_tc_while--;
/* Age out a root that went silent: reclaim the tree. */
if (stp_root_port != 0xff
&& stp_bpdu_age[stp_root_port] > (uint16_t)stp_maxage_s * STP_HZ) {
print_string("STP: root aged out, claiming root\n");
stp_claim_root();
stp_tc_count++;
}
}
/* Reset all configuration to the 802.1D/802.1w defaults. Called once at boot
* (before the startup config replays "stp ..." commands over it). */
void stp_defaults(void) __banked
{
stp_prio = 0x80; /* high byte of the priority: 0x8000 is 32768 */
stp_hello_s = 2;
stp_maxage_s = 20;
stp_fwddelay_s = 15;
stp_rstp = 1;
stp_txhold = 6;
for (stp_i = 0; stp_i < 10; stp_i++) {
/* enabled, auto-edge on: host-facing ports go forwarding after
* 3 s of BPDU silence instead of the full forward delay */
stp_pflags[stp_i] = STP_PF_ENABLED | STP_PF_AUTOEDGE;
stp_pcost[stp_i] = 0; /* auto */
stp_pprio[stp_i] = 0x80;
stp_bpdu_age[stp_i] = 0;
port_timers[stp_i] = 0;
port_hello[stp_i] = 0;
stp_tx_budget[stp_i] = 6;
}
stp_tc_count = 0;
stp_tc_while = 0;
stp_claim_root();
}
/*
* Steer BPDUs while STP runs, and restore flooding when it stops.
* Changing a port's PVID while STP runs needs "stp off" then "stp on".
*/
static void stp_fdb_update(__xdata uint16_t pmask)
{
uint16_t stp_fdb_vid;
uint8_t stp_fdb_i;
/* Unlike LACPDUs (always untagged, so per-PVID entries suffice), BPDUs
* can arrive VLAN-tagged and then classify into the tag's VID - cover
* every VLAN that exists in the VLAN table, plus every port's PVID for
* the untagged case. A duplicate VID just overwrites the same slot. */
for (stp_fdb_vid = 1; stp_fdb_vid < 4095; stp_fdb_vid++) {
if (vlan_get(stp_fdb_vid) < 0)
continue;
if (!(sfr_data[0] & 0x02)) /* bit 1: VLAN table entry valid */
continue;
port_l2mc_set(0x00, stp_fdb_vid, pmask);
}
for (stp_fdb_i = machine.min_port; stp_fdb_i <= machine.max_port; stp_fdb_i++) {
stp_fdb_vid = port_pvid_get(stp_fdb_i);
port_l2mc_set(0x00, stp_fdb_vid, pmask);
}
}
@@ -206,34 +715,216 @@ void stp_setup(void) __banked
{
print_string("Enabling STP: ");
sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0;
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
// Set STP port state to blocking
// States are: 00 disable, 01 blocking, 10 learning, 11 forwarding
uint8_t bit_mask = 0b01 << ( (i << 1) & 0x7);
sfr_data[3 - (i >> 2)] |= bit_mask;
port_hello[i] = TIME_HELLO;
port_timers[i] = 0xa00; // 10 sec in blocking state
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) {
stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED);
stp_loop_held[stp_i] = 0;
stp_bpdu_age[stp_i] = 0;
stp_tx_budget[stp_i] = stp_txhold;
stp_tx_count[stp_i] = 0;
if (!(stp_pflags[stp_i] & STP_PF_ENABLED) || (stp_pflags[stp_i] & STP_PF_ADMEDGE)) {
/* not participating, or admin edge: forwarding immediately */
if (stp_pflags[stp_i] & STP_PF_ADMEDGE)
stp_pflags[stp_i] |= STP_PF_OPEREDGE;
sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b11 << ((stp_i << 1) & 0x7));
port_timers[stp_i] = 0;
} else {
/* listen first: blocking until the forward-delay expires */
sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b01 << ((stp_i << 1) & 0x7));
port_timers[stp_i] = (uint16_t)stp_fwddelay_s * STP_HZ;
}
sfr_data[1] |= 0x0f; // Do not block CPU-Port
reg_write_m(RTL837X_MSTP_STATES); // R5310-000d555f
port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ;
}
sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9)
reg_write_m(RTL837X_MSTP_STATES);
print_reg(RTL837X_MSTP_STATES); write_char('\n');
root_bridge.prio = 0x80; // This corresponds to 32768
root_bridge.ext = 0x00;
memcpy(root_bridge.mac, uip_ethaddr.addr, 6);
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) {
if (!(stp_pflags[stp_i] & STP_PF_ENABLED))
continue;
if (port_ingress_filter_get(stp_i) != VLAN_TAGGED)
continue;
print_string("STP: port ");
write_char('0' + machine.log_to_phys_port[stp_i]);
print_string(" admits tagged frames only - BPDUs are untagged and will not arrive\n");
}
/* Seed the carrier bitmap, so turning STP on does not report every
* port that was already down as a fresh topology change. */
reg_read_m(RTL837X_REG_LINKS_STS);
stp_link_prev = (uint16_t)sfr_data[1] | ((uint16_t)sfr_data[2] << 8);
stp_claim_root();
/* Take BPDUs to the CPU only - we are a participating bridge now. */
stp_fdb_update(PMASK_CPU);
}
void stp_off(void) __banked
{
sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0;
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) {
// Set STP port state to forwarding
// States are: 00 disable, 01 blocking, 10 learning, 11 forwarding
uint8_t bit_mask = 0b11 << ( (i << 1) & 0x7);
sfr_data[3 - (i >> 2)] |= bit_mask;
sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b11 << ((stp_i << 1) & 0x7));
stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED);
stp_loop_held[stp_i] = 0;
port_timers[stp_i] = 0;
}
sfr_data[1] |= 0x0f; // Do not block CPU-Port
sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9)
reg_write_m(RTL837X_MSTP_STATES);
/* Restore BPDU transparency: flood them again like an unmanaged switch. */
stp_fdb_update(PMASK_CPU | (machine_detected.isRTL8373 ? PMASK_9 : PMASK_6));
}
void stp_parse(void) __banked __reentrant
{
uint8_t port;
if (cmd_compare(1, "on")) {
print_string("STP enabled\n");
stp_enabled = 1;
stp_setup();
return;
}
if (cmd_compare(1, "off")) {
print_string("STP disabled\n");
stp_off();
stp_enabled = 0;
return;
}
if (cmd_compare(1, "status")) {
stp_status();
return;
}
if (cmd_words_len < 3)
goto err;
if (cmd_compare(1, "port")) {
if (cmd_words_len < 4)
goto err;
if (!cmd_parse_port_separator(cmd_words_b[2]))
goto err;
port = atoi_results_u8;
if (cmd_words_len < 5 && !cmd_compare(3, "on") && !cmd_compare(3, "off"))
goto err;
if (cmd_compare(3, "on")) {
stp_pflags[port] |= STP_PF_ENABLED;
stp_pflags[port] &= ~STP_PF_TRIPPED;
if (stp_enabled) { /* (re)join: listen first */
stp_state_set(port, 0b01);
port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ;
}
} else if (cmd_compare(3, "off")) {
stp_pflags[port] &= ~STP_PF_ENABLED;
if (stp_enabled)
stp_state_set(port, 0b11); /* plain forwarding */
} else if (cmd_compare(3, "edge")) {
/* Also drop the *operational* edge flag: it is what exempts the
* port from topology changes and lets it skip the listen period,
* so leaving it set would keep the old behaviour until the next
* "stp off"/"stp on". An admin edge is operational immediately. */
stp_pflags[port] &= ~(STP_PF_ADMEDGE | STP_PF_AUTOEDGE | STP_PF_OPEREDGE);
if (cmd_compare(4, "on"))
stp_pflags[port] |= STP_PF_ADMEDGE | STP_PF_OPEREDGE;
else if (cmd_compare(4, "auto"))
stp_pflags[port] |= STP_PF_AUTOEDGE;
else if (!cmd_compare(4, "off"))
goto err;
} else if (cmd_compare(3, "cost")) {
/* raw 802.1D value, 0..200000000; 0 = auto (speed-based) */
stp_cost_scratch = 0;
{
__xdata uint8_t *cp = &cmd_buffer[cmd_words_b[4]];
if (*cp < '0' || *cp > '9')
goto err;
while (*cp >= '0' && *cp <= '9') {
stp_cost_scratch = stp_cost_scratch * 10 + (*cp - '0');
cp++;
}
}
if (stp_cost_scratch > 200000000UL)
goto err;
stp_pcost[port] = stp_cost_scratch;
} else if (cmd_compare(3, "p2p")) {
if (cmd_compare(4, "auto"))
stp_pp2p[port] = 0;
else if (cmd_compare(4, "on"))
stp_pp2p[port] = 1;
else if (cmd_compare(4, "off"))
stp_pp2p[port] = 2;
else
goto err;
} else if (cmd_compare(3, "prio")) {
if (!atoi_byte(cmd_words_b[4]))
goto err;
if (atoi_results_u8 > 240 || (atoi_results_u8 & 0x0f))
goto err;
stp_pprio[port] = atoi_results_u8;
} else if (cmd_compare(3, "guard")) {
stp_pflags[port] &= ~(STP_PF_BPDUGUARD | STP_PF_ROOTGUARD);
if (cmd_compare(4, "bpdu"))
stp_pflags[port] |= STP_PF_BPDUGUARD;
else if (cmd_compare(4, "root"))
stp_pflags[port] |= STP_PF_ROOTGUARD;
else if (!cmd_compare(4, "none"))
goto err;
} else if (cmd_compare(3, "filter")) {
if (cmd_compare(4, "on"))
stp_pflags[port] |= STP_PF_FILTER;
else if (cmd_compare(4, "off"))
stp_pflags[port] &= ~STP_PF_FILTER;
else
goto err;
} else {
goto err;
}
return;
}
if (!atoi_byte(cmd_words_b[2])) {
if (cmd_compare(1, "version")) {
if (cmd_compare(2, "rstp"))
stp_rstp = 1;
else if (cmd_compare(2, "stp"))
stp_rstp = 0;
else
goto err;
return;
}
goto err;
}
stp_scratch = atoi_results_u8;
if (cmd_compare(1, "prio")) {
if (stp_scratch > 15)
goto err;
stp_prio = stp_scratch << 4; /* n * 4096, as the BPDU's high byte */
if (stp_root_port == 0xff)
stp_claim_root(); /* re-announce with the new priority */
} else if (cmd_compare(1, "hello")) {
if (stp_scratch < 1 || stp_scratch > 10)
goto err;
stp_hello_s = stp_scratch;
} else if (cmd_compare(1, "maxage")) {
if (stp_scratch < 6 || stp_scratch > 40)
goto err;
stp_maxage_s = stp_scratch;
} else if (cmd_compare(1, "fwd")) {
if (stp_scratch < 4 || stp_scratch > 30)
goto err;
stp_fwddelay_s = stp_scratch;
} else if (cmd_compare(1, "txhold")) {
if (stp_scratch < 1 || stp_scratch > 10)
goto err;
stp_txhold = stp_scratch;
} else {
goto err;
}
return;
err:
print_string("Error: stp on|off|status | prio <0-15> | hello <1-10> | maxage <6-40> | fwd <4-30> | txhold <1-10> | version rstp|stp | port <1-9> on|off|edge|cost|prio|guard|filter ...\n");
}
+43 -1
View File
@@ -6,7 +6,49 @@ void stp_in(void) __banked;
void stp_setup(void) __banked;
void stp_timers(void) __banked;
void stp_off(void) __banked;
void stp_parse(void) __banked __reentrant;
void stp_defaults(void) __banked;
#define TIME_HELLO 0x200 // 2 sec
/* Tick rate of stp_timers(), also used by the web UI. */
#define STP_HZ 50
/* Bridge identifier as carried in a BPDU (priority, extension, MAC). */
struct bridge {
uint8_t prio;
uint8_t ext;
uint8_t mac[6];
};
extern __xdata uint8_t stp_prio;
extern __xdata uint8_t stp_hello_s;
extern __xdata uint8_t stp_maxage_s;
extern __xdata uint8_t stp_fwddelay_s;
extern __xdata uint8_t stp_rstp;
extern __xdata uint8_t stp_txhold;
/* Per-port config/status flags (stp_pflags[]) */
#define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */
#define STP_PF_ADMEDGE 0x02 /* admin edge: forwarding immediately */
#define STP_PF_AUTOEDGE 0x04 /* auto edge: forward after 3 s without BPDU */
#define STP_PF_BPDUGUARD 0x08 /* disable port if a BPDU arrives */
#define STP_PF_ROOTGUARD 0x10 /* never accept a better root on this port */
#define STP_PF_FILTER 0x20 /* neither send nor accept BPDUs */
#define STP_PF_OPEREDGE 0x40 /* runtime: port went forwarding as an edge */
#define STP_PF_TRIPPED 0x80 /* runtime: disabled by BPDU guard */
extern __xdata uint8_t stp_pflags[10];
extern __xdata uint32_t stp_pcost[10];
extern __xdata uint8_t stp_pprio[10];
extern __xdata uint8_t stp_pp2p[10];
extern __xdata struct bridge stp_dbridge[10];
extern __xdata uint16_t stp_dpid[10];
extern __xdata uint32_t stp_dcost[10];
extern __xdata uint16_t stp_bpdu_age[10];
extern __xdata struct bridge root_bridge;
extern __xdata uint32_t root_bridge_cost;
extern __xdata uint8_t stp_root_port;
extern __xdata uint16_t stp_tc_count;
#endif
+297 -98
View File
@@ -25,6 +25,7 @@
#include "machine.h"
#include "phy.h"
#include "syslog.h"
#include "httpd/page_impl.h"
extern __code const struct machine machine;
extern __xdata uint32_t flash_size;
@@ -117,9 +118,12 @@ __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE+2];
__xdata uint16_t rx_packet_vlan;
__xdata uint16_t management_vlan;
__xdata bool frame_tagged;
__xdata uint8_t tx_seq;
__xdata uint8_t stpEnabled;
__xdata bool stp_enabled;
__xdata uint8_t igmpEnabled;
__xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */
__code uint16_t bit_mask[16] = {
0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080,
@@ -137,11 +141,27 @@ __xdata char sfp_module_vendor[2][17];
__xdata char sfp_module_model[2][17];
__xdata char sfp_module_serial[2][17];
__xdata uint8_t sfp_options[2];
__xdata uint8_t sfp_buf[16]; /* scratch for one I2C transaction, the controller reads at most 16 bytes */
__xdata uint8_t sfp_speed[2];
__xdata uint8_t sfp_quirks[2];
__xdata bool button_last;
__xdata uint8_t button_sec_counter_last;
volatile __bit tx_buf_empty;
__code enum sfp_quirk {
SFP_QUIRK_DDM = (1 << 0),
};
struct sfp_quirk_entry {
__code char *vendor; // Set vendor or model to 0 to act as wildcard
__code char *model;
uint8_t quirks;
};
static __code struct sfp_quirk_entry sfp_quirk_table[] = {
{ "QSFPTEK", "QT-SFP+-T", SFP_QUIRK_DDM },
};
struct eth_in {
struct uip_eth_addr dst;
struct uip_eth_addr src;
@@ -185,6 +205,9 @@ struct nonq_frame {
// The output frame structure with 802.1Q field and the padding moved before the buffer-start
#define FRAME_Q ((__xdata struct q_frame *)&uip_buf[0])
// Ether-type of the output frame, which is the RTL tag on a CPU-tagged frame
#define FRAME_ETHERTYPE (*(__xdata uint16_t *)&uip_buf[RTL_FRAME_DESC_SIZE + 2 * sizeof(struct uip_eth_addr)])
void isr_timer0(void) __interrupt(1)
{
}
@@ -271,6 +294,12 @@ void print_string_no_syslog(__code char *p)
write_char_no_syslog(*p++);
}
void print_string_newline_no_syslog(__code char *p)
{
write_char_no_syslog('\n');
print_string_no_syslog(p);
}
void print_string_x(__xdata char *p)
{
while (*p)
@@ -286,20 +315,20 @@ void memcpy(__xdata void * __xdata dst, __xdata const void * __xdata src, uint16
*d++ = *s++;
}
void memcpyc(register __xdata uint8_t *dst, register __code uint8_t *src, register uint16_t len)
void memcpyc(__xdata uint8_t *dst, __code uint8_t *src, uint16_t len)
{
while (len--)
*dst++ = *src++;
}
void memset(register __xdata uint8_t *dst, register __xdata uint8_t v, register uint8_t len)
void memset(__xdata uint8_t *dst, __xdata uint8_t v, uint8_t len)
{
while (len--)
*dst++ = v;
}
uint16_t strtox(register __xdata uint8_t *dst, register __code const char *s)
uint16_t strtox(__xdata uint8_t *dst, __code const char *s)
{
__xdata uint8_t *b = dst;
while (*s)
@@ -309,7 +338,7 @@ uint16_t strtox(register __xdata uint8_t *dst, register __code const char *s)
}
uint16_t strlen(register __code const char *s)
uint16_t strlen(__code const char *s)
{
uint16_t l = 0;
while (s[l])
@@ -318,7 +347,7 @@ uint16_t strlen(register __code const char *s)
}
uint16_t strlen_x(register __xdata const char *s)
uint16_t strlen_x(__xdata const char *s)
{
uint16_t l = 0;
while (s[l])
@@ -327,6 +356,47 @@ uint16_t strlen_x(register __xdata const char *s)
}
char strcmp(__xdata const uint8_t *a, __code const uint8_t *b)
{
uint8_t i = 0;
while (b[i] && (b[i] == a[i]))
i++;
if (a[i] < b[i])
return -1;
else if (a[i] > b[i])
return 1;
return 0;
}
/*
* True when b is a prefix of a. Unlike strcmp() the byte after the match is not
* compared, and unlike is_word_x() it need not be a separator.
*/
bool strstart(__xdata const uint8_t *a, __code const uint8_t *b)
{
uint8_t i = 0;
while (b[i] && (b[i] == a[i]))
i++;
return !b[i];
}
bool strstart_x(__xdata const uint8_t *a, __xdata const uint8_t *b)
{
uint8_t i = 0;
while (b[i] && (b[i] == a[i]))
i++;
return !b[i];
}
void print_short(uint16_t a)
{
// allocating the registers first improves the sdcc code here
@@ -585,13 +655,21 @@ void get_random_32(void)
* data will be stored in the rx_header structure
* len is the length of data to be transferred
*/
void nic_rx_header(uint16_t ring_ptr)
bool nic_rx_header(uint16_t ring_ptr)
{
uint16_t buffer = (uint16_t) &rx_headers[0];
uint16_t guard = 0;
SFR_NIC_DATA_U16LE = buffer;
SFR_NIC_RING_U16LE = ring_ptr;
SFR_NIC_CTRL = 1;
do { } while (SFR_NIC_CTRL != 0);
while (SFR_NIC_CTRL != 0) {
if (++guard == 0) {
print_string("NIC: RX header transfer did not complete\n");
return false;
}
}
return true;
}
@@ -601,8 +679,10 @@ void nic_rx_header(uint16_t ring_ptr)
* data will be returned in the xmem buffer points to
* ring_ptr is the current position of the RX Ring on the ASIC side
*/
void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr)
bool nic_rx_packet(uint16_t buffer, uint16_t ring_ptr)
{
uint16_t guard = 0;
SFR_NIC_DATA_U16LE = buffer;
SFR_NIC_RING_U16LE = ring_ptr;
@@ -614,7 +694,13 @@ void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr)
print_short(len);
#endif
SFR_NIC_CTRL = len;
do { } while (SFR_NIC_CTRL != 0);
while (SFR_NIC_CTRL != 0) {
if (++guard == 0) {
print_string("NIC: RX transfer did not complete\n");
return false;
}
}
return true;
}
@@ -624,14 +710,13 @@ void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr)
void nic_tx_packet(uint16_t ring_ptr)
{
uint16_t len;
uint16_t guard = 0;
/* If we have a management VLAN, we have inserted a dot1Q-tag into the frame and
* the frame starts at the beginning of uip_buf with the RTL TX descriptor,
* otherwise the frame is a normal Ethernet frame which starts with
* an RTL TX descriptor being padded at the beginning, in the second case
* we need to skip the padding for the sending of the frame.
/* A frame that got a dot1Q tag was shifted forward over its padding, so it
* starts at uip_buf and carries the q_frame layout. One that did not keeps
* the padding in front and the nonq_frame layout, so the padding is skipped.
*/
if (management_vlan) {
if (frame_tagged) {
SFR_NIC_DATA_U16LE = (uint16_t) uip_buf;
len = FRAME_Q->len;
/*
@@ -658,7 +743,12 @@ void nic_tx_packet(uint16_t ring_ptr)
len += 0xf;
len >>= 3;
SFR_NIC_CTRL = len;
do { } while (SFR_NIC_CTRL != 0);
while (SFR_NIC_CTRL != 0) {
if (++guard == 0) {
print_string("NIC: TX transfer did not complete\n");
return;
}
}
}
@@ -747,6 +837,19 @@ void print_reg(uint16_t reg)
print_sfr_data();
}
// Print the physical port of a logical port number.
void print_phys_port(uint8_t port)
{
if (port < CPU_PORT)
write_char(machine.log_to_phys_port[port] + '0');
else if (port == CPU_PORT)
print_string("CPU");
else {
print_string("UNKNOWN ");
write_char(port + '0');
}
}
/*
// TODO: This uses 2 DSEG bytes and is not used!
@@ -1003,37 +1106,6 @@ void sds_config(uint8_t sds, uint8_t mode)
}
/*
* Read a register of the EEPROM via I2C
*/
uint8_t sfp_read_reg(uint8_t slot, uint8_t reg)
{
if (reg & 0x80) { // Configure SFP readings address (0x51) as I2C device address
reg &= 0x7f;
REG_WRITE(RTL837X_REG_I2C_CTRL, 0x00, 0x1 << (I2C_MEM_ADDR_WIDTH-16) | 0, 0x51 >> 5, (0x51 << 3) & 0xff);
} else {
REG_WRITE(RTL837X_REG_I2C_CTRL, 0x00, 0x1 << (I2C_MEM_ADDR_WIDTH-16) | 0, 0x50 >> 5, (0x50 << 3) & 0xff);
}
reg_read_m(RTL837X_REG_I2C_CTRL);
sfr_mask_data(1, 0xfc, i2c_bus_from_scl_pin(machine.sfp_port[slot].i2c.scl) << 5 | i2c_bus_from_sda_pin(machine.sfp_port[slot].i2c.sda) << 2);
reg_write_m(RTL837X_REG_I2C_CTRL);
REG_WRITE(RTL837X_REG_I2C_IN, 0, 0, 0, reg);
// Execute I2C Read
reg_bit_set(RTL837X_REG_I2C_CTRL, 0);
// Wait for execution to finish
do {
reg_read_m(RTL837X_REG_I2C_CTRL);
} while (sfr_data[3] & 0x1);
reg_read_m(RTL837X_REG_I2C_OUT);
return sfr_data[3];
}
/*
* Adds TX Header to uip_buf and calls nic_tx_packet to send the packet
* over the wire
@@ -1047,8 +1119,11 @@ void tcpip_output(void)
FRAME->len = uip_len;
FRAME->reserved_2[0] = 0x00; FRAME->reserved_2[1] = 0x00;
// For the management VLAN we insert an 802.1Q VLAN tag
if (management_vlan) {
// For the management VLAN we insert an 802.1Q VLAN tag, but never into a
// CPU-tagged frame, where the ASIC expects its tag right behind the addresses
frame_tagged = false;
if (management_vlan && FRAME_ETHERTYPE != HTONS(RTL_FRAME_TAG_ID)) {
frame_tagged = true;
// Shift the ethernet header before the HW type including the rtl_frame_desc to the beginning of uip_buf
// to allow space to insert the dot 1Q tag
for (uint8_t i = 0; i < sizeof(struct q_frame) - DOT_1Q_TAG_SIZE; i++)
@@ -1082,7 +1157,10 @@ void handle_rx(void)
uint16_t ring_ptr = ((uint16_t)sfr_data[2]) << 8;
ring_ptr |= sfr_data[3];
ring_ptr <<= 3;
nic_rx_header(ring_ptr);
if (!nic_rx_header(ring_ptr)) {
REG_SET(RTL837X_REG_NIC_RXCMD, 1);
return;
}
#ifdef RXTXDBG
__xdata uint8_t *ptr = rx_headers;
print_string("RX on port "); print_byte(rx_headers[3] & 0xf);
@@ -1092,7 +1170,10 @@ void handle_rx(void)
write_char(' ');
}
#endif
nic_rx_packet((uint16_t) &uip_buf[0], ring_ptr + 8);
if (!nic_rx_packet((uint16_t) &uip_buf[0], ring_ptr + 8)) {
REG_SET(RTL837X_REG_NIC_RXCMD, 1);
return;
}
#ifdef RXTXDBG
print_string("\n<< ");
@@ -1113,14 +1194,14 @@ void handle_rx(void)
print_byte(uip_buf[3]); print_byte(uip_buf[4]); print_byte(uip_buf[5]); write_char('\n');
print_string(" MGMT-VLAN: "); print_short(management_vlan); write_char('\n');
#endif
if (stpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 && uip_buf[2] == 0xc2 // STP packet?
if (stp_enabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 && uip_buf[2] == 0xc2 // STP packet?
&& uip_buf[3] == 0x00 && uip_buf[4] == 0x00 && uip_buf[5] == 0x00) {
stp_in();
if (uip_len) {
print_string("STP TX\n");
tcpip_output();
}
} else if (uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet?
} else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet?
&& uip_buf[3] == 0x00 && uip_buf[4] == 0x00 && uip_buf[5] == 0x16) {
igmp_packet_handler();
if (uip_len) {
@@ -1172,7 +1253,7 @@ void handle_tx(void)
}
static inline uint8_t sfp_rate_to_sds_config(register uint8_t rate)
static inline uint8_t sfp_rate_to_sds_config(uint8_t rate)
{
if (rate == 0x1 || rate == 0x2)
return SDS_100FX;
@@ -1180,37 +1261,75 @@ static inline uint8_t sfp_rate_to_sds_config(register uint8_t rate)
return SDS_1000BX_FIBER;
if (rate >= 0x19 && rate <= 0x20) // Ethernet 2.5 GBit
return SDS_HSG;
if (rate >= 0x63 && rate < 0x70)
if (rate >= 0x62 && rate < 0x70)
return SDS_10GR;
return 0xff;
}
void sfp_print_info(uint8_t sfp)
bool sfp_print_info(uint8_t sfp)
{
// This loops over the Vendor-name, Vendor OUI, Vendor PN and Vendor rev ASCII fields
for (uint8_t i = 20; i < 60; i++) {
if (i >= 36 && i < 40) // Skip Non-ASCII codes
for (uint8_t i = 16; i < 64; i++) {
if (!(i & 0xf) && !sfp_read_block(sfp, i, 16))
return false;
if (i < 20 || i >= 60 || (i >= 36 && i < 40)) // Skip Non-ASCII codes
continue;
uint8_t c = sfp_read_reg(sfp, i);
uint8_t c = sfp_buf[i & 0xf];
if (c)
write_char(c);
}
print_string("\n");
return true;
}
void sfp_get_info(uint8_t sfp)
// Normalize strings from EEPROM by removing any trailing spaces; this allows simpler comparisons
bool sfp_read_field(__xdata char *dst, uint8_t sfp, uint8_t start, uint8_t length) __reentrant
{
for (uint8_t i = 20; i < 36; i++)
sfp_module_vendor[sfp][i-20] = sfp_read_reg(sfp, i);
sfp_module_vendor[sfp][16] = '\0';
for (uint8_t i = 40; i < 56; i++)
sfp_module_model[sfp][i-40] = sfp_read_reg(sfp, i);
sfp_module_model[sfp][16] = '\0';
for (uint8_t i = 68; i < 84; i++)
sfp_module_serial[sfp][i-68] = sfp_read_reg(sfp, i);
sfp_module_serial[sfp][16] = '\0';
if (!sfp_read_block(sfp, start, length))
return false;
dst[length] = NUL;
memcpy(dst, sfp_buf, length);
while (length > 0 && dst[--length] == ' ')
dst[length] = NUL;
return true;
}
bool sfp_get_info(uint8_t sfp)
{
if (!sfp_read_field(sfp_module_vendor[sfp], sfp, 20, 16))
return false;
if (!sfp_read_field(sfp_module_model[sfp], sfp, 40, 16))
return false;
return sfp_read_field(sfp_module_serial[sfp], sfp, 68, 16);
}
void sfp_apply_quirks(uint8_t sfp) __reentrant
{
sfp_quirks[sfp] = 0;
for (uint8_t i = 0; i < sizeof(sfp_quirk_table) / sizeof(*sfp_quirk_table); i++) {
if (!sfp_quirk_table[i].vendor || !strcmp(sfp_module_vendor[sfp], sfp_quirk_table[i].vendor)) {
if (!sfp_quirk_table[i].model || !strcmp(sfp_module_model[sfp], sfp_quirk_table[i].model)) {
sfp_quirks[sfp] |= sfp_quirk_table[i].quirks;
}
}
}
if (sfp_quirks[sfp] & SFP_QUIRK_DDM) {
if (!(sfp_options[sfp] & 0x40)) {
// The module reports that DDM is not implemented, but try a dummy read to confirm
// 0xff would mean a failed I2C read or an impossible (per spec) voltage greater than 6.5V
if (sfp_read_block(sfp, 226, 1) && sfp_buf[0] != 0xff) {
sfp_options[sfp] |= 0x40;
}
}
}
}
@@ -1230,17 +1349,17 @@ void setup_sfp_gpio(void)
}
}
void handle_sfp(void)
static bool sfp_module_read(uint8_t sfp)
{
for (uint8_t sfp = 0; sfp < machine.n_sfp; sfp++) {
if (!gpio_pin_test(machine.sfp_port[sfp].pin_detect)) {
if (sfp_pins_last & (0x1 << (sfp << 2))) {
sfp_pins_last &= ~(0x01 << (sfp << 2));
print_string("\n<MODULE INSERTED> Slot: "); write_char('1' + sfp);
uint8_t rate;
// Read Reg 11: Encoding, see SFF-8472 and SFF-8024
// Read Reg 12: Signalling rate (including overhead) in 100Mbit: 0xd: 1Gbit, 0x67:10Gbit
delay(100); // Delay, because some modules need time to wake up
uint8_t rate = sfp_read_reg(sfp, 12);
if (!sfp_read_block(sfp, 11, 2))
return false;
rate = sfp_buf[1];
if (sfp_speed[sfp] == SFP_SPEED_100M)
rate = 0x1;
else if (sfp_speed[sfp] == SFP_SPEED_1G)
@@ -1250,12 +1369,36 @@ void handle_sfp(void)
else if (sfp_speed[sfp] == SFP_SPEED_10G)
rate = 0x69;
print_string(" Rate: "); print_byte(rate); // Normally 1, but 0 for DAC, can be ignored?
print_string(" Encoding: "); print_byte(sfp_read_reg(sfp, 11));
print_string(" Module: "); sfp_print_info(sfp);
print_string(" Encoding: "); print_byte(sfp_buf[0]);
print_string(" Module: ");
if (!sfp_print_info(sfp))
return false;
print_string("\n");
sfp_options[sfp] = sfp_read_reg(sfp, 92);
sfp_get_info(sfp);
if (!sfp_read_block(sfp, 92, 1))
return false;
sfp_options[sfp] = sfp_buf[0];
if (!sfp_get_info(sfp))
return false;
sfp_apply_quirks(sfp);
sds_config(machine.sfp_port[sfp].sds, sfp_rate_to_sds_config(rate));
return true;
}
void handle_sfp(void)
{
for (uint8_t sfp = 0; sfp < machine.n_sfp; sfp++) {
if (!gpio_pin_test(machine.sfp_port[sfp].pin_detect)) {
if (sfp_pins_last & (0x1 << (sfp << 2))) {
sfp_pins_last &= ~(0x01 << (sfp << 2));
print_string("\n<MODULE INSERTED> Slot: "); write_char('1' + sfp);
if (!sfp_module_read(sfp)) {
print_string("SFP: an I2C read failed, retrying on the next poll\n");
sfp_pins_last |= 0x01 << (sfp << 2);
}
}
} else {
if (!(sfp_pins_last & (0x1 << (sfp << 2)))) {
@@ -1408,7 +1551,7 @@ void idle(void)
if (!machine.n_10g && p5_last != p5) {
if (p5 == 0x5) // 2.5GBit Mode
sds_config(0, SDS_HISGMII);
else if (p5 == 0x2) // 1GBit
else // 1GBit and 100Mbit
sds_config(0, SDS_SGMII);
}
if (machine.n_10g)
@@ -1428,7 +1571,7 @@ void idle(void)
// Check UIP for packets to transmit
handle_tx();
// If STP protocol enabled, decrease STP timers to trigger actions
if (stpEnabled) {
if (stp_enabled) {
if (!stp_clock) {
stp_clock = STP_TICK_DIVIDER;
stp_timers();
@@ -1703,18 +1846,35 @@ void init_smi(void)
/* Set the SMI(i.e.I2C) type for PHY polling, 0b01 is 2.5/10G PHY. Disable (0b00) for the SFP-ports
* which are at port 8 and additionally at port 3 for a dual SFP device
*/
// Default: 0x00005555
// Workaround for SDCC BUG 4070: SFR_DATA_U32 = 0x00005555;
SFR_DATA_U16_UPPER = 0x0000;
SFR_DATA_U16 = 0x5555;
if (machine.n_10g == 2) {
REG_SET(RTL837X_REG_SMI_MAC_TYPE, 0x00015555);
} else {
REG_SET(RTL837X_REG_SMI_MAC_TYPE, machine.n_sfp == 2 ? 0x00005515 : 0x00005555);
}
// 0x00015555, only change the bytes that differs from the default.
SFR_DATA_16 = 0x01;
} else if (machine.n_sfp == 2)
// 0x00005515
SFR_DATA_0 = 0x15;
reg_write(RTL837X_REG_SMI_MAC_TYPE);
// Configure polling of all PHYs by the MAC to detect link-state changes
if (machine_detected.isRTL8373) {
REG_SET(RTL837X_REG_SMI_PORT_POLLING, 0xff);
// Default: 0x000000ff
// Workaround for SDCC BUG 4070: SFR_DATA_U32 = 0x000000ff;
SFR_DATA_U16_UPPER = 0x0000;
SFR_DATA_U16 = 0x00ff;
if (!machine_detected.isRTL8373) {
if (machine.n_sfp == 2) {
// 0x000000f0, only change the bytes that differs from the default.
SFR_DATA_0 = 0xf0;
} else {
REG_SET(RTL837X_REG_SMI_PORT_POLLING, machine.n_sfp == 2 ? 0xf0 : 0x1f8);
// 0x000001f8, only change the bytes that differs from the default.
SFR_DATA_8 = 0x01;
SFR_DATA_0 = 0xf8;
}
}
reg_write(RTL837X_REG_SMI_PORT_POLLING);
// Enable MDC
reg_read_m(RTL837X_REG_SMI_CTRL);
sfr_mask_data(1, 0, 0x70); // Set bits 12-14 to enable MDC for SMI0-SMI2
@@ -1945,6 +2105,34 @@ void check_and_flash_update_image(void)
}
}
/* Give the switch a name carrying the tail of its MAC, so several of them on
* one network are distinguishable out of the box. Called after the startup
* config has been replayed and returns at once if that config already set a
* name, so a configured switch does no work for it (suggested in review).
*
* Written without a loop on purpose. Locals - counters and pointers alike -
* land in the 8051's internal-RAM overlay, and on an image with LACP and STP
* both enabled that overlay is exhausted: a loop here makes the linker fail
* with "Could not get 8 consecutive bytes in internal RAM for area OSEG".
* Moving the code into its own function does not help; the overlay is shared
* across the whole image. Hoisting the locals to xdata does not help either,
* because itohex() is inline and brings its own frame. */
void set_hostname_default(void)
{
if (hostname[0] != NUL)
return;
strcpy((__xdata uint8_t *)hostname, "RTLPlayground-");
hostname[14] = hex[uip_ethaddr.addr[3] >> 4];
hostname[15] = hex[uip_ethaddr.addr[3] & 0xf];
hostname[16] = hex[uip_ethaddr.addr[4] >> 4];
hostname[17] = hex[uip_ethaddr.addr[4] & 0xf];
hostname[18] = hex[uip_ethaddr.addr[5] >> 4];
hostname[19] = hex[uip_ethaddr.addr[5] & 0xf];
hostname[20] = NUL;
}
void main(void)
{
ticks = 0;
@@ -2023,20 +2211,28 @@ void main(void)
uip_ipaddr(&uip_hostaddr, ownIP[0], ownIP[1], ownIP[2], ownIP[3]);
uip_ipaddr(&uip_draddr, gatewayIP[0], gatewayIP[1], gatewayIP[2], gatewayIP[3]);
uip_ipaddr(&uip_netmask, netmask[0], netmask[1], netmask[2], netmask[3]);
uip_ethaddr.addr[0] = 0xff;
if (machine.mac_flash_offset) {
flash_region.addr = machine.mac_flash_offset;
flash_region.len = FLASH_BUF_SIZE;
flash_read_bulk(flash_buf);
// accept only a real unicast, globally-administered address (reject blank/LAA/multicast/all-zero OUI)
if (flash_buf[0] != 0xff && !(flash_buf[0] & 0x03) && (flash_buf[0] | flash_buf[1] | flash_buf[2])) {
uip_ethaddr.addr[0] = flash_buf[0]; uip_ethaddr.addr[1] = flash_buf[1];
uip_ethaddr.addr[2] = flash_buf[2]; uip_ethaddr.addr[3] = flash_buf[3];
uip_ethaddr.addr[4] = flash_buf[4]; uip_ethaddr.addr[5] = flash_buf[5];
}
}
if (uip_ethaddr.addr[0] == 0xff) { // no valid flash MAC -> generate locally-administered
reg_read_m(RTL837X_REG_CHIP_UUID);
#ifdef DEBUG
print_string("SoC UUID: "); print_sfr_data();
#endif
uip_ethaddr.addr[0] = 0x06; // LAA prefix
uip_ethaddr.addr[3] = sfr_data[0] ^ sfr_data[3];
uip_ethaddr.addr[4] = sfr_data[1] ^ sfr_data[3];
uip_ethaddr.addr[5] = sfr_data[2] ^ sfr_data[3];
reg_read_m(RTL837X_REG_CHIP_LOT_NO);
#ifdef DEBUG
print_string(", LOT: "); print_sfr_data(); write_char(' ');
#endif
uip_ethaddr.addr[1] = sfr_data[0] ^ sfr_data[2];
uip_ethaddr.addr[2] = sfr_data[1] ^ sfr_data[3];
}
print_string("Setting MAC to: ");
print_byte(uip_ethaddr.addr[0]); write_char(':'); print_byte(uip_ethaddr.addr[1]); write_char(':');
print_byte(uip_ethaddr.addr[2]); write_char(':'); print_byte(uip_ethaddr.addr[3]); write_char(':');
@@ -2081,7 +2277,8 @@ void main(void)
REG_SET(RTL837X_REG_SEC_COUNTER, 0x3); write_char(' ');
print_reg(RTL837X_REG_SEC_COUNTER);
#endif
stpEnabled = 0;
stp_enabled = 0;
stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */
nic_setup();
vlan_setup();
port_l2_setup();
@@ -2109,6 +2306,8 @@ void main(void)
early_boot_handle_button();
execute_config();
/* After the config: a name from it wins, otherwise derive one. */
set_hostname_default();
print_cmd_prompt();
idle_ready = 1;
+8 -8
View File
@@ -30,16 +30,16 @@ void syslog_start(void) __banked
uip_ipaddr(server_ip, state.server_ip[0], state.server_ip[1], state.server_ip[2], state.server_ip[3]);
state.syslog_conn = uip_udp_new(&server_ip, HTONS(514));
if (state.syslog_conn == 0) {
print_string_no_syslog("Failed to create a new UDP client\n");
print_string_newline_no_syslog("Failed to create a new UDP client");
return;
}
print_string_no_syslog("Started syslog to IP ");
print_string_newline_no_syslog("Started syslog to IP ");
itoa(state.server_ip[0]); write_char('.'); itoa(state.server_ip[1]); write_char('.');
itoa(state.server_ip[2]); write_char('.'); itoa(state.server_ip[3]); write_char('\n');
state.enabled = 1;
}
else {
print_string_no_syslog("Syslog is already running\n");
print_string_newline_no_syslog("Syslog is already running");
}
}
@@ -49,9 +49,9 @@ void syslog_stop(void) __banked
if (state.syslog_conn != 0) {
uip_udp_remove(state.syslog_conn);
state.syslog_conn = 0;
print_string_no_syslog("Stopped syslog\n");
print_string_newline_no_syslog("Stopped syslog");
} else {
print_string_no_syslog("Syslog is not running\n");
print_string_newline_no_syslog("Syslog is not running");
}
}
@@ -62,19 +62,19 @@ void syslog_callback(uint16_t lport) __banked
if ((state.readptr != state.writeptr) && state.line_available)
{
int16_t log_size = state.writeptr - state.readptr;
__xdata int16_t log_size = state.writeptr - state.readptr;
if (log_size < 0)
log_size += LOGBUF_SIZE;
// Skipping linefeeds at the start of the log line
uint16_t log_start = state.readptr;
__xdata uint16_t log_start = state.readptr;
while (log_size > 0 && logbuf[log_start] == '\n') {
log_start = (log_start + 1) & (LOGBUF_SIZE - 1);
log_size--;
}
// Skipping linefeeds and whitespaces at the end of the log line
uint16_t log_end = state.writeptr;
__xdata uint16_t log_end = state.writeptr;
while ( (log_size > 0) &&
((logbuf[(log_end-1) & (LOGBUF_SIZE - 1)] == '\n') ||
(logbuf[(log_end-1) & (LOGBUF_SIZE - 1)] == ' ')))
+12 -3
View File
@@ -500,6 +500,10 @@ struct Server serverConstructor(int port, void (*launch)(struct Server *server))
exit(EXIT_FAILURE);
}
int reuse = 1;
if (setsockopt(server.socket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
perror("setsockopt(SO_REUSEADDR) failed");
if (bind(server.socket, (struct sockaddr*)&server.address, sizeof(server.address)) < 0) {
perror("Failed to bind socket...\n");
exit(EXIT_FAILURE);
@@ -554,8 +558,12 @@ char *scan_header(char *p)
break;
if (is_word(p, "\nContent-Type:"))
content_type = p + 15;
else if (is_word(p, "\nCookie:"))
session = p + 17;
else if (is_word(p, "\nCookie:")) {
session = p + 9;
while (*session == ' ') session++;
const char *s = strstr(session, "session=");
if (s) session = s + 8;
}
}
if (content_type && is_word(content_type, "multipart/form-data; boundary")) {
printf("Found multiplart\n");
@@ -799,7 +807,8 @@ void launch(struct Server *server)
printf("Password accepted!\n");
response = "HTTP/1.1 302 Found\r\n"
"Location: index.html\r\n"
"Set-Cookie: session=" SESSION_ID "; SameSite=Strict\r\n";
"Set-Cookie: session=" SESSION_ID "; Path=/; SameSite=Strict\r\n"
"\r\n";
} else {
response = "HTTP/1.1 302 Found\r\n"
"Location: login.html\r\n\r\n";
+149
View File
@@ -0,0 +1,149 @@
# LED GPIO Utilities
This directory contains utility talking to RTL837x switch IC via I2C bus.
These scripts are designed to help with monitoring, debugging GPIO, and identifying LED configurations.
(Only tested in Linux)
## Hardware requirements
To use these utilities, you need:
- A hardware dongle that can communicate with I2C devices. One example is the [I2C-Pico-USB](https://github.com/dquadros/I2C-Pico-USB) which provides USB-to-I2C connectivity.
- Connection between the hardware dongle and the RTL837x's I2C communication port.
## Scripts
### 1. `i2c_read_rtl_gpio.py`
This script reads RTL GPIO register values via I2C and displays changes in GPIO states.
**Features:**
- Reads live RTL GPIO register values via I2C (default address 0x5C)
- Monitors GPIO changes with delta detection
- Allows specification of I2C bus, sleep interval, and ignored GPIO pins
- Shows changes in real-time with GPIO index display
**Usage:**
```bash
# Basic usage (defaults to I2C bus 1, 2s sleep interval)
python3 i2c_read_rtl_gpio.py
# Specify I2C bus
python3 i2c_read_rtl_gpio.py --i2c-bus 0
# Specify sleep interval in seconds
python3 i2c_read_rtl_gpio.py --sleep-interval 5
# Ignore specific GPIO pins
python3 i2c_read_rtl_gpio.py --ignored-ios 28 31 34 44
# Combine options
python3 i2c_read_rtl_gpio.py --i2c-bus 2 --sleep-interval 1 --ignored-ios 28 31
```
**Output Format:**
- Displays register address and data in hex format
- Shows GPIO pins that have changed since last read
- Example: `0044: 00 00 00 00 00 00 00 00`
### 2. `i2c_dump_rtl_regs.py`
This script dumps all register values from an RTL device via I2C.
**Features:**
- Dumps registers sequentially from address 0x0000 to 0xFFFF
- Reads 16 bytes at a time for efficiency
- Configurable I2C bus number
- Provides complete register dump in hex format
**Usage:**
```bash
# Basic usage (defaults to I2C bus 1)
python3 i2c_dump_rtl_regs.py >reg_dump.txt
# Specify I2C bus
python3 i2c_dump_rtl_regs.py --bus 0 >reg_dump.txt
# Or using short option
python3 i2c_dump_rtl_regs.py -b 2 >reg_dump.txt
```
**Output Format:**
- Displays address and 16 bytes of data in hex format
- Example: `0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00`
### 3. `dec_leds_from_dump.py`
This script decodes LED configuration from a register dump file ( the output from `i2c_dump_rtl_regs.py`).
**Features:**
- Parses register dump files (reg_dump.txt format)
- Decodes LED pad configurations and LED sets
- Maps LED types to bit positions based on configuration
- Outputs LED mux configuration and set mappings
**Usage:**
```bash
# Must be run in same directory as reg_dump.txt
python3 dec_leds_from_dump.py
```
**Output Format:**
- LED pad configuration (hex values for each pad)
- LED set configurations with descriptions of LED types
- Port selection mappings
**Requirements:**
- Requires a `reg_dump.txt` file containing the register dump output
## Requirements
All scripts require:
- Python 3
- `smbus2` Python package
(Depend upon Linux distribution) Install with:
```bash
apt install python3-smbus2
```
OR
```bash
pip3 install smbus2
```
## Common Use Cases
Both `i2c_read_rtl_gpio.py` and `i2c_dump_rtl_regs.py` shall run with the **original** firmware, not RTLPlayground firmware.
### Monitoring GPIO Changes
```bash
# Monitor GPIO changes on bus 1 with 1-second intervals
python3 i2c_read_rtl_gpio.py --i2c-bus 1 --sleep-interval 1
```
### Register Analysis
```bash
# Dump all device registers
python3 i2c_dump_rtl_regs.py --bus 2 > reg_dump.txt
# Analyze the register dump to understand LED configuration
python3 dec_leds_from_dump.py
```
## Configuration
All scripts support command-line arguments for flexible configuration:
- `--i2c-bus` or `-b`: Specify I2C bus (default: 1)
- `--sleep-interval` or `-s`: Sleep between reads in seconds (default: 2)
- `--ignored-ios` or `-i`: GPIO pins to ignore (default: [28, 31, 34, 44])
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
regs = list(range(65536))
glb_mux = 0x65E0
led3_0_set1 = 0x6528
led1_0_set0 = 0x6548
led_port_sel = 0x654C
def get_word(i):
val = (regs[i + 3] << 24) + (regs[i + 2] << 16) + (regs[i + 1] << 8) + regs[i]
# print(f"READING {i:04x} : {val:08x}")
return val
with open("reg_dump.txt", "r") as file:
for line in file:
addr, data = line.split(":")
data = [int(x, 16) for x in data.strip().split(" ")]
addr = int(addr, 16)
for x in range(len(data)):
regs[addr + x] = data[x]
ledstr = []
ledfmt = []
print("LED pad Configuration:")
for i in range(28):
j = i % 5
if j == 0:
idx = glb_mux + (i // 5) * 4
val = get_word(idx)
ledval = (val >> (6 * j)) & 0x3F
ledstr.append(f"{ledval:02x}")
ledfmt.append(f"{i:02x}")
print(f"{' '.join(ledfmt)}")
print(f"{' '.join(ledstr)}")
print(f".led_mux = {{ 0x{', 0x'.join(ledstr)} }},")
LED_TYPES = [
" 10G",
16,
" TWO_5G",
17,
" 5G",
18,
" TWO_2G5",
19,
" 2G5",
0,
" TWO_1G",
1,
" 1G",
2,
" 500M",
3,
" 100M",
4,
" 10M",
5,
" LINK",
6,
" LINK_FLASH",
7,
" ACT",
8,
" RX",
9,
" TX",
10,
" COL",
11,
" DUPLEX",
12,
" TRAINING",
13,
" MASTER",
14,
"",
15,
]
led_set = []
led_set_str = []
print("\nLED-set Configuration:")
print("LED-ID 0 1 2 3")
for i in range(4):
idval = []
idvalstr = []
for id in range(4):
val = 0xFFFF & (
get_word(led1_0_set0 - 8 * i - ((id >> 1) << 2)) >> (16 * (id & 1))
)
valhi = (
0xF & (get_word(led3_0_set1 - 4 * (i >> 1)) >> (16 * (i & 1) + 4 * id))
) << 16
val += valhi
idval.append(val)
valstr = "("
for bit in range(0, len(LED_TYPES), 2):
if val & (1 << (LED_TYPES[bit + 1])):
valstr += LED_TYPES[bit]
valstr += ")"
idvalstr.append(valstr)
led_set.append(idval)
led_set_str.append(idvalstr)
idstr = " ".join([f"{d:05x}" for d in idval])
print(f"SET {i}: {idstr}")
# print(f"{idvalstr}")
portsel = get_word(led_port_sel)
for i in range(3, 9):
sel = (portsel >> (i << 1)) & 3
print(f"Port {i}: SET {sel}: {', '.join(led_set_str[sel])}")
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
import argparse
from smbus2 import SMBus, i2c_msg
# Configuration
DEVICE_ADDR = 0x5C # Replace with your device address
NUM_BYTES = 16 # Bytes to read
# Parse command line arguments
parser = argparse.ArgumentParser(description="Dump RTL registers via I2C")
parser.add_argument(
"-b",
"--i2c-bus",
type=int,
required=True,
help="I2C bus number (use i2cdetect -l to find out the bus number of the dongle)",
)
args = parser.parse_args()
# Open I2C bus
with SMBus(args.i2c_bus) as bus:
for addr in range(0, 65536, NUM_BYTES):
# Create write message (send register address)
write = i2c_msg.write(DEVICE_ADDR, [addr >> 8, addr & 0xFF])
# Create read message (read 2 bytes)
read = i2c_msg.read(DEVICE_ADDR, NUM_BYTES)
# Perform combined transaction
bus.i2c_rdwr(write, read)
new_data = list(read)
# Convert read message to string
datastr = " ".join([f"{x:02x}" for x in new_data])
print(f"{addr:04x}: {datastr}")
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
import time
import argparse
from smbus2 import SMBus, i2c_msg
# Configuration
I2C_BUS = 1
DEVICE_ADDR = 0x5C # Replace with your device address
NUM_BYTES = 8 # Bytes to read
SLEEP_INTERVAL = 1
# Handle command line arguments for ignored IOs
# default to ignore IOs related to SYS_LED, UART, SMI, SPI
IGNORED_IOS = [28, 31, 32, 34, 35, 42, 43, 44, 45]
# Setup argument parser
parser = argparse.ArgumentParser(description="Read I2C data from RTL GPIO expander")
parser.add_argument(
"-b",
"--i2c-bus",
type=int,
required=True,
help="I2C bus number (use i2cdetect -l to find out the bus number of the dongle)",
)
parser.add_argument(
"-s",
"--sleep-interval",
type=int,
default=SLEEP_INTERVAL,
help=f"Sleep interval in seconds (default: {SLEEP_INTERVAL})",
)
parser.add_argument(
"-i",
"--ignored-ios",
nargs="*",
type=int,
default=IGNORED_IOS,
help=f"List of GPIO pins to ignore (default: {IGNORED_IOS})",
)
args = parser.parse_args()
I2C_BUS = args.i2c_bus
first_read = True
last_data = [0 for x in range(NUM_BYTES)]
# Open I2C bus
with SMBus(I2C_BUS) as bus:
addr = 0x44
while True:
# Create write message (send register address)
write = i2c_msg.write(DEVICE_ADDR, [addr >> 8, addr & 0xFF])
# Create read message (read NUM_BYTES bytes)
read = i2c_msg.read(DEVICE_ADDR, NUM_BYTES)
# Perform combined transaction
bus.i2c_rdwr(write, read)
new_data = list(read)
if first_read:
delta_data = last_data
first_read = False
else:
delta_data = [new_data[x] ^ last_data[x] for x in range(NUM_BYTES)]
last_data = new_data
# Convert read message to list
datastr = " ".join([f"{x:02x}" for x in new_data])
deltastr = ""
chg_list = []
for x in range(NUM_BYTES):
if delta_data[x] == 0:
continue
for y in range(8):
if delta_data[x] & (1 << y):
idx = x * 8 + y
if idx not in args.ignored_ios:
deltastr += f"\n{' ':8s}GPIO{idx}"
# deltastr = " ".join([ f"{x:02x}" for x in delta_data])
print(f"{addr:04x}: {datastr}{deltastr}")
time.sleep(args.sleep_interval)
+9 -11
View File
@@ -75,8 +75,7 @@
/*---------------------------------------------------------------------------*/
inline void
buf_setup(register __xdata struct psock_buf *buf,
register __xdata u8_t *bufptr, register u16_t bufsize)
buf_setup(__xdata struct psock_buf *buf, __xdata u8_t *bufptr, u16_t bufsize)
{
buf->ptr = bufptr;
buf->left = bufsize;
@@ -84,7 +83,7 @@ buf_setup(register __xdata struct psock_buf *buf,
/*---------------------------------------------------------------------------*/
inline u8_t
buf_bufdata(register __xdata struct psock_buf *buf, register __xdata u8_t **dataptr, register __xdata u16_t *datalen)
buf_bufdata(__xdata struct psock_buf *buf, __xdata u8_t **dataptr, __xdata u16_t *datalen)
{
if(*datalen < buf->left) {
memcpy(buf->ptr, *dataptr, *datalen);
@@ -145,7 +144,7 @@ buf_bufto(__xdata struct psock_buf *buf, u8_t endmarker,
}
/*---------------------------------------------------------------------------*/
static char
send_data(register __xdata struct psock *s)
send_data(__xdata struct psock *s)
{
if(s->state != STATE_DATA_SENT || uip_rexmit()) {
if(s->sendlen > uip_mss()) {
@@ -160,7 +159,7 @@ send_data(register __xdata struct psock *s)
}
/*---------------------------------------------------------------------------*/
static char
data_acked(register __xdata struct psock *s)
data_acked(__xdata struct psock *s)
{
if(s->state == STATE_DATA_SENT && uip_acked()) {
if(s->sendlen > uip_mss()) {
@@ -176,8 +175,7 @@ data_acked(register __xdata struct psock *s)
return 0;
}
/*---------------------------------------------------------------------------*/
PT_THREAD(psock_send(register __xdata struct psock *s, register __xdata const char *buf,
register uint16_t len))
PT_THREAD(psock_send(__xdata struct psock *s, __xdata const char *buf, uint16_t len))
{
PT_BEGIN(&s->psockpt);
@@ -218,7 +216,7 @@ PT_THREAD(psock_send(register __xdata struct psock *s, register __xdata const ch
/*---------------------------------------------------------------------------*/
// PT_THREAD(psock_generator_send(register __xdata struct psock *s,
// PT_THREAD(psock_generator_send(__xdata struct psock *s,
// unsigned short (*generate)(void *), void *arg))
// {
// PT_BEGIN(&s->psockpt);
@@ -275,7 +273,7 @@ psock_newdata(__xdata struct psock *s)
}
}
/*---------------------------------------------------------------------------*/
PT_THREAD(psock_readto(register __xdata struct psock *psock, unsigned char c))
PT_THREAD(psock_readto(__xdata struct psock *psock, unsigned char c))
{
PT_BEGIN(&psock->psockpt);
@@ -304,7 +302,7 @@ PT_THREAD(psock_readto(register __xdata struct psock *psock, unsigned char c))
PT_END(&psock->psockpt);
}
/*---------------------------------------------------------------------------*/
PT_THREAD(psock_readbuf(register __xdata struct psock *psock))
PT_THREAD(psock_readbuf(__xdata struct psock *psock))
{
PT_BEGIN(&psock->psockpt);
@@ -334,7 +332,7 @@ PT_THREAD(psock_readbuf(register __xdata struct psock *psock))
}
/*---------------------------------------------------------------------------*/
void
psock_init(register __xdata struct psock *psock, register __xdata char *buffer, register uint16_t buffersize)
psock_init(__xdata struct psock *psock, __xdata char *buffer, uint16_t buffersize)
{
psock->state = STATE_NONE;
psock->readlen = 0;
+2 -2
View File
@@ -124,7 +124,7 @@ struct psock {
u8_t state; /* The state of the protosocket. */
};
void psock_init(__xdata struct psock *psock, register __xdata char *buffer, register uint16_t buffersize);
void psock_init(__xdata struct psock *psock, __xdata char *buffer, uint16_t buffersize);
/**
* Initialize a protosocket.
*
@@ -158,7 +158,7 @@ void psock_init(__xdata struct psock *psock, register __xdata char *buffer, regi
*/
#define PSOCK_BEGIN(psock) PT_BEGIN(&((psock)->pt))
PT_THREAD(psock_send(register __xdata struct psock *psock, register __xdata const char *buf, register uint16_t len));
PT_THREAD(psock_send(__xdata struct psock *psock, __xdata const char *buf, uint16_t len));
/**
* Send data.
*

Some files were not shown because too many files have changed in this diff Show More