mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-21 20:39:35 +00:00
archive: freeze IR-protocol WIP before stepwise integration
This commit is contained in:
@ -162,7 +162,9 @@ void IR_Decoder::_tick()
|
||||
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
|
||||
isWaitingAcceptSend = true;
|
||||
}
|
||||
gotRaw.set(&packInfo, id);
|
||||
// Raw keeps the decoder's common minimum-size contract. Known packet
|
||||
// layouts are validated by their typed BasePack::set calls above.
|
||||
gotRaw.set(&packInfo, id, false);
|
||||
}
|
||||
if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
|
||||
{
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#include "IR_DecoderRaw.h"
|
||||
#include "IR_Encoder.h"
|
||||
#include "IrInterruptGuard.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@ -53,9 +54,8 @@ IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPa
|
||||
|
||||
bool IR_DecoderRaw::isSubOverflow()
|
||||
{
|
||||
noInterrupts();
|
||||
volatile bool ret = isSubBufferOverflow;
|
||||
interrupts();
|
||||
IrInterruptGuard guard;
|
||||
const bool ret = isSubBufferOverflow;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ void IR_DecoderRaw::refreshPairMuteState()
|
||||
++active;
|
||||
}
|
||||
const uint32_t nowUs = micros();
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
const bool wasActive = (isPairSending != 0);
|
||||
isPairSending = active;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
@ -121,7 +121,6 @@ void IR_DecoderRaw::refreshPairMuteState()
|
||||
rxBriefMuteBlockedEdges = 0;
|
||||
}
|
||||
#endif
|
||||
interrupts();
|
||||
}
|
||||
|
||||
#if IR_RX_BRIEF_LOG
|
||||
@ -142,6 +141,7 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
|
||||
case RxBriefReason::Timeout: return F("TIMEOUT");
|
||||
case RxBriefReason::Crc: return F("CRC");
|
||||
case RxBriefReason::Ok: return F("OK");
|
||||
case RxBriefReason::Count: return F("UNK");
|
||||
default: return F("UNK");
|
||||
}
|
||||
}
|
||||
@ -152,7 +152,7 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
|
||||
void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
|
||||
{
|
||||
const uint8_t ri = (uint8_t)reason;
|
||||
if (ri < 14U)
|
||||
if (ri < kRxBriefReasonCount)
|
||||
rxReasonCnt[ri]++;
|
||||
#if !IR_RX_BRIEF_LOG
|
||||
(void)a; (void)b; (void)tUs;
|
||||
@ -228,6 +228,8 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin
|
||||
Serial.print(b);
|
||||
}
|
||||
break;
|
||||
case RxBriefReason::Count:
|
||||
break;
|
||||
}
|
||||
Serial.println();
|
||||
#endif // IR_RX_BRIEF_LOG (печать)
|
||||
@ -235,11 +237,14 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin
|
||||
|
||||
void IR_DecoderRaw::printRxReasonStats(Print &out) const
|
||||
{
|
||||
static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
|
||||
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
|
||||
"TIMEOUT", "CRC", "OK"};
|
||||
static const char *const kTags[] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
|
||||
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
|
||||
"TIMEOUT", "CRC", "OK"};
|
||||
static_assert(sizeof(kTags) / sizeof(kTags[0]) == kRxBriefReasonCount,
|
||||
"RX reason tag table must match RxBriefReason::Count");
|
||||
out.print(F("RXSTAT"));
|
||||
for (uint8_t i = 1; i < 14U; i++)
|
||||
for (uint8_t i = static_cast<uint8_t>(RxBriefReason::MuteBegin);
|
||||
i < kRxBriefReasonCount; ++i)
|
||||
{
|
||||
out.print(',');
|
||||
out.print(kTags[i]);
|
||||
@ -273,22 +278,23 @@ void IR_DecoderRaw::rxBriefFlushDeferredIsrLogs()
|
||||
uint16_t muteEndCnt = 0;
|
||||
uint16_t rawCnt = 0;
|
||||
uint32_t rawLastUs = 0;
|
||||
noInterrupts();
|
||||
muteBeginPending = rxBriefMuteBeginPending;
|
||||
muteBeginUs = rxBriefMuteBeginUs;
|
||||
rxBriefMuteBeginPending = false;
|
||||
rxBriefMuteBeginUs = 0;
|
||||
muteEndPending = rxBriefMuteEndPending;
|
||||
muteEndUs = rxBriefMuteEndUs;
|
||||
muteEndCnt = rxBriefMuteEndCount;
|
||||
rxBriefMuteEndPending = false;
|
||||
rxBriefMuteEndUs = 0;
|
||||
rxBriefMuteEndCount = 0;
|
||||
rawCnt = rxBriefRawOverflowDrops;
|
||||
rawLastUs = rxBriefRawOverflowLastUs;
|
||||
rxBriefRawOverflowDrops = 0;
|
||||
rxBriefRawOverflowLastUs = 0;
|
||||
interrupts();
|
||||
{
|
||||
IrInterruptGuard guard;
|
||||
muteBeginPending = rxBriefMuteBeginPending;
|
||||
muteBeginUs = rxBriefMuteBeginUs;
|
||||
rxBriefMuteBeginPending = false;
|
||||
rxBriefMuteBeginUs = 0;
|
||||
muteEndPending = rxBriefMuteEndPending;
|
||||
muteEndUs = rxBriefMuteEndUs;
|
||||
muteEndCnt = rxBriefMuteEndCount;
|
||||
rxBriefMuteEndPending = false;
|
||||
rxBriefMuteEndUs = 0;
|
||||
rxBriefMuteEndCount = 0;
|
||||
rawCnt = rxBriefRawOverflowDrops;
|
||||
rawLastUs = rxBriefRawOverflowLastUs;
|
||||
rxBriefRawOverflowDrops = 0;
|
||||
rxBriefRawOverflowLastUs = 0;
|
||||
}
|
||||
if (muteBeginPending)
|
||||
rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs);
|
||||
if (muteEndPending)
|
||||
@ -359,7 +365,7 @@ void IR_DecoderRaw::firstRX()
|
||||
#ifdef IRDEBUG
|
||||
wrCounter = 0;
|
||||
#endif
|
||||
memset(dataBuffer, 0x00, dataByteSizeMax);
|
||||
memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes);
|
||||
pulseFilterReset();
|
||||
preambleResetToIdle();
|
||||
}
|
||||
@ -368,9 +374,8 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
|
||||
{
|
||||
if (pulseFilterHoldCount != 0U)
|
||||
return true;
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
const bool busy = !subBuffer.isEmpty();
|
||||
interrupts();
|
||||
return busy;
|
||||
}
|
||||
|
||||
@ -378,7 +383,7 @@ void IR_DecoderRaw::listenStart()
|
||||
{
|
||||
if (rxTimeoutPipelineBusy())
|
||||
return;
|
||||
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
|
||||
if (isReciveRaw && ((micros() - lastEdgeTime) > receiveSilenceTimeoutUs()))
|
||||
{
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceOnTimeoutOrAbort(true);
|
||||
@ -396,7 +401,7 @@ inline void IR_DecoderRaw::checkTimeout()
|
||||
if (rxTimeoutPipelineBusy())
|
||||
return;
|
||||
|
||||
if (micros() - lastEdgeTime > IR_timeout * 2U)
|
||||
if (micros() - lastEdgeTime > receiveSilenceTimeoutUs())
|
||||
{
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceOnTimeoutOrAbort(false);
|
||||
@ -767,8 +772,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
#if !defined(IRDEBUG_SERIAL_PACK)
|
||||
(void)packTraceInvertFix;
|
||||
#endif
|
||||
if (i_dataBuffer >= dataByteSizeMax * 8)
|
||||
{ // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3)
|
||||
if (i_dataBuffer >= irproto::kMaxWireFrameBytes * 8U)
|
||||
{ // >=: не даёт записать бит за пределом 5-битной wire-длины.
|
||||
isBufferOverflow = true;
|
||||
rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros());
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
@ -876,7 +881,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
{ // Ппервый байт
|
||||
packSize = dataBuffer[0] & IR_MASK_MSG_INFO;
|
||||
// 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-чтение wire-буфера.
|
||||
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
|
||||
if (packSize != 0 && packSize < msgBytes + crcBytes)
|
||||
isWrongPack = true;
|
||||
@ -910,7 +915,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
uint8_t packTraceBfBit = 0;
|
||||
bool packTraceBfMark = false;
|
||||
if (!isAvailable) // Исправление первого бита // Очень большая затычка...
|
||||
for (size_t i = 0; i < min(uint16_t(packSize - crcBytes * 2U), uint16_t(dataByteSizeMax)); ++i)
|
||||
for (size_t i = 0; i < min(uint16_t(packSize - crcBytes * 2U), uint16_t(irproto::kMaxWireFrameBytes)); ++i)
|
||||
{
|
||||
for (int j = 0; j < 8; ++j)
|
||||
{
|
||||
@ -918,7 +923,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
dataBuffer[i] ^= 1 << j;
|
||||
|
||||
isAvailable =
|
||||
crcCheck(min(uint16_t(packSize - crcBytes), uint16_t(dataByteSizeMax - 1U)), crcValue);
|
||||
crcCheck(min(uint16_t(packSize - crcBytes), uint16_t(irproto::kMaxWireFrameBytes - 1U)), crcValue);
|
||||
// обратно инвертируем бит в исходное состояние
|
||||
|
||||
if (isAvailable)
|
||||
@ -953,7 +958,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros());
|
||||
else
|
||||
rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros());
|
||||
if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) {
|
||||
if (!isAvailable && packSize > 0 && packSize <= irproto::kMaxWireFrameBytes) {
|
||||
memcpy(rejectBuffer, dataBuffer, packSize);
|
||||
rejectPackSize = static_cast<uint8_t>(packSize);
|
||||
isRejectAvailable = true;
|
||||
@ -1007,38 +1012,34 @@ uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider)
|
||||
void IR_DecoderRaw::edgeTracePush(uint32_t t_us, uint8_t level, uint8_t flags)
|
||||
{
|
||||
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
const uint16_t w = edgeTrace_w;
|
||||
const uint16_t r = edgeTrace_r;
|
||||
const uint16_t next = static_cast<uint16_t>((w + 1u) % cap);
|
||||
if (next == r)
|
||||
{
|
||||
edgeTrace_overflow = true;
|
||||
interrupts();
|
||||
return;
|
||||
}
|
||||
edgeTrace_buf[w].t_us = t_us;
|
||||
edgeTrace_buf[w].level = level ? 1u : 0u;
|
||||
edgeTrace_buf[w].flags = flags;
|
||||
edgeTrace_w = next;
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::edgeTraceClear()
|
||||
{
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
edgeTrace_w = 0;
|
||||
edgeTrace_r = 0;
|
||||
edgeTrace_overflow = false;
|
||||
interrupts();
|
||||
}
|
||||
|
||||
uint16_t IR_DecoderRaw::edgeTracePendingCount() const
|
||||
{
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
const uint16_t w = edgeTrace_w;
|
||||
const uint16_t r = edgeTrace_r;
|
||||
interrupts();
|
||||
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
||||
if (w >= r)
|
||||
return static_cast<uint16_t>(w - r);
|
||||
@ -1054,27 +1055,28 @@ uint16_t IR_DecoderRaw::edgeTraceFlushChunk(Print &out, uint16_t maxRec)
|
||||
maxRec = kStackCap;
|
||||
|
||||
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
||||
noInterrupts();
|
||||
const uint16_t w = edgeTrace_w;
|
||||
const uint16_t r = edgeTrace_r;
|
||||
uint16_t avail = (w >= r) ? static_cast<uint16_t>(w - r) : static_cast<uint16_t>(cap - r + w);
|
||||
uint16_t toCopy = (avail > maxRec) ? maxRec : avail;
|
||||
const bool truncated = (avail > toCopy);
|
||||
if (toCopy == 0)
|
||||
{
|
||||
interrupts();
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t tmp[kStackCap * 6];
|
||||
for (uint16_t i = 0; i < toCopy; ++i)
|
||||
uint16_t toCopy = 0U;
|
||||
bool truncated = false;
|
||||
bool ovf = false;
|
||||
{
|
||||
const uint16_t idx = static_cast<uint16_t>((r + i) % cap);
|
||||
memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u);
|
||||
IrInterruptGuard guard;
|
||||
const uint16_t w = edgeTrace_w;
|
||||
const uint16_t r = edgeTrace_r;
|
||||
const uint16_t avail = (w >= r) ? static_cast<uint16_t>(w - r)
|
||||
: static_cast<uint16_t>(cap - r + w);
|
||||
toCopy = (avail > maxRec) ? maxRec : avail;
|
||||
truncated = (avail > toCopy);
|
||||
if (toCopy == 0U)
|
||||
return 0U;
|
||||
for (uint16_t i = 0; i < toCopy; ++i)
|
||||
{
|
||||
const uint16_t idx = static_cast<uint16_t>((r + i) % cap);
|
||||
memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u);
|
||||
}
|
||||
edgeTrace_r = static_cast<uint16_t>((r + toCopy) % cap);
|
||||
ovf = edgeTrace_overflow;
|
||||
}
|
||||
edgeTrace_r = static_cast<uint16_t>((r + toCopy) % cap);
|
||||
const bool ovf = edgeTrace_overflow;
|
||||
interrupts();
|
||||
|
||||
uint8_t meta = 0;
|
||||
if (ovf)
|
||||
@ -1304,7 +1306,7 @@ void IR_DecoderRaw::packTraceForceEndSyncPhase()
|
||||
void IR_DecoderRaw::packTraceEmitHex(uint8_t byteCount) const
|
||||
{
|
||||
Serial.print(F("IR hex:"));
|
||||
for (uint8_t i = 0; i < byteCount && i < dataByteSizeMax; i++)
|
||||
for (uint8_t i = 0; i < byteCount && i < irproto::kMaxWireFrameBytes; i++)
|
||||
{
|
||||
Serial.print(' ');
|
||||
ptPrintHexU8(dataBuffer[i]);
|
||||
@ -1402,8 +1404,8 @@ void IR_DecoderRaw::packTraceEmitErrorFlash(const __FlashStringHelper *msg)
|
||||
Serial.println(msg);
|
||||
{
|
||||
uint16_t nb = i_dataBuffer / 8u;
|
||||
if (nb > dataByteSizeMax)
|
||||
nb = dataByteSizeMax;
|
||||
if (nb > irproto::kMaxWireFrameBytes)
|
||||
nb = irproto::kMaxWireFrameBytes;
|
||||
packTraceEmitHex(static_cast<uint8_t>(nb));
|
||||
}
|
||||
packTraceResetFrame();
|
||||
@ -1436,8 +1438,8 @@ void IR_DecoderRaw::packTraceOnTimeoutOrAbort(bool fromListenStart)
|
||||
return;
|
||||
const uint16_t expected = (i_dataBuffer >= 8) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0;
|
||||
uint16_t gotBytes = i_dataBuffer / 8;
|
||||
if (gotBytes > dataByteSizeMax)
|
||||
gotBytes = dataByteSizeMax;
|
||||
if (gotBytes > irproto::kMaxWireFrameBytes)
|
||||
gotBytes = irproto::kMaxWireFrameBytes;
|
||||
Serial.println();
|
||||
packTraceEmitRawBitsLine(false);
|
||||
Serial.print(F(" => ERROR: TIMEOUT, rx_data_size = "));
|
||||
@ -1609,7 +1611,7 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
|
||||
|
||||
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||
{
|
||||
const uint32_t longSilence = IR_timeout * 2U;
|
||||
const uint32_t longSilence = receiveSilenceTimeoutUs();
|
||||
const uint32_t candTimeout = IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT;
|
||||
|
||||
if (preambleState == PreambleState::Idle)
|
||||
@ -1696,7 +1698,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||
err_syncBit = 0;
|
||||
isWrongPack = false;
|
||||
msgTypeReceive = 0;
|
||||
memset(dataBuffer, 0x00, dataByteSizeMax);
|
||||
memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes);
|
||||
|
||||
preambleState = PreambleState::Locked;
|
||||
isPreamb = false;
|
||||
|
||||
@ -20,12 +20,13 @@ class Print;
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#define riseTime riseSyncTime //* bitTime */ 893U // TODO: Должно высчитываться медианой
|
||||
#define riseTolerance tolerance /* 250U */ // погрешность
|
||||
#define riseTolerance IR_TIMING_TOLERANCE_US /* 250U */ // погрешность
|
||||
#define riseTimeMax (riseTime + riseTolerance)
|
||||
#define riseTimeMin (riseTime - riseTolerance)
|
||||
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
||||
#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;
|
||||
// Compatibility aliases. The named contracts and their geometry live in IR_config.h.
|
||||
#define IR_timeout (::irproto::rxInterEdgeTimeoutUs(riseTime))
|
||||
constexpr uint16_t IR_ResponseDelay = irproto::kDefaultResponseTurnaroundDelayMs;
|
||||
|
||||
class IR_Encoder;
|
||||
class IR_DecoderRaw : virtual public IR_FOX
|
||||
@ -51,7 +52,14 @@ public:
|
||||
|
||||
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
||||
bool isSubOverflow();
|
||||
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
|
||||
inline bool isReciving() const { return isRecive; } // Возвращает true, если происходит приём пакета
|
||||
/** Current adaptive silence threshold that terminates an RX candidate. */
|
||||
inline uint32_t receiveSilenceTimeoutUs() const {
|
||||
return irproto::rxSilenceTimeoutUs(riseTime);
|
||||
}
|
||||
inline uint32_t receiveSilenceTimeoutMsCeil() const {
|
||||
return irproto::microsToMillisCeil(receiveSilenceTimeoutUs());
|
||||
}
|
||||
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
|
||||
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
|
||||
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
|
||||
@ -68,8 +76,9 @@ public:
|
||||
/// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch,
|
||||
/// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok).
|
||||
/// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат).
|
||||
static constexpr uint8_t rxReasonCounterCount() { return kRxBriefReasonCount; }
|
||||
const uint16_t *rxReasonCounters() const { return rxReasonCnt; }
|
||||
void rxReasonCountersClear() { for (uint8_t i = 0; i < 14; i++) rxReasonCnt[i] = 0; }
|
||||
void rxReasonCountersClear() { for (uint8_t i = 0; i < kRxBriefReasonCount; ++i) rxReasonCnt[i] = 0; }
|
||||
/// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.."
|
||||
void printRxReasonStats(Print &out) const;
|
||||
|
||||
@ -94,12 +103,14 @@ private:
|
||||
BufferOverflow = 10,
|
||||
Timeout = 11,
|
||||
Crc = 12,
|
||||
Ok = 13
|
||||
Ok = 13,
|
||||
Count
|
||||
};
|
||||
static constexpr uint8_t kRxBriefReasonCount = static_cast<uint8_t>(RxBriefReason::Count);
|
||||
|
||||
bool isRejectAvailable = false;
|
||||
uint8_t rejectPackSize = 0;
|
||||
uint8_t rejectBuffer[dataByteSizeMax]{};
|
||||
uint8_t rejectBuffer[irproto::kMaxWireFrameBytes]{};
|
||||
|
||||
ErrorsStruct errors;
|
||||
bool isAvailable = false;
|
||||
@ -179,7 +190,7 @@ private:
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
uint8_t dataBuffer[dataByteSizeMax]{0}; // Буффер данных
|
||||
uint8_t dataBuffer[irproto::kMaxWireFrameBytes]{0}; // Буффер полного wire-кадра
|
||||
volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов
|
||||
|
||||
volatile uint32_t risePeriod;
|
||||
@ -247,7 +258,7 @@ bool isReciveRaw = false;
|
||||
// (always-on наблюдаемость по контракту живучести), печать события —
|
||||
// только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны.
|
||||
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
|
||||
uint16_t rxReasonCnt[14] = {}; // индекс = (uint8_t)RxBriefReason, 1..13
|
||||
uint16_t rxReasonCnt[kRxBriefReasonCount] = {}; // индекс = RxBriefReason, 1..Count-1
|
||||
#if IR_RX_BRIEF_LOG
|
||||
static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason);
|
||||
void rxBriefNoteMuteBlockedIsr(uint32_t tUs);
|
||||
@ -263,7 +274,7 @@ bool isReciveRaw = false;
|
||||
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
static constexpr uint16_t kPackTraceBufCap =
|
||||
uint16_t(dataByteSizeMax) * (uint16_t(bitPerByte) + uint16_t(syncBits)) + 48u;
|
||||
uint16_t(irproto::kMaxWireFrameBytes) * (uint16_t(bitPerByte) + uint16_t(syncBits)) + 48u;
|
||||
|
||||
void packTraceResetFrame();
|
||||
void packTracePushBit(bool bit);
|
||||
|
||||
728
IR_Encoder.cpp
728
IR_Encoder.cpp
File diff suppressed because it is too large
Load Diff
144
IR_Encoder.h
144
IR_Encoder.h
@ -18,20 +18,88 @@ enum class IR_SendStatus : uint8_t {
|
||||
DmaStartFailed,
|
||||
EncoderPinUnavailable,
|
||||
BufferedStorageInvalid,
|
||||
InvalidArgument,
|
||||
TimingOverflow,
|
||||
PlanMismatch,
|
||||
DmaTransferError,
|
||||
DmaStalled,
|
||||
};
|
||||
|
||||
const char* irSendStatusToString(IR_SendStatus status);
|
||||
|
||||
enum class IR_TxState : uint8_t {
|
||||
Idle = 0,
|
||||
Preparing,
|
||||
Transmitting,
|
||||
Completed,
|
||||
Failed,
|
||||
};
|
||||
|
||||
enum class IR_TxClockBasis : uint8_t {
|
||||
Nominal = 0,
|
||||
ConfiguredTimer,
|
||||
};
|
||||
|
||||
/**
|
||||
* Deterministic PHY plan produced by the same FSM that builds the actual
|
||||
* carrier-gate stream. airtimeUs is rounded up, so it is safe as a deadline
|
||||
* component; it does not include backend preparation or release latency.
|
||||
*/
|
||||
struct IR_TxPlan {
|
||||
IR_SendStatus status = IR_SendStatus::InvalidArgument;
|
||||
uint8_t wireBytes = 0;
|
||||
uint16_t carrierMultiply = 0;
|
||||
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal;
|
||||
uint32_t tickClockHz = 0; // rational tick rate numerator
|
||||
uint32_t tickDivider = 1; // rational tick rate denominator
|
||||
uint32_t physicalTicks = 0;
|
||||
uint32_t gateRunCount = 0;
|
||||
uint32_t airtimeUs = 0;
|
||||
|
||||
bool valid() const { return status == IR_SendStatus::Success; }
|
||||
uint32_t tickHzFloor() const { return tickDivider == 0U ? 0U : tickClockHz / tickDivider; }
|
||||
uint32_t airtimeMsCeil() const { return (airtimeUs + 999U) / 1000U; }
|
||||
};
|
||||
|
||||
/** Coherent main-context snapshot of one encoder's latest accepted operation. */
|
||||
struct IR_TxSnapshot {
|
||||
uint32_t operationId = 0;
|
||||
IR_TxState state = IR_TxState::Idle;
|
||||
IR_SendStatus status = IR_SendStatus::Success;
|
||||
uint16_t carrierMultiply = 0;
|
||||
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal;
|
||||
uint32_t plannedPhysicalTicks = 0;
|
||||
uint32_t plannedAirtimeUs = 0;
|
||||
uint32_t acceptedAtUs = 0;
|
||||
uint32_t armedAtUs = 0;
|
||||
uint32_t terminalAtUs = 0;
|
||||
|
||||
bool active() const {
|
||||
return state == IR_TxState::Preparing || state == IR_TxState::Transmitting;
|
||||
}
|
||||
bool terminal() const {
|
||||
return state == IR_TxState::Completed || state == IR_TxState::Failed;
|
||||
}
|
||||
};
|
||||
|
||||
// Структура для возврата результата отправки
|
||||
struct IR_SendResult {
|
||||
bool success; // Флаг успешности отправки
|
||||
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах
|
||||
bool success; // true: backend принял и запустил эту операцию
|
||||
uint32_t sendTimeMs; // ceil(plannedAirtimeUs / 1000), compatibility field
|
||||
IR_SendStatus status; // Детализированный статус старта передачи
|
||||
uint32_t operationId; // 0, если новая операция не создавалась
|
||||
uint32_t plannedAirtimeUs; // PHY airtime; без подготовки/release backend-а
|
||||
IR_TxClockBasis clockBasis;
|
||||
|
||||
IR_SendResult(bool success = false,
|
||||
uint32_t sendTimeMs = 0,
|
||||
IR_SendStatus status = IR_SendStatus::ExternalStartFailed)
|
||||
: success(success), sendTimeMs(sendTimeMs), status(status) {}
|
||||
IR_SendStatus status = IR_SendStatus::ExternalStartFailed,
|
||||
uint32_t operationId = 0,
|
||||
uint32_t plannedAirtimeUs = 0,
|
||||
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal)
|
||||
: success(success), sendTimeMs(sendTimeMs), status(status),
|
||||
operationId(operationId), plannedAirtimeUs(plannedAirtimeUs),
|
||||
clockBasis(clockBasis) {}
|
||||
};
|
||||
|
||||
class IR_DecoderRaw;
|
||||
@ -53,6 +121,12 @@ public:
|
||||
|
||||
using ExternalTxBusyFn = bool (*)(void *ctx);
|
||||
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
||||
using ExternalTxStartFnV2 = IR_SendStatus (*)(void *ctx,
|
||||
IR_Encoder *enc,
|
||||
const uint8_t *packet,
|
||||
uint8_t len,
|
||||
const IR_TxPlan& plan,
|
||||
uint32_t operationId);
|
||||
private:
|
||||
// uint16_t id; /// @brief Адрес передатчика
|
||||
public:
|
||||
@ -111,14 +185,32 @@ public:
|
||||
|
||||
/** Optional: register external TX backend (e.g. DMA driver). */
|
||||
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||||
/** Token-aware backend contract. Prefer this overload for every new backend. */
|
||||
static void setExternalTxBackendV2(ExternalTxStartFnV2 startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||||
|
||||
/** Called by external TX backend on actual end of transmission. */
|
||||
/** Legacy completion hook. It cannot reject a stale completion; retained for source compatibility. */
|
||||
void externalFinishSend();
|
||||
/** Complete exactly operationId; stale/duplicate completions are ignored. */
|
||||
void externalFinishSend(uint32_t operationId, IR_SendStatus terminalStatus);
|
||||
|
||||
/** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */
|
||||
static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns);
|
||||
/** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */
|
||||
static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply);
|
||||
/** Preflight the exact physical stream without allocating or touching hardware. */
|
||||
static IR_TxPlan planPhysicalTransmission(const uint8_t *packet, uint8_t len, uint16_t multiply);
|
||||
/** Build into caller storage and report both required runs and exact timing. */
|
||||
static IR_TxPlan buildPhysicalTransmission(const uint8_t *packet,
|
||||
uint8_t len,
|
||||
IR_TxGateRun *outRuns,
|
||||
size_t maxRuns,
|
||||
uint16_t multiply);
|
||||
/** Replace nominal tick rate with an exact rational backend clock. */
|
||||
static bool applyTickClock(IR_TxPlan& plan,
|
||||
uint32_t clockNumeratorHz,
|
||||
uint32_t clockDivider,
|
||||
IR_TxClockBasis basis = IR_TxClockBasis::ConfiguredTimer);
|
||||
IR_TxPlan planTransmission(const uint8_t *packet, uint8_t len) const;
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
@ -132,6 +224,7 @@ public:
|
||||
setBlindDecoders(decoders, static_cast<uint8_t>(N));
|
||||
}
|
||||
IR_SendStatus rawSend(uint8_t *ptr, uint8_t len);
|
||||
IR_SendResult rawSendTracked(uint8_t *ptr, uint8_t len);
|
||||
|
||||
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
||||
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
||||
@ -155,7 +248,11 @@ public:
|
||||
uint32_t testSendBack(uint8_t *data = nullptr, uint8_t len = 0) const;
|
||||
uint32_t testSendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0) const;
|
||||
|
||||
inline bool isBusy() const { return isSending;}
|
||||
inline bool isBusy() const { return isSending; }
|
||||
/** Main-context coherent snapshot. Do not spin on this from an ISR. */
|
||||
IR_TxSnapshot txSnapshot() const;
|
||||
bool isOperationTerminal(uint32_t operationId) const;
|
||||
bool isOperationComplete(uint32_t operationId) const;
|
||||
|
||||
|
||||
~IR_Encoder();
|
||||
@ -170,6 +267,7 @@ private:
|
||||
static void carrierPauseIfIdle();
|
||||
|
||||
static ExternalTxStartFn externalTxStartFn;
|
||||
static ExternalTxStartFnV2 externalTxStartFnV2;
|
||||
static ExternalTxBusyFn externalTxBusyFn;
|
||||
static void *externalTxCtx;
|
||||
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
|
||||
@ -210,6 +308,15 @@ private:
|
||||
static bool txAdvanceBoundary(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 TxFsmState initialTxFsm(uint8_t len);
|
||||
static IR_TxPlan buildPhysicalPlan(const uint8_t *packet,
|
||||
uint8_t len,
|
||||
IR_TxGateRun *outRuns,
|
||||
size_t maxRuns,
|
||||
uint16_t multiply,
|
||||
bool emitRuns);
|
||||
static bool calculateAirtimeUs(IR_TxPlan& plan);
|
||||
static void applyConfiguredTimerClock(IR_TxPlan& plan);
|
||||
void loadTxFsmFromMembers(TxFsmState &st) const;
|
||||
void storeTxFsmToMembers(const TxFsmState &st);
|
||||
bool shouldUseBufferedIsr() const;
|
||||
@ -218,9 +325,8 @@ private:
|
||||
uint16_t txPowerSnap_ = 1;
|
||||
uint16_t txMultiplySnap_ = 2;
|
||||
|
||||
/** Legacy: физических тиков на один логический шаг FSM = multiply/2. */
|
||||
uint16_t legacyPhysPerLogical_ = 1;
|
||||
uint16_t legacyPhysCounter_ = 0;
|
||||
/** Fractional 2×Fc -> multiply×Fc phase accumulator (also exact for odd multiply). */
|
||||
uint32_t legacyScaleAccumulator_ = 0;
|
||||
uint16_t legacySlotInPeriod_ = 0;
|
||||
|
||||
volatile uint16_t powerNumerator_ = 1;
|
||||
@ -235,9 +341,27 @@ private:
|
||||
uint8_t decodersCount = 0;
|
||||
|
||||
uint8_t sendLen = 0;
|
||||
uint8_t sendBuffer[dataByteSizeMax]{0}; /// @brief Буффер данных для отправки
|
||||
uint8_t sendBuffer[irproto::kMaxWireFrameBytes]{0}; /// @brief Буффер полного wire-кадра
|
||||
|
||||
volatile bool isSending = false;
|
||||
|
||||
// Single-writer-at-a-time record (main starts, ISR/backend terminates).
|
||||
// The byte seqlock makes a coherent main-context snapshot without heap/locks.
|
||||
volatile uint8_t txRecordVersion_ = 0;
|
||||
volatile IR_TxState txState_ = IR_TxState::Idle;
|
||||
volatile IR_SendStatus txTerminalStatus_ = IR_SendStatus::Success;
|
||||
volatile uint32_t txOperationId_ = 0;
|
||||
volatile uint32_t txPlannedPhysicalTicks_ = 0;
|
||||
volatile uint32_t txPlannedAirtimeUs_ = 0;
|
||||
volatile IR_TxClockBasis txClockBasis_ = IR_TxClockBasis::Nominal;
|
||||
volatile uint32_t txAcceptedAtUs_ = 0;
|
||||
volatile uint32_t txArmedAtUs_ = 0;
|
||||
volatile uint32_t txTerminalAtUs_ = 0;
|
||||
uint32_t txNextOperationId_ = 0;
|
||||
|
||||
uint32_t beginTxOperation(const IR_TxPlan& plan);
|
||||
void markTxArmed(uint32_t operationId);
|
||||
bool finishTxOperation(uint32_t operationId, IR_SendStatus terminalStatus);
|
||||
volatile bool state = LOW; /// @brief Текущий уровень генерации
|
||||
|
||||
volatile uint8_t dataByteCounter = 0;
|
||||
|
||||
@ -30,4 +30,4 @@ uint8_t IR_FOX::crc8(uint8_t *data, uint8_t start, uint8_t end, uint8_t poly)
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
};
|
||||
}
|
||||
|
||||
142
IR_config.h
142
IR_config.h
@ -15,8 +15,6 @@ constexpr size_t kDefaultDmaTxMaxStreams = 4U;
|
||||
constexpr uint32_t kDmaTxIrqPriority = 8U;
|
||||
/** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */
|
||||
constexpr uint16_t kIsrTxBsrrWordCount = 256U;
|
||||
/** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */
|
||||
constexpr size_t kIsrTxMaxGateRuns = 512U;
|
||||
static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even");
|
||||
}
|
||||
|
||||
@ -101,7 +99,7 @@ msg type:
|
||||
// ----------
|
||||
// | xxx..... | = тип сообщения (биты 7..5)
|
||||
// | ...xxxxx | = полная длина кадра в байтах (5 бит, 0..31, IR_MASK_MSG_INFO), не «31 бит» и не отдельный лимит «24 байта»
|
||||
// Полезная нагрузка в data pack: до bytePerPack байт (см. #define bytePerPack).
|
||||
// Полезная нагрузка в data pack: до irproto::kMaxDataPayloadBytes байт.
|
||||
// ---------- */
|
||||
#define IR_MSG_BACK 0U // | 000...... | = Задний сигнал машинки
|
||||
#define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение
|
||||
@ -111,7 +109,7 @@ msg type:
|
||||
// #define IR_MSG_ 5U // | 101..... | = ??
|
||||
#define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения
|
||||
#define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения
|
||||
; /* // ----------
|
||||
/* // ----------
|
||||
|
||||
/``````````````````````````````` подтверждение `````````````````````````````\ /``````````````````````````````````````` запрос ``````````````````````````````````\
|
||||
|
||||
@ -159,15 +157,25 @@ msg type:
|
||||
|
||||
*/
|
||||
|
||||
#define IR_MASK_MSG_TYPE 0b00000111
|
||||
#define IR_MASK_MSG_INFO 0b00011111
|
||||
namespace irproto {
|
||||
/** Three high header bits, shifted down, encode the message type. */
|
||||
constexpr uint8_t kMessageTypeMask = 0x07U;
|
||||
/** Five low header bits encode the complete on-wire frame length. */
|
||||
constexpr uint8_t kWireFrameLengthBits = 5U;
|
||||
constexpr uint8_t kWireFrameLengthMask =
|
||||
static_cast<uint8_t>((1U << kWireFrameLengthBits) - 1U);
|
||||
constexpr uint8_t kMaxWireFrameBytes = kWireFrameLengthMask;
|
||||
}
|
||||
|
||||
// Source-compatible aliases. New code should use the typed irproto constants.
|
||||
#define IR_MASK_MSG_TYPE (::irproto::kMessageTypeMask)
|
||||
#define IR_MASK_MSG_INFO (::irproto::kWireFrameLengthMask)
|
||||
|
||||
/*
|
||||
/////////////////////////////////////////////////////////////////////////////////////*/
|
||||
typedef uint16_t crc_t;
|
||||
|
||||
// #define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает
|
||||
#define bytePerPack (31) // колличество байтов в пакете
|
||||
#ifndef freeFrec
|
||||
#define freeFrec false
|
||||
#endif
|
||||
@ -248,8 +256,6 @@ typedef uint16_t crc_t;
|
||||
#define poly2 0x8C
|
||||
#define syncBits 3U // количество битов синхронизации
|
||||
|
||||
#define dataByteSizeMax (msgBytes + addrBytes + addrBytes + bytePerPack + crcBytes)
|
||||
|
||||
#define preambFronts (preambPulse * 2) // количество фронтов преамбулы (Приём)
|
||||
#define preambToggle ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) // колличество переключений преамбулы (Передача)
|
||||
|
||||
@ -262,7 +268,123 @@ typedef uint16_t crc_t;
|
||||
|
||||
#define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах
|
||||
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
||||
#define tolerance 300U
|
||||
namespace irproto {
|
||||
constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
||||
constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
|
||||
constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
||||
constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
|
||||
constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
|
||||
constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
|
||||
|
||||
/** RX timing geometry shared by adaptive and nominal decoder paths. */
|
||||
constexpr uint16_t kRxTimingToleranceUs = 300U;
|
||||
constexpr uint8_t kRxInterEdgeTimeoutGuardBitWindows = 1U;
|
||||
constexpr uint8_t kRxInterEdgeTimeoutBitWindows =
|
||||
static_cast<uint8_t>(bitPerByte + syncBits + kRxInterEdgeTimeoutGuardBitWindows);
|
||||
constexpr uint8_t kRxSilenceTimeoutInterEdgeWindows = 2U;
|
||||
|
||||
/**
|
||||
* Largest accepted rise-to-rise interval for one decoder byte window.
|
||||
* adaptiveBitPeriodUs is riseSyncTime when free-frequency tracking is used.
|
||||
*/
|
||||
constexpr uint32_t rxInterEdgeTimeoutUs(uint32_t adaptiveBitPeriodUs)
|
||||
{
|
||||
return (adaptiveBitPeriodUs + static_cast<uint32_t>(kRxTimingToleranceUs)) *
|
||||
static_cast<uint32_t>(kRxInterEdgeTimeoutBitWindows);
|
||||
}
|
||||
|
||||
/** Silence after which an unfinished RX candidate is retired. */
|
||||
constexpr uint32_t rxSilenceTimeoutUs(uint32_t adaptiveBitPeriodUs)
|
||||
{
|
||||
return rxInterEdgeTimeoutUs(adaptiveBitPeriodUs) *
|
||||
static_cast<uint32_t>(kRxSilenceTimeoutInterEdgeWindows);
|
||||
}
|
||||
|
||||
constexpr uint32_t microsToMillisCeil(uint32_t us)
|
||||
{
|
||||
return (us + 999U) / 1000U;
|
||||
}
|
||||
|
||||
constexpr uint32_t kNominalRxInterEdgeTimeoutUs = rxInterEdgeTimeoutUs(bitTime);
|
||||
constexpr uint32_t kNominalRxSilenceTimeoutUs = rxSilenceTimeoutUs(bitTime);
|
||||
|
||||
/**
|
||||
* Deployed response/ACK turn-around policy.
|
||||
*
|
||||
* This is empirical, not a PHY invariant. Commit 1353ab6 replaced the older
|
||||
* fixed 75 ms with a floating expression whose only reproducible result at the
|
||||
* nominal PHY is 42 ms; no measurement or physical derivation was recorded.
|
||||
* Keep the deployed value until a hardware gap campaign establishes a new
|
||||
* channel-turn-around contract.
|
||||
*/
|
||||
constexpr uint16_t kDefaultResponseTurnaroundDelayMs = 42U;
|
||||
|
||||
/**
|
||||
* Conservative logical run bound: preamble transitions plus two gate runs for
|
||||
* every data/sync bit. Physical splitting for unusually large multiply values
|
||||
* is reported by IR_TxPlan::gateRunCount and may require custom storage.
|
||||
*/
|
||||
constexpr size_t kMaxLogicalGateRuns =
|
||||
static_cast<size_t>(preambPulse * 2U) +
|
||||
static_cast<size_t>(kMaxWireFrameBytes) *
|
||||
static_cast<size_t>((bitPerByte + syncBits) * 2U);
|
||||
constexpr size_t kIsrTxMaxGateRuns = kMaxLogicalGateRuns;
|
||||
|
||||
/**
|
||||
* Compile-time PHY storage contract.
|
||||
*
|
||||
* Every data and sync bit occupies bitTakts * 2 ticks on the logical
|
||||
* 2*carrierFrec clock, independently of its value. A physical gate run is
|
||||
* stored in uint16_t and can therefore split at UINT16_MAX ticks. The bound
|
||||
* below includes the worst possible number of such split pieces; applications
|
||||
* can size fixed DMA/ISR storage from the protocol instead of duplicating a
|
||||
* packet-size constant.
|
||||
*/
|
||||
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 kMaxLogicalTransmissionTicks =
|
||||
kPreambleLogicalTicks +
|
||||
static_cast<uint32_t>(kMaxWireFrameBytes) *
|
||||
static_cast<uint32_t>(bitPerByte + syncBits) *
|
||||
kEncodedBitLogicalTicks;
|
||||
|
||||
constexpr uint16_t normalizedCarrierMultiply(uint16_t multiply)
|
||||
{
|
||||
return multiply < 2U ? 2U : multiply;
|
||||
}
|
||||
|
||||
constexpr uint64_t maxPhysicalTransmissionTicks(uint16_t multiply)
|
||||
{
|
||||
return (static_cast<uint64_t>(kMaxLogicalTransmissionTicks) *
|
||||
static_cast<uint64_t>(normalizedCarrierMultiply(multiply)) +
|
||||
1U) /
|
||||
2U;
|
||||
}
|
||||
|
||||
constexpr size_t maxPhysicalGateRunCapacity(uint16_t multiply)
|
||||
{
|
||||
return kMaxLogicalGateRuns +
|
||||
static_cast<size_t>(maxPhysicalTransmissionTicks(multiply) /
|
||||
static_cast<uint64_t>(UINT16_MAX));
|
||||
}
|
||||
|
||||
static_assert(kMaxDataPayloadBytes == 24U, "IR DATA payload contract changed");
|
||||
static_assert(kMaxBackPayloadBytes == 26U, "IR BACK payload contract changed");
|
||||
static_assert(kNominalRxInterEdgeTimeoutUs == 15144U, "IR RX timeout contract changed");
|
||||
static_assert(kNominalRxSilenceTimeoutUs == 30288U, "IR RX silence contract changed");
|
||||
static_assert(kMaxLogicalTransmissionTicks <= UINT32_MAX,
|
||||
"IR maximum transmission no longer fits IR_TxPlan");
|
||||
}
|
||||
|
||||
// Deprecated source-compatible names. They are aliases only and no longer
|
||||
// define independent storage/payload limits. bytePerPack historically meant
|
||||
// 31; preserving that value avoids silently changing external sketches.
|
||||
#define bytePerPack (::irproto::kMaxWireFrameBytes)
|
||||
#define dataByteSizeMax (::irproto::kMaxWireFrameBytes)
|
||||
#define IR_TIMING_TOLERANCE_US (::irproto::kRxTimingToleranceUs)
|
||||
|
||||
constexpr uint16_t test_all_Time = bitTime;
|
||||
constexpr uint16_t test_all_Takts = bitTakts * 2;
|
||||
|
||||
@ -120,7 +120,23 @@ public:
|
||||
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
|
||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||
if (streams_[i].enc == enc) {
|
||||
return startStream(streams_[i], packet, len);
|
||||
const IR_TxSnapshot snapshot = enc->txSnapshot();
|
||||
const IR_TxPlan plan = enc->planTransmission(packet, len);
|
||||
return startStream(streams_[i], packet, len, plan, snapshot.operationId);
|
||||
}
|
||||
}
|
||||
return IR_SendStatus::ExternalNoStream;
|
||||
}
|
||||
|
||||
IR_SendStatus startTracked(IR_Encoder* enc,
|
||||
const uint8_t* packet,
|
||||
uint8_t len,
|
||||
const IR_TxPlan& plan,
|
||||
uint32_t operationId) {
|
||||
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
|
||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||
if (streams_[i].enc == enc) {
|
||||
return startStream(streams_[i], packet, len, plan, operationId);
|
||||
}
|
||||
}
|
||||
return IR_SendStatus::ExternalNoStream;
|
||||
@ -192,6 +208,7 @@ private:
|
||||
|
||||
uint32_t totalTicks = 0;
|
||||
volatile uint32_t ticksOutput = 0;
|
||||
uint32_t operationId = 0;
|
||||
|
||||
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
|
||||
uint32_t lastTicks = 0;
|
||||
@ -204,20 +221,23 @@ private:
|
||||
ticksOutput = 0;
|
||||
totalTicks = 0;
|
||||
runCount = 0;
|
||||
operationId = 0;
|
||||
}
|
||||
|
||||
IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) {
|
||||
wave.fill(dst, count);
|
||||
}
|
||||
|
||||
void onHalf() {
|
||||
void advanceHalf() {
|
||||
ticksOutput += halfLen;
|
||||
}
|
||||
|
||||
void refillFirstHalf() {
|
||||
fill(&dmaBuf[0], halfLen);
|
||||
__DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца
|
||||
}
|
||||
|
||||
void onComplete() {
|
||||
ticksOutput += halfLen;
|
||||
void refillSecondHalf() {
|
||||
fill(&dmaBuf[halfLen], halfLen);
|
||||
__DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца
|
||||
}
|
||||
@ -262,7 +282,7 @@ private:
|
||||
void forceStop(TxStream& s) {
|
||||
HAL_NVIC_DisableIRQ(s.dmaIrq);
|
||||
if (s.active) {
|
||||
stopStream(s);
|
||||
stopStream(s, IR_SendStatus::DmaStalled);
|
||||
recoveries_++;
|
||||
}
|
||||
HAL_NVIC_EnableIRQ(s.dmaIrq);
|
||||
@ -281,18 +301,22 @@ private:
|
||||
static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) {
|
||||
auto* s = streamFromDma(hdma);
|
||||
if (s == nullptr || !s->active) return;
|
||||
s->onHalf();
|
||||
s->advanceHalf();
|
||||
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
||||
s_instance->stopStream(*s);
|
||||
s_instance->stopStream(*s, IR_SendStatus::Success);
|
||||
} else {
|
||||
s->refillFirstHalf();
|
||||
}
|
||||
}
|
||||
|
||||
static void dmaCpltCb(DMA_HandleTypeDef* hdma) {
|
||||
auto* s = streamFromDma(hdma);
|
||||
if (s == nullptr || !s->active) return;
|
||||
s->onComplete();
|
||||
s->advanceHalf();
|
||||
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
||||
s_instance->stopStream(*s);
|
||||
s_instance->stopStream(*s, IR_SendStatus::Success);
|
||||
} else {
|
||||
s->refillSecondHalf();
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,7 +326,7 @@ private:
|
||||
s->onError();
|
||||
if (s_instance != nullptr) {
|
||||
s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error
|
||||
s_instance->stopStream(*s);
|
||||
s_instance->stopStream(*s, IR_SendStatus::DmaTransferError);
|
||||
}
|
||||
}
|
||||
|
||||
@ -348,21 +372,29 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
|
||||
IR_SendStatus startStream(TxStream& s,
|
||||
const uint8_t* packet,
|
||||
uint8_t len,
|
||||
const IR_TxPlan& expectedPlan,
|
||||
uint32_t operationId) {
|
||||
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||
if (s.active) return IR_SendStatus::EncoderBusy;
|
||||
if (!expectedPlan.valid() || operationId == 0U) return IR_SendStatus::ExternalInvalidConfig;
|
||||
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||
if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||
|
||||
s.resetWave();
|
||||
|
||||
const uint16_t mult = IR_Encoder::carrierMultiply();
|
||||
s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult);
|
||||
if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed;
|
||||
|
||||
uint32_t total = 0;
|
||||
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks;
|
||||
s.totalTicks = total;
|
||||
const uint16_t mult = expectedPlan.carrierMultiply;
|
||||
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||
packet, len, s.runs, s.maxRuns, mult);
|
||||
if (!built.valid()) return built.status;
|
||||
if (built.physicalTicks != expectedPlan.physicalTicks ||
|
||||
built.gateRunCount != expectedPlan.gateRunCount)
|
||||
return IR_SendStatus::PlanMismatch;
|
||||
s.runCount = static_cast<size_t>(built.gateRunCount);
|
||||
s.totalTicks = built.physicalTicks;
|
||||
s.operationId = operationId;
|
||||
|
||||
uint16_t pwr = mult / 2U;
|
||||
if (s.enc != nullptr) {
|
||||
@ -388,6 +420,7 @@ private:
|
||||
const uint32_t dst = u32ptr(&s.port->BSRR);
|
||||
if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) {
|
||||
s.active = false;
|
||||
s.operationId = 0U;
|
||||
return IR_SendStatus::DmaStartFailed;
|
||||
}
|
||||
|
||||
@ -395,10 +428,12 @@ private:
|
||||
return IR_SendStatus::Success;
|
||||
}
|
||||
|
||||
void stopStream(TxStream& s) {
|
||||
void stopStream(TxStream& s, IR_SendStatus terminalStatus) {
|
||||
if (!s.active) return;
|
||||
|
||||
const uint32_t operationId = s.operationId;
|
||||
s.active = false;
|
||||
s.operationId = 0U;
|
||||
HAL_DMA_Abort_IT(&s.hdma);
|
||||
|
||||
if (s.port != nullptr) {
|
||||
@ -406,7 +441,7 @@ private:
|
||||
}
|
||||
|
||||
if (s.enc != nullptr) {
|
||||
s.enc->externalFinishSend();
|
||||
s.enc->externalFinishSend(operationId, terminalStatus);
|
||||
}
|
||||
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
|
||||
}
|
||||
|
||||
51
IrInterruptGuard.h
Normal file
51
IrInterruptGuard.h
Normal file
@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#if defined(__AVR__)
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/io.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Nest-safe interrupt guard for the short ISR/main shared-state sections used
|
||||
* by IR-protocol. Unlike a noInterrupts()/interrupts() pair it restores the
|
||||
* previous state and therefore never enables interrupts from inside an ISR.
|
||||
*/
|
||||
class IrInterruptGuard final
|
||||
{
|
||||
public:
|
||||
IrInterruptGuard()
|
||||
{
|
||||
#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32)
|
||||
state_ = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
#elif defined(__AVR__)
|
||||
state_ = SREG;
|
||||
cli();
|
||||
#else
|
||||
noInterrupts();
|
||||
#endif
|
||||
}
|
||||
|
||||
~IrInterruptGuard()
|
||||
{
|
||||
#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32)
|
||||
if ((state_ & 1U) == 0U)
|
||||
__enable_irq();
|
||||
#elif defined(__AVR__)
|
||||
SREG = static_cast<uint8_t>(state_);
|
||||
#else
|
||||
interrupts();
|
||||
#endif
|
||||
}
|
||||
|
||||
IrInterruptGuard(const IrInterruptGuard&) = delete;
|
||||
IrInterruptGuard& operator=(const IrInterruptGuard&) = delete;
|
||||
|
||||
private:
|
||||
#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32) || \
|
||||
defined(__AVR__)
|
||||
uint32_t state_ = 0U;
|
||||
#endif
|
||||
};
|
||||
@ -2,12 +2,49 @@
|
||||
|
||||
namespace PacketTypes
|
||||
{
|
||||
bool BasePack::checkAddress() { return true; };
|
||||
void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id)
|
||||
uint8_t minimumPacketSize(uint8_t msgType)
|
||||
{
|
||||
switch (msgType)
|
||||
{
|
||||
case IR_MSG_DATA_ACCEPT:
|
||||
case IR_MSG_DATA_NOACCEPT:
|
||||
case IR_MSG_BACK_TO:
|
||||
case IR_MSG_REQUEST:
|
||||
return uint8_t(msgBytes + addrBytes + addrBytes + crcBytes);
|
||||
case IR_MSG_BACK:
|
||||
return uint8_t(msgBytes + addrBytes + crcBytes);
|
||||
case IR_MSG_ACCEPT:
|
||||
return uint8_t(msgBytes + addrBytes + 1U + crcBytes);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize)
|
||||
{
|
||||
const uint8_t minimum = minimumPacketSize(msgType);
|
||||
return minimum != 0 && packSize >= minimum;
|
||||
}
|
||||
|
||||
bool BasePack::checkAddress() { return true; }
|
||||
bool BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize)
|
||||
{
|
||||
isAvailable = false;
|
||||
isRawAvailable = false;
|
||||
this->packInfo = packInfo;
|
||||
this->id = id;
|
||||
|
||||
if (packInfo == nullptr || packInfo->buffer == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
|
||||
if (requireTypedSize && !isTypedPacketSizeValid(msgType, packInfo->packSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkAddress())
|
||||
{
|
||||
isAvailable = true;
|
||||
@ -23,29 +60,65 @@ namespace PacketTypes
|
||||
Serial.print(" NOT-OK ");
|
||||
#endif
|
||||
}
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
uint16_t BasePack::_getAddrFrom(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||
obj->packInfo->packSize < crcBytes ||
|
||||
uint16_t(obj->addressFromOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
|
||||
};
|
||||
}
|
||||
uint16_t BasePack::_getAddrTo(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||
obj->packInfo->packSize < crcBytes ||
|
||||
uint16_t(obj->addressToOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
|
||||
};
|
||||
}
|
||||
|
||||
uint8_t BasePack::_getDataSize(BasePack *obj)
|
||||
{
|
||||
return obj->packInfo->packSize - crcBytes - obj->DataOffset;
|
||||
};
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
const uint16_t frameOverhead = uint16_t(crcBytes) + uint16_t(obj->DataOffset);
|
||||
if (uint16_t(obj->packInfo->packSize) <= frameOverhead)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return uint8_t(uint16_t(obj->packInfo->packSize) - frameOverhead);
|
||||
}
|
||||
uint8_t *BasePack::_getDataPrt(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
|
||||
obj->packInfo->packSize < crcBytes)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const uint16_t dataEnd = uint16_t(obj->packInfo->packSize) - uint16_t(crcBytes);
|
||||
if (uint16_t(obj->DataOffset) > dataEnd)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return obj->packInfo->buffer + obj->DataOffset;
|
||||
};
|
||||
}
|
||||
uint8_t BasePack::_getDataRawSize(BasePack *obj)
|
||||
{
|
||||
if (obj == nullptr || obj->packInfo == nullptr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return obj->packInfo->packSize;
|
||||
};
|
||||
}
|
||||
|
||||
bool BasePack::available()
|
||||
{
|
||||
@ -59,7 +132,7 @@ namespace PacketTypes
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
bool BasePack::availableRaw()
|
||||
{
|
||||
if (isRawAvailable)
|
||||
@ -71,7 +144,7 @@ namespace PacketTypes
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bool Data::checkAddress()
|
||||
{
|
||||
|
||||
@ -4,25 +4,34 @@
|
||||
class IR_Decoder;
|
||||
namespace PacketTypes
|
||||
{
|
||||
/**
|
||||
* Minimum complete frame size (header, addresses/data required by the type,
|
||||
* and CRC). Unknown/reserved message types return 0.
|
||||
*/
|
||||
uint8_t minimumPacketSize(uint8_t msgType);
|
||||
|
||||
/** True only for a known typed packet whose complete frame is long enough. */
|
||||
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize);
|
||||
|
||||
class BasePack
|
||||
{
|
||||
friend IR_Decoder;
|
||||
|
||||
protected:
|
||||
bool isAvailable;
|
||||
bool isRawAvailable;
|
||||
bool isNeedAccept;
|
||||
bool isAvailable = false;
|
||||
bool isRawAvailable = false;
|
||||
bool isNeedAccept = false;
|
||||
|
||||
uint8_t msgOffset;
|
||||
uint8_t addressFromOffset;
|
||||
uint8_t addressToOffset;
|
||||
uint8_t DataOffset;
|
||||
uint8_t msgOffset = 0;
|
||||
uint8_t addressFromOffset = 0;
|
||||
uint8_t addressToOffset = 0;
|
||||
uint8_t DataOffset = 0;
|
||||
|
||||
IR_FOX::PackInfo *packInfo;
|
||||
uint16_t id;
|
||||
IR_FOX::PackInfo *packInfo = nullptr;
|
||||
uint16_t id = 0;
|
||||
|
||||
virtual bool checkAddress();
|
||||
void set(IR_FOX::PackInfo *packInfo, uint16_t id);
|
||||
bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true);
|
||||
|
||||
static uint16_t _getAddrFrom(BasePack *obj);
|
||||
static uint16_t _getAddrTo(BasePack *obj);
|
||||
|
||||
14
RingBuffer.h
14
RingBuffer.h
@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
#include "IrInterruptGuard.h"
|
||||
template <typename T, unsigned int BufferSize>
|
||||
class RingBuffer {
|
||||
public:
|
||||
@ -15,39 +15,35 @@ public:
|
||||
|
||||
bool push(T element) {
|
||||
bool pushed = false;
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
if (!isFull()) {
|
||||
data[end] = element;
|
||||
end = (end + 1) % BufferSize;
|
||||
pushed = true;
|
||||
}
|
||||
interrupts();
|
||||
return pushed;
|
||||
}
|
||||
|
||||
T* pop() {
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
T* value = nullptr;
|
||||
if (!isEmpty()) {
|
||||
value = &data[start];
|
||||
start = (start + 1) % BufferSize;
|
||||
}
|
||||
interrupts();
|
||||
return value;
|
||||
}
|
||||
|
||||
// B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению.
|
||||
// (T* pop() отдаёт указатель во внутренний слот; его внутренний interrupts() снимает внешнюю
|
||||
// защиту вызывающего ДО чтения *ptr → торн-рид, если кольцо переполнится в этом окне.)
|
||||
// (T* pop() отдаёт указатель во внутренний слот; после выхода слот снова может быть перезаписан.)
|
||||
bool pop(T &out) {
|
||||
bool popped = false;
|
||||
noInterrupts();
|
||||
IrInterruptGuard guard;
|
||||
if (!isEmpty()) {
|
||||
out = data[start];
|
||||
start = (start + 1) % BufferSize;
|
||||
popped = true;
|
||||
}
|
||||
interrupts();
|
||||
return popped;
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ static constexpr uint16_t kIrDeviceAddr = 0;
|
||||
static constexpr uint8_t kCmdVersion = 0x5E;
|
||||
static constexpr uint32_t kSerialBaud = 115200;
|
||||
static constexpr uint32_t kSendPeriodMs = 500;
|
||||
static constexpr uint8_t kMaxPayload = bytePerPack;
|
||||
static constexpr uint8_t kMaxPayload = irproto::kMaxDataPayloadBytes;
|
||||
static constexpr uint8_t kMaxParamBytes = kMaxPayload - 1;
|
||||
|
||||
static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr);
|
||||
@ -30,15 +30,23 @@ static HardwareTimer irTimer(TIM17);
|
||||
namespace {
|
||||
constexpr size_t kIrDmaStreams = 1;
|
||||
constexpr uint16_t kIrDmaTxWordCount = 4096U;
|
||||
constexpr size_t kIrDmaTxMaxGateRuns = 1024U;
|
||||
// This example accepts the full uint8_t carrier-multiply configuration range.
|
||||
constexpr uint16_t kIrDmaMaxCarrierMultiply = UINT8_MAX;
|
||||
constexpr size_t kIrDmaTxMaxGateRuns =
|
||||
irproto::maxPhysicalGateRunCapacity(kIrDmaMaxCarrierMultiply);
|
||||
static uint32_t s_irDmaWords[kIrDmaTxWordCount];
|
||||
static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns];
|
||||
} // namespace
|
||||
|
||||
static IrDmaTxStm32<kIrDmaStreams> dmaBackend;
|
||||
static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); }
|
||||
static bool txStart(void * /*ctx*/, IR_Encoder *e, const uint8_t *packet, uint8_t len) {
|
||||
return dmaBackend.start(e, packet, len);
|
||||
static IR_SendStatus txStart(void * /*ctx*/,
|
||||
IR_Encoder *e,
|
||||
const uint8_t *packet,
|
||||
uint8_t len,
|
||||
const IR_TxPlan& plan,
|
||||
uint32_t operationId) {
|
||||
return dmaBackend.startTracked(e, packet, len, plan, operationId);
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -51,7 +59,11 @@ static bool s_sendLongerFrame = false;
|
||||
// 24 байта total: msg(1)+addr(2)+addr(2)+data(17)+crc(2), где data=0x5E + 16 ASCII.
|
||||
static const char kPayload16[] = "Car_v4.3.9_[12MH";
|
||||
// 25 байт total: как выше, но data=0x5E + 17 ASCII.
|
||||
static const char kPayload17[] = "Car_v4.3.9_[12MHz]_G491";
|
||||
static const char kPayload17[] = "Car_v4.3.9_[12MHz";
|
||||
static_assert(sizeof(kPayload16) - 1U == 16U, "24-byte frame fixture changed");
|
||||
static_assert(sizeof(kPayload17) - 1U == 17U, "25-byte frame fixture changed");
|
||||
static_assert(kMaxParamBytes == irproto::kMaxDataPayloadBytes - 1U,
|
||||
"longData command parameter capacity must follow the DATA wire contract");
|
||||
|
||||
static void rebuildIrPayload() {
|
||||
s_irPayload[0] = kCmdVersion;
|
||||
@ -110,7 +122,7 @@ void setup() {
|
||||
Serial.println(F("[IR_DMA] init FAILED"));
|
||||
return;
|
||||
}
|
||||
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr);
|
||||
IR_Encoder::setExternalTxBackendV2(txStart, txBusy, nullptr);
|
||||
#elif LONGDATA_LEGACY_ISR
|
||||
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
|
||||
#else
|
||||
|
||||
134
tests/arduino_stubs/Arduino.h
Normal file
134
tests/arduino_stubs/Arduino.h
Normal file
@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
struct GPIO_TypeDef
|
||||
{
|
||||
uint32_t BSRR = 0;
|
||||
uint32_t IDR = 0;
|
||||
};
|
||||
|
||||
class __FlashStringHelper;
|
||||
#define F(value) (reinterpret_cast<const __FlashStringHelper *>(value))
|
||||
|
||||
class Print
|
||||
{
|
||||
public:
|
||||
size_t print(const __FlashStringHelper *value)
|
||||
{
|
||||
return append(reinterpret_cast<const char *>(value));
|
||||
}
|
||||
|
||||
size_t print(const char *value) { return append(value); }
|
||||
|
||||
size_t print(char value)
|
||||
{
|
||||
buffer_.push_back(value);
|
||||
return 1U;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
size_t print(T value)
|
||||
{
|
||||
return append(std::to_string(value).c_str());
|
||||
}
|
||||
|
||||
size_t println()
|
||||
{
|
||||
buffer_.push_back('\n');
|
||||
return 1U;
|
||||
}
|
||||
|
||||
size_t write(uint8_t value)
|
||||
{
|
||||
buffer_.push_back(static_cast<char>(value));
|
||||
return 1U;
|
||||
}
|
||||
|
||||
const std::string &str() const { return buffer_; }
|
||||
void clear() { buffer_.clear(); }
|
||||
|
||||
private:
|
||||
size_t append(const char *value)
|
||||
{
|
||||
if (value == nullptr)
|
||||
return 0U;
|
||||
const size_t oldSize = buffer_.size();
|
||||
buffer_ += value;
|
||||
return buffer_.size() - oldSize;
|
||||
}
|
||||
|
||||
std::string buffer_;
|
||||
};
|
||||
|
||||
using IRQn_Type = int;
|
||||
enum TimerFormat_t : uint8_t { TICK_FORMAT = 0, MICROSEC_FORMAT, HERTZ_FORMAT };
|
||||
|
||||
constexpr uint8_t LOW = 0;
|
||||
constexpr uint8_t HIGH = 1;
|
||||
constexpr uint8_t INPUT = 0;
|
||||
constexpr uint8_t OUTPUT = 1;
|
||||
|
||||
class HardwareTimer
|
||||
{
|
||||
public:
|
||||
void pause() {}
|
||||
void resume() {}
|
||||
void setOverflow(uint32_t value, TimerFormat_t format = TICK_FORMAT)
|
||||
{
|
||||
if (format == HERTZ_FORMAT && value != 0U)
|
||||
{
|
||||
prescale_ = 1U;
|
||||
overflow_ = timerClockHz_ / value;
|
||||
if (overflow_ == 0U) overflow_ = 1U;
|
||||
}
|
||||
else
|
||||
{
|
||||
overflow_ = value == 0U ? 1U : value;
|
||||
}
|
||||
}
|
||||
uint32_t getOverflow(TimerFormat_t = TICK_FORMAT) { return overflow_; }
|
||||
uint32_t getPrescaleFactor() { return prescale_; }
|
||||
uint32_t getTimerClkFreq() { return timerClockHz_; }
|
||||
void attachInterrupt(uint8_t, void (*)()) {}
|
||||
|
||||
uint32_t timerClockHz_ = 12000000U;
|
||||
uint32_t prescale_ = 1U;
|
||||
uint32_t overflow_ = 1U;
|
||||
};
|
||||
|
||||
inline GPIO_TypeDef *digitalPinToPort(uint8_t)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline uint16_t digitalPinToBitMask(uint8_t)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
inline uint32_t arduino_stub_micros = 0U;
|
||||
|
||||
inline unsigned long millis()
|
||||
{
|
||||
return arduino_stub_micros / 1000U;
|
||||
}
|
||||
|
||||
inline unsigned long micros() { return arduino_stub_micros; }
|
||||
148
tests/test_packet_types.cpp
Normal file
148
tests/test_packet_types.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "PacketTypes.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename Packet>
|
||||
class ExposedPacket : public Packet
|
||||
{
|
||||
public:
|
||||
bool attach(IR_FOX::PackInfo *info, uint16_t id = 0, bool requireTypedSize = true)
|
||||
{
|
||||
return this->set(info, id, requireTypedSize);
|
||||
}
|
||||
};
|
||||
|
||||
IR_FOX::PackInfo frame(uint8_t *buffer, uint8_t msgType, uint8_t size)
|
||||
{
|
||||
buffer[0] = uint8_t((msgType << 5) | (size & IR_MASK_MSG_INFO));
|
||||
IR_FOX::PackInfo info;
|
||||
info.buffer = buffer;
|
||||
info.packSize = size;
|
||||
return info;
|
||||
}
|
||||
|
||||
template <typename Packet>
|
||||
void checkTypedBoundary(uint8_t msgType, uint8_t minimum)
|
||||
{
|
||||
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||
ExposedPacket<Packet> packet;
|
||||
|
||||
IR_FOX::PackInfo shortInfo = frame(buffer, msgType, uint8_t(minimum - 1U));
|
||||
assert(!packet.attach(&shortInfo));
|
||||
assert(!packet.available());
|
||||
assert(!packet.availableRaw());
|
||||
|
||||
IR_FOX::PackInfo minimumInfo = frame(buffer, msgType, minimum);
|
||||
assert(packet.attach(&minimumInfo));
|
||||
assert(packet.available());
|
||||
}
|
||||
|
||||
void testMinimumSizes()
|
||||
{
|
||||
struct Case
|
||||
{
|
||||
uint8_t msgType;
|
||||
uint8_t minimum;
|
||||
};
|
||||
|
||||
const Case cases[] = {
|
||||
{IR_MSG_DATA_ACCEPT, 7},
|
||||
{IR_MSG_DATA_NOACCEPT, 7},
|
||||
{IR_MSG_BACK, 5},
|
||||
{IR_MSG_BACK_TO, 7},
|
||||
{IR_MSG_REQUEST, 7},
|
||||
{IR_MSG_ACCEPT, 6},
|
||||
};
|
||||
|
||||
for (const Case &item : cases)
|
||||
{
|
||||
assert(PacketTypes::minimumPacketSize(item.msgType) == item.minimum);
|
||||
assert(!PacketTypes::isTypedPacketSizeValid(item.msgType, uint8_t(item.minimum - 1U)));
|
||||
assert(PacketTypes::isTypedPacketSizeValid(item.msgType, item.minimum));
|
||||
assert(PacketTypes::isTypedPacketSizeValid(item.msgType, uint8_t(item.minimum + 1U)));
|
||||
}
|
||||
|
||||
assert(PacketTypes::minimumPacketSize(3) == 0);
|
||||
assert(PacketTypes::minimumPacketSize(5) == 0);
|
||||
assert(!PacketTypes::isTypedPacketSizeValid(3, 31));
|
||||
assert(!PacketTypes::isTypedPacketSizeValid(5, 31));
|
||||
|
||||
checkTypedBoundary<PacketTypes::Data>(IR_MSG_DATA_ACCEPT, 7);
|
||||
checkTypedBoundary<PacketTypes::Data>(IR_MSG_DATA_NOACCEPT, 7);
|
||||
checkTypedBoundary<PacketTypes::DataBack>(IR_MSG_BACK, 5);
|
||||
checkTypedBoundary<PacketTypes::DataBack>(IR_MSG_BACK_TO, 7);
|
||||
checkTypedBoundary<PacketTypes::Request>(IR_MSG_REQUEST, 7);
|
||||
checkTypedBoundary<PacketTypes::Accept>(IR_MSG_ACCEPT, 6);
|
||||
}
|
||||
|
||||
void testPayloadAccessSaturates()
|
||||
{
|
||||
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||
ExposedPacket<PacketTypes::Data> packet;
|
||||
|
||||
for (uint8_t size = 0; size < 7; ++size)
|
||||
{
|
||||
IR_FOX::PackInfo tooShort = frame(buffer, IR_MSG_DATA_ACCEPT, size);
|
||||
assert(!packet.attach(&tooShort));
|
||||
assert(packet.getDataSize() == 0);
|
||||
assert(packet.getDataPrt() == nullptr);
|
||||
}
|
||||
|
||||
IR_FOX::PackInfo emptyPayload = frame(buffer, IR_MSG_DATA_ACCEPT, 7);
|
||||
assert(packet.attach(&emptyPayload));
|
||||
assert(packet.getDataSize() == 0);
|
||||
assert(packet.getDataPrt() == buffer + 5);
|
||||
|
||||
IR_FOX::PackInfo oneBytePayload = frame(buffer, IR_MSG_DATA_ACCEPT, 8);
|
||||
assert(packet.attach(&oneBytePayload));
|
||||
assert(packet.getDataSize() == 1);
|
||||
assert(packet.getDataPrt() == buffer + 5);
|
||||
|
||||
IR_FOX::PackInfo nullBuffer;
|
||||
nullBuffer.packSize = 31;
|
||||
assert(!packet.attach(&nullBuffer));
|
||||
assert(packet.getDataSize() == 0);
|
||||
assert(packet.getDataPrt() == nullptr);
|
||||
}
|
||||
|
||||
void testBackPayloadOffsets()
|
||||
{
|
||||
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||
ExposedPacket<PacketTypes::DataBack> packet;
|
||||
|
||||
IR_FOX::PackInfo addressed = frame(buffer, IR_MSG_BACK_TO, 7);
|
||||
assert(packet.attach(&addressed));
|
||||
assert(packet.getDataSize() == 0);
|
||||
assert(packet.getDataPrt() == buffer + 5);
|
||||
|
||||
IR_FOX::PackInfo broadcast = frame(buffer, IR_MSG_BACK, 5);
|
||||
assert(packet.attach(&broadcast));
|
||||
assert(packet.getDataSize() == 0);
|
||||
assert(packet.getDataPrt() == buffer + 3);
|
||||
}
|
||||
|
||||
void testRawContractIsIndependent()
|
||||
{
|
||||
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||
ExposedPacket<PacketTypes::BasePack> raw;
|
||||
IR_FOX::PackInfo info = frame(buffer, IR_MSG_DATA_ACCEPT, 3);
|
||||
|
||||
assert(raw.attach(&info, 0, false));
|
||||
assert(raw.availableRaw());
|
||||
assert(raw.getDataRawSize() == 3);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
testMinimumSizes();
|
||||
testPayloadAccessSaturates();
|
||||
testBackPayloadOffsets();
|
||||
testRawContractIsIndependent();
|
||||
std::cout << "packet type boundary tests: OK\n";
|
||||
return 0;
|
||||
}
|
||||
69
tests/test_protocol_contract.cpp
Normal file
69
tests/test_protocol_contract.cpp
Normal file
@ -0,0 +1,69 @@
|
||||
#include "IR_DecoderRaw.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
// Reproduces the removed 2025 expression exactly, but with integer arithmetic:
|
||||
// 2.7735 == 27735 / 10000. It is a provenance golden, not a new PHY rule.
|
||||
constexpr uint32_t removedLegacyResponseExpressionMs()
|
||||
{
|
||||
const uint64_t scaledUs =
|
||||
static_cast<uint64_t>(irproto::kNominalRxInterEdgeTimeoutUs) * 27735U / 10000U;
|
||||
return static_cast<uint16_t>(scaledUs) / 1000U;
|
||||
}
|
||||
|
||||
static_assert(irproto::kWireFrameLengthBits == 5U, "wire length field changed");
|
||||
static_assert(irproto::kWireFrameLengthMask == 31U, "wire length mask changed");
|
||||
static_assert(irproto::kMaxWireFrameBytes == 31U, "wire frame limit changed");
|
||||
static_assert(irproto::kDataFrameOverheadBytes == 7U, "DATA overhead changed");
|
||||
static_assert(irproto::kBackFrameOverheadBytes == 5U, "BACK overhead changed");
|
||||
static_assert(irproto::kBackToFrameOverheadBytes == 7U, "BACK_TO overhead changed");
|
||||
static_assert(irproto::kMaxDataPayloadBytes == 24U, "DATA payload limit changed");
|
||||
static_assert(irproto::kMaxBackPayloadBytes == 26U, "BACK payload limit changed");
|
||||
static_assert(irproto::kMaxBackToPayloadBytes == 24U, "BACK_TO payload limit changed");
|
||||
static_assert(irproto::kMaxLogicalGateRuns == 688U, "logical max-frame run bound changed");
|
||||
static_assert(irproto::maxPhysicalGateRunCapacity(UINT8_MAX) == 738U,
|
||||
"uint8 carrier-multiply storage bound changed");
|
||||
|
||||
static_assert(IR_MASK_MSG_TYPE == irproto::kMessageTypeMask, "legacy type mask diverged");
|
||||
static_assert(IR_MASK_MSG_INFO == irproto::kWireFrameLengthMask, "legacy length mask diverged");
|
||||
static_assert(bytePerPack == irproto::kMaxWireFrameBytes,
|
||||
"legacy bytePerPack value must remain source-compatible");
|
||||
static_assert(dataByteSizeMax == irproto::kMaxWireFrameBytes,
|
||||
"legacy storage alias must follow the wire limit");
|
||||
|
||||
static_assert(irproto::kRxInterEdgeTimeoutBitWindows == 12U,
|
||||
"8 data + 3 sync + 1 guard geometry changed");
|
||||
static_assert(irproto::kNominalRxInterEdgeTimeoutUs == 15144U,
|
||||
"nominal inter-edge timeout changed");
|
||||
static_assert(irproto::kNominalRxSilenceTimeoutUs == 30288U,
|
||||
"nominal RX silence timeout changed");
|
||||
static_assert(irproto::microsToMillisCeil(irproto::kNominalRxSilenceTimeoutUs) == 31U,
|
||||
"RX silence ceil-ms conversion changed");
|
||||
static_assert(IR_ResponseDelay == 42U, "deployed response turn-around changed");
|
||||
static_assert(removedLegacyResponseExpressionMs() == IR_ResponseDelay,
|
||||
"named empirical response delay no longer matches its legacy provenance");
|
||||
|
||||
void testAdaptiveTimingGeometry()
|
||||
{
|
||||
assert(irproto::rxInterEdgeTimeoutUs(700U) == 12000U);
|
||||
assert(irproto::rxSilenceTimeoutUs(700U) == 24000U);
|
||||
assert(irproto::rxInterEdgeTimeoutUs(1000U) == 15600U);
|
||||
assert(irproto::rxSilenceTimeoutUs(1000U) == 31200U);
|
||||
assert(irproto::microsToMillisCeil(0U) == 0U);
|
||||
assert(irproto::microsToMillisCeil(1U) == 1U);
|
||||
assert(irproto::microsToMillisCeil(1000U) == 1U);
|
||||
assert(irproto::microsToMillisCeil(1001U) == 2U);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
testAdaptiveTimingGeometry();
|
||||
std::cout << "IR protocol geometry contract tests: OK\n";
|
||||
return 0;
|
||||
}
|
||||
79
tests/test_rx_reason_contract.cpp
Normal file
79
tests/test_rx_reason_contract.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
#include "IR_config.h"
|
||||
#include "RingBuffer.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <type_traits>
|
||||
|
||||
// Test-only visibility: exercise the private reason enum and logging bound without
|
||||
// widening the production API. Dependencies are included first so this macro
|
||||
// cannot rewrite access specifiers in the standard library.
|
||||
#define private public
|
||||
#include "IR_DecoderRaw.h"
|
||||
#undef private
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char kZeroStats[] =
|
||||
"RXSTAT,MUTEB=0,MUTEE=0,QRAW=0,QFLT=0,HOLD=0,GLITCH=0,TIME=0,"
|
||||
"PREAMB=0,SYNC=0,BUF=0,TIMEOUT=0,CRC=0,OK=0\n";
|
||||
|
||||
constexpr const char kOneEachStats[] =
|
||||
"RXSTAT,MUTEB=1,MUTEE=1,QRAW=1,QFLT=1,HOLD=1,GLITCH=1,TIME=1,"
|
||||
"PREAMB=1,SYNC=1,BUF=1,TIMEOUT=1,CRC=1,OK=1\n";
|
||||
|
||||
static_assert(IR_DecoderRaw::rxReasonCounterCount() > 0U,
|
||||
"RX reason counter storage must not be empty");
|
||||
static_assert(IR_DecoderRaw::rxReasonCounterCount() ==
|
||||
static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Count),
|
||||
"public RX reason count must follow the enum sentinel");
|
||||
static_assert(static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Count) ==
|
||||
static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Ok) + 1U,
|
||||
"RX reason Count must remain one past the final reason");
|
||||
static_assert(std::extent<decltype(IR_DecoderRaw::rxReasonCnt)>::value ==
|
||||
IR_DecoderRaw::rxReasonCounterCount(),
|
||||
"RX reason counter array must follow the enum-derived count");
|
||||
|
||||
void testStatsWireFormatAndClearCoverage()
|
||||
{
|
||||
IR_DecoderRaw decoder(0U, 0U);
|
||||
Print out;
|
||||
|
||||
decoder.printRxReasonStats(out);
|
||||
assert(out.str() == kZeroStats);
|
||||
|
||||
const uint8_t first = static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::MuteBegin);
|
||||
const uint8_t count = IR_DecoderRaw::rxReasonCounterCount();
|
||||
for (uint8_t i = first; i < count; ++i)
|
||||
decoder.rxBriefLog(static_cast<IR_DecoderRaw::RxBriefReason>(i));
|
||||
|
||||
// The sentinel is a bound, not a loggable reason.
|
||||
decoder.rxBriefLog(IR_DecoderRaw::RxBriefReason::Count);
|
||||
|
||||
const uint16_t *const counters = decoder.rxReasonCounters();
|
||||
assert(counters[0] == 0U);
|
||||
for (uint8_t i = first; i < count; ++i)
|
||||
assert(counters[i] == 1U);
|
||||
|
||||
out.clear();
|
||||
decoder.printRxReasonStats(out);
|
||||
assert(out.str() == kOneEachStats);
|
||||
|
||||
decoder.rxReasonCountersClear();
|
||||
for (uint8_t i = 0U; i < count; ++i)
|
||||
assert(counters[i] == 0U);
|
||||
|
||||
out.clear();
|
||||
decoder.printRxReasonStats(out);
|
||||
assert(out.str() == kZeroStats);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
testStatsWireFormatAndClearCoverage();
|
||||
std::cout << "RX reason counter/tag contract tests: OK\n";
|
||||
return 0;
|
||||
}
|
||||
328
tests/test_tx_contract.cpp
Normal file
328
tests/test_tx_contract.cpp
Normal file
@ -0,0 +1,328 @@
|
||||
#include "IR_Encoder.h"
|
||||
#include "IR_DecoderRaw.h"
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
// Link-only seams for planner/lifecycle host tests. The real implementations
|
||||
// are irrelevant here; no decoder or legacy sendByte helper is exercised.
|
||||
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
|
||||
{
|
||||
constexpr size_t kRunCapacity = 2048U;
|
||||
constexpr size_t kFullMultiplyRunCapacity =
|
||||
irproto::maxPhysicalGateRunCapacity(UINT16_MAX);
|
||||
|
||||
static_assert(irproto::kMaxLogicalTransmissionTicks == 25822U,
|
||||
"golden maximum PHY duration changed");
|
||||
static_assert(irproto::maxPhysicalTransmissionTicks(2U) == 25822U,
|
||||
"nominal physical tick conversion changed");
|
||||
static_assert(irproto::maxPhysicalGateRunCapacity(UINT8_MAX) <= 1024U,
|
||||
"uint8_t carrier-multiply domain no longer fits the legacy Car allocation");
|
||||
|
||||
uint32_t sumTicks(const IrTxGateRun *runs, uint32_t count)
|
||||
{
|
||||
uint32_t total = 0U;
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
total += runs[i].lenTicks;
|
||||
return total;
|
||||
}
|
||||
|
||||
void fillPattern(uint8_t *frame, uint8_t len, uint8_t pattern)
|
||||
{
|
||||
for (uint8_t i = 0; i < len; ++i)
|
||||
{
|
||||
switch (pattern)
|
||||
{
|
||||
case 0: frame[i] = 0x00U; break;
|
||||
case 1: frame[i] = 0xFFU; break;
|
||||
case 2: frame[i] = (i & 1U) ? 0x55U : 0xAAU; break;
|
||||
default: frame[i] = static_cast<uint8_t>(i * 73U + 19U); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testPlannerMatchesBuiltStream()
|
||||
{
|
||||
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||
std::array<IrTxGateRun, kRunCapacity> runs{};
|
||||
const uint16_t multiplies[] = {2U, 3U, 6U};
|
||||
|
||||
for (uint16_t multiply : multiplies)
|
||||
{
|
||||
for (uint8_t len = 1U; len <= irproto::kMaxWireFrameBytes; ++len)
|
||||
{
|
||||
uint32_t durationForLength = 0U;
|
||||
uint32_t ticksForLength = 0U;
|
||||
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
|
||||
{
|
||||
fillPattern(frame.data(), len, pattern);
|
||||
const IR_TxPlan planned =
|
||||
IR_Encoder::planPhysicalTransmission(frame.data(), len, multiply);
|
||||
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||
frame.data(), len, runs.data(), runs.size(), multiply);
|
||||
assert(planned.valid());
|
||||
assert(built.valid());
|
||||
assert(planned.physicalTicks == built.physicalTicks);
|
||||
assert(planned.gateRunCount == built.gateRunCount);
|
||||
assert(planned.airtimeUs == built.airtimeUs);
|
||||
assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks);
|
||||
if (pattern == 0U)
|
||||
{
|
||||
durationForLength = planned.airtimeUs;
|
||||
ticksForLength = planned.physicalTicks;
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(planned.airtimeUs == durationForLength);
|
||||
assert(planned.physicalTicks == ticksForLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testGoldenNominalTimings()
|
||||
{
|
||||
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||
struct Golden { uint8_t bytes; uint32_t logicalTicks; uint32_t usCeil; };
|
||||
const Golden golden[] = {
|
||||
{6U, 5472U, 72000U},
|
||||
{10U, 8728U, 114843U},
|
||||
{31U, 25822U, 339764U},
|
||||
};
|
||||
for (const Golden& item : golden)
|
||||
{
|
||||
const IR_TxPlan plan =
|
||||
IR_Encoder::planPhysicalTransmission(frame.data(), item.bytes, 2U);
|
||||
assert(plan.valid());
|
||||
assert(plan.physicalTicks == item.logicalTicks);
|
||||
assert(plan.airtimeUs == item.usCeil);
|
||||
assert(plan.airtimeMsCeil() == (item.usCeil + 999U) / 1000U);
|
||||
}
|
||||
}
|
||||
|
||||
void testCapacityAndClockContracts()
|
||||
{
|
||||
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||
std::array<IrTxGateRun, kRunCapacity> logicalRuns{};
|
||||
fillPattern(frame.data(), frame.size(), 2U);
|
||||
assert(IR_Encoder::buildGateRuns(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||
logicalRuns.data(), logicalRuns.size()) != 0U);
|
||||
|
||||
std::array<uint8_t, irproto::kMaxWireFrameBytes + 1U> oversizedFrame{};
|
||||
assert(IR_Encoder::buildGateRuns(
|
||||
oversizedFrame.data(), static_cast<uint8_t>(oversizedFrame.size()),
|
||||
logicalRuns.data(), logicalRuns.size()) == 0U);
|
||||
|
||||
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()), 6U);
|
||||
assert(planned.valid());
|
||||
assert(planned.gateRunCount <= irproto::kIsrTxMaxGateRuns);
|
||||
|
||||
IrTxGateRun oneRun{};
|
||||
const IR_TxPlan tooSmall = IR_Encoder::buildPhysicalTransmission(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()), &oneRun, 1U, 6U);
|
||||
assert(!tooSmall.valid());
|
||||
assert(tooSmall.status == IR_SendStatus::BuildGateRunsFailed);
|
||||
assert(tooSmall.gateRunCount == planned.gateRunCount);
|
||||
assert(tooSmall.physicalTicks == planned.physicalTicks);
|
||||
|
||||
std::array<uint8_t, 10U> tenBytes{};
|
||||
IR_TxPlan configured = IR_Encoder::planPhysicalTransmission(
|
||||
tenBytes.data(), static_cast<uint8_t>(tenBytes.size()), 6U);
|
||||
assert(configured.airtimeUs == 114843U);
|
||||
assert(IR_Encoder::applyTickClock(configured, 12000000U, 52U));
|
||||
assert(configured.clockBasis == IR_TxClockBasis::ConfiguredTimer);
|
||||
assert(configured.airtimeUs == 113464U);
|
||||
|
||||
assert(!IR_Encoder::planPhysicalTransmission(nullptr, 1U, 2U).valid());
|
||||
assert(!IR_Encoder::planPhysicalTransmission(frame.data(), 0U, 2U).valid());
|
||||
assert(!IR_Encoder::planPhysicalTransmission(
|
||||
frame.data(), static_cast<uint8_t>(irproto::kMaxWireFrameBytes + 1U), 2U).valid());
|
||||
}
|
||||
|
||||
void testDerivedFixedStorageCapacity()
|
||||
{
|
||||
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||
std::array<IrTxGateRun, kFullMultiplyRunCapacity> runs{};
|
||||
const uint16_t multiplies[] = {2U, 3U, 6U, UINT8_MAX, UINT16_MAX};
|
||||
|
||||
for (uint16_t multiply : multiplies)
|
||||
{
|
||||
const size_t capacity = irproto::maxPhysicalGateRunCapacity(multiply);
|
||||
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
|
||||
{
|
||||
fillPattern(frame.data(), static_cast<uint8_t>(frame.size()), pattern);
|
||||
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()), multiply);
|
||||
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||
runs.data(), capacity, multiply);
|
||||
assert(planned.valid());
|
||||
assert(built.valid());
|
||||
assert(built.gateRunCount <= capacity);
|
||||
assert(built.gateRunCount == planned.gateRunCount);
|
||||
assert(built.physicalTicks == planned.physicalTicks);
|
||||
assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testInPlacePhysicalScaling()
|
||||
{
|
||||
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||
std::array<IrTxGateRun, kRunCapacity> logical{};
|
||||
std::array<IrTxGateRun, kRunCapacity> expected{};
|
||||
fillPattern(frame.data(), static_cast<uint8_t>(frame.size()), 3U);
|
||||
|
||||
const size_t logicalCount = IR_Encoder::buildGateRuns(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||
logical.data(), logical.size());
|
||||
assert(logicalCount != 0U);
|
||||
|
||||
for (uint16_t multiply : {2U, 3U, 6U})
|
||||
{
|
||||
auto scaled = logical;
|
||||
size_t scaledCount = logicalCount;
|
||||
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||
expected.data(), expected.size(), multiply);
|
||||
assert(built.valid());
|
||||
assert(IR_Encoder::scaleGateRunsToPhysical(
|
||||
scaled.data(), &scaledCount, scaled.size(), multiply));
|
||||
assert(scaledCount == built.gateRunCount);
|
||||
for (size_t i = 0; i < scaledCount; ++i)
|
||||
{
|
||||
assert(scaled[i].gate == expected[i].gate);
|
||||
assert(scaled[i].lenTicks == expected[i].lenTicks);
|
||||
}
|
||||
}
|
||||
|
||||
// Expansion beyond uint16_t is also in-place and preserves chunk order.
|
||||
std::array<IrTxGateRun, 8U> longRun{};
|
||||
longRun[0] = {65535U, true};
|
||||
size_t longCount = 1U;
|
||||
assert(IR_Encoder::scaleGateRunsToPhysical(
|
||||
longRun.data(), &longCount, longRun.size(), 7U));
|
||||
assert(longCount == 4U);
|
||||
assert(longRun[0].lenTicks == 65535U);
|
||||
assert(longRun[1].lenTicks == 65535U);
|
||||
assert(longRun[2].lenTicks == 65535U);
|
||||
assert(longRun[3].lenTicks == 32768U);
|
||||
assert(sumTicks(longRun.data(), static_cast<uint32_t>(longCount)) == 229373U);
|
||||
|
||||
std::array<IrTxGateRun, 2U> tooSmall{{{65535U, true}, {1U, false}}};
|
||||
size_t tooSmallCount = 1U;
|
||||
assert(!IR_Encoder::scaleGateRunsToPhysical(
|
||||
tooSmall.data(), &tooSmallCount, tooSmall.size(), 6U));
|
||||
assert(tooSmallCount == 1U);
|
||||
}
|
||||
|
||||
struct FakeBackend
|
||||
{
|
||||
IR_SendStatus startStatus = IR_SendStatus::Success;
|
||||
bool finishSynchronously = false;
|
||||
IR_Encoder *encoder = nullptr;
|
||||
uint32_t operationId = 0U;
|
||||
IR_TxPlan plan{};
|
||||
};
|
||||
|
||||
IR_SendStatus fakeStart(void *opaque,
|
||||
IR_Encoder *encoder,
|
||||
const uint8_t *,
|
||||
uint8_t,
|
||||
const IR_TxPlan& plan,
|
||||
uint32_t operationId)
|
||||
{
|
||||
auto& backend = *static_cast<FakeBackend *>(opaque);
|
||||
backend.encoder = encoder;
|
||||
backend.operationId = operationId;
|
||||
backend.plan = plan;
|
||||
if (backend.startStatus == IR_SendStatus::Success && backend.finishSynchronously)
|
||||
encoder->externalFinishSend(operationId, IR_SendStatus::Success);
|
||||
return backend.startStatus;
|
||||
}
|
||||
|
||||
void testTokenLifecycle()
|
||||
{
|
||||
FakeBackend backend;
|
||||
IR_Encoder::setExternalTxBackendV2(fakeStart, nullptr, &backend);
|
||||
IR_Encoder encoder(1U, 42U, nullptr, false);
|
||||
uint8_t payload = 0x5EU;
|
||||
|
||||
arduino_stub_micros = 100U;
|
||||
const IR_SendResult first = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||
assert(first.success);
|
||||
assert(first.operationId != 0U);
|
||||
assert(first.plannedAirtimeUs == backend.plan.airtimeUs);
|
||||
assert(encoder.isBusy());
|
||||
IR_TxSnapshot snapshot = encoder.txSnapshot();
|
||||
assert(snapshot.operationId == first.operationId);
|
||||
assert(snapshot.state == IR_TxState::Transmitting);
|
||||
|
||||
encoder.externalFinishSend(first.operationId + 1U, IR_SendStatus::Success);
|
||||
assert(encoder.isBusy());
|
||||
arduino_stub_micros = 200U;
|
||||
encoder.externalFinishSend(first.operationId, IR_SendStatus::DmaTransferError);
|
||||
snapshot = encoder.txSnapshot();
|
||||
assert(!encoder.isBusy());
|
||||
assert(snapshot.state == IR_TxState::Failed);
|
||||
assert(snapshot.status == IR_SendStatus::DmaTransferError);
|
||||
assert(snapshot.terminalAtUs == 200U);
|
||||
encoder.externalFinishSend(first.operationId, IR_SendStatus::Success);
|
||||
assert(encoder.txSnapshot().status == IR_SendStatus::DmaTransferError);
|
||||
|
||||
arduino_stub_micros = 300U;
|
||||
const IR_SendResult second = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||
assert(second.success && second.operationId != first.operationId);
|
||||
encoder.externalFinishSend(first.operationId, IR_SendStatus::Success);
|
||||
assert(encoder.isBusy());
|
||||
encoder.externalFinishSend(second.operationId, IR_SendStatus::Success);
|
||||
assert(encoder.isOperationComplete(second.operationId));
|
||||
|
||||
backend.startStatus = IR_SendStatus::DmaStartFailed;
|
||||
const IR_SendResult rejectedAfterOwnership = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||
assert(!rejectedAfterOwnership.success);
|
||||
assert(rejectedAfterOwnership.operationId != 0U);
|
||||
snapshot = encoder.txSnapshot();
|
||||
assert(snapshot.state == IR_TxState::Failed);
|
||||
assert(snapshot.status == IR_SendStatus::DmaStartFailed);
|
||||
|
||||
backend.startStatus = IR_SendStatus::Success;
|
||||
const IR_SendResult active = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||
const IR_SendResult busy = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||
assert(active.success);
|
||||
assert(!busy.success && busy.status == IR_SendStatus::EncoderBusy);
|
||||
assert(busy.operationId == 0U);
|
||||
encoder.externalFinishSend(active.operationId, IR_SendStatus::Success);
|
||||
|
||||
backend.finishSynchronously = true;
|
||||
const IR_SendResult synchronous = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||
assert(synchronous.success);
|
||||
assert(encoder.isOperationComplete(synchronous.operationId));
|
||||
assert(!encoder.isBusy());
|
||||
|
||||
IR_Encoder::setExternalTxBackendV2(nullptr, nullptr, nullptr);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
testPlannerMatchesBuiltStream();
|
||||
testGoldenNominalTimings();
|
||||
testCapacityAndClockContracts();
|
||||
testDerivedFixedStorageCapacity();
|
||||
testInPlacePhysicalScaling();
|
||||
testTokenLifecycle();
|
||||
std::cout << "IR TX contract tests: OK\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user