Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
e0040cf
TuneWizard: дизайн мастера настройки
deathnemo Oct 23, 2026
041738c
TuneWizard: план этапа 1 — каркас, темы, шаг 0
deathnemo Oct 23, 2026
dd7b9c7
TuneWizard: ядро log.js — сведения о логе, параметры, газ, VIBE
deathnemo Oct 23, 2026
742ed1f
TuneWizard: vibe_summary читает VIBE по экземплярам
deathnemo Oct 23, 2026
853e321
TuneWizard: log.js — переименовать max_of, убрать IMU из длительности…
deathnemo Oct 23, 2026
d7e45c4
TuneWizard: log.js — старый формат VIBE, границы времени, мелкие прав…
deathnemo Oct 23, 2026
e67b230
TuneWizard: ядро checks.js — проверки пригодности лога
deathnemo Oct 23, 2026
9be107c
TuneWizard: checks.js — советы по битам, газ висения с учётом предело…
deathnemo Oct 23, 2026
d78a6c3
TuneWizard: каркас страницы, темы и полоса шагов
deathnemo Oct 23, 2026
329e952
TuneWizard: шаг 0 — скрытие секций и favicon по проверке в браузере
deathnemo Oct 23, 2026
e46a68f
TuneWizard: шаг 0 — ошибки загрузки, интервал, тёмный ползунок и прав…
deathnemo Oct 24, 2026
7aad39d
TuneWizard: Readme, браузерный smoke-тест шага 0 и завершение этапа 1
deathnemo Oct 24, 2026
1b61d27
TuneWizard: надёжность браузерного теста и единый путь ошибок загрузки
deathnemo Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ node_modules/

# Local deployment backups
backup/

# TuneWizard: local flight logs for manual checks
TuneWizard/logs/
26 changes: 26 additions & 0 deletions TuneWizard/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Мастер настройки

Пошаговый мастер настройки Copter по одному логу `.bin`: проверка лога, спектр гироскопов, гармонический notch, низкочастотные фильтры, PID по осям, сводка и файл параметров. Объединяет расчёты FilterReview и PIDReview; оригинальные инструменты не изменяет.

Дизайн: `docs/plans/2026-10-23-tune-wizard-design.md`. Планы этапов — в `docs/plans/`.

### Запуск

Из корня репозитория: `python -m http.server --bind 127.0.0.1`, затем `http://127.0.0.1:8000/TuneWizard/`.

### Структура

- `index.html`, `wizard.js` — страница, состояние, навигация по шагам, графики Plotly в цветах темы.
- `core/` — математика и проверки без DOM; те же файлы гоняются под Node: `node --test tests/tunewizard.test.cjs`.
- `logs/` — локальные логи и скриншоты для проверок, в репозиторий не попадают.

### Проверки

- `npm run lint` — ESLint по странице и скриптам.
- `npm test` — тесты ядра (вместе с остальными тестами репозитория).
- `npm run test:tunewizard` — smoke-прогон шага 0 в Chrome на реальном логе (см. `tests/README.md`).

### Готовность

- Этап 1: каркас, темы, шаг 0 (лог и проверки) — готово.
- Этапы 2–6: спектр и notch, фильтры, PID, сводка, лендинг — в работе.
161 changes: 161 additions & 0 deletions TuneWizard/core/checks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Проверки пригодности лога (шаг 0). Чистые функции без DOM.
// Источники порогов: [wiki] — ardupilot.org, [код] — исходники ArduPilot, [наше] — эвристика, у ArduPilot числа нет.

const check_rules = {
vibe_warn: 30, // м/с²: выше — возможны проблемы [wiki, Measuring Vibration]
vibe_fail: 60, // м/с²: выше — почти наверняка проблемы [wiki, Measuring Vibration]
hover_min_s: 30, // с: минимальное висение для усреднения спектра [wiki, Initial tuning flight]
hover_throttle_tolerance: 0.05, // допустимое расхождение MOT_THST_HOVER и среднего газа [наше]
hover_learn_min: 0.125, // нижний предел обучения MOT_THST_HOVER [код, AP_MotorsMulticopter.h AP_MOTORS_THST_HOVER_MIN]
hover_learn_max: 0.6875, // верхний предел обучения MOT_THST_HOVER [код, AP_MotorsMulticopter.h AP_MOTORS_THST_HOVER_MAX]
hover_learn_s: 30, // с висения для обучения MOT_THST_HOVER: постоянная времени 10 с, ~95 % за 30 с [код, AP_MOTORS_THST_HOVER_TC]
log_bitmask_pid: 12, // номер бита PID в LOG_BITMASK [код, ArduCopter/defines.h MASK_LOG_PID]
log_bitmask_imu_raw: 19, // номер бита IMU_RAW [код, MASK_LOG_IMU_RAW]
}

