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