better-rtplot 0.4.8__tar.gz → 0.4.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/PKG-INFO +1 -1
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/pyproject.toml +1 -1
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/client.py +74 -6
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/server_browser.py +194 -5
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/index.html +15 -1
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/LICENSE +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/README.md +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/server.py +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/auto-render.min.js +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/katex.min.css +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/katex.min.js +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/uPlot.min.css +0 -0
|
@@ -414,8 +414,17 @@ def send_array(A, flags=0, copy=True, track=False):
|
|
|
414
414
|
socket.send(A, flags, copy=copy, track=track)
|
|
415
415
|
|
|
416
416
|
|
|
417
|
-
def initialize_plots(plot_descriptions=1):
|
|
418
|
-
"""Send a json description of desired plot
|
|
417
|
+
def initialize_plots(plot_descriptions=1, handshake_timeout=2.0):
|
|
418
|
+
"""Send a json description of desired plot and block until the
|
|
419
|
+
server acknowledges it.
|
|
420
|
+
|
|
421
|
+
Without the handshake, a fast script that calls ``initialize_plots()``
|
|
422
|
+
and immediately starts firing ``send_array()`` can lose the config
|
|
423
|
+
frame to ZMQ's PUB/SUB slow-joiner window — the server then drops
|
|
424
|
+
every subsequent data frame because no plot is declared yet and the
|
|
425
|
+
browser just stays blank. We resend the config on a short schedule
|
|
426
|
+
until the server PUSHes back a ``config_ack``, guaranteeing the
|
|
427
|
+
plot is live by the time this call returns.
|
|
419
428
|
|
|
420
429
|
Inputs
|
|
421
430
|
------
|
|
@@ -426,6 +435,11 @@ def initialize_plots(plot_descriptions=1):
|
|
|
426
435
|
- list of str: one plot, one trace per name
|
|
427
436
|
- list of list of str: one plot per sublist
|
|
428
437
|
- list of dict: multiple plots, each with full styling
|
|
438
|
+
handshake_timeout:
|
|
439
|
+
Max seconds to wait for the config_ack. On timeout we print a
|
|
440
|
+
warning and return (so an old server without the handshake
|
|
441
|
+
feature, or a server on a flaky network, doesn't block the
|
|
442
|
+
caller forever). Set to 0 to skip the handshake entirely.
|
|
429
443
|
"""
|
|
430
444
|
global plot_desc_dict
|
|
431
445
|
|
|
@@ -477,11 +491,65 @@ def initialize_plots(plot_descriptions=1):
|
|
|
477
491
|
else:
|
|
478
492
|
raise TypeError("Incorrect usage of initialize_plots, verify github for usage")
|
|
479
493
|
|
|
480
|
-
|
|
481
|
-
socket.send_string(SENDING_PLOT_UPDATE)
|
|
494
|
+
_send_initialize_with_handshake(plot_desc_dict, handshake_timeout)
|
|
482
495
|
|
|
483
|
-
|
|
484
|
-
|
|
496
|
+
|
|
497
|
+
def _send_initialize_with_handshake(cfg, timeout):
|
|
498
|
+
"""Publish ``cfg`` and block until the server acks it.
|
|
499
|
+
|
|
500
|
+
Any control events (button presses, slider updates) that happen to
|
|
501
|
+
arrive during the handshake are stashed in the module-level caches
|
|
502
|
+
so the next ``poll_controls()`` call still sees them — we don't
|
|
503
|
+
want the handshake to swallow real user interaction.
|
|
504
|
+
"""
|
|
505
|
+
global _control_button_events
|
|
506
|
+
|
|
507
|
+
def _send_once():
|
|
508
|
+
socket.send_string(SENDING_PLOT_UPDATE)
|
|
509
|
+
socket.send_json(cfg)
|
|
510
|
+
|
|
511
|
+
_send_once()
|
|
512
|
+
|
|
513
|
+
if timeout is None or timeout <= 0:
|
|
514
|
+
return # opt-out: fire-and-forget like the pre-handshake behavior
|
|
515
|
+
|
|
516
|
+
deadline = time.time() + timeout
|
|
517
|
+
resend_interval = 0.2
|
|
518
|
+
last_send = time.time()
|
|
519
|
+
|
|
520
|
+
poller = zmq.Poller()
|
|
521
|
+
poller.register(control_socket, zmq.POLLIN)
|
|
522
|
+
|
|
523
|
+
while time.time() < deadline:
|
|
524
|
+
remaining_ms = max(10, int((deadline - time.time()) * 1000))
|
|
525
|
+
next_resend_ms = max(1, int((last_send + resend_interval - time.time()) * 1000))
|
|
526
|
+
wait_ms = min(remaining_ms, next_resend_ms)
|
|
527
|
+
events = dict(poller.poll(wait_ms))
|
|
528
|
+
if control_socket in events:
|
|
529
|
+
try:
|
|
530
|
+
event = control_socket.recv_json(flags=zmq.NOBLOCK)
|
|
531
|
+
except (zmq.Again, zmq.ZMQError):
|
|
532
|
+
continue
|
|
533
|
+
evtype = event.get("type")
|
|
534
|
+
if evtype == "config_ack":
|
|
535
|
+
return
|
|
536
|
+
elif evtype == "button":
|
|
537
|
+
_control_button_events.append(event.get("id"))
|
|
538
|
+
elif evtype == "slider":
|
|
539
|
+
_control_slider_values[event.get("id")] = float(event.get("value", 0.0))
|
|
540
|
+
# Unknown / resend_config: ignore during handshake.
|
|
541
|
+
# Resend if the window elapsed without an ack; PUB/SUB will
|
|
542
|
+
# drop the first send during slow-join but retries land.
|
|
543
|
+
if time.time() - last_send >= resend_interval:
|
|
544
|
+
_send_once()
|
|
545
|
+
last_send = time.time()
|
|
546
|
+
|
|
547
|
+
print(
|
|
548
|
+
"rtplot.client: initialize_plots() timed out waiting for server ack"
|
|
549
|
+
f" after {timeout:.1f}s — the plot config may have been dropped."
|
|
550
|
+
" If you're on an older server (rtplot < 0.4.9) this warning is"
|
|
551
|
+
" expected and harmless."
|
|
552
|
+
)
|
|
485
553
|
|
|
486
554
|
|
|
487
555
|
def set_display(display_id: str, value):
|
|
@@ -23,6 +23,8 @@ import asyncio
|
|
|
23
23
|
import dataclasses
|
|
24
24
|
import json
|
|
25
25
|
import os
|
|
26
|
+
import socket
|
|
27
|
+
import subprocess
|
|
26
28
|
import struct
|
|
27
29
|
import sys
|
|
28
30
|
import time
|
|
@@ -36,6 +38,7 @@ from typing import Optional
|
|
|
36
38
|
import numpy as np
|
|
37
39
|
import zmq
|
|
38
40
|
import zmq.asyncio
|
|
41
|
+
from zmq.utils.monitor import recv_monitor_message
|
|
39
42
|
|
|
40
43
|
# pyzmq's asyncio integration needs event_loop.add_reader(), which the
|
|
41
44
|
# Windows-default ProactorEventLoop (Python 3.8+) does not implement.
|
|
@@ -206,6 +209,7 @@ MSG_DELTA = 1
|
|
|
206
209
|
|
|
207
210
|
ZMQ_DEFAULT_PORT = 5555
|
|
208
211
|
ZMQ_CONTROL_PORT = ZMQ_DEFAULT_PORT + 1
|
|
212
|
+
TCP_PROBE_TIMEOUT = 0.5
|
|
209
213
|
|
|
210
214
|
RECEIVED_PLOT_UPDATE = 0
|
|
211
215
|
RECEIVED_DATA = 1
|
|
@@ -262,6 +266,84 @@ def _control_target_from_data(ip):
|
|
|
262
266
|
return f"{raw}:{ZMQ_CONTROL_PORT}"
|
|
263
267
|
|
|
264
268
|
|
|
269
|
+
def _host_port_from_endpoint(endpoint, default_port=ZMQ_DEFAULT_PORT):
|
|
270
|
+
"""Parse ``tcp://host:port`` or ``host[:port]`` into ``(host, port)``."""
|
|
271
|
+
raw = endpoint.strip()
|
|
272
|
+
if raw.startswith("tcp://"):
|
|
273
|
+
raw = raw[len("tcp://"):]
|
|
274
|
+
if raw.count(":") >= 1:
|
|
275
|
+
host, port_str = raw.rsplit(":", 1)
|
|
276
|
+
else:
|
|
277
|
+
host, port_str = raw, str(default_port)
|
|
278
|
+
try:
|
|
279
|
+
port = int(port_str)
|
|
280
|
+
except ValueError:
|
|
281
|
+
port = default_port
|
|
282
|
+
return host, port
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _probe_tcp_endpoint(endpoint, timeout=TCP_PROBE_TIMEOUT):
|
|
286
|
+
"""Return ``(open, reachable, error)`` for a TCP endpoint.
|
|
287
|
+
|
|
288
|
+
``Connection refused`` still proves the host answered at the network
|
|
289
|
+
layer, so it is reachable even though that particular port is closed.
|
|
290
|
+
"""
|
|
291
|
+
host, port = _host_port_from_endpoint(endpoint)
|
|
292
|
+
try:
|
|
293
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
294
|
+
return True, True, None
|
|
295
|
+
except ConnectionRefusedError as exc:
|
|
296
|
+
return False, True, str(exc)
|
|
297
|
+
except OSError as exc:
|
|
298
|
+
return False, False, str(exc)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _ping_host(host):
|
|
302
|
+
"""Best-effort host reachability fallback for firewalled TCP ports."""
|
|
303
|
+
if not host or host == "*":
|
|
304
|
+
return False
|
|
305
|
+
if sys.platform == "win32":
|
|
306
|
+
cmd = ["ping", "-n", "1", "-w", "700", host]
|
|
307
|
+
else:
|
|
308
|
+
cmd = ["ping", "-c", "1", "-W", "1", host]
|
|
309
|
+
try:
|
|
310
|
+
return subprocess.run(
|
|
311
|
+
cmd,
|
|
312
|
+
stdout=subprocess.DEVNULL,
|
|
313
|
+
stderr=subprocess.DEVNULL,
|
|
314
|
+
timeout=1.5,
|
|
315
|
+
check=False,
|
|
316
|
+
).returncode == 0
|
|
317
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _probe_connect_target(endpoint):
|
|
322
|
+
"""Classify a connect-mode target for the browser status dot.
|
|
323
|
+
|
|
324
|
+
Returns ``(host_reachable, rtplot_ports_open, error)``. A reachable host
|
|
325
|
+
with closed rtplot ports is still a usable tab: ZeroMQ can connect now and
|
|
326
|
+
auto-complete the connection later when the sender starts listening.
|
|
327
|
+
"""
|
|
328
|
+
host, data_port = _host_port_from_endpoint(endpoint)
|
|
329
|
+
data_open, data_reachable, data_err = _probe_tcp_endpoint(endpoint)
|
|
330
|
+
ctrl_endpoint = _control_target_from_data(endpoint)
|
|
331
|
+
ctrl_open, ctrl_reachable, ctrl_err = _probe_tcp_endpoint(
|
|
332
|
+
ctrl_endpoint, timeout=TCP_PROBE_TIMEOUT
|
|
333
|
+
)
|
|
334
|
+
host_reachable = data_reachable or ctrl_reachable
|
|
335
|
+
if not host_reachable and _ping_host(host):
|
|
336
|
+
host_reachable = True
|
|
337
|
+
if not host_reachable:
|
|
338
|
+
return (
|
|
339
|
+
False,
|
|
340
|
+
False,
|
|
341
|
+
f"{host} unreachable (data {data_port}: {data_err}; "
|
|
342
|
+
f"control {data_port + 1}: {ctrl_err})",
|
|
343
|
+
)
|
|
344
|
+
return True, bool(data_open and ctrl_open), None
|
|
345
|
+
|
|
346
|
+
|
|
265
347
|
###############################
|
|
266
348
|
# Tab model #
|
|
267
349
|
###############################
|
|
@@ -274,7 +356,7 @@ class Tab:
|
|
|
274
356
|
name: str
|
|
275
357
|
mode: str # "bind" or "connect"
|
|
276
358
|
endpoint: str # user-visible label, e.g. "*:5555" or "pi1:5555"
|
|
277
|
-
status: str = "idle" # idle | streaming | error
|
|
359
|
+
status: str = "idle" # idle | connecting | connected | streaming | error
|
|
278
360
|
error: Optional[str] = None
|
|
279
361
|
|
|
280
362
|
# Plot state
|
|
@@ -302,6 +384,7 @@ class Tab:
|
|
|
302
384
|
data_sock: Optional[zmq.Socket] = None
|
|
303
385
|
ctrl_sock: Optional[zmq.Socket] = None
|
|
304
386
|
receiver_task: Optional[asyncio.Task] = None
|
|
387
|
+
monitor_task: Optional[asyncio.Task] = None
|
|
305
388
|
|
|
306
389
|
# Data buffer, lazy-allocated on first use (roughly 4 GB virtual,
|
|
307
390
|
# but pages in only when written on Linux/Windows — a single tab
|
|
@@ -618,9 +701,38 @@ def _open_tab_sockets(tab: Tab):
|
|
|
618
701
|
# Close any prior sockets first (e.g. on retry).
|
|
619
702
|
_close_tab_sockets(tab)
|
|
620
703
|
|
|
704
|
+
connect_label = tab.endpoint
|
|
705
|
+
connect_data_ep = None
|
|
706
|
+
if tab.mode == "connect":
|
|
707
|
+
try:
|
|
708
|
+
connect_data_ep, connect_label = _normalize_connect_target(tab.endpoint)
|
|
709
|
+
except Exception as exc: # noqa: BLE001
|
|
710
|
+
tab.status = "error"
|
|
711
|
+
tab.error = f"invalid endpoint: {exc}"
|
|
712
|
+
print(f"[{tab.id}] ZMQ open failed: {tab.error}")
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
tab.endpoint = connect_label
|
|
716
|
+
tab.status = "connecting"
|
|
717
|
+
tab.error = None
|
|
718
|
+
host_reachable, rtplot_ports_open, peer_err = _probe_connect_target(connect_label)
|
|
719
|
+
if not host_reachable:
|
|
720
|
+
tab.status = "error"
|
|
721
|
+
tab.error = peer_err
|
|
722
|
+
print(f"[{tab.id}] peer probe failed: {peer_err}")
|
|
723
|
+
return
|
|
724
|
+
if not rtplot_ports_open:
|
|
725
|
+
tab.status = "idle"
|
|
726
|
+
tab.error = None
|
|
727
|
+
print(
|
|
728
|
+
f"[{tab.id}] host reachable, rtplot ports closed;"
|
|
729
|
+
f" waiting for ZMQ peer: {connect_label}"
|
|
730
|
+
)
|
|
731
|
+
|
|
621
732
|
last_exc: Exception | None = None
|
|
622
733
|
attempts = 4 if tab.mode == "bind" else 1
|
|
623
734
|
for attempt in range(attempts):
|
|
735
|
+
monitor = None
|
|
624
736
|
try:
|
|
625
737
|
data = zmq_ctx.socket(zmq.SUB)
|
|
626
738
|
data.setsockopt_string(zmq.SUBSCRIBE, "")
|
|
@@ -635,14 +747,20 @@ def _open_tab_sockets(tab: Tab):
|
|
|
635
747
|
tab.endpoint = f"*:{ZMQ_DEFAULT_PORT}"
|
|
636
748
|
print(f"[{tab.id}] ZMQ: bound on tcp://*:{ZMQ_DEFAULT_PORT}")
|
|
637
749
|
else:
|
|
638
|
-
|
|
639
|
-
|
|
750
|
+
monitor = data.get_monitor_socket(
|
|
751
|
+
zmq.EVENT_CONNECTED
|
|
752
|
+
| zmq.EVENT_DISCONNECTED
|
|
753
|
+
| zmq.EVENT_CONNECT_DELAYED
|
|
754
|
+
| zmq.EVENT_CONNECT_RETRIED
|
|
755
|
+
)
|
|
756
|
+
data.connect(connect_data_ep)
|
|
640
757
|
ctrl_ep, _ = _normalize_connect_target(
|
|
641
758
|
_control_target_from_data(tab.endpoint), port=ZMQ_CONTROL_PORT
|
|
642
759
|
)
|
|
643
760
|
ctrl.connect(ctrl_ep)
|
|
644
|
-
tab.endpoint =
|
|
645
|
-
|
|
761
|
+
tab.endpoint = connect_label
|
|
762
|
+
tab.monitor_task = asyncio.create_task(zmq_monitor(tab, monitor))
|
|
763
|
+
print(f"[{tab.id}] ZMQ: connecting to {connect_data_ep} (ctrl {ctrl_ep})")
|
|
646
764
|
|
|
647
765
|
tab.data_sock = data
|
|
648
766
|
tab.ctrl_sock = ctrl
|
|
@@ -657,6 +775,11 @@ def _open_tab_sockets(tab: Tab):
|
|
|
657
775
|
except Exception: pass
|
|
658
776
|
try: ctrl.close(0)
|
|
659
777
|
except Exception: pass
|
|
778
|
+
try:
|
|
779
|
+
if monitor is not None:
|
|
780
|
+
monitor.close(0)
|
|
781
|
+
except Exception:
|
|
782
|
+
pass
|
|
660
783
|
if attempt < attempts - 1:
|
|
661
784
|
wait = 0.15 * (attempt + 1)
|
|
662
785
|
print(
|
|
@@ -692,6 +815,62 @@ async def _cancel_task(task):
|
|
|
692
815
|
pass
|
|
693
816
|
|
|
694
817
|
|
|
818
|
+
async def zmq_monitor(tab: Tab, monitor_sock):
|
|
819
|
+
"""Track transport-level ZMQ state for connect-mode tabs."""
|
|
820
|
+
try:
|
|
821
|
+
while True:
|
|
822
|
+
evt = await recv_monitor_message(monitor_sock)
|
|
823
|
+
event = evt.get("event")
|
|
824
|
+
if event == zmq.EVENT_CONNECTED:
|
|
825
|
+
if tab.status in ("connecting", "idle") and not tab.initialized:
|
|
826
|
+
tab.status = "connected"
|
|
827
|
+
tab.error = None
|
|
828
|
+
await broadcast_tab(tab.id)
|
|
829
|
+
elif event == zmq.EVENT_DISCONNECTED:
|
|
830
|
+
if tab.mode == "connect":
|
|
831
|
+
host_reachable, rtplot_ports_open, peer_err = _probe_connect_target(tab.endpoint)
|
|
832
|
+
if host_reachable and not rtplot_ports_open:
|
|
833
|
+
tab.status = "idle"
|
|
834
|
+
tab.error = None
|
|
835
|
+
elif host_reachable:
|
|
836
|
+
tab.status = "idle"
|
|
837
|
+
tab.error = None
|
|
838
|
+
else:
|
|
839
|
+
tab.status = "error"
|
|
840
|
+
tab.error = peer_err
|
|
841
|
+
await broadcast_tab(tab.id)
|
|
842
|
+
elif event == zmq.EVENT_CONNECT_RETRIED:
|
|
843
|
+
if tab.mode == "connect" and tab.status == "error":
|
|
844
|
+
host_reachable, _rtplot_ports_open, peer_err = _probe_connect_target(tab.endpoint)
|
|
845
|
+
if host_reachable:
|
|
846
|
+
tab.status = "idle"
|
|
847
|
+
tab.error = None
|
|
848
|
+
else:
|
|
849
|
+
tab.error = peer_err
|
|
850
|
+
await broadcast_tab(tab.id)
|
|
851
|
+
elif event == zmq.EVENT_CONNECT_DELAYED:
|
|
852
|
+
if tab.mode == "connect" and tab.status == "error":
|
|
853
|
+
host_reachable, _rtplot_ports_open, peer_err = _probe_connect_target(tab.endpoint)
|
|
854
|
+
if host_reachable:
|
|
855
|
+
tab.status = "idle"
|
|
856
|
+
tab.error = None
|
|
857
|
+
else:
|
|
858
|
+
tab.error = peer_err
|
|
859
|
+
await broadcast_tab(tab.id)
|
|
860
|
+
except asyncio.CancelledError:
|
|
861
|
+
raise
|
|
862
|
+
except Exception as exc: # noqa: BLE001
|
|
863
|
+
if tab.mode == "connect" and tab.status != "streaming":
|
|
864
|
+
tab.status = "error"
|
|
865
|
+
tab.error = f"ZMQ monitor error: {exc}"
|
|
866
|
+
await broadcast_tab(tab.id)
|
|
867
|
+
finally:
|
|
868
|
+
try:
|
|
869
|
+
monitor_sock.close(0)
|
|
870
|
+
except Exception: # noqa: BLE001
|
|
871
|
+
pass
|
|
872
|
+
|
|
873
|
+
|
|
695
874
|
async def send_control_event(tab: Tab, event):
|
|
696
875
|
"""Forward a control event to the user's Python process, best-effort."""
|
|
697
876
|
if tab.ctrl_sock is None:
|
|
@@ -780,6 +959,12 @@ async def zmq_receiver(tab: Tab):
|
|
|
780
959
|
last_time = now
|
|
781
960
|
tab.status = "streaming"
|
|
782
961
|
tab.error = None
|
|
962
|
+
# Blocking-handshake ack: tells initialize_plots() on the
|
|
963
|
+
# client side that the config made it past PUB/SUB's
|
|
964
|
+
# slow-joiner window so it can stop resending and return.
|
|
965
|
+
# Old clients ignore unknown event types, so this is safe
|
|
966
|
+
# to emit unconditionally.
|
|
967
|
+
await send_control_event(tab, {"type": "config_ack"})
|
|
783
968
|
await broadcast_text_tab(tab.id, tab.config_message)
|
|
784
969
|
await broadcast_tab(tab.id)
|
|
785
970
|
snap = make_snapshot_message(tab)
|
|
@@ -997,6 +1182,7 @@ async def delete_tab(tab_id: str):
|
|
|
997
1182
|
if t is None:
|
|
998
1183
|
return
|
|
999
1184
|
await _cancel_task(t.receiver_task)
|
|
1185
|
+
await _cancel_task(t.monitor_task)
|
|
1000
1186
|
_close_tab_sockets(t)
|
|
1001
1187
|
# Move any viewers off this tab back to bind_me on the browser side;
|
|
1002
1188
|
# server tells them via tab_removed + they re-subscribe.
|
|
@@ -1085,6 +1271,8 @@ async def reconnect_tab(tab_id: str):
|
|
|
1085
1271
|
if t is None:
|
|
1086
1272
|
return
|
|
1087
1273
|
await _cancel_task(t.receiver_task)
|
|
1274
|
+
await _cancel_task(t.monitor_task)
|
|
1275
|
+
t.monitor_task = None
|
|
1088
1276
|
# _open_tab_sockets closes first, then reopens. Status gets set to
|
|
1089
1277
|
# "idle" on success or "error" + a message on bind/connect failure.
|
|
1090
1278
|
_open_tab_sockets(t)
|
|
@@ -1461,6 +1649,7 @@ async def on_cleanup(app):
|
|
|
1461
1649
|
await _cancel_task(app.get(key))
|
|
1462
1650
|
for t in list(tabs.values()):
|
|
1463
1651
|
await _cancel_task(t.receiver_task)
|
|
1652
|
+
await _cancel_task(t.monitor_task)
|
|
1464
1653
|
_close_tab_sockets(t)
|
|
1465
1654
|
for ws in list(ws_clients):
|
|
1466
1655
|
try:
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
.tab.active { background: #fff; color: #111; border-color: #bbb; font-weight: 600; }
|
|
16
16
|
.tab .tab-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #bbb; flex: 0 0 auto; }
|
|
17
17
|
.tab .tab-dot.idle { background: #bbb; }
|
|
18
|
+
.tab .tab-dot.connected { background: #e8b13a; box-shadow: 0 0 3px rgba(232,177,58,0.5); }
|
|
18
19
|
.tab .tab-dot.streaming { background: #2ebf4e; box-shadow: 0 0 3px rgba(46,191,78,0.5); }
|
|
19
20
|
.tab .tab-dot.error { background: #e05252; }
|
|
20
21
|
.tab .tab-dot.connecting { background: #e8b13a; animation: pulse 1s ease-in-out infinite alternate; }
|
|
@@ -987,6 +988,19 @@
|
|
|
987
988
|
}
|
|
988
989
|
|
|
989
990
|
// ---- Tab bar rendering ----
|
|
991
|
+
function tabStatusTitle(t) {
|
|
992
|
+
if (t.error) return t.error;
|
|
993
|
+
if (t.status === 'streaming') return 'Receiving rtplot data';
|
|
994
|
+
if (t.status === 'connected') return 'ZMQ connected; waiting for plot config/data';
|
|
995
|
+
if (t.status === 'connecting') return 'Checking device connection';
|
|
996
|
+
if (t.status === 'idle') {
|
|
997
|
+
return t.mode === 'bind'
|
|
998
|
+
? 'Listening for a sender'
|
|
999
|
+
: 'Host reachable; rtplot ports are not connected';
|
|
1000
|
+
}
|
|
1001
|
+
return 'Status unknown';
|
|
1002
|
+
}
|
|
1003
|
+
|
|
990
1004
|
function renderTabs() {
|
|
991
1005
|
tabbar.innerHTML = '';
|
|
992
1006
|
// Stable order: bind_me first, then others in insertion order.
|
|
@@ -1002,7 +1016,7 @@
|
|
|
1002
1016
|
|
|
1003
1017
|
const dot = document.createElement('span');
|
|
1004
1018
|
dot.className = 'tab-dot ' + (t.status || 'idle');
|
|
1005
|
-
|
|
1019
|
+
dot.title = tabStatusTitle(t);
|
|
1006
1020
|
el.appendChild(dot);
|
|
1007
1021
|
|
|
1008
1022
|
// Stacked labels: name on top, endpoint on a muted subline so
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.8 → better_rtplot-0.4.10}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|