plasma-app 2.1.3__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.
- plasma_app-2.1.3/LICENSE +21 -0
- plasma_app-2.1.3/PKG-INFO +133 -0
- plasma_app-2.1.3/README.md +85 -0
- plasma_app-2.1.3/plasma/__init__.py +7 -0
- plasma_app-2.1.3/plasma/__main__.py +207 -0
- plasma_app-2.1.3/plasma/__pyinstaller/__init__.py +12 -0
- plasma_app-2.1.3/plasma/__pyinstaller/hook-plasma.py +16 -0
- plasma_app-2.1.3/plasma/api.py +524 -0
- plasma_app-2.1.3/plasma/app_context.py +121 -0
- plasma_app-2.1.3/plasma/build_info.py +48 -0
- plasma_app-2.1.3/plasma/config.py +282 -0
- plasma_app-2.1.3/plasma/ctl.py +295 -0
- plasma_app-2.1.3/plasma/desktop_shortcut.py +124 -0
- plasma_app-2.1.3/plasma/devices/__init__.py +1 -0
- plasma_app-2.1.3/plasma/devices/bitalino.py +69 -0
- plasma_app-2.1.3/plasma/devices/msense/__init__.py +29 -0
- plasma_app-2.1.3/plasma/devices/msense/ble_scan.py +70 -0
- plasma_app-2.1.3/plasma/devices/msense/bleak_probe.py +93 -0
- plasma_app-2.1.3/plasma/devices/msense/bleak_reconnect_test.py +155 -0
- plasma_app-2.1.3/plasma/devices/msense/config.py +257 -0
- plasma_app-2.1.3/plasma/devices/msense/detect.py +200 -0
- plasma_app-2.1.3/plasma/devices/msense/device.py +2075 -0
- plasma_app-2.1.3/plasma/devices/msense/extract/__init__.py +30 -0
- plasma_app-2.1.3/plasma/devices/msense/extract/__main__.py +103 -0
- plasma_app-2.1.3/plasma/devices/msense/extract/clocksync.py +250 -0
- plasma_app-2.1.3/plasma/devices/msense/extract/options.py +106 -0
- plasma_app-2.1.3/plasma/devices/msense/extract/pipeline.py +682 -0
- plasma_app-2.1.3/plasma/devices/msense/formats/__init__.py +722 -0
- plasma_app-2.1.3/plasma/devices/msense/gyro_bias.py +40 -0
- plasma_app-2.1.3/plasma/devices/msense/nus_sim.py +103 -0
- plasma_app-2.1.3/plasma/devices/msense/nus_stream.py +474 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/__init__.py +24 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/_common.py +8 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/clocksync.py +101 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/control.py +107 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/downloader.py +397 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/extractor.py +113 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/imu.py +180 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/sqc.py +513 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/tools.py +64 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/uuid_tools.py +75 -0
- plasma_app-2.1.3/plasma/devices/msense/panels/viewer.py +95 -0
- plasma_app-2.1.3/plasma/devices/msense/quaternion.py +42 -0
- plasma_app-2.1.3/plasma/devices/msense/records.py +294 -0
- plasma_app-2.1.3/plasma/devices/msense/signal_quality.py +35 -0
- plasma_app-2.1.3/plasma/devices/msense_demo/__init__.py +37 -0
- plasma_app-2.1.3/plasma/devices/msense_demo/config.py +159 -0
- plasma_app-2.1.3/plasma/devices/msense_demo/device.py +303 -0
- plasma_app-2.1.3/plasma/devices/msense_demo/fake_stream.py +202 -0
- plasma_app-2.1.3/plasma/devices/msense_demo/faults.py +56 -0
- plasma_app-2.1.3/plasma/devices/obs.py +40 -0
- plasma_app-2.1.3/plasma/devices/pupil_labs.py +175 -0
- plasma_app-2.1.3/plasma/devices/qb2.py +85 -0
- plasma_app-2.1.3/plasma/devices/shimmer.py +69 -0
- plasma_app-2.1.3/plasma/devices/template.py +211 -0
- plasma_app-2.1.3/plasma/integrated_panel.py +706 -0
- plasma_app-2.1.3/plasma/journal.py +48 -0
- plasma_app-2.1.3/plasma/lsl_recorder.py +437 -0
- plasma_app-2.1.3/plasma/lsl_session.py +40 -0
- plasma_app-2.1.3/plasma/lsl_util.py +58 -0
- plasma_app-2.1.3/plasma/plot_util.py +48 -0
- plasma_app-2.1.3/plasma/plugins.py +105 -0
- plasma_app-2.1.3/plasma/resources/icons/plasma.icns +0 -0
- plasma_app-2.1.3/plasma/resources/icons/plasma.ico +0 -0
- plasma_app-2.1.3/plasma/resources/icons/plasma.png +0 -0
- plasma_app-2.1.3/plasma/resources/icons/plasma.svg +31 -0
- plasma_app-2.1.3/plasma/status.py +238 -0
- plasma_app-2.1.3/plasma/xdf_writer.py +184 -0
- plasma_app-2.1.3/plasma_app.egg-info/PKG-INFO +133 -0
- plasma_app-2.1.3/plasma_app.egg-info/SOURCES.txt +74 -0
- plasma_app-2.1.3/plasma_app.egg-info/dependency_links.txt +1 -0
- plasma_app-2.1.3/plasma_app.egg-info/entry_points.txt +7 -0
- plasma_app-2.1.3/plasma_app.egg-info/requires.txt +46 -0
- plasma_app-2.1.3/plasma_app.egg-info/top_level.txt +1 -0
- plasma_app-2.1.3/pyproject.toml +60 -0
- plasma_app-2.1.3/setup.cfg +4 -0
plasma_app-2.1.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SenSE Lab at The Ohio State University
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plasma-app
|
|
3
|
+
Version: 2.1.3
|
|
4
|
+
Summary: PLASMA — Platform for LSL-based Acquisition of Sensor Metrics and Analytics
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/YuyiChang/PLASMA
|
|
7
|
+
Project-URL: Repository, https://github.com/YuyiChang/PLASMA
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: gradio
|
|
12
|
+
Requires-Dist: plotly
|
|
13
|
+
Requires-Dist: pylsl
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Provides-Extra: msense
|
|
16
|
+
Requires-Dist: simplepyble; extra == "msense"
|
|
17
|
+
Requires-Dist: bleak; extra == "msense"
|
|
18
|
+
Requires-Dist: scipy; extra == "msense"
|
|
19
|
+
Requires-Dist: matplotlib; extra == "msense"
|
|
20
|
+
Requires-Dist: psutil; extra == "msense"
|
|
21
|
+
Requires-Dist: Pillow; extra == "msense"
|
|
22
|
+
Requires-Dist: pandas; extra == "msense"
|
|
23
|
+
Requires-Dist: pyarrow; extra == "msense"
|
|
24
|
+
Requires-Dist: tqdm; extra == "msense"
|
|
25
|
+
Provides-Extra: qb2
|
|
26
|
+
Requires-Dist: blickfeld-qb2; extra == "qb2"
|
|
27
|
+
Provides-Extra: pupil
|
|
28
|
+
Requires-Dist: pupil-labs-realtime-api; extra == "pupil"
|
|
29
|
+
Provides-Extra: shimmer
|
|
30
|
+
Requires-Dist: pyshimmer; extra == "shimmer"
|
|
31
|
+
Requires-Dist: pyserial; extra == "shimmer"
|
|
32
|
+
Provides-Extra: obs
|
|
33
|
+
Requires-Dist: obsws-python; extra == "obs"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: plasma-app[msense,obs,pupil,qb2,shimmer]; extra == "all"
|
|
36
|
+
Provides-Extra: desktop
|
|
37
|
+
Requires-Dist: pyshortcuts; extra == "desktop"
|
|
38
|
+
Provides-Extra: build
|
|
39
|
+
Requires-Dist: pyinstaller; extra == "build"
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest; extra == "test"
|
|
42
|
+
Requires-Dist: pyxdf; extra == "test"
|
|
43
|
+
Provides-Extra: docs
|
|
44
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
45
|
+
Requires-Dist: mkdocs-glightbox; extra == "docs"
|
|
46
|
+
Requires-Dist: playwright; extra == "docs"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# PLASMA
|
|
50
|
+
|
|
51
|
+
PLASMA: Platform for LSL-based Acquisition of Sensor Metrics and Analytics
|
|
52
|
+
|
|
53
|
+
**📖 Operator handbook: <https://yuyichang.github.io/PLASMA/>** — install,
|
|
54
|
+
configure, pair MSense wristbands, run a session, read the memo panel, recover
|
|
55
|
+
from faults.
|
|
56
|
+
|
|
57
|
+
## quickstart
|
|
58
|
+
|
|
59
|
+
- `conda create -n plasma python=3.12`
|
|
60
|
+
- `conda activate plasma`
|
|
61
|
+
- `pip install -e ".[all]"` — or a lean subset, e.g. `".[msense]"` / `".[qb2,pupil]"`
|
|
62
|
+
- `python -m plasma` (or the `plasma` console script)
|
|
63
|
+
|
|
64
|
+
`pip install -r requirements.txt` still works — it's a shim for `-e .[all,build,test]`.
|
|
65
|
+
|
|
66
|
+
Want a double-clickable icon instead of running `plasma` from a terminal?
|
|
67
|
+
`pip install -e ".[desktop]"` then `plasma-install-shortcut` — see
|
|
68
|
+
[Install & first launch](https://yuyichang.github.io/PLASMA/playbook/install/).
|
|
69
|
+
|
|
70
|
+
Writable state (device config, gyro-bias calibration, `data/` recordings, session
|
|
71
|
+
log) location, in order: `$PLASMA_HOME` if set → a per-user app-data dir when
|
|
72
|
+
running as a packaged app (`~/Library/Application Support/PLASMA` on macOS,
|
|
73
|
+
`%LOCALAPPDATA%\PLASMA` on Windows, `~/.local/share/plasma` on Linux) → the
|
|
74
|
+
working directory when running from source. Resolved once at import — set
|
|
75
|
+
`PLASMA_HOME` before launching to override.
|
|
76
|
+
|
|
77
|
+
### use PLASMA from another project
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
pip install "plasma-app[msense] @ git+https://github.com/YuyiChang/PLASMA@v2.0.0"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The distribution is `plasma-app`; the import package is `plasma`. Extras map to
|
|
84
|
+
plugins: `msense`, `qb2`, `pupil`, `shimmer`, `obs` (and `all`).
|
|
85
|
+
|
|
86
|
+
## Known issue / future works
|
|
87
|
+
|
|
88
|
+
- [ ] need manually set lidar ip addr
|
|
89
|
+
- [ ] the built-in LSL→XDF recorder captures **every** stream on the default
|
|
90
|
+
liblsl session, so a concurrent `pytest` run or another lab tool leaks
|
|
91
|
+
into the recording; re-Initialize doesn't fully tear down old outlets.
|
|
92
|
+
Direction (SessionID scoping, outlet lifecycle): [`docs/reference/lsl-lifecycle-and-scoping.md`](docs/reference/lsl-lifecycle-and-scoping.md)
|
|
93
|
+
- [ ] interactive=false on ses/sub when collection started
|
|
94
|
+
- [x] linux aarch64 build — `PLASMA_Linux_arm64` for Jetson Orin (JetPack 6); CI job `build-linux-arm64`
|
|
95
|
+
- [ ] infrequent stream (e.g., PLASMA journaler) shows green after recording stop
|
|
96
|
+
- [ ] doc for msense workflow with better screenshots
|
|
97
|
+
- [ ] memo `stage` transition logic optimization (e.g., timing of "ready to start" msg wrt sensor init status)
|
|
98
|
+
|
|
99
|
+
## developing a sensor plugin
|
|
100
|
+
|
|
101
|
+
Every sensor is a plugin: a `PlasmaDevice` subclass plus a registration entry in
|
|
102
|
+
`plasma/plugins.py`. Core code never imports a concrete device.
|
|
103
|
+
|
|
104
|
+
> full guide: [`plasma/devices/README.md`](plasma/devices/README.md)
|
|
105
|
+
> base class: `PlasmaDevice` in `plasma/devices/template.py`
|
|
106
|
+
> reference package (config section + tabs + multi-unit): `plasma/devices/msense/`
|
|
107
|
+
|
|
108
|
+
## documentation
|
|
109
|
+
|
|
110
|
+
The playbook is a MkDocs Material site under `docs/`, deployed to GitHub Pages
|
|
111
|
+
by `.github/workflows/docs.yml` on every push to `main`.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
pip install -e ".[docs]" # mkdocs-material + mkdocs-glightbox + playwright
|
|
115
|
+
mkdocs serve # preview at http://127.0.0.1:8000
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Screenshots (`docs/assets/screenshots/*.png`, Git LFS) are regenerated by a
|
|
119
|
+
Playwright script that drives PLASMA with the simulated MSense device:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
brew install git-lfs && git lfs install # one-time
|
|
123
|
+
python scripts/capture_screenshots.py # ~4 min; needs Google Chrome
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
It uses `PLASMA_HOME=~/Library/Application Support/PLASMA` by default so the
|
|
127
|
+
paths shown in the memo panel match a packaged install; pass `--home /tmp/x` for
|
|
128
|
+
a disposable run.
|
|
129
|
+
|
|
130
|
+
## references
|
|
131
|
+
|
|
132
|
+
- qb2: https://docs.blickfeld.com/qb2/Qb2/v1.10/guides/api.html
|
|
133
|
+
- pupil-labs: https://pupil-labs.github.io/pl-realtime-api/dev/
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# PLASMA
|
|
2
|
+
|
|
3
|
+
PLASMA: Platform for LSL-based Acquisition of Sensor Metrics and Analytics
|
|
4
|
+
|
|
5
|
+
**📖 Operator handbook: <https://yuyichang.github.io/PLASMA/>** — install,
|
|
6
|
+
configure, pair MSense wristbands, run a session, read the memo panel, recover
|
|
7
|
+
from faults.
|
|
8
|
+
|
|
9
|
+
## quickstart
|
|
10
|
+
|
|
11
|
+
- `conda create -n plasma python=3.12`
|
|
12
|
+
- `conda activate plasma`
|
|
13
|
+
- `pip install -e ".[all]"` — or a lean subset, e.g. `".[msense]"` / `".[qb2,pupil]"`
|
|
14
|
+
- `python -m plasma` (or the `plasma` console script)
|
|
15
|
+
|
|
16
|
+
`pip install -r requirements.txt` still works — it's a shim for `-e .[all,build,test]`.
|
|
17
|
+
|
|
18
|
+
Want a double-clickable icon instead of running `plasma` from a terminal?
|
|
19
|
+
`pip install -e ".[desktop]"` then `plasma-install-shortcut` — see
|
|
20
|
+
[Install & first launch](https://yuyichang.github.io/PLASMA/playbook/install/).
|
|
21
|
+
|
|
22
|
+
Writable state (device config, gyro-bias calibration, `data/` recordings, session
|
|
23
|
+
log) location, in order: `$PLASMA_HOME` if set → a per-user app-data dir when
|
|
24
|
+
running as a packaged app (`~/Library/Application Support/PLASMA` on macOS,
|
|
25
|
+
`%LOCALAPPDATA%\PLASMA` on Windows, `~/.local/share/plasma` on Linux) → the
|
|
26
|
+
working directory when running from source. Resolved once at import — set
|
|
27
|
+
`PLASMA_HOME` before launching to override.
|
|
28
|
+
|
|
29
|
+
### use PLASMA from another project
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
pip install "plasma-app[msense] @ git+https://github.com/YuyiChang/PLASMA@v2.0.0"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The distribution is `plasma-app`; the import package is `plasma`. Extras map to
|
|
36
|
+
plugins: `msense`, `qb2`, `pupil`, `shimmer`, `obs` (and `all`).
|
|
37
|
+
|
|
38
|
+
## Known issue / future works
|
|
39
|
+
|
|
40
|
+
- [ ] need manually set lidar ip addr
|
|
41
|
+
- [ ] the built-in LSL→XDF recorder captures **every** stream on the default
|
|
42
|
+
liblsl session, so a concurrent `pytest` run or another lab tool leaks
|
|
43
|
+
into the recording; re-Initialize doesn't fully tear down old outlets.
|
|
44
|
+
Direction (SessionID scoping, outlet lifecycle): [`docs/reference/lsl-lifecycle-and-scoping.md`](docs/reference/lsl-lifecycle-and-scoping.md)
|
|
45
|
+
- [ ] interactive=false on ses/sub when collection started
|
|
46
|
+
- [x] linux aarch64 build — `PLASMA_Linux_arm64` for Jetson Orin (JetPack 6); CI job `build-linux-arm64`
|
|
47
|
+
- [ ] infrequent stream (e.g., PLASMA journaler) shows green after recording stop
|
|
48
|
+
- [ ] doc for msense workflow with better screenshots
|
|
49
|
+
- [ ] memo `stage` transition logic optimization (e.g., timing of "ready to start" msg wrt sensor init status)
|
|
50
|
+
|
|
51
|
+
## developing a sensor plugin
|
|
52
|
+
|
|
53
|
+
Every sensor is a plugin: a `PlasmaDevice` subclass plus a registration entry in
|
|
54
|
+
`plasma/plugins.py`. Core code never imports a concrete device.
|
|
55
|
+
|
|
56
|
+
> full guide: [`plasma/devices/README.md`](plasma/devices/README.md)
|
|
57
|
+
> base class: `PlasmaDevice` in `plasma/devices/template.py`
|
|
58
|
+
> reference package (config section + tabs + multi-unit): `plasma/devices/msense/`
|
|
59
|
+
|
|
60
|
+
## documentation
|
|
61
|
+
|
|
62
|
+
The playbook is a MkDocs Material site under `docs/`, deployed to GitHub Pages
|
|
63
|
+
by `.github/workflows/docs.yml` on every push to `main`.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
pip install -e ".[docs]" # mkdocs-material + mkdocs-glightbox + playwright
|
|
67
|
+
mkdocs serve # preview at http://127.0.0.1:8000
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Screenshots (`docs/assets/screenshots/*.png`, Git LFS) are regenerated by a
|
|
71
|
+
Playwright script that drives PLASMA with the simulated MSense device:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
brew install git-lfs && git lfs install # one-time
|
|
75
|
+
python scripts/capture_screenshots.py # ~4 min; needs Google Chrome
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It uses `PLASMA_HOME=~/Library/Application Support/PLASMA` by default so the
|
|
79
|
+
paths shown in the memo panel match a packaged install; pass `--home /tmp/x` for
|
|
80
|
+
a disposable run.
|
|
81
|
+
|
|
82
|
+
## references
|
|
83
|
+
|
|
84
|
+
- qb2: https://docs.blickfeld.com/qb2/Qb2/v1.10/guides/api.html
|
|
85
|
+
- pupil-labs: https://pupil-labs.github.io/pl-realtime-api/dev/
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
__version__ = "2.1.3"
|
|
2
|
+
|
|
3
|
+
# Short git commit hash baked in by CI (.github/workflows/build.yml) just
|
|
4
|
+
# before a release build is frozen with PyInstaller — a frozen bundle ships no
|
|
5
|
+
# .git directory to introspect at runtime. None from a source checkout;
|
|
6
|
+
# plasma.build_info.git_commit_hash() resolves that case live from git.
|
|
7
|
+
__git_commit__ = None
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import sys
|
|
5
|
+
import gradio as gr
|
|
6
|
+
from plasma import plugins
|
|
7
|
+
from plasma.app_context import app_context
|
|
8
|
+
from plasma.integrated_panel import IntegratedPanel
|
|
9
|
+
from plasma.config import device_config
|
|
10
|
+
from plasma import api as plasma_api
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _handle_sigterm(signum, frame):
|
|
14
|
+
# run atexit hooks (device shutdown-cleanup handlers) then exit hard, so
|
|
15
|
+
# `kill <pid>` doesn't leave a device mid-transfer holding a resource
|
|
16
|
+
# (e.g. a BLE connection slot) until its own supervision timeout
|
|
17
|
+
atexit._run_exitfuncs()
|
|
18
|
+
os._exit(143)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _relaunch_argv():
|
|
22
|
+
"""argv for re-launching this same PLASMA. Under PyInstaller (onefile)
|
|
23
|
+
`sys.executable` IS the bundle exe; from source it's the interpreter plus
|
|
24
|
+
`-m plasma`. Any extra CLI args are preserved."""
|
|
25
|
+
if getattr(sys, "frozen", False):
|
|
26
|
+
return [sys.executable, *sys.argv[1:]]
|
|
27
|
+
return [sys.executable, "-m", "plasma", *sys.argv[1:]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# set by main() so shutdown() can stop the server (frees the port before a
|
|
31
|
+
# restart re-binds it)
|
|
32
|
+
app = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def shutdown(restart=False):
|
|
36
|
+
"""Run every registered cleanup hook (each MSense driver's
|
|
37
|
+
`_shutdown_cleanup`, the recording flush + device disconnect from
|
|
38
|
+
`IntegratedPanel._atexit_cleanup`), stop the Gradio server, then re-exec
|
|
39
|
+
PLASMA (`restart=True`) or exit the process. Safe to call from a Gradio
|
|
40
|
+
event-handler thread — mirrors `_handle_sigterm`'s
|
|
41
|
+
`atexit._run_exitfuncs()` + hard-exit pattern."""
|
|
42
|
+
atexit._run_exitfuncs()
|
|
43
|
+
if app is not None:
|
|
44
|
+
try:
|
|
45
|
+
app.close()
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
if restart:
|
|
49
|
+
argv = _relaunch_argv()
|
|
50
|
+
if os.name == "nt":
|
|
51
|
+
# execv on Windows is spawn-then-exit, which flickers the console;
|
|
52
|
+
# an explicit Popen + exit is more predictable there.
|
|
53
|
+
import subprocess
|
|
54
|
+
subprocess.Popen(argv, close_fds=False)
|
|
55
|
+
os._exit(0)
|
|
56
|
+
os.execv(argv[0], argv)
|
|
57
|
+
os._exit(0)
|
|
58
|
+
|
|
59
|
+
js_func = """
|
|
60
|
+
function refresh() {
|
|
61
|
+
const url = new URL(window.location);
|
|
62
|
+
|
|
63
|
+
if (url.searchParams.get('__theme') !== 'light') {
|
|
64
|
+
url.searchParams.set('__theme', 'light');
|
|
65
|
+
window.location.href = url.href;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Gradio 5's gr.Plot destroys and recreates the whole Plotly <div> on
|
|
69
|
+
// every value it receives (a <svelte:component> {#key}), and its Plotly
|
|
70
|
+
// wrapper never calls Plotly.purge — so a plot on a gr.Timer orphans a
|
|
71
|
+
// graph div (event handlers, internal caches, a WebGL context for 3D /
|
|
72
|
+
// scattergl) roughly once a second, until the tab runs out of memory
|
|
73
|
+
// (gradio#10252). One <body> observer purges each removed graph div, and
|
|
74
|
+
// also re-applies the scroll position Plotly's DOM swap resets to top.
|
|
75
|
+
let lastScrollY = window.scrollY;
|
|
76
|
+
window.addEventListener('scroll', () => { lastScrollY = window.scrollY; },
|
|
77
|
+
{passive: true});
|
|
78
|
+
|
|
79
|
+
const purge = (node) => {
|
|
80
|
+
if (!node || node.nodeType !== 1) return;
|
|
81
|
+
const divs = node.matches && node.matches('.js-plotly-plot')
|
|
82
|
+
? [node]
|
|
83
|
+
: Array.from(node.querySelectorAll?.('.js-plotly-plot') || []);
|
|
84
|
+
for (const gd of divs) {
|
|
85
|
+
try { window.Plotly && window.Plotly.purge(gd); } catch (e) {}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let sawSwap = false;
|
|
90
|
+
const obs = new MutationObserver((records) => {
|
|
91
|
+
for (const r of records) {
|
|
92
|
+
for (const n of r.removedNodes) purge(n);
|
|
93
|
+
if (r.removedNodes.length || r.addedNodes.length) sawSwap = true;
|
|
94
|
+
}
|
|
95
|
+
if (sawSwap && Math.abs(window.scrollY - lastScrollY) > 1) {
|
|
96
|
+
window.scrollTo(0, lastScrollY);
|
|
97
|
+
}
|
|
98
|
+
sawSwap = false;
|
|
99
|
+
});
|
|
100
|
+
obs.observe(document.body, {childList: true, subtree: true});
|
|
101
|
+
window.addEventListener('pagehide', () => obs.disconnect(), {once: true});
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def build_blocks(ip):
|
|
106
|
+
"""Assemble the full PLASMA `gr.Blocks` (every tab + the headless API) for
|
|
107
|
+
the given `IntegratedPanel`. Shared by `main()` and the screenshot-capture
|
|
108
|
+
tool (`scripts/capture_screenshots.py`) so both build the identical UI.
|
|
109
|
+
Does not launch the server."""
|
|
110
|
+
with gr.Blocks(title=app_context().app_name, theme=gr.themes.Ocean(), js=js_func) as app:
|
|
111
|
+
yams_title = None
|
|
112
|
+
with gr.Tabs() as top_tabs:
|
|
113
|
+
with gr.Tab("Session Dashboard"):
|
|
114
|
+
ip.interface()
|
|
115
|
+
with gr.Tab("Data Dashboard"):
|
|
116
|
+
ip.visualizer_interface()
|
|
117
|
+
# extra tabs contributed by enabled plugins (e.g. MSense SQC / IMU).
|
|
118
|
+
# The real and the simulated MSense plugins both contribute the same
|
|
119
|
+
# "🍠 YAMS (MSense Tools)" tab — de-dupe by title so enabling both
|
|
120
|
+
# doesn't build it twice.
|
|
121
|
+
seen_tabs = set()
|
|
122
|
+
for plugin in device_config.get_active_table().values():
|
|
123
|
+
for tab_title, builder in plugin.tabs:
|
|
124
|
+
if tab_title in seen_tabs:
|
|
125
|
+
continue
|
|
126
|
+
seen_tabs.add(tab_title)
|
|
127
|
+
if "YAMS" in tab_title:
|
|
128
|
+
yams_title = tab_title
|
|
129
|
+
with gr.Tab(tab_title):
|
|
130
|
+
builder(ip)
|
|
131
|
+
with gr.Tab("Configuration"):
|
|
132
|
+
device_config.interface()
|
|
133
|
+
# with gr.Tab("PL"):
|
|
134
|
+
# pl.interface()
|
|
135
|
+
|
|
136
|
+
_wire_plot_timer_gate(ip, top_tabs, yams_title)
|
|
137
|
+
|
|
138
|
+
# stop any live MSense stream when the last browser tab goes away — a
|
|
139
|
+
# continuous stream with nobody watching just churns the BLE radio and
|
|
140
|
+
# (pre-fix) the plot memory. Best-effort; streams aren't multi-viewer.
|
|
141
|
+
def _on_unload():
|
|
142
|
+
for dev in list(getattr(ip, "available_devices", []) or []):
|
|
143
|
+
fn = getattr(dev, "stop_all_live_streams", None)
|
|
144
|
+
if callable(fn):
|
|
145
|
+
try:
|
|
146
|
+
fn()
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
app.unload(_on_unload)
|
|
150
|
+
|
|
151
|
+
# headless control + situational-awareness endpoints (/status, /start,
|
|
152
|
+
# /stop, /mark, /events) + the 1 Hz durable event-log pump. Additive —
|
|
153
|
+
# no UI. See docs/headless.md.
|
|
154
|
+
plasma_api.register(ip)
|
|
155
|
+
return app
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _wire_plot_timer_gate(ip, top_tabs, yams_title):
|
|
159
|
+
"""Run each live-plot refresh timer only while its tab is on screen.
|
|
160
|
+
gr.Plot recreates its whole Plotly <div> for every value it receives
|
|
161
|
+
(gradio#10252), so a timer ticking behind a hidden tab is a memory leak.
|
|
162
|
+
The MSense sub-tab gate is in panels/tools.py; this adds the top-level one."""
|
|
163
|
+
from plasma.devices.msense.panels.tools import _SQC_SUBTAB, _IMU_SUBTAB
|
|
164
|
+
|
|
165
|
+
specs = [(ip._viz_timer, "Data Dashboard", None)]
|
|
166
|
+
if getattr(ip, "_sqc_timer", None) is not None:
|
|
167
|
+
specs.append((ip._sqc_timer, yams_title, _SQC_SUBTAB))
|
|
168
|
+
if getattr(ip, "_imu_timer", None) is not None:
|
|
169
|
+
specs.append((ip._imu_timer, yams_title, _IMU_SUBTAB))
|
|
170
|
+
|
|
171
|
+
sub_state = getattr(ip, "_yams_sub", None) or gr.State("")
|
|
172
|
+
|
|
173
|
+
def _on_top(sub, evt: gr.SelectData):
|
|
174
|
+
top = evt.value or ""
|
|
175
|
+
updates = [
|
|
176
|
+
gr.Timer(active=(top == want_top
|
|
177
|
+
and (want_sub is None or sub == want_sub)))
|
|
178
|
+
for _, want_top, want_sub in specs
|
|
179
|
+
]
|
|
180
|
+
return updates if len(updates) > 1 else updates[0]
|
|
181
|
+
|
|
182
|
+
top_tabs.select(_on_top, inputs=sub_state, outputs=[t for t, _, _ in specs])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def main():
|
|
186
|
+
global app
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
signal.signal(signal.SIGTERM, _handle_sigterm)
|
|
190
|
+
except ValueError:
|
|
191
|
+
pass # not the main thread (e.g. imported oddly) — atexit still covers normal exit
|
|
192
|
+
|
|
193
|
+
plugins.load_plugins()
|
|
194
|
+
device_config.refresh_defaults()
|
|
195
|
+
|
|
196
|
+
ip = IntegratedPanel()
|
|
197
|
+
# pl = PupilLabsDashboard()
|
|
198
|
+
|
|
199
|
+
app = build_blocks(ip)
|
|
200
|
+
app.launch(inbrowser=True, share=False)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == '__main__':
|
|
204
|
+
main()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""PyInstaller hook directory for the ``plasma`` package.
|
|
2
|
+
|
|
3
|
+
Registered via the ``pyinstaller40`` entry point in ``pyproject.toml`` so any
|
|
4
|
+
``pyinstaller`` build of an app that imports ``plasma`` picks up
|
|
5
|
+
``hook-plasma.py`` automatically — no hand-maintained ``hiddenimports`` list in
|
|
6
|
+
the spec files.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_hook_dirs():
|
|
12
|
+
return [os.path.dirname(__file__)]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Best-effort collection of every ``plasma`` submodule + data file.
|
|
2
|
+
|
|
3
|
+
Device plugins are imported dynamically by ``plasma.plugins`` (dotted strings in
|
|
4
|
+
``_STATIC`` / ``_DISCOVERY``), so PyInstaller's static analysis never sees them.
|
|
5
|
+
|
|
6
|
+
This hook is a **fallback** for downstream apps that freeze ``plasma`` without
|
|
7
|
+
using our spec files. The project's own builds do not rely on it: ``app_*.spec``
|
|
8
|
+
call ``spec_common.plasma_hiddenimports()`` (a filesystem walk), because
|
|
9
|
+
``collect_submodules`` resolves ``plasma`` through the setuptools editable-install
|
|
10
|
+
finder, which under newer setuptools fails to enumerate the ``plasma.devices.*``
|
|
11
|
+
sub-packages.
|
|
12
|
+
"""
|
|
13
|
+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
|
14
|
+
|
|
15
|
+
hiddenimports = collect_submodules("plasma")
|
|
16
|
+
datas = collect_data_files("plasma")
|