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,164 @@
1
+ import asyncio
2
+ import requests
3
+ import sys
4
+ import pyqtgraph as pg
5
+ import numpy as np
6
+ from slm_api.helpers import webxi_helper_functions as webxi_helper
7
+
8
+ # Modules to convert webxi data
9
+ from slm_api.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 slm_api.helpers import stream_handler as stream # SLM stream functions
13
+ # Start/pause/Stop measurments functions
14
+ from slm_api.helpers import measurment_handler as meas
15
+ # Get sequences,
16
+ from slm_api.helpers import sequence_handler as seq
17
+ # Stream handler
18
+ from slm_api.helpers import stream_handlers as sh
19
+
20
+
21
+ # Async functions to control communication
22
+ from slm_api.helpers import websocket_handler as webSocket
23
+ from timeit import default_timer as timer
24
+ # Buffer and decoder for the flac stream
25
+ from slm_api.helpers.buffer import DataBuffer
26
+ from slm_api.helpers.fft import dBfft
27
+ from slm_api.helpers import flac_stream_2_samples as flac2samples
28
+ import threading
29
+
30
+ from slm_api.helpers.FigureHandler import FigureHandler
31
+ from slm_api.helpers.data_handler import MultiDataHandler, CsvDataHandler
32
+
33
+
34
+ # FLAC streaming is only available on 2255
35
+
36
+ host = webxi_helper.set_host(__file__)
37
+
38
+ sequenceID = 157
39
+
40
+
41
+ # class streamHandler:
42
+ # def __init__(self, startStream=False):
43
+ # self.i = 0
44
+ # self.max_input = 15.6263 / np.sqrt(2)
45
+ # self.streamInit()
46
+ # if startStream:
47
+ # self.startStream()
48
+
49
+ # def decode_flac_stream(self, message):
50
+ # start = timer()
51
+ # package = webxiStream.WebxiStream.from_bytes(message)
52
+ # if package.header.message_type == webxiStream.WebxiStream.Header.EMessageType.e_sequence_data:
53
+ # # Get the encoded flac block
54
+ # flac = package.content.sequence_blocks[0]
55
+ # # Decode the compressed samples and add it to the data bufffer
56
+ # DataBuffer.append(flac2samples.decode(flac, self.calibrationFactor))
57
+ # end = timer()
58
+ # total = (end - start)
59
+ # if 0.0625 < total:
60
+ # print(f"TotalTime: {total}")
61
+ # if not self.StreamRun:
62
+ # self._resolve()
63
+
64
+ # def get_calibration_factor(self):
65
+ # # Calculate calibration factor from the microphone sensitivity
66
+ # response = requests.get(f"{host}/WebXi/Applications/SLM/Outputs/Sensitivity")
67
+ # assert (response.status_code == 200)
68
+ # mic_sens = float(response.text) # V/Pa
69
+ # max_lvl = 20 * np.log10((self.max_input / mic_sens) / 20e-6) # dB SPL re 20 uPa
70
+ # self.calibrationFactor = (20e-6 * 10 ** (max_lvl / 20)) / (2 ** 23 - 1) * np.sqrt(2)
71
+
72
+ # def streamInit(self):
73
+ # # Enable audio recording analysis quality
74
+ # response = requests.put(f"{host}/WebXi/Applications/SLM/setup/AudioRecordingAnalysisQuality", 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 = "Flac 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
+
88
+ # asyncio.run(self.runStream())
89
+
90
+ # async def runStream(self):
91
+ # self.loop = asyncio.get_running_loop()
92
+ # self.fut = self.loop.create_future()
93
+ # # Create lambda function to use for the stream message. In this example is a function
94
+ # # call used
95
+ # self.msg_func = lambda msg : self.decode_flac_stream(msg)
96
+ # # Initilize and run the websocket to retrive data
97
+
98
+ # task = self.loop.create_task(webSocket.next_async_websocket(self.uri, self.msg_func))
99
+ # await self.fut
100
+ # task.cancel()
101
+ # meas.stop_measurement(host)
102
+
103
+
104
+ # def stopStream(self):
105
+ # self.StreamRun = False
106
+ # # Resolve the future directly; waiting for the next message may never happen
107
+ # if hasattr(self, "loop"):
108
+ # self.loop.call_soon_threadsafe(self._resolve)
109
+ # stream.delete_stream(host, self.streamName) # Cleaning up and deleting the stream used
110
+
111
+ # def _resolve(self):
112
+ # if not self.fut.done():
113
+ # self.fut.set_result(True)
114
+
115
+
116
+ class figureHandler(FigureHandler):
117
+
118
+ def axisConfig(self):
119
+ self.plotTime.getAxis('left').setStyle(tickFont=pg.QtGui.QFont('Arial', 11))
120
+ self.plotTime.getAxis('bottom').setStyle(tickFont=pg.QtGui.QFont('Arial', 11))
121
+ self.plotTime.getAxis('left').setLabel('Pressure', units='Pa', **self.labelStyle)
122
+ self.plotTime.getAxis('bottom').setLabel('Time', units='s', **self.labelStyle)
123
+ self.plotFreq.getAxis('left').setStyle(tickFont=pg.QtGui.QFont('Arial', 14))
124
+ self.plotFreq.getAxis('bottom').setStyle(tickFont=pg.QtGui.QFont('Arial', 14))
125
+ self.plotFreq.getAxis('left').setLabel('dB SPL re 20 µPa', **self.labelStyle)
126
+ self.plotFreq.getAxis('bottom').setLabel('Frequency', units='Hz', **self.labelStyle)
127
+
128
+ def update(self):
129
+ signal = DataBuffer.getPart(self.chunkToShow)
130
+ x = np.linspace(np.min(self.axis), np.max(self.axis), len(signal))
131
+ freq, s_dbfs = dBfft(signal, 2**16, self.fftHamming, ref=20e-6) #Reference = 20µPa
132
+ # Average the fft for a smoother plot
133
+ avg = s_dbfs / 3 + self.old / 3 + self.oldold / 3
134
+ self.curveTime.setData(x, signal)
135
+ self.curveFreq.setData(freq, avg)
136
+ self.oldold = self.old
137
+ self.old = s_dbfs
138
+ if (self.i % 5 == 0):
139
+ # Autoscale and print min/max values every 0.5 seconds
140
+ min_Pa = np.round(min(signal), 2)
141
+ max_Pa = np.round(max(signal), 2)
142
+ fft_peak = np.round(max(avg), 2)
143
+ fft_min = np.round(min(avg), 2)
144
+ peak_freq = (freq[np.argmax(avg)])
145
+ if (min_Pa != max_Pa):
146
+ self.plotTime.setYRange(min_Pa * 1.2, max_Pa * 1.2)
147
+ if (np.isinf(fft_peak) == False):
148
+ self.plotFreq.setYRange(fft_min, fft_peak * 1.2)
149
+ print(f"Min: {min_Pa} Pa, Max: {max_Pa} Pa, Peak: {fft_peak} dB SPL, Peak freq: {peak_freq} Hz")
150
+ self.i += 1
151
+
152
+ def on_close():
153
+ streamer.stopStream()
154
+
155
+ if __name__ == "__main__":
156
+ # streamer = streamHandler()
157
+ streamer = sh.WebXiStreamHandler(host, sequenceID=sequenceID, mode="flac", saving="csv", saving_path=r"C:\Users\SKREBSVINGAA\csv_test\csv_test.txt")
158
+ # streamer.setDataHandler(MultiDataHandler(PrintDataHandler(), CsvDataHandler(r"C:\Users\SKREBSVINGAA\csv_test\csv_test.txt")))
159
+ fig = figureHandler()
160
+ fig.app.aboutToQuit.connect(on_close)
161
+ threading.Thread(target=streamer.startStream, daemon=True).start()
162
+ fig.run()
163
+
164
+
@@ -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
+
slm_api/helpers/Leq.py ADDED
@@ -0,0 +1,87 @@
1
+ import numpy as np
2
+ from . import https_requests
3
+
4
+
5
+ def SLM_Setup_LAeq(host):
6
+ # Enable logging mode
7
+ https_requests.put(host, "/webxi/Applications/SLM/Setup/ControlLoggingMode", json=1)
8
+ # Set the device in free runnign mode
9
+ https_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
+ https_requests.put(host, "/webxi/Applications/SLM/Setup/BBFreqWeightB", json=False)
12
+ https_requests.put(host, "/webxi/Applications/SLM/Setup/BBFreqWeightC", json=False)
13
+ https_requests.put(host, "/webxi/Applications/SLM/Setup/BBFreqWeightZ", json=False)
14
+ https_requests.put(host, "/webxi/Applications/SLM/Setup/BBFreqWeightA", json=True)
15
+ # Enable LAeq mode on the device
16
+ https_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,5 @@
1
+ from .stream_handler import *
2
+ from .measurment_handler import *
3
+ from .sequence_handler import *
4
+ from .Leq import *
5
+ from .stream_handlers import *
@@ -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,94 @@
1
+ """Optional DataHandler for WebXiStreamHandler. By default WebXiStreamHandler
2
+ does nothing with decoded values - call streamer.setDataHandler(...) with an
3
+ instance of one of these (or your own subclass) before startStream() to
4
+ receive/print/save them."""
5
+
6
+ import csv
7
+ import json
8
+ import pickle
9
+ from pathlib import Path
10
+
11
+
12
+ class DataHandler:
13
+ """Override handle() to decide what happens with decoded stream values.
14
+ """
15
+
16
+
17
+ def handle(self, **data):
18
+ pass
19
+
20
+
21
+ class _BufferedFileDataHandler(DataHandler):
22
+ """Base class for handlers that buffer records in memory and write them
23
+ all to disk in close(). Call streamer.setDataHandler(...) before startStream(),
24
+ then handler.close() (or MultiDataHandler.close()) when you stop the stream."""
25
+
26
+ _extension = None # set by subclasses, e.g. "json", "csv", "pkl"
27
+
28
+ def __init__(self, path):
29
+ path = Path(path)
30
+ if self._extension and path.suffix.lower() != f".{self._extension}":
31
+ path = path.with_name(path.name + f".{self._extension}")
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ self._path = path
34
+ self._records = []
35
+
36
+ def handle(self, **data):
37
+ if "values" in data:
38
+ data["values"] = [float(v) for v in data["values"]]
39
+ elif "samples" in data:
40
+ samples = data.pop("samples")
41
+ data.update(count=len(samples), min=float(min(samples)), max=float(max(samples)))
42
+ self._records.append(data)
43
+
44
+ def close(self):
45
+ raise NotImplementedError
46
+
47
+
48
+ class JsonDataHandler(_BufferedFileDataHandler):
49
+ """Writes all buffered records to a single JSON file on close()."""
50
+ _extension = "json"
51
+
52
+ def close(self):
53
+ with open(self._path, "w") as f:
54
+ json.dump(self._records, f, indent=2, default=str)
55
+
56
+
57
+ class CsvDataHandler(_BufferedFileDataHandler):
58
+ """Writes all buffered records to a CSV file on close(). Columns vary per
59
+ row since different modes carry different fields."""
60
+ _extension = "csv"
61
+
62
+ def close(self):
63
+ fieldnames = list(dict.fromkeys(key for record in self._records for key in record))
64
+ with open(self._path, "w", newline="") as f:
65
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
66
+ writer.writeheader()
67
+ writer.writerows(self._records)
68
+
69
+
70
+ class PickleDataHandler(_BufferedFileDataHandler):
71
+ """Writes all buffered records (as Python objects) to a pickle file on close()."""
72
+ _extension = "pkl"
73
+
74
+ def close(self):
75
+ with open(self._path, "wb") as f:
76
+ pickle.dump(self._records, f)
77
+
78
+
79
+
80
+
81
+ class MultiDataHandler(DataHandler):
82
+ """Fans out to several handlers at once (e.g. print AND save AND plot)."""
83
+
84
+ def __init__(self, *handlers):
85
+ self._handlers = handlers
86
+
87
+ def handle(self, **data):
88
+ for h in self._handlers:
89
+ h.handle(**data)
90
+
91
+ def close(self):
92
+ for h in self._handlers:
93
+ if hasattr(h, "close"):
94
+ h.close()
slm_api/helpers/fft.py ADDED
@@ -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
@@ -0,0 +1,64 @@
1
+ import miniaudio
2
+ import numpy as np
3
+ from io import BytesIO
4
+
5
+ class BitBuffer:
6
+ def __init__(self, buf):
7
+ self.out = buf
8
+ self.bitbuffer = 0
9
+ self.bitbufferlen = 0
10
+
11
+ def write_int(self, n, val):
12
+ self.bitbuffer <<= n
13
+ self.bitbuffer |= val & ((1 << n) - 1)
14
+ self.bitbufferlen += n
15
+ while self.bitbufferlen >= 8:
16
+ self.bitbufferlen -= 8
17
+ b = (self.bitbuffer >> self.bitbufferlen) & 0xFF
18
+ self.out.write(bytes((b,)))
19
+ self.bitbuffer &= (1 << self.bitbufferlen) - 1
20
+
21
+ def close(self):
22
+ self.out.close()
23
+
24
+ def __enter__(self):
25
+ return self
26
+
27
+ def __exit__(self, type, value, traceback):
28
+ return
29
+
30
+ def add_header(buf, BLOCK_SIZE, NUMCHANNEL=1, BITDEPTH=24, SAMPLEDATALENGTH=4096, stream_data=0):
31
+ # Stream
32
+ buf.write_int(32, 0x664C6143) # fLaC
33
+ # METADATA_BLOCK_HEADER
34
+ buf.write_int(1, 1)
35
+ buf.write_int(7, 0)
36
+ buf.write_int(24, 34)
37
+ # METADATA_BLOCK_STREAMINFO
38
+ buf.write_int(16, BLOCK_SIZE * 3) # BLOCK SIZE
39
+ buf.write_int(16, BLOCK_SIZE * 3) # BLOCK SIZE
40
+ buf.write_int(24, 0) # Frame size
41
+ buf.write_int(24, 0) # Frame size
42
+ buf.write_int(20, 2**16) # SAMPLERATE
43
+ buf.write_int(3, NUMCHANNEL - 1) # NumChannels
44
+ buf.write_int(5, BITDEPTH - 1) # BIT DEPTH
45
+ buf.write_int(36, SAMPLEDATALENGTH)
46
+ for _ in range(16):
47
+ buf.write_int(8, 0)
48
+
49
+ # End flac header begin of writing data
50
+ for t in stream_data:
51
+ buf.write_int(8, t)
52
+
53
+ def decode(flac_stream, calibrationFactor):
54
+ # Add the header in front of the encoded samples
55
+ fp = BytesIO()
56
+ with BitBuffer(fp) as buf:
57
+ add_header(buf, BLOCK_SIZE=flac_stream.frame_length, stream_data = flac_stream.frame)
58
+ fp.seek(0)
59
+ # Decode samples
60
+ tmp = miniaudio.flac_read_s32(fp.read())
61
+ # now the samples is scaled to Pa
62
+ tmp = (np.array(tmp.samples) >> 8) # 32 bit -> 24 bit
63
+ samples = tmp * calibrationFactor
64
+ return samples
@@ -0,0 +1,79 @@
1
+ import ssl
2
+ import requests
3
+ from pathlib import Path
4
+ from requests.adapters import HTTPAdapter
5
+
6
+ _CERT_FILE = Path(__file__).resolve().parent / "webxi_root.pem"
7
+
8
+
9
+ class WebxiAdapter(HTTPAdapter):
10
+ """Trusts only webxi_root.pem; skips hostname check since device certs are per-serial, not per-IP."""
11
+
12
+ def init_poolmanager(self, *args, **kwargs):
13
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
14
+ ctx.load_verify_locations(cafile=str(_CERT_FILE))
15
+ ctx.check_hostname = False
16
+ ctx.verify_mode = ssl.CERT_REQUIRED
17
+ kwargs["ssl_context"] = ctx
18
+ kwargs["assert_hostname"] = False # urllib3 does its own SAN check independent of ssl_context.check_hostname
19
+ return super().init_poolmanager(*args, **kwargs)
20
+
21
+ def build_ssl_context():
22
+ """SSLContext trusting only webxi_root.pem, with hostname checking disabled."""
23
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
24
+ ctx.load_verify_locations(cafile=str(_CERT_FILE))
25
+ ctx.check_hostname = False
26
+ ctx.verify_mode = ssl.CERT_REQUIRED
27
+ return ctx
28
+
29
+
30
+ _session = requests.Session()
31
+ if _CERT_FILE.is_file():
32
+ _session.mount("https://", WebxiAdapter())
33
+
34
+ class UseHttps:
35
+ use_https = True # class attribute: shared, module-wide default
36
+
37
+
38
+
39
+ def __init__(self, use_https=None):
40
+ if use_https is not None:
41
+ UseHttps.use_https = use_https
42
+
43
+ def request(self, method, host, endpoint, **kwargs):
44
+ """Send a request to the SLM. HTTPS calls are verified against webxi_root.pem via WebxiAdapter."""
45
+ if UseHttps.use_https:
46
+ if host.startswith("http://"):
47
+ stripped_host = host.removeprefix("http://")
48
+ url = f"https://{stripped_host}{endpoint}"
49
+ elif host.startswith("https://"):
50
+ url = host + endpoint
51
+ else:
52
+ url = f"https://{host}{endpoint}"
53
+ else:
54
+ stripped_host = host.removeprefix("https://").removeprefix("http://")
55
+ url = f"http://{stripped_host}{endpoint}"
56
+
57
+ kwargs.setdefault("timeout", 10)
58
+ kwargs.setdefault("verify", str(_CERT_FILE))
59
+
60
+ # print(f"[https_requests] {method} {url}")
61
+ response = _session.request(method, url, **kwargs)
62
+ response.raise_for_status()
63
+ return response
64
+
65
+
66
+ def get(host, endpoint, **kwargs):
67
+ return UseHttps().request("GET", host, endpoint, **kwargs)
68
+
69
+
70
+ def put(host, endpoint, **kwargs):
71
+ return UseHttps().request("PUT", host, endpoint, **kwargs)
72
+
73
+
74
+ def post(host, endpoint, **kwargs):
75
+ return UseHttps().request("POST", host, endpoint, **kwargs)
76
+
77
+
78
+ def delete(host, endpoint, **kwargs):
79
+ return UseHttps().request("DELETE", host, endpoint, **kwargs)
@@ -0,0 +1,20 @@
1
+ from . import https_requests
2
+
3
+ def start_pause_measurement(hostID,start):
4
+ """This function starts or pause a measurement.
5
+ If trying to start a measurement and the device is already running
6
+ will this function do nothing, same for pause."""
7
+ response = https_requests.get(hostID, "/webxi/Applications/SLM/State")
8
+ print(response.json())
9
+ if start and response.json() != "Running":
10
+ response = https_requests.put(hostID, "/WebXi/Applications/SLM?Action=StartPause")
11
+ assert(response.status_code == 200)
12
+ elif not start and response.json() == "Running":
13
+ response = https_requests.put(hostID, "/WebXi/Applications/SLM?Action=StartPause")
14
+ assert(response.status_code == 200)
15
+
16
+ def stop_measurement(hostID):
17
+ """Stop a measurement on the device"""
18
+ response = https_requests.put(hostID, "/WebXi/Applications/SLM?Action=Stop")
19
+ assert(response.status_code == 200)
20
+