46 Commits

Author SHA1 Message Date
98c5eb0ef4 hotfix(analyzer): sync IR Fox RX terminal behavior 2026-09-09 18:15:44 +03:00
78169d1c59 Merge pull request #14 from Show-maket/analyzer/ir-fox-classifier
Анализатор IR_Fox: классификатор пакетов, настройки/результаты, тесты + перечень изменений протокола
2026-09-09 17:14:32 +03:00
fe4512d47f analyzer(IR_Fox): классификатор пакетов, настройки и результаты анализатора Saleae, тесты (работа Даши, снято с рабочей копии 09.09) 2026-09-09 17:10:32 +03:00
c01dfe16dd Merge pull request #13 from Show-maket/STM32
STM32 → main: платформенная ветка с ИК-циклом (после #12)
2026-09-09 17:02:34 +03:00
5759658d42 Merge pull request #12 from Show-maket/claude/rx-terminal-api
RX по состоянию, airtime формулами, DMA по ранам, гвард декодера после обрыва (для Car v4.3.11)
2026-09-09 17:02:16 +03:00
1b408b0de6 Merge branch 'STM32' into claude/rx-terminal-api 2026-09-09 17:01:57 +03:00
baac9fbf46 fix(rx): после обрыва кадра не требовать 30 мс тишины перед новым кандидатом преамбулы (releasePreambleGuard)
prevRise (последний декодированный фронт) не сбрасывался при abortFrame, а отброшенные гвардом фронты его не двигают —
валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика. Стенд 09.09: КУ теряла
пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга (V-серия 75–83 %, после правки 100 %).
Хост-тест (настоящий IR_DecoderRaw.cpp + шим Arduino): после abort кадр через 5…28 мс — терялся, теперь принимается;
чистый конец кадра не тронут (мин. пауза 26.25 мс), хвост оборванного кадра ложных захватов не даёт, 200/200 валидных
среди мусора, стресс 166→176/200.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-09 16:09:55 +03:00
5b220dadd8 Revert "refactor(config): IR_ResponseDelay перенесён в IR_config.h"
This reverts commit b375aa169e.
2026-09-08 14:13:14 +03:00
b375aa169e refactor(config): IR_ResponseDelay перенесён в IR_config.h
Зависит только от конфигурационных констант (bitTime, tolerance, syncBits) — это таймаут байта
(irRxByteTimeoutUs) × 2.7735 = 42 мс; значение не изменилось. Потребителям достаточно IR_config.h.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-08 14:09:38 +03:00
883c0b00cf perf(tx): заполнение DMA-буфера по ранам вместо пословного автомата
IrTxBsrrWave::fill: пауза (gate=0) — один цикл записи resetWord, несущая — копия шаблона периода
(powerN слов setWord + остальные resetWord); состояние переносится через границы порций как у
nextWord(). На 12 МГц предзаполнение 4096 слов перед стартом DMA и ISR-дозаполнение половин
(2048 слов каждые ~8.9 мс кадра — было ~88% CPU) укорачиваются в разы: старт несущей после
вызова send 15.7 → ~5 мс (с построением ранов). Эквивалентность проверена на хосте: 17856
случаев (len 1..31, multiply 2..8, powerN 1..multiply/2, порции 2048/случайные/по 1 слову),
0 расхождений на 719 млн слов.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 17:56:23 +03:00
d103d2a3ae perf(tx): таблица ранов кадра строится по границам ранов, а не по тактам несущей
buildGateRuns/buildPhysicalGateRuns крутили автомат передачи по одному такту (пинг 10 байт =
8728 тактов) — на 12 МГц это ~30 мс между вызовом send и стартом DMA, на каждой передаче
(задний ~20 мс). Обход по границам (txWalkRuns: ран = toggleCounter+1 тактов уровня state)
даёт ту же последовательность за число шагов = число ранов (225). Эквивалентность проверена на
хосте: 12480 случаев, len 1..38, множитель 2..8, логические и физические раны совпадают
бит-в-бит (len=39 — чтение за буфером в sync-фазе автомата у обеих версий, на эфире ≤31).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 17:33:20 +03:00
86956bcf99 fix(rx): таймаут кандидата преамбулы 3×→1× IR_timeout
Кандидат без фронтов дольше таймаута байта преамбулой быть не может. При 3× (45 мс)
rxLineActive() держал линию «занятой» после паразитных фронтов (засветка своим
дальномером) и откладывал пинг машинки на ~30 мс сверх заданного гэпа.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 17:07:22 +03:00
5b9f73ec7c feat(config): irRxByteTimeoutUs/irRxAbortSilenceUs — таймаут байта и тишина обрыва приёма формулой
Константы использует Car (IrCycle/Executer) вместо макроса IR_timeout,
который завязан на член класса декодера.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 16:30:44 +03:00
8f45f6e214 feat(rx): наблюдаемость приёма по состоянию + airtime кадра формулами
- IR_config.h: irFrameAirtimeUs/irFrameDecodeEndUs/irLockToDecodeEndUs/irLockLatencyUs,
  irMaxPackSize (31), irDataPackSize/irBackPackSize — все из констант FSM передатчика
  (преамбула 6×98 тактов, байт 11 бит × 74 такта, такт = полпериода несущей).
- IR_Encoder::calculateSendTime по той же формуле (раньше синхробиты считались один раз
  на кадр → занижение 21-26%, потребители компенсировали +30%).
- IR_DecoderRaw: rxLockSeq/rxLockTimeUs (лок преамбулы), rxMsgType, rxExpectedEndUs
  (по объявленной длине), rxLastEnd {seq, reason Ok/Crc/Timeout/Abort, msgType, packSize,
  tUs, expectedEndUs}. Терминалы: конец кадра, таймаут тишины, немедленный abort при
  sync-ошибке / длине <3 (в т.ч. 0) / переполнении — битый кадр больше не держит
  isReciving до конца чужой передачи + 30 мс.
- Порядок в tick: checkTimeout до listenStart (TIMEOUT-лог с реальной длиной), истечение
  кандидата преамбулы без фронтов, ложный PREAMB-инкремент на старте кандидата убран.
- rxMaxPackSize() = протокольные 31 (было 38 = размер буфера).

Совместимость: только добавления; существующие сигнатуры не тронуты. Собрано для G4 (Car)
и F4 (КУ).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA1K5dBUKrnoXVwyoVjzq
2026-09-07 12:22:22 +03:00
6c97d33c7c feat(rx): rxDeclaredPackSize/rxMaxPackSize + packAirtimeMs — бюджет удержания по размеру
Для гейта заднего у машинки (Даша 04.09): держать задний по СОСТОЯНИЮ приёма, но
не дольше airtime кадра — а если объявленный в 1-м байте размер МЕНЬШЕ протокольного
максимума, то ровно до его конца (усиление надёжности). rxDeclaredPackSize() =
packSize из 1-го байта, если валиден (не битый) и идёт приём, иначе 0 (потребитель
берёт rxMaxPackSize() — до CRC значение недоверенное). packAirtimeMs() — публичная
обёртка над calculateSendTime (чистая функция протокольных констант, годна и для RX).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3Ca6J2JJ1NrptqQjKs8BK
2026-09-04 15:51:59 +03:00
628c050702 feat(rx): rxLineActive() — признак «идёт валидный приём» по СОСТОЯНИЮ
Декодер уже знает состояние приёма (isReciving=локнут; preambleState Idle/
Candidate/Locked). rxLineActive() = локнут ИЛИ формируется ВАЛИДНАЯ преамбула
(>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного шумового
фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта
заднего у машинки: не стрелять, пока на линии идёт/формируется ответ точки —
решение по состоянию, а не по хардкод-длительности (Даша 04.09). Аддитивно, const.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3Ca6J2JJ1NrptqQjKs8BK
2026-09-04 15:09:27 +03:00
a589416cfc reconstruct: restore brave-tape3 decoder stage 2026-08-28 14:35:19 +03:00
e25feb6824 reconstruct: restore brave-tape3 packet types stage 2026-08-28 14:30:44 +03:00
96ffb91b97 fix(tx): приоритет IRQ DMA-TX явно ниже SysTick/EXTI — millis() замедлялся под передачей
HAL_NVIC_EnableIRQ без SetPriority оставлял DMA-каналы TX на приоритете 0 (уровень
SysTick на G4). Длинный refill кольца BSRR (полубуфер 2048 слов = ~3.3мс при HCLK
12МГц) не вытеснялся, тики SysTick слипались: millis() замедлялся в 1.6x (один
излучатель) / 2.2x (два) на всё время передачи. На машинке это ~37% вранья часов
в каждый ИК-кадр: PID dt, планировщик, тайминги протокола.

Мерено на стенде TofSickHunt 2026-08-11 (Car raw/idle_cur_vs_coup*): прогоны «60с»
шли 85-95 реальных секунд. Фикс: irproto::kDmaTxIrqPriority=8 — ниже EXTI приёмника
(6), выше дна; SysTick честный. ISR-TX путь (F401/КУ) не подвержен: его refill на
TIM-приоритете 14. Верификация дилатации на стенде — раунд 4 (прерван, перезапустить).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:33:07 +03:00
08ea71eb5e feat(rx): always-on счётчики причин RX-событий (rxReasonCnt) + RXSTAT-сводка
rxBriefLog теперь безусловен: ВСЕГДА инкрементирует rxReasonCnt[reason]
(наблюдаемость по контракту живучести — работает в проде без печати),
печать события — только при IR_RX_BRIEF_LOG. 14 точек вызова развёрнуты
из-под #if (Glitch/Timing/Preamble/Sync/BufOverflow/Timeout/Crc/Ok +
pulse-filter пути); ISR-агрегатные MuteBegin/End/RawOverflow остаются
только при логе (их флаш живёт в brief-механике). Публичное API:
rxReasonCounters() / rxReasonCountersClear() / printRxReasonStats(Print&)
-> 'RXSTAT,GLITCH=..,TIME=..,...,OK=..'.
Компил-чек: LaserTestCheck (G4, флаг выкл) и Plan_B (F4, флаг вкл) — чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 23:05:50 +03:00
5bcaf4092a perf(rx): батч-выемка фронтов в tick (IR_RX_TICK_BATCH=16) — приём развязан от частоты loop
Было: 1 сырой фронт за tick() -> RX-пропускная способность привязана к loop;
медленный loop (телеметрия/дисплей/КУ-печать) переполнял ISR-буфер 250 фронтов
(~120мс эфира) и кадры гибли молча (вероятная составляющая 'КУ отвечает не на
каждый пакет'). Стало: до IR_RX_TICK_BATCH фронтов за tick; idle-семантика
сохранена (flush пульс-фильтра только при пустом буфере; listenStart/
checkTimeout как раньше). Переопределяемо дефайном до include.
Компил-чек: LaserTestCheck (G491) собирается.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 18:22:44 +03:00
873277e59f docs 2026-07-01 15:33:49 +03:00
f93f4101c3 fix(tx): correct multi-byte BACK frames (packSize used min(1,len))
_sendBack и testSendBack считали packSize с min(uint8_t(1),len) → в кадр входил только 1 байт данных, остальные обрезались (и затирались CRC). Многобайтовый back (speed + customBackData/sendOrderedBack) слался битым. Теперь packSize = ...+len+crcBytes, плюс валидация полного packSize<=31 (IR_MASK_MSG_INFO). Для len=1 поведение не меняется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:20:27 +03:00
bf14b3d7ad fix(rx/tx): B5 pop-by-value, B6 ISR micros mask, B7 crc parens, B10 div guard, send length validation
B5 (MEDIUM): RingBuffer::pop(T&) копирует под одной критсекцией; tick() перешёл на неё → нет торн-рида (внутренний interrupts() в T* pop снимал внешнюю защиту до чтения *ptr). T* pop() оставлен (не используется).
B6 (MEDIUM): убрана лишняя noInterrupts/interrupts вокруг micros() в EXTI-ISR (снимала PRIMASK посреди ISR). std::function-диспетчеризация attachInterrupt — структурна, не трогаю.
B7 (LOW): скобки в crcCheck (== & 0xFF по приоритету).
B10 (LOW): guard деления на 0 в ceil_div (актуально только при freeFrec — НЕ включаю).
send: sendDataFULL — валидация полного packSize<=31 (было len>bytePerPack=31, packSize=7+len оборачивался → тихая потеря Data payload 25..31).
B8 (isSubBufferOverflow) — уже volatile, потеря флага безвредна (диагностика), не трогаю. B9 (prevRise==0 при micros()==0) — уже обработан веткой в preambleProcessEdge, намеренно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:38:51 +03:00
a8d32bbcef fix(rx): guard sub-minimum packSize before crcCheck (B1 OOB read)
packSize==1 давало crcCheck(1-crcBytes) → uint8 len=255 → crc8 читает dataBuffer[0..256] при массиве 38 (OOB-чтение ~217 байт). Триггер — лок преамбулы + первый байт с младшими 5 битами=1 (~1/32 ложных локов). Двойная защита: (1) ранний reject packSize 1..2 (< msg+crc) как битого; (2) конец кадра требует packSize>crcBytes, поэтому crcCheck(packSize-crcBytes) не уходит в underflow. Валидные кадры (packSize>=5) и будущие компактные (>=3) не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:13:27 +03:00
fc3510c425 fix(rx): guard null encoder in auto-accept (B2), init accept fields (B4), fix dataBuffer OOB off-by-one (B3)
B2 (HIGH): IR_Decoder::_tick — encoder->sendAccept() без null-check; у Car decoder создан с encoder==nullptr → HardFault при приёме IR_MSG_DATA_ACCEPT. Добавлен guard.
B4 (MEDIUM): acceptSendTimer/isWaitingAcceptSend/addrAcceptSendTo/acceptCustomByte не инициализировались → мусор мог спонтанно дёрнуть sendAccept. Дефолты в .h.
B3 (HIGH): writeToBuffer — '>' вместо '>=' → при i_dataBuffer==dataByteSizeMax*8 (304) запись dataBuffer[38] за границей 38-байтового массива (packSize==0 runaway). Валидные кадры закрываются при <=248, не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:34:12 +03:00
e5ed029cfd fix(ir-dma): __DSB() barriers before DMA arm and after circular refill (#8)
Перестраховка корректности (на M4F без D-кэша обычно не обязательно): завершить запись dmaBuf/BSRR до HAL_DMA_Start_IT и refill полубуфера до прохода циркулярного DMA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:12:28 +03:00
55935b8b92 fix(ir-dma): volatile active + errorCount() observability; live poll() in longData example
- IrDmaTxStm32.h: TxStream::active -> volatile (делится поток<->ISR: busy()/poll()/колбэки/спин applyCarrierMultiply)
- IrDmaTxStm32.h: + volatile errors_ и errorCount(), инкремент в dmaErrorCb (наблюдаемость Transfer-Error)
- test_examples/longData: dmaBackend.poll() в loop, иначе watchdog завершения DMA мёртв в эталонном примере

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:25:36 +03:00
7176fe5321 fix(ir-dma): eliminate isSending latch — free-running TIM, completion watchdog, busy() OR
C: remove racy activeCount_ + shared-timer gating; TIM17 free-running, per-stream DMA arming only (HAL_DMA_Start_IT/Abort_IT). Sets active before arming; never stops TIM on a shared counter.

D: poll() watchdog detects ticksOutput stall (>=stallTimeoutMs_, default 100ms) and forceStop()s the stuck stream under a per-channel NVIC mask; recoveryCount()/setStallTimeoutMs() accessors. dmaErrorCb now checks !active for symmetric single-stop.

E: busy() now returns true if ANY stream active (was AND) — honest contract for irAnyTxBusy()/carrier retune; does not serialize the send path.

Bug: IR TX rarely latched (LED frozen on/off) because stopStream was reachable only from the DMA HT/CPLT IRQ. Hardware bring-up (logic analyzer, recoveryCount telemetry) pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:26:52 +03:00
b1d7016147 analyzer 2026-06-02 11:19:32 +03:00
9935ac192a Merge pull request #11 from Show-maket/fix-DMA-and-Analyzer
Fix dma and analyzer
2026-04-21 07:05:29 +00:00
8361828c44 tx error tracking and inline glitch filter 2026-04-21 10:00:46 +03:00
38d93edd1b upd 2026-04-20 17:47:33 +03:00
2f5b57680b Add documentation for IR TX modes in the library 2026-04-20 14:49:38 +03:00
31ac7a3625 Refactor IR decoder and encoder for improved pulse filtering and ISR handling. Removed unused filtered sub-buffer, updated pulse filter methods, and added support for buffered ISR storage in the encoder. Enhanced documentation for clarity on DMA TX backend and ISR modes. 2026-04-20 14:48:45 +03:00
b4f644c258 Align main with fix-DMA-and-Analyzer (encoder API, sketch, .gitignore)
Restore IR_SendResult + sendDataFULL, fix IR-protocol.ino, match .gitignore to analyzer branch.

Made-with: Cursor
2026-04-17 15:07:54 +03:00
a75fd0c564 Merge branch 'main' of https://github.com/Show-maket/IR-protocol 2026-04-17 14:31:32 +03:00
4061a4ed10 Merge pull request #10 from Show-maket/fix-DMA-and-Analyzer
Fix dma and analyzer
2026-04-17 11:29:56 +00:00
fc93ea41db Merge pull request #8 from Show-maket/STM32
Stm32
2026-03-11 17:06:20 +03:00
0620d98e35 Merge branch 'main' into STM32 2026-03-11 17:06:06 +03:00
4caed06218 Merge pull request #7 from Show-maket/STM32DMA
upd
2026-03-11 16:59:08 +03:00
37522f974f upd 2024-12-25 16:56:18 +03:00
6375c4eed5 for debug test 2024-09-04 10:12:39 +03:00
c4000d6b75 test 2024-08-14 17:43:12 +03:00
fc02c79135 upd 2024-04-22 14:56:12 +03:00
d068a576f7 Merge pull request #3 from Show-maket/dev
v2.0
2024-03-18 16:24:38 +03:00
38 changed files with 3191 additions and 548 deletions

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ Analyzer/raw/dll/*.so
Analyzer/raw/dll/*.dylib
/Analyzer/raw/IR_Fox/.github
**/.build
graphify-out/*
**/.build-*/
/tests/*.exe

View File

@ -15,6 +15,7 @@ set(SOURCES
src/IrFoxAnalyzer.h
src/IrFoxDecoder.cpp
src/IrFoxDecoder.h
src/IrFoxPacketClassifier.h
src/IrFoxAnalyzerResults.cpp
src/IrFoxAnalyzerResults.h
src/IrFoxAnalyzerSettings.cpp
@ -24,3 +25,23 @@ set(SOURCES
)
add_analyzer_plugin(${PROJECT_NAME} SOURCES ${SOURCES})
if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
endif()
include(CTest)
if(BUILD_TESTING)
add_executable(IrFoxPacketClassifierTests tests/IrFoxPacketClassifierTests.cpp)
target_include_directories(IrFoxPacketClassifierTests PRIVATE src)
add_test(NAME IrFoxPacketClassifierTests COMMAND IrFoxPacketClassifierTests)
add_executable(IrFoxDecoderTests tests/IrFoxDecoderTests.cpp src/IrFoxDecoder.cpp)
target_include_directories(IrFoxDecoderTests PRIVATE src)
target_link_libraries(IrFoxDecoderTests PRIVATE Saleae::AnalyzerSDK)
add_custom_command(TARGET IrFoxDecoderTests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:Saleae::AnalyzerSDK>
$<TARGET_FILE_DIR:IrFoxDecoderTests>)
add_test(NAME IrFoxDecoderTests COMMAND IrFoxDecoderTests)
endif()

View File

@ -1,6 +1,7 @@
#include "IrFoxAnalyzer.h"
#include "IrFoxAnalyzerSettings.h"
#include "IrFoxDecoder.h"
#include "IrFoxPacketClassifier.h"
#include <AnalyzerChannelData.h>
#include <AnalyzerResults.h>
#include <algorithm>
@ -25,7 +26,6 @@ IrFoxAnalyzer::~IrFoxAnalyzer()
void IrFoxAnalyzer::SetupResults()
{
m_packet_hex_by_frame.clear();
mResults.reset(new IrFoxAnalyzerResults(this, &mSettings));
SetAnalyzerResults(mResults.get());
mResults->AddChannelBubblesWillAppearOn(mSettings.mInputChannel);
@ -46,35 +46,91 @@ static void append_hex(std::string& s, const uint8_t* p, size_t n, size_t max_by
s += "...";
}
const char* IrFoxAnalyzer::PacketHexForFrame(U64 frame_id)
static const char* packet_status_icon(IrFoxPacketOutcome outcome)
{
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();
switch (outcome)
{
case IrFoxPacketOutcome::Accepted:
return "";
case IrFoxPacketOutcome::IgnoredAddress:
return "📭";
case IrFoxPacketOutcome::RejectedCrc:
case IrFoxPacketOutcome::RejectedLength:
return "";
case IrFoxPacketOutcome::RawOnlyUnknownType:
case IrFoxPacketOutcome::RawOnlyTypedLength:
return "⚠️";
}
return "⚠️";
}
const char* IrFoxAnalyzer::BubbleTextForFrame(U64 frame_id) const
static const char* message_type_icon(uint8_t message_type)
{
auto it = m_bubble_text_by_frame.find(frame_id);
if (it == m_bubble_text_by_frame.end())
switch (message_type)
{
case irfox::kMsgBack:
return "🔙";
case irfox::kMsgAccept:
return "🤝";
case irfox::kMsgRequest:
return "📣";
case irfox::kMsgBackTo:
return "🎯";
case irfox::kMsgDataNoAccept:
return "📦";
case irfox::kMsgDataAccept:
return "📨";
default:
return "⚠️";
}
}
static const char* terminal_abort_cause_text(IrFoxAbortCause cause)
{
switch (cause)
{
case IrFoxAbortCause::BadSync:
return "SYNC";
case IrFoxAbortCause::BadLength:
return "LEN";
case IrFoxAbortCause::Overflow:
return "OVF";
case IrFoxAbortCause::None:
default:
return "";
m_bubble_scratch = it->second;
return m_bubble_scratch.c_str();
}
}
static std::string packet_icon(const IrFoxPacketDecision& decision, IrFoxPacketIconMode mode)
{
const char* status = packet_status_icon(decision.outcome);
// Icon-mode selection describes successfully accepted packets. Diagnostic
// outcomes must remain visible even when the user selected type-only mode.
if (decision.outcome != IrFoxPacketOutcome::Accepted)
return status;
const char* type = message_type_icon(decision.message_type);
switch (mode)
{
case IrFoxPacketIconMode::Status:
return status;
case IrFoxPacketIconMode::MessageType:
return type;
case IrFoxPacketIconMode::StatusAndType:
default:
return std::string(status) + type;
}
}
void IrFoxAnalyzer::WorkerThread()
{
mIr = GetAnalyzerChannelData(mSettings.mInputChannel);
m_packet_hex_by_frame.clear();
m_bubble_text_by_frame.clear();
mResults->ClearCachedFrameText();
const U32 fs = GetSampleRate();
IrFoxDecoder decoder;
decoder.reset();
/** Потоковый фильтр: убирает импульсы короче kMinFilteredPulseUs (иголки/дребезг в сэмплах). */
/** Mirrors the firmware input filter. kMinFilteredPulseUs=0 means direct edge delivery. */
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
@ -115,8 +171,19 @@ void IrFoxAnalyzer::WorkerThread()
U32 frames_since_commit = 0;
const U32 kCommitBatch = 256;
const bool detailed_presentation = mSettings.mPresentation == IrFoxPresentation::Detailed;
std::vector<IrFoxEmitBit> pending_byte_frames;
pending_byte_frames.reserve(irfox::kDataByteSizeMax);
IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) {
auto note_legacy_frame = [&]() {
if (++frames_since_commit >= kCommitBatch)
{
mResults->CommitResults();
frames_since_commit = 0;
}
};
auto add_event_frame = [&](const IrFoxEmitBit& e) {
Frame frame;
frame.mStartingSampleInclusive = static_cast<S64>(e.start_sample);
frame.mEndingSampleInclusive = static_cast<S64>(e.end_sample);
@ -124,45 +191,255 @@ void IrFoxAnalyzer::WorkerThread()
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;
mResults->AddFrame(frame);
note_legacy_frame();
};
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)
auto flush_pending_bytes = [&]() {
for (const IrFoxEmitBit& byte_event : pending_byte_frames)
add_event_frame(byte_event);
pending_byte_frames.clear();
};
IrFoxOnBit on_bit = [&](const IrFoxEmitBit& e) {
// Per-bit markers dominate Logic's render cost. They belong to Detailed
// only; Overview keeps a fast packet-level timeline.
if (e.frame_type == IRF_FT_DATA_BIT)
{
mResults->CommitResults();
frames_since_commit = 0;
if (detailed_presentation)
{
// Markers, like legacy frames, must be published in time order.
// Publish the payload boundary when the first bit arrives rather
// than inserting it retroactively after packet completion.
if (e.bit_index == 0)
mResults->AddMarker(static_cast<U64>(e.start_sample), AnalyzerResults::Start,
mSettings.mInputChannel);
const U64 marker_sample = static_cast<U64>((e.start_sample + e.end_sample) / 2);
mResults->AddMarker(marker_sample, e.bit_value ? AnalyzerResults::One : AnalyzerResults::Zero,
mSettings.mInputChannel);
}
return;
}
// Sync cells have no independent user-facing value at overview scale. A
// fatal sync mismatch is still emitted as IRF_FT_ABORT below.
if (e.frame_type == IRF_FT_SYNC_BIT)
return;
if (e.frame_type == IRF_FT_DATA_BYTE)
{
if (detailed_presentation)
pending_byte_frames.push_back(e);
return;
}
if (!detailed_presentation)
return;
if (e.frame_type == IRF_FT_PREAMBLE)
{
// A timeout can leave a few complete bytes without a packet event.
// Flush them before the next PRE so legacy frames remain monotonic.
flush_pending_bytes();
add_event_frame(e);
return;
}
if (e.frame_type == IRF_FT_OVERFLOW || e.frame_type == IRF_FT_ABORT)
flush_pending_bytes();
add_event_frame(e);
};
// Terminal events are independent of per-bit rendering. Overview passes an
// empty on_bit callback for speed, but must still show an interrupted frame.
IrFoxOnTerminal on_terminal = [&](const IrFoxEmitTerminal& terminal) {
Frame frame;
if (detailed_presentation && !pending_byte_frames.empty())
{
// Preserve every completed byte except the final one. The terminal
// frame replaces that last byte so legacy frames never overlap.
for (size_t i = 0; i + 1 < pending_byte_frames.size(); ++i)
add_event_frame(pending_byte_frames[i]);
frame.mStartingSampleInclusive = static_cast<S64>(pending_byte_frames.back().start_sample);
}
else
{
frame.mStartingSampleInclusive = static_cast<S64>(detailed_presentation ?
terminal.detail_start_sample : terminal.start_sample);
}
frame.mEndingSampleInclusive = static_cast<S64>(terminal.end_sample);
if (frame.mStartingSampleInclusive > frame.mEndingSampleInclusive)
frame.mStartingSampleInclusive = frame.mEndingSampleInclusive;
frame.mType = terminal.reason == IrFoxTerminalReason::Timeout ? IRF_FT_TIMEOUT :
(terminal.cause == IrFoxAbortCause::Overflow ? IRF_FT_OVERFLOW : IRF_FT_ABORT);
frame.mData1 = terminal.declared_size;
frame.mData2 = U64(terminal.received_bits) |
(U64(terminal.err_low) << 16) | (U64(terminal.err_high) << 24) | (U64(terminal.err_other) << 32) |
(U64(terminal.message_type) << 40) | (U64(terminal.cause) << 48) | (U64(terminal.reason) << 56);
frame.mFlags = DISPLAY_AS_ERROR_FLAG;
const U64 fid = mResults->AddFrame(frame);
pending_byte_frames.clear();
std::string short_text;
if (terminal.reason == IrFoxTerminalReason::Timeout)
short_text = "❌ TIMEOUT";
else
{
short_text = "❌ ABORT";
const char* cause = terminal_abort_cause_text(terminal.cause);
if (*cause != '\0')
short_text += std::string(" ") + cause;
}
std::string detail = short_text + " · got=" + std::to_string(terminal.received_bits) + "b";
if (terminal.message_type != 0xFFU)
{
detail += " · ";
detail += irfox::messageTypeText(terminal.message_type);
detail += " len=" + std::to_string(terminal.declared_size) + "B";
}
if (terminal.err_low != 0U || terminal.err_high != 0U || terminal.err_other != 0U)
{
detail += " · err=" + std::to_string(terminal.err_low) + "/" +
std::to_string(terminal.err_high) + "/" + std::to_string(terminal.err_other);
}
auto cached_text = std::make_shared<IrFoxCachedFrameText>();
cached_text->bubble_texts[0] = "";
cached_text->bubble_texts[1] = short_text;
cached_text->bubble_texts[2] = detail;
cached_text->bubble_text_count = 3;
mResults->CacheFrameText(fid, cached_text);
if (detailed_presentation)
mResults->AddMarker(static_cast<U64>(terminal.end_sample), AnalyzerResults::ErrorX,
mSettings.mInputChannel);
note_legacy_frame();
};
IrFoxOnPacket on_pkt = [&](const IrFoxEmitPacket& p) {
const IrFoxPacketDecision decision =
irfox::classifyPacket(p.data_bytes, p.pack_size, p.crc_ok, mSettings.mReceiverAddress);
Frame frame;
if (detailed_presentation)
{
// A Saleae legacy frame cannot overlap another legacy frame. Emit all
// completed bytes except the last one, then use the last byte's span
// for the packet outcome bubble.
for (size_t i = 0; i + 1 < pending_byte_frames.size(); ++i)
add_event_frame(pending_byte_frames[i]);
frame.mStartingSampleInclusive = static_cast<S64>(pending_byte_frames.empty() ?
p.data_start_sample : pending_byte_frames.back().start_sample);
}
else
{
frame.mStartingSampleInclusive = static_cast<S64>(p.start_sample);
}
frame.mEndingSampleInclusive = static_cast<S64>(p.end_sample);
frame.mType = p.crc_ok ? IRF_FT_PACKET_OK : IRF_FT_PACKET_CRC_FAIL;
frame.mFlags = 0;
switch (decision.outcome)
{
case IrFoxPacketOutcome::Accepted:
frame.mType = IRF_FT_PACKET_ACCEPTED;
break;
case IrFoxPacketOutcome::RejectedCrc:
frame.mType = IRF_FT_PACKET_CRC_FAIL;
frame.mFlags |= DISPLAY_AS_ERROR_FLAG;
break;
case IrFoxPacketOutcome::RejectedLength:
frame.mType = IRF_FT_PACKET_BAD_LENGTH;
frame.mFlags |= DISPLAY_AS_ERROR_FLAG;
break;
case IrFoxPacketOutcome::IgnoredAddress:
frame.mType = IRF_FT_PACKET_IGNORED_ADDRESS;
break;
case IrFoxPacketOutcome::RawOnlyUnknownType:
case IrFoxPacketOutcome::RawOnlyTypedLength:
frame.mType = IRF_FT_PACKET_RAW_ONLY;
frame.mFlags |= DISPLAY_AS_WARNING_FLAG;
break;
}
frame.mData1 = p.pack_size;
frame.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);
pending_byte_frames.clear();
const std::string icon = packet_icon(decision, mSettings.mPacketIconMode);
std::string hx;
append_hex(hx, p.data_bytes, p.pack_size);
m_packet_hex_by_frame[fid] = std::move(hx);
std::string status = irfox::packetOutcomeText(decision.outcome);
if (p.pack_size >= irfox::kMsgBytes)
{
status += " ";
status += irfox::messageTypeText(decision.message_type);
}
if (decision.has_destination)
status += " to=" + std::to_string(decision.destination);
auto cached_text = std::make_shared<IrFoxCachedFrameText>();
cached_text->export_hex = hx;
cached_text->bubble_texts[0] = icon;
if (detailed_presentation)
{
char last_byte[3] = "??";
if (p.pack_size > 0)
std::snprintf(last_byte, sizeof last_byte, "%02X", static_cast<unsigned>(p.data_bytes[p.pack_size - 1]));
cached_text->bubble_texts[1] = std::string("0x") + last_byte + " " + icon;
cached_text->bubble_texts[2] = cached_text->bubble_texts[1] + " " + status + " " +
std::to_string(p.pack_size) + "B";
if (!hx.empty())
cached_text->bubble_texts[2] += " · " + hx;
}
else
{
cached_text->bubble_texts[1] = icon + " [" + hx + "] " + icon;
cached_text->bubble_texts[2] = icon + " " + status + " " +
std::to_string(p.pack_size) + "B";
if (!hx.empty())
cached_text->bubble_texts[2] += " · [" + hx + "] " + icon;
}
cached_text->bubble_text_count = 3;
mResults->CacheFrameText(fid, cached_text);
if (detailed_presentation)
{
AnalyzerResults::MarkerType outcome_marker = AnalyzerResults::Square;
switch (decision.outcome)
{
case IrFoxPacketOutcome::Accepted:
outcome_marker = AnalyzerResults::Square;
break;
case IrFoxPacketOutcome::IgnoredAddress:
case IrFoxPacketOutcome::RawOnlyUnknownType:
case IrFoxPacketOutcome::RawOnlyTypedLength:
outcome_marker = AnalyzerResults::X;
break;
case IrFoxPacketOutcome::RejectedCrc:
case IrFoxPacketOutcome::RejectedLength:
outcome_marker = AnalyzerResults::ErrorX;
break;
}
mResults->AddMarker(static_cast<U64>(p.end_sample), outcome_marker, mSettings.mInputChannel);
}
if (detailed_presentation)
{
// Structured output is useful in Detailed. Overview intentionally keeps
// only the single legacy packet frame used by the graph bubble.
FrameV2 fv2;
fv2.AddBoolean("crc_ok", p.crc_ok);
fv2.AddBoolean("raw_accepted", decision.raw_accepted());
fv2.AddBoolean("accepted", decision.outcome == IrFoxPacketOutcome::Accepted);
fv2.AddInteger("outcome", static_cast<S64>(decision.outcome));
fv2.AddInteger("message_type", static_cast<S64>(decision.message_type));
fv2.AddInteger("receiver_address", static_cast<S64>(mSettings.mReceiverAddress));
if (decision.has_destination)
fv2.AddInteger("destination", static_cast<S64>(decision.destination));
fv2.AddInteger("len", static_cast<S64>(p.pack_size));
fv2.AddInteger("err_low", static_cast<S64>(p.err_low));
fv2.AddInteger("err_high", static_cast<S64>(p.err_high));
fv2.AddInteger("err_other", static_cast<S64>(p.err_other));
fv2.AddByteArray("data", p.data_bytes, p.pack_size);
mResults->AddFrameV2(fv2, p.crc_ok ? "packet_ok" : "packet_bad", static_cast<U64>(p.start_sample),
static_cast<U64>(p.end_sample));
const char* type = decision.outcome == IrFoxPacketOutcome::Accepted ? "packet_accepted" :
decision.raw_accepted() ? "packet_raw_only" : "packet_rejected";
mResults->AddFrameV2(fv2, type, static_cast<U64>(p.start_sample), static_cast<U64>(p.end_sample));
}
if (++frames_since_commit >= kCommitBatch)
{
@ -170,6 +447,10 @@ void IrFoxAnalyzer::WorkerThread()
frames_since_commit = 0;
}
};
// In Overview the decoder still performs the same timing, CRC, and receiver
// checks, but does not allocate and dispatch hundreds of visual bit events.
const IrFoxOnBit no_bit_events;
const IrFoxOnBit& bit_events = detailed_presentation ? on_bit : no_bit_events;
auto emit_confirmed_edges = [&]() {
for (;;)
@ -180,7 +461,7 @@ void IrFoxAnalyzer::WorkerThread()
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);
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.erase(pending.begin());
@ -192,7 +473,7 @@ void IrFoxAnalyzer::WorkerThread()
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);
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.erase(pending.begin());
@ -201,7 +482,7 @@ void IrFoxAnalyzer::WorkerThread()
}
if (pending.size() == 1)
{
decoder.processEdge(pending[0].sample, pending[0].rising, fs, on_bit, on_pkt);
decoder.processEdge(pending[0].sample, pending[0].rising, fs, bit_events, on_pkt, on_terminal);
last_dec_edge_sample = pending[0].sample;
last_dec_edge_valid = true;
pending.clear();
@ -230,7 +511,9 @@ void IrFoxAnalyzer::WorkerThread()
}
flush_pending_tail();
decoder.flushEnd(mIr->GetSampleNumber(), fs, on_bit, on_pkt);
decoder.flushEnd(mIr->GetSampleNumber(), fs, bit_events, on_pkt, on_terminal);
if (detailed_presentation)
flush_pending_bytes();
if (frames_since_commit != 0)
mResults->CommitResults();

View File

@ -6,8 +6,6 @@
#include "IrFoxAnalyzerResults.h"
#include "IrFoxSimulationDataGenerator.h"
#include <memory>
#include <string>
#include <unordered_map>
class ANALYZER_EXPORT IrFoxAnalyzer : public Analyzer2
{
@ -25,9 +23,6 @@ public:
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;
@ -36,10 +31,6 @@ protected:
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();

View File

@ -6,6 +6,7 @@
#include "IrFoxDecoder.h"
#include <cstdio>
#include <fstream>
#include <string>
IrFoxAnalyzerResults::IrFoxAnalyzerResults(IrFoxAnalyzer* analyzer, IrFoxAnalyzerSettings* settings)
: AnalyzerResults(),
@ -18,51 +19,89 @@ IrFoxAnalyzerResults::~IrFoxAnalyzerResults()
{
}
void IrFoxAnalyzerResults::ClearCachedFrameText()
{
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
m_frame_text_by_frame.clear();
m_frame_text_by_frame.reserve(1024);
}
void IrFoxAnalyzerResults::CacheFrameText(U64 frame_id, std::shared_ptr<const IrFoxCachedFrameText> text)
{
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
m_frame_text_by_frame[frame_id] = std::move(text);
}
std::shared_ptr<const IrFoxCachedFrameText> IrFoxAnalyzerResults::CachedFrameTextForFrame(U64 frame_id) const
{
std::lock_guard<std::mutex> lock(m_frame_text_mutex);
const auto it = m_frame_text_by_frame.find(frame_id);
return it == m_frame_text_by_frame.end() ? nullptr : it->second;
}
void IrFoxAnalyzerResults::GenerateBubbleText(U64 frame_index, Channel& channel, DisplayBase display_base)
{
(void)display_base;
(void)channel;
ClearResultStrings();
auto add_cached_text = [&]() {
const std::shared_ptr<const IrFoxCachedFrameText> cached = CachedFrameTextForFrame(frame_index);
if (!cached)
return false;
for (size_t i = 0; i < cached->bubble_text_count; ++i)
AddResultString(cached->bubble_texts[i].c_str());
return true;
};
// Every Overview frame is a packet with immutable, precomputed text. Avoid
// even GetFrame() and formatting on Logic's redraw callback in that mode.
if (mSettings->mPresentation == IrFoxPresentation::Overview && add_cached_text())
return;
Frame frame = GetFrame(frame_index);
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:
case IRF_FT_DATA_BYTE:
{
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");
char byte_text[3];
std::snprintf(byte_text, sizeof byte_text, "%02X", static_cast<unsigned>(frame.mData1 & 0xFFu));
AddResultString(byte_text);
AddResultString("0x", byte_text);
break;
}
case IRF_FT_PACKET_OK:
case IRF_FT_PACKET_CRC_FAIL:
case IRF_FT_PREAMBLE:
{
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);
AddResultString("📡");
AddResultString("📡 PRE");
break;
}
case IRF_FT_OVERFLOW:
case IRF_FT_ABORT:
case IRF_FT_TIMEOUT:
{
if (add_cached_text())
break;
AddResultString("");
AddResultString(frame.mType == IRF_FT_TIMEOUT ? "❌ TIMEOUT" :
(frame.mType == IRF_FT_OVERFLOW ? "❌ ABORT OVF" : "❌ ABORT"));
break;
}
case IRF_FT_PACKET_ACCEPTED:
case IRF_FT_PACKET_CRC_FAIL:
case IRF_FT_PACKET_BAD_LENGTH:
case IRF_FT_PACKET_RAW_ONLY:
case IRF_FT_PACKET_IGNORED_ADDRESS:
{
if (!add_cached_text())
AddResultString(frame.mType == IRF_FT_PACKET_ACCEPTED ? "" :
(frame.mType == IRF_FT_PACKET_CRC_FAIL || frame.mType == IRF_FT_PACKET_BAD_LENGTH) ? "" :
frame.mType == IRF_FT_PACKET_IGNORED_ADDRESS ? "📭" : "⚠️");
break;
}
@ -95,24 +134,45 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
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";
case IRF_FT_PACKET_ACCEPTED:
typ = "ACCEPT";
break;
case IRF_FT_PACKET_CRC_FAIL:
typ = "CRC";
typ = "REJECT_CRC";
break;
case IRF_FT_PACKET_BAD_LENGTH:
typ = "REJECT_LEN";
break;
case IRF_FT_PACKET_RAW_ONLY:
typ = "RAW_ONLY";
break;
case IRF_FT_PACKET_IGNORED_ADDRESS:
typ = "IGNORE_ADDR";
break;
case IRF_FT_OVERFLOW:
typ = "OVF";
typ = "ABORT_OVF";
break;
case IRF_FT_ABORT:
switch (static_cast<IrFoxAbortCause>((frame.mData2 >> 48) & 0xFFull))
{
case IrFoxAbortCause::BadSync:
typ = "ABORT_SYNC";
break;
case IrFoxAbortCause::BadLength:
typ = "ABORT_LEN";
break;
case IrFoxAbortCause::Overflow:
typ = "ABORT_OVF";
break;
case IrFoxAbortCause::None:
default:
typ = "ABORT";
break;
}
break;
case IRF_FT_TIMEOUT:
typ = "TIMEOUT";
break;
case IRF_FT_PREAMBLE:
typ = "PRE";
break;
@ -120,14 +180,12 @@ void IrFoxAnalyzerResults::GenerateExportFile(const char* file, DisplayBase disp
break;
}
const char* hx = mAnalyzer->PacketHexForFrame(i);
if (!hx)
hx = "";
const std::shared_ptr<const IrFoxCachedFrameText> cached = CachedFrameTextForFrame(i);
const char* hx = cached ? cached->export_hex.c_str() : "";
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)
if (frame.mType == IRF_FT_OVERFLOW || frame.mType == IRF_FT_ABORT || frame.mType == IRF_FT_TIMEOUT)
{
bit_idx = frame.mData2 & 0xFFFFull;
err_l = static_cast<U32>((frame.mData2 >> 16) & 0xFFull);

View File

@ -2,10 +2,22 @@
#define IRFOX_ANALYZER_RESULTS
#include <AnalyzerResults.h>
#include <array>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
class IrFoxAnalyzer;
class IrFoxAnalyzerSettings;
struct IrFoxCachedFrameText
{
std::array<std::string, 3> bubble_texts{};
size_t bubble_text_count = 0;
std::string export_hex;
};
class IrFoxAnalyzerResults : public AnalyzerResults
{
public:
@ -19,9 +31,15 @@ public:
virtual void GeneratePacketTabularText(U64 packet_id, DisplayBase display_base);
virtual void GenerateTransactionTabularText(U64 transaction_id, DisplayBase display_base);
void ClearCachedFrameText();
void CacheFrameText(U64 frame_id, std::shared_ptr<const IrFoxCachedFrameText> text);
std::shared_ptr<const IrFoxCachedFrameText> CachedFrameTextForFrame(U64 frame_id) const;
protected:
IrFoxAnalyzerSettings* mSettings;
IrFoxAnalyzer* mAnalyzer;
mutable std::mutex m_frame_text_mutex;
std::unordered_map<U64, std::shared_ptr<const IrFoxCachedFrameText>> m_frame_text_by_frame;
};
#endif

View File

@ -3,14 +3,42 @@
IrFoxAnalyzerSettings::IrFoxAnalyzerSettings()
: mInputChannel(UNDEFINED_CHANNEL),
mInputChannelInterface()
mReceiverAddress(0),
mPresentation(IrFoxPresentation::Overview),
mPacketIconMode(IrFoxPacketIconMode::StatusAndType),
mInputChannelInterface(),
mReceiverAddressInterface(),
mPresentationInterface(),
mPacketIconModeInterface()
{
mInputChannelInterface.SetTitleAndTooltip(
"IR",
"Demodulated IR receiver output (e.g. TSOP: idle HIGH, active LOW)");
mInputChannelInterface.SetChannel(mInputChannel);
mReceiverAddressInterface.SetTitleAndTooltip(
"Receiver address",
"IR receiver ID for ACCEPT/IGNORE ADDR. 0 mirrors a receiver configured to accept every address; 65000..65535 are broadcast destinations.");
mReceiverAddressInterface.SetMin(0);
mReceiverAddressInterface.SetMax(65535);
mReceiverAddressInterface.SetInteger(mReceiverAddress);
mPresentationInterface.SetTitleAndTooltip(
"Presentation",
"Overview shows one packet bubble over the full frame. Detailed separates PRE, packet, and hexadecimal bytes. Logic 2 does not expose zoom to analyzers, so this is selected explicitly.");
mPresentationInterface.AddNumber(static_cast<double>(IrFoxPresentation::Overview), "Overview", "One outcome bubble across the full frame; no PRE badge.");
mPresentationInterface.AddNumber(static_cast<double>(IrFoxPresentation::Detailed), "Detailed", "Separate PRE and packet bubbles, plus one hexadecimal bubble per byte.");
mPresentationInterface.SetNumber(static_cast<double>(mPresentation));
mPacketIconModeInterface.SetTitleAndTooltip(
"Packet icon",
"Choose whether packet bubbles show reception status, decoded message type, or both.");
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::Status), "Status ✅", "One status symbol: accepted, other address, invalid, or unknown.");
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::MessageType), "Message type 📦", "One symbol for the decoded firmware message type.");
mPacketIconModeInterface.AddNumber(static_cast<double>(IrFoxPacketIconMode::StatusAndType), "Status + type ✅📦", "Reception status followed by the decoded firmware message type.");
mPacketIconModeInterface.SetNumber(static_cast<double>(mPacketIconMode));
AddInterface(&mInputChannelInterface);
AddInterface(&mReceiverAddressInterface);
AddInterface(&mPresentationInterface);
AddInterface(&mPacketIconModeInterface);
AddExportOption(0, "Export as text/csv file");
AddExportExtension(0, "text", "txt");
@ -27,6 +55,23 @@ IrFoxAnalyzerSettings::~IrFoxAnalyzerSettings()
bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
{
mInputChannel = mInputChannelInterface.GetChannel();
mReceiverAddress = static_cast<uint16_t>(mReceiverAddressInterface.GetInteger());
const int presentation = static_cast<int>(mPresentationInterface.GetNumber());
mPresentation = presentation == static_cast<int>(IrFoxPresentation::Detailed) ?
IrFoxPresentation::Detailed : IrFoxPresentation::Overview;
const int packet_icon_mode = static_cast<int>(mPacketIconModeInterface.GetNumber());
switch (packet_icon_mode)
{
case static_cast<int>(IrFoxPacketIconMode::Status):
mPacketIconMode = IrFoxPacketIconMode::Status;
break;
case static_cast<int>(IrFoxPacketIconMode::MessageType):
mPacketIconMode = IrFoxPacketIconMode::MessageType;
break;
default:
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
break;
}
ClearChannels();
AddChannel(mInputChannel, "IR Fox", true);
@ -37,6 +82,9 @@ bool IrFoxAnalyzerSettings::SetSettingsFromInterfaces()
void IrFoxAnalyzerSettings::UpdateInterfacesFromSettings()
{
mInputChannelInterface.SetChannel(mInputChannel);
mReceiverAddressInterface.SetInteger(mReceiverAddress);
mPresentationInterface.SetNumber(static_cast<double>(mPresentation));
mPacketIconModeInterface.SetNumber(static_cast<double>(mPacketIconMode));
}
void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
@ -45,6 +93,50 @@ void IrFoxAnalyzerSettings::LoadSettings(const char* settings)
text_archive.SetString(settings);
text_archive >> mInputChannel;
S32 receiver_address = 0;
if (text_archive >> receiver_address)
{
if (receiver_address < 0)
receiver_address = 0;
if (receiver_address > 65535)
receiver_address = 65535;
mReceiverAddress = static_cast<uint16_t>(receiver_address);
}
else
{
// Version 0 settings stored only the channel.
mReceiverAddress = 0;
}
// Settings before the presentation switch contain the former "show bit cells"
// value in this position. Bits are now deliberately always shown.
S32 legacy_show_bit_cells = 0;
(void)(text_archive >> legacy_show_bit_cells);
S32 presentation = static_cast<S32>(IrFoxPresentation::Overview);
if (text_archive >> presentation && presentation == static_cast<S32>(IrFoxPresentation::Detailed))
mPresentation = IrFoxPresentation::Detailed;
else
mPresentation = IrFoxPresentation::Overview;
S32 packet_icon_mode = static_cast<S32>(IrFoxPacketIconMode::StatusAndType);
if (text_archive >> packet_icon_mode)
{
switch (packet_icon_mode)
{
case static_cast<S32>(IrFoxPacketIconMode::Status):
mPacketIconMode = IrFoxPacketIconMode::Status;
break;
case static_cast<S32>(IrFoxPacketIconMode::MessageType):
mPacketIconMode = IrFoxPacketIconMode::MessageType;
break;
default:
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
break;
}
}
else
{
// Existing analyzer instances gain the most informative mode by default.
mPacketIconMode = IrFoxPacketIconMode::StatusAndType;
}
ClearChannels();
AddChannel(mInputChannel, "IR Fox", true);
@ -57,6 +149,11 @@ const char* IrFoxAnalyzerSettings::SaveSettings()
SimpleArchive text_archive;
text_archive << mInputChannel;
text_archive << static_cast<S32>(mReceiverAddress);
// Retain the old third field so already-saved configurations remain readable.
text_archive << static_cast<S32>(1);
text_archive << static_cast<S32>(mPresentation);
text_archive << static_cast<S32>(mPacketIconMode);
return SetReturnString(text_archive.GetString());
}

View File

@ -3,6 +3,22 @@
#include <AnalyzerSettings.h>
#include <AnalyzerTypes.h>
#include <cstdint>
/** Logic 2 does not pass the current zoom level to an analyzer, so the user selects the annotation density. */
enum class IrFoxPresentation : uint8_t
{
Overview = 0,
Detailed = 1,
};
/** Which compact symbol is used at the shortest packet-bubble zoom level. */
enum class IrFoxPacketIconMode : uint8_t
{
Status = 0,
MessageType = 1,
StatusAndType = 2,
};
class IrFoxAnalyzerSettings : public AnalyzerSettings
{
@ -16,9 +32,16 @@ public:
virtual const char* SaveSettings();
Channel mInputChannel;
/** Receiver ID used by the same address rule as IR_FOX::checkAddressRuleApply. 0 means catch all. */
uint16_t mReceiverAddress;
IrFoxPresentation mPresentation;
IrFoxPacketIconMode mPacketIconMode;
protected:
AnalyzerSettingInterfaceChannel mInputChannelInterface;
AnalyzerSettingInterfaceInteger mReceiverAddressInterface;
AnalyzerSettingInterfaceNumberList mPresentationInterface;
AnalyzerSettingInterfaceNumberList mPacketIconModeInterface;
};
#endif

View File

