Redesign (simplify) top-level avclan interface to use expected/nullable unique_ptr

- Peripheral::read/send now return `expected<unique_ptr<Frame>, Error>`
for a unified success/error interface.
- Peripheral::route returns `expected<unique_ptr<Frame>, Error>` to
distinguish intent: (intentional) non-response vs unable to respond
- Other functions with optional message semantics (handle,poll,react)
return a unique_ptr whose ownership-state indicates response intent
(i.e. send new message)

Bundled necessary changes:
- Switch Frame allocation from global to local (enabled via member
  function new/delete)
- Add new header-library tl::expected to shim avr-libstdcpp

Unrelated: Defensive `continue` added after `route`, to reduce Bus
activity check latency

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Allen Hill
2026-07-17 14:24:03 -07:00
parent 200a225add
commit 5208e083f3
17 changed files with 536 additions and 187 deletions
+70
View File
@@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
add_compile_options( add_compile_options(
-Wall -Wswitch-enum -Werror -Wall -Wswitch-enum -Werror
@@ -76,6 +77,75 @@ target_include_directories(avclan PUBLIC
# flashing target. Added after the targets above so it can extend them. # flashing target. Added after the targets above so it can extend them.
add_subdirectory(src/avclan/target/${AVCLAN_TARGET}) add_subdirectory(src/avclan/target/${AVCLAN_TARGET})
# --- Polyfill headers for std facilities the toolchain may lack -------------
# tl::expected / tl::optional back-fill std::expected / std::optional (with
# C++23 monadic ops) when the build's stdlib predates them. Runs after the port
# subdirectory to provide a port-supplied freestanding stdlib via
# CMAKE_REQUIRED_INCLUDES, if necessary.
include(CheckCXXSourceCompiles)
include(FetchContent)
find_program(GIT_EXECUTABLE git REQUIRED)
if(TARGET libstdcpp)
get_target_property(_stdlib_incs libstdcpp INTERFACE_INCLUDE_DIRECTORIES)
set(CMAKE_REQUIRED_INCLUDES ${_stdlib_incs})
endif()
check_cxx_source_compiles("
#include <version>
#if !defined(__cpp_lib_expected) || __cpp_lib_expected < 202211L
#error no std::expected
#endif
int main() { return 0; }" HAVE_STD_EXPECTED)
# check_cxx_source_compiles("
# #include <version>
# #if !defined(__cpp_lib_optional) || __cpp_lib_optional < 202110L
# #error no C++23 std::optional
# #endif
# int main() { return 0; }" HAVE_STD_OPTIONAL)
unset(CMAKE_REQUIRED_INCLUDES)
if(NOT HAVE_STD_EXPECTED)
FetchContent_Declare(
tl_expected
GIT_REPOSITORY https://github.com/TartanLlama/expected.git
GIT_TAG 1770e3559f2f6ea4a5fb4f577ad22aeb30fbd8e4
PATCH_COMMAND ${CMAKE_COMMAND}
-DGIT_EXECUTABLE=${GIT_EXECUTABLE}
-DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/tl-expected-no-exception-header.patch
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/apply-patch.cmake
SYSTEM
)
set(EXPECTED_BUILD_TESTS OFF)
set(EXPECTED_BUILD_PACKAGE OFF)
FetchContent_MakeAvailable(tl_expected)
add_library(tl_expected_hdr INTERFACE)
target_include_directories(tl_expected_hdr
INTERFACE ${tl_expected_SOURCE_DIR}/include)
target_link_libraries(avclan PUBLIC tl_expected_hdr)
endif()
# std/tl::optional not currently used
# if(NOT HAVE_STD_OPTIONAL)
# FetchContent_Declare(
# tl_optional
# GIT_REPOSITORY https://github.com/TartanLlama/optional.git
# GIT_TAG 3a1209de8370bf5fe16362934956144b49591565
# PATCH_COMMAND ${CMAKE_COMMAND}
# -DGIT_EXECUTABLE=${GIT_EXECUTABLE}
# -DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/tl-optional-no-exception-header.patch
# -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/apply-patch.cmake
# SYSTEM
# )
# set(OPTIONAL_BUILD_TESTS OFF)
# set(OPTIONAL_BUILD_PACKAGE OFF)
# FetchContent_MakeAvailable(tl_optional)
# add_library(tl_optional_hdr INTERFACE)
# target_include_directories(tl_optional_hdr
# INTERFACE ${tl_optional_SOURCE_DIR}/include)
# target_link_libraries(avclan PUBLIC tl_optional_hdr)
# endif()
add_executable(mockingboard add_executable(mockingboard
src/sniffer.cc src/sniffer.cc
) )
+18
View File
@@ -0,0 +1,18 @@
# Idempotent patch driver for FetchContent PATCH_COMMAND steps.
#
# Usage: cmake -DGIT_EXECUTABLE=<git> -DPATCH_FILE=<patch> -P apply-patch.cmake
# (run with CWD = the populated source directory, as PATCH_COMMAND does).
#
# FetchContent re-runs PATCH_COMMAND after its update step on reconfigure, so a
# bare `git apply` fails the second time around. Skip if the patch is already
# applied (the reverse-apply dry run succeeds); otherwise apply it for real.
execute_process(
COMMAND "${GIT_EXECUTABLE}" apply --reverse --check --ignore-whitespace
"${PATCH_FILE}"
RESULT_VARIABLE _already_applied
OUTPUT_QUIET ERROR_QUIET)
if(NOT _already_applied EQUAL 0)
execute_process(
COMMAND "${GIT_EXECUTABLE}" apply --ignore-whitespace "${PATCH_FILE}"
COMMAND_ERROR_IS_FATAL ANY)
endif()
@@ -0,0 +1,36 @@
diff --git a/include/tl/expected.hpp b/include/tl/expected.hpp
index 59e59aa..75857bd 100644
--- a/include/tl/expected.hpp
+++ b/include/tl/expected.hpp
@@ -20,7 +20,7 @@
#define TL_EXPECTED_VERSION_MINOR 3
#define TL_EXPECTED_VERSION_PATCH 1
-#include <exception>
+#include <cstdlib>
#include <functional>
#include <type_traits>
#include <utility>
@@ -222,7 +222,7 @@ static constexpr unexpect_t unexpect{};
#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
#define TL_EXPECTED_THROW_EXCEPTION(e) throw((e));
#else
-#define TL_EXPECTED_THROW_EXCEPTION(e) std::terminate();
+#define TL_EXPECTED_THROW_EXCEPTION(e) std::abort();
#endif
namespace detail {
@@ -1249,11 +1249,11 @@ template <class T, class E> struct expected_default_ctor_base<T, E, false> {
};
} // namespace detail
-template <class E> class bad_expected_access : public std::exception {
+template <class E> class bad_expected_access {
public:
explicit bad_expected_access(E e) : m_val(std::move(e)) {}
- virtual const char *what() const noexcept override {
+ virtual const char *what() const noexcept {
return "Bad expected access";
}
@@ -0,0 +1,21 @@
diff --git a/include/tl/optional.hpp b/include/tl/optional.hpp
index e9c59c2..1ff3d4a 100644
--- a/include/tl/optional.hpp
+++ b/include/tl/optional.hpp
@@ -21,7 +21,6 @@
#define TL_OPTIONAL_VERSION_MINOR 1
#define TL_OPTIONAL_VERSION_PATCH 0
-#include <exception>
#include <functional>
#include <new>
#include <type_traits>
@@ -664,7 +663,7 @@ struct nullopt_t {
static constexpr nullopt_t nullopt{nullopt_t::do_not_use{},
nullopt_t::do_not_use{}};
-class bad_optional_access : public std::exception {
+class bad_optional_access {
public:
bad_optional_access() = default;
const char *what() const noexcept { return "Optional has no value"; }
+13
View File
@@ -121,6 +121,12 @@ enum AVCLAN_ENUM_CLASS MediaAction : uint8_t {
}; };
#ifdef __cplusplus #ifdef __cplusplus
enum class Device : uint8_t;
// Sentinel for a frame with no owning device.
// Compile-time collision check lives in device.hpp
inline constexpr Device NoDevice = Device{0xFF};
namespace detail { namespace detail {
struct Error { struct Error {
#endif #endif
@@ -140,6 +146,7 @@ struct Error {
STARTBIT_TOO_SHORT, STARTBIT_TOO_SHORT,
STARTBIT_TOO_LONG, STARTBIT_TOO_LONG,
BAD_STARTBIT, BAD_STARTBIT,
POOL_EMPTY, // non-bus error
}; };
enum AVCLAN_ENUM_CLASS Send : uint8_t { enum AVCLAN_ENUM_CLASS Send : uint8_t {
@@ -160,6 +167,12 @@ struct Error {
#ifdef __cplusplus #ifdef __cplusplus
}; };
struct SendError {
Device owning_device;
uint8_t reaction;
Error::Send err;
};
#endif #endif
enum AVCLAN_ENUM_CLASS Bit : uint8_t { enum AVCLAN_ENUM_CLASS Bit : uint8_t {
+17 -3
View File
@@ -31,11 +31,14 @@
#include <concepts> #include <concepts>
#include <cstdio> #include <cstdio>
#include <memory>
#include <new>
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "hal/phy.h" #include "hal/phy.h"
#include "stdshim.hpp"
namespace { namespace {
using Read = avclan::detail::Error::Read; using Read = avclan::detail::Error::Read;
@@ -191,7 +194,8 @@ void Bus::mute(bool mute) {
muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting muted_ = mute; // Only update muted_ *AFTER* hardware has finished muting
}; };
auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read { auto Bus::read(uint16_t address, Frame::Print print)
-> expected<std::unique_ptr<Frame>, Error::Read> {
struct errtype { struct errtype {
Read errno; Read errno;
uint16_t val; uint16_t val;
@@ -199,6 +203,12 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
using enum Read; using enum Read;
std::unique_ptr<Frame> in(new (std::nothrow) Frame);
if (!in) {
err.errno = POOL_EMPTY;
goto handle_err;
}
{ // bound handle lifetime { // bound handle lifetime
auto handle = get(); auto handle = get();
@@ -277,6 +287,7 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
handle_err:; handle_err:;
fputs("ERR(read): ", stdout); fputs("ERR(read): ", stdout);
switch (err.errno) { switch (err.errno) {
case POOL_EMPTY: puts("failed Frame alloc"); break;
case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break; case BAD_STARTBIT: fputs("bad start bit (other)", stdout); break;
case STARTBIT_TOO_SHORT: fputs("bad start bit (short)", stdout); break; case STARTBIT_TOO_SHORT: fputs("bad start bit (short)", stdout); break;
case STARTBIT_TOO_LONG: fputs("bad start bit (long)", stdout); break; case STARTBIT_TOO_LONG: fputs("bad start bit (long)", stdout); break;
@@ -297,7 +308,7 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
printf("; read 0x%02X", err.val); printf("; read 0x%02X", err.val);
} }
} }
putchar('\n'); puts(":");
} }
// Only print if some data has been correctly received // Only print if some data has been correctly received
@@ -307,7 +318,10 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read {
in->print(print); in->print(print);
} }
return err.errno; if (err.errno != Read{0})
return unexpected(err.errno);
return in;
} }
auto Bus::send(const Frame *out, Frame::Print print) -> Send { auto Bus::send(const Frame *out, Frame::Print print) -> Send {
+4 -1
View File
@@ -48,9 +48,11 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
namespace avclan { namespace avclan {
@@ -77,7 +79,8 @@ public:
void measure(); void measure();
#endif #endif
Error::Read read(uint16_t address, Frame *in, Frame::Print print); expected<std::unique_ptr<Frame>, Error::Read> read(uint16_t address,
Frame::Print print);
Error::Send send(const Frame *out, Frame::Print print); Error::Send send(const Frame *out, Frame::Print print);
private: private:
+71 -59
View File
@@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "cdchanger.hpp" #include "cdchanger.hpp"
@@ -263,66 +264,77 @@ void CDChanger::handle(const Frame *in, Frame *out) {
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
} }
void CDChanger::react(Frame *out, detail::Error::Send err) { std::unique_ptr<Frame> CDChanger::react(
auto resp = static_cast<reaction_t>(out->reaction); expected<std::unique_ptr<Frame>, detail::SendError> exp) {
out->reaction = r_Nothing;
switch (resp) { if (!exp) {
case r_StateReport: if (exp.error().reaction == to_underlying(r_StateReport) &&
if (err == detail::Error::Send::NAK_ADDRESS && exp.error().err == detail::Error::Send::NAK_ADDRESS &&
++failedStatusReports > 1) { ++failedStatusReports > 1) {
failedStatusReports = 0; failedStatusReports = 0;
stopPlaying(); // Disable periodic updates if e.g. no-one's stopPlaying(); // Disable periodic updates if e.g. no-one's
// listening (car was turned off?) // listening (car was turned off?)
}
break;
case r_Ejection: {
const uint8_t play[] = {0x00,
to_underlying(Device::COMM_CTRL),
to_underlying(Device::COMMUNICATION_V1),
to_underlying(Action::Insertion),
to_underlying(Device::CD_CHANGER),
0x01};
out->length = sizeof(play);
memcpy(out->data, play, sizeof(play));
} }
out->reaction = r_Report_Load; } else {
break; auto out = std::move(exp.value());
case r_Report_Load: auto resp = static_cast<reaction_t>(out->reaction);
out->is_unicast = false; out->reaction = r_Nothing;
out->peripheral_addr = 0x1FF; switch (resp) {
out->length = sizeof(cdloading_resp) + 1; case r_Ejection: {
memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); const uint8_t play[] = {0x00,
out->data[1] = to_underlying(Device::STATUS); to_underlying(Device::COMM_CTRL),
out->data[2] = to_underlying(Action::Loading_Status); to_underlying(Device::COMMUNICATION_V1),
out->reaction = r_SendOnly; to_underlying(Action::Insertion),
break; to_underlying(Device::CD_CHANGER),
case r_TrackChange: 0x01};
setTime(0, 0); out->length = sizeof(play);
cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is memcpy(out->data, play, sizeof(play));
// ~1 sec from now }
[[fallthrough]]; out->reaction = r_Report_Load;
case r_NormalizeState: break;
normalizeState(); case r_Report_Load:
generateStatus(out, false, Device::STATUS); out->is_unicast = false;
out->reaction = r_SendOnly; out->peripheral_addr = 0x1FF;
break; out->length = sizeof(cdloading_resp) + 1;
case r_StartPlaying: memcpy(out->data, cdloading_resp, sizeof(cdloading_resp));
normalizeState(); out->data[1] = to_underlying(Device::STATUS);
generateStatus(out, false, Device::STATUS); out->data[2] = to_underlying(Action::Loading_Status);
out->reaction = r_BeganPlaying; out->reaction = r_SendOnly;
break; break;
case r_BeganPlaying: case r_TrackChange:
startPlaying(); // only start PIT after normalizing state setTime(0, 0);
out->reaction = r_Nothing; cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is
break; // ~1 sec from now
case r_StatusReport: [[fallthrough]];
generateStatus(out, false, Device::STATUS); case r_NormalizeState:
out->reaction = r_SendOnly; normalizeState();
break; generateStatus(out, false, Device::STATUS);
case r_SendOnly: [[fallthrough]]; out->reaction = r_SendOnly;
case r_Nothing: [[fallthrough]]; break;
default: out->reaction = r_Nothing; case r_StartPlaying:
normalizeState();
generateStatus(out, false, Device::STATUS);
out->reaction = r_BeganPlaying;
break;
case r_BeganPlaying:
startPlaying(); // only start PIT after normalizing state
out->reaction = r_Nothing;
break;
case r_StatusReport:
generateStatus(out, false, Device::STATUS);
out->reaction = r_SendOnly;
break;
case r_StateReport: [[fallthrough]];
case r_SendOnly: [[fallthrough]];
case r_Nothing: [[fallthrough]];
default: break;
}
if (out->reaction > r_Nothing)
return out;
} }
return {};
} }
void CDChanger::enable(Frame *out) { void CDChanger::enable(Frame *out) {
@@ -339,11 +351,11 @@ void CDChanger::enable(Frame *out) {
} }
bool CDChanger::pending() { return cdtimer_pending(); } bool CDChanger::pending() { return cdtimer_pending(); }
void CDChanger::resolvepending() { cdtimer_clear(); }
void CDChanger::emit(Frame *out) { void CDChanger::emit(Frame *out) {
generateStatus(out, false, Device::STATUS); generateStatus(out, false, Device::STATUS);
out->reaction = r_StateReport; out->reaction = r_StateReport;
cdtimer_clear();
} }
bool CDChanger::isPlaying() const { return playing; } bool CDChanger::isPlaying() const { return playing; }
+3 -2
View File
@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "device.hpp" #include "device.hpp"
@@ -61,11 +62,11 @@ public:
void init(); void init();
void handle(const Frame *in, Frame *out); void handle(const Frame *in, Frame *out);
void react(Frame *out, detail::Error::Send err); std::unique_ptr<Frame>
react(expected<std::unique_ptr<Frame>, detail::SendError> exp);
void enable(Frame *out); void enable(Frame *out);
void disable(Frame *out); void disable(Frame *out);
static bool pending(); static bool pending();
static void resolvepending();
void emit(Frame *out); void emit(Frame *out);
void incrementTime(); void incrementTime();
bool isPlaying() const; bool isPlaying() const;
+17 -11
View File
@@ -5,9 +5,11 @@
#include <concepts> #include <concepts>
#include <cstdint> #include <cstdint>
#include <memory>
#include "avclan.h" #include "avclan.h"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
namespace avclan { namespace avclan {
@@ -46,15 +48,19 @@ enum class Device : uint8_t {
}; };
template <class T> template <class T>
concept DeviceInterface = requires { concept DeviceInterface =
std::integral_constant<Device, T::id>{}; requires { std::integral_constant<Device, T::id>{}; } &&
} && requires(T dev, const Frame *in, Frame *out, detail::Error::Send err) { requires(T dev, const Frame *in, Frame *out,
dev.init(); expected<std::unique_ptr<Frame>, detail::SendError> exp) {
dev.handle(in, out); dev.init();
dev.enable(out); dev.handle(in, out);
dev.react(out, err); dev.enable(out);
{ dev.pending() } -> std::convertible_to<bool>; {
dev.resolvepending(); dev.react(std::move(exp))
dev.emit(out); } -> std::same_as<std::unique_ptr<Frame>>;
};
{ dev.pending() } -> std::convertible_to<bool>;
// Devices must clear `pending()` after `emit()` is called
dev.emit(out);
};
} // namespace avclan } // namespace avclan
+50
View File
@@ -9,12 +9,62 @@
#include "frame.hpp" #include "frame.hpp"
#if defined(AVCLAN_FRAME_POOL_N)
#include <array>
#include <cstddef>
#include <limits>
#include <new>
namespace {
template <class T, std::uint8_t N>
requires(N >= 1 && N <= std::numeric_limits<uint8_t>::max())
class Pool {
public:
constexpr Pool() {
for (uint8_t i = 0; i < N; ++i)
ptrs_[i] = &storage_[i];
}
T *acquire() {
if (top_ == 0)
return nullptr;
return ptrs_[--top_];
}
void release(T *ptr) {
// Properly would need an origin check/confirmation if this was used more
// generally
ptrs_[top_++] = ptr;
}
private:
std::array<T, N> storage_;
std::array<T *, N> ptrs_;
uint8_t top_ = N;
};
Pool<avclan::Frame, AVCLAN_FRAME_POOL_N> pool;
} // namespace
#endif
namespace { namespace {
using Error = avclan::detail::Error; using Error = avclan::detail::Error;
using enum Error::Parse; using enum Error::Parse;
} // namespace } // namespace
namespace avclan { namespace avclan {
#if defined(AVCLAN_FRAME_POOL_N)
void *Frame::operator new(std::size_t /*count*/,
const std::nothrow_t & /*tag*/) noexcept {
return pool.acquire();
}
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
void Frame::operator delete(void *ptr) noexcept {
pool.release(static_cast<Frame *>(ptr));
}
#endif
void Frame::print(Frame::Print print) const { void Frame::print(Frame::Print print) const {
if (print.binary) { if (print.binary) {
uint8_t buffer[8]; uint8_t buffer[8];
+19 -3
View File
@@ -9,6 +9,11 @@
#include "avclan.h" #include "avclan.h"
#if defined(AVCLAN_FRAME_POOL_N)
#include <cstddef>
#include <new>
#endif
namespace avclan { namespace avclan {
enum class Device : uint8_t; enum class Device : uint8_t;
@@ -25,13 +30,24 @@ struct Frame {
Error::Parse parse(const uint8_t *bytes, uint8_t len); Error::Parse parse(const uint8_t *bytes, uint8_t len);
void print(Print print) const; void print(Print print) const;
uint8_t reaction; #if defined(AVCLAN_FRAME_POOL_N)
Device owning_device; // O(1) heapless pooled allocation. Only `new (std::nothrow) Frame` is
// supported.
static void *operator new(std::size_t) = delete;
static void *operator new(std::size_t, const std::nothrow_t &) noexcept;
// NOLINTNEXTLINE(misc-new-delete-overloads) false-positive
static void operator delete(void *) noexcept;
#endif
// reaction has a Device-defined interpretation, with the sole invariant that
// 0 == inactive/non-sendable frame
uint8_t reaction = 0;
Device owning_device = NoDevice;
bool is_unicast; bool is_unicast;
uint16_t controller_addr; // formerly "master" uint16_t controller_addr; // formerly "master"
uint16_t peripheral_addr; // formerly "slave" uint16_t peripheral_addr; // formerly "slave"
uint8_t control = 0xF; uint8_t control = 0xF;
uint8_t length; uint8_t length = 0;
uint8_t data[MAXLENGTH]; uint8_t data[MAXLENGTH];
}; };
} // namespace avclan } // namespace avclan
+82 -42
View File
@@ -4,16 +4,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <tuple>
#include <utility>
#include "avclan.h" #include "avclan.h"
#include "bus.hpp" #include "bus.hpp"
#include "device.hpp" #include "device.hpp"
#include "frame.hpp" #include "frame.hpp"
#include "stdshim.hpp"
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <tuple>
#include <utility>
namespace avclan { namespace avclan {
@@ -25,6 +29,14 @@ template <DeviceInterface... Devs> class Peripheral {
public: public:
using Error = detail::Error; using Error = detail::Error;
// lack of static reflection until C++26 limits our methods of doing a
// compile-time collision check. Within Peripheral, we can at least assert no
// collision with Devices *used in a particular instantiation* but it is not a
// universal check against all Device enum values
static_assert(((Devs::id != NoDevice) && ...),
"a registered Device id collides with the NoDevice sentinel; "
"update the sentinel value in avclan.h");
Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} { Peripheral(Bus &bus, uint16_t address) : bus{bus}, address_{address} {
bus.init(); bus.init();
(std::get<Devs>(devices_).init(), ...); (std::get<Devs>(devices_).init(), ...);
@@ -43,32 +55,46 @@ public:
Bus &get_bus() { return bus; } Bus &get_bus() { return bus; }
#endif #endif
Error::Read read(Frame *in, Frame::Print print) { expected<std::unique_ptr<Frame>, Error::Read>
return bus.read(address_, in, print); read(Frame::Print print = Frame::Print{}) {
return bus.read(address_, print);
}; };
Error::Send send(Frame *out, Frame::Print print) {
expected<std::unique_ptr<Frame>, detail::SendError>
send(std::unique_ptr<Frame> out, Frame::Print print = Frame::Print{}) {
// To "forge" a controller_addr, instantiate a new/different Peripheral // To "forge" a controller_addr, instantiate a new/different Peripheral
stamp<Sender>(out); stamp<Sender>(out.get());
out->control = 0xF; out->control = 0xF;
return bus.send(out, print); auto err = bus.send(out.get(), print);
if (err != Error::Send{0})
return unexpected{
detail::SendError{out->owning_device, out->reaction, err}};
return out;
}; };
#define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c)) #define PACK3(a, b, c) (((uint32_t)(a) << 16) | ((uint32_t)(b) << 8) | (c))
void route(const Frame *in, Frame *out) { // expected needed to distinguish don't vs can't respond
expected<std::unique_ptr<Frame>, Error::Read> route(const Frame *in) {
using enum Device; using enum Device;
using enum Action; using enum Action;
out->reaction = 0;
if (is_muted() || in->length < 3) if (is_muted() || in->length < 3)
return; return {};
std::unique_ptr<Frame> out(new (std::nothrow) Frame);
if (!out) {
puts("!! failed Frame alloc in route !!");
return unexpected{Error::Read::POOL_EMPTY};
}
// 0xFF placeholders are variant bytes filled by writing directly to // 0xFF placeholders are variant bytes filled by writing directly to
// out->data[N] after memcpy. // out->data[N] after memcpy.
static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL), static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL),
to_underlying(LAN), 0xFF, 0xFF}; to_underlying(LAN), 0xFF, 0xFF};
stamp<Recipient>(out); stamp<Recipient>(out.get());
const uint8_t *data = in->data; const uint8_t *data = in->data;
const uint8_t b0 = *data++; const uint8_t b0 = *data++;
@@ -114,7 +140,7 @@ public:
to_underlying(Advertise_Function)): { to_underlying(Advertise_Function)): {
auto enable_d = [](auto &d, auto &out) { d.enable(out); }; auto enable_d = [](auto &d, auto &out) { d.enable(out); };
((Devs::id == static_cast<Device>(b3) ((Devs::id == static_cast<Device>(b3)
? originate(std::get<Devs>(devices_), out, enable_d) ? originate(std::get<Devs>(devices_), out.get(), enable_d)
: void()), : void()),
...); ...);
break; break;
@@ -135,7 +161,7 @@ public:
case PACK3(COMMUNICATION_V2, COMM_CTRL, case PACK3(COMMUNICATION_V2, COMM_CTRL,
to_underlying(List_Functions_Req)): { to_underlying(List_Functions_Req)): {
controller_ = in->controller_addr; controller_ = in->controller_addr;
stamp<Recipient>(out); // re-stamp now that controller_ is known stamp<Recipient>(out.get()); // re-stamp now that controller_ is known
out->is_unicast = true; out->is_unicast = true;
const uint8_t list_functions_resp[] = { const uint8_t list_functions_resp[] = {
0x00, to_underlying(COMM_CTRL), from, 0x00, to_underlying(COMM_CTRL), from,
@@ -151,40 +177,54 @@ public:
} else if (in->peripheral_addr == address_ && b0 == 0x00) { } else if (in->peripheral_addr == address_ && b0 == 0x00) {
auto handle_d = [&](auto &d, auto &out) { d.handle(in, out); }; auto handle_d = [&](auto &d, auto &out) { d.handle(in, out); };
((Devs::id == static_cast<Device>(b2) ((Devs::id == static_cast<Device>(b2)
? originate(std::get<Devs>(devices_), out, handle_d) ? originate(std::get<Devs>(devices_), out.get(), handle_d)
: void()), : void()),
...); ...);
} }
if (out->reaction > 0)
return out;
return {};
} }
#undef PACK3 #undef PACK3
void react(Frame *out, Error::Send err) { std::unique_ptr<Frame>
if (((Devs::id == out->owning_device) || ...)) react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
((Devs::id == out->owning_device const Device from =
? void(std::get<Devs>(devices_).react(out, err)) exp ? exp.value()->owning_device : exp.error().owning_device;
: void()),
...); std::unique_ptr<Frame> next;
else ((Devs::id == from &&
out->reaction = 0; (next = std::get<Devs>(devices_).react(std::move(exp)))) ||
...);
return next;
} }
bool pending() const { return (std::get<Devs>(devices_).pending() || ...); }
// Service ready devices in round-robin order // Service ready devices in round-robin order
bool emit(Frame *out) { std::unique_ptr<Frame> poll() {
auto does_emit = [&](DeviceInterface auto &dev) -> bool { using U = std::unique_ptr<Frame>;
auto does_emit = [&](DeviceInterface auto &dev) -> U {
if (!dev.pending()) if (!dev.pending())
return false; return {};
originate(dev, out, [](auto &d, auto &out) { d.emit(out); });
dev.resolvepending(); U out(new (std::nothrow) Frame);
return true; if (!out) {
puts("!! failed Frame alloc in poll !!");
return {};
}
originate(dev, out.get(), [](auto &d, auto &out) { d.emit(out); });
return out;
}; };
// Runtime tuple index helper // Runtime tuple index helper
auto does_index_emit = [&](std::size_t t) -> bool { auto does_index_emit = [&](std::size_t t) -> U {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) { return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> U {
return (((Is == t) && does_emit(std::get<Is>(devices_))) || ...); U out;
((Is == t && (out = does_emit(std::get<Is>(devices_)))) || ...);
return out;
}(std::index_sequence_for<Devs...>{}); }(std::index_sequence_for<Devs...>{});
}; };
@@ -195,16 +235,16 @@ public:
static uint8_t rr_ = 0; // round-robin cursor static uint8_t rr_ = 0; // round-robin cursor
const std::size_t start = rr_; const std::size_t start = rr_;
for (std::size_t t = start; t < N; ++t) // [start, N) for (std::size_t t = start; t < N; ++t) // [start, N)
if (does_index_emit(t)) { if (auto out = does_index_emit(t)) {
rr_ = (t + 1 == N) ? 0 : t + 1; rr_ = (t + 1 == N) ? 0 : t + 1;
return true; return out;
} }
for (std::size_t t = 0; t < start; ++t) // [0, start); t+1 <= start < N for (std::size_t t = 0; t < start; ++t) // [0, start); t+1 <= start < N
if (does_index_emit(t)) { if (auto out = does_index_emit(t)) {
rr_ = t + 1; rr_ = t + 1;
return true; return out;
} }
return false; return {};
} }
} }
+25
View File
@@ -0,0 +1,25 @@
// Copyright (C) 2026 Allen Hill <allenofthehills@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <version>
#if !defined(__cpp_lib_expected) || __cpp_lib_expected < 202211L
#include <tl/expected.hpp>
namespace avclan {
template <class T, class E> using expected = tl::expected<T, E>;
template <class E> using unexpected = tl::unexpected<E>;
using tl::unexpect;
using tl::unexpect_t;
} // namespace avclan
#else
#include <expected>
namespace avclan {
template <class T, class E> using expected = std::expected<T, E>;
template <class E> using unexpected = std::unexpected<E>;
using std::unexpect;
using std::unexpect_t;
} // namespace avclan
#endif
@@ -76,6 +76,9 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS
# tick. 1000 = no correction; set per-board in CMakeUserPresets.json. # tick. 1000 = no correction; set per-board in CMakeUserPresets.json.
set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)") set(RTC_STATUS_PERIOD_MS 1000 CACHE STRING "Measured ms per nominal RTC status period (1000 = no correction)")
# --- Firmware configuration options ----------------------------------------
set(AVCLAN_FRAME_POOL_N 32 CACHE STRING "Frame pool depth (pooled targets)")
try_compile(LIBC_VERSION_TEST try_compile(LIBC_VERSION_TEST
SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp" SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp"
COMPILE_DEFINITIONS -mmcu=${AVR_MCU} COMPILE_DEFINITIONS -mmcu=${AVR_MCU}
@@ -85,11 +88,14 @@ FetchContent_Declare(
avr_libstdcpp avr_libstdcpp
GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git
GIT_TAG 5354296040a2289c911062daa82336762231e897 GIT_TAG 5354296040a2289c911062daa82336762231e897
SYSTEM
) )
FetchContent_MakeAvailable(avr_libstdcpp) FetchContent_MakeAvailable(avr_libstdcpp)
add_library(libstdcpp INTERFACE) add_library(libstdcpp STATIC ${avr_libstdcpp_SOURCE_DIR}/src/new_handler.cc)
target_include_directories(libstdcpp SYSTEM target_include_directories(libstdcpp
INTERFACE ${avr_libstdcpp_SOURCE_DIR}/include) PUBLIC ${avr_libstdcpp_SOURCE_DIR}/include)
# Let --gc-sections drop the unused set/get_new_handler functions.
target_compile_options(libstdcpp PRIVATE -ffunction-sections -fdata-sections)
target_link_libraries(avclan PUBLIC libstdcpp) target_link_libraries(avclan PUBLIC libstdcpp)
if(NOT LIBC_VERSION_TEST) if(NOT LIBC_VERSION_TEST)
@@ -142,6 +148,7 @@ target_compile_definitions(avclan PUBLIC
__CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV} __CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV}
TCB_CLKSEL=${TCB_CLKSEL} TCB_CLKSEL=${TCB_CLKSEL}
RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS} RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS}
AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N}
) )
target_compile_options(avclan PUBLIC target_compile_options(avclan PUBLIC
--param=min-pagesize=0 --param=min-pagesize=0
+47 -24
View File
@@ -8,33 +8,44 @@
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <type_traits>
namespace detail { namespace detail {
template <class T, auto N> struct Deleter; template <class T, auto N> struct Deleter;
} }
template <class T, std::integral auto N, bool Owning = false> template <class T, std::integral auto N, bool Owning = false,
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max()) class Deleter = std::default_delete<T>>
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max() &&
// push() drops the passed-in deleter and pop() fabricates a fresh
// one, so a deleter must either derive its state from the queue
// itself (the pool Deleter) or carry no state at all
(std::is_same_v<Deleter, detail::Deleter<T, N>> ||
(std::is_empty_v<Deleter> &&
std::is_nothrow_default_constructible_v<Deleter>)))
class Queue { class Queue {
using Deleter = detail::template Deleter<T, N>; friend detail::Deleter<T, N>;
friend Deleter;
public: public:
// Only full and copy-convert-from-full construction is allowed // Only empty construction is allowed for non-Owning, non-pool deleters
Queue() = delete; constexpr Queue()
requires(!Owning && !std::is_same_v<Deleter, detail::Deleter<T, N>>)
: owner{nullptr} {}
Queue(const Queue &) = delete; Queue(const Queue &) = delete;
// Moving is unsupported due to being self-referential // Moving is unsupported due to being self-referential
Queue(Queue &&) = delete; Queue(Queue &&) = delete;
Queue &operator=(Queue &&) = delete; Queue &operator=(Queue &&) = delete;
// Construct from pre-defined storage
constexpr Queue(T (&items)[N]) constexpr Queue(T (&items)[N])
requires(Owning) requires(Owning) && std::same_as<Deleter, detail::Deleter<T, N>>
: owner{this}, write{N} { : owner{this}, write{N} {
for (uint8_t i = 0; i < N; ++i) for (uint8_t i = 0; i < N; ++i)
buf[i] = &items[i]; buf[i] = &items[i];
} }
constexpr Queue(Queue<T, N, true> &queue) // Construct non-Owning empty from Owning queue
constexpr Queue(Queue<T, N, true, Deleter> &queue)
requires(!Owning) requires(!Owning)
: owner{&queue} {} : owner{&queue} {}
@@ -46,14 +57,22 @@ public:
uint8_t push(std::unique_ptr<T, Deleter> x) uint8_t push(std::unique_ptr<T, Deleter> x)
requires(!Owning) requires(!Owning)
{ {
// structurally unnecessary; empty construction from same size parent if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) {
// guarantees // structurally unnecessary for detail::Deleter, where empty construction
// !isFull assert(!isFull()); // is only from same size parent
if (isFull())
return 1;
}
if (!x || !x.get_deleter().is_owned_by(owner)) if (!x)
return 1; return 1;
reclaim(x.release()); if constexpr (std::is_same_v<Deleter, detail::Deleter<T, N>>) {
if (!x.get_deleter().is_owned_by(owner))
return 1;
}
claim(x.release());
return 0; return 0;
} }
@@ -69,35 +88,39 @@ public:
if (isEmpty()) if (isEmpty())
return nullptr; return nullptr;
if constexpr (Owning) if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>)
return {buf[mask(read++)], {this}}; return {buf[mask(read++)], Deleter{}};
else if constexpr (Owning)
return {buf[mask(read++)], Deleter{this}};
else else
return {buf[mask(read++)], {owner}}; return {buf[mask(read++)], Deleter{owner}};
} }
private: private:
uint8_t mask(uint8_t pos) const { return pos & (N - 1); } uint8_t mask(uint8_t pos) const { return pos & (N - 1); }
void reclaim(T *x) { buf[mask(write++)] = x; } void claim(T *x) { buf[mask(write++)] = x; }
std::array<T *, N> buf = {}; std::array<T *, N> buf = {};
Queue<T, N, true> *const owner; Queue<T, N, true, Deleter> *const owner;
uint8_t read = 0; uint8_t read = 0;
uint8_t write = 0; uint8_t write = 0;
}; };
template <class T, auto N> Queue(Queue<T, N, true> &) -> Queue<T, N, false>; template <class T, auto N, class D>
template <class T, auto N> Queue(T (&items)[N]) -> Queue<T, N, true>; Queue(Queue<T, N, true, D> &) -> Queue<T, N, false, D>;
template <class T, auto N>
Queue(T (&items)[N]) -> Queue<T, N, true, detail::Deleter<T, N>>;
namespace detail { namespace detail {
template <class T, auto N> struct Deleter { template <class T, auto N> struct Deleter {
Deleter() = default; Deleter() = default;
constexpr Deleter(Queue<T, N, true> *owner) : owner{owner} {} constexpr Deleter(Queue<T, N, true, Deleter> *owner) : owner{owner} {}
void operator()(T *x) const { owner->reclaim(x); } void operator()(T *x) const { owner->claim(x); }
constexpr bool is_owned_by(const Queue<T, N, true> *parent) const { constexpr bool is_owned_by(const Queue<T, N, true, Deleter> *parent) const {
return owner == parent; return owner == parent;
} }
private: private:
Queue<T, N, true> *const owner = nullptr; Queue<T, N, true, Deleter> *const owner = nullptr;
}; };
} // namespace detail } // namespace detail
+33 -39
View File
@@ -7,6 +7,8 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <new>
#include <utility>
#include "cdchanger.hpp" #include "cdchanger.hpp"
#include "frame.hpp" #include "frame.hpp"
@@ -14,6 +16,7 @@
#include "hal/stdio.h" #include "hal/stdio.h"
#include "peripheral.hpp" #include "peripheral.hpp"
#include "queue.hpp" #include "queue.hpp"
#include "stdshim.hpp"
const char *const offon[] = {"OFF", "ON"}; const char *const offon[] = {"OFF", "ON"};
@@ -22,11 +25,8 @@ constexpr uint8_t CACHE_SIZE = 32;
using namespace avclan; using namespace avclan;
namespace { namespace {
Frame frames[CACHE_SIZE]; constinit Queue<Frame, CACHE_SIZE> incoming;
constinit Queue<Frame, CACHE_SIZE> outgoing;
constinit Queue cache(frames);
constinit Queue incoming = cache;
constinit Queue outgoing = cache;
uint8_t hexChars[2]; uint8_t hexChars[2];
uint8_t hexDigit = 0; // current digit being written to hexChars uint8_t hexDigit = 0; // current digit being written to hexChars
@@ -66,7 +66,6 @@ int main() {
using enum Action; using enum Action;
using enum Device; using enum Device;
Peripheral<CDChanger> peripheral(phy, 0x360); Peripheral<CDChanger> peripheral(phy, 0x360);
using Error = decltype(peripheral)::Error;
using Print = Frame::Print; using Print = Frame::Print;
Setup(); Setup();
@@ -74,40 +73,31 @@ int main() {
while (true) { while (true) {
if (peripheral.bus_is_active()) { if (peripheral.bus_is_active()) {
if (auto msg = cache.pop()) { if (auto msg = peripheral.read(Print{.print = printAllFrames,
auto err = peripheral.read(msg.get(), Print{.print = printAllFrames, .binary = printBinary,
.binary = printBinary, .verbose = verbose}))
.verbose = verbose}); incoming.push(std::move(*msg));
if (err == Error::Read{0x00})
incoming.push(std::move(msg));
} else {
puts("!! Dropping an incoming message; cache is empty !!");
}
} }
if (const auto *in = incoming.peek()) { if (const auto *in = incoming.peek()) {
if (auto out = cache.pop()) { if (auto resp = peripheral.route(*in)) {
peripheral.route(in, out.get());
incoming.pop(); incoming.pop();
if (*resp) {
if (out->reaction > 0) outgoing.push(std::move(*resp));
outgoing.push(std::move(out)); continue; // route can be long; re-check the bus before poll/send
} else { }
puts("!! Unable to respond; cache is empty !!");
} }
} }
if (peripheral.pending()) { if (auto msg = peripheral.poll())
if (auto out = cache.pop(); out && peripheral.emit(out.get())) outgoing.push(std::move(msg));
outgoing.push(std::move(out));
}
if (auto out = outgoing.pop()) { if (auto out = outgoing.pop()) {
auto err = peripheral.send( auto result =
out.get(), Print{.print = printAllFrames, .binary = printBinary}); peripheral.send(std::move(out), Print{.print = printAllFrames,
peripheral.react(out.get(), err); .binary = printBinary});
if (out->reaction > 0) if (auto next = peripheral.react(std::move(result)))
outgoing.push(std::move(out)); outgoing.push(std::move(next));
} }
// stdin must be non-blocking: yielding EOF when idle/empty // stdin must be non-blocking: yielding EOF when idle/empty
@@ -129,7 +119,7 @@ int main() {
case 'x': set_flag(&printBinary, false, "Binary:"); break; case 'x': set_flag(&printBinary, false, "Binary:"); break;
case 'E': // Beep case 'E': // Beep
if (auto out = cache.pop()) { if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -141,10 +131,10 @@ int main() {
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} else } else
puts("!! Cache empty; unable to queue beep request"); puts("!! failed Frame alloc for Beep !! ");
break; break;
case 'P': case 'P':
if (auto out = cache.pop()) { if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
out->is_unicast = true; out->is_unicast = true;
out->peripheral_addr = peripheral.controller(); out->peripheral_addr = peripheral.controller();
{ {
@@ -159,7 +149,8 @@ int main() {
} }
out->reaction = CDChanger::reaction_t::r_Ejection; out->reaction = CDChanger::reaction_t::r_Ejection;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} } else
puts("!! failed Frame alloc for Play !! ");
break; break;
#ifndef NDEBUG #ifndef NDEBUG
@@ -218,19 +209,21 @@ int main() {
if (readSeq && seqIdx > 0) { if (readSeq && seqIdx > 0) {
if (readBinary) { if (readBinary) {
if (data_tmp[seqIdx - 1] == 0x17) { if (data_tmp[seqIdx - 1] == 0x17) {
if (auto out = cache.pop()) { if (auto out =
std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
if (out->parse(data_tmp, --seqIdx) == if (out->parse(data_tmp, --seqIdx) ==
Frame::Error::Parse{0}) { Frame::Error::Parse{0}) {
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} }
} } else
puts("!! failed Frame alloc for input message !!");
readSeq = readBinary = false; readSeq = readBinary = false;
} else } else
goto DEFAULT; // reading binary and this is a real data byte; goto DEFAULT; // reading binary and this is a real data byte;
// fall through to default // fall through to default
} else if (seqIdx <= Frame::MAXLENGTH) { } else if (seqIdx <= Frame::MAXLENGTH) {
if (auto out = cache.pop()) { if (auto out = std::unique_ptr<Frame>(new (std::nothrow) Frame)) {
out->is_unicast = seqIsUnicast; out->is_unicast = seqIsUnicast;
out->peripheral_addr = out->peripheral_addr =
seqIsUnicast ? peripheral.controller() : 0x1FF; seqIsUnicast ? peripheral.controller() : 0x1FF;
@@ -238,7 +231,8 @@ int main() {
memcpy(out->data, data_tmp, seqIdx); memcpy(out->data, data_tmp, seqIdx);
out->reaction = 1; out->reaction = 1;
outgoing.push(std::move(out)); outgoing.push(std::move(out));
} } else
puts("!! failed Frame alloc for input message !!");
printAllFrames = lastPrintAllFrames; printAllFrames = lastPrintAllFrames;
} }
break; break;