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:
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