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-кабель с передачей данных. + После прошивки нажмите «Перезагрузить» и отключите кабель.

- +
- + 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; }