From abc5f9e73563192adc1418ceb7650f2d156bfde4 Mon Sep 17 00:00:00 2001 From: lnk Date: Fri, 17 Jul 2026 15:58:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0pqdif=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mms/mms_process.c | 346 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 345 insertions(+), 1 deletion(-) diff --git a/mms/mms_process.c b/mms/mms_process.c index 792db3a..f98785a 100644 --- a/mms/mms_process.c +++ b/mms/mms_process.c @@ -44,7 +44,13 @@ SD_CONST static ST_CHAR *SD_CONST thisFileName = __FILE__; #ifdef _OS_UNIX_ #include +#include +#include +#include #endif +#include +#include +#include extern uint32_t g_dead_lock_counter; extern uint32_t g_thread_blocked_times; @@ -95,6 +101,8 @@ static void format_time_ms(long long ms, char* buf, size_t buf_len){ tm_time.tm_sec, milli); } + +static apr_status_t ChannelCheckPQDIFFiles(chnl_usr_t *chnl_usr); //lnk20250122start apr_status_t init_rem_dib_table() { @@ -1636,7 +1644,7 @@ void CheckAllConnectedChannel() HandleFileDirReqForChannel(chnl_usr);//文件目录请求 } if(g_node_id == PQDIF_DATA_BASE_NODE_ID){ - //补招PQDIF文件 + ChannelCheckPQDIFFiles(chnl_usr); } else{ ChannelCheckIECReports(chnl_usr);//报告 @@ -1988,6 +1996,208 @@ static ST_RET Write_Named_Var(LD_info_t *LD_info,chnl_usr_t *chnl_usr,char* VarN #define TICKS_PER_MIN 60LL*1000LL*1000LL + +/* PQDIF 文件召唤参数。 + * C# 读取程序的默认周期是 2 小时,文件名形如: + * PQMonitor_PQM1_20190401_1400_02.pqd + * 其中 “1_20190401_1400_” 用于匹配测点序号和时间片。 + * 当前进程没有单独的手动时间窗输入,所以这里实现自动模式: + * 1. 每个终端按固定扫描间隔读取一次 /PQDIF/ 目录; + * 2. 只下载最近一个完整周期内、且测点序号匹配的 .pqd 文件; + * 3. 下载前检查 /FeProject/data/pqdif/ 下是否已有同名文件,避免重复下载。 + */ +#define PQDIF_REMOTE_DIR "/PQDIF/" +#define PQDIF_LOCAL_DIR "/FeProject/data/pqdif" +#define PQDIF_AUTO_INTERVAL_HOURS 2 +#define PQDIF_SCAN_INTERVAL_MS (60 * 1000) +#define PQDIF_MAX_DEVICE_INDEX 4096 +#define PQDIF_MAX_FETCH_PER_SCAN 32 + +static const char* pqdif_basename(const char* filename) +{ + const char* base = filename; + const char* p; + + if (filename == NULL) + return ""; + + for (p = filename; *p != '\0'; ++p) { + if (*p == '/' || *p == '\\') + base = p + 1; + } + return base; +} + +static int pqdif_has_pqd_suffix(const char* filename) +{ + size_t len; + + if (filename == NULL) + return FALSE; + + len = strlen(filename); + if (len < 4) + return FALSE; + + return (filename[len - 4] == '.' + && tolower((unsigned char)filename[len - 3]) == 'p' + && tolower((unsigned char)filename[len - 2]) == 'q' + && tolower((unsigned char)filename[len - 1]) == 'd'); +} + +static void pqdif_make_time_match(int cpu_no, time_t tm_value, char* out, size_t out_len) +{ + struct tm tm_local; + + memset(&tm_local, 0, sizeof(tm_local)); + localtime_r(&tm_value, &tm_local); + + /* C# 目录匹配逻辑使用 “测点号_yyyyMMdd_HH00_” 作为关键片段。 + * 例如测点号 1、2026-07-17 10 点,对应 “1_20260717_1000_”。 + */ + snprintf(out, out_len, "%d_%04d%02d%02d_%02d00_", + cpu_no % 10, + tm_local.tm_year + 1900, + tm_local.tm_mon + 1, + tm_local.tm_mday, + tm_local.tm_hour); +} + +static int pqdif_match_recent_interval(const char* filename, int cpu_no) +{ + time_t now_time; + time_t hour_floor; + time_t interval_end; + time_t interval_start; + struct tm tm_now; + char start_match[64]; + char end_match[64]; + int hour_mod; + + if (!pqdif_has_pqd_suffix(filename)) + return FALSE; + + if (cpu_no <= 0) + return FALSE; + + now_time = time(NULL); + localtime_r(&now_time, &tm_now); + tm_now.tm_min = 0; + tm_now.tm_sec = 0; + hour_floor = mktime(&tm_now); + + /* 最近一个完整周期: + * 例如当前 11:xx、周期 2 小时,则完整周期是 08:00~10:00。 + * 普通设备按周期开始时间命名,PS_NET_PQDIF 振兴设备按周期结束时间命名。 + * 这里同时匹配开始和结束时间,避免需要额外区分厂家。 + */ + hour_mod = tm_now.tm_hour % PQDIF_AUTO_INTERVAL_HOURS; + interval_end = hour_floor - hour_mod * 3600; + interval_start = interval_end - PQDIF_AUTO_INTERVAL_HOURS * 3600; + + pqdif_make_time_match(cpu_no, interval_start, start_match, sizeof(start_match)); + pqdif_make_time_match(cpu_no, interval_end, end_match, sizeof(end_match)); + + return (strstr(filename, start_match) != NULL || strstr(filename, end_match) != NULL); +} + +static int pqdif_ensure_local_dir() +{ +#ifdef _OS_UNIX_ + if (mkdir("/FeProject/data", 0777) != 0 && errno != EEXIST) + return SD_FAILURE; + + if (mkdir(PQDIF_LOCAL_DIR, 0777) != 0 && errno != EEXIST) + return SD_FAILURE; +#endif + return SD_SUCCESS; +} + +static int pqdif_local_file_exists(const char* filename) +{ + char local_file[512]; + const char* base = pqdif_basename(filename); + + if (base[0] == '\0') + return FALSE; + + snprintf(local_file, sizeof(local_file), "%s/%s", PQDIF_LOCAL_DIR, base); +#ifdef _OS_UNIX_ + return access(local_file, F_OK) == 0; +#else + return FALSE; +#endif +} + +static void pqdif_build_remote_filename(const char* dir_filename, char* out, size_t out_len) +{ + if (dir_filename == NULL || dir_filename[0] == '\0') { + out[0] = '\0'; + return; + } + + /* mms_mvla_fdir 的返回值有两种可能: + * 1. 带目录:/PQDIF/xxx.pqd + * 2. 仅文件名:xxx.pqd + * PQDIF 必须固定从 /PQDIF/ 取,所以仅返回文件名时要补齐远端目录。 + */ + if (strchr(dir_filename, '/') != NULL || strchr(dir_filename, '\\') != NULL) + snprintf(out, out_len, "%s", dir_filename); + else + snprintf(out, out_len, "%s%s", PQDIF_REMOTE_DIR, dir_filename); +} + +static int pqdif_get_file_from_device(chnl_usr_t *chnl_usr, const char *rem_filename, char *only_filename_ret, size_t only_filename_ret_len) +{ + int ret; + char loc_file_fullname[512]; + char rem_file_fullname[512]; + const char *only_filename; + + if (chnl_usr == NULL || chnl_usr->net_info == NULL || rem_filename == NULL || rem_filename[0] == '\0') + return SD_FAILURE; + + only_filename = pqdif_basename(rem_filename); + if (only_filename[0] == '\0') + return SD_FAILURE; + + if (pqdif_ensure_local_dir() != SD_SUCCESS) { + echo_warn1("[PQDIF] create local dir failed: %s\n", PQDIF_LOCAL_DIR); + return SD_FAILURE; + } + + memset(loc_file_fullname,0,sizeof(loc_file_fullname)); + memset(rem_file_fullname,0,sizeof(rem_file_fullname)); + apr_snprintf(loc_file_fullname,sizeof(loc_file_fullname),"%s/%s",PQDIF_LOCAL_DIR,only_filename); + apr_snprintf(rem_file_fullname,sizeof(rem_file_fullname),"%s",rem_filename); + + if (only_filename_ret != NULL && only_filename_ret_len > 0) + apr_snprintf(only_filename_ret,only_filename_ret_len,"%s",only_filename); + + printf("[PQDIF] mms_getFile local=%s remote=%s\n", loc_file_fullname, rem_file_fullname); + + ret = mms_getFile(chnl_usr->net_info,loc_file_fullname,rem_file_fullname,g_pt61850app->mmsOpTimeout); + if (ret != SD_SUCCESS) { + echo_warn3("[PQDIF] mms_getFile failed IP=%s local=%s remote=%s\n", + chnl_usr->ip_str, loc_file_fullname, rem_file_fullname); + return ret; + } + +#ifdef _OS_UNIX_ + { + struct stat st; + + if (stat(loc_file_fullname, &st) != 0 || st.st_size <= 0) { + unlink(loc_file_fullname); + echo_warn2("[PQDIF] file is empty or invalid: %s from %s\n", + loc_file_fullname, rem_file_fullname); + return SD_FAILURE; + } + } +#endif + + return SD_SUCCESS; +} int pt61850_write_cn_file(chnl_usr_t *chnl_usr, ied_t *ied, char *rem_filename, char *only_filename_ret) @@ -2033,6 +2243,140 @@ int pt61850_write_cn_file(chnl_usr_t *chnl_usr, ied_t *ied, char *rem_filename, } return ret; } + +static apr_status_t ChannelCheckPQDIFFiles(chnl_usr_t *chnl_usr) +{ + ied_t *ied; + ied_usr_t *ied_usr; + LD_info_t *LD_info; + char **filenames = NULL; + int filenum = 0; + int ret; + int cpuno; + int file_idx; + int fetched = 0; + int matched = 0; + int scan_idx; + double now_ms; + static double s_last_scan_ms[PQDIF_MAX_DEVICE_INDEX] = {0}; + + if (chnl_usr == NULL || chnl_usr->chnl == NULL || chnl_usr->chnl->ied == NULL || + chnl_usr->chnl->ied->usr_ext == NULL || chnl_usr->net_info == NULL) { + printf("[PQDIF] invalid channel context, skip scan\n"); + return APR_EGENERAL; + } + + ied = chnl_usr->chnl->ied; + ied_usr = (ied_usr_t*)ied->usr_ext; + + /* CheckAllConnectedChannel 会轮询通道;这里按终端做节流,避免每次循环都读一次 + * /PQDIF/ 目录。目录读和文件下载都走 MMS,同一个终端一分钟扫描一次即可。 + */ + scan_idx = ied_usr->dev_idx; + if (scan_idx < 0 || scan_idx >= PQDIF_MAX_DEVICE_INDEX) + scan_idx = (int)chnl_usr->chnl_id; + if (scan_idx < 0 || scan_idx >= PQDIF_MAX_DEVICE_INDEX) + scan_idx = 0; + + now_ms = sGetMsTime(); + if (s_last_scan_ms[scan_idx] > 0 && + (now_ms - s_last_scan_ms[scan_idx]) < PQDIF_SCAN_INTERVAL_MS) { + return APR_SUCCESS; + } + s_last_scan_ms[scan_idx] = now_ms; + + /* 本函数只做 PQDIF 文件获取: + * 1. 从装置固定目录 /PQDIF/ 取文件列表; + * 2. 用台账里的 LD_info->cpuno 匹配文件名里的 “测点号_yyyyMMdd_HH00_”; + * 3. 只下载 .pqd 文件,且本地 /FeProject/data/pqdif/ 已存在同名文件时跳过; + * 4. 真正下载直接调用 mms_getFile,不改动原录波文件下载函数。 + */ + if (pqdif_ensure_local_dir() != SD_SUCCESS) { + echo_warn1("[PQDIF] create local dir failed: %s\n", PQDIF_LOCAL_DIR); + return APR_EGENERAL; + } + + ret = mms_mvla_fdir(chnl_usr->net_info,(ST_CHAR*)PQDIF_REMOTE_DIR, + 3*g_pt61850app->mmsOpTimeout,&filenames,&filenum,g_pt61850app->tmp_pool); + if (ret != SD_SUCCESS) { + echo_warn2("[PQDIF] mms_mvla_fdir failed, IED=%d IP=%s\n", ied->id, chnl_usr->ip_str); + return APR_EGENERAL; + } + + if (filenames == NULL || filenum <= 0) { + printf("[PQDIF] no file in remote dir %s, IED=%d IP=%s\n", PQDIF_REMOTE_DIR, ied->id, chnl_usr->ip_str); + return APR_SUCCESS; + } + + printf("[PQDIF] scan remote dir %s success, IED=%d IP=%s file_count=%d\n", + PQDIF_REMOTE_DIR, ied->id, chnl_usr->ip_str, filenum); + + for (cpuno = 0; cpuno < ied->cpucount; cpuno++) { + LD_info = &(ied_usr->LD_info[cpuno]); + + if (LD_info->cpuno == 0 || + LD_info->LD_name == NULL || + LD_info->mp_id[0] == '\0') { + continue; + } + + for (file_idx = 0; file_idx < filenum; file_idx++) { + const char *base_name; + char remote_filename[256]; + char only_filename_ret[256]; + int ret_get; + + if (filenames[file_idx] == NULL) + continue; + + base_name = pqdif_basename(filenames[file_idx]); + if (base_name[0] == '\0') + continue; + + /* C# 读取程序的兼容逻辑是读目录后按片段匹配: + * cpuNo%10_yyyyMMdd_HH00_ + * 这里保持同样的匹配方式,不依赖厂家前缀,也兼容普通设备用 + * 周期开始时间命名、振兴类设备用周期结束时间命名的差异。 + */ + if (!pqdif_match_recent_interval(base_name, LD_info->cpuno)) + continue; + + matched++; + if (pqdif_local_file_exists(base_name)) { + printf("[PQDIF] local file exists, skip mp_id=%s file=%s\n", LD_info->mp_id, base_name); + continue; + } + + memset(remote_filename,0,sizeof(remote_filename)); + memset(only_filename_ret,0,sizeof(only_filename_ret)); + pqdif_build_remote_filename(filenames[file_idx], remote_filename, sizeof(remote_filename)); + if (remote_filename[0] == '\0') + continue; + + ret_get = pqdif_get_file_from_device(chnl_usr, remote_filename, only_filename_ret, sizeof(only_filename_ret)); + if (ret_get == SD_SUCCESS) { + fetched++; + chnl_usr->m_LastPosRespTime = sGetMsTime(); + printf("[PQDIF] fetch success mp_id=%s name=%s remote=%s local=%s/%s\n", + LD_info->mp_id, LD_info->name, remote_filename, PQDIF_LOCAL_DIR, only_filename_ret); + } + else { + echo_warn3("[PQDIF] fetch failed mp_id=%s remote=%s IP=%s\n", + LD_info->mp_id, remote_filename, chnl_usr->ip_str); + } + + if (fetched >= PQDIF_MAX_FETCH_PER_SCAN) { + printf("[PQDIF] reach fetch limit per scan=%d, IED=%d IP=%s\n", + PQDIF_MAX_FETCH_PER_SCAN, ied->id, chnl_usr->ip_str); + return APR_SUCCESS; + } + } + } + + printf("[PQDIF] scan finish IED=%d IP=%s matched=%d fetched=%d\n", + ied->id, chnl_usr->ip_str, matched, fetched); + return APR_SUCCESS; +} apr_status_t prepare_call_cn_wavelist(LD_info_t *LD_info, int FltNum) {