From 98c5eb0ef41cb64ec7aa661f81d50f0aa9d7b14e Mon Sep 17 00:00:00 2001 From: DashyFox Date: Wed, 9 Sep 2026 18:15:44 +0300 Subject: [PATCH] hotfix(analyzer): sync IR Fox RX terminal behavior --- Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp | 91 +++++++- .../raw/IR_Fox/src/IrFoxAnalyzerResults.cpp | 45 +++- Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp | 191 ++++++++++----- Analyzer/raw/IR_Fox/src/IrFoxDecoder.h | 47 +++- .../raw/IR_Fox/src/IrFoxProtocolConstants.h | 2 +- .../raw/IR_Fox/tests/IrFoxDecoderTests.cpp | 219 +++++++++++++++--- 6 files changed, 486 insertions(+), 109 deletions(-) diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp index da03579..20fef88 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp @@ -85,6 +85,22 @@ static const char* message_type_icon(uint8_t message_type) } } +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 ""; + } +} + static std::string packet_icon(const IrFoxPacketDecision& decision, IrFoxPacketIconMode mode) { const char* status = packet_status_icon(decision.outcome); @@ -229,6 +245,73 @@ void IrFoxAnalyzer::WorkerThread() 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(pending_byte_frames.back().start_sample); + } + else + { + frame.mStartingSampleInclusive = static_cast(detailed_presentation ? + terminal.detail_start_sample : terminal.start_sample); + } + frame.mEndingSampleInclusive = static_cast(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); + pending_byte_frames.clear(); + + std::string short_text; + if (terminal.reason == IrFoxTerminalReason::Timeout) + short_text = "❌ TIMEOUT"; + else + { + short_text = "❌ ABORT"; + const char* cause = terminal_abort_cause_text(terminal.cause); + 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(); + 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(terminal.end_sample), AnalyzerResults::ErrorX, + mSettings.mInputChannel); + note_legacy_frame(); + }; + IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) { const IrFoxPacketDecision decision = irfox::classifyPacket(p.data_bytes, p.pack_size, p.crc_ok, mSettings.mReceiverAddress); @@ -378,7 +461,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, bit_events, on_pkt); + decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal); last_dec_edge_sample = pending[0].sample; last_dec_edge_valid = true; pending.erase(pending.begin()); @@ -390,7 +473,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, bit_events, on_pkt); + decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal); last_dec_edge_sample = pending[0].sample; last_dec_edge_valid = true; pending.erase(pending.begin()); @@ -399,7 +482,7 @@ void IrFoxAnalyzer::WorkerThread() } if (pending.size() == 1) { - decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt); + decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal); last_dec_edge_sample = pending[0].sample; last_dec_edge_valid = true; pending.clear(); @@ -428,7 +511,7 @@ void IrFoxAnalyzer::WorkerThread() } flush_pending_tail(); - decoder.flushEnd(mIr->GetSampleNumber(), fs, bit_events, on_pkt); + decoder.flushEnd(mIr->GetSampleNumber(), fs, bit_events, on_pkt, on_terminal); if (detailed_presentation) flush_pending_bytes(); diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp index 36ebecf..eb86228 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp @@ -74,18 +74,21 @@ void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, } case IRF_FT_PREAMBLE: + { + AddResultString("📡"); + AddResultString("📡 PRE"); + break; + } + case IRF_FT_OVERFLOW: case IRF_FT_ABORT: + case IRF_FT_TIMEOUT: { - if (frame.mType == IRF_FT_OVERFLOW) - AddResultString("OVF"); - else if (frame.mType == IRF_FT_ABORT) - AddResultString("SYNC!"); - else - { - AddResultString("📡"); - AddResultString("📡 PRE"); - } + if (add_cached_text()) + break; + AddResultString("❌"); + AddResultString(frame.mType == IRF_FT_TIMEOUT ? "❌ TIMEOUT" : + (frame.mType == IRF_FT_OVERFLOW ? "❌ ABORT OVF" : "❌ ABORT")); break; } @@ -147,10 +150,28 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp typ = "IGNORE_ADDR"; break; case IRF_FT_OVERFLOW: - typ = "OVF"; + typ = "ABORT_OVF"; break; case IRF_FT_ABORT: - typ = "ABORT"; + switch (static_cast((frame.mData2 >> 48) & 0xFFull)) + { + 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; case IRF_FT_PREAMBLE: typ = "PRE"; @@ -164,7 +185,7 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp U64 bit_idx = 0; U32 err_l = 0, err_h = 0, err_o = 0; - if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT) + if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT || frame.mType == IRF_FT_TIMEOUT) { bit_idx = frame.mData2 & 0xFFFFull; err_l = static_cast((frame.mData2 >> 16) & 0xFFull); diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp index 008bab1..9733fb4 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp @@ -52,6 +52,20 @@ bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out) 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() { err_low_signal = err_high_signal = err_other = 0; @@ -65,7 +79,7 @@ void IrFoxDecoder::first_rx() i_sync_bit = 0; err_sync_bit = 0; is_wrong_pack = false; - is_preamb = true; + is_preamb = false; is_recive = false; is_recive_raw = false; msg_type_receive = 0; @@ -78,12 +92,61 @@ void IrFoxDecoder::first_rx() 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; + 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(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(start_sample); + terminal.detail_start_sample = static_cast( + packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample); + terminal.end_sample = static_cast(end_sample); + terminal.reason = reason; + terminal.cause = cause; + terminal.message_type = i_data_buffer >= irfox::kBitPerByte ? + static_cast((data_buffer[0] >> 5U) & 0x07U) : 0xFFU; + terminal.declared_size = static_cast(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(timeout_us)) + preamble_reset_to_idle(); } void IrFoxDecoder::listen_start(double t_us) @@ -97,13 +160,20 @@ void IrFoxDecoder::listen_start(double t_us) } } -void IrFoxDecoder::check_timeout(double t_us) +void IrFoxDecoder::check_timeout(double t_us, uint32_t fs, const IrFoxOnTerminal& on_terminal) { if (!is_recive) return; const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); if (t_us - last_edge_time_us > irmax * 2.0) { + const uint64_t timeout_us = static_cast(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(fs)) / 1000000ULL; + emit_terminal(IrFoxTerminalReason::Timeout, IrFoxAbortCause::None, + last_edge_sample + timeout_samples, on_terminal); // Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM. is_recive = false; msg_type_receive = 0; @@ -114,29 +184,23 @@ 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) + const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, + const IrFoxOnTerminal& on_terminal, IrFoxEmitBitMode emit_mode) { if (i_data_buffer >= irfox::kDataByteSizeMax * 8u) { - if (!is_buffer_overflow && on_bit) - { - IrFoxEmitBit e{}; - e.start_sample = static_cast(cell_start_s); - e.end_sample = static_cast(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; + abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::Overflow, on_terminal); + return; } if (is_buffer_overflow || is_preamb || is_wrong_pack) { - // Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма. - first_rx(); + // Firmware treats overflow/invalid frame state as a terminal abort and + // immediately permits a fresh preamble candidate. + 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; } @@ -220,14 +284,10 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_ const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits); if (fatal_sync) is_wrong_pack = true; - if (on_bit && fatal_sync) + if (fatal_sync) { - IrFoxEmitBit e{static_cast(cell_start_s), static_cast(cell_end_s), IRF_FT_ABORT, - 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); + abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadSync, on_terminal); + return; } } } @@ -253,22 +313,12 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_ { 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) + // Emit a terminal abort so the capture explains why no packet follows. + if (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); + abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadLength, on_terminal); + return; } } @@ -304,14 +354,20 @@ 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, - const IrFoxOnPacket& on_pkt) + const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal) { 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); uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U; - - listen_start(t_us); - 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. */ @@ -338,7 +394,6 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const }; 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; @@ -350,7 +405,16 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const 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(); + // 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. @@ -364,9 +428,6 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const 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) { @@ -482,8 +543,6 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const // 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; @@ -591,9 +650,11 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us)) { if (high_time_us > low_time_us) - write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble); + write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, + IrFoxEmitBitMode::WithBubble); else - write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble); + write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, + IrFoxEmitBitMode::WithBubble); } else { @@ -683,13 +744,15 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const if (i == low_count - 1 && invert_err) { invert_err = false; - write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, + IrFoxEmitBitMode::Quiet); merge_warn = true; append_merge(row_is_data, true); } else { - write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, + IrFoxEmitBitMode::Quiet); append_merge(row_is_data, false); } } @@ -700,13 +763,15 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const if (i == high_count - 1 && invert_err) { invert_err = false; - write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, + IrFoxEmitBitMode::Quiet); merge_warn = true; append_merge(row_is_data, false); } else { - write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); + write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal, + IrFoxEmitBitMode::Quiet); append_merge(row_is_data, true); } } @@ -719,11 +784,13 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const 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); + check_timeout(t_us, fs, on_terminal); listen_start(t_us); - check_timeout(t_us); + expire_preamble_candidate(t_us); (void)on_bit; (void)on_pkt; } diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h index 5e2d25a..0e7e0c9 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h @@ -17,6 +17,7 @@ enum IrFoxFrameType : uint8_t 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, }; @@ -57,16 +58,47 @@ struct IrFoxEmitPacket 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; using IrFoxOnPacket = std::function; +using IrFoxOnTerminal = std::function; class IrFoxDecoder { public: void reset(); void processEdge(uint64_t sample, bool rising, 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 IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal); + void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, + const IrFoxOnTerminal& on_terminal); private: static uint16_t ceil_div_u16(uint16_t val, uint16_t divider); @@ -74,10 +106,17 @@ private: bool crc_check(uint8_t len, uint16_t& crc_out); 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 check_timeout(double t_us); + void check_timeout(double t_us, uint32_t sample_rate_hz, const IrFoxOnTerminal& on_terminal); 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 IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal, IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble); double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); } diff --git a/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h index e13b8da..f188105 100644 --- a/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h +++ b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h @@ -36,7 +36,7 @@ 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; +constexpr uint32_t kPreambleCandidateTimeoutMult = 1U; /** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */ #ifndef IRFOX_SHORT_LOW_GLITCH_REJECT diff --git a/Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp b/Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp index 2daecf1..3987b06 100644 --- a/Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp +++ b/Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp @@ -1,8 +1,20 @@ #include "IrFoxDecoder.h" -#include +#include +#include +#include #include #include +#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) @@ -22,23 +34,33 @@ struct DecoderHarness IrFoxDecoder decoder; std::vector packets; std::vector events; + std::vector terminals; uint64_t phase = 0; + bool collect_bit_events = true; static constexpr uint32_t kFs = 1000000U; - DecoderHarness() + explicit DecoderHarness(bool collect_bits = true) : collect_bit_events(collect_bits) { 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); }); + 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() { - constexpr uint64_t first_rise = 40000; + lockPreambleAt(40000U); + } + + void lockPreambleAt(uint64_t first_rise) + { constexpr uint64_t period = irfox::kBitTimeUs * 3U; edge(first_rise, true); edge(first_rise + 700U, false); @@ -69,25 +91,73 @@ struct DecoderHarness emitCell(sync); } } + + void emitPacket(const std::vector& 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 makeValidPacket() +{ + std::vector 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(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(next_preamble)); + CHECK(harness.terminals.size() == 1U); +} + } // 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); + const std::vector packet = makeValidPacket(); 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); + 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 decoded_bytes; for (const IrFoxEmitBit& event : valid.events) @@ -95,21 +165,118 @@ int main() if (event.frame_type == IRF_FT_PREAMBLE) { saw_preamble = true; - assert(event.start_sample == 40000); + CHECK(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]); + CHECK(saw_preamble); + CHECK(decoded_bytes.size() == packet.size()); + for (size_t i = 0; i < packet.size(); ++i) + CHECK(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); + 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(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(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(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(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(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(adjacent_preamble)); return 0; }