Files
IR-protocol/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp

481 lines
14 KiB
C++

#include "IrFoxAnalyzer.h"
#include "IrFoxAnalyzerSettings.h"
#include "IrFoxDecoder.h"
#include "IrFoxPacketClassifier.h"
#include <AnalyzerChannelData.h>
#include <AnalyzerResults.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
IrFoxAnalyzer::IrFoxAnalyzer()
: Analyzer2(),
mSettings(),
mSimulationInitilized(false)
{
SetAnalyzerSettings(&mSettings);
UseFrameV2();
}
IrFoxAnalyzer::~IrFoxAnalyzer()
{
KillThread();
}
void IrFoxAnalyzer::SetupResults()
{
mResults.reset(new IrFoxAnalyzerResults(this, &mSettings));
SetAnalyzerResults(mResults.get());
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
}
static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_bytes = 64)
{
static const char* hd = "0123456789abcdef";
const size_t m = n < max_bytes ? n : max_bytes;
for (size_t i = 0; i < m; i++)
{
s.push_back(hd[p[i] >> 4]);
s.push_back(hd[p[i] & 0xFu]);
if (i + 1 < m)
s.push_back(' ');
}
if (n > max_bytes)
s += "...";
}
static const char* packet_status_icon(IrFoxPacketOutcome outcome)
{
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 "⚠️";
}
static const char* message_type_icon(uint8_t message_type)
{
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);
mResults->ClearCachedFrameText();
const U32 fs = GetSampleRate();
IrFoxDecoder decoder;
decoder.reset();
/** 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
{
U64 sample;
bool rising;
};
std::vector<RawEdge> pending;
U64 last_dec_edge_sample = 0;
bool last_dec_edge_valid = false;
auto collapse_short_pairs = [&]() {
for (size_t i = 0; i + 1 < pending.size();)
{
if (pending[i + 1].sample - pending[i].sample < min_seg_samples)
{
pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i),
pending.begin() + static_cast<std::ptrdiff_t>(i + 2));
if (i > 0)
--i;
}
else
++i;
}
};
auto strip_vs_last_decoder = [&]() {
for (;;)
{
collapse_short_pairs();
if (!last_dec_edge_valid || pending.empty())
return;
if (pending[0].sample - last_dec_edge_sample >= min_seg_samples)
return;
pending.erase(pending.begin());
}
};
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);
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);
frame.mType = e.frame_type;
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;
mResults->AddFrame(frame);
note_legacy_frame();
};
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)
{
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;
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.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);
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);
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);
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)
{
mResults->CommitResults();
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 (;;)
{
collapse_short_pairs();
strip_vs_last_decoder();
if (pending.size() < 2)
return;
if (pending[1].sample - pending[0].sample < min_seg_samples)
continue;
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.erase(pending.begin());
}
};
auto flush_pending_tail = [&]() {
collapse_short_pairs();
strip_vs_last_decoder();
while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples)
{
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.erase(pending.begin());
collapse_short_pairs();
strip_vs_last_decoder();
}
if (pending.size() == 1)
{
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();
}
};
for (;;)
{
CheckIfThreadShouldExit();
const U64 segment_start = mIr->GetSampleNumber();
const BitState level = mIr->GetBitState();
mIr->AdvanceToNextEdge();
const U64 edge_sample = mIr->GetSampleNumber();
if (edge_sample == segment_start)
break;
const BitState new_level = mIr->GetBitState();
const bool rising = (new_level == BIT_HIGH);
pending.push_back(RawEdge{edge_sample, rising});
emit_confirmed_edges();
ReportProgress(edge_sample);
}
flush_pending_tail();
decoder.flushEnd(mIr->GetSampleNumber(), fs, bit_events, on_pkt);
if (detailed_presentation)
flush_pending_bytes();
if (frames_since_commit != 0)
mResults->CommitResults();
}
bool IrFoxAnalyzer::NeedsRerun()
{
return false;
}
U32 IrFoxAnalyzer::GenerateSimulationData(U64 minimum_sample_index, U32 device_sample_rate,
SimulationChannelDescriptor** simulation_channels)
{
if (mSimulationInitilized == false)
{
mSimulationDataGenerator.Initialize(GetSimulationSampleRate(), &mSettings);
mSimulationInitilized = true;
}
return mSimulationDataGenerator.GenerateSimulationData(minimum_sample_index, device_sample_rate,
simulation_channels);
}
U32 IrFoxAnalyzer::GetMinimumSampleRateHz()
{
return 200000;
}
const char* IrFoxAnalyzer::GetAnalyzerName() const
{
return "IR Fox";
}
const char* GetAnalyzerName()
{
return "IR Fox";
}
Analyzer* CreateAnalyzer()
{
return new IrFoxAnalyzer();
}
void DestroyAnalyzer(Analyzer* analyzer)
{
delete analyzer;
}