diff --git a/.gitignore b/.gitignore index 460e5c1..9663919 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,8 @@ 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/.github/workflows/build.yml b/Analyzer/raw/IR_Fox/.github/workflows/build.yml new file mode 100644 index 0000000..a7e5b12 --- /dev/null +++ b/Analyzer/raw/IR_Fox/.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/IR_Fox/build -S ${{github.workspace}}/Analyzer/raw/IR_Fox -A x64 + cmake --build ${{github.workspace}}/Analyzer/raw/IR_Fox/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/IR_Fox/build -S ${{github.workspace}}/Analyzer/raw/IR_Fox -DCMAKE_BUILD_TYPE=Release + cmake --build ${{github.workspace}}/Analyzer/raw/IR_Fox/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/IR_Fox/build -S ${{github.workspace}}/Analyzer/raw/IR_Fox -DCMAKE_BUILD_TYPE=Release + cmake --build ${{github.workspace}}/Analyzer/raw/IR_Fox/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/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..91b4991 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp @@ -0,0 +1,200 @@ +#include "IrFoxAnalyzer.h" +#include "IrFoxAnalyzerSettings.h" +#include "IrFoxDecoder.h" +#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(); + + 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; + } + }; + + 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); + + decoder.processEdge(edge_sample, rising, fs, on_bit, on_pkt); + ReportProgress(edge_sample); + } + + 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..8f946e1 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp @@ -0,0 +1,603 @@ +#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) +{ + (void)t_us; + const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us); + if (is_recive_raw && (t_us - prev_rise_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) + { + is_recive = false; + msg_type_receive = 0; + last_edge_time_us = t_us; + } +} + +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) + { + is_recive = false; + is_recive_raw = false; + msg_type_receive = 0; + return; + } + + if (buf_bit_pos == next_control_bit) + { + next_control_bit = static_cast(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); + + if (have_last_processed && (t_us - last_processed_edge_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-бабла: при первом подъёме пузыря метка ИК (активный низ) начинается со спада — берём prev_fall, если он близок. */ + 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 uint32_t cand_rp = static_cast(t_us - prev_rise_us); + 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++; +#if IRFOX_GLITCH_REJECT_PHASE_NUDGE + irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us); +#endif + 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++; +#if IRFOX_GLITCH_REJECT_PHASE_NUDGE + irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us); +#endif + 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; + } + // Как IR_DecoderRaw::tick: до prev_rise_us = t_us, иначе ниже (t_us - prev_rise_us) на подъёме == 0. + if (cand_rp > irmax * 2U && !is_recive_raw) + { + first_rx(); + preamb_front_counter = static_cast(irfox::kPreambFronts - 1U); + is_preamb = true; + is_recive = true; + is_recive_raw = true; + is_wrong_pack = false; + preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, true); + preamble_bubble_start_valid_ = true; + } + 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 + { +#if IRFOX_IN_MARK_DOUBLE_FALL_IGNORE + const double hi_since_rise = t_us - prev_rise_us; + const uint32_t mark_end_min_us = (rise_min_us * irfox::kBitActiveTakts) / irfox::kBitTakts; +#endif + if (t_us - prev_fall_us > rise_min_us / 4.0) + { + prev_fall_us = t_us; + prev_fall_sample = sample; + } + else + { +#if IRFOX_IN_MARK_DOUBLE_FALL_IGNORE + if (!(is_recive && !is_preamb && hi_since_rise < static_cast(mark_end_min_us))) +#endif + { + err_other++; + } + } + } + + // Первый фронт после длинной тишины на спаде; на подъёме — cand_rp выше (до обновления prev_rise_us). + if (t_us > prev_rise_us && (t_us - prev_rise_us) > irmax * 2.0 && !is_recive_raw) + { + first_rx(); + preamb_front_counter = static_cast(irfox::kPreambFronts - 1U); + is_preamb = true; + is_recive = true; + is_recive_raw = true; + is_wrong_pack = false; + 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 — prevRise на этом тике ещё время последнего подъёма + // (на спаде не обновлялся). В сэмплах: якорь = последний подъём + полпериода, не «спад + полпериод». + const uint64_t preamble_bubble_end_sample = + rising ? rise_period_anchor_sample_ : prev_rise_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); + } +#if IRFOX_RISE_GRAY_SINGLE_BIT_FALLBACK + else if (irfox::riseGraySingleBitFallback(rise_period_us, rise_sync_time_us)) + { + err_other++; + 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); + } +#endif + 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); + 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..b335f57 --- /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; + uint8_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..24cc137 --- /dev/null +++ b/Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h @@ -0,0 +1,89 @@ +#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_config.h: IR_SHORT_LOW_GLITCH_REJECT (1 = включено, как на железе). */ +#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT +#define IRFOX_SHORT_LOW_GLITCH_REJECT 1 +#endif +#ifndef IRFOX_RISE_INCLUSIVE_AROUND +#define IRFOX_RISE_INCLUSIVE_AROUND 1 +#endif +#ifndef IRFOX_RISE_GRAY_SINGLE_BIT_FALLBACK +#define IRFOX_RISE_GRAY_SINGLE_BIT_FALLBACK 0 +#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 +#ifndef IRFOX_IN_MARK_DOUBLE_FALL_IGNORE +#define IRFOX_IN_MARK_DOUBLE_FALL_IGNORE 0 +#endif + +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; + +inline uint32_t irTimeoutUs(uint32_t riseSyncTimeUs) +{ + const uint32_t riseMax = riseSyncTimeUs + kToleranceUs; + return riseMax * (8U + kSyncBits + 1U); +} + +inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs) +{ + const uint32_t lo = riseSyncTimeUs > kToleranceUs ? riseSyncTimeUs - kToleranceUs : 0U; + const uint32_t hi = riseSyncTimeUs + kToleranceUs; +#if IRFOX_RISE_INCLUSIVE_AROUND + return lo <= periodUs && periodUs <= hi; +#else + return lo < periodUs && periodUs < hi; +#endif +} + +/** Синхронно с IR_config.h: IR_RISE_GRAY_SINGLE_BIT_FALLBACK. */ +inline bool riseGraySingleBitFallback(uint32_t periodUs, uint32_t riseSyncTimeUs) +{ + const uint32_t hi = riseSyncTimeUs + kToleranceUs; + const uint32_t twice = riseSyncTimeUs * 2U; + return periodUs > hi && periodUs <= twice; +} + +/** Синхронно с IR_config.h: IR_GLITCH_REJECT_PHASE_NUDGE. */ +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/tools/analyze_pkt33_car_point.py b/tools/analyze_pkt33_car_point.py new file mode 100644 index 0000000..0ada184 --- /dev/null +++ b/tools/analyze_pkt33_car_point.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Сравнение трасс decode по carraw3 vs pointraw_3 для кадра pkt[33] (car: CRC fail).""" +from __future__ import annotations + +import sys +from pathlib import Path + +_TOOLS = Path(__file__).resolve().parent +if str(_TOOLS) not in sys.path: + sys.path.insert(0, str(_TOOLS)) + +from ir_decoder_raw_sim import ( + BIT_TIME_US, + RISE_MAX, + RISE_MIN, + SimState, + first_rx, + parse_raw_csv, + segments_to_edges, + tick, +) + + +def run_traced(path: Path) -> tuple[SimState, list[dict]]: + rows = parse_raw_csv(path) + assert rows is not None + edges = segments_to_edges(rows) + st = SimState() + tr: list[dict] = [] + for t_us, rising in edges: + tick(st, t_us, rising, BIT_TIME_US, trace_rise=tr) + return st, tr + + +def main() -> None: + root = Path(__file__).resolve().parents[1] / "Analyzer" / "raw" + car_p = root / "carraw3.txt" + pt_p = root / "pointraw_3.txt" + st_c, tr_c = run_traced(car_p) + st_p, tr_p = run_traced(pt_p) + + bad_i = next(i for i, x in enumerate(st_c.packets) if not x[0]) + print(f"carraw3: bad packet index {bad_i}, bytes {st_c.packets[bad_i][2].hex()}") + print(f"carraw3 packets={len(st_c.packets)} ok={sum(1 for x in st_c.packets if x[0])}") + print(f"pointraw_3 packets={len(st_p.packets)} ok={sum(1 for x in st_p.packets if x[0])}") + print(f"rise window us: min={RISE_MIN} max={RISE_MAX} bit={BIT_TIME_US}") + print() + + # Пока кадр №bad_i собирается, в списке уже bad_i завершённых пакетов → len(packets)==bad_i. + build_i = bad_i + + def frame_trace(tr: list[dict]) -> list[dict]: + return [r for r in tr if r["n_pkt"] == build_i] + + fc = frame_trace(tr_c) + fp = frame_trace(tr_p) + print( + f"decode rises while building packet[{bad_i}] (trace n_pkt=={build_i}): " + f"car {len(fc)} vs point {len(fp)} lines" + ) + print("--- car (first 40 decode steps of this frame) ---") + for i, r in enumerate(fc[:40]): + br = r["branch"] + ex = "" + if br == "ceil": + ex = f" hc={r['hc']} lc={r['lc']} ac={r['ac']}" + print( + f" {i:2} t={r['t_us'] / 1e6:.6f}s rp={r['rp']} ht={r['ht']} lt={r['lt']} " + f"{br} bits_out={r['bits_out']} i_buf={r['i_buf']}{ex}" + ) + print("--- point (first 40) ---") + for i, r in enumerate(fp[:40]): + br = r["branch"] + ex = "" + if br == "ceil": + ex = f" hc={r['hc']} lc={r['lc']} ac={r['ac']}" + print( + f" {i:2} t={r['t_us'] / 1e6:.6f}s rp={r['rp']} ht={r['ht']} lt={r['lt']} " + f"{br} bits_out={r['bits_out']} i_buf={r['i_buf']}{ex}" + ) + + print() + # Первое расхождение по (rp, ht, lt, branch, bits_out) + for i, (a, b) in enumerate(zip(fc, fp)): + ka = (a["rp"], a["ht"], a["lt"], a["branch"], a["bits_out"]) + kb = (b["rp"], b["ht"], b["lt"], b["branch"], b["bits_out"]) + if ka != kb: + print(f"First decode diff at step {i} within frame:") + print(f" car {a}") + print(f" point {b}") + break + else: + if len(fc) != len(fp): + print(f"Same tuples for min(len)={min(len(fc), len(fp))}; length car={len(fc)} point={len(fp)}") + else: + print("Identical decode trace for full frame (unexpected if CRC differs)") + + +if __name__ == "__main__": + main() diff --git a/tools/ir_decoder_raw_sim.py b/tools/ir_decoder_raw_sim.py new file mode 100644 index 0000000..825562f --- /dev/null +++ b/tools/ir_decoder_raw_sim.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +""" +Симуляция IR_DecoderRaw::tick() по CSV уровней (как Analyzer/raw/*_raw.txt). +Сверка с IR_DecoderRaw.cpp / IR_config.h — без Arduino, только логика декодера. +""" +from __future__ import annotations + +import sys +from dataclasses import dataclass, field +from pathlib import Path + +# IR_config.h +BIT_ACTIVE_TAKTS = 25 +BIT_PAUSE_TAKTS = 12 +CARRIER_HZ = 38000 +CARRIER_PERIOD_US = 1_000_000 / CARRIER_HZ +BIT_TIME_US = int((BIT_ACTIVE_TAKTS + BIT_PAUSE_TAKTS) * CARRIER_PERIOD_US) +TOLERANCE_US = 300 +SYNC_BITS = 3 +BIT_PER_BYTE = 8 +MSG_BYTES = 1 +CRC_BYTES = 2 +POLY1 = 0x31 +POLY2 = 0x8C +DATA_BYTE_SIZE_MAX = 1 + 2 + 2 + 31 + 2 +IR_MASK_MSG_INFO = 0x1F +PREAMB_FRONTS = 6 # preambPulse*2 + +RISE_MIN = BIT_TIME_US - TOLERANCE_US +RISE_MAX = BIT_TIME_US + TOLERANCE_US +IR_TIMEOUT = RISE_MAX * (8 + SYNC_BITS + 1) +# IR_config.h: IR_SHORT_LOW_GLITCH_REJECT (по умолчанию 1) +SHORT_LOW_GLITCH_REJECT = True +GLITCH_REJECT_PHASE_NUDGE = True +MICRO_GAP_RISE_REJECT = True +IN_MARK_DOUBLE_FALL_IGNORE = False # как IR_config по умолчанию; True — меньше err_other, на разбор битов не влияет +# IR_RISE_INCLUSIVE_AROUND, IR_RISE_GRAY_SINGLE_BIT_FALLBACK +RISE_INCLUSIVE_AROUND = True +RISE_GRAY_SINGLE_BIT_FALLBACK = False # как IR_config по умолчанию; True — только для экспериментов + + +def around_rise_period(rise_period: int, rise_sync: int) -> bool: + lo = max(0, rise_sync - TOLERANCE_US) + hi = rise_sync + TOLERANCE_US + if RISE_INCLUSIVE_AROUND: + return lo <= rise_period <= hi + return lo < rise_period < hi + + +def rise_gray_single_bit_fallback(rise_period: int, rise_sync: int) -> bool: + hi = rise_sync + TOLERANCE_US + return rise_period > hi and rise_period <= 2 * rise_sync + + +def glitch_phase_nudge(edge_us: float, rise_sync: int, prev_rise: float) -> float: + if not GLITCH_REJECT_PHASE_NUDGE: + return prev_rise + if edge_us <= rise_sync: + return prev_rise + nudged = edge_us - rise_sync + if nudged > prev_rise and nudged < edge_us: + return nudged + return prev_rise + + +def ceil_div(val: int, div: int) -> int: + ret = val // div + if ((val << 4) // div - (ret << 4)) >= 8: + ret += 1 + return ret + + +def crc8(data: bytes, start: int, end: int, poly: int) -> int: + crc = 0xFF + for i in range(start, end): + crc ^= data[i] + for _ in range(8): + if crc & 0x80: + crc = ((crc << 1) ^ poly) & 0xFF + else: + crc = (crc << 1) & 0xFF + return crc + + +def crc_check(buf: bytearray, pack_size: int) -> bool: + ln = pack_size - CRC_BYTES + c1 = crc8(bytes(buf), 0, ln, POLY1) + c2 = crc8(bytes(buf), 0, ln + 1, POLY2) + crc = ((c1 << 8) & 0xFF00) | (c2 & 0xFF) + return buf[ln] == ((crc >> 8) & 0xFF) and buf[ln + 1] == (crc & 0xFF) + + +def parse_raw_csv(path: Path) -> list[tuple[float, str, float]] | None: + """None — файл не в формате уровней (например экспорт битов декодера).""" + rows = [] + with path.open(encoding="utf-8", errors="replace") as f: + header = f.readline() + h = header.lower() + if "duration" not in h and "level" not in h: + if "bit_idx" in h or (len(header.split(",")) >= 3 and header.split(",")[1].strip() == "Type"): + return None + for line in f: + line = line.strip() + if not line: + continue + parts = line.split(",") + if len(parts) < 4: + continue + t_s = float(parts[0]) + level = parts[1].strip() + if level.upper() not in ("HIGH", "LOW"): + return None + dur_us = float(parts[3].replace(" us", "").strip()) + rows.append((t_s, level, dur_us)) + return rows + + +def segments_to_edges(rows: list[tuple[float, str, float]]) -> list[tuple[float, bool]]: + """Фронты: (t_us, rising).""" + edges: list[tuple[float, bool]] = [] + prev = None + for t_s, level, _dur in rows: + high = level.upper() == "HIGH" + if prev is not None: + rising = high + edges.append((t_s * 1e6, rising)) + prev = high + return edges + + +@dataclass +class SimState: + prev_rise: float = 0.0 + prev_fall: float = 0.0 + rise_period: int = 0 + high_time: int = 0 + low_time: int = 0 + last_edge: float = 0.0 + preamb_front_counter: int = 0 + is_preamb: bool = False + is_recive_raw: bool = False + is_recive: bool = False + is_wrong_pack: bool = False + is_buffer_overflow: bool = False + buf_bit_pos: int = 0 + is_data: bool = True + i_data_buffer: int = 0 + next_control_bit: int = BIT_PER_BYTE + i_sync_bit: int = 0 + err_sync_bit: int = 0 + pack_size: int = 0 + data_buffer: bytearray = field(default_factory=lambda: bytearray(DATA_BYTE_SIZE_MAX)) + err_low: int = 0 + err_high: int = 0 + err_other: int = 0 + high_count: int = 0 + low_count: int = 0 + all_count: int = 0 + packets: list[tuple[bool, int, bytes]] = field(default_factory=list) # crc_ok, pack_size, raw bytes + bits_log: list[tuple[float, str, int]] = field(default_factory=list) # t_us, kind, bit + + +def first_rx(st: SimState) -> None: + """IR_DecoderRaw::firstRX — сброс буфера; isRecive/isReciveRaw в прошивке здесь не меняются.""" + st.is_preamb = True + st.is_wrong_pack = False + st.is_buffer_overflow = False + st.buf_bit_pos = 0 + st.is_data = True + st.i_data_buffer = 0 + st.next_control_bit = BIT_PER_BYTE + st.i_sync_bit = 0 + st.err_sync_bit = 0 + st.pack_size = 0 + st.data_buffer = bytearray(DATA_BYTE_SIZE_MAX) + + +def tick( + st: SimState, + t_us: float, + rising: bool, + rise_sync_time: int, + *, + trace_rise: list[dict] | None = None, +) -> None: + """Один вызов tick — один фронт (как IR_DecoderRaw после pop).""" + rise_min = max(0, rise_sync_time - TOLERANCE_US) + rise_max = rise_sync_time + TOLERANCE_US + irmax = IR_TIMEOUT # упрощ.: без подстройки riseSyncTime в timeout + + # listenStart: обрыв незавершённого приёма + if st.is_recive_raw and (t_us - st.prev_rise) > irmax * 2: + st.is_recive_raw = False + first_rx(st) + + st.last_edge = t_us + skip_rest = False + + if rising: + cand_rp = int(t_us - st.prev_rise) + cand_ht = int(t_us - st.prev_fall) + cand_lt = int(st.prev_fall - st.prev_rise) + if SHORT_LOW_GLITCH_REJECT: + short_low_glitch = ( + st.is_recive + and not st.is_preamb + and cand_ht < (rise_min // 8) + and cand_lt >= rise_min + and cand_rp >= rise_min + and cand_rp <= IR_TIMEOUT + ) + if short_low_glitch: + st.err_other += 1 + if GLITCH_REJECT_PHASE_NUDGE: + st.prev_rise = glitch_phase_nudge(t_us, rise_sync_time, st.prev_rise) + skip_rest = True + if not skip_rest and MICRO_GAP_RISE_REJECT: + lt_ok = (cand_lt >= rise_min) or (cand_lt >= (rise_min // 4) and cand_lt < rise_min) + micro_gap = ( + st.is_recive + and not st.is_preamb + and cand_ht < (rise_min // 8) + and lt_ok + and cand_rp >= (rise_min // 4) + and cand_rp < rise_min + and cand_rp <= IR_TIMEOUT + ) + if micro_gap: + st.err_other += 1 + if GLITCH_REJECT_PHASE_NUDGE: + st.prev_rise = glitch_phase_nudge(t_us, rise_sync_time, st.prev_rise) + skip_rest = True + if not skip_rest and cand_rp <= rise_max / 4 and not st.high_count and not st.low_count: + st.err_other += 1 + skip_rest = True + if not skip_rest: + # Как IR_DecoderRaw::tick: до обновления prev_rise, иначе (t_us - prev_rise) == 0 на подъёме. + if cand_rp > irmax * 2 and not st.is_recive_raw: + first_rx(st) + st.preamb_front_counter = PREAMB_FRONTS - 1 + st.is_preamb = True + st.is_recive = True + st.is_recive_raw = True + st.is_wrong_pack = False + st.rise_period = cand_rp + st.high_time = cand_ht + st.low_time = cand_lt + st.prev_rise = t_us + else: + if t_us - st.prev_fall > rise_min / 4: + st.prev_fall = t_us + else: + skip_err = False + if IN_MARK_DOUBLE_FALL_IGNORE: + hi_since_rise = t_us - st.prev_rise + mark_end_min = (rise_min * BIT_ACTIVE_TAKTS) // ( + BIT_ACTIVE_TAKTS + BIT_PAUSE_TAKTS + ) + skip_err = ( + st.is_recive + and not st.is_preamb + and hi_since_rise < mark_end_min + ) + if not skip_err: + st.err_other += 1 + + if skip_rest: + return + + # Старт нового кадра после длинной паузы (в прошивке буфер обнуляется через available/таймаут; + # для мульти-пакета в симуляции явно first_rx, иначе i_data_buffer залипает). + if t_us > st.prev_rise and (t_us - st.prev_rise) > irmax * 2 and not st.is_recive_raw: + first_rx(st) + st.preamb_front_counter = PREAMB_FRONTS - 1 + st.is_preamb = True + st.is_recive = True + st.is_recive_raw = True + st.is_wrong_pack = False + + if st.preamb_front_counter: + if rising and st.rise_period < irmax: + if st.rise_period < rise_min // 2: + st.preamb_front_counter += 2 + st.err_other += 1 + st.preamb_front_counter -= 1 + else: + if st.is_preamb: + st.is_preamb = False + st.prev_rise += st.rise_period / 2.0 + return + + if st.is_preamb: + return + + if st.rise_period > irmax or st.is_buffer_overflow or st.rise_period < rise_min or st.is_wrong_pack: + return + + if not rising: + return + + st.high_count = st.low_count = st.all_count = 0 + invert_err = False + + def write_to_buffer(bit: bool, invert_fix: bool) -> None: + if st.i_data_buffer > DATA_BYTE_SIZE_MAX * 8: + st.is_buffer_overflow = True + if st.is_buffer_overflow or st.is_preamb or st.is_wrong_pack: + st.is_recive = False + st.is_recive_raw = False + return + if st.buf_bit_pos == st.next_control_bit: + st.next_control_bit += SYNC_BITS if st.is_data else BIT_PER_BYTE + st.is_data = not st.is_data + st.i_sync_bit = 0 + st.err_sync_bit = 0 + if st.is_data: + bi = st.i_data_buffer + st.data_buffer[bi // 8] |= (1 if bit else 0) << (7 - (bi % 8)) + st.i_data_buffer += 1 + st.buf_bit_pos += 1 + st.bits_log.append((t_us, "D", 1 if bit else 0)) + else: + if st.i_sync_bit == 0: + last_b = (st.data_buffer[((st.i_data_buffer - 1) // 8)] >> (7 - ((st.i_data_buffer - 1) % 8))) & 1 + if bit != bool(last_b): + st.buf_bit_pos += 1 + st.i_sync_bit += 1 + st.bits_log.append((t_us, "S", 1 if bit else 0)) + else: + st.i_sync_bit = 0 + st.err_other += 1 + st.err_sync_bit += 1 + if st.err_sync_bit >= SYNC_BITS: + st.is_wrong_pack = True + else: + st.buf_bit_pos += 1 + st.i_sync_bit += 1 + st.bits_log.append((t_us, "S", 1 if bit else 0)) + st.is_wrong_pack = st.err_sync_bit >= SYNC_BITS + if st.is_data and not st.is_wrong_pack: + if st.i_data_buffer == 8 * MSG_BYTES: + st.pack_size = st.data_buffer[0] & IR_MASK_MSG_INFO + if st.pack_size and st.i_data_buffer == st.pack_size * BIT_PER_BYTE: + ok = crc_check(st.data_buffer, st.pack_size) + st.packets.append((ok, st.pack_size, bytes(st.data_buffer[: st.pack_size]))) + st.is_recive = False + st.is_recive_raw = False + # буфер не чистят здесь — как в IR_DecoderRaw; firstRX по listenStart + + if around_rise_period(st.rise_period, rise_sync_time): + if st.high_time > st.low_time: + write_to_buffer(True, False) + else: + write_to_buffer(False, False) + elif RISE_GRAY_SINGLE_BIT_FALLBACK and rise_gray_single_bit_fallback(st.rise_period, rise_sync_time): + st.err_other += 1 + if st.high_time > st.low_time: + write_to_buffer(True, False) + else: + write_to_buffer(False, False) + else: + hc = ceil_div(min(st.high_time, 0xFFFF), rise_sync_time) + lc = ceil_div(min(st.low_time, 0xFFFF), rise_sync_time) + ac = ceil_div(min(st.rise_period, 0xFFFF), rise_sync_time) + st.high_count = min(hc, 127) + st.low_count = min(lc, 127) + st.all_count = min(ac, 127) + if st.high_count == 0 and st.high_time > rise_sync_time // 3: + st.high_count += 1 + st.err_other += 1 + if st.low_count + st.high_count > st.all_count: + if st.low_count > st.high_count: + st.low_count = st.all_count - st.high_count + st.err_low += st.low_count + elif st.low_count < st.high_count: + st.high_count = st.all_count - st.low_count + st.err_high += st.high_count + elif st.low_count == st.high_count: + invert_err = True + st.err_other += st.all_count + if st.low_count < st.high_count: + st.err_high += st.high_count + else: + st.err_low += st.low_count + # Как IR_DecoderRaw.cpp / IrFoxDecoder: не более 8 LOW и 8 HIGH за один подъём (i < n && 8 - i). + i = 0 + while i < st.low_count and (8 - i): + if i == st.low_count - 1 and invert_err: + invert_err = False + write_to_buffer(True, True) + else: + write_to_buffer(False, False) + i += 1 + i = 0 + while i < st.high_count and (8 - i): + if i == st.high_count - 1 and invert_err: + invert_err = False + write_to_buffer(False, True) + else: + write_to_buffer(True, False) + i += 1 + + if trace_rise is not None and rising and not skip_rest: + # После decode: залогировать только реальные записи битов (не преамбула, не ранний return выше) + if ( + not st.is_preamb + and st.rise_period <= irmax + and not st.is_buffer_overflow + and st.rise_period >= rise_min + ): + rec: dict = { + "t_us": t_us, + "rp": st.rise_period, + "ht": st.high_time, + "lt": st.low_time, + "i_buf": st.i_data_buffer, + "n_pkt": len(st.packets), + } + if around_rise_period(st.rise_period, rise_sync_time): + rec["branch"] = "around" + rec["bits_out"] = 1 + elif RISE_GRAY_SINGLE_BIT_FALLBACK and rise_gray_single_bit_fallback( + st.rise_period, rise_sync_time + ): + rec["branch"] = "gray" + rec["bits_out"] = 1 + else: + rec["branch"] = "ceil" + rec["hc"] = st.high_count + rec["lc"] = st.low_count + rec["ac"] = st.all_count + rec["bits_out"] = st.low_count + st.high_count + trace_rise.append(rec) + + +def run_file(path: Path, max_packets: int = 0) -> SimState | None: + """max_packets=0 — обработать весь файл (для регрессии по всем пакетам).""" + rows = parse_raw_csv(path) + if rows is None: + return None + edges = segments_to_edges(rows) + st = SimState() + rise_sync = BIT_TIME_US + for t_us, rising in edges: + tick(st, t_us, rising, rise_sync) + if max_packets and len(st.packets) >= max_packets: + break + return st + + +def main() -> None: + root = Path(__file__).resolve().parents[1] + default_set = [ + ("CAR", root / "Analyzer" / "raw" / "car_raw.txt"), + ("POINT", root / "Analyzer" / "raw" / "point_raw.txt"), + ("CARRAW3", root / "Analyzer" / "raw" / "carraw3.txt"), + ("POINTRA3", root / "Analyzer" / "raw" / "pointraw_3.txt"), + ] + files = default_set + if len(sys.argv) >= 2: + files = [(Path(p).name, Path(p)) for p in sys.argv[1:]] + + for label, p in files: + if not p.exists(): + print(f"skip {label}: {p} not found") + continue + st = run_file(p, max_packets=0) + print(f"=== {label} {p.name} ===") + if st is None: + print( + " (skip: decoder trace CSV Type/bit_idx, or missing Level+Duration; need Saleae level like car_raw.txt)" + ) + continue + n_ok = sum(1 for x in st.packets if x[0]) + n_bad = len(st.packets) - n_ok + print( + f" packets={len(st.packets)} crc_ok={n_ok} crc_bad={n_bad} " + f"err_other={st.err_other} err_low={st.err_low} err_high={st.err_high}" + ) + for i, (ok, psz, raw) in enumerate(st.packets[:12]): + hx = raw.hex(" ") + print(f" pkt[{i}] crc_ok={ok} len={psz} {hx}") + if len(st.packets) > 12: + print(f" ... ({len(st.packets) - 12} more packets)") + print(f" first 24 bits: {st.bits_log[:24]}") + + car = root / "Analyzer" / "raw" / "car_raw.txt" + point = root / "Analyzer" / "raw" / "point_raw.txt" + if car.exists() and point.exists(): + sc = run_file(car, max_packets=1) + sp = run_file(point, max_packets=1) + if sc is None or sp is None: + return + print("\n=== First packet bit diff (D/S sequence) CAR vs POINT ===") + for i, (a, b) in enumerate(zip(sc.bits_log, sp.bits_log)): + if a != b: + print(f" idx {i}: CAR {a} vs POINT {b}") + break + else: + if len(sc.bits_log) != len(sp.bits_log): + print(f" len CAR={len(sc.bits_log)} POINT={len(sp.bits_log)}") + else: + print(" identical bit logs for min(len)") + + +if __name__ == "__main__": + main()