@ -52,6 +52,20 @@ bool IrFoxDecoder::crc_check(uint8_t len, uint16_t& crc_out)
return ok;
}
void IrFoxDecoder::preamble_reset_to_idle()
{
preamble_state_ = PreambleState::Idle;
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
preamble_candidate_last_edge_us_ = 0;
preamble_candidate_first_rise_us_ = 0;
preamble_candidate_first_rise_valid_ = false;
preamble_bubble_start_valid_ = false;
is_preamb = false;
is_wrong_pack = false;
is_buffer_overflow = false;
}
void IrFoxDecoder::first_rx()
{
err_low_signal = err_high_signal = err_other = 0;
@ -65,7 +79,7 @@ void IrFoxDecoder::first_rx()
i_sync_bit = 0;
err_sync_bit = 0;
is_wrong_pack = false;
is_preamb = true;
is_preamb = false;
is_recive = false;
is_recive_raw = false;
msg_type_receive = 0;
@ -73,6 +87,66 @@ void IrFoxDecoder::first_rx()
std::memset(data_buffer, 0, sizeof data_buffer);
preamble_bubble_start_valid_ = false;
trim_first_data_bit_cell_ = false;
packet_start_sample_ = 0;
packet_start_valid_ = false;
packet_data_start_sample_ = 0;
packet_data_start_valid_ = false;
byte_start_sample_ = 0;
preamble_reset_to_idle();
}
void IrFoxDecoder::release_preamble_guard(double t_us)
{
const uint32_t long_silence_us = irfox::irTimeoutUs(rise_sync_time_us) * 2U;
// Mirror IR_DecoderRaw::releasePreambleGuard. A negative value is the
// floating-point equivalent of the firmware's wrap-safe unsigned offset.
prev_rise_us = t_us - static_cast<double>(long_silence_us) - 1.0;
}
void IrFoxDecoder::emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
const IrFoxOnTerminal& on_terminal) const
{
if (!on_terminal)
return;
IrFoxEmitTerminal terminal{};
const uint64_t start_sample = packet_start_valid_ ? packet_start_sample_ :
(packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
terminal.start_sample = static_cast<int64_t>(start_sample);
terminal.detail_start_sample = static_cast<int64_t>(
packet_data_start_valid_ ? packet_data_start_sample_ : last_edge_sample);
terminal.end_sample = static_cast<int64_t>(end_sample);
terminal.reason = reason;
terminal.cause = cause;
terminal.message_type = i_data_buffer >= irfox::kBitPerByte ?
static_cast<uint8_t>((data_buffer[0] >> 5U) & 0x07U) : 0xFFU;
terminal.declared_size = static_cast<uint8_t>(pack_size);
terminal.received_bits = i_data_buffer;
terminal.err_low = err_low_signal;
terminal.err_high = err_high_signal;
terminal.err_other = err_other;
on_terminal(terminal);
}
void IrFoxDecoder::abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
const IrFoxOnTerminal& on_terminal)
{
emit_terminal(IrFoxTerminalReason::Abort, cause, end_sample, on_terminal);
is_recive = false;
is_recive_raw = false;
msg_type_receive = 0;
first_rx();
release_preamble_guard(t_us);
}
void IrFoxDecoder::expire_preamble_candidate(double t_us)
{
if (preamble_state_ != PreambleState::Candidate)
return;
const uint32_t timeout_us =
irfox::irTimeoutUs(rise_sync_time_us) * irfox::kPreambleCandidateTimeoutMult;
if ((t_us - preamble_candidate_last_edge_us_) > static_cast<double>(timeout_us))
preamble_reset_to_idle();
}
void IrFoxDecoder::listen_start(double t_us)
@ -86,13 +160,20 @@ void IrFoxDecoder::listen_start(double t_us)
}
}
void IrFoxDecoder::check_timeout(double t_us)
void IrFoxDecoder::check_timeout(double t_us, uint32_t fs, const IrFoxOnTerminal& on_terminal)
{
if (!is_recive)
return;
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
if (t_us - last_edge_time_us > irmax * 2.0)
{
const uint64_t timeout_us = static_cast<uint64_t>(irmax) * 2U;
// The callback fires only after the strict > 2T boundary, but the terminal
// frame owns samples only through 2T. This leaves a following edge free to
// seed the next preamble without overlapping inclusive Saleae frames.
const uint64_t timeout_samples = (timeout_us * static_cast<uint64_t>(fs)) / 1000000ULL;
emit_terminal(IrFoxTerminalReason::Timeout, IrFoxAbortCause::None,
last_edge_sample + timeout_samples, on_terminal);
// Как IR_DecoderRaw::checkTimeout после фикса: полный сброс, иначе залипание FSM.
is_recive = false;
msg_type_receive = 0;
@ -103,29 +184,23 @@ void IrFoxDecoder::check_timeout(double t_us)
}
void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, IrFoxEmitBitMode emit_mode)
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
const IrFoxOnTerminal& on_terminal, IrFoxEmitBitMode emit_mode)
{
if (i_data_buffer > irfox::kDataByteSizeMax * 8u)
if (i_data_buffer >= irfox::kDataByteSizeMax * 8u)
{
if (!is_buffer_overflow && on_bit)
{
IrFoxEmitBit e{};
e.start_sample = static_cast<int64_t>(cell_start_s);
e.end_sample = static_cast<int64_t>(cell_end_s);
e.frame_type = IRF_FT_OVERFLOW;
e.mflags = DISPLAY_AS_ERROR_FLAG;
fill_err_snapshot(e);
std::strncpy(e.bubble_text, "OVF", sizeof e.bubble_text);
e.bubble_text[sizeof e.bubble_text - 1] = '\0';
on_bit(e);
}
is_buffer_overflow = true;
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::Overflow, on_terminal);
return;
}
if (is_buffer_overflow || is_preamb || is_wrong_pack)
{
// Как IR_DecoderRaw::writeToBuffer: полный first_rx() вместо только сброса флагов приёма.
first_rx();
// Firmware treats overflow/invalid frame state as a terminal abort and
// immediately permits a fresh preamble candidate.
const IrFoxAbortCause cause = is_buffer_overflow ? IrFoxAbortCause::Overflow :
(is_wrong_pack ? IrFoxAbortCause::BadSync : IrFoxAbortCause::None);
abort_frame(last_edge_time_us, cell_end_s, cause, on_terminal);
return;
}
@ -140,6 +215,19 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
if (is_data)
{
const bool was_first_data_bit = (i_data_buffer == 0);
const bool was_first_bit_of_byte = (i_data_buffer % irfox::kBitPerByte) == 0U;
if (was_first_data_bit && !packet_start_valid_)
{
packet_start_sample_ = cell_start_s;
packet_start_valid_ = true;
}
if (was_first_data_bit)
{
packet_data_start_sample_ = cell_start_s;
packet_data_start_valid_ = true;
}
if (was_first_bit_of_byte)
byte_start_sample_ = cell_start_s;
data_buffer[i_data_buffer / 8] |= static_cast<uint8_t>(bit ? 1 : 0) << (7 - (i_data_buffer % 8));
i_data_buffer++;
buf_bit_pos++;
@ -156,6 +244,16 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
e.bubble_text[1] = '\0';
on_bit(e);
}
if (on_bit && emit_mode == IrFoxEmitBitMode::WithBubble &&
(i_data_buffer % irfox::kBitPerByte) == 0U)
{
const uint64_t byte_index = (i_data_buffer / irfox::kBitPerByte) - 1U;
IrFoxEmitBit e{static_cast<int64_t>(byte_start_sample_), static_cast<int64_t>(cell_end_s), IRF_FT_DATA_BYTE,
data_buffer[byte_index], byte_index, fl, pack_trace_invert_fix, 0, 0, 0};
fill_err_snapshot(e);
std::snprintf(e.bubble_text, sizeof e.bubble_text, "%02X", static_cast<unsigned>(data_buffer[byte_index]));
on_bit(e);
}
if (was_first_data_bit && trim_first_data_bit_cell_)
trim_first_data_bit_cell_ = false;
}
@ -186,14 +284,10 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
const bool fatal_sync = (err_sync_bit >= irfox::kSyncBits);
if (fatal_sync)
is_wrong_pack = true;
if (on_bit && fatal_sync)
if (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);
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadSync, on_terminal);
return;
}
}
}
@ -216,12 +310,23 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
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);
// The receiver rejects a length that cannot contain its two CRC bytes.
// Emit a terminal abort so the capture explains why no packet follows.
if (pack_size < irfox::kMsgBytes + irfox::kCrcBytes)
{
is_wrong_pack = true;
abort_frame(last_edge_time_us, cell_end_s, IrFoxAbortCause::BadLength, on_terminal);
return;
}
}
if (pack_size && (i_data_buffer == 8))
msg_type_receive = static_cast<uint8_t>((data_buffer[0] >> 5) | 0xF8u);
if (pack_size && (i_data_buffer == pack_size * irfox::kBitPerByte))
if (pack_size >= irfox::kMsgBytes + irfox::kCrcBytes &&
(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);
@ -232,7 +337,8 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
is_available = crc_ok;
IrFoxEmitPacket pkt{};
pkt.start_sample = static_cast<int64_t>(cell_start_s);
pkt.start_sample = static_cast<int64_t>(packet_start_valid_ ? packet_start_sample_ : cell_start_s);
pkt.data_start_sample = static_cast<int64_t>(packet_data_start_valid_ ? packet_data_start_sample_ : cell_start_s);
pkt.end_sample = static_cast<int64_t>(cell_end_s);
pkt.crc_ok = crc_ok;
pkt.pack_size = static_cast<uint8_t>(pack_size);
@ -248,37 +354,198 @@ void IrFoxDecoder::write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_
}
void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const IrFoxOnBit& on_bit,
const IrFoxOnPacket& on_pkt)
const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal)
{
const double t_us = sample_to_us(sample, fs);
// Firmware advances terminal timers in this order while no queued edge is
// pending. For an offline capture, do the equivalent immediately before the
// next timestamped edge is consumed.
check_timeout(t_us, fs, on_terminal);
listen_start(t_us);
expire_preamble_candidate(t_us);
// A timeout/abort may have restored the nominal adaptive bit period.
const uint32_t irmax = irfox::irTimeoutUs(rise_sync_time_us);
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 битовых периодов (ИК-метка). */
/** Firmware starts a preamble candidate only on its first rising edge after silence. */
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;
}
(void)is_rising;
return edge_s;
};
// Mirror IR_DecoderRaw::preambleProcessEdge. A frame may start only after
// a long silence and two mutually consistent rise-to-rise periods.
auto start_preamble_candidate = [&]() {
preamble_state_ = PreambleState::Candidate;
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
preamble_candidate_last_edge_us_ = t_us;
preamble_candidate_first_rise_us_ = t_us;
preamble_candidate_first_rise_valid_ = rising;
is_preamb = true;
is_recive = false;
is_recive_raw = false;
is_wrong_pack = false;
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising);
preamble_bubble_start_valid_ = true;
};
const uint32_t long_silence_us = irmax * 2U;
if (preamble_state_ == PreambleState::Locked && !is_recive_raw)
{
preamble_state_ = PreambleState::Idle;
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
}
if (preamble_state_ == PreambleState::Idle)
{
const bool enough_silence = prev_rise_us == 0.0 ? t_us > static_cast<double>(long_silence_us) :
(t_us - prev_rise_us) > static_cast<double>(long_silence_us);
if (!is_recive_raw && rising && enough_silence)
{
start_preamble_candidate();
// The first rising edge only opens Candidate; it must not also be
// compared with itself as a zero-length preamble period.
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
else
{
// IR_DecoderRaw ignores idle edges until a valid preamble candidate starts.
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
}
if (preamble_state_ == PreambleState::Candidate)
{
preamble_candidate_last_edge_us_ = t_us;
if (!rising)
{
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
if (!preamble_candidate_first_rise_valid_)
{
preamble_candidate_first_rise_valid_ = true;
preamble_candidate_first_rise_us_ = t_us;
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
const uint32_t period_us = static_cast<uint32_t>(t_us - preamble_candidate_first_rise_us_);
preamble_candidate_first_rise_us_ = t_us;
if (!irfox::preambleRisePeriodCoarseOk(period_us))
{
preamble_good_periods_ = 0;
preamble_mean_period_us_ = 0;
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
if (preamble_good_periods_ == 0)
{
preamble_good_periods_ = 1;
preamble_mean_period_us_ = period_us;
}
else
{
const uint32_t delta = period_us > preamble_mean_period_us_ ? period_us - preamble_mean_period_us_ :
preamble_mean_period_us_ - period_us;
if (delta <= irfox::preambleJitterTolUs(preamble_mean_period_us_))
{
if (preamble_good_periods_ < 255U)
++preamble_good_periods_;
preamble_mean_period_us_ = (preamble_mean_period_us_ * 3U + period_us) / 4U;
}
else
{
preamble_good_periods_ = 1;
preamble_mean_period_us_ = period_us;
}
}
if (preamble_good_periods_ < irfox::kPreambleLockRisePeriods)
{
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
// The firmware clears all frame state when the candidate becomes locked.
err_low_signal = err_high_signal = err_other = 0;
pack_size = 0;
is_buffer_overflow = false;
is_available = false;
buf_bit_pos = 0;
is_data = true;
i_data_buffer = 0;
next_control_bit = irfox::kBitPerByte;
i_sync_bit = 0;
err_sync_bit = 0;
is_wrong_pack = false;
msg_type_receive = 0;
std::memset(data_buffer, 0, sizeof data_buffer);
packet_start_sample_ = preamble_bubble_start_sample_;
packet_start_valid_ = preamble_bubble_start_valid_;
packet_data_start_sample_ = 0;
packet_data_start_valid_ = false;
byte_start_sample_ = 0;
preamble_state_ = PreambleState::Locked;
is_preamb = false;
is_recive = true;
is_recive_raw = true;
rise_period_us = preamble_mean_period_us_;
prev_rise_us = t_us + static_cast<double>(preamble_mean_period_us_) / 2.0;
prev_rise_sample = sample + static_cast<uint64_t>(std::llround(
static_cast<double>(preamble_mean_period_us_) * 0.5 * static_cast<double>(fs) / 1e6));
trim_first_data_bit_cell_ = true;
// The analyzer chooses whether this is visible (Detailed) or folded into
// the full packet frame (Overview).
if (on_bit && preamble_bubble_start_valid_)
{
IrFoxEmitBit pe{};
pe.start_sample = static_cast<int64_t>(preamble_bubble_start_sample_);
pe.end_sample = static_cast<int64_t>(sample > 0 ? sample - 1 : sample);
pe.frame_type = IRF_FT_PREAMBLE;
fill_err_snapshot(pe);
on_bit(pe);
}
preamble_bubble_start_valid_ = false;
last_edge_time_us = t_us;
last_edge_sample = sample;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
// As in processDecodedFront, the edge becomes the timing reference only
// after the preamble state machine has allowed it through.
last_edge_time_us = t_us;
last_edge_sample = sample;
if (rising)
{
const double delta_rp = t_us - prev_rise_us;
@ -357,78 +624,6 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
}
}
// Как IR_DecoderRaw::tick: после длинной паузы старт сырого приёма (без отдельного firstRX — флаги ниже).
if (t_us > prev_rise_us && (t_us - prev_rise_us) > irmax * 2.0 && !is_recive_raw)
{
preamb_front_counter = static_cast<int8_t>(irfox::kPreambFronts - 1);
is_preamb = true;
is_recive = true;
is_recive_raw = true;
is_wrong_pack = false;
if (!preamble_bubble_start_valid_)
{
preamble_bubble_start_sample_ = new_bubble_preamble_start(sample, rising);
preamble_bubble_start_valid_ = true;
}
}
if (preamb_front_counter)
{
if (rising && rise_period_us < irmax)
{
if (rise_period_us < rise_min_us / 2U)
{
preamb_front_counter += 2;
err_other++;
}
}
preamb_front_counter--;
}
else
{
if (is_preamb)
{
is_preamb = false;
// IR_DecoderRaw: prevRise += risePeriod / 2 — фаза как в прошивке.
// Бабл PRE: до текущего фронта (sample1), чтобы охватить все kPreambPulse периодов (3 импульса),
// а не только до предыдущего подъёма (~2 периода).
const uint64_t preamble_bubble_end_sample = sample > 0 ? sample - 1 : sample;
prev_rise_us += rise_period_us / 2.0;
{
const double half_us = 0.5 * static_cast<double>(rise_period_us);
const uint64_t half_s = static_cast<uint64_t>(std::llround(half_us * double(fs) / 1e6));
prev_rise_sample += half_s;
}
trim_first_data_bit_cell_ = true;
if (on_bit && preamble_bubble_start_valid_)
{
int64_t pe_start = static_cast<int64_t>(preamble_bubble_start_sample_);
int64_t pe_end = static_cast<int64_t>(preamble_bubble_end_sample);
if (preamble_bubble_end_sample == 0 || pe_end < pe_start)
pe_end = static_cast<int64_t>(sample > 0 ? sample - 1 : sample);
IrFoxEmitBit pe{};
pe.start_sample = pe_start;
pe.end_sample = pe_end;
pe.frame_type = IRF_FT_PREAMBLE;
fill_err_snapshot(pe);
std::strncpy(pe.bubble_text, "PRE", sizeof pe.bubble_text);
pe.bubble_text[sizeof pe.bubble_text - 1] = '\0';
on_bit(pe);
}
preamble_bubble_start_valid_ = false;
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
}
if (is_preamb)
{
last_processed_edge_us = t_us;
have_last_processed = true;
return;
}
if (rise_period_us > irmax || is_buffer_overflow || rise_period_us < rise_min_us || is_wrong_pack)
{
last_processed_edge_us = t_us;
@ -455,9 +650,11 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
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);
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
IrFoxEmitBitMode::WithBubble);
else
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, IrFoxEmitBitMode::WithBubble);
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
IrFoxEmitBitMode::WithBubble);
}
else
{
@ -547,13 +744,15 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
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);
write_to_buffer(true, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
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);
write_to_buffer(false, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
IrFoxEmitBitMode::Quiet);
append_merge(row_is_data, false);
}
}
@ -564,13 +763,15 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
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);
write_to_buffer(false, true, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
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);
write_to_buffer(true, false, cell_start_s, cell_end_s, on_bit, on_pkt, on_terminal,
IrFoxEmitBitMode::Quiet);
append_merge(row_is_data, true);
}
}
@ -583,11 +784,13 @@ void IrFoxDecoder::processEdge(uint64_t sample, bool rising, uint32_t fs, const
have_last_processed = true;
}
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt)
void IrFoxDecoder::flushEnd(uint64_t last_sample, uint32_t fs, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
const IrFoxOnTerminal& on_terminal)
{
const double t_us = sample_to_us(last_sample, fs);
check_timeout(t_us, fs, on_terminal);
listen_start(t_us);
check_timeout(t_us);
expire_preamble_candidate(t_us);
(void)on_bit;
(void)on_pkt;
}

View File

@ -8,11 +8,17 @@ enum IrFoxFrameType : uint8_t
{
IRF_FT_DATA_BIT = 1,
IRF_FT_SYNC_BIT = 2,
IRF_FT_PACKET_OK = 3,
IRF_FT_PACKET_ACCEPTED = 3,
IRF_FT_PACKET_CRC_FAIL = 4,
IRF_FT_OVERFLOW = 5,
IRF_FT_ABORT = 6,
IRF_FT_PREAMBLE = 7,
IRF_FT_PACKET_BAD_LENGTH = 8,
IRF_FT_PACKET_RAW_ONLY = 9,
IRF_FT_PACKET_IGNORED_ADDRESS = 10,
IRF_FT_DATA_BYTE = 11,
IRF_FT_TIMEOUT = 12,
IRF_FT_PACKET_OK = IRF_FT_PACKET_ACCEPTED,
};
/** WithBubble — вызвать on_bit; Quiet — только обновить состояние (для пакета битов с одного фронта). */
@ -41,6 +47,8 @@ struct IrFoxEmitBit
struct IrFoxEmitPacket
{
int64_t start_sample;
/** First data-bit cell: the visible boundary between the preamble and payload. */
int64_t data_start_sample;
int64_t end_sample;
bool crc_ok;
uint8_t pack_size;
@ -50,16 +58,47 @@ struct IrFoxEmitPacket
uint8_t data_bytes[irfox::kDataByteSizeMax];
};
enum class IrFoxTerminalReason : uint8_t
{
Abort,
Timeout,
};
enum class IrFoxAbortCause : uint8_t
{
None,
BadSync,
BadLength,
Overflow,
};
struct IrFoxEmitTerminal
{
int64_t start_sample;
int64_t detail_start_sample;
int64_t end_sample;
IrFoxTerminalReason reason;
IrFoxAbortCause cause;
uint8_t message_type;
uint8_t declared_size;
uint16_t received_bits;
uint8_t err_low;
uint8_t err_high;
uint8_t err_other;
};
using IrFoxOnBit = std::function<void(const IrFoxEmitBit&)>;
using IrFoxOnPacket = std::function<void(const IrFoxEmitPacket&)>;
using IrFoxOnTerminal = std::function<void(const IrFoxEmitTerminal&)>;
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);
const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal);
void flushEnd(uint64_t last_sample, uint32_t sample_rate_hz, const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
const IrFoxOnTerminal& on_terminal);
private:
static uint16_t ceil_div_u16(uint16_t val, uint16_t divider);
@ -67,10 +106,17 @@ private:
bool crc_check(uint8_t len, uint16_t& crc_out);
void first_rx();
void preamble_reset_to_idle();
void release_preamble_guard(double t_us);
void emit_terminal(IrFoxTerminalReason reason, IrFoxAbortCause cause, uint64_t end_sample,
const IrFoxOnTerminal& on_terminal) const;
void abort_frame(double t_us, uint64_t end_sample, IrFoxAbortCause cause,
const IrFoxOnTerminal& on_terminal);
void expire_preamble_candidate(double t_us);
void listen_start(double t_us);
void check_timeout(double t_us);
void check_timeout(double t_us, uint32_t sample_rate_hz, const IrFoxOnTerminal& on_terminal);
void write_to_buffer(bool bit, bool pack_trace_invert_fix, uint64_t cell_start_s, uint64_t cell_end_s,
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt,
const IrFoxOnBit& on_bit, const IrFoxOnPacket& on_pkt, const IrFoxOnTerminal& on_terminal,
IrFoxEmitBitMode emit_mode = IrFoxEmitBitMode::WithBubble);
double sample_to_us(uint64_t sample, uint32_t fs) const { return double(sample) * 1e6 / double(fs); }
@ -101,6 +147,11 @@ private:
uint64_t preamble_bubble_start_sample_ = 0;
bool preamble_bubble_start_valid_ = false;
bool trim_first_data_bit_cell_ = false;
uint64_t packet_start_sample_ = 0;
bool packet_start_valid_ = false;
uint64_t packet_data_start_sample_ = 0;
bool packet_data_start_valid_ = false;
uint64_t byte_start_sample_ = 0;
double last_edge_time_us = 0;
uint64_t last_edge_sample = 0;
@ -123,7 +174,18 @@ private:
int8_t all_count = 0;
uint16_t wrong_counter = 0;
int8_t preamb_front_counter = 0;
enum class PreambleState : uint8_t
{
Idle,
Candidate,
Locked,
};
PreambleState preamble_state_ = PreambleState::Idle;
uint8_t preamble_good_periods_ = 0;
uint32_t preamble_mean_period_us_ = 0;
double preamble_candidate_last_edge_us_ = 0;
double preamble_candidate_first_rise_us_ = 0;
bool preamble_candidate_first_rise_valid_ = false;
int16_t buf_bit_pos = 0;
bool is_data = true;
uint16_t i_data_buffer = 0;

View File

@ -0,0 +1,172 @@
#pragma once
#include "IrFoxProtocolConstants.h"
#include <cstdint>
/**
* The decision made after IR_DecoderRaw::availableRaw() in IR_Decoder::_tick().
* It intentionally models only what a capture can prove: decoding, CRC, typed
* layout, and the receiver address. It does not claim that application code
* subsequently acted on the delivered message.
*/
enum class IrFoxPacketOutcome : uint8_t
{
Accepted,
RawOnlyUnknownType,
RawOnlyTypedLength,
IgnoredAddress,
RejectedCrc,
RejectedLength,
};
struct IrFoxPacketDecision
{
IrFoxPacketOutcome outcome = IrFoxPacketOutcome::RejectedLength;
uint8_t message_type = 0;
uint8_t minimum_size = 0;
uint16_t destination = 0;
bool has_destination = false;
bool raw_accepted() const
{
return outcome == IrFoxPacketOutcome::Accepted || outcome == IrFoxPacketOutcome::RawOnlyUnknownType ||
outcome == IrFoxPacketOutcome::RawOnlyTypedLength || outcome == IrFoxPacketOutcome::IgnoredAddress;
}
};
namespace irfox {
constexpr uint8_t kMsgBack = 0U;
constexpr uint8_t kMsgAccept = 1U;
constexpr uint8_t kMsgRequest = 2U;
constexpr uint8_t kMsgBackTo = 4U;
constexpr uint8_t kMsgDataNoAccept = 6U;
constexpr uint8_t kMsgDataAccept = 7U;
constexpr uint16_t kBroadcastAddress = 65000U;
inline uint8_t messageType(uint8_t header)
{
return static_cast<uint8_t>((header >> 5U) & 0x07U);
}
inline uint8_t minimumPacketSize(uint8_t message_type)
{
switch (message_type)
{
case kMsgDataAccept:
case kMsgDataNoAccept:
case kMsgBackTo:
case kMsgRequest:
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + kAddrBytes + kCrcBytes);
case kMsgBack:
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + kCrcBytes);
case kMsgAccept:
return static_cast<uint8_t>(kMsgBytes + kAddrBytes + 1U + kCrcBytes);
default:
return 0;
}
}
inline bool addressAcceptedByReceiver(uint16_t destination, uint16_t receiver_address)
{
return receiver_address == 0U || destination == receiver_address || destination >= kBroadcastAddress;
}
inline IrFoxPacketDecision classifyPacket(const uint8_t* data, uint8_t observed_size, bool crc_ok,
uint16_t receiver_address)
{
IrFoxPacketDecision result;
if (data == nullptr)
{
result.outcome = IrFoxPacketOutcome::RejectedLength;
return result;
}
result.message_type = messageType(data[0]);
if (observed_size < kMsgBytes + kCrcBytes)
{
result.outcome = IrFoxPacketOutcome::RejectedLength;
return result;
}
if ((data[0] & 0x1FU) != observed_size)
{
result.outcome = IrFoxPacketOutcome::RejectedLength;
return result;
}
if (!crc_ok)
{
result.outcome = IrFoxPacketOutcome::RejectedCrc;
return result;
}
result.minimum_size = minimumPacketSize(result.message_type);
if (result.minimum_size == 0U)
{
result.outcome = IrFoxPacketOutcome::RawOnlyUnknownType;
return result;
}
if (observed_size < result.minimum_size)
{
result.outcome = IrFoxPacketOutcome::RawOnlyTypedLength;
return result;
}
const bool addressed = result.message_type == kMsgDataAccept || result.message_type == kMsgDataNoAccept ||
result.message_type == kMsgBackTo || result.message_type == kMsgRequest;
if (addressed)
{
result.has_destination = true;
result.destination = static_cast<uint16_t>((static_cast<uint16_t>(data[3]) << 8U) | data[4]);
if (!addressAcceptedByReceiver(result.destination, receiver_address))
{
result.outcome = IrFoxPacketOutcome::IgnoredAddress;
return result;
}
}
result.outcome = IrFoxPacketOutcome::Accepted;
return result;
}
inline const char* packetOutcomeText(IrFoxPacketOutcome outcome)
{
switch (outcome)
{
case IrFoxPacketOutcome::Accepted:
return "ACCEPT";
case IrFoxPacketOutcome::RawOnlyUnknownType:
return "RAW TYPE";
case IrFoxPacketOutcome::RawOnlyTypedLength:
return "RAW SIZE";
case IrFoxPacketOutcome::IgnoredAddress:
return "IGNORE ADDR";
case IrFoxPacketOutcome::RejectedCrc:
return "REJECT CRC";
case IrFoxPacketOutcome::RejectedLength:
return "REJECT LEN";
}
return "REJECT";
}
inline const char* messageTypeText(uint8_t message_type)
{
switch (message_type)
{
case kMsgBack:
return "BACK";
case kMsgAccept:
return "ACCEPT";
case kMsgRequest:
return "REQUEST";
case kMsgBackTo:
return "BACK_TO";
case kMsgDataNoAccept:
return "DATA";
case kMsgDataAccept:
return "DATA_ACK";
default:
return "UNKNOWN";
}
}
} // namespace irfox

View File

