5 Commits

Author SHA1 Message Date
00e101990f Make IR timing and RX terminal state explicit 2026-09-04 19:22:03 +03:00
6c97d33c7c feat(rx): rxDeclaredPackSize/rxMaxPackSize + packAirtimeMs — бюджет удержания по размеру
Для гейта заднего у машинки (Даша 04.09): держать задний по СОСТОЯНИЮ приёма, но
не дольше airtime кадра — а если объявленный в 1-м байте размер МЕНЬШЕ протокольного
максимума, то ровно до его конца (усиление надёжности). rxDeclaredPackSize() =
packSize из 1-го байта, если валиден (не битый) и идёт приём, иначе 0 (потребитель
берёт rxMaxPackSize() — до CRC значение недоверенное). packAirtimeMs() — публичная
обёртка над calculateSendTime (чистая функция протокольных констант, годна и для RX).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3Ca6J2JJ1NrptqQjKs8BK
2026-09-04 15:51:59 +03:00
628c050702 feat(rx): rxLineActive() — признак «идёт валидный приём» по СОСТОЯНИЮ
Декодер уже знает состояние приёма (isReciving=локнут; preambleState Idle/
Candidate/Locked). rxLineActive() = локнут ИЛИ формируется ВАЛИДНАЯ преамбула
(>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного шумового
фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта
заднего у машинки: не стрелять, пока на линии идёт/формируется ответ точки —
решение по состоянию, а не по хардкод-длительности (Даша 04.09). Аддитивно, const.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3Ca6J2JJ1NrptqQjKs8BK
2026-09-04 15:09:27 +03:00
a589416cfc reconstruct: restore brave-tape3 decoder stage 2026-08-28 14:35:19 +03:00
e25feb6824 reconstruct: restore brave-tape3 packet types stage 2026-08-28 14:30:44 +03:00
14 changed files with 675 additions and 292 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
.vscode/*
bin/*
tests/*.exe
!.vscode/launch.json
log/*
/.vscode

View File

@ -162,7 +162,9 @@ void IR_Decoder::_tick()
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
isWaitingAcceptSend = true;
}
gotRaw.set(&packInfo, id);
// Raw keeps the decoder's common minimum-size contract. Known packet
// layouts are validated by their typed BasePack::set calls above.
gotRaw.set(&packInfo, id, false);
}
if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
{

View File

@ -374,15 +374,45 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
return busy;
}
bool IR_DecoderRaw::rxPipelineActive() const
{
return rxLineActive() || rxTimeoutPipelineBusy();
}
uint8_t IR_DecoderRaw::currentRxMsgType() const
{
if (i_dataBuffer < static_cast<uint16_t>(msgBytes) * bitPerByte)
return 0xFFU;
return static_cast<uint8_t>((dataBuffer[0] >> 5U) & IR_MASK_MSG_TYPE);
}
void IR_DecoderRaw::noteRxTerminal(IR_RxTerminalReason reason, uint8_t msgType, bool hadLock)
{
++rxTerminalInfo.seq;
rxTerminalInfo.reason = reason;
rxTerminalInfo.msgType = msgType;
rxTerminalInfo.hadLock = hadLock;
}
void IR_DecoderRaw::listenStart()
{
if (rxTimeoutPipelineBusy())
return;
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
const uint32_t nowUs = micros();
if (isReciveRaw && ((nowUs - lastEdgeTime) > IR_timeout * 2U))
{
#if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(true);
#endif
if (isRecive)
{
const uint16_t expected =
(i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, nowUs);
noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
isRecive = false;
msgTypeReceive = 0;
}
isReciveRaw = false;
firstRX();
}
@ -403,6 +433,7 @@ inline void IR_DecoderRaw::checkTimeout()
#endif
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
isRecive = false; // приём завершён
msgTypeReceive = 0;
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
@ -473,6 +504,7 @@ void IR_DecoderRaw::tick()
isSubBufferOverflow = false;
listenStart();
checkTimeout();
expirePreambleCandidateIfIdle(micros());
#if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif
@ -480,6 +512,7 @@ void IR_DecoderRaw::tick()
} // Если данных нет - ничего не делаем
listenStart();
checkTimeout();
expirePreambleCandidateIfIdle(micros());
#if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs();
#endif
@ -778,6 +811,13 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
}
if (isBufferOverflow || isPreamb || isWrongPack)
{
const bool hadLock =
isRecive || isReciveRaw || preambleState == PreambleState::Locked;
const bool wasObservable =
hadLock ||
(preambleState == PreambleState::Candidate && preambleWasObservable);
if (wasObservable)
noteRxTerminal(IR_RxTerminalReason::DecodeAbort, currentRxMsgType(), hadLock);
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
isRecive = false;
@ -941,6 +981,9 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#endif
}
#endif
noteRxTerminal(isAvailable ? IR_RxTerminalReason::FrameOk
: IR_RxTerminalReason::FrameCrcError,
currentRxMsgType(), true);
#if defined(IRDEBUG_SERIAL_PACK)
if (isAvailable)
packTraceEmitEndOk(static_cast<uint8_t>(packSize));
@ -1583,6 +1626,7 @@ void IR_DecoderRaw::preambleResetToIdle()
{
preambleState = PreambleState::Idle;
preambleGoodPeriods = 0;
preambleWasObservable = false;
preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = 0;
preambleCandidateFirstRiseTime = 0;
@ -1597,6 +1641,10 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
{
preambleState = PreambleState::Candidate;
preambleGoodPeriods = 0;
// The first post-silence rise already opens a potential frame epoch.
// Keep the line busy until that epoch locks or expires after real silence:
// even a badly distorted response may contain no coarse-valid rise period.
preambleWasObservable = true;
preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = front.time;
preambleCandidateFirstRiseTime = front.time;
@ -1607,6 +1655,24 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
isReciveRaw = false;
}
void IR_DecoderRaw::expirePreambleCandidateIfIdle(uint32_t nowUs)
{
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
return;
const uint32_t candTimeout =
IR_timeout * static_cast<uint32_t>(IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT);
if ((uint32_t)(nowUs - preambleCandidateLastEdgeTime) <= candTimeout)
return;
const uint8_t goodPeriods = preambleGoodPeriods;
const bool wasObservable = preambleWasObservable;
rxBriefLog(RxBriefReason::Preamble, goodPeriods, 0, nowUs);
preambleResetToIdle();
if (wasObservable)
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
}
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{
const uint32_t longSilence = IR_timeout * 2U;
@ -1622,7 +1688,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
if (!isReciveRaw && front.dir &&
((prevRise == 0U && front.time > longSilence) ||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
{
preambleStartCandidate(front);
return true;
}
}
if (preambleState == PreambleState::Candidate)
@ -1630,7 +1699,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
{
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
if (preambleWasObservable)
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
preambleStartCandidate(front);
return true;
}
preambleCandidateLastEdgeTime = front.time;
@ -1648,15 +1720,20 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
preambleCandidateFirstRiseTime = front.time;
if (!preambleRisePeriodCoarseOk(period))
{
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods,
irClampU16(period), front.time);
preambleGoodPeriods = 0;
preambleMeanPeriod = 0;
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time);
// Keep preambleWasObservable sticky: this edge proves the medium is
// still active, but not that a possible physical frame has ended.
// Only silence timeout or a real locked terminal releases it.
return true;
}
if (preambleGoodPeriods == 0)
{
preambleGoodPeriods = 1;
preambleWasObservable = true;
preambleMeanPeriod = (uint16_t)period;
}
else
@ -1673,6 +1750,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
preambleGoodPeriods = 1;
preambleWasObservable = true;
preambleMeanPeriod = (uint16_t)period;
}
}

View File

@ -27,6 +27,29 @@ class Print;
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
constexpr uint16_t IR_ResponseDelay = irproto::kMandatoryInterPacketQuietMs;
/** Why the most recent observable receive attempt reached a terminal state. */
enum class IR_RxTerminalReason : uint8_t
{
None = 0,
FrameOk,
FrameCrcError,
LockedTimeout,
DecodeAbort,
CandidateTimeout
};
/**
* Monotonic receive-completion snapshot for schedulers polling after decoder.tick().
* seq is allowed to wrap; consumers only compare it with their previous snapshot.
*/
struct IR_RxTerminalInfo
{
uint32_t seq = 0;
IR_RxTerminalReason reason = IR_RxTerminalReason::None;
uint8_t msgType = 0xFFU;
bool hadLock = false;
};
class IR_Encoder;
class IR_DecoderRaw : virtual public IR_FOX
{
@ -52,6 +75,38 @@ public:
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
bool isSubOverflow();
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ открыт
// Candidate после первого post-silence rise. Даже сильно искажённый ответ может не дать ни
// одного coarse-valid периода, поэтому Candidate остаётся активным до lock/terminal либо
// доказанной тишины по candidate timeout. Для гейта заднего: «не стрелять, пока на линии
// идёт/формируется потенциальный кадр (напр. ответ точки)». Аддитивно, const.
inline bool rxLineActive() const {
return isRecive ||
(preambleState == PreambleState::Candidate && preambleWasObservable);
}
/**
* True while a real frame is active or ISR/filter work is still queued.
* This closes the one-loop ordering gap when Timer::tick() runs before
* decoder.tick(): a transmitter must not start while an unprocessed edge
* is already waiting in the receive pipeline.
*/
bool rxPipelineActive() const;
/**
* Last terminal RX transition. Updated from tick()/decode context, never from ISR.
* A frame that starts and finishes within one tick is observable through seq.
*/
IR_RxTerminalInfo rxLastTerminal() const { return rxTerminalInfo; }
uint32_t rxTerminalSeq() const { return rxTerminalInfo.seq; }
// Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден;
// иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
inline uint16_t rxDeclaredPackSize() const {
return (isRecive && packSize && !isWrongPack) ? packSize : 0;
}
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
static constexpr uint16_t rxMaxPackSize() {
return static_cast<uint16_t>(irproto::kMaxWireFrameBytes);
}
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
@ -111,6 +166,7 @@ private:
volatile bool isSubBufferOverflow = false;
bool isBufferOverflow = false; // Флаг переполнения буффера данных
bool isWrongPack = false; // Флаг битого пакета
IR_RxTerminalInfo rxTerminalInfo;
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
@ -148,6 +204,10 @@ private:
};
PreambleState preambleState = PreambleState::Idle;
uint8_t preambleGoodPeriods = 0;
// Sticky potential-frame latch for one Candidate epoch. After one plausible
// rise period, coarse-invalid activity remains busy until proven silence;
// the scheduler's hard deadline may skip an optional tail under noise.
bool preambleWasObservable = false;
uint16_t preambleMeanPeriod = 0;
uint32_t preambleCandidateLastEdgeTime = 0;
uint32_t preambleCandidateFirstRiseTime = 0;
@ -216,6 +276,9 @@ bool isReciveRaw = false;
void preambleResetToIdle();
void preambleStartCandidate(const FrontStorage &front);
bool preambleProcessEdge(const FrontStorage &front);
void expirePreambleCandidateIfIdle(uint32_t nowUs);
uint8_t currentRxMsgType() const;
void noteRxTerminal(IR_RxTerminalReason reason, uint8_t msgType, bool hadLock);
/// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
/// @param len Длина в байтах проверяемых данных

View File

@ -1184,8 +1184,7 @@ uint8_t IR_Encoder::bitLow[2] = {
uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const
{
// The TX FSM emits syncBits after every wire byte (including the last)
// and its preamble runs are preambToggle+1 logical ticks long. The old
// approximation omitted the per-byte sync and shortened the preamble.
// and its preamble runs are preambToggle+1 logical ticks long.
return irproto::wireAirtimeMsCeil(packSize);
}

View File

@ -62,6 +62,10 @@ public:
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
/// (аналог setBlindDecoders() для одного RX).
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
/// Публичная оценка airtime кадра (мс) по его полной длине в байтах (packSize). Чистая функция
/// протокольных констант — подходит и для приёма (напр. бюджет удержания заднего по объявленному
/// в 1-м байте размеру принимаемого ответа). БЕЗ +30% компенсации занижения sync — добавляет потребитель.
uint32_t packAirtimeMs(uint8_t packSize) const { return calculateSendTime(packSize); }
static void isr();
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
/**

View File

@ -327,8 +327,8 @@ constexpr bool isTypedWireSizeValid(uint8_t msgType, uint8_t wireBytes)
/*
* TX FSM timing contract.
*
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
* constant runs; each run is preambToggle+1 ticks. Every data bit and every
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
* constant runs; each run is preambToggle+1 ticks. Every data bit and every
* per-byte sync bit occupies bitTakts*2 ticks, independently of its value.
*/
constexpr uint32_t kTxLogicalClockHz = static_cast<uint32_t>(carrierFrec) * 2U;
@ -360,6 +360,17 @@ constexpr uint32_t preambleAirtimeUsCeil()
return logicalTicksToUsCeil(kPreambleLogicalTicks);
}
// Decoder completion is published after the final data bit, while the TX FSM
// still emits the last byte's sync bits. Callers that schedule a following
// packet from a decoder terminal must include this physical tail.
constexpr uint32_t kTrailingByteSyncLogicalTicks =
static_cast<uint32_t>(syncBits) * kEncodedBitLogicalTicks;
constexpr uint32_t trailingByteSyncAirtimeUsCeil()
{
return logicalTicksToUsCeil(kTrailingByteSyncLogicalTicks);
}
/** Complete nominal on-air duration, rounded up to a whole microsecond. */
constexpr uint32_t wireAirtimeUsCeil(uint8_t wireBytes)
{
@ -384,10 +395,28 @@ constexpr uint16_t kMandatoryInterPacketQuietMs =
constexpr uint32_t kMandatoryInterPacketQuietUs =
static_cast<uint32_t>(kMandatoryInterPacketQuietMs) * 1000U;
constexpr uint32_t completedFrameTerminalToNextPacketGuardUs(
uint16_t requestedQuietMs)
{
const uint16_t quietMs = requestedQuietMs > kMandatoryInterPacketQuietMs
? requestedQuietMs
: kMandatoryInterPacketQuietMs;
return trailingByteSyncAirtimeUsCeil() +
static_cast<uint32_t>(quietMs) * 1000U;
}
constexpr uint32_t completedFrameTerminalToNextPacketGuardMsCeil(
uint16_t requestedQuietMs)
{
return (completedFrameTerminalToNextPacketGuardUs(requestedQuietMs) + 999U) /
1000U;
}
constexpr uint16_t kDefaultTimingGuardPermille = 1150U;
constexpr uint32_t addTimingGuardUs(uint32_t durationUs,
uint16_t marginPermille = kDefaultTimingGuardPermille)
constexpr uint32_t addTimingGuardUs(
uint32_t durationUs,
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return marginPermille == 0U
? 0U
@ -419,6 +448,7 @@ static_assert(kMaxDataPayloadBytes == 24U, "DATA payload contract changed");
static_assert(kMaxBackPayloadBytes == 26U, "BACK payload contract changed");
static_assert(kPreambleLogicalTicks == 588U, "preamble timing contract changed");
static_assert(kWireByteLogicalTicks == 814U, "wire-byte timing contract changed");
static_assert(kTrailingByteSyncLogicalTicks == 222U, "trailing sync timing changed");
static_assert(kMandatoryInterPacketQuietMs == 42U, "inter-packet quiet policy changed");
}

View File

@ -2,28 +2,47 @@
namespace PacketTypes
{
bool BasePack::checkPacketLayout() const
uint8_t minimumPacketSize(uint8_t msgType)
{
if (packInfo == nullptr || packInfo->buffer == nullptr ||
packInfo->packSize < msgBytes + crcBytes ||
packInfo->packSize > irproto::kMaxWireFrameBytes)
switch (msgType)
{
return false;
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;
}
return (packInfo->buffer[msgOffset] & IR_MASK_MSG_INFO) == packInfo->packSize;
}
bool BasePack::checkAddress() { return true; };
void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id)
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize)
{
const uint8_t minimum = minimumPacketSize(msgType);
return minimum != 0 && packSize >= minimum;
}
bool BasePack::checkAddress() { return true; }
bool BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize)
{
isAvailable = false;
isRawAvailable = false;
this->packInfo = packInfo;
this->id = id;
if (!checkPacketLayout())
if (packInfo == nullptr || packInfo->buffer == nullptr)
{
return;
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
if (requireTypedSize && !isTypedPacketSizeValid(msgType, packInfo->packSize))
{
return false;
}
if (checkAddress())
@ -41,63 +60,65 @@ namespace PacketTypes
Serial.print(" NOT-OK ");
#endif
}
return isAvailable;
}
uint16_t BasePack::_getAddrFrom(BasePack *obj)
{
if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
static_cast<uint16_t>(obj->addressFromOffset) + 1U >=
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
uint16_t(obj->addressFromOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
{
return 0U;
return 0;
}
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
};
}
uint16_t BasePack::_getAddrTo(BasePack *obj)
{
if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
static_cast<uint16_t>(obj->addressToOffset) + 1U >=
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
uint16_t(obj->addressToOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
{
return 0U;
return 0;
}
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
};
}
uint8_t BasePack::_getDataSize(BasePack *obj)
{
if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
{
return 0U;
return 0;
}
const uint16_t overhead = static_cast<uint16_t>(obj->DataOffset) + crcBytes;
return static_cast<uint16_t>(obj->packInfo->packSize) > overhead
? static_cast<uint8_t>(static_cast<uint16_t>(obj->packInfo->packSize) - overhead)
: 0U;
};
const uint16_t frameOverhead = uint16_t(crcBytes) + uint16_t(obj->DataOffset);
if (uint16_t(obj->packInfo->packSize) <= frameOverhead)
{
return 0;
}
return uint8_t(uint16_t(obj->packInfo->packSize) - frameOverhead);
}
uint8_t *BasePack::_getDataPrt(BasePack *obj)
{
if (obj == nullptr || !obj->checkPacketLayout() ||
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
static_cast<uint16_t>(obj->DataOffset) >
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes)
{
return nullptr;
}
const uint16_t dataEnd = uint16_t(obj->packInfo->packSize) - uint16_t(crcBytes);
if (uint16_t(obj->DataOffset) > dataEnd)
{
return nullptr;
}
return obj->packInfo->buffer + obj->DataOffset;
};
}
uint8_t BasePack::_getDataRawSize(BasePack *obj)
{
return obj != nullptr && obj->checkPacketLayout() && obj->packInfo != nullptr
? obj->packInfo->packSize
: 0U;
};
if (obj == nullptr || obj->packInfo == nullptr)
{
return 0;
}
return obj->packInfo->packSize;
}
bool BasePack::available()
{
@ -111,7 +132,7 @@ namespace PacketTypes
{
return false;
}
};
}
bool BasePack::availableRaw()
{
if (isRawAvailable)
@ -123,17 +144,6 @@ namespace PacketTypes
{
return false;
}
};
bool Data::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return (msgType == IR_MSG_DATA_ACCEPT || msgType == IR_MSG_DATA_NOACCEPT) &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool Data::checkAddress()
@ -143,17 +153,6 @@ namespace PacketTypes
return ret;
}
bool DataBack::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return (msgType == IR_MSG_BACK || msgType == IR_MSG_BACK_TO) &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool DataBack::checkAddress()
{
bool ret;
@ -170,30 +169,8 @@ namespace PacketTypes
return ret;
}
bool Accept::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return msgType == IR_MSG_ACCEPT &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool Accept::checkAddress() { return true; }
bool Request::checkPacketLayout() const
{
if (!BasePack::checkPacketLayout())
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
return msgType == IR_MSG_REQUEST &&
irproto::isTypedWireSizeValid(msgType, packInfo->packSize);
}
bool Request::checkAddress()
{
bool ret;

View File

@ -4,6 +4,15 @@
class IR_Decoder;
namespace PacketTypes
{
/**
* Minimum complete frame size (header, addresses/data required by the type,
* and CRC). Unknown/reserved message types return 0.
*/
uint8_t minimumPacketSize(uint8_t msgType);
/** True only for a known typed packet whose complete frame is long enough. */
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize);
class BasePack
{
friend IR_Decoder;
@ -21,9 +30,8 @@ namespace PacketTypes
IR_FOX::PackInfo *packInfo = nullptr;
uint16_t id = 0;
virtual bool checkPacketLayout() const;
virtual bool checkAddress();
void set(IR_FOX::PackInfo *packInfo, uint16_t id);
bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true);
static uint16_t _getAddrFrom(BasePack *obj);
static uint16_t _getAddrTo(BasePack *obj);
@ -35,9 +43,9 @@ namespace PacketTypes
bool available();
bool availableRaw();
inline uint8_t getMsgInfo() { return packInfo != nullptr && packInfo->buffer != nullptr ? packInfo->buffer[0] & IR_MASK_MSG_INFO : 0U; };
inline uint8_t getMsgType() { return packInfo != nullptr && packInfo->buffer != nullptr ? (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE : 0U; };
inline uint8_t getMsgRAW() { return packInfo != nullptr && packInfo->buffer != nullptr ? packInfo->buffer[0] : 0U; };
inline uint8_t getMsgInfo() { return packInfo->buffer[0] & IR_MASK_MSG_INFO; };
inline uint8_t getMsgType() { return (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE; };
inline uint8_t getMsgRAW() { return packInfo->buffer[0]; };
inline uint16_t getErrorCount() { return packInfo->err.all(); };
inline uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; };
inline uint8_t getErrorHighSignal() { return packInfo->err.highSignal; };
@ -66,7 +74,6 @@ namespace PacketTypes
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
private:
bool checkPacketLayout() const override;
bool checkAddress() override;
};
@ -88,7 +95,6 @@ namespace PacketTypes
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
private:
bool checkPacketLayout() const override;
bool checkAddress() override;
};
@ -106,7 +112,6 @@ namespace PacketTypes
inline uint8_t getCustomByte() { return packInfo->buffer[DataOffset]; };
private:
bool checkPacketLayout() const override;
bool checkAddress() override;
};
@ -125,7 +130,6 @@ namespace PacketTypes
inline uint16_t getAddrTo() { return _getAddrTo(this); };
private:
bool checkPacketLayout() const override;
bool checkAddress() override;
};

