From 552bd1cb1834699b2eac43451cddcd40cdeb8313 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:51:57 +0200 Subject: [PATCH 01/79] syslog: stop the log ring from lapping its reader write_char() masked writeptr and never looked at readptr, so once more than LOGBUF_SIZE bytes were written between two drains the ring wrapped over content that had not been sent yet. That is not a corner case. The ring is emptied once per main loop pass, while a single command runs to completion inside one pass, and a full L2 listing prints well over twice the 512 byte buffer. On hardware the whole 1227 byte dump came out as one 174 byte datagram whose first line began in the middle of a MAC address. Drop the character when the ring is full instead of overwriting, so what has been logged stays intact and it is the tail that is lost. A full ring also flags a line, otherwise a burst without a newline would leave the reader waiting on line_available forever and syslog would stop for good. --- rtlplayground.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/rtlplayground.c b/rtlplayground.c index 700df0b8..24f416bf 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -277,8 +277,19 @@ void write_char(char c) write_char_no_syslog(c); if (syslog_state.enabled) { - logbuf[syslog_state.writeptr++] = c; - syslog_state.writeptr &= (LOGBUF_SIZE - 1); + __xdata uint16_t next = (syslog_state.writeptr + 1) & (LOGBUF_SIZE - 1); + + /* The ring is drained once per main loop pass, and a single command + * can print several times its size, so writing on regardless laps + * the reader and the datagram starts mid record. Drop instead, and + * flag a line so the reader empties what is there even if no + * newline has been seen yet. */ + if (next == syslog_state.readptr) { + syslog_state.line_available = 1; + return; + } + logbuf[syslog_state.writeptr] = c; + syslog_state.writeptr = next; if (c == '\n') syslog_state.line_available = 1; } From eb4bae278bb4d9eb162660a0a4b0e1fbc86c61ae Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:07:57 +0200 Subject: [PATCH 02/79] Makefile: let the linker check external RAM against 0xBFFF The 8051 in the RTL837x has 48 kB of XDATA, 0x0000 to 0xBFFF. Writing a byte and reading it back from the same address works all the way up to 0xBFFF on a SWTGW218AS, while at 0xC000 the byte comes back as 0x00. Without --xram-size the linker assumes 16 MB, so running past the end would still build cleanly and only show up at run time. The value is 49151 rather than 49152. The linker check adds up the sizes of the XDATA areas, and XDATA allocation starts at 0x0001, so a limit of 49152 would still let the last byte land on 0xC000. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4b0190d2..cb94ca16 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ $(BUILDDIR)/%.rel: %.asm | create_build_dir # mv -f $(addprefix $(basename $^), .lst .rel .sym) . $(BUILDDIR)/rtlplayground.ihx: $(OBJS) $(BUILDDIR)/crtbank.rel $(BUILDDIR)/crc16.rel - $(CC) $(CC_FLAGS) -Wl-bHOME=0x00000 -Wl-bBANK1=0x14000 -Wl-bBANK2=0x24000 -Wl-bBANK3=0x34000 -Wl-r -o $@ $^ + $(CC) $(CC_FLAGS) --xram-size 49151 -Wl-bHOME=0x00000 -Wl-bBANK1=0x14000 -Wl-bBANK2=0x24000 -Wl-bBANK3=0x34000 -Wl-r -o $@ $^ $(BUILDDIR)/rtlplayground.img: $(BUILDDIR)/rtlplayground.ihx objcopy --input-target=ihex -O binary $< $@ From ae77ffe495238b27249dd084711becaa8eebf5eb Mon Sep 17 00:00:00 2001 From: gnacho Date: Mon, 14 Sep 2026 22:44:25 +0200 Subject: [PATCH 03/79] feat(translate): add Spanish (es) translations for single-page UI --- html/app.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ html/login.html | 3 +- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/html/app.js b/html/app.js index 0a50b322..15b417b1 100644 --- a/html/app.js +++ b/html/app.js @@ -332,6 +332,117 @@ fw_applied:"升级已应用",fw_done_t:"固件已升级", fw_done:"交换机已校验镜像并以新镜像重启。会话已重置,需要重新登录。", fw_login:"前往登录",fw_rebooting:"交换机正在重启...", fw_timeout:"150 秒后交换机仍未恢复: 请检查电源 / 串口控制台" +}, +es:{ +nav_dash:"Panel",nav_ports:"Puertos",nav_stp:"Árbol de expansión",nav_stats:"Estadísticas", +nav_vlan:"VLAN",nav_l2:"Tabla MAC",nav_mirror:"Duplicación",nav_lag:"LAG",nav_eee:"EEE", +nav_bw:"Ancho de banda",nav_system:"Sistema",nav_fw:"Firmware", +hdr_dirty:"cambios sin guardar",hdr_dirty_t:"La configuración en ejecución difiere de la guardada", +hdr_save:"Guardar en flash",hdr_save_t:"Persistir la configuración en ejecución a flash", +th_auto:"Sistema",th_auto_sel:"Sistema (Selenized)",sy_display:"Pantalla",sy_theme:"Tema",sy_display_note:"Se almacena solo en este navegador.",th_light:"Claro",th_dark:"Oscuro", +th_sel_light:"Selenized claro",th_sel_dark:"Selenized oscuro", +c_port:"Puerto",c_name:"Nombre",c_link:"Enlace",c_apply:"Aplicar",c_cancel:"Cancelar",c_confirm:"Confirmar", +c_close:"Cerrar",c_refresh:"Actualizar",c_off:"apagado",c_offc:"Apagado",c_down:"caído",c_on:"Encendido",c_auto:"Auto",c_delete:"Eliminar", +c_type:"Tipo",c_state:"Estado",c_enabled:"Habilitado",c_load:"Cargar",c_disable:"Deshabilitar",c_sfp:"SFP", +c_full:"dúplex completo",c_half:"semidúplex",c_devices:"dispositivos",c_yes:"sí",c_no:"no", +d_ports:"Puertos",d_ports_h:"pulsa un puerto para ver detalles",d_system:"Sistema",d_traffic:"Tráfico", +d_traffic_h:"paquetes/s, en vivo",d_txpps:"TX pps",d_rxpps:"RX pps",d_txbad:"TX erróneos",d_rxbad:"RX erróneos", +i_host:"Nombre de host",i_ip:"Dirección IP",i_mask:"Máscara de red",i_gw:"Puerta de enlace",i_mac:"MAC",i_fw:"Firmware", +i_built:"Compilado",i_hw:"Hardware",i_flash:"Flash",i_syslog:"Syslog", +p_state:"Estado",p_disabled:"deshabilitado",p_up:"activo",p_txgb:"TX correctos / erróneos",p_rxgb:"RX correctos / erróneos", +p_pkts:"paquetes",p_module:"Módulo",p_temp:"Temperatura",p_vcc:"Vcc",p_txbias:"Polarización TX",p_txpower:"Potencia TX", +p_rxpower:"Potencia RX",p_txfault:"Fallo TX",p_txdis:"TX deshabilitado",p_rxlos:"Pérdida RX (LOS)",p_adv:"Anunciado", +pt_title:"Configuración de puerto",pt_conn:"Conectado",pt_speed:"Velocidad",pt_mtu:"MTU", +pt_sfp_note:"La velocidad del SFP se ajusta por ranura y no se guarda en la configuración de inicio.", +pt_name_err:"Nombre de puerto: 1-15 caracteres, sin espacios",pt_mtu_err:"El MTU debe estar entre 64 y 16383", +stp_title:"Árbol de expansión",stp_h:"RSTP, 802.1w",stp_version:"Versión",stp_v_stp:"STP compatible", +stp_prio:"Prioridad del puente",stp_hello:"Hello [s]",stp_maxage:"Edad máxima [s]",stp_fwd:"Retardo de reenvío [s]", +stp_txhold:"Retención TX",stp_bridge_apply:"Aplicar ajustes del puente", +stp_off_msg:"El árbol de expansión está deshabilitado: todos los puertos reenvían.",stp_bridge:"Puente",stp_root:"Raíz", +stp_root_self:"este puente es la raíz",stp_via:"vía puerto",stp_cost:"coste de ruta",stp_tc:"cambios de topología", +stp_prio_note:"La prioridad más baja gana la elección de raíz (8 por defecto = 32768). Los ajustes se aplican inmediatamente; usa Guardar en flash para conservarlos.", +stp_portcfg:"Configuración de puertos",stp_edge:"Borde",stp_pcost:"Coste de ruta",stp_pprio:"Prioridad", +stp_guard:"Protección",stp_filter:"Filtro BPDU",stp_p2p:"Punto a punto", +stp_port_note:"Los puertos de borde reenvían inmediatamente y no causan cambios de topología; Auto trata un puerto como de borde tras 3 s sin BPDUs, y cualquier BPDU recibido lo revoca. Coste de ruta 0 = automático.", +stp_status:"Estado de puertos",stp_role:"Rol",stp_db:"Puente designado",stp_dp:"Puerto designado", +stp_dc:"Coste designado",stp_oedge:"Borde operativo",stp_op2p:"P2P operativo", +stp_s0:"deshabilitado",stp_s1:"bloqueo",stp_s2:"aprendizaje",stp_s3:"reenvío", +stp_r1:"Raíz",stp_r2:"Designado",stp_r3:"Alternativo",stp_trip:"protección disparada", +stp_g_none:"Ninguno",stp_g_bpdu:"BPDU",stp_g_root:"Raíz", +stp_en_q:"¿Habilitar el árbol de expansión?", +stp_en_d:"Los puertos empiezan bloqueados y tardan hasta el doble del retardo de reenvío en alcanzar el reenvío; los puertos de borde se recuperan inmediatamente.", +stp_dis_q:"¿Deshabilitar el árbol de expansión?",stp_dis_d:"Todos los puertos pasan directamente a reenviar; se pierde la protección contra bucles.", +stp_cost_err:"El coste de ruta debe estar entre 0 y 200000000", +st_title:"Estadísticas de puertos",st_h:"totales desde el arranque",st_txg:"TX correctos",st_txb:"TX erróneos",st_rxg:"RX correctos", +st_rxb:"RX erróneos",st_details:"Detalles",st_counters:"Contadores MIB",st_nonzero:"solo no nulos", +st_autoref:"actualización automática",st_counter:"Contador",st_value:"Valor",st_fail:"error al cargar contadores", +v_title:"VLANs",v_vid:"VID",v_members:"Miembros",v_tagged:"Etiquetados",v_untagged:"Sin etiquetar",v_pvid_on:"PVID activo", +v_empty:"No hay VLANs configuradas.",v_mgmt:"VLAN de gestión",v_mgmt_none:"sin etiquetar",v_editor:"Editor de VLANs", +v_optional:"opcional",v_setmgmt:"Establecer como VLAN de gestión",v_setmgmt_t:"Hacer esta la VLAN de gestión", +v_member:"Miembro",v_pvid:"PVID", +v_legend:"U = miembro sin etiquetar, T = miembro etiquetado, - = no miembro. PVID asigna esta VLAN al tráfico entrante sin etiquetar.", +v_ingress:"Filtrado de entrada",v_ingress_h:"solo escritura: el estado no se puede leer desde el switch", +v_accept:"Aceptar",v_ing_all:"Todos",v_ing_apply:"Aplicar modos de entrada",v_del_t:"Eliminar VLAN", +v_del_q:"¿Eliminar VLAN {n}?",v_del_d:"Los puertos conservan su PVID hasta que se reasignen.", +v_vid_err:"Introduce un ID de VLAN (1-4094)",v_name_err:"El nombre debe empezar por una letra (letras, dígitos, _)", +v_loaded:"VLAN {n} cargada",v_notfound:"VLAN {n} no encontrada (aún se puede crear una nueva)", +v_nomember:"Selecciona al menos un puerto miembro (o elimina la VLAN)", +v_ing_none:"Elige un modo de entrada para al menos un puerto",v_vid_first:"Introduce primero un ID de VLAN", +v_mgmt_q:"¿Establecer VLAN {n} como VLAN de gestión?", +v_mgmt_d:"El switch empezará a etiquetar su propio tráfico con esa VLAN. Si el puerto por el que te conectas no la transporta, esta página dejará de ser accesible y el ajuste solo podrá deshacerse por la consola.", +l2_title:"Tabla de direcciones MAC",l2_filter:"filtrar...",l2_flush:"Vaciar entradas aprendidas",l2_static:"estática", +l2_learned:"aprendida",l2_loading:"cargando...",l2_failed:"error de carga",l2_entries:"entradas", +l2_del_t:"Eliminar entrada",l2_flush_q:"¿Vaciar todas las entradas MAC aprendidas?", +m_title:"Duplicación de puertos",m_active:"activa",m_monitor:"Puerto monitor",m_mirror:"Duplicar",m_both:"Ambos", +m_note:"Ambos = duplica RX y TX del puerto al puerto monitor.",m_none:"Selecciona al menos un puerto duplicado", +lag_hash:"Hash:",lag_note:"Un LAG necesita al menos un miembro para guardarse en la configuración de inicio; aplicar un grupo vacío lo borra.", +lag_clear_q:"¿Borrar LAG {n}?",lag_clear_d:"Todos los puertos miembros vuelven a la operación normal.", +e_title:"Ethernet de bajo consumo",e_adv:"Anunciado",e_lp:"Par de enlace",e_active:"Activo",e_enable:"Habilitar", +e_note:"Banderas anunciadas y del socio de enlace por velocidad: 100M, 1G, 2.5G. Los puertos SFP no soportan EEE.", +e_idle:"inactivo",e_na:"n/d", +bw_title:"Límites de ancho de banda",bw_h:"Mbit/s, 0.016-10000",bw_in:"Límite de entrada",bw_out:"Límite de salida", +bw_exceed:"Al excederse",bw_fc:"Control de flujo",bw_drop:"Descartar", +bw_in_err:"El límite de entrada debe estar entre 0.016 y 10000 Mbit/s",bw_out_err:"El límite de salida debe estar entre 0.016 y 10000 Mbit/s", +sy_network:"Red",sy_dhcp:"Usar DHCP",sy_dhcp_t:"Solicitar dirección mediante DHCP",sy_services:"Servicios", +sy_igmp:"IGMP snooping",sy_sysip:"IP del servidor",sy_server:"Servidor",sy_port:"Puerto", +sy_services_note:"El estado del servicio refleja la configuración de inicio; el estado en ejecución no es legible.", +sy_password:"Contraseña de administrador",sy_newpw:"Nueva contraseña",sy_repeat:"Repetir",sy_pwapply:"Cambiar contraseña", +sy_pw_note:"Toma efecto inmediatamente; guarda en flash para conservarla tras el reinicio.",sy_console:"Consola", +sy_cmd:"Comando de CLI...",sy_send:"Enviar",sy_startup:"Configuración de inicio",sy_replayed:"reaplicada en cada arranque", +sy_reload:"Recargar desde flash",sy_write:"Escribir en flash",sy_unknown_note:"Las líneas desconocidas se resaltan antes de escribir.", +sy_maint:"Mantenimiento",sy_reboot:"Reiniciar switch",sy_lang:"Idioma", +sy_ip_err:"IP / máscara / puerta de enlace no válidos",sy_host_err:"Nombre de host: 1-23 caracteres imprimibles, sin espacios ni comillas", +sy_net_q:"¿Aplicar ajustes de red?",sy_net_d:"La IP de gestión cambia a {ip}: esta página deberá reabrirse allí.", +sy_ip_changed:"IP cambiada, reconecta en http://{ip}/",sy_dhcp_q:"¿Cambiar a DHCP?", +sy_dhcp_d:"El switch solicita una dirección mediante DHCP. Deberás encontrar su nueva IP para reconectar.", +sy_sysip_err:"IP del servidor syslog no válida",sy_sysport_err:"El puerto de syslog debe estar entre 1 y 65535", +sy_pw_len:"Contraseña: 1-20 caracteres",sy_pw_space:"La contraseña no puede contener espacios",sy_pw_match:"Las contraseñas no coinciden", +sy_pw_q:"¿Cambiar la contraseña de administrador?",sy_pw_d:"Toma efecto inmediatamente para los nuevos inicios de sesión. Guarda en flash para persistir.", +sy_reboot_q:"¿Reiniciar el switch?",sy_reboot_d:"Hay cambios SIN GUARDAR y se perderán. Guarda en flash primero si quieres conservarlos.", +sy_rebooting:"Reiniciando, reconecta en unos 20 s",sy_bytes:"bytes", +cw_title:"¿Escribir esta configuración de inicio?",cw_save_title:"¿Guardar la configuración en ejecución en flash?", +cw_info:"{n} / 2048 bytes, reaplicada línea a línea en cada arranque", +cw_toolarge:"Demasiado grande: el sector de configuración admite como máximo 2048 bytes. Elimina líneas primero.", +cw_unknown:"No está en la gramática de configuración conocida (aun así se escribirá): ",cw_empty:"(vacío)", +cw_writing:"Escribiendo configuración...",cw_failed:"error al escribir la configuración: HTTP {n}", +cw_verify_fail:"Verificación fallida: el contenido de la flash difiere de lo enviado. El registro de comandos NO se ha borrado.", +cw_saved:"Configuración de inicio guardada y verificada",cw_collect:"Recopilando cambios en ejecución...", +t_applied:"Aplicado: {c}",t_cmds:"{n} comando(s) aplicado(s)",t_rejected:"comando rechazado: {c}",t_failed:"fallo: {c}", +fw_title:"Actualización de firmware", +fw_intro:"Sube una imagen de RTLPlayground (512 KiB .bin). La imagen se almacena provisionalmente en flash y se comprueba con CRC; si es correcta, el switch se reinicia, vuelve a verificar la imagen y la aplica. La configuración de inicio se conserva.", +fw_upload:"Subir",fw_checking:"{f}: {n} bytes. Comprobando...",fw_size_err:"el tamaño es {n}, se esperaban 524288 (512 KiB)", +fw_magic_err:"falta la cabecera de banco / magia LJMP: no es una imagen de firmware",fw_crc_err:"comprobación CRC16 fallida: imagen corrupta", +fw_valid:"imagen de RTLPlayground válida (tamaño, magia y CRC16 correctos)",fw_q:"¿Subir firmware?", +fw_d:"Si la suma de comprobación es correcta, el switch se reinicia y aplica la imagen durante el arranque (la configuración de inicio se conserva). No lo apagues hasta que vuelva.", +fw_finishing:"terminando escritura en flash... {s} s",fw_uploading:"subiendo... {p}% / {s} s", +fw_verified:"suma de comprobación verificada, el switch se está reiniciando...", +fw_rejected:"el switch rechazó la imagen (suma de comprobación incorrecta), no se aplicó nada", +fw_lost:"error de subida: conexión perdida durante la transferencia", +fw_noreboot:"no se detectó reinicio: lo más probable es que la imagen fuera rechazada. Si actualizas desde un firmware antiguo, verifica la versión en la barra lateral tras volver a iniciar sesión.", +fw_applied:"actualización aplicada",fw_done_t:"Firmware actualizado", +fw_done:"El switch verificó la imagen y se reinició con ella. La sesión se restableció, así que se te pedirá que vuelvas a iniciar sesión.", +fw_login:"Ir al inicio de sesión",fw_rebooting:"el switch se está reiniciando...", +fw_timeout:"el switch no ha vuelto tras 150 s: comprueba la alimentación / consola serie" } }; var rtlLang=(function(){ diff --git a/html/login.html b/html/login.html index 69329e82..f4e1ee90 100644 --- a/html/login.html +++ b/html/login.html @@ -83,7 +83,8 @@ button:hover{filter:brightness(1.1)} var L={ en:["Switch Login","RTLPlayground management interface","Password","Sign in","Wrong password, try again."], ja:["スイッチ ログイン","RTLPlayground 管理インターフェース","パスワード","ログイン","パスワードが違います。もう一度お試しください。"], - zh:["交换机登录","RTLPlayground 管理界面","密码","登录","密码错误,请重试。"] + zh:["交换机登录","RTLPlayground 管理界面","密码","登录","密码错误,请重试。"], + es:["Inicio de sesión del switch","Interfaz de gestión de RTLPlayground","Contraseña","Iniciar sesión","Contraseña incorrecta, inténtalo de nuevo."] }; var l=null; try{l=localStorage.getItem("rtl_lang");}catch(e){} From f0b311fc4385556db569537042911d21c8b561d1 Mon Sep 17 00:00:00 2001 From: gnacho Date: Tue, 15 Sep 2026 00:24:45 +0200 Subject: [PATCH 04/79] httpd: make the session timeout configurable The session timeout was a compile-time constant (200 s), so changing it meant rebuilding the firmware. Make it a variable that keeps the same default and add a "session " command, so it can be set from the console or from the startup configuration like the other settings. Measured on KP_9000_9XHML_X_V3_1: ROM +180 bytes (BANK1 +17, BANK2 +161) and xdata +2 bytes; the internal RAM map is unchanged. --- README.md | 1 + cmd_parser.c | 23 +++++++++++++++++++++++ html/app.js | 3 ++- httpd/httpd.c | 8 ++++++-- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74b2ffbd..1968fb26 100644 --- a/README.md +++ b/README.md @@ -344,6 +344,7 @@ netmask zzz.zzz.zzz.zzz = Network mask of the switch port x name xxx = Name xxx the port number x port z 1g = Set 1g speed for port z igmp on/off = Turn IGMP on or off +session xxxx = Web session timeout in seconds (default 200) ``` [To be continue] diff --git a/cmd_parser.c b/cmd_parser.c index 5a3a3d60..e6decbeb 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -41,6 +41,8 @@ extern __xdata struct flash_region_t flash_region; extern __xdata char passwd[21]; +extern __xdata uint16_t session_timeout; + extern __xdata struct dhcp_state dhcp_state; __xdata uint8_t vlan_names[VLAN_NAMES_SIZE]; @@ -1593,6 +1595,25 @@ void parse_syslog(void) } } +void parse_session(void) +{ + if (cmd_words_len >= 2) { + /* atoi_short() returns zero for no digits and for a value past + * 65535; a zero-second timeout is not useful either, so both + * tests are needed. */ + if (!atoi_short(cmd_words_b[1]) || !atoi_results_short) { + print_string("Must be 1-65535 s\n"); + return; + } + session_timeout = atoi_results_short; + } + /* Also the confirmation after setting: one message for both. */ + print_string("Session timeout: "); + itoa_short(session_timeout); + print_string(" s\n"); +} + + // Parse command into words // cmd_words_len contains the number of words found. // cmd_words_b[] contains only start of a word offset. @@ -1895,6 +1916,8 @@ void cmd_parser(void) __banked } } else if (cmd_compare(0, "ingress")) { parse_ingress(); + } else if (cmd_compare(0, "session")) { + parse_session(); } else { print_string("Unknown command\n"); diff --git a/html/app.js b/html/app.js index 0a50b322..4bcb5c73 100644 --- a/html/app.js +++ b/html/app.js @@ -427,6 +427,7 @@ var 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}$/, + /^session\s+\d{1,5}$/, /^passwd\s+\S+$/,/^hostname\s+\S{1,23}$/, /^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+\d{1,2}t?)+$/, @@ -1612,7 +1613,7 @@ tabHooks.system={enter:sysLoad}; var CONF_OVERWRITE=[ /^ip\b/,/^gw\b/,/^netmask\b/,/^hostname\b/, - /^syslog\s+ip\b/,/^syslog\s+port\b/,/^passwd\b/, + /^syslog\s+ip\b/,/^syslog\s+port\b/,/^passwd\b/,/^session\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/, diff --git a/httpd/httpd.c b/httpd/httpd.c index f87de897..41a3fc65 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -12,7 +12,11 @@ #include "debug.h" #define SESSION_ID_LENGTH 12 -#define SESSION_TIMEOUT 200 +#define SESSION_TIMEOUT_DEFAULT 200 + +/* Seconds of inactivity after which a session cookie stops validating. + * Configurable with the "session" command, see cmd_parser.c. */ +__xdata uint16_t session_timeout = SESSION_TIMEOUT_DEFAULT; #define CMARK_S 6 @@ -341,7 +345,7 @@ __xdata uint8_t *scan_header(__xdata uint8_t * __xdata p) read_reg_timer(&now); if (session) { - if (now - last_session_use > SESSION_TIMEOUT) { + if (now - last_session_use > session_timeout) { dbg_string("Session expired\n"); } else { if (is_word_x(session, session_id)) { From 18b28edcbb64e52824bfa88ca129d1069631a47f Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:56:09 +0200 Subject: [PATCH 05/79] httpd: keep a continued upload away from the cmd and login handlers handle_post() takes the request path from byte 6 of the segment it is given. For the first segment of a POST that is the path, but for a later segment of a multipart upload it is whatever part of the file happens to sit there. The cmd and login handlers were matched before the multipart state was checked, so a segment of a config or firmware upload with "cmd " at that offset ran the rest of the segment as CLI commands, and one with "login " there aborted the upload with 400. Check for an upload in progress, and for new upload and config requests, before looking at cmd and login. A new POST cannot match both sets of words, so nothing changes for requests that are not uploads. --- httpd/httpd.c | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index f87de897..04b65281 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -777,7 +777,23 @@ void handle_post(void) dbg_string("Multipart request\n"); } - if (is_word(request_path, "cmd")) { + if (s->tstate == TSTATE_MULTIPART || is_word(request_path, "upload") || is_word(request_path, "config")) { + dbg_string("POST upload/config request\n"); + if (!authenticated) { + send_unauthorized(); + return; + } + if (!boundary[0]) { + dbg_string("Bad request, no boundary!\n"); + send_bad_request(); + return; + } + if (config_upload) + handle_config_fragment(p); + else + handle_firmware_fragment(p); + return; + } else if (is_word(request_path, "cmd")) { p += 4; if (!authenticated) { send_unauthorized(); @@ -803,22 +819,6 @@ void handle_post(void) return; run_login_body(p); return; - } else if (s->tstate == TSTATE_MULTIPART || is_word(request_path, "upload") || is_word(request_path, "config")) { - dbg_string("POST upload/config request\n"); - if (!authenticated) { - send_unauthorized(); - return; - } - if (!boundary[0]) { - dbg_string("Bad request, no boundary!\n"); - send_bad_request(); - return; - } - if (config_upload) - handle_config_fragment(p); - else - handle_firmware_fragment(p); - return; } else { send_not_found(); return; From 3dcab09047df69a73d3d9d88fc81166727e4ab9b Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:40:16 +0200 Subject: [PATCH 06/79] web: keep stored config lines and per-port ingress when saving Save to flash ran the stored config through the same filter as the command log, so stored lines the page does not know were dropped. The stored lines are now carried over as they are and only the log is filtered. ingress only changes the ports it lists, but the merge replaced every stored ingress line. It now merges per port; "ingress t|u|a" still replaces everything, and per-port settings an all-ports line already overrode are not revived. Write from the config editor no longer clears the command log or the unsaved marker, since the edited text does not hold the changes made on other pages; only Save, which merges them, clears the log. The command log grows from 1 kB to 2 kB, which still fits the 2500 byte output buffer, and send_cmd_log() stops at the end of that buffer. --- html/app.js | 36 ++++++++++++++++++++++++++---------- httpd/page_impl.c | 2 +- rtl837x_common.h | 2 +- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/html/app.js b/html/app.js index 0a50b322..75152fbb 100644 --- a/html/app.js +++ b/html/app.js @@ -1614,7 +1614,7 @@ var CONF_OVERWRITE=[ /^ip\b/,/^gw\b/,/^netmask\b/,/^hostname\b/, /^syslog\s+ip\b/,/^syslog\s+port\b/,/^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/, + /^pvid\s+\d{1,2}\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\s+\d\b/,/^isolate\s+\d{1,2}\b/, @@ -1635,6 +1635,21 @@ function mergeConf(base,texts){ if((m=line.match(/^lag (\d) d$/))){drop(new RegExp("^lag(hash)? "+m[1]+"( |$)"));return;} if(line==="mirror off"){drop(/^mirror /);return;} if(!isConfCmd(line))return; + if((m=line.match(/^ingress (.+)$/))){ + if(/^[tua]$/.test(m[1])){drop(/^ingress /);conf.push(line);return;} + var ports={},order=[],last=-1; + conf.forEach(function(c,i){if(/^ingress [tua]$/.test(c))last=i;}); + conf.forEach(function(c,i){ + var cm=c.match(/^ingress (.+)$/); + if(!cm||/^[tua]$/.test(cm[1])||i Date: Wed, 16 Sep 2026 18:07:49 +0200 Subject: [PATCH 07/79] web: add a session timeout field to the Services card Adds a numeric input next to the syslog settings to read and set the session timeout without knowing the command. The current value is reported in information.json, so the field also shows the default the firmware was built with. Costs 86 bytes of code (BANK1) and nothing in RAM. --- html/app.js | 21 +++++++++++++++------ html/index.html | 2 ++ httpd/page_impl.c | 5 ++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/html/app.js b/html/app.js index 4bcb5c73..e4ddb7ae 100644 --- a/html/app.js +++ b/html/app.js @@ -71,8 +71,9 @@ bw_title:"Bandwidth limits",bw_h:"Mbit/s, 0.016-10000",bw_in:"Ingress limit",bw_ bw_exceed:"When exceeded",bw_fc:"Flow control",bw_drop:"Drop", bw_in_err:"Ingress limit must be 0.016-10000 Mbit/s",bw_out_err:"Egress limit must be 0.016-10000 Mbit/s", sy_network:"Network",sy_dhcp:"Use DHCP",sy_dhcp_t:"Request address via DHCP",sy_services:"Services", -sy_igmp:"IGMP snooping",sy_sysip:"server IP",sy_server:"Server",sy_port:"Port", -sy_services_note:"Service state reflects the startup config; runtime state is not readable.", + sy_igmp:"IGMP snooping",sy_sysip:"server IP",sy_server:"Server",sy_port:"Port",sy_sesstmo:"Session timeout (s)", + sy_services_note:"Service state reflects the startup config; runtime state is not readable.", + sy_sesstmo_err:"Session timeout must be 1-65535 s", sy_password:"Admin password",sy_newpw:"New password",sy_repeat:"Repeat",sy_pwapply:"Change password", sy_pw_note:"Takes effect immediately; save to flash to keep it after reboot.",sy_console:"Console", sy_cmd:"CLI command...",sy_send:"Send",sy_startup:"Startup configuration",sy_replayed:"replayed on every boot", @@ -182,8 +183,9 @@ bw_title:"帯域制限",bw_h:"Mbit/s、0.016〜10000",bw_in:"入力制限",bw_ou bw_exceed:"超過時の動作",bw_fc:"フロー制御",bw_drop:"破棄", bw_in_err:"入力制限は 0.016〜10000 Mbit/s の範囲で指定してください",bw_out_err:"出力制限は 0.016〜10000 Mbit/s の範囲で指定してください", sy_network:"ネットワーク",sy_dhcp:"DHCP を使用",sy_dhcp_t:"DHCP でアドレスを取得",sy_services:"サービス", -sy_igmp:"IGMP スヌーピング",sy_sysip:"サーバー IP",sy_server:"サーバー",sy_port:"ポート", -sy_services_note:"サービスの状態は起動設定を反映しています。実行時の状態は読み取れません。", + sy_igmp:"IGMP スヌーピング",sy_sysip:"サーバー IP",sy_server:"サーバー",sy_port:"ポート",sy_sesstmo:"セッションタイムアウト (秒)", + sy_services_note:"サービスの状態は起動設定を反映しています。実行時の状態は読み取れません。", + sy_sesstmo_err:"セッションタイムアウトは 1〜65535 秒で指定してください", sy_password:"管理者パスワード",sy_newpw:"新しいパスワード",sy_repeat:"再入力",sy_pwapply:"パスワードを変更", sy_pw_note:"即時に反映されます。再起動後も保持するにはフラッシュに保存してください。",sy_console:"コンソール", sy_cmd:"CLI コマンド...",sy_send:"送信",sy_startup:"起動設定",sy_replayed:"起動のたびに再実行されます", @@ -293,8 +295,9 @@ bw_title:"带宽限制",bw_h:"Mbit/s,0.016-10000",bw_in:"入方向限速",bw_o bw_exceed:"超限动作",bw_fc:"流量控制",bw_drop:"丢弃", bw_in_err:"入方向限速范围为 0.016-10000 Mbit/s",bw_out_err:"出方向限速范围为 0.016-10000 Mbit/s", sy_network:"网络",sy_dhcp:"使用 DHCP",sy_dhcp_t:"通过 DHCP 获取地址",sy_services:"服务", -sy_igmp:"IGMP 侦听",sy_sysip:"服务器 IP",sy_server:"服务器",sy_port:"端口", -sy_services_note:"服务状态反映启动配置;运行时状态无法读取。", + sy_igmp:"IGMP 侦听",sy_sysip:"服务器 IP",sy_server:"服务器",sy_port:"端口",sy_sesstmo:"会话超时(秒)", + sy_services_note:"服务状态反映启动配置;运行时状态无法读取。", + sy_sesstmo_err:"会话超时必须为 1-65535 秒", sy_password:"管理员密码",sy_newpw:"新密码",sy_repeat:"重复输入",sy_pwapply:"修改密码", sy_pw_note:"立即生效;如需重启后保留请保存到 Flash。",sy_console:"控制台", sy_cmd:"CLI 命令...",sy_send:"发送",sy_startup:"启动配置",sy_replayed:"每次启动时重新执行", @@ -1503,6 +1506,7 @@ function sysLoad(){ var sl=(S.info.syslog_server||"").split(":"); if(sl[0]&&sl[0]!=="0.0.0.0")$("sy-sysip").value=sl[0]; if(sl[1])$("sy-sysport").value=sl[1]; + if(S.info.session_timeout)$("sy-sesstmo").value=S.info.session_timeout; }).catch(function(){}); cfgReload(); } @@ -1555,6 +1559,11 @@ $("sy-syslog").addEventListener("change",function(){ }else cmds.push("syslog off"); postCmds(cmds).catch(function(){}); }); +$("sy-sesstmo").addEventListener("change",function(){ + var v=+this.value; + if(!(v>=1&&v<=65535)){toast(t("sy_sesstmo_err"),"err");sysLoad();return;} + postCmd("session "+v).catch(function(){sysLoad()}); +}); $("sy-pwapply").addEventListener("click",function(){ var a=$("sy-pw1").value,b=$("sy-pw2").value; if(a.length<1||a.length>20){toast(t("sy_pw_len"),"err");return;} diff --git a/html/index.html b/html/index.html index 3e1696b1..3bd9105c 100644 --- a/html/index.html +++ b/html/index.html @@ -375,6 +375,8 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent} Port +

