cortexforge 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. cortexforge-0.1.0/PKG-INFO +16 -0
  2. cortexforge-0.1.0/README.md +65 -0
  3. cortexforge-0.1.0/pyproject.toml +41 -0
  4. cortexforge-0.1.0/setup.cfg +4 -0
  5. cortexforge-0.1.0/src/cortexforge/cli/__init__.py +0 -0
  6. cortexforge-0.1.0/src/cortexforge/cli/forge.py +58 -0
  7. cortexforge-0.1.0/src/cortexforge/cli/planner.py +45 -0
  8. cortexforge-0.1.0/src/cortexforge/datasets/__init__.py +17 -0
  9. cortexforge-0.1.0/src/cortexforge/datasets/api.py +88 -0
  10. cortexforge-0.1.0/src/cortexforge/datasets/hash.py +11 -0
  11. cortexforge-0.1.0/src/cortexforge/datasets/local.py +87 -0
  12. cortexforge-0.1.0/src/cortexforge/datasets/manifest.py +68 -0
  13. cortexforge-0.1.0/src/cortexforge/datasets/types.py +33 -0
  14. cortexforge-0.1.0/src/cortexforge/forge/__init__.py +0 -0
  15. cortexforge-0.1.0/src/cortexforge/forge/main.py +25 -0
  16. cortexforge-0.1.0/src/cortexforge/forge/radio/__init__.py +0 -0
  17. cortexforge-0.1.0/src/cortexforge/forge/radio/rx.py +100 -0
  18. cortexforge-0.1.0/src/cortexforge/forge/radio/rx_recorder.py +40 -0
  19. cortexforge-0.1.0/src/cortexforge/forge/radio/tx.py +77 -0
  20. cortexforge-0.1.0/src/cortexforge/forge/radio/tx_burst.py +107 -0
  21. cortexforge-0.1.0/src/cortexforge/forge/radio/waveforms.py +42 -0
  22. cortexforge-0.1.0/src/cortexforge/forge/radio/waveforms_analog.py +85 -0
  23. cortexforge-0.1.0/src/cortexforge/forge/radio/waveforms_numerique.py +247 -0
  24. cortexforge-0.1.0/src/cortexforge/forge/utils/__init__.py +0 -0
  25. cortexforge-0.1.0/src/cortexforge/forge/utils/compute_baseline.py +64 -0
  26. cortexforge-0.1.0/src/cortexforge/forge/utils/load_timeline.py +36 -0
  27. cortexforge-0.1.0/src/cortexforge/forge/utils/node_identity.py +15 -0
  28. cortexforge-0.1.0/src/cortexforge/forge/utils/node_layout.py +45 -0
  29. cortexforge-0.1.0/src/cortexforge/forge/utils/sigmf/hash.py +10 -0
  30. cortexforge-0.1.0/src/cortexforge/forge/utils/sigmf/sigmf_annotations.py +90 -0
  31. cortexforge-0.1.0/src/cortexforge/forge/utils/sigmf/sigmf_captures.py +21 -0
  32. cortexforge-0.1.0/src/cortexforge/forge/utils/sigmf/sigmf_global.py +13 -0
  33. cortexforge-0.1.0/src/cortexforge/forge/utils/sigmf_writer.py +58 -0
  34. cortexforge-0.1.0/src/cortexforge/forge/utils/sync_barrier/rx_barrier_server.py +46 -0
  35. cortexforge-0.1.0/src/cortexforge/forge/utils/sync_barrier/sync_config.py +8 -0
  36. cortexforge-0.1.0/src/cortexforge/forge/utils/sync_barrier/tx_barrier_client.py +38 -0
  37. cortexforge-0.1.0/src/cortexforge/forge/utils/uhd_time.py +15 -0
  38. cortexforge-0.1.0/src/cortexforge/planner/__init__.py +0 -0
  39. cortexforge-0.1.0/src/cortexforge/planner/generators/__init__.py +0 -0
  40. cortexforge-0.1.0/src/cortexforge/planner/generators/cortexlab_scenario.py +44 -0
  41. cortexforge-0.1.0/src/cortexforge/planner/generators/experiment_scenario.py +191 -0
  42. cortexforge-0.1.0/src/cortexforge/planner/main.py +57 -0
  43. cortexforge-0.1.0/src/cortexforge/utils/__init__.py +0 -0
  44. cortexforge-0.1.0/src/cortexforge/utils/loader.py +35 -0
  45. cortexforge-0.1.0/src/cortexforge/utils/logger.py +21 -0
  46. cortexforge-0.1.0/src/cortexforge.egg-info/PKG-INFO +16 -0
  47. cortexforge-0.1.0/src/cortexforge.egg-info/SOURCES.txt +49 -0
  48. cortexforge-0.1.0/src/cortexforge.egg-info/dependency_links.txt +1 -0
  49. cortexforge-0.1.0/src/cortexforge.egg-info/entry_points.txt +3 -0
  50. cortexforge-0.1.0/src/cortexforge.egg-info/requires.txt +12 -0
  51. cortexforge-0.1.0/src/cortexforge.egg-info/top_level.txt +1 -0
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: cortexforge
3
+ Version: 0.1.0
4
+ Summary: RF generator dataset based on SLICES/CorteXlab
5
+ Author-email: Andrea Joly <andrea.joly@inria.fr>
6
+ Requires-Python: >=3.10
7
+ Provides-Extra: planner
8
+ Requires-Dist: numpy==2.3.4; extra == "planner"
9
+ Requires-Dist: pandas==2.3.3; extra == "planner"
10
+ Requires-Dist: python-dateutil==2.9.0.post0; extra == "planner"
11
+ Requires-Dist: pytz==2025.2; extra == "planner"
12
+ Requires-Dist: PyYAML==6.0.3; extra == "planner"
13
+ Requires-Dist: six==1.17.0; extra == "planner"
14
+ Requires-Dist: tzdata==2025.2; extra == "planner"
15
+ Provides-Extra: forge
16
+ Requires-Dist: sigmf==1.2.12; extra == "forge"
@@ -0,0 +1,65 @@
1
+ # CorteXForge
2
+ This project is a framework designed to automate the generation and execution of radio dataset experiments on the [Slices/CorteXlab](https://www.cortexlab.fr/doku.php?id=start) testbed.
3
+ It relies on the [GNU Radio](https://www.gnuradio.org) environment to record labeled transmissions of various signals.
4
+
5
+ ## Overview
6
+ This project is organized into two main components:
7
+ - Scenario generation: this part produces configuration files describing the experiment setup. It creates:
8
+ - a `scenario.yaml` file defining which nodes will be used on CorteXlab;
9
+ - an `timeline.csv` file orchestrating the role and sequence of these nodes.
10
+ - Experiment execution: this part deploys and executes the generates experiment definitions (`timeline.csv`) directly on the [Slices/CorteXlab](https://www.cortexlab.fr/doku.php?id=start) nodes.
11
+
12
+ ## Quick start (User Guide) :rocket:
13
+
14
+ ### 1. Scenario Generator
15
+ :warning: This module is implemented in Python 3.13 !
16
+
17
+ The scenario generator can be executed locally before deployment in Slices/CorteXlab. It allows configuration of experimental parameters such as:
18
+ - selected nodes to be used
19
+ - time of recording (in seconds)
20
+
21
+ ### Example usage
22
+ - ```git clone https://github.com/Andreaj42/CorteXForge.git```
23
+ - ```python3.13 -m venv .venv```
24
+ - ```source .venv/bin/activate```
25
+ - ```pip install -e .[planner]```
26
+ - ```cortexforge-planner --nodes-path confis/nodes.yaml --duration 600 --output-path my/path/on/cortexlab```
27
+
28
+ ### 2. Forge
29
+ :warning: This part must be executed directly on the Slices/CorteXlab testbed !
30
+
31
+ Each nodes defined before in the previous stage will run a GNU Radio flowgraph according to the configuration.
32
+
33
+
34
+ ### Example usage
35
+ First, connect to the testbed:
36
+ - ```ssh username@gw.cortexlab.fr```
37
+
38
+ Next, book the testbed with your selected nodes (nodes: 5, 10, and 31 here) for the time of recording (increase the value):
39
+ - ```oarsub -l {"network_address in ('mnode5.cortexlab.fr', 'mnode10.cortexlab.fr', 'mnode31.cortexlab.fr')"}/nodes=3,walltime=0:20:00 -r "2025-10-12 21:03:00"```
40
+
41
+ To delete a job, use:
42
+ - ```oardel job_id```
43
+
44
+ And move the previously generated ```experiment``` folder into your **Cortexlab** home, then run:
45
+ - ```minus task create experiment -f```
46
+ - ```minus task submit experiment.task```
47
+
48
+ To monitor your experiment, use:
49
+ - ```minus testbed status```
50
+ - ```minus log -d```
51
+
52
+ ## Developer Guide :hammer_and_wrench:
53
+
54
+ Now clone the project:
55
+ - ```git clone https://github.com/Andreaj42/CorteXForge.git```
56
+ - ```cd forge```
57
+
58
+
59
+ ### Docker Images :whale:
60
+ To simplify deployment and ensure reproductibility, we generated a Docker image.
61
+ This image extends the standard CorteXlab toolchain and adds the required dependencies for forge.
62
+
63
+ ## Useful links :link:
64
+ - [xp.cortexlab.fr](xp.cortexlab.fr/app)
65
+ - [wiki.cortexlab.fr](wiki.cortexlab.fr)
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cortexforge"
7
+ version = "0.1.0"
8
+ description = "RF generator dataset based on SLICES/CorteXlab"
9
+ authors = [
10
+ { name = "Andrea Joly", email = "andrea.joly@inria.fr" }
11
+ ]
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+
15
+ ]
16
+
17
+
18
+ [project.optional-dependencies]
19
+ planner = [
20
+ "numpy==2.3.4",
21
+ "pandas==2.3.3",
22
+ "python-dateutil==2.9.0.post0",
23
+ "pytz==2025.2",
24
+ "PyYAML==6.0.3",
25
+ "six==1.17.0",
26
+ "tzdata==2025.2",
27
+ ]
28
+ forge = [
29
+ "sigmf==1.2.12"
30
+ ]
31
+
32
+
33
+ [project.scripts]
34
+ cortexforge-planner = "cortexforge.planner.main:main"
35
+ cortexforge-forge = "cortexforge.forge.main:main"
36
+
37
+ [tool.setuptools]
38
+ package-dir = {"" = "src"}
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,58 @@
1
+ """CLI argument parser for CorteXForge forge."""
2
+
3
+ from argparse import ArgumentParser, Namespace
4
+ from pathlib import Path
5
+
6
+
7
+ def build_parser() -> ArgumentParser:
8
+ """
9
+ Build and configure the cli argument parser for CorteXForge forge.
10
+
11
+ Returns:
12
+ ArgumentParser: Configured argument parser instance with
13
+ """
14
+ parser = ArgumentParser(prog="CorteXForge forge", description="Dataset Generator")
15
+ sub = parser.add_subparsers(
16
+ dest="role",
17
+ required=True,
18
+ help="Radio role for this node (rx=receiver, tx=transmitter)",
19
+ )
20
+
21
+ rx = sub.add_parser("rx", help="Receiver mode")
22
+ rx.add_argument(
23
+ "--frequency", type=int, required=True, help="Receiver center frequency (Hz)"
24
+ )
25
+ rx.add_argument(
26
+ "--sample-rate", type=int, required=True, help="Receiver sample rate (Sps)"
27
+ )
28
+ rx.add_argument("--gain", type=int, required=True, help="Receiver gain (dB)")
29
+ rx.add_argument(
30
+ "--duration", type=int, required=True, help="Capture duration (seconds)"
31
+ )
32
+ rx.add_argument("--timeline", type=Path, required=True, help="Path to timeline CSV")
33
+ rx.add_argument(
34
+ "--output-path",
35
+ type=Path,
36
+ required=True,
37
+ help=(
38
+ "Path to output directory for results "
39
+ "(e.g. /cortexlab/homes/<username>/out)"
40
+ ),
41
+ )
42
+
43
+ tx = sub.add_parser("tx", help="Transmitter")
44
+ tx.add_argument("--timeline", type=Path, required=True, help="Path to timeline CSV")
45
+ tx.add_argument(
46
+ "--record-node", type=str, required=True, help="Select the recorder node"
47
+ )
48
+ tx.add_argument(
49
+ "--frequency", type=int, required=True, help="Transmitter center frequency (Hz)"
50
+ )
51
+ tx.add_argument("--gain", type=int, required=True, help="Transmitter gain (dB)")
52
+ return parser
53
+
54
+
55
+ def parse_args(argv: list[str] | None = None) -> Namespace:
56
+ """Parse command line arguments."""
57
+ parser = build_parser()
58
+ return parser.parse_args(argv)
@@ -0,0 +1,45 @@
1
+ """CLI argument parser for CorteXForge planner."""
2
+
3
+ from argparse import ArgumentParser, Namespace
4
+ from pathlib import Path
5
+
6
+
7
+ def build_parser() -> ArgumentParser:
8
+ """
9
+ Build and configure the cli argument parser for CorteXForge planner.
10
+
11
+ Returns:
12
+ ArgumentParser: Configured argument parser instance with
13
+ """
14
+ parser = ArgumentParser(prog="CorteXForge planner", description="Dataset Generator")
15
+ parser.add_argument(
16
+ "--username", required=True, type=str, help="username on CorteXlab"
17
+ )
18
+ parser.add_argument(
19
+ "--duration", type=int, default=60, help="Experiment duration in seconds"
20
+ )
21
+ parser.add_argument(
22
+ "--rx-frequency", type=int, default=2450000000, help="Receiver frequency"
23
+ )
24
+ parser.add_argument("--rx-gain", type=int, default=10, help="Receiver gain")
25
+ parser.add_argument(
26
+ "--rx-sample-rate", type=int, default=250000, help="Receiver sample-rate"
27
+ )
28
+ parser.add_argument(
29
+ "--overlapping",
30
+ action="store_true",
31
+ help="Allow overlapping signals in timeline",
32
+ )
33
+ parser.add_argument(
34
+ "--nodes-path",
35
+ type=Path,
36
+ default="configs/nodes.yaml",
37
+ help="Path to nodes.yaml file",
38
+ )
39
+ return parser
40
+
41
+
42
+ def parse_args(argv: list[str] | None = None) -> Namespace:
43
+ """Parse command line arguments."""
44
+ parser = build_parser()
45
+ return parser.parse_args(argv)
@@ -0,0 +1,17 @@
1
+ from .api import (
2
+ describe_dataset,
3
+ list_datasets,
4
+ list_datasets_with_versions,
5
+ list_versions,
6
+ load_dataset,
7
+ load_manifest,
8
+ )
9
+
10
+ __all__ = [
11
+ "load_dataset",
12
+ "list_datasets",
13
+ "list_datasets_with_versions",
14
+ "describe_dataset",
15
+ "load_manifest",
16
+ "list_versions",
17
+ ]
@@ -0,0 +1,88 @@
1
+ from pathlib import Path
2
+
3
+ from .local import load_local_dataset
4
+ from .manifest import load_manifest
5
+
6
+
7
+ def load_dataset(
8
+ name: str,
9
+ version: str,
10
+ split: str,
11
+ root: str | Path,
12
+ verify: bool = True,
13
+ ):
14
+ dataset_dir = Path(root) / name / version
15
+ return load_local_dataset(dataset_dir=dataset_dir, split=split, verify=verify)
16
+
17
+
18
+ def describe_dataset(
19
+ name: str,
20
+ version: str,
21
+ root: str | Path,
22
+ ):
23
+ dataset_dir = Path(root) / name / version
24
+ return load_manifest(dataset_dir / "manifest.json")
25
+
26
+
27
+ def list_datasets(root: str | Path) -> list[str]:
28
+ root = Path(root)
29
+
30
+ if not root.exists():
31
+ return []
32
+
33
+ datasets = []
34
+
35
+ for dataset_dir in root.iterdir():
36
+ if not dataset_dir.is_dir():
37
+ continue
38
+
39
+ has_valid_version = any(
40
+ (version_dir / "manifest.json").exists()
41
+ for version_dir in dataset_dir.iterdir()
42
+ if version_dir.is_dir()
43
+ )
44
+
45
+ if has_valid_version:
46
+ datasets.append(dataset_dir.name)
47
+
48
+ return sorted(datasets)
49
+
50
+
51
+ def list_versions(name: str, root: str | Path) -> list[str]:
52
+ root = Path(root)
53
+ dataset_dir = root / name
54
+
55
+ if not dataset_dir.exists() or not dataset_dir.is_dir():
56
+ return []
57
+
58
+ versions = [
59
+ version_dir.name
60
+ for version_dir in dataset_dir.iterdir()
61
+ if version_dir.is_dir() and (version_dir / "manifest.json").exists()
62
+ ]
63
+
64
+ return sorted(versions)
65
+
66
+
67
+ def list_datasets_with_versions(root: str | Path) -> dict[str, list[str]]:
68
+ root = Path(root)
69
+
70
+ if not root.exists():
71
+ return {}
72
+
73
+ result = {}
74
+
75
+ for dataset_dir in root.iterdir():
76
+ if not dataset_dir.is_dir():
77
+ continue
78
+
79
+ versions = [
80
+ version_dir.name
81
+ for version_dir in dataset_dir.iterdir()
82
+ if (version_dir / "manifest.json").exists()
83
+ ]
84
+
85
+ if versions:
86
+ result[dataset_dir.name] = sorted(versions)
87
+
88
+ return result
@@ -0,0 +1,11 @@
1
+ import hashlib
2
+ from pathlib import Path
3
+
4
+
5
+ def _sha512_hex(path: Path, chunk_size: int = 1024 * 1024) -> str:
6
+ """Compute SHA-512 hash of a file and return it as a hex string."""
7
+ h = hashlib.sha512()
8
+ with path.open("rb") as f:
9
+ for chunk in iter(lambda: f.read(chunk_size), b""):
10
+ h.update(chunk)
11
+ return h.hexdigest()
@@ -0,0 +1,87 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from .hash import _sha512_hex
5
+ from .manifest import load_manifest
6
+ from .types import LocalDataset
7
+
8
+
9
+ def validate_sigmf_recording(meta_path: Path) -> None:
10
+ if not meta_path.exists():
11
+ raise FileNotFoundError(f"Missing SigMF metadata: {meta_path}")
12
+
13
+ # Load JSON
14
+ with meta_path.open("r", encoding="utf-8") as f:
15
+ meta = json.load(f)
16
+
17
+ if "global" not in meta:
18
+ raise ValueError(f"Invalid SigMF meta (missing 'global'): {meta_path}")
19
+
20
+ global_meta = meta["global"]
21
+
22
+ # ---- core:data_file ----
23
+ data_file = global_meta.get("core:data_file")
24
+ if not data_file:
25
+ raise ValueError(f"Missing 'core:data_file' in {meta_path}")
26
+
27
+ data_path = meta_path.parent / data_file
28
+
29
+ if not data_path.exists():
30
+ raise FileNotFoundError(
31
+ f"SigMF data file not found: {data_path} (referenced in {meta_path})"
32
+ )
33
+
34
+ # ---- SHA512 ----
35
+ expected_hash = global_meta.get("core:sha512")
36
+
37
+ if expected_hash:
38
+ actual_hash = _sha512_hex(data_path)
39
+
40
+ if actual_hash != expected_hash:
41
+ raise ValueError(
42
+ f"SHA512 mismatch for {data_path}\n"
43
+ f"Expected: {expected_hash}\n"
44
+ f"Got: {actual_hash}"
45
+ )
46
+
47
+
48
+ def _associated_data_path(meta_path: Path) -> Path:
49
+ if meta_path.name.endswith(".sigmf-meta"):
50
+ return meta_path.with_name(meta_path.name.replace(".sigmf-meta", ".sigmf-data"))
51
+ raise ValueError(f"Expected a .sigmf-meta file, got: {meta_path}")
52
+
53
+
54
+ def load_local_dataset(
55
+ dataset_dir: str | Path,
56
+ split: str,
57
+ verify: bool = True,
58
+ ) -> LocalDataset:
59
+ dataset_dir = Path(dataset_dir)
60
+ manifest = load_manifest(dataset_dir / "manifest.json")
61
+
62
+ if split not in manifest.splits:
63
+ raise ValueError(f"Unknown split: {split!r}")
64
+
65
+ recordings = []
66
+ for rel_path in manifest.splits[split].recordings:
67
+ meta_path = dataset_dir / rel_path
68
+
69
+ if verify:
70
+ validate_sigmf_recording(meta_path)
71
+
72
+ if not meta_path.exists():
73
+ raise FileNotFoundError(f"Missing SigMF metadata file: {meta_path}")
74
+
75
+ data_path = _associated_data_path(meta_path)
76
+ if not data_path.exists():
77
+ raise FileNotFoundError(f"Missing SigMF data file: {data_path}")
78
+
79
+ recordings.append(meta_path)
80
+
81
+ return LocalDataset(
82
+ name=manifest.name,
83
+ version=manifest.version,
84
+ split=split,
85
+ root=dataset_dir,
86
+ recordings=recordings,
87
+ )
@@ -0,0 +1,68 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from .types import DatasetManifest, GeneratorManifest, SplitManifest
5
+
6
+ REQUIRED_TOP_LEVEL_FIELDS = {"name", "version", "format", "generator", "splits"}
7
+ REQUIRED_GENERATOR_FIELDS = {"name", "version"}
8
+
9
+
10
+ def validate_manifest(path: str | Path) -> None:
11
+ path = Path(path)
12
+
13
+ if not path.exists():
14
+ raise FileNotFoundError(f"Manifest not found: {path}")
15
+
16
+ with path.open("r", encoding="utf-8") as f:
17
+ raw = json.load(f)
18
+
19
+ missing = REQUIRED_TOP_LEVEL_FIELDS - raw.keys()
20
+ if missing:
21
+ raise ValueError(f"Missing required manifest fields: {sorted(missing)}")
22
+
23
+ generator = raw["generator"]
24
+ if not isinstance(generator, dict):
25
+ raise ValueError("'generator' must be a dictionary")
26
+
27
+ missing_generator = REQUIRED_GENERATOR_FIELDS - generator.keys()
28
+ if missing_generator:
29
+ raise ValueError(
30
+ f"Missing required generator fields: {sorted(missing_generator)}"
31
+ )
32
+
33
+ splits = raw["splits"]
34
+ if not isinstance(splits, dict) or not splits:
35
+ raise ValueError("'splits' must be a non-empty dictionary")
36
+
37
+ for split_name, split_data in splits.items():
38
+ if not isinstance(split_data, dict):
39
+ raise ValueError(f"Split '{split_name}' must be a dictionary")
40
+
41
+ if "recordings" not in split_data:
42
+ raise ValueError(f"Split '{split_name}' is missing 'recordings'")
43
+
44
+ if not isinstance(split_data["recordings"], list):
45
+ raise ValueError(f"Split '{split_name}' field 'recordings' must be a list")
46
+
47
+
48
+ def load_manifest(path: str | Path) -> DatasetManifest:
49
+ path = Path(path)
50
+ validate_manifest(path)
51
+ with path.open("r", encoding="utf-8") as f:
52
+ raw = json.load(f)
53
+
54
+ splits = {
55
+ split_name: SplitManifest(recordings=split_data["recordings"])
56
+ for split_name, split_data in raw["splits"].items()
57
+ }
58
+
59
+ generator = GeneratorManifest(**raw["generator"])
60
+
61
+ return DatasetManifest(
62
+ name=raw["name"],
63
+ version=raw["version"],
64
+ description=raw.get("description", ""),
65
+ format=raw["format"],
66
+ generator=generator,
67
+ splits=splits,
68
+ )
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from typing import Dict, List
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class SplitManifest:
8
+ recordings: List[str]
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class GeneratorManifest:
13
+ name: str
14
+ version: str
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class DatasetManifest:
19
+ name: str
20
+ version: str
21
+ description: str
22
+ format: str
23
+ generator: GeneratorManifest
24
+ splits: Dict[str, SplitManifest]
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class LocalDataset:
29
+ name: str
30
+ version: str
31
+ split: str
32
+ root: Path
33
+ recordings: List[Path]
File without changes
@@ -0,0 +1,25 @@
1
+ from cortexforge.cli.forge import parse_args
2
+ from cortexforge.utils.logger import setup_logger
3
+
4
+ logger = setup_logger()
5
+
6
+
7
+ def main() -> None:
8
+ args = parse_args()
9
+ logger.info("Starting CorteXForge...")
10
+ logger.info(f"Args={args}")
11
+
12
+ if args.role == "rx":
13
+ from cortexforge.forge.radio.rx import main as rx_main
14
+
15
+ rx_main(args)
16
+ elif args.role == "tx":
17
+ from cortexforge.forge.radio.tx import main as tx_main
18
+
19
+ tx_main(args)
20
+ else:
21
+ raise ValueError(f"Unknown role: {args.role}")
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,100 @@
1
+ from logging import getLogger
2
+ from time import sleep
3
+ from datetime import datetime, timezone
4
+ from gnuradio import uhd
5
+
6
+ from cortexforge.forge.radio.rx_recorder import RxRecorder
7
+ from cortexforge.forge.utils.sigmf_writer import write_sigmf
8
+ from cortexforge.forge.utils.compute_baseline import compute_baseline
9
+ from cortexforge.forge.utils.load_timeline import load_timeline
10
+ from cortexforge.forge.utils.sync_barrier.rx_barrier_server import RxBarrierServer
11
+ from cortexforge.forge.utils.sync_barrier.sync_config import SyncConfig
12
+ from cortexforge.forge.utils.uhd_time import arm_time_reset_next_pps
13
+ from cortexforge.forge.utils.sigmf.sigmf_annotations import timeline_to_sigmf_annotations
14
+ from cortexforge.forge.utils.node_identity import get_node_name
15
+
16
+
17
+ logger = getLogger(__name__)
18
+
19
+
20
+ def main(args):
21
+ out_dir = args.output_path
22
+ out_dir.mkdir(parents=True, exist_ok=True)
23
+
24
+ timeline = load_timeline(args.timeline)
25
+
26
+ radios = {str(ev["radio"]) for ev in timeline if ev.get("radio")}
27
+
28
+ logger.debug(f"Radios in timeline: {radios}, expecting {len(radios)} to synchronize")
29
+
30
+ raw_path = out_dir / "temp.cf32"
31
+
32
+ tb = RxRecorder(
33
+ usrp_args="",
34
+ freq=args.frequency,
35
+ rate=args.sample_rate,
36
+ gain=args.gain,
37
+ out_path=str(raw_path),
38
+ )
39
+ cfg = SyncConfig(
40
+ server_host=get_node_name(),
41
+ port_reg=5555,
42
+ port_pub=5556,
43
+ expected_tx=len(radios),
44
+ )
45
+ barrier = RxBarrierServer(cfg)
46
+ barrier.wait_for_all()
47
+
48
+ barrier.broadcast({"type": "GO"})
49
+ arm_time_reset_next_pps(tb.src)
50
+
51
+ capture_start_uhd = 1.0
52
+ if hasattr(tb.src, "set_start_time"):
53
+ tb.src.set_start_time(uhd.time_spec(capture_start_uhd))
54
+ logger.info("RX stream scheduled at UHD t=%.3f s", capture_start_uhd)
55
+ rx_uhd_t0 = capture_start_uhd
56
+ else:
57
+ rx_uhd_t0 = None
58
+ logger.warning("RX source has no set_start_time(); using runtime t0 estimate")
59
+
60
+ tb.start()
61
+ if rx_uhd_t0 is None:
62
+ rx_uhd_t0 = tb.src.get_time_now().get_real_secs()
63
+
64
+ while tb.src.get_time_now().get_real_secs() < rx_uhd_t0 + args.duration:
65
+ sleep(0.001)
66
+
67
+ tb.stop()
68
+ tb.wait()
69
+
70
+ stats = compute_baseline(
71
+ path=str(raw_path),
72
+ sample_rate=args.sample_rate
73
+ )
74
+
75
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
76
+ base_path = out_dir / f"{stamp}"
77
+
78
+
79
+ logger.info(f"Recording stats: {stats}")
80
+ data_path, meta_path = write_sigmf(
81
+ base_path=str(base_path),
82
+ data_file=str(raw_path),
83
+ stat=stats,
84
+ sample_rate=args.sample_rate,
85
+ center_freq=args.frequency,
86
+ hardware=tb.src.get_usrp_info().get('mboard_id'),
87
+ author="Andrea Joly",
88
+ description="CorteXForge recording",
89
+ gain=args.gain,
90
+ annotations = timeline_to_sigmf_annotations(
91
+ events=timeline,
92
+ rx_sample_rate=args.sample_rate,
93
+ rx_uhd_t0=rx_uhd_t0,
94
+ tx_center_freq=args.frequency,
95
+ tx_gain=args.gain,
96
+ rx_data_path=str(raw_path),
97
+ baseline_stat=stats,
98
+ ),
99
+ )
100
+ logger.info(f"SigMF written: {data_path} and {meta_path}")