1 Commits

Author SHA1 Message Date
00e101990f Make IR timing and RX terminal state explicit 2026-09-04 19:22:03 +03:00
23 changed files with 1150 additions and 1753 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
.vscode/* .vscode/*
bin/* bin/*
tests/*.exe
!.vscode/launch.json !.vscode/launch.json
log/* log/*
/.vscode /.vscode
@ -11,5 +12,3 @@ Analyzer/raw/dll/*.dylib
/Analyzer/raw/IR_Fox/.github /Analyzer/raw/IR_Fox/.github
**/.build **/.build
graphify-out/* graphify-out/*
**/.build-*/
/tests/*.exe

View File

@ -15,7 +15,6 @@ set(SOURCES
src/IrFoxAnalyzer.h src/IrFoxAnalyzer.h
src/IrFoxDecoder.cpp src/IrFoxDecoder.cpp
src/IrFoxDecoder.h src/IrFoxDecoder.h
src/IrFoxPacketClassifier.h
src/IrFoxAnalyzerResults.cpp src/IrFoxAnalyzerResults.cpp
src/IrFoxAnalyzerResults.h src/IrFoxAnalyzerResults.h
src/IrFoxAnalyzerSettings.cpp src/IrFoxAnalyzerSettings.cpp
@ -25,23 +24,3 @@ set(SOURCES
) )
add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES}) add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES})
if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
endif()
include(CTest)
if(BUILD_TESTING)
add_executable(IrFoxPacketClassifierTests tests/IrFoxPacketClassifierTests.cpp)
target_include_directories(IrFoxPacketClassifierTests PRIVATE src)
add_test(NAME IrFoxPacketClassifierTests COMMAND IrFoxPacketClassifierTests)
add_executable(IrFoxDecoderTests tests/IrFoxDecoderTests.cpp src/IrFoxDecoder.cpp)
target_include_directories(IrFoxDecoderTests PRIVATE src)
target_link_libraries(IrFoxDecoderTests PRIVATE Saleae::AnalyzerSDK)
add_custom_command(TARGET IrFoxDecoderTests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:Saleae::AnalyzerSDK>
$<TARGET_FILE_DIR:IrFoxDecoderTests>)
add_test(NAME IrFoxDecoderTests COMMAND IrFoxDecoderTests)
endif()

View File

@ -1,7 +1,6 @@
#include "IrFoxAnalyzer.h" #include "IrFoxAnalyzer.h"
#include "IrFoxAnalyzerSettings.h" #include "IrFoxAnalyzerSettings.h"
#include "IrFoxDecoder.h" #include "IrFoxDecoder.h"
#include "IrFoxPacketClassifier.h"
#include <AnalyzerChannelData.h> #include <AnalyzerChannelData.h>
#include <AnalyzerResults.h> #include <AnalyzerResults.h>
#include <algorithm> #include <algorithm>
@ -26,6 +25,7 @@ IrFoxAnalyzer::~IrFoxAnalyzer()
void IrFoxAnalyzer::SetupResults() void IrFoxAnalyzer::SetupResults()
{ {
m_packet_hex_by_frame.clear();
mResults.reset(new IrFoxAnalyzerResults(this, &mSettings)); mResults.reset(new IrFoxAnalyzerResults(this, &mSettings));
SetAnalyzerResults(mResults.get()); SetAnalyzerResults(mResults.get());
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel); mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
@ -46,91 +46,35 @@ static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_by
s += "..."; s += "...";
} }
static const char* packet_status_icon(IrFoxPacketOutcome outcome) const char* IrFoxAnalyzer::PacketHexForFrame(U64 frame_id)
{ {
switch (outcome) auto it = m_packet_hex_by_frame.find(frame_id);
{ if (it == m_packet_hex_by_frame.end())
case IrFoxPacketOutcome::Accepted:
return "";
case IrFoxPacketOutcome::IgnoredAddress:
return "📭";
case IrFoxPacketOutcome::RejectedCrc:
case IrFoxPacketOutcome::RejectedLength:
return "";
case IrFoxPacketOutcome::RawOnlyUnknownType:
case IrFoxPacketOutcome::RawOnlyTypedLength:
return "⚠️";
}
return "⚠️";
}
static const char* message_type_icon(uint8_t message_type)
{
switch (message_type)
{
case irfox::kMsgBack:
return "🔙";
case irfox::kMsgAccept:
return "🤝";
case irfox::kMsgRequest:
return "📣";
case irfox::kMsgBackTo:
return "🎯";
case irfox::kMsgDataNoAccept:
return "📦";
case irfox::kMsgDataAccept:
return "📨";
default:
return "⚠️";
}
}
static const char* terminal_abort_cause_text(IrFoxAbortCause cause)
{
switch (cause)
{
case IrFoxAbortCause::BadSync:
return "SYNC";
case IrFoxAbortCause::BadLength:
return "LEN";
case IrFoxAbortCause::Overflow:
return "OVF";
case IrFoxAbortCause::None:
default:
return ""; return "";
} m_hex_scratch = it->second;
return m_hex_scratch.c_str();
} }
static std::string packet_icon(const IrFoxPacketDecision& decision, IrFoxPacketIconMode mode) const char* IrFoxAnalyzer::BubbleTextForFrame(U64 frame_id) const
{ {
const char* status = packet_status_icon(decision.outcome); auto it = m_bubble_text_by_frame.find(frame_id);
// Icon-mode selection describes successfully accepted packets. Diagnostic if (it == m_bubble_text_by_frame.end())
// outcomes must remain visible even when the user selected type-only mode. return "";
if (decision.outcome != IrFoxPacketOutcome::Accepted) m_bubble_scratch = it->second;
return status; return m_bubble_scratch.c_str();
const char* type = message_type_icon(decision.message_type);
switch (mode)
{
case IrFoxPacketIconMode::Status:
return status;
case IrFoxPacketIconMode::MessageType:
return type;
case IrFoxPacketIconMode::StatusAndType:
default:
return std::string(status) + type;
}
} }
void IrFoxAnalyzer::WorkerThread() void IrFoxAnalyzer::WorkerThread()
{ {
mIr = GetAnalyzerChannelData(mSettings.mInputChannel); mIr = GetAnalyzerChannelData(mSettings.mInputChannel);
mResults->ClearCachedFrameText(); m_packet_hex_by_frame.clear();
m_bubble_text_by_frame.clear();
const U32 fs = GetSampleRate(); const U32 fs = GetSampleRate();
IrFoxDecoder decoder; IrFoxDecoder decoder;
decoder.reset(); decoder.reset();
/** Mirrors the firmware input filter. kMinFilteredPulseUs=0 means direct edge delivery. */ /** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */
const U64 min_seg_samples = const U64 min_seg_samples =
std::max<U64>(1ULL, static_cast<U64>((static_cast<double>(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast<double>(fs) + 0.5)); std::max<U64>(1ULL, static_cast<U64>((static_cast<double>(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast<double>(fs) + 0.5));
struct RawEdge struct RawEdge
@ -171,19 +115,8 @@ void IrFoxAnalyzer::WorkerThread()
U32 frames_since_commit = 0; U32 frames_since_commit = 0;
const U32 kCommitBatch = 256; const U32 kCommitBatch = 256;
const bool detailed_presentation = mSettings.mPresentation == IrFoxPresentation::Detailed;
std::vector<IrFoxEmitBit> pending_byte_frames;
pending_byte_frames.reserve(irfox::kDataByteSizeMax);
auto note_legacy_frame = [&]() { IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) {
if (++frames_since_commit >= kCommitBatch)
{
mResults->CommitResults();
frames_since_commit = 0;
}
};
auto add_event_frame = [&](const IrFoxEmitBit& e) {
Frame frame; Frame frame;
frame.mStartingSampleInclusive = static_cast<S64>(e.start_sample); frame.mStartingSampleInclusive = static_cast<S64>(e.start_sample);
frame.mEndingSampleInclusive = static_cast<S64>(e.end_sample); frame.mEndingSampleInclusive = static_cast<S64>(e.end_sample);
@ -191,255 +124,45 @@ void IrFoxAnalyzer::WorkerThread()
frame.mData1 = e.bit_value; frame.mData1 = e.bit_value;
frame.mData2 = e.bit_index | (U64(e.err_low) << 16) | (U64(e.err_high) << 24) | (U64(e.err_other) << 32); frame.mData2 = e.bit_index | (U64(e.err_low) << 16) | (U64(e.err_high) << 24) | (U64(e.err_other) << 32);
frame.mFlags = e.mflags; frame.mFlags = e.mflags;
mResults->AddFrame(frame); // В SDK только ERROR/WARNING меняют цвет бабла; sync выделяем янтарным (как warning), данные — обычные.
note_legacy_frame();
};
auto flush_pending_bytes = [&]() {
for (const IrFoxEmitBit& byte_event : pending_byte_frames)
add_event_frame(byte_event);
pending_byte_frames.clear();
};
IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) {
// Per-bit markers dominate Logic's render cost. They belong to Detailed
// only; Overview keeps a fast packet-level timeline.
if (e.frame_type == IRF_FT_DATA_BIT)
{
if (detailed_presentation)
{
// Markers, like legacy frames, must be published in time order.
// Publish the payload boundary when the first bit arrives rather
// than inserting it retroactively after packet completion.
if (e.bit_index == 0)
mResults->AddMarker(static_cast<U64>(e.start_sample), AnalyzerResults::Start,
mSettings.mInputChannel);
const U64 marker_sample = static_cast<U64>((e.start_sample + e.end_sample) / 2);
mResults->AddMarker(marker_sample, e.bit_value ? AnalyzerResults::One : AnalyzerResults::Zero,
mSettings.mInputChannel);
}
return;
}
// Sync cells have no independent user-facing value at overview scale. A
// fatal sync mismatch is still emitted as IRF_FT_ABORT below.
if (e.frame_type == IRF_FT_SYNC_BIT) if (e.frame_type == IRF_FT_SYNC_BIT)
return; frame.mFlags |= DISPLAY_AS_WARNING_FLAG;
if (e.frame_type == IRF_FT_DATA_BYTE)
{
if (detailed_presentation)
pending_byte_frames.push_back(e);
return;
}
if (!detailed_presentation)
return;
if (e.frame_type == IRF_FT_PREAMBLE)
{
// A timeout can leave a few complete bytes without a packet event.
// Flush them before the next PRE so legacy frames remain monotonic.
flush_pending_bytes();
add_event_frame(e);
return;
}
if (e.frame_type == IRF_FT_OVERFLOW || e.frame_type == IRF_FT_ABORT)
flush_pending_bytes();
add_event_frame(e);
};
// Terminal events are independent of per-bit rendering. Overview passes an
// empty on_bit callback for speed, but must still show an interrupted frame.
IrFoxOnTerminal on_terminal = [&](const IrFoxEmitTerminal& terminal) {
Frame frame;
if (detailed_presentation && !pending_byte_frames.empty())
{
// Preserve every completed byte except the final one. The terminal
// frame replaces that last byte so legacy frames never overlap.
for (size_t i = 0; i + 1 < pending_byte_frames.size(); ++i)
add_event_frame(pending_byte_frames[i]);
frame.mStartingSampleInclusive = static_cast<S64>(pending_byte_frames.back().start_sample);
}
else
{
frame.mStartingSampleInclusive = static_cast<S64>(detailed_presentation ?
terminal.detail_start_sample : terminal.start_sample);
}
frame.mEndingSampleInclusive = static_cast<S64>(terminal.end_sample);
if (frame.mStartingSampleInclusive > frame.mEndingSampleInclusive)
frame.mStartingSampleInclusive = frame.mEndingSampleInclusive;
frame.mType = terminal.reason == IrFoxTerminalReason::Timeout ? IRF_FT_TIMEOUT :
(terminal.cause == IrFoxAbortCause::Overflow ? IRF_FT_OVERFLOW : IRF_FT_ABORT);
frame.mData1 = terminal.declared_size;
frame.mData2 = U64(terminal.received_bits) |
(U64(terminal.err_low) << 16) | (U64(terminal.err_high) << 24) | (U64(terminal.err_other) << 32) |
(U64(terminal.message_type) << 40) | (U64(terminal.cause) << 48) | (U64(terminal.reason) << 56);
frame.mFlags = DISPLAY_AS_ERROR_FLAG;
const U64 fid = mResults->AddFrame(frame); const U64 fid = mResults->AddFrame(frame);
pending_byte_frames.clear(); if (e.bubble_text[0] != '\0')
m_bubble_text_by_frame[fid] = e.bubble_text;
std::string short_text; if (++frames_since_commit >= kCommitBatch)
if (terminal.reason == IrFoxTerminalReason::Timeout)
short_text = "❌ TIMEOUT";
else
{ {
short_text = "❌ ABORT"; mResults->CommitResults();
const char* cause = terminal_abort_cause_text(terminal.cause); frames_since_commit = 0;
if (*cause != '\0')
short_text += std::string(" ") + cause;
} }
std::string detail = short_text + " · got=" + std::to_string(terminal.received_bits) + "b";
if (terminal.message_type != 0xFFU)
{
detail += " · ";
detail += irfox::messageTypeText(terminal.message_type);
detail += " len=" + std::to_string(terminal.declared_size) + "B";
}
if (terminal.err_low != 0U || terminal.err_high != 0U || terminal.err_other != 0U)
{
detail += " · err=" + std::to_string(terminal.err_low) + "/" +
std::to_string(terminal.err_high) + "/" + std::to_string(terminal.err_other);
}
auto cached_text = std::make_shared<IrFoxCachedFrameText>();
cached_text->bubble_texts[0] = "";
cached_text->bubble_texts[1] = short_text;
cached_text->bubble_texts[2] = detail;
cached_text->bubble_text_count = 3;
mResults->CacheFrameText(fid, cached_text);
if (detailed_presentation)
mResults->AddMarker(static_cast<U64>(terminal.end_sample), AnalyzerResults::ErrorX,
mSettings.mInputChannel);
note_legacy_frame();
}; };
IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) { IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) {
const IrFoxPacketDecision decision =
irfox::classifyPacket(p.data_bytes, p.pack_size, p.crc_ok, mSettings.mReceiverAddress);
Frame frame; Frame frame;
if (detailed_presentation) frame.mStartingSampleInclusive = static_cast<S64>(p.start_sample);
{
// A Saleae legacy frame cannot overlap another legacy frame. Emit all
// completed bytes except the last one, then use the last byte's span
// for the packet outcome bubble.
for (size_t i = 0; i + 1 < pending_byte_frames.size(); ++i)
add_event_frame(pending_byte_frames[i]);
frame.mStartingSampleInclusive = static_cast<S64>(pending_byte_frames.empty() ?
p.data_start_sample : pending_byte_frames.back().start_sample);
}
else
{
frame.mStartingSampleInclusive = static_cast<S64>(p.start_sample);
}
frame.mEndingSampleInclusive = static_cast<S64>(p.end_sample); frame.mEndingSampleInclusive = static_cast<S64>(p.end_sample);
frame.mFlags = 0; frame.mType = p.crc_ok ? IRF_FT_PACKET_OK : IRF_FT_PACKET_CRC_FAIL;
switch (decision.outcome)
{
case IrFoxPacketOutcome::Accepted:
frame.mType = IRF_FT_PACKET_ACCEPTED;
break;
case IrFoxPacketOutcome::RejectedCrc:
frame.mType = IRF_FT_PACKET_CRC_FAIL;
frame.mFlags |= DISPLAY_AS_ERROR_FLAG;
break;
case IrFoxPacketOutcome::RejectedLength:
frame.mType = IRF_FT_PACKET_BAD_LENGTH;
frame.mFlags |= DISPLAY_AS_ERROR_FLAG;
break;
case IrFoxPacketOutcome::IgnoredAddress:
frame.mType = IRF_FT_PACKET_IGNORED_ADDRESS;
break;
case IrFoxPacketOutcome::RawOnlyUnknownType:
case IrFoxPacketOutcome::RawOnlyTypedLength:
frame.mType = IRF_FT_PACKET_RAW_ONLY;
frame.mFlags |= DISPLAY_AS_WARNING_FLAG;
break;
}
frame.mData1 = p.pack_size; frame.mData1 = p.pack_size;
frame.mData2 = (U64(p.err_low) << 0) | (U64(p.err_high) << 8) | (U64(p.err_other) << 16); frame.mData2 = (U64(p.err_low) << 0) | (U64(p.err_high) << 8) | (U64(p.err_other) << 16);
if (!p.crc_ok)
frame.mFlags |= DISPLAY_AS_ERROR_FLAG;
const U64 fid = mResults->AddFrame(frame); const U64 fid = mResults->AddFrame(frame);
pending_byte_frames.clear();
const std::string icon = packet_icon(decision, mSettings.mPacketIconMode);
std::string hx; std::string hx;
append_hex(hx, p.data_bytes, p.pack_size); append_hex(hx, p.data_bytes, p.pack_size);
std::string status = irfox::packetOutcomeText(decision.outcome); m_packet_hex_by_frame[fid] = std::move(hx);
if (p.pack_size >= irfox::kMsgBytes)
{
status += " ";
status += irfox::messageTypeText(decision.message_type);
}
if (decision.has_destination)
status += " to=" + std::to_string(decision.destination);
auto cached_text = std::make_shared<IrFoxCachedFrameText>(); FrameV2 fv2;
cached_text->export_hex = hx; fv2.AddBoolean("crc_ok", p.crc_ok);
cached_text->bubble_texts[0] = icon; fv2.AddInteger("len", static_cast<S64>(p.pack_size));
if (detailed_presentation) fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
{ fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
char last_byte[3] = "??"; fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
if (p.pack_size > 0) fv2.AddByteArray("data", p.data_bytes, p.pack_size);
std::snprintf(last_byte, sizeof last_byte, "%02X", static_cast<unsigned>(p.data_bytes[p.pack_size - 1])); mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast<U64>(p.start_sample),
cached_text->bubble_texts[1] = std::string("0x") + last_byte + " " + icon; static_cast<U64>(p.end_sample));
cached_text->bubble_texts[2] = cached_text->bubble_texts[1] + " " + status + " " +
std::to_string(p.pack_size) + "B";
if (!hx.empty())
cached_text->bubble_texts[2] += " · " + hx;
}
else
{
cached_text->bubble_texts[1] = icon + " [" + hx + "] " + icon;
cached_text->bubble_texts[2] = icon + " " + status + " " +
std::to_string(p.pack_size) + "B";
if (!hx.empty())
cached_text->bubble_texts[2] += " · [" + hx + "] " + icon;
}
cached_text->bubble_text_count = 3;
mResults->CacheFrameText(fid, cached_text);
if (detailed_presentation)
{
AnalyzerResults::MarkerType outcome_marker = AnalyzerResults::Square;
switch (decision.outcome)
{
case IrFoxPacketOutcome::Accepted:
outcome_marker = AnalyzerResults::Square;
break;
case IrFoxPacketOutcome::IgnoredAddress:
case IrFoxPacketOutcome::RawOnlyUnknownType:
case IrFoxPacketOutcome::RawOnlyTypedLength:
outcome_marker = AnalyzerResults::X;
break;
case IrFoxPacketOutcome::RejectedCrc:
case IrFoxPacketOutcome::RejectedLength:
outcome_marker = AnalyzerResults::ErrorX;
break;
}
mResults->AddMarker(static_cast<U64>(p.end_sample), outcome_marker, mSettings.mInputChannel);
}
if (detailed_presentation)
{
// Structured output is useful in Detailed. Overview intentionally keeps
// only the single legacy packet frame used by the graph bubble.
FrameV2 fv2;
fv2.AddBoolean("crc_ok", p.crc_ok);
fv2.AddBoolean("raw_accepted", decision.raw_accepted());
fv2.AddBoolean("accepted", decision.outcome == IrFoxPacketOutcome::Accepted);
fv2.AddInteger("outcome", static_cast<S64>(decision.outcome));
fv2.AddInteger("message_type", static_cast<S64>(decision.message_type));
fv2.AddInteger("receiver_address", static_cast<S64>(mSettings.mReceiverAddress));
if (decision.has_destination)
fv2.AddInteger("destination", static_cast<S64>(decision.destination));
fv2.AddInteger("len", static_cast<S64>(p.pack_size));
fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
fv2.AddByteArray("data", p.data_bytes, p.pack_size);
const char* type = decision.outcome == IrFoxPacketOutcome::Accepted ? "packet_accepted" :
decision.raw_accepted() ? "packet_raw_only" : "packet_rejected";
mResults->AddFrameV2(fv2, type, static_cast<U64>(p.start_sample), static_cast<U64>(p.end_sample));
}
if (++frames_since_commit >= kCommitBatch) if (++frames_since_commit >= kCommitBatch)
{ {
@ -447,10 +170,6 @@ void IrFoxAnalyzer::WorkerThread()
frames_since_commit = 0; frames_since_commit = 0;
} }
}; };
// In Overview the decoder still performs the same timing, CRC, and receiver
// checks, but does not allocate and dispatch hundreds of visual bit events.
const IrFoxOnBit no_bit_events;
const IrFoxOnBit& bit_events = detailed_presentation ? on_bit : no_bit_events;
auto emit_confirmed_edges = [&]() { auto emit_confirmed_edges = [&]() {
for (;;) for (;;)
@ -461,7 +180,7 @@ void IrFoxAnalyzer::WorkerThread()
return; return;
if (pending[1].sample - pending[0].sample < min_seg_samples) if (pending[1].sample - pending[0].sample < min_seg_samples)
continue; continue;
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal); decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
last_dec_edge_sample = pending[0].sample; last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true; last_dec_edge_valid = true;
pending.erase(pending.begin()); pending.erase(pending.begin());
@ -473,7 +192,7 @@ void IrFoxAnalyzer::WorkerThread()
strip_vs_last_decoder(); strip_vs_last_decoder();
while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples) while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples)
{ {
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal); decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
last_dec_edge_sample = pending[0].sample; last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true; last_dec_edge_valid = true;
pending.erase(pending.begin()); pending.erase(pending.begin());
@ -482,7 +201,7 @@ void IrFoxAnalyzer::WorkerThread()
} }
if (pending.size() == 1) if (pending.size() == 1)
{ {
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal); decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
last_dec_edge_sample = pending[0].sample; last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true; last_dec_edge_valid = true;
pending.clear(); pending.clear();
@ -511,9 +230,7 @@ void IrFoxAnalyzer::WorkerThread()
} }
flush_pending_tail(); flush_pending_tail();
decoder.flushEnd(mIr->GetSampleNumber(), fs, bit_events, on_pkt, on_terminal); decoder.flushEnd(mIr->GetSampleNumber(), fs, on_bit, on_pkt);
if (detailed_presentation)
flush_pending_bytes();
if (frames_since_commit != 0) if (frames_since_commit != 0)
mResults->CommitResults(); mResults->CommitResults();

