better-rtplot 0.3.0__tar.gz → 0.4.1__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.1/PKG-INFO +149 -0
- better_rtplot-0.4.1/README.md +125 -0
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/pyproject.toml +1 -1
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/client.py +178 -9
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/server_browser.py +13 -2
- better_rtplot-0.3.0/PKG-INFO +0 -682
- better_rtplot-0.3.0/README.md +0 -658
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/LICENSE +0 -0
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/server.py +0 -0
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.3.0 → better_rtplot-0.4.1}/rtplot/static/uPlot.min.css +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: better-rtplot
|
|
3
|
+
Version: 0.4.1
|
|
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: pyqtgraph (>=0.13.0) ; extra == "server"
|
|
20
|
+
Requires-Dist: pyside6 (>6.4.0) ; extra == "server"
|
|
21
|
+
Requires-Dist: pyzmq (>=25.0.0)
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# rtplot — real-time plotting over ZMQ
|
|
25
|
+
|
|
26
|
+
**rtplot** pushes live data from a Python script to a browser plot —
|
|
27
|
+
locally or across a network — in a few lines of code. The plot page
|
|
28
|
+
also hosts interactive widgets (buttons, sliders, dials, numeric / text
|
|
29
|
+
displays) that feed values back to the sender in real time.
|
|
30
|
+
|
|
31
|
+
Typical use: a robot or data-acquisition script runs on a Raspberry Pi,
|
|
32
|
+
and you watch signals and tweak gains from a laptop on the same Wi-Fi.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
Two processes talking over ZMQ, plus a browser viewer:
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
flowchart TD
|
|
42
|
+
script["Your Python script<br/>send_array() · poll_controls()"]
|
|
43
|
+
subgraph server["rtplot-server"]
|
|
44
|
+
browser["browser tab<br/>localhost:8050"]
|
|
45
|
+
end
|
|
46
|
+
script -- "data :5555" --> server
|
|
47
|
+
server -- "controls :5556" --> script
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Sender and server don't have to be on the same machine — see the
|
|
51
|
+
[networking guide](docs/networking.md).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
**Server** — grab the prebuilt binary from the
|
|
58
|
+
[Releases page](https://github.com/jmontp/rtplot/releases):
|
|
59
|
+
|
|
60
|
+
| Platform | Asset |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Windows | `rtplot-server-<version>-windows-x64.exe` |
|
|
63
|
+
| Linux | `rtplot-server-<version>-linux-x86_64.tar.gz` |
|
|
64
|
+
| macOS (Apple Silicon) | `rtplot-server-<version>-macos-arm64.tar.gz` |
|
|
65
|
+
|
|
66
|
+
No Python needed on the viewing machine. On Windows the binary opens
|
|
67
|
+
a small Tk status window showing the listening URL.
|
|
68
|
+
|
|
69
|
+
**Client** — pip install in the env that runs your script:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install better-rtplot
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
(If you'd rather run the server from Python too, use
|
|
76
|
+
`pip install "better-rtplot[browser]"` — see the
|
|
77
|
+
[API reference](docs/api.md#install-detail).)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Your first plot
|
|
82
|
+
|
|
83
|
+
```mermaid
|
|
84
|
+
flowchart TD
|
|
85
|
+
t1["terminal 1: start the server"]
|
|
86
|
+
t2["terminal 2: run your script"]
|
|
87
|
+
br["browser: open localhost:8050"]
|
|
88
|
+
t1 --> t2 --> br
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Terminal 1 — start the server.** Run the prebuilt `rtplot-server`
|
|
92
|
+
binary you downloaded above. Open `http://localhost:8050` in a
|
|
93
|
+
browser — the page is blank until data arrives.
|
|
94
|
+
|
|
95
|
+
**Terminal 2 — run your sender script.** Save as `my_plot.py`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from rtplot import client
|
|
99
|
+
import time
|
|
100
|
+
|
|
101
|
+
client.local_plot()
|
|
102
|
+
client.initialize_plots(["my signal"])
|
|
103
|
+
|
|
104
|
+
for i in range(1000):
|
|
105
|
+
client.send_array(i * 0.01)
|
|
106
|
+
time.sleep(0.01)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python my_plot.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A rising line now draws itself in the browser tab.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Highlights
|
|
118
|
+
|
|
119
|
+
- **Fast.** Binary WebSocket deltas up to 1 kHz; the browser coalesces
|
|
120
|
+
samples into one repaint per `requestAnimationFrame`, so rendering
|
|
121
|
+
tracks your monitor refresh rate regardless of sample rate.
|
|
122
|
+
- **Browser-based.** aiohttp + uPlot, no desktop GUI toolkit, works
|
|
123
|
+
over SSH port forwarding.
|
|
124
|
+
- **Remote-friendly.** Sender or plot host can bind. Live Bind /
|
|
125
|
+
Connect buttons retarget without restart.
|
|
126
|
+
- **Config lives with the data.** The sender declares plot layout.
|
|
127
|
+
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
128
|
+
from your loop, no threads, no callbacks.
|
|
129
|
+
- **Static HTML snapshots.** `save_snapshot("out.html")` writes a
|
|
130
|
+
self-contained ~65 KB file with the current trace embedded.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Where to go next
|
|
135
|
+
|
|
136
|
+
- **[API reference](docs/api.md)** — every `rtplot.client` function,
|
|
137
|
+
the plot-layout schema, interactive controls, snapshots, browser UI,
|
|
138
|
+
and `rtplot-server` CLI flags.
|
|
139
|
+
- **[Networking guide](docs/networking.md)** — Mode A vs. Mode B,
|
|
140
|
+
viewing from a phone or second laptop, the WSL2 wrinkle, Cloudflare
|
|
141
|
+
Tunnel, Tailscale.
|
|
142
|
+
- **[Examples](examples/README.md)** — runnable scripts with embedded
|
|
143
|
+
HTML snapshots you can open offline.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
Issues and feature requests:
|
|
148
|
+
[github.com/jmontp/rtplot/issues](https://github.com/jmontp/rtplot/issues).
|
|
149
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# rtplot — real-time plotting over ZMQ
|
|
2
|
+
|
|
3
|
+
**rtplot** pushes live data from a Python script to a browser plot —
|
|
4
|
+
locally or across a network — in a few lines of code. The plot page
|
|
5
|
+
also hosts interactive widgets (buttons, sliders, dials, numeric / text
|
|
6
|
+
displays) that feed values back to the sender in real time.
|
|
7
|
+
|
|
8
|
+
Typical use: a robot or data-acquisition script runs on a Raspberry Pi,
|
|
9
|
+
and you watch signals and tweak gains from a laptop on the same Wi-Fi.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
Two processes talking over ZMQ, plus a browser viewer:
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart TD
|
|
19
|
+
script["Your Python script<br/>send_array() · poll_controls()"]
|
|
20
|
+
subgraph server["rtplot-server"]
|
|
21
|
+
browser["browser tab<br/>localhost:8050"]
|
|
22
|
+
end
|
|
23
|
+
script -- "data :5555" --> server
|
|
24
|
+
server -- "controls :5556" --> script
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Sender and server don't have to be on the same machine — see the
|
|
28
|
+
[networking guide](docs/networking.md).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
**Server** — grab the prebuilt binary from the
|
|
35
|
+
[Releases page](https://github.com/jmontp/rtplot/releases):
|
|
36
|
+
|
|
37
|
+
| Platform | Asset |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Windows | `rtplot-server-<version>-windows-x64.exe` |
|
|
40
|
+
| Linux | `rtplot-server-<version>-linux-x86_64.tar.gz` |
|
|
41
|
+
| macOS (Apple Silicon) | `rtplot-server-<version>-macos-arm64.tar.gz` |
|
|
42
|
+
|
|
43
|
+
No Python needed on the viewing machine. On Windows the binary opens
|
|
44
|
+
a small Tk status window showing the listening URL.
|
|
45
|
+
|
|
46
|
+
**Client** — pip install in the env that runs your script:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install better-rtplot
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
(If you'd rather run the server from Python too, use
|
|
53
|
+
`pip install "better-rtplot[browser]"` — see the
|
|
54
|
+
[API reference](docs/api.md#install-detail).)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Your first plot
|
|
59
|
+
|
|
60
|
+
```mermaid
|
|
61
|
+
flowchart TD
|
|
62
|
+
t1["terminal 1: start the server"]
|
|
63
|
+
t2["terminal 2: run your script"]
|
|
64
|
+
br["browser: open localhost:8050"]
|
|
65
|
+
t1 --> t2 --> br
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Terminal 1 — start the server.** Run the prebuilt `rtplot-server`
|
|
69
|
+
binary you downloaded above. Open `http://localhost:8050` in a
|
|
70
|
+
browser — the page is blank until data arrives.
|
|
71
|
+
|
|
72
|
+
**Terminal 2 — run your sender script.** Save as `my_plot.py`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from rtplot import client
|
|
76
|
+
import time
|
|
77
|
+
|
|
78
|
+
client.local_plot()
|
|
79
|
+
client.initialize_plots(["my signal"])
|
|
80
|
+
|
|
81
|
+
for i in range(1000):
|
|
82
|
+
client.send_array(i * 0.01)
|
|
83
|
+
time.sleep(0.01)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python my_plot.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A rising line now draws itself in the browser tab.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Highlights
|
|
95
|
+
|
|
96
|
+
- **Fast.** Binary WebSocket deltas up to 1 kHz; the browser coalesces
|
|
97
|
+
samples into one repaint per `requestAnimationFrame`, so rendering
|
|
98
|
+
tracks your monitor refresh rate regardless of sample rate.
|
|
99
|
+
- **Browser-based.** aiohttp + uPlot, no desktop GUI toolkit, works
|
|
100
|
+
over SSH port forwarding.
|
|
101
|
+
- **Remote-friendly.** Sender or plot host can bind. Live Bind /
|
|
102
|
+
Connect buttons retarget without restart.
|
|
103
|
+
- **Config lives with the data.** The sender declares plot layout.
|
|
104
|
+
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
105
|
+
from your loop, no threads, no callbacks.
|
|
106
|
+
- **Static HTML snapshots.** `save_snapshot("out.html")` writes a
|
|
107
|
+
self-contained ~65 KB file with the current trace embedded.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Where to go next
|
|
112
|
+
|
|
113
|
+
- **[API reference](docs/api.md)** — every `rtplot.client` function,
|
|
114
|
+
the plot-layout schema, interactive controls, snapshots, browser UI,
|
|
115
|
+
and `rtplot-server` CLI flags.
|
|
116
|
+
- **[Networking guide](docs/networking.md)** — Mode A vs. Mode B,
|
|
117
|
+
viewing from a phone or second laptop, the WSL2 wrinkle, Cloudflare
|
|
118
|
+
Tunnel, Tailscale.
|
|
119
|
+
- **[Examples](examples/README.md)** — runnable scripts with embedded
|
|
120
|
+
HTML snapshots you can open offline.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
Issues and feature requests:
|
|
125
|
+
[github.com/jmontp/rtplot/issues](https://github.com/jmontp/rtplot/issues).
|
|
@@ -2,6 +2,8 @@ import zmq
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
import time
|
|
4
4
|
from collections import OrderedDict, namedtuple
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import List, Optional, Tuple, Union
|
|
5
7
|
|
|
6
8
|
###################
|
|
7
9
|
# ZMQ Networking #
|
|
@@ -85,19 +87,177 @@ SENDING_DISPLAY = "4"
|
|
|
85
87
|
#Lightweight result type returned by poll_controls()
|
|
86
88
|
ControlState = namedtuple("ControlState", ["values", "buttons"])
|
|
87
89
|
|
|
90
|
+
|
|
91
|
+
##########################################
|
|
92
|
+
# Typed plot / controls configuration API #
|
|
93
|
+
##########################################
|
|
94
|
+
#
|
|
95
|
+
# These dataclasses are an alternative to the dict-based configuration
|
|
96
|
+
# accepted by initialize_plots(). Both APIs coexist — the typed form
|
|
97
|
+
# serializes to the exact same on-the-wire dict, so the server is
|
|
98
|
+
# unaware of which one the caller used.
|
|
99
|
+
#
|
|
100
|
+
# client.initialize_plots([
|
|
101
|
+
# client.Plot(names=["signal"], yrange=(-6, 6), title="demo"),
|
|
102
|
+
# client.ControlsRow([client.Button("reset", "Reset")]),
|
|
103
|
+
# ])
|
|
104
|
+
#
|
|
105
|
+
# is equivalent to the classic:
|
|
106
|
+
#
|
|
107
|
+
# client.initialize_plots([
|
|
108
|
+
# {"names": ["signal"], "yrange": [-6, 6], "title": "demo"},
|
|
109
|
+
# {"controls": [{"type": "button", "id": "reset", "label": "Reset"}]},
|
|
110
|
+
# ])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _drop_none(d):
|
|
114
|
+
# Wire format treats missing keys as "use default"; None would
|
|
115
|
+
# JSON-serialize to null which the server wouldn't handle.
|
|
116
|
+
return {k: v for k, v in d.items() if v is not None}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _range_to_list(r):
|
|
120
|
+
# yrange/xrange go over the wire as JSON lists.
|
|
121
|
+
return list(r) if r is not None else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class Plot:
|
|
126
|
+
"""A single plot with optional styling.
|
|
127
|
+
|
|
128
|
+
All fields beyond ``names`` are optional and mirror the styled-plot
|
|
129
|
+
dict keys documented in ``docs/api.md``.
|
|
130
|
+
"""
|
|
131
|
+
names: List[str]
|
|
132
|
+
colors: Optional[List[str]] = None
|
|
133
|
+
line_style: Optional[List[str]] = None
|
|
134
|
+
line_width: Optional[float] = None
|
|
135
|
+
title: Optional[str] = None
|
|
136
|
+
xlabel: Optional[str] = None
|
|
137
|
+
ylabel: Optional[str] = None
|
|
138
|
+
yrange: Optional[Tuple[float, float]] = None
|
|
139
|
+
xrange: Optional[int] = None
|
|
140
|
+
height: Optional[float] = None
|
|
141
|
+
|
|
142
|
+
def to_dict(self):
|
|
143
|
+
return _drop_none({
|
|
144
|
+
"names": list(self.names),
|
|
145
|
+
"colors": list(self.colors) if self.colors is not None else None,
|
|
146
|
+
"line_style": list(self.line_style) if self.line_style is not None else None,
|
|
147
|
+
"line_width": self.line_width,
|
|
148
|
+
"title": self.title,
|
|
149
|
+
"xlabel": self.xlabel,
|
|
150
|
+
"ylabel": self.ylabel,
|
|
151
|
+
"yrange": _range_to_list(self.yrange),
|
|
152
|
+
"xrange": self.xrange,
|
|
153
|
+
"height": self.height,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class Button:
|
|
159
|
+
id: str
|
|
160
|
+
label: str
|
|
161
|
+
color: Optional[str] = None
|
|
162
|
+
height: Optional[float] = None
|
|
163
|
+
|
|
164
|
+
def to_dict(self):
|
|
165
|
+
return _drop_none({
|
|
166
|
+
"type": "button", "id": self.id, "label": self.label,
|
|
167
|
+
"color": self.color, "height": self.height,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class Slider:
|
|
173
|
+
id: str
|
|
174
|
+
label: str
|
|
175
|
+
min: float
|
|
176
|
+
max: float
|
|
177
|
+
value: float = 0.0
|
|
178
|
+
step: Optional[float] = None
|
|
179
|
+
format: Optional[str] = None
|
|
180
|
+
color: Optional[str] = None
|
|
181
|
+
height: Optional[float] = None
|
|
182
|
+
|
|
183
|
+
def to_dict(self):
|
|
184
|
+
return _drop_none({
|
|
185
|
+
"type": "slider", "id": self.id, "label": self.label,
|
|
186
|
+
"min": self.min, "max": self.max, "value": self.value,
|
|
187
|
+
"step": self.step, "format": self.format,
|
|
188
|
+
"color": self.color, "height": self.height,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class Dial:
|
|
194
|
+
id: str
|
|
195
|
+
label: str
|
|
196
|
+
min: float
|
|
197
|
+
max: float
|
|
198
|
+
value: float = 0.0
|
|
199
|
+
step: Optional[float] = None
|
|
200
|
+
sensitivity: Optional[float] = None
|
|
201
|
+
format: Optional[str] = None
|
|
202
|
+
color: Optional[str] = None
|
|
203
|
+
height: Optional[float] = None
|
|
204
|
+
|
|
205
|
+
def to_dict(self):
|
|
206
|
+
return _drop_none({
|
|
207
|
+
"type": "dial", "id": self.id, "label": self.label,
|
|
208
|
+
"min": self.min, "max": self.max, "value": self.value,
|
|
209
|
+
"step": self.step, "sensitivity": self.sensitivity,
|
|
210
|
+
"format": self.format, "color": self.color,
|
|
211
|
+
"height": self.height,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class Display:
|
|
217
|
+
id: str
|
|
218
|
+
label: str
|
|
219
|
+
format: Optional[str] = None
|
|
220
|
+
height: Optional[float] = None
|
|
221
|
+
|
|
222
|
+
def to_dict(self):
|
|
223
|
+
return _drop_none({
|
|
224
|
+
"type": "display", "id": self.id, "label": self.label,
|
|
225
|
+
"format": self.format, "height": self.height,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class Text:
|
|
231
|
+
id: str
|
|
232
|
+
label: str
|
|
233
|
+
value: str = ""
|
|
234
|
+
height: Optional[float] = None
|
|
235
|
+
|
|
236
|
+
def to_dict(self):
|
|
237
|
+
return _drop_none({
|
|
238
|
+
"type": "text", "id": self.id, "label": self.label,
|
|
239
|
+
"value": self.value, "height": self.height,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class ControlsRow:
|
|
245
|
+
"""A row of control widgets, rendered in place of a plot."""
|
|
246
|
+
controls: List[Union[Button, Slider, Dial, Display, Text, dict]] = field(default_factory=list)
|
|
247
|
+
|
|
248
|
+
def to_dict(self):
|
|
249
|
+
return {"controls": [
|
|
250
|
+
c.to_dict() if hasattr(c, "to_dict") else c
|
|
251
|
+
for c in self.controls
|
|
252
|
+
]}
|
|
253
|
+
|
|
254
|
+
|
|
88
255
|
def local_plot():
|
|
89
256
|
"""Send data to a plot in the same computer"""
|
|
90
257
|
|
|
91
258
|
local_address = "tcp://127.0.0.1:5555"
|
|
92
259
|
configure_ip(ip = local_address)
|
|
93
260
|
|
|
94
|
-
def plot_to_neurobionics_tv():
|
|
95
|
-
"""Send data to a plot in the same computer"""
|
|
96
|
-
|
|
97
|
-
tv_computer_address = "tcp://141.212.77.23:5555"
|
|
98
|
-
configure_ip(ip = tv_computer_address)
|
|
99
|
-
|
|
100
|
-
|
|
101
261
|
def configure_port(new_port:int):
|
|
102
262
|
"""Rebind the local publisher on ``new_port`` (bind mode only).
|
|
103
263
|
|
|
@@ -277,6 +437,11 @@ def initialize_plots(plot_descriptions=1):
|
|
|
277
437
|
plot_desc_dict = OrderedDict()
|
|
278
438
|
plot_desc_dict["plot0"] = plot_descriptions
|
|
279
439
|
|
|
440
|
+
#Process typed inputs (Plot / ControlsRow) passed alone
|
|
441
|
+
elif hasattr(plot_descriptions, "to_dict"):
|
|
442
|
+
plot_desc_dict = OrderedDict()
|
|
443
|
+
plot_desc_dict["plot0"] = plot_descriptions.to_dict()
|
|
444
|
+
|
|
280
445
|
#Process lists of things
|
|
281
446
|
elif isinstance(plot_descriptions, list):
|
|
282
447
|
|
|
@@ -291,10 +456,14 @@ def initialize_plots(plot_descriptions=1):
|
|
|
291
456
|
for i,plot_desc in enumerate(plot_descriptions):
|
|
292
457
|
plot_desc_dict["plot{}".format(i)] = {"names":plot_desc}
|
|
293
458
|
|
|
294
|
-
#Process list of
|
|
295
|
-
|
|
459
|
+
#Process list of dicts or typed objects (Plot / ControlsRow),
|
|
460
|
+
# including mixed lists — anything exposing .to_dict() is normalized
|
|
461
|
+
# to the wire dict form.
|
|
462
|
+
elif isinstance(plot_descriptions[0],dict) or hasattr(plot_descriptions[0], "to_dict"):
|
|
296
463
|
plot_desc_dict = OrderedDict()
|
|
297
464
|
for i,plot_desc in enumerate(plot_descriptions):
|
|
465
|
+
if hasattr(plot_desc, "to_dict"):
|
|
466
|
+
plot_desc = plot_desc.to_dict()
|
|
298
467
|
plot_desc_dict["plot{}".format(i)] = plot_desc
|
|
299
468
|
|
|
300
469
|
#Throw error
|
|
@@ -787,6 +787,8 @@ INDEX_HTML = """<!doctype html>
|
|
|
787
787
|
.ctrl-item-tall > .ctrl-val { align-self: center; }
|
|
788
788
|
.ctrl-btn:hover { background: #f0f0f0; }
|
|
789
789
|
.ctrl-btn:active { background: #e2e2e2; }
|
|
790
|
+
.ctrl-btn-colored:hover { filter: brightness(0.93); }
|
|
791
|
+
.ctrl-btn-colored:active { filter: brightness(0.85); }
|
|
790
792
|
.ctrl-slider .ctrl-rangeinput { flex: 1; min-width: 120px; }
|
|
791
793
|
.ctrl-numinput { width: 72px; font-family: monospace; font-size: calc(13px * var(--ui-scale)); padding: 4px 6px; border: 1px solid #b8b8b8; border-radius: 3px; background: #fff; color: #222; text-align: right; -moz-appearance: textfield; }
|
|
792
794
|
.ctrl-numinput::-webkit-outer-spin-button,
|
|
@@ -1093,6 +1095,7 @@ INDEX_HTML = """<!doctype html>
|
|
|
1093
1095
|
min, max, step, initial: value,
|
|
1094
1096
|
commit, applyLocal,
|
|
1095
1097
|
sensitivity, hasMin, hasMax,
|
|
1098
|
+
color: el.color,
|
|
1096
1099
|
});
|
|
1097
1100
|
if (widget && widget.node) item.appendChild(widget.node);
|
|
1098
1101
|
|
|
@@ -1127,7 +1130,7 @@ INDEX_HTML = """<!doctype html>
|
|
|
1127
1130
|
return item;
|
|
1128
1131
|
}
|
|
1129
1132
|
|
|
1130
|
-
function buildSliderWidget({ min, max, step, initial, commit, applyLocal, hasMin, hasMax }) {
|
|
1133
|
+
function buildSliderWidget({ min, max, step, initial, commit, applyLocal, hasMin, hasMax, color }) {
|
|
1131
1134
|
// HTML range inputs can't represent unbounded values, so fall back
|
|
1132
1135
|
// to sane defaults when the user omits min/max on a slider.
|
|
1133
1136
|
const rangeMin = hasMin ? min : 0;
|
|
@@ -1137,6 +1140,7 @@ INDEX_HTML = """<!doctype html>
|
|
|
1137
1140
|
range.className = 'ctrl-rangeinput';
|
|
1138
1141
|
range.min = rangeMin;
|
|
1139
1142
|
range.max = rangeMax;
|
|
1143
|
+
if (color != null) range.style.accentColor = resolveColor(color);
|
|
1140
1144
|
range.step = step;
|
|
1141
1145
|
range.value = initial;
|
|
1142
1146
|
// Live preview: update the number box (and any other mirrors) on every
|
|
@@ -1149,7 +1153,7 @@ INDEX_HTML = """<!doctype html>
|
|
|
1149
1153
|
};
|
|
1150
1154
|
}
|
|
1151
1155
|
|
|
1152
|
-
function buildDialWidget({ min, max, initial, commit, applyLocal, sensitivity, hasMin, hasMax }) {
|
|
1156
|
+
function buildDialWidget({ min, max, initial, commit, applyLocal, sensitivity, hasMin, hasMax, color }) {
|
|
1153
1157
|
const svgNS = 'http://www.w3.org/2000/svg';
|
|
1154
1158
|
const size = 100;
|
|
1155
1159
|
const svg = document.createElementNS(svgNS, 'svg');
|
|
@@ -1185,6 +1189,7 @@ INDEX_HTML = """<!doctype html>
|
|
|
1185
1189
|
|
|
1186
1190
|
const indicator = document.createElementNS(svgNS, 'line');
|
|
1187
1191
|
indicator.classList.add('dial-indicator');
|
|
1192
|
+
if (color != null) indicator.style.stroke = resolveColor(color);
|
|
1188
1193
|
svg.appendChild(indicator);
|
|
1189
1194
|
|
|
1190
1195
|
// Unified rotation math: the indicator advances by 2π radians for
|
|
@@ -1270,6 +1275,12 @@ INDEX_HTML = """<!doctype html>
|
|
|
1270
1275
|
const b = document.createElement('button');
|
|
1271
1276
|
b.className = 'ctrl-btn';
|
|
1272
1277
|
b.textContent = el.label || el.id;
|
|
1278
|
+
if (el.color != null) {
|
|
1279
|
+
const c = resolveColor(el.color);
|
|
1280
|
+
b.style.backgroundColor = c;
|
|
1281
|
+
b.style.borderColor = c;
|
|
1282
|
+
b.classList.add('ctrl-btn-colored');
|
|
1283
|
+
}
|
|
1273
1284
|
b.addEventListener('click', () => sendCtrl({ type: 'control_button', id: el.id }));
|
|
1274
1285
|
item.appendChild(b);
|
|
1275
1286
|
} else if (el.type === 'slider') {
|