diff --git a/IR_Decoder.cpp b/IR_Decoder.cpp index 9998b03..a8b1809 100644 --- a/IR_Decoder.cpp +++ b/IR_Decoder.cpp @@ -162,7 +162,9 @@ void IR_Decoder::_tick() if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast) isWaitingAcceptSend = true; } - gotRaw.set(&packInfo, id); + // Raw keeps the decoder's common minimum-size contract. Known packet + // layouts are validated by their typed BasePack::set calls above. + gotRaw.set(&packInfo, id, false); } if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay) { diff --git a/IR_DecoderRaw.cpp b/IR_DecoderRaw.cpp index 325d52a..66ed504 100644 --- a/IR_DecoderRaw.cpp +++ b/IR_DecoderRaw.cpp @@ -1,5 +1,6 @@ #include "IR_DecoderRaw.h" #include "IR_Encoder.h" +#include "IrInterruptGuard.h" #include #include @@ -53,9 +54,8 @@ IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPa bool IR_DecoderRaw::isSubOverflow() { - noInterrupts(); - volatile bool ret = isSubBufferOverflow; - interrupts(); + IrInterruptGuard guard; + const bool ret = isSubBufferOverflow; return ret; } @@ -103,7 +103,7 @@ void IR_DecoderRaw::refreshPairMuteState() ++active; } const uint32_t nowUs = micros(); - noInterrupts(); + IrInterruptGuard guard; const bool wasActive = (isPairSending != 0); isPairSending = active; #if IR_RX_BRIEF_LOG @@ -121,7 +121,6 @@ void IR_DecoderRaw::refreshPairMuteState() rxBriefMuteBlockedEdges = 0; } #endif - interrupts(); } #if IR_RX_BRIEF_LOG @@ -142,6 +141,7 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason) case RxBriefReason::Timeout: return F("TIMEOUT"); case RxBriefReason::Crc: return F("CRC"); case RxBriefReason::Ok: return F("OK"); + case RxBriefReason::Count: return F("UNK"); default: return F("UNK"); } } @@ -152,7 +152,7 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason) void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs) { const uint8_t ri = (uint8_t)reason; - if (ri < 14U) + if (ri < kRxBriefReasonCount) rxReasonCnt[ri]++; #if !IR_RX_BRIEF_LOG (void)a; (void)b; (void)tUs; @@ -228,6 +228,8 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin Serial.print(b); } break; + case RxBriefReason::Count: + break; } Serial.println(); #endif // IR_RX_BRIEF_LOG (печать) @@ -235,11 +237,14 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin void IR_DecoderRaw::printRxReasonStats(Print &out) const { - static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD", - "GLITCH", "TIME", "PREAMB", "SYNC", "BUF", - "TIMEOUT", "CRC", "OK"}; + static const char *const kTags[] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD", + "GLITCH", "TIME", "PREAMB", "SYNC", "BUF", + "TIMEOUT", "CRC", "OK"}; + static_assert(sizeof(kTags) / sizeof(kTags[0]) == kRxBriefReasonCount, + "RX reason tag table must match RxBriefReason::Count"); out.print(F("RXSTAT")); - for (uint8_t i = 1; i < 14U; i++) + for (uint8_t i = static_cast(RxBriefReason::MuteBegin); + i < kRxBriefReasonCount; ++i) { out.print(','); out.print(kTags[i]); @@ -273,22 +278,23 @@ void IR_DecoderRaw::rxBriefFlushDeferredIsrLogs() uint16_t muteEndCnt = 0; uint16_t rawCnt = 0; uint32_t rawLastUs = 0; - noInterrupts(); - muteBeginPending = rxBriefMuteBeginPending; - muteBeginUs = rxBriefMuteBeginUs; - rxBriefMuteBeginPending = false; - rxBriefMuteBeginUs = 0; - muteEndPending = rxBriefMuteEndPending; - muteEndUs = rxBriefMuteEndUs; - muteEndCnt = rxBriefMuteEndCount; - rxBriefMuteEndPending = false; - rxBriefMuteEndUs = 0; - rxBriefMuteEndCount = 0; - rawCnt = rxBriefRawOverflowDrops; - rawLastUs = rxBriefRawOverflowLastUs; - rxBriefRawOverflowDrops = 0; - rxBriefRawOverflowLastUs = 0; - interrupts(); + { + IrInterruptGuard guard; + muteBeginPending = rxBriefMuteBeginPending; + muteBeginUs = rxBriefMuteBeginUs; + rxBriefMuteBeginPending = false; + rxBriefMuteBeginUs = 0; + muteEndPending = rxBriefMuteEndPending; + muteEndUs = rxBriefMuteEndUs; + muteEndCnt = rxBriefMuteEndCount; + rxBriefMuteEndPending = false; + rxBriefMuteEndUs = 0; + rxBriefMuteEndCount = 0; + rawCnt = rxBriefRawOverflowDrops; + rawLastUs = rxBriefRawOverflowLastUs; + rxBriefRawOverflowDrops = 0; + rxBriefRawOverflowLastUs = 0; + } if (muteBeginPending) rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs); if (muteEndPending) @@ -359,7 +365,7 @@ void IR_DecoderRaw::firstRX() #ifdef IRDEBUG wrCounter = 0; #endif - memset(dataBuffer, 0x00, dataByteSizeMax); + memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes); pulseFilterReset(); preambleResetToIdle(); } @@ -368,9 +374,8 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const { if (pulseFilterHoldCount != 0U) return true; - noInterrupts(); + IrInterruptGuard guard; const bool busy = !subBuffer.isEmpty(); - interrupts(); return busy; } @@ -378,7 +383,7 @@ void IR_DecoderRaw::listenStart() { if (rxTimeoutPipelineBusy()) return; - if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U)) + if (isReciveRaw && ((micros() - lastEdgeTime) > receiveSilenceTimeoutUs())) { #if defined(IRDEBUG_SERIAL_PACK) packTraceOnTimeoutOrAbort(true); @@ -396,7 +401,7 @@ inline void IR_DecoderRaw::checkTimeout() if (rxTimeoutPipelineBusy()) return; - if (micros() - lastEdgeTime > IR_timeout * 2U) + if (micros() - lastEdgeTime > receiveSilenceTimeoutUs()) { #if defined(IRDEBUG_SERIAL_PACK) packTraceOnTimeoutOrAbort(false); @@ -767,8 +772,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) #if !defined(IRDEBUG_SERIAL_PACK) (void)packTraceInvertFix; #endif - if (i_dataBuffer >= dataByteSizeMax * 8) - { // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3) + if (i_dataBuffer >= irproto::kMaxWireFrameBytes * 8U) + { // >=: не даёт записать бит за пределом 5-битной wire-длины. isBufferOverflow = true; rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros()); #if defined(IRDEBUG_SERIAL_PACK) @@ -876,7 +881,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) { // Ппервый байт packSize = dataBuffer[0] & IR_MASK_MSG_INFO; // B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё. - // Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38). + // Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение wire-буфера. // packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно. if (packSize != 0 && packSize < msgBytes + crcBytes) isWrongPack = true; @@ -910,7 +915,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) uint8_t packTraceBfBit = 0; bool packTraceBfMark = false; if (!isAvailable) // Исправление первого бита // Очень большая затычка... - for (size_t i = 0; i < min(uint16_t(packSize - crcBytes * 2U), uint16_t(dataByteSizeMax)); ++i) + for (size_t i = 0; i < min(uint16_t(packSize - crcBytes * 2U), uint16_t(irproto::kMaxWireFrameBytes)); ++i) { for (int j = 0; j < 8; ++j) { @@ -918,7 +923,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) dataBuffer[i] ^= 1 << j; isAvailable = - crcCheck(min(uint16_t(packSize - crcBytes), uint16_t(dataByteSizeMax - 1U)), crcValue); + crcCheck(min(uint16_t(packSize - crcBytes), uint16_t(irproto::kMaxWireFrameBytes - 1U)), crcValue); // обратно инвертируем бит в исходное состояние if (isAvailable) @@ -953,7 +958,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix) rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros()); else rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros()); - if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) { + if (!isAvailable && packSize > 0 && packSize <= irproto::kMaxWireFrameBytes) { memcpy(rejectBuffer, dataBuffer, packSize); rejectPackSize = static_cast(packSize); isRejectAvailable = true; @@ -1007,38 +1012,34 @@ uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider) void IR_DecoderRaw::edgeTracePush(uint32_t t_us, uint8_t level, uint8_t flags) { const uint16_t cap = static_cast(IR_EDGE_TRACE_CAPACITY); - noInterrupts(); + IrInterruptGuard guard; const uint16_t w = edgeTrace_w; const uint16_t r = edgeTrace_r; const uint16_t next = static_cast((w + 1u) % cap); if (next == r) { edgeTrace_overflow = true; - interrupts(); return; } edgeTrace_buf[w].t_us = t_us; edgeTrace_buf[w].level = level ? 1u : 0u; edgeTrace_buf[w].flags = flags; edgeTrace_w = next; - interrupts(); } void IR_DecoderRaw::edgeTraceClear() { - noInterrupts(); + IrInterruptGuard guard; edgeTrace_w = 0; edgeTrace_r = 0; edgeTrace_overflow = false; - interrupts(); } uint16_t IR_DecoderRaw::edgeTracePendingCount() const { - noInterrupts(); + IrInterruptGuard guard; const uint16_t w = edgeTrace_w; const uint16_t r = edgeTrace_r; - interrupts(); const uint16_t cap = static_cast(IR_EDGE_TRACE_CAPACITY); if (w >= r) return static_cast(w - r); @@ -1054,27 +1055,28 @@ uint16_t IR_DecoderRaw::edgeTraceFlushChunk(Print &out, uint16_t maxRec) maxRec = kStackCap; const uint16_t cap = static_cast(IR_EDGE_TRACE_CAPACITY); - noInterrupts(); - const uint16_t w = edgeTrace_w; - const uint16_t r = edgeTrace_r; - uint16_t avail = (w >= r) ? static_cast(w - r) : static_cast(cap - r + w); - uint16_t toCopy = (avail > maxRec) ? maxRec : avail; - const bool truncated = (avail > toCopy); - if (toCopy == 0) - { - interrupts(); - return 0; - } - uint8_t tmp[kStackCap * 6]; - for (uint16_t i = 0; i < toCopy; ++i) + uint16_t toCopy = 0U; + bool truncated = false; + bool ovf = false; { - const uint16_t idx = static_cast((r + i) % cap); - memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u); + IrInterruptGuard guard; + const uint16_t w = edgeTrace_w; + const uint16_t r = edgeTrace_r; + const uint16_t avail = (w >= r) ? static_cast(w - r) + : static_cast(cap - r + w); + toCopy = (avail > maxRec) ? maxRec : avail; + truncated = (avail > toCopy); + if (toCopy == 0U) + return 0U; + for (uint16_t i = 0; i < toCopy; ++i) + { + const uint16_t idx = static_cast((r + i) % cap); + memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u); + } + edgeTrace_r = static_cast((r + toCopy) % cap); + ovf = edgeTrace_overflow; } - edgeTrace_r = static_cast((r + toCopy) % cap); - const bool ovf = edgeTrace_overflow; - interrupts(); uint8_t meta = 0; if (ovf) @@ -1304,7 +1306,7 @@ void IR_DecoderRaw::packTraceForceEndSyncPhase() void IR_DecoderRaw::packTraceEmitHex(uint8_t byteCount) const { Serial.print(F("IR hex:")); - for (uint8_t i = 0; i < byteCount && i < dataByteSizeMax; i++) + for (uint8_t i = 0; i < byteCount && i < irproto::kMaxWireFrameBytes; i++) { Serial.print(' '); ptPrintHexU8(dataBuffer[i]); @@ -1402,8 +1404,8 @@ void IR_DecoderRaw::packTraceEmitErrorFlash(const __FlashStringHelper *msg) Serial.println(msg); { uint16_t nb = i_dataBuffer / 8u; - if (nb > dataByteSizeMax) - nb = dataByteSizeMax; + if (nb > irproto::kMaxWireFrameBytes) + nb = irproto::kMaxWireFrameBytes; packTraceEmitHex(static_cast(nb)); } packTraceResetFrame(); @@ -1436,8 +1438,8 @@ void IR_DecoderRaw::packTraceOnTimeoutOrAbort(bool fromListenStart) return; const uint16_t expected = (i_dataBuffer >= 8) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0; uint16_t gotBytes = i_dataBuffer / 8; - if (gotBytes > dataByteSizeMax) - gotBytes = dataByteSizeMax; + if (gotBytes > irproto::kMaxWireFrameBytes) + gotBytes = irproto::kMaxWireFrameBytes; Serial.println(); packTraceEmitRawBitsLine(false); Serial.print(F(" => ERROR: TIMEOUT, rx_data_size = ")); @@ -1609,7 +1611,7 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front) bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) { - const uint32_t longSilence = IR_timeout * 2U; + const uint32_t longSilence = receiveSilenceTimeoutUs(); const uint32_t candTimeout = IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT; if (preambleState == PreambleState::Idle) @@ -1696,7 +1698,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) err_syncBit = 0; isWrongPack = false; msgTypeReceive = 0; - memset(dataBuffer, 0x00, dataByteSizeMax); + memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes); preambleState = PreambleState::Locked; isPreamb = false; diff --git a/IR_DecoderRaw.h b/IR_DecoderRaw.h index a574934..83ffcc6 100644 --- a/IR_DecoderRaw.h +++ b/IR_DecoderRaw.h @@ -20,12 +20,13 @@ class Print; ///////////////////////////////////////////////////////////////////////////////////////////////// #define riseTime riseSyncTime //* bitTime */ 893U // TODO: Должно высчитываться медианой -#define riseTolerance tolerance /* 250U */ // погрешность +#define riseTolerance IR_TIMING_TOLERANCE_US /* 250U */ // погрешность #define riseTimeMax (riseTime + riseTolerance) #define riseTimeMin (riseTime - riseTolerance) #define aroundRise(t) (riseTimeMin < t && t < riseTimeMax) -#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1 -constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; +// Compatibility aliases. The named contracts and their geometry live in IR_config.h. +#define IR_timeout (::irproto::rxInterEdgeTimeoutUs(riseTime)) +constexpr uint16_t IR_ResponseDelay = irproto::kDefaultResponseTurnaroundDelayMs; class IR_Encoder; class IR_DecoderRaw : virtual public IR_FOX @@ -51,7 +52,14 @@ public: inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился bool isSubOverflow(); - volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета + inline bool isReciving() const { return isRecive; } // Возвращает true, если происходит приём пакета + /** Current adaptive silence threshold that terminates an RX candidate. */ + inline uint32_t receiveSilenceTimeoutUs() const { + return irproto::rxSilenceTimeoutUs(riseTime); + } + inline uint32_t receiveSilenceTimeoutMsCeil() const { + return irproto::microsToMillisCeil(receiveSilenceTimeoutUs()); + } uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } @@ -68,8 +76,9 @@ public: /// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch, /// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok). /// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат). + static constexpr uint8_t rxReasonCounterCount() { return kRxBriefReasonCount; } const uint16_t *rxReasonCounters() const { return rxReasonCnt; } - void rxReasonCountersClear() { for (uint8_t i = 0; i < 14; i++) rxReasonCnt[i] = 0; } + void rxReasonCountersClear() { for (uint8_t i = 0; i < kRxBriefReasonCount; ++i) rxReasonCnt[i] = 0; } /// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.." void printRxReasonStats(Print &out) const; @@ -94,12 +103,14 @@ private: BufferOverflow = 10, Timeout = 11, Crc = 12, - Ok = 13 + Ok = 13, + Count }; + static constexpr uint8_t kRxBriefReasonCount = static_cast(RxBriefReason::Count); bool isRejectAvailable = false; uint8_t rejectPackSize = 0; - uint8_t rejectBuffer[dataByteSizeMax]{}; + uint8_t rejectBuffer[irproto::kMaxWireFrameBytes]{}; ErrorsStruct errors; bool isAvailable = false; @@ -179,7 +190,7 @@ private: #endif //////////////////////////////////////////////////////////////////////// - uint8_t dataBuffer[dataByteSizeMax]{0}; // Буффер данных + uint8_t dataBuffer[irproto::kMaxWireFrameBytes]{0}; // Буффер полного wire-кадра volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов volatile uint32_t risePeriod; @@ -247,7 +258,7 @@ bool isReciveRaw = false; // (always-on наблюдаемость по контракту живучести), печать события — // только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны. void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0); - uint16_t rxReasonCnt[14] = {}; // индекс = (uint8_t)RxBriefReason, 1..13 + uint16_t rxReasonCnt[kRxBriefReasonCount] = {}; // индекс = RxBriefReason, 1..Count-1 #if IR_RX_BRIEF_LOG static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason); void rxBriefNoteMuteBlockedIsr(uint32_t tUs); @@ -263,7 +274,7 @@ bool isReciveRaw = false; #if defined(IRDEBUG_SERIAL_PACK) static constexpr uint16_t kPackTraceBufCap = - uint16_t(dataByteSizeMax) * (uint16_t(bitPerByte) + uint16_t(syncBits)) + 48u; + uint16_t(irproto::kMaxWireFrameBytes) * (uint16_t(bitPerByte) + uint16_t(syncBits)) + 48u; void packTraceResetFrame(); void packTracePushBit(bool bit); diff --git a/IR_Encoder.cpp b/IR_Encoder.cpp index c7c96c6..78f790f 100644 --- a/IR_Encoder.cpp +++ b/IR_Encoder.cpp @@ -57,10 +57,11 @@ IR_Encoder::IR_Encoder(uint8_t pin, uint16_t addr, IR_DecoderRaw *decPair, bool pinMode(pin, OUTPUT); } powerNumerator_ = 1; -}; +} HardwareTimer* IR_Encoder::IR_Timer = nullptr; IR_Encoder::ExternalTxStartFn IR_Encoder::externalTxStartFn = nullptr; +IR_Encoder::ExternalTxStartFnV2 IR_Encoder::externalTxStartFnV2 = nullptr; IR_Encoder::ExternalTxBusyFn IR_Encoder::externalTxBusyFn = nullptr; void *IR_Encoder::externalTxCtx = nullptr; bool IR_Encoder::txIsrLegacyMode_ = true; @@ -96,6 +97,16 @@ const char* irSendStatusToString(IR_SendStatus status) return "EncoderPinUnavailable"; case IR_SendStatus::BufferedStorageInvalid: return "BufferedStorageInvalid"; + case IR_SendStatus::InvalidArgument: + return "InvalidArgument"; + case IR_SendStatus::TimingOverflow: + return "TimingOverflow"; + case IR_SendStatus::PlanMismatch: + return "PlanMismatch"; + case IR_SendStatus::DmaTransferError: + return "DmaTransferError"; + case IR_SendStatus::DmaStalled: + return "DmaStalled"; default: return "Unknown"; } @@ -164,35 +175,68 @@ bool IR_Encoder::scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, si multiply = 2; } const size_t nIn = *ioCount; - if (nIn > irproto::kIsrTxMaxGateRuns) + if (nIn > maxRuns) { return false; } - IrTxGateRun copy[irproto::kIsrTxMaxGateRuns]; - memcpy(copy, runs, nIn * sizeof(IrTxGateRun)); - size_t w = 0; + + // First determine the exact output size without touching the caller's + // data. A physical run can split only at uint16_t storage boundaries. + // The second pass walks backwards, so expanded output never overwrites an + // input run that has not been consumed yet. This keeps the helper fully + // in-place instead of reserving several kilobytes of temporary stack. + uint64_t logicalBoundary = 0U; + uint64_t physicalBoundary = 0U; + size_t outputCount = 0U; for (size_t r = 0; r < nIn; r++) { - uint32_t phys = (uint32_t)copy[r].lenTicks * (uint32_t)multiply / 2U; - if (copy[r].lenTicks > 0 && phys == 0) + if (runs[r].lenTicks == 0U || + logicalBoundary > UINT64_MAX - runs[r].lenTicks) { - phys = 1; + return false; } - const bool g = copy[r].gate; - while (phys > 0) + logicalBoundary += runs[r].lenTicks; + if (logicalBoundary > (UINT64_MAX - 1U) / multiply) { - if (w >= maxRuns) - { - return false; - } - const uint32_t chunk = phys > 65535U ? 65535U : phys; - runs[w].lenTicks = static_cast(chunk); - runs[w].gate = g; - w++; - phys -= chunk; + return false; } + const uint64_t nextPhysicalBoundary = + (logicalBoundary * static_cast(multiply) + 1U) / 2U; + const uint64_t physicalLen = nextPhysicalBoundary - physicalBoundary; + physicalBoundary = nextPhysicalBoundary; + const uint64_t chunks = (physicalLen + 65534U) / 65535U; + if (chunks > static_cast(maxRuns - outputCount)) + { + return false; + } + outputCount += static_cast(chunks); } - *ioCount = w; + + size_t write = outputCount; + uint64_t logicalEnd = logicalBoundary; + uint64_t physicalEnd = physicalBoundary; + for (size_t r = nIn; r != 0U; --r) + { + const IR_TxGateRun input = runs[r - 1U]; + const uint64_t logicalStart = logicalEnd - input.lenTicks; + const uint64_t physicalStart = + (logicalStart * static_cast(multiply) + 1U) / 2U; + uint64_t physicalLen = physicalEnd - physicalStart; + uint64_t chunks = (physicalLen + 65534U) / 65535U; + while (chunks != 0U) + { + // We are writing backwards: emit the final (possibly short) + // chunk first, then full-sized chunks before it. + const uint64_t chunk64 = physicalLen - (chunks - 1U) * 65535U; + runs[--write].lenTicks = static_cast(chunk64); + runs[write].gate = input.gate; + physicalLen -= chunk64; + --chunks; + } + logicalEnd = logicalStart; + physicalEnd = physicalStart; + } + *ioCount = outputCount; return true; } @@ -352,6 +396,23 @@ bool IR_Encoder::txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool return txAdvanceAfterOutput(st, sendBufferLocal); } +IR_Encoder::TxFsmState IR_Encoder::initialTxFsm(uint8_t len) +{ + TxFsmState st{}; + st.sendLen = len; + st.toggleCounter = preambToggle; + st.dataBitCounter = bitPerByte - 1; + st.dataByteCounter = 0; + st.preambFrontCounter = preambPulse * 2 - 1; + st.dataSequenceCounter = bitPerByte * 2; + st.syncSequenceCounter = syncBits * 2; + st.syncLastBit = false; + st.signal = preamb; + st.state = HIGH; + st.currentBitSequence = bitHigh; + return st; +} + void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const { st.sendLen = sendLen; @@ -427,13 +488,27 @@ void IR_Encoder::beginClockOnly(HardwareTimer *timer) void IR_Encoder::setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx) { externalTxStartFn = startFn; + externalTxStartFnV2 = nullptr; + externalTxBusyFn = busyFn; + externalTxCtx = ctx; +} + +void IR_Encoder::setExternalTxBackendV2(ExternalTxStartFnV2 startFn, ExternalTxBusyFn busyFn, void *ctx) +{ + externalTxStartFn = nullptr; + externalTxStartFnV2 = startFn; externalTxBusyFn = busyFn; externalTxCtx = ctx; } void IR_Encoder::externalFinishSend() { - if (!isSending) + externalFinishSend(txOperationId_, IR_SendStatus::Success); +} + +void IR_Encoder::externalFinishSend(uint32_t operationId, IR_SendStatus terminalStatus) +{ + if (!isSending || operationId == 0U || operationId != txOperationId_) return; // Force output low. @@ -445,6 +520,95 @@ void IR_Encoder::externalFinishSend() txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; refreshBlindDecoderMuteState(); + finishTxOperation(operationId, terminalStatus); +} + +uint32_t IR_Encoder::beginTxOperation(const IR_TxPlan& plan) +{ + uint32_t operationId = txNextOperationId_ + 1U; + if (operationId == 0U) + operationId = 1U; + txNextOperationId_ = operationId; + + txRecordVersion_++; + txOperationId_ = operationId; + txState_ = IR_TxState::Preparing; + txTerminalStatus_ = IR_SendStatus::Success; + txMultiplySnap_ = plan.carrierMultiply; + txPlannedPhysicalTicks_ = plan.physicalTicks; + txPlannedAirtimeUs_ = plan.airtimeUs; + txClockBasis_ = plan.clockBasis; + txAcceptedAtUs_ = micros(); + txArmedAtUs_ = 0U; + txTerminalAtUs_ = 0U; + txRecordVersion_++; + return operationId; +} + +void IR_Encoder::markTxArmed(uint32_t operationId) +{ + if (operationId == 0U || operationId != txOperationId_ || txState_ != IR_TxState::Preparing) + return; + txRecordVersion_++; + txArmedAtUs_ = micros(); + txState_ = IR_TxState::Transmitting; + txRecordVersion_++; +} + +bool IR_Encoder::finishTxOperation(uint32_t operationId, IR_SendStatus terminalStatus) +{ + if (operationId == 0U || operationId != txOperationId_) + return false; + if (txState_ != IR_TxState::Preparing && txState_ != IR_TxState::Transmitting) + return false; + + txRecordVersion_++; + txTerminalStatus_ = terminalStatus; + txTerminalAtUs_ = micros(); + txState_ = terminalStatus == IR_SendStatus::Success ? IR_TxState::Completed : IR_TxState::Failed; + txRecordVersion_++; + return true; +} + +IR_TxSnapshot IR_Encoder::txSnapshot() const +{ + IR_TxSnapshot snapshot; + uint8_t before = 0U; + uint8_t after = 0U; + do + { + before = txRecordVersion_; + if ((before & 1U) != 0U) + continue; + snapshot.operationId = txOperationId_; + snapshot.state = txState_; + snapshot.status = txTerminalStatus_; + snapshot.carrierMultiply = txMultiplySnap_; + snapshot.clockBasis = txClockBasis_; + snapshot.plannedPhysicalTicks = txPlannedPhysicalTicks_; + snapshot.plannedAirtimeUs = txPlannedAirtimeUs_; + snapshot.acceptedAtUs = txAcceptedAtUs_; + snapshot.armedAtUs = txArmedAtUs_; + snapshot.terminalAtUs = txTerminalAtUs_; + after = txRecordVersion_; + } while (before != after || (after & 1U) != 0U); + return snapshot; +} + +bool IR_Encoder::isOperationTerminal(uint32_t operationId) const +{ + if (operationId == 0U) + return false; + const IR_TxSnapshot snapshot = txSnapshot(); + return snapshot.operationId == operationId && snapshot.terminal(); +} + +bool IR_Encoder::isOperationComplete(uint32_t operationId) const +{ + if (operationId == 0U) + return false; + const IR_TxSnapshot snapshot = txSnapshot(); + return snapshot.operationId == operationId && snapshot.state == IR_TxState::Completed; } size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns) @@ -453,27 +617,16 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu { return 0; } - if (len == 0 || len > dataByteSizeMax) + if (len == 0 || len > irproto::kMaxWireFrameBytes) { return 0; } // Copy into fixed-size buffer to match original encoder behavior (safe reads past sendLen). - uint8_t sendBufferLocal[dataByteSizeMax] = {0}; + uint8_t sendBufferLocal[irproto::kMaxWireFrameBytes] = {0}; memcpy(sendBufferLocal, packet, len); - TxFsmState st{}; - st.sendLen = len; - st.toggleCounter = preambToggle; - st.dataBitCounter = bitPerByte - 1; - st.dataByteCounter = 0; - st.preambFrontCounter = preambPulse * 2 - 1; - st.dataSequenceCounter = bitPerByte * 2; - st.syncSequenceCounter = syncBits * 2; - st.syncLastBit = false; - st.signal = preamb; - st.state = HIGH; - st.currentBitSequence = bitHigh; + TxFsmState st = initialTxFsm(len); size_t runCount = 0; bool isActive = true; @@ -502,103 +655,234 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply) { - if (packet == nullptr || outRuns == nullptr || maxRuns == 0) - { - return 0; - } - if (len == 0 || len > dataByteSizeMax) - { - return 0; - } - if (multiply < 2) - { - multiply = 2; - } + if (outRuns == nullptr || maxRuns == 0U) + return 0U; + const IR_TxPlan plan = buildPhysicalTransmission(packet, len, outRuns, maxRuns, multiply); + return plan.valid() ? static_cast(plan.gateRunCount) : 0U; +} - // Copy into fixed-size buffer to match original encoder behavior (safe reads past sendLen). - uint8_t sendBufferLocal[dataByteSizeMax] = {0}; +IR_TxPlan IR_Encoder::buildPhysicalPlan(const uint8_t *packet, + uint8_t len, + IR_TxGateRun *outRuns, + size_t maxRuns, + uint16_t multiply, + bool emitRuns) +{ + IR_TxPlan plan; + if (packet == nullptr || len == 0U) + { + plan.status = IR_SendStatus::InvalidArgument; + return plan; + } + if (len > irproto::kMaxWireFrameBytes) + { + plan.status = IR_SendStatus::BufferTooLarge; + return plan; + } + if (emitRuns && (outRuns == nullptr || maxRuns == 0U)) + { + plan.status = IR_SendStatus::InvalidArgument; + return plan; + } + if (multiply < 2U) + multiply = 2U; + + plan.wireBytes = len; + plan.carrierMultiply = multiply; + plan.clockBasis = IR_TxClockBasis::Nominal; + plan.tickClockHz = static_cast(carrierFrec) * static_cast(multiply); + plan.tickDivider = 1U; + + uint8_t sendBufferLocal[irproto::kMaxWireFrameBytes] = {0}; memcpy(sendBufferLocal, packet, len); + TxFsmState st = initialTxFsm(len); - TxFsmState st{}; - st.sendLen = len; - st.toggleCounter = preambToggle; - st.dataBitCounter = bitPerByte - 1; - st.dataByteCounter = 0; - st.preambFrontCounter = preambPulse * 2 - 1; - st.dataSequenceCounter = bitPerByte * 2; - st.syncSequenceCounter = syncBits * 2; - st.syncLastBit = false; - st.signal = preamb; - st.state = HIGH; - st.currentBitSequence = bitHigh; - - auto appendPhysicalRun = [&](bool gate, uint32_t logicalLen, size_t& runCount) -> bool { - if (logicalLen == 0) - { + uint64_t logicalBoundary = 0U; + uint64_t physicalBoundary = 0U; + uint32_t runCount = 0U; + bool capacityExceeded = false; + auto appendPhysicalRun = [&](bool gate, uint32_t logicalLen) -> bool { + if (logicalLen == 0U) return true; - } - uint32_t phys = (logicalLen * (uint32_t)multiply) / 2U; - if (logicalLen > 0 && phys == 0) - { - phys = 1; - } + logicalBoundary += logicalLen; + // One logical tick is 1/(2*carrierFrec). Cumulative ceil preserves + // the exact rational phase for both even and odd multiply values. + const uint64_t nextPhysicalBoundary = + (logicalBoundary * static_cast(multiply) + 1U) / 2U; + uint64_t physicalLen = nextPhysicalBoundary - physicalBoundary; + physicalBoundary = nextPhysicalBoundary; - while (phys > 0) + while (physicalLen != 0U) { - if (runCount >= maxRuns) - { + if (runCount == UINT32_MAX) return false; + const uint16_t chunk = static_cast( + physicalLen > 65535U ? 65535U : physicalLen); + if (emitRuns && static_cast(runCount) < maxRuns) + { + outRuns[runCount].gate = gate; + outRuns[runCount].lenTicks = chunk; } - - const uint32_t chunk = phys > 65535U ? 65535U : phys; - outRuns[runCount].gate = gate; - outRuns[runCount].lenTicks = static_cast(chunk); - runCount++; - phys -= chunk; + else if (emitRuns) + { + capacityExceeded = true; + } + ++runCount; + physicalLen -= chunk; } return true; }; - size_t runCount = 0; bool currentGate = false; - uint32_t currentLogicalLen = 0; + uint32_t currentLogicalLen = 0U; bool havePendingRun = false; bool isActive = true; while (isActive) { bool gate = false; isActive = txEmitTick(st, sendBufferLocal, gate); - if (!havePendingRun) { currentGate = gate; currentLogicalLen = 1U; havePendingRun = true; - continue; } - - if (currentGate == gate) + else if (currentGate == gate) { - currentLogicalLen++; - continue; + ++currentLogicalLen; } - - if (!appendPhysicalRun(currentGate, currentLogicalLen, runCount)) + else { - return 0; + if (!appendPhysicalRun(currentGate, currentLogicalLen)) + { + plan.status = IR_SendStatus::TimingOverflow; + return plan; + } + currentGate = gate; + currentLogicalLen = 1U; } - - currentGate = gate; - currentLogicalLen = 1U; } - - if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount)) + if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen)) { - return 0; + plan.status = IR_SendStatus::TimingOverflow; + return plan; + } + if (physicalBoundary > UINT32_MAX) + { + plan.status = IR_SendStatus::TimingOverflow; + return plan; } - return runCount; + plan.physicalTicks = static_cast(physicalBoundary); + plan.gateRunCount = runCount; + if (capacityExceeded) + { + plan.status = IR_SendStatus::BuildGateRunsFailed; + return plan; + } + plan.status = IR_SendStatus::Success; + if (!calculateAirtimeUs(plan)) + plan.status = IR_SendStatus::TimingOverflow; + return plan; +} + +bool IR_Encoder::calculateAirtimeUs(IR_TxPlan& plan) +{ + if (plan.tickClockHz == 0U || plan.tickDivider == 0U) + return false; + + auto gcd64 = [](uint64_t a, uint64_t b) -> uint64_t { + while (b != 0U) + { + const uint64_t next = a % b; + a = b; + b = next; + } + return a; + }; + + uint64_t a = plan.physicalTicks; + uint64_t b = plan.tickDivider; + uint64_t c = 1000000U; + uint64_t denominator = plan.tickClockHz; + uint64_t divisor = gcd64(a, denominator); + a /= divisor; + denominator /= divisor; + divisor = gcd64(b, denominator); + b /= divisor; + denominator /= divisor; + divisor = gcd64(c, denominator); + c /= divisor; + denominator /= divisor; + + const uint64_t max64 = ~static_cast(0U); + if ((b != 0U && a > max64 / b) || + (c != 0U && a * b > max64 / c)) + return false; + const uint64_t numerator = a * b * c; + uint64_t duration = numerator / denominator; + if ((numerator % denominator) != 0U) + ++duration; + if (duration > UINT32_MAX) + return false; + plan.airtimeUs = static_cast(duration); + return true; +} + +bool IR_Encoder::applyTickClock(IR_TxPlan& plan, + uint32_t clockNumeratorHz, + uint32_t clockDivider, + IR_TxClockBasis basis) +{ + if (!plan.valid() || clockNumeratorHz == 0U || clockDivider == 0U) + return false; + plan.clockBasis = basis; + plan.tickClockHz = clockNumeratorHz; + plan.tickDivider = clockDivider; + if (!calculateAirtimeUs(plan)) + { + plan.status = IR_SendStatus::TimingOverflow; + return false; + } + return true; +} + +void IR_Encoder::applyConfiguredTimerClock(IR_TxPlan& plan) +{ + if (!plan.valid() || IR_Timer == nullptr) + return; + const uint32_t timerClockHz = IR_Timer->getTimerClkFreq(); + const uint64_t divider = static_cast(IR_Timer->getPrescaleFactor()) * + static_cast(IR_Timer->getOverflow(TICK_FORMAT)); + if (timerClockHz == 0U || divider == 0U || divider > UINT32_MAX) + { + plan.status = IR_SendStatus::TimingOverflow; + return; + } + applyTickClock(plan, timerClockHz, static_cast(divider), + IR_TxClockBasis::ConfiguredTimer); +} + +IR_TxPlan IR_Encoder::planPhysicalTransmission(const uint8_t *packet, uint8_t len, uint16_t multiply) +{ + return buildPhysicalPlan(packet, len, nullptr, 0U, multiply, false); +} + +IR_TxPlan IR_Encoder::buildPhysicalTransmission(const uint8_t *packet, + uint8_t len, + IR_TxGateRun *outRuns, + size_t maxRuns, + uint16_t multiply) +{ + return buildPhysicalPlan(packet, len, outRuns, maxRuns, multiply, true); +} + +IR_TxPlan IR_Encoder::planTransmission(const uint8_t *packet, uint8_t len) const +{ + IR_TxPlan plan = planPhysicalTransmission(packet, len, carrierMultiply()); + applyConfiguredTimerClock(plan); + return plan; } @@ -675,7 +959,7 @@ void IR_Encoder::setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count) refreshBlindDecoderMuteState(); } -IR_Encoder::~IR_Encoder(){}; +IR_Encoder::~IR_Encoder(){} IR_SendResult IR_Encoder::sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept) { @@ -691,13 +975,15 @@ IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8 // 5-битное поле длины => ВЕСЬ кадр ≤31 байт (для Data payload ≤24). Было `len > bytePerPack(31)` — // неверно: packSize=7+len оборачивался в заголовке (packSize & 0x1F) при len 25..31 → кадр молча // терялся, а send возвращал успех. Проверяем полный packSize в широком типе (uint8_t 7+len мог переполниться). - if (((uint16_t)msgBytes + addrBytes + addrBytes + len + crcBytes) > IR_MASK_MSG_INFO) + if (len > irproto::kMaxDataPayloadBytes) { Serial.println("IR Pack to big"); return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge); } + if (len != 0U && data == nullptr) + return IR_SendResult(false, 0, IR_SendStatus::InvalidArgument); constexpr uint8_t dataStart = msgBytes + addrBytes + addrBytes; - memset(sendBuffer, 0x00, dataByteSizeMax); + memset(sendBuffer, 0x00, irproto::kMaxWireFrameBytes); uint8_t packSize = msgBytes + addrBytes + addrBytes + len + crcBytes; uint8_t msgType = ((needAccept ? IR_MSG_DATA_ACCEPT : IR_MSG_DATA_NOACCEPT) << 5) | (packSize & IR_MASK_MSG_INFO); @@ -744,22 +1030,15 @@ IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8 // } // отправка - const IR_SendStatus status = rawSend(sendBuffer, packSize); - if (status != IR_SendStatus::Success) - { - return IR_SendResult(false, 0, status); - } - - // Возвращаем результат отправки - uint32_t sendTime = calculateSendTime(packSize); - return IR_SendResult(true, sendTime, status); + return rawSendTracked(sendBuffer, packSize); } IR_SendResult IR_Encoder::sendAccept(uint16_t addrTo, uint8_t customByte) { + (void)addrTo; constexpr uint8_t packsize = msgBytes + addrBytes + 1U + crcBytes; - memset(sendBuffer, 0x00, dataByteSizeMax); + memset(sendBuffer, 0x00, irproto::kMaxWireFrameBytes); sendBuffer[0] = IR_MSG_ACCEPT << 5; sendBuffer[0] |= packsize & IR_MASK_MSG_INFO; // размер пакета @@ -776,21 +1055,13 @@ IR_SendResult IR_Encoder::sendAccept(uint16_t addrTo, uint8_t customByte) sendBuffer[4] = crc8(sendBuffer, 0, 4, poly1) & 0xFF; sendBuffer[5] = crc8(sendBuffer, 0, 5, poly2) & 0xFF; - const IR_SendStatus status = rawSend(sendBuffer, packsize); - if (status != IR_SendStatus::Success) - { - return IR_SendResult(false, 0, status); - } - - // Возвращаем результат отправки - uint32_t sendTime = calculateSendTime(packsize); - return IR_SendResult(true, sendTime, status); + return rawSendTracked(sendBuffer, packsize); } IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo) { constexpr uint8_t packsize = msgBytes + addrBytes + addrBytes + crcBytes; - memset(sendBuffer, 0x00, dataByteSizeMax); + memset(sendBuffer, 0x00, irproto::kMaxWireFrameBytes); sendBuffer[0] = IR_MSG_REQUEST << 5; sendBuffer[0] |= packsize & IR_MASK_MSG_INFO; @@ -806,15 +1077,7 @@ IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo) sendBuffer[5] = crc8(sendBuffer, 0, 5, poly1) & 0xFF; sendBuffer[6] = crc8(sendBuffer, 0, 6, poly2) & 0xFF; - const IR_SendStatus status = rawSend(sendBuffer, packsize); - if (status != IR_SendStatus::Success) - { - return IR_SendResult(false, 0, status); - } - - // Возвращаем результат отправки - uint32_t sendTime = calculateSendTime(packsize); - return IR_SendResult(true, sendTime, status); + return rawSendTracked(sendBuffer, packsize); } IR_SendResult IR_Encoder::sendBack(uint8_t data) @@ -837,11 +1100,13 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d // Длина = ВЕСЬ кадр в 5 битах (≤31). Проверяем полный packSize. Было `len>bytePerPack` + `min(1,len)`: // многобайтовый back (speed + customBackData) слался ОБРЕЗАННЫМ — packSize считал лишь 1 байт данных, // остальные не влезали в кадр и затирались CRC. Теперь учитываем полный len. - if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO) - { + const uint8_t payloadLimit = isAdressed ? irproto::kMaxBackToPayloadBytes + : irproto::kMaxBackPayloadBytes; + if (len > payloadLimit) return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge); - } - memset(sendBuffer, 0x00, dataByteSizeMax); + if (len != 0U && data == nullptr) + return IR_SendResult(false, 0, IR_SendStatus::InvalidArgument); + memset(sendBuffer, 0x00, irproto::kMaxWireFrameBytes); uint8_t dataStart = msgBytes + addrBytes + (isAdressed ? addrBytes : 0); uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes; @@ -870,15 +1135,7 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d sendBuffer[packSize - crcBytes + 1] = crc8(sendBuffer, 0, packSize - crcBytes + 1, poly2) & 0xFF; // отправка - const IR_SendStatus status = rawSend(sendBuffer, packSize); - if (status != IR_SendStatus::Success) - { - return IR_SendResult(false, 0, status); - } - - // Возвращаем результат отправки - uint32_t sendTime = calculateSendTime(packSize); - return IR_SendResult(true, sendTime, status); + return rawSendTracked(sendBuffer, packSize); } void IR_Encoder::registerWithBlindDecoders() @@ -906,58 +1163,57 @@ void IR_Encoder::refreshBlindDecoderMuteState() } IR_SendStatus IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) +{ + return rawSendTracked(ptr, len).status; +} + +IR_SendResult IR_Encoder::rawSendTracked(uint8_t *ptr, uint8_t len) { if (isSending) - { - // TODO: Обработка повторной отправки - return IR_SendStatus::EncoderBusy; - } - - // Проверка на переполнение буфера - if (len > dataByteSizeMax) - { - return IR_SendStatus::BufferTooLarge; - } + return IR_SendResult(false, 0U, IR_SendStatus::EncoderBusy); + if (ptr == nullptr || len == 0U) + return IR_SendResult(false, 0U, IR_SendStatus::InvalidArgument); - // Serial.print("IR tx hex: "); - // for (uint8_t i = 0; i < len; i++) - // { - // if (ptr[i] < 0x10) Serial.print("0"); - // Serial.print(ptr[i], HEX); - // } - // Serial.println(); + IR_TxPlan plan = planTransmission(ptr, len); + if (!plan.valid()) + return IR_SendResult(false, 0U, plan.status, 0U, plan.airtimeUs, plan.clockBasis); - if (externalTxStartFn != nullptr) + const bool hasExternalBackend = externalTxStartFnV2 != nullptr || externalTxStartFn != nullptr; + if (hasExternalBackend) { if (externalTxBusyFn != nullptr && externalTxBusyFn(externalTxCtx)) - { - return IR_SendStatus::ExternalBackendBusy; - } + return IR_SendResult(false, 0U, IR_SendStatus::ExternalBackendBusy, + 0U, plan.airtimeUs, plan.clockBasis); sendLen = len; txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; isSending = true; + const uint32_t operationId = beginTxOperation(plan); refreshBlindDecoderMuteState(); - const IR_SendStatus status = externalTxStartFn(externalTxCtx, this, ptr, len); + const IR_SendStatus status = externalTxStartFnV2 != nullptr + ? externalTxStartFnV2(externalTxCtx, this, ptr, len, plan, operationId) + : externalTxStartFn(externalTxCtx, this, ptr, len); if (status != IR_SendStatus::Success) { isSending = false; refreshBlindDecoderMuteState(); + finishTxOperation(operationId, status); + return IR_SendResult(false, 0U, status, operationId, + plan.airtimeUs, plan.clockBasis); } - return status; + markTxArmed(operationId); + return IR_SendResult(true, plan.airtimeMsCeil(), status, operationId, + plan.airtimeUs, plan.clockBasis); } if (port == nullptr || mask == 0) - { - return IR_SendStatus::EncoderPinUnavailable; - } + return IR_SendResult(false, 0U, IR_SendStatus::EncoderPinUnavailable, + 0U, plan.airtimeUs, plan.clockBasis); if (ptr != sendBuffer) - { memcpy(sendBuffer, ptr, len); - } sendLen = len; const bool useBufferedIsr = shouldUseBufferedIsr(); @@ -966,31 +1222,21 @@ IR_SendStatus IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) if (!useBufferedIsr) { - toggleCounter = preambToggle; - dataBitCounter = bitPerByte - 1; - dataByteCounter = 0; - preambFrontCounter = preambPulse * 2 - 1; - dataSequenceCounter = bitPerByte * 2; - syncSequenceCounter = syncBits * 2; - signal = preamb; - state = HIGH; - currentBitSequence = bitHigh; - txMultiplySnap_ = carrierMultiply(); + const TxFsmState initial = initialTxFsm(len); + storeTxFsmToMembers(initial); { const uint16_t cap = maxPowerNumerator(); txPowerSnap_ = (powerNumerator_ > cap) ? cap : powerNumerator_; } - legacyPhysPerLogical_ = static_cast(txMultiplySnap_ / 2U); - if (legacyPhysPerLogical_ == 0) - { - legacyPhysPerLogical_ = 1; - } - legacyPhysCounter_ = 0; + legacyScaleAccumulator_ = 0U; legacySlotInPeriod_ = 0; isSending = true; + const uint32_t operationId = beginTxOperation(plan); refreshBlindDecoderMuteState(); IR_Encoder::carrierResume(); - return IR_SendStatus::Success; + markTxArmed(operationId); + return IR_SendResult(true, plan.airtimeMsCeil(), IR_SendStatus::Success, + operationId, plan.airtimeUs, plan.clockBasis); } IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_; @@ -998,26 +1244,29 @@ IR_SendStatus IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) { txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; - return IR_SendStatus::BufferedStorageInvalid; + return IR_SendResult(false, 0U, IR_SendStatus::BufferedStorageInvalid, + 0U, plan.airtimeUs, plan.clockBasis); } + isSending = true; + const uint32_t operationId = beginTxOperation(plan); + refreshBlindDecoderMuteState(); buf->resetRuntimeState(); - - txMultiplySnap_ = carrierMultiply(); - size_t nRuns = buildPhysicalGateRuns(sendBuffer, len, buf->gateRuns, buf->maxGateRuns, txMultiplySnap_); - if (nRuns == 0U) + const IR_TxPlan built = buildPhysicalTransmission( + sendBuffer, len, buf->gateRuns, buf->maxGateRuns, plan.carrierMultiply); + if (!built.valid() || built.physicalTicks != plan.physicalTicks || + built.gateRunCount != plan.gateRunCount) { + const IR_SendStatus failure = built.valid() ? IR_SendStatus::PlanMismatch : built.status; + isSending = false; txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; - return IR_SendStatus::BuildGateRunsFailed; + refreshBlindDecoderMuteState(); + finishTxOperation(operationId, failure); + return IR_SendResult(false, 0U, failure, operationId, + plan.airtimeUs, plan.clockBasis); } - - uint32_t total = 0; - for (size_t i = 0; i < nRuns; i++) - { - total += buf->gateRuns[i].lenTicks; - } - buf->totalTicks = total; + buf->totalTicks = plan.physicalTicks; const uint32_t setW = (uint32_t)mask; const uint32_t resetW = ((uint32_t)mask) << 16U; @@ -1025,17 +1274,17 @@ IR_SendStatus IR_Encoder::rawSend(uint8_t *ptr, uint8_t len) const uint16_t cap = maxPowerNumerator(); txPowerSnap_ = (powerNumerator_ > cap) ? cap : powerNumerator_; } - buf->wave.configure(setW, resetW, buf->gateRuns, nRuns, txMultiplySnap_, txPowerSnap_); + buf->wave.configure(setW, resetW, buf->gateRuns, + static_cast(built.gateRunCount), + plan.carrierMultiply, txPowerSnap_); buf->wave.fill(buf->bsrrWords, buf->wordCount); - isSending = true; - refreshBlindDecoderMuteState(); if (port != nullptr) - { port->BSRR = resetW; - } IR_Encoder::carrierResume(); - return IR_SendStatus::Success; + markTxArmed(operationId); + return IR_SendResult(true, plan.airtimeMsCeil(), IR_SendStatus::Success, + operationId, plan.airtimeUs, plan.clockBasis); } void IR_Encoder::isr() @@ -1054,7 +1303,16 @@ void IR_Encoder::_isr() return; if (port == nullptr) + { + const uint32_t operationId = txOperationId_; + isSending = false; + txUseBufferedIsr_ = false; + txActiveBufferedCtx_ = nullptr; + refreshBlindDecoderMuteState(); + finishTxOperation(operationId, IR_SendStatus::EncoderPinUnavailable); + carrierStopPending = true; return; + } if (!txUseBufferedIsr_) { @@ -1075,12 +1333,12 @@ void IR_Encoder::_isr() } } - legacyPhysCounter_++; - if (legacyPhysCounter_ < legacyPhysPerLogical_) + legacyScaleAccumulator_ += 2U; + if (legacyScaleAccumulator_ < txMultiplySnap_) { return; } - legacyPhysCounter_ = 0; + legacyScaleAccumulator_ -= txMultiplySnap_; TxFsmState st{}; loadTxFsmFromMembers(st); @@ -1089,11 +1347,13 @@ void IR_Encoder::_isr() if (!active) { + const uint32_t operationId = txOperationId_; port->BSRR = resetW; isSending = false; txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; refreshBlindDecoderMuteState(); + finishTxOperation(operationId, IR_SendStatus::Success); carrierStopPending = true; } return; @@ -1102,11 +1362,13 @@ void IR_Encoder::_isr() IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_; if (buf == nullptr || !buf->isValid()) { + const uint32_t operationId = txOperationId_; port->BSRR = ((uint32_t)mask) << 16U; isSending = false; txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; refreshBlindDecoderMuteState(); + finishTxOperation(operationId, IR_SendStatus::BufferedStorageInvalid); carrierStopPending = true; return; } @@ -1117,11 +1379,13 @@ void IR_Encoder::_isr() if (buf->ticksSent >= buf->totalTicks) { + const uint32_t operationId = txOperationId_; port->BSRR = ((uint32_t)mask) << 16U; isSending = false; txUseBufferedIsr_ = false; txActiveBufferedCtx_ = nullptr; refreshBlindDecoderMuteState(); + finishTxOperation(operationId, IR_SendStatus::Success); carrierStopPending = true; return; } @@ -1163,7 +1427,7 @@ void IR_Encoder::addSync(bool *prev, bool *next) *prev = !*prev; break; default: - for (int16_t i = 0; i < syncBits - 1; i++) + for (uint8_t i = 0; i < syncBits - 1U; i++) { *prev ? send_LOW() : send_HIGH(); *prev = !*prev; @@ -1183,26 +1447,13 @@ uint8_t IR_Encoder::bitLow[2] = { uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const { - // Расчет времени отправки пакета в миллисекундах - - // Время преамбулы: preambPulse * 2 фронта * bitTakts тактов - uint32_t preambTime = preambPulse * 2 * bitTakts; - - // Время данных: количество бит * bitTakts тактов - uint32_t dataTime = packSize * 8 * bitTakts; - - // Время синхронизации: syncBits * 2 фронта * bitTakts тактов - uint32_t syncTime = syncBits * 2 * bitTakts; - - // Общее время в тактах - uint32_t totalTakts = preambTime + dataTime + syncTime; - - // Конвертируем в миллисекунды - // carrierPeriod - период несущей в микросекундах - // totalTakts * carrierPeriod / 1000 = время в миллисекундах - uint32_t sendTimeMs = (totalTakts * carrierPeriod) / 1000; - - return sendTimeMs; + if (packSize == 0U || packSize > irproto::kMaxWireFrameBytes) + return 0U; + // Airtime is data-independent for the current PHY, but the source of + // truth remains the real FSM planner rather than a second size formula. + uint8_t frame[irproto::kMaxWireFrameBytes] = {0}; + const IR_TxPlan plan = planTransmission(frame, packSize); + return plan.valid() ? plan.airtimeMsCeil() : 0U; } // Функции для тестирования времени отправки без фактической отправки @@ -1219,7 +1470,11 @@ uint32_t IR_Encoder::testSendTime(uint16_t addrTo, uint8_t *data, uint8_t len, b uint32_t IR_Encoder::testSendTimeFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data, uint8_t len, bool needAccept) const { - if (len > bytePerPack) + (void)addrFrom; + (void)addrTo; + (void)data; + (void)needAccept; + if (len > irproto::kMaxDataPayloadBytes) { return 0; // Возвращаем 0 для недопустимого размера } @@ -1230,12 +1485,15 @@ uint32_t IR_Encoder::testSendTimeFULL(uint16_t addrFrom, uint16_t addrTo, uint8_ uint32_t IR_Encoder::testSendAccept(uint16_t addrTo, uint8_t customByte) const { + (void)addrTo; + (void)customByte; constexpr uint8_t packsize = msgBytes + addrBytes + 1U + crcBytes; return calculateSendTime(packsize); } uint32_t IR_Encoder::testSendRequest(uint16_t addrTo) const { + (void)addrTo; constexpr uint8_t packsize = msgBytes + addrBytes + addrBytes + crcBytes; return calculateSendTime(packsize); } @@ -1257,7 +1515,11 @@ uint32_t IR_Encoder::testSendBackTo(uint16_t addrTo, uint8_t *data, uint8_t len) uint32_t IR_Encoder::testSendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len) const { - if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO) + (void)addrTo; + (void)data; + const uint8_t payloadLimit = isAdressed ? irproto::kMaxBackToPayloadBytes + : irproto::kMaxBackPayloadBytes; + if (len > payloadLimit) { return 0; // Возвращаем 0 для недопустимого размера } diff --git a/IR_Encoder.h b/IR_Encoder.h index bb21d4b..2b32259 100644 --- a/IR_Encoder.h +++ b/IR_Encoder.h @@ -18,20 +18,88 @@ enum class IR_SendStatus : uint8_t { 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; // Флаг успешности отправки - uint32_t sendTimeMs; // Время отправки пакета в миллисекундах + 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) - : success(success), sendTimeMs(sendTimeMs), status(status) {} + 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; @@ -53,6 +121,12 @@ public: 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: @@ -111,14 +185,32 @@ public: /** 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); - /** Called by external TX backend on actual end of transmission. */ + /** 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(); @@ -132,6 +224,7 @@ public: 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); @@ -155,7 +248,11 @@ public: 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;} + 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(); @@ -170,6 +267,7 @@ private: 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); @@ -210,6 +308,15 @@ private: 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; @@ -218,9 +325,8 @@ private: uint16_t txPowerSnap_ = 1; uint16_t txMultiplySnap_ = 2; - /** Legacy: физических тиков на один логический шаг FSM = multiply/2. */ - uint16_t legacyPhysPerLogical_ = 1; - uint16_t legacyPhysCounter_ = 0; + /** 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; @@ -235,9 +341,27 @@ private: uint8_t decodersCount = 0; uint8_t sendLen = 0; - uint8_t sendBuffer[dataByteSizeMax]{0}; /// @brief Буффер данных для отправки + 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; diff --git a/IR_config.cpp b/IR_config.cpp index 9ddae25..9934375 100644 --- a/IR_config.cpp +++ b/IR_config.cpp @@ -30,4 +30,4 @@ uint8_t IR_FOX::crc8(uint8_t *data, uint8_t start, uint8_t end, uint8_t poly) } } return crc; -}; +} diff --git a/IR_config.h b/IR_config.h index 31f439e..e817ea2 100644 --- a/IR_config.h +++ b/IR_config.h @@ -15,8 +15,6 @@ constexpr size_t kDefaultDmaTxMaxStreams = 4U; constexpr uint32_t kDmaTxIrqPriority = 8U; /** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */ constexpr uint16_t kIsrTxBsrrWordCount = 256U; -/** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */ -constexpr size_t kIsrTxMaxGateRuns = 512U; static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even"); } @@ -101,7 +99,7 @@ msg type:                                 //  ----------                                 // | xxx..... | = тип сообщения (биты 7..5)                                 // | ...xxxxx | = полная длина кадра в байтах (5 бит, 0..31, IR_MASK_MSG_INFO), не «31 бит» и не отдельный лимит «24 байта» -                                // Полезная нагрузка в data pack: до bytePerPack байт (см. #define bytePerPack). + // Полезная нагрузка в data pack: до irproto::kMaxDataPayloadBytes байт.                                 //  ---------- */ #define IR_MSG_BACK 0U // | 000...... | = Задний сигнал машинки #define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение @@ -111,7 +109,7 @@ msg type: // #define IR_MSG_ 5U // | 101..... | = ?? #define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения #define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения -; /*   // ---------- + /*   // ---------- /``````````````````````````````` подтверждение `````````````````````````````\      /``````````````````````````````````````` запрос ``````````````````````````````````\                                                                                                                        @@ -159,15 +157,25 @@ msg type: */ -#define IR_MASK_MSG_TYPE 0b00000111 -#define IR_MASK_MSG_INFO 0b00011111 +namespace irproto { +/** Three high header bits, shifted down, encode the message type. */ +constexpr uint8_t kMessageTypeMask = 0x07U; +/** Five low header bits encode the complete on-wire frame length. */ +constexpr uint8_t kWireFrameLengthBits = 5U; +constexpr uint8_t kWireFrameLengthMask = + static_cast((1U << kWireFrameLengthBits) - 1U); +constexpr uint8_t kMaxWireFrameBytes = kWireFrameLengthMask; +} + +// Source-compatible aliases. New code should use the typed irproto constants. +#define IR_MASK_MSG_TYPE (::irproto::kMessageTypeMask) +#define IR_MASK_MSG_INFO (::irproto::kWireFrameLengthMask) /* /////////////////////////////////////////////////////////////////////////////////////*/ typedef uint16_t crc_t; // #define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает -#define bytePerPack (31) // колличество байтов в пакете #ifndef freeFrec #define freeFrec false #endif @@ -248,8 +256,6 @@ typedef uint16_t crc_t; #define poly2 0x8C #define syncBits 3U // количество битов синхронизации -#define dataByteSizeMax (msgBytes + addrBytes + addrBytes + bytePerPack + crcBytes) - #define preambFronts (preambPulse * 2) // количество фронтов преамбулы (Приём) #define preambToggle ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) // колличество переключений преамбулы (Передача) @@ -262,7 +268,123 @@ typedef uint16_t crc_t; #define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита -#define tolerance 300U +namespace irproto { +constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes; +constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes; +constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes; +constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes; +constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes; +constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes; + +/** RX timing geometry shared by adaptive and nominal decoder paths. */ +constexpr uint16_t kRxTimingToleranceUs = 300U; +constexpr uint8_t kRxInterEdgeTimeoutGuardBitWindows = 1U; +constexpr uint8_t kRxInterEdgeTimeoutBitWindows = + static_cast(bitPerByte + syncBits + kRxInterEdgeTimeoutGuardBitWindows); +constexpr uint8_t kRxSilenceTimeoutInterEdgeWindows = 2U; + +/** + * Largest accepted rise-to-rise interval for one decoder byte window. + * adaptiveBitPeriodUs is riseSyncTime when free-frequency tracking is used. + */ +constexpr uint32_t rxInterEdgeTimeoutUs(uint32_t adaptiveBitPeriodUs) +{ + return (adaptiveBitPeriodUs + static_cast(kRxTimingToleranceUs)) * + static_cast(kRxInterEdgeTimeoutBitWindows); +} + +/** Silence after which an unfinished RX candidate is retired. */ +constexpr uint32_t rxSilenceTimeoutUs(uint32_t adaptiveBitPeriodUs) +{ + return rxInterEdgeTimeoutUs(adaptiveBitPeriodUs) * + static_cast(kRxSilenceTimeoutInterEdgeWindows); +} + +constexpr uint32_t microsToMillisCeil(uint32_t us) +{ + return (us + 999U) / 1000U; +} + +constexpr uint32_t kNominalRxInterEdgeTimeoutUs = rxInterEdgeTimeoutUs(bitTime); +constexpr uint32_t kNominalRxSilenceTimeoutUs = rxSilenceTimeoutUs(bitTime); + +/** + * Deployed response/ACK turn-around policy. + * + * This is empirical, not a PHY invariant. Commit 1353ab6 replaced the older + * fixed 75 ms with a floating expression whose only reproducible result at the + * nominal PHY is 42 ms; no measurement or physical derivation was recorded. + * Keep the deployed value until a hardware gap campaign establishes a new + * channel-turn-around contract. + */ +constexpr uint16_t kDefaultResponseTurnaroundDelayMs = 42U; + +/** + * Conservative logical run bound: preamble transitions plus two gate runs for + * every data/sync bit. Physical splitting for unusually large multiply values + * is reported by IR_TxPlan::gateRunCount and may require custom storage. + */ +constexpr size_t kMaxLogicalGateRuns = + static_cast(preambPulse * 2U) + + static_cast(kMaxWireFrameBytes) * + static_cast((bitPerByte + syncBits) * 2U); +constexpr size_t kIsrTxMaxGateRuns = kMaxLogicalGateRuns; + +/** + * Compile-time PHY storage contract. + * + * Every data and sync bit occupies bitTakts * 2 ticks on the logical + * 2*carrierFrec clock, independently of its value. A physical gate run is + * stored in uint16_t and can therefore split at UINT16_MAX ticks. The bound + * below includes the worst possible number of such split pieces; applications + * can size fixed DMA/ISR storage from the protocol instead of duplicating a + * packet-size constant. + */ +constexpr uint32_t kPreambleLogicalTicks = + static_cast(preambPulse * 2U) * + static_cast(preambToggle + 1U); +constexpr uint32_t kEncodedBitLogicalTicks = + static_cast(bitTakts * 2U); +constexpr uint32_t kMaxLogicalTransmissionTicks = + kPreambleLogicalTicks + + static_cast(kMaxWireFrameBytes) * + static_cast(bitPerByte + syncBits) * + kEncodedBitLogicalTicks; + +constexpr uint16_t normalizedCarrierMultiply(uint16_t multiply) +{ + return multiply < 2U ? 2U : multiply; +} + +constexpr uint64_t maxPhysicalTransmissionTicks(uint16_t multiply) +{ + return (static_cast(kMaxLogicalTransmissionTicks) * + static_cast(normalizedCarrierMultiply(multiply)) + + 1U) / + 2U; +} + +constexpr size_t maxPhysicalGateRunCapacity(uint16_t multiply) +{ + return kMaxLogicalGateRuns + + static_cast(maxPhysicalTransmissionTicks(multiply) / + static_cast(UINT16_MAX)); +} + +static_assert(kMaxDataPayloadBytes == 24U, "IR DATA payload contract changed"); +static_assert(kMaxBackPayloadBytes == 26U, "IR BACK payload contract changed"); +static_assert(kNominalRxInterEdgeTimeoutUs == 15144U, "IR RX timeout contract changed"); +static_assert(kNominalRxSilenceTimeoutUs == 30288U, "IR RX silence contract changed"); +static_assert(kMaxLogicalTransmissionTicks <= UINT32_MAX, + "IR maximum transmission no longer fits IR_TxPlan"); +} + +// Deprecated source-compatible names. They are aliases only and no longer +// define independent storage/payload limits. bytePerPack historically meant +// 31; preserving that value avoids silently changing external sketches. +#define bytePerPack (::irproto::kMaxWireFrameBytes) +#define dataByteSizeMax (::irproto::kMaxWireFrameBytes) +#define IR_TIMING_TOLERANCE_US (::irproto::kRxTimingToleranceUs) constexpr uint16_t test_all_Time = bitTime; constexpr uint16_t test_all_Takts = bitTakts * 2; diff --git a/IrDmaTxStm32.h b/IrDmaTxStm32.h index 3ef0549..c94efb3 100644 --- a/IrDmaTxStm32.h +++ b/IrDmaTxStm32.h @@ -120,7 +120,23 @@ public: if (enc == nullptr) return IR_SendStatus::ExternalNoStream; for (uint8_t i = 0; i < streamCount_; i++) { if (streams_[i].enc == enc) { - return startStream(streams_[i], packet, len); + const IR_TxSnapshot snapshot = enc->txSnapshot(); + const IR_TxPlan plan = enc->planTransmission(packet, len); + return startStream(streams_[i], packet, len, plan, snapshot.operationId); + } + } + return IR_SendStatus::ExternalNoStream; + } + + IR_SendStatus startTracked(IR_Encoder* enc, + const uint8_t* packet, + uint8_t len, + const IR_TxPlan& plan, + uint32_t operationId) { + if (enc == nullptr) return IR_SendStatus::ExternalNoStream; + for (uint8_t i = 0; i < streamCount_; i++) { + if (streams_[i].enc == enc) { + return startStream(streams_[i], packet, len, plan, operationId); } } return IR_SendStatus::ExternalNoStream; @@ -192,6 +208,7 @@ private: uint32_t totalTicks = 0; volatile uint32_t ticksOutput = 0; + uint32_t operationId = 0; // Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока). uint32_t lastTicks = 0; @@ -204,20 +221,23 @@ private: ticksOutput = 0; totalTicks = 0; runCount = 0; + operationId = 0; } IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) { wave.fill(dst, count); } - void onHalf() { + void advanceHalf() { ticksOutput += halfLen; + } + + void refillFirstHalf() { fill(&dmaBuf[0], halfLen); __DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца } - void onComplete() { - ticksOutput += halfLen; + void refillSecondHalf() { fill(&dmaBuf[halfLen], halfLen); __DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца } @@ -262,7 +282,7 @@ private: void forceStop(TxStream& s) { HAL_NVIC_DisableIRQ(s.dmaIrq); if (s.active) { - stopStream(s); + stopStream(s, IR_SendStatus::DmaStalled); recoveries_++; } HAL_NVIC_EnableIRQ(s.dmaIrq); @@ -281,18 +301,22 @@ private: static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) { auto* s = streamFromDma(hdma); if (s == nullptr || !s->active) return; - s->onHalf(); + s->advanceHalf(); if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) { - s_instance->stopStream(*s); + s_instance->stopStream(*s, IR_SendStatus::Success); + } else { + s->refillFirstHalf(); } } static void dmaCpltCb(DMA_HandleTypeDef* hdma) { auto* s = streamFromDma(hdma); if (s == nullptr || !s->active) return; - s->onComplete(); + s->advanceHalf(); if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) { - s_instance->stopStream(*s); + s_instance->stopStream(*s, IR_SendStatus::Success); + } else { + s->refillSecondHalf(); } } @@ -302,7 +326,7 @@ private: s->onError(); if (s_instance != nullptr) { s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error - s_instance->stopStream(*s); + s_instance->stopStream(*s, IR_SendStatus::DmaTransferError); } } @@ -348,21 +372,29 @@ private: return true; } - IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) { + IR_SendStatus startStream(TxStream& s, + const uint8_t* packet, + uint8_t len, + const IR_TxPlan& expectedPlan, + uint32_t operationId) { if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig; if (s.active) return IR_SendStatus::EncoderBusy; + if (!expectedPlan.valid() || operationId == 0U) return IR_SendStatus::ExternalInvalidConfig; if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig; if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig; s.resetWave(); - const uint16_t mult = IR_Encoder::carrierMultiply(); - s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult); - if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed; - - uint32_t total = 0; - for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks; - s.totalTicks = total; + const uint16_t mult = expectedPlan.carrierMultiply; + const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission( + packet, len, s.runs, s.maxRuns, mult); + if (!built.valid()) return built.status; + if (built.physicalTicks != expectedPlan.physicalTicks || + built.gateRunCount != expectedPlan.gateRunCount) + return IR_SendStatus::PlanMismatch; + s.runCount = static_cast(built.gateRunCount); + s.totalTicks = built.physicalTicks; + s.operationId = operationId; uint16_t pwr = mult / 2U; if (s.enc != nullptr) { @@ -388,6 +420,7 @@ private: const uint32_t dst = u32ptr(&s.port->BSRR); if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) { s.active = false; + s.operationId = 0U; return IR_SendStatus::DmaStartFailed; } @@ -395,10 +428,12 @@ private: return IR_SendStatus::Success; } - void stopStream(TxStream& s) { + void stopStream(TxStream& s, IR_SendStatus terminalStatus) { if (!s.active) return; + const uint32_t operationId = s.operationId; s.active = false; + s.operationId = 0U; HAL_DMA_Abort_IT(&s.hdma); if (s.port != nullptr) { @@ -406,7 +441,7 @@ private: } if (s.enc != nullptr) { - s.enc->externalFinishSend(); + s.enc->externalFinishSend(operationId, terminalStatus); } // Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика. } diff --git a/IrInterruptGuard.h b/IrInterruptGuard.h new file mode 100644 index 0000000..028afa5 --- /dev/null +++ b/IrInterruptGuard.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#if defined(__AVR__) +#include +#include +#endif + +/** + * Nest-safe interrupt guard for the short ISR/main shared-state sections used + * by IR-protocol. Unlike a noInterrupts()/interrupts() pair it restores the + * previous state and therefore never enables interrupts from inside an ISR. + */ +class IrInterruptGuard final +{ +public: + IrInterruptGuard() + { +#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32) + state_ = __get_PRIMASK(); + __disable_irq(); +#elif defined(__AVR__) + state_ = SREG; + cli(); +#else + noInterrupts(); +#endif + } + + ~IrInterruptGuard() + { +#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32) + if ((state_ & 1U) == 0U) + __enable_irq(); +#elif defined(__AVR__) + SREG = static_cast(state_); +#else + interrupts(); +#endif + } + + IrInterruptGuard(const IrInterruptGuard&) = delete; + IrInterruptGuard& operator=(const IrInterruptGuard&) = delete; + +private: +#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32) || \ + defined(__AVR__) + uint32_t state_ = 0U; +#endif +}; diff --git a/PacketTypes.cpp b/PacketTypes.cpp index d1c9dc4..db7fd5c 100644 --- a/PacketTypes.cpp +++ b/PacketTypes.cpp @@ -2,12 +2,49 @@ namespace PacketTypes { - bool BasePack::checkAddress() { return true; }; - void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id) + uint8_t minimumPacketSize(uint8_t msgType) { + switch (msgType) + { + case IR_MSG_DATA_ACCEPT: + case IR_MSG_DATA_NOACCEPT: + case IR_MSG_BACK_TO: + case IR_MSG_REQUEST: + return uint8_t(msgBytes + addrBytes + addrBytes + crcBytes); + case IR_MSG_BACK: + return uint8_t(msgBytes + addrBytes + crcBytes); + case IR_MSG_ACCEPT: + return uint8_t(msgBytes + addrBytes + 1U + crcBytes); + default: + return 0; + } + } + + bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize) + { + const uint8_t minimum = minimumPacketSize(msgType); + return minimum != 0 && packSize >= minimum; + } + + bool BasePack::checkAddress() { return true; } + bool BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize) + { + isAvailable = false; + isRawAvailable = false; this->packInfo = packInfo; this->id = id; + if (packInfo == nullptr || packInfo->buffer == nullptr) + { + return false; + } + + const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE; + if (requireTypedSize && !isTypedPacketSizeValid(msgType, packInfo->packSize)) + { + return false; + } + if (checkAddress()) { isAvailable = true; @@ -23,29 +60,65 @@ namespace PacketTypes Serial.print(" NOT-OK "); #endif } + return isAvailable; } uint16_t BasePack::_getAddrFrom(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr || + obj->packInfo->packSize < crcBytes || + uint16_t(obj->addressFromOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes)) + { + return 0; + } return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1]; - }; + } uint16_t BasePack::_getAddrTo(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr || + obj->packInfo->packSize < crcBytes || + uint16_t(obj->addressToOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes)) + { + return 0; + } return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1]; - }; + } uint8_t BasePack::_getDataSize(BasePack *obj) { - return obj->packInfo->packSize - crcBytes - obj->DataOffset; - }; + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr) + { + return 0; + } + const uint16_t frameOverhead = uint16_t(crcBytes) + uint16_t(obj->DataOffset); + if (uint16_t(obj->packInfo->packSize) <= frameOverhead) + { + return 0; + } + return uint8_t(uint16_t(obj->packInfo->packSize) - frameOverhead); + } uint8_t *BasePack::_getDataPrt(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr || + obj->packInfo->packSize < crcBytes) + { + return nullptr; + } + const uint16_t dataEnd = uint16_t(obj->packInfo->packSize) - uint16_t(crcBytes); + if (uint16_t(obj->DataOffset) > dataEnd) + { + return nullptr; + } return obj->packInfo->buffer + obj->DataOffset; - }; + } uint8_t BasePack::_getDataRawSize(BasePack *obj) { + if (obj == nullptr || obj->packInfo == nullptr) + { + return 0; + } return obj->packInfo->packSize; - }; + } bool BasePack::available() { @@ -59,7 +132,7 @@ namespace PacketTypes { return false; } - }; + } bool BasePack::availableRaw() { if (isRawAvailable) @@ -71,7 +144,7 @@ namespace PacketTypes { return false; } - }; + } bool Data::checkAddress() { @@ -104,4 +177,4 @@ namespace PacketTypes IR_FOX::checkAddressRuleApply(getAddrTo(), this->id, ret); return ret; } -} \ No newline at end of file +} diff --git a/PacketTypes.h b/PacketTypes.h index 37e18a8..734913a 100644 --- a/PacketTypes.h +++ b/PacketTypes.h @@ -4,25 +4,34 @@ class IR_Decoder; namespace PacketTypes { + /** + * Minimum complete frame size (header, addresses/data required by the type, + * and CRC). Unknown/reserved message types return 0. + */ + uint8_t minimumPacketSize(uint8_t msgType); + + /** True only for a known typed packet whose complete frame is long enough. */ + bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize); + class BasePack { friend IR_Decoder; protected: - bool isAvailable; - bool isRawAvailable; - bool isNeedAccept; + bool isAvailable = false; + bool isRawAvailable = false; + bool isNeedAccept = false; - uint8_t msgOffset; - uint8_t addressFromOffset; - uint8_t addressToOffset; - uint8_t DataOffset; + uint8_t msgOffset = 0; + uint8_t addressFromOffset = 0; + uint8_t addressToOffset = 0; + uint8_t DataOffset = 0; - IR_FOX::PackInfo *packInfo; - uint16_t id; + IR_FOX::PackInfo *packInfo = nullptr; + uint16_t id = 0; virtual bool checkAddress(); - void set(IR_FOX::PackInfo *packInfo, uint16_t id); + bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true); static uint16_t _getAddrFrom(BasePack *obj); static uint16_t _getAddrTo(BasePack *obj); diff --git a/RingBuffer.h b/RingBuffer.h index 2ed56c5..f06b598 100644 --- a/RingBuffer.h +++ b/RingBuffer.h @@ -1,5 +1,5 @@ #pragma once -#include "Arduino.h" +#include "IrInterruptGuard.h" template class RingBuffer { public: @@ -15,43 +15,39 @@ public: bool push(T element) { bool pushed = false; - noInterrupts(); + IrInterruptGuard guard; if (!isFull()) { data[end] = element; end = (end + 1) % BufferSize; pushed = true; } - interrupts(); return pushed; } T* pop() { - noInterrupts(); + IrInterruptGuard guard; T* value = nullptr; if (!isEmpty()) { value = &data[start]; start = (start + 1) % BufferSize; } - interrupts(); return value; } // B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению. - // (T* pop() отдаёт указатель во внутренний слот; его внутренний interrupts() снимает внешнюю - // защиту вызывающего ДО чтения *ptr → торн-рид, если кольцо переполнится в этом окне.) + // (T* pop() отдаёт указатель во внутренний слот; после выхода слот снова может быть перезаписан.) bool pop(T &out) { bool popped = false; - noInterrupts(); + IrInterruptGuard guard; if (!isEmpty()) { out = data[start]; start = (start + 1) % BufferSize; popped = true; } - interrupts(); return popped; } private: T data[BufferSize]; unsigned int start, end; -}; \ No newline at end of file +}; diff --git a/test_examples/longData/longData.ino b/test_examples/longData/longData.ino index 9aca9e5..c7ff674 100644 --- a/test_examples/longData/longData.ino +++ b/test_examples/longData/longData.ino @@ -20,7 +20,7 @@ static constexpr uint16_t kIrDeviceAddr = 0; static constexpr uint8_t kCmdVersion = 0x5E; static constexpr uint32_t kSerialBaud = 115200; static constexpr uint32_t kSendPeriodMs = 500; -static constexpr uint8_t kMaxPayload = bytePerPack; +static constexpr uint8_t kMaxPayload = irproto::kMaxDataPayloadBytes; static constexpr uint8_t kMaxParamBytes = kMaxPayload - 1; static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr); @@ -30,15 +30,23 @@ static HardwareTimer irTimer(TIM17); namespace { constexpr size_t kIrDmaStreams = 1; constexpr uint16_t kIrDmaTxWordCount = 4096U; -constexpr size_t kIrDmaTxMaxGateRuns = 1024U; +// This example accepts the full uint8_t carrier-multiply configuration range. +constexpr uint16_t kIrDmaMaxCarrierMultiply = UINT8_MAX; +constexpr size_t kIrDmaTxMaxGateRuns = + irproto::maxPhysicalGateRunCapacity(kIrDmaMaxCarrierMultiply); static uint32_t s_irDmaWords[kIrDmaTxWordCount]; static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns]; } // namespace static IrDmaTxStm32 dmaBackend; static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); } -static bool txStart(void * /*ctx*/, IR_Encoder *e, const uint8_t *packet, uint8_t len) { - return dmaBackend.start(e, packet, len); +static IR_SendStatus txStart(void * /*ctx*/, + IR_Encoder *e, + const uint8_t *packet, + uint8_t len, + const IR_TxPlan& plan, + uint32_t operationId) { + return dmaBackend.startTracked(e, packet, len, plan, operationId); } #endif @@ -51,7 +59,11 @@ static bool s_sendLongerFrame = false; // 24 байта total: msg(1)+addr(2)+addr(2)+data(17)+crc(2), где data=0x5E + 16 ASCII. static const char kPayload16[] = "Car_v4.3.9_[12MH"; // 25 байт total: как выше, но data=0x5E + 17 ASCII. -static const char kPayload17[] = "Car_v4.3.9_[12MHz]_G491"; +static const char kPayload17[] = "Car_v4.3.9_[12MHz"; +static_assert(sizeof(kPayload16) - 1U == 16U, "24-byte frame fixture changed"); +static_assert(sizeof(kPayload17) - 1U == 17U, "25-byte frame fixture changed"); +static_assert(kMaxParamBytes == irproto::kMaxDataPayloadBytes - 1U, + "longData command parameter capacity must follow the DATA wire contract"); static void rebuildIrPayload() { s_irPayload[0] = kCmdVersion; @@ -110,7 +122,7 @@ void setup() { Serial.println(F("[IR_DMA] init FAILED")); return; } - IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr); + IR_Encoder::setExternalTxBackendV2(txStart, txBusy, nullptr); #elif LONGDATA_LEGACY_ISR IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0); #else diff --git a/tests/arduino_stubs/Arduino.h b/tests/arduino_stubs/Arduino.h new file mode 100644 index 0000000..08422b5 --- /dev/null +++ b/tests/arduino_stubs/Arduino.h @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include + +struct GPIO_TypeDef +{ + uint32_t BSRR = 0; + uint32_t IDR = 0; +}; + +class __FlashStringHelper; +#define F(value) (reinterpret_cast(value)) + +class Print +{ +public: + size_t print(const __FlashStringHelper *value) + { + return append(reinterpret_cast(value)); + } + + size_t print(const char *value) { return append(value); } + + size_t print(char value) + { + buffer_.push_back(value); + return 1U; + } + + template + size_t print(T value) + { + return append(std::to_string(value).c_str()); + } + + size_t println() + { + buffer_.push_back('\n'); + return 1U; + } + + size_t write(uint8_t value) + { + buffer_.push_back(static_cast(value)); + return 1U; + } + + const std::string &str() const { return buffer_; } + void clear() { buffer_.clear(); } + +private: + size_t append(const char *value) + { + if (value == nullptr) + return 0U; + const size_t oldSize = buffer_.size(); + buffer_ += value; + return buffer_.size() - oldSize; + } + + std::string buffer_; +}; + +using IRQn_Type = int; +enum TimerFormat_t : uint8_t { TICK_FORMAT = 0, MICROSEC_FORMAT, HERTZ_FORMAT }; + +constexpr uint8_t LOW = 0; +constexpr uint8_t HIGH = 1; +constexpr uint8_t INPUT = 0; +constexpr uint8_t OUTPUT = 1; + +class HardwareTimer +{ +public: + void pause() {} + void resume() {} + void setOverflow(uint32_t value, TimerFormat_t format = TICK_FORMAT) + { + if (format == HERTZ_FORMAT && value != 0U) + { + prescale_ = 1U; + overflow_ = timerClockHz_ / value; + if (overflow_ == 0U) overflow_ = 1U; + } + else + { + overflow_ = value == 0U ? 1U : value; + } + } + uint32_t getOverflow(TimerFormat_t = TICK_FORMAT) { return overflow_; } + uint32_t getPrescaleFactor() { return prescale_; } + uint32_t getTimerClkFreq() { return timerClockHz_; } + void attachInterrupt(uint8_t, void (*)()) {} + + uint32_t timerClockHz_ = 12000000U; + uint32_t prescale_ = 1U; + uint32_t overflow_ = 1U; +}; + +inline GPIO_TypeDef *digitalPinToPort(uint8_t) +{ + return nullptr; +} + +inline uint16_t digitalPinToBitMask(uint8_t) +{ + return 0; +} + +inline void pinMode(uint8_t, uint8_t) {} +inline void digitalWrite(uint8_t, uint8_t) {} +inline void NVIC_SetPriority(IRQn_Type, uint8_t) {} +inline void noInterrupts() {} +inline void interrupts() {} + +struct ArduinoSerialStub +{ + template void print(const T&) {} + template void println(const T&) {} + void println() {} +}; + +inline ArduinoSerialStub Serial; + +inline uint32_t arduino_stub_micros = 0U; + +inline unsigned long millis() +{ + return arduino_stub_micros / 1000U; +} + +inline unsigned long micros() { return arduino_stub_micros; } diff --git a/tests/test_packet_types.cpp b/tests/test_packet_types.cpp new file mode 100644 index 0000000..fce33a8 --- /dev/null +++ b/tests/test_packet_types.cpp @@ -0,0 +1,148 @@ +#include "PacketTypes.h" + +#include +#include +#include + +namespace +{ +template +class ExposedPacket : public Packet +{ +public: + bool attach(IR_FOX::PackInfo *info, uint16_t id = 0, bool requireTypedSize = true) + { + return this->set(info, id, requireTypedSize); + } +}; + +IR_FOX::PackInfo frame(uint8_t *buffer, uint8_t msgType, uint8_t size) +{ + buffer[0] = uint8_t((msgType << 5) | (size & IR_MASK_MSG_INFO)); + IR_FOX::PackInfo info; + info.buffer = buffer; + info.packSize = size; + return info; +} + +template +void checkTypedBoundary(uint8_t msgType, uint8_t minimum) +{ + uint8_t buffer[irproto::kMaxWireFrameBytes] = {}; + ExposedPacket packet; + + IR_FOX::PackInfo shortInfo = frame(buffer, msgType, uint8_t(minimum - 1U)); + assert(!packet.attach(&shortInfo)); + assert(!packet.available()); + assert(!packet.availableRaw()); + + IR_FOX::PackInfo minimumInfo = frame(buffer, msgType, minimum); + assert(packet.attach(&minimumInfo)); + assert(packet.available()); +} + +void testMinimumSizes() +{ + struct Case + { + uint8_t msgType; + uint8_t minimum; + }; + + const Case cases[] = { + {IR_MSG_DATA_ACCEPT, 7}, + {IR_MSG_DATA_NOACCEPT, 7}, + {IR_MSG_BACK, 5}, + {IR_MSG_BACK_TO, 7}, + {IR_MSG_REQUEST, 7}, + {IR_MSG_ACCEPT, 6}, + }; + + for (const Case &item : cases) + { + assert(PacketTypes::minimumPacketSize(item.msgType) == item.minimum); + assert(!PacketTypes::isTypedPacketSizeValid(item.msgType, uint8_t(item.minimum - 1U))); + assert(PacketTypes::isTypedPacketSizeValid(item.msgType, item.minimum)); + assert(PacketTypes::isTypedPacketSizeValid(item.msgType, uint8_t(item.minimum + 1U))); + } + + assert(PacketTypes::minimumPacketSize(3) == 0); + assert(PacketTypes::minimumPacketSize(5) == 0); + assert(!PacketTypes::isTypedPacketSizeValid(3, 31)); + assert(!PacketTypes::isTypedPacketSizeValid(5, 31)); + + checkTypedBoundary(IR_MSG_DATA_ACCEPT, 7); + checkTypedBoundary(IR_MSG_DATA_NOACCEPT, 7); + checkTypedBoundary(IR_MSG_BACK, 5); + checkTypedBoundary(IR_MSG_BACK_TO, 7); + checkTypedBoundary(IR_MSG_REQUEST, 7); + checkTypedBoundary(IR_MSG_ACCEPT, 6); +} + +void testPayloadAccessSaturates() +{ + uint8_t buffer[irproto::kMaxWireFrameBytes] = {}; + ExposedPacket packet; + + for (uint8_t size = 0; size < 7; ++size) + { + IR_FOX::PackInfo tooShort = frame(buffer, IR_MSG_DATA_ACCEPT, size); + assert(!packet.attach(&tooShort)); + assert(packet.getDataSize() == 0); + assert(packet.getDataPrt() == nullptr); + } + + IR_FOX::PackInfo emptyPayload = frame(buffer, IR_MSG_DATA_ACCEPT, 7); + assert(packet.attach(&emptyPayload)); + assert(packet.getDataSize() == 0); + assert(packet.getDataPrt() == buffer + 5); + + IR_FOX::PackInfo oneBytePayload = frame(buffer, IR_MSG_DATA_ACCEPT, 8); + assert(packet.attach(&oneBytePayload)); + assert(packet.getDataSize() == 1); + assert(packet.getDataPrt() == buffer + 5); + + IR_FOX::PackInfo nullBuffer; + nullBuffer.packSize = 31; + assert(!packet.attach(&nullBuffer)); + assert(packet.getDataSize() == 0); + assert(packet.getDataPrt() == nullptr); +} + +void testBackPayloadOffsets() +{ + uint8_t buffer[irproto::kMaxWireFrameBytes] = {}; + ExposedPacket packet; + + IR_FOX::PackInfo addressed = frame(buffer, IR_MSG_BACK_TO, 7); + assert(packet.attach(&addressed)); + assert(packet.getDataSize() == 0); + assert(packet.getDataPrt() == buffer + 5); + + IR_FOX::PackInfo broadcast = frame(buffer, IR_MSG_BACK, 5); + assert(packet.attach(&broadcast)); + assert(packet.getDataSize() == 0); + assert(packet.getDataPrt() == buffer + 3); +} + +void testRawContractIsIndependent() +{ + uint8_t buffer[irproto::kMaxWireFrameBytes] = {}; + ExposedPacket raw; + IR_FOX::PackInfo info = frame(buffer, IR_MSG_DATA_ACCEPT, 3); + + assert(raw.attach(&info, 0, false)); + assert(raw.availableRaw()); + assert(raw.getDataRawSize() == 3); +} +} // namespace + +int main() +{ + testMinimumSizes(); + testPayloadAccessSaturates(); + testBackPayloadOffsets(); + testRawContractIsIndependent(); + std::cout << "packet type boundary tests: OK\n"; + return 0; +} diff --git a/tests/test_protocol_contract.cpp b/tests/test_protocol_contract.cpp new file mode 100644 index 0000000..69f4c75 --- /dev/null +++ b/tests/test_protocol_contract.cpp @@ -0,0 +1,69 @@ +#include "IR_DecoderRaw.h" + +#include +#include +#include + +namespace { + +// Reproduces the removed 2025 expression exactly, but with integer arithmetic: +// 2.7735 == 27735 / 10000. It is a provenance golden, not a new PHY rule. +constexpr uint32_t removedLegacyResponseExpressionMs() +{ + const uint64_t scaledUs = + static_cast(irproto::kNominalRxInterEdgeTimeoutUs) * 27735U / 10000U; + return static_cast(scaledUs) / 1000U; +} + +static_assert(irproto::kWireFrameLengthBits == 5U, "wire length field changed"); +static_assert(irproto::kWireFrameLengthMask == 31U, "wire length mask changed"); +static_assert(irproto::kMaxWireFrameBytes == 31U, "wire frame limit changed"); +static_assert(irproto::kDataFrameOverheadBytes == 7U, "DATA overhead changed"); +static_assert(irproto::kBackFrameOverheadBytes == 5U, "BACK overhead changed"); +static_assert(irproto::kBackToFrameOverheadBytes == 7U, "BACK_TO overhead changed"); +static_assert(irproto::kMaxDataPayloadBytes == 24U, "DATA payload limit changed"); +static_assert(irproto::kMaxBackPayloadBytes == 26U, "BACK payload limit changed"); +static_assert(irproto::kMaxBackToPayloadBytes == 24U, "BACK_TO payload limit changed"); +static_assert(irproto::kMaxLogicalGateRuns == 688U, "logical max-frame run bound changed"); +static_assert(irproto::maxPhysicalGateRunCapacity(UINT8_MAX) == 738U, + "uint8 carrier-multiply storage bound changed"); + +static_assert(IR_MASK_MSG_TYPE == irproto::kMessageTypeMask, "legacy type mask diverged"); +static_assert(IR_MASK_MSG_INFO == irproto::kWireFrameLengthMask, "legacy length mask diverged"); +static_assert(bytePerPack == irproto::kMaxWireFrameBytes, + "legacy bytePerPack value must remain source-compatible"); +static_assert(dataByteSizeMax == irproto::kMaxWireFrameBytes, + "legacy storage alias must follow the wire limit"); + +static_assert(irproto::kRxInterEdgeTimeoutBitWindows == 12U, + "8 data + 3 sync + 1 guard geometry changed"); +static_assert(irproto::kNominalRxInterEdgeTimeoutUs == 15144U, + "nominal inter-edge timeout changed"); +static_assert(irproto::kNominalRxSilenceTimeoutUs == 30288U, + "nominal RX silence timeout changed"); +static_assert(irproto::microsToMillisCeil(irproto::kNominalRxSilenceTimeoutUs) == 31U, + "RX silence ceil-ms conversion changed"); +static_assert(IR_ResponseDelay == 42U, "deployed response turn-around changed"); +static_assert(removedLegacyResponseExpressionMs() == IR_ResponseDelay, + "named empirical response delay no longer matches its legacy provenance"); + +void testAdaptiveTimingGeometry() +{ + assert(irproto::rxInterEdgeTimeoutUs(700U) == 12000U); + assert(irproto::rxSilenceTimeoutUs(700U) == 24000U); + assert(irproto::rxInterEdgeTimeoutUs(1000U) == 15600U); + assert(irproto::rxSilenceTimeoutUs(1000U) == 31200U); + assert(irproto::microsToMillisCeil(0U) == 0U); + assert(irproto::microsToMillisCeil(1U) == 1U); + assert(irproto::microsToMillisCeil(1000U) == 1U); + assert(irproto::microsToMillisCeil(1001U) == 2U); +} + +} // namespace + +int main() +{ + testAdaptiveTimingGeometry(); + std::cout << "IR protocol geometry contract tests: OK\n"; + return 0; +} diff --git a/tests/test_rx_reason_contract.cpp b/tests/test_rx_reason_contract.cpp new file mode 100644 index 0000000..5bdeba8 --- /dev/null +++ b/tests/test_rx_reason_contract.cpp @@ -0,0 +1,79 @@ +#include "IR_config.h" +#include "RingBuffer.h" + +#include +#include +#include +#include + +// Test-only visibility: exercise the private reason enum and logging bound without +// widening the production API. Dependencies are included first so this macro +// cannot rewrite access specifiers in the standard library. +#define private public +#include "IR_DecoderRaw.h" +#undef private + +namespace { + +constexpr const char kZeroStats[] = + "RXSTAT,MUTEB=0,MUTEE=0,QRAW=0,QFLT=0,HOLD=0,GLITCH=0,TIME=0," + "PREAMB=0,SYNC=0,BUF=0,TIMEOUT=0,CRC=0,OK=0\n"; + +constexpr const char kOneEachStats[] = + "RXSTAT,MUTEB=1,MUTEE=1,QRAW=1,QFLT=1,HOLD=1,GLITCH=1,TIME=1," + "PREAMB=1,SYNC=1,BUF=1,TIMEOUT=1,CRC=1,OK=1\n"; + +static_assert(IR_DecoderRaw::rxReasonCounterCount() > 0U, + "RX reason counter storage must not be empty"); +static_assert(IR_DecoderRaw::rxReasonCounterCount() == + static_cast(IR_DecoderRaw::RxBriefReason::Count), + "public RX reason count must follow the enum sentinel"); +static_assert(static_cast(IR_DecoderRaw::RxBriefReason::Count) == + static_cast(IR_DecoderRaw::RxBriefReason::Ok) + 1U, + "RX reason Count must remain one past the final reason"); +static_assert(std::extent::value == + IR_DecoderRaw::rxReasonCounterCount(), + "RX reason counter array must follow the enum-derived count"); + +void testStatsWireFormatAndClearCoverage() +{ + IR_DecoderRaw decoder(0U, 0U); + Print out; + + decoder.printRxReasonStats(out); + assert(out.str() == kZeroStats); + + const uint8_t first = static_cast(IR_DecoderRaw::RxBriefReason::MuteBegin); + const uint8_t count = IR_DecoderRaw::rxReasonCounterCount(); + for (uint8_t i = first; i < count; ++i) + decoder.rxBriefLog(static_cast(i)); + + // The sentinel is a bound, not a loggable reason. + decoder.rxBriefLog(IR_DecoderRaw::RxBriefReason::Count); + + const uint16_t *const counters = decoder.rxReasonCounters(); + assert(counters[0] == 0U); + for (uint8_t i = first; i < count; ++i) + assert(counters[i] == 1U); + + out.clear(); + decoder.printRxReasonStats(out); + assert(out.str() == kOneEachStats); + + decoder.rxReasonCountersClear(); + for (uint8_t i = 0U; i < count; ++i) + assert(counters[i] == 0U); + + out.clear(); + decoder.printRxReasonStats(out); + assert(out.str() == kZeroStats); +} + +} // namespace + +int main() +{ + testStatsWireFormatAndClearCoverage(); + std::cout << "RX reason counter/tag contract tests: OK\n"; + return 0; +} diff --git a/tests/test_tx_contract.cpp b/tests/test_tx_contract.cpp new file mode 100644 index 0000000..3260436 --- /dev/null +++ b/tests/test_tx_contract.cpp @@ -0,0 +1,328 @@ +#include "IR_Encoder.h" +#include "IR_DecoderRaw.h" + +#include +#include +#include +#include + +// Link-only seams for planner/lifecycle host tests. The real implementations +// are irrelevant here; no decoder or legacy sendByte helper is exercised. +bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *) { return true; } +void IR_DecoderRaw::refreshPairMuteState() {} +void IR_Encoder::send_HIGH(bool) {} +void IR_Encoder::send_LOW() {} +void IR_Encoder::send_EMPTY(uint8_t) {} + +namespace +{ +constexpr size_t kRunCapacity = 2048U; +constexpr size_t kFullMultiplyRunCapacity = + irproto::maxPhysicalGateRunCapacity(UINT16_MAX); + +static_assert(irproto::kMaxLogicalTransmissionTicks == 25822U, + "golden maximum PHY duration changed"); +static_assert(irproto::maxPhysicalTransmissionTicks(2U) == 25822U, + "nominal physical tick conversion changed"); +static_assert(irproto::maxPhysicalGateRunCapacity(UINT8_MAX) <= 1024U, + "uint8_t carrier-multiply domain no longer fits the legacy Car allocation"); + +uint32_t sumTicks(const IrTxGateRun *runs, uint32_t count) +{ + uint32_t total = 0U; + for (uint32_t i = 0; i < count; ++i) + total += runs[i].lenTicks; + return total; +} + +void fillPattern(uint8_t *frame, uint8_t len, uint8_t pattern) +{ + for (uint8_t i = 0; i < len; ++i) + { + switch (pattern) + { + case 0: frame[i] = 0x00U; break; + case 1: frame[i] = 0xFFU; break; + case 2: frame[i] = (i & 1U) ? 0x55U : 0xAAU; break; + default: frame[i] = static_cast(i * 73U + 19U); break; + } + } +} + +void testPlannerMatchesBuiltStream() +{ + std::array frame{}; + std::array runs{}; + const uint16_t multiplies[] = {2U, 3U, 6U}; + + for (uint16_t multiply : multiplies) + { + for (uint8_t len = 1U; len <= irproto::kMaxWireFrameBytes; ++len) + { + uint32_t durationForLength = 0U; + uint32_t ticksForLength = 0U; + for (uint8_t pattern = 0U; pattern < 4U; ++pattern) + { + fillPattern(frame.data(), len, pattern); + const IR_TxPlan planned = + IR_Encoder::planPhysicalTransmission(frame.data(), len, multiply); + const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission( + frame.data(), len, runs.data(), runs.size(), multiply); + assert(planned.valid()); + assert(built.valid()); + assert(planned.physicalTicks == built.physicalTicks); + assert(planned.gateRunCount == built.gateRunCount); + assert(planned.airtimeUs == built.airtimeUs); + assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks); + if (pattern == 0U) + { + durationForLength = planned.airtimeUs; + ticksForLength = planned.physicalTicks; + } + else + { + assert(planned.airtimeUs == durationForLength); + assert(planned.physicalTicks == ticksForLength); + } + } + } + } +} + +void testGoldenNominalTimings() +{ + std::array frame{}; + struct Golden { uint8_t bytes; uint32_t logicalTicks; uint32_t usCeil; }; + const Golden golden[] = { + {6U, 5472U, 72000U}, + {10U, 8728U, 114843U}, + {31U, 25822U, 339764U}, + }; + for (const Golden& item : golden) + { + const IR_TxPlan plan = + IR_Encoder::planPhysicalTransmission(frame.data(), item.bytes, 2U); + assert(plan.valid()); + assert(plan.physicalTicks == item.logicalTicks); + assert(plan.airtimeUs == item.usCeil); + assert(plan.airtimeMsCeil() == (item.usCeil + 999U) / 1000U); + } +} + +void testCapacityAndClockContracts() +{ + std::array frame{}; + std::array logicalRuns{}; + fillPattern(frame.data(), frame.size(), 2U); + assert(IR_Encoder::buildGateRuns( + frame.data(), static_cast(frame.size()), + logicalRuns.data(), logicalRuns.size()) != 0U); + + std::array oversizedFrame{}; + assert(IR_Encoder::buildGateRuns( + oversizedFrame.data(), static_cast(oversizedFrame.size()), + logicalRuns.data(), logicalRuns.size()) == 0U); + + const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission( + frame.data(), static_cast(frame.size()), 6U); + assert(planned.valid()); + assert(planned.gateRunCount <= irproto::kIsrTxMaxGateRuns); + + IrTxGateRun oneRun{}; + const IR_TxPlan tooSmall = IR_Encoder::buildPhysicalTransmission( + frame.data(), static_cast(frame.size()), &oneRun, 1U, 6U); + assert(!tooSmall.valid()); + assert(tooSmall.status == IR_SendStatus::BuildGateRunsFailed); + assert(tooSmall.gateRunCount == planned.gateRunCount); + assert(tooSmall.physicalTicks == planned.physicalTicks); + + std::array tenBytes{}; + IR_TxPlan configured = IR_Encoder::planPhysicalTransmission( + tenBytes.data(), static_cast(tenBytes.size()), 6U); + assert(configured.airtimeUs == 114843U); + assert(IR_Encoder::applyTickClock(configured, 12000000U, 52U)); + assert(configured.clockBasis == IR_TxClockBasis::ConfiguredTimer); + assert(configured.airtimeUs == 113464U); + + assert(!IR_Encoder::planPhysicalTransmission(nullptr, 1U, 2U).valid()); + assert(!IR_Encoder::planPhysicalTransmission(frame.data(), 0U, 2U).valid()); + assert(!IR_Encoder::planPhysicalTransmission( + frame.data(), static_cast(irproto::kMaxWireFrameBytes + 1U), 2U).valid()); +} + +void testDerivedFixedStorageCapacity() +{ + std::array frame{}; + std::array runs{}; + const uint16_t multiplies[] = {2U, 3U, 6U, UINT8_MAX, UINT16_MAX}; + + for (uint16_t multiply : multiplies) + { + const size_t capacity = irproto::maxPhysicalGateRunCapacity(multiply); + for (uint8_t pattern = 0U; pattern < 4U; ++pattern) + { + fillPattern(frame.data(), static_cast(frame.size()), pattern); + const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission( + frame.data(), static_cast(frame.size()), multiply); + const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission( + frame.data(), static_cast(frame.size()), + runs.data(), capacity, multiply); + assert(planned.valid()); + assert(built.valid()); + assert(built.gateRunCount <= capacity); + assert(built.gateRunCount == planned.gateRunCount); + assert(built.physicalTicks == planned.physicalTicks); + assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks); + } + } +} + +void testInPlacePhysicalScaling() +{ + std::array frame{}; + std::array logical{}; + std::array expected{}; + fillPattern(frame.data(), static_cast(frame.size()), 3U); + + const size_t logicalCount = IR_Encoder::buildGateRuns( + frame.data(), static_cast(frame.size()), + logical.data(), logical.size()); + assert(logicalCount != 0U); + + for (uint16_t multiply : {2U, 3U, 6U}) + { + auto scaled = logical; + size_t scaledCount = logicalCount; + const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission( + frame.data(), static_cast(frame.size()), + expected.data(), expected.size(), multiply); + assert(built.valid()); + assert(IR_Encoder::scaleGateRunsToPhysical( + scaled.data(), &scaledCount, scaled.size(), multiply)); + assert(scaledCount == built.gateRunCount); + for (size_t i = 0; i < scaledCount; ++i) + { + assert(scaled[i].gate == expected[i].gate); + assert(scaled[i].lenTicks == expected[i].lenTicks); + } + } + + // Expansion beyond uint16_t is also in-place and preserves chunk order. + std::array longRun{}; + longRun[0] = {65535U, true}; + size_t longCount = 1U; + assert(IR_Encoder::scaleGateRunsToPhysical( + longRun.data(), &longCount, longRun.size(), 7U)); + assert(longCount == 4U); + assert(longRun[0].lenTicks == 65535U); + assert(longRun[1].lenTicks == 65535U); + assert(longRun[2].lenTicks == 65535U); + assert(longRun[3].lenTicks == 32768U); + assert(sumTicks(longRun.data(), static_cast(longCount)) == 229373U); + + std::array tooSmall{{{65535U, true}, {1U, false}}}; + size_t tooSmallCount = 1U; + assert(!IR_Encoder::scaleGateRunsToPhysical( + tooSmall.data(), &tooSmallCount, tooSmall.size(), 6U)); + assert(tooSmallCount == 1U); +} + +struct FakeBackend +{ + IR_SendStatus startStatus = IR_SendStatus::Success; + bool finishSynchronously = false; + IR_Encoder *encoder = nullptr; + uint32_t operationId = 0U; + IR_TxPlan plan{}; +}; + +IR_SendStatus fakeStart(void *opaque, + IR_Encoder *encoder, + const uint8_t *, + uint8_t, + const IR_TxPlan& plan, + uint32_t operationId) +{ + auto& backend = *static_cast(opaque); + backend.encoder = encoder; + backend.operationId = operationId; + backend.plan = plan; + if (backend.startStatus == IR_SendStatus::Success && backend.finishSynchronously) + encoder->externalFinishSend(operationId, IR_SendStatus::Success); + return backend.startStatus; +} + +void testTokenLifecycle() +{ + FakeBackend backend; + IR_Encoder::setExternalTxBackendV2(fakeStart, nullptr, &backend); + IR_Encoder encoder(1U, 42U, nullptr, false); + uint8_t payload = 0x5EU; + + arduino_stub_micros = 100U; + const IR_SendResult first = encoder.sendData(IR_Broadcast, &payload, 1U); + assert(first.success); + assert(first.operationId != 0U); + assert(first.plannedAirtimeUs == backend.plan.airtimeUs); + assert(encoder.isBusy()); + IR_TxSnapshot snapshot = encoder.txSnapshot(); + assert(snapshot.operationId == first.operationId); + assert(snapshot.state == IR_TxState::Transmitting); + + encoder.externalFinishSend(first.operationId + 1U, IR_SendStatus::Success); + assert(encoder.isBusy()); + arduino_stub_micros = 200U; + encoder.externalFinishSend(first.operationId, IR_SendStatus::DmaTransferError); + snapshot = encoder.txSnapshot(); + assert(!encoder.isBusy()); + assert(snapshot.state == IR_TxState::Failed); + assert(snapshot.status == IR_SendStatus::DmaTransferError); + assert(snapshot.terminalAtUs == 200U); + encoder.externalFinishSend(first.operationId, IR_SendStatus::Success); + assert(encoder.txSnapshot().status == IR_SendStatus::DmaTransferError); + + arduino_stub_micros = 300U; + const IR_SendResult second = encoder.sendData(IR_Broadcast, &payload, 1U); + assert(second.success && second.operationId != first.operationId); + encoder.externalFinishSend(first.operationId, IR_SendStatus::Success); + assert(encoder.isBusy()); + encoder.externalFinishSend(second.operationId, IR_SendStatus::Success); + assert(encoder.isOperationComplete(second.operationId)); + + backend.startStatus = IR_SendStatus::DmaStartFailed; + const IR_SendResult rejectedAfterOwnership = encoder.sendData(IR_Broadcast, &payload, 1U); + assert(!rejectedAfterOwnership.success); + assert(rejectedAfterOwnership.operationId != 0U); + snapshot = encoder.txSnapshot(); + assert(snapshot.state == IR_TxState::Failed); + assert(snapshot.status == IR_SendStatus::DmaStartFailed); + + backend.startStatus = IR_SendStatus::Success; + const IR_SendResult active = encoder.sendData(IR_Broadcast, &payload, 1U); + const IR_SendResult busy = encoder.sendData(IR_Broadcast, &payload, 1U); + assert(active.success); + assert(!busy.success && busy.status == IR_SendStatus::EncoderBusy); + assert(busy.operationId == 0U); + encoder.externalFinishSend(active.operationId, IR_SendStatus::Success); + + backend.finishSynchronously = true; + const IR_SendResult synchronous = encoder.sendData(IR_Broadcast, &payload, 1U); + assert(synchronous.success); + assert(encoder.isOperationComplete(synchronous.operationId)); + assert(!encoder.isBusy()); + + IR_Encoder::setExternalTxBackendV2(nullptr, nullptr, nullptr); +} +} // namespace + +int main() +{ + testPlannerMatchesBuiltStream(); + testGoldenNominalTimings(); + testCapacityAndClockContracts(); + testDerivedFixedStorageCapacity(); + testInPlacePhysicalScaling(); + testTokenLifecycle(); + std::cout << "IR TX contract tests: OK\n"; + return 0; +}