View File

@ -6,6 +6,8 @@
#include "IrFoxAnalyzerResults.h" #include "IrFoxAnalyzerResults.h"
#include "IrFoxSimulationDataGenerator.h" #include "IrFoxSimulationDataGenerator.h"
#include <memory> #include <memory>
#include <string>
#include <unordered_map>
class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2 class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2
{ {
@ -23,6 +25,9 @@ public:
virtual const char* GetAnalyzerName() const; virtual const char* GetAnalyzerName() const;
virtual bool NeedsRerun(); virtual bool NeedsRerun();
const char* PacketHexForFrame(U64 frame_id);
const char* BubbleTextForFrame(U64 frame_id) const;
protected: protected:
IrFoxAnalyzerSettings mSettings; IrFoxAnalyzerSettings mSettings;
std::unique_ptr<IrFoxAnalyzerResults> mResults; std::unique_ptr<IrFoxAnalyzerResults> mResults;
@ -31,6 +36,10 @@ protected:
IrFoxSimulationDataGenerator mSimulationDataGenerator; IrFoxSimulationDataGenerator mSimulationDataGenerator;
bool mSimulationInitilized; bool mSimulationInitilized;
std::unordered_map<U64, std::string> m_packet_hex_by_frame;
std::unordered_map<U64, std::string> m_bubble_text_by_frame;
mutable std::string m_hex_scratch;
mutable std::string m_bubble_scratch;
}; };
extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName(); extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName();

View File

@ -6,7 +6,6 @@
#include "IrFoxDecoder.h" #include "IrFoxDecoder.h"
#include <cstdio> #include <cstdio>
#include <fstream> #include <fstream>
#include <string>
IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings) IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings)
: AnalyzerResults(), : AnalyzerResults(),
@ -19,89 +18,51 @@ IrFoxAnalyzerResults::~IrFoxAnalyzerResults()
{ {
} }
void IrFoxAnalyzerResults::ClearCachedFrameText()
{
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
m_frame_text_by_frame.clear();
m_frame_text_by_frame.reserve(1024);
}
void IrFoxAnalyzerResults::CacheFrameText(U64 frame_id, std::shared_ptr<const IrFoxCachedFrameText> text)
{
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
m_frame_text_by_frame[frame_id] = std::move(text);
}
std::shared_ptr<const IrFoxCachedFrameText> IrFoxAnalyzerResults::CachedFrameTextForFrame(U64 frame_id) const
{
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
const auto it = m_frame_text_by_frame.find(frame_id);
return it == m_frame_text_by_frame.end() ? nullptr : it->second;
}
void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base) void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
{ {
(void)display_base; (void)display_base;
(void)channel; (void)channel;
ClearResultStrings(); ClearResultStrings();
auto add_cached_text = [&]() {
const std::shared_ptr<const IrFoxCachedFrameText> cached = CachedFrameTextForFrame(frame_index);
if (!cached)
return false;
for (size_t i = 0; i < cached->bubble_text_count; ++i)
AddResultString(cached->bubble_texts[i].c_str());
return true;
};
// Every Overview frame is a packet with immutable, precomputed text. Avoid
// even GetFrame() and formatting on Logic's redraw callback in that mode.
if (mSettings->mPresentation == IrFoxPresentation::Overview && add_cached_text())
return;
Frame frame = GetFrame(frame_index); Frame frame = GetFrame(frame_index);
char line[256]; char line[256];
switch (frame.mType) switch (frame.mType)
{ {
case IRF_FT_DATA_BYTE: case IRF_FT_DATA_BIT:
{ case IRF_FT_SYNC_BIT:
char byte_text[3];
std::snprintf(byte_text, sizeof byte_text, "%02X", static_cast<unsigned>(frame.mData1 & 0xFFu));
AddResultString(byte_text);
AddResultString("0x", byte_text);
break;
}
case IRF_FT_PREAMBLE: case IRF_FT_PREAMBLE:
{
AddResultString("📡");
AddResultString("📡 PRE");
break;
}
case IRF_FT_OVERFLOW: case IRF_FT_OVERFLOW:
case IRF_FT_ABORT: case IRF_FT_ABORT:
case IRF_FT_TIMEOUT:
{ {
if (add_cached_text()) const char* bt = mAnalyzer->BubbleTextForFrame(frame_index);
break; if (bt && bt[0])
AddResultString(""); AddResultString(bt);
AddResultString(frame.mType == IRF_FT_TIMEOUT ? "❌ TIMEOUT" : else if (frame.mType == IRF_FT_DATA_BIT)
(frame.mType == IRF_FT_OVERFLOW ? "❌ ABORT OVF" : "❌ ABORT")); AddResultString(frame.mData1 ? "1" : "0");
else if (frame.mType == IRF_FT_SYNC_BIT)
{
snprintf(line, sizeof line, "sync: %s", frame.mData1 ? "1" : "0");
AddResultString(line);
}
else if (frame.mType == IRF_FT_OVERFLOW)
AddResultString("OVF");
else if (frame.mType == IRF_FT_ABORT)
AddResultString("SYNC!");
else
AddResultString("PRE");
break; break;
} }
case IRF_FT_PACKET_ACCEPTED: case IRF_FT_PACKET_OK:
case IRF_FT_PACKET_CRC_FAIL: case IRF_FT_PACKET_CRC_FAIL:
case IRF_FT_PACKET_BAD_LENGTH:
case IRF_FT_PACKET_RAW_ONLY:
case IRF_FT_PACKET_IGNORED_ADDRESS:
{ {
if (!add_cached_text()) snprintf(line, sizeof line, "%s %lluB", frame.mType == IRF_FT_PACKET_OK ? "OK" : "CRC",
AddResultString(frame.mType == IRF_FT_PACKET_ACCEPTED ? "" : (unsigned long long)frame.mData1);
(frame.mType == IRF_FT_PACKET_CRC_FAIL || frame.mType == IRF_FT_PACKET_BAD_LENGTH) ? "" : AddResultString(line);
frame.mType == IRF_FT_PACKET_IGNORED_ADDRESS ? "📭" : "⚠️"); const char* hx = mAnalyzer->PacketHexForFrame(frame_index);
if (hx && hx[0])
AddResultString(hx);
break; break;
} }
@ -134,44 +95,23 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
const char* typ = "?"; const char* typ = "?";
switch (frame.mType) switch (frame.mType)
{ {
case IRF_FT_PACKET_ACCEPTED: case IRF_FT_DATA_BIT:
typ = "ACCEPT"; typ = "D";
break;
case IRF_FT_SYNC_BIT:
typ = "S";
break;
case IRF_FT_PACKET_OK:
typ = "OK";
break; break;
case IRF_FT_PACKET_CRC_FAIL: case IRF_FT_PACKET_CRC_FAIL:
typ = "REJECT_CRC"; typ = "CRC";
break;
case IRF_FT_PACKET_BAD_LENGTH:
typ = "REJECT_LEN";
break;
case IRF_FT_PACKET_RAW_ONLY:
typ = "RAW_ONLY";
break;
case IRF_FT_PACKET_IGNORED_ADDRESS:
typ = "IGNORE_ADDR";
break; break;
case IRF_FT_OVERFLOW: case IRF_FT_OVERFLOW:
typ = "ABORT_OVF"; typ = "OVF";
break; break;
case IRF_FT_ABORT: case IRF_FT_ABORT:
switch (static_cast<IrFoxAbortCause>((frame.mData2 >> 48) & 0xFFull)) typ = "ABORT";
{
case IrFoxAbortCause::BadSync:
typ = "ABORT_SYNC";
break;
case IrFoxAbortCause::BadLength:
typ = "ABORT_LEN";
break;
case IrFoxAbortCause::Overflow:
typ = "ABORT_OVF";
break;
case IrFoxAbortCause::None:
default:
typ = "ABORT";
break;
}
break;
case IRF_FT_TIMEOUT:
typ = "TIMEOUT";
break; break;
case IRF_FT_PREAMBLE: case IRF_FT_PREAMBLE:
typ = "PRE"; typ = "PRE";
@ -180,12 +120,14 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
break; break;
} }
const std::shared_ptr<const IrFoxCachedFrameText> cached = CachedFrameTextForFrame(i); const char* hx = mAnalyzer->PacketHexForFrame(i);
const char* hx = cached ? cached->export_hex.c_str() : ""; if (!hx)
hx = "";
U64 bit_idx = 0; U64 bit_idx = 0;
U32 err_l = 0, err_h = 0, err_o = 0; U32 err_l = 0, err_h = 0, err_o = 0;
if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT || frame.mType == IRF_FT_TIMEOUT) if (frame.mType == IRF_FT_DATA_BIT || frame.mType == IRF_FT_SYNC_BIT ||
frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT)
{ {
bit_idx = frame.mData2 & 0xFFFFull; bit_idx = frame.mData2 & 0xFFFFull;
err_l = static_cast<U32>((frame.mData2 >> 16) & 0xFFull); err_l = static_cast<U32>((frame.mData2 >> 16) & 0xFFull);

View File

@ -2,22 +2,10 @@
#define IRFOX_ANALYZER_RESULTS #define IRFOX_ANALYZER_RESULTS
#include <AnalyzerResults.h> #include <AnalyzerResults.h>
#include <array>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
class IrFoxAnalyzer; class IrFoxAnalyzer;
class IrFoxAnalyzerSettings; class IrFoxAnalyzerSettings;
struct IrFoxCachedFrameText
{
std::array<std::string, 3> bubble_texts{};
size_t bubble_text_count = 0;
std::string export_hex;
};
class IrFoxAnalyzerResults : public AnalyzerResults class IrFoxAnalyzerResults : public AnalyzerResults
{ {
public: public:
@ -31,15 +19,9 @@ public:
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base); virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base); virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
void ClearCachedFrameText();
void CacheFrameText(U64 frame_id, std::shared_ptr<const IrFoxCachedFrameText> text);
std::shared_ptr<const IrFoxCachedFrameText> CachedFrameTextForFrame(U64 frame_id) const;
protected: protected:
IrFoxAnalyzerSettings* mSettings; IrFoxAnalyzerSettings* mSettings;
IrFoxAnalyzer* mAnalyzer; IrFoxAnalyzer* mAnalyzer;
mutable std::mutex m_frame_text_mutex;
std::unordered_map<U64, std::shared_ptr<const IrFoxCachedFrameText>> m_frame_text_by_frame;
}; };
#endif #endif

View File

@ -3,42 +3,14 @@
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings() IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
: mInputChannel(UNDEFINED_CHANNEL), : mInputChannel(UNDEFINED_CHANNEL),
mReceiverAddress(0), mInputChannelInterface()
mPresentation(IrFoxPresentation::Overview),
mPacketIconMode(IrFoxPacketIconMode::StatusAndType),
mInputChannelInterface(),
mReceiverAddressInterface(),
mPresentationInterface(),
mPacketIconModeInterface()
{ {
mInputChannelInterface.SetTitleAndTooltip( mInputChannelInterface.SetTitleAndTooltip(
"IR", "IR",
"Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)"); "Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)");
mInputChannelInterface.SetChannel(mInputChannel); mInputChannelInterface.SetChannel(mInputChannel);
mReceiverAddressInterface.SetTitleAndTooltip(
"Receiver address",
"IR receiver ID for ACCEPT/IGNORE ADDR. 0 mirrors a receiver configured to accept every address; 65000..65535 are broadcast destinations.");
mReceiverAddressInterface.SetMin(0);
mReceiverAddressInterface.SetMax(65535);
mReceiverAddressInterface.SetInteger(mReceiverAddress);
mPresentationInterface.SetTitleAndTooltip(
"Presentation",
"Overview shows one packet bubble over the full frame. Detailed separates PRE, packet, and hexadecimal bytes. Logic 2 does not expose zoom to analyzers, so this is selected explicitly.");
mPresentationInterface.AddNumber(static_cast<double>(IrFoxPresentation::Overview), "Overview", "One outcome bubble across the full frame; no PRE badge.");
mPresentationInterface.AddNumber(static_cast<double>(IrFoxPresentation::Detailed), "Detailed", "Separate PRE and packet bubbles, plus one hexadecimal bubble per byte.");
mPresentationInterface.SetNumber(static_cast<double>(mPresentation));
mPacketIconModeInterface.SetTitleAndTooltip(
"Packet icon",
"Choose whether packet bubbles show reception status, decoded message type, or both.");
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::Status), "Status ✅", "One status symbol: accepted, other address, invalid, or unknown.");
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::MessageType), "Message type 📦", "One symbol for the decoded firmware message type.");
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::StatusAndType), "Status + type ✅📦", "Reception status followed by the decoded firmware message type.");
mPacketIconModeInterface.SetNumber(static_cast<double>(mPacketIconMode));
AddInterface(&mInputChannelInterface); AddInterface(&mInputChannelInterface);
AddInterface(&mReceiverAddressInterface);
AddInterface(&mPresentationInterface);
AddInterface(&mPacketIconModeInterface);
AddExportOption(0, "Export as text/csv file"); AddExportOption(0, "Export as text/csv file");
AddExportExtension(0, "text", "txt"); AddExportExtension(0, "text", "txt");
@ -55,23 +27,6 @@ IrFoxAnalyzerSettings::~IrFoxAnalyzerSettings()
bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces() bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
{ {
mInputChannel = mInputChannelInterface.GetChannel(); mInputChannel = mInputChannelInterface.GetChannel();
mReceiverAddress = static_cast<uint16_t>(mReceiverAddressInterface.GetInteger());
const int presentation = static_cast<int>(mPresentationInterface.GetNumber());
mPresentation = presentation == static_cast<int>(IrFoxPresentation::Detailed) ?
IrFoxPresentation::Detailed : IrFoxPresentation::Overview;
const int packet_icon_mode = static_cast<int>(mPacketIconModeInterface.GetNumber());
switch (packet_icon_mode)
{
case static_cast<int>(IrFoxPacketIconMode::Status):
mPacketIconMode = IrFoxPacketIconMode::Status;
break;
case static_cast<int>(IrFoxPacketIconMode::MessageType):
mPacketIconMode = IrFoxPacketIconMode::MessageType;
break;
default:
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
break;
}
ClearChannels(); ClearChannels();
AddChannel(mInputChannel, "IR Fox", true); AddChannel(mInputChannel, "IR Fox", true);
@ -82,9 +37,6 @@ bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings() void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings()
{ {
mInputChannelInterface.SetChannel(mInputChannel); mInputChannelInterface.SetChannel(mInputChannel);
mReceiverAddressInterface.SetInteger(mReceiverAddress);
mPresentationInterface.SetNumber(static_cast<double>(mPresentation));
mPacketIconModeInterface.SetNumber(static_cast<double>(mPacketIconMode));
} }
void IrFoxAnalyzerSettings::LoadSettings(const char* settings) void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
@ -93,50 +45,6 @@ void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
text_archive.SetString(settings); text_archive.SetString(settings);
text_archive >> mInputChannel; text_archive >> mInputChannel;
S32 receiver_address = 0;
if (text_archive >> receiver_address)
{
if (receiver_address < 0)
receiver_address = 0;
if (receiver_address > 65535)
receiver_address = 65535;
mReceiverAddress = static_cast<uint16_t>(receiver_address);
}
else
{
// Version 0 settings stored only the channel.
mReceiverAddress = 0;
}
// Settings before the presentation switch contain the former "show bit cells"
// value in this position. Bits are now deliberately always shown.
S32 legacy_show_bit_cells = 0;
(void)(text_archive >> legacy_show_bit_cells);
S32 presentation = static_cast<S32>(IrFoxPresentation::Overview);
if (text_archive >> presentation && presentation == static_cast<S32>(IrFoxPresentation::Detailed))
mPresentation = IrFoxPresentation::Detailed;
else
mPresentation = IrFoxPresentation::Overview;
S32 packet_icon_mode = static_cast<S32>(IrFoxPacketIconMode::StatusAndType);
if (text_archive >> packet_icon_mode)
{
switch (packet_icon_mode)
{
case static_cast<S32>(IrFoxPacketIconMode::Status):
mPacketIconMode = IrFoxPacketIconMode::Status;
break;
case static_cast<S32>(IrFoxPacketIconMode::MessageType):
mPacketIconMode = IrFoxPacketIconMode::MessageType;
break;
default:
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
break;
}
}
else
{
// Existing analyzer instances gain the most informative mode by default.
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
}
ClearChannels(); ClearChannels();
AddChannel(mInputChannel, "IR Fox", true); AddChannel(mInputChannel, "IR Fox", true);
@ -149,11 +57,6 @@ const char* IrFoxAnalyzerSettings::SaveSettings()
SimpleArchive text_archive; SimpleArchive text_archive;
text_archive << mInputChannel; text_archive << mInputChannel;
text_archive << static_cast<S32>(mReceiverAddress);
// Retain the old third field so already-saved configurations remain readable.
text_archive << static_cast<S32>(1);
text_archive << static_cast<S32>(mPresentation);
text_archive << static_cast<S32>(mPacketIconMode);
return SetReturnString(text_archive.GetString()); return SetReturnString(text_archive.GetString());
} }

View File

@ -3,22 +3,6 @@
#include <AnalyzerSettings.h> #include <AnalyzerSettings.h>
#include <AnalyzerTypes.h> #include <AnalyzerTypes.h>
#include <cstdint>
/** Logic 2 does not pass the current zoom level to an analyzer, so the user selects the annotation density. */
enum class IrFoxPresentation : uint8_t
{
Overview = 0,
Detailed = 1,
};
/** Which compact symbol is used at the shortest packet-bubble zoom level. */
enum class IrFoxPacketIconMode : uint8_t
{
Status = 0,
MessageType = 1,
StatusAndType = 2,
};
class IrFoxAnalyzerSettings : public AnalyzerSettings class IrFoxAnalyzerSettings : public AnalyzerSettings
{ {
@ -32,16 +16,9 @@ public:
virtual const char* SaveSettings(); virtual const char* SaveSettings();
Channel mInputChannel; Channel mInputChannel;
/** Receiver ID used by the same address rule as IR_FOX::checkAddressRuleApply. 0 means catch all. */
uint16_t mReceiverAddress;
IrFoxPresentation mPresentation;
IrFoxPacketIconMode mPacketIconMode;
protected: protected:
AnalyzerSettingInterfaceChannel mInputChannelInterface; AnalyzerSettingInterfaceChannel mInputChannelInterface;
AnalyzerSettingInterfaceInteger mReceiverAddressInterface;
AnalyzerSettingInterfaceNumberList mPresentationInterface;
AnalyzerSettingInterfaceNumberList mPacketIconModeInterface;
}; };
#endif #endif

View File

