From 02eed8bf763a5f7c37d73735353cf5061dd1b216 Mon Sep 17 00:00:00 2001 From: Christos Falas Date: Thu, 15 May 2025 09:50:27 +0100 Subject: [PATCH] add `run` command to CLI --- examples/measurements.py | 4 ++- where_fi/application.py | 30 ++++++++++------- where_fi/cli/__init__.py | 69 ++++++++++++++++++++-------------------- 3 files changed, 56 insertions(+), 47 deletions(-) diff --git a/examples/measurements.py b/examples/measurements.py index 7bee965..b2d5cc9 100644 --- a/examples/measurements.py +++ b/examples/measurements.py @@ -97,4 +97,6 @@ def _(proc: CSIMatrix) -> None: cnt += 1 -app.start() +if __name__ == "__main__": + print("Starting app") + app.start() diff --git a/where_fi/application.py b/where_fi/application.py index d0e82bd..ea6f74a 100644 --- a/where_fi/application.py +++ b/where_fi/application.py @@ -166,9 +166,11 @@ class CSIApplication: self.raw_csi_callback(sample.matrix) if self.pre_merge_callback is not None: self.pre_merge_callback(sample.frames) - processed = self.preprocessor.preprocess(sample.matrix, sample.frames) - if self.visualise_raw: - self.visualise_data(processed, visualise.figures.Figure.PROCESSED_CSI) + processed = self.preprocessor.preprocess( + sample.matrix, sample.frames, visualiser=self.visualise_data + ) + # if self.visualise_raw: + # self.visualise_data(processed, visualise.figures.Figure.PROCESSED_CSI) if self.preprocessed_csi_callback is not None: self.preprocessed_csi_callback(processed) @@ -197,17 +199,21 @@ class CSIApplication: self.buffer_thread.start() self.webapp_thread.start() - if self.processing_callback is not None: + def get_proc_sample() -> None: + sample = self.preprocessor.last_sample + if self.visualise_raw and sample is not None: + self.visualise_data( + sample, + visualise.figures.Figure.PROCESSED_CSI, + ) + if sample is not None and self.processing_callback is not None: + self.processing_callback(sample) + else: + self.logger.warning("No samples to process") - def get_proc_sample() -> None: - sample = self.preprocessor.last_sample - if sample is not None and self.processing_callback is not None: - self.processing_callback(sample) - else: - self.logger.warning("No samples to process") + self.scheduler = Scheduler(config.processing_sample_rate, get_proc_sample) + self.scheduler.start() - self.scheduler = Scheduler(config.processing_sample_rate, get_proc_sample) - self.scheduler.start() try: self.webapp_thread.join() except KeyboardInterrupt: diff --git a/where_fi/cli/__init__.py b/where_fi/cli/__init__.py index 731228a..b8a817b 100644 --- a/where_fi/cli/__init__.py +++ b/where_fi/cli/__init__.py @@ -1,4 +1,6 @@ +import importlib.util import logging +from pathlib import Path from queue import Queue from typing import cast @@ -7,11 +9,11 @@ import numpy.typing as npt import torch import typer +from where_fi.application import CSIApplication from where_fi.collection import CSIMatrix +from where_fi.config import config +from where_fi.processing.aoa import AoA -from ..application import CSIApplication -from ..config import config -from ..processing.aoa import AoA from ..visualise import server as visualise from . import file, globals @@ -53,44 +55,43 @@ def heatmap() -> None: @cli.command() -def phase_analysis( - subcarriers: list[int] = [0], rx_antenna: int = 0, tx_antenna: int = 0 -) -> None: +def run(file: Path, app_name: str = "app") -> None: """ - Visualise the phase information in the CSI data received from the antennas. + Run the application with the given file. - The data goes through the same preprocessing steps as the heatmap command, but - instead of going through the AoA estimation, we simply analyse the phase of the - selected subcarrier and antenna. + Can be used for running arbitrary CSI applications with non-default data streams + (e.g. from file or environment simulation). """ + print(file.absolute()) + if not file.exists(): + print(f"Error: File '{file}' does not exist.") + raise typer.Exit(1) + module_name = file.stem - app = CSIApplication(globals.csi_producer) + spec = importlib.util.spec_from_file_location(module_name, str(file)) + if spec is None: + print(f"Could not load spec from {file}") + raise typer.Exit(1) - if isinstance(subcarriers, int): - subcarriers = [subcarriers] - print("Starting phase analysis on subcarriers: ", subcarriers) + module = importlib.util.module_from_spec(spec) + if spec.loader is None: + print(f"Could not load module from {file}") + raise typer.Exit(1) + try: + spec.loader.exec_module(module) + except Exception as e: + print(f"Failed to execute {file}: {e}") + raise typer.Exit(1) from e - subcarrier_phase: dict[int, Queue[float]] = { - x: Queue(config.collection_sample_rate) for x in subcarriers - } + if not hasattr(module, app_name): + print(f"Error: '{app_name}' not defined in the module.") + raise typer.Exit(1) + if not isinstance(module.app, CSIApplication): + print(f"Error: '{app_name}' is not a CSIApplication.") + raise typer.Exit(1) - @app.on_sample - def _(sample: CSIMatrix) -> None: - for subcarrier in subcarriers: - phase = cast(float, np.angle(sample[subcarrier, rx_antenna, tx_antenna])) - if subcarrier_phase[subcarrier].full(): - subcarrier_phase[subcarrier].get() - subcarrier_phase[subcarrier].put(phase) - - @app.on_process - def _(_: CSIMatrix) -> None: - logger.info("Updating phase visualisation") - app.visualise_data( - np.array([x.queue for x in subcarrier_phase.values()]), - visualise.figures.Figure.PHASE_ANALYSIS, - ) - - app.start() + module.app.producer = globals.csi_producer + module.app.start() cli.add_typer(file.app, name="file", help="Commands for working with CSI files")