8 Commits

Author SHA1 Message Date
fc93ea41db Merge pull request #8 from Show-maket/STM32
Stm32
2026-03-11 17:06:20 +03:00
0620d98e35 Merge branch 'main' into STM32 2026-03-11 17:06:06 +03:00
4caed06218 Merge pull request #7 from Show-maket/STM32DMA
upd
2026-03-11 16:59:08 +03:00
37522f974f upd 2024-12-25 16:56:18 +03:00
6375c4eed5 for debug test 2024-09-04 10:12:39 +03:00
c4000d6b75 test 2024-08-14 17:43:12 +03:00
fc02c79135 upd 2024-04-22 14:56:12 +03:00
d068a576f7 Merge pull request #3 from Show-maket/dev
v2.0
2024-03-18 16:24:38 +03:00
39 changed files with 103 additions and 2842 deletions

9
.gitignore vendored
View File

@ -1,12 +1,3 @@
.vscode/* .vscode/*
bin/* bin/*
!.vscode/launch.json
log/* log/*
/.vscode
*.zip
**/__pycache__
Analyzer/raw/dll/*.dll
Analyzer/raw/dll/*.so
Analyzer/raw/dll/*.dylib
/Analyzer/raw/IR_Fox/.github
**/.build

View File

@ -1,2 +0,0 @@
/build
.DS_Store

View File

@ -1,26 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(IrFoxAnalyzer)
add_definitions(-DLOGIC2)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
include(ExternalAnalyzerSDK)
set(SOURCES
src/IrFoxAnalyzer.cpp
src/IrFoxAnalyzer.h
src/IrFoxDecoder.cpp
src/IrFoxDecoder.h
src/IrFoxAnalyzerResults.cpp
src/IrFoxAnalyzerResults.h
src/IrFoxAnalyzerSettings.cpp
src/IrFoxAnalyzerSettings.h
src/IrFoxSimulationDataGenerator.cpp
src/IrFoxSimulationDataGenerator.h
)
add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES})

View File

@ -1,49 +0,0 @@
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 19,
"patch": 0
},
"configurePresets": [
{
"name": "win-vs2022-x64",
"displayName": "Visual Studio 2022 (x64)",
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"binaryDir": "${sourceDir}/build"
},
{
"name": "win-vs2019-x64",
"displayName": "Visual Studio 2019 (x64)",
"generator": "Visual Studio 16 2019",
"architecture": "x64",
"binaryDir": "${sourceDir}/build"
},
{
"name": "win-nmake-release",
"displayName": "NMake Release (только из «x64 Native Tools Command Prompt for VS»)",
"generator": "NMake Makefiles",
"binaryDir": "${sourceDir}/build-nmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "win-release",
"configurePreset": "win-vs2022-x64",
"configuration": "Release"
},
{
"name": "win-release-vs2019",
"configurePreset": "win-vs2019-x64",
"configuration": "Release"
},
{
"name": "nmake-release",
"configurePreset": "win-nmake-release"
}
]
}

View File

@ -1,21 +0,0 @@
@echo off
setlocal EnableDelayedExpansion
cd /d "%~dp0"
if not exist build\CMakeCache.txt (
echo Run configure_msvc.bat first.
exit /b 1
)
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i"
if not defined VSINSTALL (
echo MSVC not found. Add C++ workload in Visual Studio Installer.
exit /b 1
)
call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64
if errorlevel 1 exit /b 1
cmake --build build
pause
exit /b %ERRORLEVEL%

View File

@ -1,66 +0,0 @@
include(FetchContent)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
if(NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY OR NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/)
endif()
if(NOT TARGET Saleae::AnalyzerSDK)
FetchContent_Declare(
analyzersdk
GIT_REPOSITORY https://github.com/saleae/AnalyzerSDK.git
GIT_TAG master
GIT_SHALLOW True
GIT_PROGRESS True
)
FetchContent_GetProperties(analyzersdk)
if(NOT analyzersdk_POPULATED)
FetchContent_Populate(analyzersdk)
include(${analyzersdk_SOURCE_DIR}/AnalyzerSDKConfig.cmake)
if(APPLE OR WIN32)
get_target_property(analyzersdk_lib_location Saleae::AnalyzerSDK IMPORTED_LOCATION)
if(CMAKE_LIBRARY_OUTPUT_DIRECTORY)
file(COPY ${analyzersdk_lib_location} DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
else()
message(WARNING "Please define CMAKE_RUNTIME_OUTPUT_DIRECTORY and CMAKE_LIBRARY_OUTPUT_DIRECTORY if you want unit tests to locate ${analyzersdk_lib_location}")
endif()
endif()
endif()
endif()
# Shared folder for all Saleae LLA plugins in this repo: Analyzer/raw/dll
set(ANALYZER_DLL_OUT_DIR "${CMAKE_SOURCE_DIR}/../dll")
get_filename_component(ANALYZER_DLL_OUT_DIR "${ANALYZER_DLL_OUT_DIR}" ABSOLUTE)
file(MAKE_DIRECTORY "${ANALYZER_DLL_OUT_DIR}")
function(add_analyzer_plugin TARGET)
set(options)
set(single_value_args)
set(multi_value_args SOURCES)
cmake_parse_arguments(_p "${options}" "${single_value_args}" "${multi_value_args}" ${ARGN})
add_library(${TARGET} MODULE ${_p_SOURCES})
target_link_libraries(${TARGET} PRIVATE Saleae::AnalyzerSDK)
set(ANALYZER_DESTINATION "Analyzers")
install(TARGETS ${TARGET} RUNTIME DESTINATION ${ANALYZER_DESTINATION}
LIBRARY DESTINATION ${ANALYZER_DESTINATION})
set_target_properties(${TARGET} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}"
LIBRARY_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}")
if(CMAKE_CONFIGURATION_TYPES)
foreach(CFG ${CMAKE_CONFIGURATION_TYPES})
string(TOUPPER ${CFG} CFG_UPPER)
set_target_properties(${TARGET} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}"
LIBRARY_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}")
endforeach()
endif()
endfunction()

View File

@ -1,39 +0,0 @@
@echo off
setlocal EnableDelayedExpansion
cd /d "%~dp0"
echo === IrFoxAnalyzer: configure with MSVC ===
echo.
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" (
echo [ERROR] vswhere not found. Install one of:
echo - Visual Studio 2022 with workload "Desktop development with C++"
echo - Build Tools for Visual Studio 2022: https://visualstudio.microsoft.com/visual-cpp-build-tools/
echo ^(select "Desktop development with C++" / MSVC, Windows SDK^)
exit /b 1
)
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i"
if not defined VSINSTALL (
echo [ERROR] MSVC toolset not found. Add "Desktop development with C++" in Visual Studio Installer.
exit /b 1
)
echo Found: !VSINSTALL!
call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64
if errorlevel 1 exit /b 1
if exist build rmdir /s /q build
if exist build-nmake rmdir /s /q build-nmake
mkdir build
cd build
cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
if errorlevel 1 exit /b 1
echo.
echo Configure OK. Build: build_msvc.bat ^(or from same VS env: cd build ^& cmake --build .^)
echo Output DLL: ..\dll\ ^(all analyzers share this folder^)
pause
exit /b 0

View File

@ -1,280 +0,0 @@
#include "IrFoxAnalyzer.h"
#include "IrFoxAnalyzerSettings.h"
#include "IrFoxDecoder.h"
#include <AnalyzerChannelData.h>
#include <AnalyzerResults.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
IrFoxAnalyzer::IrFoxAnalyzer()
: Analyzer2(),
mSettings(),
mSimulationInitilized(false)
{
SetAnalyzerSettings(&mSettings);
UseFrameV2();
}
IrFoxAnalyzer::~IrFoxAnalyzer()
{
KillThread();
}
void IrFoxAnalyzer::SetupResults()
{
m_packet_hex_by_frame.clear();
mResults.reset(new IrFoxAnalyzerResults(this, &mSettings));
SetAnalyzerResults(mResults.get());
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
}
static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_bytes = 64)
{
static const char* hd = "0123456789abcdef";
const size_t m = n < max_bytes ? n : max_bytes;
for (size_t i = 0; i < m; i++)
{
s.push_back(hd[p[i] >> 4]);
s.push_back(hd[p[i] & 0xFu]);
if (i + 1 < m)
s.push_back(' ');
}
if (n > max_bytes)
s += "...";
}
const char* IrFoxAnalyzer::PacketHexForFrame(U64 frame_id)
{
auto it = m_packet_hex_by_frame.find(frame_id);
if (it == m_packet_hex_by_frame.end())
return "";
m_hex_scratch = it->second;
return m_hex_scratch.c_str();
}
const char* IrFoxAnalyzer::BubbleTextForFrame(U64 frame_id) const
{
auto it = m_bubble_text_by_frame.find(frame_id);
if (it == m_bubble_text_by_frame.end())
return "";
m_bubble_scratch = it->second;
return m_bubble_scratch.c_str();
}
void IrFoxAnalyzer::WorkerThread()
{
mIr = GetAnalyzerChannelData(mSettings.mInputChannel);
m_packet_hex_by_frame.clear();
m_bubble_text_by_frame.clear();
const U32 fs = GetSampleRate();
IrFoxDecoder decoder;
decoder.reset();
/** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */
const U64 min_seg_samples =
std::max<U64>(1ULL, static_cast<U64>((static_cast<double>(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast<double>(fs) + 0.5));
struct RawEdge
{
U64 sample;
bool rising;
};
std::vector<RawEdge> pending;
U64 last_dec_edge_sample = 0;
bool last_dec_edge_valid = false;
auto collapse_short_pairs = [&]() {
for (size_t i = 0; i + 1 < pending.size();)
{
if (pending[i + 1].sample - pending[i].sample < min_seg_samples)
{
pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i),
pending.begin() + static_cast<std::ptrdiff_t>(i + 2));
if (i > 0)
--i;
}
else
++i;
}
};
auto strip_vs_last_decoder = [&]() {
for (;;)
{
collapse_short_pairs();
if (!last_dec_edge_valid || pending.empty())
return;
if (pending[0].sample - last_dec_edge_sample >= min_seg_samples)
return;
pending.erase(pending.begin());
}
};
U32 frames_since_commit = 0;
const U32 kCommitBatch = 256;
IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) {
Frame frame;
frame.mStartingSampleInclusive = static_cast<S64>(e.start_sample);
frame.mEndingSampleInclusive = static_cast<S64>(e.end_sample);
frame.mType = e.frame_type;
frame.mData1 = e.bit_value;
frame.mData2 = e.bit_index | (U64(e.err_low) << 16) | (U64(e.err_high) << 24) | (U64(e.err_other) << 32);
frame.mFlags = e.mflags;
// В SDK только ERROR/WARNING меняют цвет бабла; sync выделяем янтарным (как warning), данные — обычные.
if (e.frame_type == IRF_FT_SYNC_BIT)
frame.mFlags |= DISPLAY_AS_WARNING_FLAG;
const U64 fid = mResults->AddFrame(frame);
if (e.bubble_text[0] != '\0')
m_bubble_text_by_frame[fid] = e.bubble_text;
if (++frames_since_commit >= kCommitBatch)
{
mResults->CommitResults();
frames_since_commit = 0;
}
};
IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) {
Frame frame;
frame.mStartingSampleInclusive = static_cast<S64>(p.start_sample);
frame.mEndingSampleInclusive = static_cast<S64>(p.end_sample);
frame.mType = p.crc_ok ? IRF_FT_PACKET_OK : IRF_FT_PACKET_CRC_FAIL;
frame.mData1 = p.pack_size;
frame.mData2 = (U64(p.err_low) << 0) | (U64(p.err_high) << 8) | (U64(p.err_other) << 16);
if (!p.crc_ok)
frame.mFlags |= DISPLAY_AS_ERROR_FLAG;
const U64 fid = mResults->AddFrame(frame);
std::string hx;
append_hex(hx, p.data_bytes, p.pack_size);
m_packet_hex_by_frame[fid] = std::move(hx);
FrameV2 fv2;
fv2.AddBoolean("crc_ok", p.crc_ok);
fv2.AddInteger("len", static_cast<S64>(p.pack_size));
fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
fv2.AddByteArray("data", p.data_bytes, p.pack_size);
mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast<U64>(p.start_sample),
static_cast<U64>(p.end_sample));
if (++frames_since_commit >= kCommitBatch)
{
mResults->CommitResults();
frames_since_commit = 0;
}
};
auto emit_confirmed_edges = [&]() {
for (;;)
{
collapse_short_pairs();
strip_vs_last_decoder();
if (pending.size() < 2)
return;
if (pending[1].sample - pending[0].sample < min_seg_samples)
continue;
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.erase(pending.begin());
}
};
auto flush_pending_tail = [&]() {
collapse_short_pairs();
strip_vs_last_decoder();
while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples)
{
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.erase(pending.begin());
collapse_short_pairs();
strip_vs_last_decoder();
}
if (pending.size() == 1)
{
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.clear();
}
};
for (;;)
{
CheckIfThreadShouldExit();
const U64 segment_start = mIr->GetSampleNumber();
const BitState level = mIr->GetBitState();
mIr->AdvanceToNextEdge();
const U64 edge_sample = mIr->GetSampleNumber();
if (edge_sample == segment_start)
break;
const BitState new_level = mIr->GetBitState();
const bool rising = (new_level == BIT_HIGH);
pending.push_back(RawEdge{edge_sample, rising});
emit_confirmed_edges();
ReportProgress(edge_sample);
}
flush_pending_tail();
decoder.flushEnd(mIr->GetSampleNumber(), fs, on_bit, on_pkt);
if (frames_since_commit != 0)
mResults->CommitResults();
}
bool IrFoxAnalyzer::NeedsRerun()
{
return false;
}
U32 IrFoxAnalyzer::GenerateSimulationData(U64 minimum_sample_index, U32 device_sample_rate,
SimulationChannelDescriptor** simulation_channels)
{
if (mSimulationInitilized == false)
{
mSimulationDataGenerator.Initialize(GetSimulationSampleRate(), &mSettings);
mSimulationInitilized = true;
}
return mSimulationDataGenerator.GenerateSimulationData(minimum_sample_index, device_sample_rate,
simulation_channels);
}
U32 IrFoxAnalyzer::GetMinimumSampleRateHz()
{
return 200000;
}
const char* IrFoxAnalyzer::GetAnalyzerName() const
{
return "IR Fox";
}
const char* GetAnalyzerName()
{
return "IR Fox";
}
Analyzer* CreateAnalyzer()
{
return new IrFoxAnalyzer();
}
void DestroyAnalyzer(Analyzer* analyzer)
{
delete analyzer;
}

