hotfix(analyzer): sync IR Fox RX terminal behavior

This commit is contained in:
2026-09-09 18:15:44 +03:00
parent 78169d1c59
commit 98c5eb0ef4
6 changed files with 486 additions and 109 deletions

View File

@ -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) static std::string packet_icon(const IrFoxPacketDecision& decision, IrFoxPacketIconMode mode)
{ {
const char* status = packet_status_icon(decision.outcome); const char* status = packet_status_icon(decision.outcome);
@ -229,6 +245,73 @@ void IrFoxAnalyzer::WorkerThread()
add_event_frame(e); add_event_frame(e);
}; };
// Terminal events are independent of per-bit rendering. Overview passes an
// empty on_bit callback for speed, but must still show an interrupted frame.
IrFoxOnTerminal on_terminal = [&](const IrFoxEmitTerminal& terminal) {
Frame frame;
if (detailed_presentation && !pending_byte_frames.empty())
{
// Preserve every completed byte except the final one. The terminal
// frame replaces that last byte so legacy frames never overlap.
for (size_t i = 0; i + 1 < pending_byte_frames.size(); ++i)
add_event_frame(pending_byte_frames[i]);
frame.mStartingSampleInclusive = static_cast<S64>(pending_byte_frames.back().start_sample);
}
else
{
frame.mStartingSampleInclusive = static_cast<S64>(detailed_presentation ?
terminal.detail_start_sample : terminal.start_sample);
}
frame.mEndingSampleInclusive = static_cast<S64>(terminal.end_sample);
if (frame.mStartingSampleInclusive > frame.mEndingSampleInclusive)
frame.mStartingSampleInclusive = frame.mEndingSampleInclusive;
frame.mType = terminal.reason == IrFoxTerminalReason::Timeout ? IRF_FT_TIMEOUT :
(terminal.cause == IrFoxAbortCause::Overflow ? IRF_FT_OVERFLOW : IRF_FT_ABORT);
frame.mData1 = terminal.declared_size;
frame.mData2 = U64(terminal.received_bits) |
(U64(terminal.err_low) << 16) | (U64(terminal.err_high) << 24) | (U64(terminal.err_other) << 32) |
(U64(terminal.message_type) << 40) | (U64(terminal.cause) << 48) | (U64(terminal.reason) << 56);
frame.mFlags = DISPLAY_AS_ERROR_FLAG;
const U64 fid = mResults->AddFrame(frame);
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<IrFoxCachedFrameText>();
cached_text->bubble_texts[0] = "";
cached_text->bubble_texts[1] = short_text;
cached_text->bubble_texts[2] = detail;
cached_text->bubble_text_count = 3;
mResults->CacheFrameText(fid, cached_text);
if (detailed_presentation)
mResults->AddMarker(static_cast<U64>(terminal.end_sample), AnalyzerResults::ErrorX,
mSettings.mInputChannel);
note_legacy_frame();
};
IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) { IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) {
const IrFoxPacketDecision decision = const IrFoxPacketDecision decision =
irfox::classifyPacket(p.data_bytes, p.pack_size, p.crc_ok, mSettings.mReceiverAddress); irfox::classifyPacket(p.data_bytes, p.pack_size, p.crc_ok, mSettings.mReceiverAddress);
@ -378,7 +461,7 @@ void IrFoxAnalyzer::WorkerThread()
return; return;
if (pending[1].sample - pending[0].sample < min_seg_samples) if (pending[1].sample - pending[0].sample < min_seg_samples)
continue; continue;
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt); 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_sample = pending[0].sample;
last_dec_edge_valid = true; last_dec_edge_valid = true;
pending.erase(pending.begin()); pending.erase(pending.begin());
@ -390,7 +473,7 @@ void IrFoxAnalyzer::WorkerThread()
strip_vs_last_decoder(); strip_vs_last_decoder();
while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples) while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples)
{ {
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt); 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_sample = pending[0].sample;
last_dec_edge_valid = true; last_dec_edge_valid = true;
pending.erase(pending.begin()); pending.erase(pending.begin());
@ -399,7 +482,7 @@ void IrFoxAnalyzer::WorkerThread()
} }
if (pending.size() == 1) if (pending.size() == 1)
{ {
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt); 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_sample = pending[0].sample;
last_dec_edge_valid = true; last_dec_edge_valid = true;
pending.clear(); pending.clear();
@ -428,7 +511,7 @@ void IrFoxAnalyzer::WorkerThread()
} }
flush_pending_tail(); 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) if (detailed_presentation)
flush_pending_bytes(); flush_pending_bytes();

