diff --git a/cmd_parser.c b/cmd_parser.c index d4c4df8b..e5032024 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -434,6 +434,14 @@ static void cmd_error(__code const char *msg) } +static uint8_t name_char(uint8_t c) +{ + if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') + return '.'; + return c; +} + + // Sets the management MAC from a "mac " command (or prints // the current one with a bare "mac"). Refuses blank/multicast/locally // administered/all-zero-OUI addresses so the running MAC stays reachable, and @@ -627,7 +635,7 @@ void parse_vlan(void) *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++]; + uint8_t c = name_char(cmd_buffer[pos++]); write_char(c); *vlan_str++ = c; } @@ -889,7 +897,7 @@ void parse_port(void) } else if (cmd_compare(2, "name")) { uint8_t i = 0; while ( (i < PORT_NAME_SIZE-1) && (cmd_buffer[cmd_words_b[3] + i] != NUL) ) { - port_names[phy_settings.port][i] = cmd_buffer[cmd_words_b[3] + i]; + port_names[phy_settings.port][i] = name_char(cmd_buffer[cmd_words_b[3] + i]); i++; } port_names[phy_settings.port][i] = NUL; @@ -1831,9 +1839,7 @@ void cmd_parser(void) __banked uint8_t c = *hp++; if (c == NUL || c == '\r' || c == '\n') break; - if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') - c = '.'; - *dst++ = c; + *dst++ = name_char(c); } *dst = NUL; } else { diff --git a/doc/devices/PB-2132.md b/doc/devices/PB-2132.md new file mode 100644 index 00000000..d3e91d4e --- /dev/null +++ b/doc/devices/PB-2132.md @@ -0,0 +1,53 @@ +# IG108 + +Following is documentation for unmanaged switch marked as `IG108`. + +Original software is running UART on 9600 baud rate. + +Using SPI clamp in-board is the only method for initial installation. + +### Brands + +| Brand | Type | Managed | PCB | Flash | Chip RTL | +|---------|------------|---------|------------|-------------|---------------| +| Goalake | IG108 | No | PB-2132 | cFeon Q32A | 8373N + 8224N | +| Netcore | GS9 | No | PB-2132 | cFeon Q32A | 8373N + 8224N | + +### What works + +- All eight 2.5GBASE-T RJ45 ports at 10/100/1000/2500 Mbps +- SFP port with 1G/2.5G/10G modules +- LEDs + +### PCB overview + +**Board markings** + +- Top silkscreen: PB-2132 + +Front Goalake: + + + +Label Goalake: + + + +Top side: + + + +Bottom: + + + +### Serial console + +The PCB has unpopulated through-holes near the SFP Port, with description of each pin, works out of the box. + +- **Settings**: 115200 baud / 8N1 / 3.3V TTL + +### Power supply + +Input power is delivered via barell plug, `12V 1A` adapter was provided. + diff --git a/doc/devices/photos/PB-2132/PCB-bottom.jpg b/doc/devices/photos/PB-2132/PCB-bottom.jpg new file mode 100644 index 00000000..63cc5352 Binary files /dev/null and b/doc/devices/photos/PB-2132/PCB-bottom.jpg differ diff --git a/doc/devices/photos/PB-2132/PCB-top.jpg b/doc/devices/photos/PB-2132/PCB-top.jpg new file mode 100644 index 00000000..a0d47ab9 Binary files /dev/null and b/doc/devices/photos/PB-2132/PCB-top.jpg differ diff --git a/doc/devices/photos/PB-2132/case.jpg b/doc/devices/photos/PB-2132/case.jpg new file mode 100644 index 00000000..4dbfed06 Binary files /dev/null and b/doc/devices/photos/PB-2132/case.jpg differ diff --git a/doc/devices/photos/PB-2132/label.jpg b/doc/devices/photos/PB-2132/label.jpg new file mode 100644 index 00000000..a70dc2df Binary files /dev/null and b/doc/devices/photos/PB-2132/label.jpg differ diff --git a/doc/supported_devices.md b/doc/supported_devices.md index 1073ef47..4257544e 100644 --- a/doc/supported_devices.md +++ b/doc/supported_devices.md @@ -6,6 +6,7 @@ The following devices have been tested and are fully working: | Ampcom | WAM902-SWTG018AS| No | [SWTG018AS-A V2.0](devices/SWTG018AS_A_V2_0.md) | | 8 + 1 | | Davuaz | Da-K6501W | No | [PCB-K0501W-V2.0](devices/K0501W_V2_0.md) | | 5 + 1 | | FOXNEO | FNS-1200P | No | [PCB-K0402W-U13-V2.0](devices/FNS-1200P.md) | 2M | 4 + 2 | +| Goalake | IG108 | No | [PB-2132](devices/PB-2132.md) | 4M | 8 + 1 | | Hisource | Hi-K0402WS | No | [PCB-K0402WS-V3.0](devices/PCB-K0402WS-V3.0.md) | | 4 + 2 | | Hisource | Hi-K0402WS | No | [PCB-K0402WS-V2.0](devices/PCB-K0402WS-V2.0.md) | 2M | 4 + 2 | | Hisource | Hi-K0801WS | No | [PCB-KO801W-V2.0](devices/HI-K0801WS.md) | | 8 + 1 | @@ -21,11 +22,13 @@ The following devices have been tested and are fully working: | keepLINK | KP-9000-9XHML-X | Yes | [2M-PCB23-V3.1](devices/2M-PCB23-V3_1.md) | 2M | 8 + 1 | | LIANGUO | SWTG024AS | No | [SWTG024AS-v2.0-17452](devices/SWTG024AS.md) | 0.5M | 4 + 2 | | Lianguo | ZX-SWTGW215AS | Yes | [PCB-SWTG115AS-V2.0 / PCB-SWTG115AS-V2.1](devices/SWTGW215AS.md) | 2M | 5 + 1 | +| Lianguo | HYWS-SGT0108S | No | [2G5F_20G_V1.01](devices/HYWS-SGT0108S.md) | 0.5M | 8 + 1 | | Mokerlink| 2G040210GSM | Yes | [2M-PCB43-V1.1](devices/2M-PCB43-V1.1.md) | | 4 + 2 | | Mokerlink| ZX-SWTGW218AS | Yes | [SWTG118AS-V2.0-16029](devices/SWTGW218AS.md) | 2M | 8 + 1 | | MokerLink| POE-2G080110GS | No | [2M-PCB51-ML-V1.1](devices/2M-PCB51-ML-V1_1.md) | 4M | 8 + 1 | | Binardat | 2G06-04210GSM | No | [PCB-SWTG024AS-A-2.0.1](devices/ONT-S207CW-62TS-SE.md) | 0.5M | 4 + 2 | | ONT | ONT-S207CW-62TS-SE | No | [PCB-SWTG024AS-A-2.0.1](devices/ONT-S207CW-62TS-SE.md) | 0.5M | 4 + 2 | +| Netcore | GS9 | No | [PB-2132](devices/PB-2132.md) | 4M | 8 + 1 | | Ruiying | RY-4GT-2SX | No | [FG-4GT-2SX_V2.0](devices/FG-4GT-2SX_V2.0.md) | 4M | 4 + 2 | | Ruiying | RY-8GT-1SX | No | [FG-8GT-1SX](devices/FG-8GT-1SX.md) | 1M | 8 + 1 | | SiscoLink | SL-G0402F | No | [PCB-K0402WS-V3.0](devices/PCB-K0402WS-V3.0.md) | | 4 + 2 | @@ -35,7 +38,6 @@ The following devices have been tested and are fully working: | Xikestore| SKS3200M-4GPY2XF| Yes | [SWTG024AS-v1.0](devices/SWTG024AS.md) | | 4 + 2 | | XikeStor | SKS3200-8E1X | Yes | [SWTG118AS-V2.1-17462](devices/SWTGW218AS.md) | 2M | 8 + 1 | | Ztyuav | Z-QWYT0402 | No | [PCB-K0402WS-V3.0](devices/PCB-K0402WS-V3.0.md) | | 4 + 2 | -| Lianguo | HYWS-SGT0108S | No | [2G5F_20G_V1.01](devices/HYWS-SGT0108S.md) | 0.5M | 8 + 1 | For KP-9000-6XH-X2 / KP-9000-6XHML-X2 / Mokerlink 2G040210GSM devices, select the machine target by PCB revision. The ML/non-ML or managed/unmanaged label diff --git a/html/app.js b/html/app.js index 0a50b322..a38d9521 100644 --- a/html/app.js +++ b/html/app.js @@ -533,13 +533,18 @@ TABS.forEach(function(tb){ }); $("burger").addEventListener("click",function(){$("nav").classList.toggle("open")}); +var IDLE_STOP_MS=600000,lastInput=Date.now(); +["mousemove","mousedown","keydown","touchstart","wheel"].forEach(function(ev){ + addEventListener(ev,function(){lastInput=Date.now()},{passive:true}); +}); function Poller(fn,ms){this.fn=fn;this.ms=ms;this.on=false;this.t=null} Poller.prototype.start=function(){if(this.on)return;this.on=true;this.tick()}; Poller.prototype.stop=function(){this.on=false;clearTimeout(this.t)}; Poller.prototype.tick=function(){ var self=this; if(!self.on)return; - var run=document.hidden?Promise.resolve():Promise.resolve().then(self.fn).catch(function(){}); + var quiet=document.hidden||Date.now()-lastInput>IDLE_STOP_MS; + var run=quiet?Promise.resolve():Promise.resolve().then(self.fn).catch(function(){}); run.then(function(){ if(self.on)self.t=setTimeout(function(){self.tick()},self.ms); }); }; @@ -723,6 +728,7 @@ tabHooks.dash={ var SPEEDS=[["auto","c_auto"],["2g5","2.5G"],["1g","1G"],["100m full","100M full"], ["100m half","100M half"],["10m full","10M full"],["10m half","10M half"]]; var SFPRATES=[["auto","c_auto"],["10g","10G"],["2g5","2.5G"],["1g","1G"],["100m","100M"]]; +var ADVSEL={63:"auto",31:"auto",32:"2g5",16:"1g",8:"100m full",4:"100m half",2:"10m full",1:"10m half"}; function speedLabel(s){ if(s==="c_auto")return t("c_auto"); return s.replace(" full"," "+t("c_full")).replace(" half"," "+t("c_half")); @@ -785,6 +791,8 @@ function portsStatus(){ S.ports.forEach(function(p){ var i=p.portNum-1,el=$("plink"+i); if(el)el.innerHTML=linkBadge(p); + var sel=$("pspd"+i),v=p.isSFP?0:ADVSEL[parseInt(p.adv,2)]; + if(sel&&v&&document.activeElement!==sel)sel.value=v; }); } function applyPort(i){ diff --git a/httpd/httpd.c b/httpd/httpd.c index 74c11148..2c67e4c2 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -37,6 +37,7 @@ __xdata uint8_t outbuf[TCP_OUTBUF_SIZE]; __xdata uint8_t entry; __xdata uint16_t slen; __xdata uint16_t o_idx; +__xdata uint16_t sent_len; __xdata uint16_t len_left; __xdata uint16_t cont_len; __xdata uint32_t cont_addr; @@ -347,8 +348,9 @@ __xdata uint8_t *scan_header(__xdata uint8_t * __xdata p) if (is_word_x(session, session_id)) { authenticated = 1; last_session_use = now; - } else + } else { dbg_string("Invalid session cookie!\n"); + } } } return p; @@ -559,7 +561,7 @@ static void handle_config_fragment(__xdata uint8_t *p) __xdata uint16_t frag_len; uint8_t taken; - frag_len = uip_len - (p - uip_appdata); + frag_len = uip_len - (p - (__xdata uint8_t *)uip_appdata); if (pre_acc + frag_len >= CONFIG_UPLOAD_BUF) { print_string("Configuration too large, aborting.\n"); config_upload = 0; @@ -589,7 +591,7 @@ static void handle_firmware_fragment(__xdata uint8_t *p) __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); __xdata uint16_t frag_len, payload_start; - frag_len = uip_len - (p - uip_appdata); + frag_len = uip_len - (p - (__xdata uint8_t *)uip_appdata); if (pre_acc + frag_len >= CONFIG_UPLOAD_BUF) { print_string("Firmware upload header too large, aborting.\n"); config_upload = 0; @@ -684,7 +686,7 @@ static uint8_t post_body_take(__xdata uint8_t *p) send_bad_request(); return 0; } - have = uip_len - (p - uip_appdata); + have = uip_len - (p - (__xdata uint8_t *)uip_appdata); if (have >= content_length) { p[content_length] = NUL; return 1; @@ -826,6 +828,13 @@ void handle_post(void) } +static void tx_send(uint16_t len) +{ + sent_len = len; + uip_send(outbuf + o_idx, len); +} + + void httpd_appcall(void) { __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); @@ -861,9 +870,9 @@ void httpd_appcall(void) } } else if (uip_acked() && s->tstate == TSTATE_TX) { dbg_string("ACK\n"); - if (slen > uip_mss()) { - slen -= uip_mss(); - o_idx += uip_mss(); + if (slen > sent_len) { + slen -= sent_len; + o_idx += sent_len; } else { slen = 0; } @@ -872,11 +881,11 @@ void httpd_appcall(void) if (slen > uip_mss()) { dbg_string("Sending A: "); dbg_short(slen); dbg_char('\n'); - uip_send(outbuf + o_idx, uip_mss()); + tx_send(uip_mss()); s->tstate = TSTATE_TX; } else if (slen > 0) { dbg_string("Sending B: "); dbg_short(slen); dbg_char('\n'); - uip_send(outbuf + o_idx, slen); + tx_send(slen); s->tstate = TSTATE_TX; } else if (cont_len) { dbg_string("CONT cont_len: "); dbg_short(cont_len); @@ -887,7 +896,7 @@ void httpd_appcall(void) flash_region.len = slen; flash_read_bulk(outbuf); o_idx = 0; - uip_send(outbuf + o_idx, slen); + tx_send(slen); cont_len -= slen; cont_addr += slen; s->tstate = TSTATE_TX; @@ -1045,24 +1054,20 @@ do_send: o_idx = 0; if (slen > uip_mss()) { dbg_string("Sending a: "); dbg_short(slen); dbg_char('\n'); - uip_send(outbuf + o_idx, uip_mss()); + tx_send(uip_mss()); dbg_string("Sending a done\n"); } else { dbg_string("Sending b: "); dbg_short(slen); dbg_char('\n'); - uip_send(outbuf + o_idx, slen); + tx_send(slen); dbg_string("Sending b done\n"); } s->tstate = TSTATE_TX; } else if (uip_rexmit()) { // Connection established, need to rexmit? dbg_string("RETRANSMIT requested\n"); - if (slen > uip_mss()) { - dbg_string("Sending C: "); dbg_short(slen); dbg_char('\n'); - uip_send(outbuf + o_idx, uip_mss()); + if (sent_len) { + dbg_string("Sending C: "); dbg_short(sent_len); dbg_char('\n'); + tx_send(sent_len); dbg_string("Sending C done\n"); - } else if (slen > 0) { - dbg_string("Sending D: "); dbg_short(slen); dbg_char('\n'); - uip_send(outbuf + o_idx, slen); - dbg_string("Sending D done\n"); } s->tstate = TSTATE_TX; uip_len = 0; diff --git a/httpd/page_impl.c b/httpd/page_impl.c index cb11075c..58624145 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -70,6 +70,21 @@ void char_to_html(char c) } +static void str_x_to_html(__xdata const char *s) +{ + while (*s) + outbuf[slen++] = *s++; +} + + +void json_char_to_html(uint8_t c) +{ + if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') + c = '.'; + outbuf[slen++] = c; +} + + // Convert uint8_t to ascii HEX char. void byte_to_html(uint8_t val) { @@ -198,8 +213,8 @@ void send_sfp_info(uint8_t sfp) if (i < 20 || i >= 60 || (i >= 36 && i < 40)) // Skip Non-ASCII codes continue; uint8_t c = sfp_buf[i & 0xf]; - if (c && c != 0xa0) // a0 is the byte read from a non-existant I2C EEPROM - char_to_html(c); + if (c) + json_char_to_html(c); } } @@ -293,7 +308,7 @@ void send_vlan(uint16_t vlan) dbg_string("VLAN has no name\n"); } else { while(vlan_names[n] && vlan_names[n] != ' ') - char_to_html(vlan_names[n++]); + json_char_to_html(vlan_names[n++]); } slen += strtox(outbuf + slen, "\",\"pvid\":\"0x"); uint16_t pvid_mask = 0; @@ -825,7 +840,7 @@ void send_status(void) itoa_html(i); slen += strtox(outbuf + slen, ",\"name\":\""); for (uint8_t j = 0; j < PORT_NAME_SIZE && port_names[i][j]; j++) { - char_to_html(port_names[i][j]); + json_char_to_html(port_names[i][j]); } slen += strtox(outbuf + slen, "\""); @@ -864,14 +879,11 @@ void send_status(void) sfp_send_data(sfp, 238, 1); } slen += strtox(outbuf + slen,"\",\"sfp_vendor\":\""); - for (uint8_t s = 0; s < 16 && sfp_module_vendor[sfp][s]; s++) - outbuf[slen++] = sfp_module_vendor[sfp][s]; + str_x_to_html(sfp_module_vendor[sfp]); slen += strtox(outbuf + slen,"\",\"sfp_model\":\""); - for (uint8_t s = 0; s < 16 && sfp_module_model[sfp][s]; s++) - outbuf[slen++] = sfp_module_model[sfp][s]; + str_x_to_html(sfp_module_model[sfp]); slen += strtox(outbuf + slen,"\",\"sfp_serial\":\""); - for (uint8_t s = 0; s < 16 && sfp_module_serial[sfp][s]; s++) - outbuf[slen++] = sfp_module_serial[sfp][s]; + str_x_to_html(sfp_module_serial[sfp]); slen += strtox(outbuf + slen,"\",\"sfp_los\":"); if (machine.sfp_port[sfp].pin_los == GPIO_NA) { slen += strtox(outbuf + slen,"null"); @@ -1043,7 +1055,7 @@ void send_vlanlist(void) n = vlan_name(i); if (n != 0xffff) { while (vlan_names[n] && vlan_names[n] != ' ') - char_to_html(vlan_names[n++]); + json_char_to_html(vlan_names[n++]); } slen += strtox(outbuf + slen, "\"}"); diff --git a/machine.c b/machine.c index 1b06e60b..6346dc48 100644 --- a/machine.c +++ b/machine.c @@ -1289,7 +1289,89 @@ __code const struct machine machine = { 0x1a, 0x19, 0x1d, 0x1e, 0x1c, 0x1d, 0x20, 0x21 }, }; +#elif defined MACHINE_PB_2132 +__code const struct machine machine = { + .machine_name = "PB-2132", + .isRTL8373 = 1, + .min_port = 0, + .max_port = 8, + .n_sfp = 1, + .log_to_phys_port = {1, 2, 3, 4, 5, 6, 7, 8, 9}, + .phys_to_log_port = {0, 1, 2, 3, 4, 5, 6, 7, 8}, + .is_sfp = {0, 0, 0, 0, 0, 0, 0, 0, 1}, + .sfp_port[0].pin_detect = GPIO30_ACL_BIT3_EN, + .sfp_port[0].pin_los = GPIO37, + .sfp_port[0].pin_tx_disable = GPIO_NA, + .sfp_port[0].sds = 1, + .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + + .reset_pin = GPIO_NA, + + .high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 }, + .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, + + .led_sets = { + { + /* LED0 = RJ45: Amber LED: 2.5G */ + LEDS_2G5 | LEDS_LINK | LEDS_ACT, + /* LED1 = RJ45: Green LED: 10M / 100M / 1000M */ + LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT, + 0, + 0 + }, + { + /* SFP+ single Green LED. */ + LEDS_10G | LEDS_5G | LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_LINK | LEDS_ACT, + 0, + 0, + 0 + }, + }, + }; + +#elif defined MACHINE_HASIVO_S600W_4GT_2XGT_SE +__code const struct machine machine = { + .machine_name = "Hasivo S600W-4GT-2XGT-SE", + .isRTL8373 = 0, + .min_port = 3, + .max_port = 8, + .n_sfp = 0, + .n_10g = 2, + .log_to_phys_port = {0, 0, 0, 5, 1, 2, 3, 4, 6}, + .phys_to_log_port = {4, 5, 6, 7, 3, 8, 0, 0, 0}, + .is_sfp = {0, 0, 0, 0, 0, 0, 0, 0, 0}, + .reset_pin = GPIO54_ACL_BIT2_EN, + .high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 }, + .port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + /* Ports 1-4: Green: 2.5GBit, Orange: 10/100/1000MBit + * Ports 5-6: Green: 10GBit, Orange: 5/2.5GBit, None: 10/100/1000MBit + */ + .led_sets = { + { + LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT, // Yellow + LEDS_2G5 | LEDS_LINK, // Green + LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK, // Orange + 0, // Unused + }, + { + LEDS_10G | LEDS_5G | LEDS_2G5 | LEDS_1G | LEDS_100M | LEDS_10M | LEDS_LINK | LEDS_ACT, // Yellow + LEDS_10G | LEDS_LINK, // Green + LEDS_5G | LEDS_2G5 | LEDS_LINK, // Orange + 0, // Unused + }, + }, + .led_mux_custom = 1, + .led_mux = { + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x0f, 0x20, + 0x21, 0x22, 0x10, 0x11, 0x12, + 0x14, 0x15, 0x16, 0x18, 0x19, + 0x1a, 0x1c, 0x1d, 0x1e, 0x0c, + 0x0d, 0x0e, 0x23 + }, + +}; #else #error "Please select a machine type in machine.h" #endif diff --git a/machine.h b/machine.h index 7e19167e..191c4c84 100644 --- a/machine.h +++ b/machine.h @@ -53,6 +53,8 @@ // #define MACHINE_FG_8GT_1SX // #define MACHINE_LIANGUO_HYWS_SGT0108S // #define MACHINE_POE_2G080110GS +// #define MACHINE_PB_2132 +// #define MACHINE_HASIVO_S600W_4GT_2XGT_SE typedef struct { // GPIO pins for SDA/SCL diff --git a/sfp.c b/sfp.c index 7bc4f301..1f1f6652 100644 --- a/sfp.c +++ b/sfp.c @@ -88,7 +88,12 @@ bool sfp_read_field(__xdata char *dst, uint8_t sfp, uint8_t start, uint8_t lengt return false; dst[length] = NUL; - memcpy(dst, sfp_buf, length); + for (uint8_t i = 0; i < length; i++) { + uint8_t c = sfp_buf[i]; + if (c && (c < 0x20 || c > 0x7e || c == '"' || c == '\\')) + c = '.'; + dst[i] = c; + } while (length > 0 && dst[--length] == ' ') dst[length] = NUL; diff --git a/test/Makefile b/test/Makefile index 1740bbb8..e394aec4 100644 --- a/test/Makefile +++ b/test/Makefile @@ -11,7 +11,7 @@ CC = gcc SRCROOT = .. # -include shim first so __xdata/__code/__banked vanish before any firmware header. -CFLAGS = -std=gnu11 -g -O1 -Wall -Wextra -Wno-unused-parameter -Wno-unknown-pragmas \ +CFLAGS = -std=gnu11 -g -O1 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-unknown-pragmas \ -Wno-pointer-sign \ -DRTLP_HOST_TEST \ -fsanitize=address,undefined -fno-omit-frame-pointer \ @@ -26,7 +26,8 @@ BUILDDIR = build # One binary per subsystem under test. TESTS = $(BUILDDIR)/test_cmd_editor \ $(BUILDDIR)/test_port_tables \ - $(BUILDDIR)/test_page_json + $(BUILDDIR)/test_page_json \ + $(BUILDDIR)/test_httpd_tx # The table tests link the register mock and one environment file; stub/ # stands in for the two headers a firmware build generates. @@ -49,6 +50,11 @@ $(BUILDDIR)/test_port_tables: test_port_tables.c $(TABLE_ENV) hw_mock.h | $(BUIL $(BUILDDIR)/test_page_json: test_page_json.c $(TABLE_ENV) hw_mock.h | $(BUILDDIR) $(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS) +# httpd/httpd.c and uip/uip.c are compiled UNMODIFIED from the firmware tree; +# this test carries the environment below them itself. +$(BUILDDIR)/test_httpd_tx: test_httpd_tx.c $(SRCROOT)/httpd/httpd.c $(SRCROOT)/uip/uip.c | $(BUILDDIR) + $(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS) + run: $(TESTS) @fail=0; for t in $(TESTS); do \ echo "=== $$t ==="; \ diff --git a/test/README.md b/test/README.md index cd03b247..08ec0cef 100644 --- a/test/README.md +++ b/test/README.md @@ -47,6 +47,7 @@ fires — so it drops straight into CI. | `test_cmd_editor` | `cmd_editor.c` | **C4** — full-line hang + `cmd_buffer` 1-byte overflow; basic entry & backspace regressions | | `test_port_tables` | `rtl837x_port.c` | VLAN entry layout and round trip, PVID register sharing, static multicast and management entries, per-port flush, trunk membership and hash seed | | `test_page_json` | `httpd/page_impl.c` + `rtl837x_port.c` | `/vlan.json`, `/vlanlist`, `/l2.json` (walk, wrap marker, paging inside `outbuf`), 64-bit counters in `/status.json` and `/counters.json` | +| `test_httpd_tx` | `httpd/httpd.c` + `uip/uip.c` | a GET of a static file against a client that moves its receive window: ACK accounting, continuation chunks out of flash, retransmission | ## Adding a test for another module 1. Write `test_.c` with `main()` driving the module's entry points and diff --git a/test/stub/html_data.h b/test/stub/html_data.h index 33f7fec3..6e7f0029 100644 --- a/test/stub/html_data.h +++ b/test/stub/html_data.h @@ -6,4 +6,6 @@ #include typedef enum mime_type_e { mime_HTML = 0, mime_SVG, mime_ICO, mime_PNG, mime_JS, mime_CSS, mime_TXT } mime_type_t; struct f_data { __code char *file; uint32_t start; uint16_t len; mime_type_t mime; uint8_t gzip; }; +#define FDATA_START_login_html 0x00040000UL +#define FDATA_START_favicon_ico 0x00041000UL #endif diff --git a/test/test_httpd_tx.c b/test/test_httpd_tx.c new file mode 100644 index 00000000..0b2d7a23 --- /dev/null +++ b/test/test_httpd_tx.c @@ -0,0 +1,473 @@ +/* + * Host-side bench for the httpd transmit path. + * + * Compiles the UNMODIFIED httpd/httpd.c and uip/uip.c and drives them through a + * real handshake and a real GET from a client that moves its receive window. + * Everything below the two modules - flash, console, JSON pages - is mocked + * here, so what the bench observes is the byte stream the firmware would put on + * the wire. + * + * The file served out of the simulated flash carries a position-dependent + * pattern, so a duplicated or skipped range shows up as a mismatch at a known + * offset instead of an anonymous "content differs". + * + * Run: make -C test (exit code 0 = all scenarios pass) + */ +#include +#include +#include + +/* Some hosts ship these in , and they assign to their argument; + * uip.h refuses to be included next to them. The firmware definitions are the + * ones this bench needs, so the host macros go first. */ +#undef HTONS +#undef NTOHS + +#include "httpd.h" +#include "uip.h" +#include "rtl837x_common.h" +#include "rtl837x_flash.h" +#include "page_impl.h" +#include "html_data.h" + +/* Private to uip.c, and the client needs them to build segments. */ +#define TCP_SYN 0x02 +#define TCP_PSH 0x08 +#define TCP_ACK 0x10 + +#define TCPH ((struct uip_tcpip_hdr *)&uip_buf[UIP_LLH_LEN]) + +#define MSS_FULL 1460 +#define SMALL_WINDOW 600 + +/* httpd.c serves this one file without a session, which keeps the bench clear + * of the login machinery: what is under test is the transmit path. */ +#define FILE_START FDATA_START_login_html +#define FILE_NAME "/login.html" +#define FILE_LEN 6000 +#define STREAM_MAX 16384 + +static int failures; +static int verbose; + +#define CHECK(cond, name) do { \ + if (cond) printf("PASS %s\n", name); \ + else { printf("FAIL %s\n", name); failures++; } \ +} while (0) + +static uint8_t pattern(uint32_t addr) +{ + return (uint8_t)((addr * 7u) + (addr >> 8)); +} + +/* ---- firmware environment below httpd.c --------------------------------- */ + +volatile uint8_t sfr_data[4]; +volatile uint32_t ticks; +uint8_t cmd_capture; +uint8_t err_status; +uint8_t *hex = (uint8_t *)"0123456789abcdef"; +uint16_t crc_value; +uint8_t *HTTP_RESPONCE_TXT = (uint8_t *)"HTTP/1.1 200 OK\r\n\r\n"; +uint32_t flash_size = 0x80000; +uint8_t flash_buf[FLASH_BUF_SIZE]; +struct flash_region_t flash_region; + +char *mime_strings[] = { "text/html", "image/svg+xml", "image/x-icon", + "image/png", "text/javascript", "text/css", "text/plain" }; + +struct f_data f_data[] = { + { FILE_NAME, FILE_START, FILE_LEN, mime_HTML, 0 }, + { 0, 0, 0, mime_HTML, 0 }, +}; + +void flash_read_bulk(uint8_t *dst) +{ + for (uint16_t i = 0; i < flash_region.len; i++) + dst[i] = pattern(flash_region.addr + i); +} + +void flash_init(uint8_t enable_dio) { (void)enable_dio; } +void flash_sector_erase(void) { } +void flash_write_bytes(uint8_t *ptr) { (void)ptr; } +char *get_flash_size_str(void) { return "512 kB"; } +void crc16_bank1(uint8_t *v) { (void)v; } +void reset_chip(void) { } +void delay(uint16_t t) { (void)t; } +void write_char(char c) { (void)c; } +void write_char_no_syslog(char c) { (void)c; } +void print_string(const char *p) { (void)p; } +void print_string_newline_no_syslog(const char *p) { (void)p; } +void set_sys_led_state(uint8_t state) { (void)state; } +void cmd_parser(void) { } +void execute_config(void) { } +void execute_commands(uint8_t *p) { (void)p; } +void clear_command_history(void) { } +void udp_callbacks(void) { } +void tcpip_output(void) { } +void get_random_32(void) { } +void read_reg_timer(uint32_t *tmr) { *tmr = 0; } + +/* uip-conf.h asks uIP not to define the packet buffer: on the switch it lives + * at a fixed XDATA address, so the bench provides the storage itself. */ +u8_t uip_buf[UIP_BUFSIZE + 2]; + +uint16_t strlen_x(const char *s) { return (uint16_t)strlen(s); } + +uint16_t strtox(uint8_t *dst, const char *s) +{ + uint16_t n = 0; + + while (s[n]) { dst[n] = (uint8_t)s[n]; n++; } + return n; +} + +void memcpyc(uint8_t *dst, uint8_t *src, uint16_t len) { memcpy(dst, src, len); } + +bool strstart(const uint8_t *a, const uint8_t *b) +{ + while (*b) { if (*a++ != *b++) return false; } + return true; +} + +bool strstart_x(const uint8_t *a, const uint8_t *b) { return strstart(a, b); } + +bool send_counters(uint8_t phys_port) { (void)phys_port; return false; } +void send_status(void) { } +void send_vlan(uint16_t vlan) { (void)vlan; } +void send_basic_info(void) { } +void send_bandwidth(void) { } +void send_eee(void) { } +void send_l2(uint16_t idx) { (void)idx; } +void l2_delete(uint16_t idx) { (void)idx; } +void send_mirror(void) { } +void send_mtu(void) { } +void send_config(void) { } +void send_cmd_log(void) { } +void send_lag(void) { } +void send_stp(void) { } +void send_stp_counters(void) { } +void send_vlanlist(void) { } + +extern uint8_t authenticated; + +/* uip.c defines the listen table but no header declares it; the bench reads it + * to confirm the port httpd_init() asked for is the one uIP is watching. */ +extern u16_t uip_listenports[UIP_LISTENPORTS]; + +/* ---- the simulated client ----------------------------------------------- */ + +static uint32_t cli_seq; /* next sequence number we send */ +static uint32_t cli_rcv_nxt; /* next sequence number we expect */ +static uint16_t cli_window; /* what we advertise */ + +static uint8_t stream[STREAM_MAX]; /* bytes accepted from the server */ +static int stream_len; + +static uint8_t last_tx[MSS_FULL + 64]; /* payload of the last server segment */ +static int last_tx_len; + +static uint16_t hton16(uint16_t v) +{ + return (uint16_t)((v << 8) | (v >> 8)); +} + +static void wr32(uint8_t *p, uint32_t v) +{ + p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v; +} + +static uint32_t rd32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | p[3]; +} + +static void trace(const char *tag) +{ + if (!verbose) + return; + printf(" [%-8s] uip_len=%-5u flags=0x%02x seq=%-6u ack=%-6u | conn: state=0x%02x len=%-5u mss=%-5u tstate=%u\n", + tag, uip_len, + uip_len ? TCPH->flags : 0, + uip_len ? rd32(TCPH->seqno) : 0, + uip_len ? rd32(TCPH->ackno) : 0, + uip_conns[0].tcpstateflags, uip_conns[0].len, uip_conns[0].mss, + uip_conns[0].appstate.tstate); +} + +/* Takes in one outgoing segment, if uIP produced one. */ +static int harvest(void) +{ + uint8_t *payload; + uint32_t seq; + int plen, hlen; + + if (uip_len == 0) + return -1; + + seq = rd32(TCPH->seqno); + /* The SYNACK carries the MSS option, so the header is not always 20 B. */ + hlen = (TCPH->tcpoffset >> 4) * 4; + payload = &uip_buf[UIP_LLH_LEN + 20 + hlen]; + plen = (int)uip_len - 20 - hlen; + if (plen < 0) + plen = 0; + + if (plen > 0) { + last_tx_len = plen > (int)sizeof(last_tx) ? (int)sizeof(last_tx) : plen; + memcpy(last_tx, payload, last_tx_len); + + /* A real client keeps what continues the stream and drops the + * rest, so bytes sent twice under new sequence numbers land in + * the file just as they would in a browser. */ + if (seq == cli_rcv_nxt) { + if (stream_len + plen <= STREAM_MAX) { + memcpy(stream + stream_len, payload, plen); + stream_len += plen; + } + cli_rcv_nxt += plen; + } + if (verbose) + printf(" server -> %d B, seq %u\n", plen, seq); + } + if (TCPH->flags & TCP_SYN) + cli_rcv_nxt = seq + 1; + + uip_len = 0; + return plen; +} + +static void client_send(uint8_t flags, const char *payload, int plen) +{ + int hlen = 20; + + /* A SYN carries the MSS option, as every real client does: uIP takes the + * connection MSS from it, and without one the server has nothing to send + * with. */ + if (flags & TCP_SYN) + hlen = 24; + + memset(uip_buf, 0, UIP_LLH_LEN + 20 + hlen + (plen > 0 ? plen : 0)); + TCPH->vhl = 0x45; + TCPH->tos = 0; + TCPH->len[0] = (uint8_t)((20 + hlen + plen) >> 8); + TCPH->len[1] = (uint8_t)((20 + hlen + plen) & 0xff); + TCPH->ttl = 64; + TCPH->proto = UIP_PROTO_TCP; + TCPH->ipchksum = 0; + TCPH->srcipaddr[0] = hton16(0x0a00); TCPH->srcipaddr[1] = hton16(0x0002); + TCPH->destipaddr[0] = hton16(0x0a00); TCPH->destipaddr[1] = hton16(0x0001); + TCPH->srcport = hton16(40000); + TCPH->destport = hton16(80); + wr32(TCPH->seqno, cli_seq); + wr32(TCPH->ackno, cli_rcv_nxt); + TCPH->tcpoffset = (uint8_t)((hlen / 4) << 4); + TCPH->flags = flags; + TCPH->wnd[0] = (uint8_t)(cli_window >> 8); + TCPH->wnd[1] = (uint8_t)(cli_window & 0xff); + TCPH->tcpchksum = 0; + + if (hlen == 24) { + uint8_t *opt = &uip_buf[UIP_LLH_LEN + 40]; + + opt[0] = 2; opt[1] = 4; /* kind = MSS, length 4 */ + opt[2] = MSS_FULL >> 8; opt[3] = MSS_FULL & 0xff; + } + if (plen > 0) + memcpy(&uip_buf[UIP_LLH_LEN + 20 + hlen], payload, plen); + + uip_len = 20 + hlen + plen; + uip_input(); + trace("input"); + cli_seq += plen; + if (flags & TCP_SYN) + cli_seq++; + harvest(); +} + +static void client_ack(uint16_t window) +{ + cli_window = window; + client_send(TCP_ACK, NULL, 0); +} + +static void run_periodic(int rounds) +{ + for (int i = 0; i < rounds; i++) { + uip_periodic(0); + trace("timer"); + harvest(); + } +} + +static void session_start(uint16_t window) +{ + uip_init(); + httpd_init(); + authenticated = 1; + + cli_seq = 1000; + cli_rcv_nxt = 0; + cli_window = window; + stream_len = 0; + last_tx_len = 0; + + client_send(TCP_SYN, NULL, 0); + client_ack(window); +} + +static void request_file(uint16_t window) +{ + static const char get[] = "GET " FILE_NAME " HTTP/1.1\r\nHost: sw\r\n\r\n"; + + cli_window = window; + client_send(TCP_ACK | TCP_PSH, get, (int)strlen(get)); +} + +/* Drains the response, acknowledging every segment with the given window. */ +static void drain(uint16_t window) +{ + for (int i = 0; i < 20 && stream_len < STREAM_MAX; i++) { + client_ack(window); + run_periodic(1); + } +} + +static int body_offset(void) +{ + for (int i = 0; i + 4 <= stream_len; i++) + if (!memcmp(stream + i, "\r\n\r\n", 4)) + return i + 4; + return -1; +} + +/* Offset of the first body byte that is not the one the file holds there, or + * -1 when the body matches, or -2 when no header ever arrived. */ +static int first_body_mismatch(void) +{ + int off = body_offset(); + + if (off < 0) + return -2; + for (int i = 0; i < FILE_LEN && off + i < stream_len; i++) + if (stream[off + i] != pattern(FILE_START + i)) + return i; + return -1; +} + +static void report(const char *tag) +{ + if (verbose) + printf(" %s: header %d B, stream %d B, first mismatch %d\n", + tag, body_offset(), stream_len, first_body_mismatch()); +} + +/* ---- scenarios ---------------------------------------------------------- */ + +/* Instrument check: with a window that never moves, the MSS at ACK time and the + * length sent earlier are the same number, so the file must arrive intact. If + * this one fails, the bench is wrong - not the firmware. */ +static void scenario_steady_window(void) +{ + session_start(MSS_FULL); + if (verbose) + printf(" listen port=0x%04x, initial mss=%u\n", + uip_listenports[0], uip_conns[0].initialmss); + request_file(MSS_FULL); + drain(MSS_FULL); + report("steady"); + + CHECK(body_offset() >= 0, "control: response carries an HTTP header"); + CHECK(stream_len >= body_offset() + FILE_LEN, + "control: the whole file arrives"); + CHECK(first_body_mismatch() == -1, + "control: file content without duplicates or gaps"); +} + +/* The client stops draining, so the window in the ACK is smaller than the + * segment that ACK covers. */ +static void scenario_shrinking_window(void) +{ + session_start(MSS_FULL); + request_file(MSS_FULL); + drain(SMALL_WINDOW); + report("shrinking"); + + CHECK(body_offset() >= 0, + "shrinking window: response carries an HTTP header"); + CHECK(stream_len >= body_offset() + FILE_LEN, + "shrinking window: the whole file arrives"); + CHECK(first_body_mismatch() == -1, + "shrinking window: file content without duplicates or gaps"); +} + +/* The client catches up, so the window in the ACK is larger than the segment + * that ACK covers. */ +static void scenario_growing_window(void) +{ + session_start(SMALL_WINDOW); + request_file(SMALL_WINDOW); + drain(MSS_FULL); + report("growing"); + + CHECK(stream_len >= body_offset() + FILE_LEN, + "growing window: the whole file arrives"); + CHECK(first_body_mismatch() == -1, + "growing window: file content without duplicates or gaps"); +} + +/* A segment is lost, a window update shrinks the window while it is still + * unacknowledged, and the retransmission timer fires. */ +static void scenario_rexmit_after_shrink(void) +{ + uint8_t original[MSS_FULL + 64]; + int original_len, same = 1; + + session_start(MSS_FULL); + request_file(MSS_FULL); + + original_len = last_tx_len; + memcpy(original, last_tx, original_len); + + /* The segment never arrived: take it back out of the client's stream and + * send a pure window update, which acknowledges nothing. */ + if (original_len > 0 && stream_len >= original_len) { + stream_len -= original_len; + cli_rcv_nxt -= original_len; + } + cli_window = SMALL_WINDOW; + client_send(TCP_ACK, NULL, 0); + + memset(&uip_buf[UIP_LLH_LEN + 40], 0xaa, MSS_FULL); + + last_tx_len = 0; + run_periodic(UIP_RTO + 2); + + if (last_tx_len != original_len || memcmp(last_tx, original, original_len)) + same = 0; + + if (verbose) + printf(" first %d B, retransmitted %d B\n", + original_len, last_tx_len); + + CHECK(original_len > 0, "retransmission: the first segment went out"); + CHECK(same, "retransmission: the repeat carries the same bytes"); +} + +int main(int argc, char **argv) +{ + if (argc > 1 && !strcmp(argv[1], "-v")) + verbose = 1; + + printf("== httpd: accounting for the bytes actually sent ==\n"); + scenario_steady_window(); + scenario_shrinking_window(); + scenario_growing_window(); + scenario_rexmit_after_shrink(); + + printf("\n%s (%d failure%s)\n", + failures ? "BENCH: FAILURES" : "BENCH: ALL PASS", + failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +}