1 Commits

Author SHA1 Message Date
57db9c35b8 archive: freeze IR-protocol WIP before stepwise integration 2026-08-28 13:27:46 +03:00
18 changed files with 1848 additions and 391 deletions

View File

@ -162,7 +162,9 @@ void IR_Decoder::_tick()
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast) if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
isWaitingAcceptSend = true; 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) if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
{ {

View File

@ -1,5 +1,6 @@
#include "IR_DecoderRaw.h" #include "IR_DecoderRaw.h"
#include "IR_Encoder.h" #include "IR_Encoder.h"
#include "IrInterruptGuard.h"
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@ -53,9 +54,8 @@ IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPa
bool IR_DecoderRaw::isSubOverflow() bool IR_DecoderRaw::isSubOverflow()
{ {
noInterrupts(); IrInterruptGuard guard;
volatile bool ret = isSubBufferOverflow; const bool ret = isSubBufferOverflow;
interrupts();
return ret; return ret;
} }
@ -103,7 +103,7 @@ void IR_DecoderRaw::refreshPairMuteState()
++active; ++active;
} }
const uint32_t nowUs = micros(); const uint32_t nowUs = micros();
noInterrupts(); IrInterruptGuard guard;
const bool wasActive = (isPairSending != 0); const bool wasActive = (isPairSending != 0);
isPairSending = active; isPairSending = active;
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
@ -121,7 +121,6 @@ void IR_DecoderRaw::refreshPairMuteState()
rxBriefMuteBlockedEdges = 0; rxBriefMuteBlockedEdges = 0;
} }
#endif #endif
interrupts();
} }
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
@ -142,6 +141,7 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
case RxBriefReason::Timeout: return F("TIMEOUT"); case RxBriefReason::Timeout: return F("TIMEOUT");
case RxBriefReason::Crc: return F("CRC"); case RxBriefReason::Crc: return F("CRC");
case RxBriefReason::Ok: return F("OK"); case RxBriefReason::Ok: return F("OK");
case RxBriefReason::Count: return F("UNK");
default: 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) void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
{ {
const uint8_t ri = (uint8_t)reason; const uint8_t ri = (uint8_t)reason;
if (ri < 14U) if (ri < kRxBriefReasonCount)
rxReasonCnt[ri]++; rxReasonCnt[ri]++;
#if !IR_RX_BRIEF_LOG #if !IR_RX_BRIEF_LOG
(void)a; (void)b; (void)tUs; (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); Serial.print(b);
} }
break; break;
case RxBriefReason::Count:
break;
} }
Serial.println(); Serial.println();
#endif // IR_RX_BRIEF_LOG (печать) #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 void IR_DecoderRaw::printRxReasonStats(Print &out) const
{ {
static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD", static const char *const kTags[] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF", "GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
"TIMEOUT", "CRC", "OK"}; "TIMEOUT", "CRC", "OK"};
static_assert(sizeof(kTags) / sizeof(kTags[0]) == kRxBriefReasonCount,
"RX reason tag table must match RxBriefReason::Count");
out.print(F("RXSTAT")); out.print(F("RXSTAT"));
for (uint8_t i = 1; i < 14U; i++) for (uint8_t i = static_cast<uint8_t>(RxBriefReason::MuteBegin);
i < kRxBriefReasonCount; ++i)
{ {
out.print(','); out.print(',');
out.print(kTags[i]); out.print(kTags[i]);
@ -273,7 +278,8 @@ void IR_DecoderRaw::rxBriefFlushDeferredIsrLogs()
uint16_t muteEndCnt = 0; uint16_t muteEndCnt = 0;
uint16_t rawCnt = 0; uint16_t rawCnt = 0;
uint32_t rawLastUs = 0; uint32_t rawLastUs = 0;
noInterrupts(); {
IrInterruptGuard guard;
muteBeginPending = rxBriefMuteBeginPending; muteBeginPending = rxBriefMuteBeginPending;
muteBeginUs = rxBriefMuteBeginUs; muteBeginUs = rxBriefMuteBeginUs;
rxBriefMuteBeginPending = false; rxBriefMuteBeginPending = false;
@ -288,7 +294,7 @@ void IR_DecoderRaw::rxBriefFlushDeferredIsrLogs()
rawLastUs = rxBriefRawOverflowLastUs; rawLastUs = rxBriefRawOverflowLastUs;
rxBriefRawOverflowDrops = 0; rxBriefRawOverflowDrops = 0;
rxBriefRawOverflowLastUs = 0; rxBriefRawOverflowLastUs = 0;
interrupts(); }
if (muteBeginPending) if (muteBeginPending)
rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs); rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs);
if (muteEndPending) if (muteEndPending)
@ -359,7 +365,7 @@ void IR_DecoderRaw::firstRX()
#ifdef IRDEBUG #ifdef IRDEBUG
wrCounter = 0; wrCounter = 0;
#endif #endif
memset(dataBuffer, 0x00, dataByteSizeMax); memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes);
pulseFilterReset(); pulseFilterReset();
preambleResetToIdle(); preambleResetToIdle();
} }
@ -368,9 +374,8 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
{ {
if (pulseFilterHoldCount != 0U) if (pulseFilterHoldCount != 0U)
return true; return true;
noInterrupts(); IrInterruptGuard guard;
const bool busy = !subBuffer.isEmpty(); const bool busy = !subBuffer.isEmpty();
interrupts();
return busy; return busy;
} }
@ -378,7 +383,7 @@ void IR_DecoderRaw::listenStart()
{ {
if (rxTimeoutPipelineBusy()) if (rxTimeoutPipelineBusy())
return; return;
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U)) if (isReciveRaw && ((micros() - lastEdgeTime) > receiveSilenceTimeoutUs()))
{ {
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(true); packTraceOnTimeoutOrAbort(true);
@ -396,7 +401,7 @@ inline void IR_DecoderRaw::checkTimeout()
if (rxTimeoutPipelineBusy()) if (rxTimeoutPipelineBusy())
return; return;
if (micros() - lastEdgeTime > IR_timeout * 2U) if (micros() - lastEdgeTime > receiveSilenceTimeoutUs())
{ {
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(false); packTraceOnTimeoutOrAbort(false);
@ -767,8 +772,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#if !defined(IRDEBUG_SERIAL_PACK) #if !defined(IRDEBUG_SERIAL_PACK)
(void)packTraceInvertFix; (void)packTraceInvertFix;
#endif #endif
if (i_dataBuffer >= dataByteSizeMax * 8) if (i_dataBuffer >= irproto::kMaxWireFrameBytes * 8U)
{ // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3) { // >=: не даёт записать бит за пределом 5-битной wire-длины.
isBufferOverflow = true; isBufferOverflow = true;
rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros()); rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros());
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
@ -876,7 +881,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
{ // Ппервый байт { // Ппервый байт
packSize = dataBuffer[0] & IR_MASK_MSG_INFO; packSize = dataBuffer[0] & IR_MASK_MSG_INFO;
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё. // 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 (в т.ч. будущие компактные кадры) обрабатываются как обычно. // packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
if (packSize != 0 && packSize < msgBytes + crcBytes) if (packSize != 0 && packSize < msgBytes + crcBytes)
isWrongPack = true; isWrongPack = true;
@ -910,7 +915,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
uint8_t packTraceBfBit = 0; uint8_t packTraceBfBit = 0;
bool packTraceBfMark = false; bool packTraceBfMark = false;
if (!isAvailable) // Исправление первого бита // Очень большая затычка... 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) for (int j = 0; j < 8; ++j)
{ {
@ -918,7 +923,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
dataBuffer[i] ^= 1 << j; dataBuffer[i] ^= 1 << j;
isAvailable = 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) if (isAvailable)
@ -953,7 +958,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros()); rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros());
else else
rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros()); rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros());
if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) { if (!isAvailable && packSize > 0 && packSize <= irproto::kMaxWireFrameBytes) {
memcpy(rejectBuffer, dataBuffer, packSize); memcpy(rejectBuffer, dataBuffer, packSize);
rejectPackSize = static_cast<uint8_t>(packSize); rejectPackSize = static_cast<uint8_t>(packSize);
isRejectAvailable = true; 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) void IR_DecoderRaw::edgeTracePush(uint32_t t_us, uint8_t level, uint8_t flags)
{ {
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY); const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
noInterrupts(); IrInterruptGuard guard;
const uint16_t w = edgeTrace_w; const uint16_t w = edgeTrace_w;
const uint16_t r = edgeTrace_r; const uint16_t r = edgeTrace_r;
const uint16_t next = static_cast<uint16_t>((w + 1u) % cap); const uint16_t next = static_cast<uint16_t>((w + 1u) % cap);
if (next == r) if (next == r)
{ {
edgeTrace_overflow = true; edgeTrace_overflow = true;
interrupts();
return; return;
} }
edgeTrace_buf[w].t_us = t_us; edgeTrace_buf[w].t_us = t_us;
edgeTrace_buf[w].level = level ? 1u : 0u; edgeTrace_buf[w].level = level ? 1u : 0u;
edgeTrace_buf[w].flags = flags; edgeTrace_buf[w].flags = flags;
edgeTrace_w = next; edgeTrace_w = next;
interrupts();
} }
void IR_DecoderRaw::edgeTraceClear() void IR_DecoderRaw::edgeTraceClear()
{ {
noInterrupts(); IrInterruptGuard guard;
edgeTrace_w = 0; edgeTrace_w = 0;
edgeTrace_r = 0; edgeTrace_r = 0;
edgeTrace_overflow = false; edgeTrace_overflow = false;
interrupts();
} }
uint16_t IR_DecoderRaw::edgeTracePendingCount() const uint16_t IR_DecoderRaw::edgeTracePendingCount() const
{ {
noInterrupts(); IrInterruptGuard guard;
const uint16_t w = edgeTrace_w; const uint16_t w = edgeTrace_w;
const uint16_t r = edgeTrace_r; const uint16_t r = edgeTrace_r;
interrupts();
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY); const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
if (w >= r) if (w >= r)
return static_cast<uint16_t>(w - r); return static_cast<uint16_t>(w - r);
@ -1054,27 +1055,28 @@ uint16_t IR_DecoderRaw::edgeTraceFlushChunk(Print &out, uint16_t maxRec)
maxRec = kStackCap; maxRec = kStackCap;
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY); const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
noInterrupts(); uint8_t tmp[kStackCap * 6];
uint16_t toCopy = 0U;
bool truncated = false;
bool ovf = false;
{
IrInterruptGuard guard;
const uint16_t w = edgeTrace_w; const uint16_t w = edgeTrace_w;
const uint16_t r = edgeTrace_r; const uint16_t r = edgeTrace_r;
uint16_t avail = (w >= r) ? static_cast<uint16_t>(w - r) : static_cast<uint16_t>(cap - r + w); const uint16_t avail = (w >= r) ? static_cast<uint16_t>(w - r)
uint16_t toCopy = (avail > maxRec) ? maxRec : avail; : static_cast<uint16_t>(cap - r + w);
const bool truncated = (avail > toCopy); toCopy = (avail > maxRec) ? maxRec : avail;
if (toCopy == 0) truncated = (avail > toCopy);
{ if (toCopy == 0U)
interrupts(); return 0U;
return 0;
}
uint8_t tmp[kStackCap * 6];
for (uint16_t i = 0; i < toCopy; ++i) for (uint16_t i = 0; i < toCopy; ++i)
{ {
const uint16_t idx = static_cast<uint16_t>((r + i) % cap); const uint16_t idx = static_cast<uint16_t>((r + i) % cap);
memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u); memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u);
} }
edgeTrace_r = static_cast<uint16_t>((r + toCopy) % cap); edgeTrace_r = static_cast<uint16_t>((r + toCopy) % cap);
const bool ovf = edgeTrace_overflow; ovf = edgeTrace_overflow;
interrupts(); }
uint8_t meta = 0; uint8_t meta = 0;
if (ovf) if (ovf)
@ -1304,7 +1306,7 @@ void IR_DecoderRaw::packTraceForceEndSyncPhase()
void IR_DecoderRaw::packTraceEmitHex(uint8_t byteCount) const void IR_DecoderRaw::packTraceEmitHex(uint8_t byteCount) const
{ {
Serial.print(F("IR hex:")); 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(' '); Serial.print(' ');
ptPrintHexU8(dataBuffer[i]); ptPrintHexU8(dataBuffer[i]);
@ -1402,8 +1404,8 @@ void IR_DecoderRaw::packTraceEmitErrorFlash(const __FlashStringHelper *msg)
Serial.println(msg); Serial.println(msg);
{ {
uint16_t nb = i_dataBuffer / 8u; uint16_t nb = i_dataBuffer / 8u;
if (nb > dataByteSizeMax) if (nb > irproto::kMaxWireFrameBytes)
nb = dataByteSizeMax; nb = irproto::kMaxWireFrameBytes;
packTraceEmitHex(static_cast<uint8_t>(nb)); packTraceEmitHex(static_cast<uint8_t>(nb));
} }
packTraceResetFrame(); packTraceResetFrame();
@ -1436,8 +1438,8 @@ void IR_DecoderRaw::packTraceOnTimeoutOrAbort(bool fromListenStart)
return; return;
const uint16_t expected = (i_dataBuffer >= 8) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0; const uint16_t expected = (i_dataBuffer >= 8) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0;
uint16_t gotBytes = i_dataBuffer / 8; uint16_t gotBytes = i_dataBuffer / 8;
if (gotBytes > dataByteSizeMax) if (gotBytes > irproto::kMaxWireFrameBytes)
gotBytes = dataByteSizeMax; gotBytes = irproto::kMaxWireFrameBytes;
Serial.println(); Serial.println();
packTraceEmitRawBitsLine(false); packTraceEmitRawBitsLine(false);
Serial.print(F(" => ERROR: TIMEOUT, rx_data_size = ")); 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) 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; const uint32_t candTimeout = IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT;
if (preambleState == PreambleState::Idle) if (preambleState == PreambleState::Idle)
@ -1696,7 +1698,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
err_syncBit = 0; err_syncBit = 0;
isWrongPack = false; isWrongPack = false;
msgTypeReceive = 0; msgTypeReceive = 0;
memset(dataBuffer, 0x00, dataByteSizeMax); memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes);
preambleState = PreambleState::Locked; preambleState = PreambleState::Locked;
isPreamb = false; isPreamb = false;

