cleanup + rename of some classes

This commit is contained in:
Bartosz Wieczorek 2025-05-06 09:30:06 +02:00
parent 75ac69f41f
commit bd5f60512a
16 changed files with 249 additions and 144 deletions

View File

@ -1,15 +1,10 @@
#pragma once
#include "log.hpp"
#include "syscalls/kernel.h"
#include "zephyr/sys/time_units.h"
#include "zephyr/sys_clock.h"
#include "function.hpp"
#include <chrono>
#include <cstddef>
#include <cstring>
#include <functional>
#include <optional>
#include <ratio>
#include <string_view>
@ -17,6 +12,8 @@
#include <zephyr/devicetree.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/time_units.h>
#include <zephyr/sys_clock.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/drivers/gpio.h>
@ -127,7 +124,7 @@ class Timer {
/* This is the time until the first expiration of the timer after you call k_timer_start().
It's a one-time initial delay before the timer "fires" the first time. */
k_timeout_t _duration;
/* After the first expiration, the timer will automatically restart and continue to expire repeatedly with this interval (period time)
between each expiration. If you set period to K_NO_WAIT, the timer only fires once (it becomes a one-shot timer).*/
k_timeout_t _period;
@ -164,7 +161,7 @@ class RecurringSemaphoreTimer : public Timer {
class SingleShootTimer : public Timer {
public:
SingleShootTimer(std::function<void()> cb, std::chrono::microseconds duration) : Timer(duration), _cb{std::move(cb)} {
SingleShootTimer(rims::function<void()> cb, std::chrono::microseconds duration) : Timer(duration), _cb{std::move(cb)} {
}
SingleShootTimer(const SingleShootTimer &) = delete;
@ -176,7 +173,7 @@ class SingleShootTimer : public Timer {
void expiry_cb() override {
_cb();
}
void fire_at(clock::time_point target) {
auto now = clock::now();
if (target <= now) {
@ -184,27 +181,16 @@ class SingleShootTimer : public Timer {
_duration = chronoToKTimeout(std::chrono::microseconds{1});
} else {
auto delay = std::chrono::duration_cast<std::chrono::microseconds>(target - now);
_duration = chronoToKTimeout(delay);
_duration = chronoToKTimeout(delay);
}
_period = K_NO_WAIT; // Ensure it's a one-shot
start();
}
std::function<void()> _cb;
rims::function<void()> _cb;
};
// class RecurringTimer : public Timer {
// public:
// RecurringTimer(std::function<void()> cb, std::chrono::milliseconds interval) : Timer(interval, interval), _cb{std::move(cb)} {
// }
// void expiry_cb() override {
// _cb();
// }
// std::function<void()> _cb;
// };
struct TStackBase {
z_thread_stack_element *_sp;
std::size_t _size;
@ -297,6 +283,4 @@ constexpr auto ChannelNumber = 2;
constexpr int GPIO_PIN_PB5 = 5;
constexpr int GPIO_PIN_PB6 = 6;
} // namespace rims

View File

@ -2,7 +2,9 @@
#include "ring_buffer.hpp"
namespace rims{
#include <zephyr/kernel.h>
namespace rims {
template <typename T> struct ZephyrFifoElement {
void *_reserved;
@ -14,19 +16,19 @@ template <typename T, size_t N> class fifo_queue {
// static_assert(std::is_trivial_v<T>);
fifo_queue(k_fifo &fifo) : _fifo{fifo} {
}
void k_event_init(k_poll_event &event) {
zephyr::event_pool::k_init(event, _fifo);
}
template <typename Fn> //
bool try_consume(Fn fn) {
// std::lock_guard<ZephyrMutex> _lock{_mutex};
if (_elements.empty()) return false;
/// read from queue
/// read from queue
ZephyrFifoElement<T> *el = reinterpret_cast<ZephyrFifoElement<T> *>(k_fifo_get(&_fifo, K_NO_WAIT));
try {
if (not el) return false; // should be a assert
fn(el->item); // consume item, fn can throw
@ -35,15 +37,15 @@ template <typename T, size_t N> class fifo_queue {
_elements.pop_front();
throw;
}
return true;
};
template <typename Fn> //
bool try_produce(Fn fn) {
// std::lock_guard<ZephyrMutex> _lock{_mutex};
if (_elements.full()) return false;
auto tmp = ZephyrFifoElement<T>{};
if (fn(tmp.item)) // fill new data
{
@ -51,13 +53,13 @@ template <typename T, size_t N> class fifo_queue {
k_fifo_put(&_fifo, &el); // put data into a queue
return true;
}
return false;
}
private:
// ZephyrMutex _mutex{};
ring_buffer<ZephyrFifoElement<T>, N> _elements{};
RingBuffer<ZephyrFifoElement<T>, N> _elements{};
k_fifo &_fifo;
};
}
} // namespace rims

View File

@ -1,81 +1,198 @@
#pragma once
#include <cassert>
#include <cstddef>
#include "details/function.hpp"
#include <cstring>
#include <functional>
#include <tuple>
#include <type_traits>
#include <utility>
#include <cstring> // For std::memset
namespace rims{
template <typename>
class Function;
namespace rims {
template <typename, unsigned = 1> struct trivial_function;
template <typename, unsigned = 1> struct function;
// A simple std::function alternative that never allocates. It contains
// enough space to store a lambda that captures N pointer-sized objects.
// The lambda's destructor and copy/move constructors are never used,
// which makes it very cheap to pass around. This also implies that only
// trivial types, such as pointers and references, can safely be captured.
template <typename R, typename... A, unsigned N> struct trivial_function<R(A...), N> {
constexpr trivial_function() noexcept = default;
constexpr ~trivial_function() = default;
constexpr trivial_function(trivial_function &&) noexcept = default;
constexpr trivial_function(const trivial_function &) noexcept = default;
constexpr trivial_function &operator=(trivial_function &&) noexcept = default;
constexpr trivial_function &operator=(const trivial_function &) noexcept = default;
template <typename T, unsigned M> trivial_function(function<T, M> &&) = delete;
template <typename T, unsigned M> trivial_function(const function<T, M> &) = delete;
constexpr trivial_function(std::nullptr_t) noexcept : trivial_function{} {
}
template <typename F>
requires(not detail::is_function_instance<std::remove_cvref_t<F>>)
explicit trivial_function(F &&func) : trivial_function{create(std::forward<F>(func))} {
}
template <typename F> trivial_function &operator=(F &&func) noexcept {
return *this = trivial_function{std::forward<F>(func)};
}
template <unsigned M>
requires(M < N)
trivial_function(const trivial_function<R(A...), M> &other) noexcept : call{other.call} {
std::memcpy(storage, &other.storage, sizeof(other.storage));
}
R operator()(A... args) const {
return call(&storage, std::forward<A>(args)...);
}
constexpr bool valid() const noexcept {
return call != nullptr;
}
explicit constexpr operator bool() const noexcept {
return valid();
}
template <typename Ret, typename... Args>
class Function<Ret(Args...)> {
private:
static constexpr size_t BufferSize = 16; // Adjust this size as needed for your use case.
using InvokeFn = Ret (*)(void*, Args&&...);
alignas(std::max_align_t) char buffer[BufferSize];
InvokeFn invokeFn = nullptr;
template <typename Callable>
static Ret invokeImpl(void* callable, Args&&... args) {
return (*reinterpret_cast<Callable*>(callable))(std::forward<Args>(args)...);
template <typename F> static trivial_function create(F &&func) {
using functor = detail::functor<std::remove_cvref_t<F>>;
static_assert(std::is_trivially_destructible_v<functor>);
static_assert(sizeof(functor) <= sizeof(dummy));
static_assert(alignof(functor) <= alignof(dummy));
trivial_function f;
new (&f.storage) functor{std::forward<F>(func)};
f.call = functor::template call<R, A...>;
return f;
}
template <typename Callable>
void storeCallable(Callable&& callable) {
using CallableType = std::decay_t<Callable>;
static_assert(sizeof(CallableType) <= BufferSize, "Callable too large for Function buffer");
static_assert(alignof(CallableType) <= alignof(std::max_align_t), "Callable alignment exceeds max_align_t");
new (buffer) CallableType(std::forward<Callable>(callable));
invokeFn = &invokeImpl<CallableType>;
template <typename, unsigned> friend struct trivial_function;
template <typename, unsigned> friend struct function;
using dummy = detail::functor<decltype([x = std::declval<std::array<void *, N>>()](A...) {})>;
union {
struct {
} nothing{};
alignas(dummy) std::byte storage[sizeof(dummy)];
};
R (*call)(const void *, A &&...){nullptr};
};
template <typename F, typename Signature = typename detail::member_function_signature<decltype(&F::operator())>::type>
trivial_function(F) -> trivial_function<Signature, (sizeof(F) - 1) / sizeof(void *) + 1>;
// A fixed-size function object that can store non-trivial lambdas. It is
// larger than trivial_function and requires the use of virtual function
// calls on copy/move/destroy.
template <typename R, typename... A, unsigned N> struct function<R(A...), N> {
constexpr function() noexcept = default;
constexpr ~function() {
if (call != nullptr) vtable->destroy(&storage);
}
public:
Function() = default;
template <typename Callable>
Function(Callable&& callable) {
storeCallable(std::forward<Callable>(callable));
template <typename F> function &operator=(F &&func) {
return assign(std::forward<F>(func));
}
Function(const Function&) = delete; // Disable copying for simplicity
Function& operator=(const Function&) = delete;
Function(Function&& other) noexcept {
std::memcpy(buffer, other.buffer, BufferSize);
invokeFn = other.invokeFn;
other.invokeFn = nullptr;
constexpr function(std::nullptr_t) noexcept : function{} {
}
Function& operator=(Function&& other) noexcept {
if (this != &other) {
this->~Function();
std::memcpy(buffer, other.buffer, BufferSize);
invokeFn = other.invokeFn;
other.invokeFn = nullptr;
}
return *this;
template <typename F>
requires(not detail::is_function_instance<std::remove_cvref_t<F>>)
explicit function(F &&func) : function{create(std::forward<F>(func))} {
}
~Function() {
if (invokeFn) {
reinterpret_cast<void(*)(void*)>(buffer)(buffer);
}
template <unsigned M>
requires(M <= N)
function(function<R(A...), M> &&other) : vtable{other.vtable}, call{other.call} {
if (call == nullptr) return;
vtable->move(&storage, &other.storage);
other.call = nullptr;
}
Ret operator()(Args... args) const {
if (!invokeFn) {
// throw std::bad_function_call();
assert(false); ///TODO handle asserts
}
return invokeFn(const_cast<void*>(reinterpret_cast<const void*>(buffer)), std::forward<Args>(args)...);
template <unsigned M>
requires(M <= N)
function(const function<R(A...), M> &other) noexcept : vtable{other.vtable}, call{other.call} {
if (call == nullptr) return;
vtable->copy(&storage, &other.storage);
}
explicit operator bool() const noexcept {
return invokeFn != nullptr;
template <unsigned M>
requires(M <= N)
function(const trivial_function<R(A...), M> &other) noexcept
: vtable{detail::functor_vtable::trivial<sizeof(other.storage)>()}, call{other.call} {
std::memcpy(&storage, &other.storage, sizeof(storage));
}
};}
R operator()(A... args) const {
return call(&storage, std::forward<A>(args)...);
}
constexpr bool valid() const noexcept {
return call != nullptr;
}
explicit constexpr operator bool() const noexcept {
return valid();
}
private:
template <typename F> static function create(F &&func) {
using functor = detail::functor<std::remove_cvref_t<F>>;
static_assert(sizeof(functor) <= sizeof(dummy));
static_assert(alignof(functor) <= alignof(dummy));
function f;
new (&f.storage) functor{std::forward<F>(func)};
f.call = functor::template call<R, A...>;
f.vtable = detail::functor_vtable::create<std::remove_cvref_t<F>>();
return f;
}
template <typename F> function &assign(F &&other) {
this->~function();
return *new (this) function{std::forward<F>(other)};
}
template <typename, unsigned> friend struct function;
using dummy = trivial_function<R(A...), N>::dummy;
union {
struct {
} nothing{};
struct {
alignas(dummy) std::byte storage[sizeof(dummy)];
const detail::functor_vtable *vtable;
};
};
R (*call)(const void *, A &&...){nullptr};
};
template <typename F, typename Signature = typename detail::member_function_signature<decltype(&F::operator())>::type>
function(F) -> function<Signature, (sizeof(F) - 1) / sizeof(void *) + 1>;
// A single-use function object with stored arguments.
template <typename T> struct callable_tuple {
template <typename... E> constexpr callable_tuple(E &&...elements) : tuple{std::forward<E>(elements)...} {
}
template <typename... A> decltype(auto) operator()(A &&...args) {
return call(std::make_index_sequence<std::tuple_size_v<T>>{}, std::forward<A>(args)...);
}
private:
template <std::size_t... I, typename... A> decltype(auto) call(std::index_sequence<I...>, A &&...args) {
return std::invoke(std::get<I>(std::move(tuple))..., std::forward<A>(args)...);
}
T tuple;
};
template <typename... E> callable_tuple(E...) -> callable_tuple<std::tuple<std::decay_t<E>...>>;
} // namespace rims
namespace rims::detail {
template <typename Sig, unsigned N> inline constexpr bool is_function_instance<trivial_function<Sig, N>> = true;
template <typename Sig, unsigned N> inline constexpr bool is_function_instance<function<Sig, N>> = true;
} // namespace rims::detail

View File

@ -1,6 +1,7 @@
#include "gpio.hpp"
#include "proto/gpio.pb.h"
#include "zephyr.hpp"
#include "log.hpp"
#include <exception>
namespace rims {

View File

@ -1,10 +1,8 @@
#include "common.hpp"
#include "ring_buffer.hpp"
#include "fifo_queue.hpp"
#include "proto/gpio.pb.h"
#include "zephyr.hpp"
#include "zephyr/kernel.h"
#include <array>
namespace rims {

View File

@ -2,11 +2,11 @@
#include <cstdarg>
#include <cstdio>
#include <functional>
#include <source_location>
#include <string_view>
#include "fifo_queue.hpp"
#include "function.hpp"
#include "proto/log.pb.h"
namespace rims {
@ -28,7 +28,7 @@ static LogLevel g_logLevel = LogLevel::Debug;
class Log {
public:
using Level = LogLevel;
using Sink = std::function<void(Level level, std::string_view usermsg, const std::source_location &sl)>;
using Sink = rims::function<void(Level level, std::string_view usermsg, const std::source_location &sl)>;
// static void registerSink(std::string_view name, Sink sink);
// static void unregisterSink(std::string_view name);

View File

@ -37,7 +37,7 @@
#include "zephyr.hpp"
namespace rims {
K_MSGQ_DEFINE(messenger_buffer_arrived_queue, sizeof(rims::buffer), 2, 1);
K_MSGQ_DEFINE(messenger_buffer_arrived_queue, sizeof(rims::BufferView), 2, 1);
namespace {
bool crcIsInvalid(std::span<uint8_t> data, std::uint32_t crc) {
@ -222,7 +222,7 @@ void MessengerThread::event_dataArrived() {
std::span<uint8_t> submessage{};
WireFormatID id{};
uint32_t crc{};
buffer buf;
BufferView buf;
try {
k_msgq_get(&messenger_buffer_arrived_queue, &buf, K_NO_WAIT);
@ -493,8 +493,8 @@ void MessengerThread::putActiveRequest(WireFormatID wireId, uint32_t id, AsyncUA
throw messenger::request_queue_full{};
}
std::optional<MessengerThread::requestData> MessengerThread::takeActiveRequest(int wireId_receiver, uint32_t id) {
std::optional<MessengerThread::requestData> ret{};
std::optional<MessengerThread::RequestData> MessengerThread::takeActiveRequest(int wireId_receiver, uint32_t id) {
std::optional<MessengerThread::RequestData> ret{};
// find first of active type/requestId pair
for (auto &req : _activeRequests) {
if (req.wireId.receiver == wireId_receiver && req.requestId == id) {

View File

@ -5,12 +5,13 @@
#include "id.hpp"
#include "proto/message.pb.h"
#include <span>
#include <cstdint>
namespace rims {
class AsyncUART;
struct buffer {
struct BufferView {
std::span<uint8_t> data;
AsyncUART *device;
};
@ -28,7 +29,7 @@ class MessengerThread : public ZephyrThread {
void threadMain() override;
private:
struct requestData {
struct RequestData {
WireFormatID wireId; // original ID of IngressMessage
uint32_t requestId; // requestID from message
AsyncUART *cb;
@ -53,10 +54,10 @@ class MessengerThread : public ZephyrThread {
void egressPush(void *submessage, const pb_msgdesc_t &fields, int id, std::optional<uint32_t> requestId);
void putActiveRequest(WireFormatID wireId, uint32_t id, AsyncUART *cb);
std::optional<requestData> takeActiveRequest(int type, uint32_t id);
std::optional<RequestData> takeActiveRequest(int type, uint32_t id);
std::array<k_poll_event, 6> _events;
std::array<requestData, 8> _activeRequests;
std::array<RequestData, 8> _activeRequests;
uint8_t _activeRequestsNr;
};
} // namespace rims

View File

@ -1,6 +1,5 @@
#include "power_control.hpp"
#include "ring_buffer.hpp"
#include "common.hpp"
#include "log.hpp"
#include "proto/ctrl.pb.h"
@ -15,7 +14,6 @@
#include <tuple>
#include <variant>
namespace rims {
namespace ctrl {
@ -58,8 +56,8 @@ constexpr std::array<gpio_dt_spec, MaxChannels> pins = {
PhaseModulation::PhaseModulation(const gpio_dt_spec &gpio)
: PinControlStrategyBase(gpio), //
_triacTimerStart{[this]() { this->on(); }, std::chrono::milliseconds{0}},
_triacTimerStop{[this]() { this->off(); }, std::chrono::milliseconds{0}} {
_triacTimerStart{rims::function<void()>{[this]() { this->on(); }}, std::chrono::milliseconds{0}},
_triacTimerStop{rims::function<void()>{[this]() { this->off(); }}, std::chrono::milliseconds{0}} {
ULOG_INFO("PhaseModulation started for gpio %d", gpio.pin);
}
@ -83,25 +81,24 @@ void PhaseModulation::zeroCrossDetectionTick(ZeroCrossDetectionEvent::Data data)
// Calculate offset: higher power -> shorter delay, lower power -> longer delay
// _power in percent
const float clampedPower = std::clamp(_power, 0.0f, 100.0f);
const auto usToNext = data.cycle_duration;
const auto maxDelay = usToNext - minimalGatePulse - minimalLatchTime - minimalSafeMargin;
const auto onDelay = std::min(delay(clampedPower, usToNext), maxDelay);
if (onDelay > maxDelay) {
return;
}
// Start timer to turn triac ON at calculated phase delay
// _triacTimerStart.fire_at(data.restored() + data.to_zcd + onDelay - minimalLatchTime);
// Schedule triac to turn OFF shortly after firing (e.g., 100 us pulse)
// _triacTimerStop.fire_at(data.restored() + data.to_zcd + onDelay + minimalGatePulse);
// _triacTimerStop.setDuration(onDelay + minimalGatePulse);
// // Start timer to turn triac ON at calculated phase delay
_triacTimerStart.setDuration(onDelay+data.to_zcd);
_triacTimerStart.setDuration(onDelay + data.to_zcd);
_triacTimerStart.start();
// // Schedule triac to turn OFF shortly after firing (e.g., 100 us pulse)
@ -362,7 +359,7 @@ void PhaseModulationOrchestrator::checkCurrentMode(const uint8_t channel, ModeOf
void PhaseModulationOrchestrator::setMode(uint8_t ch, ModeOfOperation mode) {
checkChannel(ch);
switch (mode) {
case ctrl_ModeOfOperation_Disabled:
_chModeOfOperationStorage[ch].emplace<DisabledMode>();
@ -399,7 +396,7 @@ ModeOfOperation PhaseModulationOrchestrator::mode(uint8_t ch) {
void PhaseModulationOrchestrator::setPowerControlAlgorithm(uint8_t ch, PowerControlAlgorithm alg) {
checkChannel(ch);
switch (alg) {
case ctrl_PowerControlAlgorithm_NoModulation:
_chPowerControlStrategy[ch].emplace<NoModulation>();

View File

@ -1,7 +1,6 @@
#pragma once
/* Kernel event for notifying other threads */
#include "ring_buffer.hpp"
#include "common.hpp"
#include "inplace_vector.hpp"
#include "proto/ctrl.pb.h"

View File

@ -6,7 +6,6 @@
#include <cstddef>
#include <cstring>
#include <mutex>
#include <type_traits>
#include <utility>
@ -151,14 +150,14 @@ struct RingIndex {
bool _full{false};
};
template <class T, std::size_t N> class ring_buffer {
template <class T, std::size_t N> class RingBuffer {
public:
static_assert(std::is_trivial_v<T>);
static_assert(std::is_trivially_destructible_v<T>);
static_assert(std::is_trivially_copyable_v<T>);
using value_type = T;
explicit ring_buffer() : _index{N} {
explicit RingBuffer() : _index{N} {
}
T &push_back(const T &item) {

View File

@ -1,6 +1,6 @@
#pragma once
#include "ring_buffer.hpp"
#include "fifo_queue.hpp"
#include "common.hpp"
#include "proto/temperature.pb.h"
@ -82,7 +82,7 @@ class TemperatureSampler {
zephyr::semaphore::sem _broadcastSem{0, 1};
RecurringSemaphoreTimer _broadcastTimer{_broadcastSem, std::chrono::seconds{60}};
ring_buffer<adc_sample, MaxSampleSize> _samples;
RingBuffer<adc_sample, MaxSampleSize> _samples;
// number of samples taken to filter
std::uint8_t _samplesNumber{MaxSampleSize};

View File

@ -196,7 +196,7 @@ uint8_t AsyncUART::rxByte() {
}
void AsyncUART::processMessage() {
buffer buf{.data = {rxBuffer().data(), rxBuffer().size()}, .device = this};
BufferView buf{.data = {rxBuffer().data(), rxBuffer().size()}, .device = this};
switchRxBuffer();
k_msgq_put(&messenger_buffer_arrived_queue, &buf, K_NO_WAIT);
}

View File

@ -19,7 +19,7 @@ class uart_rx_not_ready_error;
class AsyncUART {
using rx_buffer_t = beman::inplace_vector<uint8_t, 256>;
using tx_buffer_t = ring_buffer<uint8_t, 256>;
using tx_buffer_t = RingBuffer<uint8_t, 256>;
public:
AsyncUART();

View File

@ -1,12 +1,12 @@
#pragma once
#include "zephyr/drivers/adc.h"
#include <chrono>
#include <cstdint>
#include <exception>
#include <source_location>
#include <span>
#include <zephyr/drivers/adc.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/crc.h>

View File

@ -1,8 +1,8 @@
#pragma once
/* Kernel event for notifying other threads */
#include "ring_buffer.hpp"
#include "common.hpp"
#include "fifo_queue.hpp"
#include "zephyr.hpp"
#include <chrono>
@ -16,8 +16,8 @@ struct ZeroCrossDetectionEvent {
return clock::time_point{std::chrono::nanoseconds{time_point}};
}
void store_now() {
auto now = clock::now();
time_point = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
auto now = clock::now();
time_point = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
}
uint64_t time_point; // timepoint
microseconds_u16_t cycle_duration;
@ -52,14 +52,21 @@ class ZeroCrossDetection {
zephyr::semaphore::sem _intCheckSem;
RecurringSemaphoreTimer _intCheckTimer;
clock::time_point _pulseDown{};
rims::ring_buffer<microseconds_u16_t, 20> _pulsWidths;
rims::ring_buffer<microseconds_u16_t, 20> _cyclesWidths;
clock::time_point _pulseDown{};
rims::RingBuffer<microseconds_u16_t, 20> _pulsWidths;
rims::RingBuffer<microseconds_u16_t, 20> _cyclesWidths;
};
class ZeroCrossDetectionOrchestrator {
public:
ZeroCrossDetectionOrchestrator();
ZeroCrossDetectionOrchestrator(const ZeroCrossDetectionOrchestrator &) = delete;
ZeroCrossDetectionOrchestrator &operator=(const ZeroCrossDetectionOrchestrator &) = delete;
ZeroCrossDetectionOrchestrator(ZeroCrossDetectionOrchestrator &&) = delete;
ZeroCrossDetectionOrchestrator &operator=(ZeroCrossDetectionOrchestrator &&) = delete;
void loop();
void event_zcdCheck(ZeroCrossDetection &channel);