153 lines
6.0 KiB
Python
153 lines
6.0 KiB
Python
"""Download CSV and validate the nine PRBS identification parameters."""
|
|
|
|
import csv
|
|
import io
|
|
import math
|
|
|
|
|
|
REQUIRED_FIELDS = {
|
|
"q_in_val", "dt", "n_order", "t_c", "levels",
|
|
"dead_area", "xa_full", "V_val", "repeat",
|
|
}
|
|
|
|
|
|
def validate_identification_config(config) -> dict:
|
|
"""Validate a parsed config mapping and normalize numeric values."""
|
|
if not isinstance(config, dict):
|
|
raise ValueError("辨识配置必须是参数映射")
|
|
actual = set(config)
|
|
if actual != REQUIRED_FIELDS:
|
|
missing = sorted(REQUIRED_FIELDS - actual)
|
|
extra = sorted(actual - REQUIRED_FIELDS)
|
|
raise ValueError(f"辨识配置字段错误,缺少={missing},多余={extra}")
|
|
|
|
scalar_fields = {
|
|
"q_in_val", "dt", "t_c", "dead_area", "xa_full", "V_val"
|
|
}
|
|
for field in scalar_fields:
|
|
value = config[field]
|
|
if (isinstance(value, bool) or not isinstance(value, (int, float))
|
|
or not math.isfinite(float(value))):
|
|
raise ValueError(f"辨识参数 {field} 必须是有限数字")
|
|
|
|
for field in ("n_order", "repeat"):
|
|
value = config[field]
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
raise ValueError(f"辨识参数 {field} 必须是整数")
|
|
|
|
levels = config["levels"]
|
|
if not isinstance(levels, list) or len(levels) < 2:
|
|
raise ValueError("levels 必须是至少包含 2 项的数组")
|
|
if len(levels) & (len(levels) - 1):
|
|
raise ValueError("levels 长度必须是 2 的整数次幂")
|
|
normalized_levels = []
|
|
for value in levels:
|
|
if (isinstance(value, bool) or not isinstance(value, (int, float))
|
|
or not math.isfinite(float(value)) or not 0 <= value <= 100):
|
|
raise ValueError("levels 中的开度必须是 0 到 100 的有限数字")
|
|
normalized_levels.append(float(value))
|
|
|
|
if config["q_in_val"] < 0:
|
|
raise ValueError("q_in_val 不能小于 0")
|
|
if config["dt"] <= 0 or config["t_c"] <= 0:
|
|
raise ValueError("dt 和 t_c 必须大于 0")
|
|
if config["t_c"] < config["dt"]:
|
|
raise ValueError("t_c 必须大于等于 dt,确保每个码元至少采样一次")
|
|
if config["n_order"] < 2:
|
|
raise ValueError("n_order 必须大于等于 2")
|
|
if config["repeat"] <= 0:
|
|
raise ValueError("repeat 必须是正整数")
|
|
if config["dead_area"] < 0 or config["xa_full"] <= config["dead_area"]:
|
|
raise ValueError("必须满足 0 <= dead_area < xa_full")
|
|
if config["xa_full"] < 1000:
|
|
raise ValueError("xa_full 不能小于前置行程扫描上限 1000")
|
|
if config["V_val"] <= 0:
|
|
raise ValueError("V_val 必须大于 0")
|
|
|
|
return {
|
|
"q_in_val": float(config["q_in_val"]),
|
|
"dt": float(config["dt"]),
|
|
"n_order": config["n_order"],
|
|
"t_c": float(config["t_c"]),
|
|
"levels": normalized_levels,
|
|
"dead_area": float(config["dead_area"]),
|
|
"xa_full": float(config["xa_full"]),
|
|
"V_val": float(config["V_val"]),
|
|
"repeat": config["repeat"],
|
|
}
|
|
|
|
|
|
def parse_identification_config_csv(csv_text: str) -> dict:
|
|
"""Parse a two-column CSV into the validated identification config.
|
|
|
|
The CSV must use ``parameter,value`` as its header. ``levels`` is one
|
|
quoted comma-separated value, for example ``"10,20,30,40"``.
|
|
"""
|
|
try:
|
|
reader = csv.DictReader(io.StringIO(csv_text))
|
|
fieldnames = [name.strip() for name in (reader.fieldnames or [])]
|
|
if fieldnames != ["parameter", "value"]:
|
|
raise ValueError("CSV 表头必须为 parameter,value")
|
|
|
|
raw = {}
|
|
for row in reader:
|
|
if None in row:
|
|
raise ValueError("CSV 每行只能包含 parameter 和 value 两列")
|
|
parameter = (row.get("parameter") or "").strip()
|
|
value = (row.get("value") or "").strip()
|
|
if not parameter:
|
|
raise ValueError("CSV 存在空参数名")
|
|
if parameter in raw:
|
|
raise ValueError(f"CSV 参数重复: {parameter}")
|
|
raw[parameter] = value
|
|
except csv.Error as exc:
|
|
raise ValueError(f"辨识配置 CSV 格式错误: {exc}") from exc
|
|
|
|
actual = set(raw)
|
|
if actual != REQUIRED_FIELDS:
|
|
missing = sorted(REQUIRED_FIELDS - actual)
|
|
extra = sorted(actual - REQUIRED_FIELDS)
|
|
raise ValueError(f"辨识配置字段错误,缺少={missing},多余={extra}")
|
|
|
|
try:
|
|
levels = [float(value.strip()) for value in raw["levels"].split(",")]
|
|
config = {
|
|
"q_in_val": float(raw["q_in_val"]),
|
|
"dt": float(raw["dt"]),
|
|
"n_order": int(raw["n_order"]),
|
|
"t_c": float(raw["t_c"]),
|
|
"levels": levels,
|
|
"dead_area": float(raw["dead_area"]),
|
|
"xa_full": float(raw["xa_full"]),
|
|
"V_val": float(raw["V_val"]),
|
|
"repeat": int(raw["repeat"]),
|
|
}
|
|
except (TypeError, ValueError) as exc:
|
|
raise ValueError(f"辨识配置 CSV 参数值无效: {exc}") from exc
|
|
return validate_identification_config(config)
|
|
|
|
|
|
def download_identification_config(timeout=20) -> dict:
|
|
"""Download the current customer's CSV config through the cloud server."""
|
|
import requests
|
|
from api import data_record_url, the_folder
|
|
|
|
try:
|
|
response = requests.post(data_record_url, json={
|
|
"type": "getIdentificationConfig",
|
|
"deviceId": the_folder,
|
|
}, 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", "云服务器未返回辨识配置"))
|
|
try:
|
|
config_response = requests.get(result["url"], timeout=timeout)
|
|
config_response.raise_for_status()
|
|
return parse_identification_config_csv(config_response.text)
|
|
except (KeyError, ValueError, requests.RequestException) as exc:
|
|
raise ValueError(f"下载或解析辨识配置 CSV 失败: {exc}") from exc
|