Files
templates/doc/setprotocol.html

613 lines
47 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Интерактивная документация SETProtocol: архитектура, ABI, протоколы и переносимость.">
<title>SETProtocol — переносимое протокольное ядро</title>
<style>
:root{color-scheme:dark;--bg:#061018;--panel:#0b1b27;--panel2:#102737;--line:#244253;--text:#edf7f6;--muted:#9cb5b8;--mint:#63e6c7;--blue:#79c7ff;--amber:#f6c85f;--red:#ff8d8d;--shadow:#0007}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(circle at 80% -10%,#16445b 0,transparent 34%),linear-gradient(180deg,#061018,#08151e 65%,#061018);color:var(--text);font:15px/1.65 "Segoe UI",Arial,sans-serif}a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline}code,pre{font-family:Consolas,"Courier New",monospace}code{color:#b8f7e9}pre{position:relative;overflow:auto;margin:14px 0;padding:17px;border:1px solid var(--line);border-radius:12px;background:#050d13;color:#d7e7e9;tab-size:2}.wrap{width:min(1200px,calc(100% - 34px));margin:auto}
header{padding:66px 0 34px}.eyebrow{color:var(--mint);font-weight:800;letter-spacing:.13em;text-transform:uppercase;font-size:12px}h1{max-width:920px;margin:12px 0 16px;font-size:clamp(38px,6vw,70px);line-height:1.02;letter-spacing:-.045em}h2{margin:0 0 10px;font-size:29px;letter-spacing:-.02em}h3{margin:0 0 8px;font-size:18px}.lead{max-width:920px;color:var(--muted);font-size:19px}.pills{display:flex;flex-wrap:wrap;gap:9px;margin:27px 0 0}.pill{padding:7px 11px;border:1px solid #316270;border-radius:999px;background:#0b1b27b8;color:#b9d5d6}.pill b{color:var(--mint)}
.tabbar{position:sticky;top:0;z-index:20;border-block:1px solid var(--line);background:#061018eb;backdrop-filter:blur(14px)}.tabs{display:flex;gap:5px;padding:10px 0;overflow-x:auto}.tab{border:0;border-radius:9px;padding:10px 14px;color:var(--muted);background:transparent;font:inherit;font-weight:750;white-space:nowrap;cursor:pointer}.tab:hover{color:var(--text);background:var(--panel)}.tab[aria-selected=true]{color:#03100d;background:var(--mint)}main{padding:34px 0 74px}.panel[hidden]{display:none}.section-intro{max-width:900px;margin:-2px 0 23px;color:var(--muted);font-size:17px}
.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.grid.two{grid-template-columns:repeat(2,minmax(0,1fr))}.card{min-width:0;padding:20px;border:1px solid var(--line);border-radius:16px;background:linear-gradient(145deg,#102737ed,#0b1b27ed);box-shadow:0 18px 60px var(--shadow)}.card p:last-child{margin-bottom:0}.muted,.card p{color:var(--muted)}.accent{color:var(--mint)}.tag{display:inline-block;margin:4px 3px 0 0;padding:3px 8px;border:1px solid #356172;border-radius:999px;color:#c0dbdc;font-size:12px}.tag.ready{border-color:#26745f;color:#7fe4c9}.tag.partial{border-color:#79622a;color:#f6d47b}.tag.todo{border-color:#74404a;color:#ffa9b3}
.callout{margin:20px 0;padding:17px 19px;border-left:4px solid var(--mint);border-radius:0 12px 12px 0;background:var(--panel)}.callout.warn{border-left-color:var(--amber)}.callout.danger{border-left-color:var(--red)}
.flow{display:grid;grid-template-columns:1fr 44px 1fr 44px 1fr;align-items:stretch;margin:22px 0}.flow .node{display:grid;place-items:center;min-height:126px;padding:18px;border:1px solid var(--line);border-radius:15px;background:var(--panel);text-align:center}.flow .node b{display:block;color:var(--mint);font-size:17px}.flow .arrow{display:grid;place-items:center;color:var(--mint);font-size:28px}.core-node{box-shadow:inset 0 0 0 1px #4ddbb055,0 18px 60px var(--shadow)!important}
.wire{display:flex;flex-wrap:wrap;gap:4px;margin:15px 0}.byte{padding:9px 10px;border:1px solid var(--line);background:#07131b;color:#c9dcdf;font:13px Consolas,monospace}.byte:first-child{border-radius:9px 0 0 9px}.byte:last-child{border-radius:0 9px 9px 0}.byte.sof{border-color:#2b816b;color:var(--mint)}.byte.crc{border-color:#75612f;color:#ffd879}.byte.payload{flex:1;min-width:130px;text-align:center}
table{width:100%;border-collapse:collapse;margin:17px 0}th,td{padding:11px 13px;border:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--mint);background:var(--panel2)}td{background:#0a1822c4}.table-wrap{overflow:auto}.status{white-space:nowrap}.copy{position:absolute;right:9px;top:9px;border:1px solid var(--line);border-radius:7px;padding:5px 8px;color:var(--muted);background:#0b1b27;cursor:pointer}.copy:hover{color:var(--text);border-color:var(--mint)}
.steps{counter-reset:s;display:grid;gap:12px}.step{position:relative;padding:19px 20px 19px 68px;border:1px solid var(--line);border-radius:14px;background:var(--panel)}.step:before{counter-increment:s;content:counter(s);position:absolute;left:19px;top:19px;display:grid;place-items:center;width:32px;height:32px;border-radius:50%;color:#03100d;background:var(--mint);font-weight:900}
.full-doc{padding:27px}.full-doc h1{font-size:34px;letter-spacing:-.025em}.full-doc h2{margin-top:34px;padding-top:22px;border-top:1px solid var(--line);font-size:25px}.full-doc h3{margin-top:25px}.full-doc li{margin:5px 0}.full-doc blockquote{margin:16px 0;padding:2px 17px;border-left:4px solid var(--mint);color:var(--muted)}footer{padding:25px 0 45px;border-top:1px solid var(--line);color:var(--muted)}
@media(max-width:900px){.grid,.grid.two{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:1fr}.flow .arrow{transform:rotate(90deg);height:42px}.flow .node{min-height:100px}}@media(max-width:620px){.grid,.grid.two{grid-template-columns:1fr}header{padding-top:42px}.full-doc{padding:18px}.wrap{width:min(100% - 22px,1200px)}th,td{padding:9px}}
</style>
</head>
<body>
<header class="wrap">
<div class="eyebrow">One protocol core · many platforms</div>
<h1>SETProtocol</h1>
<p class="lead">Одно C99-ядро для SETGUI, Android, Linux и микроконтроллеров. SET v2, ProtoCAN и GUI v1 собраны вместе; COM, SLCAN, SocketCAN и HAL подключаются снаружи.</p>
<div class="pills"><span class="pill"><b>C99</b> без HAL/OS</span><span class="pill"><b>ABI v1</b> для FFI</span><span class="pill"><b>3</b> wire format</span><span class="pill"><b>0</b> malloc в ядре</span><span class="pill"><b>4</b> Android ABI</span></div>
</header>
<nav class="tabbar" aria-label="Разделы SETProtocol"><div class="wrap tabs" role="tablist">
<button class="tab" role="tab" aria-selected="true" aria-controls="overview" id="tab-overview">Обзор</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="architecture" id="tab-architecture">Архитектура</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="wire" id="tab-wire">Форматы</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="abi" id="tab-abi">ABI и память</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="platforms" id="tab-platforms">Платформы</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="build" id="tab-build">Сборка</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="reference" id="tab-reference">Полный справочник</button>
</div></nav>
<main class="wrap">
<section class="panel" id="overview" role="tabpanel" aria-labelledby="tab-overview">
<h2>Что такое ядро</h2><p class="section-intro">SETProtocol — не драйвер адаптера и не GUI framework. Это детерминированная протокольная часть, одинаковая для каждой программы и платы.</p>
<div class="flow"><div class="node"><div><b>Приложение</b>SETGUI · Android · CLI · firmware</div></div><div class="arrow"></div><div class="node core-node"><div><b>SETProtocol</b>SET v2 · ProtoCAN · GUI v1 · GAS</div></div><div class="arrow"></div><div class="node"><div><b>Порт</b>COM · SLCAN · SocketCAN · USB · HAL</div></div></div>
<div class="grid"><article class="card"><h3>Единое ядро форматов</h3><p>Windows, Android и MCU больше не реализуют CRC, порядок байт и ресинхронизацию каждый по-своему.</p></article><article class="card"><h3>Узкая платформенная граница</h3><p>Смена COM на SocketCAN заменяет адаптер ввода-вывода, но не декодер протокола и не тестовые векторы.</p></article><article class="card"><h3>Контролируемая совместимость</h3><p>FFI использует публичный <code>setprotocol_abi.h</code> версии 1; внутренние C-структуры наружу не протекают.</p></article></div>
<div class="callout"><strong>Про скорость:</strong> значение <code>500000</code> у COM — скорость последовательного интерфейса адаптера. CAN bitrate на линии задаётся отдельно в адаптере/драйвере. SETProtocol получает уже доставленные байты или CAN-кадры.</div>
<div class="callout warn"><strong>Про сниферы:</strong> SLCAN, SocketCAN, CANable и vendor-адаптеры должны нормализовать вход в <code>can_id + flags + data</code>. После этого таблица, фильтры и ProtoCAN-декодер общие.</div>
</section>
<section class="panel" id="architecture" role="tabpanel" aria-labelledby="tab-architecture" hidden>
<h2>Слои и модули</h2><p class="section-intro">Нижний слой не знает, кто его вызвал. Верхний слой не должен знать внутреннюю раскладку parser context.</p>
<div class="grid">
<article class="card"><h3>Идентификатор</h3><p><code>pcan_id</code> упаковывает Priority, Route, DeviceType, DeviceID, MsgType и Body в Extended ID без непереносимых bit-fields.</p></article>
<article class="card"><h3>Полевой поток</h3><p><code>pcan_frame</code> кодирует и разбирает <code>AA55</code>; <code>pcan_crc</code> даёт CRC16; <code>pcan_link</code> ведёт SEQ и статистику.</p></article>
<article class="card"><h3>GUI-поток</h3><p><code>gui_frame</code> реализует отдельный <code>A55A</code> transport с payload до 512 байт и CRC32.</p></article>
<article class="card"><h3>Данные прибора</h3><p><code>pcan_gas</code> входит в shared core и отображает данные в 16-битное пространство. <code>gui_catalog</code> — опциональный C-модуль вне DLL.</p></article>
<article class="card"><h3>Буферизация</h3><p><code>pcan_ring</code> — SPSC-кольцо с непрерывным участком, удобным для DMA. Размер буфера — степень двойки.</p></article>
<article class="card"><h3>Граница языков</h3><p><code>pcan_abi</code> экспортирует только скалярные значения и буферы известных размеров для ctypes/JNI/Swift.</p></article>
</div>
<h2 style="margin-top:30px">Что намеренно не входит</h2>
<div class="table-wrap"><table><thead><tr><th>Вне ядра</th><th>Почему</th><th>Где реализовать</th></tr></thead><tbody><tr><td>COM/SLCAN/SocketCAN</td><td>ОС и конкретный адаптер</td><td>desktop/android port</td></tr><tr><td>CAN bitrate и UART baud</td><td>Настройка физического интерфейса</td><td>driver/config UI</td></tr><tr><td>HAL, IRQ, DMA</td><td>Зависят от MCU и SDK</td><td><code>ports/&lt;platform&gt;</code></td></tr><tr><td>Вкладки и графики</td><td>Представление, не протокол</td><td>SETGUI / Android GUI</td></tr><tr><td>Firmware A/B flow</td><td>Отдельная предметная библиотека</td><td><code>protocan-boot</code></td></tr></tbody></table></div>
</section>
<section class="panel" id="wire" role="tabpanel" aria-labelledby="tab-wire" hidden>
<h2>Три wire format</h2><p class="section-intro">SET v2 — основной протокол новых устройств. ProtoCAN bridge и GUI v1 остаются для совместимости на время перехода.</p>
<article class="card"><h3>SET protocol v2 · A5 5A 02</h3><div class="wire"><span class="byte sof">A5 5A</span><span class="byte">02 FLAGS</span><span class="byte">TYPE LE</span><span class="byte">SOURCE/DEST LE</span><span class="byte">SEQ/SIZE LE</span><span class="byte payload">PAYLOAD 0..512</span><span class="byte crc">CRC32 LE</span></div><p>Единый кадр для управления, телеметрии и firmware flow поверх serial, USB, Ethernet и сегментированного CAN.</p></article>
<div class="grid two" style="margin-top:14px">
<article class="card"><h3>DEVICE_INFO · schema 1</h3><p><code>schema u16 · class u16 · hardware u32 · firmware u32 · dictionary u32 · serial u64 · model_length u8 · model UTF-8</code></p><p>Модель ограничена 63 байтами. Числа little-endian; body идёт после обязательного <code>status u16</code>.</p></article>
<article class="card"><h3>CAPABILITIES · schema 1</h3><p><code>schema u16 · MTU u16 · interfaces u32 · features u32 · read/write/subscription/publish limits u16</code></p><p>Флаги объявляют READ, WRITE, CATALOG, SUBSCRIBE, LOG_READ, FIRMWARE и DIAGNOSTICS. GUI включает только реально доступные функции.</p></article>
</div>
<article class="card" style="margin-top:14px"><h3>CAN bridge · AA 55</h3><div class="wire"><span class="byte sof">AA 55</span><span class="byte">LEN</span><span class="byte">SEQ</span><span class="byte">FLAGS</span><span class="byte">CAN_ID LE</span><span class="byte payload">DATA 0..8</span><span class="byte crc">CRC16 LE</span></div><p><code>LEN = 6 + DLC</code>, максимум 19 байт. CRC-16/CCITT-FALSE считается от LEN до DATA.</p></article>
<article class="card" style="margin-top:14px"><h3>GUI transport · A5 5A</h3><div class="wire"><span class="byte sof">A5 5A</span><span class="byte">VER</span><span class="byte">TYPE</span><span class="byte">SEQ BE</span><span class="byte">SIZE BE</span><span class="byte payload">PAYLOAD 0..512</span><span class="byte crc">CRC32 LE</span></div><p>Версия 1. CRC32 IEEE как у zlib. Это самостоятельный протокол, не CAN DLC.</p></article>
<div class="grid two" style="margin-top:14px"><article class="card"><h3>Chunk-safe</h3><p>Parser принимает один байт, половину кадра или несколько кадров подряд. Граница <code>read()</code> не имеет протокольного смысла.</p></article><article class="card"><h3>Самосинхронизация</h3><p>После шума parser снова ищет SOF, отбрасывает неверную длину/CRC и продолжает поток, сохраняя диагностические счётчики.</p></article></div>
<h2 style="margin-top:30px">29-битный ProtoCAN ID</h2><div class="wire"><span class="byte">Priority 1</span><span class="byte">Route 1</span><span class="byte">DeviceType 3</span><span class="byte">DeviceID 4</span><span class="byte">MsgType 4</span><span class="byte payload">MsgBody 16</span></div><p class="muted">Биты 28…0 упаковываются масками и сдвигами. Это исключает зависимость от реализации C bit-fields.</p>
</section>
<section class="panel" id="abi" role="tabpanel" aria-labelledby="tab-abi" hidden>
<h2>Стабильный C ABI v1</h2><p class="section-intro">Приложение сначала проверяет версию, затем работает через функции из <code>pcan_abi.h</code>. Внутренние структуры можно менять, сохраняя ABI.</p>
<div class="table-wrap"><table><thead><tr><th>Группа</th><th>Функции</th><th>Контракт</th></tr></thead><tbody><tr><td>Версия и ID</td><td><code>version</code>, <code>id_pack</code>, <code>id_unpack</code></td><td>Скалярные аргументы, 29-битный результат</td></tr><tr><td>CAN frame</td><td><code>crc16</code>, <code>frame_encode</code></td><td>Caller-owned input/output buffers</td></tr><tr><td>CAN parser</td><td><code>parser_size/init/push/stats</code></td><td>Opaque caller-owned context</td></tr><tr><td>GUI frame</td><td><code>gui_crc32</code>, <code>gui_frame_encode</code></td><td>Payload не более 512 байт</td></tr><tr><td>GUI parser</td><td><code>gui_parser_size/init/push/stats</code></td><td>Opaque caller-owned context</td></tr></tbody></table></div>
<div class="grid"><article class="card"><h3>Без malloc</h3><p>Ядро не выделяет память. Python использует <code>create_string_buffer</code>, MCU — static/stack, JNI хранит allocation только в адаптере.</p></article><article class="card"><h3>Без global parser</h3><p>Каждая линия имеет собственный context. Можно одновременно держать COM bridge, GUI transport и несколько устройств.</p></article><article class="card"><h3>Явное владение</h3><p>Один context — один владелец потока. Если владельцев несколько, блокировку добавляет приложение.</p></article></div>
<pre><code>size_t bytes = pcan_abi_parser_size();
void *context = allocate_on_host_or_static_storage(bytes);
pcan_abi_parser_init(context, bytes);
if (pcan_abi_parser_push(context, next_byte, &frame) == 1) {
/* frame полностью проверен */
}</code></pre>
</section>
<section class="panel" id="platforms" role="tabpanel" aria-labelledby="tab-platforms" hidden>
<h2>Матрица переносимости</h2><p class="section-intro">Переносимость ядра и готовность полной упаковки приложения — разные вещи. Здесь они разделены честно.</p>
<div class="table-wrap"><table><thead><tr><th>Цель</th><th>Библиотека</th><th>Адаптер</th><th>Статус</th></tr></thead><tbody><tr><td>Windows</td><td><code>setprotocol.dll</code></td><td>Python ctypes / COM</td><td class="status"><span class="tag ready">проверено</span></td></tr><tr><td>Android</td><td><code>libsetprotocol.so</code></td><td>JNI + Kotlin</td><td class="status"><span class="tag ready">4 ABI</span></td></tr><tr><td>Linux</td><td><code>libsetprotocol.so</code></td><td>ctypes; нужен pyserial/SocketCAN port</td><td class="status"><span class="tag partial">ядро готово</span></td></tr><tr><td>macOS</td><td><code>libsetprotocol.dylib</code></td><td>ctypes/FFI</td><td class="status"><span class="tag partial">исходники готовы</span></td></tr><tr><td>STM32F4</td><td>статический C99</td><td>UART + DMA</td><td class="status"><span class="tag ready">порт есть</span></td></tr><tr><td>Другой MCU</td><td>статический C99</td><td>I/O callbacks</td><td class="status"><span class="tag partial">нужен порт</span></td></tr><tr><td>iOS</td><td>C ABI</td><td>Swift wrapper</td><td class="status"><span class="tag todo">не добавлен</span></td></tr></tbody></table></div>
<div class="callout"><strong>Linux-путь:</strong> собрать <code>libsetprotocol.so</code>, указать <code>SETPROTOCOL_LIBRARY</code>, затем добавить backend физической линии. Для прямого CAN логичен SocketCAN; для USB-COM моста — pyserial.</div>
</section>
<section class="panel" id="build" role="tabpanel" aria-labelledby="tab-build" hidden>
<h2>Сборка и подключение</h2><p class="section-intro">CMake создаёт static core, shared ABI и тесты из одного набора C99-файлов.</p>
<div class="steps"><article class="step"><h3>Зафиксировать templates</h3><p>Подключить репозиторий как Git submodule и зафиксировать проверенный commit.</p></article><article class="step"><h3>Собрать ядро</h3><p>На host — CMake или <code>build_host.py</code>; на Android — NDK; на MCU — добавить исходники в проект.</p></article><article class="step"><h3>Подключить порт</h3><p>COM, SLCAN, SocketCAN, USB или HAL только доставляет данные через узкую границу.</p></article><article class="step"><h3>Прогнать quality gate</h3><p>C tests, ABI vectors, Python native tests, Android build и smoke-test реальной линии.</p></article></div>
<h3 style="margin-top:28px">CMake</h3><pre><code>cmake -S c/set-protocol -B build/setprotocol -DSETP_BUILD_TESTS=ON
cmake --build build/setprotocol --config Release
ctest --test-dir build/setprotocol -C Release --output-on-failure</code></pre>
<h3>Linux host tool</h3><pre><code>python3 c/set-protocol/tools/build_host.py \
--output native/libsetprotocol.so
export SETPROTOCOL_LIBRARY="$PWD/native/libsetprotocol.so"</code></pre>
<h3>Python</h3><pre><code>from protocan.native import NativeProtocol
core = NativeProtocol()
wire = core.encode(1, 1, 0x1234567, b"\xAA\xBB")
frames = core.parser().feed(wire)</code></pre>
</section>
<section class="panel" id="reference" role="tabpanel" aria-labelledby="tab-reference" hidden>
<h2>Полный справочник</h2><p class="section-intro">Этот раздел генерируется из канонического <code>c/set-protocol/docs/SETPROTOCOL.md</code>. Редактировать нужно Markdown, затем запускать <code>doc/build-setprotocol-html.ps1</code>.</p>
<!-- SETPROTOCOL:START -->
<article class="card full-doc" data-source="c/set-protocol/docs/SETPROTOCOL.md">
<h1 id="setprotocol">SETProtocol — переносимое протокольное ядро</h1>
<p>SETProtocol — общее C99-ядро для <code>SETGUI</code>, Android GUI, прошивок и утилит.
Оно объединяет основной SET protocol v2 и поддерживаемые форматы переходного
периода: ProtoCAN bridge и SETGUI transport v1. Windows, Linux, Android и
микроконтроллер используют одинаковые правила кадра, CRC, адресации,
телеметрии, обновления и потокового разбора.</p>
<p>Ядро <strong>не открывает COM-порт, CAN-адаптер или сокет</strong>. COM, SLCAN, SocketCAN,
USB CDC, TCP и аппаратный CAN относятся к портам. Они доставляют байты или
CAN-кадры, а SETProtocol проверяет и интерпретирует их одинаково на всех
платформах.</p>
<h2 id="section">1. Граница ответственности</h2>
<pre><code class="language-text">SETGUI / Android GUI / CLI / firmware
│ прикладные команды и события
Python facade / JNI / прямой C API
│ стабильный ABI или C99 API
┌──────────────────────── SETProtocol ────────────────────────┐
│ SET v2 │ ProtoCAN ID │ v1 parsers │ CRC │ GAS │ telemetry │
└─────────────────────────────────────────────────────────────┘
│ байты или нормализованный CAN frame
COM │ SLCAN │ SocketCAN │ USB CDC │ TCP │ STM32 UART/CAN
</code></pre>
<p>В ядре находятся:</p>
<ul>
<li>форматы проводных кадров и порядок байт;</li>
<li>SET protocol v2: команды, адресация, подписки и firmware state machines;</li>
<li>проверка длины, версии, DLC и контрольной суммы;</li>
<li>восстановление синхронизации после мусора или оборванного кадра;</li>
<li>упаковка и разбор ProtoCAN Extended ID;</li>
<li>счётчики качества входного потока;</li>
<li>общее адресное пространство регистров (GAS);</li>
<li>стабильная C ABI-граница для <code>ctypes</code>, JNI и будущего Swift/FFI.</li>
</ul>
<p>За пределами ядра остаются:</p>
<ul>
<li>поиск устройств и выбор <code>COM6</code>, <code>can0</code> или Bluetooth/USB endpoint;</li>
<li>скорость UART и CAN bitrate;</li>
<li>драйверы SLCAN, SocketCAN, PCAN, CANable и vendor SDK;</li>
<li>разрешения Android USB и жизненный цикл приложения;</li>
<li>виджеты, вкладки, таблицы, графики и хранение настроек;</li>
<li>HAL, IRQ, DMA, RTOS, Flash и распиновка платы.</li>
</ul>
<p>Отсюда следует важное правило: <strong>500000 на экране COM — это baud rate
последовательного моста, а 500 kbit/s в CAN-настройках — bitrate самой CAN-шины.
Ядро не подменяет одно другим и не выбирает эти значения автоматически.</strong></p>
<h2 id="section-1">2. Состав исходников</h2>
<table>
<thead>
<tr>
<th>Модуль</th>
<th>Роль</th>
<th>Платформенные зависимости</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>set_protocol</code></td>
<td>SET v2 frame, CRC32, stream/datagram parser</td>
<td>нет</td>
</tr>
<tr>
<td><code>set_can</code></td>
<td>CAN segmentation, flow control и reassembly</td>
<td>доставка CAN frame и время</td>
</tr>
<tr>
<td><code>set_telemetry</code></td>
<td>подписки и типизированные PUBLISH-пакеты</td>
<td>часы/callbacks приложения</td>
</tr>
<tr>
<td><code>set_firmware</code></td>
<td>BEGIN/DATA/END/STATUS и resume state machine</td>
<td>Flash/verify/reboot backend</td>
</tr>
<tr>
<td><code>pcan_id</code></td>
<td>Упаковка/разбор 29-битного ProtoCAN ID</td>
<td>нет</td>
</tr>
<tr>
<td><code>pcan_crc</code></td>
<td>CRC-16/CCITT-FALSE</td>
<td>нет</td>
</tr>
<tr>
<td><code>pcan_frame</code></td>
<td>Формат <code>AA 55</code>, encode и потоковый parser</td>
<td>нет</td>
</tr>
<tr>
<td><code>gui_frame</code></td>
<td>Формат <code>A5 5A</code>, CRC32, encode, parser и link</td>
<td>нет</td>
</tr>
<tr>
<td><code>pcan_link</code></td>
<td>Экземпляр канала, SEQ, RX/TX и статистика</td>
<td>два callback порта</td>
</tr>
<tr>
<td><code>pcan_ring</code></td>
<td>SPSC-кольцо и непрерывный участок для DMA</td>
<td>нет</td>
</tr>
<tr>
<td><code>pcan_gas</code></td>
<td>Карта 16-битных регистров и bridge к кадрам</td>
<td>callbacks региона</td>
</tr>
<tr>
<td><code>gui_catalog</code></td>
<td>C-каталог публикуемых GUI-полей</td>
<td>нет; входит в общий shared build</td>
</tr>
<tr>
<td><code>pcan_abi</code></td>
<td>Экспорт скалярного ABI для FFI</td>
<td>ABI компилятора C</td>
</tr>
</tbody>
</table>
<p>Общая точка включения для C-кода — <code>include/setprotocol.h</code>.
Иностранные runtimes должны использовать <code>include/setprotocol_abi.h</code>, а не
повторять внутреннюю раскладку <code>pcan_parser_t</code> или <code>gui_parser_t</code>.
Shared-библиотека <code>setprotocol</code> содержит SET v2 и совместимые legacy-модули.
ABI v1 пока экспортирует функции <code>pcan_abi_*</code>: имена намеренно сохранены для
бинарной совместимости SETGUI/Android. Расширение ABI для прямого SET v2 FFI
должно быть совместимым добавлением или новой версией ABI.</p>
<h2 id="wire-format">3. Три поддерживаемых wire format</h2>
<p>SETProtocol поддерживает основной v2 и два legacy-формата. После первого
корректного ответа формат соединения фиксируется до отключения.</p>
<h3 id="can-bridge-aa-55">3.1. CAN bridge: <code>AA 55</code></h3>
<pre><code class="language-text">AA 55 | LEN | SEQ | FLAGS | CAN_ID[4] LE | DATA[0..8] | CRC16 LE
</code></pre>
<p><code>LEN = 6 + DLC</code>, поэтому допустимый диапазон — <code>6..14</code>. CRC-16/CCITT-FALSE
считается по участку от <code>LEN</code> до последнего байта <code>DATA</code>. Максимальный размер
кадра — 19 байт. Формат переносит один classic CAN 2.0 кадр через COM, USB CDC,
RS-232, RS-485 или TCP byte stream.</p>
<p>Флаги:</p>
<table>
<thead>
<tr>
<th style="text-align: right;">Бит</th>
<th>Имя</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">0</td>
<td><code>IDE</code></td>
<td>расширенный 29-битный CAN ID</td>
</tr>
<tr>
<td style="text-align: right;">1</td>
<td><code>RTR</code></td>
<td>remote frame</td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td><code>DIR</code></td>
<td><code>0</code> из CAN в host, <code>1</code> из host в CAN</td>
</tr>
<tr>
<td style="text-align: right;">3</td>
<td><code>ERR</code></td>
<td>служебный кадр диагностики моста</td>
</tr>
</tbody>
</table>
<h3 id="gui-transport-a5-5a">3.2. GUI transport: <code>A5 5A</code></h3>
<pre><code class="language-text">A5 5A | VER | TYPE | SEQ[2] BE | SIZE[2] BE | PAYLOAD[0..512] | CRC32 LE
</code></pre>
<p>Версия сейчас равна <code>1</code>. Заголовочные <code>SEQ</code> и <code>SIZE</code> идут big-endian, CRC32
IEEE — little-endian. Payload до 512 байт нужен для каталога, чтения/записи
регистров, диагностики и потока значений. Это не CAN-кадр и у него нет DLC.</p>
<p>Оба parser принимают произвольные chunks: один вызов может содержать половину
кадра, несколько кадров или мусор между ними. Границы <code>read()</code> не считаются
границами протокольных сообщений.</p>
<h3 id="set-protocol-v2-a5-5a-02">3.3. SET protocol v2: <code>A5 5A 02</code></h3>
<pre><code class="language-text">A5 5A | VER=02 | FLAGS | TYPE u16 LE | SOURCE u16 LE | DEST u16 LE |
SEQ u16 LE | SIZE u16 LE | PAYLOAD[0..512] | CRC32 LE
</code></pre>
<p>Это основной формат новых устройств. Он одинаков поверх RS-232/485, USB CDC,
TCP и UDP; CAN переносит байты полного v2-кадра через сегментацию. В v2
объединены запросы/ответы, события телеметрии и firmware flow. Нормативный
контракт находится в <code>PROTOCOL.md</code>.</p>
<h2 id="protocan-extended-id">4. ProtoCAN Extended ID</h2>
<pre><code class="language-text">28 27 26..24 23..20 19..16 15..0
Priority | Route | DeviceType | DeviceID | MsgType | MsgBody
</code></pre>
<p>Для переносимости используются маски и сдвиги, а не C bit-fields. ABI-функции
<code>pcan_abi_id_pack()</code> и <code>pcan_abi_id_unpack()</code> дают одинаковую раскладку при
MSVC, GCC и Clang.</p>
<h2 id="abi-v1">5. Стабильный ABI v1</h2>
<p><code>pcan_abi.h</code> экспортирует простые числа, указатели и явно ограниченные буферы.
Текущая версия возвращается <code>pcan_abi_version()</code> и равна <code>1</code>.</p>
<h3 id="can-bridge-api">CAN bridge API</h3>
<table>
<thead>
<tr>
<th>Функция</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>pcan_abi_version</code></td>
<td>Проверить совместимость загруженной библиотеки</td>
</tr>
<tr>
<td><code>pcan_abi_id_pack/unpack</code></td>
<td>Преобразовать поля ProtoCAN ID</td>
</tr>
<tr>
<td><code>pcan_abi_crc16</code></td>
<td>Рассчитать CRC-16/CCITT-FALSE</td>
</tr>
<tr>
<td><code>pcan_abi_frame_encode</code></td>
<td>Собрать целый <code>AA55</code> кадр</td>
</tr>
<tr>
<td><code>pcan_abi_parser_size</code></td>
<td>Узнать размер opaque parser context</td>
</tr>
<tr>
<td><code>pcan_abi_parser_init</code></td>
<td>Инициализировать память, принадлежащую вызывающему</td>
</tr>
<tr>
<td><code>pcan_abi_parser_push</code></td>
<td>Передать один байт; <code>1</code> означает готовый кадр</td>
</tr>
<tr>
<td><code>pcan_abi_parser_stats</code></td>
<td>Получить frames/CRC/bad length/stray bytes</td>
</tr>
</tbody>
</table>
<h3 id="gui-api">GUI API</h3>
<table>
<thead>
<tr>
<th>Функция</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>pcan_abi_gui_crc32</code></td>
<td>Рассчитать CRC32 IEEE</td>
</tr>
<tr>
<td><code>pcan_abi_gui_frame_encode</code></td>
<td>Собрать целый <code>A55A</code> кадр</td>
</tr>
<tr>
<td><code>pcan_abi_gui_parser_size/init/push</code></td>
<td>Управлять opaque GUI parser context</td>
</tr>
<tr>
<td><code>pcan_abi_gui_parser_stats</code></td>
<td>Получить frames/CRC/version/length/stray bytes</td>
</tr>
</tbody>
</table>
<p>Возврат <code>0</code> из encode означает неверные аргументы или недостаточный output
buffer. Parser API возвращает отрицательное значение при неверном context,
<code>0</code> пока кадр не собран и <code>1</code> при готовом кадре.</p>
<h2 id="section-2">6. Память, состояние и многопоточность</h2>
<p>В переносимом C-слое нет <code>malloc</code>, singleton и скрытого глобального parser.
Каждый канал имеет собственное состояние. В ABI вызывающий сначала спрашивает
его размер, выделяет байтовый блок и передаёт его в <code>init</code>.</p>
<pre><code class="language-c">size_t size = pcan_abi_parser_size();
void *storage = /* память вызывающей стороны размером size */;
pcan_abi_parser_init(storage, size);
</code></pre>
<p>Это позволяет:</p>
<ul>
<li>держать память статически на MCU;</li>
<li>использовать <code>ctypes.create_string_buffer()</code> в Python;</li>
<li>выделять handle только в JNI-адаптере;</li>
<li>одновременно разбирать несколько независимых линий.</li>
</ul>
<p>Один parser context нельзя одновременно изменять из нескольких потоков.
Правильная модель — один владелец на канал или внешняя блокировка. Кольцевой
буфер рассчитан на одного писателя и одного читателя (SPSC). Для нескольких
писателей синхронизацию обеспечивает порт/приложение.</p>
<h2 id="section-3">7. Порты и адаптеры</h2>
<table>
<thead>
<tr>
<th>Среда</th>
<th>Артефакт</th>
<th>Состояние</th>
</tr>
</thead>
<tbody>
<tr>
<td>Windows desktop</td>
<td><code>setprotocol.dll</code> + Python <code>ctypes</code></td>
<td>используется SETGUI, проверено тестами</td>
</tr>
<tr>
<td>Android</td>
<td><code>libsetprotocol.so</code> + JNI + Kotlin facade</td>
<td>сборка ABI <code>arm64-v8a</code>, <code>armeabi-v7a</code>, <code>x86</code>, <code>x86_64</code> проверяется Android build</td>
</tr>
<tr>
<td>Linux desktop</td>
<td><code>libsetprotocol.so</code> + тот же ABI</td>
<td>ядро и сборщик готовы; нужен Linux CI/smoke-test приложения</td>
</tr>
<tr>
<td>macOS</td>
<td><code>libsetprotocol.dylib</code> + тот же ABI</td>
<td>исходники совместимы; отдельная упаковка не проверена</td>
</tr>
<tr>
<td>STM32F4</td>
<td>прямой C99 + UART/DMA port</td>
<td>готовый порт в <code>ports/stm32f4</code></td>
</tr>
<tr>
<td>Другой MCU</td>
<td>прямой C99</td>
<td>реализуются только callbacks I/O/времени/памяти</td>
</tr>
<tr>
<td>iOS/Swift</td>
<td>C ABI</td>
<td>ABI подходит, Swift wrapper пока не добавлен</td>
</tr>
</tbody>
</table>
<h3 id="linux">Linux</h3>
<p>Само ядро не содержит WinAPI, поэтому собирается GCC или Clang. Для SETGUI под
Linux остаются две отдельные задачи: упаковать <code>libsetprotocol.so</code> с приложением и
подключить нужный физический backend (<code>pyserial</code> для USB-COM или SocketCAN для
<code>can0</code>). Правила кадра, CRC и ID менять не потребуется.</p>
<p>SLCAN и SocketCAN — <strong>порты снифера</strong>, а не новая реализация протокола:</p>
<pre><code class="language-text">SLCAN text / struct can_frame
│ adapter
can_id + flags + data
общий decoder/UI
</code></pre>
<h2 id="section-4">8. Сборка</h2>
<h3 id="cmake-windows-linux-macos">CMake: Windows, Linux, macOS</h3>
<pre><code class="language-bash">cmake -S c/set-protocol -B build/setprotocol -DSETP_BUILD_TESTS=ON
cmake --build build/setprotocol --config Release
ctest --test-dir build/setprotocol -C Release --output-on-failure
</code></pre>
<p>Результат shared-сборки называется <code>setprotocol.dll</code>, <code>libsetprotocol.so</code> или
<code>libsetprotocol.dylib</code>. Статическая цель называется <code>setprotocol_static</code>;
совместимое имя CMake-цели SET v2 — <code>set_protocol</code>.</p>
<h3 id="host">Упрощённая host-сборка</h3>
<pre><code class="language-powershell">python c/set-protocol/tools/build_host.py --output native/setprotocol.dll
</code></pre>
<pre><code class="language-bash">python3 c/set-protocol/tools/build_host.py --output native/libsetprotocol.so
</code></pre>
<p>На Windows tool использует MSVC, на Unix ищет <code>cc</code>, <code>clang</code> или <code>gcc</code>.</p>
<h3 id="python">Python</h3>
<pre><code class="language-python">from protocan.native import NativeProtocol
core = NativeProtocol()
raw = core.encode(sequence=1, flags=1, can_id=0x1234567, data=b&quot;\xAA\xBB&quot;)
frames = core.parser().feed(raw)
</code></pre>
<p>Если библиотека лежит вне стандартного дерева:</p>
<pre><code class="language-bash">export SETPROTOCOL_LIBRARY=/opt/set/lib/libsetprotocol.so
</code></pre>
<p>В PowerShell:</p>
<pre><code class="language-powershell">$env:SETPROTOCOL_LIBRARY = 'C:\set\native\setprotocol.dll'
</code></pre>
<h3 id="android">Android</h3>
<p><code>ports/android/Android.mk</code> компилирует те же C-файлы. Kotlin-класс
<code>ru.setcorp.setprotocol.NativeSetProtocol</code> отвечает только за удобный API, а JNI — за
преобразование типов и время жизни parser handle.</p>
<h3 id="section-5">Микроконтроллер</h3>
<p>Добавьте нужные <code>src/*.c</code> и каталог <code>include/</code> в проект. Порт STM32F4 не входит
автоматически в host CMake, потому что ему нужен CMSIS. Для другой платы
реализуйте <code>pcan_io_t.write</code> и <code>pcan_io_t.tx_space</code>; ISR/DMA лишь складывает
байты, а <code>pcan_link_feed()</code> вызывается в безопасном контексте приложения.</p>
<h2 id="abi">9. Пример прямого ABI</h2>
<pre><code class="language-c">#include &quot;pcan_abi.h&quot;
uint8_t output[19];
const uint8_t data[] = {0xAA, 0xBB};
const uint8_t flags = 0x01U; /* IDE */
uint32_t id = pcan_abi_id_pack(1, 0, 2, 3, 4, 0x1234);
size_t written = pcan_abi_frame_encode(
7, flags, id, data, sizeof(data), output, sizeof(output));
</code></pre>
<p>Для firmware удобнее полный C API из <code>protocan_transport.h</code>: он даёт link,
callbacks, ring и GAS без FFI-обёртки.</p>
<h2 id="section-6">10. Диагностика</h2>
<table>
<thead>
<tr>
<th>Симптом</th>
<th>Что проверить</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>crc_errors</code> растёт</td>
<td>bitrate/baud, ground, termination, порядок байт, потерю chunks</td>
</tr>
<tr>
<td><code>bad_len</code>/<code>length_errors</code></td>
<td>выбран ли правильный формат <code>AA55</code> или <code>A55A</code></td>
</tr>
<tr>
<td><code>version_errors</code></td>
<td>версия GUI transport должна быть <code>1</code></td>
</tr>
<tr>
<td>много <code>stray_bytes</code></td>
<td>начало чтения посреди пакета допустимо; постоянный рост означает неверный порт</td>
</tr>
<tr>
<td>DLL/SO не найдена</td>
<td>путь, архитектуру процесса и <code>SETPROTOCOL_LIBRARY</code></td>
</tr>
<tr>
<td>Android <code>UnsatisfiedLinkError</code></td>
<td>имя <code>setprotocol</code>, ABI устройства и упаковку <code>jniLibs</code>/NDK</td>
</tr>
<tr>
<td>CAN пустой, но COM открыт</td>
<td>COM baud не равен CAN bitrate; проверьте настройку самого адаптера</td>
</tr>
</tbody>
</table>
<h2 id="section-7">11. Совместимость и ограничения</h2>
<ul>
<li>ABI v1 изменяется только совместимым добавлением функций. Ломающее изменение
требует нового значения <code>PCAN_ABI_VERSION</code>.</li>
<li>Wire format нельзя менять без версии/миграционного документа и тестовых
векторов для C, Python и Android.</li>
<li>CAN bridge сейчас рассчитан на classic CAN: <code>DLC &lt;= 8</code>; CAN FD не включён.</li>
<li>GUI payload ограничен 512 байтами; на MCU <code>GUI_RX_PAYLOAD_MAX</code> можно уменьшить.</li>
<li>В ядре нет готового SocketCAN/SLCAN/vendor backend: это следующий слой портов.</li>
<li>В ядро не входят виджеты GUI, настройки COM/CAN и обновление прошивки целиком.
Для firmware flow существует отдельная библиотека <code>protocan-boot</code>.</li>
</ul>
<h2 id="section-8">12. Проверка изменений</h2>
<p>Минимальный quality gate:</p>
<ol>
<li>CMake build и <code>ctest</code> для <code>test_transport</code> и <code>test_abi</code>.</li>
<li>Сверка машинных векторов <code>tests/vectors/test-vectors.json</code>.</li>
<li>Python-тесты с обязательной загрузкой native core.</li>
<li>Android unit tests и <code>assembleDebug</code>, если менялись ABI/JNI/Kotlin.</li>
<li>Smoke-test целевого порта на реальной линии.</li>
</ol>
<p>Канонический код находится в <code>templates/c/set-protocol</code>. Проекты должны
получать его как Git submodule и фиксировать конкретный commit, а не хранить
разошедшиеся копии.</p>
</article>
<!-- SETPROTOCOL:END -->
</section>
</main>
<footer><div class="wrap">SETProtocol source of truth: <a href="../c/set-protocol/README.md">README</a> · <a href="../c/set-protocol/include/setprotocol_abi.h">ABI header</a> · <a href="../c/set-protocol/docs/SETPROTOCOL.md">Markdown</a></div></footer>
<script>
const tabs=[...document.querySelectorAll('[role=tab]')];
const panels=[...document.querySelectorAll('[role=tabpanel]')];
function show(id,updateHash=true){
const tab=tabs.find(item=>item.getAttribute('aria-controls')===id)||tabs[0];
tabs.forEach(item=>item.setAttribute('aria-selected',String(item===tab)));
panels.forEach(panel=>panel.hidden=panel.id!==tab.getAttribute('aria-controls'));
if(updateHash) history.replaceState(null,'','#'+tab.getAttribute('aria-controls'));
tab.focus({preventScroll:true});
}
tabs.forEach((tab,index)=>{
tab.addEventListener('click',()=>show(tab.getAttribute('aria-controls')));
tab.addEventListener('keydown',event=>{
if(event.key==='ArrowRight'){event.preventDefault();show(tabs[(index+1)%tabs.length].getAttribute('aria-controls'));}
if(event.key==='ArrowLeft'){event.preventDefault();show(tabs[(index-1+tabs.length)%tabs.length].getAttribute('aria-controls'));}
if(event.key==='Home'){event.preventDefault();show(tabs[0].getAttribute('aria-controls'));}
if(event.key==='End'){event.preventDefault();show(tabs[tabs.length-1].getAttribute('aria-controls'));}
});
});
if(location.hash) show(location.hash.slice(1),false);
document.querySelectorAll('pre').forEach(pre=>{
const button=document.createElement('button');button.className='copy';button.type='button';button.textContent='Копировать';
button.addEventListener('click',async()=>{const code=pre.querySelector('code');await navigator.clipboard.writeText(code?code.innerText:pre.innerText);button.textContent='Готово';setTimeout(()=>button.textContent='Копировать',1200);});
pre.appendChild(button);
});
</script>
</body>
</html>