diff --git a/.gitignore b/.gitignore
index 4798457..575d946 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
__pycache__
src/_assembled.html
src/web_ui_gz.h
+dist/
diff --git a/README.md b/README.md
index 1465a79..8438379 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ build/
build_web.py ← assembles template + tool files → gzip C header
dev_server.py ← HTTP + mock WebSocket dev server, auto-reload
tools/
- userial/ ← RS-232 serial monitor/sender
+ userial/ ← Dual-UART serial monitor/injector/passthrough bridge
wifiscan/ ← WiFi network scanner
```
diff --git a/build/dev_server.py b/build/dev_server.py
index 89baf3b..83d3a14 100644
--- a/build/dev_server.py
+++ b/build/dev_server.py
@@ -42,6 +42,114 @@
_assembled_html = ""
_last_hash = ""
_mock_data = {}
+_ws_url_rewrite = None
+
+
+def _preview_ascii(data: bytes) -> str:
+ out = []
+ for value in data[:32]:
+ if 32 <= value < 127:
+ out.append(chr(value))
+ elif value == 13:
+ out.append("\\r")
+ elif value == 10:
+ out.append("\\n")
+ elif value == 9:
+ out.append("\\t")
+ else:
+ out.append(".")
+ return "".join(out)
+
+
+def _preview_hex(data: bytes) -> str:
+ return " ".join(f"{value:02X}" for value in data[:32])
+
+
+def _parse_hex_bytes(raw: str):
+ raw = raw or ""
+ out = bytearray()
+ i = 0
+ while i < len(raw):
+ ch = raw[i]
+ if ch in " ,\t\r\n":
+ i += 1
+ continue
+ if ch not in "0123456789abcdefABCDEF":
+ return None
+ token = ch
+ i += 1
+ if i < len(raw) and raw[i] in "0123456789abcdefABCDEF":
+ token += raw[i]
+ i += 1
+ if i < len(raw) and raw[i] not in " ,\t\r\n":
+ return None
+ out.append(int(token, 16))
+ return bytes(out)
+
+
+def _normalize_mock_state():
+ status = _mock_data.setdefault("status", {"type": "status"})
+ settings = _mock_data.setdefault("settings", {"type": "settings"})
+ history = _mock_data.setdefault("history", {"type": "history", "items": []})
+
+ status.setdefault("rxBytes", 0)
+ status.setdefault("txBytes", 0)
+ status.setdefault("uptime", 0)
+ status.setdefault("heap", 180000)
+ status.setdefault("baud", 9600)
+ status.setdefault("databits", 8)
+ status.setdefault("parity", "N")
+ status.setdefault("stopbits", 1)
+ status.setdefault("config", "8N1")
+ status.setdefault("passthroughMode", "both")
+ status.setdefault("wifiMode", 0)
+ status.setdefault("mdnsHost", "tool")
+ status.setdefault("mdnsActive", True)
+ status.setdefault("apIP", "192.168.4.1")
+
+ ports = status.setdefault("ports", [
+ {"id": "A", "label": "Channel A", "rxPin": 4, "txPin": 5, "rxBytes": 0, "txBytes": 0},
+ {"id": "B", "label": "Channel B", "rxPin": 6, "txPin": 7, "rxBytes": 0, "txBytes": 0},
+ ])
+ for port in ports:
+ port.setdefault("label", f"Channel {port.get('id', 'A')}")
+ port.setdefault("rxPin", 4 if port.get("id") == "A" else 6)
+ port.setdefault("txPin", 5 if port.get("id") == "A" else 7)
+ port.setdefault("rxBytes", 0)
+ port.setdefault("txBytes", 0)
+
+ settings.update({
+ "type": "settings",
+ "ssid": settings.get("ssid", "ToolAP"),
+ "apHasPass": settings.get("apHasPass", True),
+ "wifiMode": settings.get("wifiMode", status["wifiMode"]),
+ "staSSID": settings.get("staSSID", ""),
+ "staHasPass": settings.get("staHasPass", False),
+ "mdnsHost": settings.get("mdnsHost", status["mdnsHost"]),
+ "baud": settings.get("baud", status["baud"]),
+ "databits": settings.get("databits", status["databits"]),
+ "parity": settings.get("parity", status["parity"]),
+ "stopbits": settings.get("stopbits", status["stopbits"]),
+ "config": settings.get("config", status["config"]),
+ "passthroughMode": settings.get("passthroughMode", status["passthroughMode"]),
+ "ports": ports,
+ })
+ history.setdefault("type", "history")
+ history.setdefault("items", [])
+
+
+def _port_state(port_id: str):
+ port_id = (port_id or "A").upper()
+ for port in _mock_data["status"]["ports"]:
+ if port["id"] == port_id:
+ return port
+ return _mock_data["status"]["ports"][0]
+
+
+def _append_history(entry):
+ items = _mock_data.setdefault("history", {"type": "history", "items": []})["items"]
+ items.append(entry)
+ del items[:-80]
# ---------------------------------------------------------------------------
# File watching
@@ -74,6 +182,11 @@ def _rebuild_if_needed():
if current != _last_hash:
_last_hash = current
_assembled_html = assemble(_base_dir, _tool_dir)
+ if _ws_url_rewrite:
+ _assembled_html = _assembled_html.replace(
+ "ws://' + location.host + '/ws",
+ _ws_url_rewrite
+ )
# Inject auto-reload snippet
reload_script = """