1 Commits

Author SHA1 Message Date
00e101990f Make IR timing and RX terminal state explicit 2026-09-04 19:22:03 +03:00
8 changed files with 869 additions and 29 deletions

1
.gitignore vendored
View File

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

View File

@ -374,15 +374,45 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
return busy; 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() void IR_DecoderRaw::listenStart()
{ {
if (rxTimeoutPipelineBusy()) if (rxTimeoutPipelineBusy())
return; 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) #if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(true); packTraceOnTimeoutOrAbort(true);
#endif #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; isReciveRaw = false;
firstRX(); firstRX();
} }
@ -403,6 +433,7 @@ inline void IR_DecoderRaw::checkTimeout()
#endif #endif
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U; const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros()); rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
isRecive = false; // приём завершён isRecive = false; // приём завершён
msgTypeReceive = 0; msgTypeReceive = 0;
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
@ -473,6 +504,7 @@ void IR_DecoderRaw::tick()
isSubBufferOverflow = false; isSubBufferOverflow = false;
listenStart(); listenStart();
checkTimeout(); checkTimeout();
expirePreambleCandidateIfIdle(micros());
#if defined(IR_EDGE_TRACE) #if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {} while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif #endif
@ -480,6 +512,7 @@ void IR_DecoderRaw::tick()
} // Если данных нет - ничего не делаем } // Если данных нет - ничего не делаем
listenStart(); listenStart();
checkTimeout(); checkTimeout();
expirePreambleCandidateIfIdle(micros());
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs(); rxBriefFlushDeferredIsrLogs();
#endif #endif
@ -778,6 +811,13 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
} }
if (isBufferOverflow || isPreamb || isWrongPack) 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() сбрасывает буфер битов, преамбулу и // Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered. // pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
isRecive = false; isRecive = false;
@ -941,6 +981,9 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#endif #endif
} }
#endif #endif
noteRxTerminal(isAvailable ? IR_RxTerminalReason::FrameOk
: IR_RxTerminalReason::FrameCrcError,
currentRxMsgType(), true);
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
if (isAvailable) if (isAvailable)
packTraceEmitEndOk(static_cast<uint8_t>(packSize)); packTraceEmitEndOk(static_cast<uint8_t>(packSize));
@ -1583,6 +1626,7 @@ void IR_DecoderRaw::preambleResetToIdle()
{ {
preambleState = PreambleState::Idle; preambleState = PreambleState::Idle;
preambleGoodPeriods = 0; preambleGoodPeriods = 0;
preambleWasObservable = false;
preambleMeanPeriod = 0; preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = 0; preambleCandidateLastEdgeTime = 0;
preambleCandidateFirstRiseTime = 0; preambleCandidateFirstRiseTime = 0;
@ -1597,6 +1641,10 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
{ {
preambleState = PreambleState::Candidate; preambleState = PreambleState::Candidate;
preambleGoodPeriods = 0; 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; preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = front.time; preambleCandidateLastEdgeTime = front.time;
preambleCandidateFirstRiseTime = front.time; preambleCandidateFirstRiseTime = front.time;
@ -1607,6 +1655,24 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
isReciveRaw = false; 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) bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{ {
const uint32_t longSilence = IR_timeout * 2U; const uint32_t longSilence = IR_timeout * 2U;
@ -1622,7 +1688,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
if (!isReciveRaw && front.dir && if (!isReciveRaw && front.dir &&
((prevRise == 0U && front.time > longSilence) || ((prevRise == 0U && front.time > longSilence) ||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence))) (prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
{
preambleStartCandidate(front); preambleStartCandidate(front);
return true;
}
} }
if (preambleState == PreambleState::Candidate) if (preambleState == PreambleState::Candidate)
@ -1630,7 +1699,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout) if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
{ {
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time); rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
if (preambleWasObservable)
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
preambleStartCandidate(front); preambleStartCandidate(front);
return true;
} }
preambleCandidateLastEdgeTime = front.time; preambleCandidateLastEdgeTime = front.time;
@ -1648,15 +1720,20 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
preambleCandidateFirstRiseTime = front.time; preambleCandidateFirstRiseTime = front.time;
if (!preambleRisePeriodCoarseOk(period)) if (!preambleRisePeriodCoarseOk(period))
{ {
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods,
irClampU16(period), front.time);
preambleGoodPeriods = 0; preambleGoodPeriods = 0;
preambleMeanPeriod = 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; return true;
} }
if (preambleGoodPeriods == 0) if (preambleGoodPeriods == 0)
{ {
preambleGoodPeriods = 1; preambleGoodPeriods = 1;
preambleWasObservable = true;
preambleMeanPeriod = (uint16_t)period; preambleMeanPeriod = (uint16_t)period;
} }
else else
@ -1673,6 +1750,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{ {
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time); rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
preambleGoodPeriods = 1; preambleGoodPeriods = 1;
preambleWasObservable = true;
preambleMeanPeriod = (uint16_t)period; preambleMeanPeriod = (uint16_t)period;
} }
} }

