diff --git a/CMakeLists.txt b/CMakeLists.txt index 044b9c0d8c9dc3adb125ddb46b8bfd6ace9e6b9b..1be913a658697eb33f7285d291d8cea22ac23714 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ add_custom_target(check add_library(common OBJECT src/Service.cpp src/ErrorHandler.cpp + src/Logger.cpp src/Message.cpp src/MessageParser.cpp src/ServicePool.cpp @@ -39,10 +40,11 @@ add_library(common OBJECT # Specify the .cpp files for the executables file(GLOB x86_main_SRC "src/Platform/x86/*.cpp") add_executable(ecss_services - src/main.cpp $<TARGET_OBJECTS:common> ${x86_main_SRC} ) +set_target_properties(ecss_services + PROPERTIES COMPILE_DEFINITIONS LOGLEVEL_TRACE) IF (EXISTS "${PROJECT_SOURCE_DIR}/lib/Catch2/CMakeLists.txt") # Gather all the .cpp files corresponding to tests diff --git a/ci/cppcheck-misra.sh b/ci/cppcheck-misra.sh index 737eb6f217aeb121635dd5cf225b4d3121f1c7e9..b1313b80c7e7cee69cd69e42c85dd2fee6424223 100755 --- a/ci/cppcheck-misra.sh +++ b/ci/cppcheck-misra.sh @@ -11,9 +11,12 @@ cd "$(dirname "$0")/.." echo -e "\u001b[34;1mGetting prerequisites...\u001b[0m" +# get the current cppcheck version +CPCVERSION=`cppcheck --version | awk '{print $2}'` + # grab the MISRA addon and the cppcheck addon interface from github -curl https://raw.githubusercontent.com/danmar/cppcheck/master/addons/misra.py > ci/misra.py -curl https://raw.githubusercontent.com/danmar/cppcheck/master/addons/cppcheckdata.py > ci/cppcheckdata.py +curl https://raw.githubusercontent.com/danmar/cppcheck/$CPCVERSION/addons/misra.py > ci/misra.py +curl https://raw.githubusercontent.com/danmar/cppcheck/$CPCVERSION/addons/cppcheckdata.py > ci/cppcheckdata.py # generate dump files (XML representations of AST etc.) for all headers, source files etc. echo -e "\u001b[34;1mGenerating dump files...\u001b[0m" @@ -30,8 +33,13 @@ sed -i -r 's/(.*Script.*)|(.*Checking.*)|(.*MISRA.*)|(.*Undefined: .*)|(.* \(-\) # run the summarizer for a nice, clean summary of errors echo -e "\u001b[34;1mSummarizing results...\u001b[0m" python3 ci/summarizer.py --report ci/report.msr --suppress 3.1 5.1 5.2 5.3 12.3 13.4 14.4 15.5 16.3 18.4 18.8 +EXIT_CODE=$? # clean up old files echo -e "\u001b[34;1mRemoving dump files...\u001b[0m" echo > ci/report.msr # clear the report file find inc/ src/ -type f -name "*.dump" | xargs rm -rf + +# finally return the return value of the summarizer.py script +exit $EXIT_CODE + diff --git a/ci/summarizer.py b/ci/summarizer.py index 15e48d03c61bffd4caeb8c43804393abbf00016e..5139df77ab8d09d9c8acf4d90b3cd3471023f3dd 100755 --- a/ci/summarizer.py +++ b/ci/summarizer.py @@ -58,7 +58,7 @@ class Summarizer(object): with open(os.path.abspath(file_name)) as code_file: code_lines = code_file.readlines() # Read the source code file line_of_interest = code_lines[int(violation[0]) - 1] # Get the desired violation line - if line_of_interest.find("// Ignore-MISRA") >= 0: + if line_of_interest.find("// Ignore-MISRA") >= 0 or line_of_interest.find("/* Ignore-MISRA */") >= 0: continue if file_name not in self.violations_map.keys(): diff --git a/ci/vera.sh b/ci/vera.sh index 4481cfb2b2b49d904725a211d4c58b51fe4c4048..2ece81da50bfefdefba08effef4bbadc3f7d415f 100755 --- a/ci/vera.sh +++ b/ci/vera.sh @@ -10,4 +10,5 @@ echo -e "\033[0;34mRunning vera++...\033[0m" cd "$(dirname "$0")/.." -vera++ --error --parameter max-line-length=120 --profile custom `find src inc test -type f -regextype posix-egrep -regex '.*\.(cpp|hpp|c|h)'` +vera++ --error --parameter max-line-length=120 --profile custom \ + `find src inc test -type f -regextype posix-egrep -regex '.*\.(cpp|hpp|c|h)' -not -wholename 'inc/Logger.hpp'` diff --git a/inc/ECSS_Definitions.hpp b/inc/ECSS_Definitions.hpp index 43a363c6cfcdc851364bad9a2a70127081c2a651..32b69187d174171fcaee147d8b5973a3145e3de2 100644 --- a/inc/ECSS_Definitions.hpp +++ b/inc/ECSS_Definitions.hpp @@ -58,4 +58,9 @@ #define ECSS_MAX_DELTA_OF_RELEASE_TIME 60 // release time and the actual release time +/** + * @brief The maximum size of a log message + */ +#define LOGGER_MAX_MESSAGE_SIZE 512 + #endif // ECSS_SERVICES_ECSS_DEFINITIONS_H diff --git a/inc/Logger.hpp b/inc/Logger.hpp new file mode 100644 index 0000000000000000000000000000000000000000..96c2101f84aeefb375400cfa0dbd54bc34f6b7e0 --- /dev/null +++ b/inc/Logger.hpp @@ -0,0 +1,224 @@ +#ifndef ECSS_SERVICES_LOGGER_HPP +#define ECSS_SERVICES_LOGGER_HPP + +#include <cstdint> +#include <etl/String.hpp> +#include <etl/to_string.h> +#include "ECSS_Definitions.hpp" + +#if defined LOGLEVEL_TRACE +#define LOGLEVEL Logger::trace // Ignore-MISRA +#elif defined LOGLEVEL_DEBUG +#define LOGLEVEL Logger::debug // Ignore-MISRA +#elif defined LOGLEVEL_INFO +#define LOGLEVEL Logger::info // Ignore-MISRA +#elif defined LOGLEVEL_NOTICE +#define LOGLEVEL Logger::notice // Ignore-MISRA +#elif defined LOGLEVEL_WARNING +#define LOGLEVEL Logger::warning // Ignore-MISRA +#elif defined LOGLEVEL_ERROR +#define LOGLEVEL Logger::error // Ignore-MISRA +#elif defined LOGLEVEL_EMERGENCY +#define LOGLEVEL Logger::emergency // Ignore-MISRA +#elif !defined LOGLEVEL +#define LOGLEVEL Logger::disabled // Ignore-MISRA +#endif + +#define LOG_TRACE (LOG<Logger::trace>()) ///< @see LOG @relates Logger +#define LOG_DEBUG (LOG<Logger::debug>()) ///< @see LOG @relates Logger +#define LOG_INFO (LOG<Logger::info>()) ///< @see LOG @relates Logger +#define LOG_NOTICE (LOG<Logger::notice>()) ///< @see LOG @relates Logger +#define LOG_WARNING (LOG<Logger::warning>()) ///< @see LOG @relates Logger +#define LOG_ERROR (LOG<Logger::error>()) ///< @see LOG @relates Logger +#define LOG_EMERGENCY (LOG<Logger::emergency>()) ///< @see LOG @relates Logger + +/** + * A logging class for ECSS Services that supports ETL's String and is lightweight enough to be used in embedded + * development. + * + * @note Always use the \ref LOG function and its associated utility macros to log. Do not directly use the Logger + * class. + */ +class Logger { +public: + /** + * No need to instantiate a Logger object for now. + */ + Logger() = delete; + + /** + * The underlying type to be used for values of Logger::LogLevel. + */ + typedef uint8_t LogLevelType; + + /** + * ETL's string format specification, to be used for all logged messages + */ + static etl::format_spec format; + + /** + * Log levels supported by the logger. Each level represents a different severity of the logged Message, + * and messages of lower severities can be filtered on top of more significant ones. + * + * Each severity is tied to a number. The higher the number, the higher the severity. + */ + enum LogLevel : LogLevelType { + trace = 32, ///< Very detailed information, useful for tracking the individual steps of an operation + debug = 64, ///< General debugging information + info = 96, ///< Noteworthy or periodical events + notice = 128, ///< Uncommon but expected events + warning = 160, ///< Unexpected events that do not compromise the operability of a function + error = 192, ///< Unexpected failure of an operation + emergency = 254, ///< Unexpected failure that renders the entire system unusable + disabled = 255, ///< Use this log level to disable logging entirely. No message should be logged as disabled. + }; + + /** + * An empty enum representing a dummy log entry that will not be logged due to an insufficient level. + * + * @internal + */ + enum class NoLogEntry {}; + + /** + * A class that defines a log message. + * + * Instead of using this class, prefer one of the above macros. + * @see LOG + * @internal + */ + struct LogEntry { + String<LOGGER_MAX_MESSAGE_SIZE> message = ""; ///< The current log message itself, starting from a blank slate + LogLevel level; ///< The log level of this message + + explicit LogEntry(LogLevel level); ///< Create a new LogEntry + + /** + * The LogEntry destructor gets called whenever a log message is finalized, and ready to be shown to the + * user. This function is responsible for calling the Logger::log function. + * + * According to the C++ standard, a variable used only within an expression will be immediately destroyed once + * the processing of this expression is over. This allows a syntax such as `LogEntry(...) << "some" << "text"`, + * where the destructor will be called strictly **after** all the `<<` operations have been completed. This + * allows the destructor to send the finalized log entry for further processing. + */ + ~LogEntry(); + + LogEntry(LogEntry const&) = delete; ///< Unimplemented copy constructor + + /** + * Stream operator to append new values to a log record + * + * @tparam T The type of value to append + * @param value The new value to add + * @todo See if noexcept can be added here without triggering warnings + * @return The current Logger::LogEntry where the value has been appended + */ + template <class T> + Logger::LogEntry& operator<<(const T value) { + etl::to_string(value, message, format, true); + + return *this; + } + + Logger::LogEntry& operator<<(const std::string& value); + }; + + /** + * Returns whether a log entry of level \p level is logged, based on the compilation constants + * @param level The level of the log entry + * @return True if the logging is enabled for \p level, false if not + */ + static constexpr bool isLogged(LogLevelType level) { + return static_cast<LogLevelType>(LOGLEVEL) <= level; + } + + /** + * Store a new log message + */ + static void log(LogLevel level, etl::istring & message); +}; + +/** + * Create a stream to log a Message + * + * This functions appends one line to the Logs (which could be printed to screen, transferred via UART or stored for + * later use.) + * + * Examples of usage: + * @code + * LOG<Logger::debug>() << "Reached point of no return"; + * LOG<Logger::error>() << "More than " << 50 << " dogs found!"; + * @endcode + * + * You can also use one of the \ref LOG_TRACE, \ref LOG_DEBUG, \ref LOG_INFO, \ref LOG_NOTICE, \ref LOG_WARNING, + * \ref LOG_ERROR or \ref LOG_EMERGENCY defines, which avoid the need of explicitly passing the log level: + * @code + * LOG_DEBUG << "Reached point of no return"; + * LOG_ERROR << "More than " << 50 << " dogs found!"; + * @endcode + * + * See \ref Logger::LogLevel for an explanation of the different log levels. + * + * @par Implementation details + * Functions here are defined as `constexpr` in order to let them be optimized as soon as possible. The LOG() + * function returns an instance of \ref Logger::LogEntry if the level is high enough to be shown, or an instance of + * \ref Logger::NoLogEntry if the log entry will not be displayed. As this is a templated function, it is acceptable + * to support different return types using the `auto` keyword. + * + * @warning For messages that will not be logged, any calls to functions that contain **side effects will still take + * place**. + * @code + * LOG_DEBUG << "The temperature is: " << getTemperature(); + * @endcode + * In the above example, if `getTemperature()` will cause a side effect (e.g. an I2C connection or a `std::cout` print), + * it will still be executed, even if the debug message will not be printed to the screen due to an insufficient + * LOGLEVEL. + * + * @section GlobalLogLevels Global log levels + * The **global log level** defines the minimum severity of events to be displayed. Log entries with a severity equal + * to or higher than the global log level will be shown. Log entries with a severity smaller than the global log level + * will not be shown. + * + * The global log level can be set by defining one of the following constants: + * - `LOGLEVEL_TRACE` + * - `LOGLEVEL_DEBUG` + * - `LOGLEVEL_INFO` + * - `LOGLEVEL_NOTICE` + * - `LOGLEVEL_WARNING` + * - `LOGLEVEL_ERROR` + * - `LOGLEVEL_EMERGENCY` + * + * @relates Logger + * @tparam level The log level. A value of \ref Logger::LogLevel + * @return Returns \ref Logger::LogEntry if the level is sufficient to be logged, or \ref Logger::NoLogEntry if the + * message will not be logged. This is determined at compile-time. + */ +template <Logger::LogLevel level> +constexpr __attribute__((always_inline)) inline auto LOG() { + if constexpr (Logger::isLogged(level)) { + return Logger::LogEntry(level); + } else { + return Logger::NoLogEntry(); + } +}; + +/** + * A no-op function that considers an empty log entry that will not be displayed, processed or stored. + * + * @warning Note that functions containing **side effects** will get properly executed. Only use functions that return + * plain values as parts of the log function, so they might be optimized away at compile time. + * + * @tparam T The type of the data that will be ignored + * @param noLogEntry A dummy no-op log entry + * @param value The data that will be ignored + * @return A dummy no-op log entry + * @see Logger::LogEntry::operator<<(const T value) + * @relates Logger::LogEntry + */ +template <typename T> +[[maybe_unused]] constexpr Logger::NoLogEntry operator<<(const Logger::NoLogEntry noLogEntry, T value) { + return noLogEntry; +} + +#endif //ECSS_SERVICES_LOGGER_HPP diff --git a/inc/Service.hpp b/inc/Service.hpp index 32e77e19387cd0b74236c082ffc271f3557535b7..d7a8ee4378365c925a90f12ff46f81238e1e5f27 100644 --- a/inc/Service.hpp +++ b/inc/Service.hpp @@ -3,7 +3,6 @@ #include <cstdint> #include "Message.hpp" -#include <iostream> // This file should be removed class ServicePool; @@ -12,8 +11,6 @@ class ServicePool; * * A member of the Service class should be used as a singleton, i.e. must be created only once in * the code - * - * @todo Disable copy constructor */ class Service { private: diff --git a/lib/etl b/lib/etl index f2f23395cde34704686233094d9619ad8d06453a..5a7e181100dae73659db133783ae964c7661437b 160000 --- a/lib/etl +++ b/lib/etl @@ -1 +1 @@ -Subproject commit f2f23395cde34704686233094d9619ad8d06453a +Subproject commit 5a7e181100dae73659db133783ae964c7661437b diff --git a/src/Logger.cpp b/src/Logger.cpp new file mode 100644 index 0000000000000000000000000000000000000000..76e36d59af7a58a5b9c34cd9fa3aca3592fee4b1 --- /dev/null +++ b/src/Logger.cpp @@ -0,0 +1,24 @@ +#include <Logger.hpp> + +etl::format_spec Logger::format; + +// Reimplementation of the function for variable C strings +template <> +Logger::LogEntry& Logger::LogEntry::operator<<(char* value) { + message.append(value); + return *this; +} + +// Reimplementation of the function for C strings +template <> +Logger::LogEntry& Logger::LogEntry::operator<<(const char* value) { + message.append(value); + return *this; +} + +Logger::LogEntry::LogEntry(LogLevel level) : level(level) {} + +Logger::LogEntry::~LogEntry() { + // When the destructor is called, the log message is fully "designed". Now we can finally "display" it to the user. + Logger::log(level, message); +} diff --git a/src/Platform/x86/ErrorHandler.cpp b/src/Platform/x86/ErrorHandler.cpp index a59e36514ddf1a7047ab4e2e2a47f05dfd4cfd89..ffddffd2d0501e688779674251ae1da83aa035d7 100644 --- a/src/Platform/x86/ErrorHandler.cpp +++ b/src/Platform/x86/ErrorHandler.cpp @@ -7,6 +7,7 @@ #include <cxxabi.h> #include <ErrorHandler.hpp> #include <Message.hpp> +#include <Logger.hpp> // TODO: Find a way to reduce the number of copies of this chunk template void ErrorHandler::logError(const Message&, ErrorHandler::AcceptanceErrorType); @@ -18,23 +19,22 @@ template void ErrorHandler::logError(ErrorHandler::InternalErrorType); template <typename ErrorType> void ErrorHandler::logError(const Message& message, ErrorType errorType) { - std::cerr + LOG_ERROR /* * Gets the error class name from the template * Note: This is g++-dependent code and should only be used for debugging. */ << abi::__cxa_demangle(typeid(ErrorType).name(), nullptr, nullptr, nullptr) << " Error " << "[" << static_cast<uint16_t>(message.serviceType) << "," << static_cast<uint16_t>(message.messageType) - << "]: " << errorType << std::endl; + << "]: " << errorType; } template <typename ErrorType> void ErrorHandler::logError(ErrorType errorType) { - std::cerr + LOG_ERROR /* * Gets the error class name from the template * Note: This is g++-dependent code and should only be used for debugging. */ - << abi::__cxa_demangle(typeid(ErrorType).name(), nullptr, nullptr, nullptr) << " Error: " << errorType - << std::endl; + << abi::__cxa_demangle(typeid(ErrorType).name(), nullptr, nullptr, nullptr) << " Error: " << errorType; } diff --git a/src/Platform/x86/Logger.cpp b/src/Platform/x86/Logger.cpp new file mode 100644 index 0000000000000000000000000000000000000000..85d0797656a4831c718e5b64740eb977ddb98c1d --- /dev/null +++ b/src/Platform/x86/Logger.cpp @@ -0,0 +1,68 @@ +#include <Logger.hpp> +#include <etl/String.hpp> +#include <iostream> +#include <ECSS_Definitions.hpp> + +#include <chrono> +#include <iomanip> + +// The implementation of this function appends ANSI codes that should add colours to a compatible terminal +void Logger::log(Logger::LogLevel level, etl::istring & message) { + // Get the current time & date + std::time_t t = std::time(nullptr); + std::tm tm = *std::localtime(&t); + + // Get the log level and its colour + std::string name; + std::string colour; + bool keepColour = false; // Whether to keep the colour in the rest of the message + + if (level <= Logger::trace) { + name = "trace"; + colour = "90"; // bright black + keepColour = true; + } else if (level <= Logger::debug) { + name = "debug"; + colour = "90"; // bright black + } else if (level <= Logger::info) { + name = "info"; + colour = "32"; // green + } else if (level <= Logger::notice) { + name = "notice"; + colour = "36"; // cyan + } else if (level <= Logger::warning) { + name = "warning"; + colour = "33"; // yellow + } else if (level <= Logger::error) { + name = "error"; + colour = "31"; // red + } else { + name = "emergency"; + colour = "31"; // red + keepColour = true; + } + + std::ostringstream ss; // A string stream to create the log message + ss << "\033" "[0;90m" << std::put_time(&tm, "%FT%T%z") << "\033" "[0m "; // The date + ss << "[\033" "[1;" << colour << "m" << std::setfill(' ') << std::setw(7) << std::right // Ignore-MISRA + << name << std::setw(0) << "\033" "[0m] "; // The log level // Ignore-MISRA + + if (keepColour) { + ss << "\033" "[0;" << colour << "m"; // Ignore-MISRA + } + ss << message.c_str(); // The message itself + if (keepColour) { + ss << "\033" "[0m"; + } + + ss << "\n"; + std::cerr << ss.str(); +} + +// Reimplementation of the log function for C++ strings +// This is kept in the Platform files, since we don't want to mess with std::strings in the microcontroller +Logger::LogEntry& Logger::LogEntry::operator<<(const std::string & value) { + message.append(value.c_str()); + + return *this; +} diff --git a/src/Platform/x86/Service.cpp b/src/Platform/x86/Service.cpp index 7c20d2f90bea925cd43754c62a231c25bc2c5648..60786fc650c32da32cb98860886291537e378fce 100644 --- a/src/Platform/x86/Service.cpp +++ b/src/Platform/x86/Service.cpp @@ -1,21 +1,25 @@ #include <iostream> #include <iomanip> +#include <Logger.hpp> #include "Service.hpp" void Service::storeMessage(Message& message) { // appends the remaining bits to complete a byte message.finalize(); + // Create a new stream to display the packet + std::ostringstream ss; + // Just print it to the screen - std::cout << "New " << ((message.packetType == Message::TM) ? "TM" : "TC") << "[" - << std::hex - // << std::dec - << static_cast<int>(message.serviceType) << "," << static_cast<int>(message.messageType) - << "] message!\n"; - // std::cout << std::hex << std::setfill('0') << std::setw(2); - for (int i = 0; i < message.dataSize; i++) { - std::cout << static_cast<int>(message.data[i]); - std::cout << " "; + ss << "New " << ((message.packetType == Message::TM) ? "TM" : "TC") << "[" + << std::hex + << static_cast<int>(message.serviceType) << "," // Ignore-MISRA + << static_cast<int>(message.messageType) // Ignore-MISRA + << "] message! "; + + for (unsigned int i = 0; i < message.dataSize; i++) { + ss << static_cast<int>(message.data[i]) << " "; // Ignore-MISRA } - std::cout << std::endl; + + LOG_DEBUG << ss.str(); } diff --git a/src/main.cpp b/src/Platform/x86/main.cpp similarity index 99% rename from src/main.cpp rename to src/Platform/x86/main.cpp index d317286d26f07da765aaf539f67bda78371783c3..57ad74b65072e65d5b56ada6adafe1bc03a5ca6e 100644 --- a/src/main.cpp +++ b/src/Platform/x86/main.cpp @@ -1,5 +1,6 @@ #include <iostream> #include <ServicePool.hpp> +#include <Logger.hpp> #include "Helpers/CRCHelper.hpp" #include "Helpers/TimeHelper.hpp" #include "Services/TestService.hpp" @@ -20,6 +21,8 @@ #include "etl/String.hpp" int main() { + LOG_NOTICE << "ECSS Services test application"; + Message packet = Message(0, 0, Message::TC, 1); packet.appendString<5>("hello"); @@ -336,5 +339,6 @@ int main() { receivedMsg = Message(11, 12, Message::TC, 1); timeBasedSchedulingService.summaryReportActivitiesByID(receivedMsg); + LOG_NOTICE << "ECSS Services test complete"; return 0; } diff --git a/test/TestPlatform.cpp b/test/TestPlatform.cpp index a4cbee4d07452934aec010d06061b4479cb2d775..beb76de1e0fb393521b3a22fe915de5c1c68f241 100644 --- a/test/TestPlatform.cpp +++ b/test/TestPlatform.cpp @@ -3,6 +3,7 @@ #include <catch2/catch.hpp> #include <Message.hpp> #include <Service.hpp> +#include <Logger.hpp> #include "Services/ServiceTests.hpp" // Explicit template specializations for the logError() function @@ -34,6 +35,10 @@ void ErrorHandler::logError(ErrorType errorType) { ServiceTests::addError(ErrorHandler::findErrorSource(errorType), errorType); } +void Logger::log(Logger::LogLevel level, etl::istring & message) { + // Logs while testing are completely ignored +} + struct ServiceTestsListener : Catch::TestEventListenerBase { using TestEventListenerBase::TestEventListenerBase; // inherit constructor