Service state reflects the startup config; runtime state is not readable.

Admin password

diff --git a/httpd/page_impl.c b/httpd/page_impl.c index ec7ef26a..6011f80a 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -28,6 +28,7 @@ extern __code const struct machine machine; extern __xdata uint8_t outbuf[TCP_OUTBUF_SIZE]; extern __xdata uint16_t slen; extern __xdata uint16_t management_vlan; +extern __xdata uint16_t session_timeout; extern __xdata uint16_t cont_len; extern __xdata uint32_t cont_addr; extern __code uint8_t * __code hex; @@ -229,7 +230,9 @@ void send_basic_info(void) itoa_html(syslog_state.server_ip[2]); char_to_html('.'); itoa_html(syslog_state.server_ip[3]); char_to_html(':'); itoa16_html(syslog_state.server_port); - slen += strtox(outbuf + slen, "\",\"mac_address\":\""); + slen += strtox(outbuf + slen, "\",\"session_timeout\":"); + itoa16_html(session_timeout); + slen += strtox(outbuf + slen, ",\"mac_address\":\""); byte_to_html(uip_ethaddr.addr[0]); char_to_html(':'); byte_to_html(uip_ethaddr.addr[1]); char_to_html(':'); byte_to_html(uip_ethaddr.addr[2]); char_to_html(':'); From cdc84c75e4b71988f6c7d9eaa97f29ae8cb06a1b Mon Sep 17 00:00:00 2001 From: gnacho Date: Wed, 16 Sep 2026 19:05:48 +0200 Subject: [PATCH 08/79] httpd: give the session cookie a Max-Age matching the timeout The session cookie carried neither Expires nor Max-Age, so browsers that clean up session cookies aggressively (LibreWolf, for one) may drop it while the session is still valid. Send Max-Age=session_timeout so the declared lifetime matches the timeout already enforced by the firmware. itoa16_html() only converted four digits, so extend it to five to print timeouts up to 65535. That also fixes information.json, which used the same helper and reported wrong values from 10000 s up. --- httpd/httpd.c | 2 ++ httpd/page_impl.c | 6 ++++-- httpd/page_impl.h | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index fb9c678f..c01b0650 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -668,6 +668,8 @@ static void run_login_body(__xdata uint8_t *body) "Set-Cookie: session="); for (uint8_t i = 0; i < SESSION_ID_LENGTH; i++) outbuf[slen++] = session_id[i]; + slen += strtox(outbuf + slen, "; Max-Age="); + itoa16_html(session_timeout); slen += strtox(outbuf + slen, "; SameSite=Strict\r\n\r\n"); } else { dbg_string("Password invalid!\n"); diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 6011f80a..6000fd26 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -100,12 +100,14 @@ void itoa_html(uint8_t v) char_to_html('0' + (v % 10)); } -void itoa16_html(uint16_t v) /* sufficient for VLAN IDs (max 4094) */ +void itoa16_html(uint16_t v) /* up to five digits (65535) */ { uint8_t print_zeros = 0; uint8_t d; - d = v / 1000; + d = v / 10000; if (d) { char_to_html('0' + d); print_zeros = 1; } + d = (v / 1000) % 10; + if (d || print_zeros) { char_to_html('0' + d); print_zeros = 1; } d = (v / 100) % 10; if (d || print_zeros) { char_to_html('0' + d); print_zeros = 1; } d = (v / 10) % 10; diff --git a/httpd/page_impl.h b/httpd/page_impl.h index cad4cd6f..22a9f259 100644 --- a/httpd/page_impl.h +++ b/httpd/page_impl.h @@ -5,6 +5,8 @@ extern __code uint8_t * __code HTTP_RESPONCE_TXT; +void itoa16_html(uint16_t v); + bool send_counters(uint8_t phys_port); void send_status(void); void send_vlan(uint16_t vlan); From 3a6f62b1d574915b193f4a999fe2771a22e1cb0a Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:59:37 +0200 Subject: [PATCH 09/79] config: skip a line the parser cannot buffer instead of stopping execute_config() replays the saved configuration line by line into cmd_buffer. A line that does not fit, or one cmd_tokenize() rejects, ended the replay right there: everything below it was silently left unapplied, while the file itself and the editor showing it stayed complete. On a switch here, a 130-byte line placed after the last real setting was enough to lose every line that followed it on the next boot. Report the offending line and carry on with the next one. The skip flag lives across the flash page refill, so a long line that crosses a page boundary is not resumed halfway and run as a truncated command. --- cmd_parser.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index d4c4df8b..d4fe030e 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1956,6 +1956,7 @@ void execute_config(void) __banked save_cmd = 0; uint8_t cmd_idx = 0; + uint8_t skipping = 0; do { flash_region.addr = pos; flash_region.len = FLASH_READ_BURST_SIZE; @@ -1966,20 +1967,23 @@ void execute_config(void) __banked do { if (cmd_idx >= (CMD_BUF_SIZE - 1)) { cmd_buffer[cmd_idx] = NUL; - print_string("ERROR: Command too long: "); + print_string("ERROR: Command too long, skipped: "); print_string_x(cmd_buffer); write_char('\n'); - err_status = ERR_CMD_TOO_LONG; - goto config_done; + cmd_idx = 0; + skipping = 1; } c = flash_buf[cfg_idx++]; if (c == 0 || c == '\n') { - cmd_buffer[cmd_idx] = NUL; - if (cmd_idx) { - cmd_tokenize(); - if (err_status != ERR_OK) - goto config_done; - cmd_parser(); + if (skipping) { + skipping = 0; + } else { + cmd_buffer[cmd_idx] = NUL; + if (cmd_idx) { + cmd_tokenize(); + if (err_status == ERR_OK) + cmd_parser(); + } } if (c == 0) goto config_done; @@ -1987,8 +1991,10 @@ void execute_config(void) __banked continue; } - cmd_buffer[cmd_idx] = c; - cmd_idx++; + if (!skipping) { + cmd_buffer[cmd_idx] = c; + cmd_idx++; + } } while (cfg_idx); pages_left--; From 3c8de6da87221d9bf63d12dab7abcc36a7584670 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:59:37 +0200 Subject: [PATCH 10/79] httpd: refuse a configuration with a line the replay cannot take The replay takes a line of at most 126 bytes, so a longer one is skipped on boot. Neither the upload path nor the editor looked at line length, so the file the user saved was not the configuration the switch would come up with. Reject such a body in config_take(), before the sector is erased, which the upload path already maps to 400. The editor points at the offending line and its length, and measures it in bytes rather than characters, so a name written in a non-ASCII script is counted the way the switch counts it. --- CHANGELOG.md | 6 ++++++ html/app.js | 18 +++++++++++++----- httpd/httpd.c | 11 +++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 050eacf0..2c490505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ ## Fixed +- Config + - A startup configuration line longer than the command buffer no longer stops the replay: + the offending line is skipped and the following lines are still applied. + - The web UI and the configuration upload now refuse a line the replay cannot take, + instead of writing it to flash. + ## Breaking changes - Config diff --git a/html/app.js b/html/app.js index 0a50b322..0bee2210 100644 --- a/html/app.js +++ b/html/app.js @@ -90,6 +90,7 @@ sy_rebooting:"Rebooting, reconnect in about 20 s",sy_bytes:"bytes", cw_title:"Write this startup configuration?",cw_save_title:"Save running configuration to flash?", cw_info:"{n} / 2048 bytes, replayed line by line on every boot", cw_toolarge:"Too large: the config sector accepts at most 2048 bytes. Remove lines first.", +cw_longline:"Line {n} is {m} bytes long: the switch can only replay lines up to 126 bytes and would skip it on boot, so the write is refused. Shorten it first.", cw_unknown:"Not in the known config grammar (will still be written): ",cw_empty:"(empty)", cw_writing:"Writing configuration...",cw_failed:"config write failed: HTTP {n}", cw_verify_fail:"Verification failed: flash content differs from what was sent. Command log NOT cleared.", @@ -201,6 +202,7 @@ sy_rebooting:"再起動中です。約 20 秒後に再接続してください", cw_title:"この起動設定を書き込みますか?",cw_save_title:"実行中の設定をフラッシュに保存しますか?", cw_info:"{n} / 2048 バイト、起動のたびに 1 行ずつ実行されます", cw_toolarge:"サイズ超過: 設定セクタは最大 2048 バイトです。先に行を削除してください。", +cw_longline:"{n} 行目は {m} バイトです: スイッチが起動時に実行できる行は 126 バイトまでで、この行は読み飛ばされるため、書き込みを拒否します。先に短くしてください。", cw_unknown:"既知の設定文法に含まれない行 (そのまま書き込まれます): ",cw_empty:"(空)", cw_writing:"設定を書き込み中...",cw_failed:"設定の書き込みに失敗しました: HTTP {n}", cw_verify_fail:"検証失敗: フラッシュの内容が送信内容と異なります。コマンドログは消去されていません。", @@ -312,6 +314,7 @@ sy_rebooting:"正在重启,约 20 秒后重新连接",sy_bytes:"字节", cw_title:"写入此启动配置?",cw_save_title:"将运行配置保存到 Flash?", cw_info:"{n} / 2048 字节,每次启动时逐行执行", cw_toolarge:"过大: 配置扇区最多容纳 2048 字节。请先删除部分行。", +cw_longline:"第 {n} 行有 {m} 字节: 交换机启动时只能执行不超过 126 字节的行,该行会被跳过,因此拒绝写入。请先缩短该行。", cw_unknown:"不属于已知配置语法的行 (仍会写入): ",cw_empty:"(空)", cw_writing:"正在写入配置...",cw_failed:"配置写入失败: HTTP {n}", cw_verify_fail:"校验失败: Flash 内容与发送内容不一致。命令日志未清除。", @@ -1596,11 +1599,16 @@ function cfgReload(){ cfgParseKnown(x); }).catch(function(){}); } +function cfgLongLine(v){ + var ls=v.split("\n"); + for(var i=0;i126)return{n:i+1,m:n};} + return null; +} function cfgBytes(){ - var n=new Blob([$("cfgedit").value]).size; + var v=$("cfgedit").value,n=new Blob([v]).size; var el=$("cfgbytes"); el.textContent=n+" / 2048 "+t("sy_bytes"); - el.style.color=n>2048?"var(--bad)":""; + el.style.color=(n>2048||cfgLongLine(v))?"var(--bad)":""; return n; } $("cfgedit").addEventListener("input",cfgBytes); @@ -1668,12 +1676,12 @@ function writeConfig(txt,title){ ed.value=txt.replace(/\r\n/g,"\n"); var ok=h("button",{class:"ctl pri",text:t("sy_write"),onclick:function(){closeModal();doWriteConfig(cfgText(ed.value))}}); function refresh(){ - var v=cfgText(ed.value),bytes=new Blob([v]).size; + var v=cfgText(ed.value),bytes=new Blob([v]).size,long=cfgLongLine(v); var unknown=v.split("\n").filter(function(l){return l.trim()&&!isConfCmd(l.trim().replace(/\s+/g," "))}); info.textContent=t("cw_info",{n:bytes}); - ok.disabled=bytes>2048; + ok.disabled=bytes>2048||!!long; warn.style.color=ok.disabled?"var(--bad)":"var(--warn)"; - warn.textContent=ok.disabled?t("cw_toolarge"):(unknown.length?t("cw_unknown")+unknown.join(" | "):""); + warn.textContent=bytes>2048?t("cw_toolarge"):long?t("cw_longline",long):(unknown.length?t("cw_unknown")+unknown.join(" | "):""); } ed.addEventListener("input",refresh);refresh(); modal(title,h("div",null,[info,warn,ed]),[h("button",{class:"ctl",text:t("c_cancel"),onclick:closeModal}),ok]); diff --git a/httpd/httpd.c b/httpd/httpd.c index 74c11148..bc7f0ca8 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -375,8 +375,8 @@ void gen_random_hex_chars(__xdata uint8_t * b, __xdata uint8_t bytes) static uint8_t config_take(void) { // #386: needs static, otherwise it still lands in SRAM/DSEG - static __xdata uint16_t cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last; - __xdata uint8_t cfg_bl; + static __xdata uint16_t cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last, cfg_i; + __xdata uint8_t cfg_bl, cfg_run; cfg_bl = strlen_x(boundary); @@ -418,6 +418,13 @@ static uint8_t config_take(void) // the payload plus its terminator must fit the sector if (cfg_end - cfg_body + 1 > CONFIG_LEN) return 2; + cfg_run = 0; + for (cfg_i = cfg_body; cfg_i < cfg_end; cfg_i++) { + if (config_buf[cfg_i] == '\n') + cfg_run = 0; + else if (++cfg_run >= CMD_BUF_SIZE - 1) + return 2; + } config_buf[cfg_end] = 0; flash_region.addr = CONFIG_START; flash_sector_erase(); From 48f28ae7ff9dde93ba0032339763036df0ab0832 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:28:13 +0200 Subject: [PATCH 11/79] temp: read the on-chip thermal sensor The chip has a pair of thermal meters and nothing was reading them. This adds a `temp` command that prints the current reading next to the one taken at power-on. The result register at 0x340 splits a 19 bit value across two fields: bits 18:3 live in the low 16 bits, bits 2:0 in bits 18:16, with the sign at bit 18, degrees in 17:10 and a fraction below. The nice part of that layout is that the degrees fall out of a shift of the low half, so the conversion stays 16 bit. The meter comes out of reset latched: the register keeps whatever was measured when the board powered up and then never moves. Four samples 40 s apart came back identical down to the raw ADC, and 0x340 matched the power-on register at 0x350. Clearing bit 2 of the control register at 0x338 during init is what makes it live. Measured on an SWTGW218AS: 58.7 to 59.5 C while running, with the power-on register still showing the 49.2 C it started from. For what it is worth, the scale lines up three ways: the threshold register reads -40 and +125, the slope from warm-up works out at 4450 LSB/C, and the slope between the two meters at 4388 LSB/C. We only read the meter that is up after boot. The other sits in reset with no coefficients, so it would just repeat its power-on value. Costs 231 bytes in BANK2. --- cmd_parser.c | 28 ++++++++++++++++++++++++++++ rtl837x_init.c | 12 ++++++++++++ rtl837x_regs.h | 7 +++++++ 3 files changed, 47 insertions(+) diff --git a/cmd_parser.c b/cmd_parser.c index d4c4df8b..2677fb5a 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1157,6 +1157,32 @@ err: } +static void print_temperature(uint16_t reg) +{ + int16_t v; + + reg_read(reg); + v = SFR_DATA_U16; + if (v < 0) { + write_char('-'); + v = -v; + } + itoa(v >> 7); + write_char('.'); + itoa(((v & 0x7f) * 10) >> 7); + print_string(" C\n"); +} + + +void parse_temp(void) +{ + print_string("\nChip temperature: "); + print_temperature(RTL837X_TM_RESULT); + print_string("At power-on: "); + print_temperature(RTL837X_TM_RESULT_POWERON); +} + + void parse_sdsget(void) { __xdata uint8_t sds_id, page, reg, hex_size; @@ -1883,6 +1909,8 @@ void cmd_parser(void) __banked parse_eee(); } else if (cmd_compare(0, "bw")) { parse_bw(); + } else if (cmd_compare(0, "temp")) { + parse_temp(); } else if (cmd_compare(0, "version")) { print_sw_version(); } else if (cmd_compare(0, "time")) { diff --git a/rtl837x_init.c b/rtl837x_init.c index 6b24843b..5cb9df1a 100644 --- a/rtl837x_init.c +++ b/rtl837x_init.c @@ -92,8 +92,18 @@ void static sds_init(void) } +void static tm_init(void) +{ + reg_read_m(RTL837X_TM_CTRL2); + sfr_mask_data(0, TM_CTRL2_EN_LATCH, 0); + reg_write_m(RTL837X_TM_CTRL2); +} + + void rtl8373_init(void) __banked { + tm_init(); + print_string("\nrtl8373_init called\n"); // r65d8:3ffbedff R65d8-3ffbedff @@ -191,6 +201,8 @@ void rtl8373_init(void) __banked void rtl8372_init(void) __banked { + tm_init(); + print_string("\nrtl8372_init called\n"); sds_init(); diff --git a/rtl837x_regs.h b/rtl837x_regs.h index 37a7e99d..ba2d7eb1 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -9,6 +9,13 @@ #define RESET_SOC_BIT 0 #define RESET_NIC_BIT 2 +#define RTL837X_TM_CTRL2 0x0338 +#define TM_CTRL2_EN_LATCH 0x04 +#define RTL837X_TM_RESULT 0x0340 +#define RTL837X_TM_RESULT_POWERON 0x0350 +// Temperature: 19 bit value, bits 18:3 in the low 16 bits of the register, +// bits 2:0 in bits 18:16. Bit 18 signed, bits 17:10 degrees C, bits 9:0 fraction. + #define RTL837X_REG_HW_CONF 0x6040 // Bits 4 & 5: CLOCK DIVIDER from 125MHz for Timer From 52b1d1a4eaf98e92c17988a3f88f8d6d1bef18cb Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:28:13 +0200 Subject: [PATCH 12/79] web: show the chip temperature in the System page The row sits between Hardware and Flash. The firmware sends the formatted string rather than the raw register, since the System table prints its values as they arrive. Costs 186 bytes in BANK1. --- html/app.js | 8 ++++---- httpd/page_impl.c | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/html/app.js b/html/app.js index 0a50b322..a58bdec4 100644 --- a/html/app.js +++ b/html/app.js @@ -15,7 +15,7 @@ c_full:"full",c_half:"half",c_devices:"devices",c_yes:"yes",c_no:"no", d_ports:"Ports",d_ports_h:"click a port for details",d_system:"System",d_traffic:"Traffic", d_traffic_h:"packets/s, live",d_txpps:"TX pps",d_rxpps:"RX pps",d_txbad:"TX bad",d_rxbad:"RX bad", i_host:"Hostname",i_ip:"IP address",i_mask:"Netmask",i_gw:"Gateway",i_mac:"MAC",i_fw:"Firmware", -i_built:"Built",i_hw:"Hardware",i_flash:"Flash",i_syslog:"Syslog", +i_built:"Built",i_hw:"Hardware",i_temp:"Temperature",i_flash:"Flash",i_syslog:"Syslog", p_state:"State",p_disabled:"disabled",p_up:"up",p_txgb:"TX good / bad",p_rxgb:"RX good / bad", p_pkts:"pkts",p_module:"Module",p_temp:"Temperature",p_vcc:"Vcc",p_txbias:"TX bias",p_txpower:"TX power", p_rxpower:"RX power",p_txfault:"TX fault",p_txdis:"TX disabled",p_rxlos:"RX LOS",p_adv:"Advertising", @@ -126,7 +126,7 @@ c_full:"全二重",c_half:"半二重",c_devices:"デバイス",c_yes:"はい",c_ d_ports:"ポート",d_ports_h:"ポートをクリックすると詳細を表示",d_system:"システム",d_traffic:"トラフィック", d_traffic_h:"パケット/秒、リアルタイム",d_txpps:"TX pps",d_rxpps:"RX pps",d_txbad:"TX 異常",d_rxbad:"RX 異常", i_host:"ホスト名",i_ip:"IP アドレス",i_mask:"ネットマスク",i_gw:"ゲートウェイ",i_mac:"MAC",i_fw:"ファームウェア", -i_built:"ビルド日",i_hw:"ハードウェア",i_flash:"フラッシュ",i_syslog:"Syslog", +i_built:"ビルド日",i_hw:"ハードウェア",i_temp:"温度",i_flash:"フラッシュ",i_syslog:"Syslog", p_state:"状態",p_disabled:"無効",p_up:"アップ",p_txgb:"TX 正常 / 異常",p_rxgb:"RX 正常 / 異常", p_pkts:"pkts",p_module:"モジュール",p_temp:"温度",p_vcc:"電圧",p_txbias:"TX バイアス",p_txpower:"TX 電力", p_rxpower:"RX 電力",p_txfault:"TX 障害",p_txdis:"TX 無効",p_rxlos:"RX 信号ロス",p_adv:"アドバタイズ", @@ -237,7 +237,7 @@ c_full:"全双工",c_half:"半双工",c_devices:"台设备",c_yes:"是",c_no:" d_ports:"端口",d_ports_h:"点击端口查看详情",d_system:"系统",d_traffic:"流量", d_traffic_h:"包/秒,实时",d_txpps:"TX pps",d_rxpps:"RX pps",d_txbad:"TX 错误",d_rxbad:"RX 错误", i_host:"主机名",i_ip:"IP 地址",i_mask:"子网掩码",i_gw:"网关",i_mac:"MAC",i_fw:"固件", -i_built:"构建日期",i_hw:"硬件",i_flash:"Flash",i_syslog:"Syslog", +i_built:"构建日期",i_hw:"硬件",i_temp:"温度",i_flash:"Flash",i_syslog:"Syslog", p_state:"状态",p_disabled:"已禁用",p_up:"已连接",p_txgb:"TX 正常 / 错误",p_rxgb:"RX 正常 / 错误", p_pkts:"个包",p_module:"模块",p_temp:"温度",p_vcc:"供电电压",p_txbias:"TX 偏置电流",p_txpower:"TX 光功率", p_rxpower:"RX 光功率",p_txfault:"TX 故障",p_txdis:"TX 禁用",p_rxlos:"RX 信号丢失",p_adv:"通告能力", @@ -680,7 +680,7 @@ function portDetail(i){ function renderInfo(){ var m=[["i_host","hostname"],["i_ip","ip_address"],["i_mask","ip_netmask"], ["i_gw","ip_gateway"],["i_mac","mac_address"],["i_fw","sw_ver"],["i_built","build_date"], - ["i_hw","hw_ver"],["i_flash","flash_size"],["i_syslog","syslog_server"]]; + ["i_hw","hw_ver"],["i_temp","chip_temp"],["i_flash","flash_size"],["i_syslog","syslog_server"]]; var tb=$("sysinfo");tb.innerHTML=""; m.forEach(function(r){ var v=S.info[r[1]]; diff --git a/httpd/page_impl.c b/httpd/page_impl.c index cb11075c..148ccacb 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -117,6 +117,23 @@ void string_to_html(__code char *s) while (*s) char_to_html(*s++); } +static void temp_to_html(void) +{ + int16_t v; + + reg_read(RTL837X_TM_RESULT); + v = SFR_DATA_U16; + if (v < 0) { + char_to_html('-'); + v = -v; + } + itoa_html(v >> 7); + char_to_html('.'); + itoa_html(((v & 0x7f) * 10) >> 7); + char_to_html(' '); + char_to_html('C'); +} + uint16_t stat_content(void) { dbg_string("stat_content called\n"); @@ -262,6 +279,8 @@ void send_basic_info(void) slen += strtox(outbuf + slen, BUILD_DATE); slen += strtox(outbuf + slen, "\",\"hw_ver\":\""); slen += strtox(outbuf + slen, machine.machine_name); + slen += strtox(outbuf + slen, "\",\"chip_temp\":\""); + temp_to_html(); slen += strtox(outbuf + slen, "\",\"flash_size\":\""); string_to_html(get_flash_size_str()); From 0631c813e27b6ad9580fdf8f821b3c3418287a60 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 18 Sep 2026 13:56:53 -0500 Subject: [PATCH 13/79] cmd: parse the port of an eee command from the start of the word parse_eee() sniffs word 2 for a trailing 'g' or 'm' to tell a speed from a port number, and advances idx past the digits to do so. The port branch then hands that advanced idx to cmd_parse_port_separator(), which expects the first character of the number and finds the separator instead, so it returns 0 and the command is rejected. Every other caller passes cmd_words_b[]. The result is that no eee command carrying a port number works, in any argument order, and the stored per-port lines a saved configuration replays are silently dropped. Reported by tofurky in issue #459, along with this fix. --- cmd_parser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd_parser.c b/cmd_parser.c index d4c4df8b..33d4c6c3 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1419,7 +1419,7 @@ void parse_eee(void) speed_word = 2; } else if (cmd_is_space_or_nul(idx)) { // Word 2 is a port number - if (cmd_parse_port_separator(idx) == 0) { + if (cmd_parse_port_separator(cmd_words_b[2]) == 0) { cmd_error("Speed word invalid, use: [100m|1g|2g5]\n"); return; } From 0a65536a7ffcc97f83bcba6b20b2be6f0acc4e5f Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 18 Sep 2026 13:57:07 -0500 Subject: [PATCH 14/79] web: send an eee command in the documented argument order The EEE toggle posted `eee `, the reverse of the `eee [on|off|status] [port] [100m|1g|2g5]` the parser and the documentation describe, so the switch answered with its usage line and the checkbox sprang back. The multi-page UI this replaced sent `eee on` with no port at all, which toggled every port instead of one. Moving the port to the end breaks the stored-configuration merge, which builds its drop key as a line prefix: with the port trailing, a per-port line matches the bare eee entry and replaces every eee line in the configuration rather than the one for that port. Give the command its own case beside the bandwidth one, so a line with a port replaces only that port and a line without one clears them all, and accept the optional speed word the command has always taken. Reported by tofurky in issue #459. --- html/app.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/html/app.js b/html/app.js index 0a50b322..497a2ce9 100644 --- a/html/app.js +++ b/html/app.js @@ -434,7 +434,7 @@ var CONF_CMDS=[ /^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)$/, + /^eee\s+(on|off)(\s+\d{1,2})?(\s+(100m|1g|2g5))?$/, /^mirror(\s+\d{1,2})(\s+\d{1,2}[tr]?)+$/,/^mirror\s+off$/, /^lag\s+[1-4](\s+\d{1,2})+$/,/^lag\s+[1-4]\s+d$/,/^laghash\s+[1-4](\s+\w+)+$/, /^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/, @@ -1424,7 +1424,7 @@ function eeeLoad(){ var on=parseInt(p.eee,2)!==0; var sw=h("label",{class:"switch"},[ h("input",{type:"checkbox",onchange:function(){ - postCmd("eee "+p.portNum+" "+(this.checked?"on":"off")) + postCmd("eee "+(this.checked?"on":"off")+" "+p.portNum) .then(function(){setTimeout(eeeLoad,300)}).catch(function(){}); }}),h("i")]); sw.firstChild.checked=on; @@ -1616,7 +1616,7 @@ var CONF_OVERWRITE=[ /^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/, + /^mirror\b/, /^lag\s+\d\b/,/^laghash\s+\d\b/,/^isolate\s+\d{1,2}\b/, /^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/, /^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+(edge|cost|prio|guard|filter|p2p)\b/, @@ -1633,6 +1633,11 @@ function mergeConf(base,texts){ var m; if((m=line.match(/^vlan (\d{1,4}) d$/))){drop(new RegExp("^vlan "+m[1]+"( |$)"));return;} if((m=line.match(/^lag (\d) d$/))){drop(new RegExp("^lag(hash)? "+m[1]+"( |$)"));return;} + if((m=line.match(/^eee (on|off)(?: (\d{1,2}))?(?: (?:100m|1g|2g5))?$/))){ + if(m[2])drop(new RegExp("^eee (on|off) "+m[2]+"\\b")); + else drop(/^eee /); + conf.push(line);return; + } if(line==="mirror off"){drop(/^mirror /);return;} if(!isConfCmd(line))return; if((m=line.match(/^bw (in|out) (\d{1,2}) (\S+)$/))){ From d22ec04b5bbb4384eb38d959ec2833f0f062551b Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 18 Sep 2026 13:59:50 -0500 Subject: [PATCH 15/79] leds: index the second LED register from the set it belongs to leds_setup() walks all four LED sets and writes the high-speed nibble of each into one of two registers, LED3_0_SET1 for sets 0 and 1 and LED3_0_SET3 for sets 2 and 3. The second branch reuses the index expression of the first without rebasing it, so sets 2 and 3 address sfr_data[-1], [-2], [-3] and [-4]. Two things follow. Sets 2 and 3 never receive their nibble, because the read-modify-write pair writes back exactly what it read. And four bytes below sfr_data are overwritten on every boot of every machine: the loop runs over all four sets whatever the board declares. On this build the linker places sfr_data directly after the serial receive ring, so the writes land in the tail of that ring. leds_dump() reads the same two registers back and already rebases with (set - 2); use the same form here. --- rtl837x_leds.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtl837x_leds.c b/rtl837x_leds.c index e4dc5a8d..d0aa498b 100644 --- a/rtl837x_leds.c +++ b/rtl837x_leds.c @@ -275,7 +275,7 @@ void leds_setup(void) __banked reg_write_m(RTL837X_REG_LED3_0_SET1); } else { reg_read_m(RTL837X_REG_LED3_0_SET3); - sfr_data[3 - (set << 1)] = (*(lptr + 6) << 4) | (*(lptr + 2)); + sfr_data[3 - ((set - 2) << 1)] = (*(lptr + 6) << 4) | (*(lptr + 2)); reg_write_m(RTL837X_REG_LED3_0_SET3); } lptr += 8; @@ -291,7 +291,7 @@ void leds_setup(void) __banked reg_write_m(RTL837X_REG_LED3_0_SET1); } else { reg_read_m(RTL837X_REG_LED3_0_SET3); - sfr_data[2 - (set << 1)] = (*(lptr + 6) << 4) | (*(lptr + 2)); + sfr_data[2 - ((set - 2) << 1)] = (*(lptr + 6) << 4) | (*(lptr + 2)); reg_write_m(RTL837X_REG_LED3_0_SET3); } lptr += 8; From 9e3863cc4e0e4e6908ad699ce5ee763fb270a96b Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 18 Sep 2026 13:59:59 -0500 Subject: [PATCH 16/79] syslog: do not read the connection before there is one udp_callbacks() offers every UDP connection event to syslog_callback(), which decides whether the event is its own by comparing the local port against state.syslog_conn->lport. The pointer is null until syslog is started: syslog_init() clears it at boot and syslog_stop() clears it again, so with syslog off, which is the default, every poll dereferences null and reads whatever lives at the bottom of xdata. Should that word ever match the port being offered, the send path runs on another application's connection, overwrites uip_appdata and drops the log line it was holding. Test the pointer first. --- syslog.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syslog.c b/syslog.c index de11af53..eed060b9 100644 --- a/syslog.c +++ b/syslog.c @@ -60,7 +60,7 @@ void syslog_stop(void) __banked void syslog_callback(uint16_t lport) __banked { uint16_t syslog_hdr; - if (lport != state.syslog_conn->lport) + if (!state.syslog_conn || lport != state.syslog_conn->lport) return; if ((state.readptr != state.writeptr) && state.line_available) From a71c5359a52d30567fba342a873b2ae972d30127 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 18 Sep 2026 14:00:51 -0500 Subject: [PATCH 17/79] httpd: refuse an unauthenticated firmware upload before it changes state The multipart branch for /config tests authenticated before it touches anything; the branch for /upload does not, and the 401 for it is only raised further down at the dispatch. By then the request has already set config_upload, uptr, verify_crc, max_upload and pre_acc, and announced a firmware upload on the console. Those five are globals while the transfer state they steer is per connection, and the stack is built for ten connections, so a request that is about to be rejected can still retarget an upload another client is in the middle of: a configuration upload continues as a firmware one and its remaining parts are written to the staging area instead of the configuration sector. Apply the same test the neighbouring branch uses, before the assignments. --- httpd/httpd.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/httpd/httpd.c b/httpd/httpd.c index 74c11148..761d6863 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -750,6 +750,10 @@ void handle_post(void) return; } if (is_word(request_path, "upload")) { + if (!authenticated) { + send_unauthorized(); + return; + } if (flash_size < FIRMWARE_UPLOAD_START*2) { print_string("Flash too small for firmware upload!\n"); From edc469fe2957212d8ff28cc13c4be53bdd1998ed Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 18 Sep 2026 14:01:03 -0500 Subject: [PATCH 18/79] httpd: make the firmware upload length limit count max_upload was armed once when the upload began and never reduced, so `max_upload - uip_len > 0` was an unsigned expression that only went false when a segment arrived whose length matched the limit exactly. The write pointer climbed from the staging address with nothing to stop it, while flash_write_bytes() carries the address in three bytes and drops whatever does not fit. Subtract each segment as it is taken and compare before subtracting, which is what the comment there described. The limit was only ever armed for firmware uploads; configuration uploads keep the bound their own buffer already imposes. --- httpd/httpd.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index 761d6863..cfcf58e5 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -902,8 +902,9 @@ void httpd_appcall(void) reset_chip(); } } else if (uip_newdata() && s->tstate == TSTATE_POST) { - // Check here maxupload by subtracting uip_len and close socekt if fails! - if (max_upload - uip_len > 0) { + if (config_upload || uip_len <= max_upload) { + if (!config_upload) + max_upload -= uip_len; upload_settings.p = uip_appdata; upload_settings.bptr = 0; upload_settings.plen = uip_len; From fd058ea74dfa2af470cba040305ca9bb1e1b5d5c Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 18 Sep 2026 14:59:47 -0500 Subject: [PATCH 19/79] port: name the port an eee command acted on the way the user wrote it port_eee_enable() and port_eee_disable() report the port with print_byte(), which prints the internal index in hex, so turning EEE off on port 5 answers "EEE off for 04". port_eee_status() in the same file already uses print_phys_port(), and the status listing it produces counts from one, so the two halves of the same command disagreed. Use print_phys_port() in all three messages. --- rtl837x_port.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rtl837x_port.c b/rtl837x_port.c index 8ee105c6..0383aae3 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -590,12 +590,12 @@ void port_eee_enable(__xdata uint8_t port,__xdata uint8_t speed) __banked if (machine.is_sfp[port]) { - print_string("EEE can't be enabled for SFP port "); print_byte(port); print_string("\n"); + print_string("EEE can't be enabled for SFP port "); print_phys_port(port); print_string("\n"); return; } REG_SET(RTL837X_EEE_CTRL_BASE + (port << 8), EEE_RX_ENABLE | EEE_TX_ENABLE); - print_string("EEE on for "); print_byte(port); print_string(" speed "); + print_string("EEE on for "); print_phys_port(port); print_string(" speed "); // Enable all speeds up to the specified speed if (speed & EEE_100) { print_string("100m\n"); @@ -654,7 +654,7 @@ void port_eee_disable(uint8_t port) __banked if (machine.is_sfp[port]) return; - print_string("EEE off for "); print_byte(port); write_char('\n'); + print_string("EEE off for "); print_phys_port(port); write_char('\n'); REG_SET(RTL837X_EEE_CTRL_BASE + (port << 8), 0); // Disable EEE advertisement for 100/1000BASE-T via EEE Advertisement Reg phy_write(port, PHY_MMD_AN, PHY_EEE_ADV, 0); From 9984649177c848811ed3379a8be86458f5400ec8 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sun, 20 Sep 2026 00:04:35 +0200 Subject: [PATCH 20/79] uip: verify TCP and UDP checksums using the RX descriptor flag The TODO next to the disabled TCP checksum code asks which bit of the RX tag carries the result. It is bit 2 of byte 1: the hardware sets it when the L4 checksum is bad. That answers the question for UDP as well, which was not being checked either because UIP_CONF_UDP_CHECKSUMS is 0. Both paths now read the flag instead of running a checksum over the payload on the 8051. A zero UDP checksum, which is optional in IPv4, is not flagged by the hardware, so DHCP keeps working and no extra condition is needed. rx_headers lives in rtlplayground.c, which the host tests do not link, so test_httpd_tx carries the storage itself the way it already carries uip_buf. A zeroed descriptor reads as a good checksum, so the existing scenarios are unaffected. A new scenario sets the flag on a request and expects it counted as a checksum error and left unanswered, then sends the same request with the flag clear and expects it served. Built against uip.c without this change, those first two checks fail and the two control checks still pass, so the scenario tells the two versions apart rather than merely exercising the code. Costs 156 bytes of code on SWTGW218AS and no extra XDATA. --- rtl837x_regs.h | 3 +++ test/test_httpd_tx.c | 35 +++++++++++++++++++++++++++++++++++ uip/uip.c | 24 +++++++++++++++--------- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/rtl837x_regs.h b/rtl837x_regs.h index 37a7e99d..6154221b 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -144,6 +144,9 @@ #define RTL837X_REG_CPU_TAG_AWARE_PMASK 0x603C #define RTL837X_REG_MAC_FORCE_MODE 0x6344 +/* Flags in byte 1 of the RX frame descriptor */ +#define RX_TAG_L4_CSUM_BAD 0x04 + /* * Statistics related registers */ diff --git a/test/test_httpd_tx.c b/test/test_httpd_tx.c index 0b2d7a23..3a82b343 100644 --- a/test/test_httpd_tx.c +++ b/test/test_httpd_tx.c @@ -26,6 +26,7 @@ #include "httpd.h" #include "uip.h" #include "rtl837x_common.h" +#include "rtl837x_regs.h" #include "rtl837x_flash.h" #include "page_impl.h" #include "html_data.h" @@ -71,6 +72,7 @@ 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]; +uint8_t rx_headers[16]; struct flash_region_t flash_region; char *mime_strings[] = { "text/html", "image/svg+xml", "image/x-icon", @@ -455,6 +457,38 @@ static void scenario_rexmit_after_shrink(void) CHECK(same, "retransmission: the repeat carries the same bytes"); } +static void scenario_bad_l4_checksum(void) +{ + uip_stats_t chkerr_before; + uint32_t seq_before; + int len_before; + + session_start(MSS_FULL); + + chkerr_before = uip_stat.tcp.chkerr; + len_before = stream_len; + seq_before = cli_seq; + + rx_headers[1] = RX_TAG_L4_CSUM_BAD; + request_file(MSS_FULL); + rx_headers[1] = 0; + cli_seq = seq_before; + + CHECK(uip_stat.tcp.chkerr == chkerr_before + 1, + "bad checksum: the segment is counted as a checksum error"); + CHECK(stream_len == len_before, + "bad checksum: the request draws no reply"); + + request_file(MSS_FULL); + drain(MSS_FULL); + report("bad checksum"); + + CHECK(body_offset() >= 0, + "bad checksum control: the same request with a good flag is served"); + CHECK(first_body_mismatch() == -1, + "bad checksum control: the file that follows is intact"); +} + int main(int argc, char **argv) { if (argc > 1 && !strcmp(argv[1], "-v")) @@ -465,6 +499,7 @@ int main(int argc, char **argv) scenario_shrinking_window(); scenario_growing_window(); scenario_rexmit_after_shrink(); + scenario_bad_l4_checksum(); printf("\n%s (%d failure%s)\n", failures ? "BENCH: FAILURES" : "BENCH: ALL PASS", diff --git a/uip/uip.c b/uip/uip.c index de822aa8..774067da 100644 --- a/uip/uip.c +++ b/uip/uip.c @@ -95,6 +95,9 @@ #endif /* UIP_CONF_IPV6 */ #include "../rtl837x_common.h" +#include "../rtl837x_regs.h" + +extern __xdata uint8_t rx_headers[16]; /*---------------------------------------------------------------------------*/ /* Variable definitions. */ @@ -1120,6 +1123,12 @@ uip_process(u8_t flag) __banked goto drop; } #else /* UIP_UDP_CHECKSUMS */ + if (rx_headers[1] & RX_TAG_L4_CSUM_BAD) { + UIP_STAT(++uip_stat.udp.drop); + UIP_STAT(++uip_stat.udp.chkerr); + UIP_LOG("udp: bad checksum."); + goto drop; + } uip_len = uip_len - UIP_IPUDPH_LEN; #endif /* UIP_UDP_CHECKSUMS */ @@ -1200,15 +1209,12 @@ uip_process(u8_t flag) __banked /* Start of TCP input header processing code. */ -// BUG: We need to reimplement this with RTL837x TCP checksum checking -// i.e. figure out which bit in the RX-tag states the checksum is correct -// if(uip_tcpchksum() != 0xffff) { /* Compute and check the TCP -// checksum. */ -// UIP_STAT(++uip_stat.tcp.drop); -// UIP_STAT(++uip_stat.tcp.chkerr); -// UIP_LOG("tcp: bad checksum."); -// goto drop; -// } + if (rx_headers[1] & RX_TAG_L4_CSUM_BAD) { + UIP_STAT(++uip_stat.tcp.drop); + UIP_STAT(++uip_stat.tcp.chkerr); + UIP_LOG("tcp: bad checksum."); + goto drop; + } /* Demultiplex this segment. */ From f65d14f4527ea63f39e8da25bf88bf25581696c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20van=20Dorst?= Date: Sat, 12 Sep 2026 16:22:08 +0200 Subject: [PATCH 21/79] I2CBUS: Calculate I2C-settings as compiletime instead at runtime. We can reduce the i2c-settings to just one byte, and this new const value can directly be used by the I2C-peripheral without additional computation. This saves 1-xdata load and a lot of bit-shifting. I see a codesize savings of +200 bytes. --- boot.c | 5 +-- machine.c | 95 ++++++++++++++++++++++++++++---------------------- machine.h | 9 +---- rtl837x_pins.c | 35 +------------------ rtl837x_pins.h | 6 ---- 5 files changed, 59 insertions(+), 91 deletions(-) diff --git a/boot.c b/boot.c index c72e24ed..4c188ad8 100644 --- a/boot.c +++ b/boot.c @@ -397,8 +397,9 @@ void setup_i2c(void) __banked // HW Control register, enable I2C depending on PIN configuration reg_read_m(RTL837X_PIN_MUX_1); for (uint8_t sfp = 0; sfp < machine.n_sfp; sfp++) { - const uint8_t scl_bus = i2c_bus_from_scl_pin(machine.sfp_port[sfp].i2c.scl); - const uint8_t sda_bus = i2c_bus_from_sda_pin(machine.sfp_port[sfp].i2c.sda); + uint8_t i2c = machine.sfp_port[sfp].i2c; + uint8_t scl_bus = (i2c >> 5) & 0x03; + uint8_t sda_bus = (i2c >> 2) & 0x07; print_string("Configuring I2C for SFP idx="); print_byte(sfp); print_string(" SCL="); print_byte(scl_bus); print_string(", SDA="); print_byte(sda_bus); write_char('\n'); switch (scl_bus) { case 3: diff --git a/machine.c b/machine.c index 972df428..a4f8e192 100644 --- a/machine.c +++ b/machine.c @@ -5,6 +5,23 @@ #include "rtl837x_regs.h" #include "rtl837x_common.h" +#define I2CBUS(sda, scl) \ + ((uint8_t)((I2C_BUS_FROM_SCL_PIN(scl) << 5) | \ + (I2C_BUS_FROM_SDA_PIN(sda) << 2))) + +#define I2C_BUS_FROM_SDA_PIN(sda_pin) \ + ((sda_pin) == GPIO47_I2C_SDA0 ? (uint8_t)0 : \ + (sda_pin) == GPIO49_I2C_SDA1 ? (uint8_t)1 : \ + (sda_pin) == GPIO51_I2C_SDA2_UART1_RX ? (uint8_t)2 : \ + (sda_pin) == GPIO41_I2C_SDA3_MDIO1 ? (uint8_t)3 : \ + (sda_pin) == GPIO39_I2C_SDA4 ? (uint8_t)4 : -1) + +#define I2C_BUS_FROM_SCL_PIN(scl_pin) \ + ((scl_pin) == GPIO46_I2C_SCL0 ? 0 : \ + (scl_pin) == GPIO48_I2C_SCL1 ? 1 : \ + (scl_pin) == GPIO50_I2C_SCL2_UART1_TX ? 2 : \ + (scl_pin) == GPIO40_I2C_SCL3_MDC1 ? 3 : -1) + #if defined(MACHINE_KP_9000_6XHML_X2) || \ defined(MACHINE_KP_9000_6XH_X2_V1_1) || \ defined(MACHINE_KP_9000_6XHML_X2_V1_1) || \ @@ -34,13 +51,13 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO10_LED10, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 0, - .sfp_port[0].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), // Right SFP port (6) .sfp_port[1].pin_detect = GPIO30_ACL_BIT3_EN, .sfp_port[1].pin_los = GPIO37, .sfp_port[1].pin_tx_disable = GPIO_NA, .sfp_port[1].sds = 1, - .sfp_port[1].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO54_ACL_BIT2_EN, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 0}, @@ -70,7 +87,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, /* Conditions for LED on: * dual led orange: ledset_0 & ledset_2 @@ -105,13 +122,13 @@ __code const struct machine machine = { .sfp_port[0].pin_detect = GPIO38, .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), // Right SFP port .sfp_port[1].pin_detect = GPIO37, .sfp_port[1].pin_los = GPIO_NA, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO48_I2C_SCL1, // Button-Switch is unpopulated on PCB, but can be added manually (hole in case is already there) .high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_27 | LED_28_SYS | LED_29 }, @@ -152,7 +169,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 0}, @@ -177,7 +194,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO54_ACL_BIT2_EN, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1 }, @@ -227,7 +244,7 @@ __code const struct machine machine = { .sfp_port[0].pin_detect = GPIO38, .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO48_I2C_SCL1, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, @@ -265,13 +282,13 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, /* GPIO 39 */ + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), /* GPIO 39 */ // Right SFP port (J2) .sfp_port[1].pin_detect = GPIO50_I2C_SCL2_UART1_TX, .sfp_port[1].pin_los = GPIO_NA, .sfp_port[1].pin_tx_disable = GPIO_NA, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, /* GPIO 40 */ + .sfp_port[1].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), /* GPIO 40 */ .reset_pin = GPIO54_ACL_BIT2_EN, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1}, @@ -306,13 +323,13 @@ __code const struct machine machine = { .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 }, /* GPIO 39 */ + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), /* GPIO 39 */ // Right SFP port (J2) .sfp_port[1].pin_detect = GPIO50_I2C_SCL2_UART1_TX, .sfp_port[1].pin_los = GPIO51_I2C_SDA2_UART1_RX, .sfp_port[1].pin_tx_disable = GPIO_NA, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, /* GPIO 40 */ + .sfp_port[1].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), /* GPIO 40 */ .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1}, @@ -346,7 +363,7 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO48_I2C_SCL1, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, @@ -385,12 +402,12 @@ __code const struct machine machine = { .sfp_port[0].pin_los = 10, .sfp_port[0].pin_tx_disable = 0xFF, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), .sfp_port[1].pin_detect = 30, .sfp_port[1].pin_los = 51, .sfp_port[1].pin_tx_disable = 0xFF, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 , .enable = LED_27 | LED_29 }, .port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1}, @@ -425,7 +442,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO54_ACL_BIT2_EN, .high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, @@ -458,7 +475,7 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO_NA, // no LOS pin wired .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO54_ACL_BIT2_EN, .high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, @@ -499,7 +516,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO54_ACL_BIT2_EN, .high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, @@ -531,7 +548,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 0}, @@ -556,12 +573,12 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO50_I2C_SCL2_UART1_TX, .sfp_port[0].pin_tx_disable = GPIO54_ACL_BIT2_EN, .sfp_port[0].sds = 0, - .sfp_port[0].i2c = { .sda = GPIO47_I2C_SDA0, .scl = GPIO46_I2C_SCL0 }, + .sfp_port[0].i2c = I2CBUS( GPIO47_I2C_SDA0, GPIO46_I2C_SCL0 ), .sfp_port[1].pin_detect = GPIO36_PWM_OUT, .sfp_port[1].pin_los = GPIO37, .sfp_port[1].pin_tx_disable = GPIO51_I2C_SDA2_UART1_RX, .sfp_port[1].sds = 1, - .sfp_port[1].i2c = { .sda = GPIO49_I2C_SDA1, .scl = GPIO48_I2C_SCL1 }, + .sfp_port[1].i2c = I2CBUS( GPIO49_I2C_SDA1, GPIO48_I2C_SCL1 ), .reset_pin = GPIO_NA, .port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1}, .led_sets = { @@ -596,13 +613,13 @@ __code const struct machine machine = { .sfp_port[0].pin_detect = GPIO38, .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), // Right SFP port .sfp_port[1].pin_detect = GPIO37, .sfp_port[1].pin_los = GPIO_NA, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_27 | LED_28_SYS | LED_29 }, @@ -641,7 +658,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, @@ -676,7 +693,7 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO48_I2C_SCL1, .high_leds = { .mux = LED_28_SYS, .enable = LED_27 | LED_28_SYS | LED_29 }, .led_mux_custom = 1, @@ -720,14 +737,14 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), // Right SFP port (6) // LED pin 24 .sfp_port[1].pin_detect = GPIO50_I2C_SCL2_UART1_TX, .sfp_port[1].pin_los = GPIO51_I2C_SDA2_UART1_RX, .sfp_port[1].pin_tx_disable = GPIO_NA, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 1, 0, 0, 0, 0, 1}, @@ -769,11 +786,7 @@ __code const struct machine machine = { .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 - }, - + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { @@ -823,14 +836,14 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), /* Right SFP (logical 3, SDS0): GPIO50=ModAbs, GPIO51=RX_LOS */ .sfp_port[1].pin_detect = GPIO50_I2C_SCL2_UART1_TX, .sfp_port[1].pin_los = GPIO51_I2C_SDA2_UART1_RX, .sfp_port[1].pin_tx_disable = GPIO_NA, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, @@ -879,14 +892,14 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 0, - .sfp_port[0].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), // SFP port on SDS1 / logical port 8 .sfp_port[1].pin_detect = GPIO38, .sfp_port[1].pin_los = GPIO_NA, .sfp_port[1].pin_tx_disable = GPIO_NA, .sfp_port[1].sds = 1, - .sfp_port[1].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_27 | LED_28_SYS | LED_29 }, @@ -925,7 +938,7 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_27 | LED_28_SYS | LED_29 }, @@ -968,7 +981,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_28_SYS | LED_29, .enable = LED_27 | LED_28_SYS | LED_29 }, @@ -1049,13 +1062,13 @@ __code const struct machine machine = { .sfp_port[0].pin_detect = GPIO38, .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), // Right SFP port .sfp_port[1].pin_detect = GPIO37, .sfp_port[1].pin_los = GPIO_NA, .sfp_port[1].sds = 0, - .sfp_port[1].i2c = { .sda = GPIO41_I2C_SDA3_MDIO1, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[1].i2c = I2CBUS( GPIO41_I2C_SDA3_MDIO1, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 | LED_28_SYS | LED_29, .enable = LED_28_SYS | LED_29 }, @@ -1114,7 +1127,7 @@ __code const struct machine machine = { .sfp_port[0].pin_los = GPIO_NA, .sfp_port[0].pin_tx_disable = GPIO_NA, .sfp_port[0].sds = 1, - .sfp_port[0].i2c = { .sda = GPIO39_I2C_SDA4, .scl = GPIO40_I2C_SCL3_MDC1 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, 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}, diff --git a/machine.h b/machine.h index 6ca90f4c..ecf5f617 100644 --- a/machine.h +++ b/machine.h @@ -49,13 +49,6 @@ // #define MACHINE_PB_2132 // #define MACHINE_HASIVO_S600W_4GT_2XGT_SE -typedef struct { - // GPIO pins for SDA/SCL - uint8_t sda; - uint8_t scl; -} i2c_bus_t; - - #define LED_27 1 // SYSTEM LED #define LED_28_SYS 2 @@ -74,7 +67,7 @@ struct sfp_port uint8_t pin_los; // gpio number 0-63, 0xFF = don't have it? uint8_t pin_tx_disable; // gpio number 0-63, 0xFF = not present uint8_t sds; - i2c_bus_t i2c; + uint8_t i2c; }; struct machine { diff --git a/rtl837x_pins.c b/rtl837x_pins.c index bee98187..a6aaa7b8 100644 --- a/rtl837x_pins.c +++ b/rtl837x_pins.c @@ -10,38 +10,6 @@ extern __xdata uint8_t sfr_data[4]; #pragma codeseg BANK2 #pragma constseg BANK2 -uint8_t i2c_bus_from_sda_pin(uint8_t sda_pin) __banked { - switch (sda_pin) { - case GPIO47_I2C_SDA0: - return 0; - case GPIO49_I2C_SDA1: - return 1; - case GPIO51_I2C_SDA2_UART1_RX: - return 2; - case GPIO41_I2C_SDA3_MDIO1: - return 3; - case GPIO39_I2C_SDA4: - return 4; - default: - return 0xFF; - } -} - -uint8_t i2c_bus_from_scl_pin(uint8_t scl_pin) __banked{ - switch (scl_pin) { - case GPIO46_I2C_SCL0: - return 0; - case GPIO48_I2C_SCL1: - return 1; - case GPIO50_I2C_SCL2_UART1_TX: - return 2; - case GPIO40_I2C_SCL3_MDC1: - return 3; - default: - return 0xFF; - } -} - /* Returns RTL837X_REG_GPIO_XX_OUTPUT register address */ static uint16_t gpio_output_reg(uint8_t pin) __banked{ return pin < 32 ? RTL837X_REG_GPIO_00_31_OUTPUT : RTL837X_REG_GPIO_32_63_OUTPUT; @@ -147,8 +115,7 @@ bool sfp_read_block(uint8_t slot, uint8_t reg, uint8_t len) __banked __reentrant REG_WRITE(RTL837X_REG_I2C_CTRL, 0x00, 0x1 << (I2C_MEM_ADDR_WIDTH - 16) | len, - (dev >> 5) | i2c_bus_from_scl_pin(machine.sfp_port[slot].i2c.scl) << 5 - | i2c_bus_from_sda_pin(machine.sfp_port[slot].i2c.sda) << 2, + (dev >> 5) | machine.sfp_port[slot].i2c, ((dev << 3) & 0xff) | 0x1); do { diff --git a/rtl837x_pins.h b/rtl837x_pins.h index 9f84226f..cc5ffd9b 100644 --- a/rtl837x_pins.h +++ b/rtl837x_pins.h @@ -71,12 +71,6 @@ /* Not available GPIO */ #define GPIO_NA 0xFF -/* Convert SDA PIN GPIO to I2C bus number */ -uint8_t i2c_bus_from_sda_pin(uint8_t sda_pin) __banked; - -/* Convert SCL PIN GPIO to I2C bus number */ -uint8_t i2c_bus_from_scl_pin(uint8_t scl_pin) __banked; - /* * Setup a GPIO pin as input * pin: GPIO pin number 0-63 From b5fe9f217f7e5003d285d7fe98ce311074928b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20van=20Dorst?= Date: Sun, 13 Sep 2026 12:01:05 +0200 Subject: [PATCH 22/79] i2c/sfp: Demagic shift-values --- boot.c | 4 ++-- machine.c | 4 ++-- rtl837x_regs.h | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/boot.c b/boot.c index 4c188ad8..3646a0a0 100644 --- a/boot.c +++ b/boot.c @@ -398,8 +398,8 @@ void setup_i2c(void) __banked reg_read_m(RTL837X_PIN_MUX_1); for (uint8_t sfp = 0; sfp < machine.n_sfp; sfp++) { uint8_t i2c = machine.sfp_port[sfp].i2c; - uint8_t scl_bus = (i2c >> 5) & 0x03; - uint8_t sda_bus = (i2c >> 2) & 0x07; + uint8_t scl_bus = (i2c >> RTL837X_REG_I2C_SCL_SHIFT) & 0x03; + uint8_t sda_bus = (i2c >> RTL837X_REG_I2C_SDA_SHIFT) & 0x07; print_string("Configuring I2C for SFP idx="); print_byte(sfp); print_string(" SCL="); print_byte(scl_bus); print_string(", SDA="); print_byte(sda_bus); write_char('\n'); switch (scl_bus) { case 3: diff --git a/machine.c b/machine.c index a4f8e192..9ce62920 100644 --- a/machine.c +++ b/machine.c @@ -6,8 +6,8 @@ #include "rtl837x_common.h" #define I2CBUS(sda, scl) \ - ((uint8_t)((I2C_BUS_FROM_SCL_PIN(scl) << 5) | \ - (I2C_BUS_FROM_SDA_PIN(sda) << 2))) + ((uint8_t)((I2C_BUS_FROM_SCL_PIN(scl) << RTL837X_REG_I2C_SCL_SHIFT) | \ + (I2C_BUS_FROM_SDA_PIN(sda) << RTL837X_REG_I2C_SDA_SHIFT))) #define I2C_BUS_FROM_SDA_PIN(sda_pin) \ ((sda_pin) == GPIO47_I2C_SDA0 ? (uint8_t)0 : \ diff --git a/rtl837x_regs.h b/rtl837x_regs.h index 6154221b..435c0c2d 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -126,6 +126,8 @@ #define RTL837X_REG_I2C_CTRL2 0x041c #define RTL837X_REG_I2C_IN 0x0420 #define RTL837X_REG_I2C_OUT 0x0424 +#define RTL837X_REG_I2C_SCL_SHIFT 5 +#define RTL837X_REG_I2C_SDA_SHIFT 2 /* * NIC Related registers From 19ccc4003dff94c741e35e1c0bcb96801b1655c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20van=20Dorst?= Date: Sun, 13 Sep 2026 12:17:37 +0200 Subject: [PATCH 23/79] i2c: Add compiletime check macro for validating the i2c pins. Use 0x7 as error value for an invalid I2C SDA/SCL-pin value. The macro `I2CBUS_VALID` checks for this value. When a invalid pin is ues, the compiler errors out with: "machine.c:: error 176: sizeof applied to an incomplete type", Error message is not ideal but at-least points to the right location in the code. --- boot.c | 2 +- machine.c | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/boot.c b/boot.c index 3646a0a0..8b8236aa 100644 --- a/boot.c +++ b/boot.c @@ -398,7 +398,7 @@ void setup_i2c(void) __banked reg_read_m(RTL837X_PIN_MUX_1); for (uint8_t sfp = 0; sfp < machine.n_sfp; sfp++) { uint8_t i2c = machine.sfp_port[sfp].i2c; - uint8_t scl_bus = (i2c >> RTL837X_REG_I2C_SCL_SHIFT) & 0x03; + uint8_t scl_bus = (i2c >> RTL837X_REG_I2C_SCL_SHIFT) & 0x07; uint8_t sda_bus = (i2c >> RTL837X_REG_I2C_SDA_SHIFT) & 0x07; print_string("Configuring I2C for SFP idx="); print_byte(sfp); print_string(" SCL="); print_byte(scl_bus); print_string(", SDA="); print_byte(sda_bus); write_char('\n'); switch (scl_bus) { diff --git a/machine.c b/machine.c index 9ce62920..5830ff27 100644 --- a/machine.c +++ b/machine.c @@ -5,22 +5,28 @@ #include "rtl837x_regs.h" #include "rtl837x_common.h" +#define I2CBUS_INVALID (7) +// This is a MACRO to check if I2C_BUS_FROM_SDA_PIN or I2C_BUS_FROM_SCL_PIN don't return I2CBUS_INVALID. +// When I2CBUS_INVALID is returned, compiler stops with "machine.c:: error 176: sizeof applied to an incomplete type" +#define I2CBUS_VALID(bus) (0 * sizeof(char[(bus) != I2CBUS_INVALID ? 1 : 0])) #define I2CBUS(sda, scl) \ - ((uint8_t)((I2C_BUS_FROM_SCL_PIN(scl) << RTL837X_REG_I2C_SCL_SHIFT) | \ - (I2C_BUS_FROM_SDA_PIN(sda) << RTL837X_REG_I2C_SDA_SHIFT))) + ((uint8_t)((I2C_BUS_FROM_SCL_PIN(scl) << RTL837X_REG_I2C_SCL_SHIFT) | \ + (I2C_BUS_FROM_SDA_PIN(sda) << RTL837X_REG_I2C_SDA_SHIFT) | \ + I2CBUS_VALID(I2C_BUS_FROM_SCL_PIN(scl)) | \ + I2CBUS_VALID(I2C_BUS_FROM_SDA_PIN(sda)))) #define I2C_BUS_FROM_SDA_PIN(sda_pin) \ - ((sda_pin) == GPIO47_I2C_SDA0 ? (uint8_t)0 : \ - (sda_pin) == GPIO49_I2C_SDA1 ? (uint8_t)1 : \ - (sda_pin) == GPIO51_I2C_SDA2_UART1_RX ? (uint8_t)2 : \ - (sda_pin) == GPIO41_I2C_SDA3_MDIO1 ? (uint8_t)3 : \ - (sda_pin) == GPIO39_I2C_SDA4 ? (uint8_t)4 : -1) + ((sda_pin) == GPIO47_I2C_SDA0 ? 0 : \ + (sda_pin) == GPIO49_I2C_SDA1 ? 1 : \ + (sda_pin) == GPIO51_I2C_SDA2_UART1_RX ? 2 : \ + (sda_pin) == GPIO41_I2C_SDA3_MDIO1 ? 3 : \ + (sda_pin) == GPIO39_I2C_SDA4 ? 4 : I2CBUS_INVALID) #define I2C_BUS_FROM_SCL_PIN(scl_pin) \ ((scl_pin) == GPIO46_I2C_SCL0 ? 0 : \ (scl_pin) == GPIO48_I2C_SCL1 ? 1 : \ (scl_pin) == GPIO50_I2C_SCL2_UART1_TX ? 2 : \ - (scl_pin) == GPIO40_I2C_SCL3_MDC1 ? 3 : -1) + (scl_pin) == GPIO40_I2C_SCL3_MDC1 ? 3 : I2CBUS_INVALID) #if defined(MACHINE_KP_9000_6XHML_X2) || \ defined(MACHINE_KP_9000_6XH_X2_V1_1) || \ From 131bc106aff6825a8b3a18b1a0adaeff0784feef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20van=20Dorst?= Date: Sun, 13 Sep 2026 21:55:50 +0200 Subject: [PATCH 24/79] boot/i2c-setup(): Remove the switch-statement defaults. All the possibilities are handled, or otherwise a compile-time macro I2CBUS_VALID ensures invalid options are rejected with a compiler error. Also fix a compiler warning. --- boot.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/boot.c b/boot.c index 8b8236aa..b5f72b85 100644 --- a/boot.c +++ b/boot.c @@ -398,10 +398,10 @@ void setup_i2c(void) __banked reg_read_m(RTL837X_PIN_MUX_1); for (uint8_t sfp = 0; sfp < machine.n_sfp; sfp++) { uint8_t i2c = machine.sfp_port[sfp].i2c; - uint8_t scl_bus = (i2c >> RTL837X_REG_I2C_SCL_SHIFT) & 0x07; + uint8_t scl_bus = (i2c >> RTL837X_REG_I2C_SCL_SHIFT); uint8_t sda_bus = (i2c >> RTL837X_REG_I2C_SDA_SHIFT) & 0x07; print_string("Configuring I2C for SFP idx="); print_byte(sfp); print_string(" SCL="); print_byte(scl_bus); print_string(", SDA="); print_byte(sda_bus); write_char('\n'); - switch (scl_bus) { + switch (scl_bus & 0x03) { case 3: // Bit 5-6 0b10 -> SCL (implies enabled SDA on bus 3) sfr_mask_data(0, 0x60, 0x40); @@ -420,8 +420,6 @@ void setup_i2c(void) __banked sfr_mask_data(0, 0x80, 0x80); sfr_mask_data(1, 0x01, 0x00); break; - default: - print_string("Invalid SCL bus number: "); print_byte(scl_bus); write_char('\n'); } switch (sda_bus) { @@ -445,8 +443,6 @@ void setup_i2c(void) __banked // Bit 9-10 0b01 -> SDA sfr_mask_data(1, 0x06, 0x02); break; - default: - print_string("Invalid SDA bus number: "); print_byte(sda_bus); write_char('\n'); } } reg_write_m(RTL837X_PIN_MUX_1); From 1557a7cf3ce0b932e0da489cc12c261b1c62b26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20van=20Dorst?= Date: Sun, 20 Sep 2026 11:30:17 +0200 Subject: [PATCH 25/79] MACHINE: rebase, update the new added devices --- machine.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/machine.c b/machine.c index 5830ff27..23e7ca41 100644 --- a/machine.c +++ b/machine.c @@ -1179,10 +1179,7 @@ __code const struct machine machine = { .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 - }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = 0, .enable = 0 }, @@ -1216,7 +1213,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, .high_leds = { .mux = LED_27 | LED_29, .enable = LED_28_SYS | LED_29 }, .port_led_set = { 0, 0, 0, 0, 0, 0, 0, 0, 1}, @@ -1260,7 +1257,7 @@ __code const struct machine machine = { .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 }, + .sfp_port[0].i2c = I2CBUS( GPIO39_I2C_SDA4, GPIO40_I2C_SCL3_MDC1 ), .reset_pin = GPIO_NA, From 45fb382b07d70a9f1c30d4731f086766eeb21364 Mon Sep 17 00:00:00 2001 From: gnacho Date: Sun, 20 Sep 2026 13:29:12 +0200 Subject: [PATCH 26/79] feat(translate): add Spanish option to the language selector --- html/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/html/index.html b/html/index.html index 3e1696b1..6402285b 100644 --- a/html/index.html +++ b/html/index.html @@ -396,6 +396,7 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent} +