#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 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(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; };