Files
H100PowerManger/tools/pmpower_db.py
T
zjk 12f0dee86e 修复:跨线程数据竞争 + 协议字段越界 + UDP 发送失败吞错
- 并发安全:PowerManger 新增 m_stateMutex,保护 m_pmCurrentState /
  m_currentDisBreakerState / m_CcuColCmd / faultCode,并封装
  addFaultCode/clearFaultCodes/getFaultCodes 访问器
- SystemData 新增跨线程读访问器(getDynVoltageLink/getPowerBusStatus/
  getDcdcStatus/getDeviceCommonStatus 等)及配电故障码集合加锁访问
- upmsg buildMsg / httpserver pushSystemData / LowerCommManager 均改为
  持锁快照,避免 listen 线程与 comm 线程撕裂读写
- 修复析构顺序:先删 httpServer 再删其他对象,避免 use-after-free
- 修复 ccuState reserved2 越界读(协议预留 2 字节,结构体仅声明 1),
  修正 coutMsg 中 SOC 门限/功率限制字段打印
- udpComm::sendDisSysCmd 不再吞掉异常,返回 sendMsg 实际结果并记录 ERROR,
  新增 SendDisSysCmdFailsWithoutSocket 单测
- 新增 fullFeeder 全类型 UDP 集成仿真器与 fetch-data/clean-data 脚本,
  .gitignore 补充 SQLite 运行期文件与 fullFeeder 二进制
2026-08-21 11:36:19 +08:00

