diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..b897f05 --- /dev/null +++ b/README.txt @@ -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. diff --git a/main.py b/main.py new file mode 100644 index 0000000..0959756 --- /dev/null +++ b/main.py @@ -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) diff --git a/measure.py b/measure.py new file mode 100644 index 0000000..8c0671c --- /dev/null +++ b/measure.py @@ -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) diff --git a/test.py b/test.py new file mode 100644 index 0000000..827f3ed --- /dev/null +++ b/test.py @@ -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()) diff --git a/visualizer.py b/visualizer.py new file mode 100644 index 0000000..9a8dcd1 --- /dev/null +++ b/visualizer.py @@ -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() +