slm-open-api-package 0.1.0__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.1.0/PKG-INFO +21 -0
- slm_open_api_package-0.1.0/README.md +1 -0
- slm_open_api_package-0.1.0/pyproject.toml +33 -0
- slm_open_api_package-0.1.0/pyproject.toml.orig +32 -0
- slm_open_api_package-0.1.0/src/open_api_package/__init__.py +5 -0
- slm_open_api_package-0.1.0/src/open_api_package/example_multi.py +145 -0
- slm_open_api_package-0.1.0/src/open_api_package/flac_example.py +160 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/FigureHandler.py +65 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/Leq.py +87 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/__init__.py +5 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/buffer.py +27 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/fft.py +38 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/flac_stream_2_samples.py +64 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/measurment_handler.py +20 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/sequence_handler.py +44 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/stream_handler.py +86 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/stream_handlers.py +152 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/tmpFileStateMachine.py +93 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/websocket_handler.py +8 -0
- slm_open_api_package-0.1.0/src/open_api_package/helpers/webxi_helper_functions.py +67 -0
- slm_open_api_package-0.1.0/src/open_api_package/mp3_example.py +161 -0
- slm_open_api_package-0.1.0/src/open_api_package/webxi/README.md +25 -0
- slm_open_api_package-0.1.0/src/open_api_package/webxi/__init__.py +5 -0
- slm_open_api_package-0.1.0/src/open_api_package/webxi/webxi-header.ksy +32 -0
- slm_open_api_package-0.1.0/src/open_api_package/webxi/webxi-stream.ksy +342 -0
- slm_open_api_package-0.1.0/src/open_api_package/webxi/webxi_header.py +42 -0
- slm_open_api_package-0.1.0/src/open_api_package/webxi/webxi_stream.py +450 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: slm-open-api-package
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Sofus Vingaard
|
|
6
|
+
Author-email: Sofus Vingaard <sofus.krebsvingaard@hbkworld.com>
|
|
7
|
+
Requires-Dist: requests
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: websockets
|
|
10
|
+
Requires-Dist: kaitaistruct
|
|
11
|
+
Requires-Dist: python-dotenv
|
|
12
|
+
Requires-Dist: packaging
|
|
13
|
+
Requires-Dist: pyqtgraph
|
|
14
|
+
Requires-Dist: pyqt5
|
|
15
|
+
Requires-Dist: pyqt5-qt5<5.15.11
|
|
16
|
+
Requires-Dist: miniaudio
|
|
17
|
+
Requires-Dist: matplotlib>=3.11.1
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# open-api-package
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# open-api-package
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "slm-open-api-package"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"requests",
|
|
9
|
+
"numpy",
|
|
10
|
+
"websockets",
|
|
11
|
+
"kaitaistruct",
|
|
12
|
+
"python-dotenv",
|
|
13
|
+
"packaging",
|
|
14
|
+
"pyqtgraph",
|
|
15
|
+
"PyQt5",
|
|
16
|
+
"PyQt5-Qt5<5.15.11",
|
|
17
|
+
"miniaudio",
|
|
18
|
+
"matplotlib>=3.11.1",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[project.authors]]
|
|
22
|
+
name = "Sofus Vingaard"
|
|
23
|
+
email = "sofus.krebsvingaard@hbkworld.com"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
open-api-package = "open_api_package:main"
|
|
27
|
+
|
|
28
|
+
[tool.uv.build-backend]
|
|
29
|
+
module-name = "open_api_package"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
33
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "slm-open-api-package"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Sofus Vingaard", email = "sofus.krebsvingaard@hbkworld.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"requests",
|
|
12
|
+
"numpy",
|
|
13
|
+
"websockets",
|
|
14
|
+
"kaitaistruct",
|
|
15
|
+
"python-dotenv",
|
|
16
|
+
"packaging",
|
|
17
|
+
"pyqtgraph",
|
|
18
|
+
"PyQt5",
|
|
19
|
+
"PyQt5-Qt5<5.15.11",
|
|
20
|
+
"miniaudio",
|
|
21
|
+
"matplotlib>=3.11.1",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
open-api-package = "open_api_package:main"
|
|
26
|
+
|
|
27
|
+
[tool.uv.build-backend]
|
|
28
|
+
module-name = "open_api_package"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# This example will show how to stream multiple sequences at the same time using the same stream
|
|
2
|
+
# For this example enable the wanted sequences on the device
|
|
3
|
+
import asyncio
|
|
4
|
+
import requests
|
|
5
|
+
import threading
|
|
6
|
+
import sys, traceback
|
|
7
|
+
import numpy as np
|
|
8
|
+
import time
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
from matplotlib.animation import FuncAnimation
|
|
11
|
+
|
|
12
|
+
# Modules to convert webxi data
|
|
13
|
+
from open_api_package.webxi import webxi_stream as webxiStream
|
|
14
|
+
|
|
15
|
+
from open_api_package.helpers import sequence_handler as seq # Get sequences, e.g. LAeq functions
|
|
16
|
+
from open_api_package.helpers import stream_handler as stream # SLM stream functions
|
|
17
|
+
from open_api_package.helpers import measurment_handler as meas # Start/pause/Stop measurments functions
|
|
18
|
+
from open_api_package.helpers.Leq import MovingLeq, SLM_Setup_LAeq # Class to hold moving Leq
|
|
19
|
+
from open_api_package.helpers import websocket_handler as webSocket # Async functions to control communication
|
|
20
|
+
from open_api_package.helpers import webxi_helper_functions as webxi_helper
|
|
21
|
+
from open_api_package.helpers import stream_handlers as sh
|
|
22
|
+
|
|
23
|
+
host, ip = webxi_helper.set_host_ip(__file__)
|
|
24
|
+
|
|
25
|
+
# This example will stream LAeq. Add more names to the list to plot several sequences at once.
|
|
26
|
+
# Incase of error make sure the sequences are enabled on the SLM.
|
|
27
|
+
sequenceNames = ["LAeq", "LCeq", "LAF"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# class streamHandler:
|
|
31
|
+
|
|
32
|
+
# def __init__(self, startStream = False):
|
|
33
|
+
# self.streamInit()
|
|
34
|
+
# if startStream:
|
|
35
|
+
# self.startStream()
|
|
36
|
+
|
|
37
|
+
# def streamInit(self):
|
|
38
|
+
# self.IDs = []
|
|
39
|
+
# self.sequences = []
|
|
40
|
+
# self.sequenceFuncs = []
|
|
41
|
+
|
|
42
|
+
# for x in sequenceNames:
|
|
43
|
+
# ID, sequence = seq.get_sequence(host, seq.getSequenceID(host, x))
|
|
44
|
+
# self.IDs.append(ID)
|
|
45
|
+
# self.sequences.append(sequence)
|
|
46
|
+
# self.sequenceFuncs.append(MovingLeq(10, storedata=True, windowSize=100))
|
|
47
|
+
|
|
48
|
+
# self.streamName = "MultipleSequences"
|
|
49
|
+
# self.uri = stream.setup_stream(host, ip, self.IDs, self.streamName)
|
|
50
|
+
# # Start a measurement. This is needed to obtain data from the device
|
|
51
|
+
# meas.start_pause_measurement(host,True)
|
|
52
|
+
|
|
53
|
+
# def startStream(self):
|
|
54
|
+
# self.StreamRun = True
|
|
55
|
+
# asyncio.run(self.runStream())
|
|
56
|
+
|
|
57
|
+
# async def runStream(self):
|
|
58
|
+
# self.loop = asyncio.get_running_loop()
|
|
59
|
+
# self.fut = self.loop.create_future()
|
|
60
|
+
# # Create lambda function to use for the stream message. In this example is a function
|
|
61
|
+
# # call used
|
|
62
|
+
# self.msg_func = lambda msg : self.print_data(msg, self.IDs, self.sequences, self.sequenceFuncs, self.fut)
|
|
63
|
+
# # Initilize and run the websocket to retrive data
|
|
64
|
+
# task = self.loop.create_task(webSocket.next_async_websocket(self.uri, self.msg_func))
|
|
65
|
+
# await self.fut
|
|
66
|
+
# task.cancel()
|
|
67
|
+
# meas.stop_measurement(host)
|
|
68
|
+
|
|
69
|
+
# def stopStream(self):
|
|
70
|
+
# self.StreamRun = False
|
|
71
|
+
# if hasattr(self, "loop"):
|
|
72
|
+
# self.loop.call_soon_threadsafe(self._resolve)
|
|
73
|
+
# stream.delete_stream(host, self.streamName) # Cleaning up and deleting the stream used
|
|
74
|
+
|
|
75
|
+
# def _resolve(self):
|
|
76
|
+
# if not self.fut.done():
|
|
77
|
+
# self.fut.set_result(True)
|
|
78
|
+
|
|
79
|
+
# def print_data(self, message, IDs, sequences, sequenceFuncs, fut):
|
|
80
|
+
# package = webxiStream.WebxiStream.from_bytes(message)
|
|
81
|
+
# if package.header.message_type == webxiStream.WebxiStream.Header.EMessageType.e_sequence_data:
|
|
82
|
+
# for ID, sequence, Func in zip(IDs, sequences, sequenceFuncs):
|
|
83
|
+
# for data in package.content.sequence_blocks:
|
|
84
|
+
# if data.sequence_id == ID:
|
|
85
|
+
# value = stream.data_type_conv(sequence["DataType"], data.values, None)
|
|
86
|
+
# value = (np.array(value) if isinstance(value, list) else value) / 100
|
|
87
|
+
# move = Func.move(value)
|
|
88
|
+
# seqName = sequence["Name"]
|
|
89
|
+
# print(f"{seqName}: {value} and 10s avg: {move:.2f}")
|
|
90
|
+
|
|
91
|
+
# if not self.StreamRun:
|
|
92
|
+
# self._resolve()
|
|
93
|
+
|
|
94
|
+
class FigHandler:
|
|
95
|
+
|
|
96
|
+
def __init__(self, dataHandler):
|
|
97
|
+
self.fig, self.ax = plt.subplots(2,1,sharex=True, sharey=True)
|
|
98
|
+
axis = np.arange(-(len(dataHandler[0].getPlotData(True)) - 1),1,1)
|
|
99
|
+
self.dataHandler = dataHandler if isinstance(dataHandler, list) else [dataHandler]
|
|
100
|
+
self.ln1 = []
|
|
101
|
+
self.ln2 = []
|
|
102
|
+
for x, ii in zip(self.dataHandler, sequenceNames):
|
|
103
|
+
self.ln1.append((self.ax[0].plot(axis,x.getPlotData(True), label=ii))[0])
|
|
104
|
+
self.ln2.append((self.ax[1].plot(axis,x.getPlotData(False)))[0])
|
|
105
|
+
self.ax[1].set_xlim(left=np.min(axis), right=np.max(axis))
|
|
106
|
+
self.ax[1].set_ylim(bottom=30, top=100)
|
|
107
|
+
self.ax[0].set_ylabel("dB [SPL]")
|
|
108
|
+
self.ax[1].set_xlabel("Time [s]")
|
|
109
|
+
self.ax[1].set_ylabel("dB [SPL]")
|
|
110
|
+
self.ax[0].set_title('Moving avaraged')
|
|
111
|
+
self.ax[1].set_title('Instantaneous')
|
|
112
|
+
leg = self.ax[0].legend(loc='upper left')
|
|
113
|
+
self.ax[0].grid()
|
|
114
|
+
self.ax[1].grid()
|
|
115
|
+
self.fig.autofmt_xdate()
|
|
116
|
+
self.fig.tight_layout()
|
|
117
|
+
self.fig.canvas.mpl_connect('close_event', on_close)
|
|
118
|
+
self.fig.canvas.setWindowTitle('LAeq example')
|
|
119
|
+
|
|
120
|
+
def _update(self, i):
|
|
121
|
+
for idx, x in enumerate(self.dataHandler):
|
|
122
|
+
self.ln1[idx].set_ydata(x.getPlotData(True))
|
|
123
|
+
self.ln2[idx].set_ydata(x.getPlotData(False))
|
|
124
|
+
|
|
125
|
+
def startAnimation(self):
|
|
126
|
+
self.ani = FuncAnimation(self.fig, self._update, interval=1000)
|
|
127
|
+
|
|
128
|
+
def on_close(event):
|
|
129
|
+
streamer.stopStream()
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
# turns off all BB freq weights to prevent interference
|
|
133
|
+
webxi_helper.turn_off_bb_freq_weight(host)
|
|
134
|
+
|
|
135
|
+
# turns on the wanted BB freq weights for this example
|
|
136
|
+
webxi_helper.turn_on_bb_freq_weight(host, ["A", "C"])
|
|
137
|
+
|
|
138
|
+
# sets the sequences to true. You can add or remove sequences at the top of the file.
|
|
139
|
+
webxi_helper.turn_on_bb_leq(host, sequenceNames)
|
|
140
|
+
|
|
141
|
+
streamer = sh.WebXiStreamHandler(host=host, ip=ip, sequenceNames=sequenceNames, multi=True)
|
|
142
|
+
fig = FigHandler(streamer.sequenceFuncs)
|
|
143
|
+
fig.startAnimation()
|
|
144
|
+
threading.Thread(target=streamer.startStream, daemon=True).start()
|
|
145
|
+
plt.show()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import requests
|
|
3
|
+
import sys
|
|
4
|
+
import pyqtgraph as pg
|
|
5
|
+
import numpy as np
|
|
6
|
+
from open_api_package.helpers import webxi_helper_functions as webxi_helper
|
|
7
|
+
|
|
8
|
+
# Modules to convert webxi data
|
|
9
|
+
from open_api_package.webxi import webxi_stream as webxiStream
|
|
10
|
+
# Help functions located in the helpers folder
|
|
11
|
+
# Read these files to get examples on how to communicate with the SLM
|
|
12
|
+
from open_api_package.helpers import stream_handler as stream # SLM stream functions
|
|
13
|
+
# Start/pause/Stop measurments functions
|
|
14
|
+
from open_api_package.helpers import measurment_handler as meas
|
|
15
|
+
# Get sequences,
|
|
16
|
+
from open_api_package.helpers import sequence_handler as seq
|
|
17
|
+
# Stream handler
|
|
18
|
+
from open_api_package.helpers import stream_handlers as sh
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Async functions to control communication
|
|
22
|
+
from open_api_package.helpers import websocket_handler as webSocket
|
|
23
|
+
from timeit import default_timer as timer
|
|
24
|
+
# Buffer and decoder for the flac stream
|
|
25
|
+
from open_api_package.helpers.buffer import DataBuffer
|
|
26
|
+
from open_api_package.helpers.fft import dBfft
|
|
27
|
+
from open_api_package.helpers import flac_stream_2_samples as flac2samples
|
|
28
|
+
import threading
|
|
29
|
+
|
|
30
|
+
from open_api_package.helpers.FigureHandler import FigureHandler
|
|
31
|
+
|
|
32
|
+
# FLAC streaming is only available on 2255
|
|
33
|
+
|
|
34
|
+
host, ip = webxi_helper.set_host_ip(__file__)
|
|
35
|
+
|
|
36
|
+
sequenceID = 157
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# class streamHandler:
|
|
40
|
+
# def __init__(self, startStream=False):
|
|
41
|
+
# self.i = 0
|
|
42
|
+
# self.max_input = 15.6263 / np.sqrt(2)
|
|
43
|
+
# self.streamInit()
|
|
44
|
+
# if startStream:
|
|
45
|
+
# self.startStream()
|
|
46
|
+
|
|
47
|
+
# def decode_flac_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 flac block
|
|
52
|
+
# flac = package.content.sequence_blocks[0]
|
|
53
|
+
# # Decode the compressed samples and add it to the data bufffer
|
|
54
|
+
# DataBuffer.append(flac2samples.decode(flac, self.calibrationFactor))
|
|
55
|
+
# end = timer()
|
|
56
|
+
# total = (end - start)
|
|
57
|
+
# if 0.0625 < total:
|
|
58
|
+
# print(f"TotalTime: {total}")
|
|
59
|
+
# if not self.StreamRun:
|
|
60
|
+
# self._resolve()
|
|
61
|
+
|
|
62
|
+
# def get_calibration_factor(self):
|
|
63
|
+
# # Calculate calibration factor from the microphone sensitivity
|
|
64
|
+
# response = requests.get(f"{host}/WebXi/Applications/SLM/Outputs/Sensitivity")
|
|
65
|
+
# assert (response.status_code == 200)
|
|
66
|
+
# mic_sens = float(response.text) # V/Pa
|
|
67
|
+
# max_lvl = 20 * np.log10((self.max_input / mic_sens) / 20e-6) # dB SPL re 20 uPa
|
|
68
|
+
# self.calibrationFactor = (20e-6 * 10 ** (max_lvl / 20)) / (2 ** 23 - 1) * np.sqrt(2)
|
|
69
|
+
|
|
70
|
+
# def streamInit(self):
|
|
71
|
+
# # Enable audio recording analysis quality
|
|
72
|
+
# response = requests.put(f"{host}/WebXi/Applications/SLM/setup/AudioRecordingAnalysisQuality", json = 1)
|
|
73
|
+
# assert(response.status_code == 200)
|
|
74
|
+
# self.get_calibration_factor()
|
|
75
|
+
# self.ID, self.sequence = seq.get_sequence(host, sequenceID)
|
|
76
|
+
# # Get URI for stream
|
|
77
|
+
# self.streamName = "Flac stream"
|
|
78
|
+
# self.uri = stream.setup_stream(host, ip, self.ID, self.streamName)
|
|
79
|
+
|
|
80
|
+
# # Start a measurement. This is needed to obtain data from the device
|
|
81
|
+
# meas.start_pause_measurement(host, True)
|
|
82
|
+
|
|
83
|
+
# def startStream(self):
|
|
84
|
+
# self.StreamRun = True
|
|
85
|
+
|
|
86
|
+
# asyncio.run(self.runStream())
|
|
87
|
+
|
|
88
|
+
# async def runStream(self):
|
|
89
|
+
# self.loop = asyncio.get_running_loop()
|
|
90
|
+
# self.fut = self.loop.create_future()
|
|
91
|
+
# # Create lambda function to use for the stream message. In this example is a function
|
|
92
|
+
# # call used
|
|
93
|
+
# self.msg_func = lambda msg : self.decode_flac_stream(msg)
|
|
94
|
+
# # Initilize and run the websocket to retrive data
|
|
95
|
+
|
|
96
|
+
# task = self.loop.create_task(webSocket.next_async_websocket(self.uri, self.msg_func))
|
|
97
|
+
# await self.fut
|
|
98
|
+
# task.cancel()
|
|
99
|
+
# meas.stop_measurement(host)
|
|
100
|
+
|
|
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('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('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, 2**16, 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
|
+
def on_close():
|
|
151
|
+
streamer.stopStream()
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
# streamer = streamHandler()
|
|
155
|
+
streamer = sh.WebXiStreamHandler(host, ip, sequenceID = sequenceID, flac=True)
|
|
156
|
+
fig = figureHandler()
|
|
157
|
+
fig.app.aboutToQuit.connect(on_close)
|
|
158
|
+
threading.Thread(target=streamer.startStream, daemon=True).start()
|
|
159
|
+
fig.run()
|
|
160
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import pyqtgraph as pg
|
|
2
|
+
import sys
|
|
3
|
+
import numpy as np
|
|
4
|
+
from PyQt5 import QtWidgets
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
class FigureHandler(ABC):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.i = 0
|
|
11
|
+
self.chunkToShow = 2 ** 15
|
|
12
|
+
self.fftSize = self.chunkToShow
|
|
13
|
+
self.fftHamming = np.hamming(self.fftSize)
|
|
14
|
+
# Used to store "old" spectrums for fft averaging
|
|
15
|
+
self.old = 0
|
|
16
|
+
self.oldold = 0
|
|
17
|
+
|
|
18
|
+
self.x = np.linspace(0.0, 10, self.chunkToShow)
|
|
19
|
+
|
|
20
|
+
## Figures
|
|
21
|
+
self.axis = np.arange(self.chunkToShow)
|
|
22
|
+
self.axis = np.flip(self.axis * -1 / self.chunkToShow)
|
|
23
|
+
self.x = np.zeros(len(self.axis))
|
|
24
|
+
self.app = QtWidgets.QApplication(sys.argv)
|
|
25
|
+
self.win = pg.GraphicsLayoutWidget(title="Streaming")
|
|
26
|
+
self.win.setBackground('w')
|
|
27
|
+
self.win.resize(1000, 600)
|
|
28
|
+
self.plotTime = self.win.addPlot()
|
|
29
|
+
self.curveTime = self.plotTime.plot(self.x, self.axis)
|
|
30
|
+
self.curveTime.setPen(color='b', width=2, autoDownsample=True, clipToView=True)
|
|
31
|
+
self.plotTime.showGrid(x=True, y=True)
|
|
32
|
+
self.plotTime.setXRange(np.min(self.axis), np.max(self.axis))
|
|
33
|
+
self.plotTime.setYRange(-2, 2)
|
|
34
|
+
self.labelStyle = {'color': '#000', 'font-size': '13pt'}
|
|
35
|
+
|
|
36
|
+
# Subplot 2 (FFT)
|
|
37
|
+
# Calculate the frequency vector
|
|
38
|
+
self.win.nextRow()
|
|
39
|
+
self.plotFreq = self.win.addPlot()
|
|
40
|
+
freq = np.arange(self.fftSize//2 + 1) / (float(self.fftSize) / 32e3)
|
|
41
|
+
self.curveFreq = self.plotFreq.plot(freq, np.arange(len(freq)))
|
|
42
|
+
self.curveFreq.setPen(color='b', width=2, autoDownsample=True, clipToView=True)
|
|
43
|
+
self.plotFreq.setXRange(0, np.max(freq))
|
|
44
|
+
self.plotFreq.setYRange(-20, 130)
|
|
45
|
+
self.plotFreq.getAxis('bottom').enableAutoSIPrefix(enable=False)
|
|
46
|
+
self.plotFreq.setLogMode(x=True, y=False)
|
|
47
|
+
self.plotFreq.showGrid(x=True, y=True)
|
|
48
|
+
self.axisConfig()
|
|
49
|
+
self.win.show()
|
|
50
|
+
|
|
51
|
+
self.timer = pg.QtCore.QTimer()
|
|
52
|
+
self.timer.timeout.connect(self.update)
|
|
53
|
+
self.timer.start(int(0.1 * 1000))
|
|
54
|
+
|
|
55
|
+
def run(self):
|
|
56
|
+
QtWidgets.QApplication.instance().exec_()
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def axisConfig(self):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def update(self):
|
|
64
|
+
pass
|
|
65
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def SLM_Setup_LAeq(host):
|
|
6
|
+
# Enable logging mode
|
|
7
|
+
requests.put(host + "/webxi/Applications/SLM/Setup/ControlLoggingMode", json=1)
|
|
8
|
+
# Set the device in free runnign mode
|
|
9
|
+
requests.put(host + "/webxi/Applications/SLM/Setup/ControlMeasurementTimeControl", json=0)
|
|
10
|
+
# Enable A weight frequency broad band weighting, but first clear the other once to be sure
|
|
11
|
+
requests.put(host + "/webxi/Applications/SLM/Setup/BBFreqWeightB", json=False)
|
|
12
|
+
requests.put(host + "/webxi/Applications/SLM/Setup/BBFreqWeightC", json=False)
|
|
13
|
+
requests.put(host + "/webxi/Applications/SLM/Setup/BBFreqWeightZ", json=False)
|
|
14
|
+
requests.put(host + "/webxi/Applications/SLM/Setup/BBFreqWeightA", json=True)
|
|
15
|
+
# Enable LAeq mode on the device
|
|
16
|
+
requests.put(host + "/webxi/applications/slm/setup/BBLAeq", json=True)
|
|
17
|
+
|
|
18
|
+
class MovingLeq:
|
|
19
|
+
"""Class to create a moving Leq object.\n
|
|
20
|
+
Stores a window of Leq logging intervals to be combined into a total Leq\n
|
|
21
|
+
Calling move(new_interval) on the object will overwrite the oldest value in the
|
|
22
|
+
window, recalculate the total Leq and return it."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, window_length_sec, storedata=False, windowSize=None):
|
|
25
|
+
self.window_length = window_length_sec
|
|
26
|
+
windowSize = self.window_length * 10 + 1 if windowSize is None else windowSize
|
|
27
|
+
|
|
28
|
+
# Intialize an empty array, index to track oldest value, and window status
|
|
29
|
+
self.leq_window = np.zeros(window_length_sec)
|
|
30
|
+
self.leq_window.fill(np.nan)
|
|
31
|
+
self.storedata = storedata
|
|
32
|
+
if self.storedata:
|
|
33
|
+
self.rawData = LeqData(windowSize) # Used to store the raw data for plotting
|
|
34
|
+
self.leqData = LeqData(windowSize) # Used to store the Leq data for plotting
|
|
35
|
+
self.oldest_value_index = 0
|
|
36
|
+
self.leq_total = 0.0
|
|
37
|
+
self.window_full = False
|
|
38
|
+
|
|
39
|
+
def total_leq(self,window):
|
|
40
|
+
""" Function to combine several Leq periods into a total Leq, as per:\n
|
|
41
|
+
10 * log10(10^(L1/10) + 10^(L2/10) + ... 10^(Ln/10) / n)"""
|
|
42
|
+
sound_Pa = np.sum(np.power(10,window/10)) / len(window)
|
|
43
|
+
return 10 * np.log10(sound_Pa)
|
|
44
|
+
|
|
45
|
+
def move(self, new_value):
|
|
46
|
+
"""Overwrite the oldest value in the array with the new logging interval
|
|
47
|
+
and update the oldest value index to point to the next oldest value"""
|
|
48
|
+
self.leq_window[self.oldest_value_index] = new_value
|
|
49
|
+
self.oldest_value_index += 1
|
|
50
|
+
|
|
51
|
+
# Calculate the total Leq for the moving window and return it.
|
|
52
|
+
if self.window_full:
|
|
53
|
+
self.leq_total = self.total_leq(self.leq_window)
|
|
54
|
+
self.oldest_value_index = self.oldest_value_index % self.window_length
|
|
55
|
+
if self.storedata:
|
|
56
|
+
self.leqData.move(self.leq_total)
|
|
57
|
+
self.rawData.move(new_value)
|
|
58
|
+
return self.leq_total
|
|
59
|
+
|
|
60
|
+
# If the moving window is not full of measured data, simply calculate total Leq
|
|
61
|
+
# for the data that is in the window (and ignore the empty part of the array)
|
|
62
|
+
else:
|
|
63
|
+
self.leq_total = self.total_leq(self.leq_window[:self.oldest_value_index])
|
|
64
|
+
self.oldest_value_index = self.oldest_value_index % self.window_length
|
|
65
|
+
if self.oldest_value_index == self.window_length - 1:
|
|
66
|
+
self.window_full = True
|
|
67
|
+
if self.storedata:
|
|
68
|
+
self.leqData.move(self.leq_total)
|
|
69
|
+
self.rawData.move(new_value)
|
|
70
|
+
return self.leq_total
|
|
71
|
+
|
|
72
|
+
def getPlotData(self,Leq):
|
|
73
|
+
if Leq:
|
|
74
|
+
return self.leqData.getData()
|
|
75
|
+
else:
|
|
76
|
+
return self.rawData.getData()
|
|
77
|
+
|
|
78
|
+
class LeqData:
|
|
79
|
+
def __init__(self,window_length_sec):
|
|
80
|
+
self.leq_window = np.zeros(window_length_sec)
|
|
81
|
+
self.leq_window[:] = np.nan
|
|
82
|
+
|
|
83
|
+
def move(self, new_value):
|
|
84
|
+
self.leq_window = np.insert(self.leq_window[1:], self.leq_window.size-1,new_value)
|
|
85
|
+
|
|
86
|
+
def getData(self):
|
|
87
|
+
return self.leq_window
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
class buffer:
|
|
4
|
+
def __init__(self, size):
|
|
5
|
+
self.size = size
|
|
6
|
+
self.data = np.zeros(self.size)
|
|
7
|
+
|
|
8
|
+
def append(self, x):
|
|
9
|
+
"""
|
|
10
|
+
Adds data in the front of the buffer. Discard the oldest data if buffer is full
|
|
11
|
+
"""
|
|
12
|
+
self.data = np.append(self.data[-(self.size - len(x)) :: ] , x)
|
|
13
|
+
|
|
14
|
+
def get(self):
|
|
15
|
+
"""
|
|
16
|
+
Returns the whole buffer
|
|
17
|
+
"""
|
|
18
|
+
return self.data
|
|
19
|
+
|
|
20
|
+
def getPart(self, start = 2**16):
|
|
21
|
+
"""
|
|
22
|
+
Returns X points of the newest data.
|
|
23
|
+
"""
|
|
24
|
+
return self.data[-start :: ]
|
|
25
|
+
|
|
26
|
+
# Create databuffer to store the converted package data
|
|
27
|
+
DataBuffer = buffer(2**16)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def dBfft(x, fs, win=None, ref=32768):
|
|
4
|
+
"""
|
|
5
|
+
Calculate spectrum in dB scale
|
|
6
|
+
Args:
|
|
7
|
+
x: input signal
|
|
8
|
+
fs: sampling frequency
|
|
9
|
+
win: vector containing window samples (same length as x).
|
|
10
|
+
If not provided, then rectangular window is used by default.
|
|
11
|
+
ref: reference value used for dBFS scale. 32768 for int16 and 1 for float
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
freq: frequency vector
|
|
15
|
+
s_db: spectrum in dB scale
|
|
16
|
+
"""
|
|
17
|
+
N = len(x) # Length of input sequence
|
|
18
|
+
|
|
19
|
+
if win is None:
|
|
20
|
+
win = np.ones(1, N)
|
|
21
|
+
if len(x) != len(win):
|
|
22
|
+
raise ValueError('Signal and window must be of the same length')
|
|
23
|
+
x = x * win
|
|
24
|
+
|
|
25
|
+
# Calculate real FFT and frequency vector
|
|
26
|
+
sp = np.fft.rfft(x)
|
|
27
|
+
freq = np.arange((N / 2) + 1) / (float(N) / fs)
|
|
28
|
+
|
|
29
|
+
# Scale the magnitude of FFT by window and factor of 2,
|
|
30
|
+
# because we are using half of FFT spectrum.
|
|
31
|
+
s_mag = (np.abs(sp) * np.sqrt(2)) / np.sum(win)
|
|
32
|
+
|
|
33
|
+
for i in range(len(s_mag)):
|
|
34
|
+
if s_mag[i] == 0:
|
|
35
|
+
s_mag[i] = 1e-10
|
|
36
|
+
# Convert to dBFS
|
|
37
|
+
s_dbfs = 20 * np.log10(s_mag/ref)
|
|
38
|
+
return freq, s_dbfs
|