slm_open_api_package 0.2.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. slm_api/CPB_example.py +74 -0
  2. slm_api/__init__.py +5 -0
  3. slm_api/enums/fast_log_intervals_enum.py +16 -0
  4. slm_api/enums/sequence_id_enum.py +352 -0
  5. slm_api/example_multi.py +78 -0
  6. slm_api/fast_log_example.py +172 -0
  7. slm_api/flac_example.py +164 -0
  8. slm_api/helpers/FigureHandler.py +65 -0
  9. slm_api/helpers/Leq.py +87 -0
  10. slm_api/helpers/__init__.py +5 -0
  11. slm_api/helpers/buffer.py +27 -0
  12. slm_api/helpers/data_handler.py +94 -0
  13. slm_api/helpers/fft.py +38 -0
  14. slm_api/helpers/flac_stream_2_samples.py +64 -0
  15. slm_api/helpers/https_requests.py +79 -0
  16. slm_api/helpers/measurment_handler.py +20 -0
  17. slm_api/helpers/sequence_handler.py +43 -0
  18. slm_api/helpers/stream_handler.py +105 -0
  19. slm_api/helpers/stream_handlers.py +434 -0
  20. slm_api/helpers/tmpFileStateMachine.py +93 -0
  21. slm_api/helpers/websocket_handler.py +12 -0
  22. slm_api/helpers/webxi_helper_functions.py +79 -0
  23. slm_api/helpers/webxi_root.pem +24 -0
  24. slm_api/mp3_example.py +165 -0
  25. slm_api/multi_device_flac.py +105 -0
  26. slm_api/multi_instrument_example.py +74 -0
  27. slm_api/screen_record_example.py +64 -0
  28. slm_api/webxi/README.md +25 -0
  29. slm_api/webxi/__init__.py +5 -0
  30. slm_api/webxi/webxi-header.ksy +32 -0
  31. slm_api/webxi/webxi-stream.ksy +342 -0
  32. slm_api/webxi/webxi_header.py +42 -0
  33. slm_api/webxi/webxi_stream.py +450 -0
  34. slm_open_api_package-0.2.10.dist-info/METADATA +206 -0
  35. slm_open_api_package-0.2.10.dist-info/RECORD +37 -0
  36. slm_open_api_package-0.2.10.dist-info/WHEEL +4 -0
  37. slm_open_api_package-0.2.10.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,12 @@