@ -52,20 +52,6 @@ bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out)
return ok; return ok;
} }
void IrFoxDecoder::preamble_reset_to_idle()
{
preamble_state_ = PreambleState::Idle;
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
preamble_candidate_last_edge_us_ = 0;
preamble_candidate_first_rise_us_ = 0;
preamble_candidate_first_rise_valid_ = false;
preamble_bubble_start_valid_ = false;
is_preamb = false;
is_wrong_pack = false;
is_buffer_overflow = false;
}
void IrFoxDecoder::first_rx() void IrFoxDecoder::first_rx()
{ {
err_low_signal = err_high_signal = err_other = 0; err_low_signal = err_high_signal = err_other = 0;
@ -79,7 +65,7 @@ void IrFoxDecoder::first_rx()
i_sync_bit = 0; i_sync_bit = 0;
err_sync_bit = 0; err_sync_bit = 0;
is_wrong_pack = false; is_wrong_pack = false;
is_preamb = false; is_preamb = true;
is_recive = false; is_recive = false;
is_recive_raw = false; is_recive_raw = false;
msg_type_receive = 0; msg_type_receive = 0;
@ -87,66 +73,6 @@ void IrFoxDecoder::first_rx()
std::memset(data_buffer, 0, sizeof data_buffer); std::memset(data_buffer, 0, sizeof data_buffer);
preamble_bubble_start_valid_ = false; preamble_bubble_start_valid_ = false;
trim_first_data_bit_cell_ = false; trim_first_data_bit_cell_ = false;
packet_start_sample_ = 0;
packet_start_valid_ = false;
packet_data_start_sample_ = 0;
packet_data_start_valid_ = false;
byte_start_sample_ = 0;
preamble_reset_to_idle();
}
void IrFoxDecoder::release_preamble_guard(double t_us)
{
const uint32_t long_silence_us = irfox::irTimeoutUs(rise_sync_time_us) * 2U;
// Mirror IR_DecoderRaw::releasePreambleGuard. A negative value is the
// floating-point equivalent of the firmware's wrap-safe unsigned offset.
prev_rise_us = t_us - static_cast<double>(long_silence_us) - 1.0;
}
void IrFoxDecoder::emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
const IrFoxOnTerminal& on_terminal) const
{
if (!on_terminal)
return;
IrFoxEmitTerminal terminal{};
const uint64_t start_sample = packet_start_valid_ ? packet_start_sample_ :
(packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
terminal.start_sample = static_cast<int64_t>(start_sample);
terminal.detail_start_sample = static_cast<int64_t>(
packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
terminal.end_sample = static_cast<int64_t>(end_sample);
terminal.reason = reason;
terminal.cause = cause;
terminal.message_type = i_data_buffer >= irfox::kBitPerByte ?
static_cast<uint8_t>((data_buffer[0] >> 5U) & 0x07U) : 0xFFU;
terminal.declared_size = static_cast<uint8_t>(pack_size);
terminal.received_bits = i_data_buffer;
terminal.err_low = err_low_signal;
terminal.err_high = err_high_signal;
terminal.err_other = err_other;
on_terminal(terminal);
}
void IrFoxDecoder::abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
const IrFoxOnTerminal& on_terminal)
{
emit_terminal(IrFoxTerminalReason::Abort, cause, end_sample, on_terminal);
is_recive = false;
is_recive_raw = false;
msg_type_receive = 0;
first_rx();
release_preamble_guard(t_us);
}
void IrFoxDecoder::expire_preamble_candidate(double t_us)
{
if (preamble_state_ != PreambleState::Candidate)
return;
const uint32_t timeout_us =
irfox::irTimeoutUs(rise_sync_time_us) * irfox::kPreambleCandidateTimeoutMult;
if ((t_us - preamble_candidate_last_edge_us_) > static_cast<double>(timeout_us))
preamble_reset_to_idle();
} }
void IrFoxDecoder::listen_start(double t_us) void IrFoxDecoder::listen_start(double t_us)
@ -160,20 +86,13 @@ void IrFoxDecoder::listen_start(double t_us)
} }
} }
void IrFoxDecoder::check_timeout(double t_us, uint32_t fs, const IrFoxOnTerminal& on_terminal) void IrFoxDecoder::check_timeout(double t_us)
{ {
if (!is_recive) if (!is_recive)
return; return;
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
if (t_us - last_edge_time_us > irmax * 2.0) if (t_us - last_edge_time_us > irmax * 2.0)
{ {
const uint64_t timeout_us = static_cast<uint64_t>(irmax) * 2U;
// The callback fires only after the strict > 2T boundary, but the terminal
// frame owns samples only through 2T. This leaves a following edge free to
// seed the next preamble without overlapping inclusive Saleae frames.
const uint64_t timeout_samples = (timeout_us * static_cast<uint64_t>(fs)) / 1000000ULL;
emit_terminal(IrFoxTerminalReason::Timeout, IrFoxAbortCause::None,
last_edge_sample + timeout_samples, on_terminal);
// Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM. // Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM.
is_recive = false; is_recive = false;
msg_type_receive = 0; msg_type_receive = 0;
@ -184,23 +103,29 @@ void IrFoxDecoder::check_timeout(double t_us, uint32_t fs, const IrFoxOnTerminal
} }
void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s, void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, IrFoxEmitBitMode emit_mode)
const IrFoxOnTerminal& on_terminal, IrFoxEmitBitMode emit_mode)
{ {
if (i_data_buffer >= irfox::kDataByteSizeMax * 8u) if (i_data_buffer > irfox::kDataByteSizeMax * 8u)
{ {
if (!is_buffer_overflow && on_bit)
{
IrFoxEmitBit e{};
e.start_sample = static_cast<int64_t>(cell_start_s);
e.end_sample = static_cast<int64_t>(cell_end_s);
e.frame_type = IRF_FT_OVERFLOW;
e.mflags = DISPLAY_AS_ERROR_FLAG;
fill_err_snapshot(e);
std::strncpy(e.bubble_text, "OVF", sizeof e.bubble_text);
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
on_bit(e);
}
is_buffer_overflow = true; is_buffer_overflow = true;
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::Overflow, on_terminal);
return;
} }
if (is_buffer_overflow || is_preamb || is_wrong_pack) if (is_buffer_overflow || is_preamb || is_wrong_pack)
{ {
// Firmware treats overflow/invalid frame state as a terminal abort and // Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма.
// immediately permits a fresh preamble candidate. first_rx();
const IrFoxAbortCause cause = is_buffer_overflow ? IrFoxAbortCause::Overflow :
(is_wrong_pack ? IrFoxAbortCause::BadSync : IrFoxAbortCause::None);
abort_frame(last_edge_time_us, cell_end_s, cause, on_terminal);
return; return;
} }
@ -215,19 +140,6 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
if (is_data) if (is_data)
{ {
const bool was_first_data_bit = (i_data_buffer == 0); const bool was_first_data_bit = (i_data_buffer == 0);
const bool was_first_bit_of_byte = (i_data_buffer % irfox::kBitPerByte) == 0U;
if (was_first_data_bit && !packet_start_valid_)
{
packet_start_sample_ = cell_start_s;
packet_start_valid_ = true;
}
if (was_first_data_bit)
{
packet_data_start_sample_ = cell_start_s;
packet_data_start_valid_ = true;
}
if (was_first_bit_of_byte)
byte_start_sample_ = cell_start_s;
data_buffer[i_data_buffer / 8] |= static_cast<uint8_t>(bit ? 1 : 0) << (7 - (i_data_buffer % 8)); data_buffer[i_data_buffer / 8] |= static_cast<uint8_t>(bit ? 1 : 0) << (7 - (i_data_buffer % 8));
i_data_buffer++; i_data_buffer++;
buf_bit_pos++; buf_bit_pos++;
@ -244,16 +156,6 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
e.bubble_text[1] = '\0'; e.bubble_text[1] = '\0';
on_bit(e); on_bit(e);
} }
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble &&
(i_data_buffer % irfox::kBitPerByte) == 0U)
{
const uint64_t byte_index = (i_data_buffer / irfox::kBitPerByte) - 1U;
IrFoxEmitBit e{static_cast<int64_t>(byte_start_sample_), static_cast<int64_t>(cell_end_s), IRF_FT_DATA_BYTE,
data_buffer[byte_index], byte_index, fl, pack_trace_invert_fix, 0, 0, 0};
fill_err_snapshot(e);
std::snprintf(e.bubble_text, sizeof e.bubble_text, "%02X", static_cast<unsigned>(data_buffer[byte_index]));
on_bit(e);
}
if (was_first_data_bit && trim_first_data_bit_cell_) if (was_first_data_bit && trim_first_data_bit_cell_)
trim_first_data_bit_cell_ = false; trim_first_data_bit_cell_ = false;
} }
@ -284,10 +186,14 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits); const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits);
if (fatal_sync) if (fatal_sync)
is_wrong_pack = true; is_wrong_pack = true;
if (fatal_sync) if (on_bit && fatal_sync)
{ {
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadSync, on_terminal); IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_ABORT,
return; 0, 0, DISPLAY_AS_ERROR_FLAG, false, 0, 0, 0};
fill_err_snapshot(e);
std::strncpy(e.bubble_text, "SYNC!", sizeof e.bubble_text);
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
on_bit(e);
} }
} }
} }
@ -310,23 +216,12 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
if (!is_available && is_data && !is_wrong_pack) if (!is_available && is_data && !is_wrong_pack)
{ {
if (i_data_buffer == 8 * irfox::kMsgBytes) if (i_data_buffer == 8 * irfox::kMsgBytes)
{
pack_size = static_cast<uint16_t>(data_buffer[0] & 0x1Fu); pack_size = static_cast<uint16_t>(data_buffer[0] & 0x1Fu);
// The receiver rejects a length that cannot contain its two CRC bytes.
// Emit a terminal abort so the capture explains why no packet follows.
if (pack_size < irfox::kMsgBytes + irfox::kCrcBytes)
{
is_wrong_pack = true;
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadLength, on_terminal);
return;
}
}
if (pack_size && (i_data_buffer == 8)) if (pack_size && (i_data_buffer == 8))
msg_type_receive = static_cast<uint8_t>((data_buffer[0] >> 5) | 0xF8u); msg_type_receive = static_cast<uint8_t>((data_buffer[0] >> 5) | 0xF8u);
if (pack_size >= irfox::kMsgBytes + irfox::kCrcBytes && if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte))
(i_data_buffer == pack_size * irfox::kBitPerByte))
{ {
uint16_t crc_computed = 0; uint16_t crc_computed = 0;
const bool crc_ok = crc_check(static_cast<uint8_t>(pack_size - irfox::kCrcBytes), crc_computed); const bool crc_ok = crc_check(static_cast<uint8_t>(pack_size - irfox::kCrcBytes), crc_computed);
@ -337,8 +232,7 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
is_available = crc_ok; is_available = crc_ok;
IrFoxEmitPacket pkt{}; IrFoxEmitPacket pkt{};
pkt.start_sample = static_cast<int64_t>(packet_start_valid_ ? packet_start_sample_ : cell_start_s); pkt.start_sample = static_cast<int64_t>(cell_start_s);
pkt.data_start_sample = static_cast<int64_t>(packet_data_start_valid_ ? packet_data_start_sample_ : cell_start_s);
pkt.end_sample = static_cast<int64_t>(cell_end_s); pkt.end_sample = static_cast<int64_t>(cell_end_s);
pkt.crc_ok = crc_ok; pkt.crc_ok = crc_ok;
pkt.pack_size = static_cast<uint8_t>(pack_size); pkt.pack_size = static_cast<uint8_t>(pack_size);
@ -354,198 +248,37 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
} }
void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit, void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit,
const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal) const IrFoxOnPacket& on_pkt)
{ {
const double t_us = sample_to_us(sample, fs); const double t_us = sample_to_us(sample, fs);
// Firmware advances terminal timers in this order while no queued edge is
// pending. For an offline capture, do the equivalent immediately before the
// next timestamped edge is consumed.
check_timeout(t_us, fs, on_terminal);
listen_start(t_us);
expire_preamble_candidate(t_us);
// A timeout/abort may have restored the nominal adaptive bit period.
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U; uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U;
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
/** Firmware starts a preamble candidate only on its first rising edge after silence. */ listen_start(t_us);
auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t {
(void)is_rising;
return edge_s;
};
// Mirror IR_DecoderRaw::preambleProcessEdge. A frame may start only after // Как IR_DecoderRaw: пауза между фронтами по lastEdgeTime при активном приёме кадра.
// a long silence and two mutually consistent rise-to-rise periods. if (last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0 && is_recive)
auto start_preamble_candidate = [&]() { check_timeout(t_us);
preamble_state_ = PreambleState::Candidate;
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
preamble_candidate_last_edge_us_ = t_us;
preamble_candidate_first_rise_us_ = t_us;
preamble_candidate_first_rise_valid_ = rising;
is_preamb = true;
is_recive = false;
is_recive_raw = false;
is_wrong_pack = false;
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising);
preamble_bubble_start_valid_ = true;
};
const uint32_t long_silence_us = irmax * 2U;
if (preamble_state_ == PreambleState::Locked && !is_recive_raw)
{
preamble_state_ = PreambleState::Idle;
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
}
if (preamble_state_ == PreambleState::Idle)
{
const bool enough_silence = prev_rise_us == 0.0 ? t_us > static_cast<double>(long_silence_us) :
(t_us - prev_rise_us) > static_cast<double>(long_silence_us);
if (!is_recive_raw && rising && enough_silence)
{
start_preamble_candidate();
// The first rising edge only opens Candidate; it must not also be
// compared with itself as a zero-length preamble period.
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
else
{
// IR_DecoderRaw ignores idle edges until a valid preamble candidate starts.
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
}
if (preamble_state_ == PreambleState::Candidate)
{
preamble_candidate_last_edge_us_ = t_us;
if (!rising)
{
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
if (!preamble_candidate_first_rise_valid_)
{
preamble_candidate_first_rise_valid_ = true;
preamble_candidate_first_rise_us_ = t_us;
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
const uint32_t period_us = static_cast<uint32_t>(t_us - preamble_candidate_first_rise_us_);
preamble_candidate_first_rise_us_ = t_us;
if (!irfox::preambleRisePeriodCoarseOk(period_us))
{
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
if (preamble_good_periods_ == 0)
{
preamble_good_periods_ = 1;
preamble_mean_period_us_ = period_us;
}
else
{
const uint32_t delta = period_us > preamble_mean_period_us_ ? period_us - preamble_mean_period_us_ :
preamble_mean_period_us_ - period_us;
if (delta <= irfox::preambleJitterTolUs(preamble_mean_period_us_))
{
if (preamble_good_periods_ < 255U)
++preamble_good_periods_;
preamble_mean_period_us_ = (preamble_mean_period_us_ * 3U + period_us) / 4U;
}
else
{
preamble_good_periods_ = 1;
preamble_mean_period_us_ = period_us;
}
}
if (preamble_good_periods_ < irfox::kPreambleLockRisePeriods)
{
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
// The firmware clears all frame state when the candidate becomes locked.
err_low_signal = err_high_signal = err_other = 0;
pack_size = 0;
is_buffer_overflow = false;
is_available = false;
buf_bit_pos = 0;
is_data = true;
i_data_buffer = 0;
next_control_bit = irfox::kBitPerByte;
i_sync_bit = 0;
err_sync_bit = 0;
is_wrong_pack = false;
msg_type_receive = 0;
std::memset(data_buffer, 0, sizeof data_buffer);
packet_start_sample_ = preamble_bubble_start_sample_;
packet_start_valid_ = preamble_bubble_start_valid_;
packet_data_start_sample_ = 0;
packet_data_start_valid_ = false;
byte_start_sample_ = 0;
preamble_state_ = PreambleState::Locked;
is_preamb = false;
is_recive = true;
is_recive_raw = true;
rise_period_us = preamble_mean_period_us_;
prev_rise_us = t_us + static_cast<double>(preamble_mean_period_us_) / 2.0;
prev_rise_sample = sample + static_cast<uint64_t>(std::llround(
static_cast<double>(preamble_mean_period_us_) * 0.5 * static_cast<double>(fs) / 1e6));
trim_first_data_bit_cell_ = true;
// The analyzer chooses whether this is visible (Detailed) or folded into
// the full packet frame (Overview).
if (on_bit && preamble_bubble_start_valid_)
{
IrFoxEmitBit pe{};
pe.start_sample = static_cast<int64_t>(preamble_bubble_start_sample_);
pe.end_sample = static_cast<int64_t>(sample > 0 ? sample - 1 : sample);
pe.frame_type = IRF_FT_PREAMBLE;
fill_err_snapshot(pe);
on_bit(pe);
}
preamble_bubble_start_valid_ = false;
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
// As in processDecodedFront, the edge becomes the timing reference only
// after the preamble state machine has allowed it through.
last_edge_time_us = t_us; last_edge_time_us = t_us;
last_edge_sample = sample; last_edge_sample = sample;
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
/** Визуализация: начало PRE с ближайшего спада в пределах ~3 битовых периодов (ИК-метка). */
auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t {
if (!is_rising)
return edge_s;
if (edge_s > prev_fall_sample)
{
const double span_us = double(edge_s - prev_fall_sample) * 1e6 / double(fs);
const double max_us = double(rise_max_us) * 3.0;
if (span_us <= max_us)
return prev_fall_sample;
}
return edge_s;
};
if (rising) if (rising)
{ {
const double delta_rp = t_us - prev_rise_us; const double delta_rp = t_us - prev_rise_us;
@ -624,6 +357,78 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
} }
} }
// Как IR_DecoderRaw::tick: после длинной паузы старт сырого приёма (без отдельного firstRX — флаги ниже).
if (t_us > prev_rise_us && (t_us - prev_rise_us) > irmax * 2.0 && !is_recive_raw)
{
preamb_front_counter = static_cast<int8_t>(irfox::kPreambFronts - 1);
is_preamb = true;
is_recive = true;
is_recive_raw = true;
is_wrong_pack = false;
if (!preamble_bubble_start_valid_)
{
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising);
preamble_bubble_start_valid_ = true;
}
}
if (preamb_front_counter)
{
if (rising && rise_period_us < irmax)
{
if (rise_period_us < rise_min_us / 2U)
{
preamb_front_counter += 2;
err_other++;
}
}
preamb_front_counter--;
}
else
{
if (is_preamb)
{
is_preamb = false;
// IR_DecoderRaw: prevRise += risePeriod / 2 — фаза как в прошивке.
// Бабл PRE: до текущего фронта (sample1), чтобы охватить все kPreambPulse периодов (3 импульса),
// а не только до предыдущего подъёма (~2 периода).
const uint64_t preamble_bubble_end_sample = sample > 0 ? sample - 1 : sample;
prev_rise_us += rise_period_us / 2.0;
{
const double half_us = 0.5 * static_cast<double>(rise_period_us);
const uint64_t half_s = static_cast<uint64_t>(std::llround(half_us * double(fs) / 1e6));
prev_rise_sample += half_s;
}
trim_first_data_bit_cell_ = true;
if (on_bit && preamble_bubble_start_valid_)
{
int64_t pe_start = static_cast<int64_t>(preamble_bubble_start_sample_);
int64_t pe_end = static_cast<int64_t>(preamble_bubble_end_sample);
if (preamble_bubble_end_sample == 0 || pe_end < pe_start)
pe_end = static_cast<int64_t>(sample > 0 ? sample - 1 : sample);
IrFoxEmitBit pe{};
pe.start_sample = pe_start;
pe.end_sample = pe_end;
pe.frame_type = IRF_FT_PREAMBLE;
fill_err_snapshot(pe);
std::strncpy(pe.bubble_text, "PRE", sizeof pe.bubble_text);
pe.bubble_text[sizeof pe.bubble_text - 1] = '\0';
on_bit(pe);
}
preamble_bubble_start_valid_ = false;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
}
if (is_preamb)
{
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
if (rise_period_us > irmax || is_buffer_overflow || rise_period_us < rise_min_us || is_wrong_pack) if (rise_period_us > irmax || is_buffer_overflow || rise_period_us < rise_min_us || is_wrong_pack)
{ {
last_processed_edge_us = t_us; last_processed_edge_us = t_us;
@ -650,11 +455,9 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us)) if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us))
{ {
if (high_time_us > low_time_us) if (high_time_us > low_time_us)
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
IrFoxEmitBitMode::WithBubble);
else else
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
IrFoxEmitBitMode::WithBubble);
} }
else else
{ {
@ -744,15 +547,13 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
if (i == low_count - 1 && invert_err) if (i == low_count - 1 && invert_err)
{ {
invert_err = false; invert_err = false;
write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
IrFoxEmitBitMode::Quiet);
merge_warn = true; merge_warn = true;
append_merge(row_is_data, true); append_merge(row_is_data, true);
} }
else else
{ {
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
IrFoxEmitBitMode::Quiet);
append_merge(row_is_data, false); append_merge(row_is_data, false);
} }
} }
@ -763,15 +564,13 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
if (i == high_count - 1 && invert_err) if (i == high_count - 1 && invert_err)
{ {
invert_err = false; invert_err = false;
write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
IrFoxEmitBitMode::Quiet);
merge_warn = true; merge_warn = true;
append_merge(row_is_data, false); append_merge(row_is_data, false);
} }
else else
{ {
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
IrFoxEmitBitMode::Quiet);
append_merge(row_is_data, true); append_merge(row_is_data, true);
} }
} }
@ -784,13 +583,11 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
have_last_processed = true; have_last_processed = true;
} }
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt)
const IrFoxOnTerminal& on_terminal)
{ {
const double t_us = sample_to_us(last_sample, fs); const double t_us = sample_to_us(last_sample, fs);
check_timeout(t_us, fs, on_terminal);
listen_start(t_us); listen_start(t_us);
expire_preamble_candidate(t_us); check_timeout(t_us);
(void)on_bit; (void)on_bit;
(void)on_pkt; (void)on_pkt;
} }

View File

@ -8,17 +8,11 @@ enum IrFoxFrameType : uint8_t
{ {
IRF_FT_DATA_BIT = 1, IRF_FT_DATA_BIT = 1,
IRF_FT_SYNC_BIT = 2, IRF_FT_SYNC_BIT = 2,
IRF_FT_PACKET_ACCEPTED = 3, IRF_FT_PACKET_OK = 3,
IRF_FT_PACKET_CRC_FAIL = 4, IRF_FT_PACKET_CRC_FAIL = 4,
IRF_FT_OVERFLOW = 5, IRF_FT_OVERFLOW = 5,
IRF_FT_ABORT = 6, IRF_FT_ABORT = 6,
IRF_FT_PREAMBLE = 7, IRF_FT_PREAMBLE = 7,
IRF_FT_PACKET_BAD_LENGTH = 8,
IRF_FT_PACKET_RAW_ONLY = 9,
IRF_FT_PACKET_IGNORED_ADDRESS = 10,
IRF_FT_DATA_BYTE = 11,
IRF_FT_TIMEOUT = 12,
IRF_FT_PACKET_OK = IRF_FT_PACKET_ACCEPTED,
}; };
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */ /** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
@ -47,8 +41,6 @@ struct IrFoxEmitBit
struct IrFoxEmitPacket struct IrFoxEmitPacket
{ {
int64_t start_sample; int64_t start_sample;
/** First data-bit cell: the visible boundary between the preamble and payload. */
int64_t data_start_sample;
int64_t end_sample; int64_t end_sample;
bool crc_ok; bool crc_ok;
uint8_t pack_size; uint8_t pack_size;
@ -58,47 +50,16 @@ struct IrFoxEmitPacket
uint8_t data_bytes[irfox::kDataByteSizeMax]; uint8_t data_bytes[irfox::kDataByteSizeMax];
}; };
enum class IrFoxTerminalReason : uint8_t
{
Abort,
Timeout,
};
enum class IrFoxAbortCause : uint8_t
{
None,
BadSync,
BadLength,
Overflow,
};
struct IrFoxEmitTerminal
{
int64_t start_sample;
int64_t detail_start_sample;
int64_t end_sample;
IrFoxTerminalReason reason;
IrFoxAbortCause cause;
uint8_t message_type;
uint8_t declared_size;
uint16_t received_bits;
uint8_t err_low;
uint8_t err_high;
uint8_t err_other;
};
using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>; using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>;
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>; using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
using IrFoxOnTerminal = std::function<void(const IrFoxEmitTerminal&)>;
class IrFoxDecoder class IrFoxDecoder
{ {
public: public:
void reset(); void reset();
void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit,
const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal); const IrFoxOnPacket& on_pkt);
void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt);
const IrFoxOnTerminal& on_terminal);
private: private:
static uint16_t ceil_div_u16(uint16_t val, uint16_t divider); static uint16_t ceil_div_u16(uint16_t val, uint16_t divider);
@ -106,17 +67,10 @@ private:
bool crc_check(uint8_t len, uint16_t& crc_out); bool crc_check(uint8_t len, uint16_t& crc_out);
void first_rx(); void first_rx();
void preamble_reset_to_idle();
void release_preamble_guard(double t_us);
void emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
const IrFoxOnTerminal& on_terminal) const;
void abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
const IrFoxOnTerminal& on_terminal);
void expire_preamble_candidate(double t_us);
void listen_start(double t_us); void listen_start(double t_us);
void check_timeout(double t_us, uint32_t sample_rate_hz, const IrFoxOnTerminal& on_terminal); void check_timeout(double t_us);
void write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s, void write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble); IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble);
double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); } double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); }
@ -147,11 +101,6 @@ private:
uint64_t preamble_bubble_start_sample_ = 0; uint64_t preamble_bubble_start_sample_ = 0;
bool preamble_bubble_start_valid_ = false; bool preamble_bubble_start_valid_ = false;
bool trim_first_data_bit_cell_ = false; bool trim_first_data_bit_cell_ = false;
uint64_t packet_start_sample_ = 0;
bool packet_start_valid_ = false;
uint64_t packet_data_start_sample_ = 0;
bool packet_data_start_valid_ = false;
uint64_t byte_start_sample_ = 0;
double last_edge_time_us = 0; double last_edge_time_us = 0;
uint64_t last_edge_sample = 0; uint64_t last_edge_sample = 0;
@ -174,18 +123,7 @@ private:
int8_t all_count = 0; int8_t all_count = 0;
uint16_t wrong_counter = 0; uint16_t wrong_counter = 0;
enum class PreambleState : uint8_t int8_t preamb_front_counter = 0;
{
Idle,
Candidate,
Locked,
};
PreambleState preamble_state_ = PreambleState::Idle;
uint8_t preamble_good_periods_ = 0;
uint32_t preamble_mean_period_us_ = 0;
double preamble_candidate_last_edge_us_ = 0;
double preamble_candidate_first_rise_us_ = 0;
bool preamble_candidate_first_rise_valid_ = false;
int16_t buf_bit_pos = 0; int16_t buf_bit_pos = 0;
bool is_data = true; bool is_data = true;
uint16_t i_data_buffer = 0; uint16_t i_data_buffer = 0;

