analyzer(IR_Fox): классификатор пакетов, настройки и результаты анализатора Saleae, тесты (работа Даши, снято с рабочей копии 09.09)

This commit is contained in:
2026-09-09 17:08:30 +03:00
parent 6c97d33c7c
commit fe4512d47f
14 changed files with 1086 additions and 185 deletions

View File

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