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():