View File

@ -20,12 +20,13 @@ class Print;
///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
#define riseTime riseSyncTime //* bitTime */ 893U // TODO: Должно высчитываться медианой #define riseTime riseSyncTime //* bitTime */ 893U // TODO: Должно высчитываться медианой
#define riseTolerance tolerance /* 250U */ // погрешность #define riseTolerance IR_TIMING_TOLERANCE_US /* 250U */ // погрешность
#define riseTimeMax (riseTime + riseTolerance) #define riseTimeMax (riseTime + riseTolerance)
#define riseTimeMin (riseTime - riseTolerance) #define riseTimeMin (riseTime - riseTolerance)
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax) #define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1 // Compatibility aliases. The named contracts and their geometry live in IR_config.h.
constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; #define IR_timeout (::irproto::rxInterEdgeTimeoutUs(riseTime))
constexpr uint16_t IR_ResponseDelay = irproto::kDefaultResponseTurnaroundDelayMs;
class IR_Encoder; class IR_Encoder;
class IR_DecoderRaw : virtual public IR_FOX class IR_DecoderRaw : virtual public IR_FOX
@ -51,7 +52,14 @@ public:
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
bool isSubOverflow(); 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 pulseFilterDroppedByFilteredOverflow() const { return 0; }
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
@ -68,8 +76,9 @@ public:
/// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch, /// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch,
/// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok). /// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok).
/// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат). /// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат).
static constexpr uint8_t rxReasonCounterCount() { return kRxBriefReasonCount; }
const uint16_t *rxReasonCounters() const { return rxReasonCnt; } 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=.." /// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.."
void printRxReasonStats(Print &out) const; void printRxReasonStats(Print &out) const;
@ -94,12 +103,14 @@ private:
BufferOverflow = 10, BufferOverflow = 10,
Timeout = 11, Timeout = 11,
Crc = 12, Crc = 12,
Ok = 13 Ok = 13,
Count
}; };
static constexpr uint8_t kRxBriefReasonCount = static_cast<uint8_t>(RxBriefReason::Count);
bool isRejectAvailable = false; bool isRejectAvailable = false;
uint8_t rejectPackSize = 0; uint8_t rejectPackSize = 0;
uint8_t rejectBuffer[dataByteSizeMax]{}; uint8_t rejectBuffer[irproto::kMaxWireFrameBytes]{};
ErrorsStruct errors; ErrorsStruct errors;
bool isAvailable = false; bool isAvailable = false;
@ -179,7 +190,7 @@ private:
#endif #endif
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
uint8_t dataBuffer[dataByteSizeMax]{0}; // Буффер данных uint8_t dataBuffer[irproto::kMaxWireFrameBytes]{0}; // Буффер полного wire-кадра
volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов
volatile uint32_t risePeriod; volatile uint32_t risePeriod;
@ -247,7 +258,7 @@ bool isReciveRaw = false;
// (always-on наблюдаемость по контракту живучести), печать события — // (always-on наблюдаемость по контракту живучести), печать события —
// только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны. // только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны.
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0); 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 #if IR_RX_BRIEF_LOG
static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason); static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason);
void rxBriefNoteMuteBlockedIsr(uint32_t tUs); void rxBriefNoteMuteBlockedIsr(uint32_t tUs);
@ -263,7 +274,7 @@ bool isReciveRaw = false;
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
static constexpr uint16_t kPackTraceBufCap = 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 packTraceResetFrame();
void packTracePushBit(bool bit); void packTracePushBit(bool bit);

