#include "IR_config.h" #include "RingBuffer.h" // Test only: inspect the decoder state machine without adding production hooks. #define private public #include "IR_DecoderRaw.h" #undef private #include #include #include #include namespace { uint32_t decoderTimeoutUs(const IR_DecoderRaw &decoder) { return static_cast(decoder.riseSyncTime + tolerance) * static_cast(bitPerByte + syncBits + 1U); } uint32_t candidateTimeoutUs(const IR_DecoderRaw &decoder) { return decoderTimeoutUs(decoder) * static_cast(IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT); } uint8_t crc8Local(const uint8_t *data, uint8_t start, uint8_t end, uint8_t poly) { uint8_t crc = 0xFFU; for (uint8_t i = start; i < end; ++i) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8U; ++bit) crc = (crc & 0x80U) != 0U ? static_cast((crc << 1U) ^ poly) : static_cast(crc << 1U); } return crc; } void primeObservableCandidate(IR_DecoderRaw &decoder, uint32_t lastEdgeUs) { decoder.preambleState = IR_DecoderRaw::PreambleState::Candidate; decoder.preambleGoodPeriods = 1U; decoder.preambleWasObservable = true; decoder.preambleMeanPeriod = bitTime; decoder.preambleCandidateLastEdgeTime = lastEdgeUs; decoder.preambleCandidateFirstRiseTime = lastEdgeUs; decoder.preambleCandidateFirstRiseValid = true; decoder.isPreamb = true; decoder.isRecive = false; decoder.isReciveRaw = false; } void primeLocked(IR_DecoderRaw &decoder, uint8_t msgType, uint8_t wireBytes) { decoder.preambleState = IR_DecoderRaw::PreambleState::Locked; decoder.isPreamb = false; decoder.isRecive = true; decoder.isReciveRaw = true; decoder.isWrongPack = false; decoder.isBufferOverflow = false; decoder.isAvailable = false; decoder.packSize = wireBytes; decoder.dataBuffer[0] = static_cast((msgType << 5U) | (wireBytes & IR_MASK_MSG_INFO)); decoder.i_dataBuffer = 8U; } void verifyInitialSnapshot() { IR_DecoderRaw decoder(1U, 42U, nullptr); const IR_RxTerminalInfo terminal = decoder.rxLastTerminal(); assert(terminal.seq == 0U); assert(terminal.reason == IR_RxTerminalReason::None); assert(terminal.msgType == 0xFFU); assert(!terminal.hadLock); } void verifyCandidateExpiresOnIdleTick() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t lastEdgeUs = 1000U; primeObservableCandidate(decoder, lastEdgeUs); assert(decoder.rxLineActive()); arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder) + 1U; decoder.tick(); const IR_RxTerminalInfo terminal = decoder.rxLastTerminal(); assert(!decoder.rxLineActive()); assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Idle); assert(terminal.seq == 1U); assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout); assert(terminal.msgType == 0xFFU); assert(!terminal.hadLock); ++arduinoStubMicros; decoder.tick(); assert(decoder.rxTerminalSeq() == terminal.seq); } void emitEdge(IR_DecoderRaw &decoder, uint32_t timeUs, bool high) { arduinoStubMicros = timeUs; arduinoStubPort.IDR = high ? 1U : 0U; decoder.isr(); decoder.tick(); } void queueEdge(IR_DecoderRaw &decoder, uint32_t timeUs, bool high) { arduinoStubMicros = timeUs; arduinoStubPort.IDR = high ? 1U : 0U; decoder.isr(); } void verifyCandidateIdleExpiryThroughPublicPipeline() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U; const uint32_t risePeriodUs = static_cast(bitTime) * 5U / 2U; emitEdge(decoder, firstRiseUs, true); emitEdge(decoder, firstRiseUs + risePeriodUs / 2U, false); emitEdge(decoder, firstRiseUs + risePeriodUs, true); assert(decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 0U); arduinoStubMicros = firstRiseUs + risePeriodUs + candidateTimeoutUs(decoder) + 1U; decoder.tick(); assert(!decoder.rxLineActive()); assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout); assert(decoder.rxTerminalSeq() == 1U); } void verifyCoarseResetPublishesThroughBatchedPublicPipeline() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U; const uint32_t goodPeriodUs = static_cast(bitTime) * 5U / 2U; const uint32_t badPeriodUs = static_cast(bitTime) * 4U; queueEdge(decoder, firstRiseUs, true); queueEdge(decoder, firstRiseUs + goodPeriodUs / 2U, false); queueEdge(decoder, firstRiseUs + goodPeriodUs, true); queueEdge(decoder, firstRiseUs + goodPeriodUs + badPeriodUs / 2U, false); queueEdge(decoder, firstRiseUs + goodPeriodUs + badPeriodUs, true); decoder.tick(); assert(decoder.rxTerminalSeq() == 0U); assert(decoder.rxLineActive()); // Continuing coarse-invalid edges below the timeout keep the potential // frame busy. They manufacture no terminal; a Car gate reaches its bounded // hard deadline and skips the optional tail instead of transmitting here. const uint32_t nextRiseUs = firstRiseUs + goodPeriodUs + badPeriodUs + badPeriodUs; queueEdge(decoder, nextRiseUs - badPeriodUs / 2U, false); queueEdge(decoder, nextRiseUs, true); decoder.tick(); assert(decoder.rxTerminalSeq() == 0U); assert(decoder.rxLineActive()); arduinoStubMicros = nextRiseUs + candidateTimeoutUs(decoder) + 1U; decoder.tick(); const IR_RxTerminalInfo terminal = decoder.rxLastTerminal(); assert(terminal.seq == 1U); assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout); assert(!terminal.hadLock); assert(!decoder.rxLineActive()); } void verifyFreshCandidateWithOnlyCoarseInvalidEdgesStaysActive() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U; const uint32_t badPeriodUs = static_cast(bitTime) * 4U; const uint32_t startDeadlineUs = firstRiseUs + 58000U; const uint32_t hardDeadlineUs = firstRiseUs + 78000U; emitEdge(decoder, firstRiseUs, true); assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Candidate); assert(decoder.preambleGoodPeriods == 0U); assert(decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 0U); uint32_t riseUs = firstRiseUs; while (riseUs + badPeriodUs <= startDeadlineUs) { emitEdge(decoder, riseUs + badPeriodUs / 2U, false); riseUs += badPeriodUs; emitEdge(decoder, riseUs, true); assert(decoder.preambleGoodPeriods == 0U); assert(decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 0U); } arduinoStubMicros = startDeadlineUs; decoder.tick(); assert(decoder.rxLineActive()); while (riseUs + badPeriodUs <= hardDeadlineUs) { emitEdge(decoder, riseUs + badPeriodUs / 2U, false); riseUs += badPeriodUs; emitEdge(decoder, riseUs, true); assert(decoder.preambleGoodPeriods == 0U); assert(decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 0U); } arduinoStubMicros = hardDeadlineUs; decoder.tick(); assert(decoder.rxLineActive()); arduinoStubMicros = riseUs + candidateTimeoutUs(decoder); decoder.tick(); assert(decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 0U); ++arduinoStubMicros; decoder.tick(); assert(!decoder.rxLineActive()); assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout); assert(decoder.rxTerminalSeq() == 1U); } void verifyCandidateExpiryWaitsForPipelineDrain() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t lastEdgeUs = 2000U; primeObservableCandidate(decoder, lastEdgeUs); decoder.pulseFilterHoldCount = 1U; const uint32_t expiredAt = lastEdgeUs + candidateTimeoutUs(decoder) + 1U; decoder.expirePreambleCandidateIfIdle(expiredAt); assert(decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 0U); decoder.pulseFilterHoldCount = 0U; decoder.expirePreambleCandidateIfIdle(expiredAt); assert(!decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 1U); } void verifyCandidateExpiryAcrossMicrosWrap() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t lastEdgeUs = std::numeric_limits::max() - 1000U; primeObservableCandidate(decoder, lastEdgeUs); arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder) + 1U; decoder.tick(); assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout); assert(!decoder.rxLineActive()); } void verifyCandidateTimeoutBoundary() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t lastEdgeUs = 2500U; primeObservableCandidate(decoder, lastEdgeUs); arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder); decoder.tick(); assert(decoder.rxLineActive()); assert(decoder.rxTerminalSeq() == 0U); ++arduinoStubMicros; decoder.tick(); assert(!decoder.rxLineActive()); assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout); } void verifyTimedOutCandidateRestartIsTerminal() { IR_DecoderRaw decoder(1U, 42U, nullptr); const uint32_t lastEdgeUs = 3000U; primeObservableCandidate(decoder, lastEdgeUs); IR_DecoderRaw::FrontStorage nextEdge; nextEdge.time = lastEdgeUs + candidateTimeoutUs(decoder) + 1U; nextEdge.dir = true; decoder.preambleProcessEdge(nextEdge); const IR_RxTerminalInfo terminal = decoder.rxLastTerminal(); assert(terminal.seq == 1U); assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout); assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Candidate); assert(decoder.preambleGoodPeriods == 0U); assert(decoder.rxReasonCounters()[ static_cast(IR_DecoderRaw::RxBriefReason::Preamble)] == 1U); } void verifyLockedTimeoutPublishesHeaderTypeOnce() { IR_DecoderRaw decoder(1U, 42U, nullptr); primeLocked(decoder, IR_MSG_DATA_NOACCEPT, 10U); decoder.lastEdgeTime = 5000U; arduinoStubMicros = decoder.lastEdgeTime + decoderTimeoutUs(decoder) * 2U + 1U; decoder.tick(); const IR_RxTerminalInfo terminal = decoder.rxLastTerminal(); assert(terminal.seq == 1U); assert(terminal.reason == IR_RxTerminalReason::LockedTimeout); assert(terminal.msgType == IR_MSG_DATA_NOACCEPT); assert(terminal.hadLock); assert(!decoder.rxLineActive()); ++arduinoStubMicros; decoder.tick(); assert(decoder.rxTerminalSeq() == terminal.seq); } void verifyDecodeAbortPublishesTerminal() { IR_DecoderRaw decoder(1U, 42U, nullptr); primeLocked(decoder, IR_MSG_REQUEST, 7U); decoder.isWrongPack = true; decoder.writeToBuffer(false); const IR_RxTerminalInfo terminal = decoder.rxLastTerminal(); assert(terminal.seq == 1U); assert(terminal.reason == IR_RxTerminalReason::DecodeAbort); assert(terminal.msgType == IR_MSG_REQUEST); assert(terminal.hadLock); } void finishBackFrame(IR_DecoderRaw &decoder, bool corruptCrc) { constexpr uint8_t wireBytes = 5U; primeLocked(decoder, IR_MSG_BACK, wireBytes); decoder.dataBuffer[1] = 0x12U; decoder.dataBuffer[2] = 0x34U; decoder.dataBuffer[3] = crc8Local(decoder.dataBuffer, 0U, 3U, poly1); const uint8_t crcLow = crc8Local(decoder.dataBuffer, 0U, 4U, poly2); const uint8_t finalBit = static_cast((crcLow & 1U) ^ (corruptCrc ? 1U : 0U)); decoder.dataBuffer[4] = static_cast(crcLow & 0xFEU); decoder.i_dataBuffer = wireBytes * bitPerByte - 1U; decoder.bufBitPos = static_cast(decoder.i_dataBuffer); decoder.nextControlBit = 0xFFFFU; decoder.isData = true; decoder.writeToBuffer(finalBit != 0U); } void verifyCompleteFrameTerminalReasons() { IR_DecoderRaw good(1U, 42U, nullptr); finishBackFrame(good, false); const IR_RxTerminalInfo ok = good.rxLastTerminal(); assert(ok.seq == 1U); assert(ok.reason == IR_RxTerminalReason::FrameOk); assert(ok.msgType == IR_MSG_BACK); assert(ok.hadLock); IR_DecoderRaw bad(1U, 42U, nullptr); finishBackFrame(bad, true); const IR_RxTerminalInfo crc = bad.rxLastTerminal(); assert(crc.seq == 1U); assert(crc.reason == IR_RxTerminalReason::FrameCrcError); assert(crc.msgType == IR_MSG_BACK); assert(crc.hadLock); } } int main() { verifyInitialSnapshot(); verifyCandidateExpiresOnIdleTick(); verifyCandidateIdleExpiryThroughPublicPipeline(); verifyCoarseResetPublishesThroughBatchedPublicPipeline(); verifyFreshCandidateWithOnlyCoarseInvalidEdgesStaysActive(); verifyCandidateExpiryWaitsForPipelineDrain(); verifyCandidateExpiryAcrossMicrosWrap(); verifyCandidateTimeoutBoundary(); verifyTimedOutCandidateRestartIsTerminal(); verifyLockedTimeoutPublishesHeaderTypeOnce(); verifyDecodeAbortPublishesTerminal(); verifyCompleteFrameTerminalReasons(); std::cout << "IR RX terminal tests: OK\n"; return 0; }