Skip to content

PSU Data Logger

Timed logging of power supply readings

Data logging for power supply measurements.

Provides CSV-based logging of PSU output measurements over time. Useful for automated testing, characterization, and monitoring.

PSUDataLogger

PSUDataLogger(psu: PowerSupply, filepath: str, outputs: Optional[List[int]] = None)

Logs PSU output measurements to CSV file.

Records voltage, current, power, and mode for all outputs at regular intervals.

The CSV header is fixed once at start(), so every row has the same five columns per output regardless of what that output supports. An output the model's manual does not document a reading for (e.g. the SPD3303X's CH3, which has no MEASure form) gets empty fields, not fabricated numbers -- that is different from ERROR, which means a supported reading failed at query time. Check psu.model_capability.output_specs if you need to tell "this column is always blank" from "something went wrong".

Example

psu = PowerSupply('192.168.1.200') psu.connect() logger = PSUDataLogger(psu, "psu_log.csv") logger.start()

... PSU operates ...

logger.log_measurement() # Manual logging logger.stop()

Initialize data logger.

Parameters:

Name Type Description Default
psu PowerSupply

PowerSupply instance to log

required
filepath str

Path to CSV log file

required
outputs Optional[List[int]]

List of output numbers to log (None = all outputs)

None
Source code in scpi_control/psu_data_logger.py
def __init__(
    self,
    psu: "PowerSupply",
    filepath: str,
    outputs: Optional[List[int]] = None,
):
    """Initialize data logger.

    Args:
        psu: PowerSupply instance to log
        filepath: Path to CSV log file
        outputs: List of output numbers to log (None = all outputs)
    """
    self.psu = psu
    self.filepath = Path(filepath)
    self.outputs = outputs
    self._file = None
    self._writer = None
    self._is_logging = False

    # Validate outputs
    if outputs is not None:
        for output_num in outputs:
            if not hasattr(self.psu, f"output{output_num}"):
                raise ValueError(f"PSU does not have output{output_num}")

is_logging property

is_logging: bool

Check if logger is currently active.

Returns:

Type Description
bool

True if logging, False otherwise

start

start() -> None

Start logging (open file and write header).

Creates CSV file with timestamp and measurement columns.

Source code in scpi_control/psu_data_logger.py
def start(self) -> None:
    """Start logging (open file and write header).

    Creates CSV file with timestamp and measurement columns.
    """
    if self._is_logging:
        logger.warning("Logger already started")
        return

    # Create directory if needed
    self.filepath.parent.mkdir(parents=True, exist_ok=True)

    # Open CSV file
    self._file = open(self.filepath, "w", newline="")
    self._writer = csv.writer(self._file)

    # Write header
    header = ["timestamp"]
    outputs_to_log = self._get_outputs_to_log()

    for output_num in outputs_to_log:
        header.extend(
            [
                f"output{output_num}_voltage_V",
                f"output{output_num}_current_A",
                f"output{output_num}_power_W",
                f"output{output_num}_mode",
                f"output{output_num}_enabled",
            ]
        )

    self._writer.writerow(header)
    self._file.flush()

    self._is_logging = True
    logger.info(f"Started logging to {self.filepath}")

log_measurement

log_measurement() -> None

Log a single measurement from all configured outputs.

Writes timestamp and current measurements to CSV.

Raises:

Type Description
RuntimeError

If logger not started

Source code in scpi_control/psu_data_logger.py
def log_measurement(self) -> None:
    """Log a single measurement from all configured outputs.

    Writes timestamp and current measurements to CSV.

    Raises:
        RuntimeError: If logger not started
    """
    if not self._is_logging:
        raise RuntimeError("Logger not started. Call start() first.")

    timestamp = datetime.now().isoformat()
    row = [timestamp]

    outputs_to_log = self._get_outputs_to_log()

    for output_num in outputs_to_log:
        output = getattr(self.psu, f"output{output_num}")
        spec = self.psu.model_capability.output_specs[output_num - 1]

        if spec.measurable:
            try:
                voltage = output.measure_voltage()
                current = output.measure_current()
                power = output.measure_power()
                mode = output.get_mode()
                fields = [f"{voltage:.6f}", f"{current:.6f}", f"{power:.6f}", mode]
            except Exception as e:
                logger.error(f"Failed to measure output {output_num}: {e}")
                # Write placeholder values on error -- a real failure to
                # query a supported output, distinct from a documented gap.
                fields = ["ERROR", "ERROR", "ERROR", "ERROR"]
        else:
            # Not documented for this output (e.g. SPD3303X CH3) -- leave
            # the columns blank rather than fabricate or claim an error.
            fields = ["", "", "", ""]

        if spec.state_readable:
            try:
                fields.append(str(output.enabled))
            except Exception as e:
                logger.error(f"Failed to read output {output_num} enabled state: {e}")
                fields.append("ERROR")
        else:
            fields.append("")

        row.extend(fields)

    self._writer.writerow(row)
    self._file.flush()

stop

stop() -> None

Stop logging and close file.

Source code in scpi_control/psu_data_logger.py
def stop(self) -> None:
    """Stop logging and close file."""
    if not self._is_logging:
        logger.warning("Logger not running")
        return

    if self._file:
        self._file.close()
        self._file = None
        self._writer = None

    self._is_logging = False
    logger.info(f"Stopped logging. Data saved to {self.filepath}")

TimedPSULogger

TimedPSULogger(psu: PowerSupply, filepath: str, interval: float = 1.0, outputs: Optional[List[int]] = None)

Timed data logger with automatic periodic measurements.

Uses a background thread to log measurements at regular intervals.

Example

psu = PowerSupply('192.168.1.200') psu.connect() with TimedPSULogger(psu, "psu_log.csv", interval=1.0) as logger: ... time.sleep(10) # Log for 10 seconds

Initialize timed logger.

Parameters:

Name Type Description Default
psu PowerSupply

PowerSupply instance to log

required
filepath str

Path to CSV log file

required
interval float

Logging interval in seconds (default: 1.0)

1.0
outputs Optional[List[int]]

List of output numbers to log (None = all outputs)

None
Source code in scpi_control/psu_data_logger.py
def __init__(
    self,
    psu: "PowerSupply",
    filepath: str,
    interval: float = 1.0,
    outputs: Optional[List[int]] = None,
):
    """Initialize timed logger.

    Args:
        psu: PowerSupply instance to log
        filepath: Path to CSV log file
        interval: Logging interval in seconds (default: 1.0)
        outputs: List of output numbers to log (None = all outputs)
    """
    self.psu = psu
    self.interval = interval
    self.logger = PSUDataLogger(psu, filepath, outputs)
    self._timer = None
    self._running = False

is_logging property

is_logging: bool

Check if logger is currently active.

Returns:

Type Description
bool

True if logging, False otherwise

start

start() -> None

Start timed logging.

Source code in scpi_control/psu_data_logger.py
def start(self) -> None:
    """Start timed logging."""
    if self._running:
        logger.warning("Timed logger already running")
        return

    self.logger.start()
    self._running = True
    self._schedule_next_log()
    logger.info(f"Started timed logging (interval={self.interval}s)")

stop

stop() -> None

Stop timed logging.

Source code in scpi_control/psu_data_logger.py
def stop(self) -> None:
    """Stop timed logging."""
    if not self._running:
        logger.warning("Timed logger not running")
        return

    self._running = False

    if self._timer:
        self._timer.cancel()
        self._timer = None

    self.logger.stop()
    logger.info("Stopped timed logging")