View File

@ -25,7 +25,30 @@ class Print;
#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 #define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; 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_Encoder;
class IR_DecoderRaw : virtual public IR_FOX class IR_DecoderRaw : virtual public IR_FOX
@ -52,14 +75,28 @@ public:
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
bool isSubOverflow(); bool isSubOverflow();
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется // Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ открыт
// ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного // Candidate после первого post-silence rise. Даже сильно искажённый ответ может не дать ни
// шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего: // одного coarse-valid периода, поэтому Candidate остаётся активным до lock/terminal либо
// «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const. // доказанной тишины по candidate timeout. Для гейта заднего: «не стрелять, пока на линии
// идёт/формируется потенциальный кадр (напр. ответ точки)». Аддитивно, const.
inline bool rxLineActive() const { inline bool rxLineActive() const {
return isRecive || return isRecive ||
(preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U); (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 // иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра). // или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
@ -67,7 +104,9 @@ public:
return (isRecive && packSize && !isWrongPack) ? packSize : 0; return (isRecive && packSize && !isWrongPack) ? packSize : 0;
} }
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего. // Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
static constexpr uint16_t rxMaxPackSize() { return (uint16_t)dataByteSizeMax; } static constexpr uint16_t rxMaxPackSize() {
return static_cast<uint16_t>(irproto::kMaxWireFrameBytes);
}
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; }
@ -127,6 +166,7 @@ private:
volatile bool isSubBufferOverflow = false; volatile bool isSubBufferOverflow = false;
bool isBufferOverflow = false; // Флаг переполнения буффера данных bool isBufferOverflow = false; // Флаг переполнения буффера данных
bool isWrongPack = false; // Флаг битого пакета bool isWrongPack = false; // Флаг битого пакета
IR_RxTerminalInfo rxTerminalInfo;
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
@ -164,6 +204,10 @@ private:
}; };
PreambleState preambleState = PreambleState::Idle; PreambleState preambleState = PreambleState::Idle;
uint8_t preambleGoodPeriods = 0; 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; uint16_t preambleMeanPeriod = 0;
uint32_t preambleCandidateLastEdgeTime = 0; uint32_t preambleCandidateLastEdgeTime = 0;
uint32_t preambleCandidateFirstRiseTime = 0; uint32_t preambleCandidateFirstRiseTime = 0;
@ -232,6 +276,9 @@ bool isReciveRaw = false;
void preambleResetToIdle(); void preambleResetToIdle();
void preambleStartCandidate(const FrontStorage &front); void preambleStartCandidate(const FrontStorage &front);
bool preambleProcessEdge(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, пришедшим в пакете /// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
/// @param len Длина в байтах проверяемых данных /// @param len Длина в байтах проверяемых данных

View File

@ -1183,26 +1183,9 @@ uint8_t IR_Encoder::bitLow[2] = {
uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const 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.
// Время преамбулы: preambPulse * 2 фронта * bitTakts тактов return irproto::wireAirtimeMsCeil(packSize);
uint32_t preambTime = preambPulse * 2 * bitTakts;
// Время данных: количество бит * bitTakts тактов
uint32_t dataTime = packSize * 8 * bitTakts;
// Время синхронизации: syncBits * 2 фронта * bitTakts тактов
uint32_t syncTime = syncBits * 2 * bitTakts;
// Общее время в тактах
uint32_t totalTakts = preambTime + dataTime + syncTime;
// Конвертируем в миллисекунды
// carrierPeriod - период несущей в микросекундах
// totalTakts * carrierPeriod / 1000 = время в миллисекундах
uint32_t sendTimeMs = (totalTakts * carrierPeriod) / 1000;
return sendTimeMs;
} }
// Функции для тестирования времени отправки без фактической отправки // Функции для тестирования времени отправки без фактической отправки

