diff --git a/README.en.md b/README.en.md
index e0ec7f2..3d44356 100644
--- a/README.en.md
+++ b/README.en.md
@@ -62,8 +62,8 @@ everything from a **password-protected web UI** — no CLI or hand-edited config
(pySerial `rfc2217://`, Linux `socat`, Windows com0com+com2tcp / HW VSP3) — see
[`docs/VIRTUAL-COM.md`](docs/VIRTUAL-COM.md).
- **Full serial config:** baud (incl. custom), data/stop bits, parity, flow control
- (none/RTS-CTS/XON-XOFF/DSR-DTR), RTS/DTR on open, exclusive open. (RS-485 hardware
- mode is set via JSON import / the REST API today; a web-UI control is planned.)
+ (none/RTS-CTS/XON-XOFF/DSR-DTR), RTS/DTR on open, exclusive open, and RS-485 hardware
+ auto-RTS (Linux `TIOCSRS485`: enable + pre/post-TX delays + RTS-during-TX level).
- **Live port list:** privilege-free polling + optional event hotplug (Linux pyudev /
Windows WM_DEVICECHANGE), falling back to polling. **IP picker** from the machine's
own addresses (localhost / LAN / 0.0.0.0) or custom.
diff --git a/README.md b/README.md
index 12976c5..8b1a941 100644
--- a/README.md
+++ b/README.md
@@ -64,8 +64,8 @@ düzenlemeye gerek yok.
reçeteler (pyserial `rfc2217://`, Linux `socat`, Windows com0com+com2tcp / HW VSP3) —
bkz. [`docs/VIRTUAL-COM.md`](docs/VIRTUAL-COM.md).
- **Tam seri yapılandırma:** baud (custom dahil), data bit, parity, stop bit, akış
- kontrolü (none/RTS-CTS/XON-XOFF/DSR-DTR), açılışta RTS/DTR, exclusive open. (RS-485
- donanım modu şu an JSON import / REST API ile ayarlanır; web-UI kontrolü planlı.)
+ kontrolü (none/RTS-CTS/XON-XOFF/DSR-DTR), açılışta RTS/DTR, exclusive open ve RS-485
+ donanım auto-RTS (Linux `TIOCSRS485`: aç + TX öncesi/sonrası gecikme + TX'te RTS seviyesi).
- **Canlı port listesi:** ayrıcalık gerektirmeyen polling + isteğe bağlı olay-tabanlı
hotplug (Linux pyudev / Windows WM_DEVICECHANGE), yoksa polling'e düşer.
- **IP seçici:** makineye atanmış IP'ler (localhost / LAN / 0.0.0.0) veya custom.
diff --git a/ROADMAP.md b/ROADMAP.md
index 9e417b6..2c66937 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,6 +1,6 @@
# ser2net — Roadmap
-> Current release: **v2.6.2**. CI is green across Linux + Windows × Python 3.10–3.13.
+> Current release: **v2.6.3**. CI is green across Linux + Windows × Python 3.10–3.13.
## Shipped
@@ -98,8 +98,14 @@
uvicorn bind crash on a hand-edited config)
- Closed the top test gap: **per-mapping TLS data-bridge e2e** (`test_tls_bridge.py` —
verifies the channel is encrypted + plaintext is rejected) and a bind-IP validation case
-- Doc accuracy: RS-485 is config/REST/JSON-settable (no web UI yet); global logs are
- size-rotated (distinct from the per-mapping 100 MB/15-day trim)
+- Doc accuracy: global logs are size-rotated (distinct from the per-mapping
+ 100 MB/15-day trim)
+
+### v2.6.3 — RS-485 in the UI
+- **RS-485 hardware auto-RTS** (`TIOCSRS485`) is now exposed in the mapping form
+ (enable + RTS pre/post-TX delays + RTS-high-during-TX); the engine already applied it
+ via pyserial `rs485_mode`. Edit-preservation updated so the form owns the RS-485 fields
+ while still carrying over the two it doesn't expose (`hangup_when_done`, `nobreak`).
---
@@ -111,8 +117,6 @@
key-guarded peer endpoints; optional IPv6 (multicast) discovery
### v2.7 — industrial/IIoT depth
-- **RS-485 hardware auto-RTS** UI (`TIOCSRS485`) — the config/REST already carry the
- fields; expose them in the mapping form (closes the docs gap noted in v2.6.2)
- **Sparkplug B** edge payloads (Modbus register + MQTT plumbing already in place)
- **Modbus write** support (FC 5/6/15/16), per-point MQTT→register control, RTU
inter-frame tuning
diff --git a/app/__init__.py b/app/__init__.py
index 7683a3e..47207f0 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,4 +1,4 @@
"""ser2net — serial-to-network bridge with a web config UI."""
# Single source of truth for the version (ser2net.py and the web layer import this).
-__version__ = "2.6.2"
+__version__ = "2.6.3"
diff --git a/app/web/routes.py b/app/web/routes.py
index 3321f6b..9b7c10f 100644
--- a/app/web/routes.py
+++ b/app/web/routes.py
@@ -1192,6 +1192,14 @@ def _serial_dict(form, prefix: str, strict: bool) -> dict:
"rts_on_open": form.get(prefix + "rts_on_open", "keep"),
"dtr_on_open": form.get(prefix + "dtr_on_open", "keep"),
"exclusive": _checkbox(form, prefix + "exclusive"),
+ "advanced": {
+ "rs485_enabled": _checkbox(form, prefix + "rs485_enabled"),
+ "rs485_delay_before_tx_ms": _num(form, prefix + "rs485_delay_before_tx_ms",
+ 0.0, strict=False, cast=float),
+ "rs485_delay_after_tx_ms": _num(form, prefix + "rs485_delay_after_tx_ms",
+ 0.0, strict=False, cast=float),
+ "rs485_rts_level_for_tx": _checkbox(form, prefix + "rs485_rts_level_for_tx"),
+ },
}
@@ -1274,15 +1282,17 @@ def _mapping_from_form(form) -> dict:
def _preserve_unmanaged_fields(new_map, existing) -> None:
"""Carry over config fields the mapping form does not expose, so editing an
- existing mapping never silently discards them: the stable-id `match`, the
- advanced/RS-485 serial settings, open/close strings, the trace-timestamp flag
- and the RFC2217 interop knobs. No-op when creating a new mapping."""
+ existing mapping never silently discards them: the stable-id `match`, the two
+ advanced serial flags the form omits (`hangup_when_done`, `nobreak`), open/close
+ strings, the trace-timestamp flag and the RFC2217 interop knobs. The RS-485 fields
+ ARE in the form now, so they're taken from the submission. No-op when creating."""
if existing is None:
return
for cur, old in ((new_map.serial, existing.serial),
(new_map.serial_b, existing.serial_b)):
cur.match = old.match
- cur.advanced = old.advanced
+ cur.advanced.hangup_when_done = old.advanced.hangup_when_done
+ cur.advanced.nobreak = old.advanced.nobreak
o, prev = new_map.options, existing.options
o.openstr = prev.openstr
o.closestr = prev.closestr
diff --git a/app/web/templates/_mapping_form.html b/app/web/templates/_mapping_form.html
index 9e9715c..5eaa2cd 100644
--- a/app/web/templates/_mapping_form.html
+++ b/app/web/templates/_mapping_form.html
@@ -41,6 +41,17 @@
+
+ RS-485 (half-duplex, hardware auto-RTS)
+
+
+
+
+
+
+
{% endmacro %}
{% set ip_values = ips | map(attribute='value') | list %}
diff --git a/docs/screenshots/02-dashboard.png b/docs/screenshots/02-dashboard.png
index ea349ff..1975b7a 100644
Binary files a/docs/screenshots/02-dashboard.png and b/docs/screenshots/02-dashboard.png differ
diff --git a/docs/screenshots/03-add-mapping.png b/docs/screenshots/03-add-mapping.png
index 701b80e..aff62e9 100644
Binary files a/docs/screenshots/03-add-mapping.png and b/docs/screenshots/03-add-mapping.png differ
diff --git a/docs/screenshots/04-settings.png b/docs/screenshots/04-settings.png
index 4f46318..8b55286 100644
Binary files a/docs/screenshots/04-settings.png and b/docs/screenshots/04-settings.png differ
diff --git a/docs/screenshots/06-cluster.png b/docs/screenshots/06-cluster.png
index 9e6a0ff..2633565 100644
Binary files a/docs/screenshots/06-cluster.png and b/docs/screenshots/06-cluster.png differ
diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py
index 71e1c3e..3e86a1f 100644
--- a/tests/test_config_validation.py
+++ b/tests/test_config_validation.py
@@ -33,10 +33,11 @@ def test_preserve_unmanaged_fields_on_edit():
existing = MappingConfig.from_dict({
"name": "dev", "serial": {
"port": "COM3", "match": {"vid": "0403", "pid": "6001"},
- "advanced": {"rs485_enabled": True, "rs485_delay_before_tx_ms": 5.0}},
+ "advanced": {"rs485_enabled": True, "hangup_when_done": True, "nobreak": True}},
"options": {"openstr": "INIT\r\n", "closestr": "BYE\r", "trace_timestamp": False,
"rfc2217_net_timeout_s": 9.0}})
- # what the form would submit (no match/advanced/openstr/closestr/rfc2217 fields)
+ # what the form would submit: it DOES carry the RS-485 fields now (here: off),
+ # but NOT match / hangup_when_done / nobreak / openstr / closestr / rfc2217 knobs.
edited = MappingConfig.from_dict({
"id": existing.id, "name": "dev", "serial": {"port": "COM3", "baudrate": 19200},
"options": {"banner": "hello", "closeon": "logout"}})
@@ -44,15 +45,18 @@ def test_preserve_unmanaged_fields_on_edit():
_preserve_unmanaged_fields(edited, existing)
assert edited.serial.match == {"vid": "0403", "pid": "6001"}, "stable-id match dropped on edit"
- assert edited.serial.advanced.rs485_enabled is True, "RS-485 setting dropped on edit"
- assert edited.serial.advanced.rs485_delay_before_tx_ms == 5.0
+ # RS-485 is form-managed now: the (empty) submission wins, not the old value
+ assert edited.serial.advanced.rs485_enabled is False, "RS-485 should follow the form, not persist"
+ # the two advanced flags the form omits are still preserved
+ assert edited.serial.advanced.hangup_when_done is True, "hangup_when_done dropped on edit"
+ assert edited.serial.advanced.nobreak is True, "nobreak dropped on edit"
assert edited.options.openstr == "INIT\r\n" and edited.options.closestr == "BYE\r"
assert edited.options.trace_timestamp is False
assert edited.options.rfc2217_net_timeout_s == 9.0
# form-managed fields keep the submitted values
assert edited.options.banner == "hello" and edited.options.closeon == "logout"
assert edited.serial.baudrate == 19200
- print("edit preserves match / RS-485 / openstr / closestr / rfc2217 knobs OK")
+ print("edit: form-managed RS-485 follows submission; match/hangup/nobreak/strings preserved OK")
def test_preserve_is_noop_for_new_mapping():