From f21b3a32bd8c8c08e42d2ff48c42b1a43d68ec3d Mon Sep 17 00:00:00 2001 From: d00f Date: Sat, 25 Jul 2026 12:45:10 +0200 Subject: [PATCH 1/6] system: configurable hostname (device identity) Add a device hostname settable from the CLI (`hostname `) and the System Settings page. The value is sanitized on ingest to JSON-safe printable ASCII (<=23 chars), stored in a shared __xdata buffer, seeded to "RTLPlayground" at boot, persisted through the startup-config, and reported in /information.json. It lives in the common header so other modules can advertise it (LLDP uses it as the System Name TLV). --- cmd_parser.c | 12 ++++++++++++ html/config.js | 2 ++ html/i18n.js | 6 ++++++ html/system.html | 5 +++++ html/system.js | 9 +++++++++ httpd/page_impl.c | 6 ++++++ rtl837x_common.h | 5 +++++ rtlplayground.c | 2 ++ 8 files changed, 47 insertions(+) diff --git a/cmd_parser.c b/cmd_parser.c index 21ff7a8..f1f7c28 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1524,6 +1524,18 @@ void cmd_parser(void) __banked igmp_show(); else igmp_setup(); // Reverts to default with IP-MC being flooded + } else if (cmd_compare(0, "hostname")) { + /* hostname : rest of the line, sanitized to JSON-safe + * printable ASCII (<=23 chars), stored in the shared hostname. */ + __xdata uint8_t *hp = &cmd_buffer[cmd_words_b[1]]; + uint8_t hn = 0; + if (cmd_words_len >= 2) + while (hn < 23 && hp[hn] && hp[hn] != '\r' && hp[hn] != '\n') { + hostname[hn] = (hp[hn] < 0x20 || hp[hn] > 0x7e + || hp[hn] == '"' || hp[hn] == '\\') ? '.' : hp[hn]; + hn++; + } + hostname[hn] = 0; } else if (cmd_compare(0, "stp")) { if (cmd_compare(1, "on")) { print_string("STP enabled\n"); diff --git a/html/config.js b/html/config.js index 6efa283..8af3fba 100644 --- a/html/config.js +++ b/html/config.js @@ -25,6 +25,7 @@ const conf_cmds = [ /^igmp\s+(on|off)$/, /^mtu\s+\d{1,2}\s+\d+$/, /^bw\s+(in|out)\s+\d{1,2}\s+\S+$/, + /^hostname\s+.{1,23}$/, ]; const conf_overwrite = [ /^ip\b/, @@ -49,6 +50,7 @@ const conf_overwrite = [ /^igmp\b/, /^mtu\s+\d{1,2}\b/, /^bw\s+(in|out)\s+\d{1,2}\b/, + /^hostname\b/, ]; function parseConf(s){ diff --git a/html/i18n.js b/html/i18n.js index bab49dc..676e9af 100644 --- a/html/i18n.js +++ b/html/i18n.js @@ -138,6 +138,8 @@ var LANG = { sys_tab_console: 'Console', sys_heading: 'System Settings', sys_ip: 'IP address:', + sys_hostname: 'Hostname:', + sys_apply: 'Apply', sys_netmask: 'Netmask:', sys_gateway: 'Gateway:', sys_language: 'Language:', @@ -317,6 +319,8 @@ var LANG = { sys_tab_console: 'コンソール', sys_heading: 'システム設定', sys_ip: 'IP アドレス:', + sys_hostname: 'ホスト名:', + sys_apply: '適用', sys_netmask: 'ネットマスク:', sys_gateway: 'ゲートウェイ:', sys_language: '言語:', @@ -496,6 +500,8 @@ var LANG = { sys_tab_console: '控制台', sys_heading: '系统设置', sys_ip: 'IP 地址:', + sys_hostname: '主机名:', + sys_apply: '应用', sys_netmask: '子网掩码:', sys_gateway: '网关:', sys_language: '语言:', diff --git a/html/system.html b/html/system.html index d1c908a..36369fe 100644 --- a/html/system.html +++ b/html/system.html @@ -25,6 +25,11 @@

System Settings

