diff --git a/LFtid1056.zip b/LFtid1056--切分备份.zip similarity index 87% rename from LFtid1056.zip rename to LFtid1056--切分备份.zip index 5eb149c..216f06d 100644 Binary files a/LFtid1056.zip and b/LFtid1056--切分备份.zip differ diff --git a/LFtid1056/LFtid1056.vcxproj b/LFtid1056/LFtid1056.vcxproj index 206e6b8..03d521e 100644 --- a/LFtid1056/LFtid1056.vcxproj +++ b/LFtid1056/LFtid1056.vcxproj @@ -88,6 +88,7 @@ + @@ -306,6 +307,7 @@ + diff --git a/LFtid1056/LFtid1056.vcxproj.filters b/LFtid1056/LFtid1056.vcxproj.filters index 4ac9a20..5ec9e59 100644 --- a/LFtid1056/LFtid1056.vcxproj.filters +++ b/LFtid1056/LFtid1056.vcxproj.filters @@ -33,6 +33,7 @@ pqdif\include + @@ -675,6 +676,7 @@ pqdif\include + diff --git a/LFtid1056/pqdif/PQDIF.cpp b/LFtid1056/pqdif/PQDIF.cpp index 6760d68..bad6c51 100644 --- a/LFtid1056/pqdif/PQDIF.cpp +++ b/LFtid1056/pqdif/PQDIF.cpp @@ -10,7 +10,7 @@ CPQDIF::CPQDIF() { // Init - m_percont = (CPQDIF_PC_FlatFile *)theFactory.NewPersistController(PFPC_FlatFile); + m_percont = (CPQDIF_PC_FlatFile*)theFactory.NewPersistController(PFPC_FlatFile); } CPQDIF::~CPQDIF() @@ -61,7 +61,7 @@ bool CPQDIF::New() if (m_percont) delete m_percont; - m_percont = (CPQDIF_PC_FlatFile *)theFactory.NewPersistController(PFPC_FlatFile); + m_percont = (CPQDIF_PC_FlatFile*)theFactory.NewPersistController(PFPC_FlatFile); if (m_percont) ret = true; @@ -83,15 +83,15 @@ long CPQDIF::RecordGetCount() bool CPQDIF::RecordGetInfo ( long index, - GUID * tagRecordType, - string & nameRecordType + GUID* tagRecordType, + string& nameRecordType ) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) bool status = false; - CPQDIFRecord * prec; + CPQDIFRecord* prec; GUID tagRecord; string nameTag; @@ -107,11 +107,11 @@ bool CPQDIF::RecordGetInfo // Return the data... // The GUID itself - *tagRecordType=tagRecord; + *tagRecordType = tagRecord; // The GUID name nameTag = theInfo.GetNameOfTag(tagRecord); - nameRecordType=nameTag; + nameRecordType = nameTag; status = true; @@ -127,15 +127,38 @@ bool CPQDIF::RecordGetInfo return status; } -bool CPQDIF::RecordRequestObservation(long index, long * pRecordObserv) +bool CPQDIF::RecordRequestRecord(long index, long* pRecord) +{ + if (pRecord == nullptr) + return false; + + bool status = false; + try + { + CPQDIFRecord* prec = m_percont->GetRecordFull(index); + if (prec) + { + *pRecord = (long)prec; + status = true; + } + } + catch (...) + { + status = false; + } + return status; +} + + +bool CPQDIF::RecordRequestObservation(long index, long* pRecordObserv) { bool status = false; - CPQDIFRecord * precBase; - CPQDIFRecord * precCurrent = NULL; - CPQDIFRecord * precDS = NULL; - CPQDIFRecord * precSett = NULL; - CPQDIF_R_Observation * pobs = NULL; + CPQDIFRecord* precBase; + CPQDIFRecord* precCurrent = NULL; + CPQDIFRecord* precDS = NULL; + CPQDIFRecord* precSett = NULL; + CPQDIF_R_Observation* pobs = NULL; GUID tagRecord; long idxRec; @@ -225,7 +248,7 @@ bool CPQDIF::ObservationGetInfo(long pRecordObserv, DATE& timeStart, string& nam // AFX_MANAGE_STATE(AfxGetStaticModuleState()) bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; TIMESTAMPPQDIF timeStartLocal; TIMESTAMPPQDIF timeCreateLocal; // string nameLocal; @@ -267,15 +290,15 @@ bool CPQDIF::ObservationGetInfo(long pRecordObserv, DATE& timeStart, string& nam return status; } -bool CPQDIF::ObservationGetTriggerInfo(long pRecordObserv, long * idTriggerMethod, DATE * timeTriggered) +bool CPQDIF::ObservationGetTriggerInfo(long pRecordObserv, long* idTriggerMethod, DATE* timeTriggered) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; UINT4 idTriggerMethodLocal; - CPQDIF_E_Vector * pvectTriggerChanIdx = NULL; + CPQDIF_E_Vector* pvectTriggerChanIdx = NULL; TIMESTAMPPQDIF timeTriggeredLocal; *idTriggerMethod = -1; @@ -305,12 +328,12 @@ bool CPQDIF::ObservationGetTriggerInfo(long pRecordObserv, long * idTriggerMetho return status; } -bool CPQDIF::ObservationGetChannelInfo(long pRecordObserv, long idxChannel, string &name, long * countSeries) +bool CPQDIF::ObservationGetChannelInfo(long pRecordObserv, long idxChannel, string& name, long* countSeries) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; string nameLocal; UINT4 idPhaseLocal; @@ -348,10 +371,10 @@ bool CPQDIF::ObservationGetChannelInfo(long pRecordObserv, long idxChannel, stri return status; } -bool CPQDIF::ObservationGetSeriesInfo3(long pRecordObserv, long idxChannel, long idxSeries, long * idQuantityUnits, GUID * idQuantityCharacteristic, GUID * idValueType) +bool CPQDIF::ObservationGetSeriesInfo3(long pRecordObserv, long idxChannel, long idxSeries, long* idQuantityUnits, GUID* idQuantityCharacteristic, GUID* idValueType) { bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; UINT4 idQuantityUnitsLocal; GUID idValueTypeLocal; @@ -367,8 +390,8 @@ bool CPQDIF::ObservationGetSeriesInfo3(long pRecordObserv, long idxChannel, long { // Translate information *idQuantityUnits = (long)idQuantityUnitsLocal; - *idValueType=idValueTypeLocal; - *idQuantityCharacteristic=idQuantityCharacteristicLocal; + *idValueType = idValueTypeLocal; + *idQuantityCharacteristic = idQuantityCharacteristicLocal; } } @@ -384,13 +407,13 @@ bool CPQDIF::ObservationGetSeriesInfo3(long pRecordObserv, long idxChannel, long } //עҪdouble* Ϊڲ̬ڴ棬ʹҪͷ -bool CPQDIF::ObservationGetSeriesData(long pRecordObserv, long idxChannel, long idxSeries, double ** values,long* varCount) +bool CPQDIF::ObservationGetSeriesData(long pRecordObserv, long idxChannel, long idxSeries, double** values, long* varCount) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) bool status = false; - CPQDIF_R_Observation * pobs; - double * pvalues; + CPQDIF_R_Observation* pobs; + double* pvalues; long countPoints; try @@ -401,7 +424,7 @@ bool CPQDIF::ObservationGetSeriesData(long pRecordObserv, long idxChannel, long pvalues = pobs->NewResolvedSeries(idxChannel, idxSeries, countPoints); if (pvalues) { - *values= pvalues; + *values = pvalues; status = true; *varCount = countPoints; //delete[] pvalues; @@ -419,22 +442,22 @@ bool CPQDIF::ObservationGetSeriesData(long pRecordObserv, long idxChannel, long return status; } -bool CPQDIF::ObservationGetChannelFreq(long pRecObs, long idxChannel, double * freq) +bool CPQDIF::ObservationGetChannelFreq(long pRecObs, long idxChannel, double* freq) { bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; try { pobs = ValidateObservation(pRecObs); if (pobs) { - CPQDIF_E_Collection * pcol = pobs->GetOneChannel(idxChannel); + CPQDIF_E_Collection* pcol = pobs->GetOneChannel(idxChannel); if (pcol) { - CPQDIF_E_Scalar * psc = pobs->FindScalarInCollection(pcol, tagChannelFrequency); + CPQDIF_E_Scalar* psc = pobs->FindScalarInCollection(pcol, tagChannelFrequency); if (psc) { double val; @@ -457,23 +480,23 @@ bool CPQDIF::ObservationGetChannelFreq(long pRecObs, long idxChannel, double * f return status; } -bool CPQDIF::ObservationGetChannelGroupID(long pRecObs, long idxChannel, int *GroupID) +bool CPQDIF::ObservationGetChannelGroupID(long pRecObs, long idxChannel, int* GroupID) { bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; try { pobs = ValidateObservation(pRecObs); if (pobs) { - CPQDIF_E_Collection * pcol = pobs->GetOneChannel(idxChannel); + CPQDIF_E_Collection* pcol = pobs->GetOneChannel(idxChannel); if (pcol) { - CPQDIF_E_Scalar * psc = pobs->FindScalarInCollection(pcol, tagChannelGroupID); + CPQDIF_E_Scalar* psc = pobs->FindScalarInCollection(pcol, tagChannelGroupID); if (psc) { INT2 val = 0; @@ -511,11 +534,11 @@ bool CPQDIF::ObservationGetChannelGroupID(long pRecObs, long idxChannel, int *Gr } -bool CPQDIF::ObservationGetSeriesPhasicType(long pRecordObserv, long idxChannel, long idxSeries, long *valuetypes) +bool CPQDIF::ObservationGetSeriesPhasicType(long pRecordObserv, long idxChannel, long idxSeries, long* valuetypes) { bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; try { @@ -541,13 +564,13 @@ bool CPQDIF::ObservationGetSeriesPhasicType(long pRecordObserv, long idxChannel, } -bool CPQDIF::ObservationGetSeriesScale(long pRecObs, long idxChannel, long idxSeries, double * scale, double * offset) +bool CPQDIF::ObservationGetSeriesScale(long pRecObs, long idxChannel, long idxSeries, double* scale, double* offset) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; try { @@ -573,7 +596,7 @@ bool CPQDIF::RecordReleaseObservation(long pRecordObserv) // AFX_MANAGE_STATE(AfxGetStaticModuleState()) bool status = false; - CPQDIF_R_Observation * pobs; + CPQDIF_R_Observation* pobs; pobs = ValidateObservation(pRecordObserv); if (pobs) @@ -594,33 +617,33 @@ bool CPQDIF::SetDateFromTimeStamp(DATE& date, const TIMESTAMPPQDIF& ts) return true; } -CPQDIF_Element * CPQDIF::ValidateElement(long pElement) +CPQDIF_Element* CPQDIF::ValidateElement(long pElement) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) - CPQDIF_Element * pel; + CPQDIF_Element* pel; - pel = (CPQDIF_Element *)pElement; + pel = (CPQDIF_Element*)pElement; //ASSERT_VALID(pel); return pel; } -CPQDIF_E_Collection * CPQDIF::ValidateCollection(long pElement) +CPQDIF_E_Collection* CPQDIF::ValidateCollection(long pElement) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) - CPQDIF_Element * pel; - CPQDIF_E_Collection * pcoll = NULL; + CPQDIF_Element* pel; + CPQDIF_E_Collection* pcoll = NULL; - pel = (CPQDIF_Element *)pElement; + pel = (CPQDIF_Element*)pElement; //ASSERT_VALID(pel); if (pel) { if (pel->GetElementType() == ID_ELEMENT_TYPE_COLLECTION) { - pcoll = (CPQDIF_E_Collection *)pel; + pcoll = (CPQDIF_E_Collection*)pel; //ASSERT_VALID(pcoll); } } @@ -637,20 +660,20 @@ CPQDIF_E_Collection * CPQDIF::ValidateCollection(long pElement) // Valid object pointer The object is valid and of the correct type // NULL The object is invalid or has an incorrect type // -CPQDIF_E_Scalar * CPQDIF::ValidateScalar(long pElement) +CPQDIF_E_Scalar* CPQDIF::ValidateScalar(long pElement) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) - CPQDIF_Element * pel; - CPQDIF_E_Scalar * pscalar = NULL; + CPQDIF_Element* pel; + CPQDIF_E_Scalar* pscalar = NULL; - pel = (CPQDIF_Element *)pElement; + pel = (CPQDIF_Element*)pElement; //ASSERT_VALID(pel); if (pel) { if (pel->GetElementType() == ID_ELEMENT_TYPE_SCALAR) { - pscalar = (CPQDIF_E_Scalar *)pel; + pscalar = (CPQDIF_E_Scalar*)pel; //ASSERT_VALID(pscalar); } } @@ -667,20 +690,20 @@ CPQDIF_E_Scalar * CPQDIF::ValidateScalar(long pElement) // Valid object pointer The object is valid and of the correct type // NULL The object is invalid or has an incorrect type // -CPQDIF_E_Vector * CPQDIF::ValidateVector(long pElement) +CPQDIF_E_Vector* CPQDIF::ValidateVector(long pElement) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) - CPQDIF_Element * pel; - CPQDIF_E_Vector * pvector = NULL; + CPQDIF_Element* pel; + CPQDIF_E_Vector* pvector = NULL; - pel = (CPQDIF_Element *)pElement; + pel = (CPQDIF_Element*)pElement; //ASSERT_VALID(pel); if (pel) { if (pel->GetElementType() == ID_ELEMENT_TYPE_VECTOR) { - pvector = (CPQDIF_E_Vector *)pel; + pvector = (CPQDIF_E_Vector*)pel; //ASSERT_VALID(pvector); } } @@ -697,22 +720,22 @@ CPQDIF_E_Vector * CPQDIF::ValidateVector(long pElement) // Valid object pointer The object is valid and of the correct type // NULL The object is invalid or has an incorrect type // -CPQDIF_R_Observation * CPQDIF::ValidateObservation(long pRecObserv) +CPQDIF_R_Observation* CPQDIF::ValidateObservation(long pRecObserv) { // AFX_MANAGE_STATE(AfxGetStaticModuleState()) - CPQDIFRecord * prec; - CPQDIF_R_Observation * pobs = NULL; + CPQDIFRecord* prec; + CPQDIF_R_Observation* pobs = NULL; GUID tagThisRecord; - prec = (CPQDIFRecord *)pRecObserv; + prec = (CPQDIFRecord*)pRecObserv; //ASSERT_VALID(prec); if (prec) { prec->HeaderGetTag(tagThisRecord); if (PQDIF_IsEqualGUID(tagThisRecord, tagRecObservation)) { - pobs = (CPQDIF_R_Observation *)prec; + pobs = (CPQDIF_R_Observation*)prec; //ASSERT_VALID(pobs); } } @@ -720,7 +743,7 @@ CPQDIF_R_Observation * CPQDIF::ValidateObservation(long pRecObserv) return pobs; } -void testjson() +void testjson() { //{ "emspq1":[{ "type":1,"min":"{str}" },{ "type":2,"min":"{str2}" }],"emspq2":[{ "type":3,"min":"{str3}" },{ "type":4,"min":"{str4}" }] } // @@ -806,7 +829,7 @@ char* CDeal::AssJson(char* Id) // { i = getCurrentGroup(stringToTimeT(dataDate), it->second); } - else + else { i = getCurrentGroup(stringToTimeT(dataDate), aggCydeMin); } @@ -959,7 +982,7 @@ char* CDeal::AssJson(char* Id) // cJSON_AddStringToObject(emspq1_obj1, item->KeyName.c_str(), count_str.c_str()); } } - if (item->Type == "Null") + if (item->Type == "Null") { if (item->DateName == "monitorId") // ID { @@ -1064,7 +1087,7 @@ void CDeal::CheckDataTableList() // б for (std::list::iterator itemIt = seq->DateItemList.begin(); itemIt != seq->DateItemList.end(); ++itemIt) { CItem* item = *itemIt; - if (item->Type == "Null") + if (item->Type == "Null") { { std::string str = item->DateName; @@ -1331,7 +1354,7 @@ void CDeal::ResJsonCfg(char* json) str_data_name.replace(pos, 1, seq); } int start = 0, end = 0; - if (ExtractNumbersBetweenPercent(str_data_name,start,end)) + if (ExtractNumbersBetweenPercent(str_data_name, start, end)) { //printf(" Item: start=%d, end=%d\n", start,end); for (int i = start; i <= end; i++) { @@ -1361,7 +1384,7 @@ void CDeal::ResJsonCfg(char* json) //printf(" Item: KeyName=%s, DataName=%s, Type=%s\n", str_key_name_temp.c_str(), str_data_name_temp.c_str(), type); } } - else + else { CItem* citem = new CItem(key_name, str_data_name.c_str(), type); cseq->DateItemList.push_back(citem); @@ -1395,7 +1418,7 @@ std::string CDeal::convertToDateOnly(const std::string& dateTime) { return dateTime; // ûҵո񣬷ԭַȻв̫ܷ } -int CDeal::getCurrentGroup(const std::time_t& currentTime,int min) { +int CDeal::getCurrentGroup(const std::time_t& currentTime, int min) { // time_tתΪtmṹ std::tm* localTime = std::localtime(¤tTime); @@ -1432,7 +1455,7 @@ std::time_t CDeal::stringToTimeT(const std::string& dateTime) { return currentTime; } -void CDeal::clear() +void CDeal::clear() { // AvgDataͬʱͷlistеڴ棨еĻͨlistҪֶͷţ AvgData.clear(); diff --git a/LFtid1056/pqdif/PQDIF.h b/LFtid1056/pqdif/PQDIF.h index c0a0356..076d20f 100644 --- a/LFtid1056/pqdif/PQDIF.h +++ b/LFtid1056/pqdif/PQDIF.h @@ -51,7 +51,7 @@ struct PqdifSeriesInfoEx double offset = 0.0; // ƫ }; -class CPQDIF +class CPQDIF { public: CPQDIF(); @@ -66,19 +66,20 @@ public: bool RecordGetInfo ( long index, - GUID * tagRecordType, - string & nameRecordType + GUID* tagRecordType, + string& nameRecordType ); - bool RecordRequestObservation(long index, long * pRecordObserv); + bool RecordRequestRecord(long index, long* pRecord); + bool RecordRequestObservation(long index, long* pRecordObserv); bool ObservationGetInfo(long pRecordObserv, DATE& timeStart, string& name, long& countChannels); - bool ObservationGetTriggerInfo(long pRecordObserv, long * idTriggerMethod, DATE * timeTriggered); - bool ObservationGetChannelInfo(long pRecordObserv, long idxChannel, string &name, long * countSeries); - bool ObservationGetSeriesInfo3(long pRecordObserv, long idxChannel, long idxSeries, long * idQuantityUnits, GUID * idQuantityCharacteristic, GUID * idValueType); - bool ObservationGetSeriesData(long pRecordObserv, long idxChannel, long idxSeries, double ** values, long* varCount); - bool ObservationGetChannelFreq(long pRecObs, long idxChannel, double * freq); - bool ObservationGetChannelGroupID(long pRecObs, long idxChannel, int *GroupID); - bool ObservationGetSeriesPhasicType(long pRecordObserv, long idxChannel, long idxSeries, long *valuetypes); - bool ObservationGetSeriesScale(long pRecObs, long idxChannel, long idxSeries, double * scale, double * offset); + bool ObservationGetTriggerInfo(long pRecordObserv, long* idTriggerMethod, DATE* timeTriggered); + bool ObservationGetChannelInfo(long pRecordObserv, long idxChannel, string& name, long* countSeries); + bool ObservationGetSeriesInfo3(long pRecordObserv, long idxChannel, long idxSeries, long* idQuantityUnits, GUID* idQuantityCharacteristic, GUID* idValueType); + bool ObservationGetSeriesData(long pRecordObserv, long idxChannel, long idxSeries, double** values, long* varCount); + bool ObservationGetChannelFreq(long pRecObs, long idxChannel, double* freq); + bool ObservationGetChannelGroupID(long pRecObs, long idxChannel, int* GroupID); + bool ObservationGetSeriesPhasicType(long pRecordObserv, long idxChannel, long idxSeries, long* valuetypes); + bool ObservationGetSeriesScale(long pRecObs, long idxChannel, long idxSeries, double* scale, double* offset); bool RecordReleaseObservation(long pRecordObserv); // ȡ Observation ijͨչǩϢ @@ -88,18 +89,18 @@ public: bool ObservationGetSeriesInfoEx(long pRecordObserv, long idxChannel, long idxSeries, PqdifSeriesInfoEx* out); // Internal protected: - CPQDIF_Element * ValidateElement(long pElement); - CPQDIF_E_Collection * ValidateCollection(long pElement); - CPQDIF_E_Scalar * ValidateScalar(long pElement); - CPQDIF_E_Vector * ValidateVector(long pElement); - CPQDIF_R_Observation * ValidateObservation(long pRecObserv); - CPQDIF_R_DataSource * ValidateDataSource(long pRecDS); - CPQDIF_R_Settings * ValidateSettings(long pRecSettings); - CPQDIF_R_Container* ValidateContainer(long pRecCon); + CPQDIF_Element* ValidateElement(long pElement); + CPQDIF_E_Collection* ValidateCollection(long pElement); + CPQDIF_E_Scalar* ValidateScalar(long pElement); + CPQDIF_E_Vector* ValidateVector(long pElement); + CPQDIF_R_Observation* ValidateObservation(long pRecObserv); + CPQDIF_R_DataSource* ValidateDataSource(long pRecDS); + CPQDIF_R_Settings* ValidateSettings(long pRecSettings); + CPQDIF_R_Container* ValidateContainer(long pRecCon); bool SetDateFromTimeStamp(DATE& date, const TIMESTAMPPQDIF& ts); // Member data private: - CPQDIF_PC_FlatFile * m_percont; // Persistence controller + CPQDIF_PC_FlatFile* m_percont; // Persistence controller }; @@ -162,7 +163,7 @@ public: cJSON* cJSON_GetObjectItemCaseSensitive(cJSON* object, const char* string);//jsonṹ ȡֵ bool ExtractNumbersBetweenPercent(const std::string& str, int& start, int& end); std::string convertToDateOnly(const std::string& dateTime); - int getCurrentGroup(const std::time_t& currentTime,int min); + int getCurrentGroup(const std::time_t& currentTime, int min); std::time_t stringToTimeT(const std::string& dateTime); std::list DataTableList;//Ϣ diff --git a/LFtid1056/pqdif_semantic_ids.cpp b/LFtid1056/pqdif_semantic_ids.cpp new file mode 100644 index 0000000..029f1a9 --- /dev/null +++ b/LFtid1056/pqdif_semantic_ids.cpp @@ -0,0 +1,506 @@ +#include "pqdif_semantic_ids.h" + +#include +#include + +namespace pqdif_sem +{ + + // ============================================================================ + // GUID + // ============================================================================ + + std::string GuidToString(const GUID& g) + { + char buf[64] = { 0 }; + std::snprintf( + buf, + sizeof(buf), + "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", + g.Data1, g.Data2, g.Data3, + g.Data4[0], g.Data4[1], + g.Data4[2], g.Data4[3], g.Data4[4], + g.Data4[5], g.Data4[6], g.Data4[7]); + return std::string(buf); + } + + // ============================================================================ + // ͹̶ + // ԴPDF B.2 / B.3 / B.4 + pqdif/include/pqdif_id.h + // ============================================================================ + + const UIntSemanticEntry kPhaseTable[] = { + { ID_PHASE_NONE, "ID_PHASE_NONE", "", "tagPhaseID", "δָ" }, + { ID_PHASE_AN, "ID_PHASE_AN", "A-N", "tagPhaseID", "A " }, + { ID_PHASE_BN, "ID_PHASE_BN", "B-N", "tagPhaseID", "B " }, + { ID_PHASE_CN, "ID_PHASE_CN", "C-N", "tagPhaseID", "C " }, + { ID_PHASE_NG, "ID_PHASE_NG", "N-G", "tagPhaseID", "߶Ե" }, + { ID_PHASE_AB, "ID_PHASE_AB", "A-B", "tagPhaseID", "A-B " }, + { ID_PHASE_BC, "ID_PHASE_BC", "B-C", "tagPhaseID", "B-C " }, + { ID_PHASE_CA, "ID_PHASE_CA", "C-A", "tagPhaseID", "C-A " }, + { ID_PHASE_RES, "ID_PHASE_RES", "ʣ", "tagPhaseID", "Residual" }, + { ID_PHASE_NET, "ID_PHASE_NET", "ֵ", "tagPhaseID", "Net" }, + { ID_PHASE_TOTAL, "ID_PHASE_TOTAL", "", "tagPhaseID", "Total" }, + { ID_PHASE_LN_AVE, "ID_PHASE_LN_AVE", "ѹƽ", "tagPhaseID", "LN average" }, + { ID_PHASE_LL_AVE, "ID_PHASE_LL_AVE", "ߵѹƽ", "tagPhaseID", "LL average" }, + { ID_PHASE_WORST, "ID_PHASE_WORST", "", "tagPhaseID", "Worst phase" }, + { ID_PHASE_PLUS, "ID_PHASE_PLUS", "", "tagPhaseID", "Plus / forward" }, + { ID_PHASE_MINUS, "ID_PHASE_MINUS", "", "tagPhaseID", "Minus / reverse" }, + + { ID_PHASE_GENERAL_1, "ID_PHASE_GENERAL_1", "ͨ1", "tagPhaseID", "General slot 1" }, + { ID_PHASE_GENERAL_2, "ID_PHASE_GENERAL_2", "ͨ2", "tagPhaseID", "General slot 2" }, + { ID_PHASE_GENERAL_3, "ID_PHASE_GENERAL_3", "ͨ3", "tagPhaseID", "General slot 3" }, + { ID_PHASE_GENERAL_4, "ID_PHASE_GENERAL_4", "ͨ4", "tagPhaseID", "General slot 4" }, + { ID_PHASE_GENERAL_5, "ID_PHASE_GENERAL_5", "ͨ5", "tagPhaseID", "General slot 5" }, + { ID_PHASE_GENERAL_6, "ID_PHASE_GENERAL_6", "ͨ6", "tagPhaseID", "General slot 6" }, + { ID_PHASE_GENERAL_7, "ID_PHASE_GENERAL_7", "ͨ7", "tagPhaseID", "General slot 7" }, + { ID_PHASE_GENERAL_8, "ID_PHASE_GENERAL_8", "ͨ8", "tagPhaseID", "General slot 8" }, + { ID_PHASE_GENERAL_9, "ID_PHASE_GENERAL_9", "ͨ9", "tagPhaseID", "General slot 9" }, + { ID_PHASE_GENERAL_10, "ID_PHASE_GENERAL_10", "ͨ10", "tagPhaseID", "General slot 10" }, + { ID_PHASE_GENERAL_11, "ID_PHASE_GENERAL_11", "ͨ11", "tagPhaseID", "General slot 11" }, + { ID_PHASE_GENERAL_12, "ID_PHASE_GENERAL_12", "ͨ12", "tagPhaseID", "General slot 12" }, + { ID_PHASE_GENERAL_13, "ID_PHASE_GENERAL_13", "ͨ13", "tagPhaseID", "General slot 13" }, + { ID_PHASE_GENERAL_14, "ID_PHASE_GENERAL_14", "ͨ14", "tagPhaseID", "General slot 14" }, + { ID_PHASE_GENERAL_15, "ID_PHASE_GENERAL_15", "ͨ15", "tagPhaseID", "General slot 15" }, + { ID_PHASE_GENERAL_16, "ID_PHASE_GENERAL_16", "ͨ16", "tagPhaseID", "General slot 16" } + }; + const std::size_t kPhaseTableSize = sizeof(kPhaseTable) / sizeof(kPhaseTable[0]); + + const UIntSemanticEntry kQuantityMeasuredTable[] = { + { ID_QM_NONE, "ID_QM_NONE", "δָ", "tagQuantityMeasuredID", "δָ" }, + { ID_QM_VOLTAGE, "ID_QM_VOLTAGE", "ѹ", "tagQuantityMeasuredID", "Voltage" }, + { ID_QM_CURRENT, "ID_QM_CURRENT", "", "tagQuantityMeasuredID", "Current" }, + { ID_QM_POWER, "ID_QM_POWER", "", "tagQuantityMeasuredID", "Power" }, + { ID_QM_ENERGY, "ID_QM_ENERGY", "", "tagQuantityMeasuredID", "Energy" }, + { ID_QM_TEMPERATURE, "ID_QM_TEMPERATURE", "¶", "tagQuantityMeasuredID", "Temperature" }, + { ID_QM_PRESSURE, "ID_QM_PRESSURE", "ѹ", "tagQuantityMeasuredID", "Pressure" }, + { ID_QM_CHARGE, "ID_QM_CHARGE", "", "tagQuantityMeasuredID", "Charge" }, + { ID_QM_EFIELD, "ID_QM_EFIELD", "糡", "tagQuantityMeasuredID", "Electric field" }, + { ID_QM_MFIELD, "ID_QM_MFIELD", "ų", "tagQuantityMeasuredID", "Magnetic field" }, + { ID_QM_VELOCITY, "ID_QM_VELOCITY", "ٶ", "tagQuantityMeasuredID", "Velocity" }, + { ID_QM_BEARING, "ID_QM_BEARING", "λ", "tagQuantityMeasuredID", "Bearing" }, + { ID_QM_FORCE, "ID_QM_FORCE", "", "tagQuantityMeasuredID", "Force" }, + { ID_QM_TORQUE, "ID_QM_TORQUE", "ת", "tagQuantityMeasuredID", "Torque" }, + { ID_QM_POSITION, "ID_QM_POSITION", "λ", "tagQuantityMeasuredID", "Position" }, + { ID_QM_FLUXLINKAGE, "ID_QM_FLUXLINKAGE", "", "tagQuantityMeasuredID", "Flux linkage" }, + { ID_QM_FLUXDENSITY, "ID_QM_FLUXDENSITY", "ͨܶ", "tagQuantityMeasuredID", "Flux density" }, + { ID_QM_STATUS, "ID_QM_STATUS", "״̬", "tagQuantityMeasuredID", "Status quantity" } + }; + const std::size_t kQuantityMeasuredTableSize = sizeof(kQuantityMeasuredTable) / sizeof(kQuantityMeasuredTable[0]); + + const UIntSemanticEntry kQuantityUnitsTable[] = { + { ID_QU_NONE, "ID_QU_NONE", "޵λ", "tagQuantityUnitsID", "No unit" }, + { ID_QU_TIMESTAMP, "ID_QU_TIMESTAMP", "ʱ", "tagQuantityUnitsID", "Timestamp" }, + { ID_QU_SECONDS, "ID_QU_SECONDS", "", "tagQuantityUnitsID", "Seconds" }, + { ID_QU_CYCLES, "ID_QU_CYCLES", "ܲ", "tagQuantityUnitsID", "Cycles" }, + { ID_QU_VOLTS, "ID_QU_VOLTS", "V", "tagQuantityUnitsID", "Volts" }, + { ID_QU_AMPS, "ID_QU_AMPS", "A", "tagQuantityUnitsID", "Amps" }, + { ID_QU_VA, "ID_QU_VA", "VA", "tagQuantityUnitsID", "Volt-amps" }, + { ID_QU_WATTS, "ID_QU_WATTS", "W", "tagQuantityUnitsID", "Watts" }, + { ID_QU_VARS, "ID_QU_VARS", "var", "tagQuantityUnitsID", "Vars" }, + { ID_QU_OHMS, "ID_QU_OHMS", "", "tagQuantityUnitsID", "Ohms" }, + { ID_QU_SIEMENS, "ID_QU_SIEMENS", "S", "tagQuantityUnitsID", "Siemens" }, + { ID_QU_VOLTSPERAMP, "ID_QU_VOLTSPERAMP", "V/A", "tagQuantityUnitsID", "Volts per amp" }, + { ID_QU_JOULES, "ID_QU_JOULES", "J", "tagQuantityUnitsID", "Joules" }, + { ID_QU_HERTZ, "ID_QU_HERTZ", "Hz", "tagQuantityUnitsID", "Hertz" }, + { ID_QU_CELCIUS, "ID_QU_CELCIUS", "", "tagQuantityUnitsID", "Celsius" }, + { ID_QU_DEGREES, "ID_QU_DEGREES", "", "tagQuantityUnitsID", "Degrees" }, + { ID_QU_DB, "ID_QU_DB", "dB", "tagQuantityUnitsID", "Decibel" }, + { ID_QU_PERCENT, "ID_QU_PERCENT", "%", "tagQuantityUnitsID", "Percent" }, + { ID_QU_PERUNIT, "ID_QU_PERUNIT", "pu", "tagQuantityUnitsID", "Per-unit" }, + { ID_QU_SAMPLES, "ID_QU_SAMPLES", "samples", "tagQuantityUnitsID", "Samples" }, + { ID_QU_VARHOURS, "ID_QU_VARHOURS", "varh", "tagQuantityUnitsID", "Var-hours" }, + { ID_QU_WATTHOURS, "ID_QU_WATTHOURS", "Wh", "tagQuantityUnitsID", "Watt-hours" }, + { ID_QU_VAHOURS, "ID_QU_VAHOURS", "VAh", "tagQuantityUnitsID", "VA-hours" }, + { ID_QU_MPS, "ID_QU_MPS", "m/s", "tagQuantityUnitsID", "Meters per second" }, + { ID_QU_MPH, "ID_QU_MPH", "mph", "tagQuantityUnitsID", "Miles per hour" }, + { ID_QU_BARS, "ID_QU_BARS", "bar", "tagQuantityUnitsID", "Bars" }, + { ID_QU_PASCALS, "ID_QU_PASCALS", "Pa", "tagQuantityUnitsID", "Pascals" }, + { ID_QU_NEWTONS, "ID_QU_NEWTONS", "N", "tagQuantityUnitsID", "Newtons" }, + { ID_QU_NEWTONMETERS, "ID_QU_NEWTONMETERS", "Nm", "tagQuantityUnitsID", "Newton meters" }, + { ID_QU_RPM, "ID_QU_RPM", "rpm", "tagQuantityUnitsID", "Rotations per minute" }, + { ID_QU_RADPERSEC, "ID_QU_RADPERSEC", "rad/s", "tagQuantityUnitsID", "Radians per second" }, + { ID_QU_METERS, "ID_QU_METERS", "m", "tagQuantityUnitsID", "Meters" }, + { ID_QU_WEBERTURNS, "ID_QU_WEBERTURNS", "Wbturn", "tagQuantityUnitsID", "Weber-turns" }, + { ID_QU_TESLAS, "ID_QU_TESLAS", "T", "tagQuantityUnitsID", "Teslas" }, + { ID_QU_WEBERS, "ID_QU_WEBERS", "Wb", "tagQuantityUnitsID", "Webers" }, + { ID_QU_VOLTSPERVOLT, "ID_QU_VOLTSPERVOLT", "V/V", "tagQuantityUnitsID", "Volts per volt" }, + { ID_QU_AMPSPERAMP, "ID_QU_AMPSPERAMP", "A/A", "tagQuantityUnitsID", "Amps per amp" }, + { ID_QU_AMPSPERVOLT, "ID_QU_AMPSPERVOLT", "A/V", "tagQuantityUnitsID", "Amps per volt" } + }; + const std::size_t kQuantityUnitsTableSize = sizeof(kQuantityUnitsTable) / sizeof(kQuantityUnitsTable[0]); + + const UIntSemanticEntry kStorageMethodTable[] = { + { ID_SERIES_METHOD_VALUES, "ID_SERIES_METHOD_VALUES", "ֱӴֵ", "tagStorageMethodID", "SeriesValues ֱӱԭʼֵ" }, + { ID_SERIES_METHOD_SCALED, "ID_SERIES_METHOD_SCALED", "Ŵֵ", "tagStorageMethodID", "Ҫ scale/offset " }, + { ID_SERIES_METHOD_INCREMENT, "ID_SERIES_METHOD_INCREMENT", "ֵ", "tagStorageMethodID", "Ҫʽԭ" } + }; + const std::size_t kStorageMethodTableSize = sizeof(kStorageMethodTable) / sizeof(kStorageMethodTable[0]); + + const UIntSemanticEntry kTriggerMethodTable[] = { + { ID_TRIGGER_METH_NONE, "ID_TRIGGER_METH_NONE", "޴", "tagTriggerMethodID", "޴ʽ" }, + { ID_TRIGGER_METH_CHANNEL, "ID_TRIGGER_METH_CHANNEL", "ͨ", "tagTriggerMethodID", "ijͨ" }, + { ID_TRIGGER_METH_PERIODIC, "ID_TRIGGER_METH_PERIODIC", "ڴ", "tagTriggerMethodID", "̶ڲ/¼" }, + { ID_TRIGGER_METH_EXTERNAL, "ID_TRIGGER_METH_EXTERNAL", "ⲿ", "tagTriggerMethodID", "ⲿ¼" }, + { ID_TRIGGER_METH_PERIODIC_STATS, "ID_TRIGGER_METH_PERIODIC_STATS", "ͳƴ", "tagTriggerMethodID", "ͳ observation" } + }; + const std::size_t kTriggerMethodTableSize = sizeof(kTriggerMethodTable) / sizeof(kTriggerMethodTable[0]); + + // ============================================================================ + // GUID ͹̶ + // ԴPDF B.2 / B.4 + pqdif/include/pqdif_id.h + // д GUID ֱֵ pqdif_id.h + // ============================================================================ + +#define GUID_ENTRY(field_enum, guid_const, cn_name, tag_name, desc_text) \ + { guid_const, field_enum, #guid_const, cn_name, tag_name, desc_text } + +// ------------------------------ +// QuantityTypetagQuantityTypeID +// ------------------------------ + const GuidSemanticEntry kQuantityTypeTable[] = { + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_WAVEFORM, "", "tagQuantityTypeID", "㲨β"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_VALUELOG, "ͳ־", "tagQuantityTypeID", "ʱͳ/Ƽ¼ͳӳѡ"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_PHASOR, "", "tagQuantityTypeID", "ֵ/ʱ"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_RESPONSE, "Ӧ", "tagQuantityTypeID", "ƵӦ"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_FLASH, "䶨λ", "tagQuantityTypeID", "λ//Բ"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_HISTOGRAM, "ֱͼ", "tagQuantityTypeID", "BINLOW/BINHIGH/BINID/COUNT"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_HISTOGRAM3D, "άֱͼ", "tagQuantityTypeID", "X/Y ˫ά BIN + COUNT"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_CPF, "ۼƸ", "tagQuantityTypeID", "PROB + VAL"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_XY, "XY ", "tagQuantityTypeID", "˫ֵ"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_MAGDUR, "ֵʱ", "tagQuantityTypeID", "VAL + DURATION"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_XYZ, "XYZ ", "tagQuantityTypeID", "ֵ"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_MAGDURTIME, "ʱֵʱ", "tagQuantityTypeID", "TIME + VAL + DURATION"), + GUID_ENTRY(GuidSemanticField::QuantityType, ID_QT_MAGDURCOUNT, "ʱֵʱ", "tagQuantityTypeID", "TIME + VAL + DURATION + COUNT") + }; + const std::size_t kQuantityTypeTableSize = sizeof(kQuantityTypeTable) / sizeof(kQuantityTypeTable[0]); + + // ------------------------------ + // ValueTypetagValueTypeID + // ֻ PDF B.2 ȷг pqdif_id.h дڶͳƳ + // ------------------------------ + const GuidSemanticEntry kValueTypeTable[] = { + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_VAL, "ֵ", "tagValueTypeID", "Ĭֵ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_TIME, "ʱ", "tagValueTypeID", "ʱ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_MIN, "Сֵ", "tagValueTypeID", "ͳСֵ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_MAX, "ֵ", "tagValueTypeID", "ͳֵ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_AVG, "ƽֵ", "tagValueTypeID", "ͳƽֵ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_INST, "˲ʱֵ", "tagValueTypeID", "ãƼ VAL"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_PHASEANGLE, "", "tagValueTypeID", " VAL ȫ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_PHASEANGLE_MIN, "Сֵ", "tagValueTypeID", "Ӧ MIN е"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_PHASEANGLE_MAX, "ֵ", "tagValueTypeID", "Ӧ MAX е"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_PHASEANGLE_AVG, "ƽֵ", "tagValueTypeID", "Ӧ AVG е"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_AREA, "", "tagValueTypeID", ""), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_LATITUDE, "γ", "tagValueTypeID", "Latitude"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_DURATION, "ʱ", "tagValueTypeID", "Duration"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_LONGITUDE, "", "tagValueTypeID", "Longitude"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_POLARITY, "", "tagValueTypeID", "Polarity"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_BINID, "", "tagValueTypeID", "Histogram bin id"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_BINHIGH, "Ͻ", "tagValueTypeID", "Histogram bin high"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_BINLOW, "½", "tagValueTypeID", "Histogram bin low"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_XBINHIGH, "XϽ", "tagValueTypeID", "3D histogram X high"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_XBINLOW, "X½", "tagValueTypeID", "3D histogram X low"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_YBINHIGH, "YϽ", "tagValueTypeID", "3D histogram Y high"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_YBINLOW, "Y½", "tagValueTypeID", "3D histogram Y low"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_COUNT, "", "tagValueTypeID", "/¼"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_TRANSITION, "", "tagValueTypeID", "VALUELOG ¼"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_PROB, "", "tagValueTypeID", "ۼƸʰٷֱ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_INTERVAL, "", "tagValueTypeID", "ͳƼ/ֵ"), + GUID_ENTRY(GuidSemanticField::ValueType, ID_SERIES_VALUE_TYPE_STATUS, "״̬", "tagValueTypeID", "״ֵ̬/") + }; + const std::size_t kValueTypeTableSize = sizeof(kValueTypeTable) / sizeof(kValueTypeTable[0]); + + // ------------------------------ + // QuantityCharacteristictagQuantityCharacteristicID + // ֻͳӳá PDF B.2 pqdif_id.h пֱӶϵ + // ------------------------------ + const GuidSemanticEntry kQuantityCharacteristicTable[] = { + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_NONE, "", "tagQuantityCharacteristicID", "δָ"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_INSTANTANEOUS, "˲ʱ", "tagQuantityCharacteristicID", "Instantaneous f(t)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_SPECTRA, "Ƶ", "tagQuantityCharacteristicID", "Spectra F(F)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_PEAK, "ֵ", "tagQuantityCharacteristicID", "Peak value"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_RMS, "Чֵ", "tagQuantityCharacteristicID", "RMS value"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_HRMS, "гЧֵ", "tagQuantityCharacteristicID", "Harmonic RMS"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FREQUENCY, "Ƶ", "tagQuantityCharacteristicID", "Frequency"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_TOTAL_THD, "г", "tagQuantityCharacteristicID", "Total harmonic distortion (%)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_EVEN_THD, "żг", "tagQuantityCharacteristicID", "Even harmonic distortion (%)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_ODD_THD, "г", "tagQuantityCharacteristicID", "Odd harmonic distortion (%)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_CREST_FACTOR, "", "tagQuantityCharacteristicID", "Crest factor"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FORM_FACTOR, "", "tagQuantityCharacteristicID", "Form factor"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_ARITH_SUM, "", "tagQuantityCharacteristicID", "Arithmetic sum"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_S0S1, "ƽ", "tagQuantityCharacteristicID", "Zero sequence component unbalance (%)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_S2S1, "ƽ", "tagQuantityCharacteristicID", "Negative sequence component unbalance (%)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_SPOS, "", "tagQuantityCharacteristicID", "Positive sequence component"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_SNEG, "", "tagQuantityCharacteristicID", "Negative sequence component"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_SZERO, "", "tagQuantityCharacteristicID", "Zero sequence component"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_AVG_IMBAL, "ƽƽ", "tagQuantityCharacteristicID", "Imbalance by max deviation from average"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_TOTAL_THD_RMS, " THD(RMS)", "tagQuantityCharacteristicID", "Total THD normalized to RMS"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_ODD_THD_RMS, " THD(RMS)", "tagQuantityCharacteristicID", "Odd THD normalized to RMS"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_EVEN_THD_RMS, "ż THD(RMS)", "tagQuantityCharacteristicID", "Even THD normalized to RMS"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_TID, "ܼг", "tagQuantityCharacteristicID", "Total Interharmonic Distortion"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_TID_RMS, "ܼг(RMS)", "tagQuantityCharacteristicID", "Total Interharmonic Distortion normalized to RMS"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_IHRMS, "гЧֵ", "tagQuantityCharacteristicID", "Interharmonic RMS"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_SPECTRA_HGROUP, "гƵ", "tagQuantityCharacteristicID", "Spectra by Harmonic Group index"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_SPECTRA_IGROUP, "гƵ","tagQuantityCharacteristicID", "Spectra by Interharmonic Group index"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_TIF, "TIF", "tagQuantityCharacteristicID", "Telephone Influence Factor"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_MAG_AVG, "ƽֵ", "tagQuantityCharacteristicID", "Flicker average RMS value"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_MAX_DVV, " dV/V", "tagQuantityCharacteristicID", "dV/V base"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_FREQ_MAX, "гƵ", "tagQuantityCharacteristicID", "Frequency of maximum flicker harmonic"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_MAG_MAX, "гֵ", "tagQuantityCharacteristicID", "Magnitude of maximum flicker harmonic"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_WGT_AVG, "Ȩƽ", "tagQuantityCharacteristicID", "Spectrum weighted average"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_SPECTRUM, "Ƶ", "tagQuantityCharacteristicID", "Flicker spectrum VRMS(F)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_PST, "ʱ", "tagQuantityCharacteristicID", "Short Term Flicker"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_PLT, "ʱ", "tagQuantityCharacteristicID", "Long Term Flicker"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_TIF_RMS, "TIF(RMS)", "tagQuantityCharacteristicID", "TIF normalized to RMS"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_FLKR_PLTSLIDE, " PLT", "tagQuantityCharacteristicID", "Sliding PLT"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_IT, "IT", "tagQuantityCharacteristicID", "IT"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_RMS_DEMAND, "Чֵ", "tagQuantityCharacteristicID", "RMS value of current for a demand interval"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_ANSI_TDF, "ANSI ѹ", "tagQuantityCharacteristicID", "Transformer Derating Factor"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_K_FACTOR, "K ", "tagQuantityCharacteristicID", "Transformer K Factor"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_TDD, "", "tagQuantityCharacteristicID", "Total Demand Distortion"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_RMS_PEAK_DEMAND,"ֵ", "tagQuantityCharacteristicID", "Peak Demand Current"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_P, "й", "tagQuantityCharacteristicID", "Real power (watts)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_Q, "޹", "tagQuantityCharacteristicID", "Reactive power (VAR)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_S, "ڹ", "tagQuantityCharacteristicID", "Apparent power (VA)"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_PF, "", "tagQuantityCharacteristicID", "True Power Factor"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_DF, "λ", "tagQuantityCharacteristicID", "Displacement factor"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_P_DEMAND, "й", "tagQuantityCharacteristicID", "Real power demand"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_Q_DEMAND, "޹", "tagQuantityCharacteristicID", "Reactive power demand"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_S_DEMAND, "", "tagQuantityCharacteristicID", "Apparent power demand"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_DF_DEMAND, "λ", "tagQuantityCharacteristicID", "Displacement factor demand"), + GUID_ENTRY(GuidSemanticField::QuantityCharacteristic, ID_QC_PF_DEMAND, "", "tagQuantityCharacteristicID", "Power factor demand") + }; + const std::size_t kQuantityCharacteristicTableSize = + sizeof(kQuantityCharacteristicTable) / sizeof(kQuantityCharacteristicTable[0]); + + // ------------------------------ + // DisturbanceCategorytagDisturbanceCategoryID + // ͳӳҪˣΪͳָ + // ֻͷļPDF B.4 ҲܶϵĻ + // ------------------------------ + const GuidSemanticEntry kDisturbanceCategoryTable[] = { + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_NONE, "Ŷ", "tagDisturbanceCategoryID", "No IEEE 1159 definition applicable"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_TRANSIENT, "˲̬", "tagDisturbanceCategoryID", "IEEE 1159 Transient"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_LONGDUR, "ʱ RMS 仯", "tagDisturbanceCategoryID", "IEEE 1159 Long Duration RMS Variation"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_IMBALANCE, "ƽ", "tagDisturbanceCategoryID", "IEEE 1159 Imbalance"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_POWERFREQVARIATION, "Ƶ仯", "tagDisturbanceCategoryID", "IEEE 1159 Power Frequency Variation"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_VOLTAGEFLUCTUATION, "ѹ/", "tagDisturbanceCategoryID", "IEEE 1159 Voltage Fluctuation"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_WAVEDISTORT, "λ", "tagDisturbanceCategoryID", "IEEE 1159 Waveform Distortion"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_WAVEDISTORT_HARMONIC, "г", "tagDisturbanceCategoryID", "IEEE 1159 Harmonics Present"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_WAVEDISTORT_INTERHARMONIC, "г", "tagDisturbanceCategoryID", "IEEE 1159 Interharmonics Present"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_WAVEDISTORT_NOTCHING, "ȱ", "tagDisturbanceCategoryID", "IEEE 1159 Notching Present"), + GUID_ENTRY(GuidSemanticField::DisturbanceCategory, ID_DISTURB_1159_WAVEDISTORT_NOISE, "", "tagDisturbanceCategoryID", "IEEE 1159 Noise Present") + }; + const std::size_t kDisturbanceCategoryTableSize = + sizeof(kDisturbanceCategoryTable) / sizeof(kDisturbanceCategoryTable[0]); + +#undef GUID_ENTRY + + // ============================================================================ + // ѯ + // ============================================================================ + + const UIntSemanticEntry* FindUIntSemantic( + const UIntSemanticEntry* table, + std::size_t table_size, + uint32_t raw_value) + { + for (std::size_t i = 0; i < table_size; ++i) + { + if (table[i].raw_value == raw_value) + return &table[i]; + } + return nullptr; + } + + const UIntSemanticEntry* FindPhase(uint32_t raw_value) + { + return FindUIntSemantic(kPhaseTable, kPhaseTableSize, raw_value); + } + + const UIntSemanticEntry* FindQuantityMeasured(uint32_t raw_value) + { + return FindUIntSemantic(kQuantityMeasuredTable, kQuantityMeasuredTableSize, raw_value); + } + + const UIntSemanticEntry* FindQuantityUnits(uint32_t raw_value) + { + return FindUIntSemantic(kQuantityUnitsTable, kQuantityUnitsTableSize, raw_value); + } + + const UIntSemanticEntry* FindStorageMethod(uint32_t raw_value) + { + return FindUIntSemantic(kStorageMethodTable, kStorageMethodTableSize, raw_value); + } + + const UIntSemanticEntry* FindTriggerMethod(uint32_t raw_value) + { + return FindUIntSemantic(kTriggerMethodTable, kTriggerMethodTableSize, raw_value); + } + + static std::string MakeUnknownUIntName(const char* prefix, uint32_t raw_value) + { + return std::string(prefix) + "(" + std::to_string(raw_value) + ")"; + } + + std::string FindPhaseName(uint32_t raw_value) + { + const auto* e = FindPhase(raw_value); + return e ? e->display_name : MakeUnknownUIntName("UNKNOWN_PHASE", raw_value); + } + + std::string FindQuantityMeasuredName(uint32_t raw_value) + { + const auto* e = FindQuantityMeasured(raw_value); + return e ? e->display_name : MakeUnknownUIntName("UNKNOWN_MEASURED", raw_value); + } + + std::string FindQuantityUnitsName(uint32_t raw_value) + { + const auto* e = FindQuantityUnits(raw_value); + return e ? e->display_name : MakeUnknownUIntName("UNKNOWN_UNIT", raw_value); + } + + std::string FindStorageMethodName(uint32_t raw_value) + { + const auto* e = FindStorageMethod(raw_value); + return e ? e->display_name : MakeUnknownUIntName("UNKNOWN_STORAGE_METHOD", raw_value); + } + + std::string FindStorageMethodFlagsName(uint32_t raw_value) + { + std::vector parts; + + if (raw_value & ID_SERIES_METHOD_VALUES) + parts.emplace_back("VALUES"); + if (raw_value & ID_SERIES_METHOD_SCALED) + parts.emplace_back("SCALED"); + if (raw_value & ID_SERIES_METHOD_INCREMENT) + parts.emplace_back("INCREMENT"); + + if (parts.empty()) + return "UNKNOWN_STORAGE_METHOD(" + std::to_string(raw_value) + ")"; + + std::ostringstream oss; + for (size_t i = 0; i < parts.size(); ++i) + { + if (i > 0) oss << "|"; + oss << parts[i]; + } + return oss.str(); + } + + std::string FindTriggerMethodName(uint32_t raw_value) + { + const auto* e = FindTriggerMethod(raw_value); + return e ? e->display_name : MakeUnknownUIntName("UNKNOWN_TRIGGER_METHOD", raw_value); + } + + // ============================================================================ + // GUID registry + // ============================================================================ + + GuidSemanticRegistry::GuidSemanticRegistry() + { + auto add_table = [this](const GuidSemanticEntry* table, std::size_t n) + { + for (std::size_t i = 0; i < n; ++i) + { + const GuidSemanticEntry& e = table[i]; + map_.insert({ Key{ e.field, e.raw_guid }, &e }); + } + }; + + add_table(kQuantityTypeTable, kQuantityTypeTableSize); + add_table(kValueTypeTable, kValueTypeTableSize); + add_table(kQuantityCharacteristicTable, kQuantityCharacteristicTableSize); + add_table(kDisturbanceCategoryTable, kDisturbanceCategoryTableSize); + } + + const GuidSemanticEntry* GuidSemanticRegistry::Find(GuidSemanticField field, const GUID& raw_guid) const + { + auto it = map_.find(Key{ field, raw_guid }); + if (it == map_.end()) + return nullptr; + return it->second; + } + + std::string GuidSemanticRegistry::FindName(GuidSemanticField field, const GUID& raw_guid, const char* fallback) const + { + const auto* e = Find(field, raw_guid); + if (e) + return e->display_name; + return fallback ? std::string(fallback) : GuidToString(raw_guid); + } + + const GuidSemanticRegistry& GetGuidSemanticRegistry() + { + static GuidSemanticRegistry registry; + return registry; + } + + // ============================================================================ + // helper + // ============================================================================ + + bool IsQuantityTypeValueLog(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_QT_VALUELOG); + } + + bool IsValueTypeTime(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_SERIES_VALUE_TYPE_TIME); + } + + bool IsValueTypeVal(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_SERIES_VALUE_TYPE_VAL); + } + + bool IsValueTypeMin(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_SERIES_VALUE_TYPE_MIN); + } + + bool IsValueTypeMax(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_SERIES_VALUE_TYPE_MAX); + } + + bool IsValueTypeAvg(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_SERIES_VALUE_TYPE_AVG); + } + + bool IsValueTypeInterval(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_SERIES_VALUE_TYPE_INTERVAL); + } + + bool IsCharacteristicFrequency(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_QC_FREQUENCY); + } + + bool IsCharacteristicRms(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_QC_RMS); + } + + bool IsCharacteristicTotalThd(const GUID& raw_guid) + { + return GuidEqual(raw_guid, ID_QC_TOTAL_THD) || GuidEqual(raw_guid, ID_QC_TOTAL_THD_RMS); + } + + bool IsUnitHertz(uint32_t raw_value) + { + return raw_value == ID_QU_HERTZ; + } + + bool IsTriggerPeriodicStats(uint32_t raw_value) + { + return raw_value == ID_TRIGGER_METH_PERIODIC_STATS; + } + +} // namespace pqdif_sem \ No newline at end of file diff --git a/LFtid1056/pqdif_semantic_ids.h b/LFtid1056/pqdif_semantic_ids.h new file mode 100644 index 0000000..34d19d0 --- /dev/null +++ b/LFtid1056/pqdif_semantic_ids.h @@ -0,0 +1,347 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "pqdif/include/pqdif_ph.h" +#include "pqdif/include/pqdif_id.h" + +namespace pqdif_sem +{ + + // ============================================================================ + // ˵ + // ---------------------------------------------------------------------------- + // ļֻ͡ײ + // ﱣԭʼֵֶUINT4 / GUID + // ӳͨIJԭʼֵͳɡ׼ + ˵ + // ============================================================================ + + // ------------------------------ + // GUID + // ------------------------------ + + inline bool GuidEqual(const GUID& a, const GUID& b) + { + return std::memcmp(&a, &b, sizeof(GUID)) == 0; + } + + struct GuidHash + { + std::size_t operator()(const GUID& g) const noexcept + { + const unsigned char* p = reinterpret_cast(&g); + std::size_t h = 1469598103934665603ull; // FNV-1a + for (std::size_t i = 0; i < sizeof(GUID); ++i) + { + h ^= static_cast(p[i]); + h *= 1099511628211ull; + } + return h; + } + }; + + std::string GuidToString(const GUID& g); + + // ============================================================================ + // һ ID ö٣ԴPDF B.2 / B.3 / B.4 pqdif_id.h к궨壩 + // ============================================================================ + + // ------------------------------ + // 1) tagPhaseID UINT4 + // ԴPDF B.2Դpqdif/include/pqdif_id.h + // ------------------------------ + enum class PhaseId : uint32_t + { + None = ID_PHASE_NONE, + AN = ID_PHASE_AN, + BN = ID_PHASE_BN, + CN = ID_PHASE_CN, + NG = ID_PHASE_NG, + AB = ID_PHASE_AB, + BC = ID_PHASE_BC, + CA = ID_PHASE_CA, + RES = ID_PHASE_RES, + NET = ID_PHASE_NET, + Total = ID_PHASE_TOTAL, + LnAve = ID_PHASE_LN_AVE, + LlAve = ID_PHASE_LL_AVE, + Worst = ID_PHASE_WORST, + Plus = ID_PHASE_PLUS, + Minus = ID_PHASE_MINUS, + + General1 = ID_PHASE_GENERAL_1, + General2 = ID_PHASE_GENERAL_2, + General3 = ID_PHASE_GENERAL_3, + General4 = ID_PHASE_GENERAL_4, + General5 = ID_PHASE_GENERAL_5, + General6 = ID_PHASE_GENERAL_6, + General7 = ID_PHASE_GENERAL_7, + General8 = ID_PHASE_GENERAL_8, + General9 = ID_PHASE_GENERAL_9, + General10 = ID_PHASE_GENERAL_10, + General11 = ID_PHASE_GENERAL_11, + General12 = ID_PHASE_GENERAL_12, + General13 = ID_PHASE_GENERAL_13, + General14 = ID_PHASE_GENERAL_14, + General15 = ID_PHASE_GENERAL_15, + General16 = ID_PHASE_GENERAL_16 + }; + + // ------------------------------ + // 2) tagQuantityMeasuredID UINT4 + // ԴPDF B.2Դpqdif/include/pqdif_id.h + // ע⣺û FrequencyƵ characteristic  + // ------------------------------ + enum class QuantityMeasuredId : uint32_t + { + None = ID_QM_NONE, + Voltage = ID_QM_VOLTAGE, + Current = ID_QM_CURRENT, + Power = ID_QM_POWER, + Energy = ID_QM_ENERGY, + Temperature = ID_QM_TEMPERATURE, + Pressure = ID_QM_PRESSURE, + Charge = ID_QM_CHARGE, + EField = ID_QM_EFIELD, + MField = ID_QM_MFIELD, + Velocity = ID_QM_VELOCITY, + Bearing = ID_QM_BEARING, + Force = ID_QM_FORCE, + Torque = ID_QM_TORQUE, + Position = ID_QM_POSITION, + FluxLinkage = ID_QM_FLUXLINKAGE, + FluxDensity = ID_QM_FLUXDENSITY, + Status = ID_QM_STATUS + }; + + // ------------------------------ + // 3) λtagQuantityUnitsID UINT4 + // ԴPDF B.2Դpqdif/include/pqdif_id.h + // ------------------------------ + enum class QuantityUnitsId : uint32_t + { + None = ID_QU_NONE, + Timestamp = ID_QU_TIMESTAMP, + Seconds = ID_QU_SECONDS, + Cycles = ID_QU_CYCLES, + Volts = ID_QU_VOLTS, + Amps = ID_QU_AMPS, + VA = ID_QU_VA, + Watts = ID_QU_WATTS, + Vars = ID_QU_VARS, + Ohms = ID_QU_OHMS, + Siemens = ID_QU_SIEMENS, + VoltsPerAmp = ID_QU_VOLTSPERAMP, + Joules = ID_QU_JOULES, + Hertz = ID_QU_HERTZ, + Celcius = ID_QU_CELCIUS, + Degrees = ID_QU_DEGREES, + Db = ID_QU_DB, + Percent = ID_QU_PERCENT, + PerUnit = ID_QU_PERUNIT, + Samples = ID_QU_SAMPLES, + VarHours = ID_QU_VARHOURS, + WattHours = ID_QU_WATTHOURS, + VaHours = ID_QU_VAHOURS, + Mps = ID_QU_MPS, + Mph = ID_QU_MPH, + Bars = ID_QU_BARS, + Pascals = ID_QU_PASCALS, + Newtons = ID_QU_NEWTONS, + NewtonMeters = ID_QU_NEWTONMETERS, + Rpm = ID_QU_RPM, + RadPerSec = ID_QU_RADPERSEC, + Meters = ID_QU_METERS, + WeberTurns = ID_QU_WEBERTURNS, + Teslas = ID_QU_TESLAS, + Webers = ID_QU_WEBERS, + VoltsPerVolt = ID_QU_VOLTSPERVOLT, + AmpsPerAmp = ID_QU_AMPSPERAMP, + AmpsPerVolt = ID_QU_AMPSPERVOLT + }; + + // ------------------------------ + // 4) д洢ʽtagStorageMethodID UINT4 + // ԴPDF B.2Դpqdif/include/pqdif_id.h + // ------------------------------ + enum class StorageMethodId : uint32_t + { + Values = ID_SERIES_METHOD_VALUES, + Scaled = ID_SERIES_METHOD_SCALED, + Increment = ID_SERIES_METHOD_INCREMENT + }; + + // ------------------------------ + // 5) ʽtagTriggerMethodID UINT4 + // ԴPDF B.3 / B.4Դpqdif/include/pqdif_id.h + // ------------------------------ + enum class TriggerMethodId : uint32_t + { + None = ID_TRIGGER_METH_NONE, + Channel = ID_TRIGGER_METH_CHANNEL, + Periodic = ID_TRIGGER_METH_PERIODIC, + External = ID_TRIGGER_METH_EXTERNAL, + PeriodicStats = ID_TRIGGER_METH_PERIODIC_STATS + }; + + // ============================================================================ + // Ŀ + // ============================================================================ + + struct UIntSemanticEntry + { + uint32_t raw_value; // ԭʼ UINT4 ֵ + const char* standard_name; // ׼/ͷļ ID_PHASE_AN + const char* display_name; // չʾ + const char* source_tag; // ֵĸ׼ֶΣ tagPhaseID + const char* comment; // ˵ + }; + + // ============================================================================ + // GUID + // ---------------------------------------------------------------------------- + // GUID ֱֳֵ pqdif_id.h г + // ÿһܻݵ pqdif ͷļ塣 + // ============================================================================ + + enum class GuidSemanticField + { + QuantityType, // tagQuantityTypeID + ValueType, // tagValueTypeID + QuantityCharacteristic, // tagQuantityCharacteristicID + DisturbanceCategory // tagDisturbanceCategoryID + }; + + struct GuidSemanticEntry + { + GUID raw_guid; // ԭʼ GUID ֵ + GuidSemanticField field; // ֶ + const char* standard_name; // ׼/ͷļ ID_QT_VALUELOG + const char* display_name; // չʾ + const char* source_tag; // Ӧı׼ֶ + const char* comment; // ˵ + }; + + // ============================================================================ + // ġ̶̬ݹ̶Դ̶registry ֻDzѯ + // ============================================================================ + + extern const UIntSemanticEntry kPhaseTable[]; + extern const std::size_t kPhaseTableSize; + + extern const UIntSemanticEntry kQuantityMeasuredTable[]; + extern const std::size_t kQuantityMeasuredTableSize; + + extern const UIntSemanticEntry kQuantityUnitsTable[]; + extern const std::size_t kQuantityUnitsTableSize; + + extern const UIntSemanticEntry kStorageMethodTable[]; + extern const std::size_t kStorageMethodTableSize; + + extern const UIntSemanticEntry kTriggerMethodTable[]; + extern const std::size_t kTriggerMethodTableSize; + + extern const GuidSemanticEntry kQuantityTypeTable[]; + extern const std::size_t kQuantityTypeTableSize; + + extern const GuidSemanticEntry kValueTypeTable[]; + extern const std::size_t kValueTypeTableSize; + + extern const GuidSemanticEntry kQuantityCharacteristicTable[]; + extern const std::size_t kQuantityCharacteristicTableSize; + + extern const GuidSemanticEntry kDisturbanceCategoryTable[]; + extern const std::size_t kDisturbanceCategoryTableSize; + + // ============================================================================ + // 塢ѯ + // ============================================================================ + + const UIntSemanticEntry* FindUIntSemantic( + const UIntSemanticEntry* table, + std::size_t table_size, + uint32_t raw_value); + + const UIntSemanticEntry* FindPhase(uint32_t raw_value); + const UIntSemanticEntry* FindQuantityMeasured(uint32_t raw_value); + const UIntSemanticEntry* FindQuantityUnits(uint32_t raw_value); + const UIntSemanticEntry* FindStorageMethod(uint32_t raw_value); + const UIntSemanticEntry* FindTriggerMethod(uint32_t raw_value); + + std::string FindPhaseName(uint32_t raw_value); + std::string FindQuantityMeasuredName(uint32_t raw_value); + std::string FindQuantityUnitsName(uint32_t raw_value); + std::string FindStorageMethodName(uint32_t raw_value); + std::string FindStorageMethodFlagsName(uint32_t raw_value); + std::string FindTriggerMethodName(uint32_t raw_value); + + // ============================================================================ + // GUID ע + // ---------------------------------------------------------------------------- + // ݹ̶registry ֻǰѹ̶ɿٲѯ + // ============================================================================ + + class GuidSemanticRegistry + { + public: + GuidSemanticRegistry(); + + const GuidSemanticEntry* Find(GuidSemanticField field, const GUID& raw_guid) const; + std::string FindName(GuidSemanticField field, const GUID& raw_guid, const char* fallback = "UNKNOWN_GUID") const; + + private: + struct Key + { + GuidSemanticField field; + GUID guid; + }; + + struct KeyHash + { + std::size_t operator()(const Key& k) const noexcept + { + std::size_t h1 = std::hash{}(static_cast(k.field)); + std::size_t h2 = GuidHash{}(k.guid); + return h1 ^ (h2 << 1); + } + }; + + struct KeyEq + { + bool operator()(const Key& a, const Key& b) const noexcept + { + return a.field == b.field && GuidEqual(a.guid, b.guid); + } + }; + + std::unordered_map map_; + }; + + const GuidSemanticRegistry& GetGuidSemanticRegistry(); + + // ============================================================================ + // ߡͳӳ䳣 helper + // ---------------------------------------------------------------------------- + // ֻšȷԴҶͳӳ䳣áжϡ + // ² helper + // ============================================================================ + + bool IsQuantityTypeValueLog(const GUID& raw_guid); + bool IsValueTypeTime(const GUID& raw_guid); + bool IsValueTypeVal(const GUID& raw_guid); + bool IsValueTypeMin(const GUID& raw_guid); + bool IsValueTypeMax(const GUID& raw_guid); + bool IsValueTypeAvg(const GUID& raw_guid); + bool IsValueTypeInterval(const GUID& raw_guid); + bool IsCharacteristicFrequency(const GUID& raw_guid); + bool IsCharacteristicRms(const GUID& raw_guid); + bool IsCharacteristicTotalThd(const GUID& raw_guid); + bool IsUnitHertz(uint32_t raw_value); + bool IsTriggerPeriodicStats(uint32_t raw_value); + +} // namespace pqdif_sem#pragma once diff --git a/LFtid1056/pqdif_thread_processor.cpp b/LFtid1056/pqdif_thread_processor.cpp index 488a33c..254bc1b 100644 --- a/LFtid1056/pqdif_thread_processor.cpp +++ b/LFtid1056/pqdif_thread_processor.cpp @@ -12,49 +12,56 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include -// PQDIF +// 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; - constexpr int kScanIntervalSec = 60; // ɨĿ¼ - constexpr int kBackupLimit = 4800; // ɹ/ʧĿ¼ļ - constexpr size_t kParsedCacheLimit = 128; // ڴ滺 - - const char* kPqdRootDir = "download"; // Ŀ¼download//*.pqd + 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 ݴӡ - // ============================ + // PQDIF 统计桶 Base64 文件级“待后续处理队列”。 + // 对象用途:RunPqdifScanLoop() 每轮循环末尾会从生成队列取出最多一个 + // PqdifStatBase64FileBatch,并移动到这个队列中,后续入库/上传/推送逻辑 + // 可以从这里取数据。 + // 设计原因:避免在扫描解析队列上直接做耗时业务处理,同时保证取出的文件批次 + // 不会因为局部变量析构而丢失。 + std::deque g_pqdif_stat_base64_ready_queue; + std::mutex g_pqdif_stat_base64_ready_mutex; - // GUID תַڲ鿴ǩֵ std::string guid_to_string(const GUID& g) { char buf[64] = { 0 }; @@ -74,121 +81,94 @@ namespace { return std::string(buf); } - // ӡֵǰ־ - void print_value_preview(const std::vector& values, const char* title, size_t preview_count = 5) + std::string safe_tag_name(const GUID& tag) { - if (values.empty()) - return; - - std::cout << " " << title << " count=" << values.size() << " preview=["; - const size_t n = std::min(values.size(), preview_count); - for (size_t i = 0; i < n; ++i) - { - if (i > 0) - std::cout << ", "; - std::cout << values[i]; - } - if (values.size() > preview_count) - std::cout << ", ..."; - std::cout << "]" << std::endl; + const char* name = theInfo.GetNameOfTag(tag); + if (name != nullptr && name[0] != '\0') + return std::string(name); + return guid_to_string(tag); } - // ӡʱǰ - void print_time_preview(const std::vector& values, const char* title, size_t preview_count = 5) + PqdifGuidValue make_guid_value(const GUID& guid) { - if (values.empty()) - return; - - std::cout << " " << title << " count=" << values.size() << " preview=["; - const size_t n = std::min(values.size(), preview_count); - for (size_t i = 0; i < n; ++i) - { - if (i > 0) - std::cout << ", "; - std::cout << static_cast(values[i]); - } - if (values.size() > preview_count) - std::cout << ", ..."; - std::cout << "]" << std::endl; + PqdifGuidValue out; + out.value = guid; + out.symbolic_name = safe_tag_name(guid); + return out; } - // ӡijһ series ıǩ - void print_series_meta(const RawSeriesTagMeta& meta, const char* name) + std::string format_time_text(time_t ts) { - std::cout << " [" << name << "]" - << " units_id=" << meta.quantity_units_id - << " characteristic_id=" << guid_to_string(meta.quantity_characteristic_id) - << " value_type_id=" << guid_to_string(meta.value_type_id) - << " base_type=" << meta.series_base_type - << " scale=" << meta.series_scale - << " offset=" << meta.series_offset - << std::endl; + 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); } - // ӡͨ - void dump_channel_detail(const std::string& key, const RawChannelSeries& ch) + PqdifTimestampValue make_timestamp_value(CPQDIF& file_convert, const TIMESTAMPPQDIF& ts) { - std::cout << " [CHANNEL]" - << " key=" << key - << " raw_name=" << ch.channel_tag.raw_channel_name - << " phase_id=" << ch.channel_tag.phase_id - << " quantity_type_id=" << guid_to_string(ch.channel_tag.quantity_type_id) - << " quantity_measured_id=" << ch.channel_tag.quantity_measured_id - << " freq=" << ch.channel_tag.channel_frequency - << " group_id=" << ch.channel_tag.group_id - << std::endl; + PqdifTimestampValue out; + out.day = ts.day; + out.second = ts.sec; - if (!ch.times.empty()) + if (ts.day != 0 || std::fabs(ts.sec) > 1e-12) { - print_series_meta(ch.time_meta, "TIME"); - print_time_preview(ch.times, "TIME"); + 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); } - if (!ch.max_values.empty()) - { - print_series_meta(ch.max_meta, "MAX"); - print_value_preview(ch.max_values, "MAX"); - } - - if (!ch.min_values.empty()) - { - print_series_meta(ch.min_meta, "MIN"); - print_value_preview(ch.min_values, "MIN"); - } - - if (!ch.avg_values.empty()) - { - print_series_meta(ch.avg_meta, "AVG"); - print_value_preview(ch.avg_values, "AVG"); - } - - if (!ch.cp95_values.empty()) - { - print_series_meta(ch.cp95_meta, "CP95"); - print_value_preview(ch.cp95_values, "CP95"); - } - - if (!ch.val_values.empty()) - { - print_series_meta(ch.val_meta, "VAL"); - print_value_preview(ch.val_values, "VAL"); - } + return out; } - // ӡļݴժҪ - void dump_parsed_map_summary(const std::string& file_path, const RawChannelMap& raw_map) + PqdifRecordHeaderInfo build_record_header_info(CPQDIFRecord* record, long record_index) { - std::cout << "========== PQDIF PARSED SUMMARY ==========" << std::endl; - std::cout << "file=" << file_path - << ", channel_count=" << raw_map.size() - << std::endl; + PqdifRecordHeaderInfo info; + info.record_index = record_index; - for (const auto& kv : raw_map) - { - dump_channel_detail(kv.first, kv.second); - } + if (record == nullptr) + return info; - std::cout << "==========================================" << std::endl; + 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) @@ -225,7 +205,6 @@ namespace { std::cout << "[PQDIF] file basic info: path=" << file_path << ", size=" << file_size << std::endl; - // ӡǰ 32 ֽʮƣжǷ׼ļͷ unsigned char buf[32] = { 0 }; ifs.read(reinterpret_cast(buf), sizeof(buf)); std::streamsize n = ifs.gcount(); @@ -263,8 +242,6 @@ namespace { return s.substr(beg, end - beg); } - // 淶ͨ - // ע⣺ǰ׶ֻ map keyΪʶ std::string normalize_key(const std::string& src) { std::string out; @@ -349,31 +326,6 @@ namespace { } } - // ȶ keyֻڴ洢 - // ָʶҪ - std::string build_channel_key(const std::string& channel_name, double channel_freq, int group_id) - { - std::ostringstream oss; - oss << trim_copy(channel_name); - - if (channel_freq > 0.0) - { - const int harmonic_no = static_cast(std::llround(channel_freq / 50.0)); - if (harmonic_no > 0) - oss << "[" << harmonic_no << "]"; - } - else if (group_id > 0) - { - oss << "[" << group_id << "]"; - } - - return normalize_key(oss.str()); - } - - // ============================ - // - // ============================ - bool push_parsed_result_to_cache(ParsedPqdifFile&& parsed) { std::lock_guard guard(g_parsed_cache_mutex); @@ -389,13 +341,6037 @@ namespace { return true; } - // ============================ - // PQDIF ԭʼ - // ǰ׶Σֻǩ + ԭʼֵ - // ============================ - - bool parse_pqdif_file_raw(const std::string& file_path, RawChannelMap& out_map, std::string& err) + 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); @@ -414,170 +6390,1187 @@ namespace { 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; - // ǰ׶ֻ Observation Record - if (!PQDIF_IsEqualGUID(record_guid, tagRecObservation)) - continue; - - long observation_handle = 0; - if (!file_convert.RecordRequestObservation(i_record, &observation_handle)) - continue; - - DATE time_start = 0; - std::string observation_name; - long channel_count = 0; - - if (!file_convert.ObservationGetInfo(observation_handle, time_start, observation_name, channel_count)) + long raw_record_handle = 0; + if (file_convert.RecordRequestRecord(i_record, &raw_record_handle)) { - file_convert.RecordReleaseObservation(observation_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; } - // ǰֻ߳ͳ Observation¼ Observation - long trigger_method_id = 0; - DATE trigger_time = 0; - file_convert.ObservationGetTriggerInfo(observation_handle, &trigger_method_id, &trigger_time); - - if (trigger_method_id == ID_TRIGGER_METH_CHANNEL || trigger_method_id == -1) + if (PQDIF_IsEqualGUID(record_guid, tagRecDataSource)) { - file_convert.RecordReleaseObservation(observation_handle); - continue; - } - - time_t observation_start_ts = 0; - file_convert.GetTime(time_start, &observation_start_ts); - - // ͨ - for (int i_channel = 0; i_channel < channel_count; ++i_channel) - { - PqdifChannelInfoEx ch_info; - if (!file_convert.ObservationGetChannelInfoEx(observation_handle, i_channel, &ch_info)) - continue; - - if (ch_info.name.empty()) - continue; - - double channel_freq = 0.0; - int group_id = 0; - file_convert.ObservationGetChannelFreq(observation_handle, i_channel, &channel_freq); - file_convert.ObservationGetChannelGroupID(observation_handle, i_channel, &group_id); - - const std::string key = build_channel_key(ch_info.name, channel_freq, group_id); - RawChannelSeries& raw_series = out_map[key]; - - // ͨǩ - raw_series.channel_tag.raw_channel_name = ch_info.name; - raw_series.channel_tag.normalized_channel_name = key; - raw_series.channel_tag.phase_id = ch_info.phaseId; - raw_series.channel_tag.quantity_type_id = ch_info.quantityTypeId; - raw_series.channel_tag.quantity_measured_id = ch_info.quantityMeasuredId; - raw_series.channel_tag.channel_frequency = channel_freq; - raw_series.channel_tag.group_id = group_id; - - // ͨµ series - for (int i_series = 0; i_series < ch_info.countSeries; ++i_series) + 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)) { - PqdifSeriesInfoEx sr_info; - if (!file_convert.ObservationGetSeriesInfoEx(observation_handle, i_channel, i_series, &sr_info)) - continue; - - double* values = nullptr; - long value_count = 0; - - if (!file_convert.ObservationGetSeriesData(observation_handle, i_channel, i_series, &values, &value_count) || - values == nullptr || value_count <= 0) - { - delete[] values; - continue; - } - - RawSeriesTagMeta series_meta; - series_meta.quantity_units_id = sr_info.quantityUnitsId; - series_meta.quantity_characteristic_id = sr_info.quantityCharacteristicId; - series_meta.value_type_id = sr_info.valueTypeId; - series_meta.series_base_type = sr_info.seriesBaseType; - series_meta.series_scale = sr_info.scale; - series_meta.series_offset = sr_info.offset; - - // ͬ valueType 浽ͬͰ - if (PQDIF_IsEqualGUID(sr_info.valueTypeId, ID_SERIES_VALUE_TYPE_TIME)) - { - raw_series.time_meta = series_meta; - for (long i = 0; i < value_count; ++i) - { - raw_series.times.push_back( - observation_start_ts + static_cast(std::llround(values[i])) - ); - } - } - else if (PQDIF_IsEqualGUID(sr_info.valueTypeId, ID_SERIES_VALUE_TYPE_MAX)) - { - raw_series.max_meta = series_meta; - raw_series.max_values.insert(raw_series.max_values.end(), values, values + value_count); - } - else if (PQDIF_IsEqualGUID(sr_info.valueTypeId, ID_SERIES_VALUE_TYPE_MIN)) - { - raw_series.min_meta = series_meta; - raw_series.min_values.insert(raw_series.min_values.end(), values, values + value_count); - } - else if (PQDIF_IsEqualGUID(sr_info.valueTypeId, ID_SERIES_VALUE_TYPE_AVG)) - { - raw_series.avg_meta = series_meta; - raw_series.avg_values.insert(raw_series.avg_values.end(), values, values + value_count); - } - else if (PQDIF_IsEqualGUID(sr_info.valueTypeId, ID_SERIES_VALUE_TYPE_P95)) - { - raw_series.cp95_meta = series_meta; - raw_series.cp95_values.insert(raw_series.cp95_values.end(), values, values + value_count); - } - else if (PQDIF_IsEqualGUID(sr_info.valueTypeId, ID_SERIES_VALUE_TYPE_VAL)) - { - raw_series.val_meta = series_meta; - raw_series.val_values.insert(raw_series.val_values.end(), values, values + value_count); - } - - delete[] values; + 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; } - file_convert.RecordReleaseObservation(observation_handle); + 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) { - RawChannelMap raw_map; - std::string err; + ParsedPqdifFile parsed_file; + parsed_file.mac = mac; + parsed_file.source_file = file_path.string(); + parsed_file.parsed_at = std::time(nullptr); - if (!parse_pqdif_file_raw(file_path.string(), raw_map, err)) + 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_parsed_map_summary(file_path.string(), raw_map); + dump_logical_summary(parsed_file); + dump_monitor_settings_probe(parsed_file); - ParsedPqdifFile parsed_file; - parsed_file.mac = mac; - parsed_file.source_file = file_path.string(); - parsed_file.parsed_at = std::time(nullptr); - parsed_file.channels = std::move(raw_map); + 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))) { @@ -586,18 +7579,17 @@ namespace { } std::cout << "[PQDIF] processed ok: " << file_path.string() - << " channels=" << GetParsedPqdifCacheSize() + << ", cache_size=" << GetParsedPqdifCacheSize() << std::endl; - //ڴ˴ļ - - return true; } - // ============================ - // ɨĿ¼ - // ============================ + struct PendingPqdifFile + { + fs::path path; + std::string mac; + }; void scan_once() { @@ -609,58 +7601,87 @@ namespace { if (!fs::exists(kPqdRootDir, ec) || !fs::is_directory(kPqdRootDir, ec)) return; - // һĿ¼ mac + 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::vector pqdif_files; 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())) - pqdif_files.push_back(file_it->path()); + 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 (pqdif_files.empty()) - continue; + if (pending_files.empty()) + return; - // ȴļ - std::sort(pqdif_files.begin(), pqdif_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); - }); + // 旧文件优先处理,避免目录中长期积压的旧 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); - for (const auto& file_path : pqdif_files) + 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)) { - const bool ok = process_single_pqdif_file(file_path, mac); - - const fs::path target_root = ok ? fs::path(kDoneRootDir) : fs::path(kFailRootDir); - const fs::path dst = target_root / mac / file_path.filename(); - - //ʱʱرļת - /*if (!move_file_with_fallback(file_path, dst)) - { - std::cout << "[PQDIF] move failed: " - << file_path.string() << " -> " << dst.string() - << std::endl; - }*/ + std::cout << "[PQDIF] move failed: " + << item.path.string() << " -> " << dst.string() + << std::endl; } - cleanup_backup_dir(fs::path(kDoneRootDir) / mac, kBackupLimit); - cleanup_backup_dir(fs::path(kFailRootDir) / mac, kBackupLimit); + 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); @@ -696,9 +7717,165 @@ void ClearParsedPqdifCache() 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() { @@ -721,6 +7898,29 @@ void RunPqdifScanLoop() 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)); } -} \ No newline at end of file +} diff --git a/LFtid1056/pqdif_thread_processor.h b/LFtid1056/pqdif_thread_processor.h index e1a4595..02fb40a 100644 --- a/LFtid1056/pqdif_thread_processor.h +++ b/LFtid1056/pqdif_thread_processor.h @@ -1,8 +1,8 @@ #pragma once -#pragma once #include #include +#include #include #include #include @@ -10,75 +10,759 @@ #include "pqdif/include/pqdif_ph.h" // ============================ -// ͨǩ +// 新版:完整 PQDIF 逻辑结构 +// 说明: +// 1) 该结构尽量忠实表达 PQDIF 的“文件 -> 记录 -> 定义/实例 -> 数组值”关系; +// 2) 后续所有映射、归并、按时刻整合都应该基于这些结构做; +// 3) 对暂未显式建模的标签,统一进入 extra_tags,避免丢失信息。 // ============================ -struct RawChannelTagMeta + +using PqdifExtraTagMap = std::map; + +// GUID 及其可读名称 +struct PqdifGuidValue { - std::string raw_channel_name; // ԭʼ tagChannelName - std::string normalized_channel_name; // ڲõĹ淶 key + GUID value{}; // 原始 GUID 值 + std::string symbolic_name; // 通过 PQDIF 信息表解析出的标准名,如 tagPhaseID / ID_QT_VALUELOG 等 +}; - long phase_id = -1; // tagPhaseID - GUID quantity_type_id{}; // tagQuantityTypeID - long quantity_measured_id = -1; // tagQuantityMeasuredID +// PQDIF 原始时间戳 +struct PqdifTimestampValue +{ + UINT4 day = 0; // 自 1900-01-01 起的天数,PQDIF 原始 day 字段 + double second = 0.0; // 当天起算的秒,PQDIF 原始 sec 字段 + time_t unix_time = 0; // 转换后的 Unix 时间戳,便于上层使用 + std::string text; // 格式化后的本地时间文本,便于调试和日志输出 +}; - double channel_frequency = 0.0; // ʶг - int group_id = 0; // ʶг +// 记录头信息 +struct PqdifRecordHeaderInfo +{ + long record_index = -1; // 本记录在文件中的顺序索引 + long file_position = 0; // 记录头在文件中的偏移 + PqdifGuidValue record_type; // 记录类型,如 tagContainer / tagRecDataSource / tagRecObservation + long header_size = 0; // 记录头字节数 + long data_size = 0; // 记录体字节数 + long next_record_position = 0; // 下一条记录偏移 + unsigned int checksum = 0; // 记录校验值 +}; + +// 通用数组值容器:用于保存 PQDIF 向量/序列的真正数组内容 +struct PqdifValueArray +{ + int physical_type = -1; // PQDIF 物理类型 ID,例如 REAL8 / UINT4 / TIMESTAMPPQDIF + long count = 0; // 数组点数 + + std::vector bool_values; // 布尔数组 + std::vector int_values; // 有符号整数数组 + std::vector uint_values; // 无符号整数数组 + std::vector real_values; // 浮点数组,最常见的统计值/波形值 + std::vector> complex_values; // 复数数组 + std::vector timestamp_values; // 时间戳数组 + std::vector guid_values; // GUID 数组 + std::vector text_values; // 字符串数组(CHAR1/CHAR2) +}; + +// Container / General Record:文件级元数据 +struct PqdifContainerRecord +{ + PqdifRecordHeaderInfo header; // 记录头 + + std::vector version_info; // tagVersionInfo,格式版本号数组 + std::string file_name; // tagFileName,原始文件名 + PqdifTimestampValue creation_time; // tagCreation,文件创建时间 + PqdifTimestampValue last_saved_time; // tagLastSaved,最后保存时间 + unsigned int times_saved = 0; // tagTimesSaved,保存次数 + + std::string language; // tagLanguage,语言 + std::string title; // tagTitle,标题 + std::string subject; // tagSubject,主题 + std::string author; // tagAuthor,作者 + std::string keywords; // tagKeywords,关键字 + std::string comments; // tagComments,备注 + std::string last_saved_by; // tagLastSavedBy,最后保存者 + std::string application; // tagApplication,生成软件 + + PqdifGuidValue compression_style_id; // tagCompressionStyleID,压缩风格 + unsigned int compression_algorithm_id = 0; // tagCompressionAlgorithmID,压缩算法 + unsigned int compression_checksum = 0; // tagCompressionChecksum,压缩校验值 + + std::string owner; // tagOwner,所有者 + std::string copyright; // tagCopyright,版权 + std::string trademarks; // tagTrademarks,商标 + std::string notes; // tagNotes,附加说明 + + std::string address1; // tagAddress1,地址行 1 + std::string address2; // tagAddress2,地址行 2 + std::string city; // tagCity,城市 + std::string state; // tagState,州/省 + std::string postal_code; // tagPostalCode,邮编 + std::string country; // tagCountry,国家 + std::string phone_voice; // tagPhoneVoice,电话 + std::string phone_fax; // tagPhoneFAX,传真 + std::string email; // tagEmail,邮箱 + + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Series Definition:序列定义,描述“这一列是什么” +struct PqdifSeriesDefinition +{ + int series_def_index = -1; // 序列定义在所属通道定义中的顺序索引 + + unsigned int quantity_units_id = 0; // tagQuantityUnitsID,单位 ID + PqdifGuidValue quantity_characteristic_id; // tagQuantityCharacteristicID,特征量 ID + PqdifGuidValue value_type_id; // tagValueTypeID,值类型 ID,例如 TIME / AVG / MAX / MIN / VAL + unsigned int storage_method_id = 0; // tagStorageMethodID,序列存储方式 + + unsigned int significant_digits_id = 0; // tagQuantitySignificantDigitsID,有效位定义 + double quantity_resolution = 0.0; // tagQuantityResolutionID / 解析出的分辨率 + double nominal_quantity = 0.0; // tagSeriesNominalQuantity,标称值 + + std::string value_type_name; // tagValueTypeName,值类型显示名 + unsigned int hint_greek_prefix_id = 0; // tagHintGreekPrefixID,显示前缀提示 + unsigned int hint_preferred_units_id = 0; // tagHintPreferredUnitsID,优选单位提示 + unsigned int hint_default_display_id = 0; // tagHintDefaultDisplayID,默认显示方式提示 + double prob_interval = 0.0; // tagProbInterval,概率间隔 + double prob_percentile = 0.0; // tagProbPercentile,概率百分位 + PqdifTimestampValue effective_time; // tagEffective,定义生效时间 + + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Channel Definition:通道定义,描述“这个通道测什么” +struct PqdifChannelDefinition +{ + int channel_def_index = -1; // 通道定义在数据源中的顺序索引 + + std::string channel_name; // tagChannelName,通道名称 + unsigned int phase_id = 0; // tagPhaseID,相别 + std::string other_channel_identifier; // tagOtherChannelIdentifier,附加通道标识 + std::string group_name; // tagGroupName,组名称 + + PqdifGuidValue quantity_type_id; // tagQuantityTypeID,量类型(如 VALUELOG / PHASOR) + unsigned int quantity_measured_id = 0; // tagQuantityMeasuredID,被测量对象(如电压/电流/频率) + unsigned int physical_channel = 0; // tagPhysicalChannel,物理通道编号 + std::string quantity_name; // tagQuantityName,自定义量名称 + int primary_series_index = -1; // tagPrimarySeriesIdx,主序列索引 + + std::vector series_definitions; // 该通道下的全部序列定义 + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Data Source Record:数据源定义,描述设备和通道体系 +struct PqdifDataSourceRecord +{ + PqdifRecordHeaderInfo header; // 记录头 + int data_source_index = -1; // 在文件中解析出的数据源顺序索引 + long record_index = -1; // 对应文件记录索引,便于关联 Observation + + PqdifGuidValue data_source_type_id; // tagDataSourceTypeID,数据源类型 + PqdifGuidValue vendor_id; // tagVendorID,厂商 ID + PqdifGuidValue equipment_id; // tagEquipmentID,设备型号 ID + std::string custom_source_info; // tagCustomSourceInfo,扩展来源信息 + + PqdifGuidValue instrument_type_id; // tagInstrumentTypeID,仪器类型 + std::string instrument_model_name; // tagInstrumentModelName,仪器型号名称 + std::string instrument_model_number; // tagInstrumentModelNumber,仪器型号编号 + std::string serial_number; // tagSerialNumberDS,序列号 + std::string version; // tagVersionDS,版本号 + std::string name; // tagNameDS,设备名称 + std::string owner; // tagOwnerDS,设备归属 + std::string location; // tagLocationDS,安装位置 + std::string time_zone; // tagTimeZoneDS,时区文本 + std::string coordinates; // tagCoordinatesDS,坐标信息 + + std::vector channel_definitions; // 数据源下的全部通道定义 + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Monitor Settings 中单通道的配置 +struct PqdifChannelSetting +{ + int channel_setting_index = -1; // 通道设置在监测设置记录中的顺序索引 + int channel_def_index = -1; // tagChannelDefnIdx,关联的数据源通道定义索引 + + unsigned int trigger_type_id = 0; // tagTriggerTypeID,触发类型 + double full_scale = 0.0; // tagFullScale,满量程 + double noise_floor = 0.0; // tagNoiseFloor,噪声底 + PqdifValueArray trigger_shape_param; // tagTriggerShapeParam,触发曲线/参数数组 + + unsigned int xd_transformer_type_id = 0; // tagXDTransformerTypeID,互感器类型 + double xd_system_side_ratio = 0.0; // tagXDSystemSideRatio,一次侧变比 + double xd_monitor_side_ratio = 0.0; // tagXDMonitorSideRatio,监测侧变比 + PqdifValueArray xd_frequency_response; // tagXDFrequencyResponse,频率响应数组 + + double cal_time_skew = 0.0; // tagCalTimeSkew,校准时延 + double cal_offset = 0.0; // tagCalOffset,校准偏移 + double cal_ratio = 0.0; // tagCalRatio,校准比例 + bool cal_must_use_arcal = false; // tagCalMustUseARCal,是否必须使用校准曲线 + PqdifValueArray cal_applied; // tagCalApplied,校准应用值数组 + PqdifValueArray cal_recorded; // tagCalRecorded,校准记录值数组 + + double trigger_high_high = 0.0; // tagTriggerHighHigh,高高阈值 + double trigger_high = 0.0; // tagTriggerHigh,高阈值 + double trigger_low = 0.0; // tagTriggerLow,低阈值 + double trigger_low_low = 0.0; // tagTriggerLowLow,低低阈值 + double trigger_deadband = 0.0; // tagTriggerDeadband,死区 + double trigger_rate = 0.0; // tagTriggerRate,触发速率 + + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Monitor Settings Record:监测装置配置 +struct PqdifMonitorSettingsRecord +{ + PqdifRecordHeaderInfo header; // 记录头 + int settings_index = -1; // 在文件中解析出的监测设置顺序索引 + long record_index = -1; // 对应文件记录索引,便于关联 Observation + + PqdifTimestampValue effective_time; // tagEffective,配置生效时间 + PqdifTimestampValue time_installed; // tagTimeInstalled,安装时间 + PqdifTimestampValue time_removed; // tagTimeRemoved,拆除时间 + + bool use_calibration = false; // tagUseCalibration,是否使用校准 + bool use_transducer = false; // tagUseTransducer,是否使用传感器/互感器 + double nominal_frequency = 0.0; // tagNominalFrequency,额定频率 + unsigned int physical_connection = 0; // tagSettingPhysicalConnection,物理接线方式 + double nominal_voltage = 0.0; // tagNominalVoltage,额定电压 + bool is_pcc = false; // tagIsPCC,是否为 PCC 点 + + std::vector channel_settings; // 各通道的监测配置 + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Observation 中单个序列实例:保存某一次观测里的一个真实数组 +struct PqdifSeriesInstance +{ + int series_instance_index = -1; // 序列实例在通道实例中的顺序索引 + int series_def_index = -1; // 关联的数据源序列定义索引(通常与定义层顺序一致) + + unsigned int quantity_units_id = 0; // tagQuantityUnitsID,本实例对应的单位 ID + PqdifGuidValue quantity_characteristic_id; // tagQuantityCharacteristicID,本实例特征量 ID + PqdifGuidValue value_type_id; // tagValueTypeID,本实例值类型 ID + + long series_base_type = -1; // 当前库暴露的底层序列物理类型 + double series_base_quantity = 0.0; // tagSeriesBaseQuantity,序列基值 + double scale = 1.0; // tagSeriesScale,缩放系数 + double offset = 0.0; // tagSeriesOffset,偏移量 + double nominal_quantity = 0.0; // 关联定义层解析出的标称量 + unsigned int significant_digits_id = 0; // 关联定义层解析出的有效位配置 + double quantity_resolution = 0.0; // 关联定义层解析出的分辨率 + + int share_channel_index = -1; // tagSeriesShareChannelIdx,共享通道索引 + int share_series_index = -1; // tagSeriesShareSeriesIdx,共享序列索引 + + PqdifValueArray values; // tagSeriesValues,真正的数组值 + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Observation 中单个通道实例:保存某一次观测里的一个真实通道块 +struct PqdifChannelInstance +{ + int channel_instance_index = -1; // 通道实例在观测中的顺序索引 + int channel_def_index = -1; // 关联的数据源通道定义索引 + + std::string channel_name; // 从 Observation 读出的通道名 + unsigned int phase_id = 0; // 相别 + PqdifGuidValue quantity_type_id; // 量类型 + unsigned int quantity_measured_id = 0; // 被测量对象 + int primary_series_index = -1; // 主序列索引 + + double charact_duration = 0.0; // tagCharactDuration,特征持续时间 + double charact_magnitude = 0.0; // tagCharactMagnitude,特征幅值 + double charact_frequency = 0.0; // tagCharactFrequency,特征频率 + double channel_frequency = 0.0; // tagChannelFrequency,通道频率 + int channel_group_id = 0; // tagChannelGroupID,通道分组 + + std::vector series_instances; // 该通道实例下的所有序列实例 + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// Observation Record:一次真实观测 +struct PqdifObservationRecord +{ + PqdifRecordHeaderInfo header; // 记录头 + int observation_index = -1; // 在文件中解析出的观测顺序索引 + long record_index = -1; // 对应文件记录索引 + + int related_data_source_index = -1; // 该观测关联的数据源顺序索引 + long related_data_source_record_index = -1; // 该观测关联的数据源记录索引 + int related_settings_index = -1; // 该观测关联的监测设置顺序索引 + long related_settings_record_index = -1; // 该观测关联的监测设置记录索引 + + std::string observation_name; // tagObservationName,观测名称 + PqdifTimestampValue time_create; // tagTimeCreate,观测记录创建时间 + PqdifTimestampValue time_start; // tagTimeStart,观测起始时间 + + unsigned int trigger_method_id = 0; // tagTriggerMethodID,触发方式 + PqdifTimestampValue time_triggered; // tagTimeTriggered,触发时刻 + std::vector channel_trigger_indexes; // tagChannelTriggerIdx,触发通道索引数组 + + unsigned int observation_serial = 0; // tagObservationSerial,观测序号 + unsigned int observation_aggregation_serial = 0; // tagObservationAggregationSerial,聚合序号 + PqdifGuidValue disturbance_category_id; // tagDisturbanceCategoryID,扰动分类 + + std::vector channel_instances; // 本次观测中的所有通道实例 + PqdifExtraTagMap extra_tags; // 当前结构未单独建模但已读出的标签 +}; + +// 文件级完整逻辑对象 +struct PqdifLogicalFile +{ + std::vector record_headers; // 文件中所有记录的头信息,保留顺序 + std::vector containers; // Container / General 记录,一般只有一个 + std::vector data_sources; // 全部数据源记录 + std::vector monitor_settings; // 全部监测设置记录 + std::vector observations; // 全部观测记录 }; // ============================ -// ϵмǩ +// 统计识别与同一时间聚合层 +// 说明: +// 1) 这一层只服务于“从完整 PQDIF 结构中识别目标统计指标并按时间聚合”; +// 2) 当前阶段先不做 tagPqData_Float 映射; +// 3) 当前阶段只处理“筛选出的统计类 observation”,其他 observation 暂不参与识别。 // ============================ -struct RawSeriesTagMeta -{ - long quantity_units_id = -1; // tagQuantityUnitsID - GUID quantity_characteristic_id{}; // tagQuantityCharacteristicID - GUID value_type_id{}; // tagValueTypeID - long series_base_type = -1; // ǰṩϢ - double series_scale = 1.0; // ϵ - double series_offset = 0.0; // ƫ +// 解析得到的接线方式 +// 用于区分星型 / 角型 两套指标规则。 +enum class ParsedConnectionKind +{ + Unknown = 0, // 未知 / 无法确认 + Wye, // 星型 / Y + Delta // 角型 / Δ }; -// ============================ -// ͨһԭʼͳ -// ǰ׶ֻǩ + ԭʼֵ -// ============================ -struct RawChannelSeries +// 当前支持的业务指标 ID +// 后续如果新增指标,只需要继续往这里加枚举,并在 cpp 里的识别规则补充即可。 +enum class StatMetricId { - RawChannelTagMeta channel_tag; + Unknown = 0, - RawSeriesTagMeta time_meta; - RawSeriesTagMeta max_meta; - RawSeriesTagMeta min_meta; - RawSeriesTagMeta avg_meta; - RawSeriesTagMeta cp95_meta; - RawSeriesTagMeta val_meta; + // 相电压有效值 + UaRms, + UbRms, + UcRms, - std::vector times; - std::vector max_values; - std::vector min_values; - std::vector avg_values; - std::vector cp95_values; - std::vector val_values; + // 相电流有效值 + IaRms, + IbRms, + IcRms, + + // 线电压有效值 + UabRms, + UbcRms, + UcaRms, + + // 相电压偏差 + UaDeviation, + UbDeviation, + UcDeviation, + + // 线电压偏差 + UabDeviation, + UbcDeviation, + UcaDeviation, + + // 频率 + Frequency, + + // 频率偏差 + FrequencyDeviation, + + // 电压序分量 + UZeroSeq, // 电压零序分量 + UNegSeq, // 电压负序分量 + UPosSeq, // 电压正序分量 + + // 电压负序不平衡,通常对应 S2/S1,单位可能为 % 或 pu + UNegSeqUnbalance, + + // 电流序分量 + IZeroSeq, // 电流零序分量 + INegSeq, // 电流负序分量 + IPosSeq, // 电流正序分量 + + // 电流负序不平衡,通常对应 S2/S1,单位可能为 % 或 pu + INegSeqUnbalance, + + + + // 功率类:三相/总有功、无功、视在功率 + PaPower, + PbPower, + PcPower, + PTotalPower, + QaPower, + QbPower, + QcPower, + QTotalPower, + SaPower, + SbPower, + ScPower, + STotalPower, + + // 功率因数类:三相/总功率因数、三相/总基波功率因数 + PFa, + PFb, + PFc, + PFTotal, + FundPFa, + FundPFb, + FundPFc, + FundPFTotal, + + // 电压变动幅值 DVC/dV/V:三相与线电压 + UaDvc, + UbDvc, + UcDvc, + UabDvc, + UbcDvc, + UcaDvc, + + // 闪变:三相和线电压短闪 Pst、长闪 Plt + UaPst, + UbPst, + UcPst, + UabPst, + UbcPst, + UcaPst, + UaPlt, + UbPlt, + UcPlt, + UabPlt, + UbcPlt, + UcaPlt, + + // 基波功率:三相/总有功、无功、视在基波功率 + PaFundPower, + PbFundPower, + PcFundPower, + PTotalFundPower, + QaFundPower, + QbFundPower, + QcFundPower, + QTotalFundPower, + SaFundPower, + SbFundPower, + ScFundPower, + STotalFundPower, + + // 基波有效值与基波相角:三相电压、三相电流、线电压 + UaFundRms, + UbFundRms, + UcFundRms, + UaFundAngle, + UbFundAngle, + UcFundAngle, + IaFundRms, + IbFundRms, + IcFundRms, + IaFundAngle, + IbFundAngle, + IcFundAngle, + UabFundRms, + UbcFundRms, + UcaFundRms, + UabFundAngle, + UbcFundAngle, + UcaFundAngle, + + // 总谐波畸变率 THD:三相电压、三相电流、线电压 + UaThd, + UbThd, + UcThd, + IaThd, + IbThd, + IcThd, + UabThd, + UbcThd, + UcaThd, + + // 谐波/间谐波类动态指标范围。 + // + // 不再为 2-50 次谐波逐项声明 UaHarm02、UaHarm03 ... UcHarm50, + // 而是用“指标族基址 + 相别 + 次数”的方式动态构造。这样后续新增 + // 电流谐波、间谐波、谐波含有率等指标时,不需要继续堆大量 enum 项。 + // + // 当前已启用: + // VoltageHarmonicUaBase + order => UaHarmXX, order=2..50 + // VoltageHarmonicUbBase + order => UbHarmXX, order=2..50 + // VoltageHarmonicUcBase + order => UcHarmXX, order=2..50 + // 预留区间: + // CurrentHarmonic* 后续电流谐波 + // VoltageInterharmonic* 后续电压间谐波 + // CurrentInterharmonic* 后续电流间谐波 + // 三相电压谐波 RMS,order=2..50 + VoltageHarmonicUaBase = 10000, + VoltageHarmonicUbBase = 10100, + VoltageHarmonicUcBase = 10200, + + // AB/BC/CA 线电压谐波 RMS,order=2..50 + LineVoltageHarmonicUabBase = 10300, + LineVoltageHarmonicUbcBase = 10400, + LineVoltageHarmonicUcaBase = 10500, + + // 三相电流谐波 RMS,order=2..50 + CurrentHarmonicIaBase = 11000, + CurrentHarmonicIbBase = 11100, + CurrentHarmonicIcBase = 11200, + + // 三相电压谐波相角,order=2..50 + VoltageHarmonicAngleUaBase = 12000, + VoltageHarmonicAngleUbBase = 12100, + VoltageHarmonicAngleUcBase = 12200, + + // AB/BC/CA 线电压谐波相角,order=2..50 + LineVoltageHarmonicAngleUabBase = 12300, + LineVoltageHarmonicAngleUbcBase = 12400, + LineVoltageHarmonicAngleUcaBase = 12500, + + // 三相电流谐波相角,order=2..50 + CurrentHarmonicAngleIaBase = 13000, + CurrentHarmonicAngleIbBase = 13100, + CurrentHarmonicAngleIcBase = 13200, + + // 三相电压间谐波 RMS,slot=0..49 表示 0.5..49.5 次 + VoltageInterharmonicUaBase = 14000, + VoltageInterharmonicUbBase = 14100, + VoltageInterharmonicUcBase = 14200, + + // AB/BC/CA 线电压间谐波 RMS,slot=0..49 表示 0.5..49.5 次 + LineVoltageInterharmonicUabBase = 14300, + LineVoltageInterharmonicUbcBase = 14400, + LineVoltageInterharmonicUcaBase = 14500, + + // 三相电流间谐波 RMS,slot=0..49 表示 0.5..49.5 次 + CurrentInterharmonicIaBase = 15000, + CurrentInterharmonicIbBase = 15100, + CurrentInterharmonicIcBase = 15200, + + // 谐波功率 2-50 次:三相/总 有功、无功、视在功率 + HarmonicActivePowerPaBase = 16000, + HarmonicActivePowerPbBase = 16100, + HarmonicActivePowerPcBase = 16200, + HarmonicActivePowerTotalBase = 16300, + HarmonicReactivePowerQaBase = 16400, + HarmonicReactivePowerQbBase = 16500, + HarmonicReactivePowerQcBase = 16600, + HarmonicReactivePowerTotalBase = 16700, + HarmonicApparentPowerSaBase = 16800, + HarmonicApparentPowerSbBase = 16900, + HarmonicApparentPowerScBase = 17000, + HarmonicApparentPowerTotalBase = 17100, + + // 谐波含有率 2-50 次:三相电压、三相电流、线电压 + VoltageHarmonicRatioUaBase = 18000, + VoltageHarmonicRatioUbBase = 18100, + VoltageHarmonicRatioUcBase = 18200, + CurrentHarmonicRatioIaBase = 18300, + CurrentHarmonicRatioIbBase = 18400, + CurrentHarmonicRatioIcBase = 18500, + LineVoltageHarmonicRatioUabBase = 18600, + LineVoltageHarmonicRatioUbcBase = 18700, + LineVoltageHarmonicRatioUcaBase = 18800, }; -// ļͨ -using RawChannelMap = std::map; +// 统计值类型:用于把 MIN / MAX / AVG / P95 装进同一时间桶。 +enum class StatValueKind +{ + Unknown = 0, + Min, // 最小值 + Max, // 最大值 + Avg, // 平均值 + P95 // 95 值 / 百分位 95 +}; -// һ PQDIF ļݴ +// 指标质量状态。 +// Normal 表示当前指标可正常使用;其他状态用于把“重复来源/全零/量程异常/缺失”显式暴露出来, +// 防止后续新增指标时继续出现静默覆盖或误识别。 +enum class StatMetricQuality +{ + Normal = 0, + AllZero, + DuplicateSource, + SuspiciousRange, + Missing +}; + +// 单个“已展开的统计样本点” +// 含义:某个统计 observation 中某个通道/某个序列/某个时间点的一条已还原工程值样本。 +struct ExpandedStatPoint +{ + time_t timestamp = 0; // 样本对应的绝对时刻 + std::string timestamp_text; // 格式化后的时间文本,便于日志和调试 + + int observation_index = -1; // 来源 observation 索引 + int channel_instance_index = -1; // 来源通道实例索引 + int channel_def_index = -1; // 来源通道定义索引,便于排查同名/同相通道覆盖 + int channel_group_id = 0; // 来源通道分组 ID;部分厂家用它表示谐波次数 + int channel_spectrum_order_hint = -1; // 当 group/base/nominal 缺失时,从同类通道实例顺序推导出的谐波次数 2..50 + int channel_spectrum_block_offset = -1; // 同类通道实例块内偏移;用于排查厂家把谱线拆成多个通道但名称相同的情况 + int channel_spectrum_block_size = 0; // 同类通道实例候选总数;用于日志确认是否是 2-50 次整组数据 + int series_instance_index = -1; // 来源序列实例索引 + int series_def_index = -1; // 来源序列定义索引,便于核查 MIN/MAX/AVG/P95 的定义来源 + int sample_index = -1; // 序列内部样本点下标 + + std::string observation_name; // 来源 observation 名称 + std::string channel_name; // 通道名称(如 V RMS A) + std::string quantity_name; // 量名称(来自定义层) + unsigned int phase_id = 0; // 相别 + PqdifGuidValue quantity_type_id; // 量类型(VALUELOG 等) + unsigned int quantity_measured_id = 0; // 被测量对象(Voltage / Current) + unsigned int quantity_units_id = 0; // 单位 ID + PqdifGuidValue quantity_characteristic_id;// 特征量(RMS / FREQUENCY) + PqdifGuidValue value_type_id; // 值类型(MIN / MAX / AVG / P95) + + double prob_percentile = 0.0; // 序列定义中的概率百分位(若有) + double series_base_quantity = 0.0; // 序列基值;部分 PQDIF 厂家会用它表达谐波次数 + double nominal_quantity = 0.0; // 标称量;部分 PQDIF 厂家会用它表达谐波次数 + double value = 0.0; // 工程值(已完成 scale/offset 还原) + + ParsedConnectionKind connection_kind = ParsedConnectionKind::Unknown; // 当前文件识别出的接线方式 + StatMetricId metric_id = StatMetricId::Unknown; // 该样本点识别出的业务指标 + StatValueKind stat_kind = StatValueKind::Unknown; // 该样本点属于 MIN/MAX/AVG/P95 哪一种 + + bool matched_by_name_fallback = false; // 是否通过名称辅助规则匹配成功 +}; + +// 某一个业务指标在某个时间点上的统计值集合 +struct AggregatedStatValues +{ + bool has_min = false; // 是否存在最小值 + bool has_max = false; // 是否存在最大值 + bool has_avg = false; // 是否存在平均值 + bool has_p95 = false; // 是否存在 95 值 + + double min_value = 0.0; // 最小值 + double max_value = 0.0; // 最大值 + double avg_value = 0.0; // 平均值 + double p95_value = 0.0; // 95 值 + + StatMetricQuality quality = StatMetricQuality::Normal; // 指标质量状态 + std::string quality_reason; // 质量状态说明 + + int source_observation_index = -1; // 选中的来源 observation 索引 + int source_channel_instance_index = -1; // 选中的来源通道实例索引 + int source_series_instance_index = -1; // 最近一次写入该桶的来源序列实例索引 + std::string source_channel_name; // 选中的来源通道名称 +}; + +// 同一时刻聚合桶 +// 含义:在“已筛选出的统计类 observation”内,某个 timestamp 下所有已识别指标的统计值集合。 +struct TimeAggregatedStatBucket +{ + time_t timestamp = 0; // 聚合时刻 + std::string timestamp_text; // 格式化时间文本 + + std::map metrics; // 当前时刻下各指标的统计值 +}; + + +// PQDIF 统计桶 Base64 子记录对象 +// 对象用途:保存“一个 PQDIF 文件 + 一个时间点 + 一种统计类型”的最终 Base64 组装结果。 +// 数据粒度:例如同一个时间点会有 Max、Min、Avg、P95 四条子记录。 +// 注意:v20 起它不再直接作为全局队列元素,而是作为 +// PqdifStatBase64TimePointPacket.records 的子对象存在。 +struct PqdifStatBase64Record +{ + std::string pqdif_file_path; // 对象来源:PQDIF 文件全路径,便于后续处理时追溯文件 + StatValueKind value_kind = StatValueKind::Unknown; // 对象统计类型:Max / Min / Avg / P95 + std::string value_kind_name; // 对象统计类型文本:便于日志/外部队列直接查看 + int value_kind_code = 0; // 对象统计类型编码:兼容旧统计队列,1=Max, 2=Min, 3=Avg, 4=P95 + time_t timestamp = 0; // 对象时间戳:该 Base64 数据所属的桶时间点 + std::string timestamp_text; // 对象时间文本:格式化时间点,便于日志、人眼核查 + std::string base64_payload; // 对象数据主体:float 按星型/角型顺序组装后整体转换成的 Base64 字符串 + size_t float_count = 0; // 对象校验字段:本条记录实际组装的 float 数量 + size_t placeholder_count = 0; // 对象校验字段:本条记录中使用 3.14159f 占位的数量 + ParsedConnectionKind connection_kind = ParsedConnectionKind::Unknown; // 对象接线方式:星型/角型/未知 +}; + +// PQDIF Base64 时间点包对象 +// 对象用途:把同一个 PQDIF 文件、同一个时间点下的 Max/Min/Avg/P95 四类 Base64 子记录合并保存。 +// 设计原因:后续处理时可以按时间点一次性拿到四种统计类型,避免散落成单条记录。 +struct PqdifStatBase64TimePointPacket +{ + time_t timestamp = 0; // 对象时间戳:该时间点包对应的桶时间点 + std::string timestamp_text; // 对象时间文本:格式化时间点,便于日志、人眼核查 + std::vector records; // 对象子记录:同一时间点下的 Max/Min/Avg/P95 Base64 记录 + + size_t record_count = 0; // 对象统计字段:records.size() 的冗余统计,便于日志核查 + size_t total_float_count = 0; // 对象统计字段:当前时间点下所有子记录的 float 数量合计 + size_t total_placeholder_count = 0;// 对象统计字段:当前时间点下所有子记录的占位数量合计 + size_t total_base64_chars = 0; // 对象统计字段:当前时间点下所有子记录 base64 字符数合计 +}; + +// PQDIF Base64 文件级批次对象 +// 对象用途:保存“一个 PQDIF 文件解析完成后”的全部 Base64 时间点包。 +// 数据粒度:全局队列中的一个元素就是一个 PqdifStatBase64FileBatch,文件之间不会混合。 +struct PqdifStatBase64FileBatch +{ + std::string pqdif_file_path; // 对象来源:PQDIF 文件全路径 + std::string source_file; // 对象来源:原始文件路径,通常与 pqdif_file_path 一致或接近 + std::string mac; // 对象来源:设备目录名/设备标识 + time_t parsed_at = 0; // 对象生成时间戳:解析完成时间 + std::string parsed_at_text; // 对象生成时间文本:格式化解析完成时间 + ParsedConnectionKind connection_kind = ParsedConnectionKind::Unknown; // 对象接线方式:星型/角型/未知 + + std::vector time_points; // 对象主体:文件内所有时间点包 + + size_t time_point_count = 0; // 对象统计字段:time_points.size() 的冗余统计,便于日志核查 + size_t total_record_count = 0; // 对象统计字段:文件内全部 Max/Min/Avg/P95 子记录数量 + size_t total_float_count = 0; // 对象统计字段:文件内全部 float 数量 + size_t total_placeholder_count = 0;// 对象统计字段:文件内全部占位数量 + size_t total_base64_chars = 0; // 对象统计字段:文件内全部 base64 字符数 +}; + +// 一个 PQDIF 文件的暂存对象 struct ParsedPqdifFile { - std::string mac; // 豸Ŀ¼ - std::string source_file; // ԭʼļ· - time_t parsed_at = 0; // ʱ - RawChannelMap channels; // ͨ + std::string mac; // 设备目录名 + std::string source_file; // 原始文件路径 + time_t parsed_at = 0; // 解析完成时间 + + // 新结构:完整 PQDIF 拆分结果 + PqdifLogicalFile logical_file; + + // 统计识别与聚合层结果 + ParsedConnectionKind connection_kind = ParsedConnectionKind::Unknown; ///< 当前文件识别出的接线方式 + int selected_observation_index = -1; ///< 当前阶段被选中的“统计类 observation”索引 + std::string selected_observation_name; ///< 当前阶段被选中的“统计类 observation”名称 + std::vector expanded_stat_points; ///< 从 PQDIF 展开的全部统计样本点 + std::vector aggregated_stat_buckets; ///< 按 timestamp 聚合后的时间桶 }; -// PQDIF ɨ߳ +// 启动 PQDIF 扫描线程 void RunPqdifScanLoop(); -// ʽӿ +// 缓存访问接口 bool PopOldestParsedPqdifFile(ParsedPqdifFile& out); bool PeekOldestParsedPqdifFile(ParsedPqdifFile& out); size_t GetParsedPqdifCacheSize(); -void ClearParsedPqdifCache(); \ No newline at end of file +void ClearParsedPqdifCache(); + +// PQDIF 统计桶 Base64 “生成队列”访问接口 +// 对象用途:该队列由解析流程写入,RunPqdifScanLoop() 每轮循环末尾会尝试把其中一个文件批次 +// 移动到“待后续处理队列”。如果外部仍需直接读取生成队列,可以继续使用以下旧接口。 +// v20 起:队列元素 = 一个 PQDIF 文件批次。 +bool PopOldestPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out); +bool PeekOldestPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out); + +// 兼容旧接口:如旧代码仍按单条 Base64 子记录读取,会从文件批次队列的最早文件/最早时间点中拆出一条。 +bool PopOldestPqdifStatBase64Record(PqdifStatBase64Record& out); +bool PeekOldestPqdifStatBase64Record(PqdifStatBase64Record& out); + +// 返回当前生成队列文件级批次数量;如需查看内部子记录数量,使用 GetPqdifStatBase64RecordCountInQueue()。 +size_t GetPqdifStatBase64QueueSize(); +size_t GetPqdifStatBase64RecordCountInQueue(); +void ClearPqdifStatBase64Queue(); + +// PQDIF 统计桶 Base64 “待后续处理队列”访问接口 +// 对象用途:RunPqdifScanLoop() 已经从生成队列取出、准备交给后续入库/上传/推送流程的数据会放在这里。 +// 推荐后续业务优先调用 PopReadyPqdifStatBase64FileBatch(),一次取出一个完整 PQDIF 文件批次。 +bool PopReadyPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out); +bool PeekReadyPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out); +size_t GetReadyPqdifStatBase64QueueSize(); +size_t GetReadyPqdifStatBase64RecordCountInQueue(); +void ClearReadyPqdifStatBase64Queue();