1393 lines
65 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pPowerManger SQLite 数据查看工具 (tkinter GUI, 仅使用 Python 标准库)
功能:
1. 数据浏览 - 打开 power_data.db, 浏览各表, 支持行数限制/时间筛选/条件过滤/人性化解码
2. SQL 查询 - 执行任意只读 SQL, 结果表格展示, 导出 CSV
3. 帧解码 - 解析 msg_log 中的原始 hex 帧, 按 msg_id 还原结构体字段(中文标注)
4. 趋势图 - 选中数值列, 按时间绘制折线图(支持时间桶聚合)
依赖: tkinter(系统包 python3-tk), 其余均为标准库(sqlite3/csv/struct/datetime)
运行: python3 tools/pmpower_db.py
"""
import csv
import os
import struct
import sqlite3
from datetime import datetime, timedelta
try:
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
except Exception: # tkinter 未安装时仍可导入本模块用于逻辑测试
tk = None
ttk = None
APP_TITLE = "pPowerManger 数据查看工具"
# 由 setup_fonts() 在启动时设置的实际可用中文字体族
_UI_SANS = "TkDefaultFont"
_UI_MONO = "TkFixedFont"
# =====================================================================
# 枚举映射
# =====================================================================
BREAKER_MAP = {0x00: "无效", 0x55: "合闸", 0xAA: "分闸", 0x5A: "故障"}
SIGNAL_MAP = {0x00: "无效", 0x55: "正常", 0xAA: "故障"}
INSUL_MAP = {0x00: "无效", 0x55: "正常", 0xAA: "绝缘低"}
ONOFF_MAP = {0x00: "无效", 0x55: "启动", 0xAA: "停止", 0x5A: "故障"}
ENUMS = {
"breaker": BREAKER_MAP,
"signal": SIGNAL_MAP,
"insulation": INSUL_MAP,
"onoff": ONOFF_MAP,
"work_condition": {
0: "工况切换中", 1: "待机", 2: "岸基备航", 3: "水中备航", 4: "遥控工况",
5: "巡航模式", 6: "高速模式", 7: "上浮下潜", 8: "浮调模式", 9: "水下侦查",
10: "水面侦查", 11: "DJ模式", 12: "调试模式",
},
"fcu_cmd": {
0x01: "自检", 0x02: "启动", 0x03: "停机", 0x04: "复位",
0x05: "急停", 0x06: "补给", 0x07: "预热", 0x08: "维护",
},
"fcu_state": {
0x01: "初始化", 0x02: "待机", 0x03: "自检异常", 0x04: "就绪", 0x05: "启动",
0x06: "运行", 0x07: "停机", 0x08: "故障", 0x09: "补给", 0x0A: "预热",
0x0B: "预热完成", 0x0C: "维护", 0x0D: "维护完成",
},
"battery_cmd": {0x00: "保持", 0x10: "启动", 0x20: "停止"},
"fc_mode": {0x00: "无效", 0x01: "调试", 0x02: "自动", 0x03: "补给"},
"fc_fault_level": {0x00: "无故障", 0x10: "一级", 0x20: "二级", 0x30: "三级", 0x40: "四级"},
"charge_status": {0x00: "无效", 0x10: "充电", 0x20: "放电"},
"emergency_status": {0x00: "无报警", 0x3A: "4级报警", 0x3B: "3级报警", 0x3C: "2级报警", 0x3D: "1级报警"},
"flame": {0x00: "无效", 0x01: "报警", 0x02: "故障"},
"direction": {0: "接收", 1: "发送"},
"checksum_ok": {0: "错误", 1: "正确"},
}
# =====================================================================
# 列元数据: 表 -> {列名: (中文名, 缩放倍数, 单位, 枚举key 或 None)}
# =====================================================================
COL_META = {}
COL_META["ccuColCmd"] = {
"workCondition": ("工况", 1, "", "work_condition"),
"fcuCmd": ("燃料电池指令", 1, "", "fcu_cmd"),
"fcuPowerEfficiency": ("燃料电池功率效率", 1, "%", None),
"insChargeEnable": ("仪表电池充电使能", 1, "", None),
"insBatCmd": ("仪表电池指令", 1, "", "battery_cmd"),
"powerBatCmd": ("动力电池指令", 1, "", "battery_cmd"),
"powerBatEfficiency": ("动力电池效率", 1, "%", None),
"heartbeat": ("心跳计数", 1, "", None),
"masterState": ("主机状态", 1, "", "hex"),
"paceholder1": ("占位字段1", 1, "", "hex"),
"paceholder2": ("占位字段2", 1, "", "hex"),
}
COL_META["ccuSetParmCmd"] = {
"insSocHold1": ("仪表SOC保持1", 1, "%", None),
"insSocHold2": ("仪表SOC保持2", 1, "%", None),
"insSocHold3": ("仪表SOC保持3", 1, "%", None),
"fcuToInsLimit1": ("燃电到仪表限值1", 1, "", None),
"fcuToInsLimit2": ("燃电到仪表限值2", 1, "", None),
"powerSocHold1": ("动力SOC保持1", 1, "%", None),
"powerSocHold2": ("动力SOC保持2", 1, "%", None),
"powerSocHold3": ("动力SOC保持3", 1, "%", None),
"fcuToPowerLimit1": ("燃电到动力限值1", 1, "", None),
"fcuToPowerLimit2": ("燃电到动力限值2", 1, "", None),
}
COL_META["ccuSetParmFb"] = {
"flag": ("标志", 1, "", "hex"),
"failureCode": ("故障码", 1, "", "hex"),
}
# --- ccuState 复合管控状态反馈 ---
COL_META["ccuState"] = {
"fc_mode": ("燃料电池运行模式", 1, "", "fc_mode"),
"fc_status": ("燃料电池状态", 1, "", "fcu_state"),
"fc_fault_level": ("燃料电池故障等级", 1, "", "fc_fault_level"),
"output_power_limit": ("输出功率限制", 1, "W", None),
"total_generation_time": ("累计发电时间", 0.1, "h", None),
"total_generation_power": ("累计发电功率", 1, "W", None),
"fault_level_1": ("一级故障位", 1, "", "hex"),
"fault_level_2": ("二级故障位", 1, "", "hex"),
"fault_level_3": ("三级故障位", 1, "", "hex"),
"fault_level_4": ("四级故障位", 1, "", "hex"),
"hydrogen_capacity": ("储氢罐剩余容量", 1, "%", None),
"liquid_oxygen_capacity": ("液氧罐剩余容量", 1, "%", None),
"palladium_temp": ("钯膜最高温度", 1, "℃", None),
"main_pipe_pressure": ("主水路压力", 0.01, "kPa", None),
"aux_pipe_pressure": ("辅水路压力", 0.01, "kPa", None),
"cabin_pressure1": ("舱室1压力", 0.01, "kPa", None),
"cabin_pressure2": ("舱室2压力", 0.01, "kPa", None),
"cabin_temp1": ("舱室1温度", 0.01, "℃", None),
"cabin_temp2": ("舱室2温度", 0.01, "℃", None),
"cabin_humidity1": ("舱室1湿度", 0.01, "%RH", None),
"cabin_humidity2": ("舱室2湿度", 0.01, "%RH", None),
"flame_detector1": ("火焰探测器1", 1, "", "flame"),
"flame_detector2": ("火焰探测器2", 1, "", "flame"),
"h2_concentration1": ("舱室H2浓度1", 0.01, "%LEL", None),
"h2_concentration2": ("舱室H2浓度2", 0.01, "%LEL", None),
"h2_concentration3": ("舱室H2浓度3", 0.01, "%LEL", None),
"o2_concentration1": ("舱室O2浓度1", 0.01, "%LEL", None),
"o2_concentration2": ("舱室O2浓度2", 0.01, "%LEL", None),
"ch3oh_concentration1": ("舱室甲醇浓度1", 0.01, "%LEL", None),
"ch3oh_concentration2": ("舱室甲醇浓度2", 0.01, "%LEL", None),
"reserved1_1": ("保留1_1", 1, "", None),
"reserved1_2": ("保留1_2", 1, "", None),
"emergency_battery1_voltage": ("应急电池1总电压", 1, "V", None),
"emergency_battery1_current": ("应急电池1总电流", 1, "A", None),
"emergency_battery1_max_temp": ("应急电池1最高温度", 1, "℃", None),
"emergency_battery1_fault_word": ("应急电池1故障字", 1, "", "hex"),
"emergency_battery2_voltage": ("应急电池2总电压", 1, "V", None),
"emergency_battery2_current": ("应急电池2总电流", 1, "A", None),
"emergency_battery2_max_temp": ("应急电池2最高温度", 1, "℃", None),
"emergency_battery2_fault_word": ("应急电池2故障字", 1, "", "hex"),
"cabin_ox_concentration": ("仪表舱氧气浓度", 1, "%", None),
"cabin_temperature": ("仪表舱温度", 1, "℃", None),
"cabin_humidity": ("仪表舱湿度", 1, "%RH", None),
"cabin_pressure": ("仪表舱大气压", 1, "kPa", None),
"power_cabin_ox_concentration": ("动力舱氧气浓度", 1, "%", None),
"power_cabin_temperature": ("动力舱温度", 1, "℃", None),
"power_cabin_humidity": ("动力舱湿度", 1, "%RH", None),
"power_cabin_pressure": ("动力舱大气压", 1, "kPa", None),
"reserved2": ("保留2", 1, "", None),
"dyn_alarm_flag1": ("动力电池报警1", 1, "", "hex"),
"dyn_alarm_flag2": ("动力电池报警2", 1, "", "hex"),
"dyn_alarm_flag3": ("动力电池报警3", 1, "", "hex"),
"dyn_alarm_flag4": ("动力电池报警4", 1, "", "hex"),
"dyn_alarm_flag5": ("动力电池报警5", 1, "", "hex"),
"dyn_alarm_flag6": ("动力电池报警6", 1, "", "hex"),
"ins_alarm_flag1": ("仪表电池报警1", 1, "", "hex"),
"ins_alarm_flag2": ("仪表电池报警2", 1, "", "hex"),
"ins_alarm_flag3": ("仪表电池报警3", 1, "", "hex"),
"ins_alarm_flag4": ("仪表电池报警4", 1, "", "hex"),
"ins_alarm_flag5": ("仪表电池报警5", 1, "", "hex"),
"ins_alarm_flag6": ("仪表电池报警6", 1, "", "hex"),
"ins_relay_status1": ("仪表继电器正极/充电", 1, "", "hex"),
"ins_relay_status2": ("仪表继电器预充/负极", 1, "", "hex"),
"dyn_relay_status1": ("动力继电器正极/充电", 1, "", "hex"),
"dyn_relay_status2": ("动力继电器预充/负极", 1, "", "hex"),
"ins_max_discharge_power": ("仪表电池最大放电功率", 0.05, "kW", None),
"dyn_max_discharge_power": ("动力电池最大放电功率", 0.05, "kW", None),
"ins_soc": ("仪表锂电池SOC", 1, "%", None),
"dyn_soc": ("动力锂电池SOC", 1, "%", None),
"ins_total_energy": ("仪表电池总能量", 1, "kWh", None),
"dyn_total_energy": ("动力电池总能量", 1, "kWh", None),
"ins_power_input": ("仪表电池输入功率", 1, "kW", None),
"dyn_power_input": ("动力电池输入功率", 1, "kW", None),
"ins_charge_status": ("仪表电池充放电", 1, "", "charge_status"),
"dyn_charge_status": ("动力电池充放电", 1, "", "charge_status"),
"ins_voltage_link": ("仪表电池LINK电压", 0.01, "V", None),
"ins_voltage_pack": ("仪表电池PACK电压", 0.01, "V", None),
"ins_current": ("仪表电池电流", 0.01, "A", None),
"ins_resistance_pos": ("仪表电池正极绝缘电阻", 1, "kΩ", None),
"ins_resistance_neg": ("仪表电池负极绝缘电阻", 1, "kΩ", None),
"dyn_voltage_link": ("动力电池LINK电压", 0.01, "V", None),
"dyn_voltage_pack": ("动力电池PACK电压", 0.01, "V", None),
"dyn_current": ("动力电池电流", 0.01, "A", None),
"dyn_resistance_pos": ("动力电池正极绝缘电阻", 1, "kΩ", None),
"dyn_resistance_neg": ("动力电池负极绝缘电阻", 1, "kΩ", None),
"ins_emergency_status": ("仪表电池紧急状态", 1, "", "emergency_status"),
"dyn_emergency_status": ("动力电池紧急状态", 1, "", "emergency_status"),
"reserved3_1": ("保留3_1", 1, "", None),
"reserved3_2": ("保留3_2", 1, "", None),
"ins_soc_threshold1": ("仪表电池SOC门限1", 1, "%", None),
"ins_soc_threshold2": ("仪表电池SOC门限2", 1, "%", None),
"ins_soc_threshold3": ("仪表电池SOC门限3", 1, "%", None),
"dyn_soc_threshold1": ("动力电池SOC门限1", 1, "%", None),
"dyn_soc_threshold2": ("动力电池SOC门限2", 1, "%", None),
"dyn_soc_threshold3": ("动力电池SOC门限3", 1, "%", None),
"ins_power_limit1": ("仪表电池功率限制1", 1, "%", None),
"ins_power_limit2": ("仪表电池功率限制2", 1, "%", None),
"dyn_power_limit1": ("动力电池功率限制1", 1, "%", None),
"dyn_power_limit2": ("动力电池功率限制2", 1, "%", None),
"device_online_flag1": ("设备在线标志1", 1, "", "hex"),
"device_online_flag2": ("设备在线标志2", 1, "", "hex"),
"reserved4": ("保留4", 1, "", None),
"heartbeat": ("心跳计数", 1, "", None),
}
# --- 高压母线配电指令 ---
COL_META["disHighVolBusCmd"] = {
"fuelCellCircuitBreaker": ("燃料电池断路器", 1, "", "breaker"),
"powerLithiumBatteryCircuitBreaker": ("动力锂电池断路器", 1, "", "breaker"),
"propulsionMotorCircuitBreaker": ("推进电机断路器", 1, "", "breaker"),
"lithiumBatteryGroupInstrumentCircuitBreaker": ("锂电池组仪表断路器", 1, "", "breaker"),
"dcDc5ModuleCircuitBreaker": ("DC/DC5模块断路器", 1, "", "breaker"),
"bowHighVoltageDistributionBoxCircuitBreaker": ("艏部高压配电箱断路器", 1, "", "breaker"),
"sternFTDevice45CircuitBreaker": ("艉部FT装置4/5断路器", 1, "", "breaker"),
"sternRudderSwitch1CircuitBreaker": ("艉舵开关1断路器", 1, "", "breaker"),
"sternRudderSwitch2CircuitBreaker": ("艉舵开关2断路器", 1, "", "breaker"),
"fbReservedCircuitBreaker": ("FB预留断路器", 1, "", "breaker"),
"tyzReservedCircuitBreaker": ("TYZ预留断路器", 1, "", "breaker"),
"reservedCircuitBreaker": ("预留断路器", 1, "", "breaker"),
"dcdc5Module": ("DC/DC5模块启动", 1, "", "onoff"),
"coolingSystem": ("冷却系统启动", 1, "", "onoff"),
}
# --- 高压母线A配电指令 ---
COL_META["disHighAVolBusCmd"] = {
"bowFTDevice123CircuitBreaker": ("艏部FT装置1/2/3断路器", 1, "", "breaker"),
"actuatorCircuitBreaker": ("启闭机构断路器", 1, "", "breaker"),
"mastSteeringGearControlBoxCircuitBreaker": ("桅杆舵机控制箱断路器", 1, "", "breaker"),
"xczCircuitBreaker": ("XCZ断路器", 1, "", "breaker"),
"bowRudderControlBoxCircuitBreaker": ("艏舵控制箱断路器", 1, "", "breaker"),
"openWaterCoverStartCylinderCircuitBreaker": ("敞水盖启动电缸断路器", 1, "", "breaker"),
}
# --- 低压主母排配电指令 ---
COL_META["disLowMainBusCmd"] = {
"lithiumBatteryGroupInstrumentCircuitBreaker": ("锂电池组(仪表)断路器", 1, "", "breaker"),
"bowLowVoltageDistributionBoxCircuitBreaker": ("艏部低压配电箱断路器", 1, "", "breaker"),
"unit4InstrumentDC48VCircuitBreaker": ("UNIT4仪表DC48V断路器", 1, "", "breaker"),
"dcC1DCDistributionPanelCircuitBreaker": ("DC-C1直流配电板断路器", 1, "", "breaker"),
"reservedCircuitBreaker1": ("预留断路器1", 1, "", "breaker"),
"reservedCircuitBreaker2": ("预留断路器2", 1, "", "breaker"),
"emergencyLithiumBatteryGroup2CircuitBreaker": ("应急锂电池组2断路器", 1, "", "breaker"),
}
# --- 低压母线配电指令 ---
COL_META["disLowBusCmd"] = {
"unit1CircuitBreaker": ("UNIT1断路器", 1, "", "breaker"),
"unit2CircuitBreaker": ("UNIT2断路器", 1, "", "breaker"),
"unit3CircuitBreaker": ("UNIT3断路器", 1, "", "breaker"),
"unit5CircuitBreaker": ("UNIT5断路器", 1, "", "breaker"),
"bowPZDeviceCircuitBreaker": ("艏部PZ装置断路器", 1, "", "breaker"),
"reservedCircuitBreaker1": ("预留断路器1", 1, "", "breaker"),
"reservedCircuitBreaker2": ("预留断路器2", 1, "", "breaker"),
"emergencyLithiumBatteryGroup1CircuitBreaker": ("应急锂电池组1断路器", 1, "", "breaker"),
}
# --- 高压母线配电反馈 ---
COL_META["disHighVolBusState"] = {
"fuelCellCircuitBreaker": ("燃料电池断路器", 1, "", "breaker"),
"powerLithiumBatteryCircuitBreaker": ("动力锂电池断路器", 1, "", "breaker"),
"propulsionMotorCircuitBreaker": ("推进电机断路器", 1, "", "breaker"),
"lithiumBatteryGroupInstrumentCircuitBreaker": ("锂电池组仪表断路器", 1, "", "breaker"),
"dcDc5ModuleCircuitBreaker": ("DC/DC5模块断路器", 1, "", "breaker"),
"bowHighVoltageDistributionBoxCircuitBreaker": ("艏部高压配电箱断路器", 1, "", "breaker"),
"sternFTDevice45CircuitBreaker": ("艉部FT装置4/5断路器", 1, "", "breaker"),
"sternRudderSwitch1CircuitBreaker": ("艉舵开关1断路器", 1, "", "breaker"),
"sternRudderSwitch2CircuitBreaker": ("艉舵开关2断路器", 1, "", "breaker"),
"fbReservedCircuitBreaker": ("FB预留断路器", 1, "", "breaker"),
"tyzReservedCircuitBreaker": ("TYZ预留断路器", 1, "", "breaker"),
"reservedCircuitBreaker": ("预留断路器", 1, "", "breaker"),
"dcDcFaultWord1": ("DC/DC5故障字低", 1, "", "hex"),
"dcDcFaultWord2": ("DC/DC5故障字高", 1, "", "hex"),
"powerBusInsulationStatus": ("动力母排绝缘状态", 1, "", "insulation"),
"aBusInsulationStatus": ("A汇流排绝缘状态", 1, "", "insulation"),
"meterPowerLossSignal": ("仪表电源状态", 1, "", "signal"),
"emergencyPowerLossSignal": ("应急电源状态", 1, "", "signal"),
"powerBusVoltage": ("动力汇流排电压", 1, "", None),
"dcDc5ModuleCurrent": ("DC/DC5模块电流", 1, "", None),
"powerBusCurrent": ("动力汇流排电流", 1, "", None),
"busbarAVoltage": ("A汇流排电压", 1, "", None),
"propulsionMotorControlBoxCurrent": ("推进电机控制箱电流", 1, "", None),
"busbarACurrent": ("A汇流排电流", 1, "", None),
"lithiumBatteryGroupMeterCurrent": ("锂电池组(仪表)电流", 1, "", None),
"bowHighVoltageDistributionBoxCurrent": ("艏部高压配电箱电流", 1, "", None),
"sternFTDevice45Current": ("艉部FT装置4/5电流", 1, "", None),
"tyzReservedSwitchCurrent": ("TYZ预留开关电流", 1, "", None),
"sternRudderSwitch1Current": ("艉舵开关1电流", 1, "", None),
"reservedCurrent1": ("预留电流1", 1, "", None),
"sternRudderSwitch2Current": ("艉舵开关2电流", 1, "", None),
"reservedCurrent2": ("预留电流2", 1, "", None),
"fbReservedSwitchCurrent": ("FB预留电流", 1, "", None),
"reservedCurrent3": ("预留电流3", 1, "", None),
"coolWaterPressure": ("冷却水压力", 1, "", None),
}
# --- 高压母线A配电反馈 ---
COL_META["disHighAVolBusState"] = {
"bowFTDevice123CircuitBreaker": ("艏部FT装置1/2/3断路器", 1, "", "breaker"),
"actuatorCircuitBreaker": ("启闭机构断路器", 1, "", "breaker"),
"mastSteeringGearControlBoxCircuitBreaker": ("桅杆舵机控制箱断路器", 1, "", "breaker"),
"xczCircuitBreaker": ("XCZ断路器", 1, "", "breaker"),
"bowRudderControlBoxCircuitBreaker": ("艏舵控制箱断路器", 1, "", "breaker"),
"openWaterCoverStartCylinderCircuitBreaker": ("敞水盖启动电缸断路器", 1, "", "breaker"),
"instrumentPowerFailureSignal": ("仪表电源失电信号", 1, "", "signal"),
"emergencyPowerFailureSignal": ("应急电源失电信号", 1, "", "signal"),
"waterIngressionAlarm": ("浸水报警", 1, "", "signal"),
"busbarBVoltage": ("汇流排B电压", 1, "", None),
"busbarBCurrent": ("汇流排B电流", 1, "", None),
"bowFTDevice123Current": ("艏部FT装置1/2/3电流", 1, "", None),
"actuatorCurrent": ("启闭机构电流", 1, "", None),
"mastSteeringGearControlBoxCurrent": ("桅杆舵机控制箱电流", 1, "", None),
"xczCurrent": ("XCZ电流", 1, "", None),
"bowRudderControlBoxCurrent": ("艏舵控制箱电流", 1, "", None),
"openWaterCoverStartCylinderCurrent": ("敞水盖启动电缸电流", 1, "", None),
}
# --- 低压主母排配电反馈 ---
COL_META["disLowMainBusState"] = {
"lithiumBatteryGroupInstrumentCircuitBreaker": ("锂电池组(仪表)断路器", 1, "", "breaker"),
"bowLowVoltageDistributionBoxCircuitBreaker": ("艏部低压配电箱断路器", 1, "", "breaker"),
"unit4InstrumentDC48VCircuitBreaker": ("UNIT4仪表DC48V断路器", 1, "", "breaker"),
"dcC1DCDistributionPanelCircuitBreaker": ("DC-C1直流配电板断路器", 1, "", "breaker"),
"reservedCircuitBreaker1": ("预留断路器1", 1, "", "breaker"),
"reservedCircuitBreaker2": ("预留断路器2", 1, "", "breaker"),
"emergencyLithiumBatteryGroup2CircuitBreaker": ("应急锂电池组2断路器", 1, "", "breaker"),
"instrumentPowerFailureSignal": ("仪表电源失电信号", 1, "", "signal"),
"emergencyPowerFailureSignal": ("应急电源失电信号", 1, "", "signal"),
"instrumentBusbarInsulationLow": ("仪表汇流排绝缘低", 1, "", "insulation"),
"instrumentBusbarVoltage": ("仪表母排电压", 1, "", None),
"bowLowVoltageDistributionBoxCurrent": ("艏部低压配电箱电流", 1, "", None),
"dcC1InstrumentDC48VCurrent": ("DC-C1仪表DC48V电流", 1, "", None),
"reservedCurrent1": ("预留电流1", 1, "", None),
"emergencyLithiumBatteryGroup2Current": ("应急锂电池组2电流", 1, "", None),
"lithiumBatteryGroupInstrumentCurrent": ("锂电池组(仪表)电流", 1, "", None),
"unit4InstrumentDC48VCurrent": ("UNIT4仪表DC48V电流", 1, "", None),
"reservedCurrent2": ("预留电流2", 1, "", None),
"emergency2BusbarVoltage": ("应急2汇流排电压", 1, "", None),
"compositeEnergyManagementSystemEmergencyDC48VCurrent": ("复合能源管控应急DC48V电流", 1, "", None),
"fuelCellSecuritySystemEmergencyDC48VCurrent": ("燃料电池安全系统应急DC48V电流", 1, "", None),
"plcControlPowerCurrent": ("PLC控制电源电流", 1, "", None),
}
# --- 低压母线配电反馈 ---
COL_META["disLowBusState"] = {
"unit1CircuitBreaker": ("UNIT1断路器", 1, "", "breaker"),
"unit2CircuitBreaker": ("UNIT2断路器", 1, "", "breaker"),
"unit3CircuitBreaker": ("UNIT3断路器", 1, "", "breaker"),
"unit5CircuitBreaker": ("UNIT5断路器", 1, "", "breaker"),
"bowPZDeviceCircuitBreaker": ("艏部PZ装置断路器", 1, "", "breaker"),
"reservedCircuitBreaker1": ("预留断路器1", 1, "", "breaker"),
"reservedCircuitBreaker2": ("预留断路器2", 1, "", "breaker"),
"emergencyLithiumBatteryGroup1CircuitBreaker": ("应急锂电池组1断路器", 1, "", "breaker"),
"powerFailureSignal": ("仪表电源失电信号", 1, "", "signal"),
"emergencyPowerFailureSignal": ("应急电源失电信号", 1, "", "signal"),
"waterIngressionAlarm": ("浸水报警", 1, "", "signal"),
"instrumentBusbarVoltage": ("仪表母排电压", 1, "", None),
"instrumentBusbarCurrent": ("仪表母排电流", 1, "", None),
"unit1Current": ("UNIT1电流", 1, "", None),
"unit2Current": ("UNIT2电流", 1, "", None),
"unit3Current": ("UNIT3电流", 1, "", None),
"unit5Current": ("UNIT5电流", 1, "", None),
"bowPZDeviceCurrent": ("艏部PZ装置电流", 1, "", None),
"emergencyLithiumBatteryGroup1Current": ("应急锂电池组1电流", 1, "", None),
"emergency1BusbarVoltage": ("应急1汇流排电压", 1, "", None),
}
# --- 原始收发帧日志 ---
COL_META["msg_log"] = {
"ts": ("时间戳(unix秒)", 1, "", None),
"ts_local": ("本地时间", 1, "", None),
"direction": ("方向", 1, "", "direction"),
"msg_id": ("消息ID", 1, "", "hex"),
"msg_name": ("消息名", 1, "", None),
"length": ("帧长度", 1, "", None),
"payload_hex": ("载荷(hex)", 1, "", None),
"checksum_ok": ("校验和", 1, "", "checksum_ok"),
"note": ("备注", 1, "", None),
}
# 反馈表 feedback_data.db
COL_META["fb_log"] = {
"ts": ("时间戳(unix秒)", 1, "", None),
"msg": ("消息类型", 1, "", None),
"payload": ("载荷", 1, "", None),
}
# =====================================================================
# 帧解码定义: msg_id -> (消息名, [(字段名, 格式, 中文名, 枚举key)])
# 格式: B=u8, H=u16, I=u32
# =====================================================================
FRAME_DEFS = {}
FRAME_DEFS[0x01] = ("disHighVolBusCmd 高压母线配电指令", [
("fuelCellCircuitBreaker", "B", "燃料电池断路器", "breaker"),
("powerLithiumBatteryCircuitBreaker", "B", "动力锂电池断路器", "breaker"),
("propulsionMotorCircuitBreaker", "B", "推进电机断路器", "breaker"),
("lithiumBatteryGroupInstrumentCircuitBreaker", "B", "锂电池组仪表断路器", "breaker"),
("dcDc5ModuleCircuitBreaker", "B", "DC/DC5模块断路器", "breaker"),
("bowHighVoltageDistributionBoxCircuitBreaker", "B", "艏部高压配电箱断路器", "breaker"),
("sternFTDevice45CircuitBreaker", "B", "艉部FT装置4/5断路器", "breaker"),
("fbReservedCircuitBreaker", "B", "FB预留断路器", "breaker"),
("sternRudderSwitch1CircuitBreaker", "B", "艉舵开关1断路器", "breaker"),
("sternRudderSwitch2CircuitBreaker", "B", "艉舵开关2断路器", "breaker"),
("tyzReservedCircuitBreaker", "B", "TYZ预留断路器", "breaker"),
("reservedCircuitBreaker", "B", "预留断路器", "breaker"),
("dcdc5Module", "B", "DC/DC5模块启动", "onoff"),
("coolingSystem", "B", "冷却系统启动", "onoff"),
])
FRAME_DEFS[0x02] = ("disHighAVolBusCmd 高压母线A配电指令", [
("bowFTDevice123CircuitBreaker", "B", "艏部FT装置1/2/3断路器", "breaker"),
("actuatorCircuitBreaker", "B", "启闭机构断路器", "breaker"),
("mastSteeringGearControlBoxCircuitBreaker", "B", "桅杆舵机控制箱断路器", "breaker"),
("xczCircuitBreaker", "B", "XCZ断路器", "breaker"),
("bowRudderControlBoxCircuitBreaker", "B", "艏舵控制箱断路器", "breaker"),
("openWaterCoverStartCylinderCircuitBreaker", "B", "敞水盖启动电缸断路器", "breaker"),
])
FRAME_DEFS[0x03] = ("disLowMainBusCmd 低压主母排配电指令", [
("lithiumBatteryGroupInstrumentCircuitBreaker", "B", "锂电池组(仪表)断路器", "breaker"),
("bowLowVoltageDistributionBoxCircuitBreaker", "B", "艏部低压配电箱断路器", "breaker"),
("unit4InstrumentDC48VCircuitBreaker", "B", "UNIT4仪表DC48V断路器", "breaker"),
("dcC1DCDistributionPanelCircuitBreaker", "B", "DC-C1直流配电板断路器", "breaker"),
("reservedCircuitBreaker1", "B", "预留断路器1", "breaker"),
("reservedCircuitBreaker2", "B", "预留断路器2", "breaker"),
("emergencyLithiumBatteryGroup2CircuitBreaker", "B", "应急锂电池组2断路器", "breaker"),
])
FRAME_DEFS[0x04] = ("disLowBusCmd 低压母线配电指令", [
("unit1CircuitBreaker", "B", "UNIT1断路器", "breaker"),
("unit2CircuitBreaker", "B", "UNIT2断路器", "breaker"),
("unit3CircuitBreaker", "B", "UNIT3断路器", "breaker"),
("unit5CircuitBreaker", "B", "UNIT5断路器", "breaker"),
("bowPZDeviceCircuitBreaker", "B", "艏部PZ装置断路器", "breaker"),
("reservedCircuitBreaker1", "B", "预留断路器1", "breaker"),
("reservedCircuitBreaker2", "B", "预留断路器2", "breaker"),
("emergencyLithiumBatteryGroup1CircuitBreaker", "B", "应急锂电池组1断路器", "breaker"),
])
FRAME_DEFS[0x40] = ("ccuColCmd 复合管控操作指令", [
("year", "H", "年", None),
("month", "B", "月", None),
("day", "B", "日", None),
("hour", "B", "时", None),
("minute", "B", "分", None),
("second", "B", "秒", None),
("millisecond", "B", "毫秒(×10)", None),
("fcmode", "B", "工况", "work_condition"),
("fcuCmd", "B", "燃料电池指令", "fcu_cmd"),
("fcuPowerEfficiency", "B", "燃料电池功率效率", None),
("insChargeEnable", "B", "仪表电池充电使能", None),
("insBatCmd", "B", "仪表电池指令", "battery_cmd"),
("powerBatCmd", "B", "动力电池指令", "battery_cmd"),
("powerBatEfficiency", "H", "动力电池效率", None),
("heartbeat", "B", "心跳计数", None),
("masterState", "B", "主机状态", "hex"),
("paceholder1", "I", "占位字段1", "hex"),
("paceholder2", "I", "占位字段2", "hex"),
])
FRAME_DEFS[0x41] = ("ccuSetParmCmd 复合管控参数设定指令", [
("insSocHold1", "B", "仪表SOC保持1", None),
("insSocHold2", "B", "仪表SOC保持2", None),
("insSocHold3", "B", "仪表SOC保持3", None),
("fcuToInsLimit1", "B", "燃电到仪表限值1", None),
("fcuToInsLimit2", "B", "燃电到仪表限值2", None),
("powerSocHold1", "B", "动力SOC保持1", None),
("powerSocHold2", "B", "动力SOC保持2", None),
("powerSocHold3", "B", "动力SOC保持3", None),
("fcuToPowerLimit1", "B", "燃电到动力限值1", None),
("fcuToPowerLimit2", "B", "燃电到动力限值2", None),
])
# 配电状态反馈 (0x05~0x08), 供未来状态帧解码使用
FRAME_DEFS[0x05] = ("disHighVolBusState 高压母线配电反馈", [
("fuelCellCircuitBreaker", "B", "燃料电池断路器", "breaker"),
("powerLithiumBatteryCircuitBreaker", "B", "动力锂电池断路器", "breaker"),
("propulsionMotorCircuitBreaker", "B", "推进电机断路器", "breaker"),
("lithiumBatteryGroupInstrumentCircuitBreaker", "B", "锂电池组仪表断路器", "breaker"),
("dcDc5ModuleCircuitBreaker", "B", "DC/DC5模块断路器", "breaker"),
("bowHighVoltageDistributionBoxCircuitBreaker", "B", "艏部高压配电箱断路器", "breaker"),
("sternFTDevice45CircuitBreaker", "B", "艉部FT装置4/5断路器", "breaker"),
("fbReservedCircuitBreaker", "B", "FB预留断路器", "breaker"),
("sternRudderSwitch1CircuitBreaker", "B", "艉舵开关1断路器", "breaker"),
("sternRudderSwitch2CircuitBreaker", "B", "艉舵开关2断路器", "breaker"),
("tyzReservedCircuitBreaker", "B", "TYZ预留断路器", "breaker"),
("reservedCircuitBreaker", "B", "预留断路器", "breaker"),
("dcDcFaultWord1", "B", "DC/DC5故障字低", "hex"),
("dcDcFaultWord2", "B", "DC/DC5故障字高", "hex"),
("powerBusInsulationStatus", "B", "动力母排绝缘状态", "insulation"),
("aBusInsulationStatus", "B", "A汇流排绝缘状态", "insulation"),
("meterPowerLossSignal", "B", "仪表电源状态", "signal"),
("emergencyPowerLossSignal", "B", "应急电源状态", "signal"),
("dcDcTemperature", "B", "DC/DC温度", None),
("powerBusVoltage", "H", "动力汇流排电压", None),
("dcDc5ModuleCurrent", "H", "DC/DC5模块电流", None),
("powerBusCurrent", "H", "动力汇流排电流", None),
("busbarAVoltage", "H", "A汇流排电压", None),
("propulsionMotorControlBoxCurrent", "H", "推进电机控制箱电流", None),
("busbarACurrent", "H", "A汇流排电流", None),
("lithiumBatteryGroupMeterCurrent", "H", "锂电池组(仪表)电流", None),
("bowHighVoltageDistributionBoxCurrent", "H", "艏部高压配电箱电流", None),
("sternFTDevice45Current", "H", "艉部FT装置4/5电流", None),
("tyzReservedSwitchCurrent", "H", "TYZ预留开关电流", None),
("sternRudderSwitch1Current", "H", "艉舵开关1电流", None),
("reservedCurrent1", "H", "预留电流1", None),
("sternRudderSwitch2Current", "H", "艉舵开关2电流", None),
("reservedCurrent2", "H", "预留电流2", None),
("fbReservedSwitchCurrent", "H", "FB预留电流", None),
("reservedCurrent3", "H", "预留电流3", None),
("coolWaterPressure", "H", "冷却水压力", None),
])
FRAME_DEFS[0x06] = ("disHighAVolBusState 高压母线A配电反馈", [
("bowFTDevice123CircuitBreaker", "B", "艏部FT装置1/2/3断路器", "breaker"),
("actuatorCircuitBreaker", "B", "启闭机构断路器", "breaker"),
("mastSteeringGearControlBoxCircuitBreaker", "B", "桅杆舵机控制箱断路器", "breaker"),
("xczCircuitBreaker", "B", "XCZ断路器", "breaker"),
("bowRudderControlBoxCircuitBreaker", "B", "艏舵控制箱断路器", "breaker"),
("openWaterCoverStartCylinderCircuitBreaker", "B", "敞水盖启动电缸断路器", "breaker"),
("instrumentPowerFailureSignal", "B", "仪表电源失电信号", "signal"),
("emergencyPowerFailureSignal", "B", "应急电源失电信号", "signal"),
("waterIngressionAlarm", "B", "浸水报警", "signal"),
("busbarBVoltage", "H", "汇流排B电压", None),
("busbarBCurrent", "H", "汇流排B电流", None),
("bowFTDevice123Current", "H", "艏部FT装置1/2/3电流", None),
("actuatorCurrent", "H", "启闭机构电流", None),
("mastSteeringGearControlBoxCurrent", "H", "桅杆舵机控制箱电流", None),
("xczCurrent", "H", "XCZ电流", None),
("bowRudderControlBoxCurrent", "H", "艏舵控制箱电流", None),
("openWaterCoverStartCylinderCurrent", "H", "敞水盖启动电缸电流", None),
])
FRAME_DEFS[0x07] = ("disLowMainBusState 低压主母排配电反馈", [
("lithiumBatteryGroupInstrumentCircuitBreaker", "B", "锂电池组(仪表)断路器", "breaker"),
("bowLowVoltageDistributionBoxCircuitBreaker", "B", "艏部低压配电箱断路器", "breaker"),
("unit4InstrumentDC48VCircuitBreaker", "B", "UNIT4仪表DC48V断路器", "breaker"),
("dcC1DCDistributionPanelCircuitBreaker", "B", "DC-C1直流配电板断路器", "breaker"),
("reservedCircuitBreaker1", "B", "预留断路器1", "breaker"),
("reservedCircuitBreaker2", "B", "预留断路器2", "breaker"),
("emergencyLithiumBatteryGroup2CircuitBreaker", "B", "应急锂电池组2断路器", "breaker"),
("instrumentPowerFailureSignal", "B", "仪表电源失电信号", "signal"),
("emergencyPowerFailureSignal", "B", "应急电源失电信号", "signal"),
("instrumentBusbarInsulationLow", "B", "仪表汇流排绝缘低", "insulation"),
("instrumentBusbarVoltage", "H", "仪表母排电压", None),
("bowLowVoltageDistributionBoxCurrent", "H", "艏部低压配电箱电流", None),
("dcC1InstrumentDC48VCurrent", "H", "DC-C1仪表DC48V电流", None),
("reservedCurrent1", "H", "预留电流1", None),
("emergencyLithiumBatteryGroup2Current", "H", "应急锂电池组2电流", None),
("lithiumBatteryGroupInstrumentCurrent", "H", "锂电池组(仪表)电流", None),
("unit4InstrumentDC48VCurrent", "H", "UNIT4仪表DC48V电流", None),
("reservedCurrent2", "H", "预留电流2", None),
("emergency2BusbarVoltage", "H", "应急2汇流排电压", None),
("compositeEnergyManagementSystemEmergencyDC48VCurrent", "H", "复合能源管控应急DC48V电流", None),
("fuelCellSecuritySystemEmergencyDC48VCurrent", "H", "燃料电池安全系统应急DC48V电流", None),
("plcControlPowerCurrent", "H", "PLC控制电源电流", None),
])
FRAME_DEFS[0x08] = ("disLowBusState 低压母线配电反馈", [
("unit1CircuitBreaker", "B", "UNIT1断路器", "breaker"),
("unit2CircuitBreaker", "B", "UNIT2断路器", "breaker"),
("unit3CircuitBreaker", "B", "UNIT3断路器", "breaker"),
("unit5CircuitBreaker", "B", "UNIT5断路器", "breaker"),
("bowPZDeviceCircuitBreaker", "B", "艏部PZ装置断路器", "breaker"),
("reservedCircuitBreaker1", "B", "预留断路器1", "breaker"),
("reservedCircuitBreaker2", "B", "预留断路器2", "breaker"),
("emergencyLithiumBatteryGroup1CircuitBreaker", "B", "应急锂电池组1断路器", "breaker"),
("powerFailureSignal", "B", "仪表电源失电信号", "signal"),
("emergencyPowerFailureSignal", "B", "应急电源失电信号", "signal"),
("waterIngressionAlarm", "B", "浸水报警", "signal"),
("instrumentBusbarVoltage", "H", "仪表母排电压", None),
("instrumentBusbarCurrent", "H", "仪表母排电流", None),
("unit1Current", "H", "UNIT1电流", None),
("unit2Current", "H", "UNIT2电流", None),
("unit3Current", "H", "UNIT3电流", None),
("unit5Current", "H", "UNIT5电流", None),
("bowPZDeviceCurrent", "H", "艏部PZ装置电流", None),
("emergencyLithiumBatteryGroup1Current", "H", "应急锂电池组1电流", None),
("emergency1BusbarVoltage", "H", "应急1汇流排电压", None),
])
# =====================================================================
# 数据库访问
# =====================================================================
def connect_db(path):
"""只读连接数据库, 兼容 WAL, 程序运行期间可安全读取。"""
con = sqlite3.connect(path)
con.execute("PRAGMA query_only=ON")
con.row_factory = sqlite3.Row
return con
def list_tables(con):
"""返回 [(表名, 行数, 时间列名, 最小时间, 最大时间)]"""
out = []
rows = con.execute(
"SELECT name FROM sqlite_master WHERE type='table' "
"AND name NOT LIKE 'sqlite_%' ORDER BY name"
).fetchall()
for (name,) in rows:
try:
n = con.execute("SELECT COUNT(*) FROM %s" % name).fetchone()[0]
except Exception:
n = -1
tcol, tmin, tmax = detect_time_range(con, name)
out.append((name, n, tcol, tmin, tmax))
return out
def table_columns(con, table):
return [r[1] for r in con.execute("PRAGMA table_info(%s)" % table).fetchall()]
def detect_time_range(con, table):
cols = table_columns(con, table)
tcol = None
for cand in ("timestamp", "ts_local", "ts"):
if cand in cols:
tcol = cand
break
if tcol is None:
return None, None, None
try:
r = con.execute("SELECT MIN(%s), MAX(%s) FROM %s" % (tcol, tcol, table)).fetchone()
return tcol, r[0], r[1]
except Exception:
return tcol, None, None
def format_time_value(tcol, val):
if val is None:
return ""
if tcol == "ts" and isinstance(val, (int, float)):
return datetime.fromtimestamp(val).strftime("%Y-%m-%d %H:%M:%S")
return str(val)
def parse_time_input(s):
s = s.strip()
if not s:
return None
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
return datetime.strptime(s, fmt)
except ValueError:
continue
raise ValueError("时间格式应为 YYYY-MM-DD [HH:MM:SS]: " + s)
# =====================================================================
# 数值格式化 / 人性化解码
# =====================================================================
_reverse_meta = None
def _build_reverse():
global _reverse_meta
if _reverse_meta is None:
_reverse_meta = {}
for tbl, cols in COL_META.items():
for col in cols:
_reverse_meta.setdefault(col, (tbl, cols[col]))
return _reverse_meta
def fmt_value(table, col, raw, human):
"""把原始值转为展示字符串。human=True 时应用缩放/单位/枚举解码。"""
if raw is None:
return ""
if not human:
return str(raw)
meta = COL_META.get(table, {}).get(col)
if meta is None:
rev = _build_reverse().get(col)
if rev is not None:
meta = rev[1]
if meta is None:
return str(raw)
_desc, scale, unit, enum_key = meta
if enum_key == "hex" and isinstance(raw, int):
return "0x%X (%d)" % (raw, raw)
if enum_key and isinstance(raw, int):
m = ENUMS.get(enum_key, {})
if raw in m:
if enum_key in ("breaker", "signal", "insulation", "onoff"):
return "%s (0x%02X)" % (m[raw], raw)
return "%s (%d)" % (m[raw], raw)
if isinstance(raw, (int, float)) and scale != 1:
v = raw * scale
if isinstance(v, float) and v == int(v):
v = int(v)
return "%s%s" % (v, unit)
if unit:
return "%s%s" % (raw, unit)
return str(raw)
def fmt_frame_value(raw, fmt, enum_key):
size = {"B": 1, "H": 2, "I": 4}[fmt]
hexs = "0x%0*X" % (size * 2, raw)
s = "%d (%s)" % (raw, hexs)
if enum_key == "hex":
return hexs
if enum_key and raw in ENUMS.get(enum_key, {}):
s += " = " + ENUMS[enum_key][raw]
return s
def decode_frame(hexstr):
"""解析单个 hex 帧, 返回字符串行列表。"""
hs = hexstr.strip().replace(" ", "").replace("\n", "")
if not hs:
return ["(空输入)"]
try:
b = bytes.fromhex(hs)
except ValueError:
return ["非法 hex 字符串: %s" % hexstr.strip()]
if len(b) < 6:
return ["长度不足(至少6字节): %s (%d字节)" % (hs, len(b))]
start1, start2 = b[0], b[1]
msg_id = struct.unpack_from("<H", b, 2)[0]
length_field = struct.unpack_from("<H", b, 4)[0]
if start1 == 0x20 and start2 == 0x20:
proto, cs_size = "复合管控(CCU)", 4
elif start1 == 0x40 and start2 == 0x40:
proto, cs_size = "配电(DIS)", 1
else:
proto, cs_size = "未知协议", 1
if len(b) < 6 + cs_size:
return ["帧过短, 无数据区: %s" % hs]
data = b[6:len(b) - cs_size]
checksum = b[len(b) - cs_size:]
lines = [
"协议: %s 头: 0x%02X 0x%02X id=0x%04X(%d) 长度字段=%d 总长=%d字节"
% (proto, start1, start2, msg_id, msg_id, length_field, len(b)),
]
d = FRAME_DEFS.get(msg_id)
if d is None:
lines.append("未定义的消息 id=0x%04X" % msg_id)
lines.append("数据区(hex): %s" % data.hex().upper())
return lines
name, fields = d
lines.append("消息: %s" % name)
off = 0
for nm, fmt, desc, ekey in fields:
sz = {"B": 1, "H": 2, "I": 4}[fmt]
if off + sz > len(data):
lines.append(" [字段越界] %-32s (剩余 %d 字节)" % (nm, len(data) - off))
break
raw = struct.unpack_from("<" + fmt, data, off)[0]
off += sz
lines.append(" %-34s %-22s = %s" % (nm, desc, fmt_frame_value(raw, fmt, ekey)))
lines.append(" 校验和: %s 数据区(hex): %s" % (checksum.hex().upper(), data.hex().upper()))
return lines
# =====================================================================
# 默认数据库路径
# =====================================================================
def default_db_path():
base = os.path.dirname(os.path.abspath(__file__))
for p in (
os.path.join(base, "..", "bin", "power_data.db"),
os.path.join(base, "..", "src", "pPowerMangerHost", "feedback_data.db"),
os.path.join(base, "..", "bin", "power_data.db"),
):
p = os.path.normpath(p)
if os.path.exists(p):
return p
return os.path.normpath(os.path.join(base, "..", "bin", "power_data.db"))
# =====================================================================
# GUI
# =====================================================================
class PowerDbApp:
def __init__(self, root, db_path):
self.root = root
self.conn = None
self.db_path = db_path
self.current_table = None
self.current_cols = []
self.current_raw_rows = []
self.time_col = None
self.time_col_type = "text"
root.title(APP_TITLE)
root.geometry("1280x800")
self._build_toolbar()
self._build_notebook()
self._build_statusbar()
if db_path and os.path.exists(db_path):
self.set_status("请点击「打开数据库...」选择数据库文件(已默认填入: %s)" % db_path)
else:
self.set_status("请点击「打开数据库...」选择数据库文件")
# ---------- 工具栏 ----------
def _build_toolbar(self):
bar = ttk.Frame(self.root, padding=(6, 4))
bar.pack(fill="x")
self.db_var = tk.StringVar(value=self.db_path)
ttk.Button(bar, text="打开数据库...", command=self.choose_db).pack(side="left")
ent = ttk.Entry(bar, textvariable=self.db_var)
ent.pack(side="left", fill="x", expand=True, padx=6)
self.human_var = tk.BooleanVar(value=True)
ttk.Checkbutton(bar, text="人性化显示", variable=self.human_var,
command=self._on_human_toggle).pack(side="left", padx=6)
def _on_human_toggle(self):
if self.current_table:
self.load_table(self.current_table)
# ---------- 主标签页 ----------
def _build_notebook(self):
nb = ttk.Notebook(self.root)
nb.pack(fill="both", expand=True)
self.nb = nb
self.tab_browse = ttk.Frame(nb)
self.tab_query = ttk.Frame(nb)
self.tab_decode = ttk.Frame(nb)
self.tab_plot = ttk.Frame(nb)
nb.add(self.tab_browse, text="数据浏览")
nb.add(self.tab_query, text="SQL 查询")
nb.add(self.tab_decode, text="帧解码")
nb.add(self.tab_plot, text="趋势图")
self._build_browse_tab()
self._build_query_tab()
self._build_decode_tab()
self._build_plot_tab()
# ---------- 状态栏 ----------
def _build_statusbar(self):
self.status_var = tk.StringVar(value="就绪")
ttk.Label(self.root, textvariable=self.status_var, relief="sunken",
anchor="w", padding=(6, 2)).pack(fill="x", side="bottom")
def set_status(self, msg):
self.status_var.set(msg)
# ---------- 数据库打开 ----------
def choose_db(self):
p = filedialog.askopenfilename(
title="选择 SQLite 数据库",
filetypes=[("SQLite 数据库", "*.db *.sqlite *.db3"), ("所有文件", "*.*")],
)
if p:
self.open_db(p)
def open_db(self, path):
try:
if self.conn:
self.conn.close()
self.conn = connect_db(path)
self.db_path = path
self.db_var.set(path)
self._refresh_tables()
self._refresh_plot_tables()
self.set_status("已打开: %s" % path)
except Exception as e:
messagebox.showerror("打开失败", str(e))
def _refresh_tables(self):
for i in self.tables_tree.get_children():
self.tables_tree.delete(i)
if not self.conn:
return
for name, n, tcol, tmin, tmax in list_tables(self.conn):
tr = "%s ~ %s" % (format_time_value(tcol, tmin), format_time_value(tcol, tmax)) if tmin is not None else ""
self.tables_tree.insert("", "end", iid=name, values=(name, n, tr))
# ---------- 数据浏览 ----------
def _build_browse_tab(self):
left = ttk.Frame(self.tab_browse, width=340)
left.pack(side="left", fill="y")
left.pack_propagate(False)
ttk.Label(left, text="数据表").pack(anchor="w", padx=4, pady=2)
self.tables_tree = ttk.Treeview(left, columns=("name", "rows", "range"), show="headings", height=30)
self.tables_tree.heading("name", text="表名")
self.tables_tree.heading("rows", text="行数")
self.tables_tree.heading("range", text="时间范围")
self.tables_tree.column("name", width=150)
self.tables_tree.column("rows", width=70, anchor="e")
self.tables_tree.column("range", width=180)
tsb = ttk.Scrollbar(left, orient="vertical", command=self.tables_tree.yview)
self.tables_tree.configure(yscrollcommand=tsb.set)
self.tables_tree.pack(side="left", fill="both", expand=True, padx=(4, 0), pady=2)
tsb.pack(side="left", fill="y")
self.tables_tree.bind("<<TreeviewSelect>>", self._on_table_select)
right = ttk.Frame(self.tab_browse)
right.pack(side="left", fill="both", expand=True)
filter_bar = ttk.Frame(right)
filter_bar.pack(fill="x", padx=4, pady=4)
ttk.Label(filter_bar, text="条件(WHERE)").pack(side="left")
self.where_var = tk.StringVar()
ttk.Entry(filter_bar, textvariable=self.where_var, width=30).pack(side="left", padx=4)
ttk.Label(filter_bar, text="起始").pack(side="left", padx=(8, 0))
self.since_var = tk.StringVar()
ttk.Entry(filter_bar, textvariable=self.since_var, width=18).pack(side="left", padx=2)
ttk.Label(filter_bar, text="结束").pack(side="left")
self.until_var = tk.StringVar()
ttk.Entry(filter_bar, textvariable=self.until_var, width=18).pack(side="left", padx=2)
ttk.Label(filter_bar, text="行数").pack(side="left", padx=(8, 0))
self.limit_var = tk.StringVar(value="500")
ttk.Combobox(filter_bar, textvariable=self.limit_var, width=8,
values=("100", "500", "1000", "5000", "10000", "全部")).pack(side="left", padx=2)
ttk.Button(filter_bar, text="查询", command=self._refresh_browse).pack(side="left", padx=6)
ttk.Button(filter_bar, text="导出CSV", command=self._export_browse).pack(side="left", padx=2)
tree_area = ttk.Frame(right)
tree_area.pack(fill="both", expand=True)
self.data_tree = ttk.Treeview(tree_area, show="headings")
ysb = ttk.Scrollbar(tree_area, orient="vertical", command=self.data_tree.yview)
xsb = ttk.Scrollbar(tree_area, orient="horizontal", command=self.data_tree.xview)
self.data_tree.configure(yscrollcommand=ysb.set, xscrollcommand=xsb.set)
self.data_tree.grid(row=0, column=0, sticky="nsew")
ysb.grid(row=0, column=1, sticky="ns")
xsb.grid(row=1, column=0, sticky="ew")
tree_area.rowconfigure(0, weight=1)
tree_area.columnconfigure(0, weight=1)
def _on_table_select(self, _event):
sel = self.tables_tree.selection()
if not sel:
return
self.load_table(sel[0])
def load_table(self, table):
self.current_table = table
self._refresh_browse()
def _build_where(self):
parts = []
where = self.where_var.get().strip()
if where:
parts.append("(" + where + ")")
since = self.since_var.get().strip()
until = self.until_var.get().strip()
if (since or until) and self.time_col:
col = self.time_col
if self.time_col_type == "real":
if since:
parts.append("%s >= %f" % (col, parse_time_input(since).timestamp()))
if until:
parts.append("%s <= %f" % (col, parse_time_input(until).timestamp()))
else:
if since:
parts.append("%s >= '%s'" % (col, parse_time_input(since).strftime("%Y-%m-%d %H:%M:%S")))
if until:
parts.append("%s <= '%s'" % (col, parse_time_input(until).strftime("%Y-%m-%d %H:%M:%S")))
return (" WHERE " + " AND ".join(parts)) if parts else ""
def _refresh_browse(self):
if not self.conn or not self.current_table:
return
table = self.current_table
self.data_tree.delete(*self.data_tree.get_children())
cols = table_columns(self.conn, table)
self.current_cols = cols
# 时间列检测
self.time_col = None
self.time_col_type = "text"
for cand in ("timestamp", "ts_local", "ts"):
if cand in cols:
self.time_col = cand
self.time_col_type = "real" if cand == "ts" else "text"
break
self.data_tree["columns"] = cols
for c in cols:
meta = COL_META.get(table, {}).get(c)
title = meta[0] if meta else c
self.data_tree.heading(c, text=title)
self.data_tree.column(c, width=120, stretch=True, anchor="w")
sql = "SELECT * FROM %s" % table
try:
sql += self._build_where()
sql += " ORDER BY 1 DESC"
limit = self.limit_var.get().strip()
if limit != "全部":
sql += " LIMIT %d" % int(limit)
t0 = datetime.now()
rows = self.conn.execute(sql).fetchall()
dt = (datetime.now() - t0).total_seconds() * 1000
except Exception as e:
messagebox.showerror("查询失败", str(e))
return
self.current_raw_rows = rows
human = self.human_var.get()
for r in rows:
vals = [fmt_value(table, c, r[i], human) for i, c in enumerate(cols)]
self.data_tree.insert("", "end", values=vals)
total = self.conn.execute("SELECT COUNT(*) FROM %s" % table).fetchone()[0]
self.set_status("表 %s: 显示 %d / 共 %d 行, 耗时 %.1f ms" % (table, len(rows), total, dt))
def _export_browse(self):
if not self.current_raw_rows:
messagebox.showinfo("提示", "没有可导出的数据, 请先查询。")
return
self._export_to_csv(self.current_cols, self.current_raw_rows)
# ---------- SQL 查询 ----------
def _build_query_tab(self):
top = ttk.Frame(self.tab_query)
top.pack(fill="x", padx=4, pady=4)
ttk.Label(top, text="SQL 语句 (只读)").pack(anchor="w")
self.sql_text = tk.Text(top, height=5)
self.sql_text.pack(fill="x", pady=2)
btns = ttk.Frame(top)
btns.pack(fill="x")
ttk.Button(btns, text="执行", command=self._run_query).pack(side="left")
ttk.Button(btns, text="导出CSV", command=self._export_query).pack(side="left", padx=6)
self.q_human_var = tk.BooleanVar(value=False)
ttk.Checkbutton(btns, text="人性化显示", variable=self.q_human_var).pack(side="left")
self.q_table_hint = ttk.Label(btns, text="")
self.q_table_hint.pack(side="left", padx=10)
q_area = ttk.Frame(self.tab_query)
q_area.pack(fill="both", expand=True)
self.q_tree = ttk.Treeview(q_area, show="headings")
ysb = ttk.Scrollbar(q_area, orient="vertical", command=self.q_tree.yview)
xsb = ttk.Scrollbar(q_area, orient="horizontal", command=self.q_tree.xview)
self.q_tree.configure(yscrollcommand=ysb.set, xscrollcommand=xsb.set)
self.q_tree.grid(row=0, column=0, sticky="nsew")
ysb.grid(row=0, column=1, sticky="ns")
xsb.grid(row=1, column=0, sticky="ew")
q_area.rowconfigure(0, weight=1)
q_area.columnconfigure(0, weight=1)
self.q_rows = []
self.q_cols = []
self.q_table = None
def _guess_table(self, sql):
import re
m = re.search(r"\bfrom\s+([a-zA-Z_][\w]*)", sql, re.I)
if m:
return m.group(1)
return None
def _run_query(self):
if not self.conn:
messagebox.showinfo("提示", "请先打开数据库。")
return
self.q_tree.delete(*self.q_tree.get_children())
sql = self.sql_text.get("1.0", "end").strip()
if not sql:
return
try:
t0 = datetime.now()
cur = self.conn.execute(sql)
cols = [d[0] for d in cur.description] if cur.description else []
rows = cur.fetchall()
dt = (datetime.now() - t0).total_seconds() * 1000
except Exception as e:
messagebox.showerror("执行失败", str(e))
return
self.q_cols = cols
self.q_rows = rows
self.q_table = self._guess_table(sql)
self.q_table_hint.config(text="(匹配表: %s)" % self.q_table if self.q_table else "")
self.q_tree["columns"] = cols
for c in cols:
self.q_tree.heading(c, text=c)
self.q_tree.column(c, width=130, stretch=True, anchor="w")
table = self.q_table or ""
human = self.q_human_var.get()
for r in rows:
vals = [fmt_value(table, c, r[i], human) for i, c in enumerate(cols)]
self.q_tree.insert("", "end", values=vals)
self.set_status("查询返回 %d 行, 耗时 %.1f ms" % (len(rows), dt))
def _export_query(self):
if not self.q_rows:
messagebox.showinfo("提示", "没有可导出的数据, 请先执行查询。")
return
self._export_to_csv(self.q_cols, self.q_rows)
# ---------- CSV 导出 ----------
def _export_to_csv(self, cols, rows):
p = filedialog.asksaveasfilename(
title="导出 CSV", defaultextension=".csv",
filetypes=[("CSV 文件", "*.csv")],
)
if not p:
return
try:
with open(p, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(cols)
for r in rows:
w.writerow(list(r))
self.set_status("已导出 %d 行到 %s" % (len(rows), p))
except Exception as e:
messagebox.showerror("导出失败", str(e))
# ---------- 帧解码 ----------
def _build_decode_tab(self):
top = ttk.Frame(self.tab_decode)
top.pack(fill="x", padx=4, pady=4)
ttk.Label(top, text="粘贴 hex 帧 (可多帧, 空格/换行分隔), 也可从 msg_log 表复制 payload_hex:").pack(anchor="w")
self.decode_input = tk.Text(top, height=6)
self.decode_input.pack(fill="x", pady=2)
btns = ttk.Frame(top)
btns.pack(fill="x")
ttk.Button(btns, text="解码", command=self._run_decode).pack(side="left")
ttk.Button(btns, text="从 msg_log 加载示例", command=self._load_decode_samples).pack(side="left", padx=6)
ttk.Button(btns, text="清空", command=lambda: self.decode_input.delete("1.0", "end")).pack(side="left")
d_area = ttk.Frame(self.tab_decode)
d_area.pack(fill="both", expand=True)
self.decode_out = tk.Text(d_area, wrap="none", font=(_UI_MONO, 10))
ysb = ttk.Scrollbar(d_area, orient="vertical", command=self.decode_out.yview)
xsb = ttk.Scrollbar(d_area, orient="horizontal", command=self.decode_out.xview)
self.decode_out.configure(yscrollcommand=ysb.set, xscrollcommand=xsb.set)
self.decode_out.grid(row=0, column=0, sticky="nsew")
ysb.grid(row=0, column=1, sticky="ns")
xsb.grid(row=1, column=0, sticky="ew")
d_area.rowconfigure(0, weight=1)
d_area.columnconfigure(0, weight=1)
def _run_decode(self):
text = self.decode_input.get("1.0", "end")
tokens = text.split()
if not tokens:
self.decode_out.insert("end", "请输入 hex 帧。\n")
return
self.decode_out.delete("1.0", "end")
for t in tokens:
for line in decode_frame(t):
self.decode_out.insert("end", line + "\n")
self.decode_out.insert("end", "-" * 70 + "\n")
def _load_decode_samples(self):
if not self.conn:
messagebox.showinfo("提示", "请先打开数据库。")
return
rows = self.conn.execute(
"SELECT msg_id, payload_hex FROM msg_log GROUP BY msg_id LIMIT 6"
).fetchall()
if not rows:
messagebox.showinfo("提示", "msg_log 表为空。")
return
samples = [r[1] for r in rows]
self.decode_input.delete("1.0", "end")
self.decode_input.insert("end", "\n".join(samples))
self.set_status("已加载 %d 个不同消息类型的示例帧" % len(samples))
# ---------- 趋势图 ----------
def _build_plot_tab(self):
top = ttk.Frame(self.tab_plot)
top.pack(fill="x", padx=4, pady=4)
ttk.Label(top, text="表").pack(side="left")
self.plot_table_var = tk.StringVar()
self.plot_table_combo = ttk.Combobox(top, textvariable=self.plot_table_var, width=22, state="readonly")
self.plot_table_combo.pack(side="left", padx=4)
self.plot_table_combo.bind("<<ComboboxSelected>>", self._plot_on_table)
ttk.Label(top, text="数值列").pack(side="left")
self.plot_col_var = tk.StringVar()
self.plot_col_combo = ttk.Combobox(top, textvariable=self.plot_col_var, width=24, state="readonly")
self.plot_col_combo.pack(side="left", padx=4)
ttk.Label(top, text="时间桶").pack(side="left")
self.plot_bucket_var = tk.StringVar(value="原始")
ttk.Combobox(top, textvariable=self.plot_bucket_var, width=10, state="readonly",
values=("原始", "1分钟", "10分钟", "1小时", "1天")).pack(side="left", padx=4)
ttk.Button(top, text="绘图", command=self._run_plot).pack(side="left", padx=6)
self.plot_canvas = tk.Canvas(self.tab_plot, bg="white")
self.plot_canvas.pack(fill="both", expand=True, padx=4, pady=4)
def _plot_on_table(self, _event=None):
table = self.plot_table_var.get()
if not self.conn or not table:
return
cols = table_columns(self.conn, table)
# 只挑数值列
num_cols = []
for c in cols:
r = self.conn.execute("SELECT %s FROM %s WHERE %s IS NOT NULL LIMIT 1" % (c, table, c)).fetchone()
if r and isinstance(r[0], (int, float)):
num_cols.append(c)
self.plot_col_combo["values"] = num_cols
if num_cols:
self.plot_col_var.set(num_cols[0])
def _refresh_plot_tables(self):
if not self.conn:
return
names = [t[0] for t in list_tables(self.conn)]
self.plot_table_combo["values"] = names
if names:
self.plot_table_var.set(names[0])
self._plot_on_table()
def _run_plot(self):
if not self.conn:
return
table = self.plot_table_var.get()
col = self.plot_col_var.get()
if not table or not col:
return
tcol = None
cols = table_columns(self.conn, table)
for cand in ("timestamp", "ts_local", "ts"):
if cand in cols:
tcol = cand
break
if tcol is None:
messagebox.showinfo("提示", "该表没有可识别的时间列。")
return
try:
rows = self.conn.execute(
"SELECT %s, %s FROM %s WHERE %s IS NOT NULL" % (tcol, col, table, col)
).fetchall()
except Exception as e:
messagebox.showerror("查询失败", str(e))
return
pts = []
for tv, v in rows:
if not isinstance(v, (int, float)):
continue
try:
if tcol == "ts":
t = datetime.fromtimestamp(tv)
else:
t = datetime.strptime(str(tv), "%Y-%m-%d %H:%M:%S")
except Exception:
continue
pts.append((t, v))
if not pts:
messagebox.showinfo("提示", "没有可绘制的数据点。")
return
pts.sort(key=lambda x: x[0])
bucket = self.plot_bucket_var.get()
if bucket != "原始":
pts = self._bucket_points(pts, bucket)
self._draw_plot(pts, col)
def _bucket_points(self, pts, bucket):
if bucket == "1分钟":
sec = 60
elif bucket == "10分钟":
sec = 600
elif bucket == "1小时":
sec = 3600
elif bucket == "1天":
sec = 86400
else:
sec = 1
buckets = {}
for t, v in pts:
key = int(t.timestamp() // sec) * sec
buckets.setdefault(key, []).append(v)
out = []
for k in sorted(buckets):
vs = buckets[k]
out.append((datetime.fromtimestamp(k), sum(vs) / len(vs)))
return out
def _draw_plot(self, pts, col):
c = self.plot_canvas
c.delete("all")
W = c.winfo_width() or 800
H = c.winfo_height() or 500
pad_l, pad_r, pad_t, pad_b = 60, 20, 20, 40
plot_w = W - pad_l - pad_r
plot_h = H - pad_t - pad_b
if plot_w <= 0 or plot_h <= 0:
return
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]
ymin, ymax = min(ys), max(ys)
if ymin == ymax:
ymax = ymin + 1
xmin, xmax = xs[0], xs[-1]
span = (xmax - xmin).total_seconds() or 1
def sx(t):
return pad_l + (t - xmin).total_seconds() / span * plot_w
def sy(v):
return pad_t + (ymax - v) / (ymax - ymin) * plot_h
# 网格与轴
c.create_line(pad_l, pad_t + plot_h, W - pad_r, pad_t + plot_h, fill="black")
c.create_line(pad_l, pad_t, pad_l, pad_t + plot_h, fill="black")
for i in range(5):
yv = ymin + (ymax - ymin) * i / 4
yy = sy(yv)
c.create_line(pad_l, yy, W - pad_r, yy, fill="#dddddd")
c.create_text(pad_l - 5, yy, text="%.4g" % yv, anchor="e", font=("Arial", 8))
# 时间标签(首/中/尾)
for frac in (0.0, 0.5, 1.0):
t = xmin + timedelta(seconds=span * frac)
xx = pad_l + plot_w * frac
c.create_text(xx, pad_t + plot_h + 15, text=t.strftime("%m-%d %H:%M"), font=("Arial", 8))
# 折线
coords = []
for t, v in pts:
coords.append(sx(t))
coords.append(sy(v))
if len(coords) >= 4:
c.create_line(*coords, fill="#1f77b4", width=1.5)
c.create_text(pad_l + plot_w / 2, pad_t - 8, text="%s (%s, %d 点)" % (col, self.plot_bucket_var.get(), len(pts)),
font=(_UI_SANS, 10))
def _pick_font(candidates, default):
"""从候选字体族中选取第一个实际可用的, 否则返回默认。"""
if tk is None:
return default
import tkinter.font as tkfont
available = set(tkfont.families())
for fam in candidates:
if fam in available:
return fam
return default
def setup_fonts(root):
"""设置全局默认字体为支持中文的字体, 避免中文显示为方块。"""
import tkinter.font as tkfont
cjk = _pick_font(["Noto Sans CJK SC", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei",
"Microsoft YaHei", "SimHei", "Source Han Sans SC"], "TkDefaultFont")
cjk_mono = _pick_font(["Noto Sans Mono CJK SC", "WenQuanYi Micro Hei Mono",
"Microsoft YaHei", "SimHei"], "TkFixedFont")
default_font = tkfont.nametofont("TkDefaultFont")
default_font.configure(family=cjk, size=10)
fixed_font = tkfont.nametofont("TkFixedFont")
fixed_font.configure(family=cjk_mono, size=10)
style = ttk.Style(root)
try:
style.theme_use("clam")
except Exception:
pass
style.configure(".", font=(cjk, 10))
style.configure("Treeview", font=(cjk, 10), rowheight=24)
style.configure("Treeview.Heading", font=(cjk, 10, "bold"))
style.configure("TNotebook.Tab", font=(cjk, 10))
style.configure("TLabel", font=(cjk, 10))
style.configure("TButton", font=(cjk, 10))
style.configure("TCheckbutton", font=(cjk, 10))
style.configure("TCombobox", font=(cjk, 10))
style.configure("TEntry", font=(cjk, 10))
root.option_add("*Font", (cjk, 10))
root.option_add("*Text.Font", (cjk_mono, 10))
global _UI_SANS, _UI_MONO
_UI_SANS = cjk
_UI_MONO = cjk_mono
return cjk, cjk_mono
def main():
if tk is None:
print("未检测到 tkinter。请安装系统包后重试:")
print(" sudo apt install python3-tk")
return 1
root = tk.Tk()
setup_fonts(root)
app = PowerDbApp(root, default_db_path())
root.mainloop()
return 0
if __name__ == "__main__":
raise SystemExit(main())