-
-
-
-
-
+
+
Passthrough
+
+
+
+
+
+
+ When passthrough is enabled, bytes received on one UART are forwarded to the other UART while still being visible in the monitor.
+
+
+
+
+
Pin Configuration
-ESP32-C3 (XIAO) MAX3232 DB9
-────────────────────────────────────────
-GPIO5 (D3/TX) ────> T1IN
- T1OUT ─────────> Pin 3 (TX)
-GPIO4 (D2/RX) <──── R1OUT
- R1IN <───────── Pin 2 (RX)
-GND ───────────────> GND ──────────> Pin 5 (GND)
-3.3V ──────────────> VCC
+Channel A (default) Channel B (default)
+────────────────────── ──────────────────────
+RX GPIO4 (D2) RX GPIO6 (D4)
+TX GPIO5 (D3) TX GPIO7 (D5)
+
+Override with:
+- `SERIAL_A_RX_PIN` / `SERIAL_A_TX_PIN`
+- `SERIAL_B_RX_PIN` / `SERIAL_B_TX_PIN`
+
diff --git a/tools/userial/web/app.js b/tools/userial/web/app.js
index bd6a0c1..41feae5 100644
--- a/tools/userial/web/app.js
+++ b/tools/userial/web/app.js
@@ -1,19 +1,11 @@
-/* ============================================================
- RS-232 Serial Tool — App JS
- Tool-specific WebSocket handlers, UI logic
- ============================================================ */
-
const log = document.getElementById('log');
let logEntries = [];
let appendCrlf = true;
let localEcho = true;
-let termAddCrlf = true;
let termHistory = [];
let termHistoryIdx = -1;
let termCurrentInput = '';
-// --- base.js hooks -------------------------------------------------------
-
function onConnected() {
wsSend({ cmd: 'status' });
wsSend({ cmd: 'settings' });
@@ -26,120 +18,169 @@ function onSettingsOpen() {
function onMessage(data) {
if (data.type === 'status') {
- document.getElementById('rxBytes').textContent = data.rxBytes || 0;
- document.getElementById('txBytes').textContent = data.txBytes || 0;
- document.getElementById('uptime').textContent = formatUptime(data.uptime);
- document.getElementById('baudRate').textContent = data.baud || 9600;
+ renderStatus(data);
+ } else if (data.type === 'settings') {
+ populateToolSettings(data);
} else if (data.type === 'rx') {
- addLogEntry({ ...data, dir: 'RX' });
- document.getElementById('rxBytes').textContent = data.total || 0;
- terminalAppend(data.ascii || '', 'rx-line');
+ const entry = normalizeEntry({ ...data, dir: 'RX' });
+ addLogEntry(entry);
+ updateTrafficStats(data, true);
+ terminalAppend('[' + entry.port + '] ' + decodeDisplayText(entry.ascii) + '\n', 'rx-line');
} else if (data.type === 'sent') {
- addLogEntry({ ...data, dir: 'TX' });
- document.getElementById('txBytes').textContent = data.total || 0;
- // Terminal TX shown via local echo, skip here
+ const entry = normalizeEntry({ ...data, dir: 'TX' });
+ addLogEntry(entry);
+ updateTrafficStats(data, false);
} else if (data.type === 'history') {
loadHistory(data);
- } else if (data.type === 'settings') {
- document.getElementById('baudSelect').value = data.baud || 9600;
- document.getElementById('databitsSelect').value = data.databits || 8;
- document.getElementById('paritySelect').value = data.parity || 'N';
- document.getElementById('stopbitsSelect').value = data.stopbits || 1;
} else if (data.type === 'serialConfig') {
- document.getElementById('baudRate').textContent = data.baud;
+ document.getElementById('serialConfigValue').textContent = data.baud + ' ' + data.config;
+ populateToolSettings(data);
showToast('Serial configured: ' + data.baud + ' ' + data.config, 'success');
+ } else if (data.type === 'passthroughConfig') {
+ updatePassthroughBadge(data.mode || 'off');
+ document.getElementById('passthroughModeSelect').value = data.mode || 'off';
+ showToast('Passthrough updated: ' + (data.mode || 'off'), 'success');
} else if (data.type === 'cleared') {
- log.innerHTML = '
Waiting for data...
';
- logEntries = [];
+ clearLog();
document.getElementById('rxBytes').textContent = '0';
document.getElementById('txBytes').textContent = '0';
+ setPortValue('A', 'Rx', 0);
+ setPortValue('A', 'Tx', 0);
+ setPortValue('B', 'Rx', 0);
+ setPortValue('B', 'Tx', 0);
+ termClear('--- Cleared ---\n');
} else if (data.type === 'error') {
showToast(data.msg || 'Error', 'error');
}
}
-// --- Log -----------------------------------------------------------------
+function normalizeEntry(data) {
+ const port = (data.port || 'A').toUpperCase();
+ const dir = data.dir || (data.rx ? 'RX' : 'TX');
+ return {
+ port,
+ dir,
+ ts: data.ts,
+ len: data.len || 0,
+ hex: data.hex || '',
+ ascii: data.ascii || '',
+ forwardedTo: data.forwardedTo || '',
+ timeStr: data.timeStr || formatTimestamp(data.ts)
+ };
+}
-function addLogEntry(data, prepend) {
- if (prepend === undefined) prepend = true;
- if (logEntries.length === 0) log.innerHTML = '';
+function renderStatus(data) {
+ document.getElementById('rxBytes').textContent = data.rxBytes || 0;
+ document.getElementById('txBytes').textContent = data.txBytes || 0;
+ document.getElementById('uptime').textContent = formatUptime(data.uptime || 0);
+ document.getElementById('serialConfigValue').textContent = (data.baud || 9600) + ' ' + (data.config || '8N1');
+ updatePassthroughBadge(data.passthroughMode || 'off');
+ (data.ports || []).forEach(port => {
+ document.getElementById('port' + port.id + 'Pins').textContent = 'RX GPIO' + port.rxPin + ' · TX GPIO' + port.txPin;
+ setPortValue(port.id, 'Rx', port.rxBytes || 0);
+ setPortValue(port.id, 'Tx', port.txBytes || 0);
+ });
+}
- const time = data.timeStr || formatTimestamp(data.ts);
- const isRx = data.dir === 'RX';
- const showTime = document.getElementById('showTimestamp').checked;
- const showDir = document.getElementById('showDirection').checked;
- const showCtrl = document.getElementById('showControlChars').checked;
- const mode = document.getElementById('displayMode').value;
+function populateToolSettings(data) {
+ if (data.baud) document.getElementById('baudSelect').value = data.baud;
+ if (data.databits) document.getElementById('databitsSelect').value = data.databits;
+ if (data.parity) document.getElementById('paritySelect').value = data.parity;
+ if (data.stopbits) document.getElementById('stopbitsSelect').value = data.stopbits;
+ if (data.passthroughMode) document.getElementById('passthroughModeSelect').value = data.passthroughMode;
+}
- let ascii = data.ascii || '';
- if (!showCtrl) ascii = ascii.replace(/\\r/g, '').replace(/\\n/g, '').replace(/\\t/g, '');
+function updatePassthroughBadge(mode) {
+ document.getElementById('passthroughBadge').textContent = 'Passthrough: ' + mode;
+}
- let html = '';
- if (showTime) html += '
' + time + '';
- if (showDir) html += '
' + data.dir + ' ';
- if (mode === 'both' || mode === 'hex') {
- html += '
' + (data.hex || '') + '';
- }
- if (mode === 'both') {
- html += '
[' + ascii + ']';
- } else if (mode === 'ascii') {
- html += '
' + ascii + '';
+function setPortValue(port, kind, value) {
+ const el = document.getElementById('port' + port + kind);
+ if (el) el.textContent = value;
+}
+
+function updateTrafficStats(data, isRx) {
+ if (isRx) {
+ document.getElementById('rxBytes').textContent = data.total || 0;
+ if (data.port) setPortValue(data.port, 'Rx', data.portRxBytes || 0);
+ if (data.forwardedTo) setPortValue(data.forwardedTo, 'Tx', data.peerTxBytes || 0);
+ } else {
+ document.getElementById('txBytes').textContent = data.total || 0;
+ if (data.port) setPortValue(data.port, 'Tx', data.portTxBytes || 0);
}
+}
- const entry = document.createElement('div');
- entry.className = 'log-entry';
- entry.innerHTML = html;
+function addLogEntry(data, prepend = true) {
+ const entryData = normalizeEntry(data);
+ if (logEntries.length === 0) log.innerHTML = '';
if (prepend) {
- log.insertBefore(entry, log.firstChild);
- logEntries.unshift(data);
- if (logEntries.length > 200) {
- logEntries.pop();
- if (log.lastChild) log.removeChild(log.lastChild);
- }
+ logEntries.unshift(entryData);
+ if (logEntries.length > 200) logEntries.pop();
} else {
- log.appendChild(entry);
- logEntries.push(data);
+ logEntries.push(entryData);
}
+ refreshLogDisplay();
+}
+
+function entryMatchesFilter(entry) {
+ const filter = document.getElementById('monitorFilter').value;
+ return filter === 'all' || entry.port === filter;
+}
+
+function renderLogEntry(entry) {
+ const showTime = document.getElementById('showTimestamp').checked;
+ const showDir = document.getElementById('showDirection').checked;
+ const showPort = document.getElementById('showPort').checked;
+ const showCtrl = document.getElementById('showControlChars').checked;
+ const mode = document.getElementById('displayMode').value;
+ let ascii = entry.ascii || '';
+ if (!showCtrl) ascii = ascii.replace(/\\r/g, '').replace(/\\n/g, '').replace(/\\t/g, '');
+
+ const parts = [];
+ if (showTime) parts.push('
' + entry.timeStr + '');
+ if (showPort) parts.push('
' + entry.port + '');
+ if (showDir) parts.push('
' + entry.dir + '');
+ if (entry.forwardedTo) parts.push('
→ ' + entry.forwardedTo + '');
+ if (mode === 'both' || mode === 'hex') parts.push('
' + escapeHtml(entry.hex || '') + '');
+ if (mode === 'both') parts.push('
[' + escapeHtml(ascii) + ']');
+ else if (mode === 'ascii') parts.push('
' + escapeHtml(ascii) + '');
+ return '
';
}
function loadHistory(data) {
logEntries = [];
- log.innerHTML = '';
if (!data.items || data.items.length === 0) {
- log.innerHTML = '
Waiting for data...
';
+ clearLog();
return;
}
- data.items.forEach(item => {
- item.dir = item.rx ? 'RX' : 'TX';
- item.timeStr = formatTimestamp(item.ts);
- addLogEntry(item, false);
- });
+ data.items.forEach(item => logEntries.push(normalizeEntry(item)));
+ refreshLogDisplay();
}
function clearLog() {
logEntries = [];
- log.innerHTML = '
Waiting for data...
';
+ log.innerHTML = '
Waiting for traffic...
';
}
function refreshLogDisplay() {
- const entries = [...logEntries];
- logEntries = [];
- log.innerHTML = '';
- if (entries.length === 0) {
- log.innerHTML = '
Waiting for data...
';
- } else {
- entries.forEach(e => addLogEntry(e, false));
+ const filtered = logEntries.filter(entryMatchesFilter);
+ if (filtered.length === 0) {
+ log.innerHTML = '
Waiting for traffic...
';
+ return;
}
+ log.innerHTML = filtered.map(renderLogEntry).join('');
}
function clearHistory() {
- if (confirm('Clear all history and statistics?')) {
+ if (confirm('Clear captured history and counters on the device?')) {
wsSend({ cmd: 'clearHistory' });
}
}
-// --- Download / Export ---------------------------------------------------
+function decodeDisplayText(text) {
+ return String(text || '').replace(/\\r/g, '\r').replace(/\\n/g, '\n').replace(/\\t/g, '\t');
+}
function downloadLog() {
if (logEntries.length === 0) {
@@ -149,35 +190,42 @@ function downloadLog() {
const fmt = document.getElementById('exportFormat').value;
const incTime = document.getElementById('exportTimestamp').checked;
const incDir = document.getElementById('exportDirection').checked;
+ const incPort = document.getElementById('exportPort').checked;
const incCtrl = document.getElementById('exportControlChars').checked;
- let text = 'RS-232 Serial Log - ' + new Date().toISOString() + '\n';
+ let text = 'Dual UART Serial Log - ' + new Date().toISOString() + '\n';
text += '='.repeat(60) + '\n\n';
logEntries.forEach(entry => {
- const time = entry.timeStr || formatTimestamp(entry.ts);
- const dir = entry.dir || (entry.rx ? 'RX' : 'TX');
let ascii = entry.ascii || '';
if (!incCtrl) ascii = ascii.replace(/\\r/g, '').replace(/\\n/g, '').replace(/\\t/g, '');
- let line = '';
- if (incTime) line += '[' + time + '] ';
- if (incDir) line += dir + ' ';
- if (fmt === 'hex') {
- line += (entry.hex || '');
- } else if (fmt === 'ascii') {
- line += ascii;
- } else {
- line += 'HEX: ' + (entry.hex || '') + ' ASCII: ' + ascii;
- }
- text += line + '\n';
+ const line = [];
+ if (incTime) line.push('[' + entry.timeStr + ']');
+ if (incPort) line.push(entry.port);
+ if (incDir) line.push(entry.dir);
+ if (entry.forwardedTo) line.push('→ ' + entry.forwardedTo);
+ if (fmt === 'hex') line.push(entry.hex || '');
+ else if (fmt === 'ascii') line.push(ascii);
+ else line.push('HEX: ' + (entry.hex || '') + ' ASCII: ' + ascii);
+ text += line.join(' ') + '\n';
});
const blob = new Blob([text], { type: 'text/plain' });
- downloadBlob(blob, 'serial_log_' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt');
+ downloadBlob(blob, 'dual_uart_log_' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt');
showToast('Log downloaded', 'success');
}
-// --- Send ----------------------------------------------------------------
+function selectedPort(selectId) {
+ return document.getElementById(selectId).value || 'A';
+}
+
+function processEscapes(text) {
+ return text
+ .replace(/\\r/g, '\r')
+ .replace(/\\n/g, '\n')
+ .replace(/\\t/g, '\t')
+ .replace(/\\x([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
+}
function toggleCRLF() {
toggleSwitch('crlfToggle', (active) => { appendCrlf = active; });
@@ -186,37 +234,30 @@ function toggleCRLF() {
function sendAscii() {
const data = document.getElementById('asciiData').value;
if (!data) { showToast('Enter text to send', 'error'); return; }
- wsSend({ cmd: 'sendAscii', data: data, crlf: appendCrlf });
+ wsSend({ cmd: 'sendAscii', port: selectedPort('asciiPortSelect'), data: processEscapes(data), crlf: appendCrlf });
}
function sendQuickAscii(text) {
- const processed = text
- .replace(/\\r/g, '\r')
- .replace(/\\n/g, '\n')
- .replace(/\\x([0-9A-Fa-f]{2})/g, (m, h) => String.fromCharCode(parseInt(h, 16)));
- wsSend({ cmd: 'sendAscii', data: processed, crlf: false });
+ wsSend({ cmd: 'sendAscii', port: selectedPort('asciiPortSelect'), data: processEscapes(text), crlf: appendCrlf });
}
function sendHex() {
const data = document.getElementById('hexData').value.trim();
if (!data) { showToast('Enter hex bytes to send', 'error'); return; }
if (!/^[0-9a-fA-F\s,]+$/.test(data)) { showToast('Invalid hex format', 'error'); return; }
- wsSend({ cmd: 'sendHex', data: data });
+ wsSend({ cmd: 'sendHex', port: selectedPort('hexPortSelect'), data });
}
function sendQuickHex(hex) {
- wsSend({ cmd: 'sendHex', data: hex });
+ wsSend({ cmd: 'sendHex', port: selectedPort('hexPortSelect'), data: hex });
}
-// --- Terminal ------------------------------------------------------------
-
function toggleLocalEcho() {
toggleSwitch('localEchoToggle', (active) => { localEcho = active; });
}
function toggleTermCrlf() {
toggleSwitch('termCrlfToggle', (active) => {
- termAddCrlf = active;
document.getElementById('termLineEnding').value = active ? 'crlf' : 'none';
});
}
@@ -235,8 +276,7 @@ function terminalAppend(text, className) {
const output = document.getElementById('termOutput');
const span = document.createElement('span');
span.className = className || 'rx-line';
- // Clean control chars for display, keep newlines
- span.textContent = text.replace(/\r/g, '').replace(/\\r/g, '');
+ span.textContent = text;
output.appendChild(span);
output.scrollTop = output.scrollHeight;
}
@@ -246,22 +286,16 @@ function termSend(text) {
showToast('Not connected', 'error');
return;
}
- const lineEnding = getLineEnding();
- const fullText = text + lineEnding;
-
- // Local echo
- if (localEcho) {
- terminalAppend(text + '\n', 'tx-line');
- }
- // Command history
+ const fullText = text + getLineEnding();
+ if (localEcho) terminalAppend('[' + selectedPort('termPortSelect') + '] ' + text + '\n', 'tx-line');
if (text && (termHistory.length === 0 || termHistory[termHistory.length - 1] !== text)) {
termHistory.push(text);
if (termHistory.length > 50) termHistory.shift();
}
termHistoryIdx = -1;
- wsSend({ cmd: 'sendAscii', data: fullText, crlf: false });
+ wsSend({ cmd: 'sendAscii', port: selectedPort('termPortSelect'), data: fullText, crlf: false });
}
function termSendQuick(text) {
@@ -272,12 +306,10 @@ function termSendQuick(text) {
input.focus();
}
-function termClear() {
- document.getElementById('termOutput').innerHTML = '
--- Cleared ---\n';
+function termClear(message) {
+ document.getElementById('termOutput').innerHTML = '
' + (message || '--- Cleared ---\n') + '';
}
-// --- Terminal input key handling -----------------------------------------
-
document.getElementById('termInput').addEventListener('keydown', (e) => {
const input = e.target;
if (e.key === 'Enter') {
@@ -312,20 +344,22 @@ document.getElementById('termInput').addEventListener('keydown', (e) => {
});
document.getElementById('termLineEnding').addEventListener('change', (e) => {
- termAddCrlf = e.target.value !== 'none';
- document.getElementById('termCrlfToggle').classList.toggle('active', termAddCrlf);
+ document.getElementById('termCrlfToggle').classList.toggle('active', e.target.value !== 'none');
});
-// --- Settings ------------------------------------------------------------
-
function saveSerialConfig() {
wsSend({
cmd: 'setSerial',
- baud: parseInt(document.getElementById('baudSelect').value),
- databits: parseInt(document.getElementById('databitsSelect').value),
+ baud: parseInt(document.getElementById('baudSelect').value, 10),
+ databits: parseInt(document.getElementById('databitsSelect').value, 10),
parity: document.getElementById('paritySelect').value,
- stopbits: parseInt(document.getElementById('stopbitsSelect').value)
+ stopbits: parseInt(document.getElementById('stopbitsSelect').value, 10)
});
}
-
+function savePassthroughMode() {
+ wsSend({
+ cmd: 'setPassthrough',
+ mode: document.getElementById('passthroughModeSelect').value
+ });
+}
diff --git a/tools/userial/web/config.json b/tools/userial/web/config.json
index b326c48..91f0aaa 100644
--- a/tools/userial/web/config.json
+++ b/tools/userial/web/config.json
@@ -1,3 +1,3 @@
{
- "title": "RS-232 Serial Tool"
+ "title": "Dual UART Serial Tool"
}
diff --git a/tools/userial/web/mock_data.json b/tools/userial/web/mock_data.json
index 1418792..5896cbb 100644
--- a/tools/userial/web/mock_data.json
+++ b/tools/userial/web/mock_data.json
@@ -1,16 +1,24 @@
{
"status": {
"type": "status",
- "rxBytes": 1247,
- "txBytes": 384,
+ "rxBytes": 2741,
+ "txBytes": 2998,
"uptime": 4520,
"baud": 9600,
+ "databits": 8,
+ "parity": "N",
+ "stopbits": 1,
"config": "8N1",
+ "passthroughMode": "both",
"heap": 142800,
"mac": "AA:BB:CC:DD:EE:FF",
"wifiMode": 0,
"staConnected": false,
- "apIP": "192.168.4.1"
+ "apIP": "192.168.4.1",
+ "ports": [
+ { "id": "A", "label": "Channel A", "rxPin": 4, "txPin": 5, "rxBytes": 1247, "txBytes": 1536 },
+ { "id": "B", "label": "Channel B", "rxPin": 6, "txPin": 7, "rxBytes": 1494, "txBytes": 1462 }
+ ]
},
"settings": {
"type": "settings",
@@ -23,15 +31,12 @@
"baud": 9600,
"databits": 8,
"parity": "N",
- "stopbits": 1
- },
- "wifiscan": {
- "type": "wifiscan",
- "networks": [
- { "ssid": "HomeNetwork", "rssi": -42, "enc": true },
- { "ssid": "Office-5G", "rssi": -58, "enc": true },
- { "ssid": "Guest-WiFi", "rssi": -71, "enc": false },
- { "ssid": "Neighbor", "rssi": -82, "enc": true }
+ "stopbits": 1,
+ "config": "8N1",
+ "passthroughMode": "both",
+ "ports": [
+ { "id": "A", "label": "Channel A", "rxPin": 4, "txPin": 5, "rxBytes": 1247, "txBytes": 1536 },
+ { "id": "B", "label": "Channel B", "rxPin": 6, "txPin": 7, "rxBytes": 1494, "txBytes": 1462 }
]
},
"history": {
@@ -39,19 +44,24 @@
"items": [
{
"ts": 4510000,
- "rx": true,
+ "port": "A",
+ "dir": "RX",
+ "forwardedTo": "B",
"hex": "41 54 49 0D 0A",
"ascii": "ATI\\r\\n"
},
{
- "ts": 4511000,
- "rx": false,
+ "ts": 4510500,
+ "port": "B",
+ "dir": "RX",
+ "forwardedTo": "A",
"hex": "4F 4B 0D 0A",
"ascii": "OK\\r\\n"
},
{
"ts": 4515000,
- "rx": true,
+ "port": "A",
+ "dir": "TX",
"hex": "48 65 6C 6C 6F",
"ascii": "Hello"
}
From 89aa4f03185746d04490522988b4398f6ca4444c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 08:12:08 +0000
Subject: [PATCH 4/5] Polish dual-channel passthrough validation fixes
Agent-Logs-Url: https://github.com/bring42/tbx/sessions/2816ed76-19fb-4145-812b-d366a6770f48
Co-authored-by: bring42 <63049750+bring42@users.noreply.github.com>
---
build/dev_server.py | 53 +-
tools/userial/dist/index.html | 572 ++++++---
tools/userial/src/_assembled.html | 572 ++++++---
tools/userial/src/web_ui_gz.h | 1987 +++++++++++++++--------------
tools/userial/web/app.js | 2 +-
5 files changed, 1876 insertions(+), 1310 deletions(-)
diff --git a/build/dev_server.py b/build/dev_server.py
index d10339f..83d3a14 100644
--- a/build/dev_server.py
+++ b/build/dev_server.py
@@ -25,7 +25,6 @@
import argparse
import sys
import os
-import re
from pathlib import Path
from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading
@@ -43,6 +42,7 @@
_assembled_html = ""
_last_hash = ""
_mock_data = {}
+_ws_url_rewrite = None
def _preview_ascii(data: bytes) -> str:
@@ -65,11 +65,26 @@ def _preview_hex(data: bytes) -> str:
return " ".join(f"{value:02X}" for value in data[:32])
-def _parse_hex_bytes(raw: str) -> bytes:
- tokens = re.findall(r"[0-9A-Fa-f]{1,2}", raw or "")
- if not tokens:
- return b""
- return bytes(int(token, 16) for token in tokens)
+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():
@@ -167,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 = """