View File

@ -1,49 +0,0 @@
#ifndef IRFOX_ANALYZER_H
#define IRFOX_ANALYZER_H
#include <Analyzer.h>
#include "IrFoxAnalyzerSettings.h"
#include "IrFoxAnalyzerResults.h"
#include "IrFoxSimulationDataGenerator.h"
#include <memory>
#include <string>
#include <unordered_map>
class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2
{
public:
IrFoxAnalyzer();
virtual ~IrFoxAnalyzer();
virtual void SetupResults();
virtual void WorkerThread();
virtual U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate,
SimulationChannelDescriptor** simulation_channels);
virtual U32 GetMinimumSampleRateHz();
virtual const char* GetAnalyzerName() const;
virtual bool NeedsRerun();
const char* PacketHexForFrame(U64 frame_id);
const char* BubbleTextForFrame(U64 frame_id) const;
protected:
IrFoxAnalyzerSettings mSettings;
std::unique_ptr<IrFoxAnalyzerResults> mResults;
AnalyzerChannelData* mIr;
IrFoxSimulationDataGenerator mSimulationDataGenerator;
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 Analyzer* __cdecl CreateAnalyzer();
extern "C" ANALYZER_EXPORT void __cdecl DestroyAnalyzer(Analyzer* analyzer);
#endif

View File

@ -1,175 +0,0 @@
#include "IrFoxAnalyzerResults.h"
#include <AnalyzerHelpers.h>
#include <AnalyzerResults.h>
#include "IrFoxAnalyzer.h"
#include "IrFoxAnalyzerSettings.h"
#include "IrFoxDecoder.h"
#include <cstdio>
#include <fstream>
IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings)
: AnalyzerResults(),
mSettings(settings),
mAnalyzer(analyzer)
{
}
IrFoxAnalyzerResults::~IrFoxAnalyzerResults()
{
}
void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
{
(void)display_base;
(void)channel;
ClearResultStrings();
Frame frame = GetFrame(frame_index);
char line[256];
switch (frame.mType)
{
case IRF_FT_DATA_BIT:
case IRF_FT_SYNC_BIT:
case IRF_FT_PREAMBLE:
case IRF_FT_OVERFLOW:
case IRF_FT_ABORT:
{
const char* bt = mAnalyzer->BubbleTextForFrame(frame_index);
if (bt && bt[0])
AddResultString(bt);
else if (frame.mType == IRF_FT_DATA_BIT)
AddResultString(frame.mData1 ? "1" : "0");
else if (frame.mType == IRF_FT_SYNC_BIT)
{
snprintf(line, sizeof line, "sync: %s", frame.mData1 ? "1" : "0");
AddResultString(line);
}
else if (frame.mType == IRF_FT_OVERFLOW)
AddResultString("OVF");
else if (frame.mType == IRF_FT_ABORT)
AddResultString("SYNC!");
else
AddResultString("PRE");
break;
}
case IRF_FT_PACKET_OK:
case IRF_FT_PACKET_CRC_FAIL:
{
snprintf(line, sizeof line, "%s %lluB", frame.mType == IRF_FT_PACKET_OK ? "OK" : "CRC",
(unsigned long long)frame.mData1);
AddResultString(line);
const char* hx = mAnalyzer->PacketHexForFrame(frame_index);
if (hx && hx[0])
AddResultString(hx);
break;
}
default:
snprintf(line, sizeof line, "? type=%u", static_cast<unsigned>(frame.mType));
AddResultString(line);
break;
}
}
void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id)
{
(void)export_type_user_id;
(void)display_base;
std::ofstream file_stream(file, std::ios::out);
const U64 trigger_sample = mAnalyzer->GetTriggerSample();
const U32 sample_rate = mAnalyzer->GetSampleRate();
file_stream << "Time[s],Type,Data1,bit_idx,err_low,err_high,err_other,Flags,Hex" << std::endl;
const U64 num_frames = GetNumFrames();
for (U32 i = 0; i < num_frames; i++)
{
Frame frame = GetFrame(i);
char time_str[128];
AnalyzerHelpers::GetTimeString(frame.mStartingSampleInclusive, trigger_sample, sample_rate, time_str, 128);
const char* typ = "?";
switch (frame.mType)
{
case IRF_FT_DATA_BIT:
typ = "D";
break;
case IRF_FT_SYNC_BIT:
typ = "S";
break;
case IRF_FT_PACKET_OK:
typ = "OK";
break;
case IRF_FT_PACKET_CRC_FAIL:
typ = "CRC";
break;
case IRF_FT_OVERFLOW:
typ = "OVF";
break;
case IRF_FT_ABORT:
typ = "ABORT";
break;
case IRF_FT_PREAMBLE:
typ = "PRE";
break;
default:
break;
}
const char* hx = mAnalyzer->PacketHexForFrame(i);
if (!hx)
hx = "";
U64 bit_idx = 0;
U32 err_l = 0, err_h = 0, err_o = 0;
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;
err_l = static_cast<U32>((frame.mData2 >> 16) & 0xFFull);
err_h = static_cast<U32>((frame.mData2 >> 24) & 0xFFull);
err_o = static_cast<U32>((frame.mData2 >> 32) & 0xFFull);
}
file_stream << time_str << "," << typ << "," << frame.mData1 << "," << bit_idx << "," << err_l << "," << err_h
<< "," << err_o << "," << static_cast<unsigned>(frame.mFlags) << "," << hx << std::endl;
if (UpdateExportProgressAndCheckForCancel(i, num_frames) == true)
{
file_stream.close();
return;
}
}
file_stream.close();
}
void IrFoxAnalyzerResults::GenerateFrameTabularText(U64 frame_index, DisplayBase display_base)
{
#ifdef SUPPORTS_PROTOCOL_SEARCH
(void)display_base;
Frame frame = GetFrame(frame_index);
ClearTabularText();
char buf[64];
snprintf(buf, sizeof buf, "t%u", static_cast<unsigned>(frame.mType));
AddTabularText(buf);
snprintf(buf, sizeof buf, "%llu", (unsigned long long)frame.mData1);
AddTabularText(buf);
#endif
}
void IrFoxAnalyzerResults::GeneratePacketTabularText(U64 packet_id, DisplayBase display_base)
{
(void)packet_id;
(void)display_base;
}
void IrFoxAnalyzerResults::GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base)
{
(void)transaction_id;
(void)display_base;
}

View File

@ -1,27 +0,0 @@
#ifndef IRFOX_ANALYZER_RESULTS
#define IRFOX_ANALYZER_RESULTS
#include <AnalyzerResults.h>
class IrFoxAnalyzer;
class IrFoxAnalyzerSettings;
class IrFoxAnalyzerResults : public AnalyzerResults
{
public:
IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings);
virtual ~IrFoxAnalyzerResults();
virtual void GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base);
virtual void GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id);
virtual void GenerateFrameTabularText(U64 frame_index, DisplayBase display_base);
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
protected:
IrFoxAnalyzerSettings* mSettings;
IrFoxAnalyzer* mAnalyzer;
};
#endif

View File

@ -1,62 +0,0 @@
#include "IrFoxAnalyzerSettings.h"
#include <AnalyzerHelpers.h>
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
: mInputChannel(UNDEFINED_CHANNEL),
mInputChannelInterface()
{
mInputChannelInterface.SetTitleAndTooltip(
"IR",
"Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)");
mInputChannelInterface.SetChannel(mInputChannel);
AddInterface(&mInputChannelInterface);
AddExportOption(0, "Export as text/csv file");
AddExportExtension(0, "text", "txt");
AddExportExtension(0, "csv", "csv");
ClearChannels();
AddChannel(mInputChannel, "IR", false);
}
IrFoxAnalyzerSettings::~IrFoxAnalyzerSettings()
{
}
bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
{
mInputChannel = mInputChannelInterface.GetChannel();
ClearChannels();
AddChannel(mInputChannel, "IR Fox", true);
return true;
}
void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings()
{
mInputChannelInterface.SetChannel(mInputChannel);
}
void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
{
SimpleArchive text_archive;
text_archive.SetString(settings);
text_archive >> mInputChannel;
ClearChannels();
AddChannel(mInputChannel, "IR Fox", true);
UpdateInterfacesFromSettings();
}
const char* IrFoxAnalyzerSettings::SaveSettings()
{
SimpleArchive text_archive;
text_archive << mInputChannel;
return SetReturnString(text_archive.GetString());
}

View File

@ -1,24 +0,0 @@
#ifndef IRFOX_ANALYZER_SETTINGS
#define IRFOX_ANALYZER_SETTINGS
#include <AnalyzerSettings.h>
#include <AnalyzerTypes.h>
class IrFoxAnalyzerSettings : public AnalyzerSettings
{
public:
IrFoxAnalyzerSettings();
virtual ~IrFoxAnalyzerSettings();
virtual bool SetSettingsFromInterfaces();
void UpdateInterfacesFromSettings();
virtual void LoadSettings(const char* settings);
virtual const char* SaveSettings();
Channel mInputChannel;
protected:
AnalyzerSettingInterfaceChannel mInputChannelInterface;
};
#endif

View File

