Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 9 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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`).

---

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
18 changes: 14 additions & 4 deletions app/web/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
}


Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions app/web/templates/_mapping_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@
<select name="{{ prefix }}dtr_on_open">{% for x in line_states %}<option value="{{ x }}" {% if x == s.dtr_on_open %}selected{% endif %}>{{ x }}</option>{% endfor %}</select>
</label>
</div>
<details class="rs485"{% if s.advanced.rs485_enabled %} open{% endif %}>
<summary>RS-485 (half-duplex, hardware auto-RTS)</summary>
<label class="chk"><input type="checkbox" name="{{ prefix }}rs485_enabled" {% if s.advanced.rs485_enabled %}checked{% endif %}> Enable RS-485 mode <span class="muted">(Linux <span class="mono">TIOCSRS485</span>; driver must support it)</span></label>
<div class="grid3">
<label>RTS delay before TX (ms)
<input type="number" step="0.1" min="0" name="{{ prefix }}rs485_delay_before_tx_ms" value="{{ s.advanced.rs485_delay_before_tx_ms }}"></label>
<label>RTS delay after TX (ms)
<input type="number" step="0.1" min="0" name="{{ prefix }}rs485_delay_after_tx_ms" value="{{ s.advanced.rs485_delay_after_tx_ms }}"></label>
<label class="chk chk-pad"><input type="checkbox" name="{{ prefix }}rs485_rts_level_for_tx" {% if s.advanced.rs485_rts_level_for_tx %}checked{% endif %}> RTS high during TX</label>
</div>
</details>
{% endmacro %}

{% set ip_values = ips | map(attribute='value') | list %}
Expand Down
Binary file modified docs/screenshots/02-dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/03-add-mapping.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/04-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/06-cluster.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions tests/test_config_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,30 @@ 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"}})

_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():
Expand Down
Loading