"""Validation helpers for controller inputs.""" import math import re from typing import Any from .constants import ( TEMP_MIN_C, TEMP_MAX_C, CURRENT_MIN_MA, CURRENT_MAX_MA, PROFILE_NAME_ALLOWED_PATTERN, PROFILE_NAME_MAX_LENGTH, ) from .exceptions import ( ValidationError, TemperatureOutOfRangeError, CurrentOutOfRangeError, InvalidParameterError, ) class ParameterValidator: """Validates all input parameters for the laser controller.""" @staticmethod def _check_numeric(value: Any, param_name: str) -> float: """Check that value is a valid finite number. Returns float.""" if value is None: raise InvalidParameterError(param_name, "Value must not be None") if not isinstance(value, (int, float)): raise InvalidParameterError(param_name, "Value must be a number") if math.isnan(value): raise InvalidParameterError(param_name, "Value must not be NaN") if math.isinf(value): raise InvalidParameterError(param_name, "Value must not be infinite") return float(value) @staticmethod def validate_temperature(value: Any, param_name: str) -> float: """ Validate a laser temperature value. Args: value: Temperature in °C. param_name: Parameter name for error messages. Returns: Validated temperature as float. Raises: InvalidParameterError: If value is not a valid number. TemperatureOutOfRangeError: If value is outside [TEMP_MIN_C, TEMP_MAX_C]. """ value = ParameterValidator._check_numeric(value, param_name) if value < TEMP_MIN_C or value > TEMP_MAX_C: raise TemperatureOutOfRangeError( param_name, value, TEMP_MIN_C, TEMP_MAX_C ) return value @staticmethod def validate_current(value: Any, param_name: str) -> float: """ Validate a laser drive current value. Args: value: Current in mA. param_name: Parameter name for error messages. Returns: Validated current as float. Raises: InvalidParameterError: If value is not a valid number. CurrentOutOfRangeError: If value is outside [CURRENT_MIN_MA, CURRENT_MAX_MA]. """ value = ParameterValidator._check_numeric(value, param_name) if value < CURRENT_MIN_MA or value > CURRENT_MAX_MA: raise CurrentOutOfRangeError( param_name, value, CURRENT_MIN_MA, CURRENT_MAX_MA ) return value @staticmethod def validate_manual_mode_params( temp1: Any, temp2: Any, current1: Any, current2: Any, ) -> dict[str, float]: """ Validate all four manual mode parameters. Args: temp1: Laser 1 temperature, °C. temp2: Laser 2 temperature, °C. current1: Laser 1 current, mA. current2: Laser 2 current, mA. Returns: Dict with validated floats: temp1, temp2, current1, current2. Raises: ValidationError: For any out-of-range value. InvalidParameterError: For wrong types. """ return { 'temp1': ParameterValidator.validate_temperature(temp1, 'temp1'), 'temp2': ParameterValidator.validate_temperature(temp2, 'temp2'), 'current1': ParameterValidator.validate_current(current1, 'current1'), 'current2': ParameterValidator.validate_current(current2, 'current2'), } @staticmethod def validate_profile_name(value: Any) -> str: """Validate a short ASCII profile name suitable for the device LCD.""" if not isinstance(value, str): raise InvalidParameterError("profile_name", "Value must be a string") normalized = value.strip() if not normalized: raise InvalidParameterError("profile_name", "Value must not be empty") if len(normalized) > PROFILE_NAME_MAX_LENGTH: raise InvalidParameterError( "profile_name", f"Value must be at most {PROFILE_NAME_MAX_LENGTH} characters long", ) if re.fullmatch(PROFILE_NAME_ALLOWED_PATTERN, normalized) is None: raise InvalidParameterError( "profile_name", "Only ASCII letters, digits, spaces, '-' and '_' are allowed", ) return normalized