34 Commits
Author SHA1 Message Date
logicog d3f1069285 Merge pull request #315 from eraiza0816/feat/webui-compression
Feat/webui: compress the embedded assets and switch to a single-page layout
2026-09-09 15:31:22 +02:00
eraiza0816 98bda62181 Fix send_counters validation and changelog 2026-09-09 17:51:51 +09:00
René van Dorst 3f65db9984 Merge pull request #413 from DrDoof/fix/vlan-name-bounds
vlan: bound the name table before writing into it
2026-09-09 08:47:05 +02:00
logicog df95c544d4 Merge pull request #418 from DrDoof/fix/crc16-psbank
crc16: leave the code bank as it was found
2026-09-09 06:13:56 +02:00
eraiza0816 7df3c87892 Fix counters rejection of logical port 0 2026-09-09 10:58:16 +09:00
d00f 53f12a42c5 crc16: banked wrapper for cross-bank callers
The lookup tables live in BANK1, so the routine switches the code bank to
reach them with movc and never switches it back. That is the contract the
__naked declaration already implied: same bank or common segment only.
The name now says it.

crc16_bank1 is the routine unchanged. crc16 becomes a __banked wrapper in
BANK1, so a caller from another bank has its own bank put back by
__sdcc_banked_ret. The two existing call sites take the direct path and
pay nothing.
2026-09-09 01:52:12 +02:00
d00f 1b514b5ea8 vlan: bound the name table before writing into it
A VLAN name is copied into vlan_names with nothing checking vlan_ptr
against VLAN_NAMES_SIZE. The only limit is the 128 byte command line, so
enough named VLANs walk straight off the end of a 1 kB table.

What sits immediately behind it makes this worse than a plain overflow: at
0x048A comes vlan_ptr itself, the index driving the write, then port_names
and cmd_buffer. Once the index is corrupted the following writes land
wherever it now points, and the configuration is replayed at boot, so a
config file that overflows does it again on every start.

Measure the name first and refuse it if the three id digits, the name, its
separating space and the terminator would not fit. The check goes before
vlan_name_remove() so a name that does not fit cannot take the existing one
with it, and the VLAN still gets its ports: dropping the whole line would
lose port membership during a config replay, which is worse than losing a
name.

The copy walks a pointer instead of indexing vlan_names[vlan_ptr++] each time,
and the word offset comes out of cmd_words_b once. Writing the bound as
VLAN_NAMES_SIZE - 5 folds the constant at compile time instead of adding it on
every call. BANK2 ends up 25 bytes smaller than it was before the check
existed.
2026-09-09 00:38:37 +02:00
René van Dorst dd09be9dd0 Merge pull request #412 from DrDoof/fix/httpd-init-conn
httpd: do not initialise tstate through uip_conn
2026-09-08 22:22:08 +00:00
René van Dorst b2b7f8a0c6 Merge pull request #414 from DrDoof/fix/dhcp-restart-leak
dhcp: do not leave the old socket behind when starting again
2026-09-08 22:19:18 +00:00
René van Dorst 5b28ddf80a Merge pull request #416 from DrDoof/fix/phy-100m-half-adv
phy: advertise half duplex only when half duplex was asked for
2026-09-08 22:06:11 +00:00
d00f fe3da0c7a5 phy: advertise half duplex only when half duplex was asked for
The 100M branch of phy_set_speed() was missing an else. A port set to 100M
half wrote 0x1481, walked into the next if, found duplex was not 1, took its
else and overwrote the register with 0x1581 straight away, so it advertised
100BASE-TX half and full both. That is what asking for both is meant to do,
and forcing half is something people do to match a partner that cannot
negotiate, so the link comes up looking fine and misbehaves under load.

Bit 7 is 100BASE-TX half and bit 8 is full, which makes 0x1481, 0x1501 and
0x1581 half, full and both, the same shape as 0x1421, 0x1441 and 0x1461 in
the 10M branch above.

Both branches now choose the advertisement into a variable and write it once,
and they compare against the PHY_DUPLEX_* names instead of 0, 1 and whatever
is left. Same values reach the PHY; BANK2 loses 92 bytes.
2026-09-08 23:49:11 +02:00
d00f 412098d0ed phy: advertise half duplex only when half duplex was asked for
The 100M branch of phy_set_speed() is missing an else. A port set to 100M
half writes 0x1481, falls into the next if, finds duplex is not 1 and takes
its else, and immediately overwrites the register with 0x1581. The port
ends up advertising 100BASE-TX half and full, which is what "both" is
supposed to mean.

The 10M branch directly above has the chain the 100M one meant to have, and
the values line up with it: bit 7 is 100BASE-TX half, bit 8 is full, so
0x1481, 0x1501 and 0x1581 are half, full and both, exactly as 0x1421,
0x1441 and 0x1461 are for 10M.

Forcing half duplex is something people do to match a link partner that
cannot negotiate, so quietly advertising full as well is the wrong way for
this to fail.

Not tried on hardware: every port on the switch here has a link up, and
setting the speed restarts negotiation.
2026-09-08 23:49:11 +02:00
René van Dorst 5ecce86ef1 Merge pull request #422 from DrDoof/fix/no-idle-in-isr
Do not enter Idle mode from the external interrupt 2 handler
2026-09-08 21:47:02 +00:00
d00f 247b230212 Do not enter Idle mode from the external interrupt 2 handler
isr_ext2() ends with PCON |= 1, putting the core to sleep from inside an
interrupt handler.

The DW8051 leaves idle when any enabled interrupt is activated, so the core
does not stay down for long. It naps until the next enabled interrupt request
turns up, with the 200 Hz system tick putting a ceiling of 5 ms on that, and
then carries on.

The sleep buys nothing either way, because the instruction after it is the
return. While it lasts the handler holds the priority latch, so everything at
its level or below waits, and that includes the tick, the console and the NIC
receive interrupt.

The handler still clears its flag, which is what it has to do. The bit numbers
beside the two masks were wrong: 0xef clears bit 4 and 0xdf clears bit 5,
not 7 and 6.
2026-09-08 23:35:27 +02:00
d00f c012dda029 Do not enter Idle mode from the external interrupt 2 handler
isr_ext2() ends with PCON |= 1, putting the core to sleep from inside an
interrupt handler.

The DW8051 leaves idle when any enabled interrupt is activated, so the core
does not stay down for long. It naps until the next enabled interrupt request
turns up, with the 200 Hz system tick putting a ceiling of 5 ms on that, and
then carries on.

The sleep buys nothing either way, because the instruction after it is the
return. While it lasts the handler holds the priority latch, so everything at
its level or below waits, and that includes the tick, the console and the NIC
receive interrupt.

The handler still clears its flag, which is what it has to do. The two wrong
comments next to the masks go as well: 0xef clears bit 4 and 0xdf clears bit 5,
not 7 and 6, and the masks already say that without help.
2026-09-08 23:01:28 +02:00
d00f eab8c901cf Fix the EXIF bit numbers in the ext2 and ext3 handler comments
0xef clears bit 4 (IE2) and 0xdf clears bit 5 (IE3). The comments
said 7 and 6.
2026-09-08 22:43:06 +02:00
d00f 21a9fdf45a Do not enter Idle mode from the external interrupt 2 handler
isr_ext2() ends with PCON |= 1. On the DW8051 any enabled interrupt request
ends idle, so the handler sleeps until the next request arrives, the system
tick at the latest, and only then returns. Everything at the same priority
waits with it, and the instruction after the idle is RETI, so the sleep buys
nothing.

The handler still clears its flag, which is what it has to do. The other four
uses of PCON |= 1 are in task context and are left alone.
2026-09-08 22:43:05 +02:00
René van Dorst 1ed12d1101 Merge pull request #417 from DrDoof/fix/dhcp-missing-t1
dhcp: renew at half the lease when the server omits T1
2026-09-08 19:50:39 +00:00
eraiza0816 af4101f060 Fix language select, stat port index, devWalk polling, and duplicate BW command 2026-09-08 14:15:44 +09:00
d00f eab17f8a3f dhcp: renew at half the lease when the server omits T1
Option 58 is optional, and dhcp_state.renewal is only ever written by
parse_opts(), so a server that leaves it out keeps it at zero. The ACK
handler then sets dhcp_timer to zero, and the callback treats a zero timer
in LEASING as time to renew, so it sends a REQUEST on the next pass.

That REQUEST does set a 30 second timeout, but the ACK that answers it
comes back through the same path and zeroes the timer again, so the two
sides trade REQUEST and ACK as fast as the round trip allows, with the
console printing the whole lease each time round.

RFC 2131 has the client renew at half the lease when the server does not
say, so use that, and fall back to a fixed value if there is no lease
either. Both only fill in a value that is missing: a server that asks for a
short renewal still gets exactly what it asked for, which a blanket lower
bound would have overridden.

Not reproduced here, that needs a server configured without the option.
2026-09-07 23:10:29 +02:00
d00f 4bcbada764 dhcp: do not leave the old socket behind when starting again
dhcp_start() never looked at whether DHCP was already running, and "ip dhcp"
calls it without stopping first, so a second start took a fresh slot from
uip_udp_conns and left the previous one bound.

The leak is the smaller half of it. dhcp_callback() decides whether a call
is for it by comparing the local port, not the connection, and every
leftover socket still holds port 68, so each one runs the state machine on
every poll. The timers live in one shared dhcp_state, so ticks count down
once per socket: two starts and the retransmit and renewal timing runs at
twice the intended rate.

Stop the old one first. dhcp_stop() already does exactly that, and the
state test keeps it away from a connection that was never created.
2026-09-07 22:44:51 +02:00
d00f 9fae0f9249 httpd: do not initialise tstate through uip_conn
uip_init() does not set uip_conn, and httpd_init() runs right after it, so
the assignment went through a pointer nothing had assigned yet.

It has stayed invisible because the startup code does clear xdata: uip_conn
is NULL rather than garbage, appstate sits 28 bytes into struct uip_conn, and
the byte therefore lands at xdata 0x1C. With the current map that is inside
the command line buffer, at an offset nothing reads while the line is empty,
and cmd_editor_init() runs later anyway.

Nothing is broken today, but it is a write to an address that has nothing to
do with the state being set, and it moves the moment the struct grows or the
map shifts. Address the connection directly.
2026-09-07 22:23:12 +02:00
eraiza0816 774e04816e Fix login loop and system input wipe 2026-09-08 00:31:14 +09:00
eraiza0816 ead1c40aac Merge upstream/main into feat/webui-compression 2026-09-07 15:36:46 +09:00
eraiza0816 d245249385 Merge upstream/main into feat/webui-compression
Resolve conflicts against ~55 upstream commits since 37eefb4:
- Makefile: keep upstream parallel-build fixes (machine_init.c, SUBDIRSCLEAN, html_data grouped target) and retain minify+gzip pipeline (-z, html_min, .css/.ico)
- tools: retain gzip support (zlib, -lz) lost in upstream revert
- httpd/httpd.c: keep Content-Encoding: gzip alongside upstream GET walk, config upload, syslog/hostname changes
- html: port upstream HTML/JS changes into SPA (single-page) bundle:
  * i18n: add port_col_devices / port_devices keys (en/ja/zh)
  * ports: add Connected devices column, devRender/devWalk via walkL2 (8c3df64)
  * config: add syslog port, extended stp/igmp/bw patterns, conf_toggle handling
  * system: show /cmd output in console (console_out), 401 handling
  * l2: hide delete for CPU entry, delL2Button, sort/filter improvements
  * navigation: add STP entry
