#include "IR_Encoder.h" #include "IR_DecoderRaw.h" #include #include #include #include // 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(i * 73U + 19U); break; } } } void testPlannerMatchesBuiltStream() { std::array frame{}; std::array 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 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 frame{}; std::array logicalRuns{}; fillPattern(frame.data(), frame.size(), 2U); assert(IR_Encoder::buildGateRuns( frame.data(), static_cast(frame.size()), logicalRuns.data(), logicalRuns.size()) != 0U); std::array oversizedFrame{}; assert(IR_Encoder::buildGateRuns( oversizedFrame.data(), static_cast(oversizedFrame.size()), logicalRuns.data(), logicalRuns.size()) == 0U); const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission( frame.data(), static_cast(frame.size()), 6U); assert(planned.valid()); assert(planned.gateRunCount <= irproto::kIsrTxMaxGateRuns); IrTxGateRun oneRun{}; const IR_TxPlan tooSmall = IR_Encoder::buildPhysicalTransmission( frame.data(), static_cast(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 tenBytes{}; IR_TxPlan configured = IR_Encoder::planPhysicalTransmission( tenBytes.data(), static_cast(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(irproto::kMaxWireFrameBytes + 1U), 2U).valid()); } void testDerivedFixedStorageCapacity() { std::array frame{}; std::array 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(frame.size()), pattern); const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission( frame.data(), static_cast(frame.size()), multiply); const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission( frame.data(), static_cast(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 frame{}; std::array logical{}; std::array expected{}; fillPattern(frame.data(), static_cast(frame.size()), 3U); const size_t logicalCount = IR_Encoder::buildGateRuns( frame.data(), static_cast(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(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 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(longCount)) == 229373U); std::array 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(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; }