diff --git a/.gitignore b/.gitignore index ae3844c..ecee7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ Analyzer/raw/dll/*.dylib /Analyzer/raw/IR_Fox/.github **/.build graphify-out/* +**/.build-*/ +/tests/*.exe diff --git a/Analyzer/raw/IR_Fox/CMakeLists.txt b/Analyzer/raw/IR_Fox/CMakeLists.txt index 6a7f575..a5feeec 100644 --- a/Analyzer/raw/IR_Fox/CMakeLists.txt +++ b/Analyzer/raw/IR_Fox/CMakeLists.txt @@ -15,6 +15,7 @@ set(SOURCES src/IrFoxAnalyzer.h src/IrFoxDecoder.cpp src/IrFoxDecoder.h + src/IrFoxPacketClassifier.h src/IrFoxAnalyzerResults.cpp src/IrFoxAnalyzerResults.h src/IrFoxAnalyzerSettings.cpp @@ -24,3 +25,23 @@ set(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 + $ + $) + add_test(NAME IrFoxDecoderTests COMMAND IrFoxDecoderTests) +endif() diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp index e4cc182..da03579 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp @@ -1,6 +1,7 @@ #include "IrFoxAnalyzer.h" #include "IrFoxAnalyzerSettings.h" #include "IrFoxDecoder.h" +#include "IrFoxPacketClassifier.h" #include #include #include @@ -25,7 +26,6 @@ IrFoxAnalyzer::~IrFoxAnalyzer() void IrFoxAnalyzer::SetupResults() { - m_packet_hex_by_frame.clear(); mResults.reset(new IrFoxAnalyzerResults(this, &mSettings)); SetAnalyzerResults(mResults.get()); mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel); @@ -46,35 +46,75 @@ static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_by s += "..."; } -const char* IrFoxAnalyzer::PacketHexForFrame(U64 frame_id) +static const char* packet_status_icon(IrFoxPacketOutcome outcome) { - auto it = m_packet_hex_by_frame.find(frame_id); - if (it == m_packet_hex_by_frame.end()) - return ""; - m_hex_scratch = it->second; - return m_hex_scratch.c_str(); + switch (outcome) + { + case IrFoxPacketOutcome::Accepted: + return "✅"; + case IrFoxPacketOutcome::IgnoredAddress: + return "📭"; + case IrFoxPacketOutcome::RejectedCrc: + case IrFoxPacketOutcome::RejectedLength: + return "❌"; + case IrFoxPacketOutcome::RawOnlyUnknownType: + case IrFoxPacketOutcome::RawOnlyTypedLength: + return "⚠️"; + } + return "⚠️"; } -const char* IrFoxAnalyzer::BubbleTextForFrame(U64 frame_id) const +static const char* message_type_icon(uint8_t message_type) { - auto it = m_bubble_text_by_frame.find(frame_id); - if (it == m_bubble_text_by_frame.end()) - return ""; - m_bubble_scratch = it->second; - return m_bubble_scratch.c_str(); + 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 std::string packet_icon(const IrFoxPacketDecision& decision, IrFoxPacketIconMode mode) +{ + const char* status = packet_status_icon(decision.outcome); + // Icon-mode selection describes successfully accepted packets. Diagnostic + // outcomes must remain visible even when the user selected type-only mode. + if (decision.outcome != IrFoxPacketOutcome::Accepted) + return status; + 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() { mIr = GetAnalyzerChannelData(mSettings.mInputChannel); - m_packet_hex_by_frame.clear(); - m_bubble_text_by_frame.clear(); + mResults->ClearCachedFrameText(); const U32 fs = GetSampleRate(); IrFoxDecoder decoder; decoder.reset(); - /** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */ + /** Mirrors the firmware input filter. kMinFilteredPulseUs=0 means direct edge delivery. */ const U64 min_seg_samples = std::max(1ULL, static_cast((static_cast(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast(fs) + 0.5)); struct RawEdge @@ -115,8 +155,19 @@ void IrFoxAnalyzer::WorkerThread() U32 frames_since_commit = 0; const U32 kCommitBatch = 256; + const bool detailed_presentation = mSettings.mPresentation == IrFoxPresentation::Detailed; + std::vector pending_byte_frames; + pending_byte_frames.reserve(irfox::kDataByteSizeMax); - IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) { + auto note_legacy_frame = [&]() { + if (++frames_since_commit >= kCommitBatch) + { + mResults->CommitResults(); + frames_since_commit = 0; + } + }; + + auto add_event_frame = [&](const IrFoxEmitBit& e) { Frame frame; frame.mStartingSampleInclusive = static_cast(e.start_sample); frame.mEndingSampleInclusive = static_cast(e.end_sample); @@ -124,45 +175,188 @@ void IrFoxAnalyzer::WorkerThread() 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.mFlags = e.mflags; - // В SDK только ERROR/WARNING меняют цвет бабла; sync выделяем янтарным (как warning), данные — обычные. - if (e.frame_type == IRF_FT_SYNC_BIT) - frame.mFlags |= DISPLAY_AS_WARNING_FLAG; + mResults->AddFrame(frame); + note_legacy_frame(); + }; - const U64 fid = mResults->AddFrame(frame); - if (e.bubble_text[0] != '\0') - m_bubble_text_by_frame[fid] = e.bubble_text; - if (++frames_since_commit >= kCommitBatch) + 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) { - mResults->CommitResults(); - frames_since_commit = 0; + 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(e.start_sample), AnalyzerResults::Start, + mSettings.mInputChannel); + const U64 marker_sample = static_cast((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) + return; + 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); }; 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.mStartingSampleInclusive = static_cast(p.start_sample); + if (detailed_presentation) + { + // 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(pending_byte_frames.empty() ? + p.data_start_sample : pending_byte_frames.back().start_sample); + } + else + { + frame.mStartingSampleInclusive = static_cast(p.start_sample); + } frame.mEndingSampleInclusive = static_cast(p.end_sample); - frame.mType = p.crc_ok ? IRF_FT_PACKET_OK : IRF_FT_PACKET_CRC_FAIL; + frame.mFlags = 0; + 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.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); + pending_byte_frames.clear(); + + const std::string icon = packet_icon(decision, mSettings.mPacketIconMode); std::string hx; append_hex(hx, p.data_bytes, p.pack_size); - m_packet_hex_by_frame[fid] = std::move(hx); + std::string status = irfox::packetOutcomeText(decision.outcome); + if (p.pack_size >= irfox::kMsgBytes) + { + status += " "; + status += irfox::messageTypeText(decision.message_type); + } + if (decision.has_destination) + status += " to=" + std::to_string(decision.destination); - FrameV2 fv2; - fv2.AddBoolean("crc_ok", p.crc_ok); - fv2.AddInteger("len", static_cast(p.pack_size)); - fv2.AddInteger("err_low", static_cast(p.err_low)); - fv2.AddInteger("err_high", static_cast(p.err_high)); - fv2.AddInteger("err_other", static_cast(p.err_other)); - fv2.AddByteArray("data", p.data_bytes, p.pack_size); - mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast(p.start_sample), - static_cast(p.end_sample)); + auto cached_text = std::make_shared(); + cached_text->export_hex = hx; + cached_text->bubble_texts[0] = icon; + if (detailed_presentation) + { + char last_byte[3] = "??"; + if (p.pack_size > 0) + std::snprintf(last_byte, sizeof last_byte, "%02X", static_cast(p.data_bytes[p.pack_size - 1])); + cached_text->bubble_texts[1] = std::string("0x") + last_byte + " " + icon; + 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(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(decision.outcome)); + fv2.AddInteger("message_type", static_cast(decision.message_type)); + fv2.AddInteger("receiver_address", static_cast(mSettings.mReceiverAddress)); + if (decision.has_destination) + fv2.AddInteger("destination", static_cast(decision.destination)); + fv2.AddInteger("len", static_cast(p.pack_size)); + fv2.AddInteger("err_low", static_cast(p.err_low)); + fv2.AddInteger("err_high", static_cast(p.err_high)); + fv2.AddInteger("err_other", static_cast(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(p.start_sample), static_cast(p.end_sample)); + } if (++frames_since_commit >= kCommitBatch) { @@ -170,6 +364,10 @@ void IrFoxAnalyzer::WorkerThread() 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 = [&]() { for (;;) @@ -180,7 +378,7 @@ void IrFoxAnalyzer::WorkerThread() return; if (pending[1].sample - pending[0].sample < min_seg_samples) continue; - decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt); + decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt); last_dec_edge_sample = pending[0].sample; last_dec_edge_valid = true; pending.erase(pending.begin()); @@ -192,7 +390,7 @@ void IrFoxAnalyzer::WorkerThread() strip_vs_last_decoder(); while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples) { - decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt); + decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt); last_dec_edge_sample = pending[0].sample; last_dec_edge_valid = true; pending.erase(pending.begin()); @@ -201,7 +399,7 @@ void IrFoxAnalyzer::WorkerThread() } if (pending.size() == 1) { - decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt); + decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt); last_dec_edge_sample = pending[0].sample; last_dec_edge_valid = true; pending.clear(); @@ -230,7 +428,9 @@ void IrFoxAnalyzer::WorkerThread() } flush_pending_tail(); - decoder.flushEnd(mIr->GetSampleNumber(), fs, on_bit, on_pkt); + decoder.flushEnd(mIr->GetSampleNumber(), fs, bit_events, on_pkt); + if (detailed_presentation) + flush_pending_bytes(); if (frames_since_commit != 0) mResults->CommitResults(); diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h index dbf3167..0551910 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h @@ -6,8 +6,6 @@ #include "IrFoxAnalyzerResults.h" #include "IrFoxSimulationDataGenerator.h" #include -#include -#include class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2 { @@ -25,9 +23,6 @@ public: virtual const char* GetAnalyzerName() const; virtual bool NeedsRerun(); - const char* PacketHexForFrame(U64 frame_id); - const char* BubbleTextForFrame(U64 frame_id) const; - protected: IrFoxAnalyzerSettings mSettings; std::unique_ptr mResults; @@ -36,10 +31,6 @@ protected: IrFoxSimulationDataGenerator mSimulationDataGenerator; bool mSimulationInitilized; - std::unordered_map m_packet_hex_by_frame; - std::unordered_map 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(); diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp index 0499232..36ebecf 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp @@ -6,6 +6,7 @@ #include "IrFoxDecoder.h" #include #include +#include IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings) : AnalyzerResults(), @@ -18,51 +19,86 @@ IrFoxAnalyzerResults::~IrFoxAnalyzerResults() { } +void IrFoxAnalyzerResults::ClearCachedFrameText() +{ + std::lock_guard 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 text) +{ + std::lock_guard lock(m_frame_text_mutex); + m_frame_text_by_frame[frame_id] = std::move(text); +} + +std::shared_ptr IrFoxAnalyzerResults::CachedFrameTextForFrame(U64 frame_id) const +{ + std::lock_guard 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)display_base; (void)channel; ClearResultStrings(); + auto add_cached_text = [&]() { + const std::shared_ptr 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); char line[256]; switch (frame.mType) { - case IRF_FT_DATA_BIT: - case IRF_FT_SYNC_BIT: + case IRF_FT_DATA_BYTE: + { + char byte_text[3]; + std::snprintf(byte_text, sizeof byte_text, "%02X", static_cast(frame.mData1 & 0xFFu)); + AddResultString(byte_text); + AddResultString("0x", byte_text); + break; + } + case IRF_FT_PREAMBLE: case IRF_FT_OVERFLOW: case IRF_FT_ABORT: { - const char* bt = mAnalyzer->BubbleTextForFrame(frame_index); - if (bt && bt[0]) - AddResultString(bt); - else if (frame.mType == IRF_FT_DATA_BIT) - 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) + if (frame.mType == IRF_FT_OVERFLOW) AddResultString("OVF"); else if (frame.mType == IRF_FT_ABORT) AddResultString("SYNC!"); else - AddResultString("PRE"); + { + AddResultString("📡"); + AddResultString("📡 PRE"); + } break; } - case IRF_FT_PACKET_OK: + case IRF_FT_PACKET_ACCEPTED: case IRF_FT_PACKET_CRC_FAIL: + case IRF_FT_PACKET_BAD_LENGTH: + case IRF_FT_PACKET_RAW_ONLY: + case IRF_FT_PACKET_IGNORED_ADDRESS: { - snprintf(line, sizeof line, "%s %lluB", frame.mType == IRF_FT_PACKET_OK ? "OK" : "CRC", - (unsigned long long)frame.mData1); - AddResultString(line); - const char* hx = mAnalyzer->PacketHexForFrame(frame_index); - if (hx && hx[0]) - AddResultString(hx); + if (!add_cached_text()) + AddResultString(frame.mType == IRF_FT_PACKET_ACCEPTED ? "✅" : + (frame.mType == IRF_FT_PACKET_CRC_FAIL || frame.mType == IRF_FT_PACKET_BAD_LENGTH) ? "❌" : + frame.mType == IRF_FT_PACKET_IGNORED_ADDRESS ? "📭" : "⚠️"); break; } @@ -95,17 +131,20 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp const char* typ = "?"; switch (frame.mType) { - case IRF_FT_DATA_BIT: - typ = "D"; - break; - case IRF_FT_SYNC_BIT: - typ = "S"; - break; - case IRF_FT_PACKET_OK: - typ = "OK"; + case IRF_FT_PACKET_ACCEPTED: + typ = "ACCEPT"; break; case IRF_FT_PACKET_CRC_FAIL: - typ = "CRC"; + typ = "REJECT_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; case IRF_FT_OVERFLOW: typ = "OVF"; @@ -120,14 +159,12 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp break; } - const char* hx = mAnalyzer->PacketHexForFrame(i); - if (!hx) - hx = ""; + const std::shared_ptr cached = CachedFrameTextForFrame(i); + const char* hx = cached ? cached->export_hex.c_str() : ""; U64 bit_idx = 0; U32 err_l = 0, err_h = 0, err_o = 0; - if (frame.mType == IRF_FT_DATA_BIT || frame.mType == IRF_FT_SYNC_BIT || - frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT) + if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT) { bit_idx = frame.mData2 & 0xFFFFull; err_l = static_cast((frame.mData2 >> 16) & 0xFFull); diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h index 4007fcc..b28f764 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h @@ -2,10 +2,22 @@ #define IRFOX_ANALYZER_RESULTS #include +#include +#include +#include +#include +#include class IrFoxAnalyzer; class IrFoxAnalyzerSettings; +struct IrFoxCachedFrameText +{ + std::array bubble_texts{}; + size_t bubble_text_count = 0; + std::string export_hex; +}; + class IrFoxAnalyzerResults : public AnalyzerResults { public: @@ -19,9 +31,15 @@ public: virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base); virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base); + void ClearCachedFrameText(); + void CacheFrameText(U64 frame_id, std::shared_ptr text); + std::shared_ptr CachedFrameTextForFrame(U64 frame_id) const; + protected: IrFoxAnalyzerSettings* mSettings; IrFoxAnalyzer* mAnalyzer; + mutable std::mutex m_frame_text_mutex; + std::unordered_map> m_frame_text_by_frame; }; #endif diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp index c7b0163..ebdc6a9 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp @@ -3,14 +3,42 @@ IrFoxAnalyzerSettings::IrFoxAnalyzerSettings() : mInputChannel(UNDEFINED_CHANNEL), - mInputChannelInterface() + mReceiverAddress(0), + mPresentation(IrFoxPresentation::Overview), + mPacketIconMode(IrFoxPacketIconMode::StatusAndType), + mInputChannelInterface(), + mReceiverAddressInterface(), + mPresentationInterface(), + mPacketIconModeInterface() { mInputChannelInterface.SetTitleAndTooltip( "IR", "Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)"); 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(IrFoxPresentation::Overview), "Overview", "One outcome bubble across the full frame; no PRE badge."); + mPresentationInterface.AddNumber(static_cast(IrFoxPresentation::Detailed), "Detailed", "Separate PRE and packet bubbles, plus one hexadecimal bubble per byte."); + mPresentationInterface.SetNumber(static_cast(mPresentation)); + mPacketIconModeInterface.SetTitleAndTooltip( + "Packet icon", + "Choose whether packet bubbles show reception status, decoded message type, or both."); + mPacketIconModeInterface.AddNumber(static_cast(IrFoxPacketIconMode::Status), "Status ✅", "One status symbol: accepted, other address, invalid, or unknown."); + mPacketIconModeInterface.AddNumber(static_cast(IrFoxPacketIconMode::MessageType), "Message type 📦", "One symbol for the decoded firmware message type."); + mPacketIconModeInterface.AddNumber(static_cast(IrFoxPacketIconMode::StatusAndType), "Status + type ✅📦", "Reception status followed by the decoded firmware message type."); + mPacketIconModeInterface.SetNumber(static_cast(mPacketIconMode)); AddInterface(&mInputChannelInterface); + AddInterface(&mReceiverAddressInterface); + AddInterface(&mPresentationInterface); + AddInterface(&mPacketIconModeInterface); AddExportOption(0, "Export as text/csv file"); AddExportExtension(0, "text", "txt"); @@ -27,6 +55,23 @@ IrFoxAnalyzerSettings::~IrFoxAnalyzerSettings() bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces() { mInputChannel = mInputChannelInterface.GetChannel(); + mReceiverAddress = static_cast(mReceiverAddressInterface.GetInteger()); + const int presentation = static_cast(mPresentationInterface.GetNumber()); + mPresentation = presentation == static_cast(IrFoxPresentation::Detailed) ? + IrFoxPresentation::Detailed : IrFoxPresentation::Overview; + const int packet_icon_mode = static_cast(mPacketIconModeInterface.GetNumber()); + switch (packet_icon_mode) + { + case static_cast(IrFoxPacketIconMode::Status): + mPacketIconMode = IrFoxPacketIconMode::Status; + break; + case static_cast(IrFoxPacketIconMode::MessageType): + mPacketIconMode = IrFoxPacketIconMode::MessageType; + break; + default: + mPacketIconMode = IrFoxPacketIconMode::StatusAndType; + break; + } ClearChannels(); AddChannel(mInputChannel, "IR Fox", true); @@ -37,6 +82,9 @@ bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces() void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings() { mInputChannelInterface.SetChannel(mInputChannel); + mReceiverAddressInterface.SetInteger(mReceiverAddress); + mPresentationInterface.SetNumber(static_cast(mPresentation)); + mPacketIconModeInterface.SetNumber(static_cast(mPacketIconMode)); } void IrFoxAnalyzerSettings::LoadSettings(const char* settings) @@ -45,6 +93,50 @@ void IrFoxAnalyzerSettings::LoadSettings(const char* settings) text_archive.SetString(settings); 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(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(IrFoxPresentation::Overview); + if (text_archive >> presentation && presentation == static_cast(IrFoxPresentation::Detailed)) + mPresentation = IrFoxPresentation::Detailed; + else + mPresentation = IrFoxPresentation::Overview; + S32 packet_icon_mode = static_cast(IrFoxPacketIconMode::StatusAndType); + if (text_archive >> packet_icon_mode) + { + switch (packet_icon_mode) + { + case static_cast(IrFoxPacketIconMode::Status): + mPacketIconMode = IrFoxPacketIconMode::Status; + break; + case static_cast(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(); AddChannel(mInputChannel, "IR Fox", true); @@ -57,6 +149,11 @@ const char* IrFoxAnalyzerSettings::SaveSettings() SimpleArchive text_archive; text_archive << mInputChannel; + text_archive << static_cast(mReceiverAddress); + // Retain the old third field so already-saved configurations remain readable. + text_archive << static_cast(1); + text_archive << static_cast(mPresentation); + text_archive << static_cast(mPacketIconMode); return SetReturnString(text_archive.GetString()); } diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h index 694239a..7c70b0f 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h @@ -3,6 +3,22 @@ #include #include +#include + +/** 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 { @@ -16,9 +32,16 @@ public: virtual const char* SaveSettings(); 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: AnalyzerSettingInterfaceChannel mInputChannelInterface; + AnalyzerSettingInterfaceInteger mReceiverAddressInterface; + AnalyzerSettingInterfaceNumberList mPresentationInterface; + AnalyzerSettingInterfaceNumberList mPacketIconModeInterface; }; #endif diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp index de25571..008bab1 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp @@ -73,6 +73,17 @@ void IrFoxDecoder::first_rx() std::memset(data_buffer, 0, sizeof data_buffer); preamble_bubble_start_valid_ = 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_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; } void IrFoxDecoder::listen_start(double t_us) @@ -105,7 +116,7 @@ void IrFoxDecoder::check_timeout(double t_us) 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, IrFoxEmitBitMode emit_mode) { - if (i_data_buffer > irfox::kDataByteSizeMax * 8u) + if (i_data_buffer >= irfox::kDataByteSizeMax * 8u) { if (!is_buffer_overflow && on_bit) { @@ -140,6 +151,19 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_ if (is_data) { 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(bit ? 1 : 0) << (7 - (i_data_buffer % 8)); i_data_buffer++; buf_bit_pos++; @@ -156,6 +180,16 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_ e.bubble_text[1] = '\0'; 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(byte_start_sample_), static_cast(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(data_buffer[byte_index])); + on_bit(e); + } if (was_first_data_bit && trim_first_data_bit_cell_) trim_first_data_bit_cell_ = false; } @@ -216,12 +250,33 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_ if (!is_available && is_data && !is_wrong_pack) { if (i_data_buffer == 8 * irfox::kMsgBytes) + { pack_size = static_cast(data_buffer[0] & 0x1Fu); + // The receiver rejects a length that cannot contain its two CRC bytes. + // Emit the rejection here so the capture explains why no packet follows. + if (pack_size != 0 && pack_size < irfox::kMsgBytes + irfox::kCrcBytes) + { + is_wrong_pack = true; + IrFoxEmitPacket pkt{}; + pkt.start_sample = static_cast(packet_start_valid_ ? packet_start_sample_ : cell_start_s); + pkt.data_start_sample = static_cast(packet_data_start_valid_ ? packet_data_start_sample_ : cell_start_s); + pkt.end_sample = static_cast(cell_end_s); + pkt.crc_ok = false; + pkt.pack_size = static_cast(pack_size); + pkt.err_low = err_low_signal; + pkt.err_high = err_high_signal; + pkt.err_other = err_other; + pkt.data_bytes[0] = data_buffer[0]; + if (on_pkt) + on_pkt(pkt); + } + } if (pack_size && (i_data_buffer == 8)) msg_type_receive = static_cast((data_buffer[0] >> 5) | 0xF8u); - if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte)) + if (pack_size >= irfox::kMsgBytes + irfox::kCrcBytes && + (i_data_buffer == pack_size * irfox::kBitPerByte)) { uint16_t crc_computed = 0; const bool crc_ok = crc_check(static_cast(pack_size - irfox::kCrcBytes), crc_computed); @@ -232,7 +287,8 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_ is_available = crc_ok; IrFoxEmitPacket pkt{}; - pkt.start_sample = static_cast(cell_start_s); + pkt.start_sample = static_cast(packet_start_valid_ ? packet_start_sample_ : cell_start_s); + pkt.data_start_sample = static_cast(packet_data_start_valid_ ? packet_data_start_sample_ : cell_start_s); pkt.end_sample = static_cast(cell_end_s); pkt.crc_ok = crc_ok; pkt.pack_size = static_cast(pack_size); @@ -256,29 +312,181 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const listen_start(t_us); - // Как IR_DecoderRaw: пауза между фронтами по lastEdgeTime при активном приёме кадра. - if (last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0 && is_recive) - check_timeout(t_us); - - last_edge_time_us = t_us; - last_edge_sample = sample; - const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs; - /** Визуализация: начало PRE с ближайшего спада в пределах ~3 битовых периодов (ИК-метка). */ + /** Firmware starts a preamble candidate only on its first rising edge after silence. */ 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; - } + (void)is_rising; return edge_s; }; + // Mirror IR_DecoderRaw::preambleProcessEdge. A frame may start only after + // a long silence and two mutually consistent rise-to-rise periods. + auto start_preamble_candidate = [&]() { + 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; + const uint32_t candidate_timeout_us = irmax * irfox::kPreambleCandidateTimeoutMult; + 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(long_silence_us) : + (t_us - prev_rise_us) > static_cast(long_silence_us); + if (!is_recive_raw && rising && enough_silence) + start_preamble_candidate(); + 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) + { + if ((t_us - preamble_candidate_last_edge_us_) > static_cast(candidate_timeout_us)) + start_preamble_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(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(preamble_mean_period_us_) / 2.0; + prev_rise_sample = sample + static_cast(std::llround( + static_cast(preamble_mean_period_us_) * 0.5 * static_cast(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(preamble_bubble_start_sample_); + pe.end_sample = static_cast(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. + if (last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0 && is_recive) + check_timeout(t_us); + last_edge_time_us = t_us; + last_edge_sample = sample; + if (rising) { const double delta_rp = t_us - prev_rise_us; @@ -357,78 +565,6 @@ 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(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: до текущего фронта (sample−1), чтобы охватить все 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(rise_period_us); - const uint64_t half_s = static_cast(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(preamble_bubble_start_sample_); - int64_t pe_end = static_cast(preamble_bubble_end_sample); - if (preamble_bubble_end_sample == 0 || pe_end < pe_start) - pe_end = static_cast(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) { last_processed_edge_us = t_us; diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h index c36d6c1..5e2d25a 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h @@ -8,11 +8,16 @@ enum IrFoxFrameType : uint8_t { IRF_FT_DATA_BIT = 1, IRF_FT_SYNC_BIT = 2, - IRF_FT_PACKET_OK = 3, + IRF_FT_PACKET_ACCEPTED = 3, IRF_FT_PACKET_CRC_FAIL = 4, IRF_FT_OVERFLOW = 5, IRF_FT_ABORT = 6, 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_PACKET_OK = IRF_FT_PACKET_ACCEPTED, }; /** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */ @@ -41,6 +46,8 @@ struct IrFoxEmitBit struct IrFoxEmitPacket { 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; bool crc_ok; uint8_t pack_size; @@ -101,6 +108,11 @@ private: uint64_t preamble_bubble_start_sample_ = 0; bool preamble_bubble_start_valid_ = 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; uint64_t last_edge_sample = 0; @@ -123,7 +135,18 @@ private: int8_t all_count = 0; uint16_t wrong_counter = 0; - int8_t preamb_front_counter = 0; + enum class PreambleState : uint8_t + { + 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; bool is_data = true; uint16_t i_data_buffer = 0; diff --git a/Analyzer/raw/IR_Fox/src/IrFoxPacketClassifier.h b/Analyzer/raw/IR_Fox/src/IrFoxPacketClassifier.h new file mode 100644 index 0000000..79309f0 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxPacketClassifier.h @@ -0,0 +1,172 @@ +#pragma once + +#include "IrFoxProtocolConstants.h" +#include + +/** + * 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((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(kMsgBytes + kAddrBytes + kAddrBytes + kCrcBytes); + case kMsgBack: + return static_cast(kMsgBytes + kAddrBytes + kCrcBytes); + case kMsgAccept: + return static_cast(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((static_cast(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 diff --git a/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h index 63ff42b..e13b8da 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h +++ b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h @@ -11,8 +11,12 @@ constexpr uint32_t kBitTakts = kBitActiveTakts + kBitPauseTakts; constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs; constexpr uint32_t kToleranceUs = 300U; -/** Мин. длительность плато (мкс) для потокового анти-глитча в анализаторе; согласовано с IR_INPUT_MIN_PULSE_US. */ -constexpr uint32_t kMinFilteredPulseUs = 10U; +/** + * Must match IR_INPUT_MIN_PULSE_US in the firmware configuration. The current + * 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 kMsgBytes = 1; @@ -27,6 +31,12 @@ constexpr uint8_t kDataByteSizeMax = constexpr uint8_t kPreambPulse = 3; 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 = 3U; /** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */ #ifndef IRFOX_SHORT_LOW_GLITCH_REJECT @@ -53,6 +63,19 @@ inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs) 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) { #if IRFOX_GLITCH_REJECT_PHASE_NUDGE diff --git a/Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp b/Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp new file mode 100644 index 0000000..2daecf1 --- /dev/null +++ b/Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp @@ -0,0 +1,115 @@ +#include "IrFoxDecoder.h" +#include +#include +#include + +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((crc << 1U) ^ poly) : static_cast(crc << 1U); + } + return crc; +} + +struct DecoderHarness +{ + IrFoxDecoder decoder; + std::vector packets; + std::vector events; + uint64_t phase = 0; + static constexpr uint32_t kFs = 1000000U; + + DecoderHarness() + { + decoder.reset(); + } + + void edge(uint64_t sample, bool rising) + { + decoder.processEdge(sample, rising, kFs, [this](const IrFoxEmitBit& event) { events.push_back(event); }, + [this](const IrFoxEmitPacket& packet) { packets.push_back(packet); }); + } + + void lockPreamble() + { + constexpr uint64_t first_rise = 40000; + 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(0x80U >> i)) != 0U); + if (emit_sync) + { + const bool sync = (value & 1U) == 0U; + for (uint8_t i = 0; i < irfox::kSyncBits; ++i) + emitCell(sync); + } + } +}; + +} // namespace + +int main() +{ + uint8_t packet[] = {0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00}; + packet[5] = crc8(packet, 5, irfox::kPoly1); + packet[6] = crc8(packet, 6, irfox::kPoly2); + + DecoderHarness valid; + valid.lockPreamble(); + for (uint8_t i = 0; i < sizeof packet; ++i) + valid.emitByte(packet[i], i + 1U != sizeof packet); + assert(valid.packets.size() == 1U); + assert(valid.packets[0].crc_ok); + assert(valid.packets[0].pack_size == sizeof packet); + assert(valid.packets[0].start_sample == 40000); + assert(valid.packets[0].start_sample < valid.packets[0].end_sample); + bool saw_preamble = false; + std::vector decoded_bytes; + for (const IrFoxEmitBit& event : valid.events) + { + if (event.frame_type == IRF_FT_PREAMBLE) + { + saw_preamble = true; + assert(event.start_sample == 40000); + } + if (event.frame_type == IRF_FT_DATA_BYTE) + decoded_bytes.push_back(static_cast(event.bit_value)); + } + assert(saw_preamble); + assert(decoded_bytes.size() == sizeof packet); + for (uint8_t i = 0; i < sizeof packet; ++i) + assert(decoded_bytes[i] == packet[i]); + + DecoderHarness too_short; + too_short.lockPreamble(); + too_short.emitByte(0xE1, false); + assert(too_short.packets.size() == 1U); + assert(!too_short.packets[0].crc_ok); + assert(too_short.packets[0].pack_size == 1U); + return 0; +} diff --git a/Analyzer/raw/IR_Fox/tests/IrFoxPacketClassifierTests.cpp b/Analyzer/raw/IR_Fox/tests/IrFoxPacketClassifierTests.cpp new file mode 100644 index 0000000..0feb38d --- /dev/null +++ b/Analyzer/raw/IR_Fox/tests/IrFoxPacketClassifierTests.cpp @@ -0,0 +1,43 @@ +#include "IrFoxPacketClassifier.h" +#include +#include + +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; +}