@ -11,8 +11,12 @@ 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;
/**
* Must match IR_INPUT_MIN_PULSE_US in the firmware configuration. The current
* receiver configuration keeps this filter disabled, so a capture must not
* silently lose short edges that the receiver would see.
*/
constexpr uint32_t kMinFilteredPulseUs = 0U;
constexpr uint8_t kBitPerByte = 8U;
constexpr uint8_t kMsgBytes = 1;
@ -27,6 +31,12 @@ constexpr uint8_t kDataByteSizeMax =
constexpr uint8_t kPreambPulse = 3;
constexpr uint8_t kPreambFronts = kPreambPulse * 2U;
constexpr uint8_t kPreambleLockRisePeriods = 2U;
constexpr uint8_t kPreambleJitterPct = 18U;
constexpr uint32_t kPreambleJitterUsMin = 80U;
constexpr uint32_t kPreamblePeriodMinFactorPct = 220U;
constexpr uint32_t kPreamblePeriodMaxFactorPct = 340U;
constexpr uint32_t kPreambleCandidateTimeoutMult = 1U;
/** Отброс ложного подъёма после микро-LOW в паузе; зеркало IR_config.h (прошивка). */
#ifndef IRFOX_SHORT_LOW_GLITCH_REJECT
@ -53,6 +63,19 @@ inline bool aroundRisePeriod(uint32_t periodUs, uint32_t riseSyncTimeUs)
return lo < periodUs && periodUs < hi;
}
inline uint32_t preambleJitterTolUs(uint32_t baselineUs)
{
const uint32_t pct = (baselineUs * kPreambleJitterPct) / 100U;
return pct > kPreambleJitterUsMin ? pct : kPreambleJitterUsMin;
}
inline bool preambleRisePeriodCoarseOk(uint32_t periodUs)
{
const uint32_t min_period = (kBitTimeUs * kPreamblePeriodMinFactorPct) / 100U;
const uint32_t max_period = (kBitTimeUs * kPreamblePeriodMaxFactorPct) / 100U;
return periodUs >= min_period && periodUs <= max_period;
}
inline void irfoxGlitchPhaseNudgeUs(double edge_us, uint32_t rise_sync_us, double& prev_rise_us)
{
#if IRFOX_GLITCH_REJECT_PHASE_NUDGE

View File

@ -0,0 +1,282 @@
#include "IrFoxDecoder.h"
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <vector>
#define CHECK(expression) \
do \
{ \
if (!(expression)) \
{ \
std::fprintf(stderr, "CHECK failed: %s (%s:%d)\n", #expression, __FILE__, __LINE__); \
std::exit(EXIT_FAILURE); \
} \
} while (false)
namespace {
uint8_t crc8(const uint8_t* data, uint8_t end, uint8_t poly)
{
uint8_t crc = 0xFF;
for (uint8_t i = 0; i < end; ++i)
{
crc ^= data[i];
for (uint8_t bit = 0; bit < 8; ++bit)
crc = (crc & 0x80U) ? static_cast<uint8_t>((crc << 1U) ^ poly) : static_cast<uint8_t>(crc << 1U);
}
return crc;
}
struct DecoderHarness
{
IrFoxDecoder decoder;
std::vector<IrFoxEmitPacket> packets;
std::vector<IrFoxEmitBit> events;
std::vector<IrFoxEmitTerminal> terminals;
uint64_t phase = 0;
bool collect_bit_events = true;
static constexpr uint32_t kFs = 1000000U;
explicit DecoderHarness(bool collect_bits = true) : collect_bit_events(collect_bits)
{
decoder.reset();
}
void edge(uint64_t sample, bool rising)
{
IrFoxOnBit on_bit;
if (collect_bit_events)
on_bit = [this](const IrFoxEmitBit& event) { events.push_back(event); };
decoder.processEdge(sample, rising, kFs, on_bit,
[this](const IrFoxEmitPacket& packet) { packets.push_back(packet); },
[this](const IrFoxEmitTerminal& terminal) { terminals.push_back(terminal); });
}
void lockPreamble()
{
lockPreambleAt(40000U);
}
void lockPreambleAt(uint64_t first_rise)
{
constexpr uint64_t period = irfox::kBitTimeUs * 3U;
edge(first_rise, true);
edge(first_rise + 700U, false);
edge(first_rise + period, true);
edge(first_rise + period + 700U, false);
edge(first_rise + period * 2U, true);
phase = first_rise + period * 2U + period / 2U;
}
void emitCell(bool bit)
{
// The decoder calls a cell a one when the inactive (HIGH) interval is
// longer than the active (LOW) interval. The waveform is TSOP output.
const uint64_t high_us = bit ? 262U : 700U;
edge(phase + high_us, false);
phase += irfox::kBitTimeUs;
edge(phase, true);
}
void emitByte(uint8_t value, bool emit_sync)
{
for (uint8_t i = 0; i < 8; ++i)
emitCell((value & static_cast<uint8_t>(0x80U >> i)) != 0U);
if (emit_sync)
{
const bool sync = (value & 1U) == 0U;
for (uint8_t i = 0; i < irfox::kSyncBits; ++i)
emitCell(sync);
}
}
void emitPacket(const std::vector<uint8_t>& packet)
{
for (size_t i = 0; i < packet.size(); ++i)
emitByte(packet[i], i + 1U != packet.size());
}
void flushAt(uint64_t sample)
{
IrFoxOnBit on_bit;
if (collect_bit_events)
on_bit = [this](const IrFoxEmitBit& event) { events.push_back(event); };
decoder.flushEnd(sample, kFs, on_bit,
[this](const IrFoxEmitPacket& packet) { packets.push_back(packet); },
[this](const IrFoxEmitTerminal& terminal) { terminals.push_back(terminal); });
}
};
std::vector<uint8_t> makeValidPacket()
{
std::vector<uint8_t> packet{0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
packet[5] = crc8(packet.data(), 5, irfox::kPoly1);
packet[6] = crc8(packet.data(), 6, irfox::kPoly2);
return packet;
}
void assertBadLengthAbortsAndRecovers(uint8_t declared_size)
{
DecoderHarness harness;
harness.lockPreamble();
const uint8_t header = static_cast<uint8_t>(0xE0U | declared_size);
harness.emitByte(header, false);
CHECK(harness.packets.empty());
CHECK(harness.terminals.size() == 1U);
CHECK(harness.terminals[0].reason == IrFoxTerminalReason::Abort);
CHECK(harness.terminals[0].cause == IrFoxAbortCause::BadLength);
CHECK(harness.terminals[0].message_type == 7U);
CHECK(harness.terminals[0].declared_size == declared_size);
CHECK(harness.terminals[0].received_bits == irfox::kBitPerByte);
// Firmware abortFrame() releases the 30.288 ms preamble guard. A receiver
// that merely sets is_wrong_pack will miss this complete nearby frame.
const uint64_t next_preamble = harness.phase + 5000U;
harness.lockPreambleAt(next_preamble);
harness.emitPacket(makeValidPacket());
CHECK(harness.packets.size() == 1U);
CHECK(harness.packets[0].crc_ok);
CHECK(harness.packets[0].start_sample == static_cast<int64_t>(next_preamble));
CHECK(harness.terminals.size() == 1U);
}
} // namespace
int main()
{
const std::vector<uint8_t> packet = makeValidPacket();
DecoderHarness valid;
valid.lockPreamble();
valid.emitPacket(packet);
CHECK(valid.packets.size() == 1U);
CHECK(valid.packets[0].crc_ok);
CHECK(valid.packets[0].pack_size == packet.size());
CHECK(valid.packets[0].start_sample == 40000);
CHECK(valid.packets[0].start_sample < valid.packets[0].end_sample);
bool saw_preamble = false;
std::vector<uint8_t> decoded_bytes;
for (const IrFoxEmitBit& event : valid.events)
{
if (event.frame_type == IRF_FT_PREAMBLE)
{
saw_preamble = true;
CHECK(event.start_sample == 40000);
}
if (event.frame_type == IRF_FT_DATA_BYTE)
decoded_bytes.push_back(static_cast<uint8_t>(event.bit_value));
}
CHECK(saw_preamble);
CHECK(decoded_bytes.size() == packet.size());
for (size_t i = 0; i < packet.size(); ++i)
CHECK(decoded_bytes[i] == packet[i]);
for (uint8_t declared_size = 0; declared_size < irfox::kMsgBytes + irfox::kCrcBytes; ++declared_size)
assertBadLengthAbortsAndRecovers(declared_size);
// Overview supplies no per-bit callback. Terminal reporting must not depend
// on Detailed-mode bit/event generation.
DecoderHarness bad_sync(false);
bad_sync.lockPreamble();
bad_sync.emitByte(0xE7, false);
// Header 0xE7 ends in one, while the first sync bit must be inverted.
bad_sync.emitCell(true);
CHECK(bad_sync.terminals.empty());
bad_sync.emitCell(true);
CHECK(bad_sync.terminals.empty());
bad_sync.emitCell(true);
CHECK(bad_sync.events.empty());
CHECK(bad_sync.terminals.size() == 1U);
CHECK(bad_sync.terminals[0].reason == IrFoxTerminalReason::Abort);
CHECK(bad_sync.terminals[0].cause == IrFoxAbortCause::BadSync);
CHECK(bad_sync.terminals[0].message_type == 7U);
CHECK(bad_sync.terminals[0].declared_size == 7U);
CHECK(bad_sync.terminals[0].received_bits == irfox::kBitPerByte);
const uint64_t after_sync_abort = bad_sync.phase + 5000U;
bad_sync.lockPreambleAt(after_sync_abort);
bad_sync.emitPacket(packet);
CHECK(bad_sync.packets.size() == 1U);
CHECK(bad_sync.packets[0].crc_ok);
CHECK(bad_sync.packets[0].start_sample == static_cast<int64_t>(after_sync_abort));
CHECK(bad_sync.terminals.size() == 1U);
DecoderHarness stale_candidate;
constexpr uint64_t stale_rise = 40000U;
const uint64_t candidate_gap =
irfox::irTimeoutUs(irfox::kBitTimeUs) + 1U; // New 1x timeout, still below the old 3x timeout.
const uint64_t fresh_preamble = stale_rise + candidate_gap;
stale_candidate.edge(stale_rise, true);
stale_candidate.lockPreambleAt(fresh_preamble);
stale_candidate.emitPacket(packet);
CHECK(stale_candidate.packets.size() == 1U);
CHECK(stale_candidate.packets[0].crc_ok);
CHECK(stale_candidate.packets[0].start_sample == static_cast<int64_t>(fresh_preamble));
CHECK(stale_candidate.terminals.empty());
// If reception times out after PRE lock but before the first data bit, the
// Detailed terminal span must begin immediately after the separate PRE frame.
DecoderHarness pre_only;
pre_only.lockPreamble();
const IrFoxEmitBit* pre_event = nullptr;
for (const IrFoxEmitBit& event : pre_only.events)
{
if (event.frame_type == IRF_FT_PREAMBLE)
pre_event = &event;
}
CHECK(pre_event != nullptr);
constexpr uint64_t preamble_period = irfox::kBitTimeUs * 3U;
const uint64_t pre_lock_edge = 40000U + preamble_period * 2U;
const uint64_t abort_silence = 2U * irfox::irTimeoutUs(irfox::kBitTimeUs);
pre_only.flushAt(pre_lock_edge + abort_silence + 1U);
CHECK(pre_only.terminals.size() == 1U);
CHECK(pre_only.terminals[0].reason == IrFoxTerminalReason::Timeout);
CHECK(pre_only.terminals[0].received_bits == 0U);
CHECK(pre_only.terminals[0].detail_start_sample == pre_event->end_sample + 1);
CHECK(pre_only.terminals[0].end_sample == static_cast<int64_t>(pre_lock_edge + abort_silence));
DecoderHarness truncated;
truncated.lockPreamble();
truncated.emitByte(0xE7, true);
CHECK(truncated.packets.empty());
truncated.flushAt(truncated.phase + abort_silence);
CHECK(truncated.terminals.empty());
truncated.flushAt(truncated.phase + abort_silence + 1U);
CHECK(truncated.packets.empty());
CHECK(truncated.terminals.size() == 1U);
CHECK(truncated.terminals[0].reason == IrFoxTerminalReason::Timeout);
CHECK(truncated.terminals[0].cause == IrFoxAbortCause::None);
CHECK(truncated.terminals[0].message_type == 7U);
CHECK(truncated.terminals[0].declared_size == 7U);
CHECK(truncated.terminals[0].received_bits == irfox::kBitPerByte);
CHECK(truncated.terminals[0].end_sample == static_cast<int64_t>(truncated.phase + abort_silence));
truncated.flushAt(truncated.phase + abort_silence + 100U);
CHECK(truncated.terminals.size() == 1U);
const uint64_t after_timeout = truncated.phase + abort_silence + 5000U;
truncated.lockPreambleAt(after_timeout);
truncated.emitPacket(packet);
CHECK(truncated.packets.size() == 1U);
CHECK(truncated.packets[0].crc_ok);
CHECK(truncated.packets[0].start_sample == static_cast<int64_t>(after_timeout));
CHECK(truncated.terminals.size() == 1U);
// With no flush/tick between frames, the first rise strictly beyond 2T both
// closes the old frame and opens the new preamble. Inclusive spans must not
// share that sample.
DecoderHarness adjacent_timeout;
adjacent_timeout.lockPreamble();
adjacent_timeout.emitByte(0xE7, true);
const uint64_t adjacent_preamble = adjacent_timeout.phase + abort_silence + 1U;
adjacent_timeout.lockPreambleAt(adjacent_preamble);
adjacent_timeout.emitPacket(packet);
CHECK(adjacent_timeout.terminals.size() == 1U);
CHECK(adjacent_timeout.terminals[0].reason == IrFoxTerminalReason::Timeout);
CHECK(adjacent_timeout.packets.size() == 1U);
CHECK(adjacent_timeout.packets[0].crc_ok);
CHECK(adjacent_timeout.terminals[0].end_sample < adjacent_timeout.packets[0].start_sample);
CHECK(adjacent_timeout.packets[0].start_sample == static_cast<int64_t>(adjacent_preamble));
return 0;
}

View File

@ -0,0 +1,43 @@
#include "IrFoxPacketClassifier.h"
#include <cassert>
#include <cstdint>
static IrFoxPacketDecision classify(const uint8_t* data, uint8_t size, bool crc_ok, uint16_t id = 0)
{
return irfox::classifyPacket(data, size, crc_ok, id);
}
int main()
{
const uint8_t data_to_42[] = {0xE7, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
assert(classify(data_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
assert(classify(data_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
const uint8_t broadcast_data[] = {0xC7, 0x00, 0x01, 0xFD, 0xE8, 0x00, 0x00};
assert(classify(broadcast_data, 7, true, 41).outcome == IrFoxPacketOutcome::Accepted);
const uint8_t request_to_42[] = {0x47, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
assert(classify(request_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
assert(classify(request_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
const uint8_t back_to_42[] = {0x87, 0x00, 0x01, 0x00, 0x2A, 0x00, 0x00};
assert(classify(back_to_42, 7, true, 42).outcome == IrFoxPacketOutcome::Accepted);
assert(classify(back_to_42, 7, true, 41).outcome == IrFoxPacketOutcome::IgnoredAddress);
const uint8_t back[] = {0x05, 0x00, 0x01, 0x00, 0x00};
assert(classify(back, 5, true, 41).outcome == IrFoxPacketOutcome::Accepted);
const uint8_t accept[] = {0x26, 0x00, 0x01, 0x55, 0x00, 0x00};
assert(classify(accept, 6, true, 41).outcome == IrFoxPacketOutcome::Accepted);
const uint8_t unknown[] = {0x63, 0x00, 0x00};
assert(classify(unknown, 3, true).outcome == IrFoxPacketOutcome::RawOnlyUnknownType);
const uint8_t short_data[] = {0xE5, 0x00, 0x01, 0x00, 0x2A};
assert(classify(short_data, 5, true).outcome == IrFoxPacketOutcome::RawOnlyTypedLength);
assert(classify(data_to_42, 7, false).outcome == IrFoxPacketOutcome::RejectedCrc);
const uint8_t too_short[] = {0xE2, 0x00};
const IrFoxPacketDecision short_decision = classify(too_short, 2, false);
assert(short_decision.outcome == IrFoxPacketOutcome::RejectedLength);
assert(short_decision.message_type == irfox::kMsgDataAccept);
return 0;
}

View File

@ -162,11 +162,13 @@ void IR_Decoder::_tick()
if (addrAcceptSendTo && addrAcceptSendTo < IR_Broadcast)
isWaitingAcceptSend = true;
}
gotRaw.set(&packInfo, id);
// Raw keeps the decoder's common minimum-size contract. Known packet
// layouts are validated by their typed BasePack::set calls above.
gotRaw.set(&packInfo, id, false);
}
if (isWaitingAcceptSend && millis() - acceptSendTimer > acceptDelay)
{
encoder->sendAccept(addrAcceptSendTo, acceptCustomByte);
if (encoder != nullptr) encoder->sendAccept(addrAcceptSendTo, acceptCustomByte); // B2: у Car decoder без парного энкодера (nullptr) → был HardFault
isWaitingAcceptSend = false;
}
}

View File

@ -10,12 +10,12 @@ private:
static std::list<IR_Decoder*>& get_dec_list();
void _tick();
uint32_t acceptSendTimer;
bool isWaitingAcceptSend;
uint16_t addrAcceptSendTo;
uint32_t acceptSendTimer = 0;
bool isWaitingAcceptSend = false; // B4: без инициализации мусор мог дёрнуть sendAccept при старте
uint16_t addrAcceptSendTo = 0;
uint16_t acceptDelay = IR_ResponseDelay;
uint8_t acceptCustomByte;
uint8_t acceptCustomByte = 0;
bool extiEnabled_ = false;
bool rxExtiPreemptConfigured_ = false;

View File

@ -74,7 +74,6 @@ bool IR_DecoderRaw::availableRaw()
void IR_DecoderRaw::pulseFilterResetStats()
{
pulseFilterDropFilteredOverflow = 0;
pulseFilterDropHoldOverflow = 0;
pulseFilterDropGlitchPairs = 0;
}
@ -146,9 +145,19 @@ const __FlashStringHelper *IR_DecoderRaw::rxBriefReasonTag(RxBriefReason reason)
default: return F("UNK");
}
}
#endif // IR_RX_BRIEF_LOG (rxBriefReasonTag)
// Безусловна: always-on счёт причин RX-событий (наблюдаемость по контракту
// живучести — счётчики живут и в проде без печати); печать — только под флагом.
void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uint32_t tUs)
{
const uint8_t ri = (uint8_t)reason;
if (ri < 14U)
rxReasonCnt[ri]++;
#if !IR_RX_BRIEF_LOG
(void)a; (void)b; (void)tUs;
return;
#else
#if IR_RX_BRIEF_LOG_REJECT_ONLY
if (reason == RxBriefReason::Ok || reason == RxBriefReason::Preamble)
return;
@ -221,8 +230,26 @@ void IR_DecoderRaw::rxBriefLog(RxBriefReason reason, uint16_t a, uint16_t b, uin
break;
}
Serial.println();
#endif // IR_RX_BRIEF_LOG (печать)
}
void IR_DecoderRaw::printRxReasonStats(Print &out) const
{
static const char *const kTags[14] = {"?", "MUTEB", "MUTEE", "QRAW", "QFLT", "HOLD",
"GLITCH", "TIME", "PREAMB", "SYNC", "BUF",
"TIMEOUT", "CRC", "OK"};
out.print(F("RXSTAT"));
for (uint8_t i = 1; i < 14U; i++)
{
out.print(',');
out.print(kTags[i]);
out.print('=');
out.print(rxReasonCnt[i]);
}
out.println();
}
#if IR_RX_BRIEF_LOG
void IR_DecoderRaw::rxBriefNoteMuteBlockedIsr(uint32_t tUs)
{
(void)tUs;
@ -277,10 +304,8 @@ void IR_DecoderRaw::isr()
// Интервалы между соседними фронтами считаются как (uint32_t)(t1 - t0) — корректно при
// паузе < ~35 мин между фронтами; условие «тишина > longSilence» в preambleProcessEdge
// переписано без front.time > prevRise (оно ломается при wrap).
uint32_t t;
noInterrupts();
t = micros();
interrupts();
// B6: micros() в ISR не требует маскировки; прежний interrupts() посреди ISR снимал PRIMASK.
const uint32_t t = micros();
FrontStorage edge;
edge.dir = port->IDR & mask;
@ -344,7 +369,7 @@ bool IR_DecoderRaw::rxTimeoutPipelineBusy() const
if (pulseFilterHoldCount != 0U)
return true;
noInterrupts();
const bool busy = !subBuffer.isEmpty() || !filteredSubBuffer.isEmpty();
const bool busy = !subBuffer.isEmpty();
interrupts();
return busy;
}
@ -376,10 +401,9 @@ inline void IR_DecoderRaw::checkTimeout()
#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
noteRxEnd(RxEndReason::Timeout, micros());
isRecive = false; // приём завершён
msgTypeReceive = 0;
// Как после listenStart(): без сброса isReciveRaw + firstRX() декодер остаётся
@ -394,6 +418,62 @@ inline void IR_DecoderRaw::checkTimeout()
}
// ====================================================================
void IR_DecoderRaw::noteRxEnd(RxEndReason reason, uint32_t tUs)
{
rxEnd.seq++;
rxEnd.reason = reason;
rxEnd.msgType = (i_dataBuffer >= 8U * msgBytes) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF;
rxEnd.packSize = (uint8_t)packSize;
rxEnd.tUs = tUs;
rxEnd.expectedEndUs = (packSize >= msgBytes + crcBytes)
? rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize) + irTicksToUs((uint32_t)syncBits * irBitTicks)
: 0U;
}
void IR_DecoderRaw::abortFrame(uint32_t tUs)
{
#if defined(IRDEBUG_SERIAL_PACK)
packTraceOnTimeoutOrAbort(false);
#endif
noteRxEnd(RxEndReason::Abort, tUs);
isRecive = false;
isReciveRaw = false;
msgTypeReceive = 0;
firstRX();
releasePreambleGuard(tUs);
}
// После обрыва кадра «длинная тишина» (IR_timeout × 2 ≈ 30 мс) перед новым кандидатом преамбулы не требуется.
// prevRise — последний ДЕКОДИРОВАННЫЙ фронт; после abort он свежий, а фронты, отброшенные гвардом, его не двигают,
// поэтому валидный кадр, начавшийся через <30 мс после обрыва мусора, проглатывался целиком без счётчика
// (стенд 09.09: КУ теряла пинг машинки после обрывков чужого заднего и всплеска её дальномера за 24 мс до пинга).
// Ложных захватов это не добавляет: хвост оборванного кадра (период фронтов 962 мкс, синхробиты ~1100) не проходит
// грубый фильтр периода преамбулы (2116…3270 мкс), а настоящая преамбула перезапускает кандидата по паузе > IR_timeout.
// После чистого конца кадра гвард остаётся: там он отсекает хвост синхробитов.
void IR_DecoderRaw::releasePreambleGuard(uint32_t tUs)
{
prevRise = tUs - IR_timeout * 2U - 1U; // «тишина уже была»: (front.time - prevRise) > longSilence для следующего фронта
}
void IR_DecoderRaw::expirePreambleCandidate()
{
if (preambleState != PreambleState::Candidate || rxTimeoutPipelineBusy())
return;
if ((micros() - preambleCandidateLastEdgeTime) > IR_timeout * (uint32_t)IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT)
{
if (preambleGoodPeriods)
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, 0, micros());
preambleResetToIdle();
}
}
uint32_t IR_DecoderRaw::rxExpectedEndUs() const
{
if (!isRecive || preambleState != PreambleState::Locked || isWrongPack || packSize < msgBytes + crcBytes)
return 0;
return rxLockTimeUsVal + irLockToDecodeEndUs((uint8_t)packSize);
}
void IR_DecoderRaw::tick()
{
#if IR_RX_BRIEF_LOG
@ -403,56 +483,75 @@ void IR_DecoderRaw::tick()
// Не в начале до pop: иначе после checkTimeout lastEdgeTime vs micros() расходятся
// с метками ISR из очереди → ложные TIMEOUT (bits=0) каждый пакет.
FrontStorage currentFront;
bool hasCurrentFront = false;
FrontStorage rawFront;
bool hasRawFront = false;
noInterrupts();
FrontStorage *rawPtr = subBuffer.pop();
if (rawPtr != nullptr)
bool processedFront = false;
// Батч-выемка (IR_RX_TICK_BATCH): раньше 1 фронт/tick — приём был привязан
// к частоте loop, буфер 250 фронтов (~120мс эфира) переполнялся на медленном
// loop и кадры гибли молча. Семантика idle-пути сохранена: flush
// пульс-фильтра — только когда буфер пуст (как прежняя else-ветка).
uint8_t batchBudget = IR_RX_TICK_BATCH;
bool hasRawFront = subBuffer.pop(rawFront); // B5: копия под одной критсекцией (без торн-рида)
while (hasRawFront)
{
rawFront = *rawPtr;
hasRawFront = true;
}
interrupts();
if (IR_INPUT_MIN_PULSE_US > 0U)
{
if (hasRawFront)
pulseFilterFeedOneRaw(rawFront);
pulseFilterPushRaw(rawFront);
FrontStorage confirmedFront;
while (pulseFilterTryTakeConfirmed(confirmedFront))
{
processDecodedFront(confirmedFront);
processedFront = true;
}
}
else
pulseFilterFlushTimeout(micros());
noInterrupts();
FrontStorage *flt = filteredSubBuffer.pop();
if (flt != nullptr)
{
currentFront = *flt;
hasCurrentFront = true;
processDecodedFront(rawFront);
processedFront = true;
}
interrupts();
}
else if (hasRawFront)
if (--batchBudget == 0U)
{
currentFront = rawFront;
hasCurrentFront = true;
break;
}
hasRawFront = subBuffer.pop(rawFront);
}
if (!hasRawFront && IR_INPUT_MIN_PULSE_US > 0U)
{
const uint32_t nowUs = micros();
FrontStorage confirmedFront;
while (pulseFilterTryFlushOne(nowUs, confirmedFront))
{
processDecodedFront(confirmedFront);
processedFront = true;
}
}
if (!hasCurrentFront)
if (!processedFront)
{
isSubBufferOverflow = false;
listenStart();
checkTimeout();
listenStart();
expirePreambleCandidate();
#if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif
return;
} // Если данных нет - ничего не делаем
checkTimeout();
listenStart();
#if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs();
#endif
#if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif
}
void IR_DecoderRaw::processDecodedFront(const FrontStorage &currentFront)
{
if (preambleProcessEdge(currentFront))
{
lastEdgeTime = currentFront.time;
goto END;
return;
}
lastEdgeTime = currentFront.time; // запоминаем любой фронт
@ -472,13 +571,11 @@ void IR_DecoderRaw::tick()
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;
return;
}
#endif
#if IR_MICRO_GAP_RISE_REJECT
@ -489,22 +586,18 @@ void IR_DecoderRaw::tick()
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;
return;
}
#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;
return;
}
if (candRp > riseTimeMax / 4 || highCount || lowCount)
@ -557,7 +650,7 @@ void IR_DecoderRaw::tick()
}
}
#ifdef IRDEBUG
// goto END; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// return; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#endif
//----------------------------------------------------------------------------------
#ifdef IRDEBUG
@ -567,12 +660,10 @@ void IR_DecoderRaw::tick()
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;
return;
}
// определить направление фронта
@ -727,17 +818,6 @@ void IR_DecoderRaw::tick()
else
{ // Если ```\__ ↓
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
END:;
listenStart();
checkTimeout();
#if IR_RX_BRIEF_LOG
rxBriefFlushDeferredIsrLogs();
#endif
#if defined(IR_EDGE_TRACE)
while (edgeTraceFlushChunk(Serial, 48) > 0) {}
#endif
}
void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
@ -745,12 +825,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#if !defined(IRDEBUG_SERIAL_PACK)
(void)packTraceInvertFix;
#endif
if (i_dataBuffer > dataByteSizeMax * 8)
{ // проверка переполнения
if (i_dataBuffer >= dataByteSizeMax * 8)
{ // проверка переполнения (>=: иначе при i_dataBuffer==dataByteSizeMax*8 запись dataBuffer[38] за границей массива — B3)
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"));
@ -760,10 +838,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
{
// Как checkTimeout/listenStart: firstRX() сбрасывает буфер битов, преамбулу и
// pulseFilterReset() — при IR_INPUT_MIN_PULSE_US > 0 иначе остаётся «хвост» в hold/filtered.
isRecive = false;
isReciveRaw = false;
msgTypeReceive = 0;
firstRX();
abortFrame(micros());
return;
}
@ -827,12 +902,12 @@ 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
abortFrame(micros()); // битый кадр не удерживает приёмник до таймаута
return;
}
}
}
@ -857,6 +932,15 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
if (i_dataBuffer == 8 * msgBytes)
{ // Ппервый байт
packSize = dataBuffer[0] & IR_MASK_MSG_INFO;
// B1: под-минимальная длина (1..2) физически не несёт CRC (min кадр = msg+crc = 3 байта) → шум/битьё.
// Без отсева packSize==1 даёт crcCheck(1-2) → len=255 → OOB-чтение dataBuffer[0..256] (массив 38).
// packSize>=3 (в т.ч. будущие компактные кадры) обрабатываются как обычно.
if (packSize < msgBytes + crcBytes) // 0..2: кадр физически не несёт CRC — шум/битьё
{
isWrongPack = true;
abortFrame(micros());
return;
}
}
// Тип приёма (для isReceive): выставляем сразу после первого байта, ДО проверки «Конец».
@ -867,8 +951,8 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
msgTypeReceive = (dataBuffer[0] >> 5) | 0b11111000;
}
if (packSize && (i_dataBuffer == packSize * bitPerByte))
{ // Конец
if (packSize > crcBytes && (i_dataBuffer == packSize * bitPerByte))
{ // Конец (B1: packSize>crcBytes → crcCheck(packSize-crcBytes) не уходит в underflow/OOB)
packInfo.buffer = dataBuffer;
packInfo.crc = crcValue;
packInfo.err = errors;
@ -880,6 +964,7 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
preambleResetToIdle();
msgTypeReceive = 0;
isAvailable = crcCheck(packSize - crcBytes, crcValue);
noteRxEnd(isAvailable ? RxEndReason::Ok : RxEndReason::Crc, micros());
#ifdef BRUTEFORCE_CHECK
{
@ -926,12 +1011,10 @@ void IR_DecoderRaw::writeToBuffer(bool bit, bool packTraceInvertFix)
#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);
rejectPackSize = static_cast<uint8_t>(packSize);
@ -951,7 +1034,7 @@ bool IR_DecoderRaw::crcCheck(uint8_t len, crc_t &crc)
crc = (crc8(dataBuffer, 0, len, poly1) << 8) & ~((crc_t)0xFF);
crc |= crc8(dataBuffer, 0, len + 1, poly2) & (crc_t)0xFF;
if (dataBuffer[len] == (crc >> 8) & 0xFF &&
if (dataBuffer[len] == ((crc >> 8) & 0xFF) && // B7: скобки — иначе (== & 0xFF) по приоритету
dataBuffer[len + 1] == (crc & 0xFF))
{
crcOK = true;
@ -974,6 +1057,7 @@ bool IR_DecoderRaw::availableReject()
uint16_t IR_DecoderRaw::ceil_div(uint16_t val, uint16_t divider)
{
if (divider == 0) return 0; // B10: защита от деления на 0 (актуально при freeFrec — сейчас выкл, не включаем)
int ret = val / divider;
if ((val << 4) / divider - (ret << 4) >= 8)
ret++;
@ -1455,34 +1539,18 @@ void IR_DecoderRaw::pulseFilterShiftLeft(uint8_t 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)
void IR_DecoderRaw::pulseFilterPushRaw(const FrontStorage &e)
{
const uint32_t minUs = IR_INPUT_MIN_PULSE_US;
if (minUs == 0U)
{
pulseFilterEmit(e);
return;
}
@ -1492,47 +1560,52 @@ void IR_DecoderRaw::pulseFilterFeedOneRaw(const FrontStorage &e)
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();
}
bool IR_DecoderRaw::pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime)
{
const uint32_t minUs = IR_INPUT_MIN_PULSE_US;
if (minUs == 0U)
return false;
const uint8_t holdback = irPulseFilterHoldbackEdges();
if (logTime == 0U)
logTime = pulseFilterLastRawTime;
for (;;)
{
if (pulseFilterHoldCount < 2)
return;
return false;
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
rxBriefLog(RxBriefReason::Glitch, irClampU16(pulseFilterDropGlitchPairs), 0, logTime);
pulseFilterShiftLeft(2);
continue;
}
if (pulseFilterHoldCount <= holdback)
return;
return false;
pulseFilterEmit(pulseFilterHoldEdges[0]);
out = pulseFilterHoldEdges[0];
pulseFilterShiftLeft(1);
return true;
}
}
void IR_DecoderRaw::pulseFilterFlushTimeout(uint32_t nowUs)
bool IR_DecoderRaw::pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out)
{
if (IR_INPUT_MIN_PULSE_US == 0U || !pulseFilterLastRawValid || pulseFilterHoldCount == 0)
return;
return false;
const uint32_t waitUs = IR_INPUT_MIN_PULSE_US * (uint32_t)IR_INPUT_FILTER_TIMEOUT_MULT;
if ((uint32_t)(nowUs - pulseFilterLastRawTime) < waitUs)
return;
return false;
while (pulseFilterHoldCount > 0)
{
@ -1542,16 +1615,16 @@ void IR_DecoderRaw::pulseFilterFlushTimeout(uint32_t nowUs)
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]);
out = pulseFilterHoldEdges[0];
pulseFilterShiftLeft(1);
return true;
}
return false;
}
uint32_t IR_DecoderRaw::preambleJitterTolUs(uint32_t baselineUs) const
@ -1611,16 +1684,17 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
if (!isReciveRaw && front.dir &&
((prevRise == 0U && front.time > longSilence) ||
(prevRise != 0U && (uint32_t)(front.time - prevRise) > longSilence)))
{
preambleStartCandidate(front);
return true;
}
}
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);
}
@ -1641,9 +1715,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
{
preambleGoodPeriods = 0;
preambleMeanPeriod = 0;
#if IR_RX_BRIEF_LOG
rxBriefLog(RxBriefReason::Preamble, 0, irClampU16(period), front.time);
#endif
return true;
}
@ -1664,9 +1736,7 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
}
else
{
#if IR_RX_BRIEF_LOG
rxBriefLog(RxBriefReason::Preamble, preambleGoodPeriods, irClampU16(period), front.time);
#endif
preambleGoodPeriods = 1;
preambleMeanPeriod = (uint16_t)period;
}
@ -1698,6 +1768,8 @@ bool IR_DecoderRaw::preambleProcessEdge(const FrontStorage &front)
isRecive = true;
isReciveRaw = true;
risePeriod = preambleMeanPeriod;
rxLockSeqCnt++;
rxLockTimeUsVal = front.time;
#if defined(IRDEBUG_SERIAL_PACK)
packTraceResetFrame();
packTraceOpen = true;

View File

