diff --git a/.clang-tidy b/.clang-tidy index 58ccc08..8061554 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,6 +15,7 @@ CheckOptions: readability-identifier-naming.VariableCase: lower_case readability-identifier-naming.ConstantCase: CamelCase readability-identifier-naming.ConstantPrefix: k + readability-identifier-naming.ConstantParameterCase: lower_case readability-identifier-naming.ConstantIgnoredRegexp: .*V$ readability-identifier-naming.FunctionCase: CamelCase readability-identifier-naming.FunctionIgnoredRegexp: ^(begin|end)$ diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index a3cc5dc..8858b55 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -12,6 +12,7 @@ set(COMMON_HEADERS ${PROJECT_SOURCE_DIR}/Inc/obc/utils/handle.hpp ${PROJECT_SOURCE_DIR}/Inc/obc/utils/meta.hpp ${PROJECT_SOURCE_DIR}/Inc/obc/bus.hpp + ${PROJECT_SOURCE_DIR}/Inc/obc/bus/can.hpp ${PROJECT_SOURCE_DIR}/Inc/obc/port.hpp ) diff --git a/common/Inc/obc/bus.hpp b/common/Inc/obc/bus.hpp index ef71c5b..7e12fb9 100644 --- a/common/Inc/obc/bus.hpp +++ b/common/Inc/obc/bus.hpp @@ -27,6 +27,8 @@ #include #include +#include + #include "obc/ipc/callback.hpp" #include "obc/utils/error.hpp" #include "obc/utils/handle.hpp" @@ -108,7 +110,7 @@ concept Buffer = requires(T& buf, V v, std::size_t i) { * * Can be used to facilitate trivial deserialization of C-style structs sent on * a bus. - * + * * @warning For portability, the struct should be tightly packed (containing * explicit padding fields if required) with fixed sized integers. * @@ -127,7 +129,7 @@ auto StructAsBuffer(const T& s) -> std::span { * * Can be used to facilitate trivial deserialization of C-style structs sent on * a bus. - * + * * @warning For portability, the struct should be tightly packed (containing * explicit padding fields if required) with fixed sized integers. * @@ -149,9 +151,14 @@ auto StructAsBuffer(T& s) -> std::span { * @tparam M The type of message to send. */ template -concept SendBus = requires(T& bus, const M& msg) { - { bus.Send(msg) } -> std::same_as; -} && Message; +concept SendBus = + requires(T& bus, const M& msg, const units::milliseconds timeout) { + typename T::SendError; + + { + bus.Send(msg, timeout) + } -> std::same_as>; + } && utils::MaybeError && Message; /** * @brief Represents a bus that can listen to all received messages. @@ -238,13 +245,14 @@ template< concept RequestBus = requires( T& bus, const Req& req, - ipc::Callback::DummyProvider cb, Buf& buf + ipc::Callback::DummyProvider cb, Buf& buf, + const units::milliseconds timeout ) { typename T::RequestHandle; typename T::RequestError; { - bus.Request(req, cb.Func, buf) + bus.Request(req, cb.Func, buf, timeout) } -> std::same_as< std::expected>; } && @@ -278,9 +286,10 @@ concept MessageFilter = requires(T& filter, const M& msg) { template< typename T, typename Req = BasicMessage, typename Res = BasicMessage, typename Flt = ipc::Callback> -concept RequestBusMixinRequirements = requires(T& bus, const Req& req) { - { bus.IssueRequest(req) } -> utils::ExpectedReturn; -} && Message && Message && MessageFilter; +concept RequestBusMixinRequirements = + requires(T& bus, const Req& req, const units::milliseconds timeout) { + { bus.IssueRequest(req, timeout) } -> utils::ExpectedReturn; + } && Message && Message && MessageFilter; /** * @brief A helper class for implementing request functionality. @@ -329,11 +338,13 @@ class RequestBusMixin { * @return An opaque handle that must be retained until the request is * fulfilled or the response is no longer desired. */ - auto Request(const Req& req, RequestCallback&& cb, Buf buf) - -> std::expected { + auto Request( + const Req& req, RequestCallback&& cb, Buf buf, + const units::milliseconds timeout + ) -> std::expected { return RequestHandle( m_request_handlers, - {.filter = UNWRAP(Derived().IssueRequest(req, buf)), + {.filter = UNWRAP(Derived().IssueRequest(req, buf, timeout)), .callback = std::move(cb)} ); } diff --git a/common/Inc/obc/bus/can.hpp b/common/Inc/obc/bus/can.hpp new file mode 100644 index 0000000..038eefb --- /dev/null +++ b/common/Inc/obc/bus/can.hpp @@ -0,0 +1,171 @@ +/* USER CODE BEGIN Header */ +/* + * 401 Ballon OBC + * Copyright (C) 2024 Bluesat and contributors. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +/* USER CODE END Header */ + +#define FDCAN1 +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "bus.hpp" +#include "ipc/mutex.hpp" +#include "scheduling/delay.hpp" +#include "scheduling/task.hpp" + +namespace obc::bus { +/** + * To prevent this driver from locking up the entire system in an ISR, the + * implementation elects to use a polling approach. + * + * todo(evan): The more efficient approach would be to register an interupt + * which sets the ready flag on this task. This avoids excessive polling. + */ +class CanFd : public scheduling::StackTask<>, bus::ListenBusMixin<> { + public: + static constexpr size_t kMaxPayloadSize = 64; + + enum class SendError { kTimeout, kInvalidSize }; + + inline CanFd(FDCAN_HandleTypeDef* handle) : m_handle {handle} {} + + inline auto Send( + const BasicMessage& msg, const units::milliseconds timeout + ) -> std::expected { + std::lock_guard lock {m_send_lock}; + + // Wait for a free slot in the message queue + scheduling::Timeout timeout_block(timeout); + if (!timeout_block.Poll([&]() { + return HAL_FDCAN_GetTxFifoFreeLevel(m_handle); + })) + return std::unexpected {SendError::kTimeout}; + + // Pad the payload so that it fits within a valid DLC size if required + auto [dlc, padding] { + UNWRAP_TAGGED(EncodeDlc(msg.data.size()), SendError::kInvalidSize) + }; + // Don't waste time zeroing memory if this buffer is not used + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + std::array padded_payload; + uint8_t* payload_ptr {reinterpret_cast( + padding ? ({ + std::ranges::copy(msg.data, padded_payload.begin()); + std::ranges::fill_n( + padded_payload.begin() + msg.data.size(), padding, + std::byte {0} + ); + padded_payload.data(); + }) + : ({ msg.data.data(); }) + )}; + + FDCAN_TxHeaderTypeDef header { + .Identifier {msg.address}, + .IdType {FDCAN_EXTENDED_ID}, + .TxFrameType {FDCAN_DATA_FRAME}, + .DataLength {}, + .ErrorStateIndicator {FDCAN_ESI_ACTIVE}, + .BitRateSwitch {FDCAN_BRS_ON}, + .FDFormat {FDCAN_FD_CAN}, + .TxEventFifoControl {FDCAN_STORE_TX_EVENTS}, + .MessageMarker {0}, + }; + + // The call can only fail if the queue is full or the can bus is not + // started, both of these are our fault. + utils::CheckOrPanic( + HAL_FDCAN_AddMessageToTxFifoQ(m_handle, &header, payload_ptr), HalOk + ); + return {}; + } + + protected: + inline auto Run() -> void override { + for (const auto& fifo : {FDCAN_RX_FIFO0, FDCAN_RX_FIFO1}) { + // Does this FIFO have any incoming messages? + if (!HAL_FDCAN_GetRxFifoFillLevel(m_handle, fifo)) continue; + + FDCAN_RxHeaderTypeDef header {}; + std::array payload {}; + // The call can only fail if the queue is full or the can bus is not + // started, both of these are our fault. + utils::CheckOrPanic( + HAL_FDCAN_GetRxMessage( + m_handle, fifo, &header, + reinterpret_cast(payload.data()) + ), + HalOk + ); + FeedListeners( + {.address = header.Identifier, + .data = std::span( + payload.data(), + // Only well formed CAN frames should exist in the FIFO + utils::UnwrapOrPanic(DecodeDlc(header.DataLength)) + )} + ); + } + } + + private: + static constexpr std::array, 15> kDlcSizeMap { + std::pair { FDCAN_DLC_BYTES_0, 0}, + std::pair { FDCAN_DLC_BYTES_1, 1}, + std::pair { FDCAN_DLC_BYTES_2, 2}, + std::pair { FDCAN_DLC_BYTES_3, 3}, + std::pair { FDCAN_DLC_BYTES_4, 4}, + std::pair { FDCAN_DLC_BYTES_5, 5}, + std::pair { FDCAN_DLC_BYTES_6, 6}, + std::pair { FDCAN_DLC_BYTES_7, 7}, + std::pair { FDCAN_DLC_BYTES_8, 8}, + std::pair {FDCAN_DLC_BYTES_12, 12}, + std::pair {FDCAN_DLC_BYTES_16, 16}, + std::pair {FDCAN_DLC_BYTES_24, 24}, + std::pair {FDCAN_DLC_BYTES_32, 32}, + std::pair {FDCAN_DLC_BYTES_48, 48}, + std::pair {FDCAN_DLC_BYTES_64, 64}, + }; + + [[nodiscard]] inline static constexpr auto DecodeDlc(uint32_t dlc) + -> std::optional { + for (auto& [map_dlc, map_size] : kDlcSizeMap) + if (dlc == map_dlc) return map_size; + return std::nullopt; + } + + [[nodiscard]] inline static constexpr auto EncodeDlc(size_t size) + -> std::optional> { + for (auto& [map_dlc, map_size] : kDlcSizeMap) + if (size <= map_size) + return { + {map_dlc, map_size - size} + }; + return std::nullopt; + } + + FDCAN_HandleTypeDef* m_handle; + ipc::Mutex m_send_lock {}; +}; +} // namespace obc::bus diff --git a/common/Inc/obc/scheduling/delay.hpp b/common/Inc/obc/scheduling/delay.hpp index aecdb83..290ab71 100644 --- a/common/Inc/obc/scheduling/delay.hpp +++ b/common/Inc/obc/scheduling/delay.hpp @@ -90,10 +90,10 @@ class Timeout : public detail::Timeout { */ template auto Poll(F& f) -> std::optional> { - do { + while (!(*this)) { if (auto x = f()) return *x; Yield(); - } while (!(*this)); + } return std::nullopt; } diff --git a/common/Inc/obc/scheduling/task.hpp b/common/Inc/obc/scheduling/task.hpp index 1022233..14fa069 100644 --- a/common/Inc/obc/scheduling/task.hpp +++ b/common/Inc/obc/scheduling/task.hpp @@ -41,6 +41,18 @@ namespace obc::scheduling { */ class Task { public: + Task(const Task& other) = delete; + Task(Task&& other) = delete; + + auto operator=(const Task& other) -> Task& = delete; + auto operator=(Task&& other) -> Task& = delete; + + /** + * @brief Stops the underlying task immediately. + */ + virtual inline ~Task() { vTaskDelete(NULL); } + + protected: // This is interfacing with C-Style FreeRTOS code which uses out // parameters to initialise values // NOLINTBEGIN(cppcoreguidelines-pro-type-member-init,hicpp-member-init) @@ -55,18 +67,6 @@ class Task { // NOLINTEND(cppcoreguidelines-pro-type-member-init,hicpp-member-init) - Task(const Task& other) = delete; - Task(Task&& other) = delete; - - auto operator=(const Task& other) -> Task& = delete; - auto operator=(Task&& other) -> Task& = delete; - - /** - * @brief Stops the underlying task immediately. - */ - virtual inline ~Task() { vTaskDelete(NULL); } - - protected: /** * @brief The function to be called periodically to execute the task. */ @@ -128,8 +128,11 @@ constexpr std::uint32_t kDefaultStackDepth = 4096; * @brief Mixin class to statically create a fixed-size stack for a task. */ template -class StackTask { +class StackTask : public Task { protected: + StackTask() : Task(m_task_stack) = default; + + private: std::array m_task_stack {}; }; } // namespace obc::scheduling diff --git a/common/Inc/obc/utils/error.hpp b/common/Inc/obc/utils/error.hpp index 4d5991b..844f1da 100644 --- a/common/Inc/obc/utils/error.hpp +++ b/common/Inc/obc/utils/error.hpp @@ -35,7 +35,7 @@ namespace obc::utils { * * @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. + * `std::expected` instead. */ template concept MaybeError = std::same_as; @@ -45,6 +45,11 @@ concept ExpectedReturn = Specializes && MaybeError && std::convertible_to>; +template +concept PredicateFunction = requires(T& f, const U& x) { + { f(x) } -> std::convertible_to; +}; + /* * This functionality is impossible is impossible to implement without * macros since unwrapping is an operation which can return from the @@ -56,12 +61,51 @@ concept ExpectedReturn = auto res {expr}; \ static_assert(obc::utils::Specializes); \ if constexpr (!std::same_as< \ - typename std::remove_cvref::error_type, \ obc::utils::Never>) { \ - if (!res) return std::unexpected(std::move(res.error())); \ + if (!res) return std::unexpected {std::move(res.error())}; \ } \ res.value(); \ }) + +#define UNWRAP_TAGGED(expr, tag) \ + ({ \ + auto res {expr}; \ + static_assert(obc::utils::Specializes || obc::utils::Specializes); \ + if constexpr (obc::utils::Specializes) { \ + if (!res) return std::unexpected {tag}; \ + } else if constexpr (!std::same_as< \ + typename std::remove_cvref_t::error_type, \ + obc::utils::Never>) { \ + if (!res) \ + return std::unexpected { \ + {tag, std::move(res.error())} \ + }; \ + } \ + res.value(); \ + }) + +#define UNWRAP_OPT(expr) \ + ({ \ + auto res {expr}; \ + static_assert(obc::utils::Specializes); \ + if (!res) return std::nullopt; \ + res.value(); \ + }) + // NOLINTEND(cppcoreguidelines-macro-usage) + +template +inline auto UnwrapOrPanic(T x) -> std::remove_reference_t { + if (static_cast(x)) return *x; + Panic(); +} + +template C> +inline auto CheckOrPanic(T x, C& check) -> T { + if (check(x)) return x; + Panic(); +} } // namespace obc::utils diff --git a/common/Inc/obc/utils/meta.hpp b/common/Inc/obc/utils/meta.hpp index 40b48aa..e68f46b 100644 --- a/common/Inc/obc/utils/meta.hpp +++ b/common/Inc/obc/utils/meta.hpp @@ -37,6 +37,9 @@ concept TupleLike = requires { typename std::tuple_element_t<0, T>; }; +template +concept NonVoid = !std::same_as; + /** * @brief A type which has a truthiness associated with an instance being * convertable to a particular type with the deref operator. @@ -52,13 +55,25 @@ concept OptionLike = requires(T t) { }; /** - * @brief An appoximation of a bottom type. + * @brief A type which has a truthiness associated with an instance being + * convertable to some value with the deref operator. + * + * `std::optional`, `std::expected` and `T*` statisfy this. + */ +template +concept OptionLikeAny = requires(T t) { + { static_cast(t) }; + { *t } -> NonVoid; +}; + +/** + * @brief An appoximation of a 🥺 type. * - * Bottom types are used to represent values which cannot exist in contexts + * 🥺 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 + * Unlike a true 🥺 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 *