Files
STM32F4-Base/IEC60870-Slave/104/IEC104_Log.c
T
STF 4c55544f05
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
通信协议代码
2026-10-26 08:31:48 -04:00

222 lines
7.2 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.
/**
* IEC104_Log.c —— IEC104 通用日志模块实现。
*
* 日志处理流程:
* 级别过滤 -> 时间戳/级别前缀 -> 正文格式化 -> 回调或标准流输出
*
* 模块不依赖 IEC104 报文结构,不使用动态内存。输出缓冲区位于调用栈,
* 用户回调不得在返回后继续引用收到的字符串地址。
*/
/* localtime_r 和 clock_gettime 是 POSIX 接口。特性宏必须先于所有头文件。 */
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
# define _POSIX_C_SOURCE 200809L
#endif
#include "IEC104_Log.h"
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#ifdef _WIN32
/* 减少 <windows.h> 引入的非必要声明,并避免旧版 Winsock 头文件干扰。 */
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# include <windows.h>
#else
/* 非 Windows 平台使用 POSIX 实时时钟和线程安全的本地时间转换接口。 */
# include <time.h>
#endif
/* 包含结尾 '\0';时间戳、级别与正文最多合计511字节,不等同于511个多字节字符。 */
#define IEC104_LOG_BUFFER_SIZE 512U
/*
* 日志配置状态。
*
* 默认级别为 INFO,默认回调为空。Set_* 应在开始并发输出之前调用;
* 本模块不为配置读写增加锁。
*/
static IEC104_LogCb_t g_log_callback = NULL;
static int g_min_log_level = IEC104_LOG_LEVEL_INFO;
/**
* 功能:把格式化函数报告的长度限制在缓冲区实际可存储范围内。
* 输入参数:length——snprintf/vsnprintf 的返回值;capacity——缓冲区容量。
* 输出参数:无。
* 返回值:截断后占用的字节数,不包含结尾 '\0';格式化失败或容量为0时返回0。
*/
static size_t Log_Clamp_Length(int length, size_t capacity)
{
if (length <= 0 || capacity == 0U) {
return 0U;
}
if ((size_t)length >= capacity) {
return capacity - 1U;
}
return (size_t)length;
}
/**
* 功能:将日志级别转换为时间戳后显示的级别名称。
* 输入参数:level——日志级别。
* 输出参数:无。
* 返回值:指向静态只读字符串;未知级别返回 "???"。
*/
static const char *Log_Level_Name(int level)
{
switch (level) {
case IEC104_LOG_LEVEL_DEBUG:
return "DBG ";
case IEC104_LOG_LEVEL_INFO:
return "INFO";
case IEC104_LOG_LEVEL_WARN:
return "WARN";
case IEC104_LOG_LEVEL_ERR:
return "ERR ";
default:
return "???";
}
}
/**
* 功能:格式化本地时间戳和日志级别前缀。
* 输入参数:buffer——目标缓冲区;capacity——缓冲区容量;level——日志级别。
* 输出参数:buffer——形如 "[yyyy-mm-dd hh:mm:ss.mmm][INFO] " 的前缀。
* 返回值:缓冲区内实际占用的字节数,不包含结尾 '\0'。
* 说明:POSIX 时间接口失败时输出全零时间戳,避免使用未初始化数据。
*/
static size_t Log_Format_Prefix(char *buffer, size_t capacity, int level)
{
int length;
#ifdef _WIN32
SYSTEMTIME local_time;
/* GetLocalTime 直接返回含毫秒字段的本地时间。 */
GetLocalTime(&local_time);
length = snprintf(
buffer, capacity,
"[%04d-%02d-%02d %02d:%02d:%02d.%03d][%s] ",
local_time.wYear, local_time.wMonth, local_time.wDay,
local_time.wHour, local_time.wMinute, local_time.wSecond,
local_time.wMilliseconds, Log_Level_Name(level));
#else
struct timespec timestamp;
struct tm local_time;
/* clock_gettime 提供毫秒来源,localtime_r 负责线程安全的时区转换。 */
if (clock_gettime(CLOCK_REALTIME, &timestamp) != 0 ||
localtime_r(&timestamp.tv_sec, &local_time) == NULL) {
length = snprintf(buffer, capacity,
"[0000-00-00 00:00:00.000][%s] ",
Log_Level_Name(level));
} else {
length = snprintf(
buffer, capacity,
"[%04d-%02d-%02d %02d:%02d:%02d.%03d][%s] ",
local_time.tm_year + 1900, local_time.tm_mon + 1,
local_time.tm_mday, local_time.tm_hour, local_time.tm_min,
local_time.tm_sec, (int)(timestamp.tv_nsec / 1000000L),
Log_Level_Name(level));
}
#endif
return Log_Clamp_Length(length, capacity);
}
/**
* 功能:使用默认通道输出一条日志。
* 输入参数:level——日志级别;message——不含行末换行的完整日志字符串。
* 输出参数:无。
* 返回值:无。
* 说明:WARN/ERR 写 stderr,DEBUG/INFO 写 stdout;每条日志立即刷新。
*/
static void Log_Write_Default(int level, const char *message)
{
FILE *output =
(level >= IEC104_LOG_LEVEL_WARN) ? stderr : stdout;
fputs(message, output);
fputc('\n', output);
fflush(output);
}
/**
* 功能:设置日志输出回调。
* 输入参数:cb——新的日志回调;传入空指针时恢复默认标准流输出。
* 输出参数:无。
* 返回值:无。
* 说明:该配置没有内部同步,应在开始并发输出日志之前完成。
*/
void IEC104_Set_Log_Callback(IEC104_LogCb_t cb)
{
/* 空回调表示恢复 Log_Write_Default()。 */
g_log_callback = cb;
}
/**
* 功能:设置允许输出的最低日志级别。
* 输入参数:min_level——IEC104_LOG_LEVEL_* 之一。
* 输出参数:无。
* 返回值:无。
* 说明:低于该级别的消息会在格式化之前被丢弃。
*/
void IEC104_Set_Log_Level(int min_level)
{
/* 保留 int 接口,调用方通常传入 IEC104_LOG_LEVEL_*。 */
g_min_log_level = min_level;
}
/**
* 功能:格式化并输出一条带时间戳和级别前缀的日志。
* 输入参数:level——日志级别;fmt——非空且不含行末换行的 printf 格式串;
* 其后为格式化参数。
* 输出参数:无;优先交给用户回调,否则写入默认标准流。
* 返回值:无。
* 说明:低级别消息直接忽略;超出固定缓冲区的内容按字节截断,可能截断多字节字符。
*/
void IEC104_Log_Emit(int level, const char *fmt, ...)
{
char buffer[IEC104_LOG_BUFFER_SIZE];
size_t prefix_length;
size_t remaining_capacity;
size_t total_length;
int body_length;
va_list args;
/* 被过滤的消息不执行时间和正文格式化。 */
if (fmt == NULL || level < g_min_log_level) {
return;
}
/* 先生成前缀,再把正文写入同一个固定长度缓冲区。 */
prefix_length = Log_Format_Prefix(buffer, sizeof(buffer), level);
remaining_capacity = sizeof(buffer) - prefix_length;
va_start(args, fmt);
body_length = vsnprintf(buffer + prefix_length, remaining_capacity,
fmt, args);
va_end(args);
/* 格式化失败时仍可输出已经生成的前缀。 */
if (body_length < 0) {
total_length = prefix_length;
} else {
total_length = prefix_length +
Log_Clamp_Length(body_length, remaining_capacity);
}
buffer[total_length] = '\0';
/* 回调优先;未配置回调时使用模块默认输出通道。 */
if (g_log_callback != NULL) {
g_log_callback(level, buffer);
} else {
Log_Write_Default(level, buffer);
}
}