better-rtplot 0.2.0__tar.gz → 0.2.2__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.2.2/PKG-INFO +538 -0
- better_rtplot-0.2.2/README.md +512 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/pyproject.toml +1 -1
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/interactive_test.py +9 -3
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/server_browser.py +168 -58
- better_rtplot-0.2.0/PKG-INFO +0 -278
- better_rtplot-0.2.0/README.md +0 -252
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/LICENSE +0 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/client.py +0 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/example_code.py +0 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/plot_log.py +0 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/server.py +0 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.2.0 → better_rtplot-0.2.2}/rtplot/static/uPlot.min.css +0 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: better-rtplot
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary:
|
|
5
|
+
License: GPL V3.0
|
|
6
|
+
Author: jmontp
|
|
7
|
+
Author-email: jmontp@umich.edu
|
|
8
|
+
Requires-Python: >=3.9,<3.13
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Provides-Extra: browser
|
|
16
|
+
Provides-Extra: server
|
|
17
|
+
Requires-Dist: aiohttp (>=3.9.0) ; extra == "browser"
|
|
18
|
+
Requires-Dist: numpy (>=1.23.5)
|
|
19
|
+
Requires-Dist: pandas (>=1.5.3) ; extra == "server" or extra == "browser"
|
|
20
|
+
Requires-Dist: pyarrow (>=11.0.0) ; extra == "server" or extra == "browser"
|
|
21
|
+
Requires-Dist: pyqtgraph (>=0.13.0) ; extra == "server"
|
|
22
|
+
Requires-Dist: pyside6 (>6.4.0) ; extra == "server"
|
|
23
|
+
Requires-Dist: pyzmq (>=25.0.0)
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
# rtplot — real-time plotting over ZMQ
|
|
29
|
+
|
|
30
|
+
**rtplot** lets a Python script push live data to a plot window — locally, or
|
|
31
|
+
across the network — with a few lines of code on the sender side. The plot
|
|
32
|
+
window runs in any modern browser and supports interactive controls
|
|
33
|
+
(buttons, sliders, dials, text and numeric displays) that feed values
|
|
34
|
+
back into the sending script in real time.
|
|
35
|
+
|
|
36
|
+
Typical use: a robot or data-acquisition script runs on a Raspberry Pi or
|
|
37
|
+
microcontroller host, and you watch live signals and tweak gains from a
|
|
38
|
+
laptop on the same network.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Table of contents
|
|
43
|
+
|
|
44
|
+
- [Highlights](#highlights)
|
|
45
|
+
- [Install](#install)
|
|
46
|
+
- [60-second quickstart](#60-second-quickstart)
|
|
47
|
+
- [Interactive controls](#interactive-controls)
|
|
48
|
+
- [Reading controls from Python](#reading-controls-from-python)
|
|
49
|
+
- [Pushing values into displays](#pushing-values-into-displays)
|
|
50
|
+
- [Element reference](#element-reference)
|
|
51
|
+
- [Plot configuration](#plot-configuration)
|
|
52
|
+
- [Sending data](#sending-data)
|
|
53
|
+
- [Saving data](#saving-data)
|
|
54
|
+
- [Networking modes](#networking-modes)
|
|
55
|
+
- [Viewing the plot from another device](#viewing-the-plot-from-another-device)
|
|
56
|
+
- [Performance tuning](#performance-tuning)
|
|
57
|
+
- [CLI reference](#cli-reference)
|
|
58
|
+
- [Examples](#examples)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Highlights
|
|
63
|
+
|
|
64
|
+
- **Fast.** Binary WebSocket deltas push data at up to 1 kHz. The
|
|
65
|
+
browser coalesces incoming samples into a single repaint per
|
|
66
|
+
`requestAnimationFrame`, so rendering runs at your monitor's refresh
|
|
67
|
+
rate (typically 60 Hz, 120 Hz on higher-refresh displays) regardless
|
|
68
|
+
of how fast samples arrive.
|
|
69
|
+
- **Browser-based.** The plot window is served by aiohttp and rendered
|
|
70
|
+
by uPlot in any modern browser. No desktop GUI toolkit to install,
|
|
71
|
+
works over SSH port forwarding out of the box.
|
|
72
|
+
- **Remote-friendly.** Either the sender or the plot host can bind —
|
|
73
|
+
pick whichever fits your network. Works across LAN, WSL, and SSH
|
|
74
|
+
tunnels.
|
|
75
|
+
- **Plot config lives with the data.** The sender declares the plot
|
|
76
|
+
layout, so a Pi running your experiment owns the look of its own
|
|
77
|
+
dashboards.
|
|
78
|
+
- **Interactive controls.** Declare buttons, sliders, dials,
|
|
79
|
+
numeric/text displays in the same `initialize_plots` call. Poll from
|
|
80
|
+
your tight loop; no threads, no callbacks.
|
|
81
|
+
- **Save to Parquet** with a single button click or `client.save_plot()`
|
|
82
|
+
call.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Install
|
|
87
|
+
|
|
88
|
+
Install rtplot with the server bundle — this is the normal path and
|
|
89
|
+
gets you everything:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install "better-rtplot[browser]"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This pulls `aiohttp` (for serving the plot UI) plus `pandas` + `pyarrow`
|
|
96
|
+
(for saving runs to Parquet). If you only need the sender side — your
|
|
97
|
+
script pushes data to someone else's plot host and you don't run a
|
|
98
|
+
server locally — you can install the client-only minimum instead:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install better-rtplot
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
In that case, if you later try to launch a server locally you'll get a
|
|
105
|
+
clear error telling you to add the `[browser]` extra.
|
|
106
|
+
|
|
107
|
+
WSL users: nothing extra needed. The plot window is served by HTTP, so
|
|
108
|
+
just open the URL rtplot prints in your Windows browser.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 60-second quickstart
|
|
113
|
+
|
|
114
|
+
**Terminal 1 — start the plot server:**
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
python -m rtplot.server_browser
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It prints a URL like `http://localhost:8050` — open that in your
|
|
121
|
+
browser. The page stays blank until a client sends a plot config.
|
|
122
|
+
|
|
123
|
+
**Terminal 2 — send data:**
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from rtplot import client
|
|
127
|
+
import numpy as np, time
|
|
128
|
+
|
|
129
|
+
client.local_plot() # send to the server on 127.0.0.1
|
|
130
|
+
client.initialize_plots(["sin", "cos"]) # one plot with two named traces
|
|
131
|
+
|
|
132
|
+
for i in range(10000):
|
|
133
|
+
t = i * 0.01
|
|
134
|
+
client.send_array([np.sin(t), np.cos(t)])
|
|
135
|
+
time.sleep(0.01)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
That's it. The browser tab you opened will start drawing the two
|
|
139
|
+
traces in real time.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Interactive controls
|
|
144
|
+
|
|
145
|
+
Declare a control row inline in your plot layout:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from rtplot import client
|
|
149
|
+
import numpy as np, time
|
|
150
|
+
|
|
151
|
+
client.local_plot()
|
|
152
|
+
client.initialize_plots([
|
|
153
|
+
{"names": ["signal"], "yrange": [-6, 6]},
|
|
154
|
+
{"controls": [
|
|
155
|
+
{"type": "button", "id": "reset", "label": "Reset"},
|
|
156
|
+
{"type": "button", "id": "pause", "label": "Pause"},
|
|
157
|
+
{"type": "slider", "id": "gain", "label": "Gain",
|
|
158
|
+
"min": 0, "max": 5, "value": 1.0, "step": 0.1, "format": "{:.2f}"},
|
|
159
|
+
]},
|
|
160
|
+
{"controls": [
|
|
161
|
+
{"type": "dial", "id": "freq", "label": "Freq (Hz)",
|
|
162
|
+
"min": 0.1, "max": 5.0, "value": 1.0, "step": 0.05,
|
|
163
|
+
"sensitivity": 0.5, "format": "{:.2f}"},
|
|
164
|
+
{"type": "display", "id": "t", "label": "t (s)", "format": "{:.2f}"},
|
|
165
|
+
{"type": "text", "id": "msg", "label": "Status",
|
|
166
|
+
"value": "running"},
|
|
167
|
+
]},
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
running = True
|
|
171
|
+
t0 = time.time()
|
|
172
|
+
while True:
|
|
173
|
+
ctrl = client.poll_controls()
|
|
174
|
+
for btn in ctrl.buttons:
|
|
175
|
+
if btn == "reset": t0 = time.time()
|
|
176
|
+
if btn == "pause": running = not running
|
|
177
|
+
|
|
178
|
+
gain = ctrl.values.get("gain", 1.0)
|
|
179
|
+
freq = ctrl.values.get("freq", 1.0)
|
|
180
|
+
t = time.time() - t0
|
|
181
|
+
amp = gain * np.sin(2 * np.pi * freq * t) if running else 0.0
|
|
182
|
+
|
|
183
|
+
client.set_display("t", t)
|
|
184
|
+
client.set_display("msg", "paused" if not running else "running")
|
|
185
|
+
client.send_array(amp)
|
|
186
|
+
time.sleep(0.01)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Reading controls from Python
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
ctrl = client.poll_controls() # non-blocking, cheap to call every loop
|
|
193
|
+
gain = ctrl.values.get("gain", 1.0) # latest slider/dial value
|
|
194
|
+
for btn_id in ctrl.buttons: # list of buttons fired since last poll
|
|
195
|
+
handle(btn_id)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`poll_controls()` returns a `ControlState(values, buttons)` namedtuple:
|
|
199
|
+
|
|
200
|
+
- `values` — a `dict` of `{element_id: float}` for every slider and dial
|
|
201
|
+
the server has told the client about. Defaults declared in
|
|
202
|
+
`initialize_plots` are pre-seeded so the **first** call already sees
|
|
203
|
+
them.
|
|
204
|
+
- `buttons` — a `list` of button ids fired since the previous poll, in
|
|
205
|
+
order. The list is cleared on return, so each event is delivered
|
|
206
|
+
exactly once.
|
|
207
|
+
|
|
208
|
+
Call it from your tight loop before computing the next sample. No
|
|
209
|
+
threads, no callbacks, no missed events.
|
|
210
|
+
|
|
211
|
+
### Pushing values into displays
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
client.set_display("t", 12.34) # numeric display box
|
|
215
|
+
client.set_display("msg", "running") # text field
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`set_display()` accepts either a number (for `type: "display"` elements)
|
|
219
|
+
or a string (for `type: "text"` elements). Updates are coalesced on the
|
|
220
|
+
server and rebroadcast to every connected browser at ~30 Hz.
|
|
221
|
+
|
|
222
|
+
### Element reference
|
|
223
|
+
|
|
224
|
+
| Type | Purpose | Notable fields |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `button` | Fires a discrete event when clicked | `id`, `label` |
|
|
227
|
+
| `slider` | Scalar input via horizontal range | `id`, `label`, `min`, `max`, `value`, `step`, `format` |
|
|
228
|
+
| `dial` | Scalar input via rotational drag | same as slider, plus `sensitivity` (full turns per range sweep; default `1.0`) |
|
|
229
|
+
| `display` | Read-only numeric readout | `id`, `label`, `format` |
|
|
230
|
+
| `text` | Read-only text field (prompts, status) | `id`, `label`, `value` |
|
|
231
|
+
|
|
232
|
+
Slider and dial widgets both render as **`[widget] [−] [number input] [+]`**,
|
|
233
|
+
so you can drag, type a value directly, or nudge by `step`. The dial
|
|
234
|
+
accepts "round and round" circular drag — each full rotation walks the
|
|
235
|
+
value through `(max − min) × sensitivity`, so `sensitivity: 0.25` gives
|
|
236
|
+
you four rotations per sweep for fine control.
|
|
237
|
+
|
|
238
|
+
The `format` field accepts Python-style `{:.Nf}` strings (e.g. `"{:.2f}"`).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Plot configuration
|
|
243
|
+
|
|
244
|
+
Each entry in `initialize_plots` is one of:
|
|
245
|
+
|
|
246
|
+
- an **integer** — `client.initialize_plots(3)` → one plot with 3 anonymous
|
|
247
|
+
traces
|
|
248
|
+
- a **string** — `client.initialize_plots("torque")` → one plot with one
|
|
249
|
+
named trace
|
|
250
|
+
- a **list of strings** — one plot, one trace per name
|
|
251
|
+
- a **list of lists of strings** — one plot per sublist
|
|
252
|
+
- a **dict** — one plot, with full styling options (below)
|
|
253
|
+
- a **list of dicts** — multiple plots with full styling
|
|
254
|
+
|
|
255
|
+
A styled plot dict accepts any of:
|
|
256
|
+
|
|
257
|
+
| Key | Meaning |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `names` | **Required.** List of trace names. |
|
|
260
|
+
| `colors` | List of per-trace colors. Single letter (`r g b c m y k w`) or any CSS color string. |
|
|
261
|
+
| `line_style` | `"-"` for dashed, `""` (or anything else) for solid, per trace. |
|
|
262
|
+
| `line_width` | Per-trace line width in pixels. |
|
|
263
|
+
| `title` | Plot title. |
|
|
264
|
+
| `xlabel` / `ylabel` | Axis labels. |
|
|
265
|
+
| `yrange` | `[ymin, ymax]` — pins the Y axis and significantly speeds up rendering. |
|
|
266
|
+
| `xrange` | Integer number of samples visible at once (default 200). |
|
|
267
|
+
|
|
268
|
+
Special row entries (not plots themselves):
|
|
269
|
+
|
|
270
|
+
- `{"controls": [...]}` — a row of interactive controls (see
|
|
271
|
+
[Interactive controls](#interactive-controls))
|
|
272
|
+
- `{"non_plot_labels": ["name1", "name2"]}` — extra scalar names that ride
|
|
273
|
+
along with `send_array` and get saved into the output Parquet file, but
|
|
274
|
+
aren't rendered as traces
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Sending data
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
client.send_array(scalar) # float
|
|
282
|
+
client.send_array([a, b, c]) # 1-D list: one sample per trace
|
|
283
|
+
client.send_array(np.array([...])) # 1-D numpy array: one sample per trace
|
|
284
|
+
client.send_array(np.array([[...]]))# 2-D (num_traces, N): N samples at once
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Passing a 2-D array with `N > 1` lets you push a batch of samples per
|
|
288
|
+
`send_array` call, which is the fastest way to get many samples through
|
|
289
|
+
without dropping frames.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Saving data
|
|
294
|
+
|
|
295
|
+
The server saves every sample it has received since the latest
|
|
296
|
+
`initialize_plots` call to a Parquet file, including any
|
|
297
|
+
`non_plot_labels` data that rode along with your normal data.
|
|
298
|
+
|
|
299
|
+
Trigger a save from either side:
|
|
300
|
+
|
|
301
|
+
- **Browser UI:** click the **Save Plot** button.
|
|
302
|
+
- **Python:** `client.save_plot("my_run")`
|
|
303
|
+
|
|
304
|
+
Control where things get written:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
python -m rtplot.server_browser -sd ./saved_plots -sn experiment1
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- `-sd` / `--save-dir` — target directory
|
|
311
|
+
- `-sn` / `--save-name` — filename prefix (a timestamp is always appended)
|
|
312
|
+
|
|
313
|
+
### Save non-plot signals alongside the plotted ones
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
client.initialize_plots([
|
|
317
|
+
{"names": ["hip_angle", "knee_angle"]},
|
|
318
|
+
{"non_plot_labels": ["battery", "cpu_temp", "loop_latency"]},
|
|
319
|
+
])
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Send `battery`, `cpu_temp` and `loop_latency` as extra rows after the
|
|
323
|
+
plotted traces in each `send_array` call; they won't be drawn but they
|
|
324
|
+
will land in the Parquet file.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Networking modes
|
|
329
|
+
|
|
330
|
+
rtplot uses ZMQ, so either the sender or the plot host can be the one
|
|
331
|
+
that *binds* a socket. Pick whichever works for your network and
|
|
332
|
+
firewalls.
|
|
333
|
+
|
|
334
|
+
**Mode A — plot host binds, sender connects** *(typical for lab laptops)*
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# on the plot host (e.g. your laptop)
|
|
338
|
+
python -m rtplot.server_browser
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
# on the sender (e.g. the Pi)
|
|
343
|
+
from rtplot import client
|
|
344
|
+
client.configure_ip("192.168.1.42") # the laptop's LAN IP
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Mode B — sender binds, plot host connects** *(typical when the sender
|
|
348
|
+
has a static IP and the viewer roams around)*
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
# on the plot host
|
|
352
|
+
python -m rtplot.server_browser -p 192.168.1.50 # the sender's IP
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
# on the sender
|
|
357
|
+
from rtplot import client
|
|
358
|
+
# no configure_ip call needed — the default behavior binds
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
If you pass `-p host:port` to the server, rtplot also derives the control
|
|
362
|
+
return-channel endpoint from that same host/port (it uses `port+1`). This
|
|
363
|
+
means sliders, buttons, and dials work transparently in both modes with
|
|
364
|
+
no extra config.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Viewing the plot from another device
|
|
369
|
+
|
|
370
|
+
The section above is about the link between your *sender script* and the
|
|
371
|
+
*plot host* (the machine running `rtplot.server_browser`). This section
|
|
372
|
+
is about the other relationship: the link between the plot host and a
|
|
373
|
+
separate *viewer device* — a phone, tablet, or another laptop that just
|
|
374
|
+
wants to open the browser UI.
|
|
375
|
+
|
|
376
|
+
**You don't need SSH for this.** The plot host already runs a plain HTTP
|
|
377
|
+
server on port `8050`, bound to every interface, and the viewer device
|
|
378
|
+
is only a web browser. All you need to do is get traffic from the
|
|
379
|
+
viewer to port `8050` on the plot host.
|
|
380
|
+
|
|
381
|
+
### On the same LAN (phone, tablet, another laptop on the same Wi-Fi)
|
|
382
|
+
|
|
383
|
+
1. Find the plot host's LAN IP:
|
|
384
|
+
|
|
385
|
+
```powershell
|
|
386
|
+
ipconfig | findstr IPv4 # Windows
|
|
387
|
+
```
|
|
388
|
+
```bash
|
|
389
|
+
ip -4 addr | grep inet # Linux/WSL
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
2. Open `http://<lan_ip>:8050` in the browser on the viewer device.
|
|
393
|
+
|
|
394
|
+
3. If Windows, allow inbound connections on port `8050` through Windows
|
|
395
|
+
Defender Firewall. The very first time you run
|
|
396
|
+
`python -m rtplot.server_browser`, Windows pops up an "Allow Python to
|
|
397
|
+
receive connections" dialog — tick **Private networks** and click
|
|
398
|
+
**Allow**. If you missed the dialog, add the rule manually from an
|
|
399
|
+
elevated PowerShell:
|
|
400
|
+
|
|
401
|
+
```powershell
|
|
402
|
+
# PowerShell as Administrator
|
|
403
|
+
New-NetFirewallRule -DisplayName "rtplot" `
|
|
404
|
+
-Direction Inbound -LocalPort 8050 -Protocol TCP `
|
|
405
|
+
-Action Allow -Profile Private
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Only allow on **Private** (home / trusted Wi-Fi), not **Public**,
|
|
409
|
+
unless you know what you're doing. To remove the rule later:
|
|
410
|
+
|
|
411
|
+
```powershell
|
|
412
|
+
Remove-NetFirewallRule -DisplayName "rtplot"
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
No router configuration, no SSH tunneling, no external accounts. Just a
|
|
416
|
+
firewall exception.
|
|
417
|
+
|
|
418
|
+
### WSL2 wrinkle
|
|
419
|
+
|
|
420
|
+
If you run the server inside WSL2 instead of native Windows, WSL2's
|
|
421
|
+
`localhost` auto-forward lets **you** reach it from your Windows browser,
|
|
422
|
+
but does **not** forward traffic from the LAN. To expose a WSL2-hosted
|
|
423
|
+
server to other devices you need one extra hop — a Windows-side port
|
|
424
|
+
proxy that forwards incoming LAN traffic into WSL2:
|
|
425
|
+
|
|
426
|
+
```powershell
|
|
427
|
+
# PowerShell as Administrator
|
|
428
|
+
$wslIp = (wsl hostname -I).Trim().Split()[0]
|
|
429
|
+
netsh interface portproxy add v4tov4 `
|
|
430
|
+
listenport=8050 listenaddress=0.0.0.0 `
|
|
431
|
+
connectport=8050 connectaddress=$wslIp
|
|
432
|
+
New-NetFirewallRule -DisplayName "rtplot wsl" `
|
|
433
|
+
-Direction Inbound -LocalPort 8050 -Protocol TCP `
|
|
434
|
+
-Action Allow -Profile Private
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
WSL2's IP changes on every reboot, so rerun the `netsh` line after a
|
|
438
|
+
restart (or just run `rtplot.server_browser` from native Windows and
|
|
439
|
+
skip this whole step).
|
|
440
|
+
|
|
441
|
+
To undo:
|
|
442
|
+
```powershell
|
|
443
|
+
netsh interface portproxy delete v4tov4 listenport=8050 listenaddress=0.0.0.0
|
|
444
|
+
Remove-NetFirewallRule -DisplayName "rtplot wsl"
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Across the internet (viewer on cellular, another network, etc.)
|
|
448
|
+
|
|
449
|
+
Two easy options, neither of which requires touching your router:
|
|
450
|
+
|
|
451
|
+
**Cloudflare Tunnel** (free, one-shot URL):
|
|
452
|
+
|
|
453
|
+
```powershell
|
|
454
|
+
winget install --id Cloudflare.cloudflared
|
|
455
|
+
cloudflared tunnel --url http://localhost:8050
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Prints an `https://<random>.trycloudflare.com` URL valid for the
|
|
459
|
+
lifetime of the command — paste it into the viewer's browser. Kill the
|
|
460
|
+
command when you're done.
|
|
461
|
+
|
|
462
|
+
**Tailscale** (private mesh VPN, best for recurring setups):
|
|
463
|
+
|
|
464
|
+
Install [Tailscale](https://tailscale.com) on both the plot host and
|
|
465
|
+
every viewer device. Each device gets a stable `100.x.y.z` IP that
|
|
466
|
+
works from any network. Open `http://100.x.y.z:8050` on the viewer.
|
|
467
|
+
|
|
468
|
+
Both tunnel paths forward the HTTP + WebSocket traffic that the browser
|
|
469
|
+
needs; neither involves ZMQ, since the viewer is browser-only. Your
|
|
470
|
+
sender script keeps talking to the plot host locally as usual.
|
|
471
|
+
|
|
472
|
+
### Ports at a glance
|
|
473
|
+
|
|
474
|
+
| Port | What it's for | Who actually needs it open |
|
|
475
|
+
|---|---|---|
|
|
476
|
+
| `8050` (TCP) | HTTP + WebSocket to the browser UI | the plot host, inbound from viewers |
|
|
477
|
+
| `5555` (TCP) | ZMQ data (sender → server) | only the sender and the plot host |
|
|
478
|
+
| `5556` (TCP) | ZMQ control return channel (server → sender) | only the sender and the plot host |
|
|
479
|
+
|
|
480
|
+
For the "other device is a viewer" case, you only need to expose `8050`.
|
|
481
|
+
`5555` / `5556` are between the sender script and the plot host — they
|
|
482
|
+
do not need to be reachable from the viewer device at all.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Performance tuning
|
|
487
|
+
|
|
488
|
+
If you start running out of frames, try these, in roughly this order:
|
|
489
|
+
|
|
490
|
+
1. **Pin the Y range.** `{"yrange": [-2, 2]}` on each plot lets the
|
|
491
|
+
renderer skip autoscaling work and gives the single biggest win.
|
|
492
|
+
2. **Batch your samples.** Pass a 2-D numpy array to `send_array` so N
|
|
493
|
+
samples ship per call.
|
|
494
|
+
3. **Shrink the window.** Fewer pixels to redraw per frame.
|
|
495
|
+
4. **Reduce `line_width`.** Thicker lines cost more to rasterize.
|
|
496
|
+
5. **Use the `-s N` / `--skip N` server flag** to push every Nth sample
|
|
497
|
+
batch to the browser instead of every one. Add `-a` / `--adaptable`
|
|
498
|
+
to let the server tune `N` to your data rate automatically.
|
|
499
|
+
6. **Increase `xrange`.** Counterintuitively, a longer visible history
|
|
500
|
+
can be cheaper than a short one because the browser ring-buffers the
|
|
501
|
+
data and only replaces the tail on each push.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## CLI reference
|
|
506
|
+
|
|
507
|
+
`python -m rtplot.server_browser` accepts:
|
|
508
|
+
|
|
509
|
+
| Flag | Default | Meaning |
|
|
510
|
+
|---|---|---|
|
|
511
|
+
| `-p HOST[:PORT]` | (bind) | Connect to a sender at this address instead of binding |
|
|
512
|
+
| `--host HOST` | `0.0.0.0` | HTTP bind interface |
|
|
513
|
+
| `--port N` | `8050` | HTTP port |
|
|
514
|
+
| `--no-browser` | off | Don't try to open a browser on startup |
|
|
515
|
+
| `--rate N` | `1000` | Max WebSocket push rate (Hz) |
|
|
516
|
+
| `-n N` / `--skip N` | `1` | Push every Nth sample batch |
|
|
517
|
+
| `-a` / `--adaptable` | off | Auto-tune skip rate to data rate |
|
|
518
|
+
| `-c` / `--column` | row | Lay plots out in columns instead of rows |
|
|
519
|
+
| `-d` / `--debug` | off | Extra debug logging |
|
|
520
|
+
| `-sd DIR` / `--save-dir DIR` | cwd | Where to write `.parquet` saves |
|
|
521
|
+
| `-sn NAME` / `--save-name NAME` | — | Prefix for saved filenames |
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Examples
|
|
526
|
+
|
|
527
|
+
- [`rtplot/example_code.py`](rtplot/example_code.py) — a walk through
|
|
528
|
+
every `initialize_plots` signature, plus a controls demo at the bottom.
|
|
529
|
+
- [`rtplot/interactive_test.py`](rtplot/interactive_test.py) — a guided
|
|
530
|
+
end-to-end test that walks you through clicking buttons, dragging
|
|
531
|
+
sliders, typing into the number input, using the ± nudge arrows, and
|
|
532
|
+
spinning the dial. Good for smoke-testing a fresh install.
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
python -m rtplot.server_browser &
|
|
536
|
+
python -m rtplot.interactive_test
|
|
537
|
+
```
|
|
538
|
+
|