Files
templates/doc/setprotocol.html

1085 lines
66 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{max-width:100%;overflow:auto}.full-doc .table-wrap table{min-width:720px}.full-doc .table-wrap th,.full-doc .table-wrap td{overflow-wrap:normal;word-break:normal}.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="can-frames" id="tab-can-frames">CAN v1 / v2</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="can-frames" role="tabpanel" aria-labelledby="tab-can-frames" hidden>
<h2>Разбор CAN-кадров v1 и v2</h2><p class="section-intro">Раздел генерируется из <code>doc/CAN_FRAME_PARSE_V1_V2.md</code>. В нём собраны wire-разметка, примеры и готовые Python-парсеры.</p>
<!-- CAN-FRAME-PARSE:START -->
<article class="card full-doc" data-source="doc/CAN_FRAME_PARSE_V1_V2.md">
<h1 id="can-protocan-boot-v1-setprotocol-v2">Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2</h1>
<p>Документ описывает wire-форматы двух протоколов обновления прошивки:</p>
<ul>
<li><strong>v1</strong><code>templates/c/protocan-boot</code>, одна команда или 8 байт образа в одном
Extended CAN-кадре;</li>
<li><strong>v2</strong><code>templates/c/set-protocol</code>, полный кадр SETProtocol разбивается на
несколько Extended CAN-кадров.</li>
</ul>
<p>Все многобайтные поля payload передаются <strong>little-endian</strong>. CAN ID — 29-битный.
Для рабочего кода нужно использовать канонические реализации из <code>templates</code>,
а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.</p>
<h2 id="protocan-boot-v1">1. ProtoCAN Boot v1</h2>
<h3 id="extended-can-id">1.1. Разметка Extended CAN ID</h3>
<pre><code class="language-text">bits size field
28 1 Priority
27 1 Route: 0 = host -&gt; device, 1 = device -&gt; host
26..24 3 Device Type
23..20 4 Device ID
19..16 4 Message Type
15..0 16 Message Body
</code></pre>
<p>Формула:</p>
<pre><code class="language-text">ID = Priority &lt;&lt; 28 |
Route &lt;&lt; 27 |
DeviceType &lt;&lt; 24 |
DeviceID &lt;&lt; 20 |
MessageType &lt;&lt; 16 |
MessageBody
</code></pre>
<p>Типы загрузочных сообщений:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th style="text-align: right;">Message Type</th>
<th>Имя</th>
<th>Message Body</th>
<th>CAN payload</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x9</code></td>
<td><code>BOOT_CONTROL</code></td>
<td><code>SessionID &lt;&lt; 8 \| Command</code></td>
<td>параметры команды</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xA</code></td>
<td><code>BOOT_DATA_A</code></td>
<td>индекс блока</td>
<td>8 байт слота A</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xB</code></td>
<td><code>BOOT_DATA_B</code></td>
<td>индекс блока</td>
<td>8 байт слота B</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xC</code></td>
<td><code>BOOT_STATUS</code></td>
<td><code>SessionID &lt;&lt; 8 \| Command</code></td>
<td>статус и прогресс</td>
</tr>
<tr>
<td style="text-align: right;"><code>0xD</code></td>
<td><code>BOOT_DISCOVERY</code></td>
<td>подтип</td>
<td>информация об устройстве</td>
</tr>
</tbody>
</table></div>
<p>Команды <code>BOOT_CONTROL</code>:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th style="text-align: right;">Код</th>
<th>Команда</th>
<th>Payload</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td><code>IDENTIFY</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td><code>ENTER_BOOT</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x03</code></td>
<td><code>BEGIN_IMAGE</code></td>
<td><code>image_size u32</code>, <code>image_crc32 u32</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td><code>BEGIN_COMPAT</code></td>
<td><code>product u16</code>, <code>hw_min u8</code>, <code>hw_max u8</code>, <code>version u32</code></td>
</tr>
<tr>
<td style="text-align: right;"><code>0x05</code></td>
<td><code>ERASE</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x06</code></td>
<td><code>VERIFY</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x07</code></td>
<td><code>COMMIT</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td><code>CONFIRM</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x09</code></td>
<td><code>REBOOT</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0A</code></td>
<td><code>ABORT</code></td>
<td>пустой</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x0B</code></td>
<td><code>QUERY_PROGRESS</code></td>
<td>пустой</td>
</tr>
</tbody>
</table></div>
<p><code>BOOT_STATUS</code> всегда содержит 8 байт:</p>
<pre><code class="language-text">offset size field
0 1 status
1 1 target_slot
2 2 next_block u16 LE
4 4 running_crc32 u32 LE
</code></pre>
<p><code>BOOT_DISCOVERY</code> с body <code>1</code> содержит:</p>
<pre><code class="language-text">offset size field
0 2 product_type u16 LE
2 1 hardware_revision
3 1 protocol_version = 1
4 4 firmware_version u32 LE
</code></pre>
<p>Пример запроса <code>IDENTIFY</code> для <code>DeviceType=7</code>, <code>DeviceID=13</code>:</p>
<pre><code class="language-text">CAN ID: 17D90001
DLC: 0
</code></pre>
<h3 id="python-v1">1.2. Python-парсер v1</h3>
<pre><code class="language-python">def parse_v1(can_id: int, data: bytes) -&gt; dict:
if not 0 &lt;= can_id &lt;= 0x1FFFFFFF:
raise ValueError(&quot;неверный Extended CAN ID&quot;)
if len(data) &gt; 8:
raise ValueError(&quot;DLC больше 8&quot;)
result = {
&quot;version&quot;: 1,
&quot;priority&quot;: (can_id &gt;&gt; 28) &amp; 0x01,
&quot;route&quot;: (can_id &gt;&gt; 27) &amp; 0x01,
&quot;device_type&quot;: (can_id &gt;&gt; 24) &amp; 0x07,
&quot;device_id&quot;: (can_id &gt;&gt; 20) &amp; 0x0F,
&quot;message_type&quot;: (can_id &gt;&gt; 16) &amp; 0x0F,
&quot;message_body&quot;: can_id &amp; 0xFFFF,
&quot;data&quot;: bytes(data),
}
msg_type = result[&quot;message_type&quot;]
body = result[&quot;message_body&quot;]
if msg_type in (0x9, 0xC):
result[&quot;session_id&quot;] = (body &gt;&gt; 8) &amp; 0xFF
result[&quot;command&quot;] = body &amp; 0xFF
elif msg_type in (0xA, 0xB):
result[&quot;slot&quot;] = msg_type - 0xA
result[&quot;block_index&quot;] = body
if msg_type == 0xC:
if len(data) != 8:
raise ValueError(&quot;BOOT_STATUS должен содержать 8 байт&quot;)
result.update({
&quot;status&quot;: data[0],
&quot;target_slot&quot;: data[1],
&quot;next_block&quot;: int.from_bytes(data[2:4], &quot;little&quot;),
&quot;running_crc32&quot;: int.from_bytes(data[4:8], &quot;little&quot;),
})
elif msg_type == 0xD and body == 1:
if len(data) != 8:
raise ValueError(&quot;BOOT_DISCOVERY должен содержать 8 байт&quot;)
result.update({
&quot;product_type&quot;: int.from_bytes(data[0:2], &quot;little&quot;),
&quot;hardware_revision&quot;: data[2],
&quot;protocol_version&quot;: data[3],
&quot;firmware_version&quot;: int.from_bytes(data[4:8], &quot;little&quot;),
})
return result
</code></pre>
<h2 id="setprotocol-v2-classic-can">2. SETProtocol v2 поверх classic CAN</h2>
<p>В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.</p>
<h3 id="extended-can-id-1">2.1. Разметка Extended CAN ID</h3>
<pre><code class="language-text">bits size field
28..24 5 Prefix = 0x12
23..16 8 Destination node
15..8 8 Source node
7 1 Priority
6..0 7 Channel
</code></pre>
<p>Формула:</p>
<pre><code class="language-text">ID = 0x12 &lt;&lt; 24 |
Destination &lt;&lt; 16 |
Source &lt;&lt; 8 |
Priority &lt;&lt; 7 |
Channel
</code></pre>
<h3 id="can">2.2. CAN-сегменты</h3>
<p>Первый байт CAN payload — PCI:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th style="text-align: right;">PCI</th>
<th>Назначение</th>
<th>Формат CAN payload</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x10</code></td>
<td>первый сегмент</td>
<td><code>10</code>, <code>total_length u16 LE</code>, первые 5 байт SETP</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x20..0x2F</code></td>
<td>продолжение</td>
<td><code>2N</code>, следующие 17 байт SETP</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x30..0x32</code></td>
<td>flow control</td>
<td><code>3S</code>, <code>block_size</code>, <code>st_min_ms</code></td>
</tr>
</tbody>
</table></div>
<p><code>N</code> — циклический номер сегмента <code>1..15,0..</code>; следующий сегмент обязан иметь
ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.</p>
<h3 id="setprotocol-v2">2.3. Внутренний кадр SETProtocol v2</h3>
<pre><code class="language-text">offset size field
0 2 SOF = A5 5A
2 1 version = 02
3 1 flags
4 2 message_type u16 LE
6 2 source u16 LE
8 2 destination u16 LE
10 2 sequence u16 LE
12 2 payload_length u16 LE
14 N payload
14+N 4 CRC32 IEEE u32 LE
</code></pre>
<p>CRC32 считается по байтам от <code>version</code> на offset 2 до конца payload. Поля
<code>source</code>, <code>destination</code> и <code>priority</code> внутреннего заголовка должны совпадать с
CAN ID.</p>
<p>Флаги:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th style="text-align: right;">Бит</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><code>0x01</code></td>
<td>RESPONSE</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x02</code></td>
<td>EVENT</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x04</code></td>
<td>ERROR</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x08</code></td>
<td>ACK_REQUIRED</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x10</code></td>
<td>MORE</td>
</tr>
<tr>
<td style="text-align: right;"><code>0x20</code></td>
<td>PRIORITY</td>
</tr>
</tbody>
</table></div>
<p>Каждый response начинается с <code>status u16 LE</code>. Основные firmware message types:
<code>FW_BEGIN=0x0100</code>, <code>FW_DATA=0x0101</code>, <code>FW_END=0x0102</code>, <code>FW_ABORT=0x0103</code>,
<code>FW_STATUS=0x0104</code>, <code>FW_ACTIVATE=0x0105</code>.</p>
<p>Пример <code>PING</code> к BALZAM node <code>13</code>, source <code>0</code>, sequence <code>1</code>, priority <code>1</code>,
channel <code>1</code>:</p>
<pre><code class="language-text">Полный SETP:
A5 5A 02 28 01 00 00 00 0D 00 01 00 00 00 E7 29 51 40
CAN ID 120D0081, сегменты:
10 12 00 A5 5A 02 28 01
21 00 00 00 0D 00 01 00
22 00 00 E7 29 51 40
</code></pre>
<h3 id="python-v2">2.4. Python-парсер и сборщик v2</h3>
<pre><code class="language-python">import binascii
def parse_v2_can_id(can_id: int) -&gt; dict:
if not 0 &lt;= can_id &lt;= 0x1FFFFFFF:
raise ValueError(&quot;неверный Extended CAN ID&quot;)
if (can_id &gt;&gt; 24) &amp; 0x1F != 0x12:
raise ValueError(&quot;не SETProtocol v2 CAN ID&quot;)
return {
&quot;destination&quot;: (can_id &gt;&gt; 16) &amp; 0xFF,
&quot;source&quot;: (can_id &gt;&gt; 8) &amp; 0xFF,
&quot;priority&quot;: (can_id &gt;&gt; 7) &amp; 0x01,
&quot;channel&quot;: can_id &amp; 0x7F,
}
def parse_setp(packet: bytes, can_id: int) -&gt; dict:
if len(packet) &lt; 18 or packet[:2] != b&quot;\xA5\x5A&quot;:
raise ValueError(&quot;нет полного SETP-кадра&quot;)
if packet[2] != 2:
raise ValueError(&quot;неподдерживаемая версия SETP&quot;)
flags = packet[3]
if flags &amp; 0xC0:
raise ValueError(&quot;установлены зарезервированные флаги&quot;)
payload_length = int.from_bytes(packet[12:14], &quot;little&quot;)
if len(packet) != 14 + payload_length + 4:
raise ValueError(&quot;не совпадает payload_length&quot;)
expected_crc = int.from_bytes(packet[-4:], &quot;little&quot;)
actual_crc = binascii.crc32(packet[2:-4]) &amp; 0xFFFFFFFF
if actual_crc != expected_crc:
raise ValueError(&quot;ошибка CRC32 SETP&quot;)
address = parse_v2_can_id(can_id)
source = int.from_bytes(packet[6:8], &quot;little&quot;)
destination = int.from_bytes(packet[8:10], &quot;little&quot;)
priority = int(bool(flags &amp; 0x20))
if (source, destination, priority) != (
address[&quot;source&quot;], address[&quot;destination&quot;], address[&quot;priority&quot;]
):
raise ValueError(&quot;SETP header не совпадает с CAN ID&quot;)
payload = packet[14:-4]
result = {
&quot;version&quot;: 2,
&quot;flags&quot;: flags,
&quot;message_type&quot;: int.from_bytes(packet[4:6], &quot;little&quot;),
&quot;source&quot;: source,
&quot;destination&quot;: destination,
&quot;sequence&quot;: int.from_bytes(packet[10:12], &quot;little&quot;),
&quot;payload&quot;: payload,
&quot;can&quot;: address,
}
if flags &amp; 0x01:
if len(payload) &lt; 2:
raise ValueError(&quot;response не содержит status&quot;)
result[&quot;status&quot;] = int.from_bytes(payload[:2], &quot;little&quot;)
result[&quot;body&quot;] = payload[2:]
return result
class V2CanReassembler:
def __init__(self, timeout_ms: int = 500):
self.timeout_ms = timeout_ms
self.reset()
def reset(self):
self.can_id = None
self.total = 0
self.data = bytearray()
self.next_sequence = 1
self.deadline_ms = 0
def feed(self, can_id: int, data: bytes, now_ms: int):
parse_v2_can_id(can_id)
if not 1 &lt;= len(data) &lt;= 8:
raise ValueError(&quot;DLC вне диапазона 1..8&quot;)
if self.can_id is not None and now_ms &gt;= self.deadline_ms:
self.reset()
raise ValueError(&quot;тайм-аут сборки SETP&quot;)
pci_type = data[0] &amp; 0xF0
if pci_type == 0x10:
if len(data) != 8:
raise ValueError(&quot;первый сегмент должен иметь DLC 8&quot;)
total = int.from_bytes(data[1:3], &quot;little&quot;)
if not 18 &lt;= total &lt;= 530:
raise ValueError(&quot;неверный размер SETP&quot;)
self.can_id = can_id
self.total = total
self.data = bytearray(data[3:])
self.next_sequence = 1
self.deadline_ms = now_ms + self.timeout_ms
return None
if pci_type == 0x20:
sequence = data[0] &amp; 0x0F
if (
self.can_id is None
or can_id != self.can_id
or sequence != self.next_sequence
or len(data) &lt; 2
):
self.reset()
raise ValueError(&quot;ошибка последовательности CAN-сегментов&quot;)
if len(data) - 1 &gt; self.total - len(self.data):
self.reset()
raise ValueError(&quot;лишние байты CAN-сегмента&quot;)
self.data.extend(data[1:])
self.next_sequence = (self.next_sequence + 1) &amp; 0x0F
self.deadline_ms = now_ms + self.timeout_ms
if len(self.data) == self.total:
packet = bytes(self.data)
packet_can_id = self.can_id
self.reset()
return parse_setp(packet, packet_can_id)
return None
if pci_type == 0x30:
return {&quot;flow_control&quot;: data[0] &amp; 0x0F, &quot;data&quot;: data[1:]}
raise ValueError(&quot;неизвестный PCI&quot;)
</code></pre>
<p>В SETGUI эти операции уже реализованы в
<code>third_party/templates/python/setprotocol/can.py</code>; собственный parser нужен
только внешнему анализатору или диагностическому скрипту.</p>
<h2 id="v1-v2">3. Как отличать v1 от v2</h2>
<p>Для используемых сейчас адресов достаточно следующих признаков:</p>
<ul>
<li>v2: верхние пять бит CAN ID равны <code>0x12</code>, PCI начинается с <code>0x10</code>, <code>0x2N</code>
или <code>0x3S</code>, после reassembly присутствует <code>A5 5A 02</code>;</li>
<li>v1: <code>MessageType</code> в битах <code>19..16</code> равен <code>0x9..0xD</code>, каждый кадр разбирается
самостоятельно.</li>
</ul>
<p>Однако универсальное автоопределение только по одному CAN ID невозможно:
комбинация <code>Priority/Route/DeviceType</code> v1 теоретически тоже может дать верхнее
поле <code>0x12</code>, а первый байт firmware data v1 может случайно совпасть с PCI.
Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
только после сборки кадра с корректными <code>A5 5A 02</code>, длиной и CRC32.</p>
<h2 id="section">4. Канонические исходники</h2>
<ul>
<li>v1 ID и state machine: <code>third_party/templates/c/protocan-boot/src/pcan_boot.c</code>;</li>
<li>v2 CAN transport: <code>third_party/templates/c/set-protocol/src/set_can.c</code>;</li>
<li>v2 frame/CRC: <code>third_party/templates/c/set-protocol/src/set_protocol.c</code>;</li>
<li>v2 firmware payload: <code>third_party/templates/c/set-protocol/src/set_firmware.c</code>;</li>
<li>Python v2 CAN: <code>third_party/templates/python/setprotocol/can.py</code>.</li>
</ul>
</article>
<!-- CAN-FRAME-PARSE:END -->
</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>
<div class="table-wrap"><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></div>
<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>
<div class="table-wrap"><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></div>
<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>
<div class="table-wrap"><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></div>
<h3 id="gui-api">GUI API</h3>
<div class="table-wrap"><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></div>
<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>
<div class="table-wrap"><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></div>
<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>
<div class="table-wrap"><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></div>
<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>