Files
STM32F4-Base/IEC60870-Slave/test/iec104_command_test.c
T
STF cd42b4d61d
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m51s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 18s
添加遥控业务代码
2026-10-30 20:03:19 -07:00

1426 lines
75 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路径验证45/46事务。
* 复用已有测试的可控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;
/* 消费全部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时使用Command默认配置。
* 链路和本地发送期限刻意放宽,使业务期限测试不会先触发无关的传输超时。
* 查找内部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);
IEC104_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 == 3U && config.select_required_mask == 3U);
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 |= IEC104_COMMAND_TYPE_BIT(47U); break;
case 2U: invalid.select_timeout_ms = 0U; break;
case 3U: invalid.type_mask = 1U; invalid.select_required_mask = 2U; break;
default: invalid.select_required_mask = UINT16_MAX; 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();
}
command_open(NULL);
command_send(47U, 6U, 0U, 1U, 6001U, 1U);
CHECK(command_last_handled == IEC104_COMMAND_EVENT_IGNORED);
{
uint8_t timed[17] = {0};
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;
IEC104_Command_Config_Default(&config);
config.type_mask = config.select_required_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;
IEC104_Command_Config_Default(&config);
config.select_required_mask = 0U;
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;
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U;
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;
IEC104_Command_Config_Default(&config);
config.decision_timeout_ms = 100U;
if (phase == 1U) { config.select_required_mask = 0U; }
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;
IEC104_Command_Config_Default(&config); config.decision_timeout_ms = 10U;
if (phase == 1U) { config.select_required_mask = 0U; }
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;
IEC104_Command_Config_Default(&config);
config.type_mask = config.select_required_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;
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U;
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;
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U;
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;
IEC104_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;
IEC104_Command_Config_Default(&config);
config.select_required_mask = 0U;
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;
IEC104_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];
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U; 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;
IEC104_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;
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U;
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;
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U;
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);
IEC104_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;
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U;
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;
IEC104_Command_Config_Default(&config); config.select_required_mask = 0U;
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;
}
/* 执行27组契约测试;各组内部的类型、动作结果、期限及策略组合分别断言。
* 尺寸输出用于记录当前宿主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();
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;
}