Files
STM32F4-Base/IEC60870-Slave/common/IEC60870_Time.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

503 lines
24 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.
/*
* 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;
}