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:
2026-07-15 15:30:28 +08:00
co-authored by Claude Fable 5
commit c948c7565c
7 changed files with 444 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# ESP-IDF component glue. Zephyr consumers use zephyr/module.yml instead.
idf_component_register(
INCLUDE_DIRS inc
REQUIRES esp_timer
)
+36
View File
@@ -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.
+38
View File
@@ -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>;
}
+105
View File
@@ -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_;
};
}
+251
View File
@@ -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
}
+6
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
name: app_common
build:
cmake: zephyr