- SPA: integrate new STP page (stp.html/js) as page-stp section with sectionInits.stp, bridge/prio handling, per-port config/status tables
- Remove per-page HTML/JS files (now bundled) and standalone stp.html/js

Verified: tools build, html_min generation (main.js 93k->81k->19k gzip, 8 files), html_data.h gzip sizes, main.js syntax check.
2026-09-06 04:45:46 +09:00
eraiza0816 37eefb4b35 Merge upstream/main into feat/webui-compression
Resolve the conflicts against the 41 upstream commits since 52692d0:

- Makefile: keep the upstream parallel-build fixes (grouped html_data
  target with order-only deps, create_build_dir, crtbank, deterministic
  BUILD_DATE, atomic version.h write) and keep the minify + gzip
  pipeline (-z, html_min, .css/.ico in the html file list) on top.
- html: l2.js stays removed; port the upstream L2 fixes into the
  single-page main.js (walkL2() with empty-reply and 4096-entry cap
  handling, 3 retries on error).  The L2 poll now re-schedules itself
  after each walk and stops when the section is hidden.
- httpd/httpd.c merged cleanly (gzip Content-Encoding alongside the
  upstream GET walk bound and counters.json port check).
- doc/webui-compression.md: describe the upstream version.h write.

Verified: KP_9000_6XHML_X2 and PCB_K0402WS_V3 build sequentially and
with make -j8; all 8 embedded files decompress byte-identically;
httpd_sim serves main.js/index.html and the L2 JSON endpoint.
2026-08-17 22:53:06 +09:00
eraiza0816 3f5093f180 Delete: go-lang 2026-08-17 22:18:08 +09:00
eraiza0816 4c4232cc07 fix(makefile): undo 2026-08-12 14:15:44 +09:00
eraiza0816 c4928e5546 docs(webui): describe the single-page layout and updated numbers
The Web UI is now 8 files at 25,302 bytes gzip (23.0 % of the raw
109,814); section switching measured on a real device.
2026-08-11 06:38:55 +09:00
eraiza0816 67ae4598d9 feat(webui): turn the Web UI into a single-page application
index.html now contains all pages as sections (overview, ports, stat,
vlan, l2, mirror, lag, eee, bandwidth, system, update); the sidebar
switches between them via the URL hash (#/ports, #/vlan, ...).
login.html stays separate as the authentication gate.

A section's initialisation runs when the section is shown, its polling
intervals run only while it is visible, and re-entering a section
refetches the data; the checkbox builders (lag/vlan/mirror) and the L2
scanner were made idempotent so re-initialising cannot duplicate rows.

tools/minify.py: recognise regex literals - an escaped slash before the
closing delimiter was previously stripped as a // comment.

Measured (make MACHINE=KP_9000_6XHML_X2): 8 embedded files, 109,814 ->
97,730 -> 25,302 bytes gzip (23.0 %); the data block ends at 0x462d6
(~86 KB freed).  On a real PCB-K0402WS-V3: cold load ~450 ms, section
switches ~60-160 ms with no asset re-downloads, all sections render
without JavaScript errors.
2026-08-11 06:37:20 +09:00
eraiza0816 b651890a3a docs(webui): update the compression numbers for the merged bundle
The bundle merge brings the Web UI to 28,200 bytes gzip (25.3 % of the
raw 111,280); the table of embedded files now has 18 entries.
2026-08-11 05:40:58 +09:00
eraiza0816 caa7874bb3 feat(webui): consolidate the page scripts into a single main.js bundle
All 17 page scripts (i18n, shared helpers, navigation and the per-page
scripts) are merged into one html/main.js that every page loads as its
only script, so the browser fetches it once and gzip can compress
across all of it.

The page-specific initialisation is guarded by the presence of the
matching page elements (it used to be selected implicitly by which
script a page loaded); the sidebar fill and the stat popup wiring moved
into DOMContentLoaded handlers because the bundle loads in <head>,
before the body exists.

tools/httpd_sim.h: BUFFER_SIZE 24000 -> 131072 so the simulator can
serve the larger bundle (the firmware is unaffected, it serves the
gzip'd bundle of ~17 KB).

Measured (make MACHINE=KP_9000_6XHML_X2): the JS drops from 23,302 to
16,948 bytes gzip; the whole Web UI from 34,784 to 28,200 bytes (25.3 %
of the raw 111,280); the data block ends at 0x46e28.  Verified with a
headless browser against httpd_sim and against a real PCB-K0402WS-V3:
all pages render, the sidebar and the port strip are drawn, no
JavaScript errors.
2026-08-11 05:40:01 +09:00
eraiza0816 53f33bd606 docs(webui): describe the minify + gzip asset compression
Pipeline, measured per-stage sizes (113,015 -> 101,498 -> 34,784 bytes)
and how to verify a build against the embedded image.
2026-08-11 03:06:45 +09:00
eraiza0816 57e3fce95e feat(webui): minify and gzip-compress the embedded web assets
Two build-time steps shrink the embedded Web UI (html/) from 113,015 to
34,784 bytes (30.8 %):

- tools/minify.py (new): string-literal-safe minifier that strips
  comments and indentation; comments are pure overhead on the wire, so
  the transfer size drops already before gzip (113,015 -> 101,498).
  Binary files (.ico) are copied through unchanged.  The output goes to
  output/html_min/, the new fileadder input.
- fileadder -z: gzip-compresses every added file (zlib gzip format,
  Z_BEST_COMPRESSION); the generated f_data entries carry a gzip flag.
  tools/Makefile links the tool against -lz.
- httpd: adds "Content-Encoding: gzip" for flagged entries; the 8051
  only copies compressed bytes, decompression happens in the browser.
  The JSON API stays uncompressed.

Fixes required to make the sizes deterministic:
- fileadder terminated files at data_read + 1, so the strlen()-based
  size computation depended on uninitialised buffer content; the NUL
  now lands directly after the content.
- version.h was .PHONY, so every build rewrote it while parallel builds
  could read a half-written copy.
- the file list was unscoped and missed .css/.ico; it is now scoped to
  html/ and covers all embedded types.

Measured with make MACHINE=KP_9000_6XHML_X2: data block end moves from
0x5b977 to 0x487e0 (~78 KB freed on a 512 KB image).  Verified with
MACHINE=PCB_K0402WS_V3 and on real hardware (PCB-K0402WS-V3).
2026-08-11 03:04:19 +09:00
46 changed files with 3138 additions and 2918 deletions

No files matched your search

+3
View File
@@ -4,6 +4,9 @@
## Added
- Web UI
- Compress the embedded assets (minify + gzip) and switch to a single-page layout. #315
## Changed
## Fixed
-1
View File
@@ -7,7 +7,6 @@ RUN apt-get update && apt-get install -y \
xxd \
python3 \
libjson-c-dev \
golang-go \
git \
&& rm -rf /var/lib/apt/lists/*
+14 -4
View File
@@ -87,10 +87,20 @@ SRCS += \
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' -or -name '*.css' -or -name '*.ico')
html_data.c html_data.h &: $(HTML) | tools
tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -d html -p html_data
# Minified copy of the web UI sources, used as the fileadder input.
# The raw html/ sources stay untouched for development; the minified
# copy is a build artifact under output/.
HTML_MIN := output/html_min
.PHONY: html_min
html_min: $(HTML)
rm -rf $(HTML_MIN)
mkdir -p $(HTML_MIN)
@for f in $(HTML); do python3 tools/minify.py $$f $(HTML_MIN)/$$(basename $$f) || exit 1; done
html_data.c html_data.h &: $(HTML) | tools html_min
tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -z -d $(HTML_MIN) -p html_data
$(VERSION_HEADER):
@printf '%s\n' "#ifndef VERSION_H" "#define VERSION_H" \
@@ -132,7 +142,7 @@ $(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.i
tools/output/imagebuilder -i $^ $@
tools/output/fileadder -a $(DEFAULT_CONFIG_LOCATION) -s $(IMAGESIZE) -d config.txt $@
tools/output/fileadder -a $(CONFIG_LOCATION) -s $(IMAGESIZE) -d config.txt $@
tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -d html -p html_data -b BANK1 $@
tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -z -d $(HTML_MIN) -p html_data -b BANK1 $@
tools/output/crc_calculator -u $@
ln -sf $(MACHINE)/rtlplayground-$(FILENAME_EXTENSION).bin output/rtlplayground.bin
+19 -9
View File
@@ -504,20 +504,30 @@ void parse_vlan(void)
}
uint8_t w = 2;
if (cmd_words_len > w && isletter(cmd_buffer[cmd_words_b[w]])) {
uint8_t pos = cmd_words_b[w];
if (cmd_words_len > w && isletter(cmd_buffer[pos])) {
uint8_t i = 0;
while (!cmd_is_space_or_nul(pos + i))
i++;
if (vlan_ptr + i > (VLAN_NAMES_SIZE - 5)) {
print_string("VLAN name table full, name ignored\n");
} else {
vlan_name_remove(vlan_settings.vlan);
vlan_names[vlan_ptr++] = hex[(vlan_settings.vlan >> 8) & 0xf];
vlan_names[vlan_ptr++] = hex[(vlan_settings.vlan >> 4) & 0xf] ;
vlan_names[vlan_ptr++] = hex[vlan_settings.vlan & 0xf];
while(!cmd_is_space_or_nul(cmd_words_b[w] + i)) {
write_char(cmd_buffer[cmd_words_b[w] + i]);
vlan_names[vlan_ptr++] = cmd_buffer[cmd_words_b[w] + i++];
__xdata uint8_t *vlan_str = &vlan_names[vlan_ptr];
*vlan_str++ = hex[(vlan_settings.vlan >> 8) & 0xf];
*vlan_str++ = hex[(vlan_settings.vlan >> 4) & 0xf] ;
*vlan_str++ = hex[vlan_settings.vlan & 0xf];
while(!cmd_is_space_or_nul(pos)) {
uint8_t c = cmd_buffer[pos++];
write_char(c);
*vlan_str++ = c;
}
vlan_names[vlan_ptr++] = ' '; vlan_names[vlan_ptr] = NUL;
w++;
*vlan_str++ = ' '; *vlan_str = NUL;
vlan_ptr = vlan_str - vlan_names;
print_string("<\n");
}
w++;
}
uint8_t ret;
uint8_t idx;
+7 -1
View File
@@ -3,6 +3,8 @@
;
.globl _crc_value
.globl _crc16
.globl _crc16_bank1
.globl __sdcc_banked_ret
.equ BANK, 0x96
; .equ DPS, 0x86
; Variable in XMEM holding current CRC16 value, being updated
@@ -18,7 +20,7 @@ _crc_value::
.area HOME (CODE)
.area CSEG (CODE)
; .area BANK1 (CODE)
_crc16:
_crc16_bank1:
mov BANK, #1
push dph
push dpl
@@ -52,6 +54,10 @@ _crc16:
.area BANK1 (CODE)
_crc16:
lcall _crc16_bank1
ljmp __sdcc_banked_ret
crc16_table_l:
.byte #0x00, #0xc1, #0x81, #0x40, #0x01, #0xc0, #0x80, #0x41
.byte #0x01, #0xc0, #0x80, #0x41, #0x00, #0xc1, #0x81, #0x40
+18
View File
@@ -332,6 +332,18 @@ void parse_dhcp(void)
uip_ipaddr(&uip_hostaddr, dhcp_state.current_ip[0], dhcp_state.current_ip[1], dhcp_state.current_ip[2], dhcp_state.current_ip[3]);
uip_ipaddr(&uip_draddr, dhcp_state.router[0], dhcp_state.router[1], dhcp_state.router[2], dhcp_state.router[3]);
uip_ipaddr(&uip_netmask, dhcp_state.subnet[0], dhcp_state.subnet[1], dhcp_state.subnet[2], dhcp_state.subnet[3]);
/* T1 is optional. Without it the renewal time stays zero, which
* makes dhcp_timer zero, and the callback then sends a REQUEST on
* every pass through LEASING: client and server trade REQUEST and
* ACK as fast as the round trip allows. RFC 2131 has the client
* renew at half the lease when the server does not say, and if
* there is no lease either, pick something rather than zero. Only
* filled in when absent, so a server that asks for a short renewal
* still gets it. */
if (!dhcp_state.renewal)
dhcp_state.renewal = dhcp_state.lease / 2;
if (!dhcp_state.renewal)
dhcp_state.renewal = 60;
dhcp_state.state = DHCP_LEASING;
dhcp_state.ticks = SYS_TICK_HZ;
dhcp_state.dhcp_timer = dhcp_state.renewal > 0xffff ? 0xffff : dhcp_state.renewal;
@@ -341,6 +353,12 @@ void parse_dhcp(void)
void dhcp_start(void) __banked
{
/* Starting twice would leave the first socket bound: uip_udp_new() takes
* the next free slot while the old one keeps port 68, and dhcp_callback()
* matches on that port rather than on the connection, so every leftover
* runs the state machine too and the timers count down once per socket. */
if (dhcp_state.state != DHCP_OFF)
dhcp_stop();
uip_ipaddr(server, 255,255,255,255);
dhcp_state.conn = uip_udp_new(&server, HTONS(DHCPC_SERVER_PORT));
dhcp_state.current_ip[0] = dhcp_state.current_ip[1] = dhcp_state.current_ip[2] = dhcp_state.current_ip[3] = 0;
+106
View File
@@ -0,0 +1,106 @@
# Compressing the Web UI Assets
The Web UI (all files under `html/`) is embedded into the flash image and
served over HTTP. Two build-time steps keep it small: a safe minifier and
gzip compression of the embedded files.
## How it works
```
html/ --(minify.py)--> output/html_min/ --(fileadder -z)--> flash image
```
- `tools/minify.py` removes comments (`<!-- -->`, `//`, `/* */`) and
line-leading whitespace without touching string or regex literals, so
the output is functionally identical to the input. Binary files
(`.ico`) are copied through unchanged. The raw sources in `html/` stay
untouched for development; the minified copy in `output/` is a pure
build artifact. Comments and indentation are pure overhead for the
served bytes, so minification alone already reduces the transfer size
from 109,814 to 97,730 bytes, independent of gzip.
- The Web UI is a single-page application: `index.html` holds all pages
as sections (`#/ports`, `#/vlan`, ...) that the sidebar switches
between via the URL hash. `login.html` stays separate because it is
the authentication gate. Only these two pages plus `main.js`,
`style.css`, the three SVG port images and the favicon are embedded.
- All JavaScript (i18n, shared helpers, navigation and the per-section
scripts) lives in one `html/main.js` bundle that the pages load as
their only script, so the browser fetches it once and gzip can
compress across all of it. A section's initialisation runs when the
section is shown, its polling intervals run only while it is visible,
and re-entering a section refetches the data.
- `fileadder -z` gzip-compresses every file (zlib, gzip format,
`Z_BEST_COMPRESSION`) when it generates the file table
(`html_data.c`/`html_data.h`) and when it embeds the files into the
final image. Both invocations run on `output/html_min/`, so the table
and the image always agree.
- The httpd serves each file with `Content-Encoding: gzip` according to
the `gzip` flag in its `f_data` entry. The 8051 only copies compressed
bytes from flash to the socket; decompression happens in the browser.
- Only the static files are compressed. The JSON API responses
(`/information.json`, `/vlanlist`, ...) are generated on the fly and
stay uncompressed.
## Measured effect
Build on any machine (the Web UI is machine-independent):
| | bytes |
|---|---:|
| raw sources | 109,814 |
| minified (comments and indentation removed) | 97,730 |
| gzip (flash + wire) | 25,302 (23.0 %) |
The minified assets are also what the browser receives when gzip support
is missing or disabled, so the transfer size drops in both stages:
109,814 → 97,730 bytes from minification, then → 25,302 bytes from gzip.
The embedded data block ends at `0x5b977` without compression and at
`0x462d6` with it, freeing ~86 KB of the 512 KB image.
The consolidation helps at every step. The 17 scripts gzip to 23,302
bytes as separate files but to 17,231 bytes as one `main.js` bundle,
because the gzip dictionary spans all of them. The single-page layout
then removes the per-page HTML and, more importantly, the repeated
downloads: with the multi-page UI the browser fetched `main.js` on every
navigation; the SPA fetches everything once and section switches are
pure in-page JavaScript (measured on a real device: cold load ~450 ms,
section switch ~60-160 ms, the only network traffic afterwards is the
JSON polling of the visible section).
## Notes
- The files stay well below the `uint16_t` size limit of the file table
(largest gzip output: 17.2 KB for the merged `main.js`).
- `fileadder` terminates the embedded files with a NUL directly after the
content (previously at `data_read + 1`), so the `strlen()`-based size
computation no longer depends on uninitialised buffer content.
- `version.h` is now written by a single atomic `printf` (change from
upstream) instead of five separate `echo`s, so a parallel build
(`make -j`) can no longer read a half-written copy. It stays in
`.PHONY` so it is regenerated on every build.
## Verification
Decompress what the firmware embeds and compare it with the minified
sources:
```python
import gzip, re
h = open('html_data.h').read(); c = open('html_data.c').read()
starts = dict((k, int(v, 16)) for k, v in
re.findall(r'#define FDATA_START_(\w+) 0x([0-9a-f]+)', h))
sizes = dict((k, int(v)) for k, v in
re.findall(r'#define FDATA_SIZE_(\w+) (\d+)', h))
entries = [(p, sizes[z], starts[s]) for p, s, z in
re.findall(r'\{"/([^"]+)", FDATA_START_(\w+), '
r'FDATA_SIZE_(\w+), \w+, 1\}', c)]
d = open('output/rtlplayground.bin', 'rb').read()
for name, size, start in entries:
assert gzip.decompress(d[start:start+size]) == \
open('output/html_min/' + name, 'rb').read()
```
`tools/httpd_sim` serves the raw files from `html/` and does not exercise
the gzip path; a browser session against a flashed switch (or `curl
--compressed`) is the final check.
-21
View File
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 data-i18n="bw_heading">Ingress and Egress Bandwidth</h1>
<table id="bwtable">
<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>
<script src="/navigation.js"></script>
</body>
</html>
-138
View File
@@ -1,138 +0,0 @@
const iLayout = '" type="number" maxlength="10" size="10" onfocus="inputFocus(';
function createBW() {
var tbl = document.getElementById('bwtable');
const limit = '<input type="checkbox" id="limit_port" onchange="exec();">'
if (tbl.rows.length <= 2 && numPorts) {
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(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 = 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 = 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;
}
}
}
function iClicked(i)
{
document.getElementById("bwapply_" + i).disabled=false;
var tbl = document.getElementById('bwtable');
var tr = tbl.rows[i];
if (!document.getElementById("ilimit_port_" + i).checked) {
tr.cells[2].innerHTML = t('bw_unlimited');
document.getElementById("fc_port_" + i).disabled = true;
document.getElementById("fc_port_" + i).checked = true;
} else {
tr.cells[2].innerHTML = '<input id="ibw_' + i + iLayout + i + ')" value="0"/>';
document.getElementById("fc_port_" + i).disabled = false;
document.getElementById("fc_port_" + i).checked = true;
}
}
function eClicked(i)
{
document.getElementById("bwapply_" + i).disabled=false;
var tbl = document.getElementById('bwtable');
var tr = tbl.rows[i];
if (!document.getElementById("elimit_port_" + i).checked) {
tr.cells[5].innerHTML = t('bw_unlimited');
} else {
tr.cells[5].innerHTML = '<input id="ebw_' + i + iLayout + i + ')" value="0"/>';
}
}
function inputFocus(i)
{
document.getElementById("bwapply_" + i).disabled=false;
}
async function doCMD(cmd)
{
console.log("Sending >" + cmd + "<");
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
console.log('Completed!', response);
} catch(err) {
console.error(`Error: ${err}`);
}
}
async function applyBandwidth(i) {
var tbl = document.getElementById('bwtable');
var tr = tbl.rows[i];
var cmd = "bw in " + (i-1) + " off";
if (document.getElementById("ilimit_port_" + i).checked)
cmd = 'bw in ' + (i-1) + ' ' + parseInt(document.getElementById("ibw_" + i).value).toString(16).padStart(4, "0");;
doCMD(cmd);
if (document.getElementById("ilimit_port_" + i).checked) {
if (!document.getElementById("fc_port_" + i).checked)
cmd = "bw in " + (i-1) + " drop";
doCMD(cmd);
}
var cmd = "bw out " + (i-1) + " off";
if (document.getElementById("elimit_port_" + i).checked)
cmd = 'bw out ' + (i-1) + ' ' + parseInt(document.getElementById("ebw_" + i).value).toString(16).padStart(4, "0");;
doCMD(cmd);
}
function getBW() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
console.log("IN getBW ");
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("BW: ", JSON.stringify(s));
var tbl = document.getElementById('bwtable');
if (tbl.rows.length > 2 && numPorts) {
for (let i = 2; i < 2 + numPorts; i++) {
p = s[i-2];
let n = p.portNum;
let tr = tbl.rows[n+1];
if (!document.getElementById("bwapply_" + (n+1)).disabled)
continue;
console.log("Table Update row: " + i + " portNum is " + n + ", pState is " + pState[i-2] + ", row number is " + (n+1));
let iBW = parseInt(p.iBW,16) * 16; let eBW = parseInt(p.eBW,16) * 16;
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 = 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 = t('bw_unlimited');
} else {
tr.cells[5].innerHTML = '<input id="ebw_' + (n+1) + iLayout + (n+1) + ')" value="' + eBW +'"/>';
}
document.getElementById("fc_port_" + (n+1)).checked = p.iFC==1?true:false;
document.getElementById("fc_port_" + (n+1)).disabled = p.iLimited==1?false:true;
}
}
}
};
xhttp.open("GET", "/bandwidth.json", true);
xhttp.timeout = 1500; sendXHTTP(xhttp);
}
window.addEventListener("load", function() {
update( () => {
createBW();
getBW();
const interval = setInterval(update, 2000);
});
});
-138
View File
@@ -1,138 +0,0 @@
var configInterval = Number();
var configuration = [];
const conf_cmds = [
/^ip\s+(\d{1,3}\.){3}\d{1,3}$/,
/^ip\s+dhcp$/,
/^gw\s+(\d{1,3}\.){3}\d{1,3}$/,
/^netmask\s+(\d{1,3}\.){3}\d{1,3}$/,
/^syslog\s+(on|off)$/,
/^syslog\s+ip\s+(\d{1,3}\.){3}\d{1,3}$/,
/^syslog\s+port\s+\d{1,5}$/,
/^passwd\s+\S+$/,
/^vlan\s+\d{1,4}\s+d$/,
/^vlan\s+\d{1,4}\s+mgmt$/,
/^vlan\s+\d{1,4}(\s+[a-zA-Z]\w*)?(\s+[1-9]t?)+$/,
/^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]?)+$/,
/^lag\s+\d(\s+\d{1,2})+$/,
/^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}$/,
];
/* Commands that come in an on/off pair replace each other, which the list
* below cannot express: it drops lines starting with the text it matched, and
* "syslog off" does not start with "syslog on". Naming the stem separately
* keeps the pair collapsed without widening the match to the whole family. */
const conf_toggle = [
/^(syslog)\s+(?:on|off)$/,
];
const conf_overwrite = [
/^ip\b/,
/^gw\b/,
/^netmask\b/,
/^syslog\s+ip\b/,
/^syslog\s+port\b/,
/^syslog\s+(on|off)$/,
/^passwd\b/,
/^vlan\s+\d{1,4}\s+mgmt$/,
/^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/,
/^mirror\b/,
/^lag\s+\d+\b/,
/^laghash\b/,
/^isolate\s+\d{1,2}\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){
var a = s.split(/\r\n|\n/);
for (var l = 0; l < a.length; l++) {
var line = a[l].trim().replace(/\s+/g, ' ');
if (!line.length) continue;
const deleteMatch = line.match(/^vlan\s+(\d{1,4})\s+d$/);
if (deleteMatch) {
const prefix = "vlan " + deleteMatch[1] + " ";
configuration = configuration.filter(c => !c.startsWith(prefix));
continue;
}
console.log(l + ' --> ' + line);
var ignore = true;
for (const x of conf_cmds)
if (x.test(line)) { ignore = false; break; }
if (ignore) continue;
for (const x of conf_toggle) {
const t = line.match(x);
if (t) {
configuration = configuration.filter(item => item !== t[1] + " on" && item !== t[1] + " off");
break;
}
}
for (const x of conf_overwrite) {
if (x.test(line)) {
let m = line.match(x);
let matchStr = m[0];
configuration = configuration.filter(item =>
!(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:");
for (const x of configuration) { console.log(x); }
}
async function fetchConfig() {
try {
const response = await fetch('/config');
console.log("CONFIG: ", response);
const t = await response.text();
return t;
} catch(err) {
console.error("Error: ", err);
}
}
async function fetchCmdLog() {
try {
const response = await fetch('/cmd_log');
console.log("CMD-Log: ", response);
const t = await response.text();
return t;
} catch(err) {
console.error("Error: ", err);
return "";
}
}
-26
View File
@@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 data-i18n="eee_heading">EEE Status</h1>
<table id="eeetable">
<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" 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>
</div>
<script src="/navigation.js"></script>
</body>
</html>
-52
View File
@@ -1,52 +0,0 @@
function createEEE() {
var tbl = document.getElementById('eeetable');
if (tbl.rows.length <= 2 && numPorts) {
console.log("CREATING TABLE ", tbl.rows.length);
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(t('common_port') + (i-1)));
for (let j = 0; j < 7; j++) {
td = tr.insertCell(); td.appendChild(document.createTextNode(" "));
}
}
}
}
function getEEE() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("EEE: ", JSON.stringify(s));
var tbl = document.getElementById('eeetable');
if (tbl.rows.length > 2 && numPorts) {
for (let i = 2; i < 2 + numPorts; i++) {
p = s[i-2];
let n = p.portNum;
console.log("Table Update row: " + i + " portNum is " + n + ", pState is " + pState[i-2]);
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?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);
}
tr.classList.toggle('isSFP', p.isSFP);
}
}
}
};
xhttp.open("GET", "/eee.json", true);
xhttp.timeout = 1500; sendXHTTP(xhttp);
}
window.addEventListener("load", function() {
update( () => {
createEEE();
getEEE();
const interval = setInterval(update, 2000);
const iCount = setInterval(getEEE, 2000);
});
});
-18
View File
@@ -1,18 +0,0 @@
async function eeeSub(port, enable) {
var cmd = "eee ";
if (enable)
cmd = cmd + "on";
else
cmd = cmd + "off";
console.log("eeeSub port " + port, ", value " + enable);
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
console.log('Completed!', response);
} catch(err) {
console.error(`Error: ${err}`);
}
}
-609
View File
@@ -1,609 +0,0 @@
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);
});
});
+291 -13
View File
@@ -1,23 +1,41 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/main_info.js"></script>
<script src="/i18n.js"></script>
<script>
window.addEventListener("load", function() {
update( () => {
const interval = setInterval(update, 2000);
});
});
</script>
<link rel="stylesheet" href="style.css">
<title data-i18n="index_title">FreeSwitchOS Main Page</title>
<style>
.popup {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.popup-content {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
height: 80%
}
.popup .popup-content {
overflow-y: scroll;
}
.tab-bar { display: flex; border-bottom: 2px solid #226; margin-bottom: 0; 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; }
.tab-btn:hover { background-color: #aaf; }
.tab-btn.active { background-color: #aaf; font-weight: bold; border-bottom: 2px solid #aaf; margin-bottom: -2px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<div id="ports" style="margin-left:16%;padding:1px 16px"></div>
<div class="page" id="page-overview" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="index_heading">Switch Configuration</h1>
<table id="infoTable">
<tr>
@@ -27,6 +45,266 @@
</tbody>
</table>
</div>
<script src="/navigation.js"></script>
<div class="page" id="page-ports" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="port_heading">Port Configuration</h1>
<table id="speedtable">
<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" data-i18n="port_mtu_heading">Configure Maximum Frame Size (MTU) forwarded at Port</h2>
<table id="mtutable" style="margin-top:1em">
</table>
</div>
<div class="page" id="page-stat" style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="popup" class="popup">
<div class="popup-content">
<h2 data-i18n="stat_detailed">Detailed Port Statistics</h2>
<div id="popup_text"></div>
<button id="closePopup" class="action" data-i18n="stat_close">Close</button>
</div>
</div>
<h1 data-i18n="stat_heading">Port Statistics</h1>
<table id="statstable">
<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>
</table>
</div>
<div class="page" id="page-vlan" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="vlan_heading">VLAN Configuration</h1>
<form id="vform">
<div>
<label for="vlanSelect" data-i18n="vlan_select">VLAN Select:</label>
<select id="vlanSelect" style="margin: 0 0 0 8px">
<option value="" disabled selected data-i18n="vlan_choose">— VLAN Choose —</option>
</select>
</div>
<br/>
<div>
<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();" data-i18n="vlan_get_config">Get Configuration</button>
</div>
<br/><br/>
<label for="vname" data-i18n="vlan_name">VLAN Name:</label>
<input type="text" id="vname" name="vname"><br><br>
<br/>
<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>
<br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" data-i18n="vlan_update" value="Update / Create">
</form>
<h2 data-i18n="vlan_configured">Configured VLANs</h2>
<table id="vlanTable" style="width:90%">
<thead>
<tr>
<th>VLAN</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">
</tbody>
</table>
</div>
<div class="page" id="page-l2" style="margin-left:16%;padding:1px 16px;height:1000px;">
<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><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>
</table>
</div>
<div class="page" id="page-stp" style="margin-left:16%;padding:1px 16px;height:1000px;">
<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>
</div>
<div class="page" id="page-mirror" style="margin-left:16%;padding:1px 16px;height:1000px;">
<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 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" 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">
</div>
<div class="page" id="page-lag" style="margin-left:16%;padding:1px 16px;height:1000px;">
<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" 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" 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" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG3"></div>
</div>
<div class="page" id="page-eee" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="eee_heading">EEE Status</h1>
<table id="eeetable">
<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" 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>
</div>
<div class="page" id="page-bandwidth" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="bw_heading">Ingress and Egress Bandwidth</h1>
<table id="bwtable">
<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>
</div>
<div class="page" id="page-system" style="margin-left:16%;padding:1px 16px;height:1000px;">
<div class="tab-bar">
<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>
<div id="system-tab" class="tab-content active">
<h1 data-i18n="sys_heading">System Settings</h1>
<div class="row">
<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"/>
</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" type="text" size="15"/>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="netmask" data-i18n="sys_netmask">Netmask:</label></div>
<div class="rcol"> <input id="netmask" type="text" size="15"/>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="gw" data-i18n="sys_gateway">Gateway:</label></div>
<div class="rcol"> <input id="gw" type="text" size="15"/>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="model" data-i18n="sys_model">Model:</label></div>
<div class="rcol"> <span id="model">-</span>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="lang-select" 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>
<div class="row">
<div class="lcol"> <label for="mgmtvlan" data-i18n="sys_mgmt_vlan">Management VLAN:</label></div>
<div class="rcol">
<select id="mgmtvlan" onchange="mgmtVlanChanged()">
</select>
</div>
</div>
<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/>
<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 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/>
<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" 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" data-i18n="sys_reset" value="Reset Switch">
</div>
<div id="console-tab" class="tab-content">
<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" data-i18n="sys_send_cmd" value="Send Command"><br/>
<br/><br/>
<span data-i18n="sys_console_warn">Be careful when entering console commands, you can lock yourself out!</span><br/>
<br/>
<pre id="console_out" style="height:22em;overflow:auto;white-space:pre-wrap;border:1px solid;padding:0.5em;"></pre>
</div>
</div>
<div class="page" id="page-update" style="margin-left:16%;padding:1px 16px;height:1000px;width:40%;">
<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" />
<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" data-i18n="update_upload" value="Upload File" />
</form>
</div>
</body>
</html>
-30
View File
@@ -1,30 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 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><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>
</body>
<script src="/navigation.js"></script>
</html>
-178
View File
@@ -1,178 +0,0 @@
function fillStats() {
var tbl = document.getElementById('statstable');
if (!numPorts)
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[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(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')));
}
}
}
function l2CMP(a, b)
{
if (a.port < b.port)
return -1;
if (a.port > b.port)
return 1;
if (a.mac < b.mac)
return -1;
if (a.mac > b.mac)
return 1;
if (a.vlan < b.vlan)
return -1;
if (a.vlan > b.vlan)
return 1;
return 0;
}
function uniq(a) {
return a.filter(function(item, pos, ary) {
return !pos || item.idx != ary[pos - 1].idx;
});
}
function delL2(idx) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (JSON.parse(xhttp.responseText).result == 1) {
l2All = l2All.filter(function(e) { return e.idx != idx; });
renderL2();
}
}
};
xhttp.open("GET", "/l2_del.json?idx=" + idx, true);
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') {
if (e.port === 'CPU') return Number.MAX_SAFE_INTEGER;
if (e.lag) return 100 + e.lag;
return 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');
if (!s.length)
return;
s.sort(l2CMP);
s = uniq(s);
l2All = s.map(function(e) {
if (e.lag)
e.port = 'LAG' + e.lag;
return e;
});
renderL2();
}
function delL2Button(e)
{
if (e.port == 'CPU')
return '';
return '<button type="button" onclick="delL2(' + e.idx + ');">' + t('l2_delete') + '</button>';
}
function paintL2(tbl, s)
{
console.log("L2: ", JSON.stringify(s));
for (let i = 0; i < s.length; i++) {
var e = s[i];
console.log(i, e);
if (tbl.rows[i+1]) {
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[3].innerHTML = `${e.type}`;
tbl.rows[i+1].cells[4].innerHTML = delL2Button(e);
} 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 = delL2Button(e);
}
}
for (let i = tbl.rows.length - 1; i > s.length; i--)
tbl.deleteRow(i);
}
function getL2() {
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);
});
}
window.addEventListener("load", function() {
update( () => {
getL2();
const interval = setInterval(update, 2000);
});;
});
-28
View File
@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 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" 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" 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" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG3"></div>
<script src="/lag.js"></script>
</div>
<script src="/navigation.js"></script>
</body>
</html>
-81
View File
@@ -1,81 +0,0 @@
var lagInterval = Number();
function lagForm() {
if (!numPorts)
return;
for (let j=0; j < 4; j++) {
var lag = "mLAG" + j
console.log("Adding LAG " + lag)
var m = document.getElementById(lag);
for (let i = 1; i <= numPorts; i++) {
const d = document.createElement("div");
d.classList.add("cbgroup");
const l = document.createElement("label");
l.innerHTML = "" + i;
l.classList.add("cbgroup");
const inp = document.createElement("input");
inp.type = "checkbox"; inp.setAttribute("class","psel");
inp.id = "p_" + lag + "_" + i;
const o = document.createElement("img");
if (pIsSFP[i - 1]) {
o.src = "sfp.svg"; o.width ="60"; o.height ="60";
} else {
o.src = "port.svg"; o.width = "40"; o.height = "40";
}
l.appendChild(inp); l.appendChild(o);
d.appendChild(l)
m.appendChild(d);
}
}
fetchLag();
}
function setL(p, c){
console.log("LAG setting: ", p, " to ", c);
document.getElementById(p).checked=c;
}
function fetchLag() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("LAG: ", JSON.stringify(s));
for (let l = 0; l < 4; l++) {
let members = parseInt(s[l].members, 2);
let hash = parseInt(s[l].hash, 16);
for (let i = 1; i <= numPorts; i++) {
let p = i - 1;
if (numPorts < 9)
p = physToLogPort[p];
setL("p_mLAG"+l+"_"+i, members & (1<<p));
}
}
}
};
xhttp.open("GET", `/lag.json`, true);
sendXHTTP(xhttp);
}
async function lagSub(l) {
var cmd = "lag " + (l + 1);
for (let i = 1; i <= numPorts; i++) {
if (document.getElementById("p_mLAG"+l+"_"+i).checked)
cmd = cmd + ` ${i}`;
}
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
console.log('Completed!', response);
} catch(err) {
console.error(`Error: ${err}`);
}
}
window.addEventListener("load", function() {
update( () => {
lagForm();
const interval = setInterval(update, 2000);
});
});
+1 -1
View File
@@ -2,7 +2,7 @@
<html>
<title data-i18n="login_title">RTL Switch Login</title>
<link rel="stylesheet" href="style.css">
<script src="/i18n.js"></script>
<script src="/main.js"></script>
<script>
function removeNote() {
document.getElementById("incorrect").innerHTML = "";
+2489
View File
File diff suppressed because it is too large. Load diff
-22
View File
@@ -1,22 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
fetch('/information.json')
.then(response => response.json())
.then(data => {
const tableBody = document.getElementById('infoTable').querySelector('tbody');
// Create table rows
for (const [key, value] of Object.entries(data)) {
const row = document.createElement('tr');
const cellKey = document.createElement('td');
const cellValue = document.createElement('td');
cellKey.textContent = key;
cellValue.textContent = value;
row.appendChild(cellKey);
row.appendChild(cellValue);
tableBody.appendChild(row);
}
})
.catch(error => console.error('Error fetching the data:', error));
});
-27
View File
@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 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 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" 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>
<script src="/navigation.js"></script>
</body>
</html>
-64
View File
@@ -1,64 +0,0 @@
var mirrorInterval = Number();
const mirrors = ["mPortsTX", "mPortsRX"];
function mirrorForm() {
if (!numPorts)
return;
for (let j=0; j < mirrors.length; j++) {
console.log("Adding Mirror " + j)
var m = document.getElementById(mirrors[j]);
for (let i = 1; i <= numPorts; i++) {
const d = document.createElement("div");
d.classList.add("cbgroup");
const l = document.createElement("label");
l.innerHTML = "" + i;
l.classList.add("cbgroup");
const inp = document.createElement("input");
inp.type = "checkbox"; inp.setAttribute("class","psel");
inp.id = mirrors[j] + i;
const o = document.createElement("img");
if (pIsSFP[i - 1]) {
o.src = "sfp.svg"; o.width ="60"; o.height ="60";
} else {
o.src = "port.svg"; o.width = "40"; o.height = "40";
}
l.appendChild(inp); l.appendChild(o);
d.appendChild(l)
m.appendChild(d);
}
}
fetchMirror();
}
function setM(p, c){
document.getElementById(p).checked=c;
}
function fetchMirror() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("MIRROR: ", JSON.stringify(s));
document.getElementById('me').checked = s.enabled;
document.getElementById('mp').value = s.mPort;
let m_tx = parseInt(s.mirror_tx, 2);
let m_rx = parseInt(s.mirror_rx, 2);
for (let i = 1; i <= numPorts; i++) {
let p = i - 1;
if (numPorts < 9)
p = physToLogPort[p];
setM("mPortsTX"+i, m_tx&(1<<p)); setM("mPortsRX"+i, m_rx&(1<<p));
}
}
};
xhttp.open("GET", `/mirror.json`, true);
sendXHTTP(xhttp);
}
window.addEventListener("load", function() {
update( () => {
mirrorForm();
const interval = setInterval(update, 2000);
});
});
-43
View File
@@ -1,43 +0,0 @@
async function mirrorSub() {
var cmd = "mirror ";
var mp=document.getElementById('mp').value
if (!mp) {
alert(t('mirror_set_port_first'));
return;
}
document.getElementById(mirrors[0]+mp).checked=false;document.getElementById(mirrors[1]+mp).checked=false;
cmd = cmd + mp;
for (let i = 1; i <= numPorts; i++) {
if (document.getElementById(mirrors[0] + i).checked && document.getElementById(mirrors[1] + i).checked)
cmd = cmd + ` ${i}`;
else if (document.getElementById(mirrors[0] + i).checked)
cmd = cmd + ` ${i}t`;
else if (document.getElementById(mirrors[1] + i).checked)
cmd = cmd + ` ${i}r`;
}
if (cmd.length < 10) {
alert(t('mirror_select_ports'));
return;
}
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
console.log('Completed!', response);
} catch(err) {
console.error(`Error: ${err}`);
}
}
async function mirrorDel() {
var cmd = "mirror off";
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
location.reload();
} catch(err) {
console.error(`Error: ${err}`);
}
}
-21
View File
@@ -1,21 +0,0 @@
document.getElementById('sidebar').innerHTML =
"<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);
});
});
-25
View File
@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 data-i18n="port_heading">Port Configuration</h1>
<form id="vform" action="/vlan.html">
<table id="speedtable">
<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" 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>
</form>
</div>
<script src="/navigation.js"></script>
</body>
</html>
-190
View File
@@ -1,190 +0,0 @@
var mtus = new Int16Array(10);
var clicked = new Int8Array(10);
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">' + 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++) {
if (pIsSFP[i-1])
continue;
const tr = tbl.insertRow();
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 = linkText(pState[i-1] + 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 + ');">' + t('port_apply') + '</button>';
td = tr.insertCell();
td.innerHTML = button;
}
}
tbl = document.getElementById('mtutable');
if (tbl.rows.length <= 2 && numPorts) {
const mSelect = '<select name="mtu_sel" id="mtu_sel">'
+ '<option value="16383">16383</option>'
+ '<option value="1522">1522</option>'
+ '<option value="1536">1536</option>'
+ '<option value="1552">1552</option>'
+ '<option value="9216">9216</option>'
+ '</select>';
var tr = tbl.insertRow();
for (let i = 1; i <= numPorts; i++) {
let td = tr.insertCell();
if (pIsSFP[i-1])
td.innerHTML = '<object type="image/svg+xml" data="sfp.svg" width="60"></object>'
else
td.innerHTML = '<object type="image/svg+xml" data="port.svg" width="40"></object>'
}
tr = tbl.insertRow();
for (let i = 1; i <= numPorts; i++) {
let td = tr.insertCell();
td.innerHTML = mSelect.replaceAll("mtu_sel", "mtu_sel_" + i);
}
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 + ');">' + t('port_apply') + '</button>';
}
}
}
function updatePortTable() {
console.log("updatePortTable called");
var tbl = document.getElementById('speedtable');
if (tbl.rows.length <= 2 || !numPorts)
return;
for (let i = 1; i <= numPorts ; i++) {
if (pIsSFP[i-1])
continue;
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;
}
}
}
async function applySpeed(port) {
var speed = document.getElementById('speed_sel_' + port).value;
var disabled = document.getElementById('disable_port_' + port).checked;
var cmd = "port " + port + " ";
if (!disabled)
cmd = cmd + speed;
else
cmd = cmd + "off";
console.log("CMD: " + cmd);
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
console.log('Completed!', response);
} catch(err) {
console.error(`Error: ${err}`);
}
}
async function portOnOff(p) {
var disabled = document.getElementById('disable_port_' + p).checked;
document.getElementById('speed_sel_' + p).disabled = disabled;
clicked[p] = 1;
}
async function applyMTU(port) {
var mtu = document.getElementById('mtu_sel_' + port).value;
var cmd = "mtu " + port + " " + mtu;
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
console.log('MTU Completed!', response);
getMTUs();
} catch(err) {
console.error(`Error: ${err}`);
}
}
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() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("MTUS: ", JSON.stringify(s));
for (let i = 0; i < s.length; i++) {
p = s[i];
let n = p.portNum;
mtus[n] = parseInt(p.mtu, 16);
var mtu = document.getElementById('mtu_sel_' + n);
if (!mtu)
continue;
mtu.value = mtus[n];
}
}
};
xhttp.open("GET", "/mtu.json", true);
xhttp.timeout = 1500; sendXHTTP(xhttp);
}
window.addEventListener("load", function() {
update( () => {
createPortTable();
updatePortTable();
getMTUs()
setTimeout(devWalk, 3000);
const interval = setInterval(update, 2000);
const updatePortTableInterval = setInterval(updatePortTable, 1000);
});
});
-48
View File
@@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="stat_title">FreeSwitchOS Port Statistics</title>
<style>
.popup {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.popup-content {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
height: 80%
}
.popup .popup-content {
overflow-y: scroll;
}
</style>
</head>
<body>
<nav id="sidebar"></nav>
<div style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="ports"></div>
<div id="popup" class="popup">
<div class="popup-content">
<h2 data-i18n="stat_detailed">Detailed Port Statistics</h2>
<div id="popup_text"></div>
<button id="closePopup" class="action" data-i18n="stat_close">Close</button>
</div>
</div>
<h1 data-i18n="stat_heading">Port Statistics</h1>
<table id="statstable">
<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>
</body>
<script src="/navigation.js"></script>
</html>
-207
View File
@@ -1,207 +0,0 @@
const mib_counters = [
"Interface in Octets", 8,
"", 0,
"Interface out Octets", 8,
"", 0,
"Interface in Unicast Pkts", 8,
"", 0,
"Interface in Multicast Pkts", 8,
"", 0,
"Interface in Broadcast Pkts", 8,
"", 0,
"Interface out Unicast Pkts", 8, // 10
"", 0,
"Interface out Multicast Pkts", 8,
"", 0,
"Interface out Broadcast Pkts", 8,
"", 0,
"Interface out discards", 4,
"802.1d Tp Port in discards", 4,
"802.3 Single collision frames", 4,
"802.3 Multi collision frames", 4,
"802.3 Deferred transmissions", 4, // 20
"802.3 Late collisions", 4,
"802.3 Excessive collisions", 4,
"802.3 Symbol errors", 4,
"802.3 Control in unknown opcodes", 4,
"802.3 In Pause frames", 4,
"802.3 Out Pause frames", 4,
"Ether drop events", 4,
"TX Ether Broadcast Pkts", 4,
"TX Ether Multicast Pkts", 4,
"TX Ether CRC Align errors", 4, // 30
"RX Ether CRC Align errors", 4,
"TX Ether Undersized Pkts", 4,
"RX Ether Undersized Pkts", 4,
"TX Ether Oversized Pkts", 4,
"RX Ether Oversized Pkts", 4,
"TX Ether Fragments", 4,
"RX Ether fragments", 4,
"TX Ether Jabbers", 4,
"RX Ether Jabbers", 4,
"TX Ether Collisions", 4, // 40
"TX Ether Pkts 640 Octets", 4,
"RX Ether Pkts 640 Octets", 4,
"TX Ether 65-127 Octets", 4,
"RX Ether 65-127 Octets", 4,
"TX Ether Pkts 128-255 Octets", 4,
"RX Ether Pkts 128-255 Octets", 4,
"TX Ether Pkts 256-511 Octets", 4,
"RX Ether Pkts 256-511 Octets", 4,
"TX Ether Pkts 512-1023 Octets", 4,
"RX Ether Pkts 512-1023 Octets", 4, // 50
"TX Ether Pkts 1024-1518 Octets", 4,
"RX Ether Pkts 1024-1518 Octets", 4,
"", 4,
"RX Ether Undersized Drop Pkts", 4, // 54
"TX Ether Pkts >1518 Octets", 4,
"RX Ether Pkts >1518 Octets", 4,
"TX Ether Pkts too large", 4,
"RX Ether Pkts too large", 4,
"TX Ether Flexible Octets Set 1", 4,
"RX Ether Flexible Octets Set 1", 4,// 60
"TX Ether Flexible Octets CRC Set 1", 4,
"RX Ether Flexible Octets CRC Set 1", 4,
"TX Ether Flexible Octets Set 0", 4,
"RX Ether Flexible Octets Set 0", 4,
"TX Ether Flexible Octets CRC Set 0", 4,
"RX Ether Flexible Octets CRC Set 0", 4,
"Lenth Field Errors", 4,
"False Carriers", 4,
"Undersized Octets", 4,
"Framing Errors", 4, // 70
"", 4,
"RX MAC Discards", 4, // 72
"RX MAC IPG Short Drop", 4,
"", 4,
"802.1d TP Learned Entry Discards", 4, // 75
"Egress Queue 7 Dropped Pkts", 4,
"Egress Queue 6 Dropped Pkts", 4,
"Egress Queue 5 Dropped Pkts", 4,
"Egress Queue 4 Dropped Pkts", 4,
"Egress Queue 3 Dropped Pkts", 4, // 80
"Egress Queue 2 Dropped Pkts", 4,
"Egress Queue 1 Dropped Pkts", 4,
"Egress Queue 0 Dropped Pkts", 4,
"Egress Queue 7 Out Pkts", 4,
"Egress Queue 6 Out Pkts", 4,
"Egress Queue 5 Out Pkts", 4,
"Egress Queue 4 Out Pkts", 4,
"Egress Queue 3 Out Pkts", 4,
"Egress Queue 2 Out Pkts", 4,
"Egress Queue 1 Out Pkts", 4, // 90
"Egress Queue 0 Out Pkts", 4,
"TX Good Counter", 8,
"", 0,
"RX Good Counter", 8,
"", 0,
"RX Error Counter", 4,
"TX Error Counter", 4,
"TX Good Counter PHY", 8,
"", 0,
"RX Good Counter PHY", 8, // 100
"", 0,
"RX Error Counter PHY", 4,
"TX Error Counter PHY", 4
];
function getCounters(port) {
var xhttp = new XMLHttpRequest();
const popup = document.getElementById('popup');
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 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]);
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) {
tableHtml += "<td>" + mib_counters[i] + "</td><td>" + count.toString() + "</td>";
c += 1;
} else if (mib_counters[i + 1] == 4) {
if (mib_counters[i] != "") {
tableHtml += "<td>" + mib_counters[i] + "</td><td>" + (count >> 32n).toString() + "</td>";
c += 1;
}
if (c == 2) {
tableHtml += "</tr> <tr>";
c = 0;
}
if (mib_counters[i + 2] != "") {
tableHtml += "<td>" + mib_counters[i + 2] + "</td><td>" + (count & 4294967295n).toString() + "</td>";
c += 1;
}
}
if (c == 2) {
tableHtml += "</tr> <tr>";
c = 0;
}
}
ptext.innerHTML = tableHtml + "</tr></table>";
popup.style.display = 'flex';
}
};
xhttp.open("GET", "/counters.json?port=" + port, true);
xhttp.timeout = 1500; sendXHTTP(xhttp);
}
function fillStats() {
var tbl = document.getElementById('statstable');
if (!numPorts)
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 = 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(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(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;
}
}
}
const popup = document.getElementById('popup');
const closePopup = document.getElementById('closePopup');
closePopup.addEventListener('click', () => {
popup.style.display = 'none';
});
window.addEventListener('click', (event) => {
if (event.target === popup) {
popup.style.display = 'none';
}
});
window.addEventListener("load", function () {
update(() => {
update();
fillStats();
const stat = setInterval(fillStats, 1000);
const interval = setInterval(update, 2000);
});
});
-47
View File
@@ -1,47 +0,0 @@
<!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
@@ -1,187 +0,0 @@
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);
});
});
-109
View File
@@ -1,109 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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; }
.tab-btn:hover { background-color: #aaf; }
.tab-btn.active { background-color: #aaf; font-weight: bold; border-bottom: 2px solid #aaf; margin-bottom: -2px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head>
<body>
<div class="tab-bar">
<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 data-i18n="sys_heading">System Settings</h1>
<div class="row">
<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" 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" 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/>
<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/>
<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 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/>
<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" 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" data-i18n="sys_reset" value="Reset Switch">
</div>
<div id="console-tab" class="tab-content">
<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" data-i18n="sys_send_cmd" value="Send Command"><br/>
<br/><br/>
<span data-i18n="sys_console_warn">Be careful when entering console commands, you can lock yourself out!</span><br/>
<br/>
<pre id="console_out" style="height:22em;overflow:auto;white-space:pre-wrap;border:1px solid;padding:0.5em;"></pre>
</div>
</div>
<script src="/config.js"></script>
<script src="/system.js"></script>
<script src="/navigation.js"></script>
<script>
function openTab(evt, tabId) {
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
evt.currentTarget.classList.add('active');
}
</script>
</body>
</html>
-222
View File
@@ -1,222 +0,0 @@
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(t('sys_invalid_ip') + ip); return false };
return true;
}
async function ipSub() {
for (let i=0;i<3;i++) {
if (!checkIp(document.getElementById(ips[i]).value))
return;
}
var cmd = '';
for (let i=0; i<3;i++){
cmd += ips[i]+' '+document.getElementById(ips[i]).value+'\n';
}
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
console.log('Completed!', response);
fetchIP();
} catch(err) {
console.error(`Error: ${err}`);
}
}
async function cmdSub() {
const input = document.getElementById('console_cmd');
const out = document.getElementById('console_out');
const cmd = input.value;
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
if (response.status == 401) {
window.location.href = 'login.html';
return;
}
let text = await response.text();
if (text != "" && !text.endsWith("\n"))
text += "\n";
if (out.textContent.length > 20000)
out.textContent = out.textContent.slice(-16000);
out.textContent += "> " + cmd + "\n" + text;
out.scrollTop = out.scrollHeight;
input.value = "";
} catch(err) {
out.textContent += "> " + cmd + "\n" + err + "\n";
console.error(`Error: ${err}`);
}
}
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;
clearInterval(systemInterval);
const form = new FormData();
form.append("MAX_FILE_SIZE", "4096");
form.append("configuration", new Blob([c], {type: "application/octet-stream"}), "config.txt");
try {
const response = await fetch('/config', {
method: 'POST',
body: form
});
console.log('Completed!', response);
try {
await fetch('/cmd_log_clear', { method: 'GET' });
} catch(e) {}
} catch(err) {
console.error(`Error: ${err}`);
} finally {
isSaving = false;
systemInterval = setInterval(fetchIP, 1000);
}
}
async function flashSave() {
configuration = [];
const savedConfig = await fetchConfig();
const cmdLog = await fetchCmdLog();
if (savedConfig) parseConf(savedConfig);
if (cmdLog) parseConf(cmdLog);
const body = configuration.join('\n') + '\n';
console.log("CONFIGURATION to save: ", body);
await sendConfig(body);
}
async function flashStartupSave() {
var configContent = document.getElementById("config_display").value;
console.log("CONFIGURATION to save: ", configContent);
sendConfig(configContent);
// Clear the command log 1 second after initiating the config save
setTimeout(() => {
fetch('/cmd_log_clear', { method: 'GET' })
.then(response => console.log('Command log cleared', response))
.catch(err => console.error('Error clearing command log:', err));
}, 1000);
}
function clearConfig() {
document.getElementById("config_display").value = "";
// Validate and populate with current IP settings
for (let i=0; i<3; i++) {
if (!checkIp(document.getElementById(ips[i]).value))
return;
}
var configLines = "";
for (let i=0; i<3; i++){
var cmd = ips[i]+' '+document.getElementById(ips[i]).value;
configLines += cmd + "\n";
}
document.getElementById("config_display").value = configLines;
}
function fetchIP() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("IP: ", s);
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) => {
let fullConfig = configText;
// Fetch and append cmd_log
//return fetchCmdLog().then((cmdLogText) => {
// if (cmdLogText) {
// fullConfig = fullConfig + cmdLogText;
// }
document.getElementById("config_display").value = fullConfig;
});
};
}
xhttp.open("GET", `/information.json`, true);
xhttp.send();
}
function resetSwitch() {
if (!confirm(t('sys_reset_confirm'))) {
return;
}
fetch('/reset', { method: 'GET' }).catch(() => {});
setTimeout(() => {
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; });
}
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 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" />
<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" data-i18n="update_upload" value="Upload File" />
</form>
<script src="/navigation.js"></script>
</body>
</html>
-59
View File
@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<link rel="stylesheet" href="style.css">
<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 data-i18n="vlan_heading">VLAN Configuration</h1>
<form id="vform" action="/vlan.html">
<div>
<label for="vlanSelect" data-i18n="vlan_select">VLAN Select:</label>
<select id="vlanSelect" style="margin: 0 0 0 8px">
<option value="" disabled selected data-i18n="vlan_choose">— VLAN Choose —</option>
</select>
</div>
<br/>
<div>
<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();" data-i18n="vlan_get_config">Get Configuration</button>
</div>
<br/><br/>
<label for="vname" data-i18n="vlan_name">VLAN Name:</label>
<input type="text" id="vname" name="vname"><br><br>
<br/>
<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" data-i18n="vlan_update" value="Update / Create">
<script src="/vlan_sub.js"></script>
</form>
<h2 data-i18n="vlan_configured">Configured VLANs</h2>
<table id="vlanTable" style="width:90%">
<thead>
<tr>
<th>VLAN</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">
</tbody>
</table>
</div>
<script src="/navigation.js"></script>
</body>
</html>
-211
View File
@@ -1,211 +0,0 @@
var vlanInterval = Number();
function vlanForm() {
if (!numPorts)
return;
var t = document.getElementById('tPorts');
var u = document.getElementById('uPorts');
var p = document.getElementById('pPorts');
for (let i = 1; i <= numPorts; i++) {
const d = document.createElement("div");
d.classList.add("cbgroup");
const l = document.createElement("label");
l.innerHTML = "" + i;
l.classList.add("cbgroup");
const inp = document.createElement("input");
inp.type = "checkbox"; inp.setAttribute("class","psel");
inp.id = "tport" + i;
inp.setAttribute('onclick', `setC("u", ${i}, false);`);
const o = document.createElement("img");
if (pIsSFP[i - 1]) {
o.src = "sfp.svg"; o.width ="60"; o.height ="60";
} else {
o.src = "port.svg"; o.width = "40"; o.height = "40";
}
l.appendChild(inp); l.appendChild(o);
d.appendChild(l)
t.appendChild(d);
var d2=d.cloneNode(true);
d2.children[0].children[0].id = "uport" + i;
d2.children[0].children[0].setAttribute('onclick', `setC("t", ${i}, false);`);
u.appendChild(d2);
var d3=d.cloneNode(true);
d3.children[0].children[0].id = "pport" + i;
d3.children[0].children[0].removeAttribute('onclick');
p.appendChild(d3);
}
}
function setC(t, p, c){
document.getElementById(t+'port'+p).checked=c;
}
function utClicked(t){
for (let i = 1; i <= numPorts; i++) {
setC('t', i, t); setC('u', i, !t);
}
}
function pvClicked(p){
for (let i = 1; i <= numPorts; i++) {
setC('p', i, p);
}
}
function fetchVLAN() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const s = JSON.parse(xhttp.responseText);
console.log("VLAN: ", JSON.stringify(s));
m = parseInt(s.members, 16);
document.getElementById('vname').value = s.name;
var members = m & 0x3FF;
var untag = (m >> 10) & 0x3FF;
var pvid = parseInt(s.pvid, 16);
console.log("PVID: ", pvid);
for (let p = 1; p <= numPorts; p++) {
var bit = physToLogPort[p-1];
var isMember = (members >> bit) & 1;
var isUntag = (untag >> bit) & 1;
setC('t', p, isMember && !isUntag);
setC('u', p, isMember && isUntag);
setC('p', p, (pvid >> bit) & 1);
}
}
};
var v=document.getElementById('vid').value
if (!v) {
alert(t('vlan_set_id_first'));
return;
}
xhttp.open("GET", `/vlan.json?vid=${v}`, true);
sendXHTTP(xhttp);
}
function portsToRange(mask, nPorts) {
var parts = [];
var start = -1, prev = -1;
for (var p = 1; p <= nPorts; p++) {
var bit = physToLogPort[p - 1];
if ((mask >> bit) & 1) {
if (start < 0) start = p;
prev = p;
} else {
if (start >= 0) {
parts.push(start === prev ? String(start) : start + '-' + prev);
start = -1; prev = -1;
}
}
}
if (start >= 0)
parts.push(start === prev ? String(start) : start + '-' + prev);
return parts.length ? parts.join(',') : '-';
}
async function loadVlanTable() {
var tbody = document.getElementById('vlanTableBody');
if (!tbody) return;
tbody.innerHTML = '';
var resp;
try { resp = await fetch('/vlanlist'); } catch(e) { return; }
if (!resp.ok) return;
var vlans = (await resp.json()).vlan || [];
for (var i = 0; i < vlans.length; i++) {
var v = vlans[i];
var vresp;
try { vresp = await fetch('/vlan.json?vid=' + v.id); } catch(e) { continue; }
if (!vresp.ok) continue;
var s = await vresp.json();
var m = parseInt(s.members, 16);
var members = m & 0x3FF;
var untag = ((m >> 10) & 0x3FF) & members;
var tagged = members & ~untag;
var pvid = parseInt(s.pvid, 16) & 0x3FF;
var tr = document.createElement('tr');
var td, a, btn;
td = document.createElement('td');
a = document.createElement('a');
a.href = '#';
a.textContent = v.id;
(function(vid) {
a.onclick = function(e) {
e.preventDefault();
document.getElementById('vid').value = vid;
fetchVLAN();
};
})(v.id);
td.appendChild(a); tr.appendChild(td);
td = document.createElement('td');
td.textContent = v.name || ''; tr.appendChild(td);
td = document.createElement('td');
td.textContent = portsToRange(members, numPorts); tr.appendChild(td);
td = document.createElement('td');
td.textContent = portsToRange(tagged, numPorts); tr.appendChild(td);
td = document.createElement('td');
td.textContent = portsToRange(untag, numPorts); tr.appendChild(td);
td = document.createElement('td');
td.textContent = portsToRange(pvid, numPorts); tr.appendChild(td);
td = document.createElement('td');
if (v.id !== 1) {
btn = document.createElement('button');
btn.textContent = '✕';
(function(vid) {
btn.onclick = function() { deleteVlan(vid); };
})(v.id);
td.appendChild(btn);
}
tr.appendChild(td);
tbody.appendChild(tr);
}
}
function deleteVlan(id) {
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); });
}
function refreshVlanViews() {
loadVlanList();
loadVlanTable();
}
function loadVlanList() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState !== 4) return;
var sel = document.getElementById('vlanSelect');
if (this.status !== 200) {
sel.style.display = 'none';
return;
}
var vlans = JSON.parse(this.responseText).vlan || [];
if (!vlans.length) {
sel.style.display = 'none';
return;
}
sel.options.length = 1;
for (var i = 0; i < vlans.length; i++) {
var opt = document.createElement('option');
opt.value = vlans[i].id;
opt.text = vlans[i].name ? vlans[i].id + ' — ' + vlans[i].name : String(vlans[i].id);
sel.appendChild(opt);
}
};
xhttp.open('GET', '/vlanlist', true);
sendXHTTP(xhttp);
}
window.addEventListener("load", function() {
update( () => {
vlanForm();
refreshVlanViews();
document.getElementById('vlanSelect').onchange = function() {
document.getElementById('vid').value = this.value;
fetchVLAN();
};
const interval = setInterval(update, 2000);
});
});
-36
View File
@@ -1,36 +0,0 @@
async function vlanSub() {
var commands = [];
var cmd = "vlan ";
var v=document.getElementById('vid').value
if (!v) {
alert(t('vlan_set_id_first'));
return;
}
cmd = cmd + v;
if (document.getElementById('vname').value)
cmd = cmd + ' ' + document.getElementById('vname').value;
for (let i = 1; i <= numPorts; i++) {
if (document.getElementById('tport' + i).checked)
cmd = cmd + ` ${i}t`;
else if (document.getElementById('uport' + i).checked)
cmd = cmd + ` ${i}`;
}
commands.push(cmd);
for (let i = 1; i <= numPorts; i++) {
if (document.getElementById('pport' + i).checked)
commands.push(`pvid ${i} ${v}`);
}
try {
for (let c of commands) {
const response = await fetch('/cmd', {
method: 'POST',
body: c
});
console.log('Completed!', response);
}
refreshVlanViews();
} catch(err) {
console.error(`Error: ${err}`);
}
}
+11 -7
View File
@@ -89,7 +89,7 @@ __xdata uint32_t last_session_use;
extern __xdata uint16_t crc_value;
__xdata uint16_t crc_final;
void crc16(__xdata uint8_t *v) __naked;
void crc16_bank1(__xdata uint8_t *v) __naked;
inline uint8_t is_separator(uint8_t c)
@@ -101,10 +101,11 @@ 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;
// Not through uip_conn: it only points at a connection while uIP is
// handling one, and nothing has set it yet at init time.
uip_conns[0].appstate.tstate = TSTATE_CLOSED;
fw_reset_pending = 0; // xdata is not zeroed by the startup code
}
@@ -517,7 +518,7 @@ uint8_t stream_upload(void)
if (upload_settings.p[upload_settings.bptr] == boundary[bindex]) {
if (!bindex)
crc_final = crc_value;
crc16(upload_settings.p + upload_settings.bptr);
crc16_bank1(upload_settings.p + upload_settings.bptr);
upload_settings.bptr++;
bindex++;
} else {
@@ -526,7 +527,7 @@ uint8_t stream_upload(void)
write_len += bindex;
bindex = 0;
}
crc16(upload_settings.p + upload_settings.bptr);
crc16_bank1(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(' ');
@@ -1012,7 +1013,7 @@ void httpd_appcall(void)
if (!authenticated && !(f_data[entry].start == FDATA_START_login_html
|| f_data[entry].start == FDATA_START_port_svg
|| f_data[entry].start == FDATA_START_sfp_svg
|| f_data[entry].start == FDATA_START_i18n_js
|| f_data[entry].start == FDATA_START_main_js
|| f_data[entry].start == FDATA_START_style_css)) {
send_to_login();
goto do_send;
@@ -1030,7 +1031,10 @@ void httpd_appcall(void)
* 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");
slen += strtox(outbuf + slen, "; charset=UTF-8\r\n");
if (f_data[entry].gzip)
slen += strtox(outbuf + slen, "Content-Encoding: gzip\r\n");
slen += strtox(outbuf + slen, "Cache-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)) {
+1 -3
View File
@@ -299,11 +299,9 @@ void send_vlan(uint16_t vlan)
bool send_counters(uint8_t phys_port)
{
uint8_t phys_port_idx = phys_port - 1;
if (phys_port_idx > 8)
if (phys_port_idx > (machine.max_port - machine.min_port))
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);
+14 -10
View File
@@ -332,21 +332,25 @@ void phy_set_speed(void) __banked
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_CTRL, 0x2000); // Clear bit 12: No Autoneg, Set Extended Pages (bit 13)
if (phy_settings.speed == PHY_SPEED_10M) {
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_MGBASE_CTRL, 0x6001);
if (!phy_settings.duplex)
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, 0x1421);
else if (phy_settings.duplex == 1)
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, 0x1441);
uint16_t adv10;
if (phy_settings.duplex == PHY_DUPLEX_HALF)
adv10 = 0x1421;
else if (phy_settings.duplex == PHY_DUPLEX_FULL)
adv10 = 0x1441;
else
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, 0x1461);
adv10 = 0x1461;
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, adv10);
phy_modify(phy_settings.port, PHY_MMD31, PHY_MMD31_GBCR, 0x0200, 0x0000);
} else if (phy_settings.speed == PHY_SPEED_100M) {
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_MGBASE_CTRL, 0x6001);
if (!phy_settings.duplex)
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, 0x1481);
if (phy_settings.duplex == 1)
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, 0x1501);
uint16_t adv100;
if (phy_settings.duplex == PHY_DUPLEX_HALF)
adv100 = 0x1481;
else if (phy_settings.duplex == PHY_DUPLEX_FULL)
adv100 = 0x1501;
else
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, 0x1581);
adv100 = 0x1581;
phy_write(phy_settings.port, PHY_MMD_AN, PHY_ANEG_ADV, adv100);
phy_modify(phy_settings.port, PHY_MMD31, PHY_MMD31_GBCR, 0x0200, 0x0000);
} else {
// AN Advertisement Register (MMD 7.0x0010)
+4 -5
View File
@@ -32,7 +32,7 @@ extern __xdata uint32_t flash_size;
extern __xdata uint16_t crc_value;
__xdata struct machine_runtime machine_detected;
void crc16(__xdata uint8_t *v) __naked;
void crc16_bank1(__xdata uint8_t *v) __naked;
void flash_default_config(void);
void early_boot_handle_button(void);
@@ -515,8 +515,7 @@ void isr_ext1(void) __interrupt(2)
*/
void isr_ext2(void) __interrupt(8)
{
EXIF &= 0xef; // Clear IRQ flag (bit 7) in EXIF
PCON |= 1; // Enter Idle mode until interrupt occurs
EXIF &= 0xef; // Clear IRQ flag (bit 4) in EXIF
}
/*
@@ -525,7 +524,7 @@ void isr_ext2(void) __interrupt(8)
*/
void isr_ext3(void) __interrupt(9)
{
EXIF &= 0xdf; // Clear IRQ flag (bit 6) in EXIF
EXIF &= 0xdf; // Clear IRQ flag (bit 5) in EXIF
}
// Timer2: handles system tick.
@@ -2096,7 +2095,7 @@ void check_and_flash_update_image(void)
flash_read_bulk(flash_buf);
bptr = flash_buf;
for (j = 0; j < FLASH_BUF_SIZE; j++) {
crc16(bptr++);
crc16_bank1(bptr++);
}
source += FLASH_BUF_SIZE;
if (i%16 == 0) write_char('.');
+1 -1
View File
@@ -15,7 +15,7 @@ $(BUILDDIR)/injector: injector.c
gcc $^ $(CCFLAGS) $@
$(BUILDDIR)/fileadder: fileadder.c
gcc $^ $(CCFLAGS) $@
gcc $^ $(CCFLAGS) $@ -lz
$(BUILDDIR)/crc_calculator: crc_calculator.c
gcc $^ $(CCFLAGS) $@
+58 -5
View File
@@ -11,6 +11,7 @@
#include <string.h>
#include <argp.h>
#include <stdbool.h>
#include <zlib.h>
#define OFFSET 2
@@ -33,6 +34,7 @@ int fbuf_p;
int xbuf_p;
bool addsDir = false;
int callNum = 0;
struct arguments arguments;
const char *argp_program_version = "fileadder 0.1";
const char *argp_program_bug_address = "<git@logicog.de>";
@@ -45,6 +47,7 @@ static struct argp_option options[] = {
{ "address", 'a', "SIZE", 0, "Address where data is placed, default is 0x1000000 if option is used, otherwise 0x1fd000"},
{ "prefix", 'p', "FILE", 0, "Prefix for header and index file generation"},
{ "bank", 'b', "BANKNAME", 0, "Generate #pragma with given bank-name"},
{ "gzip", 'z', 0, 0, "gzip-compress the added files"},
{ 0 }
};
@@ -57,6 +60,7 @@ struct arguments {
char *bank;
bool overwrite;
bool add_zero;
bool gzip;
};
@@ -86,6 +90,9 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
case 'b':
arguments->bank = arg? arg : "BANK1";
break;
case 'z':
arguments->gzip = true;
break;
default:
return ARGP_ERR_UNKNOWN;
}
@@ -118,6 +125,47 @@ int addfile(const char *name, int addr)
}
/*
* gzip-compresses the data at buffer[addr] in place (gzip format, as
* expected by the Content-Encoding: gzip header the httpd serves it
* with). Returns the compressed size.
*/
int gzipBuffer(int addr, int len)
{
uLong bound = compressBound(len);
unsigned char *out = malloc(bound);
if (!out) {
perror("malloc");
return 0;
}
z_stream zs;
memset(&zs, 0, sizeof(zs));
if (deflateInit2(&zs, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8,
Z_DEFAULT_STRATEGY) != Z_OK) {
fprintf(stderr, "gzip: deflateInit2 failed\n");
free(out);
return 0;
}
zs.next_in = (Bytef *)&buffer[addr];
zs.avail_in = len;
zs.next_out = out;
zs.avail_out = bound;
if (deflate(&zs, Z_FINISH) != Z_STREAM_END) {
fprintf(stderr, "gzip: deflate failed\n");
deflateEnd(&zs);
free(out);
return 0;
}
int out_len = zs.total_out;
deflateEnd(&zs);
memcpy(&buffer[addr], out, out_len);
free(out);
return out_len;
}
int hasSuffix(const char *str, const char *suffix)
{
if (!str || !suffix)
@@ -164,9 +212,9 @@ int addidx(const char *name, int addr, int len)
defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "#define FDATA_START_%s 0x%x\n", s, addr);
defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "#define FDATA_SIZE_%s %d\n", s, len);
ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/%s\", FDATA_START_%s, FDATA_SIZE_%s, %s},\n", name, s, s, getMime(name));
ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/%s\", FDATA_START_%s, FDATA_SIZE_%s, %s, %d},\n", name, s, s, getMime(name), arguments.gzip? 1 : 0);
if (!strcmp(name, "index.html"))
ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/\", FDATA_START_%s, FDATA_SIZE_%s, mime_HTML},\n", s, s);
ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/\", FDATA_START_%s, FDATA_SIZE_%s, mime_HTML, %d},\n", s, s, arguments.gzip? 1 : 0);
return 0;
}
@@ -216,7 +264,6 @@ int replaceCalls(int pos)
int main(int argc, char **argv)
{
struct arguments arguments;
int arg_index;
char tmpfilename[] = "image_XXXXXX";
struct stat s;
@@ -227,6 +274,7 @@ int main(int argc, char **argv)
arguments.overwrite = true;
arguments.prefix = 0;
arguments.bank = NULL;
arguments.gzip = false;
argp_parse(&argp, argc, argv, 0, &arg_index, &arguments);
if (!arg_index)
@@ -269,7 +317,7 @@ int main(int argc, char **argv)
defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "#include <stdint.h>\n\n");
defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p,
"typedef enum mime_type_e {\n mime_HTML = 0,\n mime_SVG,\n mime_ICO,\n mime_PNG,\n mime_JS,\n mime_CSS,\n mime_TXT\n} mime_type_t;\n\n");
defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "struct f_data {\n __code char *file;\n uint32_t start;\n uint16_t len;\n mime_type_t mime;\n};\n\n");
defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "struct f_data {\n __code char *file;\n uint32_t start;\n uint16_t len;\n mime_type_t mime;\n uint8_t gzip;\n};\n\n");
// defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "typedef uint16_t (* fcall_ptr)(void);\n\n");
ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, "// This file is automatically generated, do not edit!\n\n");
@@ -310,7 +358,10 @@ int main(int argc, char **argv)
size_t data_read = addfile(pathbuffer, addr);
if (data_read) {
if (arguments.add_zero) {
buffer[addr + data_read + 1] = '\0';
/* NUL-terminate the data so the file table and the
* strlen()-based size computation are deterministic
* (the buffer past the file is otherwise uninitialised) */
buffer[addr + data_read] = '\0';
data_read++;
}
printf("Data inserted from %s at 0x%x, size: %ld\n", pathbuffer, addr, data_read);
@@ -319,6 +370,8 @@ int main(int argc, char **argv)
data_read = replaceCalls(addr);
if (old_len > data_read)
memset(buffer + addr + data_read, 0, old_len - data_read);
if (arguments.gzip)
data_read = gzipBuffer(addr, data_read);
addidx(in_file->d_name, addr, data_read);
addr += data_read;
}
+1 -1
View File
@@ -3,7 +3,7 @@
#include <netinet/in.h>
#define BUFFER_SIZE 24000
#define BUFFER_SIZE 131072
struct Server {
int domain;
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Safe minifier for the WebUI sources (HTML/JS).
Removes comments and line-leading whitespace without touching string or
regex literals, so the output is functionally identical to the input.
Used by the firmware Makefile before the files are embedded in the
flash; the raw sources in html/ stay untouched for development.
Binary files (.ico) are copied through unchanged.
Usage: minify.py <src> <dst>
"""
import re
import shutil
import sys
def js_min(s):
out = []
i = 0
n = len(s)
prev = '\n'
# Characters after which a '/' starts a regex literal rather than a
# division: operator / delimiter / statement-start contexts.
regex_start = set('([{=,:;!&|?+-*%^~<>')
while i < n:
c = s[i]
if c == '"' or c == "'":
j = i + 1
while j < n and s[j] != c:
if s[j] == '\\':
j += 1
j += 1
out.append(s[i:j + 1])
i = j + 1
prev = c
elif c == '/' and (i + 1 >= n or s[i + 1] not in '/=*') and \
(prev in regex_start or prev == '\n'):
# Regex literal (e.g. /^#\// or /https?:\/\/x/g): copy through
# the closing '/', honouring escapes and character classes.
j = i + 1
in_class = False
while j < n:
ch = s[j]
if ch == '\\':
j += 2
continue
if ch == '[':
in_class = True
elif ch == ']':
in_class = False
elif ch == '/' and not in_class:
break
j += 1
out.append(s[i:j + 1])
i = j + 1
prev = '/'
elif c == '/' and i + 1 < n and s[i + 1] == '/':
j = s.find('\n', i)
i = j if j >= 0 else n
elif c == '/' and i + 1 < n and s[i + 1] == '*':
j = s.find('*/', i + 2)
i = j + 2 if j >= 0 else n
else:
out.append(c)
i += 1
if c not in ' \t':
prev = c
lines = [l.strip() for l in ''.join(out).split('\n')]
return '\n'.join(l for l in lines if l) + '\n'
def html_min(s):
s = re.sub(r'<!--.*?-->', '', s, flags=re.S)
lines = [l.strip() for l in s.split('\n')]
return '\n'.join(l for l in lines if l) + '\n'
def main():
if len(sys.argv) != 3:
print('usage: minify.py <src> <dst>', file=sys.stderr)
sys.exit(1)
src, dst = sys.argv[1], sys.argv[2]
if src.endswith('.ico'):
shutil.copyfile(src, dst)
print('%s: copied %d bytes' % (src.split('/')[-1], len(open(src, 'rb').read())))
return
with open(src, 'r', encoding='utf-8', errors='replace') as f:
s = f.read()
if src.endswith('.js'):
out = js_min(s)
else:
out = html_min(s)
with open(dst, 'w', encoding='utf-8') as f:
f.write(out)
print('%s: %d -> %d bytes' % (src.split('/')[-1], len(s), len(out)))
if __name__ == '__main__':
main()