Files
STM32F4-Base/IEC60870-Slave/test/iec104_command_test.c
T
STF e1b0434774
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 43s
补充遥调业务代码
2026-11-01 00:38:09 -07:00

2949 lines
150 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遥控业务回归:通过真实Slave、TCP及SendManager路径验证控制事务。
* 复用已有测试的可控Net、单调时钟和收发缓冲;重命名后的原测试入口不在本文件执行。
* 输入及期望应答由字节手工构造,避免输入与被测编码器共享同一种错误。
* 本文件已间接包含TcpServer.c,构建时不得再链接TcpServer.c或平台Net后端。
* 同一组用例分别运行于TCP容量1/2、独立应答槽容量1/4,输出用例组数与公开结构尺寸。
*/
#define main command_net_fixture_main
#include "iec104_tcp_event_ring_test.c"
#undef main
#include "IEC104_Command.h"
#ifndef IEC104_COMMAND_REPLY_CAPACITY
#define IEC104_COMMAND_REPLY_CAPACITY 4U
#endif
/* 每个用例独占一套服务、业务实例和站点;open/close负责隔离上一轮状态。
* last_input保留修复自引用后的真实Slave事件,供构造发送拥塞或重入校验使用。
* action_result控制模拟动作结果,action_calls和action_request核对至多一次执行及参数。
* window只在窗口背压用例中缩小,close恢复默认值,防止影响下一组测试。 */
static IEC104_Slave_t *command_slave;
static IEC104_Command_t *command;
static IEC104_Station_t command_station;
static IEC104_CommandStatus_t command_status;
static IEC104_SlaveEvent_t command_last_input;
static IEC104_CommandEventResult_t command_last_handled;
static unsigned command_master_ns, command_action_calls, command_checks;
static IEC104_CommandActionResult_t command_action_result;
static IEC104_CommandRequest_t command_action_request;
static uint16_t command_window = 32U;
/* These immutable tables belong to the test fixture, not the library defaults.
* Keep all borrowed point storage alive until the corresponding Destroy. */
static const IEC104_CommandPoint_t command_select_points[] = {
{1U, 6001U, UINT16_C(0x1fbf), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{1U, 6002U, UINT16_C(0x1fbf), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{2U, 6001U, UINT16_C(0x1fbf), IEC104_COMMAND_MODE_SELECT_EXECUTE}
};
static const IEC104_CommandPoint_t command_direct_points[] = {
{1U, 6001U, UINT16_C(0x3fff), IEC104_COMMAND_MODE_DIRECT},
{1U, 6002U, UINT16_C(0x3fff), IEC104_COMMAND_MODE_DIRECT},
{2U, 6001U, UINT16_C(0x3fff), IEC104_COMMAND_MODE_DIRECT}
};
static void command_config_mode(IEC104_CommandConfig_t *config, IEC104_CommandMode_t mode)
{
config->points = mode == IEC104_COMMAND_MODE_DIRECT ? command_direct_points : command_select_points;
config->point_count = sizeof(command_select_points) / sizeof(command_select_points[0]);
}
static void command_config_default(IEC104_CommandConfig_t *config)
{
IEC104_Command_Config_Default(config);
command_config_mode(config, IEC104_COMMAND_MODE_SELECT_EXECUTE);
}
/* 消费全部Slave通知并按原FIFO交给Command,不在此消费业务事件或执行应用动作。
* last_handled只记录最近一条I帧的分派结果,后续RECORD/启停通知不得覆盖它。
* 用于验证“先读空通知,再推进动作”的接入约定。 */
static void command_drain_transport(void)
{
IEC104_SlaveEvent_t event;
IEC104_TcpEventResult_t result;
while ((result = IEC104_Slave_NextEvent(command_slave, &event)) == IEC104_TCP_EVENT_READY) {
IEC104_CommandEventResult_t handled;
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) {
CHECK(IEC104_Slave_Copy_Event(&command_last_input, &event));
}
handled = IEC104_Command_Handle_Event(command, &event);
CHECK(handled != IEC104_COMMAND_EVENT_ERROR);
if (event.transport.type == IEC104_TCP_EVENT_I_FRAME) { command_last_handled = handled; }
}
CHECK(result == IEC104_TCP_EVENT_EMPTY);
}
/* 推进一次真实传输层并读空通知;等待参数为0,可控后端不会真实阻塞。 */
static void command_poll(void)
{
CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK);
command_drain_transport();
}
/* 推进一步业务并刷新只读状态;等待或已正常终结也必须返回处理成功。 */
static void command_process(void)
{
CHECK(IEC104_Command_Process(command));
CHECK(IEC104_Command_Get_Status(command, &command_status));
}
/* 有界交替驱动传输与业务,使已就绪应答交接、写出及通知消费能够完成。
* 不自动Accept、不读取FINISHED,也不伪造主站ACK;仍然等待的事务由各用例显式处理。 */
static void command_flush(void)
{
unsigned index;
for (index = 0U; index < 16U; ++index) {
command_poll();
command_process();
}
}
/* 经可控accept和真实STARTDT建立新会话,核对Command收到激活通知。
* 新TCP会话的主站发送序号从0开始;事务及旧事件身份由业务模块自行隔离。 */
static void command_connect(void)
{
fake.accept_pending = 1U;
command_poll();
push(start_act, sizeof(start_act));
command_poll();
CHECK(IEC104_Command_Get_Status(command, &command_status));
CHECK(command_status.active);
command_master_ns = 0U;
}
/* 创建并激活测试服务,configuration为NULL时使用默认参数及显式测试点表。
* 链路和本地发送期限刻意放宽,使业务期限测试不会先触发无关的传输超时。
* 查找内部TCP实例仅用于现有夹具的容量背压和故障注入,不替换公开收发入口。 */
static void command_open(const IEC104_CommandConfig_t *configuration)
{
IEC104_SlaveConfig_t server_configuration;
IEC104_CommandConfig_t defaults;
unsigned slot;
memset(&fake, 0, sizeof(fake));
memset(&command_station, 0, sizeof(command_station));
memset(&command_last_input, 0, sizeof(command_last_input));
fake.now = 1000U;
command_action_calls = 0U;
command_action_result = IEC104_COMMAND_ACTION_STARTED;
command_slave = IEC104_Slave_Create();
CHECK(command_slave != NULL);
IEC104_Slave_Config_Default(&server_configuration);
server_configuration.protocol.link.k = command_window;
server_configuration.protocol.link.t1_ms = 600000U;
server_configuration.protocol.link.t3_ms = 600000U;
server_configuration.protocol.local_send_timeout_ms = 600000U;
CHECK(IEC104_Slave_Init(command_slave, &server_configuration, NULL) == IEC104_TCP_OK);
CHECK(IEC104_Slave_Add_Station(command_slave, &command_station, 1U, &command_action_calls) == IEC104_STATION_OK);
command_config_default(&defaults);
command = IEC104_Command_Create(command_slave, configuration == NULL ? &defaults : configuration);
CHECK(command != NULL);
sut = NULL;
for (slot = 0U; slot < IEC104_TCP_SERVER_CAPACITY; ++slot) {
if (server_pool[slot].in_use) { sut = &server_pool[slot].server; break; }
}
CHECK(sut != NULL);
command_connect();
}
/* 读取且核对恰好一个指定类型的业务事件,返回可跨循环保存的纯值副本。
* 调用后事件才算已交付,后续是否欠应用FINISHED以此为界。 */
static IEC104_CommandEvent_t command_take(IEC104_CommandEventType_t type)
{
IEC104_CommandEvent_t event;
memset(&event, 0, sizeof(event));
CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_READY);
CHECK(event.type == type);
return event;
}
/* 核对业务队列确实为空,并验证EMPTY不改写调用方输出存储的接口契约。 */
static void command_empty(void)
{
IEC104_CommandEvent_t event, before;
memset(&event, 0x5a, sizeof(event));
before = event;
CHECK(IEC104_Command_NextEvent(command, &event) == IEC104_COMMAND_NEXT_EMPTY);
CHECK(memcmp(&event, &before, sizeof(event)) == 0);
}
/* 按Cancel、读完FINISHED、关闭服务、读空通知、Destroy、移除站点的顺序收尾。
* 节点必须已可复用,才能销毁Slave;此处不替应用归还额外Acquire的引用。 */
static void command_close(void)
{
IEC104_CommandEvent_t event;
IEC104_CommandNextResult_t result;
IEC104_Command_Cancel(command);
while ((result = IEC104_Command_NextEvent(command, &event)) == IEC104_COMMAND_NEXT_READY) {
CHECK(event.type == IEC104_COMMAND_FINISHED);
}
CHECK(result == IEC104_COMMAND_NEXT_EMPTY);
CHECK(IEC104_Slave_Close(command_slave, NULL) == IEC104_TCP_OK);
command_drain_transport();
CHECK(IEC104_Command_Destroy(command));
if (IEC104_Station_Is_Registered(&command_station)) {
CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK);
}
CHECK(IEC104_Station_Is_Reusable(&command_station));
CHECK(IEC104_Slave_Destroy(command_slave, NULL) == IEC104_TCP_OK);
command = NULL;
command_slave = NULL;
command_window = 32U;
}
/* 将调用方提供的ASDU字节封装成主站I帧,维护主站N(S),不自动确认子站数据。
* 只写入可控Net输入队列;何时接收、交付和推进业务由用例决定。 */
static void command_queue(const uint8_t *payload, size_t length)
{
uint8_t frame[255] = {0x68};
CHECK(length <= 249U);
frame[1] = (uint8_t)(length + 4U);
frame[2] = (uint8_t)(command_master_ns << 1);
frame[3] = (uint8_t)(command_master_ns >> 7);
++command_master_ns;
memcpy(frame + 6U, payload, length);
push(frame, length + 6U);
}
/* 手工生成固定10字节的45/46请求或等长监视对象,用于输入和独立的期望结果。
* cause包含完整COT字节(包括TEST/P/N),value保留原SCO/DCO或监视对象字节。 */
static void command_bytes(uint8_t bytes[10], unsigned type, unsigned cause,
unsigned oa, unsigned ca, unsigned ioa, unsigned value)
{
bytes[0] = (uint8_t)type; bytes[1] = 1U; bytes[2] = (uint8_t)cause; bytes[3] = (uint8_t)oa;
bytes[4] = (uint8_t)ca; bytes[5] = (uint8_t)(ca >> 8);
bytes[6] = (uint8_t)ioa; bytes[7] = (uint8_t)(ioa >> 8); bytes[8] = (uint8_t)(ioa >> 16);
bytes[9] = (uint8_t)value;
}
/* 发送一条手工构造的请求并消费传输通知;不会自动读取或应答工作事件。 */
static void command_send(unsigned type, unsigned cause, unsigned oa, unsigned ca, unsigned ioa, unsigned value)
{
uint8_t bytes[10];
command_bytes(bytes, type, cause, oa, ca, ioa, value);
command_queue(bytes, sizeof(bytes));
command_poll();
}
/* 扫描已经写出的APDU,返回真实业务ASDU的地址及长度,保持线上原顺序。
* 忽略S/U帧和制造拥塞的Type200填充帧;输入必须已写成完整帧。
* 输出指针借用fake.output,各用例应在缓冲内容改变前完成断言。 */
static unsigned command_collect(const uint8_t **frames, size_t *lengths)
{
size_t offset = 0U;
unsigned count = 0U;
while (offset < fake.sent_bytes) {
const uint8_t *frame = fake.output + offset;
size_t length;
CHECK(fake.sent_bytes - offset >= 6U && frame[0] == 0x68U);
length = (size_t)frame[1] + 2U;
CHECK(length >= 6U && offset + length <= fake.sent_bytes);
if ((frame[2] & 1U) == 0U && frame[6] != 200U) {
CHECK(count < 64U);
frames[count] = frame + 6U;
lengths[count++] = length - 6U;
}
offset += length;
}
return count;
}
/* 核对第index条业务输出的完整原文;同时检查长度、OA/CA/IOA、原命令值及COT。
* 期望原文由手工字节构造,避免只检查状态计数而遗漏回显或帧序错误。 */
static void command_reply_at(unsigned index, unsigned type, unsigned cause,
unsigned oa, unsigned ca, unsigned ioa, unsigned value)
{
const uint8_t *frames[64];
size_t lengths[64];
uint8_t expected[10];
CHECK(command_collect(frames, lengths) > index);
command_bytes(expected, type, cause, oa, ca, ioa, value);
CHECK(lengths[index] == sizeof(expected));
CHECK(memcmp(frames[index], expected, sizeof(expected)) == 0);
}
/* 返回已写出的业务I帧数量,过滤规则与command_reply_at一致。 */
static unsigned command_frame_count(void)
{
const uint8_t *frames[64]; size_t lengths[64];
return command_collect(frames, lengths);
}
/* 模拟短时应用动作:记录调用次数与模块实际交付的请求,返回用例预设结果。
* 不发布数据、不推进网络,因此重复调用可直接识别为事务执行错误。 */
static IEC104_CommandActionResult_t command_action(void *context, const IEC104_CommandRequest_t *request)
{
CHECK(context == &command_action_calls);
++command_action_calls;
command_action_request = *request;
return command_action_result;
}
/* 受理并接受一次选择,推进其确认后返回原SELECT事件。
* 固定使用CA=1、IOA=6001、OA=0x51,方便后续匹配与逐字段变异测试。 */
static IEC104_CommandEvent_t command_select(unsigned type, unsigned value)
{
IEC104_CommandEvent_t event;
command_send(type, 6U, 0x51U, 1U, 6001U, value | 0x80U);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_flush();
return event;
}
/* 接受一次执行并返回其事件;刻意不等待确认交接,支持结果先于确认的测试。 */
static IEC104_CommandEvent_t command_execute(unsigned type, unsigned value)
{
IEC104_CommandEvent_t event;
command_send(type, 6U, 0x51U, 1U, 6001U, value);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
return event;
}
/* 构造CA=1、监视IOA=1001、COT=11的返回信息视图;监视IOA与遥控IOA不同。
* object借用bytes,调用方在提交返回WAIT期间必须保留并维护该缓冲。 */
static void command_return(IEC60870_Asdu_t *object, uint8_t bytes[10], unsigned type, unsigned value)
{
command_bytes(bytes, type, 11U, 0x51U, 1U, 1001U, value);
CHECK(IEC60870_Asdu_Create_From_Buffer(object,
IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK);
}
/* 报告给定设备结果、关闭返回信息阶段并读出FINISHED,核对成功/失败终结原因。
* 只适用于需要的返回信息已经提交完毕、且无需额外ACK才能推进的用例。 */
static IEC104_CommandEvent_t command_done(const IEC104_CommandEvent_t *event, IEC104_CommandResult_t result)
{
IEC104_CommandEvent_t finished;
command_flush();
CHECK(IEC104_Command_Report_Result(command, event, result) == IEC104_COMMAND_REPLY_OK);
CHECK(IEC104_Command_Complete(command, event) == IEC104_COMMAND_REPLY_OK);
command_flush();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == (result == IEC104_COMMAND_RESULT_SUCCEEDED ?
IEC104_COMMAND_FINISH_COMPLETED : IEC104_COMMAND_FINISH_EXECUTION_FAILED));
command_empty();
return finished;
}
/* 阻塞实际Net写入,再填满当前帧与HIGH容量,直到公开发送接口真实返回WAIT。
* 业务层没有伪造的提交结果;解除send_blocked后由原发送管理器恢复推进。 */
static void command_congest(void)
{
uint8_t bytes[249] = {200, 0, 3, 0, 1, 0};
IEC60870_Asdu_t filler;
unsigned index;
fake.send_blocked = 1U;
CHECK(IEC60870_Asdu_Create_From_Buffer(&filler,
IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, sizeof(bytes)) == IEC60870_ASDU_OK);
for (index = 0U; index < 32U; ++index) {
if (!IEC60870_MasterConnection_Send_ASDU(&command_last_input.connection, &filler)) {
CHECK(IEC104_Slave_Get_Last_Submit_Result(&command_last_input.connection) == IEC104_TCP_SUBMIT_WAIT);
return;
}
}
CHECK(0);
}
/* 配置与实例契约:默认值、类型位边界、非法配置、重复绑定及NULL参数。
* 先销毁合法空实例再逐项尝试非法配置,确保拒绝不是被已有实例掩盖。 */
static void test_command_configuration(void)
{
IEC104_CommandConfig_t config, invalid;
IEC104_CommandStatus_t status;
unsigned index;
IEC104_Command_Config_Default(&config);
CHECK(config.type_mask == UINT16_C(0x3fff));
CHECK(config.points == NULL && config.point_count == 0U);
CHECK(config.select_timeout_ms == 30000U && config.decision_timeout_ms == 15000U);
CHECK(config.execute_timeout_ms == 60000U && config.send_act_term && !config.act_term_on_failure);
CHECK(IEC104_COMMAND_TYPE_BIT(44U) == 0U && IEC104_COMMAND_TYPE_BIT(52U) == 0U);
CHECK(IEC104_COMMAND_TYPE_BIT(58U) == 128U && IEC104_COMMAND_TYPE_BIT(64U) == 8192U);
CHECK(IEC104_COMMAND_TYPE_BIT(65U) == 0U);
CHECK(IEC104_Command_Create(NULL, &config) == NULL);
CHECK(!IEC104_Command_Destroy(NULL));
CHECK(!IEC104_Command_Get_Status(NULL, &status));
command_open(NULL);
CHECK(IEC104_Command_Create(command_slave, &config) == NULL);
CHECK(IEC104_Command_Destroy(command));
for (index = 0U; index < 5U; ++index) {
invalid = config;
switch (index) {
case 0U: invalid.type_mask = 0U; break;
case 1U: invalid.type_mask |= UINT16_C(0x4000); break;
case 2U: invalid.select_timeout_ms = 0U; break;
case 3U: invalid.point_count = 1U; break;
default: invalid.points = command_select_points; break;
}
CHECK(IEC104_Command_Create(command_slave, &invalid) == NULL);
}
CHECK(IEC104_Command_Create(command_slave, NULL) == NULL);
command = IEC104_Command_Create(command_slave, &config);
CHECK(command != NULL);
command_close();
++command_checks;
}
/* 接收校验顺序:先COT、再CA、再IOA、最后对象内容;TEST/P/N随否定回显保留。
* 无唯一完整对象时静默丢弃,SQ和尾字节异常则规范成单对象10字节否定。
* 另验证未支持或被type_mask禁用的类型返回IGNORED,不误交付工作事件。 */
static void test_command_wire_validation(void)
{
static const struct { unsigned type, cot, ca, ioa, value, cause; } cases[] = {
{45, 5, 65535, 0, 2, 45}, {45, 8, 65535, 0, 2, 46},
{45, 6, 2, 0, 2, 46}, {45, 6, 1, 0, 2, 47},
{45, 6, 1, 6001, 2, 7}, {46, 6, 1, 6001, 0, 7},
{46, 6, 1, 6001, 3, 7}, {46, 8, 1, 6001, 3, 9},
{45, 0x46, 1, 6001, 1, 7}, {45, 0x86, 1, 6001, 1, 0x87}
};
unsigned index;
for (index = 0U; index < sizeof(cases) / sizeof(cases[0]); ++index) {
command_open(NULL);
command_send(cases[index].type, cases[index].cot, 0x61U, cases[index].ca,
cases[index].ioa, cases[index].value);
command_flush();
CHECK(command_frame_count() == 1U);
command_reply_at(0U, cases[index].type, cases[index].cause | 0x40U,
0x61U, cases[index].ca, cases[index].ioa, cases[index].value);
CHECK(command_status.rejected == 1U && command_action_calls == 0U);
command_empty();
command_close();
}
for (index = 0U; index < 5U; ++index) {
uint8_t bytes[14];
size_t length = 10U;
command_open(NULL);
command_bytes(bytes, 45U, 6U, 0x62U, 1U, 6001U, 0x81U);
switch (index) {
case 0U: bytes[1] = 0U; break;
case 1U: bytes[1] = 2U; length = 14U; memset(bytes + 10U, 0, 4U); break;
case 2U: length = 9U; break;
case 3U: bytes[1] = 0x81U; break;
default: bytes[10] = 0x55U; length = 11U; break;
}
command_queue(bytes, length); command_poll(); command_flush();
CHECK(command_frame_count() == (index < 3U ? 0U : 1U));
if (index >= 3U) { command_reply_at(0U, 45U, 0x47U, 0x62U, 1U, 6001U, 0x81U); }
command_empty(); command_close();
}
{
IEC104_CommandConfig_t config;
uint8_t timed[17] = {0};
command_config_default(&config);
config.type_mask =
IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(46U);
command_open(&config);
command_send(47U, 6U, 0U, 1U, 6001U, 1U);
CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED);
command_bytes(timed, 58U, 6U, 0U, 1U, 6001U, 1U);
timed[14] = 1U; timed[15] = 1U; timed[16] = 26U;
command_queue(timed, sizeof(timed)); command_poll();
}
CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED);
command_empty(); command_close();
{
IEC104_CommandConfig_t config;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(46U);
command_open(&config);
command_send(45U, 6U, 0U, 1U, 6001U, 0x81U);
CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED);
command_empty();
command_send(46U, 6U, 0U, 1U, 6001U, 0x82U);
CHECK(command_take(IEC104_COMMAND_SELECT).request.type_id == 46U);
command_close();
}
++command_checks;
}
/* 单/双命令的完整选择执行链:分别提交0、1、2帧返回信息,逐字节检查帧序。
* 同时核对选择与执行属于同一事务但使用不同事件令牌、结果冻结、Complete一次性、
* 旧SELECT只能查询事务不能报告执行结果,以及槽回收后旧令牌失效。 */
static void test_command_complete_flows(void)
{
unsigned type, count;
for (type = 45U; type <= 46U; ++type) {
for (count = 0U; count <= 2U; ++count) {
IEC104_CommandEvent_t selection, execution, finished;
IEC104_CommandTransactionInfo_t info;
IEC60870_Asdu_t data;
uint8_t bytes[10];
unsigned index, value = type == 45U ? 1U : 2U;
command_open(NULL);
selection = command_select(type, value | 4U);
CHECK(selection.request.state == value && selection.request.qualifier == 1U);
CHECK(selection.request.is_select && !selection.request.selected);
execution = command_execute(type, value | 4U);
CHECK(execution.request.selected && !execution.request.is_select);
CHECK(execution.transaction_id == selection.transaction_id);
CHECK(execution.event_serial != selection.event_serial);
command_flush();
CHECK(IEC104_Command_Get_Transaction(command, &selection, &info) && info.active);
CHECK(info.phase == IEC104_COMMAND_PHASE_EXECUTING && info.confirm_submitted);
CHECK(IEC104_Command_Report_Result(command, &selection, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE);
CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR);
CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK);
CHECK(IEC104_Command_Report_Result(command, &execution, IEC104_COMMAND_RESULT_FAILED) == IEC104_COMMAND_REPLY_ERROR);
for (index = 0U; index < count; ++index) {
command_return(&data, bytes, type == 45U ? 1U : 3U, value);
CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_OK);
command_poll();
}
CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_OK);
CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_ERROR);
command_return(&data, bytes, 1U, 1U);
CHECK(IEC104_Command_Submit_Return_Info(command, &execution, &data) == IEC104_COMMAND_REPLY_ERROR);
command_flush();
CHECK(command_frame_count() == count + 3U);
command_reply_at(0U, type, 7U, 0x51U, 1U, 6001U, value | 0x84U);
command_reply_at(1U, type, 7U, 0x51U, 1U, 6001U, value | 4U);
for (index = 0U; index < count; ++index) {
command_reply_at(index + 2U, type == 45U ? 1U : 3U, 11U, 0x51U, 1U, 1001U, value);
}
command_reply_at(count + 2U, type, 10U, 0x51U, 1U, 6001U, value | 4U);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && finished.select_effect && finished.execute_effect);
CHECK(finished.request.receive_serial == execution.request.receive_serial);
CHECK(command_action_calls == 2U);
CHECK(!IEC104_Command_Get_Transaction(command, &execution, &info));
CHECK(IEC104_Command_Complete(command, &execution) == IEC104_COMMAND_REPLY_STALE);
command_empty(); command_close();
}
}
++command_checks;
}
/* 直接执行的结果/终止策略矩阵:设备成功或失败,组合两个ACT_TERM配置开关。
* 明确失败终止的COT=10/P/N=0只表示流程结束,FINISHED仍保留EXECUTION_FAILED。 */
static void test_command_result_policies(void)
{
unsigned success, send_term, failure_term;
for (success = 0U; success < 2U; ++success) {
for (send_term = 0U; send_term < 2U; ++send_term) {
for (failure_term = 0U; failure_term < 2U; ++failure_term) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
command_config_default(&config);
command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
config.send_act_term = send_term != 0U;
config.act_term_on_failure = failure_term != 0U;
command_open(&config);
event = command_execute(45U, 1U);
CHECK(!event.request.selected);
finished = command_done(&event, success ? IEC104_COMMAND_RESULT_SUCCEEDED : IEC104_COMMAND_RESULT_FAILED);
CHECK(finished.execute_effect && !finished.select_effect);
CHECK(command_frame_count() == 1U + (send_term && (success || failure_term) ? 1U : 0U));
command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U);
if (send_term && (success || failure_term)) { command_reply_at(1U, 45U, 10U, 0x51U, 1U, 6001U, 1U); }
command_close();
}
}
}
++command_checks;
}
/* 选择与执行各覆盖NOT_STARTED、STARTED、FAILED,检查确认、原因及副作用标志。
* Accept的OK只代表事件消费;再次Accept不能重复动作。无返校动作的NULL选择
* 可以生效,但不得错误标记为曾产生物理选择副作用。 */
static void test_command_action_outcomes(void)
{
unsigned selected, outcome;
for (selected = 0U; selected < 2U; ++selected) {
for (outcome = 0U; outcome < 3U; ++outcome) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
command_config_default(&config);
if (!selected) { command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT); }
command_open(&config);
command_send(45U, 6U, 0x51U, 1U, 6001U, selected ? 0x81U : 1U);
event = command_take(selected ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE);
command_action_result = (IEC104_CommandActionResult_t)outcome;
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
CHECK(command_action_calls == 1U);
command_flush();
command_reply_at(0U, 45U, outcome == IEC104_COMMAND_ACTION_STARTED ? 7U : 0x47U,
0x51U, 1U, 6001U, selected ? 0x81U : 1U);
if (outcome == IEC104_COMMAND_ACTION_STARTED) {
IEC104_Command_Cancel(command);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED);
} else {
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == (outcome == IEC104_COMMAND_ACTION_FAILED ?
IEC104_COMMAND_FINISH_ACTION_FAILED : IEC104_COMMAND_FINISH_REJECTED));
}
CHECK(finished.select_effect == (selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED));
CHECK(finished.execute_effect == (!selected && outcome != IEC104_COMMAND_ACTION_NOT_STARTED));
command_close();
}
}
{
IEC104_CommandEvent_t event, finished;
command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush(); IEC104_Command_Cancel(command);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(!finished.select_effect && command_action_calls == 0U);
command_close();
}
++command_checks;
}
/* 事件身份和拒绝接口:分别篡改实例、事务、事件序号和类型,旧事件不得执行动作。
* Reject只能消费一次;ENDED仍可查询,槽回收后查询失败且输出不变,新事务也不能
* 重新接纳上一事务的令牌。 */
static void test_command_rejection_and_stale(void)
{
IEC104_CommandEvent_t event, stale, finished;
IEC104_CommandTransactionInfo_t info, before;
unsigned index;
command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT);
for (index = 0U; index < 4U; ++index) {
stale = event;
switch (index) {
case 0U: ++stale.instance_id; break;
case 1U: ++stale.transaction_id; break;
case 2U: ++stale.event_serial; break;
default: stale.type = IEC104_COMMAND_EXECUTE; break;
}
CHECK(IEC104_Command_Accept(command, &stale, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
}
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE);
CHECK(IEC104_Command_Reject(command, &event, (IEC104_CommandRejectCause_t)0) == IEC104_COMMAND_REPLY_ERROR);
CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_UNKNOWN_IOA) == IEC104_COMMAND_REPLY_OK);
CHECK(IEC104_Command_Reject(command, &event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_STALE);
command_flush(); command_reply_at(0U, 45U, 0x6fU, 0x51U, 1U, 6001U, 0x81U);
CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.active);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_REJECTED && command_action_calls == 0U);
memset(&info, 0x5a, sizeof(info)); before = info;
CHECK(!IEC104_Command_Get_Transaction(command, &event, &info));
CHECK(memcmp(&before, &info, sizeof(info)) == 0);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
command_close();
++command_checks;
}
/* 选择已确认后逐项改变执行类型、状态、QU、OA,必须取消原选择并回显新请求。
* 其他点的新选择只得到忙否定,原选择仍可正常执行;默认禁止无选择直接执行。 */
static void test_command_matching_and_busy(void)
{
unsigned change;
for (change = 0U; change < 4U; ++change) {
IEC104_CommandEvent_t selected, finished;
unsigned type = change == 0U ? 46U : 45U;
unsigned value = change == 0U ? 2U : (change == 1U ? 0U : (change == 2U ? 5U : 1U));
command_open(NULL); selected = command_select(45U, 1U);
command_send(type, 6U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value);
command_flush();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH);
CHECK(finished.request.receive_serial == selected.request.receive_serial && finished.request.type_id == 45U);
command_reply_at(1U, type, 0x47U, change == 3U ? 0x52U : 0x51U, 1U, 6001U, value);
CHECK(command_action_calls == 1U); command_close();
}
{
IEC104_CommandEvent_t event;
command_open(NULL); (void)command_select(45U, 1U);
command_send(45U, 6U, 0x51U, 1U, 6002U, 0x81U); command_flush();
CHECK(command_status.busy && command_status.busy_rejected == 1U);
command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6002U, 0x81U);
event = command_execute(45U, 1U); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_close();
}
command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush();
CHECK(!command_status.busy); command_reply_at(0U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U);
command_empty(); command_close();
++command_checks;
}
/* 决定窗口内的同点激活静默丢弃:覆盖两种类型、选择/直接执行/选择后执行,
* 以及工作事件未取和已取两种情况。未取分支把原请求与重发放在同一批输入;
* 已取分支制造真实发送背压。变更类型、OA、S/E、状态、QU仍不能插入忙否定,
* 原工作身份、动作参数及决定期限保持不变,最终只出现原请求自己的确认。 */
static void test_command_pending_same_point(void)
{
unsigned type, phase, taken;
for (type = 45U; type <= 46U; ++type) {
for (phase = 0U; phase < 3U; ++phase) {
for (taken = 0U; taken < 2U; ++taken) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
IEC104_CommandTransactionInfo_t info;
uint8_t bytes[10];
unsigned state = type == 45U ? 1U : 2U;
unsigned select_bit = phase == 0U ? 0x80U : 0U;
unsigned prior = phase == 2U ? 1U : 0U;
uint64_t started, original_receive;
command_config_default(&config);
config.decision_timeout_ms = 100U;
if (phase == 1U) { command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT); }
command_open(&config);
if (phase == 2U) { (void)command_select(type, state); }
started = fake.now;
command_bytes(bytes, type, 6U, 0x51U, 1U, 6001U, state | select_bit);
command_queue(bytes, sizeof(bytes));
if (taken != 0U) {
command_poll();
event = command_take(phase == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE);
command_congest();
}
command_queue(bytes, sizeof(bytes)); command_flush();
original_receive = command_last_input.transport.data.i_frame.receive_serial - 1U;
fake.now += 5U;
command_send(type, 6U, 0x51U, 1U, 6001U, state | (select_bit ^ 0x80U));
command_send(type, 6U, 0x51U, 1U, 6001U, (type == 45U ? 0U : 1U) | select_bit);
command_send(type, 6U, 0x51U, 1U, 6001U, state | select_bit | 4U);
command_send(type, 6U, 0x52U, 1U, 6001U, state | select_bit);
command_send(type == 45U ? 46U : 45U, 6U, 0x51U, 1U, 6001U,
(type == 45U ? 2U : 1U) | select_bit);
CHECK(command_last_handled == IEC104_COMMAND_EVENT_HANDLED);
command_flush();
CHECK(command_status.accepted == prior + 1U && command_status.dropped_requests == 6U);
CHECK(command_status.rejected == 0U && command_status.busy_rejected == 0U &&
command_status.overloads == 0U && command_status.negative_submitted == 0U);
CHECK(command_status.pending_replies == 0U && command_status.pending_events == (taken == 0U ? 1U : 0U));
CHECK(command_status.timer_active && command_status.timeout_remaining_ms == 95U);
CHECK(command_frame_count() == prior && command_action_calls == prior);
if (taken == 0U) { event = command_take(phase == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); }
CHECK(event.request.type_id == type && event.request.originator_address == 0x51U &&
event.request.state == state && event.request.qualifier == 0U &&
event.request.is_select == (phase == 0U) && event.request.selected == (phase == 2U));
CHECK(event.request.accepted_ms == started && event.request.receive_serial == original_receive);
CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && info.active &&
info.phase == (phase == 0U ? IEC104_COMMAND_PHASE_SELECT_WAIT : IEC104_COMMAND_PHASE_EXECUTE_WAIT));
command_empty();
fake.send_blocked = 0U; command_flush();
CHECK(command_frame_count() == prior);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_flush();
CHECK(command_action_calls == prior + 1U && command_frame_count() == prior + 1U);
CHECK(command_action_request.receive_serial == original_receive &&
command_action_request.state == state && command_action_request.qualifier == 0U);
command_reply_at(prior, type, 7U, 0x51U, 1U, 6001U, state | select_bit);
if (phase != 0U) { (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); }
command_close();
}
}
}
++command_checks;
}
/* 重发不能续期,也不能替换超时确认的回显来源。决定到期后只拒绝原请求;
* FINISHED仍按事务历史配对,选择曾交付时未读EXECUTE超时也须通知清理。 */
static void test_command_pending_repeat_deadline(void)
{
unsigned phase, taken;
for (phase = 0U; phase < 3U; ++phase) {
for (taken = 0U; taken < 2U; ++taken) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
unsigned prior = phase == 2U ? 1U : 0U;
unsigned value = phase == 0U ? 0x82U : 2U;
uint64_t original_receive;
command_config_default(&config); config.decision_timeout_ms = 10U;
if (phase == 1U) { command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT); }
command_open(&config);
if (phase == 2U) { (void)command_select(46U, 2U); }
command_send(46U, 6U, 0x51U, 1U, 6001U, value);
original_receive = command_last_input.transport.data.i_frame.receive_serial;
if (taken != 0U) { event = command_take(phase == 0U ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE); }
fake.now += 9U; command_send(45U, 6U, 0x52U, 1U, 6001U, 0x85U); command_process();
CHECK(command_status.dropped_requests == 1U && command_status.timeout_remaining_ms == 1U);
CHECK(command_frame_count() == prior);
++fake.now; command_flush();
CHECK(command_status.decision_timeouts == 1U && command_action_calls == prior);
CHECK(command_frame_count() == prior + 1U);
command_reply_at(prior, 46U, 0x47U, 0x51U, 1U, 6001U, value);
if (taken != 0U || phase == 2U) {
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_DECISION_TIMEOUT &&
finished.request.receive_serial == original_receive && finished.request.type_id == 46U);
}
if (taken != 0U) {
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
}
command_empty(); command_close();
}
}
++command_checks;
}
/* 静默策略只作用于通过校验的同点COT=6:禁用类型仍IGNORED,错误请求仍答错,
* 其他IOA/站点仍忙拒绝,合法撤销仍能终结原待决定事务且回显撤销请求。 */
static void test_command_pending_repeat_boundaries(void)
{
static const unsigned invalid[][3] = {
{3U, 0x81U, 0x6dU}, {0x86U, 0x81U, 0xc7U},
{0x46U, 0x81U, 0x47U}, {6U, 0x83U, 0x47U}
};
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
IEC104_Station_t other = {0};
unsigned index;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(45U);
command_open(&config);
CHECK(IEC104_Slave_Add_Station(command_slave, &other, 2U, &other) == IEC104_STATION_OK);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); event = command_take(IEC104_COMMAND_SELECT);
command_send(45U, 6U, 0x52U, 1U, 6001U, 0U);
command_send(46U, 6U, 0x51U, 1U, 6001U, 0x82U);
CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED);
for (index = 0U; index < sizeof(invalid) / sizeof(invalid[0]); ++index) {
command_send(45U, invalid[index][0], 0x51U, 1U, 6001U, invalid[index][1]); command_flush();
command_reply_at(index, 45U, invalid[index][2], 0x51U, 1U, 6001U, invalid[index][1]);
}
command_send(45U, 6U, 0x51U, 1U, 6002U, 0x81U); command_flush();
command_reply_at(index++, 45U, 0x47U, 0x51U, 1U, 6002U, 0x81U);
command_send(45U, 6U, 0x51U, 2U, 6001U, 0x81U); command_flush();
command_reply_at(index++, 45U, 0x47U, 0x51U, 2U, 6001U, 0x81U);
CHECK(command_status.dropped_requests == 1U && command_status.busy_rejected == 2U &&
command_status.rejected == 6U && command_status.overloads == 0U && command_action_calls == 0U);
command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_flush();
command_reply_at(index++, 45U, 9U, 0x51U, 1U, 6001U, 0U);
CHECK(command_frame_count() == index && command_status.dropped_requests == 1U);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED &&
finished.request.receive_serial == event.request.receive_serial);
CHECK(IEC104_Slave_Remove_Station(command_slave, &other) == IEC104_STATION_OK);
command_empty(); command_close(); ++command_checks;
}
/* 同点依据原注册身份及会话;删除后未知CA仍回46,同CA替代节点仍属其他点并忙拒绝。
* STOP/断线终结待决定事务后,相同原文可建立新事务;此前被丢弃的请求不得留下回复或动作。 */
static void test_command_pending_repeat_lifetime(void)
{
IEC104_CommandEvent_t event, next, finished;
IEC104_Station_t replacement = {0};
unsigned mode;
command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); command_flush();
command_reply_at(0U, 45U, 0x6eU, 0x51U, 1U, 6001U, 0x81U);
CHECK(IEC104_Slave_Add_Station(command_slave, &replacement, 1U, &replacement) == IEC104_STATION_OK);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); command_flush();
command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6001U, 0x81U);
CHECK(command_status.dropped_requests == 0U && command_status.busy_rejected == 1U);
IEC104_Command_Cancel(command); finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED &&
finished.request.station_id == event.request.station_id && IEC104_Station_Is_Reusable(&command_station));
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U); next = command_take(IEC104_COMMAND_SELECT);
CHECK(next.request.station_id != event.request.station_id && next.transaction_id != event.transaction_id);
CHECK(IEC104_Command_Accept(command, &next, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_flush(); command_reply_at(2U, 45U, 7U, 0x51U, 1U, 6001U, 0x81U);
CHECK(command_action_calls == 1U && command_frame_count() == 3U);
CHECK(IEC104_Slave_Remove_Station(command_slave, &replacement) == IEC104_STATION_OK);
command_close(); CHECK(IEC104_Station_Is_Reusable(&replacement));
for (mode = 0U; mode < 2U; ++mode) {
IEC104_CommandConfig_t config;
IEC104_SlaveEvent_t old_input;
command_config_default(&config); command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
command_open(&config); command_send(46U, 6U, 0x51U, 1U, 6001U, 2U);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(IEC104_Slave_Copy_Event(&old_input, &command_last_input));
command_send(46U, 6U, 0x51U, 1U, 6001U, 2U);
if (mode == 0U) { push(stop_act, sizeof(stop_act)); }
else { fake.recv_eof = 1U; }
command_poll(); finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == (mode == 0U ? IEC104_COMMAND_FINISH_STOPPED : IEC104_COMMAND_FINISH_DISCONNECTED));
CHECK(command_action_calls == 0U && command_frame_count() == 0U);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
if (mode == 0U) { push(start_act, sizeof(start_act)); command_poll(); }
else {
fake.recv_eof = 0U; command_connect();
CHECK(IEC104_Command_Handle_Event(command, &old_input) == IEC104_COMMAND_EVENT_IGNORED);
}
command_send(46U, 6U, 0x51U, 1U, 6001U, 2U); next = command_take(IEC104_COMMAND_EXECUTE);
CHECK(next.transaction_id != event.transaction_id);
CHECK((next.request.generation == event.request.generation) == (mode == 0U));
CHECK(IEC104_Command_Accept(command, &next, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
(void)command_done(&next, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_action_calls == 1U && command_frame_count() == 2U && command_status.dropped_requests == 1U);
command_reply_at(0U, 46U, 7U, 0x51U, 1U, 6001U, 2U);
command_empty(); command_close();
}
++command_checks;
}
/* FINISHED配对按事务历史判断:未读SELECT、未读直接EXECUTE不欠终结通知;
* SELECT曾交付后,即使匹配的EXECUTE尚未读,撤销也必须恰好交付一次FINISHED。
* 撤销不要求命令值相同,DEACT_CON应回显撤销请求自身的值。 */
static void test_command_finished_pairing(void)
{
unsigned mode;
for (mode = 0U; mode < 4U; ++mode) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
command_config_default(&config);
if (mode == 1U) { command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT); }
command_open(&config);
if (mode >= 2U) {
event = command_select(45U, 1U);
command_send(45U, 6U, 0x51U, 1U, 6001U, 1U);
} else {
command_send(45U, 6U, 0x51U, 1U, 6001U, mode == 0U ? 0x81U : 1U);
}
if (mode == 3U) { event = command_take(IEC104_COMMAND_EXECUTE); }
command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_flush();
if (mode < 2U) { command_empty(); }
else {
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED);
CHECK(finished.transaction_id == event.transaction_id && finished.select_effect);
command_empty();
}
command_reply_at(mode >= 2U ? 1U : 0U, 45U, 9U, 0x51U, 1U, 6001U, 0U);
command_close();
}
++command_checks;
}
/* 选择确认未交接时的优先级:即使新执行值或类型不匹配,也只按实例忙拒绝。
* 发送恢复后原选择仍有效,再发匹配执行应成功,不能提前走MISMATCH取消路径。 */
static void test_command_unsubmitted_selection(void)
{
IEC104_CommandEvent_t selected, executed;
IEC104_CommandTransactionInfo_t info;
command_open(NULL);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
selected = command_take(IEC104_COMMAND_SELECT); command_congest();
CHECK(IEC104_Command_Accept(command, &selected, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_process();
CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && !info.confirm_submitted);
/* 选择确认尚在模块内等待;此时不匹配执行也不能使已生效选择作废。 */
command_send(46U, 6U, 0x51U, 1U, 6001U, 2U);
CHECK(IEC104_Command_Get_Transaction(command, &selected, &info) && info.active);
CHECK(info.phase == IEC104_COMMAND_PHASE_SELECTED);
fake.send_blocked = 0U; command_flush();
CHECK(command_status.mismatches == 0U && command_status.busy_rejected == 1U);
command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 0x81U);
command_reply_at(1U, 46U, 0x47U, 0x51U, 1U, 6001U, 2U);
executed = command_execute(45U, 1U);
(void)command_done(&executed, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_action_calls == 2U);
command_close(); ++command_checks;
}
/* 应用结束与槽释放分离:先读取撤销FINISHED,但DEACT_CON仍被HIGH背压阻塞。
* 事务必须继续占槽,Destroy失败,新选择忙拒绝;确认交接后才允许释放。 */
static void test_command_deactivation_wait(void)
{
IEC104_CommandEvent_t event, finished;
command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT); command_congest();
command_send(45U, 8U, 0x51U, 1U, 6001U, 0U); command_process();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED);
CHECK(IEC104_Command_Get_Status(command, &command_status));
CHECK(command_status.busy && command_status.pending_replies != 0U);
CHECK(!IEC104_Command_Destroy(command));
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
command_send(45U, 6U, 0x52U, 1U, 6002U, 0x81U);
fake.send_blocked = 0U; command_flush();
CHECK(!command_status.busy);
command_reply_at(0U, 45U, 9U, 0x51U, 1U, 6001U, 0U);
command_reply_at(1U, 45U, 0x47U, 0x52U, 1U, 6002U, 0x81U);
command_empty(); command_close();
++command_checks;
}
/* 决定、选择、执行三种期限分别验证到期前、恰好到期和单调时间倒退。
* 不先调用Process,而由Accept、Handle_Event、Report_Result或NextEvent触发检查,
* 防止过期状态被新的操作复活;关闭持续时长限制仍不能掩盖时钟倒退。 */
static void test_command_deadlines(void)
{
unsigned phase, boundary;
for (phase = 0U; phase < 3U; ++phase) {
for (boundary = 0U; boundary < 3U; ++boundary) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint64_t started;
command_config_default(&config);
config.decision_timeout_ms = config.select_timeout_ms = config.execute_timeout_ms = 10U;
command_open(&config);
if (phase == 0U) {
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT);
} else {
event = command_select(45U, 1U);
if (phase == 2U) { event = command_execute(45U, 1U); command_flush(); }
}
started = fake.now;
if (phase == 1U) {
uint8_t bytes[10];
command_bytes(bytes, 45U, 6U, 0x51U, 1U, 6001U, 1U);
command_queue(bytes, sizeof(bytes));
CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK);
}
/* 传输接收时保持正常时间,再改变业务受理时刻,隔离链路计时的影响。 */
fake.now = boundary == 2U ? started - 1U : started + 9U + boundary;
if (phase == 0U) {
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) ==
(boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE));
} else if (phase == 1U) {
command_drain_transport();
} else {
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) ==
(boundary == 0U ? IEC104_COMMAND_REPLY_OK : IEC104_COMMAND_REPLY_STALE));
}
if (boundary != 0U) {
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT :
(phase == 1U ? IEC104_COMMAND_FINISH_SELECT_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT)));
CHECK(IEC104_Command_Get_Status(command, &command_status));
CHECK(command_status.clock_errors == (boundary == 2U ? 1U : 0U));
} else if (phase == 1U) {
event = command_take(IEC104_COMMAND_EXECUTE); CHECK(event.request.selected);
}
fake.now = started + 10U;
command_close();
}
}
/* 期限配置0仅关闭持续时长限制;单调时钟倒退仍须终结并累计clock_errors。 */
for (phase = 0U; phase < 2U; ++phase) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
command_config_default(&config);
command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
config.decision_timeout_ms = config.execute_timeout_ms = 0U;
command_open(&config);
command_send(45U, 6U, 0x51U, 1U, 6001U, 1U);
event = command_take(IEC104_COMMAND_EXECUTE);
if (phase != 0U) {
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_flush();
}
--fake.now;
CHECK(IEC104_Command_Get_Status(command, &command_status));
CHECK(!command_status.timer_active && command_status.need_process);
if (phase == 0U) {
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
CHECK(command_action_calls == 0U);
} else {
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE);
}
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == (phase == 0U ? IEC104_COMMAND_FINISH_DECISION_TIMEOUT : IEC104_COMMAND_FINISH_EXECUTE_TIMEOUT));
CHECK(IEC104_Command_Get_Status(command, &command_status));
CHECK(command_status.clock_errors == 1U);
++fake.now;
command_close();
}
/* SELECT已经交付,即使后续EXECUTE尚未读取,决定超时仍欠应用一次FINISHED。 */
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
command_config_default(&config); config.decision_timeout_ms = 10U;
command_open(&config); (void)command_select(45U, 1U);
command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); fake.now += 10U;
event = command_take(IEC104_COMMAND_FINISHED);
CHECK(event.reason == IEC104_COMMAND_FINISH_DECISION_TIMEOUT);
command_empty(); command_close();
}
++command_checks;
}
/* 设备结果与发送等待分离:ACT_CON被真实发送背压阻塞时先报告设备成功。
* 随后跨过执行期限也不得超时或重复动作;解除背压后依次输出确认、返回信息、终止。 */
static void test_command_result_before_confirm(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
IEC104_CommandTransactionInfo_t info;
IEC60870_Asdu_t data;
uint8_t bytes[10];
command_config_default(&config); command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT); config.execute_timeout_ms = 10U;
command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U);
event = command_take(IEC104_COMMAND_EXECUTE); command_congest();
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_process();
CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && !info.confirm_submitted);
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK);
command_return(&data, bytes, 1U, 1U);
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT);
fake.now += 100U; command_process();
CHECK(command_status.execute_timeouts == 0U && command_action_calls == 1U);
fake.send_blocked = 0U; command_flush();
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK);
CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK);
command_flush(); finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_COMPLETED && command_frame_count() == 3U);
command_reply_at(0U, 45U, 7U, 0x51U, 1U, 6001U, 1U);
command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U);
command_reply_at(2U, 45U, 10U, 0x51U, 1U, 6001U, 1U);
command_close(); ++command_checks;
}
/* 全局FIFO与独立应答满池:连续忙请求填满固定槽,多出的新请求丢弃并计数。
* 只排空传输缓冲而不推进Command时,Submit_Return_Info仍须等待模块内旧应答,
* 证明直接Try路径不能绕过业务FIFO;解除后按原请求OA顺序回包。 */
static void test_command_fifo_and_overload(void)
{
IEC104_CommandEvent_t event;
IEC60870_Asdu_t data;
uint8_t bytes[10];
unsigned index;
command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush();
command_congest();
for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY + 2U; ++index) {
command_send(45U, 6U, 0x60U + index, 1U, 6002U, 0x81U);
}
CHECK(IEC104_Command_Get_Status(command, &command_status));
CHECK(command_status.pending_replies == IEC104_COMMAND_REPLY_CAPACITY);
CHECK(command_status.overloads == 2U && command_status.dropped_requests == 2U);
fake.send_blocked = 0U;
/* 只推进TCP,保留Command队首;底层空闲不能成为返回信息插队的理由。 */
for (index = 0U; index < 16U; ++index) { command_poll(); }
command_return(&data, bytes, 1U, 1U);
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT);
command_flush();
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK);
command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
for (index = 0U; index < IEC104_COMMAND_REPLY_CAPACITY; ++index) {
command_reply_at(index + 2U, 45U, 0x47U, 0x60U + index, 1U, 6002U, 0x81U);
}
command_reply_at(IEC104_COMMAND_REPLY_CAPACITY + 2U, 1U, 11U, 0x51U, 1U, 1001U, 1U);
command_close(); ++command_checks;
}
/* 返回信息必须完整验证类型、COT、P/N、TEST、CA、对象品质和精确长度。
* 错误ASDU不得接手;合法的COT=3及与遥控点不同的监视IOA应可提交。 */
static void test_command_return_validation(void)
{
IEC104_CommandEvent_t event;
IEC60870_Asdu_t object;
uint8_t bytes[11];
unsigned index;
command_open(NULL); (void)command_select(45U, 1U); event = command_execute(45U, 1U); command_flush();
for (index = 0U; index < 7U; ++index) {
size_t length = 10U;
command_bytes(bytes, 1U, 11U, 0x51U, 1U, 1001U, 1U);
switch (index) {
case 0U: bytes[0] = 45U; break;
case 1U: bytes[2] = 5U; break;
case 2U: bytes[2] |= 0x40U; break;
case 3U: bytes[2] |= 0x80U; break;
case 4U: bytes[4] = 2U; break;
case 5U: bytes[9] = 2U; break;
default: bytes[10] = 1U; length = 11U; break;
}
CHECK(IEC60870_Asdu_Create_From_Buffer(&object,
IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, length) == IEC60870_ASDU_OK);
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_ERROR);
}
command_bytes(bytes, 1U, 3U, 0x51U, 1U, 9876U, 0U);
CHECK(IEC60870_Asdu_Create_From_Buffer(&object,
IEC104_Slave_Get_Application_Layer_Parameters(command_slave), bytes, 10U) == IEC60870_ASDU_OK);
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK);
command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_reply_at(2U, 1U, 3U, 0x51U, 1U, 9876U, 0U);
command_close(); ++command_checks;
}
/* STOP/START通知优先于同一时刻的决定超时:未读通知时Accept只返回WAIT且不动作。
* 按FIFO消费后,旧事务原因保持STOPPED,重新激活也不能复活旧事件。 */
static void test_command_stop_notifications(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
command_config_default(&config); config.decision_timeout_ms = 10U;
command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT);
fake.now += 10U;
push(stop_act, sizeof(stop_act)); push(start_act, sizeof(start_act));
CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_WAIT);
command_process(); CHECK(command_status.events_pending && command_status.decision_timeouts == 0U);
command_drain_transport();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED && command_action_calls == 0U);
command_empty(); CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.active);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
command_close();
++command_checks;
}
/* 执行中断线不回滚动作,但必须交付DISCONNECTED并拒绝迟到结果。
* 重连后的同点新事务具有新连接代次与事务号,旧令牌不能作用于新输出。 */
static void test_command_disconnect_and_reuse(void)
{
IEC104_CommandEvent_t event, next, finished;
IEC104_CommandConfig_t config;
uint64_t generation;
command_config_default(&config); command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
command_open(&config); event = command_execute(45U, 1U); command_flush();
generation = event.request.generation;
fake.recv_eof = 1U; command_poll();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_DISCONNECTED && finished.execute_effect);
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE);
command_empty(); fake.recv_eof = 0U; command_connect();
next = command_execute(45U, 1U);
CHECK(next.request.generation != generation && next.transaction_id != event.transaction_id);
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE);
(void)command_done(&next, IEC104_COMMAND_RESULT_SUCCEEDED); command_close();
++command_checks;
}
/* 原站点删除、同CA重新注册时,已受理事务仍持原节点,不能匹配替代站点的执行。
* 应用在工作事件阶段额外Acquire,保证FINISHED出队并释放模块引用之后仍能清理;
* 最后一次应用Release才允许旧节点复用。 */
static void test_command_station_lifetime(void)
{
IEC104_CommandEvent_t event, finished;
IEC104_Station_t replacement = {0};
uint64_t original;
command_open(NULL); command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT); original = event.request.station_id;
CHECK(IEC104_Slave_Acquire_Station(command_slave, &command_station) == IEC104_STATION_OK);
CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK);
CHECK(!IEC104_Station_Is_Reusable(&command_station));
CHECK(IEC104_Slave_Add_Station(command_slave, &replacement, 1U, &replacement) == IEC104_STATION_OK);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_flush(); CHECK(command_action_request.station_id == original);
command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush();
CHECK(command_action_calls == 1U && command_status.busy);
command_reply_at(1U, 45U, 0x47U, 0x51U, 1U, 6001U, 1U);
IEC104_Command_Cancel(command); finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_CANCELLED && finished.request.station_id == original);
CHECK(IEC104_Slave_Get_Station_By_Id(command_slave, original) == &command_station);
CHECK(IEC104_Slave_Release_Station(command_slave, &command_station) == IEC104_STATION_OK);
CHECK(IEC104_Station_Is_Reusable(&command_station));
CHECK(IEC104_Slave_Remove_Station(command_slave, &replacement) == IEC104_STATION_OK);
command_close(); ++command_checks;
}
/* 供动作回调使用的已交付事件,专门验证重入时不能再次消费或销毁当前事务。 */
static IEC104_CommandEvent_t command_reentry_event;
/* 回调中只读查询应成功,推进、消费、拒绝、销毁及重复动作应被拒绝。
* Cancel为无效果操作;返回后原动作仍只执行一次,并可正常报告结果。 */
static IEC104_CommandActionResult_t command_reentry_action(void *context, const IEC104_CommandRequest_t *request)
{
IEC104_CommandTransactionInfo_t info;
IEC104_CommandEvent_t ignored;
IEC104_CommandStatus_t status;
CHECK(IEC104_Command_Get_Status(command, &status));
CHECK(IEC104_Command_Get_Transaction(command, &command_reentry_event, &info));
CHECK(!IEC104_Command_Destroy(command));
CHECK(!IEC104_Command_Process(command));
CHECK(IEC104_Command_NextEvent(command, &ignored) == IEC104_COMMAND_NEXT_ERROR);
CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_action, context) == IEC104_COMMAND_REPLY_ERROR);
CHECK(IEC104_Command_Reject(command, &command_reentry_event, IEC104_COMMAND_REJECT_NEGATIVE) == IEC104_COMMAND_REPLY_ERROR);
CHECK(IEC104_Command_Handle_Event(command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR);
IEC104_Command_Cancel(command);
return command_action(context, request);
}
/* EXECUTE必须提供动作;回调重入受测入口不会改变当前事务或触发第二次动作。 */
static void test_command_reentry(void)
{
IEC104_CommandConfig_t config;
command_config_default(&config); command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U);
command_reentry_event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(IEC104_Command_Accept(command, &command_reentry_event, NULL, NULL) == IEC104_COMMAND_REPLY_ERROR);
CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_reentry_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
CHECK(command_action_calls == 1U);
(void)command_done(&command_reentry_event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_close(); ++command_checks;
}
/* 将k缩为1,逐帧给出真实主站ACK:选择确认、执行确认、返回信息各占一个窗口。
* 未获得容量时Submit_Return_Info返回WAIT,Complete可关闭阶段但TERM仍等待窗口,
* 最后交接TERM后才交付完成通知,随后按统一生命周期释放资源。 */
static void test_command_window_and_cancel(void)
{
IEC104_CommandEvent_t event;
IEC60870_Asdu_t data;
uint8_t bytes[10];
command_window = 1U; command_open(NULL); (void)command_select(45U, 1U);
event = command_execute(45U, 1U); command_process();
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_OK);
command_return(&data, bytes, 1U, 1U);
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT);
push_ack(1U); command_poll(); command_flush();
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_WAIT);
push_ack(2U); command_poll(); command_flush();
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &data) == IEC104_COMMAND_REPLY_OK);
CHECK(IEC104_Command_Complete(command, &event) == IEC104_COMMAND_REPLY_OK);
command_flush(); CHECK(command_frame_count() == 3U);
push_ack(3U); command_poll(); command_flush();
CHECK(command_take(IEC104_COMMAND_FINISHED).reason == IEC104_COMMAND_FINISH_COMPLETED);
CHECK(command_frame_count() == 4U); command_close();
++command_checks;
}
/* TCP容量至少为2时创建第二个独立Slave,验证实例池可分配且事件归属不串服务。
* 外来Slave通知被拒绝,外来工作事件不能触发动作;容量1构建仍保留其余测试。 */
static void test_command_two_services(void)
{
#if IEC104_TCP_SERVER_CAPACITY >= 2
IEC104_Slave_t *other;
IEC104_Command_t *other_command;
IEC104_CommandConfig_t config;
IEC104_SlaveConfig_t service_config;
IEC104_CommandEvent_t event;
command_open(NULL);
other = IEC104_Slave_Create(); CHECK(other != NULL);
IEC104_Slave_Config_Default(&service_config);
CHECK(IEC104_Slave_Init(other, &service_config, NULL) == IEC104_TCP_OK);
command_config_default(&config);
other_command = IEC104_Command_Create(other, &config); CHECK(other_command != NULL);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Handle_Event(other_command, &command_last_input) == IEC104_COMMAND_EVENT_ERROR);
CHECK(IEC104_Command_Accept(other_command, &event, command_action, &command_action_calls) != IEC104_COMMAND_REPLY_OK);
CHECK(command_action_calls == 0U);
CHECK(IEC104_Command_Destroy(other_command));
CHECK(IEC104_Slave_Close(other, NULL) == IEC104_TCP_OK);
CHECK(IEC104_Slave_Destroy(other, NULL) == IEC104_TCP_OK);
command_close();
#endif
++command_checks;
}
/* 对照NORMAL覆盖行为:先把一条返回信息入NORMAL,再以填充事件将其覆盖。
* Command的返回接口在队列未排空时返回WAIT且不接手,排空后直接提交的返回帧
* 仍能按顺序送出,证明命令返回路径没有依赖可覆盖缓存。 */
static void test_command_normal_overwrite(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
IEC60870_Asdu_t object, filler;
uint8_t bytes[10], fill_bytes[249] = {200, 0, 3, 0, 1, 0};
unsigned index;
command_config_default(&config); command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
command_open(&config); event = command_execute(45U, 1U); command_flush();
command_return(&object, bytes, 1U, 0U);
CHECK(IEC104_Slave_Enqueue_Event(command_slave, &object) == IEC104_TCP_SUBMIT_OK);
CHECK(IEC60870_Asdu_Create_From_Buffer(&filler,
IEC104_Slave_Get_Application_Layer_Parameters(command_slave), fill_bytes, sizeof(fill_bytes)) == IEC60870_ASDU_OK);
for (index = 0U; index < IEC104_TCP_NORMAL_CAPACITY / sizeof(fill_bytes) + 4U; ++index) {
CHECK(IEC104_Slave_Enqueue_Event(command_slave, &filler) == IEC104_TCP_SUBMIT_OK);
}
command_return(&object, bytes, 1U, 1U);
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_WAIT);
command_flush();
/* NORMAL中的第一条返回信息(值0)已被覆盖;过滤填充帧后只有ACT_CON。 */
CHECK(command_frame_count() == 1U);
CHECK(IEC104_Command_Submit_Return_Info(command, &event, &object) == IEC104_COMMAND_REPLY_OK);
command_poll(); (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_frame_count() == 3U);
command_reply_at(1U, 1U, 11U, 0x51U, 1U, 1001U, 1U);
command_close(); ++command_checks;
}
/* 临时拒绝启停通知准入,让真实STOP停留在传输层,供提交入口再次处理。 */
static int command_hold_transfer(void *context) { (void)context; return 0; }
/* 利用原传输观察者的背压入口保留STOP,随后恢复观察者。
* 此时服务仍显示已启动且通知队列为空,下一次提交必须亲自处理STOP竞态。 */
static void command_retain_stop(void)
{
IEC104_ConnectionTransferObserver_t observer = sut->connection.transfer_observer;
IEC104_TcpServerStatus_t status;
sut->connection.transfer_observer.can_accept = command_hold_transfer;
push(stop_act, sizeof(stop_act));
CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK);
sut->connection.transfer_observer = observer;
CHECK(sut->rx_blocked && !sut->connection.receive_batch_pending);
CHECK(IEC104_Slave_Get_Status(command_slave, &status, NULL) == IEC104_TCP_OK);
CHECK(status.pending_event_count == 0U && status.connection.data_transfer_started);
}
/* 先真实交付启停通知,再报告观察者失败,构造“已有STOP通知但结果为FATAL”。
* 用于区分可以让出的正常STOP与必须立即终结的传输故障。 */
static int command_fail_after_stop(void *context, IEC104_DataTransferState_t state)
{
CHECK(Notify_Transfer_State(context, state));
return 0;
}
/* 事务确认与独立忙否定同时挂起时,在提交入口内才受理STOP。
* 正常STOP应等待通知再以STOPPED收尾;注入FATAL则立即记SEND_ERROR,不能降为WAIT。
* 两种路径都清理独立应答、保留副作用事实,并拒绝迟到Report_Result。 */
static void test_command_stop_during_submit(void)
{
unsigned fatal;
for (fatal = 0U; fatal < 2U; ++fatal) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
command_config_default(&config); command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
command_open(&config); command_send(45U, 6U, 0x51U, 1U, 6001U, 1U);
event = command_take(IEC104_COMMAND_EXECUTE); command_congest();
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_process();
command_send(45U, 6U, 0x62U, 1U, 6002U, 0x81U);
CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 2U);
command_retain_stop(); fake.send_blocked = 0U;
if (fatal) { sut->connection.transfer_observer.notify = command_fail_after_stop; }
command_process();
if (fatal) {
/* 即使已有STOP通知,FATAL仍必须保留发送错误语义,不能当成普通让出。 */
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_SEND_ERROR);
sut->connection.transfer_observer.notify = Notify_Transfer_State;
command_drain_transport();
} else {
CHECK(command_status.events_pending && command_status.busy && !command_status.need_process);
command_drain_transport(); finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_STOPPED);
}
CHECK(finished.execute_effect && command_action_calls == 1U);
CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U);
CHECK(IEC104_Command_Report_Result(command, &event, IEC104_COMMAND_RESULT_SUCCEEDED) == IEC104_COMMAND_REPLY_STALE);
command_empty(); command_close();
}
++command_checks;
}
/* 无活动事务但仍有独立错误应答时,实例也不能销毁。
* Cancel必须清理该应答池;不会因不存在工作事件而凭空产生FINISHED。 */
static void test_command_independent_reply_lifetime(void)
{
command_open(NULL);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
command_congest();
/* 取消尚未交付的事务,再制造独立错误应答,隔离应答池自身的生命周期。 */
IEC104_Command_Cancel(command);
command_send(45U, 6U, 0x61U, 2U, 6001U, 0x81U);
CHECK(IEC104_Command_Get_Status(command, &command_status));
CHECK(!command_status.busy && command_status.pending_replies == 1U);
CHECK(!IEC104_Command_Destroy(command));
IEC104_Command_Cancel(command);
CHECK(IEC104_Command_Get_Status(command, &command_status) && command_status.pending_replies == 0U);
command_empty(); command_close(); ++command_checks;
}
/* 新增类型输入仍用独立的手工字节,不调用被测公共编码器生成期望结果。
* qualifier_offset为255表示没有限定词;状态类的状态位位于限定词中。 */
static const struct {
unsigned type;
size_t payload_length;
unsigned qualifier_offset;
uint8_t payload[5];
} command_value_cases[] = {
{47U, 1U, 0U, {0x0dU, 0U, 0U, 0U, 0U}},
{48U, 3U, 2U, {0xc7U, 0xcfU, 0x25U, 0U, 0U}},
{49U, 3U, 2U, {0U, 0x80U, 0x7fU, 0U, 0U}},
{50U, 5U, 4U, {0U, 0U, 0xa0U, 0xbfU, 0x11U}},
{51U, 4U, 255U, {0xd5U, 0xaaU, 0x80U, 0xffU, 0U}}
};
/* 输出缓冲由用例提供至少32字节;当前控制族值载荷最多12字节。
* 保留原10字节助手,新增助手只拼接调用者给定的字节和公共头。 */
static size_t command_payload_bytes(uint8_t *bytes, unsigned type, unsigned cause,
unsigned oa, unsigned ca, unsigned ioa, const uint8_t *payload, size_t payload_length)
{
CHECK(payload != NULL && payload_length > 0U && payload_length <= 12U);
command_bytes(bytes, type, cause, oa, ca, ioa, 0U);
memcpy(bytes + 9U, payload, payload_length);
return 9U + payload_length;
}
static void command_raw_send(const uint8_t *bytes, size_t length)
{
command_queue(bytes, length);
command_poll();
}
/* 请求可含SQ或尾部错误,length由用例显式指定期望规范回复长度。
* 完整比较保证没有截断浮点/时标,也没有携带上一条长回复的尾部。 */
static void command_raw_reply_at(unsigned index, const uint8_t *request, size_t length, unsigned cause)
{
const uint8_t *frames[64];
size_t lengths[64];
uint8_t expected[32];
CHECK(length <= sizeof(expected));
CHECK(command_collect(frames, lengths) > index);
memcpy(expected, request, length);
expected[1] = 1U;
expected[2] = (uint8_t)cause;
CHECK(lengths[index] == length);
CHECK(memcmp(frames[index], expected, length) == 0);
}
static void command_check_value(const IEC104_CommandRequest_t *request, unsigned type)
{
CHECK(request->type_id == type);
switch (type) {
case 47U: case 58U: case 60U: CHECK(request->state == 1U && request->qualifier == 3U); break;
case 59U: CHECK(request->state == 2U && request->qualifier == 3U); break;
case 48U: case 61U: CHECK(request->value.normalized == -12345 && request->qualifier == 37U); break;
case 49U: case 62U: CHECK(request->value.scaled == -32768 && request->qualifier == 127U); break;
case 50U: case 63U: CHECK(request->value.short_float == -1.25f && request->qualifier == 17U); break;
default:
CHECK((type == 51U || type == 64U) && request->value.bitstring == UINT32_C(0xff80aad5));
CHECK(request->qualifier == 0U && !request->is_select);
break;
}
}
/* 可配置的类型与默认启用集合分开验证;无S/E的比特串不能要求先选择。 */
static void test_command_extended_configuration(void)
{
static const IEC104_CommandPoint_t invalid = {1U, 6001U,
IEC104_COMMAND_TYPE_BIT(51U), IEC104_COMMAND_MODE_SELECT_EXECUTE};
IEC104_CommandConfig_t config;
unsigned type;
command_open(NULL);
CHECK(IEC104_Command_Destroy(command));
for (type = 47U; type <= 51U; ++type) {
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(type);
command_config_mode(&config, type == 51U ? IEC104_COMMAND_MODE_DIRECT : IEC104_COMMAND_MODE_SELECT_EXECUTE);
command = IEC104_Command_Create(command_slave, &config);
CHECK(command != NULL && IEC104_Command_Destroy(command));
}
config.points = &invalid;
config.point_count = 1U;
CHECK(IEC104_Command_Create(command_slave, &config) == NULL);
command_config_default(&config);
CHECK(config.type_mask == UINT16_C(0x3fff));
command = IEC104_Command_Create(command_slave, &config);
CHECK(command != NULL);
command_close();
++command_checks;
}
/* 逐类型完成选择/执行/结果/终止;51直接执行,且首个值字节bit7=1不能误判选择。
* 应用改写事件副本的数值后,动作和FINISHED仍须取自模块保存的原请求。 */
static void test_command_extended_flows(void)
{
size_t index;
for (index = 0U; index < sizeof(command_value_cases) / sizeof(command_value_cases[0]); ++index) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t bytes[32], selected[32], payload[5];
size_t length;
unsigned type = command_value_cases[index].type;
unsigned preceding = type == 51U ? 0U : 1U;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(type);
command_config_mode(&config, type == 51U ? IEC104_COMMAND_MODE_DIRECT : IEC104_COMMAND_MODE_SELECT_EXECUTE);
command_open(&config);
memcpy(payload, command_value_cases[index].payload, sizeof(payload));
length = command_payload_bytes(bytes, type, 6U, 0x51U, 1U, 6001U,
payload, command_value_cases[index].payload_length);
if (type != 51U) {
memcpy(selected, bytes, length);
selected[9U + command_value_cases[index].qualifier_offset] |= 0x80U;
command_raw_send(selected, length);
event = command_take(IEC104_COMMAND_SELECT);
command_check_value(&event.request, type);
CHECK(event.request.is_select && !event.request.selected);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
command_raw_reply_at(0U, selected, length, 7U);
}
command_raw_send(bytes, length);
event = command_take(IEC104_COMMAND_EXECUTE);
command_check_value(&event.request, type);
CHECK(!event.request.is_select && event.request.selected == (type != 51U));
event.request.value.bitstring = 0U;
event.request.state = 0U;
event.request.qualifier = 0U;
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
CHECK(command_action_calls == 1U);
command_check_value(&command_action_request, type);
finished = command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_check_value(&finished.request, type);
command_raw_reply_at(preceding, bytes, length, 7U);
command_raw_reply_at(preceding + 1U, bytes, length, 10U);
CHECK(command_frame_count() == preceding + 2U);
command_close();
}
++command_checks;
}
/* 改变值或限定词必须结束原选择,否定回显本次执行;不允许只比较限定词。
* 短浮点的+0和-0作为不同线上位模式处理,避免改成浮点数值相等比较。 */
static void test_command_extended_matching(void)
{
size_t index;
unsigned variant;
for (index = 0U; index < 4U; ++index) {
for (variant = 0U; variant < 2U; ++variant) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t selected[32], executed[32];
size_t length;
unsigned type = command_value_cases[index].type;
unsigned qualifier = 9U + command_value_cases[index].qualifier_offset;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(type);
command_open(&config);
length = command_payload_bytes(executed, type, 6U, 0x51U, 1U, 6001U,
command_value_cases[index].payload, command_value_cases[index].payload_length);
memcpy(selected, executed, length);
selected[qualifier] |= 0x80U;
command_raw_send(selected, length);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
if (variant == 0U) { executed[9] ^= type == 47U ? 3U : 1U; }
else { executed[qualifier] ^= type == 47U ? 4U : 1U; }
command_raw_send(executed, length);
command_flush();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH);
command_check_value(&finished.request, type);
CHECK(command_action_calls == 0U);
command_raw_reply_at(1U, executed, length, 0x47U);
command_empty();
command_close();
}
}
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t selected[32], executed[32];
static const uint8_t plus_zero[] = {0U, 0U, 0U, 0U, 0x80U};
static const uint8_t minus_zero[] = {0U, 0U, 0U, 0x80U, 0U};
size_t length;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(50U);
command_open(&config);
length = command_payload_bytes(selected, 50U, 6U, 0x51U, 1U, 6001U, plus_zero, sizeof(plus_zero));
command_raw_send(selected, length);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
CHECK(command_payload_bytes(executed, 50U, 6U, 0x51U, 1U, 6001U, minus_zero, sizeof(minus_zero)) == length);
command_raw_send(executed, length);
command_flush();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH && command_action_calls == 0U);
command_raw_reply_at(1U, executed, length, 0x47U);
command_empty();
command_close();
}
++command_checks;
}
/* RCS保留状态和浮点NaN/Inf必须否定,完整非法值原样回显,不经合法对象重编码。 */
static void test_command_extended_invalid_values(void)
{
static const uint8_t values[][5] = {
{0U, 0U, 0U, 0U, 0U}, {3U, 0U, 0U, 0U, 0U},
{0U, 0U, 0x80U, 0x7fU, 0x55U}, {0U, 0U, 0x80U, 0xffU, 0x55U},
{0U, 0U, 0xc0U, 0x7fU, 0x55U}, {1U, 0U, 0x80U, 0x7fU, 0x55U}
};
size_t index;
for (index = 0U; index < sizeof(values) / sizeof(values[0]); ++index) {
IEC104_CommandConfig_t config;
uint8_t bytes[32];
size_t length;
unsigned type = index < 2U ? 47U : 50U;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(type);
command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
command_open(&config);
length = command_payload_bytes(bytes, type, 6U, 0x61U, 1U, 6001U, values[index], index < 2U ? 1U : 5U);
command_raw_send(bytes, length);
command_flush();
CHECK(command_frame_count() == 1U && command_action_calls == 0U);
command_raw_reply_at(0U, bytes, length, 0x47U);
CHECK(command_status.rejected == 1U && !command_status.busy);
command_empty();
command_close();
}
++command_checks;
}
/* 每种新增长度都检查COT/CA/IOA优先级、SQ/尾部规范回复和缺失完整对象时丢弃。
* TEST/P/N只改变应答原因及标志,不得改变原命令值或启动动作。 */
static void test_command_extended_wire_validation(void)
{
size_t index;
unsigned variant;
for (index = 0U; index < sizeof(command_value_cases) / sizeof(command_value_cases[0]); ++index) {
for (variant = 0U; variant < 10U; ++variant) {
IEC104_CommandConfig_t config;
uint8_t bytes[32];
size_t length, received;
unsigned expected = 0x47U;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(command_value_cases[index].type);
command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
command_open(&config);
length = command_payload_bytes(bytes, command_value_cases[index].type, 6U, 0x62U, 1U, 6001U,
command_value_cases[index].payload, command_value_cases[index].payload_length);
received = length;
switch (variant) {
case 0U: bytes[2] = 5U; bytes[4] = 2U; bytes[6] = bytes[7] = bytes[8] = 0U; expected = 0x6dU; break;
case 1U: bytes[4] = 2U; bytes[6] = bytes[7] = bytes[8] = 0U; expected = 0x6eU; break;
case 2U: bytes[6] = bytes[7] = bytes[8] = 0U; expected = 0x6fU; break;
case 3U: bytes[1] = 0x81U; break;
case 4U: bytes[received++] = 0xa5U; break;
case 5U: bytes[2] |= 0x40U; break;
case 6U: bytes[2] |= 0x80U; expected = 0xc7U; break;
case 7U: --received; expected = 0U; break;
case 8U: bytes[1] = 0U; expected = 0U; break;
default:
memcpy(bytes + length, bytes + 6U, length - 6U);
received += length - 6U;
bytes[1] = 2U;
expected = 0U;
break;
}
command_raw_send(bytes, received);
command_flush();
CHECK(command_frame_count() == (expected == 0U ? 0U : 1U));
if (expected != 0U) { command_raw_reply_at(0U, bytes, length, expected); }
CHECK(command_action_calls == 0U && !command_status.busy);
command_empty();
command_close();
}
}
++command_checks;
}
/* 长请求被另一类型同点执行终结时,回复长度属于新请求,FINISHED仍属于原选择。
* 等待发送期间请求、事务回复及独立回复池同时持有不同长度的原文。 */
static void test_command_extended_reply_lengths(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t selected[32], executed[32], short_reply[32];
static const uint8_t selected_payload[] = {0U, 0U, 0xa0U, 0xbfU, 0x91U};
static const uint8_t executed_payload[] = {0U, 0x80U, 0x7fU};
static const uint8_t short_payload[] = {0x81U};
size_t selected_length, executed_length, short_length;
command_config_default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(49U) | IEC104_COMMAND_TYPE_BIT(50U);
command_config_mode(&config, IEC104_COMMAND_MODE_SELECT_EXECUTE);
command_open(&config);
selected_length = command_payload_bytes(selected, 50U, 6U, 0x51U, 1U, 6001U,
selected_payload, sizeof(selected_payload));
command_raw_send(selected, selected_length);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
command_congest();
short_length = command_payload_bytes(short_reply, 45U, 6U, 0x61U, 1U, 6002U,
short_payload, sizeof(short_payload));
command_raw_send(short_reply, short_length);
executed_length = command_payload_bytes(executed, 49U, 6U, 0x51U, 1U, 6001U,
executed_payload, sizeof(executed_payload));
command_raw_send(executed, executed_length);
command_process();
CHECK(command_status.pending_replies == 2U);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH);
CHECK(finished.request.type_id == 50U && finished.request.value.short_float == -1.25f);
CHECK(command_action_calls == 0U);
fake.send_blocked = 0U;
command_flush();
command_raw_reply_at(0U, selected, selected_length, 7U);
command_raw_reply_at(1U, short_reply, short_length, 0x47U);
command_raw_reply_at(2U, executed, executed_length, 0x47U);
CHECK(command_frame_count() == 3U && !command_status.busy);
command_empty();
command_close();
++command_checks;
}
/* UTC 2026-01-01 00:00:00.000;日期和期望纪元毫秒独立固定,不用被测转换器算期望。 */
static const uint64_t command_epoch_ms = UINT64_C(1767225600000);
static const uint8_t command_epoch_time[7] = {0U, 0U, 0U, 0U, 1U, 1U, 26U};
static const struct {
unsigned type;
size_t payload_length;
unsigned qualifier_offset;
uint8_t payload[5];
} command_timed_cases[] = {
{58U, 1U, 0U, {0x0dU, 0U, 0U, 0U, 0U}},
{59U, 1U, 0U, {0x0eU, 0U, 0U, 0U, 0U}},
{60U, 1U, 0U, {0x0dU, 0U, 0U, 0U, 0U}},
{61U, 3U, 2U, {0xc7U, 0xcfU, 0x25U, 0U, 0U}},
{62U, 3U, 2U, {0U, 0x80U, 0x7fU, 0U, 0U}},
{63U, 5U, 4U, {0U, 0U, 0xa0U, 0xbfU, 0x11U}},
{64U, 4U, 255U, {0xd5U, 0xaaU, 0x80U, 0xffU, 0U}}
};
static size_t command_timed_bytes(uint8_t *bytes, unsigned type, unsigned cause,
unsigned oa, unsigned ca, unsigned ioa, const uint8_t *value, size_t value_length,
const uint8_t time_bytes[7])
{
uint8_t payload[12];
CHECK(value_length >= 1U && value_length <= 5U);
memcpy(payload, value, value_length);
memcpy(payload + value_length, time_bytes, 7U);
return command_payload_bytes(bytes, type, cause, oa, ca, ioa, payload, value_length + 7U);
}
static void command_timed_config(IEC104_CommandConfig_t *config, unsigned type, uint32_t maximum)
{
IEC104_Command_Config_Default(config);
config->type_mask = IEC104_COMMAND_TYPE_BIT(type);
command_config_mode(config, IEC104_COMMAND_MODE_DIRECT);
config->time_tag_max_delay_ms = maximum;
config->master_utc_offset_minutes = 0;
}
/* 七类时标命令分别验证完整规范长度和字段;选择/执行时标不同仍匹配。
* max=0不依赖基准;执行使用的值和时标不能被应用修改的事件副本替换。 */
static void test_command_timed_flows(void)
{
size_t index;
for (index = 0U; index < sizeof(command_timed_cases) / sizeof(command_timed_cases[0]); ++index) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t selected[32], executed[32], time_bytes[7];
size_t length;
unsigned type = command_timed_cases[index].type;
unsigned preceding = type == 64U ? 0U : 1U;
command_timed_config(&config, type, 0U);
if (type != 64U) { command_config_mode(&config, IEC104_COMMAND_MODE_SELECT_EXECUTE); }
command_open(&config);
length = command_timed_bytes(selected, type, 6U, 0x51U, 1U, 6001U,
command_timed_cases[index].payload, command_timed_cases[index].payload_length, command_epoch_time);
CHECK(length == (type <= 60U ? 17U : (type <= 62U ? 19U : (type == 63U ? 21U : 20U))));
if (type != 64U) {
selected[9U + command_timed_cases[index].qualifier_offset] |= 0x80U;
command_raw_send(selected, length);
event = command_take(IEC104_COMMAND_SELECT);
command_check_value(&event.request, type);
CHECK(event.request.has_time && event.request.time_utc_ms == command_epoch_ms);
CHECK(event.request.is_select && !event.request.selected);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
command_raw_reply_at(0U, selected, length, 7U);
}
memcpy(time_bytes, command_epoch_time, sizeof(time_bytes));
time_bytes[0] = 0xd2U; time_bytes[1] = 4U;
CHECK(command_timed_bytes(executed, type, 6U, 0x51U, 1U, 6001U,
command_timed_cases[index].payload, command_timed_cases[index].payload_length, time_bytes) == length);
command_raw_send(executed, length);
event = command_take(IEC104_COMMAND_EXECUTE);
command_check_value(&event.request, type);
CHECK(event.request.has_time && event.request.time_utc_ms == command_epoch_ms + 1234U);
CHECK(event.request.time.time.second == 1U && event.request.time.time.ms == 234U);
CHECK(event.request.selected == (type != 64U) && !event.request.is_select);
event.request.value.bitstring = 0U;
event.request.state = 0U;
event.request.time_utc_ms = 0U;
memset(&event.request.time, 0, sizeof(event.request.time));
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
command_check_value(&command_action_request, type);
CHECK(command_action_calls == 1U && command_action_request.time_utc_ms == command_epoch_ms + 1234U);
finished = command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_check_value(&finished.request, type);
CHECK(finished.request.has_time && finished.request.time_utc_ms == command_epoch_ms + 1234U);
command_raw_reply_at(preceding, executed, length, 7U);
command_raw_reply_at(preceding + 1U, executed, length, 10U);
CHECK(command_frame_count() == preceding + 2U);
command_close();
}
/* 相同语义值不能跨类型执行:49->48、45->58;最长命令还单独验证OA不匹配。 */
for (index = 0U; index < 3U; ++index) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t selected[32], executed[32];
static const uint8_t scaled[] = {0U, 0x80U, 0x7fU};
static const uint8_t single[] = {1U};
static const uint8_t real_value[] = {0U, 0U, 0xa0U, 0xbfU, 0x11U};
unsigned select_type = index == 0U ? 49U : (index == 1U ? 45U : 63U);
unsigned execute_type = index == 0U ? 48U : (index == 1U ? 58U : 63U);
size_t selected_length, executed_length;
command_timed_config(&config, select_type, 0U);
config.type_mask |= IEC104_COMMAND_TYPE_BIT(execute_type);
command_config_mode(&config, IEC104_COMMAND_MODE_SELECT_EXECUTE);
command_open(&config);
if (index == 0U) {
selected_length = command_payload_bytes(selected, select_type, 6U, 0x51U, 1U, 6001U,
scaled, sizeof(scaled));
selected[11] |= 0x80U;
executed_length = command_payload_bytes(executed, execute_type, 6U, 0x51U, 1U, 6001U,
scaled, sizeof(scaled));
} else if (index == 1U) {
selected_length = command_payload_bytes(selected, select_type, 6U, 0x51U, 1U, 6001U,
single, sizeof(single));
selected[9] |= 0x80U;
executed_length = command_timed_bytes(executed, execute_type, 6U, 0x51U, 1U, 6001U,
single, sizeof(single), command_epoch_time);
} else {
selected_length = command_timed_bytes(selected, select_type, 6U, 0x51U, 1U, 6001U,
real_value, sizeof(real_value), command_epoch_time);
selected[13] |= 0x80U;
executed_length = command_timed_bytes(executed, execute_type, 6U, 0x52U, 1U, 6001U,
real_value, sizeof(real_value), command_epoch_time);
}
command_raw_send(selected, selected_length);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
command_raw_send(executed, executed_length);
command_flush();
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH);
CHECK(finished.request.type_id == select_type && finished.request.originator_address == 0x51U);
CHECK(command_action_calls == 0U && command_frame_count() == 2U);
command_raw_reply_at(0U, selected, selected_length, 7U);
command_raw_reply_at(1U, executed, executed_length, 0x47U);
command_empty();
command_close();
}
++command_checks;
}
/* 原始时标带SU时只扣一次夏令时,并使用显式主站标准时区;保留毫秒和原始SU。 */
static void test_command_time_conversion(void)
{
static const struct { int offset; uint8_t bytes[7]; unsigned summer; } cases[] = {
{0, {0xd2U, 4U, 0U, 0U, 1U, 1U, 26U}, 0U},
{480, {0xd2U, 4U, 0U, 8U, 1U, 1U, 26U}, 0U},
{480, {0xd2U, 4U, 0U, 0x89U, 1U, 1U, 26U}, 1U},
{0, {0xd2U, 4U, 0U, 0x81U, 1U, 1U, 26U}, 1U},
{-330, {0xd2U, 4U, 30U, 18U, 31U, 12U, 25U}, 0U}
};
static const uint8_t value[] = {1U};
size_t index;
for (index = 0U; index < sizeof(cases) / sizeof(cases[0]); ++index) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
uint8_t bytes[32];
size_t length;
command_timed_config(&config, 58U, 0U);
config.master_utc_offset_minutes = (int16_t)cases[index].offset;
command_open(&config);
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U, value, sizeof(value), cases[index].bytes);
command_raw_send(bytes, length);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(event.request.has_time && event.request.time_utc_ms == command_epoch_ms + 1234U);
CHECK(event.request.time.time.ms == 234U && event.request.time.is_summer_time == cases[index].summer);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
(void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_raw_reply_at(0U, bytes, length, 7U);
command_raw_reply_at(1U, bytes, length, 10U);
command_close();
}
++command_checks;
}
/* 关闭新鲜度检查仍拒绝IV、非法CP56和UTC越界,且保留完整非法原文用于否定回显。 */
static void test_command_timed_invalid_values(void)
{
size_t index;
unsigned variant;
for (index = 0U; index < sizeof(command_timed_cases) / sizeof(command_timed_cases[0]); ++index) {
for (variant = 0U; variant < 10U; ++variant) {
IEC104_CommandConfig_t config;
uint8_t bytes[32], time_bytes[7];
size_t length;
command_timed_config(&config, command_timed_cases[index].type, 0U);
memcpy(time_bytes, command_epoch_time, sizeof(time_bytes));
switch (variant) {
case 0U: time_bytes[2] = 0x80U; break;
case 1U: time_bytes[2] = 0x40U; break;
case 2U: time_bytes[3] = 0x20U; break;
case 3U: time_bytes[5] = 0x11U; break;
case 4U: time_bytes[6] |= 0x80U; break;
case 5U: time_bytes[0] = 0x60U; time_bytes[1] = 0xeaU; break;
case 6U: time_bytes[4] = 30U; time_bytes[5] = 2U; break;
case 7U: time_bytes[4] = 0x21U; break;
case 8U: time_bytes[6] = 100U; break;
default:
time_bytes[0] = 0x5fU; time_bytes[1] = 0xeaU;
time_bytes[2] = 59U; time_bytes[3] = 23U;
time_bytes[4] = 31U; time_bytes[5] = 12U; time_bytes[6] = 99U;
config.master_utc_offset_minutes = -1;
break;
}
command_open(&config);
length = command_timed_bytes(bytes, command_timed_cases[index].type, 6U, 0x61U, 1U, 6001U,
command_timed_cases[index].payload, command_timed_cases[index].payload_length, time_bytes);
command_raw_send(bytes, length);
command_flush();
CHECK(command_frame_count() == 1U && command_action_calls == 0U);
command_raw_reply_at(0U, bytes, length, 0x47U);
CHECK(command_status.rejected == 1U && !command_status.busy);
CHECK(command_status.time_rejected == (variant == 0U || variant == 9U ? 1U : 0U));
command_empty();
command_close();
}
}
/* 时标类型也必须复用SCO/DCS/RCS及浮点值校验,而非只验证CP56。 */
for (variant = 0U; variant < 6U; ++variant) {
IEC104_CommandConfig_t config;
uint8_t bytes[32], payload[5] = {0U, 0U, 0x80U, 0x7fU, 0U};
unsigned type = variant < 3U ? 58U + variant : 63U;
size_t payload_length = type == 63U ? 5U : 1U;
size_t length;
if (variant == 0U) { payload[0] = 2U; }
else if (variant == 1U) { payload[0] = 0U; }
else if (variant == 2U) { payload[0] = 3U; }
else if (variant == 4U) { payload[3] = 0xffU; }
else if (variant == 5U) { payload[2] = 0xc0U; }
command_timed_config(&config, type, 0U);
command_open(&config);
length = command_timed_bytes(bytes, type, 6U, 0x61U, 1U, 6001U, payload, payload_length, command_epoch_time);
command_raw_send(bytes, length);
command_flush();
command_raw_reply_at(0U, bytes, length, 0x47U);
CHECK(command_frame_count() == 1U && command_action_calls == 0U && !command_status.busy);
command_empty();
command_close();
}
++command_checks;
}
/* 过去和未来采用同一严格窗口:差值等于上限即不接受;过去默认静默,未来总是否定。 */
static void test_command_time_boundaries(void)
{
unsigned future, negative, boundary;
static const uint8_t value[] = {1U};
for (future = 0U; future < 2U; ++future) {
for (negative = 0U; negative < 2U; ++negative) {
for (boundary = 0U; boundary < 3U; ++boundary) {
IEC104_CommandConfig_t config;
uint8_t bytes[32];
size_t length;
uint64_t difference = 999U + boundary;
command_timed_config(&config, 58U, 1000U);
config.negative_on_expired = negative != 0U;
command_open(&config);
CHECK(IEC104_Command_Set_Time_Base(command,
future ? command_epoch_ms - difference : command_epoch_ms + difference, fake.now));
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U,
value, sizeof(value), command_epoch_time);
command_raw_send(bytes, length);
command_flush();
CHECK(command_status.time_base_valid && command_status.dropped_requests == 0U);
if (boundary == 0U) {
IEC104_CommandEvent_t event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(event.request.time_utc_ms == command_epoch_ms);
CHECK(command_status.time_expired == 0U && command_status.time_rejected == 0U);
CHECK(command_status.rejected == 0U && command_frame_count() == 0U);
} else {
CHECK(command_status.time_expired == (future ? 0U : 1U));
CHECK(command_status.time_rejected == (future ? 1U : 0U));
CHECK(command_status.rejected == (future || negative ? 1U : 0U));
CHECK(command_frame_count() == (future || negative ? 1U : 0U));
if (future || negative) { command_raw_reply_at(0U, bytes, length, 0x47U); }
command_empty();
}
CHECK(command_action_calls == 0U);
command_close();
}
}
}
++command_checks;
}
/* 基准缺失、单调时钟倒退和推算UTC越界都明确否定;不能通过无符号回绕变成新鲜请求。 */
static void test_command_time_base_errors(void)
{
unsigned variant;
static const uint8_t value[] = {1U};
const uint64_t maximum = (uint64_t)IEC60870_TIME_MAX_SECONDS * 1000U + 999U;
for (variant = 0U; variant < 4U; ++variant) {
IEC104_CommandConfig_t config;
uint8_t bytes[32];
size_t length;
command_timed_config(&config, 58U, 1000U);
command_open(&config);
if (variant == 1U) {
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
} else if (variant == 2U) {
CHECK(IEC104_Command_Set_Time_Base(command, maximum, fake.now));
++fake.now;
} else if (variant == 3U) {
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
IEC104_Command_Clear_Time_Base(command);
}
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U,
value, sizeof(value), command_epoch_time);
if (variant == 1U) {
/* 在网络已收帧、业务尚未消费通知之间注入回退,避免传输层先处理同一个时钟故障。 */
command_queue(bytes, length);
CHECK(IEC104_Slave_Poll(command_slave, 0U, NULL) == IEC104_TCP_OK);
--fake.now;
command_drain_transport();
++fake.now;
} else { command_raw_send(bytes, length); }
command_flush();
if (variant != 1U) { command_raw_reply_at(0U, bytes, length, 0x47U); }
CHECK(command_status.time_rejected == 1U && command_status.time_expired == 0U);
CHECK(command_status.rejected == 1U && command_status.dropped_requests == 0U);
CHECK(command_status.clock_errors == (variant == 1U ? 1U : 0U));
CHECK(command_status.time_base_valid == (variant == 1U || variant == 2U));
/* 回退同样会被发送路径的Net期限检查发现并收尾,不能要求故障连接仍成功写出否定。 */
CHECK(command_action_calls == 0U && command_frame_count() == (variant == 1U ? 0U : 1U));
command_empty();
command_close();
}
++command_checks;
}
/* 无效Set保持原基准,基准UTC上下限可表示;受理后Clear/Set不追溯改变工作事件。 */
static void test_command_time_base_api(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
uint8_t bytes[32];
size_t length;
static const uint8_t value[] = {1U};
const uint64_t maximum = (uint64_t)IEC60870_TIME_MAX_SECONDS * 1000U + 999U;
command_timed_config(&config, 58U, 1000U);
command_open(&config);
CHECK(!IEC104_Command_Set_Time_Base(NULL, command_epoch_ms, fake.now));
IEC104_Command_Clear_Time_Base(NULL);
CHECK(IEC104_Command_Set_Time_Base(command, 0U, fake.now));
CHECK(IEC104_Command_Set_Time_Base(command, maximum, fake.now));
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms - 100U, fake.now - 100U));
CHECK(!IEC104_Command_Set_Time_Base(command, maximum + 1U, fake.now));
CHECK(!IEC104_Command_Set_Time_Base(command, 0U, fake.now + 1U));
CHECK(!IEC104_Command_Set_Time_Base(command, UINT64_MAX, fake.now));
CHECK(!IEC104_Command_Set_Time_Base(command, 0U, UINT64_MAX));
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U, value, sizeof(value), command_epoch_time);
command_raw_send(bytes, length);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(event.request.time_utc_ms == command_epoch_ms);
IEC104_Command_Clear_Time_Base(command);
CHECK(IEC104_Command_Get_Status(command, &command_status) && !command_status.time_base_valid);
CHECK(IEC104_Command_Set_Time_Base(command, 0U, fake.now));
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
CHECK(command_action_calls == 1U && command_action_request.time_utc_ms == command_epoch_ms);
(void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_status.time_rejected == 0U && command_status.time_expired == 0U);
command_raw_reply_at(0U, bytes, length, 7U);
command_raw_reply_at(1U, bytes, length, 10U);
command_close();
++command_checks;
}
/* STOP和断线只结束连接事务,可信时间基准继续有效;停用及离线时仍可显式更新。 */
static void test_command_time_base_lifecycle(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
uint8_t bytes[32];
size_t length;
static const uint8_t value[] = {1U};
command_timed_config(&config, 58U, 1000U);
command_open(&config);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
push(stop_act, sizeof(stop_act));
command_flush();
CHECK(!command_status.active && command_status.time_base_valid);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
push(start_act, sizeof(start_act));
command_flush();
CHECK(command_status.active && command_status.time_base_valid);
fake.recv_eof = 1U;
command_flush();
CHECK(!command_status.active && command_status.time_base_valid);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
fake.recv_eof = 0U;
command_connect();
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U, value, sizeof(value), command_epoch_time);
command_raw_send(bytes, length);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(event.request.time_utc_ms == command_epoch_ms);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
(void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_status.time_base_valid);
IEC104_Command_Clear_Time_Base(command);
CHECK(IEC104_Command_Get_Status(command, &command_status) && !command_status.time_base_valid);
command_close();
++command_checks;
}
/* 公共类型支持、时区配置边界和无S/E选择限制均在Create时校验。 */
static void test_command_time_configuration(void)
{
static const IEC104_CommandPoint_t invalid = {1U, 6001U,
IEC104_COMMAND_TYPE_BIT(64U), IEC104_COMMAND_MODE_SELECT_EXECUTE};
IEC104_CommandConfig_t config;
unsigned type;
command_open(NULL);
CHECK(IEC104_Command_Destroy(command));
for (type = 58U; type <= 64U; ++type) {
command_timed_config(&config, type, 1000U);
config.master_utc_offset_minutes = type % 2U == 0U ? 1439 : -1439;
command = IEC104_Command_Create(command_slave, &config);
CHECK(command != NULL && IEC104_Command_Destroy(command));
}
config.points = &invalid;
config.point_count = 1U;
CHECK(IEC104_Command_Create(command_slave, &config) == NULL);
command_config_mode(&config, IEC104_COMMAND_MODE_DIRECT);
config.master_utc_offset_minutes = 1440;
CHECK(IEC104_Command_Create(command_slave, &config) == NULL);
config.master_utc_offset_minutes = -1440;
CHECK(IEC104_Command_Create(command_slave, &config) == NULL);
command_config_default(&config);
CHECK(config.type_mask == UINT16_C(0x3fff));
command = IEC104_Command_Create(command_slave, &config);
CHECK(command != NULL);
command_close();
++command_checks;
}
/* 决定期同点重复先于时效,格式错误仍否定;其他点过期先于busy,撤销不要求新鲜时标或基准。 */
static void test_command_time_ordering(void)
{
unsigned selecting;
for (selecting = 0U; selecting < 2U; ++selecting) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t bytes[32], changed[32], payload[1];
size_t length;
command_timed_config(&config, 58U, 1000U);
if (selecting) { command_config_mode(&config, IEC104_COMMAND_MODE_SELECT_EXECUTE); }
command_open(&config);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
payload[0] = selecting ? 0x81U : 1U;
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U,
payload, sizeof(payload), command_epoch_time);
command_raw_send(bytes, length);
event = command_take(selecting ? IEC104_COMMAND_SELECT : IEC104_COMMAND_EXECUTE);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms + 1000U, fake.now));
memcpy(changed, bytes, length);
changed[3] = 0x62U;
command_raw_send(changed, length);
command_flush();
CHECK(command_status.dropped_requests == 1U && command_status.time_expired == 0U);
CHECK(command_status.time_rejected == 0U && command_frame_count() == 0U);
changed[12] |= 0x40U;
command_raw_send(changed, length);
command_flush();
command_raw_reply_at(0U, changed, length, 0x47U);
CHECK(command_status.rejected == 1U && command_status.dropped_requests == 1U);
memcpy(changed, bytes, length);
changed[6] += 1U;
command_raw_send(changed, length);
command_flush();
CHECK(command_status.time_expired == 1U && command_status.busy_rejected == 0U);
CHECK(command_status.rejected == 1U && command_frame_count() == 1U);
IEC104_Command_Clear_Time_Base(command);
memcpy(changed, bytes, length);
changed[2] = 8U;
changed[9] = 0U;
changed[length - 1U] = 25U;
command_raw_send(changed, length);
command_flush();
command_raw_reply_at(1U, changed, length, 9U);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED);
CHECK(finished.request.receive_serial == event.request.receive_serial);
CHECK(finished.request.time_utc_ms == command_epoch_ms);
CHECK(command_status.time_rejected == 0U && command_action_calls == 0U);
command_empty();
command_close();
}
/* 过期执行不破坏原选择;分别验证恢复基准后执行,以及最终到选择保持期限终结。 */
for (selecting = 0U; selecting < 2U; ++selecting) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
IEC104_CommandTransactionInfo_t info;
uint8_t bytes[32];
static const uint8_t payload[] = {0x81U};
size_t length;
command_timed_config(&config, 58U, 1000U);
command_config_mode(&config, IEC104_COMMAND_MODE_SELECT_EXECUTE);
command_open(&config);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U,
payload, sizeof(payload), command_epoch_time);
command_raw_send(bytes, length);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms + 1000U, fake.now));
bytes[9] = 1U;
command_raw_send(bytes, length);
command_flush();
command_empty();
CHECK(IEC104_Command_Get_Transaction(command, &event, &info));
CHECK(info.active && info.phase == IEC104_COMMAND_PHASE_SELECTED);
CHECK(command_status.time_expired == 1U && command_frame_count() == 1U);
if (selecting == 0U) {
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
command_raw_send(bytes, length);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
(void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_action_calls == 1U && command_status.time_expired == 1U);
} else {
fake.now += config.select_timeout_ms;
command_flush();
event = command_take(IEC104_COMMAND_FINISHED);
CHECK(event.reason == IEC104_COMMAND_FINISH_SELECT_TIMEOUT);
CHECK(command_status.select_timeouts == 1U && command_status.time_expired == 1U);
CHECK(command_action_calls == 0U && command_frame_count() == 1U);
command_empty();
}
command_close();
}
++command_checks;
}
static IEC104_CommandActionResult_t command_time_reentry_action(void *context,
const IEC104_CommandRequest_t *request)
{
IEC104_CommandStatus_t status;
CHECK(!IEC104_Command_Set_Time_Base(command, 0U, fake.now));
IEC104_Command_Clear_Time_Base(command);
CHECK(IEC104_Command_Get_Status(command, &status) && status.time_base_valid);
return command_reentry_action(context, request);
}
/* 时基修改同样受动作回调保护;回调返回后再收请求,证明旧基准没有被替换或清除。 */
static void test_command_time_base_reentry(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
uint8_t bytes[32];
static const uint8_t payload[] = {1U};
size_t length;
command_timed_config(&config, 58U, 1000U);
command_open(&config);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6001U,
payload, sizeof(payload), command_epoch_time);
command_raw_send(bytes, length);
command_reentry_event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(IEC104_Command_Accept(command, &command_reentry_event, command_time_reentry_action,
&command_action_calls) == IEC104_COMMAND_REPLY_OK);
(void)command_done(&command_reentry_event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_raw_send(bytes, length);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(event.request.time_utc_ms == command_epoch_ms);
CHECK(command_status.time_rejected == 0U && command_action_calls == 1U);
command_close();
++command_checks;
}
/* 完整21字节请求、17字节事务回复及独立短回复在WAIT期间各自保留长度。
* 独立槽先保存最长非法请求,再复用为短请求,不能截断时标或带出旧尾部。 */
static void test_command_timed_reply_lengths(void)
{
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
uint8_t selected[32], executed[32], rejected[32], short_reply[32];
static const uint8_t selected_payload[] = {0U, 0U, 0xa0U, 0xbfU, 0x91U};
static const uint8_t executed_payload[] = {1U};
static const uint8_t short_payload[] = {0x81U};
size_t selected_length, executed_length, short_length;
command_timed_config(&config, 63U, 0U);
config.type_mask |= IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(58U);
command_config_mode(&config, IEC104_COMMAND_MODE_SELECT_EXECUTE);
command_open(&config);
selected_length = command_timed_bytes(selected, 63U, 6U, 0x51U, 1U, 6001U,
selected_payload, sizeof(selected_payload), command_epoch_time);
command_raw_send(selected, selected_length);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
command_congest();
memcpy(rejected, selected, selected_length);
rejected[3] = 0x62U;
rejected[4] = 2U;
command_raw_send(rejected, selected_length);
executed_length = command_timed_bytes(executed, 58U, 6U, 0x51U, 1U, 6001U,
executed_payload, sizeof(executed_payload), command_epoch_time);
command_raw_send(executed, executed_length);
command_process();
CHECK(command_status.pending_replies == 2U);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH);
CHECK(finished.request.type_id == 63U && finished.request.value.short_float == -1.25f);
CHECK(finished.request.time_utc_ms == command_epoch_ms);
fake.send_blocked = 0U;
command_flush();
command_raw_reply_at(0U, selected, selected_length, 7U);
command_raw_reply_at(1U, rejected, selected_length, 0x6eU);
command_raw_reply_at(2U, executed, executed_length, 0x47U);
CHECK(command_frame_count() == 3U && !command_status.busy);
command_congest();
short_length = command_payload_bytes(short_reply, 45U, 6U, 0x63U, 2U, 6001U,
short_payload, sizeof(short_payload));
command_raw_send(short_reply, short_length);
command_process();
CHECK(command_status.pending_replies == 1U);
fake.send_blocked = 0U;
command_flush();
command_raw_reply_at(3U, short_reply, short_length, 0x6eU);
CHECK(command_frame_count() == 4U && command_action_calls == 0U);
command_empty();
command_close();
++command_checks;
}
/* 所有时标长度独立验证SQ/尾部规范化及截断/多对象丢弃;地址错误优先于基准缺失。 */
static void test_command_timed_wire_validation(void)
{
size_t index;
unsigned variant;
for (index = 0U; index < sizeof(command_timed_cases) / sizeof(command_timed_cases[0]); ++index) {
for (variant = 0U; variant < 8U; ++variant) {
IEC104_CommandConfig_t config;
uint8_t bytes[64];
size_t length, received;
unsigned expected = 0x47U;
command_timed_config(&config, command_timed_cases[index].type, 1000U);
command_open(&config);
length = command_timed_bytes(bytes, command_timed_cases[index].type, 6U, 0x61U, 1U, 6001U,
command_timed_cases[index].payload, command_timed_cases[index].payload_length, command_epoch_time);
received = length;
switch (variant) {
case 0U: bytes[1] = 0x81U; break;
case 1U: bytes[received++] = 0xa5U; break;
case 2U: --received; expected = 0U; break;
case 3U: bytes[1] = 0U; expected = 0U; break;
case 4U:
memcpy(bytes + length, bytes + 6U, length - 6U);
received += length - 6U;
bytes[1] = 2U;
expected = 0U;
break;
case 5U: bytes[2] = 5U; bytes[4] = 2U; expected = 0x6dU; break;
case 6U: bytes[4] = 2U; bytes[6] = bytes[7] = bytes[8] = 0U; expected = 0x6eU; break;
default: bytes[6] = bytes[7] = bytes[8] = 0U; expected = 0x6fU; break;
}
command_raw_send(bytes, received);
command_flush();
CHECK(command_frame_count() == (expected == 0U ? 0U : 1U));
if (expected != 0U) { command_raw_reply_at(0U, bytes, length, expected); }
CHECK(command_status.time_rejected == 0U && command_status.time_expired == 0U);
CHECK(command_action_calls == 0U && !command_status.busy);
command_empty();
command_close();
}
}
++command_checks;
}
/* 保持默认全局类型掩码,用显式测试点表逐一接入14类命令。
* 点表令51/64直接执行,其余先选择;时标类无基准时拒绝,有基准后完成事务。 */
static void test_command_default_all_types(void)
{
static const unsigned types[] = {45U, 46U, 47U, 48U, 49U, 50U, 51U,
58U, 59U, 60U, 61U, 62U, 63U, 64U};
static const uint8_t local_time[7] = {0U, 0U, 0U, 8U, 1U, 1U, 26U};
IEC104_CommandConfig_t defaults;
size_t index;
IEC104_Command_Config_Default(&defaults);
CHECK(defaults.type_mask == UINT16_C(0x3fff));
CHECK(defaults.points == NULL && defaults.point_count == 0U);
CHECK(defaults.time_tag_max_delay_ms == 10000U && defaults.master_utc_offset_minutes == 480);
CHECK(!defaults.negative_on_expired);
for (index = 0U; index < sizeof(types) / sizeof(types[0]); ++index) {
unsigned type = types[index];
unsigned qualifier, reply_index = 0U;
bool timed = type >= 58U;
bool direct = type == 51U || type == 64U;
IEC104_CommandEvent_t event, finished;
uint8_t selected[32], executed[32], simple[1];
const uint8_t *payload;
size_t payload_length, length;
if (type <= 46U) {
simple[0] = type == 45U ? 1U : 2U;
payload = simple;
payload_length = sizeof(simple);
qualifier = 0U;
} else if (!timed) {
payload = command_value_cases[type - 47U].payload;
payload_length = command_value_cases[type - 47U].payload_length;
qualifier = command_value_cases[type - 47U].qualifier_offset;
} else {
payload = command_timed_cases[type - 58U].payload;
payload_length = command_timed_cases[type - 58U].payload_length;
qualifier = command_timed_cases[type - 58U].qualifier_offset;
}
command_config_mode(&defaults, direct ? IEC104_COMMAND_MODE_DIRECT : IEC104_COMMAND_MODE_SELECT_EXECUTE);
command_open(&defaults);
length = timed ? command_timed_bytes(executed, type, 6U, 0x51U, 1U, 6001U,
payload, payload_length, local_time) : command_payload_bytes(executed, type, 6U,
0x51U, 1U, 6001U, payload, payload_length);
memcpy(selected, executed, length);
if (!direct) { selected[9U + qualifier] |= 0x80U; }
if (timed) {
command_raw_send(selected, length);
command_flush();
CHECK(command_status.time_rejected == 1U && command_status.accepted == 0U);
CHECK(command_action_calls == 0U && !command_status.busy);
command_raw_reply_at(reply_index++, selected, length, 0x47U);
command_empty();
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
}
if (!direct) {
command_raw_send(executed, length);
command_flush();
CHECK(command_status.accepted == 0U && command_action_calls == 0U);
command_raw_reply_at(reply_index++, executed, length, 0x47U);
command_empty();
command_raw_send(selected, length);
event = command_take(IEC104_COMMAND_SELECT);
CHECK(event.request.type_id == type && event.request.is_select);
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
command_raw_reply_at(reply_index++, selected, length, 7U);
}
command_raw_send(executed, length);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(event.request.type_id == type && !event.request.is_select);
CHECK(event.request.selected == !direct && event.request.has_time == timed);
if (type > 46U) { command_check_value(&event.request, type); }
else { CHECK(event.request.state == (type == 45U ? 1U : 2U)); }
if (timed) { CHECK(event.request.time_utc_ms == command_epoch_ms); }
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
finished = command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_action_calls == 1U && finished.request.type_id == type);
command_raw_reply_at(reply_index++, executed, length, 7U);
command_raw_reply_at(reply_index++, executed, length, 10U);
CHECK(command_frame_count() == reply_index);
command_close();
}
command_open(NULL);
command_send(105U, 6U, 0U, 1U, 0U, 1U);
CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED && command_frame_count() == 0U);
command_empty();
command_close();
++command_checks;
}
/* A raw default configuration has no points: all 14 enabled, valid types reach
* the unknown-IOA reply without publishing work or FINISHED. */
static void test_command_point_default_empty(void)
{
static const unsigned types[] = {45U, 46U, 47U, 48U, 49U, 50U, 51U,
58U, 59U, 60U, 61U, 62U, 63U, 64U};
static const uint8_t local_time[7] = {0U, 0U, 0U, 8U, 1U, 1U, 26U};
size_t index;
for (index = 0U; index < sizeof(types) / sizeof(types[0]); ++index) {
IEC104_CommandConfig_t config;
uint8_t bytes[32], simple[1];
const uint8_t *payload;
size_t payload_length, length;
unsigned type = types[index];
IEC104_Command_Config_Default(&config);
CHECK(config.type_mask == UINT16_C(0x3fff));
CHECK(config.points == NULL && config.point_count == 0U);
command_open(&config);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
if (type <= 46U) {
simple[0] = type == 45U ? 1U : 2U;
payload = simple; payload_length = sizeof(simple);
} else if (type < 58U) {
payload = command_value_cases[type - 47U].payload;
payload_length = command_value_cases[type - 47U].payload_length;
} else {
payload = command_timed_cases[type - 58U].payload;
payload_length = command_timed_cases[type - 58U].payload_length;
}
length = type >= 58U ? command_timed_bytes(bytes, type, 6U, 0x51U, 1U, 6001U,
payload, payload_length, local_time) : command_payload_bytes(bytes, type, 6U,
0x51U, 1U, 6001U, payload, payload_length);
command_raw_send(bytes, length); command_flush();
CHECK(command_last_handled == IEC104_COMMAND_EVENT_HANDLED);
CHECK(command_status.rejected == 1U && command_status.accepted == 0U);
CHECK(!command_status.busy && command_action_calls == 0U);
CHECK(command_frame_count() == 1U);
command_raw_reply_at(0U, bytes, length, 0x6fU);
command_empty(); command_close();
}
++command_checks;
}
/* Validate the entire borrowed table before binding, including entries whose
* type bits are currently disabled globally. Mutations occur only unbound. */
static void test_command_point_configuration(void)
{
static const IEC104_CommandPoint_t valid[] = {
{1U, 1U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{1U, UINT32_C(0xffffff), IEC104_COMMAND_TYPE_BIT(51U), IEC104_COMMAND_MODE_DIRECT},
{65534U, UINT32_C(0xffffff), IEC104_COMMAND_TYPE_BIT(64U), IEC104_COMMAND_MODE_DIRECT}
};
IEC104_CommandConfig_t config;
IEC104_CommandPoint_t points[3];
unsigned variant;
command_open(NULL);
CHECK(IEC104_Command_Destroy(command));
for (variant = 0U; variant < 17U; ++variant) {
IEC104_Command_Config_Default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(45U);
memcpy(points, valid, sizeof(points));
config.points = points; config.point_count = 3U;
switch (variant) {
case 0U: points[0].common_address = 0U; break;
case 1U: points[2].common_address = 65535U; break;
case 2U: points[0].ioa = 0U; break;
case 3U: points[2].ioa = UINT32_C(0x1000000); break;
case 4U: points[0].type_mask = 0U; break;
case 5U: points[0].type_mask |= UINT16_C(0x4000); break;
case 6U: points[2].type_mask |= UINT16_C(0x8000); break;
case 7U: points[0].mode = (IEC104_CommandMode_t)0; break;
case 8U: points[2].mode = (IEC104_CommandMode_t)3; break;
case 9U: points[1].ioa = points[0].ioa; break;
case 10U: points[0].ioa = 2U; points[1].ioa = 1U; break;
case 11U: points[0].common_address = 2U; break;
case 12U: points[1].mode = IEC104_COMMAND_MODE_SELECT_EXECUTE; break;
case 13U: points[2].mode = IEC104_COMMAND_MODE_SELECT_EXECUTE; break;
case 14U: config.points = NULL; break;
case 15U: config.point_count = 0U; break;
default: config.point_count = SIZE_MAX; break;
}
CHECK(IEC104_Command_Create(command_slave, &config) == NULL);
}
IEC104_Command_Config_Default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(45U);
config.points = valid; config.point_count = sizeof(valid) / sizeof(valid[0]);
command = IEC104_Command_Create(command_slave, &config);
CHECK(command != NULL);
command_close();
++command_checks;
}
/* The same type can have different modes at two IOAs or at equal IOAs in two
* stations. Policy rejections never borrow a transaction or call application. */
static void test_command_point_modes(void)
{
static const IEC104_CommandPoint_t points[] = {
{1U, 6001U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_DIRECT},
{1U, 6002U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{1U, 6003U, IEC104_COMMAND_TYPE_BIT(46U), IEC104_COMMAND_MODE_DIRECT},
{2U, 6001U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{2U, 6002U, IEC104_COMMAND_TYPE_BIT(46U), IEC104_COMMAND_MODE_DIRECT}
};
static const struct { unsigned ca, ioa, value, event, cause; } cases[] = {
{1U, 6001U, 0x81U, 0U, 0x47U}, {1U, 6001U, 1U, IEC104_COMMAND_EXECUTE, 7U},
{1U, 6002U, 1U, 0U, 0x47U}, {1U, 6002U, 0x81U, IEC104_COMMAND_SELECT, 7U},
{2U, 6001U, 1U, 0U, 0x47U}, {2U, 6001U, 0x81U, IEC104_COMMAND_SELECT, 7U},
{1U, 6003U, 1U, 0U, 0x47U}, {2U, 6002U, 0x81U, 0U, 0x47U},
{1U, 6004U, 1U, 0U, 0x6fU}, {2U, 6003U, 0x81U, 0U, 0x6fU}
};
size_t index;
for (index = 0U; index < sizeof(cases) / sizeof(cases[0]); ++index) {
IEC104_CommandConfig_t config;
IEC104_Station_t other = {0};
IEC104_CommandEvent_t event;
IEC104_Command_Config_Default(&config);
config.points = points; config.point_count = sizeof(points) / sizeof(points[0]);
command_open(&config);
CHECK(IEC104_Slave_Add_Station(command_slave, &other, 2U, &other) == IEC104_STATION_OK);
/* Config is copied; the original const table remains alive and unchanged. */
config.points = NULL; config.point_count = 0U; config.type_mask = 0U;
command_send(45U, 6U, 0x51U, cases[index].ca, cases[index].ioa, cases[index].value);
if (cases[index].event != 0U) {
event = command_take((IEC104_CommandEventType_t)cases[index].event);
CHECK(event.request.common_address == cases[index].ca && event.request.ioa == cases[index].ioa);
CHECK(!event.request.selected);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
if (event.type == IEC104_COMMAND_EXECUTE) { (void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED); }
else {
command_flush(); IEC104_Command_Cancel(command);
CHECK(command_take(IEC104_COMMAND_FINISHED).reason == IEC104_COMMAND_FINISH_CANCELLED);
}
CHECK(command_action_calls == 1U);
} else {
command_flush(); command_empty();
CHECK(command_status.rejected == 1U && command_status.accepted == 0U);
CHECK(!command_status.busy && command_status.busy_rejected == 0U && command_action_calls == 0U);
CHECK(command_frame_count() == 1U);
IEC104_Command_Cancel(command); command_empty();
}
command_reply_at(0U, 45U, cases[index].cause, 0x51U, cases[index].ca,
cases[index].ioa, cases[index].value);
CHECK(IEC104_Slave_Remove_Station(command_slave, &other) == IEC104_STATION_OK);
command_empty(); command_close();
CHECK(IEC104_Station_Is_Reusable(&other));
}
++command_checks;
}
/* A point may name globally disabled types, but global disable still produces
* IGNORED while an enabled type absent from that point produces negative 7. */
static void test_command_point_type_intersection(void)
{
static const IEC104_CommandPoint_t points[] = {
{1U, 6001U, IEC104_COMMAND_TYPE_BIT(45U) | IEC104_COMMAND_TYPE_BIT(46U), IEC104_COMMAND_MODE_DIRECT},
{1U, 6002U, IEC104_COMMAND_TYPE_BIT(46U), IEC104_COMMAND_MODE_DIRECT}
};
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event;
unsigned ioa;
IEC104_Command_Config_Default(&config);
config.type_mask = IEC104_COMMAND_TYPE_BIT(45U);
config.points = points; config.point_count = sizeof(points) / sizeof(points[0]);
command_open(&config);
for (ioa = 6001U; ioa <= 6003U; ++ioa) {
command_send(46U, 6U, 0x51U, 1U, ioa, 2U);
CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED);
}
command_flush(); command_empty();
CHECK(command_status.rejected == 0U && command_frame_count() == 0U);
command_send(45U, 6U, 0x51U, 1U, 6002U, 1U); command_flush();
command_reply_at(0U, 45U, 0x47U, 0x51U, 1U, 6002U, 1U);
command_empty(); CHECK(command_action_calls == 0U && !command_status.busy);
event = command_execute(45U, 1U);
(void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_status.accepted == 1U && command_status.rejected == 1U);
CHECK(command_action_calls == 1U && command_frame_count() == 3U);
command_close();
++command_checks;
}
/* Pending same-point activation and selected-command matching precede point
* policy; policy itself precedes busy. Cancellation still uses transaction identity. */
static void test_command_point_ordering(void)
{
static const IEC104_CommandPoint_t points[] = {
{1U, 6001U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{1U, 6002U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_DIRECT}
};
unsigned phase;
for (phase = 0U; phase < 3U; ++phase) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t event, finished;
IEC104_CommandTransactionInfo_t info;
unsigned ioa = phase == 2U ? 6002U : 6001U;
IEC104_Command_Config_Default(&config);
config.points = points; config.point_count = sizeof(points) / sizeof(points[0]);
command_open(&config);
command_send(45U, 6U, 0x51U, 1U, ioa, phase == 2U ? 1U : 0x81U);
event = command_take(phase == 2U ? IEC104_COMMAND_EXECUTE : IEC104_COMMAND_SELECT);
if (phase == 1U) {
CHECK(IEC104_Command_Accept(command, &event, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
}
/* Type 46 is globally enabled but absent from this point. */
command_send(46U, 6U, 0x51U, 1U, ioa, 2U); command_flush();
if (phase == 1U) {
command_reply_at(1U, 46U, 0x47U, 0x51U, 1U, ioa, 2U);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_MISMATCH);
CHECK(finished.transaction_id == event.transaction_id && command_status.mismatches == 1U);
CHECK(command_status.dropped_requests == 0U && command_frame_count() == 2U);
} else {
CHECK(command_status.dropped_requests == 1U && command_status.rejected == 0U);
CHECK(command_frame_count() == 0U);
if (phase == 2U) {
command_send(45U, 6U, 0x52U, 1U, ioa, 0x81U); command_flush();
CHECK(command_status.dropped_requests == 2U && command_status.rejected == 0U);
}
CHECK(IEC104_Command_Get_Transaction(command, &event, &info) && info.active);
command_send(45U, 6U, 0x51U, 1U, 6003U, 1U); command_flush();
command_reply_at(0U, 45U, 0x6fU, 0x51U, 1U, 6003U, 1U);
CHECK(command_status.busy_rejected == 0U);
command_send(46U, 6U, 0x51U, 1U, phase == 2U ? 6001U : 6002U, 2U); command_flush();
command_reply_at(1U, 46U, 0x47U, 0x51U, 1U, phase == 2U ? 6001U : 6002U, 2U);
CHECK(command_status.busy_rejected == 0U);
command_send(45U, 6U, 0x51U, 1U, phase == 2U ? 6001U : 6002U, phase == 2U ? 0x81U : 1U);
command_flush();
CHECK(command_status.busy_rejected == 1U && command_status.rejected == 3U);
command_send(45U, 8U, 0x51U, 1U, ioa, 0U); command_flush();
command_reply_at(3U, 45U, 9U, 0x51U, 1U, ioa, 0U);
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == IEC104_COMMAND_FINISH_DEACTIVATED);
CHECK(finished.transaction_id == event.transaction_id && command_frame_count() == 4U);
}
CHECK(command_action_calls == 0U);
command_empty(); command_close();
}
{
IEC104_CommandConfig_t config;
uint8_t bytes[32];
static const uint8_t payload[1] = {1U};
size_t length;
command_timed_config(&config, 58U, 1000U);
config.points = points; config.point_count = sizeof(points) / sizeof(points[0]);
command_open(&config);
length = command_timed_bytes(bytes, 58U, 6U, 0x51U, 1U, 6003U,
payload, sizeof(payload), command_epoch_time);
command_raw_send(bytes, length); command_flush();
command_raw_reply_at(0U, bytes, length, 0x47U);
CHECK(command_status.time_rejected == 1U);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms + 1000U, fake.now));
command_raw_send(bytes, length); command_flush();
CHECK(command_status.time_expired == 1U && command_frame_count() == 1U);
CHECK(IEC104_Command_Set_Time_Base(command, command_epoch_ms, fake.now));
command_raw_send(bytes, length); command_flush();
command_raw_reply_at(1U, bytes, length, 0x6fU);
bytes[6] -= 2U;
command_raw_send(bytes, length); command_flush();
command_raw_reply_at(2U, bytes, length, 0x47U);
CHECK(command_status.rejected == 3U && command_status.time_rejected == 1U);
CHECK(command_status.accepted == 0U && !command_status.busy && command_action_calls == 0U);
command_empty(); command_close();
}
++command_checks;
}
/* Binary lookup must distinguish both CA and all 24 IOA bits at first, middle,
* last and missing keys. The table may precede station registration. */
static void test_command_point_lookup_boundaries(void)
{
static const IEC104_CommandPoint_t points[] = {
{1U, 1U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_DIRECT},
{1U, 6001U, IEC104_COMMAND_TYPE_BIT(46U), IEC104_COMMAND_MODE_DIRECT},
{1U, UINT32_C(0xffffff), IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_DIRECT},
{65534U, 1U, IEC104_COMMAND_TYPE_BIT(46U), IEC104_COMMAND_MODE_DIRECT},
{65534U, UINT32_C(0xffffff), IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_DIRECT}
};
static const struct { unsigned ca, ioa, type, cause; } cases[] = {
{1U, 1U, 45U, 7U}, {1U, 6001U, 46U, 7U}, {1U, 0xffffffU, 45U, 7U},
{65534U, 1U, 46U, 7U}, {65534U, 0xffffffU, 45U, 7U},
{1U, 2U, 45U, 0x6fU}, {1U, 6000U, 45U, 0x6fU},
{65534U, 0xfffffeU, 45U, 0x6fU}, {65534U, 6001U, 45U, 0x6fU},
{65533U, 1U, 45U, 0x6eU}
};
size_t index;
for (index = 0U; index < sizeof(cases) / sizeof(cases[0]); ++index) {
IEC104_CommandConfig_t config;
IEC104_Station_t other = {0};
unsigned value = cases[index].type == 45U ? 1U : 2U;
IEC104_Command_Config_Default(&config);
config.points = points; config.point_count = sizeof(points) / sizeof(points[0]);
command_open(&config);
CHECK(IEC104_Slave_Add_Station(command_slave, &other, 65534U, &other) == IEC104_STATION_OK);
command_send(cases[index].type, 6U, 0x51U, cases[index].ca, cases[index].ioa, value);
if (cases[index].cause == 7U) {
IEC104_CommandEvent_t event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(event.request.ioa == cases[index].ioa && event.request.common_address == cases[index].ca);
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
(void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
CHECK(command_action_calls == 1U);
} else {
command_flush(); command_empty();
CHECK(command_action_calls == 0U && command_status.accepted == 0U && !command_status.busy);
}
command_reply_at(0U, cases[index].type, cases[index].cause, 0x51U,
cases[index].ca, cases[index].ioa, value);
CHECK(IEC104_Slave_Remove_Station(command_slave, &other) == IEC104_STATION_OK);
command_close();
CHECK(IEC104_Station_Is_Reusable(&other));
}
++command_checks;
}
/* Ending a selection cannot transfer its permission to another IOA or a new
* station registration. The immutable table remains usable after every end. */
static void test_command_point_lifetime(void)
{
static const IEC104_CommandPoint_t points[] = {
{1U, 6001U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_SELECT_EXECUTE},
{1U, 6002U, IEC104_COMMAND_TYPE_BIT(45U), IEC104_COMMAND_MODE_DIRECT}
};
unsigned ending;
for (ending = 0U; ending < 3U; ++ending) {
IEC104_CommandConfig_t config;
IEC104_CommandEvent_t selected, event, finished;
IEC104_Station_t replacement = {0};
unsigned reply_index = 1U;
IEC104_Command_Config_Default(&config);
config.points = points; config.point_count = sizeof(points) / sizeof(points[0]);
config.select_timeout_ms = 10U;
command_open(&config);
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
selected = command_take(IEC104_COMMAND_SELECT);
CHECK(IEC104_Command_Accept(command, &selected, NULL, NULL) == IEC104_COMMAND_REPLY_OK);
command_flush();
if (ending == 0U) { fake.now += 10U; command_flush(); }
else if (ending == 1U) { push(stop_act, sizeof(stop_act)); command_poll(); }
else {
CHECK(IEC104_Slave_Remove_Station(command_slave, &command_station) == IEC104_STATION_OK);
CHECK(IEC104_Slave_Add_Station(command_slave, &replacement, 1U, &replacement) == IEC104_STATION_OK);
command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush();
command_reply_at(reply_index++, 45U, 0x47U, 0x51U, 1U, 6001U, 1U);
CHECK(command_status.mismatches == 0U && command_status.busy_rejected == 1U);
IEC104_Command_Cancel(command);
}
finished = command_take(IEC104_COMMAND_FINISHED);
CHECK(finished.reason == (ending == 0U ? IEC104_COMMAND_FINISH_SELECT_TIMEOUT :
(ending == 1U ? IEC104_COMMAND_FINISH_STOPPED : IEC104_COMMAND_FINISH_CANCELLED)));
CHECK(finished.request.station_id == selected.request.station_id);
CHECK(IEC104_Command_Accept(command, &selected, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_STALE);
if (ending == 1U) { push(start_act, sizeof(start_act)); command_poll(); }
command_send(45U, 6U, 0x51U, 1U, 6001U, 1U); command_flush();
command_reply_at(reply_index++, 45U, 0x47U, 0x51U, 1U, 6001U, 1U);
command_empty(); CHECK(command_action_calls == 0U && !command_status.busy);
command_send(45U, 6U, 0x51U, 1U, 6002U, 1U);
event = command_take(IEC104_COMMAND_EXECUTE);
CHECK(!event.request.selected && event.transaction_id != selected.transaction_id);
CHECK((event.request.station_id == selected.request.station_id) == (ending != 2U));
CHECK(IEC104_Command_Accept(command, &event, command_action, &command_action_calls) == IEC104_COMMAND_REPLY_OK);
(void)command_done(&event, IEC104_COMMAND_RESULT_SUCCEEDED);
command_reply_at(reply_index++, 45U, 7U, 0x51U, 1U, 6002U, 1U);
command_reply_at(reply_index++, 45U, 10U, 0x51U, 1U, 6002U, 1U);
CHECK(command_frame_count() == reply_index && command_action_calls == 1U);
if (ending == 2U) {
CHECK(IEC104_Station_Is_Reusable(&command_station));
CHECK(IEC104_Slave_Remove_Station(command_slave, &replacement) == IEC104_STATION_OK);
}
command_close();
CHECK(IEC104_Station_Is_Reusable(&replacement));
}
++command_checks;
}
/* 执行契约测试;各组内部的类型、动作结果、期限及策略组合分别断言。
* 尺寸输出用于记录当前宿主ABI的公开结构布局,不当作MCU内存预算。 */
int main(void)
{
test_command_configuration();
test_command_wire_validation();
test_command_complete_flows();
test_command_result_policies();
test_command_action_outcomes();
test_command_rejection_and_stale();
test_command_matching_and_busy();
test_command_pending_same_point();
test_command_pending_repeat_deadline();
test_command_pending_repeat_boundaries();
test_command_pending_repeat_lifetime();
test_command_finished_pairing();
test_command_unsubmitted_selection();
test_command_deactivation_wait();
test_command_deadlines();
test_command_result_before_confirm();
test_command_fifo_and_overload();
test_command_return_validation();
test_command_stop_notifications();
test_command_disconnect_and_reuse();
test_command_station_lifetime();
test_command_reentry();
test_command_window_and_cancel();
test_command_two_services();
test_command_normal_overwrite();
test_command_stop_during_submit();
test_command_independent_reply_lifetime();
test_command_extended_configuration();
test_command_extended_flows();
test_command_extended_matching();
test_command_extended_invalid_values();
test_command_extended_wire_validation();
test_command_extended_reply_lengths();
test_command_timed_flows();
test_command_time_conversion();
test_command_timed_invalid_values();
test_command_time_boundaries();
test_command_time_base_errors();
test_command_time_base_api();
test_command_time_base_lifecycle();
test_command_time_configuration();
test_command_time_ordering();
test_command_time_base_reentry();
test_command_timed_reply_lengths();
test_command_timed_wire_validation();
test_command_default_all_types();
test_command_point_default_empty();
test_command_point_configuration();
test_command_point_modes();
test_command_point_type_intersection();
test_command_point_ordering();
test_command_point_lookup_boundaries();
test_command_point_lifetime();
printf("Command tests passed: groups=%u tcp=%u replies=%u request=%zu event=%zu status=%zu\n",
command_checks, (unsigned)IEC104_TCP_SERVER_CAPACITY, (unsigned)IEC104_COMMAND_REPLY_CAPACITY,
sizeof(IEC104_CommandRequest_t), sizeof(IEC104_CommandEvent_t), sizeof(IEC104_CommandStatus_t));
return 0;
}