feat(flasher): блоки версий, заметный поиск платы и прошивка своего файла

This commit is contained in:
shade committed 2026-10-16 18:10:02 +03:00
1 parent e1d40a5a81
commit 98c6272c24
3 files changed
+247 -55

No files matched your search

+119 -8
View File
@@ -5,6 +5,7 @@ const logEl = $('log');
let index = null;
let selected = null; // выбранный файл из index.json
let ownFile = null; // файл, загруженный пользователем с компьютера
let transport = null;
let loader = null;
let connected = false;
@@ -132,17 +133,31 @@ function boardCard(section, board) {
const fmtOrder = { merged: 0, app: 1, uf2: 2, zip: 3 };
const tags = [...new Set(board.files.map((f) => f.tag))].sort((a, b) => compareReleases(a, b, rank));
const newest = tags[0];
const relByTag = {};
(section.releases || []).forEach((r) => { relByTag[r.tag] = r; });
tags.forEach((tag) => {
const files = board.files.filter((f) => f.tag === tag)
.sort((a, b) => a.role_label.localeCompare(b.role_label, 'ru')
|| (fmtOrder[a.format] ?? 9) - (fmtOrder[b.format] ?? 9));
const head = document.createElement('div');
head.className = 'ver';
head.innerHTML = `<span class="vtag">${files[0].version || tag}</span>`
const rel = relByTag[tag] || {};
const date = String(rel.date || rel.created_at || '').slice(0, 10);
// каждая версия — отдельный блок: свежая раскрыта, остальные свёрнуты
const block = document.createElement('details');
block.className = 'verblock' + (tag === newest ? ' newest' : '');
block.open = tag === newest;
const sum = document.createElement('summary');
sum.innerHTML = `<span class="vtag">${files[0].version || tag}</span>`
+ (tag === newest ? '<span class="vnew">свежая</span>' : '')
+ (date ? `<span class="vdate">${date}</span>` : '')
+ `<span class="vcount">${files.length} ${plural(files.length, 'файл', 'файла', 'файлов')}</span>`;
box.appendChild(head);
files.forEach((f) => box.appendChild(fileRow(section, board, f)));
block.appendChild(sum);
const inner = document.createElement('div');
inner.className = 'verfiles';
files.forEach((f) => inner.appendChild(fileRow(section, board, f)));
block.appendChild(inner);
box.appendChild(block);
});
el.appendChild(box);
return el;
@@ -164,16 +179,31 @@ function sectionBlock(section) {
function applyFilter(query) {
const q = query.trim().toLowerCase();
let found = 0;
document.querySelectorAll('section.group').forEach((sec) => {
let visibleBoards = 0;
sec.querySelectorAll('details.board').forEach((b) => {
const name = b.querySelector('summary').textContent.toLowerCase();
const hit = !q || name.includes(q);
// ищем и по названию платы, и по версиям, и по именам файлов
const hay = [
b.querySelector('summary') ? b.querySelector('summary').textContent : '',
...[...b.querySelectorAll('.vtag, .fname')].map((n) => n.textContent),
].join(' ').toLowerCase();
const hit = !q || hay.includes(q);
b.style.display = hit ? '' : 'none';
if (hit) visibleBoards++;
b.classList.toggle('hit', Boolean(q) && hit);
if (hit) { visibleBoards++; found++; }
// при поиске раскрываем блоки версий, чтобы совпадения были видны сразу
if (q) b.querySelectorAll('details.verblock').forEach((v) => { v.open = true; });
});
sec.style.display = visibleBoards ? '' : 'none';
});
const hint = $('search-hint');
if (hint) {
hint.textContent = q
? (found ? `Найдено плат: ${found}` : 'Ничего не найдено — попробуйте другое название')
: 'Начните вводить название платы — список отфильтруется';
hint.className = 'search-hint' + (q ? (found ? ' ok' : ' err') : '');
}
}
async function loadIndex() {
@@ -213,6 +243,7 @@ function updateButtons() {
$('btn-erase').disabled = busy || !connected;
$('btn-reset').disabled = busy || !connected;
$('btn-connect').disabled = busy || connected;
$('btn-own-flash').disabled = busy || !ownFile;
}
// ---------------------------------------------------------------- работа с портом
@@ -347,6 +378,67 @@ async function resetDevice() {
}
}
// ---------------------------------------------------------------- своя прошивка
function setOwnFile(file) {
ownFile = file || null;
$('own-name').textContent = ownFile
? `${ownFile.name} · ${human(ownFile.size)}`
: 'Перетащите файл сюда или нажмите, чтобы выбрать';
$('drop').classList.toggle('filled', Boolean(ownFile));
if (ownFile) log('Выбран свой файл: ' + ownFile.name + ' (' + human(ownFile.size) + ')');
updateButtons();
}
async function flashOwn() {
if (!ownFile) return;
if (!connected && !(await connect())) return;
const want = $('own-chip').value;
if (want !== 'auto' && chipName) {
const known = { esp32: 'ESP32', esp32s3: 'ESP32-S3', esp32c3: 'ESP32-C3', esp32c6: 'ESP32-C6', esp32s2: 'ESP32-S2' };
const short = want.replace('esp32', '').toUpperCase();
if (short && !chipName.toUpperCase().includes(short)) {
if (!confirm(`Выбран чип ${known[want]}, а подключён ${chipName}. Продолжить прошивку?`)) return;
}
}
const addr = parseInt($('own-addr').value, 16) || 0;
const eraseAll = $('erase-first').checked;
try {
busy = true; updateButtons();
setProgress(0, 'подготовка');
log(`Чтение своего файла ${ownFile.name} (${human(ownFile.size)})…`);
const bytes = new Uint8Array(await ownFile.arrayBuffer());
const data = bytesToBinaryString(bytes);
if (data.length !== bytes.length) throw new Error('данные повреждены при подготовке: ' + data.length + ' вместо ' + bytes.length + ' байт');
if (eraseAll) {
log('Полное стирание флеша…', 'warn');
await loader.eraseFlash();
log('Флеш очищен', 'ok');
}
log(`Запись по адресу 0x${addr.toString(16)}…`);
await loader.writeFlash({
fileArray: [{ data, address: addr }],
flashSize: 'keep',
eraseAll: false,
compress: true,
reportProgress: (fileIndex, written, total) => setProgress(written / total),
});
log('Запись завершена', 'ok');
setProgress(1, '100 %');
await loader.after('hard_reset');
log('Устройство перезагружено — прошивка применена', 'ok');
} catch (err) {
log('Ошибка прошивки: ' + (err && err.message ? err.message : err), 'err');
setProgress(0, 'ошибка');
} finally {
busy = false; updateButtons();
}
}
$('btn-connect').addEventListener('click', connect);
$('btn-flash').addEventListener('click', flash);
$('btn-erase').addEventListener('click', eraseOnly);
@@ -356,6 +448,25 @@ $('erase-first').addEventListener('change', (e) => {
if (e.target.checked) log('Включено полное стирание перед прошивкой — настройки узла будут удалены', 'warn');
});
// своя прошивка: выбор файла, drag&drop, очистка поиска
$('own-file').addEventListener('change', (e) => setOwnFile(e.target.files && e.target.files[0]));
$('btn-own-flash').addEventListener('click', flashOwn);
['dragenter', 'dragover'].forEach((ev) => $('drop').addEventListener(ev, (e) => {
e.preventDefault(); $('drop').classList.add('over');
}));
['dragleave', 'dragend'].forEach((ev) => $('drop').addEventListener(ev, () => $('drop').classList.remove('over')));
$('drop').addEventListener('drop', (e) => {
e.preventDefault();
$('drop').classList.remove('over');
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
if (f) setOwnFile(f);
});
$('search-clear').addEventListener('click', () => {
$('search').value = '';
applyFilter('');
$('search').focus();
});
if ('serial' in navigator) {
navigator.serial.addEventListener('disconnect', () => {
connected = false;
+88 -47
View File
@@ -1,117 +1,158 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Прошивальщик MeshCore и PosadMesh</title>
<meta name="description" content="Прошивка плат MeshCore и MQTT-репитеров PosadMesh прямо из браузера">
<link rel="stylesheet" href="style.css?v=20261015">
<title>Прошивальщик MeshCore и PosadMesh</title>
<meta name="description" content="Прошивка плат MeshCore и MQTT-репитеров PosadMesh прямо из браузера">
<link rel="stylesheet" href="style.css?v=20261016">
</head>
<body>
<div class="wrap">
<header class="top">
<h1>Прошивальщик MeshCore и PosadMesh</h1>
<p>Прошивка плат прямо из браузера по USB. Выберите плату, файл и нажмите «Прошить» — образ запишется
через Web Serial. Для nRF52 браузер прошить не может, такие файлы скачиваются и копируются на устройство.</p>
<h1>Прошивальщик MeshCore и PosadMesh</h1>
<p>Прошивка плат прямо из браузера по USB. Выберите плату, файл и нажмите «Прошить» — образ запишется
через Web Serial. Для nRF52 браузер прошить не может, такие файлы скачиваются и копируются на устройство.</p>
<div class="badges">
<span class="badge" id="b-serial">проверяю Web Serial…</span>
<span class="badge" id="b-secure">проверяю защищённый контекст…</span>
<span class="badge" id="b-index">читаю список прошивок…</span>
<span class="badge" id="b-serial">проверяю Web Serial…</span>
<span class="badge" id="b-secure">проверяю защищённый контекст…</span>
<span class="badge" id="b-index">читаю список прошивок…</span>
</div>
</header>
<div class="layout">
<main>
<div class="notes" style="margin-top:0;border-top:none;padding-top:0">
<h2>Что означают файлы</h2>
<h2>Что означают файлы</h2>
<div class="cards">
<div class="card">
<h4><code>-merged.bin</code> — полный образ</h4>
<p>Bootloader, таблица разделов и прошивка в одном файле. Нужен для чистой установки: пишется
по адресу <code>0x0</code>, полностью заменяет содержимое флеша.</p>
<h4><code>-merged.bin</code> — полный образ</h4>
<p>Bootloader, таблица разделов и прошивка в одном файле. Нужен для чистой установки: пишется
по адресу <code>0x0</code>, полностью заменяет содержимое флеша.</p>
</div>
<div class="card">
<h4><code>.bin</code> — только прошивка</h4>
<p>Обновление поверх текущей версии: пишется по адресу <code>0x10000</code>, настройки, ключи
и координаты узла сохраняются.</p>
<h4><code>.bin</code> — только прошивка</h4>
<p>Обновление поверх текущей версии: пишется по адресу <code>0x10000</code>, настройки, ключи
и координаты узла сохраняются.</p>
</div>
<div class="card">
<h4><code>.uf2</code> — для nRF52</h4>
<p>Дважды нажмите кнопку сброса, чтобы плата появилась как USB-диск (например <code>NICENANO</code>),
и скопируйте файл на этот диск. Прошивка установится сама.</p>
<h4><code>.uf2</code> — для nRF52</h4>
<p>Дважды нажмите кнопку сброса, чтобы плата появилась как USB-диск (например <code>NICENANO</code>),
и скопируйте файл на этот диск. Прошивка установится сама.</p>
</div>
<div class="card">
<h4><code>.zip</code> — для nRF52 через DFU</h4>
<p>Пакет обновления для nRF Connect (Programmer → DFU) или <code>adafruit-nrfutil</code>.
Пригодится, когда диск UF2 недоступен.</p>
<h4><code>.zip</code> — для nRF52 через DFU</h4>
<p>Пакет обновления для nRF Connect (Programmer → DFU) или <code>adafruit-nrfutil</code>.
Пригодится, когда диск UF2 недоступен.</p>
</div>
<div class="card">
<h4>Стирание чипа</h4>
<p>Галочка «Стереть флеш полностью» очищает весь чип перед записью: разделы, настройки, ключи
и координаты. После этого узел настраивается с нуля — выбирайте эту опцию при первой прошивке
или при переходе с чужой разметки.</p>
<h4>Стирание чипа</h4>
<p>Галочка «Стереть флеш полностью» очищает весь чип перед записью: разделы, настройки, ключи
и координаты. После этого узел настраивается с нуля — выбирайте эту опцию при первой прошивке
или при переходе с чужой разметки.</p>
</div>
<div class="card">
<h4>Что нужно</h4>
<p>Chrome или Edge версии 89+, страница по HTTPS, USB-кабель с передачей данных.
После прошивки нажмите «Перезагрузить» и отключите кабель.</p>
<h4>Что нужно</h4>
<p>Chrome или Edge версии 89+, страница по HTTPS, USB-кабель с передачей данных.
После прошивки нажмите «Перезагрузить» и отключите кабель.</p>
</div>
</div>
</div>
<input class="search" id="search" type="search" placeholder="Поиск платы: heltec, tbeam, e22, tdeck…" autocomplete="off">
<div class="searchbar">
<label class="search-label" for="search">Поиск платы</label>
<div class="search-row">
<span class="search-icon" aria-hidden="true">рџ”Ќ</span>
<input class="search" id="search" type="search"
placeholder="heltec, tbeam, e22, tdeck, meshadventurer…" autocomplete="off" autofocus>
<button class="search-clear small" id="search-clear" type="button">Сбросить</button>
</div>
<div class="search-hint" id="search-hint">Начните вводить название платы — список отфильтруется</div>
</div>
<div id="sections"></div>
</main>
<aside class="panel">
<h3>Прошивка</h3>
<div class="kv"><span>Устройство</span><span id="v-port">не подключено</span></div>
<div class="kv"><span>Чип</span><span id="v-chip">—</span></div>
<div class="kv"><span>Выбрано</span><span id="v-file">файл не выбран</span></div>
<div class="kv"><span>Адрес записи</span><span id="v-addr">—</span></div>
<div class="kv"><span>Размер</span><span id="v-size">—</span></div>
<h3>Прошивка</h3>
<div class="kv"><span>Устройство</span><span id="v-port">не подключено</span></div>
<div class="kv"><span>Чип</span><span id="v-chip">—</span></div>
<div class="kv"><span>Выбрано</span><span id="v-file">файл не выбран</span></div>
<div class="kv"><span>Адрес записи</span><span id="v-addr">—</span></div>
<div class="kv"><span>Размер</span><span id="v-size">—</span></div>
<div class="row">
<button id="btn-connect" class="primary">Подключить устройство</button>
<button id="btn-connect" class="primary">Подключить устройство</button>
</div>
<label class="check" style="margin-top:14px">
<span>Скорость</span>
<span>Скорость</span>
<select id="baud">
<option value="921600" selected>921600</option>
<option value="460800">460800</option>
<option value="115200">115200 (медленно, но надёжно)</option>
<option value="115200">115200 (медленно, но надёжно)</option>
</select>
</label>
<label class="check">
<input type="checkbox" id="erase-first">
<span><b>Стереть флеш полностью перед прошивкой.</b> Разделы, настройки и ключи будут удалены.</span>
<span><b>Стереть флеш полностью перед прошивкой.</b> Разделы, настройки и ключи будут удалены.</span>
</label>
<div class="progress"><i id="bar"></i></div>
<div class="progress-label" id="progress-label"></div>
<div class="row">
<button id="btn-flash" class="primary" disabled>Прошить</button>
<button id="btn-erase" class="danger" disabled>Только стереть</button>
<button id="btn-flash" class="primary" disabled>Прошить</button>
<button id="btn-erase" class="danger" disabled>Только стереть</button>
</div>
<div class="row">
<button id="btn-reset" class="small" disabled>Перезагрузить</button>
<button id="btn-clear-log" class="small">Очистить журнал</button>
<button id="btn-reset" class="small" disabled>Перезагрузить</button>
<button id="btn-clear-log" class="small">Очистить журнал</button>
</div>
<h3>Своя прошивка</h3>
<p class="hint">Файл с компьютера прошивается так же, как из списка. Адрес: <code>0x0</code> для полного
образа (<code>-merged.bin</code>), <code>0x10000</code> для обновления (<code>.bin</code>).</p>
<label class="drop" id="drop">
<input type="file" id="own-file" accept=".bin,.uf2,.zip,.hex">
<span id="own-name">Перетащите файл сюда или нажмите, чтобы выбрать</span>
</label>
<label class="check">
<span>Адрес записи</span>
<select id="own-addr">
<option value="0x0" selected>0x0 — полный образ</option>
<option value="0x10000">0x10000 — только прошивка</option>
<option value="0x1000">0x1000 — bootloader</option>
<option value="0x8000">0x8000 — таблица разделов</option>
</select>
</label>
<label class="check">
<span>Р§РёРї</span>
<select id="own-chip">
<option value="auto" selected>определить автоматически</option>
<option value="esp32">ESP32</option>
<option value="esp32s3">ESP32-S3</option>
<option value="esp32c3">ESP32-C3</option>
<option value="esp32c6">ESP32-C6</option>
<option value="esp32s2">ESP32-S2</option>
</select>
</label>
<div class="row">
<button id="btn-own-flash" class="primary" disabled>Прошить свой файл</button>
</div>
<h3>Журнал</h3>
<div class="log" id="log"></div>
</aside>
</div>
<footer>
MeshCore-Posadmesh · список прошивок обновляется автоматически из релизов Gitea ·
MeshCore-Posadmesh · список прошивок обновляется автоматически из релизов Gitea ·
<a href="/flasher/index.json">index.json</a>
</footer>
</div>
<script type="module" src="app.js?v=20261015"></script>
<script type="module" src="app.js?v=20261016"></script>
</body>
</html>
+40
View File
@@ -157,3 +157,43 @@ aside.panel select {
.notes .card p { margin: 0; color: var(--muted); font-size: 13.5px; }
.notes .card code { background: var(--panel-2); padding: 1px 5px; border-radius: 5px; font-size: 12.5px; }
footer { margin-top: 28px; color: var(--muted); font-size: 13px; border-top: 1px solid var(--line); padding-top: 14px; }
/* --- заметный поиск платы --- */
.searchbar {
position: sticky; top: 0; z-index: 5;
margin: 18px 0 14px; padding: 14px 16px 12px;
background: linear-gradient(180deg, #12161d 0%, #0e1218 100%);
border: 1px solid #2a3342; border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,.35);
}
.search-label { display: block; font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: #8fa3bf; margin-bottom: 8px; font-weight: 600; }
.search-row { display: flex; gap: 10px; align-items: center; }
.search-icon { font-size: 20px; line-height: 1; }
.searchbar .search { flex: 1; font-size: 17px; padding: 12px 14px; border-radius: 10px; border: 2px solid #33415a; background: #0b0f15; color: #e8eef7; }
.searchbar .search:focus { outline: none; border-color: #4c8dff; box-shadow: 0 0 0 3px rgba(76,141,255,.25); }
.search-hint { margin-top: 8px; font-size: 13px; color: #7d8ea6; }
.search-hint.ok { color: #4ec97a; }
.search-hint.err { color: #ff6b6b; }
.search-clear { white-space: nowrap; }
details.board.hit > summary { color: #cfe2ff; }
/* --- блоки версий прошивки --- */
details.verblock { border: 1px solid #232c39; border-radius: 10px; margin: 10px 0; overflow: hidden; background: #0d1117; }
details.verblock.newest { border-color: #2f6f4f; }
details.verblock > summary { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; cursor: pointer; padding: 10px 12px; background: #131923; list-style: none; }
details.verblock > summary::-webkit-details-marker { display: none; }
details.verblock > summary::before { content: '▸'; color: #6f8199; font-size: 13px; }
details.verblock[open] > summary::before { content: '▾'; }
details.verblock.newest > summary { background: #14241c; }
.verblock .vtag { font-weight: 700; font-size: 15px; color: #e8eef7; }
.verblock .vnew { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: #1d5c3a; color: #b9f3cf; }
.verblock .vdate { font-size: 12px; color: #7d8ea6; }
.verblock .vcount { margin-left: auto; font-size: 12px; color: #7d8ea6; }
.verfiles { padding: 6px 10px 10px; border-top: 1px solid #1c2430; }
/* --- своя прошивка --- */
.drop { display: block; margin: 10px 0; padding: 16px 12px; border: 2px dashed #33415a; border-radius: 10px; text-align: center; color: #93a4bb; cursor: pointer; transition: border-color .15s, background .15s; }
.drop:hover, .drop.over { border-color: #4c8dff; background: rgba(76,141,255,.08); color: #cfe2ff; }
.drop.filled { border-style: solid; border-color: #2f6f4f; color: #cdebd8; }
.drop input[type=file] { display: none; }
.hint { font-size: 13px; color: #7d8ea6; line-height: 1.45; }