Files
front_linux/LFtid1056/cloudfront/code/cfg_parser.cpp
2025-11-03 15:39:29 +08:00

6742 lines
308 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/////////////////////////////////////////////////////////////////////////////////////////////////
#include <ctime>
#include <unistd.h>
#include <string>
#include <iostream>
#include <list>
#include <sstream> //流解析
#include <set> //写去重的设备类型
#include <fstream> //打开文件
#include <algorithm>
#include <cctype>
#include <dirent.h>
#include <vector>
#include <array>
#include <map>
#include <mutex>
#include <thread>
#include <atomic>
#include <cstdio>
#include <sys/stat.h>
#include <fnmatch.h>
#include <memory>
#include <unordered_set>
/////////////////////////////////////////////////////////////////////////////////////////////////
#include "nlohmann/json.hpp"
#include "curl/curl.h"
#include "log4.h" //关键上送日志
#include "interface.h" //台账结构
#include "tinyxml2.h"
#include "rocketmq.h"
/////////////////////////////////////////////////////////////////////////////////////////////////
using namespace std;
/////////////////////////////////////////////////////////////////////////////////////////////////
//进程标识
extern std::string subdir;
extern int g_front_seg_index;
extern int g_front_seg_num;
extern unsigned int g_node_id; //前置程序类型(100-600)
//初始化完成标识
extern int INITFLAG;
//线程阻塞计数
extern uint32_t g_ontime_blocked_times;
//台账锁
extern std::mutex ledgermtx;
//队列
extern std::mutex queue_data_list_mutex; //queue发送数据锁
extern std::list<queue_data_t> queue_data_list; //queue发送数据链表
extern int three_secs_enabled;
extern std::map<std::string, Xmldata*> xmlinfo_list;//保存所有型号对应的icd映射文件解析数据
extern XmlConfig xmlcfg;//星形接线xml节点解析的数据-默认映射文件解析数据
extern std::list<CTopic *> topicList; //队列发送主题链表
extern XmlConfig xmlcfg2;//角型接线xml节点解析的数据-默认映射文件解析数据
extern std::list<CTopic*> topicList2; //角型接线发送主题链表
extern std::map<std::string, Xmldata*> xmlinfo_list2;//保存所有型号角形接线对应的icd映射文件解析数据
//////////////////////////////////////////////////////////////////////////////////////////////////
extern time_t ConvertToTimestamp(const tagTime& time);
////////////////////////////////////////////////////////////////////////////////////////////////////
//补招记录文件
std::mutex g_recall_file_mtx;
std::map<std::pair<std::string,std::string>, std::string> g_recall_file_index;
//实时数据时间记录
std::mutex g_last_ts_mtx;
std::unordered_map<std::string, time_t> g_last_ts_by_devid;
static std::mutex g_filemenu_cache_mtx;
std::map<std::string, std::vector<tag_dir_info>> g_filemenu_cache;
//补招
std::list<JournalRecall> g_StatisticLackList; //日志补招结构类链表
std::mutex g_StatisticLackList_list_mutex; //补招队列数据锁
std::string DEFAULT_CONFIG_FN = "Default_Config.xml"; //默认映射文件
std::string LEDGER_UPDATE_FN = "LedgerUpdate.log"; //台账更新本地记录日志
//一个终端最大检测点数
const int MAX_CPUNO = 10;
//角型接线标志,0不存在角形接线1存在角形接线
int isdelta_flag = 0;
//多前置flag:1为开启,0为关闭
int MULTIPLE_NODE_FLAG = 1;
//终端台账数量配置
int IED_COUNT = 300; //默认300
//前置标志
std::string FRONT_INST = "";
std::string FRONT_IP = "";
//终端和监测点的状态筛选
std::string TERMINAL_STATUS = "";
std::string MONITOR_STATUS = "";
std::string ICD_FLAG = "";
//web接口
std::string WEB_DEVICE = "";
std::string WEB_ICD = "";
std::string WEB_EVENT = "";
std::string WEB_FILEUPLOAD = "";
std::string WEB_FILEDOWNLOAD = "";
////////////////////////////////////////////////////////////////////////////mq配置
//备用
std::string BROKER_LIST = "";
//通用主题
std::string TOPIC_STAT = "";
std::string TOPIC_PST = "";
std::string TOPIC_PLT = "";
std::string TOPIC_EVENT = "";
std::string TOPIC_ALARM = "";
std::string TOPIC_SNG = "";
std::string TOPIC_RTDATA = "";
//通用tagkey
std::string G_ROCKETMQ_TAG = "";//tag
std::string G_ROCKETMQ_KEY = "";//key
//实时数据tagkey
std::string G_RT_TAG = "";//tag
std::string G_RT_KEY = "";//key
//生产者
std::string G_ROCKETMQ_PRODUCER = ""; //rocketmq producer
std::string G_MQPRODUCER_IPPORT = ""; //rocketmq ip+port
std::string G_MQPRODUCER_ACCESSKEY = ""; //rocketmq 认证
std::string G_MQPRODUCER_SECRETKEY = ""; //rocketmq 秘钥
//日志
std::string G_LOG_TOPIC = "";//topie
std::string G_LOG_TAG = "";//tag
std::string G_LOG_KEY = "";//key
//终端连接
std::string G_CONNECT_TOPIC = "";//consumer topie
std::string G_CONNECT_TAG = "";//consumer tag
std::string G_CONNECT_KEY = "";//consumer key
//心跳
std::string Heart_Beat_Topic = "";
std::string Heart_Beat_Tag = "";
std::string Heart_Beat_Key = "";
//消息响应
std::string Topic_Reply_Topic = "";
std::string Topic_Reply_Tag = "";
std::string Topic_Reply_Key = "";
//消费者
std::string G_ROCKETMQ_CONSUMER = "";//rocketmq consumer
std::string G_MQCONSUMER_IPPORT = "";//consumer ip+port
std::string G_MQCONSUMER_ACCESSKEY = "";
std::string G_MQCONSUMER_SECRETKEY = "";
std::string G_MQCONSUMER_CHANNEL = "";
//实时数据请求
std::string G_MQCONSUMER_TOPIC_RT = "";//consumer topie
std::string G_MQCONSUMER_TAG_RT = "";//consumer tag
std::string G_MQCONSUMER_KEY_RT = "";//consumer key
//台账更新请求
std::string G_MQCONSUMER_TOPIC_UD = "";//consumer topie
std::string G_MQCONSUMER_TAG_UD = "";//consumer tag
std::string G_MQCONSUMER_KEY_UD = "";//consumer key
//补招数据请求
std::string G_MQCONSUMER_TOPIC_RC = "";//consumer topie
std::string G_MQCONSUMER_TAG_RC = "";//consumer tag
std::string G_MQCONSUMER_KEY_RC = "";//consumer key
//进程控制请求
std::string G_MQCONSUMER_TOPIC_SET = "";//consumer topie
std::string G_MQCONSUMER_TAG_SET = "";//consumer tag
std::string G_MQCONSUMER_KEY_SET = "";//consumer key
//日志数据请求
std::string G_MQCONSUMER_TOPIC_LOG = "";//consumer topie
std::string G_MQCONSUMER_TAG_LOG = "";//consumer tag
std::string G_MQCONSUMER_KEY_LOG = "";//consumer key
std::string G_MQCONSUMER_TOPIC_CLOUD = "";//consumer topie
std::string G_MQCONSUMER_TAG_CLOUD = "";//consumer tag
std::string G_MQCONSUMER_KEY_CLOUD = "";//consumer key
//测试用的主题
std::string G_ROCKETMQ_TOPIC_TEST = "";//topie
std::string G_ROCKETMQ_TAG_TEST = "";//tag
std::string G_ROCKETMQ_KEY_TEST = "";//key
//测试相关配置
int G_TEST_FLAG = 0;
int G_TEST_NUM = 0;
int G_TEST_TYPE = 0;
int LEDGER_MAX_ITEMS = 5; //台账打印最大项数限制
int TEST_PORT = 11000; //用于当前进程登录测试shell的端口
std::string G_TEST_LIST = ""; //测试用的发送实际数据的终端列表
std::vector<std::string> TESTARRAY; //解析的列表数组
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////其他文件定义的函数引用声明
bool enqueue_direct_download(const std::string& dev_id,
const std::string& monitor_id,
const std::string& filename,
const std::string& guid);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////当前文件函数声明
void create_ledger_log(trigger_update_xml_t* ledger_update_xml);
void print_trigger_update_xml(const trigger_update_xml_t& trigger_update);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////辅助函数
//去除字符串前后空格
static void trim(std::string& s) {
auto not_space = [](int ch) { return !std::isspace(ch); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space));
s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end());
}
//判断空格
bool is_blank(const std::string& str)
{
for (std::string::const_iterator it = str.begin(); it != str.end(); ++it)
{
if (!std::isspace(*it)) {
return false;
}
}
return true;
}
// 获取本地当天 00:00:00 的毫秒时间戳
static inline uint64_t get_local_midnight_ms_today() {
std::time_t now = std::time(nullptr);
std::tm local_tm{};
#if defined(_WIN32) || defined(_WIN64)
localtime_s(&local_tm, &now);
#else
local_tm = *std::localtime(&now);
#endif
local_tm.tm_hour = 0;
local_tm.tm_min = 0;
local_tm.tm_sec = 0;
std::time_t midnight_sec = std::mktime(&local_tm); // 本地时区
return static_cast<uint64_t>(midnight_sec) * 1000ULL;
}
// 获取“本地前一天 00:00:00”的毫秒时间戳
static inline uint64_t get_local_midnight_ms_prev_day() {
uint64_t today_ms = get_local_midnight_ms_today();
const uint64_t one_day_ms = 24ULL * 60ULL * 60ULL * 1000ULL;
return today_ms - one_day_ms;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////配置文件读取
//将 G_TEST_LIST 按逗号分割,填充 TESTARRAY
static void parseTestList() {
TESTARRAY.clear();
std::istringstream iss(G_TEST_LIST);
std::string token;
while (std::getline(iss, token, ',')) {
trim(token);
if (!token.empty()) {
TESTARRAY.push_back(token);
}
}
}
//简易映射方式解析配置文件文件
void loadConfig(const std::string& filename) {
// 1. 构造“节.键 → 变量地址”映射表
std::unordered_map<std::string, std::string*> strMap;
std::unordered_map<std::string, int*> intMap;
// [Flag]
strMap["Flag.FrontInst"] = &FRONT_INST;
strMap["Flag.FrontIP"] = &FRONT_IP;
// [Ledger]
intMap["Ledger.IedCount"] = &IED_COUNT;
strMap["Ledger.TerminalStatus"]= &TERMINAL_STATUS;
strMap["Ledger.MonitorStatus"] = &MONITOR_STATUS;
strMap["Ledger.IcdFlag"] = &ICD_FLAG;
// [Http]
strMap["Http.WebDevice"] = &WEB_DEVICE;
strMap["Http.WebIcd"] = &WEB_ICD;
strMap["Http.WebEvent"] = &WEB_EVENT;
strMap["Http.WebFileupload"] = &WEB_FILEUPLOAD;
strMap["Http.WebFiledownload"]= &WEB_FILEDOWNLOAD;
// [Queue]
strMap["Queue.BrokerList"] = &BROKER_LIST;
strMap["Queue.RTDataTopic"] = &TOPIC_RTDATA;
strMap["Queue.HisTopic"] = &TOPIC_STAT;
strMap["Queue.PSTTopic"] = &TOPIC_PST;
strMap["Queue.PLTTopic"] = &TOPIC_PLT;
strMap["Queue.EventTopic"] = &TOPIC_EVENT;
strMap["Queue.AlmTopic"] = &TOPIC_ALARM;
strMap["Queue.SngTopic"] = &TOPIC_SNG;
strMap["Queue.QUEUE_TAG"] = &G_ROCKETMQ_TAG;
strMap["Queue.QUEUE_KEY"] = &G_ROCKETMQ_KEY;
//添加rt的tagkey
strMap["Queue.RT_TAG"] = &G_RT_TAG;
strMap["Queue.RT_KEY"] = &G_RT_KEY;
// [RocketMq] —— 生产者
strMap["RocketMq.producer"] = &G_ROCKETMQ_PRODUCER;
strMap["RocketMq.Ipport"] = &G_MQPRODUCER_IPPORT;
strMap["RocketMq.AccessKey"] = &G_MQPRODUCER_ACCESSKEY;
strMap["RocketMq.SecretKey"] = &G_MQPRODUCER_SECRETKEY;
strMap["RocketMq.LOGTopic"] = &G_LOG_TOPIC;
strMap["RocketMq.LOGTag"] = &G_LOG_TAG;
strMap["RocketMq.LOGKey"] = &G_LOG_KEY;
strMap["RocketMq.CONNECTTopic"] = &G_CONNECT_TOPIC;
strMap["RocketMq.CONNECTTag"] = &G_CONNECT_TAG;
strMap["RocketMq.CONNECTKey"] = &G_CONNECT_KEY;
strMap["RocketMq.Heart_Beat_Topic"] = &Heart_Beat_Topic;
strMap["RocketMq.Heart_Beat_Tag"] = &Heart_Beat_Tag;
strMap["RocketMq.Heart_Beat_Key"] = &Heart_Beat_Key;
strMap["RocketMq.Topic_Reply_Topic"] = &Topic_Reply_Topic;
strMap["RocketMq.Topic_Reply_Tag"] = &Topic_Reply_Tag;
strMap["RocketMq.Topic_Reply_Key"] = &Topic_Reply_Key;
// [RocketMq] —— 消费者
strMap["RocketMq.consumer"] = &G_ROCKETMQ_CONSUMER;
strMap["RocketMq.ConsumerIpport"] = &G_MQCONSUMER_IPPORT;
strMap["RocketMq.ConsumerAccessKey"] = &G_MQCONSUMER_ACCESSKEY;
strMap["RocketMq.ConsumerSecretKey"] = &G_MQCONSUMER_SECRETKEY;
strMap["RocketMq.ConsumerChannel"] = &G_MQCONSUMER_CHANNEL;
strMap["RocketMq.ConsumerTopicRT"] = &G_MQCONSUMER_TOPIC_RT;
strMap["RocketMq.ConsumerTagRT"] = &G_MQCONSUMER_TAG_RT;
strMap["RocketMq.ConsumerKeyRT"] = &G_MQCONSUMER_KEY_RT;
strMap["RocketMq.ConsumerTopicUD"] = &G_MQCONSUMER_TOPIC_UD;
strMap["RocketMq.ConsumerTagUD"] = &G_MQCONSUMER_TAG_UD;
strMap["RocketMq.ConsumerKeyUD"] = &G_MQCONSUMER_KEY_UD;
strMap["RocketMq.ConsumerTopicRC"] = &G_MQCONSUMER_TOPIC_RC;
strMap["RocketMq.ConsumerTagRC"] = &G_MQCONSUMER_TAG_RC;
strMap["RocketMq.ConsumerKeyRC"] = &G_MQCONSUMER_KEY_RC;
strMap["RocketMq.ConsumerTopicSET"] = &G_MQCONSUMER_TOPIC_SET;
strMap["RocketMq.ConsumerTagSET"] = &G_MQCONSUMER_TAG_SET;
strMap["RocketMq.ConsumerKeySET"] = &G_MQCONSUMER_KEY_SET;
strMap["RocketMq.ConsumerTopicLOG"] = &G_MQCONSUMER_TOPIC_LOG;
strMap["RocketMq.ConsumerTagLOG"] = &G_MQCONSUMER_TAG_LOG;
strMap["RocketMq.ConsumerKeyLOG"] = &G_MQCONSUMER_KEY_LOG;
strMap["RocketMq.ConsumerTopicCLOUD"] = &G_MQCONSUMER_TOPIC_CLOUD;
strMap["RocketMq.ConsumerTagCLOUD"] = &G_MQCONSUMER_TAG_CLOUD;
strMap["RocketMq.ConsumerKeyCLOUD"] = &G_MQCONSUMER_KEY_CLOUD;
strMap["RocketMq.Topic_Test"] = &G_ROCKETMQ_TOPIC_TEST;
strMap["RocketMq.Tag_Test"] = &G_ROCKETMQ_TAG_TEST;
strMap["RocketMq.Key_Test"] = &G_ROCKETMQ_KEY_TEST;
intMap["RocketMq.Testflag"] = &G_TEST_FLAG;
intMap["RocketMq.Testnum"] = &G_TEST_NUM;
intMap["RocketMq.Testtype"] = &G_TEST_TYPE;
intMap["RocketMq.TestPort"] = &TEST_PORT;
strMap["RocketMq.TestList"] = &G_TEST_LIST;
// 2. 打开并逐行解析 INI 文件
std::ifstream fin(filename);
if (!fin.is_open()) {
std::cerr << "无法打开配置文件: " << filename << "\n";
return;
}
std::string line;
std::string currentSection;
while (std::getline(fin, line)) {
trim(line);
if (line.empty() || line[0] == ';' || line[0] == '#') {
continue; // 跳过空白或注释
}
if (line.front() == '[' && line.back() == ']') {
currentSection = line.substr(1, line.size() - 2);
trim(currentSection);
continue;
}
auto pos = line.find('=');
if (pos == std::string::npos) {
continue;
}
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
trim(key);
trim(value);
// 去掉值两端双引号
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
value = value.substr(1, value.size() - 2);
}
// 拼出 "节.键"
std::string fullKey = currentSection + "." + key;
// 如果在字符串映射表里,就写入对应的全局 std::string
auto sit = strMap.find(fullKey);
if (sit != strMap.end()) {
*(sit->second) = value;
continue;
}
// 如果在整型映射表里,就 stoi 后写入对应的全局 int
auto iit = intMap.find(fullKey);
if (iit != intMap.end()) {
try {
*(iit->second) = std::stoi(value);
} catch (...) {
*(iit->second) = 0;
}
}
}
fin.close();
// 3. 将 G_TEST_LIST 拆分到 TESTARRAY
parseTestList();
}
//打印所有全局变量,名称对齐
void printConfig() {
const int nameWidth = 30; // 变量名区域宽度
std::cout << "------- Loaded Configuration -------\n";
// 辅助 lambda 方便打印
auto printStr = [&](const std::string& name, const std::string& val) {
std::cout << std::left << std::setw(nameWidth) << name
<< " = " << val << "\n";
};
auto printInt = [&](const std::string& name, int val) {
std::cout << std::left << std::setw(nameWidth) << name
<< " = " << val << "\n";
};
std::cout << "\n// 前置区分 —— 通用\n";
printInt("IED_COUNT", IED_COUNT);
printStr("FRONT_INST", FRONT_INST);
printStr("FRONT_IP", FRONT_IP);
std::cout << "\n// 消息队列 —— 通用\n";
printStr("BROKER_LIST", BROKER_LIST);
printStr("TOPIC_STAT", TOPIC_STAT);
printStr("TOPIC_PST", TOPIC_PST);
printStr("TOPIC_PLT", TOPIC_PLT);
printStr("TOPIC_EVENT", TOPIC_EVENT);
printStr("TOPIC_ALARM", TOPIC_ALARM);
printStr("TOPIC_SNG", TOPIC_SNG);
printStr("TOPIC_RTDATA", TOPIC_RTDATA);
std::cout << "\n// MQ —— 生产者\n";
printStr("G_ROCKETMQ_PRODUCER", G_ROCKETMQ_PRODUCER);
printStr("G_MQPRODUCER_IPPORT", G_MQPRODUCER_IPPORT);
printStr("G_MQPRODUCER_ACCESSKEY", G_MQPRODUCER_ACCESSKEY);
printStr("G_MQPRODUCER_SECRETKEY", G_MQPRODUCER_SECRETKEY);
printStr("G_LOG_TOPIC", G_LOG_TOPIC);
printStr("G_LOG_TAG", G_LOG_TAG);
printStr("G_LOG_KEY", G_LOG_KEY);
printStr("G_CONNECT_TOPIC", G_CONNECT_TOPIC);
printStr("G_CONNECT_TAG", G_CONNECT_TAG);
printStr("G_CONNECT_KEY", G_CONNECT_KEY);
std::cout << "\n// MQ —— 心跳 & 响应\n";
printStr("Heart_Beat_Topic", Heart_Beat_Topic);
printStr("Heart_Beat_Tag", Heart_Beat_Tag);
printStr("Heart_Beat_Key", Heart_Beat_Key);
printStr("Topic_Reply_Topic", Topic_Reply_Topic);
printStr("Topic_Reply_Tag", Topic_Reply_Tag);
printStr("Topic_Reply_Key", Topic_Reply_Key);
std::cout << "\n// MQ —— 消费者\n";
printStr("G_ROCKETMQ_CONSUMER", G_ROCKETMQ_CONSUMER);
printStr("G_MQCONSUMER_IPPORT", G_MQCONSUMER_IPPORT);
printStr("G_MQCONSUMER_ACCESSKEY", G_MQCONSUMER_ACCESSKEY);
printStr("G_MQCONSUMER_SECRETKEY", G_MQCONSUMER_SECRETKEY);
printStr("G_MQCONSUMER_CHANNEL", G_MQCONSUMER_CHANNEL);
std::cout << "\n// MQ —— 主题细分类\n";
printStr("G_MQCONSUMER_TOPIC_RT", G_MQCONSUMER_TOPIC_RT);
printStr("G_MQCONSUMER_TAG_RT", G_MQCONSUMER_TAG_RT);
printStr("G_MQCONSUMER_KEY_RT", G_MQCONSUMER_KEY_RT);
printStr("G_MQCONSUMER_TOPIC_UD", G_MQCONSUMER_TOPIC_UD);
printStr("G_MQCONSUMER_TAG_UD", G_MQCONSUMER_TAG_UD);
printStr("G_MQCONSUMER_KEY_UD", G_MQCONSUMER_KEY_UD);
printStr("G_MQCONSUMER_TOPIC_RC", G_MQCONSUMER_TOPIC_RC);
printStr("G_MQCONSUMER_TAG_RC", G_MQCONSUMER_TAG_RC);
printStr("G_MQCONSUMER_KEY_RC", G_MQCONSUMER_KEY_RC);
printStr("G_MQCONSUMER_TOPIC_SET", G_MQCONSUMER_TOPIC_SET);
printStr("G_MQCONSUMER_TAG_SET", G_MQCONSUMER_TAG_SET);
printStr("G_MQCONSUMER_KEY_SET", G_MQCONSUMER_KEY_SET);
printStr("G_MQCONSUMER_TOPIC_LOG", G_MQCONSUMER_TOPIC_LOG);
printStr("G_MQCONSUMER_TAG_LOG", G_MQCONSUMER_TAG_LOG);
printStr("G_MQCONSUMER_KEY_LOG", G_MQCONSUMER_KEY_LOG);
std::cout << "\n// MQ —— 测试用主题 & 参数\n";
printStr("G_ROCKETMQ_TOPIC_TEST", G_ROCKETMQ_TOPIC_TEST);
printStr("G_ROCKETMQ_TAG_TEST", G_ROCKETMQ_TAG_TEST);
printStr("G_ROCKETMQ_KEY_TEST", G_ROCKETMQ_KEY_TEST);
printInt("G_TEST_FLAG", G_TEST_FLAG);
printInt("G_TEST_NUM", G_TEST_NUM);
printInt("G_TEST_TYPE", G_TEST_TYPE);
printInt("TEST_PORT", TEST_PORT);
printStr("G_TEST_LIST", G_TEST_LIST);
// 打印解析后的 TESTARRAY
std::cout << std::left << std::setw(nameWidth) << "TESTARRAY" << " = [";
for (size_t i = 0; i < TESTARRAY.size(); ++i) {
std::cout << TESTARRAY[i];
if (i + 1 < TESTARRAY.size()) {
std::cout << ", ";
}
}
std::cout << "]\n";
std::cout << "\n// 终端 & 监测点状态筛选\n";
printStr("TERMINAL_STATUS", TERMINAL_STATUS);
printStr("MONITOR_STATUS", MONITOR_STATUS);
printStr("ICD_FLAG", ICD_FLAG);
std::cout << "\n// Web 接口\n";
printStr("WEB_DEVICE", WEB_DEVICE);
printStr("WEB_ICD", WEB_ICD);
printStr("WEB_EVENT", WEB_EVENT);
printStr("WEB_FILEUPLOAD", WEB_FILEUPLOAD);
printStr("WEB_FILEDOWNLOAD", WEB_FILEDOWNLOAD);
std::cout << "-------------------------------------\n";
}
//初始化配置
void init_config() {
loadConfig(FRONT_PATH + "/config/front.cfg");
printConfig();
//多前置处理
if (g_front_seg_index > 0 && g_front_seg_num > 0) {
MULTIPLE_NODE_FLAG = 1;
std::cout << "this is multiple process of index:" << g_front_seg_index << std::endl;
if(g_front_seg_index > g_front_seg_num){
DIY_ERRORLOG("process","【ERROR】前置当前进程的进程号为:%d,前置的多进程最大进程号为:%d,当前进程的进程号应该为1到最大进程号范围内的整数,退出该进程",g_front_seg_index,g_front_seg_num);
exit(-1039);
}
}
else if(g_front_seg_num == 0 && g_front_seg_index == 0){
MULTIPLE_NODE_FLAG = 0;
std::cout << "this is single process" << std::endl;
}
else{
DIY_ERRORLOG("process","【ERROR】前置当前进程的进程号为:%d,前置的多进程最大进程号为:%d,应该为大于0的整数,退出该进程",g_front_seg_index,g_front_seg_num);
exit(-1039);
}
//测试进程端口
/*if (g_node_id == STAT_DATA_BASE_NODE_ID)//统计采集
TEST_PORT = TEST_PORT + STAT_DATA_BASE_NODE_ID + g_front_seg_index;
else if (g_node_id == RECALL_HIS_DATA_BASE_NODE_ID) {//补召
TEST_PORT = TEST_PORT + RECALL_HIS_DATA_BASE_NODE_ID + g_front_seg_index;
}
else if (g_node_id == THREE_SECS_DATA_BASE_NODE_ID) {//3秒采集
TEST_PORT = TEST_PORT + THREE_SECS_DATA_BASE_NODE_ID + g_front_seg_index;
}
else if (g_node_id == SOE_COMTRADE_BASE_NODE_ID) {//暂态录波
TEST_PORT = TEST_PORT + SOE_COMTRADE_BASE_NODE_ID + g_front_seg_index;
}*/
TEST_PORT = TEST_PORT + g_front_seg_index;
}
////////////////////////////////////////////////////////////////////////////////////////////获取当前时间
// 用于获取当前时间,单位毫秒
double sGetMsTime() {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
).count();
return static_cast<double>(ms);
}
//秒时间转为标准时间字符串
// ★新增:将 epoch 秒级时间转成 "YYYY-MM-DD HH:MM:SS"
static std::string epoch_to_datetime_str(long long epoch_sec) {
char buf[20];
std::tm tm{};
time_t t = static_cast<time_t>(epoch_sec);
localtime_r(&t, &tm);
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
return std::string(buf);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////实时触发部分
//获取实时触发文件
std::string get_3s_trig_fn() {
const std::string dirPath = FRONT_PATH + "/etc/trigger3s";
DIR* dp = opendir(dirPath.c_str());
if (!dp) return "";
struct dirent* entry;
std::vector<std::pair<std::string, time_t>> xmlFiles;
while ((entry = readdir(dp)) != nullptr) {
if (strstr(entry->d_name, ".xml")) {
std::string fullPath = dirPath + "/" + entry->d_name;
struct stat st;
if (stat(fullPath.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
xmlFiles.emplace_back(entry->d_name, st.st_mtime);
}
}
}
closedir(dp);
if (xmlFiles.empty()) return "";
std::sort(xmlFiles.begin(), xmlFiles.end(),
[](const std::pair<std::string, time_t>& a, const std::pair<std::string, time_t>& b) {
return a.second > b.second; // 最近时间在前
});
return xmlFiles.front().first;
}
// 打印 trigger_t 结构体的函数
void print_trigger(const trigger_t& trigger) {
std::cout << " dev_idx: " << trigger.dev_idx
<< ", line_id: " << trigger.line_id
<< ", real_data: " << trigger.real_data
<< ", soe_data: " << trigger.soe_data
<< ", limit: " << trigger.limit
<< ", count: " << trigger.count
<< std::endl;
}
// 打印 trigger_3s_xml_t 结构体的函数
void print_trigger_3s_xml(const trigger_3s_xml_t& trigger_3s_xml) {
std::cout << "Work Trigger Count: " << trigger_3s_xml.work_trigger_num << std::endl;
for (int i = 0; i < trigger_3s_xml.work_trigger_num; ++i) {
std::cout << " Work Trigger [" << (i + 1) << "]:" << std::endl;
print_trigger(trigger_3s_xml.work_triggers[i]);
}
std::cout << "New Trigger Count: " << trigger_3s_xml.new_trigger_num << std::endl;
for (int i = 0; i < trigger_3s_xml.new_trigger_num; ++i) {
std::cout << " New Trigger [" << (i + 1) << "]:" << std::endl;
print_trigger(trigger_3s_xml.new_triggers[i]);
}
std::cout << "Delete Trigger Count: " << trigger_3s_xml.delete_trigger_num << std::endl;
for (int i = 0; i < trigger_3s_xml.delete_trigger_num; ++i) {
std::cout << " Delete Trigger [" << (i + 1) << "]:" << std::endl;
print_trigger(trigger_3s_xml.delete_triggers[i]);
}
std::cout << "Modify Trigger Count: " << trigger_3s_xml.modify_trigger_num << std::endl;
for (int i = 0; i < trigger_3s_xml.modify_trigger_num; ++i) {
std::cout << " Modify Trigger [" << (i + 1) << "]:" << std::endl;
print_trigger(trigger_3s_xml.modify_triggers[i]);
}
}
//处理触发标志
int getValueFromElemAttrStr(std::string str)
{
if (str == "true")
return 1;
else if (str == "false")
return 0;
else
return -1;
}
//将文件内容读取到结构中
void parse_3s_trigger(trigger_3s_xml_t* trigger_3s_xml, const std::string& parentTag, tinyxml2::XMLElement* trigger_e)
{
if (!trigger_3s_xml || !trigger_e) return;
trigger_t trigger;
const char* attr = nullptr;
// DevSeries
attr = trigger_e->Attribute("DevSeries");
trigger.dev_idx = attr ? std::atoi(attr) : 0;
// Line
attr = trigger_e->Attribute("Line");
trigger.line_id = attr ? std::atoi(attr) : 0;
// RealData
attr = trigger_e->Attribute("RealData");
std::string realDataStr = attr ? attr : "";
std::transform(realDataStr.begin(), realDataStr.end(), realDataStr.begin(), ::tolower);
trigger.real_data = getValueFromElemAttrStr(realDataStr);
// SOEData
attr = trigger_e->Attribute("SOEData");
std::string soeDataStr = attr ? attr : "";
std::transform(soeDataStr.begin(), soeDataStr.end(), soeDataStr.begin(), ::tolower);
trigger.soe_data = getValueFromElemAttrStr(soeDataStr);
// Limit
attr = trigger_e->Attribute("Limit");
trigger.limit = attr ? std::atoi(attr) : 0;
// Count
attr = trigger_e->Attribute("Count");
trigger.count = attr ? std::atoi(attr) : 0;
// 分类插入
if (parentTag == "Work") {
trigger_3s_xml->work_triggers[trigger_3s_xml->work_trigger_num++] = trigger;
} else if (parentTag == "New") {
trigger_3s_xml->new_triggers[trigger_3s_xml->new_trigger_num++] = trigger;
} else if (parentTag == "Delete") {
trigger_3s_xml->delete_triggers[trigger_3s_xml->delete_trigger_num++] = trigger;
} else if (parentTag == "Modify") {
trigger_3s_xml->modify_triggers[trigger_3s_xml->modify_trigger_num++] = trigger;
}
// 调试用
print_trigger_3s_xml(*trigger_3s_xml);
}
//实时触发文件处理
int load_3s_data_from_xml(trigger_3s_xml_t* trigger_3s_xml, const std::string& xml_fn) {
if (!trigger_3s_xml) return 1;
tinyxml2::XMLDocument doc;
tinyxml2::XMLError result = doc.LoadFile(xml_fn.c_str());
if (result == tinyxml2::XML_ERROR_FILE_NOT_FOUND) {
return 1;
} else if (result != tinyxml2::XML_SUCCESS) {
return 1;
}
tinyxml2::XMLElement* root = doc.RootElement();
if (!root) return 1;
for (tinyxml2::XMLElement* groupElem = root->FirstChildElement(); groupElem != nullptr; groupElem = groupElem->NextSiblingElement()) {
std::string strTag = groupElem->Name();
if (strTag == "Work" || strTag == "New" || strTag == "Delete" || strTag == "Modify") {
for (tinyxml2::XMLElement* triggerElem = groupElem->FirstChildElement("Trigger"); triggerElem != nullptr; triggerElem = triggerElem->NextSiblingElement("Trigger")) {
parse_3s_trigger(trigger_3s_xml, strTag, triggerElem);
}
}
}
return 0;
}
//将内容写入协议
void process_3s_config(trigger_3s_xml_t *trigger_3s_xml)
{
//根据协议补充内容
//根据协议补充内容
}
//实时触发处理
int parse_3s_xml(trigger_3s_xml_t* trigger_3s_xml) {
std::cout << "begin 3s xml..." << std::endl;
std::memset(trigger_3s_xml, 0, sizeof(trigger_3s_xml_t));
std::string THREE_SECS_WEBSERVICE_DIR = FRONT_PATH + "/etc/trigger3s/"; //实时数据读取目录
std::string BAK_WEBSERVICE_3S_TRIG_COMMAND_XML_FN = THREE_SECS_WEBSERVICE_DIR + "bak_3s_trig_command.txt"; //实时触发文件备份文件名
std::string the_webservice_xml_fn = get_3s_trig_fn(); // 获取目录下最新 XML 文件名
if (the_webservice_xml_fn.length() > 4) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等待 0.1 秒
std::string full_path = std::string(THREE_SECS_WEBSERVICE_DIR) + the_webservice_xml_fn;
if (load_3s_data_from_xml(trigger_3s_xml, full_path) != 0) {
std::cerr << "Failed to load 3s data from XML: " << full_path << std::endl;
return 1;
}
std::remove(BAK_WEBSERVICE_3S_TRIG_COMMAND_XML_FN.c_str()); // 删除旧备份
if (std::rename(full_path.c_str(), BAK_WEBSERVICE_3S_TRIG_COMMAND_XML_FN.c_str()) != 0) {
std::perror("Rename failed");
}
std::cout << "/etc/trigger3s/*.xml success..." << std::endl;
DIY_WARNLOG("process", "【WARN】前置读取实时数据触发文件成功,即将注册实时数据报告");
return 0;
}
std::cout << "3s xml fail..." << std::endl;
return 1;
}
void check_3s_config() {
double now;
static double last_check_3s_config_time = 0.0; // 初始化时间
trigger_3s_xml_t trigger_3s_xml; // 3s触发文件
if (!three_secs_enabled) // 只有cfg_3s_data进程才会开启
return;
now = sGetMsTime(); // 当前时间
if (std::fabs(now - last_check_3s_config_time) < 3 * 1000) // wait 3secs
return; // 当前进程任务执行时查看当前时间和上次执行时间间隔小于3秒不执行大于等于3秒往下执行
last_check_3s_config_time = now; // 记录本次运行时间
while (0 == parse_3s_xml(&trigger_3s_xml)) { // 处理3秒文件一次处理一个
process_3s_config(&trigger_3s_xml); // 根据文件处理数据
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////补招文件部分
//将文件内容读取到结构中
void parse_recall(recall_xml_t* recall_xml, const std::string& parentTag, const std::map<std::string, std::string>& attributes, const std::string& id) {
recall_t recall;
std::memset(&recall, 0, sizeof(recall_t));
// 设置监测点 ID
recall.line_id = id;
// 解析时间字符串
auto parse_time = [](const std::string& time_str) -> std::time_t {
std::tm tm = {};
std::istringstream ss(time_str);
ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
if (ss.fail()) return 0;
return std::mktime(&tm);
};
recall.start_time = parse_time(attributes.at("StartTime"));
recall.end_time = parse_time(attributes.at("EndTime"));
recall.need_steady = std::stoi(attributes.at("STEADY"));
recall.need_voltage = std::stoi(attributes.at("VOLTAGE"));
std::cout << parentTag << " -> " << recall.line_id
<< " " << recall.need_steady
<< " " << recall.need_voltage
<< " " << recall.start_time
<< " " << recall.end_time << std::endl;
if (parentTag == "Work" && recall_xml->work_recall_num < MAX_RECALL_NUM) {
recall_xml->work_recalls[recall_xml->work_recall_num++] = recall;
} else if (parentTag == "New" && recall_xml->new_recall_num < MAX_RECALL_NUM) {
recall_xml->new_recalls[recall_xml->new_recall_num++] = recall;
}
}
//读取补招文件
int parse_recall_xml(recall_xml_t* recall_xml, const std::string& id) {
std::string cfg_dir = FRONT_PATH + "/etc/recall";
std::string pattern = subdir + "_" + std::to_string(g_front_seg_index) + "_" + id + "_*_Recall.xml";
DIR* dir = opendir(cfg_dir.c_str());
if (!dir) {
DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 无法解析补招文件补招文件路径FRONT_PATH + /etc/recall/不存在", g_front_seg_index);
return false;
}
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
std::string filename = entry->d_name;
if (fnmatch(pattern.c_str(), filename.c_str(), 0) != 0) continue;
std::string filepath = cfg_dir + "/" + filename;
tinyxml2::XMLDocument doc;
if (doc.LoadFile(filepath.c_str()) != tinyxml2::XML_SUCCESS) {
DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 无法解析补招文件%s,补招内容无效", g_front_seg_index, filepath.c_str());
continue;
}
tinyxml2::XMLElement* root = doc.RootElement();
if (!root) continue;
for (tinyxml2::XMLElement* elem = root->FirstChildElement(); elem != nullptr; elem = elem->NextSiblingElement()) {
std::string tag = elem->Name();
if (tag == "Work" || tag == "New") {
for (tinyxml2::XMLElement* recallElem = elem->FirstChildElement("Recall"); recallElem != nullptr; recallElem = recallElem->NextSiblingElement("Recall")) {
std::map<std::string, std::string> attrs;
const char* start = recallElem->Attribute("StartTime");
const char* end = recallElem->Attribute("EndTime");
const char* steady = recallElem->Attribute("STEADY");
const char* voltage = recallElem->Attribute("VOLTAGE");
if (start && end && steady && voltage) {
attrs["StartTime"] = start;
attrs["EndTime"] = end;
attrs["STEADY"] = steady;
attrs["VOLTAGE"] = voltage;
parse_recall(recall_xml, tag, attrs, id);
}
}
}
}
}
closedir(dir);
return 0;
}
//将读取到的补招文件写入到监测点的运行结构中,后续根据实际补充
void process_recall_config(recall_xml_t* recall_xml)
{
}
//根据监测点id来获取补招数据补招时调用这个
void Check_Recall_Config(const std::string& id) {
/*if (g_node_id == HIS_DATA_BASE_NODE_ID ||
g_node_id == NEW_HIS_DATA_BASE_NODE_ID ||
g_node_id == RECALL_HIS_DATA_BASE_NODE_ID ||
g_node_id == RECALL_ALL_DATA_BASE_NODE_ID) {*/
recall_xml_t recall_xml;
std::memset(&recall_xml, 0, sizeof(recall_xml_t));
// 解析补招文件
parse_recall_xml(&recall_xml, id);
// 将补招数据赋值到全局变量
process_recall_config(&recall_xml);
//}
}
//补招成功后删除补招文件,补招后调用这个
int delete_recall_xml(const std::string& id) {
std::string cfg_dir = FRONT_PATH + "/etc/recall";
std::string pattern = std::string(subdir) + "_" +
std::to_string(g_front_seg_index) + "_" +
id + "_*_Recall.xml";
DIR* dir = opendir(cfg_dir.c_str());
if (!dir) {
std::cerr << "Folder does not exist or cannot be opened: " << cfg_dir << std::endl;
return false;
}
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
std::string filename = entry->d_name;
if (fnmatch(pattern.c_str(), filename.c_str(), 0) == 0) {
std::string fullpath = cfg_dir + "/" + filename;
if (remove(fullpath.c_str()) == 0) {
std::cout << "Deleted: " << fullpath << std::endl;
} else {
std::cerr << "Failed to delete: " << fullpath << std::endl;
}
}
}
closedir(dir);
return 0;
}
//删除过期的xmllnk:多个进程并发删除导致的失败不会影响进程
void DeletcRecallXml() {
std::string cfg_dir = FRONT_PATH + "/etc/recall";
std::string pattern = std::string(subdir) + "_*_Recall.xml";
DIR* dir = opendir(cfg_dir.c_str());
if (!dir) {
std::cerr << "folder does not exist!" << std::endl;
DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 删除旧的补招文件失败,补招文件路径FRONT_PATH + /etc/recall/不存在", g_front_seg_index);
return;
}
// 获取当前时间,计算 2 天前的时间戳
std::time_t now = std::time(nullptr);
std::time_t cutoff = now - 2 * 24 * 60 * 60; // 两天前
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
std::string filename = entry->d_name;
if (fnmatch(pattern.c_str(), filename.c_str(), 0) == 0) {
std::string fullpath = cfg_dir + "/" + filename;
struct stat file_stat;
if (stat(fullpath.c_str(), &file_stat) == 0) {
if (file_stat.st_mtime < cutoff) {
if (remove(fullpath.c_str()) == 0) {
DIY_INFOLOG("process", "【NORMAL】前置的%d号进程 删除超过两天的补招文件", g_front_seg_index);
} else {
std::cerr << "Failed to remove file: " << fullpath << std::endl;
}
}
}
}
}
closedir(dir);
}
//根据补招列表创建补招文件
void CreateRecallXml() {
std::time_t now = std::time(nullptr);
std::tm* tm_now = std::localtime(&now);
char timestamp[32] = {0};
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d%H%M%S", tm_now);
g_StatisticLackList_list_mutex.lock();
if (!g_StatisticLackList.empty()) {
DIY_INFOLOG("process", "【NORMAL】前置的%d号进程 开始写入补招文件", g_front_seg_index);
std::map<std::string, std::list<JournalRecall>> id_map;
for (const auto& jr : g_StatisticLackList) {
id_map[jr.MonitorID].push_back(jr);
}
for (const auto& pair : id_map) {
const std::string& monitor_id = pair.first;
const std::list<JournalRecall>& recalls = pair.second;
std::ostringstream path;
path << FRONT_PATH << "/etc/recall/" << subdir << "_" << g_front_seg_index << "_" << monitor_id << "_" << timestamp << "_Recall.xml";
tinyxml2::XMLDocument doc;
tinyxml2::XMLDeclaration* decl = doc.NewDeclaration();
doc.InsertFirstChild(decl);
tinyxml2::XMLElement* root = doc.NewElement("RecallList");
doc.InsertEndChild(root);
// 空 Work 段
tinyxml2::XMLElement* work = doc.NewElement("Work");
root->InsertEndChild(work);
// New 段
tinyxml2::XMLElement* new_elem = doc.NewElement("New");
for (const auto& jr : recalls) {
tinyxml2::XMLElement* recall = doc.NewElement("Recall");
recall->SetAttribute("MonitorID", jr.MonitorID.c_str());
recall->SetAttribute("StartTime", jr.StartTime.c_str());
recall->SetAttribute("EndTime", jr.EndTime.c_str());
recall->SetAttribute("STEADY", jr.STEADY.c_str());
recall->SetAttribute("VOLTAGE", jr.VOLTAGE.c_str());
new_elem->InsertEndChild(recall);
}
root->InsertEndChild(new_elem);
tinyxml2::XMLError save_result = doc.SaveFile(path.str().c_str());
if (save_result != tinyxml2::XML_SUCCESS) {
DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 无法将补招文件写入路径: %s", g_front_seg_index, path.str().c_str());
continue;
}
}
}
g_StatisticLackList.clear();
g_StatisticLackList_list_mutex.unlock();
}
//生成待补招xml文件
void create_recall_xml()
{
//if (g_node_id == HIS_DATA_BASE_NODE_ID || g_node_id == NEW_HIS_DATA_BASE_NODE_ID || g_node_id == RECALL_HIS_DATA_BASE_NODE_ID || (g_node_id == RECALL_ALL_DATA_BASE_NODE_ID)) {
DeletcRecallXml();
CreateRecallXml();
//}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////补招部分
// 工具函数:将时间字符串转为 time_t秒级
// ▲新增:从 monitorId 提取结尾数字(不含前导非数字部分),失败返回空串
static std::string get_monitor_digits_from_terminal_list(const std::string& dev_id,
const std::string& monitor_id)
{
std::lock_guard<std::mutex> lk(ledgermtx);
// 找终端
const terminal_dev* dev = NULL;
for (std::vector<terminal_dev>::const_iterator it = terminal_devlist.begin();
it != terminal_devlist.end(); ++it)
{
if (it->terminal_id == dev_id) { dev = &(*it); break; }
}
if (!dev) {
std::cout << "[digits] dev not found: " << dev_id << std::endl;
return std::string();
}
// 找监测点
for (std::vector<ledger_monitor>::const_iterator itLm = dev->line.begin();
itLm != dev->line.end(); ++itLm)
{
if (!itLm->monitor_id.empty() && itLm->monitor_id == monitor_id) {
// 常见就是 logical_device_seq比如 "1"、"02" 等
std::string seq = itLm->logical_device_seq;
// 可选:去掉前导 0与您生成“数字_时间.xxx”的命名规则保持一致
// 若不需要去零,注释以下 5 行即可。
size_t p = 0;
while (p < seq.size() && seq[p] == '0') ++p;
seq = (p >= seq.size()) ? "0" : seq.substr(p);
return seq;
}
}
std::cout << "[digits] monitor not found in dev: mon=" << monitor_id
<< " dev=" << dev_id << std::endl;
return std::string();
}
// ▲新增:把 "YYYY-MM-DD HH:MM:SS[.ffffff]" -> "YYYYMMDD_HHMMSS_mmm"
static std::string compact_ts_for_filename(const std::string& ts) {
// 允许输入 "2025-09-09 07:46:57.246000"
// 输出 "20250909_074657_246"
int year, mon, day, hour, min, sec, ms = 0;
char dotpart[16] = {0};
if (sscanf(ts.c_str(), "%d-%d-%d %d:%d:%d.%15s",
&year, &mon, &day, &hour, &min, &sec, dotpart) >= 6)
{
// 提取前三位毫秒
if (dotpart[0]) {
std::string frac(dotpart);
while (frac.size() < 3) frac.push_back('0'); // 不足3补0
ms = std::atoi(frac.substr(0, 3).c_str());
}
char buf[32];
/*snprintf(buf, sizeof(buf), "%04d%02d%02d_%02d%02d%02d_%03d",
year, mon, day, hour, min, sec, ms);*/
snprintf(buf, sizeof(buf), "%04d%02d%02d_%02d%02d%02d",
year, mon, day, hour, min, sec);
return std::string(buf);
}
return "";
}
// ▲新增按“数字_时间后缀.后缀”拼直下文件名(返回{*.cfg, *.dat}两种)
static std::vector<std::string> build_direct_filenames(const std::string& monitorDigits,
const std::string& ts_compact)
{
std::vector<std::string> out;
if (monitorDigits.empty() || ts_compact.empty()) return out;
out.push_back(monitorDigits + "_" + ts_compact);
return out;
}
static long long parse_time_to_epoch(const std::string& time_str) {
std::string s = time_str;
// ★去掉可能存在的微秒部分(例如 .000000
size_t dot_pos = s.find('.');
if (dot_pos != std::string::npos) {
s = s.substr(0, dot_pos);
}
std::tm tm = {};
std::istringstream ss(s);
ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
if (ss.fail()) {
std::cerr << "[parse_time_to_epoch] failed to parse time string: " << s << std::endl;
return -1; // ★返回 -1 表示失败(便于判断)
}
return static_cast<long long>(std::mktime(&tm));
}
// 数据完整性补招判断划分为1小时
void Get_Recall_Time_Char(const std::string& start_time_str,
const std::string& end_time_str,
std::vector<RecallInfo>& recallinfo_list_hour) {
long long starttime = parse_time_to_epoch(start_time_str);
long long endtime = parse_time_to_epoch(end_time_str);
if (starttime == 0 || endtime == 0 || starttime >= endtime) {
return;
}
// 初始区间加入
RecallInfo initial;
initial.starttime = starttime;
initial.endtime = endtime;
std::vector<RecallInfo> recallinfo_list;
recallinfo_list.push_back(initial);
const long long max_interval = 3600; // 1小时
for (const auto& interval : recallinfo_list) {
long long duration = interval.endtime - interval.starttime;
for (long long j = 0; j <= duration; j += max_interval) {
RecallInfo info;
info.starttime = interval.starttime + j;
if (j + max_interval > duration) {
info.endtime = interval.endtime;
} else {
info.endtime = interval.starttime + j + max_interval - 1;
}
recallinfo_list_hour.push_back(info);
}
}
}
//mq调用将补招信息写入补招列表
int recall_json_handle_from_mq(const std::string& body)
{
try {
// ====== 解析外层 JSON ======
nlohmann::json root;
try {
root = nlohmann::json::parse(body);
} catch (const std::exception& e) {
std::cerr << "Error parsing JSON: " << e.what() << std::endl;
// ★与原逻辑等价:无法解析,不再进入 recall_json_handle
DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,消息的json结构不正确",
g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str());
return 10004;
}
// 提取 "messageBody"(字符串)
if (!root.contains("messageBody") || !root["messageBody"].is_string()) {
std::cerr << "'messageBody' is missing or is not a string" << std::endl;
DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,没有messageBody字段",
g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str());
return 10004;
}
std::string messageBodyStr = root["messageBody"].get<std::string>();
if (messageBodyStr.empty()) {
std::cerr << "'messageBody' is empty" << std::endl;
DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,messageBody为空",
g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str());
return 10004;
}
// 解析 messageBody 内层 JSON
nlohmann::json mb;
try {
mb = nlohmann::json::parse(messageBodyStr);
} catch (const std::exception& e) {
std::cerr << "Failed to parse 'messageBody' JSON: " << e.what() << std::endl;
DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,messageBody的json结构不正确",
g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str());
return 10004;
}
if (mb.is_array()) {
// ====== 新格式(数组):支持 dataType=0/1 的区间补招 & dataType=2 的直下文件 ======
for (const auto& rec : mb) {
if (!rec.is_object()) continue;
// 必要字段
std::string guid = rec.value("guid", "");
std::string terminalId = rec.value("terminalId", "");
if (terminalId.empty()) continue;
// ▲dataTypestring "0/1/2" 或 number 0/1/2先判断 contains
int dt = -1;
if (rec.contains("dataType")) {
if (rec["dataType"].is_number_integer()) {
dt = rec["dataType"].get<int>();
} else if (rec["dataType"].is_string()) {
std::string s = rec["dataType"].get<std::string>();
if (s == "0") dt = 0;
else if (s == "1") dt = 1;
else if (s == "2") dt = 2;
}
}
if (dt == -1) {
std::cout << "[recall] skip: invalid dataType, guid=" << guid << "\n";
continue;
}
// === 统一收集监测点:支持 monitorIdList 或 monitorId ===
std::vector<std::string> monitors;
if (rec.contains("monitorIdList") && rec["monitorIdList"].is_array()) {
for (const auto& m : rec["monitorIdList"]) {
if (m.is_string()) monitors.push_back(m.get<std::string>());
}
}
if (monitors.empty() && rec.contains("monitorId")) {
if (rec["monitorId"].is_array()) {
for (const auto& m : rec["monitorId"]) {
if (m.is_string()) monitors.push_back(m.get<std::string>());
}
} else if (rec["monitorId"].is_string()) {
monitors.push_back(rec["monitorId"].get<std::string>());
}
}
if (monitors.empty()) {
std::cout << "[recall] skip: monitors empty (no monitorIdList/monitorId), guid=" << guid << "\n";
continue;
}
// ▲沿用:校验终端归属 + 在线性
{
std::lock_guard<std::mutex> lock(ledgermtx);
const terminal_dev* targetDev = NULL;
for (std::vector<terminal_dev>::const_iterator it = terminal_devlist.begin(); it != terminal_devlist.end(); ++it) {
if (it->terminal_id == terminalId) { targetDev = &(*it); break; }
}
if (!targetDev) { std::cout << "terminalId未匹配当前进程内装置: " << terminalId << std::endl; continue; }
if (ClientManager::instance().get_dev_status(targetDev->terminal_id) != 1) {
std::cout << "terminalId对应装置不在线: " << targetDev->terminal_id << std::endl;
std::string msg = std::string("装置:") + targetDev->terminal_name + " 不在线,无法补招";
send_reply_to_kafka_recall(guid, dt, static_cast<int>(ResponseCode::NOT_FOUND), msg, targetDev->terminal_id, "", "", "");
continue;
}
}
if (dt == 2) { //一个测点一个guid对应多个文件
// ▲直下文件timeInterval -> fun1/fun2 -> enqueue_direct_download
if (!rec.contains("timeInterval") || !rec["timeInterval"].is_array()) continue;
for (const auto& monId : monitors) {
// fun1提取 monitor 数字
std::string digits = get_monitor_digits_from_terminal_list(terminalId, monId);//有锁
if (digits.empty()) { std::cout << "monitorId数字提取失败: " << monId << std::endl; continue; }
// 根据 monitorId 和提取的数字初始化补招记录
init_recall_record_file(guid, terminalId, monId, "", "");
//根据时间戳单独补招事件
if(0)// ★新增dt==2 同步生成“按小时”的事件补招到 recall_list与 dt==1 逻辑一致)——开始
{
std::lock_guard<std::mutex> lock2(ledgermtx); // 复用与 dt==1 相同的加锁粒度
// 找终端
terminal_dev* dev_nc = NULL;
for (auto& d : terminal_devlist) if (d.terminal_id == terminalId) { dev_nc = &d; break; }
if (!dev_nc) { /* 理论上前面已校验,这里兜底 */ continue; }
// 找监测点
ledger_monitor* lm = NULL;
for (auto itLm = dev_nc->line.begin(); itLm != dev_nc->line.end(); ++itLm) {
if (!itLm->monitor_id.empty() && itLm->monitor_id == monId) { lm = &(*itLm); break; }
}
if (!lm) { std::cout << "monitorId未在terminal内找到(直下事件回灌): " << monId << " @ " << terminalId << std::endl; continue; }
// 将 timeList 的每个时间点映射为 [该整点, 下一整点) 的一小时窗口
for (const auto& t : rec["timeInterval"]) {
if (!t.is_string()) continue;
std::string tstr = t.get<std::string>();
long long ep = parse_time_to_epoch(tstr); // 需与系统时间解析格式一致(已有同名函数)
if (ep < 0) {
std::cout << "[direct->event] parse_time_to_epoch fail: " << tstr << std::endl;
continue;
}
long long hour_start = ep - (ep % 3600);
long long hour_end = hour_start + 3600 - 1; // 与 Get_Recall_Time_Char 产物风格保持一致(闭区间)
RecallMonitor rm;
rm.recall_guid = guid;
rm.recall_status = 0;
rm.StartTime = epoch_to_datetime_str(hour_start);
rm.EndTime = epoch_to_datetime_str(hour_end);
rm.STEADY = "0"; // 与 dt==1 一致:事件(波形)
rm.VOLTAGE = "1"; // 与 dt==1 一致
lm->recall_list.push_back(rm);
}
}
// ★新增dt==2 同步生成“按小时”的事件补招到 recall_list与 dt==1 逻辑一致)——结束
//根据时间戳单独补招事件
for (const auto& t : rec["timeInterval"]) {
if (!t.is_string()) continue;
std::string ts_compact = compact_ts_for_filename(t.get<std::string>());
if (ts_compact.empty()) { std::cout << "时间解析失败: " << t << std::endl; continue; }
// fun2生成 *.cfg/*.dat 两个文件名
std::vector<std::string> fns = build_direct_filenames(digits, ts_compact);
for (const auto& fn : fns) {
bool ok = enqueue_direct_download(terminalId, monId, fn, guid);//有锁
std::cout << "[direct] enqueue " << (ok ? "ok " : "fail ")
<< "dev=" << terminalId << " mon=" << monId
<< " file=" << fn << std::endl;
}
}
}
} else if (dt == 0 || dt == 1) { //一个装置对应一个guid对应多个监测点的多个时间段
// ▲保持老逻辑(与“对象+data”一致timeInterval 数组
if (!rec.contains("timeInterval") || !rec["timeInterval"].is_array()) continue;
// 解析 dataType-> stat/voltage
int stat = (dt == 0) ? 1 : 0;
int voltage = (dt == 1) ? 1 : 0;
// 把每个 monitor 的区间写入 recall_list / recall_list_static
std::lock_guard<std::mutex> lock(ledgermtx);
// 找终端
terminal_dev* dev_nc = NULL;
for (auto& d : terminal_devlist) if (d.terminal_id == terminalId) { dev_nc = &d; break; }
if (!dev_nc) continue;
for (const auto& monId : monitors) {
// 找监测点
ledger_monitor* lm = NULL;
for (auto itLm = dev_nc->line.begin(); itLm != dev_nc->line.end(); ++itLm) {
if (!itLm->monitor_id.empty() && itLm->monitor_id == monId) { lm = &(*itLm); break; }
}
if (!lm) { std::cout << "monitorId未在terminal内找到: " << monId << " @ " << terminalId << std::endl; continue; }
for (const auto& ti : rec["timeInterval"]) {
if (!ti.is_string()) continue;
std::string s = ti.get<std::string>();
std::string::size_type pos = s.find('~');
if (pos == std::string::npos) { std::cout << "timeInterval格式错误: " << s << std::endl; continue; }
std::string start = s.substr(0, pos);
std::string end = s.substr(pos + 1);
RecallFile rm_all;
rm_all.recall_guid = guid;
rm_all.recall_status = 0;
rm_all.StartTime = start;
rm_all.EndTime = end;
rm_all.STEADY = std::to_string(stat);
rm_all.VOLTAGE = std::to_string(voltage);
//lnk 20251027xuyang request:生成文件记录单个测点单个时间段的补招记录文件,补招结束后使用这个文件信息来响应
init_recall_record_file(guid, terminalId, monId, start, end);
if (voltage == 1) {
std::vector<RecallInfo> recallinfo_list_hour;
Get_Recall_Time_Char(start, end, recallinfo_list_hour);
for (size_t i = 0; i < recallinfo_list_hour.size(); ++i) {
const RecallInfo& info = recallinfo_list_hour[i];
RecallMonitor rm;
rm.recall_guid = guid;
rm.recall_status = 0;
rm.StartTime = epoch_to_datetime_str(info.starttime);
rm.EndTime = epoch_to_datetime_str(info.endtime);
rm.STEADY = std::to_string(stat);
rm.VOLTAGE = std::to_string(voltage);
lm->recall_list.push_back(rm);
}
}
if (stat == 1) {
lm->recall_list_static.push_back(rm_all);
}
if (stat == 0 && voltage == 0) return 10003;
}
}
} else {
// 未知 dataType忽略
std::cout << "unknown dataType: " << dt << std::endl;
continue;
}
}
}
else {
// 不支持的 messageBody 形态
std::cout << "unknown messageBody form" << std::endl;
return 10004;
}
}
catch (const std::exception& e) {
std::cout << "处理客户端发送的消息错误,原因:" << e.what() << std::endl;
return 10004;
}
return 0;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////台账接口打印
// 打印 terminal_dev_map 中所有内容的函数
void printTerminalDevMap(const std::map<std::string, terminal_dev>& terminal_dev_map) {
for (const auto& kv : terminal_dev_map) {
const std::string& key = kv.first;
const terminal_dev& dev = kv.second; // 引用对象
std::cout << "Key: " << key
<< ", Terminal ID: " << dev.terminal_id
<< ", Terminal Code: " << dev.terminal_name
<< ", Organization Name: "<< dev.org_name
<< ", Maintenance Name: " << dev.maint_name
<< ", Station Name: " << dev.station_name
<< ", Factory: " << dev.tmnl_factory
<< ", Status: " << dev.tmnl_status
<< ", Device Type: " << dev.dev_type
<< ", Device Key: " << dev.dev_key
<< ", Device Series: " << dev.dev_series
<< ", ProcessNo: " << dev.processNo
<< ", MaxProcessNum: " << dev.maxProcessNum
<< ", Address: " << dev.addr_str
<< ", Port: " << dev.port
<< ", Timestamp: " << dev.timestamp
<< ", mac: " << dev.mac
<< std::endl;
// 打印监测点信息
for (size_t i = 0; i < dev.line.size(); ++i) {
const auto& m = dev.line[i];
std::cout << " Monitor [" << i << "] "
<< "ID: " << m.monitor_id
<< ", Code: " << m.terminal_id
<< ", Name: " << m.monitor_name
<< ", Seq: " << m.logical_device_seq
<< ", Voltage: "<< m.voltage_level
<< ", Connect: "<< m.terminal_connect
<< ", Timestamp:"<< m.timestamp
<< ", Status: " << m.status
<< ", CT1: " << m.CT1
<< ", CT2: " << m.CT2
<< ", PT1: " << m.PT1
<< ", PT2: " << m.PT2
<< std::endl;
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////更新台账
//在台账更新目录查找自己进程要处理的文件
std::list<std::string> find_xml_belong_to_this_process()
{
char prefix[20]; // 假设最多需要20个字符根据实际需要调整
sprintf(prefix, "%d_%d", g_node_id, g_front_seg_index); // 将g_node_id和g_front_seg_index格式化为字符串
std::string LEDGER_UPDATE_DIR = FRONT_PATH + "/etc/ledgerupdate/"; //台账更新读取目录
DIR *dir = opendir(LEDGER_UPDATE_DIR.c_str()); // 打开目录
struct dirent *entry;
std::list<std::string> found_files; // 用于存储找到的所有匹配文件名
if (dir == NULL) {
std::cout << "Failed to open directory: " << LEDGER_UPDATE_DIR << std::endl;
return found_files; // 返回空的list
}
// 遍历目录中的所有文件
while ((entry = readdir(dir)) != NULL) {
std::string filename = entry->d_name;
// 排除 "." 和 ".." 目录
if (filename == "." || filename == "..") {
continue;
}
std::cout << "find" << filename << "in" << LEDGER_UPDATE_DIR << std::endl;
// 判断文件名是否以 prefix 开头且扩展名是 .xml
if (filename.find(prefix) == 0 && filename.substr(filename.find_last_of('.') + 1) == "xml") {
std::string full_path = LEDGER_UPDATE_DIR + filename;
found_files.push_back(full_path); // 将完整路径加入容器
}
}
closedir(dir); // 关闭目录
return found_files; // 返回所有找到的文件名
}
// 根据 str_tag 将 terminal 添加到对应的数组
void add_terminal_to_trigger_update(trigger_update_xml_t& trigger_update_xml,
const std::string& str_tag,
const update_dev& work_terminal) {
if (str_tag == "add") {
std::cout << "new ledger!!!!" << std::endl;
trigger_update_xml.new_updates.push_back(work_terminal);
} else if (str_tag == "modify") {
std::cout << "modify ledger!!!" << std::endl;
trigger_update_xml.modify_updates.push_back(work_terminal);
} else {
std::cerr << "Unknown tag: " << str_tag << std::endl;
}
}
// 将添加和修改的文件内容写入结构
void parse_terminal_from_data(trigger_update_xml_t& trigger_update_xml,
const std::string& str_tag,
const std::string& data,
const std::string& guid_value) {
update_dev work_terminal;
work_terminal.guid = guid_value;
tinyxml2::XMLDocument doc;
if (doc.Parse(data.c_str()) != tinyxml2::XML_SUCCESS) {
return;
}
auto root = doc.FirstChildElement("terminal");
if (!root) return;
auto get_value = [&](const char* tag) -> std::string {
auto elem = root->FirstChildElement(tag);
return elem && elem->GetText() ? elem->GetText() : "";
};
work_terminal.terminal_id = get_value("id");
work_terminal.terminal_name = get_value("terminalCode");
//work_terminal.org_name = get_value("orgName");
//work_terminal.maint_name = get_value("maintName");
//work_terminal.station_name = get_value("stationName");
//work_terminal.tmnl_factory = get_value("manufacturer");
work_terminal.tmnl_status = get_value("status");
work_terminal.dev_type = get_value("devType");
//work_terminal.dev_key = get_value("devKey");
//work_terminal.dev_series = get_value("series");
work_terminal.processNo = get_value("processNo");
//work_terminal.addr_str = get_value("ip");
//work_terminal.port = get_value("port");
//work_terminal.timestamp = get_value("updateTime");
work_terminal.Righttime = get_value("Righttime");
work_terminal.mac = get_value("mac");
for (tinyxml2::XMLElement* monitor = root->FirstChildElement("monitorData");
monitor;
monitor = monitor->NextSiblingElement("monitorData")) {
update_monitor mon;
mon.monitor_id = monitor->FirstChildElement("id") ? monitor->FirstChildElement("id")->GetText() : "N/A";
mon.monitor_name = monitor->FirstChildElement("name") ? monitor->FirstChildElement("name")->GetText() : "N/A";
mon.voltage_level = monitor->FirstChildElement("voltageLevel") ? monitor->FirstChildElement("voltageLevel")->GetText() : "N/A";
mon.terminal_connect = monitor->FirstChildElement("ptType") ? monitor->FirstChildElement("ptType")->GetText() : "N/A";
mon.logical_device_seq = monitor->FirstChildElement("lineNo") ? monitor->FirstChildElement("lineNo")->GetText() : "N/A";
//mon.timestamp = monitor->FirstChildElement("timestamp") ? monitor->FirstChildElement("timestamp")->GetText() : "N/A";
mon.terminal_id = monitor->FirstChildElement("terminal_id") ? monitor->FirstChildElement("terminal_name")->GetText() : "N/A";
mon.status = monitor->FirstChildElement("status") ? monitor->FirstChildElement("status")->GetText() : "N/A";
mon.CT1 = monitor->FirstChildElement("CT1") && monitor->FirstChildElement("CT1")->GetText()
? atof(monitor->FirstChildElement("CT1")->GetText()) : 0.0;
mon.CT2 = monitor->FirstChildElement("CT2") && monitor->FirstChildElement("CT2")->GetText()
? atof(monitor->FirstChildElement("CT2")->GetText()) : 0.0;
mon.PT1 = monitor->FirstChildElement("PT1") && monitor->FirstChildElement("PT1")->GetText()
? atof(monitor->FirstChildElement("PT1")->GetText()) : 0.0;
mon.PT2 = monitor->FirstChildElement("PT2") && monitor->FirstChildElement("PT2")->GetText()
? atof(monitor->FirstChildElement("PT2")->GetText()) : 0.0;
work_terminal.line.push_back(mon);
}
add_terminal_to_trigger_update(trigger_update_xml, str_tag, work_terminal);
}
// 统一处理文件内容和结构
void parse_ledger_update(trigger_update_xml_t& trigger_update_xml,
const std::string& strTag,
const std::string& data,
const std::string& guid_value) {
std::cout << "record one xml.." << std::endl;
if (strTag == "add" || strTag == "modify") {
parse_terminal_from_data(trigger_update_xml, strTag, data, guid_value);
} else if (strTag == "delete") {
update_dev delete_terminal;
tinyxml2::XMLDocument doc;
if (doc.Parse(data.c_str()) != tinyxml2::XML_SUCCESS) {
std::cerr << "Failed to parse XML for delete tag." << std::endl;
return;
}
tinyxml2::XMLElement* root = doc.FirstChildElement("terminalData");
if (!root) {
std::cerr << "Missing terminalData element in delete tag." << std::endl;
return;
}
tinyxml2::XMLElement* idElem = root->FirstChildElement("id");
if (idElem && idElem->GetText()) {
delete_terminal.terminal_id = idElem->GetText();
} else {
std::cerr << "Missing id element in delete tag." << std::endl;
return;
}
delete_terminal.guid = guid_value;
trigger_update_xml.delete_updates.push_back(delete_terminal);
} else {
std::cerr << "Unsupported strTag: " << strTag << std::endl;
}
}
//读取台账更新文件
int load_ledger_update_from_xml(trigger_update_xml_t& trigger_update_xml, const std::string& xml_fn) {
std::cout << "start to load one xml.." << std::endl;
std::ifstream file(xml_fn);
if (!file.is_open()) {
std::cerr << "Failed to open file: " << xml_fn << std::endl;
return -1;
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
file.close();
tinyxml2::XMLDocument doc;
if (doc.Parse(content.c_str()) != tinyxml2::XML_SUCCESS) {
std::cerr << "Failed to parse XML content." << std::endl;
return -1;
}
auto* root = doc.FirstChildElement("ledger_update");
if (!root) {
std::cerr << "Missing <ledger_update> root tag." << std::endl;
return -1;
}
std::string guid_value;
auto* guidElem = root->FirstChildElement("guid");
if (guidElem && guidElem->GetText()) {
guid_value = guidElem->GetText();
std::cout << "Found guid: " << guid_value << std::endl;
}
const char* tag_names[] = {"add", "modify", "delete"};
tinyxml2::XMLElement* tagElem = nullptr;
std::string target_tag;
for (const auto& tag : tag_names) {
tagElem = root->FirstChildElement(tag);
if (tagElem) {
target_tag = tag;
break;
}
}
if (!tagElem) {
std::cerr << "No <add>, <modify>, or <delete> tag found!" << std::endl;
return -1;
}
for (auto* termElem = tagElem->FirstChildElement("terminalData");
termElem;
termElem = termElem->NextSiblingElement("terminalData")) {
tinyxml2::XMLPrinter printer;
termElem->Accept(&printer);
std::string data_content = printer.CStr();
std::cout << "ledger data_content is " << data_content << std::endl;
parse_ledger_update(trigger_update_xml, target_tag, data_content, guid_value);
}
std::cout << "load one xml finish" << std::endl;
return 0;
}
//台账更新处理文件
int parse_ledger_update_xml(trigger_update_xml_t& trigger_update_xml)
{
std::list<std::string> result = find_xml_belong_to_this_process();
if (result.empty()) return 1;
for (const auto& filename : result) {
std::cout << "Found XML: " << filename << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (!load_ledger_update_from_xml(trigger_update_xml, filename)) {
DIY_WARNLOG("process", "【WARN】成功读取台账更新文件: %s", filename.c_str());
}
if (std::remove(filename.c_str()) != 0) {
DIY_ERRORLOG("process", "【ERROR】删除台账更新文件失败: %s", filename.c_str());
return 1;
} else {
DIY_INFOLOG("process", "【NORMAL】成功删除台账更新文件: %s", filename.c_str());
}
}
if (!trigger_update_xml.new_updates.empty() ||
!trigger_update_xml.modify_updates.empty() ||
!trigger_update_xml.delete_updates.empty()) {
std::cout << "ledger update xml have data..." << std::endl;
return 0;
}
std::cout << "ledger update xml no data..." << std::endl;
return 1;
}
//更新单个台账
int update_one_terminal_ledger(const update_dev& update,terminal_dev& target_dev) {
// 更新基本信息
if (!update.terminal_id.empty()) {
target_dev.terminal_id = update.terminal_id;
std::cout << "terminal_id: " << target_dev.terminal_id << std::endl;
}
if (!update.terminal_name.empty()) {
target_dev.terminal_name = update.terminal_name;
std::cout << "terminal_name: " << target_dev.terminal_name << std::endl;
}
/*if (!update.tmnl_factory.empty()) {
target_dev.tmnl_factory = update.tmnl_factory;
std::cout << "tmnl_factory: " << target_dev.tmnl_factory << std::endl;
}
if (!update.tmnl_status.empty()) {
target_dev.tmnl_status = update.tmnl_status;
std::cout << "tmnl_status: " << target_dev.tmnl_status << std::endl;
}*/
if (!update.dev_type.empty()) {
target_dev.dev_type = update.dev_type;
std::cout << "dev_type: " << target_dev.dev_type << std::endl;
}
if (!update.processNo.empty()) {
target_dev.processNo = update.processNo;
std::cout << "processNo: " << target_dev.processNo << std::endl;
}
/*if (!update.dev_series.empty()) {
target_dev.dev_series = update.dev_series;
std::cout << "dev_series: " << target_dev.dev_series << std::endl;
}
if (!update.dev_key.empty()) {
target_dev.dev_key = update.dev_key;
std::cout << "dev_key: " << target_dev.dev_key << std::endl;
}*/
if (!update.addr_str.empty()) {
target_dev.addr_str = update.addr_str;
std::cout << "addr_str: " << target_dev.addr_str << std::endl;
}
/*if (!update.port.empty()) {
target_dev.port = update.port;
std::cout << "port: " << target_dev.port << std::endl;
}
if (!update.mac.empty()) {
target_dev.mac = update.mac;
std::cout << "mac: " << target_dev.mac << std::endl;
}
if (!update.timestamp.empty()) {
struct tm timeinfo = {};
if (sscanf(update.timestamp.c_str(), "%4d-%2d-%2d %2d:%2d:%2d",
&timeinfo.tm_year, &timeinfo.tm_mon, &timeinfo.tm_mday,
&timeinfo.tm_hour, &timeinfo.tm_min, &timeinfo.tm_sec) == 6) {
timeinfo.tm_year -= 1900;
timeinfo.tm_mon -= 1;
timeinfo.tm_isdst = -1;
time_t raw_time = mktime(&timeinfo);
if (raw_time != -1) {
target_dev.timestamp = static_cast<long long>(raw_time);
std::cout << "timestamp (unix): " << target_dev.timestamp << std::endl;
} else {
std::cerr << "Error: mktime failed." << std::endl;
return -1;
}
} else {
std::cerr << "Error: invalid timestamp format." << std::endl;
return -1;
}
}*/
// 清空旧监测点并重新填充
target_dev.line.clear();
for (const auto& mon : update.line) {
if (mon.monitor_id.empty()) break;
ledger_monitor m;
m.monitor_id = mon.monitor_id;
m.monitor_name = mon.monitor_name;
m.logical_device_seq = mon.logical_device_seq;
m.voltage_level = mon.voltage_level;
m.terminal_connect = mon.terminal_connect;
m.status = mon.status;
m.terminal_id = mon.terminal_id;
//m.timestamp = mon.timestamp;
m.CT1 = mon.CT1;
m.CT2 = mon.CT2;
m.PT1 = mon.PT1;
m.PT2 = mon.PT2;
if (m.terminal_connect != "0") {
isdelta_flag = 1;
std::cout << "monitor_id " << m.monitor_id << " uses delta wiring." << std::endl;
}
/*if (!m.timestamp.empty()) {
struct tm timeinfo = {};
if (sscanf(m.timestamp.c_str(), "%4d-%2d-%2d %2d:%2d:%2d",
&timeinfo.tm_year, &timeinfo.tm_mon, &timeinfo.tm_mday,
&timeinfo.tm_hour, &timeinfo.tm_min, &timeinfo.tm_sec) == 6) {
timeinfo.tm_year -= 1900;
timeinfo.tm_mon -= 1;
timeinfo.tm_isdst = -1;
time_t raw_time = mktime(&timeinfo);
if (raw_time != -1) {
m.timestamp = static_cast<long long>(raw_time);
std::cout << "monitor time (unix): " << m.timestamp << std::endl;
}
}
}*/
target_dev.line.push_back(m);
}
return 0;
}
//台账更新到台账列表
void process_ledger_update(trigger_update_xml_t& ledger_update_xml)
{
// --- 1. 新增处理 ---
std::cout << "add ledger num: " << ledger_update_xml.new_updates.size() << std::endl;
for (auto it = ledger_update_xml.new_updates.begin(); it != ledger_update_xml.new_updates.end(); ) {
update_dev& new_dev = *it;
auto found = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev& d) { return d.terminal_id == new_dev.terminal_id; });
if (found != terminal_devlist.end()) {
if (ledger_update_xml.modify_updates.size() < MAX_UPDATEA_NUM) {
ledger_update_xml.modify_updates.push_back(new_dev);
} else {
std::cerr << "Exceeded MAX_UPDATEA_NUM limit for modify_updates!" << std::endl;
}
it = ledger_update_xml.new_updates.erase(it); // 删除已处理项
continue;
}
if (terminal_devlist.size() >= static_cast<size_t>(IED_COUNT)) {
send_reply_to_queue(new_dev.guid, static_cast<int>(ResponseCode::BAD_REQUEST),
"终端 id: " + new_dev.terminal_id + " 台账更新失败,配置台账数量已满");
++it;
continue;
}
terminal_dev target_dev;
if (update_one_terminal_ledger(new_dev, target_dev) != 0) {
send_reply_to_queue(new_dev.guid, static_cast<int>(ResponseCode::BAD_REQUEST),
"终端 id: " + new_dev.terminal_id + " 台账更新失败,无法写入台账");
++it;
continue;
}
init_loggers_bydevid(target_dev.terminal_id);
terminal_devlist.push_back(target_dev);
DeviceInfo device = make_device_from_terminal(target_dev);
ClientManager::instance().add_device(device);
send_reply_to_queue(new_dev.guid, static_cast<int>(ResponseCode::OK),
"终端 id: " + new_dev.terminal_id + " 台账添加成功");
it = ledger_update_xml.new_updates.erase(it);
}
// --- 2. 修改处理 ---
std::cout << "modify ledger num: " << ledger_update_xml.modify_updates.size() << std::endl;
for (auto& mod_dev : ledger_update_xml.modify_updates) {
auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev& d) { return d.terminal_id == mod_dev.terminal_id; });
if (it != terminal_devlist.end()) {
erase_one_terminals_by_id(mod_dev.terminal_id);
if (update_one_terminal_ledger(mod_dev, *it) != 0) {
send_reply_to_queue(mod_dev.guid, static_cast<int>(ResponseCode::BAD_REQUEST),
"终端 id: " + mod_dev.terminal_id + " 台账更新失败,写入失败");
continue;
}
init_loggers_bydevid(mod_dev.terminal_id);
DeviceInfo device = make_device_from_terminal(*it);
ClientManager::instance().add_device(device);
send_reply_to_queue(mod_dev.guid, static_cast<int>(ResponseCode::OK),
"终端 id: " + mod_dev.terminal_id + " 台账修改成功");
} else {
send_reply_to_queue(mod_dev.guid, static_cast<int>(ResponseCode::NOT_FOUND),
"终端 id: " + mod_dev.terminal_id + " 台账修改失败,未找到终端");
}
}
// --- 3. 删除处理 ---
std::cout << "delete ledger num: " << ledger_update_xml.delete_updates.size() << std::endl;
for (auto& del_dev : ledger_update_xml.delete_updates) {
auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev& d) { return d.terminal_id == del_dev.terminal_id; });
if (it != terminal_devlist.end()) {
erase_one_terminals_by_id(del_dev.terminal_id);
ClientManager::instance().remove_device(del_dev.terminal_id);
send_reply_to_queue(del_dev.guid, static_cast<int>(ResponseCode::OK),
"终端 id: " + del_dev.terminal_id + " 台账删除成功");
} else {
send_reply_to_queue(del_dev.guid, static_cast<int>(ResponseCode::NOT_FOUND),
"终端 id: " + del_dev.terminal_id + " 台账删除失败,未找到终端");
}
}
// --- 4. 日志记录 ---
if (!ledger_update_xml.modify_updates.empty() ||
!ledger_update_xml.new_updates.empty() ||
!ledger_update_xml.delete_updates.empty()) {
create_ledger_log(&ledger_update_xml);
}
}
//台账更新处理函数
void check_ledger_update()
{
static double last_check_time = 0.0;
double now = sGetMsTime();
if (now - last_check_time < 3000.0)
return;
last_check_time = now;
std::unique_ptr<trigger_update_xml_t> trigger_ledger_update_xml(new trigger_update_xml_t());
if (0 == parse_ledger_update_xml(*trigger_ledger_update_xml)) {
print_trigger_update_xml(*trigger_ledger_update_xml);
std::lock_guard<std::mutex> lock(ledgermtx);
process_ledger_update(*trigger_ledger_update_xml);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////台账更新记录日志
// 获取当前时间并格式化为 "YYYY-MM-DD HH:MM:SS"
std::string get_current_time() {
std::time_t t = std::time(NULL);
struct std::tm tm = *std::localtime(&t);
char buffer[80];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm);
return std::string(buffer);
}
// 写入日志条目
void write_log_entry(std::ofstream &log_file, const std::string &action, const std::string &terminal_id, const std::string &current_time) {
log_file << terminal_id << "\t" << action << "time:" << current_time << "\n";
}
// 创建台账更新日志
void create_ledger_log(trigger_update_xml_t* ledger_update_xml) {
std::cout << "create_ledger_log." << std::endl;
std::string log_filename = FRONT_PATH + "/etc/" + LEDGER_UPDATE_FN;
std::ofstream log_file(log_filename.c_str(), std::ios::app); // 以追加模式打开文件
if (!log_file.is_open()) {
std::cerr << "Failed to open log file: " << log_filename << std::endl;
return;
}
std::vector<std::pair<std::string, std::string>> new_entries;
std::vector<std::pair<std::string, std::string>> modify_entries;
std::vector<std::pair<std::string, std::string>> delete_entries;
std::string current_time = get_current_time(); // 获取当前时间
// new_updates
for (const auto& dev : ledger_update_xml->new_updates) {
new_entries.emplace_back(dev.terminal_id, current_time);
}
// modify_updates
for (const auto& dev : ledger_update_xml->modify_updates) {
modify_entries.emplace_back(dev.terminal_id, current_time);
}
// delete_updates
for (const auto& dev : ledger_update_xml->delete_updates) {
delete_entries.emplace_back(dev.terminal_id, current_time);
}
// 写入日志
if (!new_entries.empty()) {
log_file << "<new>\n";
for (const auto& entry : new_entries) {
write_log_entry(log_file, "add", entry.first, entry.second);
}
log_file << "</new>\n";
}
if (!modify_entries.empty()) {
log_file << "<modify>\n";
for (const auto& entry : modify_entries) {
write_log_entry(log_file, "modify", entry.first, entry.second);
}
log_file << "</modify>\n";
}
if (!delete_entries.empty()) {
log_file << "<delete>\n";
for (const auto& entry : delete_entries) {
write_log_entry(log_file, "delete", entry.first, entry.second);
}
log_file << "</delete>\n";
}
log_file.close();
std::cout << "Ledger log has been updated." << std::endl;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////shell打印日志
// ------------------ 全局日志列表和锁 ------------------
std::list<std::string> errorList;
std::list<std::string> warnList;
std::list<std::string> normalList;
pthread_mutex_t errorListMutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t warnListMutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t normalListMutex = PTHREAD_MUTEX_INITIALIZER;
// ------------------ 输出开关 ------------------
bool errorOutputEnabled = false; // 是否将 error 级别写入 errorList
bool warnOutputEnabled = false; // 是否将 warn 级别写入 warnList
bool normalOutputEnabled = false; // 是否将 normal 级别写入 normalList
// ------------------ 用于恢复原始缓冲区 ------------------
static std::streambuf* g_originalCoutBuf = NULL;
static std::streambuf* g_originalClogBuf = NULL;
static std::streambuf* g_originalCerrBuf = NULL;
// ------------------ 日志级别枚举C++98 ------------------
enum LogLevel {
LOGERROR,
LOGWARN,
LOGNORMAL
};
// ------------------------------------------------------------------
// TeeStreamBuf: 先写到原始buf(保持终端输出)再拷贝到list里
// ------------------------------------------------------------------
class TeeStreamBuf : public std::streambuf
{
public:
// 默认构造先把指针设为NULL
TeeStreamBuf()
: m_originalBuf(NULL), m_level(LOGNORMAL)
{
pthread_mutex_init(&m_mutex, NULL);
}
// 带参构造:直接初始化
TeeStreamBuf(std::streambuf* originalBuf, LogLevel level)
: m_originalBuf(originalBuf), m_level(level)
{
pthread_mutex_init(&m_mutex, NULL);
}
// 析构函数:销毁互斥锁
virtual ~TeeStreamBuf()
{
pthread_mutex_destroy(&m_mutex);
}
// 自定义 init(...) 函数:在同一个对象上重新设置
void init(std::streambuf* originalBuf, LogLevel level)
{
m_originalBuf = originalBuf;
m_level = level;
pthread_mutex_lock(&m_mutex);
m_buffer.clear();
pthread_mutex_unlock(&m_mutex);
}
protected:
// 当 flush 或 std::endl 时会调用 sync()
virtual int sync()
{
// 先让原始缓冲区执行同步
if (m_originalBuf) {
m_originalBuf->pubsync();
}
// 再将自身的缓存 flush
flushBuffer();
return 0; // 成功
}
// 当写入一个新字符时overflow() 被调用
virtual int_type overflow(int_type ch)
{
if (ch == traits_type::eof()) {
return ch;
}
// 1) 写到原始缓冲区 → 保留终端输出
if (m_originalBuf) {
if (m_originalBuf->sputc(static_cast<char>(ch)) == traits_type::eof()) {
return traits_type::eof();
}
}
// 2) 存到我们的临时缓存,注意加锁保护
pthread_mutex_lock(&m_mutex); //防止多线程推入崩溃lnk20250305
m_buffer.push_back(static_cast<char>(ch));
// 3) 遇到换行就 flushBuffer()
if (ch == '\n') {
flushBuffer_locked();
}
pthread_mutex_unlock(&m_mutex);
return ch;
}
private:
// 内部版本:假定互斥锁已经被加锁
void flushBuffer_locked()
{
if (m_buffer.empty()) {
return;
}
// 根据等级和对应开关,将 m_buffer 写入相应的 list
switch (m_level) {
case LOGERROR:
if (normalOutputEnabled) {
pthread_mutex_lock(&normalListMutex);
normalList.push_back(m_buffer);
pthread_mutex_unlock(&normalListMutex);
}
else if (warnOutputEnabled) {
pthread_mutex_lock(&warnListMutex);
warnList.push_back(m_buffer);
pthread_mutex_unlock(&warnListMutex);
}
else if (errorOutputEnabled) {
pthread_mutex_lock(&errorListMutex);
errorList.push_back(m_buffer);
pthread_mutex_unlock(&errorListMutex);
}
break;
case LOGWARN:
if (normalOutputEnabled) {
pthread_mutex_lock(&normalListMutex);
normalList.push_back(m_buffer);
pthread_mutex_unlock(&normalListMutex);
}
else if (warnOutputEnabled) {
pthread_mutex_lock(&warnListMutex);
warnList.push_back(m_buffer);
pthread_mutex_unlock(&warnListMutex);
}
break;
case LOGNORMAL:
if (normalOutputEnabled) {
pthread_mutex_lock(&normalListMutex);
normalList.push_back(m_buffer);
pthread_mutex_unlock(&normalListMutex);
}
break;
}
m_buffer.clear();
}
// 对外接口,内部对 m_buffer 加锁
void flushBuffer()
{
pthread_mutex_lock(&m_mutex);
flushBuffer_locked();
pthread_mutex_unlock(&m_mutex);
}
private:
// 禁止自动生成的赋值函数
TeeStreamBuf& operator=(const TeeStreamBuf&);
private:
std::streambuf* m_originalBuf;
LogLevel m_level;
std::string m_buffer;
pthread_mutex_t m_mutex;
};
// ------------------ 全局Tee对象避免重复赋值 ------------------
static TeeStreamBuf g_errorTeeBuf;
static TeeStreamBuf g_warnTeeBuf;
static TeeStreamBuf g_normalTeeBuf;
// ------------------ 重定向函数 ------------------
// 只在第一次启用时,用 init(...) 初始化 TeeStreamBuf
// 之后再启用时,不再 new 或 赋值,而是直接用之前构造好的对象。
void redirectErrorOutput(bool enabled)
{
errorOutputEnabled = enabled;
if (enabled) {
if (g_originalCerrBuf == NULL) {
g_originalCerrBuf = std::cerr.rdbuf();
g_errorTeeBuf.init(g_originalCerrBuf, LOGERROR);
}
std::cerr.rdbuf(&g_errorTeeBuf);
} else {
if (g_originalCerrBuf) {
std::cerr.rdbuf(g_originalCerrBuf);
}
}
}
void redirectWarnOutput(bool enabled)
{
warnOutputEnabled = enabled;
if (enabled) {
if (g_originalClogBuf == NULL) {
g_originalClogBuf = std::clog.rdbuf();
g_warnTeeBuf.init(g_originalClogBuf, LOGWARN);
}
std::clog.rdbuf(&g_warnTeeBuf);
} else {
if (g_originalClogBuf) {
std::clog.rdbuf(g_originalClogBuf);
}
}
}
void redirectNormalOutput(bool enabled)
{
normalOutputEnabled = enabled;
if (enabled) {
if (g_originalCoutBuf == NULL) {
g_originalCoutBuf = std::cout.rdbuf();
g_normalTeeBuf.init(g_originalCoutBuf, LOGNORMAL);
}
std::cout.rdbuf(&g_normalTeeBuf);
} else {
if (g_originalCoutBuf) {
std::cout.rdbuf(g_originalCoutBuf);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////脚本控制
void execute_bash(string fun,int process_num,string type)
{
// 为 char 数组分配足够的空间
char p_num_str[20];
// 使用 sprintf 转换
std::sprintf(p_num_str, "%d", process_num);
std::string script = FRONT_PATH + "/bin/set_process.sh";//使用setsid防止端口占用
const char* param1 = fun.c_str();
const char* param2 = p_num_str;
const char* param3 = type.c_str();
// 构造完整的命令
char command[256];
snprintf(command, sizeof(command), "%s %s %s %s &", script.c_str(), param1, param2, param3);
std::cout << "command:" << command <<std::endl;
// 执行命令
system(command);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////打印台账更新
void print_trigger_update_xml(const trigger_update_xml_t& trigger_update) {
std::cout << "Work Updates Count: " << trigger_update.work_updates.size() << "\n";
std::cout << "New Updates Count: " << trigger_update.new_updates.size() << "\n";
std::cout << "Delete Updates Count: " << trigger_update.delete_updates.size() << "\n";
std::cout << "Modify Updates Count: " << trigger_update.modify_updates.size() << "\n\n";
std::cout << "Work Updates:\n";
for (size_t i = 0; i < trigger_update.work_updates.size(); ++i) {
std::cout << "Work Update " << (i + 1) << ":\n";
print_terminal(trigger_update.work_updates[i]);
}
std::cout << "\nNew Updates:\n";
for (size_t i = 0; i < trigger_update.new_updates.size(); ++i) {
std::cout << "New Update " << (i + 1) << ":\n";
print_terminal(trigger_update.new_updates[i]);
}
std::cout << "\nDelete Updates:\n";
for (size_t i = 0; i < trigger_update.delete_updates.size(); ++i) {
std::cout << "Delete Update " << (i + 1) << ":\n";
print_terminal(trigger_update.delete_updates[i]);
}
std::cout << "\nModify Updates:\n";
for (size_t i = 0; i < trigger_update.modify_updates.size(); ++i) {
std::cout << "Modify Update " << (i + 1) << ":\n";
print_terminal(trigger_update.modify_updates[i]);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////解析模板文件
//解析映射文件
bool ParseXMLConfig2(int xml_flag, XmlConfig *cfg, std::list<CTopic*> *ctopiclist, const std::string& path)
{
// 注释同原代码
const std::string strPhasic[4] = { "A", "B", "C", "T" };
const std::string strLine[4] = { "AB", "BC", "CA", "T" };
int nStart = 1, nEnd = 1;
std::string strValueTemp;
tinyxml2::XMLDocument doc;
std::string xmlFile;
if (path == "not define") {
xmlFile = FRONT_PATH + "/etc/" + DEFAULT_CONFIG_FN; //默认映射文件
std::cout << "[调试] 加载XML路径: " << xmlFile << std::endl;
} else {
xmlFile = FRONT_PATH + "/dat/" + path + ".xml";
std::cout << "[调试] 加载XML路径: " << xmlFile << std::endl;
}
auto result = doc.LoadFile(xmlFile.c_str());
if (result != tinyxml2::XML_SUCCESS) {
std::cout << "[错误] 无法打开XML文件: " << xmlFile << std::endl;
return false;
}
tinyxml2::XMLElement* root = doc.RootElement();
if (!root) {
std::cout << "[错误] XML无根节点" << std::endl;
return false;
}
for (tinyxml2::XMLElement* topicElem = root->FirstChildElement(); topicElem; topicElem = topicElem->NextSiblingElement()) {
std::string strTag = topicElem->Name();
if (strTag == "Topic") {
CTopic* topic = new CTopic();
topic->strTopic = topicElem->Attribute("name") ? topicElem->Attribute("name") : "";
ctopiclist->push_back(topic);
std::cout << "[调试] 解析Topic节点: " << topic->strTopic << std::endl;
// HISDATA、RTDATA
if (topic->strTopic == "HISDATA" || topic->strTopic == "RTDATA") {
for (tinyxml2::XMLElement* dataTypeElem = topicElem->FirstChildElement(); dataTypeElem; dataTypeElem = dataTypeElem->NextSiblingElement()) {
if (std::string(dataTypeElem->Name()) == "DataType") {
CDataType* dt = new CDataType();
dt->iDataType = dataTypeElem->IntAttribute("value", 0);
dt->type = dataTypeElem->Attribute("type") ? dataTypeElem->Attribute("type") : "";
dt->BaseFlag0 = 0;
dt->BaseFlag1 = 0;
topic->DataTypeList.push_back(dt);
// 监测点
for (tinyxml2::XMLElement* monitorElem = dataTypeElem->FirstChildElement(); monitorElem; monitorElem = monitorElem->NextSiblingElement()) {
if (std::string(monitorElem->Name()) == "Monitor") {
CMonitor* mt = new CMonitor();
mt->strMonitor = monitorElem->Attribute("name") ? monitorElem->Attribute("name") : "";
mt->type = monitorElem->Attribute("type") ? monitorElem->Attribute("type") : "";
dt->MonitorList.push_back(mt);
// 数据项
for (tinyxml2::XMLElement* itemElem = monitorElem->FirstChildElement(); itemElem; itemElem = itemElem->NextSiblingElement()) {
if (std::string(itemElem->Name()) == "Item") {
CItem* it = new CItem();
it->strItemName = itemElem->Attribute("name") ? itemElem->Attribute("name") : "";
it->type = itemElem->Attribute("type") ? itemElem->Attribute("type") : "";
if (it->strItemName == "FLAG")
it->strItemValue = itemElem->Attribute("value") ? itemElem->Attribute("value") : "";
mt->ItemList.push_back(it);
// Sequence
for (tinyxml2::XMLElement* seqElem = itemElem->FirstChildElement(); seqElem; seqElem = seqElem->NextSiblingElement()) {
if (std::string(seqElem->Name()) == "Sequence") {
std::string strPhase = seqElem->Attribute("value") ? seqElem->Attribute("value") : "";
// 读取ABC三相
if ((!xml_flag || it->strItemName != "V") && strPhase == "7") {
for (int n = 0; n < 3; ++n) {
CSequence* sq = new CSequence();
sq->strSValue = strPhase;
sq->type = seqElem->Attribute("type") ? seqElem->Attribute("type") : "";
sq->strSeq = strPhasic[n];
it->SequenceList.push_back(sq);
for (tinyxml2::XMLElement* valueElem = seqElem->FirstChildElement(); valueElem; valueElem = valueElem->NextSiblingElement()) {
if (std::string(valueElem->Name()) == "Value") {
std::string strDVName = valueElem->Attribute("name") ? valueElem->Attribute("name") : "";
std::string strDAName = valueElem->Attribute("DA") ? valueElem->Attribute("DA") : "";
// l_phs* → phsAB
if (strDAName.find("l_phs*") != std::string::npos) {
strDAName.replace(strDAName.find("l_phs"), 5, "phs");
size_t starPos = strDAName.find("*");
if (starPos != std::string::npos)
strDAName.replace(starPos, 1, strLine[n]);
} else if (strDAName.find("phs*") != std::string::npos) {
size_t starPos = strDAName.find("*");
if (starPos != std::string::npos)
strDAName.replace(starPos, 1, sq->strSeq);
}
// 谐波数据
if (strDVName.find("%") != std::string::npos && strDAName.find("%-") != std::string::npos) {
size_t firstP = strDVName.find('%'), lastP = strDVName.rfind('%');
if (firstP != std::string::npos && lastP != std::string::npos && firstP != lastP) {
std::string harmRange = strDVName.substr(firstP + 1, lastP - firstP - 1); // 0,49
size_t comma = harmRange.find(',');
if (comma != std::string::npos) {
nStart = std::stoi(harmRange.substr(0, comma));
nEnd = std::stoi(harmRange.substr(comma + 1));
strValueTemp = "%" + harmRange + "%";
}
}
// 取DA的范围
size_t lb = strDAName.find('['), rb = strDAName.find(']');
int strDAoffset = 0;
if (lb != std::string::npos && rb != std::string::npos && lb < rb) {
std::string sub = strDAName.substr(lb + 1, rb - lb - 1);
size_t dash = sub.find('-');
if (dash != std::string::npos)
strDAoffset = std::stoi(sub.substr(dash + 1));
}
int offset = valueElem->IntAttribute("Offset", 0);
for (int i = nStart; i <= nEnd; ++i) {
std::string strDVNameTemp = strDVName;
std::string strDANameTemp = strDAName;
CDataValue* dv1 = new CDataValue();
dv1->type = valueElem->Attribute("type") ? valueElem->Attribute("type") : "";
if (valueElem->Attribute("Coefficient")) {
dv1->strCoefficient = valueElem->Attribute("Coefficient");
dv1->fCoefficient = valueElem->FloatAttribute("Coefficient", 1.0f);
}
dv1->strOffset = valueElem->Attribute("Offset") ? valueElem->Attribute("Offset") : "";
dv1->iOffset = offset;
dv1->DO = valueElem->Attribute("DO") ? valueElem->Attribute("DO") : "";
size_t tmpPos = strDVNameTemp.find(strValueTemp);
if (tmpPos != std::string::npos)
strDVNameTemp.replace(tmpPos, strValueTemp.size(), std::to_string(i + offset));
size_t subPos = strDANameTemp.find('[');
size_t subPos2 = strDANameTemp.find(']');
if (subPos != std::string::npos && subPos2 != std::string::npos)
strDANameTemp.replace(subPos + 1, subPos2 - subPos - 1, std::to_string(i - strDAoffset));
dv1->strName = strDVNameTemp;
dv1->DA = strDANameTemp;
// BaseFlag, LimitUp, LimitDown
if (valueElem->Attribute("BaseFlag")) {
dv1->BaseFlag = valueElem->Attribute("BaseFlag");
if (dv1->BaseFlag == "1") dt->BaseFlag1++; else dt->BaseFlag0++;
} else {
dt->BaseFlag0++;
dv1->BaseFlag = "0";
}
dv1->LimitUp = valueElem->Attribute("LimitUp") ? valueElem->Attribute("LimitUp") : "not define";
dv1->LimitDown = valueElem->Attribute("LimitDown") ? valueElem->Attribute("LimitDown") : "not define";
if (!dv1->DO.empty() && !dv1->DA.empty())
dv1->strFullName = dv1->DO + "$" + dv1->DA;
else
dv1->strFullName = "not define";
sq->DataValueList.push_back(dv1);
// 调试打印谐波展开
std::cout << "[调试] 谐波展开DataValue: " << dv1->strName << " DA=" << dv1->DA << std::endl;
}
} else {
// 非谐波数据
CDataValue* dv2 = new CDataValue();
dv2->strName = strDVName;
dv2->type = valueElem->Attribute("type") ? valueElem->Attribute("type") : "";
if (valueElem->Attribute("Coefficient")) {
dv2->strCoefficient = valueElem->Attribute("Coefficient");
dv2->fCoefficient = valueElem->FloatAttribute("Coefficient", 1.0f);
}
dv2->DO = valueElem->Attribute("DO") ? valueElem->Attribute("DO") : "";
dv2->DA = strDAName;
if (valueElem->Attribute("BaseFlag")) {
dv2->BaseFlag = valueElem->Attribute("BaseFlag");
if (dv2->BaseFlag == "1") dt->BaseFlag1++; else dt->BaseFlag0++;
} else {
dt->BaseFlag0++;
dv2->BaseFlag = "0";
}
dv2->LimitUp = valueElem->Attribute("LimitUp") ? valueElem->Attribute("LimitUp") : "not define";
dv2->LimitDown = valueElem->Attribute("LimitDown") ? valueElem->Attribute("LimitDown") : "not define";
if (valueElem->Attribute("PltFlag") && std::string(valueElem->Attribute("PltFlag")) == "True")
dv2->bPlt = true;
else
dv2->bPlt = false;
if (!dv2->DO.empty() && !dv2->DA.empty())
dv2->strFullName = dv2->DO + "$" + dv2->DA;
else
dv2->strFullName = "not define";
sq->DataValueList.push_back(dv2);
std::cout << "[调试] 普通DataValue: " << dv2->strName << " DA=" << dv2->DA << std::endl;
}
}
}
}
}
// T相
if (strPhase == "8") {
CSequence* sq = new CSequence();
sq->strSValue = strPhase;
sq->type = seqElem->Attribute("type") ? seqElem->Attribute("type") : "";
sq->strSeq = strPhasic[3];
it->SequenceList.push_back(sq);
for (tinyxml2::XMLElement* valueElem = seqElem->FirstChildElement(); valueElem; valueElem = valueElem->NextSiblingElement()) {
if (std::string(valueElem->Name()) == "Value") {
CDataValue* dv2 = new CDataValue();
dv2->strName = valueElem->Attribute("name") ? valueElem->Attribute("name") : "";
dv2->type = valueElem->Attribute("type") ? valueElem->Attribute("type") : "";
if (valueElem->Attribute("Coefficient")) {
dv2->strCoefficient = valueElem->Attribute("Coefficient");
dv2->fCoefficient = valueElem->FloatAttribute("Coefficient", 1.0f);
}
dv2->DO = valueElem->Attribute("DO") ? valueElem->Attribute("DO") : "";
dv2->DA = valueElem->Attribute("DA") ? valueElem->Attribute("DA") : "";
if (valueElem->Attribute("BaseFlag")) {
dv2->BaseFlag = valueElem->Attribute("BaseFlag");
if (dv2->BaseFlag == "1") dt->BaseFlag1++; else dt->BaseFlag0++;
} else {
dt->BaseFlag0++;
dv2->BaseFlag = "0";
}
dv2->LimitUp = valueElem->Attribute("LimitUp") ? valueElem->Attribute("LimitUp") : "not define";
dv2->LimitDown = valueElem->Attribute("LimitDown") ? valueElem->Attribute("LimitDown") : "not define";
if (!dv2->DO.empty() && !dv2->DA.empty())
dv2->strFullName = dv2->DO + "$" + dv2->DA;
else
dv2->strFullName = "not define";
sq->DataValueList.push_back(dv2);
std::cout << "[调试] T相DataValue: " << dv2->strName << std::endl;
}
}
}
}
}
}
}
}
}
}
}
}
// RTDATASOE
else if (topic->strTopic == "RTDATASOE") {
for (tinyxml2::XMLElement* dataTypeElem = topicElem->FirstChildElement(); dataTypeElem; dataTypeElem = dataTypeElem->NextSiblingElement()) {
if (std::string(dataTypeElem->Name()) == "DataType") {
CDataType* dt = new CDataType();
dt->iDataType = dataTypeElem->IntAttribute("value", 0);
dt->type = dataTypeElem->Attribute("type") ? dataTypeElem->Attribute("type") : "";
topic->DataTypeList.push_back(dt);
for (tinyxml2::XMLElement* soeElem = dataTypeElem->FirstChildElement(); soeElem; soeElem = soeElem->NextSiblingElement()) {
if (std::string(soeElem->Name()) == "SOE") {
CEventData* ed = new CEventData();
ed->triggerFlag = soeElem->Attribute("TriggerFlag") ? soeElem->Attribute("TriggerFlag") : "";
ed->DO = soeElem->Attribute("DO") ? soeElem->Attribute("DO") : "";
ed->DA = soeElem->Attribute("DA") ? soeElem->Attribute("DA") : "";
ed->type = soeElem->Attribute("type") ? soeElem->Attribute("type") : "";
if (!ed->DO.empty() && !ed->DA.empty())
ed->strFullName = ed->DO + "$" + ed->DA;
else
ed->strFullName = "not define";
dt->SOEList.push_back(ed);
std::cout << "[调试] SOE事件: " << ed->strFullName << std::endl;
}
}
}
}
}
// SOEDATA
else if (topic->strTopic == "SOEDATA") {
for (tinyxml2::XMLElement* dataTypeElem = topicElem->FirstChildElement(); dataTypeElem; dataTypeElem = dataTypeElem->NextSiblingElement()) {
if (std::string(dataTypeElem->Name()) == "DataType") {
CEventData* ed = new CEventData();
ed->triggerFlag = dataTypeElem->Attribute("name") ? dataTypeElem->Attribute("name") : "";
ed->DO = dataTypeElem->Attribute("DO") ? dataTypeElem->Attribute("DO") : "";
ed->DA = dataTypeElem->Attribute("DA") ? dataTypeElem->Attribute("DA") : "";
ed->type = dataTypeElem->Attribute("type") ? dataTypeElem->Attribute("type") : "";
if (!ed->DO.empty() && !ed->DA.empty())
ed->strFullName = ed->DO + "$" + ed->DA;
else
ed->strFullName = "not define";
cfg->SOEList.push_back(ed);
std::cout << "[调试] SOEDATA事件: " << ed->strFullName << std::endl;
}
}
}
}
// 其他节点
else if (strTag == "WavePhasic") {
const char* flagAttr = topicElem->Attribute("Flag");
if (flagAttr)
cfg->WavePhasicFlag = flagAttr;
if (!cfg->WavePhasicFlag.empty() && cfg->WavePhasicFlag == "1") {
const char* aAttr = topicElem->Attribute("A");
if (aAttr) cfg->WavePhasicA = aAttr;
const char* bAttr = topicElem->Attribute("B");
if (bAttr) cfg->WavePhasicB = bAttr;
const char* cAttr = topicElem->Attribute("C");
if (cAttr) cfg->WavePhasicC = cAttr;
}
std::cout << "[调试] WavePhasic解析" << std::endl;
} else if (strTag == "UnitOfTime") {
const char* unitAttr = topicElem->Attribute("Unit");
if (unitAttr)
cfg->UnitOfTimeUnit = unitAttr;
std::cout << "[调试] UnitOfTime解析" << std::endl;
} else if (strTag == "ValueOfTime") {
const char* unitAttr = topicElem->Attribute("Unit");
if (unitAttr)
cfg->ValueOfTimeUnit = unitAttr;
std::cout << "[调试] ValueOfTime解析" << std::endl;
} else if (strTag == "ComtradeFile") {
const char* waveTimeFlag = topicElem->Attribute("WaveTimeFlag");
if (waveTimeFlag)
cfg->WaveTimeFlag = waveTimeFlag;
std::cout << "[调试] ComtradeFile解析" << std::endl;
} else if (strTag == "IED") {
const char* nameAttr = topicElem->Attribute("name");
if (nameAttr)
cfg->IEDname = nameAttr;
std::cout << "[调试] IED解析" << std::endl;
} else if (strTag == "LDevice") {
const char* prefixAttr = topicElem->Attribute("Prefix");
if (prefixAttr)
cfg->LDevicePrefix = prefixAttr;
std::cout << "[调试] LDevice解析" << std::endl;
}
}
return true;
}
//清理旧的映射配置
void clearXmlConfigAndTopicList(Xmldata* data) {
// 清空 XmlConfig
data->xmlcfg = XmlConfig(); // 通过重新赋值重置 xmlcfg
// 清空 topicList
std::list<CTopic*>::iterator it;
for (it = data->topicList.begin(); it != data->topicList.end(); ++it) {
delete *it; // 释放内存
}
data->topicList.clear(); // 清空链表
}
//配置映射文件单个
void Set_xml_nodeinfo_one(const std::string& dev_type)
{
bool ret = false;
// 假定 xmlinfo_list 是 std::map<std::string, Xmldata*>
if (xmlinfo_list.count(dev_type) && xmlinfo_list[dev_type] != nullptr) { // 已存在这个类型的节点
if (xmlinfo_list[dev_type]->updataflag == true) { // 需要更新
// 删除这个点的 xmlcfg 和 topicList
clearXmlConfigAndTopicList(xmlinfo_list[dev_type]);
ret = ParseXMLConfig2(
0,
&(xmlinfo_list[dev_type]->xmlcfg),
&(xmlinfo_list[dev_type]->topicList),
xmlinfo_list[dev_type]->xmlbase.MODEL_ID
);
if (!ret) {
std::cout << "!!!! this ledger xml config fail!!!!" << std::endl;
}
}
}
else {
std::cout << "xmlinfo_list not contain this devtype" << std::endl;
}
// 处理角形
if (isdelta_flag) {
if (xmlinfo_list2.count(dev_type) && xmlinfo_list2[dev_type] != nullptr) { // 已存在
if (xmlinfo_list2[dev_type]->updataflag == true) { // 需要更新
// 删除这个点的 xmlcfg 和 topicList
clearXmlConfigAndTopicList(xmlinfo_list2[dev_type]);
ret = ParseXMLConfig2(
1,
&(xmlinfo_list2[dev_type]->xmlcfg),
&(xmlinfo_list2[dev_type]->topicList),
xmlinfo_list2[dev_type]->xmlbase.MODEL_ID
);
if (!ret) {
std::cout << "!!!! this ledger xml config fail!!!!" << std::endl;
}
}
}
else {
std::cout << "xmlinfo_list2 not contain this devtype" << std::endl;
}
}
}
//配置映射文件全部
void Set_xml_nodeinfo()
{
// 配置无对应 xml 文件时的默认解析配置
if (!DEFAULT_CONFIG_FN.empty()) { //可改为在配置中读取默认映射文件名
std::string path = "not define";
ParseXMLConfig2(0, &xmlcfg, &topicList, path); // 调用 ParseXMLConfig() 解析 JiangSu_Config.xml 配置文件
if (isdelta_flag) {
ParseXMLConfig2(1, &xmlcfg2, &topicList2, path); // 角型接线
}
}
if (!xmlinfo_list.empty()) {
std::cout << "!!!!!!!!!! xmlinfo_list.size() != 0 !!!!!!!!!!!" << std::endl;
for (auto& pair : xmlinfo_list) {
const std::string& key = pair.first;
Xmldata* value = pair.second;
if (value && value->updataflag) {
ParseXMLConfig2(0, &(value->xmlcfg), &(value->topicList), value->xmlbase.MODEL_ID);
}
}
if (isdelta_flag) {
for (auto& pair : xmlinfo_list2) {
const std::string& key = pair.first;
Xmldata* value = pair.second;
if (value && value->updataflag) {
ParseXMLConfig2(1, &(value->xmlcfg), &(value->topicList), value->xmlbase.MODEL_ID);
}
}
}
} else {
std::cout << "!!!!!!!!!! xmlinfo_list.size() == 0 !!!!!!!!!!!" << std::endl;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////录播波形匹配
// 工具函数:解析 "dd/MM/yyyy,hh:mm:ss.zzz" 格式的字符串为时间戳(毫秒)
static long long parse_datetime_to_epoch_ms(const std::string& dt_str) {
std::tm tm = {};
char dummy;
int ms = 0;
std::istringstream ss(dt_str);
ss >> std::get_time(&tm, "%d/%m/%Y,%H:%M:%S") >> dummy >> ms;
if (ss.fail()) return 0;
std::time_t time_sec = std::mktime(&tm);
if (time_sec < 0) return 0;
return static_cast<long long>(time_sec) * 1000 + ms;
}
// 主函数:从 .cfg 文件中提取起始和触发时间戳(毫秒)
bool extract_timestamp_from_cfg_file(const std::string& cfg_path, long long& start_tm, long long& trig_tm) {
struct stat st;
if (stat(cfg_path.c_str(), &st) != 0) {
std::cerr << "File not found: " << cfg_path << "\n";
return false;
}
std::ifstream infile(cfg_path);
if (!infile) {
std::cerr << "Cannot open file: " << cfg_path << "\n";
return false;
}
std::string line, prev_line, current_line;
while (std::getline(infile, line)) {
// 去除前后空白
line.erase(line.find_last_not_of(" \t\r\n") + 1);
line.erase(0, line.find_first_not_of(" \t\r\n"));
std::string upper = line;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (upper == "ASCII" || upper == "BINARY") break;
prev_line = current_line;
current_line = line;
}
if (prev_line.length() > 3) prev_line = prev_line.substr(0, prev_line.length() - 3);
if (current_line.length() > 3) current_line = current_line.substr(0, current_line.length() - 3);
start_tm = parse_datetime_to_epoch_ms(prev_line);
trig_tm = parse_datetime_to_epoch_ms(current_line);
return start_tm > 0 && trig_tm > 0;
}
bool compare_qvvr_and_file(const std::string& cfg_path, std::vector<qvvr_data>& data_list,qvvr_data& matched_data) {
long long start_tm = 0;
long long trig_tm = 0;
// 提取 .cfg 文件中的时间戳
if (!extract_timestamp_from_cfg_file(cfg_path, start_tm, trig_tm)) {
std::cerr << "Failed to extract timestamp from cfg file: " << cfg_path << "\n";
return false;
}
//打印提取到的时间戳
std::cout << "[调试] 提取到的起始时间戳: " << start_tm << ", 触发时间戳: " << trig_tm << "\n";
// 遍历所有暂态事件,查找与 trig_tm 匹配的
for (auto& data : data_list) {
long long diff = static_cast<long long>(data.QVVR_time) - trig_tm;
if (std::abs(diff) <= 1) {
data.is_pair = true; // 标记为已匹配
matched_data = data; // 返回匹配到的事件
return true;
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////数据转换函数
// DataArrayItem to_json
void to_json(nlohmann::json& j, const DataArrayItem& d) {
j = nlohmann::json{
{"dataAttr", d.DataAttr},
{"dataTimeSec", d.DataTimeSec},
{"dataTimeUSec", d.DataTimeUSec},
{"dataTag", d.DataTag},
{"data", d.Data}
};
}
// MsgObj to_json
void to_json(nlohmann::json& j, const MsgObj& m) {
j = nlohmann::json{
{"clDid", m.Cldid},
{"dataType", m.DataType},
{"dataAttr", m.DataAttr},
{"dsNameIdx", m.DsNameIdx},
{"dataArray", m.DataArray}
};
}
// FullObj to_json
void to_json(nlohmann::json& j, const FullObj& f) {
j = nlohmann::json{
{"id", f.mac},
{"mid", f.Mid},
{"did", f.Did},
{"pri", f.Pri},
{"type", f.Type},
{"msg", f.Msg}
};
}
std::string generate_json(
const std::string mac,
int Mid, //需应答的报文订阅者收到后需以此ID应答无需应答填入“-1”
int Did, //设备唯一标识Ldid填入0代表Ndid。
int Pri, //报文处理的优先级
int Type, //消息类型
int Cldid, //逻辑子设备ID0-逻辑设备本身,无填-1
int DataType, //数据类型0-表示以数据集方式上送
int DataAttr, //数据属性无“0”、实时“1”、统计“2”等。
int DsNameIdx, //数据集序号(以数据集方式上送),无填-1
const std::vector<DataArrayItem>& dataArray //数据数组。
) {
FullObj fobj;
fobj.mac = mac;
fobj.Mid = Mid;
fobj.Did = Did;
fobj.Pri = Pri;
fobj.Type = Type;
fobj.Msg.Cldid = Cldid;
fobj.Msg.DataType = DataType;
fobj.Msg.DataAttr = DataAttr;
fobj.Msg.DsNameIdx = DsNameIdx;
fobj.Msg.DataArray = dataArray;
nlohmann::json j = fobj;
return j.dump(); // 输出标准 json 字符串
}
void upload_data_test(){
std::vector<DataArrayItem> arr;
arr.push_back({1, 1725477660, 0, 1, "xxxx"}); //数据属性 -1-无, 0-“Rt”,1-“Max”,2-“Min”,3-“Avg”,4-“Cp95”
//数据时标相对1970年的秒无效填入“-1”
//数据时标,微秒钟,无效填入“-1”
//数据标识1-标识数据异常
//数据序列数据集上送时将二进制数据流转换成Base64字符串其他数据为object
arr.push_back({2, 1691741340, 0, 1, "yyyy"});
std::string js = generate_json(
"123",-1, 1, 1, 4866, 1, 0, 2, 1, arr
);
std::cout << js << std::endl;
queue_data_t data;
data.monitor_no = 1;
data.strTopic = TOPIC_ALARM;
data.strText = js;
data.mp_id = "test";
data.tag = G_ROCKETMQ_TAG_TEST;
data.key = G_ROCKETMQ_KEY_TEST;
std::lock_guard<std::mutex> lock(queue_data_list_mutex);
queue_data_list.push_back(data);
}
//////////////////////////////////////////////////////////////////////////////////////////////////台账赋值给通信
std::vector<DeviceInfo> GenerateDeviceInfoFromLedger(const std::vector<terminal_dev>& terminal_devlist) {
std::lock_guard<std::mutex> lock(ledgermtx);
std::vector<DeviceInfo> devices;
for (const auto& terminal : terminal_devlist) {
DeviceInfo device;
device.device_id = terminal.terminal_id;
device.name = terminal.terminal_name;
device.model = terminal.dev_type;
device.mac = terminal.addr_str;
device.status = 1;
for (const auto& monitor : terminal.line) {
PointInfo point;
point.point_id = monitor.monitor_id;
point.name = monitor.monitor_name;
point.device_id = terminal.terminal_id;
point.PT1 = monitor.PT1;
point.PT2 = monitor.PT2;
point.CT1 = monitor.CT1;
point.CT2 = monitor.CT2;
point.strScale = monitor.voltage_level;
point.nCpuNo = std::stoi(monitor.logical_device_seq);
point.nPTType = std::stoi(monitor.terminal_connect);
device.points.push_back(point);
}
devices.push_back(device);
}
return devices;
}
////////////////////////////////////////////////////////////////////////////////////////////////////录波文件通知
bool assign_qvvr_file_list(const std::string& id,
ushort nCpuNo,
const std::vector<std::string>& file_list_raw) {
// ★新增:台账加锁(若上层已有锁,可移除本行)
std::lock_guard<std::mutex> lk(ledgermtx);
std::vector<std::string> file_names;
// 1. 提取文件名部分
for (const auto& full_path_raw : file_list_raw) {
std::string full_path = sanitize(full_path_raw); // ★修改:清洗入参路径
size_t pos = full_path.find_last_of("/\\");
std::string name = (pos != std::string::npos && pos + 1 < full_path.size())
? full_path.substr(pos + 1)
: full_path;
name = sanitize(name); // ★修改:清洗提取的文件名
file_names.push_back(name);
}
// ★可选:去重(如果 file_list_raw 里可能有重复)
std::sort(file_names.begin(), file_names.end());
file_names.erase(std::unique(file_names.begin(), file_names.end()), file_names.end());
// 2. 遍历终端
for (auto& dev : terminal_devlist) {
if (dev.terminal_id == id) { // 根据终端id匹配终端
for (auto& monitor : dev.line) {
try {
// ★修改:清洗 logical_device_seq 再进行转换
std::string seq_str = sanitize(monitor.logical_device_seq);
ushort monitor_seq = static_cast<ushort>(std::stoi(seq_str));
if (monitor_seq == nCpuNo) { // 根据监测点编号匹配监测点
// 构造 qvvr_file
qvvr_file qfile;
qfile.file_name.assign(file_names.begin(), file_names.end()); // 终端文件列表(已清洗)
qfile.is_download = false;
qfile.is_pair = false;
qfile.file_time_count = 0;
qfile.used_status = true;
// 添加到唯一的 qvvrevent
monitor.qvvrevent.qvvrfile.push_back(std::move(qfile)); // 记录暂态文件组
return true;
}
} catch (...) {
continue;
}
}
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////下载成功通知
//提取下载路径的文件名
std::string extract_filename1(const std::string& path) {
size_t pos = path.find_last_of("/\\");
return (pos != std::string::npos) ? path.substr(pos + 1) : path;
}
// ★新增dirname返回“目录/”(保留末尾斜杠;若没有目录则返回空串)
static inline std::string dirname_with_slash(const std::string& path) {
size_t pos = path.find_last_of("/\\");
if (pos == std::string::npos) return std::string{};
return path.substr(0, pos + 1);
}
//发送匹配的所有录波文件
bool SendAllQvvrFiles(qvvr_file& qfile, std::string& out_wavepath) {
std::vector<std::string> wavepaths;
std::string first_wavepath;
bool send_success = true;
for (const auto& file_localpath : qfile.file_download) {
std::string file_cloudpath = "comtrade/" + dirname_with_slash(file_localpath);
std::string wavepath_result;
// 发送本地文件到远端,返回 wavepath
SOEFileWeb(const_cast<std::string&>(file_localpath), file_cloudpath, wavepath_result);
// 如果失败,重发一次
if (wavepath_result.empty()) {
std::cerr << "[SOEFileWeb] Warning: first send failed for file: " << file_localpath << ", retrying...\n";
SOEFileWeb(const_cast<std::string&>(file_localpath), file_cloudpath, wavepath_result);
}
if (wavepath_result.empty()) {
send_success = false;
std::cerr << "[SOEFileWeb] Failed: wavepath empty for file: " << file_localpath << "\n";
break;
}
if (wavepaths.empty()) {
first_wavepath = wavepath_result;
} else if (wavepath_result != first_wavepath) {
send_success = false;
std::cerr << "[SOEFileWeb] Mismatch wavepath: " << wavepath_result
<< " vs " << first_wavepath << "\n";
break;
}
wavepaths.push_back(wavepath_result);
}
// 检查数量是否一致
if (!send_success || wavepaths.size() != qfile.file_download.size()) {
std::cerr << "[SOEFileWeb] Failed to send all qvvr files. "
<< "Sent: " << wavepaths.size()
<< ", Expected: " << qfile.file_download.size() << "\n";
return false;
}
out_wavepath = first_wavepath; // 返回统一的 wavepath
std::cout << "[SOEFileWeb] Success: all files sent for qfile. Wavepath = "
<< first_wavepath << "\n";
return true;
}
//文件下载结束接口
bool update_qvvr_file_download(const std::string& filename_with_mac_in, const std::string& terminal_id) {
// ★ 先把原始入参清洗
std::string filename_with_mac = sanitize(filename_with_mac_in);
std::cout << "[update_qvvr_file_download] raw=" << filename_with_mac_in
<< " | sanitized=" << filename_with_mac
<< " | terminal_id=" << terminal_id << std::endl;
//台账加锁
std::unique_lock<std::mutex> lock(ledgermtx);
// 去除 mac 路径前缀,仅保留文件名
std::string filename = sanitize(extract_filename1(filename_with_mac));
// 提取逻辑序号(如 PQ_PQLD1 → 1
size_t under_pos1 = filename.find('_');
if (under_pos1 == std::string::npos) {
std::cout << "[DEBUG] 未找到 '_'filename=" << filename
<< "under_pos=npos返回 false\n";
return false;
}
size_t under_pos2 = filename.find('_', under_pos1 + 1);
std::string type_part = (under_pos2 == std::string::npos)
? filename.substr(0, under_pos1) // 兜底:只有一个下划线
: filename.substr(0, under_pos2); // 取到第二个下划线(得到 PQ_PQLD1
std::cout << "[DEBUG] type_part=" << type_part
<< " (under_pos1=" << under_pos1
<< ", under_pos2=" << under_pos2 << ")\n";
size_t num_start = type_part.find_last_not_of("0123456789");
if (num_start == std::string::npos || num_start + 1 >= type_part.size()) {
std::cout << "[DEBUG] 数字起始位置异常num_start=" << num_start
<< "type_part.size()=" << type_part.size()
<< "type_part=\"" << type_part << "\",返回 false\n";
return false;
}
std::string seq_str = type_part.substr(num_start + 1);
unsigned short logical_seq = static_cast<unsigned short>(std::stoul(seq_str));
std::cout << "[DEBUG] 解析到 logical_seq=" << logical_seq << "\n";
//找终端
for (auto& dev : terminal_devlist) {
if (dev.terminal_id != terminal_id) {
std::cout << "[cmp-terminal-id][NOT-MATCH]"
<< " dev_id=" << dev.terminal_id
<< " target_id=" << terminal_id
<< std::endl;
continue;
}
//找监测点
for (auto& monitor : dev.line) {
try {
// 将监测点台账中的 logical_device_seq 转换为数字进行匹配
ushort monitor_seq = static_cast<ushort>(std::stoi(monitor.logical_device_seq));
if (monitor_seq != logical_seq) {
// ★新增:不匹配时对比打印
std::cout << "[cmp-monitor-seq][NOT-MATCH]"
<< " monitor_id=" << monitor.monitor_id
// ★ 这里之前打印的是 seq_str容易误导。改为 ledger 的原始串:
<< " seq_in_ledger_raw=\"" << monitor.logical_device_seq << "\""
<< " parsed=" << monitor_seq
<< " target_seq=" << logical_seq
<< std::endl;
continue;
}
else{
std::cout << "[cmp-monitor-seq][MATCH!!!]"
<< " monitor_id=" << monitor.monitor_id
<< " seq_in_ledger_raw=\"" << monitor.logical_device_seq << "\""
<< " parsed=" << monitor_seq
<< " target_seq=" << logical_seq
<< std::endl;
}
} catch (const std::exception& e) {
// ★新增:解析失败详细原因
std::cout << "[cmp-monitor-seq][PARSE-FAIL]"
<< " monitor_id=" << monitor.monitor_id
<< " seq_in_ledger=\"" << monitor.logical_device_seq << "\""
<< " err=" << e.what()
<< std::endl;
continue; // logical_device_seq 非法,跳过
}catch (...) {
// ★新增:未知异常
std::cout << "[cmp-monitor-seq][PARSE-FAIL]"
<< " monitor_id=" << monitor.monitor_id
<< " seq_in_ledger=\"" << monitor.logical_device_seq << "\""
<< " err=<unknown>"
<< std::endl;
continue; // logical_device_seq 非法,跳过
}
// 匹配监测点下 qvvrfile 中的 file_name
for (size_t i = 0; i < monitor.qvvrevent.qvvrfile.size(); ++i) {
auto& qfile = monitor.qvvrevent.qvvrfile[i];
// file_name 中是文件名,需与提取的 filename 比较
auto it = std::find(qfile.file_name.begin(), qfile.file_name.end(), filename);
//找到匹配文件名
if (it != qfile.file_name.end()) {
// 添加到 file_download记录完整路径避免重复
if (std::find(qfile.file_download.begin(), qfile.file_download.end(), filename_with_mac) == qfile.file_download.end()) {
std::cout << "[update_qvvr_file_download] Adding downloaded file: " << filename_with_mac << std::endl;
qfile.file_download.push_back(filename_with_mac);
}
qfile.file_time_count = 0; // 任一录波文件下载后,计时归零
// file_download 中是完整路径,需提取文件名后与 file_name 做集合比较
std::set<std::string> s_name(qfile.file_name.begin(), qfile.file_name.end());
std::set<std::string> s_down;
for (const auto& path : qfile.file_download) {
s_down.insert(sanitize(extract_filename1(path))); // 提取每个路径中的文件名
}
//打印s_name和s_down内容
std::cout << "[update_qvvr_file_download] Expected files (file_name): ";
for (const auto& fn : s_name) std::cout << fn << " ";
std::cout << std::endl;
std::cout << "[update_qvvr_file_download] Downloaded files (file_download): ";
for (const auto& fn : s_down) std::cout << fn << " ";
std::cout << std::endl;
// 检查 file_download 是否与 file_name 完全一致(集合相同)
if (s_name == s_down) {
std::cout << "[update_qvvr_file_download] All files downloaded for qfile in logical_seq=" << logical_seq << std::endl;
qfile.is_download = true; // 全部下载完成
// 找到其中的 .cfg 文件进行匹配
for (const auto& fpath : qfile.file_download) {
std::string fname = extract_filename1(fpath);
if (fname.size() >= 4 && fname.substr(fname.size() - 4) == ".cfg") {
// 提取文件时标和监测点事件的时标匹配
qvvr_data matched;
if (compare_qvvr_and_file(fpath, monitor.qvvrevent.qvvrdata,matched)) {
qfile.is_pair = true; // 文件与事件匹配成功
// ★新增:上传前拷贝“将要上传的文件列表”,避免锁外用容器引用
std::vector<std::string> files_to_send(qfile.file_download.begin(),
qfile.file_download.end());
// ★新增:构造一个临时 qvvr_file仅用于上传不改动原结构
qvvr_file tmp_send;
tmp_send.file_download.assign(files_to_send.begin(), files_to_send.end());
// 发送所有文件(已下载完成)
std::string wavepath;
// ★在解锁前,备份“签名”,用于回锁后定位同一个 qfile
std::set<std::string> sig_names(qfile.file_name.begin(), qfile.file_name.end());
std::set<std::string> sig_downs(qfile.file_download.begin(), qfile.file_download.end());
// ★修改:把上传与上送 JSON 放到“解锁区间”
lock.unlock(); // ★新增:提前解锁
if (SendAllQvvrFiles(qfile, wavepath)) {
//文件发送成功后更新事件
transfer_json_qvvr_data(terminal_id,
logical_seq,
matched.QVVR_Amg,
matched.QVVR_PerTime,
matched.QVVR_time,
matched.QVVR_type,
matched.phase,
wavepath);
// ★新增:上传成功后再加锁,准备修改台账
lock.lock();
// 删除上传成功的文件
for (const auto& uploaded_file : qfile.file_download) {
if (std::remove(uploaded_file.c_str()) != 0) {
std::cerr << "[Cleanup] Failed to delete file: " << uploaded_file << "\n";
} else {
std::cout << "[Cleanup] Deleted uploaded file: " << uploaded_file << "\n";
}
}
// ★替换原来的 i<size 判断为:按签名查找当前容器里的那一条
auto it_qf = std::find_if(monitor.qvvrevent.qvvrfile.begin(),
monitor.qvvrevent.qvvrfile.end(),
[&](const qvvr_file& x){
std::set<std::string> n(x.file_name.begin(), x.file_name.end());
std::set<std::string> d(x.file_download.begin(), x.file_download.end());
return n==sig_names && d==sig_downs;
});
if (it_qf != monitor.qvvrevent.qvvrfile.end()) {
monitor.qvvrevent.qvvrfile.erase(it_qf); // ✔ 删到同一条
} else {
std::cerr << "[Cleanup] qvvrfile changed; target group not found, skip erase\n";
}
//清除暂态事件
auto it = std::find_if(
monitor.qvvrevent.qvvrdata.begin(),
monitor.qvvrevent.qvvrdata.end(),
[&](const qvvr_data& d) {
return d.QVVR_time == matched.QVVR_time;
});
if (it != monitor.qvvrevent.qvvrdata.end()) {
monitor.qvvrevent.qvvrdata.erase(it);
}
}
else {
lock.lock(); // ★新增:失败时补回锁
std::cerr << "[update_qvvr_file_download] Failed to send qvvr files for logical_seq=" << logical_seq << std::endl;
}
}
else {
std::cout << "[update_qvvr_file_download] No matching qvvr_data found for cfg file: " << fpath << std::endl;
}
break; // 只处理第一个 cfg 文件
}
}
}
else{
std::cout << "qvvr file still imcomplete!!!" << std::endl;
}
lock.unlock();
return true; // 当前文件处理成功
}
}
std::cout << "file name doesnt match any file in this monitor!!!" << std::endl;
}
}
lock.unlock();
return false; // 未匹配到终端ID或逻辑序号对应的监测点
}
////////////////////////////////////////////////////////////////////////////////////////提取mac
std::string normalize_mac(const std::string &mac) {
std::string res;
res.reserve(mac.size());
for (char c : mac) {
if (c != '-' && c != ':' && c != ' ')
res.push_back(c);
}
return res;
}
////////////////////////////////////////////////////////////////////////////////////////目录信息发送接口函数
bool send_file_list(terminal_dev* dev, const std::vector<tag_dir_info>& FileList) {
if (!dev) {
std::cerr << "[send_file_list_locked] dev=nullptr\n";
return false;
}
// 判断 isbusy==1 且 busytype==READING_FILEMENU
if (dev->isbusy != 1 || dev->busytype != static_cast<int>(DeviceState::READING_FILEMENU)) {
std::cerr << "[send_file_list] device not in READING_FILEMENU state." << std::endl;
return false;
}
// 构造 JSON 报文
nlohmann::json j;
j["guid"] = dev->guid;
j["FrontId"] = FRONT_INST; // 这里填你的前置机id
j["Node"] = g_front_seg_index; // 节点号
j["Dev_mac"] = normalize_mac(dev->addr_str); // addr_str 存的是 MAC
// 构造 DirInfo 数组
nlohmann::json dirArray = nlohmann::json::array();
for (const auto &f : FileList) {
nlohmann::json item;
item["Name"] = f.name;
item["Type"] = (f.flag == 0) ? "dir" : "file";
item["Size"] = f.size;
dirArray.push_back(item);
}
// 构造 Detail 部分
nlohmann::json detail;
detail["Type"] = 0x2131; // 读取目录
detail["Msg"] = { {"DirInfo", dirArray} };
detail["Code"] = 200; // 请求成功
// 放到顶层
j["Detail"] = detail;
// 打印调试
std::cout << j.dump(4) << std::endl;
// ---- 入队发送 ----
queue_data_t connect_info;
connect_info.strTopic = Topic_Reply_Topic;
connect_info.strText = j.dump(); // 序列化为字符串
connect_info.tag = Topic_Reply_Tag;
connect_info.key = Topic_Reply_Key;
{
std::lock_guard<std::mutex> lock(queue_data_list_mutex);
queue_data_list.push_back(std::move(connect_info));
}
// 调试打印
std::cout << "[send_file_list] queued: " << j.dump() << std::endl;
//发送后清除guid和标志
if (dev->isbusy > 0) {
dev->isbusy--;
}
if(dev->isbusy == 0){
dev->guid.clear();
dev->busytype = 0;
}
return true;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////检查云前置终端的mq业务超时
std::string get_type_by_state(int state) {
switch (state) {
case static_cast<int>(DeviceState::IDLE):
return "空闲状态";
case static_cast<int>(DeviceState::READING_STATS):
return "读取统计数据";
case static_cast<int>(DeviceState::READING_STATS_TIME):
return "读取统计时间";
case static_cast<int>(DeviceState::READING_REALSTAT):
return "读取实时数据";
case static_cast<int>(DeviceState::READING_EVENTFILE):
return "暂态波形文件下载";
case static_cast<int>(DeviceState::READING_FILEMENU):
return "读取文件目录";
case static_cast<int>(DeviceState::READING_FILEDATA):
return "读取文件数据";
case static_cast<int>(DeviceState::READING_FIXEDVALUE):
return "读取测点定值";
case static_cast<int>(DeviceState::READING_FIXEDVALUEDES):
return "读取测点定值描述";
case static_cast<int>(DeviceState::SET_FIXEDVALUE):
return "设置测点定值";
case static_cast<int>(DeviceState::READING_INTERFIXEDVALUE):
return "读取内部定值";
case static_cast<int>(DeviceState::READING_INTERFIXEDVALUEDES):
return "读取内部定值描述";
case static_cast<int>(DeviceState::READING_CONTROLWORD):
return "读取控制字描述";
case static_cast<int>(DeviceState::SET_INTERFIXEDVALUE):
return "设置内部定值";
case static_cast<int>(DeviceState::READING_RUNNINGINFORMATION_1):
return "读取装置运行信息(主动触发)";
case static_cast<int>(DeviceState::READING_RUNNINGINFORMATION_2):
return "读取装置运行信息(定时执行)";
case static_cast<int>(DeviceState::READING_DEVVERSION):
return "读取装置版本配置信息";
case static_cast<int>(DeviceState::SET_RIGHTTIME):
return "设置装置对时";
case static_cast<int>(DeviceState::READING_EVENTLOG):
return "补招事件日志";
case static_cast<int>(DeviceState::READING_STATSFILE):
return "补招稳态数据文件";
case static_cast<int>(DeviceState::CUSTOM_ACTION):
return "自定义动作";
default:
return "未知业务"; // 未匹配的类型
}
}
// 定时检查业务超时
void check_device_busy_timeout()
{
std::lock_guard<std::mutex> lock(ledgermtx);
for (auto &dev : terminal_devlist)
{
if (dev.isbusy != 0) // 有业务在进行
{
dev.busytimecount++;
if (dev.busytype == static_cast<int>(DeviceState::READING_FILEDATA) || dev.busytype == static_cast<int>(DeviceState::READING_STATSFILE)) //下载文件业务
{
if (dev.busytimecount > 60)
{
std::cout << "[Timeout] Device " << dev.terminal_id
<< " busytype=READING_FILEDATA 超时("
<< dev.busytimecount << "s)" << std::endl;
//标记当前 RUNNING 的首条为 FAILED或 TIMEOUT而不是只清设备状态
bool marked = false;
if (dev.busytype == static_cast<int>(DeviceState::READING_STATSFILE)) {
for (auto& lm : dev.line) {
if (!lm.recall_list_static.empty() &&
lm.recall_list_static.front().recall_status == static_cast<int>(RecallStatus::RUNNING)) {
lm.recall_list_static.front().recall_status = static_cast<int>(RecallStatus::FAILED);
marked = true;
break;
}
}
}
//发送超时响应
//send_reply_to_cloud(static_cast<int>(ResponseCode::TIMEOUT),dev.terminal_id,get_type_by_state(dev.busytype),dev.guid,dev.mac);
send_reply_to_queue(dev.guid, static_cast<int>(ResponseCode::TIMEOUT),
"终端 id: " + dev.terminal_id + "进行业务:" + get_type_by_state(dev.busytype) +"超时,停止该业务处理");
// 超时清空状态
//若还有未处理条目,则仅复位计时,不清设备状态,交给 check_recall_event() 弹掉 FAILED 并继续下一条
bool any_non_empty = false;
for (auto& lm : dev.line) {
if (!lm.recall_list_static.empty()) { any_non_empty = true; break; }
}
//补招业务的话
if (any_non_empty && dev.busytype == static_cast<int>(DeviceState::READING_STATSFILE)) {
dev.busytimecount = 0; // 复位计时
dev.isbusy = 1; // 仍视为在补招流程中
dev.busytype = static_cast<int>(DeviceState::READING_STATSFILE);
//不要清 dev.guid让回调/通知能“对上号” //因为超时后发送结果需要guid所以不能清在补招任务中pop就行
} else {
//队列都空了,或者是正在进行其他业务才清设备状态
dev.guid.clear();
dev.busytype = static_cast<int>(DeviceState::IDLE);
dev.isbusy = 0;
dev.busytimecount = 0;
}
}
}
else //其他业务包括补招日志都是20s一条一问一答的时间
{
if (dev.busytimecount > 20)
{
std::cout << "[Timeout] Device " << dev.terminal_id
<< " busytype=" << dev.busytype
<< " 超时(" << dev.busytimecount << "s)" << std::endl;
//标记当前 RUNNING 的首条为 FAILED或 TIMEOUT而不是只清设备状态
bool marked = false;
if (dev.busytype == static_cast<int>(DeviceState::READING_EVENTLOG)) {
for (auto& lm : dev.line) {
if (!lm.recall_list.empty() &&
lm.recall_list.front().recall_status == static_cast<int>(RecallStatus::RUNNING)) {
lm.recall_list.front().recall_status = static_cast<int>(RecallStatus::FAILED);
marked = true;
break;
}
}
}
//发送超时响应
//send_reply_to_cloud(static_cast<int>(ResponseCode::TIMEOUT),dev.terminal_id,get_type_by_state(dev.busytype),dev.guid,dev.mac);
send_reply_to_queue(dev.guid, static_cast<int>(ResponseCode::TIMEOUT),
"终端 id: " + dev.terminal_id + "进行业务:" + get_type_by_state(dev.busytype) +"超时,停止该业务处理");
// 超时清空状态
//若还有未处理条目,则仅复位计时,不清设备状态,交给 check_recall_event() 弹掉 FAILED 并继续下一条
bool any_non_empty = false;
for (auto& lm : dev.line) {
if (!lm.recall_list.empty()) { any_non_empty = true; break; }
}
//补招业务的话
if (any_non_empty && dev.busytype == static_cast<int>(DeviceState::READING_EVENTLOG)) {
dev.busytimecount = 0; // 复位计时
dev.isbusy = 1; // 仍视为在补招流程中
dev.busytype = static_cast<int>(DeviceState::READING_EVENTLOG);
//不要清 dev.guid让回调/通知能“对上号” //因为超时后发送结果需要guid所以不能清在补招任务中pop就行
} else {
//队列都空了,或者是正在进行其他业务才清设备状态
dev.guid.clear();
dev.busytype = static_cast<int>(DeviceState::IDLE);
dev.isbusy = 0;
dev.busytimecount = 0;
}
}
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////// 一分钟调用一次:检查 qvvr_file 超时并备份
static bool ensure_dir_exists(const std::string &path)
{
struct stat st;
if (stat(path.c_str(), &st) == 0) {
if (S_ISDIR(st.st_mode)) {
return true; // 已存在
}
return false; // 存在但不是目录
}
// 不存在则创建
if (mkdir(path.c_str(), 0755) == 0) {
return true;
}
return false;
}
void check_and_backup_qvvr_files() {
std::lock_guard<std::mutex> lock(ledgermtx);
const std::string data_dir = FRONT_PATH + "/data";
const std::string backup_dir = data_dir + "/comtrade_bak";
if (!ensure_dir_exists(data_dir) && !ensure_dir_exists(data_dir)) {
std::cerr << "[check_and_backup_qvvr_files] 创建 data 目录失败\n";
return;
}
if (!ensure_dir_exists(backup_dir) && !ensure_dir_exists(backup_dir)) {
std::cerr << "[check_and_backup_qvvr_files] 创建 comtrade_bak 目录失败\n";
return;
}
for (auto &dev : terminal_devlist) {
for (auto &line : dev.line) {
// 用迭代器遍历,允许在循环中 erase 当前元素
for (std::vector<qvvr_file>::iterator qit = line.qvvrevent.qvvrfile.begin();
qit != line.qvvrevent.qvvrfile.end(); /* no ++ here */) {
qvvr_file &qfile = *qit;
if (!qfile.used_status) {
++qit;
continue;
}
++qfile.file_time_count; // 每分钟+1
// 超时阈值:>10 分钟
if (qfile.file_time_count > 10) {
std::cout << "[Qvvr Timeout] dev=" << dev.terminal_id
<< " -> move files to: " << backup_dir << std::endl;
// 移动该记录内的所有文件
for (std::list<std::string>::const_iterator it = qfile.file_download.begin();
it != qfile.file_download.end(); ++it) {
const std::string &src = *it;
const size_t pos = src.find_last_of("/\\");
const std::string base = (pos == std::string::npos) ? src : src.substr(pos + 1);
const std::string dst = backup_dir + "/" + base;
if (rename(src.c_str(), dst.c_str()) != 0) {
std::cerr << " [ERROR] 移动失败: " << src
<< " -> " << dst << " , " << strerror(errno) << std::endl;
} else {
std::cout << " moved: " << src << " -> " << dst << std::endl;
}
}
// ✅ 直接从 qvvrfile 向量中删除这条记录
qit = line.qvvrevent.qvvrfile.erase(qit);
// 注意:不再对 qfile 读写,因为它已被删除
continue; // 不自增,由 erase 返回的新迭代器继续
}
++qit; // 正常前进
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////定时清理内存的暂态事件
// 每天执行一次删除“is_pair=false 且 QVVR_time < 昨天 00:00:00(毫秒)”的 qvvr_data
void cleanup_old_unpaired_qvvr_events() {
const uint64_t cutoff_ms = get_local_midnight_ms_prev_day();
std::lock_guard<std::mutex> lk(ledgermtx); // ★新增:加锁保护台账
size_t total_removed = 0;
for (auto& dev : terminal_devlist) {
for (auto& lm : dev.line) {
auto& vec = lm.qvvrevent.qvvrdata;
const size_t before = vec.size();
// 删除条件:未配对 且 发生时间 < 昨天 00:00:00本地时区毫秒
vec.erase(std::remove_if(vec.begin(), vec.end(),
[&](const qvvr_data& d){
return (d.is_pair == false) && (d.QVVR_time < cutoff_ms);
}),
vec.end());
const size_t removed = before - vec.size();
if (removed > 0) {
total_removed += removed;
std::cout << "[cleanup_qvvr] dev=" << dev.terminal_id
<< " mon=" << lm.monitor_id
<< " removed=" << removed
<< " cutoff_ms=" << cutoff_ms
<< " (prev 00:00 local)" << std::endl;
}
}
}
std::cout << "[cleanup_qvvr] total_removed=" << total_removed << std::endl;
}
/////////////////////////////////////////////////////////////////////////////////////////定值信息发送接口函数
bool save_set_value(const std::string &dev_id, unsigned char mp_index, const std::vector<float> &fabsf) {
std::lock_guard<std::mutex> lock(ledgermtx);
// 1. 找到对应 terminal_dev
auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev &dev) { return dev.terminal_id == dev_id; });
if (it == terminal_devlist.end()) {
std::cerr << "[send_set_reply] device not found: " << dev_id << std::endl;
return false;
}
terminal_dev &dev = *it;
// 2. 检查状态
if (dev.isbusy != 2 || dev.busytype != static_cast<int>(DeviceState::READING_FIXEDVALUE)) {
std::cerr << "[send_set_reply] device not in READING_FIXEDVALUE state." << std::endl;
return false;
}
// 3. 遍历监测点,找到 logical_device_seq == mp_index 的监测点
bool found = false;
for (auto &mon : dev.line) {
if (std::atoi(mon.logical_device_seq.c_str()) == static_cast<int>(mp_index)) {
// ★ 清理原有的 set_values
mon.set_values.clear();
// ★ 将 fabsf 依次存入该监测点的 set_values
for (const auto &val : fabsf) {
mon.set_values.push_back(val);
}
found = true;
break;
}
}
if (!found) {
std::cerr << "[send_set_reply] monitor with seq=" << (int)mp_index
<< " not found in terminal " << dev_id << std::endl;
return false;
}
// 4. 状态递减
dev.isbusy--;
return true;
}
bool save_internal_value(const std::string &dev_id, const std::vector<ushort> &fabsf) {
// 找到对应 terminal_dev
std::lock_guard<std::mutex> lock(ledgermtx);
auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev &dev) { return dev.terminal_id == dev_id; });
if (it == terminal_devlist.end()) {
std::cerr << "[send_set_reply] device not found: " << dev_id << std::endl;
return false;
}
terminal_dev &dev = *it;
// 判断 isbusy==3 且 busytype==READING_INTERFIXEDVALUE
if (dev.isbusy != 3 || dev.busytype != static_cast<int>(DeviceState::READING_INTERFIXEDVALUE)) {
std::cerr << "[send_set_reply] device not in READING_INTERFIXEDVALUE state." << std::endl;
return false;
}
// ★ 新增:清理原有的内部定值列表
dev.internal_values.clear();
//将值严格按顺序存入list中
for (const auto &val : fabsf) {
dev.internal_values.push_back(val);
}
dev.isbusy--;
return true;
}
bool save_internal_info(const std::string &dev_id, const std::vector<NameFixValue> &fixValueList) {
// 找到对应 terminal_dev
std::lock_guard<std::mutex> lock(ledgermtx);
auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev &dev) { return dev.terminal_id == dev_id; });
if (it == terminal_devlist.end()) {
std::cerr << "[send_set_reply] device not found: " << dev_id << std::endl;
return false;
}
terminal_dev &dev = *it;
// 判断 isbusy==2 且 busytype==READING_INTERFIXEDVALUE
if (dev.isbusy != 2 || dev.busytype != static_cast<int>(DeviceState::READING_INTERFIXEDVALUE)) {
std::cerr << "[send_set_reply] device not in READING_INTERFIXEDVALUE state." << std::endl;
return false;
}
// ★ 新增:清理原有的内部定值列表
dev.dz_internal_info_list.clear();
//将值严格按顺序存入list中
for (const auto &info : fixValueList) {
dev.dz_internal_info_list.push_back(info);
}
dev.isbusy--;
return true;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////回复定值读取响应
bool send_set_value_reply(const std::string &dev_id, unsigned char mp_index, const std::vector<DZ_TAB_STRUCT> &dz_info) {
std::lock_guard<std::mutex> lock(ledgermtx);
// 1) 找终端
auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev &d) { return d.terminal_id == dev_id; });
if (it == terminal_devlist.end()) {
std::cerr << "[send_set_value_reply] device not found: " << dev_id << std::endl;
return false;
}
terminal_dev &dev = *it;
// 2) 校验状态:发送“定值读取结果”回复,应处于 READING_FIXEDVALUEisbusy == 1
if (dev.isbusy != 1 || dev.busytype != static_cast<int>(DeviceState::READING_FIXEDVALUE)) { //定值读取
std::cerr << "[send_set_value_reply] device not in READING_FIXEDVALUE state." << std::endl;
return false;
}
// 3) 定位监测点mp_index ∈ [1,6]),与 logical_device_seq 比对
ledger_monitor *pMon = nullptr;
for (auto &mon : dev.line) {
if (std::atoi(mon.logical_device_seq.c_str()) == static_cast<int>(mp_index)) {
pMon = &mon;
break;
}
}
if (!pMon) {
std::cerr << "[send_set_value_reply] monitor with seq=" << (int)mp_index
<< " not found in terminal " << dev_id << std::endl;
return false;
}
//将dz_info存入监测点
pMon->dz_info_list.clear();
for (const auto &dz : dz_info) {
pMon->dz_info_list.push_back(dz);
}
// 4) 取该监测点的 set_values严格按顺序用于 DZ_Value
std::vector<float> ordered_vals;
ordered_vals.reserve(pMon->set_values.size());
for (float v : pMon->set_values) ordered_vals.push_back(v);
if (ordered_vals.empty()) {
std::cerr << "[send_set_value_reply] monitor seq=" << (int)mp_index
<< " has empty set_values." << std::endl;
return false;
}
// 5) 生成 JSON结构严格贴合你给的样例
nlohmann::json j;
// 顶层
j["guid"] = dev.guid;
j["FrontId"] = FRONT_INST; // 你的前置机 IP项目已有常量/变量)
j["Node"] = g_front_seg_index; // 节点号(项目已有变量)
j["Dev_mac"] = normalize_mac(dev.addr_str);
// Detail
nlohmann::json detail;
detail["Type"] = 0x2106; // 设备数据
// Msg
nlohmann::json msg;
msg["Cldid"] = mp_index; //测点序号
msg["DataType"] = 0x0C; //定值
// DataArray对象数组逐个填充DZ_Value 严格按 set_values 顺序
nlohmann::json dataArray = nlohmann::json::array();
const size_t n_meta = dz_info.size();
const size_t n_vals = ordered_vals.size();
const size_t n = std::min(n_meta, n_vals); // 以两者较短长度为准
if (n_meta != n_vals) {
std::cerr << "[send_set_value_reply] warn: dz_info size(" << n_meta
<< ") != set_values size(" << n_vals << "), will emit " << n << " items.\n";
return false; // 或者继续发送,视需求而定
}
for (size_t i = 0; i < n; ++i) {
const DZ_TAB_STRUCT &dz = dz_info[i];
nlohmann::json item;
item["LN_Num"] = dz.LN_Num;
item["DZ_Num"] = dz.DZ_Num;
item["DZ_Name"] = dz.DZ_Name;
item["DZ_Value"] = ordered_vals[i]; // ★ 严格按顺序
item["DZ_Type"] = dz.DZ_Type;
item["DZ_Min"] = dz.DZ_Min;
item["DZ_Max"] = dz.DZ_Max;
item["DZ_Default"]= dz.DZ_Default;
item["DZ_UNIT"] = dz.DZ_UNIT;
dataArray.push_back(std::move(item));
}
msg["DataArray"] = std::move(dataArray);
detail["Msg"] = std::move(msg);
detail["Code"] = 200;
j["Detail"] = std::move(detail);
// 6) 入队发送
queue_data_t connect_info;
connect_info.strTopic = Topic_Reply_Topic;
connect_info.strText = j.dump(); // 序列化为字符串
connect_info.tag = Topic_Reply_Tag;
connect_info.key = Topic_Reply_Key;
{
std::lock_guard<std::mutex> lock(queue_data_list_mutex);
queue_data_list.push_back(std::move(connect_info));
}
// 调试打印
std::cout << "[send_set_value_reply] queued JSON:\n" << j.dump(4) << std::endl;
// 7) 发送后更新终端状态(按你现有规则)
if (dev.isbusy > 0) {
dev.isbusy--;
}
if (dev.isbusy == 0) {
dev.guid.clear();
dev.busytype = 0;
if (pMon) {
pMon->set_values.clear();//清理本次定值记录
}
}
return true;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////回复内部定值响应读取
bool send_internal_value_reply(const std::string &dev_id, const std::vector<DZ_kzz_bit> &control_words)
{
std::lock_guard<std::mutex> lock(ledgermtx);
// 1) 找终端
auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev &d) { return d.terminal_id == dev_id; });
if (it == terminal_devlist.end()) {
std::cerr << "[send_internal_value_reply] device not found: " << dev_id << std::endl;
return false;
}
terminal_dev &dev = *it;
// 2) 校验状态:发送“内部定值读取结果”回复,应处于 READING_INTERFIXEDVALUEisbusy == 1
if (dev.isbusy != 1 || dev.busytype != static_cast<int>(DeviceState::READING_INTERFIXEDVALUE)) {
std::cerr << "[send_internal_value_reply] device not in READING_INTERFIXEDVALUE state." << std::endl;
return false;
}
//将control_words存入dev
dev.control_words.clear();
for (const auto &cw : control_words) {
dev.control_words.push_back(cw);
}
// -------------------- [新增] 建立 internal_values 与 dz_internal_info_list 的一一对应 --------------------
// 说明:按索引次序一一对应(第 i 个 NameFixValue 对应 internal_values 的第 i 个)
// 若数量不同,按 min 对齐,忽略多出来的一边并告警。
std::vector<float> internal_vals; // [新增]
internal_vals.reserve(dev.internal_values.size()); // [新增]
for (float v : dev.internal_values) internal_vals.push_back(v); // [新增]
const size_t n_dz = dev.dz_internal_info_list.size(); // [新增]
const size_t n_val = internal_vals.size(); // [新增]
const size_t n_use = std::min(n_dz, n_val); // [新增]
if (n_dz != n_val) { // [新增]
std::cerr << "[send_internal_value_reply] WARN: dz_internal_info_list size("
<< n_dz << ") != internal_values size(" << n_val
<< "), will use min(" << n_use << ")." << std::endl;
}
// ------------------------------------------------------------------------------------------------------
// 3) 组包顶层
nlohmann::json j;
j["guid"] = dev.guid;
j["FrontId"] = FRONT_INST;
j["Node"] = g_front_seg_index;
j["Dev_mac"] = normalize_mac(dev.addr_str);
nlohmann::json detail;
detail["Type"] = 0x2106; // 设备数据
nlohmann::json msg;
msg["DataType"] = 0x0D; // 内部定值
// 4) === 将 C# 的拼接逻辑移植为 DataArray ===
// C# 变量对应关系:
// DevInfo.nDevIndex -> 这里用 1
// DevInfo.strGuId -> 这里用 装置id
// DevInfo.controlwordlist -> 这里用参数 control_wordsDZ_kzz_bit 含 kzz_bit/bit_enable
//
// NameFixValue 列表:使用 dev.dz_internal_info_list
//
// 关键逻辑:
// - 遍历每个 NameFixValuek 从 1 递增nStep 每个定值递增 1
// - 若 DataType == 1将 Max/Min/Default 都 /100并 property 输出一个空对象 [{}](保持与 C# 一致)
// - 否则:为该定值构建 property 位数组,范围 [nStep*16, (nStep+1)*16)
// 名称为空则提前结束本定值的 propertyflag = (DefaultValue >> j) & 0x01
//
nlohmann::json dataArray = nlohmann::json::array(); // [新增]
int nStep = 0; // [新增] 每个 NameFixValue 递增
int kSort = 1; // [新增] 排序号,从 1 开始
// 保护dz_internal_info_list 是引用成员,确保不会因并发被改动
//for (const auto& nf : dev.dz_internal_info_list) { // [新增]
for (size_t idxNF = 0; idxNF < n_use; ++idxNF) { // [修改] 使用 idxNF 控制索引
const auto& nf = dev.dz_internal_info_list[idxNF];
// 取字段
const uint16_t dataType = nf.DataType;
const uint16_t minRaw = nf.MinValue;
const uint16_t maxRaw = nf.MaxValue;
const uint16_t defaultRaw = nf.DefaultValue;
const std::string unit = trim_cstr(nf.sDimension, sizeof(nf.sDimension));
const std::string name = trim_cstr(nf.sFixValueName, sizeof(nf.sFixValueName));
// 取对应内部值
const float internal_v_raw = internal_vals[idxNF]; // [新增]
const double internal_v_out = static_cast<double>(internal_v_raw); // [新增] 直接转 double 输出,不缩放
// 构造一条记录
nlohmann::json one;
one["cpu_no"] = 1; // [新增] C#: DevInfo.nDevIndex 填设备号固定为1
one["dev_type"] = dev_id; // [新增] C#: DevInfo.strGuId 填装置id
one["type"] = 90; // [新增] 固定 "90"
one["unit"] = unit; // [新增]
one["describe"] = name; // [新增]
one["sort"] = kSort; // [新增]
one["Internal_Value"] = internal_v_out; // [新增] 精确对应 internal_values 的值(含必要缩放)
// 数值DataType == 1 时缩放 /100
if (dataType == 1) { // [新增] 缩放分支
int ChangeMaxValue = static_cast<int>(maxRaw) / 100;
int ChangeMinValue = static_cast<int>(minRaw) / 100;
int ChangeDefaultValue = static_cast<int>(defaultRaw) / 100;
one["maxvalue"] = ChangeMaxValue;
one["minvalue"] = ChangeMinValue;
one["defaultvalue"] = ChangeDefaultValue;
one["value"] = ChangeDefaultValue;
// C# 在该分支 property 写成 [{ }](一个空对象的数组)
nlohmann::json prop = nlohmann::json::array();
prop.push_back(nlohmann::json::object()); // [{}]
one["property"] = std::move(prop);
} else { // [新增] 未缩放分支 + property 位描述
one["maxvalue"] = static_cast<int>(maxRaw);
one["minvalue"] = static_cast<int>(minRaw);
one["defaultvalue"] = static_cast<int>(defaultRaw);
one["value"] = static_cast<int>(defaultRaw);
// 构建 property16 位窗口,从 nStep*16 到 (nStep+1)*16 - 1
nlohmann::json prop = nlohmann::json::array();
bool hasAny = false;
const int begin = nStep * 16;
const int end = (nStep + 1) * 16; // 不含 end
for (int idx = begin, jbit = 0; idx < end; ++idx, ++jbit) {
if (idx < 0 || static_cast<size_t>(idx) >= control_words.size()) break;
// 名称空则提前退出(仿 C#temp=="" break
const std::string cw_name = trim_cstr(control_words[idx].kzz_bit, sizeof(control_words[idx].kzz_bit));
if (cw_name.empty()) {
// 注意C# 如果 j==0 则设置了 flag2=1仅用于逗号处理这里不需要
break;
}
int flag = (defaultRaw >> jbit) & 0x01; // 取该位默认值
nlohmann::json bitItem;
bitItem["type_num"] = jbit;
bitItem["bit0"] = ""; // 保持与 C# 一致
bitItem["bit1"] = "";
bitItem["describe"] = cw_name;
bitItem["flag"] = (flag ? "1" : "0"); // C# 用字符串
prop.push_back(std::move(bitItem));
hasAny = true;
}
if (!hasAny) {
// 与 C# 对齐:如果一个都没有,就给 [{}] 以避免 "property":[] 的结构差异
prop.push_back(nlohmann::json::object());
}
one["property"] = std::move(prop);
}
dataArray.push_back(std::move(one)); // [新增]
++nStep; // [新增] 进入下一个 16 位窗口
++kSort; // [新增]
}
msg["DataArray"] = std::move(dataArray); // [新增]
detail["Msg"] = std::move(msg);
j["Detail"] = std::move(detail);
// 5) 入队发送(保持你的队列逻辑)
queue_data_t connect_info;
connect_info.strTopic = Topic_Reply_Topic;
connect_info.strText = j.dump(); // 序列化为字符串
connect_info.tag = Topic_Reply_Tag;
connect_info.key = Topic_Reply_Key;
{
std::lock_guard<std::mutex> lock(queue_data_list_mutex);
queue_data_list.push_back(std::move(connect_info));
}
// 调试打印
std::cout << "[send_internal_value_reply] queued JSON:\n" << j.dump(4) << std::endl;
// 6) 发送后更新终端状态(保持你现有规则)
if (dev.isbusy > 0) {
dev.isbusy--;
}
if (dev.isbusy == 0) {
dev.guid.clear();
dev.busytype = 0;
dev.internal_values.clear(); // 清理本次定值记录
dev.dz_internal_info_list.clear(); // 清理本次定值描述记录(注意:如果这是引用成员,确保其实际容器存在)
}
return true;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////处理补招逻辑
//发送补招响应给web
void send_reply_to_kafka_recall(const std::string& guid, int dataType,int code, const std::string& result,const std::string& terminalId,const std::string& monitorId,const std::string& recallStartDate,const std::string& recallEndDate){
// 构造 JSON 字符串
std::ostringstream oss;
oss << "{"
<< "\"guid\":\"" << guid << "\","
<< "\"dataType\":\"" << dataType << "\","
<< "\"code\":" << code << ","
<< "\"result\":\"" << result << "\","
<< "\"terminalId\":\"" << terminalId << "\","
<< "\"monitorId\":\"" << monitorId << "\","
<< "\"recallStartDate\":\"" << recallStartDate << "\","
<< "\"recallEndDate\":\"" << recallEndDate << "\","
<< "\"processNo\":\"" << g_front_seg_index << "\","
<< "\"nodeId\":\"" << FRONT_INST << "\""
<< "}";
std::string jsonString = oss.str();
// 封装 Kafka 消息
queue_data_t connect_info;
connect_info.strTopic = Topic_Reply_Topic;
connect_info.strText = jsonString;
connect_info.tag = "RECALL";
connect_info.key = Topic_Reply_Key;
// 加入发送队列(带互斥锁保护)
queue_data_list_mutex.lock();
queue_data_list.push_back(connect_info);
queue_data_list_mutex.unlock();
}
// ===== 一次遍历可下发“多个终端的一条” =====
void check_recall_event() {
std::vector<RecallTask> tasks; // 本轮要发送的“每终端一条”
{ //锁作用域
std::lock_guard<std::mutex> lock(ledgermtx);
// 遍历所有 terminal —— 每个 terminal 只挑一条,先不判断运行状态,因为正在处理其他事务的装置也可以记录待补招信息
for (auto& dev : terminal_devlist) {
//如果该终端不是正在补招或者idle则直接跳过节省运行时间
bool allow_check =
dev.busytype == static_cast<int>(DeviceState::IDLE) ||
dev.busytype == static_cast<int>(DeviceState::READING_EVENTLOG);
// 如果你允许“事件抢占文件”,并且当前没有文件 RUNNING则也允许评估事件
if (!allow_check && dev.busytype == static_cast<int>(DeviceState::READING_STATSFILE)) {
bool has_file_running = false;
for (const auto& lm2 : dev.line) {
if (!lm2.recall_list_static.empty() &&
lm2.recall_list_static.front().recall_status != static_cast<int>(RecallStatus::NOT_STARTED)) {
has_file_running = true; break;
}
}
if (!has_file_running) allow_check = true; // 没有文件正在跑,允许事件优先
}
if (!allow_check) {
std::cout << "[check_recall_event] skip dev=" << dev.terminal_id
<< " busytype=" << dev.busytype << std::endl;
continue;
}
// 对正在补招或idle终端的所有监测点的待补招列表进行处理
// 1) 先弹掉首条为 DONE/FAILED/EMPTY 的记录(所有 monitor 都要处理首条)
bool any_non_empty = false;
for (auto& lm : dev.line) {
// 标记这个测点在本轮开始时是否非空
bool had_items_before = !lm.recall_list.empty();
bool popped_any = false; // 本轮是否真的弹过
//std::string popped_guid_for_file; // 记录被弹出的条目的 guid用于文件
while (!lm.recall_list.empty()) {
const RecallMonitor& front = lm.recall_list.front();
// ===== 弹掉首条前:打印首条内容与列表大小 =====
std::cout << "[check_recall_event] before pop: list size=" << lm.recall_list.size()
<< " first=(" << front.StartTime << " ~ " << front.EndTime
<< ", status=" << front.recall_status << ")" << std::endl;
if (front.recall_status == static_cast<int>(RecallStatus::DONE)) {
std::cout << "[check_recall_event] DONE dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " " << front.StartTime << " ~ " << front.EndTime << std::endl;
//调用reply接口通知web端该时间段补招完成
std::string msg = std::string("监测点:") + lm.monitor_name
+ " 补招时间范围:" + front.StartTime
+ " ~ " + front.EndTime
+ " 补招执行完成";
//send_reply_to_kafka_recall(dev.guid,1,static_cast<int>(ResponseCode::OK),msg,dev.terminal_id,lm.monitor_id,front.StartTime,front.EndTime);
append_recall_record_line(dev.guid, lm.monitor_id, msg);
lm.recall_list.pop_front(); // 弹掉首条
popped_any = true;
// ===== 弹掉首条后:打印新首条与列表大小 =====
if (!lm.recall_list.empty()) {
const RecallMonitor& newfront = lm.recall_list.front();
std::cout << "[check_recall_event] after pop: list size=" << lm.recall_list.size()
<< " new first=(" << newfront.StartTime << " ~ " << newfront.EndTime
<< ", status=" << newfront.recall_status << ")" << std::endl;
} else {
std::cout << "[check_recall_event success] after pop: list empty" << std::endl;
}
break;
}
else if (front.recall_status == static_cast<int>(RecallStatus::EMPTY)) {
std::cout << "[check_recall_event] EMPTY dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " " << front.StartTime << " ~ " << front.EndTime << std::endl;
//调用reply接口通知web端该时间段补招失败
std::string msg = std::string("监测点:") + lm.monitor_name
+ " 补招时间范围:" + front.StartTime
+ " ~ " + front.EndTime
+ " 补招无事件日志";
//send_reply_to_kafka_recall(dev.guid,1,static_cast<int>(ResponseCode::NOT_FOUND),msg,dev.terminal_id,lm.monitor_id,front.StartTime,front.EndTime);
append_recall_record_line(dev.guid, lm.monitor_id, msg);
lm.recall_list.pop_front(); // 弹掉首条
popped_any = true;
// ===== 弹掉首条后:打印新首条与列表大小 =====
if (!lm.recall_list.empty()) {
const RecallMonitor& newfront = lm.recall_list.front();
std::cout << "[check_recall_event] after pop: list size=" << lm.recall_list.size()
<< " new first=(" << newfront.StartTime << " ~ " << newfront.EndTime
<< ", status=" << newfront.recall_status << ")" << std::endl;
} else {
std::cout << "[check_recall_event empty] after pop: list empty" << std::endl;
}
break;
}
else if (front.recall_status == static_cast<int>(RecallStatus::FAILED)) {
std::cout << "[check_recall_event] FAILED dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " " << front.StartTime << " ~ " << front.EndTime << std::endl;
//调用reply接口通知web端该时间段补招失败
std::string msg = std::string("监测点:") + lm.monitor_name
+ " 补招时间范围:" + front.StartTime
+ " ~ " + front.EndTime
+ " 补招执行失败";
//send_reply_to_kafka_recall(dev.guid,1,static_cast<int>(ResponseCode::BAD_REQUEST),msg,dev.terminal_id,lm.monitor_id,front.StartTime,front.EndTime);
append_recall_record_line(dev.guid, lm.monitor_id, msg);
lm.recall_list.pop_front(); // 弹掉首条
popped_any = true;
// ===== 弹掉首条后:打印新首条与列表大小 =====
if (!lm.recall_list.empty()) {
const RecallMonitor& newfront = lm.recall_list.front();
std::cout << "[check_recall_event] after pop: list size=" << lm.recall_list.size()
<< " new first=(" << newfront.StartTime << " ~ " << newfront.EndTime
<< ", status=" << newfront.recall_status << ")" << std::endl;
} else {
std::cout << "[check_recall_event fail] after pop: list empty" << std::endl;
}
break;
} else {//如果每个测点都有补招记录,但是首条不是 DONE/FAILED/EMPTY 则跳过该测点,检查下一个测点
std::cout << "[check_recall_event] skip line=" << lm.monitor_name<< std::endl;
break; // 首条不是 2/3/4如果是正在处理其他业务或者idle的装置写入了待补招列表应该都是0如果是正在补招的装置新增的部分不会影响原有顺序
}
}
if (had_items_before && popped_any && lm.recall_list.empty()) {//处理后,当前测点补招记录为空
//通知补招全部完成
std::cout << "[check_recall_event] finish recall monitor=" << lm.monitor_id << std::endl;
//读取记录文件获取响应参数
std::string file_guid, terminalId, file_monitorId, startTime, endTime, msg;
if (get_recall_record_fields_by_guid_monitor(dev.guid, lm.monitor_id,
file_guid, terminalId, file_monitorId,
startTime, endTime, msg)) {
// 校验通过,拿到参数了
std::cout << "guid=" << file_guid << std::endl;
std::cout << "terminalId=" << terminalId << std::endl;
std::cout << "monitor_id=" << file_monitorId << std::endl;
std::cout << "start=" << startTime << std::endl;
std::cout << "end=" << endTime << std::endl;
std::cout << "msg:\n" << msg << std::endl;
send_reply_to_kafka_recall(file_guid, 1, static_cast<int>(ResponseCode::OK),
msg, terminalId, file_monitorId, startTime, endTime);
}
delete_recall_record_file(dev.guid, lm.monitor_id);
}
//处理后,当前测点补招记录非空
if (!lm.recall_list.empty())
{
any_non_empty = true; // 处理了成功/失败/无消息以后该装置只要有任一测点有一条记录就标记,可能是待处理或者正在处理的补招
}
}
// pop后判断是否仍有 RUNNINGpop后应该都为unstarted没pop的才会是running
bool has_running = false;
for (auto& lm : dev.line) {
if (!lm.recall_list.empty() &&
lm.recall_list.front().recall_status == static_cast<int>(RecallStatus::RUNNING)) {
has_running = true;
break;
}
}
if (!has_running && dev.busytype == static_cast<int>(DeviceState::READING_EVENTLOG)) {
dev.busytimecount = 0; // 避免“上一条刚结束但下一条尚未起”期间被误判超时
}
// 有条目但目前存在 RUNNING继续等待该 RUNNING 完成,当前装置本轮不新发
if (any_non_empty && has_running) {
std::cout << "[check_recall_event] skip dev=" << dev.terminal_id
<< " already running recall" << std::endl;
continue;
}
//pop后无任何补招条目且处于补招状态清空运行态
if (!any_non_empty && dev.busytype == static_cast<int>(DeviceState::READING_EVENTLOG)) {
// 该终端本轮已无任何补招条目,且处于补招暂态事件的状态清空运行态
std::cout << "[check_recall_event] finish recall dev=" << dev.terminal_id << std::endl;
dev.guid.clear(); // 清空 guid
dev.busytype = static_cast<int>(DeviceState::IDLE); // 业务类型归零
dev.isbusy = 0; // 清空业务标志
dev.busytimecount = 0; // 计时归零
continue; //处理完处理下一个装置
}
//有待补招任务且idle或者在补招继续补招处理
//std::cout << "[check_recall_event] idle or continue recall dev=" << dev.terminal_id << std::endl;
// 若无 RUNNING则说明该终端空闲可以挑选新的补招任务
if (any_non_empty && !has_running) {
// 3) 选择该终端的“第一条 NOT_STARTED(0)”作为本终端本轮任务
bool picked = false;
for (auto& lm : dev.line) {
if (lm.recall_list.empty()) {
std::cout << "[check_recall_event] skip empty line=" << lm.monitor_name<< std::endl;
continue; //跳过空的监测点
}
RecallMonitor& front = lm.recall_list.front(); //取非空测点的列表的第一条
if (front.recall_status == static_cast<int>(RecallStatus::NOT_STARTED)) {//未补招
// 标记为 RUNNING并设置终端忙状态
front.recall_status = static_cast<int>(RecallStatus::RUNNING);//该补招记录刷新为补招中
dev.guid = front.recall_guid; // 记录本次补招的 guid
dev.isbusy = 1; //标记为忙
dev.busytype = static_cast<int>(DeviceState::READING_EVENTLOG);//装置状态正在补招和idle的都刷新为正在补招
dev.busytimecount = 0; //刷新业务超时计数
// 记录任务(每终端只取这一条,多个装置可以同时进行)
tasks.push_back(RecallTask{
dev.terminal_id,
front.StartTime,
front.EndTime,
lm.logical_device_seq//记录测点号
});
picked = true; //该装置已取
break;
}
}
// 若该终端没有 NOT_STARTED 的首条(可能剩下的都是 RUNNING 或内部已经被清空),
if (!picked) {
// 没挑到 NOT_STARTED例如都是“首条非空但非 NOT_STARTED/非 RUNNING”的情况
// 这种情况下本终端留给下一轮处理即可。
std::cout << "[check_recall_event] skip dev=" << dev.terminal_id
<< " no NOT_STARTED at head" << std::endl;
}
// 就留待下一轮;不要清空运行态,直到所有条目被处理为止。
continue;
}
}
} // 解锁
// 对于本轮挑选到的任务,逐终端下发一条,这些终端的状态都已设为补招
for (const auto& t : tasks) {
//处理入参
std::tm tm1{}, tm2{};
if (!parse_datetime_tm(t.start_time, tm1) || !parse_datetime_tm(t.end_time, tm2)) {
std::cout << "[check_recall_event] parse time fail: "
<< t.start_time << " ~ " << t.end_time << std::endl;
continue;
}
uint8_t mp = 1; // 默认
try {
mp = static_cast<uint8_t>(std::stoi(t.monitor_index));
} catch (...) {
std::cout << "[check_recall_event] parse mpid fail: "
<< t.monitor_index << std::endl;
continue;
}
// 下发补招请求action=2
ClientManager::instance().read_eventlog_action_to_device(
t.dev_id, tm1, tm2, 2, mp);//2是暂态事件
std::cout << "[check_recall_event] SEND dev=" << t.dev_id
<< " monitor=" << static_cast<int>(mp)
<< " start=" << t.start_time
<< " end=" << t.end_time
<< " action(2)" << std::endl;
}
// 重要:本函数不把 RUNNING 改成 DONE/FAILED
// 应由设备回调/结果处理逻辑在完成后调用通知函数,把对应 monitor.front().recall_status 从1置为 2/3
// 下一轮本函数会弹掉 2/3并在该终端所有 recall_list 全部清空后重置装置状态。
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////处理补招逻辑统计数据
// ====== 从文件名中提取“第二段下划线分隔字段”并转换为 epoch 秒 ======
static bool extract_epoch_from_filename(const std::string& name,
long long& out_epoch,
int logical_device_seq)
{
// 拆分
std::vector<std::string> parts;
parts.reserve(8);
size_t start = 0, pos;
while ((pos = name.find('_', start)) != std::string::npos) {
parts.emplace_back(name.substr(start, pos - start));
start = pos + 1;
}
parts.emplace_back(name.substr(start)); // 最后一段(含扩展名)
if (parts.size() < 4) return false;
// 第二段序号是倒数第4段
const std::string& seq_str = parts[parts.size() - 4];
// 允许前导 0把字符串转 int 后比较
for (char c : seq_str) if (!std::isdigit(static_cast<unsigned char>(c))) return false;
int seq_val = 0;
try {
seq_val = std::stoi(seq_str);
} catch (...) {
return false;
}
if (seq_val != logical_device_seq) return false;
// 其余与上面相同
const std::string& date_str = parts[parts.size() - 3];
const std::string& time_str = parts[parts.size() - 2];
std::string ms_str = parts.back();
size_t dot = ms_str.find('.');
if (dot != std::string::npos) {
ms_str.erase(dot);
}
if (date_str.size() != 8 || time_str.size() != 6) return false;
for (char c : date_str) if (!std::isdigit(static_cast<unsigned char>(c))) return false;
for (char c : time_str) if (!std::isdigit(static_cast<unsigned char>(c))) return false;
for (char c : ms_str) if (!std::isdigit(static_cast<unsigned char>(c))) return false;
int year = std::stoi(date_str.substr(0, 4));
int month = std::stoi(date_str.substr(4, 2));
int day = std::stoi(date_str.substr(6, 2));
int hour = std::stoi(time_str.substr(0, 2));
int min = std::stoi(time_str.substr(2, 2));
int sec = std::stoi(time_str.substr(4, 2));
// int msec = std::stoi(ms_str);
std::tm tm{}; tm.tm_isdst = -1;
tm.tm_year = year - 1900;
tm.tm_mon = month - 1;
tm.tm_mday = day;
tm.tm_hour = hour;
tm.tm_min = min;
tm.tm_sec = sec;
time_t t = timegm(&tm);
if (t < 0) return false;
out_epoch = static_cast<long long>(t); // 秒级
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 从文件名中解析出 "监测点号_YYYYMMDD_HHMMSS_mmm";成功返回 true
static bool make_target_key_from_filename(const std::string& fname, std::string& out_key) {
// 例PQMonitor_PQM1_000005_20250909_074657_246.dat
// 例PQ_PQLD1_000005_20250909_074656_435.dat
// 以 '_' 切分,应至少 6 段
std::vector<std::string> parts;
parts.reserve(8);
size_t start = 0, pos;
while ((pos = fname.find('_', start)) != std::string::npos) {
parts.emplace_back(fname.substr(start, pos - start));
start = pos + 1;
}
parts.emplace_back(fname.substr(start));
if (parts.size() < 6) return false;
// 索引意义:
// [0]=前缀(如 PQMonitor / PQ)
// [1]=监测点号(如 PQM1 / PQLD1)
// [2]=序号(如 000005)
// [3]=YYYYMMDD
// [4]=HHMMSS
// [5]=mmm(.dat)
const std::string& monitor = parts[1];
const std::string& ymd = parts[3];
const std::string& hms = parts[4];
// 去掉末段的扩展名(如 "246.dat" -> "246"
std::string mmm = parts[5];
size_t dot = mmm.rfind('.');
if (dot != std::string::npos) mmm.erase(dot);
// 基本合法性校验(长度与全数字)
auto all_digits = [](const std::string& s) {
return !s.empty() && std::find_if(s.begin(), s.end(),
[](unsigned char c){ return !std::isdigit(c); }) == s.end();
};
if (ymd.size() != 8 || !all_digits(ymd)) return false;
if (hms.size() != 6 || !all_digits(hms)) return false;
if (mmm.size() != 3 || !all_digits(mmm)) return false;
if (monitor.empty()) return false;
// 目标 key监测点号_YYYYMMDD_HHMMSS_mmm
//out_key.reserve(monitor.size() + 1 + 8 + 1 + 6 + 1 + 3);
out_key.reserve(monitor.size() + 1 + 8 + 1 + 6);
out_key.clear();
//out_key.append(monitor).append("_").append(ymd).append("_").append(hms).append("_").append(mmm);
out_key.append(monitor).append("_").append(ymd).append("_").append(hms);
return true;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////补招文件匹配事件
//////////////////////////////////////////////////////////////////////////////////////////////////////////////补招文件逻辑
// ====== ★修改check_recall_stat —— 加入“两步法”状态机 ======
void check_recall_file() {
std::vector<RecallTask> tasks; // 本轮要发送的“每终端一条”(目录请求 或 文件下载 请求)
// ★修改开始:新增“待上传动作”容器与两个小工具(局部作用域,函数私有)
struct PendingUpload {
std::string terminal_id;
unsigned short logical_seq = 0;
std::vector<std::string> files_to_send; // 完整路径含MAC目录
qvvr_data matched{}; // 与 .cfg 匹配到的事件(如有)
qvvr_data mismatched{}; // 未匹配到的事件(如有)
bool has_matched = false;
// 用于回锁后在台账中定位并删掉对应 qvvr_file 组
std::set<std::string> sig_names; // 仅文件名集合
std::set<std::string> sig_downs; // 完整路径集合(与 files_to_send 相同内容的 set
};
auto ExtractFileNames = [](const std::vector<std::string>& full_paths) {
std::vector<std::string> names;
names.reserve(full_paths.size());
for (const auto& p : full_paths) {
auto s = sanitize(p);
size_t pos = s.find_last_of("/\\");
std::string name = (pos == std::string::npos) ? s : s.substr(pos + 1);
names.push_back(sanitize(name));
}
std::sort(names.begin(), names.end());
names.erase(std::unique(names.begin(), names.end()), names.end());
return names;
};
auto MakeSigNames = [&](const std::vector<std::string>& full_paths){
auto names = ExtractFileNames(full_paths);
return std::set<std::string>(names.begin(), names.end());
};
// ★修改结束
std::vector<PendingUpload> pending_uploads; // ★修改:锁外执行上传与清理
{ //锁作用域
std::lock_guard<std::mutex> lock(ledgermtx);
// ★优先级开始:事件优先 —— 若任一测点存在事件补招recall_list尚未清空则本轮跳过文件补招
{
bool has_event_pending = false;
for (const auto& dev : terminal_devlist) {
for (const auto& lm : dev.line) {
if (!lm.recall_list.empty()) { // 只要存在事件待处理就让位
has_event_pending = true;
break;
}
}
if (has_event_pending) break;
}
if (has_event_pending) {
std::cout << "[check_recall_file] skip this round: event recall pending, give priority to check_recall_event()\n";
return; // 直接让位给 check_recall_event本轮不处理文件补招
}
}
// ★优先级结束
for (auto& dev : terminal_devlist) {
// 仅处理“正在补招/空闲”终端,与你原逻辑一致
if (dev.busytype != static_cast<int>(DeviceState::READING_STATSFILE) &&
dev.busytype != static_cast<int>(DeviceState::IDLE)) {
continue;
}
// 1) 清理首条 DONE/FAILED
bool any_non_empty = false;
for (auto& lm : dev.line) {
// 标记这个测点在本轮开始时是否非空
bool had_items_before = !lm.recall_list_static.empty();
bool popped_any = false; // 本轮是否真的弹过
while (!lm.recall_list_static.empty()) {
const RecallFile& front = lm.recall_list_static.front();
// ===== 弹掉首条前:打印首条内容与列表大小 =====
std::cout << "[check_recall_file] before pop | dev=" << dev.terminal_id
<< " mon=" << lm.monitor_id
<< " size=" << lm.recall_list_static.size()
<< " first{guid=" << front.recall_guid
<< ", status=" << front.recall_status
<< ", direct=" << (front.direct_mode ? 1 : 0)
<< ", phase=" << static_cast<int>(front.phase)
<< ", cur_dir=" << front.cur_dir
<< ", cur_file=" << front.downloading_file
<< ", list_result=" << static_cast<int>(front.list_result)
<< ", download_result=" << static_cast<int>(front.download_result)
<< ", download_queue=" << front.download_queue.size()
<< "}" << std::endl;
// ===== 打印 download_queue 内的全部文件 =====
if (!front.download_queue.empty()) {
std::cout << " [download_queue files] (" << front.download_queue.size() << "):" << std::endl;
int idx = 0;
for (const auto& path : front.download_queue) {
std::cout << " [" << idx++ << "] " << path << std::endl;
}
} else {
std::cout << " [download_queue files] (empty)" << std::endl;
}
if (front.recall_status == static_cast<int>(RecallStatus::DONE)) {
std::cout << "[check_recall_stat] DONE dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id << std::endl;
lm.recall_list_static.pop_front();
//弹出后再打印首条内容与列表大小
if (!lm.recall_list_static.empty()) {
const RecallFile& nf = lm.recall_list_static.front();
std::cout << "[check_recall_file] after pop | dev=" << dev.terminal_id
<< " mon=" << lm.monitor_id
<< " size=" << lm.recall_list_static.size()
<< " first{guid=" << nf.recall_guid
<< ", status=" << nf.recall_status
<< ", direct=" << (nf.direct_mode ? 1 : 0)
<< ", phase=" << static_cast<int>(nf.phase)
<< ", cur_dir=" << nf.cur_dir
<< ", cur_file=" << nf.downloading_file
<< ", list_result=" << static_cast<int>(nf.list_result)
<< ", download_result=" << static_cast<int>(nf.download_result)
<< ", download_queue=" << nf.download_queue.size()
<< "}" << std::endl;
// ===== 打印 download_queue 内的全部文件 =====
if (!nf.download_queue.empty()) {
std::cout << " [download_queue files] (" << nf.download_queue.size() << "):" << std::endl;
int idx = 0;
for (const auto& path : nf.download_queue) {
std::cout << " [" << idx++ << "] " << path << std::endl;
}
} else {
std::cout << " [download_queue files] (empty)" << std::endl;
}
}else {
std::cout << "[check_recall_file] after pop | list empty" << std::endl;
}
popped_any = true;
break;
} else if (front.recall_status == static_cast<int>(RecallStatus::FAILED)) {
std::cout << "[check_recall_stat] FAILED dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " " << front.StartTime << " ~ " << front.EndTime << std::endl;
lm.recall_list_static.pop_front();
//弹出后再打印首条内容与列表大小
if (!lm.recall_list_static.empty()) {
const RecallFile& nf = lm.recall_list_static.front();
std::cout << "[check_recall_file] after pop | dev=" << dev.terminal_id
<< " mon=" << lm.monitor_id
<< " size=" << lm.recall_list_static.size()
<< " first{guid=" << nf.recall_guid
<< ", status=" << nf.recall_status
<< ", direct=" << (nf.direct_mode ? 1 : 0)
<< ", phase=" << static_cast<int>(nf.phase)
<< ", cur_dir=" << nf.cur_dir
<< ", cur_file=" << nf.downloading_file
<< ", list_result=" << static_cast<int>(nf.list_result)
<< ", download_result=" << static_cast<int>(nf.download_result)
<< ", download_queue=" << nf.download_queue.size()
<< "}" << std::endl;
// ===== 打印 download_queue 内的全部文件 =====
if (!nf.download_queue.empty()) {
std::cout << " [download_queue files] (" << nf.download_queue.size() << "):" << std::endl;
int idx = 0;
for (const auto& path : nf.download_queue) {
std::cout << " [" << idx++ << "] " << path << std::endl;
}
} else {
std::cout << " [download_queue files] (empty)" << std::endl;
}
} else {
std::cout << "[check_recall_file] after pop | list empty" << std::endl;
}
popped_any = true;
break;
} else {
std::cout << "[check_recall_file] skip line=" << lm.monitor_name<< std::endl;
break;//找到第一条不是成功或失败的记录就退出循环
}
}
if (had_items_before && popped_any && lm.recall_list_static.empty()) {//处理后,当前测点补招记录为空
//通知补招全部完成
std::cout << "[check_recall_file] finish recall monitor=" << lm.monitor_id << std::endl;
//读取记录文件获取响应参数
std::string file_guid, terminalId, file_monitorId, startTime, endTime, msg;
if (get_recall_record_fields_by_guid_monitor(dev.guid, lm.monitor_id,
file_guid, terminalId, file_monitorId,
startTime, endTime, msg)) {
// 校验通过,拿到参数了
std::cout << "guid=" << file_guid << std::endl;
std::cout << "terminalId=" << terminalId << std::endl;
std::cout << "monitor_id=" << file_monitorId << std::endl;
std::cout << "start=" << startTime << std::endl;
std::cout << "end=" << endTime << std::endl;
std::cout << "msg:\n" << msg << std::endl;
send_reply_to_kafka_recall(file_guid, 1, static_cast<int>(ResponseCode::OK),
msg, terminalId, file_monitorId, startTime, endTime);
}
delete_recall_record_file(dev.guid, lm.monitor_id);
}
//处理后,当前测点补招记录非空
if (!lm.recall_list_static.empty())
{
any_non_empty = true; // 处理了成功/失败/无消息以后该装置只要有任一测点有一条记录就标记,可能是待处理或者正在处理的补招
}
}
if (!any_non_empty && dev.busytype == static_cast<int>(DeviceState::READING_STATSFILE)) {
std::cout << "[check_recall_file] finish recall dev=" << dev.terminal_id << std::endl;
dev.guid.clear();
dev.busytype = static_cast<int>(DeviceState::IDLE);
dev.isbusy = 0;
dev.busytimecount = 0;
continue;//这个装置处理下一个装置
}
// pop后判断是否仍有 RUNNINGpop后应该都为unstarted没pop的才会是running
bool has_running = false;
for (auto& lm : dev.line) {
if (!lm.recall_list_static.empty() &&
lm.recall_list_static.front().recall_status == static_cast<int>(RecallStatus::RUNNING)) {
has_running = true;
break;
}
}
if (!has_running && dev.busytype == static_cast<int>(DeviceState::READING_STATSFILE)) dev.busytimecount = 0;
// ★新增:当存在 RUNNING 时,推进“该终端的首条补招记录”的两步状态机
if (has_running) {
for (auto& lm : dev.line) {
if (lm.recall_list_static.empty()) continue;//跳过没有记录的测点
RecallFile& front = lm.recall_list_static.front();//有记录测点的第一个补招
if (front.recall_status != static_cast<int>(RecallStatus::RUNNING)) continue;//跳过不是正在补招的记录
// 初始化阶段:补招分为两个阶段,读文件列表和下载文件,如果是刚进入 RUNNING 状态则初始化
if (front.phase == RecallPhase::IDLE) { //暂态补招
front.phase = RecallPhase::LISTING;
front.cur_dir_index = 0;//初始化查询的目录下标
front.cur_dir.clear();//初始化查询的目录名
front.list_result = ActionResult::PENDING; //目录请求结果置为待定
front.download_result = ActionResult::PENDING;//下载结果置为待定
front.download_queue.clear();//初始化下载文件队列
front.required_files.clear();
front.file_success.clear();
// 立即发起第一个目录请求
if (front.cur_dir_index < static_cast<int>(front.dir_candidates.size())) {
front.cur_dir = front.dir_candidates[front.cur_dir_index];//从第一个目录开始
// ★★ 只发一个目录请求,并等待外部线程回写结果与文件名列表
ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir);
std::cout << "[check_recall_stat] LIST req dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " dir=" << front.cur_dir << std::endl;
} else {
// 无目录可查
front.recall_status = static_cast<int>(RecallStatus::FAILED);
std::string msg_fail;
if (front.direct_mode) {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,目标时标:"
+ front.target_filetimes;
} else {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,时间范围:"
+ front.StartTime + " ~ " + front.EndTime;
}
append_recall_record_line(dev.guid, lm.monitor_id, msg_fail);
std::cout << "[check_recall_stat] no dir candidates, FAIL dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id << std::endl;
}
// 一个终端只推进首条,跳出
break;
}
// LISTING 阶段:等待其他线程回写 list_result + dir_files[cur_dir]
if (front.phase == RecallPhase::LISTING) {
if (front.list_result == ActionResult::PENDING) {
// 还在等目录回执,本轮不再发任何请求
// 外部线程拿到目录文件列表后应写入front.dir_files[front.cur_dir] = {a,b,c...} 并置 list_result=OK/FAIL
break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录
}
if (front.list_result == ActionResult::FAIL) {
// 尝试下一个目录
front.cur_dir_index++;
front.list_result = ActionResult::PENDING;//重置状态
front.download_queue.clear();//重置下载队列
front.required_files.clear();
front.file_success.clear();
if (front.cur_dir_index < static_cast<int>(front.dir_candidates.size())) {
front.cur_dir = front.dir_candidates[front.cur_dir_index];
ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir);
std::cout << "[check_recall_stat] LIST retry dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " dir=" << front.cur_dir << std::endl;
} else {
// 所有目录都失败
front.recall_status = static_cast<int>(RecallStatus::FAILED);
std::string msg_fail;
if (front.direct_mode) {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,目标时标:"
+ front.target_filetimes;
} else {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,时间范围:"
+ front.StartTime + " ~ " + front.EndTime;
}
append_recall_record_line(dev.guid, lm.monitor_id, msg_fail);
std::cout << "[check_recall_stat] all dir failed, FAIL dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id << std::endl;
}
break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录;如果失败,下个循环会弹出
}
// OK根据起止时间筛选文件
{
/*//这部分用于稳态数据文件
long long beg = parse_time_to_epoch(front.StartTime);
long long end = parse_time_to_epoch(front.EndTime);
//错误判断:如果是直下文件的方式,会给默认的正确的时间范围
if (beg < 0 || end < 0 || beg > end) {
front.recall_status = static_cast<int>(RecallStatus::FAILED);
std::cout << "[check_recall_stat] time parse ERR, FAIL dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " start=" << front.StartTime
<< " end=" << front.EndTime << std::endl;
break;//跳出循环,一个装置一次只能处理一个测点的一个补招记录;如果失败,下个循环会弹出
}*/
//装置消息返回后通知成功的处理:
auto it = front.dir_files.find(front.cur_dir);//在map中查找当前目录名对应的目录下的文件名列表
if (it != front.dir_files.end()) {
if (front.direct_mode) {
// 目标时间戳(不含毫秒、形如 yyyyMMdd_HHmmss
const std::string& want_ts = front.target_filetimes;
for (const auto& ent : it->second) {
// 打印目录下的所有条目
std::cout << "[check_recall_file] dir file dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " dir=" << front.cur_dir
<< " file=" << ent.name
<< " flag=" << ent.flag
<< std::endl;
if (ent.flag == 0) continue; // 只要文件,跳过目录
size_t n = ::strnlen(ent.name, sizeof(ent.name));
std::string fname(ent.name, n);
// 解析出 key = 监测点号_YYYYMMDD_HHMMSS注意确保你的 make_target_key_from_filename
// 已经改成“去掉毫秒”的版本;若还带毫秒,请先调整该函数)
std::string key;
if (!make_target_key_from_filename(fname, key)) {
std::cout << "[check_recall_file] dir file dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " dir=" << front.cur_dir
<< " file=" << fname
<< " key=" << key
<< " ERR: invalid filename format, skip"
<< std::endl;
continue;
}
// key 形如 MON_YYYYMMDD_HHMMSS目标是只按时间戳匹配
if (has_suffix(key, want_ts)) {
//打印放入的文件名
std::cout << "[check_recall_file] dir file dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " dir=" << front.cur_dir
<< " file=" << fname
<< " key=" << key
<< " MATCH, add to download queue"
<< std::endl;
front.download_queue.push_back(front.cur_dir + "/" + fname);
}
}
} else { //稳态文件
// ☆原有:按时间窗筛选
long long beg = parse_time_to_epoch(front.StartTime);
long long end = parse_time_to_epoch(front.EndTime);
for (const auto& ent : it->second) {
if (ent.flag != 1) continue; // 只要文件
// 文件名
size_t n = ::strnlen(ent.name, sizeof(ent.name));
std::string fname(ent.name, n);
long long fe = -1;
int seq = 0;
try { seq = std::stoi(lm.logical_device_seq); } catch (...) { seq = 0; }
if (!extract_epoch_from_filename(fname, fe, seq)) continue;
if (fe >= beg && fe <= end) {
front.download_queue.push_back(front.cur_dir + "/" + fname);
}
}
}
}
}
if (front.download_queue.empty()) {
// 当前目录无匹配文件 -> 试下一个目录
front.cur_dir_index++;
front.list_result = ActionResult::PENDING;
if (front.cur_dir_index < static_cast<int>(front.dir_candidates.size())) {
front.cur_dir = front.dir_candidates[front.cur_dir_index];
ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir);
std::cout << "[check_recall_stat] LIST next dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " dir=" << front.cur_dir << std::endl;
} else {
// 所有目录都“无匹配文件”
front.recall_status = static_cast<int>(RecallStatus::FAILED);
std::string msg_fail;
if (front.direct_mode) {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,目标时标:"
+ front.target_filetimes;
} else {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,时间范围:"
+ front.StartTime + " ~ " + front.EndTime;
}
append_recall_record_line(dev.guid, lm.monitor_id, msg_fail);
std::cout << "[check_recall_stat] no matched files in ALL dirs, FAIL dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id << std::endl;
}
break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录;如果失败,下个循环会弹出
}
else {
// 进入下载阶段
front.required_files.clear();
//for (const auto& p : front.download_queue) front.required_files.insert(p);
//转成本地保存路径 download/<dev.addr_str>/<fname>
front.required_files.clear();
{
const std::string base_dir = std::string("download/") + sanitize(dev.addr_str);
for (const auto& p : front.download_queue) {
// p 形如 "<cur_dir>/<fname>" —— 提取纯文件名
std::string fname = sanitize(extract_filename1(p));
if (!fname.empty()) {
front.required_files.insert(base_dir + "/" + fname);
}
}
}
front.file_success.clear();
front.phase = RecallPhase::DOWNLOADING;
front.download_result = ActionResult::PENDING;
front.downloading_file.clear();
std::cout << "[check_recall_stat] enter DOWNLOADING dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " count=" << front.download_queue.size() << std::endl;
}
}
//在上面的处理中已经找到要下载的文件列表,进入下载阶段
// DOWNLOADING 阶段:一次只下一个文件,等待外部线程填充 download_result
if (front.phase == RecallPhase::DOWNLOADING) {
if (front.downloading_file.empty()) {//当前无正在下载文件
if (front.download_queue.empty()) {//下载队列无文件
// 所有文件下载完毕
std::cout << "dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " ok=" << front.file_success.size()
<< " total=" << front.required_files.size()
<< std::endl;
if (!front.required_files.empty()
&& front.file_success.size() == front.required_files.size()) {
front.recall_status = static_cast<int>(RecallStatus::DONE);//两个文件都下好了标记为成功
//更新事件
// ★修改开始替换“assign_qvvr_file_list + update_qvvr_file_download(有锁)”
// 组装完整路径列表
std::vector<std::string> fullFilenames;
fullFilenames.reserve(front.required_files.size());
for (const auto& f : front.required_files) fullFilenames.push_back(f);
// 仅对“当前监测点”添加一组 qvvr_filefile_name=仅文件名,下载标记留到下行)
{
qvvr_file qfile;
auto names = ExtractFileNames(fullFilenames);
qfile.file_name.assign(names.begin(), names.end());
qfile.is_download = false;
qfile.is_pair = false;
qfile.file_time_count = 0;
qfile.used_status = true;
lm.qvvrevent.qvvrfile.push_back(std::move(qfile));
}
// 把这组标记为“已全部下载”,并尝试用 .cfg 匹配事件;准备锁外上传动作
{
// 找到刚刚 append 的那组file_name 集合 == fullFilenames 的文件名集合
auto want_names = MakeSigNames(fullFilenames);
auto it_qf = std::find_if(lm.qvvrevent.qvvrfile.begin(),
lm.qvvrevent.qvvrfile.end(),
[&](const qvvr_file& x){
std::set<std::string> s(x.file_name.begin(), x.file_name.end());
return s == want_names;
});
if (it_qf != lm.qvvrevent.qvvrfile.end()) {
// 原it_qf->file_download = fullFilenames;
it_qf->file_download.clear();
it_qf->file_download.assign(fullFilenames.begin(), fullFilenames.end()); // ★修复vector -> list
it_qf->is_download = true;
// 寻找第一个 .cfg 做匹配
qvvr_data matched{};
qvvr_data mismatched{};
bool has_matched = false;
for (const auto& p : it_qf->file_download) {
auto s = sanitize(p);
auto dot = s.find_last_of('.');
if (dot != std::string::npos) {
std::string ext = s.substr(dot);
for (auto& c : ext) c = (char)std::tolower((unsigned char)c);
if (ext == ".cfg") {
if (compare_qvvr_and_file(p, lm.qvvrevent.qvvrdata, matched)) {
it_qf->is_pair = true;
has_matched = true;
}
else {
it_qf->is_pair = false;
long long start_t;
long long trigger_t;
if (extract_timestamp_from_cfg_file(p, start_t, trigger_t)) {
mismatched.QVVR_time = trigger_t;
} else {
mismatched.QVVR_time = 0; // 未能提取时间
}
// 其他字段置空/置零
mismatched.QVVR_Amg = 0.0;
mismatched.QVVR_PerTime = 0.0;
mismatched.QVVR_type = 0;
mismatched.phase = 0;
}
break; // 只看一个 .cfg
}
}
}
// 记录一个“待上传动作”,锁外执行上传、回写 JSON、删文件与回锁清理
PendingUpload pu;
pu.terminal_id = dev.terminal_id;
try { pu.logical_seq = static_cast<unsigned short>(std::stoi(lm.logical_device_seq)); }
catch (...) { pu.logical_seq = 0; }
// 原pu.files_to_send = it_qf->file_download;
pu.files_to_send.clear();
pu.files_to_send.assign(it_qf->file_download.begin(), it_qf->file_download.end()); // ★修复list -> vector
pu.has_matched = has_matched;
pu.matched = matched;
pu.mismatched = mismatched;
pu.sig_names = want_names;
pu.sig_downs = std::set<std::string>(pu.files_to_send.begin(), pu.files_to_send.end());
pending_uploads.push_back(std::move(pu));
}
}
// ★修改结束
std::cout << "[check_recall_stat] DONE dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " ok=" << front.file_success.size()
<< " total=" << front.required_files.size()
<< std::endl;
} else {
front.recall_status = static_cast<int>(RecallStatus::FAILED);
std::cout << "[check_recall_stat] some files failed, FAIL dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " ok=" << front.file_success.size()
<< " need=" << front.required_files.size() << std::endl;
}
break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录
}
// 发起下一文件下载
front.downloading_file = front.download_queue.front();
front.download_queue.pop_front();//弹出队首
front.download_result = ActionResult::PENDING;
// ★★ 只发一个下载请求,等待外部线程回写结果
ClientManager::instance().add_file_download_action_to_device(dev.terminal_id, front.downloading_file);
std::cout << "[check_recall_stat] DL req dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " file=" << front.downloading_file << std::endl;
break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录
} else {
// 等待 download_result
if (front.download_result == ActionResult::PENDING) {
break; // 仍在等待
}
if (front.download_result == ActionResult::OK) {
front.file_success.insert(front.downloading_file);
std::string msg_ok = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件:" + front.downloading_file
+ " 执行完成";
append_recall_record_line(dev.guid, lm.monitor_id, msg_ok);
std::cout << "[check_recall_stat] DL ok dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " file=" << front.downloading_file << std::endl;
// 清空当前文件标志,进入下一轮取队首
front.downloading_file.clear();
front.download_result = ActionResult::PENDING;
break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录
} else {
std::string msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件:" + front.downloading_file
+ " 执行失败";
append_recall_record_line(dev.guid, lm.monitor_id, msg_fail);
// 失败:直接尝试下一个文件(不中断整条补招)
std::cout << "[check_recall_stat] DL fail dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " file=" << front.downloading_file << std::endl;
front.downloading_file.clear();
front.download_result = ActionResult::PENDING;
break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录
}
}
}
// 一个终端只推进“一个监测点的首条”状态机,避免并行
break;
}
// 本终端已有 RUNNING 项,且已推进;不再挑选新的 NOT_STARTED开始下一个终端的处理
continue;
}
// 3) 没有 RUNNING挑选第一条 NOT_STARTED并发起“首个目录”的请求
for (auto& lm : dev.line) {
if (lm.recall_list_static.empty()) continue; //跳过补招列表为空的测点
RecallFile& front = lm.recall_list_static.front(); //取测点第一条记录
if (front.recall_status == static_cast<int>(RecallStatus::NOT_STARTED)) { //补招未开始
// 标记为 RUNNING并设置终端忙状态
front.recall_status = static_cast<int>(RecallStatus::RUNNING); //该补招记录刷新为补招中
dev.isbusy = 1; //装置由idle标记为忙
dev.busytype = static_cast<int>(DeviceState::READING_STATSFILE);//装置状态刷新为正在补招文件
dev.busytimecount = 0; //清空业务超时计数
dev.guid = front.recall_guid;
// 初始化状态机并发出第一个目录请求
front.reset_runtime(true);//保留直下文件信息
front.phase = RecallPhase::LISTING; //正在请求并等待“目录文件名列表”
if (!front.dir_candidates.empty()) {//目录列表非空
front.cur_dir_index = 0; //正在尝试的目录下标
front.cur_dir = front.dir_candidates[0]; //第一个目录
front.list_result = ActionResult::PENDING; //目录状态:等待返回
ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir);//调用目录请求接口
std::cout << "[check_recall_stat] LIST start dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id
<< " dir=" << front.cur_dir
<< " start=" << front.StartTime
<< " end=" << front.EndTime << std::endl;
} else {
front.recall_status = static_cast<int>(RecallStatus::FAILED); //目录列表空,失败
std::string msg_fail;
if (front.direct_mode) {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,目标时标:"
+ front.target_filetimes;
} else {
msg_fail = std::string("监测点:") + lm.monitor_name
+ " 补招波形文件未找到,时间范围:"
+ front.StartTime + " ~ " + front.EndTime;
}
append_recall_record_line(dev.guid, lm.monitor_id, msg_fail);
std::cout << "[check_recall_stat] empty dir_candidates, FAIL dev=" << dev.terminal_id
<< " monitor=" << lm.monitor_id << std::endl;
}
// 每终端本轮仅取一条
break;
}
}
} // end for dev
} // 解锁
// ★修改开始:锁外执行上传、回写 JSON、删除本地文件并回锁做台账清理
for (auto& pu : pending_uploads) {
// 1) 上传所有文件:构造临时 qvvr_file 给现有 SendAllQvvrFiles 使用
qvvr_file tmp;
// 原tmp.file_download = pu.files_to_send;
tmp.file_download.clear();
tmp.file_download.assign(pu.files_to_send.begin(), pu.files_to_send.end()); // ★修复vector -> list
std::string wavepath;
bool sent_ok = SendAllQvvrFiles(tmp, wavepath);
if (sent_ok) {
// 2) 回写 JSON若匹配到事件
if (pu.has_matched) {
transfer_json_qvvr_data(pu.terminal_id,
pu.logical_seq,
pu.matched.QVVR_Amg,
pu.matched.QVVR_PerTime,
pu.matched.QVVR_time,
pu.matched.QVVR_type,
pu.matched.phase,
wavepath);
}
else{
//打印提示:没有从装置读到匹配的事件,使用文件中的数据进行更新
std::cout << "[Upload] No matched QVVR event from device for terminal="
<< pu.terminal_id << " seq=" << pu.logical_seq
<< ", using data extracted from .cfg file for JSON update.\n";
transfer_json_qvvr_data(pu.terminal_id,
pu.logical_seq,
//从文件中获取
pu.mismatched.QVVR_Amg,
pu.mismatched.QVVR_PerTime,
pu.mismatched.QVVR_time,
pu.mismatched.QVVR_type,
pu.mismatched.phase,
wavepath);
}
// 3) 删除本地文件
for (const auto& f : pu.files_to_send) {
if (std::remove(f.c_str()) != 0) {
std::cerr << "[Cleanup] Failed to delete file: " << f << "\n";
} else {
std::cout << "[Cleanup] Deleted uploaded file: " << f << "\n";
}
}
// 4) 回锁做台账清理:删除对应 qvvr_file 组与匹配到的事件
{
std::lock_guard<std::mutex> lk(ledgermtx);
for (auto& dev : terminal_devlist) {
if (dev.terminal_id != pu.terminal_id) continue;
for (auto& lm : dev.line) {
unsigned short seq = 0;
try { seq = static_cast<unsigned short>(std::stoi(lm.logical_device_seq)); } catch (...) { seq = 0; }
if (seq != pu.logical_seq) continue;
// 定位并删除相同签名的组
auto it_qf = std::find_if(lm.qvvrevent.qvvrfile.begin(),
lm.qvvrevent.qvvrfile.end(),
[&](const qvvr_file& x){
std::set<std::string> n(x.file_name.begin(), x.file_name.end());
std::set<std::string> d(x.file_download.begin(), x.file_download.end());
return n==pu.sig_names && d==pu.sig_downs;
});
if (it_qf != lm.qvvrevent.qvvrfile.end()) {
lm.qvvrevent.qvvrfile.erase(it_qf);
} else {
std::cerr << "[Cleanup] qvvrfile changed; target group not found, skip erase\n";
}
// 如有匹配事件,按时间删除
if (pu.has_matched) {
auto it_evt = std::find_if(lm.qvvrevent.qvvrdata.begin(),
lm.qvvrevent.qvvrdata.end(),
[&](const qvvr_data& d){ return d.QVVR_time == pu.matched.QVVR_time; });
if (it_evt != lm.qvvrevent.qvvrdata.end()) {
lm.qvvrevent.qvvrdata.erase(it_evt);
}
}
}
}
}
} else {
std::cerr << "[check_recall_file][Upload] SendAllQvvrFiles failed for terminal="
<< pu.terminal_id << " seq=" << pu.logical_seq << std::endl;
}
}
// ★修改结束
// ★说明:本函数不主动把 RUNNING 改 DONE/FAILED
// 由“其他线程”在目录/下载结果返回后,找到对应 dev/monitor 的 lm.recall_list_static.front()
// 写入:
// - 目录请求front.dir_files[front.cur_dir] = {...}; front.list_result = OK/FAIL;
// - 文件下载front.download_result = OK/FAIL;
// 下一轮本函数会推进状态机(继续本目录的文件或切到下一个目录)。
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////根据下发的指令直接补招文件
// ★新增:直下文件的任务入队(终端、测点、文件名)
// 约定:外部线程的回调,照旧把目录返回值写到 lm.recall_list_static.front().dir_files[dir]
// 并置 front.list_result=OK/FAIL下载回执置 front.download_result=OK/FAIL。
// 处理指令部分将文件名拼接出来调用这个函数
bool enqueue_direct_download(const std::string& dev_id,
const std::string& monitor_id,
const std::string& filetime,
const std::string& guid)
{
std::lock_guard<std::mutex> lk(ledgermtx);
// 找终端
auto dev_it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev& d){ return d.terminal_id == dev_id; });
if (dev_it == terminal_devlist.end()) return false;
// 找监测点
auto lm_it = std::find_if(dev_it->line.begin(), dev_it->line.end(),
[&](const ledger_monitor& lm){ return lm.monitor_id == monitor_id; });
if (lm_it == dev_it->line.end()) return false;
// 组装一条 RecallFile
RecallFile rf;
rf.recall_guid = guid;
rf.recall_status = static_cast<int>(RecallStatus::NOT_STARTED);
rf.StartTime = "1970-01-01 00:00:00"; // 仅占位,直下文件不会用到时间窗
rf.EndTime = "1970-01-01 00:00:01";
//rf.dir_candidates = dir_candidates; // 要检索的目录列表和默认的一致
rf.direct_mode = true; // ★关键:直下文件
rf.target_filetimes=filetime; // ▲单个文件时间入“列表”
lm_it->recall_list_static.push_back(std::move(rf));
// 若设备空闲,可直接置忙(可选,视你的流程而定)
if (dev_it->busytype == static_cast<int>(DeviceState::IDLE)) {
dev_it->isbusy = 1;
dev_it->busytype = static_cast<int>(DeviceState::READING_STATSFILE); // 或 READING_EVENTFILE
dev_it->busytimecount = 0;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////通讯响应处理
void filemenu_cache_put(const std::string& dev_id,
std::vector<tag_dir_info> FileList)
{
std::lock_guard<std::mutex> lk(g_filemenu_cache_mtx);
g_filemenu_cache[dev_id] = std::move(FileList); // 直接存 vector<tag_dir_info>
}
bool filemenu_cache_take(const std::string& dev_id, std::vector<tag_dir_info>& out)
{
std::lock_guard<std::mutex> lk(g_filemenu_cache_mtx);
auto it = g_filemenu_cache.find(dev_id);
if (it == g_filemenu_cache.end()) return false;
out = std::move(it->second); // 转移 vector<tag_dir_info>
g_filemenu_cache.erase(it); // 删除缓存
return true;
}
// 提取文件名列表(仅 flag==1
static inline void build_file_name_list(const std::vector<tag_dir_info>& in,
std::list<std::string>& out)
{
out.clear();
for (const auto& e : in) {
if (e.flag == 1) {
// 安全地从固定长度 char[64] 转成 std::string
size_t n = ::strnlen(e.name, sizeof(e.name));
out.emplace_back(std::string(e.name, n));
}
}
}
void on_device_response_minimal(int response_code,
const std::string& id,
unsigned char cid,
int device_state_int)
{
const bool ok = is_ok(response_code);
DeviceState state = static_cast<DeviceState>(device_state_int);
switch (state) {
// ================= 特殊:补招事件日志 =================
case DeviceState::READING_EVENTLOG: {
std::lock_guard<std::mutex> lk(ledgermtx);
terminal_dev* dev = nullptr;
for (auto& d : terminal_devlist) {
if (d.terminal_id == id) { dev = &d; break; }
}
if (!dev) {
std::cout << "[RESP][EVENTLOG] dev not found, id=" << id
<< " rc=" << response_code << std::endl;
return;
}
// 找到“首条 RUNNING”的补招条目按 rc==200 成功/失败标记
bool updated = false;
// 1) 找到 RecallStatus::RUNNING 的 monitor优先
// 若没有,则回退到按 cid -> logical_device_seq 匹配
ledger_monitor* matched_monitor = nullptr;
//优先:遍历查找首条补招项处于 RUNNING 的监测点
for (auto& lm : dev->line) {
if (!lm.recall_list.empty()) {
const RecallMonitor& head = lm.recall_list.front();
if (head.recall_status == static_cast<int>(RecallStatus::RUNNING)) {
matched_monitor = &lm;
break;
}
}
}
//未找到 RUNNING 的监测点时,按原逻辑用 cid 匹配 logical_device_seq
if (!matched_monitor) {
const std::string cid_str = std::to_string(static_cast<int>(cid));
for (auto& lm : dev->line) {
if (lm.logical_device_seq == cid_str) {
matched_monitor = &lm;
break;
}
}
}
if (!matched_monitor) { // 没有匹配的监测点
std::cout << "[RESP][EVENTLOG][WARN] dev=" << id
<< " cannot find monitor (no RUNNING item, no cid match), cid="
<< static_cast<int>(cid) << std::endl;
} else {
// 2) 仅更新该监测点 recall_list 的首条,且要求处于 RUNNING
if (!matched_monitor->recall_list.empty()) {
RecallMonitor& front = matched_monitor->recall_list.front(); //取出首条
if (front.recall_status == static_cast<int>(RecallStatus::RUNNING)) { //正在补招
if (ok) { //补招成功
front.recall_status = static_cast<int>(RecallStatus::DONE); //标记为完成,补招任务会执行下一条
std::cout << "[RESP][EVENTLOG][OK] dev=" << id
<< " mon=" << matched_monitor->monitor_id
<< " " << front.StartTime << "~" << front.EndTime
<< " recall_status=" << front.recall_status
<< std::endl;
} else if (response_code == 404) { //补招无数据
front.recall_status = static_cast<int>(RecallStatus::EMPTY);
std::cout << "[RESP][EVENTLOG][WARN] dev=" << id
<< " mon=" << matched_monitor->monitor_id
<< " " << front.StartTime << "~" << front.EndTime
<< " rc=" << response_code << " recall_status=" << front.recall_status << std::endl; //错误响应码
//记录日志
DIY_ERRORLOG_CODE(matched_monitor->monitor_id.c_str(),2,static_cast<int>(LogCode::LOG_CODE_RECALL),"【ERROR】监测点:%s 补招数据失败 - 失败时间点:%lld 至 %lld",matched_monitor->monitor_id.c_str(),front.StartTime,front.EndTime);
}
else { //补招失败
front.recall_status = static_cast<int>(RecallStatus::FAILED);
std::cout << "[RESP][EVENTLOG][FAIL] dev=" << id
<< " mon=" << matched_monitor->monitor_id
<< " " << front.StartTime << "~" << front.EndTime
<< " rc=" << response_code << " recall_status=" << front.recall_status<< std::endl; //错误响应码
//记录日志
DIY_ERRORLOG_CODE(matched_monitor->monitor_id.c_str(),2,static_cast<int>(LogCode::LOG_CODE_RECALL),"【ERROR】监测点:%s 补招数据失败 - 失败时间点:%lld 至 %lld",matched_monitor->monitor_id.c_str(),front.StartTime,front.EndTime);
}
updated = true;
} else { //首条不是 RUNNING 状态,不应该收到这条响应
std::cout << "[RESP][EVENTLOG][WARN] dev=" << id
<< " mon=" << matched_monitor->monitor_id
<< " first item not RUNNING (status="
<< front.recall_status << ")" << std::endl; //打印调试
}
} else { //补招列表为空,不应该收到这条响应
std::cout << "[RESP][EVENTLOG][WARN] dev=" << id
<< " mon=" << matched_monitor->monitor_id
<< " recall_list empty" << std::endl;
}
}
if (!updated) {//不更新补招列表
std::cout << "[RESP][EVENTLOG][WARN] no RUNNING item, dev=" << id
<< " rc=" << response_code << std::endl;
}
break;
}
// ================= 特殊:读取文件目录 =================
case DeviceState::READING_FILEMENU: {
std::lock_guard<std::mutex> lk(ledgermtx);
// 1) 找到对应终端
terminal_dev* dev = nullptr;
for (auto& d : terminal_devlist) {
if (d.terminal_id == id) { dev = &d; break; }
}
if (!dev) {
std::cout << "[RESP][FILEMENU] dev not found, id=" << id
<< " rc=" << response_code << std::endl;
break;
}
std::vector<tag_dir_info> names;
// 2) 按该终端当前 busytype 分支处理
const int bt = dev->busytype;
if (bt == static_cast<int>(DeviceState::READING_FILEMENU)) {
// ====== 分支 A当前业务就是“读取文件目录” ======
if (ok) {
if (filemenu_cache_take(id, names)) {
//发送目录
send_file_list(dev,names);
} else {
// 失败响应web并复位为空闲
//send_reply_to_cloud(static_cast<int>(ResponseCode::BAD_REQUEST), id, static_cast<int>(DeviceState::READING_FILEMENU),dev->guid,dev->mac);
send_reply_to_queue(dev->guid, static_cast<int>(ResponseCode::BAD_REQUEST),
"终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"失败,停止该业务处理");
std::cout << "[RESP][FILEMENU->FILEMENU][WARN] dev=" << id
<< " names missing in cache" << std::endl;
}
// 成功:复位
dev->guid.clear();
dev->isbusy = 0;
dev->busytype = 0;
dev->busytimecount = 0;
std::cout << "[RESP][FILEMENU->FILEMENU][OK] dev=" << id << std::endl;
} else {
// 失败响应web并复位为空闲
//send_reply_to_cloud(response_code, id, static_cast<int>(DeviceState::READING_FILEMENU),dev->guid,dev->mac);
send_reply_to_queue(dev->guid, static_cast<int>(ResponseCode::BAD_REQUEST),
"终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"失败,停止该业务处理");
dev->guid.clear();
dev->isbusy = 0;
dev->busytype = 0;
dev->busytimecount = 0;
std::cout << "[RESP][FILEMENU->FILEMENU][FAIL] dev=" << id
<< " rc=" << response_code << std::endl;
}
}
else if (
bt == static_cast<int>(DeviceState::READING_EVENTFILE)
|| bt == static_cast<int>(DeviceState::READING_STATSFILE)
) {
// ====== 分支 B当前业务为“事件文件/统计文件”(两者处理相同) ======
// 一个装置同一时刻只会有一个监测点在补招
ledger_monitor* running_monitor = nullptr;
RecallFile* running_front = nullptr;
// 在该终端下,找到“首条 recall_list_static.front() 正在运行且处于 LISTING 的监测点”
for (auto& lm : dev->line) {
if (lm.recall_list_static.empty()) continue;
RecallFile& f = lm.recall_list_static.front();
if (f.recall_status == static_cast<int>(RecallStatus::RUNNING)//补招中
&& f.phase == RecallPhase::LISTING) {//正在请求目录
running_monitor = &lm; //补招的测点
running_front = &f; //补招记录
break;
}
else {
// 不是正在补招的测点,跳过//打印跳过的信息
std::cout << "[check_recall_file] skip non-running monitor dev=" << dev->terminal_id
<< " monitor=" << lm.monitor_id
<< " status=" << static_cast<int>(f.recall_status)
<< " phase=" << static_cast<int>(f.phase)
<< std::endl;
continue;
}
}
if (!running_monitor || !running_front) { //该装置没有正在补招的测点和补招记录,退出处理
std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][WARN] dev=" << id
<< " no RUNNING/LISTING recall on this device, ignore resp"
<< " rc=" << response_code << std::endl;
break;
}
// 根据回执结果,回写目录结果;状态机会在下一轮推进到下一个目录/结束
if (ok) {
if(filemenu_cache_take(id, names)) { // 从缓存取目录列表
running_front->dir_files[running_front->cur_dir] = std::move(names);
running_front->list_result = ActionResult::OK;
std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][OK] dev=" << id
<< " monitor=" << running_monitor->monitor_id
<< " dir=" << running_front->cur_dir
<< " entries=" << running_front->dir_files[running_front->cur_dir].size()
<< std::endl;
} else {
running_front->dir_files[running_front->cur_dir] = {};
running_front->list_result = ActionResult::OK;
std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][WARN] dev=" << id
<< " monitor=" << running_monitor->monitor_id
<< " dir=" << running_front->cur_dir
<< " cache miss -> treat as empty OK" << std::endl;
}
} else {
running_front->list_result = ActionResult::FAIL;
std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][FAIL] dev=" << id
<< " monitor=" << running_monitor->monitor_id
<< " dir=" << running_front->cur_dir
<< " rc=" << response_code << std::endl;
}
break;
}
else {
// ====== 分支 C其他业务场景下收到 FILEMENU 响应,统一处理 打印提示======
if (ok) {
// 成功:
std::cout << "[unknown][RESP][FILEMENU->OTHER][OK] dev=" << id
<< " keep busytype=" << bt << std::endl;
} else {
// 失败:
std::cout << "[unknown][RESP][FILEMENU->OTHER][FAIL] dev=" << id
<< " rc=" << response_code << " prev busytype=" << bt << std::endl;
}
}
break;
}
// ================= 特殊:下载文件 =================
case DeviceState::READING_FILEDATA: {
std::lock_guard<std::mutex> lk(ledgermtx);
// 1) 找到对应终端
terminal_dev* dev = nullptr;
for (auto& d : terminal_devlist) {
if (d.terminal_id == id) { dev = &d; break; }
}
if (!dev) {
std::cout << "[RESP][FILEDATA] dev not found, id=" << id
<< " rc=" << response_code << std::endl;
break;
}
// 2) 按该终端当前 busytype 分支处理
const int bt = dev->busytype;
if (bt == static_cast<int>(DeviceState::READING_FILEDATA)) {
// ====== 分支 A当前业务就是“读取文件数据” ======
if (ok) {
// 成功:复位
//send_reply_to_cloud(static_cast<int>(ResponseCode::OK), id, static_cast<int>(DeviceState::READING_FILEDATA),dev->guid,dev->mac);
send_reply_to_queue(dev->guid, static_cast<int>(ResponseCode::OK),
"终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"成功,停止该业务处理");
dev->guid.clear();
dev->isbusy = 0;
dev->busytype = 0;
dev->busytimecount = 0;
std::cout << "[RESP][FILEDATA->FILEDATA][OK] dev=" << id << std::endl;
} else {
// 失败响应web并复位
//send_reply_to_cloud(response_code, id, static_cast<int>(DeviceState::READING_FILEDATA),dev->guid,dev->mac);
send_reply_to_queue(dev->guid, static_cast<int>(ResponseCode::BAD_REQUEST),
"终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"失败,停止该业务处理");
dev->guid.clear();
dev->isbusy = 0;
dev->busytype = 0;
dev->busytimecount = 0;
std::cout << "[RESP][FILEDATA->FILEDATA][FAIL] dev=" << id
<< " rc=" << response_code << std::endl;
}
}
else if (
bt == static_cast<int>(DeviceState::READING_EVENTFILE)
|| bt == static_cast<int>(DeviceState::READING_STATSFILE)
) {
// ====== 分支 B当前业务为“下载事件文件/统计文件”(两者处理相同) ======
//优先:找“首条处于 RUNNING 且 phase==DOWNLOADING”的监测点
ledger_monitor* matched_monitor = nullptr;
RecallFile* running_front = nullptr;
for (auto& lm : dev->line) {
if (lm.recall_list_static.empty()) continue;
RecallFile& f = lm.recall_list_static.front();
std::cout << "[RESP][FILEDATA][SCAN] dev=" << id
<< " monitor=" << lm.monitor_id
<< " status=" << static_cast<int>(f.recall_status)
<< " phase=" << static_cast<int>(f.phase)
<< std::endl;
if (f.recall_status == static_cast<int>(RecallStatus::RUNNING) &&
f.phase == RecallPhase::DOWNLOADING) {
matched_monitor = &lm;
running_front = &f;
break;
}
}
// 若没找到 RUNNING/DOWNLOADING用 cid 精确匹配测点号
if (!matched_monitor) {
const std::string cid_str = std::to_string(static_cast<int>(cid));
for (auto& lm : dev->line) {
if (lm.logical_device_seq == cid_str) {
matched_monitor = &lm;
break;
}
}
if (matched_monitor && !matched_monitor->recall_list_static.empty()) {
RecallFile& f = matched_monitor->recall_list_static.front();
if (f.recall_status == static_cast<int>(RecallStatus::RUNNING) &&
f.phase == RecallPhase::DOWNLOADING) {
running_front = &f;
std::cout << "[RESP][FILEDATA][FALLBACK-CID] dev=" << id
<< " cid=" << static_cast<int>(cid)
<< " monitor=" << matched_monitor->monitor_id << std::endl;
} else {
std::cout << "[RESP][FILEDATA][WARN] dev=" << id
<< " cid matched monitor=" << matched_monitor->monitor_id
<< " but status=" << f.recall_status
<< " phase=" << static_cast<int>(f.phase)
<< " — ignore this resp" << std::endl;
matched_monitor = nullptr;
}
}
}
if (!matched_monitor || !running_front) {
std::cout << "[RESP][FILEDATA->(EVENT/STATS)FILE][WARN] dev=" << id
<< " no RUNNING/DOWNLOADING recall to accept filedata"
<< " rc=" << response_code
<< " cid=" << static_cast<int>(cid) << std::endl;
break;
}
// 到这里一定是 RUNNING/DOWNLOADING 的 front按回执设置下载结果
if (ok) {
running_front->download_result = ActionResult::OK;
std::cout << "[RESP][FILEDATA->(EVENT/STATS)FILE][OK] dev=" << id
<< " monitor=" << matched_monitor->monitor_id
<< " file=" << running_front->downloading_file << std::endl;
} else {
running_front->download_result = ActionResult::FAIL;
std::cout << "[RESP][FILEDATA->(EVENT/STATS)FILE][FAIL] dev=" << id
<< " monitor=" << matched_monitor->monitor_id
<< " file=" << running_front->downloading_file
<< " rc=" << response_code << std::endl;
}
break;
}
else {
// ====== 分支 C其他业务场景下收到 FILEDATA 响应,统一处理 打印提示======
if (ok) {
// 成功:
std::cout << "[unknown][RESP][FILEDATA->OTHER][OK] dev=" << id
<< " keep busytype=" << bt << std::endl;
} else {
// 失败:
std::cout << "[unknown][RESP][FILEDATA->OTHER][FAIL] dev=" << id
<< " rc=" << response_code << " prev busytype=" << bt << std::endl;
}
}
break;
}
// ================= 其它状态统一处理 =================
default: {
std::lock_guard<std::mutex> lk(ledgermtx);
terminal_dev* dev = nullptr;
for (auto& d : terminal_devlist) {
if (d.terminal_id == id) { dev = &d; break; }
}
if (dev) {
//直接根据输入响应mq
//send_reply_to_cloud(response_code, id, device_state_int, dev->guid, dev->mac);
send_reply_to_queue(dev->guid, response_code,
"终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) + "," + ResponseCodeToString(response_code) + "停止该业务处理");
//其他的错误和成功都会结束业务
dev->guid.clear(); // 清空 guid
dev->busytype = 0; // 业务类型归零
dev->isbusy = 0; // 清空业务标志
dev->busytimecount = 0; // 计时归零
std::cout << "[clear_terminal_runtime_state] Cleared runtime state for terminal_id="
<< id << std::endl;
}
break;
}
} // end switch
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////记录暂态事件到本地
// 将一条暂态数据更新/写入到指定终端ID与逻辑序号的监测点
// 返回 true 表示已写入更新或追加false 表示未找到对应终端/监测点。
bool append_qvvr_event(const std::string& terminal_id,
int logical_seq, // 监测点序号(如 1
int nType, // 事件类型
double fPersisstime_sec, // 持续时间(秒)
double fMagnitude_pu, // 幅值pu
uint64_t triggerTimeMs, // 触发时间(毫秒)
int phase) // 相别
{
std::cout << "[append_qvvr_event] enter"
<< " tid=" << std::this_thread::get_id()
<< " terminal_id=" << terminal_id
<< " logical_seq=" << logical_seq
<< " type=" << nType
<< " per_s=" << fPersisstime_sec
<< " mag_pu=" << fMagnitude_pu
<< " time_ms=" << static_cast<unsigned long long>(triggerTimeMs)
<< " phase=" << phase
<< std::endl;
{
std::lock_guard<std::mutex> lk(ledgermtx);
std::cout << "[append_qvvr_event] lock acquired. terminal_devlist.size="
<< terminal_devlist.size() << std::endl;
// 1) 找终端
auto dev_it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev& d){ return d.terminal_id == terminal_id; });
if (dev_it == terminal_devlist.end()) {
std::cout << "[append_qvvr_event][MISS] terminal not found: "
<< terminal_id << std::endl;
return false;
}
std::cout << "[append_qvvr_event][HIT] terminal_id=" << terminal_id
<< " monitors(line).size=" << dev_it->line.size()
<< std::endl;
// 2) 找监测点(按逻辑序号匹配:字符串等于 或 数值等于)
ledger_monitor* pMon = nullptr;
for (size_t i = 0; i < dev_it->line.size(); ++i) {
auto& m = dev_it->line[i];
bool eq_str = (m.logical_device_seq == std::to_string(logical_seq));
bool eq_num = false;
try {
if (!m.logical_device_seq.empty())
eq_num = (std::stoi(m.logical_device_seq) == logical_seq);
} catch (...) {
// 仅调试提示,不改变原逻辑
std::cout << "[append_qvvr_event][monitor #" << i
<< "] stoi fail, logical_device_seq=\""
<< m.logical_device_seq << "\"" << std::endl;
}
std::cout << "[append_qvvr_event][probe monitor #" << i
<< "] monitor_id=" << m.monitor_id
<< " logical_device_seq=\"" << m.logical_device_seq << "\""
<< " eq_str=" << eq_str << " eq_num=" << eq_num << std::endl;
if (eq_str || eq_num) { pMon = &m; break; }
}
if (!pMon) {
std::cout << "[append_qvvr_event][MISS] monitor not found by seq="
<< logical_seq << " in terminal_id=" << terminal_id << std::endl;
return false;
}
std::cout << "[append_qvvr_event][HIT] monitor_id=" << pMon->monitor_id
<< " logical_device_seq=" << pMon->logical_device_seq
<< " qvvrdata.size=" << pMon->qvvrevent.qvvrdata.size()
<< std::endl;
qvvr_event& qe = pMon->qvvrevent;
// 3) 先尝试“就地更新”(同类型 + 同时间戳 视为同一事件)
for (size_t i = 0; i < qe.qvvrdata.size(); ++i) {
auto& q = qe.qvvrdata[i];
if (q.QVVR_type == nType && q.QVVR_time == triggerTimeMs) {
std::cout << "[append_qvvr_event][UPDATE match idx=" << i << "]"
<< " old{used=" << q.used_status
<< ", per=" << q.QVVR_PerTime
<< ", mag=" << q.QVVR_Amg
<< ", phase=" << q.phase
<< "} -> new{used=true"
<< ", per=" << fPersisstime_sec
<< ", mag=" << fMagnitude_pu
<< ", phase=" << phase
<< "}" << std::endl;
//lnk20251030
q.is_pair = false;
q.used_status = true;
q.QVVR_PerTime = fPersisstime_sec;
q.QVVR_Amg = fMagnitude_pu;
q.phase = phase;
std::cout << "[append_qvvr_event] done(update)."
<< std::endl;
return true; //更新完毕
}
}
//新的事件
// 4) 复用空槽used_status=false
for (size_t i = 0; i < qe.qvvrdata.size(); ++i) {
auto& q = qe.qvvrdata[i];
if (!q.used_status) {
std::cout << "[append_qvvr_event][REUSE idx=" << i << "]"
<< " set{type=" << nType
<< ", time_ms=" << static_cast<unsigned long long>(triggerTimeMs)
<< ", per=" << fPersisstime_sec
<< ", mag=" << fMagnitude_pu
<< ", phase=" << phase
<< "}" << std::endl;
//lnk20251030
q.is_pair = false;
q.used_status = true;
q.QVVR_type = nType;
q.QVVR_time = triggerTimeMs;
q.QVVR_PerTime = fPersisstime_sec;
q.QVVR_Amg = fMagnitude_pu;
q.phase = phase;
std::cout << "[append_qvvr_event] done(reuse)."
<< std::endl;
return true; //复用完毕
}
}
// 5) 直接追加
qvvr_data q{};
//lnk20251030
q.is_pair = false;
q.used_status = true;
q.QVVR_type = nType;
q.QVVR_time = triggerTimeMs; // ms
q.QVVR_PerTime = fPersisstime_sec; // s
q.QVVR_Amg = fMagnitude_pu;
q.phase = phase;
qe.qvvrdata.push_back(q);
std::cout << "[append_qvvr_event][PUSH_BACK]"
<< " new_size=" << qe.qvvrdata.size()
<< " last_idx=" << (qe.qvvrdata.empty() ? -1 : (int)qe.qvvrdata.size()-1)
<< " {type=" << nType
<< ", time_ms=" << static_cast<unsigned long long>(triggerTimeMs)
<< ", per=" << fPersisstime_sec
<< ", mag=" << fMagnitude_pu
<< ", phase=" << phase
<< "}" << std::endl;
}
std::cout << "[append_qvvr_event] done(push_back)."
<< std::endl;
return true;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////实时数据封装发送
void enqueue_realtime_pq(const RealtagPqDate_float& realdata,
int nPTType,
unsigned char cid,
const std::string& mac,
const std::string& devid)
{
// 先根据 devIdxMap 的配置决定编码分支:
// 2: 基础数据 3: 谐波电压含有率 4: 谐波电流有效值 5: 间谐波电压含有率
int idx = 0;
std::string base64;
// 这里尝试用 mac 作为 key 获取 idx如果项目里 devIdxMap 的 key 不是 mac
// 你可以把这里改成对应的设备 iddevid。未命中则再尝试用规范化后的 mac。
if (devidx_get(devid, idx)) {
switch (idx) {
case 2: // 基础数据(根据接线方式选择转换方法 数据全集解析)
base64 = realdata.ConvertToBase64(nPTType);
break;
case 3: // 谐波电压含有率
base64 = realdata.ConvertToBase64_RtHarmV(nPTType);
break;
case 4: // 谐波电流有效值(幅值)
base64 = realdata.ConvertToBase64_RtHarmI();
break;
case 5: // 间谐波电压含有率
base64 = realdata.ConvertToBase64_RtInHarmV();
break;
default:
// 未知 idx回退到基础数据
base64 = realdata.ConvertToBase64(nPTType);
break;
}
} else {
// 未配置 idx回退到基础数据
base64 = realdata.ConvertToBase64(nPTType);
}
//std::cout << base64 << std::endl;
//lnk实时数据使用接口发送20250711
time_t data_time = ConvertToTimestamp(realdata.time);
{
std::lock_guard<std::mutex> lk(g_last_ts_mtx);
auto it = g_last_ts_by_devid.find(devid);
if (it != g_last_ts_by_devid.end() && it->second == data_time) {
std::cout << "[enqueue_realtime_pq] duplicate timestamp, devid="
<< devid << " time=" << data_time << std::endl;
// 同一设备与上次时间相同 → 丢弃本次
return;
}
// 记录本次时间
g_last_ts_by_devid[devid] = data_time;
std::cout << "[enqueue_realtime_pq] record timestamp, devid="
<< devid << " time=" << data_time << std::endl;
}
std::vector<DataArrayItem> arr;
arr.push_back({1, //数据属性 -1-无, 0-“Rt”,1-“Max”,2-“Min”,3-“Avg”,4-“Cp95”
data_time, //数据转换出来的时间数据时标相对1970年的秒无效填入“-1”
0, //数据时标,微秒钟,无效填入“-1”
0, //数据标识1-标识数据异常
base64});
std::string js = generate_json(
normalize_mac(mac),
-1, //需应答的报文订阅者收到后需以此ID应答无需应答填入“-1”
1, //设备唯一标识Ldid填入0代表Ndid,后续根据商议决定填id还是数字
1, //报文处理的优先级1 I类紧急请求/响应 2 II类紧急请求/响应 3 普通请求/响应 4 广播报文
0x1302, //设备数据主动上送的数据类型
cid, //逻辑子设备ID0-逻辑设备本身,无填-1
0x04, //数据类型固定为电能质量数据
1, //数据属性无“0”、实时“1”、统计“2”等
idx, //数据集序号(以数据集方式上送),无填-1
arr //数据数组
);
//std::cout << js << std::en
queue_data_t data;
data.monitor_no = 1; //上送的实时数据没有测点序号统一填1
data.strTopic = TOPIC_RTDATA; //实时topic
data.strText = js;
data.mp_id = ""; //监测点id暂时不需要
data.tag = G_RT_TAG; //实时tag
data.key = G_RT_KEY; //实时key
std::lock_guard<std::mutex> lock(queue_data_list_mutex);
queue_data_list.push_back(data);
}
////////////////////////////////////////////////////////////////////////////////统计数据打包发送
// 封装:组装统计数据并入队发送
void enqueue_stat_pq(const std::string& max_base64Str,
const std::string& min_base64Str,
const std::string& avg_base64Str,
const std::string& cp95_base64Str,
time_t data_time,
const std::string& mac,
short cid)
{
std::vector<DataArrayItem> arr;
arr.push_back({1, //数据属性 -1-无, 0-“Rt”,1-“Max”,2-“Min”,3-“Avg”4-“Cp95”
data_time, //数据转换出来的时间数据时标相对1970年的秒无效填入“-1”
0, //数据时标,微秒钟,无效填入“-1”
0, //数据标识1-标识数据异常
max_base64Str});
arr.push_back({2, data_time, 0, 0, min_base64Str});
arr.push_back({3, data_time, 0, 0, avg_base64Str});
arr.push_back({4, data_time, 0, 0, cp95_base64Str});
std::string js = generate_json(
normalize_mac(mac),
-1, //需应答的报文订阅者收到后需以此ID应答无需应答填入“-1”
1, //设备唯一标识Ldid填入0代表Ndid,后续根据商议决定填id还是数字
1, //报文处理的优先级1 I类紧急请求/响应 2 II类紧急请求/响应 3 普通请求/响应 4 广播报文
0x1302, //设备数据主动上送的数据类型
cid, //逻辑子设备ID0-逻辑设备本身,无填-1avg_data.name
0x04, //数据类型固定为电能质量
2, //数据属性无“0”、实时“1”、统计“2”等
1, //数据集序号(以数据集方式上送),无填-1
arr //数据数组
);
//std::cout << js << std::endl;
queue_data_t data;
data.monitor_no = cid; //监测点序号avg_data.name
data.strTopic = TOPIC_STAT;//统计topic
data.strText = js;
data.mp_id = ""; //监测点id暂时不需要
data.tag = G_ROCKETMQ_TAG; //统计tag
data.key = G_ROCKETMQ_KEY; //统计key
std::lock_guard<std::mutex> lock(queue_data_list_mutex);
queue_data_list.push_back(data);
std::cout << "Successfully assembled tagPqData for line: "
<< cid << std::endl;
}
/////////////////////////////////////////////////////////////////////////////////////清空一个装置运行数据
size_t erase_one_terminals_by_id(const std::string& terminal_id) {
// 先对所有匹配项做日志清理
for (const auto& d : terminal_devlist) {
if (d.terminal_id == terminal_id) {
remove_loggers_by_terminal_id(terminal_id);
}
}
// 再统一擦除
const auto old_size = terminal_devlist.size();
terminal_devlist.erase(
std::remove_if(terminal_devlist.begin(), terminal_devlist.end(),
[&](const terminal_dev& d){ return d.terminal_id == terminal_id; }),
terminal_devlist.end());
return old_size - terminal_devlist.size();
}
///////////////////////////////////////////////////////////////////////////////////////////////
DeviceInfo make_device_from_terminal(const terminal_dev& t) {
DeviceInfo d;
// 基本信息
d.device_id = t.terminal_id;
d.name = t.terminal_name;
d.model = t.dev_type;
d.mac = t.mac;
// status优先按数字解析其次按 online/true/yes/on 识别;默认 0
int status = 0;
if (!t.tmnl_status.empty()) {
bool parsed_num = false;
// 尝试解析为整数
char* endp = nullptr;
long v = std::strtol(t.tmnl_status.c_str(), &endp, 10);
if (endp && *endp == '\0') { parsed_num = true; status = (v != 0) ? 1 : 0; }
if (!parsed_num) {
std::string s = t.tmnl_status;
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
status = (s == "online" || s == "true" || s == "yes" || s == "on" || s == "1") ? 1 : 0;
}
}
d.status = status;
// righttime支持 "1/true/yes/on";默认 false
bool rt = false;
if (!t.Righttime.empty()) {
std::string s = t.Righttime;
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
rt = (s == "1" || s == "true" || s == "yes" || s == "on");
}
d.righttime = rt;
// points
d.points.clear();
d.points.reserve(t.line.size());
for (const auto& m : t.line) {
PointInfo p;
p.point_id = m.monitor_id;
p.name = m.monitor_name;
p.device_id = t.terminal_id;
// nCpuNo从 logical_device_seq 转 ushort非法则置 0
unsigned short cpu_no = 0;
if (!m.logical_device_seq.empty()) {
char* endp = nullptr;
long v = std::strtol(m.logical_device_seq.c_str(), &endp, 10);
if (endp && *endp == '\0' && v >= 0) {
if (v > 0xFFFF) v = 0xFFFF;
cpu_no = static_cast<unsigned short>(v);
}
}
p.nCpuNo = cpu_no;
// 变比与电压等级
p.PT1 = m.PT1;
p.PT2 = m.PT2;
p.CT1 = m.CT1;
p.CT2 = m.CT2;
p.strScale = m.voltage_level;
// 接线方式0-星型 1-角型;支持 "0/1"、包含“角/三角/delta/Δ”
int pttype = 0; // 默认星型
if (!m.terminal_connect.empty()) {
std::string s = m.terminal_connect;
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (s == "1" || s.find("") != std::string::npos || s.find("三角") != std::string::npos
|| s.find("delta") != std::string::npos || s.find("") != std::string::npos || s.find("Δ") != std::string::npos) {
pttype = 1;
}
}
p.nPTType = pttype;
d.points.push_back(std::move(p));
}
return d;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////补招文件
// --------- mkdir -p ----------
static bool mkdir_p(const std::string& path, mode_t mode = 0755) {
if (path.empty()) return false;
if (access(path.c_str(), F_OK) == 0) return true;
std::string cur;
for (size_t i = 0; i < path.size(); ++i) {
cur.push_back(path[i]);
if (path[i] == '/' && !cur.empty()) {
if (access(cur.c_str(), F_OK) != 0) {
if (mkdir(cur.c_str(), mode) != 0 && errno != EEXIST) {
return false;
}
}
}
}
// 最后一层(不以 / 结尾时)
if (access(path.c_str(), F_OK) != 0) {
if (mkdir(path.c_str(), mode) != 0 && errno != EEXIST) {
return false;
}
}
return true;
}
static std::string build_recall_filepath(const std::string& guid,
const std::string& monitorId,
const std::string& start,
const std::string& end) {
std::string dir = "/FeProject/dat/recall/";
ensure_dir_exists("/FeProject");
ensure_dir_exists("/FeProject/dat");
ensure_dir_exists(dir);
// 替换非法字符(例如空格、冒号)
auto sanitize = [](std::string s) {
for (auto& c : s) {
if (c == ' ' || c == ':' || c == '/' || c == '\\') c = '_';
}
return s;
};
std::string filename = sanitize(guid) + "_" + sanitize(monitorId) + "_" +
sanitize(start) + "_" + sanitize(end) + ".txt";
return dir + filename;
}
std::string sanitize_for_filename(std::string s) {
// 把空格->'_',冒号/波浪号/斜杠去掉,避免非法字符
for (char& c : s) {
if (c == ' ') c = '_';
else if (c == ':') c = '-';
else if (c == '~' || c == '/' || c == '\\') c = '-';
}
return s;
}
bool init_recall_record_file(const std::string& guid,
const std::string& terminalId,
const std::string& monitorId,
const std::string& start,
const std::string& end)
{
try {
std::string path = build_recall_filepath(guid, monitorId, start, end);
// 建立索引guid + monitorId -> path
{
std::lock_guard<std::mutex> lk(g_recall_file_mtx);
g_recall_file_index[std::make_pair(guid, monitorId)] = path;
}
std::ofstream ofs(path.c_str(), std::ios::out | std::ios::trunc);
if (!ofs.is_open()) {
std::cerr << "[recall_file] ERROR: cannot open file: " << path
<< " errno=" << errno << " (" << strerror(errno) << ")"
<< std::endl;
return false;
}
// 写入头
ofs << "guid: " << guid << "\n";
ofs << "terminalId: " << terminalId << "\n";
ofs << "monitor_id: " << monitorId << "\n";
ofs << "start_time: " << start << "\n";
ofs << "end_time: " << end << "\n";
ofs << "msglist:\n";
ofs.close();
std::cout << "[recall_file] created file OK: " << path << std::endl;
return true;
}
catch (const std::exception& e) {
std::cerr << "[recall_file] Exception in init_recall_record_file: "
<< e.what() << std::endl;
return false;
}
catch (...) {
std::cerr << "[recall_file] Unknown exception in init_recall_record_file"
<< std::endl;
return false;
}
}
bool append_recall_record_line(const std::string& guid,
const std::string& monitorId,
const std::string& msg) {
std::string path;
{
std::lock_guard<std::mutex> lk(g_recall_file_mtx);
auto it = g_recall_file_index.find(std::make_pair(guid, monitorId));
if (it == g_recall_file_index.end()) return false;
path = it->second;
}
std::ofstream ofs(path.c_str(), std::ios::out | std::ios::app);
if (!ofs.is_open()) return false;
ofs << msg << "\n";
ofs.close();
return true;
}
bool delete_recall_record_file(const std::string& guid,
const std::string& monitorId) {
std::string path;
{
std::lock_guard<std::mutex> lk(g_recall_file_mtx);
auto it = g_recall_file_index.find(std::make_pair(guid, monitorId));
if (it == g_recall_file_index.end()) return false;
path = it->second;
g_recall_file_index.erase(it);
}
// 删除文件
if (access(path.c_str(), F_OK) == 0) {
::remove(path.c_str());
}
return true;
}
static std::string find_recall_file(const std::string& guid,
const std::string& monitorId) {
std::string dir = "/FeProject/dat/recall/";
ensure_dir_exists("/FeProject");
ensure_dir_exists("/FeProject/dat");
ensure_dir_exists(dir);
std::string prefix = sanitize_for_filename(guid) + "_" +
sanitize_for_filename(monitorId) + "_";
DIR* d = opendir(dir.c_str());
if (!d) return "";
std::string found;
struct dirent* ent;
while ((ent = readdir(d)) != nullptr) {
if (ent->d_type == DT_REG) {
std::string name = ent->d_name;
if (name.compare(0, prefix.size(), prefix) == 0) {
found = dir + name;
break;
}
}
}
closedir(d);
return found;
}
bool get_recall_record_fields_by_guid_monitor(const std::string& guid,
const std::string& monitorId,
std::string& outGuid,
std::string& terminalId,
std::string& outMonitorId,
std::string& startTime,
std::string& endTime,
std::string& msg)
{
std::string filepath = find_recall_file(guid, monitorId);
if (filepath.empty()) {
std::cerr << "[recall_file] 未找到匹配文件 guid=" << guid
<< " monitorId=" << monitorId << std::endl;
return false;
}
std::ifstream ifs(filepath.c_str());
if (!ifs.is_open()) {
std::cerr << "[recall_file] 打开文件失败: " << filepath << std::endl;
return false;
}
std::string line;
bool inMsglist = false;
std::ostringstream msgbuf;
while (std::getline(ifs, line)) {
line = trim_copy(line);
if (line.empty()) continue;
if (!inMsglist) {
parse_kv_line(line, "guid", outGuid);
parse_kv_line(line, "terminalId", terminalId);
parse_kv_line(line, "monitor_id", outMonitorId);
parse_kv_line(line, "start_time", startTime);
parse_kv_line(line, "end_time", endTime);
if (line == "msglist:" || line == "msglist") {
inMsglist = true;
}
} else {
msgbuf << line << "\n";
}
}
ifs.close();
msg = msgbuf.str();
if (!msg.empty() && msg.back() == '\n') msg.pop_back();
// ===== 校验 guid 和 monitor_id =====
if (outGuid != guid || outMonitorId != monitorId) {
std::cerr << "[recall_file] 文件头与请求不一致: guid=" << outGuid
<< " monitor_id=" << outMonitorId << std::endl;
return false;
}
// 简单检查字段完整性
if (terminalId.empty()) {
std::cerr << "[recall_file] 文件字段不完整: " << filepath << std::endl;
return false;
}
//将 msg 内的换行符替换为逗号,避免 JSON 解析出错
{
for (auto& ch : msg) {
if (ch == '\n' || ch == '\r') {
ch = ','; // 统一替换为逗号
}
}
// 连续逗号去重(例如 "\n\n" -> ",," -> ",")
while (msg.find(",,") != std::string::npos) {
msg.replace(msg.find(",,"), 2, ",");
}
// 去除首尾多余的逗号
if (!msg.empty() && msg.front() == ',') msg.erase(msg.begin());
if (!msg.empty() && msg.back() == ',') msg.pop_back();
}
std::cout << "[recall_file] 读取成功: " << filepath << std::endl;
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////上送文件选择
// ========================= 自动选择上传URL的封装函数 =========================
// 功能:根据终端 id 判断当前业务类型busytype自动选择不同上传接口。
// 入参和 SendFileWeb 完全一致。
bool SendFileWebAuto(const std::string& id,
const std::string& local_path,
const std::string& remote_path,
std::string& out_filename)
{
try {
// 查找对应装置并判断状态
std::lock_guard<std::mutex> lk(ledgermtx);
const terminal_dev* dev_ptr = nullptr;
for (const auto& d : terminal_devlist) {
if (d.terminal_id == id) {
dev_ptr = &d;
break;
}
}
std::string file_cloudpath;
if (dev_ptr) {
const int bt = dev_ptr->busytype;
// 若处于“事件文件/统计文件”补招阶段则使用补招专用上传目录comtrade/wave/...
if (bt == static_cast<int>(DeviceState::READING_EVENTFILE) ||
bt == static_cast<int>(DeviceState::READING_STATSFILE)) {
std::string rel = dirname_with_slash(local_path); // 例如download/00:B7:.../
// 将 download/ 前缀替换为 wave/
if (!replace_prefix(rel, "download/", "wave/")) {
// 若不是以 download/ 开头,兜底拼 wave/ + 原目录
rel = "wave/" + rel;
}
file_cloudpath = "comtrade/" + rel; // 目标comtrade/wave/00:B7:.../
std::cout << "[SendFileWebAuto] dev=" << id
<< " busytype=" << bt
<< " -> use recall upload URL (cloud path=" << file_cloudpath << ")\n";
} else {
// 非补招场景沿用原来的 download 目录
file_cloudpath = dirname_with_slash(local_path); // 保持原逻辑
std::cout << "[SendFileWebAuto] dev=" << id
<< " busytype=" << bt
<< " -> use default upload URL (cloud path=" << file_cloudpath << ")\n";
}
} else {
std::cout << "[SendFileWebAuto][WARN] device not found for id=" << id
<< ", fallback to default URL\n";
file_cloudpath = dirname_with_slash(local_path);
}
// 实际上传调用
SendFileWeb(WEB_FILEUPLOAD, local_path, file_cloudpath, out_filename);
std::cout << "[SendFileWebAuto] File upload complete: " << out_filename << std::endl;
return true;
} catch (const std::exception& e) {
std::cerr << "[SendFileWebAuto][ERROR] Exception: " << e.what() << std::endl;
} catch (...) {
std::cerr << "[SendFileWebAuto][ERROR] Unknown exception" << std::endl;
}
return false;
}