@ -1,593 +0,0 @@
#include "IrFoxDecoder.h"
#include <AnalyzerResults.h>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <cstring>
void IrFoxDecoder::reset()
{
*this = IrFoxDecoder{};
rise_sync_time_us = irfox::kBitTimeUs;
next_control_bit = irfox::kBitPerByte;
have_last_processed = false;
last_processed_edge_us = 0;
}
uint16_t IrFoxDecoder::ceil_div_u16(uint16_t val, uint16_t divider)
{
if (divider == 0)
return 0;
int ret = val / divider;
if ((val << 4) / divider - (ret << 4) >= 8)
ret++;
return static_cast<uint16_t>(ret);
}
uint8_t IrFoxDecoder::crc8(const uint8_t* data, uint8_t start, uint8_t end, uint8_t poly)
{
uint8_t crc = 0xff;
for (size_t i = start; i < end; i++)
{
crc ^= data[i];
for (size_t j = 0; j < 8; j++)
{
if ((crc & 0x80) != 0)
crc = static_cast<uint8_t>((crc << 1) ^ poly);
else
crc <<= 1;
}
}
return crc;
}
bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out)
{
crc_out = 0;
crc_out = static_cast<uint16_t>(static_cast<uint16_t>(crc8(data_buffer, 0, len, irfox::kPoly1) << 8) & 0xFF00u);
crc_out = static_cast<uint16_t>(crc_out | (crc8(data_buffer, 0, static_cast<uint8_t>(len + 1), irfox::kPoly2) & 0xFFu));
const bool ok = (data_buffer[len] == static_cast<uint8_t>((crc_out >> 8) & 0xFF)) &&
(data_buffer[len + 1] == static_cast<uint8_t>(crc_out & 0xFF));
return ok;
}
void IrFoxDecoder::first_rx()
{
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;
is_preamb = true;
is_recive = false;
is_recive_raw = false;
msg_type_receive = 0;
rise_sync_time_us = irfox::kBitTimeUs;
std::memset(data_buffer, 0, sizeof data_buffer);
preamble_bubble_start_valid_ = false;
trim_first_data_bit_cell_ = false;
}
void IrFoxDecoder::listen_start(double t_us)
{
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
// Как IR_DecoderRaw::listenStart: пауза по lastEdgeTime, не по prevRise.
if (is_recive_raw && last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0)
{
is_recive_raw = false;
first_rx();
}
}
void IrFoxDecoder::check_timeout(double t_us)
{
if (!is_recive)
return;
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
if (t_us - last_edge_time_us > irmax * 2.0)
{
// Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM.
is_recive = false;
msg_type_receive = 0;
is_recive_raw = false;
first_rx();
// Не last_edge_time_us = t_us: как IR_DecoderRaw — не расходить с «хвостом» фронтов.
}
}
void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, IrFoxEmitBitMode emit_mode)
{
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;
}
if (is_buffer_overflow || is_preamb || is_wrong_pack)
{
// Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма.
first_rx();
return;
}
if (buf_bit_pos == next_control_bit)
{
next_control_bit = next_control_bit + (is_data ? irfox::kSyncBits : irfox::kBitPerByte);
is_data = !is_data;
i_sync_bit = 0;
err_sync_bit = 0;
}
if (is_data)
{
const bool was_first_data_bit = (i_data_buffer == 0);
data_buffer[i_data_buffer / 8] |= static_cast<uint8_t>(bit ? 1 : 0) << (7 - (i_data_buffer % 8));
i_data_buffer++;
buf_bit_pos++;
uint8_t fl = 0;
if (pack_trace_invert_fix)
fl |= DISPLAY_AS_WARNING_FLAG;
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble)
{
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_DATA_BIT,
bit ? 1u : 0u, static_cast<uint64_t>(i_data_buffer - 1), fl, pack_trace_invert_fix, 0, 0, 0};
fill_err_snapshot(e);
e.bubble_text[0] = static_cast<char>(bit ? '1' : '0');
e.bubble_text[1] = '\0';
on_bit(e);
}
if (was_first_data_bit && trim_first_data_bit_cell_)
trim_first_data_bit_cell_ = false;
}
else
{
if (i_sync_bit == 0)
{
const bool last_data_bit =
(data_buffer[((i_data_buffer - 1) / 8)] >> (7 - ((i_data_buffer - 1) % 8))) & 1;
if (bit != static_cast<bool>(last_data_bit))
{
buf_bit_pos++;
i_sync_bit++;
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble)
{
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_SYNC_BIT,
bit ? 1u : 0u, static_cast<uint64_t>(buf_bit_pos), 0, false, 0, 0, 0};
fill_err_snapshot(e);
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %c", bit ? '1' : '0');
on_bit(e);
}
}
else
{
i_sync_bit = 0;
err_other++;
err_sync_bit++;
const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits);
if (fatal_sync)
is_wrong_pack = true;
if (on_bit && fatal_sync)
{
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_ABORT,
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);
}
}
}
else
{
buf_bit_pos++;
i_sync_bit++;
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble)
{
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_SYNC_BIT,
bit ? 1u : 0u, static_cast<uint64_t>(buf_bit_pos), 0, false, 0, 0, 0};
fill_err_snapshot(e);
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %c", bit ? '1' : '0');
on_bit(e);
}
}
is_wrong_pack = (err_sync_bit >= irfox::kSyncBits);
}
if (!is_available && is_data && !is_wrong_pack)
{
if (i_data_buffer == 8 * irfox::kMsgBytes)
pack_size = static_cast<uint16_t>(data_buffer[0] & 0x1Fu);
if (pack_size && (i_data_buffer == 8))
msg_type_receive = static_cast<uint8_t>((data_buffer[0] >> 5) | 0xF8u);
if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte))
{
uint16_t crc_computed = 0;
const bool crc_ok = crc_check(static_cast<uint8_t>(pack_size - irfox::kCrcBytes), crc_computed);
crc_value = crc_computed;
is_recive = false;
is_recive_raw = false;
msg_type_receive = 0;
is_available = crc_ok;
IrFoxEmitPacket pkt{};
pkt.start_sample = static_cast<int64_t>(cell_start_s);
pkt.end_sample = static_cast<int64_t>(cell_end_s);
pkt.crc_ok = crc_ok;
pkt.pack_size = static_cast<uint8_t>(pack_size);
pkt.err_low = err_low_signal;
pkt.err_high = err_high_signal;
pkt.err_other = err_other;
if (pack_size > 0 && pack_size <= irfox::kDataByteSizeMax)
std::memcpy(pkt.data_bytes, data_buffer, pack_size);
if (on_pkt)
on_pkt(pkt);
}
}
}
void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit,
const IrFoxOnPacket& on_pkt)
{
const double t_us = sample_to_us(sample, fs);
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;
listen_start(t_us);
// Как IR_DecoderRaw: пауза между фронтами по lastEdgeTime при активном приёме кадра.
if (last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0 && is_recive)
check_timeout(t_us);
last_edge_time_us = t_us;
last_edge_sample = sample;
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
/** Визуализация: начало 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)
{
const double delta_rp = t_us - prev_rise_us;
const uint32_t cand_rp = static_cast<uint32_t>(delta_rp);
const uint32_t cand_ht = static_cast<uint32_t>(t_us - prev_fall_us);
const uint32_t cand_lt = static_cast<uint32_t>(prev_fall_us - prev_rise_us);
#if IRFOX_SHORT_LOW_GLITCH_REJECT
const bool short_low_glitch =
is_recive && !is_preamb && cand_ht < (rise_min_us / 8U) && cand_lt >= rise_min_us &&
cand_rp >= rise_min_us && cand_rp <= irmax;
if (short_low_glitch)
{
err_other++;
irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us);
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
#endif
#if IRFOX_MICRO_GAP_RISE_REJECT
const bool micro_gap_cand_lt_ok =
(cand_lt >= rise_min_us) || (cand_lt >= (rise_min_us / 4U) && cand_lt < rise_min_us);
const bool micro_gap_rise = is_recive && !is_preamb && cand_ht < (rise_min_us / 8U) && micro_gap_cand_lt_ok &&
cand_rp >= (rise_min_us / 4U) && cand_rp < rise_min_us && cand_rp <= irmax;
if (micro_gap_rise)
{
err_other++;
irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us);
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
#endif
if (cand_rp <= rise_max_us / 4U && !high_count && !low_count)
{
err_other++;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
// Визуализация PRE: длинная пауза, первый подъём — якорь от спада метки (декод как STM32DMA).
if (cand_rp > irmax * 2U && !is_recive_raw)
{
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, true);
preamble_bubble_start_valid_ = true;
}
const bool accept_rise_timing =
(delta_rp > static_cast<double>(rise_max_us) / 4.0) || high_count != 0 || low_count != 0;
if (accept_rise_timing)
{
rise_period_anchor_sample_ = prev_rise_sample;
rise_period_us = cand_rp;
high_time_us = cand_ht;
low_time_us = cand_lt;
prev_rise_us = t_us;
prev_rise_sample = sample;
}
else
{
err_other++;
}
}
else
{
if (t_us - prev_fall_us > rise_min_us / 4.0)
{
prev_fall_us = t_us;
prev_fall_sample = sample;
}
else
{
err_other++;
}
}
// Как 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: до текущего фронта (sample1), чтобы охватить все 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)
{
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
if (rising)
{
high_count = low_count = all_count = 0;
bool invert_err = false;
uint64_t cell_start_s = rise_period_anchor_sample_;
const uint64_t cell_end_s = sample > 0 ? sample - 1 : sample;
// После prev_rise += half period якорь может оказаться близко к текущему подъёму;
// max(anchor, prev_fall) > cell_end даёт пустой интервал — бабл первого бита пропадает.
if (trim_first_data_bit_cell_ && is_data && i_data_buffer == 0)
{
const uint64_t trimmed = std::max(cell_start_s, prev_fall_sample);
if (trimmed <= cell_end_s)
cell_start_s = trimmed;
}
if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us))
{
if (high_time_us > low_time_us)
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
else
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
}
else
{
uint16_t hc = ceil_div_u16(static_cast<uint16_t>(high_time_us > 0xFFFF ? 0xFFFF : high_time_us),
static_cast<uint16_t>(rise_sync_time_us));
uint16_t lc = ceil_div_u16(static_cast<uint16_t>(low_time_us > 0xFFFF ? 0xFFFF : low_time_us),
static_cast<uint16_t>(rise_sync_time_us));
uint16_t ac = ceil_div_u16(static_cast<uint16_t>(rise_period_us > 0xFFFF ? 0xFFFF : rise_period_us),
static_cast<uint16_t>(rise_sync_time_us));
high_count = static_cast<int8_t>(hc > 127 ? 127 : hc);
low_count = static_cast<int8_t>(lc > 127 ? 127 : lc);
all_count = static_cast<int8_t>(ac > 127 ? 127 : ac);
if (high_count == 0 && high_time_us > rise_sync_time_us / 3U)
{
high_count++;
err_other++;
}
if (low_count + high_count > all_count)
{
if (low_count > high_count)
{
low_count = static_cast<int8_t>(all_count - high_count);
err_low_signal = static_cast<uint8_t>(err_low_signal + static_cast<uint8_t>(low_count));
}
else if (low_count < high_count)
{
high_count = static_cast<int8_t>(all_count - low_count);
err_high_signal = static_cast<uint8_t>(err_high_signal + static_cast<uint8_t>(high_count));
}
else if (low_count == high_count)
{
invert_err = true;
err_other = static_cast<uint8_t>(err_other + static_cast<uint8_t>(all_count));
}
}
if (low_count < high_count)
err_high_signal = static_cast<uint8_t>(err_high_signal + static_cast<uint8_t>(high_count));
else
err_low_signal = static_cast<uint8_t>(err_low_signal + static_cast<uint8_t>(low_count));
const bool burst_is_data_start = is_data;
const uint64_t merge_bit_index =
burst_is_data_start ? static_cast<uint64_t>(i_data_buffer) : static_cast<uint64_t>(buf_bit_pos + 1);
char s_bits[20]{};
char d_bits[20]{};
size_t s_n = 0;
size_t d_n = 0;
int first_merge_bit = -1;
bool merge_warn = false;
auto append_merge = [&](bool as_data, bool bitv) {
if (first_merge_bit < 0)
first_merge_bit = bitv ? 1 : 0;
char* buf = as_data ? d_bits : s_bits;
size_t& n = as_data ? d_n : s_n;
if (n + 1 < sizeof s_bits)
buf[n++] = static_cast<char>(bitv ? '1' : '0');
};
auto emit_merge_if_needed = [&]() {
if ((s_n == 0 && d_n == 0) || !on_bit)
return;
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 = (d_n > 0) ? IRF_FT_DATA_BIT : IRF_FT_SYNC_BIT;
e.bit_value = (first_merge_bit > 0) ? 1u : 0u;
e.bit_index = merge_bit_index;
e.mflags = merge_warn ? DISPLAY_AS_WARNING_FLAG : 0;
e.invert_fix = merge_warn;
fill_err_snapshot(e);
s_bits[s_n] = '\0';
d_bits[d_n] = '\0';
if (s_n && d_n)
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %s d: %s", s_bits, d_bits);
else if (s_n)
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %s", s_bits);
else
std::memcpy(e.bubble_text, d_bits, d_n + 1);
on_bit(e);
};
for (int8_t i = 0; i < low_count && 8 - i; i++)
{
const bool row_is_data = is_data;
if (i == low_count - 1 && invert_err)
{
invert_err = false;
write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
merge_warn = true;
append_merge(row_is_data, true);
}
else
{
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
append_merge(row_is_data, false);
}
}
for (int8_t i = 0; i < high_count && 8 - i; i++)
{
const bool row_is_data = is_data;
if (i == high_count - 1 && invert_err)
{
invert_err = false;
write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
merge_warn = true;
append_merge(row_is_data, false);
}
else
{
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
append_merge(row_is_data, true);
}
}
emit_merge_if_needed();
}
}
last_processed_edge_us = t_us;
have_last_processed = true;
}
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt)
{
const double t_us = sample_to_us(last_sample, fs);
listen_start(t_us);
check_timeout(t_us);
(void)on_bit;
(void)on_pkt;
}

View File

@ -1,135 +0,0 @@
#pragma once
#include "IrFoxProtocolConstants.h"
#include <cstdint>
#include <functional>
enum IrFoxFrameType : uint8_t
{
IRF_FT_DATA_BIT = 1,
IRF_FT_SYNC_BIT = 2,
IRF_FT_PACKET_OK = 3,
IRF_FT_PACKET_CRC_FAIL = 4,
IRF_FT_OVERFLOW = 5,
IRF_FT_ABORT = 6,
IRF_FT_PREAMBLE = 7,
};
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
enum class IrFoxEmitBitMode : uint8_t
{
WithBubble,
Quiet,
};
struct IrFoxEmitBit
{
int64_t start_sample;
int64_t end_sample;
uint8_t frame_type;
uint64_t bit_value;
uint64_t bit_index;
uint8_t mflags;
bool invert_fix;
uint8_t err_low;
uint8_t err_high;
uint8_t err_other;
/** Подпись бабла: "0", "1" или склейка "001"; пусто — смотреть bit_value. */
char bubble_text[32]{};
};
struct IrFoxEmitPacket
{
int64_t start_sample;
int64_t end_sample;
bool crc_ok;
uint8_t pack_size;
uint8_t err_low;
uint8_t err_high;
uint8_t err_other;
uint8_t data_bytes[irfox::kDataByteSizeMax];
};
using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>;
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
class IrFoxDecoder
{
public:
void reset();
void processEdge(uint64_t sample, bool rising, 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);
private:
static uint16_t ceil_div_u16(uint16_t val, uint16_t divider);
static uint8_t crc8(const uint8_t* data, uint8_t start, uint8_t end, uint8_t poly);
bool crc_check(uint8_t len, uint16_t& crc_out);
void first_rx();
void listen_start(double t_us);
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,
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble);
double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); }
// --- state (mirror IR_DecoderRaw) ---
uint8_t data_buffer[irfox::kDataByteSizeMax]{};
uint8_t err_low_signal = 0;
uint8_t err_high_signal = 0;
uint8_t err_other = 0;
bool is_available = false;
uint16_t pack_size = 0;
uint16_t crc_value = 0;
bool is_recive = false;
bool is_recive_raw = false;
bool is_preamb = false;
bool is_buffer_overflow = false;
bool is_wrong_pack = false;
uint32_t rise_sync_time_us = irfox::kBitTimeUs;
double prev_rise_us = 0;
double prev_fall_us = 0;
uint64_t prev_rise_sample = 0;
uint64_t prev_fall_sample = 0;
/** Сэмпл предыдущего нарастающего фронта до обновления на текущем тике (граница ячейки бита, см. IR_DecoderRaw::tick). */
uint64_t rise_period_anchor_sample_ = 0;
uint64_t preamble_bubble_start_sample_ = 0;
bool preamble_bubble_start_valid_ = false;
bool trim_first_data_bit_cell_ = false;
double last_edge_time_us = 0;
uint64_t last_edge_sample = 0;
double last_processed_edge_us = 0;
bool have_last_processed = false;
void fill_err_snapshot(IrFoxEmitBit& e) const
{
e.err_low = err_low_signal;
e.err_high = err_high_signal;
e.err_other = err_other;
}
uint32_t rise_period_us = 0;
uint32_t high_time_us = 0;
uint32_t low_time_us = 0;
int8_t high_count = 0;
int8_t low_count = 0;
int8_t all_count = 0;
uint16_t wrong_counter = 0;
int8_t preamb_front_counter = 0;
int16_t buf_bit_pos = 0;
bool is_data = true;
uint16_t i_data_buffer = 0;
uint16_t next_control_bit = irfox::kBitPerByte;
uint8_t i_sync_bit = 0;
uint8_t err_sync_bit = 0;
uint16_t error_counter = 0;
uint8_t msg_type_receive = 0;
};