View File

@ -1,172 +0,0 @@
#pragma once
#include "IrFoxProtocolConstants.h"
#include <cstdint>
/**
* The decision made after IR_DecoderRaw::availableRaw() in IR_Decoder::_tick().
* It intentionally models only what a capture can prove: decoding, CRC, typed
* layout, and the receiver address. It does not claim that application code
* subsequently acted on the delivered message.
*/
enum class IrFoxPacketOutcome : uint8_t
{
Accepted,
RawOnlyUnknownType,
RawOnlyTypedLength,
IgnoredAddress,
RejectedCrc,
RejectedLength,
};
struct IrFoxPacketDecision
{
IrFoxPacketOutcome outcome = IrFoxPacketOutcome::RejectedLength;
uint8_t message_type = 0;
uint8_t minimum_size = 0;
uint16_t destination = 0;
bool has_destination = false;
bool raw_accepted() const
{
return outcome == IrFoxPacketOutcome::Accepted || outcome == IrFoxPacketOutcome::RawOnlyUnknownType ||
outcome == IrFoxPacketOutcome::RawOnlyTypedLength || outcome == IrFoxPacketOutcome::IgnoredAddress;
}
};
namespace irfox {
constexpr uint8_t kMsgBack = 0U;
constexpr uint8_t kMsgAccept = 1U;
constexpr uint8_t kMsgRequest = 2U;
constexpr uint8_t kMsgBackTo = 4U;
constexpr uint8_t kMsgDataNoAccept = 6U;
constexpr uint8_t kMsgDataAccept = 7U;
constexpr uint16_t kBroadcastAddress = 65000U;
inline uint8_t messageType(uint8_t header)
{
return static_cast<uint8_t>((header >> 5U) & 0x07U);
}
inline uint8_t minimumPacketSize(uint8_t message_type)
{
switch (message_type)
{
case kMsgDataAccept:
case kMsgDataNoAccept:
case kMsgBackTo:
case kMsgRequest:
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + kAddrBytes + kCrcBytes);
case kMsgBack:
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + kCrcBytes);
case kMsgAccept:
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + 1U + kCrcBytes);
default:
return 0;
}
}
inline bool addressAcceptedByReceiver(uint16_t destination, uint16_t receiver_address)
{
return receiver_address == 0U || destination == receiver_address || destination >= kBroadcastAddress;
}
inline IrFoxPacketDecision classifyPacket(const uint8_t* data, uint8_t observed_size, bool crc_ok,
uint16_t receiver_address)
{
IrFoxPacketDecision result;
if (data == nullptr)
{
result.outcome = IrFoxPacketOutcome::RejectedLength;
return result;
}
result.message_type = messageType(data[0]);
if (observed_size < kMsgBytes + kCrcBytes)
{
result.outcome = IrFoxPacketOutcome::RejectedLength;
return result;
}
if ((data[0] & 0x1FU) != observed_size)
{
result.outcome = IrFoxPacketOutcome::RejectedLength;
return result;
}
if (!crc_ok)
{
result.outcome = IrFoxPacketOutcome::RejectedCrc;
return result;
}
result.minimum_size = minimumPacketSize(result.message_type);
if (result.minimum_size == 0U)
{
result.outcome = IrFoxPacketOutcome::RawOnlyUnknownType;
return result;
}
if (observed_size < result.minimum_size)
{
result.outcome = IrFoxPacketOutcome::RawOnlyTypedLength;
return result;
}
const bool addressed = result.message_type == kMsgDataAccept || result.message_type == kMsgDataNoAccept ||
result.message_type == kMsgBackTo || result.message_type == kMsgRequest;
if (addressed)
{
result.has_destination = true;
result.destination = static_cast<uint16_t>((static_cast<uint16_t>(data[3]) << 8U) | data[4]);
if (!addressAcceptedByReceiver(result.destination, receiver_address))
{
result.outcome = IrFoxPacketOutcome::IgnoredAddress;
return result;
}
}
result.outcome = IrFoxPacketOutcome::Accepted;
return result;
}
inline const char* packetOutcomeText(IrFoxPacketOutcome outcome)
{
switch (outcome)
{
case IrFoxPacketOutcome::Accepted:
return "ACCEPT";
case IrFoxPacketOutcome::RawOnlyUnknownType:
return "RAW TYPE";
case IrFoxPacketOutcome::RawOnlyTypedLength:
return "RAW SIZE";
case IrFoxPacketOutcome::IgnoredAddress:
return "IGNORE ADDR";
case IrFoxPacketOutcome::RejectedCrc:
return "REJECT CRC";
case IrFoxPacketOutcome::RejectedLength:
return "REJECT LEN";
}
return "REJECT";
}
inline const char* messageTypeText(uint8_t message_type)
{
switch (message_type)
{
case kMsgBack:
return "BACK";
case kMsgAccept:
return "ACCEPT";
case kMsgRequest:
return "REQUEST";
case kMsgBackTo:
return "BACK_TO";
case kMsgDataNoAccept:
return "DATA";
case kMsgDataAccept:
return "DATA_ACK";
default:
return "UNKNOWN";
}
}
} // namespace irfox

View File

