diff --git a/src/TargetController/CommandManager.hpp b/src/TargetController/CommandManager.hpp new file mode 100644 index 00000000..c16c78ff --- /dev/null +++ b/src/TargetController/CommandManager.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +#include "Commands/Command.hpp" +#include "Responses/Response.hpp" +#include "Responses/Error.hpp" +#include "TargetControllerComponent.hpp" + +#include "src/Exceptions/Exception.hpp" + +#include "src/Logger/Logger.hpp" + +namespace Bloom::TargetController +{ + class CommandManager + { + public: + template + requires + std::is_base_of_v + && std::is_base_of_v + auto sendCommandAndWaitForResponse( + std::unique_ptr command, + std::chrono::milliseconds timeout + ) { + using SuccessResponseType = typename CommandType::SuccessResponseType; + + Logger::debug("Issuing " + CommandType::name + " command to TargetController"); + + const auto commandId = command->id; + TargetControllerComponent::registerCommand(std::move(command)); + + auto optionalResponse = TargetControllerComponent::waitForResponse(commandId, timeout); + + if (!optionalResponse.has_value()) { + Logger::debug( + "Timed out whilst waiting for TargetController to respond to " + CommandType::name + " command" + ); + throw Exceptions::Exception("Command timed out"); + } + + auto& response = optionalResponse.value(); + + if (response->getType() == Responses::ResponseType::ERROR) { + const auto errorResponse = dynamic_cast(response.get()); + + Logger::debug( + "TargetController returned error in response to " + CommandType::name + " command. Error: " + + errorResponse->errorMessage + ); + throw Exceptions::Exception(errorResponse->errorMessage); + } + + // Only downcast if the command's SuccessResponseType is not the generic Response type. + if constexpr (!std::is_same_v) { + assert(response->getType() == SuccessResponseType::type); + return std::unique_ptr( + dynamic_cast(response.release()) + ); + } + + return std::move(response); + } + }; +} diff --git a/src/TargetController/Commands/Command.hpp b/src/TargetController/Commands/Command.hpp new file mode 100644 index 00000000..d50dc667 --- /dev/null +++ b/src/TargetController/Commands/Command.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +#include "CommandTypes.hpp" + +#include "src/TargetController/Responses/Response.hpp" + +namespace Bloom::TargetController::Commands +{ + using CommandIdType = int; + static_assert(std::atomic::is_always_lock_free); + + class Command + { + public: + using SuccessResponseType = Responses::Response; + + CommandIdType id = ++(Command::lastCommandId); + + static constexpr CommandType type = CommandType::GENERIC; + static inline const std::string name = "GenericCommand"; + + Command() = default; + virtual ~Command() = default; + + Command(const Command& other) = default; + Command(Command&& other) = default; + + Command& operator = (const Command& other) = default; + Command& operator = (Command&& other) = default; + + [[nodiscard]] virtual CommandType getType() const { + return Command::type; + } + + private: + static inline std::atomic lastCommandId = 0; + }; +} diff --git a/src/TargetController/Commands/CommandTypes.hpp b/src/TargetController/Commands/CommandTypes.hpp new file mode 100644 index 00000000..762bd950 --- /dev/null +++ b/src/TargetController/Commands/CommandTypes.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace Bloom::TargetController::Commands +{ + enum class CommandType: std::uint8_t + { + GENERIC, + }; +} diff --git a/src/TargetController/Responses/Error.hpp b/src/TargetController/Responses/Error.hpp new file mode 100644 index 00000000..bb5f4560 --- /dev/null +++ b/src/TargetController/Responses/Error.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Response.hpp" + +namespace Bloom::TargetController::Responses +{ + class Error: public Response + { + public: + static constexpr ResponseType type = ResponseType::ERROR; + + std::string errorMessage; + + explicit Error(const std::string& errorMessage) + : errorMessage(errorMessage) + {} + + [[nodiscard]] ResponseType getType() const override { + return Error::type; + } + }; +} diff --git a/src/TargetController/Responses/Response.hpp b/src/TargetController/Responses/Response.hpp new file mode 100644 index 00000000..255e6d56 --- /dev/null +++ b/src/TargetController/Responses/Response.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "ResponseTypes.hpp" + +namespace Bloom::TargetController::Responses +{ + class Response + { + public: + static constexpr ResponseType type = ResponseType::GENERIC; + + Response() = default; + virtual ~Response() = default; + + Response(const Response& other) = default; + Response(Response&& other) = default; + + Response& operator = (const Response& other) = default; + Response& operator = (Response&& other) = default; + + [[nodiscard]] virtual ResponseType getType() const { + return Response::type; + } + }; +} diff --git a/src/TargetController/Responses/ResponseTypes.hpp b/src/TargetController/Responses/ResponseTypes.hpp new file mode 100644 index 00000000..8372d2b3 --- /dev/null +++ b/src/TargetController/Responses/ResponseTypes.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace Bloom::TargetController::Responses +{ + enum class ResponseType: std::uint8_t + { + GENERIC, + ERROR, + }; +} diff --git a/src/TargetController/TargetControllerComponent.cpp b/src/TargetController/TargetControllerComponent.cpp index 221cb932..99638ec1 100644 --- a/src/TargetController/TargetControllerComponent.cpp +++ b/src/TargetController/TargetControllerComponent.cpp @@ -20,6 +20,9 @@ namespace Bloom::TargetController using namespace Bloom::Events; using namespace Bloom::Exceptions; + using Commands::Command; + using Commands::CommandIdType; + using Responses::Response; TargetControllerComponent::TargetControllerComponent( const ProjectConfig& projectConfig, const EnvironmentConfig& environmentConfig @@ -41,7 +44,10 @@ namespace Bloom::TargetController this->fireTargetEvents(); } - this->eventListener->waitAndDispatch(60); + TargetControllerComponent::notifier.waitForNotification(std::chrono::milliseconds(60)); + + this->processQueuedCommands(); + this->eventListener->dispatchCurrentEvents(); } catch (const DeviceFailure& exception) { /* @@ -76,6 +82,48 @@ namespace Bloom::TargetController this->shutdown(); } + void TargetControllerComponent::registerCommand(std::unique_ptr command) { + auto commandQueueLock = TargetControllerComponent::commandQueue.acquireLock(); + TargetControllerComponent::commandQueue.getValue().push(std::move(command)); + TargetControllerComponent::notifier.notify(); + } + + std::optional> TargetControllerComponent::waitForResponse( + CommandIdType commandId, + std::optional timeout + ) { + auto response = std::unique_ptr(nullptr); + + const auto predicate = [commandId, &response] { + auto& responsesByCommandId = TargetControllerComponent::responsesByCommandId.getValue(); + auto responseIt = responsesByCommandId.find(commandId); + + if (responseIt != responsesByCommandId.end()) { + response.swap(responseIt->second); + responsesByCommandId.erase(responseIt); + + return true; + } + + return false; + }; + + auto responsesByCommandIdLock = TargetControllerComponent::responsesByCommandId.acquireLock(); + + if (timeout.has_value()) { + TargetControllerComponent::responsesByCommandIdCv.wait_for( + responsesByCommandIdLock, + timeout.value(), + predicate + ); + + } else { + TargetControllerComponent::responsesByCommandIdCv.wait(responsesByCommandIdLock, predicate); + } + + return (response != nullptr) ? std::optional(std::move(response)) : std::nullopt; + } + void TargetControllerComponent::startup() { this->setName("TC"); Logger::info("Starting TargetController"); @@ -102,6 +150,151 @@ namespace Bloom::TargetController this->resume(); } + std::map< + std::string, + std::function()> + > TargetControllerComponent::getSupportedDebugTools() { + return std::map()>> { + { + "atmel-ice", + [] { + return std::make_unique(); + } + }, + { + "power-debugger", + [] { + return std::make_unique(); + } + }, + { + "snap", + [] { + return std::make_unique(); + } + }, + { + "pickit-4", + [] { + return std::make_unique(); + } + }, + { + "xplained-pro", + [] { + return std::make_unique(); + } + }, + { + "xplained-mini", + [] { + return std::make_unique(); + } + }, + { + "xplained-nano", + [] { + return std::make_unique(); + } + }, + { + "curiosity-nano", + [] { + return std::make_unique(); + } + }, + }; + } + + std::map< + std::string, + std::function()> + > TargetControllerComponent::getSupportedTargets() { + using Avr8TargetDescriptionFile = Targets::Microchip::Avr::Avr8Bit::TargetDescription::TargetDescriptionFile; + + auto mapping = std::map()>>({ + { + "avr8", + [] { + return std::make_unique(); + } + }, + }); + + // Include all targets from AVR8 target description files + auto avr8PdMapping = Avr8TargetDescriptionFile::getTargetDescriptionMapping(); + + for (auto mapIt = avr8PdMapping.begin(); mapIt != avr8PdMapping.end(); mapIt++) { + // Each target signature maps to an array of targets, as numerous targets can possess the same signature. + auto targets = mapIt.value().toArray(); + + for (auto targetIt = targets.begin(); targetIt != targets.end(); targetIt++) { + auto targetName = targetIt->toObject().find("targetName").value().toString() + .toLower().toStdString(); + auto targetSignatureHex = mapIt.key().toLower().toStdString(); + + if (!mapping.contains(targetName)) { + mapping.insert({ + targetName, + [targetName, targetSignatureHex] { + return std::make_unique( + targetName, + Targets::Microchip::Avr::TargetSignature(targetSignatureHex) + ); + } + }); + } + } + } + + return mapping; + } + + void TargetControllerComponent::processQueuedCommands() { + auto commands = std::queue>(); + + { + auto queueLock = TargetControllerComponent::commandQueue.acquireLock(); + commands.swap(TargetControllerComponent::commandQueue.getValue()); + } + + while (!commands.empty()) { + const auto command = std::move(commands.front()); + commands.pop(); + + const auto commandId = command->id; + const auto commandType = command->getType(); + + try { + if (!this->commandHandlersByCommandType.contains(commandType)) { + throw Exception("No handler registered for this command."); + } + + this->registerCommandResponse( + commandId, + this->commandHandlersByCommandType.at(commandType)(*(command.get())) + ); + + } catch (const Exception& exception) { + this->registerCommandResponse( + commandId, + std::make_unique(exception.getMessage()) + ); + } + } + } + + void TargetControllerComponent::registerCommandResponse( + CommandIdType commandId, + std::unique_ptr response + ) { + auto responseMappingLock = TargetControllerComponent::responsesByCommandId.acquireLock(); + TargetControllerComponent::responsesByCommandId.getValue().insert( + std::pair(commandId, std::move(response)) + ); + TargetControllerComponent::responsesByCommandIdCv.notify_all(); + } + void TargetControllerComponent::checkUdevRules() { auto bloomRulesPath = std::string("/etc/udev/rules.d/99-bloom.rules"); auto latestBloomRulesPath = Paths::resourcesDirPath() + "/UDevRules/99-bloom.rules"; @@ -283,8 +476,8 @@ namespace Bloom::TargetController auto debugToolName = this->environmentConfig.debugToolConfig.name; auto targetName = this->environmentConfig.targetConfig.name; - auto supportedDebugTools = TargetControllerComponent::getSupportedDebugTools(); - auto supportedTargets = TargetControllerComponent::getSupportedTargets(); + static auto supportedDebugTools = this->getSupportedDebugTools(); + static auto supportedTargets = this->getSupportedTargets(); if (!supportedDebugTools.contains(debugToolName)) { throw Exceptions::InvalidConfig( diff --git a/src/TargetController/TargetControllerComponent.hpp b/src/TargetController/TargetControllerComponent.hpp index e465b780..d2937bf1 100644 --- a/src/TargetController/TargetControllerComponent.hpp +++ b/src/TargetController/TargetControllerComponent.hpp @@ -1,6 +1,10 @@ #pragma once #include +#include +#include +#include +#include #include #include #include @@ -8,6 +12,15 @@ #include #include "src/Helpers/Thread.hpp" +#include "src/Helpers/SyncSafe.hpp" +#include "src/Helpers/ConditionVariableNotifier.hpp" + +// Commands +#include "Commands/Command.hpp" + +// Responses +#include "Responses/Response.hpp" + #include "TargetControllerState.hpp" #include "src/DebugToolDrivers/DebugTools.hpp" @@ -44,7 +57,24 @@ namespace Bloom::TargetController */ void run(); + static void registerCommand(std::unique_ptr command); + + static std::optional> waitForResponse( + Commands::CommandIdType commandId, + std::optional timeout = std::nullopt + ); + private: + static inline SyncSafe< + std::queue> + > commandQueue; + + static inline SyncSafe< + std::map> + > responsesByCommandId; + + static inline ConditionVariableNotifier notifier = ConditionVariableNotifier(); + static inline std::condition_variable responsesByCommandIdCv = std::condition_variable(); /** * The TC starts off in a suspended state. TargetController::resume() is invoked from the startup routine. */ @@ -61,6 +91,11 @@ namespace Bloom::TargetController std::unique_ptr target = nullptr; std::unique_ptr debugTool = nullptr; + std::map< + Commands::CommandType, + std::function(Commands::Command&)> + > commandHandlersByCommandType; + EventListenerPointer eventListener = std::make_shared("TargetControllerEventListener"); /** @@ -85,116 +120,14 @@ namespace Bloom::TargetController */ std::map registerAddressRangeByMemoryType; - /** - * Constructs a mapping of supported debug tool names to lambdas. The lambdas should *only* instantiate - * and return an instance to the derived DebugTool class. They should not attempt to establish - * a connection to the device. - * - * @return - */ - static auto getSupportedDebugTools() { - static auto mapping = std::map()>> { - { - "atmel-ice", - [] { - return std::make_unique(); - } - }, - { - "power-debugger", - [] { - return std::make_unique(); - } - }, - { - "snap", - [] { - return std::make_unique(); - } - }, - { - "pickit-4", - [] { - return std::make_unique(); - } - }, - { - "xplained-pro", - [] { - return std::make_unique(); - } - }, - { - "xplained-mini", - [] { - return std::make_unique(); - } - }, - { - "xplained-nano", - [] { - return std::make_unique(); - } - }, - { - "curiosity-nano", - [] { - return std::make_unique(); - } - }, + template + void registerCommandHandler(std::function(CommandType&)> callback) { + auto parentCallback = [callback] (Commands::Command& command) { + // Downcast the command to the expected type + return callback(dynamic_cast(command)); }; - return mapping; - } - - /** - * Constructs a mapping of supported target names to lambdas. The lambdas should instantiate and return an - * instance to the appropriate Target class. - * - * @return - */ - static auto getSupportedTargets() { - static auto mapping = std::map()>>(); - - if (mapping.empty()) { - mapping = { - { - "avr8", - [] { - return std::make_unique(); - } - }, - }; - - // Include all targets from AVR8 target description files - auto avr8PdMapping = - Targets::Microchip::Avr::Avr8Bit::TargetDescription::TargetDescriptionFile::getTargetDescriptionMapping(); - - for (auto mapIt = avr8PdMapping.begin(); mapIt != avr8PdMapping.end(); mapIt++) { - // Each target signature maps to an array of targets, as numerous targets can possess the same signature. - auto targets = mapIt.value().toArray(); - - for (auto targetIt = targets.begin(); targetIt != targets.end(); targetIt++) { - auto targetName = targetIt->toObject().find("targetName").value().toString() - .toLower().toStdString(); - auto targetSignatureHex = mapIt.key().toLower().toStdString(); - - if (!mapping.contains(targetName)) { - mapping.insert({ - targetName, - [targetName, targetSignatureHex] { - return std::make_unique( - targetName, - Targets::Microchip::Avr::TargetSignature(targetSignatureHex) - ); - } - }); - } - } - } - } - - return mapping; + this->commandHandlersByCommandType.insert(std::pair(CommandType::type, parentCallback)); } /** @@ -215,6 +148,27 @@ namespace Bloom::TargetController */ void startup(); + /** + * Constructs a mapping of supported debug tool names to lambdas. The lambdas should *only* instantiate + * and return an instance to the derived DebugTool class. They should not attempt to establish + * a connection to the device. + * + * @return + */ + std::map()>> getSupportedDebugTools(); + + /** + * Constructs a mapping of supported target names to lambdas. The lambdas should instantiate and return an + * instance to the appropriate Target class. + * + * @return + */ + std::map()>> getSupportedTargets(); + + void processQueuedCommands(); + + void registerCommandResponse(Commands::CommandIdType commandId, std::unique_ptr response); + /** * Installs Bloom's udev rules on user's machine. Rules are copied from build/Distribution/Resources/UdevRules * to /etc/udev/rules.d/. This method will report an error if Bloom isn't running as root (as root privileges diff --git a/src/TargetController/TargetControllerConsole.hpp b/src/TargetController/TargetControllerConsole.hpp index eb889f87..67b0fced 100644 --- a/src/TargetController/TargetControllerConsole.hpp +++ b/src/TargetController/TargetControllerConsole.hpp @@ -3,6 +3,7 @@ #include #include +#include "CommandManager.hpp" #include "TargetControllerState.hpp" #include "src/EventManager/EventListener.hpp" @@ -171,6 +172,7 @@ namespace Bloom::TargetController void resetTarget(); private: + CommandManager commandManager = CommandManager(); EventListener& eventListener; std::chrono::milliseconds defaultTimeout = std::chrono::milliseconds(20000);