503 lines
24 KiB
C
503 lines
24 KiB
C
/*
|
||
* IEC 60870-5-101/104 公共时间工具:日历计算、CP56Time2a 编解码及本地标准时间转换。
|
||
* 供 101 和 104 的业务层按需调用,编解码不读取协议类型或链路状态。
|
||
* CP56 编解码保留 IV 和 SU;转本地标准时间时检查 IV,处理 SU 和源时区,返回完整日历。
|
||
* 公共编码允许 SU=0/1;后续业务发送入口负责提供本地标准时间日历并设置 SU=0。
|
||
* 本文件只提供公共工具,不自动接入 ASDU 业务处理或执行校时。
|
||
* 整数公历换算按 400 年周期展开,将三月作为年内起点以统一处理二月和闰日。
|
||
* 仅在校验后的 1970..2099 年范围内运算,天数及秒数均可用 uint32_t 表示。
|
||
* 时间戳转日历可选平台库或纯整数算法;接入层提供约定的标准时区偏移,负责系统时钟、RTC 和单调计时。
|
||
*/
|
||
#include "IEC60870_Time.h"
|
||
|
||
#include <string.h>
|
||
|
||
/**
|
||
* 功能:编码两字节经过时间。
|
||
* 输入参数:value——毫秒值;capacity——out的实际容量。
|
||
* 输出参数:out——成功写入2字节,失败不变。
|
||
* 返回值:1成功;0空指针或容量不足。
|
||
* 说明:先保存输入,允许输入输出重叠;不将经过时间解释为日历时间。
|
||
*/
|
||
int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity)
|
||
{
|
||
uint16_t milliseconds;
|
||
if (value == NULL || out == NULL || capacity < IEC60870_CP16_LEN) { return 0; }
|
||
milliseconds = value->milliseconds;
|
||
out[0] = (uint8_t)milliseconds;
|
||
out[1] = (uint8_t)(milliseconds >> 8);
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* 功能:解码两字节经过时间。
|
||
* 输入参数:in——低字节在前的输入;length——至少2字节。
|
||
* 输出参数:value——经过时间,失败不变。
|
||
* 返回值:1成功;0空指针或长度不足。
|
||
* 说明:CP16使用完整16位数值范围,不含质量标志。
|
||
*/
|
||
int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value)
|
||
{
|
||
IEC60870_CP16Time2a_t result;
|
||
if (in == NULL || value == NULL || length < IEC60870_CP16_LEN) { return 0; }
|
||
result.milliseconds = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8));
|
||
*value = result;
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* 功能:编码三字节短时标。
|
||
* 输入参数:value——分钟、分钟内毫秒和IV;capacity——out容量。
|
||
* 输出参数:out——成功写入3字节,失败不变。
|
||
* 返回值:1成功;0参数或字段非法。
|
||
* 说明:分钟0..59,毫秒0..59999,IV为0/1;第三字节bit6始终为0。
|
||
*/
|
||
int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity)
|
||
{
|
||
uint8_t encoded[IEC60870_CP24_LEN];
|
||
if (value == NULL || out == NULL || capacity < sizeof(encoded) ||
|
||
value->milliseconds > 59999U || value->minute > 59U || value->is_invalid > 1U) { return 0; }
|
||
encoded[0] = (uint8_t)value->milliseconds;
|
||
encoded[1] = (uint8_t)(value->milliseconds >> 8);
|
||
encoded[2] = (uint8_t)(value->minute | (value->is_invalid << 7));
|
||
memcpy(out, encoded, sizeof(encoded));
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* 功能:校验并解码三字节短时标。
|
||
* 输入参数:in——至少3字节;length——实际可读长度。
|
||
* 输出参数:value——成功保存解析值;失败保持原值。
|
||
* 返回值:1成功;0长度、保留位、分钟或毫秒非法。
|
||
* 说明:IV保留;调用方结合已有时间基准恢复小时及日期。
|
||
*/
|
||
int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value)
|
||
{
|
||
IEC60870_CP24Time2a_t result = {0};
|
||
if (in == NULL || value == NULL || length < IEC60870_CP24_LEN || (in[2] & 0x40U)) { return 0; }
|
||
result.milliseconds = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8));
|
||
result.minute = in[2] & 0x3FU;
|
||
result.is_invalid = in[2] >> 7;
|
||
if (result.milliseconds > 59999U || result.minute > 59U) { return 0; }
|
||
*value = result;
|
||
return 1;
|
||
}
|
||
|
||
#if IEC60870_TIME_USE_PLATFORM != 0 && IEC60870_TIME_USE_PLATFORM != 1
|
||
#error "IEC60870_TIME_USE_PLATFORM must be 0 or 1"
|
||
#endif
|
||
|
||
#if IEC60870_TIME_USE_PLATFORM
|
||
#ifndef _WIN32
|
||
#error "Platform time conversion currently requires Windows; use IEC60870_TIME_USE_PLATFORM=0"
|
||
#endif
|
||
#include <time.h>
|
||
#endif
|
||
|
||
/*
|
||
* 函数功能:判断公历闰年。
|
||
* 入口参数:year——公历年份;本函数只执行闰年规则,不检查模块支持的年份范围。
|
||
* 返回值:1 为闰年,0 为平年。
|
||
* 说明:能被 4 整除且不能被 100 整除,或能被 400 整除的年份为闰年。
|
||
*/
|
||
static int Is_Leap(uint32_t year)
|
||
{
|
||
return ((year % 4U) == 0U && (year % 100U) != 0U) || (year % 400U) == 0U;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:取得指定公历月份的实际天数。
|
||
* 入口参数:year——公历年份;month——月份,合法范围为 1..12。
|
||
* 返回值:合法月份返回 28..31,非法月份返回 0。
|
||
* 说明:先检查月份再访问表,闰年二月单独返回 29;年份范围由上层日期检查负责。
|
||
*/
|
||
static uint32_t Days_In_Month(uint32_t year, uint32_t month)
|
||
{
|
||
static const uint8_t days[12] = {31U, 28U, 31U, 30U, 31U, 30U,
|
||
31U, 31U, 30U, 31U, 30U, 31U};
|
||
|
||
if (month < 1U || month > 12U) {
|
||
return 0U;
|
||
}
|
||
if (month == 2U && Is_Leap(year)) {
|
||
return 29U;
|
||
}
|
||
return days[month - 1U];
|
||
}
|
||
|
||
/*
|
||
* 函数功能:统一检查年份范围和日期是否真实存在。
|
||
* 入口参数:year——1970..2099;month——1..12;day——待检查的日号。
|
||
* 返回值:1 为合法日期,0 为年份、月份或日号非法。
|
||
* 说明:按当月实际天数检查 day,不接受平年二月二十九;非法月份的天数为 0,也会被拒绝。
|
||
*/
|
||
static int Date_Is_Valid(uint32_t year, uint32_t month, uint32_t day)
|
||
{
|
||
uint32_t limit;
|
||
|
||
if (year < IEC60870_TIME_MIN_YEAR || year > IEC60870_TIME_MAX_YEAR) {
|
||
return 0;
|
||
}
|
||
limit = Days_In_Month(year, month);
|
||
return day >= 1U && day <= limit;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:检查完整日历值的日期、时分秒和毫秒范围。
|
||
* 入口参数:time——待检查的日历指针,可为空。
|
||
* 返回值:1 为数值合法,0 为空指针、无效日期或时间字段越界。
|
||
* 说明:小时 0..23,分/秒 0..59,毫秒 0..999;weekday 为派生字段,不参与检查。
|
||
* 这里只检查数值,不能据此判断时间基准、时钟是否准确或该值是否允许用于校时。
|
||
*/
|
||
static int WallTime_Is_Valid(const IEC60870_WallTime_t *time)
|
||
{
|
||
return time != NULL && Date_Is_Valid(time->year, time->month, time->day) &&
|
||
time->hour <= 23U && time->minute <= 59U && time->second <= 59U && time->ms <= 999U;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将公历日期转换为自 1970-01-01 起的完整天数。
|
||
* 入口参数:year、month、day——已校验的合法日期,年份须在 1970..2099 内。
|
||
* 返回值:完整天数,0 对应 1970-01-01;本内部函数不重复校验参数。
|
||
* 说明:将三月作为年首,一、二月归入上一年,使闰日统一落在年末。
|
||
* era 为 400 年周期编号,yoe 为周期内年份,mp 为从三月起算的月份编号 0..11。
|
||
* doy 为从三月起算的年内天数,doe 为周期内天数;每 400 年有 146097 天。
|
||
* 最后减去 719468,将算法的天数原点平移到 1970-01-01。
|
||
*/
|
||
static uint32_t Days_From_Civil(uint32_t year, uint32_t month, uint32_t day)
|
||
{
|
||
uint32_t y = (month <= 2U) ? (year - 1U) : year;
|
||
uint32_t era = y / 400U;
|
||
uint32_t yoe = y - era * 400U;
|
||
uint32_t mp = (month > 2U) ? (month - 3U) : (month + 9U);
|
||
uint32_t doy = (153U * mp + 2U) / 5U + day - 1U;
|
||
uint32_t doe = yoe * 365U + yoe / 4U - yoe / 100U + doy;
|
||
|
||
return era * 146097U + doe - 719468U;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将已校验的日历字段展开为相对 1970-01-01 00:00:00 的线性秒数。
|
||
* 入口参数:time——非空且已通过 WallTime_Is_Valid 检查的日历值。
|
||
* 返回值:0..IEC60870_TIME_MAX_SECONDS 的完整秒数,不包含毫秒。
|
||
* 说明:此处只计算天数和日内秒数,忽略 ms、weekday,不处理时区或 SU。
|
||
* 只有输入日历本身采用 UTC 时,结果才可直接作为 UTC 纪元秒数使用。
|
||
*/
|
||
static uint32_t Seconds_From_Civil(const IEC60870_WallTime_t *time)
|
||
{
|
||
return Days_From_Civil(time->year, time->month, time->day) * 86400U +
|
||
(uint32_t)time->hour * 3600U + (uint32_t)time->minute * 60U + (uint32_t)time->second;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:根据自 1970-01-01 起的完整天数计算星期。
|
||
* 入口参数:days——模块支持年份内的天数,0 对应 1970-01-01。
|
||
* 返回值:0=周日,1=周一,...,6=周六。
|
||
* 说明:纪元日是周四,因此先加 4 再对 7 取余;本内部函数不检查日期范围。
|
||
*/
|
||
static uint8_t Weekday_From_Days(uint32_t days)
|
||
{
|
||
return (uint8_t)((days + 4U) % 7U);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:供时间源适配等调用方复用日历合法性检查,不把本地日历当作UTC转秒数。
|
||
* 入口参数:time——待检查的日历指针,可以为空;weekday不参与检查。
|
||
* 返回值:1 年月日、时分秒和毫秒在模块范围内;0 参数非法。
|
||
*/
|
||
int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time)
|
||
{
|
||
return WallTime_Is_Valid(time);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:校验公历日期并计算星期。
|
||
* 入口参数:year——1970..2099;month——1..12;day——当月日号。
|
||
* 出口参数:weekday——接收 0..6 的星期值,0 表示周日。
|
||
* 返回值:1 成功;0 日期非法或输出指针为空,失败时输出不变。
|
||
*/
|
||
int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday)
|
||
{
|
||
if (weekday == NULL || !Date_Is_Valid(year, month, day)) {
|
||
return 0;
|
||
}
|
||
*weekday = Weekday_From_Days(Days_From_Civil(year, month, day));
|
||
return 1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将 UTC 日历转换为自 1970-01-01 00:00:00 UTC 起的完整秒数。
|
||
* 入口参数:time——1970..2099 年的合法 UTC 日历;ms 须在 0..999 内。
|
||
* 出口参数:seconds——接收 0..IEC60870_TIME_MAX_SECONDS;0 是有效时间值。
|
||
* 返回值:1 成功;0 参数非法,失败时输出不变。
|
||
* 说明:日历类型没有时区或 SU,调用方须保证输入采用 UTC;ms 只检查范围,不计入完整秒数。
|
||
* 含 IV/SU 的 CP56 解析值应使用 CP56_To_Seconds,不能直接丢弃标志后调用本函数。
|
||
*/
|
||
int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds)
|
||
{
|
||
uint32_t result;
|
||
|
||
if (seconds == NULL || !WallTime_Is_Valid(time)) {
|
||
return 0;
|
||
}
|
||
result = Seconds_From_Civil(time);
|
||
*seconds = result;
|
||
return 1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将 UTC 纪元秒数和毫秒余数还原为完整公历时间。
|
||
* 入口参数:seconds——0..IEC60870_TIME_MAX_SECONDS;ms——0..999。
|
||
* 出口参数:time——接收 UTC 年月日、时分秒、毫秒及重新计算的 weekday。
|
||
* 返回值:1 成功;0 参数非法或平台转换失败,失败时输出不变,不对越界毫秒进行截断或进位。
|
||
* 说明:编译时选择平台库或整数算法;使用局部结果,全部完成后才写回调用方输出。
|
||
*/
|
||
int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time)
|
||
{
|
||
IEC60870_WallTime_t result = {0};
|
||
#if IEC60870_TIME_USE_PLATFORM
|
||
__time64_t stamp;
|
||
struct tm utc;
|
||
#else
|
||
uint32_t days, rem, z, era, doe, yoe, doy, mp, month, year;
|
||
#endif
|
||
|
||
if (time == NULL || seconds > IEC60870_TIME_MAX_SECONDS || ms > 999U) {
|
||
return 0;
|
||
}
|
||
#if IEC60870_TIME_USE_PLATFORM
|
||
/* 显式使用 64 位接口,避免 32 位 time_t 的 2038 年上限;转换结果始终为 UTC。 */
|
||
stamp = (__time64_t)seconds;
|
||
if (_gmtime64_s(&utc, &stamp) != 0) {
|
||
return 0;
|
||
}
|
||
result.year = (uint16_t)(utc.tm_year + 1900);
|
||
result.month = (uint8_t)(utc.tm_mon + 1);
|
||
result.day = (uint8_t)utc.tm_mday;
|
||
result.hour = (uint8_t)utc.tm_hour;
|
||
result.minute = (uint8_t)utc.tm_min;
|
||
result.second = (uint8_t)utc.tm_sec;
|
||
result.weekday = (uint8_t)utc.tm_wday;
|
||
#else
|
||
/* 先分成完整天数和日内秒数,日期换算与时分秒拆分分别进行。 */
|
||
days = seconds / 86400U;
|
||
rem = seconds % 86400U;
|
||
/*
|
||
* 与 Days_From_Civil 反向对应:先恢复以三月为年首的天数,再求 400 年周期及周期内年份。
|
||
* yoe 中的修正项处理闰日、世纪和周期末日,避免将周期最后一天归入下一周期。
|
||
* mp 先得到从三月起算的月份编号,再还原为 1..12;一、二月归回下一公历年。
|
||
*/
|
||
z = days + 719468U;
|
||
era = z / 146097U;
|
||
doe = z - era * 146097U;
|
||
yoe = (doe - doe / 1460U + doe / 36524U - doe / 146096U) / 365U;
|
||
doy = doe - (365U * yoe + yoe / 4U - yoe / 100U);
|
||
mp = (5U * doy + 2U) / 153U;
|
||
month = (mp < 10U) ? (mp + 3U) : (mp - 9U);
|
||
year = yoe + era * 400U + ((month <= 2U) ? 1U : 0U);
|
||
|
||
result.year = (uint16_t)year;
|
||
result.month = (uint8_t)month;
|
||
result.day = (uint8_t)(doy - (153U * mp + 2U) / 5U + 1U);
|
||
result.hour = (uint8_t)(rem / 3600U);
|
||
result.minute = (uint8_t)((rem % 3600U) / 60U);
|
||
result.second = (uint8_t)(rem % 60U);
|
||
result.weekday = Weekday_From_Days(days);
|
||
#endif
|
||
result.ms = ms;
|
||
*time = result;
|
||
return 1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将 UTC 纪元秒数和毫秒直接展开为固定 UTC+8 的本地标准时间。
|
||
* 入口参数:utc_seconds——未加时区偏移的 UTC 纪元秒数;ms——0..999。
|
||
* 出口参数:local_time——本地年月日、时分秒、毫秒及重新计算的星期。
|
||
* 返回值:1 成功;0 参数非法、本地日期超出 1970..2099 或平台转换失败,失败时输出不变。
|
||
* 说明:先检查再平移秒数,只展开一次日历;调用方无需手动调整小时、日期或星期。
|
||
*/
|
||
int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms,
|
||
IEC60870_WallTime_t *local_time)
|
||
{
|
||
const uint32_t local_offset_seconds = (uint32_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60U;
|
||
|
||
if (local_time == NULL || ms > 999U ||
|
||
utc_seconds > IEC60870_TIME_MAX_SECONDS - local_offset_seconds) {
|
||
return 0;
|
||
}
|
||
/* 平移值只用于展开本地日历,不作为新的 UTC 时间戳保存或返回。 */
|
||
return IEC60870_Time_From_Seconds(utc_seconds + local_offset_seconds, ms, local_time);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将源日历、SU 和源标准时区一起换算为有效的 UTC 纪元秒数。
|
||
* 入口参数:value——合法的 2000..2099 年 CP56 解析值,IV=0,SU/has_weekday 为 0/1;
|
||
* standard_utc_offset_minutes——源标准当地时间减 UTC 的分钟数,范围 -1439..1439,
|
||
* 不包含夏令时;源标准时区 UTC+8 填 480,即使 SU=1 也不改变该偏移。
|
||
* 出口参数:seconds——接收 UTC 完整秒数,毫秒仍由输入日历保存。
|
||
* 返回值:1 成功;0 参数非法、IV=1 或结果越界,失败时输出不变。
|
||
* 说明:原始解码尚未修正 SU;在本函数中扣除一小时后,不应再由业务重复扣减。
|
||
*/
|
||
int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value,
|
||
int16_t standard_utc_offset_minutes, uint32_t *seconds)
|
||
{
|
||
int64_t utc_seconds;
|
||
|
||
/*
|
||
* 标准时区偏移不包含夏令时,绝对值须小于一天;这是本接口的参数范围。
|
||
* IV=1 的时间可被原始编解码保留,但不能在这里作为有效时间转换;SU=1 则正常参与换算。
|
||
*/
|
||
if (value == NULL || seconds == NULL || !WallTime_Is_Valid(&value->time) ||
|
||
value->time.year < IEC60870_CP56_MIN_YEAR || value->time.year > IEC60870_CP56_MAX_YEAR ||
|
||
value->is_invalid != 0U || value->is_summer_time > 1U || value->has_weekday > 1U ||
|
||
standard_utc_offset_minutes < -1439 || standard_utc_offset_minutes > 1439) {
|
||
return 0;
|
||
}
|
||
/*
|
||
* 标准时间 = 报文日历 - SU * 3600;UTC = 标准时间 - 标准时区偏移。
|
||
* 在秒数上调整可跨日/月/年;有符号 64 位中间值避免 2038 年问题及加减回绕。
|
||
* 原日历和 SU 保持配套,不能先改小时再保留 SU,否则再次换算会重复扣减。
|
||
*/
|
||
utc_seconds = (int64_t)Seconds_From_Civil(&value->time) -
|
||
(int64_t)value->is_summer_time * INT64_C(3600) -
|
||
(int64_t)standard_utc_offset_minutes * INT64_C(60);
|
||
/* 在有符号域中检查最终值,再转 uint32_t,避免负数或超范围结果被当成有效纪元秒数。 */
|
||
if (utc_seconds < 0 || utc_seconds > (int64_t)IEC60870_TIME_MAX_SECONDS) {
|
||
return 0;
|
||
}
|
||
/* 读取输入和检查结果范围后才提交输出;毫秒余数仍取 value->time.ms。 */
|
||
*seconds = (uint32_t)utc_seconds;
|
||
return 1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将 CP56 解析值转换为固定 UTC+8 的本地标准时间日历。
|
||
* 入口参数:value——合法 CP56 解析值,须保留配套的源日历及 IV/SU;
|
||
* source_standard_utc_offset_minutes——源标准时区偏移,单位为分钟,范围 -1439..1439,
|
||
* 不包含夏令时;源标准时区与本地相同时传 IEC60870_LOCAL_UTC_OFFSET_MINUTES。
|
||
* 出口参数:local_time——接收本地标准时间年月日、时分秒、毫秒和重新计算的星期。
|
||
* 返回值:1 成功;0 参数非法、IV=1、转换结果越界或平台转换失败,失败时输出不变。
|
||
* 说明:通常在 From_CP56 成功后调用;输出已处理 SU 和时区,业务不再加八小时或减一小时。
|
||
*/
|
||
int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value,
|
||
int16_t source_standard_utc_offset_minutes,
|
||
IEC60870_WallTime_t *local_time)
|
||
{
|
||
uint32_t utc_seconds;
|
||
|
||
/* 下层已统一检查 IV、SU、源偏移和日期,并完成夏令时修正;本层不再重复扣除一小时。 */
|
||
if (local_time == NULL ||
|
||
!IEC60870_Time_CP56_To_Seconds(value, source_standard_utc_offset_minutes, &utc_seconds)) {
|
||
return 0;
|
||
}
|
||
/* 与直接从时间戳取本地时间共用入口,统一处理上限、跨日/月/年和星期。 */
|
||
return IEC60870_Time_Seconds_To_Local(utc_seconds, value->time.ms, local_time);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将日历及 IV/SU/星期标志编码为七字节 CP56Time2a。
|
||
* 入口参数:value——合法的 2000..2099 年日历,三个标志均为 0/1;
|
||
* capacity——out 的实际可写字节数,至少 IEC60870_CP56_LEN。
|
||
* 出口参数:out——只写前七字节,超出部分保持原样。
|
||
* 返回值:1 成功;0 参数非法,失败时整个输出缓冲区不变。
|
||
* 说明:公共编码只校验并写入日历和标志;业务发送统一设置 SU=0 的约束由后续业务入口落实。
|
||
*/
|
||
int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity)
|
||
{
|
||
uint8_t encoded[IEC60870_CP56_LEN];
|
||
uint8_t weekday = 0U;
|
||
uint16_t msec;
|
||
|
||
if (value == NULL || out == NULL || capacity < IEC60870_CP56_LEN ||
|
||
!WallTime_Is_Valid(&value->time) ||
|
||
value->time.year < IEC60870_CP56_MIN_YEAR || value->time.year > IEC60870_CP56_MAX_YEAR ||
|
||
value->is_invalid > 1U || value->is_summer_time > 1U || value->has_weekday > 1U) {
|
||
return 0;
|
||
}
|
||
/* 报文星期 0 表示未使用;携带星期时由日期重算,将内存中的周日 0 转成报文值 7。 */
|
||
if (value->has_weekday != 0U) {
|
||
weekday = Weekday_From_Days(Days_From_Civil(value->time.year, value->time.month, value->time.day));
|
||
weekday = (uint8_t)((weekday == 0U) ? 7U : weekday);
|
||
}
|
||
/* CP56 前两字节保存分钟内毫秒,范围 0..59999,由秒和毫秒余数组合而成。 */
|
||
msec = (uint16_t)((uint32_t)value->time.second * 1000U + value->time.ms);
|
||
/* 逐字节布局,不把结构体转成报文,也不要求输出缓冲区对齐。 */
|
||
encoded[0] = (uint8_t)(msec & 0xFFU);
|
||
encoded[1] = (uint8_t)(msec >> 8);
|
||
encoded[2] = (uint8_t)(value->time.minute | (uint8_t)(value->is_invalid << 7));
|
||
/*
|
||
* SU 按输入写入 bit7,不改变小时值;小时字段及 SU 已校验,保留位保持为 0。
|
||
* 这里不能直接清除 SU=1:若小时仍是夏令时值,单独清标志会改变报文所表达的时刻。
|
||
*/
|
||
encoded[3] = (uint8_t)(value->time.hour | (uint8_t)(value->is_summer_time << 7));
|
||
/* 日号占低五位、星期占高三位;月份和两位年份单独存储,未使用的高位保持为 0。 */
|
||
encoded[4] = (uint8_t)(value->time.day | (uint8_t)(weekday << 5));
|
||
encoded[5] = value->time.month;
|
||
encoded[6] = (uint8_t)(value->time.year - IEC60870_CP56_MIN_YEAR);
|
||
/* 校验和读取输入完成后才统一提交输出;也允许 out 与 value 的存储重叠。 */
|
||
memcpy(out, encoded, IEC60870_CP56_LEN);
|
||
return 1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:校验并解码前七字节 CP56Time2a,保留源日历及 IV/SU。
|
||
* 入口参数:in——报文起点;length——实际可读字节数,至少 IEC60870_CP56_LEN。
|
||
* 出口参数:value——接收日历、IV、SU 和是否携带星期;weekday 始终按日期重新计算。
|
||
* 返回值:1 成功;0 指针、长度、保留位、日期时间或非零星期不合法,失败时输出不变。
|
||
* 说明:后续字节不处理;SU=1 或 IV=1 本身不导致解码失败,仍要求日期及其余字段合法。
|
||
* 成功只表示满足格式约束,尚未进行时间转换,也不表示该时间可以用于校时。
|
||
*/
|
||
int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value)
|
||
{
|
||
IEC60870_CP56Time2a_t result = {0};
|
||
uint16_t msec;
|
||
uint8_t received_weekday, expected_weekday;
|
||
|
||
if (in == NULL || value == NULL || length < IEC60870_CP56_LEN) {
|
||
return 0;
|
||
}
|
||
/*
|
||
* 本模块要求分钟 bit6、小时 bit5..6、月份 bit4..7、年份 bit7 为 0。
|
||
* 非零保留位直接拒绝,不能用掩码静默丢弃;IV 和 SU 为有效标志,另外提取。
|
||
*/
|
||
if ((in[2] & 0x40U) != 0U || (in[3] & 0x60U) != 0U ||
|
||
(in[5] & 0xF0U) != 0U || (in[6] & 0x80U) != 0U) {
|
||
return 0;
|
||
}
|
||
/* 按低字节在前还原分钟内毫秒;超过 59999 不能表示合法的秒与毫秒余数。 */
|
||
msec = (uint16_t)((uint16_t)in[0] | ((uint16_t)in[1] << 8));
|
||
if (msec > 59999U) {
|
||
return 0;
|
||
}
|
||
result.time.year = (uint16_t)(IEC60870_CP56_MIN_YEAR + in[6]);
|
||
result.time.month = (uint8_t)(in[5] & 0x0FU);
|
||
result.time.day = (uint8_t)(in[4] & 0x1FU);
|
||
result.time.hour = (uint8_t)(in[3] & 0x1FU);
|
||
result.time.minute = (uint8_t)(in[2] & 0x3FU);
|
||
result.time.second = (uint8_t)(msec / 1000U);
|
||
result.time.ms = (uint16_t)(msec % 1000U);
|
||
result.is_invalid = (uint8_t)((in[2] >> 7) & 1U);
|
||
/* 与原始小时配套保留 SU,交给后续转换接口处理,避免丢失信息或重复扣除一小时。 */
|
||
result.is_summer_time = (uint8_t)((in[3] >> 7) & 1U);
|
||
/* 报文星期 0 表示未提供星期;内存中的 weekday=0 表示周日,需由 has_weekday 区分。 */
|
||
received_weekday = (uint8_t)(in[4] >> 5);
|
||
result.has_weekday = (uint8_t)(received_weekday != 0U);
|
||
/* 年份 100..127 会展开为 2100..2127,在此随其他无效日期一起拒绝,不自动映射世纪。 */
|
||
if (!WallTime_Is_Valid(&result.time)) {
|
||
return 0;
|
||
}
|
||
/* 无论报文是否携带星期,都从日期计算输出;非零报文星期还须与计算结果一致。 */
|
||
result.time.weekday = Weekday_From_Days(Days_From_Civil(result.time.year, result.time.month,
|
||
result.time.day));
|
||
expected_weekday = (uint8_t)((result.time.weekday == 0U) ? 7U : result.time.weekday);
|
||
if (received_weekday != 0U && received_weekday != expected_weekday) {
|
||
return 0;
|
||
}
|
||
/*
|
||
* IV=1 仍可供业务记录;取有效的本地标准时间时,应将完整结果交给 CP56_To_Local 检查和转换。
|
||
* 是否允许校时、远端时间是否合理仍由业务判断,此处不设置 RTC。
|
||
*/
|
||
*value = result;
|
||
return 1;
|
||
}
|