+
+
+
+
+
diff --git a/html/system.js b/html/system.js index 091ff61..4c8aa86 100644 --- a/html/system.js +++ b/html/system.js @@ -48,6 +48,14 @@ async function cmdSub() { } +async function hostSub() { + const h = document.getElementById("hostname").value; + try { await fetch('/cmd', { method: 'POST', body: "hostname " + h }); } + catch(err) { console.error(`Error: ${err}`); } + fetchIP(); +} + + async function sendConfig(c) { if (isSaving) return; isSaving = true; @@ -123,6 +131,7 @@ function fetchIP() { document.getElementById("ip").value=s.ip_address; document.getElementById("netmask").value=s.ip_netmask; document.getElementById("gw").value=s.ip_gateway; + if (document.getElementById("hostname")) document.getElementById("hostname").value = s.hostname || ""; clearInterval(systemInterval); // Fetch and populate the config textbox fetchConfig().then((configText) => { diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 7ca29e4..223e5d5 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -252,6 +252,12 @@ void send_basic_info(void) byte_to_html(uip_ethaddr.addr[3]); char_to_html(':'); byte_to_html(uip_ethaddr.addr[4]); char_to_html(':'); byte_to_html(uip_ethaddr.addr[5]); + slen += strtox(outbuf + slen, "\",\"hostname\":\""); + { + __xdata char *hp = hostname; /* sanitized on ingest, emit verbatim */ + while (*hp) + char_to_html(*hp++); + } slen += strtox(outbuf + slen, "\",\"sw_ver\":\""); slen += strtox(outbuf + slen, VERSION_SW); slen += strtox(outbuf + slen, "\",\"build_date\":\""); diff --git a/rtl837x_common.h b/rtl837x_common.h index 9aa9af4..66df88a 100644 --- a/rtl837x_common.h +++ b/rtl837x_common.h @@ -115,6 +115,11 @@ struct flash_region_t { extern __xdata char port_names[9][PORT_NAME_SIZE]; +/* System hostname (device identity). Set via `hostname ` and the System + * Settings page, reported in /information.json. Other modules (e.g. LLDP, which + * advertises it as the System Name TLV) read it from here. */ +extern __xdata char hostname[24]; + extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE+2]; extern __xdata struct uip_eth_addr uip_ethaddr; diff --git a/rtlplayground.c b/rtlplayground.c index 30b15d8..4886848 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -120,6 +120,7 @@ __xdata uint16_t management_vlan; __xdata uint8_t tx_seq; __xdata uint8_t stpEnabled; +__xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ __code uint16_t bit_mask[16] = { 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, @@ -2145,6 +2146,7 @@ void main(void) vlan_setup(); port_l2_setup(); igmp_setup(); + strcpy((__xdata uint8_t *)hostname, "RTLPlayground"); /* default until "hostname ..." replay */ bandwidth_setup(); uip_init(); uip_arp_init(); From e6cd362a1e3efa2f0b49e477399b68cea6a7383d Mon Sep 17 00:00:00 2001 From: d00f Date: Sat, 25 Jul 2026 12:47:49 +0200 Subject: [PATCH 2/6] system: show the hardware model on the System page machine.machine_name is already reported in /information.json as hw_ver; surface it read-only on the System Settings page so the exact build target is visible in the UI, without adding a redundant JSON field. --- html/i18n.js | 3 +++ html/system.html | 4 ++++ html/system.js | 1 + 3 files changed, 8 insertions(+) diff --git a/html/i18n.js b/html/i18n.js index 676e9af..a8d43cb 100644 --- a/html/i18n.js +++ b/html/i18n.js @@ -138,6 +138,7 @@ var LANG = { sys_tab_console: 'Console', sys_heading: 'System Settings', sys_ip: 'IP address:', + sys_model: 'Model:', sys_hostname: 'Hostname:', sys_apply: 'Apply', sys_netmask: 'Netmask:', @@ -319,6 +320,7 @@ var LANG = { sys_tab_console: 'コンソール', sys_heading: 'システム設定', sys_ip: 'IP アドレス:', + sys_model: 'モデル:', sys_hostname: 'ホスト名:', sys_apply: '適用', sys_netmask: 'ネットマスク:', @@ -500,6 +502,7 @@ var LANG = { sys_tab_console: '控制台', sys_heading: '系统设置', sys_ip: 'IP 地址:', + sys_model: '型号:', sys_hostname: '主机名:', sys_apply: '应用', sys_netmask: '子网掩码:', diff --git a/html/system.html b/html/system.html index 36369fe..4b147c1 100644 --- a/html/system.html +++ b/html/system.html @@ -30,6 +30,10 @@
+
+
+
+
diff --git a/html/system.js b/html/system.js index 4c8aa86..a907ecd 100644 --- a/html/system.js +++ b/html/system.js @@ -132,6 +132,7 @@ function fetchIP() { document.getElementById("netmask").value=s.ip_netmask; document.getElementById("gw").value=s.ip_gateway; if (document.getElementById("hostname")) document.getElementById("hostname").value = s.hostname || ""; + if (document.getElementById("model")) document.getElementById("model").textContent = s.hw_ver || ""; clearInterval(systemInterval); // Fetch and populate the config textbox fetchConfig().then((configText) => { From 13120127dfe7ff4696da76c3d5e3b7c62640f5af Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 05:43:05 +0200 Subject: [PATCH 3/6] cmd: make "hostname" report the name and reject stray arguments Typing "hostname" on its own cleared the name: with no argument the copy loop never ran and the terminating NUL landed at index 0. Report the current name instead, accept exactly one argument to set it, and reject anything longer - a name with spaces tokenizes into several words, and silently keeping only the first one is worse than an error. Walk the buffer with a pointer, which the compiler codes better than indexing. Suggested-by: vDorst --- cmd_parser.c | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index f1f7c28..4e8107e 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1525,17 +1525,28 @@ void cmd_parser(void) __banked else igmp_setup(); // Reverts to default with IP-MC being flooded } else if (cmd_compare(0, "hostname")) { - /* hostname : rest of the line, sanitized to JSON-safe - * printable ASCII (<=23 chars), stored in the shared hostname. */ - __xdata uint8_t *hp = &cmd_buffer[cmd_words_b[1]]; - uint8_t hn = 0; - if (cmd_words_len >= 2) - while (hn < 23 && hp[hn] && hp[hn] != '\r' && hp[hn] != '\n') { - hostname[hn] = (hp[hn] < 0x20 || hp[hn] > 0x7e - || hp[hn] == '"' || hp[hn] == '\\') ? '.' : hp[hn]; - hn++; + /* "hostname" alone reports the current name; "hostname " + * sets it, sanitized to JSON-safe printable ASCII. A name with + * spaces would tokenize into several words - reject it instead + * of silently keeping the first one. */ + if (cmd_words_len == 1) { + print_string_x(hostname); + write_char('\n'); + } else if (cmd_words_len == 2) { + __xdata uint8_t *hp = &cmd_buffer[cmd_words_b[1]]; + __xdata char *dst = hostname; + for (uint8_t hn = 0; hn < sizeof(hostname) - 1; hn++) { + uint8_t c = *hp++; + if (c == '\0' || c == '\r' || c == '\n') + break; + if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') + c = '.'; + *dst++ = c; } - hostname[hn] = 0; + *dst = '\0'; + } else { + print_string("Error: hostname [name] - the name must not contain spaces\n"); + } } else if (cmd_compare(0, "stp")) { if (cmd_compare(1, "on")) { print_string("STP enabled\n"); From 94716ceb5d6304a445f86eb1bb522062834555f2 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 05:43:05 +0200 Subject: [PATCH 4/6] system: derive the default hostname from the MAC address Every switch came up as "RTLPlayground", so several of them on one network were indistinguishable until someone configured a name. Append the last three MAC octets (e.g. RTLPlayground-1ef924) - unique in practice, still recognisable, and any "hostname ..." line in the startup config overrides it as before. Suggested-by: plaes --- rtlplayground.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/rtlplayground.c b/rtlplayground.c index 4886848..bc010a2 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -2146,7 +2146,19 @@ void main(void) vlan_setup(); port_l2_setup(); igmp_setup(); - strcpy((__xdata uint8_t *)hostname, "RTLPlayground"); /* default until "hostname ..." replay */ + /* Default name carries the tail of the MAC, so several switches on one + * network are distinguishable out of the box (suggested in review). + * Overridden by a "hostname ..." line in the startup config. */ + strcpy((__xdata uint8_t *)hostname, "RTLPlayground-"); + /* Spelled out rather than looped: locals here land in the 8051's + * internal-RAM overlay, which is full on a build with every feature on. */ + hostname[14] = hex[uip_ethaddr.addr[3] >> 4]; + hostname[15] = hex[uip_ethaddr.addr[3] & 0xf]; + hostname[16] = hex[uip_ethaddr.addr[4] >> 4]; + hostname[17] = hex[uip_ethaddr.addr[4] & 0xf]; + hostname[18] = hex[uip_ethaddr.addr[5] >> 4]; + hostname[19] = hex[uip_ethaddr.addr[5] & 0xf]; + hostname[20] = '\0'; bandwidth_setup(); uip_init(); uip_arp_init(); From 1194823cd581bc022446e386dc5cfb47186c4f4e Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 05:43:05 +0200 Subject: [PATCH 5/6] system: drop the defensive checks around the System page fields The hostname and model fields were populated through element-existence checks and empty-string fallbacks that the other fields on the page do without. Assign them the same way. Suggested-by: vDorst --- html/system.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/html/system.js b/html/system.js index a907ecd..0add17f 100644 --- a/html/system.js +++ b/html/system.js @@ -131,8 +131,8 @@ function fetchIP() { document.getElementById("ip").value=s.ip_address; document.getElementById("netmask").value=s.ip_netmask; document.getElementById("gw").value=s.ip_gateway; - if (document.getElementById("hostname")) document.getElementById("hostname").value = s.hostname || ""; - if (document.getElementById("model")) document.getElementById("model").textContent = s.hw_ver || ""; + document.getElementById("hostname").value=s.hostname; + document.getElementById("model").textContent=s.hw_ver; clearInterval(systemInterval); // Fetch and populate the config textbox fetchConfig().then((configText) => { From 108ac9b8fc14e1ee5fea4872ebf16eab6896251c Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 16:27:56 +0200 Subject: [PATCH 6/6] system: derive the default hostname after the startup config Move the MAC-derived default name out of main() into its own function and call it after execute_config(), returning early when the config already set a name - a configured switch then does no work for it at all. The body deliberately has no local variables. Locals here - counters and pointers alike - land in the 8051's internal-RAM overlay, and on an image with LACP and STP both enabled that overlay is exhausted: a loop makes the linker fail with "Could not get 8 consecutive bytes in internal RAM for area OSEG". Moving the code into its own function does not help, since the overlay is shared across the whole image, and hoisting the locals to xdata does not either, because itohex() is inline and brings its own frame. This only shows up in an integrated build; the branch on its own links fine. Suggested-by: vDorst --- rtlplayground.c | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/rtlplayground.c b/rtlplayground.c index bc010a2..4865089 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -25,6 +25,7 @@ #include "machine.h" #include "phy.h" #include "syslog.h" +#include "httpd/page_impl.h" extern __code const struct machine machine; extern __xdata uint32_t flash_size; @@ -2005,6 +2006,34 @@ void check_and_flash_update_image(void) } } +/* Give the switch a name carrying the tail of its MAC, so several of them on + * one network are distinguishable out of the box. Called after the startup + * config has been replayed and returns at once if that config already set a + * name, so a configured switch does no work for it (suggested in review). + * + * Written without a loop on purpose. Locals - counters and pointers alike - + * land in the 8051's internal-RAM overlay, and on an image with LACP and STP + * both enabled that overlay is exhausted: a loop here makes the linker fail + * with "Could not get 8 consecutive bytes in internal RAM for area OSEG". + * Moving the code into its own function does not help; the overlay is shared + * across the whole image. Hoisting the locals to xdata does not help either, + * because itohex() is inline and brings its own frame. */ +void set_hostname_default(void) +{ + if (hostname[0] != '\0') + return; + + strcpy((__xdata uint8_t *)hostname, "RTLPlayground-"); + hostname[14] = hex[uip_ethaddr.addr[3] >> 4]; + hostname[15] = hex[uip_ethaddr.addr[3] & 0xf]; + hostname[16] = hex[uip_ethaddr.addr[4] >> 4]; + hostname[17] = hex[uip_ethaddr.addr[4] & 0xf]; + hostname[18] = hex[uip_ethaddr.addr[5] >> 4]; + hostname[19] = hex[uip_ethaddr.addr[5] & 0xf]; + hostname[20] = '\0'; +} + + void main(void) { ticks = 0; @@ -2146,19 +2175,6 @@ void main(void) vlan_setup(); port_l2_setup(); igmp_setup(); - /* Default name carries the tail of the MAC, so several switches on one - * network are distinguishable out of the box (suggested in review). - * Overridden by a "hostname ..." line in the startup config. */ - strcpy((__xdata uint8_t *)hostname, "RTLPlayground-"); - /* Spelled out rather than looped: locals here land in the 8051's - * internal-RAM overlay, which is full on a build with every feature on. */ - hostname[14] = hex[uip_ethaddr.addr[3] >> 4]; - hostname[15] = hex[uip_ethaddr.addr[3] & 0xf]; - hostname[16] = hex[uip_ethaddr.addr[4] >> 4]; - hostname[17] = hex[uip_ethaddr.addr[4] & 0xf]; - hostname[18] = hex[uip_ethaddr.addr[5] >> 4]; - hostname[19] = hex[uip_ethaddr.addr[5] & 0xf]; - hostname[20] = '\0'; bandwidth_setup(); uip_init(); uip_arp_init(); @@ -2182,6 +2198,8 @@ void main(void) early_boot_handle_button(); execute_config(); + /* After the config: a name from it wins, otherwise derive one. */ + set_hostname_default(); print_cmd_prompt(); idle_ready = 1;