mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-21 12:29:35 +00:00
329 lines
13 KiB
C++
329 lines
13 KiB
C++
#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;
|
|
}
|