File diff suppressed because it is too large Load Diff

View File

@ -18,20 +18,88 @@ enum class IR_SendStatus : uint8_t {
DmaStartFailed, DmaStartFailed,
EncoderPinUnavailable, EncoderPinUnavailable,
BufferedStorageInvalid, BufferedStorageInvalid,
InvalidArgument,
TimingOverflow,
PlanMismatch,
DmaTransferError,
DmaStalled,
}; };
const char* irSendStatusToString(IR_SendStatus status); 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 { struct IR_SendResult {
bool success; // Флаг успешности отправки bool success; // true: backend принял и запустил эту операцию
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах uint32_t sendTimeMs; // ceil(plannedAirtimeUs / 1000), compatibility field
IR_SendStatus status; // Детализированный статус старта передачи IR_SendStatus status; // Детализированный статус старта передачи
uint32_t operationId; // 0, если новая операция не создавалась
uint32_t plannedAirtimeUs; // PHY airtime; без подготовки/release backend-а
IR_TxClockBasis clockBasis;
IR_SendResult(bool success = false, IR_SendResult(bool success = false,
uint32_t sendTimeMs = 0, uint32_t sendTimeMs = 0,
IR_SendStatus status = IR_SendStatus::ExternalStartFailed) IR_SendStatus status = IR_SendStatus::ExternalStartFailed,
: success(success), sendTimeMs(sendTimeMs), status(status) {} uint32_t operationId = 0,
uint32_t plannedAirtimeUs = 0,
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal)
: success(success), sendTimeMs(sendTimeMs), status(status),
operationId(operationId), plannedAirtimeUs(plannedAirtimeUs),
clockBasis(clockBasis) {}
}; };
class IR_DecoderRaw; class IR_DecoderRaw;
@ -53,6 +121,12 @@ public:
using ExternalTxBusyFn = bool (*)(void *ctx); using ExternalTxBusyFn = bool (*)(void *ctx);
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len); 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: private:
// uint16_t id; /// @brief Адрес передатчика // uint16_t id; /// @brief Адрес передатчика
public: public:
@ -111,14 +185,32 @@ public:
/** Optional: register external TX backend (e.g. DMA driver). */ /** Optional: register external TX backend (e.g. DMA driver). */
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx); 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(); 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). */ /** 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); 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). */ /** 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); 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 enable();
void disable(); void disable();
@ -132,6 +224,7 @@ public:
setBlindDecoders(decoders, static_cast<uint8_t>(N)); setBlindDecoders(decoders, static_cast<uint8_t>(N));
} }
IR_SendStatus rawSend(uint8_t *ptr, uint8_t len); 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 dataByte, bool needAccept = false);
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false); IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
@ -156,6 +249,10 @@ public:
uint32_t testSendBackTo(uint16_t addrTo, 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(); ~IR_Encoder();
@ -170,6 +267,7 @@ private:
static void carrierPauseIfIdle(); static void carrierPauseIfIdle();
static ExternalTxStartFn externalTxStartFn; static ExternalTxStartFn externalTxStartFn;
static ExternalTxStartFnV2 externalTxStartFnV2;
static ExternalTxBusyFn externalTxBusyFn; static ExternalTxBusyFn externalTxBusyFn;
static void *externalTxCtx; static void *externalTxCtx;
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len); 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 txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
static bool txAdvanceAfterOutput(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 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 loadTxFsmFromMembers(TxFsmState &st) const;
void storeTxFsmToMembers(const TxFsmState &st); void storeTxFsmToMembers(const TxFsmState &st);
bool shouldUseBufferedIsr() const; bool shouldUseBufferedIsr() const;
@ -218,9 +325,8 @@ private:
uint16_t txPowerSnap_ = 1; uint16_t txPowerSnap_ = 1;
uint16_t txMultiplySnap_ = 2; uint16_t txMultiplySnap_ = 2;
/** Legacy: физических тиков на один логический шаг FSM = multiply/2. */ /** Fractional 2×Fc -> multiply×Fc phase accumulator (also exact for odd multiply). */
uint16_t legacyPhysPerLogical_ = 1; uint32_t legacyScaleAccumulator_ = 0;
uint16_t legacyPhysCounter_ = 0;
uint16_t legacySlotInPeriod_ = 0; uint16_t legacySlotInPeriod_ = 0;
volatile uint16_t powerNumerator_ = 1; volatile uint16_t powerNumerator_ = 1;
@ -235,9 +341,27 @@ private:
uint8_t decodersCount = 0; uint8_t decodersCount = 0;
uint8_t sendLen = 0; uint8_t sendLen = 0;
uint8_t sendBuffer[dataByteSizeMax]{0}; /// @brief Буффер данных для отправки uint8_t sendBuffer[irproto::kMaxWireFrameBytes]{0}; /// @brief Буффер полного wire-кадра
volatile bool isSending = false; 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 bool state = LOW; /// @brief Текущий уровень генерации
volatile uint8_t dataByteCounter = 0; volatile uint8_t dataByteCounter = 0;