View File

@ -264,6 +264,194 @@ typedef uint16_t crc_t;
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
#define tolerance 300U #define tolerance 300U
namespace irproto
{
/** Maximum complete frame length representable by the five header bits. */
constexpr uint8_t kMaxWireFrameBytes = static_cast<uint8_t>(IR_MASK_MSG_INFO);
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 kAcceptFrameBytes = msgBytes + addrBytes + 1U + crcBytes;
constexpr uint8_t kRequestFrameBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
/** Complete DATA frame size, or zero when payloadBytes cannot fit on wire. */
constexpr uint8_t dataWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxDataPayloadBytes
? static_cast<uint8_t>(kDataFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Complete non-addressed BACK frame size, or zero when it cannot fit. */
constexpr uint8_t backWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackPayloadBytes
? static_cast<uint8_t>(kBackFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Complete addressed BACK_TO frame size, or zero when it cannot fit. */
constexpr uint8_t backToWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackToPayloadBytes
? static_cast<uint8_t>(kBackToFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Minimum complete frame size for a known message type; zero means reserved/unknown. */
constexpr uint8_t minimumWireBytes(uint8_t msgType)
{
return (msgType == IR_MSG_DATA_ACCEPT || msgType == IR_MSG_DATA_NOACCEPT)
? kDataFrameOverheadBytes
: msgType == IR_MSG_BACK
? kBackFrameOverheadBytes
: (msgType == IR_MSG_BACK_TO || msgType == IR_MSG_REQUEST)
? kRequestFrameBytes
: msgType == IR_MSG_ACCEPT
? kAcceptFrameBytes
: 0U;
}
constexpr bool isTypedWireSizeValid(uint8_t msgType, uint8_t wireBytes)
{
return minimumWireBytes(msgType) != 0U &&
wireBytes >= minimumWireBytes(msgType) &&
wireBytes <= kMaxWireFrameBytes;
}
/*
* 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
* per-byte sync bit occupies bitTakts*2 ticks, independently of its value.
*/
constexpr uint32_t kTxLogicalClockHz = static_cast<uint32_t>(carrierFrec) * 2U;
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 kWireByteLogicalTicks =
static_cast<uint32_t>(bitPerByte + syncBits) * kEncodedBitLogicalTicks;
constexpr uint32_t wireLogicalTicks(uint8_t wireBytes)
{
return wireBytes != 0U && wireBytes <= kMaxWireFrameBytes
? kPreambleLogicalTicks + static_cast<uint32_t>(wireBytes) * kWireByteLogicalTicks
: 0U;
}
constexpr uint32_t logicalTicksToUsCeil(uint32_t logicalTicks)
{
return logicalTicks == 0U
? 0U
: static_cast<uint32_t>(
(static_cast<uint64_t>(logicalTicks) * 1000000ULL +
static_cast<uint64_t>(kTxLogicalClockHz) - 1ULL) /
static_cast<uint64_t>(kTxLogicalClockHz));
}
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)
{
return logicalTicksToUsCeil(wireLogicalTicks(wireBytes));
}
constexpr uint32_t wireAirtimeMsCeil(uint8_t wireBytes)
{
return wireAirtimeUsCeil(wireBytes) == 0U
? 0U
: (wireAirtimeUsCeil(wireBytes) + 999U) / 1000U;
}
/* Preserve the deployed library turn-around policy, but expose it by name. */
constexpr uint16_t kMandatoryInterPacketQuietMs =
static_cast<uint16_t>(
static_cast<uint16_t>(
(static_cast<uint32_t>(bitTime + tolerance) *
static_cast<uint32_t>(bitPerByte + syncBits + 1U)) *
2.7735) /
1000U);
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)
{
return marginPermille == 0U
? 0U
: static_cast<uint32_t>(
(static_cast<uint64_t>(durationUs) * marginPermille + 999ULL) / 1000ULL);
}
/** Deadline for seeing enough preamble to know that a response has started. */
constexpr uint32_t responseStartGuardUs(
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return addTimingGuardUs(kMandatoryInterPacketQuietUs + preambleAirtimeUsCeil(),
marginPermille);
}
/** Conservative deadline for receiving a complete response of maxWireBytes. */
constexpr uint32_t responseFrameGuardUs(
uint8_t maxWireBytes,
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return wireAirtimeUsCeil(maxWireBytes) == 0U
? 0U
: addTimingGuardUs(kMandatoryInterPacketQuietUs +
wireAirtimeUsCeil(maxWireBytes),
marginPermille);
}
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");
}
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;
constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0); constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0);

