mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-21 20:39:35 +00:00
Compare commits
2 Commits
archive/co
...
archive/ex
| Author | SHA1 | Date | |
|---|---|---|---|
| c7efd1cb65 | |||
| 36f234739a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
bin/*
|
bin/*
|
||||||
tests/*.exe
|
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
log/*
|
log/*
|
||||||
/.vscode
|
/.vscode
|
||||||
|
|||||||
@ -162,9 +162,7 @@ void IR_Decoder::_tick()
|
|||||||
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
|
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
|
||||||
isWaitingAcceptSend = true;
|
isWaitingAcceptSend = true;
|
||||||
}
|
}
|
||||||
// Raw keeps the decoder's common minimum-size contract. Known packet
|
gotRaw.set(&packInfo, id);
|
||||||
// layouts are validated by their typed BasePack::set calls above.
|
|
||||||
gotRaw.set(&packInfo, id, false);
|
|
||||||
}
|
}
|
||||||
if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
|
if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -374,45 +374,15 @@ 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;
|
||||||
const uint32_t nowUs = micros();
|
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
|
||||||
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();
|
||||||
}
|
}
|
||||||
@ -433,7 +403,6 @@ 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() декодер остаётся
|
||||||
@ -504,7 +473,6 @@ 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
|
||||||
@ -512,7 +480,6 @@ void IR_DecoderRaw::tick()
|
|||||||
} // Если данных нет - ничего не делаем
|
} // Если данных нет - ничего не делаем
|
||||||
listenStart();
|
listenStart();
|
||||||
checkTimeout();
|
checkTimeout();
|
||||||
expirePreambleCandidateIfIdle(micros());
|
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
rxBriefFlushDeferredIsrLogs();
|
rxBriefFlushDeferredIsrLogs();
|
||||||
#endif
|
#endif
|
||||||
@ -811,13 +778,6 @@ 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;
|
||||||
@ -981,9 +941,6 @@ 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));
|
||||||
@ -1626,7 +1583,6 @@ 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;
|
||||||
@ -1641,10 +1597,6 @@ 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;
|
||||||
@ -1655,24 +1607,6 @@ 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;
|
||||||
@ -1688,10 +1622,7 @@ 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)
|
||||||
@ -1699,10 +1630,7 @@ 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;
|
||||||
@ -1720,20 +1648,15 @@ 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;
|
||||||
// Keep preambleWasObservable sticky: this edge proves the medium is
|
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time);
|
||||||
// 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
|
||||||
@ -1750,7 +1673,6 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,29 +27,6 @@ class Print;
|
|||||||
#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 = irproto::kMandatoryInterPacketQuietMs;
|
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
|
||||||
{
|
{
|
||||||
@ -75,38 +52,6 @@ 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, если происходит приём пакета
|
||||||
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ открыт
|
|
||||||
// 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 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; }
|
||||||
@ -166,7 +111,6 @@ 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; // Подстраиваемое время бита в мкс
|
||||||
|
|
||||||
@ -204,10 +148,6 @@ 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;
|
||||||
@ -276,9 +216,6 @@ 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 Длина в байтах проверяемых данных
|
||||||
|
|||||||
@ -1184,7 +1184,8 @@ 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)
|
// The TX FSM emits syncBits after every wire byte (including the last)
|
||||||
// and its preamble runs are preambToggle+1 logical ticks long.
|
// and its preamble runs are preambToggle+1 logical ticks long. The old
|
||||||
|
// approximation omitted the per-byte sync and shortened the preamble.
|
||||||
return irproto::wireAirtimeMsCeil(packSize);
|
return irproto::wireAirtimeMsCeil(packSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,10 +62,6 @@ public:
|
|||||||
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
||||||
/// (аналог setBlindDecoders() для одного RX).
|
/// (аналог setBlindDecoders() для одного RX).
|
||||||
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
|
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 isr();
|
||||||
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
|
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
|
||||||
/**
|
/**
|
||||||
|
|||||||
38
IR_config.h
38
IR_config.h
@ -327,8 +327,8 @@ constexpr bool isTypedWireSizeValid(uint8_t msgType, uint8_t wireBytes)
|
|||||||
/*
|
/*
|
||||||
* TX FSM timing contract.
|
* TX FSM timing contract.
|
||||||
*
|
*
|
||||||
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
|
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
|
||||||
* constant runs; each run is preambToggle+1 ticks. Every data bit and every
|
* 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.
|
* 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 kTxLogicalClockHz = static_cast<uint32_t>(carrierFrec) * 2U;
|
||||||
@ -360,17 +360,6 @@ constexpr uint32_t preambleAirtimeUsCeil()
|
|||||||
return logicalTicksToUsCeil(kPreambleLogicalTicks);
|
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. */
|
/** Complete nominal on-air duration, rounded up to a whole microsecond. */
|
||||||
constexpr uint32_t wireAirtimeUsCeil(uint8_t wireBytes)
|
constexpr uint32_t wireAirtimeUsCeil(uint8_t wireBytes)
|
||||||
{
|
{
|
||||||
@ -395,28 +384,10 @@ constexpr uint16_t kMandatoryInterPacketQuietMs =
|
|||||||
constexpr uint32_t kMandatoryInterPacketQuietUs =
|
constexpr uint32_t kMandatoryInterPacketQuietUs =
|
||||||
static_cast<uint32_t>(kMandatoryInterPacketQuietMs) * 1000U;
|
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 uint16_t kDefaultTimingGuardPermille = 1150U;
|
||||||
|
|
||||||
constexpr uint32_t addTimingGuardUs(
|
constexpr uint32_t addTimingGuardUs(uint32_t durationUs,
|
||||||
uint32_t durationUs,
|
uint16_t marginPermille = kDefaultTimingGuardPermille)
|
||||||
uint16_t marginPermille = kDefaultTimingGuardPermille)
|
|
||||||
{
|
{
|
||||||
return marginPermille == 0U
|
return marginPermille == 0U
|
||||||
? 0U
|
? 0U
|
||||||
@ -448,7 +419,6 @@ static_assert(kMaxDataPayloadBytes == 24U, "DATA payload contract changed");
|
|||||||
static_assert(kMaxBackPayloadBytes == 26U, "BACK payload contract changed");
|
static_assert(kMaxBackPayloadBytes == 26U, "BACK payload contract changed");
|
||||||
static_assert(kPreambleLogicalTicks == 588U, "preamble timing contract changed");
|
static_assert(kPreambleLogicalTicks == 588U, "preamble timing contract changed");
|
||||||
static_assert(kWireByteLogicalTicks == 814U, "wire-byte 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");
|
static_assert(kMandatoryInterPacketQuietMs == 42U, "inter-packet quiet policy changed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
147
PacketTypes.cpp
147
PacketTypes.cpp
@ -2,47 +2,28 @@
|
|||||||
|
|
||||||
namespace PacketTypes
|
namespace PacketTypes
|
||||||
{
|
{
|
||||||
uint8_t minimumPacketSize(uint8_t msgType)
|
bool BasePack::checkPacketLayout() const
|
||||||
{
|
{
|
||||||
switch (msgType)
|
if (packInfo == nullptr || packInfo->buffer == nullptr ||
|
||||||
|
packInfo->packSize < msgBytes + crcBytes ||
|
||||||
|
packInfo->packSize > irproto::kMaxWireFrameBytes)
|
||||||
{
|
{
|
||||||
case IR_MSG_DATA_ACCEPT:
|
return false;
|
||||||
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 isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize)
|
bool BasePack::checkAddress() { return true; };
|
||||||
{
|
void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id)
|
||||||
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;
|
isAvailable = false;
|
||||||
isRawAvailable = false;
|
isRawAvailable = false;
|
||||||
this->packInfo = packInfo;
|
this->packInfo = packInfo;
|
||||||
this->id = id;
|
this->id = id;
|
||||||
|
|
||||||
if (packInfo == nullptr || packInfo->buffer == nullptr)
|
if (!checkPacketLayout())
|
||||||
{
|
{
|
||||||
return false;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
|
|
||||||
if (requireTypedSize && !isTypedPacketSizeValid(msgType, packInfo->packSize))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkAddress())
|
if (checkAddress())
|
||||||
@ -60,65 +41,63 @@ namespace PacketTypes
|
|||||||
Serial.print(" NOT-OK ");
|
Serial.print(" NOT-OK ");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
return isAvailable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t BasePack::_getAddrFrom(BasePack *obj)
|
uint16_t BasePack::_getAddrFrom(BasePack *obj)
|
||||||
{
|
{
|
||||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
if (obj == nullptr || !obj->checkPacketLayout() ||
|
||||||
|
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||||
obj->packInfo->packSize < crcBytes ||
|
obj->packInfo->packSize < crcBytes ||
|
||||||
uint16_t(obj->addressFromOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
|
static_cast<uint16_t>(obj->addressFromOffset) + 1U >=
|
||||||
|
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0U;
|
||||||
}
|
}
|
||||||
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
|
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
|
||||||
}
|
};
|
||||||
uint16_t BasePack::_getAddrTo(BasePack *obj)
|
uint16_t BasePack::_getAddrTo(BasePack *obj)
|
||||||
{
|
{
|
||||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
if (obj == nullptr || !obj->checkPacketLayout() ||
|
||||||
|
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||||
obj->packInfo->packSize < crcBytes ||
|
obj->packInfo->packSize < crcBytes ||
|
||||||
uint16_t(obj->addressToOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
|
static_cast<uint16_t>(obj->addressToOffset) + 1U >=
|
||||||
|
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0U;
|
||||||
}
|
}
|
||||||
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
|
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
|
||||||
}
|
};
|
||||||
|
|
||||||
uint8_t BasePack::_getDataSize(BasePack *obj)
|
uint8_t BasePack::_getDataSize(BasePack *obj)
|
||||||
{
|
{
|
||||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
|
if (obj == nullptr || !obj->checkPacketLayout() ||
|
||||||
|
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0U;
|
||||||
}
|
}
|
||||||
const uint16_t frameOverhead = uint16_t(crcBytes) + uint16_t(obj->DataOffset);
|
const uint16_t overhead = static_cast<uint16_t>(obj->DataOffset) + crcBytes;
|
||||||
if (uint16_t(obj->packInfo->packSize) <= frameOverhead)
|
return static_cast<uint16_t>(obj->packInfo->packSize) > overhead
|
||||||
{
|
? static_cast<uint8_t>(static_cast<uint16_t>(obj->packInfo->packSize) - overhead)
|
||||||
return 0;
|
: 0U;
|
||||||
}
|
};
|
||||||
return uint8_t(uint16_t(obj->packInfo->packSize) - frameOverhead);
|
|
||||||
}
|
|
||||||
uint8_t *BasePack::_getDataPrt(BasePack *obj)
|
uint8_t *BasePack::_getDataPrt(BasePack *obj)
|
||||||
{
|
{
|
||||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
if (obj == nullptr || !obj->checkPacketLayout() ||
|
||||||
obj->packInfo->packSize < crcBytes)
|
obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||||
{
|
obj->packInfo->packSize < crcBytes ||
|
||||||
return nullptr;
|
static_cast<uint16_t>(obj->DataOffset) >
|
||||||
}
|
static_cast<uint16_t>(obj->packInfo->packSize - crcBytes))
|
||||||
const uint16_t dataEnd = uint16_t(obj->packInfo->packSize) - uint16_t(crcBytes);
|
|
||||||
if (uint16_t(obj->DataOffset) > dataEnd)
|
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return obj->packInfo->buffer + obj->DataOffset;
|
return obj->packInfo->buffer + obj->DataOffset;
|
||||||
}
|
};
|
||||||
uint8_t BasePack::_getDataRawSize(BasePack *obj)
|
uint8_t BasePack::_getDataRawSize(BasePack *obj)
|
||||||
{
|
{
|
||||||
if (obj == nullptr || obj->packInfo == nullptr)
|
return obj != nullptr && obj->checkPacketLayout() && obj->packInfo != nullptr
|
||||||
{
|
? obj->packInfo->packSize
|
||||||
return 0;
|
: 0U;
|
||||||
}
|
};
|
||||||
return obj->packInfo->packSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BasePack::available()
|
bool BasePack::available()
|
||||||
{
|
{
|
||||||
@ -132,7 +111,7 @@ namespace PacketTypes
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
bool BasePack::availableRaw()
|
bool BasePack::availableRaw()
|
||||||
{
|
{
|
||||||
if (isRawAvailable)
|
if (isRawAvailable)
|
||||||
@ -144,6 +123,17 @@ namespace PacketTypes
|
|||||||
{
|
{
|
||||||
return false;
|
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()
|
bool Data::checkAddress()
|
||||||
@ -153,6 +143,17 @@ namespace PacketTypes
|
|||||||
return ret;
|
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 DataBack::checkAddress()
|
||||||
{
|
{
|
||||||
bool ret;
|
bool ret;
|
||||||
@ -169,8 +170,30 @@ namespace PacketTypes
|
|||||||
return ret;
|
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 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 Request::checkAddress()
|
||||||
{
|
{
|
||||||
bool ret;
|
bool ret;
|
||||||
|
|||||||
@ -4,15 +4,6 @@
|
|||||||
class IR_Decoder;
|
class IR_Decoder;
|
||||||
namespace PacketTypes
|
namespace PacketTypes
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Minimum complete frame size (header, addresses/data required by the type,
|
|
||||||
* and CRC). Unknown/reserved message types return 0.
|
|
||||||
*/
|
|
||||||
uint8_t minimumPacketSize(uint8_t msgType);
|
|
||||||
|
|
||||||
/** True only for a known typed packet whose complete frame is long enough. */
|
|
||||||
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize);
|
|
||||||
|
|
||||||
class BasePack
|
class BasePack
|
||||||
{
|
{
|
||||||
friend IR_Decoder;
|
friend IR_Decoder;
|
||||||
@ -30,8 +21,9 @@ namespace PacketTypes
|
|||||||
IR_FOX::PackInfo *packInfo = nullptr;
|
IR_FOX::PackInfo *packInfo = nullptr;
|
||||||
uint16_t id = 0;
|
uint16_t id = 0;
|
||||||
|
|
||||||
|
virtual bool checkPacketLayout() const;
|
||||||
virtual bool checkAddress();
|
virtual bool checkAddress();
|
||||||
bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true);
|
void set(IR_FOX::PackInfo *packInfo, uint16_t id);
|
||||||
|
|
||||||
static uint16_t _getAddrFrom(BasePack *obj);
|
static uint16_t _getAddrFrom(BasePack *obj);
|
||||||
static uint16_t _getAddrTo(BasePack *obj);
|
static uint16_t _getAddrTo(BasePack *obj);
|
||||||
@ -43,9 +35,9 @@ namespace PacketTypes
|
|||||||
bool available();
|
bool available();
|
||||||
bool availableRaw();
|
bool availableRaw();
|
||||||
|
|
||||||
inline uint8_t getMsgInfo() { return packInfo->buffer[0] & IR_MASK_MSG_INFO; };
|
inline uint8_t getMsgInfo() { return packInfo != nullptr && packInfo->buffer != nullptr ? packInfo->buffer[0] & IR_MASK_MSG_INFO : 0U; };
|
||||||
inline uint8_t getMsgType() { return (packInfo->buffer[0] >> 5) & IR_MASK_MSG_TYPE; };
|
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->buffer[0]; };
|
inline uint8_t getMsgRAW() { return packInfo != nullptr && packInfo->buffer != nullptr ? packInfo->buffer[0] : 0U; };
|
||||||
inline uint16_t getErrorCount() { return packInfo->err.all(); };
|
inline uint16_t getErrorCount() { return packInfo->err.all(); };
|
||||||
inline uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; };
|
inline uint8_t getErrorLowSignal() { return packInfo->err.lowSignal; };
|
||||||
inline uint8_t getErrorHighSignal() { return packInfo->err.highSignal; };
|
inline uint8_t getErrorHighSignal() { return packInfo->err.highSignal; };
|
||||||
@ -74,6 +66,7 @@ namespace PacketTypes
|
|||||||
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
|
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool checkPacketLayout() const override;
|
||||||
bool checkAddress() override;
|
bool checkAddress() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,6 +88,7 @@ namespace PacketTypes
|
|||||||
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
|
inline uint8_t *getDataPrt() { return _getDataPrt(this); };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool checkPacketLayout() const override;
|
||||||
bool checkAddress() override;
|
bool checkAddress() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,6 +106,7 @@ namespace PacketTypes
|
|||||||
inline uint8_t getCustomByte() { return packInfo->buffer[DataOffset]; };
|
inline uint8_t getCustomByte() { return packInfo->buffer[DataOffset]; };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool checkPacketLayout() const override;
|
||||||
bool checkAddress() override;
|
bool checkAddress() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -130,6 +125,7 @@ namespace PacketTypes
|
|||||||
inline uint16_t getAddrTo() { return _getAddrTo(this); };
|
inline uint16_t getAddrTo() { return _getAddrTo(this); };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool checkPacketLayout() const override;
|
||||||
bool checkAddress() override;
|
bool checkAddress() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,6 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
class __FlashStringHelper;
|
|
||||||
#define F(value) reinterpret_cast<const __FlashStringHelper *>(value)
|
|
||||||
|
|
||||||
struct GPIO_TypeDef
|
struct GPIO_TypeDef
|
||||||
{
|
{
|
||||||
uint32_t BSRR = 0U;
|
uint32_t BSRR = 0U;
|
||||||
@ -35,26 +32,19 @@ private:
|
|||||||
uint32_t overflow_ = 1U;
|
uint32_t overflow_ = 1U;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline GPIO_TypeDef arduinoStubPort;
|
inline GPIO_TypeDef *digitalPinToPort(uint8_t) { return nullptr; }
|
||||||
inline GPIO_TypeDef *digitalPinToPort(uint8_t) { return &arduinoStubPort; }
|
inline uint16_t digitalPinToBitMask(uint8_t) { return 0U; }
|
||||||
inline uint16_t digitalPinToBitMask(uint8_t) { return 1U; }
|
|
||||||
inline void pinMode(uint8_t, uint8_t) {}
|
inline void pinMode(uint8_t, uint8_t) {}
|
||||||
inline void digitalWrite(uint8_t, uint8_t) {}
|
inline void digitalWrite(uint8_t, uint8_t) {}
|
||||||
inline void NVIC_SetPriority(IRQn_Type, uint8_t) {}
|
inline void NVIC_SetPriority(IRQn_Type, uint8_t) {}
|
||||||
inline void noInterrupts() {}
|
inline void noInterrupts() {}
|
||||||
inline void interrupts() {}
|
inline void interrupts() {}
|
||||||
|
|
||||||
inline uint32_t arduinoStubMicros = 0U;
|
struct ArduinoSerialStub
|
||||||
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 print(const T &) {}
|
||||||
template <typename T> void println(const T &) {}
|
template <typename T> void println(const T &) {}
|
||||||
void println() {}
|
void println() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
using ArduinoSerialStub = Print;
|
|
||||||
inline ArduinoSerialStub Serial;
|
inline ArduinoSerialStub Serial;
|
||||||
|
|||||||
38
tests/run_host_tests.ps1
Normal file
38
tests/run_host_tests.ps1
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
$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' }
|
||||||
145
tests/test_packet_types.cpp
Normal file
145
tests/test_packet_types.cpp
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@ -1,386 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@ -27,23 +27,11 @@ 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(10U) == 8728U, "10-byte tick count changed");
|
||||||
static_assert(irproto::wireLogicalTicks(31U) == 25822U, "31-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::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(6U) == 72000U, "6-byte airtime changed");
|
||||||
static_assert(irproto::wireAirtimeUsCeil(10U) == 114843U, "10-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::wireAirtimeUsCeil(31U) == 339764U, "31-byte airtime changed");
|
||||||
static_assert(irproto::responseStartGuardUs() == 57198U, "response-start guard changed");
|
static_assert(irproto::responseStartGuardUs() == 57198U, "response-start guard changed");
|
||||||
static_assert(irproto::responseFrameGuardUs(6U) == 131100U, "response-frame 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 sumLogicalTicks(const IrTxGateRun *runs, size_t count)
|
||||||
{
|
{
|
||||||
@ -82,9 +70,9 @@ void verifyPublicSendTimeResults()
|
|||||||
IR_Encoder encoder(1U, 42U, nullptr, false);
|
IR_Encoder encoder(1U, 42U, nullptr, false);
|
||||||
uint8_t payload[26]{};
|
uint8_t payload[26]{};
|
||||||
|
|
||||||
assert(encoder.testSendAccept(1U) == 72U);
|
assert(encoder.testSendAccept(1U) == 72U); // six-byte wire frame
|
||||||
assert(encoder.testSendTime(1U, payload, 3U) == 115U);
|
assert(encoder.testSendTime(1U, payload, 3U) == 115U); // ten-byte wire frame
|
||||||
assert(encoder.testSendBack(payload, 26U) == 340U);
|
assert(encoder.testSendBack(payload, 26U) == 340U); // 31-byte wire frame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user