mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-18 19:13:58 +00:00
481 lines
14 KiB
C++
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;
|
|
}
|