better-rtplot 0.4.15__tar.gz → 0.5.0__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.15 → better_rtplot-0.5.0}/PKG-INFO +9 -6
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/README.md +6 -4
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/pyproject.toml +1 -1
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/client.py +173 -9
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/server_browser.py +273 -51
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/index.html +98 -45
- better_rtplot-0.5.0/rtplot/static/ui-view.css +24 -0
- better_rtplot-0.5.0/rtplot/static/ui-view.js +138 -0
- better_rtplot-0.5.0/rtplot/ui_state.py +102 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/LICENSE +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/server.py +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/auto-render.min.js +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/katex.min.css +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/katex/katex.min.js +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.4.15 → better_rtplot-0.5.0}/rtplot/static/uPlot.min.css +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: better-rtplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary:
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: jmontp
|
|
7
8
|
Author-email: jmontp@umich.edu
|
|
8
9
|
Requires-Python: >=3.9,<3.13
|
|
@@ -67,10 +68,11 @@ Sender and server don't have to be on the same machine — see the
|
|
|
67
68
|
No Python needed on the viewing machine. On Windows the binary opens
|
|
68
69
|
a small Tk status window showing the listening URL.
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
the
|
|
73
|
-
[multi-column example](examples/05_multi_column/)
|
|
71
|
+
Set columns per row in your sender's layout with
|
|
72
|
+
`PlotRow([Plot(names=["a"]), Plot(names=["b"])], columns=2)`.
|
|
73
|
+
Start the server normally; each row can use a different column count.
|
|
74
|
+
See the [multi-column example](examples/05_multi_column/) for typed and
|
|
75
|
+
dict forms, including a layout with two, one, and three columns.
|
|
74
76
|
|
|
75
77
|
**Client** — pip install in the env that runs your script:
|
|
76
78
|
|
|
@@ -130,6 +132,7 @@ A rising line now draws itself in the browser tab.
|
|
|
130
132
|
- **Remote-friendly.** Sender or plot host can bind. Live Bind /
|
|
131
133
|
Connect buttons retarget without restart.
|
|
132
134
|
- **Config lives with the data.** The sender declares plot layout.
|
|
135
|
+
- **Sections and runtime state.** Optional collapsible sections, persistent status controls, and presentation updates that preserve streaming history. See the [section API](docs/sections.md) and [workflow example](examples/06_sections/README.md).
|
|
133
136
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
134
137
|
from your loop, no threads, no callbacks.
|
|
135
138
|
- **LaTeX labels.** Use `$...$` or `\(...\)` in plot and control text;
|
|
@@ -43,10 +43,11 @@ Sender and server don't have to be on the same machine — see the
|
|
|
43
43
|
No Python needed on the viewing machine. On Windows the binary opens
|
|
44
44
|
a small Tk status window showing the listening URL.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
the
|
|
49
|
-
[multi-column example](examples/05_multi_column/)
|
|
46
|
+
Set columns per row in your sender's layout with
|
|
47
|
+
`PlotRow([Plot(names=["a"]), Plot(names=["b"])], columns=2)`.
|
|
48
|
+
Start the server normally; each row can use a different column count.
|
|
49
|
+
See the [multi-column example](examples/05_multi_column/) for typed and
|
|
50
|
+
dict forms, including a layout with two, one, and three columns.
|
|
50
51
|
|
|
51
52
|
**Client** — pip install in the env that runs your script:
|
|
52
53
|
|
|
@@ -106,6 +107,7 @@ A rising line now draws itself in the browser tab.
|
|
|
106
107
|
- **Remote-friendly.** Sender or plot host can bind. Live Bind /
|
|
107
108
|
Connect buttons retarget without restart.
|
|
108
109
|
- **Config lives with the data.** The sender declares plot layout.
|
|
110
|
+
- **Sections and runtime state.** Optional collapsible sections, persistent status controls, and presentation updates that preserve streaming history. See the [section API](docs/sections.md) and [workflow example](examples/06_sections/README.md).
|
|
109
111
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
110
112
|
from your loop, no threads, no callbacks.
|
|
111
113
|
- **LaTeX labels.** Use `$...$` or `\(...\)` in plot and control text;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import zmq
|
|
2
2
|
import numpy as np
|
|
3
3
|
import time
|
|
4
|
+
import uuid
|
|
5
|
+
import warnings
|
|
6
|
+
from .ui_state import META_KEY, declarations, merge_state
|
|
4
7
|
from collections import OrderedDict, namedtuple
|
|
5
8
|
from dataclasses import dataclass, field
|
|
6
9
|
from typing import List, Optional, Tuple, Union
|
|
@@ -24,6 +27,7 @@ control_socket.setsockopt(zmq.RCVHWM, 1000)
|
|
|
24
27
|
#Non-blocking local state for controls, drained on each poll_controls() call
|
|
25
28
|
_control_values = {}
|
|
26
29
|
_control_button_events = []
|
|
30
|
+
_last_control_poll = time.monotonic()
|
|
27
31
|
|
|
28
32
|
# Cached payload of the most recent initialize_plots() call. The server
|
|
29
33
|
# uses this to recover from its own crashes: when its tab loses the
|
|
@@ -91,6 +95,16 @@ SENDING_PLOT_UPDATE = "0"
|
|
|
91
95
|
SENDING_DATA = "1"
|
|
92
96
|
SENDING_DISPLAY = "4"
|
|
93
97
|
SENDING_TEXT_INPUT = "5"
|
|
98
|
+
SENDING_UI_STATE = "6"
|
|
99
|
+
_session = uuid.uuid4().hex
|
|
100
|
+
_generation = 0
|
|
101
|
+
_ui_revision = 0
|
|
102
|
+
_ui_state = {"controls": {}, "sections": {}}
|
|
103
|
+
_view = {}
|
|
104
|
+
_control_registry = {}
|
|
105
|
+
_server_capabilities = set()
|
|
106
|
+
_source_epoch = None
|
|
107
|
+
_last_ui_publish = 0.0
|
|
94
108
|
|
|
95
109
|
#Lightweight result type returned by poll_controls()
|
|
96
110
|
ControlState = namedtuple("ControlState", ["values", "buttons"])
|
|
@@ -147,6 +161,7 @@ class Plot:
|
|
|
147
161
|
yrange: Optional[Tuple[float, float]] = None
|
|
148
162
|
xrange: Optional[int] = None
|
|
149
163
|
height: Optional[float] = None
|
|
164
|
+
section: Optional[str] = None
|
|
150
165
|
|
|
151
166
|
def to_dict(self):
|
|
152
167
|
return _drop_none({
|
|
@@ -161,9 +176,33 @@ class Plot:
|
|
|
161
176
|
"yrange": _range_to_list(self.yrange),
|
|
162
177
|
"xrange": self.xrange,
|
|
163
178
|
"height": self.height,
|
|
179
|
+
"section": self.section,
|
|
164
180
|
})
|
|
165
181
|
|
|
166
182
|
|
|
183
|
+
@dataclass
|
|
184
|
+
class PlotRow:
|
|
185
|
+
"""A group of plots with its own column count in the browser."""
|
|
186
|
+
plots: List[Union[Plot, dict]]
|
|
187
|
+
columns: int
|
|
188
|
+
section: Optional[str] = None
|
|
189
|
+
|
|
190
|
+
def to_dict(self):
|
|
191
|
+
if isinstance(self.columns, bool) or not isinstance(self.columns, int) or self.columns < 1:
|
|
192
|
+
raise ValueError("PlotRow.columns must be a positive integer")
|
|
193
|
+
if not self.plots:
|
|
194
|
+
raise ValueError("PlotRow.plots must contain at least one plot")
|
|
195
|
+
plots = [
|
|
196
|
+
p.to_dict() if hasattr(p, "to_dict") else p
|
|
197
|
+
for p in self.plots
|
|
198
|
+
]
|
|
199
|
+
if any(not isinstance(p, dict) or "names" not in p
|
|
200
|
+
or any(k in p for k in ("plots", "controls", "non_plot_labels"))
|
|
201
|
+
for p in plots):
|
|
202
|
+
raise ValueError("PlotRow can contain only plot descriptions")
|
|
203
|
+
return _drop_none({"columns": self.columns, "plots": plots, "section": self.section})
|
|
204
|
+
|
|
205
|
+
|
|
167
206
|
@dataclass
|
|
168
207
|
class Button:
|
|
169
208
|
id: str
|
|
@@ -272,11 +311,37 @@ class ControlsRow:
|
|
|
272
311
|
"""A row of control widgets, rendered in place of a plot."""
|
|
273
312
|
controls: List[Union[Button, Slider, Dial, Display, Text, TextInput, dict]] = field(default_factory=list)
|
|
274
313
|
|
|
314
|
+
section: Optional[str] = None
|
|
315
|
+
|
|
275
316
|
def to_dict(self):
|
|
276
|
-
return {"controls": [
|
|
317
|
+
return _drop_none({"controls": [
|
|
277
318
|
c.to_dict() if hasattr(c, "to_dict") else c
|
|
278
319
|
for c in self.controls
|
|
279
|
-
]}
|
|
320
|
+
], "section": self.section})
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@dataclass
|
|
324
|
+
class Section:
|
|
325
|
+
id: str
|
|
326
|
+
title: str
|
|
327
|
+
description: str = ""
|
|
328
|
+
collapsible: bool = False
|
|
329
|
+
collapsed: bool = False
|
|
330
|
+
visible: bool = True
|
|
331
|
+
|
|
332
|
+
def to_dict(self):
|
|
333
|
+
return dict(vars(self))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@dataclass
|
|
337
|
+
class View:
|
|
338
|
+
sections: List[Union[Section, dict]]
|
|
339
|
+
persistent_section: Optional[str] = None
|
|
340
|
+
|
|
341
|
+
def to_dict(self):
|
|
342
|
+
return _drop_none({"sections": [s.to_dict() if hasattr(s, "to_dict") else s
|
|
343
|
+
for s in self.sections],
|
|
344
|
+
"persistent_section": self.persistent_section})
|
|
280
345
|
|
|
281
346
|
|
|
282
347
|
def local_plot():
|
|
@@ -426,6 +491,8 @@ def send_array(A, flags=0, copy=True, track=False):
|
|
|
426
491
|
shape = A.shape,
|
|
427
492
|
)
|
|
428
493
|
|
|
494
|
+
_publish_ui_if_due()
|
|
495
|
+
|
|
429
496
|
#Send category
|
|
430
497
|
socket.send_string(SENDING_DATA)
|
|
431
498
|
#Send json description
|
|
@@ -434,7 +501,7 @@ def send_array(A, flags=0, copy=True, track=False):
|
|
|
434
501
|
socket.send(A, flags, copy=copy, track=track)
|
|
435
502
|
|
|
436
503
|
|
|
437
|
-
def initialize_plots(plot_descriptions=1, handshake_timeout=2.0):
|
|
504
|
+
def initialize_plots(plot_descriptions=1, handshake_timeout=2.0, *, view=None, ui_state=None):
|
|
438
505
|
"""Send a json description of desired plot and block until the
|
|
439
506
|
server acknowledges it.
|
|
440
507
|
|
|
@@ -455,13 +522,21 @@ def initialize_plots(plot_descriptions=1, handshake_timeout=2.0):
|
|
|
455
522
|
- list of str: one plot, one trace per name
|
|
456
523
|
- list of list of str: one plot per sublist
|
|
457
524
|
- list of dict: multiple plots, each with full styling
|
|
525
|
+
- PlotRow or {"plots": [...], "columns": N}: a row with N columns
|
|
458
526
|
handshake_timeout:
|
|
459
527
|
Max seconds to wait for the config_ack. On timeout we print a
|
|
460
528
|
warning and return (so an old server without the handshake
|
|
461
529
|
feature, or a server on a flaky network, doesn't block the
|
|
462
530
|
caller forever). Set to 0 to skip the handshake entirely.
|
|
531
|
+
view:
|
|
532
|
+
Optional View or dictionary declaring sections and a persistent section.
|
|
533
|
+
Requires a browser server >= 0.5.0; unconfirmed support emits a warning.
|
|
534
|
+
ui_state:
|
|
535
|
+
Optional initial controls/sections presentation overrides. Subsequent
|
|
536
|
+
set_ui_state() patches preserve plots and values; None resets overrides.
|
|
463
537
|
"""
|
|
464
|
-
global plot_desc_dict
|
|
538
|
+
global plot_desc_dict, _generation, _ui_revision, _ui_state, _view
|
|
539
|
+
global _control_registry, _server_capabilities, _source_epoch, _control_button_events, _last_control_poll
|
|
465
540
|
|
|
466
541
|
#Process int inputs
|
|
467
542
|
if isinstance(plot_descriptions,int):
|
|
@@ -511,7 +586,19 @@ def initialize_plots(plot_descriptions=1, handshake_timeout=2.0):
|
|
|
511
586
|
else:
|
|
512
587
|
raise TypeError("Incorrect usage of initialize_plots, verify github for usage")
|
|
513
588
|
|
|
589
|
+
_view, _control_registry = declarations(plot_desc_dict, view)
|
|
590
|
+
_ui_state = merge_state({"controls": {}, "sections": {}}, ui_state or {}, _control_registry, _view)
|
|
591
|
+
_generation += 1
|
|
592
|
+
_ui_revision = 0
|
|
593
|
+
_source_epoch = None
|
|
594
|
+
_server_capabilities = set()
|
|
595
|
+
_control_button_events = []
|
|
596
|
+
_last_control_poll = time.monotonic()
|
|
514
597
|
_send_initialize_with_handshake(plot_desc_dict, handshake_timeout)
|
|
598
|
+
if (view is not None or ui_state is not None) and "ui_state_v1" not in _server_capabilities:
|
|
599
|
+
warnings.warn("Sections/UI state require rtplot server >= 0.5.0. This server did not "
|
|
600
|
+
"confirm support: layout may be flat and disabled states are NOT enforced.",
|
|
601
|
+
RuntimeWarning, stacklevel=2)
|
|
515
602
|
|
|
516
603
|
|
|
517
604
|
def _send_initialize_with_handshake(cfg, timeout):
|
|
@@ -526,7 +613,7 @@ def _send_initialize_with_handshake(cfg, timeout):
|
|
|
526
613
|
|
|
527
614
|
def _send_once():
|
|
528
615
|
socket.send_string(SENDING_PLOT_UPDATE)
|
|
529
|
-
socket.send_json(cfg)
|
|
616
|
+
socket.send_json(_wire_config(cfg))
|
|
530
617
|
|
|
531
618
|
_send_once()
|
|
532
619
|
|
|
@@ -552,8 +639,9 @@ def _send_initialize_with_handshake(cfg, timeout):
|
|
|
552
639
|
continue
|
|
553
640
|
evtype = event.get("type")
|
|
554
641
|
if evtype == "config_ack":
|
|
555
|
-
|
|
556
|
-
|
|
642
|
+
if _accept_ack(event):
|
|
643
|
+
return
|
|
644
|
+
elif evtype == "button" and _current_event(event):
|
|
557
645
|
_control_button_events.append(event.get("id"))
|
|
558
646
|
elif evtype == "slider":
|
|
559
647
|
_control_values[event.get("id")] = float(event.get("value", 0.0))
|
|
@@ -574,6 +662,65 @@ def _send_initialize_with_handshake(cfg, timeout):
|
|
|
574
662
|
)
|
|
575
663
|
|
|
576
664
|
|
|
665
|
+
def _wire_config(cfg):
|
|
666
|
+
# Old servers already skip non_plot_labels entries. Metadata adds no traces.
|
|
667
|
+
return {**cfg, META_KEY: {"non_plot_labels": [], "rtplot": {
|
|
668
|
+
"session": _session, "generation": _generation, "view": _view,
|
|
669
|
+
"revision": _ui_revision, "state": _ui_state,
|
|
670
|
+
}}}
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _accept_ack(event):
|
|
674
|
+
global _server_capabilities, _source_epoch, _control_button_events
|
|
675
|
+
if event.get("session") is not None and (event.get("session"), event.get("generation")) != (_session, _generation):
|
|
676
|
+
return False
|
|
677
|
+
_server_capabilities = set(event.get("capabilities", []))
|
|
678
|
+
epoch = event.get("source_epoch")
|
|
679
|
+
if epoch != _source_epoch:
|
|
680
|
+
_control_button_events = []
|
|
681
|
+
_source_epoch = epoch
|
|
682
|
+
return True
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _current_event(event):
|
|
686
|
+
if "ui_state_v1" not in _server_capabilities:
|
|
687
|
+
return True
|
|
688
|
+
return (event.get("session"), event.get("generation"), event.get("source_epoch")) == (_session, _generation, _source_epoch)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _publish_ui_if_due(force=False):
|
|
692
|
+
global _last_ui_publish
|
|
693
|
+
if "ui_state_v1" not in _server_capabilities:
|
|
694
|
+
return
|
|
695
|
+
now = time.monotonic()
|
|
696
|
+
if not force and now - _last_ui_publish < 1.0:
|
|
697
|
+
return
|
|
698
|
+
socket.send_string(SENDING_UI_STATE, zmq.SNDMORE)
|
|
699
|
+
socket.send_json({"session": _session, "generation": _generation,
|
|
700
|
+
"revision": _ui_revision, "state": _ui_state})
|
|
701
|
+
_last_ui_publish = now
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def set_ui_state(patch):
|
|
705
|
+
"""Patch control/section presentation state without reinitializing plots.
|
|
706
|
+
|
|
707
|
+
None removes a property override (or all overrides for a target ID).
|
|
708
|
+
Returns whether the merged state changed. Call poll_controls regularly
|
|
709
|
+
while idle to maintain liveness and recover dropped presentation updates.
|
|
710
|
+
"""
|
|
711
|
+
global _ui_state, _ui_revision
|
|
712
|
+
if "ui_state_v1" not in _server_capabilities:
|
|
713
|
+
raise RuntimeError("set_ui_state requires an acknowledged rtplot server >= 0.5.0; "
|
|
714
|
+
"disabled-state enforcement is unavailable")
|
|
715
|
+
merged = merge_state(_ui_state, patch, _control_registry, _view)
|
|
716
|
+
changed = merged != _ui_state
|
|
717
|
+
if changed:
|
|
718
|
+
_ui_state = merged
|
|
719
|
+
_ui_revision += 1
|
|
720
|
+
_publish_ui_if_due(force=changed)
|
|
721
|
+
return changed
|
|
722
|
+
|
|
723
|
+
|
|
577
724
|
def set_display(display_id: str, value):
|
|
578
725
|
"""Push a single display box value to the browser.
|
|
579
726
|
|
|
@@ -623,7 +770,16 @@ def poll_controls():
|
|
|
623
770
|
restarting their script. The request is consumed silently — the
|
|
624
771
|
returned ControlState only ever contains control/button events.
|
|
625
772
|
"""
|
|
626
|
-
global _control_button_events
|
|
773
|
+
global _control_button_events, _source_epoch, _last_control_poll
|
|
774
|
+
now = time.monotonic()
|
|
775
|
+
stale_poll = "ui_state_v1" in _server_capabilities and now - _last_control_poll > 5.0
|
|
776
|
+
_last_control_poll = now
|
|
777
|
+
if stale_poll:
|
|
778
|
+
# An idle/disconnected application's PULL socket can still hold actions
|
|
779
|
+
# sent before the server detected the outage. Drain them without replay.
|
|
780
|
+
_control_button_events = []
|
|
781
|
+
_source_epoch = None
|
|
782
|
+
_publish_ui_if_due(force=stale_poll)
|
|
627
783
|
while True:
|
|
628
784
|
try:
|
|
629
785
|
event = control_socket.recv_json(flags=zmq.NOBLOCK)
|
|
@@ -632,6 +788,13 @@ def poll_controls():
|
|
|
632
788
|
except zmq.ZMQError:
|
|
633
789
|
break
|
|
634
790
|
evtype = event.get("type")
|
|
791
|
+
if stale_poll and evtype in ("config_ack", "button", "slider", "text"):
|
|
792
|
+
continue
|
|
793
|
+
if evtype == "config_ack":
|
|
794
|
+
_accept_ack(event)
|
|
795
|
+
continue
|
|
796
|
+
if evtype in ("button", "slider", "text") and not _current_event(event):
|
|
797
|
+
continue
|
|
635
798
|
if evtype == "button":
|
|
636
799
|
_control_button_events.append(event.get("id"))
|
|
637
800
|
elif evtype == "slider":
|
|
@@ -641,7 +804,8 @@ def poll_controls():
|
|
|
641
804
|
elif evtype == "resend_config":
|
|
642
805
|
if plot_desc_dict is not None:
|
|
643
806
|
socket.send_string(SENDING_PLOT_UPDATE)
|
|
644
|
-
|
|
807
|
+
_control_button_events = []
|
|
808
|
+
socket.send_json(_wire_config(plot_desc_dict))
|
|
645
809
|
# Surface a one-line confirmation so the user can see in
|
|
646
810
|
# their script log that the recovery handshake worked.
|
|
647
811
|
# If they never see this after clicking Reconnect, the
|