slm_open_api_package 0.2.10__tar.gz → 0.2.12__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.12}/PKG-INFO +17 -4
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/README.md +14 -3
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/pyproject.toml +3 -1
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/pyproject.toml.orig +3 -1
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/CPB_example.py +10 -5
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/example_multi.py +6 -2
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/fast_log_example.py +19 -4
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/flac_example.py +4 -1
- slm_open_api_package-0.2.12/src/slm_api/helpers/data_handler.py +251 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/stream_handlers.py +80 -52
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/tmpFileStateMachine.py +36 -10
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/webxi_helper_functions.py +2 -0
- slm_open_api_package-0.2.12/src/slm_api/leq.py +73 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/mp3_example.py +5 -2
- slm_open_api_package-0.2.12/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.12}/src/slm_api/__init__.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/enums/fast_log_intervals_enum.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/enums/sequence_id_enum.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/FigureHandler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/Leq.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/__init__.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/buffer.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/fft.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/flac_stream_2_samples.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/https_requests.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/measurment_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/sequence_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/stream_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/websocket_handler.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/helpers/webxi_root.pem +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/multi_device_flac.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/multi_instrument_example.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/screen_record_example.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/webxi/README.md +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/webxi/__init__.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/webxi/webxi-header.ksy +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/webxi/webxi-stream.ksy +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/src/slm_api/webxi/webxi_header.py +0 -0
- {slm_open_api_package-0.2.10 → slm_open_api_package-0.2.12}/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.12
|
|
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.12"
|
|
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.12"
|
|
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()
|