1
+ import websockets
2
+ import asyncio
3
+ from . import https_requests
4
+
5
+ async def next_async_websocket(uri, packetHandler):
6
+ ssl_context = https_requests.build_ssl_context() if uri.startswith("wss://") else None
7
+ async with websockets.connect(uri, ping_interval=None, ssl=ssl_context) as websocket:
8
+ async for message in websocket:
9
+ try:
10
+ packetHandler(message)
11
+ except Exception as exc:
12
+ print(f"Error handling message from {uri}: {exc!r}")
@@ -0,0 +1,79 @@
1
+ import requests
2
+ from pathlib import Path
3
+ from slm_api.helpers.https_requests import get, put
4
+
5
+ # Functions to turn on/off BB freq weights and sequences. These are used in the examples to make sure the wanted sequences are enabled on the SLM.
6
+ def turn_off_bb_freq_weight(host):
7
+
8
+ for endpoint in (
9
+ "/webxi/Applications/SLM/Setup/BBFreqWeightB",
10
+ "/webxi/Applications/SLM/Setup/BBFreqWeightZ",
11
+ "/webxi/Applications/SLM/Setup/BBFreqWeightA",
12
+ "/webxi/Applications/SLM/Setup/BBFreqWeightC",
13
+ ):
14
+ try:
15
+ put(host , endpoint, json=False, timeout=10)
16
+ except requests.exceptions.HTTPError as exc:
17
+ print(f"Skipping {endpoint}: {exc}")
18
+
19
+ def turn_on_bb_freq_weight(host, weights, fast=False):
20
+ weights = [weights] if isinstance(weights, str) else weights
21
+ if not fast:
22
+ for weight in weights:
23
+ put(host , f"/webxi/Applications/SLM/Setup/BBFreqWeight{weight}", json=True)
24
+ else:
25
+ for weight in weights:
26
+ put(host , f"/webxi/Applications/SLM/Setup/FastBBFreqWeight{weight}", json=True)
27
+
28
+ def turn_on_bb_leq(host, weights, fast=False):
29
+ weights = [weights] if isinstance(weights, str) else weights
30
+ if not fast:
31
+ for weight in weights:
32
+ put(host, f"/webxi/Applications/SLM/Setup/BB{weight}", json=True)
33
+ else:
34
+ for weight in weights:
35
+ put(host, f"/webxi/Applications/SLM/Setup/FastBB{weight}", json=True)
36
+
37
+
38
+ def turn_off_CPB_freq_weight(host):
39
+ put(host , "/webxi/Applications/SLM/Setup/CPBFreqWeightB", json=False)
40
+ put(host , "/webxi/Applications/SLM/Setup/CPBFreqWeightZ", json=False)
41
+ put(host , "/webxi/Applications/SLM/Setup/CPBFreqWeightA", json=False)
42
+ put(host , "/webxi/Applications/SLM/Setup/CPBFreqWeightC", json=False)
43
+
44
+ def turn_on_CPB_freq_weight(host, weights):
45
+ weights = [weights] if isinstance(weights, str) else weights
46
+ for weight in weights:
47
+ put(host , f"/webxi/Applications/SLM/Setup/CPBFreqWeight{weight}", json=True)
48
+
49
+ def turn_on_cpb_leq(host, weights):
50
+ weights = [weights] if isinstance(weights, str) else weights
51
+ for weight in weights:
52
+ put(host , f"/webxi/Applications/SLM/Setup/{weight}", json=True)
53
+
54
+
55
+ def _find_ip_file(caller_file=None):
56
+ """Walk up from caller_file (or cwd) to the nearest 'slm_ip' file, else the project root (pyproject.toml/.git)."""
57
+ start = Path(caller_file).resolve().parent if caller_file else Path.cwd().resolve()
58
+ for directory in (start, *start.parents):
59
+ if (directory / "slm_ip").is_file():
60
+ return directory / "slm_ip"
61
+ if (directory / "pyproject.toml").is_file() or (directory / ".git").exists() or (directory / "requirements.txt").is_file():
62
+ return directory / "slm_ip"
63
+ return start / "slm_ip"
64
+
65
+ def set_host(caller_file=None):
66
+ """Resolve the SLM host. Pass __file__ to anchor the 'slm_ip' file search to your script."""
67
+ ip_path = _find_ip_file(caller_file)
68
+ ip = ip_path.read_text(encoding="utf-8").strip() if ip_path.is_file() else None
69
+ if not ip:
70
+ # First run (or missing IP): ask once and persist it to the ip file for next time
71
+ ip = input("No IP found. Enter the IP address of your SLM: ").strip()
72
+ if not ip:
73
+ raise RuntimeError(f'Missing SLM IP. Create a "{ip_path.name}" file containing the IP address.')
74
+ try:
75
+ ip_path.write_text(ip, encoding="utf-8")
76
+ print(f"Saved IP to {ip_path}")
77
+ except OSError as exc:
78
+ print(f"Warning: could not save IP to {ip_path} ({exc}). Create it manually with the IP address to skip this prompt.")
79
+ return ip
@@ -0,0 +1,24 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIEBTCCAu2gAwIBAgIJAPJZCza59BhGMA0GCSqGSIb3DQEBCwUAMIGYMQswCQYD
3
+ VQQGEwJVSzEPMA0GA1UECAwGU3VycmV5MQ4wDAYDVQQHDAVFZ2hhbTEVMBMGA1UE
4
+ CgwMU3BlY3RyaXMgcGxjMRgwFgYDVQQLDA9JbnN0cnVtZW50YXRpb24xDzANBgNV
5
+ BAMMBndlYi14aTEmMCQGCSqGSIb3DQEJARYXQ2Fyc3Rlbi5IYW5zZW5AYmtzdi5j
6
+ b20wHhcNMTgxMjE5MTQ1MzM4WhcNNDgxMjExMTQ1MzM4WjCBmDELMAkGA1UEBhMC
7
+ VUsxDzANBgNVBAgMBlN1cnJleTEOMAwGA1UEBwwFRWdoYW0xFTATBgNVBAoMDFNw
8
+ ZWN0cmlzIHBsYzEYMBYGA1UECwwPSW5zdHJ1bWVudGF0aW9uMQ8wDQYDVQQDDAZ3
9
+ ZWIteGkxJjAkBgkqhkiG9w0BCQEWF0NhcnN0ZW4uSGFuc2VuQGJrc3YuY29tMIIB
10
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwBbiEL0V2VF7gVzRPb45pkJ/
11
+ Uip84zS6MWgyCUWUuqYLSlpoLkfbsVPK7JUPUfUlhw4KjYFd7M6jra98hkqPaap8
12
+ ybeCmu3o6m4zfP0cbQD3QCEjPDNVy8fLP08D1mF3C3ksoIhGgv/0OdIC5/mJ0CEB
13
+ RBnr4xk8Sy7wa4TEOkH1eRtOvYQyKDGEBWcuWJER5fKhBn5ZdKta1ts78ELMy/8i
14
+ MXXtKbPxR9ZMPuqQmeYkcT9CEDVMSYoCV4Yov7Edi6JZNp6U6hqYx8UxHtl7ljTt
15
+ aOnICI+ACcvYWKgsBZcdl4ZQ0VUTDa3FD2zUI63Sx5U2/th1ChZoRjApx0BwpwID
16
+ AQABo1AwTjAdBgNVHQ4EFgQUMpXkaVWWnTSjYLF8UHwCvWRxMyowHwYDVR0jBBgw
17
+ FoAUMpXkaVWWnTSjYLF8UHwCvWRxMyowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
18
+ AQsFAAOCAQEAKcbCeH+WzWiXo5xWemVz/YLq3X/QJKB4UQgbyuXWItOjMvXGP0gi
19
+ owEY8s4y/YPGhl0R3lHFz2yLHRX0Vc+LzAB/8ob0Q0nKNttqnE3ofgZhpzUcGxVI
20
+ Yb2CpvHHdU4TXDYdLT4pkCb6+8pqlnOyRBBlJNvO5gKMSSvxcStVLp+2CwPhtex3
21
+ u43FzdRvC3kp0MmW1PCZgm5LqVh1IEx2TBiy9/7W5IAoSIh0bE+fGWi7NfcgZsv1
22
+ 2ZmKe/6p2vbHbarBTiOgD3GISr47+yhjN8Qd4eYPRLswZDHwkEDTW+TUvCmgbAfa
23
+ 3WCwBk4+GVXcGL/yIEWaSAIZbgAq03JsYA==
24
+ -----END CERTIFICATE-----
slm_api/mp3_example.py ADDED
@@ -0,0 +1,165 @@
1
+ import asyncio
2
+
3
+ import miniaudio
4
+ import requests
5
+ import sys
6
+ import numpy as np
7
+ import pyqtgraph as pg
8
+
9
+ # Modules to convert webxi data
10
+ from slm_api.webxi import webxi_stream as webxiStream
11
+ from slm_api.helpers import stream_handler as stream # SLM stream functions
12
+ # Start/pause/Stop measurments functions
13
+ from slm_api.helpers import measurment_handler as meas
14
+ # Get sequences,
15
+ from slm_api.helpers import sequence_handler as seq
16
+ # Stream handler
17
+ from slm_api.helpers import stream_handlers as sh
18
+
19
+ # Async functions to control communication
20
+ from slm_api.helpers import websocket_handler as webSocket
21
+ from timeit import default_timer as timer
22
+
23
+ # Buffer and decoder for the mp3 stream
24
+ from slm_api.helpers.buffer import DataBuffer
25
+ from slm_api.helpers.fft import dBfft
26
+ import threading
27
+
28
+ from slm_api.helpers.FigureHandler import FigureHandler
29
+ from slm_api.helpers import TmpfileStateMachine
30
+ from slm_api.helpers import webxi_helper_functions as webxi_helper
31
+ from slm_api.helpers.data_handler import CsvDataHandler, MultiDataHandler, DataHandler
32
+
33
+ host = webxi_helper.set_host(__file__)
34
+ sequenceID = 156
35
+
36
+
37
+
38
+ # class streamHandler:
39
+ # def __init__(self, startStream=False):
40
+ # self.i = 0
41
+ # self.tmpFileSM = TmpfileStateMachine()
42
+ # self.max_input = 15.6263 / np.sqrt(2)
43
+ # self.streamInit()
44
+ # if startStream:
45
+ # self.startStream()
46
+
47
+ # def decode_mp3_stream(self, message):
48
+ # start = timer()
49
+ # package = webxiStream.WebxiStream.from_bytes(message)
50
+ # if package.header.message_type == webxiStream.WebxiStream.Header.EMessageType.e_sequence_data:
51
+ # # Get the encoded mp3 block
52
+ # mp3 = package.content.sequence_blocks[0]
53
+ # data = self.tmpFileSM.runStateMachine(mp3.frame)
54
+ # mp3DecodedData = miniaudio.mp3_read_s16(data)
55
+
56
+ # DataBuffer.append((np.asarray(mp3DecodedData.samples[-mp3DecodedData.num_frames:], dtype=np.int32) << 8) * self.calibrationFactor)
57
+
58
+ # end = timer()
59
+ # total = (end - start)
60
+ # if 0.0625 < total:
61
+ # print(f"TotalTime: {total}")
62
+ # if not self.StreamRun:
63
+ # self._resolve()
64
+
65
+ # def get_calibration_factor(self):
66
+ # # Calculate calibration factor from the microphone sensitivity
67
+ # response = requests.get(f"{host}/WebXi/Applications/SLM/Outputs/Sensitivity")
68
+ # assert (response.status_code == 200)
69
+ # mic_sens = float(response.text) # V/Pa
70
+ # max_lvl = 20 * np.log10((self.max_input / mic_sens) / 20e-6) # dB SPL re 20 uPa
71
+ # self.calibrationFactor = (20e-6 * 10 ** (max_lvl / 20)) / (2 ** 23 - 1) * np.sqrt(2)
72
+
73
+ # def streamInit(self):
74
+ # response = requests.put(f"{host}/WebXi/Applications/SLM/setup/AudioRecordingListenQuality", json=1)
75
+ # assert (response.status_code == 200)
76
+ # self.get_calibration_factor()
77
+ # self.ID, self.sequence = seq.get_sequence(host, sequenceID)
78
+ # # Get URI for stream
79
+ # self.streamName = "Mp3 stream"
80
+ # self.uri = stream.setup_stream(host, self.ID, self.streamName)
81
+
82
+ # # Start a measurement. This is needed to obtain data from the device
83
+ # meas.start_pause_measurement(host, True)
84
+
85
+ # def startStream(self):
86
+ # self.StreamRun = True
87
+ # asyncio.run(self.runStream())
88
+
89
+ # async def runStream(self):
90
+ # self.loop = asyncio.get_running_loop()
91
+ # self.fut = self.loop.create_future()
92
+ # # Create lambda function to use for the stream message. In this example is a function
93
+ # # call used
94
+ # self.msg_func = lambda msg: self.decode_mp3_stream(msg)
95
+ # # Initialize and run the websocket to retrieve data
96
+
97
+ # task = self.loop.create_task(webSocket.next_async_websocket(self.uri, self.msg_func))
98
+ # await self.fut
99
+ # task.cancel()
100
+ # meas.stop_measurement(host)
101
+
102
+ # def stopStream(self):
103
+ # self.StreamRun = False
104
+ # # Resolve the future directly; waiting for the next message may never happen
105
+ # if hasattr(self, "loop"):
106
+ # self.loop.call_soon_threadsafe(self._resolve)
107
+ # stream.delete_stream(host, self.streamName) # Cleaning up and deleting the stream used
108
+
109
+ # def _resolve(self):
110
+ # if not self.fut.done():
111
+ # self.fut.set_result(True)
112
+
113
+
114
+ class figureHandler(FigureHandler):
115
+
116
+ def axisConfig(self):
117
+ self.plotTime.getAxis('left').setStyle(tickFont=pg.QtGui.QFont('Arial', 11))
118
+ self.plotTime.getAxis('bottom').setStyle(tickFont=pg.QtGui.QFont('Arial', 11))
119
+ self.plotTime.getAxis('left').setLabel('Approximately pressure', units='Pa', **self.labelStyle)
120
+ self.plotTime.getAxis('bottom').setLabel('Time', units='s', **self.labelStyle)
121
+ self.plotFreq.getAxis('left').setStyle(tickFont=pg.QtGui.QFont('Arial', 14))
122
+ self.plotFreq.getAxis('bottom').setStyle(tickFont=pg.QtGui.QFont('Arial', 14))
123
+ self.plotFreq.getAxis('left').setLabel('Approximately dB SPL re 20 µPa', **self.labelStyle)
124
+ self.plotFreq.getAxis('bottom').setLabel('Frequency', units='Hz', **self.labelStyle)
125
+
126
+ def update(self):
127
+ signal = DataBuffer.getPart(self.chunkToShow)
128
+ x = np.linspace(np.min(self.axis), np.max(self.axis), len(signal))
129
+ freq, s_dbfs = dBfft(signal, 32e3, self.fftHamming, ref=20e-6) #Reference = 20µPa
130
+ # Average the fft for a smoother plot
131
+ avg = s_dbfs / 3 + self.old / 3 + self.oldold / 3
132
+ self.curveTime.setData(x, signal)
133
+ self.curveFreq.setData(freq, avg)
134
+ self.oldold = self.old
135
+ self.old = s_dbfs
136
+ if (self.i % 5 == 0):
137
+ # Autoscale and print min/max values every 0.5 seconds
138
+ min_Pa = np.round(min(signal), 2)
139
+ max_Pa = np.round(max(signal), 2)
140
+ fft_peak = np.round(max(avg), 2)
141
+ fft_min = np.round(min(avg), 2)
142
+ peak_freq = (freq[np.argmax(avg)])
143
+ if (min_Pa != max_Pa):
144
+ self.plotTime.setYRange(min_Pa * 1.2, max_Pa * 1.2)
145
+ if (np.isinf(fft_peak) == False):
146
+ self.plotFreq.setYRange(fft_min, fft_peak * 1.2)
147
+ print(f"Min: {min_Pa} Pa, Max: {max_Pa} Pa, Peak: {fft_peak} dB SPL, Peak freq: {peak_freq} Hz")
148
+ self.i += 1
149
+
150
+
151
+ def on_close():
152
+ # streamer.data_handler.close()
153
+ streamer.stopStream()
154
+
155
+
156
+ if __name__ == "__main__":
157
+ # streamer = streamHandler()
158
+ streamer = sh.WebXiStreamHandler(host, sequenceID, mode="mp3")
159
+ # streamer.setDataHandler(CsvDataHandler(r"C:\Users\SKREBSVINGAA\csv_test\csv_test.txt"))
160
+ # streamer.setDataHandler(PrintDataHandler())
161
+
162
+ fig = figureHandler()
163
+ fig.app.aboutToQuit.connect(on_close)
164
+ threading.Thread(target=streamer.startStream, daemon=True).start()
165
+ fig.run()
@@ -0,0 +1,105 @@
1
+ """
2
+ Multi-device FLAC/mp3 audio streaming
3
+ --------------------------------------
4
+ Streams FLAC- (or mp3-)encoded audio from several devices at once and plots
5
+ each device's waveform + spectrum, same as flac_example.py/mp3_example.py's
6
+ figureHandler, stacked in one window (one time/freq row pair per device).
7
+
8
+ Note: WebXiStreamHandler._msg_func_audio also appends decoded samples to the
9
+ module-level `DataBuffer` (helpers/buffer.py), which is a single shared buffer.
10
+ With multiple devices, samples from all of them interleave in that shared
11
+ buffer, so it isn't usable for per-device plotting here - each device instead
12
+ gets its own `buffer` instance (BufferDataHandler below).
13
+ """
14
+
15
+ import sys
16
+ import time
17
+
18
+ import numpy as np
19
+ import pyqtgraph as pg
20
+ from PyQt5 import QtWidgets
21
+
22
+ from slm_api.helpers.buffer import buffer as Buffer
23
+ from slm_api.helpers.fft import dBfft
24
+ from slm_api.helpers.stream_handlers import WebXiStreamHandler, start_all_synced, stop_all
25
+ from slm_api.helpers.data_handler import DataHandler
26
+ from slm_api.enums.sequence_id_enum import SequenceIdEnums
27
+
28
+ USE_MP3 = True # False = FLAC (SequenceIdEnums.FLACSignal), True = mp3 (SequenceIdEnums.MP3Signal)
29
+ SEQUENCE_ID = SequenceIdEnums.MP3Signal.value if USE_MP3 else SequenceIdEnums.FLACSignal.value
30
+ SAMPLE_RATE = 32e3 if USE_MP3 else 2 ** 16 # matches mp3_example.py / flac_example.py dBfft() calls
31
+
32
+ CHUNK_TO_SHOW = 2 ** 15
33
+
34
+
35
+ class BufferDataHandler(DataHandler):
36
+ """Per-device handler: appends decoded samples into its own buffer instead
37
+ of the shared DataBuffer singleton, so devices don't mix samples together."""
38
+
39
+ def __init__(self):
40
+ self.buffer = Buffer(2 ** 16)
41
+
42
+ def handle(self, samples, **data):
43
+ self.buffer.append(samples)
44
+
45
+
46
+ class PrintOnlyHandler:
47
+ """Periodically FFTs each device's buffer and prints Min/Max/Peak/Peak freq, no GUI."""
48
+
49
+ def __init__(self, labels_and_handlers):
50
+ self.labels_and_handlers = labels_and_handlers
51
+ self.fft_hamming = np.hamming(CHUNK_TO_SHOW)
52
+ self.old = [0] * len(labels_and_handlers)
53
+ self.oldold = [0] * len(labels_and_handlers)
54
+ self.i = 0
55
+
56
+ def update(self):
57
+ if self.i % 5 != 0:
58
+ self.i += 1
59
+ return
60
+ for idx, (label, handler) in enumerate(self.labels_and_handlers):
61
+ signal = handler.buffer.getPart(CHUNK_TO_SHOW)
62
+ freq, s_dbfs = dBfft(signal, SAMPLE_RATE, self.fft_hamming, ref=20e-6) # Reference = 20µPa
63
+ avg = s_dbfs / 3 + self.old[idx] / 3 + self.oldold[idx] / 3
64
+ self.oldold[idx] = self.old[idx]
65
+ self.old[idx] = s_dbfs
66
+ min_Pa = np.round(min(signal), 2)
67
+ max_Pa = np.round(max(signal), 2)
68
+ fft_peak = np.round(max(avg), 2)
69
+ peak_freq = freq[np.argmax(avg)]
70
+ print(f"{label} Min: {min_Pa} Pa, Max: {max_Pa} Pa, Peak: {fft_peak} dB SPL, Peak freq: {peak_freq} Hz")
71
+ self.i += 1
72
+
73
+
74
+ if __name__ == "__main__":
75
+ devices = [
76
+ "192.168.1.183",
77
+ # "192.168.1.216",
78
+ # "192.168.1.191",
79
+ ]
80
+
81
+ streams = [
82
+ WebXiStreamHandler(
83
+ host=host, sequenceID=SEQUENCE_ID,
84
+ streamName=f"Stream_{host}", multi_device=True,
85
+ mode="mp3" if USE_MP3 else "flac",
86
+ # flac=True
87
+ )
88
+ for host in devices
89
+ ]
90
+ handlers = [BufferDataHandler() for _ in streams]
91
+ for s, h in zip(streams, handlers):
92
+ s.setDataHandler(h)
93
+
94
+ printer = PrintOnlyHandler([(s.host, h) for s, h in zip(streams, handlers)])
95
+
96
+ time.sleep(1)
97
+ start_all_synced(streams)
98
+
99
+ try:
100
+ while True:
101
+ printer.update()
102
+ time.sleep(0.1)
103
+ except KeyboardInterrupt:
104
+ stop_all(streams)
105
+ print("\nStreams stopped by user.")
@@ -0,0 +1,74 @@
1
+ """
2
+ 18. Stream FastLAF (Sequence 115)
3
+ -----------------------------------
4
+ Streams the FastLAF (LAeq) broadband Leq value from sequence 115
5
+ and pretty-prints each received value to the terminal.
6
+
7
+ Sequence 115 properties:
8
+ Name: FastLAF
9
+ LocalName: LAeq
10
+ FunctionType: BroadbandLeq
11
+ DataType: Int16
12
+ Scale: 0.01
13
+ Weighting: A
14
+ AveragingMode: Linear
15
+ Unit: dB re 20uPa
16
+ """
17
+
18
+
19
+
20
+ import time
21
+ import datetime
22
+ import threading
23
+ from slm_api.helpers.stream_handlers import WebXiStreamHandler, start_all_synced, stop_all
24
+ from slm_api.helpers.data_handler import DataHandler
25
+ from slm_api.enums.sequence_id_enum import SequenceIdEnums
26
+ from slm_api.enums.fast_log_intervals_enum import FastLogInterval
27
+ from slm_api.helpers import webxi_helper_functions as webxi_helper
28
+
29
+
30
+ FAST_LOGGING_INTERVAL_MS = FastLogInterval.interval_1000ms.value
31
+ SEQUENCE_ID = SequenceIdEnums.LAeq.value
32
+ SEQUENCE_NAME = SequenceIdEnums.LAeq.name
33
+
34
+
35
+
36
+
37
+ class PrintDataHandler(DataHandler):
38
+ def __init__(self, label):
39
+ self.label = label
40
+
41
+ def handle(self, value, **data):
42
+ print(f"{self.label} {SEQUENCE_NAME}: {value}")
43
+
44
+
45
+ if __name__ == "__main__":
46
+ devices = [
47
+ "192.168.1.183",
48
+ "192.168.1.216",
49
+ "192.168.1.191",
50
+ ]
51
+
52
+ saving_format = "csv"
53
+ streams = []
54
+ for host in devices:
55
+ filename = f'{host}_{datetime.datetime.now().strftime("%H%M_%m%d%Y")}'
56
+ streams.append(
57
+ WebXiStreamHandler(
58
+ host=host, sequenceID=SEQUENCE_ID,
59
+ streamName=f"Stream_{host}", multi_device=True, mode="cpb",
60
+ saving=saving_format, saving_path=f"./saved_data/{filename}"
61
+ )
62
+ )
63
+ for s in streams:
64
+ s.setDataHandler(PrintDataHandler(s.host))
65
+
66
+ time.sleep(1)
67
+ start_all_synced(streams)
68
+
69
+ try:
70
+ while True:
71
+ time.sleep(1)
72
+ except KeyboardInterrupt:
73
+ stop_all(streams)
74
+ print("\nStreams stopped by user.")
@@ -0,0 +1,64 @@
1
+ """
2
+ Continuously streams the ScreenImage sequence (Name="ScreenImage", DataType="Byte",
3
+ DataFormat="PNG") from the device. Unlike most metadata, this isn't a static REST
4
+ resource - it's a streamed sequence, like the FLAC/mp3 audio sequences. Each received
5
+ block's raw bytes are already a complete PNG file. The plot is refreshed with each
6
+ new image received, like a (low frame rate) screen recording of the device.
7
+ """
8
+ import io
9
+ import threading
10
+ import numpy as np
11
+ import matplotlib.pyplot as plt
12
+ from matplotlib.animation import FuncAnimation
13
+ from PIL import Image
14
+
15
+ from slm_api.helpers import sequence_handler as seq
16
+ from slm_api.helpers import stream_handler as stream
17
+ from slm_api.helpers import measurment_handler as meas
18
+ from slm_api.helpers.webxi_helper_functions import set_host
19
+ from slm_api.webxi import webxi_stream
20
+ from slm_api.helpers.stream_handlers import WebXiStreamHandler
21
+
22
+
23
+ host = set_host(__file__)
24
+
25
+
26
+ class FigHandler:
27
+ def __init__(self, streamer):
28
+ self.streamer = streamer
29
+ self.fig, self.ax = plt.subplots()
30
+ self.ax.axis("off")
31
+ self.im = None
32
+ self.fig.canvas.mpl_connect("close_event", on_close)
33
+ self.fig.canvas.manager.set_window_title("SLM screen (live)")
34
+
35
+ def _update(self, i):
36
+ data = self.streamer.image_bytes
37
+ if data is None:
38
+ return
39
+ image = np.array(Image.open(io.BytesIO(data)))
40
+ if self.im is None or self.im.get_array().shape != image.shape:
41
+ self.ax.clear()
42
+ self.ax.axis("off")
43
+ self.im = self.ax.imshow(image)
44
+ else:
45
+ self.im.set_data(image)
46
+
47
+ def startAnimation(self):
48
+ # interval controls how often the plot polls for a new image, not how
49
+ # often the device actually sends one
50
+ self.ani = FuncAnimation(self.fig, self._update, interval=500)
51
+
52
+
53
+ def on_close(event):
54
+ streamer.stopStream()
55
+
56
+
57
+ if __name__ == "__main__":
58
+ streamer = WebXiStreamHandler(host, mode="screen_record")
59
+ fig = FigHandler(streamer)
60
+ fig.startAnimation()
61
+ threading.Thread(target=streamer.startStream, daemon=True).start()
62
+ plt.show()
63
+ stream.delete_stream(host, streamer.streamName)
64
+
@@ -0,0 +1,25 @@
1
+
2
+
3
+ # WebXi binary data streaming
4
+ #### *An introduction*
5
+
6
+ Aside from communicating with the SLM via HTTP/REST/JSON, the sound level meter (SLM) also offers WebXi data streams via WebSockets. A faster and more efficient way to stream continuous binary data with a minimized overhead.
7
+
8
+ WebXi streams are setup on the SLM using JSON and REST. Once the WebXi streams has been configured data is streamed to the client (pushed from the SLM to the client via the network).
9
+
10
+ Which data sources to stream from the SLM (sound data, status, events, etc.) are selected by the client during setup, using REST and JSON.
11
+
12
+ Data leaving the SLM via the WebXi streams are time stamped from the same clock source.
13
+
14
+ Binary WebXi data streams are documented as part of the '*WebXi protocol specification*', found in the [Documentation](../../Documentation) folder. Based on this documentation the WebXi streaming format has been described using **Kaitai**.
15
+
16
+ The Kaitai(.kty) files may be compiled into parsers to be used from within various programming languages (C#, Java, Python, …).
17
+
18
+ To generate parsers for Python use:
19
+ * ksc webxi-stream.ksy -t python
20
+ * ksc webxi-header.ksy -t python
21
+
22
+ This creates two python files, one for parsing the data header and one for parsing the rest of the binary stream.
23
+
24
+ Find more information about Kaitai here: https://kaitai.io
25
+
@@ -0,0 +1,5 @@
1
+ from .webxi_header import *
2
+ from .webxi_stream import *
3
+
4
+ #from .openapi_header import *
5
+ #from .openapi_stream import *
@@ -0,0 +1,32 @@
1
+ meta:
2
+ id: webxi_header
3
+ endian: le
4
+ seq:
5
+ - id: magic
6
+ contents: [0x42, 0x4b]
7
+ - id: header_length
8
+ type: u2
9
+ - id: message_type
10
+ type: u2
11
+ enum: e_message_type
12
+ - id: content_version
13
+ type: u2
14
+ - id: reserved2
15
+ type: u4
16
+ - id: time
17
+ type: u8
18
+ - id: content_length
19
+ type: u4
20
+
21
+ enums:
22
+ e_message_type:
23
+ 1: e_sequence_data
24
+ 2: e_data_quality
25
+ 3: state
26
+ 4: status
27
+ 5: trigger
28
+ 6: node
29
+ 7: sync
30
+ 9: debug
31
+ 10: package
32
+ 11: aux_sequence_data