Files
IR-protocol/tests/test_rx_terminal.cpp

387 lines
13 KiB
C++

#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 <cassert>
#include <cstdint>
#include <iostream>
#include <limits>
namespace
{
uint32_t decoderTimeoutUs(const IR_DecoderRaw &decoder)
{
return static_cast<uint32_t>(decoder.riseSyncTime + tolerance) *
static_cast<uint32_t>(bitPerByte + syncBits + 1U);
}
uint32_t candidateTimeoutUs(const IR_DecoderRaw &decoder)
{
return decoderTimeoutUs(decoder) *
static_cast<uint32_t>(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<uint8_t>((crc << 1U) ^ poly)
: static_cast<uint8_t>(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<uint8_t>((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<uint32_t>(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<uint32_t>(bitTime) * 5U / 2U;
const uint32_t badPeriodUs = static_cast<uint32_t>(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<uint32_t>(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<uint32_t>::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<uint8_t>(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<uint8_t>((crcLow & 1U) ^ (corruptCrc ? 1U : 0U));
decoder.dataBuffer[4] = static_cast<uint8_t>(crcLow & 0xFEU);
decoder.i_dataBuffer = wireBytes * bitPerByte - 1U;
decoder.bufBitPos = static_cast<int16_t>(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;
}