@ -11,12 +11,8 @@ constexpr uint32_t kBitTakts = kBitActiveTakts + kBitPauseTakts;
constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs; constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs;
constexpr uint32_t kToleranceUs = 300U; constexpr uint32_t kToleranceUs = 300U;
/** /** Мин. длительность плато (мкс) для потокового анти-глитча в анализаторе; согласовано с IR_INPUT_MIN_PULSE_US. */
* Must match IR_INPUT_MIN_PULSE_US in the firmware configuration. The current constexpr uint32_t kMinFilteredPulseUs = 10U;
* receiver configuration keeps this filter disabled, so a capture must not
* silently lose short edges that the receiver would see.
*/
constexpr uint32_t kMinFilteredPulseUs = 0U;
constexpr uint8_t kBitPerByte = 8U; constexpr uint8_t kBitPerByte = 8U;
constexpr uint8_t kMsgBytes = 1; constexpr uint8_t kMsgBytes = 1;
@ -31,12 +27,6 @@ constexpr uint8_t kDataByteSizeMax =
constexpr uint8_t kPreambPulse = 3; constexpr uint8_t kPreambPulse = 3;
constexpr uint8_t kPreambFronts = kPreambPulse * 2U; constexpr uint8_t kPreambFronts = kPreambPulse * 2U;
constexpr uint8_t kPreambleLockRisePeriods = 2U;
constexpr uint8_t kPreambleJitterPct = 18U;
constexpr uint32_t kPreambleJitterUsMin = 80U;
constexpr uint32_t kPreamblePeriodMinFactorPct = 220U;
constexpr uint32_t kPreamblePeriodMaxFactorPct = 340U;
constexpr uint32_t kPreambleCandidateTimeoutMult = 1U;
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */ /** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT #ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
@ -63,19 +53,6 @@ inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs)
return lo < periodUs && periodUs < hi; return lo < periodUs && periodUs < hi;
} }
inline uint32_t preambleJitterTolUs(uint32_t baselineUs)
{
const uint32_t pct = (baselineUs * kPreambleJitterPct) / 100U;
return pct > kPreambleJitterUsMin ? pct : kPreambleJitterUsMin;
}
inline bool preambleRisePeriodCoarseOk(uint32_t periodUs)
{
const uint32_t min_period = (kBitTimeUs * kPreamblePeriodMinFactorPct) / 100U;
const uint32_t max_period = (kBitTimeUs * kPreamblePeriodMaxFactorPct) / 100U;
return periodUs >= min_period && periodUs <= max_period;
}
inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us) inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us)
{ {
#if IRFOX_GLITCH_REJECT_PHASE_NUDGE #if IRFOX_GLITCH_REJECT_PHASE_NUDGE

View File

@ -1,282 +0,0 @@
#include "IrFoxDecoder.h"
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <vector>
#define CHECK(expression) \
do \
{ \
if (!(expression)) \
{ \
std::fprintf(stderr, "CHECK failed: %s (%s:%d)\n", #expression, __FILE__, __LINE__); \
std::exit(EXIT_FAILURE); \
} \
} while (false)
namespace {
uint8_t crc8(const uint8_t* data, uint8_t end, uint8_t poly)
{
uint8_t crc = 0xFF;
for (uint8_t i = 0; i < end; ++i)
{
crc ^= data[i];
for (uint8_t bit = 0; bit < 8; ++bit)
crc = (crc & 0x80U) ? static_cast<uint8_t>((crc << 1U) ^ poly) : static_cast<uint8_t>(crc << 1U);
}
return crc;
}
struct DecoderHarness
{
IrFoxDecoder decoder;
std::vector<IrFoxEmitPacket> packets;
std::vector<IrFoxEmitBit> events;
std::vector<IrFoxEmitTerminal> terminals;
uint64_t phase = 0;
bool collect_bit_events = true;
static constexpr uint32_t kFs = 1000000U;
explicit DecoderHarness(bool collect_bits = true) : collect_bit_events(collect_bits)
{
decoder.reset();
}
void edge(uint64_t sample, bool rising)
{
IrFoxOnBit on_bit;
if (collect_bit_events)
on_bit = [this](const IrFoxEmitBit& event) { events.push_back(event); };
decoder.processEdge(sample, rising, kFs, on_bit,
[this](const IrFoxEmitPacket& packet) { packets.push_back(packet); },
[this](const IrFoxEmitTerminal& terminal) { terminals.push_back(terminal); });
}
void lockPreamble()
{
lockPreambleAt(40000U);
}
void lockPreambleAt(uint64_t first_rise)
{
constexpr uint64_t period = irfox::kBitTimeUs * 3U;
edge(first_rise, true);
edge(first_rise + 700U, false);
edge(first_rise + period, true);
edge(first_rise + period + 700U, false);
edge(first_rise + period * 2U, true);
phase = first_rise + period * 2U + period / 2U;
}
void emitCell(bool bit)
{
// The decoder calls a cell a one when the inactive (HIGH) interval is
// longer than the active (LOW) interval. The waveform is TSOP output.
const uint64_t high_us = bit ? 262U : 700U;
edge(phase + high_us, false);
phase += irfox::kBitTimeUs;
edge(phase, true);
}
void emitByte(uint8_t value, bool emit_sync)
{
for (uint8_t i = 0; i < 8; ++i)
emitCell((value & static_cast<uint8_t>(0x80U >> i)) != 0U);
if (emit_sync)
{
const bool sync = (value & 1U) == 0U;
for (uint8_t i = 0; i < irfox::kSyncBits; ++i)
emitCell(sync);
}
}
void emitPacket(const std::vector<uint8_t>& packet)
{
for (size_t i = 0; i < packet.size(); ++i)
emitByte(packet[i], i + 1U != packet.size());
}
void flushAt(uint64_t sample)
{
IrFoxOnBit on_bit;
if (collect_bit_events)
on_bit = [this](const IrFoxEmitBit& event) { events.push_back(event); };
decoder.flushEnd(sample, kFs, on_bit,
[this](const IrFoxEmitPacket& packet) { packets.push_back(packet); },
[this](const IrFoxEmitTerminal& terminal) { terminals.push_back(terminal); });
}
};
std::vector<uint8_t> makeValidPacket()
{
std::vector<uint8_t> packet{0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
packet[5] = crc8(packet.data(), 5, irfox::kPoly1);
packet[6] = crc8(packet.data(), 6, irfox::kPoly2);
return packet;
}
void assertBadLengthAbortsAndRecovers(uint8_t declared_size)
{
DecoderHarness harness;
harness.lockPreamble();
const uint8_t header = static_cast<uint8_t>(0xE0U | declared_size);
harness.emitByte(header, false);
CHECK(harness.packets.empty());
CHECK(harness.terminals.size() == 1U);
CHECK(harness.terminals[0].reason == IrFoxTerminalReason::Abort);
CHECK(harness.terminals[0].cause == IrFoxAbortCause::BadLength);
CHECK(harness.terminals[0].message_type == 7U);
CHECK(harness.terminals[0].declared_size == declared_size);
CHECK(harness.terminals[0].received_bits == irfox::kBitPerByte);
// Firmware abortFrame() releases the 30.288 ms preamble guard. A receiver
// that merely sets is_wrong_pack will miss this complete nearby frame.
const uint64_t next_preamble = harness.phase + 5000U;
harness.lockPreambleAt(next_preamble);
harness.emitPacket(makeValidPacket());
CHECK(harness.packets.size() == 1U);
CHECK(harness.packets[0].crc_ok);
CHECK(harness.packets[0].start_sample == static_cast<int64_t>(next_preamble));
CHECK(harness.terminals.size() == 1U);
}
} // namespace
int main()
{
const std::vector<uint8_t> packet = makeValidPacket();
DecoderHarness valid;
valid.lockPreamble();
valid.emitPacket(packet);
CHECK(valid.packets.size() == 1U);
CHECK(valid.packets[0].crc_ok);
CHECK(valid.packets[0].pack_size == packet.size());
CHECK(valid.packets[0].start_sample == 40000);
CHECK(valid.packets[0].start_sample < valid.packets[0].end_sample);
bool saw_preamble = false;
std::vector<uint8_t> decoded_bytes;
for (const IrFoxEmitBit& event : valid.events)
{
if (event.frame_type == IRF_FT_PREAMBLE)
{
saw_preamble = true;
CHECK(event.start_sample == 40000);
}
if (event.frame_type == IRF_FT_DATA_BYTE)
decoded_bytes.push_back(static_cast<uint8_t>(event.bit_value));
}
CHECK(saw_preamble);
CHECK(decoded_bytes.size() == packet.size());
for (size_t i = 0; i < packet.size(); ++i)
CHECK(decoded_bytes[i] == packet[i]);
for (uint8_t declared_size = 0; declared_size < irfox::kMsgBytes + irfox::kCrcBytes; ++declared_size)
assertBadLengthAbortsAndRecovers(declared_size);
// Overview supplies no per-bit callback. Terminal reporting must not depend
// on Detailed-mode bit/event generation.
DecoderHarness bad_sync(false);
bad_sync.lockPreamble();
bad_sync.emitByte(0xE7, false);
// Header 0xE7 ends in one, while the first sync bit must be inverted.
bad_sync.emitCell(true);
CHECK(bad_sync.terminals.empty());
bad_sync.emitCell(true);
CHECK(bad_sync.terminals.empty());
bad_sync.emitCell(true);
CHECK(bad_sync.events.empty());
CHECK(bad_sync.terminals.size() == 1U);
CHECK(bad_sync.terminals[0].reason == IrFoxTerminalReason::Abort);
CHECK(bad_sync.terminals[0].cause == IrFoxAbortCause::BadSync);
CHECK(bad_sync.terminals[0].message_type == 7U);
CHECK(bad_sync.terminals[0].declared_size == 7U);
CHECK(bad_sync.terminals[0].received_bits == irfox::kBitPerByte);
const uint64_t after_sync_abort = bad_sync.phase + 5000U;
bad_sync.lockPreambleAt(after_sync_abort);
bad_sync.emitPacket(packet);
CHECK(bad_sync.packets.size() == 1U);
CHECK(bad_sync.packets[0].crc_ok);
CHECK(bad_sync.packets[0].start_sample == static_cast<int64_t>(after_sync_abort));
CHECK(bad_sync.terminals.size() == 1U);
DecoderHarness stale_candidate;
constexpr uint64_t stale_rise = 40000U;
const uint64_t candidate_gap =
irfox::irTimeoutUs(irfox::kBitTimeUs) + 1U; // New 1x timeout, still below the old 3x timeout.
const uint64_t fresh_preamble = stale_rise + candidate_gap;
stale_candidate.edge(stale_rise, true);
stale_candidate.lockPreambleAt(fresh_preamble);
stale_candidate.emitPacket(packet);
CHECK(stale_candidate.packets.size() == 1U);
CHECK(stale_candidate.packets[0].crc_ok);
CHECK(stale_candidate.packets[0].start_sample == static_cast<int64_t>(fresh_preamble));
CHECK(stale_candidate.terminals.empty());
// If reception times out after PRE lock but before the first data bit, the
// Detailed terminal span must begin immediately after the separate PRE frame.
DecoderHarness pre_only;
pre_only.lockPreamble();
const IrFoxEmitBit* pre_event = nullptr;
for (const IrFoxEmitBit& event : pre_only.events)
{
if (event.frame_type == IRF_FT_PREAMBLE)
pre_event = &event;
}
CHECK(pre_event != nullptr);
constexpr uint64_t preamble_period = irfox::kBitTimeUs * 3U;
const uint64_t pre_lock_edge = 40000U + preamble_period * 2U;
const uint64_t abort_silence = 2U * irfox::irTimeoutUs(irfox::kBitTimeUs);
pre_only.flushAt(pre_lock_edge + abort_silence + 1U);
CHECK(pre_only.terminals.size() == 1U);
CHECK(pre_only.terminals[0].reason == IrFoxTerminalReason::Timeout);
CHECK(pre_only.terminals[0].received_bits == 0U);
CHECK(pre_only.terminals[0].detail_start_sample == pre_event->end_sample + 1);
CHECK(pre_only.terminals[0].end_sample == static_cast<int64_t>(pre_lock_edge + abort_silence));
DecoderHarness truncated;
truncated.lockPreamble();
truncated.emitByte(0xE7, true);
CHECK(truncated.packets.empty());
truncated.flushAt(truncated.phase + abort_silence);
CHECK(truncated.terminals.empty());
truncated.flushAt(truncated.phase + abort_silence + 1U);
CHECK(truncated.packets.empty());
CHECK(truncated.terminals.size() == 1U);
CHECK(truncated.terminals[0].reason == IrFoxTerminalReason::Timeout);
CHECK(truncated.terminals[0].cause == IrFoxAbortCause::None);
CHECK(truncated.terminals[0].message_type == 7U);
CHECK(truncated.terminals[0].declared_size == 7U);
CHECK(truncated.terminals[0].received_bits == irfox::kBitPerByte);
CHECK(truncated.terminals[0].end_sample == static_cast<int64_t>(truncated.phase + abort_silence));
truncated.flushAt(truncated.phase + abort_silence + 100U);
CHECK(truncated.terminals.size() == 1U);
const uint64_t after_timeout = truncated.phase + abort_silence + 5000U;
truncated.lockPreambleAt(after_timeout);
truncated.emitPacket(packet);
CHECK(truncated.packets.size() == 1U);
CHECK(truncated.packets[0].crc_ok);
CHECK(truncated.packets[0].start_sample == static_cast<int64_t>(after_timeout));
CHECK(truncated.terminals.size() == 1U);
// With no flush/tick between frames, the first rise strictly beyond 2T both
// closes the old frame and opens the new preamble. Inclusive spans must not
// share that sample.
DecoderHarness adjacent_timeout;
adjacent_timeout.lockPreamble();
adjacent_timeout.emitByte(0xE7, true);
const uint64_t adjacent_preamble = adjacent_timeout.phase + abort_silence + 1U;
adjacent_timeout.lockPreambleAt(adjacent_preamble);
adjacent_timeout.emitPacket(packet);
CHECK(adjacent_timeout.terminals.size() == 1U);
CHECK(adjacent_timeout.terminals[0].reason == IrFoxTerminalReason::Timeout);
CHECK(adjacent_timeout.packets.size() == 1U);
CHECK(adjacent_timeout.packets[0].crc_ok);
CHECK(adjacent_timeout.terminals[0].end_sample < adjacent_timeout.packets[0].start_sample);
CHECK(adjacent_timeout.packets[0].start_sample == static_cast<int64_t>(adjacent_preamble));
return 0;
}

View File

@ -1,43 +0,0 @@
#include "IrFoxPacketClassifier.h"
#include <cassert>
#include <cstdint>
static IrFoxPacketDecision classify(const uint8_t* data, uint8_t size, bool crc_ok, uint16_t id = 0)
{
return irfox::classifyPacket(data, size, crc_ok, id);
}
int main()
{
const uint8_t data_to_42[] = {0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
assert(classify(data_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
assert(classify(data_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
const uint8_t broadcast_data[] = {0xC7, 0x00, 0x01, 0xFD, 0xE8, 0x00, 0x00};
assert(classify(broadcast_data, 7, true, 41).outcome == IrFoxPacketOutcome::Accepted);
const uint8_t request_to_42[] = {0x47, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
assert(classify(request_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
assert(classify(request_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
const uint8_t back_to_42[] = {0x87, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
assert(classify(back_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
assert(classify(back_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
const uint8_t back[] = {0x05, 0x00, 0x01, 0x00, 0x00};
assert(classify(back, 5, true, 41).outcome == IrFoxPacketOutcome::Accepted);
const uint8_t accept[] = {0x26, 0x00, 0x01, 0x55, 0x00, 0x00};
assert(classify(accept, 6, true, 41).outcome == IrFoxPacketOutcome::Accepted);
const uint8_t unknown[] = {0x63, 0x00, 0x00};
assert(classify(unknown, 3, true).outcome == IrFoxPacketOutcome::RawOnlyUnknownType);
const uint8_t short_data[] = {0xE5, 0x00, 0x01, 0x00, 0x2A};
assert(classify(short_data, 5, true).outcome == IrFoxPacketOutcome::RawOnlyTypedLength);
assert(classify(data_to_42, 7, false).outcome == IrFoxPacketOutcome::RejectedCrc);
const uint8_t too_short[] = {0xE2, 0x00};
const IrFoxPacketDecision short_decision = classify(too_short, 2, false);
assert(short_decision.outcome == IrFoxPacketOutcome::RejectedLength);
assert(short_decision.message_type == irfox::kMsgDataAccept);
return 0;
}

View File

@ -374,15 +374,45 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
return busy; return busy;
} }
bool IR_DecoderRaw::rxPipelineActive() const
{
return rxLineActive() || rxTimeoutPipelineBusy();
}
uint8_t IR_DecoderRaw::currentRxMsgType() const
{
if (i_dataBuffer < static_cast<uint16_t>(msgBytes) * bitPerByte)
return 0xFFU;
return static_cast<uint8_t>((dataBuffer[0] >> 5U) & IR_MASK_MSG_TYPE);
}
void IR_DecoderRaw::noteRxTerminal(IR_RxTerminalReason reason, uint8_t msgType, bool hadLock)
{
++rxTerminalInfo.seq;
rxTerminalInfo.reason = reason;
rxTerminalInfo.msgType = msgType;
rxTerminalInfo.hadLock = hadLock;
}
void IR_DecoderRaw::listenStart() void IR_DecoderRaw::listenStart()
{ {
if (rxTimeoutPipelineBusy()) if (rxTimeoutPipelineBusy())
return; return;
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U)) const uint32_t nowUs = micros();
if (isReciveRaw && ((nowUs - lastEdgeTime) > IR_timeout * 2U))
{ {
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(true); packTraceOnTimeoutOrAbort(true);
#endif #endif
if (isRecive)
{
const uint16_t expected =
(i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, nowUs);
noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
isRecive = false;
msgTypeReceive = 0;
}
isReciveRaw = false; isReciveRaw = false;
firstRX(); firstRX();
} }
@ -403,7 +433,7 @@ inline void IR_DecoderRaw::checkTimeout()
#endif #endif
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U; const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros()); rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
noteRxEnd(RxEndReason::Timeout, micros()); noteRxTerminal(IR_RxTerminalReason::LockedTimeout, currentRxMsgType(), true);
isRecive = false; // приём завершён isRecive = false; // приём завершён
msgTypeReceive = 0; msgTypeReceive = 0;
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся // Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
@ -418,62 +448,6 @@ inline void IR_DecoderRaw::checkTimeout()
} }
// ==================================================================== // ====================================================================
void IR_DecoderRaw::noteRxEnd(RxEndReason reason, uint32_t tUs)
{
rxEnd.seq++;
rxEnd.reason = reason;
rxEnd.msgType = (i_dataBuffer >= 8U * msgBytes) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF;
rxEnd.packSize = (uint8_t)packSize;
rxEnd.tUs = tUs;
rxEnd.expectedEndUs = (packSize >= msgBytes + crcBytes)
? rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize) + irTicksToUs((uint32_t)syncBits * irBitTicks)
: 0U;
}
void IR_DecoderRaw::abortFrame(uint32_t tUs)
{
#if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(false);
#endif
noteRxEnd(RxEndReason::Abort, tUs);
isRecive = false;
isReciveRaw = false;
msgTypeReceive = 0;
firstRX();
releasePreambleGuard(tUs);
}
// После обрыва кадра «длинная тишина» (IR_timeout × 2 ≈ 30 мс) перед новым кандидатом преамбулы не требуется.
// prevRise — последний ДЕКОДИРОВАННЫЙ фронт; после abort он свежий, а фронты, отброшенные гвардом, его не двигают,
// поэтому валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика
// (стенд 09.09: КУ теряла пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга).
// Ложных захватов это не добавляет: хвост оборванного кадра (период фронтов 962 мкс, синхробиты ~1100) не проходит
// грубый фильтр периода преамбулы (2116…3270 мкс), а настоящая преамбула перезапускает кандидата по паузе > IR_timeout.
// После чистого конца кадра гвард остаётся: там он отсекает хвост синхробитов.
void IR_DecoderRaw::releasePreambleGuard(uint32_t tUs)
{
prevRise = tUs - IR_timeout * 2U - 1U; // «тишина уже была»: (front.time - prevRise) > longSilence для следующего фронта
}
void IR_DecoderRaw::expirePreambleCandidate()
{
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
return;
if ((micros() - preambleCandidateLastEdgeTime) > IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT)
{
if (preambleGoodPeriods)
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, micros());
preambleResetToIdle();
}
}
uint32_t IR_DecoderRaw::rxExpectedEndUs() const
{
if (!isRecive || preambleState != PreambleState::Locked || isWrongPack || packSize < msgBytes + crcBytes)
return 0;
return rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize);
}
void IR_DecoderRaw::tick() void IR_DecoderRaw::tick()
{ {
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
@ -528,16 +502,17 @@ void IR_DecoderRaw::tick()
if (!processedFront) if (!processedFront)
{ {
isSubBufferOverflow = false; isSubBufferOverflow = false;
checkTimeout();
listenStart(); listenStart();
expirePreambleCandidate(); checkTimeout();
expirePreambleCandidateIfIdle(micros());
#if defined(IR_EDGE_TRACE) #if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {} while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif #endif
return; return;
} // Если данных нет - ничего не делаем } // Если данных нет - ничего не делаем
checkTimeout();
listenStart(); listenStart();
checkTimeout();
expirePreambleCandidateIfIdle(micros());
#if IR_RX_BRIEF_LOG #if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs(); rxBriefFlushDeferredIsrLogs();
#endif #endif
@ -836,9 +811,19 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
} }
if (isBufferOverflow || isPreamb || isWrongPack) if (isBufferOverflow || isPreamb || isWrongPack)
{ {
const bool hadLock =
isRecive || isReciveRaw || preambleState == PreambleState::Locked;
const bool wasObservable =
hadLock ||
(preambleState == PreambleState::Candidate && preambleWasObservable);
if (wasObservable)
noteRxTerminal(IR_RxTerminalReason::DecodeAbort, currentRxMsgType(), hadLock);
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и // Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered. // pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
abortFrame(micros()); isRecive = false;
isReciveRaw = false;
msgTypeReceive = 0;
firstRX();
return; return;
} }
@ -906,8 +891,6 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit")); packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
#endif #endif
abortFrame(micros()); // битый кадр не удерживает приёмник до таймаута
return;
} }
} }
} }
@ -935,12 +918,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё. // B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38). // Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно. // packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
if (packSize < msgBytes + crcBytes) // 0..2: кадр физически не несёт CRC — шум/битьё if (packSize != 0 && packSize < msgBytes + crcBytes)
{
isWrongPack = true; isWrongPack = true;
abortFrame(micros());
return;
}
} }
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец». // Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
@ -964,7 +943,6 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
preambleResetToIdle(); preambleResetToIdle();
msgTypeReceive = 0; msgTypeReceive = 0;
isAvailable = crcCheck(packSize - crcBytes, crcValue); isAvailable = crcCheck(packSize - crcBytes, crcValue);
noteRxEnd(isAvailable ? RxEndReason::Ok : RxEndReason::Crc, micros());
#ifdef BRUTEFORCE_CHECK #ifdef BRUTEFORCE_CHECK
{ {
@ -1003,6 +981,9 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#endif #endif
} }
#endif #endif
noteRxTerminal(isAvailable ? IR_RxTerminalReason::FrameOk
: IR_RxTerminalReason::FrameCrcError,
currentRxMsgType(), true);
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
if (isAvailable) if (isAvailable)
packTraceEmitEndOk(static_cast<uint8_t>(packSize)); packTraceEmitEndOk(static_cast<uint8_t>(packSize));
@ -1645,6 +1626,7 @@ void IR_DecoderRaw::preambleResetToIdle()
{ {
preambleState = PreambleState::Idle; preambleState = PreambleState::Idle;
preambleGoodPeriods = 0; preambleGoodPeriods = 0;
preambleWasObservable = false;
preambleMeanPeriod = 0; preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = 0; preambleCandidateLastEdgeTime = 0;
preambleCandidateFirstRiseTime = 0; preambleCandidateFirstRiseTime = 0;
@ -1659,6 +1641,10 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
{ {
preambleState = PreambleState::Candidate; preambleState = PreambleState::Candidate;
preambleGoodPeriods = 0; preambleGoodPeriods = 0;
// The first post-silence rise already opens a potential frame epoch.
// Keep the line busy until that epoch locks or expires after real silence:
// even a badly distorted response may contain no coarse-valid rise period.
preambleWasObservable = true;
preambleMeanPeriod = 0; preambleMeanPeriod = 0;
preambleCandidateLastEdgeTime = front.time; preambleCandidateLastEdgeTime = front.time;
preambleCandidateFirstRiseTime = front.time; preambleCandidateFirstRiseTime = front.time;
@ -1669,6 +1655,24 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
isReciveRaw = false; isReciveRaw = false;
} }
void IR_DecoderRaw::expirePreambleCandidateIfIdle(uint32_t nowUs)
{
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
return;
const uint32_t candTimeout =
IR_timeout * static_cast<uint32_t>(IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT);
if ((uint32_t)(nowUs - preambleCandidateLastEdgeTime) <= candTimeout)
return;
const uint8_t goodPeriods = preambleGoodPeriods;
const bool wasObservable = preambleWasObservable;
rxBriefLog(RxBriefReason::Preamble, goodPeriods, 0, nowUs);
preambleResetToIdle();
if (wasObservable)
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
}
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front) bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{ {
const uint32_t longSilence = IR_timeout * 2U; const uint32_t longSilence = IR_timeout * 2U;
@ -1695,7 +1699,10 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout) if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
{ {
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time); rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
if (preambleWasObservable)
noteRxTerminal(IR_RxTerminalReason::CandidateTimeout, 0xFFU, false);
preambleStartCandidate(front); preambleStartCandidate(front);
return true;
} }
preambleCandidateLastEdgeTime = front.time; preambleCandidateLastEdgeTime = front.time;
@ -1713,15 +1720,20 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
preambleCandidateFirstRiseTime = front.time; preambleCandidateFirstRiseTime = front.time;
if (!preambleRisePeriodCoarseOk(period)) if (!preambleRisePeriodCoarseOk(period))
{ {
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods,
irClampU16(period), front.time);
preambleGoodPeriods = 0; preambleGoodPeriods = 0;
preambleMeanPeriod = 0; preambleMeanPeriod = 0;
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time); // Keep preambleWasObservable sticky: this edge proves the medium is
// still active, but not that a possible physical frame has ended.
// Only silence timeout or a real locked terminal releases it.
return true; return true;
} }
if (preambleGoodPeriods == 0) if (preambleGoodPeriods == 0)
{ {
preambleGoodPeriods = 1; preambleGoodPeriods = 1;
preambleWasObservable = true;
preambleMeanPeriod = (uint16_t)period; preambleMeanPeriod = (uint16_t)period;
} }
else else
@ -1738,6 +1750,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{ {
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time); rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
preambleGoodPeriods = 1; preambleGoodPeriods = 1;
preambleWasObservable = true;
preambleMeanPeriod = (uint16_t)period; preambleMeanPeriod = (uint16_t)period;
} }
} }
@ -1768,8 +1781,6 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
isRecive = true; isRecive = true;
isReciveRaw = true; isReciveRaw = true;
risePeriod = preambleMeanPeriod; risePeriod = preambleMeanPeriod;
rxLockSeqCnt++;
rxLockTimeUsVal = front.time;
#if defined(IRDEBUG_SERIAL_PACK) #if defined(IRDEBUG_SERIAL_PACK)
packTraceResetFrame(); packTraceResetFrame();
packTraceOpen = true; packTraceOpen = true;

View File

@ -25,7 +25,30 @@ class Print;
#define riseTimeMin (riseTime - riseTolerance) #define riseTimeMin (riseTime - riseTolerance)
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax) #define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1 #define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000; constexpr uint16_t IR_ResponseDelay = irproto::kMandatoryInterPacketQuietMs;
/** Why the most recent observable receive attempt reached a terminal state. */
enum class IR_RxTerminalReason : uint8_t
{
None = 0,
FrameOk,
FrameCrcError,
LockedTimeout,
DecodeAbort,
CandidateTimeout
};
/**
* Monotonic receive-completion snapshot for schedulers polling after decoder.tick().
* seq is allowed to wrap; consumers only compare it with their previous snapshot.
*/
struct IR_RxTerminalInfo
{
uint32_t seq = 0;
IR_RxTerminalReason reason = IR_RxTerminalReason::None;
uint8_t msgType = 0xFFU;
bool hadLock = false;
};
class IR_Encoder; class IR_Encoder;
class IR_DecoderRaw : virtual public IR_FOX class IR_DecoderRaw : virtual public IR_FOX
@ -52,14 +75,28 @@ public:
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
bool isSubOverflow(); bool isSubOverflow();
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется // Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ открыт
// ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного // Candidate после первого post-silence rise. Даже сильно искажённый ответ может не дать ни
// шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего: // одного coarse-valid периода, поэтому Candidate остаётся активным до lock/terminal либо
// «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const. // доказанной тишины по candidate timeout. Для гейта заднего: «не стрелять, пока на линии
// идёт/формируется потенциальный кадр (напр. ответ точки)». Аддитивно, const.
inline bool rxLineActive() const { inline bool rxLineActive() const {
return isRecive || return isRecive ||
(preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U); (preambleState == PreambleState::Candidate && preambleWasObservable);
} }
/**
* True while a real frame is active or ISR/filter work is still queued.
* This closes the one-loop ordering gap when Timer::tick() runs before
* decoder.tick(): a transmitter must not start while an unprocessed edge
* is already waiting in the receive pipeline.
*/
bool rxPipelineActive() const;
/**
* Last terminal RX transition. Updated from tick()/decode context, never from ISR.
* A frame that starts and finishes within one tick is observable through seq.
*/
IR_RxTerminalInfo rxLastTerminal() const { return rxTerminalInfo; }
uint32_t rxTerminalSeq() const { return rxTerminalInfo.seq; }
// Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден; // Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден;
// иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0 // иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра). // или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
@ -67,25 +104,9 @@ public:
return (isRecive && packSize && !isWrongPack) ? packSize : 0; return (isRecive && packSize && !isWrongPack) ? packSize : 0;
} }
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего. // Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; } static constexpr uint16_t rxMaxPackSize() {
return static_cast<uint16_t>(irproto::kMaxWireFrameBytes);
// ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ---- }
enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort };
struct RxEndInfo {
uint16_t seq = 0; // номер завершения (растёт на каждом терминале)
RxEndReason reason = RxEndReason::None;
uint8_t msgType = 0xFF; // 0xFF = первый байт не был принят
uint8_t packSize = 0; // объявленная длина (0 = неизвестна)
uint32_t tUs = 0; // micros() терминала
uint32_t expectedEndUs = 0; // расчётный конец кадра в эфире (0 = неизвестен)
};
uint16_t rxLockSeq() const { return rxLockSeqCnt; } // ++ в момент лока преамбулы
uint32_t rxLockTimeUs() const { return rxLockTimeUsVal; } // метка фронта лока (ISR-время)
/// Тип принимаемого кадра (3 бита) после первого байта; 0xFF пока неизвестен / приём не идёт.
uint8_t rxMsgType() const { return (isRecive && packSize) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; }
const RxEndInfo &rxLastEnd() const { return rxEnd; }
/// Расчётный момент последнего бита данных текущего кадра (по объявленной длине); 0 = не Locked / длина неизвестна.
uint32_t rxExpectedEndUs() const;
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; } uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; } uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; } uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
@ -145,6 +166,7 @@ private:
volatile bool isSubBufferOverflow = false; volatile bool isSubBufferOverflow = false;
bool isBufferOverflow = false; // Флаг переполнения буффера данных bool isBufferOverflow = false; // Флаг переполнения буффера данных
bool isWrongPack = false; // Флаг битого пакета bool isWrongPack = false; // Флаг битого пакета
IR_RxTerminalInfo rxTerminalInfo;
uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс uint16_t riseSyncTime = bitTime; // Подстраиваемое время бита в мкс
@ -181,14 +203,11 @@ private:
Locked = 2 Locked = 2
}; };
PreambleState preambleState = PreambleState::Idle; PreambleState preambleState = PreambleState::Idle;
uint16_t rxLockSeqCnt = 0;
uint32_t rxLockTimeUsVal = 0;
RxEndInfo rxEnd;
void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец
void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow)
void releasePreambleGuard(uint32_t tUs); // после abort: новый кандидат преамбулы без ожидания длинной тишины
void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle
uint8_t preambleGoodPeriods = 0; uint8_t preambleGoodPeriods = 0;
// Sticky potential-frame latch for one Candidate epoch. After one plausible
// rise period, coarse-invalid activity remains busy until proven silence;
// the scheduler's hard deadline may skip an optional tail under noise.
bool preambleWasObservable = false;
uint16_t preambleMeanPeriod = 0; uint16_t preambleMeanPeriod = 0;
uint32_t preambleCandidateLastEdgeTime = 0; uint32_t preambleCandidateLastEdgeTime = 0;
uint32_t preambleCandidateFirstRiseTime = 0; uint32_t preambleCandidateFirstRiseTime = 0;
@ -257,6 +276,9 @@ bool isReciveRaw = false;
void preambleResetToIdle(); void preambleResetToIdle();
void preambleStartCandidate(const FrontStorage &front); void preambleStartCandidate(const FrontStorage &front);
bool preambleProcessEdge(const FrontStorage &front); bool preambleProcessEdge(const FrontStorage &front);
void expirePreambleCandidateIfIdle(uint32_t nowUs);
uint8_t currentRxMsgType() const;
void noteRxTerminal(IR_RxTerminalReason reason, uint8_t msgType, bool hadLock);
/// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете /// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
/// @param len Длина в байтах проверяемых данных /// @param len Длина в байтах проверяемых данных

