"""数据驱动的状态/操作面板构建器。 由 BusSpec / CmdSpec 声明式描述每个母线的字段、显示方式与控制项, build_bus_page / update_bus_page 负责创建与刷新界面, 避免每个母线重复手写布局与 if/elif 更新逻辑。 """ import tkinter as tk from dataclasses import dataclass from tkinter import ttk from . import protocol from .protocol import BREAKER_ON, BREAKER_OFF, BREAKER_FAULT # 配色 GREEN = "#2e7d32" RED = "#d32f2f" BLACK = "#212121" GRAY = "#9e9e9e" def breaker_color(raw): if raw == BREAKER_ON: return GREEN if raw == BREAKER_OFF: return BLACK if raw == BREAKER_FAULT: return RED return GRAY def bool_ok(raw, ok=BREAKER_ON): return raw == ok def make_light(parent, size=18): canvas = tk.Canvas(parent, width=size, height=size, bg="white", highlightthickness=0) canvas.create_oval(2, 2, size - 2, size - 2, fill=GRAY, outline="black") return canvas def set_light(canvas, color): canvas.delete("all") canvas.create_oval(2, 2, int(canvas["width"]) - 2, int(canvas["height"]) - 2, fill=color, outline="black") # ------------------------------------------------------------------ @dataclass class BusSpec: key: str # 总线标识 title: str # 页标题 msg_cls: type breakers: list # [(label, field)] voltages: list # [(label, field, scale, unit)] currents: list # [(label, field, scale, unit)] booleans: list # [(label, field)] ok=BREAKER_ON 为正常 derived: list # [(label, getter)] getter(msg)->(text, color) HIGH_VOL_BUS = BusSpec( key="high_vol", title="动力母线状态", msg_cls=protocol.MsgDisHighVolBusFb, breakers=[ ("燃料电池断路器", "fuelCellCircuitBreaker"), ("动力锂电池断路器", "powerLithiumBatteryCircuitBreaker"), ("推进电机断路器", "propulsionMotorCircuitBreaker"), ("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"), ("DC/DC模块断路器", "dcDc5ModuleCircuitBreaker"), ("艏高压配电盒断路器", "bowHighVoltageDistributionBoxCircuitBreaker"), ("艉FT设备45断路器", "sternFTDevice45CircuitBreaker"), ("艉舵机开关1断路器", "sternRudderSwitch1CircuitBreaker"), ("艉舵机开关2断路器", "sternRudderSwitch2CircuitBreaker"), ("FB备用断路器", "fbReservedCircuitBreaker"), ("TYZ备用断路器", "tyzReservedCircuitBreaker"), ("备用断路器", "reservedCircuitBreaker"), ], voltages=[ ("动力母线电压", "powerBusVoltage", 0.1, "V"), ("A母线电压", "busbarAVoltage", 0.1, "V"), ], currents=[ ("动力母线电流", "powerBusCurrent", 0.1, "A"), ("A汇流排电流", "busbarACurrent", 0.1, "A"), ("DC/DC5模块电流", "dcDc5ModuleCurrent", 0.1, "A"), ("推进电机控制箱电流", "propulsionMotorControlBoxCurrent", 0.1, "A"), ("锂电池组(仪表)电流", "lithiumBatteryGroupMeterCurrent", 0.1, "A"), ("艏部高压配电箱电流", "bowHighVoltageDistributionBoxCurrent", 0.1, "A"), ("艉部FT装置4/5电流", "sternFTDevice45Current", 0.1, "A"), ("TYZ预留开关电流", "tyzReservedSwitchCurrent", 0.1, "A"), ("艉舵开关1电流", "sternRudderSwitch1Current", 0.1, "A"), ("艉舵开关2电流", "sternRudderSwitch2Current", 0.1, "A"), ("FB预留开关电流", "fbReservedSwitchCurrent", 0.1, "A"), ("备用电流1", "reservedCurrent1", 0.1, "A"), ("备用电流2", "reservedCurrent2", 0.1, "A"), ("备用电流3", "reservedCurrent3", 0.1, "A"), ], booleans=[ ("仪表失电信号", "meterPowerLossSignal"), ("应急失电信号", "emergencyPowerLossSignal"), ("动力母线绝缘", "powerBusInsulationStatus"), ("艏高压绝缘", "aBusInsulationStatus"), ], derived=[ ("冷却水压力", lambda m: (f"{m.coolWaterPressure * 0.1:.1f} kPa", BLACK)), ("DC/DC温度", lambda m: (f"{m.dcdcTemperature - 40} ℃", BLACK)), ], ) HIGH_A_VOL_BUS = BusSpec( key="high_a_vol", title="艏部高压状态", msg_cls=protocol.MsgDisHighAVolBusFb, breakers=[ ("艏FT设备123断路器", "bowFTDevice123CircuitBreaker"), ("执行机构断路器", "actuatorCircuitBreaker"), ("桅舵机控制箱断路器", "mastSteeringGearControlBoxCircuitBreaker"), ("XCZ断路器", "xczCircuitBreaker"), ("艏舵控制箱断路器", "bowRudderControlBoxCircuitBreaker"), ("开盖启动气缸断路器", "openWaterCoverStartCylinderCircuitBreaker"), ], voltages=[ ("汇流排B电压", "busbarBVoltage", 0.1, "V"), ], currents=[ ("汇流排B电流", "busbarBCurrent", 0.1, "A"), ("艏部FT设备123电流", "bowFTDevice123Current", 0.1, "A"), ("启闭机构电流", "actuatorCurrent", 0.1, "A"), ("桅杆舵机控制箱电流", "mastSteeringGearControlBoxCurrent", 0.1, "A"), ("XCZ电流", "xczCurrent", 0.1, "A"), ("艏舵控制箱电流", "bowRudderControlBoxCurrent", 0.1, "A"), ("敞水盖启动电缸电流", "openWaterCoverStartCylinderCurrent", 0.1, "A"), ], booleans=[ ("仪表电源失电信号", "instrumentPowerFailureSignal"), ("应急电源失电信号", "emergencyPowerFailureSignal"), ("进水报警", "waterIngressionAlarm"), ], derived=[], ) LOW_MAIN_BUS = BusSpec( key="low_main", title="仪表母线状态", msg_cls=protocol.MsgDisLowMainBusFb, breakers=[ ("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"), ("艏低压配电盒断路器", "bowLowVoltageDistributionBoxCircuitBreaker"), ("单元4仪表48V断路器", "unit4InstrumentDC48VCircuitBreaker"), ("DC/C1直流配电板断路器", "dcC1DCDistributionPanelCircuitBreaker"), ("备用断路器1", "reservedCircuitBreaker1"), ("备用断路器2", "reservedCircuitBreaker2"), ("应急锂电池组2断路器", "emergencyLithiumBatteryGroup2CircuitBreaker"), ], voltages=[ ("仪表母线电压", "instrumentBusbarVoltage", 0.1, "V"), ("应急2汇流排电压", "emergency2BusbarVoltage", 0.1, "V"), ], currents=[ ("艏部低压配电箱电流", "bowLowVoltageDistributionBoxCurrent", 0.1, "A"), ("锂电池组(仪表)电流", "lithiumBatteryGroupInstrumentCurrent", 0.1, "A"), ("DC-C1仪表DC48V电流", "dcC1InstrumentDC48VCurrent", 0.1, "A"), ("应急锂电池组2电流", "emergencyLithiumBatteryGroup2Current", 0.1, "A"), ("单元4仪表48V电流", "unit4InstrumentDC48VCurrent", 0.1, "A"), ("复合能源管控应急DC48V电流", "compositeEnergyManagementSystemEmergencyDC48VCurrent", 0.1, "A"), ("燃料电池安保应急DC48V电流", "fuelCellSecuritySystemEmergencyDC48VCurrent", 0.1, "A"), ("PLC控制电源电流", "plcControlPowerCurrent", 0.1, "A"), ("备用电流1", "reservedCurrent1", 0.1, "A"), ("备用电流2", "reservedCurrent2", 0.1, "A"), ], booleans=[ ("仪表电源失电信号", "instrumentPowerFailureSignal"), ("应急电源失电信号", "emergencyPowerFailureSignal"), ("仪表汇流排绝缘低", "instrumentBusbarInsulationLow"), ], derived=[], ) LOW_BUS = BusSpec( key="low_bus", title="艏部低压状态", msg_cls=protocol.MsgDisLowBusFb, breakers=[ ("单元1断路器", "unit1CircuitBreaker"), ("单元2断路器", "unit2CircuitBreaker"), ("单元3断路器", "unit3CircuitBreaker"), ("单元5断路器", "unit5CircuitBreaker"), ("艏PZ设备断路器", "bowPZDeviceCircuitBreaker"), ("备用断路器1", "reservedCircuitBreaker1"), ("备用断路器2", "reservedCircuitBreaker2"), ("应急锂电池组1断路器", "emergencyLithiumBatteryGroup1CircuitBreaker"), ], voltages=[ ("仪表母排电压", "instrumentBusbarVoltage", 0.1, "V"), ("应急1母线电压", "emergency1BusbarVoltage", 0.1, "V"), ], currents=[ ("仪表母排电流", "instrumentBusbarCurrent", 0.1, "A"), ("单元1电流", "unit1Current", 0.1, "A"), ("单元2电流", "unit2Current", 0.1, "A"), ("单元3电流", "unit3Current", 0.1, "A"), ("单元5电流", "unit5Current", 0.1, "A"), ("艏PZ设备电流", "bowPZDeviceCurrent", 0.1, "A"), ("应急锂电池组1电流", "emergencyLithiumBatteryGroup1Current", 0.1, "A"), ], booleans=[ ("仪表电失电信号", "instrumentPowerFailureSignal"), ("应急电失电信号", "emergencyPowerFailureSignal"), ("浸水报警", "waterIngressionAlarm"), ], derived=[], ) BUS_SPECS = {spec.key: spec for spec in (HIGH_VOL_BUS, HIGH_A_VOL_BUS, LOW_MAIN_BUS, LOW_BUS)} # ------------------------------------------------------------------ # 状态页构建 / 刷新 # ------------------------------------------------------------------ def build_bus_page(parent, spec): """构建一个母线状态页,返回用于刷新的 refs 字典。""" page = ttk.Frame(parent, padding=6) page.grid_columnconfigure(0, weight=1) # ---- 顶部:电压 + 断路器 ---- top = ttk.LabelFrame(page, text="电压与断路器状态", padding=8) top.grid(row=0, column=0, sticky="nsew", pady=(0, 6)) for c in range(3): top.grid_columnconfigure(c, weight=1) voltage_widgets = {} for i, (label, field, scale, unit) in enumerate(spec.voltages): sub = ttk.Frame(top) sub.grid(row=0, column=i, padx=10, pady=4, sticky="w") ttk.Label(sub, text=f"{label}({unit}):", width=20, anchor="w").pack(side="left") value_lbl = ttk.Label(sub, text="--", width=10, anchor="e", relief="groove") value_lbl.pack(side="right") voltage_widgets[field] = {"widget": value_lbl, "scale": scale, "unit": unit} ttk.Label(top, text="断路器状态:", font=("Arial", 10, "bold")).grid(row=1, column=0, columnspan=6, sticky="w", pady=(10, 4)) breaker_lights = {} field_for_label = {label: f for label, f in spec.breakers} for i, (label, _f) in enumerate(spec.breakers): bf = tk.Frame(top, relief="solid", borderwidth=1, padx=6, pady=2) bf.grid(row=2 + i // 3, column=i % 3, padx=6, pady=4, sticky="w") ttk.Label(bf, text=label, width=20, anchor="w").pack(side="left") light = make_light(bf) light.pack(side="right") breaker_lights[label] = light # ---- 下部:电流 / 布尔量 / 派生量 ---- group = ttk.LabelFrame(page, text="系统状态", padding=8) group.grid(row=1, column=0, sticky="nsew") page.grid_rowconfigure(1, weight=1) for c in range(3): group.grid_columnconfigure(c, weight=1) values = {} booleans = {} derived = {} items = [] for it in spec.currents: items.append(("value", it)) for it in spec.booleans: items.append(("bool", it)) for it in spec.derived: items.append(("derived", it)) for i, (kind, it) in enumerate(items): f = ttk.Frame(group) f.grid(row=i // 3, column=i % 3, sticky="w", padx=8, pady=3) if kind == "value": label, field, scale, unit = it ttk.Label(f, text=f"{label}({unit}):", width=26, anchor="w").pack(side="left") v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove") v.pack(side="right") values[field] = {"widget": v, "scale": scale, "unit": unit} elif kind == "bool": label, field = it ttk.Label(f, text=f"{label}:", width=26, anchor="w").pack(side="left") v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove") v.pack(side="right") booleans[field] = {"widget": v} else: label, getter = it ttk.Label(f, text=f"{label}:", width=26, anchor="w").pack(side="left") v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove") v.pack(side="right") derived[label] = {"widget": v, "getter": getter} return { "frame": page, "field_for_label": field_for_label, "breaker_lights": breaker_lights, "voltage_widgets": voltage_widgets, "values": values, "booleans": booleans, "derived": derived, } def update_bus_page(spec, refs, msg): """用最新反馈消息刷新状态页。msg 为 None 时跳过。""" if msg is None: return for label, light in refs["breaker_lights"].items(): field = refs["field_for_label"][label] set_light(light, breaker_color(getattr(msg, field))) for field, info in refs["voltage_widgets"].items(): v = getattr(msg, field) * info["scale"] info["widget"].config(text=f"{v:.1f} {info['unit']}") for field, info in refs["values"].items(): v = getattr(msg, field) * info["scale"] info["widget"].config(text=f"{v:.1f} {info['unit']}") for field, info in refs["booleans"].items(): ok = bool_ok(getattr(msg, field)) info["widget"].config(text="正常" if ok else "故障", foreground=GREEN if ok else RED) for label, info in refs["derived"].items(): text, color = info["getter"](msg) info["widget"].config(text=text, foreground=color) # ------------------------------------------------------------------ # 控制面板 # ------------------------------------------------------------------ @dataclass class CmdSpec: key: str title: str msg_cls: type dest: str # 'c1' / 'c2' actions: list # [(label, field)] HVB_CMD = CmdSpec("hvb", "动力母线配电指令", protocol.MsgDisHVBCmd, "c1", [ ("燃料电池断路器", "fuelCellCircuitBreaker"), ("动力锂电池断路器", "powerLithiumBatteryCircuitBreaker"), ("推进电机断路器", "propulsionMotorCircuitBreaker"), ("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"), ("DC/DC模块断路器", "dcDc5ModuleCircuitBreaker"), ("艏高压配电盒断路器", "bowHighVoltageDistributionBoxCircuitBreaker"), ("艉FT设备45断路器", "sternFTDevice45CircuitBreaker"), ("艉舵机开关1断路器", "sternRudderSwitch1CircuitBreaker"), ("艉舵机开关2断路器", "sternRudderSwitch2CircuitBreaker"), ("FB备用断路器", "fbReservedCircuitBreaker"), ("TYZ备用断路器", "tyzReservedCircuitBreaker"), ("备用断路器", "reservedCircuitBreaker"), ("DC/DC模块启动", "dcdc5Module"), ("冷却系统启动", "coolingSystem"), ]) HVBA_CMD = CmdSpec("hvba", "艏部高压配电指令", protocol.MsgDisHVBACmd, "c1", [ ("艏FT设备123断路器", "bowFTDevice123CircuitBreaker"), ("执行机构断路器", "actuatorCircuitBreaker"), ("桅舵机控制箱断路器", "mastSteeringGearControlBoxCircuitBreaker"), ("XCZ断路器", "xczCircuitBreaker"), ("艏舵控制箱断路器", "bowRudderControlBoxCircuitBreaker"), ("开盖启动气缸断路器", "openWaterCoverStartCylinderCircuitBreaker"), ]) LVMB_CMD = CmdSpec("lvmb", "仪表母线配电指令", protocol.MsgDisLVMBCmd, "c2", [ ("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"), ("艏低压配电盒断路器", "bowLowVoltageDistributionBoxCircuitBreaker"), ("单元4仪表48V断路器", "unit4InstrumentDC48VCircuitBreaker"), ("DC/C1直流配电板断路器", "dcC1DCDistributionPanelCircuitBreaker"), ("备用断路器1", "reservedCircuitBreaker1"), ("备用断路器2", "reservedCircuitBreaker2"), ("应急锂电池组2断路器", "emergencyLithiumBatteryGroup2CircuitBreaker"), ]) LVB_CMD = CmdSpec("lvb", "艏部低压配电指令", protocol.MsgDisLVBCmd, "c2", [ ("单元1断路器", "unit1CircuitBreaker"), ("单元2断路器", "unit2CircuitBreaker"), ("单元3断路器", "unit3CircuitBreaker"), ("单元5断路器", "unit5CircuitBreaker"), ("艏PZ设备断路器", "bowPZDeviceCircuitBreaker"), ("备用断路器1", "reservedCircuitBreaker1"), ("备用断路器2", "reservedCircuitBreaker2"), ("应急锂电池组1断路器", "emergencyLithiumBatteryGroup1CircuitBreaker"), ]) CMD_SPECS = {spec.key: spec for spec in (HVB_CMD, HVBA_CMD, LVMB_CMD, LVB_CMD)} def build_control_panel(parent, spec, on_cmd): """构建控制面板。on_cmd(spec, field, state)。""" frame = ttk.LabelFrame(parent, text=spec.title, padding=10) frame.pack(fill="x", pady=5) for i, (label, field) in enumerate(spec.actions): row, col = i // 3, i % 3 bf = ttk.Frame(frame) bf.grid(row=row, column=col, padx=10, pady=2, sticky="w") ttk.Label(bf, text=label, width=20).pack(side="left") ttk.Button(bf, text="合", width=3, command=lambda f=field: on_cmd(spec, f, BREAKER_ON)).pack(side="left", padx=2) ttk.Button(bf, text="分", width=3, command=lambda f=field: on_cmd(spec, f, BREAKER_OFF)).pack(side="left", padx=2) return frame # ------------------------------------------------------------------ # DC/DC 故障字显示(供状态页与独立弹窗复用) # ------------------------------------------------------------------ FAULT1_LABELS = ["过温关机", "输出过流", "输出过压", "输出欠压", "输入过压", "输入欠压", "输出短路保护", "内部故障"] FAULT2_LABELS = ["通信故障", "工作状态", "冷却泵1故障", "冷却泵2故障", "冷却水泄漏", "冷却泵1运行状态", "冷却泵2运行状态", "保留"] def build_dcdc_display(parent, title="DC/DC 故障字"): """构建 DC/DC 故障字灯组,返回 refs={"words": {1: [canvas...], 2: [...]}}。""" frame = ttk.LabelFrame(parent, text=title, padding=8) frame.grid_columnconfigure(0, weight=1) words = {1: [], 2: []} for wi, labels in ((1, FAULT1_LABELS), (2, FAULT2_LABELS)): ttk.Label(frame, text=f"DC/DC故障字{wi}:", font=("Microsoft YaHei", 9, "bold")).grid( row=(wi - 1) * 2, column=0, sticky="w", pady=(6, 2)) for i, label in enumerate(labels): pf = tk.Frame(frame, borderwidth=1, relief="solid", padx=4, pady=2) pf.grid(row=(wi - 1) * 2 + 1 + i // 4, column=i % 4, padx=5, pady=3, sticky="w") ttk.Label(pf, text=label).pack(side="left", padx=(0, 6)) light = make_light(pf, 18) light.pack(side="right") words[wi].append(light) return {"widget": frame, "words": words} def update_dcdc_display(refs, word1, word2): for i in range(8): set_light(refs["words"][1][i], RED if (word1 & (1 << i)) else GREEN) set_light(refs["words"][2][i], RED if (word2 & (1 << i)) else GREEN)