mirror of
https://github.com/Show-maket/IR-protocol.git
synced 2026-04-28 11:18:15 +00:00
Compare commits
9 Commits
STM32DMA
...
opti-dma-e
| Author | SHA1 | Date | |
|---|---|---|---|
| d788358ac8 | |||
| 8daff9c46a | |||
| 8631f23b53 | |||
| ad1e16cfda | |||
| 57f79b35c7 | |||
| f8daa68381 | |||
| ddb8a9e143 | |||
| c29fe2cf7c | |||
| 7651f07e0a |
8
.gitignore
vendored
8
.gitignore
vendored
@ -5,6 +5,8 @@ log/*
|
||||
/.vscode
|
||||
*.zip
|
||||
**/__pycache__
|
||||
/Analyzer/raw/IR_Fox/build
|
||||
/Analyzer/raw/PulseLengthStat/build
|
||||
*.dll
|
||||
Analyzer/raw/dll/*.dll
|
||||
Analyzer/raw/dll/*.so
|
||||
Analyzer/raw/dll/*.dylib
|
||||
/Analyzer/raw/IR_Fox/.github
|
||||
**/.build
|
||||
|
||||
2
.vscode/arduino.json
vendored
2
.vscode/arduino.json
vendored
@ -1,5 +1,5 @@
|
||||
{
|
||||
"board": "STMicroelectronics:stm32:GenF4",
|
||||
"board": "STMicroelectronics:stm32:GenG4",
|
||||
"port": "COM17",
|
||||
"prebuild": "if exist bin rd /s /q bin"
|
||||
}
|
||||
52
Analyzer/raw/IR_Fox/.github/workflows/build.yml
vendored
Normal file
52
Analyzer/raw/IR_Fox/.github/workflows/build.yml
vendored
Normal file
@ -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
|
||||
2
Analyzer/raw/IR_Fox/.gitignore
vendored
Normal file
2
Analyzer/raw/IR_Fox/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/build
|
||||
.DS_Store
|
||||
26
Analyzer/raw/IR_Fox/CMakeLists.txt
Normal file
26
Analyzer/raw/IR_Fox/CMakeLists.txt
Normal file
@ -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})
|
||||
49
Analyzer/raw/IR_Fox/CMakePresets.json
Normal file
49
Analyzer/raw/IR_Fox/CMakePresets.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
Analyzer/raw/IR_Fox/build_msvc.bat
Normal file
21
Analyzer/raw/IR_Fox/build_msvc.bat
Normal file
@ -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%
|
||||
66
Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake
Normal file
66
Analyzer/raw/IR_Fox/cmake/ExternalAnalyzerSDK.cmake
Normal file
@ -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()
|
||||
39
Analyzer/raw/IR_Fox/configure_msvc.bat
Normal file
39
Analyzer/raw/IR_Fox/configure_msvc.bat
Normal file
@ -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
|
||||
280
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp
Normal file
280
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.cpp
Normal file
@ -0,0 +1,280 @@
|
||||
#include "IrFoxAnalyzer.h"
|
||||
#include "IrFoxAnalyzerSettings.h"
|
||||
#include "IrFoxDecoder.h"
|
||||
#include <AnalyzerChannelData.h>
|
||||
#include <AnalyzerResults.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
IrFoxAnalyzer::IrFoxAnalyzer()
|
||||
: Analyzer2(),
|
||||
mSettings(),
|
||||
mSimulationInitilized(false)
|
||||
{
|
||||
SetAnalyzerSettings(&mSettings);
|
||||
UseFrameV2();
|
||||
}
|
||||
|
||||
IrFoxAnalyzer::~IrFoxAnalyzer()
|
||||
{
|
||||
KillThread();
|
||||
}
|
||||
|
||||
void IrFoxAnalyzer::SetupResults()
|
||||
{
|
||||
m_packet_hex_by_frame.clear();
|
||||
mResults.reset(new IrFoxAnalyzerResults(this, &mSettings));
|
||||
SetAnalyzerResults(mResults.get());
|
||||
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
|
||||
}
|
||||
|
||||
static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_bytes = 64)
|
||||
{
|
||||
static const char* hd = "0123456789abcdef";
|
||||
const size_t m = n < max_bytes ? n : max_bytes;
|
||||
for (size_t i = 0; i < m; i++)
|
||||
{
|
||||
s.push_back(hd[p[i] >> 4]);
|
||||
s.push_back(hd[p[i] & 0xFu]);
|
||||
if (i + 1 < m)
|
||||
s.push_back(' ');
|
||||
}
|
||||
if (n > max_bytes)
|
||||
s += "...";
|
||||
}
|
||||
|
||||
const char* IrFoxAnalyzer::PacketHexForFrame(U64 frame_id)
|
||||
{
|
||||
auto it = m_packet_hex_by_frame.find(frame_id);
|
||||
if (it == m_packet_hex_by_frame.end())
|
||||
return "";
|
||||
m_hex_scratch = it->second;
|
||||
return m_hex_scratch.c_str();
|
||||
}
|
||||
|
||||
const char* IrFoxAnalyzer::BubbleTextForFrame(U64 frame_id) const
|
||||
{
|
||||
auto it = m_bubble_text_by_frame.find(frame_id);
|
||||
if (it == m_bubble_text_by_frame.end())
|
||||
return "";
|
||||
m_bubble_scratch = it->second;
|
||||
return m_bubble_scratch.c_str();
|
||||
}
|
||||
|
||||
void IrFoxAnalyzer::WorkerThread()
|
||||
{
|
||||
mIr = GetAnalyzerChannelData(mSettings.mInputChannel);
|
||||
m_packet_hex_by_frame.clear();
|
||||
m_bubble_text_by_frame.clear();
|
||||
|
||||
const U32 fs = GetSampleRate();
|
||||
IrFoxDecoder decoder;
|
||||
decoder.reset();
|
||||
|
||||
/** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */
|
||||
const U64 min_seg_samples =
|
||||
std::max<U64>(1ULL, static_cast<U64>((static_cast<double>(irfox::kMinFilteredPulseUs) * 1e-6) * static_cast<double>(fs) + 0.5));
|
||||
struct RawEdge
|
||||
{
|
||||
U64 sample;
|
||||
bool rising;
|
||||
};
|
||||
std::vector<RawEdge> pending;
|
||||
U64 last_dec_edge_sample = 0;
|
||||
bool last_dec_edge_valid = false;
|
||||
|
||||
auto collapse_short_pairs = [&]() {
|
||||
for (size_t i = 0; i + 1 < pending.size();)
|
||||
{
|
||||
if (pending[i + 1].sample - pending[i].sample < min_seg_samples)
|
||||
{
|
||||
pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
pending.begin() + static_cast<std::ptrdiff_t>(i + 2));
|
||||
if (i > 0)
|
||||
--i;
|
||||
}
|
||||
else
|
||||
++i;
|
||||
}
|
||||
};
|
||||
|
||||
auto strip_vs_last_decoder = [&]() {
|
||||
for (;;)
|
||||
{
|
||||
collapse_short_pairs();
|
||||
if (!last_dec_edge_valid || pending.empty())
|
||||
return;
|
||||
if (pending[0].sample - last_dec_edge_sample >= min_seg_samples)
|
||||
return;
|
||||
pending.erase(pending.begin());
|
||||
}
|
||||
};
|
||||
|
||||
U32 frames_since_commit = 0;
|
||||
const U32 kCommitBatch = 256;
|
||||
|
||||
IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) {
|
||||
Frame frame;
|
||||
frame.mStartingSampleInclusive = static_cast<S64>(e.start_sample);
|
||||
frame.mEndingSampleInclusive = static_cast<S64>(e.end_sample);
|
||||
frame.mType = e.frame_type;
|
||||
frame.mData1 = e.bit_value;
|
||||
frame.mData2 = e.bit_index | (U64(e.err_low) << 16) | (U64(e.err_high) << 24) | (U64(e.err_other) << 32);
|
||||
frame.mFlags = e.mflags;
|
||||
// В SDK только ERROR/WARNING меняют цвет бабла; sync выделяем янтарным (как warning), данные — обычные.
|
||||
if (e.frame_type == IRF_FT_SYNC_BIT)
|
||||
frame.mFlags |= DISPLAY_AS_WARNING_FLAG;
|
||||
|
||||
const U64 fid = mResults->AddFrame(frame);
|
||||
if (e.bubble_text[0] != '\0')
|
||||
m_bubble_text_by_frame[fid] = e.bubble_text;
|
||||
if (++frames_since_commit >= kCommitBatch)
|
||||
{
|
||||
mResults->CommitResults();
|
||||
frames_since_commit = 0;
|
||||
}
|
||||
};
|
||||
|
||||
IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) {
|
||||
Frame frame;
|
||||
frame.mStartingSampleInclusive = static_cast<S64>(p.start_sample);
|
||||
frame.mEndingSampleInclusive = static_cast<S64>(p.end_sample);
|
||||
frame.mType = p.crc_ok ? IRF_FT_PACKET_OK : IRF_FT_PACKET_CRC_FAIL;
|
||||
frame.mData1 = p.pack_size;
|
||||
frame.mData2 = (U64(p.err_low) << 0) | (U64(p.err_high) << 8) | (U64(p.err_other) << 16);
|
||||
if (!p.crc_ok)
|
||||
frame.mFlags |= DISPLAY_AS_ERROR_FLAG;
|
||||
|
||||
const U64 fid = mResults->AddFrame(frame);
|
||||
|
||||
std::string hx;
|
||||
append_hex(hx, p.data_bytes, p.pack_size);
|
||||
m_packet_hex_by_frame[fid] = std::move(hx);
|
||||
|
||||
FrameV2 fv2;
|
||||
fv2.AddBoolean("crc_ok", p.crc_ok);
|
||||
fv2.AddInteger("len", static_cast<S64>(p.pack_size));
|
||||
fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
|
||||
fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
|
||||
fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
|
||||
fv2.AddByteArray("data", p.data_bytes, p.pack_size);
|
||||
mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast<U64>(p.start_sample),
|
||||
static_cast<U64>(p.end_sample));
|
||||
|
||||
if (++frames_since_commit >= kCommitBatch)
|
||||
{
|
||||
mResults->CommitResults();
|
||||
frames_since_commit = 0;
|
||||
}
|
||||
};
|
||||
|
||||
auto emit_confirmed_edges = [&]() {
|
||||
for (;;)
|
||||
{
|
||||
collapse_short_pairs();
|
||||
strip_vs_last_decoder();
|
||||
if (pending.size() < 2)
|
||||
return;
|
||||
if (pending[1].sample - pending[0].sample < min_seg_samples)
|
||||
continue;
|
||||
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
|
||||
last_dec_edge_sample = pending[0].sample;
|
||||
last_dec_edge_valid = true;
|
||||
pending.erase(pending.begin());
|
||||
}
|
||||
};
|
||||
|
||||
auto flush_pending_tail = [&]() {
|
||||
collapse_short_pairs();
|
||||
strip_vs_last_decoder();
|
||||
while (pending.size() >= 2 && pending[1].sample - pending[0].sample >= min_seg_samples)
|
||||
{
|
||||
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
|
||||
last_dec_edge_sample = pending[0].sample;
|
||||
last_dec_edge_valid = true;
|
||||
pending.erase(pending.begin());
|
||||
collapse_short_pairs();
|
||||
strip_vs_last_decoder();
|
||||
}
|
||||
if (pending.size() == 1)
|
||||
{
|
||||
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
|
||||
last_dec_edge_sample = pending[0].sample;
|
||||
last_dec_edge_valid = true;
|
||||
pending.clear();
|
||||
}
|
||||
};
|
||||
|
||||
for (;;)
|
||||
{
|
||||
CheckIfThreadShouldExit();
|
||||
|
||||
const U64 segment_start = mIr->GetSampleNumber();
|
||||
const BitState level = mIr->GetBitState();
|
||||
|
||||
mIr->AdvanceToNextEdge();
|
||||
|
||||
const U64 edge_sample = mIr->GetSampleNumber();
|
||||
if (edge_sample == segment_start)
|
||||
break;
|
||||
|
||||
const BitState new_level = mIr->GetBitState();
|
||||
const bool rising = (new_level == BIT_HIGH);
|
||||
|
||||
pending.push_back(RawEdge{edge_sample, rising});
|
||||
emit_confirmed_edges();
|
||||
ReportProgress(edge_sample);
|
||||
}
|
||||
|
||||
flush_pending_tail();
|
||||
decoder.flushEnd(mIr->GetSampleNumber(), fs, on_bit, on_pkt);
|
||||
|
||||
if (frames_since_commit != 0)
|
||||
mResults->CommitResults();
|
||||
}
|
||||
|
||||
bool IrFoxAnalyzer::NeedsRerun()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
U32 IrFoxAnalyzer::GenerateSimulationData(U64 minimum_sample_index, U32 device_sample_rate,
|
||||
SimulationChannelDescriptor** simulation_channels)
|
||||
{
|
||||
if (mSimulationInitilized == false)
|
||||
{
|
||||
mSimulationDataGenerator.Initialize(GetSimulationSampleRate(), &mSettings);
|
||||
mSimulationInitilized = true;
|
||||
}
|
||||
|
||||
return mSimulationDataGenerator.GenerateSimulationData(minimum_sample_index, device_sample_rate,
|
||||
simulation_channels);
|
||||
}
|
||||
|
||||
U32 IrFoxAnalyzer::GetMinimumSampleRateHz()
|
||||
{
|
||||
return 200000;
|
||||
}
|
||||
|
||||
const char* IrFoxAnalyzer::GetAnalyzerName() const
|
||||
{
|
||||
return "IR Fox";
|
||||
}
|
||||
|
||||
const char* GetAnalyzerName()
|
||||
{
|
||||
return "IR Fox";
|
||||
}
|
||||
|
||||
Analyzer* CreateAnalyzer()
|
||||
{
|
||||
return new IrFoxAnalyzer();
|
||||
}
|
||||
|
||||
void DestroyAnalyzer(Analyzer* analyzer)
|
||||
{
|
||||
delete analyzer;
|
||||
}
|
||||
49
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h
Normal file
49
Analyzer/raw/IR_Fox/src/IrFoxAnalyzer.h
Normal file
@ -0,0 +1,49 @@
|
||||
#ifndef IRFOX_ANALYZER_H
|
||||
#define IRFOX_ANALYZER_H
|
||||
|
||||
#include <Analyzer.h>
|
||||
#include "IrFoxAnalyzerSettings.h"
|
||||
#include "IrFoxAnalyzerResults.h"
|
||||
#include "IrFoxSimulationDataGenerator.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2
|
||||
{
|
||||
public:
|
||||
IrFoxAnalyzer();
|
||||
virtual ~IrFoxAnalyzer();
|
||||
|
||||
virtual void SetupResults();
|
||||
virtual void WorkerThread();
|
||||
|
||||
virtual U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate,
|
||||
SimulationChannelDescriptor** simulation_channels);
|
||||
virtual U32 GetMinimumSampleRateHz();
|
||||
|
||||
virtual const char* GetAnalyzerName() const;
|
||||
virtual bool NeedsRerun();
|
||||
|
||||
const char* PacketHexForFrame(U64 frame_id);
|
||||
const char* BubbleTextForFrame(U64 frame_id) const;
|
||||
|
||||
protected:
|
||||
IrFoxAnalyzerSettings mSettings;
|
||||
std::unique_ptr<IrFoxAnalyzerResults> mResults;
|
||||
AnalyzerChannelData* mIr;
|
||||
|
||||
IrFoxSimulationDataGenerator mSimulationDataGenerator;
|
||||
bool mSimulationInitilized;
|
||||
|
||||
std::unordered_map<U64, std::string> m_packet_hex_by_frame;
|
||||
std::unordered_map<U64, std::string> m_bubble_text_by_frame;
|
||||
mutable std::string m_hex_scratch;
|
||||
mutable std::string m_bubble_scratch;
|
||||
};
|
||||
|
||||
extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName();
|
||||
extern "C" ANALYZER_EXPORT Analyzer* __cdecl CreateAnalyzer();
|
||||
extern "C" ANALYZER_EXPORT void __cdecl DestroyAnalyzer(Analyzer* analyzer);
|
||||
|
||||
#endif
|
||||
175
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp
Normal file
175
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.cpp
Normal file
@ -0,0 +1,175 @@
|
||||
#include "IrFoxAnalyzerResults.h"
|
||||
#include <AnalyzerHelpers.h>
|
||||
#include <AnalyzerResults.h>
|
||||
#include "IrFoxAnalyzer.h"
|
||||
#include "IrFoxAnalyzerSettings.h"
|
||||
#include "IrFoxDecoder.h"
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings)
|
||||
: AnalyzerResults(),
|
||||
mSettings(settings),
|
||||
mAnalyzer(analyzer)
|
||||
{
|
||||
}
|
||||
|
||||
IrFoxAnalyzerResults::~IrFoxAnalyzerResults()
|
||||
{
|
||||
}
|
||||
|
||||
void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
|
||||
{
|
||||
(void)display_base;
|
||||
(void)channel;
|
||||
ClearResultStrings();
|
||||
Frame frame = GetFrame(frame_index);
|
||||
|
||||
char line[256];
|
||||
|
||||
switch (frame.mType)
|
||||
{
|
||||
case IRF_FT_DATA_BIT:
|
||||
case IRF_FT_SYNC_BIT:
|
||||
case IRF_FT_PREAMBLE:
|
||||
case IRF_FT_OVERFLOW:
|
||||
case IRF_FT_ABORT:
|
||||
{
|
||||
const char* bt = mAnalyzer->BubbleTextForFrame(frame_index);
|
||||
if (bt && bt[0])
|
||||
AddResultString(bt);
|
||||
else if (frame.mType == IRF_FT_DATA_BIT)
|
||||
AddResultString(frame.mData1 ? "1" : "0");
|
||||
else if (frame.mType == IRF_FT_SYNC_BIT)
|
||||
{
|
||||
snprintf(line, sizeof line, "sync: %s", frame.mData1 ? "1" : "0");
|
||||
AddResultString(line);
|
||||
}
|
||||
else if (frame.mType == IRF_FT_OVERFLOW)
|
||||
AddResultString("OVF");
|
||||
else if (frame.mType == IRF_FT_ABORT)
|
||||
AddResultString("SYNC!");
|
||||
else
|
||||
AddResultString("PRE");
|
||||
break;
|
||||
}
|
||||
|
||||
case IRF_FT_PACKET_OK:
|
||||
case IRF_FT_PACKET_CRC_FAIL:
|
||||
{
|
||||
snprintf(line, sizeof line, "%s %lluB", frame.mType == IRF_FT_PACKET_OK ? "OK" : "CRC",
|
||||
(unsigned long long)frame.mData1);
|
||||
AddResultString(line);
|
||||
const char* hx = mAnalyzer->PacketHexForFrame(frame_index);
|
||||
if (hx && hx[0])
|
||||
AddResultString(hx);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
snprintf(line, sizeof line, "? type=%u", static_cast<unsigned>(frame.mType));
|
||||
AddResultString(line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id)
|
||||
{
|
||||
(void)export_type_user_id;
|
||||
(void)display_base;
|
||||
std::ofstream file_stream(file, std::ios::out);
|
||||
|
||||
const U64 trigger_sample = mAnalyzer->GetTriggerSample();
|
||||
const U32 sample_rate = mAnalyzer->GetSampleRate();
|
||||
|
||||
file_stream << "Time[s],Type,Data1,bit_idx,err_low,err_high,err_other,Flags,Hex" << std::endl;
|
||||
|
||||
const U64 num_frames = GetNumFrames();
|
||||
for (U32 i = 0; i < num_frames; i++)
|
||||
{
|
||||
Frame frame = GetFrame(i);
|
||||
|
||||
char time_str[128];
|
||||
AnalyzerHelpers::GetTimeString(frame.mStartingSampleInclusive, trigger_sample, sample_rate, time_str, 128);
|
||||
|
||||
const char* typ = "?";
|
||||
switch (frame.mType)
|
||||
{
|
||||
case IRF_FT_DATA_BIT:
|
||||
typ = "D";
|
||||
break;
|
||||
case IRF_FT_SYNC_BIT:
|
||||
typ = "S";
|
||||
break;
|
||||
case IRF_FT_PACKET_OK:
|
||||
typ = "OK";
|
||||
break;
|
||||
case IRF_FT_PACKET_CRC_FAIL:
|
||||
typ = "CRC";
|
||||
break;
|
||||
case IRF_FT_OVERFLOW:
|
||||
typ = "OVF";
|
||||
break;
|
||||
case IRF_FT_ABORT:
|
||||
typ = "ABORT";
|
||||
break;
|
||||
case IRF_FT_PREAMBLE:
|
||||
typ = "PRE";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const char* hx = mAnalyzer->PacketHexForFrame(i);
|
||||
if (!hx)
|
||||
hx = "";
|
||||
|
||||
U64 bit_idx = 0;
|
||||
U32 err_l = 0, err_h = 0, err_o = 0;
|
||||
if (frame.mType == IRF_FT_DATA_BIT || frame.mType == IRF_FT_SYNC_BIT ||
|
||||
frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT)
|
||||
{
|
||||
bit_idx = frame.mData2 & 0xFFFFull;
|
||||
err_l = static_cast<U32>((frame.mData2 >> 16) & 0xFFull);
|
||||
err_h = static_cast<U32>((frame.mData2 >> 24) & 0xFFull);
|
||||
err_o = static_cast<U32>((frame.mData2 >> 32) & 0xFFull);
|
||||
}
|
||||
|
||||
file_stream << time_str << "," << typ << "," << frame.mData1 << "," << bit_idx << "," << err_l << "," << err_h
|
||||
<< "," << err_o << "," << static_cast<unsigned>(frame.mFlags) << "," << hx << std::endl;
|
||||
|
||||
if (UpdateExportProgressAndCheckForCancel(i, num_frames) == true)
|
||||
{
|
||||
file_stream.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
file_stream.close();
|
||||
}
|
||||
|
||||
void IrFoxAnalyzerResults::GenerateFrameTabularText(U64 frame_index, DisplayBase display_base)
|
||||
{
|
||||
#ifdef SUPPORTS_PROTOCOL_SEARCH
|
||||
(void)display_base;
|
||||
Frame frame = GetFrame(frame_index);
|
||||
ClearTabularText();
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof buf, "t%u", static_cast<unsigned>(frame.mType));
|
||||
AddTabularText(buf);
|
||||
snprintf(buf, sizeof buf, "%llu", (unsigned long long)frame.mData1);
|
||||
AddTabularText(buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
void IrFoxAnalyzerResults::GeneratePacketTabularText(U64 packet_id, DisplayBase display_base)
|
||||
{
|
||||
(void)packet_id;
|
||||
(void)display_base;
|
||||
}
|
||||
|
||||
void IrFoxAnalyzerResults::GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base)
|
||||
{
|
||||
(void)transaction_id;
|
||||
(void)display_base;
|
||||
}
|
||||
27
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h
Normal file
27
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerResults.h
Normal file
@ -0,0 +1,27 @@
|
||||
#ifndef IRFOX_ANALYZER_RESULTS
|
||||
#define IRFOX_ANALYZER_RESULTS
|
||||
|
||||
#include <AnalyzerResults.h>
|
||||
|
||||
class IrFoxAnalyzer;
|
||||
class IrFoxAnalyzerSettings;
|
||||
|
||||
class IrFoxAnalyzerResults : public AnalyzerResults
|
||||
{
|
||||
public:
|
||||
IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings);
|
||||
virtual ~IrFoxAnalyzerResults();
|
||||
|
||||
virtual void GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base);
|
||||
virtual void GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id);
|
||||
|
||||
virtual void GenerateFrameTabularText(U64 frame_index, DisplayBase display_base);
|
||||
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
|
||||
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
|
||||
|
||||
protected:
|
||||
IrFoxAnalyzerSettings* mSettings;
|
||||
IrFoxAnalyzer* mAnalyzer;
|
||||
};
|
||||
|
||||
#endif
|
||||
62
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp
Normal file
62
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
#include "IrFoxAnalyzerSettings.h"
|
||||
#include <AnalyzerHelpers.h>
|
||||
|
||||
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
|
||||
: mInputChannel(UNDEFINED_CHANNEL),
|
||||
mInputChannelInterface()
|
||||
{
|
||||
mInputChannelInterface.SetTitleAndTooltip(
|
||||
"IR",
|
||||
"Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)");
|
||||
mInputChannelInterface.SetChannel(mInputChannel);
|
||||
|
||||
AddInterface(&mInputChannelInterface);
|
||||
|
||||
AddExportOption(0, "Export as text/csv file");
|
||||
AddExportExtension(0, "text", "txt");
|
||||
AddExportExtension(0, "csv", "csv");
|
||||
|
||||
ClearChannels();
|
||||
AddChannel(mInputChannel, "IR", false);
|
||||
}
|
||||
|
||||
IrFoxAnalyzerSettings::~IrFoxAnalyzerSettings()
|
||||
{
|
||||
}
|
||||
|
||||
bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
|
||||
{
|
||||
mInputChannel = mInputChannelInterface.GetChannel();
|
||||
|
||||
ClearChannels();
|
||||
AddChannel(mInputChannel, "IR Fox", true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings()
|
||||
{
|
||||
mInputChannelInterface.SetChannel(mInputChannel);
|
||||
}
|
||||
|
||||
void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
|
||||
{
|
||||
SimpleArchive text_archive;
|
||||
text_archive.SetString(settings);
|
||||
|
||||
text_archive >> mInputChannel;
|
||||
|
||||
ClearChannels();
|
||||
AddChannel(mInputChannel, "IR Fox", true);
|
||||
|
||||
UpdateInterfacesFromSettings();
|
||||
}
|
||||
|
||||
const char* IrFoxAnalyzerSettings::SaveSettings()
|
||||
{
|
||||
SimpleArchive text_archive;
|
||||
|
||||
text_archive << mInputChannel;
|
||||
|
||||
return SetReturnString(text_archive.GetString());
|
||||
}
|
||||
24
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h
Normal file
24
Analyzer/raw/IR_Fox/src/IrFoxAnalyzerSettings.h
Normal file
@ -0,0 +1,24 @@
|
||||
#ifndef IRFOX_ANALYZER_SETTINGS
|
||||
#define IRFOX_ANALYZER_SETTINGS
|
||||
|
||||
#include <AnalyzerSettings.h>
|
||||
#include <AnalyzerTypes.h>
|
||||
|
||||
class IrFoxAnalyzerSettings : public AnalyzerSettings
|
||||
{
|
||||
public:
|
||||
IrFoxAnalyzerSettings();
|
||||
virtual ~IrFoxAnalyzerSettings();
|
||||
|
||||
virtual bool SetSettingsFromInterfaces();
|
||||
void UpdateInterfacesFromSettings();
|
||||
virtual void LoadSettings(const char* settings);
|
||||
virtual const char* SaveSettings();
|
||||
|
||||
Channel mInputChannel;
|
||||
|
||||
protected:
|
||||
AnalyzerSettingInterfaceChannel mInputChannelInterface;
|
||||
};
|
||||
|
||||
#endif
|
||||
593
Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp
Normal file
593
Analyzer/raw/IR_Fox/src/IrFoxDecoder.cpp
Normal file
@ -0,0 +1,593 @@
|
||||
#include "IrFoxDecoder.h"
|
||||
#include <AnalyzerResults.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
void IrFoxDecoder::reset()
|
||||
{
|
||||
*this = IrFoxDecoder{};
|
||||
rise_sync_time_us = irfox::kBitTimeUs;
|
||||
next_control_bit = irfox::kBitPerByte;
|
||||
have_last_processed = false;
|
||||
last_processed_edge_us = 0;
|
||||
}
|
||||
|
||||
uint16_t IrFoxDecoder::ceil_div_u16(uint16_t val, uint16_t divider)
|
||||
{
|
||||
if (divider == 0)
|
||||
return 0;
|
||||
int ret = val / divider;
|
||||
if ((val << 4) / divider - (ret << 4) >= 8)
|
||||
ret++;
|
||||
return static_cast<uint16_t>(ret);
|
||||
}
|
||||
|
||||
uint8_t IrFoxDecoder::crc8(const uint8_t* data, uint8_t start, uint8_t end, uint8_t poly)
|
||||
{
|
||||
uint8_t crc = 0xff;
|
||||
for (size_t i = start; i < end; i++)
|
||||
{
|
||||
crc ^= data[i];
|
||||
for (size_t j = 0; j < 8; j++)
|
||||
{
|
||||
if ((crc & 0x80) != 0)
|
||||
crc = static_cast<uint8_t>((crc << 1) ^ poly);
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out)
|
||||
{
|
||||
crc_out = 0;
|
||||
crc_out = static_cast<uint16_t>(static_cast<uint16_t>(crc8(data_buffer, 0, len, irfox::kPoly1) << 8) & 0xFF00u);
|
||||
crc_out = static_cast<uint16_t>(crc_out | (crc8(data_buffer, 0, static_cast<uint8_t>(len + 1), irfox::kPoly2) & 0xFFu));
|
||||
|
||||
const bool ok = (data_buffer[len] == static_cast<uint8_t>((crc_out >> 8) & 0xFF)) &&
|
||||
(data_buffer[len + 1] == static_cast<uint8_t>(crc_out & 0xFF));
|
||||
return ok;
|
||||
}
|
||||
|
||||
void IrFoxDecoder::first_rx()
|
||||
{
|
||||
err_low_signal = err_high_signal = err_other = 0;
|
||||
pack_size = 0;
|
||||
is_buffer_overflow = false;
|
||||
is_available = false;
|
||||
buf_bit_pos = 0;
|
||||
is_data = true;
|
||||
i_data_buffer = 0;
|
||||
next_control_bit = irfox::kBitPerByte;
|
||||
i_sync_bit = 0;
|
||||
err_sync_bit = 0;
|
||||
is_wrong_pack = false;
|
||||
is_preamb = true;
|
||||
is_recive = false;
|
||||
is_recive_raw = false;
|
||||
msg_type_receive = 0;
|
||||
rise_sync_time_us = irfox::kBitTimeUs;
|
||||
std::memset(data_buffer, 0, sizeof data_buffer);
|
||||
preamble_bubble_start_valid_ = false;
|
||||
trim_first_data_bit_cell_ = false;
|
||||
}
|
||||
|
||||
void IrFoxDecoder::listen_start(double t_us)
|
||||
{
|
||||
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
|
||||
// Как IR_DecoderRaw::listenStart: пауза по lastEdgeTime, не по prevRise.
|
||||
if (is_recive_raw && last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0)
|
||||
{
|
||||
is_recive_raw = false;
|
||||
first_rx();
|
||||
}
|
||||
}
|
||||
|
||||
void IrFoxDecoder::check_timeout(double t_us)
|
||||
{
|
||||
if (!is_recive)
|
||||
return;
|
||||
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
|
||||
if (t_us - last_edge_time_us > irmax * 2.0)
|
||||
{
|
||||
// Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM.
|
||||
is_recive = false;
|
||||
msg_type_receive = 0;
|
||||
is_recive_raw = false;
|
||||
first_rx();
|
||||
// Не last_edge_time_us = t_us: как IR_DecoderRaw — не расходить с «хвостом» фронтов.
|
||||
}
|
||||
}
|
||||
|
||||
void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
|
||||
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, IrFoxEmitBitMode emit_mode)
|
||||
{
|
||||
if (i_data_buffer > irfox::kDataByteSizeMax * 8u)
|
||||
{
|
||||
if (!is_buffer_overflow && on_bit)
|
||||
{
|
||||
IrFoxEmitBit e{};
|
||||
e.start_sample = static_cast<int64_t>(cell_start_s);
|
||||
e.end_sample = static_cast<int64_t>(cell_end_s);
|
||||
e.frame_type = IRF_FT_OVERFLOW;
|
||||
e.mflags = DISPLAY_AS_ERROR_FLAG;
|
||||
fill_err_snapshot(e);
|
||||
std::strncpy(e.bubble_text, "OVF", sizeof e.bubble_text);
|
||||
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
|
||||
on_bit(e);
|
||||
}
|
||||
is_buffer_overflow = true;
|
||||
}
|
||||
|
||||
if (is_buffer_overflow || is_preamb || is_wrong_pack)
|
||||
{
|
||||
// Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма.
|
||||
first_rx();
|
||||
return;
|
||||
}
|
||||
|
||||
if (buf_bit_pos == next_control_bit)
|
||||
{
|
||||
next_control_bit = next_control_bit + (is_data ? irfox::kSyncBits : irfox::kBitPerByte);
|
||||
is_data = !is_data;
|
||||
i_sync_bit = 0;
|
||||
err_sync_bit = 0;
|
||||
}
|
||||
|
||||
if (is_data)
|
||||
{
|
||||
const bool was_first_data_bit = (i_data_buffer == 0);
|
||||
data_buffer[i_data_buffer / 8] |= static_cast<uint8_t>(bit ? 1 : 0) << (7 - (i_data_buffer % 8));
|
||||
i_data_buffer++;
|
||||
buf_bit_pos++;
|
||||
|
||||
uint8_t fl = 0;
|
||||
if (pack_trace_invert_fix)
|
||||
fl |= DISPLAY_AS_WARNING_FLAG;
|
||||
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble)
|
||||
{
|
||||
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_DATA_BIT,
|
||||
bit ? 1u : 0u, static_cast<uint64_t>(i_data_buffer - 1), fl, pack_trace_invert_fix, 0, 0, 0};
|
||||
fill_err_snapshot(e);
|
||||
e.bubble_text[0] = static_cast<char>(bit ? '1' : '0');
|
||||
e.bubble_text[1] = '\0';
|
||||
on_bit(e);
|
||||
}
|
||||
if (was_first_data_bit && trim_first_data_bit_cell_)
|
||||
trim_first_data_bit_cell_ = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i_sync_bit == 0)
|
||||
{
|
||||
const bool last_data_bit =
|
||||
(data_buffer[((i_data_buffer - 1) / 8)] >> (7 - ((i_data_buffer - 1) % 8))) & 1;
|
||||
if (bit != static_cast<bool>(last_data_bit))
|
||||
{
|
||||
buf_bit_pos++;
|
||||
i_sync_bit++;
|
||||
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble)
|
||||
{
|
||||
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_SYNC_BIT,
|
||||
bit ? 1u : 0u, static_cast<uint64_t>(buf_bit_pos), 0, false, 0, 0, 0};
|
||||
fill_err_snapshot(e);
|
||||
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %c", bit ? '1' : '0');
|
||||
on_bit(e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
i_sync_bit = 0;
|
||||
err_other++;
|
||||
err_sync_bit++;
|
||||
const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits);
|
||||
if (fatal_sync)
|
||||
is_wrong_pack = true;
|
||||
if (on_bit && fatal_sync)
|
||||
{
|
||||
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_ABORT,
|
||||
0, 0, DISPLAY_AS_ERROR_FLAG, false, 0, 0, 0};
|
||||
fill_err_snapshot(e);
|
||||
std::strncpy(e.bubble_text, "SYNC!", sizeof e.bubble_text);
|
||||
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
|
||||
on_bit(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buf_bit_pos++;
|
||||
i_sync_bit++;
|
||||
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble)
|
||||
{
|
||||
IrFoxEmitBit e{static_cast<int64_t>(cell_start_s), static_cast<int64_t>(cell_end_s), IRF_FT_SYNC_BIT,
|
||||
bit ? 1u : 0u, static_cast<uint64_t>(buf_bit_pos), 0, false, 0, 0, 0};
|
||||
fill_err_snapshot(e);
|
||||
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %c", bit ? '1' : '0');
|
||||
on_bit(e);
|
||||
}
|
||||
}
|
||||
is_wrong_pack = (err_sync_bit >= irfox::kSyncBits);
|
||||
}
|
||||
|
||||
if (!is_available && is_data && !is_wrong_pack)
|
||||
{
|
||||
if (i_data_buffer == 8 * irfox::kMsgBytes)
|
||||
pack_size = static_cast<uint16_t>(data_buffer[0] & 0x1Fu);
|
||||
|
||||
if (pack_size && (i_data_buffer == 8))
|
||||
msg_type_receive = static_cast<uint8_t>((data_buffer[0] >> 5) | 0xF8u);
|
||||
|
||||
if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte))
|
||||
{
|
||||
uint16_t crc_computed = 0;
|
||||
const bool crc_ok = crc_check(static_cast<uint8_t>(pack_size - irfox::kCrcBytes), crc_computed);
|
||||
crc_value = crc_computed;
|
||||
is_recive = false;
|
||||
is_recive_raw = false;
|
||||
msg_type_receive = 0;
|
||||
is_available = crc_ok;
|
||||
|
||||
IrFoxEmitPacket pkt{};
|
||||
pkt.start_sample = static_cast<int64_t>(cell_start_s);
|
||||
pkt.end_sample = static_cast<int64_t>(cell_end_s);
|
||||
pkt.crc_ok = crc_ok;
|
||||
pkt.pack_size = static_cast<uint8_t>(pack_size);
|
||||
pkt.err_low = err_low_signal;
|
||||
pkt.err_high = err_high_signal;
|
||||
pkt.err_other = err_other;
|
||||
if (pack_size > 0 && pack_size <= irfox::kDataByteSizeMax)
|
||||
std::memcpy(pkt.data_bytes, data_buffer, pack_size);
|
||||
if (on_pkt)
|
||||
on_pkt(pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit,
|
||||
const IrFoxOnPacket& on_pkt)
|
||||
{
|
||||
const double t_us = sample_to_us(sample, fs);
|
||||
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
|
||||
uint32_t rise_min_us = rise_sync_time_us > irfox::kToleranceUs ? rise_sync_time_us - irfox::kToleranceUs : 0U;
|
||||
|
||||
listen_start(t_us);
|
||||
|
||||
// Как IR_DecoderRaw: пауза между фронтами по lastEdgeTime при активном приёме кадра.
|
||||
if (last_edge_time_us > 0.0 && (t_us - last_edge_time_us) > irmax * 2.0 && is_recive)
|
||||
check_timeout(t_us);
|
||||
|
||||
last_edge_time_us = t_us;
|
||||
last_edge_sample = sample;
|
||||
|
||||
const uint32_t rise_max_us = rise_sync_time_us + irfox::kToleranceUs;
|
||||
|
||||
/** Визуализация: начало PRE с ближайшего спада в пределах ~3 битовых периодов (ИК-метка). */
|
||||
auto new_bubble_preamble_start = [&](uint64_t edge_s, bool is_rising) -> uint64_t {
|
||||
if (!is_rising)
|
||||
return edge_s;
|
||||
if (edge_s > prev_fall_sample)
|
||||
{
|
||||
const double span_us = double(edge_s - prev_fall_sample) * 1e6 / double(fs);
|
||||
const double max_us = double(rise_max_us) * 3.0;
|
||||
if (span_us <= max_us)
|
||||
return prev_fall_sample;
|
||||
}
|
||||
return edge_s;
|
||||
};
|
||||
|
||||
if (rising)
|
||||
{
|
||||
const double delta_rp = t_us - prev_rise_us;
|
||||
const uint32_t cand_rp = static_cast<uint32_t>(delta_rp);
|
||||
const uint32_t cand_ht = static_cast<uint32_t>(t_us - prev_fall_us);
|
||||
const uint32_t cand_lt = static_cast<uint32_t>(prev_fall_us - prev_rise_us);
|
||||
|
||||
#if IRFOX_SHORT_LOW_GLITCH_REJECT
|
||||
const bool short_low_glitch =
|
||||
is_recive && !is_preamb && cand_ht < (rise_min_us / 8U) && cand_lt >= rise_min_us &&
|
||||
cand_rp >= rise_min_us && cand_rp <= irmax;
|
||||
if (short_low_glitch)
|
||||
{
|
||||
err_other++;
|
||||
irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us);
|
||||
last_processed_edge_us = t_us;
|
||||
have_last_processed = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if IRFOX_MICRO_GAP_RISE_REJECT
|
||||
const bool micro_gap_cand_lt_ok =
|
||||
(cand_lt >= rise_min_us) || (cand_lt >= (rise_min_us / 4U) && cand_lt < rise_min_us);
|
||||
const bool micro_gap_rise = is_recive && !is_preamb && cand_ht < (rise_min_us / 8U) && micro_gap_cand_lt_ok &&
|
||||
cand_rp >= (rise_min_us / 4U) && cand_rp < rise_min_us && cand_rp <= irmax;
|
||||
if (micro_gap_rise)
|
||||
{
|
||||
err_other++;
|
||||
irfox::irfoxGlitchPhaseNudgeUs(t_us, rise_sync_time_us, prev_rise_us);
|
||||
last_processed_edge_us = t_us;
|
||||
have_last_processed = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (cand_rp <= rise_max_us / 4U && !high_count && !low_count)
|
||||
{
|
||||
err_other++;
|
||||
last_processed_edge_us = t_us;
|
||||
have_last_processed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Визуализация PRE: длинная пауза, первый подъём — якорь от спада метки (декод как STM32DMA).
|
||||
if (cand_rp > irmax * 2U && !is_recive_raw)
|
||||
{
|
||||
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, true);
|
||||
preamble_bubble_start_valid_ = true;
|
||||
}
|
||||
|
||||
const bool accept_rise_timing =
|
||||
(delta_rp > static_cast<double>(rise_max_us) / 4.0) || high_count != 0 || low_count != 0;
|
||||
if (accept_rise_timing)
|
||||
{
|
||||
rise_period_anchor_sample_ = prev_rise_sample;
|
||||
rise_period_us = cand_rp;
|
||||
high_time_us = cand_ht;
|
||||
low_time_us = cand_lt;
|
||||
prev_rise_us = t_us;
|
||||
prev_rise_sample = sample;
|
||||
}
|
||||
else
|
||||
{
|
||||
err_other++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (t_us - prev_fall_us > rise_min_us / 4.0)
|
||||
{
|
||||
prev_fall_us = t_us;
|
||||
prev_fall_sample = sample;
|
||||
}
|
||||
else
|
||||
{
|
||||
err_other++;
|
||||
}
|
||||
}
|
||||
|
||||
// Как IR_DecoderRaw::tick: после длинной паузы старт сырого приёма (без отдельного firstRX — флаги ниже).
|
||||
if (t_us > prev_rise_us && (t_us - prev_rise_us) > irmax * 2.0 && !is_recive_raw)
|
||||
{
|
||||
preamb_front_counter = static_cast<int8_t>(irfox::kPreambFronts - 1);
|
||||
is_preamb = true;
|
||||
is_recive = true;
|
||||
is_recive_raw = true;
|
||||
is_wrong_pack = false;
|
||||
if (!preamble_bubble_start_valid_)
|
||||
{
|
||||
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising);
|
||||
preamble_bubble_start_valid_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (preamb_front_counter)
|
||||
{
|
||||
if (rising && rise_period_us < irmax)
|
||||
{
|
||||
if (rise_period_us < rise_min_us / 2U)
|
||||
{
|
||||
preamb_front_counter += 2;
|
||||
err_other++;
|
||||
}
|
||||
}
|
||||
preamb_front_counter--;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (is_preamb)
|
||||
{
|
||||
is_preamb = false;
|
||||
// IR_DecoderRaw: prevRise += risePeriod / 2 — фаза как в прошивке.
|
||||
// Бабл PRE: до текущего фронта (sample−1), чтобы охватить все kPreambPulse периодов (3 импульса),
|
||||
// а не только до предыдущего подъёма (~2 периода).
|
||||
const uint64_t preamble_bubble_end_sample = sample > 0 ? sample - 1 : sample;
|
||||
prev_rise_us += rise_period_us / 2.0;
|
||||
{
|
||||
const double half_us = 0.5 * static_cast<double>(rise_period_us);
|
||||
const uint64_t half_s = static_cast<uint64_t>(std::llround(half_us * double(fs) / 1e6));
|
||||
prev_rise_sample += half_s;
|
||||
}
|
||||
trim_first_data_bit_cell_ = true;
|
||||
if (on_bit && preamble_bubble_start_valid_)
|
||||
{
|
||||
int64_t pe_start = static_cast<int64_t>(preamble_bubble_start_sample_);
|
||||
int64_t pe_end = static_cast<int64_t>(preamble_bubble_end_sample);
|
||||
if (preamble_bubble_end_sample == 0 || pe_end < pe_start)
|
||||
pe_end = static_cast<int64_t>(sample > 0 ? sample - 1 : sample);
|
||||
IrFoxEmitBit pe{};
|
||||
pe.start_sample = pe_start;
|
||||
pe.end_sample = pe_end;
|
||||
pe.frame_type = IRF_FT_PREAMBLE;
|
||||
fill_err_snapshot(pe);
|
||||
std::strncpy(pe.bubble_text, "PRE", sizeof pe.bubble_text);
|
||||
pe.bubble_text[sizeof pe.bubble_text - 1] = '\0';
|
||||
on_bit(pe);
|
||||
}
|
||||
preamble_bubble_start_valid_ = false;
|
||||
last_processed_edge_us = t_us;
|
||||
have_last_processed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_preamb)
|
||||
{
|
||||
last_processed_edge_us = t_us;
|
||||
have_last_processed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (rise_period_us > irmax || is_buffer_overflow || rise_period_us < rise_min_us || is_wrong_pack)
|
||||
{
|
||||
last_processed_edge_us = t_us;
|
||||
have_last_processed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (rising)
|
||||
{
|
||||
high_count = low_count = all_count = 0;
|
||||
bool invert_err = false;
|
||||
|
||||
uint64_t cell_start_s = rise_period_anchor_sample_;
|
||||
const uint64_t cell_end_s = sample > 0 ? sample - 1 : sample;
|
||||
// После prev_rise += half period якорь может оказаться близко к текущему подъёму;
|
||||
// max(anchor, prev_fall) > cell_end даёт пустой интервал — бабл первого бита пропадает.
|
||||
if (trim_first_data_bit_cell_ && is_data && i_data_buffer == 0)
|
||||
{
|
||||
const uint64_t trimmed = std::max(cell_start_s, prev_fall_sample);
|
||||
if (trimmed <= cell_end_s)
|
||||
cell_start_s = trimmed;
|
||||
}
|
||||
|
||||
if (irfox::aroundRisePeriod(rise_period_us, rise_sync_time_us))
|
||||
{
|
||||
if (high_time_us > low_time_us)
|
||||
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
|
||||
else
|
||||
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint16_t hc = ceil_div_u16(static_cast<uint16_t>(high_time_us > 0xFFFF ? 0xFFFF : high_time_us),
|
||||
static_cast<uint16_t>(rise_sync_time_us));
|
||||
uint16_t lc = ceil_div_u16(static_cast<uint16_t>(low_time_us > 0xFFFF ? 0xFFFF : low_time_us),
|
||||
static_cast<uint16_t>(rise_sync_time_us));
|
||||
uint16_t ac = ceil_div_u16(static_cast<uint16_t>(rise_period_us > 0xFFFF ? 0xFFFF : rise_period_us),
|
||||
static_cast<uint16_t>(rise_sync_time_us));
|
||||
high_count = static_cast<int8_t>(hc > 127 ? 127 : hc);
|
||||
low_count = static_cast<int8_t>(lc > 127 ? 127 : lc);
|
||||
all_count = static_cast<int8_t>(ac > 127 ? 127 : ac);
|
||||
|
||||
if (high_count == 0 && high_time_us > rise_sync_time_us / 3U)
|
||||
{
|
||||
high_count++;
|
||||
err_other++;
|
||||
}
|
||||
|
||||
if (low_count + high_count > all_count)
|
||||
{
|
||||
if (low_count > high_count)
|
||||
{
|
||||
low_count = static_cast<int8_t>(all_count - high_count);
|
||||
err_low_signal = static_cast<uint8_t>(err_low_signal + static_cast<uint8_t>(low_count));
|
||||
}
|
||||
else if (low_count < high_count)
|
||||
{
|
||||
high_count = static_cast<int8_t>(all_count - low_count);
|
||||
err_high_signal = static_cast<uint8_t>(err_high_signal + static_cast<uint8_t>(high_count));
|
||||
}
|
||||
else if (low_count == high_count)
|
||||
{
|
||||
invert_err = true;
|
||||
err_other = static_cast<uint8_t>(err_other + static_cast<uint8_t>(all_count));
|
||||
}
|
||||
}
|
||||
|
||||
if (low_count < high_count)
|
||||
err_high_signal = static_cast<uint8_t>(err_high_signal + static_cast<uint8_t>(high_count));
|
||||
else
|
||||
err_low_signal = static_cast<uint8_t>(err_low_signal + static_cast<uint8_t>(low_count));
|
||||
|
||||
const bool burst_is_data_start = is_data;
|
||||
const uint64_t merge_bit_index =
|
||||
burst_is_data_start ? static_cast<uint64_t>(i_data_buffer) : static_cast<uint64_t>(buf_bit_pos + 1);
|
||||
char s_bits[20]{};
|
||||
char d_bits[20]{};
|
||||
size_t s_n = 0;
|
||||
size_t d_n = 0;
|
||||
int first_merge_bit = -1;
|
||||
bool merge_warn = false;
|
||||
auto append_merge = [&](bool as_data, bool bitv) {
|
||||
if (first_merge_bit < 0)
|
||||
first_merge_bit = bitv ? 1 : 0;
|
||||
char* buf = as_data ? d_bits : s_bits;
|
||||
size_t& n = as_data ? d_n : s_n;
|
||||
if (n + 1 < sizeof s_bits)
|
||||
buf[n++] = static_cast<char>(bitv ? '1' : '0');
|
||||
};
|
||||
auto emit_merge_if_needed = [&]() {
|
||||
if ((s_n == 0 && d_n == 0) || !on_bit)
|
||||
return;
|
||||
IrFoxEmitBit e{};
|
||||
e.start_sample = static_cast<int64_t>(cell_start_s);
|
||||
e.end_sample = static_cast<int64_t>(cell_end_s);
|
||||
e.frame_type = (d_n > 0) ? IRF_FT_DATA_BIT : IRF_FT_SYNC_BIT;
|
||||
e.bit_value = (first_merge_bit > 0) ? 1u : 0u;
|
||||
e.bit_index = merge_bit_index;
|
||||
e.mflags = merge_warn ? DISPLAY_AS_WARNING_FLAG : 0;
|
||||
e.invert_fix = merge_warn;
|
||||
fill_err_snapshot(e);
|
||||
s_bits[s_n] = '\0';
|
||||
d_bits[d_n] = '\0';
|
||||
if (s_n && d_n)
|
||||
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %s d: %s", s_bits, d_bits);
|
||||
else if (s_n)
|
||||
std::snprintf(e.bubble_text, sizeof e.bubble_text, "s: %s", s_bits);
|
||||
else
|
||||
std::memcpy(e.bubble_text, d_bits, d_n + 1);
|
||||
on_bit(e);
|
||||
};
|
||||
|
||||
for (int8_t i = 0; i < low_count && 8 - i; i++)
|
||||
{
|
||||
const bool row_is_data = is_data;
|
||||
if (i == low_count - 1 && invert_err)
|
||||
{
|
||||
invert_err = false;
|
||||
write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||
merge_warn = true;
|
||||
append_merge(row_is_data, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||
append_merge(row_is_data, false);
|
||||
}
|
||||
}
|
||||
|
||||
for (int8_t i = 0; i < high_count && 8 - i; i++)
|
||||
{
|
||||
const bool row_is_data = is_data;
|
||||
if (i == high_count - 1 && invert_err)
|
||||
{
|
||||
invert_err = false;
|
||||
write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||
merge_warn = true;
|
||||
append_merge(row_is_data, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::Quiet);
|
||||
append_merge(row_is_data, true);
|
||||
}
|
||||
}
|
||||
|
||||
emit_merge_if_needed();
|
||||
}
|
||||
}
|
||||
|
||||
last_processed_edge_us = t_us;
|
||||
have_last_processed = true;
|
||||
}
|
||||
|
||||
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt)
|
||||
{
|
||||
const double t_us = sample_to_us(last_sample, fs);
|
||||
listen_start(t_us);
|
||||
check_timeout(t_us);
|
||||
(void)on_bit;
|
||||
(void)on_pkt;
|
||||
}
|
||||
135
Analyzer/raw/IR_Fox/src/IrFoxDecoder.h
Normal file
135
Analyzer/raw/IR_Fox/src/IrFoxDecoder.h
Normal file
@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include "IrFoxProtocolConstants.h"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
enum IrFoxFrameType : uint8_t
|
||||
{
|
||||
IRF_FT_DATA_BIT = 1,
|
||||
IRF_FT_SYNC_BIT = 2,
|
||||
IRF_FT_PACKET_OK = 3,
|
||||
IRF_FT_PACKET_CRC_FAIL = 4,
|
||||
IRF_FT_OVERFLOW = 5,
|
||||
IRF_FT_ABORT = 6,
|
||||
IRF_FT_PREAMBLE = 7,
|
||||
};
|
||||
|
||||
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
|
||||
enum class IrFoxEmitBitMode : uint8_t
|
||||
{
|
||||
WithBubble,
|
||||
Quiet,
|
||||
};
|
||||
|
||||
struct IrFoxEmitBit
|
||||
{
|
||||
int64_t start_sample;
|
||||
int64_t end_sample;
|
||||
uint8_t frame_type;
|
||||
uint64_t bit_value;
|
||||
uint64_t bit_index;
|
||||
uint8_t mflags;
|
||||
bool invert_fix;
|
||||
uint8_t err_low;
|
||||
uint8_t err_high;
|
||||
uint8_t err_other;
|
||||
/** Подпись бабла: "0", "1" или склейка "001"; пусто — смотреть bit_value. */
|
||||
char bubble_text[32]{};
|
||||
};
|
||||
|
||||
struct IrFoxEmitPacket
|
||||
{
|
||||
int64_t start_sample;
|
||||
int64_t end_sample;
|
||||
bool crc_ok;
|
||||
uint8_t pack_size;
|
||||
uint8_t err_low;
|
||||
uint8_t err_high;
|
||||
uint8_t err_other;
|
||||
uint8_t data_bytes[irfox::kDataByteSizeMax];
|
||||
};
|
||||
|
||||
using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>;
|
||||
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
|
||||
|
||||
class IrFoxDecoder
|
||||
{
|
||||
public:
|
||||
void reset();
|
||||
void processEdge(uint64_t sample, bool rising, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit,
|
||||
const IrFoxOnPacket& on_pkt);
|
||||
void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt);
|
||||
|
||||
private:
|
||||
static uint16_t ceil_div_u16(uint16_t val, uint16_t divider);
|
||||
static uint8_t crc8(const uint8_t* data, uint8_t start, uint8_t end, uint8_t poly);
|
||||
bool crc_check(uint8_t len, uint16_t& crc_out);
|
||||
|
||||
void first_rx();
|
||||
void listen_start(double t_us);
|
||||
void check_timeout(double t_us);
|
||||
void write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
|
||||
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
|
||||
IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble);
|
||||
|
||||
double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); }
|
||||
|
||||
// --- state (mirror IR_DecoderRaw) ---
|
||||
uint8_t data_buffer[irfox::kDataByteSizeMax]{};
|
||||
uint8_t err_low_signal = 0;
|
||||
uint8_t err_high_signal = 0;
|
||||
uint8_t err_other = 0;
|
||||
|
||||
bool is_available = false;
|
||||
uint16_t pack_size = 0;
|
||||
uint16_t crc_value = 0;
|
||||
bool is_recive = false;
|
||||
bool is_recive_raw = false;
|
||||
bool is_preamb = false;
|
||||
bool is_buffer_overflow = false;
|
||||
bool is_wrong_pack = false;
|
||||
|
||||
uint32_t rise_sync_time_us = irfox::kBitTimeUs;
|
||||
|
||||
double prev_rise_us = 0;
|
||||
double prev_fall_us = 0;
|
||||
uint64_t prev_rise_sample = 0;
|
||||
uint64_t prev_fall_sample = 0;
|
||||
/** Сэмпл предыдущего нарастающего фронта до обновления на текущем тике (граница ячейки бита, см. IR_DecoderRaw::tick). */
|
||||
uint64_t rise_period_anchor_sample_ = 0;
|
||||
uint64_t preamble_bubble_start_sample_ = 0;
|
||||
bool preamble_bubble_start_valid_ = false;
|
||||
bool trim_first_data_bit_cell_ = false;
|
||||
|
||||
double last_edge_time_us = 0;
|
||||
uint64_t last_edge_sample = 0;
|
||||
double last_processed_edge_us = 0;
|
||||
bool have_last_processed = false;
|
||||
|
||||
void fill_err_snapshot(IrFoxEmitBit& e) const
|
||||
{
|
||||
e.err_low = err_low_signal;
|
||||
e.err_high = err_high_signal;
|
||||
e.err_other = err_other;
|
||||
}
|
||||
|
||||
uint32_t rise_period_us = 0;
|
||||
uint32_t high_time_us = 0;
|
||||
uint32_t low_time_us = 0;
|
||||
|
||||
int8_t high_count = 0;
|
||||
int8_t low_count = 0;
|
||||
int8_t all_count = 0;
|
||||
|
||||
uint16_t wrong_counter = 0;
|
||||
int8_t preamb_front_counter = 0;
|
||||
int16_t buf_bit_pos = 0;
|
||||
bool is_data = true;
|
||||
uint16_t i_data_buffer = 0;
|
||||
uint16_t next_control_bit = irfox::kBitPerByte;
|
||||
uint8_t i_sync_bit = 0;
|
||||
uint8_t err_sync_bit = 0;
|
||||
uint16_t error_counter = 0;
|
||||
uint8_t msg_type_receive = 0;
|
||||
};
|
||||
71
Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h
Normal file
71
Analyzer/raw/IR_Fox/src/IrFoxProtocolConstants.h
Normal file
@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace irfox {
|
||||
|
||||
constexpr uint32_t kCarrierPeriodUs = 1000000U / 38000U;
|
||||
constexpr uint32_t kBitActiveTakts = 25U;
|
||||
constexpr uint32_t kBitPauseTakts = 12U;
|
||||
constexpr uint32_t kBitTakts = kBitActiveTakts + kBitPauseTakts;
|
||||
constexpr uint32_t kBitTimeUs = kBitTakts * kCarrierPeriodUs;
|
||||
constexpr uint32_t kToleranceUs = 300U;
|
||||
|
||||
/** Мин. длительность плато (мкс) для потокового анти-глитча в анализаторе; согласовано с IR_INPUT_MIN_PULSE_US. */
|
||||
constexpr uint32_t kMinFilteredPulseUs = 10U;
|
||||
|
||||
constexpr uint8_t kBitPerByte = 8U;
|
||||
constexpr uint8_t kMsgBytes = 1;
|
||||
constexpr uint8_t kAddrBytes = 2;
|
||||
constexpr uint8_t kCrcBytes = 2;
|
||||
constexpr uint8_t kPoly1 = 0x31;
|
||||
constexpr uint8_t kPoly2 = 0x8C;
|
||||
constexpr uint8_t kSyncBits = 3U;
|
||||
constexpr uint8_t kBytePerPack = 31;
|
||||
constexpr uint8_t kDataByteSizeMax =
|
||||
static_cast<uint8_t>(kMsgBytes + kAddrBytes + kAddrBytes + kBytePerPack + kCrcBytes);
|
||||
|
||||
constexpr uint8_t kPreambPulse = 3;
|
||||
constexpr uint8_t kPreambFronts = kPreambPulse * 2U;
|
||||
|
||||
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
|
||||
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
|
||||
#define IRFOX_SHORT_LOW_GLITCH_REJECT 1
|
||||
#endif
|
||||
#ifndef IRFOX_GLITCH_REJECT_PHASE_NUDGE
|
||||
#define IRFOX_GLITCH_REJECT_PHASE_NUDGE 1
|
||||
#endif
|
||||
#ifndef IRFOX_MICRO_GAP_RISE_REJECT
|
||||
#define IRFOX_MICRO_GAP_RISE_REJECT 1
|
||||
#endif
|
||||
|
||||
inline uint32_t irTimeoutUs(uint32_t riseSyncTimeUs)
|
||||
{
|
||||
const uint32_t riseMax = riseSyncTimeUs + kToleranceUs;
|
||||
return riseMax * (8U + kSyncBits + 1U);
|
||||
}
|
||||
|
||||
/** Как IR_DecoderRaw.h: aroundRise(t) → riseTimeMin < t && t < riseTimeMax (ветка STM32DMA). */
|
||||
inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs)
|
||||
{
|
||||
const uint32_t lo = riseSyncTimeUs > kToleranceUs ? riseSyncTimeUs - kToleranceUs : 0U;
|
||||
const uint32_t hi = riseSyncTimeUs + kToleranceUs;
|
||||
return lo < periodUs && periodUs < hi;
|
||||
}
|
||||
|
||||
inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us)
|
||||
{
|
||||
#if IRFOX_GLITCH_REJECT_PHASE_NUDGE
|
||||
if (!(edge_us > static_cast<double>(rise_sync_us)))
|
||||
return;
|
||||
const double nudged = edge_us - static_cast<double>(rise_sync_us);
|
||||
if (nudged > prev_rise_us && nudged < edge_us)
|
||||
prev_rise_us = nudged;
|
||||
#else
|
||||
(void)edge_us;
|
||||
(void)rise_sync_us;
|
||||
(void)prev_rise_us;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace irfox
|
||||
67
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp
Normal file
67
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.cpp
Normal file
@ -0,0 +1,67 @@
|
||||
#include "IrFoxSimulationDataGenerator.h"
|
||||
#include "IrFoxAnalyzerSettings.h"
|
||||
#include <AnalyzerHelpers.h>
|
||||
|
||||
IrFoxSimulationDataGenerator::IrFoxSimulationDataGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
IrFoxSimulationDataGenerator::~IrFoxSimulationDataGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
void IrFoxSimulationDataGenerator::Initialize(U32 simulation_sample_rate, IrFoxAnalyzerSettings* settings)
|
||||
{
|
||||
mSimulationSampleRateHz = simulation_sample_rate;
|
||||
mSettings = settings;
|
||||
|
||||
mIrSimulationData.SetChannel(mSettings->mInputChannel);
|
||||
mIrSimulationData.SetSampleRate(simulation_sample_rate);
|
||||
// Как у «покоя» на выходе TSOP: линия подтянута вверх
|
||||
mIrSimulationData.SetInitialBitState(BIT_HIGH);
|
||||
}
|
||||
|
||||
void IrFoxSimulationDataGenerator::EmitIdle(U32 samples)
|
||||
{
|
||||
mIrSimulationData.Advance(samples);
|
||||
}
|
||||
|
||||
void IrFoxSimulationDataGenerator::EmitLow(U32 samples)
|
||||
{
|
||||
mIrSimulationData.TransitionIfNeeded(BIT_LOW);
|
||||
mIrSimulationData.Advance(samples);
|
||||
}
|
||||
|
||||
void IrFoxSimulationDataGenerator::EmitHigh(U32 samples)
|
||||
{
|
||||
mIrSimulationData.TransitionIfNeeded(BIT_HIGH);
|
||||
mIrSimulationData.Advance(samples);
|
||||
}
|
||||
|
||||
U32 IrFoxSimulationDataGenerator::GenerateSimulationData(U64 largest_sample_requested, U32 sample_rate,
|
||||
SimulationChannelDescriptor** simulation_channel)
|
||||
{
|
||||
const U64 adjusted_largest_sample_requested =
|
||||
AnalyzerHelpers::AdjustSimulationTargetSample(largest_sample_requested, sample_rate, mSimulationSampleRateHz);
|
||||
|
||||
// Упрощённый «пакет»: несколько импульсов вниз (активный уровень приёмника).
|
||||
while (mIrSimulationData.GetCurrentSampleNumber() < adjusted_largest_sample_requested)
|
||||
{
|
||||
const U32 us_to_samples = mSimulationSampleRateHz / 1000000;
|
||||
if (us_to_samples == 0)
|
||||
break;
|
||||
|
||||
EmitIdle(500 * us_to_samples);
|
||||
EmitLow(4500 * us_to_samples);
|
||||
EmitHigh(4500 * us_to_samples);
|
||||
EmitLow(4500 * us_to_samples);
|
||||
EmitHigh(4500 * us_to_samples);
|
||||
EmitLow(560 * us_to_samples);
|
||||
EmitHigh(560 * us_to_samples);
|
||||
EmitLow(560 * us_to_samples);
|
||||
EmitHigh(20000 * us_to_samples);
|
||||
}
|
||||
|
||||
*simulation_channel = &mIrSimulationData;
|
||||
return 1;
|
||||
}
|
||||
27
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h
Normal file
27
Analyzer/raw/IR_Fox/src/IrFoxSimulationDataGenerator.h
Normal file
@ -0,0 +1,27 @@
|
||||
#ifndef IRFOX_SIMULATION_DATA_GENERATOR
|
||||
#define IRFOX_SIMULATION_DATA_GENERATOR
|
||||
|
||||
#include <SimulationChannelDescriptor.h>
|
||||
|
||||
class IrFoxAnalyzerSettings;
|
||||
|
||||
class IrFoxSimulationDataGenerator
|
||||
{
|
||||
public:
|
||||
IrFoxSimulationDataGenerator();
|
||||
~IrFoxSimulationDataGenerator();
|
||||
|
||||
void Initialize(U32 simulation_sample_rate, IrFoxAnalyzerSettings* settings);
|
||||
U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, SimulationChannelDescriptor** simulation_channel);
|
||||
|
||||
protected:
|
||||
IrFoxAnalyzerSettings* mSettings;
|
||||
U32 mSimulationSampleRateHz;
|
||||
SimulationChannelDescriptor mIrSimulationData;
|
||||
|
||||
void EmitIdle(U32 samples);
|
||||
void EmitLow(U32 samples);
|
||||
void EmitHigh(U32 samples);
|
||||
};
|
||||
|
||||
#endif
|
||||
52
Analyzer/raw/PulseLengthStat/.github/workflows/build.yml
vendored
Normal file
52
Analyzer/raw/PulseLengthStat/.github/workflows/build.yml
vendored
Normal file
@ -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
|
||||
3
Analyzer/raw/PulseLengthStat/.gitignore
vendored
Normal file
3
Analyzer/raw/PulseLengthStat/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/build
|
||||
/build-nmake
|
||||
.DS_Store
|
||||
24
Analyzer/raw/PulseLengthStat/CMakeLists.txt
Normal file
24
Analyzer/raw/PulseLengthStat/CMakeLists.txt
Normal file
@ -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})
|
||||
49
Analyzer/raw/PulseLengthStat/CMakePresets.json
Normal file
49
Analyzer/raw/PulseLengthStat/CMakePresets.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
Analyzer/raw/PulseLengthStat/build_msvc.bat
Normal file
21
Analyzer/raw/PulseLengthStat/build_msvc.bat
Normal file
@ -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%
|
||||
66
Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake
Normal file
66
Analyzer/raw/PulseLengthStat/cmake/ExternalAnalyzerSDK.cmake
Normal file
@ -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()
|
||||
39
Analyzer/raw/PulseLengthStat/configure_msvc.bat
Normal file
39
Analyzer/raw/PulseLengthStat/configure_msvc.bat
Normal file
@ -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
|
||||
110
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp
Normal file
110
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.cpp
Normal file
@ -0,0 +1,110 @@
|
||||
#include "PulseLengthStatAnalyzer.h"
|
||||
#include "PulseLengthStatAnalyzerSettings.h"
|
||||
#include <AnalyzerChannelData.h>
|
||||
|
||||
// One frame per stable level between edges. mData1 = duration in samples; mFlags: 1 = HIGH, 0 = LOW.
|
||||
|
||||
PulseLengthStatAnalyzer::PulseLengthStatAnalyzer()
|
||||
: Analyzer2(),
|
||||
mSettings(),
|
||||
mSimulationInitilized(false)
|
||||
{
|
||||
SetAnalyzerSettings(&mSettings);
|
||||
}
|
||||
|
||||
PulseLengthStatAnalyzer::~PulseLengthStatAnalyzer()
|
||||
{
|
||||
KillThread();
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzer::SetupResults()
|
||||
{
|
||||
mResults.reset(new PulseLengthStatAnalyzerResults(this, &mSettings));
|
||||
SetAnalyzerResults(mResults.get());
|
||||
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzer::WorkerThread()
|
||||
{
|
||||
mChannelData = GetAnalyzerChannelData(mSettings.mInputChannel);
|
||||
|
||||
U32 frames_since_commit = 0;
|
||||
const U32 kCommitBatch = 256;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
CheckIfThreadShouldExit();
|
||||
|
||||
const U64 segment_start = mChannelData->GetSampleNumber();
|
||||
const BitState level = mChannelData->GetBitState();
|
||||
|
||||
mChannelData->AdvanceToNextEdge();
|
||||
|
||||
const U64 edge_sample = mChannelData->GetSampleNumber();
|
||||
if (edge_sample == segment_start)
|
||||
break;
|
||||
|
||||
const U64 duration_samples = edge_sample - segment_start;
|
||||
const U64 end_inclusive = edge_sample > segment_start ? edge_sample - 1 : segment_start;
|
||||
|
||||
Frame frame;
|
||||
frame.mData1 = duration_samples;
|
||||
frame.mFlags = (level == BIT_HIGH) ? 1 : 0;
|
||||
frame.mStartingSampleInclusive = static_cast<S64>(segment_start);
|
||||
frame.mEndingSampleInclusive = static_cast<S64>(end_inclusive);
|
||||
|
||||
mResults->AddFrame(frame);
|
||||
if (++frames_since_commit >= kCommitBatch)
|
||||
{
|
||||
mResults->CommitResults();
|
||||
frames_since_commit = 0;
|
||||
}
|
||||
ReportProgress(edge_sample);
|
||||
}
|
||||
|
||||
if (frames_since_commit != 0)
|
||||
mResults->CommitResults();
|
||||
}
|
||||
|
||||
bool PulseLengthStatAnalyzer::NeedsRerun()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
U32 PulseLengthStatAnalyzer::GenerateSimulationData(U64 minimum_sample_index, U32 device_sample_rate,
|
||||
SimulationChannelDescriptor** simulation_channels)
|
||||
{
|
||||
if (mSimulationInitilized == false)
|
||||
{
|
||||
mSimulationDataGenerator.Initialize(GetSimulationSampleRate(), &mSettings);
|
||||
mSimulationInitilized = true;
|
||||
}
|
||||
|
||||
return mSimulationDataGenerator.GenerateSimulationData(minimum_sample_index, device_sample_rate,
|
||||
simulation_channels);
|
||||
}
|
||||
|
||||
U32 PulseLengthStatAnalyzer::GetMinimumSampleRateHz()
|
||||
{
|
||||
return 200000;
|
||||
}
|
||||
|
||||
const char* PulseLengthStatAnalyzer::GetAnalyzerName() const
|
||||
{
|
||||
return "Pulse Length Stat";
|
||||
}
|
||||
|
||||
const char* GetAnalyzerName()
|
||||
{
|
||||
return "Pulse Length Stat";
|
||||
}
|
||||
|
||||
Analyzer* CreateAnalyzer()
|
||||
{
|
||||
return new PulseLengthStatAnalyzer();
|
||||
}
|
||||
|
||||
void DestroyAnalyzer(Analyzer* analyzer)
|
||||
{
|
||||
delete analyzer;
|
||||
}
|
||||
39
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h
Normal file
39
Analyzer/raw/PulseLengthStat/src/PulseLengthStatAnalyzer.h
Normal file
@ -0,0 +1,39 @@
|
||||
#ifndef PULSELENGTHSTAT_ANALYZER_H
|
||||
#define PULSELENGTHSTAT_ANALYZER_H
|
||||
|
||||
#include <Analyzer.h>
|
||||
#include "PulseLengthStatAnalyzerSettings.h"
|
||||
#include "PulseLengthStatAnalyzerResults.h"
|
||||
#include "PulseLengthStatSimulationDataGenerator.h"
|
||||
#include <memory>
|
||||
|
||||
class ANALYZER_EXPORT PulseLengthStatAnalyzer : public Analyzer2
|
||||
{
|
||||
public:
|
||||
PulseLengthStatAnalyzer();
|
||||
virtual ~PulseLengthStatAnalyzer();
|
||||
|
||||
virtual void SetupResults();
|
||||
virtual void WorkerThread();
|
||||
|
||||
virtual U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate,
|
||||
SimulationChannelDescriptor** simulation_channels);
|
||||
virtual U32 GetMinimumSampleRateHz();
|
||||
|
||||
virtual const char* GetAnalyzerName() const;
|
||||
virtual bool NeedsRerun();
|
||||
|
||||
protected:
|
||||
PulseLengthStatAnalyzerSettings mSettings;
|
||||
std::unique_ptr<PulseLengthStatAnalyzerResults> mResults;
|
||||
AnalyzerChannelData* mChannelData;
|
||||
|
||||
PulseLengthStatSimulationDataGenerator mSimulationDataGenerator;
|
||||
bool mSimulationInitilized;
|
||||
};
|
||||
|
||||
extern "C" ANALYZER_EXPORT const char* __cdecl GetAnalyzerName();
|
||||
extern "C" ANALYZER_EXPORT Analyzer* __cdecl CreateAnalyzer();
|
||||
extern "C" ANALYZER_EXPORT void __cdecl DestroyAnalyzer(Analyzer* analyzer);
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,107 @@
|
||||
#include "PulseLengthStatAnalyzerResults.h"
|
||||
#include <AnalyzerHelpers.h>
|
||||
#include "PulseLengthStatAnalyzer.h"
|
||||
#include "PulseLengthStatAnalyzerSettings.h"
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
PulseLengthStatAnalyzerResults::PulseLengthStatAnalyzerResults(PulseLengthStatAnalyzer* analyzer,
|
||||
PulseLengthStatAnalyzerSettings* settings)
|
||||
: AnalyzerResults(),
|
||||
mSettings(settings),
|
||||
mAnalyzer(analyzer)
|
||||
{
|
||||
}
|
||||
|
||||
PulseLengthStatAnalyzerResults::~PulseLengthStatAnalyzerResults()
|
||||
{
|
||||
}
|
||||
|
||||
static void FormatDurationUs(U64 duration_samples, U32 sample_rate_hz, char* out, size_t out_sz)
|
||||
{
|
||||
if (sample_rate_hz == 0)
|
||||
{
|
||||
snprintf(out, out_sz, "? us");
|
||||
return;
|
||||
}
|
||||
const double us = double(duration_samples) * 1e6 / double(sample_rate_hz);
|
||||
snprintf(out, out_sz, "%.2f us", us);
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
|
||||
{
|
||||
(void)display_base;
|
||||
ClearResultStrings();
|
||||
Frame frame = GetFrame(frame_index);
|
||||
|
||||
const U32 fs = mAnalyzer->GetSampleRate();
|
||||
char dur[64];
|
||||
FormatDurationUs(frame.mData1, fs, dur, sizeof dur);
|
||||
|
||||
const char* lev = (frame.mFlags != 0) ? "HIGH" : "LOW";
|
||||
char line[160];
|
||||
snprintf(line, sizeof line, "%s %s", lev, dur);
|
||||
AddResultString(line);
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzerResults::GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id)
|
||||
{
|
||||
(void)export_type_user_id;
|
||||
(void)display_base;
|
||||
std::ofstream file_stream(file, std::ios::out);
|
||||
|
||||
const U64 trigger_sample = mAnalyzer->GetTriggerSample();
|
||||
const U32 sample_rate = mAnalyzer->GetSampleRate();
|
||||
|
||||
file_stream << "Time [s],Level,Duration_samples,Duration_us" << std::endl;
|
||||
|
||||
const U64 num_frames = GetNumFrames();
|
||||
for (U32 i = 0; i < num_frames; i++)
|
||||
{
|
||||
Frame frame = GetFrame(i);
|
||||
|
||||
char time_str[128];
|
||||
AnalyzerHelpers::GetTimeString(frame.mStartingSampleInclusive, trigger_sample, sample_rate, time_str, 128);
|
||||
|
||||
char dur_us[64];
|
||||
FormatDurationUs(frame.mData1, sample_rate, dur_us, sizeof dur_us);
|
||||
|
||||
file_stream << time_str << "," << ((frame.mFlags != 0) ? "HIGH" : "LOW") << "," << frame.mData1 << ","
|
||||
<< dur_us << std::endl;
|
||||
|
||||
if (UpdateExportProgressAndCheckForCancel(i, num_frames) == true)
|
||||
{
|
||||
file_stream.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
file_stream.close();
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzerResults::GenerateFrameTabularText(U64 frame_index, DisplayBase display_base)
|
||||
{
|
||||
#ifdef SUPPORTS_PROTOCOL_SEARCH
|
||||
(void)display_base;
|
||||
Frame frame = GetFrame(frame_index);
|
||||
ClearTabularText();
|
||||
|
||||
const U32 fs = mAnalyzer->GetSampleRate();
|
||||
char dur[64];
|
||||
FormatDurationUs(frame.mData1, fs, dur, sizeof dur);
|
||||
AddTabularText((frame.mFlags != 0) ? "H" : "L");
|
||||
AddTabularText(dur);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzerResults::GeneratePacketTabularText(U64 packet_id, DisplayBase display_base)
|
||||
{
|
||||
(void)packet_id;
|
||||
(void)display_base;
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzerResults::GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base)
|
||||
{
|
||||
(void)transaction_id;
|
||||
(void)display_base;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
#ifndef PULSELENGTHSTAT_ANALYZER_RESULTS
|
||||
#define PULSELENGTHSTAT_ANALYZER_RESULTS
|
||||
|
||||
#include <AnalyzerResults.h>
|
||||
|
||||
class PulseLengthStatAnalyzer;
|
||||
class PulseLengthStatAnalyzerSettings;
|
||||
|
||||
class PulseLengthStatAnalyzerResults : public AnalyzerResults
|
||||
{
|
||||
public:
|
||||
PulseLengthStatAnalyzerResults(PulseLengthStatAnalyzer* analyzer, PulseLengthStatAnalyzerSettings* settings);
|
||||
virtual ~PulseLengthStatAnalyzerResults();
|
||||
|
||||
virtual void GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base);
|
||||
virtual void GenerateExportFile(const char* file, DisplayBase display_base, U32 export_type_user_id);
|
||||
|
||||
virtual void GenerateFrameTabularText(U64 frame_index, DisplayBase display_base);
|
||||
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
|
||||
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
|
||||
|
||||
protected:
|
||||
PulseLengthStatAnalyzerSettings* mSettings;
|
||||
PulseLengthStatAnalyzer* mAnalyzer;
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,62 @@
|
||||
#include "PulseLengthStatAnalyzerSettings.h"
|
||||
#include <AnalyzerHelpers.h>
|
||||
|
||||
PulseLengthStatAnalyzerSettings::PulseLengthStatAnalyzerSettings()
|
||||
: mInputChannel(UNDEFINED_CHANNEL),
|
||||
mInputChannelInterface()
|
||||
{
|
||||
mInputChannelInterface.SetTitleAndTooltip(
|
||||
"Input",
|
||||
"Digital channel: one frame per stable level between edges (duration in samples / us).");
|
||||
mInputChannelInterface.SetChannel(mInputChannel);
|
||||
|
||||
AddInterface(&mInputChannelInterface);
|
||||
|
||||
AddExportOption(0, "Export as text/csv file");
|
||||
AddExportExtension(0, "text", "txt");
|
||||
AddExportExtension(0, "csv", "csv");
|
||||
|
||||
ClearChannels();
|
||||
AddChannel(mInputChannel, "Input", false);
|
||||
}
|
||||
|
||||
PulseLengthStatAnalyzerSettings::~PulseLengthStatAnalyzerSettings()
|
||||
{
|
||||
}
|
||||
|
||||
bool PulseLengthStatAnalyzerSettings::SetSettingsFromInterfaces()
|
||||
{
|
||||
mInputChannel = mInputChannelInterface.GetChannel();
|
||||
|
||||
ClearChannels();
|
||||
AddChannel(mInputChannel, "Pulse Length Stat", true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzerSettings::UpdateInterfacesFromSettings()
|
||||
{
|
||||
mInputChannelInterface.SetChannel(mInputChannel);
|
||||
}
|
||||
|
||||
void PulseLengthStatAnalyzerSettings::LoadSettings(const char* settings)
|
||||
{
|
||||
SimpleArchive text_archive;
|
||||
text_archive.SetString(settings);
|
||||
|
||||
text_archive >> mInputChannel;
|
||||
|
||||
ClearChannels();
|
||||
AddChannel(mInputChannel, "Pulse Length Stat", true);
|
||||
|
||||
UpdateInterfacesFromSettings();
|
||||
}
|
||||
|
||||
const char* PulseLengthStatAnalyzerSettings::SaveSettings()
|
||||
{
|
||||
SimpleArchive text_archive;
|
||||
|
||||
text_archive << mInputChannel;
|
||||
|
||||
return SetReturnString(text_archive.GetString());
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
#ifndef PULSELENGTHSTAT_ANALYZER_SETTINGS
|
||||
#define PULSELENGTHSTAT_ANALYZER_SETTINGS
|
||||
|
||||
#include <AnalyzerSettings.h>
|
||||
#include <AnalyzerTypes.h>
|
||||
|
||||
class PulseLengthStatAnalyzerSettings : public AnalyzerSettings
|
||||
{
|
||||
public:
|
||||
PulseLengthStatAnalyzerSettings();
|
||||
virtual ~PulseLengthStatAnalyzerSettings();
|
||||
|
||||
virtual bool SetSettingsFromInterfaces();
|
||||
void UpdateInterfacesFromSettings();
|
||||
virtual void LoadSettings(const char* settings);
|
||||
virtual const char* SaveSettings();
|
||||
|
||||
Channel mInputChannel;
|
||||
|
||||
protected:
|
||||
AnalyzerSettingInterfaceChannel mInputChannelInterface;
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,65 @@
|
||||
#include "PulseLengthStatSimulationDataGenerator.h"
|
||||
#include "PulseLengthStatAnalyzerSettings.h"
|
||||
#include <AnalyzerHelpers.h>
|
||||
|
||||
PulseLengthStatSimulationDataGenerator::PulseLengthStatSimulationDataGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
PulseLengthStatSimulationDataGenerator::~PulseLengthStatSimulationDataGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
void PulseLengthStatSimulationDataGenerator::Initialize(U32 simulation_sample_rate, PulseLengthStatAnalyzerSettings* settings)
|
||||
{
|
||||
mSimulationSampleRateHz = simulation_sample_rate;
|
||||
mSettings = settings;
|
||||
|
||||
mSimChannel.SetChannel(mSettings->mInputChannel);
|
||||
mSimChannel.SetSampleRate(simulation_sample_rate);
|
||||
mSimChannel.SetInitialBitState(BIT_HIGH);
|
||||
}
|
||||
|
||||
void PulseLengthStatSimulationDataGenerator::EmitIdle(U32 samples)
|
||||
{
|
||||
mSimChannel.Advance(samples);
|
||||
}
|
||||
|
||||
void PulseLengthStatSimulationDataGenerator::EmitLow(U32 samples)
|
||||
{
|
||||
mSimChannel.TransitionIfNeeded(BIT_LOW);
|
||||
mSimChannel.Advance(samples);
|
||||
}
|
||||
|
||||
void PulseLengthStatSimulationDataGenerator::EmitHigh(U32 samples)
|
||||
{
|
||||
mSimChannel.TransitionIfNeeded(BIT_HIGH);
|
||||
mSimChannel.Advance(samples);
|
||||
}
|
||||
|
||||
U32 PulseLengthStatSimulationDataGenerator::GenerateSimulationData(U64 largest_sample_requested, U32 sample_rate,
|
||||
SimulationChannelDescriptor** simulation_channel)
|
||||
{
|
||||
const U64 adjusted_largest_sample_requested =
|
||||
AnalyzerHelpers::AdjustSimulationTargetSample(largest_sample_requested, sample_rate, mSimulationSampleRateHz);
|
||||
|
||||
while (mSimChannel.GetCurrentSampleNumber() < adjusted_largest_sample_requested)
|
||||
{
|
||||
const U32 us_to_samples = mSimulationSampleRateHz / 1000000;
|
||||
if (us_to_samples == 0)
|
||||
break;
|
||||
|
||||
EmitIdle(500 * us_to_samples);
|
||||
EmitLow(4500 * us_to_samples);
|
||||
EmitHigh(4500 * us_to_samples);
|
||||
EmitLow(4500 * us_to_samples);
|
||||
EmitHigh(4500 * us_to_samples);
|
||||
EmitLow(560 * us_to_samples);
|
||||
EmitHigh(560 * us_to_samples);
|
||||
EmitLow(560 * us_to_samples);
|
||||
EmitHigh(20000 * us_to_samples);
|
||||
}
|
||||
|
||||
*simulation_channel = &mSimChannel;
|
||||
return 1;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
#ifndef PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR
|
||||
#define PULSELENGTHSTAT_SIMULATION_DATA_GENERATOR
|
||||
|
||||
#include <SimulationChannelDescriptor.h>
|
||||
|
||||
class PulseLengthStatAnalyzerSettings;
|
||||
|
||||
class PulseLengthStatSimulationDataGenerator
|
||||
{
|
||||
public:
|
||||
PulseLengthStatSimulationDataGenerator();
|
||||
~PulseLengthStatSimulationDataGenerator();
|
||||
|
||||
void Initialize(U32 simulation_sample_rate, PulseLengthStatAnalyzerSettings* settings);
|
||||
U32 GenerateSimulationData(U64 newest_sample_requested, U32 sample_rate, SimulationChannelDescriptor** simulation_channel);
|
||||
|
||||
protected:
|
||||
PulseLengthStatAnalyzerSettings* mSettings;
|
||||
U32 mSimulationSampleRateHz;
|
||||
SimulationChannelDescriptor mSimChannel;
|
||||
|
||||
void EmitIdle(U32 samples);
|
||||
void EmitLow(U32 samples);
|
||||
void EmitHigh(U32 samples);
|
||||
};
|
||||
|
||||
#endif
|
||||
BIN
Analyzer/raw/Session 0.sal
Normal file
BIN
Analyzer/raw/Session 0.sal
Normal file
Binary file not shown.
BIN
Analyzer/raw/Session 2.sal
Normal file
BIN
Analyzer/raw/Session 2.sal
Normal file
Binary file not shown.
BIN
Analyzer/raw/Session 3.sal
Normal file
BIN
Analyzer/raw/Session 3.sal
Normal file
Binary file not shown.
BIN
Analyzer/raw/Session Sync anchor.sal
Normal file
BIN
Analyzer/raw/Session Sync anchor.sal
Normal file
Binary file not shown.
25175
Analyzer/raw/car.txt
Normal file
25175
Analyzer/raw/car.txt
Normal file
File diff suppressed because it is too large
Load Diff
0
Analyzer/raw/dll/.gitkeep
Normal file
0
Analyzer/raw/dll/.gitkeep
Normal file
18243
Analyzer/raw/point.txt
Normal file
18243
Analyzer/raw/point.txt
Normal file
File diff suppressed because it is too large
Load Diff
32791
Analyzer/raw/rx_car.txt
Normal file
32791
Analyzer/raw/rx_car.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Analyzer/raw/Неорректная преамбула.sal
Normal file
BIN
Analyzer/raw/Неорректная преамбула.sal
Normal file
Binary file not shown.
@ -207,7 +207,7 @@ HardwareTimer IR_Timer(TIM3);
|
||||
|
||||
void setup()
|
||||
{
|
||||
IR_Timer.setOverflow(carrierFrec * 2, HERTZ_FORMAT);
|
||||
IR_Timer.setOverflow((uint32_t)carrierFrec * (uint32_t)IR_Encoder::carrierMultiply(), HERTZ_FORMAT);
|
||||
IR_Timer.attachInterrupt(1, EncoderISR);
|
||||
NVIC_SetPriority(IRQn_Type::TIM3_IRQn, 0);
|
||||
IR_Timer.resume();
|
||||
|
||||
@ -3,6 +3,34 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#if IR_GLITCH_REJECT_PHASE_NUDGE
|
||||
/** Подтяжка опоры фазы после отброса шипа (как irfoxGlitchPhaseNudgeUs в плагине). */
|
||||
static inline void irGlitchPhaseNudge(uint32_t edgeTime, uint16_t riseSync, volatile uint32_t &prevRise)
|
||||
{
|
||||
if (edgeTime > riseSync)
|
||||
{
|
||||
const uint32_t nudged = edgeTime - riseSync;
|
||||
if (nudged > prevRise && nudged < edgeTime)
|
||||
prevRise = nudged;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline uint8_t irPulseFilterHoldbackEdges()
|
||||
{
|
||||
uint8_t hb = (uint8_t)IR_INPUT_FILTER_HOLDBACK_EDGES;
|
||||
if (hb < 2U)
|
||||
hb = 2U;
|
||||
if (hb > 4U)
|
||||
hb = 4U;
|
||||
return hb;
|
||||
}
|
||||
|
||||
static inline uint16_t irClampU16(uint32_t v)
|
||||
{
|
||||
return (v > 0xFFFFU) ? 0xFFFFU : (uint16_t)v;
|
||||
}
|
||||
|
||||
IR_DecoderRaw::IR_DecoderRaw(const uint8_t pin, uint16_t addr, IR_Encoder *encPair) : encoder(encPair)
|
||||
{
|
||||
setPin(pin);
|
||||
@ -44,23 +72,219 @@ bool IR_DecoderRaw::availableRaw()
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////// isr ///////////////////////////////////////////
|
||||
volatile uint32_t time_;
|
||||
void IR_DecoderRaw::pulseFilterResetStats()
|
||||
{
|
||||
pulseFilterDropFilteredOverflow = 0;
|
||||
pulseFilterDropHoldOverflow = 0;
|
||||
pulseFilterDropGlitchPairs = 0;
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *enc)
|
||||
{
|
||||
if (enc == nullptr)
|
||||
return false;
|
||||
for (uint8_t i = 0; i < pairMuteEncoderCount; ++i)
|
||||
{
|
||||
if (pairMuteEncoders[i] == enc)
|
||||
return true;
|
||||
}
|
||||
if (pairMuteEncoderCount >= IR_PAIR_MUTE_MAX_ENCODERS)
|
||||
return false;
|
||||
pairMuteEncoders[pairMuteEncoderCount++] = enc;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::refreshPairMuteState()
|
||||
{
|
||||
uint16_t active = 0;
|
||||
for (uint8_t i = 0; i < pairMuteEncoderCount; ++i)
|
||||
{
|
||||
IR_Encoder *enc = pairMuteEncoders[i];
|
||||
if (enc != nullptr && enc->isBusy())
|
||||
++active;
|
||||
}
|
||||
const uint32_t nowUs = micros();
|
||||
noInterrupts();
|
||||
const bool wasActive = (isPairSending != 0);
|
||||
isPairSending = active;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
if (!wasActive && active != 0)
|
||||
{
|
||||
rxBriefMuteBlockedEdges = 0;
|
||||
rxBriefMuteBeginPending = true;
|
||||
rxBriefMuteBeginUs = nowUs;
|
||||
}
|
||||
else if (wasActive && active == 0)
|
||||
{
|
||||
rxBriefMuteEndPending = true;
|
||||
rxBriefMuteEndUs = nowUs;
|
||||
rxBriefMuteEndCount = rxBriefMuteBlockedEdges;
|
||||
rxBriefMuteBlockedEdges = 0;
|
||||
}
|
||||
#endif
|
||||
interrupts();
|
||||
}
|
||||
|
||||
#if IR_RX_BRIEF_LOG
|
||||
const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
|
||||
{
|
||||
switch (reason)
|
||||
{
|
||||
case RxBriefReason::MuteBegin: return F("MUTE_BEGIN");
|
||||
case RxBriefReason::MuteEnd: return F("MUTE_END");
|
||||
case RxBriefReason::RawOverflow: return F("QRAW");
|
||||
case RxBriefReason::FilterOverflow: return F("QFLT");
|
||||
case RxBriefReason::HoldOverflow: return F("HOLD");
|
||||
case RxBriefReason::Glitch: return F("GLITCH");
|
||||
case RxBriefReason::Timing: return F("TIME");
|
||||
case RxBriefReason::Preamble: return F("PREAMB");
|
||||
case RxBriefReason::Sync: return F("SYNC");
|
||||
case RxBriefReason::BufferOverflow: return F("BUF");
|
||||
case RxBriefReason::Timeout: return F("TIMEOUT");
|
||||
case RxBriefReason::Crc: return F("CRC");
|
||||
case RxBriefReason::Ok: return F("OK");
|
||||
default: return F("UNK");
|
||||
}
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG_REJECT_ONLY
|
||||
if (reason == RxBriefReason::Ok || reason == RxBriefReason::Preamble)
|
||||
return;
|
||||
#endif
|
||||
if (tUs == 0U)
|
||||
tUs = micros();
|
||||
Serial.print(F("IRRX t="));
|
||||
Serial.print((unsigned long)tUs);
|
||||
Serial.print(F(" rsn="));
|
||||
Serial.print(rxBriefReasonTag(reason));
|
||||
switch (reason)
|
||||
{
|
||||
case RxBriefReason::MuteBegin:
|
||||
break;
|
||||
case RxBriefReason::MuteEnd:
|
||||
case RxBriefReason::RawOverflow:
|
||||
Serial.print(F(" cnt="));
|
||||
Serial.print(a);
|
||||
break;
|
||||
case RxBriefReason::FilterOverflow:
|
||||
case RxBriefReason::HoldOverflow:
|
||||
case RxBriefReason::Glitch:
|
||||
Serial.print(F(" total="));
|
||||
Serial.print(a);
|
||||
break;
|
||||
case RxBriefReason::Timing:
|
||||
Serial.print(F(" rp="));
|
||||
Serial.print(a);
|
||||
if (b)
|
||||
{
|
||||
Serial.print(F(" hp="));
|
||||
Serial.print(b);
|
||||
}
|
||||
break;
|
||||
case RxBriefReason::Preamble:
|
||||
Serial.print(F(" good="));
|
||||
Serial.print(a);
|
||||
if (b)
|
||||
{
|
||||
Serial.print(F(" per="));
|
||||
Serial.print(b);
|
||||
}
|
||||
break;
|
||||
case RxBriefReason::Sync:
|
||||
Serial.print(F(" err="));
|
||||
Serial.print(a);
|
||||
break;
|
||||
case RxBriefReason::BufferOverflow:
|
||||
Serial.print(F(" bits="));
|
||||
Serial.print(a);
|
||||
break;
|
||||
case RxBriefReason::Timeout:
|
||||
Serial.print(F(" bits="));
|
||||
Serial.print(a);
|
||||
if (b)
|
||||
{
|
||||
Serial.print(F(" exp="));
|
||||
Serial.print(b);
|
||||
}
|
||||
break;
|
||||
case RxBriefReason::Crc:
|
||||
case RxBriefReason::Ok:
|
||||
Serial.print(F(" len="));
|
||||
Serial.print(a);
|
||||
if (b)
|
||||
{
|
||||
Serial.print(F(" err="));
|
||||
Serial.print(b);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::rxBriefNoteMuteBlockedIsr(uint32_t tUs)
|
||||
{
|
||||
(void)tUs;
|
||||
if (rxBriefMuteBlockedEdges != UINT16_MAX)
|
||||
++rxBriefMuteBlockedEdges;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::rxBriefNoteRawOverflowIsr(uint32_t tUs)
|
||||
{
|
||||
if (rxBriefRawOverflowDrops != UINT16_MAX)
|
||||
++rxBriefRawOverflowDrops;
|
||||
rxBriefRawOverflowLastUs = tUs;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::rxBriefFlushDeferredIsrLogs()
|
||||
{
|
||||
bool muteBeginPending = false;
|
||||
uint32_t muteBeginUs = 0;
|
||||
bool muteEndPending = false;
|
||||
uint32_t muteEndUs = 0;
|
||||
uint16_t muteEndCnt = 0;
|
||||
uint16_t rawCnt = 0;
|
||||
uint32_t rawLastUs = 0;
|
||||
noInterrupts();
|
||||
muteBeginPending = rxBriefMuteBeginPending;
|
||||
muteBeginUs = rxBriefMuteBeginUs;
|
||||
rxBriefMuteBeginPending = false;
|
||||
rxBriefMuteBeginUs = 0;
|
||||
muteEndPending = rxBriefMuteEndPending;
|
||||
muteEndUs = rxBriefMuteEndUs;
|
||||
muteEndCnt = rxBriefMuteEndCount;
|
||||
rxBriefMuteEndPending = false;
|
||||
rxBriefMuteEndUs = 0;
|
||||
rxBriefMuteEndCount = 0;
|
||||
rawCnt = rxBriefRawOverflowDrops;
|
||||
rawLastUs = rxBriefRawOverflowLastUs;
|
||||
rxBriefRawOverflowDrops = 0;
|
||||
rxBriefRawOverflowLastUs = 0;
|
||||
interrupts();
|
||||
if (muteBeginPending)
|
||||
rxBriefLog(RxBriefReason::MuteBegin, 0, 0, muteBeginUs);
|
||||
if (muteEndPending)
|
||||
rxBriefLog(RxBriefReason::MuteEnd, muteEndCnt, 0, muteEndUs);
|
||||
if (rawCnt)
|
||||
rxBriefLog(RxBriefReason::RawOverflow, rawCnt, 0, rawLastUs);
|
||||
}
|
||||
#endif
|
||||
|
||||
//////////////////////////////////// isr ///////////////////////////////////////////
|
||||
void IR_DecoderRaw::isr()
|
||||
{
|
||||
// Интервалы между соседними фронтами считаются как (uint32_t)(t1 - t0) — корректно при
|
||||
// паузе < ~35 мин между фронтами; условие «тишина > longSilence» в preambleProcessEdge
|
||||
// переписано без front.time > prevRise (оно ломается при wrap).
|
||||
uint32_t t;
|
||||
noInterrupts();
|
||||
time_ = micros();
|
||||
t = micros();
|
||||
interrupts();
|
||||
if (time_ < oldTime)
|
||||
{
|
||||
time_ += 1000;
|
||||
}
|
||||
oldTime = time_;
|
||||
|
||||
FrontStorage edge;
|
||||
edge.dir = port->IDR & mask;
|
||||
edge.time = time_;
|
||||
edge.time = t;
|
||||
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
edgeTracePush(edge.time, edge.dir ? 1u : 0u,
|
||||
@ -69,10 +293,19 @@ void IR_DecoderRaw::isr()
|
||||
|
||||
if (isPairSending)
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefNoteMuteBlockedIsr(edge.time);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
subBuffer.push(edge);
|
||||
if (!subBuffer.push(edge))
|
||||
{
|
||||
isSubBufferOverflow = true;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefNoteRawOverflowIsr(edge.time);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -93,6 +326,7 @@ void IR_DecoderRaw::firstRX()
|
||||
i_dataBuffer = 0;
|
||||
nextControlBit = bitPerByte;
|
||||
i_syncBit = 0;
|
||||
err_syncBit = 0;
|
||||
isWrongPack = false;
|
||||
|
||||
isPreamb = true;
|
||||
@ -101,11 +335,25 @@ void IR_DecoderRaw::firstRX()
|
||||
wrCounter = 0;
|
||||
#endif
|
||||
memset(dataBuffer, 0x00, dataByteSizeMax);
|
||||
pulseFilterReset();
|
||||
preambleResetToIdle();
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
|
||||
{
|
||||
if (pulseFilterHoldCount != 0U)
|
||||
return true;
|
||||
noInterrupts();
|
||||
const bool busy = !subBuffer.isEmpty() || !filteredSubBuffer.isEmpty();
|
||||
interrupts();
|
||||
return busy;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::listenStart()
|
||||
{
|
||||
if (isReciveRaw && ((micros() - prevRise) > IR_timeout * 2))
|
||||
if (rxTimeoutPipelineBusy())
|
||||
return;
|
||||
if (isReciveRaw && ((micros() - lastEdgeTime) > IR_timeout * 2U))
|
||||
{
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceOnTimeoutOrAbort(true);
|
||||
@ -120,65 +368,150 @@ void IR_DecoderRaw::listenStart()
|
||||
inline void IR_DecoderRaw::checkTimeout()
|
||||
{
|
||||
if (!isRecive) return; // уже не принимаем – нечего проверять
|
||||
if (rxTimeoutPipelineBusy())
|
||||
return;
|
||||
|
||||
if (micros() - lastEdgeTime > IR_timeout * 2U)
|
||||
{
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceOnTimeoutOrAbort(false);
|
||||
#endif
|
||||
#if IR_RX_BRIEF_LOG
|
||||
const uint16_t expected = (i_dataBuffer >= 8U) ? uint16_t(dataBuffer[0] & IR_MASK_MSG_INFO) : 0U;
|
||||
rxBriefLog(RxBriefReason::Timeout, i_dataBuffer, expected, micros());
|
||||
#endif
|
||||
isRecive = false; // приём завершён
|
||||
msgTypeReceive = 0;
|
||||
// firstRX(); // подготовка к новому пакету
|
||||
lastEdgeTime = micros(); // защита от повторного срабатывания
|
||||
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
|
||||
// с «сырым» флагом приёма / Locked и не может заново поймать преамбулу до очень
|
||||
// длинной тишины (см. preambleProcessEdge Idle и listenStart).
|
||||
isReciveRaw = false;
|
||||
firstRX();
|
||||
// Не подставлять lastEdgeTime = micros(): при хвосте в subBuffer следующий фронт
|
||||
// имеет edge.time < micros() — откат lastEdgeTime даёт ложный TIMEOUT на следующем tick().
|
||||
// Повторного checkTimeout при тишине нет: isRecive уже false.
|
||||
}
|
||||
}
|
||||
// ====================================================================
|
||||
|
||||
void IR_DecoderRaw::tick()
|
||||
{
|
||||
// FrontStorage *currentFrontPtr;
|
||||
// noInterrupts();
|
||||
// currentFrontPtr = subBuffer.pop();
|
||||
// interrupts();
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefFlushDeferredIsrLogs();
|
||||
#endif
|
||||
// listenStart/checkTimeout: в конце tick (END) и при отсутствии фронта (ниже).
|
||||
// Не в начале до pop: иначе после checkTimeout lastEdgeTime vs micros() расходятся
|
||||
// с метками ISR из очереди → ложные TIMEOUT (bits=0) каждый пакет.
|
||||
|
||||
FrontStorage currentFront;
|
||||
bool hasCurrentFront = false;
|
||||
FrontStorage rawFront;
|
||||
bool hasRawFront = false;
|
||||
noInterrupts();
|
||||
listenStart();
|
||||
FrontStorage *currentFrontPtr;
|
||||
currentFrontPtr = subBuffer.pop();
|
||||
if (currentFrontPtr == nullptr)
|
||||
FrontStorage *rawPtr = subBuffer.pop();
|
||||
if (rawPtr != nullptr)
|
||||
{
|
||||
rawFront = *rawPtr;
|
||||
hasRawFront = true;
|
||||
}
|
||||
interrupts();
|
||||
|
||||
if (IR_INPUT_MIN_PULSE_US > 0U)
|
||||
{
|
||||
if (hasRawFront)
|
||||
pulseFilterFeedOneRaw(rawFront);
|
||||
else
|
||||
pulseFilterFlushTimeout(micros());
|
||||
|
||||
noInterrupts();
|
||||
FrontStorage *flt = filteredSubBuffer.pop();
|
||||
if (flt != nullptr)
|
||||
{
|
||||
currentFront = *flt;
|
||||
hasCurrentFront = true;
|
||||
}
|
||||
interrupts();
|
||||
}
|
||||
else if (hasRawFront)
|
||||
{
|
||||
currentFront = rawFront;
|
||||
hasCurrentFront = true;
|
||||
}
|
||||
|
||||
if (!hasCurrentFront)
|
||||
{
|
||||
isSubBufferOverflow = false;
|
||||
checkTimeout(); // <--- новое место проверки
|
||||
interrupts();
|
||||
listenStart();
|
||||
checkTimeout();
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
||||
#endif
|
||||
return;
|
||||
} // Если данных нет - ничего не делаем
|
||||
currentFront = *currentFrontPtr;
|
||||
interrupts();
|
||||
|
||||
// ---------- буфер пуст: фронтов нет, проверяем тайм-аут ----------
|
||||
// if (currentFrontPtr == nullptr)
|
||||
// {
|
||||
// isSubBufferOverflow = false;
|
||||
// return;
|
||||
// }
|
||||
if (preambleProcessEdge(currentFront))
|
||||
{
|
||||
lastEdgeTime = currentFront.time;
|
||||
goto END;
|
||||
}
|
||||
|
||||
// // ---------- есть фронт: продолжаем обработку ----------
|
||||
// FrontStorage currentFront = *currentFrontPtr;
|
||||
lastEdgeTime = currentFront.time; // запоминаем любой фронт
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (currentFront.dir)
|
||||
{ // Если __/``` ↑
|
||||
if (currentFront.time - prevRise > riseTimeMax / 4 || highCount || lowCount)
|
||||
const uint32_t candRp = currentFront.time - prevRise;
|
||||
const uint32_t candHt = currentFront.time - prevFall;
|
||||
const uint32_t candLt = prevFall - prevRise;
|
||||
|
||||
#if IR_SHORT_LOW_GLITCH_REJECT
|
||||
const bool short_low_glitch =
|
||||
isRecive && !isPreamb && candHt < (riseTimeMin / 8U) && candLt >= riseTimeMin &&
|
||||
candRp >= riseTimeMin && candRp <= IR_timeout;
|
||||
if (short_low_glitch)
|
||||
{
|
||||
errors.other++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time);
|
||||
#endif
|
||||
#if IR_GLITCH_REJECT_PHASE_NUDGE
|
||||
irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise);
|
||||
#endif
|
||||
goto END;
|
||||
}
|
||||
#endif
|
||||
#if IR_MICRO_GAP_RISE_REJECT
|
||||
const bool micro_gap_cand_lt_ok =
|
||||
(candLt >= riseTimeMin) || (candLt >= (riseTimeMin / 4U) && candLt < riseTimeMin);
|
||||
const bool micro_gap_rise = isRecive && !isPreamb && candHt < (riseTimeMin / 8U) && micro_gap_cand_lt_ok &&
|
||||
candRp >= (riseTimeMin / 4U) && candRp < riseTimeMin && candRp <= IR_timeout;
|
||||
if (micro_gap_rise)
|
||||
{
|
||||
errors.other++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, 1, 0, currentFront.time);
|
||||
#endif
|
||||
#if IR_GLITCH_REJECT_PHASE_NUDGE
|
||||
irGlitchPhaseNudge(currentFront.time, riseSyncTime, prevRise);
|
||||
#endif
|
||||
goto END;
|
||||
}
|
||||
#endif
|
||||
if (candRp <= riseTimeMax / 4U && !highCount && !lowCount)
|
||||
{
|
||||
errors.other++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Timing, irClampU16(candRp), 0, currentFront.time);
|
||||
#endif
|
||||
goto END;
|
||||
}
|
||||
|
||||
if (candRp > riseTimeMax / 4 || highCount || lowCount)
|
||||
{ // комплексный фикс рваной единицы
|
||||
risePeriod = currentFront.time - prevRise;
|
||||
highTime = currentFront.time - prevFall;
|
||||
lowTime = prevFall - prevRise;
|
||||
risePeriod = candRp;
|
||||
highTime = candHt;
|
||||
lowTime = candLt;
|
||||
prevRise = currentFront.time;
|
||||
|
||||
if (
|
||||
@ -231,82 +564,14 @@ void IR_DecoderRaw::tick()
|
||||
digitalWrite(errOut, currentFront.dir);
|
||||
#endif
|
||||
|
||||
if (currentFront.time > prevRise && currentFront.time - prevRise > IR_timeout * 2 && !isReciveRaw)
|
||||
{ // первый
|
||||
#ifdef IRDEBUG
|
||||
errPulse(up, 50);
|
||||
errPulse(down, 50);
|
||||
errPulse(up, 150);
|
||||
errPulse(down, 150);
|
||||
#endif
|
||||
preambFrontCounter = preambFronts - 1U;
|
||||
isPreamb = true;
|
||||
|
||||
isRecive = true;
|
||||
isReciveRaw = true;
|
||||
isWrongPack = false;
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceResetFrame();
|
||||
packTraceOpen = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------
|
||||
|
||||
if (preambFrontCounter)
|
||||
{ // в преамбуле
|
||||
#ifdef IRDEBUG
|
||||
// Serial.print("risePeriod: ");
|
||||
// Serial.println(risePeriod);
|
||||
#endif
|
||||
if (currentFront.dir && risePeriod < IR_timeout)
|
||||
{ // __/``` ↑ и мы в внутри пакета
|
||||
|
||||
if (risePeriod < riseTimeMin / 2)
|
||||
{ // fix рваной единицы
|
||||
preambFrontCounter += 2;
|
||||
errors.other++;
|
||||
#ifdef IRDEBUG
|
||||
errPulse(down, 350);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
if (freeFrec)
|
||||
{
|
||||
riseSyncTime = (riseSyncTime + risePeriod / 2) / 2;
|
||||
} // tuner
|
||||
}
|
||||
}
|
||||
else
|
||||
{ /* riseSyncTime = bitTime; */
|
||||
} // сброс тюнера
|
||||
preambFrontCounter--;
|
||||
// Serial.print("preambFrontCounter: "); Serial.println(preambFrontCounter);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isPreamb)
|
||||
{ // первый фронт после
|
||||
// gotTune.set(riseSyncTime);
|
||||
isPreamb = false;
|
||||
#ifdef IRDEBUG
|
||||
errPulse(up, 50);
|
||||
errPulse(down, 50);
|
||||
#endif
|
||||
prevRise += risePeriod / 2;
|
||||
// prevRise = currentFront.time + riseTime;
|
||||
goto END;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPreamb)
|
||||
{
|
||||
goto END;
|
||||
}
|
||||
if (risePeriod > IR_timeout || isBufferOverflow || risePeriod < riseTimeMin || isWrongPack)
|
||||
// ~Мы в пределах таймаута и буффер не переполнен и fix дроблёных единиц
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG
|
||||
if (!isBufferOverflow && !isWrongPack)
|
||||
rxBriefLog(RxBriefReason::Timing, irClampU16((uint32_t)risePeriod),
|
||||
irClampU16((uint32_t)highTime), currentFront.time);
|
||||
#endif
|
||||
goto END;
|
||||
}
|
||||
|
||||
@ -465,6 +730,11 @@ void IR_DecoderRaw::tick()
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
END:;
|
||||
listenStart();
|
||||
checkTimeout();
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefFlushDeferredIsrLogs();
|
||||
#endif
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
|
||||
#endif
|
||||
@ -478,6 +748,9 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
if (i_dataBuffer > dataByteSizeMax * 8)
|
||||
{ // проверка переполнения
|
||||
isBufferOverflow = true;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::BufferOverflow, i_dataBuffer, 0, micros());
|
||||
#endif
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
if (packTraceOpen)
|
||||
packTraceEmitErrorFlash(F("ERROR: buffer overflow"));
|
||||
@ -485,9 +758,12 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
}
|
||||
if (isBufferOverflow || isPreamb || isWrongPack)
|
||||
{
|
||||
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
|
||||
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
|
||||
isRecive = false;
|
||||
isReciveRaw = false;
|
||||
msgTypeReceive = 0;
|
||||
firstRX();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -551,6 +827,9 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
#endif
|
||||
{
|
||||
isWrongPack = true;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Sync, err_syncBit, 0, micros());
|
||||
#endif
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceEmitErrorFlash(F("ERROR: Wrong sync bit"));
|
||||
#endif
|
||||
@ -598,6 +877,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
|
||||
isRecive = false;
|
||||
isReciveRaw = false;
|
||||
preambleResetToIdle();
|
||||
msgTypeReceive = 0;
|
||||
isAvailable = crcCheck(packSize - crcBytes, crcValue);
|
||||
|
||||
@ -643,6 +923,14 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
|
||||
packTraceEmitEndOk(static_cast<uint8_t>(packSize));
|
||||
else
|
||||
packTraceEmitEndBadCrc(static_cast<uint8_t>(packSize));
|
||||
#endif
|
||||
const uint16_t errSum =
|
||||
uint16_t(errors.lowSignal) + uint16_t(errors.highSignal) + uint16_t(errors.other);
|
||||
#if IR_RX_BRIEF_LOG
|
||||
if (isAvailable)
|
||||
rxBriefLog(RxBriefReason::Ok, packSize, errSum, micros());
|
||||
else
|
||||
rxBriefLog(RxBriefReason::Crc, packSize, errSum, micros());
|
||||
#endif
|
||||
if (!isAvailable && packSize > 0 && packSize <= dataByteSizeMax) {
|
||||
memcpy(rejectBuffer, dataBuffer, packSize);
|
||||
@ -1147,6 +1435,293 @@ __attribute__((weak)) void irPackTracePrintOkCommand(const uint8_t *buf, uint8_t
|
||||
|
||||
#endif // IRDEBUG_SERIAL_PACK
|
||||
|
||||
uint32_t IR_DecoderRaw::absDiffU32(uint32_t a, uint32_t b)
|
||||
{
|
||||
return (a > b) ? (a - b) : (b - a);
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::pulseFilterShiftLeft(uint8_t n)
|
||||
{
|
||||
if (n == 0 || pulseFilterHoldCount == 0)
|
||||
return;
|
||||
if (n >= pulseFilterHoldCount)
|
||||
{
|
||||
pulseFilterHoldCount = 0;
|
||||
return;
|
||||
}
|
||||
const uint8_t newCount = static_cast<uint8_t>(pulseFilterHoldCount - n);
|
||||
for (uint8_t i = 0; i < newCount; ++i)
|
||||
pulseFilterHoldEdges[i] = pulseFilterHoldEdges[static_cast<uint8_t>(i + n)];
|
||||
pulseFilterHoldCount = newCount;
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::pulseFilterEmit(const FrontStorage &e)
|
||||
{
|
||||
if (filteredSubBuffer.isFull())
|
||||
{
|
||||
pulseFilterDropFilteredOverflow++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::FilterOverflow, irClampU16(pulseFilterDropFilteredOverflow), 0, e.time);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
filteredSubBuffer.push(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::pulseFilterReset()
|
||||
{
|
||||
pulseFilterHoldCount = 0;
|
||||
pulseFilterLastRawValid = false;
|
||||
pulseFilterLastRawTime = 0;
|
||||
while (filteredSubBuffer.pop() != nullptr) {}
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::pulseFilterFeedOneRaw(const FrontStorage &e)
|
||||
{
|
||||
const uint32_t minUs = IR_INPUT_MIN_PULSE_US;
|
||||
if (minUs == 0U)
|
||||
{
|
||||
pulseFilterEmit(e);
|
||||
return;
|
||||
}
|
||||
|
||||
pulseFilterLastRawTime = e.time;
|
||||
pulseFilterLastRawValid = true;
|
||||
|
||||
if (pulseFilterHoldCount >= kPulseFilterHoldCap)
|
||||
{
|
||||
pulseFilterDropHoldOverflow++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::HoldOverflow, irClampU16(pulseFilterDropHoldOverflow), 0, e.time);
|
||||
#endif
|
||||
pulseFilterEmit(pulseFilterHoldEdges[0]);
|
||||
pulseFilterShiftLeft(1);
|
||||
}
|
||||
|
||||
pulseFilterHoldEdges[pulseFilterHoldCount++] = e;
|
||||
const uint8_t holdback = irPulseFilterHoldbackEdges();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (pulseFilterHoldCount < 2)
|
||||
return;
|
||||
|
||||
const uint32_t dt = pulseFilterHoldEdges[1].time - pulseFilterHoldEdges[0].time;
|
||||
if (dt < minUs)
|
||||
{
|
||||
pulseFilterDropGlitchPairs++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, e.time);
|
||||
#endif
|
||||
pulseFilterShiftLeft(2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pulseFilterHoldCount <= holdback)
|
||||
return;
|
||||
|
||||
pulseFilterEmit(pulseFilterHoldEdges[0]);
|
||||
pulseFilterShiftLeft(1);
|
||||
}
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::pulseFilterFlushTimeout(uint32_t nowUs)
|
||||
{
|
||||
if (IR_INPUT_MIN_PULSE_US == 0U || !pulseFilterLastRawValid || pulseFilterHoldCount == 0)
|
||||
return;
|
||||
const uint32_t waitUs = IR_INPUT_MIN_PULSE_US * (uint32_t)IR_INPUT_FILTER_TIMEOUT_MULT;
|
||||
if ((uint32_t)(nowUs - pulseFilterLastRawTime) < waitUs)
|
||||
return;
|
||||
|
||||
while (pulseFilterHoldCount > 0)
|
||||
{
|
||||
if (pulseFilterHoldCount >= 2)
|
||||
{
|
||||
const uint32_t dt = pulseFilterHoldEdges[1].time - pulseFilterHoldEdges[0].time;
|
||||
if (dt < IR_INPUT_MIN_PULSE_US)
|
||||
{
|
||||
pulseFilterDropGlitchPairs++;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, nowUs);
|
||||
#endif
|
||||
pulseFilterShiftLeft(2);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pulseFilterEmit(pulseFilterHoldEdges[0]);
|
||||
pulseFilterShiftLeft(1);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t IR_DecoderRaw::preambleJitterTolUs(uint32_t baselineUs) const
|
||||
{
|
||||
const uint32_t pct = (baselineUs * (uint32_t)IR_PREAMBLE_JITTER_PCT) / 100U;
|
||||
return (pct > (uint32_t)IR_PREAMBLE_JITTER_US_MIN) ? pct : (uint32_t)IR_PREAMBLE_JITTER_US_MIN;
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::preambleRisePeriodCoarseOk(uint32_t periodUs) const
|
||||
{
|
||||
const uint32_t base = (uint32_t)bitTime;
|
||||
const uint32_t minP = (base * (uint32_t)IR_PREAMBLE_PERIOD_MIN_FACTOR_PCT) / 100U;
|
||||
const uint32_t maxP = (base * (uint32_t)IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT) / 100U;
|
||||
return periodUs >= minP && periodUs <= maxP;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::preambleResetToIdle()
|
||||
{
|
||||
preambleState = PreambleState::Idle;
|
||||
preambleGoodPeriods = 0;
|
||||
preambleMeanPeriod = 0;
|
||||
preambleCandidateLastEdgeTime = 0;
|
||||
preambleCandidateFirstRiseTime = 0;
|
||||
preambleCandidateFirstRiseValid = false;
|
||||
preambFrontCounter = 0;
|
||||
isPreamb = false;
|
||||
isWrongPack = false;
|
||||
isBufferOverflow = false;
|
||||
}
|
||||
|
||||
void IR_DecoderRaw::preambleStartCandidate(const FrontStorage &front)
|
||||
{
|
||||
preambleState = PreambleState::Candidate;
|
||||
preambleGoodPeriods = 0;
|
||||
preambleMeanPeriod = 0;
|
||||
preambleCandidateLastEdgeTime = front.time;
|
||||
preambleCandidateFirstRiseTime = front.time;
|
||||
preambleCandidateFirstRiseValid = front.dir;
|
||||
preambFrontCounter = 0;
|
||||
isPreamb = true;
|
||||
isRecive = false;
|
||||
isReciveRaw = false;
|
||||
}
|
||||
|
||||
bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
|
||||
{
|
||||
const uint32_t longSilence = IR_timeout * 2U;
|
||||
const uint32_t candTimeout = IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT;
|
||||
|
||||
if (preambleState == PreambleState::Idle)
|
||||
{
|
||||
if (isReciveRaw)
|
||||
return false;
|
||||
// Не использовать front.time > prevRise: после переполнения micros (~2^32 µs) новое t
|
||||
// меньше prevRise в unsigned-смысле — преамбула никогда не стартует («залипание» RX).
|
||||
// prevRise == 0: как раньше — «длинная тишина» только по абсолютному front.time > longSilence.
|
||||
if (!isReciveRaw && front.dir &&
|
||||
((prevRise == 0U && front.time > longSilence) ||
|
||||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
|
||||
preambleStartCandidate(front);
|
||||
}
|
||||
|
||||
if (preambleState == PreambleState::Candidate)
|
||||
{
|
||||
if ((uint32_t)(front.time - preambleCandidateLastEdgeTime) > candTimeout)
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, front.time);
|
||||
#endif
|
||||
preambleStartCandidate(front);
|
||||
}
|
||||
|
||||
preambleCandidateLastEdgeTime = front.time;
|
||||
if (!front.dir)
|
||||
return true;
|
||||
|
||||
if (!preambleCandidateFirstRiseValid)
|
||||
{
|
||||
preambleCandidateFirstRiseValid = true;
|
||||
preambleCandidateFirstRiseTime = front.time;
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t period = front.time - preambleCandidateFirstRiseTime;
|
||||
preambleCandidateFirstRiseTime = front.time;
|
||||
if (!preambleRisePeriodCoarseOk(period))
|
||||
{
|
||||
preambleGoodPeriods = 0;
|
||||
preambleMeanPeriod = 0;
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preambleGoodPeriods == 0)
|
||||
{
|
||||
preambleGoodPeriods = 1;
|
||||
preambleMeanPeriod = (uint16_t)period;
|
||||
}
|
||||
else
|
||||
{
|
||||
const uint32_t tol = preambleJitterTolUs(preambleMeanPeriod);
|
||||
if (absDiffU32(period, preambleMeanPeriod) <= tol)
|
||||
{
|
||||
if (preambleGoodPeriods < 255U)
|
||||
++preambleGoodPeriods;
|
||||
preambleMeanPeriod =
|
||||
(uint16_t)(((uint32_t)preambleMeanPeriod * 3U + period) / 4U);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if IR_RX_BRIEF_LOG
|
||||
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
|
||||
#endif
|
||||
preambleGoodPeriods = 1;
|
||||
preambleMeanPeriod = (uint16_t)period;
|
||||
}
|
||||
}
|
||||
|
||||
if (freeFrec)
|
||||
riseSyncTime = (riseSyncTime + period / 2U) / 2U;
|
||||
|
||||
if (preambleGoodPeriods >= kPreambleLockNeed)
|
||||
{
|
||||
// Новый кадр обязан стартовать с чистого state, иначе возможны CRC/sync срывы
|
||||
// при lock без промежуточного listenStart()->firstRX().
|
||||
errors.reset();
|
||||
packSize = 0;
|
||||
isBufferOverflow = false;
|
||||
isAvailable = false;
|
||||
bufBitPos = 0;
|
||||
isData = true;
|
||||
i_dataBuffer = 0;
|
||||
nextControlBit = bitPerByte;
|
||||
i_syncBit = 0;
|
||||
err_syncBit = 0;
|
||||
isWrongPack = false;
|
||||
msgTypeReceive = 0;
|
||||
memset(dataBuffer, 0x00, dataByteSizeMax);
|
||||
|
||||
preambleState = PreambleState::Locked;
|
||||
isPreamb = false;
|
||||
isRecive = true;
|
||||
isReciveRaw = true;
|
||||
risePeriod = preambleMeanPeriod;
|
||||
#if defined(IRDEBUG_SERIAL_PACK)
|
||||
packTraceResetFrame();
|
||||
packTraceOpen = true;
|
||||
#endif
|
||||
#ifdef IRDEBUG
|
||||
errPulse(up, 50);
|
||||
errPulse(down, 50);
|
||||
#endif
|
||||
prevRise = front.time + preambleMeanPeriod / 2U;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preambleState == PreambleState::Locked)
|
||||
{
|
||||
if (!isReciveRaw)
|
||||
preambleResetToIdle();
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isReciveRaw;
|
||||
}
|
||||
|
||||
// IRDEBUG FUNC
|
||||
#ifdef IRDEBUG
|
||||
inline void IR_DecoderRaw::errPulse(uint8_t pin, uint8_t count)
|
||||
|
||||
@ -52,6 +52,10 @@ public:
|
||||
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
|
||||
bool isSubOverflow();
|
||||
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
|
||||
uint32_t pulseFilterDroppedByFilteredOverflow() const { return pulseFilterDropFilteredOverflow; }
|
||||
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
|
||||
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
|
||||
void pulseFilterResetStats();
|
||||
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
void edgeTraceClear();
|
||||
@ -68,15 +72,32 @@ public:
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
private:
|
||||
enum class RxBriefReason : uint8_t
|
||||
{
|
||||
MuteBegin = 1,
|
||||
MuteEnd = 2,
|
||||
RawOverflow = 3,
|
||||
FilterOverflow = 4,
|
||||
HoldOverflow = 5,
|
||||
Glitch = 6,
|
||||
Timing = 7,
|
||||
Preamble = 8,
|
||||
Sync = 9,
|
||||
BufferOverflow = 10,
|
||||
Timeout = 11,
|
||||
Crc = 12,
|
||||
Ok = 13
|
||||
};
|
||||
|
||||
bool isRejectAvailable = false;
|
||||
uint8_t rejectPackSize = 0;
|
||||
uint8_t rejectBuffer[dataByteSizeMax]{};
|
||||
|
||||
ErrorsStruct errors;
|
||||
bool isAvailable = false;
|
||||
uint16_t packSize;
|
||||
uint16_t crcValue;
|
||||
volatile uint16_t isPairSending = 0; // Флаг передачи парного передатчика
|
||||
uint16_t packSize = 0;
|
||||
uint16_t crcValue = 0;
|
||||
volatile uint16_t isPairSending = 0; // Число активных TX, временно глушащих этот RX.
|
||||
volatile bool isRecive = false; // Флаг приёма
|
||||
volatile bool isPreamb = false; // флаг начальной последовости
|
||||
volatile bool isSubBufferOverflow = false;
|
||||
@ -101,6 +122,31 @@ private:
|
||||
// volatile FrontStorage subBuffer[subBufferSize]; // вспомогательный буфер для хранения необработанных фронтов/спадов
|
||||
|
||||
RingBuffer<FrontStorage, subBufferSize> subBuffer;
|
||||
/** Очередь фронтов после потокового анти-глитча; tick() читает из неё при включённом фильтре. */
|
||||
RingBuffer<FrontStorage, subBufferSize> filteredSubBuffer;
|
||||
IR_Encoder *pairMuteEncoders[IR_PAIR_MUTE_MAX_ENCODERS]{};
|
||||
uint8_t pairMuteEncoderCount = 0;
|
||||
static constexpr uint8_t kPulseFilterHoldCap = 6;
|
||||
FrontStorage pulseFilterHoldEdges[kPulseFilterHoldCap]{};
|
||||
uint8_t pulseFilterHoldCount = 0;
|
||||
bool pulseFilterLastRawValid = false;
|
||||
uint32_t pulseFilterLastRawTime = 0;
|
||||
uint32_t pulseFilterDropFilteredOverflow = 0;
|
||||
uint32_t pulseFilterDropHoldOverflow = 0;
|
||||
uint32_t pulseFilterDropGlitchPairs = 0;
|
||||
static constexpr uint8_t kPreambleLockNeed = (uint8_t)IR_PREAMBLE_LOCK_RISE_PERIODS;
|
||||
enum class PreambleState : uint8_t
|
||||
{
|
||||
Idle = 0,
|
||||
Candidate = 1,
|
||||
Locked = 2
|
||||
};
|
||||
PreambleState preambleState = PreambleState::Idle;
|
||||
uint8_t preambleGoodPeriods = 0;
|
||||
uint16_t preambleMeanPeriod = 0;
|
||||
uint32_t preambleCandidateLastEdgeTime = 0;
|
||||
uint32_t preambleCandidateFirstRiseTime = 0;
|
||||
bool preambleCandidateFirstRiseValid = false;
|
||||
|
||||
#if defined(IR_EDGE_TRACE)
|
||||
struct IrEdgeTraceRec
|
||||
@ -116,6 +162,17 @@ private:
|
||||
volatile bool edgeTrace_overflow = false;
|
||||
#endif
|
||||
|
||||
#if IR_RX_BRIEF_LOG
|
||||
volatile bool rxBriefMuteBeginPending = false;
|
||||
volatile uint32_t rxBriefMuteBeginUs = 0;
|
||||
volatile bool rxBriefMuteEndPending = false;
|
||||
volatile uint32_t rxBriefMuteEndUs = 0;
|
||||
volatile uint16_t rxBriefMuteEndCount = 0;
|
||||
volatile uint16_t rxBriefMuteBlockedEdges = 0;
|
||||
volatile uint16_t rxBriefRawOverflowDrops = 0;
|
||||
volatile uint32_t rxBriefRawOverflowLastUs = 0;
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
uint8_t dataBuffer[dataByteSizeMax]{0}; // Буффер данных
|
||||
volatile uint32_t prevRise, prevPrevRise, prevFall, prevPrevFall; // Время предыдущих фронтов/спадов
|
||||
@ -124,7 +181,6 @@ private:
|
||||
volatile uint32_t highTime;
|
||||
volatile uint32_t lowTime;
|
||||
|
||||
uint32_t oldTime;
|
||||
uint16_t wrongCounter;
|
||||
|
||||
int8_t highCount;
|
||||
@ -136,9 +192,25 @@ private:
|
||||
int16_t bufBitPos = 0; // Позиция для записи бита в буффер
|
||||
|
||||
private:
|
||||
bool isReciveRaw;
|
||||
bool isReciveRaw = false;
|
||||
void listenStart();
|
||||
void checkTimeout(); //
|
||||
/** В очередях/hold фильтра ещё есть фронты — не оценивать таймаут по micros()-lastEdgeTime (ложный TIMEOUT). */
|
||||
bool rxTimeoutPipelineBusy() const;
|
||||
/** Один сырой фронт из subBuffer -> потоковый holdback-антиглитч. */
|
||||
void pulseFilterFeedOneRaw(const FrontStorage &e);
|
||||
void pulseFilterFlushTimeout(uint32_t nowUs);
|
||||
bool pulseFilterEmit(const FrontStorage &e);
|
||||
void pulseFilterShiftLeft(uint8_t n);
|
||||
void pulseFilterReset();
|
||||
static uint32_t absDiffU32(uint32_t a, uint32_t b);
|
||||
bool registerPairMuteEncoder(IR_Encoder *enc);
|
||||
void refreshPairMuteState();
|
||||
uint32_t preambleJitterTolUs(uint32_t baselineUs) const;
|
||||
bool preambleRisePeriodCoarseOk(uint32_t periodUs) const;
|
||||
void preambleResetToIdle();
|
||||
void preambleStartCandidate(const FrontStorage &front);
|
||||
bool preambleProcessEdge(const FrontStorage &front);
|
||||
|
||||
/// @brief Проверка CRC. Проверяет len байт со значением crc, пришедшим в пакете
|
||||
/// @param len Длина в байтах проверяемых данных
|
||||
@ -148,10 +220,10 @@ bool isReciveRaw;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
bool isData = true; // Флаг относится ли бит к данным, или битам синхронизации
|
||||
uint16_t i_dataBuffer; // Счётчик буфера данных
|
||||
uint8_t nextControlBit = bitPerByte; // Метка для смены флага isData
|
||||
uint8_t i_syncBit; // Счётчик битов синхронизации
|
||||
uint8_t err_syncBit; // Счётчик ошибок синхронизации
|
||||
uint16_t i_dataBuffer = 0; // Счётчик буфера данных
|
||||
uint16_t nextControlBit = bitPerByte; // Метка для смены флага isData; uint16_t нужен для длинных кадров (>24 байт total)
|
||||
uint8_t i_syncBit = 0; // Счётчик битов синхронизации
|
||||
uint8_t err_syncBit = 0; // Счётчик ошибок синхронизации
|
||||
|
||||
/// @brief Запиь бита в буффер, а так же проверка битов синхранизации и их фильтрация
|
||||
/// @param packTraceInvertFix если true — в IRDEBUG_SERIAL_PACK бит в трассе пишется как `0`/`1` (исправление по фронтам)
|
||||
@ -166,6 +238,14 @@ bool isReciveRaw;
|
||||
/// @return Результат
|
||||
uint16_t ceil_div(uint16_t val, uint16_t divider);
|
||||
|
||||
#if IR_RX_BRIEF_LOG
|
||||
static const __FlashStringHelper *rxBriefReasonTag(RxBriefReason reason);
|
||||
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
|
||||
void rxBriefNoteMuteBlockedIsr(uint32_t tUs);
|
||||
void rxBriefNoteRawOverflowIsr(uint32_t tUs);
|
||||
void rxBriefFlushDeferredIsrLogs();
|
||||
#endif
|
||||
|
||||
#ifdef IRDEBUG
|
||||
uint32_t wrCounter;
|
||||
inline void errPulse(uint8_t pin, uint8_t count);
|
||||
|
||||
628
IR_Encoder.cpp
628
IR_Encoder.cpp
@ -15,19 +15,14 @@ IR_Encoder::IR_Encoder(uint8_t pin, uint16_t addr, IR_DecoderRaw *decPair, bool
|
||||
setPin(pin);
|
||||
id = addr;
|
||||
this->decPair = decPair;
|
||||
signal = noSignal;
|
||||
isSending = false;
|
||||
#if disablePairDec
|
||||
if (decPair != nullptr)
|
||||
{
|
||||
blindDecoders = new IR_DecoderRaw *[1]{decPair};
|
||||
singleBlindDecoder = decPair;
|
||||
blindDecoders = &singleBlindDecoder;
|
||||
decodersCount = 1;
|
||||
}
|
||||
#endif
|
||||
if (decPair != nullptr)
|
||||
{
|
||||
decPair->encoder = this;
|
||||
}
|
||||
registerWithBlindDecoders();
|
||||
|
||||
if (autoHandle)
|
||||
{
|
||||
@ -43,12 +38,243 @@ IR_Encoder::IR_Encoder(uint8_t pin, uint16_t addr, IR_DecoderRaw *decPair, bool
|
||||
|
||||
pinMode(pin, OUTPUT);
|
||||
}
|
||||
powerNumerator_ = 1;
|
||||
};
|
||||
|
||||
HardwareTimer* IR_Encoder::IR_Timer = nullptr;
|
||||
IR_Encoder::ExternalTxStartFn IR_Encoder::externalTxStartFn = nullptr;
|
||||
IR_Encoder::ExternalTxBusyFn IR_Encoder::externalTxBusyFn = nullptr;
|
||||
void *IR_Encoder::externalTxCtx = nullptr;
|
||||
bool IR_Encoder::txIsrLegacyMode_ = false;
|
||||
uint16_t IR_Encoder::s_carrierMultiply = 2;
|
||||
|
||||
void IR_Encoder::setCarrierMultiply(uint16_t multiply)
|
||||
{
|
||||
if (multiply < 2)
|
||||
{
|
||||
multiply = 2;
|
||||
}
|
||||
s_carrierMultiply = multiply;
|
||||
}
|
||||
|
||||
uint16_t IR_Encoder::carrierMultiply()
|
||||
{
|
||||
return s_carrierMultiply;
|
||||
}
|
||||
|
||||
void IR_Encoder::retuneCarrierClock()
|
||||
{
|
||||
if (IR_Timer == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
IR_Timer->pause();
|
||||
IR_Timer->setOverflow((uint32_t)carrierFrec * (uint32_t)s_carrierMultiply, HERTZ_FORMAT);
|
||||
IR_Timer->pause();
|
||||
}
|
||||
|
||||
uint16_t IR_Encoder::maxPowerNumerator()
|
||||
{
|
||||
return static_cast<uint16_t>(s_carrierMultiply / 2U);
|
||||
}
|
||||
|
||||
void IR_Encoder::setPowerNumerator(uint16_t n)
|
||||
{
|
||||
const uint16_t cap = maxPowerNumerator();
|
||||
powerNumerator_ = (n > cap) ? cap : n;
|
||||
}
|
||||
|
||||
void IR_Encoder::setPowerPercent(uint8_t p)
|
||||
{
|
||||
if (p > 100U)
|
||||
{
|
||||
p = 100U;
|
||||
}
|
||||
const uint16_t cap = maxPowerNumerator();
|
||||
const uint32_t n = ((uint32_t)p * (uint32_t)cap + 50U) / 100U;
|
||||
powerNumerator_ = static_cast<uint16_t>(n);
|
||||
}
|
||||
|
||||
uint16_t IR_Encoder::powerNumerator() const
|
||||
{
|
||||
return powerNumerator_;
|
||||
}
|
||||
|
||||
bool IR_Encoder::scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, size_t maxRuns, uint16_t multiply)
|
||||
{
|
||||
if (runs == nullptr || ioCount == nullptr || maxRuns == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (multiply < 2)
|
||||
{
|
||||
multiply = 2;
|
||||
}
|
||||
const size_t nIn = *ioCount;
|
||||
if (nIn > irproto::kIsrTxMaxGateRuns)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IrTxGateRun copy[irproto::kIsrTxMaxGateRuns];
|
||||
memcpy(copy, runs, nIn * sizeof(IrTxGateRun));
|
||||
size_t w = 0;
|
||||
for (size_t r = 0; r < nIn; r++)
|
||||
{
|
||||
uint32_t phys = (uint32_t)copy[r].lenTicks * (uint32_t)multiply / 2U;
|
||||
if (copy[r].lenTicks > 0 && phys == 0)
|
||||
{
|
||||
phys = 1;
|
||||
}
|
||||
const bool g = copy[r].gate;
|
||||
while (phys > 0)
|
||||
{
|
||||
if (w >= maxRuns)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const uint32_t chunk = phys > 65535U ? 65535U : phys;
|
||||
runs[w].lenTicks = static_cast<uint16_t>(chunk);
|
||||
runs[w].gate = g;
|
||||
w++;
|
||||
phys -= chunk;
|
||||
}
|
||||
}
|
||||
*ioCount = w;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IR_Encoder::setTxIsrLegacyMode(bool legacy)
|
||||
{
|
||||
txIsrLegacyMode_ = legacy;
|
||||
}
|
||||
|
||||
bool IR_Encoder::txIsrLegacyMode()
|
||||
{
|
||||
return txIsrLegacyMode_;
|
||||
}
|
||||
|
||||
bool IR_Encoder::txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
switch (st.signal)
|
||||
{
|
||||
case noSignal:
|
||||
st.signal = preamb;
|
||||
return false;
|
||||
|
||||
case preamb:
|
||||
if (st.preambFrontCounter)
|
||||
{
|
||||
st.preambFrontCounter--;
|
||||
st.toggleCounter = preambToggle;
|
||||
st.state = !st.state;
|
||||
return true;
|
||||
}
|
||||
st.signal = data;
|
||||
st.state = !LOW;
|
||||
continue;
|
||||
|
||||
case data:
|
||||
if (st.dataSequenceCounter)
|
||||
{
|
||||
if (!(st.dataSequenceCounter & 1U))
|
||||
{
|
||||
st.currentBitSequence =
|
||||
((sendBufferLocal[st.dataByteCounter] >> st.dataBitCounter) & 1U) ? bitHigh : bitLow;
|
||||
st.dataBitCounter--;
|
||||
}
|
||||
st.toggleCounter = st.currentBitSequence[!st.state];
|
||||
st.dataSequenceCounter--;
|
||||
st.state = !st.state;
|
||||
return true;
|
||||
}
|
||||
st.syncLastBit = ((sendBufferLocal[st.dataByteCounter]) & 1U);
|
||||
st.dataByteCounter++;
|
||||
st.dataBitCounter = bitPerByte - 1;
|
||||
st.dataSequenceCounter = bitPerByte * 2;
|
||||
st.signal = sync;
|
||||
continue;
|
||||
|
||||
case sync:
|
||||
if (st.syncSequenceCounter)
|
||||
{
|
||||
if (!(st.syncSequenceCounter & 1U))
|
||||
{
|
||||
if (st.syncSequenceCounter == 2)
|
||||
{
|
||||
st.currentBitSequence = ((sendBufferLocal[st.dataByteCounter]) & 0b10000000) ? bitLow : bitHigh;
|
||||
}
|
||||
else
|
||||
{
|
||||
st.currentBitSequence = st.syncLastBit ? bitLow : bitHigh;
|
||||
st.syncLastBit = !st.syncLastBit;
|
||||
}
|
||||
}
|
||||
st.toggleCounter = st.currentBitSequence[!st.state];
|
||||
st.syncSequenceCounter--;
|
||||
st.state = !st.state;
|
||||
return true;
|
||||
}
|
||||
st.signal = data;
|
||||
st.syncSequenceCounter = syncBits * 2;
|
||||
if (st.dataByteCounter >= st.sendLen)
|
||||
{
|
||||
st.signal = noSignal;
|
||||
}
|
||||
continue;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IR_Encoder::txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal)
|
||||
{
|
||||
if (st.toggleCounter)
|
||||
{
|
||||
st.toggleCounter--;
|
||||
return true;
|
||||
}
|
||||
return txAdvanceBoundary(st, sendBufferLocal);
|
||||
}
|
||||
|
||||
bool IR_Encoder::txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut)
|
||||
{
|
||||
gateOut = st.state;
|
||||
return txAdvanceAfterOutput(st, sendBufferLocal);
|
||||
}
|
||||
|
||||
void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const
|
||||
{
|
||||
st.sendLen = sendLen;
|
||||
st.toggleCounter = toggleCounter;
|
||||
st.dataBitCounter = dataBitCounter;
|
||||
st.dataByteCounter = dataByteCounter;
|
||||
st.preambFrontCounter = preambFrontCounter;
|
||||
st.dataSequenceCounter = dataSequenceCounter;
|
||||
st.syncSequenceCounter = syncSequenceCounter;
|
||||
st.syncLastBit = syncLastBit;
|
||||
st.state = state;
|
||||
st.currentBitSequence = currentBitSequence;
|
||||
st.signal = signal;
|
||||
}
|
||||
|
||||
void IR_Encoder::storeTxFsmToMembers(const TxFsmState &st)
|
||||
{
|
||||
sendLen = st.sendLen;
|
||||
toggleCounter = st.toggleCounter;
|
||||
dataBitCounter = st.dataBitCounter;
|
||||
dataByteCounter = st.dataByteCounter;
|
||||
preambFrontCounter = st.preambFrontCounter;
|
||||
dataSequenceCounter = st.dataSequenceCounter;
|
||||
syncSequenceCounter = st.syncSequenceCounter;
|
||||
syncLastBit = st.syncLastBit;
|
||||
state = st.state;
|
||||
currentBitSequence = st.currentBitSequence;
|
||||
signal = st.signal;
|
||||
}
|
||||
|
||||
inline HardwareTimer* IR_Encoder::get_IR_Timer(){return IR_Encoder::IR_Timer;}
|
||||
|
||||
@ -76,7 +302,7 @@ void IR_Encoder::begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, ui
|
||||
IR_Timer = timer;
|
||||
if(IR_Timer == nullptr) return;
|
||||
IR_Timer->pause();
|
||||
IR_Timer->setOverflow(carrierFrec * 2, HERTZ_FORMAT);
|
||||
IR_Timer->setOverflow((uint32_t)carrierFrec * (uint32_t)s_carrierMultiply, HERTZ_FORMAT);
|
||||
IR_Timer->attachInterrupt(channel, (isrCallback == nullptr ? IR_Encoder::isr : isrCallback));
|
||||
NVIC_SetPriority(IRQn, priority);
|
||||
IR_Timer->pause();
|
||||
@ -88,7 +314,7 @@ void IR_Encoder::beginClockOnly(HardwareTimer *timer)
|
||||
if (IR_Timer == nullptr)
|
||||
return;
|
||||
IR_Timer->pause();
|
||||
IR_Timer->setOverflow(carrierFrec * 2, HERTZ_FORMAT);
|
||||
IR_Timer->setOverflow((uint32_t)carrierFrec * (uint32_t)s_carrierMultiply, HERTZ_FORMAT);
|
||||
IR_Timer->pause();
|
||||
}
|
||||
|
||||
@ -110,7 +336,7 @@ void IR_Encoder::externalFinishSend()
|
||||
}
|
||||
|
||||
isSending = false;
|
||||
setDecoder_isSending();
|
||||
refreshBlindDecoderMuteState();
|
||||
}
|
||||
|
||||
size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns)
|
||||
@ -128,28 +354,29 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu
|
||||
uint8_t sendBufferLocal[dataByteSizeMax] = {0};
|
||||
memcpy(sendBufferLocal, packet, len);
|
||||
|
||||
uint8_t sendLenLocal = len;
|
||||
uint8_t toggleCounterLocal = preambToggle;
|
||||
uint8_t dataBitCounterLocal = bitPerByte - 1;
|
||||
uint8_t dataByteCounterLocal = 0;
|
||||
uint8_t preambFrontCounterLocal = preambPulse * 2 - 1;
|
||||
uint8_t dataSequenceCounterLocal = bitPerByte * 2;
|
||||
uint8_t syncSequenceCounterLocal = syncBits * 2;
|
||||
bool syncLastBitLocal = false;
|
||||
SignalPart signalLocal = preamb;
|
||||
bool stateLocal = HIGH;
|
||||
uint8_t *currentBitSequenceLocal = bitHigh;
|
||||
TxFsmState st{};
|
||||
st.sendLen = len;
|
||||
st.toggleCounter = preambToggle;
|
||||
st.dataBitCounter = bitPerByte - 1;
|
||||
st.dataByteCounter = 0;
|
||||
st.preambFrontCounter = preambPulse * 2 - 1;
|
||||
st.dataSequenceCounter = bitPerByte * 2;
|
||||
st.syncSequenceCounter = syncBits * 2;
|
||||
st.syncLastBit = false;
|
||||
st.signal = preamb;
|
||||
st.state = HIGH;
|
||||
st.currentBitSequence = bitHigh;
|
||||
|
||||
size_t runCount = 0;
|
||||
|
||||
while (true)
|
||||
bool isActive = true;
|
||||
while (isActive)
|
||||
{
|
||||
const bool gate = stateLocal;
|
||||
const uint16_t runLenTicks = (uint16_t)toggleCounterLocal + 1U;
|
||||
bool gate = false;
|
||||
isActive = txEmitTick(st, sendBufferLocal, gate);
|
||||
|
||||
if (runCount > 0 && outRuns[runCount - 1].gate == gate)
|
||||
{
|
||||
outRuns[runCount - 1].lenTicks = (uint16_t)(outRuns[runCount - 1].lenTicks + runLenTicks);
|
||||
outRuns[runCount - 1].lenTicks = (uint16_t)(outRuns[runCount - 1].lenTicks + 1U);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -158,86 +385,11 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu
|
||||
return 0;
|
||||
}
|
||||
outRuns[runCount].gate = gate;
|
||||
outRuns[runCount].lenTicks = runLenTicks;
|
||||
outRuns[runCount].lenTicks = 1U;
|
||||
runCount++;
|
||||
}
|
||||
|
||||
// Advance state to the next run boundary (equivalent to ISR iteration when toggleCounter == 0).
|
||||
while (true)
|
||||
{
|
||||
switch (signalLocal)
|
||||
{
|
||||
case noSignal:
|
||||
}
|
||||
return runCount;
|
||||
|
||||
case preamb:
|
||||
if (preambFrontCounterLocal)
|
||||
{
|
||||
preambFrontCounterLocal--;
|
||||
toggleCounterLocal = preambToggle;
|
||||
break;
|
||||
}
|
||||
// End of preamble.
|
||||
signalLocal = data;
|
||||
stateLocal = !LOW;
|
||||
continue;
|
||||
|
||||
case data:
|
||||
if (dataSequenceCounterLocal)
|
||||
{
|
||||
if (!(dataSequenceCounterLocal & 1U))
|
||||
{
|
||||
currentBitSequenceLocal = ((sendBufferLocal[dataByteCounterLocal] >> dataBitCounterLocal) & 1U) ? bitHigh : bitLow;
|
||||
dataBitCounterLocal--;
|
||||
}
|
||||
toggleCounterLocal = currentBitSequenceLocal[!stateLocal];
|
||||
dataSequenceCounterLocal--;
|
||||
break;
|
||||
}
|
||||
// End of data byte.
|
||||
syncLastBitLocal = ((sendBufferLocal[dataByteCounterLocal]) & 1U);
|
||||
dataByteCounterLocal++;
|
||||
dataBitCounterLocal = bitPerByte - 1;
|
||||
dataSequenceCounterLocal = bitPerByte * 2;
|
||||
signalLocal = sync;
|
||||
continue;
|
||||
|
||||
case sync:
|
||||
if (syncSequenceCounterLocal)
|
||||
{
|
||||
if (!(syncSequenceCounterLocal & 1U))
|
||||
{
|
||||
if (syncSequenceCounterLocal == 2)
|
||||
{
|
||||
currentBitSequenceLocal = ((sendBufferLocal[dataByteCounterLocal]) & 0b10000000) ? bitLow : bitHigh;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentBitSequenceLocal = syncLastBitLocal ? bitLow : bitHigh;
|
||||
syncLastBitLocal = !syncLastBitLocal;
|
||||
}
|
||||
}
|
||||
toggleCounterLocal = currentBitSequenceLocal[!stateLocal];
|
||||
syncSequenceCounterLocal--;
|
||||
break;
|
||||
}
|
||||
// End of sync.
|
||||
signalLocal = data;
|
||||
syncSequenceCounterLocal = syncBits * 2;
|
||||
if (dataByteCounterLocal >= sendLenLocal)
|
||||
{
|
||||
signalLocal = noSignal;
|
||||
}
|
||||
continue;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
stateLocal = !stateLocal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -302,12 +454,16 @@ void IR_Encoder::disable()
|
||||
|
||||
void IR_Encoder::setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count)
|
||||
{
|
||||
#if disablePairDec
|
||||
if (blindDecoders != nullptr)
|
||||
delete[] blindDecoders;
|
||||
#endif
|
||||
if (count > IR_PAIR_MUTE_MAX_ENCODERS)
|
||||
{
|
||||
decodersCount = 0;
|
||||
blindDecoders = nullptr;
|
||||
return;
|
||||
}
|
||||
decodersCount = count;
|
||||
blindDecoders = decoders;
|
||||
registerWithBlindDecoders();
|
||||
refreshBlindDecoderMuteState();
|
||||
}
|
||||
|
||||
IR_Encoder::~IR_Encoder(){};
|
||||
@ -494,19 +650,28 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d
|
||||
return IR_SendResult(true, sendTime);
|
||||
}
|
||||
|
||||
void IR_Encoder::setDecoder_isSending()
|
||||
{
|
||||
if (decodersCount)
|
||||
void IR_Encoder::registerWithBlindDecoders()
|
||||
{
|
||||
if (!decodersCount || blindDecoders == nullptr)
|
||||
return;
|
||||
|
||||
for (uint8_t i = 0; i < decodersCount; i++)
|
||||
{
|
||||
blindDecoders[i]->isPairSending ^= id;
|
||||
// Serial.print("setDecoder_isSending() id = ");
|
||||
// Serial.print(id);
|
||||
// Serial.print(" isPairSending = ");
|
||||
// Serial.println(blindDecoders[i]->isPairSending);
|
||||
if (blindDecoders[i] != nullptr)
|
||||
blindDecoders[i]->registerPairMuteEncoder(this);
|
||||
}
|
||||
}
|
||||
|
||||
void IR_Encoder::refreshBlindDecoderMuteState()
|
||||
{
|
||||
if (!decodersCount || blindDecoders == nullptr)
|
||||
return;
|
||||
|
||||
for (uint8_t i = 0; i < decodersCount; i++)
|
||||
{
|
||||
if (blindDecoders[i] != nullptr)
|
||||
blindDecoders[i]->refreshPairMuteState();
|
||||
}
|
||||
}
|
||||
|
||||
void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
@ -523,6 +688,14 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
return;
|
||||
}
|
||||
|
||||
// Serial.print("IR tx hex: ");
|
||||
// for (uint8_t i = 0; i < len; i++)
|
||||
// {
|
||||
// if (ptr[i] < 0x10) Serial.print("0");
|
||||
// Serial.print(ptr[i], HEX);
|
||||
// }
|
||||
// Serial.println();
|
||||
|
||||
if (externalTxStartFn != nullptr)
|
||||
{
|
||||
if (externalTxBusyFn != nullptr && externalTxBusyFn(externalTxCtx))
|
||||
@ -530,40 +703,98 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as sending and delegate actual signal output to external backend.
|
||||
setDecoder_isSending();
|
||||
sendLen = len;
|
||||
isSending = true;
|
||||
refreshBlindDecoderMuteState();
|
||||
|
||||
const bool ok = externalTxStartFn(externalTxCtx, this, ptr, len);
|
||||
if (!ok)
|
||||
{
|
||||
isSending = false;
|
||||
setDecoder_isSending();
|
||||
refreshBlindDecoderMuteState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
IR_Encoder::carrierResume();
|
||||
// Serial.println("START");
|
||||
setDecoder_isSending();
|
||||
|
||||
// noInterrupts();
|
||||
if (port == nullptr || mask == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ptr != sendBuffer)
|
||||
{
|
||||
memcpy(sendBuffer, ptr, len);
|
||||
}
|
||||
sendLen = len;
|
||||
toggleCounter = preambToggle; // Первая генерация для первого signal
|
||||
|
||||
if (txIsrLegacyMode_)
|
||||
{
|
||||
toggleCounter = preambToggle;
|
||||
dataBitCounter = bitPerByte - 1;
|
||||
dataByteCounter = 0;
|
||||
|
||||
preambFrontCounter = preambPulse * 2 - 1; // -1 за счёт генерации уже на этапе сразу после инициализации
|
||||
preambFrontCounter = preambPulse * 2 - 1;
|
||||
dataSequenceCounter = bitPerByte * 2;
|
||||
syncSequenceCounter = syncBits * 2;
|
||||
|
||||
signal = preamb;
|
||||
isSending = true;
|
||||
state = HIGH;
|
||||
|
||||
currentBitSequence = bitHigh;
|
||||
// interrupts();
|
||||
txMultiplySnap_ = carrierMultiply();
|
||||
{
|
||||
const uint16_t cap = maxPowerNumerator();
|
||||
txPowerSnap_ = (powerNumerator_ > cap) ? cap : powerNumerator_;
|
||||
}
|
||||
legacyPhysPerLogical_ = static_cast<uint16_t>(txMultiplySnap_ / 2U);
|
||||
if (legacyPhysPerLogical_ == 0)
|
||||
{
|
||||
legacyPhysPerLogical_ = 1;
|
||||
}
|
||||
legacyPhysCounter_ = 0;
|
||||
legacySlotInPeriod_ = 0;
|
||||
isSending = true;
|
||||
refreshBlindDecoderMuteState();
|
||||
IR_Encoder::carrierResume();
|
||||
return;
|
||||
}
|
||||
|
||||
size_t nRuns = buildGateRuns(sendBuffer, len, txGateRuns_, irproto::kIsrTxMaxGateRuns);
|
||||
if (nRuns == 0U)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!scaleGateRunsToPhysical(txGateRuns_, &nRuns, irproto::kIsrTxMaxGateRuns, carrierMultiply()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t total = 0;
|
||||
for (size_t i = 0; i < nRuns; i++)
|
||||
{
|
||||
total += txGateRuns_[i].lenTicks;
|
||||
}
|
||||
txBsrrTotalTicks_ = total;
|
||||
|
||||
const uint32_t setW = (uint32_t)mask;
|
||||
const uint32_t resetW = ((uint32_t)mask) << 16U;
|
||||
txMultiplySnap_ = carrierMultiply();
|
||||
{
|
||||
const uint16_t cap = maxPowerNumerator();
|
||||
txPowerSnap_ = (powerNumerator_ > cap) ? cap : powerNumerator_;
|
||||
}
|
||||
txBsrrWave_.configure(setW, resetW, txGateRuns_, nRuns, txMultiplySnap_, txPowerSnap_);
|
||||
|
||||
txBsrrHalfLen_ = (uint16_t)(irproto::kIsrTxBsrrWordCount / 2U);
|
||||
txBsrrWave_.fill(txBsrrWords_, irproto::kIsrTxBsrrWordCount);
|
||||
|
||||
txBsrrReadIdx_ = 0;
|
||||
txBsrrTicksSent_ = 0;
|
||||
|
||||
isSending = true;
|
||||
refreshBlindDecoderMuteState();
|
||||
if (port != nullptr)
|
||||
{
|
||||
port->BSRR = resetW;
|
||||
}
|
||||
IR_Encoder::carrierResume();
|
||||
}
|
||||
|
||||
void IR_Encoder::isr()
|
||||
@ -581,106 +812,71 @@ void IR_Encoder::_isr()
|
||||
if (!isSending)
|
||||
return;
|
||||
|
||||
ir_out_virtual = !ir_out_virtual && state;
|
||||
if (port == nullptr)
|
||||
return;
|
||||
|
||||
port->ODR &= ~(mask);
|
||||
port->ODR |= mask & (ir_out_virtual ? (uint16_t)0xFFFF : (uint16_t)0x0000);
|
||||
|
||||
if (toggleCounter)
|
||||
if (txIsrLegacyMode_)
|
||||
{
|
||||
toggleCounter--;
|
||||
const uint32_t setW = (uint32_t)mask;
|
||||
const uint32_t resetW = ((uint32_t)mask) << 16U;
|
||||
if (!state)
|
||||
{
|
||||
port->BSRR = resetW;
|
||||
legacySlotInPeriod_ = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsrStart:
|
||||
switch (signal)
|
||||
port->BSRR = (legacySlotInPeriod_ < txPowerSnap_) ? setW : resetW;
|
||||
legacySlotInPeriod_++;
|
||||
if (legacySlotInPeriod_ >= txMultiplySnap_)
|
||||
{
|
||||
case noSignal:
|
||||
signal = preamb;
|
||||
// сброс счетчиков
|
||||
// ...
|
||||
legacySlotInPeriod_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
legacyPhysCounter_++;
|
||||
if (legacyPhysCounter_ < legacyPhysPerLogical_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
legacyPhysCounter_ = 0;
|
||||
|
||||
TxFsmState st{};
|
||||
loadTxFsmFromMembers(st);
|
||||
const bool active = txAdvanceAfterOutput(st, sendBuffer);
|
||||
storeTxFsmToMembers(st);
|
||||
|
||||
if (!active)
|
||||
{
|
||||
port->BSRR = resetW;
|
||||
isSending = false;
|
||||
// Serial.println("STOP");
|
||||
setDecoder_isSending();
|
||||
refreshBlindDecoderMuteState();
|
||||
carrierStopPending = true;
|
||||
// Serial.println();
|
||||
}
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
case preamb:
|
||||
if (preambFrontCounter)
|
||||
port->BSRR = txBsrrWords_[txBsrrReadIdx_];
|
||||
txBsrrReadIdx_++;
|
||||
txBsrrTicksSent_++;
|
||||
|
||||
if (txBsrrTicksSent_ >= txBsrrTotalTicks_)
|
||||
{
|
||||
preambFrontCounter--;
|
||||
toggleCounter = preambToggle; // Вторая и последующие генерации для этого signal
|
||||
}
|
||||
else
|
||||
{ // Конец преамбулы, переход на следующий signal
|
||||
signal = data;
|
||||
state = !LOW; // Инверсное состояние первой генерации следующего signal
|
||||
goto IsrStart; // Применение новых параметров в этй же итерации прерывания
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case data:
|
||||
if (dataSequenceCounter)
|
||||
{
|
||||
if (!(dataSequenceCounter & 1U))
|
||||
{ // если чётный - смена бита
|
||||
currentBitSequence = ((sendBuffer[dataByteCounter] >> dataBitCounter) & 1U) ? bitHigh : bitLow; // определение текущего бита
|
||||
dataBitCounter--;
|
||||
}
|
||||
toggleCounter = currentBitSequence[!state];
|
||||
dataSequenceCounter--;
|
||||
}
|
||||
else
|
||||
{ // Конец data, переход на следующий signal
|
||||
syncLastBit = ((sendBuffer[dataByteCounter]) & 1U);
|
||||
dataByteCounter++;
|
||||
dataBitCounter = bitPerByte - 1;
|
||||
dataSequenceCounter = bitPerByte * 2;
|
||||
signal = sync;
|
||||
goto IsrStart; // Применение новых параметров в этй же итерации прерывания
|
||||
}
|
||||
break;
|
||||
|
||||
case sync:
|
||||
if (syncSequenceCounter)
|
||||
{
|
||||
if (!(syncSequenceCounter & 1U))
|
||||
{ // если чётный - смена бита
|
||||
if (syncSequenceCounter == 2)
|
||||
{ // Если последний бит
|
||||
currentBitSequence = ((sendBuffer[dataByteCounter]) & 0b10000000) ? bitLow : bitHigh;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentBitSequence = syncLastBit ? bitLow : bitHigh; // определение текущего бита
|
||||
syncLastBit = !syncLastBit;
|
||||
}
|
||||
}
|
||||
toggleCounter = currentBitSequence[!state];
|
||||
syncSequenceCounter--;
|
||||
}
|
||||
else
|
||||
{ // Конец sync, переход на следующий signal
|
||||
signal = data;
|
||||
syncSequenceCounter = syncBits * 2;
|
||||
|
||||
if (dataByteCounter >= sendLen)
|
||||
{ // определение конца данных
|
||||
signal = noSignal;
|
||||
}
|
||||
goto IsrStart; // Применение новых параметров в этй же итерации прерывания
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
port->BSRR = ((uint32_t)mask) << 16U;
|
||||
isSending = false;
|
||||
refreshBlindDecoderMuteState();
|
||||
carrierStopPending = true;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
state = !state;
|
||||
if (txBsrrReadIdx_ == txBsrrHalfLen_)
|
||||
{
|
||||
txBsrrWave_.fill(&txBsrrWords_[0], txBsrrHalfLen_);
|
||||
}
|
||||
else if (txBsrrReadIdx_ >= irproto::kIsrTxBsrrWordCount)
|
||||
{
|
||||
txBsrrReadIdx_ = 0;
|
||||
txBsrrWave_.fill(&txBsrrWords_[txBsrrHalfLen_], txBsrrHalfLen_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
122
IR_Encoder.h
122
IR_Encoder.h
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "IR_config.h"
|
||||
#include "IrTxBsrrWave.h"
|
||||
|
||||
// TODO: Отложенная передача после завершения приема
|
||||
|
||||
@ -22,10 +23,7 @@ class IR_Encoder : public IR_FOX
|
||||
public:
|
||||
static HardwareTimer* IR_Timer;
|
||||
|
||||
struct IR_TxGateRun {
|
||||
uint16_t lenTicks; // number of timer ticks at carrierFrec*2
|
||||
bool gate; // true: carrier enabled (output toggles), false: silent (output forced low)
|
||||
};
|
||||
using IR_TxGateRun = IrTxGateRun;
|
||||
|
||||
using ExternalTxBusyFn = bool (*)(void *ctx);
|
||||
using ExternalTxStartFn = bool (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
|
||||
@ -35,16 +33,47 @@ public:
|
||||
/// @brief Класс передатчика
|
||||
/// @param addr Адрес передатчика
|
||||
/// @param pin Вывод передатчика
|
||||
/// @param decPair Приёмник, для которого отключается приём в момент передачи передатчиком
|
||||
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
|
||||
/// (аналог setBlindDecoders() для одного RX).
|
||||
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
|
||||
static void isr();
|
||||
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
|
||||
/** Configure timer frequency for TX clock (carrierFrec*2) without attaching ISR. */
|
||||
/**
|
||||
* Глобальный знаменатель: частота таймера TX = carrierFrec × multiply (слотов на период несущей).
|
||||
* По умолчанию multiply=2 (как бывшие carrierFrec×2). Задавать до begin/beginClockOnly либо после
|
||||
* изменения вызвать retuneCarrierClock() (не менять multiply во время активной передачи).
|
||||
*/
|
||||
static void setCarrierMultiply(uint16_t multiply);
|
||||
static uint16_t carrierMultiply();
|
||||
/** Повторно применить carrierFrec×multiply к IR_Timer (pause + setOverflow), ISR не перенавешивает. */
|
||||
static void retuneCarrierClock();
|
||||
|
||||
/** Максимальный числитель мощности: ⌊multiply/2⌋ (100% в setPowerPercent). */
|
||||
static uint16_t maxPowerNumerator();
|
||||
|
||||
/** Числитель N: при открытой огибающей N из multiply тиков HIGH за период несущей. Clamped к maxPowerNumerator(). */
|
||||
void setPowerNumerator(uint16_t n);
|
||||
uint16_t powerNumerator() const;
|
||||
/** p∈[0,100] → ближайший допустимый числитель; 100% даёт N = maxPowerNumerator(). */
|
||||
void setPowerPercent(uint8_t p);
|
||||
|
||||
/** После buildGateRuns: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */
|
||||
static bool scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, size_t maxRuns, uint16_t multiply);
|
||||
|
||||
/** Configure timer frequency for TX clock (carrierFrec × multiply) without attaching ISR. */
|
||||
static void beginClockOnly(HardwareTimer *timer);
|
||||
static HardwareTimer* get_IR_Timer();
|
||||
/** Call from main loop/tick: if ISR requested carrier stop, pause timer here (not in ISR). */
|
||||
static void tick();
|
||||
|
||||
/**
|
||||
* Режим внутреннего TX без DMA: false — BSRR + кольцо (buildGateRuns + scaleGateRunsToPhysical);
|
||||
* true — FSM «налету» + скважность несущей как у буферного пути (подшаги multiply/2 на шаг FSM).
|
||||
* Выставить до begin/rawSend (глобально на все IR_Encoder). Игнорируется при externalTxStartFn.
|
||||
*/
|
||||
static void setTxIsrLegacyMode(bool legacy);
|
||||
static bool txIsrLegacyMode();
|
||||
|
||||
/** Optional: register external TX backend (e.g. DMA driver). */
|
||||
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
|
||||
|
||||
@ -58,6 +87,13 @@ public:
|
||||
void disable();
|
||||
|
||||
void setBlindDecoders(IR_DecoderRaw *decoders[], uint8_t count);
|
||||
template <size_t N>
|
||||
void setBlindDecoders(IR_DecoderRaw *(&decoders)[N])
|
||||
{
|
||||
static_assert(N <= IR_PAIR_MUTE_MAX_ENCODERS,
|
||||
"IR_Encoder::setBlindDecoders: array size exceeds IR_PAIR_MUTE_MAX_ENCODERS");
|
||||
setBlindDecoders(decoders, static_cast<uint8_t>(N));
|
||||
}
|
||||
void rawSend(uint8_t *ptr, uint8_t len);
|
||||
|
||||
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
|
||||
@ -91,6 +127,8 @@ public:
|
||||
void _isr();
|
||||
private:
|
||||
static volatile bool carrierStopPending;
|
||||
static bool txIsrLegacyMode_;
|
||||
static uint16_t s_carrierMultiply;
|
||||
static void carrierResume();
|
||||
static void carrierPauseIfIdle();
|
||||
|
||||
@ -99,7 +137,8 @@ private:
|
||||
static void *externalTxCtx;
|
||||
IR_SendResult _sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len);
|
||||
|
||||
void setDecoder_isSending();
|
||||
void refreshBlindDecoderMuteState();
|
||||
void registerWithBlindDecoders();
|
||||
void sendByte(uint8_t byte, bool *prev, bool LOW_FIRST);
|
||||
void addSync(bool *prev, bool *next);
|
||||
uint32_t calculateSendTime(uint8_t packSize) const;
|
||||
@ -116,25 +155,66 @@ private:
|
||||
sync = 3
|
||||
};
|
||||
|
||||
IR_DecoderRaw *decPair;
|
||||
IR_DecoderRaw **blindDecoders;
|
||||
uint8_t decodersCount;
|
||||
struct TxFsmState
|
||||
{
|
||||
uint8_t sendLen = 0;
|
||||
uint8_t toggleCounter = 0;
|
||||
uint8_t dataBitCounter = 0;
|
||||
uint8_t dataByteCounter = 0;
|
||||
uint8_t preambFrontCounter = 0;
|
||||
uint8_t dataSequenceCounter = 0;
|
||||
uint8_t syncSequenceCounter = 0;
|
||||
bool syncLastBit = false;
|
||||
bool state = LOW;
|
||||
uint8_t *currentBitSequence = nullptr;
|
||||
SignalPart signal = noSignal;
|
||||
};
|
||||
|
||||
uint8_t sendLen;
|
||||
static bool txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||||
static bool txAdvanceAfterOutput(TxFsmState &st, const uint8_t *sendBufferLocal);
|
||||
static bool txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool &gateOut);
|
||||
void loadTxFsmFromMembers(TxFsmState &st) const;
|
||||
void storeTxFsmToMembers(const TxFsmState &st);
|
||||
|
||||
IrTxBsrrWave txBsrrWave_{};
|
||||
IR_TxGateRun txGateRuns_[irproto::kIsrTxMaxGateRuns]{};
|
||||
uint32_t txBsrrWords_[irproto::kIsrTxBsrrWordCount]{};
|
||||
uint16_t txBsrrReadIdx_ = 0;
|
||||
uint16_t txBsrrHalfLen_ = 0;
|
||||
uint32_t txBsrrTotalTicks_ = 0;
|
||||
uint32_t txBsrrTicksSent_ = 0;
|
||||
|
||||
/** Снимок на старт TX (буферный и legacy путь). */
|
||||
uint16_t txPowerSnap_ = 1;
|
||||
uint16_t txMultiplySnap_ = 2;
|
||||
|
||||
/** Legacy: физических тиков на один логический шаг FSM = multiply/2. */
|
||||
uint16_t legacyPhysPerLogical_ = 1;
|
||||
uint16_t legacyPhysCounter_ = 0;
|
||||
uint16_t legacySlotInPeriod_ = 0;
|
||||
|
||||
volatile uint16_t powerNumerator_ = 1;
|
||||
|
||||
IR_DecoderRaw *decPair = nullptr;
|
||||
IR_DecoderRaw *singleBlindDecoder = nullptr;
|
||||
IR_DecoderRaw **blindDecoders = nullptr;
|
||||
uint8_t decodersCount = 0;
|
||||
|
||||
uint8_t sendLen = 0;
|
||||
uint8_t sendBuffer[dataByteSizeMax]{0}; /// @brief Буффер данных для отправки
|
||||
|
||||
volatile bool isSending;
|
||||
volatile bool state; /// @brief Текущий уровень генерации
|
||||
volatile bool isSending = false;
|
||||
volatile bool state = LOW; /// @brief Текущий уровень генерации
|
||||
|
||||
volatile uint8_t dataByteCounter;
|
||||
volatile uint8_t dataByteCounter = 0;
|
||||
|
||||
volatile uint8_t toggleCounter; /// @brief Счётчик переключений
|
||||
volatile uint8_t dataBitCounter;
|
||||
volatile uint8_t toggleCounter = 0; /// @brief Счётчик переключений
|
||||
volatile uint8_t dataBitCounter = 0;
|
||||
|
||||
volatile uint8_t preambFrontCounter;
|
||||
volatile uint8_t dataSequenceCounter;
|
||||
volatile uint8_t syncSequenceCounter;
|
||||
volatile bool syncLastBit;
|
||||
volatile uint8_t preambFrontCounter = 0;
|
||||
volatile uint8_t dataSequenceCounter = 0;
|
||||
volatile uint8_t syncSequenceCounter = 0;
|
||||
volatile bool syncLastBit = false;
|
||||
|
||||
struct BitSequence
|
||||
{
|
||||
@ -144,5 +224,5 @@ private:
|
||||
static uint8_t bitHigh[2];
|
||||
static uint8_t bitLow[2];
|
||||
uint8_t *currentBitSequence = bitLow;
|
||||
volatile SignalPart signal;
|
||||
volatile SignalPart signal = noSignal;
|
||||
};
|
||||
|
||||
70
IR_config.h
70
IR_config.h
@ -6,6 +6,11 @@
|
||||
/** Число потоков DMA-TX задаётся шаблоном: IrDmaTxStm32<2>, см. IrDmaTxStm32.h и irproto::kDefaultDmaTxMaxStreams. */
|
||||
namespace irproto {
|
||||
constexpr size_t kDefaultDmaTxMaxStreams = 4U;
|
||||
/** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */
|
||||
constexpr uint16_t kIsrTxBsrrWordCount = 256U;
|
||||
/** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */
|
||||
constexpr size_t kIsrTxMaxGateRuns = 512U;
|
||||
static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be even");
|
||||
}
|
||||
|
||||
// Пошаговый разбор кадра на Serial (по умолчанию выключено). Пульсы IRDEBUG на пинах не меняют.
|
||||
@ -13,6 +18,16 @@ constexpr size_t kDefaultDmaTxMaxStreams = 4U;
|
||||
// Не обрывать приём сразу при накопленной sync-ошибке — «дописывать» до таймаута (только вместе с IRDEBUG_SERIAL_PACK).
|
||||
// #define IRDEBUG_SERIAL_SOFT_REJECT
|
||||
|
||||
// Краткий лог причин, почему физический сигнал не дошёл до распознанного пакета.
|
||||
// Формат и коды: ref/IR_RX_BRIEF_LOG.md
|
||||
#ifndef IR_RX_BRIEF_LOG
|
||||
#define IR_RX_BRIEF_LOG 1
|
||||
#endif
|
||||
// 1: печатать только отклонённые/ошибочные события; успехи и шумовые PREAMB скрыть.
|
||||
#ifndef IR_RX_BRIEF_LOG_REJECT_ONLY
|
||||
#define IR_RX_BRIEF_LOG_REJECT_ONLY 1
|
||||
#endif
|
||||
|
||||
// Журнал фронтов ИК в ISR; сброс строк @IRF1v1: в IR_DecoderRaw::tick(). См. ref/IR_EDGE_TRACE_FORMAT.md
|
||||
// Расход RAM ≈ IR_EDGE_TRACE_CAPACITY * 6 байт на декодер. Выключить — закомментировать:
|
||||
// #define IR_EDGE_TRACE
|
||||
@ -154,9 +169,60 @@ typedef uint16_t crc_t;
|
||||
#define subBufferSize 250 // Буфер для складирования фронтов, пока их не обработают (передатчик)
|
||||
#endif
|
||||
|
||||
#define preambPulse 3
|
||||
/** Максимальное число передатчиков, способных временно заглушить один декодер. */
|
||||
#ifndef IR_PAIR_MUTE_MAX_ENCODERS
|
||||
#define IR_PAIR_MUTE_MAX_ENCODERS 8U
|
||||
#endif
|
||||
|
||||
#define disablePairDec false // Отключать парный приёмник, возможны баги, используйте setBlindDecoders()
|
||||
/** Минимальная длительность удержания уровня (мкс): короче — импульс/пара фронтов выкидывается до tick()
|
||||
* (иголки на плато, дребезг). 0 — фильтр выключен, фронты идут в декодер как с ISR. */
|
||||
#ifndef IR_INPUT_MIN_PULSE_US
|
||||
#define IR_INPUT_MIN_PULSE_US 0
|
||||
#endif
|
||||
/** Сколько подтверждённых фронтов держать перед выпуском в декодер (потоковая задержка). */
|
||||
#ifndef IR_INPUT_FILTER_HOLDBACK_EDGES
|
||||
#define IR_INPUT_FILTER_HOLDBACK_EDGES 3U
|
||||
#endif
|
||||
/** Если новых фронтов нет, через minPulse*mult держатель принудительно сбрасывается в декодер. */
|
||||
#ifndef IR_INPUT_FILTER_TIMEOUT_MULT
|
||||
#define IR_INPUT_FILTER_TIMEOUT_MULT 5U
|
||||
#endif
|
||||
|
||||
/** Синхронно с IrFoxProtocolConstants.h / IrFoxDecoder (плагин Saleae). */
|
||||
#ifndef IR_SHORT_LOW_GLITCH_REJECT
|
||||
#define IR_SHORT_LOW_GLITCH_REJECT 1
|
||||
#endif
|
||||
#ifndef IR_GLITCH_REJECT_PHASE_NUDGE
|
||||
#define IR_GLITCH_REJECT_PHASE_NUDGE 1
|
||||
#endif
|
||||
#ifndef IR_MICRO_GAP_RISE_REJECT
|
||||
#define IR_MICRO_GAP_RISE_REJECT 1
|
||||
#endif
|
||||
/** Лок преамбулы: сколько одинаковых подряд периодов подъёма нужно для старта кадра. */
|
||||
#ifndef IR_PREAMBLE_LOCK_RISE_PERIODS
|
||||
#define IR_PREAMBLE_LOCK_RISE_PERIODS 2U
|
||||
#endif
|
||||
/** Допуск одинаковости периода преамбулы (проценты) + минимальная абсолютная полка в мкс. */
|
||||
#ifndef IR_PREAMBLE_JITTER_PCT
|
||||
#define IR_PREAMBLE_JITTER_PCT 18U
|
||||
#endif
|
||||
#ifndef IR_PREAMBLE_JITTER_US_MIN
|
||||
#define IR_PREAMBLE_JITTER_US_MIN 80U
|
||||
#endif
|
||||
/** Грубое окно валидности периода преамбулы RISE->RISE (в процентах от bitTime).
|
||||
* Для текущего протокола преамбула заметно длиннее обычного битового периода. */
|
||||
#ifndef IR_PREAMBLE_PERIOD_MIN_FACTOR_PCT
|
||||
#define IR_PREAMBLE_PERIOD_MIN_FACTOR_PCT 220U
|
||||
#endif
|
||||
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
|
||||
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
|
||||
#endif
|
||||
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. */
|
||||
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
|
||||
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U
|
||||
#endif
|
||||
|
||||
#define preambPulse 3
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "IR_Encoder.h"
|
||||
#include "IrTxBsrrWave.h"
|
||||
|
||||
#if defined(ARDUINO_ARCH_STM32) && defined(STM32G4xx)
|
||||
|
||||
@ -16,7 +17,7 @@
|
||||
#include "stm32g4xx_hal.h"
|
||||
|
||||
/**
|
||||
* STM32G4: ИК TX через DMA в GPIO BSRR, такт от TIM UPDATE (carrierFrec×2).
|
||||
* STM32G4: ИК TX через DMA в GPIO BSRR, такт от TIM UPDATE (carrierFrec × IR_Encoder::carrierMultiply()).
|
||||
*
|
||||
* Число слотов потоков — параметр шаблона (без макросов), например IrDmaTxStm32<2>.
|
||||
* По умолчанию: IrDmaTxStm32<> ≡ irproto::kDefaultDmaTxMaxStreams (см. IR_config.h).
|
||||
@ -37,7 +38,7 @@ public:
|
||||
uint32_t* dmaWords = nullptr;
|
||||
uint16_t dmaWordCount = 0;
|
||||
|
||||
IR_Encoder::IR_TxGateRun* gateRuns = nullptr;
|
||||
IrTxGateRun* gateRuns = nullptr;
|
||||
size_t maxGateRuns = 0;
|
||||
};
|
||||
|
||||
@ -134,13 +135,11 @@ private:
|
||||
uint16_t bufLen = 0;
|
||||
uint16_t halfLen = 0;
|
||||
|
||||
IR_Encoder::IR_TxGateRun* runs = nullptr;
|
||||
IrTxGateRun* runs = nullptr;
|
||||
size_t maxRuns = 0;
|
||||
|
||||
size_t runCount = 0;
|
||||
size_t runIndex = 0;
|
||||
uint16_t ticksLeftInRun = 0;
|
||||
bool carrierPhase = false;
|
||||
IrTxBsrrWave wave{};
|
||||
|
||||
uint32_t totalTicks = 0;
|
||||
volatile uint32_t ticksOutput = 0;
|
||||
@ -148,45 +147,14 @@ private:
|
||||
bool active = false;
|
||||
|
||||
void resetWave() {
|
||||
runIndex = 0;
|
||||
carrierPhase = false;
|
||||
wave.configure(setWord, resetWord, nullptr, 0, 2, 1);
|
||||
ticksOutput = 0;
|
||||
totalTicks = 0;
|
||||
runCount = 0;
|
||||
ticksLeftInRun = 0;
|
||||
}
|
||||
|
||||
IR_DMA_TX_HOT uint32_t nextWord() {
|
||||
if (runIndex >= runCount) {
|
||||
return resetWord;
|
||||
}
|
||||
const bool gate = runs[runIndex].gate;
|
||||
if (!gate) {
|
||||
carrierPhase = false;
|
||||
} else {
|
||||
carrierPhase = !carrierPhase;
|
||||
}
|
||||
const uint32_t out = (gate && carrierPhase) ? setWord : resetWord;
|
||||
|
||||
if (ticksLeftInRun > 0) {
|
||||
ticksLeftInRun--;
|
||||
}
|
||||
if (ticksLeftInRun == 0) {
|
||||
runIndex++;
|
||||
if (runIndex < runCount) {
|
||||
ticksLeftInRun = runs[runIndex].lenTicks;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
IR_DMA_TX_HOT void fill(uint32_t* dst, uint16_t count) {
|
||||
if (dst == nullptr || count == 0) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
*dst++ = nextWord();
|
||||
} while (--count != 0);
|
||||
wave.fill(dst, count);
|
||||
}
|
||||
|
||||
void onHalf() {
|
||||
@ -325,13 +293,25 @@ private:
|
||||
s.runCount = IR_Encoder::buildGateRuns(packet, len, s.runs, s.maxRuns);
|
||||
if (s.runCount == 0) return false;
|
||||
|
||||
size_t rc = s.runCount;
|
||||
if (!IR_Encoder::scaleGateRunsToPhysical(s.runs, &rc, s.maxRuns, IR_Encoder::carrierMultiply())) {
|
||||
return false;
|
||||
}
|
||||
s.runCount = rc;
|
||||
|
||||
uint32_t total = 0;
|
||||
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks;
|
||||
s.totalTicks = total;
|
||||
|
||||
s.runIndex = 0;
|
||||
s.ticksLeftInRun = s.runs[0].lenTicks;
|
||||
s.carrierPhase = false;
|
||||
const uint16_t mult = IR_Encoder::carrierMultiply();
|
||||
uint16_t pwr = mult / 2U;
|
||||
if (s.enc != nullptr) {
|
||||
const uint16_t want = s.enc->powerNumerator();
|
||||
const uint16_t cap = IR_Encoder::maxPowerNumerator();
|
||||
pwr = (want > cap) ? cap : want;
|
||||
}
|
||||
|
||||
s.wave.configure(s.setWord, s.resetWord, s.runs, s.runCount, mult, pwr);
|
||||
|
||||
s.fill(&s.dmaBuf[0], s.bufLen);
|
||||
|
||||
|
||||
87
IrTxBsrrWave.h
Normal file
87
IrTxBsrrWave.h
Normal file
@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include "IrTxGateTypes.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#if defined(__GNUC__)
|
||||
#define IR_TX_BSRR_WAVE_HOT __attribute__((always_inline)) inline
|
||||
#else
|
||||
#define IR_TX_BSRR_WAVE_HOT inline
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Генерация потока 32-бит слов для GPIO BSRR из RLE-сегментов IrTxGateRun.
|
||||
* За один период несущей — multiply физических тиков; при gate — powerN из них HIGH (N ≤ multiply/2).
|
||||
*/
|
||||
class IrTxBsrrWave {
|
||||
public:
|
||||
void configure(uint32_t setW, uint32_t resetW, IrTxGateRun* r, size_t n, uint16_t multiply, uint16_t powerN) {
|
||||
setWord = setW;
|
||||
resetWord = resetW;
|
||||
runs = r;
|
||||
runCount = n;
|
||||
multiply_ = multiply < 2 ? 2 : multiply;
|
||||
const uint16_t cap = static_cast<uint16_t>(multiply_ / 2U);
|
||||
powerN_ = (powerN > cap) ? cap : powerN;
|
||||
resetWave();
|
||||
}
|
||||
|
||||
void resetWave() {
|
||||
runIndex_ = 0;
|
||||
slotInPeriod_ = 0;
|
||||
ticksLeftInRun_ = 0;
|
||||
if (runCount > 0U && runs != nullptr) {
|
||||
ticksLeftInRun_ = runs[0].lenTicks;
|
||||
}
|
||||
}
|
||||
|
||||
IR_TX_BSRR_WAVE_HOT uint32_t nextWord() {
|
||||
if (runIndex_ >= runCount) {
|
||||
return resetWord;
|
||||
}
|
||||
const bool gate = runs[runIndex_].gate;
|
||||
uint32_t out;
|
||||
if (!gate) {
|
||||
slotInPeriod_ = 0;
|
||||
out = resetWord;
|
||||
} else {
|
||||
out = (slotInPeriod_ < powerN_) ? setWord : resetWord;
|
||||
slotInPeriod_++;
|
||||
if (slotInPeriod_ >= multiply_) {
|
||||
slotInPeriod_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (ticksLeftInRun_ > 0) {
|
||||
ticksLeftInRun_--;
|
||||
}
|
||||
if (ticksLeftInRun_ == 0) {
|
||||
runIndex_++;
|
||||
if (runIndex_ < runCount) {
|
||||
ticksLeftInRun_ = runs[runIndex_].lenTicks;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
|
||||
if (dst == nullptr || count == 0) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
*dst++ = nextWord();
|
||||
} while (--count != 0);
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t setWord = 0;
|
||||
uint32_t resetWord = 0;
|
||||
IrTxGateRun* runs = nullptr;
|
||||
size_t runCount = 0;
|
||||
uint16_t multiply_ = 2;
|
||||
uint16_t powerN_ = 1;
|
||||
size_t runIndex_ = 0;
|
||||
uint16_t ticksLeftInRun_ = 0;
|
||||
uint16_t slotInPeriod_ = 0;
|
||||
};
|
||||
13
IrTxGateTypes.h
Normal file
13
IrTxGateTypes.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Один RLE-сегмент огибающей несущей.
|
||||
* В buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec (как раньше).
|
||||
* После IR_Encoder::scaleGateRunsToPhysical — в физических тиках carrierFrec×multiply.
|
||||
*/
|
||||
struct IrTxGateRun {
|
||||
uint16_t lenTicks;
|
||||
bool gate;
|
||||
};
|
||||
@ -13,13 +13,16 @@ public:
|
||||
return start == end;
|
||||
}
|
||||
|
||||
void push(T element) {
|
||||
bool push(T element) {
|
||||
bool pushed = false;
|
||||
noInterrupts();
|
||||
if (!isFull()) {
|
||||
data[end] = element;
|
||||
end = (end + 1) % BufferSize;
|
||||
pushed = true;
|
||||
}
|
||||
interrupts();
|
||||
return pushed;
|
||||
}
|
||||
|
||||
T* pop() {
|
||||
|
||||
62
ref/IR_RX_BRIEF_LOG.md
Normal file
62
ref/IR_RX_BRIEF_LOG.md
Normal file
@ -0,0 +1,62 @@
|
||||
# IR RX Brief Log
|
||||
|
||||
Краткий лог включается через:
|
||||
|
||||
```cpp
|
||||
#define IR_RX_BRIEF_LOG 1
|
||||
#define IR_RX_BRIEF_LOG_REJECT_ONLY 1 // только отклонённые/ошибочные события
|
||||
```
|
||||
|
||||
Лог печатается короткими строками вида:
|
||||
|
||||
```text
|
||||
IRRX t=1234567 rsn=CRC len=25 err=3
|
||||
IRRX t=1234000 rsn=MUTE_BEGIN
|
||||
IRRX t=1234988 rsn=MUTE_END cnt=42
|
||||
```
|
||||
|
||||
Где:
|
||||
|
||||
- `t` — uptime МК в `micros()`
|
||||
- `rsn` — краткий код причины
|
||||
- остальные поля зависят от причины
|
||||
|
||||
## Коды `rsn`
|
||||
|
||||
| Код | Смысл | Типичные поля |
|
||||
|-----|-------|---------------|
|
||||
| `MUTE_BEGIN` | Началось окно mute: RX временно игнорирует вход, пока активен связанный TX | - |
|
||||
| `MUTE_END` | Окно mute завершилось; `cnt` показывает число заблокированных фронтов за всё окно | `cnt` |
|
||||
| `QRAW` | Потеря фронтов из-за переполнения сырой очереди `subBuffer` | `cnt` |
|
||||
| `QFLT` | Потеря фронтов из-за переполнения очереди после входного фильтра | `total` |
|
||||
| `HOLD` | Переполнен holdback фильтра до выпуска фронтов | `total` |
|
||||
| `GLITCH` | Фронт/пара фронтов отброшены как глитч | `total` |
|
||||
| `TIME` | Плохой тайминг фронтов/битов, кадр не может нормально разбираться | `rp`, `hp` |
|
||||
| `PREAMB` | Кандидат преамбулы не залочился или был перезапущен | `good`, `per` |
|
||||
| `SYNC` | Ошибка sync-бита привела к reject кадра | `err` |
|
||||
| `BUF` | Переполнен битовый буфер кадра | `bits` |
|
||||
| `TIMEOUT` | Кадр оборвался по таймауту до завершения; после записи в лог вызываются `isReciveRaw=false` и `firstRX()` (полный сброс декодера) | `bits`, `exp` |
|
||||
| `CRC` | Кадр дошёл до конца по длине, но CRC не сошёлся | `len`, `err` |
|
||||
| `OK` | Кадр успешно распознан | `len`, `err` |
|
||||
|
||||
## Поля
|
||||
|
||||
- `cnt` — число событий/фронтов, накопленных за окно или пакетную группу
|
||||
- `total` — накопленный счётчик отбраковок данного типа
|
||||
- `rp` — `risePeriod`
|
||||
- `hp` — `highTime`
|
||||
- `good` — число подряд подходящих периодов преамбулы перед срывом
|
||||
- `per` — период преамбулы/кандидата
|
||||
- `err` — суммарные ошибки `lowSignal + highSignal + other` либо счётчик sync-ошибок
|
||||
- `bits` — сколько data-бит успело накопиться
|
||||
- `exp` — ожидаемая длина кадра из первого байта, если уже известна
|
||||
- `len` — полная длина кадра в байтах
|
||||
|
||||
## Когда смотреть подробный debug
|
||||
|
||||
- `listenStart` / `checkTimeout` — в конце обработки фронта (`END:`) и во ветке «нет фронта» в `tick()`; не в начале до `pop`, иначе после таймаута `lastEdgeTime` расходится с метками ISR из очереди → ложные `TIMEOUT` (`bits=0`).
|
||||
- Пока в `subBuffer` / `filteredSubBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`.
|
||||
- Если нужен полный поток битов и sync: включать `IRDEBUG_SERIAL_PACK`
|
||||
- Если нужно понять, какие именно фронты пришли в ISR: включать `IR_EDGE_TRACE`
|
||||
- `IR_RX_BRIEF_LOG` нужен как короткий always-on-ish индикатор сути проблемы, без длинного дампа
|
||||
- `IR_RX_BRIEF_LOG_REJECT_ONLY=1` скрывает `OK` и `PREAMB`, оставляя только отклонения/ошибки
|
||||
151
test_examples/longData/longData.ino
Normal file
151
test_examples/longData/longData.ino
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Тест длинной полезной нагрузки IR (0x5E + строка, как ответ версии в Car).
|
||||
* Передача через DMA + TIM17 — как в машинке (Car/src/IR/IR.cpp, BoardTest/IR_DMA).
|
||||
* Авто-режим: чередование total длины кадра 24/25 байт.
|
||||
*/
|
||||
#include "IR_Decoder.h"
|
||||
#include "IR_Encoder.h"
|
||||
#include "TimerStatic.h"
|
||||
|
||||
/** 1 — TX через DMA (STM32G4). */
|
||||
#ifndef LONGDATA_USE_DMA
|
||||
#define LONGDATA_USE_DMA 0
|
||||
#endif
|
||||
|
||||
#if LONGDATA_USE_DMA
|
||||
#include <IrDmaTxStm32.h>
|
||||
#endif
|
||||
|
||||
static constexpr uint16_t kIrDeviceAddr = 0;
|
||||
static constexpr uint8_t kCmdVersion = 0x5E;
|
||||
static constexpr uint32_t kSerialBaud = 115200;
|
||||
static constexpr uint32_t kSendPeriodMs = 500;
|
||||
static constexpr uint8_t kMaxPayload = bytePerPack;
|
||||
static constexpr uint8_t kMaxParamBytes = kMaxPayload - 1;
|
||||
|
||||
static IR_Encoder enc(PIN_IR_ENC_FORWARD, kIrDeviceAddr, nullptr);
|
||||
static HardwareTimer irTimer(TIM17);
|
||||
|
||||
#if LONGDATA_USE_DMA
|
||||
namespace {
|
||||
constexpr size_t kIrDmaStreams = 1;
|
||||
constexpr uint16_t kIrDmaTxWordCount = 4096U;
|
||||
constexpr size_t kIrDmaTxMaxGateRuns = 1024U;
|
||||
static uint32_t s_irDmaWords[kIrDmaTxWordCount];
|
||||
static IR_Encoder::IR_TxGateRun s_irGateRuns[kIrDmaTxMaxGateRuns];
|
||||
} // namespace
|
||||
|
||||
static IrDmaTxStm32<kIrDmaStreams> dmaBackend;
|
||||
static bool txBusy(void * /*ctx*/) { return dmaBackend.busy(); }
|
||||
static bool txStart(void * /*ctx*/, IR_Encoder *e, const uint8_t *packet, uint8_t len) {
|
||||
return dmaBackend.start(e, packet, len);
|
||||
}
|
||||
#endif
|
||||
|
||||
static char s_paramAscii[kMaxParamBytes + 1];
|
||||
static uint8_t s_irPayload[kMaxPayload];
|
||||
static uint8_t s_irPayloadLen = 0;
|
||||
static uint32_t s_lastSendMs = 0;
|
||||
static bool s_sendLongerFrame = false;
|
||||
|
||||
// 24 байта total: msg(1)+addr(2)+addr(2)+data(17)+crc(2), где data=0x5E + 16 ASCII.
|
||||
static const char kPayload16[] = "Car_v4.3.9_[12MH";
|
||||
// 25 байт total: как выше, но data=0x5E + 17 ASCII.
|
||||
static const char kPayload17[] = "Car_v4.3.9_[12MHz]_G491";
|
||||
|
||||
static void rebuildIrPayload() {
|
||||
s_irPayload[0] = kCmdVersion;
|
||||
size_t n = 0;
|
||||
while (n < sizeof(s_paramAscii) && s_paramAscii[n]) {
|
||||
++n;
|
||||
}
|
||||
const size_t copyLen = (n > kMaxParamBytes) ? kMaxParamBytes : n;
|
||||
memcpy(s_irPayload + 1, s_paramAscii, copyLen);
|
||||
s_irPayloadLen = static_cast<uint8_t>(1 + copyLen);
|
||||
}
|
||||
|
||||
static void setAlternatingPayload() {
|
||||
const char* src = s_sendLongerFrame ? kPayload17 : kPayload16;
|
||||
strncpy(s_paramAscii, src, kMaxParamBytes);
|
||||
s_paramAscii[kMaxParamBytes] = '\0';
|
||||
s_sendLongerFrame = !s_sendLongerFrame;
|
||||
}
|
||||
|
||||
static void sendVersionPacket() {
|
||||
rebuildIrPayload();
|
||||
const IR_SendResult r = enc.sendData(IR_Broadcast, s_irPayload, s_irPayloadLen);
|
||||
Serial.print(F("TX 0x5E + "));
|
||||
Serial.print((unsigned)(s_irPayloadLen - 1));
|
||||
Serial.print(F(" B, ok="));
|
||||
Serial.print(r.success ? F("1") : F("0"));
|
||||
Serial.print(F(", t="));
|
||||
Serial.print(r.sendTimeMs);
|
||||
Serial.println(F(" ms"));
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(kSerialBaud);
|
||||
|
||||
strncpy(s_paramAscii, kPayload16, kMaxParamBytes);
|
||||
s_paramAscii[kMaxParamBytes] = '\0';
|
||||
rebuildIrPayload();
|
||||
|
||||
#if LONGDATA_USE_DMA
|
||||
// IR_Encoder::setCarrierMultiply(N); // до beginClockOnly; после смены — retuneCarrierClock()
|
||||
IR_Encoder::beginClockOnly(&irTimer);
|
||||
|
||||
IrDmaTxStm32<kIrDmaStreams>::Config cfg;
|
||||
cfg.timer = &irTimer;
|
||||
cfg.streamCount = kIrDmaStreams;
|
||||
cfg.streams[0].instance = DMA1_Channel1;
|
||||
cfg.streams[0].irq = DMA1_Channel1_IRQn;
|
||||
cfg.streams[0].dmamuxRequest = DMA_REQUEST_TIM17_UP;
|
||||
cfg.streams[0].enc = &enc;
|
||||
cfg.streams[0].dmaWords = s_irDmaWords;
|
||||
cfg.streams[0].dmaWordCount = kIrDmaTxWordCount;
|
||||
cfg.streams[0].gateRuns = s_irGateRuns;
|
||||
cfg.streams[0].maxGateRuns = kIrDmaTxMaxGateRuns;
|
||||
|
||||
if (!dmaBackend.begin(cfg)) {
|
||||
Serial.println(F("[IR_DMA] init FAILED"));
|
||||
return;
|
||||
}
|
||||
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr);
|
||||
#elif LONGDATA_LEGACY_ISR
|
||||
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
|
||||
#else
|
||||
IR_Encoder::begin(&irTimer, 1, TIM17_IRQn, 0);
|
||||
#endif
|
||||
enc.enable();
|
||||
|
||||
#if LONGDATA_USE_DMA
|
||||
Serial.println(F("longData: DMA TX alternating 24/25 bytes"));
|
||||
#elif LONGDATA_LEGACY_ISR
|
||||
Serial.println(F("longData: legacy ISR TX (pre-unified FSM) 24/25 bytes"));
|
||||
#else
|
||||
Serial.println(F("longData: ISR TX (unified FSM) alternating 24/25 bytes"));
|
||||
#endif
|
||||
Serial.print(F("Auto-period ms = "));
|
||||
Serial.println((unsigned long)kSendPeriodMs);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
#if LONGDATA_USE_DMA
|
||||
IR_Encoder::tick();
|
||||
#endif
|
||||
|
||||
const uint32_t now = millis();
|
||||
if (now - s_lastSendMs >= kSendPeriodMs) {
|
||||
s_lastSendMs = now;
|
||||
setAlternatingPayload();
|
||||
sendVersionPacket();
|
||||
}
|
||||
}
|
||||
|
||||
#if LONGDATA_USE_DMA && defined(STM32G4xx)
|
||||
extern "C" void DMA1_Channel1_IRQHandler(void) {
|
||||
if (auto *p = IrDmaTxStm32<kIrDmaStreams>::instance()) {
|
||||
p->irqForStream(0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
100
tools/analyze_pkt33_car_point.py
Normal file
100
tools/analyze_pkt33_car_point.py
Normal file
@ -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()
|
||||
517
tools/ir_decoder_raw_sim.py
Normal file
517
tools/ir_decoder_raw_sim.py
Normal file
@ -0,0 +1,517 @@
|
||||
#!/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 сбрасывают вызывающие пути (listenStart, checkTimeout, конец кадра);
|
||||
затем firstRX() обнуляет буфер и preambleResetToIdle()."""
|
||||
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: как IR_DecoderRaw — пауза по lastEdgeTime (между обработанными фронтами), не по prevRise.
|
||||
if st.last_edge > 0 and st.is_recive_raw and (t_us - st.last_edge) > irmax * 2:
|
||||
st.is_recive_raw = False
|
||||
first_rx(st)
|
||||
|
||||
# checkTimeout: как IR_DecoderRaw после фикса — isReciveRaw=0 и firstRX(), иначе залипание FSM.
|
||||
if st.last_edge > 0 and st.is_recive and (t_us - st.last_edge) > irmax * 2:
|
||||
st.is_recive = False
|
||||
st.is_recive_raw = False
|
||||
first_rx(st)
|
||||
# Не подставлять last_edge = t_us здесь: как IR_DecoderRaw после фикса.
|
||||
|
||||
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
|
||||
first_rx(st)
|
||||
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: буфер не чистят на успешном CRC; сброс по listenStart/checkTimeout/firstRX
|
||||
|
||||
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()
|
||||
@ -16,7 +16,8 @@ WRONG_PACK_SYNC — отдельное событие с причиной и т
|
||||
отброшенных фронтов; без флага эти события только в счётчиках сводки.
|
||||
|
||||
Не моделирует IRDEBUG_SERIAL_SOFT_REJECT (жёсткий Wrong sync).
|
||||
Таймауты: между фронтами, если gap > IR_timeout*2 и isRecive — checkTimeout.
|
||||
Таймауты: как IR_DecoderRaw::tick — listenStart и checkTimeout в начале каждого тика (не только при пустых
|
||||
очередях), пауза > IR_timeout*2 по lastEdgeTime; при checkTimeout: isReciveRaw=0, firstRX(), lastEdgeTime=now.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -360,8 +361,8 @@ class DecoderSim:
|
||||
|
||||
def listen_start(self, now: int) -> None:
|
||||
to = ir_timeout_us(self.rise_sync_time)
|
||||
if self.is_recive_raw and (now - self.prev_rise) > to * 2:
|
||||
self.events.append(f"t={now} listenStart abort raw (gap from prev_rise)")
|
||||
if self.is_recive_raw and self.last_edge_time > 0 and (now - self.last_edge_time) > to * 2:
|
||||
self.events.append(f"t={now} listenStart abort raw (gap since last edge, как IR_DecoderRaw)")
|
||||
self.is_recive_raw = False
|
||||
self._clear_packet_state()
|
||||
self.first_rx()
|
||||
@ -371,9 +372,12 @@ class DecoderSim:
|
||||
return
|
||||
to = ir_timeout_us(self.rise_sync_time)
|
||||
if now - self.last_edge_time > to * 2:
|
||||
self.events.append(f"t={now} checkTimeout (gap since last edge)")
|
||||
self.events.append(f"t={now} checkTimeout -> isReciveRaw=0, firstRX() (как IR_DecoderRaw)")
|
||||
self.is_recive = False
|
||||
self.last_edge_time = now
|
||||
self.is_recive_raw = False
|
||||
self._clear_packet_state()
|
||||
self.first_rx()
|
||||
# Не last_edge_time = now: в прошивке убрано — расхождение с метками фронтов из очереди.
|
||||
|
||||
def write_to_buffer(self, bit_val: int) -> None:
|
||||
if self.i_data_buffer > DATA_BYTE_SIZE_MAX * 8:
|
||||
@ -382,6 +386,7 @@ class DecoderSim:
|
||||
if self.is_buffer_overflow or self.is_preamb or self.is_wrong_pack:
|
||||
self.is_recive = False
|
||||
self.is_recive_raw = False
|
||||
self.first_rx()
|
||||
return
|
||||
|
||||
if self.buf_bit_pos == self.next_control_bit:
|
||||
@ -478,6 +483,9 @@ class DecoderSim:
|
||||
def tick_edge(self, t: int, level: int) -> None:
|
||||
"""Один фронт: level = состояние линии ПОСЛЕ фронта (как dir в C++)."""
|
||||
self.listen_start(t)
|
||||
to = ir_timeout_us(self.rise_sync_time)
|
||||
if self.is_recive and self.last_edge_time > 0 and (t - self.last_edge_time) > to * 2:
|
||||
self.check_timeout(t)
|
||||
self.last_edge_time = t
|
||||
rising = level == 1
|
||||
|
||||
@ -689,9 +697,6 @@ def main() -> int:
|
||||
|
||||
dec = DecoderSim(verbose=args.verbose)
|
||||
for e in edges:
|
||||
to_us = ir_timeout_us(dec.rise_sync_time)
|
||||
if dec.is_recive and dec.last_edge_time > 0 and (e.t_us - dec.last_edge_time) > to_us * 2:
|
||||
dec.check_timeout(e.t_us)
|
||||
dec.tick_edge(e.t_us, e.level)
|
||||
|
||||
print("--- События декодера (первые N), пакеты разделены пустой строкой ---")
|
||||
|
||||
Reference in New Issue
Block a user