diff --git a/README.md b/README.md index e310270..7bb0a8f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,60 @@ # balloon-obc-401 A flexible on-board-computer for high altitude balloons based on the dual core STM32H7 series running FreeRTOS. -## Setup / Build Instructions -1. Windows -> ARM cross toolchain available [here](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads). -2. Run `git submodule update --init --remote --recursive` -3. `cmake -DCMAKE_C_COMPILER=... -DCMAKE_CXX_COMPILER=... -Bbuild .` -4. `cmake --build build --config Debug --target all` -5. Profit +## Getting Started +### Dependencies +Before starting you require the following: +- [ARM Toolchain](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads) +- [CMake](https://cmake.org/download/) +- [Clang](https://releases.llvm.org/download.html) +- [STLINK](https://github.com/stlink-org/stlink) +- [STM32CubeMX](https://www.st.com/content/st_com/en/stm32cubemx.html) + +```sh +# Arch Installation +sudo pacman -Syu gcc-arm-none-eabi gcc-arm-none-eabi-newlib clang cmake stlink +paru -S stm32cubemx +``` + +This repository uses Git submodules for software libraries. The submodules can be downloaded with: +```sh +git submodule update --init --remote --recursive +``` + +### Building +All commands should be run from the root directory of the project. To build everything run: +```sh +cmake -DCMAKE_C_COMPILER=[C_COMPILER] -DCMAKE_CXX_COMPILER=[CPP_COMPILER] -Bbuild . +cmake --build build --config Debug --target all +``` +`[C_COMPILER]` and `[CPP_COMPILER]` must be replaced with with the compiler which corresponds to the hardware being targetted. For local testing this is `gcc` and `g++`, for deployment to STM32 this is `gcc-arm-none-gcc` and `gcc-arm-none-g++`. + +### Local Testing +Units tests are run with GTest. To run all tests use CTest (a part of CMake). +```sh +cd build +ctest +``` + +To debug a test which is failing with GDB the following procedure can be used. +```sh +# Find the command used to run the test to debug, replace [NAME_OF_TEST] with the name or a regex with matches it +ctest -R $[NAME_OF_TEST] -V -N +# Run GDB with the test command eg. gdb obc_m7 "-v" "test0" +gdb [TEST_COMMAND] +``` + +### Device Testing +Debugging is done with GDB. +```sh +# In a secondary terminal +st-util + +# In the primary terminal +arm-none-eabi-gdb build/obc_m7.elf +target extended localhost:4242 +load +``` ## Multithreading Given the low level nature of embedded development, we do not have the luxury of symmetric multiprocessing. diff --git a/common/Inc/obc/bus.hpp b/common/Inc/obc/bus.hpp index fe19f8c..0bf993b 100644 --- a/common/Inc/obc/bus.hpp +++ b/common/Inc/obc/bus.hpp @@ -24,10 +24,12 @@ #include #include #include +#include #include #include #include "obc/ipc/callback.hpp" +#include "obc/utils/error.hpp" #include "obc/utils/handle.hpp" /** @@ -157,9 +159,14 @@ template concept ListenBus = requires(T& bus, ipc::Callback::DummyProvider&& cb) { typename T::ListenHandle; + typename T::ListenError; - { bus.Listen(cb.Func) } -> std::same_as; - } && std::semiregular && Message; + { + bus.Listen(cb.Func) + } -> std::same_as< + std::expected>; + } && std::semiregular && + utils::MaybeError && Message; /** * @brief A helper class for implementing listening functionality. @@ -177,6 +184,7 @@ class ListenBusMixin { public: using ListenHandle = utils::Handle; + using ListenError = utils::Never; /** * @brief Adds a listener to be notified upon receiving a message. @@ -185,7 +193,7 @@ class ListenBusMixin { * @return An opaque handle that must be retained for the listener to remain * active. */ - ListenHandle Listen(ListenCallback&& cb) { + std::expected Listen(ListenCallback&& cb) { return ListenHandle(m_listeners, std::move(cb)); } @@ -226,12 +234,15 @@ concept RequestBus = ipc::Callback::DummyProvider cb, Buf& buf ) { typename T::RequestHandle; + typename T::RequestError; { bus.Request(req, cb.Func, buf) - } -> std::same_as; + } -> std::same_as< + std::expected>; } && - std::semiregular && Message && + std::semiregular && + utils::MaybeError && Message && Message && Buffer; /** @@ -261,7 +272,7 @@ template< typename T, typename Req = BasicMessage, typename Res = BasicMessage, typename Flt = ipc::Callback> concept RequestBusMixinRequirements = requires(T& bus, const Req& req) { - { bus.IssueRequest(req) } -> std::same_as; + { bus.IssueRequest(req) } -> utils::ExpectedReturn; } && Message && Message && MessageFilter; /** @@ -290,6 +301,9 @@ class RequestBusMixin { public: using RequestHandle = utils::Handle; + using RequestError = decltype(Derived().IssueRequest( + std::declval(), std::declval() + ))::error_type; /** * @brief Sends a request on the bus. @@ -299,14 +313,16 @@ class RequestBusMixin { * @return An opaque handle that must be retained until the request is * fulfilled or the response is no longer desired. */ - RequestHandle Request(const Req& req, RequestCallback&& cb, Buf buf) { - return RequestHandle( - m_request_handlers, - { - .filter = Derived().IssueRequest(req, buf), - .callback = std::move(cb), - } - ); + std::expected Request( + const Req& req, RequestCallback&& cb, Buf buf + ) { + if (auto filter = Derived().IssueRequest(req, buf)) { + return RequestHandle( + m_request_handlers, {.filter = filter.value(), .callback = cb} + ); + } else { + return std::unexpected(filter.error()); + } } protected: @@ -351,10 +367,14 @@ template concept ProcessBus = requires(T& bus, ipc::Callback, Req>::DummyProvider cb) { typename T::ProcessHandle; + typename T::ProcessError; - { bus.Process(cb.Func) } -> std::same_as; - } && std::semiregular && Message && - Message; + { + bus.Process(cb.Func) + } -> std::same_as< + std::expected>; + } && std::semiregular && + utils::MaybeError && Message && Message; /** * @brief Specifies the required methods for the class \ref ProcessBusMixin is @@ -385,6 +405,7 @@ class ProcessBusMixin { public: using ProcessHandle = utils::Handle; + using ProcessError = utils::Never; /** * @brief Registers a processor callback. @@ -394,7 +415,7 @@ class ProcessBusMixin { * @return An opaque handle that must be retained for the processor to * remain active. */ - ProcessHandle Process(ProcessCallback&& cb) { + std::expected Process(ProcessCallback&& cb) { return ProcessHandle(m_processors, std::move(cb)); } diff --git a/common/Inc/obc/port.hpp b/common/Inc/obc/port.hpp index 24e3201..2c847e1 100644 --- a/common/Inc/obc/port.hpp +++ b/common/Inc/obc/port.hpp @@ -22,6 +22,8 @@ #include +#include "utils/error.hpp" + /** * @brief Defines a common interface for IO ports. * @@ -33,6 +35,12 @@ * used to signal intent, however there may be additional constraints placed on * them in future. * + * Ports should be able to source or sink an unlimited amount of data. Reaching + * the end of the iterator signals some sort of error state. The error can be + * queried from the iterator. + * + * @todo Timeouts and error states + * * @warning Data is only pulled/pushed to the port upon calling the increment * operator, this operation may block for an unspecified duration. * diff --git a/common/Inc/obc/utils/error.hpp b/common/Inc/obc/utils/error.hpp new file mode 100644 index 0000000..34d0718 --- /dev/null +++ b/common/Inc/obc/utils/error.hpp @@ -0,0 +1,37 @@ +#include +#include + +#include "utils/meta.hpp" + +namespace obc::utils { +/** + * @brief A class which implements the error interface or + * is the special Never type (ie. not an error). + * + * @todo Implment the error interface, currently only `Never` + * is valid. + * + * @warning MaybeError is not a valid return type for a function + * which may fail or return nothing. You probably want to use + * `std::optional` instead. + */ +template +concept MaybeError = std::same_as; + +template +concept ExpectedReturn = + Specializes && MaybeError && + std::convertible_to>; + +#define UNWRAP_ASSIGN_IMPL(lhs, rexpr, temp) \ + auto temp = rexpr; \ + static_assert(obc::utils::ExpectedReturn>); \ + if constexpr (!std::same_as< \ + std::remove_cvref::error_type, \ + obc::utils::Never>) { \ + if (!temp) return std::unexpected(std::move(temp).error()); \ + } \ + lhs = std::move(temp.value()); + +#define UNWRAP_ASSIGN(lhs, rexpr) UNWRAP_ASSIGN_IMPL(lhs, rexpr, TEMP_NAME) +} // namespace obc::utils diff --git a/common/Inc/obc/utils/meta.hpp b/common/Inc/obc/utils/meta.hpp index 67987b2..9e84170 100644 --- a/common/Inc/obc/utils/meta.hpp +++ b/common/Inc/obc/utils/meta.hpp @@ -29,6 +29,26 @@ concept OptionLike = requires(T t) { { *t } -> std::convertible_to; }; +/** + * @brief An appoximation of a bottom type. + * + * Bottom types are used to represent values which cannot exist in contexts + * where a type must be specified. In other words, a type for which there + * are no valid values. + * + * Unlike a true bottom type (like those in Rust), this cannot be cast into + * any other type. This was an intentional design choice to prevent the + * accidental creation and + * + * This type can be privately subclassed by other marker types to prevent + * construction and use. + */ +struct Never { + Never() = delete; + Never(const Never&) = delete; + Never& operator=(const Never&) = delete; +}; + /** * @brief Check if a predicate is true for all elements of the zip of the * element types of tuples. @@ -68,4 +88,19 @@ concept AllPairsConvertable = MultiwayConjunctionV; */ template class TypeAmalgam : public Ts... {}; + +template class Z> +struct is_specialization_of : std::false_type {}; + +template class Z> +struct is_specialization_of, Z> : std::true_type {}; + +template class Z> +inline constexpr bool is_specialization_of_v = + is_specialization_of::value; + +template class Z> +concept Specializes = is_specialization_of_v; + +#define TEMP_NAME temp_##__COUNTER__ } // namespace obc::utils