#include "pqdif_thread_processor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // PQDIF 解析库 #include "pqdif/PQDIF.h" #include "pqdif/include/pqdif_ph.h" #include "pqdif/include/pqdif_id.h" #include "pqdif/include/pqdif_lg.h" #include "pqdif_semantic_ids.h" namespace fs = std::experimental::filesystem; namespace { constexpr int kScanIntervalSec = 60; constexpr int kBackupLimit = 4800; constexpr int kMaxPqdifFilesPerScan = 1; constexpr size_t kParsedCacheLimit = 128; const char* kPqdRootDir = "download"; const char* kDoneRootDir = "download_done"; const char* kFailRootDir = "download_fail"; std::deque g_parsed_cache; std::mutex g_parsed_cache_mutex; // PQDIF 统计桶 Base64 文件级“生成队列”。 // 对象用途:解析线程在完成一个 PQDIF 文件的桶数据组装后,会把一个 // PqdifStatBase64FileBatch 放入这个队列。 // 数据粒度:一个队列元素 = 一个 PQDIF 文件批次;批次内部再按 // “时间点 -> Max/Min/Avg/P95 子记录”保存,避免不同 PQDIF 文件的数据混合。 constexpr size_t kPqdifStatBase64QueueLimit = 128; std::deque g_pqdif_stat_base64_queue; std::mutex g_pqdif_stat_base64_mutex; // PQDIF 统计桶 Base64 文件级“待后续处理队列”。 // 对象用途:RunPqdifScanLoop() 每轮循环末尾会从生成队列取出最多一个 // PqdifStatBase64FileBatch,并移动到这个队列中,后续入库/上传/推送逻辑 // 可以从这里取数据。 // 设计原因:避免在扫描解析队列上直接做耗时业务处理,同时保证取出的文件批次 // 不会因为局部变量析构而丢失。 std::deque g_pqdif_stat_base64_ready_queue; std::mutex g_pqdif_stat_base64_ready_mutex; std::string guid_to_string(const GUID& g) { char buf[64] = { 0 }; std::snprintf(buf, sizeof(buf), "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", static_cast(g.Data1), static_cast(g.Data2), static_cast(g.Data3), static_cast(g.Data4[0]), static_cast(g.Data4[1]), static_cast(g.Data4[2]), static_cast(g.Data4[3]), static_cast(g.Data4[4]), static_cast(g.Data4[5]), static_cast(g.Data4[6]), static_cast(g.Data4[7])); return std::string(buf); } std::string safe_tag_name(const GUID& tag) { const char* name = theInfo.GetNameOfTag(tag); if (name != nullptr && name[0] != '\0') return std::string(name); return guid_to_string(tag); } PqdifGuidValue make_guid_value(const GUID& guid) { PqdifGuidValue out; out.value = guid; out.symbolic_name = safe_tag_name(guid); return out; } std::string format_time_text(time_t ts) { if (ts == 0) return std::string(); std::tm tm_value{}; #if defined(_WIN32) localtime_s(&tm_value, &ts); #else std::tm* ptm = std::localtime(&ts); if (ptm == nullptr) return std::string(); tm_value = *ptm; #endif char buf[32] = { 0 }; if (std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_value) == 0) return std::string(); return std::string(buf); } PqdifTimestampValue make_timestamp_value(CPQDIF& file_convert, const TIMESTAMPPQDIF& ts) { PqdifTimestampValue out; out.day = ts.day; out.second = ts.sec; if (ts.day != 0 || std::fabs(ts.sec) > 1e-12) { DATE dt = static_cast(ts.day) + (ts.sec / static_cast(SECONDS_PER_DAY)); file_convert.GetTime(dt, &out.unix_time); out.text = format_time_text(out.unix_time); } return out; } PqdifRecordHeaderInfo build_record_header_info(CPQDIFRecord* record, long record_index) { PqdifRecordHeaderInfo info; info.record_index = record_index; if (record == nullptr) return info; LINKABS4 pos = 0; LINKABS4 next_pos = 0; SIZE4 size_header = 0; SIZE4 size_data = 0; UINT checksum = 0; GUID record_tag{}; record->HeaderGetPos(pos); record->HeaderGetTag(record_tag); record->HeaderGetSize(size_header, size_data); record->HeaderGetPosNextRecord(next_pos); record->HeaderGetChecksum(checksum); info.file_position = static_cast(pos); info.record_type = make_guid_value(record_tag); info.header_size = static_cast(size_header); info.data_size = static_cast(size_data); info.next_record_position = static_cast(next_pos); info.checksum = static_cast(checksum); return info; } std::string trim_trailing_nulls(const std::string& value) { std::string out = value; while (!out.empty() && out.back() == '\0') out.pop_back(); return out; } bool dump_file_basic_info(const std::string& file_path, std::string& err) { std::error_code ec; fs::path p(file_path); if (!fs::exists(p, ec)) { err = "file not exists"; return false; } if (!fs::is_regular_file(p, ec)) { err = "not a regular file"; return false; } auto file_size = fs::file_size(p, ec); if (ec) { err = "cannot get file size"; return false; } std::ifstream ifs(file_path, std::ios::binary); if (!ifs.is_open()) { err = "ifstream open failed"; return false; } std::cout << "[PQDIF] file basic info: path=" << file_path << ", size=" << file_size << std::endl; unsigned char buf[32] = { 0 }; ifs.read(reinterpret_cast(buf), sizeof(buf)); std::streamsize n = ifs.gcount(); std::ostringstream oss; oss << "[PQDIF] first " << n << " bytes: "; for (std::streamsize i = 0; i < n; ++i) { oss << std::hex << std::setw(2) << std::setfill('0') << static_cast(buf[i]) << " "; } std::cout << oss.str() << std::endl; return true; } std::string to_upper_copy(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); return s; } std::string trim_copy(const std::string& s) { size_t beg = 0; while (beg < s.size() && std::isspace(static_cast(s[beg]))) ++beg; size_t end = s.size(); while (end > beg && std::isspace(static_cast(s[end - 1]))) --end; return s.substr(beg, end - beg); } std::string normalize_key(const std::string& src) { std::string out; out.reserve(src.size()); for (char c : src) { unsigned char uc = static_cast(c); if (std::isalnum(uc) || c == '[' || c == ']') out.push_back(static_cast(std::toupper(uc))); } return out; } bool is_pqdif_file(const fs::path& path) { if (!fs::is_regular_file(path)) return false; const std::string ext = to_upper_copy(path.extension().string()); return ext == ".PQD" || ext == ".PQDIF"; } bool ensure_dir(const fs::path& dir) { std::error_code ec; if (fs::exists(dir, ec)) return true; return fs::create_directories(dir, ec) || fs::exists(dir, ec); } bool move_file_with_fallback(const fs::path& src, const fs::path& dst) { std::error_code ec; ensure_dir(dst.parent_path()); fs::rename(src, dst, ec); if (!ec) return true; ec.clear(); fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); if (ec) return false; ec.clear(); fs::remove(src, ec); return !ec; } void cleanup_backup_dir(const fs::path& dir, int limit) { if (limit < 0) return; std::error_code ec; if (!fs::exists(dir, ec) || !fs::is_directory(dir, ec)) return; std::vector files; for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec)) { if (!ec && is_pqdif_file(it->path())) files.push_back(it->path()); } if (files.size() <= static_cast(limit)) return; std::sort(files.begin(), files.end(), [](const fs::path& a, const fs::path& b) { std::error_code ea, eb; return fs::last_write_time(a, ea) < fs::last_write_time(b, eb); }); const size_t remove_count = files.size() - static_cast(limit); for (size_t i = 0; i < remove_count; ++i) { std::error_code remove_ec; fs::remove(files[i], remove_ec); } } bool push_parsed_result_to_cache(ParsedPqdifFile&& parsed) { std::lock_guard guard(g_parsed_cache_mutex); if (g_parsed_cache.size() >= kParsedCacheLimit) { std::cout << "[PQDIF] cache full, drop oldest: " << g_parsed_cache.front().source_file << std::endl; g_parsed_cache.pop_front(); } g_parsed_cache.emplace_back(std::move(parsed)); return true; } bool read_scalar_raw(CPQDIF_E_Collection* collection, const GUID& tag, long& physical_type, PQDIFValue& value) { if (collection == nullptr) return false; CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_SCALAR); if (element == nullptr) return false; CPQDIF_E_Scalar* scalar = static_cast(element); return scalar->GetValue(physical_type, value); } bool read_string_tag(CPQDIF_E_Collection* collection, const GUID& tag, std::string& out) { if (collection == nullptr) return false; CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_VECTOR); if (element == nullptr) return false; CPQDIF_E_Vector* vector = static_cast(element); std::string text; if (!vector->GetValues(text)) return false; out = trim_trailing_nulls(text); return true; } bool read_uint_tag(CPQDIF_E_Collection* collection, const GUID& tag, unsigned int& out) { long physical_type = -1; PQDIFValue value{}; if (!read_scalar_raw(collection, tag, physical_type, value)) return false; switch (physical_type) { case ID_PHYS_TYPE_UNS_INTEGER1: out = static_cast(value.uint1); return true; case ID_PHYS_TYPE_UNS_INTEGER2: out = static_cast(value.uint2); return true; case ID_PHYS_TYPE_UNS_INTEGER4: out = static_cast(value.uint4); return true; case ID_PHYS_TYPE_INTEGER1: out = static_cast(value.int1); return true; case ID_PHYS_TYPE_INTEGER2: out = static_cast(value.int2); return true; case ID_PHYS_TYPE_INTEGER4: out = static_cast(value.int4); return true; default: return false; } } bool read_int_tag(CPQDIF_E_Collection* collection, const GUID& tag, int& out) { long physical_type = -1; PQDIFValue value{}; if (!read_scalar_raw(collection, tag, physical_type, value)) return false; switch (physical_type) { case ID_PHYS_TYPE_INTEGER1: out = static_cast(value.int1); return true; case ID_PHYS_TYPE_INTEGER2: out = static_cast(value.int2); return true; case ID_PHYS_TYPE_INTEGER4: out = static_cast(value.int4); return true; case ID_PHYS_TYPE_UNS_INTEGER1: out = static_cast(value.uint1); return true; case ID_PHYS_TYPE_UNS_INTEGER2: out = static_cast(value.uint2); return true; case ID_PHYS_TYPE_UNS_INTEGER4: out = static_cast(value.uint4); return true; default: return false; } } bool read_bool_tag(CPQDIF_E_Collection* collection, const GUID& tag, bool& out) { long physical_type = -1; PQDIFValue value{}; if (!read_scalar_raw(collection, tag, physical_type, value)) return false; switch (physical_type) { case ID_PHYS_TYPE_BOOLEAN1: out = (value.bool1 != 0); return true; case ID_PHYS_TYPE_BOOLEAN2: out = (value.bool2 != 0); return true; case ID_PHYS_TYPE_BOOLEAN4: out = (value.bool4 != 0); return true; default: return false; } } bool read_double_tag(CPQDIF_E_Collection* collection, const GUID& tag, double& out) { long physical_type = -1; PQDIFValue value{}; if (!read_scalar_raw(collection, tag, physical_type, value)) return false; switch (physical_type) { case ID_PHYS_TYPE_REAL4: out = static_cast(value.real4); return true; case ID_PHYS_TYPE_REAL8: out = value.real8; return true; case ID_PHYS_TYPE_INTEGER1: out = static_cast(value.int1); return true; case ID_PHYS_TYPE_INTEGER2: out = static_cast(value.int2); return true; case ID_PHYS_TYPE_INTEGER4: out = static_cast(value.int4); return true; case ID_PHYS_TYPE_UNS_INTEGER1: out = static_cast(value.uint1); return true; case ID_PHYS_TYPE_UNS_INTEGER2: out = static_cast(value.uint2); return true; case ID_PHYS_TYPE_UNS_INTEGER4: out = static_cast(value.uint4); return true; default: return false; } } bool read_guid_tag(CPQDIF_E_Collection* collection, const GUID& tag, PqdifGuidValue& out) { long physical_type = -1; PQDIFValue value{}; if (!read_scalar_raw(collection, tag, physical_type, value)) return false; if (physical_type != ID_PHYS_TYPE_GUID) return false; out = make_guid_value(value.guid); return true; } bool read_timestamp_tag(CPQDIF_E_Collection* collection, const GUID& tag, CPQDIF& file_convert, PqdifTimestampValue& out) { long physical_type = -1; PQDIFValue value{}; if (!read_scalar_raw(collection, tag, physical_type, value)) return false; if (physical_type != ID_PHYS_TYPE_TIMESTAMPPQDIF) return false; out = make_timestamp_value(file_convert, value.ts); return true; } std::vector read_vector_uint_values(CPQDIF_E_Collection* collection, const GUID& tag) { std::vector out; if (collection == nullptr) return out; CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_VECTOR); if (element == nullptr) return out; CPQDIF_E_Vector* vector = static_cast(element); long count = 0; vector->GetCount(count); for (long i = 0; i < count; ++i) { PQDIFValue value{}; if (!vector->GetValue(i, value)) continue; switch (vector->GetPhysicalType()) { case ID_PHYS_TYPE_UNS_INTEGER1: out.push_back(static_cast(value.uint1)); break; case ID_PHYS_TYPE_UNS_INTEGER2: out.push_back(static_cast(value.uint2)); break; case ID_PHYS_TYPE_UNS_INTEGER4: out.push_back(static_cast(value.uint4)); break; case ID_PHYS_TYPE_INTEGER1: out.push_back(static_cast(value.int1)); break; case ID_PHYS_TYPE_INTEGER2: out.push_back(static_cast(value.int2)); break; case ID_PHYS_TYPE_INTEGER4: out.push_back(static_cast(value.int4)); break; default: break; } } return out; } std::vector read_vector_int_values(CPQDIF_E_Collection* collection, const GUID& tag) { std::vector out; if (collection == nullptr) return out; CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_VECTOR); if (element == nullptr) return out; CPQDIF_E_Vector* vector = static_cast(element); long count = 0; vector->GetCount(count); for (long i = 0; i < count; ++i) { PQDIFValue value{}; if (!vector->GetValue(i, value)) continue; switch (vector->GetPhysicalType()) { case ID_PHYS_TYPE_INTEGER1: out.push_back(static_cast(value.int1)); break; case ID_PHYS_TYPE_INTEGER2: out.push_back(static_cast(value.int2)); break; case ID_PHYS_TYPE_INTEGER4: out.push_back(static_cast(value.int4)); break; case ID_PHYS_TYPE_UNS_INTEGER1: out.push_back(static_cast(value.uint1)); break; case ID_PHYS_TYPE_UNS_INTEGER2: out.push_back(static_cast(value.uint2)); break; case ID_PHYS_TYPE_UNS_INTEGER4: out.push_back(static_cast(value.uint4)); break; default: break; } } return out; } PqdifValueArray extract_vector_values(CPQDIF_E_Vector* vector, CPQDIF& file_convert) { PqdifValueArray out; if (vector == nullptr) return out; out.physical_type = vector->GetPhysicalType(); vector->GetCount(out.count); if (out.physical_type == ID_PHYS_TYPE_CHAR1 || out.physical_type == ID_PHYS_TYPE_CHAR2) { std::string text; if (vector->GetValues(text)) out.text_values.push_back(trim_trailing_nulls(text)); return out; } for (long i = 0; i < out.count; ++i) { PQDIFValue value{}; if (!vector->GetValue(i, value)) continue; switch (out.physical_type) { case ID_PHYS_TYPE_BOOLEAN1: out.bool_values.push_back(value.bool1 != 0); break; case ID_PHYS_TYPE_BOOLEAN2: out.bool_values.push_back(value.bool2 != 0); break; case ID_PHYS_TYPE_BOOLEAN4: out.bool_values.push_back(value.bool4 != 0); break; case ID_PHYS_TYPE_INTEGER1: out.int_values.push_back(static_cast(value.int1)); break; case ID_PHYS_TYPE_INTEGER2: out.int_values.push_back(static_cast(value.int2)); break; case ID_PHYS_TYPE_INTEGER4: out.int_values.push_back(static_cast(value.int4)); break; case ID_PHYS_TYPE_UNS_INTEGER1: out.uint_values.push_back(static_cast(value.uint1)); break; case ID_PHYS_TYPE_UNS_INTEGER2: out.uint_values.push_back(static_cast(value.uint2)); break; case ID_PHYS_TYPE_UNS_INTEGER4: out.uint_values.push_back(static_cast(value.uint4)); break; case ID_PHYS_TYPE_REAL4: out.real_values.push_back(static_cast(value.real4)); break; case ID_PHYS_TYPE_REAL8: out.real_values.push_back(value.real8); break; case ID_PHYS_TYPE_COMPLEX8: out.complex_values.push_back(std::complex(value.complex8.real, value.complex8.image)); break; case ID_PHYS_TYPE_COMPLEX16: out.complex_values.push_back(std::complex(value.complex16.real, value.complex16.image)); break; case ID_PHYS_TYPE_TIMESTAMPPQDIF: out.timestamp_values.push_back(make_timestamp_value(file_convert, value.ts)); break; case ID_PHYS_TYPE_GUID: out.guid_values.push_back(make_guid_value(value.guid)); break; default: break; } } return out; } std::string scalar_value_to_text(long physical_type, const PQDIFValue& value, CPQDIF& file_convert) { std::ostringstream oss; switch (physical_type) { case ID_PHYS_TYPE_BOOLEAN1: oss << (value.bool1 ? "true" : "false"); break; case ID_PHYS_TYPE_BOOLEAN2: oss << (value.bool2 ? "true" : "false"); break; case ID_PHYS_TYPE_BOOLEAN4: oss << (value.bool4 ? "true" : "false"); break; case ID_PHYS_TYPE_INTEGER1: oss << static_cast(value.int1); break; case ID_PHYS_TYPE_INTEGER2: oss << value.int2; break; case ID_PHYS_TYPE_INTEGER4: oss << value.int4; break; case ID_PHYS_TYPE_UNS_INTEGER1: oss << static_cast(value.uint1); break; case ID_PHYS_TYPE_UNS_INTEGER2: oss << value.uint2; break; case ID_PHYS_TYPE_UNS_INTEGER4: oss << value.uint4; break; case ID_PHYS_TYPE_REAL4: oss << value.real4; break; case ID_PHYS_TYPE_REAL8: oss << value.real8; break; case ID_PHYS_TYPE_COMPLEX8: oss << value.complex8.real << "+" << value.complex8.image << "j"; break; case ID_PHYS_TYPE_COMPLEX16: oss << value.complex16.real << "+" << value.complex16.image << "j"; break; case ID_PHYS_TYPE_TIMESTAMPPQDIF: { PqdifTimestampValue ts = make_timestamp_value(file_convert, value.ts); oss << ts.text; break; } case ID_PHYS_TYPE_GUID: oss << safe_tag_name(value.guid) << "(" << guid_to_string(value.guid) << ")"; break; default: oss << "physical_type=" << physical_type; break; } return oss.str(); } std::string element_to_text(CPQDIF_Element* element, CPQDIF& file_convert) { if (element == nullptr) return std::string(); std::ostringstream oss; if (element->GetElementType() == ID_ELEMENT_TYPE_SCALAR) { CPQDIF_E_Scalar* scalar = static_cast(element); long physical_type = -1; PQDIFValue value{}; if (scalar->GetValue(physical_type, value)) return scalar_value_to_text(physical_type, value, file_convert); return std::string(); } if (element->GetElementType() == ID_ELEMENT_TYPE_VECTOR) { CPQDIF_E_Vector* vector = static_cast(element); PqdifValueArray values = extract_vector_values(vector, file_convert); oss << "vector(type=" << values.physical_type << ", count=" << values.count << ")"; size_t shown = 0; auto append_preview = [&](const std::string& text) { if (shown == 0) oss << " ["; else oss << ", "; oss << text; ++shown; if (shown == 5) return false; return true; }; for (size_t i = 0; i < values.real_values.size() && shown < 5; ++i) append_preview(std::to_string(values.real_values[i])); for (size_t i = 0; i < values.int_values.size() && shown < 5; ++i) append_preview(std::to_string(values.int_values[i])); for (size_t i = 0; i < values.uint_values.size() && shown < 5; ++i) append_preview(std::to_string(values.uint_values[i])); for (size_t i = 0; i < values.bool_values.size() && shown < 5; ++i) append_preview(values.bool_values[i] ? "true" : "false"); for (size_t i = 0; i < values.text_values.size() && shown < 5; ++i) append_preview(values.text_values[i]); for (size_t i = 0; i < values.timestamp_values.size() && shown < 5; ++i) append_preview(values.timestamp_values[i].text); for (size_t i = 0; i < values.guid_values.size() && shown < 5; ++i) append_preview(values.guid_values[i].symbolic_name); if (shown > 0) oss << (values.count > 5 ? ", ...]" : "]"); return oss.str(); } if (element->GetElementType() == ID_ELEMENT_TYPE_COLLECTION) { CPQDIF_E_Collection* collection = static_cast(element); oss << "collection(count=" << collection->GetCount() << ")"; return oss.str(); } return std::string(); } void collect_extra_tags(CPQDIF_E_Collection* collection, const std::set& known_tag_names, CPQDIF& file_convert, PqdifExtraTagMap& out) { out.clear(); if (collection == nullptr) return; const long count = collection->GetCount(); for (long i = 0; i < count; ++i) { CPQDIF_Element* element = collection->GetElement(i); if (element == nullptr) continue; const std::string tag_name = safe_tag_name(element->GetTag()); if (known_tag_names.find(tag_name) != known_tag_names.end()) continue; out[tag_name] = element_to_text(element, file_convert); } } void dump_logical_summary(const ParsedPqdifFile& parsed_file) { std::cout << "========== PQDIF LOGICAL SUMMARY ==========" << std::endl; std::cout << "file=" << parsed_file.source_file << ", records=" << parsed_file.logical_file.record_headers.size() << ", containers=" << parsed_file.logical_file.containers.size() << ", data_sources=" << parsed_file.logical_file.data_sources.size() << ", settings=" << parsed_file.logical_file.monitor_settings.size() << ", observations=" << parsed_file.logical_file.observations.size() << std::endl; if (!parsed_file.logical_file.observations.empty()) { const PqdifObservationRecord& obs = parsed_file.logical_file.observations.front(); std::cout << " first_observation=" << obs.observation_name << ", start=" << obs.time_start.text << ", channels=" << obs.channel_instances.size() << std::endl; } std::cout << "==========================================" << std::endl; } /// @brief PQDIF 日志级别。 /// @details /// 后续新增打印时请按级别输出: /// - Core:默认核心摘要,必须短,适合平时测试; /// - Info:处理流程、fallback 命中情况、文件移动等; /// - Debug:observation 列表、疑似通道、指标来源排查; /// - Trace:逐通道/逐序列/全量 bucket 明细,日志量很大。 enum class PqdifLogLevel { Core = 0, Info = 1, Debug = 2, Trace = 3 }; bool pqdif_env_truthy(const char* v) { if (v == nullptr) return false; std::string s(v); for (char& ch : s) ch = static_cast(std::toupper(static_cast(ch))); return s == "1" || s == "TRUE" || s == "YES" || s == "ON" || s == "DEBUG" || s == "DETAIL" || s == "TRACE" || s == "VERBOSE"; } PqdifLogLevel pqdif_parse_log_level(const char* v) { if (v == nullptr || *v == '\0') return PqdifLogLevel::Core; std::string s(v); for (char& ch : s) ch = static_cast(std::toupper(static_cast(ch))); if (s == "0" || s == "CORE" || s == "QUIET") return PqdifLogLevel::Core; if (s == "1" || s == "INFO") return PqdifLogLevel::Info; if (s == "2" || s == "DEBUG" || s == "DETAIL") return PqdifLogLevel::Debug; if (s == "3" || s == "TRACE" || s == "VERBOSE") return PqdifLogLevel::Trace; return pqdif_env_truthy(v) ? PqdifLogLevel::Debug : PqdifLogLevel::Core; } PqdifLogLevel pqdif_current_log_level() { static int cached = -1; if (cached >= 0) return static_cast(cached); PqdifLogLevel level = pqdif_parse_log_level(std::getenv("PQDIF_LOG_LEVEL")); // 向后兼容旧开关。 if (level == PqdifLogLevel::Core) { if (pqdif_env_truthy(std::getenv("PQDIF_DETAIL_LOG"))) level = PqdifLogLevel::Debug; if (pqdif_env_truthy(std::getenv("PQDIF_VERBOSE"))) level = PqdifLogLevel::Trace; } cached = static_cast(level); return level; } bool pqdif_log_enabled(PqdifLogLevel level) { return static_cast(pqdif_current_log_level()) >= static_cast(level); } /// @brief 是否开启详细调试日志。保留旧函数名,便于已有打印逻辑复用。 bool pqdif_is_detail_log_enabled() { return pqdif_log_enabled(PqdifLogLevel::Debug); } bool pqdif_is_trace_log_enabled() { return pqdif_log_enabled(PqdifLogLevel::Trace); } std::string short_guid_name(const PqdifGuidValue& g) { if (!g.symbolic_name.empty()) return g.symbolic_name; return guid_to_string(g.value); } std::string preview_value_array(const PqdifValueArray& v) { std::ostringstream oss; oss << "physical_type=" << v.physical_type << ", count=" << v.count; auto append_real_preview = [&](const std::vector& arr, const char* name) { if (arr.empty()) return; oss << ", " << name << "=["; for (size_t i = 0; i < arr.size() && i < 3; ++i) { if (i > 0) oss << ", "; oss << arr[i]; } if (arr.size() > 3) oss << ", ..."; oss << "]"; }; auto append_int_preview = [&](const std::vector& arr, const char* name) { if (arr.empty()) return; oss << ", " << name << "=["; for (size_t i = 0; i < arr.size() && i < 3; ++i) { if (i > 0) oss << ", "; oss << arr[i]; } if (arr.size() > 3) oss << ", ..."; oss << "]"; }; auto append_uint_preview = [&](const std::vector& arr, const char* name) { if (arr.empty()) return; oss << ", " << name << "=["; for (size_t i = 0; i < arr.size() && i < 3; ++i) { if (i > 0) oss << ", "; oss << arr[i]; } if (arr.size() > 3) oss << ", ..."; oss << "]"; }; auto append_time_preview = [&](const std::vector& arr, const char* name) { if (arr.empty()) return; oss << ", " << name << "=["; for (size_t i = 0; i < arr.size() && i < 3; ++i) { if (i > 0) oss << ", "; oss << arr[i].text; } if (arr.size() > 3) oss << ", ..."; oss << "]"; }; append_real_preview(v.real_values, "real"); append_int_preview(v.int_values, "int"); append_uint_preview(v.uint_values, "uint"); append_time_preview(v.timestamp_values, "time"); if (!v.text_values.empty()) { oss << ", text=["; for (size_t i = 0; i < v.text_values.size() && i < 2; ++i) { if (i > 0) oss << ", "; oss << "\"" << v.text_values[i] << "\""; } if (v.text_values.size() > 2) oss << ", ..."; oss << "]"; } return oss.str(); } void dump_semantic_probe(const ParsedPqdifFile& parsed_file) { const auto& lf = parsed_file.logical_file; const auto& reg = pqdif_sem::GetGuidSemanticRegistry(); std::cout << "========== PQDIF SEMANTIC PROBE ==========" << std::endl; std::cout << "file=" << parsed_file.source_file << std::endl; std::cout << "data_sources=" << lf.data_sources.size() << ", observations=" << lf.observations.size() << std::endl; // -------------------------------------------------------------------- // 1) 打印第一条数据源的前几个通道定义 // -------------------------------------------------------------------- if (!lf.data_sources.empty()) { const auto& ds = lf.data_sources.front(); std::cout << "[DEF] data_source[0]" << " name=" << ds.name << ", model=" << ds.instrument_model_name << ", channels=" << ds.channel_definitions.size() << std::endl; const size_t ch_limit = std::min(ds.channel_definitions.size(), 3); for (size_t i = 0; i < ch_limit; ++i) { const auto& ch = ds.channel_definitions[i]; std::cout << " [CH-DEF " << i << "]" << " name=" << ch.channel_name << ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id) << ", qty_type=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityType, ch.quantity_type_id.value, short_guid_name(ch.quantity_type_id).c_str()) << ", series_defs=" << ch.series_definitions.size() << std::endl; const size_t sd_limit = std::min(ch.series_definitions.size(), 5); for (size_t j = 0; j < sd_limit; ++j) { const auto& sd = ch.series_definitions[j]; std::cout << " [SER-DEF " << j << "]" << " value_type=" << reg.FindName( pqdif_sem::GuidSemanticField::ValueType, sd.value_type_id.value, short_guid_name(sd.value_type_id).c_str()) << ", characteristic=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityCharacteristic, sd.quantity_characteristic_id.value, short_guid_name(sd.quantity_characteristic_id).c_str()) << ", unit=" << pqdif_sem::FindQuantityUnitsName(sd.quantity_units_id) << ", storage=" << pqdif_sem::FindStorageMethodFlagsName(sd.storage_method_id) << ", nominal=" << sd.nominal_quantity << std::endl; } } } // -------------------------------------------------------------------- // 2) 打印第一条 observation 的前几个通道实例 // -------------------------------------------------------------------- if (!lf.observations.empty()) { const auto& obs = lf.observations.front(); std::cout << "[OBS] observation[0]" << " name=" << obs.observation_name << ", start=" << obs.time_start.text << ", trigger=" << pqdif_sem::FindTriggerMethodName(obs.trigger_method_id) << ", channels=" << obs.channel_instances.size() << std::endl; const size_t ch_limit = std::min(obs.channel_instances.size(), 3); for (size_t i = 0; i < ch_limit; ++i) { const auto& ch = obs.channel_instances[i]; std::cout << " [CH-INS " << i << "]" << " name=" << ch.channel_name << ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id) << ", qty_type=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityType, ch.quantity_type_id.value, short_guid_name(ch.quantity_type_id).c_str()) << ", freq=" << ch.channel_frequency << ", group=" << ch.channel_group_id << ", series_instances=" << ch.series_instances.size() << std::endl; const size_t si_limit = std::min(ch.series_instances.size(), 5); for (size_t j = 0; j < si_limit; ++j) { const auto& si = ch.series_instances[j]; std::cout << " [SER-INS " << j << "]" << " value_type=" << reg.FindName( pqdif_sem::GuidSemanticField::ValueType, si.value_type_id.value, short_guid_name(si.value_type_id).c_str()) << ", characteristic=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityCharacteristic, si.quantity_characteristic_id.value, short_guid_name(si.quantity_characteristic_id).c_str()) << ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id) << ", storage=N/A" << ", " << preview_value_array(si.values) << ", share_ch=" << si.share_channel_index << ", share_ser=" << si.share_series_index << ", scale=" << si.scale << ", offset=" << si.offset << std::endl; } } } std::cout << "==========================================" << std::endl; } long max_series_value_count_in_channel(const PqdifChannelInstance& ch) { long max_count = 0; for (const auto& si : ch.series_instances) max_count = std::max(max_count, si.values.count); return max_count; } long max_series_value_count_in_observation(const PqdifObservationRecord& obs) { long max_count = 0; for (const auto& ch : obs.channel_instances) max_count = std::max(max_count, max_series_value_count_in_channel(ch)); return max_count; } const PqdifChannelDefinition* find_channel_definition( const PqdifLogicalFile& lf, const PqdifObservationRecord& obs, const PqdifChannelInstance& ch) { if (obs.related_data_source_index < 0 || obs.related_data_source_index >= static_cast(lf.data_sources.size())) return nullptr; const auto& ds = lf.data_sources[static_cast(obs.related_data_source_index)]; if (ch.channel_def_index < 0 || ch.channel_def_index >= static_cast(ds.channel_definitions.size())) return nullptr; return &ds.channel_definitions[static_cast(ch.channel_def_index)]; } bool is_integer_in_range(double value, int min_value, int max_value) { if (!std::isfinite(value)) return false; const double rounded = std::floor(value + 0.5); if (std::fabs(value - rounded) > 1e-6) return false; const int n = static_cast(rounded); return n >= min_value && n <= max_value; } std::string harmonic_order_token(int order) { char buf[8] = { 0 }; std::snprintf(buf, sizeof(buf), "%02d", order); return std::string(buf); } bool text_looks_like_harmonic(const std::string& text) { const std::string key = normalize_key(text); if (key.empty()) return false; if (key.find("HARM") != std::string::npos || key.find("HARMONIC") != std::string::npos || key.find("THD") != std::string::npos || key.find("IHD") != std::string::npos || key.find("VHD") != std::string::npos || key.find("HRMS") != std::string::npos) return true; for (int order = 2; order <= 50; ++order) { const std::string two = harmonic_order_token(order); const std::string plain = std::to_string(order); const std::string patterns[] = { "H" + two, "H" + plain, "HD" + two, "HD" + plain, "VH" + two, "VH" + plain, "UH" + two, "UH" + plain, "IH" + two, "IH" + plain }; for (const auto& pat : patterns) { if (key.find(pat) != std::string::npos) return true; } } return false; } bool extra_tags_look_like_harmonic(const PqdifExtraTagMap& tags) { for (const auto& kv : tags) { if (text_looks_like_harmonic(kv.first) || text_looks_like_harmonic(kv.second)) return true; } return false; } bool series_definition_looks_like_harmonic(const PqdifSeriesDefinition& sd) { if (is_integer_in_range(sd.nominal_quantity, 2, 50)) return true; if (text_looks_like_harmonic(sd.value_type_name) || text_looks_like_harmonic(sd.value_type_id.symbolic_name) || text_looks_like_harmonic(sd.quantity_characteristic_id.symbolic_name) || extra_tags_look_like_harmonic(sd.extra_tags)) return true; return false; } bool series_instance_looks_like_harmonic(const PqdifSeriesInstance& si) { if (is_integer_in_range(si.series_base_quantity, 2, 50) || is_integer_in_range(si.nominal_quantity, 2, 50)) return true; if (text_looks_like_harmonic(si.value_type_id.symbolic_name) || text_looks_like_harmonic(si.quantity_characteristic_id.symbolic_name) || extra_tags_look_like_harmonic(si.extra_tags)) return true; return false; } bool channel_definition_looks_like_harmonic(const PqdifChannelDefinition& ch) { if (text_looks_like_harmonic(ch.channel_name) || text_looks_like_harmonic(ch.quantity_name) || text_looks_like_harmonic(ch.group_name) || text_looks_like_harmonic(ch.other_channel_identifier) || text_looks_like_harmonic(ch.quantity_type_id.symbolic_name) || extra_tags_look_like_harmonic(ch.extra_tags)) return true; for (const auto& sd : ch.series_definitions) { if (series_definition_looks_like_harmonic(sd)) return true; } return false; } bool channel_instance_looks_like_harmonic( const PqdifChannelInstance& ch, const PqdifChannelDefinition* def) { if (text_looks_like_harmonic(ch.channel_name) || text_looks_like_harmonic(ch.quantity_type_id.symbolic_name) || extra_tags_look_like_harmonic(ch.extra_tags)) return true; if (def != nullptr && channel_definition_looks_like_harmonic(*def)) return true; for (const auto& si : ch.series_instances) { if (series_instance_looks_like_harmonic(si)) return true; } return false; } void dump_observation_list(const ParsedPqdifFile& parsed_file) { const auto& lf = parsed_file.logical_file; std::cout << "========== PQDIF OBSERVATION LIST ==========" << std::endl; std::cout << "file=" << parsed_file.source_file << ", observation_count=" << lf.observations.size() << std::endl; for (size_t i = 0; i < lf.observations.size(); ++i) { const auto& obs = lf.observations[i]; std::cout << " [OBS " << i << "]" << " index=" << obs.observation_index << ", record=" << obs.record_index << ", name=" << obs.observation_name << ", start=" << obs.time_start.text << ", create=" << obs.time_create.text << ", trigger=" << pqdif_sem::FindTriggerMethodName(obs.trigger_method_id) << ", triggered=" << obs.time_triggered.text << ", channels=" << obs.channel_instances.size() << ", max_series_count=" << max_series_value_count_in_observation(obs) << ", related_ds=" << obs.related_data_source_index << ", related_settings=" << obs.related_settings_index << std::endl; const size_t preview_count = std::min(obs.channel_instances.size(), 10); if (preview_count > 0) { std::cout << " channel_preview="; for (size_t j = 0; j < preview_count; ++j) { const auto& ch = obs.channel_instances[j]; if (j > 0) std::cout << " | "; std::cout << "#" << ch.channel_instance_index << ":" << ch.channel_name; } if (obs.channel_instances.size() > preview_count) std::cout << " | ..."; std::cout << std::endl; } } std::cout << "============================================" << std::endl; } void dump_harmonic_channel_probe(const ParsedPqdifFile& parsed_file) { const auto& lf = parsed_file.logical_file; const auto& reg = pqdif_sem::GetGuidSemanticRegistry(); std::cout << "========== PQDIF HARMONIC CHANNEL PROBE ==========" << std::endl; std::cout << "file=" << parsed_file.source_file << std::endl; std::cout << "rule=print channel definitions/instances whose name, GUID name, extra tags, " << "series_base_quantity or nominal_quantity looks like harmonic order 2-50" << std::endl; size_t def_hits = 0; for (size_t ds_idx = 0; ds_idx < lf.data_sources.size(); ++ds_idx) { const auto& ds = lf.data_sources[ds_idx]; for (size_t ch_idx = 0; ch_idx < ds.channel_definitions.size(); ++ch_idx) { const auto& ch = ds.channel_definitions[ch_idx]; if (!channel_definition_looks_like_harmonic(ch)) continue; ++def_hits; std::cout << " [HARM-DEF " << def_hits << "]" << " ds=" << ds_idx << ", ch_def=" << ch.channel_def_index << ", name=" << ch.channel_name << ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id) << ", qty_type=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityType, ch.quantity_type_id.value, short_guid_name(ch.quantity_type_id).c_str()) << ", physical_ch=" << ch.physical_channel << ", primary_series=" << ch.primary_series_index << ", series_defs=" << ch.series_definitions.size() << std::endl; const size_t sd_limit = std::min(ch.series_definitions.size(), 8); for (size_t j = 0; j < sd_limit; ++j) { const auto& sd = ch.series_definitions[j]; std::cout << " [HARM-SER-DEF " << j << "]" << " value_type=" << reg.FindName( pqdif_sem::GuidSemanticField::ValueType, sd.value_type_id.value, short_guid_name(sd.value_type_id).c_str()) << ", characteristic=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityCharacteristic, sd.quantity_characteristic_id.value, short_guid_name(sd.quantity_characteristic_id).c_str()) << ", unit=" << pqdif_sem::FindQuantityUnitsName(sd.quantity_units_id) << ", storage=" << pqdif_sem::FindStorageMethodFlagsName(sd.storage_method_id) << ", nominal=" << sd.nominal_quantity << ", percentile=" << sd.prob_percentile << std::endl; } } } size_t inst_hits = 0; for (size_t obs_idx = 0; obs_idx < lf.observations.size(); ++obs_idx) { const auto& obs = lf.observations[obs_idx]; for (size_t ch_idx = 0; ch_idx < obs.channel_instances.size(); ++ch_idx) { const auto& ch = obs.channel_instances[ch_idx]; const PqdifChannelDefinition* def = find_channel_definition(lf, obs, ch); if (!channel_instance_looks_like_harmonic(ch, def)) continue; ++inst_hits; std::cout << " [HARM-INS " << inst_hits << "]" << " obs=" << obs_idx << ", obs_name=" << obs.observation_name << ", obs_start=" << obs.time_start.text << ", ch=" << ch.channel_instance_index << ", ch_def=" << ch.channel_def_index << ", name=" << ch.channel_name << ", def_name=" << (def != nullptr ? def->channel_name : std::string()) << ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id) << ", qty_type=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityType, ch.quantity_type_id.value, short_guid_name(ch.quantity_type_id).c_str()) << ", group=" << ch.channel_group_id << ", freq=" << ch.channel_frequency << ", primary_series=" << ch.primary_series_index << ", series_instances=" << ch.series_instances.size() << std::endl; const size_t si_limit = std::min(ch.series_instances.size(), 8); for (size_t j = 0; j < si_limit; ++j) { const auto& si = ch.series_instances[j]; std::cout << " [HARM-SER-INS " << j << "]" << " value_type=" << reg.FindName( pqdif_sem::GuidSemanticField::ValueType, si.value_type_id.value, short_guid_name(si.value_type_id).c_str()) << ", characteristic=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityCharacteristic, si.quantity_characteristic_id.value, short_guid_name(si.quantity_characteristic_id).c_str()) << ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id) << ", base_q=" << si.series_base_quantity << ", nominal=" << si.nominal_quantity << ", scale=" << si.scale << ", offset=" << si.offset << ", share_ch=" << si.share_channel_index << ", share_ser=" << si.share_series_index << ", " << preview_value_array(si.values) << std::endl; } } } if (def_hits == 0 && inst_hits == 0) { std::cout << " [HARMONIC PROBE RESULT] no suspected harmonic channel found in definitions or observations" << std::endl; } else { std::cout << " [HARMONIC PROBE RESULT] definition_candidates=" << def_hits << ", observation_channel_candidates=" << inst_hits << std::endl; } std::cout << "=================================================" << std::endl; } int stat_make_channel_spectrum_order_hint( const PqdifObservationRecord& obs, const PqdifChannelInstance& ch, int& block_offset, int& block_size); bool pqdif_probe_guid_is_harmonic_characteristic(const GUID& g) { return PQDIF_IsEqualGUID(g, ID_QC_HRMS) || PQDIF_IsEqualGUID(g, ID_QC_SPECTRA) || PQDIF_IsEqualGUID(g, ID_QC_SPECTRA_HGROUP); } bool pqdif_probe_guid_is_interharmonic_characteristic(const GUID& g) { return PQDIF_IsEqualGUID(g, ID_QC_SPECTRA_IGROUP); } bool pqdif_probe_guid_is_phase_angle_value_type(const GUID& g) { return PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE) || PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE_MIN) || PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE_MAX) || PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE_AVG); } bool pqdif_probe_text_looks_like_interharmonic(const std::string& text) { const std::string key = normalize_key(text); return key.find("INTERHARM") != std::string::npos || key.find("INTERHARMONIC") != std::string::npos || key.find("IHARM") != std::string::npos || key.find("IGROUP") != std::string::npos || key.find("IHGROUP") != std::string::npos; } bool pqdif_probe_text_looks_like_angle(const std::string& text) { const std::string key = normalize_key(text); return key.find("ANGLE") != std::string::npos || key.find("PHASEANGLE") != std::string::npos || key.find("PANGLE") != std::string::npos || key.find("HANGLE") != std::string::npos; } bool pqdif_probe_phase_is_line_voltage(unsigned int phase_id, const std::string& name) { if (phase_id == ID_PHASE_AB || phase_id == ID_PHASE_BC || phase_id == ID_PHASE_CA) return true; const std::string key = normalize_key(name); return key.find("UAB") != std::string::npos || key.find("VAB") != std::string::npos || key.find("UBC") != std::string::npos || key.find("VBC") != std::string::npos || key.find("UCA") != std::string::npos || key.find("VCA") != std::string::npos || key.find("AB") != std::string::npos || key.find("BC") != std::string::npos || key.find("CA") != std::string::npos; } bool pqdif_probe_series_def_is_dynamic_candidate(const PqdifSeriesDefinition& sd) { return pqdif_probe_guid_is_harmonic_characteristic(sd.quantity_characteristic_id.value) || pqdif_probe_guid_is_interharmonic_characteristic(sd.quantity_characteristic_id.value) || pqdif_probe_guid_is_phase_angle_value_type(sd.value_type_id.value) || sd.quantity_units_id == ID_QU_DEGREES || text_looks_like_harmonic(sd.quantity_characteristic_id.symbolic_name) || pqdif_probe_text_looks_like_interharmonic(sd.quantity_characteristic_id.symbolic_name) || pqdif_probe_text_looks_like_angle(sd.value_type_id.symbolic_name); } bool pqdif_probe_series_ins_is_dynamic_candidate(const PqdifSeriesInstance& si) { return pqdif_probe_guid_is_harmonic_characteristic(si.quantity_characteristic_id.value) || pqdif_probe_guid_is_interharmonic_characteristic(si.quantity_characteristic_id.value) || pqdif_probe_guid_is_phase_angle_value_type(si.value_type_id.value) || si.quantity_units_id == ID_QU_DEGREES || is_integer_in_range(si.series_base_quantity, 2, 50) || is_integer_in_range(si.nominal_quantity, 2, 50) || text_looks_like_harmonic(si.quantity_characteristic_id.symbolic_name) || pqdif_probe_text_looks_like_interharmonic(si.quantity_characteristic_id.symbolic_name) || pqdif_probe_text_looks_like_angle(si.value_type_id.symbolic_name); } void dump_dynamic_spectrum_candidate_probe(const ParsedPqdifFile& parsed_file) { const auto& lf = parsed_file.logical_file; const auto& reg = pqdif_sem::GetGuidSemanticRegistry(); std::cout << "========== PQDIF DYNAMIC SPECTRUM CANDIDATE PROBE ==========" << std::endl; std::cout << "file=" << parsed_file.source_file << std::endl; std::cout << "rule=DEBUG level: print all channel definitions/instances that look like harmonic, angle, interharmonic, spectra, HRMS, HGroup or IGroup" << std::endl; size_t def_hits = 0; size_t def_line_like = 0; size_t def_inter_like = 0; for (size_t ds_idx = 0; ds_idx < lf.data_sources.size(); ++ds_idx) { const auto& ds = lf.data_sources[ds_idx]; for (size_t ch_idx = 0; ch_idx < ds.channel_definitions.size(); ++ch_idx) { const auto& ch = ds.channel_definitions[ch_idx]; bool candidate = channel_definition_looks_like_harmonic(ch) || pqdif_probe_text_looks_like_interharmonic(ch.channel_name) || pqdif_probe_text_looks_like_angle(ch.channel_name); for (const auto& sd : ch.series_definitions) candidate = candidate || pqdif_probe_series_def_is_dynamic_candidate(sd); if (!candidate) continue; const bool line_like = pqdif_probe_phase_is_line_voltage(ch.phase_id, ch.channel_name); bool inter_like = pqdif_probe_text_looks_like_interharmonic(ch.channel_name); bool angle_like = pqdif_probe_text_looks_like_angle(ch.channel_name); bool harmonic_like = text_looks_like_harmonic(ch.channel_name); for (const auto& sd : ch.series_definitions) { inter_like = inter_like || pqdif_probe_guid_is_interharmonic_characteristic(sd.quantity_characteristic_id.value) || pqdif_probe_text_looks_like_interharmonic(sd.quantity_characteristic_id.symbolic_name); angle_like = angle_like || pqdif_probe_guid_is_phase_angle_value_type(sd.value_type_id.value) || sd.quantity_units_id == ID_QU_DEGREES || pqdif_probe_text_looks_like_angle(sd.value_type_id.symbolic_name); harmonic_like = harmonic_like || pqdif_probe_guid_is_harmonic_characteristic(sd.quantity_characteristic_id.value) || text_looks_like_harmonic(sd.quantity_characteristic_id.symbolic_name); } ++def_hits; if (line_like) ++def_line_like; if (inter_like) ++def_inter_like; std::cout << " [DYN-DEF " << def_hits << "]" << " ds=" << ds_idx << ", ch_def=" << ch.channel_def_index << ", name=" << ch.channel_name << ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id) << ", line_like=" << (line_like ? "true" : "false") << ", harmonic_like=" << (harmonic_like ? "true" : "false") << ", interharmonic_like=" << (inter_like ? "true" : "false") << ", angle_like=" << (angle_like ? "true" : "false") << ", series_defs=" << ch.series_definitions.size() << std::endl; for (size_t j = 0; j < ch.series_definitions.size(); ++j) { const auto& sd = ch.series_definitions[j]; if (!pqdif_probe_series_def_is_dynamic_candidate(sd) && !pqdif_is_trace_log_enabled()) continue; std::cout << " [DYN-SER-DEF " << j << "]" << " value_type=" << reg.FindName( pqdif_sem::GuidSemanticField::ValueType, sd.value_type_id.value, short_guid_name(sd.value_type_id).c_str()) << ", characteristic=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityCharacteristic, sd.quantity_characteristic_id.value, short_guid_name(sd.quantity_characteristic_id).c_str()) << ", unit=" << pqdif_sem::FindQuantityUnitsName(sd.quantity_units_id) << ", nominal=" << sd.nominal_quantity << ", percentile=" << sd.prob_percentile << std::endl; } } } size_t inst_hits = 0; size_t inst_line_like = 0; size_t inst_inter_like = 0; for (size_t obs_idx = 0; obs_idx < lf.observations.size(); ++obs_idx) { const auto& obs = lf.observations[obs_idx]; for (size_t ch_idx = 0; ch_idx < obs.channel_instances.size(); ++ch_idx) { const auto& ch = obs.channel_instances[ch_idx]; const PqdifChannelDefinition* def = find_channel_definition(lf, obs, ch); bool candidate = channel_instance_looks_like_harmonic(ch, def) || pqdif_probe_text_looks_like_interharmonic(ch.channel_name) || pqdif_probe_text_looks_like_angle(ch.channel_name); for (const auto& si : ch.series_instances) candidate = candidate || pqdif_probe_series_ins_is_dynamic_candidate(si); if (!candidate) continue; const bool line_like = pqdif_probe_phase_is_line_voltage(ch.phase_id, ch.channel_name); bool inter_like = pqdif_probe_text_looks_like_interharmonic(ch.channel_name); bool angle_like = pqdif_probe_text_looks_like_angle(ch.channel_name); bool harmonic_like = text_looks_like_harmonic(ch.channel_name); for (const auto& si : ch.series_instances) { inter_like = inter_like || pqdif_probe_guid_is_interharmonic_characteristic(si.quantity_characteristic_id.value) || pqdif_probe_text_looks_like_interharmonic(si.quantity_characteristic_id.symbolic_name); angle_like = angle_like || pqdif_probe_guid_is_phase_angle_value_type(si.value_type_id.value) || si.quantity_units_id == ID_QU_DEGREES || pqdif_probe_text_looks_like_angle(si.value_type_id.symbolic_name); harmonic_like = harmonic_like || pqdif_probe_guid_is_harmonic_characteristic(si.quantity_characteristic_id.value) || text_looks_like_harmonic(si.quantity_characteristic_id.symbolic_name); } int hint_offset = -1; int hint_size = 0; const int hint_order = stat_make_channel_spectrum_order_hint(obs, ch, hint_offset, hint_size); ++inst_hits; if (line_like) ++inst_line_like; if (inter_like) ++inst_inter_like; std::cout << " [DYN-INS " << inst_hits << "]" << " obs=" << obs_idx << ", obs_name=" << obs.observation_name << ", ch=" << ch.channel_instance_index << ", ch_def=" << ch.channel_def_index << ", name=" << ch.channel_name << ", def_name=" << (def != nullptr ? def->channel_name : std::string()) << ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id) << ", group=" << ch.channel_group_id << ", order_hint=" << hint_order << ", hint_offset=" << hint_offset << ", hint_block_size=" << hint_size << ", line_like=" << (line_like ? "true" : "false") << ", harmonic_like=" << (harmonic_like ? "true" : "false") << ", interharmonic_like=" << (inter_like ? "true" : "false") << ", angle_like=" << (angle_like ? "true" : "false") << ", series_instances=" << ch.series_instances.size() << std::endl; for (size_t j = 0; j < ch.series_instances.size(); ++j) { const auto& si = ch.series_instances[j]; if (!pqdif_probe_series_ins_is_dynamic_candidate(si) && !pqdif_is_trace_log_enabled()) continue; std::cout << " [DYN-SER-INS " << j << "]" << " value_type=" << reg.FindName( pqdif_sem::GuidSemanticField::ValueType, si.value_type_id.value, short_guid_name(si.value_type_id).c_str()) << ", characteristic=" << reg.FindName( pqdif_sem::GuidSemanticField::QuantityCharacteristic, si.quantity_characteristic_id.value, short_guid_name(si.quantity_characteristic_id).c_str()) << ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id) << ", base_q=" << si.series_base_quantity << ", nominal=" << si.nominal_quantity << ", scale=" << si.scale << ", offset=" << si.offset << ", share_ch=" << si.share_channel_index << ", share_ser=" << si.share_series_index << ", " << preview_value_array(si.values) << std::endl; } } } std::cout << " [DYNAMIC SPECTRUM PROBE RESULT] definition_candidates=" << def_hits << ", definition_line_like=" << def_line_like << ", definition_interharmonic_like=" << def_inter_like << ", observation_channel_candidates=" << inst_hits << ", observation_line_like=" << inst_line_like << ", observation_interharmonic_like=" << inst_inter_like << std::endl; std::cout << "==========================================================" << std::endl; } void dump_monitor_settings_probe(const ParsedPqdifFile& parsed_file) { const auto& lf = parsed_file.logical_file; std::cout << "========== PQDIF MONITOR SETTINGS ==========" << std::endl; std::cout << "settings_count=" << lf.monitor_settings.size() << std::endl; for (size_t i = 0; i < lf.monitor_settings.size() && i < 3; ++i) { const auto& s = lf.monitor_settings[i]; std::cout << " [SET " << i << "]" << " effective=" << s.effective_time.text << ", nominal_freq=" << s.nominal_frequency << ", nominal_voltage=" << s.nominal_voltage << ", physical_connection=" << s.physical_connection << ", is_pcc=" << (s.is_pcc ? "true" : "false") << ", use_transducer=" << (s.use_transducer ? "true" : "false") << std::endl; } std::cout << "===========================================" << std::endl; } std::string stat_two_digit(int n); int stat_dynamic_metric_base(StatMetricId id) { return static_cast(id); } enum class StatDynamicMetricGroup { None = 0, VoltageHarmonic, LineVoltageHarmonic, CurrentHarmonic, VoltageHarmonicAngle, LineVoltageHarmonicAngle, CurrentHarmonicAngle, HarmonicActivePower, HarmonicReactivePower, HarmonicApparentPower, VoltageHarmonicRatio, LineVoltageHarmonicRatio, CurrentHarmonicRatio, VoltageInterharmonic, LineVoltageInterharmonic, CurrentInterharmonic }; struct StatDynamicMetricRange { StatDynamicMetricGroup group; int base; int phase_index; const char* phase_label; const char* prefix; bool interharmonic; }; const std::vector& stat_dynamic_metric_ranges() { static const std::vector ranges = { { StatDynamicMetricGroup::VoltageHarmonic, static_cast(StatMetricId::VoltageHarmonicUaBase), 0, "a", "U", false }, { StatDynamicMetricGroup::VoltageHarmonic, static_cast(StatMetricId::VoltageHarmonicUbBase), 1, "b", "U", false }, { StatDynamicMetricGroup::VoltageHarmonic, static_cast(StatMetricId::VoltageHarmonicUcBase), 2, "c", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonic, static_cast(StatMetricId::LineVoltageHarmonicUabBase), 0, "ab", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonic, static_cast(StatMetricId::LineVoltageHarmonicUbcBase), 1, "bc", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonic, static_cast(StatMetricId::LineVoltageHarmonicUcaBase), 2, "ca", "U", false }, { StatDynamicMetricGroup::CurrentHarmonic, static_cast(StatMetricId::CurrentHarmonicIaBase), 0, "a", "I", false }, { StatDynamicMetricGroup::CurrentHarmonic, static_cast(StatMetricId::CurrentHarmonicIbBase), 1, "b", "I", false }, { StatDynamicMetricGroup::CurrentHarmonic, static_cast(StatMetricId::CurrentHarmonicIcBase), 2, "c", "I", false }, { StatDynamicMetricGroup::VoltageHarmonicAngle, static_cast(StatMetricId::VoltageHarmonicAngleUaBase), 0, "a", "U", false }, { StatDynamicMetricGroup::VoltageHarmonicAngle, static_cast(StatMetricId::VoltageHarmonicAngleUbBase), 1, "b", "U", false }, { StatDynamicMetricGroup::VoltageHarmonicAngle, static_cast(StatMetricId::VoltageHarmonicAngleUcBase), 2, "c", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonicAngle, static_cast(StatMetricId::LineVoltageHarmonicAngleUabBase), 0, "ab", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonicAngle, static_cast(StatMetricId::LineVoltageHarmonicAngleUbcBase), 1, "bc", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonicAngle, static_cast(StatMetricId::LineVoltageHarmonicAngleUcaBase), 2, "ca", "U", false }, { StatDynamicMetricGroup::CurrentHarmonicAngle, static_cast(StatMetricId::CurrentHarmonicAngleIaBase), 0, "a", "I", false }, { StatDynamicMetricGroup::CurrentHarmonicAngle, static_cast(StatMetricId::CurrentHarmonicAngleIbBase), 1, "b", "I", false }, { StatDynamicMetricGroup::CurrentHarmonicAngle, static_cast(StatMetricId::CurrentHarmonicAngleIcBase), 2, "c", "I", false }, { StatDynamicMetricGroup::HarmonicActivePower, static_cast(StatMetricId::HarmonicActivePowerPaBase), 0, "a", "P", false }, { StatDynamicMetricGroup::HarmonicActivePower, static_cast(StatMetricId::HarmonicActivePowerPbBase), 1, "b", "P", false }, { StatDynamicMetricGroup::HarmonicActivePower, static_cast(StatMetricId::HarmonicActivePowerPcBase), 2, "c", "P", false }, { StatDynamicMetricGroup::HarmonicActivePower, static_cast(StatMetricId::HarmonicActivePowerTotalBase), 3, "total", "P", false }, { StatDynamicMetricGroup::HarmonicReactivePower, static_cast(StatMetricId::HarmonicReactivePowerQaBase), 0, "a", "Q", false }, { StatDynamicMetricGroup::HarmonicReactivePower, static_cast(StatMetricId::HarmonicReactivePowerQbBase), 1, "b", "Q", false }, { StatDynamicMetricGroup::HarmonicReactivePower, static_cast(StatMetricId::HarmonicReactivePowerQcBase), 2, "c", "Q", false }, { StatDynamicMetricGroup::HarmonicReactivePower, static_cast(StatMetricId::HarmonicReactivePowerTotalBase), 3, "total", "Q", false }, { StatDynamicMetricGroup::HarmonicApparentPower, static_cast(StatMetricId::HarmonicApparentPowerSaBase), 0, "a", "S", false }, { StatDynamicMetricGroup::HarmonicApparentPower, static_cast(StatMetricId::HarmonicApparentPowerSbBase), 1, "b", "S", false }, { StatDynamicMetricGroup::HarmonicApparentPower, static_cast(StatMetricId::HarmonicApparentPowerScBase), 2, "c", "S", false }, { StatDynamicMetricGroup::HarmonicApparentPower, static_cast(StatMetricId::HarmonicApparentPowerTotalBase), 3, "total", "S", false }, { StatDynamicMetricGroup::VoltageHarmonicRatio, static_cast(StatMetricId::VoltageHarmonicRatioUaBase), 0, "a", "U", false }, { StatDynamicMetricGroup::VoltageHarmonicRatio, static_cast(StatMetricId::VoltageHarmonicRatioUbBase), 1, "b", "U", false }, { StatDynamicMetricGroup::VoltageHarmonicRatio, static_cast(StatMetricId::VoltageHarmonicRatioUcBase), 2, "c", "U", false }, { StatDynamicMetricGroup::CurrentHarmonicRatio, static_cast(StatMetricId::CurrentHarmonicRatioIaBase), 0, "a", "I", false }, { StatDynamicMetricGroup::CurrentHarmonicRatio, static_cast(StatMetricId::CurrentHarmonicRatioIbBase), 1, "b", "I", false }, { StatDynamicMetricGroup::CurrentHarmonicRatio, static_cast(StatMetricId::CurrentHarmonicRatioIcBase), 2, "c", "I", false }, { StatDynamicMetricGroup::LineVoltageHarmonicRatio, static_cast(StatMetricId::LineVoltageHarmonicRatioUabBase), 0, "ab", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonicRatio, static_cast(StatMetricId::LineVoltageHarmonicRatioUbcBase), 1, "bc", "U", false }, { StatDynamicMetricGroup::LineVoltageHarmonicRatio, static_cast(StatMetricId::LineVoltageHarmonicRatioUcaBase), 2, "ca", "U", false }, { StatDynamicMetricGroup::VoltageInterharmonic, static_cast(StatMetricId::VoltageInterharmonicUaBase), 0, "a", "U", true }, { StatDynamicMetricGroup::VoltageInterharmonic, static_cast(StatMetricId::VoltageInterharmonicUbBase), 1, "b", "U", true }, { StatDynamicMetricGroup::VoltageInterharmonic, static_cast(StatMetricId::VoltageInterharmonicUcBase), 2, "c", "U", true }, { StatDynamicMetricGroup::LineVoltageInterharmonic, static_cast(StatMetricId::LineVoltageInterharmonicUabBase), 0, "ab", "U", true }, { StatDynamicMetricGroup::LineVoltageInterharmonic, static_cast(StatMetricId::LineVoltageInterharmonicUbcBase), 1, "bc", "U", true }, { StatDynamicMetricGroup::LineVoltageInterharmonic, static_cast(StatMetricId::LineVoltageInterharmonicUcaBase), 2, "ca", "U", true }, { StatDynamicMetricGroup::CurrentInterharmonic, static_cast(StatMetricId::CurrentInterharmonicIaBase), 0, "a", "I", true }, { StatDynamicMetricGroup::CurrentInterharmonic, static_cast(StatMetricId::CurrentInterharmonicIbBase), 1, "b", "I", true }, { StatDynamicMetricGroup::CurrentInterharmonic, static_cast(StatMetricId::CurrentInterharmonicIcBase), 2, "c", "I", true } }; return ranges; } int stat_dynamic_metric_min_offset(bool interharmonic) { return interharmonic ? 0 : 2; } int stat_dynamic_metric_max_offset(bool interharmonic) { return interharmonic ? 49 : 50; } const StatDynamicMetricRange* stat_find_dynamic_metric_range(StatMetricId id) { const int v = static_cast(id); for (const auto& r : stat_dynamic_metric_ranges()) { const int min_offset = stat_dynamic_metric_min_offset(r.interharmonic); const int max_offset = stat_dynamic_metric_max_offset(r.interharmonic); if (v >= r.base + min_offset && v <= r.base + max_offset) return &r; } return nullptr; } bool stat_is_dynamic_metric(StatMetricId id) { return stat_find_dynamic_metric_range(id) != nullptr; } bool stat_is_dynamic_metric_group(StatMetricId id, StatDynamicMetricGroup group) { const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id); return r != nullptr && r->group == group; } int stat_dynamic_metric_order_or_slot(StatMetricId id) { const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id); if (r == nullptr) return -1; return static_cast(id) - r->base; } double stat_dynamic_metric_order_value(StatMetricId id) { const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id); if (r == nullptr) return -1.0; const int offset = stat_dynamic_metric_order_or_slot(id); return r->interharmonic ? (static_cast(offset) + 0.5) : static_cast(offset); } StatMetricId stat_dynamic_metric_id(StatDynamicMetricGroup group, int phase_index, int offset) { for (const auto& r : stat_dynamic_metric_ranges()) { if (r.group != group || r.phase_index != phase_index) continue; const int min_offset = stat_dynamic_metric_min_offset(r.interharmonic); const int max_offset = stat_dynamic_metric_max_offset(r.interharmonic); if (offset < min_offset || offset > max_offset) return StatMetricId::Unknown; return static_cast(r.base + offset); } return StatMetricId::Unknown; } std::string stat_format_interharmonic_slot(int slot) { char buf[16] = { 0 }; std::snprintf(buf, sizeof(buf), "%02dp5", slot); return std::string(buf); } std::string stat_dynamic_metric_name(StatMetricId id) { const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id); if (r == nullptr) return "Unknown"; const int offset = stat_dynamic_metric_order_or_slot(id); std::string phase(r->phase_label); if (!phase.empty()) phase[0] = static_cast(std::toupper(static_cast(phase[0]))); std::ostringstream os; os << r->prefix << phase; switch (r->group) { case StatDynamicMetricGroup::VoltageHarmonic: case StatDynamicMetricGroup::LineVoltageHarmonic: case StatDynamicMetricGroup::CurrentHarmonic: os << "Harm" << stat_two_digit(offset); break; case StatDynamicMetricGroup::VoltageHarmonicAngle: case StatDynamicMetricGroup::LineVoltageHarmonicAngle: case StatDynamicMetricGroup::CurrentHarmonicAngle: os << "HarmAng" << stat_two_digit(offset); break; case StatDynamicMetricGroup::HarmonicActivePower: case StatDynamicMetricGroup::HarmonicReactivePower: case StatDynamicMetricGroup::HarmonicApparentPower: os << "Harm" << stat_two_digit(offset); break; case StatDynamicMetricGroup::VoltageHarmonicRatio: case StatDynamicMetricGroup::LineVoltageHarmonicRatio: case StatDynamicMetricGroup::CurrentHarmonicRatio: os << "HarmRatio" << stat_two_digit(offset); break; case StatDynamicMetricGroup::VoltageInterharmonic: case StatDynamicMetricGroup::LineVoltageInterharmonic: case StatDynamicMetricGroup::CurrentInterharmonic: os << "InterHarm" << stat_format_interharmonic_slot(offset); break; default: os << "Dynamic" << offset; break; } return os.str(); } std::vector stat_dynamic_metric_order_for_group(StatDynamicMetricGroup group) { std::vector out; for (const auto& r : stat_dynamic_metric_ranges()) { if (r.group != group) continue; const int min_offset = stat_dynamic_metric_min_offset(r.interharmonic); const int max_offset = stat_dynamic_metric_max_offset(r.interharmonic); for (int offset = min_offset; offset <= max_offset; ++offset) out.push_back(static_cast(r.base + offset)); } return out; } std::vector stat_all_dynamic_metric_order() { std::vector out; const std::vector groups = { StatDynamicMetricGroup::VoltageHarmonic, StatDynamicMetricGroup::LineVoltageHarmonic, StatDynamicMetricGroup::CurrentHarmonic, StatDynamicMetricGroup::VoltageHarmonicAngle, StatDynamicMetricGroup::LineVoltageHarmonicAngle, StatDynamicMetricGroup::CurrentHarmonicAngle, StatDynamicMetricGroup::HarmonicActivePower, StatDynamicMetricGroup::HarmonicReactivePower, StatDynamicMetricGroup::HarmonicApparentPower, StatDynamicMetricGroup::VoltageHarmonicRatio, StatDynamicMetricGroup::LineVoltageHarmonicRatio, StatDynamicMetricGroup::CurrentHarmonicRatio, StatDynamicMetricGroup::VoltageInterharmonic, StatDynamicMetricGroup::LineVoltageInterharmonic, StatDynamicMetricGroup::CurrentInterharmonic }; for (auto g : groups) { auto part = stat_dynamic_metric_order_for_group(g); out.insert(out.end(), part.begin(), part.end()); } return out; } /// @brief 兼容旧调用:判断三相电压 2-50 次谐波 RMS。 bool stat_is_voltage_harmonic_metric(StatMetricId id) { return stat_is_dynamic_metric_group(id, StatDynamicMetricGroup::VoltageHarmonic); } int stat_voltage_harmonic_order(StatMetricId id) { if (!stat_is_voltage_harmonic_metric(id)) return -1; return stat_dynamic_metric_order_or_slot(id); } int stat_voltage_harmonic_phase_index(StatMetricId id) { const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id); if (r == nullptr || r->group != StatDynamicMetricGroup::VoltageHarmonic) return -1; return r->phase_index; } StatMetricId stat_voltage_harmonic_metric_id(int phase_index, int order) { return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonic, phase_index, order); } std::string stat_voltage_harmonic_metric_name(StatMetricId id) { return stat_dynamic_metric_name(id); } /// @brief 将业务指标 ID 转为可读字符串。 /// @param id 业务指标枚举。 /// @return 指标名称字符串,便于调试日志输出。 std::string stat_metric_name(StatMetricId id) { if (stat_is_dynamic_metric(id)) return stat_dynamic_metric_name(id); switch (id) { case StatMetricId::UaRms: return "UaRms"; case StatMetricId::UbRms: return "UbRms"; case StatMetricId::UcRms: return "UcRms"; case StatMetricId::IaRms: return "IaRms"; case StatMetricId::IbRms: return "IbRms"; case StatMetricId::IcRms: return "IcRms"; case StatMetricId::UabRms: return "UabRms"; case StatMetricId::UbcRms: return "UbcRms"; case StatMetricId::UcaRms: return "UcaRms"; case StatMetricId::UaDeviation: return "UaDeviation"; case StatMetricId::UbDeviation: return "UbDeviation"; case StatMetricId::UcDeviation: return "UcDeviation"; case StatMetricId::UabDeviation: return "UabDeviation"; case StatMetricId::UbcDeviation: return "UbcDeviation"; case StatMetricId::UcaDeviation: return "UcaDeviation"; case StatMetricId::Frequency: return "Frequency"; case StatMetricId::FrequencyDeviation: return "FrequencyDeviation"; case StatMetricId::UZeroSeq: return "UZeroSeq"; case StatMetricId::UNegSeq: return "UNegSeq"; case StatMetricId::UPosSeq: return "UPosSeq"; case StatMetricId::UNegSeqUnbalance: return "UNegSeqUnbalance"; case StatMetricId::IZeroSeq: return "IZeroSeq"; case StatMetricId::INegSeq: return "INegSeq"; case StatMetricId::IPosSeq: return "IPosSeq"; case StatMetricId::INegSeqUnbalance: return "INegSeqUnbalance"; case StatMetricId::PaPower: return "PaPower"; case StatMetricId::PbPower: return "PbPower"; case StatMetricId::PcPower: return "PcPower"; case StatMetricId::PTotalPower: return "PTotalPower"; case StatMetricId::QaPower: return "QaPower"; case StatMetricId::QbPower: return "QbPower"; case StatMetricId::QcPower: return "QcPower"; case StatMetricId::QTotalPower: return "QTotalPower"; case StatMetricId::SaPower: return "SaPower"; case StatMetricId::SbPower: return "SbPower"; case StatMetricId::ScPower: return "ScPower"; case StatMetricId::STotalPower: return "STotalPower"; case StatMetricId::PFa: return "PFa"; case StatMetricId::PFb: return "PFb"; case StatMetricId::PFc: return "PFc"; case StatMetricId::PFTotal: return "PFTotal"; case StatMetricId::FundPFa: return "FundPFa"; case StatMetricId::FundPFb: return "FundPFb"; case StatMetricId::FundPFc: return "FundPFc"; case StatMetricId::FundPFTotal: return "FundPFTotal"; case StatMetricId::UaDvc: return "UaDvc"; case StatMetricId::UbDvc: return "UbDvc"; case StatMetricId::UcDvc: return "UcDvc"; case StatMetricId::UabDvc: return "UabDvc"; case StatMetricId::UbcDvc: return "UbcDvc"; case StatMetricId::UcaDvc: return "UcaDvc"; case StatMetricId::UaPst: return "UaPst"; case StatMetricId::UbPst: return "UbPst"; case StatMetricId::UcPst: return "UcPst"; case StatMetricId::UabPst: return "UabPst"; case StatMetricId::UbcPst: return "UbcPst"; case StatMetricId::UcaPst: return "UcaPst"; case StatMetricId::UaPlt: return "UaPlt"; case StatMetricId::UbPlt: return "UbPlt"; case StatMetricId::UcPlt: return "UcPlt"; case StatMetricId::UabPlt: return "UabPlt"; case StatMetricId::UbcPlt: return "UbcPlt"; case StatMetricId::UcaPlt: return "UcaPlt"; case StatMetricId::PaFundPower: return "PaFundPower"; case StatMetricId::PbFundPower: return "PbFundPower"; case StatMetricId::PcFundPower: return "PcFundPower"; case StatMetricId::PTotalFundPower: return "PTotalFundPower"; case StatMetricId::QaFundPower: return "QaFundPower"; case StatMetricId::QbFundPower: return "QbFundPower"; case StatMetricId::QcFundPower: return "QcFundPower"; case StatMetricId::QTotalFundPower: return "QTotalFundPower"; case StatMetricId::SaFundPower: return "SaFundPower"; case StatMetricId::SbFundPower: return "SbFundPower"; case StatMetricId::ScFundPower: return "ScFundPower"; case StatMetricId::STotalFundPower: return "STotalFundPower"; case StatMetricId::UaFundRms: return "UaFundRms"; case StatMetricId::UbFundRms: return "UbFundRms"; case StatMetricId::UcFundRms: return "UcFundRms"; case StatMetricId::UaFundAngle: return "UaFundAngle"; case StatMetricId::UbFundAngle: return "UbFundAngle"; case StatMetricId::UcFundAngle: return "UcFundAngle"; case StatMetricId::IaFundRms: return "IaFundRms"; case StatMetricId::IbFundRms: return "IbFundRms"; case StatMetricId::IcFundRms: return "IcFundRms"; case StatMetricId::IaFundAngle: return "IaFundAngle"; case StatMetricId::IbFundAngle: return "IbFundAngle"; case StatMetricId::IcFundAngle: return "IcFundAngle"; case StatMetricId::UabFundRms: return "UabFundRms"; case StatMetricId::UbcFundRms: return "UbcFundRms"; case StatMetricId::UcaFundRms: return "UcaFundRms"; case StatMetricId::UabFundAngle: return "UabFundAngle"; case StatMetricId::UbcFundAngle: return "UbcFundAngle"; case StatMetricId::UcaFundAngle: return "UcaFundAngle"; case StatMetricId::UaThd: return "UaThd"; case StatMetricId::UbThd: return "UbThd"; case StatMetricId::UcThd: return "UcThd"; case StatMetricId::IaThd: return "IaThd"; case StatMetricId::IbThd: return "IbThd"; case StatMetricId::IcThd: return "IcThd"; case StatMetricId::UabThd: return "UabThd"; case StatMetricId::UbcThd: return "UbcThd"; case StatMetricId::UcaThd: return "UcaThd"; default: return "Unknown"; } } /// @brief 将统计值类型转为可读字符串。 /// @param kind 统计值类型。 /// @return 统计值类型名称。 std::string stat_value_kind_name(StatValueKind kind) { switch (kind) { case StatValueKind::Min: return "Min"; case StatValueKind::Max: return "Max"; case StatValueKind::Avg: return "Avg"; case StatValueKind::P95: return "P95"; default: return "Unknown"; } } /// @brief 将接线方式枚举转为可读字符串。 /// @param kind 接线方式枚举。 /// @return 可读字符串。 std::string stat_connection_kind_name(ParsedConnectionKind kind) { switch (kind) { case ParsedConnectionKind::Wye: return "Wye"; case ParsedConnectionKind::Delta: return "Delta"; default: return "Unknown"; } } /// @brief 将指标质量状态转为可读字符串。 /// @param q 指标质量状态。 /// @return 可读质量状态名称。 std::string stat_metric_quality_name(StatMetricQuality q) { switch (q) { case StatMetricQuality::Normal: return "OK"; case StatMetricQuality::AllZero: return "ALL_ZERO"; case StatMetricQuality::DuplicateSource: return "DUPLICATE_SOURCE"; case StatMetricQuality::SuspiciousRange: return "SUSPICIOUS_RANGE"; case StatMetricQuality::Missing: return "MISSING"; default: return "UNKNOWN"; } } /// @brief 非谐波核心指标打印顺序。 /// @details 平时核心日志只打印这些指标,避免 2-50 次谐波全部展开导致日志过大。 const std::vector& stat_core_metric_print_order() { static const std::vector order = { StatMetricId::UaRms, StatMetricId::UbRms, StatMetricId::UcRms, StatMetricId::IaRms, StatMetricId::IbRms, StatMetricId::IcRms, StatMetricId::UabRms, StatMetricId::UbcRms, StatMetricId::UcaRms, StatMetricId::UaDeviation, StatMetricId::UbDeviation, StatMetricId::UcDeviation, StatMetricId::Frequency, StatMetricId::FrequencyDeviation, StatMetricId::UZeroSeq, StatMetricId::UNegSeq, StatMetricId::UPosSeq, StatMetricId::UNegSeqUnbalance, StatMetricId::IZeroSeq, StatMetricId::INegSeq, StatMetricId::IPosSeq, StatMetricId::INegSeqUnbalance, StatMetricId::PaPower, StatMetricId::PbPower, StatMetricId::PcPower, StatMetricId::PTotalPower, StatMetricId::QaPower, StatMetricId::QbPower, StatMetricId::QcPower, StatMetricId::QTotalPower, StatMetricId::SaPower, StatMetricId::SbPower, StatMetricId::ScPower, StatMetricId::STotalPower, StatMetricId::PFa, StatMetricId::PFb, StatMetricId::PFc, StatMetricId::PFTotal, StatMetricId::FundPFa, StatMetricId::FundPFb, StatMetricId::FundPFc, StatMetricId::FundPFTotal, StatMetricId::UaDvc, StatMetricId::UbDvc, StatMetricId::UcDvc, StatMetricId::UabDvc, StatMetricId::UbcDvc, StatMetricId::UcaDvc, StatMetricId::UaPst, StatMetricId::UbPst, StatMetricId::UcPst, StatMetricId::UabPst, StatMetricId::UbcPst, StatMetricId::UcaPst, StatMetricId::UaPlt, StatMetricId::UbPlt, StatMetricId::UcPlt, StatMetricId::UabPlt, StatMetricId::UbcPlt, StatMetricId::UcaPlt, StatMetricId::PaFundPower, StatMetricId::PbFundPower, StatMetricId::PcFundPower, StatMetricId::PTotalFundPower, StatMetricId::QaFundPower, StatMetricId::QbFundPower, StatMetricId::QcFundPower, StatMetricId::QTotalFundPower, StatMetricId::SaFundPower, StatMetricId::SbFundPower, StatMetricId::ScFundPower, StatMetricId::STotalFundPower, StatMetricId::UaFundRms, StatMetricId::UbFundRms, StatMetricId::UcFundRms, StatMetricId::UaFundAngle, StatMetricId::UbFundAngle, StatMetricId::UcFundAngle, StatMetricId::IaFundRms, StatMetricId::IbFundRms, StatMetricId::IcFundRms, StatMetricId::IaFundAngle, StatMetricId::IbFundAngle, StatMetricId::IcFundAngle, StatMetricId::UabFundRms, StatMetricId::UbcFundRms, StatMetricId::UcaFundRms, StatMetricId::UabFundAngle, StatMetricId::UbcFundAngle, StatMetricId::UcaFundAngle, StatMetricId::UaThd, StatMetricId::UbThd, StatMetricId::UcThd, StatMetricId::IaThd, StatMetricId::IbThd, StatMetricId::IcThd, StatMetricId::UabThd, StatMetricId::UbcThd, StatMetricId::UcaThd }; return order; } /// @brief 动态生成三相电压 2-50 次谐波指标顺序。 /// @details 兼容旧调用;新的谐波/间谐波族统一走 stat_dynamic_metric_order_for_group / stat_all_dynamic_metric_order。 std::vector stat_voltage_harmonic_metric_order() { return stat_dynamic_metric_order_for_group(StatDynamicMetricGroup::VoltageHarmonic); } /// @brief 当前全部主要指标打印顺序。 /// @details 核心 22 项 + 所有动态谐波/间谐波指标。动态指标不逐项写 enum,统一按族和区间生成。 const std::vector& stat_primary_metric_print_order() { static const std::vector order = []() { std::vector out = stat_core_metric_print_order(); const std::vector dynamic_metrics = stat_all_dynamic_metric_order(); out.insert(out.end(), dynamic_metrics.begin(), dynamic_metrics.end()); return out; }(); return order; } /// @brief 已声明但当前 14 项之外的扩展指标打印顺序。 /// @details 这些指标不会计入“14 项”,但如果后续规则命中,也会在日志末尾显示,便于扩展核查。 const std::vector& stat_extra_metric_print_order() { static const std::vector order = { StatMetricId::UabDeviation, StatMetricId::UbcDeviation, StatMetricId::UcaDeviation }; return order; } bool stat_is_line_voltage_metric(StatMetricId id) { return id == StatMetricId::UabRms || id == StatMetricId::UbcRms || id == StatMetricId::UcaRms; } bool stat_is_phase_voltage_metric(StatMetricId id) { return id == StatMetricId::UaRms || id == StatMetricId::UbRms || id == StatMetricId::UcRms; } bool stat_is_current_metric(StatMetricId id) { return id == StatMetricId::IaRms || id == StatMetricId::IbRms || id == StatMetricId::IcRms; } bool stat_is_frequency_metric(StatMetricId id) { return id == StatMetricId::Frequency || id == StatMetricId::FrequencyDeviation; } bool stat_is_voltage_sequence_component_metric(StatMetricId id) { return id == StatMetricId::UZeroSeq || id == StatMetricId::UNegSeq || id == StatMetricId::UPosSeq; } bool stat_is_voltage_sequence_unbalance_metric(StatMetricId id) { return id == StatMetricId::UNegSeqUnbalance; } bool stat_is_current_sequence_component_metric(StatMetricId id) { return id == StatMetricId::IZeroSeq || id == StatMetricId::INegSeq || id == StatMetricId::IPosSeq; } bool stat_is_current_sequence_unbalance_metric(StatMetricId id) { return id == StatMetricId::INegSeqUnbalance; } bool stat_allow_all_zero_without_score_penalty(StatMetricId id) { return stat_is_current_metric(id) || stat_is_dynamic_metric(id) || id == StatMetricId::UZeroSeq || id == StatMetricId::UNegSeq || id == StatMetricId::UNegSeqUnbalance || stat_is_current_sequence_component_metric(id) || stat_is_current_sequence_unbalance_metric(id); } bool stat_has_value_kind(const AggregatedStatValues& agg, StatValueKind kind) { switch (kind) { case StatValueKind::Min: return agg.has_min; case StatValueKind::Max: return agg.has_max; case StatValueKind::Avg: return agg.has_avg; case StatValueKind::P95: return agg.has_p95; default: return false; } } double stat_get_value_by_kind(const AggregatedStatValues& agg, StatValueKind kind) { switch (kind) { case StatValueKind::Min: return agg.min_value; case StatValueKind::Max: return agg.max_value; case StatValueKind::Avg: return agg.avg_value; case StatValueKind::P95: return agg.p95_value; default: return 0.0; } } /// @brief 根据 observation 找到其关联的数据源定义。 /// @param lf 已解析完成的完整 PQDIF 逻辑对象。 /// @param obs 当前 observation。 /// @return 关联到的数据源定义指针;找不到时返回 nullptr。 const PqdifDataSourceRecord* stat_find_related_data_source( const PqdifLogicalFile& lf, const PqdifObservationRecord& obs) { if (obs.related_data_source_index >= 0 && static_cast(obs.related_data_source_index) < lf.data_sources.size()) { return &lf.data_sources[static_cast(obs.related_data_source_index)]; } if (!lf.data_sources.empty()) return &lf.data_sources.front(); return nullptr; } /// @brief 根据通道定义索引查找通道定义。 /// @param ds 数据源定义指针。 /// @param channel_def_index 通道定义索引。 /// @return 通道定义指针;找不到时返回 nullptr。 const PqdifChannelDefinition* stat_find_channel_definition( const PqdifDataSourceRecord* ds, int channel_def_index) { if (ds == nullptr || channel_def_index < 0) return nullptr; const size_t idx = static_cast(channel_def_index); if (idx >= ds->channel_definitions.size()) return nullptr; return &ds->channel_definitions[idx]; } /// @brief 根据序列定义索引查找序列定义。 /// @param ch_def 通道定义指针。 /// @param series_def_index 序列定义索引。 /// @return 序列定义指针;找不到时返回 nullptr。 const PqdifSeriesDefinition* stat_find_series_definition( const PqdifChannelDefinition* ch_def, int series_def_index) { if (ch_def == nullptr || series_def_index < 0) return nullptr; const size_t idx = static_cast(series_def_index); if (idx >= ch_def->series_definitions.size()) return nullptr; return &ch_def->series_definitions[idx]; } /// @brief 根据 monitor settings 与相别分布识别接线方式。 /// @details /// 优先使用正式配置字段 physical_connection;若不可用,则回退到相别启发式判断。 /// @param lf 已解析完成的完整 PQDIF 逻辑对象。 /// @return 识别出的接线方式。 ParsedConnectionKind stat_classify_connection_kind(const PqdifLogicalFile& lf) { if (!lf.monitor_settings.empty()) { const unsigned int pc = lf.monitor_settings.front().physical_connection; switch (pc) { case ID_2_5ELEMENT_WYE: case ID_3ELMENT_WYE: return ParsedConnectionKind::Wye; case ID_2ELEMENT_DELTA: case ID_3ELEMENT_DELTA: return ParsedConnectionKind::Delta; default: break; } } bool has_phase_to_neutral = false; bool has_line_to_line = false; for (const auto& ds : lf.data_sources) { for (const auto& ch : ds.channel_definitions) { if (ch.phase_id == ID_PHASE_AN || ch.phase_id == ID_PHASE_BN || ch.phase_id == ID_PHASE_CN) has_phase_to_neutral = true; if (ch.phase_id == ID_PHASE_AB || ch.phase_id == ID_PHASE_BC || ch.phase_id == ID_PHASE_CA) has_line_to_line = true; } } if (has_phase_to_neutral) return ParsedConnectionKind::Wye; if (has_line_to_line) return ParsedConnectionKind::Delta; return ParsedConnectionKind::Unknown; } /// @brief 判断 observation 是否为当前阶段需要处理的“统计类 observation”。 /// @details /// 当前阶段不处理全部 observation,而是先筛出统计类 observation。判断顺序: /// 1) 触发方式为 ID_TRIGGER_METH_PERIODIC_STATS; /// 2) 名称中包含 TREND / STAT / STATISTIC; /// 3) 只要 observation 下有 VALUELOG 型通道,也认为它是统计 observation。 /// @param obs 当前 observation。 /// @param lf 完整 PQDIF 逻辑对象。 /// @return 若是统计类 observation,则返回 true。 bool stat_is_statistical_observation( const PqdifLogicalFile& lf, const PqdifObservationRecord& obs) { if (obs.trigger_method_id == ID_TRIGGER_METH_PERIODIC_STATS) return true; auto compact = [&](const std::string& s) { std::string out; for (unsigned char ch : s) { if (std::isalnum(ch)) out.push_back(static_cast(std::toupper(ch))); } return out; }; const std::string obs_name = compact(obs.observation_name); if (obs_name.find("TREND") != std::string::npos || obs_name.find("STAT") != std::string::npos || obs_name.find("STATISTIC") != std::string::npos) { return true; } for (const auto& ch : obs.channel_instances) { if (pqdif_sem::IsQuantityTypeValueLog(ch.quantity_type_id.value)) return true; } (void)lf; return false; } /// @brief 选择当前阶段要处理的“主统计 observation”。 /// @details /// 当前阶段为了避免不同 observation 在同一 timestamp 上相互覆盖, /// 只选择第一条命中的统计类 observation 参与识别与聚合。 /// @param lf 完整 PQDIF 逻辑对象。 /// @return 命中的 observation 指针;若没有则返回 nullptr。 const PqdifObservationRecord* stat_select_primary_statistical_observation(const PqdifLogicalFile& lf) { for (const auto& obs : lf.observations) { if (stat_is_statistical_observation(lf, obs)) return &obs; } return nullptr; } /// @brief 解析共享序列。 /// @details /// 某些序列自身没有 values,而是通过 share_channel_index/share_series_index /// 共享另一个序列的数组值,例如 B/C 相共享 A 相时间轴。 /// @param obs 当前 observation。 /// @param si 当前序列实例。 /// @return 最终应使用的“真实值序列实例”指针。 const PqdifSeriesInstance* stat_resolve_shared_series( const PqdifObservationRecord& obs, const PqdifSeriesInstance& si) { const PqdifSeriesInstance* current = &si; for (int depth = 0; depth < 8; ++depth) { if (current->values.count > 0) return current; if (current->share_channel_index < 0 || current->share_series_index < 0) return current; const size_t ch_idx = static_cast(current->share_channel_index); const size_t ser_idx = static_cast(current->share_series_index); if (ch_idx >= obs.channel_instances.size()) return current; const auto& shared_ch = obs.channel_instances[ch_idx]; if (ser_idx >= shared_ch.series_instances.size()) return current; const PqdifSeriesInstance* next = &shared_ch.series_instances[ser_idx]; if (next == current) return current; current = next; } return current; } /// @brief 从数组容器中读取指定下标的原始数值。 /// @details 支持 real/int/uint/bool。 /// @param arr 数组容器。 /// @param idx 样本下标。 /// @param out_raw 返回原始数值。 /// @return 成功读取返回 true,否则返回 false。 bool stat_try_get_raw_numeric_at(const PqdifValueArray& arr, size_t idx, double& out_raw) { if (idx < arr.real_values.size()) { out_raw = arr.real_values[idx]; return true; } if (idx < arr.int_values.size()) { out_raw = static_cast(arr.int_values[idx]); return true; } if (idx < arr.uint_values.size()) { out_raw = static_cast(arr.uint_values[idx]); return true; } if (idx < arr.bool_values.size()) { out_raw = arr.bool_values[idx] ? 1.0 : 0.0; return true; } return false; } /// @brief 将原始值还原为工程值。 /// @details /// 当序列存储方式包含 SCALED 且底层值是整型时,使用 raw*scale+offset; /// 其他情况默认认为原始值即为工程值。 /// @param raw_value 原始数值。 /// @param si 序列实例,用于读取 scale/offset。 /// @param storage_method_id 序列定义层的存储方式。 /// @param physical_type 当前数组的物理类型。 /// @return 工程值。 double stat_decode_engineering_value( double raw_value, const PqdifSeriesInstance& si, unsigned int storage_method_id, int physical_type) { const bool integer_like = physical_type == ID_PHYS_TYPE_INTEGER1 || physical_type == ID_PHYS_TYPE_INTEGER2 || physical_type == ID_PHYS_TYPE_INTEGER4 || physical_type == ID_PHYS_TYPE_UNS_INTEGER1 || physical_type == ID_PHYS_TYPE_UNS_INTEGER2 || physical_type == ID_PHYS_TYPE_UNS_INTEGER4; if ((storage_method_id & ID_SERIES_METHOD_SCALED) && integer_like) return raw_value * si.scale + si.offset; return raw_value; } /// @brief 解析某一个样本点对应的绝对时刻。 /// @details /// 优先使用 timestamp_values;若时间序列是数值型,则按 /// observation.time_start + 偏移秒 计算。 /// @param obs 当前 observation。 /// @param time_series 已解析出的时间序列实例。 /// @param idx 样本下标。 /// @param out_ts 返回绝对时刻。 /// @return 成功返回 true,否则返回 false。 bool stat_resolve_timestamp_at( const PqdifObservationRecord& obs, const PqdifSeriesInstance& time_series, size_t idx, time_t& out_ts) { if (idx < time_series.values.timestamp_values.size()) { out_ts = time_series.values.timestamp_values[idx].unix_time; return true; } double raw_time = 0.0; if (stat_try_get_raw_numeric_at(time_series.values, idx, raw_time)) { if (obs.time_start.unix_time != 0) { out_ts = obs.time_start.unix_time + static_cast(std::llround(raw_time)); return true; } out_ts = static_cast(std::llround(raw_time)); return true; } return false; } /// @brief 判断 GUID 是否相等。 /// @param a GUID A。 /// @param b GUID B。 /// @return 相等返回 true。 bool stat_guid_equals(const GUID& a, const GUID& b) { return PQDIF_IsEqualGUID(a, b); } /// @brief 判断是否为旧式 P95 值类型。 /// @param value_type_id 值类型 GUID。 /// @return 若是旧式 P95 值类型则返回 true。 bool stat_is_p95_value_type(const GUID& value_type_id) { return stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_P95); } /// @brief 识别序列属于哪一种统计值类型。 /// @details 支持 MIN / MAX / AVG / P95。 /// @param si 序列实例。 /// @param sd 关联的序列定义,可为空。 /// @return 识别出的统计值类型。 StatValueKind stat_identify_value_kind( const PqdifSeriesInstance& si, const PqdifSeriesDefinition* sd) { if (pqdif_sem::IsValueTypeMin(si.value_type_id.value) || stat_guid_equals(si.value_type_id.value, ID_SERIES_VALUE_TYPE_PHASEANGLE_MIN)) return StatValueKind::Min; if (pqdif_sem::IsValueTypeMax(si.value_type_id.value) || stat_guid_equals(si.value_type_id.value, ID_SERIES_VALUE_TYPE_PHASEANGLE_MAX)) return StatValueKind::Max; if (pqdif_sem::IsValueTypeAvg(si.value_type_id.value) || stat_guid_equals(si.value_type_id.value, ID_SERIES_VALUE_TYPE_PHASEANGLE_AVG)) return StatValueKind::Avg; if (stat_is_p95_value_type(si.value_type_id.value)) return StatValueKind::P95; if (sd != nullptr && std::fabs(sd->prob_percentile - 95.0) < 1e-6) return StatValueKind::P95; return StatValueKind::Unknown; } /// @brief 判断是否为可以用“单值序列”兜底填充 Min/Max/Avg/P95 的指标。 /// @details 部分 PQDIF 厂家会把 Pst/Plt/DVC 这类已经是统计窗口结果的量, /// 存成 Value/Instantaneous 单序列,而不是四条 Min/Max/Avg/P95 序列。 bool stat_metric_can_use_scalar_series_as_all_kinds(StatMetricId id) { switch (id) { case StatMetricId::UaDvc: case StatMetricId::UbDvc: case StatMetricId::UcDvc: case StatMetricId::UabDvc: case StatMetricId::UbcDvc: case StatMetricId::UcaDvc: case StatMetricId::UaPst: case StatMetricId::UbPst: case StatMetricId::UcPst: case StatMetricId::UabPst: case StatMetricId::UbcPst: case StatMetricId::UcaPst: case StatMetricId::UaPlt: case StatMetricId::UbPlt: case StatMetricId::UcPlt: case StatMetricId::UabPlt: case StatMetricId::UbcPlt: case StatMetricId::UcaPlt: return true; default: return false; } } /// @brief 判断未知值类型序列是否可作为 Pst/Plt/DVC 的单值数据序列。 /// @details 排除时间轴、状态、计数、区间等非物理量序列,避免把质量标志误写成指标值。 bool stat_series_can_be_scalar_stat_value( const PqdifSeriesInstance& si, const PqdifSeriesDefinition* sd) { const GUID& vt = si.value_type_id.value; if (pqdif_sem::IsValueTypeTime(vt) || stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_TIME) || stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_STATUS) || stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_COUNT) || stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_INTERVAL) || stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_DURATION)) { return false; } if (sd != nullptr) { const GUID& def_vt = sd->value_type_id.value; if (pqdif_sem::IsValueTypeTime(def_vt) || stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_TIME) || stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_STATUS) || stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_COUNT) || stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_INTERVAL) || stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_DURATION)) { return false; } } return true; } /// @brief 指标对象族。 /// @details 先按 C.2 表语义识别对象族,再根据相别拆成最终 metric_id。 enum class StatFamily { Unknown = 0, VoltageRms, CurrentRms, VoltageDeviation, Frequency, FrequencyDeviation, VoltageZeroSequence, VoltageNegativeSequence, VoltagePositiveSequence, VoltageNegativeSequenceUnbalance, CurrentZeroSequence, CurrentNegativeSequence, CurrentPositiveSequence, CurrentNegativeSequenceUnbalance }; /// @brief 将名称归一化为“仅保留字母数字并转大写”的紧凑形式。 /// @param s 原始名称。 /// @return 归一化后的字符串。 std::string stat_compact_upper(const std::string& s) { std::string out; out.reserve(s.size()); for (unsigned char ch : s) { if (std::isalnum(ch)) out.push_back(static_cast(std::toupper(ch))); } return out; } /// @brief 判断归一化后的文本中是否包含任一别名。 /// @param compact_text 归一化文本。 /// @param aliases 别名集合。 /// @return 命中任一别名则返回 true。 bool stat_contains_any_alias( const std::string& compact_text, const std::vector& aliases) { for (const auto& alias : aliases) { if (!alias.empty() && compact_text.find(alias) != std::string::npos) return true; } return false; } bool stat_ends_with(const std::string& text, const std::string& suffix) { return text.size() >= suffix.size() && text.compare(text.size() - suffix.size(), suffix.size(), suffix) == 0; } std::string stat_two_digit(int n) { char buf[8] = { 0 }; std::snprintf(buf, sizeof(buf), "%02d", n); return std::string(buf); } int stat_round_order_if_valid(double value) { const int rounded = static_cast(std::llround(value)); if (rounded < 2 || rounded > 50) return -1; if (std::fabs(value - static_cast(rounded)) > 1e-6) return -1; return rounded; } /// @brief 动态谱类次数解析的特殊返回值:标准 group=1 表示基波,应跳过,不能兜底映射为 2 次。 constexpr int STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL = -100000; bool stat_is_voltage_harmonic_characteristic(const ExpandedStatPoint& p) { return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_HRMS) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA_HGROUP); } bool stat_is_voltage_harmonic_unit(unsigned int unit_id) { // UaHarmXX/UbHarmXX/UcHarmXX 表示谐波电压 RMS 值,单位应为 V。 // OBS 中常见的 V HR A/B/C 是谐波含有率/百分比,后续应单独建 HarmRatio 指标, // 这里不能混入同一个谐波电压指标。 return unit_id == ID_QU_VOLTS; } /// @brief C.2 标准字段优先识别:电压谐波 RMS / 按谐波组频谱。 /// @details /// 这里优先使用三个标准字段,而不是名称: /// tagQuantityMeasuredID = ID_QM_VOLTAGE /// tagQuantityCharacteristicID = ID_QC_HRMS / ID_QC_SPECTRA / ID_QC_SPECTRA_HGROUP /// tagQuantityUnitsID = ID_QU_VOLTS /// 谐波次数不在这三个 ID 中,当前文件通过 tagChannelGroupID 表达 group=2..50。 bool stat_match_c2_voltage_harmonic_rms(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_VOLTAGE && stat_is_voltage_harmonic_unit(p.quantity_units_id) && stat_is_voltage_harmonic_characteristic(p); } int stat_extract_harmonic_order_from_compact_text(const std::string& compact_text) { if (compact_text.empty()) return -1; // 优先匹配带有明确 HARM/HRMS/H 前缀或后缀的写法,避免误把普通数字当作谐波次数。 for (int order = 2; order <= 50; ++order) { const std::string n = std::to_string(order); const std::string nn = stat_two_digit(order); const std::vector aliases = { "HARMONIC" + n, "HARMONIC" + nn, "HARM" + n, "HARM" + nn, "HRMS" + n, "HRMS" + nn, "HGROUP" + n, "HGROUP" + nn, "H" + n, "H" + nn, n + "HARMONIC", nn + "HARMONIC", n + "HARM", nn + "HARM" }; if (stat_contains_any_alias(compact_text, aliases)) return order; } return -1; } int stat_extract_order_from_standard_fields(const ExpandedStatPoint& p) { // 当前厂家文件把谐波次数写在 channel_group_id 中:group=1 为基波,group=2..50 为 2-50 次谐波。 // 修复点:一旦明确出现 group=1,必须视为基波并跳过,不能继续用 order_hint/name 兜底成 2 次。 if (p.channel_group_id == 1) return STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL; const int order_by_group = stat_round_order_if_valid(static_cast(p.channel_group_id)); if (order_by_group >= 2 && order_by_group <= 50) return order_by_group; // 其他厂家可能放在 series_base_quantity 或 nominal_quantity,作为标准字段不足时的补充。 // 仅当 channel_group_id 没有给出明确次数时才使用这些字段。 const int order_by_base = stat_round_order_if_valid(p.series_base_quantity); if (order_by_base >= 2 && order_by_base <= 50) return order_by_base; const int order_by_nominal = stat_round_order_if_valid(p.nominal_quantity); if (order_by_nominal >= 2 && order_by_nominal <= 50) return order_by_nominal; // 有些厂家把谐波/相角谱线拆成多个 channel instance,但每条通道名称完全相同, // 且没有写 channel_group_id/base/nominal。此时在展开通道时会按同类通道实例顺序 // 生成 order_hint,避免所有通道都被名称里的 “HARM2” 误识别为 2 次。 // 注意:如果上面已检测到 group=1,这里不会执行。 if (p.channel_spectrum_order_hint >= 2 && p.channel_spectrum_order_hint <= 50) return p.channel_spectrum_order_hint; return -1; } int stat_extract_voltage_harmonic_order(const ExpandedStatPoint& p) { if (p.quantity_measured_id != ID_QM_VOLTAGE) return -1; if (!stat_is_voltage_harmonic_unit(p.quantity_units_id)) return -1; // 第一优先级:C.2 标准字段组合。 // 只要 measured + characteristic + unit 命中,就直接从 group/base/nominal 读取次数, // 不要求通道名包含 HRMS/HARM/HGROUP。 if (stat_match_c2_voltage_harmonic_rms(p)) { const int order_by_standard = stat_extract_order_from_standard_fields(p); if (order_by_standard == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL) return -1; if (order_by_standard >= 2 && order_by_standard <= 50) return order_by_standard; } // 第二优先级:厂家名称兜底。仅在标准 characteristic 缺失或不规范时使用。 const std::string compact_text = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name); const bool text_has_harmonic_hint = stat_contains_any_alias(compact_text, { "HARM", "HARMONIC", "HRMS", "HGROUP", "SPECTRA" }); if (!text_has_harmonic_hint) return -1; const int order_by_name = stat_extract_harmonic_order_from_compact_text(compact_text); if (order_by_name >= 2 && order_by_name <= 50) return order_by_name; { const int order_by_standard = stat_extract_order_from_standard_fields(p); if (order_by_standard == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL) return -1; return order_by_standard; } } int stat_extract_voltage_harmonic_phase_index(const ExpandedStatPoint& p) { if (p.phase_id == ID_PHASE_AN) return 0; if (p.phase_id == ID_PHASE_BN) return 1; if (p.phase_id == ID_PHASE_CN) return 2; const std::string compact_text = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name); if (stat_contains_any_alias(compact_text, { "PHASEA", "PHA", "VHA", "UHARMA", "VHARMONICA", "UHARMONICA", "HRMSA" })) return 0; if (stat_contains_any_alias(compact_text, { "PHASEB", "PHB", "VHB", "UHARMB", "VHARMONICB", "UHARMONICB", "HRMSB" })) return 1; if (stat_contains_any_alias(compact_text, { "PHASEC", "PHC", "VHC", "UHARMC", "VHARMONICC", "UHARMONICC", "HRMSC" })) return 2; // 常见格式如 VH02A / V2HA / U50B:优先使用末尾相别,但避免把单独的 HARMONIC 误判成 C 相。 if (stat_ends_with(compact_text, "A") && !stat_ends_with(compact_text, "HARMONICA")) return 0; if (stat_ends_with(compact_text, "B") && !stat_ends_with(compact_text, "HARMONICB")) return 1; if (stat_ends_with(compact_text, "C") && !stat_ends_with(compact_text, "HARMONIC")) return 2; return -1; } bool stat_is_harmonic_group_characteristic(const ExpandedStatPoint& p) { return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_HRMS) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA_HGROUP); } bool stat_is_interharmonic_group_characteristic(const ExpandedStatPoint& p) { return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA_IGROUP); } bool stat_is_phase_angle_value_type(const GUID& value_type_id) { return stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE) || stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE_MIN) || stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE_MAX) || stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE_AVG); } bool stat_is_angle_like_point(const ExpandedStatPoint& p) { if (stat_is_phase_angle_value_type(p.value_type_id.value)) return true; if (p.quantity_units_id == ID_QU_DEGREES) return true; const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.value_type_id.symbolic_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name); return stat_contains_any_alias(compact, { "PHASEANGLE", "PHASANGLE", "ANGLE", "PANGLE", "HANGLE" }); } bool stat_is_magnitude_unit_for_measured(unsigned int measured_id, unsigned int unit_id) { if (measured_id == ID_QM_VOLTAGE) return unit_id == ID_QU_VOLTS; if (measured_id == ID_QM_CURRENT) return unit_id == ID_QU_AMPS; return false; } int stat_extract_line_voltage_phase_index(const ExpandedStatPoint& p) { if (p.phase_id == ID_PHASE_AB) return 0; if (p.phase_id == ID_PHASE_BC) return 1; if (p.phase_id == ID_PHASE_CA) return 2; const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name); if (stat_contains_any_alias(compact, { "PHASEAB", "PHAB", "UAB", "VAB", "HRMSAB", "HARMAB" })) return 0; if (stat_contains_any_alias(compact, { "PHASEBC", "PHBC", "UBC", "VBC", "HRMSBC", "HARMBC" })) return 1; if (stat_contains_any_alias(compact, { "PHASECA", "PHCA", "UCA", "VCA", "HRMSCA", "HARMCA" })) return 2; if (stat_ends_with(compact, "AB")) return 0; if (stat_ends_with(compact, "BC")) return 1; if (stat_ends_with(compact, "CA")) return 2; return -1; } int stat_extract_phase_current_phase_index(const ExpandedStatPoint& p) { if (p.phase_id == ID_PHASE_AN) return 0; if (p.phase_id == ID_PHASE_BN) return 1; if (p.phase_id == ID_PHASE_CN) return 2; const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name); if (stat_contains_any_alias(compact, { "PHASEA", "PHA", "IHA", "IHARMA", "IHARMONICA", "HRMSA" })) return 0; if (stat_contains_any_alias(compact, { "PHASEB", "PHB", "IHB", "IHARMB", "IHARMONICB", "HRMSB" })) return 1; if (stat_contains_any_alias(compact, { "PHASEC", "PHC", "IHC", "IHARMC", "IHARMONICC", "HRMSC" })) return 2; if (stat_ends_with(compact, "A")) return 0; if (stat_ends_with(compact, "B")) return 1; if (stat_ends_with(compact, "C")) return 2; return -1; } int stat_round_interharmonic_slot_if_valid(double value) { if (value < -1e-6 || value > 49.5 + 1e-6) return -1; const double slot_value = value - 0.5; const int slot = static_cast(std::llround(slot_value)); if (slot < 0 || slot > 49) return -1; if (std::fabs(slot_value - static_cast(slot)) > 1e-6) return -1; return slot; } int stat_extract_interharmonic_slot_from_standard_fields(const ExpandedStatPoint& p) { // 间谐波优先按标准 group 解析:group=1..50 -> 0.5..49.5。 // 即 slot=0..49。不要再兼容 group=0..49,否则会把缺失/无效 group 错映射为 0.5。 if (p.channel_group_id >= 1 && p.channel_group_id <= 50) return p.channel_group_id - 1; int slot = stat_round_interharmonic_slot_if_valid(p.series_base_quantity); if (slot >= 0) return slot; slot = stat_round_interharmonic_slot_if_valid(p.nominal_quantity); if (slot >= 0) return slot; // 仅当标准字段没有给出 group/base/nominal 时,才使用展开阶段生成的顺序提示。 if (p.channel_spectrum_order_hint >= 1 && p.channel_spectrum_order_hint <= 50) return p.channel_spectrum_order_hint - 1; return -1; } bool stat_text_has_harmonic_hint(const std::string& compact) { return stat_contains_any_alias(compact, { "HARM", "HARMONIC", "HRMS", "HGROUP", "SPECTRA" }); } bool stat_text_has_interharmonic_hint(const std::string& compact) { return stat_contains_any_alias(compact, { "INTERHARM", "INTERHARMONIC", "IHARM", "IGROUP", "IHGROUP" }); } int stat_extract_interharmonic_slot_from_text(const std::string& compact) { if (!stat_text_has_interharmonic_hint(compact)) return -1; for (int slot = 0; slot <= 49; ++slot) { const int n = slot; const std::string nn = stat_two_digit(n); const std::vector aliases = { "IH" + nn + "P5", "IH" + std::to_string(n) + "P5", "INTERHARM" + nn + "P5", "INTERHARM" + std::to_string(n) + "P5", "INTERHARMONIC" + nn + "P5", "INTERHARMONIC" + std::to_string(n) + "P5" }; if (stat_contains_any_alias(compact, aliases)) return slot; } return -1; } bool stat_guid_in(const GUID& value, const std::vector& candidates) { for (const GUID* g : candidates) { if (g != nullptr && stat_guid_equals(value, *g)) return true; } return false; } bool stat_unit_is_ratio_like(unsigned int unit_id) { return unit_id == ID_QU_PERCENT || unit_id == ID_QU_PERUNIT || unit_id == ID_QU_NONE; } bool stat_unit_is_power_unit(unsigned int unit_id) { return unit_id == ID_QU_WATTS || unit_id == ID_QU_VARS || unit_id == ID_QU_VA; } bool stat_text_has_total_hint(const std::string& compact) { return stat_contains_any_alias(compact, { "TOTAL", "TOT", "SUM", "THREEPHASE", "3PHASE", "ALLPHASE", "NET", "PTOTAL", "QTOTAL", "STOTAL", "PALL", "QALL", "SALL" }); } int stat_extract_phase_or_total_index(const ExpandedStatPoint& p) { if (p.phase_id == ID_PHASE_AN) return 0; if (p.phase_id == ID_PHASE_BN) return 1; if (p.phase_id == ID_PHASE_CN) return 2; if (p.phase_id == ID_PHASE_TOTAL || p.phase_id == ID_PHASE_NET) return 3; const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name); if (stat_text_has_total_hint(compact)) return 3; if (stat_contains_any_alias(compact, { "PHASEA", "PHA", " PA", "PA", "A相" }) || stat_ends_with(compact, "A")) return 0; if (stat_contains_any_alias(compact, { "PHASEB", "PHB", " PB", "PB", "B相" }) || stat_ends_with(compact, "B")) return 1; if (stat_contains_any_alias(compact, { "PHASEC", "PHC", " PC", "PC", "C相" }) || stat_ends_with(compact, "C")) return 2; return -1; } StatMetricId stat_metric_from_phase4(int phase, StatMetricId a, StatMetricId b, StatMetricId c, StatMetricId total) { if (phase == 0) return a; if (phase == 1) return b; if (phase == 2) return c; if (phase == 3) return total; return StatMetricId::Unknown; } StatMetricId stat_metric_from_phase3(int phase, StatMetricId a, StatMetricId b, StatMetricId c) { if (phase == 0) return a; if (phase == 1) return b; if (phase == 2) return c; return StatMetricId::Unknown; } bool stat_is_power_factor_characteristic(const ExpandedStatPoint& p) { return stat_guid_in(p.quantity_characteristic_id.value, { &ID_QC_PF, &ID_QC_PF_VECTOR, &ID_QC_PF_ARITH }); } bool stat_is_fund_power_factor_characteristic(const ExpandedStatPoint& p) { return stat_guid_in(p.quantity_characteristic_id.value, { &ID_QC_DF, &ID_QC_DF_VECTOR, &ID_QC_DF_ARITH }); } bool stat_is_active_power_characteristic(const ExpandedStatPoint& p) { return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P); } bool stat_is_reactive_power_characteristic(const ExpandedStatPoint& p) { return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_Q); } bool stat_is_apparent_power_characteristic(const ExpandedStatPoint& p) { return stat_guid_in(p.quantity_characteristic_id.value, { &ID_QC_S, &ID_QC_S_VECTOR, &ID_QC_S_ARITH }); } bool stat_is_active_fund_power_characteristic(const ExpandedStatPoint& p) { return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P_FUND); } bool stat_is_reactive_fund_power_characteristic(const ExpandedStatPoint& p) { return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_Q_FUND); } bool stat_is_apparent_fund_power_characteristic(const ExpandedStatPoint& p) { return stat_guid_in(p.quantity_characteristic_id.value, { &ID_QC_S_FUND, &ID_QC_S_VECTOR_FUND, &ID_QC_S_ARITH_FUND }); } bool stat_text_has_fund_hint(const ExpandedStatPoint& p) { const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name); return stat_contains_any_alias(compact, { "FUND", "FUNDAMENTAL", "BASEFREQ", "FREQFUND", "1STHARM", "HARM1" }); } bool stat_text_has_dvc_hint(const ExpandedStatPoint& p) { const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name); return stat_contains_any_alias(compact, { "DVC", "DVV", "DELTAV", "VOLTAGECHANGE", "VCHANGE", "FLKRMAXDVV" }); } bool stat_text_has_pst_hint(const ExpandedStatPoint& p) { const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name); return stat_contains_any_alias(compact, { "PST", "SHORTTERMFLICKER", "SHORTFLICKER", "FLKRPST" }); } bool stat_text_has_plt_hint(const ExpandedStatPoint& p) { const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name); return stat_contains_any_alias(compact, { "PLT", "LONGTERMFLICKER", "LONGFLICKER", "FLKRPLT" }); } bool stat_text_has_thd_hint(const ExpandedStatPoint& p) { const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name); return stat_contains_any_alias(compact, { "THD", "TOTALHARMONICDISTORTION", "TOTALTHD" }) && !stat_contains_any_alias(compact, { "OTHD", "ETHD", "ODDTHD", "EVENTHD" }); } bool stat_is_total_thd_characteristic(const ExpandedStatPoint& p) { return stat_guid_in(p.quantity_characteristic_id.value, { &ID_QC_TOTAL_THD, &ID_QC_TOTAL_THD_RMS }); } StatMetricId stat_identify_scalar_extension_metric(const ExpandedStatPoint& p, bool& matched_by_name_fallback) { matched_by_name_fallback = false; const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name); // 三相/总有功、无功、视在功率;以及基波功率。 if (p.quantity_measured_id == ID_QM_POWER) { const int phase = stat_extract_phase_or_total_index(p); if (phase >= 0) { if (stat_is_active_power_characteristic(p) && p.quantity_units_id == ID_QU_WATTS) return stat_metric_from_phase4(phase, StatMetricId::PaPower, StatMetricId::PbPower, StatMetricId::PcPower, StatMetricId::PTotalPower); if (stat_is_reactive_power_characteristic(p) && p.quantity_units_id == ID_QU_VARS) return stat_metric_from_phase4(phase, StatMetricId::QaPower, StatMetricId::QbPower, StatMetricId::QcPower, StatMetricId::QTotalPower); if (stat_is_apparent_power_characteristic(p) && p.quantity_units_id == ID_QU_VA) return stat_metric_from_phase4(phase, StatMetricId::SaPower, StatMetricId::SbPower, StatMetricId::ScPower, StatMetricId::STotalPower); if (stat_is_active_fund_power_characteristic(p) && p.quantity_units_id == ID_QU_WATTS) return stat_metric_from_phase4(phase, StatMetricId::PaFundPower, StatMetricId::PbFundPower, StatMetricId::PcFundPower, StatMetricId::PTotalFundPower); if (stat_is_reactive_fund_power_characteristic(p) && p.quantity_units_id == ID_QU_VARS) return stat_metric_from_phase4(phase, StatMetricId::QaFundPower, StatMetricId::QbFundPower, StatMetricId::QcFundPower, StatMetricId::QTotalFundPower); if (stat_is_apparent_fund_power_characteristic(p) && p.quantity_units_id == ID_QU_VA) return stat_metric_from_phase4(phase, StatMetricId::SaFundPower, StatMetricId::SbFundPower, StatMetricId::ScFundPower, StatMetricId::STotalFundPower); if (stat_is_power_factor_characteristic(p) && stat_unit_is_ratio_like(p.quantity_units_id)) return stat_metric_from_phase4(phase, StatMetricId::PFa, StatMetricId::PFb, StatMetricId::PFc, StatMetricId::PFTotal); if (stat_is_fund_power_factor_characteristic(p) && stat_unit_is_ratio_like(p.quantity_units_id)) return stat_metric_from_phase4(phase, StatMetricId::FundPFa, StatMetricId::FundPFb, StatMetricId::FundPFc, StatMetricId::FundPFTotal); } // 名称兜底:厂家可能只填 NONE/INSTANTANEOUS,但通道名中含 P/Q/S/PF/FUND。 if (phase >= 0) { if (p.quantity_units_id == ID_QU_WATTS && stat_contains_any_alias(compact, { "PFUND", "FUNDAMENTALP", "FUNDPOWERP" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::PaFundPower, StatMetricId::PbFundPower, StatMetricId::PcFundPower, StatMetricId::PTotalFundPower); } if (p.quantity_units_id == ID_QU_VARS && stat_contains_any_alias(compact, { "QFUND", "FUNDAMENTALQ", "FUNDPOWERQ" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::QaFundPower, StatMetricId::QbFundPower, StatMetricId::QcFundPower, StatMetricId::QTotalFundPower); } if (p.quantity_units_id == ID_QU_VA && stat_contains_any_alias(compact, { "SFUND", "FUNDAMENTALS", "FUNDPOWERS" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::SaFundPower, StatMetricId::SbFundPower, StatMetricId::ScFundPower, StatMetricId::STotalFundPower); } if (stat_unit_is_ratio_like(p.quantity_units_id) && stat_contains_any_alias(compact, { "FUNDPF", "FUNDAMENTALPF", "DISPLACEMENTFACTOR", "DF" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::FundPFa, StatMetricId::FundPFb, StatMetricId::FundPFc, StatMetricId::FundPFTotal); } if (stat_unit_is_ratio_like(p.quantity_units_id) && stat_contains_any_alias(compact, { "POWERFACTOR", "PF" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::PFa, StatMetricId::PFb, StatMetricId::PFc, StatMetricId::PFTotal); } if (p.quantity_units_id == ID_QU_WATTS && stat_contains_any_alias(compact, { "ACTIVEPOWER", "REALPOWER", "PW", "PPOWER" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::PaPower, StatMetricId::PbPower, StatMetricId::PcPower, StatMetricId::PTotalPower); } if (p.quantity_units_id == ID_QU_VARS && stat_contains_any_alias(compact, { "REACTIVEPOWER", "QPOWER", "VAR" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::QaPower, StatMetricId::QbPower, StatMetricId::QcPower, StatMetricId::QTotalPower); } if (p.quantity_units_id == ID_QU_VA && stat_contains_any_alias(compact, { "APPARENTPOWER", "SPOWER", "VA" })) { matched_by_name_fallback = true; return stat_metric_from_phase4(phase, StatMetricId::SaPower, StatMetricId::SbPower, StatMetricId::ScPower, StatMetricId::STotalPower); } } } // 总谐波畸变率 THD:三相电压/电流/线电压。 if ((p.quantity_measured_id == ID_QM_VOLTAGE || p.quantity_measured_id == ID_QM_CURRENT) && stat_unit_is_ratio_like(p.quantity_units_id) && (stat_is_total_thd_characteristic(p) || stat_text_has_thd_hint(p))) { if (!stat_is_total_thd_characteristic(p)) matched_by_name_fallback = true; if (p.quantity_measured_id == ID_QM_CURRENT) { const int phase = stat_extract_phase_current_phase_index(p); return stat_metric_from_phase3(phase, StatMetricId::IaThd, StatMetricId::IbThd, StatMetricId::IcThd); } int phase = stat_extract_voltage_harmonic_phase_index(p); if (phase >= 0) return stat_metric_from_phase3(phase, StatMetricId::UaThd, StatMetricId::UbThd, StatMetricId::UcThd); phase = stat_extract_line_voltage_phase_index(p); if (phase >= 0) return stat_metric_from_phase3(phase, StatMetricId::UabThd, StatMetricId::UbcThd, StatMetricId::UcaThd); } // 电压变动幅值 DVC/dV/V。 // 先按 C.2 ID 组合识别;若厂家未规范填写 measured,只要名称明确包含电压变动提示,也允许名称兜底。 const bool measured_is_voltage_or_voltage_name = p.quantity_measured_id == ID_QM_VOLTAGE || stat_contains_any_alias(compact, { "VOLTAGE", "VFLICKER", "VFLKR", "VDVC", "UDVC", "DVV" }); if (measured_is_voltage_or_voltage_name && stat_unit_is_ratio_like(p.quantity_units_id) && (stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_MAX_DVV) || stat_text_has_dvc_hint(p))) { if (p.quantity_measured_id != ID_QM_VOLTAGE || !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_MAX_DVV)) { matched_by_name_fallback = true; } int phase = stat_extract_voltage_harmonic_phase_index(p); if (phase >= 0) return stat_metric_from_phase3(phase, StatMetricId::UaDvc, StatMetricId::UbDvc, StatMetricId::UcDvc); phase = stat_extract_line_voltage_phase_index(p); if (phase >= 0) return stat_metric_from_phase3(phase, StatMetricId::UabDvc, StatMetricId::UbcDvc, StatMetricId::UcaDvc); } // 闪变 Pst/Plt。 // 修复点: // 1) ID_QC_FLKR_PST / ID_QC_FLKR_PLT 仍然是第一优先级; // 2) 对 V Flicker Plt A/B/C、PLT VA/VB/VC 等厂家名称进行兜底; // 3) 不强依赖 measured 必须填成 Voltage,避免厂家只靠通道名表达对象。 const bool id_is_pst = stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_PST); const bool id_is_plt = stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_PLT); const bool is_pst = id_is_pst || stat_text_has_pst_hint(p); const bool is_plt = id_is_plt || stat_text_has_plt_hint(p); const bool measured_is_voltage_or_flicker_name = measured_is_voltage_or_voltage_name || is_pst || is_plt; // Plt/Pst 修复:厂家有时会把 Plt 序列 unit 写成 Unknown/Unrecognized,而不是 None/pu/%。 // 只要 C.2 characteristic ID 或通道名称已经明确是 Pst/Plt,就不再用 unit 作为硬性门槛。 // unit 仅用于辅助判断,不作为清晰 Flicker 通道的排除条件。 if (measured_is_voltage_or_flicker_name && (is_pst || is_plt)) { if (p.quantity_measured_id != ID_QM_VOLTAGE || !(id_is_pst || id_is_plt)) matched_by_name_fallback = true; // 若同时命中 Pst 和 Plt,以标准 ID 为准;没有标准 ID 时 Plt 优先, // 避免 “V Flicker Plt A/B/C” 因 unit 非标准而被跳过。 const bool choose_plt = id_is_plt || (!id_is_pst && is_plt); int phase = stat_extract_voltage_harmonic_phase_index(p); if (phase >= 0) return choose_plt ? stat_metric_from_phase3(phase, StatMetricId::UaPlt, StatMetricId::UbPlt, StatMetricId::UcPlt) : stat_metric_from_phase3(phase, StatMetricId::UaPst, StatMetricId::UbPst, StatMetricId::UcPst); phase = stat_extract_line_voltage_phase_index(p); if (phase >= 0) return choose_plt ? stat_metric_from_phase3(phase, StatMetricId::UabPlt, StatMetricId::UbcPlt, StatMetricId::UcaPlt) : stat_metric_from_phase3(phase, StatMetricId::UabPst, StatMetricId::UbcPst, StatMetricId::UcaPst); } // 基波有效值和基波相角。 if ((p.quantity_measured_id == ID_QM_VOLTAGE || p.quantity_measured_id == ID_QM_CURRENT) && stat_text_has_fund_hint(p)) { const bool is_angle = stat_is_angle_like_point(p) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND); const bool is_rms = stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_INSTANTANEOUS) || stat_contains_any_alias(compact, { "RMS", "FUND" }); if (p.quantity_measured_id == ID_QM_CURRENT) { const int phase = stat_extract_phase_current_phase_index(p); if (is_angle && p.quantity_units_id == ID_QU_DEGREES) { matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND); return stat_metric_from_phase3(phase, StatMetricId::IaFundAngle, StatMetricId::IbFundAngle, StatMetricId::IcFundAngle); } if (is_rms && p.quantity_units_id == ID_QU_AMPS) { matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS); return stat_metric_from_phase3(phase, StatMetricId::IaFundRms, StatMetricId::IbFundRms, StatMetricId::IcFundRms); } } else { int phase = stat_extract_voltage_harmonic_phase_index(p); if (phase >= 0) { if (is_angle && p.quantity_units_id == ID_QU_DEGREES) { matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND); return stat_metric_from_phase3(phase, StatMetricId::UaFundAngle, StatMetricId::UbFundAngle, StatMetricId::UcFundAngle); } if (is_rms && p.quantity_units_id == ID_QU_VOLTS) { matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS); return stat_metric_from_phase3(phase, StatMetricId::UaFundRms, StatMetricId::UbFundRms, StatMetricId::UcFundRms); } } phase = stat_extract_line_voltage_phase_index(p); if (phase >= 0) { if (is_angle && p.quantity_units_id == ID_QU_DEGREES) { matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND); return stat_metric_from_phase3(phase, StatMetricId::UabFundAngle, StatMetricId::UbcFundAngle, StatMetricId::UcaFundAngle); } if (is_rms && p.quantity_units_id == ID_QU_VOLTS) { matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS); return stat_metric_from_phase3(phase, StatMetricId::UabFundRms, StatMetricId::UbcFundRms, StatMetricId::UcaFundRms); } } } } return StatMetricId::Unknown; } StatMetricId stat_identify_dynamic_extension_metric(const ExpandedStatPoint& p, bool& matched_by_name_fallback) { matched_by_name_fallback = false; const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name) + stat_compact_upper(p.value_type_id.symbolic_name); const bool standard_harmonic = stat_is_harmonic_group_characteristic(p) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P_HARMONIC) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P_HARMONIC_UNSIGNED); const bool fallback_harmonic = !standard_harmonic && stat_text_has_harmonic_hint(compact) && !stat_text_has_interharmonic_hint(compact); if (!standard_harmonic && !fallback_harmonic) return StatMetricId::Unknown; if (stat_is_angle_like_point(p)) return StatMetricId::Unknown; int order = -1; if (standard_harmonic) order = stat_extract_order_from_standard_fields(p); if (order == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL) return StatMetricId::Unknown; // 名称兜底场景下,仍然优先尝试 group/base/nominal/order_hint; // 这样 P HARM A 这类“通道名不含具体次数,但通道序列成组排列”的文件也能识别 2-50 次。 if (order < 2 || order > 50) { const int order_by_standard_or_hint = stat_extract_order_from_standard_fields(p); if (order_by_standard_or_hint == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL) return StatMetricId::Unknown; if (order_by_standard_or_hint >= 2 && order_by_standard_or_hint <= 50) order = order_by_standard_or_hint; } if (order < 2 || order > 50) order = stat_extract_harmonic_order_from_compact_text(compact); if (order < 2 || order > 50) return StatMetricId::Unknown; // 谐波功率:按 POWER + W/var/VA 识别,支持三相和总。 if (p.quantity_measured_id == ID_QM_POWER && stat_unit_is_power_unit(p.quantity_units_id)) { const int phase = stat_extract_phase_or_total_index(p); if (phase < 0) return StatMetricId::Unknown; matched_by_name_fallback = !standard_harmonic; if (p.quantity_units_id == ID_QU_WATTS) return stat_dynamic_metric_id(StatDynamicMetricGroup::HarmonicActivePower, phase, order); if (p.quantity_units_id == ID_QU_VARS) return stat_dynamic_metric_id(StatDynamicMetricGroup::HarmonicReactivePower, phase, order); if (p.quantity_units_id == ID_QU_VA) return stat_dynamic_metric_id(StatDynamicMetricGroup::HarmonicApparentPower, phase, order); } // 谐波含有率:按 %/pu/无量纲 识别,和 V/A RMS 谐波分离。 if ((p.quantity_measured_id == ID_QM_VOLTAGE || p.quantity_measured_id == ID_QM_CURRENT) && stat_unit_is_ratio_like(p.quantity_units_id)) { matched_by_name_fallback = !standard_harmonic; if (p.quantity_measured_id == ID_QM_CURRENT) { const int phase = stat_extract_phase_current_phase_index(p); if (phase >= 0) return stat_dynamic_metric_id(StatDynamicMetricGroup::CurrentHarmonicRatio, phase, order); } else { int phase = stat_extract_voltage_harmonic_phase_index(p); if (phase >= 0) return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonicRatio, phase, order); phase = stat_extract_line_voltage_phase_index(p); if (phase >= 0) return stat_dynamic_metric_id(StatDynamicMetricGroup::LineVoltageHarmonicRatio, phase, order); } } return StatMetricId::Unknown; } StatMetricId stat_identify_dynamic_spectrum_metric(const ExpandedStatPoint& p, bool& matched_by_name_fallback) { matched_by_name_fallback = false; if (p.quantity_measured_id != ID_QM_VOLTAGE && p.quantity_measured_id != ID_QM_CURRENT) return StatMetricId::Unknown; const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) + stat_compact_upper(p.quantity_characteristic_id.symbolic_name) + stat_compact_upper(p.value_type_id.symbolic_name); const bool standard_harmonic = stat_is_harmonic_group_characteristic(p); const bool standard_interharmonic = stat_is_interharmonic_group_characteristic(p); const bool has_interharmonic_text = stat_text_has_interharmonic_hint(compact); const bool fallback_interharmonic = !standard_interharmonic && has_interharmonic_text; const bool fallback_harmonic = !standard_harmonic && stat_text_has_harmonic_hint(compact) && !has_interharmonic_text; const bool is_angle = stat_is_angle_like_point(p); const bool magnitude_unit_ok = stat_is_magnitude_unit_for_measured(p.quantity_measured_id, p.quantity_units_id); const bool angle_unit_ok = is_angle || p.quantity_units_id == ID_QU_DEGREES; if (!is_angle && !magnitude_unit_ok) return StatMetricId::Unknown; if (is_angle && !angle_unit_ok) return StatMetricId::Unknown; // 间谐波必须优先于普通谐波识别,避免 IHRMS/INTERHARMONIC 被 HRMS/HARMONIC 兜底误吃掉。 if ((standard_interharmonic || fallback_interharmonic) && !is_angle) { int slot = -1; if (standard_interharmonic) slot = stat_extract_interharmonic_slot_from_standard_fields(p); if (slot < 0) slot = stat_extract_interharmonic_slot_from_text(compact); if (slot < 0 || slot > 49) return StatMetricId::Unknown; if (p.quantity_measured_id == ID_QM_VOLTAGE) { int phase = stat_extract_voltage_harmonic_phase_index(p); if (phase >= 0) { matched_by_name_fallback = !standard_interharmonic; return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageInterharmonic, phase, slot); } phase = stat_extract_line_voltage_phase_index(p); if (phase >= 0) { matched_by_name_fallback = !standard_interharmonic; return stat_dynamic_metric_id(StatDynamicMetricGroup::LineVoltageInterharmonic, phase, slot); } } else if (p.quantity_measured_id == ID_QM_CURRENT) { const int phase = stat_extract_phase_current_phase_index(p); if (phase >= 0) { matched_by_name_fallback = !standard_interharmonic; return stat_dynamic_metric_id(StatDynamicMetricGroup::CurrentInterharmonic, phase, slot); } } } if (standard_harmonic || fallback_harmonic) { int order = -1; if (standard_harmonic) order = stat_extract_order_from_standard_fields(p); // group=1 明确是基波,必须跳过。不能继续通过 order_hint/name fallback 变成 2 次谐波。 if (order == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL) return StatMetricId::Unknown; if (order < 2 || order > 50) order = stat_extract_harmonic_order_from_compact_text(compact); if (order < 2 || order > 50) return StatMetricId::Unknown; if (p.quantity_measured_id == ID_QM_VOLTAGE) { int phase = stat_extract_voltage_harmonic_phase_index(p); if (phase >= 0) { matched_by_name_fallback = !standard_harmonic; return stat_dynamic_metric_id(is_angle ? StatDynamicMetricGroup::VoltageHarmonicAngle : StatDynamicMetricGroup::VoltageHarmonic, phase, order); } phase = stat_extract_line_voltage_phase_index(p); if (phase >= 0) { matched_by_name_fallback = !standard_harmonic; return stat_dynamic_metric_id(is_angle ? StatDynamicMetricGroup::LineVoltageHarmonicAngle : StatDynamicMetricGroup::LineVoltageHarmonic, phase, order); } } else if (p.quantity_measured_id == ID_QM_CURRENT) { const int phase = stat_extract_phase_current_phase_index(p); if (phase >= 0) { matched_by_name_fallback = !standard_harmonic; return stat_dynamic_metric_id(is_angle ? StatDynamicMetricGroup::CurrentHarmonicAngle : StatDynamicMetricGroup::CurrentHarmonic, phase, order); } } } return StatMetricId::Unknown; } /// @brief 判断样本点是否为 C.2 表中的“频率”对象族。 /// @details /// 根据附录 C 表 C.2: /// Frequency -> ID_QM_VOLTAGE + ID_QC_FREQUENCY + ID_QU_HERTZ。 /// @param p 样本点。 /// @return 若命中 Frequency 对象族则返回 true。 bool stat_match_c2_frequency(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_VOLTAGE && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FREQUENCY) && p.quantity_units_id == ID_QU_HERTZ; } /// @brief 判断样本点是否为 C.2 表中的“电压有效值”对象族。 /// @details /// 根据附录 C 表 C.2: /// V RMS -> ID_QM_VOLTAGE + ID_QC_RMS + ID_QU_VOLTS。 /// @param p 样本点。 /// @return 若命中 VoltageRms 对象族则返回 true。 bool stat_match_c2_voltage_rms(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_VOLTAGE && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) && p.quantity_units_id == ID_QU_VOLTS; } /// @brief 判断样本点是否为 C.2 表中的“电流有效值”对象族。 /// @details /// 根据附录 C 表 C.2: /// I RMS -> ID_QM_CURRENT + ID_QC_RMS + ID_QU_AMPS。 /// @param p 样本点。 /// @return 若命中 CurrentRms 对象族则返回 true。 bool stat_match_c2_current_rms(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_CURRENT && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) && p.quantity_units_id == ID_QU_AMPS; } /// @brief 判断样本点是否为 C.2 表中的“电压偏差”对象族。 /// @details /// 根据附录 C 表 C.2: /// V RMS Deviation -> ID_QM_VOLTAGE + ID_QC_RMS + ID_QU_PERCENT。 /// 为兼容部分设备文件,也允许单位为 PU 作为辅助兼容。 /// @param p 样本点。 /// @return 若命中 VoltageDeviation 对象族则返回 true。 bool stat_match_c2_voltage_deviation(const ExpandedStatPoint& p) { const bool unit_match = p.quantity_units_id == ID_QU_PERCENT || p.quantity_units_id == ID_QU_PERUNIT; return p.quantity_measured_id == ID_QM_VOLTAGE && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) && unit_match; } /// @brief 判断样本点是否为 C.2 表中的“电压零序分量”对象族。 /// @details Zero sequence component -> ID_QM_VOLTAGE + ID_QC_SZERO + ID_QU_VOLTS。 bool stat_match_c2_voltage_zero_sequence(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_VOLTAGE && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SZERO) && p.quantity_units_id == ID_QU_VOLTS; } /// @brief 判断样本点是否为 C.2 表中的“电压负序分量”对象族。 /// @details Negative sequence component -> ID_QM_VOLTAGE + ID_QC_SNEG + ID_QU_VOLTS。 bool stat_match_c2_voltage_negative_sequence(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_VOLTAGE && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SNEG) && p.quantity_units_id == ID_QU_VOLTS; } /// @brief 判断样本点是否为 C.2 表中的“电压正序分量”对象族。 /// @details Positive sequence component -> ID_QM_VOLTAGE + ID_QC_SPOS + ID_QU_VOLTS。 bool stat_match_c2_voltage_positive_sequence(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_VOLTAGE && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPOS) && p.quantity_units_id == ID_QU_VOLTS; } /// @brief 判断样本点是否为 C.2 表中的“电压负序不平衡”对象族。 /// @details Negative sequence component unbalance -> ID_QM_VOLTAGE + ID_QC_S2S1 + ID_QU_PERCENT/ID_QU_PERUNIT。 bool stat_match_c2_voltage_negative_sequence_unbalance(const ExpandedStatPoint& p) { const bool unit_match = p.quantity_units_id == ID_QU_PERCENT || p.quantity_units_id == ID_QU_PERUNIT; return p.quantity_measured_id == ID_QM_VOLTAGE && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_S2S1) && unit_match; } /// @brief 判断样本点是否为 C.2 表中的“电流零序分量”对象族。 /// @details Zero sequence component -> ID_QM_CURRENT + ID_QC_SZERO + ID_QU_AMPS。 bool stat_match_c2_current_zero_sequence(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_CURRENT && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SZERO) && p.quantity_units_id == ID_QU_AMPS; } /// @brief 判断样本点是否为 C.2 表中的“电流负序分量”对象族。 /// @details Negative sequence component -> ID_QM_CURRENT + ID_QC_SNEG + ID_QU_AMPS。 bool stat_match_c2_current_negative_sequence(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_CURRENT && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SNEG) && p.quantity_units_id == ID_QU_AMPS; } /// @brief 判断样本点是否为 C.2 表中的“电流正序分量”对象族。 /// @details Positive sequence component -> ID_QM_CURRENT + ID_QC_SPOS + ID_QU_AMPS。 bool stat_match_c2_current_positive_sequence(const ExpandedStatPoint& p) { return p.quantity_measured_id == ID_QM_CURRENT && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPOS) && p.quantity_units_id == ID_QU_AMPS; } /// @brief 判断样本点是否为 C.2 表中的“电流负序不平衡”对象族。 /// @details Negative sequence component unbalance -> ID_QM_CURRENT + ID_QC_S2S1 + ID_QU_PERCENT/ID_QU_PERUNIT。 bool stat_match_c2_current_negative_sequence_unbalance(const ExpandedStatPoint& p) { const bool unit_match = p.quantity_units_id == ID_QU_PERCENT || p.quantity_units_id == ID_QU_PERUNIT; return p.quantity_measured_id == ID_QM_CURRENT && stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_S2S1) && unit_match; } /// @brief 判断样本点是否为“频率偏差”对象族。 /// @details /// 频率偏差和频率在 C.2 表里共用同一组核心语义: /// ID_QM_VOLTAGE + ID_QC_FREQUENCY + ID_QU_HERTZ,差别主要体现在对象名称。 /// 因此这里采用“C.2 语义 + 名称辅助”的方式识别。 /// @param p 样本点。 /// @return 若命中 FrequencyDeviation 对象族则返回 true。 bool stat_match_c2_frequency_deviation(const ExpandedStatPoint& p) { if (!stat_match_c2_frequency(p)) return false; const std::string compact_channel = stat_compact_upper(p.channel_name); const std::string compact_quantity = stat_compact_upper(p.quantity_name); const std::vector aliases = { "FREQDEV", "FREQUENCYDEV", "DELTAF", "FDEV", "DEVIATION" }; return stat_contains_any_alias(compact_channel, aliases) || stat_contains_any_alias(compact_quantity, aliases); } /// @brief 基于 C.2 表的核心语义识别对象族。 /// @param p 已展开样本点。 /// @return 命中的对象族。 StatFamily stat_identify_family_by_c2(const ExpandedStatPoint& p) { if (stat_match_c2_frequency_deviation(p)) return StatFamily::FrequencyDeviation; if (stat_match_c2_frequency(p)) return StatFamily::Frequency; if (stat_match_c2_voltage_negative_sequence_unbalance(p)) return StatFamily::VoltageNegativeSequenceUnbalance; if (stat_match_c2_voltage_zero_sequence(p)) return StatFamily::VoltageZeroSequence; if (stat_match_c2_voltage_negative_sequence(p)) return StatFamily::VoltageNegativeSequence; if (stat_match_c2_voltage_positive_sequence(p)) return StatFamily::VoltagePositiveSequence; if (stat_match_c2_current_negative_sequence_unbalance(p)) return StatFamily::CurrentNegativeSequenceUnbalance; if (stat_match_c2_current_zero_sequence(p)) return StatFamily::CurrentZeroSequence; if (stat_match_c2_current_negative_sequence(p)) return StatFamily::CurrentNegativeSequence; if (stat_match_c2_current_positive_sequence(p)) return StatFamily::CurrentPositiveSequence; if (stat_match_c2_voltage_deviation(p)) return StatFamily::VoltageDeviation; if (stat_match_c2_voltage_rms(p)) return StatFamily::VoltageRms; if (stat_match_c2_current_rms(p)) return StatFamily::CurrentRms; return StatFamily::Unknown; } /// @brief 根据接线方式、相别与 C.2 对象族,把样本点映射成最终业务指标。 /// @param connection_kind 当前文件接线方式。 /// @param p 已展开样本点。 /// @param matched_by_name_fallback 返回是否通过名称辅助识别。 /// @return 识别出的业务指标 ID。 StatMetricId stat_identify_metric_id( ParsedConnectionKind connection_kind, const ExpandedStatPoint& p, bool& matched_by_name_fallback) { matched_by_name_fallback = false; // 扩展动态指标入口:谐波功率、谐波含有率。 // 仍然保持“ID 组合优先,名称兜底”策略;名称只在标准字段不足时参与。 { bool dynamic_name_fallback = false; const StatMetricId dynamic_metric = stat_identify_dynamic_extension_metric(p, dynamic_name_fallback); if (dynamic_metric != StatMetricId::Unknown) { matched_by_name_fallback = dynamic_name_fallback; return dynamic_metric; } } // 谐波/间谐波类指标统一入口: // 先按 C.2 标准 ID 组合识别 measured/characteristic/unit/value_type,再从 group/base/nominal 取次数; // 标准字段不足时才使用通道名兜底。覆盖电压/电流/线电压、幅值/相角、谐波/间谐波。 { bool dynamic_name_fallback = false; const StatMetricId dynamic_metric = stat_identify_dynamic_spectrum_metric(p, dynamic_name_fallback); if (dynamic_metric != StatMetricId::Unknown) { matched_by_name_fallback = dynamic_name_fallback; return dynamic_metric; } } // 扩展静态指标入口:功率、功率因数、DVC、闪变、THD、基波 RMS/相角。 { bool scalar_name_fallback = false; const StatMetricId scalar_metric = stat_identify_scalar_extension_metric(p, scalar_name_fallback); if (scalar_metric != StatMetricId::Unknown) { matched_by_name_fallback = scalar_name_fallback; return scalar_metric; } } const StatFamily family = stat_identify_family_by_c2(p); switch (family) { case StatFamily::VoltageRms: if (p.phase_id == ID_PHASE_AN) return StatMetricId::UaRms; if (p.phase_id == ID_PHASE_BN) return StatMetricId::UbRms; if (p.phase_id == ID_PHASE_CN) return StatMetricId::UcRms; if (p.phase_id == ID_PHASE_AB) return StatMetricId::UabRms; if (p.phase_id == ID_PHASE_BC) return StatMetricId::UbcRms; if (p.phase_id == ID_PHASE_CA) return StatMetricId::UcaRms; break; case StatFamily::CurrentRms: if (p.phase_id == ID_PHASE_AN) return StatMetricId::IaRms; if (p.phase_id == ID_PHASE_BN) return StatMetricId::IbRms; if (p.phase_id == ID_PHASE_CN) return StatMetricId::IcRms; break; case StatFamily::VoltageDeviation: if (connection_kind == ParsedConnectionKind::Wye || connection_kind == ParsedConnectionKind::Unknown) { if (p.phase_id == ID_PHASE_AN) return StatMetricId::UaDeviation; if (p.phase_id == ID_PHASE_BN) return StatMetricId::UbDeviation; if (p.phase_id == ID_PHASE_CN) return StatMetricId::UcDeviation; } if (connection_kind == ParsedConnectionKind::Delta || connection_kind == ParsedConnectionKind::Unknown) { if (p.phase_id == ID_PHASE_AB) return StatMetricId::UabDeviation; if (p.phase_id == ID_PHASE_BC) return StatMetricId::UbcDeviation; if (p.phase_id == ID_PHASE_CA) return StatMetricId::UcaDeviation; } break; case StatFamily::VoltageZeroSequence: return StatMetricId::UZeroSeq; case StatFamily::VoltageNegativeSequence: return StatMetricId::UNegSeq; case StatFamily::VoltagePositiveSequence: return StatMetricId::UPosSeq; case StatFamily::VoltageNegativeSequenceUnbalance: return StatMetricId::UNegSeqUnbalance; case StatFamily::CurrentZeroSequence: return StatMetricId::IZeroSeq; case StatFamily::CurrentNegativeSequence: return StatMetricId::INegSeq; case StatFamily::CurrentPositiveSequence: return StatMetricId::IPosSeq; case StatFamily::CurrentNegativeSequenceUnbalance: return StatMetricId::INegSeqUnbalance; case StatFamily::Frequency: return StatMetricId::Frequency; case StatFamily::FrequencyDeviation: matched_by_name_fallback = true; return StatMetricId::FrequencyDeviation; default: break; } // 部分厂家文件可能没有正确填写序分量的 QuantityCharacteristicID, // 但通道名/量名会包含 Pos/Neg/Zero/Sequence/S2S1 等信息; // 这里只作为补充兜底,优先级低于上面的 C.2 GUID 语义识别。 const std::string compact_text = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name); if (p.quantity_measured_id == ID_QM_VOLTAGE) { if (stat_contains_any_alias(compact_text, { "S2S1", "NEGATIVESEQUENCEUNBALANCE", "NEGSEQUNBALANCE", "NEGATIVEUNBALANCE", "NEGUNBALANCE", "VOLTAGEUNBALANCE", "VUNBALANCE", "VOLTAGEIMBALANCE" })) { matched_by_name_fallback = true; return StatMetricId::UNegSeqUnbalance; } if (stat_contains_any_alias(compact_text, { "ZEROSEQUENCE", "ZEROSEQ", "ZEROSQ", "SZERO", "VZEROSEQ", "UZEROSEQ", "V0", "U0" })) { matched_by_name_fallback = true; return StatMetricId::UZeroSeq; } if (stat_contains_any_alias(compact_text, { "NEGATIVESEQUENCE", "NEGSEQ", "NEGSQ", "SNEG", "VNEGSEQ", "UNEGSEQ", "V2", "U2" })) { matched_by_name_fallback = true; return StatMetricId::UNegSeq; } if (stat_contains_any_alias(compact_text, { "POSITIVESEQUENCE", "POSSEQ", "POSSQ", "SPOS", "VPOSSEQ", "UPOSSEQ", "V1", "U1" })) { matched_by_name_fallback = true; return StatMetricId::UPosSeq; } } // 电流序分量使用同一组序分量 characteristic,但 QuantityMeasured 为 CURRENT; // 部分厂家仍可能只在通道名里写 I SPos / I SNeg / I SZero / I S2S1,因此也做名称兜底。 if (p.quantity_measured_id == ID_QM_CURRENT) { if (stat_contains_any_alias(compact_text, { "IS2S1", "CURRENTS2S1", "CURRENTNEGATIVESEQUENCEUNBALANCE", "CURRENTNEGSEQUNBALANCE", "INEGSEQUNBALANCE", "NEGSEQUNBALANCEI", "CURRENTUNBALANCE", "IUNBALANCE", "CURRENTIMBALANCE", "IIMBALANCE" })) { matched_by_name_fallback = true; return StatMetricId::INegSeqUnbalance; } if (stat_contains_any_alias(compact_text, { "CURRENTZEROSEQUENCE", "CURRENTZEROSEQ", "IZEROSEQUENCE", "IZEROSEQ", "ISZERO", "IZEROSQ", "I0" })) { matched_by_name_fallback = true; return StatMetricId::IZeroSeq; } if (stat_contains_any_alias(compact_text, { "CURRENTNEGATIVESEQUENCE", "CURRENTNEGSEQ", "INEGATIVESEQUENCE", "INEGSEQ", "ISNEG", "INEGSQ", "I2" })) { matched_by_name_fallback = true; return StatMetricId::INegSeq; } if (stat_contains_any_alias(compact_text, { "CURRENTPOSITIVESEQUENCE", "CURRENTPOSSEQ", "IPOSITIVESEQUENCE", "IPOSSEQ", "ISPOS", "IPOSSQ", "I1" })) { matched_by_name_fallback = true; return StatMetricId::IPosSeq; } } return StatMetricId::Unknown; } struct StatMetricSourceKey { int observation_index = -1; int channel_instance_index = -1; bool operator<(const StatMetricSourceKey& other) const { if (observation_index != other.observation_index) return observation_index < other.observation_index; return channel_instance_index < other.channel_instance_index; } bool operator==(const StatMetricSourceKey& other) const { return observation_index == other.observation_index && channel_instance_index == other.channel_instance_index; } }; struct StatMetricSourceStats { StatMetricId metric_id = StatMetricId::Unknown; StatMetricSourceKey key; std::string observation_name; std::string channel_name; std::string quantity_name; unsigned int phase_id = 0; unsigned int quantity_measured_id = 0; unsigned int quantity_units_id = 0; PqdifGuidValue quantity_characteristic_id; int channel_def_index = -1; size_t point_count = 0; size_t non_zero_count = 0; double first_value = 0.0; double min_value = 0.0; double max_value = 0.0; double sum_abs_value = 0.0; bool has_first = false; bool has_min_kind = false; bool has_max_kind = false; bool has_avg_kind = false; bool has_p95_kind = false; void add(const ExpandedStatPoint& p) { if (!has_first) { has_first = true; first_value = p.value; min_value = p.value; max_value = p.value; metric_id = p.metric_id; key.observation_index = p.observation_index; key.channel_instance_index = p.channel_instance_index; observation_name = p.observation_name; channel_name = p.channel_name; quantity_name = p.quantity_name; phase_id = p.phase_id; quantity_measured_id = p.quantity_measured_id; quantity_units_id = p.quantity_units_id; quantity_characteristic_id = p.quantity_characteristic_id; channel_def_index = p.channel_def_index; } ++point_count; if (std::fabs(p.value) > 1e-12) ++non_zero_count; if (p.value < min_value) min_value = p.value; if (p.value > max_value) max_value = p.value; sum_abs_value += std::fabs(p.value); switch (p.stat_kind) { case StatValueKind::Min: has_min_kind = true; break; case StatValueKind::Max: has_max_kind = true; break; case StatValueKind::Avg: has_avg_kind = true; break; case StatValueKind::P95: has_p95_kind = true; break; default: break; } } double avg_abs_value() const { if (point_count == 0) return 0.0; return sum_abs_value / static_cast(point_count); } bool all_zero() const { return point_count > 0 && non_zero_count == 0; } }; struct StatMetricQualityInfo { StatMetricQuality quality = StatMetricQuality::Normal; std::string reason; StatMetricSourceKey source_key; std::string source_channel_name; }; StatMetricSourceKey stat_make_source_key(const ExpandedStatPoint& p) { StatMetricSourceKey key; key.observation_index = p.observation_index; key.channel_instance_index = p.channel_instance_index; return key; } bool stat_metric_has_alias(StatMetricId metric_id, const std::string& compact_channel) { if (stat_is_voltage_harmonic_metric(metric_id)) { const int order = stat_voltage_harmonic_order(metric_id); const int phase = stat_voltage_harmonic_phase_index(metric_id); const std::string n = std::to_string(order); const std::string nn = stat_two_digit(order); const char p = phase == 0 ? 'A' : (phase == 1 ? 'B' : 'C'); std::string phase_text(1, p); std::vector aliases = { "VHARM" + n + phase_text, "VHARM" + nn + phase_text, "VHARMONIC" + n + phase_text, "VHARMONIC" + nn + phase_text, "VHRMS" + n + phase_text, "VHRMS" + nn + phase_text, "UHARM" + n + phase_text, "UHARM" + nn + phase_text, "UHARMONIC" + n + phase_text, "UHARMONIC" + nn + phase_text, "UHRMS" + n + phase_text, "UHRMS" + nn + phase_text, "H" + n + phase_text, "H" + nn + phase_text, "HARM" + n + phase_text, "HARM" + nn + phase_text, "HARMONIC" + n + phase_text, "HARMONIC" + nn + phase_text }; return stat_contains_any_alias(compact_channel, aliases); } std::vector aliases; switch (metric_id) { case StatMetricId::UaRms: aliases = { "VRMSA", "VARMS", "VAN", "UA", "URMSA" }; break; case StatMetricId::UbRms: aliases = { "VRMSB", "VBRMS", "VBN", "UB", "URMSB" }; break; case StatMetricId::UcRms: aliases = { "VRMSC", "VCRMS", "VCN", "UC", "URMSC" }; break; case StatMetricId::IaRms: aliases = { "IRMSA", "IARMS", "IA" }; break; case StatMetricId::IbRms: aliases = { "IRMSB", "IBRMS", "IB" }; break; case StatMetricId::IcRms: aliases = { "IRMSC", "ICRMS", "IC" }; break; case StatMetricId::UabRms: aliases = { "VRMSAB", "VABRMS", "VAB", "UAB", "URMSAB" }; break; case StatMetricId::UbcRms: aliases = { "VRMSBC", "VBCRMS", "VBC", "UBC", "URMSBC" }; break; case StatMetricId::UcaRms: aliases = { "VRMSCA", "VCARMS", "VCA", "UCA", "URMSCA" }; break; case StatMetricId::UaDeviation: aliases = { "UADEVIATION", "VADEVIATION", "VDEVA", "DEVA" }; break; case StatMetricId::UbDeviation: aliases = { "UBDEVIATION", "VBDEVIATION", "VDEVB", "DEVB" }; break; case StatMetricId::UcDeviation: aliases = { "UCDEVIATION", "VCDEVIATION", "VDEVC", "DEVC" }; break; case StatMetricId::UabDeviation: aliases = { "UABDEVIATION", "VABDEVIATION", "VDEVAB", "DEVAB" }; break; case StatMetricId::UbcDeviation: aliases = { "UBCDEVIATION", "VBCDEVIATION", "VDEVBC", "DEVBC" }; break; case StatMetricId::UcaDeviation: aliases = { "UCADEVIATION", "VCADEVIATION", "VDEVCA", "DEVCA" }; break; case StatMetricId::Frequency: aliases = { "FREQUENCY", "FREQ", "HZ" }; break; case StatMetricId::FrequencyDeviation: aliases = { "FREQDEV", "FREQUENCYDEV", "DELTAF", "FDEV" }; break; case StatMetricId::UZeroSeq: aliases = { "ZEROSEQUENCE", "ZEROSEQ", "ZEROSQ", "SZERO", "VZEROSEQ", "UZEROSEQ", "V0", "U0" }; break; case StatMetricId::UNegSeq: aliases = { "NEGATIVESEQUENCE", "NEGSEQ", "NEGSQ", "SNEG", "VNEGSEQ", "UNEGSEQ", "V2", "U2" }; break; case StatMetricId::UPosSeq: aliases = { "POSITIVESEQUENCE", "POSSEQ", "POSSQ", "SPOS", "VPOSSEQ", "UPOSSEQ", "V1", "U1" }; break; case StatMetricId::UNegSeqUnbalance: aliases = { "S2S1", "NEGATIVESEQUENCEUNBALANCE", "NEGSEQUNBALANCE", "NEGATIVEUNBALANCE", "NEGUNBALANCE", "VOLTAGEUNBALANCE", "VUNBALANCE", "VOLTAGEIMBALANCE" }; break; case StatMetricId::IZeroSeq: aliases = { "CURRENTZEROSEQUENCE", "CURRENTZEROSEQ", "IZEROSEQUENCE", "IZEROSEQ", "ISZERO", "IZEROSQ", "I0" }; break; case StatMetricId::INegSeq: aliases = { "CURRENTNEGATIVESEQUENCE", "CURRENTNEGSEQ", "INEGATIVESEQUENCE", "INEGSEQ", "ISNEG", "INEGSQ", "I2" }; break; case StatMetricId::IPosSeq: aliases = { "CURRENTPOSITIVESEQUENCE", "CURRENTPOSSEQ", "IPOSITIVESEQUENCE", "IPOSSEQ", "ISPOS", "IPOSSQ", "I1" }; break; case StatMetricId::INegSeqUnbalance: aliases = { "IS2S1", "CURRENTS2S1", "CURRENTNEGATIVESEQUENCEUNBALANCE", "CURRENTNEGSEQUNBALANCE", "INEGSEQUNBALANCE", "NEGSEQUNBALANCEI", "CURRENTUNBALANCE", "IUNBALANCE", "CURRENTIMBALANCE", "IIMBALANCE" }; break; default: break; } return stat_contains_any_alias(compact_channel, aliases); } int stat_metric_source_score(const StatMetricSourceStats& s) { int score = 0; const std::string compact_channel = stat_compact_upper(s.channel_name); const std::string compact_quantity = stat_compact_upper(s.quantity_name); if (stat_metric_has_alias(s.metric_id, compact_channel)) score += 300; if (stat_metric_has_alias(s.metric_id, compact_quantity)) score += 80; if (s.has_min_kind) score += 10; if (s.has_max_kind) score += 10; if (s.has_avg_kind) score += 10; if (s.has_p95_kind) score += 10; // 统计点数越完整越优先,但不要让点数压过名称/语义。 if (s.point_count > 0) score += static_cast(std::min(s.point_count / 100, 50)); // 电压/频率一般不应全为 0;但电流、零序/负序分量、负序不平衡为 0 在业务上可能是有效状态, // 所以这些指标不在来源择优阶段扣分,只在质量状态中显式标记。 if (s.all_zero() && !stat_allow_all_zero_without_score_penalty(s.metric_id)) score -= 200; // 线电压 RMS 若是毫伏级,极大概率不是线电压有效值,但仍保留为候选并标记质量, // 这里仅降低其优先级,避免有更可信来源时被选中。 if (stat_is_line_voltage_metric(s.metric_id) && s.avg_abs_value() < 1.0) score -= 500; // 频率值通常应在合理范围;异常值降低优先级。 if (stat_is_frequency_metric(s.metric_id)) { const double avg_abs = s.avg_abs_value(); if (avg_abs > 0.0 && (avg_abs < 1.0 || avg_abs > 1000.0)) score -= 200; } return score; } void stat_print_stream_stats_line( const char* prefix, const StatMetricSourceStats& s, int score, const char* extra_text) { std::cout << prefix << " metric=" << stat_metric_name(s.metric_id) << ", ch=" << s.key.channel_instance_index << ", ch_def=" << s.channel_def_index << ", channel=" << s.channel_name << ", phase=" << pqdif_sem::FindPhaseName(s.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(s.quantity_measured_id) << ", unit=" << pqdif_sem::FindQuantityUnitsName(s.quantity_units_id) << ", characteristic=" << short_guid_name(s.quantity_characteristic_id) << ", points=" << s.point_count << ", first=" << s.first_value << ", min=" << s.min_value << ", max=" << s.max_value << ", avg_abs=" << s.avg_abs_value() << ", kinds=" << (s.has_min_kind ? "Min" : "-") << "/" << (s.has_max_kind ? "Max" : "-") << "/" << (s.has_avg_kind ? "Avg" : "-") << "/" << (s.has_p95_kind ? "P95" : "-") << ", score=" << score; if (extra_text != nullptr && extra_text[0] != '\0') std::cout << ", " << extra_text; std::cout << std::endl; } /// @brief 对同一 metric 的多来源候选流进行择优,避免进入聚合层后静默覆盖。 /// @details /// 保留同一 metric 最可信的一个通道来源;详细模式下打印所有候选来源。 /// 核心模式只打印汇总和必要的重复来源提示,避免谐波指标造成海量日志。 std::vector stat_select_best_metric_sources( const std::vector& points) { typedef std::map SourceMap; std::map by_metric; for (const auto& p : points) { StatMetricSourceKey key = stat_make_source_key(p); StatMetricSourceStats& stats = by_metric[p.metric_id][key]; stats.add(p); } std::map selected_source; const bool detail_log = pqdif_is_detail_log_enabled(); size_t selected_metric_count = 0; size_t selected_voltage_harmonic_count = 0; size_t selected_dynamic_metric_count = 0; size_t duplicate_metric_count = 0; std::cout << "========== STAT STREAM SOURCE SUMMARY ==========" << std::endl; std::cout << "candidate_metric_count=" << by_metric.size() << ", candidate_point_count=" << points.size() << ", detail_log=" << (detail_log ? "on" : "off") << std::endl; for (auto& metric_pair : by_metric) { const StatMetricId metric_id = metric_pair.first; SourceMap& sources = metric_pair.second; bool has_selected = false; StatMetricSourceKey best_key; int best_score = std::numeric_limits::min(); for (auto& src_pair : sources) { const int score = stat_metric_source_score(src_pair.second); if (!has_selected || score > best_score) { has_selected = true; best_score = score; best_key = src_pair.first; } } if (has_selected) { selected_source[metric_id] = best_key; ++selected_metric_count; if (stat_is_voltage_harmonic_metric(metric_id)) ++selected_voltage_harmonic_count; if (stat_is_dynamic_metric(metric_id)) ++selected_dynamic_metric_count; } if (sources.size() > 1) { ++duplicate_metric_count; if (pqdif_log_enabled(PqdifLogLevel::Info)) { std::cout << " [DUPLICATE METRIC SOURCES] metric=" << stat_metric_name(metric_id) << ", source_count=" << sources.size() << ", dynamic=" << (stat_is_dynamic_metric(metric_id) ? "true" : "false") << ", action=select_best_source_and_drop_others" << std::endl; } } // 只有 Debug/Trace 才展开每个 selected/dropped source。Core/Info 保持短日志, // 避免一个谱类指标重复 50 个来源时刷屏。 if (detail_log) { for (auto& src_pair : sources) { const int score = stat_metric_source_score(src_pair.second); const bool selected = has_selected && (src_pair.first == best_key); stat_print_stream_stats_line( selected ? " [STREAM SELECTED]" : " [STREAM DROPPED]", src_pair.second, score, selected ? "selected=true" : "reason=duplicate_or_lower_score"); } } } std::cout << " [STAT STREAM CORE] selected_metric_count=" << selected_metric_count << ", selected_dynamic_metrics=" << selected_dynamic_metric_count << "/" << stat_all_dynamic_metric_order().size() << ", selected_voltage_harmonics=" << selected_voltage_harmonic_count << "/147" << ", duplicate_metric_count=" << duplicate_metric_count << std::endl; std::cout << "================================================" << std::endl; std::vector out; out.reserve(points.size()); for (const auto& p : points) { const auto it = selected_source.find(p.metric_id); if (it == selected_source.end()) continue; if (stat_make_source_key(p) == it->second) out.push_back(p); } return out; } std::map stat_collect_metric_stats( const std::vector& points) { std::map out; for (const auto& p : points) { StatMetricSourceStats& stats = out[p.metric_id]; stats.add(p); } return out; } std::map stat_analyze_metric_quality( const std::vector& points) { std::map out; std::map stats_by_metric = stat_collect_metric_stats(points); double phase_voltage_avg_sum = 0.0; int phase_voltage_avg_count = 0; const StatMetricId phase_metrics[3] = { StatMetricId::UaRms, StatMetricId::UbRms, StatMetricId::UcRms }; for (int i = 0; i < 3; ++i) { const auto it = stats_by_metric.find(phase_metrics[i]); if (it != stats_by_metric.end() && it->second.avg_abs_value() > 1.0) { phase_voltage_avg_sum += it->second.avg_abs_value(); ++phase_voltage_avg_count; } } const double phase_voltage_avg = phase_voltage_avg_count > 0 ? phase_voltage_avg_sum / static_cast(phase_voltage_avg_count) : 0.0; for (const auto& metric_pair : stats_by_metric) { const StatMetricId metric_id = metric_pair.first; const StatMetricSourceStats& s = metric_pair.second; StatMetricQualityInfo qi; qi.source_key = s.key; qi.source_channel_name = s.channel_name; qi.quality = StatMetricQuality::Normal; qi.reason = "ok"; if (s.all_zero()) { qi.quality = StatMetricQuality::AllZero; qi.reason = "all selected values are zero"; } else if (stat_is_phase_voltage_metric(metric_id) && s.avg_abs_value() < 1.0) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = "phase voltage RMS avg_abs < 1V"; } else if (stat_is_line_voltage_metric(metric_id)) { const double line_avg = s.avg_abs_value(); bool suspicious = false; std::ostringstream reason; if (line_avg < 1.0) { suspicious = true; reason << "line voltage RMS avg_abs < 1V"; } if (phase_voltage_avg > 1.0) { const double ratio = line_avg / phase_voltage_avg; if (ratio < 1.25 || ratio > 2.20) { if (suspicious) reason << "; "; suspicious = true; reason << "line/phase ratio=" << ratio << " outside expected Wye range [1.25,2.20]"; } } if (suspicious) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = reason.str(); } } else if (stat_is_dynamic_metric(metric_id)) { // 谐波/间谐波/相角类指标可以为 0 或很小;这里只标记明显量级异常。 const double avg_abs = s.avg_abs_value(); if (avg_abs > 1000000.0) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = "dynamic spectrum metric avg_abs > 1e6"; } } else if (stat_is_voltage_sequence_component_metric(metric_id)) { // 序分量可以很小,尤其是零序/负序;这里只对明显不合理的大幅值做提示, // 不使用 <1V 判定,避免把健康系统的低负序/低零序误标为异常。 const double avg_abs = s.avg_abs_value(); if (avg_abs > 1000000.0) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = "voltage sequence component avg_abs > 1e6 V"; } } else if (stat_is_voltage_sequence_unbalance_metric(metric_id)) { const double avg_abs = s.avg_abs_value(); if (avg_abs > 1000.0) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = "voltage negative sequence unbalance avg_abs > 1000"; } } else if (stat_is_current_sequence_component_metric(metric_id)) { // 电流序分量可以为 0 或很小;这里只对明显异常的大幅值做提示。 const double avg_abs = s.avg_abs_value(); if (avg_abs > 1000000.0) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = "current sequence component avg_abs > 1e6 A"; } } else if (stat_is_current_sequence_unbalance_metric(metric_id)) { const double avg_abs = s.avg_abs_value(); if (avg_abs > 1000.0) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = "current negative sequence unbalance avg_abs > 1000"; } } else if (metric_id == StatMetricId::Frequency) { const double avg_abs = s.avg_abs_value(); if (avg_abs < 1.0 || avg_abs > 1000.0) { qi.quality = StatMetricQuality::SuspiciousRange; qi.reason = "frequency avg_abs outside [1,1000] Hz"; } } out[metric_id] = qi; } return out; } const StatMetricQualityInfo* stat_find_quality_info( const std::map& quality_by_metric, StatMetricId metric_id) { const auto it = quality_by_metric.find(metric_id); if (it == quality_by_metric.end()) return nullptr; return &it->second; } /// @brief 将一个 observation 中的单个通道实例展开为若干统计样本点。 bool stat_channel_has_explicit_harmonic_group_id(const PqdifChannelInstance& ch) { return ch.channel_group_id >= 2 && ch.channel_group_id <= 50; } bool stat_same_spectrum_channel_family( const PqdifChannelInstance& a, const PqdifChannelInstance& b) { if (a.channel_def_index != b.channel_def_index) return false; if (a.phase_id != b.phase_id) return false; if (a.quantity_measured_id != b.quantity_measured_id) return false; return stat_compact_upper(a.channel_name) == stat_compact_upper(b.channel_name); } int stat_make_channel_spectrum_order_hint( const PqdifObservationRecord& obs, const PqdifChannelInstance& ch, int& block_offset, int& block_size) { block_offset = -1; block_size = 0; // 已经有明确 group=2..50 时,必须优先走标准字段,不使用顺序推导。 if (stat_channel_has_explicit_harmonic_group_id(ch)) return -1; int offset = 0; for (const auto& other : obs.channel_instances) { if (!stat_same_spectrum_channel_family(other, ch)) continue; // 只在“同一通道族内 group/base/nominal 缺失”的谱线中按顺序补次数。 // 这样不会把已经有标准 group 的通道也混入顺序计算。 if (stat_channel_has_explicit_harmonic_group_id(other)) continue; if (other.channel_instance_index == ch.channel_instance_index) block_offset = offset; ++offset; ++block_size; } if (block_offset < 0) return -1; // 2-50 次谐波通常有 49 条谱线;个别厂家会额外带一个总量或占位, // 所以允许 >=49,但只映射前 49 条为 2..50。 if (block_size < 49) return -1; const int order = block_offset + 2; return (order >= 2 && order <= 50) ? order : -1; } /// @details /// 处理内容: /// 1) 自动解析共享时间轴; /// 2) 自动完成工程值还原; /// 3) 依据 C.2 表语义 + 相别 + 值类型 识别业务指标。 /// @param lf 完整 PQDIF 逻辑对象。 /// @param connection_kind 当前文件接线方式。 /// @param obs 当前 observation。 /// @param ch 当前通道实例。 /// @return 当前通道展开出的统计样本点集合。 std::vector stat_expand_channel_points( const PqdifLogicalFile& lf, ParsedConnectionKind connection_kind, const PqdifObservationRecord& obs, const PqdifChannelInstance& ch) { std::vector out; if (!pqdif_sem::IsQuantityTypeValueLog(ch.quantity_type_id.value)) return out; const PqdifDataSourceRecord* ds = stat_find_related_data_source(lf, obs); const PqdifChannelDefinition* ch_def = stat_find_channel_definition(ds, ch.channel_def_index); const PqdifSeriesInstance* time_series = nullptr; for (const auto& si : ch.series_instances) { if (pqdif_sem::IsValueTypeTime(si.value_type_id.value)) { time_series = stat_resolve_shared_series(obs, si); break; } } // Pst/Plt/DVC 等窗口统计量在部分设备中只有“一个值序列”,甚至不写独立时间轴。 // 这里不再因为缺失时间轴直接丢弃整个通道;后面仅允许可识别的标量统计指标使用 // observation 的 start/create time 作为兜底时间,避免 Plt 通道存在但被静默忽略。 const bool has_usable_time_series = (time_series != nullptr && time_series->values.count > 0); std::set explicit_stat_value_kinds; for (const auto& stat_probe_si : ch.series_instances) { const PqdifSeriesDefinition* stat_probe_sd = stat_find_series_definition(ch_def, stat_probe_si.series_def_index); const StatValueKind probe_kind = stat_identify_value_kind(stat_probe_si, stat_probe_sd); if (probe_kind != StatValueKind::Unknown) explicit_stat_value_kinds.insert(probe_kind); } const bool channel_has_single_or_no_stat_value_kind = explicit_stat_value_kinds.size() <= 1; for (size_t i = 0; i < ch.series_instances.size(); ++i) { const auto& si = ch.series_instances[i]; const PqdifSeriesDefinition* sd = stat_find_series_definition(ch_def, si.series_def_index); const StatValueKind stat_kind = stat_identify_value_kind(si, sd); const PqdifSeriesInstance* resolved_value_series = stat_resolve_shared_series(obs, si); if (resolved_value_series == nullptr || resolved_value_series->values.count <= 0) continue; const unsigned int storage_method_id = (sd != nullptr) ? sd->storage_method_id : 0; const size_t value_count = static_cast(std::max(resolved_value_series->values.count, 0)); const size_t time_count = has_usable_time_series ? static_cast(std::max(time_series->values.count, 0)) : value_count; const size_t point_count = std::min(time_count, value_count); for (size_t k = 0; k < point_count; ++k) { time_t ts = 0; if (has_usable_time_series) { if (!stat_resolve_timestamp_at(obs, *time_series, k, ts)) continue; } else { // 单值统计通道无时间轴时,使用 observation 起始时间兜底;没有起始时间则用创建时间。 ts = (obs.time_start.unix_time != 0) ? obs.time_start.unix_time : obs.time_create.unix_time; if (ts == 0) ts = std::time(nullptr); if (k > 0) ts += static_cast(k); } double raw_value = 0.0; if (!stat_try_get_raw_numeric_at(resolved_value_series->values, k, raw_value)) continue; ExpandedStatPoint p; p.timestamp = ts; p.timestamp_text = format_time_text(ts); p.observation_index = obs.observation_index; p.channel_instance_index = ch.channel_instance_index; p.channel_def_index = ch.channel_def_index; p.channel_group_id = ch.channel_group_id; p.channel_spectrum_order_hint = stat_make_channel_spectrum_order_hint( obs, ch, p.channel_spectrum_block_offset, p.channel_spectrum_block_size); p.series_instance_index = static_cast(i); p.series_def_index = si.series_def_index; p.sample_index = static_cast(k); p.observation_name = obs.observation_name; p.channel_name = ch.channel_name; p.quantity_name = (ch_def != nullptr) ? ch_def->quantity_name : std::string(); p.phase_id = ch.phase_id; p.quantity_type_id = ch.quantity_type_id; p.quantity_measured_id = ch.quantity_measured_id; p.quantity_units_id = si.quantity_units_id; p.quantity_characteristic_id = si.quantity_characteristic_id; p.value_type_id = si.value_type_id; p.prob_percentile = (sd != nullptr) ? sd->prob_percentile : 0.0; p.series_base_quantity = si.series_base_quantity; p.nominal_quantity = si.nominal_quantity; p.connection_kind = connection_kind; p.stat_kind = stat_kind; p.value = stat_decode_engineering_value( raw_value, *resolved_value_series, storage_method_id, resolved_value_series->values.physical_type); p.metric_id = stat_identify_metric_id(connection_kind, p, p.matched_by_name_fallback); if (p.metric_id == StatMetricId::Unknown) continue; const bool scalar_stat_metric = stat_metric_can_use_scalar_series_as_all_kinds(p.metric_id) && stat_series_can_be_scalar_stat_value(si, sd); // 没有时间轴的通道只允许 Pst/Plt/DVC 这类窗口标量指标走兜底, // 防止其它普通统计通道在无时间轴时被错误展开。 if (!has_usable_time_series && !scalar_stat_metric) continue; // 修复 Pst/Plt:很多设备把 Pst/Plt 写成单个 Avg/Value 序列,而不是四条 // Min/Max/Avg/P95 序列。只要该通道只有单个统计序列或无明确统计值类型,就用同一物理值补齐四种 // 统计值,确保最终 bucket 与 METRIC STATUS 都能看到完整 max/min/avg/p95。 if (scalar_stat_metric && channel_has_single_or_no_stat_value_kind) { const StatValueKind fallback_kinds[] = { StatValueKind::Min, StatValueKind::Max, StatValueKind::Avg, StatValueKind::P95 }; for (const StatValueKind fallback_kind : fallback_kinds) { ExpandedStatPoint q = p; q.stat_kind = fallback_kind; out.push_back(std::move(q)); } continue; } if (stat_kind != StatValueKind::Unknown) { out.push_back(std::move(p)); continue; } } } return out; } bool stat_metric_in_primary_targets(StatMetricId id) { const std::vector& targets = stat_primary_metric_print_order(); return std::find(targets.begin(), targets.end(), id) != targets.end(); } /// @brief 从全部 observation 中补齐主 observation 缺失的目标指标。 /// @details /// 统一 fallback 策略:所有指标都先从主统计 observation 解析;如果某个目标指标在主 /// observation 中完全找不到,则遍历其他 observations,直到找到该指标的数据来源。 /// 这样后续新增谐波、间谐波、电流谐波等指标时,不需要为每类指标写一套独立的 /// observation 查找流程。 std::vector stat_expand_missing_metrics_from_all_observations( const PqdifLogicalFile& lf, ParsedConnectionKind connection_kind, const std::vector& primary_points, int selected_observation_index) { std::vector out; std::set present_metrics; for (const auto& p : primary_points) { if (p.metric_id != StatMetricId::Unknown) present_metrics.insert(p.metric_id); } std::set missing_targets; for (const auto metric_id : stat_primary_metric_print_order()) { if (present_metrics.find(metric_id) == present_metrics.end()) missing_targets.insert(metric_id); } if (missing_targets.empty()) return out; std::map> fallback_by_metric; for (const auto& obs : lf.observations) { if (obs.observation_index == selected_observation_index) continue; for (const auto& ch : obs.channel_instances) { auto points = stat_expand_channel_points(lf, connection_kind, obs, ch); for (auto& p : points) { if (missing_targets.find(p.metric_id) == missing_targets.end()) continue; fallback_by_metric[p.metric_id].push_back(std::move(p)); } } // 已经为所有缺失指标找到了候选点,可以停止继续扫后续 observation。 bool all_found = true; for (const auto metric_id : missing_targets) { if (fallback_by_metric.find(metric_id) == fallback_by_metric.end()) { all_found = false; break; } } if (all_found) break; } for (const auto metric_id : stat_primary_metric_print_order()) { const auto it = fallback_by_metric.find(metric_id); if (it == fallback_by_metric.end()) continue; out.insert(out.end(), std::make_move_iterator(it->second.begin()), std::make_move_iterator(it->second.end())); } if (!out.empty() && pqdif_log_enabled(PqdifLogLevel::Info)) { size_t dynamic_metrics = 0; size_t core_metrics = 0; std::set loaded_metrics; for (const auto& p : out) loaded_metrics.insert(p.metric_id); for (const auto metric_id : loaded_metrics) { if (stat_is_dynamic_metric(metric_id)) ++dynamic_metrics; else ++core_metrics; } std::cout << "[PQDIF] observation fallback loaded " << out.size() << " points for " << loaded_metrics.size() << " missing metrics" << " (core=" << core_metrics << ", dynamic_spectrum=" << dynamic_metrics << ")" << std::endl; } else if (out.empty() && pqdif_log_enabled(PqdifLogLevel::Debug)) { std::cout << "[PQDIF] observation fallback: no missing target metric found in other observations" << std::endl; } return out; } bool stat_has_any_voltage_harmonic_points(const std::vector& points) { for (const auto& p : points) { if (stat_is_voltage_harmonic_metric(p.metric_id)) return true; } return false; } /// @brief 展开统计样本点,并对缺失指标执行 observation fallback。 /// @details /// 第一阶段仍以主统计 observation 为基础,避免普通 RMS/频率/序分量在多 observation 之间互相覆盖。 /// 第二阶段针对当前已知会出现在其他 observation 的三相电压谐波 RMS,遍历全部 observations 补充。 /// 后续新增间谐波、电流谐波等指标时,应优先复用这种“主 observation + 指标族 fallback”的模式。 std::vector stat_expand_selected_statistical_observation( const PqdifLogicalFile& lf, ParsedConnectionKind connection_kind, int& selected_observation_index, std::string& selected_observation_name) { selected_observation_index = -1; selected_observation_name.clear(); std::vector out; const PqdifObservationRecord* selected = stat_select_primary_statistical_observation(lf); if (selected != nullptr) { selected_observation_index = selected->observation_index; selected_observation_name = selected->observation_name; for (const auto& ch : selected->channel_instances) { auto points = stat_expand_channel_points(lf, connection_kind, *selected, ch); out.insert(out.end(), std::make_move_iterator(points.begin()), std::make_move_iterator(points.end())); } } // 对主 observation 缺失的目标指标执行统一 observation fallback。 // 例如:普通趋势指标可能在 Trend observation,而 V HRMS A/B/C 2-50 次谐波在另一条 observation。 auto fallback_points = stat_expand_missing_metrics_from_all_observations( lf, connection_kind, out, selected_observation_index); if (!fallback_points.empty()) { out.insert(out.end(), std::make_move_iterator(fallback_points.begin()), std::make_move_iterator(fallback_points.end())); } // 对同一 metric 的多来源候选流先做择优,避免后续按 timestamp 聚合时静默覆盖。 return stat_select_best_metric_sources(out); } bool pqdif_probe_text_looks_like_flicker(const std::string& text) { const std::string key = normalize_key(text); return key.find("FLICKER") != std::string::npos || key.find("FLKR") != std::string::npos || key.find("PST") != std::string::npos || key.find("PLT") != std::string::npos || key.find("DVC") != std::string::npos || key.find("DELTAV") != std::string::npos; } /// @brief DEBUG 级别闪变候选通道诊断。 /// @details 用于核查 Pst/Plt/DVC 通道是否真实有数据;尤其是 Plt 通道存在但 values.count=0 时, /// 这里能直接看出来,避免误以为是名称匹配失败。 void dump_flicker_candidate_probe(const ParsedPqdifFile& parsed_file) { const auto& lf = parsed_file.logical_file; std::cout << "========== PQDIF FLICKER CANDIDATE PROBE V18 ==========" << std::endl; std::cout << "file=" << parsed_file.source_file << std::endl; std::cout << "rule=DEBUG level: print all Pst/Plt/DVC/Flicker candidate channels and series counts" << std::endl; size_t hit_count = 0; size_t plt_channel_count = 0; size_t plt_value_point_count = 0; for (const auto& obs : lf.observations) { const PqdifDataSourceRecord* ds = stat_find_related_data_source(lf, obs); for (const auto& ch : obs.channel_instances) { const PqdifChannelDefinition* ch_def = stat_find_channel_definition(ds, ch.channel_def_index); bool candidate = pqdif_probe_text_looks_like_flicker(ch.channel_name) || pqdif_probe_text_looks_like_flicker(ch.quantity_type_id.symbolic_name); if (ch_def != nullptr) { candidate = candidate || pqdif_probe_text_looks_like_flicker(ch_def->channel_name) || pqdif_probe_text_looks_like_flicker(ch_def->quantity_name); } for (const auto& si : ch.series_instances) { candidate = candidate || pqdif_probe_text_looks_like_flicker(si.value_type_id.symbolic_name) || pqdif_probe_text_looks_like_flicker(si.quantity_characteristic_id.symbolic_name); } if (!candidate) continue; ++hit_count; const std::string compact_name = normalize_key(ch.channel_name + " " + ((ch_def != nullptr) ? ch_def->channel_name : std::string())); const bool is_plt_candidate = compact_name.find("PLT") != std::string::npos; if (is_plt_candidate) ++plt_channel_count; std::cout << " [FLICKER-CH] obs=" << obs.observation_index << ", obs_name=" << obs.observation_name << ", obs_start=" << obs.time_start.text << ", ch=" << ch.channel_instance_index << ", ch_def=" << ch.channel_def_index << ", channel=" << ch.channel_name << ", def_channel=" << ((ch_def != nullptr) ? ch_def->channel_name : std::string()) << ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id) << ", series_instances=" << ch.series_instances.size() << std::endl; for (size_t si_index = 0; si_index < ch.series_instances.size(); ++si_index) { const auto& si = ch.series_instances[si_index]; const PqdifSeriesDefinition* sd = stat_find_series_definition(ch_def, si.series_def_index); const PqdifSeriesInstance* resolved = stat_resolve_shared_series(obs, si); const long value_count = (resolved != nullptr) ? resolved->values.count : -1; if (is_plt_candidate && value_count > 0) plt_value_point_count += static_cast(value_count); std::cout << " [FLICKER-SER] ser=" << si_index << ", ser_def=" << si.series_def_index << ", value_type=" << short_guid_name(si.value_type_id) << ", characteristic=" << short_guid_name(si.quantity_characteristic_id) << ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id) << ", resolved_count=" << value_count; if (resolved != nullptr && value_count > 0) { double raw_value = 0.0; if (stat_try_get_raw_numeric_at(resolved->values, 0, raw_value)) { const unsigned int storage_method_id = (sd != nullptr) ? sd->storage_method_id : 0; const double first_value = stat_decode_engineering_value( raw_value, *resolved, storage_method_id, resolved->values.physical_type); std::cout << ", first=" << first_value; } } std::cout << std::endl; } } } std::cout << " [FLICKER SUMMARY] candidate_channels=" << hit_count << ", plt_candidate_channels=" << plt_channel_count << ", plt_value_points=" << plt_value_point_count << std::endl; std::cout << "===================================================" << std::endl; } /// @brief 将单个样本点应用到同一时间桶里的某个指标聚合值上。 /// @details /// 这里不再允许静默覆盖:同一 timestamp + metric + kind 再次写入时,保留首次写入值, /// 并把该 metric 标记为 DuplicateSource,同时打印冲突来源。 /// @param bucket 目标时间桶。 /// @param p 已识别统计样本点。 /// @param quality_by_metric 指标级质量状态。 void stat_apply_point_to_bucket( TimeAggregatedStatBucket& bucket, const ExpandedStatPoint& p, const std::map& quality_by_metric) { auto& agg = bucket.metrics[p.metric_id]; const StatMetricQualityInfo* qi = stat_find_quality_info(quality_by_metric, p.metric_id); if (qi != nullptr && agg.quality == StatMetricQuality::Normal) { agg.quality = qi->quality; agg.quality_reason = qi->reason; } if (agg.source_observation_index < 0) { agg.source_observation_index = p.observation_index; agg.source_channel_instance_index = p.channel_instance_index; agg.source_channel_name = p.channel_name; } if (stat_has_value_kind(agg, p.stat_kind)) { const double old_value = stat_get_value_by_kind(agg, p.stat_kind); std::ostringstream reason; reason << "duplicate write: kind=" << stat_value_kind_name(p.stat_kind) << ", old_value=" << old_value << ", new_value=" << p.value << ", old_ch=" << agg.source_channel_instance_index << ", new_ch=" << p.channel_instance_index; agg.quality = StatMetricQuality::DuplicateSource; agg.quality_reason = reason.str(); static size_t duplicate_print_count = 0; if (duplicate_print_count < 80) { std::cout << " [DUPLICATE STAT VALUE]" << " time=" << p.timestamp_text << ", metric=" << stat_metric_name(p.metric_id) << ", kind=" << stat_value_kind_name(p.stat_kind) << ", old_value=" << old_value << ", new_value=" << p.value << ", old_ch=" << agg.source_channel_instance_index << ", new_ch=" << p.channel_instance_index << ", new_ser=" << p.series_instance_index << ", new_channel=" << p.channel_name << std::endl; ++duplicate_print_count; if (duplicate_print_count == 80) { std::cout << " [DUPLICATE STAT VALUE] print limit reached, further duplicate details suppressed" << std::endl; } } // 调试和修复阶段保留首次写入,拒绝覆盖,避免结果继续被污染。 return; } agg.source_series_instance_index = p.series_instance_index; switch (p.stat_kind) { case StatValueKind::Min: agg.has_min = true; agg.min_value = p.value; break; case StatValueKind::Max: agg.has_max = true; agg.max_value = p.value; break; case StatValueKind::Avg: agg.has_avg = true; agg.avg_value = p.value; break; case StatValueKind::P95: agg.has_p95 = true; agg.p95_value = p.value; break; default: break; } } /// @brief 将统计样本点按 timestamp 聚合成时间桶。 /// @details /// 当前阶段已经先筛掉非统计 observation,并做过 metric 来源择优;这里仍保留覆盖检测, /// 用于防止后续新增指标时重新引入静默覆盖。 /// @param points 已识别统计样本点。 /// @return 聚合后的时间桶数组,按时间升序输出。 std::vector stat_group_points_by_timestamp( const std::vector& points) { const std::map quality_by_metric = stat_analyze_metric_quality(points); std::map buckets; for (const auto& p : points) { auto& bucket = buckets[p.timestamp]; if (bucket.timestamp == 0) { bucket.timestamp = p.timestamp; bucket.timestamp_text = p.timestamp_text; } stat_apply_point_to_bucket(bucket, p, quality_by_metric); } std::vector out; out.reserve(buckets.size()); for (auto& kv : buckets) out.push_back(std::move(kv.second)); return out; } void stat_print_aggregated_metric_line(StatMetricId metric_id, const AggregatedStatValues* agg) { std::cout << " metric=" << stat_metric_name(metric_id); if (agg == nullptr) { std::cout << ", quality=" << stat_metric_quality_name(StatMetricQuality::Missing) << std::endl; return; } if (agg->has_min) std::cout << ", min=" << agg->min_value; else std::cout << ", min=N/A"; if (agg->has_max) std::cout << ", max=" << agg->max_value; else std::cout << ", max=N/A"; if (agg->has_avg) std::cout << ", avg=" << agg->avg_value; else std::cout << ", avg=N/A"; if (agg->has_p95) std::cout << ", p95=" << agg->p95_value; else std::cout << ", p95=N/A"; std::cout << ", quality=" << stat_metric_quality_name(agg->quality); if (!agg->quality_reason.empty()) std::cout << ", reason=" << agg->quality_reason; if (agg->source_channel_instance_index >= 0) { std::cout << ", source_ch=" << agg->source_channel_instance_index; if (!agg->source_channel_name.empty()) std::cout << ", source_channel=" << agg->source_channel_name; } std::cout << std::endl; } size_t stat_count_present_metrics(const std::vector& points) { std::set ids; for (const auto& p : points) ids.insert(p.metric_id); return ids.size(); } size_t stat_count_present_voltage_harmonic_metrics(const std::vector& points) { std::set ids; for (const auto& p : points) { if (stat_is_voltage_harmonic_metric(p.metric_id)) ids.insert(p.metric_id); } return ids.size(); } std::vector stat_present_voltage_harmonic_metrics( const std::map& stats_by_metric) { std::vector out; for (const auto& kv : stats_by_metric) { if (stat_is_voltage_harmonic_metric(kv.first)) out.push_back(kv.first); } std::sort(out.begin(), out.end(), [](StatMetricId a, StatMetricId b) { return static_cast(a) < static_cast(b); }); return out; } const char* stat_dynamic_group_display_name(StatDynamicMetricGroup group) { switch (group) { case StatDynamicMetricGroup::VoltageHarmonic: return "VoltageHarmonic"; case StatDynamicMetricGroup::LineVoltageHarmonic: return "LineVoltageHarmonic"; case StatDynamicMetricGroup::CurrentHarmonic: return "CurrentHarmonic"; case StatDynamicMetricGroup::VoltageHarmonicAngle: return "VoltageHarmonicAngle"; case StatDynamicMetricGroup::LineVoltageHarmonicAngle: return "LineVoltageHarmonicAngle"; case StatDynamicMetricGroup::CurrentHarmonicAngle: return "CurrentHarmonicAngle"; case StatDynamicMetricGroup::HarmonicActivePower: return "HarmonicActivePower"; case StatDynamicMetricGroup::HarmonicReactivePower: return "HarmonicReactivePower"; case StatDynamicMetricGroup::HarmonicApparentPower: return "HarmonicApparentPower"; case StatDynamicMetricGroup::VoltageHarmonicRatio: return "VoltageHarmonicRatio"; case StatDynamicMetricGroup::LineVoltageHarmonicRatio: return "LineVoltageHarmonicRatio"; case StatDynamicMetricGroup::CurrentHarmonicRatio: return "CurrentHarmonicRatio"; case StatDynamicMetricGroup::VoltageInterharmonic: return "VoltageInterharmonic"; case StatDynamicMetricGroup::LineVoltageInterharmonic: return "LineVoltageInterharmonic"; case StatDynamicMetricGroup::CurrentInterharmonic: return "CurrentInterharmonic"; default: return "UnknownDynamicGroup"; } } size_t stat_dynamic_group_expected_count(StatDynamicMetricGroup group) { return stat_dynamic_metric_order_for_group(group).size(); } std::vector stat_present_dynamic_group_metrics( const std::map& stats_by_metric, StatDynamicMetricGroup group) { std::vector out; for (const auto& kv : stats_by_metric) { if (stat_is_dynamic_metric_group(kv.first, group)) out.push_back(kv.first); } std::sort(out.begin(), out.end(), [](StatMetricId a, StatMetricId b) { return static_cast(a) < static_cast(b); }); return out; } size_t stat_count_present_dynamic_group_metrics( const std::vector& points, StatDynamicMetricGroup group) { std::set ids; for (const auto& p : points) { if (stat_is_dynamic_metric_group(p.metric_id, group)) ids.insert(p.metric_id); } return ids.size(); } size_t stat_count_present_all_dynamic_metrics(const std::vector& points) { std::set ids; for (const auto& p : points) { if (stat_is_dynamic_metric(p.metric_id)) ids.insert(p.metric_id); } return ids.size(); } const std::vector& stat_dynamic_summary_groups() { static const std::vector groups = { StatDynamicMetricGroup::VoltageHarmonic, StatDynamicMetricGroup::LineVoltageHarmonic, StatDynamicMetricGroup::CurrentHarmonic, StatDynamicMetricGroup::VoltageHarmonicAngle, StatDynamicMetricGroup::LineVoltageHarmonicAngle, StatDynamicMetricGroup::CurrentHarmonicAngle, StatDynamicMetricGroup::HarmonicActivePower, StatDynamicMetricGroup::HarmonicReactivePower, StatDynamicMetricGroup::HarmonicApparentPower, StatDynamicMetricGroup::VoltageHarmonicRatio, StatDynamicMetricGroup::LineVoltageHarmonicRatio, StatDynamicMetricGroup::CurrentHarmonicRatio, StatDynamicMetricGroup::VoltageInterharmonic, StatDynamicMetricGroup::LineVoltageInterharmonic, StatDynamicMetricGroup::CurrentInterharmonic }; return groups; } void stat_print_dynamic_group_compact_summaries( const std::map& stats_by_metric) { for (auto group : stat_dynamic_summary_groups()) { const std::vector present = stat_present_dynamic_group_metrics(stats_by_metric, group); std::cout << " [DYNAMIC SPECTRUM SUMMARY] group=" << stat_dynamic_group_display_name(group) << ", present=" << present.size() << "/" << stat_dynamic_group_expected_count(group); if (!present.empty()) { const double first_order = stat_dynamic_metric_order_value(present.front()); const double last_order = stat_dynamic_metric_order_value(present.back()); std::cout << ", range=" << first_order << "-" << last_order; } std::cout << std::endl; } } void stat_print_voltage_harmonic_compact_summary( const std::map& stats_by_metric) { bool present[3][51] = {}; for (const auto& kv : stats_by_metric) { if (!stat_is_voltage_harmonic_metric(kv.first)) continue; const int phase = stat_voltage_harmonic_phase_index(kv.first); const int order = stat_voltage_harmonic_order(kv.first); if (phase >= 0 && phase < 3 && order >= 2 && order <= 50) present[phase][order] = true; } size_t total_present = 0; for (int phase = 0; phase < 3; ++phase) { for (int order = 2; order <= 50; ++order) { if (present[phase][order]) ++total_present; } } std::cout << " [VOLTAGE HARMONIC SUMMARY] present=" << total_present << "/147"; const char* phase_names[3] = { "A", "B", "C" }; for (int phase = 0; phase < 3; ++phase) { int first = -1; int last = -1; int count = 0; for (int order = 2; order <= 50; ++order) { if (!present[phase][order]) continue; if (first < 0) first = order; last = order; ++count; } std::cout << ", " << phase_names[phase] << "="; if (count == 0) std::cout << "missing"; else if (count == 49) std::cout << "2-50"; else std::cout << "count:" << count << " range:" << first << "-" << last; } std::cout << std::endl; } /// @brief 打印已展开统计样本点预览。 /// @details /// 不再只打印前 12 个样本点,而是按“主要指标 × Min/Max/Avg/P95”打印每个流的首样本, /// 这样后续新增指标时可以直接看出哪个 metric/kind 缺失或来自哪个通道。 /// @param parsed_file 当前已解析文件对象。 void dump_expanded_stat_preview(const ParsedPqdifFile& parsed_file) { const auto& points = parsed_file.expanded_stat_points; if (!pqdif_is_trace_log_enabled()) { std::cout << "========== EXPANDED STAT SUMMARY ==========" << std::endl; std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind) << ", selected_observation_index=" << parsed_file.selected_observation_index << ", selected_observation_name=" << parsed_file.selected_observation_name << ", selected_points=" << points.size() << ", present_metrics=" << stat_count_present_metrics(points) << ", present_dynamic_spectrum=" << stat_count_present_all_dynamic_metrics(points) << "/" << stat_all_dynamic_metric_order().size() << ", present_voltage_harmonics=" << stat_count_present_voltage_harmonic_metrics(points) << "/147" << std::endl; std::cout << "===========================================" << std::endl; return; } std::cout << "========== EXPANDED STAT PREVIEW ==========" << std::endl; std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind) << ", selected_observation_index=" << parsed_file.selected_observation_index << ", selected_observation_name=" << parsed_file.selected_observation_name << ", selected_points=" << points.size() << std::endl; typedef std::pair StreamKey; std::map first_point_by_stream; for (const auto& p : points) { StreamKey key(p.metric_id, p.stat_kind); if (first_point_by_stream.find(key) == first_point_by_stream.end()) first_point_by_stream[key] = &p; } const StatValueKind kinds[4] = { StatValueKind::Min, StatValueKind::Max, StatValueKind::Avg, StatValueKind::P95 }; std::cout << " [PRIMARY METRIC STREAMS] metric_slots=" << stat_primary_metric_print_order().size() << ", stream_slots=" << stat_primary_metric_print_order().size() * 4 << std::endl; for (const auto metric_id : stat_primary_metric_print_order()) { for (int i = 0; i < 4; ++i) { const StatValueKind kind = kinds[i]; StreamKey key(metric_id, kind); const auto it = first_point_by_stream.find(key); std::cout << " [STREAM]" << " metric=" << stat_metric_name(metric_id) << ", kind=" << stat_value_kind_name(kind); if (it == first_point_by_stream.end()) { std::cout << ", status=MISSING" << std::endl; continue; } const ExpandedStatPoint& p = *it->second; std::cout << ", status=OK" << ", first_time=" << p.timestamp_text << ", first_value=" << p.value << ", obs=" << p.observation_index << ", ch=" << p.channel_instance_index << ", ch_def=" << p.channel_def_index << ", group=" << p.channel_group_id << ", ser=" << p.series_instance_index << ", ser_def=" << p.series_def_index << ", channel=" << p.channel_name << ", phase=" << pqdif_sem::FindPhaseName(p.phase_id) << ", measured=" << pqdif_sem::FindQuantityMeasuredName(p.quantity_measured_id) << ", unit=" << pqdif_sem::FindQuantityUnitsName(p.quantity_units_id) << (p.matched_by_name_fallback ? ", by_name=true" : "") << std::endl; } } // 扩展指标只有命中时才打印,避免干扰当前 14 项核查。 for (const auto metric_id : stat_extra_metric_print_order()) { bool has_any = false; for (int i = 0; i < 4; ++i) { StreamKey key(metric_id, kinds[i]); if (first_point_by_stream.find(key) != first_point_by_stream.end()) { has_any = true; break; } } if (!has_any) continue; for (int i = 0; i < 4; ++i) { StreamKey key(metric_id, kinds[i]); const auto it = first_point_by_stream.find(key); std::cout << " [EXTRA STREAM]" << " metric=" << stat_metric_name(metric_id) << ", kind=" << stat_value_kind_name(kinds[i]); if (it == first_point_by_stream.end()) { std::cout << ", status=MISSING" << std::endl; continue; } const ExpandedStatPoint& p = *it->second; std::cout << ", status=OK" << ", first_time=" << p.timestamp_text << ", first_value=" << p.value << ", ch=" << p.channel_instance_index << ", channel=" << p.channel_name << std::endl; } } std::cout << "===========================================" << std::endl; } /// @brief 打印时间聚合桶预览。 /// @details /// 每个预览桶固定打印当前核查的主要指标,不再用 shown>=12 截断。 /// 若某项缺失,会显式打印 quality=MISSING。 /// @param parsed_file 当前已解析文件对象。 void dump_grouped_bucket_preview(const ParsedPqdifFile& parsed_file) { if (!pqdif_is_trace_log_enabled()) { std::cout << "========== GROUPED STAT CORE SUMMARY ==========" << std::endl; std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind) << ", selected_observation_index=" << parsed_file.selected_observation_index << ", selected_observation_name=" << parsed_file.selected_observation_name << ", expanded_points=" << parsed_file.expanded_stat_points.size() << ", buckets=" << parsed_file.aggregated_stat_buckets.size() << ", core_metric_slots=" << stat_core_metric_print_order().size() << ", dynamic_spectrum_slots=" << stat_all_dynamic_metric_order().size() << std::endl; std::map stats_by_metric = stat_collect_metric_stats(parsed_file.expanded_stat_points); std::map quality_by_metric = stat_analyze_metric_quality(parsed_file.expanded_stat_points); stat_print_dynamic_group_compact_summaries(stats_by_metric); const size_t bucket_limit = std::min(parsed_file.aggregated_stat_buckets.size(), 3); for (size_t i = 0; i < bucket_limit; ++i) { const auto& b = parsed_file.aggregated_stat_buckets[i]; std::cout << " [BUCKET " << i << "]" << " time=" << b.timestamp_text << ", metric_count_present=" << b.metrics.size() << std::endl; for (const auto metric_id : stat_core_metric_print_order()) { const auto it = b.metrics.find(metric_id); stat_print_aggregated_metric_line( metric_id, it == b.metrics.end() ? nullptr : &it->second); } // 核心日志仅抽样打印 2/3/5 次谐波,避免每桶输出 147 行。 const int sample_orders[] = { 2, 3, 5 }; bool printed_header = false; for (int phase = 0; phase < 3; ++phase) { for (int oi = 0; oi < 3; ++oi) { const StatMetricId hid = stat_voltage_harmonic_metric_id(phase, sample_orders[oi]); const auto hit = b.metrics.find(hid); if (hit == b.metrics.end()) continue; if (!printed_header) { std::cout << " [VOLTAGE HARMONIC SAMPLE] orders=2/3/5" << std::endl; printed_header = true; } stat_print_aggregated_metric_line(hid, &hit->second); } } } std::cout << "========== METRIC STATUS CORE SUMMARY ==========" << std::endl; for (const auto metric_id : stat_core_metric_print_order()) { const auto stats_it = stats_by_metric.find(metric_id); const auto quality_it = quality_by_metric.find(metric_id); std::cout << " [METRIC STATUS] metric=" << stat_metric_name(metric_id); if (stats_it == stats_by_metric.end()) { std::cout << ", quality=" << stat_metric_quality_name(StatMetricQuality::Missing) << ", points=0" << std::endl; continue; } const StatMetricSourceStats& st = stats_it->second; const StatMetricQuality quality = quality_it == quality_by_metric.end() ? StatMetricQuality::Missing : quality_it->second.quality; const std::string reason = quality_it == quality_by_metric.end() ? std::string("missing") : quality_it->second.reason; std::cout << ", quality=" << stat_metric_quality_name(quality) << ", reason=" << reason << ", points=" << st.point_count << ", source_obs=" << st.key.observation_index << ", source_ch=" << st.key.channel_instance_index << ", source_channel=" << st.channel_name << ", min=" << st.min_value << ", max=" << st.max_value << ", avg_abs=" << st.avg_abs_value() << std::endl; } for (auto group : stat_dynamic_summary_groups()) { const std::vector present = stat_present_dynamic_group_metrics(stats_by_metric, group); std::cout << " [DYNAMIC SPECTRUM STATUS] group=" << stat_dynamic_group_display_name(group) << ", present_metrics=" << present.size() << "/" << stat_dynamic_group_expected_count(group) << std::endl; size_t sample_count = 0; for (const auto metric_id : present) { if (sample_count >= 6) break; const auto stats_it = stats_by_metric.find(metric_id); if (stats_it == stats_by_metric.end()) continue; const StatMetricSourceStats& st = stats_it->second; std::cout << " [DYNAMIC STATUS SAMPLE] metric=" << stat_metric_name(metric_id) << ", points=" << st.point_count << ", source_obs=" << st.key.observation_index << ", source_ch=" << st.key.channel_instance_index << ", order=" << stat_dynamic_metric_order_value(metric_id) << ", source_channel=" << st.channel_name << ", min=" << st.min_value << ", max=" << st.max_value << ", avg_abs=" << st.avg_abs_value() << std::endl; ++sample_count; } } std::cout << "=================================================" << std::endl; return; } std::cout << "========== GROUPED STAT BUCKET PREVIEW ==========" << std::endl; std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind) << ", selected_observation_index=" << parsed_file.selected_observation_index << ", selected_observation_name=" << parsed_file.selected_observation_name << ", expanded_points=" << parsed_file.expanded_stat_points.size() << ", buckets=" << parsed_file.aggregated_stat_buckets.size() << ", primary_metric_slots=" << stat_primary_metric_print_order().size() << std::endl; const size_t bucket_limit = std::min(parsed_file.aggregated_stat_buckets.size(), 3); for (size_t i = 0; i < bucket_limit; ++i) { const auto& b = parsed_file.aggregated_stat_buckets[i]; std::cout << " [BUCKET " << i << "]" << " time=" << b.timestamp_text << ", metric_count_present=" << b.metrics.size() << std::endl; for (const auto metric_id : stat_primary_metric_print_order()) { const auto it = b.metrics.find(metric_id); stat_print_aggregated_metric_line( metric_id, it == b.metrics.end() ? nullptr : &it->second); } for (const auto metric_id : stat_extra_metric_print_order()) { const auto it = b.metrics.find(metric_id); if (it != b.metrics.end()) stat_print_aggregated_metric_line(metric_id, &it->second); } } std::cout << "========== METRIC STATUS SUMMARY ==========" << std::endl; std::map stats_by_metric = stat_collect_metric_stats(parsed_file.expanded_stat_points); std::map quality_by_metric = stat_analyze_metric_quality(parsed_file.expanded_stat_points); for (const auto metric_id : stat_primary_metric_print_order()) { const auto stats_it = stats_by_metric.find(metric_id); const auto quality_it = quality_by_metric.find(metric_id); std::cout << " [METRIC STATUS] metric=" << stat_metric_name(metric_id); if (stats_it == stats_by_metric.end()) { std::cout << ", quality=" << stat_metric_quality_name(StatMetricQuality::Missing) << ", points=0" << std::endl; continue; } const StatMetricSourceStats& s = stats_it->second; const StatMetricQuality quality = quality_it == quality_by_metric.end() ? StatMetricQuality::Missing : quality_it->second.quality; const std::string reason = quality_it == quality_by_metric.end() ? std::string("missing") : quality_it->second.reason; std::cout << ", quality=" << stat_metric_quality_name(quality) << ", reason=" << reason << ", points=" << s.point_count << ", source_ch=" << s.key.channel_instance_index << ", source_channel=" << s.channel_name << ", min=" << s.min_value << ", max=" << s.max_value << ", avg_abs=" << s.avg_abs_value() << std::endl; } std::cout << "=================================================" << std::endl; } bool parse_container_record(CPQDIF& file_convert, CPQDIF_R_General* record, long record_index, PqdifContainerRecord& out) { if (record == nullptr) return false; out.header = build_record_header_info(record, record_index); CPQDIF_E_Collection* main = record->GetMainCollection(); if (main == nullptr) return true; out.version_info = read_vector_uint_values(main, tagVersionInfo); read_string_tag(main, tagFileName, out.file_name); read_timestamp_tag(main, tagCreation, file_convert, out.creation_time); read_timestamp_tag(main, tagLastSaved, file_convert, out.last_saved_time); read_uint_tag(main, tagTimesSaved, out.times_saved); read_string_tag(main, tagLanguage, out.language); read_string_tag(main, tagTitle, out.title); read_string_tag(main, tagSubject, out.subject); read_string_tag(main, tagAuthor, out.author); read_string_tag(main, tagKeywords, out.keywords); read_string_tag(main, tagComments, out.comments); read_string_tag(main, tagLastSavedBy, out.last_saved_by); read_string_tag(main, tagApplication, out.application); read_guid_tag(main, tagCompressionStyleID, out.compression_style_id); read_uint_tag(main, tagCompressionAlgorithmID, out.compression_algorithm_id); read_uint_tag(main, tagCompressionChecksum, out.compression_checksum); read_string_tag(main, tagOwner, out.owner); read_string_tag(main, tagCopyright, out.copyright); read_string_tag(main, tagTrademarks, out.trademarks); read_string_tag(main, tagNotes, out.notes); read_string_tag(main, tagAddress1, out.address1); read_string_tag(main, tagAddress2, out.address2); read_string_tag(main, tagCity, out.city); read_string_tag(main, tagState, out.state); read_string_tag(main, tagPostalCode, out.postal_code); read_string_tag(main, tagCountry, out.country); read_string_tag(main, tagPhoneVoice, out.phone_voice); read_string_tag(main, tagPhoneFAX, out.phone_fax); read_string_tag(main, tagEMail, out.email); const std::set known = { "tagVersionInfo", "tagFileName", "tagCreation", "tagLastSaved", "tagTimesSaved", "tagLanguage", "tagTitle", "tagSubject", "tagAuthor", "tagKeywords", "tagComments", "tagLastSavedBy", "tagApplication", "tagCompressionStyleID", "tagCompressionAlgorithmID", "tagCompressionChecksum", "tagOwner", "tagCopyright", "tagTrademarks", "tagNotes", "tagAddress1", "tagAddress2", "tagCity", "tagState", "tagPostalCode", "tagCountry", "tagPhoneVoice", "tagPhoneFAX", "tagEMail" }; collect_extra_tags(main, known, file_convert, out.extra_tags); return true; } bool parse_data_source_record(CPQDIF& file_convert, CPQDIF_R_DataSource* record, long record_index, int data_source_index, PqdifDataSourceRecord& out) { if (record == nullptr) return false; out.header = build_record_header_info(record, record_index); out.data_source_index = data_source_index; out.record_index = record_index; GUID ds_type{}; GUID vendor{}; GUID equip{}; std::string serial; std::string version; std::string name; std::string owner; std::string location; std::string time_zone; if (record->GetInfo(ds_type, vendor, equip, serial, version, name, owner, location, time_zone)) { out.data_source_type_id = make_guid_value(ds_type); out.vendor_id = make_guid_value(vendor); out.equipment_id = make_guid_value(equip); out.serial_number = serial; out.version = version; out.name = name; out.owner = owner; out.location = location; out.time_zone = time_zone; } CPQDIF_E_Collection* main = record->GetMainCollection(); if (main != nullptr) { read_string_tag(main, tagCustomSourceInfo, out.custom_source_info); read_guid_tag(main, tagInstrumentTypeID, out.instrument_type_id); read_string_tag(main, tagInstrumentModelName, out.instrument_model_name); read_string_tag(main, tagInstrumentModelNumber, out.instrument_model_number); read_string_tag(main, tagSerialNumberDS, out.serial_number); read_string_tag(main, tagVersionDS, out.version); read_string_tag(main, tagNameDS, out.name); read_string_tag(main, tagOwnerDS, out.owner); read_string_tag(main, tagLocationDS, out.location); read_string_tag(main, tagTimeZoneDS, out.time_zone); read_string_tag(main, tagCoordinatesDS, out.coordinates); const std::set known = { "tagDataSourceTypeID", "tagVendorID", "tagEquipmentID", "tagCustomSourceInfo", "tagInstrumentTypeID", "tagInstrumentModelName", "tagInstrumentModelNumber", "tagSerialNumberDS", "tagVersionDS", "tagNameDS", "tagOwnerDS", "tagLocationDS", "tagTimeZoneDS", "tagCoordinatesDS", "tagChannelDefns" }; collect_extra_tags(main, known, file_convert, out.extra_tags); } const long channel_count = record->GetCountChannelDefns(); out.channel_definitions.reserve(static_cast(std::max(channel_count, 0))); for (long i = 0; i < channel_count; ++i) { PqdifChannelDefinition channel_def; channel_def.channel_def_index = static_cast(i); std::string channel_name; UINT4 phase_id = 0; GUID quantity_type{}; UINT4 quantity_measured = 0; if (record->GetChannelDefnInfo(i, channel_name, phase_id, quantity_type, quantity_measured)) { channel_def.channel_name = channel_name; channel_def.phase_id = phase_id; channel_def.quantity_type_id = make_guid_value(quantity_type); channel_def.quantity_measured_id = quantity_measured; } long primary_series = -1; if (record->GetChannelPrimarySeries(i, primary_series)) channel_def.primary_series_index = static_cast(primary_series); CPQDIF_E_Collection* channel_coll = record->GetOneChannelDefn(i); if (channel_coll != nullptr) { read_string_tag(channel_coll, tagOtherChannelIdentifier, channel_def.other_channel_identifier); read_string_tag(channel_coll, tagGroupName, channel_def.group_name); read_uint_tag(channel_coll, tagPhysicalChannel, channel_def.physical_channel); read_string_tag(channel_coll, tagQuantityName, channel_def.quantity_name); const std::set known_channel = { "tagChannelName", "tagPhaseID", "tagOtherChannelIdentifier", "tagGroupName", "tagQuantityTypeID", "tagQuantityMeasuredID", "tagPhysicalChannel", "tagQuantityName", "tagPrimarySeriesIdx", "tagSeriesDefns" }; collect_extra_tags(channel_coll, known_channel, file_convert, channel_def.extra_tags); } const long series_count = record->GetCountSeriesDefns(static_cast(i)); channel_def.series_definitions.reserve(static_cast(std::max(series_count, 0))); for (long j = 0; j < series_count; ++j) { PqdifSeriesDefinition series_def; series_def.series_def_index = static_cast(j); UINT4 quantity_units = 0; GUID value_type{}; GUID quantity_characteristic{}; UINT4 storage_method = 0; if (record->GetSeriesDefnInfo(i, j, quantity_units, value_type, quantity_characteristic, storage_method)) { series_def.quantity_units_id = quantity_units; series_def.value_type_id = make_guid_value(value_type); series_def.quantity_characteristic_id = make_guid_value(quantity_characteristic); series_def.storage_method_id = storage_method; } UINT4 precision = 0; double resolution = 0.0; if (record->GetSeriesDefnPrecisionAndResolution(i, j, precision, resolution)) { series_def.significant_digits_id = precision; series_def.quantity_resolution = resolution; } double nominal = 0.0; if (record->GetSeriesDefnNominal(i, j, nominal)) series_def.nominal_quantity = nominal; CPQDIF_E_Collection* series_coll = record->GetOneSeriesDefn(i, j); if (series_coll != nullptr) { read_string_tag(series_coll, tagValueTypeName, series_def.value_type_name); read_uint_tag(series_coll, tagHintGreekPrefixID, series_def.hint_greek_prefix_id); read_uint_tag(series_coll, tagHintPreferredUnitsID, series_def.hint_preferred_units_id); read_uint_tag(series_coll, tagHintDefaultDisplayID, series_def.hint_default_display_id); read_double_tag(series_coll, tagProbInterval, series_def.prob_interval); read_double_tag(series_coll, tagProbPercentile, series_def.prob_percentile); read_double_tag(series_coll, tagSeriesNominalQuantity, series_def.nominal_quantity); read_timestamp_tag(series_coll, tagEffective, file_convert, series_def.effective_time); const std::set known_series = { "tagValueTypeID", "tagQuantityUnitsID", "tagQuantityCharacteristicID", "tagQuantitySignificantDigitsID", "tagQuantityResolutionID", "tagStorageMethodID", "tagValueTypeName", "tagHintGreekPrefixID", "tagHintPreferredUnitsID", "tagHintDefaultDisplayID", "tagProbInterval", "tagProbPercentile", "tagSeriesNominalQuantity", "tagEffective" }; collect_extra_tags(series_coll, known_series, file_convert, series_def.extra_tags); } channel_def.series_definitions.push_back(std::move(series_def)); } out.channel_definitions.push_back(std::move(channel_def)); } return true; } bool parse_monitor_settings_record(CPQDIF& file_convert, CPQDIF_R_Settings* record, long record_index, int settings_index, PqdifMonitorSettingsRecord& out) { if (record == nullptr) return false; out.header = build_record_header_info(record, record_index); out.settings_index = settings_index; out.record_index = record_index; TIMESTAMPPQDIF time_effective{}; TIMESTAMPPQDIF time_installed{}; TIMESTAMPPQDIF time_removed{}; bool use_cal = false; bool use_trans = false; if (record->GetInfo(time_effective, time_installed, time_removed, use_cal, use_trans)) { out.effective_time = make_timestamp_value(file_convert, time_effective); out.time_installed = make_timestamp_value(file_convert, time_installed); out.time_removed = make_timestamp_value(file_convert, time_removed); out.use_calibration = use_cal; out.use_transducer = use_trans; } UINT4 connection_type = 0; if (record->GetConnectionInfo(connection_type)) out.physical_connection = connection_type; REAL8 nominal_frequency = 0.0; if (record->GetNominalFrequency(nominal_frequency)) out.nominal_frequency = nominal_frequency; CPQDIF_E_Collection* main = record->GetMainCollection(); if (main != nullptr) { read_double_tag(main, tagNominalVoltage, out.nominal_voltage); read_bool_tag(main, tagIsPCC, out.is_pcc); const std::set known = { "tagEffective", "tagTimeInstalled", "tagTimeRemoved", "tagUseCalibration", "tagUseTransducer", "tagNominalFrequency", "tagSettingPhysicalConnection", "tagNominalVoltage", "tagIsPCC", "tagChannelSettings" }; collect_extra_tags(main, known, file_convert, out.extra_tags); } const long channel_count = record->GetCountChannels(); out.channel_settings.reserve(static_cast(std::max(channel_count, 0))); for (long i = 0; i < channel_count; ++i) { PqdifChannelSetting channel_setting; channel_setting.channel_setting_index = static_cast(i); UINT4 channel_def_index = 0; if (record->GetChannelInfo(i, channel_def_index)) channel_setting.channel_def_index = static_cast(channel_def_index); CPQDIF_E_Collection* channel_coll = record->GetOneChannelSetting(i); if (channel_coll != nullptr) { read_uint_tag(channel_coll, tagTriggerTypeID, channel_setting.trigger_type_id); read_double_tag(channel_coll, tagFullScale, channel_setting.full_scale); read_double_tag(channel_coll, tagNoiseFloor, channel_setting.noise_floor); read_uint_tag(channel_coll, tagXDTransformerTypeID, channel_setting.xd_transformer_type_id); read_double_tag(channel_coll, tagXDSystemSideRatio, channel_setting.xd_system_side_ratio); read_double_tag(channel_coll, tagXDMonitorSideRatio, channel_setting.xd_monitor_side_ratio); read_double_tag(channel_coll, tagCalTimeSkew, channel_setting.cal_time_skew); read_double_tag(channel_coll, tagCalOffset, channel_setting.cal_offset); read_double_tag(channel_coll, tagCalRatio, channel_setting.cal_ratio); read_bool_tag(channel_coll, tagCalMustUseARCal, channel_setting.cal_must_use_arcal); read_double_tag(channel_coll, tagTriggerHighHigh, channel_setting.trigger_high_high); read_double_tag(channel_coll, tagTriggerHigh, channel_setting.trigger_high); read_double_tag(channel_coll, tagTriggerLow, channel_setting.trigger_low); read_double_tag(channel_coll, tagTriggerLowLow, channel_setting.trigger_low_low); read_double_tag(channel_coll, tagTriggerDeadBand, channel_setting.trigger_deadband); read_double_tag(channel_coll, tagTriggerRate, channel_setting.trigger_rate); CPQDIF_Element* trigger_shape = channel_coll->GetElement(tagTriggerShapeParam, ID_ELEMENT_TYPE_VECTOR); if (trigger_shape != nullptr) channel_setting.trigger_shape_param = extract_vector_values(static_cast(trigger_shape), file_convert); CPQDIF_Element* xd_response = channel_coll->GetElement(tagXDFrequencyResponse, ID_ELEMENT_TYPE_VECTOR); if (xd_response != nullptr) channel_setting.xd_frequency_response = extract_vector_values(static_cast(xd_response), file_convert); CPQDIF_Element* cal_applied = channel_coll->GetElement(tagCalApplied, ID_ELEMENT_TYPE_VECTOR); if (cal_applied != nullptr) channel_setting.cal_applied = extract_vector_values(static_cast(cal_applied), file_convert); CPQDIF_Element* cal_recorded = channel_coll->GetElement(tagCalRecorded, ID_ELEMENT_TYPE_VECTOR); if (cal_recorded != nullptr) channel_setting.cal_recorded = extract_vector_values(static_cast(cal_recorded), file_convert); const std::set known_channel = { "tagChannelDefnIdx", "tagTriggerTypeID", "tagFullScale", "tagNoiseFloor", "tagTriggerShapeParam", "tagXDTransformerTypeID", "tagXDSystemSideRatio", "tagXDMonitorSideRatio", "tagXDFrequencyResponse", "tagCalTimeSkew", "tagCalOffset", "tagCalRatio", "tagCalMustUseARCal", "tagCalApplied", "tagCalRecorded", "tagTriggerHighHigh", "tagTriggerHigh", "tagTriggerLow", "tagTriggerLowLow", "tagTriggerDeadBand", "tagTriggerRate" }; collect_extra_tags(channel_coll, known_channel, file_convert, channel_setting.extra_tags); } out.channel_settings.push_back(std::move(channel_setting)); } return true; } bool parse_observation_record(CPQDIF& file_convert, CPQDIF_R_Observation* record, long record_index, int observation_index, int related_data_source_index, long related_data_source_record_index, int related_settings_index, long related_settings_record_index, PqdifObservationRecord& out) { if (record == nullptr) return false; out.header = build_record_header_info(record, record_index); out.observation_index = observation_index; out.record_index = record_index; out.related_data_source_index = related_data_source_index; out.related_data_source_record_index = related_data_source_record_index; out.related_settings_index = related_settings_index; out.related_settings_record_index = related_settings_record_index; TIMESTAMPPQDIF time_start{}; TIMESTAMPPQDIF time_create{}; std::string observation_name; if (record->GetInfo(time_start, time_create, observation_name)) { out.observation_name = observation_name; out.time_start = make_timestamp_value(file_convert, time_start); out.time_create = make_timestamp_value(file_convert, time_create); } UINT4 trigger_method = 0; CPQDIF_E_Vector* trigger_channels = nullptr; TIMESTAMPPQDIF time_triggered{}; if (record->GetTriggerInfo(trigger_method, &trigger_channels, time_triggered)) { out.trigger_method_id = trigger_method; out.time_triggered = make_timestamp_value(file_convert, time_triggered); if (trigger_channels != nullptr) { PqdifValueArray trigger_indexes = extract_vector_values(trigger_channels, file_convert); for (size_t i = 0; i < trigger_indexes.int_values.size(); ++i) out.channel_trigger_indexes.push_back(static_cast(trigger_indexes.int_values[i])); for (size_t i = 0; i < trigger_indexes.uint_values.size(); ++i) out.channel_trigger_indexes.push_back(static_cast(trigger_indexes.uint_values[i])); } } CPQDIF_E_Collection* main = record->GetMainCollection(); if (main != nullptr) { read_uint_tag(main, tagObservationSerial, out.observation_serial); read_uint_tag(main, tagObservationAggregationSerial, out.observation_aggregation_serial); read_guid_tag(main, tagDisturbanceCategoryID, out.disturbance_category_id); const std::set known = { "tagObservationName", "tagTimeCreate", "tagTimeStart", "tagTriggerMethodID", "tagTimeTriggered", "tagChannelTriggerIdx", "tagObservationSerial", "tagObservationAggregationSerial", "tagDisturbanceCategoryID", "tagChannelInstances" }; collect_extra_tags(main, known, file_convert, out.extra_tags); } const long channel_count = record->GetCountChannels(); out.channel_instances.reserve(static_cast(std::max(channel_count, 0))); for (long i = 0; i < channel_count; ++i) { PqdifChannelInstance channel_instance; channel_instance.channel_instance_index = static_cast(i); long channel_def_index = -1; if (record->GetChannelDefnIdx(i, channel_def_index)) channel_instance.channel_def_index = static_cast(channel_def_index); std::string channel_name; UINT4 phase_id = 0; GUID quantity_type{}; UINT4 quantity_measured = 0; if (record->GetChannelInfo(i, channel_name, phase_id, quantity_type, quantity_measured)) { channel_instance.channel_name = channel_name; channel_instance.phase_id = phase_id; channel_instance.quantity_type_id = make_guid_value(quantity_type); channel_instance.quantity_measured_id = quantity_measured; } long primary_series = -1; if (record->GetChannelPrimarySeries(i, primary_series)) channel_instance.primary_series_index = static_cast(primary_series); CPQDIF_E_Collection* channel_coll = record->GetOneChannel(i); if (channel_coll != nullptr) { read_double_tag(channel_coll, tagCharactDuration, channel_instance.charact_duration); read_double_tag(channel_coll, tagCharactMagnitude, channel_instance.charact_magnitude); read_double_tag(channel_coll, tagCharactFrequency, channel_instance.charact_frequency); read_double_tag(channel_coll, tagChannelFrequency, channel_instance.channel_frequency); read_int_tag(channel_coll, tagChannelGroupID, channel_instance.channel_group_id); const std::set known_channel = { "tagChannelDefnIdx", "tagCharactDuration", "tagCharactMagnitude", "tagCharactFrequency", "tagChannelFrequency", "tagChannelGroupID", "tagSeriesInstances" }; collect_extra_tags(channel_coll, known_channel, file_convert, channel_instance.extra_tags); } const long series_count = record->GetCountSeries(static_cast(i)); channel_instance.series_instances.reserve(static_cast(std::max(series_count, 0))); for (long j = 0; j < series_count; ++j) { PqdifSeriesInstance series_instance; series_instance.series_instance_index = static_cast(j); series_instance.series_def_index = static_cast(j); UINT4 quantity_units = 0; GUID quantity_characteristic{}; GUID value_type{}; if (record->GetSeriesInfo(i, j, quantity_units, quantity_characteristic, value_type)) { series_instance.quantity_units_id = quantity_units; series_instance.quantity_characteristic_id = make_guid_value(quantity_characteristic); series_instance.value_type_id = make_guid_value(value_type); } long base_type = -1; if (record->GetSeriesBaseType(i, j, base_type)) series_instance.series_base_type = base_type; record->GetSeriesBaseQuantity(i, j, series_instance.series_base_quantity); record->GetSeriesScale(i, j, series_instance.scale, series_instance.offset); record->GetSeriesDefnNominal(i, j, series_instance.nominal_quantity); UINT4 precision = 0; double resolution = 0.0; if (record->GetSeriesDefnPrecisionAndResolution(i, j, precision, resolution)) { series_instance.significant_digits_id = precision; series_instance.quantity_resolution = resolution; } CPQDIF_E_Collection* series_coll = record->GetOneSeries(i, j); if (series_coll != nullptr) { read_int_tag(series_coll, tagSeriesShareChannelIdx, series_instance.share_channel_index); read_int_tag(series_coll, tagSeriesShareSeriesIdx, series_instance.share_series_index); const std::set known_series = { "tagSeriesBaseQuantity", "tagSeriesScale", "tagSeriesOffset", "tagSeriesShareChannelIdx", "tagSeriesShareSeriesIdx", "tagSeriesValues" }; collect_extra_tags(series_coll, known_series, file_convert, series_instance.extra_tags); } CPQDIF_E_Vector* series_values = record->GetSeriesValueVector(i, j); if (series_values != nullptr) series_instance.values = extract_vector_values(series_values, file_convert); channel_instance.series_instances.push_back(std::move(series_instance)); } out.channel_instances.push_back(std::move(channel_instance)); } return true; } bool parse_pqdif_file_full(const std::string& file_path, PqdifLogicalFile& out_file, std::string& err) { out_file = PqdifLogicalFile{}; CPQDIF file_convert; file_convert.put_FlatFileName(file_path); std::string basic_err; if (!dump_file_basic_info(file_path, basic_err)) { err = "precheck failed: " + basic_err; return false; } if (!file_convert.Read()) { err = "CPQDIF::Read() failed"; return false; } const int record_count = static_cast(file_convert.RecordGetCount()); int current_data_source_index = -1; long current_data_source_record_index = -1; int current_settings_index = -1; long current_settings_record_index = -1; for (int i_record = 0; i_record < record_count; ++i_record) { GUID record_guid{}; std::string record_name; if (!file_convert.RecordGetInfo(i_record, &record_guid, record_name)) continue; long raw_record_handle = 0; if (file_convert.RecordRequestRecord(i_record, &raw_record_handle)) { CPQDIFRecord* raw_record = reinterpret_cast(raw_record_handle); out_file.record_headers.push_back(build_record_header_info(raw_record, i_record)); } if (PQDIF_IsEqualGUID(record_guid, tagContainer)) { CPQDIF_R_General* record = reinterpret_cast(raw_record_handle); PqdifContainerRecord container; if (parse_container_record(file_convert, record, i_record, container)) out_file.containers.push_back(std::move(container)); continue; } if (PQDIF_IsEqualGUID(record_guid, tagRecDataSource)) { CPQDIF_R_DataSource* record = reinterpret_cast(raw_record_handle); PqdifDataSourceRecord data_source; const int data_source_index = static_cast(out_file.data_sources.size()); if (parse_data_source_record(file_convert, record, i_record, data_source_index, data_source)) { out_file.data_sources.push_back(std::move(data_source)); current_data_source_index = data_source_index; current_data_source_record_index = i_record; current_settings_index = -1; current_settings_record_index = -1; } continue; } if (PQDIF_IsEqualGUID(record_guid, tagRecMonitorSettings)) { CPQDIF_R_Settings* record = reinterpret_cast(raw_record_handle); PqdifMonitorSettingsRecord settings; const int settings_index = static_cast(out_file.monitor_settings.size()); if (parse_monitor_settings_record(file_convert, record, i_record, settings_index, settings)) { out_file.monitor_settings.push_back(std::move(settings)); current_settings_index = settings_index; current_settings_record_index = i_record; } continue; } if (PQDIF_IsEqualGUID(record_guid, tagRecObservation)) { long observation_handle = 0; if (!file_convert.RecordRequestObservation(i_record, &observation_handle)) continue; CPQDIF_R_Observation* observation = reinterpret_cast(observation_handle); PqdifObservationRecord record; const int observation_index = static_cast(out_file.observations.size()); parse_observation_record( file_convert, observation, i_record, observation_index, current_data_source_index, current_data_source_record_index, current_settings_index, current_settings_record_index, record); out_file.observations.push_back(std::move(record)); file_convert.RecordReleaseObservation(observation_handle); } } file_convert.Close(); return true; } constexpr float kPqdifBase64MissingFloat = 3.14159f; int stat_value_kind_code_for_base64(StatValueKind kind) { switch (kind) { case StatValueKind::Max: return 1; case StatValueKind::Min: return 2; case StatValueKind::Avg: return 3; case StatValueKind::P95: return 4; default: return 0; } } std::string pqdif_base64_encode_bytes(const unsigned char* bytes_to_encode, size_t in_len) { static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; std::string ret; ret.reserve(((in_len + 2) / 3) * 4); int i = 0; unsigned char char_array_3[3] = { 0, 0, 0 }; unsigned char char_array_4[4] = { 0, 0, 0, 0 }; while (in_len--) { char_array_3[i++] = *(bytes_to_encode++); if (i == 3) { char_array_4[0] = static_cast((char_array_3[0] & 0xfc) >> 2); char_array_4[1] = static_cast(((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4)); char_array_4[2] = static_cast(((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6)); char_array_4[3] = static_cast(char_array_3[2] & 0x3f); for (i = 0; i < 4; ++i) ret += base64_chars[char_array_4[i]]; i = 0; } } if (i != 0) { int j = 0; for (j = i; j < 3; ++j) char_array_3[j] = '\0'; char_array_4[0] = static_cast((char_array_3[0] & 0xfc) >> 2); char_array_4[1] = static_cast(((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4)); char_array_4[2] = static_cast(((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6)); char_array_4[3] = static_cast(char_array_3[2] & 0x3f); for (j = 0; j < i + 1; ++j) ret += base64_chars[char_array_4[j]]; while (i++ < 3) ret += '='; } return ret; } std::string pqdif_base64_encode_float_vector(const std::vector& values) { if (values.empty()) return std::string(); const unsigned char* byte_data = reinterpret_cast(values.data()); const size_t byte_size = values.size() * sizeof(float); return pqdif_base64_encode_bytes(byte_data, byte_size); } size_t pqdif_stat_base64_count_records_in_batch(const PqdifStatBase64FileBatch& batch) { size_t n = 0; for (const auto& tp : batch.time_points) n += tp.records.size(); return n; } // 统计任意 Base64 文件级批次队列内部包含的子记录总数。 // 对象用途:既可以统计“生成队列”,也可以统计“待后续处理队列”。 // 调用约定:调用方必须已经持有对应队列的互斥锁。 size_t pqdif_stat_base64_count_records_in_queue_unlocked(const std::deque& queue) { size_t n = 0; for (const auto& batch : queue) n += pqdif_stat_base64_count_records_in_batch(batch); return n; } // 统计“生成队列”内部的子记录总数。 // 对象用途:保留旧函数名,供已有 GetPqdifStatBase64RecordCountInQueue() 复用。 // 调用约定:调用方必须已经持有 g_pqdif_stat_base64_mutex。 size_t pqdif_stat_base64_count_records_in_queue_unlocked() { return pqdif_stat_base64_count_records_in_queue_unlocked(g_pqdif_stat_base64_queue); } void pqdif_dump_stat_base64_file_batch_full(const PqdifStatBase64FileBatch& batch) { std::cout << "========== PQDIF BASE64 SAVED OBJECT FULL DUMP ==========" << std::endl; std::cout << "[FILE BATCH] file=" << batch.pqdif_file_path << ", source_file=" << batch.source_file << ", mac=" << batch.mac << ", parsed_at=" << batch.parsed_at_text << ", connection_kind=" << stat_connection_kind_name(batch.connection_kind) << ", time_point_count=" << batch.time_point_count << ", total_record_count=" << batch.total_record_count << ", total_float_count=" << batch.total_float_count << ", total_placeholder_count=" << batch.total_placeholder_count << ", total_base64_chars=" << batch.total_base64_chars << std::endl; for (size_t i = 0; i < batch.time_points.size(); ++i) { const PqdifStatBase64TimePointPacket& tp = batch.time_points[i]; std::cout << " [TIME POINT " << i << "] time=" << tp.timestamp_text << ", timestamp=" << static_cast(tp.timestamp) << ", record_count=" << tp.record_count << ", total_float_count=" << tp.total_float_count << ", total_placeholder_count=" << tp.total_placeholder_count << ", total_base64_chars=" << tp.total_base64_chars << std::endl; for (size_t j = 0; j < tp.records.size(); ++j) { const PqdifStatBase64Record& rec = tp.records[j]; std::cout << " [BASE64 SUB RECORD " << j << "] file=" << rec.pqdif_file_path << ", time=" << rec.timestamp_text << ", timestamp=" << static_cast(rec.timestamp) << ", kind=" << rec.value_kind_name << ", kind_code=" << rec.value_kind_code << ", connection_kind=" << stat_connection_kind_name(rec.connection_kind) << ", float_count=" << rec.float_count << ", placeholder_count=" << rec.placeholder_count << ", base64_len=" << rec.base64_payload.size() << std::endl; std::cout << " base64_payload=" << rec.base64_payload << std::endl; } } std::cout << "==========================================================" << std::endl; } bool push_pqdif_stat_base64_file_batch(PqdifStatBase64FileBatch&& batch) { // 对象用途:把“一个 PQDIF 文件解析完成后的 Base64 文件级批次”放入生成队列。 // 这里不直接做入库/上传,避免解析线程被后续业务阻塞。 std::lock_guard guard(g_pqdif_stat_base64_mutex); if (g_pqdif_stat_base64_queue.size() >= kPqdifStatBase64QueueLimit) { std::cout << "[PQDIF BASE64] file-batch queue full, drop oldest file batch: file=" << g_pqdif_stat_base64_queue.front().pqdif_file_path << ", time_points=" << g_pqdif_stat_base64_queue.front().time_point_count << ", records=" << g_pqdif_stat_base64_queue.front().total_record_count << std::endl; g_pqdif_stat_base64_queue.pop_front(); } g_pqdif_stat_base64_queue.emplace_back(std::move(batch)); return true; } bool pqdif_move_one_generated_base64_batch_to_ready_queue() { // 对象用途:RunPqdifScanLoop() 每轮循环末尾调用。 // 从“生成队列”取出最多一个 PQDIF 文件级批次,移动到“待后续处理队列”。 // 注意:这里只做快速移动和核心摘要打印,不在锁内执行耗时业务逻辑。 PqdifStatBase64FileBatch batch; { std::lock_guard guard(g_pqdif_stat_base64_mutex); if (g_pqdif_stat_base64_queue.empty()) return false; batch = std::move(g_pqdif_stat_base64_queue.front()); g_pqdif_stat_base64_queue.pop_front(); } const std::string moved_file = batch.pqdif_file_path; const size_t moved_time_points = batch.time_point_count; const size_t moved_records = batch.total_record_count; const size_t moved_float_count = batch.total_float_count; const size_t moved_placeholder_count = batch.total_placeholder_count; { std::lock_guard guard(g_pqdif_stat_base64_ready_mutex); if (g_pqdif_stat_base64_ready_queue.size() >= kPqdifStatBase64QueueLimit) { std::cout << "[PQDIF BASE64 READY] ready queue full, drop oldest file batch: file=" << g_pqdif_stat_base64_ready_queue.front().pqdif_file_path << ", time_points=" << g_pqdif_stat_base64_ready_queue.front().time_point_count << ", records=" << g_pqdif_stat_base64_ready_queue.front().total_record_count << std::endl; g_pqdif_stat_base64_ready_queue.pop_front(); } g_pqdif_stat_base64_ready_queue.emplace_back(std::move(batch)); } std::cout << "[PQDIF BASE64 READY] moved one file batch for next step" << ", file=" << moved_file << ", time_points=" << moved_time_points << ", records=" << moved_records << ", float_count=" << moved_float_count << ", placeholders=" << moved_placeholder_count << std::endl; return true; } std::string pqdif_absolute_path_text(const std::string& path) { try { return fs::absolute(fs::path(path)).string(); } catch (...) { return path; } } bool bucket_has_metric_value_for_kind(const AggregatedStatValues& values, StatValueKind kind, float& out) { switch (kind) { case StatValueKind::Min: if (!values.has_min) return false; out = static_cast(values.min_value); return true; case StatValueKind::Max: if (!values.has_max) return false; out = static_cast(values.max_value); return true; case StatValueKind::Avg: if (!values.has_avg) return false; out = static_cast(values.avg_value); return true; case StatValueKind::P95: if (!values.has_p95) return false; out = static_cast(values.p95_value); return true; default: return false; } } struct PqdifBase64BuildContext { PqdifBase64BuildContext( const ParsedPqdifFile& file_ref, const TimeAggregatedStatBucket& bucket_ref, StatValueKind value_kind, std::vector& value_buffer) : parsed_file(file_ref), bucket(bucket_ref), kind(value_kind), values(value_buffer), placeholder_count(0), missing_metric_count(0) { } const ParsedPqdifFile& parsed_file; const TimeAggregatedStatBucket& bucket; StatValueKind kind; std::vector& values; size_t placeholder_count; size_t missing_metric_count; size_t delta_line_phase_fallback_count = 0; std::vector missing_metric_names; }; bool pqdif_base64_try_get_metric_value( const TimeAggregatedStatBucket& bucket, StatMetricId metric_id, StatValueKind kind, float& out_value) { auto it = bucket.metrics.find(metric_id); if (it == bucket.metrics.end()) return false; return bucket_has_metric_value_for_kind(it->second, kind, out_value); } StatMetricId pqdif_base64_delta_line_to_phase_metric_id(StatMetricId metric_id) { switch (metric_id) { case StatMetricId::UabRms: return StatMetricId::UaRms; case StatMetricId::UbcRms: return StatMetricId::UbRms; case StatMetricId::UcaRms: return StatMetricId::UcRms; case StatMetricId::UabDeviation: return StatMetricId::UaDeviation; case StatMetricId::UbcDeviation: return StatMetricId::UbDeviation; case StatMetricId::UcaDeviation: return StatMetricId::UcDeviation; case StatMetricId::UabThd: return StatMetricId::UaThd; case StatMetricId::UbcThd: return StatMetricId::UbThd; case StatMetricId::UcaThd: return StatMetricId::UcThd; case StatMetricId::UabDvc: return StatMetricId::UaDvc; case StatMetricId::UbcDvc: return StatMetricId::UbDvc; case StatMetricId::UcaDvc: return StatMetricId::UcDvc; case StatMetricId::UabPst: return StatMetricId::UaPst; case StatMetricId::UbcPst: return StatMetricId::UbPst; case StatMetricId::UcaPst: return StatMetricId::UcPst; case StatMetricId::UabPlt: return StatMetricId::UaPlt; case StatMetricId::UbcPlt: return StatMetricId::UbPlt; case StatMetricId::UcaPlt: return StatMetricId::UcPlt; case StatMetricId::UabFundRms: return StatMetricId::UaFundRms; case StatMetricId::UbcFundRms: return StatMetricId::UbFundRms; case StatMetricId::UcaFundRms: return StatMetricId::UcFundRms; case StatMetricId::UabFundAngle: return StatMetricId::UaFundAngle; case StatMetricId::UbcFundAngle: return StatMetricId::UbFundAngle; case StatMetricId::UcaFundAngle: return StatMetricId::UcFundAngle; default: break; } const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(metric_id); if (r == nullptr) return StatMetricId::Unknown; const int offset = stat_dynamic_metric_order_or_slot(metric_id); if (r->group == StatDynamicMetricGroup::LineVoltageHarmonic) return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonic, r->phase_index, offset); if (r->group == StatDynamicMetricGroup::LineVoltageHarmonicAngle) return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonicAngle, r->phase_index, offset); if (r->group == StatDynamicMetricGroup::LineVoltageInterharmonic) return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageInterharmonic, r->phase_index, offset); if (r->group == StatDynamicMetricGroup::LineVoltageHarmonicRatio) return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonicRatio, r->phase_index, offset); return StatMetricId::Unknown; } void pqdif_base64_push_metric(PqdifBase64BuildContext& ctx, StatMetricId metric_id) { float value = kPqdifBase64MissingFloat; bool ok = false; ok = pqdif_base64_try_get_metric_value(ctx.bucket, metric_id, ctx.kind, value); if (!ok) { value = kPqdifBase64MissingFloat; ++ctx.placeholder_count; ++ctx.missing_metric_count; const std::string name = stat_metric_name(metric_id); ctx.missing_metric_names.push_back(name); if (pqdif_log_enabled(PqdifLogLevel::Debug)) { std::cout << " [BASE64 MISSING METRIC] time=" << ctx.bucket.timestamp_text << ", kind=" << stat_value_kind_name(ctx.kind) << ", metric=" << name << ", placeholder=" << kPqdifBase64MissingFloat << std::endl; } } ctx.values.push_back(value); } void pqdif_base64_push_delta_line_metric_with_phase_fallback(PqdifBase64BuildContext& ctx, StatMetricId line_metric_id) { float value = kPqdifBase64MissingFloat; if (pqdif_base64_try_get_metric_value(ctx.bucket, line_metric_id, ctx.kind, value)) { ctx.values.push_back(value); return; } const StatMetricId phase_metric_id = pqdif_base64_delta_line_to_phase_metric_id(line_metric_id); if (phase_metric_id != StatMetricId::Unknown && pqdif_base64_try_get_metric_value(ctx.bucket, phase_metric_id, ctx.kind, value)) { ctx.values.push_back(value); ++ctx.delta_line_phase_fallback_count; if (pqdif_log_enabled(PqdifLogLevel::Info)) { std::cout << " [BASE64 DELTA LINE FALLBACK] time=" << ctx.bucket.timestamp_text << ", kind=" << stat_value_kind_name(ctx.kind) << ", line_metric=" << stat_metric_name(line_metric_id) << ", fallback_phase_metric=" << stat_metric_name(phase_metric_id) << ", value=" << value << std::endl; } return; } ctx.values.push_back(kPqdifBase64MissingFloat); ++ctx.placeholder_count; ++ctx.missing_metric_count; const std::string line_name = stat_metric_name(line_metric_id); ctx.missing_metric_names.push_back(line_name); if (pqdif_log_enabled(PqdifLogLevel::Debug)) { std::cout << " [BASE64 MISSING METRIC] time=" << ctx.bucket.timestamp_text << ", kind=" << stat_value_kind_name(ctx.kind) << ", metric=" << line_name << ", placeholder=" << kPqdifBase64MissingFloat << ", reason=delta_line_metric_and_phase_fallback_missing"; if (phase_metric_id != StatMetricId::Unknown) std::cout << ", fallback_phase_metric=" << stat_metric_name(phase_metric_id); std::cout << std::endl; } } void pqdif_base64_push_placeholder(PqdifBase64BuildContext& ctx, const std::string& label) { ctx.values.push_back(kPqdifBase64MissingFloat); ++ctx.placeholder_count; if (pqdif_log_enabled(PqdifLogLevel::Trace)) { std::cout << " [BASE64 PLACEHOLDER] time=" << ctx.bucket.timestamp_text << ", kind=" << stat_value_kind_name(ctx.kind) << ", label=" << label << ", value=" << kPqdifBase64MissingFloat << std::endl; } } void pqdif_base64_push_placeholder_block(PqdifBase64BuildContext& ctx, const std::string& label, size_t count) { for (size_t i = 0; i < count; ++i) { ctx.values.push_back(kPqdifBase64MissingFloat); ++ctx.placeholder_count; } if (pqdif_log_enabled(PqdifLogLevel::Info)) { std::cout << " [BASE64 PLACEHOLDER BLOCK] time=" << ctx.bucket.timestamp_text << ", kind=" << stat_value_kind_name(ctx.kind) << ", label=" << label << ", count=" << count << ", value=" << kPqdifBase64MissingFloat << std::endl; } } void pqdif_base64_push_dynamic_order_range( PqdifBase64BuildContext& ctx, StatDynamicMetricGroup group, int phase_index, int first_offset, int last_offset) { for (int offset = first_offset; offset <= last_offset; ++offset) { pqdif_base64_push_metric(ctx, stat_dynamic_metric_id(group, phase_index, offset)); } } void pqdif_base64_push_dynamic_three_phases( PqdifBase64BuildContext& ctx, StatDynamicMetricGroup group, int first_offset, int last_offset) { for (int phase = 0; phase < 3; ++phase) pqdif_base64_push_dynamic_order_range(ctx, group, phase, first_offset, last_offset); } void pqdif_base64_push_dynamic_order_range_delta_line_fallback( PqdifBase64BuildContext& ctx, StatDynamicMetricGroup line_group, int phase_index, int first_offset, int last_offset) { for (int offset = first_offset; offset <= last_offset; ++offset) { pqdif_base64_push_delta_line_metric_with_phase_fallback( ctx, stat_dynamic_metric_id(line_group, phase_index, offset)); } } void pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback( PqdifBase64BuildContext& ctx, StatDynamicMetricGroup line_group, int first_offset, int last_offset) { for (int phase = 0; phase < 3; ++phase) { pqdif_base64_push_dynamic_order_range_delta_line_fallback( ctx, line_group, phase, first_offset, last_offset); } } void pqdif_base64_push_harmonic_power_phase( PqdifBase64BuildContext& ctx, int phase_index) { pqdif_base64_push_dynamic_order_range(ctx, StatDynamicMetricGroup::HarmonicActivePower, phase_index, 2, 50); pqdif_base64_push_dynamic_order_range(ctx, StatDynamicMetricGroup::HarmonicReactivePower, phase_index, 2, 50); pqdif_base64_push_dynamic_order_range(ctx, StatDynamicMetricGroup::HarmonicApparentPower, phase_index, 2, 50); } std::vector pqdif_build_star_float_buffer_for_bucket( const ParsedPqdifFile& parsed_file, const TimeAggregatedStatBucket& bucket, StatValueKind kind, size_t& placeholder_count, size_t& missing_metric_count, std::vector& missing_metric_names) { std::vector float_buffer; float_buffer.reserve(2463); PqdifBase64BuildContext ctx{ parsed_file, bucket, kind, float_buffer }; // 1) 三相电压有效值、三相电流有效值、三线电压有效值 pqdif_base64_push_metric(ctx, StatMetricId::UaRms); pqdif_base64_push_metric(ctx, StatMetricId::UbRms); pqdif_base64_push_metric(ctx, StatMetricId::UcRms); pqdif_base64_push_metric(ctx, StatMetricId::IaRms); pqdif_base64_push_metric(ctx, StatMetricId::IbRms); pqdif_base64_push_metric(ctx, StatMetricId::IcRms); pqdif_base64_push_metric(ctx, StatMetricId::UabRms); pqdif_base64_push_metric(ctx, StatMetricId::UbcRms); pqdif_base64_push_metric(ctx, StatMetricId::UcaRms); // 2) 三相电压偏差 + 三线电压偏差占位 pqdif_base64_push_metric(ctx, StatMetricId::UaDeviation); pqdif_base64_push_metric(ctx, StatMetricId::UbDeviation); pqdif_base64_push_metric(ctx, StatMetricId::UcDeviation); pqdif_base64_push_placeholder_block(ctx, "line voltage deviation placeholder(Uab/Ubc/Uca)", 3); // 3) 频率偏差 + 频率 pqdif_base64_push_metric(ctx, StatMetricId::FrequencyDeviation); pqdif_base64_push_metric(ctx, StatMetricId::Frequency); // 4) 电压零/负/正序和负序不平衡 pqdif_base64_push_metric(ctx, StatMetricId::UZeroSeq); pqdif_base64_push_metric(ctx, StatMetricId::UNegSeq); pqdif_base64_push_metric(ctx, StatMetricId::UPosSeq); pqdif_base64_push_metric(ctx, StatMetricId::UNegSeqUnbalance); // 5) 电流零/负/正序和负序不平衡 pqdif_base64_push_metric(ctx, StatMetricId::IZeroSeq); pqdif_base64_push_metric(ctx, StatMetricId::INegSeq); pqdif_base64_push_metric(ctx, StatMetricId::IPosSeq); pqdif_base64_push_metric(ctx, StatMetricId::INegSeqUnbalance); // 6) 三相电压谐波 2-50 次指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageHarmonic, 2, 50); // 7) 三相电流谐波 2-50 次指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonic, 2, 50); // 8) 三线电压谐波占位 pqdif_base64_push_placeholder_block(ctx, "line voltage harmonic placeholder(Uab/Ubc/Uca,2-50)", 49 * 3); // 9) 三相电压谐波 2-50 次相角指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageHarmonicAngle, 2, 50); // 10) 三相电流谐波 2-50 次相角指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicAngle, 2, 50); // 11) 三线电压谐波相角占位 pqdif_base64_push_placeholder_block(ctx, "line voltage harmonic angle placeholder(Uab/Ubc/Uca,2-50)", 49 * 3); // 12) 三相电压间谐波 0.5-49.5 次指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageInterharmonic, 0, 49); // 13) 三相电流间谐波 0.5-49.5 次指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentInterharmonic, 0, 49); // 14) 三线电压间谐波占位 pqdif_base64_push_placeholder_block(ctx, "line voltage interharmonic placeholder(Uab/Ubc/Uca,0.5-49.5)", 50 * 3); // 15) A/B/C/总 有功/无功/视在功率 pqdif_base64_push_metric(ctx, StatMetricId::PaPower); pqdif_base64_push_metric(ctx, StatMetricId::QaPower); pqdif_base64_push_metric(ctx, StatMetricId::SaPower); pqdif_base64_push_metric(ctx, StatMetricId::PbPower); pqdif_base64_push_metric(ctx, StatMetricId::QbPower); pqdif_base64_push_metric(ctx, StatMetricId::SbPower); pqdif_base64_push_metric(ctx, StatMetricId::PcPower); pqdif_base64_push_metric(ctx, StatMetricId::QcPower); pqdif_base64_push_metric(ctx, StatMetricId::ScPower); pqdif_base64_push_metric(ctx, StatMetricId::PTotalPower); pqdif_base64_push_metric(ctx, StatMetricId::QTotalPower); pqdif_base64_push_metric(ctx, StatMetricId::STotalPower); // 16) A/B/C/总 谐波 2-50 次 有功/无功/视在功率 pqdif_base64_push_harmonic_power_phase(ctx, 0); pqdif_base64_push_harmonic_power_phase(ctx, 1); pqdif_base64_push_harmonic_power_phase(ctx, 2); pqdif_base64_push_harmonic_power_phase(ctx, 3); // 17) 谐波含有率:三相电压、三相电流、三线电压占位 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageHarmonicRatio, 2, 50); pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicRatio, 2, 50); pqdif_base64_push_placeholder_block(ctx, "line voltage harmonic ratio placeholder(Uab/Ubc/Uca,2-50)", 49 * 3); // 18) 谐波总畸变率:三相电压、三相电流、三线电压占位 pqdif_base64_push_metric(ctx, StatMetricId::UaThd); pqdif_base64_push_metric(ctx, StatMetricId::UbThd); pqdif_base64_push_metric(ctx, StatMetricId::UcThd); pqdif_base64_push_metric(ctx, StatMetricId::IaThd); pqdif_base64_push_metric(ctx, StatMetricId::IbThd); pqdif_base64_push_metric(ctx, StatMetricId::IcThd); pqdif_base64_push_placeholder_block(ctx, "line voltage THD placeholder(Uab/Ubc/Uca)", 3); // 19) 三相功率因数和总功率因数 pqdif_base64_push_metric(ctx, StatMetricId::PFa); pqdif_base64_push_metric(ctx, StatMetricId::PFb); pqdif_base64_push_metric(ctx, StatMetricId::PFc); pqdif_base64_push_metric(ctx, StatMetricId::PFTotal); // 20) 三相基波功率因数和总基波功率因数 pqdif_base64_push_metric(ctx, StatMetricId::FundPFa); pqdif_base64_push_metric(ctx, StatMetricId::FundPFb); pqdif_base64_push_metric(ctx, StatMetricId::FundPFc); pqdif_base64_push_metric(ctx, StatMetricId::FundPFTotal); // 21) 三相电压变动幅值 + 三线占位 pqdif_base64_push_metric(ctx, StatMetricId::UaDvc); pqdif_base64_push_metric(ctx, StatMetricId::UbDvc); pqdif_base64_push_metric(ctx, StatMetricId::UcDvc); pqdif_base64_push_placeholder_block(ctx, "line voltage DVC placeholder(Uab/Ubc/Uca)", 3); // 22) 三相短闪 + 三线占位 pqdif_base64_push_metric(ctx, StatMetricId::UaPst); pqdif_base64_push_metric(ctx, StatMetricId::UbPst); pqdif_base64_push_metric(ctx, StatMetricId::UcPst); pqdif_base64_push_placeholder_block(ctx, "line voltage Pst placeholder(Uab/Ubc/Uca)", 3); // 23) 三相长闪 + 三线占位 pqdif_base64_push_metric(ctx, StatMetricId::UaPlt); pqdif_base64_push_metric(ctx, StatMetricId::UbPlt); pqdif_base64_push_metric(ctx, StatMetricId::UcPlt); pqdif_base64_push_placeholder_block(ctx, "line voltage Plt placeholder(Uab/Ubc/Uca)", 3); // 24) A/B/C/总 基波有功/无功/视在功率 pqdif_base64_push_metric(ctx, StatMetricId::PaFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QaFundPower); pqdif_base64_push_metric(ctx, StatMetricId::SaFundPower); pqdif_base64_push_metric(ctx, StatMetricId::PbFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QbFundPower); pqdif_base64_push_metric(ctx, StatMetricId::SbFundPower); pqdif_base64_push_metric(ctx, StatMetricId::PcFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QcFundPower); pqdif_base64_push_metric(ctx, StatMetricId::ScFundPower); pqdif_base64_push_metric(ctx, StatMetricId::PTotalFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QTotalFundPower); pqdif_base64_push_metric(ctx, StatMetricId::STotalFundPower); // 25) 基波有效值:三相电压、三相电流、三线电压占位 pqdif_base64_push_metric(ctx, StatMetricId::UaFundRms); pqdif_base64_push_metric(ctx, StatMetricId::UbFundRms); pqdif_base64_push_metric(ctx, StatMetricId::UcFundRms); pqdif_base64_push_metric(ctx, StatMetricId::IaFundRms); pqdif_base64_push_metric(ctx, StatMetricId::IbFundRms); pqdif_base64_push_metric(ctx, StatMetricId::IcFundRms); pqdif_base64_push_placeholder_block(ctx, "line voltage fundamental RMS placeholder(Uab/Ubc/Uca)", 3); // 26) 基波相角:三相电压、三相电流、三线电压相角占位 pqdif_base64_push_metric(ctx, StatMetricId::UaFundAngle); pqdif_base64_push_metric(ctx, StatMetricId::UbFundAngle); pqdif_base64_push_metric(ctx, StatMetricId::UcFundAngle); pqdif_base64_push_metric(ctx, StatMetricId::IaFundAngle); pqdif_base64_push_metric(ctx, StatMetricId::IbFundAngle); pqdif_base64_push_metric(ctx, StatMetricId::IcFundAngle); pqdif_base64_push_placeholder_block(ctx, "line voltage fundamental angle placeholder(Uab/Ubc/Uca)", 3); placeholder_count = ctx.placeholder_count; missing_metric_count = ctx.missing_metric_count; missing_metric_names = ctx.missing_metric_names; return float_buffer; } std::vector pqdif_build_delta_float_buffer_for_bucket( const ParsedPqdifFile& parsed_file, const TimeAggregatedStatBucket& bucket, StatValueKind kind, size_t& placeholder_count, size_t& missing_metric_count, std::vector& missing_metric_names) { std::vector float_buffer; // Delta 角型与 Wye 星型必须保持完全相同的 float 数量,便于后续统一解码。 float_buffer.reserve(2463); PqdifBase64BuildContext ctx(parsed_file, bucket, kind, float_buffer); // 1) 三相电压有效值、三相电流有效值、三线电压有效值 pqdif_base64_push_metric(ctx, StatMetricId::UaRms); pqdif_base64_push_metric(ctx, StatMetricId::UbRms); pqdif_base64_push_metric(ctx, StatMetricId::UcRms); pqdif_base64_push_metric(ctx, StatMetricId::IaRms); pqdif_base64_push_metric(ctx, StatMetricId::IbRms); pqdif_base64_push_metric(ctx, StatMetricId::IcRms); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabRms); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcRms); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaRms); // 2) 三相电压偏差占位 + 三线电压偏差 pqdif_base64_push_placeholder_block(ctx, "phase voltage deviation placeholder(Ua/Ub/Uc)", 3); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabDeviation); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcDeviation); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaDeviation); // 3) 频率偏差 + 频率 pqdif_base64_push_metric(ctx, StatMetricId::FrequencyDeviation); pqdif_base64_push_metric(ctx, StatMetricId::Frequency); // 4) 电压零/负/正序和负序不平衡 pqdif_base64_push_metric(ctx, StatMetricId::UZeroSeq); pqdif_base64_push_metric(ctx, StatMetricId::UNegSeq); pqdif_base64_push_metric(ctx, StatMetricId::UPosSeq); pqdif_base64_push_metric(ctx, StatMetricId::UNegSeqUnbalance); // 5) 电流零/负/正序和负序不平衡 pqdif_base64_push_metric(ctx, StatMetricId::IZeroSeq); pqdif_base64_push_metric(ctx, StatMetricId::INegSeq); pqdif_base64_push_metric(ctx, StatMetricId::IPosSeq); pqdif_base64_push_metric(ctx, StatMetricId::INegSeqUnbalance); // 6) 三相电压谐波 2-50 次指标占位 pqdif_base64_push_placeholder_block(ctx, "phase voltage harmonic placeholder(Ua/Ub/Uc,2-50)", 49 * 3); // 7) 三相电流谐波 2-50 次指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonic, 2, 50); // 8) 三线电压谐波 2-50 次指标 pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageHarmonic, 2, 50); // 9) 三相电压谐波 2-50 次相角指标占位 pqdif_base64_push_placeholder_block(ctx, "phase voltage harmonic angle placeholder(Ua/Ub/Uc,2-50)", 49 * 3); // 10) 三相电流谐波 2-50 次相角指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicAngle, 2, 50); // 11) 三线电压谐波 2-50 次相角指标 pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageHarmonicAngle, 2, 50); // 12) 三相电压间谐波 0.5-49.5 次指标占位 pqdif_base64_push_placeholder_block(ctx, "phase voltage interharmonic placeholder(Ua/Ub/Uc,0.5-49.5)", 50 * 3); // 13) 三相电流间谐波 0.5-49.5 次指标 pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentInterharmonic, 0, 49); // 14) 三线电压间谐波 0.5-49.5 次指标 pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageInterharmonic, 0, 49); // 15) A/B/C/总 有功/无功/视在功率 pqdif_base64_push_metric(ctx, StatMetricId::PaPower); pqdif_base64_push_metric(ctx, StatMetricId::QaPower); pqdif_base64_push_metric(ctx, StatMetricId::SaPower); pqdif_base64_push_metric(ctx, StatMetricId::PbPower); pqdif_base64_push_metric(ctx, StatMetricId::QbPower); pqdif_base64_push_metric(ctx, StatMetricId::SbPower); pqdif_base64_push_metric(ctx, StatMetricId::PcPower); pqdif_base64_push_metric(ctx, StatMetricId::QcPower); pqdif_base64_push_metric(ctx, StatMetricId::ScPower); pqdif_base64_push_metric(ctx, StatMetricId::PTotalPower); pqdif_base64_push_metric(ctx, StatMetricId::QTotalPower); pqdif_base64_push_metric(ctx, StatMetricId::STotalPower); // 16) A/B/C/总 谐波 2-50 次 有功/无功/视在功率 pqdif_base64_push_harmonic_power_phase(ctx, 0); pqdif_base64_push_harmonic_power_phase(ctx, 1); pqdif_base64_push_harmonic_power_phase(ctx, 2); pqdif_base64_push_harmonic_power_phase(ctx, 3); // 17) 谐波含有率:三相电压占位、三相电流、三线电压 pqdif_base64_push_placeholder_block(ctx, "phase voltage harmonic ratio placeholder(Ua/Ub/Uc,2-50)", 49 * 3); pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicRatio, 2, 50); pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageHarmonicRatio, 2, 50); // 18) 谐波总畸变率:三相电压占位、三相电流、三线电压 pqdif_base64_push_placeholder_block(ctx, "phase voltage THD placeholder(Ua/Ub/Uc)", 3); pqdif_base64_push_metric(ctx, StatMetricId::IaThd); pqdif_base64_push_metric(ctx, StatMetricId::IbThd); pqdif_base64_push_metric(ctx, StatMetricId::IcThd); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabThd); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcThd); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaThd); // 19) 三相功率因数和总功率因数 pqdif_base64_push_metric(ctx, StatMetricId::PFa); pqdif_base64_push_metric(ctx, StatMetricId::PFb); pqdif_base64_push_metric(ctx, StatMetricId::PFc); pqdif_base64_push_metric(ctx, StatMetricId::PFTotal); // 20) 三相基波功率因数和总基波功率因数 pqdif_base64_push_metric(ctx, StatMetricId::FundPFa); pqdif_base64_push_metric(ctx, StatMetricId::FundPFb); pqdif_base64_push_metric(ctx, StatMetricId::FundPFc); pqdif_base64_push_metric(ctx, StatMetricId::FundPFTotal); // 21) 三相电压变动幅值占位 + 三线电压变动幅值 pqdif_base64_push_placeholder_block(ctx, "phase voltage DVC placeholder(Ua/Ub/Uc)", 3); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabDvc); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcDvc); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaDvc); // 22) 三相短闪占位 + 三线短闪 pqdif_base64_push_placeholder_block(ctx, "phase voltage Pst placeholder(Ua/Ub/Uc)", 3); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabPst); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcPst); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaPst); // 23) 三相长闪占位 + 三线长闪 pqdif_base64_push_placeholder_block(ctx, "phase voltage Plt placeholder(Ua/Ub/Uc)", 3); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabPlt); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcPlt); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaPlt); // 24) A/B/C/总 基波有功/无功/视在功率 pqdif_base64_push_metric(ctx, StatMetricId::PaFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QaFundPower); pqdif_base64_push_metric(ctx, StatMetricId::SaFundPower); pqdif_base64_push_metric(ctx, StatMetricId::PbFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QbFundPower); pqdif_base64_push_metric(ctx, StatMetricId::SbFundPower); pqdif_base64_push_metric(ctx, StatMetricId::PcFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QcFundPower); pqdif_base64_push_metric(ctx, StatMetricId::ScFundPower); pqdif_base64_push_metric(ctx, StatMetricId::PTotalFundPower); pqdif_base64_push_metric(ctx, StatMetricId::QTotalFundPower); pqdif_base64_push_metric(ctx, StatMetricId::STotalFundPower); // 25) 基波有效值:三相基波电压占位、三相基波电流、三线基波电压 pqdif_base64_push_placeholder_block(ctx, "phase voltage fundamental RMS placeholder(Ua/Ub/Uc)", 3); pqdif_base64_push_metric(ctx, StatMetricId::IaFundRms); pqdif_base64_push_metric(ctx, StatMetricId::IbFundRms); pqdif_base64_push_metric(ctx, StatMetricId::IcFundRms); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabFundRms); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcFundRms); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaFundRms); // 26) 基波相角:三相基波电压相角占位、三相基波电流相角、三线基波电压相角 pqdif_base64_push_placeholder_block(ctx, "phase voltage fundamental angle placeholder(Ua/Ub/Uc)", 3); pqdif_base64_push_metric(ctx, StatMetricId::IaFundAngle); pqdif_base64_push_metric(ctx, StatMetricId::IbFundAngle); pqdif_base64_push_metric(ctx, StatMetricId::IcFundAngle); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabFundAngle); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcFundAngle); pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaFundAngle); if (ctx.delta_line_phase_fallback_count > 0 && pqdif_log_enabled(PqdifLogLevel::Core)) { std::cout << " [BASE64 DELTA LINE FALLBACK SUMMARY] time=" << ctx.bucket.timestamp_text << ", kind=" << stat_value_kind_name(ctx.kind) << ", fallback_count=" << ctx.delta_line_phase_fallback_count << std::endl; } placeholder_count = ctx.placeholder_count; missing_metric_count = ctx.missing_metric_count; missing_metric_names = ctx.missing_metric_names; return float_buffer; } void pqdif_build_and_queue_base64_records(ParsedPqdifFile& parsed_file) { if (parsed_file.aggregated_stat_buckets.empty()) { std::cout << "[PQDIF BASE64] skip: no aggregated buckets, file=" << parsed_file.source_file << std::endl; return; } // Wye 星型按星型顺序组装;Delta 角型按角型顺序组装。 // Unknown 暂按星型顺序组装,避免因为接线方式缺失直接丢弃数据。 const bool use_delta_assembly = (parsed_file.connection_kind == ParsedConnectionKind::Delta); const char* assembly_name = use_delta_assembly ? "DELTA" : "STAR"; const std::string full_path = pqdif_absolute_path_text(parsed_file.source_file); const std::vector kind_order = { StatValueKind::Max, StatValueKind::Min, StatValueKind::Avg, StatValueKind::P95 }; PqdifStatBase64FileBatch file_batch; file_batch.pqdif_file_path = full_path; file_batch.source_file = parsed_file.source_file; file_batch.mac = parsed_file.mac; file_batch.parsed_at = parsed_file.parsed_at; file_batch.parsed_at_text = format_time_text(parsed_file.parsed_at); file_batch.connection_kind = parsed_file.connection_kind; file_batch.time_points.reserve(parsed_file.aggregated_stat_buckets.size()); size_t generated = 0; size_t total_placeholders = 0; size_t total_missing_metrics = 0; size_t expected_float_count = 0; std::cout << "========== PQDIF BASE64 ASSEMBLY ==========" << std::endl; std::cout << "file=" << full_path << ", buckets=" << parsed_file.aggregated_stat_buckets.size() << ", connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind) << ", assembly=" << assembly_name << ", kind_order=Max/Min/Avg/P95" << ", queue_mode=file_batch/time_point/records" << std::endl; for (const auto& bucket : parsed_file.aggregated_stat_buckets) { PqdifStatBase64TimePointPacket time_packet; time_packet.timestamp = bucket.timestamp; time_packet.timestamp_text = bucket.timestamp_text; time_packet.records.reserve(kind_order.size()); for (StatValueKind kind : kind_order) { size_t placeholder_count = 0; size_t missing_metric_count = 0; std::vector missing_metric_names; std::vector floats = use_delta_assembly ? pqdif_build_delta_float_buffer_for_bucket( parsed_file, bucket, kind, placeholder_count, missing_metric_count, missing_metric_names) : pqdif_build_star_float_buffer_for_bucket( parsed_file, bucket, kind, placeholder_count, missing_metric_count, missing_metric_names); if (expected_float_count == 0) expected_float_count = floats.size(); PqdifStatBase64Record rec; rec.pqdif_file_path = full_path; rec.value_kind = kind; rec.value_kind_name = stat_value_kind_name(kind); rec.value_kind_code = stat_value_kind_code_for_base64(kind); rec.timestamp = bucket.timestamp; rec.timestamp_text = bucket.timestamp_text; rec.float_count = floats.size(); rec.placeholder_count = placeholder_count; rec.connection_kind = parsed_file.connection_kind; rec.base64_payload = pqdif_base64_encode_float_vector(floats); time_packet.total_float_count += rec.float_count; time_packet.total_placeholder_count += rec.placeholder_count; time_packet.total_base64_chars += rec.base64_payload.size(); time_packet.records.emplace_back(std::move(rec)); ++generated; total_placeholders += placeholder_count; total_missing_metrics += missing_metric_count; if (pqdif_log_enabled(PqdifLogLevel::Info) || generated <= 4) { const PqdifStatBase64Record& log_rec = time_packet.records.back(); std::cout << " [BASE64 SUB RECORD] time=" << bucket.timestamp_text << ", kind=" << log_rec.value_kind_name << ", kind_code=" << log_rec.value_kind_code << ", floats=" << log_rec.float_count << ", placeholders=" << log_rec.placeholder_count << ", missing_metrics=" << missing_metric_count << ", base64_len=" << log_rec.base64_payload.size() << std::endl; } if (pqdif_log_enabled(PqdifLogLevel::Info) && !missing_metric_names.empty()) { std::cout << " [BASE64 MISSING SUMMARY] time=" << bucket.timestamp_text << ", kind=" << stat_value_kind_name(kind) << ", count=" << missing_metric_names.size() << ", first_metrics="; const size_t limit = std::min(missing_metric_names.size(), 12); for (size_t i = 0; i < limit; ++i) { if (i != 0) std::cout << "/"; std::cout << missing_metric_names[i]; } if (missing_metric_names.size() > limit) std::cout << "/..."; std::cout << std::endl; } } time_packet.record_count = time_packet.records.size(); file_batch.total_record_count += time_packet.record_count; file_batch.total_float_count += time_packet.total_float_count; file_batch.total_placeholder_count += time_packet.total_placeholder_count; file_batch.total_base64_chars += time_packet.total_base64_chars; file_batch.time_points.emplace_back(std::move(time_packet)); } file_batch.time_point_count = file_batch.time_points.size(); std::cout << " [BASE64 FILE BATCH SUMMARY] generated_records=" << generated << ", time_points=" << file_batch.time_point_count << ", expected_float_count_per_record=" << expected_float_count << ", total_placeholders=" << total_placeholders << ", total_missing_metrics=" << total_missing_metrics << ", total_base64_chars=" << file_batch.total_base64_chars << std::endl; // 按你的要求,入队前完整打印保存对象内部结构:文件批次 -> 时间点 -> Max/Min/Avg/P95 子记录 -> Base64 内容。 // 日志会很长;确认完成后可以临时注释掉这一行,或改成 if (pqdif_log_enabled(PqdifLogLevel::Info)) 包裹。 //pqdif_dump_stat_base64_file_batch_full(file_batch); push_pqdif_stat_base64_file_batch(std::move(file_batch)); std::cout << " [BASE64 QUEUE SUMMARY] file_batch_queue_size=" << GetPqdifStatBase64QueueSize() << ", total_sub_records_in_queue=" << GetPqdifStatBase64RecordCountInQueue() << std::endl; std::cout << "================================================" << std::endl; } bool process_single_pqdif_file(const fs::path& file_path, const std::string& mac) { ParsedPqdifFile parsed_file; parsed_file.mac = mac; parsed_file.source_file = file_path.string(); parsed_file.parsed_at = std::time(nullptr); std::string err; if (!parse_pqdif_file_full(file_path.string(), parsed_file.logical_file, err)) { std::cout << "[PQDIF] parse failed: " << file_path.string() << " reason=" << err << std::endl; return false; } dump_logical_summary(parsed_file); dump_monitor_settings_probe(parsed_file); if (pqdif_log_enabled(PqdifLogLevel::Debug)) { // Debug 级别:打印全部 observation 概览,帮助确认指标是否位于非 Trend observation 中。 dump_observation_list(parsed_file); // Debug 级别:打印动态谱类候选通道,用于确认线电压谐波、相角、间谐波是否存在。 dump_dynamic_spectrum_candidate_probe(parsed_file); // Debug 级别:打印闪变候选通道,用于确认 Plt 通道是否存在数据点。 dump_flicker_candidate_probe(parsed_file); } if (pqdif_is_trace_log_enabled()) { // Trace 级别:保留旧的疑似谐波全量探针;日志量更大,仅排查复杂问题时打开。 dump_harmonic_channel_probe(parsed_file); } // 1) 识别接线方式。 // 后续指标判断需要先区分星型 / 角型两套规则。 parsed_file.connection_kind = stat_classify_connection_kind(parsed_file.logical_file); // 2) 只筛选“统计类 observation”,并对其展开目标统计指标。 // 当前阶段不处理全部 observation,避免不同 observation 混入同一时间聚合结果。 parsed_file.expanded_stat_points = stat_expand_selected_statistical_observation( parsed_file.logical_file, parsed_file.connection_kind, parsed_file.selected_observation_index, parsed_file.selected_observation_name); // 3) 对已筛选出的统计 observation,按 timestamp 聚合。 parsed_file.aggregated_stat_buckets = stat_group_points_by_timestamp(parsed_file.expanded_stat_points); if (pqdif_is_trace_log_enabled()) dump_semantic_probe(parsed_file); dump_expanded_stat_preview(parsed_file); // 分组结果平时也要打印核心摘要;开启详细日志时才会打印完整明细。 dump_grouped_bucket_preview(parsed_file); // 4) 将每个时间桶按接线方式组装为 Max/Min/Avg/P95 四条 Base64 暂存记录。 // Wye 使用星型顺序,Delta 使用角型顺序;两者 float 数量保持一致。 pqdif_build_and_queue_base64_records(parsed_file); if (!push_parsed_result_to_cache(std::move(parsed_file))) { std::cout << "[PQDIF] push cache failed: " << file_path.string() << std::endl; return false; } std::cout << "[PQDIF] processed ok: " << file_path.string() << ", cache_size=" << GetParsedPqdifCacheSize() << std::endl; return true; } struct PendingPqdifFile { fs::path path; std::string mac; }; void scan_once() { ensure_dir(kPqdRootDir); ensure_dir(kDoneRootDir); ensure_dir(kFailRootDir); std::error_code ec; if (!fs::exists(kPqdRootDir, ec) || !fs::is_directory(kPqdRootDir, ec)) return; std::vector pending_files; // 先收集所有 MAC 目录下的 PQDIF 文件,但本轮不会全部处理。 // 后面会按文件修改时间排序,并最多处理 kMaxPqdifFilesPerScan 个文件。 for (fs::directory_iterator mac_it(kPqdRootDir, ec), mac_end; !ec && mac_it != mac_end; mac_it.increment(ec)) { if (ec || !fs::is_directory(mac_it->path())) continue; const std::string mac = mac_it->path().filename().string(); std::error_code file_ec; for (fs::directory_iterator file_it(mac_it->path(), file_ec), file_end; !file_ec && file_it != file_end; file_it.increment(file_ec)) { if (file_ec || !is_pqdif_file(file_it->path())) continue; PendingPqdifFile item; item.path = file_it->path(); item.mac = mac; pending_files.push_back(item); } } if (pending_files.empty()) return; // 旧文件优先处理,避免目录中长期积压的旧 PQDIF 一直排在后面。 std::sort(pending_files.begin(), pending_files.end(), [](const PendingPqdifFile& a, const PendingPqdifFile& b) { std::error_code ea, eb; const auto ta = fs::last_write_time(a.path, ea); const auto tb = fs::last_write_time(b.path, eb); if (ea && !eb) return false; if (!ea && eb) return true; if (!ea && !eb && ta != tb) return ta < tb; return a.path.string() < b.path.string(); }); int processed_count = 0; for (const auto& item : pending_files) { if (processed_count >= kMaxPqdifFilesPerScan) break; std::cout << "[PQDIF] scan selected file: " << item.path.string() << ", pending_count=" << pending_files.size() << ", max_per_scan=" << kMaxPqdifFilesPerScan << std::endl; const bool ok = process_single_pqdif_file(item.path, item.mac); const fs::path target_root = ok ? fs::path(kDoneRootDir) : fs::path(kFailRootDir); const fs::path dst = target_root / item.mac / item.path.filename(); // 处理完成后移动文件,避免下一轮 scan_once() 重复解析同一个 PQDIF。 // 解析成功:download//.pqd -> download_done//.pqd // 解析失败:download//.pqd -> download_fail//.pqd // // 调试时如果想让文件保留在 download 目录中、方便反复解析同一个文件, // 可以临时注释掉下面这个 if 块;调试结束后建议恢复,否则每一轮都会重复解析。 if (!move_file_with_fallback(item.path, dst)) { std::cout << "[PQDIF] move failed: " << item.path.string() << " -> " << dst.string() << std::endl; } cleanup_backup_dir(fs::path(kDoneRootDir) / item.mac, kBackupLimit); cleanup_backup_dir(fs::path(kFailRootDir) / item.mac, kBackupLimit); ++processed_count; } } } // namespace bool PopOldestParsedPqdifFile(ParsedPqdifFile& out) { std::lock_guard guard(g_parsed_cache_mutex); if (g_parsed_cache.empty()) return false; out = std::move(g_parsed_cache.front()); g_parsed_cache.pop_front(); return true; } bool PeekOldestParsedPqdifFile(ParsedPqdifFile& out) { std::lock_guard guard(g_parsed_cache_mutex); if (g_parsed_cache.empty()) return false; out = g_parsed_cache.front(); return true; } size_t GetParsedPqdifCacheSize() { std::lock_guard guard(g_parsed_cache_mutex); return g_parsed_cache.size(); } void ClearParsedPqdifCache() { std::lock_guard guard(g_parsed_cache_mutex); g_parsed_cache.clear(); } bool PopOldestPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out) { std::lock_guard guard(g_pqdif_stat_base64_mutex); if (g_pqdif_stat_base64_queue.empty()) return false; out = std::move(g_pqdif_stat_base64_queue.front()); g_pqdif_stat_base64_queue.pop_front(); return true; } bool PeekOldestPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out) { std::lock_guard guard(g_pqdif_stat_base64_mutex); if (g_pqdif_stat_base64_queue.empty()) return false; out = g_pqdif_stat_base64_queue.front(); return true; } bool PopOldestPqdifStatBase64Record(PqdifStatBase64Record& out) { // 兼容旧接口:从“文件级批次队列”的最早文件、最早时间点中拆出一条子记录。 std::lock_guard guard(g_pqdif_stat_base64_mutex); while (!g_pqdif_stat_base64_queue.empty()) { PqdifStatBase64FileBatch& batch = g_pqdif_stat_base64_queue.front(); while (!batch.time_points.empty() && batch.time_points.front().records.empty()) batch.time_points.erase(batch.time_points.begin()); if (batch.time_points.empty()) { g_pqdif_stat_base64_queue.pop_front(); continue; } PqdifStatBase64TimePointPacket& tp = batch.time_points.front(); out = std::move(tp.records.front()); const size_t out_base64_len = out.base64_payload.size(); if (tp.record_count > 0) --tp.record_count; if (tp.total_float_count >= out.float_count) tp.total_float_count -= out.float_count; if (tp.total_placeholder_count >= out.placeholder_count) tp.total_placeholder_count -= out.placeholder_count; if (tp.total_base64_chars >= out_base64_len) tp.total_base64_chars -= out_base64_len; if (batch.total_record_count > 0) --batch.total_record_count; if (batch.total_float_count >= out.float_count) batch.total_float_count -= out.float_count; if (batch.total_placeholder_count >= out.placeholder_count) batch.total_placeholder_count -= out.placeholder_count; if (batch.total_base64_chars >= out_base64_len) batch.total_base64_chars -= out_base64_len; tp.records.erase(tp.records.begin()); if (tp.records.empty()) { batch.time_points.erase(batch.time_points.begin()); if (batch.time_point_count > 0) --batch.time_point_count; } if (batch.time_points.empty()) g_pqdif_stat_base64_queue.pop_front(); return true; } return false; } bool PeekOldestPqdifStatBase64Record(PqdifStatBase64Record& out) { // 兼容旧接口:只查看“文件级批次队列”的最早文件、最早时间点中的第一条子记录。 std::lock_guard guard(g_pqdif_stat_base64_mutex); for (const auto& batch : g_pqdif_stat_base64_queue) { for (const auto& tp : batch.time_points) { if (!tp.records.empty()) { out = tp.records.front(); return true; } } } return false; } size_t GetPqdifStatBase64QueueSize() { // v20 起返回文件级批次数量,不再是子记录数量。 std::lock_guard guard(g_pqdif_stat_base64_mutex); return g_pqdif_stat_base64_queue.size(); } size_t GetPqdifStatBase64RecordCountInQueue() { std::lock_guard guard(g_pqdif_stat_base64_mutex); return pqdif_stat_base64_count_records_in_queue_unlocked(); } void ClearPqdifStatBase64Queue() { // 清空 Base64 生成队列。 // 对象用途:调试或重置解析状态时使用;不会清空待后续处理队列。 std::lock_guard guard(g_pqdif_stat_base64_mutex); g_pqdif_stat_base64_queue.clear(); } bool PopReadyPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out) { // 从“待后续处理队列”取出一个完整 PQDIF 文件批次。 // 对象用途:后续入库/上传/推送逻辑应优先调用这个接口,而不是直接访问全局 deque。 std::lock_guard guard(g_pqdif_stat_base64_ready_mutex); if (g_pqdif_stat_base64_ready_queue.empty()) return false; out = std::move(g_pqdif_stat_base64_ready_queue.front()); g_pqdif_stat_base64_ready_queue.pop_front(); return true; } bool PeekReadyPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out) { // 查看“待后续处理队列”最早的 PQDIF 文件批次,但不移除。 // 对象用途:仅检查当前准备处理的数据内容,适合调试或状态展示。 std::lock_guard guard(g_pqdif_stat_base64_ready_mutex); if (g_pqdif_stat_base64_ready_queue.empty()) return false; out = g_pqdif_stat_base64_ready_queue.front(); return true; } size_t GetReadyPqdifStatBase64QueueSize() { // 返回待后续处理队列中的文件级批次数量。 // 注意:返回的是文件数量,不是 Max/Min/Avg/P95 子记录数量。 std::lock_guard guard(g_pqdif_stat_base64_ready_mutex); return g_pqdif_stat_base64_ready_queue.size(); } size_t GetReadyPqdifStatBase64RecordCountInQueue() { // 返回待后续处理队列中全部文件批次包含的 Base64 子记录数量。 std::lock_guard guard(g_pqdif_stat_base64_ready_mutex); return pqdif_stat_base64_count_records_in_queue_unlocked(g_pqdif_stat_base64_ready_queue); } void ClearReadyPqdifStatBase64Queue() { // 清空待后续处理队列。 // 对象用途:调试或重置后续处理状态时使用;不会清空解析生成队列。 std::lock_guard guard(g_pqdif_stat_base64_ready_mutex); g_pqdif_stat_base64_ready_queue.clear(); } void RunPqdifScanLoop() { std::cout << "[PQDIF] scan loop started, root=" << kPqdRootDir << ", interval=" << kScanIntervalSec << "s" << std::endl; while (true) { try { scan_once(); } catch (const std::exception& ex) { std::cout << "[PQDIF] scan exception: " << ex.what() << std::endl; } catch (...) { std::cout << "[PQDIF] scan exception: unknown" << std::endl; } try { // 循环最后,检查 Base64 生成队列中是否已经存在一个完整 PQDIF 文件批次。 // 如果存在,则取出最多一个并移动到“待后续处理队列”。 // 后续业务处理请调用 PopReadyPqdifStatBase64FileBatch() 从待处理队列取数据。 pqdif_move_one_generated_base64_batch_to_ready_queue(); PqdifStatBase64FileBatch batch; if (PopReadyPqdifStatBase64FileBatch(batch)) { // batch 就是一个 PQDIF 文件完整的 Base64 组装结果 // 在此处处理上送逻辑 } } catch (const std::exception& ex) { std::cout << "[PQDIF BASE64 READY] move exception: " << ex.what() << std::endl; } catch (...) { std::cout << "[PQDIF BASE64 READY] move exception: unknown" << std::endl; } std::this_thread::sleep_for(std::chrono::seconds(kScanIntervalSec)); } }