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.
Files changed (76) hide show
  1. plasma_app-2.1.3/LICENSE +21 -0
  2. plasma_app-2.1.3/PKG-INFO +133 -0
  3. plasma_app-2.1.3/README.md +85 -0
  4. plasma_app-2.1.3/plasma/__init__.py +7 -0
  5. plasma_app-2.1.3/plasma/__main__.py +207 -0
  6. plasma_app-2.1.3/plasma/__pyinstaller/__init__.py +12 -0
  7. plasma_app-2.1.3/plasma/__pyinstaller/hook-plasma.py +16 -0
  8. plasma_app-2.1.3/plasma/api.py +524 -0
  9. plasma_app-2.1.3/plasma/app_context.py +121 -0
  10. plasma_app-2.1.3/plasma/build_info.py +48 -0
  11. plasma_app-2.1.3/plasma/config.py +282 -0
  12. plasma_app-2.1.3/plasma/ctl.py +295 -0
  13. plasma_app-2.1.3/plasma/desktop_shortcut.py +124 -0
  14. plasma_app-2.1.3/plasma/devices/__init__.py +1 -0
  15. plasma_app-2.1.3/plasma/devices/bitalino.py +69 -0
  16. plasma_app-2.1.3/plasma/devices/msense/__init__.py +29 -0
  17. plasma_app-2.1.3/plasma/devices/msense/ble_scan.py +70 -0
  18. plasma_app-2.1.3/plasma/devices/msense/bleak_probe.py +93 -0
  19. plasma_app-2.1.3/plasma/devices/msense/bleak_reconnect_test.py +155 -0
  20. plasma_app-2.1.3/plasma/devices/msense/config.py +257 -0
  21. plasma_app-2.1.3/plasma/devices/msense/detect.py +200 -0
  22. plasma_app-2.1.3/plasma/devices/msense/device.py +2075 -0
  23. plasma_app-2.1.3/plasma/devices/msense/extract/__init__.py +30 -0
  24. plasma_app-2.1.3/plasma/devices/msense/extract/__main__.py +103 -0
  25. plasma_app-2.1.3/plasma/devices/msense/extract/clocksync.py +250 -0
  26. plasma_app-2.1.3/plasma/devices/msense/extract/options.py +106 -0
  27. plasma_app-2.1.3/plasma/devices/msense/extract/pipeline.py +682 -0
  28. plasma_app-2.1.3/plasma/devices/msense/formats/__init__.py +722 -0
  29. plasma_app-2.1.3/plasma/devices/msense/gyro_bias.py +40 -0
  30. plasma_app-2.1.3/plasma/devices/msense/nus_sim.py +103 -0
  31. plasma_app-2.1.3/plasma/devices/msense/nus_stream.py +474 -0
  32. plasma_app-2.1.3/plasma/devices/msense/panels/__init__.py +24 -0
  33. plasma_app-2.1.3/plasma/devices/msense/panels/_common.py +8 -0
  34. plasma_app-2.1.3/plasma/devices/msense/panels/clocksync.py +101 -0
  35. plasma_app-2.1.3/plasma/devices/msense/panels/control.py +107 -0
  36. plasma_app-2.1.3/plasma/devices/msense/panels/downloader.py +397 -0
  37. plasma_app-2.1.3/plasma/devices/msense/panels/extractor.py +113 -0
  38. plasma_app-2.1.3/plasma/devices/msense/panels/imu.py +180 -0
  39. plasma_app-2.1.3/plasma/devices/msense/panels/sqc.py +513 -0
  40. plasma_app-2.1.3/plasma/devices/msense/panels/tools.py +64 -0
  41. plasma_app-2.1.3/plasma/devices/msense/panels/uuid_tools.py +75 -0
  42. plasma_app-2.1.3/plasma/devices/msense/panels/viewer.py +95 -0
  43. plasma_app-2.1.3/plasma/devices/msense/quaternion.py +42 -0
  44. plasma_app-2.1.3/plasma/devices/msense/records.py +294 -0
  45. plasma_app-2.1.3/plasma/devices/msense/signal_quality.py +35 -0
  46. plasma_app-2.1.3/plasma/devices/msense_demo/__init__.py +37 -0
  47. plasma_app-2.1.3/plasma/devices/msense_demo/config.py +159 -0
  48. plasma_app-2.1.3/plasma/devices/msense_demo/device.py +303 -0
  49. plasma_app-2.1.3/plasma/devices/msense_demo/fake_stream.py +202 -0
  50. plasma_app-2.1.3/plasma/devices/msense_demo/faults.py +56 -0
  51. plasma_app-2.1.3/plasma/devices/obs.py +40 -0
  52. plasma_app-2.1.3/plasma/devices/pupil_labs.py +175 -0
  53. plasma_app-2.1.3/plasma/devices/qb2.py +85 -0
  54. plasma_app-2.1.3/plasma/devices/shimmer.py +69 -0
  55. plasma_app-2.1.3/plasma/devices/template.py +211 -0
  56. plasma_app-2.1.3/plasma/integrated_panel.py +706 -0
  57. plasma_app-2.1.3/plasma/journal.py +48 -0
  58. plasma_app-2.1.3/plasma/lsl_recorder.py +437 -0
  59. plasma_app-2.1.3/plasma/lsl_session.py +40 -0
  60. plasma_app-2.1.3/plasma/lsl_util.py +58 -0
  61. plasma_app-2.1.3/plasma/plot_util.py +48 -0
  62. plasma_app-2.1.3/plasma/plugins.py +105 -0
  63. plasma_app-2.1.3/plasma/resources/icons/plasma.icns +0 -0
  64. plasma_app-2.1.3/plasma/resources/icons/plasma.ico +0 -0
  65. plasma_app-2.1.3/plasma/resources/icons/plasma.png +0 -0
  66. plasma_app-2.1.3/plasma/resources/icons/plasma.svg +31 -0
  67. plasma_app-2.1.3/plasma/status.py +238 -0
  68. plasma_app-2.1.3/plasma/xdf_writer.py +184 -0
  69. plasma_app-2.1.3/plasma_app.egg-info/PKG-INFO +133 -0
  70. plasma_app-2.1.3/plasma_app.egg-info/SOURCES.txt +74 -0
  71. plasma_app-2.1.3/plasma_app.egg-info/dependency_links.txt +1 -0
  72. plasma_app-2.1.3/plasma_app.egg-info/entry_points.txt +7 -0
  73. plasma_app-2.1.3/plasma_app.egg-info/requires.txt +46 -0
  74. plasma_app-2.1.3/plasma_app.egg-info/top_level.txt +1 -0
  75. plasma_app-2.1.3/pyproject.toml +60 -0
  76. plasma_app-2.1.3/setup.cfg +4 -0
@@ -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")