View File

@ -1,71 +0,0 @@
#pragma once
#include <cstdint>
namespace irfox {
constexpr uint32_t kCarrierPeriodUs = 1000000U / 38000U;
constexpr uint32_t kBitActiveTakts = 25U;
constexpr uint32_t kBitPauseTakts = 12U;
constexpr uint32_t kBitTakts = kBitActiveTakts + kBitPauseTakts;
constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs;
constexpr uint32_t kToleranceUs = 300U;
/** Мин. длительность плато (мкс) для потокового анти-глитча в анализаторе; согласовано с IR_INPUT_MIN_PULSE_US. */
constexpr uint32_t kMinFilteredPulseUs = 10U;
constexpr uint8_t kBitPerByte = 8U;
constexpr uint8_t kMsgBytes = 1;
constexpr uint8_t kAddrBytes = 2;
constexpr uint8_t kCrcBytes = 2;
constexpr uint8_t kPoly1 = 0x31;
constexpr uint8_t kPoly2 = 0x8C;
constexpr uint8_t kSyncBits = 3U;
constexpr uint8_t kBytePerPack = 31;
constexpr uint8_t kDataByteSizeMax =
static_cast<uint8_t>(kMsgBytes + kAddrBytes + kAddrBytes + kBytePerPack + kCrcBytes);
constexpr uint8_t kPreambPulse = 3;
constexpr uint8_t kPreambFronts = kPreambPulse * 2U;
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
#define IRFOX_SHORT_LOW_GLITCH_REJECT 1
#endif
#ifndef IRFOX_GLITCH_REJECT_PHASE_NUDGE
#define IRFOX_GLITCH_REJECT_PHASE_NUDGE 1
#endif
#ifndef IRFOX_MICRO_GAP_RISE_REJECT
#define IRFOX_MICRO_GAP_RISE_REJECT 1
#endif
inline uint32_t irTimeoutUs(uint32_t riseSyncTimeUs)
{
const uint32_t riseMax = riseSyncTimeUs + kToleranceUs;
return riseMax * (8U + kSyncBits + 1U);
}
/** Как IR_DecoderRaw.h: aroundRise(t) → riseTimeMin < t && t < riseTimeMax (ветка STM32DMA). */
inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs)
{
const uint32_t lo = riseSyncTimeUs > kToleranceUs ? riseSyncTimeUs - kToleranceUs : 0U;
const uint32_t hi = riseSyncTimeUs + kToleranceUs;
return lo < periodUs && periodUs < hi;
}
inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us)
{
#if IRFOX_GLITCH_REJECT_PHASE_NUDGE
if (!(edge_us > static_cast<double>(rise_sync_us)))
return;
const double nudged = edge_us - static_cast<double>(rise_sync_us);
if (nudged > prev_rise_us && nudged < edge_us)
prev_rise_us = nudged;
#else
(void)edge_us;
(void)rise_sync_us;
(void)prev_rise_us;
#endif
}
} // namespace irfox

View File

@ -1,67 +0,0 @@
#include "IrFoxSimulationDataGenerator.h"
#include "IrFoxAnalyzerSettings.h"
#include <AnalyzerHelpers.h>
IrFoxSimulationDataGenerator::IrFoxSimulationDataGenerator()
{
}
IrFoxSimulationDataGenerator::~IrFoxSimulationDataGenerator()
{
}
void IrFoxSimulationDataGenerator::Initialize(U32 simulation_sample_rate, IrFoxAnalyzerSettings* settings)
{
mSimulationSampleRateHz = simulation_sample_rate;
mSettings = settings;
mIrSimulationData.SetChannel(mSettings->mInputChannel);
mIrSimulationData.SetSampleRate(simulation_sample_rate);
// Как у «покоя» на выходе TSOP: линия подтянута вверх
mIrSimulationData.SetInitialBitState(BIT_HIGH);
}
void IrFoxSimulationDataGenerator::EmitIdle(U32 samples)
{
mIrSimulationData.Advance(samples);
}
void IrFoxSimulationDataGenerator::EmitLow(U32 samples)
{
mIrSimulationData.TransitionIfNeeded(BIT_LOW);
mIrSimulationData.Advance(samples);
}
void IrFoxSimulationDataGenerator::EmitHigh(U32 samples)
{
mIrSimulationData.TransitionIfNeeded(BIT_HIGH);
mIrSimulationData.Advance(samples);
}
U32 IrFoxSimulationDataGenerator::GenerateSimulationData(U64 largest_sample_requested, U32 sample_rate,
SimulationChannelDescriptor** simulation_channel)
{
const U64 adjusted_largest_sample_requested =
AnalyzerHelpers::AdjustSimulationTargetSample(largest_sample_requested, sample_rate, mSimulationSampleRateHz);
// Упрощённый «пакет»: несколько импульсов вниз (активный уровень приёмника).
while (mIrSimulationData.GetCurrentSampleNumber() < adjusted_largest_sample_requested)
{
const U32 us_to_samples = mSimulationSampleRateHz / 1000000;
if (us_to_samples == 0)
break;
EmitIdle(500 * us_to_samples);
EmitLow(4500 * us_to_samples);
EmitHigh(4500 * us_to_samples);
EmitLow(4500 * us_to_samples);
EmitHigh(4500 * us_to_samples);
EmitLow(560 * us_to_samples);
EmitHigh(560 * us_to_samples);
EmitLow(560 * us_to_samples);
EmitHigh(20000 * us_to_samples);
}
*simulation_channel = &mIrSimulationData;
return 1;
}

View File

@ -1,27 +0,0 @@
#ifndef IRFOX_SIMULATION_DATA_GENERATOR
#define IRFOX_SIMULATION_DATA_GENERATOR
#include <SimulationChannelDescriptor.h>
class IrFoxAnalyzerSettings;
class IrFoxSimulationDataGenerator
{
public:
IrFoxSimulationDataGenerator();
~IrFoxSimulationDataGenerator();
void Initialize(U32 simulation_sample_rate, IrFoxAnalyzerSettings* settings);
U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, SimulationChannelDescriptor** simulation_channel);
protected:
IrFoxAnalyzerSettings* mSettings;
U32 mSimulationSampleRateHz;
SimulationChannelDescriptor mIrSimulationData;
void EmitIdle(U32 samples);
void EmitLow(U32 samples);
void EmitHigh(U32 samples);
};
#endif

View File

