From c948c7565c9ad3d955d384e1e4d05533571c2177 Mon Sep 17 00:00:00 2001 From: crosstyan Date: Wed, 15 Jul 2026 15:30:28 +0800 Subject: [PATCH] feat: unify app_utils/app_clock headers shared by the fleet Merged from the three drifted hand-copies in TrackBackFwd, healthy-band-nrf and zephyr_gateway_fwd: - as_u8s(T&) overloads and deferrer (band/hub side) - hexdump, add_overflow, constrain_value_in_range_static, to_mac_string (TrackBackFwd side) - add_overflow now returns std::expected directly instead of the app_result alias; to_mac_string guarded by __cpp_lib_format for GCC 12 toolchains - app_clock.hpp now() selects k_uptime_ticks / esp_timer_get_time via __ZEPHYR__ / ESP_PLATFORM; fixed doc comment (microseconds, not ms) - dropped vestigial esp_err.h/esp_system.h/FreeRTOS.h includes Dual build glue: ESP-IDF component (root CMakeLists) + Zephyr module (zephyr/). Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 5 + README.md | 36 ++++++ inc/app_clock.hpp | 38 ++++++ inc/app_clock_instant.hpp | 105 ++++++++++++++++ inc/app_utils.hpp | 251 ++++++++++++++++++++++++++++++++++++++ zephyr/CMakeLists.txt | 6 + zephyr/module.yml | 3 + 7 files changed, 444 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 inc/app_clock.hpp create mode 100644 inc/app_clock_instant.hpp create mode 100644 inc/app_utils.hpp create mode 100644 zephyr/CMakeLists.txt create mode 100644 zephyr/module.yml diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8834a3e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +# ESP-IDF component glue. Zephyr consumers use zephyr/module.yml instead. +idf_component_register( + INCLUDE_DIRS inc + REQUIRES esp_timer +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e91a6a2 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# app_common + +Shared header-only utilities for the TrackBackFwd / healthy-band-nrf / +zephyr_gateway_fwd fleet. Single source of truth for code that used to be +hand-copied (and had drifted) between the three repos. + +## Contents + +- `inc/app_utils.hpp` — byte/span casts (`as_bytes`, `as_u8s`), `overloads`, + `try_get`/`try_get_ref`, `deferrer`, `hexdump`, `add_overflow`, + `constrain_value_in_range_static`, `to_mac_string` (only when the toolchain + has ``; guarded by `__cpp_lib_format`). +- `inc/app_clock.hpp` — monotonic microsecond `TrivialClock`. The only + OS-specific code in this repo: `now()` is `k_uptime_ticks()` on Zephyr and + `esp_timer_get_time()` on ESP-IDF, selected by `__ZEPHYR__` / `ESP_PLATFORM`. +- `inc/app_clock_instant.hpp` — `Instant`, Rust-style elapsed-time helper on + top of `clock_t`. Pure `std::chrono`, no OS dependency. + +Everything requires C++23 (all consumers build with `-std=gnu++23` / +`CONFIG_STD_CPP2B`). + +## Consuming + +**ESP-IDF**: add as a git submodule under `components/app_common`. The root +`CMakeLists.txt` registers the component; add `app_common` to your `REQUIRES`. + +**Zephyr**: add as a git submodule (e.g. `modules/app_common`) and append it to +`EXTRA_ZEPHYR_MODULES` in the application CMakeLists before +`find_package(Zephyr)`. The include directory is registered globally; targets +`app_common` / `app_utils` / `app_utils_clock` exist for explicit linking. + +## Rules + +- Nothing platform-specific beyond the `app_clock.hpp` seam. If a helper needs + ESP-IDF or Zephyr APIs, it belongs in the consuming repo, not here. +- Never copy these headers back into a consumer repo. Bump the submodule. diff --git a/inc/app_clock.hpp b/inc/app_clock.hpp new file mode 100644 index 0000000..42ce0b0 --- /dev/null +++ b/inc/app_clock.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include + +#if defined(__ZEPHYR__) +#include +#elif defined(ESP_PLATFORM) +#include +#else +#error "app_clock.hpp: unsupported platform (expected Zephyr or ESP-IDF)" +#endif + +namespace app::utils { +/** + * @brief a monotonic clock with microsecond resolution + * @see https://en.cppreference.com/w/cpp/named_req/TrivialClock.html + * @see https://en.cppreference.com/w/cpp/named_req/Clock.html + */ +struct clock_t { + using rep = int64_t; // signed arithmetic type for tick count + using period = std::micro; // tick period in seconds (1/1000000) + using duration = std::chrono::duration; + using time_point = std::chrono::time_point; + static constexpr bool is_steady = true; + + static time_point now() { +#if defined(__ZEPHYR__) + auto t = k_uptime_ticks(); + auto dur = k_ticks_to_us_floor64(t); + return time_point(duration(static_cast(dur))); +#else + return time_point(duration(static_cast(esp_timer_get_time()))); +#endif + } +}; +using milliseconds = std::chrono::duration; +} diff --git a/inc/app_clock_instant.hpp b/inc/app_clock_instant.hpp new file mode 100644 index 0000000..2be17fb --- /dev/null +++ b/inc/app_clock_instant.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +#include "app_clock.hpp" + +namespace app::utils { +/** + * @brief A measurement of a monotonically nondecreasing clock. + * @tparam T the data type of the counter + * @sa https://doc.rust-lang.org/std/time/struct.Instant.html + */ +struct Instant { +public: + using clock = typename app::utils::clock_t; + using duration = typename clock::duration; + using time_point = typename clock::time_point; + using milliseconds = typename std::chrono::duration; + using ms_rep = typename milliseconds::rep; + + + Instant() { + time_ = clock::now(); + } + + static Instant now() { + return Instant{}; + } + + [[nodiscard]] + duration elapsed() const { + return clock::now() - time_; + } + + [[nodiscard]] + ms_rep elapsed_ms() const { + return std::chrono::duration_cast(elapsed()).count(); + } + + template + [[nodiscard]] + bool has_elapsed(const std::chrono::duration duration) const { + return elapsed() >= duration; + } + + [[nodiscard]] + bool has_elapsed_ms(ms_rep ms) const { + return has_elapsed(milliseconds{ms}); + } + + /** + * @brief Check if the instant has elapsed and reset the time point if it has. + * @note use if periodic task to get some timer like behavior. (note that + * you might not want to busy wait to check this, although you are free to do so) + * + * @tparam Rep representation of the duration (int, uint32_t, uint64_t, etc.) + * @tparam Period unit of the duration (std::milli, std::micro, etc.) + * @param duration the duration to check against + * @return true if the instant has elapsed, and would be reset + * @return false if the instant has not elapsed + */ + template + [[nodiscard]] + bool mut_every(const std::chrono::duration duration) { + if (has_elapsed(duration)) { + mut_reset(); + return true; + } + return false; + } + + [[nodiscard]] + bool mut_every_ms(ms_rep ms) { + return mut_every(milliseconds{ms}); + } + + void mut_reset() { + time_ = clock::now(); + } + + /** + * @deprecated use `mut_reset` instead + */ + [[deprecated("use `mut_reset` instead")]] + void reset() { + mut_reset(); + } + + [[nodiscard]] + duration mut_elapsed_and_reset() { + auto e = elapsed(); + mut_reset(); + return e; + } + + [[nodiscard]] + time_point last() const { + return time_; + } + +private: + time_point time_; +}; +} diff --git a/inc/app_utils.hpp b/inc/app_utils.hpp new file mode 100644 index 0000000..0cdc524 --- /dev/null +++ b/inc/app_utils.hpp @@ -0,0 +1,251 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __cpp_lib_format +#include +#include +#endif + +namespace app::utils { +/// @brief reinterpret_cast a trivially copyable value to a span of bytes +template + requires std::is_trivially_copyable_v +inline std::span as_bytes(const T &value) { + return {reinterpret_cast(&value), sizeof(value)}; +} + +/// @brief reinterpret_cast a trivially copyable value to a span of bytes (mutable) +template + requires std::is_trivially_copyable_v +inline std::span as_bytes(T &value) { + return {reinterpret_cast(&value), sizeof(value)}; +} + +/// @brief convert a uint8_t span to a std::byte span +inline std::span as_bytes(std::span span) { + return {reinterpret_cast(span.data()), span.size()}; +} + +/// @brief convert a uint8_t span to a std::byte span (mutable) +inline std::span as_bytes(std::span span) { + return {reinterpret_cast(span.data()), span.size()}; +} + +/// @brief construct a std::byte span from a raw pointer and size +inline std::span as_bytes(std::byte *data, std::size_t size) { + return {data, size}; +} + +/// @brief construct an immutable std::byte span from a raw pointer and size +inline std::span as_bytes(const std::byte *data, std::size_t size) { + return {data, size}; +} + +/// @brief construct a std::byte span from a uint8_t raw pointer and size +inline std::span as_bytes(std::uint8_t *data, std::size_t size) { + return {reinterpret_cast(data), size}; +} + +/// @brief construct an immutable std::byte span from a uint8_t raw pointer and size +inline std::span as_bytes(const std::uint8_t *data, std::size_t size) { + return {reinterpret_cast(data), size}; +} + + +/// @brief reinterpret_cast a trivially copyable value to a span of uint8_t +template + requires std::is_trivially_copyable_v +inline std::span as_u8s(const T &value) { + return {reinterpret_cast(&value), sizeof(value)}; +} + +/// @brief reinterpret_cast a trivially copyable value to a span of uint8_t span +template + requires std::is_trivially_copyable_v +inline std::span as_u8s(T &value) { + return {reinterpret_cast(&value), sizeof(value)}; +} + +/// @brief convert a std::byte span to a uint8_t span +inline std::span as_u8s(std::span span) { + return {reinterpret_cast(span.data()), span.size()}; +} + +/// @brief convert a std::byte span to a uint8_t span (mutable) +inline std::span as_u8s(std::span span) { + return {reinterpret_cast(span.data()), span.size()}; +} + +/// @brief helper type for the visitor +template +struct overloads : Ts... { + using Ts::operator()...; +}; + +inline void hexdump(std::span data) { + const auto enumerate = [](const auto &data) { + return data | std::views::transform([i = 0](const auto &value) mutable { + return std::make_tuple(i++, value); + }); + }; + for (const auto [i, byte] : enumerate(data)) { + bool is_end = i == data.size() - 1; + if (is_end) { + printf("%02x\n", static_cast(byte)); + } else { + if (i % 16 == 15) { + printf("%02x\n", static_cast(byte)); + } else { + printf("%02x ", static_cast(byte)); + } + } + } +} + +/** + * @brief try to get an element from a span + * @tparam T, should be trivially copyable + * @param s span + * @param idx index, can be negative, will be shifted into [0, s.size()) + * @return std::optional element, or std::nullopt if index is out of range + */ +template +std::optional try_get(std::span s, int idx) { + const int n = static_cast(s.size()); + // 1) if negative, shift into [−n, 0) → [0, n) + if (idx < 0) { + idx += n; + } + // 2) now both negative-too-big and positive-too-big land outside [0,n) + if (idx < 0 || idx >= n) { + return std::nullopt; + } + return s[idx]; +} + +/** + * @brief try to get an element from a const span + * @tparam T + * @param s const span + * @param idx index, can be negative, will be shifted into [0, s.size()) + * @return std::optional> element reference, or std::nullopt if index is out of range + * @see try_get + */ +template +std::optional> +try_get_ref(std::span s, int idx) { + const int n = static_cast(s.size()); + if (idx < 0) { + idx += n; + } + if (idx < 0 || idx >= n) { + return std::nullopt; + } + return std::cref(s[idx]); +} + +/** + * @brief a simple RAII helper for `defer` like behavior + */ +struct deferrer { +#ifdef __cpp_lib_move_only_function + using func_t = std::move_only_function; +#else + using func_t = std::function; +#endif + + deferrer(func_t &&f) : _f(std::move(f)) {} + ~deferrer() { + if (_f) { + _f(); + } + } + deferrer(const deferrer &) = delete; + deferrer &operator=(const deferrer &) = delete; + deferrer(deferrer &&other) noexcept : _f(std::move(other._f)) { + other._f = {}; + } + deferrer &operator=(deferrer &&other) noexcept { + if (this != &other) { + // call current function before overwriting + if (_f) { + _f(); + } + _f = std::move(other._f); + other._f = {}; + } + return *this; + } + +private: + func_t _f; +}; + +using overflow_error = std::monostate; +/** + * @brief a safe addition that returns an overflow error if the result is out of range + */ +template + requires std::is_integral_v +std::expected add_overflow(T a, T b) { + using ue = std::unexpected; + + if constexpr (std::is_unsigned_v) { + // For unsigned: check if a > max - b (rearranged to avoid overflow in the check itself) + if (a > std::numeric_limits::max() - b) { + return ue{overflow_error{}}; + } + } else { + // For signed integers: need to check both positive and negative overflow + if (b > 0) { + if (a > std::numeric_limits::max() - b) { + return ue{overflow_error{}}; + } + } else { + if (a < std::numeric_limits::min() - b) { + return ue{overflow_error{}}; + } + } + } + + return a + b; +} + + +/** + * @see `std::clamp` + */ +template +T constrain_value_in_range_static(T value) { + static_assert(min < max, "min must be less than max"); + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} + +#ifdef __cpp_lib_format +inline std::string to_mac_string(std::span mac) { + return std::format("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + static_cast(mac[0]), + static_cast(mac[1]), + static_cast(mac[2]), + static_cast(mac[3]), + static_cast(mac[4]), + static_cast(mac[5])); +} +#endif +} diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt new file mode 100644 index 0000000..3bb8ee6 --- /dev/null +++ b/zephyr/CMakeLists.txt @@ -0,0 +1,6 @@ +zephyr_interface_library_named(app_common) +zephyr_include_directories(${CMAKE_CURRENT_LIST_DIR}/../inc) + +# compatibility aliases for consumers that still link the old split targets +add_library(app_utils ALIAS app_common) +add_library(app_utils_clock ALIAS app_common) diff --git a/zephyr/module.yml b/zephyr/module.yml new file mode 100644 index 0000000..e04a274 --- /dev/null +++ b/zephyr/module.yml @@ -0,0 +1,3 @@ +name: app_common +build: + cmake: zephyr