generated from Grigo/AndroidTemplate
326 lines
18 KiB
HTML
326 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>LoraTester</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<link rel="stylesheet" href="/static/app.css" />
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>LoraTester</h1>
|
||
<span class="muted" id="status">загрузка…</span>
|
||
<span class="muted" id="pollStatus" title="Автообновление">⟳ 1 с</span>
|
||
<span class="legend"><span class="legend-tx">● TX</span> <span class="legend-rx">● RX</span></span>
|
||
<span id="gpsDistance" class="muted">GPS: —</span>
|
||
<input type="text" id="webDeviceId" placeholder="ник в чате" style="padding:6px;border-radius:4px;border:none;max-width:160px" />
|
||
</header>
|
||
<main>
|
||
<div id="mapWrap">
|
||
<div id="mapCenterBar">
|
||
<button type="button" id="btnMapTx">TX</button>
|
||
<button type="button" id="btnMapRx">RX</button>
|
||
<button type="button" id="btnMapBoth">Оба</button>
|
||
<button type="button" id="btnMapRuler">Линейка</button>
|
||
<button type="button" id="btnApiDebug">API</button>
|
||
</div>
|
||
<div id="apiDebugPanel">
|
||
<div id="apiDebugHead">
|
||
<span>RF API · расчёты</span>
|
||
<button type="button" id="btnApiDebugClose" aria-label="Закрыть">×</button>
|
||
</div>
|
||
<label>API base URL
|
||
<input type="text" id="apiDebugBase" />
|
||
</label>
|
||
<div class="api-debug-help">По умолчанию запросы идут через серверный proxy <b>/rf-api</b>, не напрямую из браузера на порт 5603.</div>
|
||
<div class="api-debug-grid">
|
||
<label>TX lat<input type="number" id="apiDebugTxLat" step="0.000001" /></label>
|
||
<label>TX lon<input type="number" id="apiDebugTxLon" step="0.000001" /></label>
|
||
<label>RX lat<input type="number" id="apiDebugRxLat" step="0.000001" /></label>
|
||
<label>RX lon<input type="number" id="apiDebugRxLon" step="0.000001" /></label>
|
||
<label>TX height, m<input type="number" id="apiDebugTxH" step="1" value="30" /></label>
|
||
<label>RX height, m<input type="number" id="apiDebugRxH" step="1" value="2" /></label>
|
||
<label>Frequency, MHz<input type="number" id="apiDebugFreq" step="1" value="433" /></label>
|
||
<label>Samples<input type="number" id="apiDebugSamples" min="2" max="2048" value="128" /></label>
|
||
<label>Модель budget
|
||
<select id="apiDebugModel">
|
||
<option value="manual" selected>manual</option>
|
||
<option value="itm">itm</option>
|
||
<option value="p452">p452</option>
|
||
</select>
|
||
</label>
|
||
<label class="api-debug-check"><input type="checkbox" id="apiDebugIncludeCanopy" />canopy в профиле</label>
|
||
</div>
|
||
<div class="api-debug-section">
|
||
<div class="api-debug-section-title">1. Выбор точек</div>
|
||
<div class="api-debug-help">Для линии нужны две точки: TX и RX. Самый простой способ — нажать “Два клика TX→RX”, затем кликнуть на карте первую и вторую точку.</div>
|
||
<div class="api-debug-actions">
|
||
<button type="button" id="btnApiPickTx">Клик: TX</button>
|
||
<button type="button" id="btnApiPickRx">Клик: RX</button>
|
||
<button type="button" id="btnApiPickLink">Два клика TX→RX</button>
|
||
<button type="button" id="btnApiDebugUseRuler">A/B линейки</button>
|
||
<button type="button" id="btnApiDebugUseMap">TX/RX устройств</button>
|
||
<button type="button" id="btnApiClearPoints">Очистить точки</button>
|
||
</div>
|
||
</div>
|
||
<div class="api-debug-section">
|
||
<div class="api-debug-section-title">2. Расчёт</div>
|
||
<div class="api-debug-actions">
|
||
<button type="button" id="btnApiDebugLine">Расчёт линии</button>
|
||
<button type="button" data-api-demo="elevation">Высота TX</button>
|
||
<button type="button" data-api-demo="profile">Только профиль</button>
|
||
<button type="button" data-api-demo="los">Только LOS/Френель</button>
|
||
<button type="button" data-api-demo="fresnelSlice">Срез Френеля</button>
|
||
<button type="button" data-api-demo="budget">Только link budget</button>
|
||
<button type="button" data-api-demo="buildings">Здания вдоль линии</button>
|
||
<button type="button" data-api-demo="landcover">Landcover path</button>
|
||
<button type="button" data-api-demo="beam">Сектор антенны</button>
|
||
</div>
|
||
</div>
|
||
<div class="api-debug-section">
|
||
<div class="api-debug-section-title">3. Async jobs (Celery)</div>
|
||
<div class="api-debug-help">Viewshed и coverage ставятся в очередь; UI опрашивает <code>/api/v1/jobs/{id}</code> и подгружает GeoJSON артефакт. Coverage с <b>FSPL</b> даёт круг (без рельефа) — для реалистичного контура выберите <b>ITM</b>.</div>
|
||
<div class="api-debug-grid" style="margin-top:6px">
|
||
<label>Модель coverage
|
||
<select id="apiDebugCoverageModel">
|
||
<option value="itm" selected>itm</option>
|
||
<option value="fspl">fspl</option>
|
||
<option value="p1812">p1812</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="api-debug-actions">
|
||
<button type="button" data-api-demo="viewshed">Viewshed</button>
|
||
<button type="button" data-api-demo="coverage">Coverage</button>
|
||
</div>
|
||
</div>
|
||
<div class="api-debug-section">
|
||
<div class="api-debug-section-title">4. Диагностика API</div>
|
||
<div class="api-debug-actions">
|
||
<button type="button" data-api-demo="health">Health</button>
|
||
<button type="button" data-api-demo="data">Data status</button>
|
||
<button type="button" data-api-demo="openapi">OpenAPI paths</button>
|
||
</div>
|
||
</div>
|
||
<div id="apiDebugStatus">Готово</div>
|
||
<div class="api-debug-mini">“Расчёт линии” — profile + LOS + link budget (manual/ITM/P.452). Landcover — если растр есть. Coverage/viewshed — async через Celery.</div>
|
||
<canvas id="apiDebugChart" width="800" height="140"></canvas>
|
||
<div class="api-debug-legend">
|
||
<span><i class="api-debug-swatch" style="background:#00ff88"></i> поверхность/препятствия</span>
|
||
<span><i class="api-debug-swatch" style="background:#ff9800"></i> луч TX→RX</span>
|
||
<span><i class="api-debug-swatch" style="background:#ffd54f"></i> границы 60% зоны Френеля</span>
|
||
<span><i class="api-debug-swatch" style="background:#ba68c8"></i> границы 100% зоны Френеля</span>
|
||
<span><i class="api-debug-swatch" style="background:#ff1744"></i> нарушения зоны Френеля</span>
|
||
<label><input type="checkbox" id="apiDebugShowFresnel" checked /> зона Френеля</label>
|
||
</div>
|
||
<div id="apiDebugSummary">Нет расчёта.</div>
|
||
<pre id="apiDebugOutput">Выберите запрос для проверки RF API.</pre>
|
||
</div>
|
||
<div id="mapRulerPanel">
|
||
<div id="mapRulerHead">
|
||
<span>Линейка высот <span id="mapRulerStatus"></span></span>
|
||
<button type="button" id="btnMapRulerClose" aria-label="Закрыть">×</button>
|
||
</div>
|
||
<div id="mapRulerTools">
|
||
<button type="button" id="btnRulerPick" class="active">Точки</button>
|
||
<button type="button" id="btnRulerAutoTxRx">TX→RX</button>
|
||
<button type="button" id="btnRulerClear">Сброс</button>
|
||
</div>
|
||
<div id="mapRulerHint">Клик на карте — точка A, затем точка B</div>
|
||
<div id="mapRulerPointsRow">
|
||
<label><input type="checkbox" id="mapRulerPointsAuto" checked /> Авто</label>
|
||
<input type="range" id="mapRulerPointsSlider" min="20" max="500" value="100" disabled />
|
||
<span id="mapRulerPointsLabel">100 точек</span>
|
||
</div>
|
||
<canvas id="mapRulerCanvas" width="800" height="120"></canvas>
|
||
</div>
|
||
<div id="map"></div>
|
||
</div>
|
||
<aside>
|
||
<div class="panel" style="flex:1;display:flex;flex-direction:column">
|
||
<h2>Чат</h2>
|
||
<div id="chatLog"></div>
|
||
<form id="chatForm">
|
||
<input type="text" id="chatInput" placeholder="Сообщение…" autocomplete="off" />
|
||
<button type="submit">→</button>
|
||
</form>
|
||
</div>
|
||
<div class="panel">
|
||
<h2>Устройства</h2>
|
||
<ul id="deviceList"></ul>
|
||
</div>
|
||
<div class="panel">
|
||
<h2>Статистика</h2>
|
||
<div id="stats">Выберите устройство</div>
|
||
<div id="history" class="muted" style="margin-top:8px"></div>
|
||
</div>
|
||
<div class="panel" id="controlPanel">
|
||
<h2>Управление</h2>
|
||
<label class="muted">Целевое устройство</label>
|
||
<select id="cmdTargetSelect"><option value="">—</option></select>
|
||
<div id="cmdCurrentValues" class="muted" style="margin-top:4px;font-size:0.75rem">—</div>
|
||
<div id="cmdDraftHint" class="muted" style="display:none;margin-top:4px;font-size:0.7rem;color:#ffc107">Черновик — значения с устройства не подставляются</div>
|
||
<div class="cmd-row" style="margin-top:6px">
|
||
<label class="muted" style="flex:1">FQ MHz<input type="number" id="cmdFq" step="0.001" placeholder="433" /></label>
|
||
<label class="muted" style="flex:1">PW dBm<input type="number" id="cmdPw" min="-9" max="22" placeholder="22" /></label>
|
||
</div>
|
||
<div class="cmd-row">
|
||
<label class="muted" style="flex:1">SF<input type="number" id="cmdSf" min="5" max="12" placeholder="7" /></label>
|
||
<label class="muted" style="flex:1">PL<input type="number" id="cmdPl" min="1" max="64" placeholder="8" /></label>
|
||
</div>
|
||
<label class="muted">BW kHz</label>
|
||
<select id="cmdBw">
|
||
<option value="">—</option>
|
||
<option>7.81</option><option>10.42</option><option>15.63</option><option>20.83</option>
|
||
<option>31.25</option><option>41.67</option><option>62.5</option>
|
||
<option selected>125</option><option>250</option><option>500</option>
|
||
</select>
|
||
<label class="muted">CR</label>
|
||
<select id="cmdCr">
|
||
<option value="">—</option><option>4/5</option><option>4/6</option><option>4/7</option><option>4/8</option>
|
||
</select>
|
||
<label class="muted">TM ms</label>
|
||
<input type="number" id="cmdTm" min="0" max="60000" placeholder="0" />
|
||
<label class="muted">Роль</label>
|
||
<select id="cmdRole"><option value="">—</option><option value="TX">TX</option><option value="RX">RX</option></select>
|
||
<button type="button" id="btnCmdApply" style="width:100%;margin-top:6px;padding:6px;background:#e94560;color:#fff;border:none;border-radius:4px;font-weight:600;cursor:pointer">Применить</button>
|
||
<button type="button" id="btnPairedStart" style="width:100%;margin-top:8px;padding:6px;background:#00ff88;color:#111;border:none;border-radius:4px;font-weight:600;cursor:pointer">Старт трека (оба)</button>
|
||
<div id="pairedStatus">—</div>
|
||
</div>
|
||
<div class="panel" id="macroPresetPanel">
|
||
<h2>Макро-пресеты</h2>
|
||
<div id="macroPresetList" class="preset-list">
|
||
<div class="muted">Загрузка…</div>
|
||
</div>
|
||
<div class="preset-actions">
|
||
<button type="button" id="btnMacroPresetApply">Применить</button>
|
||
<button type="button" id="btnMacroPresetCreate">Создать</button>
|
||
<button type="button" id="btnMacroPresetEdit">Редактировать</button>
|
||
</div>
|
||
<div id="macroPresetStatus" class="muted">—</div>
|
||
</div>
|
||
<div class="panel" id="referencePointPanel">
|
||
<h2>Реперные точки</h2>
|
||
<button type="button" id="btnReferenceRefresh">Обновить</button>
|
||
<select id="referencePointSelect"><option value="">— сессия —</option></select>
|
||
<div id="referencePointSummary" class="muted">Нет данных</div>
|
||
<canvas id="referencePointChart" width="320" height="140"></canvas>
|
||
</div>
|
||
<div class="panel">
|
||
<h2>Треки</h2>
|
||
<div class="track-mode">
|
||
<button type="button" id="btnModeSingle" class="active">Один трек</button>
|
||
<button type="button" id="btnModeDual">Сравнение TX+RX</button>
|
||
</div>
|
||
<div id="trackPanelSingle">
|
||
<div class="track-row">
|
||
<label>Трек</label>
|
||
<select id="trackSingleSelect"><option value="">—</option></select>
|
||
</div>
|
||
</div>
|
||
<div id="trackPanelDual" style="display:none">
|
||
<div class="track-row">
|
||
<label class="legend-tx">Трек TX</label>
|
||
<select id="trackTxSelect"><option value="">—</option></select>
|
||
</div>
|
||
<div class="track-row">
|
||
<label class="legend-rx">Трек RX</label>
|
||
<select id="trackRxSelect"><option value="">—</option></select>
|
||
</div>
|
||
</div>
|
||
<div class="track-actions">
|
||
<button type="button" id="btnShowTracks">Показать на карте</button>
|
||
<button type="button" id="btnHideTracks" disabled>Скрыть треки</button>
|
||
</div>
|
||
<div id="trackInfo" class="muted" style="margin-top:6px">Выберите трек</div>
|
||
<div id="timelineStatsPanel">
|
||
<h3 style="margin:0 0 6px;font-size:0.9rem">Статистика по времени</h3>
|
||
<div id="timelineNote"></div>
|
||
<div id="distanceNow">Расстояние GPS: —</div>
|
||
<div id="timelineStats">—</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
<div id="trackTimeline">
|
||
<div class="timeline-bar">
|
||
<div class="timeline-bar-header">
|
||
<span class="timeline-bar-title">Время теста</span>
|
||
<button type="button" id="btnPlay" class="muted" style="padding:4px 10px;border:none;border-radius:4px;cursor:pointer;background:#0a0a14;color:#eee">▶ Play</button>
|
||
</div>
|
||
<div class="timeline-labels">
|
||
<span id="timeStart">—</span>
|
||
<span id="timeCurrent">—</span>
|
||
<span id="timeEnd">—</span>
|
||
</div>
|
||
<input type="range" id="timeSlider" min="0" max="100" value="0" step="0.1" />
|
||
<div id="elevationPanel">
|
||
<div id="elevationPanelTitle">
|
||
<span>Линейка высот <span id="elevationStatus" class="muted">—</span></span>
|
||
<span class="elev-legend"><span class="legend-tx">TX</span> <span class="legend-rx">RX</span></span>
|
||
</div>
|
||
<canvas id="elevationCanvas" width="800" height="130"></canvas>
|
||
</div>
|
||
<div id="qualityVizPanel">
|
||
<div class="quality-viz-box">
|
||
<div class="quality-viz-title">RX Quality vs расстояние TX↔RX · колёсико — шаг шкалы</div>
|
||
<canvas id="qualityDistCanvas" width="800" height="140"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
<div id="mapModal">
|
||
<div id="mapModalHeader">
|
||
<span>Детали</span>
|
||
<button type="button" id="mapModalClose" aria-label="Закрыть">×</button>
|
||
</div>
|
||
<div id="mapModalBody"></div>
|
||
</div>
|
||
<div id="macroPresetModal">
|
||
<div id="macroPresetModalCard">
|
||
<div id="macroPresetModalHeader">
|
||
<span id="macroPresetModalTitle">Пресет</span>
|
||
<button type="button" id="btnMacroPresetClose" aria-label="Закрыть">×</button>
|
||
</div>
|
||
<div id="macroPresetModalBody">
|
||
<input type="hidden" id="presetEditId" />
|
||
<label>Название<input type="text" id="presetEditName" placeholder="SF7 125 RX" /></label>
|
||
<label>Описание<input type="text" id="presetEditDescription" placeholder="Быстрая конфигурация устройства" /></label>
|
||
<div class="preset-edit-grid">
|
||
<label>FQ MHz<input type="number" id="presetEditFq" step="0.001" placeholder="433" /></label>
|
||
<label>PW dBm<input type="number" id="presetEditPw" min="-9" max="22" placeholder="22" /></label>
|
||
<label>SF<input type="number" id="presetEditSf" min="5" max="12" placeholder="7" /></label>
|
||
<label>PL<input type="number" id="presetEditPl" min="1" max="64" placeholder="8" /></label>
|
||
<label>BW kHz
|
||
<select id="presetEditBw">
|
||
<option value="">—</option>
|
||
<option>7.81</option><option>10.42</option><option>15.63</option><option>20.83</option>
|
||
<option>31.25</option><option>41.67</option><option>62.5</option>
|
||
<option selected>125</option><option>250</option><option>500</option>
|
||
</select>
|
||
</label>
|
||
<label>CR
|
||
<select id="presetEditCr">
|
||
<option value="">—</option><option>4/5</option><option>4/6</option><option>4/7</option><option>4/8</option>
|
||
</select>
|
||
</label>
|
||
<label>TM ms<input type="number" id="presetEditTm" min="0" max="60000" placeholder="0" /></label>
|
||
<label>Роль
|
||
<select id="presetEditRole"><option value="">—</option><option value="TX">TX</option><option value="RX">RX</option></select>
|
||
</label>
|
||
</div>
|
||
<div class="preset-modal-actions">
|
||
<button type="button" id="btnMacroPresetSave">Сохранить</button>
|
||
<button type="button" id="btnMacroPresetDelete">Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script src="/static/radio-ui.js"></script>
|
||
<script src="/static/quality-viz.js"></script>
|
||
<script src="/static/app.js"></script>
|
||
</body>
|
||
</html>
|