"""Load, download, and validate the volume-measurement configuration.""" import json import math import os from pathlib import Path import sys REQUIRED_FIELDS = { "q_in_val", "dt", "p_max", "fit_low", "fit_high", "T_delta", "xa_full", "num_runs", } def default_config_path() -> Path: """Return the config path without exposing a file picker in the GUI.""" override = os.environ.get("REINLOOP_VOLUME_CONFIG") if override: return Path(override).expanduser().resolve() base_dir = (Path(sys.executable).resolve().parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent.parent) return base_dir / "config" / "volume_measurement.json" def load_volume_config(path=None) -> dict: """Read exactly eight validated parameters from a JSON object.""" config_path = Path(path).resolve() if path else default_config_path() try: with config_path.open("r", encoding="utf-8") as file_obj: config = json.load(file_obj) except FileNotFoundError as exc: raise ValueError(f"容积测试配置文件不存在: {config_path}") from exc except json.JSONDecodeError as exc: raise ValueError(f"容积测试配置不是有效 JSON: {exc}") from exc except OSError as exc: raise ValueError(f"无法读取容积测试配置: {exc}") from exc return validate_volume_config(config) def validate_volume_config(config) -> dict: """Validate exactly eight parameters and normalize numeric values.""" if not isinstance(config, dict): raise ValueError("容积测试配置必须是 JSON 对象") actual_fields = set(config) if actual_fields != REQUIRED_FIELDS: missing = sorted(REQUIRED_FIELDS - actual_fields) extra = sorted(actual_fields - REQUIRED_FIELDS) raise ValueError(f"配置字段错误,缺少={missing},多余={extra}") for field in REQUIRED_FIELDS - {"num_runs"}: value = config[field] if (isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value))): raise ValueError(f"参数 {field} 必须是有限数字") runs = config["num_runs"] if isinstance(runs, bool) or not isinstance(runs, int) or runs <= 0: raise ValueError("参数 num_runs 必须是正整数") if config["q_in_val"] <= 0: raise ValueError("q_in_val 必须大于 0") if config["dt"] <= 0 or config["p_max"] <= 0: raise ValueError("dt 和 p_max 必须大于 0") if config["fit_low"] < 0 or config["fit_high"] <= config["fit_low"]: raise ValueError("必须满足 0 <= fit_low < fit_high") if config["fit_high"] > config["p_max"]: raise ValueError("fit_high 不能大于 p_max") if config["xa_full"] <= 0: raise ValueError("xa_full 必须大于 0") return { "q_in_val": float(config["q_in_val"]), "dt": float(config["dt"]), "p_max": float(config["p_max"]), "fit_low": float(config["fit_low"]), "fit_high": float(config["fit_high"]), "T_delta": float(config["T_delta"]), "xa_full": float(config["xa_full"]), "num_runs": runs, } def _post_volume_request(payload, timeout=10): import requests from api import data_record_url try: response = requests.post(data_record_url, json=payload, timeout=timeout) response.raise_for_status() result = response.json() except Exception as exc: raise ValueError(f"连接云端容积参数服务失败: {exc}") from exc if not result.get("success"): raise ValueError(result.get("errMsg", "云端拒绝容积参数请求")) return result def create_volume_config_request(timeout=10) -> dict: """Create exactly one cloud request after the customer clicks Test.""" from api import the_folder result = _post_volume_request({ "type": "createVolumeConfigRequest", "deviceId": the_folder, }, timeout=timeout) if not result.get("requestId") or not result.get("expiresAtMs"): raise ValueError("云端未返回有效的容积参数请求编号") return { "request_id": result["requestId"], "expires_at_ms": int(result["expiresAtMs"]), } def poll_volume_config_request(request_id: str, timeout=10) -> dict: """Poll one request; download and validate JSON only when it is ready.""" import requests from api import the_folder result = _post_volume_request({ "type": "getVolumeConfigRequest", "deviceId": the_folder, "requestId": request_id, }, timeout=timeout) if result.get("expired"): return {"ready": False, "expired": True} if not result.get("ready"): return {"ready": False, "expired": False} try: response = requests.get(result["url"], timeout=timeout) response.raise_for_status() config = response.json() except Exception as exc: raise ValueError(f"下载或解析容积参数 JSON 失败: {exc}") from exc return { "ready": True, "expired": False, "config": validate_volume_config(config), } def acknowledge_volume_config_request(request_id: str, timeout=10) -> None: """Delete the consumed/abandoned request and its temporary JSON file.""" from api import the_folder _post_volume_request({ "type": "ackVolumeConfigRequest", "deviceId": the_folder, "requestId": request_id, }, timeout=timeout)