mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-21 12:29:35 +00:00
387 lines
17 KiB
C++
387 lines
17 KiB
C++
#pragma once
|
||
#include "IR_config.h"
|
||
#include "IrTxGateTypes.h"
|
||
|
||
// TODO: Отложенная передача после завершения приема
|
||
|
||
enum class IR_SendStatus : uint8_t {
|
||
Success = 0,
|
||
PayloadTooLarge,
|
||
EncoderBusy,
|
||
BufferTooLarge,
|
||
ExternalBackendBusy,
|
||
ExternalStartFailed,
|
||
ExternalNoStream,
|
||
ExternalInvalidConfig,
|
||
BuildGateRunsFailed,
|
||
ScaleGateRunsFailed,
|
||
DmaStartFailed,
|
||
EncoderPinUnavailable,
|
||
BufferedStorageInvalid,
|
||
InvalidArgument,
|
||
TimingOverflow,
|
||
PlanMismatch,
|
||
DmaTransferError,
|
||
DmaStalled,
|
||
};
|
||
|
||
const char* irSendStatusToString(IR_SendStatus status);
|
||
|
||
enum class IR_TxState : uint8_t {
|
||
Idle = 0,
|
||
Preparing,
|
||
Transmitting,
|
||
Completed,
|
||
Failed,
|
||
};
|
||
|
||
enum class IR_TxClockBasis : uint8_t {
|
||
Nominal = 0,
|
||
ConfiguredTimer,
|
||
};
|
||
|
||
/**
|
||
* Deterministic PHY plan produced by the same FSM that builds the actual
|
||
* carrier-gate stream. airtimeUs is rounded up, so it is safe as a deadline
|
||
* component; it does not include backend preparation or release latency.
|
||
*/
|
||
struct IR_TxPlan {
|
||
IR_SendStatus status = IR_SendStatus::InvalidArgument;
|
||
uint8_t wireBytes = 0;
|
||
uint16_t carrierMultiply = 0;
|
||
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal;
|
||
uint32_t tickClockHz = 0; // rational tick rate numerator
|
||
uint32_t tickDivider = 1; // rational tick rate denominator
|
||
uint32_t physicalTicks = 0;
|
||
uint32_t gateRunCount = 0;
|
||
uint32_t airtimeUs = 0;
|
||
|
||
bool valid() const { return status == IR_SendStatus::Success; }
|
||
uint32_t tickHzFloor() const { return tickDivider == 0U ? 0U : tickClockHz / tickDivider; }
|
||
uint32_t airtimeMsCeil() const { return (airtimeUs + 999U) / 1000U; }
|
||
};
|
||
|
||
/** Coherent main-context snapshot of one encoder's latest accepted operation. */
|
||
struct IR_TxSnapshot {
|
||
uint32_t operationId = 0;
|
||
IR_TxState state = IR_TxState::Idle;
|
||
IR_SendStatus status = IR_SendStatus::Success;
|
||
uint16_t carrierMultiply = 0;
|
||
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal;
|
||
uint32_t plannedPhysicalTicks = 0;
|
||
uint32_t plannedAirtimeUs = 0;
|
||
uint32_t acceptedAtUs = 0;
|
||
uint32_t armedAtUs = 0;
|
||
uint32_t terminalAtUs = 0;
|
||
|
||
bool active() const {
|
||
return state == IR_TxState::Preparing || state == IR_TxState::Transmitting;
|
||
}
|
||
bool terminal() const {
|
||
return state == IR_TxState::Completed || state == IR_TxState::Failed;
|
||
}
|
||
};
|
||
|
||
// Структура для возврата результата отправки
|
||
struct IR_SendResult {
|
||
bool success; // true: backend принял и запустил эту операцию
|
||
uint32_t sendTimeMs; // ceil(plannedAirtimeUs / 1000), compatibility field
|
||
IR_SendStatus status; // Детализированный статус старта передачи
|
||
uint32_t operationId; // 0, если новая операция не создавалась
|
||
uint32_t plannedAirtimeUs; // PHY airtime; без подготовки/release backend-а
|
||
IR_TxClockBasis clockBasis;
|
||
|
||
IR_SendResult(bool success = false,
|
||
uint32_t sendTimeMs = 0,
|
||
IR_SendStatus status = IR_SendStatus::ExternalStartFailed,
|
||
uint32_t operationId = 0,
|
||
uint32_t plannedAirtimeUs = 0,
|
||
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal)
|
||
: success(success), sendTimeMs(sendTimeMs), status(status),
|
||
operationId(operationId), plannedAirtimeUs(plannedAirtimeUs),
|
||
clockBasis(clockBasis) {}
|
||
};
|
||
|
||
class IR_DecoderRaw;
|
||
class IrTxIsrBufferedStorageBase;
|
||
class IR_Encoder : public IR_FOX
|
||
{
|
||
friend IR_DecoderRaw;
|
||
static IR_Encoder *head;
|
||
static IR_Encoder *last;
|
||
IR_Encoder *next;
|
||
public:
|
||
static HardwareTimer* IR_Timer;
|
||
|
||
using IR_TxGateRun = IrTxGateRun;
|
||
enum class TxIsrMode : uint8_t {
|
||
Legacy = 0,
|
||
Buffered = 1
|
||
};
|
||
|
||
using ExternalTxBusyFn = bool (*)(void *ctx);
|
||
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
||
using ExternalTxStartFnV2 = IR_SendStatus (*)(void *ctx,
|
||
IR_Encoder *enc,
|
||
const uint8_t *packet,
|
||
uint8_t len,
|
||
const IR_TxPlan& plan,
|
||
uint32_t operationId);
|
||
private:
|
||
// uint16_t id; /// @brief Адрес передатчика
|
||
public:
|
||
/// @brief Класс передатчика
|
||
/// @param addr Адрес передатчика
|
||
/// @param pin Вывод передатчика
|
||
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
||
/// (аналог setBlindDecoders() для одного RX).
|
||
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
|
||
static void isr();
|
||
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
|
||
/**
|
||
* Глобальный знаменатель: частота таймера TX = carrierFrec × multiply (слотов на период несущей).
|
||
* По умолчанию multiply=2 (как бывшие carrierFrec×2). Задавать до begin/beginClockOnly либо после
|
||
* изменения вызвать retuneCarrierClock() (не менять multiply во время активной передачи).
|
||
*/
|
||
static void setCarrierMultiply(uint16_t multiply);
|
||
static uint16_t carrierMultiply();
|
||
/** Повторно применить carrierFrec×multiply к IR_Timer (pause + setOverflow), ISR не перенавешивает. */
|
||
static void retuneCarrierClock();
|
||
|
||
/** Максимальный числитель мощности: ⌊multiply/2⌋ (100% в setPowerPercent). */
|
||
static uint16_t maxPowerNumerator();
|
||
|
||
/** Числитель N: при открытой огибающей N из multiply тиков HIGH за период несущей. Clamped к maxPowerNumerator(). */
|
||
void setPowerNumerator(uint16_t n);
|
||
uint16_t powerNumerator() const;
|
||
/** p∈[0,100] → ближайший допустимый числитель; 100% даёт N = maxPowerNumerator(). */
|
||
void setPowerPercent(uint8_t p);
|
||
|
||
/** Legacy helper: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */
|
||
static bool scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, size_t maxRuns, uint16_t multiply);
|
||
|
||
/** Configure timer frequency for TX clock (carrierFrec × multiply) without attaching ISR. */
|
||
static void beginClockOnly(HardwareTimer *timer);
|
||
static HardwareTimer* get_IR_Timer();
|
||
/** Call from main loop/tick: if ISR requested carrier stop, pause timer here (not in ISR). */
|
||
static void tick();
|
||
|
||
/**
|
||
* Режим внутреннего TX без DMA: false — BSRR + кольцо (direct physical gate-runs builder);
|
||
* true — FSM «налету» + скважность несущей как у буферного пути (подшаги multiply/2 на шаг FSM).
|
||
* По умолчанию включён legacy=true для обратной совместимости. Вызов меняет default и обновляет
|
||
* все зарегистрированные encoder-объекты. Buffered ISR реально используется только если у encoder
|
||
* привязан storage через attachBufferedIsrStorage()/enableBufferedIsr().
|
||
* Выставить до begin/rawSend. Игнорируется при externalTxStartFn.
|
||
*/
|
||
static void setTxIsrLegacyMode(bool legacy);
|
||
static bool txIsrLegacyMode();
|
||
void attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage);
|
||
void detachBufferedIsrStorage();
|
||
bool hasBufferedIsrStorage() const;
|
||
void enableBufferedIsr(IrTxIsrBufferedStorageBase& storage);
|
||
void disableBufferedIsr();
|
||
TxIsrMode txIsrMode() const;
|
||
|
||
/** Optional: register external TX backend (e.g. DMA driver). */
|
||
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||
/** Token-aware backend contract. Prefer this overload for every new backend. */
|
||
static void setExternalTxBackendV2(ExternalTxStartFnV2 startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||
|
||
/** Legacy completion hook. It cannot reject a stale completion; retained for source compatibility. */
|
||
void externalFinishSend();
|
||
/** Complete exactly operationId; stale/duplicate completions are ignored. */
|
||
void externalFinishSend(uint32_t operationId, IR_SendStatus terminalStatus);
|
||
|
||
/** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */
|
||
static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns);
|
||
/** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */
|
||
static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply);
|
||
/** Preflight the exact physical stream without allocating or touching hardware. */
|
||
static IR_TxPlan planPhysicalTransmission(const uint8_t *packet, uint8_t len, uint16_t multiply);
|
||
/** Build into caller storage and report both required runs and exact timing. */
|
||
static IR_TxPlan buildPhysicalTransmission(const uint8_t *packet,
|
||
uint8_t len,
|
||
IR_TxGateRun *outRuns,
|
||
size_t maxRuns,
|
||
uint16_t multiply);
|
||
/** Replace nominal tick rate with an exact rational backend clock. */
|
||
static bool applyTickClock(IR_TxPlan& plan,
|
||
uint32_t clockNumeratorHz,
|
||
uint32_t clockDivider,
|
||
IR_TxClockBasis basis = IR_TxClockBasis::ConfiguredTimer);
|
||
IR_TxPlan planTransmission(const uint8_t *packet, uint8_t len) const;
|
||
|
||
void enable();
|
||
void disable();
|
||
|
||
void setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count);
|
||
template <size_t N>
|
||
void setBlindDecoders(IR_DecoderRaw *(&decoders)[N])
|
||
{
|
||
static_assert(N <= IR_PAIR_MUTE_MAX_ENCODERS,
|
||
"IR_Encoder::setBlindDecoders: array size exceeds IR_PAIR_MUTE_MAX_ENCODERS");
|
||
setBlindDecoders(decoders, static_cast<uint8_t>(N));
|
||
}
|
||
IR_SendStatus rawSend(uint8_t *ptr, uint8_t len);
|
||
IR_SendResult rawSendTracked(uint8_t *ptr, uint8_t len);
|
||
|
||
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
||
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
||
IR_SendResult sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
||
|
||
|
||
IR_SendResult sendAccept(uint16_t addrTo, uint8_t customByte = 0);
|
||
IR_SendResult sendRequest(uint16_t addrTo);
|
||
|
||
IR_SendResult sendBack(uint8_t data);
|
||
IR_SendResult sendBack(uint8_t *data = nullptr, uint8_t len = 0);
|
||
IR_SendResult sendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0);
|
||
|
||
// Функция для тестирования времени отправки без фактической отправки
|
||
uint32_t testSendTime(uint16_t addrTo, uint8_t dataByte, bool needAccept = false) const;
|
||
uint32_t testSendTime(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false) const;
|
||
uint32_t testSendTimeFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false) const;
|
||
uint32_t testSendAccept(uint16_t addrTo, uint8_t customByte = 0) const;
|
||
uint32_t testSendRequest(uint16_t addrTo) const;
|
||
uint32_t testSendBack(uint8_t data) const;
|
||
uint32_t testSendBack(uint8_t *data = nullptr, uint8_t len = 0) const;
|
||
uint32_t testSendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0) const;
|
||
|
||
inline bool isBusy() const { return isSending; }
|
||
/** Main-context coherent snapshot. Do not spin on this from an ISR. */
|
||
IR_TxSnapshot txSnapshot() const;
|
||
bool isOperationTerminal(uint32_t operationId) const;
|
||
bool isOperationComplete(uint32_t operationId) const;
|
||
|
||
|
||
~IR_Encoder();
|
||
volatile bool ir_out_virtual;
|
||
|
||
void _isr();
|
||
private:
|
||
static volatile bool carrierStopPending;
|
||
static bool txIsrLegacyMode_;
|
||
static uint16_t s_carrierMultiply;
|
||
static void carrierResume();
|
||
static void carrierPauseIfIdle();
|
||
|
||
static ExternalTxStartFn externalTxStartFn;
|
||
static ExternalTxStartFnV2 externalTxStartFnV2;
|
||
static ExternalTxBusyFn externalTxBusyFn;
|
||
static void *externalTxCtx;
|
||
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
|
||
|
||
void refreshBlindDecoderMuteState();
|
||
void registerWithBlindDecoders();
|
||
void sendByte(uint8_t byte, bool *prev, bool LOW_FIRST);
|
||
void addSync(bool *prev, bool *next);
|
||
uint32_t calculateSendTime(uint8_t packSize) const;
|
||
uint32_t testSendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len) const;
|
||
void send_HIGH(bool = 1);
|
||
void send_LOW();
|
||
void send_EMPTY(uint8_t count);
|
||
|
||
enum SignalPart : uint8_t
|
||
{
|
||
noSignal = 0,
|
||
preamb = 1,
|
||
data = 2,
|
||
sync = 3
|
||
};
|
||
|
||
struct TxFsmState
|
||
{
|
||
uint8_t sendLen = 0;
|
||
uint8_t toggleCounter = 0;
|
||
uint8_t dataBitCounter = 0;
|
||
uint8_t dataByteCounter = 0;
|
||
uint8_t preambFrontCounter = 0;
|
||
uint8_t dataSequenceCounter = 0;
|
||
uint8_t syncSequenceCounter = 0;
|
||
bool syncLastBit = false;
|
||
bool state = LOW;
|
||
uint8_t *currentBitSequence = nullptr;
|
||
SignalPart signal = noSignal;
|
||
};
|
||
|
||
static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||
static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
|
||
static TxFsmState initialTxFsm(uint8_t len);
|
||
static IR_TxPlan buildPhysicalPlan(const uint8_t *packet,
|
||
uint8_t len,
|
||
IR_TxGateRun *outRuns,
|
||
size_t maxRuns,
|
||
uint16_t multiply,
|
||
bool emitRuns);
|
||
static bool calculateAirtimeUs(IR_TxPlan& plan);
|
||
static void applyConfiguredTimerClock(IR_TxPlan& plan);
|
||
void loadTxFsmFromMembers(TxFsmState &st) const;
|
||
void storeTxFsmToMembers(const TxFsmState &st);
|
||
bool shouldUseBufferedIsr() const;
|
||
|
||
/** Снимок на старт TX (буферный и legacy путь). */
|
||
uint16_t txPowerSnap_ = 1;
|
||
uint16_t txMultiplySnap_ = 2;
|
||
|
||
/** Fractional 2×Fc -> multiply×Fc phase accumulator (also exact for odd multiply). */
|
||
uint32_t legacyScaleAccumulator_ = 0;
|
||
uint16_t legacySlotInPeriod_ = 0;
|
||
|
||
volatile uint16_t powerNumerator_ = 1;
|
||
IrTxIsrBufferedStorageBase* txBufferedCtx_ = nullptr;
|
||
IrTxIsrBufferedStorageBase* txActiveBufferedCtx_ = nullptr;
|
||
TxIsrMode txIsrMode_ = TxIsrMode::Legacy;
|
||
bool txUseBufferedIsr_ = false;
|
||
|
||
IR_DecoderRaw *decPair = nullptr;
|
||
IR_DecoderRaw *singleBlindDecoder = nullptr;
|
||
IR_DecoderRaw **blindDecoders = nullptr;
|
||
uint8_t decodersCount = 0;
|
||
|
||
uint8_t sendLen = 0;
|
||
uint8_t sendBuffer[irproto::kMaxWireFrameBytes]{0}; /// @brief Буффер полного wire-кадра
|
||
|
||
volatile bool isSending = false;
|
||
|
||
// Single-writer-at-a-time record (main starts, ISR/backend terminates).
|
||
// The byte seqlock makes a coherent main-context snapshot without heap/locks.
|
||
volatile uint8_t txRecordVersion_ = 0;
|
||
volatile IR_TxState txState_ = IR_TxState::Idle;
|
||
volatile IR_SendStatus txTerminalStatus_ = IR_SendStatus::Success;
|
||
volatile uint32_t txOperationId_ = 0;
|
||
volatile uint32_t txPlannedPhysicalTicks_ = 0;
|
||
volatile uint32_t txPlannedAirtimeUs_ = 0;
|
||
volatile IR_TxClockBasis txClockBasis_ = IR_TxClockBasis::Nominal;
|
||
volatile uint32_t txAcceptedAtUs_ = 0;
|
||
volatile uint32_t txArmedAtUs_ = 0;
|
||
volatile uint32_t txTerminalAtUs_ = 0;
|
||
uint32_t txNextOperationId_ = 0;
|
||
|
||
uint32_t beginTxOperation(const IR_TxPlan& plan);
|
||
void markTxArmed(uint32_t operationId);
|
||
bool finishTxOperation(uint32_t operationId, IR_SendStatus terminalStatus);
|
||
volatile bool state = LOW; /// @brief Текущий уровень генерации
|
||
|
||
volatile uint8_t dataByteCounter = 0;
|
||
|
||
volatile uint8_t toggleCounter = 0; /// @brief Счётчик переключений
|
||
volatile uint8_t dataBitCounter = 0;
|
||
|
||
volatile uint8_t preambFrontCounter = 0;
|
||
volatile uint8_t dataSequenceCounter = 0;
|
||
volatile uint8_t syncSequenceCounter = 0;
|
||
volatile bool syncLastBit = false;
|
||
|
||
struct BitSequence
|
||
{
|
||
uint8_t low;
|
||
uint8_t high;
|
||
};
|
||
static uint8_t bitHigh[2];
|
||
static uint8_t bitLow[2];
|
||
uint8_t *currentBitSequence = bitLow;
|
||
volatile SignalPart signal = noSignal;
|
||
};
|