/* 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; }