2 Commits

13 changed files with 632 additions and 340 deletions

View File

@ -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)
{ {

View File

@ -403,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());
noteRxEnd(RxEndReason::Timeout, micros());
isRecive = false; // приём завершён isRecive = false; // приём завершён
msgTypeReceive = 0; msgTypeReceive = 0;
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
@ -418,62 +417,6 @@ inline void IR_DecoderRaw::checkTimeout()
} }
// ==================================================================== // ====================================================================
void IR_DecoderRaw::noteRxEnd(RxEndReason reason, uint32_t tUs)
{
rxEnd.seq++;
rxEnd.reason = reason;
rxEnd.msgType = (i_dataBuffer >= 8U * msgBytes) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF;
rxEnd.packSize = (uint8_t)packSize;
rxEnd.tUs = tUs;
rxEnd.expectedEndUs = (packSize >= msgBytes + crcBytes)
? rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize) + irTicksToUs((uint32_t)syncBits * irBitTicks)
: 0U;
}
void IR_DecoderRaw::abortFrame(uint32_t tUs)
{
#if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(false);
#endif
noteRxEnd(RxEndReason::Abort, tUs);
isRecive = false;
isReciveRaw = false;
msgTypeReceive = 0;
firstRX();
releasePreambleGuard(tUs);
}
// После обрыва кадра «длинная тишина» (IR_timeout × 2 ≈ 30 мс) перед новым кандидатом преамбулы не требуется.
// prevRise — последний ДЕКОДИРОВАННЫЙ фронт; после abort он свежий, а фронты, отброшенные гвардом, его не двигают,
// поэтому валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика
// (стенд 09.09: КУ теряла пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга).
// Ложных захватов это не добавляет: хвост оборванного кадра (период фронтов 962 мкс, синхробиты ~1100) не проходит
// грубый фильтр периода преамбулы (2116…3270 мкс), а настоящая преамбула перезапускает кандидата по паузе > IR_timeout.
// После чистого конца кадра гвард остаётся: там он отсекает хвост синхробитов.
void IR_DecoderRaw::releasePreambleGuard(uint32_t tUs)
{
prevRise = tUs - IR_timeout * 2U - 1U; // «тишина уже была»: (front.time - prevRise) > longSilence для следующего фронта
}
void IR_DecoderRaw::expirePreambleCandidate()
{
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
return;
if ((micros() - preambleCandidateLastEdgeTime) > IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT)
{
if (preambleGoodPeriods)
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, micros());
preambleResetToIdle();
}
}
uint32_t IR_DecoderRaw::rxExpectedEndUs() const
{
if (!isRecive || preambleState != PreambleState::Locked || isWrongPack || packSize < msgBytes + crcBytes)
return 0;
return rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize);
}
void IR_DecoderRaw::tick() void IR_DecoderRaw::tick()
{ {
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
@ -528,16 +471,15 @@ void IR_DecoderRaw::tick()
if (!processedFront) if (!processedFront)
{ {
isSubBufferOverflow = false; isSubBufferOverflow = false;
checkTimeout();
listenStart(); listenStart();
expirePreambleCandidate(); checkTimeout();
#if defined(IR_EDGE_TRACE) #if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {} while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif #endif
return; return;
} // Если данных нет - ничего не делаем } // Если данных нет - ничего не делаем
checkTimeout();
listenStart(); listenStart();
checkTimeout();
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs(); rxBriefFlushDeferredIsrLogs();
#endif #endif
@ -838,7 +780,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
{ {
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и // Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered. // pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
abortFrame(micros()); isRecive = false;
isReciveRaw = false;
msgTypeReceive = 0;
firstRX();
return; return;
} }
@ -906,8 +851,6 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit")); packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
#endif #endif
abortFrame(micros()); // битый кадр не удерживает приёмник до таймаута
return;
} }
} }
} }
@ -935,12 +878,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё. // B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38). // Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно. // packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
if (packSize < msgBytes + crcBytes) // 0..2: кадр физически не несёт CRC — шум/битьё if (packSize != 0 && packSize < msgBytes + crcBytes)
{
isWrongPack = true; isWrongPack = true;
abortFrame(micros());
return;
}
} }
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец». // Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
@ -964,7 +903,6 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
preambleResetToIdle(); preambleResetToIdle();
msgTypeReceive = 0; msgTypeReceive = 0;
isAvailable = crcCheck(packSize - crcBytes, crcValue); isAvailable = crcCheck(packSize - crcBytes, crcValue);
noteRxEnd(isAvailable ? RxEndReason::Ok : RxEndReason::Crc, micros());
#ifdef BRUTEFORCE_CHECK #ifdef BRUTEFORCE_CHECK
{ {
@ -1684,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)
@ -1768,8 +1703,6 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
isRecive = true; isRecive = true;
isReciveRaw = true; isReciveRaw = true;
risePeriod = preambleMeanPeriod; risePeriod = preambleMeanPeriod;
rxLockSeqCnt++;
rxLockTimeUsVal = front.time;
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceResetFrame(); packTraceResetFrame();
packTraceOpen = true; packTraceOpen = true;

View File

@ -25,7 +25,7 @@ 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;
class IR_Encoder; class IR_Encoder;
class IR_DecoderRaw : virtual public IR_FOX class IR_DecoderRaw : virtual public IR_FOX
@ -52,40 +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, если происходит приём пакета
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется
// ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного
// шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего:
// «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const.
inline bool rxLineActive() const {
return isRecive ||
(preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U);
}
// Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден;
// иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
inline uint16_t rxDeclaredPackSize() const {
return (isRecive && packSize && !isWrongPack) ? packSize : 0;
}
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; }
// ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ----
enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort };
struct RxEndInfo {
uint16_t seq = 0; // номер завершения (растёт на каждом терминале)
RxEndReason reason = RxEndReason::None;
uint8_t msgType = 0xFF; // 0xFF = первый байт не был принят
uint8_t packSize = 0; // объявленная длина (0 = неизвестна)
uint32_t tUs = 0; // micros() терминала
uint32_t expectedEndUs = 0; // расчётный конец кадра в эфире (0 = неизвестен)
};
uint16_t rxLockSeq() const { return rxLockSeqCnt; } // ++ в момент лока преамбулы
uint32_t rxLockTimeUs() const { return rxLockTimeUsVal; } // метка фронта лока (ISR-время)
/// Тип принимаемого кадра (3 бита) после первого байта; 0xFF пока неизвестен / приём не идёт.
uint8_t rxMsgType() const { return (isRecive && packSize) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; }
const RxEndInfo &rxLastEnd() const { return rxEnd; }
/// Расчётный момент последнего бита данных текущего кадра (по объявленной длине); 0 = не Locked / длина неизвестна.
uint32_t rxExpectedEndUs() const;
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; }
@ -181,13 +147,6 @@ private:
Locked = 2 Locked = 2
}; };
PreambleState preambleState = PreambleState::Idle; PreambleState preambleState = PreambleState::Idle;
uint16_t rxLockSeqCnt = 0;
uint32_t rxLockTimeUsVal = 0;
RxEndInfo rxEnd;
void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец
void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow)
void releasePreambleGuard(uint32_t tUs); // после abort: новый кандидат преамбулы без ожидания длинной тишины
void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle
uint8_t preambleGoodPeriods = 0; uint8_t preambleGoodPeriods = 0;
uint16_t preambleMeanPeriod = 0; uint16_t preambleMeanPeriod = 0;
uint32_t preambleCandidateLastEdgeTime = 0; uint32_t preambleCandidateLastEdgeTime = 0;

