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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# ESP-IDF component glue. Zephyr consumers use zephyr/module.yml instead.
|
||||
idf_component_register(
|
||||
INCLUDE_DIRS inc
|
||||
REQUIRES esp_timer
|
||||
)
|
||||
@@ -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 `<format>`; 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.
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <ratio>
|
||||
|
||||
#if defined(__ZEPHYR__)
|
||||
#include <zephyr/kernel.h>
|
||||
#elif defined(ESP_PLATFORM)
|
||||
#include <esp_timer.h>
|
||||
#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<rep, period>;
|
||||
using time_point = std::chrono::time_point<clock_t>;
|
||||
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<rep>(dur)));
|
||||
#else
|
||||
return time_point(duration(static_cast<rep>(esp_timer_get_time())));
|
||||
#endif
|
||||
}
|
||||
};
|
||||
using milliseconds = std::chrono::duration<int32_t, std::milli>;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
#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<uint32_t, std::milli>;
|
||||
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<milliseconds>(elapsed()).count();
|
||||
}
|
||||
|
||||
template <typename Rep, typename Period>
|
||||
[[nodiscard]]
|
||||
bool has_elapsed(const std::chrono::duration<Rep, Period> 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 <typename Rep, typename Period>
|
||||
[[nodiscard]]
|
||||
bool mut_every(const std::chrono::duration<Rep, Period> 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_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
#include <version>
|
||||
#ifdef __cpp_lib_format
|
||||
#include <format>
|
||||
#include <string>
|
||||
#endif
|
||||
|
||||
namespace app::utils {
|
||||
/// @brief reinterpret_cast a trivially copyable value to a span of bytes
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
inline std::span<const std::byte> as_bytes(const T &value) {
|
||||
return {reinterpret_cast<const std::byte *>(&value), sizeof(value)};
|
||||
}
|
||||
|
||||
/// @brief reinterpret_cast a trivially copyable value to a span of bytes (mutable)
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
inline std::span<std::byte> as_bytes(T &value) {
|
||||
return {reinterpret_cast<std::byte *>(&value), sizeof(value)};
|
||||
}
|
||||
|
||||
/// @brief convert a uint8_t span to a std::byte span
|
||||
inline std::span<const std::byte> as_bytes(std::span<const uint8_t> span) {
|
||||
return {reinterpret_cast<const std::byte *>(span.data()), span.size()};
|
||||
}
|
||||
|
||||
/// @brief convert a uint8_t span to a std::byte span (mutable)
|
||||
inline std::span<std::byte> as_bytes(std::span<uint8_t> span) {
|
||||
return {reinterpret_cast<std::byte *>(span.data()), span.size()};
|
||||
}
|
||||
|
||||
/// @brief construct a std::byte span from a raw pointer and size
|
||||
inline std::span<std::byte> 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<const std::byte> 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<std::byte> as_bytes(std::uint8_t *data, std::size_t size) {
|
||||
return {reinterpret_cast<std::byte *>(data), size};
|
||||
}
|
||||
|
||||
/// @brief construct an immutable std::byte span from a uint8_t raw pointer and size
|
||||
inline std::span<const std::byte> as_bytes(const std::uint8_t *data, std::size_t size) {
|
||||
return {reinterpret_cast<const std::byte *>(data), size};
|
||||
}
|
||||
|
||||
|
||||
/// @brief reinterpret_cast a trivially copyable value to a span of uint8_t
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
inline std::span<const std::uint8_t> as_u8s(const T &value) {
|
||||
return {reinterpret_cast<const std::uint8_t *>(&value), sizeof(value)};
|
||||
}
|
||||
|
||||
/// @brief reinterpret_cast a trivially copyable value to a span of uint8_t span
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
inline std::span<std::uint8_t> as_u8s(T &value) {
|
||||
return {reinterpret_cast<std::uint8_t *>(&value), sizeof(value)};
|
||||
}
|
||||
|
||||
/// @brief convert a std::byte span to a uint8_t span
|
||||
inline std::span<const std::uint8_t> as_u8s(std::span<const std::byte> span) {
|
||||
return {reinterpret_cast<const std::uint8_t *>(span.data()), span.size()};
|
||||
}
|
||||
|
||||
/// @brief convert a std::byte span to a uint8_t span (mutable)
|
||||
inline std::span<std::uint8_t> as_u8s(std::span<std::byte> span) {
|
||||
return {reinterpret_cast<std::uint8_t *>(span.data()), span.size()};
|
||||
}
|
||||
|
||||
/// @brief helper type for the visitor
|
||||
template <class... Ts>
|
||||
struct overloads : Ts... {
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
inline void hexdump(std::span<const std::byte> 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<uint8_t>(byte));
|
||||
} else {
|
||||
if (i % 16 == 15) {
|
||||
printf("%02x\n", static_cast<uint8_t>(byte));
|
||||
} else {
|
||||
printf("%02x ", static_cast<uint8_t>(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<T> element, or std::nullopt if index is out of range
|
||||
*/
|
||||
template <typename T>
|
||||
std::optional<T> try_get(std::span<T> s, int idx) {
|
||||
const int n = static_cast<int>(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<std::reference_wrapper<const T>> element reference, or std::nullopt if index is out of range
|
||||
* @see try_get
|
||||
*/
|
||||
template <typename T>
|
||||
std::optional<std::reference_wrapper<const T>>
|
||||
try_get_ref(std::span<const T> s, int idx) {
|
||||
const int n = static_cast<int>(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<void()>;
|
||||
#else
|
||||
using func_t = std::function<void()>;
|
||||
#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 <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
std::expected<T, overflow_error> add_overflow(T a, T b) {
|
||||
using ue = std::unexpected<overflow_error>;
|
||||
|
||||
if constexpr (std::is_unsigned_v<T>) {
|
||||
// For unsigned: check if a > max - b (rearranged to avoid overflow in the check itself)
|
||||
if (a > std::numeric_limits<T>::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<T>::max() - b) {
|
||||
return ue{overflow_error{}};
|
||||
}
|
||||
} else {
|
||||
if (a < std::numeric_limits<T>::min() - b) {
|
||||
return ue{overflow_error{}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a + b;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see `std::clamp`
|
||||
*/
|
||||
template <typename T, T min, T max>
|
||||
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<const std::byte, 6> mac) {
|
||||
return std::format("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||
static_cast<uint8_t>(mac[0]),
|
||||
static_cast<uint8_t>(mac[1]),
|
||||
static_cast<uint8_t>(mac[2]),
|
||||
static_cast<uint8_t>(mac[3]),
|
||||
static_cast<uint8_t>(mac[4]),
|
||||
static_cast<uint8_t>(mac[5]));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
name: app_common
|
||||
build:
|
||||
cmake: zephyr
|
||||
Reference in New Issue
Block a user