mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-09-21 20:39:35 +00:00
Compare commits
1 Commits
car-v4.3.1
...
archive/ar
| Author | SHA1 | Date | |
|---|---|---|---|
| 57db9c35b8 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,5 +11,3 @@ Analyzer/raw/dll/*.dylib
|
|||||||
/Analyzer/raw/IR_Fox/.github
|
/Analyzer/raw/IR_Fox/.github
|
||||||
**/.build
|
**/.build
|
||||||
graphify-out/*
|
graphify-out/*
|
||||||
**/.build-*/
|
|
||||||
/tests/*.exe
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ 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
|
||||||
@ -25,23 +24,3 @@ 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,7 +1,6 @@
|
|||||||
#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>
|
||||||
@ -26,6 +25,7 @@ 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,91 +46,35 @@ static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_by
|
|||||||
s += "...";
|
s += "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* packet_status_icon(IrFoxPacketOutcome outcome)
|
const char* IrFoxAnalyzer::PacketHexForFrame(U64 frame_id)
|
||||||
{
|
{
|
||||||
switch (outcome)
|
auto it = m_packet_hex_by_frame.find(frame_id);
|
||||||
{
|
if (it == m_packet_hex_by_frame.end())
|
||||||
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 const char* terminal_abort_cause_text(IrFoxAbortCause cause)
|
|
||||||
{
|
|
||||||
switch (cause)
|
|
||||||
{
|
|
||||||
case IrFoxAbortCause::BadSync:
|
|
||||||
return "SYNC";
|
|
||||||
case IrFoxAbortCause::BadLength:
|
|
||||||
return "LEN";
|
|
||||||
case IrFoxAbortCause::Overflow:
|
|
||||||
return "OVF";
|
|
||||||
case IrFoxAbortCause::None:
|
|
||||||
default:
|
|
||||||
return "";
|
return "";
|
||||||
}
|
m_hex_scratch = it->second;
|
||||||
|
return m_hex_scratch.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string packet_icon(const IrFoxPacketDecision& decision, IrFoxPacketIconMode mode)
|
const char* IrFoxAnalyzer::BubbleTextForFrame(U64 frame_id) const
|
||||||
{
|
{
|
||||||
const char* status = packet_status_icon(decision.outcome);
|
auto it = m_bubble_text_by_frame.find(frame_id);
|
||||||
// Icon-mode selection describes successfully accepted packets. Diagnostic
|
if (it == m_bubble_text_by_frame.end())
|
||||||
// outcomes must remain visible even when the user selected type-only mode.
|
return "";
|
||||||
if (decision.outcome != IrFoxPacketOutcome::Accepted)
|
m_bubble_scratch = it->second;
|
||||||
return status;
|
return m_bubble_scratch.c_str();
|
||||||
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);
|
||||||
mResults->ClearCachedFrameText();
|
m_packet_hex_by_frame.clear();
|
||||||
|
m_bubble_text_by_frame.clear();
|
||||||
|
|
||||||
const U32 fs = GetSampleRate();
|
const U32 fs = GetSampleRate();
|
||||||
IrFoxDecoder decoder;
|
IrFoxDecoder decoder;
|
||||||
decoder.reset();
|
decoder.reset();
|
||||||
|
|
||||||
/** Mirrors the firmware input filter. kMinFilteredPulseUs=0 means direct edge delivery. */
|
/** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */
|
||||||
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
|
||||||
@ -171,19 +115,8 @@ 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);
|
|
||||||
|
|
||||||
auto note_legacy_frame = [&]() {
|
IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) {
|
||||||
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);
|
||||||
@ -191,255 +124,45 @@ 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;
|
||||||
mResults->AddFrame(frame);
|
// В SDK только ERROR/WARNING меняют цвет бабла; sync выделяем янтарным (как warning), данные — обычные.
|
||||||
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)
|
if (e.frame_type == IRF_FT_SYNC_BIT)
|
||||||
return;
|
frame.mFlags |= DISPLAY_AS_WARNING_FLAG;
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Terminal events are independent of per-bit rendering. Overview passes an
|
|
||||||
// empty on_bit callback for speed, but must still show an interrupted frame.
|
|
||||||
IrFoxOnTerminal on_terminal = [&](const IrFoxEmitTerminal& terminal) {
|
|
||||||
Frame frame;
|
|
||||||
if (detailed_presentation && !pending_byte_frames.empty())
|
|
||||||
{
|
|
||||||
// Preserve every completed byte except the final one. The terminal
|
|
||||||
// frame replaces that last byte so legacy frames never overlap.
|
|
||||||
for (size_t i = 0; i + 1 < pending_byte_frames.size(); ++i)
|
|
||||||
add_event_frame(pending_byte_frames[i]);
|
|
||||||
frame.mStartingSampleInclusive = static_cast<S64>(pending_byte_frames.back().start_sample);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
frame.mStartingSampleInclusive = static_cast<S64>(detailed_presentation ?
|
|
||||||
terminal.detail_start_sample : terminal.start_sample);
|
|
||||||
}
|
|
||||||
frame.mEndingSampleInclusive = static_cast<S64>(terminal.end_sample);
|
|
||||||
if (frame.mStartingSampleInclusive > frame.mEndingSampleInclusive)
|
|
||||||
frame.mStartingSampleInclusive = frame.mEndingSampleInclusive;
|
|
||||||
frame.mType = terminal.reason == IrFoxTerminalReason::Timeout ? IRF_FT_TIMEOUT :
|
|
||||||
(terminal.cause == IrFoxAbortCause::Overflow ? IRF_FT_OVERFLOW : IRF_FT_ABORT);
|
|
||||||
frame.mData1 = terminal.declared_size;
|
|
||||||
frame.mData2 = U64(terminal.received_bits) |
|
|
||||||
(U64(terminal.err_low) << 16) | (U64(terminal.err_high) << 24) | (U64(terminal.err_other) << 32) |
|
|
||||||
(U64(terminal.message_type) << 40) | (U64(terminal.cause) << 48) | (U64(terminal.reason) << 56);
|
|
||||||
frame.mFlags = DISPLAY_AS_ERROR_FLAG;
|
|
||||||
const U64 fid = mResults->AddFrame(frame);
|
const U64 fid = mResults->AddFrame(frame);
|
||||||
pending_byte_frames.clear();
|
if (e.bubble_text[0] != '\0')
|
||||||
|
m_bubble_text_by_frame[fid] = e.bubble_text;
|
||||||
std::string short_text;
|
if (++frames_since_commit >= kCommitBatch)
|
||||||
if (terminal.reason == IrFoxTerminalReason::Timeout)
|
|
||||||
short_text = "❌ TIMEOUT";
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
short_text = "❌ ABORT";
|
mResults->CommitResults();
|
||||||
const char* cause = terminal_abort_cause_text(terminal.cause);
|
frames_since_commit = 0;
|
||||||
if (*cause != '\0')
|
|
||||||
short_text += std::string(" ") + cause;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string detail = short_text + " · got=" + std::to_string(terminal.received_bits) + "b";
|
|
||||||
if (terminal.message_type != 0xFFU)
|
|
||||||
{
|
|
||||||
detail += " · ";
|
|
||||||
detail += irfox::messageTypeText(terminal.message_type);
|
|
||||||
detail += " len=" + std::to_string(terminal.declared_size) + "B";
|
|
||||||
}
|
|
||||||
if (terminal.err_low != 0U || terminal.err_high != 0U || terminal.err_other != 0U)
|
|
||||||
{
|
|
||||||
detail += " · err=" + std::to_string(terminal.err_low) + "/" +
|
|
||||||
std::to_string(terminal.err_high) + "/" + std::to_string(terminal.err_other);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto cached_text = std::make_shared<IrFoxCachedFrameText>();
|
|
||||||
cached_text->bubble_texts[0] = "❌";
|
|
||||||
cached_text->bubble_texts[1] = short_text;
|
|
||||||
cached_text->bubble_texts[2] = detail;
|
|
||||||
cached_text->bubble_text_count = 3;
|
|
||||||
mResults->CacheFrameText(fid, cached_text);
|
|
||||||
|
|
||||||
if (detailed_presentation)
|
|
||||||
mResults->AddMarker(static_cast<U64>(terminal.end_sample), AnalyzerResults::ErrorX,
|
|
||||||
mSettings.mInputChannel);
|
|
||||||
note_legacy_frame();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) {
|
IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) {
|
||||||
const IrFoxPacketDecision decision =
|
|
||||||
irfox::classifyPacket(p.data_bytes, p.pack_size, p.crc_ok, mSettings.mReceiverAddress);
|
|
||||||
Frame frame;
|
Frame frame;
|
||||||
if (detailed_presentation)
|
frame.mStartingSampleInclusive = static_cast<S64>(p.start_sample);
|
||||||
{
|
|
||||||
// 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.mFlags = 0;
|
frame.mType = p.crc_ok ? IRF_FT_PACKET_OK : IRF_FT_PACKET_CRC_FAIL;
|
||||||
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);
|
||||||
std::string status = irfox::packetOutcomeText(decision.outcome);
|
m_packet_hex_by_frame[fid] = std::move(hx);
|
||||||
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>();
|
FrameV2 fv2;
|
||||||
cached_text->export_hex = hx;
|
fv2.AddBoolean("crc_ok", p.crc_ok);
|
||||||
cached_text->bubble_texts[0] = icon;
|
fv2.AddInteger("len", static_cast<S64>(p.pack_size));
|
||||||
if (detailed_presentation)
|
fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
|
||||||
{
|
fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
|
||||||
char last_byte[3] = "??";
|
fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
|
||||||
if (p.pack_size > 0)
|
fv2.AddByteArray("data", p.data_bytes, p.pack_size);
|
||||||
std::snprintf(last_byte, sizeof last_byte, "%02X", static_cast<unsigned>(p.data_bytes[p.pack_size - 1]));
|
mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast<U64>(p.start_sample),
|
||||||
cached_text->bubble_texts[1] = std::string("0x") + last_byte + " " + icon;
|
static_cast<U64>(p.end_sample));
|
||||||
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)
|
||||||
{
|
{
|
||||||
@ -447,10 +170,6 @@ 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 (;;)
|
||||||
@ -461,7 +180,7 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
return;
|
return;
|
||||||
if (pending[1].sample - pending[0].sample < min_seg_samples)
|
if (pending[1].sample - pending[0].sample < min_seg_samples)
|
||||||
continue;
|
continue;
|
||||||
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal);
|
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, 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());
|
||||||
@ -473,7 +192,7 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
strip_vs_last_decoder();
|
strip_vs_last_decoder();
|
||||||
while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples)
|
while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples)
|
||||||
{
|
{
|
||||||
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal);
|
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, 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());
|
||||||
@ -482,7 +201,7 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
}
|
}
|
||||||
if (pending.size() == 1)
|
if (pending.size() == 1)
|
||||||
{
|
{
|
||||||
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal);
|
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, 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();
|
||||||
@ -511,9 +230,7 @@ void IrFoxAnalyzer::WorkerThread()
|
|||||||
}
|
}
|
||||||
|
|
||||||
flush_pending_tail();
|
flush_pending_tail();
|
||||||
decoder.flushEnd(mIr->GetSampleNumber(), fs, bit_events, on_pkt, on_terminal);
|
decoder.flushEnd(mIr->GetSampleNumber(), fs, on_bit, on_pkt);
|
||||||
if (detailed_presentation)
|
|
||||||
flush_pending_bytes();
|
|
||||||
|
|
||||||
if (frames_since_commit != 0)
|
if (frames_since_commit != 0)
|
||||||
mResults->CommitResults();
|
mResults->CommitResults();
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
#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
|
||||||
{
|
{
|
||||||
@ -23,6 +25,9 @@ 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;
|
||||||
@ -31,6 +36,10 @@ 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,7 +6,6 @@
|
|||||||
#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(),
|
||||||
@ -19,89 +18,51 @@ 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_BYTE:
|
case IRF_FT_DATA_BIT:
|
||||||
{
|
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:
|
||||||
{
|
|
||||||
AddResultString("📡");
|
|
||||||
AddResultString("📡 PRE");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case IRF_FT_OVERFLOW:
|
case IRF_FT_OVERFLOW:
|
||||||
case IRF_FT_ABORT:
|
case IRF_FT_ABORT:
|
||||||
case IRF_FT_TIMEOUT:
|
|
||||||
{
|
{
|
||||||
if (add_cached_text())
|
const char* bt = mAnalyzer->BubbleTextForFrame(frame_index);
|
||||||
break;
|
if (bt && bt[0])
|
||||||
AddResultString("❌");
|
AddResultString(bt);
|
||||||
AddResultString(frame.mType == IRF_FT_TIMEOUT ? "❌ TIMEOUT" :
|
else if (frame.mType == IRF_FT_DATA_BIT)
|
||||||
(frame.mType == IRF_FT_OVERFLOW ? "❌ ABORT OVF" : "❌ ABORT"));
|
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");
|
||||||
|
else if (frame.mType == IRF_FT_ABORT)
|
||||||
|
AddResultString("SYNC!");
|
||||||
|
else
|
||||||
|
AddResultString("PRE");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case IRF_FT_PACKET_ACCEPTED:
|
case IRF_FT_PACKET_OK:
|
||||||
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:
|
|
||||||
{
|
{
|
||||||
if (!add_cached_text())
|
snprintf(line, sizeof line, "%s %lluB", frame.mType == IRF_FT_PACKET_OK ? "OK" : "CRC",
|
||||||
AddResultString(frame.mType == IRF_FT_PACKET_ACCEPTED ? "✅" :
|
(unsigned long long)frame.mData1);
|
||||||
(frame.mType == IRF_FT_PACKET_CRC_FAIL || frame.mType == IRF_FT_PACKET_BAD_LENGTH) ? "❌" :
|
AddResultString(line);
|
||||||
frame.mType == IRF_FT_PACKET_IGNORED_ADDRESS ? "📭" : "⚠️");
|
const char* hx = mAnalyzer->PacketHexForFrame(frame_index);
|
||||||
|
if (hx && hx[0])
|
||||||
|
AddResultString(hx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,44 +95,23 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
|
|||||||
const char* typ = "?";
|
const char* typ = "?";
|
||||||
switch (frame.mType)
|
switch (frame.mType)
|
||||||
{
|
{
|
||||||
case IRF_FT_PACKET_ACCEPTED:
|
case IRF_FT_DATA_BIT:
|
||||||
typ = "ACCEPT";
|
typ = "D";
|
||||||
|
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 = "REJECT_CRC";
|
typ = "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 = "ABORT_OVF";
|
typ = "OVF";
|
||||||
break;
|
break;
|
||||||
case IRF_FT_ABORT:
|
case IRF_FT_ABORT:
|
||||||
switch (static_cast<IrFoxAbortCause>((frame.mData2 >> 48) & 0xFFull))
|
typ = "ABORT";
|
||||||
{
|
|
||||||
case IrFoxAbortCause::BadSync:
|
|
||||||
typ = "ABORT_SYNC";
|
|
||||||
break;
|
|
||||||
case IrFoxAbortCause::BadLength:
|
|
||||||
typ = "ABORT_LEN";
|
|
||||||
break;
|
|
||||||
case IrFoxAbortCause::Overflow:
|
|
||||||
typ = "ABORT_OVF";
|
|
||||||
break;
|
|
||||||
case IrFoxAbortCause::None:
|
|
||||||
default:
|
|
||||||
typ = "ABORT";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case IRF_FT_TIMEOUT:
|
|
||||||
typ = "TIMEOUT";
|
|
||||||
break;
|
break;
|
||||||
case IRF_FT_PREAMBLE:
|
case IRF_FT_PREAMBLE:
|
||||||
typ = "PRE";
|
typ = "PRE";
|
||||||
@ -180,12 +120,14 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::shared_ptr<const IrFoxCachedFrameText> cached = CachedFrameTextForFrame(i);
|
const char* hx = mAnalyzer->PacketHexForFrame(i);
|
||||||
const char* hx = cached ? cached->export_hex.c_str() : "";
|
if (!hx)
|
||||||
|
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_OVERFLOW || frame.mType == IRF_FT_ABORT || frame.mType == IRF_FT_TIMEOUT)
|
if (frame.mType == IRF_FT_DATA_BIT || frame.mType == IRF_FT_SYNC_BIT ||
|
||||||
|
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,22 +2,10 @@
|
|||||||
#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:
|
||||||
@ -31,15 +19,9 @@ 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,42 +3,14 @@
|
|||||||
|
|
||||||
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
|
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
|
||||||
: mInputChannel(UNDEFINED_CHANNEL),
|
: mInputChannel(UNDEFINED_CHANNEL),
|
||||||
mReceiverAddress(0),
|
mInputChannelInterface()
|
||||||
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");
|
||||||
@ -55,23 +27,6 @@ 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);
|
||||||
@ -82,9 +37,6 @@ 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)
|
||||||
@ -93,50 +45,6 @@ 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);
|
||||||
@ -149,11 +57,6 @@ 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,22 +3,6 @@
|
|||||||
|
|
||||||
#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
|
||||||
{
|
{
|
||||||
@ -32,16 +16,9 @@ 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
|
||||||
|
|||||||
@ -52,20 +52,6 @@ bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out)
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IrFoxDecoder::preamble_reset_to_idle()
|
|
||||||
{
|
|
||||||
preamble_state_ = PreambleState::Idle;
|
|
||||||
preamble_good_periods_ = 0;
|
|
||||||
preamble_mean_period_us_ = 0;
|
|
||||||
preamble_candidate_last_edge_us_ = 0;
|
|
||||||
preamble_candidate_first_rise_us_ = 0;
|
|
||||||
preamble_candidate_first_rise_valid_ = false;
|
|
||||||
preamble_bubble_start_valid_ = false;
|
|
||||||
is_preamb = false;
|
|
||||||
is_wrong_pack = false;
|
|
||||||
is_buffer_overflow = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IrFoxDecoder::first_rx()
|
void IrFoxDecoder::first_rx()
|
||||||
{
|
{
|
||||||
err_low_signal = err_high_signal = err_other = 0;
|
err_low_signal = err_high_signal = err_other = 0;
|
||||||
@ -79,7 +65,7 @@ void IrFoxDecoder::first_rx()
|
|||||||
i_sync_bit = 0;
|
i_sync_bit = 0;
|
||||||
err_sync_bit = 0;
|
err_sync_bit = 0;
|
||||||
is_wrong_pack = false;
|
is_wrong_pack = false;
|
||||||
is_preamb = false;
|
is_preamb = true;
|
||||||
is_recive = false;
|
is_recive = false;
|
||||||
is_recive_raw = false;
|
is_recive_raw = false;
|
||||||
msg_type_receive = 0;
|
msg_type_receive = 0;
|
||||||
@ -87,66 +73,6 @@ 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_reset_to_idle();
|
|
||||||
}
|
|
||||||
|
|
||||||
void IrFoxDecoder::release_preamble_guard(double t_us)
|
|
||||||
{
|
|
||||||
const uint32_t long_silence_us = irfox::irTimeoutUs(rise_sync_time_us) * 2U;
|
|
||||||
// Mirror IR_DecoderRaw::releasePreambleGuard. A negative value is the
|
|
||||||
// floating-point equivalent of the firmware's wrap-safe unsigned offset.
|
|
||||||
prev_rise_us = t_us - static_cast<double>(long_silence_us) - 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IrFoxDecoder::emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
|
|
||||||
const IrFoxOnTerminal& on_terminal) const
|
|
||||||
{
|
|
||||||
if (!on_terminal)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IrFoxEmitTerminal terminal{};
|
|
||||||
const uint64_t start_sample = packet_start_valid_ ? packet_start_sample_ :
|
|
||||||
(packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
|
|
||||||
terminal.start_sample = static_cast<int64_t>(start_sample);
|
|
||||||
terminal.detail_start_sample = static_cast<int64_t>(
|
|
||||||
packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
|
|
||||||
terminal.end_sample = static_cast<int64_t>(end_sample);
|
|
||||||
terminal.reason = reason;
|
|
||||||
terminal.cause = cause;
|
|
||||||
terminal.message_type = i_data_buffer >= irfox::kBitPerByte ?
|
|
||||||
static_cast<uint8_t>((data_buffer[0] >> 5U) & 0x07U) : 0xFFU;
|
|
||||||
terminal.declared_size = static_cast<uint8_t>(pack_size);
|
|
||||||
terminal.received_bits = i_data_buffer;
|
|
||||||
terminal.err_low = err_low_signal;
|
|
||||||
terminal.err_high = err_high_signal;
|
|
||||||
terminal.err_other = err_other;
|
|
||||||
on_terminal(terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IrFoxDecoder::abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
|
|
||||||
const IrFoxOnTerminal& on_terminal)
|
|
||||||
{
|
|
||||||
emit_terminal(IrFoxTerminalReason::Abort, cause, end_sample, on_terminal);
|
|
||||||
is_recive = false;
|
|
||||||
is_recive_raw = false;
|
|
||||||
msg_type_receive = 0;
|
|
||||||
first_rx();
|
|
||||||
release_preamble_guard(t_us);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IrFoxDecoder::expire_preamble_candidate(double t_us)
|
|
||||||
{
|
|
||||||
if (preamble_state_ != PreambleState::Candidate)
|
|
||||||
return;
|
|
||||||
const uint32_t timeout_us =
|
|
||||||
irfox::irTimeoutUs(rise_sync_time_us) * irfox::kPreambleCandidateTimeoutMult;
|
|
||||||
if ((t_us - preamble_candidate_last_edge_us_) > static_cast<double>(timeout_us))
|
|
||||||
preamble_reset_to_idle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void IrFoxDecoder::listen_start(double t_us)
|
void IrFoxDecoder::listen_start(double t_us)
|
||||||
@ -160,20 +86,13 @@ void IrFoxDecoder::listen_start(double t_us)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IrFoxDecoder::check_timeout(double t_us, uint32_t fs, const IrFoxOnTerminal& on_terminal)
|
void IrFoxDecoder::check_timeout(double t_us)
|
||||||
{
|
{
|
||||||
if (!is_recive)
|
if (!is_recive)
|
||||||
return;
|
return;
|
||||||
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
|
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
|
||||||
if (t_us - last_edge_time_us > irmax * 2.0)
|
if (t_us - last_edge_time_us > irmax * 2.0)
|
||||||
{
|
{
|
||||||
const uint64_t timeout_us = static_cast<uint64_t>(irmax) * 2U;
|
|
||||||
// The callback fires only after the strict > 2T boundary, but the terminal
|
|
||||||
// frame owns samples only through 2T. This leaves a following edge free to
|
|
||||||
// seed the next preamble without overlapping inclusive Saleae frames.
|
|
||||||
const uint64_t timeout_samples = (timeout_us * static_cast<uint64_t>(fs)) / 1000000ULL;
|
|
||||||
emit_terminal(IrFoxTerminalReason::Timeout, IrFoxAbortCause::None,
|
|
||||||
last_edge_sample + timeout_samples, on_terminal);
|
|
||||||
// Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM.
|
// Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM.
|
||||||
is_recive = false;
|
is_recive = false;
|
||||||
msg_type_receive = 0;
|
msg_type_receive = 0;
|
||||||
@ -184,23 +103,29 @@ void IrFoxDecoder::check_timeout(double t_us, uint32_t fs, const IrFoxOnTerminal
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,
|
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, IrFoxEmitBitMode emit_mode)
|
||||||
const IrFoxOnTerminal& on_terminal, IrFoxEmitBitMode emit_mode)
|
|
||||||
{
|
{
|
||||||
if (i_data_buffer >= irfox::kDataByteSizeMax * 8u)
|
if (i_data_buffer > irfox::kDataByteSizeMax * 8u)
|
||||||
{
|
{
|
||||||
|
if (!is_buffer_overflow && on_bit)
|
||||||
|
{
|
||||||
|
IrFoxEmitBit e{};
|
||||||
|
e.start_sample = static_cast<int64_t>(cell_start_s);
|
||||||
|
e.end_sample = static_cast<int64_t>(cell_end_s);
|
||||||
|
e.frame_type = IRF_FT_OVERFLOW;
|
||||||
|
e.mflags = DISPLAY_AS_ERROR_FLAG;
|
||||||
|
fill_err_snapshot(e);
|
||||||
|
std::strncpy(e.bubble_text, "OVF", sizeof e.bubble_text);
|
||||||
|
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
|
||||||
|
on_bit(e);
|
||||||
|
}
|
||||||
is_buffer_overflow = true;
|
is_buffer_overflow = true;
|
||||||
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::Overflow, on_terminal);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_buffer_overflow || is_preamb || is_wrong_pack)
|
if (is_buffer_overflow || is_preamb || is_wrong_pack)
|
||||||
{
|
{
|
||||||
// Firmware treats overflow/invalid frame state as a terminal abort and
|
// Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма.
|
||||||
// immediately permits a fresh preamble candidate.
|
first_rx();
|
||||||
const IrFoxAbortCause cause = is_buffer_overflow ? IrFoxAbortCause::Overflow :
|
|
||||||
(is_wrong_pack ? IrFoxAbortCause::BadSync : IrFoxAbortCause::None);
|
|
||||||
abort_frame(last_edge_time_us, cell_end_s, cause, on_terminal);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,19 +140,6 @@ 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++;
|
||||||
@ -244,16 +156,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -284,10 +186,14 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
|
|||||||
const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits);
|
const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits);
|
||||||
if (fatal_sync)
|
if (fatal_sync)
|
||||||
is_wrong_pack = true;
|
is_wrong_pack = true;
|
||||||
if (fatal_sync)
|
if (on_bit && fatal_sync)
|
||||||
{
|
{
|
||||||
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadSync, on_terminal);
|
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_ABORT,
|
||||||
return;
|
0, 0, DISPLAY_AS_ERROR_FLAG, false, 0, 0, 0};
|
||||||
|
fill_err_snapshot(e);
|
||||||
|
std::strncpy(e.bubble_text, "SYNC!", sizeof e.bubble_text);
|
||||||
|
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
|
||||||
|
on_bit(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,23 +216,12 @@ 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 a terminal abort so the capture explains why no packet follows.
|
|
||||||
if (pack_size < irfox::kMsgBytes + irfox::kCrcBytes)
|
|
||||||
{
|
|
||||||
is_wrong_pack = true;
|
|
||||||
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadLength, on_terminal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 >= irfox::kMsgBytes + irfox::kCrcBytes &&
|
if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte))
|
||||||
(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);
|
||||||
@ -337,8 +232,7 @@ 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>(packet_start_valid_ ? packet_start_sample_ : cell_start_s);
|
pkt.start_sample = static_cast<int64_t>(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);
|
||||||
@ -354,198 +248,37 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
|
|||||||
}
|
}
|
||||||
|
|
||||||
void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit,
|
void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit,
|
||||||
const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal)
|
const IrFoxOnPacket& on_pkt)
|
||||||
{
|
{
|
||||||
const double t_us = sample_to_us(sample, fs);
|
const double t_us = sample_to_us(sample, fs);
|
||||||
|
|
||||||
// Firmware advances terminal timers in this order while no queued edge is
|
|
||||||
// pending. For an offline capture, do the equivalent immediately before the
|
|
||||||
// next timestamped edge is consumed.
|
|
||||||
check_timeout(t_us, fs, on_terminal);
|
|
||||||
listen_start(t_us);
|
|
||||||
expire_preamble_candidate(t_us);
|
|
||||||
|
|
||||||
// A timeout/abort may have restored the nominal adaptive bit period.
|
|
||||||
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
|
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
|
||||||
uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U;
|
uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U;
|
||||||
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
|
|
||||||
|
|
||||||
/** Firmware starts a preamble candidate only on its first rising edge after silence. */
|
listen_start(t_us);
|
||||||
auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t {
|
|
||||||
(void)is_rising;
|
|
||||||
return edge_s;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mirror IR_DecoderRaw::preambleProcessEdge. A frame may start only after
|
// Как IR_DecoderRaw: пауза между фронтами по lastEdgeTime при активном приёме кадра.
|
||||||
// a long silence and two mutually consistent rise-to-rise periods.
|
if (last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0 && is_recive)
|
||||||
auto start_preamble_candidate = [&]() {
|
check_timeout(t_us);
|
||||||
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;
|
|
||||||
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();
|
|
||||||
// The first rising edge only opens Candidate; it must not also be
|
|
||||||
// compared with itself as a zero-length preamble period.
|
|
||||||
last_edge_time_us = t_us;
|
|
||||||
last_edge_sample = sample;
|
|
||||||
last_processed_edge_us = t_us;
|
|
||||||
have_last_processed = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 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)
|
|
||||||
{
|
|
||||||
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.
|
|
||||||
last_edge_time_us = t_us;
|
last_edge_time_us = t_us;
|
||||||
last_edge_sample = sample;
|
last_edge_sample = sample;
|
||||||
|
|
||||||
|
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
|
||||||
|
|
||||||
|
/** Визуализация: начало PRE с ближайшего спада в пределах ~3 битовых периодов (ИК-метка). */
|
||||||
|
auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t {
|
||||||
|
if (!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;
|
||||||
|
};
|
||||||
|
|
||||||
if (rising)
|
if (rising)
|
||||||
{
|
{
|
||||||
const double delta_rp = t_us - prev_rise_us;
|
const double delta_rp = t_us - prev_rise_us;
|
||||||
@ -624,6 +357,78 @@ 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;
|
||||||
@ -650,11 +455,9 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
|
|||||||
if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us))
|
if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us))
|
||||||
{
|
{
|
||||||
if (high_time_us > low_time_us)
|
if (high_time_us > low_time_us)
|
||||||
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
|
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
|
||||||
IrFoxEmitBitMode::WithBubble);
|
|
||||||
else
|
else
|
||||||
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
|
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
|
||||||
IrFoxEmitBitMode::WithBubble);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -744,15 +547,13 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
|
|||||||
if (i == low_count - 1 && invert_err)
|
if (i == low_count - 1 && invert_err)
|
||||||
{
|
{
|
||||||
invert_err = false;
|
invert_err = false;
|
||||||
write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
|
write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||||
IrFoxEmitBitMode::Quiet);
|
|
||||||
merge_warn = true;
|
merge_warn = true;
|
||||||
append_merge(row_is_data, true);
|
append_merge(row_is_data, true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
|
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||||
IrFoxEmitBitMode::Quiet);
|
|
||||||
append_merge(row_is_data, false);
|
append_merge(row_is_data, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -763,15 +564,13 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
|
|||||||
if (i == high_count - 1 && invert_err)
|
if (i == high_count - 1 && invert_err)
|
||||||
{
|
{
|
||||||
invert_err = false;
|
invert_err = false;
|
||||||
write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
|
write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||||
IrFoxEmitBitMode::Quiet);
|
|
||||||
merge_warn = true;
|
merge_warn = true;
|
||||||
append_merge(row_is_data, false);
|
append_merge(row_is_data, false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
|
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||||
IrFoxEmitBitMode::Quiet);
|
|
||||||
append_merge(row_is_data, true);
|
append_merge(row_is_data, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -784,13 +583,11 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
|
|||||||
have_last_processed = true;
|
have_last_processed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
|
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt)
|
||||||
const IrFoxOnTerminal& on_terminal)
|
|
||||||
{
|
{
|
||||||
const double t_us = sample_to_us(last_sample, fs);
|
const double t_us = sample_to_us(last_sample, fs);
|
||||||
check_timeout(t_us, fs, on_terminal);
|
|
||||||
listen_start(t_us);
|
listen_start(t_us);
|
||||||
expire_preamble_candidate(t_us);
|
check_timeout(t_us);
|
||||||
(void)on_bit;
|
(void)on_bit;
|
||||||
(void)on_pkt;
|
(void)on_pkt;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,17 +8,11 @@ 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_ACCEPTED = 3,
|
IRF_FT_PACKET_OK = 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_TIMEOUT = 12,
|
|
||||||
IRF_FT_PACKET_OK = IRF_FT_PACKET_ACCEPTED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
|
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
|
||||||
@ -47,8 +41,6 @@ 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;
|
||||||
@ -58,47 +50,16 @@ struct IrFoxEmitPacket
|
|||||||
uint8_t data_bytes[irfox::kDataByteSizeMax];
|
uint8_t data_bytes[irfox::kDataByteSizeMax];
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class IrFoxTerminalReason : uint8_t
|
|
||||||
{
|
|
||||||
Abort,
|
|
||||||
Timeout,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class IrFoxAbortCause : uint8_t
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
BadSync,
|
|
||||||
BadLength,
|
|
||||||
Overflow,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct IrFoxEmitTerminal
|
|
||||||
{
|
|
||||||
int64_t start_sample;
|
|
||||||
int64_t detail_start_sample;
|
|
||||||
int64_t end_sample;
|
|
||||||
IrFoxTerminalReason reason;
|
|
||||||
IrFoxAbortCause cause;
|
|
||||||
uint8_t message_type;
|
|
||||||
uint8_t declared_size;
|
|
||||||
uint16_t received_bits;
|
|
||||||
uint8_t err_low;
|
|
||||||
uint8_t err_high;
|
|
||||||
uint8_t err_other;
|
|
||||||
};
|
|
||||||
|
|
||||||
using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>;
|
using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>;
|
||||||
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
|
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
|
||||||
using IrFoxOnTerminal = std::function<void(const IrFoxEmitTerminal&)>;
|
|
||||||
|
|
||||||
class IrFoxDecoder
|
class IrFoxDecoder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void reset();
|
void reset();
|
||||||
void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit,
|
void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit,
|
||||||
const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal);
|
const IrFoxOnPacket& on_pkt);
|
||||||
void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
|
void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt);
|
||||||
const IrFoxOnTerminal& on_terminal);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static uint16_t ceil_div_u16(uint16_t val, uint16_t divider);
|
static uint16_t ceil_div_u16(uint16_t val, uint16_t divider);
|
||||||
@ -106,17 +67,10 @@ private:
|
|||||||
bool crc_check(uint8_t len, uint16_t& crc_out);
|
bool crc_check(uint8_t len, uint16_t& crc_out);
|
||||||
|
|
||||||
void first_rx();
|
void first_rx();
|
||||||
void preamble_reset_to_idle();
|
|
||||||
void release_preamble_guard(double t_us);
|
|
||||||
void emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
|
|
||||||
const IrFoxOnTerminal& on_terminal) const;
|
|
||||||
void abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
|
|
||||||
const IrFoxOnTerminal& on_terminal);
|
|
||||||
void expire_preamble_candidate(double t_us);
|
|
||||||
void listen_start(double t_us);
|
void listen_start(double t_us);
|
||||||
void check_timeout(double t_us, uint32_t sample_rate_hz, const IrFoxOnTerminal& on_terminal);
|
void check_timeout(double t_us);
|
||||||
void write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
|
void write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
|
||||||
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal,
|
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
|
||||||
IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble);
|
IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble);
|
||||||
|
|
||||||
double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); }
|
double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); }
|
||||||
@ -147,11 +101,6 @@ 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;
|
||||||
@ -174,18 +123,7 @@ private:
|
|||||||
int8_t all_count = 0;
|
int8_t all_count = 0;
|
||||||
|
|
||||||
uint16_t wrong_counter = 0;
|
uint16_t wrong_counter = 0;
|
||||||
enum class PreambleState : uint8_t
|
int8_t preamb_front_counter = 0;
|
||||||
{
|
|
||||||
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;
|
||||||
|
|||||||
@ -1,172 +0,0 @@
|
|||||||
#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,12 +11,8 @@ 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. */
|
||||||
* Must match IR_INPUT_MIN_PULSE_US in the firmware configuration. The current
|
constexpr uint32_t kMinFilteredPulseUs = 10U;
|
||||||
* 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;
|
||||||
@ -31,12 +27,6 @@ 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 = 1U;
|
|
||||||
|
|
||||||
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
|
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
|
||||||
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
|
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
|
||||||
@ -63,19 +53,6 @@ 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
|
||||||
|
|||||||
@ -1,282 +0,0 @@
|
|||||||
#include "IrFoxDecoder.h"
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#define CHECK(expression) \
|
|
||||||
do \
|
|
||||||
{ \
|
|
||||||
if (!(expression)) \
|
|
||||||
{ \
|
|
||||||
std::fprintf(stderr, "CHECK failed: %s (%s:%d)\n", #expression, __FILE__, __LINE__); \
|
|
||||||
std::exit(EXIT_FAILURE); \
|
|
||||||
} \
|
|
||||||
} while (false)
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
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;
|
|
||||||
std::vector<IrFoxEmitTerminal> terminals;
|
|
||||||
uint64_t phase = 0;
|
|
||||||
bool collect_bit_events = true;
|
|
||||||
static constexpr uint32_t kFs = 1000000U;
|
|
||||||
|
|
||||||
explicit DecoderHarness(bool collect_bits = true) : collect_bit_events(collect_bits)
|
|
||||||
{
|
|
||||||
decoder.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void edge(uint64_t sample, bool rising)
|
|
||||||
{
|
|
||||||
IrFoxOnBit on_bit;
|
|
||||||
if (collect_bit_events)
|
|
||||||
on_bit = [this](const IrFoxEmitBit& event) { events.push_back(event); };
|
|
||||||
decoder.processEdge(sample, rising, kFs, on_bit,
|
|
||||||
[this](const IrFoxEmitPacket& packet) { packets.push_back(packet); },
|
|
||||||
[this](const IrFoxEmitTerminal& terminal) { terminals.push_back(terminal); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void lockPreamble()
|
|
||||||
{
|
|
||||||
lockPreambleAt(40000U);
|
|
||||||
}
|
|
||||||
|
|
||||||
void lockPreambleAt(uint64_t first_rise)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void emitPacket(const std::vector<uint8_t>& packet)
|
|
||||||
{
|
|
||||||
for (size_t i = 0; i < packet.size(); ++i)
|
|
||||||
emitByte(packet[i], i + 1U != packet.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void flushAt(uint64_t sample)
|
|
||||||
{
|
|
||||||
IrFoxOnBit on_bit;
|
|
||||||
if (collect_bit_events)
|
|
||||||
on_bit = [this](const IrFoxEmitBit& event) { events.push_back(event); };
|
|
||||||
decoder.flushEnd(sample, kFs, on_bit,
|
|
||||||
[this](const IrFoxEmitPacket& packet) { packets.push_back(packet); },
|
|
||||||
[this](const IrFoxEmitTerminal& terminal) { terminals.push_back(terminal); });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<uint8_t> makeValidPacket()
|
|
||||||
{
|
|
||||||
std::vector<uint8_t> packet{0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
|
|
||||||
packet[5] = crc8(packet.data(), 5, irfox::kPoly1);
|
|
||||||
packet[6] = crc8(packet.data(), 6, irfox::kPoly2);
|
|
||||||
return packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
void assertBadLengthAbortsAndRecovers(uint8_t declared_size)
|
|
||||||
{
|
|
||||||
DecoderHarness harness;
|
|
||||||
harness.lockPreamble();
|
|
||||||
const uint8_t header = static_cast<uint8_t>(0xE0U | declared_size);
|
|
||||||
harness.emitByte(header, false);
|
|
||||||
|
|
||||||
CHECK(harness.packets.empty());
|
|
||||||
CHECK(harness.terminals.size() == 1U);
|
|
||||||
CHECK(harness.terminals[0].reason == IrFoxTerminalReason::Abort);
|
|
||||||
CHECK(harness.terminals[0].cause == IrFoxAbortCause::BadLength);
|
|
||||||
CHECK(harness.terminals[0].message_type == 7U);
|
|
||||||
CHECK(harness.terminals[0].declared_size == declared_size);
|
|
||||||
CHECK(harness.terminals[0].received_bits == irfox::kBitPerByte);
|
|
||||||
|
|
||||||
// Firmware abortFrame() releases the 30.288 ms preamble guard. A receiver
|
|
||||||
// that merely sets is_wrong_pack will miss this complete nearby frame.
|
|
||||||
const uint64_t next_preamble = harness.phase + 5000U;
|
|
||||||
harness.lockPreambleAt(next_preamble);
|
|
||||||
harness.emitPacket(makeValidPacket());
|
|
||||||
|
|
||||||
CHECK(harness.packets.size() == 1U);
|
|
||||||
CHECK(harness.packets[0].crc_ok);
|
|
||||||
CHECK(harness.packets[0].start_sample == static_cast<int64_t>(next_preamble));
|
|
||||||
CHECK(harness.terminals.size() == 1U);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
const std::vector<uint8_t> packet = makeValidPacket();
|
|
||||||
|
|
||||||
DecoderHarness valid;
|
|
||||||
valid.lockPreamble();
|
|
||||||
valid.emitPacket(packet);
|
|
||||||
CHECK(valid.packets.size() == 1U);
|
|
||||||
CHECK(valid.packets[0].crc_ok);
|
|
||||||
CHECK(valid.packets[0].pack_size == packet.size());
|
|
||||||
CHECK(valid.packets[0].start_sample == 40000);
|
|
||||||
CHECK(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;
|
|
||||||
CHECK(event.start_sample == 40000);
|
|
||||||
}
|
|
||||||
if (event.frame_type == IRF_FT_DATA_BYTE)
|
|
||||||
decoded_bytes.push_back(static_cast<uint8_t>(event.bit_value));
|
|
||||||
}
|
|
||||||
CHECK(saw_preamble);
|
|
||||||
CHECK(decoded_bytes.size() == packet.size());
|
|
||||||
for (size_t i = 0; i < packet.size(); ++i)
|
|
||||||
CHECK(decoded_bytes[i] == packet[i]);
|
|
||||||
|
|
||||||
for (uint8_t declared_size = 0; declared_size < irfox::kMsgBytes + irfox::kCrcBytes; ++declared_size)
|
|
||||||
assertBadLengthAbortsAndRecovers(declared_size);
|
|
||||||
|
|
||||||
// Overview supplies no per-bit callback. Terminal reporting must not depend
|
|
||||||
// on Detailed-mode bit/event generation.
|
|
||||||
DecoderHarness bad_sync(false);
|
|
||||||
bad_sync.lockPreamble();
|
|
||||||
bad_sync.emitByte(0xE7, false);
|
|
||||||
// Header 0xE7 ends in one, while the first sync bit must be inverted.
|
|
||||||
bad_sync.emitCell(true);
|
|
||||||
CHECK(bad_sync.terminals.empty());
|
|
||||||
bad_sync.emitCell(true);
|
|
||||||
CHECK(bad_sync.terminals.empty());
|
|
||||||
bad_sync.emitCell(true);
|
|
||||||
CHECK(bad_sync.events.empty());
|
|
||||||
CHECK(bad_sync.terminals.size() == 1U);
|
|
||||||
CHECK(bad_sync.terminals[0].reason == IrFoxTerminalReason::Abort);
|
|
||||||
CHECK(bad_sync.terminals[0].cause == IrFoxAbortCause::BadSync);
|
|
||||||
CHECK(bad_sync.terminals[0].message_type == 7U);
|
|
||||||
CHECK(bad_sync.terminals[0].declared_size == 7U);
|
|
||||||
CHECK(bad_sync.terminals[0].received_bits == irfox::kBitPerByte);
|
|
||||||
const uint64_t after_sync_abort = bad_sync.phase + 5000U;
|
|
||||||
bad_sync.lockPreambleAt(after_sync_abort);
|
|
||||||
bad_sync.emitPacket(packet);
|
|
||||||
CHECK(bad_sync.packets.size() == 1U);
|
|
||||||
CHECK(bad_sync.packets[0].crc_ok);
|
|
||||||
CHECK(bad_sync.packets[0].start_sample == static_cast<int64_t>(after_sync_abort));
|
|
||||||
CHECK(bad_sync.terminals.size() == 1U);
|
|
||||||
|
|
||||||
DecoderHarness stale_candidate;
|
|
||||||
constexpr uint64_t stale_rise = 40000U;
|
|
||||||
const uint64_t candidate_gap =
|
|
||||||
irfox::irTimeoutUs(irfox::kBitTimeUs) + 1U; // New 1x timeout, still below the old 3x timeout.
|
|
||||||
const uint64_t fresh_preamble = stale_rise + candidate_gap;
|
|
||||||
stale_candidate.edge(stale_rise, true);
|
|
||||||
stale_candidate.lockPreambleAt(fresh_preamble);
|
|
||||||
stale_candidate.emitPacket(packet);
|
|
||||||
CHECK(stale_candidate.packets.size() == 1U);
|
|
||||||
CHECK(stale_candidate.packets[0].crc_ok);
|
|
||||||
CHECK(stale_candidate.packets[0].start_sample == static_cast<int64_t>(fresh_preamble));
|
|
||||||
CHECK(stale_candidate.terminals.empty());
|
|
||||||
|
|
||||||
// If reception times out after PRE lock but before the first data bit, the
|
|
||||||
// Detailed terminal span must begin immediately after the separate PRE frame.
|
|
||||||
DecoderHarness pre_only;
|
|
||||||
pre_only.lockPreamble();
|
|
||||||
const IrFoxEmitBit* pre_event = nullptr;
|
|
||||||
for (const IrFoxEmitBit& event : pre_only.events)
|
|
||||||
{
|
|
||||||
if (event.frame_type == IRF_FT_PREAMBLE)
|
|
||||||
pre_event = &event;
|
|
||||||
}
|
|
||||||
CHECK(pre_event != nullptr);
|
|
||||||
constexpr uint64_t preamble_period = irfox::kBitTimeUs * 3U;
|
|
||||||
const uint64_t pre_lock_edge = 40000U + preamble_period * 2U;
|
|
||||||
const uint64_t abort_silence = 2U * irfox::irTimeoutUs(irfox::kBitTimeUs);
|
|
||||||
pre_only.flushAt(pre_lock_edge + abort_silence + 1U);
|
|
||||||
CHECK(pre_only.terminals.size() == 1U);
|
|
||||||
CHECK(pre_only.terminals[0].reason == IrFoxTerminalReason::Timeout);
|
|
||||||
CHECK(pre_only.terminals[0].received_bits == 0U);
|
|
||||||
CHECK(pre_only.terminals[0].detail_start_sample == pre_event->end_sample + 1);
|
|
||||||
CHECK(pre_only.terminals[0].end_sample == static_cast<int64_t>(pre_lock_edge + abort_silence));
|
|
||||||
|
|
||||||
DecoderHarness truncated;
|
|
||||||
truncated.lockPreamble();
|
|
||||||
truncated.emitByte(0xE7, true);
|
|
||||||
CHECK(truncated.packets.empty());
|
|
||||||
truncated.flushAt(truncated.phase + abort_silence);
|
|
||||||
CHECK(truncated.terminals.empty());
|
|
||||||
truncated.flushAt(truncated.phase + abort_silence + 1U);
|
|
||||||
CHECK(truncated.packets.empty());
|
|
||||||
CHECK(truncated.terminals.size() == 1U);
|
|
||||||
CHECK(truncated.terminals[0].reason == IrFoxTerminalReason::Timeout);
|
|
||||||
CHECK(truncated.terminals[0].cause == IrFoxAbortCause::None);
|
|
||||||
CHECK(truncated.terminals[0].message_type == 7U);
|
|
||||||
CHECK(truncated.terminals[0].declared_size == 7U);
|
|
||||||
CHECK(truncated.terminals[0].received_bits == irfox::kBitPerByte);
|
|
||||||
CHECK(truncated.terminals[0].end_sample == static_cast<int64_t>(truncated.phase + abort_silence));
|
|
||||||
truncated.flushAt(truncated.phase + abort_silence + 100U);
|
|
||||||
CHECK(truncated.terminals.size() == 1U);
|
|
||||||
const uint64_t after_timeout = truncated.phase + abort_silence + 5000U;
|
|
||||||
truncated.lockPreambleAt(after_timeout);
|
|
||||||
truncated.emitPacket(packet);
|
|
||||||
CHECK(truncated.packets.size() == 1U);
|
|
||||||
CHECK(truncated.packets[0].crc_ok);
|
|
||||||
CHECK(truncated.packets[0].start_sample == static_cast<int64_t>(after_timeout));
|
|
||||||
CHECK(truncated.terminals.size() == 1U);
|
|
||||||
|
|
||||||
// With no flush/tick between frames, the first rise strictly beyond 2T both
|
|
||||||
// closes the old frame and opens the new preamble. Inclusive spans must not
|
|
||||||
// share that sample.
|
|
||||||
DecoderHarness adjacent_timeout;
|
|
||||||
adjacent_timeout.lockPreamble();
|
|
||||||
adjacent_timeout.emitByte(0xE7, true);
|
|
||||||
const uint64_t adjacent_preamble = adjacent_timeout.phase + abort_silence + 1U;
|
|
||||||
adjacent_timeout.lockPreambleAt(adjacent_preamble);
|
|
||||||
adjacent_timeout.emitPacket(packet);
|
|
||||||
CHECK(adjacent_timeout.terminals.size() == 1U);
|
|
||||||
CHECK(adjacent_timeout.terminals[0].reason == IrFoxTerminalReason::Timeout);
|
|
||||||
CHECK(adjacent_timeout.packets.size() == 1U);
|
|
||||||
CHECK(adjacent_timeout.packets[0].crc_ok);
|
|
||||||
CHECK(adjacent_timeout.terminals[0].end_sample < adjacent_timeout.packets[0].start_sample);
|
|
||||||
CHECK(adjacent_timeout.packets[0].start_sample == static_cast<int64_t>(adjacent_preamble));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
#include "IR_DecoderRaw.h"
|
#include "IR_DecoderRaw.h"
|
||||||
#include "IR_Encoder.h"
|
#include "IR_Encoder.h"
|
||||||
|
#include "IrInterruptGuard.h"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@ -53,9 +54,8 @@ IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPa
|
|||||||
|
|
||||||
bool IR_DecoderRaw::isSubOverflow()
|
bool IR_DecoderRaw::isSubOverflow()
|
||||||
{
|
{
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
volatile bool ret = isSubBufferOverflow;
|
const bool ret = isSubBufferOverflow;
|
||||||
interrupts();
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ void IR_DecoderRaw::refreshPairMuteState()
|
|||||||
++active;
|
++active;
|
||||||
}
|
}
|
||||||
const uint32_t nowUs = micros();
|
const uint32_t nowUs = micros();
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
const bool wasActive = (isPairSending != 0);
|
const bool wasActive = (isPairSending != 0);
|
||||||
isPairSending = active;
|
isPairSending = active;
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
@ -121,7 +121,6 @@ void IR_DecoderRaw::refreshPairMuteState()
|
|||||||
rxBriefMuteBlockedEdges = 0;
|
rxBriefMuteBlockedEdges = 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
interrupts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
@ -142,6 +141,7 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
|
|||||||
case RxBriefReason::Timeout: return F("TIMEOUT");
|
case RxBriefReason::Timeout: return F("TIMEOUT");
|
||||||
case RxBriefReason::Crc: return F("CRC");
|
case RxBriefReason::Crc: return F("CRC");
|
||||||
case RxBriefReason::Ok: return F("OK");
|
case RxBriefReason::Ok: return F("OK");
|
||||||
|
case RxBriefReason::Count: return F("UNK");
|
||||||
default: return F("UNK");
|
default: return F("UNK");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
|
|||||||
void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
|
void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
|
||||||
{
|
{
|
||||||
const uint8_t ri = (uint8_t)reason;
|
const uint8_t ri = (uint8_t)reason;
|
||||||
if (ri < 14U)
|
if (ri < kRxBriefReasonCount)
|
||||||
rxReasonCnt[ri]++;
|
rxReasonCnt[ri]++;
|
||||||
#if !IR_RX_BRIEF_LOG
|
#if !IR_RX_BRIEF_LOG
|
||||||
(void)a; (void)b; (void)tUs;
|
(void)a; (void)b; (void)tUs;
|
||||||
@ -228,6 +228,8 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin
|
|||||||
Serial.print(b);
|
Serial.print(b);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case RxBriefReason::Count:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Serial.println();
|
Serial.println();
|
||||||
#endif // IR_RX_BRIEF_LOG (печать)
|
#endif // IR_RX_BRIEF_LOG (печать)
|
||||||
@ -235,11 +237,14 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin
|
|||||||
|
|
||||||
void IR_DecoderRaw::printRxReasonStats(Print &out) const
|
void IR_DecoderRaw::printRxReasonStats(Print &out) const
|
||||||
{
|
{
|
||||||
static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
|
static const char *const kTags[] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
|
||||||
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
|
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
|
||||||
"TIMEOUT", "CRC", "OK"};
|
"TIMEOUT", "CRC", "OK"};
|
||||||
|
static_assert(sizeof(kTags) / sizeof(kTags[0]) == kRxBriefReasonCount,
|
||||||
|
"RX reason tag table must match RxBriefReason::Count");
|
||||||
out.print(F("RXSTAT"));
|
out.print(F("RXSTAT"));
|
||||||
for (uint8_t i = 1; i < 14U; i++)
|
for (uint8_t i = static_cast<uint8_t>(RxBriefReason::MuteBegin);
|
||||||
|
i < kRxBriefReasonCount; ++i)
|
||||||
{
|
{
|
||||||
out.print(',');
|
out.print(',');
|
||||||
out.print(kTags[i]);
|
out.print(kTags[i]);
|
||||||
@ -273,22 +278,23 @@ void IR_DecoderRaw::rxBriefFlushDeferredIsrLogs()
|
|||||||
uint16_t muteEndCnt = 0;
|
uint16_t muteEndCnt = 0;
|
||||||
uint16_t rawCnt = 0;
|
uint16_t rawCnt = 0;
|
||||||
uint32_t rawLastUs = 0;
|
uint32_t rawLastUs = 0;
|
||||||
noInterrupts();
|
{
|
||||||
muteBeginPending = rxBriefMuteBeginPending;
|
IrInterruptGuard guard;
|
||||||
muteBeginUs = rxBriefMuteBeginUs;
|
muteBeginPending = rxBriefMuteBeginPending;
|
||||||
rxBriefMuteBeginPending = false;
|
muteBeginUs = rxBriefMuteBeginUs;
|
||||||
rxBriefMuteBeginUs = 0;
|
rxBriefMuteBeginPending = false;
|
||||||
muteEndPending = rxBriefMuteEndPending;
|
rxBriefMuteBeginUs = 0;
|
||||||
muteEndUs = rxBriefMuteEndUs;
|
muteEndPending = rxBriefMuteEndPending;
|
||||||
muteEndCnt = rxBriefMuteEndCount;
|
muteEndUs = rxBriefMuteEndUs;
|
||||||
rxBriefMuteEndPending = false;
|
muteEndCnt = rxBriefMuteEndCount;
|
||||||
rxBriefMuteEndUs = 0;
|
rxBriefMuteEndPending = false;
|
||||||
rxBriefMuteEndCount = 0;
|
rxBriefMuteEndUs = 0;
|
||||||
rawCnt = rxBriefRawOverflowDrops;
|
rxBriefMuteEndCount = 0;
|
||||||
rawLastUs = rxBriefRawOverflowLastUs;
|
rawCnt = rxBriefRawOverflowDrops;
|
||||||
rxBriefRawOverflowDrops = 0;
|
rawLastUs = rxBriefRawOverflowLastUs;
|
||||||
rxBriefRawOverflowLastUs = 0;
|
rxBriefRawOverflowDrops = 0;
|
||||||
interrupts();
|
rxBriefRawOverflowLastUs = 0;
|
||||||
|
}
|
||||||
if (muteBeginPending)
|
if (muteBeginPending)
|
||||||
rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs);
|
rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs);
|
||||||
if (muteEndPending)
|
if (muteEndPending)
|
||||||
@ -359,7 +365,7 @@ void IR_DecoderRaw::firstRX()
|
|||||||
#ifdef IRDEBUG
|
#ifdef IRDEBUG
|
||||||
wrCounter = 0;
|
wrCounter = 0;
|
||||||
#endif
|
#endif
|
||||||
memset(dataBuffer, 0x00, dataByteSizeMax);
|
memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes);
|
||||||
pulseFilterReset();
|
pulseFilterReset();
|
||||||
preambleResetToIdle();
|
preambleResetToIdle();
|
||||||
}
|
}
|
||||||
@ -368,9 +374,8 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
|
|||||||
{
|
{
|
||||||
if (pulseFilterHoldCount != 0U)
|
if (pulseFilterHoldCount != 0U)
|
||||||
return true;
|
return true;
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
const bool busy = !subBuffer.isEmpty();
|
const bool busy = !subBuffer.isEmpty();
|
||||||
interrupts();
|
|
||||||
return busy;
|
return busy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +383,7 @@ void IR_DecoderRaw::listenStart()
|
|||||||
{
|
{
|
||||||
if (rxTimeoutPipelineBusy())
|
if (rxTimeoutPipelineBusy())
|
||||||
return;
|
return;
|
||||||
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
|
if (isReciveRaw && ((micros() - lastEdgeTime) > receiveSilenceTimeoutUs()))
|
||||||
{
|
{
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
packTraceOnTimeoutOrAbort(true);
|
packTraceOnTimeoutOrAbort(true);
|
||||||
@ -396,14 +401,13 @@ inline void IR_DecoderRaw::checkTimeout()
|
|||||||
if (rxTimeoutPipelineBusy())
|
if (rxTimeoutPipelineBusy())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (micros() - lastEdgeTime > IR_timeout * 2U)
|
if (micros() - lastEdgeTime > receiveSilenceTimeoutUs())
|
||||||
{
|
{
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
packTraceOnTimeoutOrAbort(false);
|
packTraceOnTimeoutOrAbort(false);
|
||||||
#endif
|
#endif
|
||||||
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
|
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
|
||||||
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
|
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
|
||||||
noteRxEnd(RxEndReason::Timeout, micros());
|
|
||||||
isRecive = false; // приём завершён
|
isRecive = false; // приём завершён
|
||||||
msgTypeReceive = 0;
|
msgTypeReceive = 0;
|
||||||
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
|
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
|
||||||
@ -418,62 +422,6 @@ inline void IR_DecoderRaw::checkTimeout()
|
|||||||
}
|
}
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
void IR_DecoderRaw::noteRxEnd(RxEndReason reason, uint32_t tUs)
|
|
||||||
{
|
|
||||||
rxEnd.seq++;
|
|
||||||
rxEnd.reason = reason;
|
|
||||||
rxEnd.msgType = (i_dataBuffer >= 8U * msgBytes) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF;
|
|
||||||
rxEnd.packSize = (uint8_t)packSize;
|
|
||||||
rxEnd.tUs = tUs;
|
|
||||||
rxEnd.expectedEndUs = (packSize >= msgBytes + crcBytes)
|
|
||||||
? rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize) + irTicksToUs((uint32_t)syncBits * irBitTicks)
|
|
||||||
: 0U;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IR_DecoderRaw::abortFrame(uint32_t tUs)
|
|
||||||
{
|
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
|
||||||
packTraceOnTimeoutOrAbort(false);
|
|
||||||
#endif
|
|
||||||
noteRxEnd(RxEndReason::Abort, tUs);
|
|
||||||
isRecive = false;
|
|
||||||
isReciveRaw = false;
|
|
||||||
msgTypeReceive = 0;
|
|
||||||
firstRX();
|
|
||||||
releasePreambleGuard(tUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// После обрыва кадра «длинная тишина» (IR_timeout × 2 ≈ 30 мс) перед новым кандидатом преамбулы не требуется.
|
|
||||||
// prevRise — последний ДЕКОДИРОВАННЫЙ фронт; после abort он свежий, а фронты, отброшенные гвардом, его не двигают,
|
|
||||||
// поэтому валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика
|
|
||||||
// (стенд 09.09: КУ теряла пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга).
|
|
||||||
// Ложных захватов это не добавляет: хвост оборванного кадра (период фронтов 962 мкс, синхробиты ~1100) не проходит
|
|
||||||
// грубый фильтр периода преамбулы (2116…3270 мкс), а настоящая преамбула перезапускает кандидата по паузе > IR_timeout.
|
|
||||||
// После чистого конца кадра гвард остаётся: там он отсекает хвост синхробитов.
|
|
||||||
void IR_DecoderRaw::releasePreambleGuard(uint32_t tUs)
|
|
||||||
{
|
|
||||||
prevRise = tUs - IR_timeout * 2U - 1U; // «тишина уже была»: (front.time - prevRise) > longSilence для следующего фронта
|
|
||||||
}
|
|
||||||
|
|
||||||
void IR_DecoderRaw::expirePreambleCandidate()
|
|
||||||
{
|
|
||||||
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
|
|
||||||
return;
|
|
||||||
if ((micros() - preambleCandidateLastEdgeTime) > IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT)
|
|
||||||
{
|
|
||||||
if (preambleGoodPeriods)
|
|
||||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, micros());
|
|
||||||
preambleResetToIdle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t IR_DecoderRaw::rxExpectedEndUs() const
|
|
||||||
{
|
|
||||||
if (!isRecive || preambleState != PreambleState::Locked || isWrongPack || packSize < msgBytes + crcBytes)
|
|
||||||
return 0;
|
|
||||||
return rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IR_DecoderRaw::tick()
|
void IR_DecoderRaw::tick()
|
||||||
{
|
{
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
@ -528,16 +476,15 @@ void IR_DecoderRaw::tick()
|
|||||||
if (!processedFront)
|
if (!processedFront)
|
||||||
{
|
{
|
||||||
isSubBufferOverflow = false;
|
isSubBufferOverflow = false;
|
||||||
checkTimeout();
|
|
||||||
listenStart();
|
listenStart();
|
||||||
expirePreambleCandidate();
|
checkTimeout();
|
||||||
#if defined(IR_EDGE_TRACE)
|
#if defined(IR_EDGE_TRACE)
|
||||||
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
||||||
#endif
|
#endif
|
||||||
return;
|
return;
|
||||||
} // Если данных нет - ничего не делаем
|
} // Если данных нет - ничего не делаем
|
||||||
checkTimeout();
|
|
||||||
listenStart();
|
listenStart();
|
||||||
|
checkTimeout();
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
rxBriefFlushDeferredIsrLogs();
|
rxBriefFlushDeferredIsrLogs();
|
||||||
#endif
|
#endif
|
||||||
@ -825,8 +772,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
#if !defined(IRDEBUG_SERIAL_PACK)
|
#if !defined(IRDEBUG_SERIAL_PACK)
|
||||||
(void)packTraceInvertFix;
|
(void)packTraceInvertFix;
|
||||||
#endif
|
#endif
|
||||||
if (i_dataBuffer >= dataByteSizeMax * 8)
|
if (i_dataBuffer >= irproto::kMaxWireFrameBytes * 8U)
|
||||||
{ // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3)
|
{ // >=: не даёт записать бит за пределом 5-битной wire-длины.
|
||||||
isBufferOverflow = true;
|
isBufferOverflow = true;
|
||||||
rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros());
|
rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros());
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
@ -838,7 +785,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
{
|
{
|
||||||
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
|
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
|
||||||
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
|
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
|
||||||
abortFrame(micros());
|
isRecive = false;
|
||||||
|
isReciveRaw = false;
|
||||||
|
msgTypeReceive = 0;
|
||||||
|
firstRX();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -906,8 +856,6 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
|
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
|
||||||
#endif
|
#endif
|
||||||
abortFrame(micros()); // битый кадр не удерживает приёмник до таймаута
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -933,14 +881,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
{ // Ппервый байт
|
{ // Ппервый байт
|
||||||
packSize = dataBuffer[0] & IR_MASK_MSG_INFO;
|
packSize = dataBuffer[0] & IR_MASK_MSG_INFO;
|
||||||
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
|
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
|
||||||
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
|
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение wire-буфера.
|
||||||
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
|
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
|
||||||
if (packSize < msgBytes + crcBytes) // 0..2: кадр физически не несёт CRC — шум/битьё
|
if (packSize != 0 && packSize < msgBytes + crcBytes)
|
||||||
{
|
|
||||||
isWrongPack = true;
|
isWrongPack = true;
|
||||||
abortFrame(micros());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
|
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
|
||||||
@ -964,7 +908,6 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
preambleResetToIdle();
|
preambleResetToIdle();
|
||||||
msgTypeReceive = 0;
|
msgTypeReceive = 0;
|
||||||
isAvailable = crcCheck(packSize - crcBytes, crcValue);
|
isAvailable = crcCheck(packSize - crcBytes, crcValue);
|
||||||
noteRxEnd(isAvailable ? RxEndReason::Ok : RxEndReason::Crc, micros());
|
|
||||||
|
|
||||||
#ifdef BRUTEFORCE_CHECK
|
#ifdef BRUTEFORCE_CHECK
|
||||||
{
|
{
|
||||||
@ -972,7 +915,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
uint8_t packTraceBfBit = 0;
|
uint8_t packTraceBfBit = 0;
|
||||||
bool packTraceBfMark = false;
|
bool packTraceBfMark = false;
|
||||||
if (!isAvailable) // Исправление первого бита // Очень большая затычка...
|
if (!isAvailable) // Исправление первого бита // Очень большая затычка...
|
||||||
for (size_t i = 0; i < min(uint16_t(packSize - crcBytes * 2U), uint16_t(dataByteSizeMax)); ++i)
|
for (size_t i = 0; i < min(uint16_t(packSize - crcBytes * 2U), uint16_t(irproto::kMaxWireFrameBytes)); ++i)
|
||||||
{
|
{
|
||||||
for (int j = 0; j < 8; ++j)
|
for (int j = 0; j < 8; ++j)
|
||||||
{
|
{
|
||||||
@ -980,7 +923,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
dataBuffer[i] ^= 1 << j;
|
dataBuffer[i] ^= 1 << j;
|
||||||
|
|
||||||
isAvailable =
|
isAvailable =
|
||||||
crcCheck(min(uint16_t(packSize - crcBytes), uint16_t(dataByteSizeMax - 1U)), crcValue);
|
crcCheck(min(uint16_t(packSize - crcBytes), uint16_t(irproto::kMaxWireFrameBytes - 1U)), crcValue);
|
||||||
// обратно инвертируем бит в исходное состояние
|
// обратно инвертируем бит в исходное состояние
|
||||||
|
|
||||||
if (isAvailable)
|
if (isAvailable)
|
||||||
@ -1015,7 +958,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
|||||||
rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros());
|
rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros());
|
||||||
else
|
else
|
||||||
rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros());
|
rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros());
|
||||||
if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) {
|
if (!isAvailable && packSize > 0 && packSize <= irproto::kMaxWireFrameBytes) {
|
||||||
memcpy(rejectBuffer, dataBuffer, packSize);
|
memcpy(rejectBuffer, dataBuffer, packSize);
|
||||||
rejectPackSize = static_cast<uint8_t>(packSize);
|
rejectPackSize = static_cast<uint8_t>(packSize);
|
||||||
isRejectAvailable = true;
|
isRejectAvailable = true;
|
||||||
@ -1069,38 +1012,34 @@ uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider)
|
|||||||
void IR_DecoderRaw::edgeTracePush(uint32_t t_us, uint8_t level, uint8_t flags)
|
void IR_DecoderRaw::edgeTracePush(uint32_t t_us, uint8_t level, uint8_t flags)
|
||||||
{
|
{
|
||||||
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
const uint16_t w = edgeTrace_w;
|
const uint16_t w = edgeTrace_w;
|
||||||
const uint16_t r = edgeTrace_r;
|
const uint16_t r = edgeTrace_r;
|
||||||
const uint16_t next = static_cast<uint16_t>((w + 1u) % cap);
|
const uint16_t next = static_cast<uint16_t>((w + 1u) % cap);
|
||||||
if (next == r)
|
if (next == r)
|
||||||
{
|
{
|
||||||
edgeTrace_overflow = true;
|
edgeTrace_overflow = true;
|
||||||
interrupts();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
edgeTrace_buf[w].t_us = t_us;
|
edgeTrace_buf[w].t_us = t_us;
|
||||||
edgeTrace_buf[w].level = level ? 1u : 0u;
|
edgeTrace_buf[w].level = level ? 1u : 0u;
|
||||||
edgeTrace_buf[w].flags = flags;
|
edgeTrace_buf[w].flags = flags;
|
||||||
edgeTrace_w = next;
|
edgeTrace_w = next;
|
||||||
interrupts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void IR_DecoderRaw::edgeTraceClear()
|
void IR_DecoderRaw::edgeTraceClear()
|
||||||
{
|
{
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
edgeTrace_w = 0;
|
edgeTrace_w = 0;
|
||||||
edgeTrace_r = 0;
|
edgeTrace_r = 0;
|
||||||
edgeTrace_overflow = false;
|
edgeTrace_overflow = false;
|
||||||
interrupts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t IR_DecoderRaw::edgeTracePendingCount() const
|
uint16_t IR_DecoderRaw::edgeTracePendingCount() const
|
||||||
{
|
{
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
const uint16_t w = edgeTrace_w;
|
const uint16_t w = edgeTrace_w;
|
||||||
const uint16_t r = edgeTrace_r;
|
const uint16_t r = edgeTrace_r;
|
||||||
interrupts();
|
|
||||||
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
||||||
if (w >= r)
|
if (w >= r)
|
||||||
return static_cast<uint16_t>(w - r);
|
return static_cast<uint16_t>(w - r);
|
||||||
@ -1116,27 +1055,28 @@ uint16_t IR_DecoderRaw::edgeTraceFlushChunk(Print &out, uint16_t maxRec)
|
|||||||
maxRec = kStackCap;
|
maxRec = kStackCap;
|
||||||
|
|
||||||
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
const uint16_t cap = static_cast<uint16_t>(IR_EDGE_TRACE_CAPACITY);
|
||||||
noInterrupts();
|
|
||||||
const uint16_t w = edgeTrace_w;
|
|
||||||
const uint16_t r = edgeTrace_r;
|
|
||||||
uint16_t avail = (w >= r) ? static_cast<uint16_t>(w - r) : static_cast<uint16_t>(cap - r + w);
|
|
||||||
uint16_t toCopy = (avail > maxRec) ? maxRec : avail;
|
|
||||||
const bool truncated = (avail > toCopy);
|
|
||||||
if (toCopy == 0)
|
|
||||||
{
|
|
||||||
interrupts();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t tmp[kStackCap * 6];
|
uint8_t tmp[kStackCap * 6];
|
||||||
for (uint16_t i = 0; i < toCopy; ++i)
|
uint16_t toCopy = 0U;
|
||||||
|
bool truncated = false;
|
||||||
|
bool ovf = false;
|
||||||
{
|
{
|
||||||
const uint16_t idx = static_cast<uint16_t>((r + i) % cap);
|
IrInterruptGuard guard;
|
||||||
memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u);
|
const uint16_t w = edgeTrace_w;
|
||||||
|
const uint16_t r = edgeTrace_r;
|
||||||
|
const uint16_t avail = (w >= r) ? static_cast<uint16_t>(w - r)
|
||||||
|
: static_cast<uint16_t>(cap - r + w);
|
||||||
|
toCopy = (avail > maxRec) ? maxRec : avail;
|
||||||
|
truncated = (avail > toCopy);
|
||||||
|
if (toCopy == 0U)
|
||||||
|
return 0U;
|
||||||
|
for (uint16_t i = 0; i < toCopy; ++i)
|
||||||
|
{
|
||||||
|
const uint16_t idx = static_cast<uint16_t>((r + i) % cap);
|
||||||
|
memcpy(tmp + i * 6u, &edgeTrace_buf[idx], 6u);
|
||||||
|
}
|
||||||
|
edgeTrace_r = static_cast<uint16_t>((r + toCopy) % cap);
|
||||||
|
ovf = edgeTrace_overflow;
|
||||||
}
|
}
|
||||||
edgeTrace_r = static_cast<uint16_t>((r + toCopy) % cap);
|
|
||||||
const bool ovf = edgeTrace_overflow;
|
|
||||||
interrupts();
|
|
||||||
|
|
||||||
uint8_t meta = 0;
|
uint8_t meta = 0;
|
||||||
if (ovf)
|
if (ovf)
|
||||||
@ -1366,7 +1306,7 @@ void IR_DecoderRaw::packTraceForceEndSyncPhase()
|
|||||||
void IR_DecoderRaw::packTraceEmitHex(uint8_t byteCount) const
|
void IR_DecoderRaw::packTraceEmitHex(uint8_t byteCount) const
|
||||||
{
|
{
|
||||||
Serial.print(F("IR hex:"));
|
Serial.print(F("IR hex:"));
|
||||||
for (uint8_t i = 0; i < byteCount && i < dataByteSizeMax; i++)
|
for (uint8_t i = 0; i < byteCount && i < irproto::kMaxWireFrameBytes; i++)
|
||||||
{
|
{
|
||||||
Serial.print(' ');
|
Serial.print(' ');
|
||||||
ptPrintHexU8(dataBuffer[i]);
|
ptPrintHexU8(dataBuffer[i]);
|
||||||
@ -1464,8 +1404,8 @@ void IR_DecoderRaw::packTraceEmitErrorFlash(const __FlashStringHelper *msg)
|
|||||||
Serial.println(msg);
|
Serial.println(msg);
|
||||||
{
|
{
|
||||||
uint16_t nb = i_dataBuffer / 8u;
|
uint16_t nb = i_dataBuffer / 8u;
|
||||||
if (nb > dataByteSizeMax)
|
if (nb > irproto::kMaxWireFrameBytes)
|
||||||
nb = dataByteSizeMax;
|
nb = irproto::kMaxWireFrameBytes;
|
||||||
packTraceEmitHex(static_cast<uint8_t>(nb));
|
packTraceEmitHex(static_cast<uint8_t>(nb));
|
||||||
}
|
}
|
||||||
packTraceResetFrame();
|
packTraceResetFrame();
|
||||||
@ -1498,8 +1438,8 @@ void IR_DecoderRaw::packTraceOnTimeoutOrAbort(bool fromListenStart)
|
|||||||
return;
|
return;
|
||||||
const uint16_t expected = (i_dataBuffer >= 8) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0;
|
const uint16_t expected = (i_dataBuffer >= 8) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0;
|
||||||
uint16_t gotBytes = i_dataBuffer / 8;
|
uint16_t gotBytes = i_dataBuffer / 8;
|
||||||
if (gotBytes > dataByteSizeMax)
|
if (gotBytes > irproto::kMaxWireFrameBytes)
|
||||||
gotBytes = dataByteSizeMax;
|
gotBytes = irproto::kMaxWireFrameBytes;
|
||||||
Serial.println();
|
Serial.println();
|
||||||
packTraceEmitRawBitsLine(false);
|
packTraceEmitRawBitsLine(false);
|
||||||
Serial.print(F(" => ERROR: TIMEOUT, rx_data_size = "));
|
Serial.print(F(" => ERROR: TIMEOUT, rx_data_size = "));
|
||||||
@ -1671,7 +1611,7 @@ void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
|
|||||||
|
|
||||||
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||||
{
|
{
|
||||||
const uint32_t longSilence = IR_timeout * 2U;
|
const uint32_t longSilence = receiveSilenceTimeoutUs();
|
||||||
const uint32_t candTimeout = IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT;
|
const uint32_t candTimeout = IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT;
|
||||||
|
|
||||||
if (preambleState == PreambleState::Idle)
|
if (preambleState == PreambleState::Idle)
|
||||||
@ -1684,10 +1624,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
|||||||
if (!isReciveRaw && front.dir &&
|
if (!isReciveRaw && front.dir &&
|
||||||
((prevRise == 0U && front.time > longSilence) ||
|
((prevRise == 0U && front.time > longSilence) ||
|
||||||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
|
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
|
||||||
{
|
|
||||||
preambleStartCandidate(front);
|
preambleStartCandidate(front);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preambleState == PreambleState::Candidate)
|
if (preambleState == PreambleState::Candidate)
|
||||||
@ -1761,15 +1698,13 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
|||||||
err_syncBit = 0;
|
err_syncBit = 0;
|
||||||
isWrongPack = false;
|
isWrongPack = false;
|
||||||
msgTypeReceive = 0;
|
msgTypeReceive = 0;
|
||||||
memset(dataBuffer, 0x00, dataByteSizeMax);
|
memset(dataBuffer, 0x00, irproto::kMaxWireFrameBytes);
|
||||||
|
|
||||||
preambleState = PreambleState::Locked;
|
preambleState = PreambleState::Locked;
|
||||||
isPreamb = false;
|
isPreamb = false;
|
||||||
isRecive = true;
|
isRecive = true;
|
||||||
isReciveRaw = true;
|
isReciveRaw = true;
|
||||||
risePeriod = preambleMeanPeriod;
|
risePeriod = preambleMeanPeriod;
|
||||||
rxLockSeqCnt++;
|
|
||||||
rxLockTimeUsVal = front.time;
|
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
packTraceResetFrame();
|
packTraceResetFrame();
|
||||||
packTraceOpen = true;
|
packTraceOpen = true;
|
||||||
|
|||||||
@ -20,12 +20,13 @@ class Print;
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#define riseTime riseSyncTime //* bitTime */ 893U // TODO: Должно высчитываться медианой
|
#define riseTime riseSyncTime //* bitTime */ 893U // TODO: Должно высчитываться медианой
|
||||||
#define riseTolerance tolerance /* 250U */ // погрешность
|
#define riseTolerance IR_TIMING_TOLERANCE_US /* 250U */ // погрешность
|
||||||
#define riseTimeMax (riseTime + riseTolerance)
|
#define riseTimeMax (riseTime + riseTolerance)
|
||||||
#define riseTimeMin (riseTime - riseTolerance)
|
#define riseTimeMin (riseTime - riseTolerance)
|
||||||
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
#define aroundRise(t) (riseTimeMin < t && t < riseTimeMax)
|
||||||
#define IR_timeout (riseTimeMax * (8 + syncBits + 1)) // us // таймаут в 8 data + 3 sync + 1
|
// Compatibility aliases. The named contracts and their geometry live in IR_config.h.
|
||||||
constexpr uint16_t IR_ResponseDelay = ((uint16_t)(((bitTime+riseTolerance) * (8 + syncBits + 1))*2.7735))/1000;
|
#define IR_timeout (::irproto::rxInterEdgeTimeoutUs(riseTime))
|
||||||
|
constexpr uint16_t IR_ResponseDelay = irproto::kDefaultResponseTurnaroundDelayMs;
|
||||||
|
|
||||||
class IR_Encoder;
|
class IR_Encoder;
|
||||||
class IR_DecoderRaw : virtual public IR_FOX
|
class IR_DecoderRaw : virtual public IR_FOX
|
||||||
@ -51,41 +52,14 @@ public:
|
|||||||
|
|
||||||
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
||||||
bool isSubOverflow();
|
bool isSubOverflow();
|
||||||
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
|
inline bool isReciving() const { return isRecive; } // Возвращает true, если происходит приём пакета
|
||||||
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется
|
/** Current adaptive silence threshold that terminates an RX candidate. */
|
||||||
// ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного
|
inline uint32_t receiveSilenceTimeoutUs() const {
|
||||||
// шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего:
|
return irproto::rxSilenceTimeoutUs(riseTime);
|
||||||
// «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const.
|
|
||||||
inline bool rxLineActive() const {
|
|
||||||
return isRecive ||
|
|
||||||
(preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U);
|
|
||||||
}
|
}
|
||||||
// Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден;
|
inline uint32_t receiveSilenceTimeoutMsCeil() const {
|
||||||
// иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
|
return irproto::microsToMillisCeil(receiveSilenceTimeoutUs());
|
||||||
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
|
|
||||||
inline uint16_t rxDeclaredPackSize() const {
|
|
||||||
return (isRecive && packSize && !isWrongPack) ? packSize : 0;
|
|
||||||
}
|
}
|
||||||
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
|
|
||||||
static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; }
|
|
||||||
|
|
||||||
// ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ----
|
|
||||||
enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort };
|
|
||||||
struct RxEndInfo {
|
|
||||||
uint16_t seq = 0; // номер завершения (растёт на каждом терминале)
|
|
||||||
RxEndReason reason = RxEndReason::None;
|
|
||||||
uint8_t msgType = 0xFF; // 0xFF = первый байт не был принят
|
|
||||||
uint8_t packSize = 0; // объявленная длина (0 = неизвестна)
|
|
||||||
uint32_t tUs = 0; // micros() терминала
|
|
||||||
uint32_t expectedEndUs = 0; // расчётный конец кадра в эфире (0 = неизвестен)
|
|
||||||
};
|
|
||||||
uint16_t rxLockSeq() const { return rxLockSeqCnt; } // ++ в момент лока преамбулы
|
|
||||||
uint32_t rxLockTimeUs() const { return rxLockTimeUsVal; } // метка фронта лока (ISR-время)
|
|
||||||
/// Тип принимаемого кадра (3 бита) после первого байта; 0xFF пока неизвестен / приём не идёт.
|
|
||||||
uint8_t rxMsgType() const { return (isRecive && packSize) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; }
|
|
||||||
const RxEndInfo &rxLastEnd() const { return rxEnd; }
|
|
||||||
/// Расчётный момент последнего бита данных текущего кадра (по объявленной длине); 0 = не Locked / длина неизвестна.
|
|
||||||
uint32_t rxExpectedEndUs() const;
|
|
||||||
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
|
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
|
||||||
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
|
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
|
||||||
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
|
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
|
||||||
@ -102,8 +76,9 @@ public:
|
|||||||
/// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch,
|
/// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch,
|
||||||
/// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok).
|
/// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok).
|
||||||
/// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат).
|
/// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат).
|
||||||
|
static constexpr uint8_t rxReasonCounterCount() { return kRxBriefReasonCount; }
|
||||||
const uint16_t *rxReasonCounters() const { return rxReasonCnt; }
|
const uint16_t *rxReasonCounters() const { return rxReasonCnt; }
|
||||||
void rxReasonCountersClear() { for (uint8_t i = 0; i < 14; i++) rxReasonCnt[i] = 0; }
|
void rxReasonCountersClear() { for (uint8_t i = 0; i < kRxBriefReasonCount; ++i) rxReasonCnt[i] = 0; }
|
||||||
/// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.."
|
/// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.."
|
||||||
void printRxReasonStats(Print &out) const;
|
void printRxReasonStats(Print &out) const;
|
||||||
|
|
||||||
@ -128,12 +103,14 @@ private:
|
|||||||
BufferOverflow = 10,
|
BufferOverflow = 10,
|
||||||
Timeout = 11,
|
Timeout = 11,
|
||||||
Crc = 12,
|
Crc = 12,
|
||||||
Ok = 13
|
Ok = 13,
|
||||||
|
Count
|
||||||
};
|
};
|
||||||
|
static constexpr uint8_t kRxBriefReasonCount = static_cast<uint8_t>(RxBriefReason::Count);
|
||||||
|
|
||||||
bool isRejectAvailable = false;
|
bool isRejectAvailable = false;
|
||||||
uint8_t rejectPackSize = 0;
|
uint8_t rejectPackSize = 0;
|
||||||
uint8_t rejectBuffer[dataByteSizeMax]{};
|
uint8_t rejectBuffer[irproto::kMaxWireFrameBytes]{};
|
||||||
|
|
||||||
ErrorsStruct errors;
|
ErrorsStruct errors;
|
||||||
bool isAvailable = false;
|
bool isAvailable = false;
|
||||||
@ -181,13 +158,6 @@ private:
|
|||||||
Locked = 2
|
Locked = 2
|
||||||
};
|
};
|
||||||
PreambleState preambleState = PreambleState::Idle;
|
PreambleState preambleState = PreambleState::Idle;
|
||||||
uint16_t rxLockSeqCnt = 0;
|
|
||||||
uint32_t rxLockTimeUsVal = 0;
|
|
||||||
RxEndInfo rxEnd;
|
|
||||||
void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец
|
|
||||||
void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow)
|
|
||||||
void releasePreambleGuard(uint32_t tUs); // после abort: новый кандидат преамбулы без ожидания длинной тишины
|
|
||||||
void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle
|
|
||||||
uint8_t preambleGoodPeriods = 0;
|
uint8_t preambleGoodPeriods = 0;
|
||||||
uint16_t preambleMeanPeriod = 0;
|
uint16_t preambleMeanPeriod = 0;
|
||||||
uint32_t preambleCandidateLastEdgeTime = 0;
|
uint32_t preambleCandidateLastEdgeTime = 0;
|
||||||
@ -220,7 +190,7 @@ private:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
uint8_t dataBuffer[dataByteSizeMax]{0}; // Буффер данных
|
uint8_t dataBuffer[irproto::kMaxWireFrameBytes]{0}; // Буффер полного wire-кадра
|
||||||
volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов
|
volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов
|
||||||
|
|
||||||
volatile uint32_t risePeriod;
|
volatile uint32_t risePeriod;
|
||||||
@ -288,7 +258,7 @@ bool isReciveRaw = false;
|
|||||||
// (always-on наблюдаемость по контракту живучести), печать события —
|
// (always-on наблюдаемость по контракту живучести), печать события —
|
||||||
// только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны.
|
// только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны.
|
||||||
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
|
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
|
||||||
uint16_t rxReasonCnt[14] = {}; // индекс = (uint8_t)RxBriefReason, 1..13
|
uint16_t rxReasonCnt[kRxBriefReasonCount] = {}; // индекс = RxBriefReason, 1..Count-1
|
||||||
#if IR_RX_BRIEF_LOG
|
#if IR_RX_BRIEF_LOG
|
||||||
static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason);
|
static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason);
|
||||||
void rxBriefNoteMuteBlockedIsr(uint32_t tUs);
|
void rxBriefNoteMuteBlockedIsr(uint32_t tUs);
|
||||||
@ -304,7 +274,7 @@ bool isReciveRaw = false;
|
|||||||
|
|
||||||
#if defined(IRDEBUG_SERIAL_PACK)
|
#if defined(IRDEBUG_SERIAL_PACK)
|
||||||
static constexpr uint16_t kPackTraceBufCap =
|
static constexpr uint16_t kPackTraceBufCap =
|
||||||
uint16_t(dataByteSizeMax) * (uint16_t(bitPerByte) + uint16_t(syncBits)) + 48u;
|
uint16_t(irproto::kMaxWireFrameBytes) * (uint16_t(bitPerByte) + uint16_t(syncBits)) + 48u;
|
||||||
|
|
||||||
void packTraceResetFrame();
|
void packTraceResetFrame();
|
||||||
void packTracePushBit(bool bit);
|
void packTracePushBit(bool bit);
|
||||||
|
|||||||
779
IR_Encoder.cpp
779
IR_Encoder.cpp
File diff suppressed because it is too large
Load Diff
150
IR_Encoder.h
150
IR_Encoder.h
@ -18,20 +18,88 @@ enum class IR_SendStatus : uint8_t {
|
|||||||
DmaStartFailed,
|
DmaStartFailed,
|
||||||
EncoderPinUnavailable,
|
EncoderPinUnavailable,
|
||||||
BufferedStorageInvalid,
|
BufferedStorageInvalid,
|
||||||
|
InvalidArgument,
|
||||||
|
TimingOverflow,
|
||||||
|
PlanMismatch,
|
||||||
|
DmaTransferError,
|
||||||
|
DmaStalled,
|
||||||
};
|
};
|
||||||
|
|
||||||
const char* irSendStatusToString(IR_SendStatus status);
|
const char* irSendStatusToString(IR_SendStatus status);
|
||||||
|
|
||||||
|
enum class IR_TxState : uint8_t {
|
||||||
|
Idle = 0,
|
||||||
|
Preparing,
|
||||||
|
Transmitting,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class IR_TxClockBasis : uint8_t {
|
||||||
|
Nominal = 0,
|
||||||
|
ConfiguredTimer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic PHY plan produced by the same FSM that builds the actual
|
||||||
|
* carrier-gate stream. airtimeUs is rounded up, so it is safe as a deadline
|
||||||
|
* component; it does not include backend preparation or release latency.
|
||||||
|
*/
|
||||||
|
struct IR_TxPlan {
|
||||||
|
IR_SendStatus status = IR_SendStatus::InvalidArgument;
|
||||||
|
uint8_t wireBytes = 0;
|
||||||
|
uint16_t carrierMultiply = 0;
|
||||||
|
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal;
|
||||||
|
uint32_t tickClockHz = 0; // rational tick rate numerator
|
||||||
|
uint32_t tickDivider = 1; // rational tick rate denominator
|
||||||
|
uint32_t physicalTicks = 0;
|
||||||
|
uint32_t gateRunCount = 0;
|
||||||
|
uint32_t airtimeUs = 0;
|
||||||
|
|
||||||
|
bool valid() const { return status == IR_SendStatus::Success; }
|
||||||
|
uint32_t tickHzFloor() const { return tickDivider == 0U ? 0U : tickClockHz / tickDivider; }
|
||||||
|
uint32_t airtimeMsCeil() const { return (airtimeUs + 999U) / 1000U; }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Coherent main-context snapshot of one encoder's latest accepted operation. */
|
||||||
|
struct IR_TxSnapshot {
|
||||||
|
uint32_t operationId = 0;
|
||||||
|
IR_TxState state = IR_TxState::Idle;
|
||||||
|
IR_SendStatus status = IR_SendStatus::Success;
|
||||||
|
uint16_t carrierMultiply = 0;
|
||||||
|
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal;
|
||||||
|
uint32_t plannedPhysicalTicks = 0;
|
||||||
|
uint32_t plannedAirtimeUs = 0;
|
||||||
|
uint32_t acceptedAtUs = 0;
|
||||||
|
uint32_t armedAtUs = 0;
|
||||||
|
uint32_t terminalAtUs = 0;
|
||||||
|
|
||||||
|
bool active() const {
|
||||||
|
return state == IR_TxState::Preparing || state == IR_TxState::Transmitting;
|
||||||
|
}
|
||||||
|
bool terminal() const {
|
||||||
|
return state == IR_TxState::Completed || state == IR_TxState::Failed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Структура для возврата результата отправки
|
// Структура для возврата результата отправки
|
||||||
struct IR_SendResult {
|
struct IR_SendResult {
|
||||||
bool success; // Флаг успешности отправки
|
bool success; // true: backend принял и запустил эту операцию
|
||||||
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах
|
uint32_t sendTimeMs; // ceil(plannedAirtimeUs / 1000), compatibility field
|
||||||
IR_SendStatus status; // Детализированный статус старта передачи
|
IR_SendStatus status; // Детализированный статус старта передачи
|
||||||
|
uint32_t operationId; // 0, если новая операция не создавалась
|
||||||
|
uint32_t plannedAirtimeUs; // PHY airtime; без подготовки/release backend-а
|
||||||
|
IR_TxClockBasis clockBasis;
|
||||||
|
|
||||||
IR_SendResult(bool success = false,
|
IR_SendResult(bool success = false,
|
||||||
uint32_t sendTimeMs = 0,
|
uint32_t sendTimeMs = 0,
|
||||||
IR_SendStatus status = IR_SendStatus::ExternalStartFailed)
|
IR_SendStatus status = IR_SendStatus::ExternalStartFailed,
|
||||||
: success(success), sendTimeMs(sendTimeMs), status(status) {}
|
uint32_t operationId = 0,
|
||||||
|
uint32_t plannedAirtimeUs = 0,
|
||||||
|
IR_TxClockBasis clockBasis = IR_TxClockBasis::Nominal)
|
||||||
|
: success(success), sendTimeMs(sendTimeMs), status(status),
|
||||||
|
operationId(operationId), plannedAirtimeUs(plannedAirtimeUs),
|
||||||
|
clockBasis(clockBasis) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class IR_DecoderRaw;
|
class IR_DecoderRaw;
|
||||||
@ -53,6 +121,12 @@ public:
|
|||||||
|
|
||||||
using ExternalTxBusyFn = bool (*)(void *ctx);
|
using ExternalTxBusyFn = bool (*)(void *ctx);
|
||||||
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
||||||
|
using ExternalTxStartFnV2 = IR_SendStatus (*)(void *ctx,
|
||||||
|
IR_Encoder *enc,
|
||||||
|
const uint8_t *packet,
|
||||||
|
uint8_t len,
|
||||||
|
const IR_TxPlan& plan,
|
||||||
|
uint32_t operationId);
|
||||||
private:
|
private:
|
||||||
// uint16_t id; /// @brief Адрес передатчика
|
// uint16_t id; /// @brief Адрес передатчика
|
||||||
public:
|
public:
|
||||||
@ -62,10 +136,6 @@ public:
|
|||||||
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
||||||
/// (аналог setBlindDecoders() для одного RX).
|
/// (аналог setBlindDecoders() для одного RX).
|
||||||
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
|
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
|
||||||
/// Публичная оценка airtime кадра (мс) по его полной длине в байтах (packSize). Чистая функция
|
|
||||||
/// протокольных констант — подходит и для приёма (напр. бюджет удержания заднего по объявленному
|
|
||||||
/// в 1-м байте размеру принимаемого ответа). БЕЗ +30% компенсации занижения sync — добавляет потребитель.
|
|
||||||
uint32_t packAirtimeMs(uint8_t packSize) const { return calculateSendTime(packSize); }
|
|
||||||
static void isr();
|
static void isr();
|
||||||
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
|
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
|
||||||
/**
|
/**
|
||||||
@ -115,14 +185,32 @@ public:
|
|||||||
|
|
||||||
/** Optional: register external TX backend (e.g. DMA driver). */
|
/** Optional: register external TX backend (e.g. DMA driver). */
|
||||||
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
|
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||||||
|
/** Token-aware backend contract. Prefer this overload for every new backend. */
|
||||||
|
static void setExternalTxBackendV2(ExternalTxStartFnV2 startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||||||
|
|
||||||
/** Called by external TX backend on actual end of transmission. */
|
/** Legacy completion hook. It cannot reject a stale completion; retained for source compatibility. */
|
||||||
void externalFinishSend();
|
void externalFinishSend();
|
||||||
|
/** Complete exactly operationId; stale/duplicate completions are ignored. */
|
||||||
|
void externalFinishSend(uint32_t operationId, IR_SendStatus terminalStatus);
|
||||||
|
|
||||||
/** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */
|
/** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */
|
||||||
static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns);
|
static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns);
|
||||||
/** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */
|
/** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */
|
||||||
static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply);
|
static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply);
|
||||||
|
/** Preflight the exact physical stream without allocating or touching hardware. */
|
||||||
|
static IR_TxPlan planPhysicalTransmission(const uint8_t *packet, uint8_t len, uint16_t multiply);
|
||||||
|
/** Build into caller storage and report both required runs and exact timing. */
|
||||||
|
static IR_TxPlan buildPhysicalTransmission(const uint8_t *packet,
|
||||||
|
uint8_t len,
|
||||||
|
IR_TxGateRun *outRuns,
|
||||||
|
size_t maxRuns,
|
||||||
|
uint16_t multiply);
|
||||||
|
/** Replace nominal tick rate with an exact rational backend clock. */
|
||||||
|
static bool applyTickClock(IR_TxPlan& plan,
|
||||||
|
uint32_t clockNumeratorHz,
|
||||||
|
uint32_t clockDivider,
|
||||||
|
IR_TxClockBasis basis = IR_TxClockBasis::ConfiguredTimer);
|
||||||
|
IR_TxPlan planTransmission(const uint8_t *packet, uint8_t len) const;
|
||||||
|
|
||||||
void enable();
|
void enable();
|
||||||
void disable();
|
void disable();
|
||||||
@ -136,6 +224,7 @@ public:
|
|||||||
setBlindDecoders(decoders, static_cast<uint8_t>(N));
|
setBlindDecoders(decoders, static_cast<uint8_t>(N));
|
||||||
}
|
}
|
||||||
IR_SendStatus rawSend(uint8_t *ptr, uint8_t len);
|
IR_SendStatus rawSend(uint8_t *ptr, uint8_t len);
|
||||||
|
IR_SendResult rawSendTracked(uint8_t *ptr, uint8_t len);
|
||||||
|
|
||||||
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
||||||
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
|
||||||
@ -159,7 +248,11 @@ public:
|
|||||||
uint32_t testSendBack(uint8_t *data = nullptr, uint8_t len = 0) const;
|
uint32_t testSendBack(uint8_t *data = nullptr, uint8_t len = 0) const;
|
||||||
uint32_t testSendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0) const;
|
uint32_t testSendBackTo(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0) const;
|
||||||
|
|
||||||
inline bool isBusy() const { return isSending;}
|
inline bool isBusy() const { return isSending; }
|
||||||
|
/** Main-context coherent snapshot. Do not spin on this from an ISR. */
|
||||||
|
IR_TxSnapshot txSnapshot() const;
|
||||||
|
bool isOperationTerminal(uint32_t operationId) const;
|
||||||
|
bool isOperationComplete(uint32_t operationId) const;
|
||||||
|
|
||||||
|
|
||||||
~IR_Encoder();
|
~IR_Encoder();
|
||||||
@ -174,6 +267,7 @@ private:
|
|||||||
static void carrierPauseIfIdle();
|
static void carrierPauseIfIdle();
|
||||||
|
|
||||||
static ExternalTxStartFn externalTxStartFn;
|
static ExternalTxStartFn externalTxStartFn;
|
||||||
|
static ExternalTxStartFnV2 externalTxStartFnV2;
|
||||||
static ExternalTxBusyFn externalTxBusyFn;
|
static ExternalTxBusyFn externalTxBusyFn;
|
||||||
static void *externalTxCtx;
|
static void *externalTxCtx;
|
||||||
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
|
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
|
||||||
@ -214,8 +308,15 @@ private:
|
|||||||
static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
|
static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||||||
static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal);
|
static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||||||
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
|
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
|
||||||
template <typename Emit>
|
static TxFsmState initialTxFsm(uint8_t len);
|
||||||
static bool txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit);
|
static IR_TxPlan buildPhysicalPlan(const uint8_t *packet,
|
||||||
|
uint8_t len,
|
||||||
|
IR_TxGateRun *outRuns,
|
||||||
|
size_t maxRuns,
|
||||||
|
uint16_t multiply,
|
||||||
|
bool emitRuns);
|
||||||
|
static bool calculateAirtimeUs(IR_TxPlan& plan);
|
||||||
|
static void applyConfiguredTimerClock(IR_TxPlan& plan);
|
||||||
void loadTxFsmFromMembers(TxFsmState &st) const;
|
void loadTxFsmFromMembers(TxFsmState &st) const;
|
||||||
void storeTxFsmToMembers(const TxFsmState &st);
|
void storeTxFsmToMembers(const TxFsmState &st);
|
||||||
bool shouldUseBufferedIsr() const;
|
bool shouldUseBufferedIsr() const;
|
||||||
@ -224,9 +325,8 @@ private:
|
|||||||
uint16_t txPowerSnap_ = 1;
|
uint16_t txPowerSnap_ = 1;
|
||||||
uint16_t txMultiplySnap_ = 2;
|
uint16_t txMultiplySnap_ = 2;
|
||||||
|
|
||||||
/** Legacy: физических тиков на один логический шаг FSM = multiply/2. */
|
/** Fractional 2×Fc -> multiply×Fc phase accumulator (also exact for odd multiply). */
|
||||||
uint16_t legacyPhysPerLogical_ = 1;
|
uint32_t legacyScaleAccumulator_ = 0;
|
||||||
uint16_t legacyPhysCounter_ = 0;
|
|
||||||
uint16_t legacySlotInPeriod_ = 0;
|
uint16_t legacySlotInPeriod_ = 0;
|
||||||
|
|
||||||
volatile uint16_t powerNumerator_ = 1;
|
volatile uint16_t powerNumerator_ = 1;
|
||||||
@ -241,9 +341,27 @@ private:
|
|||||||
uint8_t decodersCount = 0;
|
uint8_t decodersCount = 0;
|
||||||
|
|
||||||
uint8_t sendLen = 0;
|
uint8_t sendLen = 0;
|
||||||
uint8_t sendBuffer[dataByteSizeMax]{0}; /// @brief Буффер данных для отправки
|
uint8_t sendBuffer[irproto::kMaxWireFrameBytes]{0}; /// @brief Буффер полного wire-кадра
|
||||||
|
|
||||||
volatile bool isSending = false;
|
volatile bool isSending = false;
|
||||||
|
|
||||||
|
// Single-writer-at-a-time record (main starts, ISR/backend terminates).
|
||||||
|
// The byte seqlock makes a coherent main-context snapshot without heap/locks.
|
||||||
|
volatile uint8_t txRecordVersion_ = 0;
|
||||||
|
volatile IR_TxState txState_ = IR_TxState::Idle;
|
||||||
|
volatile IR_SendStatus txTerminalStatus_ = IR_SendStatus::Success;
|
||||||
|
volatile uint32_t txOperationId_ = 0;
|
||||||
|
volatile uint32_t txPlannedPhysicalTicks_ = 0;
|
||||||
|
volatile uint32_t txPlannedAirtimeUs_ = 0;
|
||||||
|
volatile IR_TxClockBasis txClockBasis_ = IR_TxClockBasis::Nominal;
|
||||||
|
volatile uint32_t txAcceptedAtUs_ = 0;
|
||||||
|
volatile uint32_t txArmedAtUs_ = 0;
|
||||||
|
volatile uint32_t txTerminalAtUs_ = 0;
|
||||||
|
uint32_t txNextOperationId_ = 0;
|
||||||
|
|
||||||
|
uint32_t beginTxOperation(const IR_TxPlan& plan);
|
||||||
|
void markTxArmed(uint32_t operationId);
|
||||||
|
bool finishTxOperation(uint32_t operationId, IR_SendStatus terminalStatus);
|
||||||
volatile bool state = LOW; /// @brief Текущий уровень генерации
|
volatile bool state = LOW; /// @brief Текущий уровень генерации
|
||||||
|
|
||||||
volatile uint8_t dataByteCounter = 0;
|
volatile uint8_t dataByteCounter = 0;
|
||||||
|
|||||||
@ -30,4 +30,4 @@ uint8_t IR_FOX::crc8(uint8_t *data, uint8_t start, uint8_t end, uint8_t poly)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return crc;
|
return crc;
|
||||||
};
|
}
|
||||||
|
|||||||
172
IR_config.h
172
IR_config.h
@ -15,8 +15,6 @@ constexpr size_t kDefaultDmaTxMaxStreams = 4U;
|
|||||||
constexpr uint32_t kDmaTxIrqPriority = 8U;
|
constexpr uint32_t kDmaTxIrqPriority = 8U;
|
||||||
/** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */
|
/** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */
|
||||||
constexpr uint16_t kIsrTxBsrrWordCount = 256U;
|
constexpr uint16_t kIsrTxBsrrWordCount = 256U;
|
||||||
/** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */
|
|
||||||
constexpr size_t kIsrTxMaxGateRuns = 512U;
|
|
||||||
static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even");
|
static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +99,7 @@ msg type:
|
|||||||
// ----------
|
// ----------
|
||||||
// | xxx..... | = тип сообщения (биты 7..5)
|
// | xxx..... | = тип сообщения (биты 7..5)
|
||||||
// | ...xxxxx | = полная длина кадра в байтах (5 бит, 0..31, IR_MASK_MSG_INFO), не «31 бит» и не отдельный лимит «24 байта»
|
// | ...xxxxx | = полная длина кадра в байтах (5 бит, 0..31, IR_MASK_MSG_INFO), не «31 бит» и не отдельный лимит «24 байта»
|
||||||
// Полезная нагрузка в data pack: до bytePerPack байт (см. #define bytePerPack).
|
// Полезная нагрузка в data pack: до irproto::kMaxDataPayloadBytes байт.
|
||||||
// ---------- */
|
// ---------- */
|
||||||
#define IR_MSG_BACK 0U // | 000...... | = Задний сигнал машинки
|
#define IR_MSG_BACK 0U // | 000...... | = Задний сигнал машинки
|
||||||
#define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение
|
#define IR_MSG_ACCEPT 1U // | 001..... | = подтверждение
|
||||||
@ -111,7 +109,7 @@ msg type:
|
|||||||
// #define IR_MSG_ 5U // | 101..... | = ??
|
// #define IR_MSG_ 5U // | 101..... | = ??
|
||||||
#define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения
|
#define IR_MSG_DATA_NOACCEPT 6U // | 110..... | = данные, не требующие подтверждения
|
||||||
#define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения
|
#define IR_MSG_DATA_ACCEPT 7U // | 111..... | = данные требующие подтверждения
|
||||||
; /* // ----------
|
/* // ----------
|
||||||
|
|
||||||
/``````````````````````````````` подтверждение `````````````````````````````\ /``````````````````````````````````````` запрос ``````````````````````````````````\
|
/``````````````````````````````` подтверждение `````````````````````````````\ /``````````````````````````````````````` запрос ``````````````````````````````````\
|
||||||
|
|
||||||
@ -159,15 +157,25 @@ msg type:
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define IR_MASK_MSG_TYPE 0b00000111
|
namespace irproto {
|
||||||
#define IR_MASK_MSG_INFO 0b00011111
|
/** Three high header bits, shifted down, encode the message type. */
|
||||||
|
constexpr uint8_t kMessageTypeMask = 0x07U;
|
||||||
|
/** Five low header bits encode the complete on-wire frame length. */
|
||||||
|
constexpr uint8_t kWireFrameLengthBits = 5U;
|
||||||
|
constexpr uint8_t kWireFrameLengthMask =
|
||||||
|
static_cast<uint8_t>((1U << kWireFrameLengthBits) - 1U);
|
||||||
|
constexpr uint8_t kMaxWireFrameBytes = kWireFrameLengthMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source-compatible aliases. New code should use the typed irproto constants.
|
||||||
|
#define IR_MASK_MSG_TYPE (::irproto::kMessageTypeMask)
|
||||||
|
#define IR_MASK_MSG_INFO (::irproto::kWireFrameLengthMask)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
/////////////////////////////////////////////////////////////////////////////////////*/
|
/////////////////////////////////////////////////////////////////////////////////////*/
|
||||||
typedef uint16_t crc_t;
|
typedef uint16_t crc_t;
|
||||||
|
|
||||||
// #define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает
|
// #define BRUTEFORCE_CHECK // Перепроверяет пакет на 1 битные ошибки //TODO: зависает
|
||||||
#define bytePerPack (31) // колличество байтов в пакете
|
|
||||||
#ifndef freeFrec
|
#ifndef freeFrec
|
||||||
#define freeFrec false
|
#define freeFrec false
|
||||||
#endif
|
#endif
|
||||||
@ -231,11 +239,9 @@ typedef uint16_t crc_t;
|
|||||||
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
|
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
|
||||||
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
|
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
|
||||||
#endif
|
#endif
|
||||||
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. Кандидат без фронтов дольше таймаута байта
|
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. */
|
||||||
преамбулой быть не может; при 3× линия считалась занятой (rxLineActive) ещё 45 мс после последнего
|
|
||||||
паразитного фронта (напр. засветка своим дальномером) и откладывала передачу. */
|
|
||||||
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
|
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
|
||||||
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 1U
|
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define preambPulse 3
|
#define preambPulse 3
|
||||||
@ -250,8 +256,6 @@ typedef uint16_t crc_t;
|
|||||||
#define poly2 0x8C
|
#define poly2 0x8C
|
||||||
#define syncBits 3U // количество битов синхронизации
|
#define syncBits 3U // количество битов синхронизации
|
||||||
|
|
||||||
#define dataByteSizeMax (msgBytes + addrBytes + addrBytes + bytePerPack + crcBytes)
|
|
||||||
|
|
||||||
#define preambFronts (preambPulse * 2) // количество фронтов преамбулы (Приём)
|
#define preambFronts (preambPulse * 2) // количество фронтов преамбулы (Приём)
|
||||||
#define preambToggle ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) // колличество переключений преамбулы (Передача)
|
#define preambToggle ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1) // колличество переключений преамбулы (Передача)
|
||||||
|
|
||||||
@ -264,33 +268,123 @@ typedef uint16_t crc_t;
|
|||||||
|
|
||||||
#define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах
|
#define bitTakts (bitActiveTakts + bitPauseTakts) // Общая длительность бита в тактах
|
||||||
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
|
||||||
#define tolerance 300U
|
namespace irproto {
|
||||||
|
constexpr uint8_t kDataFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
||||||
|
constexpr uint8_t kBackFrameOverheadBytes = msgBytes + addrBytes + crcBytes;
|
||||||
|
constexpr uint8_t kBackToFrameOverheadBytes = msgBytes + addrBytes + addrBytes + crcBytes;
|
||||||
|
constexpr uint8_t kMaxDataPayloadBytes = kMaxWireFrameBytes - kDataFrameOverheadBytes;
|
||||||
|
constexpr uint8_t kMaxBackPayloadBytes = kMaxWireFrameBytes - kBackFrameOverheadBytes;
|
||||||
|
constexpr uint8_t kMaxBackToPayloadBytes = kMaxWireFrameBytes - kBackToFrameOverheadBytes;
|
||||||
|
|
||||||
// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ----
|
/** RX timing geometry shared by adaptive and nominal decoder paths. */
|
||||||
// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по
|
constexpr uint16_t kRxTimingToleranceUs = 300U;
|
||||||
// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта.
|
constexpr uint8_t kRxInterEdgeTimeoutGuardBitWindows = 1U;
|
||||||
constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U);
|
constexpr uint8_t kRxInterEdgeTimeoutBitWindows =
|
||||||
constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U);
|
static_cast<uint8_t>(bitPerByte + syncBits + kRxInterEdgeTimeoutGuardBitWindows);
|
||||||
constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U);
|
constexpr uint8_t kRxSilenceTimeoutInterEdgeWindows = 2U;
|
||||||
constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U;
|
|
||||||
constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks;
|
/**
|
||||||
constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); }
|
* Largest accepted rise-to-rise interval for one decoder byte window.
|
||||||
/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс.
|
* adaptiveBitPeriodUs is riseSyncTime when free-frequency tracking is used.
|
||||||
constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); }
|
*/
|
||||||
/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс.
|
constexpr uint32_t rxInterEdgeTimeoutUs(uint32_t adaptiveBitPeriodUs)
|
||||||
constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
|
{
|
||||||
/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс.
|
return (adaptiveBitPeriodUs + static_cast<uint32_t>(kRxTimingToleranceUs)) *
|
||||||
constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
|
static_cast<uint32_t>(kRxInterEdgeTimeoutBitWindows);
|
||||||
/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс.
|
}
|
||||||
constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks);
|
|
||||||
/// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём.
|
/** Silence after which an unfinished RX candidate is retired. */
|
||||||
constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U);
|
constexpr uint32_t rxSilenceTimeoutUs(uint32_t adaptiveBitPeriodUs)
|
||||||
constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs;
|
{
|
||||||
/// Протокольный максимум длины кадра (5-битное поле длины).
|
return rxInterEdgeTimeoutUs(adaptiveBitPeriodUs) *
|
||||||
constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO;
|
static_cast<uint32_t>(kRxSilenceTimeoutInterEdgeWindows);
|
||||||
/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from).
|
}
|
||||||
constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); }
|
|
||||||
constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); }
|
constexpr uint32_t microsToMillisCeil(uint32_t us)
|
||||||
|
{
|
||||||
|
return (us + 999U) / 1000U;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t kNominalRxInterEdgeTimeoutUs = rxInterEdgeTimeoutUs(bitTime);
|
||||||
|
constexpr uint32_t kNominalRxSilenceTimeoutUs = rxSilenceTimeoutUs(bitTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployed response/ACK turn-around policy.
|
||||||
|
*
|
||||||
|
* This is empirical, not a PHY invariant. Commit 1353ab6 replaced the older
|
||||||
|
* fixed 75 ms with a floating expression whose only reproducible result at the
|
||||||
|
* nominal PHY is 42 ms; no measurement or physical derivation was recorded.
|
||||||
|
* Keep the deployed value until a hardware gap campaign establishes a new
|
||||||
|
* channel-turn-around contract.
|
||||||
|
*/
|
||||||
|
constexpr uint16_t kDefaultResponseTurnaroundDelayMs = 42U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conservative logical run bound: preamble transitions plus two gate runs for
|
||||||
|
* every data/sync bit. Physical splitting for unusually large multiply values
|
||||||
|
* is reported by IR_TxPlan::gateRunCount and may require custom storage.
|
||||||
|
*/
|
||||||
|
constexpr size_t kMaxLogicalGateRuns =
|
||||||
|
static_cast<size_t>(preambPulse * 2U) +
|
||||||
|
static_cast<size_t>(kMaxWireFrameBytes) *
|
||||||
|
static_cast<size_t>((bitPerByte + syncBits) * 2U);
|
||||||
|
constexpr size_t kIsrTxMaxGateRuns = kMaxLogicalGateRuns;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile-time PHY storage contract.
|
||||||
|
*
|
||||||
|
* Every data and sync bit occupies bitTakts * 2 ticks on the logical
|
||||||
|
* 2*carrierFrec clock, independently of its value. A physical gate run is
|
||||||
|
* stored in uint16_t and can therefore split at UINT16_MAX ticks. The bound
|
||||||
|
* below includes the worst possible number of such split pieces; applications
|
||||||
|
* can size fixed DMA/ISR storage from the protocol instead of duplicating a
|
||||||
|
* packet-size constant.
|
||||||
|
*/
|
||||||
|
constexpr uint32_t kPreambleLogicalTicks =
|
||||||
|
static_cast<uint32_t>(preambPulse * 2U) *
|
||||||
|
static_cast<uint32_t>(preambToggle + 1U);
|
||||||
|
constexpr uint32_t kEncodedBitLogicalTicks =
|
||||||
|
static_cast<uint32_t>(bitTakts * 2U);
|
||||||
|
constexpr uint32_t kMaxLogicalTransmissionTicks =
|
||||||
|
kPreambleLogicalTicks +
|
||||||
|
static_cast<uint32_t>(kMaxWireFrameBytes) *
|
||||||
|
static_cast<uint32_t>(bitPerByte + syncBits) *
|
||||||
|
kEncodedBitLogicalTicks;
|
||||||
|
|
||||||
|
constexpr uint16_t normalizedCarrierMultiply(uint16_t multiply)
|
||||||
|
{
|
||||||
|
return multiply < 2U ? 2U : multiply;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint64_t maxPhysicalTransmissionTicks(uint16_t multiply)
|
||||||
|
{
|
||||||
|
return (static_cast<uint64_t>(kMaxLogicalTransmissionTicks) *
|
||||||
|
static_cast<uint64_t>(normalizedCarrierMultiply(multiply)) +
|
||||||
|
1U) /
|
||||||
|
2U;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t maxPhysicalGateRunCapacity(uint16_t multiply)
|
||||||
|
{
|
||||||
|
return kMaxLogicalGateRuns +
|
||||||
|
static_cast<size_t>(maxPhysicalTransmissionTicks(multiply) /
|
||||||
|
static_cast<uint64_t>(UINT16_MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
static_assert(kMaxDataPayloadBytes == 24U, "IR DATA payload contract changed");
|
||||||
|
static_assert(kMaxBackPayloadBytes == 26U, "IR BACK payload contract changed");
|
||||||
|
static_assert(kNominalRxInterEdgeTimeoutUs == 15144U, "IR RX timeout contract changed");
|
||||||
|
static_assert(kNominalRxSilenceTimeoutUs == 30288U, "IR RX silence contract changed");
|
||||||
|
static_assert(kMaxLogicalTransmissionTicks <= UINT32_MAX,
|
||||||
|
"IR maximum transmission no longer fits IR_TxPlan");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated source-compatible names. They are aliases only and no longer
|
||||||
|
// define independent storage/payload limits. bytePerPack historically meant
|
||||||
|
// 31; preserving that value avoids silently changing external sketches.
|
||||||
|
#define bytePerPack (::irproto::kMaxWireFrameBytes)
|
||||||
|
#define dataByteSizeMax (::irproto::kMaxWireFrameBytes)
|
||||||
|
#define IR_TIMING_TOLERANCE_US (::irproto::kRxTimingToleranceUs)
|
||||||
|
|
||||||
constexpr uint16_t test_all_Time = bitTime;
|
constexpr uint16_t test_all_Time = bitTime;
|
||||||
constexpr uint16_t test_all_Takts = bitTakts * 2;
|
constexpr uint16_t test_all_Takts = bitTakts * 2;
|
||||||
|
|||||||
@ -120,7 +120,23 @@ public:
|
|||||||
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
|
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
|
||||||
for (uint8_t i = 0; i < streamCount_; i++) {
|
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||||
if (streams_[i].enc == enc) {
|
if (streams_[i].enc == enc) {
|
||||||
return startStream(streams_[i], packet, len);
|
const IR_TxSnapshot snapshot = enc->txSnapshot();
|
||||||
|
const IR_TxPlan plan = enc->planTransmission(packet, len);
|
||||||
|
return startStream(streams_[i], packet, len, plan, snapshot.operationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return IR_SendStatus::ExternalNoStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
IR_SendStatus startTracked(IR_Encoder* enc,
|
||||||
|
const uint8_t* packet,
|
||||||
|
uint8_t len,
|
||||||
|
const IR_TxPlan& plan,
|
||||||
|
uint32_t operationId) {
|
||||||
|
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
|
||||||
|
for (uint8_t i = 0; i < streamCount_; i++) {
|
||||||
|
if (streams_[i].enc == enc) {
|
||||||
|
return startStream(streams_[i], packet, len, plan, operationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return IR_SendStatus::ExternalNoStream;
|
return IR_SendStatus::ExternalNoStream;
|
||||||
@ -192,6 +208,7 @@ private:
|
|||||||
|
|
||||||
uint32_t totalTicks = 0;
|
uint32_t totalTicks = 0;
|
||||||
volatile uint32_t ticksOutput = 0;
|
volatile uint32_t ticksOutput = 0;
|
||||||
|
uint32_t operationId = 0;
|
||||||
|
|
||||||
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
|
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
|
||||||
uint32_t lastTicks = 0;
|
uint32_t lastTicks = 0;
|
||||||
@ -204,20 +221,23 @@ private:
|
|||||||
ticksOutput = 0;
|
ticksOutput = 0;
|
||||||
totalTicks = 0;
|
totalTicks = 0;
|
||||||
runCount = 0;
|
runCount = 0;
|
||||||
|
operationId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) {
|
IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) {
|
||||||
wave.fill(dst, count);
|
wave.fill(dst, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onHalf() {
|
void advanceHalf() {
|
||||||
ticksOutput += halfLen;
|
ticksOutput += halfLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
void refillFirstHalf() {
|
||||||
fill(&dmaBuf[0], halfLen);
|
fill(&dmaBuf[0], halfLen);
|
||||||
__DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца
|
__DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца
|
||||||
}
|
}
|
||||||
|
|
||||||
void onComplete() {
|
void refillSecondHalf() {
|
||||||
ticksOutput += halfLen;
|
|
||||||
fill(&dmaBuf[halfLen], halfLen);
|
fill(&dmaBuf[halfLen], halfLen);
|
||||||
__DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца
|
__DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца
|
||||||
}
|
}
|
||||||
@ -262,7 +282,7 @@ private:
|
|||||||
void forceStop(TxStream& s) {
|
void forceStop(TxStream& s) {
|
||||||
HAL_NVIC_DisableIRQ(s.dmaIrq);
|
HAL_NVIC_DisableIRQ(s.dmaIrq);
|
||||||
if (s.active) {
|
if (s.active) {
|
||||||
stopStream(s);
|
stopStream(s, IR_SendStatus::DmaStalled);
|
||||||
recoveries_++;
|
recoveries_++;
|
||||||
}
|
}
|
||||||
HAL_NVIC_EnableIRQ(s.dmaIrq);
|
HAL_NVIC_EnableIRQ(s.dmaIrq);
|
||||||
@ -281,18 +301,22 @@ private:
|
|||||||
static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) {
|
static void dmaHalfCpltCb(DMA_HandleTypeDef* hdma) {
|
||||||
auto* s = streamFromDma(hdma);
|
auto* s = streamFromDma(hdma);
|
||||||
if (s == nullptr || !s->active) return;
|
if (s == nullptr || !s->active) return;
|
||||||
s->onHalf();
|
s->advanceHalf();
|
||||||
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
||||||
s_instance->stopStream(*s);
|
s_instance->stopStream(*s, IR_SendStatus::Success);
|
||||||
|
} else {
|
||||||
|
s->refillFirstHalf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void dmaCpltCb(DMA_HandleTypeDef* hdma) {
|
static void dmaCpltCb(DMA_HandleTypeDef* hdma) {
|
||||||
auto* s = streamFromDma(hdma);
|
auto* s = streamFromDma(hdma);
|
||||||
if (s == nullptr || !s->active) return;
|
if (s == nullptr || !s->active) return;
|
||||||
s->onComplete();
|
s->advanceHalf();
|
||||||
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
if (s_instance != nullptr && s->ticksOutput >= s->totalTicks) {
|
||||||
s_instance->stopStream(*s);
|
s_instance->stopStream(*s, IR_SendStatus::Success);
|
||||||
|
} else {
|
||||||
|
s->refillSecondHalf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,7 +326,7 @@ private:
|
|||||||
s->onError();
|
s->onError();
|
||||||
if (s_instance != nullptr) {
|
if (s_instance != nullptr) {
|
||||||
s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error
|
s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error
|
||||||
s_instance->stopStream(*s);
|
s_instance->stopStream(*s, IR_SendStatus::DmaTransferError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,21 +372,29 @@ private:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
|
IR_SendStatus startStream(TxStream& s,
|
||||||
|
const uint8_t* packet,
|
||||||
|
uint8_t len,
|
||||||
|
const IR_TxPlan& expectedPlan,
|
||||||
|
uint32_t operationId) {
|
||||||
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
|
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||||
if (s.active) return IR_SendStatus::EncoderBusy;
|
if (s.active) return IR_SendStatus::EncoderBusy;
|
||||||
|
if (!expectedPlan.valid() || operationId == 0U) return IR_SendStatus::ExternalInvalidConfig;
|
||||||
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
|
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||||
if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
|
if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
|
||||||
|
|
||||||
s.resetWave();
|
s.resetWave();
|
||||||
|
|
||||||
const uint16_t mult = IR_Encoder::carrierMultiply();
|
const uint16_t mult = expectedPlan.carrierMultiply;
|
||||||
s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult);
|
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||||
if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed;
|
packet, len, s.runs, s.maxRuns, mult);
|
||||||
|
if (!built.valid()) return built.status;
|
||||||
uint32_t total = 0;
|
if (built.physicalTicks != expectedPlan.physicalTicks ||
|
||||||
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks;
|
built.gateRunCount != expectedPlan.gateRunCount)
|
||||||
s.totalTicks = total;
|
return IR_SendStatus::PlanMismatch;
|
||||||
|
s.runCount = static_cast<size_t>(built.gateRunCount);
|
||||||
|
s.totalTicks = built.physicalTicks;
|
||||||
|
s.operationId = operationId;
|
||||||
|
|
||||||
uint16_t pwr = mult / 2U;
|
uint16_t pwr = mult / 2U;
|
||||||
if (s.enc != nullptr) {
|
if (s.enc != nullptr) {
|
||||||
@ -388,6 +420,7 @@ private:
|
|||||||
const uint32_t dst = u32ptr(&s.port->BSRR);
|
const uint32_t dst = u32ptr(&s.port->BSRR);
|
||||||
if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) {
|
if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) {
|
||||||
s.active = false;
|
s.active = false;
|
||||||
|
s.operationId = 0U;
|
||||||
return IR_SendStatus::DmaStartFailed;
|
return IR_SendStatus::DmaStartFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,10 +428,12 @@ private:
|
|||||||
return IR_SendStatus::Success;
|
return IR_SendStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopStream(TxStream& s) {
|
void stopStream(TxStream& s, IR_SendStatus terminalStatus) {
|
||||||
if (!s.active) return;
|
if (!s.active) return;
|
||||||
|
|
||||||
|
const uint32_t operationId = s.operationId;
|
||||||
s.active = false;
|
s.active = false;
|
||||||
|
s.operationId = 0U;
|
||||||
HAL_DMA_Abort_IT(&s.hdma);
|
HAL_DMA_Abort_IT(&s.hdma);
|
||||||
|
|
||||||
if (s.port != nullptr) {
|
if (s.port != nullptr) {
|
||||||
@ -406,7 +441,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (s.enc != nullptr) {
|
if (s.enc != nullptr) {
|
||||||
s.enc->externalFinishSend();
|
s.enc->externalFinishSend(operationId, terminalStatus);
|
||||||
}
|
}
|
||||||
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
|
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
|
||||||
}
|
}
|
||||||
|
|||||||
51
IrInterruptGuard.h
Normal file
51
IrInterruptGuard.h
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#if defined(__AVR__)
|
||||||
|
#include <avr/interrupt.h>
|
||||||
|
#include <avr/io.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nest-safe interrupt guard for the short ISR/main shared-state sections used
|
||||||
|
* by IR-protocol. Unlike a noInterrupts()/interrupts() pair it restores the
|
||||||
|
* previous state and therefore never enables interrupts from inside an ISR.
|
||||||
|
*/
|
||||||
|
class IrInterruptGuard final
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IrInterruptGuard()
|
||||||
|
{
|
||||||
|
#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32)
|
||||||
|
state_ = __get_PRIMASK();
|
||||||
|
__disable_irq();
|
||||||
|
#elif defined(__AVR__)
|
||||||
|
state_ = SREG;
|
||||||
|
cli();
|
||||||
|
#else
|
||||||
|
noInterrupts();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
~IrInterruptGuard()
|
||||||
|
{
|
||||||
|
#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32)
|
||||||
|
if ((state_ & 1U) == 0U)
|
||||||
|
__enable_irq();
|
||||||
|
#elif defined(__AVR__)
|
||||||
|
SREG = static_cast<uint8_t>(state_);
|
||||||
|
#else
|
||||||
|
interrupts();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
IrInterruptGuard(const IrInterruptGuard&) = delete;
|
||||||
|
IrInterruptGuard& operator=(const IrInterruptGuard&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if defined(__arm__) || defined(__thumb__) || defined(ARDUINO_ARCH_STM32) || \
|
||||||
|
defined(__AVR__)
|
||||||
|
uint32_t state_ = 0U;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
@ -65,63 +65,13 @@ public:
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Заполнение по ранам, а не по словам: тот же поток слов, что даёт nextWord() (состояние
|
|
||||||
// runIndex_/ticksLeftInRun_/slotInPeriod_ переносится через границы порций), но пауза (gate=0)
|
|
||||||
// пишется одним циклом записи, а несущая — копией готового шаблона периода. На 12 МГц это
|
|
||||||
// ~1 мс на 4096 слов вместо ~16 (пословный автомат) — и в предзаполнении перед стартом DMA,
|
|
||||||
// и в ISR-дозаполнении половин буфера во время передачи.
|
|
||||||
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
|
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
|
||||||
if (dst == nullptr || count == 0) {
|
if (dst == nullptr || count == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while (count != 0) {
|
do {
|
||||||
if (runIndex_ >= runCount) {
|
*dst++ = nextWord();
|
||||||
do { *dst++ = resetWord; } while (--count != 0);
|
} while (--count != 0);
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bool gate = runs[runIndex_].gate;
|
|
||||||
uint16_t n = ticksLeftInRun_; // слов до конца текущего рана
|
|
||||||
if (n == 0) n = 1; // ран нулевой длины: nextWord() выдаёт одно слово и переходит дальше
|
|
||||||
if (n > count) n = count;
|
|
||||||
if (!gate) {
|
|
||||||
slotInPeriod_ = 0;
|
|
||||||
uint16_t k = n;
|
|
||||||
do { *dst++ = resetWord; } while (--k != 0);
|
|
||||||
} else {
|
|
||||||
uint16_t k = n;
|
|
||||||
// добить текущий период до слота 0 (если ран начался посреди периода на границе порции)
|
|
||||||
while (k != 0 && slotInPeriod_ != 0) {
|
|
||||||
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
|
|
||||||
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
|
|
||||||
k--;
|
|
||||||
}
|
|
||||||
// целые периоды: powerN_ слов setWord, остальные resetWord
|
|
||||||
while (k >= multiply_) {
|
|
||||||
uint16_t i = 0;
|
|
||||||
for (; i < powerN_; ++i) *dst++ = setWord;
|
|
||||||
for (; i < multiply_; ++i) *dst++ = resetWord;
|
|
||||||
k = (uint16_t)(k - multiply_);
|
|
||||||
}
|
|
||||||
// хвост неполного периода
|
|
||||||
while (k != 0) {
|
|
||||||
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
|
|
||||||
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
|
|
||||||
k--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count = (uint16_t)(count - n);
|
|
||||||
if (ticksLeftInRun_ > n) {
|
|
||||||
ticksLeftInRun_ = (uint16_t)(ticksLeftInRun_ - n);
|
|
||||||
} else {
|
|
||||||
ticksLeftInRun_ = 0;
|
|
||||||
}
|
|
||||||
if (ticksLeftInRun_ == 0) {
|
|
||||||
runIndex_++;
|
|
||||||
if (runIndex_ < runCount) {
|
|
||||||
ticksLeftInRun_ = runs[runIndex_].lenTicks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
16
RingBuffer.h
16
RingBuffer.h
@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "Arduino.h"
|
#include "IrInterruptGuard.h"
|
||||||
template <typename T, unsigned int BufferSize>
|
template <typename T, unsigned int BufferSize>
|
||||||
class RingBuffer {
|
class RingBuffer {
|
||||||
public:
|
public:
|
||||||
@ -15,43 +15,39 @@ public:
|
|||||||
|
|
||||||
bool push(T element) {
|
bool push(T element) {
|
||||||
bool pushed = false;
|
bool pushed = false;
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
if (!isFull()) {
|
if (!isFull()) {
|
||||||
data[end] = element;
|
data[end] = element;
|
||||||
end = (end + 1) % BufferSize;
|
end = (end + 1) % BufferSize;
|
||||||
pushed = true;
|
pushed = true;
|
||||||
}
|
}
|
||||||
interrupts();
|
|
||||||
return pushed;
|
return pushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
T* pop() {
|
T* pop() {
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
T* value = nullptr;
|
T* value = nullptr;
|
||||||
if (!isEmpty()) {
|
if (!isEmpty()) {
|
||||||
value = &data[start];
|
value = &data[start];
|
||||||
start = (start + 1) % BufferSize;
|
start = (start + 1) % BufferSize;
|
||||||
}
|
}
|
||||||
interrupts();
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению.
|
// B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению.
|
||||||
// (T* pop() отдаёт указатель во внутренний слот; его внутренний interrupts() снимает внешнюю
|
// (T* pop() отдаёт указатель во внутренний слот; после выхода слот снова может быть перезаписан.)
|
||||||
// защиту вызывающего ДО чтения *ptr → торн-рид, если кольцо переполнится в этом окне.)
|
|
||||||
bool pop(T &out) {
|
bool pop(T &out) {
|
||||||
bool popped = false;
|
bool popped = false;
|
||||||
noInterrupts();
|
IrInterruptGuard guard;
|
||||||
if (!isEmpty()) {
|
if (!isEmpty()) {
|
||||||
out = data[start];
|
out = data[start];
|
||||||
start = (start + 1) % BufferSize;
|
start = (start + 1) % BufferSize;
|
||||||
popped = true;
|
popped = true;
|
||||||
}
|
}
|
||||||
interrupts();
|
|
||||||
return popped;
|
return popped;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
T data[BufferSize];
|
T data[BufferSize];
|
||||||
unsigned int start, end;
|
unsigned int start, end;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,7 +20,7 @@ static constexpr uint16_t kIrDeviceAddr = 0;
|
|||||||
static constexpr uint8_t kCmdVersion = 0x5E;
|
static constexpr uint8_t kCmdVersion = 0x5E;
|
||||||
static constexpr uint32_t kSerialBaud = 115200;
|
static constexpr uint32_t kSerialBaud = 115200;
|
||||||
static constexpr uint32_t kSendPeriodMs = 500;
|
static constexpr uint32_t kSendPeriodMs = 500;
|
||||||
static constexpr uint8_t kMaxPayload = bytePerPack;
|
static constexpr uint8_t kMaxPayload = irproto::kMaxDataPayloadBytes;
|
||||||
static constexpr uint8_t kMaxParamBytes = kMaxPayload - 1;
|
static constexpr uint8_t kMaxParamBytes = kMaxPayload - 1;
|
||||||
|
|
||||||
static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr);
|
static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr);
|
||||||
@ -30,15 +30,23 @@ static HardwareTimer irTimer(TIM17);
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr size_t kIrDmaStreams = 1;
|
constexpr size_t kIrDmaStreams = 1;
|
||||||
constexpr uint16_t kIrDmaTxWordCount = 4096U;
|
constexpr uint16_t kIrDmaTxWordCount = 4096U;
|
||||||
constexpr size_t kIrDmaTxMaxGateRuns = 1024U;
|
// This example accepts the full uint8_t carrier-multiply configuration range.
|
||||||
|
constexpr uint16_t kIrDmaMaxCarrierMultiply = UINT8_MAX;
|
||||||
|
constexpr size_t kIrDmaTxMaxGateRuns =
|
||||||
|
irproto::maxPhysicalGateRunCapacity(kIrDmaMaxCarrierMultiply);
|
||||||
static uint32_t s_irDmaWords[kIrDmaTxWordCount];
|
static uint32_t s_irDmaWords[kIrDmaTxWordCount];
|
||||||
static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns];
|
static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns];
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
static IrDmaTxStm32<kIrDmaStreams> dmaBackend;
|
static IrDmaTxStm32<kIrDmaStreams> dmaBackend;
|
||||||
static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); }
|
static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); }
|
||||||
static bool txStart(void * /*ctx*/, IR_Encoder *e, const uint8_t *packet, uint8_t len) {
|
static IR_SendStatus txStart(void * /*ctx*/,
|
||||||
return dmaBackend.start(e, packet, len);
|
IR_Encoder *e,
|
||||||
|
const uint8_t *packet,
|
||||||
|
uint8_t len,
|
||||||
|
const IR_TxPlan& plan,
|
||||||
|
uint32_t operationId) {
|
||||||
|
return dmaBackend.startTracked(e, packet, len, plan, operationId);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -51,7 +59,11 @@ static bool s_sendLongerFrame = false;
|
|||||||
// 24 байта total: msg(1)+addr(2)+addr(2)+data(17)+crc(2), где data=0x5E + 16 ASCII.
|
// 24 байта total: msg(1)+addr(2)+addr(2)+data(17)+crc(2), где data=0x5E + 16 ASCII.
|
||||||
static const char kPayload16[] = "Car_v4.3.9_[12MH";
|
static const char kPayload16[] = "Car_v4.3.9_[12MH";
|
||||||
// 25 байт total: как выше, но data=0x5E + 17 ASCII.
|
// 25 байт total: как выше, но data=0x5E + 17 ASCII.
|
||||||
static const char kPayload17[] = "Car_v4.3.9_[12MHz]_G491";
|
static const char kPayload17[] = "Car_v4.3.9_[12MHz";
|
||||||
|
static_assert(sizeof(kPayload16) - 1U == 16U, "24-byte frame fixture changed");
|
||||||
|
static_assert(sizeof(kPayload17) - 1U == 17U, "25-byte frame fixture changed");
|
||||||
|
static_assert(kMaxParamBytes == irproto::kMaxDataPayloadBytes - 1U,
|
||||||
|
"longData command parameter capacity must follow the DATA wire contract");
|
||||||
|
|
||||||
static void rebuildIrPayload() {
|
static void rebuildIrPayload() {
|
||||||
s_irPayload[0] = kCmdVersion;
|
s_irPayload[0] = kCmdVersion;
|
||||||
@ -110,7 +122,7 @@ void setup() {
|
|||||||
Serial.println(F("[IR_DMA] init FAILED"));
|
Serial.println(F("[IR_DMA] init FAILED"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr);
|
IR_Encoder::setExternalTxBackendV2(txStart, txBusy, nullptr);
|
||||||
#elif LONGDATA_LEGACY_ISR
|
#elif LONGDATA_LEGACY_ISR
|
||||||
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
|
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
|
||||||
#else
|
#else
|
||||||
|
|||||||
134
tests/arduino_stubs/Arduino.h
Normal file
134
tests/arduino_stubs/Arduino.h
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct GPIO_TypeDef
|
||||||
|
{
|
||||||
|
uint32_t BSRR = 0;
|
||||||
|
uint32_t IDR = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class __FlashStringHelper;
|
||||||
|
#define F(value) (reinterpret_cast<const __FlashStringHelper *>(value))
|
||||||
|
|
||||||
|
class Print
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
size_t print(const __FlashStringHelper *value)
|
||||||
|
{
|
||||||
|
return append(reinterpret_cast<const char *>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t print(const char *value) { return append(value); }
|
||||||
|
|
||||||
|
size_t print(char value)
|
||||||
|
{
|
||||||
|
buffer_.push_back(value);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
size_t print(T value)
|
||||||
|
{
|
||||||
|
return append(std::to_string(value).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t println()
|
||||||
|
{
|
||||||
|
buffer_.push_back('\n');
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t write(uint8_t value)
|
||||||
|
{
|
||||||
|
buffer_.push_back(static_cast<char>(value));
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string &str() const { return buffer_; }
|
||||||
|
void clear() { buffer_.clear(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t append(const char *value)
|
||||||
|
{
|
||||||
|
if (value == nullptr)
|
||||||
|
return 0U;
|
||||||
|
const size_t oldSize = buffer_.size();
|
||||||
|
buffer_ += value;
|
||||||
|
return buffer_.size() - oldSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string buffer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
using IRQn_Type = int;
|
||||||
|
enum TimerFormat_t : uint8_t { TICK_FORMAT = 0, MICROSEC_FORMAT, HERTZ_FORMAT };
|
||||||
|
|
||||||
|
constexpr uint8_t LOW = 0;
|
||||||
|
constexpr uint8_t HIGH = 1;
|
||||||
|
constexpr uint8_t INPUT = 0;
|
||||||
|
constexpr uint8_t OUTPUT = 1;
|
||||||
|
|
||||||
|
class HardwareTimer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void pause() {}
|
||||||
|
void resume() {}
|
||||||
|
void setOverflow(uint32_t value, TimerFormat_t format = TICK_FORMAT)
|
||||||
|
{
|
||||||
|
if (format == HERTZ_FORMAT && value != 0U)
|
||||||
|
{
|
||||||
|
prescale_ = 1U;
|
||||||
|
overflow_ = timerClockHz_ / value;
|
||||||
|
if (overflow_ == 0U) overflow_ = 1U;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
overflow_ = value == 0U ? 1U : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t getOverflow(TimerFormat_t = TICK_FORMAT) { return overflow_; }
|
||||||
|
uint32_t getPrescaleFactor() { return prescale_; }
|
||||||
|
uint32_t getTimerClkFreq() { return timerClockHz_; }
|
||||||
|
void attachInterrupt(uint8_t, void (*)()) {}
|
||||||
|
|
||||||
|
uint32_t timerClockHz_ = 12000000U;
|
||||||
|
uint32_t prescale_ = 1U;
|
||||||
|
uint32_t overflow_ = 1U;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline GPIO_TypeDef *digitalPinToPort(uint8_t)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline uint16_t digitalPinToBitMask(uint8_t)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void pinMode(uint8_t, uint8_t) {}
|
||||||
|
inline void digitalWrite(uint8_t, uint8_t) {}
|
||||||
|
inline void NVIC_SetPriority(IRQn_Type, uint8_t) {}
|
||||||
|
inline void noInterrupts() {}
|
||||||
|
inline void interrupts() {}
|
||||||
|
|
||||||
|
struct ArduinoSerialStub
|
||||||
|
{
|
||||||
|
template <typename T> void print(const T&) {}
|
||||||
|
template <typename T> void println(const T&) {}
|
||||||
|
void println() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ArduinoSerialStub Serial;
|
||||||
|
|
||||||
|
inline uint32_t arduino_stub_micros = 0U;
|
||||||
|
|
||||||
|
inline unsigned long millis()
|
||||||
|
{
|
||||||
|
return arduino_stub_micros / 1000U;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline unsigned long micros() { return arduino_stub_micros; }
|
||||||
148
tests/test_packet_types.cpp
Normal file
148
tests/test_packet_types.cpp
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
#include "PacketTypes.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template <typename Packet>
|
||||||
|
class ExposedPacket : public Packet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool attach(IR_FOX::PackInfo *info, uint16_t id = 0, bool requireTypedSize = true)
|
||||||
|
{
|
||||||
|
return this->set(info, id, requireTypedSize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
IR_FOX::PackInfo frame(uint8_t *buffer, uint8_t msgType, uint8_t size)
|
||||||
|
{
|
||||||
|
buffer[0] = uint8_t((msgType << 5) | (size & IR_MASK_MSG_INFO));
|
||||||
|
IR_FOX::PackInfo info;
|
||||||
|
info.buffer = buffer;
|
||||||
|
info.packSize = size;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Packet>
|
||||||
|
void checkTypedBoundary(uint8_t msgType, uint8_t minimum)
|
||||||
|
{
|
||||||
|
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||||
|
ExposedPacket<Packet> packet;
|
||||||
|
|
||||||
|
IR_FOX::PackInfo shortInfo = frame(buffer, msgType, uint8_t(minimum - 1U));
|
||||||
|
assert(!packet.attach(&shortInfo));
|
||||||
|
assert(!packet.available());
|
||||||
|
assert(!packet.availableRaw());
|
||||||
|
|
||||||
|
IR_FOX::PackInfo minimumInfo = frame(buffer, msgType, minimum);
|
||||||
|
assert(packet.attach(&minimumInfo));
|
||||||
|
assert(packet.available());
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMinimumSizes()
|
||||||
|
{
|
||||||
|
struct Case
|
||||||
|
{
|
||||||
|
uint8_t msgType;
|
||||||
|
uint8_t minimum;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Case cases[] = {
|
||||||
|
{IR_MSG_DATA_ACCEPT, 7},
|
||||||
|
{IR_MSG_DATA_NOACCEPT, 7},
|
||||||
|
{IR_MSG_BACK, 5},
|
||||||
|
{IR_MSG_BACK_TO, 7},
|
||||||
|
{IR_MSG_REQUEST, 7},
|
||||||
|
{IR_MSG_ACCEPT, 6},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const Case &item : cases)
|
||||||
|
{
|
||||||
|
assert(PacketTypes::minimumPacketSize(item.msgType) == item.minimum);
|
||||||
|
assert(!PacketTypes::isTypedPacketSizeValid(item.msgType, uint8_t(item.minimum - 1U)));
|
||||||
|
assert(PacketTypes::isTypedPacketSizeValid(item.msgType, item.minimum));
|
||||||
|
assert(PacketTypes::isTypedPacketSizeValid(item.msgType, uint8_t(item.minimum + 1U)));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(PacketTypes::minimumPacketSize(3) == 0);
|
||||||
|
assert(PacketTypes::minimumPacketSize(5) == 0);
|
||||||
|
assert(!PacketTypes::isTypedPacketSizeValid(3, 31));
|
||||||
|
assert(!PacketTypes::isTypedPacketSizeValid(5, 31));
|
||||||
|
|
||||||
|
checkTypedBoundary<PacketTypes::Data>(IR_MSG_DATA_ACCEPT, 7);
|
||||||
|
checkTypedBoundary<PacketTypes::Data>(IR_MSG_DATA_NOACCEPT, 7);
|
||||||
|
checkTypedBoundary<PacketTypes::DataBack>(IR_MSG_BACK, 5);
|
||||||
|
checkTypedBoundary<PacketTypes::DataBack>(IR_MSG_BACK_TO, 7);
|
||||||
|
checkTypedBoundary<PacketTypes::Request>(IR_MSG_REQUEST, 7);
|
||||||
|
checkTypedBoundary<PacketTypes::Accept>(IR_MSG_ACCEPT, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPayloadAccessSaturates()
|
||||||
|
{
|
||||||
|
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||||
|
ExposedPacket<PacketTypes::Data> packet;
|
||||||
|
|
||||||
|
for (uint8_t size = 0; size < 7; ++size)
|
||||||
|
{
|
||||||
|
IR_FOX::PackInfo tooShort = frame(buffer, IR_MSG_DATA_ACCEPT, size);
|
||||||
|
assert(!packet.attach(&tooShort));
|
||||||
|
assert(packet.getDataSize() == 0);
|
||||||
|
assert(packet.getDataPrt() == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
IR_FOX::PackInfo emptyPayload = frame(buffer, IR_MSG_DATA_ACCEPT, 7);
|
||||||
|
assert(packet.attach(&emptyPayload));
|
||||||
|
assert(packet.getDataSize() == 0);
|
||||||
|
assert(packet.getDataPrt() == buffer + 5);
|
||||||
|
|
||||||
|
IR_FOX::PackInfo oneBytePayload = frame(buffer, IR_MSG_DATA_ACCEPT, 8);
|
||||||
|
assert(packet.attach(&oneBytePayload));
|
||||||
|
assert(packet.getDataSize() == 1);
|
||||||
|
assert(packet.getDataPrt() == buffer + 5);
|
||||||
|
|
||||||
|
IR_FOX::PackInfo nullBuffer;
|
||||||
|
nullBuffer.packSize = 31;
|
||||||
|
assert(!packet.attach(&nullBuffer));
|
||||||
|
assert(packet.getDataSize() == 0);
|
||||||
|
assert(packet.getDataPrt() == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testBackPayloadOffsets()
|
||||||
|
{
|
||||||
|
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||||
|
ExposedPacket<PacketTypes::DataBack> packet;
|
||||||
|
|
||||||
|
IR_FOX::PackInfo addressed = frame(buffer, IR_MSG_BACK_TO, 7);
|
||||||
|
assert(packet.attach(&addressed));
|
||||||
|
assert(packet.getDataSize() == 0);
|
||||||
|
assert(packet.getDataPrt() == buffer + 5);
|
||||||
|
|
||||||
|
IR_FOX::PackInfo broadcast = frame(buffer, IR_MSG_BACK, 5);
|
||||||
|
assert(packet.attach(&broadcast));
|
||||||
|
assert(packet.getDataSize() == 0);
|
||||||
|
assert(packet.getDataPrt() == buffer + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testRawContractIsIndependent()
|
||||||
|
{
|
||||||
|
uint8_t buffer[irproto::kMaxWireFrameBytes] = {};
|
||||||
|
ExposedPacket<PacketTypes::BasePack> raw;
|
||||||
|
IR_FOX::PackInfo info = frame(buffer, IR_MSG_DATA_ACCEPT, 3);
|
||||||
|
|
||||||
|
assert(raw.attach(&info, 0, false));
|
||||||
|
assert(raw.availableRaw());
|
||||||
|
assert(raw.getDataRawSize() == 3);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
testMinimumSizes();
|
||||||
|
testPayloadAccessSaturates();
|
||||||
|
testBackPayloadOffsets();
|
||||||
|
testRawContractIsIndependent();
|
||||||
|
std::cout << "packet type boundary tests: OK\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
69
tests/test_protocol_contract.cpp
Normal file
69
tests/test_protocol_contract.cpp
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#include "IR_DecoderRaw.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Reproduces the removed 2025 expression exactly, but with integer arithmetic:
|
||||||
|
// 2.7735 == 27735 / 10000. It is a provenance golden, not a new PHY rule.
|
||||||
|
constexpr uint32_t removedLegacyResponseExpressionMs()
|
||||||
|
{
|
||||||
|
const uint64_t scaledUs =
|
||||||
|
static_cast<uint64_t>(irproto::kNominalRxInterEdgeTimeoutUs) * 27735U / 10000U;
|
||||||
|
return static_cast<uint16_t>(scaledUs) / 1000U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static_assert(irproto::kWireFrameLengthBits == 5U, "wire length field changed");
|
||||||
|
static_assert(irproto::kWireFrameLengthMask == 31U, "wire length mask changed");
|
||||||
|
static_assert(irproto::kMaxWireFrameBytes == 31U, "wire frame limit changed");
|
||||||
|
static_assert(irproto::kDataFrameOverheadBytes == 7U, "DATA overhead changed");
|
||||||
|
static_assert(irproto::kBackFrameOverheadBytes == 5U, "BACK overhead changed");
|
||||||
|
static_assert(irproto::kBackToFrameOverheadBytes == 7U, "BACK_TO overhead changed");
|
||||||
|
static_assert(irproto::kMaxDataPayloadBytes == 24U, "DATA payload limit changed");
|
||||||
|
static_assert(irproto::kMaxBackPayloadBytes == 26U, "BACK payload limit changed");
|
||||||
|
static_assert(irproto::kMaxBackToPayloadBytes == 24U, "BACK_TO payload limit changed");
|
||||||
|
static_assert(irproto::kMaxLogicalGateRuns == 688U, "logical max-frame run bound changed");
|
||||||
|
static_assert(irproto::maxPhysicalGateRunCapacity(UINT8_MAX) == 738U,
|
||||||
|
"uint8 carrier-multiply storage bound changed");
|
||||||
|
|
||||||
|
static_assert(IR_MASK_MSG_TYPE == irproto::kMessageTypeMask, "legacy type mask diverged");
|
||||||
|
static_assert(IR_MASK_MSG_INFO == irproto::kWireFrameLengthMask, "legacy length mask diverged");
|
||||||
|
static_assert(bytePerPack == irproto::kMaxWireFrameBytes,
|
||||||
|
"legacy bytePerPack value must remain source-compatible");
|
||||||
|
static_assert(dataByteSizeMax == irproto::kMaxWireFrameBytes,
|
||||||
|
"legacy storage alias must follow the wire limit");
|
||||||
|
|
||||||
|
static_assert(irproto::kRxInterEdgeTimeoutBitWindows == 12U,
|
||||||
|
"8 data + 3 sync + 1 guard geometry changed");
|
||||||
|
static_assert(irproto::kNominalRxInterEdgeTimeoutUs == 15144U,
|
||||||
|
"nominal inter-edge timeout changed");
|
||||||
|
static_assert(irproto::kNominalRxSilenceTimeoutUs == 30288U,
|
||||||
|
"nominal RX silence timeout changed");
|
||||||
|
static_assert(irproto::microsToMillisCeil(irproto::kNominalRxSilenceTimeoutUs) == 31U,
|
||||||
|
"RX silence ceil-ms conversion changed");
|
||||||
|
static_assert(IR_ResponseDelay == 42U, "deployed response turn-around changed");
|
||||||
|
static_assert(removedLegacyResponseExpressionMs() == IR_ResponseDelay,
|
||||||
|
"named empirical response delay no longer matches its legacy provenance");
|
||||||
|
|
||||||
|
void testAdaptiveTimingGeometry()
|
||||||
|
{
|
||||||
|
assert(irproto::rxInterEdgeTimeoutUs(700U) == 12000U);
|
||||||
|
assert(irproto::rxSilenceTimeoutUs(700U) == 24000U);
|
||||||
|
assert(irproto::rxInterEdgeTimeoutUs(1000U) == 15600U);
|
||||||
|
assert(irproto::rxSilenceTimeoutUs(1000U) == 31200U);
|
||||||
|
assert(irproto::microsToMillisCeil(0U) == 0U);
|
||||||
|
assert(irproto::microsToMillisCeil(1U) == 1U);
|
||||||
|
assert(irproto::microsToMillisCeil(1000U) == 1U);
|
||||||
|
assert(irproto::microsToMillisCeil(1001U) == 2U);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
testAdaptiveTimingGeometry();
|
||||||
|
std::cout << "IR protocol geometry contract tests: OK\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
79
tests/test_rx_reason_contract.cpp
Normal file
79
tests/test_rx_reason_contract.cpp
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#include "IR_config.h"
|
||||||
|
#include "RingBuffer.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
// Test-only visibility: exercise the private reason enum and logging bound without
|
||||||
|
// widening the production API. Dependencies are included first so this macro
|
||||||
|
// cannot rewrite access specifiers in the standard library.
|
||||||
|
#define private public
|
||||||
|
#include "IR_DecoderRaw.h"
|
||||||
|
#undef private
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char kZeroStats[] =
|
||||||
|
"RXSTAT,MUTEB=0,MUTEE=0,QRAW=0,QFLT=0,HOLD=0,GLITCH=0,TIME=0,"
|
||||||
|
"PREAMB=0,SYNC=0,BUF=0,TIMEOUT=0,CRC=0,OK=0\n";
|
||||||
|
|
||||||
|
constexpr const char kOneEachStats[] =
|
||||||
|
"RXSTAT,MUTEB=1,MUTEE=1,QRAW=1,QFLT=1,HOLD=1,GLITCH=1,TIME=1,"
|
||||||
|
"PREAMB=1,SYNC=1,BUF=1,TIMEOUT=1,CRC=1,OK=1\n";
|
||||||
|
|
||||||
|
static_assert(IR_DecoderRaw::rxReasonCounterCount() > 0U,
|
||||||
|
"RX reason counter storage must not be empty");
|
||||||
|
static_assert(IR_DecoderRaw::rxReasonCounterCount() ==
|
||||||
|
static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Count),
|
||||||
|
"public RX reason count must follow the enum sentinel");
|
||||||
|
static_assert(static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Count) ==
|
||||||
|
static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::Ok) + 1U,
|
||||||
|
"RX reason Count must remain one past the final reason");
|
||||||
|
static_assert(std::extent<decltype(IR_DecoderRaw::rxReasonCnt)>::value ==
|
||||||
|
IR_DecoderRaw::rxReasonCounterCount(),
|
||||||
|
"RX reason counter array must follow the enum-derived count");
|
||||||
|
|
||||||
|
void testStatsWireFormatAndClearCoverage()
|
||||||
|
{
|
||||||
|
IR_DecoderRaw decoder(0U, 0U);
|
||||||
|
Print out;
|
||||||
|
|
||||||
|
decoder.printRxReasonStats(out);
|
||||||
|
assert(out.str() == kZeroStats);
|
||||||
|
|
||||||
|
const uint8_t first = static_cast<uint8_t>(IR_DecoderRaw::RxBriefReason::MuteBegin);
|
||||||
|
const uint8_t count = IR_DecoderRaw::rxReasonCounterCount();
|
||||||
|
for (uint8_t i = first; i < count; ++i)
|
||||||
|
decoder.rxBriefLog(static_cast<IR_DecoderRaw::RxBriefReason>(i));
|
||||||
|
|
||||||
|
// The sentinel is a bound, not a loggable reason.
|
||||||
|
decoder.rxBriefLog(IR_DecoderRaw::RxBriefReason::Count);
|
||||||
|
|
||||||
|
const uint16_t *const counters = decoder.rxReasonCounters();
|
||||||
|
assert(counters[0] == 0U);
|
||||||
|
for (uint8_t i = first; i < count; ++i)
|
||||||
|
assert(counters[i] == 1U);
|
||||||
|
|
||||||
|
out.clear();
|
||||||
|
decoder.printRxReasonStats(out);
|
||||||
|
assert(out.str() == kOneEachStats);
|
||||||
|
|
||||||
|
decoder.rxReasonCountersClear();
|
||||||
|
for (uint8_t i = 0U; i < count; ++i)
|
||||||
|
assert(counters[i] == 0U);
|
||||||
|
|
||||||
|
out.clear();
|
||||||
|
decoder.printRxReasonStats(out);
|
||||||
|
assert(out.str() == kZeroStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
testStatsWireFormatAndClearCoverage();
|
||||||
|
std::cout << "RX reason counter/tag contract tests: OK\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
328
tests/test_tx_contract.cpp
Normal file
328
tests/test_tx_contract.cpp
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
#include "IR_Encoder.h"
|
||||||
|
#include "IR_DecoderRaw.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
// Link-only seams for planner/lifecycle host tests. The real implementations
|
||||||
|
// are irrelevant here; no decoder or legacy sendByte helper is exercised.
|
||||||
|
bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *) { return true; }
|
||||||
|
void IR_DecoderRaw::refreshPairMuteState() {}
|
||||||
|
void IR_Encoder::send_HIGH(bool) {}
|
||||||
|
void IR_Encoder::send_LOW() {}
|
||||||
|
void IR_Encoder::send_EMPTY(uint8_t) {}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr size_t kRunCapacity = 2048U;
|
||||||
|
constexpr size_t kFullMultiplyRunCapacity =
|
||||||
|
irproto::maxPhysicalGateRunCapacity(UINT16_MAX);
|
||||||
|
|
||||||
|
static_assert(irproto::kMaxLogicalTransmissionTicks == 25822U,
|
||||||
|
"golden maximum PHY duration changed");
|
||||||
|
static_assert(irproto::maxPhysicalTransmissionTicks(2U) == 25822U,
|
||||||
|
"nominal physical tick conversion changed");
|
||||||
|
static_assert(irproto::maxPhysicalGateRunCapacity(UINT8_MAX) <= 1024U,
|
||||||
|
"uint8_t carrier-multiply domain no longer fits the legacy Car allocation");
|
||||||
|
|
||||||
|
uint32_t sumTicks(const IrTxGateRun *runs, uint32_t count)
|
||||||
|
{
|
||||||
|
uint32_t total = 0U;
|
||||||
|
for (uint32_t i = 0; i < count; ++i)
|
||||||
|
total += runs[i].lenTicks;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fillPattern(uint8_t *frame, uint8_t len, uint8_t pattern)
|
||||||
|
{
|
||||||
|
for (uint8_t i = 0; i < len; ++i)
|
||||||
|
{
|
||||||
|
switch (pattern)
|
||||||
|
{
|
||||||
|
case 0: frame[i] = 0x00U; break;
|
||||||
|
case 1: frame[i] = 0xFFU; break;
|
||||||
|
case 2: frame[i] = (i & 1U) ? 0x55U : 0xAAU; break;
|
||||||
|
default: frame[i] = static_cast<uint8_t>(i * 73U + 19U); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPlannerMatchesBuiltStream()
|
||||||
|
{
|
||||||
|
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||||
|
std::array<IrTxGateRun, kRunCapacity> runs{};
|
||||||
|
const uint16_t multiplies[] = {2U, 3U, 6U};
|
||||||
|
|
||||||
|
for (uint16_t multiply : multiplies)
|
||||||
|
{
|
||||||
|
for (uint8_t len = 1U; len <= irproto::kMaxWireFrameBytes; ++len)
|
||||||
|
{
|
||||||
|
uint32_t durationForLength = 0U;
|
||||||
|
uint32_t ticksForLength = 0U;
|
||||||
|
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
|
||||||
|
{
|
||||||
|
fillPattern(frame.data(), len, pattern);
|
||||||
|
const IR_TxPlan planned =
|
||||||
|
IR_Encoder::planPhysicalTransmission(frame.data(), len, multiply);
|
||||||
|
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||||
|
frame.data(), len, runs.data(), runs.size(), multiply);
|
||||||
|
assert(planned.valid());
|
||||||
|
assert(built.valid());
|
||||||
|
assert(planned.physicalTicks == built.physicalTicks);
|
||||||
|
assert(planned.gateRunCount == built.gateRunCount);
|
||||||
|
assert(planned.airtimeUs == built.airtimeUs);
|
||||||
|
assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks);
|
||||||
|
if (pattern == 0U)
|
||||||
|
{
|
||||||
|
durationForLength = planned.airtimeUs;
|
||||||
|
ticksForLength = planned.physicalTicks;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assert(planned.airtimeUs == durationForLength);
|
||||||
|
assert(planned.physicalTicks == ticksForLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testGoldenNominalTimings()
|
||||||
|
{
|
||||||
|
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||||
|
struct Golden { uint8_t bytes; uint32_t logicalTicks; uint32_t usCeil; };
|
||||||
|
const Golden golden[] = {
|
||||||
|
{6U, 5472U, 72000U},
|
||||||
|
{10U, 8728U, 114843U},
|
||||||
|
{31U, 25822U, 339764U},
|
||||||
|
};
|
||||||
|
for (const Golden& item : golden)
|
||||||
|
{
|
||||||
|
const IR_TxPlan plan =
|
||||||
|
IR_Encoder::planPhysicalTransmission(frame.data(), item.bytes, 2U);
|
||||||
|
assert(plan.valid());
|
||||||
|
assert(plan.physicalTicks == item.logicalTicks);
|
||||||
|
assert(plan.airtimeUs == item.usCeil);
|
||||||
|
assert(plan.airtimeMsCeil() == (item.usCeil + 999U) / 1000U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testCapacityAndClockContracts()
|
||||||
|
{
|
||||||
|
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||||
|
std::array<IrTxGateRun, kRunCapacity> logicalRuns{};
|
||||||
|
fillPattern(frame.data(), frame.size(), 2U);
|
||||||
|
assert(IR_Encoder::buildGateRuns(
|
||||||
|
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||||
|
logicalRuns.data(), logicalRuns.size()) != 0U);
|
||||||
|
|
||||||
|
std::array<uint8_t, irproto::kMaxWireFrameBytes + 1U> oversizedFrame{};
|
||||||
|
assert(IR_Encoder::buildGateRuns(
|
||||||
|
oversizedFrame.data(), static_cast<uint8_t>(oversizedFrame.size()),
|
||||||
|
logicalRuns.data(), logicalRuns.size()) == 0U);
|
||||||
|
|
||||||
|
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
|
||||||
|
frame.data(), static_cast<uint8_t>(frame.size()), 6U);
|
||||||
|
assert(planned.valid());
|
||||||
|
assert(planned.gateRunCount <= irproto::kIsrTxMaxGateRuns);
|
||||||
|
|
||||||
|
IrTxGateRun oneRun{};
|
||||||
|
const IR_TxPlan tooSmall = IR_Encoder::buildPhysicalTransmission(
|
||||||
|
frame.data(), static_cast<uint8_t>(frame.size()), &oneRun, 1U, 6U);
|
||||||
|
assert(!tooSmall.valid());
|
||||||
|
assert(tooSmall.status == IR_SendStatus::BuildGateRunsFailed);
|
||||||
|
assert(tooSmall.gateRunCount == planned.gateRunCount);
|
||||||
|
assert(tooSmall.physicalTicks == planned.physicalTicks);
|
||||||
|
|
||||||
|
std::array<uint8_t, 10U> tenBytes{};
|
||||||
|
IR_TxPlan configured = IR_Encoder::planPhysicalTransmission(
|
||||||
|
tenBytes.data(), static_cast<uint8_t>(tenBytes.size()), 6U);
|
||||||
|
assert(configured.airtimeUs == 114843U);
|
||||||
|
assert(IR_Encoder::applyTickClock(configured, 12000000U, 52U));
|
||||||
|
assert(configured.clockBasis == IR_TxClockBasis::ConfiguredTimer);
|
||||||
|
assert(configured.airtimeUs == 113464U);
|
||||||
|
|
||||||
|
assert(!IR_Encoder::planPhysicalTransmission(nullptr, 1U, 2U).valid());
|
||||||
|
assert(!IR_Encoder::planPhysicalTransmission(frame.data(), 0U, 2U).valid());
|
||||||
|
assert(!IR_Encoder::planPhysicalTransmission(
|
||||||
|
frame.data(), static_cast<uint8_t>(irproto::kMaxWireFrameBytes + 1U), 2U).valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDerivedFixedStorageCapacity()
|
||||||
|
{
|
||||||
|
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||||
|
std::array<IrTxGateRun, kFullMultiplyRunCapacity> runs{};
|
||||||
|
const uint16_t multiplies[] = {2U, 3U, 6U, UINT8_MAX, UINT16_MAX};
|
||||||
|
|
||||||
|
for (uint16_t multiply : multiplies)
|
||||||
|
{
|
||||||
|
const size_t capacity = irproto::maxPhysicalGateRunCapacity(multiply);
|
||||||
|
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
|
||||||
|
{
|
||||||
|
fillPattern(frame.data(), static_cast<uint8_t>(frame.size()), pattern);
|
||||||
|
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
|
||||||
|
frame.data(), static_cast<uint8_t>(frame.size()), multiply);
|
||||||
|
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||||
|
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||||
|
runs.data(), capacity, multiply);
|
||||||
|
assert(planned.valid());
|
||||||
|
assert(built.valid());
|
||||||
|
assert(built.gateRunCount <= capacity);
|
||||||
|
assert(built.gateRunCount == planned.gateRunCount);
|
||||||
|
assert(built.physicalTicks == planned.physicalTicks);
|
||||||
|
assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testInPlacePhysicalScaling()
|
||||||
|
{
|
||||||
|
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
|
||||||
|
std::array<IrTxGateRun, kRunCapacity> logical{};
|
||||||
|
std::array<IrTxGateRun, kRunCapacity> expected{};
|
||||||
|
fillPattern(frame.data(), static_cast<uint8_t>(frame.size()), 3U);
|
||||||
|
|
||||||
|
const size_t logicalCount = IR_Encoder::buildGateRuns(
|
||||||
|
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||||
|
logical.data(), logical.size());
|
||||||
|
assert(logicalCount != 0U);
|
||||||
|
|
||||||
|
for (uint16_t multiply : {2U, 3U, 6U})
|
||||||
|
{
|
||||||
|
auto scaled = logical;
|
||||||
|
size_t scaledCount = logicalCount;
|
||||||
|
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
|
||||||
|
frame.data(), static_cast<uint8_t>(frame.size()),
|
||||||
|
expected.data(), expected.size(), multiply);
|
||||||
|
assert(built.valid());
|
||||||
|
assert(IR_Encoder::scaleGateRunsToPhysical(
|
||||||
|
scaled.data(), &scaledCount, scaled.size(), multiply));
|
||||||
|
assert(scaledCount == built.gateRunCount);
|
||||||
|
for (size_t i = 0; i < scaledCount; ++i)
|
||||||
|
{
|
||||||
|
assert(scaled[i].gate == expected[i].gate);
|
||||||
|
assert(scaled[i].lenTicks == expected[i].lenTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expansion beyond uint16_t is also in-place and preserves chunk order.
|
||||||
|
std::array<IrTxGateRun, 8U> longRun{};
|
||||||
|
longRun[0] = {65535U, true};
|
||||||
|
size_t longCount = 1U;
|
||||||
|
assert(IR_Encoder::scaleGateRunsToPhysical(
|
||||||
|
longRun.data(), &longCount, longRun.size(), 7U));
|
||||||
|
assert(longCount == 4U);
|
||||||
|
assert(longRun[0].lenTicks == 65535U);
|
||||||
|
assert(longRun[1].lenTicks == 65535U);
|
||||||
|
assert(longRun[2].lenTicks == 65535U);
|
||||||
|
assert(longRun[3].lenTicks == 32768U);
|
||||||
|
assert(sumTicks(longRun.data(), static_cast<uint32_t>(longCount)) == 229373U);
|
||||||
|
|
||||||
|
std::array<IrTxGateRun, 2U> tooSmall{{{65535U, true}, {1U, false}}};
|
||||||
|
size_t tooSmallCount = 1U;
|
||||||
|
assert(!IR_Encoder::scaleGateRunsToPhysical(
|
||||||
|
tooSmall.data(), &tooSmallCount, tooSmall.size(), 6U));
|
||||||
|
assert(tooSmallCount == 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeBackend
|
||||||
|
{
|
||||||
|
IR_SendStatus startStatus = IR_SendStatus::Success;
|
||||||
|
bool finishSynchronously = false;
|
||||||
|
IR_Encoder *encoder = nullptr;
|
||||||
|
uint32_t operationId = 0U;
|
||||||
|
IR_TxPlan plan{};
|
||||||
|
};
|
||||||
|
|
||||||
|
IR_SendStatus fakeStart(void *opaque,
|
||||||
|
IR_Encoder *encoder,
|
||||||
|
const uint8_t *,
|
||||||
|
uint8_t,
|
||||||
|
const IR_TxPlan& plan,
|
||||||
|
uint32_t operationId)
|
||||||
|
{
|
||||||
|
auto& backend = *static_cast<FakeBackend *>(opaque);
|
||||||
|
backend.encoder = encoder;
|
||||||
|
backend.operationId = operationId;
|
||||||
|
backend.plan = plan;
|
||||||
|
if (backend.startStatus == IR_SendStatus::Success && backend.finishSynchronously)
|
||||||
|
encoder->externalFinishSend(operationId, IR_SendStatus::Success);
|
||||||
|
return backend.startStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testTokenLifecycle()
|
||||||
|
{
|
||||||
|
FakeBackend backend;
|
||||||
|
IR_Encoder::setExternalTxBackendV2(fakeStart, nullptr, &backend);
|
||||||
|
IR_Encoder encoder(1U, 42U, nullptr, false);
|
||||||
|
uint8_t payload = 0x5EU;
|
||||||
|
|
||||||
|
arduino_stub_micros = 100U;
|
||||||
|
const IR_SendResult first = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||||
|
assert(first.success);
|
||||||
|
assert(first.operationId != 0U);
|
||||||
|
assert(first.plannedAirtimeUs == backend.plan.airtimeUs);
|
||||||
|
assert(encoder.isBusy());
|
||||||
|
IR_TxSnapshot snapshot = encoder.txSnapshot();
|
||||||
|
assert(snapshot.operationId == first.operationId);
|
||||||
|
assert(snapshot.state == IR_TxState::Transmitting);
|
||||||
|
|
||||||
|
encoder.externalFinishSend(first.operationId + 1U, IR_SendStatus::Success);
|
||||||
|
assert(encoder.isBusy());
|
||||||
|
arduino_stub_micros = 200U;
|
||||||
|
encoder.externalFinishSend(first.operationId, IR_SendStatus::DmaTransferError);
|
||||||
|
snapshot = encoder.txSnapshot();
|
||||||
|
assert(!encoder.isBusy());
|
||||||
|
assert(snapshot.state == IR_TxState::Failed);
|
||||||
|
assert(snapshot.status == IR_SendStatus::DmaTransferError);
|
||||||
|
assert(snapshot.terminalAtUs == 200U);
|
||||||
|
encoder.externalFinishSend(first.operationId, IR_SendStatus::Success);
|
||||||
|
assert(encoder.txSnapshot().status == IR_SendStatus::DmaTransferError);
|
||||||
|
|
||||||
|
arduino_stub_micros = 300U;
|
||||||
|
const IR_SendResult second = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||||
|
assert(second.success && second.operationId != first.operationId);
|
||||||
|
encoder.externalFinishSend(first.operationId, IR_SendStatus::Success);
|
||||||
|
assert(encoder.isBusy());
|
||||||
|
encoder.externalFinishSend(second.operationId, IR_SendStatus::Success);
|
||||||
|
assert(encoder.isOperationComplete(second.operationId));
|
||||||
|
|
||||||
|
backend.startStatus = IR_SendStatus::DmaStartFailed;
|
||||||
|
const IR_SendResult rejectedAfterOwnership = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||||
|
assert(!rejectedAfterOwnership.success);
|
||||||
|
assert(rejectedAfterOwnership.operationId != 0U);
|
||||||
|
snapshot = encoder.txSnapshot();
|
||||||
|
assert(snapshot.state == IR_TxState::Failed);
|
||||||
|
assert(snapshot.status == IR_SendStatus::DmaStartFailed);
|
||||||
|
|
||||||
|
backend.startStatus = IR_SendStatus::Success;
|
||||||
|
const IR_SendResult active = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||||
|
const IR_SendResult busy = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||||
|
assert(active.success);
|
||||||
|
assert(!busy.success && busy.status == IR_SendStatus::EncoderBusy);
|
||||||
|
assert(busy.operationId == 0U);
|
||||||
|
encoder.externalFinishSend(active.operationId, IR_SendStatus::Success);
|
||||||
|
|
||||||
|
backend.finishSynchronously = true;
|
||||||
|
const IR_SendResult synchronous = encoder.sendData(IR_Broadcast, &payload, 1U);
|
||||||
|
assert(synchronous.success);
|
||||||
|
assert(encoder.isOperationComplete(synchronous.operationId));
|
||||||
|
assert(!encoder.isBusy());
|
||||||
|
|
||||||
|
IR_Encoder::setExternalTxBackendV2(nullptr, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
testPlannerMatchesBuiltStream();
|
||||||
|
testGoldenNominalTimings();
|
||||||
|
testCapacityAndClockContracts();
|
||||||
|
testDerivedFixedStorageCapacity();
|
||||||
|
testInPlacePhysicalScaling();
|
||||||
|
testTokenLifecycle();
|
||||||
|
std::cout << "IR TX contract tests: OK\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user