diff --git a/tactical-microgrid-standard/CMakeLists.txt b/tactical-microgrid-standard/CMakeLists.txt index 81f90ca..50ee2f9 100644 --- a/tactical-microgrid-standard/CMakeLists.txt +++ b/tactical-microgrid-standard/CMakeLists.txt @@ -57,6 +57,7 @@ add_executable(Controller controller/ControllerCommandDataReaderListenerImpl.cpp controller/ReplyDataReaderListenerImpl.cpp controller/PowerTopologyDataReaderListenerImpl.cpp + controller/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp ) target_include_directories(Controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(Controller PRIVATE Commands_Idl PowerSim_Idl) @@ -64,7 +65,6 @@ target_link_libraries(Controller PRIVATE Commands_Idl PowerSim_Idl) add_executable(CLI cli/main.cpp cli/CLIClient.cpp - cli/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp ) target_include_directories(CLI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(CLI PRIVATE Commands_Idl PowerSim_Idl) diff --git a/tactical-microgrid-standard/cli/CLIClient.cpp b/tactical-microgrid-standard/cli/CLIClient.cpp index e54e080..c4c85bd 100644 --- a/tactical-microgrid-standard/cli/CLIClient.cpp +++ b/tactical-microgrid-standard/cli/CLIClient.cpp @@ -1,7 +1,6 @@ #include "CLIClient.h" #include "common/QosHelper.h" #include "common/Utils.h" -#include "ActiveMicrogridControllerStateDataReaderListenerImpl.h" #include #include @@ -92,46 +91,6 @@ DDS::ReturnCode_t CLIClient::init_tms(DDS::DomainId_t domain_id, int argc, char* return DDS::RETCODE_ERROR; } - // Subscribe to the tms::ActiveMicrogridControllerState topic - tms::ActiveMicrogridControllerStateTypeSupport_var amcs_ts = new tms::ActiveMicrogridControllerStateTypeSupportImpl; - if (DDS::RETCODE_OK != amcs_ts->register_type(dp, "")) { - ACE_ERROR((LM_ERROR, "(%P|%t) CLIClient::init: register_type ActiveMicrogridControllerState failed\n")); - return DDS::RETCODE_ERROR; - } - - CORBA::String_var amcs_type_name = amcs_ts->get_type_name(); - DDS::Topic_var amcs_topic = dp->create_topic(tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE.c_str(), - amcs_type_name, - TOPIC_QOS_DEFAULT, - nullptr, - ::OpenDDS::DCPS::DEFAULT_STATUS_MASK); - if (!amcs_topic) { - ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::init: create_topic \"%C\" failed\n", - tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE.c_str())); - return DDS::RETCODE_ERROR; - } - - const DDS::SubscriberQos tms_sub_qos = Qos::Subscriber::get_qos(); - DDS::Subscriber_var tms_sub = dp->create_subscriber(tms_sub_qos, - nullptr, - ::OpenDDS::DCPS::DEFAULT_STATUS_MASK); - if (!tms_sub) { - ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::init: create_subscriber with TMS QoS failed\n")); - return DDS::RETCODE_ERROR; - } - - const DDS::DataReaderQos& amcs_dr_qos = Qos::DataReader::fn_map.at(tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE)(device_id); - DDS::DataReaderListener_var amcs_listener(new ActiveMicrogridControllerStateDataReaderListenerImpl(*this)); - DDS::DataReader_var amcs_dr_base = tms_sub->create_datareader(amcs_topic, - amcs_dr_qos, - amcs_listener, - ::OpenDDS::DCPS::DEFAULT_STATUS_MASK); - if (!amcs_dr_base) { - ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::init: create_datareader for topic \"%C\" failed\n", - tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE.c_str())); - return DDS::RETCODE_ERROR; - } - return DDS::RETCODE_OK; } @@ -323,6 +282,21 @@ bool CLIClient::cli_stopped() const return stop_cli_; } +void CLIClient::display_commands() const +{ + const char* msg = R"(=== Command-Line Interface for Microgrid Controller (MC) === +list-mc : list the connected MCs. +list-pd : list the power devices reported by the connected MCs. +connect-pd : connect power devices to simulate the power topology of a microgrid. +start : start a power device with the given Id. +stop : stop a power device with the given Id. +suspend : suspend the heartbeats of the given MC (simulating an MC becomming unavailable). +resume : resume the heartbeats of the given MC (simulating an MC becomming available). +term : terminate the given MC's process. +show : display this list of CLI commands.)"; + std::cout << msg << std::endl; +} + void CLIClient::run_cli() { const std::string prompt = "ENTER COMMAND:> "; @@ -342,18 +316,16 @@ void CLIClient::run_cli() list_power_devices(); } else if (op == "connect-pd") { connect_power_devices(); - } else if (op == "set") { - set_controller(op_pair); - } else if (op == "enable") { + } else if (op == "start") { send_start_device_cmd(op_pair); - } else if (op == "disable") { - send_stop_device_cmd(op_pair); } else if (op == "stop") { - send_stop_controller_cmd(); + send_stop_device_cmd(op_pair); + } else if (op == "suspend") { + send_suspend_controller_cmd(op_pair); } else if (op == "resume") { - send_resume_controller_cmd(); + send_resume_controller_cmd(op_pair); } else if (op == "term") { - send_terminate_controller_cmd(); + send_terminate_controller_cmd(op_pair); } else if (op == "show") { display_commands(); } else { @@ -370,14 +342,6 @@ void CLIClient::run() thr.join(); } -void CLIClient::set_active_controller(const tms::Identity& device_id, - const std::optional& master_id) -{ - std::lock_guard guard(active_controllers_m_); - // The value is absent if the device has lost its active controller or hasn't selected one yet. - active_controllers_[device_id] = master_id.value_or(""); -} - void CLIClient::tolower(std::string& s) const { for (size_t i = 0; i < s.size(); ++i) { @@ -390,38 +354,20 @@ OpArgPair CLIClient::parse(const std::string& input) const const std::string whitespace = " \t"; const size_t first = input.find_first_not_of(whitespace); if (first == std::string::npos) { - return std::make_pair("", OpenDDS::DCPS::optional()); + return std::make_pair("", std::optional()); } const size_t last = input.find_last_not_of(whitespace); const std::string trimed_input = input.substr(first, last - first + 1); const size_t delim = trimed_input.find_first_of(whitespace); if (delim == std::string::npos) { - return std::make_pair(trimed_input, OpenDDS::DCPS::optional()); + return std::make_pair(trimed_input, std::optional()); } const std::string op = trimed_input.substr(0, delim); const size_t arg_begin = trimed_input.find_first_not_of(whitespace, delim + 1); const std::string arg = trimed_input.substr(arg_begin); - return std::make_pair(op, OpenDDS::DCPS::optional(arg)); -} - -void CLIClient::display_commands() const -{ - const char* msg = R"(=== Command-Line Interface for Microgrid Controller === -list-mc : list the connected microgrid controllers. -set : set the current microgrid controller. - Subsequent commands target this controller - until another set command is used. -list-pd : list the power devices connected to the current controller. -connect-pd : connect power devices to simulate a power topology. -enable : start a power device with the given Id. -disable : stop a power device with the given Id. -stop : stop the current controller's heartbeats. -resume : resume the current controller's heartbeats. -term : terminate the current controller. -show : display the list of CLI commands.)"; - std::cout << msg << std::endl; + return std::make_pair(op, std::optional(arg)); } std::string CLIClient::energy_level_to_string(tms::EnergyStartStopLevel essl) const @@ -446,44 +392,52 @@ std::string CLIClient::energy_level_to_string(tms::EnergyStartStopLevel essl) co } } -void CLIClient::list_power_devices() +// Collect the list of power devices from the available MCs. +bool CLIClient::collect_power_devices() { - std::lock_guard guard(data_m_); - if (curr_controller_.empty()) { - std::cerr << "Must set the current controller first!" << std::endl; - return; - } - - if (send_power_devices_request()) { - display_power_devices(); + bool ret = true; + const auto now = Clock::now(); + for (auto it = controllers_.begin(); it != controllers_.end(); ++it) { + const auto status = controller_status(now, it->second.last_hb); + if (status == ControllerStatus::AVAILABLE) { + ret &= send_power_devices_request(it->first); + } else { + // There may be stale entries that need to be deleted, + // so displaying power devices looks clean. + mc_to_devices_.erase(it->first); + } } + return ret; } void CLIClient::display_power_devices() const { - std::cout << "Current Microgrid Controller's Id: " << curr_controller_ << std::endl; - std::cout << "Number of Connected Power Devices: " << power_devices_.size() << std::endl; size_t i = 1; - for (auto it = power_devices_.begin(); it != power_devices_.end(); ++it) { - std::string selected_controller; - { - std::lock_guard guard(active_controllers_m_); - auto ac_it = active_controllers_.find(it->first); - if (ac_it != active_controllers_.end()) { - selected_controller = "\"" + ac_it->second + "\""; - } else { - selected_controller = "\"Undetermined\""; - } + for (auto it = mc_to_devices_.begin(); it != mc_to_devices_.end(); ++it) { + const auto mc_id = it->first; + const auto power_devices = it->second; + std::cout << "Devices connected to Microgrid Controller \"" << mc_id << "\":" << std::endl; + for (auto it2 = power_devices.begin(); it2 != power_devices.end(); ++it2) { + const std::string formated_id = std::string("\"") + it2->first + "\""; + std::string selected_controller = std::string("\"") + it2->second.master_id().value_or("Undetermined") + "\""; + + std::cout << std::right << std::setfill(' ') << std::setw(3) << i++ + << ". Id: " << std::left << std::setw(15) << formated_id + << "| Type: " << std::left << std::setw(18) << Utils::device_role_to_string(it2->second.device_info().role()) + << "| Energy Level: " << std::left << std::setw(15) << energy_level_to_string(it2->second.essl()) + << "| Active Controller: " << std::left << selected_controller << std::endl; } - const std::string formated_id = "\"" + it->first + "\""; - std::cout << std::right << std::setfill(' ') << std::setw(3) << i++ - << ". Id: " << std::left << std::setw(15) << formated_id - << "| Type: " << std::left << std::setw(18) << Utils::device_role_to_string(it->second.device_info().role()) - << "| Energy Level: " << std::left << std::setw(15) << energy_level_to_string(it->second.essl()) - << "| Active Controller: " << std::left << selected_controller << std::endl; + std::cout << std::endl; } } +void CLIClient::list_power_devices() +{ + std::lock_guard guard(data_m_); + collect_power_devices(); + display_power_devices(); +} + bool CLIClient::is_single_port_device(tms::DeviceRole role) const { switch (role) { @@ -522,10 +476,31 @@ void CLIClient::connect(const tms::Identity& id1, tms::DeviceRole role1, power_connections_[id2].insert(powersim::ConnectedDevice{id1, role1}); } +// Gather information for all power devices in the microgrid from all MCs. +// The total information of all devices is then used to construct a topology +// of power devices to simulate a microgrid with devices connected by power cable. +// This is necessary when simulating network partition is supported, e.g., MC 1 +// has connections to a subset of devices and MC 2 has connections to the remaining devices. +// When there is no network partition, all MCs should have the same set of devices. +void CLIClient::consolidate_power_devices() +{ + if (mc_to_devices_.empty()) { + collect_power_devices(); + } + + for (auto it = mc_to_devices_.begin(); it != mc_to_devices_.end(); ++it) { + const PowerDevices& pds = it->second; + power_devices_.insert(pds.begin(), pds.end()); + } +} + +// Construct connections between power devices to simulate a microgrid. +// Each connection simulates a physical connection using a power cable. void CLIClient::connect_power_devices() { + std::lock_guard guard(data_m_); if (power_devices_.empty()) { - send_power_devices_request(); + consolidate_power_devices(); } for (auto it1 = power_devices_.begin(); it1 != power_devices_.end(); ++it1) { @@ -561,7 +536,6 @@ void CLIClient::connect_power_devices() // Send the power topology to the current controller which then // distributes the power connections to its managed power devices. powersim::PowerTopology pt; - pt.mc_id() = curr_controller_; pt.connections().reserve(power_connections_.size()); CORBA::ULong i = 0; for (auto it = power_connections_.begin(); it != power_connections_.end(); ++it, ++i) { @@ -574,58 +548,66 @@ void CLIClient::connect_power_devices() pt.connections().push_back(pc); } - DDS::ReturnCode_t rc = pt_dw_->write(pt, DDS::HANDLE_NIL); - if (rc != DDS::RETCODE_OK) { + // Forward to the first available controller in the list + const auto now = Clock::now(); + bool sent = false; + for (auto it = controllers_.begin(); !sent && it != controllers_.end(); ++it) { + if (controller_status(now, it->second.last_hb) == ControllerStatus::UNAVAILABLE) { + continue; + } + + const auto& mc_id = it->first; + pt.mc_id() = mc_id; + DDS::ReturnCode_t rc = pt_dw_->write(pt, DDS::HANDLE_NIL); + if (rc != DDS::RETCODE_OK) { + ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::connect_power_devices: " + "write power topology to controller \"%C\" failed\n", mc_id.c_str())); + } else { + sent = true; + } + } + + if (!sent) { ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::connect_power_devices: " - "write power topology to controller \"%C\" failed\n", curr_controller_.c_str())); + "Failed to setup power topology for the simulation!\n")); } } +CLIClient::ControllerStatus CLIClient::controller_status(TimePoint now, TimePoint last_heartbeat) const +{ + return (now - last_heartbeat < unavail_controller_delay) ? ControllerStatus::AVAILABLE : ControllerStatus::UNAVAILABLE; +} + void CLIClient::display_controllers() const { std::lock_guard guard(data_m_); std::cout << "Number of Connected Microgrid Controllers: " << controllers_.size() << std::endl; size_t i = 1; + const auto now = Clock::now(); for (auto it = controllers_.begin(); it != controllers_.end(); ++it) { - std::cout << i << ". Controller Id: " << it->first << " (" << - (it->second.status == ControllerInfo::Status::AVAILABLE ? "available)" : "unavailable)") << std::endl; - } -} - -void CLIClient::set_controller(const OpArgPair& op_arg) -{ - if (!op_arg.second.has_value()) { - std::cerr << "No microgrid controller specified!" << std::endl; - return; + std::cout << i << ". Controller Id: " << it->first << " (" << + (controller_status(now, it->second.last_hb) == ControllerStatus::AVAILABLE ? "available)" : "unavailable)") + << std::endl; } - - std::lock_guard guard(data_m_); - auto& value = op_arg.second.value(); - if (!controllers_.count(value)) { - std::cerr << "No controller with Id: " << value << std::endl; - return; - } - - curr_controller_ = value; - std::cout << "Current microgrid controller set to: " << curr_controller_ << std::endl; } -bool CLIClient::send_power_devices_request() +// Caller must already hold data_m_ lock +bool CLIClient::send_power_devices_request(const tms::Identity& mc_id) { cli::PowerDevicesRequest pd_req; - pd_req.mc_id(curr_controller_); + pd_req.mc_id(mc_id); DDS::ReturnCode_t rc = pdreq_dw_->write(pd_req, DDS::HANDLE_NIL); if (rc != DDS::RETCODE_OK) { ACE_ERROR((LM_WARNING, "(%P|%t) WARNING: CLIClient::send_power_devices_request: " - "write to controller \"%C\" failed\n", curr_controller_.c_str())); + "write to controller \"%C\" failed\n", mc_id.c_str())); return false; } // Wait for the reply DDS::StringSeq params; params.length(1); - params[0] = curr_controller_.c_str(); + params[0] = mc_id.c_str(); DDS::QueryCondition_var qc = pdrep_dr_->create_querycondition(DDS::NOT_READ_SAMPLE_STATE, DDS::ANY_VIEW_STATE, DDS::ANY_INSTANCE_STATE, @@ -634,7 +616,7 @@ bool CLIClient::send_power_devices_request() if (!qc) { ACE_ERROR((LM_WARNING, "(%P|%t) WARNING: CLIClient::send_power_devices_request: " "create_querycondition to receive from controller \"%C\" failed\n", - curr_controller_.c_str())); + mc_id.c_str())); return false; } @@ -662,123 +644,142 @@ bool CLIClient::send_power_devices_request() bool received = false; for (CORBA::ULong i = 0; !received && i < data.length(); ++i) { - if (data[i].mc_id() != curr_controller_) { + if (data[i].mc_id() != mc_id) { ACE_ERROR((LM_WARNING, "(%P|%t) WARNING: CLIClient::send_power_devices_request: " "reply expected from controller \"%C\". Received from \"%C\"\n", - curr_controller_.c_str(), data[i].mc_id().c_str())); + mc_id.c_str(), data[i].mc_id().c_str())); continue; } if (info_seq[i].valid_data) { const cli::PowerDeviceInfoSeq& pdi_seq = data[i].devices(); + auto& power_devices = mc_to_devices_[mc_id]; for (auto it = pdi_seq.begin(); it != pdi_seq.end(); ++it) { - power_devices_.insert(std::make_pair(it->device_info().deviceId(), *it)); + // Add to the existing list of power devices. + // This allows power devices to be added gradually in case + // the "list-pd" command is issued before all devices have joined. + power_devices.insert_or_assign(it->device_info().deviceId(), *it); } } else if (info_seq[i].instance_state == DDS::NOT_ALIVE_DISPOSED_INSTANCE_STATE) { - power_devices_.clear(); + mc_to_devices_.erase(mc_id); } received = true; } if (!received) { ACE_DEBUG((LM_DEBUG, "(%P|%t) DEBUG: CLIClient::send_power_devices_request: " - "Failed to receive data from current controller (%C)\n", curr_controller_.c_str())); + "Failed to receive data from controller (%C)\n", mc_id.c_str())); } return true; } void CLIClient::send_start_stop_request(const OpArgPair& op_arg, - tms::OperatorPriorityType opt) const + tms::OperatorPriorityType opt) { if (!op_arg.second.has_value()) { std::cerr << "No power device specified!" << std::endl; return; } + auto& pd_id = op_arg.second.value(); - if (curr_controller_.empty()) { - std::cerr << "Must set the current controller first!" << std::endl; - return; - } - - auto& value = op_arg.second.value(); - { - std::lock_guard guard(data_m_); - if (!power_devices_.count(value)) { - std::cerr << "The current controller has no connected device with Id: " << value << std::endl; - return; - } + std::lock_guard guard(data_m_); + if (mc_to_devices_.empty()) { + collect_power_devices(); } - const tms::Identity device_id = handshaking_.get_device_id(); + const tms::Identity my_id = handshaking_.get_device_id(); tms::OperatorIntentRequest oir; - oir.requestId().requestingDeviceId() = device_id; + oir.requestId().requestingDeviceId() = my_id; // This doesn't seem to get used since there is no reply for this request. oir.sequenceId() = 0; tms::OperatorIntent oi; - { - std::lock_guard guard(data_m_); - oi.requestId().requestingDeviceId() = curr_controller_; - } oi.intentType() = tms::OperatorIntentType::OIT_OPERATOR_DEFINED; tms::DeviceIntent intent; - intent.deviceId() = value; + intent.deviceId() = pd_id; intent.battleShort() = false; intent.priority().priorityType() = opt; oi.devices().push_back(intent); oir.desiredOperatorIntent() = oi; - DDS::ReturnCode_t rc = oir_dw_->write(oir, DDS::HANDLE_NIL); - if (rc != DDS::RETCODE_OK) { - ACE_ERROR((LM_WARNING, "(%P|%t) WARNING: CLIClient::send_start_stop_request: write %C request returned \"%C\"\n", - opt == tms::OperatorPriorityType::OPT_ALWAYS_OPERATE ? "start" : "stop", - OpenDDS::DCPS::retcode_to_string(rc))); - return; + // Send a request to all controllers. + // Only the controller responsible for the device will send a command to it. + // The other controllers only update the energy level of the device locally. + const auto now = Clock::now(); + for (auto it = mc_to_devices_.begin(); it != mc_to_devices_.end(); ++it) { + // Verify that the controller is available + const tms::Identity& mc_id = it->first; + auto it2 = controllers_.find(mc_id); + if (it2 == controllers_.end()) { + ACE_ERROR((LM_WARNING, "(%P|%t) WARNING: CLIClient::send_start_stop_request: controller \"%C\" not found\n", + mc_id.c_str())); + return; + } + + if (controller_status(now, it2->second.last_hb) == ControllerStatus::UNAVAILABLE) { + continue; + } + + oir.desiredOperatorIntent().requestId().requestingDeviceId() = mc_id; + + DDS::ReturnCode_t rc = oir_dw_->write(oir, DDS::HANDLE_NIL); + if (rc != DDS::RETCODE_OK) { + ACE_ERROR((LM_WARNING, "(%P|%t) WARNING: CLIClient::send_start_stop_request: write %C request for device \"%C\" " + "to controller \"%C\" returned \"%C\"\n", + opt == tms::OperatorPriorityType::OPT_ALWAYS_OPERATE ? "start" : "stop", + pd_id.c_str(), mc_id.c_str(), OpenDDS::DCPS::retcode_to_string(rc))); + } } } -void CLIClient::send_start_device_cmd(const OpArgPair& op_arg) const +void CLIClient::send_start_device_cmd(const OpArgPair& op_arg) { send_start_stop_request(op_arg, tms::OperatorPriorityType::OPT_ALWAYS_OPERATE); } -void CLIClient::send_stop_device_cmd(const OpArgPair& op_arg) const +void CLIClient::send_stop_device_cmd(const OpArgPair& op_arg) { send_start_stop_request(op_arg, tms::OperatorPriorityType::OPT_NEVER_OPERATE); } -void CLIClient::send_controller_cmd(cli::ControllerCmdType cmd_type) const +void CLIClient::send_controller_cmd(const OpArgPair& op_arg, cli::ControllerCmdType cmd_type) const { std::lock_guard guard(data_m_); - if (curr_controller_.empty()) { - std::cerr << "Must set the current controller first!" << std::endl; + if (!op_arg.second.has_value()) { + std::cerr << "No microgrid controller specified!" << std::endl; + return; + } + + const auto& mc_id = op_arg.second.value(); + if (!controllers_.count(mc_id)) { + std::cerr << "Unknown controller \"" << mc_id << "\"!!!" << std::endl; return; } cli::ControllerCommand cmd; - cmd.mc_id() = curr_controller_; + cmd.mc_id() = mc_id; cmd.type() = cmd_type; DDS::ReturnCode_t rc = cc_dw_->write(cmd, DDS::HANDLE_NIL); if (rc != DDS::RETCODE_OK) { - ACE_ERROR((LM_WARNING, "CLIClient::send_controller_cmd: write ControllerCommand failed: \"%C\"\n", - OpenDDS::DCPS::retcode_to_string(rc))); + ACE_ERROR((LM_WARNING, "CLIClient::send_controller_cmd: write ControllerCommand to MC \"%C\" failed: \"%C\"\n", + mc_id.c_str(), OpenDDS::DCPS::retcode_to_string(rc))); } } -void CLIClient::send_stop_controller_cmd() const +void CLIClient::send_suspend_controller_cmd(const OpArgPair& op_arg) const { - send_controller_cmd(cli::ControllerCmdType::CCT_STOP); + send_controller_cmd(op_arg, cli::ControllerCmdType::CCT_STOP); } -void CLIClient::send_resume_controller_cmd() const +void CLIClient::send_resume_controller_cmd(const OpArgPair& op_arg) const { - send_controller_cmd(cli::ControllerCmdType::CCT_RESUME); + send_controller_cmd(op_arg, cli::ControllerCmdType::CCT_RESUME); } -void CLIClient::send_terminate_controller_cmd() const +void CLIClient::send_terminate_controller_cmd(const OpArgPair& op_arg) const { - send_controller_cmd(cli::ControllerCmdType::CCT_TERMINATE); + send_controller_cmd(op_arg, cli::ControllerCmdType::CCT_TERMINATE); } void CLIClient::process_device_info(const tms::DeviceInfo& di, const DDS::SampleInfo& si) @@ -786,11 +787,9 @@ void CLIClient::process_device_info(const tms::DeviceInfo& di, const DDS::Sample std::lock_guard guard(data_m_); if (si.valid_data) { if (di.role() == tms::DeviceRole::ROLE_MICROGRID_CONTROLLER) { - controllers_.insert(std::make_pair(di.deviceId(), ControllerInfo{di, ControllerInfo::Status::AVAILABLE, Clock::now()})); - schedule_once(di.deviceId(), UnavailableController{di.deviceId()}, unavail_controller_delay); + controllers_.insert(std::make_pair(di.deviceId(), ControllerInfo{ di, Clock::now() })); } } else if (si.instance_state & DDS::NOT_ALIVE_DISPOSED_INSTANCE_STATE){ - cancel(di.deviceId()); controllers_.erase(di.deviceId()); } } @@ -801,28 +800,13 @@ void CLIClient::process_heartbeat(const tms::Heartbeat& hb, const DDS::SampleInf if (si.valid_data) { auto it = controllers_.find(hb.deviceId()); if (it != controllers_.end()) { - it->second.status = ControllerInfo::Status::AVAILABLE; it->second.last_hb = Clock::now(); - reschedule(hb.deviceId()); } } else if (si.instance_state & DDS::NOT_ALIVE_DISPOSED_INSTANCE_STATE) { - cancel(hb.deviceId()); controllers_.erase(hb.deviceId()); } } -void CLIClient::any_timer_fired(TimerHandler::AnyTimer any_timer) -{ - const tms::Identity& mc_id = std::get::Ptr>(any_timer)->arg.id; - std::lock_guard guard(data_m_); - auto it = controllers_.find(mc_id); - if (it != controllers_.end()) { - it->second.status = ControllerInfo::Status::UNAVAILABLE; - } else { - ACE_ERROR((LM_WARNING, "(%P|%t) WARNING: CLIClient::any_timer_fired: Could't find controller with Id \"%C\"\n", mc_id.c_str()));; - } -} - int CLIClient::handle_signal(int, siginfo_t*, ucontext_t*) { std::lock_guard cli_guard(cli_m_); diff --git a/tactical-microgrid-standard/cli/CLIClient.h b/tactical-microgrid-standard/cli/CLIClient.h index 8671d1b..69b0df4 100644 --- a/tactical-microgrid-standard/cli/CLIClient.h +++ b/tactical-microgrid-standard/cli/CLIClient.h @@ -12,11 +12,6 @@ #include #include -struct UnavailableController { - tms::Identity id; - static const char* name() { return "UnavailableController"; } -}; - struct ConnectedDeviceEqual { bool operator()(const powersim::ConnectedDevice& cd1, const powersim::ConnectedDevice& cd2) const { @@ -31,7 +26,7 @@ struct ConnectedDeviceHash { } }; -class CLIClient : public TimerHandler { +class CLIClient { public: explicit CLIClient(const tms::Identity& id); ~CLIClient() {} @@ -40,8 +35,6 @@ class CLIClient : public TimerHandler { void run(); - void set_active_controller(const tms::Identity& device_id, const OPENDDS_OPTIONAL_NS::optional& master_id); - private: // Initialize DDS entities in the TMS domain DDS::ReturnCode_t init_tms(DDS::DomainId_t tms_domain_id, int argc = 0, char* argv[] = nullptr); @@ -57,8 +50,16 @@ class CLIClient : public TimerHandler { void display_commands() const; std::string energy_level_to_string(tms::EnergyStartStopLevel essl) const; + + enum class ControllerStatus { + AVAILABLE, + UNAVAILABLE, + }; + + ControllerStatus controller_status(TimePoint now, TimePoint last_heartbeat) const; void display_controllers() const; - void set_controller(const OpArgPair& op_arg); + bool collect_power_devices(); + void display_power_devices() const; void list_power_devices(); bool is_single_port_device(tms::DeviceRole role) const; @@ -69,24 +70,23 @@ class CLIClient : public TimerHandler { // Create a power connection between two devices void connect(const tms::Identity& id1, tms::DeviceRole role1, const tms::Identity& id2, tms::DeviceRole role2); - + void consolidate_power_devices(); void connect_power_devices(); - bool send_power_devices_request(); - void display_power_devices() const; - void send_start_device_cmd(const OpArgPair& op_arg) const; - void send_stop_device_cmd(const OpArgPair& op_arg) const; - void send_start_stop_request(const OpArgPair& op_arg, tms::OperatorPriorityType opt) const; - void send_stop_controller_cmd() const; - void send_resume_controller_cmd() const; - void send_terminate_controller_cmd() const; - void send_controller_cmd(cli::ControllerCmdType cmd_type) const; + bool send_power_devices_request(const tms::Identity& mc_id); + void send_start_device_cmd(const OpArgPair& op_arg); + void send_stop_device_cmd(const OpArgPair& op_arg); + void send_start_stop_request(const OpArgPair& op_arg, tms::OperatorPriorityType opt); + void send_suspend_controller_cmd(const OpArgPair& op_pair) const; + void send_resume_controller_cmd(const OpArgPair& op_pair) const; + void send_terminate_controller_cmd(const OpArgPair& op_pair) const; + void send_controller_cmd(const OpArgPair& op_pair, cli::ControllerCmdType cmd_type) const; void process_device_info(const tms::DeviceInfo& di, const DDS::SampleInfo& si); void process_heartbeat(const tms::Heartbeat& hb, const DDS::SampleInfo& si); - void any_timer_fired(TimerHandler::AnyTimer any_timer) final; int handle_signal(int, siginfo_t*, ucontext_t*); + ACE_Reactor* const reactor_ = ACE_Reactor::instance(); DDS::DomainParticipant_var sim_participant_; Handshaking handshaking_; cli::PowerDevicesRequestDataWriter_var pdreq_dw_; @@ -104,39 +104,30 @@ class CLIClient : public TimerHandler { static constexpr Sec unavail_controller_delay = missed_controller_delay + lost_controller_delay; struct ControllerInfo { - enum class Status { - AVAILABLE, - UNAVAILABLE, - }; - tms::DeviceInfo info; - Status status; TimePoint last_hb; }; mutable std::mutex cli_m_; bool stop_cli_; + // Mutex for the following data mutable std::mutex data_m_; - // Microgrid controllers to which the CLI client is connected + // Microgrid controllers that are (and were) reachable by the CLI std::unordered_map controllers_; - // The power devices that are connected to the current controller + // For each controller, cache the power devices that select it as the active controller. + // This mapping can change during the operation of the microgrid due to the availability of the MCs. + std::unordered_map mc_to_devices_; + + // All power devices in the microgrid, reported by the MCs. PowerDevices power_devices_; // Store the simulated power connections between power devices - using PowerConnection = std::unordered_map>; + using PowerConnection = std::unordered_map>; PowerConnection power_connections_; - - // The current microgrid controller with which the CLI client is interacting - tms::Identity curr_controller_; - - mutable std::mutex active_controllers_m_; - - // Active controller selected by each power device (power device => its controller). - // Can be used to check that all power devices will eventually select the same active controller. - std::map active_controllers_; }; #endif diff --git a/tactical-microgrid-standard/cli_idl/CLICommands.idl b/tactical-microgrid-standard/cli_idl/CLICommands.idl index 31ec9d9..17b4e42 100644 --- a/tactical-microgrid-standard/cli_idl/CLICommands.idl +++ b/tactical-microgrid-standard/cli_idl/CLICommands.idl @@ -13,10 +13,13 @@ module cli { }; @nested - @extensibility(FINAL) + @extensibility(APPENDABLE) struct PowerDeviceInfo { tms::DeviceInfo device_info; tms::EnergyStartStopLevel essl; + // The MC that controls this device. + // Obtain from tms::ActiveMicrogridControllerState sent by the device. + @optional tms::Identity master_id; }; typedef sequence PowerDeviceInfoSeq; diff --git a/tactical-microgrid-standard/common/ControllerSelector.cpp b/tactical-microgrid-standard/common/ControllerSelector.cpp index 8510364..83208b4 100644 --- a/tactical-microgrid-standard/common/ControllerSelector.cpp +++ b/tactical-microgrid-standard/common/ControllerSelector.cpp @@ -50,24 +50,20 @@ void ControllerSelector::timer_fired(Timer& timer) Guard g(lock_); const auto& mc_id = timer.arg.id; ACE_DEBUG((LM_INFO, "(%P|%t) INFO: ControllerSelector::timed_event(NewController): " - "\"%C\" -> \"%C\"\n", selected_.c_str(), mc_id.c_str())); + "triggered by \"%C\" heartbeat\n", mc_id.c_str())); - // The TMS spec isn't clear to whether the device needs to verify that the last - // heartbeat of this controller was received less than 3s (i.e., heartbeat deadline) ago. - // This check makes sense since if its last heartbeat was more than 3s ago, that means - // the controller is not available and should not be selected as the active controller. - const TimePoint now = Clock::now(); - auto it = all_controllers_.find(mc_id); - if (it == all_controllers_.end()) { + if (all_controllers_.find(mc_id) == all_controllers_.end()) { ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: ControllerSelector::timed_event(NewController): Controller \"%C\" not found!\n", mc_id.c_str())); return; } - if (now - it->second < heartbeat_deadline) { - selected_ = mc_id; - send_controller_state(); - } + // Select the highest priority controller from the list of available controllers. + // The TMS spec doesn't specify this, and an implementation may select the controller + // associated with this timer. However, that could cause different controllers to be + // selected by different devices, since each device may receive the first heartbeat from + // a different controller and thus select a different controller when the timer expires. + select_controller(); } void ControllerSelector::timer_fired(Timer& timer) @@ -102,14 +98,7 @@ void ControllerSelector::timer_fired(Timer&) // Select a new controller if possible. If there are no recent controllers // and we don't hear from another, the NoControllers timer will fire. - const TimePoint now = Clock::now(); - for (auto it = all_controllers_.begin(); it != all_controllers_.end(); ++it) { - const auto last_hb = now - it->second; - if (last_hb < heartbeat_deadline) { - select(it->first, std::chrono::duration_cast(last_hb)); - break; - } - } + select_controller(); } void ControllerSelector::timer_fired(Timer&) @@ -119,6 +108,25 @@ void ControllerSelector::timer_fired(Timer&) // TODO: CONFIG_ON_COMMS_LOSS } +// Select a new controller at start up, +// or when a device loses its active controller and has to select a new one. +bool ControllerSelector::select_controller() +{ + const TimePoint now = Clock::now(); + + // Select an available controller with smallest identity alphabetically + for (auto it = all_controllers_.begin(); it != all_controllers_.end(); ++it) { + const auto last_hb = now - it->second; + // TMS spec doesn't specify this. But it should make sure the controller is still available + // i.e., last heartbeat received within 3 seconds. + if (last_hb < heartbeat_deadline) { + select(it->first, std::chrono::duration_cast(last_hb)); + return true; + } + } + return false; +} + void ControllerSelector::select(const tms::Identity& id, Sec last_hb) { ACE_DEBUG((LM_INFO, "(%P|%t) INFO: ControllerSelector::select: \"%C\"\n", id.c_str())); diff --git a/tactical-microgrid-standard/common/ControllerSelector.h b/tactical-microgrid-standard/common/ControllerSelector.h index 8eca3d9..f000099 100644 --- a/tactical-microgrid-standard/common/ControllerSelector.h +++ b/tactical-microgrid-standard/common/ControllerSelector.h @@ -103,6 +103,7 @@ class OpenDDS_TMS_Export ControllerSelector : void select(const tms::Identity& id, Sec last_hb = Sec(0)); + bool select_controller(); void send_controller_state(); tms::Identity selected_; diff --git a/tactical-microgrid-standard/cli/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp b/tactical-microgrid-standard/controller/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp similarity index 94% rename from tactical-microgrid-standard/cli/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp rename to tactical-microgrid-standard/controller/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp index 91fe6dc..5e36063 100644 --- a/tactical-microgrid-standard/cli/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp +++ b/tactical-microgrid-standard/controller/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp @@ -17,7 +17,7 @@ void ActiveMicrogridControllerStateDataReaderListenerImpl::on_data_available(DDS if (info_seq[i].valid_data) { const tms::Identity& device_id = data[i].deviceId(); auto master_id = data[i].masterId(); - cli_client_.set_active_controller(device_id, master_id); + controller_.set_active_controller(device_id, master_id); } } } diff --git a/tactical-microgrid-standard/cli/ActiveMicrogridControllerStateDataReaderListenerImpl.h b/tactical-microgrid-standard/controller/ActiveMicrogridControllerStateDataReaderListenerImpl.h similarity index 64% rename from tactical-microgrid-standard/cli/ActiveMicrogridControllerStateDataReaderListenerImpl.h rename to tactical-microgrid-standard/controller/ActiveMicrogridControllerStateDataReaderListenerImpl.h index fc65746..d58560f 100644 --- a/tactical-microgrid-standard/cli/ActiveMicrogridControllerStateDataReaderListenerImpl.h +++ b/tactical-microgrid-standard/controller/ActiveMicrogridControllerStateDataReaderListenerImpl.h @@ -1,21 +1,21 @@ -#ifndef CLI_ACTIVE_MICROGRID_CONTROLLER_STATE_DATA_READER_LISTENER_IMPL_H -#define CLI_ACTIVE_MICROGRID_CONTROLLER_STATE_DATA_READER_LISTENER_IMPL_H +#ifndef ACTIVE_MICROGRID_CONTROLLER_STATE_DATA_READER_LISTENER_IMPL_H +#define ACTIVE_MICROGRID_CONTROLLER_STATE_DATA_READER_LISTENER_IMPL_H #include "common/DataReaderListenerBase.h" -#include "CLIClient.h" +#include "Controller.h" class ActiveMicrogridControllerStateDataReaderListenerImpl : public DataReaderListenerBase { public: - explicit ActiveMicrogridControllerStateDataReaderListenerImpl(CLIClient& cli_client) + explicit ActiveMicrogridControllerStateDataReaderListenerImpl(Controller& controller) : DataReaderListenerBase("tms::ActiveMicrogridControllerState - DataReaderListenerImpl") - , cli_client_(cli_client) {} + , controller_(controller) {} virtual ~ActiveMicrogridControllerStateDataReaderListenerImpl() = default; void on_data_available(DDS::DataReader_ptr reader) final; private: - CLIClient& cli_client_; + Controller& controller_; }; #endif diff --git a/tactical-microgrid-standard/controller/CLIServer.cpp b/tactical-microgrid-standard/controller/CLIServer.cpp index 4805539..0a10177 100644 --- a/tactical-microgrid-standard/controller/CLIServer.cpp +++ b/tactical-microgrid-standard/controller/CLIServer.cpp @@ -384,6 +384,14 @@ void CLIServer::start_stop_device(const tms::Identity& pd_id, tms::OperatorPrior return; } + // Only update the energy level of the device locally if this is not its active controller. + const auto& master_id = it->second.master_id(); + if (!master_id.has_value() || master_id.value() != controller_.get_device_id()) { + controller_.update_essl(pd_id, to_essl); + return; + } + + // Else, send a command to the device to actually start/stop it. tms::EnergyStartStopRequest essr; essr.requestId().requestingDeviceId(controller_.id()); essr.requestId().targetDeviceId(pd_id); diff --git a/tactical-microgrid-standard/controller/Common.h b/tactical-microgrid-standard/controller/Common.h index 607cc8f..a847a5f 100644 --- a/tactical-microgrid-standard/controller/Common.h +++ b/tactical-microgrid-standard/controller/Common.h @@ -4,11 +4,10 @@ #include #include -#include - #include +#include -using OpArgPair = std::pair>; +using OpArgPair = std::pair>; using PowerDevices = std::unordered_map; #endif diff --git a/tactical-microgrid-standard/controller/Controller.cpp b/tactical-microgrid-standard/controller/Controller.cpp index bc07b4c..955d329 100644 --- a/tactical-microgrid-standard/controller/Controller.cpp +++ b/tactical-microgrid-standard/controller/Controller.cpp @@ -1,5 +1,9 @@ #include "Controller.h" +#include "ActiveMicrogridControllerStateDataReaderListenerImpl.h" #include "common/Utils.h" +#include "common/QosHelper.h" + +#include DDS::ReturnCode_t Controller::init(DDS::DomainId_t domain_id, int argc, char* argv[]) { @@ -20,6 +24,48 @@ DDS::ReturnCode_t Controller::init(DDS::DomainId_t domain_id, int argc, char* ar return rc; } + DDS::DomainParticipant_var dp = get_domain_participant(); + + // Subscribe to the tms::ActiveMicrogridControllerState topic + tms::ActiveMicrogridControllerStateTypeSupport_var amcs_ts = new tms::ActiveMicrogridControllerStateTypeSupportImpl; + if (DDS::RETCODE_OK != amcs_ts->register_type(dp, "")) { + ACE_ERROR((LM_ERROR, "(%P|%t) CLIClient::init: register_type ActiveMicrogridControllerState failed\n")); + return DDS::RETCODE_ERROR; + } + + CORBA::String_var amcs_type_name = amcs_ts->get_type_name(); + DDS::Topic_var amcs_topic = dp->create_topic(tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE.c_str(), + amcs_type_name, + TOPIC_QOS_DEFAULT, + nullptr, + ::OpenDDS::DCPS::DEFAULT_STATUS_MASK); + if (!amcs_topic) { + ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::init: create_topic \"%C\" failed\n", + tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE.c_str())); + return DDS::RETCODE_ERROR; + } + + const DDS::SubscriberQos tms_sub_qos = Qos::Subscriber::get_qos(); + DDS::Subscriber_var tms_sub = dp->create_subscriber(tms_sub_qos, + nullptr, + ::OpenDDS::DCPS::DEFAULT_STATUS_MASK); + if (!tms_sub) { + ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::init: create_subscriber with TMS QoS failed\n")); + return DDS::RETCODE_ERROR; + } + + const DDS::DataReaderQos& amcs_dr_qos = Qos::DataReader::fn_map.at(tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE)(device_id_); + DDS::DataReaderListener_var amcs_listener(new ActiveMicrogridControllerStateDataReaderListenerImpl(*this)); + DDS::DataReader_var amcs_dr_base = tms_sub->create_datareader(amcs_topic, + amcs_dr_qos, + amcs_listener, + ::OpenDDS::DCPS::DEFAULT_STATUS_MASK); + if (!amcs_dr_base) { + ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: CLIClient::init: create_datareader for topic \"%C\" failed\n", + tms::topic::TOPIC_ACTIVE_MICROGRID_CONTROLLER_STATE.c_str())); + return DDS::RETCODE_ERROR; + } + auto di = populate_device_info(); tms_domain_id_ = domain_id; @@ -65,8 +111,15 @@ void Controller::device_info_cb(const tms::DeviceInfo& di, const DDS::SampleInfo } ACE_DEBUG((LM_INFO, "(%P|%t) INFO: Controller::device_info_cb: device: \"%C\"\n", di.deviceId().c_str())); - power_devices_.insert(std::make_pair(di.deviceId(), - cli::PowerDeviceInfo(di, tms::EnergyStartStopLevel::ESSL_OPERATIONAL))); + + // Ignore other control devices, such as microgrid controllers. + // Store all power devices, including those that select a different MC as its active MC. + if (di.role() != tms::DeviceRole::ROLE_MICROGRID_CONTROLLER) { + std::lock_guard guard(mut_); + power_devices_.insert(std::make_pair(di.deviceId(), + cli::PowerDeviceInfo(di, tms::EnergyStartStopLevel::ESSL_OPERATIONAL, + std::optional()))); + } } void Controller::heartbeat_cb(const tms::Heartbeat& hb, const DDS::SampleInfo& si) @@ -79,14 +132,24 @@ void Controller::heartbeat_cb(const tms::Heartbeat& hb, const DDS::SampleInfo& s const uint32_t seqnum = hb.sequenceNumber(); if (OpenDDS::DCPS::DCPS_debug_level >= 8) { + std::lock_guard guard(mut_); if (power_devices_.count(id) > 0) { ACE_DEBUG((LM_INFO, "(%P|%t) INFO: Controller::heartbeat_cb: known device: \"%C\", seqnum: %u\n", id.c_str(), seqnum)); } else { - ACE_DEBUG((LM_INFO, "(%P|%t) INFO: Controller::heartbeat_cb: new device: \"%C\", seqnum: %u\n", id.c_str(), seqnum)); + ACE_DEBUG((LM_INFO, "(%P|%t) INFO: Controller::heartbeat_cb: unknown device: \"%C\", seqnum: %u\n", id.c_str(), seqnum)); } } } +void Controller::set_active_controller(const tms::Identity& pd_id, const std::optional& master_id) +{ + std::lock_guard guard(mut_); + auto it = power_devices_.find(pd_id); + if (it != power_devices_.end()) { + it->second.master_id() = master_id; + } +} + tms::DeviceInfo Controller::populate_device_info() const { auto device_info = get_device_info(); diff --git a/tactical-microgrid-standard/controller/Controller.h b/tactical-microgrid-standard/controller/Controller.h index eeef828..f2c5bdb 100644 --- a/tactical-microgrid-standard/controller/Controller.h +++ b/tactical-microgrid-standard/controller/Controller.h @@ -20,6 +20,8 @@ class Controller : public Handshaking { return tms_domain_id_; } + void set_active_controller(const tms::Identity& pd_id, const std::optional& master_id); + private: void device_info_cb(const tms::DeviceInfo& di, const DDS::SampleInfo& si); void heartbeat_cb(const tms::Heartbeat& hb, const DDS::SampleInfo& si); diff --git a/tactical-microgrid-standard/controller/PowerDevicesRequestDataReaderListenerImpl.cpp b/tactical-microgrid-standard/controller/PowerDevicesRequestDataReaderListenerImpl.cpp index 0c861ac..78038ee 100644 --- a/tactical-microgrid-standard/controller/PowerDevicesRequestDataReaderListenerImpl.cpp +++ b/tactical-microgrid-standard/controller/PowerDevicesRequestDataReaderListenerImpl.cpp @@ -19,8 +19,10 @@ void PowerDevicesRequestDataReaderListenerImpl::on_data_available(DDS::DataReade bool my_request = false; for (CORBA::ULong i = 0; i < data.length(); ++i) { if (data[i].mc_id() != id) { - ACE_DEBUG((LM_INFO, "(%P|%t) INFO: PowerDevicesRequestDataReaderListenerImpl::on_data_available: " - " Received request for different controller with Id: %C\n", data[i].mc_id().c_str())); + if (OpenDDS::DCPS::DCPS_debug_level >= 8) { + ACE_DEBUG((LM_INFO, "(%P|%t) INFO: PowerDevicesRequestDataReaderListenerImpl::on_data_available: " + " Received request for different controller with Id: %C\n", data[i].mc_id().c_str())); + } continue; } @@ -40,9 +42,15 @@ void PowerDevicesRequestDataReaderListenerImpl::on_data_available(DDS::DataReade reply.mc_id(id); const size_t num_devices = power_devices.size(); cli::PowerDeviceInfoSeq& pdi_seq = reply.devices(); - pdi_seq.reserve(num_devices); + + // Only reply with the power devices that: + // - have selected this MC as its active MC, or + // - haven't selected an active MC yet. for (auto it = power_devices.begin(); it != power_devices.end(); ++it) { - pdi_seq.push_back(it->second); + const auto selected_mc = it->second.master_id(); + if (!selected_mc.has_value() || selected_mc.value() == id) { + pdi_seq.push_back(it->second); + } } cli::PowerDevicesReplyDataWriter_var pdreply_writer = cli_server_.get_PowerDevicesReply_writer(); diff --git a/tactical-microgrid-standard/controller/PowerTopologyDataReaderListenerImpl.cpp b/tactical-microgrid-standard/controller/PowerTopologyDataReaderListenerImpl.cpp index 99adbb2..030277d 100644 --- a/tactical-microgrid-standard/controller/PowerTopologyDataReaderListenerImpl.cpp +++ b/tactical-microgrid-standard/controller/PowerTopologyDataReaderListenerImpl.cpp @@ -35,4 +35,4 @@ void PowerTopologyDataReaderListenerImpl::on_data_available(DDS::DataReader_ptr break; } } -} \ No newline at end of file +} diff --git a/tactical-microgrid-standard/tests/mc-sel/CMakeLists.txt b/tactical-microgrid-standard/tests/mc-sel/CMakeLists.txt index 543fa6e..4eb9448 100644 --- a/tactical-microgrid-standard/tests/mc-sel/CMakeLists.txt +++ b/tactical-microgrid-standard/tests/mc-sel/CMakeLists.txt @@ -9,7 +9,10 @@ include(opendds_testing) add_executable(mc-sel-test mc-sel-test.cpp) target_link_libraries(mc-sel-test PRIVATE PowerSim_Idl) -add_executable(basic-mc ${CMAKE_SOURCE_DIR}/controller/Controller.cpp basic-mc.cpp) +add_executable(basic-mc + ${CMAKE_SOURCE_DIR}/controller/Controller.cpp + ${CMAKE_SOURCE_DIR}/controller/ActiveMicrogridControllerStateDataReaderListenerImpl.cpp + basic-mc.cpp) target_link_libraries(basic-mc PRIVATE Commands_Idl) opendds_add_test(COMMAND ./mc-sel-test)