diff --git a/CMakeLists.txt b/CMakeLists.txt index a0982fc..4cbb5a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) add_compile_options( -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. 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 + #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 +# #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 src/sniffer.cc ) diff --git a/cmake/apply-patch.cmake b/cmake/apply-patch.cmake new file mode 100644 index 0000000..4382bbb --- /dev/null +++ b/cmake/apply-patch.cmake @@ -0,0 +1,18 @@ +# Idempotent patch driver for FetchContent PATCH_COMMAND steps. +# +# Usage: cmake -DGIT_EXECUTABLE= -DPATCH_FILE= -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() diff --git a/cmake/tl-expected-no-exception-header.patch b/cmake/tl-expected-no-exception-header.patch new file mode 100644 index 0000000..239beda --- /dev/null +++ b/cmake/tl-expected-no-exception-header.patch @@ -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 ++#include + #include + #include + #include +@@ -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 struct expected_default_ctor_base { + }; + } // namespace detail + +-template class bad_expected_access : public std::exception { ++template 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"; + } + diff --git a/cmake/tl-optional-no-exception-header.patch b/cmake/tl-optional-no-exception-header.patch new file mode 100644 index 0000000..6e4f43f --- /dev/null +++ b/cmake/tl-optional-no-exception-header.patch @@ -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 + #include + #include + #include +@@ -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"; } diff --git a/src/avclan/avclan.h b/src/avclan/avclan.h index f9d2c63..303b16a 100644 --- a/src/avclan/avclan.h +++ b/src/avclan/avclan.h @@ -121,6 +121,12 @@ enum AVCLAN_ENUM_CLASS MediaAction : uint8_t { }; #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 { struct Error { #endif @@ -140,6 +146,7 @@ struct Error { STARTBIT_TOO_SHORT, STARTBIT_TOO_LONG, BAD_STARTBIT, + POOL_EMPTY, // non-bus error }; enum AVCLAN_ENUM_CLASS Send : uint8_t { @@ -160,6 +167,12 @@ struct Error { #ifdef __cplusplus }; + +struct SendError { + Device owning_device; + uint8_t reaction; + Error::Send err; +}; #endif enum AVCLAN_ENUM_CLASS Bit : uint8_t { diff --git a/src/avclan/bus.cc b/src/avclan/bus.cc index ac4b209..df0a562 100644 --- a/src/avclan/bus.cc +++ b/src/avclan/bus.cc @@ -31,11 +31,14 @@ #include #include +#include +#include #include "avclan.h" #include "bus.hpp" #include "frame.hpp" #include "hal/phy.h" +#include "stdshim.hpp" namespace { 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 }; -auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read { +auto Bus::read(uint16_t address, Frame::Print print) + -> expected, Error::Read> { struct errtype { Read errno; uint16_t val; @@ -199,6 +203,12 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read { using enum Read; + std::unique_ptr in(new (std::nothrow) Frame); + if (!in) { + err.errno = POOL_EMPTY; + goto handle_err; + } + { // bound handle lifetime auto handle = get(); @@ -277,6 +287,7 @@ auto Bus::read(uint16_t address, Frame *in, Frame::Print print) -> Read { handle_err:; fputs("ERR(read): ", stdout); switch (err.errno) { + case POOL_EMPTY: puts("failed Frame alloc"); 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_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); } } - putchar('\n'); + puts(":"); } // 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); } - return err.errno; + if (err.errno != Read{0}) + return unexpected(err.errno); + + return in; } auto Bus::send(const Frame *out, Frame::Print print) -> Send { diff --git a/src/avclan/bus.hpp b/src/avclan/bus.hpp index 487d26d..c5b0046 100644 --- a/src/avclan/bus.hpp +++ b/src/avclan/bus.hpp @@ -48,9 +48,11 @@ #pragma once #include +#include #include "avclan.h" #include "frame.hpp" +#include "stdshim.hpp" namespace avclan { @@ -77,7 +79,8 @@ public: void measure(); #endif - Error::Read read(uint16_t address, Frame *in, Frame::Print print); + expected, Error::Read> read(uint16_t address, + Frame::Print print); Error::Send send(const Frame *out, Frame::Print print); private: diff --git a/src/avclan/cdchanger.cc b/src/avclan/cdchanger.cc index ff5b4b9..11903d4 100644 --- a/src/avclan/cdchanger.cc +++ b/src/avclan/cdchanger.cc @@ -5,6 +5,7 @@ #include #include +#include #include "avclan.h" #include "cdchanger.hpp" @@ -263,66 +264,77 @@ void CDChanger::handle(const Frame *in, Frame *out) { #pragma GCC diagnostic pop } -void CDChanger::react(Frame *out, detail::Error::Send err) { - auto resp = static_cast(out->reaction); - out->reaction = r_Nothing; - switch (resp) { - case r_StateReport: - if (err == detail::Error::Send::NAK_ADDRESS && - ++failedStatusReports > 1) { - failedStatusReports = 0; - stopPlaying(); // Disable periodic updates if e.g. no-one's - // 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)); +std::unique_ptr CDChanger::react( + expected, detail::SendError> exp) { + + if (!exp) { + if (exp.error().reaction == to_underlying(r_StateReport) && + exp.error().err == detail::Error::Send::NAK_ADDRESS && + ++failedStatusReports > 1) { + failedStatusReports = 0; + stopPlaying(); // Disable periodic updates if e.g. no-one's + // listening (car was turned off?) } - out->reaction = r_Report_Load; - break; - case r_Report_Load: - out->is_unicast = false; - out->peripheral_addr = 0x1FF; - out->length = sizeof(cdloading_resp) + 1; - memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); - out->data[1] = to_underlying(Device::STATUS); - out->data[2] = to_underlying(Action::Loading_Status); - out->reaction = r_SendOnly; - break; - case r_TrackChange: - setTime(0, 0); - cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is - // ~1 sec from now - [[fallthrough]]; - case r_NormalizeState: - normalizeState(); - generateStatus(out, false, Device::STATUS); - out->reaction = r_SendOnly; - break; - 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_SendOnly: [[fallthrough]]; - case r_Nothing: [[fallthrough]]; - default: out->reaction = r_Nothing; + } else { + auto out = std::move(exp.value()); + auto resp = static_cast(out->reaction); + out->reaction = r_Nothing; + switch (resp) { + 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; + break; + case r_Report_Load: + out->is_unicast = false; + out->peripheral_addr = 0x1FF; + out->length = sizeof(cdloading_resp) + 1; + memcpy(out->data, cdloading_resp, sizeof(cdloading_resp)); + out->data[1] = to_underlying(Device::STATUS); + out->data[2] = to_underlying(Action::Loading_Status); + out->reaction = r_SendOnly; + break; + case r_TrackChange: + setTime(0, 0); + cdtimer_reset(); // Skipped to a whole/round sec; ensure next tick is + // ~1 sec from now + [[fallthrough]]; + case r_NormalizeState: + normalizeState(); + generateStatus(out, false, Device::STATUS); + out->reaction = r_SendOnly; + break; + 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) { @@ -339,11 +351,11 @@ void CDChanger::enable(Frame *out) { } bool CDChanger::pending() { return cdtimer_pending(); } -void CDChanger::resolvepending() { cdtimer_clear(); } void CDChanger::emit(Frame *out) { generateStatus(out, false, Device::STATUS); out->reaction = r_StateReport; + cdtimer_clear(); } bool CDChanger::isPlaying() const { return playing; } diff --git a/src/avclan/cdchanger.hpp b/src/avclan/cdchanger.hpp index f15b296..8bdf845 100644 --- a/src/avclan/cdchanger.hpp +++ b/src/avclan/cdchanger.hpp @@ -6,6 +6,7 @@ #pragma once #include +#include #include "avclan.h" #include "device.hpp" @@ -61,11 +62,11 @@ public: void init(); void handle(const Frame *in, Frame *out); - void react(Frame *out, detail::Error::Send err); + std::unique_ptr + react(expected, detail::SendError> exp); void enable(Frame *out); void disable(Frame *out); static bool pending(); - static void resolvepending(); void emit(Frame *out); void incrementTime(); bool isPlaying() const; diff --git a/src/avclan/device.hpp b/src/avclan/device.hpp index 2bf5a9f..d00577f 100644 --- a/src/avclan/device.hpp +++ b/src/avclan/device.hpp @@ -5,9 +5,11 @@ #include #include +#include #include "avclan.h" #include "frame.hpp" +#include "stdshim.hpp" namespace avclan { @@ -46,15 +48,19 @@ enum class Device : uint8_t { }; template -concept DeviceInterface = requires { - std::integral_constant{}; -} && requires(T dev, const Frame *in, Frame *out, detail::Error::Send err) { - dev.init(); - dev.handle(in, out); - dev.enable(out); - dev.react(out, err); - { dev.pending() } -> std::convertible_to; - dev.resolvepending(); - dev.emit(out); -}; +concept DeviceInterface = + requires { std::integral_constant{}; } && + requires(T dev, const Frame *in, Frame *out, + expected, detail::SendError> exp) { + dev.init(); + dev.handle(in, out); + dev.enable(out); + { + dev.react(std::move(exp)) + } -> std::same_as>; + + { dev.pending() } -> std::convertible_to; + // Devices must clear `pending()` after `emit()` is called + dev.emit(out); + }; } // namespace avclan diff --git a/src/avclan/frame.cc b/src/avclan/frame.cc index a7a6780..0c3e5c9 100644 --- a/src/avclan/frame.cc +++ b/src/avclan/frame.cc @@ -9,12 +9,62 @@ #include "frame.hpp" +#if defined(AVCLAN_FRAME_POOL_N) + #include + #include + #include + #include + +namespace { +template + requires(N >= 1 && N <= std::numeric_limits::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 storage_; + std::array ptrs_; + uint8_t top_ = N; +}; + +Pool pool; +} // namespace +#endif + namespace { using Error = avclan::detail::Error; using enum Error::Parse; } // namespace 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(ptr)); +} +#endif + void Frame::print(Frame::Print print) const { if (print.binary) { uint8_t buffer[8]; diff --git a/src/avclan/frame.hpp b/src/avclan/frame.hpp index 056f45c..e327f14 100644 --- a/src/avclan/frame.hpp +++ b/src/avclan/frame.hpp @@ -9,6 +9,11 @@ #include "avclan.h" +#if defined(AVCLAN_FRAME_POOL_N) + #include + #include +#endif + namespace avclan { enum class Device : uint8_t; @@ -25,13 +30,24 @@ struct Frame { Error::Parse parse(const uint8_t *bytes, uint8_t len); void print(Print print) const; - uint8_t reaction; - Device owning_device; +#if defined(AVCLAN_FRAME_POOL_N) + // 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; uint16_t controller_addr; // formerly "master" uint16_t peripheral_addr; // formerly "slave" uint8_t control = 0xF; - uint8_t length; + uint8_t length = 0; uint8_t data[MAXLENGTH]; }; } // namespace avclan diff --git a/src/avclan/peripheral.hpp b/src/avclan/peripheral.hpp index 0ff1e22..96c1e3b 100644 --- a/src/avclan/peripheral.hpp +++ b/src/avclan/peripheral.hpp @@ -4,16 +4,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include +#include +#include +#include +#include +#include +#include +#include + #include "avclan.h" #include "bus.hpp" #include "device.hpp" #include "frame.hpp" - -#include -#include -#include -#include -#include +#include "stdshim.hpp" namespace avclan { @@ -25,6 +29,14 @@ template class Peripheral { public: 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} { bus.init(); (std::get(devices_).init(), ...); @@ -43,32 +55,46 @@ public: Bus &get_bus() { return bus; } #endif - Error::Read read(Frame *in, Frame::Print print) { - return bus.read(address_, in, print); + expected, Error::Read> + read(Frame::Print print = Frame::Print{}) { + return bus.read(address_, print); }; - Error::Send send(Frame *out, Frame::Print print) { + + expected, detail::SendError> + send(std::unique_ptr out, Frame::Print print = Frame::Print{}) { // To "forge" a controller_addr, instantiate a new/different Peripheral - stamp(out); + stamp(out.get()); 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)) - void route(const Frame *in, Frame *out) { + // expected needed to distinguish don't vs can't respond + expected, Error::Read> route(const Frame *in) { using enum Device; using enum Action; - out->reaction = 0; if (is_muted() || in->length < 3) - return; + return {}; + + std::unique_ptr 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 // out->data[N] after memcpy. static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL), to_underlying(LAN), 0xFF, 0xFF}; - stamp(out); + stamp(out.get()); const uint8_t *data = in->data; const uint8_t b0 = *data++; @@ -114,7 +140,7 @@ public: to_underlying(Advertise_Function)): { auto enable_d = [](auto &d, auto &out) { d.enable(out); }; ((Devs::id == static_cast(b3) - ? originate(std::get(devices_), out, enable_d) + ? originate(std::get(devices_), out.get(), enable_d) : void()), ...); break; @@ -135,7 +161,7 @@ public: case PACK3(COMMUNICATION_V2, COMM_CTRL, to_underlying(List_Functions_Req)): { controller_ = in->controller_addr; - stamp(out); // re-stamp now that controller_ is known + stamp(out.get()); // re-stamp now that controller_ is known out->is_unicast = true; const uint8_t list_functions_resp[] = { 0x00, to_underlying(COMM_CTRL), from, @@ -151,40 +177,54 @@ public: } else if (in->peripheral_addr == address_ && b0 == 0x00) { auto handle_d = [&](auto &d, auto &out) { d.handle(in, out); }; ((Devs::id == static_cast(b2) - ? originate(std::get(devices_), out, handle_d) + ? originate(std::get(devices_), out.get(), handle_d) : void()), ...); } + + if (out->reaction > 0) + return out; + + return {}; } #undef PACK3 - void react(Frame *out, Error::Send err) { - if (((Devs::id == out->owning_device) || ...)) - ((Devs::id == out->owning_device - ? void(std::get(devices_).react(out, err)) - : void()), - ...); - else - out->reaction = 0; + std::unique_ptr + react(expected, detail::SendError> exp) { + const Device from = + exp ? exp.value()->owning_device : exp.error().owning_device; + + std::unique_ptr next; + ((Devs::id == from && + (next = std::get(devices_).react(std::move(exp)))) || + ...); + return next; } - bool pending() const { return (std::get(devices_).pending() || ...); } - // Service ready devices in round-robin order - bool emit(Frame *out) { - auto does_emit = [&](DeviceInterface auto &dev) -> bool { + std::unique_ptr poll() { + using U = std::unique_ptr; + auto does_emit = [&](DeviceInterface auto &dev) -> U { if (!dev.pending()) - return false; - originate(dev, out, [](auto &d, auto &out) { d.emit(out); }); - dev.resolvepending(); - return true; + return {}; + + U out(new (std::nothrow) Frame); + 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 - auto does_index_emit = [&](std::size_t t) -> bool { - return [&](std::index_sequence) { - return (((Is == t) && does_emit(std::get(devices_))) || ...); + auto does_index_emit = [&](std::size_t t) -> U { + return [&](std::index_sequence) -> U { + U out; + ((Is == t && (out = does_emit(std::get(devices_)))) || ...); + return out; }(std::index_sequence_for{}); }; @@ -195,16 +235,16 @@ public: static uint8_t rr_ = 0; // round-robin cursor const std::size_t start = rr_; 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; - return true; + return out; } 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; - return true; + return out; } - return false; + return {}; } } diff --git a/src/avclan/stdshim.hpp b/src/avclan/stdshim.hpp new file mode 100644 index 0000000..08c0715 --- /dev/null +++ b/src/avclan/stdshim.hpp @@ -0,0 +1,25 @@ +// Copyright (C) 2026 Allen Hill +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +#if !defined(__cpp_lib_expected) || __cpp_lib_expected < 202211L + #include + +namespace avclan { +template using expected = tl::expected; +template using unexpected = tl::unexpected; +using tl::unexpect; +using tl::unexpect_t; +} // namespace avclan +#else + #include + +namespace avclan { +template using expected = std::expected; +template using unexpected = std::unexpected; +using std::unexpect; +using std::unexpect_t; +} // namespace avclan +#endif diff --git a/src/avclan/target/avr-attiny3216/CMakeLists.txt b/src/avclan/target/avr-attiny3216/CMakeLists.txt index 941ca7a..204fc00 100644 --- a/src/avclan/target/avr-attiny3216/CMakeLists.txt +++ b/src/avclan/target/avr-attiny3216/CMakeLists.txt @@ -76,6 +76,9 @@ set_property(CACHE USART_RXMODE PROPERTY STRINGS # 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)") +# --- Firmware configuration options ---------------------------------------- +set(AVCLAN_FRAME_POOL_N 32 CACHE STRING "Frame pool depth (pooled targets)") + try_compile(LIBC_VERSION_TEST SOURCES "${CMAKE_SOURCE_DIR}/cmake/libc-version-test.cpp" COMPILE_DEFINITIONS -mmcu=${AVR_MCU} @@ -85,11 +88,14 @@ FetchContent_Declare( avr_libstdcpp GIT_REPOSITORY https://github.com/modm-io/avr-libstdcpp.git GIT_TAG 5354296040a2289c911062daa82336762231e897 + SYSTEM ) FetchContent_MakeAvailable(avr_libstdcpp) -add_library(libstdcpp INTERFACE) -target_include_directories(libstdcpp SYSTEM - INTERFACE ${avr_libstdcpp_SOURCE_DIR}/include) +add_library(libstdcpp STATIC ${avr_libstdcpp_SOURCE_DIR}/src/new_handler.cc) +target_include_directories(libstdcpp + 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) if(NOT LIBC_VERSION_TEST) @@ -142,6 +148,7 @@ target_compile_definitions(avclan PUBLIC __CLK_PRESCALE_DIV=__${CLK_PRESCALE_DIV} TCB_CLKSEL=${TCB_CLKSEL} RTC_STATUS_PERIOD_MS=${RTC_STATUS_PERIOD_MS} + AVCLAN_FRAME_POOL_N=${AVCLAN_FRAME_POOL_N} ) target_compile_options(avclan PUBLIC --param=min-pagesize=0 diff --git a/src/queue.hpp b/src/queue.hpp index aef6cff..20da5c4 100644 --- a/src/queue.hpp +++ b/src/queue.hpp @@ -8,33 +8,44 @@ #include #include #include +#include namespace detail { template struct Deleter; } -template - requires((N & (N - 1)) == 0 && N <= std::numeric_limits::max()) +template > + requires((N & (N - 1)) == 0 && N <= std::numeric_limits::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> || + (std::is_empty_v && + std::is_nothrow_default_constructible_v))) class Queue { - using Deleter = detail::template Deleter; - friend Deleter; + friend detail::Deleter; public: - // Only full and copy-convert-from-full construction is allowed - Queue() = delete; + // Only empty construction is allowed for non-Owning, non-pool deleters + constexpr Queue() + requires(!Owning && !std::is_same_v>) + : owner{nullptr} {} Queue(const Queue &) = delete; // Moving is unsupported due to being self-referential Queue(Queue &&) = delete; Queue &operator=(Queue &&) = delete; + // Construct from pre-defined storage constexpr Queue(T (&items)[N]) - requires(Owning) + requires(Owning) && std::same_as> : owner{this}, write{N} { for (uint8_t i = 0; i < N; ++i) buf[i] = &items[i]; } - constexpr Queue(Queue &queue) + // Construct non-Owning empty from Owning queue + constexpr Queue(Queue &queue) requires(!Owning) : owner{&queue} {} @@ -46,14 +57,22 @@ public: uint8_t push(std::unique_ptr x) requires(!Owning) { - // structurally unnecessary; empty construction from same size parent - // guarantees - // !isFull assert(!isFull()); + if constexpr (!std::is_same_v>) { + // structurally unnecessary for detail::Deleter, where empty construction + // is only from same size parent + if (isFull()) + return 1; + } - if (!x || !x.get_deleter().is_owned_by(owner)) + if (!x) return 1; - reclaim(x.release()); + if constexpr (std::is_same_v>) { + if (!x.get_deleter().is_owned_by(owner)) + return 1; + } + + claim(x.release()); return 0; } @@ -69,35 +88,39 @@ public: if (isEmpty()) return nullptr; - if constexpr (Owning) - return {buf[mask(read++)], {this}}; + if constexpr (!std::is_same_v>) + return {buf[mask(read++)], Deleter{}}; + else if constexpr (Owning) + return {buf[mask(read++)], Deleter{this}}; else - return {buf[mask(read++)], {owner}}; + return {buf[mask(read++)], Deleter{owner}}; } private: 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 buf = {}; - Queue *const owner; + Queue *const owner; uint8_t read = 0; uint8_t write = 0; }; -template Queue(Queue &) -> Queue; -template Queue(T (&items)[N]) -> Queue; +template +Queue(Queue &) -> Queue; +template +Queue(T (&items)[N]) -> Queue>; namespace detail { template struct Deleter { Deleter() = default; - constexpr Deleter(Queue *owner) : owner{owner} {} - void operator()(T *x) const { owner->reclaim(x); } - constexpr bool is_owned_by(const Queue *parent) const { + constexpr Deleter(Queue *owner) : owner{owner} {} + void operator()(T *x) const { owner->claim(x); } + constexpr bool is_owned_by(const Queue *parent) const { return owner == parent; } private: - Queue *const owner = nullptr; + Queue *const owner = nullptr; }; } // namespace detail diff --git a/src/sniffer.cc b/src/sniffer.cc index fcdda46..47182d3 100644 --- a/src/sniffer.cc +++ b/src/sniffer.cc @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include "cdchanger.hpp" #include "frame.hpp" @@ -14,6 +16,7 @@ #include "hal/stdio.h" #include "peripheral.hpp" #include "queue.hpp" +#include "stdshim.hpp" const char *const offon[] = {"OFF", "ON"}; @@ -22,11 +25,8 @@ constexpr uint8_t CACHE_SIZE = 32; using namespace avclan; namespace { -Frame frames[CACHE_SIZE]; - -constinit Queue cache(frames); -constinit Queue incoming = cache; -constinit Queue outgoing = cache; +constinit Queue incoming; +constinit Queue outgoing; uint8_t hexChars[2]; uint8_t hexDigit = 0; // current digit being written to hexChars @@ -66,7 +66,6 @@ int main() { using enum Action; using enum Device; Peripheral peripheral(phy, 0x360); - using Error = decltype(peripheral)::Error; using Print = Frame::Print; Setup(); @@ -74,40 +73,31 @@ int main() { while (true) { if (peripheral.bus_is_active()) { - if (auto msg = cache.pop()) { - auto err = peripheral.read(msg.get(), Print{.print = printAllFrames, - .binary = printBinary, - .verbose = verbose}); - if (err == Error::Read{0x00}) - incoming.push(std::move(msg)); - } else { - puts("!! Dropping an incoming message; cache is empty !!"); - } + if (auto msg = peripheral.read(Print{.print = printAllFrames, + .binary = printBinary, + .verbose = verbose})) + incoming.push(std::move(*msg)); } if (const auto *in = incoming.peek()) { - if (auto out = cache.pop()) { - peripheral.route(in, out.get()); + if (auto resp = peripheral.route(*in)) { incoming.pop(); - - if (out->reaction > 0) - outgoing.push(std::move(out)); - } else { - puts("!! Unable to respond; cache is empty !!"); + if (*resp) { + outgoing.push(std::move(*resp)); + continue; // route can be long; re-check the bus before poll/send + } } } - if (peripheral.pending()) { - if (auto out = cache.pop(); out && peripheral.emit(out.get())) - outgoing.push(std::move(out)); - } + if (auto msg = peripheral.poll()) + outgoing.push(std::move(msg)); if (auto out = outgoing.pop()) { - auto err = peripheral.send( - out.get(), Print{.print = printAllFrames, .binary = printBinary}); - peripheral.react(out.get(), err); - if (out->reaction > 0) - outgoing.push(std::move(out)); + auto result = + peripheral.send(std::move(out), Print{.print = printAllFrames, + .binary = printBinary}); + if (auto next = peripheral.react(std::move(result))) + outgoing.push(std::move(next)); } // 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 'E': // Beep - if (auto out = cache.pop()) { + if (auto out = std::unique_ptr(new (std::nothrow) Frame)) { out->is_unicast = true; out->peripheral_addr = peripheral.controller(); { @@ -141,10 +131,10 @@ int main() { out->reaction = 1; outgoing.push(std::move(out)); } else - puts("!! Cache empty; unable to queue beep request"); + puts("!! failed Frame alloc for Beep !! "); break; case 'P': - if (auto out = cache.pop()) { + if (auto out = std::unique_ptr(new (std::nothrow) Frame)) { out->is_unicast = true; out->peripheral_addr = peripheral.controller(); { @@ -159,7 +149,8 @@ int main() { } out->reaction = CDChanger::reaction_t::r_Ejection; outgoing.push(std::move(out)); - } + } else + puts("!! failed Frame alloc for Play !! "); break; #ifndef NDEBUG @@ -218,19 +209,21 @@ int main() { if (readSeq && seqIdx > 0) { if (readBinary) { if (data_tmp[seqIdx - 1] == 0x17) { - if (auto out = cache.pop()) { + if (auto out = + std::unique_ptr(new (std::nothrow) Frame)) { if (out->parse(data_tmp, --seqIdx) == Frame::Error::Parse{0}) { out->reaction = 1; outgoing.push(std::move(out)); } - } + } else + puts("!! failed Frame alloc for input message !!"); readSeq = readBinary = false; } else goto DEFAULT; // reading binary and this is a real data byte; // fall through to default } else if (seqIdx <= Frame::MAXLENGTH) { - if (auto out = cache.pop()) { + if (auto out = std::unique_ptr(new (std::nothrow) Frame)) { out->is_unicast = seqIsUnicast; out->peripheral_addr = seqIsUnicast ? peripheral.controller() : 0x1FF; @@ -238,7 +231,8 @@ int main() { memcpy(out->data, data_tmp, seqIdx); out->reaction = 1; outgoing.push(std::move(out)); - } + } else + puts("!! failed Frame alloc for input message !!"); printAllFrames = lastPrintAllFrames; } break;