1426 lines
75 KiB
C
1426 lines
75 KiB
C
/*
|
||
* 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;
|
||
}
|