View File

@ -352,29 +352,6 @@ bool IR_Encoder::txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool
return txAdvanceAfterOutput(st, sendBufferLocal); return txAdvanceAfterOutput(st, sendBufferLocal);
} }
// Обход кадра по границам ранов: между границами автомат выдаёт st.toggleCounter+1 тактов
// уровня st.state (txAdvanceAfterOutput считает toggleCounter до нуля, затем txAdvanceBoundary
// открывает следующий ран). Даёт ту же последовательность тактов, что потиковый обход, но за
// число шагов = число ранов (пинг: ~230 вместо ~8700 тактов — на 12 МГц это ~30 мс перед стартом DMA).
template <typename Emit>
bool IR_Encoder::txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit)
{
for (;;)
{
const bool gate = st.state;
const uint32_t lenTicks = (uint32_t)st.toggleCounter + 1U;
if (!emit(gate, lenTicks))
{
return false;
}
st.toggleCounter = 0;
if (!txAdvanceBoundary(st, sendBufferLocal))
{
return true;
}
}
}
void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const
{ {
st.sendLen = sendLen; st.sendLen = sendLen;
@ -499,27 +476,28 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu
st.currentBitSequence = bitHigh; st.currentBitSequence = bitHigh;
size_t runCount = 0; size_t runCount = 0;
const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool { bool isActive = true;
while (isActive)
{
bool gate = false;
isActive = txEmitTick(st, sendBufferLocal, gate);
if (runCount > 0 && outRuns[runCount - 1].gate == gate) if (runCount > 0 && outRuns[runCount - 1].gate == gate)
{ {
const uint32_t merged = (uint32_t)outRuns[runCount - 1].lenTicks + lenTicks; outRuns[runCount - 1].lenTicks = (uint16_t)(outRuns[runCount - 1].lenTicks + 1U);
if (merged > 65535U)
{
return false;
}
outRuns[runCount - 1].lenTicks = (uint16_t)merged;
return true;
} }
if (runCount >= maxRuns || lenTicks > 65535U) else
{ {
return false; if (runCount >= maxRuns)
{
return 0;
}
outRuns[runCount].gate = gate;
outRuns[runCount].lenTicks = 1U;
runCount++;
} }
outRuns[runCount].gate = gate; }
outRuns[runCount].lenTicks = (uint16_t)lenTicks; return runCount;
runCount++;
return true;
});
return ok ? runCount : 0;
} }
size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply) size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply)
@ -586,29 +564,40 @@ size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_
bool currentGate = false; bool currentGate = false;
uint32_t currentLogicalLen = 0; uint32_t currentLogicalLen = 0;
bool havePendingRun = false; bool havePendingRun = false;
const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool { bool isActive = true;
if (havePendingRun && currentGate == gate) while (isActive)
{
currentLogicalLen += lenTicks;
return true;
}
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
{
return false;
}
currentGate = gate;
currentLogicalLen = lenTicks;
havePendingRun = true;
return true;
});
if (!ok)
{ {
return 0; bool gate = false;
isActive = txEmitTick(st, sendBufferLocal, gate);
if (!havePendingRun)
{
currentGate = gate;
currentLogicalLen = 1U;
havePendingRun = true;
continue;
}
if (currentGate == gate)
{
currentLogicalLen++;
continue;
}
if (!appendPhysicalRun(currentGate, currentLogicalLen, runCount))
{
return 0;
}
currentGate = gate;
currentLogicalLen = 1U;
} }
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount)) if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
{ {
return 0; return 0;
} }
return runCount; return runCount;
} }
@ -1194,8 +1183,10 @@ uint8_t IR_Encoder::bitLow[2] = {
uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const
{ {
// Полное время кадра в эфире по формуле FSM (преамбула + байты с синхробитами), округление вверх до мс. // The TX FSM emits syncBits after every wire byte (including the last)
return (irFrameAirtimeUs(packSize) + 999U) / 1000U; // 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);
} }
// Функции для тестирования времени отправки без фактической отправки // Функции для тестирования времени отправки без фактической отправки

View File

@ -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);
/** /**
@ -214,8 +210,6 @@ private:
static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal); static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal); static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal);
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut); static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
template <typename Emit>
static bool txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit);
void loadTxFsmFromMembers(TxFsmState &st) const; void loadTxFsmFromMembers(TxFsmState &st) const;
void storeTxFsmToMembers(const TxFsmState &st); void storeTxFsmToMembers(const TxFsmState &st);
bool shouldUseBufferedIsr() const; bool shouldUseBufferedIsr() const;

View File

@ -231,11 +231,9 @@ typedef uint16_t crc_t;
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT #ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U #define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
#endif #endif
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. Кандидат без фронтов дольше таймаута байта /** Таймаут окна кандидата преамбулы: IR_timeout * mult. */
преамбулой быть не может; при 3× линия считалась занятой (rxLineActive) ещё 45 мс после последнего
паразитного фронта (напр. засветка своим дальномером) и откладывала передачу. */
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT #ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 1U #define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U
#endif #endif
#define preambPulse 3 #define preambPulse 3
@ -266,31 +264,163 @@ typedef uint16_t crc_t;
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
#define tolerance 300U #define tolerance 300U
// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ---- namespace irproto
// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по {
// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта. /** Maximum complete frame length representable by the five header bits. */
constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U); constexpr uint8_t kMaxWireFrameBytes = static_cast<uint8_t>(IR_MASK_MSG_INFO);
constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U);
constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U); constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U; constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks; constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); } constexpr uint8_t kAcceptFrameBytes = msgBytes + addrBytes + 1U + crcBytes;
/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс. constexpr uint8_t kRequestFrameBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); }
/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс. constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); } constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс. constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс. /** Complete DATA frame size, or zero when payloadBytes cannot fit on wire. */
constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks); constexpr uint8_t dataWireBytes(uint8_t payloadBytes)
/// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём. {
constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U); return payloadBytes <= kMaxDataPayloadBytes
constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs; ? static_cast<uint8_t>(kDataFrameOverheadBytes + payloadBytes)
/// Протокольный максимум длины кадра (5-битное поле длины). : 0U;
constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO; }
/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from).
constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); } /** Complete non-addressed BACK frame size, or zero when it cannot fit. */
constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); } 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);
}
/** 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 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(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;

View File

@ -65,63 +65,13 @@ public:
return out; return out;
} }
// Заполнение по ранам, а не по словам: тот же поток слов, что даёт nextWord() (состояние
// runIndex_/ticksLeftInRun_/slotInPeriod_ переносится через границы порций), но пауза (gate=0)
// пишется одним циклом записи, а несущая — копией готового шаблона периода. На 12 МГц это
// ~1 мс на 4096 слов вместо ~16 (пословный автомат) — и в предзаполнении перед стартом DMA,
// и в ISR-дозаполнении половин буфера во время передачи.
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) { IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
if (dst == nullptr || count == 0) { if (dst == nullptr || count == 0) {
return; return;
} }
while (count != 0) { do {
if (runIndex_ >= runCount) { *dst++ = nextWord();
do { *dst++ = resetWord; } while (--count != 0); } while (--count != 0);
return;
}
const bool gate = runs[runIndex_].gate;
uint16_t n = ticksLeftInRun_; // слов до конца текущего рана
if (n == 0) n = 1; // ран нулевой длины: nextWord() выдаёт одно слово и переходит дальше
if (n > count) n = count;
if (!gate) {
slotInPeriod_ = 0;
uint16_t k = n;
do { *dst++ = resetWord; } while (--k != 0);
} else {
uint16_t k = n;
// добить текущий период до слота 0 (если ран начался посреди периода на границе порции)
while (k != 0 && slotInPeriod_ != 0) {
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
k--;
}
// целые периоды: powerN_ слов setWord, остальные resetWord
while (k >= multiply_) {
uint16_t i = 0;
for (; i < powerN_; ++i) *dst++ = setWord;
for (; i < multiply_; ++i) *dst++ = resetWord;
k = (uint16_t)(k - multiply_);
}
// хвост неполного периода
while (k != 0) {
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
k--;
}
}
count = (uint16_t)(count - n);
if (ticksLeftInRun_ > n) {
ticksLeftInRun_ = (uint16_t)(ticksLeftInRun_ - n);
} else {
ticksLeftInRun_ = 0;
}
if (ticksLeftInRun_ == 0) {
runIndex_++;
if (runIndex_ < runCount) {
ticksLeftInRun_ = runs[runIndex_].lenTicks;
}
}
}
} }
private: private:

View File

@ -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;

View File

@ -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;
}; };

View File

@ -0,0 +1,50 @@
#pragma once
#include <cstddef>
#include <cstdint>
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 *digitalPinToPort(uint8_t) { return nullptr; }
inline uint16_t digitalPinToBitMask(uint8_t) { return 0U; }
inline void pinMode(uint8_t, uint8_t) {}
inline void digitalWrite(uint8_t, uint8_t) {}
inline void NVIC_SetPriority(IRQn_Type, uint8_t) {}
inline void noInterrupts() {}
inline void interrupts() {}
struct ArduinoSerialStub
{
template <typename T> void print(const T &) {}
template <typename T> void println(const T &) {}
void println() {}
};
inline ArduinoSerialStub Serial;

38
tests/run_host_tests.ps1 Normal file
View 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
View 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;
}

View File

@ -0,0 +1,85 @@
#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::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");
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); // six-byte wire frame
assert(encoder.testSendTime(1U, payload, 3U) == 115U); // ten-byte wire frame
assert(encoder.testSendBack(payload, 26U) == 340U); // 31-byte wire frame
}
}
int main()
{
verifyFormulaAgainstTxFsm();
verifyPublicSendTimeResults();
std::cout << "IR timing contract tests: OK\n";
return 0;
}