View File

@ -3,6 +3,9 @@
#include <cstddef>
#include <cstdint>
class __FlashStringHelper;
#define F(value) reinterpret_cast<const __FlashStringHelper *>(value)
struct GPIO_TypeDef
{
uint32_t BSRR = 0U;
@ -32,19 +35,26 @@ private:
uint32_t overflow_ = 1U;
};
inline GPIO_TypeDef *digitalPinToPort(uint8_t) { return nullptr; }
inline uint16_t digitalPinToBitMask(uint8_t) { return 0U; }
inline GPIO_TypeDef arduinoStubPort;
inline GPIO_TypeDef *digitalPinToPort(uint8_t) { return &arduinoStubPort; }
inline uint16_t digitalPinToBitMask(uint8_t) { return 1U; }
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
inline uint32_t arduinoStubMicros = 0U;
inline uint32_t micros() { return arduinoStubMicros; }
inline uint32_t millis() { return arduinoStubMicros / 1000U; }
class Print
{
public:
template <typename T> void print(const T &) {}
template <typename T> void println(const T &) {}
void println() {}
};
using ArduinoSerialStub = Print;
inline ArduinoSerialStub Serial;

View File

@ -1,38 +0,0 @@
$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
$build = Join-Path $PSScriptRoot '.build'
New-Item -ItemType Directory -Force -Path $build | Out-Null
$compiler = if (Test-Path -LiteralPath 'C:\MinGW\bin\g++.exe') {
'C:\MinGW\bin\g++.exe'
} else {
(Get-Command g++ -ErrorAction Stop).Source
}
$common = @(
'-std=c++17', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-Wno-ignored-qualifiers', '-Wno-sign-compare',
'-I', (Join-Path $PSScriptRoot 'arduino_stubs'),
'-I', $repo
)
& $compiler @common `
(Join-Path $PSScriptRoot 'test_timing_contract.cpp') `
(Join-Path $repo 'IR_Encoder.cpp') `
(Join-Path $repo 'IR_config.cpp') `
'-o' (Join-Path $build 'test_timing_contract.exe')
if ($LASTEXITCODE -ne 0) { throw 'timing test build failed' }
& (Join-Path $build 'test_timing_contract.exe')
if ($LASTEXITCODE -ne 0) { throw 'timing test failed' }
& $compiler @common `
(Join-Path $PSScriptRoot 'test_packet_types.cpp') `
(Join-Path $repo 'PacketTypes.cpp') `
(Join-Path $repo 'IR_config.cpp') `
'-o' (Join-Path $build 'test_packet_types.exe')
if ($LASTEXITCODE -ne 0) { throw 'packet test build failed' }
& (Join-Path $build 'test_packet_types.exe')
if ($LASTEXITCODE -ne 0) { throw 'packet test failed' }