View File

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

View File

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

View File

@ -231,11 +231,9 @@ typedef uint16_t crc_t;
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT #ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U #define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
#endif #endif
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. Кандидат без фронтов дольше таймаута байта /** Таймаут окна кандидата преамбулы: IR_timeout * mult. */
преамбулой быть не может; при 3× линия считалась занятой (rxLineActive) ещё 45 мс после последнего
паразитного фронта (напр. засветка своим дальномером) и откладывала передачу. */
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT #ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 1U #define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U
#endif #endif
#define preambPulse 3 #define preambPulse 3
@ -266,31 +264,193 @@ typedef uint16_t crc_t;
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита #define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
#define tolerance 300U #define tolerance 300U
// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ---- namespace irproto
// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по {
// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта. /** Maximum complete frame length representable by the five header bits. */
constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U); constexpr uint8_t kMaxWireFrameBytes = static_cast<uint8_t>(IR_MASK_MSG_INFO);
constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U);
constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U); constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U; constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks; constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); } constexpr uint8_t kAcceptFrameBytes = msgBytes + addrBytes + 1U + crcBytes;
/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс. constexpr uint8_t kRequestFrameBytes = msgBytes + addrBytes + addrBytes + crcBytes;
constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); }
/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс. constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); } constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс. constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс. /** Complete DATA frame size, or zero when payloadBytes cannot fit on wire. */
constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks); constexpr uint8_t dataWireBytes(uint8_t payloadBytes)
/// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём. {
constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U); return payloadBytes <= kMaxDataPayloadBytes
constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs; ? static_cast<uint8_t>(kDataFrameOverheadBytes + payloadBytes)
/// Протокольный максимум длины кадра (5-битное поле длины). : 0U;
constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO; }
/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from).
constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); } /** Complete non-addressed BACK frame size, or zero when it cannot fit. */
constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); } constexpr uint8_t backWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackPayloadBytes
? static_cast<uint8_t>(kBackFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Complete addressed BACK_TO frame size, or zero when it cannot fit. */
constexpr uint8_t backToWireBytes(uint8_t payloadBytes)
{
return payloadBytes <= kMaxBackToPayloadBytes
? static_cast<uint8_t>(kBackToFrameOverheadBytes + payloadBytes)
: 0U;
}
/** Minimum complete frame size for a known message type; zero means reserved/unknown. */
constexpr uint8_t minimumWireBytes(uint8_t msgType)
{
return (msgType == IR_MSG_DATA_ACCEPT || msgType == IR_MSG_DATA_NOACCEPT)
? kDataFrameOverheadBytes
: msgType == IR_MSG_BACK
? kBackFrameOverheadBytes
: (msgType == IR_MSG_BACK_TO || msgType == IR_MSG_REQUEST)
? kRequestFrameBytes
: msgType == IR_MSG_ACCEPT
? kAcceptFrameBytes
: 0U;
}
constexpr bool isTypedWireSizeValid(uint8_t msgType, uint8_t wireBytes)
{
return minimumWireBytes(msgType) != 0U &&
wireBytes >= minimumWireBytes(msgType) &&
wireBytes <= kMaxWireFrameBytes;
}
/*
* TX FSM timing contract.
*
* The FSM runs on 2*carrierFrec. The preamble contains preambPulse*2
* constant runs; each run is preambToggle+1 ticks. Every data bit and every
* per-byte sync bit occupies bitTakts*2 ticks, independently of its value.
*/
constexpr uint32_t kTxLogicalClockHz = static_cast<uint32_t>(carrierFrec) * 2U;
constexpr uint32_t kPreambleLogicalTicks =
static_cast<uint32_t>(preambPulse * 2U) * static_cast<uint32_t>(preambToggle + 1U);
constexpr uint32_t kEncodedBitLogicalTicks = static_cast<uint32_t>(bitTakts * 2U);
constexpr uint32_t kWireByteLogicalTicks =
static_cast<uint32_t>(bitPerByte + syncBits) * kEncodedBitLogicalTicks;
constexpr uint32_t wireLogicalTicks(uint8_t wireBytes)
{
return wireBytes != 0U && wireBytes <= kMaxWireFrameBytes
? kPreambleLogicalTicks + static_cast<uint32_t>(wireBytes) * kWireByteLogicalTicks
: 0U;
}
constexpr uint32_t logicalTicksToUsCeil(uint32_t logicalTicks)
{
return logicalTicks == 0U
? 0U
: static_cast<uint32_t>(
(static_cast<uint64_t>(logicalTicks) * 1000000ULL +
static_cast<uint64_t>(kTxLogicalClockHz) - 1ULL) /
static_cast<uint64_t>(kTxLogicalClockHz));
}
constexpr uint32_t preambleAirtimeUsCeil()
{
return logicalTicksToUsCeil(kPreambleLogicalTicks);
}
// Decoder completion is published after the final data bit, while the TX FSM
// still emits the last byte's sync bits. Callers that schedule a following
// packet from a decoder terminal must include this physical tail.
constexpr uint32_t kTrailingByteSyncLogicalTicks =
static_cast<uint32_t>(syncBits) * kEncodedBitLogicalTicks;
constexpr uint32_t trailingByteSyncAirtimeUsCeil()
{
return logicalTicksToUsCeil(kTrailingByteSyncLogicalTicks);
}
/** Complete nominal on-air duration, rounded up to a whole microsecond. */
constexpr uint32_t wireAirtimeUsCeil(uint8_t wireBytes)
{
return logicalTicksToUsCeil(wireLogicalTicks(wireBytes));
}
constexpr uint32_t wireAirtimeMsCeil(uint8_t wireBytes)
{
return wireAirtimeUsCeil(wireBytes) == 0U
? 0U
: (wireAirtimeUsCeil(wireBytes) + 999U) / 1000U;
}
/* Preserve the deployed library turn-around policy, but expose it by name. */
constexpr uint16_t kMandatoryInterPacketQuietMs =
static_cast<uint16_t>(
static_cast<uint16_t>(
(static_cast<uint32_t>(bitTime + tolerance) *
static_cast<uint32_t>(bitPerByte + syncBits + 1U)) *
2.7735) /
1000U);
constexpr uint32_t kMandatoryInterPacketQuietUs =
static_cast<uint32_t>(kMandatoryInterPacketQuietMs) * 1000U;
constexpr uint32_t completedFrameTerminalToNextPacketGuardUs(
uint16_t requestedQuietMs)
{
const uint16_t quietMs = requestedQuietMs > kMandatoryInterPacketQuietMs
? requestedQuietMs
: kMandatoryInterPacketQuietMs;
return trailingByteSyncAirtimeUsCeil() +
static_cast<uint32_t>(quietMs) * 1000U;
}
constexpr uint32_t completedFrameTerminalToNextPacketGuardMsCeil(
uint16_t requestedQuietMs)
{
return (completedFrameTerminalToNextPacketGuardUs(requestedQuietMs) + 999U) /
1000U;
}
constexpr uint16_t kDefaultTimingGuardPermille = 1150U;
constexpr uint32_t addTimingGuardUs(
uint32_t durationUs,
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return marginPermille == 0U
? 0U
: static_cast<uint32_t>(
(static_cast<uint64_t>(durationUs) * marginPermille + 999ULL) / 1000ULL);
}
/** Deadline for seeing enough preamble to know that a response has started. */
constexpr uint32_t responseStartGuardUs(
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return addTimingGuardUs(kMandatoryInterPacketQuietUs + preambleAirtimeUsCeil(),
marginPermille);
}
/** Conservative deadline for receiving a complete response of maxWireBytes. */
constexpr uint32_t responseFrameGuardUs(
uint8_t maxWireBytes,
uint16_t marginPermille = kDefaultTimingGuardPermille)
{
return wireAirtimeUsCeil(maxWireBytes) == 0U
? 0U
: addTimingGuardUs(kMandatoryInterPacketQuietUs +
wireAirtimeUsCeil(maxWireBytes),
marginPermille);
}
static_assert(kMaxDataPayloadBytes == 24U, "DATA payload contract changed");
static_assert(kMaxBackPayloadBytes == 26U, "BACK payload contract changed");
static_assert(kPreambleLogicalTicks == 588U, "preamble timing contract changed");
static_assert(kWireByteLogicalTicks == 814U, "wire-byte timing contract changed");
static_assert(kTrailingByteSyncLogicalTicks == 222U, "trailing sync timing changed");
static_assert(kMandatoryInterPacketQuietMs == 42U, "inter-packet quiet policy changed");
}
constexpr uint16_t test_all_Time = bitTime; constexpr uint16_t test_all_Time = bitTime;
constexpr uint16_t test_all_Takts = bitTakts * 2; constexpr uint16_t test_all_Takts = bitTakts * 2;

View File

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

View File

@ -0,0 +1,60 @@
#pragma once
#include <cstddef>
#include <cstdint>
class __FlashStringHelper;
#define F(value) reinterpret_cast<const __FlashStringHelper *>(value)
struct GPIO_TypeDef
{
uint32_t BSRR = 0U;
uint32_t IDR = 0U;
};
using IRQn_Type = int;
enum TimerFormat_t : uint8_t { TICK_FORMAT = 0, MICROSEC_FORMAT, HERTZ_FORMAT };
constexpr uint8_t LOW = 0U;
constexpr uint8_t HIGH = 1U;
constexpr uint8_t INPUT = 0U;
constexpr uint8_t OUTPUT = 1U;
class HardwareTimer
{
public:
void pause() {}
void resume() {}
void setOverflow(uint32_t value, TimerFormat_t = TICK_FORMAT) { overflow_ = value; }
uint32_t getOverflow(TimerFormat_t = TICK_FORMAT) { return overflow_; }
uint32_t getPrescaleFactor() { return 1U; }
uint32_t getTimerClkFreq() { return 12000000U; }
void attachInterrupt(uint8_t, void (*)()) {}
private:
uint32_t overflow_ = 1U;
};
inline GPIO_TypeDef arduinoStubPort;
inline GPIO_TypeDef *digitalPinToPort(uint8_t) { return &arduinoStubPort; }
inline uint16_t digitalPinToBitMask(uint8_t) { return 1U; }
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() {}
inline uint32_t arduinoStubMicros = 0U;
inline uint32_t micros() { return arduinoStubMicros; }
inline uint32_t millis() { return arduinoStubMicros / 1000U; }
class Print
{
public:
template <typename T> void print(const T &) {}
template <typename T> void println(const T &) {}
void println() {}
};
using ArduinoSerialStub = Print;
inline ArduinoSerialStub Serial;

386
tests/test_rx_terminal.cpp Normal file
View File