@ -52,7 +52,41 @@ public:
inline bool isOverflow() { return isBufferOverflow; }; // Буффер переполнился
bool isSubOverflow();
volatile inline bool isReciving() { return isRecive; }; // Возвращает true, если происходит приём пакета
uint32_t pulseFilterDroppedByFilteredOverflow() const { return pulseFilterDropFilteredOverflow; }
// Активность линии по СОСТОЯНИЮ (не по хардкод-длительности): кадр залочен ИЛИ формируется
// ВАЛИДНАЯ преамбула (>=1 совпавший по периоду фронт — отличает реальный кадр от одиночного
// шумового фронта, который лишь заводит Candidate, но не набирает goodPeriods). Для гейта заднего:
// «не стрелять, пока на линии идёт/формируется кадр (напр. ответ точки)». Аддитивно, const.
inline bool rxLineActive() const {
return isRecive ||
(preambleState == PreambleState::Candidate && preambleGoodPeriods >= 1U);
}
// Объявленная длина ПРИНИМАЕМОГО кадра (байт) из ПЕРВОГО байта, если он уже принят и валиден;
// иначе 0 (ещё не знаем / битый). До CRC это НЕДОВЕРЕННОЕ значение — потребитель, получив 0
// или чрезмерное, обязан брать rxMaxPackSize() (безопасно держать задний до конца макс.кадра).
inline uint16_t rxDeclaredPackSize() const {
return (isRecive && packSize && !isWrongPack) ? packSize : 0;
}
// Протокольный МАКСИМУМ длины кадра (байт) — верхняя граница бюджета удержания заднего.
static constexpr uint16_t rxMaxPackSize() { return (uint16_t)irMaxPackSize; }
// ---- Наблюдаемость приёма по СОСТОЯНИЮ: лок / ожидаемый конец / факт завершения с причиной ----
enum class RxEndReason : uint8_t { None = 0, Ok, Crc, Timeout, Abort };
struct RxEndInfo {
uint16_t seq = 0; // номер завершения (растёт на каждом терминале)
RxEndReason reason = RxEndReason::None;
uint8_t msgType = 0xFF; // 0xFF = первый байт не был принят
uint8_t packSize = 0; // объявленная длина (0 = неизвестна)
uint32_t tUs = 0; // micros() терминала
uint32_t expectedEndUs = 0; // расчётный конец кадра в эфире (0 = неизвестен)
};
uint16_t rxLockSeq() const { return rxLockSeqCnt; } // ++ в момент лока преамбулы
uint32_t rxLockTimeUs() const { return rxLockTimeUsVal; } // метка фронта лока (ISR-время)
/// Тип принимаемого кадра (3 бита) после первого байта; 0xFF пока неизвестен / приём не идёт.
uint8_t rxMsgType() const { return (isRecive && packSize) ? (uint8_t)((dataBuffer[0] >> 5) & IR_MASK_MSG_TYPE) : 0xFF; }
const RxEndInfo &rxLastEnd() const { return rxEnd; }
/// Расчётный момент последнего бита данных текущего кадра (по объявленной длине); 0 = не Locked / длина неизвестна.
uint32_t rxExpectedEndUs() const;
uint32_t pulseFilterDroppedByFilteredOverflow() const { return 0; }
uint32_t pulseFilterDroppedByHoldOverflow() const { return pulseFilterDropHoldOverflow; }
uint32_t pulseFilterDroppedGlitchPairs() const { return pulseFilterDropGlitchPairs; }
void pulseFilterResetStats();
@ -65,6 +99,14 @@ public:
uint16_t edgeTraceFlushChunk(Print &out, uint16_t maxRec = 48);
#endif
/// Always-on счётчики RX-событий по причинам (см. RxBriefReason: 6=Glitch,
/// 7=Timing, 8=Preamble, 9=Sync, 10=BufOverflow, 11=Timeout, 12=Crc, 13=Ok).
/// MuteBegin/End и RawOverflow(1..3) тикают только при IR_RX_BRIEF_LOG (ISR-агрегат).
const uint16_t *rxReasonCounters() const { return rxReasonCnt; }
void rxReasonCountersClear() { for (uint8_t i = 0; i < 14; i++) rxReasonCnt[i] = 0; }
/// Однострочная сводка: "RXSTAT,GLITCH=..,TIME=..,PREAMB=..,SYNC=..,BUF=..,TIMEOUT=..,CRC=..,OK=.."
void printRxReasonStats(Print &out) const;
/// Кадр собран по длине из заголовка, но CRC не сошёлся — один раз можно прочитать копию сырых байтов.
bool availableReject();
uint8_t getRejectSize() const { return rejectPackSize; }
@ -122,8 +164,6 @@ 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;
@ -131,7 +171,6 @@ private:
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;
@ -142,6 +181,13 @@ private:
Locked = 2
};
PreambleState preambleState = PreambleState::Idle;
uint16_t rxLockSeqCnt = 0;
uint32_t rxLockTimeUsVal = 0;
RxEndInfo rxEnd;
void noteRxEnd(RxEndReason reason, uint32_t tUs); // терминал: фиксирует тип/длину/расчётный конец
void abortFrame(uint32_t tUs); // немедленный сброс битого кадра (sync/длина/overflow)
void releasePreambleGuard(uint32_t tUs); // после abort: новый кандидат преамбулы без ожидания длинной тишины
void expirePreambleCandidate(); // кандидат без фронтов дольше таймаута → Idle
uint8_t preambleGoodPeriods = 0;
uint16_t preambleMeanPeriod = 0;
uint32_t preambleCandidateLastEdgeTime = 0;
@ -197,12 +243,12 @@ bool isReciveRaw = false;
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 pulseFilterPushRaw(const FrontStorage &e);
bool pulseFilterTryTakeConfirmed(FrontStorage &out, uint32_t logTime = 0);
bool pulseFilterTryFlushOne(uint32_t nowUs, FrontStorage &out);
void pulseFilterShiftLeft(uint8_t n);
void pulseFilterReset();
void processDecodedFront(const FrontStorage &currentFront);
static uint32_t absDiffU32(uint32_t a, uint32_t b);
bool registerPairMuteEncoder(IR_Encoder *enc);
void refreshPairMuteState();
@ -238,9 +284,13 @@ bool isReciveRaw = false;
/// @return Результат
uint16_t ceil_div(uint16_t val, uint16_t divider);
// rxBriefLog безусловен: ВСЕГДА инкрементирует rxReasonCnt[reason]
// (always-on наблюдаемость по контракту живучести), печать события —
// только при IR_RX_BRIEF_LOG. Вызовы в местах отказов тоже безусловны.
void rxBriefLog(RxBriefReason reason, uint16_t a = 0, uint16_t b = 0, uint32_t tUs = 0);
uint16_t rxReasonCnt[14] = {}; // индекс = (uint8_t)RxBriefReason, 1..13
#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();

View File

@ -1,7 +1,24 @@
#include "IR_Encoder.h"
#include "IR_DecoderRaw.h"
#include "IrTxIsrBufferedStorage.h"
#include <string.h>
#if defined(_MSC_VER)
#define IRPROTO_PRAGMA_MESSAGE(text) __pragma(message(text))
#else
#define IRPROTO_PRAGMA_MESSAGE(text) _Pragma(#text)
#endif
#if defined(ARDUINO_ARCH_STM32)
#if defined(STM32G4xx)
IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR + built-in DMA"))
#elif defined(STM32F4xx)
IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR only"))
#else
IRPROTO_PRAGMA_MESSAGE(message("[IR-protocol] TX backends: ISR"))
#endif
#endif
#define LoopOut 12
#define ISR_Out 10
#define TestOut 13
@ -14,6 +31,7 @@ IR_Encoder::IR_Encoder(uint8_t pin, uint16_t addr, IR_DecoderRaw *decPair, bool
{
setPin(pin);
id = addr;
txIsrMode_ = txIsrLegacyMode_ ? TxIsrMode::Legacy : TxIsrMode::Buffered;
this->decPair = decPair;
if (decPair != nullptr)
{
@ -45,9 +63,44 @@ 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;
bool IR_Encoder::txIsrLegacyMode_ = true;
uint16_t IR_Encoder::s_carrierMultiply = 2;
const char* irSendStatusToString(IR_SendStatus status)
{
switch (status)
{
case IR_SendStatus::Success:
return "Success";
case IR_SendStatus::PayloadTooLarge:
return "PayloadTooLarge";
case IR_SendStatus::EncoderBusy:
return "EncoderBusy";
case IR_SendStatus::BufferTooLarge:
return "BufferTooLarge";
case IR_SendStatus::ExternalBackendBusy:
return "ExternalBackendBusy";
case IR_SendStatus::ExternalStartFailed:
return "ExternalStartFailed";
case IR_SendStatus::ExternalNoStream:
return "ExternalNoStream";
case IR_SendStatus::ExternalInvalidConfig:
return "ExternalInvalidConfig";
case IR_SendStatus::BuildGateRunsFailed:
return "BuildGateRunsFailed";
case IR_SendStatus::ScaleGateRunsFailed:
return "ScaleGateRunsFailed";
case IR_SendStatus::DmaStartFailed:
return "DmaStartFailed";
case IR_SendStatus::EncoderPinUnavailable:
return "EncoderPinUnavailable";
case IR_SendStatus::BufferedStorageInvalid:
return "BufferedStorageInvalid";
default:
return "Unknown";
}
}
void IR_Encoder::setCarrierMultiply(uint16_t multiply)
{
if (multiply < 2)
@ -146,6 +199,11 @@ bool IR_Encoder::scaleGateRunsToPhysical(IR_TxGateRun* runs, size_t* ioCount, si
void IR_Encoder::setTxIsrLegacyMode(bool legacy)
{
txIsrLegacyMode_ = legacy;
const TxIsrMode mode = legacy ? TxIsrMode::Legacy : TxIsrMode::Buffered;
for (IR_Encoder *p = head; p != nullptr; p = p->next)
{
p->txIsrMode_ = mode;
}
}
bool IR_Encoder::txIsrLegacyMode()
@ -153,6 +211,54 @@ bool IR_Encoder::txIsrLegacyMode()
return txIsrLegacyMode_;
}
void IR_Encoder::attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage)
{
txBufferedCtx_ = &storage;
}
void IR_Encoder::detachBufferedIsrStorage()
{
txBufferedCtx_ = nullptr;
if (!isSending)
{
txActiveBufferedCtx_ = nullptr;
txUseBufferedIsr_ = false;
}
}
bool IR_Encoder::hasBufferedIsrStorage() const
{
return txBufferedCtx_ != nullptr && txBufferedCtx_->isValid();
}
void IR_Encoder::enableBufferedIsr(IrTxIsrBufferedStorageBase& storage)
{
attachBufferedIsrStorage(storage);
txIsrMode_ = TxIsrMode::Buffered;
}
void IR_Encoder::disableBufferedIsr()
{
txIsrMode_ = TxIsrMode::Legacy;
if (!isSending)
{
txActiveBufferedCtx_ = nullptr;
txUseBufferedIsr_ = false;
}
}
IR_Encoder::TxIsrMode IR_Encoder::txIsrMode() const
{
return txIsrMode_;
}
bool IR_Encoder::shouldUseBufferedIsr() const
{
return txIsrMode_ == TxIsrMode::Buffered &&
txBufferedCtx_ != nullptr &&
txBufferedCtx_->isValid();
}
bool IR_Encoder::txAdvanceBoundary(TxFsmState &st, const uint8_t *sendBufferLocal)
{
while (true)
@ -246,6 +352,29 @@ bool IR_Encoder::txEmitTick(TxFsmState &st, const uint8_t *sendBufferLocal, bool
return txAdvanceAfterOutput(st, sendBufferLocal);
}
// Обход кадра по границам ранов: между границами автомат выдаёт st.toggleCounter+1 тактов
// уровня st.state (txAdvanceAfterOutput считает toggleCounter до нуля, затем txAdvanceBoundary
// открывает следующий ран). Даёт ту же последовательность тактов, что потиковый обход, но за
// число шагов = число ранов (пинг: ~230 вместо ~8700 тактов — на 12 МГц это ~30 мс перед стартом DMA).
template <typename Emit>
bool IR_Encoder::txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit)
{
for (;;)
{
const bool gate = st.state;
const uint32_t lenTicks = (uint32_t)st.toggleCounter + 1U;
if (!emit(gate, lenTicks))
{
return false;
}
st.toggleCounter = 0;
if (!txAdvanceBoundary(st, sendBufferLocal))
{
return true;
}
}
}
void IR_Encoder::loadTxFsmFromMembers(TxFsmState &st) const
{
st.sendLen = sendLen;
@ -336,6 +465,8 @@ void IR_Encoder::externalFinishSend()
}
isSending = false;
txUseBufferedIsr_ = false;
txActiveBufferedCtx_ = nullptr;
refreshBlindDecoderMuteState();
}
@ -368,26 +499,115 @@ size_t IR_Encoder::buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRu
st.currentBitSequence = bitHigh;
size_t runCount = 0;
bool isActive = true;
while (isActive)
{
bool gate = false;
isActive = txEmitTick(st, sendBufferLocal, gate);
const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool {
if (runCount > 0 && outRuns[runCount - 1].gate == gate)
{
outRuns[runCount - 1].lenTicks = (uint16_t)(outRuns[runCount - 1].lenTicks + 1U);
}
else
const uint32_t merged = (uint32_t)outRuns[runCount - 1].lenTicks + lenTicks;
if (merged > 65535U)
{
if (runCount >= maxRuns)
return false;
}
outRuns[runCount - 1].lenTicks = (uint16_t)merged;
return true;
}
if (runCount >= maxRuns || lenTicks > 65535U)
{
return false;
}
outRuns[runCount].gate = gate;
outRuns[runCount].lenTicks = (uint16_t)lenTicks;
runCount++;
return true;
});
return ok ? runCount : 0;
}
size_t IR_Encoder::buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply)
{
if (packet == nullptr || outRuns == nullptr || maxRuns == 0)
{
return 0;
}
outRuns[runCount].gate = gate;
outRuns[runCount].lenTicks = 1U;
runCount++;
if (len == 0 || len > dataByteSizeMax)
{
return 0;
}
if (multiply < 2)
{
multiply = 2;
}
// Copy into fixed-size buffer to match original encoder behavior (safe reads past sendLen).
uint8_t sendBufferLocal[dataByteSizeMax] = {0};
memcpy(sendBufferLocal, packet, len);
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;
auto appendPhysicalRun = [&](bool gate, uint32_t logicalLen, size_t& runCount) -> bool {
if (logicalLen == 0)
{
return true;
}
uint32_t phys = (logicalLen * (uint32_t)multiply) / 2U;
if (logicalLen > 0 && phys == 0)
{
phys = 1;
}
while (phys > 0)
{
if (runCount >= maxRuns)
{
return false;
}
const uint32_t chunk = phys > 65535U ? 65535U : phys;
outRuns[runCount].gate = gate;
outRuns[runCount].lenTicks = static_cast<uint16_t>(chunk);
runCount++;
phys -= chunk;
}
return true;
};
size_t runCount = 0;
bool currentGate = false;
uint32_t currentLogicalLen = 0;
bool havePendingRun = false;
const bool ok = txWalkRuns(st, sendBufferLocal, [&](bool gate, uint32_t lenTicks) -> bool {
if (havePendingRun && currentGate == gate)
{
currentLogicalLen += lenTicks;
return true;
}
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
{
return false;
}
currentGate = gate;
currentLogicalLen = lenTicks;
havePendingRun = true;
return true;
});
if (!ok)
{
return 0;
}
if (havePendingRun && !appendPhysicalRun(currentGate, currentLogicalLen, runCount))
{
return 0;
}
return runCount;
}
@ -479,10 +699,13 @@ IR_SendResult IR_Encoder::sendData(uint16_t addrTo, uint8_t *data, uint8_t len,
IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8_t *data, uint8_t len, bool needAccept)
{
if (len > bytePerPack)
// 5-битное поле длины => ВЕСЬ кадр ≤31 байт (для Data payload ≤24). Было `len > bytePerPack(31)` —
// неверно: packSize=7+len оборачивался в заголовке (packSize & 0x1F) при len 25..31 → кадр молча
// терялся, а send возвращал успех. Проверяем полный packSize в широком типе (uint8_t 7+len мог переполниться).
if (((uint16_t)msgBytes + addrBytes + addrBytes + len + crcBytes) > IR_MASK_MSG_INFO)
{
Serial.println("IR Pack to big");
return IR_SendResult(false, 0);
return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge);
}
constexpr uint8_t dataStart = msgBytes + addrBytes + addrBytes;
memset(sendBuffer, 0x00, dataByteSizeMax);
@ -532,11 +755,15 @@ IR_SendResult IR_Encoder::sendDataFULL(uint16_t addrFrom, uint16_t addrTo, uint8
// }
// отправка
rawSend(sendBuffer, packSize);
const IR_SendStatus status = rawSend(sendBuffer, packSize);
if (status != IR_SendStatus::Success)
{
return IR_SendResult(false, 0, status);
}
// Возвращаем результат отправки
uint32_t sendTime = calculateSendTime(packSize);
return IR_SendResult(true, sendTime);
return IR_SendResult(true, sendTime, status);
}
@ -560,11 +787,15 @@ IR_SendResult IR_Encoder::sendAccept(uint16_t addrTo, uint8_t customByte)
sendBuffer[4] = crc8(sendBuffer, 0, 4, poly1) & 0xFF;
sendBuffer[5] = crc8(sendBuffer, 0, 5, poly2) & 0xFF;
rawSend(sendBuffer, packsize);
const IR_SendStatus status = rawSend(sendBuffer, packsize);
if (status != IR_SendStatus::Success)
{
return IR_SendResult(false, 0, status);
}
// Возвращаем результат отправки
uint32_t sendTime = calculateSendTime(packsize);
return IR_SendResult(true, sendTime);
return IR_SendResult(true, sendTime, status);
}
IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo)
@ -586,11 +817,15 @@ IR_SendResult IR_Encoder::sendRequest(uint16_t addrTo)
sendBuffer[5] = crc8(sendBuffer, 0, 5, poly1) & 0xFF;
sendBuffer[6] = crc8(sendBuffer, 0, 6, poly2) & 0xFF;
rawSend(sendBuffer, packsize);
const IR_SendStatus status = rawSend(sendBuffer, packsize);
if (status != IR_SendStatus::Success)
{
return IR_SendResult(false, 0, status);
}
// Возвращаем результат отправки
uint32_t sendTime = calculateSendTime(packsize);
return IR_SendResult(true, sendTime);
return IR_SendResult(true, sendTime, status);
}
IR_SendResult IR_Encoder::sendBack(uint8_t data)
@ -610,14 +845,17 @@ IR_SendResult IR_Encoder::sendBackTo(uint16_t addrTo, uint8_t *data, uint8_t len
IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len)
{
if (len > bytePerPack)
// Длина = ВЕСЬ кадр в 5 битах (≤31). Проверяем полный packSize. Было `len>bytePerPack` + `min(1,len)`:
// многобайтовый back (speed + customBackData) слался ОБРЕЗАННЫМ — packSize считал лишь 1 байт данных,
// остальные не влезали в кадр и затирались CRC. Теперь учитываем полный len.
if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO)
{
return IR_SendResult(false, 0);
return IR_SendResult(false, 0, IR_SendStatus::PayloadTooLarge);
}
memset(sendBuffer, 0x00, dataByteSizeMax);
uint8_t dataStart = msgBytes + addrBytes + (isAdressed ? addrBytes : 0);
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + min(uint8_t(1), len) + crcBytes;
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes;
uint8_t msgType =
((isAdressed ? IR_MSG_BACK_TO : IR_MSG_BACK) << 5) | ((packSize) & IR_MASK_MSG_INFO);
@ -643,11 +881,15 @@ IR_SendResult IR_Encoder::_sendBack(bool isAdressed, uint16_t addrTo, uint8_t *d
sendBuffer[packSize - crcBytes + 1] = crc8(sendBuffer, 0, packSize - crcBytes + 1, poly2) & 0xFF;
// отправка
rawSend(sendBuffer, packSize);
const IR_SendStatus status = rawSend(sendBuffer, packSize);
if (status != IR_SendStatus::Success)
{
return IR_SendResult(false, 0, status);
}
// Возвращаем результат отправки
uint32_t sendTime = calculateSendTime(packSize);
return IR_SendResult(true, sendTime);
return IR_SendResult(true, sendTime, status);
}
void IR_Encoder::registerWithBlindDecoders()
@ -674,18 +916,18 @@ void IR_Encoder::refreshBlindDecoderMuteState()
}
}
void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
IR_SendStatus IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
{
if (isSending)
{
// TODO: Обработка повторной отправки
return;
return IR_SendStatus::EncoderBusy;
}
// Проверка на переполнение буфера
if (len > dataByteSizeMax)
{
return;
return IR_SendStatus::BufferTooLarge;
}
// Serial.print("IR tx hex: ");
@ -700,25 +942,27 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
{
if (externalTxBusyFn != nullptr && externalTxBusyFn(externalTxCtx))
{
return;
return IR_SendStatus::ExternalBackendBusy;
}
sendLen = len;
txUseBufferedIsr_ = false;
txActiveBufferedCtx_ = nullptr;
isSending = true;
refreshBlindDecoderMuteState();
const bool ok = externalTxStartFn(externalTxCtx, this, ptr, len);
if (!ok)
const IR_SendStatus status = externalTxStartFn(externalTxCtx, this, ptr, len);
if (status != IR_SendStatus::Success)
{
isSending = false;
refreshBlindDecoderMuteState();
}
return;
return status;
}
if (port == nullptr || mask == 0)
{
return;
return IR_SendStatus::EncoderPinUnavailable;
}
if (ptr != sendBuffer)
@ -727,7 +971,11 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
}
sendLen = len;
if (txIsrLegacyMode_)
const bool useBufferedIsr = shouldUseBufferedIsr();
txUseBufferedIsr_ = useBufferedIsr;
txActiveBufferedCtx_ = useBufferedIsr ? txBufferedCtx_ : nullptr;
if (!useBufferedIsr)
{
toggleCounter = preambToggle;
dataBitCounter = bitPerByte - 1;
@ -753,40 +1001,43 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
isSending = true;
refreshBlindDecoderMuteState();
IR_Encoder::carrierResume();
return;
return IR_SendStatus::Success;
}
size_t nRuns = buildGateRuns(sendBuffer, len, txGateRuns_, irproto::kIsrTxMaxGateRuns);
IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_;
if (buf == nullptr || !buf->isValid())
{
txUseBufferedIsr_ = false;
txActiveBufferedCtx_ = nullptr;
return IR_SendStatus::BufferedStorageInvalid;
}
buf->resetRuntimeState();
txMultiplySnap_ = carrierMultiply();
size_t nRuns = buildPhysicalGateRuns(sendBuffer, len, buf->gateRuns, buf->maxGateRuns, txMultiplySnap_);
if (nRuns == 0U)
{
return;
}
if (!scaleGateRunsToPhysical(txGateRuns_, &nRuns, irproto::kIsrTxMaxGateRuns, carrierMultiply()))
{
return;
txUseBufferedIsr_ = false;
txActiveBufferedCtx_ = nullptr;
return IR_SendStatus::BuildGateRunsFailed;
}
uint32_t total = 0;
for (size_t i = 0; i < nRuns; i++)
{
total += txGateRuns_[i].lenTicks;
total += buf->gateRuns[i].lenTicks;
}
txBsrrTotalTicks_ = total;
buf->totalTicks = 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;
buf->wave.configure(setW, resetW, buf->gateRuns, nRuns, txMultiplySnap_, txPowerSnap_);
buf->wave.fill(buf->bsrrWords, buf->wordCount);
isSending = true;
refreshBlindDecoderMuteState();
@ -795,6 +1046,7 @@ void IR_Encoder::rawSend(uint8_t *ptr, uint8_t len)
port->BSRR = resetW;
}
IR_Encoder::carrierResume();
return IR_SendStatus::Success;
}
void IR_Encoder::isr()
@ -815,7 +1067,7 @@ void IR_Encoder::_isr()
if (port == nullptr)
return;
if (txIsrLegacyMode_)
if (!txUseBufferedIsr_)
{
const uint32_t setW = (uint32_t)mask;
const uint32_t resetW = ((uint32_t)mask) << 16U;
@ -850,33 +1102,49 @@ void IR_Encoder::_isr()
{
port->BSRR = resetW;
isSending = false;
txUseBufferedIsr_ = false;
txActiveBufferedCtx_ = nullptr;
refreshBlindDecoderMuteState();
carrierStopPending = true;
}
return;
}
port->BSRR = txBsrrWords_[txBsrrReadIdx_];
txBsrrReadIdx_++;
txBsrrTicksSent_++;
if (txBsrrTicksSent_ >= txBsrrTotalTicks_)
IrTxIsrBufferedStorageBase* buf = txActiveBufferedCtx_;
if (buf == nullptr || !buf->isValid())
{
port->BSRR = ((uint32_t)mask) << 16U;
isSending = false;
txUseBufferedIsr_ = false;
txActiveBufferedCtx_ = nullptr;
refreshBlindDecoderMuteState();
carrierStopPending = true;
return;
}
if (txBsrrReadIdx_ == txBsrrHalfLen_)
port->BSRR = buf->bsrrWords[buf->readIdx];
buf->readIdx++;
buf->ticksSent++;
if (buf->ticksSent >= buf->totalTicks)
{
txBsrrWave_.fill(&txBsrrWords_[0], txBsrrHalfLen_);
port->BSRR = ((uint32_t)mask) << 16U;
isSending = false;
txUseBufferedIsr_ = false;
txActiveBufferedCtx_ = nullptr;
refreshBlindDecoderMuteState();
carrierStopPending = true;
return;
}
else if (txBsrrReadIdx_ >= irproto::kIsrTxBsrrWordCount)
if (buf->readIdx == buf->halfLen)
{
txBsrrReadIdx_ = 0;
txBsrrWave_.fill(&txBsrrWords_[txBsrrHalfLen_], txBsrrHalfLen_);
buf->wave.fill(&buf->bsrrWords[0], buf->halfLen);
}
else if (buf->readIdx >= buf->wordCount)
{
buf->readIdx = 0;
buf->wave.fill(&buf->bsrrWords[buf->halfLen], buf->halfLen);
}
}
@ -926,26 +1194,8 @@ uint8_t IR_Encoder::bitLow[2] = {
uint32_t IR_Encoder::calculateSendTime(uint8_t packSize) const
{
// Расчет времени отправки пакета в миллисекундах
// Время преамбулы: preambPulse * 2 фронта * bitTakts тактов
uint32_t preambTime = preambPulse * 2 * bitTakts;
// Время данных: количество бит * bitTakts тактов
uint32_t dataTime = packSize * 8 * bitTakts;
// Время синхронизации: syncBits * 2 фронта * bitTakts тактов
uint32_t syncTime = syncBits * 2 * bitTakts;
// Общее время в тактах
uint32_t totalTakts = preambTime + dataTime + syncTime;
// Конвертируем в миллисекунды
// carrierPeriod - период несущей в микросекундах
// totalTakts * carrierPeriod / 1000 = время в миллисекундах
uint32_t sendTimeMs = (totalTakts * carrierPeriod) / 1000;
return sendTimeMs;
// Полное время кадра в эфире по формуле FSM (преамбула + байты с синхробитами), округление вверх до мс.
return (irFrameAirtimeUs(packSize) + 999U) / 1000U;
}
// Функции для тестирования времени отправки без фактической отправки
@ -1000,12 +1250,12 @@ uint32_t IR_Encoder::testSendBackTo(uint16_t addrTo, uint8_t *data, uint8_t len)
uint32_t IR_Encoder::testSendBack(bool isAdressed, uint16_t addrTo, uint8_t *data, uint8_t len) const
{
if (len > bytePerPack)
if (((uint16_t)msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes) > IR_MASK_MSG_INFO)
{
return 0; // Возвращаем 0 для недопустимого размера
}
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + min(uint8_t(1), len) + crcBytes;
uint8_t packSize = msgBytes + addrBytes + (isAdressed ? addrBytes : 0) + len + crcBytes;
return calculateSendTime(packSize);
}

View File

@ -1,19 +1,41 @@
#pragma once
#include "IR_config.h"
#include "IrTxBsrrWave.h"
#include "IrTxGateTypes.h"
// TODO: Отложенная передача после завершения приема
enum class IR_SendStatus : uint8_t {
Success = 0,
PayloadTooLarge,
EncoderBusy,
BufferTooLarge,
ExternalBackendBusy,
ExternalStartFailed,
ExternalNoStream,
ExternalInvalidConfig,
BuildGateRunsFailed,
ScaleGateRunsFailed,
DmaStartFailed,
EncoderPinUnavailable,
BufferedStorageInvalid,
};
const char* irSendStatusToString(IR_SendStatus status);
// Структура для возврата результата отправки
struct IR_SendResult {
bool success; // Флаг успешности отправки
uint32_t sendTimeMs; // Время отправки пакета в миллисекундах
IR_SendStatus status; // Детализированный статус старта передачи
IR_SendResult(bool success = false, uint32_t sendTimeMs = 0)
: success(success), sendTimeMs(sendTimeMs) {}
IR_SendResult(bool success = false,
uint32_t sendTimeMs = 0,
IR_SendStatus status = IR_SendStatus::ExternalStartFailed)
: success(success), sendTimeMs(sendTimeMs), status(status) {}
};
class IR_DecoderRaw;
class IrTxIsrBufferedStorageBase;
class IR_Encoder : public IR_FOX
{
friend IR_DecoderRaw;
@ -24,9 +46,13 @@ public:
static HardwareTimer* IR_Timer;
using IR_TxGateRun = IrTxGateRun;
enum class TxIsrMode : uint8_t {
Legacy = 0,
Buffered = 1
};
using ExternalTxBusyFn = bool (*)(void *ctx);
using ExternalTxStartFn = bool (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
using ExternalTxStartFn = IR_SendStatus (*)(void *ctx, IR_Encoder *enc, const uint8_t *packet, uint8_t len);
private:
// uint16_t id; /// @brief Адрес передатчика
public:
@ -36,6 +62,10 @@ public:
/// @param decPair Если задан, конструктор регистрирует этот один приёмник как blind-decoder
/// (аналог setBlindDecoders() для одного RX).
IR_Encoder(uint8_t pin, uint16_t addr = 0, IR_DecoderRaw *decPair = nullptr, bool autoHandle = true);
/// Публичная оценка airtime кадра (мс) по его полной длине в байтах (packSize). Чистая функция
/// протокольных констант — подходит и для приёма (напр. бюджет удержания заднего по объявленному
/// в 1-м байте размеру принимаемого ответа). БЕЗ +30% компенсации занижения sync — добавляет потребитель.
uint32_t packAirtimeMs(uint8_t packSize) const { return calculateSendTime(packSize); }
static void isr();
static void begin(HardwareTimer* timer, uint8_t channel, IRQn_Type IRQn, uint8_t priority, void(*isrCallback)() = nullptr);
/**
@ -57,7 +87,7 @@ public:
/** p∈[0,100] → ближайший допустимый числитель; 100% даёт N = maxPowerNumerator(). */
void setPowerPercent(uint8_t p);
/** После buildGateRuns: lenTicks в тактах 2×Fc → физические тики (carrierFrec×multiply). Может разбить сегменты. */
/** Legacy helper: 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. */
@ -67,12 +97,21 @@ public:
static void tick();
/**
* Режим внутреннего TX без DMA: false — BSRR + кольцо (buildGateRuns + scaleGateRunsToPhysical);
* Режим внутреннего TX без DMA: false — BSRR + кольцо (direct physical gate-runs builder);
* true — FSM «налету» + скважность несущей как у буферного пути (подшаги multiply/2 на шаг FSM).
* Выставить до begin/rawSend (глобально на все IR_Encoder). Игнорируется при externalTxStartFn.
* По умолчанию включён legacy=true для обратной совместимости. Вызов меняет default и обновляет
* все зарегистрированные encoder-объекты. Buffered ISR реально используется только если у encoder
* привязан storage через attachBufferedIsrStorage()/enableBufferedIsr().
* Выставить до begin/rawSend. Игнорируется при externalTxStartFn.
*/
static void setTxIsrLegacyMode(bool legacy);
static bool txIsrLegacyMode();
void attachBufferedIsrStorage(IrTxIsrBufferedStorageBase& storage);
void detachBufferedIsrStorage();
bool hasBufferedIsrStorage() const;
void enableBufferedIsr(IrTxIsrBufferedStorageBase& storage);
void disableBufferedIsr();
TxIsrMode txIsrMode() const;
/** Optional: register external TX backend (e.g. DMA driver). */
static void setExternalTxBackend(ExternalTxStartFn startFn, ExternalTxBusyFn busyFn, void *ctx);
@ -80,8 +119,10 @@ public:
/** Called by external TX backend on actual end of transmission. */
void externalFinishSend();
/** Build RLE runs of carrier gate for a packet (no HW access). */
/** Build RLE runs of carrier gate for a packet in logical 2×Fc ticks (no HW access). */
static size_t buildGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns);
/** Build RLE runs directly in physical carrierFrec×multiply ticks (DMA/buffered ISR path). */
static size_t buildPhysicalGateRuns(const uint8_t *packet, uint8_t len, IR_TxGateRun *outRuns, size_t maxRuns, uint16_t multiply);
void enable();
void disable();
@ -94,7 +135,7 @@ public:
"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_SendStatus rawSend(uint8_t *ptr, uint8_t len);
IR_SendResult sendData(uint16_t addrTo, uint8_t dataByte, bool needAccept = false);
IR_SendResult sendData(uint16_t addrTo, uint8_t *data = nullptr, uint8_t len = 0, bool needAccept = false);
@ -173,16 +214,11 @@ private:
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);
template <typename Emit>
static bool txWalkRuns(TxFsmState &st, const uint8_t *sendBufferLocal, Emit emit);
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;
bool shouldUseBufferedIsr() const;
/** Снимок на старт TX (буферный и legacy путь). */
uint16_t txPowerSnap_ = 1;
@ -194,6 +230,10 @@ private:
uint16_t legacySlotInPeriod_ = 0;
volatile uint16_t powerNumerator_ = 1;
IrTxIsrBufferedStorageBase* txBufferedCtx_ = nullptr;
IrTxIsrBufferedStorageBase* txActiveBufferedCtx_ = nullptr;
TxIsrMode txIsrMode_ = TxIsrMode::Legacy;
bool txUseBufferedIsr_ = false;
IR_DecoderRaw *decPair = nullptr;
IR_DecoderRaw *singleBlindDecoder = nullptr;

View File

@ -6,6 +6,13 @@
/** Число потоков DMA-TX задаётся шаблоном: IrDmaTxStm32<2>, см. IrDmaTxStm32.h и irproto::kDefaultDmaTxMaxStreams. */
namespace irproto {
constexpr size_t kDefaultDmaTxMaxStreams = 4U;
/** NVIC-приоритет IRQ DMA-каналов TX (refill кольца BSRR-слов). Refill длинный (полубуфер
* 2048 слов = миллисекунды на небыстрых SYSCLK), а его дедлайн мягкий (~9 мс при 38кГц×6),
* поэтому он обязан сидеть НИЖЕ SysTick (0) и EXTI приёмника (6): с приоритетом по умолчанию
* (0, HAL_NVIC_EnableIRQ без SetPriority) он не вытеснялся SysTick'ом, тики слипались и
* millis() замедлялся в 1.6×/2.2× (один/два потока) на всё время передачи — мерено на стенде
* TofSickHunt 2026-08-11. Меньше число = выше приоритет; 8 = ниже EXTI, выше самого дна. */
constexpr uint32_t kDmaTxIrqPriority = 8U;
/** Кольцевой буфер BSRR-слов для ISR-TX (как у DMA: два полублока). Чётное число. */
constexpr uint16_t kIsrTxBsrrWordCount = 256U;
/** Максимум RLE-сегментов для buildGateRuns при ISR-TX. */
@ -21,7 +28,7 @@ static_assert((kIsrTxBsrrWordCount & 1U) == 0U, "kIsrTxBsrrWordCount must be eve
// Краткий лог причин, почему физический сигнал не дошёл до распознанного пакета.
// Формат и коды: ref/IR_RX_BRIEF_LOG.md
#ifndef IR_RX_BRIEF_LOG
#define IR_RX_BRIEF_LOG 1
#define IR_RX_BRIEF_LOG 0
#endif
// 1: печатать только отклонённые/ошибочные события; успехи и шумовые PREAMB скрыть.
#ifndef IR_RX_BRIEF_LOG_REJECT_ONLY
@ -179,6 +186,13 @@ typedef uint16_t crc_t;
#ifndef IR_INPUT_MIN_PULSE_US
#define IR_INPUT_MIN_PULSE_US 0
#endif
/** Сколько сырых фронтов забирать из ISR-буфера за один tick() (батч-выемка).
* 1 фронт/tick привязывал RX-пропускную способность к частоте loop: медленный
* loop (телеметрия/дисплей) переполнял буфер 250 фронтов (~120мс эфира) и
* кадры терялись молча. Батч развязывает приём от темпа loop. */
#ifndef IR_RX_TICK_BATCH
#define IR_RX_TICK_BATCH 16U
#endif
/** Сколько подтверждённых фронтов держать перед выпуском в декодер (потоковая задержка). */
#ifndef IR_INPUT_FILTER_HOLDBACK_EDGES
#define IR_INPUT_FILTER_HOLDBACK_EDGES 3U
@ -217,9 +231,11 @@ typedef uint16_t crc_t;
#ifndef IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT
#define IR_PREAMBLE_PERIOD_MAX_FACTOR_PCT 340U
#endif
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. */
/** Таймаут окна кандидата преамбулы: IR_timeout * mult. Кандидат без фронтов дольше таймаута байта
преамбулой быть не может; при 3× линия считалась занятой (rxLineActive) ещё 45 мс после последнего
паразитного фронта (напр. засветка своим дальномером) и откладывала передачу. */
#ifndef IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 3U
#define IR_PREAMBLE_CANDIDATE_TIMEOUT_MULT 1U
#endif
#define preambPulse 3
@ -250,6 +266,32 @@ typedef uint16_t crc_t;
#define bitTime (bitTakts * carrierPeriod) // Общая длительность бита
#define tolerance 300U
// ---- Длительности и размеры кадра ФОРМУЛАМИ из FSM передатчика (IR_Encoder::txAdvanceBoundary) ----
// Логический такт TX = полпериода несущей (toggleCounter считает полупериоды). Преамбула = 6 ран по
// (preambToggle+1) тактов; лок декодера — на 3-м RISE (конец 5-й раны); байт = (8 данных + 3 sync) бит по 74 такта.
constexpr uint32_t irTxTickNs = 1000000000UL / (carrierFrec * 2U);
constexpr uint32_t irPreambleTicks = (uint32_t)preambPulse * 2U * ((uint32_t)preambToggle + 1U);
constexpr uint32_t irLockTicks = ((uint32_t)preambPulse * 2U - 1U) * ((uint32_t)preambToggle + 1U);
constexpr uint32_t irBitTicks = (uint32_t)bitTakts * 2U;
constexpr uint32_t irByteTicks = ((uint32_t)bitPerByte + (uint32_t)syncBits) * irBitTicks;
constexpr uint32_t irTicksToUs(uint32_t ticks) { return (uint32_t)(((uint64_t)ticks * irTxTickNs + 500U) / 1000U); }
/// Полное время кадра в эфире (от первой несущей до последнего sync-бита), мкс.
constexpr uint32_t irFrameAirtimeUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks); }
/// От старта кадра до последнего БИТА ДАННЫХ (момент, когда декодер отдаёт кадр), мкс.
constexpr uint32_t irFrameDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
/// От лока декодера (3-й RISE преамбулы) до последнего бита данных, мкс.
constexpr uint32_t irLockToDecodeEndUs(uint8_t packSize) { return irTicksToUs(irPreambleTicks - irLockTicks + (uint32_t)packSize * irByteTicks - (uint32_t)syncBits * irBitTicks); }
/// Латентность лока: от первой несущей чужого кадра до лока декодера, мкс.
constexpr uint32_t irLockLatencyUs = irTicksToUs(irLockTicks);
/// Таймаут байта декодера (как IR_timeout при номинальном bitTime) и тишина, по которой декодер обрывает приём.
constexpr uint32_t irRxByteTimeoutUs = ((uint32_t)bitTime + tolerance) * ((uint32_t)bitPerByte + syncBits + 1U);
constexpr uint32_t irRxAbortSilenceUs = 2U * irRxByteTimeoutUs;
/// Протокольный максимум длины кадра (5-битное поле длины).
constexpr uint8_t irMaxPackSize = IR_MASK_MSG_INFO;
/// Размер кадра по полезной нагрузке: DATA (from+to) и BACK (только from).
constexpr uint8_t irDataPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes * 2 + payload + crcBytes); }
constexpr uint8_t irBackPackSize(uint8_t payload) { return (uint8_t)(msgBytes + addrBytes + payload + crcBytes); }
constexpr uint16_t test_all_Time = bitTime;
constexpr uint16_t test_all_Takts = bitTakts * 2;
constexpr uint16_t test_hi = ((bitPauseTakts) * 2 - 0) + ((bitActiveTakts) * 2 - 0);