View File

@ -30,4 +30,4 @@ uint8_t IR_FOX::crc8(uint8_t *data, uint8_t start, uint8_t end, uint8_t poly)
} }
} }
return crc; return crc;
}; }

View File

@ -15,8 +15,6 @@ constexpr size_t kDefaultDmaTxMaxStreams = 4U;
constexpr uint32_t kDmaTxIrqPriority = 8U; constexpr uint32_t kDmaTxIrqPriority = 8U;
/** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */ /** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */
constexpr uint16_t kIsrTxBsrrWordCount = 256U; constexpr uint16_t kIsrTxBsrrWordCount = 256U;
/** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */
constexpr size_t kIsrTxMaxGateRuns = 512U;
static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even"); static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even");
} }
@ -101,7 +99,7 @@ msg type:
                                //  ----------                                 //  ----------
                                // | xxx..... | = тип сообщения (биты 7..5)                                 // | xxx..... | = тип сообщения (биты 7..5)
                                // | ...xxxxx | = полная длина кадра в байтах (5 бит, 0..31, IR_MASK_MSG_INFO), не «31 бит» и не отдельный лимит «24 байта»                                 // | ...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_BACK 0U // | 000...... | = Задний сигнал машинки
#define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение #define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение
@ -111,7 +109,7 @@ msg type:
// #define IR_MSG_ 5U // | 101..... | = ?? // #define IR_MSG_ 5U // | 101..... | = ??
#define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения #define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения
#define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения #define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения
; /*   // ---------- /*   // ----------
/``````````````````````````````` подтверждение `````````````````````````````\      /``````````````````````````````````````` запрос ``````````````````````````````````\ /``````````````````````````````` подтверждение `````````````````````````````\      /``````````````````````````````````````` запрос ``````````````````````````````````\
                                                                                                                                                                                                                                             
@ -159,15 +157,25 @@ msg type:
*/ */
#define IR_MASK_MSG_TYPE 0b00000111 namespace irproto {
#define IR_MASK_MSG_INFO 0b00011111 /** 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<uint8_t>((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; typedef uint16_t crc_t;
// #define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает // #define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает
#define bytePerPack (31) // колличество байтов в пакете
#ifndef freeFrec #ifndef freeFrec
#define freeFrec false #define freeFrec false
#endif #endif
@ -248,8 +256,6 @@ typedef uint16_t crc_t;
#define poly2 0x8C #define poly2 0x8C
#define syncBits 3U // количество битов синхронизации #define syncBits 3U // количество битов синхронизации
#define dataByteSizeMax (msgBytes + addrBytes + addrBytes + bytePerPack + crcBytes)
#define preambFronts (preambPulse * 2) // количество фронтов преамбулы (Приём) #define preambFronts (preambPulse * 2) // количество фронтов преамбулы (Приём)
#define preambToggle ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) // колличество переключений преамбулы (Передача) #define preambToggle ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) // колличество переключений преамбулы (Передача)
@ -262,7 +268,123 @@ typedef uint16_t crc_t;
#define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах #define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #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<uint8_t>(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<uint32_t>(kRxTimingToleranceUs)) *
static_cast<uint32_t>(kRxInterEdgeTimeoutBitWindows);
}
/** Silence after which an unfinished RX candidate is retired. */
constexpr uint32_t rxSilenceTimeoutUs(uint32_t adaptiveBitPeriodUs)
{
return rxInterEdgeTimeoutUs(adaptiveBitPeriodUs) *
static_cast<uint32_t>(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<size_t>(preambPulse * 2U) +
static_cast<size_t>(kMaxWireFrameBytes) *
static_cast<size_t>((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<uint32_t>(preambPulse * 2U) *
static_cast<uint32_t>(preambToggle + 1U);
constexpr uint32_t kEncodedBitLogicalTicks =
static_cast<uint32_t>(bitTakts * 2U);
constexpr uint32_t kMaxLogicalTransmissionTicks =
kPreambleLogicalTicks +
static_cast<uint32_t>(kMaxWireFrameBytes) *
static_cast<uint32_t>(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<uint64_t>(kMaxLogicalTransmissionTicks) *
static_cast<uint64_t>(normalizedCarrierMultiply(multiply)) +
1U) /
2U;
}
constexpr size_t maxPhysicalGateRunCapacity(uint16_t multiply)
{
return kMaxLogicalGateRuns +
static_cast<size_t>(maxPhysicalTransmissionTicks(multiply) /
static_cast<uint64_t>(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_Time = bitTime;
constexpr uint16_t test_all_Takts = bitTakts * 2; constexpr uint16_t test_all_Takts = bitTakts * 2;

View File

@ -120,7 +120,23 @@ public:
if (enc == nullptr) return IR_SendStatus::ExternalNoStream; if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
for (uint8_t i = 0; i < streamCount_; i++) { for (uint8_t i = 0; i < streamCount_; i++) {
if (streams_[i].enc == enc) { 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; return IR_SendStatus::ExternalNoStream;
@ -192,6 +208,7 @@ private:
uint32_t totalTicks = 0; uint32_t totalTicks = 0;
volatile uint32_t ticksOutput = 0; volatile uint32_t ticksOutput = 0;
uint32_t operationId = 0;
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока). // Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
uint32_t lastTicks = 0; uint32_t lastTicks = 0;
@ -204,20 +221,23 @@ private:
ticksOutput = 0; ticksOutput = 0;
totalTicks = 0; totalTicks = 0;
runCount = 0; runCount = 0;
operationId = 0;
} }
IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) { IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) {
wave.fill(dst, count); wave.fill(dst, count);
} }
void onHalf() { void advanceHalf() {
ticksOutput += halfLen; ticksOutput += halfLen;
}
void refillFirstHalf() {
fill(&dmaBuf[0], halfLen); fill(&dmaBuf[0], halfLen);
__DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца __DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца
} }
void onComplete() { void refillSecondHalf() {
ticksOutput += halfLen;
fill(&dmaBuf[halfLen], halfLen); fill(&dmaBuf[halfLen], halfLen);
__DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца __DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца
} }
@ -262,7 +282,7 @@ private:
void forceStop(TxStream& s) { void forceStop(TxStream& s) {
HAL_NVIC_DisableIRQ(s.dmaIrq); HAL_NVIC_DisableIRQ(s.dmaIrq);
if (s.active) { if (s.active) {
stopStream(s); stopStream(s, IR_SendStatus::DmaStalled);
recoveries_++; recoveries_++;
} }
HAL_NVIC_EnableIRQ(s.dmaIrq); HAL_NVIC_EnableIRQ(s.dmaIrq);
@ -281,18 +301,22 @@ private:
static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) { static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) {
auto* s = streamFromDma(hdma); auto* s = streamFromDma(hdma);
if (s == nullptr || !s->active) return; if (s == nullptr || !s->active) return;
s->onHalf(); s->advanceHalf();
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) { 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) { static void dmaCpltCb(DMA_HandleTypeDef* hdma) {
auto* s = streamFromDma(hdma); auto* s = streamFromDma(hdma);
if (s == nullptr || !s->active) return; if (s == nullptr || !s->active) return;
s->onComplete(); s->advanceHalf();
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) { 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(); s->onError();
if (s_instance != nullptr) { if (s_instance != nullptr) {
s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error 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; 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.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
if (s.active) return IR_SendStatus::EncoderBusy; 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.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig; if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
s.resetWave(); s.resetWave();
const uint16_t mult = IR_Encoder::carrierMultiply(); const uint16_t mult = expectedPlan.carrierMultiply;
s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult); const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed; packet, len, s.runs, s.maxRuns, mult);
if (!built.valid()) return built.status;
uint32_t total = 0; if (built.physicalTicks != expectedPlan.physicalTicks ||
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks; built.gateRunCount != expectedPlan.gateRunCount)
s.totalTicks = total; return IR_SendStatus::PlanMismatch;
s.runCount = static_cast<size_t>(built.gateRunCount);
s.totalTicks = built.physicalTicks;
s.operationId = operationId;
uint16_t pwr = mult / 2U; uint16_t pwr = mult / 2U;
if (s.enc != nullptr) { if (s.enc != nullptr) {
@ -388,6 +420,7 @@ private:
const uint32_t dst = u32ptr(&s.port->BSRR); 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) { if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) {
s.active = false; s.active = false;
s.operationId = 0U;
return IR_SendStatus::DmaStartFailed; return IR_SendStatus::DmaStartFailed;
} }
@ -395,10 +428,12 @@ private:
return IR_SendStatus::Success; return IR_SendStatus::Success;
} }
void stopStream(TxStream& s) { void stopStream(TxStream& s, IR_SendStatus terminalStatus) {
if (!s.active) return; if (!s.active) return;
const uint32_t operationId = s.operationId;
s.active = false; s.active = false;
s.operationId = 0U;
HAL_DMA_Abort_IT(&s.hdma); HAL_DMA_Abort_IT(&s.hdma);
if (s.port != nullptr) { if (s.port != nullptr) {
@ -406,7 +441,7 @@ private:
} }
if (s.enc != nullptr) { if (s.enc != nullptr) {
s.enc->externalFinishSend(); s.enc->externalFinishSend(operationId, terminalStatus);
} }
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика. // Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
} }

51
IrInterruptGuard.h Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include <Arduino.h>
#if defined(__AVR__)
#include <avr/interrupt.h>
#include <avr/io.h>
#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<uint8_t>(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
};

View File

@ -2,12 +2,49 @@
namespace PacketTypes namespace PacketTypes
{ {
bool BasePack::checkAddress() { return true; }; uint8_t minimumPacketSize(uint8_t msgType)
void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id)
{ {
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->packInfo = packInfo;
this->id = id; 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()) if (checkAddress())
{ {
isAvailable = true; isAvailable = true;
@ -23,29 +60,65 @@ namespace PacketTypes
Serial.print(" NOT-OK "); Serial.print(" NOT-OK ");
#endif #endif
} }
return isAvailable;
} }
uint16_t BasePack::_getAddrFrom(BasePack *obj) 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]; return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
}; }
uint16_t BasePack::_getAddrTo(BasePack *obj) 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]; return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
}; }
uint8_t BasePack::_getDataSize(BasePack *obj) 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) 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; return obj->packInfo->buffer + obj->DataOffset;
}; }
uint8_t BasePack::_getDataRawSize(BasePack *obj) uint8_t BasePack::_getDataRawSize(BasePack *obj)
{ {
if (obj == nullptr || obj->packInfo == nullptr)
{
return 0;
}
return obj->packInfo->packSize; return obj->packInfo->packSize;
}; }
bool BasePack::available() bool BasePack::available()
{ {
@ -59,7 +132,7 @@ namespace PacketTypes
{ {
return false; return false;
} }
}; }
bool BasePack::availableRaw() bool BasePack::availableRaw()
{ {
if (isRawAvailable) if (isRawAvailable)
@ -71,7 +144,7 @@ namespace PacketTypes
{ {
return false; return false;
} }
}; }
bool Data::checkAddress() bool Data::checkAddress()
{ {

View File

@ -4,25 +4,34 @@
class IR_Decoder; class IR_Decoder;
namespace PacketTypes 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 class BasePack
{ {
friend IR_Decoder; friend IR_Decoder;
protected: protected:
bool isAvailable; bool isAvailable = false;
bool isRawAvailable; bool isRawAvailable = false;
bool isNeedAccept; bool isNeedAccept = false;
uint8_t msgOffset; uint8_t msgOffset = 0;
uint8_t addressFromOffset; uint8_t addressFromOffset = 0;
uint8_t addressToOffset; uint8_t addressToOffset = 0;
uint8_t DataOffset; uint8_t DataOffset = 0;
IR_FOX::PackInfo *packInfo; IR_FOX::PackInfo *packInfo = nullptr;
uint16_t id; uint16_t id = 0;
virtual bool checkAddress(); 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 _getAddrFrom(BasePack *obj);
static uint16_t _getAddrTo(BasePack *obj); static uint16_t _getAddrTo(BasePack *obj);

View File

@ -1,5 +1,5 @@
#pragma once #pragma once
#include "Arduino.h" #include "IrInterruptGuard.h"
template <typename T, unsigned int BufferSize> template <typename T, unsigned int BufferSize>
class RingBuffer { class RingBuffer {
public: public:
@ -15,39 +15,35 @@ public:
bool push(T element) { bool push(T element) {
bool pushed = false; bool pushed = false;
noInterrupts(); IrInterruptGuard guard;
if (!isFull()) { if (!isFull()) {
data[end] = element; data[end] = element;
end = (end + 1) % BufferSize; end = (end + 1) % BufferSize;
pushed = true; pushed = true;
} }
interrupts();
return pushed; return pushed;
} }
T* pop() { T* pop() {
noInterrupts(); IrInterruptGuard guard;
T* value = nullptr; T* value = nullptr;
if (!isEmpty()) { if (!isEmpty()) {
value = &data[start]; value = &data[start];
start = (start + 1) % BufferSize; start = (start + 1) % BufferSize;
} }
interrupts();
return value; return value;
} }
// B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению. // B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению.
// (T* pop() отдаёт указатель во внутренний слот; его внутренний interrupts() снимает внешнюю // (T* pop() отдаёт указатель во внутренний слот; после выхода слот снова может быть перезаписан.)
// защиту вызывающего ДО чтения *ptr → торн-рид, если кольцо переполнится в этом окне.)
bool pop(T &out) { bool pop(T &out) {
bool popped = false; bool popped = false;
noInterrupts(); IrInterruptGuard guard;
if (!isEmpty()) { if (!isEmpty()) {
out = data[start]; out = data[start];
start = (start + 1) % BufferSize; start = (start + 1) % BufferSize;
popped = true; popped = true;
} }
interrupts();
return popped; return popped;
} }

View File

@ -20,7 +20,7 @@ static constexpr uint16_t kIrDeviceAddr = 0;
static constexpr uint8_t kCmdVersion = 0x5E; static constexpr uint8_t kCmdVersion = 0x5E;
static constexpr uint32_t kSerialBaud = 115200; static constexpr uint32_t kSerialBaud = 115200;
static constexpr uint32_t kSendPeriodMs = 500; 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 constexpr uint8_t kMaxParamBytes = kMaxPayload - 1;
static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr); static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr);
@ -30,15 +30,23 @@ static HardwareTimer irTimer(TIM17);
namespace { namespace {
constexpr size_t kIrDmaStreams = 1; constexpr size_t kIrDmaStreams = 1;
constexpr uint16_t kIrDmaTxWordCount = 4096U; 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 uint32_t s_irDmaWords[kIrDmaTxWordCount];
static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns]; static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns];
} // namespace } // namespace
static IrDmaTxStm32<kIrDmaStreams> dmaBackend; static IrDmaTxStm32<kIrDmaStreams> dmaBackend;
static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); } static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); }
static bool txStart(void * /*ctx*/, IR_Encoder *e, const uint8_t *packet, uint8_t len) { static IR_SendStatus txStart(void * /*ctx*/,
return dmaBackend.start(e, packet, len); 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 #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. // 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"; static const char kPayload16[] = "Car_v4.3.9_[12MH";
// 25 байт total: как выше, но data=0x5E + 17 ASCII. // 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() { static void rebuildIrPayload() {
s_irPayload[0] = kCmdVersion; s_irPayload[0] = kCmdVersion;
@ -110,7 +122,7 @@ void setup() {
Serial.println(F("[IR_DMA] init FAILED")); Serial.println(F("[IR_DMA] init FAILED"));
return; return;
} }
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr); IR_Encoder::setExternalTxBackendV2(txStart, txBusy, nullptr);
#elif LONGDATA_LEGACY_ISR #elif LONGDATA_LEGACY_ISR
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0); IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
#else #else

