diff --git a/app/services/recording_service.py b/app/services/recording_service.py index 041caf5..8c0e3e3 100644 --- a/app/services/recording_service.py +++ b/app/services/recording_service.py @@ -14,8 +14,8 @@ } The data files themselves are named by the loggers from the same name: -.csv (fas_bridge), _sensors.csv and _actuators.csv -(novaGround). ops/Nova.ps1 `archive` bundles all of them with the referenced +FAS_.csv (fas_bridge), novaGround__sensors.csv and +novaGround__actuators.csv (novaGround). ops/Nova.ps1 `archive` bundles all of them with the referenced config snapshots. """ from __future__ import annotations @@ -39,10 +39,12 @@ def recording_files(data_dir: Path, name: str) -> list[Path]: """Every file belonging to recording `name`. - Matches . and _. but not 0... so that - "..._data_1" never claims "..._data_10_sensors.csv". + Matches [_]. and [_]_. but not + 0... so that "..._data_1" never claims "..._data_10_sensors.csv". + A logger prefix is a word followed by "_"; recording names start with a + digit, so a prefix can never swallow another recording's name. """ - pattern = re.compile(rf"^{re.escape(name)}(\.|_[A-Za-z])") + pattern = re.compile(rf"^([A-Za-z][A-Za-z0-9]*_)?{re.escape(name)}(\.|_[A-Za-z])") return sorted(p for p in data_dir.iterdir() if p.is_file() and pattern.match(p.name)) diff --git a/tests/test_fas_bridge_data_file.py b/tests/test_fas_bridge_data_file.py new file mode 100644 index 0000000..1968051 --- /dev/null +++ b/tests/test_fas_bridge_data_file.py @@ -0,0 +1,40 @@ +"""FAS bridge data-file naming: every logger prefixes its files with its own name.""" +import importlib.util +from pathlib import Path + +import pytest + +pytest.importorskip("serial") +pytest.importorskip("paho.mqtt.client") + +_spec = importlib.util.spec_from_file_location( + "fas_bridge", Path(__file__).resolve().parents[1] / "tools" / "fas_bridge.py" +) +fas_bridge = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(fas_bridge) + + +@pytest.mark.parametrize( + "given, expected", + [ + ("2026-10-04-14_data_0", "FAS_2026-10-04-14_data_0.csv"), + ("2026-10-04-14_data_0.csv", "FAS_2026-10-04-14_data_0.csv"), + ("FAS_2026-10-04-14_data_0", "FAS_2026-10-04-14_data_0.csv"), + (None, "FAS_data.csv"), + ], +) +def test_data_file_name_is_prefixed(given, expected) -> None: + assert fas_bridge.data_file_name(given) == expected + + +def test_start_data_saving_writes_prefixed_file(tmp_path) -> None: + bridge = fas_bridge.FasBridge.__new__(fas_bridge.FasBridge) + bridge._data_dir = str(tmp_path) + bridge._record_file = None + import threading + + bridge._record_lock = threading.Lock() + bridge._log = lambda *a, **k: None + bridge._cmd_data_file({"type": "data_file", "action": "start_data_saving", "filename": "r_data_0"}) + bridge._cmd_data_file({"type": "data_file", "action": "stop_data_saving"}) + assert [p.name for p in tmp_path.iterdir()] == ["FAS_r_data_0.csv"] diff --git a/tests/test_station_config.py b/tests/test_station_config.py index 133ea1e..e227368 100644 --- a/tests/test_station_config.py +++ b/tests/test_station_config.py @@ -138,6 +138,17 @@ def test_recording_files_does_not_claim_longer_names(tmp_path) -> None: ] +def test_recording_files_includes_logger_prefixed_files(tmp_path) -> None: + for fname in [ + "FAS_r_data_1.csv", "novaGround_r_data_1_sensors.csv", "novaGround_r_data_1_actuators.csv", + "novaGround_r_data_10_sensors.csv", "FAS_r_data_10.csv", "other_data_1.csv", + ]: + (tmp_path / fname).write_text("", encoding="utf-8") + assert [p.name for p in recording_files(tmp_path, "r_data_1")] == [ + "FAS_r_data_1.csv", "novaGround_r_data_1_actuators.csv", "novaGround_r_data_1_sensors.csv", + ] + + def test_data_saving_publishes_the_recording_name(tmp_path) -> None: ctx = _context(tmp_path) calls: list[tuple] = [] diff --git a/tools/fas_bridge.py b/tools/fas_bridge.py index 45af0eb..73a60da 100644 --- a/tools/fas_bridge.py +++ b/tools/fas_bridge.py @@ -1462,6 +1462,23 @@ def _resolve_fas_board_id(cmd: dict) -> int: # ───────────────────────────────────────────────────────────────────────────── +DATA_FILE_PREFIX = "FAS" + + +def data_file_name(filename) -> str: + """CSV name for a recording: `FAS_.csv`. + + Every logger prefixes its files with its own name so the source is clear + in the shared data folder (novaGround: `novaGround__sensors.csv`). + """ + name = str(filename or "data") + if name.endswith(".csv"): + name = name[:-4] + if not name.startswith(DATA_FILE_PREFIX + "_"): + name = f"{DATA_FILE_PREFIX}_{name}" + return name + ".csv" + + class FasBridge: def __init__(self, port: str, baud: int, broker: str, node_id: str, publish_ms: int, verbosity: int, @@ -1602,16 +1619,13 @@ def _cmd_data_file(self, cmd: dict) -> None: """Start/stop recording ADC samples to a CSV in the bridge's data dir. Mirrors the backend's data-saving command: - {"type":"data_file","action":"start_data_saving","filename":"..._data_0"} + {"type":"data_file","action":"start_data_saving","filename":""} {"type":"data_file","action":"stop_data_saving"} """ action = str(cmd.get("action", "")).lower() if action == "start_data_saving": - filename = str(cmd.get("filename") or "fas_data") - if not filename.endswith(".csv"): - filename += ".csv" - path = os.path.join(self._data_dir, filename) + path = os.path.join(self._data_dir, data_file_name(cmd.get("filename"))) try: os.makedirs(self._data_dir, exist_ok=True) f = open(path, "w", newline="", encoding="utf-8")