View File

@ -5,6 +5,14 @@
#if defined(ARDUINO_ARCH_STM32) && defined(STM32G4xx)
#if defined(_MSC_VER)
#define IRPROTO_DMA_PRAGMA_MESSAGE(text) __pragma(message(text))
#else
#define IRPROTO_DMA_PRAGMA_MESSAGE(text) _Pragma(#text)
#endif
IRPROTO_DMA_PRAGMA_MESSAGE(message("[IR-protocol] TX path available: built-in DMA"))
#include <Arduino.h>
#include <HardwareTimer.h>
@ -77,9 +85,12 @@ public:
}
s_instance = this;
activeCount_ = 0;
for (uint8_t i = 0; i < streamCount_; i++) {
// Приоритет ЯВНО и ДО включения: без SetPriority канал остаётся на 0 (уровень
// SysTick), длинный refill не вытесняется и глотает тики millis. Обоснование
// и измерения — у irproto::kDmaTxIrqPriority (IR_config.h).
HAL_NVIC_SetPriority(streams_[i].dmaIrq, irproto::kDmaTxIrqPriority, 0);
HAL_NVIC_EnableIRQ(streams_[i].dmaIrq);
}
@ -90,22 +101,29 @@ public:
return s_instance;
}
// Fix E (контракт busy): "занят" = АКТИВЕН ХОТЯ БЫ ОДИН поток (OR).
// Раньше было AND (все потоки сразу) — почти всегда false, не отражало занятости и было
// избыточно (per-stream s.active в startStream + per-encoder isSending уже не дают повторный
// старт того же потока). OR нужен, чтобы irAnyTxBusy()/applyCarrierMultiply реально дожидались
// простоя ВСЕХ эмиттеров перед ретюном несущей.
// ВНИМАНИЕ: это НЕ сериализует независимые front/rear — путь отправки гейтится per-stream;
// внешний busy()-гейт в IR_Encoder::rawSend намеренно нейтрализован в прошивке (txBusy()=false).
// Чтобы ВКЛЮЧИТЬ сериализацию front/rear — вернуть txBusy()=busy() в Car/src/IR/IR.cpp.
bool busy() const {
if (streamCount_ == 0) return false;
for (uint8_t i = 0; i < streamCount_; i++) {
if (!streams_[i].active) return false;
if (streams_[i].active) return true;
}
return true;
return false;
}
bool start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) {
if (enc == nullptr) return false;
IR_SendStatus start(IR_Encoder* enc, const uint8_t* packet, uint8_t len) {
if (enc == nullptr) return IR_SendStatus::ExternalNoStream;
for (uint8_t i = 0; i < streamCount_; i++) {
if (streams_[i].enc == enc) {
return startStream(streams_[i], packet, len);
}
}
return false;
return IR_SendStatus::ExternalNoStream;
}
void irqForStream(size_t streamIndex) {
@ -118,6 +136,37 @@ public:
return &streams_[streamIndex].hdma;
}
// Fix D: не-IRQ страховка завершения. Вызывать из главного цикла (IR_Module::tick()).
// Штатно isSending сбрасывает только DMA HT/CPLT IRQ; если он не сработает (пропуск/слипание
// флагов, Transfer-Error, последствие любой гонки) — поток «залипает» навсегда. Здесь ловим
// застой прогресса ticksOutput по стенным часам и форсим stopStream как аварийное
// восстановление. Это recovery (не prevention): фикс C убирает саму гонку, а poll() страхует
// структурный SPOF завершения и делает редкий баг наблюдаемым (recoveryCount()).
void poll() {
const uint32_t now = millis();
for (uint8_t i = 0; i < streamCount_; i++) {
TxStream& s = streams_[i];
if (!s.active) continue; // atomic: bool
const uint32_t t = s.ticksOutput; // atomic: выровненный u32 на Cortex-M
if (t != s.lastTicks) {
s.lastTicks = t;
s.lastProgressMs = now;
continue;
}
if ((uint32_t)(now - s.lastProgressMs) >= stallTimeoutMs_) {
forceStop(s);
}
}
}
/** Порог застоя для watchdog (мс). По умолчанию 100; должен быть заметно больше времени
вывода одной половины DMA-буфера на самой медленной несущей. 0 игнорируется. */
void setStallTimeoutMs(uint32_t ms) { if (ms != 0U) stallTimeoutMs_ = ms; }
/** Сколько раз watchdog аварийно восстановил залипший поток (наблюдаемость/логи). */
uint32_t recoveryCount() const { return recoveries_; }
/** Сколько раз DMA-передача завершилась по Transfer-Error (наблюдаемость/логи). */
uint32_t errorCount() const { return errors_; }
private:
struct TxStream {
DMA_HandleTypeDef hdma{};
@ -144,7 +193,11 @@ private:
uint32_t totalTicks = 0;
volatile uint32_t ticksOutput = 0;
bool active = false;
// Fix D (watchdog): прогресс ticksOutput против стенных часов (контекст потока).
uint32_t lastTicks = 0;
uint32_t lastProgressMs = 0;
volatile bool active = false; // Fix: делится поток<->ISR (busy()/poll()/колбэки/спин applyCarrierMultiply) — запрет кэширования компилятором
void resetWave() {
wave.configure(setWord, resetWord, nullptr, 0, 2, 1);
@ -160,11 +213,13 @@ private:
void onHalf() {
ticksOutput += halfLen;
fill(&dmaBuf[0], halfLen);
__DSB(); // Fix #8: refill первой половины виден DMA до следующего прохода кольца
}
void onComplete() {
ticksOutput += halfLen;
fill(&dmaBuf[halfLen], halfLen);
__DSB(); // Fix #8: refill второй половины виден DMA до следующего прохода кольца
}
void onError() {}
@ -178,29 +233,39 @@ private:
TxStream streams_[MaxStreams]{};
uint8_t streamCount_ = 0;
volatile uint8_t activeCount_ = 0;
// Fix C: разделяемый activeCount_ удалён — он и был источником гонки поток↔ISR.
uint32_t stallTimeoutMs_ = 100; // Fix D
uint32_t recoveries_ = 0; // Fix D (только контекст потока)
volatile uint32_t errors_ = 0; // Fix #5: счётчик Transfer-Error (пишется в ISR dmaErrorCb, читается из потока)
static uint32_t u32ptr(const volatile void* p) {
return (uint32_t)(uintptr_t)p;
}
void startTimerIfNeeded() {
// Fix C: TIM держим free-running. Включаем ИДЕМПОТЕНТНО при старте потока и НИКОГДА не
// останавливаем по разделяемому счётчику (счётчик убран → гонка поток↔ISR устранена).
// Счётчик TIM НЕ сбрасываем: сброс глитчил бы уже активный поток; для ИК ≤1 период несущей
// фазового сдвига пренебрежим. В простое (ни один DMA-канал не заармлен) update-события TIM
// генерят DMA-запросы «в никуда» — безвредно, в GPIO ничего не пишется, светодиод не дёргается.
void ensureTimerRunning() {
if (htim_ == nullptr) return;
if (activeCount_ != 1) return;
__HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE);
__HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE);
__HAL_TIM_SET_COUNTER(htim_, 0);
__HAL_TIM_ENABLE_DMA(htim_, TIM_DMA_UPDATE);
if ((htim_->Instance->CR1 & TIM_CR1_CEN) == 0U) {
__HAL_TIM_CLEAR_FLAG(htim_, TIM_FLAG_UPDATE);
HAL_TIM_Base_Start(htim_);
}
}
void stopTimerIfIdle() {
if (htim_ == nullptr) return;
if (activeCount_ != 0) return;
__HAL_TIM_DISABLE_DMA(htim_, TIM_DMA_UPDATE);
HAL_TIM_Base_Stop(htim_);
// Fix D: аварийный стоп залипшего потока из контекста потока (не ISR). Маскируем ИМЕННО IRQ
// этого DMA-канала, чтобы штатный HT/CPLT/Error-обработчик не вытеснил нас посреди stopStream
// (иначе двойной abort/externalFinishSend). Другие потоки и прочие IRQ не трогаем.
void forceStop(TxStream& s) {
HAL_NVIC_DisableIRQ(s.dmaIrq);
if (s.active) {
stopStream(s);
recoveries_++;
}
HAL_NVIC_EnableIRQ(s.dmaIrq);
}
static TxStream* streamFromDma(DMA_HandleTypeDef* hdma) {
@ -233,9 +298,10 @@ private:
static void dmaErrorCb(DMA_HandleTypeDef* hdma) {
auto* s = streamFromDma(hdma);
if (s == nullptr) return;
if (s == nullptr || !s->active) return; // симметрично HT/CPLT: одиночный stop, без двойного finish
s->onError();
if (s_instance != nullptr) {
s_instance->errors_++; // Fix #5: наблюдаемость аварийных завершений по Transfer-Error
s_instance->stopStream(*s);
}
}
@ -282,28 +348,22 @@ private:
return true;
}
bool startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return false;
if (s.active) return false;
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return false;
if (s.runs == nullptr || s.maxRuns == 0) return false;
IR_SendStatus startStream(TxStream& s, const uint8_t* packet, uint8_t len) {
if (s.enc == nullptr || s.port == nullptr || s.mask == 0) return IR_SendStatus::ExternalInvalidConfig;
if (s.active) return IR_SendStatus::EncoderBusy;
if (s.dmaBuf == nullptr || s.bufLen < 2 || s.halfLen == 0) return IR_SendStatus::ExternalInvalidConfig;
if (s.runs == nullptr || s.maxRuns == 0) return IR_SendStatus::ExternalInvalidConfig;
s.resetWave();
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;
const uint16_t mult = IR_Encoder::carrierMultiply();
s.runCount = IR_Encoder::buildPhysicalGateRuns(packet, len, s.runs, s.maxRuns, mult);
if (s.runCount == 0) return IR_SendStatus::BuildGateRunsFailed;
uint32_t total = 0;
for (size_t i = 0; i < s.runCount; i++) total += s.runs[i].lenTicks;
s.totalTicks = total;
const uint16_t mult = IR_Encoder::carrierMultiply();
uint16_t pwr = mult / 2U;
if (s.enc != nullptr) {
const uint16_t want = s.enc->powerNumerator();
@ -317,15 +377,22 @@ private:
s.port->BSRR = s.resetWord;
// Fix C/D: помечаем active и инициализируем watchdog ДО арминга DMA — иначе ранний
// HT/CPLT IRQ при active==false выйдет из callback'а и пропустит refill → недосчёт
// ticksOutput → латч. TIM уже (или станет) free-running, старт по счётчику не нужен.
s.lastTicks = 0;
s.lastProgressMs = millis();
s.active = true;
__DSB(); // Fix #8: завершить запись dmaBuf (fill) и BSRR до арминга DMA
const uint32_t dst = u32ptr(&s.port->BSRR);
if (HAL_DMA_Start_IT(&s.hdma, (uint32_t)(uintptr_t)s.dmaBuf, dst, s.bufLen) != HAL_OK) {
return false;
s.active = false;
return IR_SendStatus::DmaStartFailed;
}
s.active = true;
activeCount_++;
startTimerIfNeeded();
return true;
ensureTimerRunning();
return IR_SendStatus::Success;
}
void stopStream(TxStream& s) {
@ -341,9 +408,7 @@ private:
if (s.enc != nullptr) {
s.enc->externalFinishSend();
}
if (activeCount_ > 0) activeCount_--;
stopTimerIfIdle();
// Fix C: TIM НЕ останавливаем — он free-running, без разделяемого счётчика.
}
};

View File

@ -65,13 +65,63 @@ public:
return out;
}
// Заполнение по ранам, а не по словам: тот же поток слов, что даёт nextWord() (состояние
// runIndex_/ticksLeftInRun_/slotInPeriod_ переносится через границы порций), но пауза (gate=0)
// пишется одним циклом записи, а несущая — копией готового шаблона периода. На 12 МГц это
// ~1 мс на 4096 слов вместо ~16 (пословный автомат) — и в предзаполнении перед стартом DMA,
// и в ISR-дозаполнении половин буфера во время передачи.
IR_TX_BSRR_WAVE_HOT void fill(uint32_t* dst, uint16_t count) {
if (dst == nullptr || count == 0) {
return;
}
do {
*dst++ = nextWord();
} while (--count != 0);
while (count != 0) {
if (runIndex_ >= runCount) {
do { *dst++ = resetWord; } while (--count != 0);
return;
}
const bool gate = runs[runIndex_].gate;
uint16_t n = ticksLeftInRun_; // слов до конца текущего рана
if (n == 0) n = 1; // ран нулевой длины: nextWord() выдаёт одно слово и переходит дальше
if (n > count) n = count;
if (!gate) {
slotInPeriod_ = 0;
uint16_t k = n;
do { *dst++ = resetWord; } while (--k != 0);
} else {
uint16_t k = n;
// добить текущий период до слота 0 (если ран начался посреди периода на границе порции)
while (k != 0 && slotInPeriod_ != 0) {
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
k--;
}
// целые периоды: powerN_ слов setWord, остальные resetWord
while (k >= multiply_) {
uint16_t i = 0;
for (; i < powerN_; ++i) *dst++ = setWord;
for (; i < multiply_; ++i) *dst++ = resetWord;
k = (uint16_t)(k - multiply_);
}
// хвост неполного периода
while (k != 0) {
*dst++ = (slotInPeriod_ < powerN_) ? setWord : resetWord;
if (++slotInPeriod_ >= multiply_) slotInPeriod_ = 0;
k--;
}
}
count = (uint16_t)(count - n);
if (ticksLeftInRun_ > n) {
ticksLeftInRun_ = (uint16_t)(ticksLeftInRun_ - n);
} else {
ticksLeftInRun_ = 0;
}
if (ticksLeftInRun_ == 0) {
runIndex_++;
if (runIndex_ < runCount) {
ticksLeftInRun_ = runs[runIndex_].lenTicks;
}
}
}
}
private:

View File

@ -4,8 +4,8 @@
/**
* Один RLE-сегмент огибающей несущей.
* В buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec (как раньше).
* После IR_Encoder::scaleGateRunsToPhysical — в физических тиках carrierFrec×multiply.
* В legacy buildGateRuns: lenTicks в тактах логической шкалы 2×carrierFrec.
* В современном DMA/buffered ISR пути buildPhysicalGateRuns строит lenTicks сразу в физических тиках carrierFrec×multiply.
*/
struct IrTxGateRun {
uint16_t lenTicks;

64
IrTxIsrBufferedStorage.h Normal file
View File

@ -0,0 +1,64 @@
#pragma once
#include "IR_config.h"
#include "IrTxBsrrWave.h"
class IrTxIsrBufferedStorageBase {
public:
IrTxGateRun* gateRuns = nullptr;
size_t maxGateRuns = 0;
uint32_t* bsrrWords = nullptr;
uint16_t wordCount = 0;
IrTxBsrrWave wave{};
uint16_t readIdx = 0;
uint16_t halfLen = 0;
uint32_t totalTicks = 0;
uint32_t ticksSent = 0;
bool isValid() const {
return gateRuns != nullptr &&
maxGateRuns != 0U &&
bsrrWords != nullptr &&
wordCount >= 2U &&
(wordCount & 1U) == 0U;
}
void resetRuntimeState() {
readIdx = 0;
halfLen = static_cast<uint16_t>(wordCount / 2U);
totalTicks = 0;
ticksSent = 0;
}
};
class IrTxIsrBufferedStorageView : public IrTxIsrBufferedStorageBase {
public:
IrTxIsrBufferedStorageView(IrTxGateRun* runs, size_t runCount, uint32_t* words, uint16_t wordsCount) {
gateRuns = runs;
maxGateRuns = runCount;
bsrrWords = words;
wordCount = wordsCount;
resetRuntimeState();
}
};
template<size_t MaxGateRuns = irproto::kIsrTxMaxGateRuns, uint16_t WordCount = irproto::kIsrTxBsrrWordCount>
class IrTxIsrBufferedStorage : public IrTxIsrBufferedStorageBase {
static_assert(MaxGateRuns > 0U, "IrTxIsrBufferedStorage: MaxGateRuns > 0");
static_assert(WordCount >= 2U, "IrTxIsrBufferedStorage: WordCount >= 2");
static_assert((WordCount & 1U) == 0U, "IrTxIsrBufferedStorage: WordCount must be even");
public:
IrTxIsrBufferedStorage() {
gateRuns = gateRunsStorage_;
maxGateRuns = MaxGateRuns;
bsrrWords = bsrrWordsStorage_;
wordCount = WordCount;
resetRuntimeState();
}
private:
IrTxGateRun gateRunsStorage_[MaxGateRuns]{};
uint32_t bsrrWordsStorage_[WordCount]{};
};

View File

@ -2,12 +2,49 @@
namespace PacketTypes
{
bool BasePack::checkAddress() { return true; };
void BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id)
uint8_t minimumPacketSize(uint8_t msgType)
{
switch (msgType)
{
case IR_MSG_DATA_ACCEPT:
case IR_MSG_DATA_NOACCEPT:
case IR_MSG_BACK_TO:
case IR_MSG_REQUEST:
return uint8_t(msgBytes + addrBytes + addrBytes + crcBytes);
case IR_MSG_BACK:
return uint8_t(msgBytes + addrBytes + crcBytes);
case IR_MSG_ACCEPT:
return uint8_t(msgBytes + addrBytes + 1U + crcBytes);
default:
return 0;
}
}
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize)
{
const uint8_t minimum = minimumPacketSize(msgType);
return minimum != 0 && packSize >= minimum;
}
bool BasePack::checkAddress() { return true; }
bool BasePack::set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize)
{
isAvailable = false;
isRawAvailable = false;
this->packInfo = packInfo;
this->id = id;
if (packInfo == nullptr || packInfo->buffer == nullptr)
{
return false;
}
const uint8_t msgType = (packInfo->buffer[msgOffset] >> 5) & IR_MASK_MSG_TYPE;
if (requireTypedSize && !isTypedPacketSizeValid(msgType, packInfo->packSize))
{
return false;
}
if (checkAddress())
{
isAvailable = true;
@ -23,29 +60,65 @@ namespace PacketTypes
Serial.print(" NOT-OK ");
#endif
}
return isAvailable;
}
uint16_t BasePack::_getAddrFrom(BasePack *obj)
{
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
uint16_t(obj->addressFromOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
{
return 0;
}
return (obj->packInfo->buffer[obj->addressFromOffset] << 8) | obj->packInfo->buffer[obj->addressFromOffset + 1];
};
}
uint16_t BasePack::_getAddrTo(BasePack *obj)
{
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes ||
uint16_t(obj->addressToOffset) + 1U >= uint16_t(obj->packInfo->packSize - crcBytes))
{
return 0;
}
return (obj->packInfo->buffer[obj->addressToOffset] << 8) | obj->packInfo->buffer[obj->addressToOffset + 1];
};
}
uint8_t BasePack::_getDataSize(BasePack *obj)
{
return obj->packInfo->packSize - crcBytes - obj->DataOffset;
};
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr)
{
return 0;
}
const uint16_t frameOverhead = uint16_t(crcBytes) + uint16_t(obj->DataOffset);
if (uint16_t(obj->packInfo->packSize) <= frameOverhead)
{
return 0;
}
return uint8_t(uint16_t(obj->packInfo->packSize) - frameOverhead);
}
uint8_t *BasePack::_getDataPrt(BasePack *obj)
{
if (obj == nullptr || obj->packInfo == nullptr || obj->packInfo->buffer == nullptr ||
obj->packInfo->packSize < crcBytes)
{
return nullptr;
}
const uint16_t dataEnd = uint16_t(obj->packInfo->packSize) - uint16_t(crcBytes);
if (uint16_t(obj->DataOffset) > dataEnd)
{
return nullptr;
}
return obj->packInfo->buffer + obj->DataOffset;
};
}
uint8_t BasePack::_getDataRawSize(BasePack *obj)
{
if (obj == nullptr || obj->packInfo == nullptr)
{
return 0;
}
return obj->packInfo->packSize;
};
}
bool BasePack::available()
{
@ -59,7 +132,7 @@ namespace PacketTypes
{
return false;
}
};
}
bool BasePack::availableRaw()
{
if (isRawAvailable)
@ -71,7 +144,7 @@ namespace PacketTypes
{
return false;
}
};
}
bool Data::checkAddress()
{

View File

@ -4,25 +4,34 @@
class IR_Decoder;
namespace PacketTypes
{
/**
* Minimum complete frame size (header, addresses/data required by the type,
* and CRC). Unknown/reserved message types return 0.
*/
uint8_t minimumPacketSize(uint8_t msgType);
/** True only for a known typed packet whose complete frame is long enough. */
bool isTypedPacketSizeValid(uint8_t msgType, uint8_t packSize);
class BasePack
{
friend IR_Decoder;
protected:
bool isAvailable;
bool isRawAvailable;
bool isNeedAccept;
bool isAvailable = false;
bool isRawAvailable = false;
bool isNeedAccept = false;
uint8_t msgOffset;
uint8_t addressFromOffset;
uint8_t addressToOffset;
uint8_t DataOffset;
uint8_t msgOffset = 0;
uint8_t addressFromOffset = 0;
uint8_t addressToOffset = 0;
uint8_t DataOffset = 0;
IR_FOX::PackInfo *packInfo;
uint16_t id;
IR_FOX::PackInfo *packInfo = nullptr;
uint16_t id = 0;
virtual bool checkAddress();
void set(IR_FOX::PackInfo *packInfo, uint16_t id);
bool set(IR_FOX::PackInfo *packInfo, uint16_t id, bool requireTypedSize = true);
static uint16_t _getAddrFrom(BasePack *obj);
static uint16_t _getAddrTo(BasePack *obj);

View File

@ -36,6 +36,21 @@ public:
return value;
}
// B5: безопасный pop — копирует элемент под ОДНОЙ критсекцией и отдаёт по значению.
// (T* pop() отдаёт указатель во внутренний слот; его внутренний interrupts() снимает внешнюю
// защиту вызывающего ДО чтения *ptr → торн-рид, если кольцо переполнится в этом окне.)
bool pop(T &out) {
bool popped = false;
noInterrupts();
if (!isEmpty()) {
out = data[start];
start = (start + 1) % BufferSize;
popped = true;
}
interrupts();
return popped;
}
private:
T data[BufferSize];
unsigned int start, end;

View File

@ -0,0 +1,142 @@
# IR-protocol — глубокий анализ (RX-производительность, обработка ошибок, протокол, мины)
> Дата: 2026-07-01. Только чтение исходников. Цель: после переноса TX на DMA приём (RX) стал узким местом — разобрать RX-конвейер, обработку ошибок сигнала, дизайн протокола и найти баги/мины. Тайминги: `carrierFrec=38000`, `bitTakts=37` → `carrierPeriod=26µs`, `bitTime≈962µs` (комментарий про 1100 в `IR_DecoderRaw.h:107` устарел), `riseTime=962`, окно ±300µs, `IR_timeout≈15144µs`, `IR_ResponseDelay≈42мс`. Числа длительностей — оценки (замеров в коде нет).
## 0. Резюме (главное)
1. **Найдены реальные баги, триггеримые ШУМОМ (не только валидным трафиком):**
- **CRITICAL — OOB-чтение** в `crcCheck` при `packSize==1` (`IR_DecoderRaw.cpp:887`): `packSize - crcBytes` = `1-2``uint8_t len=255``crc8` читает `dataBuffer[0..254]` при массиве 38 → OOB ~217 байт + `dataBuffer[255/256]`. Мусорный CRC (редко — ложно-валидный кадр) или HardFault у границы SRAM. Нижняя граница `packSize` нигде не проверяется.
- **HIGH — реальный крэш на Car: null-deref** `encoder->sendAccept()` без проверки (`IR_Decoder.cpp:169`). У Car декодер создан с `encoder==nullptr` (`Car/src/IR/IR.cpp:31`). Приходит валидный кадр `IR_MSG_DATA_ACCEPT`, адресованный машинке (addrTo==id, addrFrom≠0 и <broadcast) через `acceptDelay` `sendAccept` по nullptr HardFault. Детерминированно, если кто-то шлёт машинке DATA_ACCEPT; плюс шумом (1/65536 на ложный кадр нужного типа).
- **HIGH OOB-запись** `dataBuffer[38]` (off-by-one) при `packSize==0` (`IR_DecoderRaw.cpp:753` `>` вместо `>=`): при `packSize==0` конец кадра не наступает, `i_dataBuffer` растёт до 304, запись `dataBuffer[304/8=38]` затирает соседнее поле `prevRise`.
- **MEDIUM неинициализированные** `isWaitingAcceptSend`/`acceptSendTimer`/… (`IR_Decoder.cpp:72-79`): мусор при старте возможен спонтанный `sendAccept` (и крэш по null-deref) без приёма чего-либо.
2. **RX-bottleneck — НЕ ISR и НЕ тяжёлый декод в прерывании** (декода в ISR нет, ISR лёгкий ~2-4µs). Узкое место структурное: **`tick()` вынимает РОВНО один сырой фронт за вызов** скорость слива кольца привязана к частоте `loop()`; при медленном loop 250-элементный буфер (~120мс запаса) переполняется и рвёт кадры. Плюс битовый CRC ×2 на конец кадра и маленький hold-фильтр.
3. **Коррекции ошибок по сути нет** detect-and-drop: 2×CRC8 (~16 бит), FEC/повтор отсутствует, однобитный bruteforce выключен зависает»). Нет seq/дедупа/ретраев; ACK задуман, но отправитель его игнорирует (мёртвый механизм). 5-битная длина молча обрезает payload >24 байт (send возвращает Success, кадр теряется).
---
## 1. RX-конвейер и производительность
### Стадии (кто где исполняется)
- **A. EXTI-фронт → ISR** (`IR_DecoderRaw::isr`, `.cpp:274-308`; `attachInterrupt(pin, CHANGE)` в `IR_Decoder::enable :88-89`). Приёмник демодулирующий (TSOP), EXTI по фронтам огибающей. В ISR: `micros()``noInterrupts/interrupts`), `dir = port->IDR&mask` (пост-фактум чтение уровня — при коротком импульсе возможна неверная dir), mute-гейт (`isPairSending` → фронт только в edgeTrace и return), `subBuffer.push(edge)`.
- **B. Сырое кольцо** `RingBuffer<FrontStorage,250>` (`.h:124`, `subBufferSize=250`, `IR_config.h:169`), ~8 байт/фронт ≈ 2КБ/декодер. `push/pop` под своей критсекцией. **`pop()` — один элемент за вызов.**
- **C. Фильтр импульсов** (`tick :417-444`) — по умолчанию **выключен** (`IR_INPUT_MIN_PULSE_US=0`), сырой фронт идёт прямо в декод. Включённый: hold-массив `[6]`, отсев пар <minUs как глитч.
- **D. FSM преамбулы** (`preambleProcessEdge :1599-1723`): IdleCandidateLocked, нужно 2 согласованных периода riserise в окне 220-340% bitTime после «тишины» >`IR_timeout*2`.
- **E. Декод бита** (`processDecodedFront :466-746`): инлайн-отсев глитчей, окна таймингов, реконструкция пропущенных бит через `ceil_div`.
- **F. Сборка/sync/CRC** (`writeToBuffer :748-949`): биты в `dataBuffer[38]`, sync 3 бита/байт, длина из первого байта, на конце — `crcCheck`.
- **G. Диспетчеризация** (`IR_Decoder::_tick :130-172`): разбор msgType → gotData/gotBack/gotAccept/gotRequest/gotRaw; авто-ACK для DATA_ACCEPT.
**Весь декод (CG) — в `tick()`/loop, не в ISR.** Это осознанная разгрузка прерывания.
### Узкое место
- ISR лёгкий (~2-4µs, оценка), даже при 5-6к фронтов/с ~1.5-2% CPU. Не он предел.
- **Структурный предел: 1 сырой pop/tick** (`.cpp:409-415`) → RX-throughput = частота `loop()`. Полный кадр ≈ 836 фронтов ≈ 836 tick'ов. Медленный loop (Serial-отладка, моторы, дисплей) < частоты фронтов кольцо копится overflow рваные кадры CRC/timeout reject.
- Буфер 250 = ~120мс запаса (250/2080 фр/с). Хватает от коротких стопоров, не от устойчивого отставания.
- Финальный CRC битовый `crc8` дважды по всей длине (~5-10µs, оценка). `BRUTEFORCE_CHECK` (выкл, «зависает») при включении O(len²·64), отдельная мина.
### Переполнение/backpressure
- `subBuffer` полон `push`=false, `isSubBufferOverflow=true` (транзиентный, сбрасывается в tick), счётчик `rxBriefRawOverflowDrops` (u16, насыщение), лог `QRAW`.
- hold-фильтр(6) переполнение `pulseFilterDropHoldOverflow++` (стойкий), лог `HOLD`.
- Битовый буфер кадра `isBufferOverflow`, лог `BUF`, сброс.
- `QFLT`/`pulseFilterDroppedByFilteredOverflow()` **мёртвый код**, всегда 0.
- **Стойкого счётчика потерянных ПАКЕТОВ нет** потери видны косвенно (CRC/TIMEOUT/SYNC в RX-brief-логе), последний битый кадр в `rejectBuffer`.
### RX во время своего TX (mute)
- `isPairSending` (число busy-энкодеров, пересчёт `refreshPairMuteState` под критсекцией) в ISR фронт отбрасывается на входе (`:293-299`). Глушится **любой** чужой кадр на всё время своей передачи (`isSending`). Для DMA-TX снятие mute зависит от `externalFinishSend()` задержка удлиняет «глухоту» на хвост.
- NVIC: библиотека приоритеты почти не трогает; RX-EXTI дефолт ядра, поднять `setReceiveExtiPreemptPriority` (`IR_Decoder.cpp:44-62`). **Нужно: preempt RX-EXTI срочнее DMA-TX**, иначе чужой DMA/IRQ сдвигает метку фронта искажение `risePeriod` (окно всего ±300µs).
### Идеи ускорения RX (trade-off)
1. **Таймер input-capture + DMA вместо софт-EXTI** аппаратный штамп фронтов, убирает per-edge ISR и джиттер метки. Дорого (канал таймера, wrap, восстановление dir), но чистейшие тайминги.
2. **Батч-pop сырых фронтов за tick** (с лимитом на итерацию) снимает привязку к частоте loop. Главный дешёвый выигрыш.
3. **subBuffer 250→512/1024** больше запаса (RAM: 10248КБ).
4. **Табличный CRC** вместо битового ×2 (~8× быстрее, +~512Б flash).
5. **Поднять preempt RX-EXTI выше DMA-TX** (механизм есть).
6. **Дешёвая метка** DWT->CYCCNT вместо `micros()`+критсекции в ISR.
---
## 2. Обработка ошибок сигнала и протокол
### CRC — это НЕ CRC16-CCITT, а два сцепленных CRC8
`crc8()` (`IR_config.cpp:17-33`) битовый MSB-first, init=0xFF. Полиномы `poly1=0x31`, `poly2=0x8C` (=битовое зеркало 0x31, но алгоритм тот же → фактически два разных CRC8). Сборка (`crcCheck :951-970`): `crc = crc8(0..len,poly1)<<8 | crc8(0..len+1,poly2)`. Асимметрия: байт2 покрывает данные+байт1, байт1 — только данные. Суммарно ~16 бит, вероятность пропуска ~1/65536, но гарантированной хэмминг-дистанции реального CRC16 нет. Мелочь `:959`: `dataBuffer[len] == (crc>>8) & 0xFF` парсится как `(==)&0xFF` — работает случайно (хрупко при смене `crc_t`).
### Фильтры/устойчивость
- Pulse-filter по длительности — **по умолчанию выкл** (`IR_INPUT_MIN_PULSE_US=0`).
- Инлайн-отсев глитчей в декодере — вкл: `IR_SHORT_LOW_GLITCH_REJECT`, `IR_MICRO_GAP_RISE_REJECT` (`:484-516`), «подтяжка фазы».
- Преамбула устойчива к случайному шуму (2 согласованных периода в узком окне). Sync 3 бита/байт — ловит расстройку сетки (до 3 ошибок → reject), но это не коррекция.
### Коррекции нет
- FEC отсутствует. Есть эвристическая реконструкция бит по времени (`:636-741`) — угадывание, финально валидируется CRC.
- `BRUTEFORCE_CHECK` (однобитная коррекция перебором) — **выключен, `//TODO: зависает`** (`IR_config.h:162`). При 16-битном контроле ещё и рискует ложной коррекцией.
- `rejectBuffer` — не коррекция, а «detect-and-expose» битого кадра приложению.
### Подтверждения (Accept) — механизм есть, но мёртвый
Приёмник на `IR_MSG_DATA_ACCEPT` планирует `sendAccept(from, crc8(данные))` через `acceptDelay` (`IR_Decoder::_tick :157-171`). **Отправитель Accept игнорирует** — логика ожидания/ретрая закомментирована (`IR_Encoder.cpp:736-741`, `rawSend:906 «TODO»`). Ретраев/дедупа/seq нет. Гарантий доставки нет.
### Форматы кадров
Первый байт `(msgType<<5)|(packSize&0x1F)`, `packSize` — полный размер кадра, длина 5 бит (0..31). Адреса big-endian.
| Тип | Класс | Поля | data offset | CRC |
|---|---|---|---|---|
| BACK 0 | DataBack | msg, addrFrom(1-2) | 3 | 2 |
| ACCEPT 1 | Accept | msg, addrFrom, customByte(3) | — | 2 |
| REQUEST 2 | Request | msg, addrFrom, addrTo | — | 2 |
| BACK_TO 4 | DataBack | msg, addrFrom, addrTo | 5 | 2 |
| DATA_NOACCEPT 6 | Data | msg, addrFrom, addrTo | 5 | 2 |
| DATA_ACCEPT 7 | Data (нужен Accept) | как 6 | 5 | 2 |
Адресация (`IR_config`): Broadcast=65000+; id==0 = promiscuous (принимает всё); машинки 1..31999, КТ 32000..63999, пульты 64000..64999.
### Что теряется/портится молча
1. **Переполнение 5-битной длины (серьёзно):** накладные Data = 7 байт → payload ≤24, но send разрешает `len` до `bytePerPack=31` (`IR_Encoder.cpp:691`). При len 25..31 `packSize` пишется как `&0x1F` (оборот) → декодер читает короткий кадр → CRC не сойдётся → пакет теряется, **а send вернул Success**. (Комментарий `config:96` сам предупреждает про «31 vs 24».)
2. **Нет seq** → дубликаты/пропуски незаметны (любой будущий ретрай = повтор действия).
3. **Коллизии не детектируются** (half-duplex без CSMA/CD; `isPairSending` глушит только свой RX). TODO «отложить TX после приёма» (`IR_Encoder.cpp:5`) не реализован.
4. Тайминговая реконструкция может собрать «правдоподобный» буфер, редко проходящий CRC на неверных данных (~1/65536).
5. Accept-customByte — 1 байт CRC8 (1/256 ложного подтверждения), но т.к. игнорируется — без эффекта.
---
## 3. Мины/баги (severity, подтверждено по коду)
| # | Severity | Файл:line | Суть | Триггер |
|---|----------|-----------|------|---------|
| B1 | **CRITICAL** | `IR_DecoderRaw.cpp:887,951-970` | `crcCheck(packSize-crcBytes)` при `packSize==1` → len=255 → OOB-чтение `dataBuffer[0..256]` (массив 38) | шум: первый байт с младш. 5 бит =1 (0x01,0x21,…) после лока преамбулы |
| B2 | **HIGH** | `IR_Decoder.cpp:167-169` | `encoder->sendAccept()` без null-check; у Car `encoder==nullptr` (`IR.cpp:31`) → HardFault | валидный DATA_ACCEPT машинке (детерминированно) или шум (1/65536) |
| B3 | **HIGH** | `IR_DecoderRaw.cpp:753` | off-by-one `>` вместо `>=` → OOB-запись `dataBuffer[38]` (затирает `prevRise`) при `packSize==0` | шум: первый байт с младш. 5 бит =0, ~38 «дата»-байт подряд |
| B4 | **MEDIUM** | `IR_Decoder.cpp:72-79` (конструктор) | `isWaitingAcceptSend`/`acceptSendTimer`/`addrAcceptSendTo`/`acceptCustomByte` не инициализированы → спонтанный `sendAccept` (+ B2 крэш) | мусор в RAM при старте |
| B5 | **MEDIUM** | `RingBuffer.h:28-37` + `IR_DecoderRaw.cpp:408-415` | вложенные критсекции не вкладываются (`interrupts()` в `pop()` снимает внешний `noInterrupts()` из `tick()`) → торн-рид `*rawPtr`, слот уже свободен для ISR | заполненное кольцо в узком окне |
| B6 | MEDIUM | `IR_Decoder.cpp:112-115`, `IR_DecoderRaw.cpp:280-282` | джиттер метки фронта: `std::function` indirect-call + лишний `noInterrupts/interrupts` вокруг `micros()` в ISR → разброс `risePeriod` | нагрузка/параллельный DMA |
| B7 | LOW | `IR_DecoderRaw.cpp:959` | приоритет операторов в сравнении CRC — работает случайно | смена `crc_t` |
| B8 | LOW | `IR_DecoderRaw.cpp:448` | `isSubBufferOverflow=false` без критсекции параллельно ISR `=true` → потеря флага (диагностика) | — |
| B9 | LOW | `IR_DecoderRaw.cpp:1612` | sentinel `prevRise==0` при `micros()==0` (раз в ~71.6 мин) → разовый глитч преамбулы | редко |
| B10 | LOW (спит) | `IR_DecoderRaw.cpp:980-986`, `:1676` | `ceil_div` /`riseTime`; при `freeFrec==true` `riseSyncTime` может уйти <300 `riseTimeMin` overflow / деление на 0 | только при `freeFrec` (по умолч. выкл) |
**Опровергнуто (REFUTED):** wrap `micros()` в таймаутах (wrap-safe, `(uint32_t)(a-b)`); переполнение subBuffer за границы (push проверяет isFull); `packSize>dataByteSizeMax` (5 бит max 31 < 38, опасна только НИЖНЯЯ граница); overflow `highCount/lowCount` (гейт по IR_timeout); аллокации в ISR (bind SBO на attachInterrupt, в ISR только indirect-call); реентерабельность одной EXTI (M4 не вытесняет сам себя).
**Требует железной проверки:** джиттер метки фронта под параллельным DMA-TX; NVIC preempt RX>TX; воспроизведение B1/B3 (подать первый байт 0x01 / 0x00 под отладчиком); RAM-бюджет subBuffer (2КБ/декодер) на F4-пульте с несколькими декодерами.
---
## 4. Приоритетный список доработок
### Сначала — баги (кандидаты на фикс, все в общей библиотеке → blast radius на все проекты)
1. **B1 CRITICAL:** валидировать `packSize >= crcBytes` (и разумную нижнюю границу) ДО `crcCheck`; отбрасывать кадр иначе. Однострочная защита от OOB.
2. **B2 HIGH:** `if (encoder) encoder->sendAccept(...)` (null-check). Спасает Car от HardFault.
3. **B3 HIGH:** `>=` вместо `>` в границе `writeToBuffer:753` (+ обрабатывать `packSize==0` как невалидный).
4. **B4 MEDIUM:** инициализировать accept-поля в конструкторе (`isWaitingAcceptSend=false` и т.д.).
5. **B5 MEDIUM:** `pop()` отдавать по значению (out-параметр) под одной критсекцией, не указателем во внутренний слот.
### Затем — производительность RX (эффект/сложность)
- Батч-pop за tick (высокий эффект / низкая сложность) → снять привязку к частоте loop.
- Табличный CRC; больше subBuffer; preempt RX>TX; (крупно) input-capture+DMA.
### Протокол (эффект/сложность)
1. **Валидация полной длины в send** (высокий/низкий): проверять `packSize<=31` (Data ≤24, BACK ≤26), возвращать `PayloadTooLarge` — убирает тихую потерю.
2. **Seq + дедуп** (высокий/средний): 1 байт seq, дедуп по (addrFrom, seq). Предпосылка для любого ретрая.
3. **Реальный ACK+ретрай на отправителе** (высокий/средне-высокий): механизм генерации Accept уже есть, замкнуть петлю (нужен seq).
4. **Listen-before-talk** (средний/низкий): отложить TX пока `isReciving()` — TODO уже помечен.
5. **Заголовочный CRC/дублирование длины** (средний/низкий): битая длина не должна ломать границу кадра.
6. Кадровый повтор как лёгкий FEC (в паре с seq); CRC16-CCITT вместо 2×CRC8 (маргинально, ломает совместимость); фрагментация >31 байт с ACK (если реально нужны большие payload); починить/включить bruteforce (сомнительно).
---
## Ключевые файлы
`IR-protocol/`: `IR_DecoderRaw.{h,cpp}` (ISR, декод, CRC, reject, преамбула, mute), `IR_Decoder.{h,cpp}` (диспетчеризация, Accept-петля, EXTI/NVIC), `IR_Encoder.{h,cpp}` (сборка кадра, CRC, send, isSending), `PacketTypes.{h,cpp}` (форматы, адресация), `IR_config.{h,cpp}` (тайминги, framing, crc8), `RingBuffer.h`, `IrTxIsrBufferedStorage.h`.