View File

@ -0,0 +1,134 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
struct GPIO_TypeDef
{
uint32_t BSRR = 0;
uint32_t IDR = 0;
};
class __FlashStringHelper;
#define F(value) (reinterpret_cast<const __FlashStringHelper *>(value))
class Print
{
public:
size_t print(const __FlashStringHelper *value)
{
return append(reinterpret_cast<const char *>(value));
}
size_t print(const char *value) { return append(value); }
size_t print(char value)
{
buffer_.push_back(value);
return 1U;
}
template <typename T>
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<char>(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 <typename T> void print(const T&) {}
template <typename T> 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; }

148
tests/test_packet_types.cpp Normal file
View File

@ -0,0 +1,148 @@
#include "PacketTypes.h"
#include <cassert>
#include <cstdint>
#include <iostream>
namespace
{
template <typename Packet>
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 <typename Packet>
void checkTypedBoundary(uint8_t msgType, uint8_t minimum)
{
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
ExposedPacket<Packet> 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<PacketTypes::Data>(IR_MSG_DATA_ACCEPT, 7);
checkTypedBoundary<PacketTypes::Data>(IR_MSG_DATA_NOACCEPT, 7);
checkTypedBoundary<PacketTypes::DataBack>(IR_MSG_BACK, 5);
checkTypedBoundary<PacketTypes::DataBack>(IR_MSG_BACK_TO, 7);
checkTypedBoundary<PacketTypes::Request>(IR_MSG_REQUEST, 7);
checkTypedBoundary<PacketTypes::Accept>(IR_MSG_ACCEPT, 6);
}
void testPayloadAccessSaturates()
{
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
ExposedPacket<PacketTypes::Data> 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<PacketTypes::DataBack> 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<PacketTypes::BasePack> 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;
}

View File

@ -0,0 +1,69 @@
#include "IR_DecoderRaw.h"
#include <cassert>
#include <cstdint>
#include <iostream>
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<uint64_t>(irproto::kNominalRxInterEdgeTimeoutUs) * 27735U / 10000U;
return static_cast<uint16_t>(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;
}

View File

@ -0,0 +1,79 @@
#include "IR_config.h"
#include "RingBuffer.h"
#include <cassert>
#include <cstdint>
#include <iostream>
#include <type_traits>
// 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<uint8_t>(IR_DecoderRaw::RxBriefReason::Count),
"public RX reason count must follow the enum sentinel");
static_assert(static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Count) ==
static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Ok) + 1U,
"RX reason Count must remain one past the final reason");
static_assert(std::extent<decltype(IR_DecoderRaw::rxReasonCnt)>::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<uint8_t>(IR_DecoderRaw::RxBriefReason::MuteBegin);
const uint8_t count = IR_DecoderRaw::rxReasonCounterCount();
for (uint8_t i = first; i < count; ++i)
decoder.rxBriefLog(static_cast<IR_DecoderRaw::RxBriefReason>(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;
}

328
tests/test_tx_contract.cpp Normal file
View File

@ -0,0 +1,328 @@
#include "IR_Encoder.h"
#include "IR_DecoderRaw.h"
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
// 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<uint8_t>(i * 73U + 19U); break;
}
}
}
void testPlannerMatchesBuiltStream()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kRunCapacity> 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<uint8_t, irproto::kMaxWireFrameBytes> 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<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kRunCapacity> logicalRuns{};
fillPattern(frame.data(), frame.size(), 2U);
assert(IR_Encoder::buildGateRuns(
frame.data(), static_cast<uint8_t>(frame.size()),
logicalRuns.data(), logicalRuns.size()) != 0U);
std::array<uint8_t, irproto::kMaxWireFrameBytes + 1U> oversizedFrame{};
assert(IR_Encoder::buildGateRuns(
oversizedFrame.data(), static_cast<uint8_t>(oversizedFrame.size()),
logicalRuns.data(), logicalRuns.size()) == 0U);
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
frame.data(), static_cast<uint8_t>(frame.size()), 6U);
assert(planned.valid());
assert(planned.gateRunCount <= irproto::kIsrTxMaxGateRuns);
IrTxGateRun oneRun{};
const IR_TxPlan tooSmall = IR_Encoder::buildPhysicalTransmission(
frame.data(), static_cast<uint8_t>(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<uint8_t, 10U> tenBytes{};
IR_TxPlan configured = IR_Encoder::planPhysicalTransmission(
tenBytes.data(), static_cast<uint8_t>(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<uint8_t>(irproto::kMaxWireFrameBytes + 1U), 2U).valid());
}
void testDerivedFixedStorageCapacity()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kFullMultiplyRunCapacity> 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<uint8_t>(frame.size()), pattern);
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
frame.data(), static_cast<uint8_t>(frame.size()), multiply);
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
frame.data(), static_cast<uint8_t>(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<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kRunCapacity> logical{};
std::array<IrTxGateRun, kRunCapacity> expected{};
fillPattern(frame.data(), static_cast<uint8_t>(frame.size()), 3U);
const size_t logicalCount = IR_Encoder::buildGateRuns(
frame.data(), static_cast<uint8_t>(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<uint8_t>(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<IrTxGateRun, 8U> 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<uint32_t>(longCount)) == 229373U);
std::array<IrTxGateRun, 2U> 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<FakeBackend *>(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;
}