Files
SETCAN/doc/index.html

318 lines
27 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="Интерактивная документация SETCAN: архитектура, API ProtoCAN и перенос в STM32-проект.">
<title>SETCAN · Руководство разработчика</title>
<style>
:root {
color-scheme: dark;
--bg: #07100f; --panel: #0d1917; --panel-2: #12211e; --line: #263b36;
--text: #e7f2ee; --muted: #96aaa4; --mint: #4ee0aa; --cyan: #62cbe8;
--amber: #ffca6a; --red: #ff887d; --code: #091412; --radius: 16px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; min-height: 100vh; font: 15px/1.6 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--text); background: radial-gradient(circle at 82% -10%, #183d34 0, transparent 34rem), var(--bg); }
button, a { -webkit-tap-highlight-color: transparent; }
a { color: var(--cyan); text-decoration: none; }
a:hover { text-decoration: underline; }
.shell { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 42px 0 64px; }
.eyebrow { color: var(--mint); font: 700 12px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .14em; text-transform: uppercase; }
h1 { max-width: 800px; margin: 10px 0 12px; font-size: clamp(38px, 7vw, 72px); line-height: .98; letter-spacing: -.055em; }
.lead { max-width: 760px; margin: 0; color: var(--muted); font-size: 18px; }
.hero { padding: 34px 0 30px; }
.stats { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }
.stat { padding: 8px 12px; border: 1px solid var(--line); border-radius: 999px; background: #0b1714cc; color: var(--muted); font-size: 13px; }
.stat b { color: var(--text); }
.tabs-wrap { position: sticky; top: 0; z-index: 5; margin: 0 -10px 28px; padding: 10px; overflow-x: auto; background: linear-gradient(var(--bg) 70%, transparent); }
.tabs { display: flex; width: max-content; min-width: 100%; gap: 6px; padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: #0b1614ed; backdrop-filter: blur(14px); }
.tab { flex: 1; min-width: 130px; border: 0; border-radius: 9px; padding: 10px 16px; color: var(--muted); background: transparent; cursor: pointer; font: 650 14px/1 system-ui; }
.tab:hover { color: var(--text); background: var(--panel-2); }
.tab[aria-selected="true"] { color: #06110e; background: var(--mint); }
.panel { display: none; animation: enter .24s ease-out; }
.panel.active { display: block; }
@keyframes enter { from { opacity: 0; transform: translateY(5px); } }
h2 { margin: 0 0 8px; font-size: clamp(27px, 4vw, 38px); letter-spacing: -.035em; }
h3 { margin: 0 0 8px; font-size: 19px; letter-spacing: -.015em; }
p { margin: 0 0 14px; }
.section-intro { max-width: 760px; margin-bottom: 24px; color: var(--muted); }
.grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 14px; }
.card { grid-column: span 4; min-width: 0; padding: 21px; border: 1px solid var(--line); border-radius: var(--radius); background: linear-gradient(145deg, #10201c, #0b1715); }
.card.wide { grid-column: span 8; }
.card.full { grid-column: 1 / -1; }
.card p, .card li { color: var(--muted); }
.card strong { color: var(--text); }
.tag { display: inline-block; margin-bottom: 14px; padding: 3px 8px; border-radius: 6px; color: var(--mint); background: #173d31; font: 700 11px/1.5 ui-monospace, monospace; text-transform: uppercase; }
.tag.amber { color: var(--amber); background: #3a2d16; }
.tag.blue { color: var(--cyan); background: #15343b; }
.filetree, pre { margin: 14px 0 0; border: 1px solid var(--line); border-radius: 12px; background: var(--code); color: #cce0da; font: 13px/1.65 ui-monospace, SFMono-Regular, Consolas, monospace; }
.filetree { padding: 17px; white-space: pre-wrap; }
pre { position: relative; overflow: auto; padding: 44px 18px 18px; }
code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
:not(pre) > code { padding: 2px 6px; border-radius: 5px; color: #d5eee5; background: #172824; }
.copy { position: absolute; top: 9px; right: 9px; border: 1px solid var(--line); border-radius: 7px; padding: 6px 9px; color: var(--muted); background: var(--panel-2); cursor: pointer; font-size: 12px; }
.copy:hover { color: var(--text); border-color: var(--mint); }
ul, ol { margin: 10px 0 0; padding-left: 20px; }
li + li { margin-top: 7px; }
.flow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 18px; }
.step { position: relative; padding: 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); }
.step b { display: block; color: var(--mint); font-size: 13px; }
.step span { color: var(--muted); font-size: 13px; }
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 650px; }
th, td { padding: 11px 13px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
td:first-child { color: var(--mint); font-family: ui-monospace, monospace; }
.callout { margin-top: 16px; padding: 15px 17px; border-left: 3px solid var(--amber); border-radius: 4px 12px 12px 4px; color: #dfd2b4; background: #251f14; }
.checklist { list-style: none; padding: 0; }
.checklist li { position: relative; padding-left: 28px; }
.checklist li::before { content: "✓"; position: absolute; left: 0; color: var(--mint); font-weight: 800; }
.docs { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.doclink { display: inline-flex; align-items: center; gap: 8px; padding: 9px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); }
.doclink:hover { border-color: var(--cyan); text-decoration: none; }
footer { margin-top: 42px; padding-top: 20px; border-top: 1px solid var(--line); color: var(--muted); font-size: 13px; }
@media (max-width: 850px) { .card, .card.wide { grid-column: 1 / -1; } .flow { grid-template-columns: 1fr 1fr; } }
@media (max-width: 520px) { .shell { width: min(100% - 20px, 1180px); padding-top: 20px; } .hero { padding-top: 22px; } .flow { grid-template-columns: 1fr; } .tabs-wrap { margin-bottom: 18px; } }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; animation: none !important; } }
</style>
</head>
<body>
<main class="shell">
<header class="hero">
<div class="eyebrow">STM32 · Classic CAN 2.0B · HAL</div>
<h1>SETCAN<br>руководство разработчика</h1>
<p class="lead">Карта библиотеки ProtoCAN, формат кадров и практический маршрут переноса в существующую прошивку STM32.</p>
<div class="stats" aria-label="Краткие характеристики">
<span class="stat"><b>29 бит</b> Extended ID</span><span class="stat"><b>0…8 байт</b> payload</span><span class="stat"><b>128</b> адресов устройств</span><span class="stat"><b>127</b> кадров в RX-очереди</span>
</div>
</header>
<div class="tabs-wrap">
<nav class="tabs" role="tablist" aria-label="Разделы документации">
<button class="tab" role="tab" aria-selected="true" aria-controls="overview" id="tab-overview" data-tab="overview">Обзор</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="library" id="tab-library" data-tab="library">Библиотека</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="protocol" id="tab-protocol" data-tab="protocol">Протокол</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="porting" id="tab-porting" data-tab="porting">Портирование</button>
<button class="tab" role="tab" aria-selected="false" aria-controls="examples" id="tab-examples" data-tab="examples">Примеры</button>
</nav>
</div>
<section class="panel active" id="overview" role="tabpanel" aria-labelledby="tab-overview">
<h2>Что находится в проекте</h2>
<p class="section-intro">SETCAN — не готовый CubeIDE-проект, а подключаемый C-модуль поверх STM32 HAL. Его задача — фильтровать, принимать, разбирать и отправлять прикладные сообщения ProtoCAN.</p>
<div class="grid">
<article class="card wide"><span class="tag">Структура</span><h3>Минимальное ядро + нормативные документы</h3><div class="filetree">SETCAN/
├── Inc/protocan.h публичные типы, настройки и API
├── Src/protocan.c фильтры, RX-очередь, разбор и отправка
├── docs/protocan/ спецификация протокола
│ ├── PROTOCOL.md 29-битный ID и типы сообщений
│ ├── OAP.md общее адресное пространство
│ ├── BOOTLOADER.md обновление прошивки по CAN (draft)
│ └── examples/ эталонные кадры JSON
├── Протокол CAN и ОАП.xlsx редактируемый реестр ОАП
└── doc/index.html эта страница</div></article>
<article class="card"><span class="tag blue">Зависимости</span><h3>Что ожидает модуль</h3><ul><li><code>main.h</code> и <code>can.h</code> из CubeMX</li><li>STM32 HAL CAN, RTC и TIM</li><li>CAN FIFO0 и Extended ID</li><li>STM32 GCC ABI для C bit-fields</li></ul></article>
<article class="card"><span class="tag">Входящие</span><h3>Путь кадра</h3><p>IRQ вычитывает FIFO0. Pulse обрабатывается сразу, остальные Extended-кадры попадают в кольцевой буфер и разбираются в основном цикле.</p></article>
<article class="card"><span class="tag">Исходящие</span><h3>Единая точка отправки</h3><p><code>PROTOCAN_SEND()</code> выбирает упаковщик по <code>MsgType</code>. GAS и Modbus автоматически разбиваются на кадры до 8 байт.</p></article>
<article class="card"><span class="tag amber">Граница</span><h3>Что приложение реализует само</h3><p>Прикладные действия, хранение SETTINGS, EEPROM, DS18B20, таймаут online/offline и полноценный bootloader не входят в ядро.</p></article>
</div>
<div class="flow" aria-label="Поток обработки">
<div class="step"><b>01 · FIFO0 IRQ</b><span>HAL сообщает о новом кадре</span></div><div class="step"><b>02 · RX buffer</b><span>Extended ID помещается в очередь</span></div><div class="step"><b>03 · Dispatcher</b><span>Разбор по MsgType</span></div><div class="step"><b>04 · Weak handler</b><span>Логика приложения</span></div>
</div>
</section>
<section class="panel" id="library" role="tabpanel" aria-labelledby="tab-library">
<h2>Состав библиотеки</h2><p class="section-intro">Два файла образуют один модуль. Заголовок — контракт интеграции; C-файл — транспортная реализация и демонстрационные слабые обработчики.</p>
<div class="grid">
<article class="card"><span class="tag">Inc</span><h3>protocan.h</h3><p>Конфигурация текущего устройства, размеры буфера, enum типов сообщений, битовые представления ID/Body, структуры TX/RX и публичные функции.</p><p><strong>Меняют при переносе:</strong> <code>CURRENT_TYPE_DEVICE</code>, <code>CURRENT_ID_DEVICE</code>, иногда <code>PROTOCAN_RX_BUFFER_SIZE</code>.</p></article>
<article class="card"><span class="tag">Src</span><h3>protocan.c</h3><p>Инициализация, фильтры, кольцевой буфер, диспетчеризация, RTC sync, pulse и упаковка кадров.</p><p><strong>Обычно не правят:</strong> прикладную логику лучше добавлять переопределением <code>__weak</code>-функций.</p></article>
<article class="card"><span class="tag amber">HAL</span><h3>Внешний слой</h3><p><code>CAN_HandleTypeDef</code>, <code>RTC_HandleTypeDef</code> и <code>TIM_HandleTypeDef</code> передаются в <code>PROTOCAN_INIT()</code>. Запуск периферии выполняет приложение.</p></article>
<article class="card full"><h3>Основной API</h3><div class="table-scroll"><table><thead><tr><th>Функция</th><th>Назначение</th><th>Где вызывать</th></tr></thead><tbody>
<tr><td>PROTOCAN_INIT</td><td>Сохраняет HAL handles, настраивает фильтры и callbackи при доступной регистрации.</td><td>После <code>MX_*_Init()</code>, до <code>HAL_CAN_Start()</code>.</td></tr>
<tr><td>PROTOCAN_ProcessAllRxMsgs</td><td>Обрабатывает всю накопленную RX-очередь и возвращает первую ошибку.</td><td>В основном цикле.</td></tr>
<tr><td>PROTOCAN_ProcessSingleRxMsg</td><td>Обрабатывает максимум один кадр.</td><td>В планировщике или ограниченном временном слоте.</td></tr>
<tr><td>PROTOCAN_SEND</td><td>Упаковывает и отправляет сообщение выбранного типа.</td><td>Из логики приложения, не удерживая изменяемые данные.</td></tr>
<tr><td>ProtoCanRxFifo0MsgPendingCallback</td><td>Забирает кадры из CAN FIFO0.</td><td>Из HAL callback, если авто-регистрация выключена.</td></tr>
<tr><td>ProtoCanPulseCallback</td><td>Отправляет периодический pulse со счётчиком.</td><td>Из callback нужного базового таймера.</td></tr>
<tr><td>PROTOCAN_SEND_SETTINGS_RESPONSE / ERROR</td><td>Формирует успешный либо ошибочный ответ привязки ROM.</td><td>Из пользовательского обработчика SETTINGS.</td></tr>
</tbody></table></div></article>
<article class="card full"><h3>Точки расширения</h3><p>Переопределите нужную <code>__weak</code>-функцию в собственном C-файле: семейства <code>ProtoCanMsgToBroadcast…</code>, <code>…Discrete…</code>, <code>…Analog…</code>, <code>ProtoCanMsgToSettings</code>, <code>ProtoCanMsgToGeneralAddressSpace</code> и <code>…Modbus…</code>. Не редактируйте демонстрационную реализацию в ядре — так обновлять библиотеку проще.</p></article>
</div>
</section>
<section class="panel" id="protocol" role="tabpanel" aria-labelledby="tab-protocol">
<h2>Формат ProtoCAN</h2><p class="section-intro">Протокол использует только CAN 2.0B Extended ID. Адрес устройства — пара <code>DeviceType/DeviceID</code>; назначение младших 16 бит зависит от типа сообщения.</p>
<article class="card full"><h3>29-битный идентификатор</h3><pre><code>28 27 26··24 23··20 19··16 15········0
Priority Route DeviceType DeviceID MsgType MsgBody
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит</code></pre><div class="callout">Многобайтовые значения payload передаются little-endian. Разметка ID через C bit-fields требует проверки при смене компилятора или ABI.</div></article>
<div class="grid" style="margin-top:14px">
<article class="card full"><h3>Реестр MsgType</h3><div class="table-scroll"><table><thead><tr><th>Код</th><th>Тип</th><th>Назначение</th><th>Статус</th></tr></thead><tbody>
<tr><td>0x0</td><td>BROADCAST</td><td>Общие команды всем устройствам</td><td>stable</td></tr><tr><td>0x1</td><td>DISCRETE</td><td>Состояния, команды, флаги</td><td>stable</td></tr><tr><td>0x2</td><td>ANALOG</td><td>Датчики U / I / T и универсальные данные</td><td>stable</td></tr><tr><td>0x3</td><td>GAS</td><td>Общее 16-битное адресное пространство</td><td>stable</td></tr><tr><td>0x4…0x7</td><td>MODBUS</td><td>Coil, Discrete, Holding, Input</td><td>stable</td></tr><tr><td>0x8</td><td>ERROR</td><td>Код ошибки и дополнительная информация</td><td>stable</td></tr><tr><td>0x9…0xD</td><td>BOOT</td><td>Управление, блоки A/B, статус, discovery</td><td>draft</td></tr><tr><td>0xE</td><td>SETTINGS</td><td>Привязка 1-Wire ROM к локации Z/Y</td><td>stable</td></tr><tr><td>0xF</td><td>PULSE</td><td>Присутствие устройства</td><td>stable</td></tr>
</tbody></table></div></article>
</div>
</section>
<section class="panel" id="porting" role="tabpanel" aria-labelledby="tab-porting">
<h2>Как портировать в свой STM32-проект</h2><p class="section-intro">Рабочая последовательность для CubeMX/CubeIDE. Имена <code>hcan</code>, <code>hrtc</code> и <code>htim2</code> замените на handles своего проекта.</p>
<div class="grid">
<article class="card"><span class="tag">Шаг 1</span><h3>Добавьте исходники</h3><ul class="checklist"><li><code>Inc/protocan.h</code> в include path</li><li><code>Src/protocan.c</code> в сборку</li><li>Проверьте доступность <code>main.h</code> и <code>can.h</code></li></ul></article>
<article class="card"><span class="tag">Шаг 2</span><h3>Настройте адрес</h3><p>В <code>protocan.h</code> задайте тип <code>0…7</code> и ID <code>0…15</code>. Пара должна быть уникальной на шине.</p><pre><code>#define CURRENT_TYPE_DEVICE 0b011
#define CURRENT_ID_DEVICE 0b0101</code></pre></article>
<article class="card"><span class="tag">Шаг 3</span><h3>Настройте CubeMX</h3><ul><li>CAN: Extended ID, FIFO0</li><li>RTC: если нужна синхронизация времени</li><li>TIM: период pulse</li><li>Проверьте filter banks 0, 1, 2 и границу 14</li></ul></article>
<article class="card full"><span class="tag">Шаг 4</span><h3>Инициализация и основной цикл</h3><pre><button class="copy" type="button">Копировать</button><code>#include "protocan.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN_Init();
MX_RTC_Init();
MX_TIM2_Init();
if (PROTOCAN_INIT(&amp;hcan, &amp;hrtc, &amp;htim2) != PROTOCAN_INIT_OK) {
Error_Handler();
}
if (HAL_CAN_Start(&amp;hcan) != HAL_OK) {
Error_Handler();
}
if (HAL_CAN_ActivateNotification(
&amp;hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) {
Error_Handler();
}
if (HAL_TIM_Base_Start_IT(&amp;htim2) != HAL_OK) {
Error_Handler();
}
while (1) {
(void)PROTOCAN_ProcessAllRxMsgs();
}
}</code></pre></article>
<article class="card wide"><span class="tag">Шаг 5</span><h3>Свяжите callbackи при отключённой регистрации HAL</h3><pre><button class="copy" type="button">Копировать</button><code>void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan_ptr)
{
if (hcan_ptr == &amp;hcan) {
ProtoCanRxFifo0MsgPendingCallback(hcan_ptr);
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim_ptr)
{
if (htim_ptr == &amp;htim2) {
ProtoCanPulseCallback(htim_ptr);
}
}</code></pre></article>
<article class="card"><span class="tag amber">Важно</span><h3>Не дублируйте события</h3><p>Если <code>USE_HAL_*_REGISTER_CALLBACKS == 1</code>, библиотека регистрирует callbackи сама. Не вызывайте те же функции ещё раз из стандартных callbackов.</p></article>
<article class="card full"><span class="tag">Шаг 6</span><h3>Перенесите прикладную логику</h3><p>Создайте, например, <code>protocan_app.c</code> и определите там только нужные weak handlers. Так ядро остаётся обновляемым.</p><pre><button class="copy" type="button">Копировать</button><code>#include "protocan.h"
PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg msg)
{
msgBodyAnalogType body = {0};
body.Body = msg.eID.Fields.MsgBody;
uint16_t sensor_id = body.Fields.SensorID;
/* Получить температуру sensor_id и сформировать ответ. */
return PROTOCAN_OK;
}</code></pre></article>
<article class="card full"><h3>Проверка после переноса</h3><ul class="checklist"><li>Проект собирается без повторных определений HAL callbackов.</li><li>CAN стартует и принимает только Extended ID.</li><li>Pulse появляется с периодом выбранного таймера.</li><li>Эталонный ID <code>0x13590702</code> декодируется как DeviceType=3, DeviceID=5, MsgType=9, Body=<code>0x0702</code>.</li><li>При нагрузке учтено, что буфер размера 128 хранит максимум 127 кадров.</li></ul></article>
</div>
</section>
<section class="panel" id="examples" role="tabpanel" aria-labelledby="tab-examples">
<h2>Готовые шаблоны</h2><p class="section-intro">Минимальные примеры формирования исходящего кадра и обработчика SETTINGS. Их можно вынести в прикладной слой проекта.</p>
<div class="grid">
<article class="card full"><h3>Отправка трёх регистров GAS</h3><pre><button class="copy" type="button">Копировать</button><code>uint16_t registers[] = { 0x1234, 0x5678, 0x9ABC };
ProtoCanId_t id = {0};
id.Fields.Priority = PROTOCAN_PRIORITY_STANDARD;
id.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
id.Fields.DeviceType = CURRENT_TYPE_DEVICE;
id.Fields.DeviceID = CURRENT_ID_DEVICE;
id.Fields.MsgType = PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE;
ProtoCanData_t tx = {0};
tx.GeneralAddressSpaceData.RegStartAdr = 100;
tx.GeneralAddressSpaceData.Data = registers;
tx.GeneralAddressSpaceData.RegCount = 3;
if (PROTOCAN_SEND(id, tx) != PROTOCAN_OK) {
/* CAN занят или HAL вернул ошибку. */
}</code></pre></article>
<article class="card full"><h3>SETTINGS: прикладное хранение ROM</h3><pre><button class="copy" type="button">Копировать</button><code>PROTOCAN_StatusTypeDef ProtoCanMsgToSettings(
const ProtoCanSettingsMsg_t *message)
{
uint8_t current_rom[PROTOCAN_SETTINGS_ROM_SIZE] = {0};
switch (message-&gt;Operation) {
case PROTOCAN_SETTINGS_GET:
/* Загрузить ROM из EEPROM в current_rom. */
return PROTOCAN_SEND_SETTINGS_RESPONSE(
message-&gt;AssemblySerial, message-&gt;Position, current_rom);
case PROTOCAN_SETTINGS_WRITE:
/* Проверить CRC/наличие и атомарно записать message-&gt;Rom. */
return PROTOCAN_SEND_SETTINGS_RESPONSE(
message-&gt;AssemblySerial, message-&gt;Position, message-&gt;Rom);
case PROTOCAN_SETTINGS_CLEAR:
/* Очистить запись; current_rom должен содержать нули. */
return PROTOCAN_SEND_SETTINGS_RESPONSE(
message-&gt;AssemblySerial, message-&gt;Position, current_rom);
default:
return PROTOCAN_SEND_SETTINGS_ERROR(
message-&gt;AssemblySerial, message-&gt;Position,
PROTOCAN_SETTINGS_RESULT_INVALID_DLC);
}
}</code></pre></article>
</div>
<div class="docs" aria-label="Исходная документация">
<a class="doclink" href="../README.md">README проекта ↗</a><a class="doclink" href="../docs/protocan/PROTOCOL.md">PROTOCOL.md ↗</a><a class="doclink" href="../docs/protocan/OAP.md">OAP.md ↗</a><a class="doclink" href="../docs/protocan/BOOTLOADER.md">BOOTLOADER.md ↗</a><a class="doclink" href="../docs/protocan/examples/test-vectors.json">test-vectors.json ↗</a>
</div>
</section>
<footer>Сформировано по исходникам и документации SETCAN. Нормативный приоритет сохраняют PROTOCOL.md, BOOTLOADER.md и реестр ОАП.</footer>
</main>
<script>
const tabs = [...document.querySelectorAll('[role="tab"]')];
const panels = [...document.querySelectorAll('[role="tabpanel"]')];
function activate(name, focus = false) {
const tab = tabs.find(item => item.dataset.tab === name) || tabs[0];
tabs.forEach(item => item.setAttribute('aria-selected', String(item === tab)));
panels.forEach(panel => panel.classList.toggle('active', panel.id === tab.dataset.tab));
if (focus) tab.focus();
history.replaceState(null, '', '#' + tab.dataset.tab);
}
tabs.forEach((tab, index) => {
tab.addEventListener('click', () => activate(tab.dataset.tab));
tab.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
let next = index;
if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
if (event.key === 'Home') next = 0;
if (event.key === 'End') next = tabs.length - 1;
activate(tabs[next].dataset.tab, true);
});
});
document.querySelectorAll('.copy').forEach(button => button.addEventListener('click', async () => {
const code = button.parentElement.querySelector('code').innerText;
try {
await navigator.clipboard.writeText(code);
button.textContent = 'Скопировано';
setTimeout(() => button.textContent = 'Копировать', 1400);
} catch { button.textContent = 'Выделите код'; }
}));
activate(location.hash.slice(1) || 'overview');
</script>
</body>
</html>