mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-18 19:13:58 +00:00
Merge pull request #14 from Show-maket/analyzer/ir-fox-classifier
Анализатор IR_Fox: классификатор пакетов, настройки/результаты, тесты + перечень изменений протокола
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ Analyzer/raw/dll/*.dylib
|
|||||||
/Analyzer/raw/IR_Fox/.github
|
/Analyzer/raw/IR_Fox/.github
|
||||||
**/.build
|
**/.build
|
||||||
graphify-out/*
|
graphify-out/*
|
||||||
|
**/.build-*/
|
||||||
|
/tests/*.exe
|
||||||
|
|||||||
@ -15,6 +15,7 @@ set(SOURCES
|
|||||||
src/IrFoxAnalyzer.h
|
src/IrFoxAnalyzer.h
|
||||||
src/IrFoxDecoder.cpp
|
src/IrFoxDecoder.cpp
|
||||||
src/IrFoxDecoder.h
|
src/IrFoxDecoder.h
|
||||||
|
src/IrFoxPacketClassifier.h
|
||||||
src/IrFoxAnalyzerResults.cpp
|
src/IrFoxAnalyzerResults.cpp
|
||||||
src/IrFoxAnalyzerResults.h
|
src/IrFoxAnalyzerResults.h
|
||||||
src/IrFoxAnalyzerSettings.cpp
|
src/IrFoxAnalyzerSettings.cpp
|
||||||
@ -24,3 +25,23 @@ set(SOURCES
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES})
|
add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES})
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_executable(IrFoxPacketClassifierTests tests/IrFoxPacketClassifierTests.cpp)
|
||||||
|
target_include_directories(IrFoxPacketClassifierTests PRIVATE src)
|
||||||
|
add_test(NAME IrFoxPacketClassifierTests COMMAND IrFoxPacketClassifierTests)
|
||||||
|
|
||||||
|
add_executable(IrFoxDecoderTests tests/IrFoxDecoderTests.cpp src/IrFoxDecoder.cpp)
|
||||||
|
target_include_directories(IrFoxDecoderTests PRIVATE src)
|
||||||
|
target_link_libraries(IrFoxDecoderTests PRIVATE Saleae::AnalyzerSDK)
|
||||||
|
add_custom_command(TARGET IrFoxDecoderTests POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
$<TARGET_FILE:Saleae::AnalyzerSDK>
|
||||||
|
$<TARGET_FILE_DIR:IrFoxDecoderTests>)
|
||||||
|
add_test(NAME IrFoxDecoderTests COMMAND IrFoxDecoderTests)
|
||||||
|
endif()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "IrFoxAnalyzer.h"
|
#include "IrFoxAnalyzer.h"
|
||||||
#include "IrFoxAnalyzerSettings.h"
|
#include "IrFoxAnalyzerSettings.h"
|
||||||
#include "IrFoxDecoder.h"
|
#include "IrFoxDecoder.h"
|
||||||
|
#include "IrFoxPacketClassifier.h"
|
||||||
#include <AnalyzerChannelData.h>
|
#include <AnalyzerChannelData.h>
|
||||||
#include <AnalyzerResults.h>
|
#include <AnalyzerResults.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@ -25,7 +26,6 @@ IrFoxAnalyzer::~IrFoxAnalyzer()
|
|||||||
|
|
||||||
void IrFoxAnalyzer::SetupResults()
|
void IrFoxAnalyzer::SetupResults()
|
||||||
{
|
{
|
||||||
m_packet_hex_by_frame.clear();
|
|
||||||
mResults.reset(new IrFoxAnalyzerResults(this, &mSettings));
|
mResults.reset(new IrFoxAnalyzerResults(this, &mSettings));
|
||||||
SetAnalyzerResults(mResults.get());
|
SetAnalyzerResults(mResults.get());
|
||||||
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
|
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 += "...";
|
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);
|
switch (outcome)
|
||||||
if (it == m_packet_hex_by_frame.end())
|
{
|
||||||
return "";
|
case IrFoxPacketOutcome::Accepted:
|
||||||
m_hex_scratch = it->second;
|
return "✅";
|
||||||
return m_hex_scratch.c_str();
|
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);
|
switch (message_type)
|
||||||
if (it == m_bubble_text_by_frame.end())
|
{
|
||||||
return "";
|
case irfox::kMsgBack:
|
||||||
m_bubble_scratch = it->second;
|
return "🔙";
|
||||||
return m_bubble_scratch.c_str();
|
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()
|
void IrFoxAnalyzer::WorkerThread()
|
||||||
{
|
{
|
||||||
mIr = GetAnalyzerChannelData(mSettings.mInputChannel);
|
mIr = GetAnalyzerChannelData(mSettings.mInputChannel);
|
||||||
m_packet_hex_by_frame.clear();
|
mResults->ClearCachedFrameText();
|
||||||
m_bubble_text_by_frame.clear();
|
|
||||||
|
|
||||||
const U32 fs = GetSampleRate();
|
const U32 fs = GetSampleRate();
|
||||||
IrFoxDecoder decoder;
|
IrFoxDecoder decoder;
|
||||||
decoder.reset();
|
decoder.reset();
|
||||||
|
|
||||||
/** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */
|
/** Mirrors the firmware input filter. kMinFilteredPulseUs=0 means direct edge delivery. */
|
||||||
const U64 min_seg_samples =
|
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));
|
std::max<U64>(1ULL, static_cast<U64>((static_cast<double>(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast<double>(fs) + 0.5));
|
||||||
struct RawEdge
|
struct RawEdge
|
||||||
@ -115,8 +155,19 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
|
|
||||||
U32 frames_since_commit = 0;
|
U32 frames_since_commit = 0;
|
||||||
const U32 kCommitBatch = 256;
|
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 frame;
|
||||||
frame.mStartingSampleInclusive = static_cast<S64>(e.start_sample);
|
frame.mStartingSampleInclusive = static_cast<S64>(e.start_sample);
|
||||||
frame.mEndingSampleInclusive = static_cast<S64>(e.end_sample);
|
frame.mEndingSampleInclusive = static_cast<S64>(e.end_sample);
|
||||||
@ -124,45 +175,188 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
frame.mData1 = e.bit_value;
|
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.mData2 = e.bit_index | (U64(e.err_low) << 16) | (U64(e.err_high) << 24) | (U64(e.err_other) << 32);
|
||||||
frame.mFlags = e.mflags;
|
frame.mFlags = e.mflags;
|
||||||
// В SDK только ERROR/WARNING меняют цвет бабла; sync выделяем янтарным (как warning), данные — обычные.
|
mResults->AddFrame(frame);
|
||||||
if (e.frame_type == IRF_FT_SYNC_BIT)
|
note_legacy_frame();
|
||||||
frame.mFlags |= DISPLAY_AS_WARNING_FLAG;
|
};
|
||||||
|
|
||||||
const U64 fid = mResults->AddFrame(frame);
|
auto flush_pending_bytes = [&]() {
|
||||||
if (e.bubble_text[0] != '\0')
|
for (const IrFoxEmitBit& byte_event : pending_byte_frames)
|
||||||
m_bubble_text_by_frame[fid] = e.bubble_text;
|
add_event_frame(byte_event);
|
||||||
if (++frames_since_commit >= kCommitBatch)
|
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();
|
if (detailed_presentation)
|
||||||
frames_since_commit = 0;
|
{
|
||||||
|
// 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) {
|
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 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.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.mData1 = p.pack_size;
|
||||||
frame.mData2 = (U64(p.err_low) << 0) | (U64(p.err_high) << 8) | (U64(p.err_other) << 16);
|
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);
|
const U64 fid = mResults->AddFrame(frame);
|
||||||
|
pending_byte_frames.clear();
|
||||||
|
|
||||||
|
const std::string icon = packet_icon(decision, mSettings.mPacketIconMode);
|
||||||
|
|
||||||
std::string hx;
|
std::string hx;
|
||||||
append_hex(hx, p.data_bytes, p.pack_size);
|
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;
|
auto cached_text = std::make_shared<IrFoxCachedFrameText>();
|
||||||
fv2.AddBoolean("crc_ok", p.crc_ok);
|
cached_text->export_hex = hx;
|
||||||
fv2.AddInteger("len", static_cast<S64>(p.pack_size));
|
cached_text->bubble_texts[0] = icon;
|
||||||
fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
|
if (detailed_presentation)
|
||||||
fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
|
{
|
||||||
fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
|
char last_byte[3] = "??";
|
||||||
fv2.AddByteArray("data", p.data_bytes, p.pack_size);
|
if (p.pack_size > 0)
|
||||||
mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast<U64>(p.start_sample),
|
std::snprintf(last_byte, sizeof last_byte, "%02X", static_cast<unsigned>(p.data_bytes[p.pack_size - 1]));
|
||||||
static_cast<U64>(p.end_sample));
|
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)
|
if (++frames_since_commit >= kCommitBatch)
|
||||||
{
|
{
|
||||||
@ -170,6 +364,10 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
frames_since_commit = 0;
|
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 = [&]() {
|
auto emit_confirmed_edges = [&]() {
|
||||||
for (;;)
|
for (;;)
|
||||||
@ -180,7 +378,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, 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_sample = pending[0].sample;
|
||||||
last_dec_edge_valid = true;
|
last_dec_edge_valid = true;
|
||||||
pending.erase(pending.begin());
|
pending.erase(pending.begin());
|
||||||
@ -192,7 +390,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, 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_sample = pending[0].sample;
|
||||||
last_dec_edge_valid = true;
|
last_dec_edge_valid = true;
|
||||||
pending.erase(pending.begin());
|
pending.erase(pending.begin());
|
||||||
@ -201,7 +399,7 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
}
|
}
|
||||||
if (pending.size() == 1)
|
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_sample = pending[0].sample;
|
||||||
last_dec_edge_valid = true;
|
last_dec_edge_valid = true;
|
||||||
pending.clear();
|
pending.clear();
|
||||||
@ -230,7 +428,9 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
}
|
}
|
||||||
|
|
||||||
flush_pending_tail();
|
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)
|
if (frames_since_commit != 0)
|
||||||
mResults->CommitResults();
|
mResults->CommitResults();
|
||||||
|
|||||||
@ -6,8 +6,6 @@
|
|||||||
#include "IrFoxAnalyzerResults.h"
|
#include "IrFoxAnalyzerResults.h"
|
||||||
#include "IrFoxSimulationDataGenerator.h"
|
#include "IrFoxSimulationDataGenerator.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2
|
class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2
|
||||||
{
|
{
|
||||||
@ -25,9 +23,6 @@ public:
|
|||||||
virtual const char* GetAnalyzerName() const;
|
virtual const char* GetAnalyzerName() const;
|
||||||
virtual bool NeedsRerun();
|
virtual bool NeedsRerun();
|
||||||
|
|
||||||
const char* PacketHexForFrame(U64 frame_id);
|
|
||||||
const char* BubbleTextForFrame(U64 frame_id) const;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
IrFoxAnalyzerSettings mSettings;
|
IrFoxAnalyzerSettings mSettings;
|
||||||
std::unique_ptr<IrFoxAnalyzerResults> mResults;
|
std::unique_ptr<IrFoxAnalyzerResults> mResults;
|
||||||
@ -36,10 +31,6 @@ protected:
|
|||||||
IrFoxSimulationDataGenerator mSimulationDataGenerator;
|
IrFoxSimulationDataGenerator mSimulationDataGenerator;
|
||||||
bool mSimulationInitilized;
|
bool mSimulationInitilized;
|
||||||
|
|
||||||
std::unordered_map<U64, std::string> m_packet_hex_by_frame;
|
|
||||||
std::unordered_map<U64, std::string> m_bubble_text_by_frame;
|
|
||||||
mutable std::string m_hex_scratch;
|
|
||||||
mutable std::string m_bubble_scratch;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName();
|
extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName();
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include "IrFoxDecoder.h"
|
#include "IrFoxDecoder.h"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings)
|
IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings)
|
||||||
: AnalyzerResults(),
|
: AnalyzerResults(),
|
||||||
@ -18,51 +19,86 @@ IrFoxAnalyzerResults::~IrFoxAnalyzerResults()
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IrFoxAnalyzerResults::ClearCachedFrameText()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
|
||||||
|
m_frame_text_by_frame.clear();
|
||||||
|
m_frame_text_by_frame.reserve(1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IrFoxAnalyzerResults::CacheFrameText(U64 frame_id, std::shared_ptr<const IrFoxCachedFrameText> text)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
|
||||||
|
m_frame_text_by_frame[frame_id] = std::move(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<const IrFoxCachedFrameText> IrFoxAnalyzerResults::CachedFrameTextForFrame(U64 frame_id) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
|
||||||
|
const auto it = m_frame_text_by_frame.find(frame_id);
|
||||||
|
return it == m_frame_text_by_frame.end() ? nullptr : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
|
void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
|
||||||
{
|
{
|
||||||
(void)display_base;
|
(void)display_base;
|
||||||
(void)channel;
|
(void)channel;
|
||||||
ClearResultStrings();
|
ClearResultStrings();
|
||||||
|
auto add_cached_text = [&]() {
|
||||||
|
const std::shared_ptr<const IrFoxCachedFrameText> cached = CachedFrameTextForFrame(frame_index);
|
||||||
|
if (!cached)
|
||||||
|
return false;
|
||||||
|
for (size_t i = 0; i < cached->bubble_text_count; ++i)
|
||||||
|
AddResultString(cached->bubble_texts[i].c_str());
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every Overview frame is a packet with immutable, precomputed text. Avoid
|
||||||
|
// even GetFrame() and formatting on Logic's redraw callback in that mode.
|
||||||
|
if (mSettings->mPresentation == IrFoxPresentation::Overview && add_cached_text())
|
||||||
|
return;
|
||||||
|
|
||||||
Frame frame = GetFrame(frame_index);
|
Frame frame = GetFrame(frame_index);
|
||||||
|
|
||||||
char line[256];
|
char line[256];
|
||||||
|
|
||||||
switch (frame.mType)
|
switch (frame.mType)
|
||||||
{
|
{
|
||||||
case IRF_FT_DATA_BIT:
|
case IRF_FT_DATA_BYTE:
|
||||||
case IRF_FT_SYNC_BIT:
|
{
|
||||||
|
char byte_text[3];
|
||||||
|
std::snprintf(byte_text, sizeof byte_text, "%02X", static_cast<unsigned>(frame.mData1 & 0xFFu));
|
||||||
|
AddResultString(byte_text);
|
||||||
|
AddResultString("0x", byte_text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case IRF_FT_PREAMBLE:
|
case IRF_FT_PREAMBLE:
|
||||||
case IRF_FT_OVERFLOW:
|
case IRF_FT_OVERFLOW:
|
||||||
case IRF_FT_ABORT:
|
case IRF_FT_ABORT:
|
||||||
{
|
{
|
||||||
const char* bt = mAnalyzer->BubbleTextForFrame(frame_index);
|
if (frame.mType == IRF_FT_OVERFLOW)
|
||||||
if (bt && bt[0])
|
|
||||||
AddResultString(bt);
|
|
||||||
else if (frame.mType == IRF_FT_DATA_BIT)
|
|
||||||
AddResultString(frame.mData1 ? "1" : "0");
|
|
||||||
else if (frame.mType == IRF_FT_SYNC_BIT)
|
|
||||||
{
|
|
||||||
snprintf(line, sizeof line, "sync: %s", frame.mData1 ? "1" : "0");
|
|
||||||
AddResultString(line);
|
|
||||||
}
|
|
||||||
else if (frame.mType == IRF_FT_OVERFLOW)
|
|
||||||
AddResultString("OVF");
|
AddResultString("OVF");
|
||||||
else if (frame.mType == IRF_FT_ABORT)
|
else if (frame.mType == IRF_FT_ABORT)
|
||||||
AddResultString("SYNC!");
|
AddResultString("SYNC!");
|
||||||
else
|
else
|
||||||
AddResultString("PRE");
|
{
|
||||||
|
AddResultString("📡");
|
||||||
|
AddResultString("📡 PRE");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case IRF_FT_PACKET_OK:
|
case IRF_FT_PACKET_ACCEPTED:
|
||||||
case IRF_FT_PACKET_CRC_FAIL:
|
case IRF_FT_PACKET_CRC_FAIL:
|
||||||
|
case IRF_FT_PACKET_BAD_LENGTH:
|
||||||
|
case IRF_FT_PACKET_RAW_ONLY:
|
||||||
|
case IRF_FT_PACKET_IGNORED_ADDRESS:
|
||||||
{
|
{
|
||||||
snprintf(line, sizeof line, "%s %lluB", frame.mType == IRF_FT_PACKET_OK ? "OK" : "CRC",
|
if (!add_cached_text())
|
||||||
(unsigned long long)frame.mData1);
|
AddResultString(frame.mType == IRF_FT_PACKET_ACCEPTED ? "✅" :
|
||||||
AddResultString(line);
|
(frame.mType == IRF_FT_PACKET_CRC_FAIL || frame.mType == IRF_FT_PACKET_BAD_LENGTH) ? "❌" :
|
||||||
const char* hx = mAnalyzer->PacketHexForFrame(frame_index);
|
frame.mType == IRF_FT_PACKET_IGNORED_ADDRESS ? "📭" : "⚠️");
|
||||||
if (hx && hx[0])
|
|
||||||
AddResultString(hx);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,17 +131,20 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
|
|||||||
const char* typ = "?";
|
const char* typ = "?";
|
||||||
switch (frame.mType)
|
switch (frame.mType)
|
||||||
{
|
{
|
||||||
case IRF_FT_DATA_BIT:
|
case IRF_FT_PACKET_ACCEPTED:
|
||||||
typ = "D";
|
typ = "ACCEPT";
|
||||||
break;
|
|
||||||
case IRF_FT_SYNC_BIT:
|
|
||||||
typ = "S";
|
|
||||||
break;
|
|
||||||
case IRF_FT_PACKET_OK:
|
|
||||||
typ = "OK";
|
|
||||||
break;
|
break;
|
||||||
case IRF_FT_PACKET_CRC_FAIL:
|
case IRF_FT_PACKET_CRC_FAIL:
|
||||||
typ = "CRC";
|
typ = "REJECT_CRC";
|
||||||
|
break;
|
||||||
|
case IRF_FT_PACKET_BAD_LENGTH:
|
||||||
|
typ = "REJECT_LEN";
|
||||||
|
break;
|
||||||
|
case IRF_FT_PACKET_RAW_ONLY:
|
||||||
|
typ = "RAW_ONLY";
|
||||||
|
break;
|
||||||
|
case IRF_FT_PACKET_IGNORED_ADDRESS:
|
||||||
|
typ = "IGNORE_ADDR";
|
||||||
break;
|
break;
|
||||||
case IRF_FT_OVERFLOW:
|
case IRF_FT_OVERFLOW:
|
||||||
typ = "OVF";
|
typ = "OVF";
|
||||||
@ -120,14 +159,12 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* hx = mAnalyzer->PacketHexForFrame(i);
|
const std::shared_ptr<const IrFoxCachedFrameText> cached = CachedFrameTextForFrame(i);
|
||||||
if (!hx)
|
const char* hx = cached ? cached->export_hex.c_str() : "";
|
||||||
hx = "";
|
|
||||||
|
|
||||||
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_DATA_BIT || frame.mType == IRF_FT_SYNC_BIT ||
|
if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT)
|
||||||
frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
|
|||||||
@ -2,10 +2,22 @@
|
|||||||
#define IRFOX_ANALYZER_RESULTS
|
#define IRFOX_ANALYZER_RESULTS
|
||||||
|
|
||||||
#include <AnalyzerResults.h>
|
#include <AnalyzerResults.h>
|
||||||
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
class IrFoxAnalyzer;
|
class IrFoxAnalyzer;
|
||||||
class IrFoxAnalyzerSettings;
|
class IrFoxAnalyzerSettings;
|
||||||
|
|
||||||
|
struct IrFoxCachedFrameText
|
||||||
|
{
|
||||||
|
std::array<std::string, 3> bubble_texts{};
|
||||||
|
size_t bubble_text_count = 0;
|
||||||
|
std::string export_hex;
|
||||||
|
};
|
||||||
|
|
||||||
class IrFoxAnalyzerResults : public AnalyzerResults
|
class IrFoxAnalyzerResults : public AnalyzerResults
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -19,9 +31,15 @@ public:
|
|||||||
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
|
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
|
||||||
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
|
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
|
||||||
|
|
||||||
|
void ClearCachedFrameText();
|
||||||
|
void CacheFrameText(U64 frame_id, std::shared_ptr<const IrFoxCachedFrameText> text);
|
||||||
|
std::shared_ptr<const IrFoxCachedFrameText> CachedFrameTextForFrame(U64 frame_id) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
IrFoxAnalyzerSettings* mSettings;
|
IrFoxAnalyzerSettings* mSettings;
|
||||||
IrFoxAnalyzer* mAnalyzer;
|
IrFoxAnalyzer* mAnalyzer;
|
||||||
|
mutable std::mutex m_frame_text_mutex;
|
||||||
|
std::unordered_map<U64, std::shared_ptr<const IrFoxCachedFrameText>> m_frame_text_by_frame;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -3,14 +3,42 @@
|
|||||||
|
|
||||||
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
|
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
|
||||||
: mInputChannel(UNDEFINED_CHANNEL),
|
: mInputChannel(UNDEFINED_CHANNEL),
|
||||||
mInputChannelInterface()
|
mReceiverAddress(0),
|
||||||
|
mPresentation(IrFoxPresentation::Overview),
|
||||||
|
mPacketIconMode(IrFoxPacketIconMode::StatusAndType),
|
||||||
|
mInputChannelInterface(),
|
||||||
|
mReceiverAddressInterface(),
|
||||||
|
mPresentationInterface(),
|
||||||
|
mPacketIconModeInterface()
|
||||||
{
|
{
|
||||||
mInputChannelInterface.SetTitleAndTooltip(
|
mInputChannelInterface.SetTitleAndTooltip(
|
||||||
"IR",
|
"IR",
|
||||||
"Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)");
|
"Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)");
|
||||||
mInputChannelInterface.SetChannel(mInputChannel);
|
mInputChannelInterface.SetChannel(mInputChannel);
|
||||||
|
mReceiverAddressInterface.SetTitleAndTooltip(
|
||||||
|
"Receiver address",
|
||||||
|
"IR receiver ID for ACCEPT/IGNORE ADDR. 0 mirrors a receiver configured to accept every address; 65000..65535 are broadcast destinations.");
|
||||||
|
mReceiverAddressInterface.SetMin(0);
|
||||||
|
mReceiverAddressInterface.SetMax(65535);
|
||||||
|
mReceiverAddressInterface.SetInteger(mReceiverAddress);
|
||||||
|
mPresentationInterface.SetTitleAndTooltip(
|
||||||
|
"Presentation",
|
||||||
|
"Overview shows one packet bubble over the full frame. Detailed separates PRE, packet, and hexadecimal bytes. Logic 2 does not expose zoom to analyzers, so this is selected explicitly.");
|
||||||
|
mPresentationInterface.AddNumber(static_cast<double>(IrFoxPresentation::Overview), "Overview", "One outcome bubble across the full frame; no PRE badge.");
|
||||||
|
mPresentationInterface.AddNumber(static_cast<double>(IrFoxPresentation::Detailed), "Detailed", "Separate PRE and packet bubbles, plus one hexadecimal bubble per byte.");
|
||||||
|
mPresentationInterface.SetNumber(static_cast<double>(mPresentation));
|
||||||
|
mPacketIconModeInterface.SetTitleAndTooltip(
|
||||||
|
"Packet icon",
|
||||||
|
"Choose whether packet bubbles show reception status, decoded message type, or both.");
|
||||||
|
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::Status), "Status ✅", "One status symbol: accepted, other address, invalid, or unknown.");
|
||||||
|
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::MessageType), "Message type 📦", "One symbol for the decoded firmware message type.");
|
||||||
|
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::StatusAndType), "Status + type ✅📦", "Reception status followed by the decoded firmware message type.");
|
||||||
|
mPacketIconModeInterface.SetNumber(static_cast<double>(mPacketIconMode));
|
||||||
|
|
||||||
AddInterface(&mInputChannelInterface);
|
AddInterface(&mInputChannelInterface);
|
||||||
|
AddInterface(&mReceiverAddressInterface);
|
||||||
|
AddInterface(&mPresentationInterface);
|
||||||
|
AddInterface(&mPacketIconModeInterface);
|
||||||
|
|
||||||
AddExportOption(0, "Export as text/csv file");
|
AddExportOption(0, "Export as text/csv file");
|
||||||
AddExportExtension(0, "text", "txt");
|
AddExportExtension(0, "text", "txt");
|
||||||
@ -27,6 +55,23 @@ IrFoxAnalyzerSettings::~IrFoxAnalyzerSettings()
|
|||||||
bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
|
bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
|
||||||
{
|
{
|
||||||
mInputChannel = mInputChannelInterface.GetChannel();
|
mInputChannel = mInputChannelInterface.GetChannel();
|
||||||
|
mReceiverAddress = static_cast<uint16_t>(mReceiverAddressInterface.GetInteger());
|
||||||
|
const int presentation = static_cast<int>(mPresentationInterface.GetNumber());
|
||||||
|
mPresentation = presentation == static_cast<int>(IrFoxPresentation::Detailed) ?
|
||||||
|
IrFoxPresentation::Detailed : IrFoxPresentation::Overview;
|
||||||
|
const int packet_icon_mode = static_cast<int>(mPacketIconModeInterface.GetNumber());
|
||||||
|
switch (packet_icon_mode)
|
||||||
|
{
|
||||||
|
case static_cast<int>(IrFoxPacketIconMode::Status):
|
||||||
|
mPacketIconMode = IrFoxPacketIconMode::Status;
|
||||||
|
break;
|
||||||
|
case static_cast<int>(IrFoxPacketIconMode::MessageType):
|
||||||
|
mPacketIconMode = IrFoxPacketIconMode::MessageType;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
ClearChannels();
|
ClearChannels();
|
||||||
AddChannel(mInputChannel, "IR Fox", true);
|
AddChannel(mInputChannel, "IR Fox", true);
|
||||||
@ -37,6 +82,9 @@ bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
|
|||||||
void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings()
|
void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings()
|
||||||
{
|
{
|
||||||
mInputChannelInterface.SetChannel(mInputChannel);
|
mInputChannelInterface.SetChannel(mInputChannel);
|
||||||
|
mReceiverAddressInterface.SetInteger(mReceiverAddress);
|
||||||
|
mPresentationInterface.SetNumber(static_cast<double>(mPresentation));
|
||||||
|
mPacketIconModeInterface.SetNumber(static_cast<double>(mPacketIconMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
|
void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
|
||||||
@ -45,6 +93,50 @@ void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
|
|||||||
text_archive.SetString(settings);
|
text_archive.SetString(settings);
|
||||||
|
|
||||||
text_archive >> mInputChannel;
|
text_archive >> mInputChannel;
|
||||||
|
S32 receiver_address = 0;
|
||||||
|
if (text_archive >> receiver_address)
|
||||||
|
{
|
||||||
|
if (receiver_address < 0)
|
||||||
|
receiver_address = 0;
|
||||||
|
if (receiver_address > 65535)
|
||||||
|
receiver_address = 65535;
|
||||||
|
mReceiverAddress = static_cast<uint16_t>(receiver_address);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Version 0 settings stored only the channel.
|
||||||
|
mReceiverAddress = 0;
|
||||||
|
}
|
||||||
|
// Settings before the presentation switch contain the former "show bit cells"
|
||||||
|
// value in this position. Bits are now deliberately always shown.
|
||||||
|
S32 legacy_show_bit_cells = 0;
|
||||||
|
(void)(text_archive >> legacy_show_bit_cells);
|
||||||
|
S32 presentation = static_cast<S32>(IrFoxPresentation::Overview);
|
||||||
|
if (text_archive >> presentation && presentation == static_cast<S32>(IrFoxPresentation::Detailed))
|
||||||
|
mPresentation = IrFoxPresentation::Detailed;
|
||||||
|
else
|
||||||
|
mPresentation = IrFoxPresentation::Overview;
|
||||||
|
S32 packet_icon_mode = static_cast<S32>(IrFoxPacketIconMode::StatusAndType);
|
||||||
|
if (text_archive >> packet_icon_mode)
|
||||||
|
{
|
||||||
|
switch (packet_icon_mode)
|
||||||
|
{
|
||||||
|
case static_cast<S32>(IrFoxPacketIconMode::Status):
|
||||||
|
mPacketIconMode = IrFoxPacketIconMode::Status;
|
||||||
|
break;
|
||||||
|
case static_cast<S32>(IrFoxPacketIconMode::MessageType):
|
||||||
|
mPacketIconMode = IrFoxPacketIconMode::MessageType;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Existing analyzer instances gain the most informative mode by default.
|
||||||
|
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
|
||||||
|
}
|
||||||
|
|
||||||
ClearChannels();
|
ClearChannels();
|
||||||
AddChannel(mInputChannel, "IR Fox", true);
|
AddChannel(mInputChannel, "IR Fox", true);
|
||||||
@ -57,6 +149,11 @@ const char* IrFoxAnalyzerSettings::SaveSettings()
|
|||||||
SimpleArchive text_archive;
|
SimpleArchive text_archive;
|
||||||
|
|
||||||
text_archive << mInputChannel;
|
text_archive << mInputChannel;
|
||||||
|
text_archive << static_cast<S32>(mReceiverAddress);
|
||||||
|
// Retain the old third field so already-saved configurations remain readable.
|
||||||
|
text_archive << static_cast<S32>(1);
|
||||||
|
text_archive << static_cast<S32>(mPresentation);
|
||||||
|
text_archive << static_cast<S32>(mPacketIconMode);
|
||||||
|
|
||||||
return SetReturnString(text_archive.GetString());
|
return SetReturnString(text_archive.GetString());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,22 @@
|
|||||||
|
|
||||||
#include <AnalyzerSettings.h>
|
#include <AnalyzerSettings.h>
|
||||||
#include <AnalyzerTypes.h>
|
#include <AnalyzerTypes.h>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
/** Logic 2 does not pass the current zoom level to an analyzer, so the user selects the annotation density. */
|
||||||
|
enum class IrFoxPresentation : uint8_t
|
||||||
|
{
|
||||||
|
Overview = 0,
|
||||||
|
Detailed = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Which compact symbol is used at the shortest packet-bubble zoom level. */
|
||||||
|
enum class IrFoxPacketIconMode : uint8_t
|
||||||
|
{
|
||||||
|
Status = 0,
|
||||||
|
MessageType = 1,
|
||||||
|
StatusAndType = 2,
|
||||||
|
};
|
||||||
|
|
||||||
class IrFoxAnalyzerSettings : public AnalyzerSettings
|
class IrFoxAnalyzerSettings : public AnalyzerSettings
|
||||||
{
|
{
|
||||||
@ -16,9 +32,16 @@ public:
|
|||||||
virtual const char* SaveSettings();
|
virtual const char* SaveSettings();
|
||||||
|
|
||||||
Channel mInputChannel;
|
Channel mInputChannel;
|
||||||
|
/** Receiver ID used by the same address rule as IR_FOX::checkAddressRuleApply. 0 means catch all. */
|
||||||
|
uint16_t mReceiverAddress;
|
||||||
|
IrFoxPresentation mPresentation;
|
||||||
|
IrFoxPacketIconMode mPacketIconMode;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AnalyzerSettingInterfaceChannel mInputChannelInterface;
|
AnalyzerSettingInterfaceChannel mInputChannelInterface;
|
||||||
|
AnalyzerSettingInterfaceInteger mReceiverAddressInterface;
|
||||||
|
AnalyzerSettingInterfaceNumberList mPresentationInterface;
|
||||||
|
AnalyzerSettingInterfaceNumberList mPacketIconModeInterface;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -73,6 +73,17 @@ void IrFoxDecoder::first_rx()
|
|||||||
std::memset(data_buffer, 0, sizeof data_buffer);
|
std::memset(data_buffer, 0, sizeof data_buffer);
|
||||||
preamble_bubble_start_valid_ = false;
|
preamble_bubble_start_valid_ = false;
|
||||||
trim_first_data_bit_cell_ = false;
|
trim_first_data_bit_cell_ = false;
|
||||||
|
packet_start_sample_ = 0;
|
||||||
|
packet_start_valid_ = false;
|
||||||
|
packet_data_start_sample_ = 0;
|
||||||
|
packet_data_start_valid_ = false;
|
||||||
|
byte_start_sample_ = 0;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IrFoxDecoder::listen_start(double t_us)
|
void IrFoxDecoder::listen_start(double t_us)
|
||||||
@ -105,7 +116,7 @@ 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, IrFoxEmitBitMode emit_mode)
|
||||||
{
|
{
|
||||||
if (i_data_buffer > irfox::kDataByteSizeMax * 8u)
|
if (i_data_buffer >= irfox::kDataByteSizeMax * 8u)
|
||||||
{
|
{
|
||||||
if (!is_buffer_overflow && on_bit)
|
if (!is_buffer_overflow && on_bit)
|
||||||
{
|
{
|
||||||
@ -140,6 +151,19 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
|
|||||||
if (is_data)
|
if (is_data)
|
||||||
{
|
{
|
||||||
const bool was_first_data_bit = (i_data_buffer == 0);
|
const bool was_first_data_bit = (i_data_buffer == 0);
|
||||||
|
const bool was_first_bit_of_byte = (i_data_buffer % irfox::kBitPerByte) == 0U;
|
||||||
|
if (was_first_data_bit && !packet_start_valid_)
|
||||||
|
{
|
||||||
|
packet_start_sample_ = cell_start_s;
|
||||||
|
packet_start_valid_ = true;
|
||||||
|
}
|
||||||
|
if (was_first_data_bit)
|
||||||
|
{
|
||||||
|
packet_data_start_sample_ = cell_start_s;
|
||||||
|
packet_data_start_valid_ = true;
|
||||||
|
}
|
||||||
|
if (was_first_bit_of_byte)
|
||||||
|
byte_start_sample_ = cell_start_s;
|
||||||
data_buffer[i_data_buffer / 8] |= static_cast<uint8_t>(bit ? 1 : 0) << (7 - (i_data_buffer % 8));
|
data_buffer[i_data_buffer / 8] |= static_cast<uint8_t>(bit ? 1 : 0) << (7 - (i_data_buffer % 8));
|
||||||
i_data_buffer++;
|
i_data_buffer++;
|
||||||
buf_bit_pos++;
|
buf_bit_pos++;
|
||||||
@ -156,6 +180,16 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
|
|||||||
e.bubble_text[1] = '\0';
|
e.bubble_text[1] = '\0';
|
||||||
on_bit(e);
|
on_bit(e);
|
||||||
}
|
}
|
||||||
|
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble &&
|
||||||
|
(i_data_buffer % irfox::kBitPerByte) == 0U)
|
||||||
|
{
|
||||||
|
const uint64_t byte_index = (i_data_buffer / irfox::kBitPerByte) - 1U;
|
||||||
|
IrFoxEmitBit e{static_cast<int64_t>(byte_start_sample_), static_cast<int64_t>(cell_end_s), IRF_FT_DATA_BYTE,
|
||||||
|
data_buffer[byte_index], byte_index, fl, pack_trace_invert_fix, 0, 0, 0};
|
||||||
|
fill_err_snapshot(e);
|
||||||
|
std::snprintf(e.bubble_text, sizeof e.bubble_text, "%02X", static_cast<unsigned>(data_buffer[byte_index]));
|
||||||
|
on_bit(e);
|
||||||
|
}
|
||||||
if (was_first_data_bit && trim_first_data_bit_cell_)
|
if (was_first_data_bit && trim_first_data_bit_cell_)
|
||||||
trim_first_data_bit_cell_ = false;
|
trim_first_data_bit_cell_ = false;
|
||||||
}
|
}
|
||||||
@ -216,12 +250,33 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
|
|||||||
if (!is_available && is_data && !is_wrong_pack)
|
if (!is_available && is_data && !is_wrong_pack)
|
||||||
{
|
{
|
||||||
if (i_data_buffer == 8 * irfox::kMsgBytes)
|
if (i_data_buffer == 8 * irfox::kMsgBytes)
|
||||||
|
{
|
||||||
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.
|
||||||
|
// Emit the rejection here so the capture explains why no packet follows.
|
||||||
|
if (pack_size != 0 && pack_size < irfox::kMsgBytes + irfox::kCrcBytes)
|
||||||
|
{
|
||||||
|
is_wrong_pack = true;
|
||||||
|
IrFoxEmitPacket pkt{};
|
||||||
|
pkt.start_sample = static_cast<int64_t>(packet_start_valid_ ? packet_start_sample_ : cell_start_s);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (pack_size && (i_data_buffer == 8))
|
if (pack_size && (i_data_buffer == 8))
|
||||||
msg_type_receive = static_cast<uint8_t>((data_buffer[0] >> 5) | 0xF8u);
|
msg_type_receive = static_cast<uint8_t>((data_buffer[0] >> 5) | 0xF8u);
|
||||||
|
|
||||||
if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte))
|
if (pack_size >= irfox::kMsgBytes + irfox::kCrcBytes &&
|
||||||
|
(i_data_buffer == pack_size * irfox::kBitPerByte))
|
||||||
{
|
{
|
||||||
uint16_t crc_computed = 0;
|
uint16_t crc_computed = 0;
|
||||||
const bool crc_ok = crc_check(static_cast<uint8_t>(pack_size - irfox::kCrcBytes), crc_computed);
|
const bool crc_ok = crc_check(static_cast<uint8_t>(pack_size - irfox::kCrcBytes), crc_computed);
|
||||||
@ -232,7 +287,8 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
|
|||||||
is_available = crc_ok;
|
is_available = crc_ok;
|
||||||
|
|
||||||
IrFoxEmitPacket pkt{};
|
IrFoxEmitPacket pkt{};
|
||||||
pkt.start_sample = static_cast<int64_t>(cell_start_s);
|
pkt.start_sample = static_cast<int64_t>(packet_start_valid_ ? packet_start_sample_ : cell_start_s);
|
||||||
|
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.end_sample = static_cast<int64_t>(cell_end_s);
|
||||||
pkt.crc_ok = crc_ok;
|
pkt.crc_ok = crc_ok;
|
||||||
pkt.pack_size = static_cast<uint8_t>(pack_size);
|
pkt.pack_size = static_cast<uint8_t>(pack_size);
|
||||||
@ -256,29 +312,181 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
|
|||||||
|
|
||||||
listen_start(t_us);
|
listen_start(t_us);
|
||||||
|
|
||||||
// Как IR_DecoderRaw: пауза между фронтами по lastEdgeTime при активном приёме кадра.
|
|
||||||
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_sample = sample;
|
|
||||||
|
|
||||||
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
|
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
|
||||||
|
|
||||||
/** Визуализация: начало PRE с ближайшего спада в пределах ~3 битовых периодов (ИК-метка). */
|
/** Firmware starts a preamble candidate only on its first rising edge after silence. */
|
||||||
auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t {
|
auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t {
|
||||||
if (!is_rising)
|
(void)is_rising;
|
||||||
return edge_s;
|
|
||||||
if (edge_s > prev_fall_sample)
|
|
||||||
{
|
|
||||||
const double span_us = double(edge_s - prev_fall_sample) * 1e6 / double(fs);
|
|
||||||
const double max_us = double(rise_max_us) * 3.0;
|
|
||||||
if (span_us <= max_us)
|
|
||||||
return prev_fall_sample;
|
|
||||||
}
|
|
||||||
return edge_s;
|
return edge_s;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mirror IR_DecoderRaw::preambleProcessEdge. A frame may start only after
|
||||||
|
// a long silence and two mutually consistent rise-to-rise periods.
|
||||||
|
auto start_preamble_candidate = [&]() {
|
||||||
|
preamble_state_ = PreambleState::Candidate;
|
||||||
|
preamble_good_periods_ = 0;
|
||||||
|
preamble_mean_period_us_ = 0;
|
||||||
|
preamble_candidate_last_edge_us_ = t_us;
|
||||||
|
preamble_candidate_first_rise_us_ = t_us;
|
||||||
|
preamble_candidate_first_rise_valid_ = rising;
|
||||||
|
is_preamb = true;
|
||||||
|
is_recive = false;
|
||||||
|
is_recive_raw = false;
|
||||||
|
is_wrong_pack = false;
|
||||||
|
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising);
|
||||||
|
preamble_bubble_start_valid_ = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
preamble_state_ = PreambleState::Idle;
|
||||||
|
preamble_good_periods_ = 0;
|
||||||
|
preamble_mean_period_us_ = 0;
|
||||||
|
}
|
||||||
|
if (preamble_state_ == PreambleState::Idle)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
if (!is_recive_raw && rising && enough_silence)
|
||||||
|
start_preamble_candidate();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// IR_DecoderRaw ignores idle edges until a valid preamble candidate starts.
|
||||||
|
last_edge_time_us = t_us;
|
||||||
|
last_edge_sample = sample;
|
||||||
|
last_processed_edge_us = t_us;
|
||||||
|
have_last_processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (!rising)
|
||||||
|
{
|
||||||
|
last_edge_time_us = t_us;
|
||||||
|
last_edge_sample = sample;
|
||||||
|
last_processed_edge_us = t_us;
|
||||||
|
have_last_processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preamble_candidate_first_rise_valid_)
|
||||||
|
{
|
||||||
|
preamble_candidate_first_rise_valid_ = true;
|
||||||
|
preamble_candidate_first_rise_us_ = t_us;
|
||||||
|
last_edge_time_us = t_us;
|
||||||
|
last_edge_sample = sample;
|
||||||
|
last_processed_edge_us = t_us;
|
||||||
|
have_last_processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t period_us = static_cast<uint32_t>(t_us - preamble_candidate_first_rise_us_);
|
||||||
|
preamble_candidate_first_rise_us_ = t_us;
|
||||||
|
if (!irfox::preambleRisePeriodCoarseOk(period_us))
|
||||||
|
{
|
||||||
|
preamble_good_periods_ = 0;
|
||||||
|
preamble_mean_period_us_ = 0;
|
||||||
|
last_edge_time_us = t_us;
|
||||||
|
last_edge_sample = sample;
|
||||||
|
last_processed_edge_us = t_us;
|
||||||
|
have_last_processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preamble_good_periods_ == 0)
|
||||||
|
{
|
||||||
|
preamble_good_periods_ = 1;
|
||||||
|
preamble_mean_period_us_ = period_us;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const uint32_t delta = period_us > preamble_mean_period_us_ ? period_us - preamble_mean_period_us_ :
|
||||||
|
preamble_mean_period_us_ - period_us;
|
||||||
|
if (delta <= irfox::preambleJitterTolUs(preamble_mean_period_us_))
|
||||||
|
{
|
||||||
|
if (preamble_good_periods_ < 255U)
|
||||||
|
++preamble_good_periods_;
|
||||||
|
preamble_mean_period_us_ = (preamble_mean_period_us_ * 3U + period_us) / 4U;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
preamble_good_periods_ = 1;
|
||||||
|
preamble_mean_period_us_ = period_us;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preamble_good_periods_ < irfox::kPreambleLockRisePeriods)
|
||||||
|
{
|
||||||
|
last_edge_time_us = t_us;
|
||||||
|
last_edge_sample = sample;
|
||||||
|
last_processed_edge_us = t_us;
|
||||||
|
have_last_processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The firmware clears all frame state when the candidate becomes locked.
|
||||||
|
err_low_signal = err_high_signal = err_other = 0;
|
||||||
|
pack_size = 0;
|
||||||
|
is_buffer_overflow = false;
|
||||||
|
is_available = false;
|
||||||
|
buf_bit_pos = 0;
|
||||||
|
is_data = true;
|
||||||
|
i_data_buffer = 0;
|
||||||
|
next_control_bit = irfox::kBitPerByte;
|
||||||
|
i_sync_bit = 0;
|
||||||
|
err_sync_bit = 0;
|
||||||
|
is_wrong_pack = false;
|
||||||
|
msg_type_receive = 0;
|
||||||
|
std::memset(data_buffer, 0, sizeof data_buffer);
|
||||||
|
packet_start_sample_ = preamble_bubble_start_sample_;
|
||||||
|
packet_start_valid_ = preamble_bubble_start_valid_;
|
||||||
|
packet_data_start_sample_ = 0;
|
||||||
|
packet_data_start_valid_ = false;
|
||||||
|
byte_start_sample_ = 0;
|
||||||
|
preamble_state_ = PreambleState::Locked;
|
||||||
|
is_preamb = false;
|
||||||
|
is_recive = true;
|
||||||
|
is_recive_raw = true;
|
||||||
|
rise_period_us = preamble_mean_period_us_;
|
||||||
|
prev_rise_us = t_us + static_cast<double>(preamble_mean_period_us_) / 2.0;
|
||||||
|
prev_rise_sample = sample + static_cast<uint64_t>(std::llround(
|
||||||
|
static_cast<double>(preamble_mean_period_us_) * 0.5 * static_cast<double>(fs) / 1e6));
|
||||||
|
trim_first_data_bit_cell_ = true;
|
||||||
|
|
||||||
|
// The analyzer chooses whether this is visible (Detailed) or folded into
|
||||||
|
// the full packet frame (Overview).
|
||||||
|
if (on_bit && preamble_bubble_start_valid_)
|
||||||
|
{
|
||||||
|
IrFoxEmitBit pe{};
|
||||||
|
pe.start_sample = static_cast<int64_t>(preamble_bubble_start_sample_);
|
||||||
|
pe.end_sample = static_cast<int64_t>(sample > 0 ? sample - 1 : sample);
|
||||||
|
pe.frame_type = IRF_FT_PREAMBLE;
|
||||||
|
fill_err_snapshot(pe);
|
||||||
|
on_bit(pe);
|
||||||
|
}
|
||||||
|
preamble_bubble_start_valid_ = false;
|
||||||
|
last_edge_time_us = t_us;
|
||||||
|
last_edge_sample = sample;
|
||||||
|
last_processed_edge_us = t_us;
|
||||||
|
have_last_processed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// As in processDecodedFront, the edge becomes the timing reference only
|
||||||
|
// 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_sample = sample;
|
||||||
|
|
||||||
if (rising)
|
if (rising)
|
||||||
{
|
{
|
||||||
const double delta_rp = t_us - prev_rise_us;
|
const double delta_rp = t_us - prev_rise_us;
|
||||||
@ -357,78 +565,6 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Как IR_DecoderRaw::tick: после длинной паузы старт сырого приёма (без отдельного firstRX — флаги ниже).
|
|
||||||
if (t_us > prev_rise_us && (t_us - prev_rise_us) > irmax * 2.0 && !is_recive_raw)
|
|
||||||
{
|
|
||||||
preamb_front_counter = static_cast<int8_t>(irfox::kPreambFronts - 1);
|
|
||||||
is_preamb = true;
|
|
||||||
is_recive = true;
|
|
||||||
is_recive_raw = true;
|
|
||||||
is_wrong_pack = false;
|
|
||||||
if (!preamble_bubble_start_valid_)
|
|
||||||
{
|
|
||||||
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising);
|
|
||||||
preamble_bubble_start_valid_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preamb_front_counter)
|
|
||||||
{
|
|
||||||
if (rising && rise_period_us < irmax)
|
|
||||||
{
|
|
||||||
if (rise_period_us < rise_min_us / 2U)
|
|
||||||
{
|
|
||||||
preamb_front_counter += 2;
|
|
||||||
err_other++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
preamb_front_counter--;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (is_preamb)
|
|
||||||
{
|
|
||||||
is_preamb = false;
|
|
||||||
// IR_DecoderRaw: prevRise += risePeriod / 2 — фаза как в прошивке.
|
|
||||||
// Бабл PRE: до текущего фронта (sample−1), чтобы охватить все kPreambPulse периодов (3 импульса),
|
|
||||||
// а не только до предыдущего подъёма (~2 периода).
|
|
||||||
const uint64_t preamble_bubble_end_sample = sample > 0 ? sample - 1 : sample;
|
|
||||||
prev_rise_us += rise_period_us / 2.0;
|
|
||||||
{
|
|
||||||
const double half_us = 0.5 * static_cast<double>(rise_period_us);
|
|
||||||
const uint64_t half_s = static_cast<uint64_t>(std::llround(half_us * double(fs) / 1e6));
|
|
||||||
prev_rise_sample += half_s;
|
|
||||||
}
|
|
||||||
trim_first_data_bit_cell_ = true;
|
|
||||||
if (on_bit && preamble_bubble_start_valid_)
|
|
||||||
{
|
|
||||||
int64_t pe_start = static_cast<int64_t>(preamble_bubble_start_sample_);
|
|
||||||
int64_t pe_end = static_cast<int64_t>(preamble_bubble_end_sample);
|
|
||||||
if (preamble_bubble_end_sample == 0 || pe_end < pe_start)
|
|
||||||
pe_end = static_cast<int64_t>(sample > 0 ? sample - 1 : sample);
|
|
||||||
IrFoxEmitBit pe{};
|
|
||||||
pe.start_sample = pe_start;
|
|
||||||
pe.end_sample = pe_end;
|
|
||||||
pe.frame_type = IRF_FT_PREAMBLE;
|
|
||||||
fill_err_snapshot(pe);
|
|
||||||
std::strncpy(pe.bubble_text, "PRE", sizeof pe.bubble_text);
|
|
||||||
pe.bubble_text[sizeof pe.bubble_text - 1] = '\0';
|
|
||||||
on_bit(pe);
|
|
||||||
}
|
|
||||||
preamble_bubble_start_valid_ = false;
|
|
||||||
last_processed_edge_us = t_us;
|
|
||||||
have_last_processed = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_preamb)
|
|
||||||
{
|
|
||||||
last_processed_edge_us = t_us;
|
|
||||||
have_last_processed = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rise_period_us > irmax || is_buffer_overflow || rise_period_us < rise_min_us || is_wrong_pack)
|
if (rise_period_us > irmax || is_buffer_overflow || rise_period_us < rise_min_us || is_wrong_pack)
|
||||||
{
|
{
|
||||||
last_processed_edge_us = t_us;
|
last_processed_edge_us = t_us;
|
||||||
|
|||||||
@ -8,11 +8,16 @@ enum IrFoxFrameType : uint8_t
|
|||||||
{
|
{
|
||||||
IRF_FT_DATA_BIT = 1,
|
IRF_FT_DATA_BIT = 1,
|
||||||
IRF_FT_SYNC_BIT = 2,
|
IRF_FT_SYNC_BIT = 2,
|
||||||
IRF_FT_PACKET_OK = 3,
|
IRF_FT_PACKET_ACCEPTED = 3,
|
||||||
IRF_FT_PACKET_CRC_FAIL = 4,
|
IRF_FT_PACKET_CRC_FAIL = 4,
|
||||||
IRF_FT_OVERFLOW = 5,
|
IRF_FT_OVERFLOW = 5,
|
||||||
IRF_FT_ABORT = 6,
|
IRF_FT_ABORT = 6,
|
||||||
IRF_FT_PREAMBLE = 7,
|
IRF_FT_PREAMBLE = 7,
|
||||||
|
IRF_FT_PACKET_BAD_LENGTH = 8,
|
||||||
|
IRF_FT_PACKET_RAW_ONLY = 9,
|
||||||
|
IRF_FT_PACKET_IGNORED_ADDRESS = 10,
|
||||||
|
IRF_FT_DATA_BYTE = 11,
|
||||||
|
IRF_FT_PACKET_OK = IRF_FT_PACKET_ACCEPTED,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
|
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
|
||||||
@ -41,6 +46,8 @@ struct IrFoxEmitBit
|
|||||||
struct IrFoxEmitPacket
|
struct IrFoxEmitPacket
|
||||||
{
|
{
|
||||||
int64_t start_sample;
|
int64_t start_sample;
|
||||||
|
/** First data-bit cell: the visible boundary between the preamble and payload. */
|
||||||
|
int64_t data_start_sample;
|
||||||
int64_t end_sample;
|
int64_t end_sample;
|
||||||
bool crc_ok;
|
bool crc_ok;
|
||||||
uint8_t pack_size;
|
uint8_t pack_size;
|
||||||
@ -101,6 +108,11 @@ private:
|
|||||||
uint64_t preamble_bubble_start_sample_ = 0;
|
uint64_t preamble_bubble_start_sample_ = 0;
|
||||||
bool preamble_bubble_start_valid_ = false;
|
bool preamble_bubble_start_valid_ = false;
|
||||||
bool trim_first_data_bit_cell_ = false;
|
bool trim_first_data_bit_cell_ = false;
|
||||||
|
uint64_t packet_start_sample_ = 0;
|
||||||
|
bool packet_start_valid_ = false;
|
||||||
|
uint64_t packet_data_start_sample_ = 0;
|
||||||
|
bool packet_data_start_valid_ = false;
|
||||||
|
uint64_t byte_start_sample_ = 0;
|
||||||
|
|
||||||
double last_edge_time_us = 0;
|
double last_edge_time_us = 0;
|
||||||
uint64_t last_edge_sample = 0;
|
uint64_t last_edge_sample = 0;
|
||||||
@ -123,7 +135,18 @@ private:
|
|||||||
int8_t all_count = 0;
|
int8_t all_count = 0;
|
||||||
|
|
||||||
uint16_t wrong_counter = 0;
|
uint16_t wrong_counter = 0;
|
||||||
int8_t preamb_front_counter = 0;
|
enum class PreambleState : uint8_t
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Candidate,
|
||||||
|
Locked,
|
||||||
|
};
|
||||||
|
PreambleState preamble_state_ = PreambleState::Idle;
|
||||||
|
uint8_t preamble_good_periods_ = 0;
|
||||||
|
uint32_t preamble_mean_period_us_ = 0;
|
||||||
|
double preamble_candidate_last_edge_us_ = 0;
|
||||||
|
double preamble_candidate_first_rise_us_ = 0;
|
||||||
|
bool preamble_candidate_first_rise_valid_ = false;
|
||||||
int16_t buf_bit_pos = 0;
|
int16_t buf_bit_pos = 0;
|
||||||
bool is_data = true;
|
bool is_data = true;
|
||||||
uint16_t i_data_buffer = 0;
|
uint16_t i_data_buffer = 0;
|
||||||
|
|||||||
172
Analyzer/raw/IR_Fox/src/IrFoxPacketClassifier.h
Normal file
172
Analyzer/raw/IR_Fox/src/IrFoxPacketClassifier.h
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IrFoxProtocolConstants.h"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The decision made after IR_DecoderRaw::availableRaw() in IR_Decoder::_tick().
|
||||||
|
* It intentionally models only what a capture can prove: decoding, CRC, typed
|
||||||
|
* layout, and the receiver address. It does not claim that application code
|
||||||
|
* subsequently acted on the delivered message.
|
||||||
|
*/
|
||||||
|
enum class IrFoxPacketOutcome : uint8_t
|
||||||
|
{
|
||||||
|
Accepted,
|
||||||
|
RawOnlyUnknownType,
|
||||||
|
RawOnlyTypedLength,
|
||||||
|
IgnoredAddress,
|
||||||
|
RejectedCrc,
|
||||||
|
RejectedLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IrFoxPacketDecision
|
||||||
|
{
|
||||||
|
IrFoxPacketOutcome outcome = IrFoxPacketOutcome::RejectedLength;
|
||||||
|
uint8_t message_type = 0;
|
||||||
|
uint8_t minimum_size = 0;
|
||||||
|
uint16_t destination = 0;
|
||||||
|
bool has_destination = false;
|
||||||
|
|
||||||
|
bool raw_accepted() const
|
||||||
|
{
|
||||||
|
return outcome == IrFoxPacketOutcome::Accepted || outcome == IrFoxPacketOutcome::RawOnlyUnknownType ||
|
||||||
|
outcome == IrFoxPacketOutcome::RawOnlyTypedLength || outcome == IrFoxPacketOutcome::IgnoredAddress;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace irfox {
|
||||||
|
|
||||||
|
constexpr uint8_t kMsgBack = 0U;
|
||||||
|
constexpr uint8_t kMsgAccept = 1U;
|
||||||
|
constexpr uint8_t kMsgRequest = 2U;
|
||||||
|
constexpr uint8_t kMsgBackTo = 4U;
|
||||||
|
constexpr uint8_t kMsgDataNoAccept = 6U;
|
||||||
|
constexpr uint8_t kMsgDataAccept = 7U;
|
||||||
|
constexpr uint16_t kBroadcastAddress = 65000U;
|
||||||
|
|
||||||
|
inline uint8_t messageType(uint8_t header)
|
||||||
|
{
|
||||||
|
return static_cast<uint8_t>((header >> 5U) & 0x07U);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline uint8_t minimumPacketSize(uint8_t message_type)
|
||||||
|
{
|
||||||
|
switch (message_type)
|
||||||
|
{
|
||||||
|
case kMsgDataAccept:
|
||||||
|
case kMsgDataNoAccept:
|
||||||
|
case kMsgBackTo:
|
||||||
|
case kMsgRequest:
|
||||||
|
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + kAddrBytes + kCrcBytes);
|
||||||
|
case kMsgBack:
|
||||||
|
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + kCrcBytes);
|
||||||
|
case kMsgAccept:
|
||||||
|
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + 1U + kCrcBytes);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool addressAcceptedByReceiver(uint16_t destination, uint16_t receiver_address)
|
||||||
|
{
|
||||||
|
return receiver_address == 0U || destination == receiver_address || destination >= kBroadcastAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline IrFoxPacketDecision classifyPacket(const uint8_t* data, uint8_t observed_size, bool crc_ok,
|
||||||
|
uint16_t receiver_address)
|
||||||
|
{
|
||||||
|
IrFoxPacketDecision result;
|
||||||
|
if (data == nullptr)
|
||||||
|
{
|
||||||
|
result.outcome = IrFoxPacketOutcome::RejectedLength;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.message_type = messageType(data[0]);
|
||||||
|
if (observed_size < kMsgBytes + kCrcBytes)
|
||||||
|
{
|
||||||
|
result.outcome = IrFoxPacketOutcome::RejectedLength;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if ((data[0] & 0x1FU) != observed_size)
|
||||||
|
{
|
||||||
|
result.outcome = IrFoxPacketOutcome::RejectedLength;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!crc_ok)
|
||||||
|
{
|
||||||
|
result.outcome = IrFoxPacketOutcome::RejectedCrc;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.minimum_size = minimumPacketSize(result.message_type);
|
||||||
|
if (result.minimum_size == 0U)
|
||||||
|
{
|
||||||
|
result.outcome = IrFoxPacketOutcome::RawOnlyUnknownType;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (observed_size < result.minimum_size)
|
||||||
|
{
|
||||||
|
result.outcome = IrFoxPacketOutcome::RawOnlyTypedLength;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool addressed = result.message_type == kMsgDataAccept || result.message_type == kMsgDataNoAccept ||
|
||||||
|
result.message_type == kMsgBackTo || result.message_type == kMsgRequest;
|
||||||
|
if (addressed)
|
||||||
|
{
|
||||||
|
result.has_destination = true;
|
||||||
|
result.destination = static_cast<uint16_t>((static_cast<uint16_t>(data[3]) << 8U) | data[4]);
|
||||||
|
if (!addressAcceptedByReceiver(result.destination, receiver_address))
|
||||||
|
{
|
||||||
|
result.outcome = IrFoxPacketOutcome::IgnoredAddress;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.outcome = IrFoxPacketOutcome::Accepted;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* packetOutcomeText(IrFoxPacketOutcome outcome)
|
||||||
|
{
|
||||||
|
switch (outcome)
|
||||||
|
{
|
||||||
|
case IrFoxPacketOutcome::Accepted:
|
||||||
|
return "ACCEPT";
|
||||||
|
case IrFoxPacketOutcome::RawOnlyUnknownType:
|
||||||
|
return "RAW TYPE";
|
||||||
|
case IrFoxPacketOutcome::RawOnlyTypedLength:
|
||||||
|
return "RAW SIZE";
|
||||||
|
case IrFoxPacketOutcome::IgnoredAddress:
|
||||||
|
return "IGNORE ADDR";
|
||||||
|
case IrFoxPacketOutcome::RejectedCrc:
|
||||||
|
return "REJECT CRC";
|
||||||
|
case IrFoxPacketOutcome::RejectedLength:
|
||||||
|
return "REJECT LEN";
|
||||||
|
}
|
||||||
|
return "REJECT";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* messageTypeText(uint8_t message_type)
|
||||||
|
{
|
||||||
|
switch (message_type)
|
||||||
|
{
|
||||||
|
case kMsgBack:
|
||||||
|
return "BACK";
|
||||||
|
case kMsgAccept:
|
||||||
|
return "ACCEPT";
|
||||||
|
case kMsgRequest:
|
||||||
|
return "REQUEST";
|
||||||
|
case kMsgBackTo:
|
||||||
|
return "BACK_TO";
|
||||||
|
case kMsgDataNoAccept:
|
||||||
|
return "DATA";
|
||||||
|
case kMsgDataAccept:
|
||||||
|
return "DATA_ACK";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace irfox
|
||||||
@ -11,8 +11,12 @@ constexpr uint32_t kBitTakts = kBitActiveTakts + kBitPauseTakts;
|
|||||||
constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs;
|
constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs;
|
||||||
constexpr uint32_t kToleranceUs = 300U;
|
constexpr uint32_t kToleranceUs = 300U;
|
||||||
|
|
||||||
/** Мин. длительность плато (мкс) для потокового анти-глитча в анализаторе; согласовано с IR_INPUT_MIN_PULSE_US. */
|
/**
|
||||||
constexpr uint32_t kMinFilteredPulseUs = 10U;
|
* Must match IR_INPUT_MIN_PULSE_US in the firmware configuration. The current
|
||||||
|
* receiver configuration keeps this filter disabled, so a capture must not
|
||||||
|
* silently lose short edges that the receiver would see.
|
||||||
|
*/
|
||||||
|
constexpr uint32_t kMinFilteredPulseUs = 0U;
|
||||||
|
|
||||||
constexpr uint8_t kBitPerByte = 8U;
|
constexpr uint8_t kBitPerByte = 8U;
|
||||||
constexpr uint8_t kMsgBytes = 1;
|
constexpr uint8_t kMsgBytes = 1;
|
||||||
@ -27,6 +31,12 @@ constexpr uint8_t kDataByteSizeMax =
|
|||||||
|
|
||||||
constexpr uint8_t kPreambPulse = 3;
|
constexpr uint8_t kPreambPulse = 3;
|
||||||
constexpr uint8_t kPreambFronts = kPreambPulse * 2U;
|
constexpr uint8_t kPreambFronts = kPreambPulse * 2U;
|
||||||
|
constexpr uint8_t kPreambleLockRisePeriods = 2U;
|
||||||
|
constexpr uint8_t kPreambleJitterPct = 18U;
|
||||||
|
constexpr uint32_t kPreambleJitterUsMin = 80U;
|
||||||
|
constexpr uint32_t kPreamblePeriodMinFactorPct = 220U;
|
||||||
|
constexpr uint32_t kPreamblePeriodMaxFactorPct = 340U;
|
||||||
|
constexpr uint32_t kPreambleCandidateTimeoutMult = 3U;
|
||||||
|
|
||||||
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
|
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
|
||||||
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
|
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
|
||||||
@ -53,6 +63,19 @@ inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs)
|
|||||||
return lo < periodUs && periodUs < hi;
|
return lo < periodUs && periodUs < hi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline uint32_t preambleJitterTolUs(uint32_t baselineUs)
|
||||||
|
{
|
||||||
|
const uint32_t pct = (baselineUs * kPreambleJitterPct) / 100U;
|
||||||
|
return pct > kPreambleJitterUsMin ? pct : kPreambleJitterUsMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool preambleRisePeriodCoarseOk(uint32_t periodUs)
|
||||||
|
{
|
||||||
|
const uint32_t min_period = (kBitTimeUs * kPreamblePeriodMinFactorPct) / 100U;
|
||||||
|
const uint32_t max_period = (kBitTimeUs * kPreamblePeriodMaxFactorPct) / 100U;
|
||||||
|
return periodUs >= min_period && periodUs <= max_period;
|
||||||
|
}
|
||||||
|
|
||||||
inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us)
|
inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us)
|
||||||
{
|
{
|
||||||
#if IRFOX_GLITCH_REJECT_PHASE_NUDGE
|
#if IRFOX_GLITCH_REJECT_PHASE_NUDGE
|
||||||
|
|||||||
115
Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp
Normal file
115
Analyzer/raw/IR_Fox/tests/IrFoxDecoderTests.cpp
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
#include "IrFoxDecoder.h"
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
uint8_t crc8(const uint8_t* data, uint8_t end, uint8_t poly)
|
||||||
|
{
|
||||||
|
uint8_t crc = 0xFF;
|
||||||
|
for (uint8_t i = 0; i < end; ++i)
|
||||||
|
{
|
||||||
|
crc ^= data[i];
|
||||||
|
for (uint8_t bit = 0; bit < 8; ++bit)
|
||||||
|
crc = (crc & 0x80U) ? static_cast<uint8_t>((crc << 1U) ^ poly) : static_cast<uint8_t>(crc << 1U);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DecoderHarness
|
||||||
|
{
|
||||||
|
IrFoxDecoder decoder;
|
||||||
|
std::vector<IrFoxEmitPacket> packets;
|
||||||
|
std::vector<IrFoxEmitBit> events;
|
||||||
|
uint64_t phase = 0;
|
||||||
|
static constexpr uint32_t kFs = 1000000U;
|
||||||
|
|
||||||
|
DecoderHarness()
|
||||||
|
{
|
||||||
|
decoder.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void edge(uint64_t sample, bool rising)
|
||||||
|
{
|
||||||
|
decoder.processEdge(sample, rising, kFs, [this](const IrFoxEmitBit& event) { events.push_back(event); },
|
||||||
|
[this](const IrFoxEmitPacket& packet) { packets.push_back(packet); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void lockPreamble()
|
||||||
|
{
|
||||||
|
constexpr uint64_t first_rise = 40000;
|
||||||
|
constexpr uint64_t period = irfox::kBitTimeUs * 3U;
|
||||||
|
edge(first_rise, true);
|
||||||
|
edge(first_rise + 700U, false);
|
||||||
|
edge(first_rise + period, true);
|
||||||
|
edge(first_rise + period + 700U, false);
|
||||||
|
edge(first_rise + period * 2U, true);
|
||||||
|
phase = first_rise + period * 2U + period / 2U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void emitCell(bool bit)
|
||||||
|
{
|
||||||
|
// The decoder calls a cell a one when the inactive (HIGH) interval is
|
||||||
|
// longer than the active (LOW) interval. The waveform is TSOP output.
|
||||||
|
const uint64_t high_us = bit ? 262U : 700U;
|
||||||
|
edge(phase + high_us, false);
|
||||||
|
phase += irfox::kBitTimeUs;
|
||||||
|
edge(phase, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void emitByte(uint8_t value, bool emit_sync)
|
||||||
|
{
|
||||||
|
for (uint8_t i = 0; i < 8; ++i)
|
||||||
|
emitCell((value & static_cast<uint8_t>(0x80U >> i)) != 0U);
|
||||||
|
if (emit_sync)
|
||||||
|
{
|
||||||
|
const bool sync = (value & 1U) == 0U;
|
||||||
|
for (uint8_t i = 0; i < irfox::kSyncBits; ++i)
|
||||||
|
emitCell(sync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
uint8_t packet[] = {0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
|
||||||
|
packet[5] = crc8(packet, 5, irfox::kPoly1);
|
||||||
|
packet[6] = crc8(packet, 6, irfox::kPoly2);
|
||||||
|
|
||||||
|
DecoderHarness valid;
|
||||||
|
valid.lockPreamble();
|
||||||
|
for (uint8_t i = 0; i < sizeof packet; ++i)
|
||||||
|
valid.emitByte(packet[i], i + 1U != sizeof packet);
|
||||||
|
assert(valid.packets.size() == 1U);
|
||||||
|
assert(valid.packets[0].crc_ok);
|
||||||
|
assert(valid.packets[0].pack_size == sizeof packet);
|
||||||
|
assert(valid.packets[0].start_sample == 40000);
|
||||||
|
assert(valid.packets[0].start_sample < valid.packets[0].end_sample);
|
||||||
|
bool saw_preamble = false;
|
||||||
|
std::vector<uint8_t> decoded_bytes;
|
||||||
|
for (const IrFoxEmitBit& event : valid.events)
|
||||||
|
{
|
||||||
|
if (event.frame_type == IRF_FT_PREAMBLE)
|
||||||
|
{
|
||||||
|
saw_preamble = true;
|
||||||
|
assert(event.start_sample == 40000);
|
||||||
|
}
|
||||||
|
if (event.frame_type == IRF_FT_DATA_BYTE)
|
||||||
|
decoded_bytes.push_back(static_cast<uint8_t>(event.bit_value));
|
||||||
|
}
|
||||||
|
assert(saw_preamble);
|
||||||
|
assert(decoded_bytes.size() == sizeof packet);
|
||||||
|
for (uint8_t i = 0; i < sizeof packet; ++i)
|
||||||
|
assert(decoded_bytes[i] == packet[i]);
|
||||||
|
|
||||||
|
DecoderHarness too_short;
|
||||||
|
too_short.lockPreamble();
|
||||||
|
too_short.emitByte(0xE1, false);
|
||||||
|
assert(too_short.packets.size() == 1U);
|
||||||
|
assert(!too_short.packets[0].crc_ok);
|
||||||
|
assert(too_short.packets[0].pack_size == 1U);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
43
Analyzer/raw/IR_Fox/tests/IrFoxPacketClassifierTests.cpp
Normal file
43
Analyzer/raw/IR_Fox/tests/IrFoxPacketClassifierTests.cpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#include "IrFoxPacketClassifier.h"
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
static IrFoxPacketDecision classify(const uint8_t* data, uint8_t size, bool crc_ok, uint16_t id = 0)
|
||||||
|
{
|
||||||
|
return irfox::classifyPacket(data, size, crc_ok, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const uint8_t data_to_42[] = {0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
|
||||||
|
assert(classify(data_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
|
||||||
|
assert(classify(data_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
|
||||||
|
|
||||||
|
const uint8_t broadcast_data[] = {0xC7, 0x00, 0x01, 0xFD, 0xE8, 0x00, 0x00};
|
||||||
|
assert(classify(broadcast_data, 7, true, 41).outcome == IrFoxPacketOutcome::Accepted);
|
||||||
|
const uint8_t request_to_42[] = {0x47, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
|
||||||
|
assert(classify(request_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
|
||||||
|
assert(classify(request_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
|
||||||
|
const uint8_t back_to_42[] = {0x87, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
|
||||||
|
assert(classify(back_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
|
||||||
|
assert(classify(back_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
|
||||||
|
|
||||||
|
const uint8_t back[] = {0x05, 0x00, 0x01, 0x00, 0x00};
|
||||||
|
assert(classify(back, 5, true, 41).outcome == IrFoxPacketOutcome::Accepted);
|
||||||
|
|
||||||
|
const uint8_t accept[] = {0x26, 0x00, 0x01, 0x55, 0x00, 0x00};
|
||||||
|
assert(classify(accept, 6, true, 41).outcome == IrFoxPacketOutcome::Accepted);
|
||||||
|
|
||||||
|
const uint8_t unknown[] = {0x63, 0x00, 0x00};
|
||||||
|
assert(classify(unknown, 3, true).outcome == IrFoxPacketOutcome::RawOnlyUnknownType);
|
||||||
|
|
||||||
|
const uint8_t short_data[] = {0xE5, 0x00, 0x01, 0x00, 0x2A};
|
||||||
|
assert(classify(short_data, 5, true).outcome == IrFoxPacketOutcome::RawOnlyTypedLength);
|
||||||
|
assert(classify(data_to_42, 7, false).outcome == IrFoxPacketOutcome::RejectedCrc);
|
||||||
|
|
||||||
|
const uint8_t too_short[] = {0xE2, 0x00};
|
||||||
|
const IrFoxPacketDecision short_decision = classify(too_short, 2, false);
|
||||||
|
assert(short_decision.outcome == IrFoxPacketOutcome::RejectedLength);
|
||||||
|
assert(short_decision.message_type == irfox::kMsgDataAccept);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user