// info — из log_info, params — из log_params, vibe — из vibe_summary (может быть null).
// Возвращает список { id, status: "ok" | "warn" | "fail", title, text, fix }.
function run_checks(info, params, vibe) {
const checks = []
const add = (id, status, title, text, fix) => checks.push({ id, status, title, text, fix: fix ?? null })
const m = info.messages
const r = check_rules

// Аппарат
if (info.vehicle === "Copter") {
add("vehicle", "ok", "Аппарат", "Copter, " + (info.fw_string ?? "версия неизвестна") + (info.board ? ", " + info.board : ""))
} else {
add("vehicle", "fail", "Аппарат", "Лог от " + (info.vehicle ?? "неизвестного аппарата") + "; мастер поддерживает только Copter", "Загрузите лог ArduCopter")
}

// Сообщения PID
const pid_fix = "Включите бит PID (" + r.log_bitmask_pid + ") в LOG_BITMASK и перелетите"
if (m.pid) {
add("pid_log", "ok", "Сообщения PID", "PIDR/PIDP/PIDY есть: доступны компоненты P/I/D/FF, отфильтрованные цель и ошибка")
} else if (m.rate) {
add("pid_log", "warn", "Сообщения PID", "Есть только RATE: на шаге 4 не будет компонент P/I/D", pid_fix)
} else {
add("pid_log", "fail", "Сообщения PID", "Нет ни PID, ни RATE: шаг 4 недоступен", pid_fix)
}

// Данные гироскопа
if (m.gyro_raw || m.gyro_batch) {
add("gyro_log", "ok", "Данные гироскопа", "Есть " + (m.gyro_raw ? "raw (GYR)" : "batch (ISBH/ISBD)") + ": шаги 1–3 доступны")
} else {
add("gyro_log", "fail", "Данные гироскопа", "Нет ни raw (GYR), ни batch (ISBH/ISBD): шаги 1–3 недоступны",
"Batch: INS_LOG_BAT_MASK = 1, INS_LOG_BAT_OPT = 4. Raw (нужна быстрая плата, обычно H7): бит IMU_RAW (" + r.log_bitmask_imu_raw + ") в LOG_BITMASK, INS_RAW_LOG_OPT = 8")
}

// До/после фильтров
if (m.gyro_raw) {
const opt = params.get("INS_RAW_LOG_OPT") ?? 0
if (opt & (1 << 3)) {
add("pre_post", "ok", "До/после фильтров", "Raw-лог до и после фильтров: оценку «станет» можно сверить с реальными данными")
} else if (opt & (1 << 2)) {
add("pre_post", "warn", "До/после фильтров", "Raw-лог только после фильтров: спектра до фильтров и оценки «станет» не будет",
"Добавьте бит 3 (+8) к INS_RAW_LOG_OPT и снимите бит 2 (−4): логирование до и после фильтров")
} else {
add("pre_post", "ok", "До/после фильтров", "Raw-лог только до фильтров: оценка «станет» строится расчётом, реальных данных после фильтров нет")
}
} else if (m.gyro_batch) {
const opt = params.get("INS_LOG_BAT_OPT") ?? 0
if (opt & (1 << 2)) {
add("pre_post", "ok", "До/после фильтров", "Batch-лог до и после фильтров: оценку «станет» можно сверить с реальными данными")
} else if (opt & (1 << 1)) {
add("pre_post", "warn", "До/после фильтров", "Batch-лог только после фильтров: спектра до фильтров и оценки «станет» не будет",
"Добавьте бит 2 (+4) к INS_LOG_BAT_OPT и снимите бит 1 (−2): логирование до и после фильтров")
} else {
add("pre_post", "ok", "До/после фильтров", "Batch-лог только до фильтров: оценка «станет» строится расчётом, реальных данных после фильтров нет")
}
}

// Источник трекинга notch
if (m.esc) {
add("tracking", "ok", "Источник трекинга notch", "ESC-телеметрия (ESC): лучший источник, сам подстраивается под нагрузку")
} else if (m.rpm) {
add("tracking", "ok", "Источник трекинга notch", "Датчик оборотов (RPM)")
} else {
add("tracking", "warn", "Источник трекинга notch", "Нет ESC-телеметрии и RPM: доступен только трекинг по газу (INS_HNTCH_MODE = 1)",
"Если регуляторы умеют телеметрию или двунаправленный DShot — включите, это точнее")
}

// Полёт
if (info.flight == null) {
if (info.throttle_source == null) {
add("flight", "fail", "Полёт", "Газ не определить: в логе нет CTUN и RATE")
} else {
// Окно полёта схлопывается, если ненулевой газ короче отступов throttle_window
add("flight", "fail", "Полёт", "Ненулевого газа нет или он короче 3 с: полёта в логе нет")
}
} else if (info.flight.duration_s < r.hover_min_s) {
add("flight", "warn", "Полёт", "Полёт с газом " + Math.round(info.flight.duration_s) + " с: мало для усреднения спектра", "Висение не меньше " + r.hover_min_s + " с без ввода")
} else {
const mean_text = (info.flight.mean_throttle != null) ? ", средний газ " + info.flight.mean_throttle.toFixed(2) : ""
add("flight", "ok", "Полёт", "Полёт с газом " + Math.round(info.flight.duration_s) + " с" + mean_text)
}

// Вибрации
if (vibe == null) {
add("vibe", "warn", "Вибрации", "Сообщений VIBE нет: уровень вибраций не проверить")
} else {
if (!Number.isFinite(vibe.level)) {
add("vibe", "warn", "Вибрации", "Уровень вибраций не определён (нет корректных данных VIBE)")
} else {
// Одна десятичная: 59.6 рядом с порогом 60 не должно читаться как «60»
const text = "Уровень (95-й процентиль) " + vibe.level.toFixed(1) + " м/с², максимум " + vibe.max.toFixed(1) + " м/с²"
if (vibe.level >= r.vibe_fail) {
add("vibe", "fail", "Вибрации", text + ": выше " + r.vibe_fail + " — почти наверняка проблемы", "Проверьте крепление автопилота, балансировку пропеллеров и моторов")
} else if (vibe.level >= r.vibe_warn) {
add("vibe", "warn", "Вибрации", text + ": выше " + r.vibe_warn + " — возможны проблемы", "Проверьте крепление автопилота и балансировку пропеллеров")
} else {
add("vibe", "ok", "Вибрации", text)
}
}
if (vibe.clip > 0) {
add("clipping", "warn", "Clipping", vibe.clip + " событий clipping акселерометра", "Проверьте крепление автопилота и балансировку пропеллеров")
} else {
add("clipping", "ok", "Clipping", "Событий clipping нет")
}
}

// Газ висения против MOT_THST_HOVER. Расхождение считается не к самому среднему газу, а к тому значению,
// которое обучение вообще может дать: ArduPilot зажимает MOT_THST_HOVER в пределах hover_learn_min…hover_learn_max
const hover = params.get("MOT_THST_HOVER")
const mean = info.flight?.mean_throttle ?? null
if ((hover != null) && (mean != null)) {
const target = Math.min(Math.max(mean, r.hover_learn_min), r.hover_learn_max)
const diff = Math.abs(hover - target)
// Пояснение, почему значение не совпадёт с газом даже после обучения — в обеих ветках
const limit_note = (mean < r.hover_learn_min)
? " (обучение ограничено снизу значением " + r.hover_learn_min + " — для очень мощного аппарата это нормально)"
: ""
let text = "MOT_THST_HOVER " + hover.toFixed(2) + ", средний газ в полёте " + mean.toFixed(2)
if (diff > r.hover_throttle_tolerance) {
const learning = params.get("MOT_HOVER_LEARN") == 2
text += ": расхождение " + diff.toFixed(2) + limit_note
if (learning) {
text += " (обучение включено: большое расхождение значит, что висения в AltHold было мало)"
}
add("hover_throttle", "warn", "Газ висения", text,
learning ? "Повисите не меньше " + r.hover_learn_s + " с в AltHold, чтобы значение обучилось" : "MOT_HOVER_LEARN = 2 (обучение с сохранением)")
} else {
add("hover_throttle", "ok", "Газ висения", text + limit_note)
}
}

return checks
}

// Итог по списку проверок
function checks_summary(checks) {
const count = { ok: 0, warn: 0, fail: 0 }
for (const c of checks) {
count[c.status]++
}
const status = (count.fail > 0) ? "fail" : (count.warn > 0) ? "warn" : "ok"
return { status, ...count }
}

if (typeof module !== "undefined") {
module.exports = { check_rules, run_checks, checks_summary }
}
Loading