View File

@ -1,145 +0,0 @@
#include "PacketTypes.h"
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
namespace
{
template <typename Packet>
class ExposedPacket : public Packet
{
public:
void attach(IR_FOX::PackInfo *info, uint16_t id = 0U)
{
this->set(info, id);
}
};
IR_FOX::PackInfo makeFrame(uint8_t *buffer, uint8_t msgType, uint8_t wireBytes)
{
buffer[0] = static_cast<uint8_t>((msgType << 5) | (wireBytes & IR_MASK_MSG_INFO));
IR_FOX::PackInfo result;
result.buffer = buffer;
result.packSize = wireBytes;
return result;
}
template <typename Packet>
void verifyMinimum(uint8_t msgType, uint8_t minimum)
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<Packet> packet;
IR_FOX::PackInfo shortFrame = makeFrame(buffer.data(), msgType, minimum - 1U);
packet.attach(&shortFrame);
assert(!packet.available());
assert(!packet.availableRaw());
IR_FOX::PackInfo minimumFrame = makeFrame(buffer.data(), msgType, minimum);
packet.attach(&minimumFrame);
assert(packet.available());
}
void verifyTypedMinimums()
{
verifyMinimum<PacketTypes::Data>(IR_MSG_DATA_ACCEPT, 7U);
verifyMinimum<PacketTypes::Data>(IR_MSG_DATA_NOACCEPT, 7U);
verifyMinimum<PacketTypes::DataBack>(IR_MSG_BACK, 5U);
verifyMinimum<PacketTypes::DataBack>(IR_MSG_BACK_TO, 7U);
verifyMinimum<PacketTypes::Accept>(IR_MSG_ACCEPT, 6U);
verifyMinimum<PacketTypes::Request>(IR_MSG_REQUEST, 7U);
}
void verifyDataAccessCannotUnderflow()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<PacketTypes::Data> data;
for (uint8_t wireBytes = 0U; wireBytes < irproto::kDataFrameOverheadBytes; ++wireBytes)
{
IR_FOX::PackInfo malformed = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, wireBytes);
data.attach(&malformed);
assert(!data.available());
assert(data.getDataSize() == 0U);
assert(data.getDataPrt() == nullptr);
assert(data.getAddrTo() == 0U);
}
IR_FOX::PackInfo empty = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, 7U);
data.attach(&empty);
assert(data.available());
assert(data.getDataSize() == 0U);
assert(data.getDataPrt() == buffer.data() + 5U);
IR_FOX::PackInfo oneByte = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, 8U);
data.attach(&oneByte);
assert(data.available());
assert(data.getDataSize() == 1U);
assert(data.getDataPrt() == buffer.data() + 5U);
}
void verifyBackLayouts()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<PacketTypes::DataBack> back;
IR_FOX::PackInfo shortBroadcast = makeFrame(buffer.data(), IR_MSG_BACK, 4U);
back.attach(&shortBroadcast);
assert(!back.available());
assert(back.getDataSize() == 0U);
assert(back.getDataPrt() == nullptr);
IR_FOX::PackInfo broadcast = makeFrame(buffer.data(), IR_MSG_BACK, 5U);
back.attach(&broadcast);
assert(back.available());
assert(back.getDataSize() == 0U);
assert(back.getDataPrt() == buffer.data() + 3U);
IR_FOX::PackInfo shortAddressed = makeFrame(buffer.data(), IR_MSG_BACK_TO, 6U);
back.attach(&shortAddressed);
assert(!back.available());
assert(back.getDataSize() == 0U);
IR_FOX::PackInfo addressed = makeFrame(buffer.data(), IR_MSG_BACK_TO, 7U);
back.attach(&addressed);
assert(back.available());
assert(back.getDataSize() == 0U);
assert(back.getDataPrt() == buffer.data() + 5U);
}
void verifyRawAndHeaderContracts()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> buffer{};
ExposedPacket<PacketTypes::BasePack> raw;
// Raw diagnostics remain able to observe a CRC-sized frame even when its
// declared type-specific layout is too short.
IR_FOX::PackInfo shortTyped = makeFrame(buffer.data(), IR_MSG_DATA_ACCEPT, 3U);
raw.attach(&shortTyped);
assert(raw.availableRaw());
IR_FOX::PackInfo inconsistent = makeFrame(buffer.data(), IR_MSG_BACK, 5U);
inconsistent.packSize = 6U;
raw.attach(&inconsistent);
assert(!raw.available());
assert(!raw.availableRaw());
IR_FOX::PackInfo nullFrame;
nullFrame.packSize = 31U;
raw.attach(&nullFrame);
assert(!raw.available());
assert(!raw.availableRaw());
}
}
int main()
{
verifyTypedMinimums();
verifyDataAccessCannotUnderflow();
verifyBackLayouts();
verifyRawAndHeaderContracts();
std::cout << "IR packet boundary tests: OK\n";
return 0;
}

