613 lines
47 KiB
HTML
613 lines
47 KiB
HTML
<!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/<platform></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"\xAA\xBB")
|
||
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 "pcan_abi.h"
|
||
|
||
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 <= 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>
|