Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions app/services/recording_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
}

The data files themselves are named by the loggers from the same name:
<name>.csv (fas_bridge), <name>_sensors.csv and <name>_actuators.csv
(novaGround). ops/Nova.ps1 `archive` bundles all of them with the referenced
FAS_<name>.csv (fas_bridge), novaGround_<name>_sensors.csv and
novaGround_<name>_actuators.csv (novaGround). ops/Nova.ps1 `archive` bundles all of them with the referenced
config snapshots.
"""
from __future__ import annotations
Expand All @@ -39,10 +39,12 @@
def recording_files(data_dir: Path, name: str) -> list[Path]:
"""Every file belonging to recording `name`.

Matches <name>.<ext> and <name>_<word>.<ext> but not <name>0... so that
"..._data_1" never claims "..._data_10_sensors.csv".
Matches [<logger>_]<name>.<ext> and [<logger>_]<name>_<word>.<ext> but not
<name>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))


Expand Down
40 changes: 40 additions & 0 deletions tests/test_fas_bridge_data_file.py
Original file line number Diff line number Diff line change
@@ -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"]
11 changes: 11 additions & 0 deletions tests/test_station_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down
24 changes: 19 additions & 5 deletions tools/fas_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_<recording>.csv`.

Every logger prefixes its files with its own name so the source is clear
in the shared data folder (novaGround: `novaGround_<recording>_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,
Expand Down Expand Up @@ -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":"<recording>"}
{"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")
Expand Down
Loading