diff --git a/posadmesh-tools/flasher/app.js b/posadmesh-tools/flasher/app.js
index f15b665a..714335d3 100644
--- a/posadmesh-tools/flasher/app.js
+++ b/posadmesh-tools/flasher/app.js
@@ -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 = `${files[0].version || tag} `
+ 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 = `${files[0].version || tag} `
+ (tag === newest ? 'свежая ' : '')
+ + (date ? `${date} ` : '')
+ `${files.length} ${plural(files.length, 'файл', 'файла', 'файлов')} `;
- 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;
diff --git a/posadmesh-tools/flasher/index.html b/posadmesh-tools/flasher/index.html
index 4b2fb537..e6ac8bb3 100644
--- a/posadmesh-tools/flasher/index.html
+++ b/posadmesh-tools/flasher/index.html
@@ -1,117 +1,158 @@
-
+
-Прошивальщик MeshCore и PosadMesh
-
-
+Прошивальщик MeshCore и PosadMesh
+
+
- Прошивальщик MeshCore и PosadMesh
- Прошивка плат прямо из браузера по USB. Выберите плату, файл и нажмите «Прошить» — образ запишется
- через Web Serial. Для nRF52 браузер прошить не может, такие файлы скачиваются и копируются на устройство.
+ Прошивальщик MeshCore и PosadMesh
+ Прошивка плат прямо из браузера по USB. Выберите плату, файл и нажмите «Прошить» — образ запишется
+ через Web Serial. Для nRF52 браузер прошить не может, такие файлы скачиваются и копируются на устройство.
- проверяю Web Serial…
- проверяю защищённый контекст…
- читаю список прошивок…
+ проверяю Web Serial…
+ проверяю защищённый контекст…
+ читаю список прошивок…
-
Что означают файлы
+
Что означают файлы
-
-merged.bin — полный образ
-
Bootloader, таблица разделов и прошивка в одном файле. Нужен для чистой установки: пишется
- по адресу 0x0, полностью заменяет содержимое флеша.
+
-merged.bin — полный образ
+
Bootloader, таблица разделов и прошивка в одном файле. Нужен для чистой установки: пишется
+ по адресу 0x0, полностью заменяет содержимое флеша.
-
.bin — только прошивка
-
Обновление поверх текущей версии: пишется по адресу 0x10000, настройки, ключи
- и координаты узла сохраняются.
+
.bin — только прошивка
+
Обновление поверх текущей версии: пишется по адресу 0x10000, настройки, ключи
+ и координаты узла сохраняются.
-
.uf2 — для nRF52
-
Дважды нажмите кнопку сброса, чтобы плата появилась как USB-диск (например NICENANO),
- и скопируйте файл на этот диск. Прошивка установится сама.
+
.uf2 — для nRF52
+
Дважды нажмите кнопку сброса, чтобы плата появилась как USB-диск (например NICENANO),
+ и скопируйте файл на этот диск. Прошивка установится сама.
-
.zip — для nRF52 через DFU
-
Пакет обновления для nRF Connect (Programmer → DFU) или adafruit-nrfutil.
- Пригодится, когда диск UF2 недоступен.
+
.zip — для nRF52 через DFU
+
Пакет обновления для nRF Connect (Programmer → DFU) или adafruit-nrfutil.
+ Пригодится, когда диск UF2 недоступен.
-
Стирание чипа
-
Галочка «Стереть флеш полностью» очищает весь чип перед записью: разделы, настройки, ключи
- и координаты. После этого узел настраивается с нуля — выбирайте эту опцию при первой прошивке
- или при переходе с чужой разметки.
+
Стирание чипа
+
Галочка «Стереть флеш полностью» очищает весь чип перед записью: разделы, настройки, ключи
+ и координаты. После этого узел настраивается с нуля — выбирайте эту опцию при первой прошивке
+ или при переходе с чужой разметки.
-
Что нужно
-
Chrome или Edge версии 89+, страница по HTTPS, USB-кабель с передачей данных.
- После прошивки нажмите «Перезагрузить» и отключите кабель.
+
Что нужно
+
Chrome или Edge версии 89+, страница по HTTPS, USB-кабель с передачей данных.
+ После прошивки нажмите «Перезагрузить» и отключите кабель.
-
+
+
Поиск платы
+
+ рџ”Ќ
+
+ Сбросить
+
+
Начните вводить название платы — список отфильтруется
+
- Прошивка
- Устройство не подключено
- Чип —
- Выбрано файл не выбран
- Адрес записи —
- Размер —
+ Прошивка
+ Устройство не подключено
+ Чип —
+ Выбрано файл не выбран
+ Адрес записи —
+ Размер —
- Подключить устройство
+ Подключить устройство
- Скорость
+ Скорость
921600
460800
- 115200 (медленно, но надёжно)
+ 115200 (медленно, но надёжно)
- Стереть флеш полностью перед прошивкой. Разделы, настройки и ключи будут удалены.
+ Стереть флеш полностью перед прошивкой. Разделы, настройки и ключи будут удалены.
- Прошить
- Только стереть
+ Прошить
+ Только стереть
- Перезагрузить
- Очистить журнал
+ Перезагрузить
+ Очистить журнал
+ Своя прошивка
+ Файл с компьютера прошивается так же, как из списка. Адрес: 0x0 для полного
+ образа (-merged.bin), 0x10000 для обновления (.bin).
+
+
+ Перетащите файл сюда или нажмите, чтобы выбрать
+
+
+ Адрес записи
+
+ 0x0 — полный образ
+ 0x10000 — только прошивка
+ 0x1000 — bootloader
+ 0x8000 — таблица разделов
+
+
+
+ Р§РёРї
+
+ определить автоматически
+ ESP32
+ ESP32-S3
+ ESP32-C3
+ ESP32-C6
+ ESP32-S2
+
+
+
+ Прошить свой файл
+
+
+ Журнал
- MeshCore-Posadmesh · список прошивок обновляется автоматически из релизов Gitea ·
+ MeshCore-Posadmesh · список прошивок обновляется автоматически из релизов Gitea ·
index.json
-
+
diff --git a/posadmesh-tools/flasher/style.css b/posadmesh-tools/flasher/style.css
index dfd0f10a..8556649a 100644
--- a/posadmesh-tools/flasher/style.css
+++ b/posadmesh-tools/flasher/style.css
@@ -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; }