@ -1,52 +0,0 @@
name: Build
on:
push:
branches: [master, main]
tags:
- "*"
pull_request:
branches: [master, main]
jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: |
cmake -B ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build -S ${{github.workspace}}/Analyzer/raw/PulseLengthStat -A x64
cmake --build ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build --config Release
- uses: actions/upload-artifact@v4
with:
name: windows
path: ${{github.workspace}}/Analyzer/raw/dll/*.dll
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: |
cmake -B ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build -S ${{github.workspace}}/Analyzer/raw/PulseLengthStat -DCMAKE_BUILD_TYPE=Release
cmake --build ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build
- uses: actions/upload-artifact@v4
with:
name: macos
path: ${{github.workspace}}/Analyzer/raw/dll/*.so
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: |
cmake -B ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build -S ${{github.workspace}}/Analyzer/raw/PulseLengthStat -DCMAKE_BUILD_TYPE=Release
cmake --build ${{github.workspace}}/Analyzer/raw/PulseLengthStat/build
env:
CC: gcc-10
CXX: g++-10
- uses: actions/upload-artifact@v4
with:
name: linux
path: ${{github.workspace}}/Analyzer/raw/dll/*.so

View File

@ -1,3 +0,0 @@
/build
/build-nmake
.DS_Store

View File

@ -1,24 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(PulseLengthStatAnalyzer)
add_definitions(-DLOGIC2)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
include(ExternalAnalyzerSDK)
set(SOURCES
src/PulseLengthStatAnalyzer.cpp
src/PulseLengthStatAnalyzer.h
src/PulseLengthStatAnalyzerResults.cpp
src/PulseLengthStatAnalyzerResults.h
src/PulseLengthStatAnalyzerSettings.cpp
src/PulseLengthStatAnalyzerSettings.h
src/PulseLengthStatSimulationDataGenerator.cpp
src/PulseLengthStatSimulationDataGenerator.h
)
add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES})

View File

@ -1,49 +0,0 @@
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 19,
"patch": 0
},
"configurePresets": [
{
"name": "win-vs2022-x64",
"displayName": "Visual Studio 2022 (x64)",
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"binaryDir": "${sourceDir}/build"
},
{
"name": "win-vs2019-x64",
"displayName": "Visual Studio 2019 (x64)",
"generator": "Visual Studio 16 2019",
"architecture": "x64",
"binaryDir": "${sourceDir}/build"
},
{
"name": "win-nmake-release",
"displayName": "NMake Release (x64 Native Tools Command Prompt)",
"generator": "NMake Makefiles",
"binaryDir": "${sourceDir}/build-nmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "win-release",
"configurePreset": "win-vs2022-x64",
"configuration": "Release"
},
{
"name": "win-release-vs2019",
"configurePreset": "win-vs2019-x64",
"configuration": "Release"
},
{
"name": "nmake-release",
"configurePreset": "win-nmake-release"
}
]
}

View File

@ -1,21 +0,0 @@
@echo off
setlocal EnableDelayedExpansion
cd /d "%~dp0"
if not exist build\CMakeCache.txt (
echo Run configure_msvc.bat first.
exit /b 1
)
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i"
if not defined VSINSTALL (
echo MSVC not found. Add C++ workload in Visual Studio Installer.
exit /b 1
)
call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64
if errorlevel 1 exit /b 1
cmake --build build
pause
exit /b %ERRORLEVEL%

View File

@ -1,66 +0,0 @@
include(FetchContent)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
if(NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY OR NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/)
endif()
if(NOT TARGET Saleae::AnalyzerSDK)
FetchContent_Declare(
analyzersdk
GIT_REPOSITORY https://github.com/saleae/AnalyzerSDK.git
GIT_TAG master
GIT_SHALLOW True
GIT_PROGRESS True
)
FetchContent_GetProperties(analyzersdk)
if(NOT analyzersdk_POPULATED)
FetchContent_Populate(analyzersdk)
include(${analyzersdk_SOURCE_DIR}/AnalyzerSDKConfig.cmake)
if(APPLE OR WIN32)
get_target_property(analyzersdk_lib_location Saleae::AnalyzerSDK IMPORTED_LOCATION)
if(CMAKE_LIBRARY_OUTPUT_DIRECTORY)
file(COPY ${analyzersdk_lib_location} DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
else()
message(WARNING "Please define CMAKE_RUNTIME_OUTPUT_DIRECTORY and CMAKE_LIBRARY_OUTPUT_DIRECTORY if you want unit tests to locate ${analyzersdk_lib_location}")
endif()
endif()
endif()
endif()
# Shared folder for all Saleae LLA plugins in this repo: Analyzer/raw/dll
set(ANALYZER_DLL_OUT_DIR "${CMAKE_SOURCE_DIR}/../dll")
get_filename_component(ANALYZER_DLL_OUT_DIR "${ANALYZER_DLL_OUT_DIR}" ABSOLUTE)
file(MAKE_DIRECTORY "${ANALYZER_DLL_OUT_DIR}")
function(add_analyzer_plugin TARGET)
set(options)
set(single_value_args)
set(multi_value_args SOURCES)
cmake_parse_arguments(_p "${options}" "${single_value_args}" "${multi_value_args}" ${ARGN})
add_library(${TARGET} MODULE ${_p_SOURCES})
target_link_libraries(${TARGET} PRIVATE Saleae::AnalyzerSDK)
set(ANALYZER_DESTINATION "Analyzers")
install(TARGETS ${TARGET} RUNTIME DESTINATION ${ANALYZER_DESTINATION}
LIBRARY DESTINATION ${ANALYZER_DESTINATION})
set_target_properties(${TARGET} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}"
LIBRARY_OUTPUT_DIRECTORY "${ANALYZER_DLL_OUT_DIR}")
if(CMAKE_CONFIGURATION_TYPES)
foreach(CFG ${CMAKE_CONFIGURATION_TYPES})
string(TOUPPER ${CFG} CFG_UPPER)
set_target_properties(${TARGET} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}"
LIBRARY_OUTPUT_DIRECTORY_${CFG_UPPER} "${ANALYZER_DLL_OUT_DIR}")
endforeach()
endif()
endfunction()

View File

@ -1,39 +0,0 @@
@echo off
setlocal EnableDelayedExpansion
cd /d "%~dp0"
echo === PulseLengthStatAnalyzer: configure with MSVC ===
echo.
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" (
echo [ERROR] vswhere not found. Install one of:
echo - Visual Studio 2022 with workload "Desktop development with C++"
echo - Build Tools for Visual Studio 2022: https://visualstudio.microsoft.com/visual-cpp-build-tools/
echo ^(select "Desktop development with C++" / MSVC, Windows SDK^)
exit /b 1
)
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i"
if not defined VSINSTALL (
echo [ERROR] MSVC toolset not found. Add "Desktop development with C++" in Visual Studio Installer.
exit /b 1
)
echo Found: !VSINSTALL!
call "!VSINSTALL!\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64
if errorlevel 1 exit /b 1
if exist build rmdir /s /q build
if exist build-nmake rmdir /s /q build-nmake
mkdir build
cd build
cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
if errorlevel 1 exit /b 1
echo.
echo Configure OK. Build: build_msvc.bat ^(or from same VS env: cd build ^& cmake --build .^)
echo Output DLL: ..\dll\ ^(all analyzers share this folder^)
pause
exit /b 0

View File

@ -1,110 +0,0 @@
#include "PulseLengthStatAnalyzer.h"
#include "PulseLengthStatAnalyzerSettings.h"
#include <AnalyzerChannelData.h>
// One frame per stable level between edges. mData1 = duration in samples; mFlags: 1 = HIGH, 0 = LOW.
PulseLengthStatAnalyzer::PulseLengthStatAnalyzer()
: Analyzer2(),
mSettings(),
mSimulationInitilized(false)
{
SetAnalyzerSettings(&mSettings);
}
PulseLengthStatAnalyzer::~PulseLengthStatAnalyzer()
{
KillThread();
}
void PulseLengthStatAnalyzer::SetupResults()
{
mResults.reset(new PulseLengthStatAnalyzerResults(this, &mSettings));
SetAnalyzerResults(mResults.get());
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
}
void PulseLengthStatAnalyzer::WorkerThread()
{
mChannelData = GetAnalyzerChannelData(mSettings.mInputChannel);
U32 frames_since_commit = 0;
const U32 kCommitBatch = 256;
for (;;)
{
CheckIfThreadShouldExit();
const U64 segment_start = mChannelData->GetSampleNumber();
const BitState level = mChannelData->GetBitState();
mChannelData->AdvanceToNextEdge();
const U64 edge_sample = mChannelData->GetSampleNumber();
if (edge_sample == segment_start)
break;
const U64 duration_samples = edge_sample - segment_start;
const U64 end_inclusive = edge_sample > segment_start ? edge_sample - 1 : segment_start;
Frame frame;
frame.mData1 = duration_samples;
frame.mFlags = (level == BIT_HIGH) ? 1 : 0;
frame.mStartingSampleInclusive = static_cast<S64>(segment_start);
frame.mEndingSampleInclusive = static_cast<S64>(end_inclusive);
mResults->AddFrame(frame);
if (++frames_since_commit >= kCommitBatch)
{
mResults->CommitResults();
frames_since_commit = 0;
}
ReportProgress(edge_sample);
}
if (frames_since_commit != 0)
mResults->CommitResults();
}
bool PulseLengthStatAnalyzer::NeedsRerun()
{
return false;
}
U32 PulseLengthStatAnalyzer::GenerateSimulationData(U64 minimum_sample_index, U32 device_sample_rate,
SimulationChannelDescriptor** simulation_channels)
{
if (mSimulationInitilized == false)
{
mSimulationDataGenerator.Initialize(GetSimulationSampleRate(), &mSettings);
mSimulationInitilized = true;
}
return mSimulationDataGenerator.GenerateSimulationData(minimum_sample_index, device_sample_rate,
simulation_channels);
}
U32 PulseLengthStatAnalyzer::GetMinimumSampleRateHz()
{
return 200000;
}
const char* PulseLengthStatAnalyzer::GetAnalyzerName() const
{
return "Pulse Length Stat";
}
const char* GetAnalyzerName()
{
return "Pulse Length Stat";
}
Analyzer* CreateAnalyzer()
{
return new PulseLengthStatAnalyzer();
}
void DestroyAnalyzer(Analyzer* analyzer)
{
delete analyzer;
}

View File

@ -1,39 +0,0 @@
#ifndef PULSELENGTHSTAT_ANALYZER_H
#define PULSELENGTHSTAT_ANALYZER_H
#include <Analyzer.h>
#include "PulseLengthStatAnalyzerSettings.h"
#include "PulseLengthStatAnalyzerResults.h"
#include "PulseLengthStatSimulationDataGenerator.h"
#include <memory>
class ANALYZER_EXPORT PulseLengthStatAnalyzer : public Analyzer2
{
public:
PulseLengthStatAnalyzer();
virtual ~PulseLengthStatAnalyzer();
virtual void SetupResults();
virtual void WorkerThread();
virtual U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate,
SimulationChannelDescriptor** simulation_channels);
virtual U32 GetMinimumSampleRateHz();
virtual const char* GetAnalyzerName() const;
virtual bool NeedsRerun();
protected:
PulseLengthStatAnalyzerSettings mSettings;
std::unique_ptr<PulseLengthStatAnalyzerResults> mResults;
AnalyzerChannelData* mChannelData;
PulseLengthStatSimulationDataGenerator mSimulationDataGenerator;
bool mSimulationInitilized;
};
extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName();
extern "C" ANALYZER_EXPORT Analyzer* __cdecl CreateAnalyzer();
extern "C" ANALYZER_EXPORT void __cdecl DestroyAnalyzer(Analyzer* analyzer);
#endif

View File

@ -1,107 +0,0 @@
#include "PulseLengthStatAnalyzerResults.h"
#include <AnalyzerHelpers.h>
#include "PulseLengthStatAnalyzer.h"
#include "PulseLengthStatAnalyzerSettings.h"
#include <cstdio>
#include <fstream>
PulseLengthStatAnalyzerResults::PulseLengthStatAnalyzerResults(PulseLengthStatAnalyzer* analyzer,
PulseLengthStatAnalyzerSettings* settings)
: AnalyzerResults(),
mSettings(settings),
mAnalyzer(analyzer)
{
}
PulseLengthStatAnalyzerResults::~PulseLengthStatAnalyzerResults()
{
}
static void FormatDurationUs(U64 duration_samples, U32 sample_rate_hz, char* out, size_t out_sz)
{
if (sample_rate_hz == 0)
{
snprintf(out, out_sz, "? us");
return;
}
const double us = double(duration_samples) * 1e6 / double(sample_rate_hz);
snprintf(out, out_sz, "%.2f us", us);
}
void PulseLengthStatAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
{
(void)display_base;
ClearResultStrings();
Frame frame = GetFrame(frame_index);
const U32 fs = mAnalyzer->GetSampleRate();
char dur[64];
FormatDurationUs(frame.mData1, fs, dur, sizeof dur);
const char* lev = (frame.mFlags != 0) ? "HIGH" : "LOW";
char line[160];
snprintf(line, sizeof line, "%s %s", lev, dur);
AddResultString(line);
}
void PulseLengthStatAnalyzerResults::GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id)
{
(void)export_type_user_id;
(void)display_base;
std::ofstream file_stream(file, std::ios::out);
const U64 trigger_sample = mAnalyzer->GetTriggerSample();
const U32 sample_rate = mAnalyzer->GetSampleRate();
file_stream << "Time [s],Level,Duration_samples,Duration_us" << std::endl;
const U64 num_frames = GetNumFrames();
for (U32 i = 0; i < num_frames; i++)
{
Frame frame = GetFrame(i);
char time_str[128];
AnalyzerHelpers::GetTimeString(frame.mStartingSampleInclusive, trigger_sample, sample_rate, time_str, 128);
char dur_us[64];
FormatDurationUs(frame.mData1, sample_rate, dur_us, sizeof dur_us);
file_stream << time_str << "," << ((frame.mFlags != 0) ? "HIGH" : "LOW") << "," << frame.mData1 << ","
<< dur_us << std::endl;
if (UpdateExportProgressAndCheckForCancel(i, num_frames) == true)
{
file_stream.close();
return;
}
}
file_stream.close();
}
void PulseLengthStatAnalyzerResults::GenerateFrameTabularText(U64 frame_index, DisplayBase display_base)
{
#ifdef SUPPORTS_PROTOCOL_SEARCH
(void)display_base;
Frame frame = GetFrame(frame_index);
ClearTabularText();
const U32 fs = mAnalyzer->GetSampleRate();
char dur[64];
FormatDurationUs(frame.mData1, fs, dur, sizeof dur);
AddTabularText((frame.mFlags != 0) ? "H" : "L");
AddTabularText(dur);
#endif
}
void PulseLengthStatAnalyzerResults::GeneratePacketTabularText(U64 packet_id, DisplayBase display_base)
{
(void)packet_id;
(void)display_base;
}
void PulseLengthStatAnalyzerResults::GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base)
{
(void)transaction_id;
(void)display_base;
}

View File

@ -1,27 +0,0 @@
#ifndef PULSELENGTHSTAT_ANALYZER_RESULTS
#define PULSELENGTHSTAT_ANALYZER_RESULTS
#include <AnalyzerResults.h>
class PulseLengthStatAnalyzer;
class PulseLengthStatAnalyzerSettings;
class PulseLengthStatAnalyzerResults : public AnalyzerResults
{
public:
PulseLengthStatAnalyzerResults(PulseLengthStatAnalyzer* analyzer, PulseLengthStatAnalyzerSettings* settings);
virtual ~PulseLengthStatAnalyzerResults();
virtual void GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base);
virtual void GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id);
virtual void GenerateFrameTabularText(U64 frame_index, DisplayBase display_base);
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
protected:
PulseLengthStatAnalyzerSettings* mSettings;
PulseLengthStatAnalyzer* mAnalyzer;
};
#endif

View File

@ -1,62 +0,0 @@
#include "PulseLengthStatAnalyzerSettings.h"
#include <AnalyzerHelpers.h>
PulseLengthStatAnalyzerSettings::PulseLengthStatAnalyzerSettings()
: mInputChannel(UNDEFINED_CHANNEL),
mInputChannelInterface()
{
mInputChannelInterface.SetTitleAndTooltip(
"Input",
"Digital channel: one frame per stable level between edges (duration in samples / us).");
mInputChannelInterface.SetChannel(mInputChannel);
AddInterface(&mInputChannelInterface);
AddExportOption(0, "Export as text/csv file");
AddExportExtension(0, "text", "txt");
AddExportExtension(0, "csv", "csv");
ClearChannels();
AddChannel(mInputChannel, "Input", false);
}
PulseLengthStatAnalyzerSettings::~PulseLengthStatAnalyzerSettings()
{
}
bool PulseLengthStatAnalyzerSettings::SetSettingsFromInterfaces()
{
mInputChannel = mInputChannelInterface.GetChannel();
ClearChannels();
AddChannel(mInputChannel, "Pulse Length Stat", true);
return true;
}
void PulseLengthStatAnalyzerSettings::UpdateInterfacesFromSettings()
{
mInputChannelInterface.SetChannel(mInputChannel);
}
void PulseLengthStatAnalyzerSettings::LoadSettings(const char* settings)
{
SimpleArchive text_archive;
text_archive.SetString(settings);
text_archive >> mInputChannel;
ClearChannels();
AddChannel(mInputChannel, "Pulse Length Stat", true);
UpdateInterfacesFromSettings();
}
const char* PulseLengthStatAnalyzerSettings::SaveSettings()
{
SimpleArchive text_archive;
text_archive << mInputChannel;
return SetReturnString(text_archive.GetString());
}

View File

@ -1,24 +0,0 @@
#ifndef PULSELENGTHSTAT_ANALYZER_SETTINGS
#define PULSELENGTHSTAT_ANALYZER_SETTINGS
#include <AnalyzerSettings.h>
#include <AnalyzerTypes.h>
class PulseLengthStatAnalyzerSettings : public AnalyzerSettings
{
public:
PulseLengthStatAnalyzerSettings();
virtual ~PulseLengthStatAnalyzerSettings();
virtual bool SetSettingsFromInterfaces();
void UpdateInterfacesFromSettings();
virtual void LoadSettings(const char* settings);
virtual const char* SaveSettings();
Channel mInputChannel;
protected:
AnalyzerSettingInterfaceChannel mInputChannelInterface;
};
#endif

View File

@ -1,65 +0,0 @@
#include "PulseLengthStatSimulationDataGenerator.h"
#include "PulseLengthStatAnalyzerSettings.h"
#include <AnalyzerHelpers.h>
PulseLengthStatSimulationDataGenerator::PulseLengthStatSimulationDataGenerator()
{
}
PulseLengthStatSimulationDataGenerator::~PulseLengthStatSimulationDataGenerator()
{
}
void PulseLengthStatSimulationDataGenerator::Initialize(U32 simulation_sample_rate, PulseLengthStatAnalyzerSettings* settings)
{
mSimulationSampleRateHz = simulation_sample_rate;
mSettings = settings;
mSimChannel.SetChannel(mSettings->mInputChannel);
mSimChannel.SetSampleRate(simulation_sample_rate);
mSimChannel.SetInitialBitState(BIT_HIGH);
}
void PulseLengthStatSimulationDataGenerator::EmitIdle(U32 samples)
{
mSimChannel.Advance(samples);
}
void PulseLengthStatSimulationDataGenerator::EmitLow(U32 samples)
{
mSimChannel.TransitionIfNeeded(BIT_LOW);
mSimChannel.Advance(samples);
}
void PulseLengthStatSimulationDataGenerator::EmitHigh(U32 samples)
{
mSimChannel.TransitionIfNeeded(BIT_HIGH);
mSimChannel.Advance(samples);
}
U32 PulseLengthStatSimulationDataGenerator::GenerateSimulationData(U64 largest_sample_requested, U32 sample_rate,
SimulationChannelDescriptor** simulation_channel)
{
const U64 adjusted_largest_sample_requested =
AnalyzerHelpers::AdjustSimulationTargetSample(largest_sample_requested, sample_rate, mSimulationSampleRateHz);
while (mSimChannel.GetCurrentSampleNumber() < adjusted_largest_sample_requested)
{
const U32 us_to_samples = mSimulationSampleRateHz / 1000000;
if (us_to_samples == 0)
break;
EmitIdle(500 * us_to_samples);
EmitLow(4500 * us_to_samples);
EmitHigh(4500 * us_to_samples);
EmitLow(4500 * us_to_samples);
EmitHigh(4500 * us_to_samples);
EmitLow(560 * us_to_samples);
EmitHigh(560 * us_to_samples);
EmitLow(560 * us_to_samples);
EmitHigh(20000 * us_to_samples);
}
*simulation_channel = &mSimChannel;
return 1;
}

View File

@ -1,27 +0,0 @@
#ifndef PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR
#define PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR
#include <SimulationChannelDescriptor.h>
class PulseLengthStatAnalyzerSettings;
class PulseLengthStatSimulationDataGenerator
{
public:
PulseLengthStatSimulationDataGenerator();
~PulseLengthStatSimulationDataGenerator();
void Initialize(U32 simulation_sample_rate, PulseLengthStatAnalyzerSettings* settings);
U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, SimulationChannelDescriptor** simulation_channel);
protected:
PulseLengthStatAnalyzerSettings* mSettings;
U32 mSimulationSampleRateHz;
SimulationChannelDescriptor mSimChannel;
void EmitIdle(U32 samples);
void EmitLow(U32 samples);
void EmitHigh(U32 samples);
};
#endif

View File

@ -2,26 +2,16 @@
#include "IR_Encoder.h" #include "IR_Encoder.h"
#include "TimerStatic.h" #include "TimerStatic.h"
#include "MemoryCheck.h" #include "MemoryCheck.h"
#include "CarCmdList.h"
/////////////// Pinout /////////////// /////////////// Pinout ///////////////
#define dec0_PIN PIN_KT1_IN #define encForward_PIN 0
#define dec1_PIN PIN_KT2_IN #define encBackward_PIN 5
#define dec2_PIN PIN_KT3_IN
#define dec3_PIN PIN_KT4_IN
#define dec4_PIN PIN_KT5_IN
#define dec5_PIN PIN_KT6_IN
#define dec6_PIN PIN_KT7_IN
#define dec7_PIN PIN_KT8_IN
// #define dec8_PIN PB8
// #define dec9_PIN PB9
// #define dec10_PIN PB10
// #define dec11_PIN PB11
// #define dec12_PIN PB12
// #define dec13_PIN PB13
// #define dec14_PIN PB14
// #define dec15_PIN PB15
#define LoopOut PC13 #define LoopOut 12
#define ISR_Out 10
#define TestOut 13
//////////////// Ini ///////////////// //////////////// Ini /////////////////
@ -49,13 +39,35 @@ void EncoderISR()
IR_Encoder::isr(); IR_Encoder::isr();
} }
//------------------------------------------------------------------- void decBackwardISR() {
decBackward.isr();
}
IR_Decoder dec0(dec0_PIN, 0); static uint8_t* portOut;
void dec_0_ISR() { dec0.isr(); } ISR(TIMER2_COMPA_vect) {
encForward.isr();
// encBackward.isr();
// encTree.isr();
//TODO: Сделать выбор порта
*portOut = (*portOut & 0b11111110) |
(
encForward.ir_out_virtual << 0U
// | encBackward.ir_out_virtual << 6U
// | encTree.ir_out_virtual << 2U
);
}
/////////////////////////////////////////////////////////////////////
uint8_t data0 [] = { };
uint8_t data1 [] = { 42 };
uint8_t data2 [] = { 42 , 127 };
uint8_t data3 [] = { 42 , 127, 137 };
uint8_t data4 [] = { 42 , 127, 137, 255 };
IR_Decoder dec1(dec1_PIN, 1); uint32_t loopTimer;
void dec_1_ISR() { dec1.isr(); } uint8_t sig = 255;
uint16_t targetAddr = IR_Broadcast;
Timer t1(750, millis, []() {
IR_Decoder dec2(dec2_PIN, 2); IR_Decoder dec2(dec2_PIN, 2);
void dec_2_ISR() { dec2.isr(); } void dec_2_ISR() { dec2.isr(); }
@ -81,8 +93,19 @@ void dec_7_ISR() { dec7.isr(); }
// IR_Decoder dec9(dec9_PIN, 9); // IR_Decoder dec9(dec9_PIN, 9);
// void dec_9_ISR() { dec9.isr(); } // void dec_9_ISR() { dec9.isr(); }
// IR_Decoder dec10(dec10_PIN, 10); default:
// void dec_10_ISR() { dec10.isr(); } break;
}
// encBackward.sendData(IR_Broadcast, data2);
// encTree.sendData(IR_Broadcast, rawData3);
});
Timer t2(500, millis, []() {
digitalToggle(13);
});
/////////////////////////////////////////////////////////////////////
void setup() {
IR_Encoder::timerSetup();
portOut = &PORTB;
// IR_Decoder dec11(dec11_PIN, 11); // IR_Decoder dec11(dec11_PIN, 11);
// void dec_11_ISR() { dec11.isr(); } // void dec_11_ISR() { dec11.isr(); }
@ -100,9 +123,14 @@ uint8_t data2[] = {42, 127};
uint8_t data3[] = {42, 127, 137}; uint8_t data3[] = {42, 127, 137};
uint8_t data4[] = {42, 127, 137, 255}; uint8_t data4[] = {42, 127, 137, 255};
uint32_t loopTimer; pinMode(8, OUTPUT);
uint8_t sig = 0; pinMode(9, OUTPUT);
uint16_t targetAddr = IR_Broadcast; pinMode(11, OUTPUT);
pinMode(13, OUTPUT);
pinMode(encForward_PIN, OUTPUT);
pinMode(encBackward_PIN, OUTPUT);
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
Timer t1(500, millis, []() Timer t1(500, millis, []()
{ {
@ -217,61 +245,30 @@ void setup()
pinMode(LoopOut, OUTPUT); pinMode(LoopOut, OUTPUT);
pinMode(dec0_PIN, INPUT_PULLUP); attachInterrupt(0, decForwardISR, CHANGE); // D2
pinMode(dec1_PIN, INPUT_PULLUP); attachInterrupt(1, decBackwardISR, CHANGE); // D3
pinMode(dec2_PIN, INPUT_PULLUP);
pinMode(dec3_PIN, INPUT_PULLUP);
pinMode(dec4_PIN, INPUT_PULLUP);
pinMode(dec5_PIN, INPUT_PULLUP);
pinMode(dec6_PIN, INPUT_PULLUP);
pinMode(dec7_PIN, INPUT_PULLUP);
// pinMode(dec8_PIN, INPUT_PULLUP);
// pinMode(dec9_PIN, INPUT_PULLUP);
// pinMode(dec10_PIN, INPUT_PULLUP);
// pinMode(dec11_PIN, INPUT_PULLUP);
// pinMode(dec12_PIN, INPUT_PULLUP);
// pinMode(dec13_PIN, INPUT_PULLUP);
attachInterrupt(dec0_PIN, dec_0_ISR, CHANGE);
attachInterrupt(dec1_PIN, dec_1_ISR, CHANGE);
attachInterrupt(dec2_PIN, dec_2_ISR, CHANGE);
attachInterrupt(dec3_PIN, dec_3_ISR, CHANGE);
attachInterrupt(dec4_PIN, dec_4_ISR, CHANGE);
attachInterrupt(dec5_PIN, dec_5_ISR, CHANGE);
attachInterrupt(dec6_PIN, dec_6_ISR, CHANGE);
attachInterrupt(dec7_PIN, dec_7_ISR, CHANGE);
// attachInterrupt(dec8_PIN, dec_8_ISR, CHANGE);
// attachInterrupt(dec9_PIN, dec_9_ISR, CHANGE);
// attachInterrupt(dec10_PIN, dec_10_ISR, CHANGE);
// attachInterrupt(dec11_PIN, dec_11_ISR, CHANGE);
// attachInterrupt(dec12_PIN, dec_12_ISR, CHANGE);
// attachInterrupt(dec13_PIN, dec_13_ISR, CHANGE);
} }
void loop()
{ bool testLed = false;
digitalToggle(LoopOut); uint32_t testLed_timer;
void loop() {
// digitalToggle(LoopOut);
Timer::tick(); Timer::tick();
IR_Decoder::tick();
bool rx_flag = false; decForward.tick();
rx_flag |= status(dec0); decBackward.tick();
rx_flag |= status(dec1);
rx_flag |= status(dec2);
rx_flag |= status(dec3);
rx_flag |= status(dec4);
rx_flag |= status(dec5);
rx_flag |= status(dec6);
rx_flag |= status(dec7);
// rx_flag |= status(dec8);
// rx_flag |= status(dec9);
// rx_flag |= status(dec10);
// rx_flag |= status(dec11);
// rx_flag |= status(dec12);
// rx_flag |= status(dec13);
if (Serial.available()) status(decForward);
{ status(decBackward);
// Serial.println(micros() - loopTimer);
// loopTimer = micros();
// delayMicroseconds(120*5);
if (Serial.available()) {
uint8_t in = Serial.parseInt(); uint8_t in = Serial.parseInt();
switch (in) switch (in)
{ {
@ -289,6 +286,13 @@ void loop()
break; break;
} }
} }
if(testLed && millis() - testLed_timer > 100){
testLed=false;
digitalWrite(12, LOW);
}
} }
@ -316,13 +320,19 @@ void detectSignal()
// { digitalWrite(SignalDetectLed, LOW); }); // { digitalWrite(SignalDetectLed, LOW); });
} }
// test
bool status(IR_Decoder &dec)
{ //test
if (dec.gotData.available()) void status(IR_Decoder& dec) {
{ if (dec.gotData.available() && dec.gotData.getAddrFrom() != 42) {
detectSignal();
// Serial.println(micros()); digitalWrite(12, HIGH);
testLed = true;
testLed_timer = millis();
encForward.sendData(IR_Broadcast, CarCmd::stop); //<<<<<<<<<<<<<<<<<<<<<<<<<<< CMD IS HERE
String str; String str;
if (/* dec.gotData.getDataPrt()[1] */ 1) if (/* dec.gotData.getDataPrt()[1] */ 1)
{ {

View File

@ -1,141 +0,0 @@
# IR DMA vs ISR: анализ согласованности сигнала и ответа версии
Связка с остальным пультом (модули, настройки): **[`ARCHITECTURE.md`](ARCHITECTURE.md)**.
Документ фиксирует наблюдения по переходу машинки (проект Car) на **DMA-передачу** ИК через `IR_Encoder::setExternalTxBackend` и `IrDmaBackend`, сравнение со **старым путём** (таймер + **`_isr()`**), ручную проверку CRC по логу пульта и роль **`buildGateRuns`** в библиотеке **IR-protocol**.
---
## 1. Контекст
- До введения DMA передача шла через **`IR_Encoder::begin(..., IR_Encoder::isr)`**: на каждый тик таймера (`carrierFrec * 2`) вызывается **`_isr()`**, формируются преамбула, данные, синхробиты.
- После коммита с **IR_DMA** (`Car`, `IR.cpp`): **`beginClockOnly`**, **`setExternalTxBackend`**, фактическая модуляция — **`IrDmaBackend::start`** → **`IR_Encoder::buildGateRuns`** + DMA в **BSRR**.
- Ответ версии — один из самых **длинных** кадров (до **31 байта** полного кадра по заголовку). Короткие пакеты (эхо `Version_Query`, 8 байт) в логе остаются **FrameOK**; длинный ответ версии даёт **CRC fail** / `Frame reject`.
---
## 2. Два пути: ISR и DMA
| Этап | Старый ISR | DMA |
|------|------------|-----|
| Байты пакета + CRC | `sendDataFULL``sendBuffer` | То же; в `buildGateRuns``memcpy` в локальный буфер размером `dataByteSizeMax` |
| Развёртка в импульсы | **`_isr()`**: счётчик `toggleCounter`, ветки preamb / data / sync | **`buildGateRuns`**: RLE-сегменты `(gate, lenTicks)`**`nextWord()`** по тикам таймера |
| Останов передачи | `signal == noSignal`, `isSending = false` | `ticksOutput >= totalTicks`, `sum(runs[i].lenTicks)` |
Идея `buildGateRuns`: **эмулировать** шаги FSM, которые в ISR выполняются при **`toggleCounter == 0`** (см. комментарий в `IR_Encoder.cpp` рядом с внутренним `while`).
### 2.1. Приоритеты NVIC: приём ИК выше, чем DMA передачи (STM32)
Пока активна **внешняя** передача по DMA (`IrDmaBackend` и т.п.), таймер крутит поток запросов к DMA — срабатывают **`DMA1_Channelx_IRQn`** (половина/конец буфера и т.д.). Если их приоритет **выше**, чем у **EXTI** линии пина приёмника, обработка фронтов на входе ИК **откладывается** → растёт джиттер `micros()` и страдает заполнение `subBuffer` / журнал `@IRF1v1`, хотя алгоритм `tick`/`writeToBuffer` не менялся.
**Требование:** числовой приоритет **приёма (EXTI)** должен быть **выше приоритета DMA передачи** (в терминах Cortex-M / STM32 HAL: **меньше** значение preempt priority у EXTI, чем у канала DMA ИК).
**В репозитории:**
- **`IR_Decoder`**: библиотека **не** задаёт приоритет EXTI по умолчанию. На Arduino STM32 пользователь вызывает **`setReceiveExtiPreemptPriority(preempt)`** (до или после `enable()`); после `attachInterrupt` применяется поверх приоритета ядра. Семейства с укороченной картой EXTI (C0/F0/G0/L0) — без изменения NVIC из этой функции.
- **DMA ИК-TX** (например **`Car/src/IR/IrDmaBackend.cpp`**): preempt задаётся в прошивке носителя (**`CarIrq::kIrTxDmaPreempt`** и т.д.) и должен быть **больше** (ниже срочность), чем у приёма.
Свой проект: пользователь обязан согласовать приоритеты; **ни один** канал DMA ИК-TX не должен вытеснять EXTI приёма (меньший preempt у DMA = ошибка).
---
## 3. Ключевое наблюдение: `runLenTicks = toggleCounter + 1`
В **`IR_Encoder::buildGateRuns`** на каждой итерации внешнего цикла:
```cpp
const uint16_t runLenTicks = (uint16_t)toggleCounterLocal + 1U;
```
В **`_isr()`** при стартовом **`toggleCounter == N`** выполняется **ровно N** раз ветка `if (toggleCounter) { toggleCounter--; }` подряд, пока счётчик не станет **0**; **следующий** тик попадает в `else` и делает один шаг `switch (signal)`.
Между двумя такими визитами в `else` проходит **N тиков таймера**, не **N+1**.
В `buildGateRuns` для того же начального `toggleCounterLocal` в run записывается **`N + 1` тик**. Это даёт **систематическое удлинение каждого сегмента на 1 тик** относительно модели «счётчик убывает N раз до нуля».
**Следствие:**
- `totalTicks = Σ lenTicks` в **`IrDmaBackend::startStream`** **больше**, чем число тиков, которое дал бы чистый ISR при том же пакете.
- Число внешних итераций `buildGateRuns` (шагов FSM) совпадает с числом таких сегментов; приближённо:
`totalTicks ≈ totalTicks_ISR + (число_внешнихагов)`.
Короткий кадр: ошибка может «теряться» в допусках приёмника. Длинный (версия) — **накопление** ошибки по времени → сдвиг границ битов → **неверные байты**, в том числе **CRC**.
---
## 4. Ручная проверка CRC по логу (пульт)
Алгоритм: **`IR_FOX::crc8`** (`IR_config.cpp`), два байта как в **`sendDataFULL`**:
- первый байт CRC = `crc8(data, 0, packSize - 2, poly1)`;
- второй = `crc8(data, 0, packSize - 1, poly2)` (в расчёт второго входит уже первый байт CRC).
Пример **31-байтного** кадра из лога `Frame reject`:
- Тело **0…28** (29 байт).
- Байты **29…30** — CRC на проводе.
Для фиксированного дампа байтов **0…28** корректная пара CRC по формуле библиотеки — **`6E 54`**, в логе на проводе — **`96 62`** → **не совпадает**; приёмник обоснованно отклоняет кадр.
Это **не** объясняется разницей AVR vs STM32: счёт идёт по массиву `uint8_t` побайтно.
Эхо **8 байт** `C8 FA 2A FD E8 5D AA B4`: пересчёт даёт **`AA B4`** — совпадает с последними байтами кадра → для этого пакета цепочка **байт → CRC** согласована.
---
## 5. Скрипт симуляции
В репозитории: **`docs/scripts/ir_protocol_gate_runs_sim.py`**.
Запуск:
```bash
python docs/scripts/ir_protocol_gate_runs_sim.py
```
Скрипт:
1. Считает **CRC** для примеров пакетов (8 байт эха и 31 байт из reject).
2. Воспроизводит логику **`buildGateRuns`** (с дополнением буфера до `dataByteSizeMax`, как в C++).
3. Печатает **`totalTicks`**, число **внешних шагов** FSM и связь **`totalTicks - outer_steps`** как оценку «тиков в модели ISR без +1 на каждый шаг».
Пример вывода (значения могут слегка отличаться при смене констант в `IR_config.h`):
- `preambToggle = 97`
- для 8-байт пакета: сотни шагов FSM, `totalTicks` порядка тысяч тиков
- для 31-байт: больше шагов и `totalTicks` (~25k+ тиков для текущих констант)
---
## 6. Связь с проектами
- **Car** (`Executer.cpp`): ответ версии через **`IR_Module::getENC().sendData(...)`** — тот же **`sendDataFULL`**, затем **`rawSend`** → DMA.
- **ControlPointUnion** (`CustomCmd.h`, слоты): запрос версии через **`sendResp`** с **`version_query`** — задержка **`IR_ResponseDelay`**, затем **`sendData`** на адрес машинки.
- **ControlPointUnion** (`Plan_B.ino`): разбор **`version_response`** из **`gotData` / `gotBackData`** только после **успешного CRC** в декодере.
---
## 7. Выводы
1. **Байты в RAM** на передаче формируются корректно библиотекой; проблема «после DMA» укладывается в **расхождение тайминговой развёртки** (`buildGateRuns` + DMA) со **старой** развёрткой ISR, а не в «другой CRC на машинке» при неизменённой библиотеке.
2. **Подозрение №1:** `runLenTicks = toggleCounter + 1` в **`buildGateRuns`** не совпадает с числом тиков ISR между шагами FSM (**`N`** vs **`N+1`**). Требуется сверка с эталонной трассой ISR или логическим анализатором.
3. **Проверка на будущее:** сравнить побитово выходы ISR и DMA на **одном** буфере (8 и 31 байт); при необходимости поправить формулу длины run в **`IR-protocol`** и пересобрать Car и пульт.
4. При **DMA-режиме передачи** на STM32 соблюдать **приоритеты NVIC** (раздел **2.1**): приём EXTI **выше**, чем DMA ИК-TX.
---
## 8. Ссылки на файлы
| Файл | Назначение |
|------|------------|
| `Documents/Arduino/libraries/IR-protocol/IR_Encoder.cpp` | `buildGateRuns`, `_isr`, `rawSend` |
| `Documents/Arduino/libraries/IR-protocol/IR_Decoder.cpp` | `setReceiveExtiPreemptPriority` / `enable`: опциональный `NVIC_SetPriority` для EXTI (Arduino STM32) |
| `Documents/Arduino/libraries/IR-protocol/IR_config.cpp` | `crc8` |
| `Car/src/IR/IR.cpp` | `setExternalTxBackend`, `txStart` |
| `Car/src/IR/IrDmaBackend.cpp` | `startStream`, `totalTicks`, `nextWord`, NVIC DMA из `CarIrq` |
| `Car/src/IR/IR.cpp` | `setReceiveExtiPreemptPriority` + `enable` декодера |
| `ControlPointUnion/Plan_B/TestPoints/CustomCmd.h` | `sendResp` / `version_query` для тестовых слотов |
---
*Документ составлен по обсуждению в чате; при смене версии IR-protocol числа констант и `totalTicks` пересчитывайте скриптом.*

View File

@ -321,7 +321,10 @@ IR_SendResult IR_Encoder::sendData(uint16_t addrTo, uint8_t *data, uint8_t len,
return sendDataFULL(id, addrTo, data, len, needAccept); return sendDataFULL(id, addrTo, data, len, needAccept);
} }
IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data, uint8_t len, bool needAccept) void IR_Encoder::sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false){
sendData(id, addrTo, data, len, needAccept);
}
void IR_Encoder::sendData(uint16_t addrFrom, uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false)
{ {
if (len > bytePerPack) if (len > bytePerPack)
{ {

View File

@ -20,7 +20,8 @@ class IR_Encoder : public IR_FOX
static IR_Encoder *last; static IR_Encoder *last;
IR_Encoder *next; IR_Encoder *next;
public: public:
static HardwareTimer* IR_Timer; private:
// uint16_t id; /// @brief Адрес передатчика
struct IR_TxGateRun { struct IR_TxGateRun {
uint16_t lenTicks; // number of timer ticks at carrierFrec*2 uint16_t lenTicks; // number of timer ticks at carrierFrec*2
@ -60,10 +61,9 @@ public:
void setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count); void setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count);
void rawSend(uint8_t *ptr, uint8_t len); void rawSend(uint8_t *ptr, uint8_t len);
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false); void 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); void sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
IR_SendResult sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false); void sendData(uint16_t addrFrom, uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
IR_SendResult sendAccept(uint16_t addrTo, uint8_t customByte = 0); IR_SendResult sendAccept(uint16_t addrTo, uint8_t customByte = 0);
IR_SendResult sendRequest(uint16_t addrTo); IR_SendResult sendRequest(uint16_t addrTo);

View File

@ -1,174 +0,0 @@
#!/usr/bin/env python3
"""
Симуляция логики IR_Encoder::buildGateRuns (IR-protocol) и утилиты CRC8.
Запуск из корня репозитория: python docs/scripts/ir_protocol_gate_runs_sim.py
Или: python ir_protocol_gate_runs_sim.py из каталога scripts/
"""
from __future__ import annotations
import sys
# --- IR_config.h (фрагмент) ---
bitPauseTakts = 12
bitActiveTakts = 25
preambPulse = 3
syncBits = 3
bitPerByte = 8
preambToggle = ((bitPauseTakts * 2 + bitActiveTakts) * 2 - 1)
bitHigh = [(bitPauseTakts) * 2 - 1, (bitActiveTakts) * 2 - 1]
bitLow = [(bitPauseTakts // 2 + bitActiveTakts) * 2 - 1, (bitPauseTakts) - 1]
preamb, data, sync, noSignal = 0, 1, 2, 3
HIGH = True
def crc8(data: bytes, start: int, end: int, poly: int) -> int:
"""Как IR_FOX::crc8 в IR_config.cpp: [start, end)."""
crc = 0xFF
for i in range(start, end):
crc ^= data[i]
for _ in range(8):
if (crc & 0x80) != 0:
crc = ((crc << 1) ^ poly) & 0xFF
else:
crc = (crc << 1) & 0xFF
return crc
def crc_pair_over_wire(packet: bytes) -> tuple[int, int]:
"""Два байта CRC как в IR_Encoder::sendDataFULL (poly1 старший, poly2 младший)."""
ps = len(packet)
if ps < 2:
return 0, 0
b1 = crc8(packet, 0, ps - 2, 0x31) & 0xFF
b2 = crc8(packet, 0, ps - 1, 0x8C) & 0xFF
return b1, b2
# Как dataByteSizeMax в IR_config.h (msg+addr+addr+bytePerPack+crc)
DATA_BYTE_SIZE_MAX = 1 + 2 + 2 + 31 + 2
def build_gate_runs(packet: bytes):
"""
Повторяет IR_Encoder::buildGateRuns: список (gate: bool, lenTicks: int), сумма lenTicks = totalTicks DMA.
Буфер дополняется нулями до dataByteSizeMax, как sendBufferLocal[dataByteSizeMax] в C++.
"""
send_len = len(packet)
send_buf = bytearray(packet) + bytes(max(0, DATA_BYTE_SIZE_MAX - len(packet)))
toggle = preambToggle
data_bit = bitPerByte - 1
data_byte = 0
preamb_front = preambPulse * 2 - 1
data_seq = bitPerByte * 2
sync_seq = syncBits * 2
sync_last = False
sig = preamb
state = HIGH
cur_seq = bitHigh
runs: list[tuple[bool, int]] = []
outer_steps = 0
while True:
outer_steps += 1
gate = state
run_len = toggle + 1 # как в C++: (uint16_t)toggleCounterLocal + 1U
if runs and runs[-1][0] == gate:
g, ln = runs[-1]
runs[-1] = (g, ln + run_len)
else:
runs.append((gate, run_len))
while True:
if sig == noSignal:
return runs, outer_steps
if sig == preamb:
if preamb_front:
preamb_front -= 1
toggle = preambToggle
break
sig = data
state = not False
continue
if sig == data:
if data_seq:
if not (data_seq & 1):
cur_seq = bitHigh if ((send_buf[data_byte] >> data_bit) & 1) else bitLow
data_bit -= 1
toggle = cur_seq[not state]
data_seq -= 1
break
sync_last = send_buf[data_byte] & 1
data_byte += 1
data_bit = bitPerByte - 1
data_seq = bitPerByte * 2
sig = sync
continue
if sig == sync:
if sync_seq:
if not (sync_seq & 1):
if sync_seq == 2:
cur_seq = bitLow if (send_buf[data_byte] & 0x80) else bitHigh
else:
cur_seq = bitLow if sync_last else bitHigh
sync_last = not sync_last
toggle = cur_seq[not state]
sync_seq -= 1
break
sig = data
sync_seq = syncBits * 2
if data_byte >= send_len:
sig = noSignal
continue
return [], 0
state = not state
def main() -> int:
print("IR-protocol: preambToggle =", preambToggle)
print()
# Пример из лога: 8-байтный эхо-пакет Version_Query (CRC OK на приёме)
echo = bytes.fromhex("C8 FA 2A FD E8 5D AA B4")
c1, c2 = crc_pair_over_wire(echo)
print("8 байт (эхо): CRC вычисленный:", f"{c1:02X}", f"{c2:02X}", "| на проводе:", f"{echo[6]:02X}", f"{echo[7]:02X}")
runs8, steps8 = build_gate_runs(echo)
total8 = sum(r[1] for r in runs8)
print(" buildGateRuns: внешних шагов FSM =", steps8, ", totalTicks =", total8, ", число run-сегментов =", len(runs8))
print()
# 31 байт из лога Frame reject (пример)
reject = bytes.fromhex(
"DF 00 00 FA 2A 5E 43 61 72 5F 76 34 2E 33 2E 38 5F 5B 31 32 4D 68 7A 5D 6B ED 1D 9A 53 96 62"
)
if len(reject) == 31:
c1, c2 = crc_pair_over_wire(reject)
print("31 байт (reject): CRC по телу 0..28 должен быть:", f"{c1:02X}", f"{c2:02X}", "| байты [29:31]:", f"{reject[29]:02X}", f"{reject[30]:02X}")
print(" Совпадение с формулой:", c1 == reject[29] and c2 == reject[30])
runs31, steps31 = build_gate_runs(reject)
total31 = sum(r[1] for r in runs31)
print(" buildGateRuns: внешних шагов =", steps31, ", totalTicks =", total31, ", run-сегментов =", len(runs31))
print()
# Связь totalTicks с моделью «N тиков на сегмент до шага FSM»
# total_build = sum(toggle_i + 1); если бы было sum(toggle_i), разница = steps
theoretical_isr_ticks = total31 - steps31
print("Для 31-байт пакета: totalTicks (buildGateRuns) =", total31)
print(" Если каждый внешний шаг даёт +1 к длине сегмента относительно ISR (runLen = toggle+1 vs toggle),")
print(" оценка «ISR-тиков» как totalTicks - outer_steps =", theoretical_isr_ticks)
return 0
if __name__ == "__main__":
sys.exit(main())