@ -0,0 +1,386 @@
#include "IR_config.h"
#include "RingBuffer.h"
// Test only: inspect the decoder state machine without adding production hooks.
#define private public
#include "IR_DecoderRaw.h"
#undef private
#include <cassert>
#include <cstdint>
#include <iostream>
#include <limits>
namespace
{
uint32_t decoderTimeoutUs(const IR_DecoderRaw &decoder)
{
return static_cast<uint32_t>(decoder.riseSyncTime + tolerance) *
static_cast<uint32_t>(bitPerByte + syncBits + 1U);
}
uint32_t candidateTimeoutUs(const IR_DecoderRaw &decoder)
{
return decoderTimeoutUs(decoder) *
static_cast<uint32_t>(IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT);
}
uint8_t crc8Local(const uint8_t *data, uint8_t start, uint8_t end, uint8_t poly)
{
uint8_t crc = 0xFFU;
for (uint8_t i = start; i < end; ++i)
{
crc ^= data[i];
for (uint8_t bit = 0; bit < 8U; ++bit)
crc = (crc & 0x80U) != 0U
? static_cast<uint8_t>((crc << 1U) ^ poly)
: static_cast<uint8_t>(crc << 1U);
}
return crc;
}
void primeObservableCandidate(IR_DecoderRaw &decoder, uint32_t lastEdgeUs)
{
decoder.preambleState = IR_DecoderRaw::PreambleState::Candidate;
decoder.preambleGoodPeriods = 1U;
decoder.preambleWasObservable = true;
decoder.preambleMeanPeriod = bitTime;
decoder.preambleCandidateLastEdgeTime = lastEdgeUs;
decoder.preambleCandidateFirstRiseTime = lastEdgeUs;
decoder.preambleCandidateFirstRiseValid = true;
decoder.isPreamb = true;
decoder.isRecive = false;
decoder.isReciveRaw = false;
}
void primeLocked(IR_DecoderRaw &decoder, uint8_t msgType, uint8_t wireBytes)
{
decoder.preambleState = IR_DecoderRaw::PreambleState::Locked;
decoder.isPreamb = false;
decoder.isRecive = true;
decoder.isReciveRaw = true;
decoder.isWrongPack = false;
decoder.isBufferOverflow = false;
decoder.isAvailable = false;
decoder.packSize = wireBytes;
decoder.dataBuffer[0] =
static_cast<uint8_t>((msgType << 5U) | (wireBytes & IR_MASK_MSG_INFO));
decoder.i_dataBuffer = 8U;
}
void verifyInitialSnapshot()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
assert(terminal.seq == 0U);
assert(terminal.reason == IR_RxTerminalReason::None);
assert(terminal.msgType == 0xFFU);
assert(!terminal.hadLock);
}
void verifyCandidateExpiresOnIdleTick()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t lastEdgeUs = 1000U;
primeObservableCandidate(decoder, lastEdgeUs);
assert(decoder.rxLineActive());
arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
decoder.tick();
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
assert(!decoder.rxLineActive());
assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Idle);
assert(terminal.seq == 1U);
assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout);
assert(terminal.msgType == 0xFFU);
assert(!terminal.hadLock);
++arduinoStubMicros;
decoder.tick();
assert(decoder.rxTerminalSeq() == terminal.seq);
}
void emitEdge(IR_DecoderRaw &decoder, uint32_t timeUs, bool high)
{
arduinoStubMicros = timeUs;
arduinoStubPort.IDR = high ? 1U : 0U;
decoder.isr();
decoder.tick();
}
void queueEdge(IR_DecoderRaw &decoder, uint32_t timeUs, bool high)
{
arduinoStubMicros = timeUs;
arduinoStubPort.IDR = high ? 1U : 0U;
decoder.isr();
}
void verifyCandidateIdleExpiryThroughPublicPipeline()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U;
const uint32_t risePeriodUs = static_cast<uint32_t>(bitTime) * 5U / 2U;
emitEdge(decoder, firstRiseUs, true);
emitEdge(decoder, firstRiseUs + risePeriodUs / 2U, false);
emitEdge(decoder, firstRiseUs + risePeriodUs, true);
assert(decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 0U);
arduinoStubMicros = firstRiseUs + risePeriodUs + candidateTimeoutUs(decoder) + 1U;
decoder.tick();
assert(!decoder.rxLineActive());
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
assert(decoder.rxTerminalSeq() == 1U);
}
void verifyCoarseResetPublishesThroughBatchedPublicPipeline()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U;
const uint32_t goodPeriodUs = static_cast<uint32_t>(bitTime) * 5U / 2U;
const uint32_t badPeriodUs = static_cast<uint32_t>(bitTime) * 4U;
queueEdge(decoder, firstRiseUs, true);
queueEdge(decoder, firstRiseUs + goodPeriodUs / 2U, false);
queueEdge(decoder, firstRiseUs + goodPeriodUs, true);
queueEdge(decoder, firstRiseUs + goodPeriodUs + badPeriodUs / 2U, false);
queueEdge(decoder, firstRiseUs + goodPeriodUs + badPeriodUs, true);
decoder.tick();
assert(decoder.rxTerminalSeq() == 0U);
assert(decoder.rxLineActive());
// Continuing coarse-invalid edges below the timeout keep the potential
// frame busy. They manufacture no terminal; a Car gate reaches its bounded
// hard deadline and skips the optional tail instead of transmitting here.
const uint32_t nextRiseUs =
firstRiseUs + goodPeriodUs + badPeriodUs + badPeriodUs;
queueEdge(decoder, nextRiseUs - badPeriodUs / 2U, false);
queueEdge(decoder, nextRiseUs, true);
decoder.tick();
assert(decoder.rxTerminalSeq() == 0U);
assert(decoder.rxLineActive());
arduinoStubMicros = nextRiseUs + candidateTimeoutUs(decoder) + 1U;
decoder.tick();
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
assert(terminal.seq == 1U);
assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout);
assert(!terminal.hadLock);
assert(!decoder.rxLineActive());
}
void verifyFreshCandidateWithOnlyCoarseInvalidEdgesStaysActive()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t firstRiseUs = decoderTimeoutUs(decoder) * 2U + 1000U;
const uint32_t badPeriodUs = static_cast<uint32_t>(bitTime) * 4U;
const uint32_t startDeadlineUs = firstRiseUs + 58000U;
const uint32_t hardDeadlineUs = firstRiseUs + 78000U;
emitEdge(decoder, firstRiseUs, true);
assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Candidate);
assert(decoder.preambleGoodPeriods == 0U);
assert(decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 0U);
uint32_t riseUs = firstRiseUs;
while (riseUs + badPeriodUs <= startDeadlineUs)
{
emitEdge(decoder, riseUs + badPeriodUs / 2U, false);
riseUs += badPeriodUs;
emitEdge(decoder, riseUs, true);
assert(decoder.preambleGoodPeriods == 0U);
assert(decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 0U);
}
arduinoStubMicros = startDeadlineUs;
decoder.tick();
assert(decoder.rxLineActive());
while (riseUs + badPeriodUs <= hardDeadlineUs)
{
emitEdge(decoder, riseUs + badPeriodUs / 2U, false);
riseUs += badPeriodUs;
emitEdge(decoder, riseUs, true);
assert(decoder.preambleGoodPeriods == 0U);
assert(decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 0U);
}
arduinoStubMicros = hardDeadlineUs;
decoder.tick();
assert(decoder.rxLineActive());
arduinoStubMicros = riseUs + candidateTimeoutUs(decoder);
decoder.tick();
assert(decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 0U);
++arduinoStubMicros;
decoder.tick();
assert(!decoder.rxLineActive());
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
assert(decoder.rxTerminalSeq() == 1U);
}
void verifyCandidateExpiryWaitsForPipelineDrain()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t lastEdgeUs = 2000U;
primeObservableCandidate(decoder, lastEdgeUs);
decoder.pulseFilterHoldCount = 1U;
const uint32_t expiredAt = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
decoder.expirePreambleCandidateIfIdle(expiredAt);
assert(decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 0U);
decoder.pulseFilterHoldCount = 0U;
decoder.expirePreambleCandidateIfIdle(expiredAt);
assert(!decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 1U);
}
void verifyCandidateExpiryAcrossMicrosWrap()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t lastEdgeUs = std::numeric_limits<uint32_t>::max() - 1000U;
primeObservableCandidate(decoder, lastEdgeUs);
arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
decoder.tick();
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
assert(!decoder.rxLineActive());
}
void verifyCandidateTimeoutBoundary()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t lastEdgeUs = 2500U;
primeObservableCandidate(decoder, lastEdgeUs);
arduinoStubMicros = lastEdgeUs + candidateTimeoutUs(decoder);
decoder.tick();
assert(decoder.rxLineActive());
assert(decoder.rxTerminalSeq() == 0U);
++arduinoStubMicros;
decoder.tick();
assert(!decoder.rxLineActive());
assert(decoder.rxLastTerminal().reason == IR_RxTerminalReason::CandidateTimeout);
}
void verifyTimedOutCandidateRestartIsTerminal()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
const uint32_t lastEdgeUs = 3000U;
primeObservableCandidate(decoder, lastEdgeUs);
IR_DecoderRaw::FrontStorage nextEdge;
nextEdge.time = lastEdgeUs + candidateTimeoutUs(decoder) + 1U;
nextEdge.dir = true;
decoder.preambleProcessEdge(nextEdge);
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
assert(terminal.seq == 1U);
assert(terminal.reason == IR_RxTerminalReason::CandidateTimeout);
assert(decoder.preambleState == IR_DecoderRaw::PreambleState::Candidate);
assert(decoder.preambleGoodPeriods == 0U);
assert(decoder.rxReasonCounters()[
static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Preamble)] == 1U);
}
void verifyLockedTimeoutPublishesHeaderTypeOnce()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
primeLocked(decoder, IR_MSG_DATA_NOACCEPT, 10U);
decoder.lastEdgeTime = 5000U;
arduinoStubMicros = decoder.lastEdgeTime + decoderTimeoutUs(decoder) * 2U + 1U;
decoder.tick();
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
assert(terminal.seq == 1U);
assert(terminal.reason == IR_RxTerminalReason::LockedTimeout);
assert(terminal.msgType == IR_MSG_DATA_NOACCEPT);
assert(terminal.hadLock);
assert(!decoder.rxLineActive());
++arduinoStubMicros;
decoder.tick();
assert(decoder.rxTerminalSeq() == terminal.seq);
}
void verifyDecodeAbortPublishesTerminal()
{
IR_DecoderRaw decoder(1U, 42U, nullptr);
primeLocked(decoder, IR_MSG_REQUEST, 7U);
decoder.isWrongPack = true;
decoder.writeToBuffer(false);
const IR_RxTerminalInfo terminal = decoder.rxLastTerminal();
assert(terminal.seq == 1U);
assert(terminal.reason == IR_RxTerminalReason::DecodeAbort);
assert(terminal.msgType == IR_MSG_REQUEST);
assert(terminal.hadLock);
}
void finishBackFrame(IR_DecoderRaw &decoder, bool corruptCrc)
{
constexpr uint8_t wireBytes = 5U;
primeLocked(decoder, IR_MSG_BACK, wireBytes);
decoder.dataBuffer[1] = 0x12U;
decoder.dataBuffer[2] = 0x34U;
decoder.dataBuffer[3] = crc8Local(decoder.dataBuffer, 0U, 3U, poly1);
const uint8_t crcLow = crc8Local(decoder.dataBuffer, 0U, 4U, poly2);
const uint8_t finalBit = static_cast<uint8_t>((crcLow & 1U) ^ (corruptCrc ? 1U : 0U));
decoder.dataBuffer[4] = static_cast<uint8_t>(crcLow & 0xFEU);
decoder.i_dataBuffer = wireBytes * bitPerByte - 1U;
decoder.bufBitPos = static_cast<int16_t>(decoder.i_dataBuffer);
decoder.nextControlBit = 0xFFFFU;
decoder.isData = true;
decoder.writeToBuffer(finalBit != 0U);
}
void verifyCompleteFrameTerminalReasons()
{
IR_DecoderRaw good(1U, 42U, nullptr);
finishBackFrame(good, false);
const IR_RxTerminalInfo ok = good.rxLastTerminal();
assert(ok.seq == 1U);
assert(ok.reason == IR_RxTerminalReason::FrameOk);
assert(ok.msgType == IR_MSG_BACK);
assert(ok.hadLock);
IR_DecoderRaw bad(1U, 42U, nullptr);
finishBackFrame(bad, true);
const IR_RxTerminalInfo crc = bad.rxLastTerminal();
assert(crc.seq == 1U);
assert(crc.reason == IR_RxTerminalReason::FrameCrcError);
assert(crc.msgType == IR_MSG_BACK);
assert(crc.hadLock);
}
}
int main()
{
verifyInitialSnapshot();
verifyCandidateExpiresOnIdleTick();
verifyCandidateIdleExpiryThroughPublicPipeline();
verifyCoarseResetPublishesThroughBatchedPublicPipeline();
verifyFreshCandidateWithOnlyCoarseInvalidEdgesStaysActive();
verifyCandidateExpiryWaitsForPipelineDrain();
verifyCandidateExpiryAcrossMicrosWrap();
verifyCandidateTimeoutBoundary();
verifyTimedOutCandidateRestartIsTerminal();
verifyLockedTimeoutPublishesHeaderTypeOnce();
verifyDecodeAbortPublishesTerminal();
verifyCompleteFrameTerminalReasons();
std::cout << "IR RX terminal tests: OK\n";
return 0;
}

View File

@ -0,0 +1,97 @@
#include "IR_Encoder.h"
#include "IR_DecoderRaw.h"
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
// Link seams: these paths are not exercised by the pure host timing test.
bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *) { return true; }
void IR_DecoderRaw::refreshPairMuteState() {}
void IR_Encoder::send_HIGH(bool) {}
void IR_Encoder::send_LOW() {}
void IR_Encoder::send_EMPTY(uint8_t) {}
namespace
{
static_assert(irproto::dataWireBytes(0U) == 7U, "empty DATA wire size changed");
static_assert(irproto::dataWireBytes(3U) == 10U, "DATA wire size changed");
static_assert(irproto::dataWireBytes(24U) == 31U, "maximum DATA wire size changed");
static_assert(irproto::dataWireBytes(25U) == 0U, "oversized DATA must be rejected");
static_assert(irproto::backWireBytes(1U) == 6U, "BACK wire size changed");
static_assert(irproto::backWireBytes(26U) == 31U, "maximum BACK wire size changed");
static_assert(irproto::backToWireBytes(24U) == 31U, "maximum BACK_TO wire size changed");
static_assert(irproto::wireLogicalTicks(6U) == 5472U, "6-byte tick count changed");
static_assert(irproto::wireLogicalTicks(10U) == 8728U, "10-byte tick count changed");
static_assert(irproto::wireLogicalTicks(31U) == 25822U, "31-byte tick count changed");
static_assert(irproto::preambleAirtimeUsCeil() == 7737U, "preamble airtime changed");
static_assert(irproto::trailingByteSyncAirtimeUsCeil() == 2922U,
"trailing sync airtime changed");
static_assert(irproto::wireAirtimeUsCeil(6U) == 72000U, "6-byte airtime changed");
static_assert(irproto::wireAirtimeUsCeil(10U) == 114843U, "10-byte airtime changed");
static_assert(irproto::wireAirtimeUsCeil(31U) == 339764U, "31-byte airtime changed");
static_assert(irproto::responseStartGuardUs() == 57198U, "response-start guard changed");
static_assert(irproto::responseFrameGuardUs(6U) == 131100U, "response-frame guard changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardUs(0U) == 44922U,
"mandatory physical post-terminal quiet changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(0U) == 45U,
"mandatory post-terminal guard rounding changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(60U) == 63U,
"configured post-terminal guard changed");
static_assert(irproto::completedFrameTerminalToNextPacketGuardMsCeil(65535U) ==
65538U,
"large guard must not wrap uint16");
static_assert(IR_DecoderRaw::rxMaxPackSize() == 31U, "RX max must be the wire max");
uint32_t sumLogicalTicks(const IrTxGateRun *runs, size_t count)
{
uint32_t total = 0U;
for (size_t i = 0U; i < count; ++i)
total += runs[i].lenTicks;
return total;
}
void verifyFormulaAgainstTxFsm()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, 1024U> runs{};
for (uint8_t wireBytes = 1U; wireBytes <= irproto::kMaxWireFrameBytes; ++wireBytes)
{
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
{
for (uint8_t i = 0U; i < wireBytes; ++i)
{
frame[i] = pattern == 0U ? 0x00U
: pattern == 1U ? 0xFFU
: pattern == 2U ? static_cast<uint8_t>((i & 1U) ? 0x55U : 0xAAU)
: static_cast<uint8_t>(i * 73U + 19U);
}
const size_t count = IR_Encoder::buildGateRuns(
frame.data(), wireBytes, runs.data(), runs.size());
assert(count != 0U);
assert(sumLogicalTicks(runs.data(), count) == irproto::wireLogicalTicks(wireBytes));
}
}
}
void verifyPublicSendTimeResults()
{
IR_Encoder encoder(1U, 42U, nullptr, false);
uint8_t payload[26]{};
assert(encoder.testSendAccept(1U) == 72U);
assert(encoder.testSendTime(1U, payload, 3U) == 115U);
assert(encoder.testSendBack(payload, 26U) == 340U);
}
}
int main()
{
verifyFormulaAgainstTxFsm();
verifyPublicSendTimeResults();
std::cout << "IR timing contract tests: OK\n";
return 0;
}