386
tests/test_rx_terminal.cpp Normal file
View File

@ -0,0 +1,386 @@
#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;
}

View File

@ -27,11 +27,23 @@ static_assert(irproto::wireLogicalTicks(6U) == 5472U, "6-byte tick count changed
static_assert(irproto::wireLogicalTicks(10U) == 8728U, "10-byte tick count changed");
static_assert(irproto::wireLogicalTicks(31U) == 25822U, "31-byte tick count changed");
static_assert(irproto::preambleAirtimeUsCeil() == 7737U, "preamble airtime changed");
static_assert(irproto::trailingByteSyncAirtimeUsCeil() == 2922U,
"trailing sync airtime changed");
static_assert(irproto::wireAirtimeUsCeil(6U) == 72000U, "6-byte airtime changed");
static_assert(irproto::wireAirtimeUsCeil(10U) == 114843U, "10-byte airtime changed");
static_assert(irproto::wireAirtimeUsCeil(31U) == 339764U, "31-byte airtime changed");
static_assert(irproto::responseStartGuardUs() == 57198U, "response-start guard changed");
static_assert(irproto::responseFrameGuardUs(6U) == 131100U, "response-frame guard changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardUs(0U) == 44922U,
"mandatory physical post-terminal quiet changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(0U) == 45U,
"mandatory post-terminal guard rounding changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(60U) == 63U,
"configured post-terminal guard changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(65535U) ==
65538U,
"large guard must not wrap uint16");
static_assert(IR_DecoderRaw::rxMaxPackSize() == 31U, "RX max must be the wire max");
uint32_t sumLogicalTicks(const IrTxGateRun *runs, size_t count)
{
@ -70,9 +82,9 @@ void verifyPublicSendTimeResults()
IR_Encoder encoder(1U, 42U, nullptr, false);
uint8_t payload[26]{};
assert(encoder.testSendAccept(1U) == 72U); // six-byte wire frame
assert(encoder.testSendTime(1U, payload, 3U) == 115U); // ten-byte wire frame
assert(encoder.testSendBack(payload, 26U) == 340U); // 31-byte wire frame
assert(encoder.testSendAccept(1U) == 72U);
assert(encoder.testSendTime(1U, payload, 3U) == 115U);
assert(encoder.testSendBack(payload, 26U) == 340U);
}
}