mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-18 19:13:58 +00:00
hotfix(analyzer): sync IR Fox RX terminal behavior
This commit is contained in:
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -74,18 +74,21 @@ void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
case IRF_FT_PREAMBLE:
|
case IRF_FT_PREAMBLE:
|
||||||
|
{
|
||||||
|
AddResultString("📡");
|
||||||
|
AddResultString("📡 PRE");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case IRF_FT_OVERFLOW:
|
case IRF_FT_OVERFLOW:
|
||||||
case IRF_FT_ABORT:
|
case IRF_FT_ABORT:
|
||||||
|
case IRF_FT_TIMEOUT:
|
||||||
{
|
{
|
||||||
if (frame.mType == IRF_FT_OVERFLOW)
|
if (add_cached_text())
|
||||||
AddResultString("OVF");
|
break;
|
||||||
else if (frame.mType == IRF_FT_ABORT)
|
AddResultString("❌");
|
||||||
AddResultString("SYNC!");
|
AddResultString(frame.mType == IRF_FT_TIMEOUT ? "❌ TIMEOUT" :
|
||||||
else
|
(frame.mType == IRF_FT_OVERFLOW ? "❌ ABORT OVF" : "❌ ABORT"));
|
||||||
{
|
|
||||||
AddResultString("📡");
|
|
||||||
AddResultString("📡 PRE");
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,10 +150,28 @@ 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:
|
||||||
typ = "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";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IRF_FT_TIMEOUT:
|
||||||
|
typ = "TIMEOUT";
|
||||||
break;
|
break;
|
||||||
case IRF_FT_PREAMBLE:
|
case IRF_FT_PREAMBLE:
|
||||||
typ = "PRE";
|
typ = "PRE";
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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); }
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user