diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..87751ce --- /dev/null +++ b/logger.py @@ -0,0 +1,22 @@ +from datetime import datetime +from colorist import Color + +STYLE_DEFAULT = Color.WHITE + + +def log_time(): + return datetime.now().strftime("%H:%M:%S.%f")[:-3] + +def log_time_name(): + return datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + +log_file = open(f'logs/L-{log_time_name()}.txt', 'a') + +def spr(x, style=STYLE_DEFAULT): + print(f"[{log_time()}] {style}{x}{Color.OFF}") + log_file.write(f'[{log_time()}] {x}\n') + +def prefixed_spr(prefix, pr=spr): + def ret(x, *args): + pr(f"{prefix}: {x}", *args) + return ret diff --git a/measurement_station.py b/measurement_station.py new file mode 100644 index 0000000..beab1bb --- /dev/null +++ b/measurement_station.py @@ -0,0 +1,91 @@ +import asyncio +from collections.abc import Iterable, Sequence +from dataclasses import dataclass +from logging import debug +import logging + +from logger import spr +import thrust_stand +import norsonic + +from norsonic_fetcher import nor_get_reports, recording_path +from thrust_stand import ThrustStand, ThrustStandMeasurement + +@dataclass +class OpPointData: + data_thrust_stand: Sequence[ThrustStandMeasurement] + data_accustic: dict + + @property + def data_thrust_stand_avg(self): + return sum(self.data_thrust_stand, ThrustStandMeasurement.zero())/len(self.data_thrust_stand) + + @property + def data_accustic_avg(self): + return {k: sum(map(float, (row[k] for row in self.data_accustic)))/len(self.data_accustic) for k in self.data_accustic[0].keys() if k != 'Date'} + + + +@dataclass +class ConnectionParams: + stand_tty: str + nor_addr: str + nor_ftp_user: str + nor_ftp_pass: str + nor_recordings_dir: str + +CONN_PARAMS = ConnectionParams( + stand_tty='/dev/ttyUSB0', + nor_addr='10.145.1.1', + nor_ftp_user='AAAA', + nor_ftp_pass='1234', + nor_recordings_dir='/SD Card/NorMeas/Nor14530408/TEST' + ) + +async def meas_series(params: ConnectionParams, pwms: Iterable[int]): + stand = await ThrustStand.open_connection(params.stand_tty) + nor = await norsonic.open_connection(params.nor_addr) + + results_stand = {} + files_nor = {} + + # TODO TARA + sample, = stand.get_samples_raw(1) + stand.tare_thrust = thrust_stand.raw_thrust(sample.load_thrust) + stand.tare_torque = thrust_stand.raw_torque(sample.load_left, sample.load_right) + + for pwm in pwms: + stand.mot_pwm = pwm + spr(f'Output: {pwm}PWM') + await stand.stabilize_rpm(5, 1) + spr(f'Starting measurement') + stand_series_pending = stand.start_meas_series() + files_nor[pwm] = await norsonic.record(nor) + spr(f'Done') + results_stand[pwm] = stand.finish_meas_series(stand_series_pending) + + stand.mot_pwm = 1000 + + await asyncio.sleep(3) + + spr('Downlaoding reports') + + + nor_reports = await nor_get_reports(params.nor_addr, params.nor_ftp_user, params.nor_ftp_pass, [files_nor[pwm] for pwm in pwms]) + spr('Done') + + ret = { + pwm: OpPointData( + data_thrust_stand=results_stand[pwm], + data_accustic=nor_reports[i] + ) for i, pwm in enumerate(pwms) + } + return ret + + +async def main(): + global x + x = await meas_series(CONN_PARAMS, range(1100, 1950, 30)) + +# logging.basicConfig(level=logging.INFO) +# asyncio.run(main(), debug=True) diff --git a/norsonic.py b/norsonic.py new file mode 100644 index 0000000..883ae92 --- /dev/null +++ b/norsonic.py @@ -0,0 +1,105 @@ +import asyncio +from collections.abc import Callable, Iterable, Sequence +from typing import Optional +from urllib import parse +import aioftp +import websockets +from websockets.asyncio.client import ClientConnection + +from logger import spr +import norsonic_fetcher + +MSG_OUT_NEW = "NewMeasurement" +MSG_OUT_START = "StartMeasurement" + +KEY_STATE = 'State' +KEY_FILENAME = 'GraphHeader' +VAL_STATE_NEW = '' +VAL_STATE_RECORDING = "" +VAL_STATE_WAITING = "" +VAL_STATE_DONE = "" + +def parse_msg(msg: str) -> dict[str, str]: + ret = {} + for l in msg.splitlines(): + # print(f'par "{l}"') + if len(l) == 0: + continue + if l == 'clear': + break + if ':' not in l: + print(f'W: Unexpected line in message: {l}') + continue + + k, v = l.split(':', 1) + if k in ret: + if ret[k] == v: + continue + if len(v) == 0: + continue + print(f'W: Duplicate key detected ({k}: {v})') + raise RuntimeError() + ret[k] = v + + return ret + +class NSWSConnection: + # def __init__(self, ws: Web) + ... + + +async def recv_msg(sock: ClientConnection) -> dict[str, str]: + recv = await sock.recv() + if not isinstance(recv, str): + raise RuntimeError('Invalid message received') + msg = parse_msg(recv) + return msg + + +async def wait_for_state(sock: ClientConnection, state: str, expected_states: Iterable[str] = []) -> dict[str, str]: + while True: + msg = await recv_msg(sock) + # spr(msg) + if KEY_STATE in msg: + if msg[KEY_STATE] == state: + return msg + elif msg[KEY_STATE] not in expected_states: + spr(f'W: Unexpected state received: {msg[KEY_STATE]}') + + +async def record(sock: ClientConnection, start_callback: Optional[Callable[[], None]] = None): + await sock.send(MSG_OUT_NEW) + spr('Initializing measurement') + await wait_for_state(sock, VAL_STATE_NEW, (VAL_STATE_DONE, )) + + await sock.send(MSG_OUT_START) + spr('Starting measurement') + await wait_for_state(sock, VAL_STATE_WAITING, (VAL_STATE_NEW, )) + spr('Waiting for start') + await wait_for_state(sock, VAL_STATE_RECORDING) + + if start_callback is not None: + start_callback() + + spr('Waiting for finish') + msg = await wait_for_state(sock, VAL_STATE_DONE) + return msg[KEY_FILENAME] + +async def open_connection(address: str) -> ClientConnection: + return await websockets.connect(f'ws://{address}/live', ping_interval=None) + + +# async def main(): +# sock = await websockets.connect('ws://10.145.1.1/live') +# spr('Connected to server') +# name = await record(sock) +# spr(f'finished {name}') +# c = aioftp.Client(parse_list_line_custom=norsonic_fetcher.ftp_parse_line) +# res = await c.connect('10.145.1.1') +# res = await c.login('AAAA', '1234') +# global x +# # x = norsonic_fetcher.parse_report(await norsonic_fetcher.ftp_fetch(c, norsonic_fetcher.recording_path(name))) +# x = norsonic_fetcher.parse_report(await norsonic_fetcher.ftp_fetch(c, norsonic_fetcher.FNAME)) +# print(x) + + diff --git a/norsonic_fetcher.py b/norsonic_fetcher.py new file mode 100644 index 0000000..09b356a --- /dev/null +++ b/norsonic_fetcher.py @@ -0,0 +1,90 @@ +import asyncio +from pathlib import Path +from re import split +from typing import Iterable, Sequence +import aioftp + +from logger import spr + +FNAME = '/SD Card/NorMeas/Nor14530408/TEST/VIP 124 2024-12-10 15-31-19/VIP 124 2024-12-10 15-31-19.txt' +DNAME = '/SD Card/NorMeas/Nor14530408/TEST/VIP 124 2024-12-10 15-31-19/' +RECORDINGS_PATH = '/SD Card/NorMeas/Nor14530408/TEST' +CRLF = '\r\n' +COL_SEPARATOR = '\t' +STR_DIR = '' + +def recording_path(rec_name: str) -> str: + return f'{RECORDINGS_PATH}/{rec_name}/{rec_name}.txt' + +def ftp_parse_line(line: bytes): + l = line.decode() + date, time, size, name = l.split(None, 3) + _ = date + _ = time + attrdict = {'modify': None, 'size': 0, 'type': 'dir'} + if size != STR_DIR: + attrdict['type'] = 'file' + attrdict['size'] = int(size) + + if not name.endswith(CRLF): + raise ValueError() + name = name[:-2] + path = Path(name) + return path, attrdict + +async def ftp_fetch(client: aioftp.Client, path: str) -> bytes: + spr(f'fetching {path}') + stream = await client.download_stream(path) + file = b''.join([block async for block in stream.iter_by_block()]) + await stream.finish() + stream.close() + return file + +def parse_report_table(table: str): + _, head_cols, *rows = table.split(CRLF) + cols = head_cols.split(COL_SEPARATOR) + + def parse_row(row: str): + vals = row.split(COL_SEPARATOR) + if len(vals) != len(cols): + print(vals) + print(cols) + print(len(cols)) + print(len(vals)) + raise ValueError('NorPaeser: Invalid row read') + return {cols[i]: vals[i] for i in range(len(cols))} + + return [parse_row(r) for r in rows if len(r) > 0] + +def parse_report(report: bytes): + *header, glob, prof = report.decode().split(2*CRLF) + t_prof = parse_report_table(prof) + # t_glob = parse_report_table(glob) + # return t_glob, t_prof + return t_prof + +async def nor_get_reports(addr: str, user: str, password: str, recs: Iterable[str]) -> Sequence: + async with aioftp.Client.context(addr, user=user, password=password, parse_list_line_custom=ftp_parse_line) as ftp: + return [parse_report(await ftp_fetch(ftp, recording_path(p))) for p in recs] + + +async def main(): + c = aioftp.Client(parse_list_line_custom=ftp_parse_line) + res = await c.connect('10.145.1.1') + res = await c.login('AAAA', '1234') + global x + ns = [ + 'VIP 197 2024-12-13 11-48-21', + 'VIP 198 2024-12-13 11-48-28', + 'VIP 199 2024-12-13 11-48-35', + ] + + x = await nor_get_reports('10.145.1.1', 'AAAA', '1234', ns) + print(x) + + + # for n in ns: + # x = parse_report(await ftp_fetch(c, n)) + # print(x) + +# asyncio.run(main())