View File

@ -0,0 +1,72 @@
# Анализ физики сигнала: TX-форма против RX-коррекций
## 1. Резюме
Система в основном **не борется сама с собой**: и TX, и RX выведены из одних констант `IR_config.h`, форма на передаче при `multiply=2` (дефолт, чётный) — **бит-точная, без округлений**, а подавляющая часть RX-эвристик ловит деформации, которые реально вносит ИК-канал через демодулятор (TSOP-АРУ, отражения, фон, расхождение кварцев). Различение 0/1 идёт по скважности (`highTime>lowTime`, IR_DecoderRaw.cpp:620-633) внутри периода и **не зависит** от широкого окна периода — это архитектурно здоровое решение.
Но есть **три очага самопорождённых ошибок**, которые RX молча компенсирует:
1. **Главный (чистый баг, не канал):** RX строит всю временную базу на усечённой константе `carrierPeriod = 1e6/38000 = 26` (bitTime=962 µs), тогда как TX физически излучает на реальной частоте таймера — `26.316 µs`, bitTime≈973.7 µs. Систематический **односторонний сдвиг ~11.7 µs/бит (1.2%)**, который целиком съедается допуском `tolerance=300`. Это TX/модель кривит — RX терпит.
2. **Конфиг-зависимый:** ISR-путь вносит программный джиттер фронтов (латентность/вытеснение, IR_Encoder.cpp:1037-1042), которого нет у DMA-пути. Часть glitch/reconstruction-логики RX страхует именно этот самопорождённый джиттер.
3. **Спровоцированный дизайном модуляции:** протокол использует **длинные непрерывные MARK** (преамбула 49 периодов ≈1290 µs, бит '1' = 25 периодов ≈658 µs). Длинные непрерывные burst'ы — классический триггер АРУ TSOP-демодулятора (просадка огибающей в середине долгого mark) → «рваная единица», которую чинит реконструкция E/G. Деформацию вносит канал (TSOP), но провоцирует её выбор формы на TX.
Остальное — стартовая фаза (≤26 µs), дрейф периода, дребезг фронтов — это то, что **обязан** обрабатывать любой ИК-приёмник; здесь самолечения нет.
## 2. Карта механизмов RX
| Механизм RX (file:line) | Какую деформацию ловит/чинит | Детект/Коррекция | Источник деформации | Вердикт |
|---|---|---|---|---|
| **A.** pulse-filter, минимум ширины (DecoderRaw 1493-1555) | иголка/дребезг компаратора, пара «↑↓» короче минимума | детект | реальный канал (front-end, дребезг) | оправданно (но дефолт `MIN_PULSE_US=0` — выключен) |
| **B.** short-LOW glitch reject + phase nudge (484-499, 8-16) | короткий ложный ↑ после микро-провала огибающей | детект + коррекция фазы | реальный канал (TSOP-АРУ, просадка) | оправданно |
| **C.** micro-gap rise reject + nudge (500-516) | распад фронта на два близких | детект + коррекция фазы | реальный канал; **частично** ISR-джиттер | в основном оправданно |
| **D.** отсев короткого стартового периода / спада (517-524, 566-573) | сжатый ↑→↑ без контекста, дребезг спада | детект | реальный канал (шум/дребезг) | оправданно |
| **E.** сбор рваной единицы (526-557) | длинный HIGH-бит, распавшийся из-за пропадания несущей | подготовка к коррекции | канал (TSOP-АРУ), **спровоцировано длинными MARK на TX** | оправданно, но триггер — дизайн формы |
| **F.** окно валидности периода (h:24-27; 583-592) | грубо растянутый/сжатый период | детект/брак | реальный канал (потеря кадра) | оправданно |
| **G.** реконструкция ceil_div, invertErr, «лишний ноль/единица» (617-741, 980-986) | пропущенные ↑-фронты, N слипшихся бит, дроблёный/короткий бит | **коррекция (ядро)** | канал (дропаут несущей); косвенно ISR-джиттер | оправданно под канал; часть работы — следствие TX-дизайна |
| **H.** восстановление сетки по sync (804-853) | сдвиг сетки data↔sync из-за лишнего/недостающего бита | детект срыва + локальная коррекция на 1 бит | канал (потеря бита); маркер `{!L,L,!M}` от TX детерминирован | оправданно (детект реальной потери) |
| **I.** преамбула Idle→Candidate→Locked, EMA периода, якорь фазы, freeFrec (1557-1723) | дрейф периода, разброс кварцев, старт-фаза, чужая частота | коррекция базы/фазы | **смешанный:** реальный (кварцы/дрейф) + самопорождённый (старт-фаза TIM ≤26 µs, сдвиг 1.2% из carrierPeriod=26) | необходимо всегда; но частично адаптируется к TX-багу |
## 3. Где Даша сама порождает ошибку формы и потом её чинит
**(а) Усечённый `carrierPeriod=26` — самое чистое «самолечение».**
Физическая генерация НЕ использует эту константу: `setOverflow(carrierFrec*multiply, HERTZ_FORMAT)` (IR_Encoder.cpp:411) даёт реальные ~26.316 µs. А вся сетка RX (`riseSyncTime=962`, `riseTimeMin/Max`, `aroundRise`, `IR_timeout`) выведена из `carrierPeriod=26`. Результат: TX-биты систематически **на 1.2% длиннее**, чем ждёт RX. Сдвиг **односторонний** (всегда в плюс), поэтому запас `aroundRise` со стороны `riseTimeMax=1262` меньше, чем со стороны `riseTimeMin=662`. На бит это 11.7 µs, на преамбулу — ~15 µs (1274 vs 1289.5 µs). Это не канальная ошибка — это несогласованность двух представлений одной частоты внутри одного кода. **RX-допуск `tolerance=300` существует в т.ч. чтобы это не всплыло.** Исправляется тривиально (общий реальный период), после чего часть запаса окна освобождается под настоящий канал.
**(б) ISR-путь как источник джиттера, который чинит glitch-логика.**
DMA-путь детерминирован (аппаратная пересылка BSRR по TIM_UP, IrDmaTxStm32.h:246-253). ISR-путь плавает по времени из-за латентности прерывания и обхода списка энкодеров (IR_Encoder.cpp:1037-1042). Плавающие фронты — ровно то, что подхватывают B/C (nudge) и частично G. То есть при работе через ISR приёмник страхует **собственный** TX-джиттер, а не канал. Лечение: всегда использовать DMA-путь (или признать ISR как деградированный режим).
**(в) Длинные непрерывные MARK провоцируют TSOP-АРУ → «рваная единица».**
Кодирование pulse-width с MARK до 25 периодов и преамбулой 49 периодов непрерывной несущей — это осознанный выбор формы на TX. TSOP-демодуляторы имеют АРУ и штатно «проседают» на длинных непрерывных burst'ах, порождая ложные провалы огибающей в середине mark. Реконструкция E + G (ceil_div, `highCount==0 && highTime>riseTime/3 → highCount++`, 641-648) существует во многом чтобы это дособрать. Деформацию физически вносит канал (TSOP), но её вероятность задаёт форма на TX. Это не баг, но это «дизайн, требующий костыля на приёме».
**(г) Что НЕ является самолечением (важно не перепутать):**
- Стартовая фаза кадра ≤1 период несущей (free-running TIM, IrDmaTxStm32.h:241-253) — компенсируется якорем фазы преамбулы (I, `prevRise=front.time+mean/2`, 1709). Но приёмник **в принципе** не знает абсолютную фазу передатчика и обязан выводить её из преамбулы — это необходимая функция, а не борьба с собой.
- Маркер sync `{!L, L, !M}` детерминирован (IR_Encoder.cpp:305-331), буфер зануляется — H ловит **реальную** потерю/вставку бита в канале, не TX-артефакт.
- Квантование фронтов сеткой 13.16 µs (DMA/BSRR) — тонет в допуске TSOP (десятки µs) и погоды не делает.
## 4. Широкие ли допуски и что они маскируют
`aroundRise` = 662..1262 µs при бите 962 → **±31%**. По логике подсчёта бит это **не** широко и путаницы 1-бит/2-бита не создаёт: 2 бита ≈1924 µs, порог `ceil_div` на 2 бита ≈1443 µs; между концом окна одного бита (1262) и порогом двух (1443) есть зазор ~180 µs — перекрытия нет. Значение бита определяется по duty, а не по длине периода, поэтому широкое окно значения не портит.
Куда реально уходит ±300 µs:
- **Легитимно:** асимметрия leading/trailing фронтов TSOP-демодулятора (типично десятки–сотни µs), разброс кварцев RX/TX, дрожание огибающей, задержка АРУ. Для настоящего ИК-канала запас в этом порядке нужен.
- **Маскирует баг:** самопорождённый односторонний сдвиг 1.2% (пункт 3а) полностью прячется в этом окне. Именно поэтому баг `carrierPeriod=26` не проявляется как отказ — допуск его глотает. Это и есть «система не замечает, что борется сама с собой».
Слабое место (согласен с RX-разбором): при `freeFrec=false` база `riseTime` жёстко=962 и **не** подстраивается по ходу кадра (медиана — «TODO», IR_DecoderRaw.h:22). Односторонний TX-сдвиг + систематическое смещение периода передатчика съедают запас именно со стороны `riseTimeMax`; на краю окна шум легче перекидывает интервал в ветку реконструкции G, накручивая `errors.*`. Также джиттер-полки преамбулы (18%, но не менее 80 µs) **жёстче** рабочего окна декодера (31%) — залочиться труднее, чем потом удержать биты; это перекос порогов, а не форма.
## 5. Рекомендации
**Чинить на TX (чтобы соответствующие RX-эвристики стали не нужны):**
1. **Согласовать период несущей между TX и RX.** Убрать усечение `carrierPeriod=26` из временной модели RX: выводить `bitTime`/`riseSyncTime` из той же реальной частоты, что даёт `setOverflow(carrierFrec*multiply,...)` (дробно, напр. фиксированная точка). Устраняет односторонний сдвиг 1.2% и освобождает ~12 µs/бит запаса окна. *Trade-off:* дробная арифметика на MCU; но можно один раз посчитать реальный период при инициализации.
2. **Всегда использовать DMA-путь; ISR — только как явный деградированный fallback.** Снимает программный джиттер фронтов и часть нагрузки с B/C/G. *Trade-off:* требует свободного канала DMA+таймера; на платах без ресурса — остаётся ISR с его джиттером.
3. **Пересмотреть длину непрерывных MARK/преамбулы под конкретный TSOP.** Если приёмник — демодулирующий TSOP с АРУ, длинные burst'ы (658/1290 µs) провоцируют «рваную единицу». Сократить максимальный непрерывный mark или разбить преамбулу на более частые фронты в пределах спецификации TSOP по burst-length. *Trade-off:* меняет протокол/энергетику кадра, требует ревизии окон преамбулы RX; выигрыш — можно ослабить E/G.
4. **(Опционально) фазовое выравнивание старта кадра** (сброс счётчика TIM при старте потока). Уберёт стартовый сдвиг ≤26 µs. *Trade-off:* минимальный выигрыш (преамбула всё равно якорит фазу) — низкий приоритет.
**Что из «коррекций» оставить под настоящий канал (даже после идеального TX):**
- **G (ceil_div реконструкция) и E (сбор рваной единицы)** — оставить: дропаут несущей и просадка АРУ на реальной дистанции/отражениях никуда не денутся. Это ядро устойчивости к каналу.
- **B/C (glitch reject + phase nudge)** и **A (pulse-filter)** — оставить: дребезг/иголки front-end реальны; A стоит **включить** (`IR_INPUT_MIN_PULSE_US>0`) на STM32, где сейчас фронты идут в декодер напрямую (tick 440-444).
- **H (sync-grid recovery)** — оставить: детектирует реальную потерю/вставку бита; маркер детерминирован, ложных срабатываний от TX не будет.
- **I (преамбула: EMA периода, якорь фазы)** — оставить: адаптация под кварцы/дрейф и вывод фазы обязательны для любого RX. Рекомендуется **выровнять пороги**: сделать джиттер-полку преамбулы не жёстче рабочего окна декодера, иначе лок труднее удержания.
- **Кандидаты на сужение после фикса TX:** окно `aroundRise` можно сузить в сторону `riseTimeMax` (убрать заложенный под 1.2% односторонний запас) и реализовать бегущую медиану периода (IR_DecoderRaw.h:22 TODO) — тогда допуск начнёт отражать реальный канал, а не прятать несогласованность констант.
**Итог-вердикт:** коррекции RX в массе **оправданны под реальный ИК-канал**, но система содержит одно явное самолечение (усечённый `carrierPeriod` → 1.2% сдвиг, спрятанный в `tolerance`) и два условных (ISR-джиттер; длинные MARK, провоцирующие TSOP-АРУ). Широкий допуск ±31% сам по себе безопасен для подсчёта бит, но именно он маскирует баг с частотой — то есть система действительно частично борется сама с собой, просто настолько тихо, что это не видно в `errors.*`.

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Точное сравнение CRC-схем на бит-слипе. crc8 как в IR_config.cpp.
# ВАЖНО: CRC в протоколе считается над БАЙТАМИ данных (sync НЕ входит — подтверждено кодом),
# поэтому здесь sync в CRC-математику не включён (это корректно для оценки самой CRC).
# Второй ниббл — СО СЦЕПКОЙ: poly2 над (data + crc1_байт), как в оригинале crcCheck.
# Сквозной анализ с sync-фильтром требует модели FSM декодера (resync/‘исправление лишнего нуля’) — см. заметку.
import random
random.seed(3)
POLY1=0x31; POLY2=0x8C
def crc8(d,poly):
c=0xFF
for b in d:
c^=b
for _ in range(8): c=((c<<1)^poly)&0xFF if c&0x80 else (c<<1)&0xFF
return c
def fold4(x): return (x^(x>>4))&0x0F
def single(d): return (crc8(d,POLY1),)
def double(d): c1=crc8(d,POLY1); return (c1, crc8(d+[c1],POLY2)) # текущая 2 байта, сцепка
def combo_indep(d): return ((fold4(crc8(d,POLY1))<<4)|fold4(crc8(d,POLY2)),) # 1 байт, БЕЗ сцепки
def combo_chain(d): c1=crc8(d,POLY1); return ((fold4(c1)<<4)|fold4(crc8(d+[c1],POLY2)),) # 1 байт, СО сцепкой (как ориг.)
def bitsof(d): return [x for b in d for x in [(b>>(7-j))&1 for j in range(8)]]
def bytesof(b): return [int(''.join(map(str,b[i:i+8])),2) for i in range(0,len(b),8)]
def slip(d,p): b=bitsof(d); return bytesof(b[:p]+b[p+1:]+[0])
S=[("single 1б",single),("double 2б (текущая, сцепка)",double),
("combo 1б БЕЗ сцепки",combo_indep),("combo 1б СО сцепкой (как ориг.)",combo_chain)]
for L in (4,6,8):
und={n:0 for n,_ in S}; ch=0
for _ in range(300000):
d=[random.randint(0,255) for _ in range(L)]; p=random.randint(L*2,L*6)
c=slip(d,p)
if c==d: continue
ch+=1
for n,f in S:
if f(c)==f(d): und[n]+=1
print(f"данные {L}б, {ch} слипов:")
for n,_ in S: print(f" {n:34s}: {und[n]/ch*100:.4f}% необнаружено")

