From b1d7016147435c4c91aceccbe5a7038e8bbdbc5e Mon Sep 17 00:00:00 2001 From: DashyFox Date: Tue, 2 Jun 2026 11:19:32 +0300 Subject: [PATCH] analyzer --- .gitignore | 7 + Analyzer/raw/IR_Fox/.gitignore | 2 + Analyzer/raw/IR_Fox/CMakeLists.txt | 26 + Analyzer/raw/IR_Fox/CMakePresets.json | 49 ++ Analyzer/raw/IR_Fox/build_msvc.bat | 21 + .../IR_Fox/cmake/ExternalAnalyzerSDK.cmake | 66 ++ Analyzer/raw/IR_Fox/configure_msvc.bat | 39 ++ Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp | 280 +++++++++ Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h | 49 ++ .../raw/IR_Fox/src/IrFoxAnalyzerResults.cpp | 175 ++++++ .../raw/IR_Fox/src/IrFoxAnalyzerResults.h | 27 + .../raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp | 62 ++ .../raw/IR_Fox/src/IrFoxAnalyzerSettings.h | 24 + Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp | 593 ++++++++++++++++++ Analyzer/raw/IR_Fox/src/IrFoxDecoder.h | 135 ++++ .../raw/IR_Fox/src/IrFoxProtocolConstants.h | 71 +++ .../src/IrFoxSimulationDataGenerator.cpp | 67 ++ .../IR_Fox/src/IrFoxSimulationDataGenerator.h | 27 + .../.github/workflows/build.yml | 52 ++ Analyzer/raw/PulseLengthStat/.gitignore | 3 + Analyzer/raw/PulseLengthStat/CMakeLists.txt | 24 + .../raw/PulseLengthStat/CMakePresets.json | 49 ++ Analyzer/raw/PulseLengthStat/build_msvc.bat | 21 + .../cmake/ExternalAnalyzerSDK.cmake | 66 ++ .../raw/PulseLengthStat/configure_msvc.bat | 39 ++ .../src/PulseLengthStatAnalyzer.cpp | 110 ++++ .../src/PulseLengthStatAnalyzer.h | 39 ++ .../src/PulseLengthStatAnalyzerResults.cpp | 107 ++++ .../src/PulseLengthStatAnalyzerResults.h | 27 + .../src/PulseLengthStatAnalyzerSettings.cpp | 62 ++ .../src/PulseLengthStatAnalyzerSettings.h | 24 + ...PulseLengthStatSimulationDataGenerator.cpp | 65 ++ .../PulseLengthStatSimulationDataGenerator.h | 27 + Analyzer/raw/dll/.gitkeep | 0 IR_DMA_ISR_signal_analysis.md | 141 +++++ ir_protocol_gate_runs_sim.py | 174 +++++ 36 files changed, 2750 insertions(+) create mode 100644 Analyzer/raw/IR_Fox/.gitignore create mode 100644 Analyzer/raw/IR_Fox/CMakeLists.txt create mode 100644 Analyzer/raw/IR_Fox/CMakePresets.json create mode 100644 Analyzer/raw/IR_Fox/build_msvc.bat create mode 100644 Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake create mode 100644 Analyzer/raw/IR_Fox/configure_msvc.bat create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxDecoder.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp create mode 100644 Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h create mode 100644 Analyzer/raw/PulseLengthStat/.github/workflows/build.yml create mode 100644 Analyzer/raw/PulseLengthStat/.gitignore create mode 100644 Analyzer/raw/PulseLengthStat/CMakeLists.txt create mode 100644 Analyzer/raw/PulseLengthStat/CMakePresets.json create mode 100644 Analyzer/raw/PulseLengthStat/build_msvc.bat create mode 100644 Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake create mode 100644 Analyzer/raw/PulseLengthStat/configure_msvc.bat create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp create mode 100644 Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h create mode 100644 Analyzer/raw/dll/.gitkeep create mode 100644 IR_DMA_ISR_signal_analysis.md create mode 100644 ir_protocol_gate_runs_sim.py diff --git a/.gitignore b/.gitignore index 9403dff..9663919 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,10 @@ bin/* !.vscode/launch.json log/* /.vscode +*.zip +**/__pycache__ +Analyzer/raw/dll/*.dll +Analyzer/raw/dll/*.so +Analyzer/raw/dll/*.dylib +/Analyzer/raw/IR_Fox/.github +**/.build diff --git a/Analyzer/raw/IR_Fox/.gitignore b/Analyzer/raw/IR_Fox/.gitignore new file mode 100644 index 0000000..7981c18 --- /dev/null +++ b/Analyzer/raw/IR_Fox/.gitignore @@ -0,0 +1,2 @@ +/build +.DS_Store diff --git a/Analyzer/raw/IR_Fox/CMakeLists.txt b/Analyzer/raw/IR_Fox/CMakeLists.txt new file mode 100644 index 0000000..6a7f575 --- /dev/null +++ b/Analyzer/raw/IR_Fox/CMakeLists.txt @@ -0,0 +1,26 @@ +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}) diff --git a/Analyzer/raw/IR_Fox/CMakePresets.json b/Analyzer/raw/IR_Fox/CMakePresets.json new file mode 100644 index 0000000..3636fb2 --- /dev/null +++ b/Analyzer/raw/IR_Fox/CMakePresets.json @@ -0,0 +1,49 @@ +{ + "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" + } + ] +} diff --git a/Analyzer/raw/IR_Fox/build_msvc.bat b/Analyzer/raw/IR_Fox/build_msvc.bat new file mode 100644 index 0000000..9bad4a4 --- /dev/null +++ b/Analyzer/raw/IR_Fox/build_msvc.bat @@ -0,0 +1,21 @@ +@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% diff --git a/Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake b/Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake new file mode 100644 index 0000000..10ca7ba --- /dev/null +++ b/Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake @@ -0,0 +1,66 @@ +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() diff --git a/Analyzer/raw/IR_Fox/configure_msvc.bat b/Analyzer/raw/IR_Fox/configure_msvc.bat new file mode 100644 index 0000000..d3ed219 --- /dev/null +++ b/Analyzer/raw/IR_Fox/configure_msvc.bat @@ -0,0 +1,39 @@ +@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 diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp new file mode 100644 index 0000000..e4cc182 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp @@ -0,0 +1,280 @@ +#include "IrFoxAnalyzer.h" +#include "IrFoxAnalyzerSettings.h" +#include "IrFoxDecoder.h" +#include +#include +#include +#include +#include +#include +#include + +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(1ULL, static_cast((static_cast(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast(fs) + 0.5)); + struct RawEdge + { + U64 sample; + bool rising; + }; + std::vector 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(i), + pending.begin() + static_cast(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(e.start_sample); + frame.mEndingSampleInclusive = static_cast(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(p.start_sample); + frame.mEndingSampleInclusive = static_cast(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(p.pack_size)); + fv2.AddInteger("err_low", static_cast(p.err_low)); + fv2.AddInteger("err_high", static_cast(p.err_high)); + fv2.AddInteger("err_other", static_cast(p.err_other)); + fv2.AddByteArray("data", p.data_bytes, p.pack_size); + mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast(p.start_sample), + static_cast(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; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h new file mode 100644 index 0000000..dbf3167 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h @@ -0,0 +1,49 @@ +#ifndef IRFOX_ANALYZER_H +#define IRFOX_ANALYZER_H + +#include +#include "IrFoxAnalyzerSettings.h" +#include "IrFoxAnalyzerResults.h" +#include "IrFoxSimulationDataGenerator.h" +#include +#include +#include + +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 mResults; + AnalyzerChannelData* mIr; + + IrFoxSimulationDataGenerator mSimulationDataGenerator; + bool mSimulationInitilized; + + std::unordered_map m_packet_hex_by_frame; + std::unordered_map 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 diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp new file mode 100644 index 0000000..0499232 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp @@ -0,0 +1,175 @@ +#include "IrFoxAnalyzerResults.h" +#include +#include +#include "IrFoxAnalyzer.h" +#include "IrFoxAnalyzerSettings.h" +#include "IrFoxDecoder.h" +#include +#include + +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(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((frame.mData2 >> 16) & 0xFFull); + err_h = static_cast((frame.mData2 >> 24) & 0xFFull); + err_o = static_cast((frame.mData2 >> 32) & 0xFFull); + } + + file_stream << time_str << "," << typ << "," << frame.mData1 << "," << bit_idx << "," << err_l << "," << err_h + << "," << err_o << "," << static_cast(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(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; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h new file mode 100644 index 0000000..4007fcc --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h @@ -0,0 +1,27 @@ +#ifndef IRFOX_ANALYZER_RESULTS +#define IRFOX_ANALYZER_RESULTS + +#include + +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 diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp new file mode 100644 index 0000000..c7b0163 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp @@ -0,0 +1,62 @@ +#include "IrFoxAnalyzerSettings.h" +#include + +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()); +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h new file mode 100644 index 0000000..694239a --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h @@ -0,0 +1,24 @@ +#ifndef IRFOX_ANALYZER_SETTINGS +#define IRFOX_ANALYZER_SETTINGS + +#include +#include + +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 diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp new file mode 100644 index 0000000..de25571 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp @@ -0,0 +1,593 @@ +#include "IrFoxDecoder.h" +#include +#include +#include +#include +#include + +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(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((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(static_cast(crc8(data_buffer, 0, len, irfox::kPoly1) << 8) & 0xFF00u); + crc_out = static_cast(crc_out | (crc8(data_buffer, 0, static_cast(len + 1), irfox::kPoly2) & 0xFFu)); + + const bool ok = (data_buffer[len] == static_cast((crc_out >> 8) & 0xFF)) && + (data_buffer[len + 1] == static_cast(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(cell_start_s); + e.end_sample = static_cast(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(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(cell_start_s), static_cast(cell_end_s), IRF_FT_DATA_BIT, + bit ? 1u : 0u, static_cast(i_data_buffer - 1), fl, pack_trace_invert_fix, 0, 0, 0}; + fill_err_snapshot(e); + e.bubble_text[0] = static_cast(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(last_data_bit)) + { + buf_bit_pos++; + i_sync_bit++; + if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble) + { + IrFoxEmitBit e{static_cast(cell_start_s), static_cast(cell_end_s), IRF_FT_SYNC_BIT, + bit ? 1u : 0u, static_cast(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(cell_start_s), static_cast(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(cell_start_s), static_cast(cell_end_s), IRF_FT_SYNC_BIT, + bit ? 1u : 0u, static_cast(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(data_buffer[0] & 0x1Fu); + + if (pack_size && (i_data_buffer == 8)) + msg_type_receive = static_cast((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(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(cell_start_s); + pkt.end_sample = static_cast(cell_end_s); + pkt.crc_ok = crc_ok; + pkt.pack_size = static_cast(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(delta_rp); + const uint32_t cand_ht = static_cast(t_us - prev_fall_us); + const uint32_t cand_lt = static_cast(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(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(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(rise_period_us); + const uint64_t half_s = static_cast(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(preamble_bubble_start_sample_); + int64_t pe_end = static_cast(preamble_bubble_end_sample); + if (preamble_bubble_end_sample == 0 || pe_end < pe_start) + pe_end = static_cast(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(high_time_us > 0xFFFF ? 0xFFFF : high_time_us), + static_cast(rise_sync_time_us)); + uint16_t lc = ceil_div_u16(static_cast(low_time_us > 0xFFFF ? 0xFFFF : low_time_us), + static_cast(rise_sync_time_us)); + uint16_t ac = ceil_div_u16(static_cast(rise_period_us > 0xFFFF ? 0xFFFF : rise_period_us), + static_cast(rise_sync_time_us)); + high_count = static_cast(hc > 127 ? 127 : hc); + low_count = static_cast(lc > 127 ? 127 : lc); + all_count = static_cast(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(all_count - high_count); + err_low_signal = static_cast(err_low_signal + static_cast(low_count)); + } + else if (low_count < high_count) + { + high_count = static_cast(all_count - low_count); + err_high_signal = static_cast(err_high_signal + static_cast(high_count)); + } + else if (low_count == high_count) + { + invert_err = true; + err_other = static_cast(err_other + static_cast(all_count)); + } + } + + if (low_count < high_count) + err_high_signal = static_cast(err_high_signal + static_cast(high_count)); + else + err_low_signal = static_cast(err_low_signal + static_cast(low_count)); + + const bool burst_is_data_start = is_data; + const uint64_t merge_bit_index = + burst_is_data_start ? static_cast(i_data_buffer) : static_cast(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(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(cell_start_s); + e.end_sample = static_cast(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; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h new file mode 100644 index 0000000..c36d6c1 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.h @@ -0,0 +1,135 @@ +#pragma once + +#include "IrFoxProtocolConstants.h" +#include +#include + +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; +using IrFoxOnPacket = std::function; + +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; +}; diff --git a/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h new file mode 100644 index 0000000..63ff42b --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +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(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(rise_sync_us))) + return; + const double nudged = edge_us - static_cast(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 diff --git a/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp new file mode 100644 index 0000000..3aae375 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp @@ -0,0 +1,67 @@ +#include "IrFoxSimulationDataGenerator.h" +#include "IrFoxAnalyzerSettings.h" +#include + +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; +} diff --git a/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h new file mode 100644 index 0000000..00fd2c3 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h @@ -0,0 +1,27 @@ +#ifndef IRFOX_SIMULATION_DATA_GENERATOR +#define IRFOX_SIMULATION_DATA_GENERATOR + +#include + +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 diff --git a/Analyzer/raw/PulseLengthStat/.github/workflows/build.yml b/Analyzer/raw/PulseLengthStat/.github/workflows/build.yml new file mode 100644 index 0000000..90a4b51 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/.github/workflows/build.yml @@ -0,0 +1,52 @@ +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 diff --git a/Analyzer/raw/PulseLengthStat/.gitignore b/Analyzer/raw/PulseLengthStat/.gitignore new file mode 100644 index 0000000..01ee8ea --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/.gitignore @@ -0,0 +1,3 @@ +/build +/build-nmake +.DS_Store diff --git a/Analyzer/raw/PulseLengthStat/CMakeLists.txt b/Analyzer/raw/PulseLengthStat/CMakeLists.txt new file mode 100644 index 0000000..e7756de --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/CMakeLists.txt @@ -0,0 +1,24 @@ +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}) diff --git a/Analyzer/raw/PulseLengthStat/CMakePresets.json b/Analyzer/raw/PulseLengthStat/CMakePresets.json new file mode 100644 index 0000000..d0ed249 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/CMakePresets.json @@ -0,0 +1,49 @@ +{ + "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" + } + ] +} diff --git a/Analyzer/raw/PulseLengthStat/build_msvc.bat b/Analyzer/raw/PulseLengthStat/build_msvc.bat new file mode 100644 index 0000000..9bad4a4 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/build_msvc.bat @@ -0,0 +1,21 @@ +@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% diff --git a/Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake b/Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake new file mode 100644 index 0000000..10ca7ba --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake @@ -0,0 +1,66 @@ +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() diff --git a/Analyzer/raw/PulseLengthStat/configure_msvc.bat b/Analyzer/raw/PulseLengthStat/configure_msvc.bat new file mode 100644 index 0000000..04288dc --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/configure_msvc.bat @@ -0,0 +1,39 @@ +@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 diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp new file mode 100644 index 0000000..84d42dc --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp @@ -0,0 +1,110 @@ +#include "PulseLengthStatAnalyzer.h" +#include "PulseLengthStatAnalyzerSettings.h" +#include + +// 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(segment_start); + frame.mEndingSampleInclusive = static_cast(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; +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h new file mode 100644 index 0000000..29c5714 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h @@ -0,0 +1,39 @@ +#ifndef PULSELENGTHSTAT_ANALYZER_H +#define PULSELENGTHSTAT_ANALYZER_H + +#include +#include "PulseLengthStatAnalyzerSettings.h" +#include "PulseLengthStatAnalyzerResults.h" +#include "PulseLengthStatSimulationDataGenerator.h" +#include + +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 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 diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp new file mode 100644 index 0000000..40076c7 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.cpp @@ -0,0 +1,107 @@ +#include "PulseLengthStatAnalyzerResults.h" +#include +#include "PulseLengthStatAnalyzer.h" +#include "PulseLengthStatAnalyzerSettings.h" +#include +#include + +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; +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h new file mode 100644 index 0000000..8a1d355 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerResults.h @@ -0,0 +1,27 @@ +#ifndef PULSELENGTHSTAT_ANALYZER_RESULTS +#define PULSELENGTHSTAT_ANALYZER_RESULTS + +#include + +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 diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp new file mode 100644 index 0000000..ec939b9 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.cpp @@ -0,0 +1,62 @@ +#include "PulseLengthStatAnalyzerSettings.h" +#include + +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()); +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h new file mode 100644 index 0000000..132a65c --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzerSettings.h @@ -0,0 +1,24 @@ +#ifndef PULSELENGTHSTAT_ANALYZER_SETTINGS +#define PULSELENGTHSTAT_ANALYZER_SETTINGS + +#include +#include + +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 diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp new file mode 100644 index 0000000..3cffbe2 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.cpp @@ -0,0 +1,65 @@ +#include "PulseLengthStatSimulationDataGenerator.h" +#include "PulseLengthStatAnalyzerSettings.h" +#include + +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; +} diff --git a/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h new file mode 100644 index 0000000..0aa5139 --- /dev/null +++ b/Analyzer/raw/PulseLengthStat/src/PulseLengthStatSimulationDataGenerator.h @@ -0,0 +1,27 @@ +#ifndef PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR +#define PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR + +#include + +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 diff --git a/Analyzer/raw/dll/.gitkeep b/Analyzer/raw/dll/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/IR_DMA_ISR_signal_analysis.md b/IR_DMA_ISR_signal_analysis.md new file mode 100644 index 0000000..8812651 --- /dev/null +++ b/IR_DMA_ISR_signal_analysis.md @@ -0,0 +1,141 @@ +# 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` пересчитывайте скриптом.* diff --git a/ir_protocol_gate_runs_sim.py b/ir_protocol_gate_runs_sim.py new file mode 100644 index 0000000..ae72860 --- /dev/null +++ b/ir_protocol_gate_runs_sim.py @@ -0,0 +1,174 @@ +#!/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())