This commit is contained in:
Paweł Sadowski 2025-02-10 17:42:34 +00:00
parent 2d5a2bb53b
commit 04d9f40ab3
5 changed files with 225 additions and 0 deletions

47
README.txt Normal file
View File

@ -0,0 +1,47 @@
# Measurement tool
Configure in main.py file. Any Python expression may be used.
## ARGS
In any place, where quick changing of parameters is desired, command line arguments might be used instead of fixed configuration.
When program is run with:
python main.py ABC DEF
then ARGS[1] = "ABC" and ARGS[2] = "DEF"
## CONN_PARAMS
This parameter contains data required to connect to thrust stand and microphone.
- nor_addr - microphone IP address
- nor_ftp_user - micropphone FTP username
- nor_ftp_pass - microphone FTP password
- nor_recordings_dir - microphone recorded data path
- stand_tty - serial port, trust stand is attached to, COM... for windows, /dev/tty... for linux
## PWM_RANGE
Sets the sequence of throttle levels, that will be included in measurement.
Use any Python that will evaluate to Iterable[int].
Examples:
- PWM_RANGE = range(1100, 2000, 100) - 1000, 1100, ..., 1800, 1900 - range from 1100 to 2000 with step of 100. Note it does NOT include the end value.
- PWM_RANGE = list(range(1100, 1500, 50)) + list(range(1500, 2000, 50)) - 1000, 1100, 1200, 1300, 1400, 1500, 1550, ..., 1900, 1950. - Ranges may be joined to achieve not uniform distribution.
- PWM_RANGE = [1300, 1800, 1900, 1950, 1975, 1990, 2000] - Fully custom measurement may be defined by specifying list of throttle levels manually.
## OUTPUT_FILE
This parameter specifies name of the directory, that will be created and written with measurement series data.
Convenient to use with ARGS.
Examples:
- OUTPUT_FILE = 'tests/baseline_r1_a0' - measurement will be saved to specified directory
- OUTPUT_FILE = ARGS[1] - first argument will be used as output name. Run program with 'python main.py tests/baseline_r1_a0' to achieve the same effect as above
# Data visualizer
Run with:
python visualizer.py [list of measurement series to visualize]
Examples:
- python visualizer.py tests/baseline_r1_a0 tests/baseline_r1_a90 - plots these two series
- python visualizer.py tests/* - plots all series in 'tests' directory
- python visualizer.py tests/baseline_r*_a0 - plots all series matching the expression - baseline_r1_a0, baseline_r2_a0, etc.

29
main.py Normal file
View File

@ -0,0 +1,29 @@
from measure import measurement
from measurement_station import ConnectionParams
import sys
ARGS = sys.argv
# Configuration start
# See README
CONN_PARAMS = ConnectionParams(
stand_tty='/dev/ttyUSB0',
# stand_tty='COM7',
nor_addr='10.145.1.1',
nor_ftp_user='AAAA',
nor_ftp_pass='1234',
nor_recordings_dir='/SD Card/NorMeas/Nor14530408/TEST'
)
# PWM_RANGE = list(range(1200, 1500, 100)) + list(range(1500,1800,50))
PWM_RANGE = range(1100, 1800, 100)
# PWM_RANGE = [1100, 1200, 1300, 1350, 1375, 1400, 1425, 1450, 1475, 1500]
# OUTPUT_FILE = folder1/subfolderfolder
OUTPUT_FILE = ARGS[1]
# Configuration end
measurement(CONN_PARAMS, PWM_RANGE, OUTPUT_FILE)

10
measure.py Normal file
View File

@ -0,0 +1,10 @@
import asyncio
from measurement_station import ConnectionParams, meas_series
import pickle
def measurement(params: ConnectionParams, pwm_range, filename: str):
x = asyncio.run(meas_series(params, pwm_range))
with open(filename, 'wb') as f:
pickle.dump(x, f)

22
test.py Normal file
View File

@ -0,0 +1,22 @@
import asyncio
async def c1(r: asyncio.StreamReader):
data = await r.readexactly(1)
print(data)
print('1')
async def c2(r: asyncio.StreamReader):
while True:
data = await r.readexactly(1)
print('2')
print(data)
async def main():
r, w = await asyncio.open_connection('localhost', 9999)
t1 = asyncio.create_task(c1(r))
t2 = asyncio.create_task(c2(r))
while True:
await asyncio.sleep(100000)
asyncio.run(main())

117
visualizer.py Normal file
View File

@ -0,0 +1,117 @@
from dataclasses import fields
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, MultiChoice, Select
from bokeh.plotting import figure
import pickle
from measurement_station import OpPointData
import math
from bokeh.server.server import Server
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
import sys
def oppoint_data_src(p: OpPointData) -> dict[str, float]:
ret = {}
ret.update({k: v for k, v in field_entries(p.data_thrust_stand_avg).items()})
ret.update({k: v for k, v in p.data_accustic_avg.items()})
return ret
def field_entries(datacls) -> dict[str, float]:
fnames = [f.name for f in fields(datacls)]
return {n: getattr(datacls, n) for n in fnames}
def read_series(fname: str) -> dict[int, OpPointData]:
with open(fname, 'rb') as f:
return pickle.load(f)
def series_dataclosrc(series: dict[int, OpPointData]) -> ColumnDataSource:
entries = [oppoint_data_src(p) for p in series.values()]
keys = entries[0].keys()
data = { key: [e[key] for e in entries] for key in keys }
return ColumnDataSource(data)
def fft_datasrc(op: OpPointData) -> ColumnDataSource:
fft_data = op.nor_report_parsed.glob_fft
freqs = sorted(fft_data.keys())
vals = [fft_data[f] for f in freqs]
data = { "FREQ": freqs, "POWER": vals }
return data
def make_doc(doc, files):
series = [ read_series(f) for f in files ]
sources = [series_dataclosrc(s) for s in series]
keys = list(sources[0].data.keys())
p = figure(title='', x_axis_label='X', y_axis_label='Y', tools=['hover', 'pan', 'xwheel_zoom'])
p.sizing_mode = 'scale_both' # type: ignore
multi_choice = MultiChoice(title="Y Axis", options=keys)
xsel = Select(title="X axis:", value="", options=keys)
srcsel = MultiChoice(title="Source:", value=files, options=files)
colors = ['blue', 'green', 'red', 'yellow', 'orange', 'purple']
pfft = figure(title='', x_axis_label='X', y_axis_label='Y', tools=['hover', 'pan', 'xwheel_zoom'])
pfft.sizing_mode = 'scale_both' # type: ignore
def update_plot(attr, _, new_values):
p.renderers = [] # type: ignore
p.xaxis.axis_label = xsel.value
ymin = math.inf
ymax = -math.inf
for source, fname, color in zip(sources, files, colors):
if fname not in srcsel.value: #type: ignore
continue
for column in multi_choice.value: # type: ignore
p.line(x=xsel.value, y=column, source=source, legend_label=f'{fname} {column}', line_width=2, color=color)
ymin = min(min(source.data[column]), ymin)
ymax = max(max(source.data[column]), ymax)
margin = (ymax-ymin) * 0.1
p.y_range.start = ymin - margin
p.y_range.end = ymax + margin
pfft.renderers = [] # type: ignore
for serie, fname, color in zip(series, files, colors):
if fname not in srcsel.value: #type: ignore
continue
data = fft_datasrc(serie[1500])
pfft.line(x='FREQ', y='POWER', source=data, legend_label=f'{fname} FFT', line_width=2, color=color)
multi_choice.on_change('value', update_plot)
xsel.on_change('value', update_plot)
srcsel.on_change('value', update_plot)
layout = column(column(srcsel, multi_choice, xsel), p, pfft)
layout.sizing_mode = 'scale_both' # type: ignore
doc.add_root(layout)
if __name__ == "__main__":
files = sys.argv[1:]
if len(files) == 0:
print(f'Usage: {sys.argv[0]} [measurement directory/directories]')
print(f'Examples:')
print(f'- {sys.argv[0]} benchmark1/shroud_1m')
print(f'- {sys.argv[0]} benchmark1/shroud_*')
print(f'- {sys.argv[0]} benchmark1/*')
exit()
apps = {'/': Application(FunctionHandler(lambda doc: make_doc(doc, files)))}
server = Server(apps, port=5000)
print('Visualizer started. navigate to http://localhost:5000/ to continue')
print('Exit with interrupt (Ctrl+C)')
server.run_until_shutdown()