View File

@ -0,0 +1,33 @@
# CRC при сдвиге битов — симуляция (воспроизведение и оценка сжатия)
> Скрипт: `crc_shift_sim.py` (рядом). `crc8` точно как в `IR_config.cpp` (MSB-first, init=0xFF, без final-xor). `poly1=0x31`, `poly2=0x8C` (битовое зеркало 0x31). Модель ошибки: сдвиг битов в СЕРЕДИНЕ данных (бит-слип / циклический сдвиг), длина кадра сохраняется; «чек» сверяется как `check(искажённые данные) == check(оригинал)`. 200000 попыток на модель, данные 3/5/8 байт.
## 1. Твой исторический баг ВОСПРОИЗВЕДён
Одиночный CRC8 (один полином) реально пропускает сдвиг в середине пакета. Примеры (данные → сдвиг → искажённые):
- `26 0b 05``26 16 0a`: `crc1=0x78` совпал → **single ПРОШЁЛ**; двойная схема: `crc2 0xd8≠0x5c`**поймала**.
- `cc4fb8f633``cc5f71ec66`: `crc1=0x20` совпал → single прошёл; double поймала (`crc2 0xfc≠0x90`).
- `b983315b416440c3``b983315e82c88186`: `crc1=0x3a` совпал → single прошёл; double поймала.
## 2. Частота пропуска (доля НЕобнаруженных сдвигов)
| Схема | slip-delete | rotate-all | rotate-mid-window |
|---|---|---|---|
| **single** (1×CRC8, 8 бит) | 0.290.41 % | ~0.78 % | 0.550.78 % |
| **double** (2×CRC8, 16 бит) — текущая | **0.0070.015 %** | **0.0250.03 %** | **0.0080.026 %** |
| nibble-fold (4+4 бит) | 0.760.80 % | ~1.58 % | 1.181.55 % |
| nibble-trunc (4+4 бит) | 1.371.57 % | ~1.53 % | 1.221.57 % |
Вывод: **double примерно в 2040× надёжнее single** и ловит практически все сдвиги, которые single пропускает. Это подтверждает, зачем добавляли второй полином (0x8C = зеркало 0x31 ловит «зеркальные» сдвиговые ошибки, слепые для 0x31).
## 3. Идея «склеить 2 байта в 1» (полбайта на полином) — ХУЖЕ, чем есть
- **nibble-fold** (свернуть каждый CRC8 в 4 бита xor'ом) и **nibble-trunc** (взять по 4 бита) дают **0.61.6 % пропусков — это ХУЖЕ даже одиночного полного CRC8** и в ~50150× хуже текущей двойной схемы.
- Burst-ошибки (подряд искажённые биты — типичный IR-сбой: бит-слипы/всплески). Гарантия обнаружения = ширине контроля:
- single 8 бит и double — надёжны на коротких burst (≥8 бит), эмпирически в выборке не пропускали и длиннее;
- **nibble-fold** — гарантия ~4 бита (эмпирически до 7);
- **nibble-trunc** — **пропускает даже ОДИНОЧНЫЙ бит** (усечение теряет старший ниббл) → так делать нельзя.
## 4. Рекомендация
- **CRC — не то место, где стоит экономить байт.** Сжатие контроля до 1 байта повышает пропуск сдвигов/burst в ~50150× (с ~0.01 % до ~1 %) — прямо в том классе ошибок, ради которого второй полином и вводили.
- Если байт очень нужен — забирать его **не из CRC**, а из уже выявленного резерва: свободные `msgType` (3,5), «пустая» зона длины (компактные кадры), или переупаковка адресов/полей. 16-битный контроль (2×CRC8) сохранить.
- Если всё же сжимать CRC до 1 байта — только **fold (xor нибблов)**, никогда не truncate; и принять ~1 % пропуска на сдвигах (в ~50100× хуже текущего). Как отдельный компромисс — обсуждать вместе с обратной совместимостью.
(Обратную совместимость версий протокола обсудим отдельно — по запросу Даши.)

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Симуляция CRC-схем IR-protocol на "сдвиговых" ошибках.
# crc8 воспроизводит IR_config.cpp: MSB-first, init=0xFF, без final-xor.
import random
random.seed(1)
POLY1 = 0x31
POLY2 = 0x8C # битовое зеркало 0x31
def crc8(data, poly):
crc = 0xFF
for b in data:
crc ^= b
for _ in range(8):
crc = ((crc << 1) ^ poly) & 0xFF if (crc & 0x80) else (crc << 1) & 0xFF
return crc
def fold4(x): # свернуть 8-бит CRC в 4 бита (xor старший^младший ниббл)
return (x ^ (x >> 4)) & 0x0F
# --- схемы контроля: считают "чек" от данных и сверяют ---
def single_chk(d): return (crc8(d, POLY1),) # 8 бит, 1 полином
def double_chk(d): c1 = crc8(d, POLY1); return (c1, crc8(d + [c1], POLY2)) # текущая: 16 бит
def nib_fold_chk(d):return ((fold4(crc8(d, POLY1)) << 4) | fold4(crc8(d, POLY2)),) # предложенная: 4+4 бит
def nib_trunc_chk(d):return (((crc8(d, POLY1) & 0x0F) << 4) | (crc8(d, POLY2) & 0x0F),)
def bits_of(data):
return [(b >> (7 - j)) & 1 for b in data for j in range(8)]
def bytes_of(bits):
return [sum(bits[i + j] << (7 - j) for j in range(8)) for i in range(0, len(bits), 8)]
# --- модели "сдвига" в середине пакета (над битами данных, длина сохраняется) ---
def slip_delete(data, p): # выкинуть бит p, сдвинуть хвост влево, добить 0 в конец
b = bits_of(data); b = b[:p] + b[p+1:] + [0]; return bytes_of(b)
def rotate_all_right(data, _): # циклический сдвиг ВСЕХ бит вправо на 1
b = bits_of(data); b = [b[-1]] + b[:-1]; return bytes_of(b)
def rotate_mid_window(data, p): # циклический сдвиг окна [p, p+16) на 1 (локальный сдвиг в середине)
b = bits_of(data); q = min(p+16, len(b))
w = b[p:q]; w = [w[-1]] + w[:-1]; b = b[:p] + w + b[q:]; return bytes_of(b)
MODELS = [("slip-delete (бит-слип)", slip_delete),
("rotate-all (сдвиг всего на 1)", rotate_all_right),
("rotate-mid-window (сдвиг окна в середине)", rotate_mid_window)]
SCHEMES = [("single (1×CRC8, 8бит)", single_chk),
("double (2×CRC8, 16бит) — текущая", double_chk),
("nibble-fold (4+4бит)", nib_fold_chk),
("nibble-trunc (4+4бит)", nib_trunc_chk)]
def run(datalen, trials):
print(f"\n===== данные {datalen} байт, {trials} попыток на модель =====")
for mname, model in MODELS:
# для каждой схемы: сколько ошибок НЕ обнаружено (чек совпал, а данные изменились)
undetected = {s[0]: 0 for s in SCHEMES}
changed = 0
first_single_miss = None
for _ in range(trials):
d = [random.randint(0, 255) for _ in range(datalen)]
p = random.randint(datalen*8//4, datalen*8*3//4) # позиция в середине
c = model(d, p)
if c == d:
continue
changed += 1
for sname, chk in SCHEMES:
if chk(c) == chk(d):
undetected[sname] += 1
if sname.startswith("single") and first_single_miss is None:
first_single_miss = (d[:], c[:], p)
print(f" модель: {mname} (изменённых кадров: {changed})")
for sname, _ in SCHEMES:
u = undetected[sname]
rate = u / changed if changed else 0
print(f" {sname:38s}: пропущено {u:6d} ({rate*100:.4f}%)")
if first_single_miss:
d, c, p = first_single_miss
print(f" ПРИМЕР пропуска single (poly1): data={bytes(d).hex()} -> shift@{p} -> {bytes(c).hex()}")
print(f" single: crc1(orig)={crc8(d,POLY1):02x} == crc1(shift)={crc8(c,POLY1):02x} -> ПРОШЁЛ")
print(f" double: crc2(orig)={double_chk(d)[1]:02x} vs crc2(shift)={double_chk(c)[1]:02x} -> {'ПРОШЁЛ (не поймал!)' if double_chk(d)==double_chk(c) else 'ПОЙМАЛ'}")
for L in (3, 5, 8):
run(L, 200000)
# Гарантированное покрытие burst-ошибок (подряд идущие искажённые биты) — ключ для IR (бит-слипы/всплески)
print("\n===== Гарантия обнаружения BURST-ошибок (100% всех burst данной длины?) =====")
def burst_guaranteed(chk, datalen, blen):
# перебор всех burst длины blen по всем позициям на нескольких случайных базах; ищем ХОТЬ ОДИН пропуск
for _ in range(300):
d = [random.randint(0,255) for _ in range(datalen)]
base = bits_of(d)
for start in range(len(base)-blen+1):
b2 = base[:]
for k in range(blen):
b2[start+k] ^= 1
c = bytes_of(b2)
if c != d and chk(c) == chk(d):
return False # найден необнаруженный burst
return True
for sname, chk in SCHEMES:
maxb = 0
for blen in range(1, 25):
if burst_guaranteed(chk, 5, blen):
maxb = blen
else:
break
print(f" {sname:38s}: 100%% ловит burst до {maxb} бит подряд")

View File

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# Визуализация кадров и сдвигов битов (с синхробитами и подписями).
# crc8 как в IR_config.cpp. Синхро (syncBits=3) логически = [~last, last, ~next] (addSync, send_HIGH=1).
POLY1=0x31; POLY2=0x8C
def crc8(data,poly):
c=0xFF
for b in data:
c^=b
for _ in range(8):
c=((c<<1)^poly)&0xFF if c&0x80 else (c<<1)&0xFF
return c
def bits(b): return [(b>>(7-j))&1 for j in range(8)]
def bstr(bl): return ''.join(str(x) for x in bl)
def onair(frame_bytes, labels):
"""строка вида: dddddddd sss dddddddd sss ... + строка подписей под байтами"""
rows=[]
line=''; lab=''
for i,by in enumerate(frame_bytes):
db=bstr(bits(by))
# синхрогруппа ПОСЛЕ байта (между байтами), кроме последнего
if i < len(frame_bytes)-1:
last=bits(by)[7]; nxt=bits(frame_bytes[i+1])[0]
sy=f"{1-last}{last}{1-nxt}"
piece=db+' ['+sy+'] '
else:
piece=db
# подпись байта, центрируем под 8 бит
L=labels[i] if i<len(labels) else ''
lab+=(L[:8]).center(8)+ (' s ' if i<len(frame_bytes)-1 else '')
line+=piece
return line, lab
def show_frame(title, frame_bytes, labels):
print(f"\n### {title}")
print(" байты (hex):", ' '.join(f"{b:02x}" for b in frame_bytes))
print(" поля :", ' '.join((labels[i] if i<len(labels) else '?')[:2].ljust(2) for i in range(len(frame_bytes))),
" (по 1 байту)")
line,lab=onair(frame_bytes,labels)
print(" эфир (data 8 бит + [sync 3]) и подписи:")
print(" "+line)
print(" "+lab)
def slip_delete_databits(data, p):
b=[x for by in data for x in bits(by)]
b=b[:p]+b[p+1:]+[0]
return [int(''.join(map(str,b[i:i+8])),2) for i in range(0,len(b),8)]
def show_shift(title, data, p):
print(f"\n### {title}")
shifted=slip_delete_databits(data,p)
b0=[x for by in data for x in bits(by)]
b1=[x for by in shifted for x in bits(by)]
# выравненные строки битов по байтам
def grp(bl): return ' '.join(bstr(bl[i:i+8]) for i in range(0,len(bl),8))
print(" ДО :", ' '.join(f"{x:02x}" for x in data), " crc1(poly1)=%02x"%crc8(data,POLY1),
" crc2(poly2 over data+crc1)=%02x"%crc8(data+[crc8(data,POLY1)],POLY2))
print(" ПОСЛЕ :", ' '.join(f"{x:02x}" for x in shifted), " crc1(poly1)=%02x"%crc8(shifted,POLY1),
" crc2=%02x"%crc8(shifted+[crc8(shifted,POLY1)],POLY2))
print(" биты ДО :", grp(b0))
# маркер позиции удалённого бита p
mk=[' ']*len(b0);
pos = p + p//8 # +пробелы между байтами
mk_line=''
# построим маркерную строку под "биты ДО"
s=grp(b0)
# индекс символа для бита p:
char_idx = p + (p//8) # каждый байт(8) + 1 пробел
mk_line=' '*char_idx+'^'
print(" ", mk_line, f"<- бит #{p} удалён, всё правее сдвинуто влево на 1")
print(" биты ПОСЛЕ:", grp(b1))
# различия побитно
diff=''.join('X' if (i<len(b1) and b0[i]!=b1[i]) else '.' for i in range(len(b0)))
print(" дельта :", ' '.join(diff[i:i+8] for i in range(0,len(diff),8)), " (X=бит изменился)")
same1 = crc8(data,POLY1)==crc8(shifted,POLY1)
c2o=crc8(data+[crc8(data,POLY1)],POLY2); c2s=crc8(shifted+[crc8(shifted,POLY1)],POLY2)
print(f" ВЕРДИКТ: single(poly1) {'ПРОШЁЛ (не заметил сдвиг!)' if same1 else 'поймал'};"
f" double(+poly2) {'НЕ поймал' if (same1 and c2o==c2s) else 'ПОЙМАЛ'} (crc2 {c2o:02x}{'=='if c2o==c2s else ''}{c2s:02x})")
print("="*70)
print("ЧАСТЬ 1. Реалистичный кадр на эфире (структура + синхробиты)")
print("="*70)
# BACK-кадр: msgType=0(BACK), packSize=5 → [hdr][addrFrom_hi][addrFrom_lo][crc1][crc2]
# hdr = (0<<5)|5 = 0x05 ; addrFrom = 300 = 0x012C
data_back=[0x05, 0x01, 0x2C]
c1=crc8(data_back,POLY1); c2=crc8(data_back+[c1],POLY2)
frame_back=data_back+[c1,c2]
show_frame("BACK, addrFrom=300, packSize=5", frame_back,
["HDR","AfHi","AfLo","CRC1","CRC2"])
print(" (HDR = 000 00101: msgType=0(BACK), len=5=весь пакет; sync между байтами = [¬last,last,¬next])")
print("\n"+"="*70)
print("ЧАСТЬ 2. Сдвиги, где ОДИНОЧНЫЙ CRC8 слеп (из симуляции), в битах")
print("="*70)
show_shift("Пример A: data 26 0b 05, слип бита #10", [0x26,0x0b,0x05], 10)
show_shift("Пример B: data 3b 7c 43, слип бита #14 (окно в середине)", [0x3b,0x7c,0x43], 14)
show_shift("Пример C: data cc 4f b8 f6 33, слип бита #11", [0xcc,0x4f,0xb8,0xf6,0x33], 11)

View File

@ -1,5 +1,7 @@
# Контракт бэкенда DMA-TX ИК (`IrDmaTxStm32`)
См. также: [IR_TX_MODES.md](IR_TX_MODES.md) — общая схема выбора `legacy ISR`, `buffered ISR` и `external backend`.
Платформа: **STM32G4**, Arduino STM32. Передача: **DMA memory → GPIO BSRR**, запрос от **TIM UPDATE** (частота `carrierFrec×2` из `IR_Encoder::beginClockOnly`).
### Число потоков (шаблон)

View File

@ -55,7 +55,7 @@ IRRX t=1234988 rsn=MUTE_END cnt=42
## Когда смотреть подробный debug
- `listenStart` / `checkTimeout` — в конце обработки фронта (`END:`) и во ветке «нет фронта» в `tick()`; не в начале до `pop`, иначе после таймаута `lastEdgeTime` расходится с метками ISR из очереди → ложные `TIMEOUT` (`bits=0`).
- Пока в `subBuffer` / `filteredSubBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`.
- Пока в `subBuffer` или в hold фильтра есть необработанные фронты, таймаут по `micros() - lastEdgeTime` **не оценивается** (`rxTimeoutPipelineBusy`): иначе при хвосте очереди «тихая пауза» считается слишком длинной и снова ложный `TIMEOUT`.
- Если нужен полный поток битов и sync: включать `IRDEBUG_SERIAL_PACK`
- Если нужно понять, какие именно фронты пришли в ISR: включать `IR_EDGE_TRACE`
- `IR_RX_BRIEF_LOG` нужен как короткий always-on-ish индикатор сути проблемы, без длинного дампа

146
ref/IR_TX_MODES.md Normal file
View File

@ -0,0 +1,146 @@
# Режимы IR TX в библиотеке
Этот документ описывает, как в библиотеке выбирается путь передачи IR и как его правильно использовать в проектах Arduino STM32.
## Кратко
У библиотеки есть три варианта TX:
- `legacy ISR` — внутренний ISR-путь без внешнего backend. Это путь по умолчанию для обратной совместимости.
- `buffered ISR` — внутренний ISR-путь с предварительной подготовкой BSRR-слов и кольцевым буфером.
- `external backend` — передача делегируется проекту через `IR_Encoder::setExternalTxBackend(...)`, например в DMA backend.
Порядок выбора такой:
1. Если зарегистрирован `external backend`, используется он.
2. Иначе используется внутренний ISR библиотеки.
3. Для внутреннего ISR:
- по умолчанию включён `legacy ISR`
- `buffered ISR` включается явно: нужно привязать storage к encoder и переключить режим
## 1. Legacy ISR
Это режим по умолчанию. Старые проекты могут ничего не менять:
```cpp
static HardwareTimer timer(TIM11);
static IR_Encoder enc(PA9, 42, &dec);
void setup() {
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
enc.enable();
}
```
Если проект не регистрирует внешний backend и не переключает режим явно, библиотека работает в `legacy ISR`.
Для явного выбора можно написать:
```cpp
IR_Encoder::setTxIsrLegacyMode(true);
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
```
## 2. Buffered ISR
Этот режим использует внутренний буферный ISR-путь библиотеки. Он включается только явно:
```cpp
#include <IrTxIsrBufferedStorage.h>
static IrTxIsrBufferedStorage<> txStorage;
enc.enableBufferedIsr(txStorage);
IR_Encoder::begin(&timer, 1, TIM11_IRQn, 0);
```
Нижнеуровневый вариант API — отдельно привязать storage через `attachBufferedIsrStorage(...)`, но в обычном проекте удобнее использовать `enableBufferedIsr(...)`.
Смысл режима:
- пакет сначала превращается в `gate runs`
- затем в поток слов `GPIO->BSRR`
- ISR выдаёт готовые слова из кольцевого буфера
### Важно про RAM
В текущей реализации память под буферный ISR вынесена из `IR_Encoder` в отдельный storage-объект.
То есть:
- `legacy ISR` не тянет buffered-буферы в RAM самого `IR_Encoder`
- память под `gate runs` и `BSRR words` появляется только там, где проект сам создал `IrTxIsrBufferedStorage<>`
Это важно для STM32 с небольшим объёмом RAM, например для `STM32F401`.
## 3. External backend
Если проект хочет полностью взять TX на себя, библиотека позволяет зарегистрировать внешний backend:
```cpp
static bool txBusy(void* ctx);
static bool txStart(void* ctx, IR_Encoder* enc, const uint8_t* packet, uint8_t len);
void setup() {
IR_Encoder::beginClockOnly(&timer);
IR_Encoder::setExternalTxBackend(txStart, txBusy, nullptr);
enc.enable();
}
```
После вызова `setExternalTxBackend(...)` библиотека больше не использует свои внутренние ISR-пути для фактической передачи.
В этом режиме:
- `setTxIsrLegacyMode(true/false)` игнорируется
- завершение передачи должен сигнализировать сам backend через `enc->externalFinishSend()`
Подробности по встроенному DMA backend для `STM32G4xx`: см. [IR_DMA_TX_backend.md](IR_DMA_TX_backend.md).
## Когда какой режим использовать
### Старый проект, который ничего не настраивает
Использовать как есть:
```cpp
IR_Encoder::begin(...);
```
Итог: `legacy ISR`
### Нужен новый внутренний буферный ISR
Включить явно:
```cpp
#include <IrTxIsrBufferedStorage.h>
static IrTxIsrBufferedStorage<> txStorage;
enc.enableBufferedIsr(txStorage);
IR_Encoder::begin(...);
```
Итог: `buffered ISR`
### Нужен проектный DMA или другой свой транспорт
Подключить внешний backend:
```cpp
IR_Encoder::beginClockOnly(...);
IR_Encoder::setExternalTxBackend(...);
```
Итог: `external backend`
## Рекомендация для совместимости
Для старых проектов безопаснее не вызывать `setTxIsrLegacyMode(false)`, если нет явной причины переходить на buffered ISR.
Если задача — сохранить старое поведение без неожиданного роста нагрузки на TX-логику, оставляйте default `legacy ISR` или задавайте его явно:
```cpp
IR_Encoder::setTxIsrLegacyMode(true);
```

View File

@ -132,6 +132,7 @@ void setup() {
void loop() {
#if LONGDATA_USE_DMA
IR_Encoder::tick();
dmaBackend.poll(); // Fix D: watchdog завершения DMA-передачи (как в Car/src/IR/IR.cpp)
#endif
const uint32_t now = millis();