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.
- slm_api/CPB_example.py +74 -0
- slm_api/__init__.py +5 -0
- slm_api/enums/fast_log_intervals_enum.py +16 -0
- slm_api/enums/sequence_id_enum.py +352 -0
- slm_api/example_multi.py +78 -0
- slm_api/fast_log_example.py +172 -0
- slm_api/flac_example.py +164 -0
- slm_api/helpers/FigureHandler.py +65 -0
- slm_api/helpers/Leq.py +87 -0
- slm_api/helpers/__init__.py +5 -0
- slm_api/helpers/buffer.py +27 -0
- slm_api/helpers/data_handler.py +94 -0
- slm_api/helpers/fft.py +38 -0
- slm_api/helpers/flac_stream_2_samples.py +64 -0
- slm_api/helpers/https_requests.py +79 -0
- slm_api/helpers/measurment_handler.py +20 -0
- slm_api/helpers/sequence_handler.py +43 -0
- slm_api/helpers/stream_handler.py +105 -0
- slm_api/helpers/stream_handlers.py +434 -0
- slm_api/helpers/tmpFileStateMachine.py +93 -0
- slm_api/helpers/websocket_handler.py +12 -0
- slm_api/helpers/webxi_helper_functions.py +79 -0
- slm_api/helpers/webxi_root.pem +24 -0
- slm_api/mp3_example.py +165 -0
- slm_api/multi_device_flac.py +105 -0
- slm_api/multi_instrument_example.py +74 -0
- slm_api/screen_record_example.py +64 -0
- slm_api/webxi/README.md +25 -0
- slm_api/webxi/__init__.py +5 -0
- slm_api/webxi/webxi-header.ksy +32 -0
- slm_api/webxi/webxi-stream.ksy +342 -0
- slm_api/webxi/webxi_header.py +42 -0
- slm_api/webxi/webxi_stream.py +450 -0
- slm_open_api_package-0.2.10.dist-info/METADATA +206 -0
- slm_open_api_package-0.2.10.dist-info/RECORD +37 -0
- slm_open_api_package-0.2.10.dist-info/WHEEL +4 -0
- 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
|
+
|
slm_api/webxi/README.md
ADDED
|
@@ -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,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
|