slm_open_api_package 0.2.10__tar.gz → 0.2.11__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.
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/PKG-INFO +17 -4
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/README.md +14 -3
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/pyproject.toml +3 -1
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/pyproject.toml.orig +3 -1
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/CPB_example.py +10 -5
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/example_multi.py +6 -2
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/fast_log_example.py +19 -4
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/flac_example.py +4 -1
- slm_open_api_package-0.2.11/src/slm_api/helpers/data_handler.py +251 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/stream_handlers.py +76 -49
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/tmpFileStateMachine.py +36 -10
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/webxi_helper_functions.py +2 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/mp3_example.py +5 -2
- slm_open_api_package-0.2.11/src/slm_api/plot_saved_data_example.py +163 -0
- slm_open_api_package-0.2.10/src/slm_api/helpers/data_handler.py +0 -94
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/__init__.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/enums/fast_log_intervals_enum.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/enums/sequence_id_enum.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/FigureHandler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/Leq.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/__init__.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/buffer.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/fft.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/flac_stream_2_samples.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/https_requests.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/measurment_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/sequence_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/stream_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/websocket_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/webxi_root.pem +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/multi_device_flac.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/multi_instrument_example.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/screen_record_example.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/README.md +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/__init__.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi-header.ksy +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi-stream.ksy +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi_header.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi_stream.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: slm_open_api_package
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.11
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author: Sofus Vingaard
|
|
6
6
|
Author-email: Sofus Vingaard <sofus.krebsvingaard@hbkworld.com>
|
|
7
7
|
Requires-Dist: requests
|
|
8
8
|
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pandas
|
|
9
10
|
Requires-Dist: websockets
|
|
10
11
|
Requires-Dist: kaitaistruct
|
|
11
12
|
Requires-Dist: packaging
|
|
@@ -13,6 +14,7 @@ Requires-Dist: pyqtgraph
|
|
|
13
14
|
Requires-Dist: pyqt5
|
|
14
15
|
Requires-Dist: pyqt5-qt5<5.15.11
|
|
15
16
|
Requires-Dist: miniaudio
|
|
17
|
+
Requires-Dist: soundfile
|
|
16
18
|
Requires-Dist: matplotlib>=3.11.1
|
|
17
19
|
Requires-Dist: cryptography>=50.0.1
|
|
18
20
|
Requires-Python: >=3.12
|
|
@@ -78,7 +80,7 @@ except KeyboardInterrupt:
|
|
|
78
80
|
streamer.stopStream()
|
|
79
81
|
```
|
|
80
82
|
|
|
81
|
-
`stopStream()` stops the measurement,
|
|
83
|
+
JSON and CSV publish complete snapshots in a background worker, at most once per second while new data arrives, rather than rewriting the file for every sample. They retain records in memory and preserve their existing layouts; full-history snapshot costs still grow with recording length. Construct `JsonDataHandler(path, snapshot_interval=2.0)` or `CsvDataHandler(path, snapshot_interval=2.0)` and attach it with `setDataHandler()` to use a different interval. `close()` waits for the worker and saves all remaining records; background write failures are reported on subsequent updates or during the final flush. WAV still appends each audio chunk without buffering the recording in memory. `stopStream()` stops the measurement, closes saving handlers, and removes the device stream. Measurement saving formats are `"csv"` and `"json"`; audio uses `"wav"`. File extensions are added automatically.
|
|
82
84
|
|
|
83
85
|
## Multiple Sequences
|
|
84
86
|
|
|
@@ -147,11 +149,22 @@ streamer = WebXiStreamHandler(
|
|
|
147
149
|
host="192.168.1.183",
|
|
148
150
|
sequenceID=SequenceIdEnums.MP3Signal.value,
|
|
149
151
|
mode="mp3",
|
|
152
|
+
saving="wav",
|
|
153
|
+
saving_path="saved_data/audio",
|
|
150
154
|
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
streamer.startStream()
|
|
158
|
+
except KeyboardInterrupt:
|
|
159
|
+
streamer.stopStream()
|
|
151
160
|
```
|
|
152
161
|
|
|
153
162
|
The matching `flac` form uses `SequenceIdEnums.FLACSignal.value` and `mode="flac"`. Audio setup is enabled by the handler. The SLM can take a few seconds to initialize audio recording when its quality setting changes.
|
|
154
163
|
|
|
164
|
+
WAV saving appends decoded mono samples as recording happens, updating the file header after each chunk so the file is readable before shutdown. Saving happens after decoding and calibration to pascals. Samples are divided by the fixed full-scale pressure `calibrationFactor * (2**23 - 1)` and saved as `PCM_24`, not peak-normalized per chunk. The filename includes `_fullscale-<value>Pa.wav`. To recover pressure values, multiply floating-point samples read from the WAV by that full-scale value (within 24-bit quantization accuracy). Samples outside the calibrated range produce a clipping warning; this scaling cannot repair upstream clipping. MP3 uses the decoded sample rate; the current FLAC decoder uses 65536 Hz. JSON and CSV saving are rejected for audio modes, and WAV saving is rejected for measurement modes. Run `uv sync` to install the `soundfile` dependency used for progressive WAV writing.
|
|
165
|
+
|
|
166
|
+
To attach the handler directly, use `WavDataHandler("saved_data/audio", sample_rate=65536, full_scale_pa=calibrationFactor * (2**23 - 1))` from `slm_api.helpers.data_handler`, using the device's actual calibration factor. You can instead supply `sample_rate` and `full_scale_pa` to `handle()`; the stream supplies both automatically. Call `close()` when finished. Use a separate WAV handler and output path for each device. Existing float32 pressure WAVs are unchanged; this format applies to new recordings.
|
|
167
|
+
|
|
155
168
|
## Synchronized Multi-Device Stream
|
|
156
169
|
|
|
157
170
|
Based on [multi_device_flac.py](src/slm_api/multi_device_flac.py), pass `multi_device=True` and start/stop the group through the synchronized helpers:
|
|
@@ -168,7 +181,7 @@ streams = [
|
|
|
168
181
|
mode="mp3",
|
|
169
182
|
streamName=f"Audio_{host}",
|
|
170
183
|
multi_device=True,
|
|
171
|
-
saving="
|
|
184
|
+
saving="wav",
|
|
172
185
|
saving_path=f"saved_data/{host}",
|
|
173
186
|
)
|
|
174
187
|
for host in hosts
|
|
@@ -203,4 +216,4 @@ Legacy flags such as `mp3=True` remain supported, but `mode=` is preferred. Only
|
|
|
203
216
|
|
|
204
217
|
- **No values arrive:** Confirm the requested sequences are enabled on the SLM.
|
|
205
218
|
- **Audio returns no samples after first enable:** Allow the SLM a few seconds to initialize audio recording before retrying.
|
|
206
|
-
- **Saved file is missing:**
|
|
219
|
+
- **Saved file is missing:** Files are written when the handler first receives data (nonempty samples for WAV). Check the saving path and that the stream is delivering data. Still call `stopStream()` or `stop_all()` to clean up recording resources.
|
|
@@ -58,7 +58,7 @@ except KeyboardInterrupt:
|
|
|
58
58
|
streamer.stopStream()
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
`stopStream()` stops the measurement,
|
|
61
|
+
JSON and CSV publish complete snapshots in a background worker, at most once per second while new data arrives, rather than rewriting the file for every sample. They retain records in memory and preserve their existing layouts; full-history snapshot costs still grow with recording length. Construct `JsonDataHandler(path, snapshot_interval=2.0)` or `CsvDataHandler(path, snapshot_interval=2.0)` and attach it with `setDataHandler()` to use a different interval. `close()` waits for the worker and saves all remaining records; background write failures are reported on subsequent updates or during the final flush. WAV still appends each audio chunk without buffering the recording in memory. `stopStream()` stops the measurement, closes saving handlers, and removes the device stream. Measurement saving formats are `"csv"` and `"json"`; audio uses `"wav"`. File extensions are added automatically.
|
|
62
62
|
|
|
63
63
|
## Multiple Sequences
|
|
64
64
|
|
|
@@ -127,11 +127,22 @@ streamer = WebXiStreamHandler(
|
|
|
127
127
|
host="192.168.1.183",
|
|
128
128
|
sequenceID=SequenceIdEnums.MP3Signal.value,
|
|
129
129
|
mode="mp3",
|
|
130
|
+
saving="wav",
|
|
131
|
+
saving_path="saved_data/audio",
|
|
130
132
|
)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
streamer.startStream()
|
|
136
|
+
except KeyboardInterrupt:
|
|
137
|
+
streamer.stopStream()
|
|
131
138
|
```
|
|
132
139
|
|
|
133
140
|
The matching `flac` form uses `SequenceIdEnums.FLACSignal.value` and `mode="flac"`. Audio setup is enabled by the handler. The SLM can take a few seconds to initialize audio recording when its quality setting changes.
|
|
134
141
|
|
|
142
|
+
WAV saving appends decoded mono samples as recording happens, updating the file header after each chunk so the file is readable before shutdown. Saving happens after decoding and calibration to pascals. Samples are divided by the fixed full-scale pressure `calibrationFactor * (2**23 - 1)` and saved as `PCM_24`, not peak-normalized per chunk. The filename includes `_fullscale-<value>Pa.wav`. To recover pressure values, multiply floating-point samples read from the WAV by that full-scale value (within 24-bit quantization accuracy). Samples outside the calibrated range produce a clipping warning; this scaling cannot repair upstream clipping. MP3 uses the decoded sample rate; the current FLAC decoder uses 65536 Hz. JSON and CSV saving are rejected for audio modes, and WAV saving is rejected for measurement modes. Run `uv sync` to install the `soundfile` dependency used for progressive WAV writing.
|
|
143
|
+
|
|
144
|
+
To attach the handler directly, use `WavDataHandler("saved_data/audio", sample_rate=65536, full_scale_pa=calibrationFactor * (2**23 - 1))` from `slm_api.helpers.data_handler`, using the device's actual calibration factor. You can instead supply `sample_rate` and `full_scale_pa` to `handle()`; the stream supplies both automatically. Call `close()` when finished. Use a separate WAV handler and output path for each device. Existing float32 pressure WAVs are unchanged; this format applies to new recordings.
|
|
145
|
+
|
|
135
146
|
## Synchronized Multi-Device Stream
|
|
136
147
|
|
|
137
148
|
Based on [multi_device_flac.py](src/slm_api/multi_device_flac.py), pass `multi_device=True` and start/stop the group through the synchronized helpers:
|
|
@@ -148,7 +159,7 @@ streams = [
|
|
|
148
159
|
mode="mp3",
|
|
149
160
|
streamName=f"Audio_{host}",
|
|
150
161
|
multi_device=True,
|
|
151
|
-
saving="
|
|
162
|
+
saving="wav",
|
|
152
163
|
saving_path=f"saved_data/{host}",
|
|
153
164
|
)
|
|
154
165
|
for host in hosts
|
|
@@ -183,4 +194,4 @@ Legacy flags such as `mp3=True` remain supported, but `mode=` is preferred. Only
|
|
|
183
194
|
|
|
184
195
|
- **No values arrive:** Confirm the requested sequences are enabled on the SLM.
|
|
185
196
|
- **Audio returns no samples after first enable:** Allow the SLM a few seconds to initialize audio recording before retrying.
|
|
186
|
-
- **Saved file is missing:**
|
|
197
|
+
- **Saved file is missing:** Files are written when the handler first receives data (nonempty samples for WAV). Check the saving path and that the stream is delivering data. Still call `stopStream()` or `stop_all()` to clean up recording resources.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "slm_open_api_package"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.11"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"requests",
|
|
9
9
|
"numpy",
|
|
10
|
+
"pandas",
|
|
10
11
|
"websockets",
|
|
11
12
|
"kaitaistruct",
|
|
12
13
|
"packaging",
|
|
@@ -14,6 +15,7 @@ dependencies = [
|
|
|
14
15
|
"PyQt5",
|
|
15
16
|
"PyQt5-Qt5<5.15.11",
|
|
16
17
|
"miniaudio",
|
|
18
|
+
"soundfile",
|
|
17
19
|
"matplotlib>=3.11.1",
|
|
18
20
|
"cryptography>=50.0.1",
|
|
19
21
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "slm_open_api_package"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.11"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -10,6 +10,7 @@ requires-python = ">=3.12"
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"requests",
|
|
12
12
|
"numpy",
|
|
13
|
+
"pandas",
|
|
13
14
|
"websockets",
|
|
14
15
|
"kaitaistruct",
|
|
15
16
|
"packaging",
|
|
@@ -17,6 +18,7 @@ dependencies = [
|
|
|
17
18
|
"PyQt5",
|
|
18
19
|
"PyQt5-Qt5<5.15.11",
|
|
19
20
|
"miniaudio",
|
|
21
|
+
"soundfile",
|
|
20
22
|
"matplotlib>=3.11.1",
|
|
21
23
|
"cryptography>=50.0.1",
|
|
22
24
|
]
|
|
@@ -6,7 +6,8 @@ from matplotlib.animation import FuncAnimation
|
|
|
6
6
|
from slm_api.helpers.stream_handlers import WebXiStreamHandler
|
|
7
7
|
from slm_api.helpers import webxi_helper_functions as webxi_helper
|
|
8
8
|
from slm_api.enums.sequence_id_enum import SequenceIdEnums
|
|
9
|
-
from slm_api.helpers.data_handler import CsvDataHandler,
|
|
9
|
+
from slm_api.helpers.data_handler import CsvDataHandler,JsonDataHandler,MultiDataHandler, DataHandler
|
|
10
|
+
import datetime
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
host = webxi_helper.set_host(__file__)
|
|
@@ -17,8 +18,8 @@ cpb_sequence_names = ["CPBLAeq"]
|
|
|
17
18
|
sequenceID= SequenceIdEnums.CPBLAeq.value
|
|
18
19
|
|
|
19
20
|
class PrintHandler(DataHandler):
|
|
20
|
-
def handle(self, value, **data ):
|
|
21
|
-
print(f"{value}")
|
|
21
|
+
def handle(self, timestamp, value, **data ):
|
|
22
|
+
print(f"time = {timestamp} value: {value}")
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class FigureHandler:
|
|
@@ -59,12 +60,16 @@ if __name__ == "__main__":
|
|
|
59
60
|
# turns on the CPB eq we want to use
|
|
60
61
|
webxi_helper.turn_on_cpb_leq(host, cpb_sequence_names)
|
|
61
62
|
|
|
63
|
+
filename = f'name-of-file_{datetime.datetime.now().strftime("%H%M_%m%d%Y")}'
|
|
64
|
+
|
|
65
|
+
|
|
62
66
|
streamer = WebXiStreamHandler(host,
|
|
63
67
|
sequenceID=sequenceID,
|
|
64
68
|
mode="cpb",
|
|
65
69
|
sequenceNames=cpb_sequence_names,
|
|
66
|
-
saving_path=
|
|
67
|
-
saving="json"
|
|
70
|
+
saving_path=f"./saved_data/{filename}",
|
|
71
|
+
saving="json",
|
|
72
|
+
time=True)
|
|
68
73
|
# streamer.setDataHandler(JsonDataHandler(r"C:\Users\SKREBSVINGAA\csv_test\csv_test.txt"))
|
|
69
74
|
streamer.setDataHandler(PrintHandler())
|
|
70
75
|
|
|
@@ -8,6 +8,8 @@ from matplotlib.animation import FuncAnimation
|
|
|
8
8
|
from slm_api.helpers import webxi_helper_functions as webxi_helper
|
|
9
9
|
from slm_api.helpers import stream_handlers as sh
|
|
10
10
|
from slm_api.helpers.data_handler import DataHandler, MultiDataHandler
|
|
11
|
+
import datetime
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
# UseHttps(use_https=False)
|
|
13
15
|
|
|
@@ -15,7 +17,7 @@ host = webxi_helper.set_host(__file__)
|
|
|
15
17
|
|
|
16
18
|
# This example will stream LAeq. Add more names to the list to plot several sequences at once.
|
|
17
19
|
# Incase of error make sure the sequences are enabled on the SLM.
|
|
18
|
-
sequenceNames = ["LAeq", "LCeq"
|
|
20
|
+
sequenceNames = ["LAeq", "LCeq"]
|
|
19
21
|
|
|
20
22
|
class PrintHandler(DataHandler):
|
|
21
23
|
def handle(self, *, name, value, **data):
|
|
@@ -70,7 +72,9 @@ if __name__ == "__main__":
|
|
|
70
72
|
# sets the sequences to true. You can add or remove sequences at the top of the file.
|
|
71
73
|
webxi_helper.turn_on_bb_leq(host, sequenceNames)
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
filename = f'name-of-file_{datetime.datetime.now().strftime("%H%M_%m%d%Y")}'
|
|
76
|
+
|
|
77
|
+
streamer = sh.WebXiStreamHandler(host=host, sequenceNames=sequenceNames, time=True, mode="multi", saving="csv", saving_path=f"./saved_data/{filename}")
|
|
74
78
|
fig = FigHandler(sequenceNames)
|
|
75
79
|
streamer.setDataHandler(MultiDataHandler(PrintHandler(), fig))
|
|
76
80
|
fig.startAnimation()
|
|
@@ -36,6 +36,7 @@ from slm_api.helpers import webxi_helper_functions as webxi_helper
|
|
|
36
36
|
# enums
|
|
37
37
|
from slm_api.enums.sequence_id_enum import SequenceIdEnums
|
|
38
38
|
from slm_api.enums.fast_log_intervals_enum import FastLogInterval
|
|
39
|
+
import datetime
|
|
39
40
|
|
|
40
41
|
# ---------- Configuration ----------
|
|
41
42
|
host = webxi_helper.set_host(__file__)
|
|
@@ -52,8 +53,8 @@ FAST_LOGGING_INTERVAL_MS = FastLogInterval.interval_1000ms.value
|
|
|
52
53
|
SEQUENCE_ID = SequenceIdEnums.FastLAF.value
|
|
53
54
|
|
|
54
55
|
class PrintDataHandler(DataHandler):
|
|
55
|
-
def handle(self, timestamp, name, local, value, unit):
|
|
56
|
-
print(f"{timestamp}{name} ({local}): {value:7.2f} {unit}")
|
|
56
|
+
def handle(self, timestamp, name, local, value, unit, **data):
|
|
57
|
+
print(f"{timestamp} {name} ({local}): {value:7.2f} {unit}")
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
|
|
@@ -150,14 +151,28 @@ class PrintDataHandler(DataHandler):
|
|
|
150
151
|
|
|
151
152
|
|
|
152
153
|
if __name__ == "__main__":
|
|
154
|
+
|
|
155
|
+
filename = f'name-of-file_{datetime.datetime.now().strftime("%H%M_%m%d%Y")}'
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# turns off all BB freq weights to prevent interference
|
|
159
|
+
webxi_helper.turn_off_bb_freq_weight(host)
|
|
160
|
+
|
|
161
|
+
# turns on the wanted BB freq weights for this example
|
|
162
|
+
webxi_helper.turn_on_bb_freq_weight(host, ["A"], fast=True)
|
|
163
|
+
|
|
164
|
+
# sets the sequences to true.
|
|
165
|
+
# For fastlogging add fast=True so it routes to the correct endpoint
|
|
166
|
+
webxi_helper.turn_on_bb_leq(host, "LAF", fast=True)
|
|
153
167
|
|
|
154
168
|
try:
|
|
155
169
|
streamer = WebXiStreamHandler(host,
|
|
156
170
|
sequenceID = SEQUENCE_ID,
|
|
157
171
|
mode="fast_logging",
|
|
158
172
|
fast_logging_interval=FAST_LOGGING_INTERVAL_MS,
|
|
159
|
-
saving="
|
|
160
|
-
saving_path=
|
|
173
|
+
saving="json",
|
|
174
|
+
saving_path=f"./saved_data/{filename}",
|
|
175
|
+
time=True)
|
|
161
176
|
streamer.setDataHandler(PrintDataHandler())
|
|
162
177
|
|
|
163
178
|
|
|
@@ -29,6 +29,7 @@ import threading
|
|
|
29
29
|
|
|
30
30
|
from slm_api.helpers.FigureHandler import FigureHandler
|
|
31
31
|
from slm_api.helpers.data_handler import MultiDataHandler, CsvDataHandler
|
|
32
|
+
import datetime
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
# FLAC streaming is only available on 2255
|
|
@@ -153,8 +154,10 @@ def on_close():
|
|
|
153
154
|
streamer.stopStream()
|
|
154
155
|
|
|
155
156
|
if __name__ == "__main__":
|
|
157
|
+
filename = f'name-of-file_{datetime.datetime.now().strftime("%H%M_%m%d%Y")}'
|
|
158
|
+
|
|
156
159
|
# streamer = streamHandler()
|
|
157
|
-
streamer = sh.WebXiStreamHandler(host, sequenceID=sequenceID, mode="flac", saving="
|
|
160
|
+
streamer = sh.WebXiStreamHandler(host, sequenceID=sequenceID, mode="flac", saving="wav", saving_path=f"./saved_data/flac_{filename}")
|
|
158
161
|
# streamer.setDataHandler(MultiDataHandler(PrintDataHandler(), CsvDataHandler(r"C:\Users\SKREBSVINGAA\csv_test\csv_test.txt")))
|
|
159
162
|
fig = figureHandler()
|
|
160
163
|
fig.app.aboutToQuit.connect(on_close)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Optional DataHandler for WebXiStreamHandler. By default WebXiStreamHandler
|
|
2
|
+
does nothing with decoded values - call streamer.setDataHandler(...) with an
|
|
3
|
+
instance of one of these (or your own subclass) before startStream() to
|
|
4
|
+
receive/print/save them."""
|
|
5
|
+
|
|
6
|
+
import csv
|
|
7
|
+
import json
|
|
8
|
+
import tempfile
|
|
9
|
+
import threading
|
|
10
|
+
import warnings
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import numpy as np
|
|
14
|
+
import soundfile as sf
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataHandler:
|
|
18
|
+
"""Override handle() to decide what happens with decoded stream values.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle(self, **data):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _BufferedFileDataHandler(DataHandler):
|
|
27
|
+
"""Buffers records and publishes snapshots in a background worker.
|
|
28
|
+
Call close() to stop the worker and save remaining records. WAV writes chunks.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_extension = None # set by subclasses, e.g. "json", "csv", "wav"
|
|
32
|
+
|
|
33
|
+
def __init__(self, path, snapshot_interval=1.0):
|
|
34
|
+
if not np.isfinite(snapshot_interval) or snapshot_interval <= 0:
|
|
35
|
+
raise ValueError("Snapshot interval must be positive and finite")
|
|
36
|
+
path = Path(path)
|
|
37
|
+
if self._extension and path.suffix.lower() != f".{self._extension}":
|
|
38
|
+
path = path.with_name(path.name + f".{self._extension}")
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
self._path = path
|
|
41
|
+
self._records = []
|
|
42
|
+
self._lock = threading.RLock()
|
|
43
|
+
self._write_lock = threading.Lock()
|
|
44
|
+
self._snapshot_interval = snapshot_interval
|
|
45
|
+
self._stop_event = threading.Event()
|
|
46
|
+
self._worker = None
|
|
47
|
+
self._saved_count = -1
|
|
48
|
+
self._closed = False
|
|
49
|
+
self._write_error = None
|
|
50
|
+
|
|
51
|
+
def handle(self, **data):
|
|
52
|
+
with self._lock:
|
|
53
|
+
if self._closed:
|
|
54
|
+
return
|
|
55
|
+
if self._write_error is not None:
|
|
56
|
+
raise RuntimeError("Background snapshot saving failed") from self._write_error
|
|
57
|
+
if "values" in data:
|
|
58
|
+
data["values"] = [float(v) for v in data["values"]]
|
|
59
|
+
elif "samples" in data:
|
|
60
|
+
samples = data.pop("samples")
|
|
61
|
+
data.update(count=len(samples), min=float(min(samples)), max=float(max(samples)))
|
|
62
|
+
self._records.append(data)
|
|
63
|
+
if self._worker is None:
|
|
64
|
+
self._worker = threading.Thread(
|
|
65
|
+
target=self._run_snapshots, name=f"snapshot-{self._path.name}", daemon=True,
|
|
66
|
+
)
|
|
67
|
+
self._worker.start()
|
|
68
|
+
|
|
69
|
+
def _run_snapshots(self):
|
|
70
|
+
while not self._stop_event.wait(self._snapshot_interval):
|
|
71
|
+
try:
|
|
72
|
+
self._flush_snapshot()
|
|
73
|
+
except Exception as error:
|
|
74
|
+
with self._lock:
|
|
75
|
+
self._write_error = error
|
|
76
|
+
self._stop_event.set()
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
def _flush_snapshot(self):
|
|
80
|
+
with self._write_lock:
|
|
81
|
+
with self._lock:
|
|
82
|
+
count = len(self._records)
|
|
83
|
+
if count == self._saved_count:
|
|
84
|
+
return
|
|
85
|
+
records = self._records.copy()
|
|
86
|
+
self._write_snapshot(records)
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._saved_count = count
|
|
89
|
+
|
|
90
|
+
def _write_snapshot(self, records):
|
|
91
|
+
raise NotImplementedError
|
|
92
|
+
|
|
93
|
+
@contextmanager
|
|
94
|
+
def _snapshot_file(self, mode, **options):
|
|
95
|
+
temporary_path = None
|
|
96
|
+
try:
|
|
97
|
+
with tempfile.NamedTemporaryFile(
|
|
98
|
+
mode=mode, dir=self._path.parent, prefix=f".{self._path.name}.",
|
|
99
|
+
suffix=".tmp", delete=False, **options,
|
|
100
|
+
) as target:
|
|
101
|
+
temporary_path = Path(target.name)
|
|
102
|
+
yield target
|
|
103
|
+
temporary_path.replace(self._path)
|
|
104
|
+
finally:
|
|
105
|
+
if temporary_path is not None:
|
|
106
|
+
temporary_path.unlink(missing_ok=True)
|
|
107
|
+
|
|
108
|
+
def close(self):
|
|
109
|
+
with self._lock:
|
|
110
|
+
self._closed = True
|
|
111
|
+
self._stop_event.set()
|
|
112
|
+
worker = self._worker
|
|
113
|
+
if worker is not None:
|
|
114
|
+
worker.join()
|
|
115
|
+
self._flush_snapshot()
|
|
116
|
+
|
|
117
|
+
def _group_records(self, records=None):
|
|
118
|
+
hosts = {}
|
|
119
|
+
for record in self._records if records is None else records:
|
|
120
|
+
row = record.copy()
|
|
121
|
+
if not row.get("timestamp"):
|
|
122
|
+
row.pop("timestamp", None)
|
|
123
|
+
host = row.pop("host", None)
|
|
124
|
+
rows = hosts.setdefault(host, [])
|
|
125
|
+
if "name" in row and "value" in row:
|
|
126
|
+
name = row.pop("name")
|
|
127
|
+
value = row.pop("value")
|
|
128
|
+
if (rows and rows[-1].get("timestamp") == row.get("timestamp")
|
|
129
|
+
and name not in rows[-1]):
|
|
130
|
+
rows[-1].update(row)
|
|
131
|
+
rows[-1][name] = value
|
|
132
|
+
else:
|
|
133
|
+
row[name] = value
|
|
134
|
+
rows.append(row)
|
|
135
|
+
else:
|
|
136
|
+
rows.append(row)
|
|
137
|
+
return hosts
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class JsonDataHandler(_BufferedFileDataHandler):
|
|
141
|
+
"""Writes records grouped by host, combining named values per timestamp."""
|
|
142
|
+
_extension = "json"
|
|
143
|
+
|
|
144
|
+
def _write_snapshot(self, records):
|
|
145
|
+
with self._snapshot_file("w", encoding="utf-8") as target:
|
|
146
|
+
records = [{"host": host, "data": rows} for host, rows in self._group_records(records).items()]
|
|
147
|
+
json.dump(
|
|
148
|
+
records,
|
|
149
|
+
target,
|
|
150
|
+
indent=2,
|
|
151
|
+
default=lambda value: (
|
|
152
|
+
value.tolist()
|
|
153
|
+
if isinstance(value, (np.ndarray, np.generic))
|
|
154
|
+
else str(value)
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
class CsvDataHandler(_BufferedFileDataHandler):
|
|
159
|
+
"""Writes rows per host and timestamp, storing vectors as JSON list cells."""
|
|
160
|
+
_extension = "csv"
|
|
161
|
+
|
|
162
|
+
def _write_snapshot(self, records):
|
|
163
|
+
with self._snapshot_file("w", encoding="utf-8", newline="") as target:
|
|
164
|
+
records = [
|
|
165
|
+
{"host": host, **row}
|
|
166
|
+
for host, rows in self._group_records(records).items()
|
|
167
|
+
for row in rows
|
|
168
|
+
]
|
|
169
|
+
for record in records:
|
|
170
|
+
for name, value in record.items():
|
|
171
|
+
if isinstance(value, np.ndarray):
|
|
172
|
+
record[name] = json.dumps(value.tolist())
|
|
173
|
+
elif isinstance(value, (list, tuple)):
|
|
174
|
+
record[name] = json.dumps(value)
|
|
175
|
+
fields = dict.fromkeys(key for record in records for key in record)
|
|
176
|
+
fieldnames = [key for key in ("host", "timestamp") if key in fields]
|
|
177
|
+
fieldnames.extend(key for key in fields if key not in fieldnames)
|
|
178
|
+
writer = csv.DictWriter(target, fieldnames=fieldnames)
|
|
179
|
+
writer.writeheader()
|
|
180
|
+
writer.writerows(records)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class WavDataHandler(_BufferedFileDataHandler):
|
|
184
|
+
"""Writes PCM_24 chunks scaled by the full-scale pressure in the filename."""
|
|
185
|
+
_extension = "wav"
|
|
186
|
+
|
|
187
|
+
def __init__(self, path, sample_rate=None, full_scale_pa=None):
|
|
188
|
+
super().__init__(path)
|
|
189
|
+
self._sample_rate = sample_rate
|
|
190
|
+
self._full_scale_pa = full_scale_pa
|
|
191
|
+
self._started = False
|
|
192
|
+
self._closed = False
|
|
193
|
+
|
|
194
|
+
def handle(self, *, samples, sample_rate=None, full_scale_pa=None, **data):
|
|
195
|
+
with self._lock:
|
|
196
|
+
if self._closed:
|
|
197
|
+
return
|
|
198
|
+
rate = sample_rate if sample_rate is not None else self._sample_rate
|
|
199
|
+
if rate is None or rate <= 0 or int(rate) != rate:
|
|
200
|
+
raise ValueError("WAV samples require a positive integer sample rate")
|
|
201
|
+
if self._sample_rate is not None and rate != self._sample_rate:
|
|
202
|
+
raise ValueError("Sample rate cannot change within a WAV file")
|
|
203
|
+
scale = full_scale_pa if full_scale_pa is not None else self._full_scale_pa
|
|
204
|
+
if scale is None or not np.isfinite(scale) or scale <= 0:
|
|
205
|
+
raise ValueError("PCM_24 WAV samples require a positive finite full_scale_pa")
|
|
206
|
+
if self._full_scale_pa is not None and scale != self._full_scale_pa:
|
|
207
|
+
raise ValueError("Full-scale pressure cannot change within a WAV file")
|
|
208
|
+
values = np.asarray(samples, dtype=np.float64)
|
|
209
|
+
if values.ndim != 1:
|
|
210
|
+
raise ValueError("WAV samples must be a mono, one-dimensional array")
|
|
211
|
+
if not np.isfinite(values).all():
|
|
212
|
+
raise ValueError("WAV samples must be finite")
|
|
213
|
+
self._sample_rate = int(rate)
|
|
214
|
+
self._full_scale_pa = float(scale)
|
|
215
|
+
if values.size:
|
|
216
|
+
normalized = values / self._full_scale_pa
|
|
217
|
+
if np.any(normalized > 1.0) or np.any(normalized < -(2 ** 23) / (2 ** 23 - 1)):
|
|
218
|
+
warnings.warn("Audio exceeds the calibrated full-scale pressure; PCM_24 samples will clip", RuntimeWarning, stacklevel=2)
|
|
219
|
+
normalized = np.clip(normalized, -1.0, 1.0)
|
|
220
|
+
if not self._started:
|
|
221
|
+
self._path = self._path.with_name(
|
|
222
|
+
f"{self._path.stem}_fullscale-{self._full_scale_pa:.17g}Pa.wav"
|
|
223
|
+
)
|
|
224
|
+
options = {"mode": "r+"} if self._started else {
|
|
225
|
+
"mode": "w", "samplerate": self._sample_rate,
|
|
226
|
+
"channels": 1, "format": "WAV", "subtype": "PCM_24",
|
|
227
|
+
}
|
|
228
|
+
with sf.SoundFile(self._path, **options) as target:
|
|
229
|
+
target.seek(0, sf.SEEK_END)
|
|
230
|
+
target.write(normalized)
|
|
231
|
+
self._started = True
|
|
232
|
+
|
|
233
|
+
def close(self):
|
|
234
|
+
with self._lock:
|
|
235
|
+
self._closed = True
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class MultiDataHandler(DataHandler):
|
|
239
|
+
"""Fans out to several handlers at once (e.g. print AND save AND plot)."""
|
|
240
|
+
|
|
241
|
+
def __init__(self, *handlers):
|
|
242
|
+
self._handlers = handlers
|
|
243
|
+
|
|
244
|
+
def handle(self, **data):
|
|
245
|
+
for h in self._handlers:
|
|
246
|
+
h.handle(**data)
|
|
247
|
+
|
|
248
|
+
def close(self):
|
|
249
|
+
for h in self._handlers:
|
|
250
|
+
if hasattr(h, "close"):
|
|
251
|
+
h.close()
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/stream_handlers.py
RENAMED
|
@@ -5,7 +5,7 @@ is picked with boolean flags (cpb, multi, flac, mp3) instead of subclassing."""
|
|
|
5
5
|
from . import https_requests
|
|
6
6
|
import time
|
|
7
7
|
import re
|
|
8
|
-
from datetime import datetime
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
9
|
from timeit import default_timer as timer
|
|
10
10
|
import numpy as np
|
|
11
11
|
import miniaudio
|
|
@@ -19,14 +19,14 @@ from . import flac_stream_2_samples as flac2samples
|
|
|
19
19
|
from .tmpFileStateMachine import TmpfileStateMachine
|
|
20
20
|
from ..webxi import webxi_stream
|
|
21
21
|
from slm_api.enums.fast_log_intervals_enum import FastLogInterval
|
|
22
|
-
from .data_handler import MultiDataHandler, JsonDataHandler, CsvDataHandler,
|
|
22
|
+
from .data_handler import MultiDataHandler, JsonDataHandler, CsvDataHandler, WavDataHandler
|
|
23
23
|
|
|
24
24
|
_SAVING_HANDLERS = {
|
|
25
25
|
"json": JsonDataHandler,
|
|
26
26
|
"csv": CsvDataHandler,
|
|
27
|
-
"
|
|
27
|
+
"wav": WavDataHandler,
|
|
28
28
|
}
|
|
29
|
-
_SAVING_DEFAULT_EXTENSIONS = {"json": "json", "csv": "csv", "
|
|
29
|
+
_SAVING_DEFAULT_EXTENSIONS = {"json": "json", "csv": "csv", "wav": "wav"}
|
|
30
30
|
|
|
31
31
|
# WebXi header timestamps are 64-bit fixed-point seconds since the Unix epoch
|
|
32
32
|
# (Q32.32): upper 32 bits = whole seconds, lower 32 bits = fraction of a second.
|
|
@@ -65,15 +65,21 @@ def _set_fast_logging_interval(host, enum_value):
|
|
|
65
65
|
def _get_slm_utc_offset_seconds(host):
|
|
66
66
|
"""Read the SLM's own configured time zone offset from UTC, in seconds.
|
|
67
67
|
The device reports it as a string like 'GMT+02:00'."""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
endpoints = (
|
|
69
|
+
"/WebXi/Applications/SLM/Outputs/TimeZone",
|
|
70
|
+
"/WebXi/Applications/SLM/Setup/RegionalTimeZoneGMT",
|
|
71
|
+
)
|
|
72
|
+
for endpoint in endpoints:
|
|
73
|
+
response = https_requests.get(host, endpoint)
|
|
74
|
+
response.raise_for_status()
|
|
75
|
+
value = str(response.json()).strip()
|
|
76
|
+
match = re.fullmatch(r"(?:GMT|UTC)?([+-])(\d{2}):?(\d{2})", value)
|
|
77
|
+
if match:
|
|
78
|
+
sign, hours, minutes = match.groups()
|
|
79
|
+
if int(hours) < 24 and int(minutes) < 60:
|
|
80
|
+
offset = int(hours) * 3600 + int(minutes) * 60
|
|
81
|
+
return -offset if sign == "-" else offset
|
|
82
|
+
raise ValueError("SLM reported no valid timezone in Outputs/TimeZone or Setup/RegionalTimeZoneGMT")
|
|
77
83
|
|
|
78
84
|
|
|
79
85
|
class _TimeStamps:
|
|
@@ -176,8 +182,9 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
176
182
|
- mode="screen_record": streams the device's ScreenImage sequence, keeping the latest
|
|
177
183
|
received PNG in `self.image_bytes`.
|
|
178
184
|
|
|
179
|
-
time=True additionally subscribes the device's
|
|
180
|
-
|
|
185
|
+
time=True additionally subscribes the device's SLMTime sequence and supplies
|
|
186
|
+
ISO timestamps in the SLM's configured time zone. Fast logging uses the device
|
|
187
|
+
header time plus the sample interval for per-sample timestamps.
|
|
181
188
|
"""
|
|
182
189
|
|
|
183
190
|
def __init__(self, host, sequenceID=None, sequenceNames=None,
|
|
@@ -220,6 +227,8 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
220
227
|
if saving is not None:
|
|
221
228
|
if saving.lower() not in _SAVING_HANDLERS:
|
|
222
229
|
raise ValueError(f"Invalid saving format: {saving!r}, must be one of {list(_SAVING_HANDLERS)}")
|
|
230
|
+
if saving.lower() == "wav" and not (self.mp3 or self.flac):
|
|
231
|
+
raise ValueError("WAV saving is only supported for FLAC and MP3 streams")
|
|
223
232
|
if saving_path is None:
|
|
224
233
|
saving_path = "stream_data"
|
|
225
234
|
self._saving_handler = _SAVING_HANDLERS[saving.lower()](saving_path)
|
|
@@ -247,6 +256,13 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
247
256
|
self.calibrationFactor = (20e-6 * 10 ** (max_lvl / 20)) / (2 ** 23 - 1) * np.sqrt(2)
|
|
248
257
|
|
|
249
258
|
def streamInit(self):
|
|
259
|
+
if (self.mp3 or self.flac) and isinstance(
|
|
260
|
+
self._saving_handler,
|
|
261
|
+
(JsonDataHandler, CsvDataHandler),
|
|
262
|
+
):
|
|
263
|
+
raise ValueError(
|
|
264
|
+
"FLAC and MP3 streams must be saved as WAV, not JSON or CSV."
|
|
265
|
+
)
|
|
250
266
|
if self.mp3 or self.flac:
|
|
251
267
|
quality_setting = "AudioRecordingListenQuality" if self.mp3 else "AudioRecordingAnalysisQuality"
|
|
252
268
|
response = https_requests.put(self.host, f"/WebXi/Applications/SLM/setup/{quality_setting}", json=1)
|
|
@@ -286,17 +302,17 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
286
302
|
self.data_type = self.sequence["DataType"]
|
|
287
303
|
|
|
288
304
|
stream_ids = list(self.ID) if isinstance(self.ID, list) else [self.ID]
|
|
289
|
-
self.
|
|
305
|
+
if self.time:
|
|
306
|
+
utc_offset_seconds = _get_slm_utc_offset_seconds(self.host)
|
|
307
|
+
self._slm_timezone = timezone(timedelta(seconds=utc_offset_seconds))
|
|
308
|
+
self._slm_time = None
|
|
309
|
+
time_id, _ = seq.get_sequence(self.host, seq.getSequenceID(self.host, "SLMTime"))
|
|
310
|
+
self._time_ids = {"SLMTime": time_id}
|
|
311
|
+
stream_ids.append(time_id)
|
|
312
|
+
self.stream_ids = stream_ids
|
|
290
313
|
if self.multi_device:
|
|
291
314
|
return
|
|
292
315
|
|
|
293
|
-
if self.time:
|
|
294
|
-
utc_offset_seconds = _get_slm_utc_offset_seconds(self.host)
|
|
295
|
-
self._time_stamps = _TimeStamps(utc_offset_seconds=utc_offset_seconds)
|
|
296
|
-
start_id, _ = seq.get_sequence(self.host, seq.getSequenceID(self.host, "StartTime"))
|
|
297
|
-
elapsed_id, _ = seq.get_sequence(self.host, seq.getSequenceID(self.host, "ElapsedTime"))
|
|
298
|
-
self._time_ids = {"StartTime": start_id, "ElapsedTime": elapsed_id}
|
|
299
|
-
stream_ids += [start_id, elapsed_id]
|
|
300
316
|
self.uri = stream.setup_stream(self.host, stream_ids, self.streamName)
|
|
301
317
|
meas.start_pause_measurement(self.host, True)
|
|
302
318
|
|
|
@@ -321,13 +337,20 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
321
337
|
if package.header.message_type != webxi_stream.WebxiStream.Header.EMessageType.e_sequence_data:
|
|
322
338
|
return
|
|
323
339
|
for block in package.content.sequence_blocks:
|
|
324
|
-
if block
|
|
325
|
-
self.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
340
|
+
if getattr(block, "sequence_id", None) == self._time_ids["SLMTime"]:
|
|
341
|
+
self._slm_time = int.from_bytes(block.values, "little", signed=True)
|
|
342
|
+
|
|
343
|
+
def _time_prefix(self, epoch_seconds=None):
|
|
344
|
+
if not self.time:
|
|
345
|
+
return ""
|
|
346
|
+
if epoch_seconds is None:
|
|
347
|
+
epoch_seconds = self._slm_time
|
|
348
|
+
if epoch_seconds is None:
|
|
349
|
+
return ""
|
|
350
|
+
timestamp = datetime.fromtimestamp(epoch_seconds, self._slm_timezone)
|
|
351
|
+
if self.fast_logging:
|
|
352
|
+
return timestamp.isoformat(timespec="microseconds")
|
|
353
|
+
return timestamp.strftime("%Y-%m-%dT%H:%M:%S")
|
|
331
354
|
|
|
332
355
|
def _handle_data(self, **data):
|
|
333
356
|
"""Deliver a stream record with the host it originated from."""
|
|
@@ -344,9 +367,10 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
344
367
|
def _msg_func_cpb(self, message):
|
|
345
368
|
package = webxi_stream.WebxiStream.from_bytes(message)
|
|
346
369
|
if package.header.message_type == webxi_stream.WebxiStream.Header.EMessageType.e_sequence_data:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
370
|
+
for block in package.content.sequence_blocks:
|
|
371
|
+
if block.sequence_id == self.ID:
|
|
372
|
+
self.CPB_values = np.array(stream.data_type_conv(self.sequence["DataType"], block.values, self.sequence["VectorLength"])) / 100
|
|
373
|
+
self._handle_data(timestamp=self._time_prefix(), name=self.sequence["Name"], value=self.CPB_values)
|
|
350
374
|
|
|
351
375
|
def calcFreqBands(self):
|
|
352
376
|
"""Only meaningful when cpb=True."""
|
|
@@ -371,11 +395,7 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
371
395
|
if package.header.message_type != webxi_stream.WebxiStream.Header.EMessageType.e_sequence_data:
|
|
372
396
|
return
|
|
373
397
|
|
|
374
|
-
# The header time is the time of the first sample; fall back to wall clock if
|
|
375
|
-
# it is not in the expected Q32.32 format/epoch.
|
|
376
398
|
start = _webxi_time_to_epoch(package.header.time)
|
|
377
|
-
if abs(start - time.time()) > 86400:
|
|
378
|
-
start = time.time()
|
|
379
399
|
|
|
380
400
|
# A 1-second package batches many fast-logged samples.
|
|
381
401
|
samples = []
|
|
@@ -398,8 +418,9 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
398
418
|
name = self.sequence.get("Name", "FastLAeq")
|
|
399
419
|
local = self.sequence.get("LocalName", "LAeq")
|
|
400
420
|
unit = self.sequence.get("Unit", "dB re 20uPa")
|
|
401
|
-
for
|
|
402
|
-
|
|
421
|
+
for sample_index, value in enumerate(samples):
|
|
422
|
+
timestamp = self._time_prefix(start + sample_index * self.fast_logging_interval_s)
|
|
423
|
+
self._handle_data(timestamp=timestamp, name=name, local=local, value=value, unit=unit)
|
|
403
424
|
|
|
404
425
|
def _msg_func_screen_record(self, message):
|
|
405
426
|
package = webxi_stream.WebxiStream.from_bytes(message)
|
|
@@ -414,16 +435,22 @@ class WebXiStreamHandler(stream.StreamHandler):
|
|
|
414
435
|
start = timer()
|
|
415
436
|
package = webxi_stream.WebxiStream.from_bytes(message)
|
|
416
437
|
if package.header.message_type == webxi_stream.WebxiStream.Header.EMessageType.e_sequence_data:
|
|
417
|
-
block
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
438
|
+
for block in package.content.sequence_blocks:
|
|
439
|
+
if self.ID not in getattr(block, "sequence_ids", ()):
|
|
440
|
+
continue
|
|
441
|
+
if self.mp3:
|
|
442
|
+
samples, sample_rate = self.tmpFileSM.decode(block.frame)
|
|
443
|
+
if not samples:
|
|
444
|
+
continue
|
|
445
|
+
decoded = (np.asarray(samples, dtype=np.int32) << 8) * self.calibrationFactor
|
|
446
|
+
else:
|
|
447
|
+
decoded = flac2samples.decode(block, self.calibrationFactor)
|
|
448
|
+
sample_rate = 2 ** 16
|
|
449
|
+
DataBuffer.append(decoded)
|
|
450
|
+
self._handle_data(
|
|
451
|
+
samples=decoded, sample_rate=sample_rate,
|
|
452
|
+
full_scale_pa=self.calibrationFactor * (2 ** 23 - 1),
|
|
453
|
+
)
|
|
427
454
|
total = timer() - start
|
|
428
455
|
if 0.0625 < total:
|
|
429
456
|
print(f"TotalTime: {total}")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
import tempfile
|
|
3
|
+
import miniaudio
|
|
3
4
|
|
|
4
5
|
# a dual bufferfile stores the data which is used for decoding the mp3 stream.
|
|
5
6
|
class TmpfileStateMachine:
|
|
@@ -15,6 +16,29 @@ class TmpfileStateMachine:
|
|
|
15
16
|
self.tempFile1 = None
|
|
16
17
|
self.tempFile2 = None
|
|
17
18
|
self.state = self.State.init
|
|
19
|
+
self._decoded_file = None
|
|
20
|
+
self._decoded_frames = 0
|
|
21
|
+
|
|
22
|
+
def decode(self, mp3Data):
|
|
23
|
+
active_file = (
|
|
24
|
+
self.tempFile1
|
|
25
|
+
if self.state in (self.State.init, self.State.file1, self.State.preFile2)
|
|
26
|
+
else self.tempFile2
|
|
27
|
+
)
|
|
28
|
+
if active_file is not self._decoded_file:
|
|
29
|
+
self._decoded_frames = 0
|
|
30
|
+
if active_file is not None:
|
|
31
|
+
active_file.seek(0)
|
|
32
|
+
overlap = active_file.read()
|
|
33
|
+
if overlap:
|
|
34
|
+
self._decoded_frames = miniaudio.mp3_read_s16(overlap).num_frames
|
|
35
|
+
decoded = miniaudio.mp3_read_s16(self.runStateMachine(mp3Data))
|
|
36
|
+
if decoded.nchannels != 1:
|
|
37
|
+
raise ValueError("SLM MP3 audio must be mono")
|
|
38
|
+
samples = decoded.samples[self._decoded_frames:]
|
|
39
|
+
self._decoded_frames = decoded.num_frames
|
|
40
|
+
self._decoded_file = active_file if active_file is not None else self.tempFile1
|
|
41
|
+
return samples, decoded.sample_rate
|
|
18
42
|
|
|
19
43
|
def runStateMachine(self, mp3Data):
|
|
20
44
|
readData = None
|
|
@@ -46,9 +70,10 @@ class TmpfileStateMachine:
|
|
|
46
70
|
if self.tempFile2:
|
|
47
71
|
self.tempFile2.close()
|
|
48
72
|
self.tempFile2 = None
|
|
73
|
+
self.tempFile1.seek(0, 2)
|
|
74
|
+
self.tempFile1.write(bytes(mp3Data))
|
|
49
75
|
self.tempFile1.seek(0)
|
|
50
76
|
dataFromFile = self.tempFile1.read()
|
|
51
|
-
self.tempFile1.write(bytes(mp3Data))
|
|
52
77
|
if (len(dataFromFile) > 20 * 1024):
|
|
53
78
|
self.state = self.State.preFile2
|
|
54
79
|
return dataFromFile
|
|
@@ -57,12 +82,12 @@ class TmpfileStateMachine:
|
|
|
57
82
|
if not self.tempFile2:
|
|
58
83
|
self.tempFile2 = tempfile.SpooledTemporaryFile(max_size=1 * 1024 * 1024, mode='w+b', suffix=".mp3",
|
|
59
84
|
prefix="tmp2")
|
|
60
|
-
self.tempFile1.seek(0)
|
|
61
|
-
self.tempFile2.seek(0)
|
|
62
|
-
dataFromFile = self.tempFile1.read()
|
|
63
|
-
tmp = self.tempFile2.read()
|
|
85
|
+
self.tempFile1.seek(0, 2)
|
|
86
|
+
self.tempFile2.seek(0, 2)
|
|
64
87
|
self.tempFile1.write(bytes(mp3Data))
|
|
65
88
|
self.tempFile2.write(bytes(mp3Data))
|
|
89
|
+
self.tempFile1.seek(0)
|
|
90
|
+
dataFromFile = self.tempFile1.read()
|
|
66
91
|
if (len(dataFromFile) > 30 * 1024):
|
|
67
92
|
self.state = self.State.file2
|
|
68
93
|
return dataFromFile
|
|
@@ -71,9 +96,10 @@ class TmpfileStateMachine:
|
|
|
71
96
|
if self.tempFile1:
|
|
72
97
|
self.tempFile1.close()
|
|
73
98
|
self.tempFile1 = None
|
|
99
|
+
self.tempFile2.seek(0, 2)
|
|
100
|
+
self.tempFile2.write(bytes(mp3Data))
|
|
74
101
|
self.tempFile2.seek(0)
|
|
75
102
|
dataFromFile = self.tempFile2.read()
|
|
76
|
-
self.tempFile2.write(bytes(mp3Data))
|
|
77
103
|
if (len(dataFromFile) > 20 * 1024):
|
|
78
104
|
self.state = self.State.preFile1
|
|
79
105
|
return dataFromFile
|
|
@@ -82,12 +108,12 @@ class TmpfileStateMachine:
|
|
|
82
108
|
if not self.tempFile1:
|
|
83
109
|
self.tempFile1 = tempfile.SpooledTemporaryFile(max_size=1 * 1024 * 1024, mode='w+b', suffix=".mp3",
|
|
84
110
|
prefix="tmp1")
|
|
85
|
-
self.tempFile1.seek(0)
|
|
86
|
-
self.tempFile2.seek(0)
|
|
87
|
-
dataFromFile = self.tempFile2.read()
|
|
88
|
-
tmp = self.tempFile1.read()
|
|
111
|
+
self.tempFile1.seek(0, 2)
|
|
112
|
+
self.tempFile2.seek(0, 2)
|
|
89
113
|
self.tempFile1.write(bytes(mp3Data))
|
|
90
114
|
self.tempFile2.write(bytes(mp3Data))
|
|
115
|
+
self.tempFile2.seek(0)
|
|
116
|
+
dataFromFile = self.tempFile2.read()
|
|
91
117
|
if (len(dataFromFile) > 30 * 1024):
|
|
92
118
|
self.state = self.State.file1
|
|
93
119
|
return dataFromFile
|
|
@@ -22,6 +22,8 @@ def turn_on_bb_freq_weight(host, weights, fast=False):
|
|
|
22
22
|
for weight in weights:
|
|
23
23
|
put(host , f"/webxi/Applications/SLM/Setup/BBFreqWeight{weight}", json=True)
|
|
24
24
|
else:
|
|
25
|
+
for weight in weights:
|
|
26
|
+
put(host , f"/webxi/Applications/SLM/Setup/BBFreqWeight{weight}", json=True)
|
|
25
27
|
for weight in weights:
|
|
26
28
|
put(host , f"/webxi/Applications/SLM/Setup/FastBBFreqWeight{weight}", json=True)
|
|
27
29
|
|
|
@@ -29,6 +29,7 @@ from slm_api.helpers.FigureHandler import FigureHandler
|
|
|
29
29
|
from slm_api.helpers import TmpfileStateMachine
|
|
30
30
|
from slm_api.helpers import webxi_helper_functions as webxi_helper
|
|
31
31
|
from slm_api.helpers.data_handler import CsvDataHandler, MultiDataHandler, DataHandler
|
|
32
|
+
import datetime
|
|
32
33
|
|
|
33
34
|
host = webxi_helper.set_host(__file__)
|
|
34
35
|
sequenceID = 156
|
|
@@ -154,9 +155,11 @@ def on_close():
|
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
if __name__ == "__main__":
|
|
158
|
+
|
|
159
|
+
filename = f'name-of-file_{datetime.datetime.now().strftime("%H%M_%m%d%Y")}'
|
|
160
|
+
|
|
157
161
|
# streamer = streamHandler()
|
|
158
|
-
streamer = sh.WebXiStreamHandler(host, sequenceID, mode="mp3")
|
|
159
|
-
# streamer.setDataHandler(CsvDataHandler(r"C:\Users\SKREBSVINGAA\csv_test\csv_test.txt"))
|
|
162
|
+
streamer = sh.WebXiStreamHandler(host, sequenceID, mode="mp3", saving="wav", saving_path=f"./saved_data/mp3_{filename}")
|
|
160
163
|
# streamer.setDataHandler(PrintDataHandler())
|
|
161
164
|
|
|
162
165
|
fig = figureHandler()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Plot saved JSON/CSV measurements without connecting to an SLM.
|
|
2
|
+
|
|
3
|
+
Usage: uv run src/slm_api/plot_saved_data_example.py saved_data/measurements.json
|
|
4
|
+
Add --output-dir saved_data/plots to save PNGs instead of opening plot windows.
|
|
5
|
+
CPB bars energy-average equally weighted saved readings, not measurement durations.
|
|
6
|
+
Band numbers are used because the saved files do not include centre frequencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import matplotlib.dates as mdates
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_records(path):
|
|
21
|
+
path = Path(path)
|
|
22
|
+
if path.suffix.lower() == ".csv":
|
|
23
|
+
frame = pd.read_csv(path, encoding="utf-8-sig", dtype=str, keep_default_na=False)
|
|
24
|
+
return frame.to_dict(orient="records")
|
|
25
|
+
if path.suffix.lower() != ".json":
|
|
26
|
+
raise ValueError("Select a .json or .csv measurement file")
|
|
27
|
+
frame = pd.read_json(path, encoding="utf-8-sig", orient="records", convert_dates=False, dtype=False)
|
|
28
|
+
frame = frame.astype(object).where(pd.notna(frame), None)
|
|
29
|
+
records = []
|
|
30
|
+
for entry in frame.to_dict(orient="records"):
|
|
31
|
+
entry = {key: value for key, value in entry.items() if value is not None}
|
|
32
|
+
if "data" in entry:
|
|
33
|
+
if not isinstance(entry["data"], list):
|
|
34
|
+
raise ValueError("Each host's data must be a list")
|
|
35
|
+
for row in entry["data"]:
|
|
36
|
+
if not isinstance(row, dict):
|
|
37
|
+
raise ValueError("Each measurement row must be an object")
|
|
38
|
+
records.append({**row, "host": entry.get("host", "Unknown")})
|
|
39
|
+
else:
|
|
40
|
+
records.append(entry)
|
|
41
|
+
return records
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def measurement_value(value, name):
|
|
45
|
+
if isinstance(value, str):
|
|
46
|
+
if not value.strip():
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
value = json.loads(value)
|
|
50
|
+
except ValueError:
|
|
51
|
+
if name.upper().startswith("CPB"):
|
|
52
|
+
raise ValueError(f"{name} must contain a numeric JSON array, not array display text") from None
|
|
53
|
+
return None
|
|
54
|
+
if isinstance(value, bool) or value is None:
|
|
55
|
+
return None
|
|
56
|
+
if not isinstance(value, (int, float, list)):
|
|
57
|
+
return None
|
|
58
|
+
values = np.asarray(value, dtype=float)
|
|
59
|
+
if values.ndim > 1:
|
|
60
|
+
raise ValueError(f"{name} must be a scalar or a one-dimensional band array")
|
|
61
|
+
return values
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def plot_records(records):
|
|
65
|
+
hosts = {}
|
|
66
|
+
for record in records:
|
|
67
|
+
hosts.setdefault(record.get("host") or "Unknown", []).append(record)
|
|
68
|
+
figures = []
|
|
69
|
+
metadata = {"host", "timestamp", "name", "local", "unit", "count", "min", "max"}
|
|
70
|
+
for host, rows in hosts.items():
|
|
71
|
+
broadband = {}
|
|
72
|
+
spectra = {}
|
|
73
|
+
for row_index, row in enumerate(rows):
|
|
74
|
+
measurements = (
|
|
75
|
+
{row["name"]: row["value"]}
|
|
76
|
+
if "name" in row and "value" in row else row
|
|
77
|
+
)
|
|
78
|
+
for name, value in measurements.items():
|
|
79
|
+
if name in metadata:
|
|
80
|
+
continue
|
|
81
|
+
values = measurement_value(value, name)
|
|
82
|
+
if values is None or values.size == 0:
|
|
83
|
+
continue
|
|
84
|
+
if values.ndim == 1:
|
|
85
|
+
spectra.setdefault(name, []).append(values)
|
|
86
|
+
elif np.isfinite(values):
|
|
87
|
+
broadband.setdefault(name, []).append((row_index, float(values)))
|
|
88
|
+
|
|
89
|
+
if broadband:
|
|
90
|
+
timed = all(row.get("timestamp") for row in rows)
|
|
91
|
+
if timed:
|
|
92
|
+
try:
|
|
93
|
+
positions = [datetime.fromisoformat(row["timestamp"].strip()) for row in rows]
|
|
94
|
+
offsets = {position.utcoffset() for position in positions}
|
|
95
|
+
if len(offsets) != 1:
|
|
96
|
+
raise ValueError("Inconsistent timezone offsets")
|
|
97
|
+
except ValueError:
|
|
98
|
+
timed = False
|
|
99
|
+
if not timed:
|
|
100
|
+
positions = list(range(1, len(rows) + 1))
|
|
101
|
+
figure, axis = plt.subplots(figsize=(12, 5), layout="constrained")
|
|
102
|
+
for name, points in broadband.items():
|
|
103
|
+
indices, levels = zip(*points)
|
|
104
|
+
axis.plot([positions[index] for index in indices], levels, label=name)
|
|
105
|
+
if timed:
|
|
106
|
+
locator = mdates.AutoDateLocator()
|
|
107
|
+
axis.xaxis.set_major_locator(locator)
|
|
108
|
+
axis.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
|
|
109
|
+
axis.set(title=f"{host} - Broadband", xlabel="Time" if timed else "Record number", ylabel="Level [dB SPL]")
|
|
110
|
+
axis.grid(alpha=0.3)
|
|
111
|
+
axis.legend()
|
|
112
|
+
figures.append(figure)
|
|
113
|
+
|
|
114
|
+
for name, readings in spectra.items():
|
|
115
|
+
band_count = len(readings[0])
|
|
116
|
+
if any(len(reading) != band_count for reading in readings):
|
|
117
|
+
raise ValueError(f"{host}: {name} changes band count within the file")
|
|
118
|
+
matrix = np.stack(readings)
|
|
119
|
+
bands = [column[np.isfinite(column)] for column in matrix.T]
|
|
120
|
+
if not any(band.size for band in bands):
|
|
121
|
+
continue
|
|
122
|
+
levels = []
|
|
123
|
+
for band in bands:
|
|
124
|
+
if band.size:
|
|
125
|
+
peak = band.max()
|
|
126
|
+
levels.append(peak + 10 * np.log10(np.mean(10 ** ((band - peak) / 10))))
|
|
127
|
+
else:
|
|
128
|
+
levels.append(np.nan)
|
|
129
|
+
figure, axis = plt.subplots(figsize=(12, 5), layout="constrained")
|
|
130
|
+
positions = np.arange(1, band_count + 1)
|
|
131
|
+
axis.bar(positions, levels, width=0.9)
|
|
132
|
+
axis.set_xticks(positions)
|
|
133
|
+
axis.set(title=f"{host} - {name} (energy average of {len(readings)} readings)", xlabel="Frequency band number", ylabel="Level [dB SPL]")
|
|
134
|
+
axis.grid(axis="y", alpha=0.3)
|
|
135
|
+
axis.tick_params(axis="x", labelsize=8)
|
|
136
|
+
figures.append(figure)
|
|
137
|
+
if not figures:
|
|
138
|
+
raise ValueError("No numeric measurement data found in the file")
|
|
139
|
+
return figures
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main():
|
|
143
|
+
parser = argparse.ArgumentParser(description="Plot broadband levels as lines and CPB bands as energy-averaged bars")
|
|
144
|
+
parser.add_argument("path", type=Path, help="Saved JSON or CSV measurement file")
|
|
145
|
+
parser.add_argument("--output-dir", type=Path, help="Save PNG plots instead of opening windows")
|
|
146
|
+
arguments = parser.parse_args()
|
|
147
|
+
try:
|
|
148
|
+
figures = plot_records(load_records(arguments.path))
|
|
149
|
+
except (OSError, ValueError) as error:
|
|
150
|
+
parser.error(str(error))
|
|
151
|
+
if arguments.output_dir:
|
|
152
|
+
arguments.output_dir.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
for index, figure in enumerate(figures, start=1):
|
|
154
|
+
output = arguments.output_dir / f"{arguments.path.stem}_plot_{index}.png"
|
|
155
|
+
figure.savefig(output, dpi=150)
|
|
156
|
+
print(output)
|
|
157
|
+
plt.close(figure)
|
|
158
|
+
else:
|
|
159
|
+
plt.show()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main()
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""Optional DataHandler for WebXiStreamHandler. By default WebXiStreamHandler
|
|
2
|
-
does nothing with decoded values - call streamer.setDataHandler(...) with an
|
|
3
|
-
instance of one of these (or your own subclass) before startStream() to
|
|
4
|
-
receive/print/save them."""
|
|
5
|
-
|
|
6
|
-
import csv
|
|
7
|
-
import json
|
|
8
|
-
import pickle
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DataHandler:
|
|
13
|
-
"""Override handle() to decide what happens with decoded stream values.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def handle(self, **data):
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class _BufferedFileDataHandler(DataHandler):
|
|
22
|
-
"""Base class for handlers that buffer records in memory and write them
|
|
23
|
-
all to disk in close(). Call streamer.setDataHandler(...) before startStream(),
|
|
24
|
-
then handler.close() (or MultiDataHandler.close()) when you stop the stream."""
|
|
25
|
-
|
|
26
|
-
_extension = None # set by subclasses, e.g. "json", "csv", "pkl"
|
|
27
|
-
|
|
28
|
-
def __init__(self, path):
|
|
29
|
-
path = Path(path)
|
|
30
|
-
if self._extension and path.suffix.lower() != f".{self._extension}":
|
|
31
|
-
path = path.with_name(path.name + f".{self._extension}")
|
|
32
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
-
self._path = path
|
|
34
|
-
self._records = []
|
|
35
|
-
|
|
36
|
-
def handle(self, **data):
|
|
37
|
-
if "values" in data:
|
|
38
|
-
data["values"] = [float(v) for v in data["values"]]
|
|
39
|
-
elif "samples" in data:
|
|
40
|
-
samples = data.pop("samples")
|
|
41
|
-
data.update(count=len(samples), min=float(min(samples)), max=float(max(samples)))
|
|
42
|
-
self._records.append(data)
|
|
43
|
-
|
|
44
|
-
def close(self):
|
|
45
|
-
raise NotImplementedError
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class JsonDataHandler(_BufferedFileDataHandler):
|
|
49
|
-
"""Writes all buffered records to a single JSON file on close()."""
|
|
50
|
-
_extension = "json"
|
|
51
|
-
|
|
52
|
-
def close(self):
|
|
53
|
-
with open(self._path, "w") as f:
|
|
54
|
-
json.dump(self._records, f, indent=2, default=str)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class CsvDataHandler(_BufferedFileDataHandler):
|
|
58
|
-
"""Writes all buffered records to a CSV file on close(). Columns vary per
|
|
59
|
-
row since different modes carry different fields."""
|
|
60
|
-
_extension = "csv"
|
|
61
|
-
|
|
62
|
-
def close(self):
|
|
63
|
-
fieldnames = list(dict.fromkeys(key for record in self._records for key in record))
|
|
64
|
-
with open(self._path, "w", newline="") as f:
|
|
65
|
-
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
66
|
-
writer.writeheader()
|
|
67
|
-
writer.writerows(self._records)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class PickleDataHandler(_BufferedFileDataHandler):
|
|
71
|
-
"""Writes all buffered records (as Python objects) to a pickle file on close()."""
|
|
72
|
-
_extension = "pkl"
|
|
73
|
-
|
|
74
|
-
def close(self):
|
|
75
|
-
with open(self._path, "wb") as f:
|
|
76
|
-
pickle.dump(self._records, f)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class MultiDataHandler(DataHandler):
|
|
82
|
-
"""Fans out to several handlers at once (e.g. print AND save AND plot)."""
|
|
83
|
-
|
|
84
|
-
def __init__(self, *handlers):
|
|
85
|
-
self._handlers = handlers
|
|
86
|
-
|
|
87
|
-
def handle(self, **data):
|
|
88
|
-
for h in self._handlers:
|
|
89
|
-
h.handle(**data)
|
|
90
|
-
|
|
91
|
-
def close(self):
|
|
92
|
-
for h in self._handlers:
|
|
93
|
-
if hasattr(h, "close"):
|
|
94
|
-
h.close()
|
|
File without changes
|
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/enums/sequence_id_enum.py
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/FigureHandler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/https_requests.py
RENAMED
|
File without changes
|
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/sequence_handler.py
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/stream_handler.py
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/websocket_handler.py
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/helpers/webxi_root.pem
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/multi_device_flac.py
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/multi_instrument_example.py
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/screen_record_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi-header.ksy
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi-stream.ksy
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi_header.py
RENAMED
|
File without changes
|
{slm_open_api_package-0.2.10 → slm_open_api_package-0.2.11}/src/slm_api/webxi/webxi_stream.py
RENAMED
|
File without changes
|