mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-08-11 16:32:52 +00:00
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:
@@ -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 {
|
||||
|
||||
+17
-3
@@ -31,11 +31,14 @@
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#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<std::unique_ptr<Frame>, 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<Frame> 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 {
|
||||
|
||||
+4
-1
@@ -48,9 +48,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#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<std::unique_ptr<Frame>, Error::Read> read(uint16_t address,
|
||||
Frame::Print print);
|
||||
Error::Send send(const Frame *out, Frame::Print print);
|
||||
|
||||
private:
|
||||
|
||||
+71
-59
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#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<reaction_t>(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<Frame> CDChanger::react(
|
||||
expected<std::unique_ptr<Frame>, 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<reaction_t>(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; }
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#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<Frame>
|
||||
react(expected<std::unique_ptr<Frame>, 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;
|
||||
|
||||
+17
-11
@@ -5,9 +5,11 @@
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "avclan.h"
|
||||
#include "frame.hpp"
|
||||
#include "stdshim.hpp"
|
||||
|
||||
namespace avclan {
|
||||
|
||||
@@ -46,15 +48,19 @@ enum class Device : uint8_t {
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept DeviceInterface = requires {
|
||||
std::integral_constant<Device, T::id>{};
|
||||
} && 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<bool>;
|
||||
dev.resolvepending();
|
||||
dev.emit(out);
|
||||
};
|
||||
concept DeviceInterface =
|
||||
requires { std::integral_constant<Device, T::id>{}; } &&
|
||||
requires(T dev, const Frame *in, Frame *out,
|
||||
expected<std::unique_ptr<Frame>, detail::SendError> exp) {
|
||||
dev.init();
|
||||
dev.handle(in, out);
|
||||
dev.enable(out);
|
||||
{
|
||||
dev.react(std::move(exp))
|
||||
} -> 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
|
||||
|
||||
@@ -9,12 +9,62 @@
|
||||
|
||||
#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 {
|
||||
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<Frame *>(ptr));
|
||||
}
|
||||
#endif
|
||||
|
||||
void Frame::print(Frame::Print print) const {
|
||||
if (print.binary) {
|
||||
uint8_t buffer[8];
|
||||
|
||||
+19
-3
@@ -9,6 +9,11 @@
|
||||
|
||||
#include "avclan.h"
|
||||
|
||||
#if defined(AVCLAN_FRAME_POOL_N)
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#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
|
||||
|
||||
+82
-42
@@ -4,16 +4,20 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include "avclan.h"
|
||||
#include "bus.hpp"
|
||||
#include "device.hpp"
|
||||
#include "frame.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include "stdshim.hpp"
|
||||
|
||||
namespace avclan {
|
||||
|
||||
@@ -25,6 +29,14 @@ template <DeviceInterface... Devs> 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<Devs>(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<std::unique_ptr<Frame>, Error::Read>
|
||||
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
|
||||
stamp<Sender>(out);
|
||||
stamp<Sender>(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<std::unique_ptr<Frame>, 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<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
|
||||
// out->data[N] after memcpy.
|
||||
static const uint8_t lancheck_resp[] = {0x00, to_underlying(COMM_CTRL),
|
||||
to_underlying(LAN), 0xFF, 0xFF};
|
||||
|
||||
stamp<Recipient>(out);
|
||||
stamp<Recipient>(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<Device>(b3)
|
||||
? originate(std::get<Devs>(devices_), out, enable_d)
|
||||
? originate(std::get<Devs>(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<Recipient>(out); // re-stamp now that controller_ is known
|
||||
stamp<Recipient>(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<Device>(b2)
|
||||
? originate(std::get<Devs>(devices_), out, handle_d)
|
||||
? originate(std::get<Devs>(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<Devs>(devices_).react(out, err))
|
||||
: void()),
|
||||
...);
|
||||
else
|
||||
out->reaction = 0;
|
||||
std::unique_ptr<Frame>
|
||||
react(expected<std::unique_ptr<Frame>, detail::SendError> exp) {
|
||||
const Device from =
|
||||
exp ? exp.value()->owning_device : exp.error().owning_device;
|
||||
|
||||
std::unique_ptr<Frame> next;
|
||||
((Devs::id == from &&
|
||||
(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
|
||||
bool emit(Frame *out) {
|
||||
auto does_emit = [&](DeviceInterface auto &dev) -> bool {
|
||||
std::unique_ptr<Frame> poll() {
|
||||
using U = std::unique_ptr<Frame>;
|
||||
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::size_t... Is>(std::index_sequence<Is...>) {
|
||||
return (((Is == t) && does_emit(std::get<Is>(devices_))) || ...);
|
||||
auto does_index_emit = [&](std::size_t t) -> U {
|
||||
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> U {
|
||||
U out;
|
||||
((Is == t && (out = does_emit(std::get<Is>(devices_)))) || ...);
|
||||
return out;
|
||||
}(std::index_sequence_for<Devs...>{});
|
||||
};
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
|
||||
+47
-24
@@ -8,33 +8,44 @@
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
namespace detail {
|
||||
template <class T, auto N> struct Deleter;
|
||||
}
|
||||
|
||||
template <class T, std::integral auto N, bool Owning = false>
|
||||
requires((N & (N - 1)) == 0 && N <= std::numeric_limits<uint8_t>::max())
|
||||
template <class T, std::integral auto N, bool Owning = false,
|
||||
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 {
|
||||
using Deleter = detail::template Deleter<T, N>;
|
||||
friend Deleter;
|
||||
friend detail::Deleter<T, N>;
|
||||
|
||||
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<Deleter, detail::Deleter<T, N>>)
|
||||
: 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<Deleter, detail::Deleter<T, N>>
|
||||
: owner{this}, write{N} {
|
||||
for (uint8_t i = 0; i < N; ++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)
|
||||
: owner{&queue} {}
|
||||
|
||||
@@ -46,14 +57,22 @@ public:
|
||||
uint8_t push(std::unique_ptr<T, Deleter> x)
|
||||
requires(!Owning)
|
||||
{
|
||||
// structurally unnecessary; empty construction from same size parent
|
||||
// guarantees
|
||||
// !isFull assert(!isFull());
|
||||
if constexpr (!std::is_same_v<Deleter, detail::Deleter<T, N>>) {
|
||||
// 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<Deleter, detail::Deleter<T, N>>) {
|
||||
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<Deleter, detail::Deleter<T, N>>)
|
||||
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<T *, N> buf = {};
|
||||
Queue<T, N, true> *const owner;
|
||||
Queue<T, N, true, Deleter> *const owner;
|
||||
uint8_t read = 0;
|
||||
uint8_t write = 0;
|
||||
};
|
||||
|
||||
template <class T, auto N> Queue(Queue<T, N, true> &) -> Queue<T, N, false>;
|
||||
template <class T, auto N> Queue(T (&items)[N]) -> Queue<T, N, true>;
|
||||
template <class T, auto N, class D>
|
||||
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 {
|
||||
template <class T, auto N> struct Deleter {
|
||||
Deleter() = default;
|
||||
constexpr Deleter(Queue<T, N, true> *owner) : owner{owner} {}
|
||||
void operator()(T *x) const { owner->reclaim(x); }
|
||||
constexpr bool is_owned_by(const Queue<T, N, true> *parent) const {
|
||||
constexpr Deleter(Queue<T, N, true, Deleter> *owner) : owner{owner} {}
|
||||
void operator()(T *x) const { owner->claim(x); }
|
||||
constexpr bool is_owned_by(const Queue<T, N, true, Deleter> *parent) const {
|
||||
return owner == parent;
|
||||
}
|
||||
|
||||
private:
|
||||
Queue<T, N, true> *const owner = nullptr;
|
||||
Queue<T, N, true, Deleter> *const owner = nullptr;
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
+33
-39
@@ -7,6 +7,8 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
#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<Frame, CACHE_SIZE> incoming;
|
||||
constinit Queue<Frame, CACHE_SIZE> 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<CDChanger> 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<Frame>(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<Frame>(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<Frame>(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<Frame>(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;
|
||||
|
||||
Reference in New Issue
Block a user