zfp-oss #27
|
|
@ -297,6 +297,7 @@ class CampaignConfig:
|
||||||
qa: QAConfig = field(default_factory=QAConfig)
|
qa: QAConfig = field(default_factory=QAConfig)
|
||||||
output: OutputConfig = field(default_factory=OutputConfig)
|
output: OutputConfig = field(default_factory=OutputConfig)
|
||||||
mode: str = "controlled_testbed"
|
mode: str = "controlled_testbed"
|
||||||
|
loops: int = 1 # repeat full schedule this many times; labels get _run{N:02d} suffix
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Loaders
|
# Loaders
|
||||||
|
|
@ -324,6 +325,7 @@ class CampaignConfig:
|
||||||
return cls(
|
return cls(
|
||||||
name=safe_name,
|
name=safe_name,
|
||||||
mode=str(campaign_meta.get("mode", "controlled_testbed")),
|
mode=str(campaign_meta.get("mode", "controlled_testbed")),
|
||||||
|
loops=max(1, int(campaign_meta.get("loops", 1))),
|
||||||
recorder=RecorderConfig.from_dict(raw["recorder"]),
|
recorder=RecorderConfig.from_dict(raw["recorder"]),
|
||||||
transmitters=transmitters,
|
transmitters=transmitters,
|
||||||
qa=QAConfig.from_dict(raw.get("qa", {})),
|
qa=QAConfig.from_dict(raw.get("qa", {})),
|
||||||
|
|
@ -388,6 +390,7 @@ class CampaignConfig:
|
||||||
return cls(
|
return cls(
|
||||||
name=safe_name,
|
name=safe_name,
|
||||||
mode=str(campaign_meta.get("mode", "controlled_testbed")),
|
mode=str(campaign_meta.get("mode", "controlled_testbed")),
|
||||||
|
loops=max(1, int(campaign_meta.get("loops", 1))),
|
||||||
recorder=RecorderConfig.from_dict(raw["recorder"]),
|
recorder=RecorderConfig.from_dict(raw["recorder"]),
|
||||||
transmitters=transmitters,
|
transmitters=transmitters,
|
||||||
qa=QAConfig.from_dict(raw.get("qa", {})),
|
qa=QAConfig.from_dict(raw.get("qa", {})),
|
||||||
|
|
@ -490,9 +493,9 @@ class CampaignConfig:
|
||||||
)
|
)
|
||||||
|
|
||||||
def total_capture_time_s(self) -> float:
|
def total_capture_time_s(self) -> float:
|
||||||
"""Sum of all step durations across all transmitters."""
|
"""Sum of all step durations across all transmitters and loops."""
|
||||||
return sum(step.duration for tx in self.transmitters for step in tx.schedule)
|
return sum(step.duration for tx in self.transmitters for step in tx.schedule) * self.loops
|
||||||
|
|
||||||
def total_steps(self) -> int:
|
def total_steps(self) -> int:
|
||||||
"""Total number of capture steps across all transmitters."""
|
"""Total number of capture steps across all transmitters and loops."""
|
||||||
return sum(len(tx.schedule) for tx in self.transmitters)
|
return sum(len(tx.schedule) for tx in self.transmitters) * self.loops
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
|
@ -236,10 +236,12 @@ class CampaignExecutor:
|
||||||
"""
|
"""
|
||||||
result = CampaignResult(campaign_name=self.config.name)
|
result = CampaignResult(campaign_name=self.config.name)
|
||||||
|
|
||||||
|
loops = self.config.loops
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Starting campaign '{self.config.name}': "
|
f"Starting campaign '{self.config.name}': "
|
||||||
f"{self.config.total_steps()} steps, "
|
f"{self.config.total_steps()} steps"
|
||||||
f"~{self.config.total_capture_time_s():.0f}s capture time"
|
+ (f" ({self.config.total_steps() // loops} × {loops} loops)" if loops > 1 else "")
|
||||||
|
+ f", ~{self.config.total_capture_time_s():.0f}s capture time"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._init_sdr()
|
self._init_sdr()
|
||||||
|
|
@ -248,10 +250,14 @@ class CampaignExecutor:
|
||||||
total = self.config.total_steps()
|
total = self.config.total_steps()
|
||||||
step_index = 0
|
step_index = 0
|
||||||
|
|
||||||
|
for loop_idx in range(loops):
|
||||||
|
if loops > 1:
|
||||||
|
logger.info(f"Loop {loop_idx + 1}/{loops}")
|
||||||
for transmitter in self.config.transmitters:
|
for transmitter in self.config.transmitters:
|
||||||
logger.info(f"Transmitter: {transmitter.id} ({len(transmitter.schedule)} steps)")
|
logger.info(f"Transmitter: {transmitter.id} ({len(transmitter.schedule)} steps)")
|
||||||
for step in transmitter.schedule:
|
for step in transmitter.schedule:
|
||||||
step_result = self._execute_step(transmitter, step)
|
looped_step = replace(step, label=f"{step.label}_run{loop_idx + 1:02d}") if loops > 1 else step
|
||||||
|
step_result = self._execute_step(transmitter, looped_step)
|
||||||
result.steps.append(step_result)
|
result.steps.append(step_result)
|
||||||
step_index += 1
|
step_index += 1
|
||||||
|
|
||||||
|
|
@ -259,12 +265,14 @@ class CampaignExecutor:
|
||||||
self.progress_cb(step_index, total, step_result)
|
self.progress_cb(step_index, total, step_result)
|
||||||
|
|
||||||
if step_result.error:
|
if step_result.error:
|
||||||
logger.warning(f"Step '{step.label}' error: {step_result.error}")
|
logger.warning(f"Step '{looped_step.label}' error: {step_result.error}")
|
||||||
elif step_result.qa.flagged:
|
elif step_result.qa.flagged:
|
||||||
logger.warning(f"Step '{step.label}' flagged for review: " + "; ".join(step_result.qa.issues))
|
logger.warning(
|
||||||
|
f"Step '{looped_step.label}' flagged for review: " + "; ".join(step_result.qa.issues)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Step '{step.label}' OK "
|
f"Step '{looped_step.label}' OK "
|
||||||
f"(SNR {step_result.qa.snr_db:.1f} dB, "
|
f"(SNR {step_result.qa.snr_db:.1f} dB, "
|
||||||
f"{step_result.qa.duration_s:.1f}s)"
|
f"{step_result.qa.duration_s:.1f}s)"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ class TxExecutor:
|
||||||
center_freq: float = _parse_hz(agent_cfg.get("center_frequency", 0.0))
|
center_freq: float = _parse_hz(agent_cfg.get("center_frequency", 0.0))
|
||||||
filter_type: str = agent_cfg.get("filter", "rrc").lower()
|
filter_type: str = agent_cfg.get("filter", "rrc").lower()
|
||||||
rolloff: float = float(agent_cfg.get("rolloff", 0.35))
|
rolloff: float = float(agent_cfg.get("rolloff", 0.35))
|
||||||
|
loops: int = max(1, int(self.config.get("loops", 1)))
|
||||||
|
|
||||||
# Upsampling factor: samples_per_symbol, fixed at 8 for SDR compatibility.
|
# Upsampling factor: samples_per_symbol, fixed at 8 for SDR compatibility.
|
||||||
sps = 8
|
sps = 8
|
||||||
|
|
@ -119,10 +120,18 @@ class TxExecutor:
|
||||||
|
|
||||||
self._init_sdr(sample_rate, center_freq)
|
self._init_sdr(sample_rate, center_freq)
|
||||||
try:
|
try:
|
||||||
|
for loop_idx in range(loops):
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
break
|
||||||
|
if loops > 1:
|
||||||
|
logger.info("TX loop %d/%d", loop_idx + 1, loops)
|
||||||
for step in schedule:
|
for step in schedule:
|
||||||
if self.stop_event.is_set():
|
if self.stop_event.is_set():
|
||||||
break
|
break
|
||||||
self._execute_step(step, modulation, sps, symbol_rate, filter_type, rolloff)
|
looped_step = (
|
||||||
|
{**step, "label": f"{step.get('label', 'step')}_run{loop_idx + 1:02d}"} if loops > 1 else step
|
||||||
|
)
|
||||||
|
self._execute_step(looped_step, modulation, sps, symbol_rate, filter_type, rolloff)
|
||||||
finally:
|
finally:
|
||||||
self._close_sdr()
|
self._close_sdr()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user