View File

@ -0,0 +1,60 @@
#pragma once
#include <cstddef>
#include <cstdint>
class __FlashStringHelper;
#define F(value) reinterpret_cast<const __FlashStringHelper *>(value)
struct GPIO_TypeDef
{
uint32_t BSRR = 0U;
uint32_t IDR = 0U;
};
using IRQn_Type = int;
enum TimerFormat_t : uint8_t { TICK_FORMAT = 0, MICROSEC_FORMAT, HERTZ_FORMAT };
constexpr uint8_t LOW = 0U;
constexpr uint8_t HIGH = 1U;
constexpr uint8_t INPUT = 0U;
constexpr uint8_t OUTPUT = 1U;
class HardwareTimer
{
public:
void pause() {}
void resume() {}
void setOverflow(uint32_t value, TimerFormat_t = TICK_FORMAT) { overflow_ = value; }
uint32_t getOverflow(TimerFormat_t = TICK_FORMAT) { return overflow_; }
uint32_t getPrescaleFactor() { return 1U; }
uint32_t getTimerClkFreq() { return 12000000U; }
void attachInterrupt(uint8_t, void (*)()) {}
private:
uint32_t overflow_ = 1U;
};
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() {}
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;

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

@ -0,0 +1,97 @@
#include "IR_Encoder.h"
#include "IR_DecoderRaw.h"
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
// Link seams: these paths are not exercised by the pure host timing test.
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
{
static_assert(irproto::dataWireBytes(0U) == 7U, "empty DATA wire size changed");
static_assert(irproto::dataWireBytes(3U) == 10U, "DATA wire size changed");
static_assert(irproto::dataWireBytes(24U) == 31U, "maximum DATA wire size changed");
static_assert(irproto::dataWireBytes(25U) == 0U, "oversized DATA must be rejected");
static_assert(irproto::backWireBytes(1U) == 6U, "BACK wire size changed");
static_assert(irproto::backWireBytes(26U) == 31U, "maximum BACK wire size changed");
static_assert(irproto::backToWireBytes(24U) == 31U, "maximum BACK_TO wire size changed");
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)
{
uint32_t total = 0U;
for (size_t i = 0U; i < count; ++i)
total += runs[i].lenTicks;
return total;
}
void verifyFormulaAgainstTxFsm()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, 1024U> runs{};
for (uint8_t wireBytes = 1U; wireBytes <= irproto::kMaxWireFrameBytes; ++wireBytes)
{
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
{
for (uint8_t i = 0U; i < wireBytes; ++i)
{
frame[i] = pattern == 0U ? 0x00U
: pattern == 1U ? 0xFFU
: pattern == 2U ? static_cast<uint8_t>((i & 1U) ? 0x55U : 0xAAU)
: static_cast<uint8_t>(i * 73U + 19U);
}
const size_t count = IR_Encoder::buildGateRuns(
frame.data(), wireBytes, runs.data(), runs.size());
assert(count != 0U);
assert(sumLogicalTicks(runs.data(), count) == irproto::wireLogicalTicks(wireBytes));
}
}
}
void verifyPublicSendTimeResults()
{
IR_Encoder encoder(1U, 42U, nullptr, false);
uint8_t payload[26]{};
assert(encoder.testSendAccept(1U) == 72U);
assert(encoder.testSendTime(1U, payload, 3U) == 115U);
assert(encoder.testSendBack(payload, 26U) == 340U);
}
}
int main()
{
verifyFormulaAgainstTxFsm();
verifyPublicSendTimeResults();
std::cout << "IR timing contract tests: OK\n";
return 0;
}