View File

@ -74,18 +74,21 @@ void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel,
} }
case IRF_FT_PREAMBLE: case IRF_FT_PREAMBLE:
case IRF_FT_OVERFLOW:
case IRF_FT_ABORT:
{
if (frame.mType == IRF_FT_OVERFLOW)
AddResultString("OVF");
else if (frame.mType == IRF_FT_ABORT)
AddResultString("SYNC!");
else
{ {
AddResultString("📡"); AddResultString("📡");
AddResultString("📡 PRE"); AddResultString("📡 PRE");
break;
} }
case IRF_FT_OVERFLOW:
case IRF_FT_ABORT:
case IRF_FT_TIMEOUT:
{
if (add_cached_text())
break;
AddResultString("");
AddResultString(frame.mType == IRF_FT_TIMEOUT ? "❌ TIMEOUT" :
(frame.mType == IRF_FT_OVERFLOW ? "❌ ABORT OVF" : "❌ ABORT"));
break; break;
} }
@ -147,11 +150,29 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
typ = "IGNORE_ADDR"; typ = "IGNORE_ADDR";
break; break;
case IRF_FT_OVERFLOW: case IRF_FT_OVERFLOW:
typ = "OVF"; typ = "ABORT_OVF";
break; break;
case IRF_FT_ABORT: case IRF_FT_ABORT:
switch (static_cast<IrFoxAbortCause>((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"; typ = "ABORT";
break; break;
}
break;
case IRF_FT_TIMEOUT:
typ = "TIMEOUT";
break;
case IRF_FT_PREAMBLE: case IRF_FT_PREAMBLE:
typ = "PRE"; typ = "PRE";
break; break;
@ -164,7 +185,7 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
U64 bit_idx = 0; U64 bit_idx = 0;
U32 err_l = 0, err_h = 0, err_o = 0; U32 err_l = 0, err_h = 0, err_o = 0;
if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT) if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT || frame.mType == IRF_FT_TIMEOUT)
{ {
bit_idx = frame.mData2 & 0xFFFFull; bit_idx = frame.mData2 & 0xFFFFull;
err_l = static_cast<U32>((frame.mData2 >> 16) & 0xFFull); err_l = static_cast<U32>((frame.mData2 >> 16) & 0xFFull);

View File

@ -52,6 +52,20 @@ bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out)
return ok; return ok;
} }
void IrFoxDecoder::preamble_reset_to_idle()
{
preamble_state_ = PreambleState::Idle;
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
preamble_candidate_last_edge_us_ = 0;
preamble_candidate_first_rise_us_ = 0;
preamble_candidate_first_rise_valid_ = false;
preamble_bubble_start_valid_ = false;
is_preamb = false;
is_wrong_pack = false;
is_buffer_overflow = false;
}
void IrFoxDecoder::first_rx() void IrFoxDecoder::first_rx()
{ {
err_low_signal = err_high_signal = err_other = 0; err_low_signal = err_high_signal = err_other = 0;
@ -65,7 +79,7 @@ void IrFoxDecoder::first_rx()
i_sync_bit = 0; i_sync_bit = 0;
err_sync_bit = 0; err_sync_bit = 0;
is_wrong_pack = false; is_wrong_pack = false;
is_preamb = true; is_preamb = false;
is_recive = false; is_recive = false;
is_recive_raw = false; is_recive_raw = false;
msg_type_receive = 0; msg_type_receive = 0;
@ -78,12 +92,61 @@ void IrFoxDecoder::first_rx()
packet_data_start_sample_ = 0; packet_data_start_sample_ = 0;
packet_data_start_valid_ = false; packet_data_start_valid_ = false;
byte_start_sample_ = 0; byte_start_sample_ = 0;
preamble_state_ = PreambleState::Idle; preamble_reset_to_idle();
preamble_good_periods_ = 0; }
preamble_mean_period_us_ = 0;
preamble_candidate_last_edge_us_ = 0; void IrFoxDecoder::release_preamble_guard(double t_us)
preamble_candidate_first_rise_us_ = 0; {
preamble_candidate_first_rise_valid_ = false; const uint32_t long_silence_us = irfox::irTimeoutUs(rise_sync_time_us) * 2U;
// Mirror IR_DecoderRaw::releasePreambleGuard. A negative value is the
// floating-point equivalent of the firmware's wrap-safe unsigned offset.
prev_rise_us = t_us - static_cast<double>(long_silence_us) - 1.0;
}
void IrFoxDecoder::emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
const IrFoxOnTerminal& on_terminal) const
{
if (!on_terminal)
return;
IrFoxEmitTerminal terminal{};
const uint64_t start_sample = packet_start_valid_ ? packet_start_sample_ :
(packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
terminal.start_sample = static_cast<int64_t>(start_sample);
terminal.detail_start_sample = static_cast<int64_t>(
packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
terminal.end_sample = static_cast<int64_t>(end_sample);
terminal.reason = reason;
terminal.cause = cause;
terminal.message_type = i_data_buffer >= irfox::kBitPerByte ?
static_cast<uint8_t>((data_buffer[0] >> 5U) & 0x07U) : 0xFFU;
terminal.declared_size = static_cast<uint8_t>(pack_size);
terminal.received_bits = i_data_buffer;
terminal.err_low = err_low_signal;
terminal.err_high = err_high_signal;
terminal.err_other = err_other;
on_terminal(terminal);
}
void IrFoxDecoder::abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
const IrFoxOnTerminal& on_terminal)
{
emit_terminal(IrFoxTerminalReason::Abort, cause, end_sample, on_terminal);
is_recive = false;
is_recive_raw = false;
msg_type_receive = 0;
first_rx();
release_preamble_guard(t_us);
}
void IrFoxDecoder::expire_preamble_candidate(double t_us)
{
if (preamble_state_ != PreambleState::Candidate)
return;
const uint32_t timeout_us =
irfox::irTimeoutUs(rise_sync_time_us) * irfox::kPreambleCandidateTimeoutMult;
if ((t_us - preamble_candidate_last_edge_us_) > static_cast<double>(timeout_us))
preamble_reset_to_idle();
} }
void IrFoxDecoder::listen_start(double t_us) void IrFoxDecoder::listen_start(double t_us)
@ -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) if (!is_recive)
return; return;
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
if (t_us - last_edge_time_us > irmax * 2.0) if (t_us - last_edge_time_us > irmax * 2.0)
{ {
const uint64_t timeout_us = static_cast<uint64_t>(irmax) * 2U;
// The callback fires only after the strict > 2T boundary, but the terminal
// frame owns samples only through 2T. This leaves a following edge free to
// seed the next preamble without overlapping inclusive Saleae frames.
const uint64_t timeout_samples = (timeout_us * static_cast<uint64_t>(fs)) / 1000000ULL;
emit_terminal(IrFoxTerminalReason::Timeout, IrFoxAbortCause::None,
last_edge_sample + timeout_samples, on_terminal);
// Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM. // Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM.
is_recive = false; is_recive = false;
msg_type_receive = 0; msg_type_receive = 0;
@ -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, 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 (i_data_buffer >= irfox::kDataByteSizeMax * 8u)
{ {
if (!is_buffer_overflow && on_bit)
{
IrFoxEmitBit e{};
e.start_sample = static_cast<int64_t>(cell_start_s);
e.end_sample = static_cast<int64_t>(cell_end_s);
e.frame_type = IRF_FT_OVERFLOW;
e.mflags = DISPLAY_AS_ERROR_FLAG;
fill_err_snapshot(e);
std::strncpy(e.bubble_text, "OVF", sizeof e.bubble_text);
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
on_bit(e);
}
is_buffer_overflow = true; is_buffer_overflow = true;
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::Overflow, on_terminal);
return;
} }
if (is_buffer_overflow || is_preamb || is_wrong_pack) if (is_buffer_overflow || is_preamb || is_wrong_pack)
{ {
// Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма. // Firmware treats overflow/invalid frame state as a terminal abort and
first_rx(); // 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; 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); const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits);
if (fatal_sync) if (fatal_sync)
is_wrong_pack = true; is_wrong_pack = true;
if (on_bit && fatal_sync) if (fatal_sync)
{ {
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_ABORT, abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadSync, on_terminal);
0, 0, DISPLAY_AS_ERROR_FLAG, false, 0, 0, 0}; return;
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);
} }
} }
} }
@ -253,22 +313,12 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
{ {
pack_size = static_cast<uint16_t>(data_buffer[0] & 0x1Fu); pack_size = static_cast<uint16_t>(data_buffer[0] & 0x1Fu);
// The receiver rejects a length that cannot contain its two CRC bytes. // The receiver rejects a length that cannot contain its two CRC bytes.
// Emit the rejection here so the capture explains why no packet follows. // Emit a terminal abort so the capture explains why no packet follows.
if (pack_size != 0 && pack_size < irfox::kMsgBytes + irfox::kCrcBytes) if (pack_size < irfox::kMsgBytes + irfox::kCrcBytes)
{ {
is_wrong_pack = true; is_wrong_pack = true;
IrFoxEmitPacket pkt{}; abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadLength, on_terminal);
pkt.start_sample = static_cast<int64_t>(packet_start_valid_ ? packet_start_sample_ : cell_start_s); return;
pkt.data_start_sample = static_cast<int64_t>(packet_data_start_valid_ ? packet_data_start_sample_ : cell_start_s);
pkt.end_sample = static_cast<int64_t>(cell_end_s);
pkt.crc_ok = false;
pkt.pack_size = static_cast<uint8_t>(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);
} }
} }
@ -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, 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); const double t_us = sample_to_us(sample, fs);
// Firmware advances terminal timers in this order while no queued edge is
// pending. For an offline capture, do the equivalent immediately before the
// next timestamped edge is consumed.
check_timeout(t_us, fs, on_terminal);
listen_start(t_us);
expire_preamble_candidate(t_us);
// A timeout/abort may have restored the nominal adaptive bit period.
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U; uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U;
listen_start(t_us);
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs; 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. */ /** 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 long_silence_us = irmax * 2U;
const uint32_t candidate_timeout_us = irmax * irfox::kPreambleCandidateTimeoutMult;
if (preamble_state_ == PreambleState::Locked && !is_recive_raw) if (preamble_state_ == PreambleState::Locked && !is_recive_raw)
{ {
preamble_state_ = PreambleState::Idle; 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<double>(long_silence_us) : const bool enough_silence = prev_rise_us == 0.0 ? t_us > static_cast<double>(long_silence_us) :
(t_us - prev_rise_us) > static_cast<double>(long_silence_us); (t_us - prev_rise_us) > static_cast<double>(long_silence_us);
if (!is_recive_raw && rising && enough_silence) if (!is_recive_raw && rising && enough_silence)
{
start_preamble_candidate(); 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 else
{ {
// IR_DecoderRaw ignores idle edges until a valid preamble candidate starts. // 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 (preamble_state_ == PreambleState::Candidate)
{ {
if ((t_us - preamble_candidate_last_edge_us_) > static_cast<double>(candidate_timeout_us))
start_preamble_candidate();
preamble_candidate_last_edge_us_ = t_us; preamble_candidate_last_edge_us_ = t_us;
if (!rising) 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 // As in processDecodedFront, the edge becomes the timing reference only
// after the preamble state machine has allowed it through. // 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_time_us = t_us;
last_edge_sample = sample; 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 (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us))
{ {
if (high_time_us > low_time_us) if (high_time_us > low_time_us)
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble); write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
IrFoxEmitBitMode::WithBubble);
else 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 else
{ {
@ -683,13 +744,15 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
if (i == low_count - 1 && invert_err) if (i == low_count - 1 && invert_err)
{ {
invert_err = false; invert_err = false;
write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
IrFoxEmitBitMode::Quiet);
merge_warn = true; merge_warn = true;
append_merge(row_is_data, true); append_merge(row_is_data, true);
} }
else else
{ {
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, 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); 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) if (i == high_count - 1 && invert_err)
{ {
invert_err = false; invert_err = false;
write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet); write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
IrFoxEmitBitMode::Quiet);
merge_warn = true; merge_warn = true;
append_merge(row_is_data, false); append_merge(row_is_data, false);
} }
else else
{ {
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, 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); 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; have_last_processed = true;
} }
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt) void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
const IrFoxOnTerminal& on_terminal)
{ {
const double t_us = sample_to_us(last_sample, fs); const double t_us = sample_to_us(last_sample, fs);
check_timeout(t_us, fs, on_terminal);
listen_start(t_us); listen_start(t_us);
check_timeout(t_us); expire_preamble_candidate(t_us);
(void)on_bit; (void)on_bit;
(void)on_pkt; (void)on_pkt;
} }

View File

@ -17,6 +17,7 @@ enum IrFoxFrameType : uint8_t
IRF_FT_PACKET_RAW_ONLY = 9, IRF_FT_PACKET_RAW_ONLY = 9,
IRF_FT_PACKET_IGNORED_ADDRESS = 10, IRF_FT_PACKET_IGNORED_ADDRESS = 10,
IRF_FT_DATA_BYTE = 11, IRF_FT_DATA_BYTE = 11,
IRF_FT_TIMEOUT = 12,
IRF_FT_PACKET_OK = IRF_FT_PACKET_ACCEPTED, IRF_FT_PACKET_OK = IRF_FT_PACKET_ACCEPTED,
}; };
@ -57,16 +58,47 @@ struct IrFoxEmitPacket
uint8_t data_bytes[irfox::kDataByteSizeMax]; uint8_t data_bytes[irfox::kDataByteSizeMax];
}; };
enum class IrFoxTerminalReason : uint8_t
{
Abort,
Timeout,
};
enum class IrFoxAbortCause : uint8_t
{
None,
BadSync,
BadLength,
Overflow,
};
struct IrFoxEmitTerminal
{
int64_t start_sample;
int64_t detail_start_sample;
int64_t end_sample;
IrFoxTerminalReason reason;
IrFoxAbortCause cause;
uint8_t message_type;
uint8_t declared_size;
uint16_t received_bits;
uint8_t err_low;
uint8_t err_high;
uint8_t err_other;
};
using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>; using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>;
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>; using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
using IrFoxOnTerminal = std::function<void(const IrFoxEmitTerminal&)>;
class IrFoxDecoder class IrFoxDecoder
{ {
public: public:
void reset(); void reset();
void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit,
const IrFoxOnPacket& on_pkt); const 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); void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
const IrFoxOnTerminal& on_terminal);
private: private:
static uint16_t ceil_div_u16(uint16_t val, uint16_t divider); static uint16_t ceil_div_u16(uint16_t val, uint16_t divider);
@ -74,10 +106,17 @@ private:
bool crc_check(uint8_t len, uint16_t& crc_out); bool crc_check(uint8_t len, uint16_t& crc_out);
void first_rx(); void first_rx();
void preamble_reset_to_idle();
void release_preamble_guard(double t_us);
void emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
const IrFoxOnTerminal& on_terminal) const;
void abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
const IrFoxOnTerminal& on_terminal);
void expire_preamble_candidate(double t_us);
void listen_start(double t_us); void listen_start(double t_us);
void check_timeout(double t_us); 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, 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); IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble);
double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); } double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); }

View File

@ -36,7 +36,7 @@ constexpr uint8_t kPreambleJitterPct = 18U;
constexpr uint32_t kPreambleJitterUsMin = 80U; constexpr uint32_t kPreambleJitterUsMin = 80U;
constexpr uint32_t kPreamblePeriodMinFactorPct = 220U; constexpr uint32_t kPreamblePeriodMinFactorPct = 220U;
constexpr uint32_t kPreamblePeriodMaxFactorPct = 340U; constexpr uint32_t kPreamblePeriodMaxFactorPct = 340U;
constexpr uint32_t kPreambleCandidateTimeoutMult = 3U; constexpr uint32_t kPreambleCandidateTimeoutMult = 1U;
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */ /** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT #ifndef IRFOX_SHORT_LOW_GLITCH_REJECT

View File

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