Initial commit: UV-K5 firmware site with flasher

This commit is contained in:
ua1zbe
2026-04-16 15:11:33 +03:00
commit e461752581
1426 changed files with 261828 additions and 0 deletions

324
static/css/style.css Normal file
View File

@@ -0,0 +1,324 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #1a73e8;
--primary-dark: #1557b0;
--bg: #f5f5f5;
--surface: #ffffff;
--text: #333333;
--text-muted: #666666;
--border: #e0e0e0;
--success: #4caf50;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
header {
background: var(--primary);
color: white;
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
header .container {
max-width: 900px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 1.5rem;
}
nav a {
color: white;
text-decoration: none;
margin-left: 1.5rem;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background 0.2s;
}
nav a:hover {
background: rgba(255,255,255,0.15);
}
main {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
.card {
background: var(--surface);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.btn {
display: inline-block;
background: var(--primary);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
transition: background 0.2s;
}
.btn:hover {
background: var(--primary-dark);
}
.btn-download {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.release-version {
font-size: 1.25rem;
font-weight: 600;
color: var(--primary);
}
.release-date {
color: var(--text-muted);
font-size: 0.9rem;
}
.changelog {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
margin: 1rem 0;
}
.feature-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0;
}
.tag {
background: #e3f2fd;
color: var(--primary);
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
}
.doc-list {
list-style: none;
}
.doc-list li {
margin-bottom: 0.5rem;
}
.doc-list a {
display: block;
padding: 0.75rem 1rem;
background: var(--surface);
border-radius: 4px;
text-decoration: none;
color: var(--text);
transition: background 0.2s;
}
.doc-list a:hover {
background: #e3f2fd;
}
.markdown-content h1 { font-size: 1.75rem; margin: 1.5rem 0 1rem; }
.markdown-content h2 { font-size: 1.5rem; margin: 1.25rem 0 0.75rem; }
.markdown-content h3 { font-size: 1.25rem; margin: 1rem 0 0.5rem; }
.markdown-content p { margin: 0.75rem 0; }
.markdown-content ul, .markdown-content ol { margin: 0.75rem 0 0.75rem 1.5rem; }
.markdown-content li { margin: 0.25rem 0; }
.markdown-content code {
background: #f5f5f5;
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-size: 0.9em;
}
.markdown-content pre {
background: #263238;
color: #aed581;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
.markdown-content pre code {
background: none;
padding: 0;
color: inherit;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.markdown-content th, .markdown-content td {
border: 1px solid var(--border);
padding: 0.5rem;
text-align: left;
}
.markdown-content th {
background: #f5f5f5;
}
footer {
text-align: center;
padding: 2rem 0;
color: var(--text-muted);
font-size: 0.9rem;
}
.hero {
text-align: center;
padding: 2rem 1rem;
}
.hero h2 {
font-size: 2rem;
margin-bottom: 1rem;
}
.hero p {
font-size: 1.1rem;
color: var(--text-muted);
max-width: 600px;
margin: 0 auto 1.5rem;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.feature-item {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
}
.feature-item h3 {
color: var(--primary);
font-size: 1rem;
margin-bottom: 0.5rem;
}
table.downloads {
width: 100%;
border-collapse: collapse;
}
table.downloads th, table.downloads td {
padding: 0.75rem;
border-bottom: 1px solid var(--border);
text-align: left;
}
table.downloads th {
background: #f5f5f5;
font-weight: 600;
}
.version-badge {
display: inline-block;
background: var(--success);
color: white;
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
header .container {
flex-direction: column;
gap: 0.5rem;
}
nav a {
margin: 0 0.5rem;
}
}
input[type="file"] {
display: block;
width: 100%;
padding: 1rem;
border: 2px dashed var(--border);
border-radius: 4px;
background: #fafafa;
cursor: pointer;
margin: 1rem 0;
}
input[type="file"]:hover {
border-color: var(--primary);
background: #f0f7ff;
}
#fileInfo {
margin: 1rem 0;
padding: 0.75rem;
background: #f5f5f5;
border-radius: 4px;
}
#status {
margin-top: 1rem;
padding: 1rem;
border-radius: 4px;
}
#status.loading {
background: #fff3e0;
color: #e65100;
}
#status.success {
background: #e8f5e9;
color: #2e7d32;
}
#status.error {
background: #ffebee;
color: #c62828;
}
.note {
color: var(--text-muted);
font-size: 0.9rem;
margin-top: 1rem;
}
.note a {
color: var(--primary);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

63
static/js/flasher.js Normal file
View File

@@ -0,0 +1,63 @@
const firmwareFile = document.getElementById('firmwareFile');
const fileInfo = document.getElementById('fileInfo');
const flashBtn = document.getElementById('flashBtn');
const status = document.getElementById('status');
let selectedFile = null;
firmwareFile.addEventListener('change', (e) => {
selectedFile = e.target.files[0];
if (selectedFile) {
fileInfo.textContent = `Выбран файл: ${selectedFile.name} (${(selectedFile.size / 1024).toFixed(1)} KB)`;
flashBtn.disabled = false;
} else {
fileInfo.textContent = '';
flashBtn.disabled = true;
}
});
flashBtn.addEventListener('click', async () => {
if (!selectedFile) return;
status.textContent = 'Подключите радиостанцию к компьютеру и нажмите кнопку на устройстве...';
status.className = 'loading';
try {
const device = await navigator.usb.requestDevice({
filters: [{ vendorId: 0x15e2, productId: 0x9100 }]
});
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
const buffer = await selectedFile.arrayBuffer();
const chunkSize = 64;
const totalChunks = Math.ceil(buffer.byteLength / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, buffer.byteLength);
const chunk = buffer.slice(start, end);
await device.transferOut(1, chunk);
status.textContent = `Загрузка: ${Math.round((i / totalChunks) * 100)}%`;
await new Promise(r => setTimeout(r, 10));
}
await device.releaseInterface(0);
await device.close();
status.textContent = 'Прошивка успешно загружена!';
status.className = 'success';
} catch (err) {
if (err.name === 'NotFoundError') {
status.textContent = 'Устройство не найдено. Подключите радиостанцию и разрешите доступ.';
status.className = 'error';
} else {
status.textContent = 'Ошибка: ' + err.message;
status.className = 'error';
}
}
});