245 lines
15 KiB
C
245 lines
15 KiB
C
/*
|
||
* 模块名称:IEC 60870-5-101/104 公共时间工具
|
||
* 模块功能:CP16/CP24/CP56编解码、公历日期/星期、本地标准时间及纪元秒数工具。
|
||
* 适用协议:101 和 104 的时间格式共用同一实现,独立于各自的帧格式和链路层。
|
||
* 职责边界:公共编解码保留 IV/SU,允许合法的 SU=0/1,不调整输入日历。
|
||
* 业务接收时通过 CP56_To_Local 检查 IV、处理 SU 和源时区,取得本地标准时间日历。
|
||
* 后续业务发送入口须保证日历已是本地标准时间,并统一设置 SU=0。
|
||
* 接入状态:IEC60870_Asdu 已复用 CP56 编解码;事件采集、时间源读取和实际校时仍由业务接入。
|
||
* 适用平台:可选平台库或纯整数日期转换;不读取系统时钟、不设置 RTC、不推断时区规则或处理链路定时器。
|
||
*/
|
||
#ifndef IEC60870_TIME_H
|
||
#define IEC60870_TIME_H
|
||
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/* CP16为经过时间;CP24仅含分钟及分钟内毫秒,不能单独恢复完整日期。 */
|
||
#define IEC60870_CP16_LEN 2U
|
||
#define IEC60870_CP24_LEN 3U
|
||
#define IEC60870_CP56_LEN 7U
|
||
#define IEC60870_TIME_MIN_YEAR 1970U
|
||
#define IEC60870_TIME_MAX_YEAR 2099U
|
||
/* 两位年份按 2000 年起解释是本实现的世纪约定,须与通信对端一致。 */
|
||
#define IEC60870_CP56_MIN_YEAR 2000U
|
||
#define IEC60870_CP56_MAX_YEAR 2099U
|
||
/* 2099-12-31 23:59:59 UTC;本模块不接受 uint32_t 可表示但超出约定年份的秒数。 */
|
||
#define IEC60870_TIME_MAX_SECONDS UINT32_C(4102444799)
|
||
/*
|
||
* 本地标准时区固定为 UTC+8,不读取系统时区。
|
||
* 源标准时区也为 UTC+8 时,转换接口传入此值;即使报文 SU=1,源标准偏移仍为 480。
|
||
* 其他源时区由调用方按双方约定提供标准偏移,不能一律传入此值。
|
||
*/
|
||
#define IEC60870_LOCAL_UTC_OFFSET_MINUTES 480
|
||
|
||
/*
|
||
* 时间戳转日历的编译选项:1 使用平台库,0 使用本模块保留的纯整数算法。
|
||
* 当前平台库后端为 Windows CRT 的 _gmtime64_s;Windows 默认 1,其他平台默认 0。
|
||
* 无合适时间库时,在编译 IEC60870_Time.c 时定义 IEC60870_TIME_USE_PLATFORM=0。
|
||
* 两种实现均输出 UTC,支持相同的 1970..2099 年范围,不读取当前时间或本地时区。
|
||
*/
|
||
#ifndef IEC60870_TIME_USE_PLATFORM
|
||
#ifdef _WIN32
|
||
#define IEC60870_TIME_USE_PLATFORM 1
|
||
#else
|
||
#define IEC60870_TIME_USE_PLATFORM 0
|
||
#endif
|
||
#endif
|
||
|
||
typedef struct {
|
||
uint16_t milliseconds; /* CP16经过时间,0..65535毫秒,无IV或SU。 */
|
||
} IEC60870_CP16Time2a_t;
|
||
|
||
typedef struct {
|
||
uint16_t milliseconds; /* CP24分钟内毫秒,0..59999,包含秒。 */
|
||
uint8_t minute; /* 0..59;不含小时、日期或时区。 */
|
||
uint8_t is_invalid; /* IV,0/1;编码和解码保留。 */
|
||
} IEC60870_CP24Time2a_t;
|
||
|
||
/**
|
||
* 功能:编码CP16经过时间或CP24短时标。
|
||
* 输入参数:value——非空解析值;capacity——输出容量,分别至少2/3字节。
|
||
* 输出参数:out——低字节在前的协议字节,不改变输入时间基准。
|
||
* 返回值:1成功;0参数或字段非法,输出不变。
|
||
* 说明:CP24保留IV但检查分钟和毫秒范围,不推断小时或日期,不携带SU。
|
||
*/
|
||
int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity);
|
||
int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity);
|
||
|
||
/**
|
||
* 功能:解码CP16经过时间或CP24短时标。
|
||
* 输入参数:in——非空字节串;length——可读长度,分别至少2/3字节。
|
||
* 输出参数:value——成功后保存独立解析值,失败不变。
|
||
* 返回值:1成功;0参数、长度、CP24保留位或字段非法。
|
||
* 说明:只读取前2/3字节;CP24的IV=1仍可解析,但不表示时间有效。
|
||
*/
|
||
int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value);
|
||
int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value);
|
||
|
||
/*
|
||
* 公历时间值,不携带时区或 SU;Seconds_To_Local、CP56_To_Local 输出本地标准时间,
|
||
* From_Seconds 输出 UTC。
|
||
* 调用 To_Seconds 时须提供 UTC 日历,不能把本地标准时间日历直接当成 UTC 转秒数。
|
||
* weekday 为派生字段:0=周日,1=周一,...,6=周六。
|
||
* 编码及转秒数时忽略传入的 weekday;解码及从秒数转换时按日期重新计算。
|
||
* 此类型仅供内存中传值,不能直接作为 CP56Time2a 的报文字节布局。
|
||
*/
|
||
typedef struct {
|
||
uint16_t year; /* 普通日历为 1970..2099;CP56 编解码限于 2000..2099 */
|
||
uint8_t month; /* 1..12 */
|
||
uint8_t day; /* 1..当月实际天数 */
|
||
uint8_t hour; /* 0..23 */
|
||
uint8_t minute; /* 0..59 */
|
||
uint8_t second; /* 0..59,不表示闰秒 */
|
||
uint16_t ms; /* 0..999 */
|
||
uint8_t weekday; /* 0..6,输出时由日期推导 */
|
||
} IEC60870_WallTime_t;
|
||
|
||
/*
|
||
* CP56Time2a 的解析值;标志与普通日历值分开,避免丢失报文中的时间质量信息。
|
||
* is_invalid、is_summer_time 和 has_weekday 只能为 0 或 1。
|
||
* 编码由 has_weekday 决定是否携带星期,IV 和 SU 按输入保留。
|
||
* IV=1 仍可编解码,但日期字段仍须合法;成功不表示该时间可用于校时。
|
||
* 标准允许选用 SU 但不推荐使用;本项目后续业务发送入口负责统一设置 SU=0。
|
||
* 公共编解码支持 SU=0/1,不强制清零;发送前须确保日历与标志一致,不能只清 SU 而保留夏令时日历。
|
||
* SU 不携带时区信息;取本地标准时间时将完整解析值及源标准时区偏移交给 CP56_To_Local。
|
||
*/
|
||
typedef struct {
|
||
IEC60870_WallTime_t time; /* 2000..2099;按双方约定的时间基准及 SU 解释 */
|
||
uint8_t is_invalid; /* IV:1=时间无效,0=未置无效标志 */
|
||
uint8_t is_summer_time; /* SU:0=标准时间,1=夏令时;原始编解码保留,CP56 转换接口在为 1 时扣除一小时 */
|
||
uint8_t has_weekday; /* 1=报文带星期,0=报文星期域为 0(未使用) */
|
||
} IEC60870_CP56Time2a_t;
|
||
|
||
/*
|
||
* 所有接口:成功返回 1;参数/格式非法、结果越界、平台转换失败或将 IV=1 转为有效时间时返回 0。
|
||
* 返回 0 时不修改任何输出;调用方仍须保证指针有效且实际空间不少于所报长度。
|
||
* 模块不维护共享可变状态或动态内存。输入和输出允许重叠。
|
||
* 成功写回输出时,会覆盖与输出重叠的输入存储;需要保留原始输入时应使用独立输出存储。
|
||
*/
|
||
|
||
/**
|
||
* 功能:检查日历的年月日、时分秒及毫秒是否合法,不进行时间转换。
|
||
* 输入:time——待检查的日历值;年份1970..2099,毫秒0..999。
|
||
* 返回:1 数值合法;0 空指针、无效日期或字段越界。
|
||
* 说明:忽略派生字段weekday;不判断时区、时钟是否准确或是否允许校时。
|
||
*/
|
||
int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time);
|
||
|
||
/**
|
||
* 功能:检查公历日期并计算星期。
|
||
* 输入:year——1970..2099;month、day——合法月份和日期。
|
||
* 输出:weekday——0=周日,1=周一,...,6=周六。
|
||
* 返回:1 成功;0 日期非法或输出为空,输出不变。
|
||
*/
|
||
int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday);
|
||
|
||
/**
|
||
* 功能:将 UTC 日期和时间转换为自 1970-01-01 00:00:00 UTC 起的完整秒数。
|
||
* 输入:time——年份 1970..2099,毫秒 0..999,其余字段须组成合法时间。
|
||
* 输出:seconds——0..IEC60870_TIME_MAX_SECONDS;0 是有效纪元时间,不表示失败。
|
||
* 返回:1 成功;0 参数非法,输出不变。
|
||
* 说明:ms 参与范围校验,但不计入完整秒数;需保留亚秒精度时另行保存 time->ms。
|
||
* 忽略 weekday,不进行本地时区换算,也不计闰秒。
|
||
* time 没有时区或 SU,函数无法检查其是否真的采用 UTC,须由调用方保证。
|
||
* 对 CP56 解析值,取本地标准时间调用 CP56_To_Local;取纪元秒数调用 CP56_To_Seconds。
|
||
*/
|
||
int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds);
|
||
|
||
/**
|
||
* 功能:将纪元秒数和毫秒余数转换为 UTC 日期和时间。
|
||
* 输入:seconds——0..IEC60870_TIME_MAX_SECONDS;ms——0..999。
|
||
* 输出:time——完整公历时间及按日期计算的 weekday。
|
||
* 返回:1 成功;0 参数非法或平台转换失败,输出不变;不会把越界毫秒截断成 999。
|
||
* 说明:由 IEC60870_TIME_USE_PLATFORM 选择平台库或纯整数算法;均不做本地时区换算。
|
||
*/
|
||
int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time);
|
||
|
||
/**
|
||
* 功能:将 UTC 纪元秒数和毫秒余数直接转换为本地标准时间日历(固定 UTC+8)。
|
||
* 输入:utc_seconds——未加时区偏移的 UTC 纪元秒数;ms——0..999。
|
||
* 本地日期须在 1970..2099 年内,因此 utc_seconds 上限为
|
||
* IEC60870_TIME_MAX_SECONDS - IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60。
|
||
* 输出:local_time——本地年月日、时分秒、毫秒及重新计算的星期。
|
||
* 返回:1 成功;0 参数非法、本地日期越界或平台转换失败,输出不变。
|
||
* 说明:内部统一处理时区偏移及跨日/月/年,调用方不再加八小时。
|
||
* 不读取系统时区或开启夏令时;已是固定 UTC+8 标准时间的 RTC 日历可直接用于 CP56 编码。
|
||
*/
|
||
int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms,
|
||
IEC60870_WallTime_t *local_time);
|
||
|
||
/**
|
||
* 功能:将 CP56Time2a 解析值转换为 UTC 纪元完整秒数,SU=1 时扣除一小时。
|
||
* 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1;
|
||
* standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数,范围 -1439..1439,
|
||
* 向东为正、向西为负;UTC 填 0,UTC+8 填 480,UTC-5 填 -300。
|
||
* 此偏移必须不含夏令时的一小时;SU 会单独扣除,不能重复计算。
|
||
* 输出:seconds——0..IEC60870_TIME_MAX_SECONDS 的 UTC 秒数。
|
||
* 返回:1 成功;0 参数非法、IV=1 或换算结果越界,输出不变。
|
||
* 说明:UTC = 报文日历对应秒数 - SU * 3600 - 标准时区偏移 * 60。
|
||
* 在秒数上运算,支持跨日/月/年;年份 2000 的输入可换算为 1999 年的 UTC。
|
||
* ms 校验后不计入完整秒数;未与输出重叠时,输入日期、SU 和毫秒均保持不变。
|
||
* 忽略传入的 weekday;不根据日期猜测夏令时,不访问系统时区或设置 RTC。
|
||
*/
|
||
int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value,
|
||
int16_t standard_utc_offset_minutes, uint32_t *seconds);
|
||
|
||
/**
|
||
* 功能:将 CP56Time2a 解析值转换为本地标准时间日历(固定 UTC+8),并处理 SU。
|
||
* 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1;
|
||
* source_standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数,
|
||
* 范围 -1439..1439,不含夏令时;双方约定本地标准时间时传
|
||
* IEC60870_LOCAL_UTC_OFFSET_MINUTES(480),源报文为 UTC 时传 0。
|
||
* 输出:local_time——1970..2099 年的本地标准时间年月日、时分秒、毫秒及重新计算的星期。
|
||
* 返回:1 成功;0 参数非法、IV=1、本地标准时间越界或平台转换失败,输出不变。
|
||
* 说明:本地标准时间 = 报文日历 - SU * 一小时 - 源标准时区偏移 + 八小时。
|
||
* 源标准时区为 UTC+8 时,SU=0 保持原时间,SU=1 减一小时,自动处理跨日/月/年。
|
||
* 毫秒原样保留;日历结果通过 local_time 输出,函数返回值仅表示成功或失败。
|
||
* 输入与输出未重叠时,不改写输入日历或 SU。
|
||
* 结果已是本地标准时间,调用方无需再次加八小时或减夏令时的一小时。
|
||
* 业务再次发送此日历时应构造新的 CP56 值并置 SU=0,不能沿用原报文的 SU=1。
|
||
*/
|
||
int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value,
|
||
int16_t source_standard_utc_offset_minutes,
|
||
IEC60870_WallTime_t *local_time);
|
||
|
||
/**
|
||
* 功能:将 CP56Time2a 解析值编码为 7 字节报文,毫秒域按低字节在前写入。
|
||
* 输入:value——合法的 2000..2099 年日期时间,三个标志字段均为 0/1;
|
||
* capacity——out 可写字节数,至少 IEC60870_CP56_LEN。
|
||
* 输出:out——仅写前 7 字节,保留位清零,IV 和 SU 按输入保留。
|
||
* 返回:1 成功;0 参数非法,整个输出缓冲区不变。
|
||
* 说明:has_weekday=1 时由日期计算报文星期(1=周一..7=周日),否则填 0。
|
||
* 不校时、不转换时区,也不因 SU=1 而调整小时值。
|
||
* SU=0/1 均可编码,本接口不会自动清零或因 SU=1 而拒绝合法输入。
|
||
* 后续业务发送入口负责传入已是本地标准时间的日历并设置 SU=0;此处只执行公共编码。
|
||
* IV=1 不放宽日期、时间及标志取值的检查。
|
||
*/
|
||
int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity);
|
||
|
||
/**
|
||
* 功能:检查并解析前 7 字节 CP56Time2a,后续字节留给调用方处理。
|
||
* 输入:in——报文起点;length——可读字节数,至少 IEC60870_CP56_LEN。
|
||
* 输出:value——源报文的日历时间、IV、SU 及报文是否携带星期,尚未转换为本地标准时间;
|
||
* weekday 始终由日期计算。
|
||
* 返回:1 成功;0 参数、保留位、日期时间非法,输出不变。
|
||
* 说明:年份域只接受 0..99,解释为 2000..2099;星期域 0 表示未使用,
|
||
* 非零星期必须与日期一致。分钟 bit6 等保留位必须为 0,不接收扩展含义。
|
||
* SU=1 本身不导致失败,IV/SU 均原样返回;不转换时区或夏令时。
|
||
* IV=1 且其余字段合法时仍可成功解析,供业务记录;两个 CP56 转换接口会拒绝该无效时间。
|
||
* 解码后取本地标准时间调用 CP56_To_Local;需要纪元秒数时调用 CP56_To_Seconds。
|
||
* 转换前须保留完整的日历及 SU,不可先清 SU,也不可先减一小时再携带 SU=1 转换。
|
||
* 业务仍须检查时间约定及校时权限;不自动设置 RTC。
|
||
*/
|
||
int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value);
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
|
||
#endif /* IEC60870_TIME_H */
|