1102 lines
51 KiB
Plaintext
1102 lines
51 KiB
Plaintext
/* Command transactions over the real Slave/TCP/SendManager path with controlled I/O.
|
|
* Reuse only the existing Net fixture; its renamed test entry is not run here.
|
|
* Do not separately link TcpServer.c or a platform Net backend for this target. */
|
|
#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
|
|
|
|
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;
|
|
|
|
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);
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
static void command_flush(void)
|
|
{
|
|
unsigned index;
|
|
for (index = 0U; index < 16U; ++index) {
|
|
command_poll();
|
|
command_process();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* Input and expected responses are byte literals, independent of object encoders. */
|
|
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);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* Fill the real transport buffers; the business layer never receives a fake WAIT. */
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
/* Confirmation not handed over yet: even a mismatching execution is merely busy. */
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
/* Receive at a valid transport time, then test business acceptance time. */
|
|
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();
|
|
}
|
|
}
|
|
/* Disabled duration limits still reject a reversed monotonic time source. */
|
|
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();
|
|
}
|
|
/* A previously delivered SELECT still owes FINISHED when unread EXECUTE expires. */
|
|
{
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
/* Drain only TCP: pending Command replies must still prevent direct Try insertion. */
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
/* The first NORMAL return (value=0) was overwritten; only ACT_CON remains. */
|
|
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;
|
|
}
|
|
|
|
static int command_hold_transfer(void *context) { (void)context; return 0; }
|
|
|
|
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);
|
|
}
|
|
|
|
static int command_fail_after_stop(void *context, IEC104_DataTransferState_t state)
|
|
{
|
|
CHECK(Notify_Transfer_State(context, state));
|
|
return 0;
|
|
}
|
|
|
|
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) {
|
|
/* FATAL must not be mistaken for benign STOP even when a STOP event exists. */
|
|
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;
|
|
}
|
|
|
|
static void test_command_independent_reply_lifetime(void)
|
|
{
|
|
command_open(NULL);
|
|
command_send(45U, 6U, 0x51U, 1U, 6001U, 0x81U);
|
|
command_congest();
|
|
/* Cancel the unread transaction, then leave only an independent error response. */
|
|
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;
|
|
}
|
|
|
|
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_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;
|
|
}
|