poe-svelte-ui-lib 1.42.0 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,47 +16,64 @@
16
16
 
17
17
  Поток телеметрии (`Telemetry_SendObject`, ключ `Data`) — облако раскладывает поля в значения группы:
18
18
 
19
- | Поле | Что | Источник MAVLink | Частота |
20
- | ---------------------------------------- | ----------------------------------------------- | ---------------------------- | ------- |
21
- | `MAV.Roll`, `MAV.Pitch`, `MAV.Heading` | крен, тангаж, курс (°) | ATTITUDE | 20 Гц |
22
- | `MAV.GSpeed`, `MAV.ASpeed`, `MAV.Climb` | путевая, воздушная, вертикальная скорость (м/с) | VFR_HUD | 2 Гц |
23
- | `MAV.Alt`, `MAV.AltMSL` | высота над взлётом и над морем (м) | GLOBAL_POSITION_INT, VFR_HUD | 2 Гц |
24
- | `MAV.Lat`, `MAV.Lon`, `MAV.Vx`, `MAV.Vy` | координаты, скорость на север/восток | GLOBAL_POSITION_INT | 2 Гц |
25
- | `MAV.Fix`, `MAV.Sat` | тип фиксации, спутники | GPS_RAW_INT | 2 Гц |
26
- | `MAV.BV`, `MAV.BC`, `MAV.BRem` | батарея: В, А (-1 нет), % (-1 нет) | SYS_STATUS | 2 Гц |
27
- | `MAV.Mode`, `MAV.VType`, `MAV.Armed` | custom_mode, MAV_TYPE, взведён | HEARTBEAT | 2 Гц |
28
- | `MAV.HomeLat`, `MAV.HomeLon` | точка взлёта (0 — нет) | HOME_POSITION | 2 Гц |
29
- | `MAV.Press`, `MAV.Temp` | давление (гПа), температура (°C) | SCALED_PRESSURE | 2 Гц |
19
+ | Поле | Что | Источник MAVLink | Частота |
20
+ | ---------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------ | ------------- |
21
+ | `MAV.Roll`, `MAV.Pitch`, `MAV.Heading` | крен, тангаж, курс (°) | ATTITUDE | 20 Гц |
22
+ | `MAV.GSpeed`, `MAV.ASpeed`, `MAV.Climb` | путевая, воздушная, вертикальная скорость (м/с) | VFR_HUD | 2 Гц |
23
+ | `MAV.Alt`, `MAV.AltMSL` | высота над взлётом и над морем (м) | GLOBAL_POSITION_INT, VFR_HUD | 2 Гц |
24
+ | `MAV.Lat`, `MAV.Lon`, `MAV.Vx`, `MAV.Vy` | координаты, скорость на север/восток | GLOBAL_POSITION_INT | 2 Гц |
25
+ | `MAV.Fix`, `MAV.Sat` | тип фиксации, спутники | GPS_RAW_INT | 2 Гц |
26
+ | `MAV.BV`, `MAV.BC`, `MAV.BRem` | батарея: В, А (-1 нет), % (-1 нет) | SYS_STATUS | 2 Гц |
27
+ | `MAV.Mode`, `MAV.VType`, `MAV.Armed` | custom_mode, MAV_TYPE, взведён | HEARTBEAT | 2 Гц |
28
+ | `MAV.HomeLat`, `MAV.HomeLon` | точка взлёта (0 — нет) | HOME_POSITION | 2 Гц |
29
+ | `MAV.Press`, `MAV.Temp` | давление (гПа), температура (°C) | SCALED_PRESSURE | 2 Гц |
30
+ | `MAV.WPNum`, `MAV.WPDist`, `MAV.WPBrg` | номер точки маршрута, расстояние (м) и курс на неё (°) | MISSION_CURRENT, NAV_CONTROLLER_OUTPUT | 2 Гц |
31
+ | `MAV.AltErr`, `MAV.SpdErr`, `MAV.XTE` | ошибка высоты и скорости (цель − текущее), боковое отклонение (м) | NAV_CONTROLLER_OUTPUT | 2 Гц |
32
+ | `MAV.RSSI`, `MAV.RemRSSI`, `MAV.Drop` | сигнал своей/удалённой стороны (%), потери пакетов (%) | RADIO_STATUS, SYS_STATUS | 2 Гц |
33
+ | `MAV.HDOP`, `MAV.EKF`, `MAV.SensErr` | точность GPS, наибольшая дисперсия EKF, число неисправных датчиков | GPS_RAW_INT, EKF_STATUS_REPORT, SYS_STATUS | 2 Гц |
34
+ | `MAV.Msg`, `MAV.MsgSev`, `MAV.MsgSeq` | сообщение автопилота, важность, номер | STATUSTEXT | при появлении |
30
35
 
31
36
  Расстояние и пеленг на дом, трек и возраст данных виджет считает сам.
32
37
 
33
38
  ## Ключи
34
39
 
35
- | Ключ | Описание |
36
- | -------------------- | --------------------------------------------------------------------------------------------------- |
37
- | `Roll`, `Pitch` | числа, °: крен (вправо +) и тангаж (вверх +) |
38
- | `Heading` | число, 0–360°: курс |
39
- | `GSpeed`, `ASpeed` | числа, м/с: путевая и воздушная скорость |
40
- | `Alt` | число, м: высота над точкой взлёта; `AltMSL` — над уровнем моря |
41
- | `Climb` | число, м/с: вертикальная скорость (вверх +) |
42
- | `Lat`, `Lon` | числа, °: координаты |
43
- | `Vx`, `Vy` | числа, м/с: скорость на север и восток — для трека |
44
- | `Fix` | число: тип фиксации GPS, `0`–`1` нет, `2` 2D, `3` 3D, `4`–`6` DGPS/RTK; `Sat` — число спутников |
45
- | `BV` | В; `BC` — А (`-1` не измеряется); `BRem` — % (`-1` неизвестно) |
46
- | `Mode` | число: режим автопилота; `VType` — тип аппарата (по нему — названия режимов); `Armed` — `1` взведён |
47
- | `HomeLat`, `HomeLon` | числа, °: точка взлёта, `0` — неизвестна |
48
- | `Press` | гПа, `Temp` — °C |
40
+ | Ключ | Описание |
41
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
42
+ | `Roll`, `Pitch` | числа, °: крен (вправо +) и тангаж (вверх +) |
43
+ | `Heading` | число, 0–360°: курс |
44
+ | `GSpeed`, `ASpeed` | числа, м/с: путевая и воздушная скорость |
45
+ | `Alt` | число, м: высота над точкой взлёта; `AltMSL` — над уровнем моря |
46
+ | `Climb` | число, м/с: вертикальная скорость (вверх +) |
47
+ | `Lat`, `Lon` | числа, °: координаты |
48
+ | `Vx`, `Vy` | числа, м/с: скорость на север и восток — для трека |
49
+ | `Fix` | число: тип фиксации GPS, `0`–`1` нет, `2` 2D, `3` 3D, `4`–`6` DGPS/RTK; `Sat` — число спутников |
50
+ | `BV` | В; `BC` — А (`-1` не измеряется); `BRem` — % (`-1` неизвестно) |
51
+ | `Mode` | число: режим автопилота; `VType` — тип аппарата (по нему — названия режимов); `Armed` — `1` взведён |
52
+ | `HomeLat`, `HomeLon` | числа, °: точка взлёта, `0` — неизвестна |
53
+ | `Press` | гПа, `Temp` — °C |
54
+ | `WPNum` | число: номер текущей точки маршрута; `0` — маршрута нет, метки целей не рисуются |
55
+ | `WPDist`, `WPBrg` | числа: расстояние до точки, м; курс на неё, 0–360° — бирюзовый ◆ на ленте курса |
56
+ | `AltErr`, `SpdErr` | числа: цель минус текущее — высота, м, и воздушная скорость, м/с; бирюзовые метки на лентах |
57
+ | `XTE` | число, м: боковое отклонение от линии маршрута |
58
+ | `RSSI`, `RemRSSI` | числа, %: уровень сигнала своей и удалённой стороны; `-1` — неизвестно |
59
+ | `Drop` | число, %: потери пакетов телеметрии |
60
+ | `HDOP` | число: точность GPS по горизонтали (до 1 — отлично, больше 2 — неточно); `-1` — неизвестно |
61
+ | `EKF` | число: наибольшая дисперсия навигационного фильтра; от `0.5` — внимание, от `0.8` — тревога (индикатор «НАВ») |
62
+ | `SensErr` | число: включённых, но неисправных датчиков; больше 0 — «НАВ» красный |
63
+ | `Msg`, `MsgSev`, `MsgSeq` | сообщение автопилота (строка), важность MAV_SEVERITY (`0`–`3` ошибка, `4` предупреждение, `5`–`7` информация), номер — новое сообщение даже с тем же текстом |
49
64
 
50
65
  ## Особенности
51
66
 
52
67
  Смарт-виджет «Пилотажный дисплей» (Primary Flight Display) — главный прибор контроля полёта, как в стеклянной кабине самолёта или наземной станции управления. Работает с полями группы (по умолчанию `MAV`) ровно в том виде, в каком их отдаёт телеметрия автопилота MAVLink устройства 20.00.000-01. Только отображение: ничего не отправляет и не знает про DeviceStore/WebSocket.
53
68
 
54
- - **Строка состояния** — ARMED/DISARMED, режим полёта (название по типу аппарата: самолёт, коптер, наземный), GPS (тип фиксации и спутники), батарея (В, А, %), расстояние и пеленг на точку взлёта.
69
+ - **Строка состояния** — группы «Система» (ARMED/DISARMED, режим по типу аппарата, индикатор «НАВ» по дисперсии EKF и неисправным датчикам), «GPS» (фиксация, спутники, HDOP), «Связь» (сигнал своей/удалённой стороны, потери пакетов), «Батарея» (В, А, %).
55
70
  - **Авиагоризонт** — небо и земля по крену и тангажу, лестница тангажа через 5°, дуга крена (10/20/30/45/60°), символ самолёта. Ориентация сглаживается между пакетами (Tween 120 мс).
56
71
  - **Лента скорости** (слева, путевая над землёй) и **лента высоты** (справа, над точкой взлёта) с вариометром у края, **лента курса** (снизу): розовый ▲ — трек (куда аппарат реально движется, по `Vx`/`Vy`), зелёный ● — направление на точку взлёта.
57
72
  - **Углы под лентами** — воздушная скорость (относительно воздуха, отличается от путевой на ветер) и вертикальная скорость (+ набор зелёным, − снижение оранжевым).
58
- - **Подсказки** — наведите курсор на любой элемент прибора, строки состояния или значений: всплывающая подсказка с расшифровкой и текущим значением (элементы помечены `data-tip`).
59
- - **Под прибором** — легенда трека и направления на дом с градусами, координаты, высота над уровнем моря, давление, температура.
73
+ - **Подсказки** — наведите курсор на любой элемент прибора, строки состояния или значений: расшифровка появляется в информационной строке внизу горизонта: первая строка — величина, единица и текущее значение (обновляется, пока курсор стоит на месте), ниже по строке — что означают знаки, цвета и пороги. Элемент помечен `data-tip` с ключом, тексты собраны в `tips` в скрипте виджета, поэтому её не обрезают края и прокрутка. Без наведения в той же строке — последнее сообщение автопилота (10 с), наведение на неё показывает историю. Галочка «Подсказки» в шапке включает и выключает их; выбор общий для всех PFD в браузере (`localStorage`, ключ `WidgetPFD.tips`).
74
+ - **Под прибором** — группы «Маршрут» (точка, расстояние, боковое отклонение), «Дом» (расстояние и направление), «Трек», «Позиция» (координаты, MSL), «Воздух» (давление, температура); каждая величина показана один раз.
75
+ - **Метки целей** (бирюзовые) — целевая скорость и высота текущей точки на лентах, курс на точку ◆ на ленте курса; цель за пределами ленты прижимается к краю.
76
+ - **Сообщения автопилота** (`STATUSTEXT`) — в информационной строке внизу горизонта на 10 с, цвет по важности; история последних пяти — при наведении на строку.
60
77
  - **Нет данных** — если пакеты не приходили дольше `limits.staleMs` (по умолчанию 2 с), прибор перечёркивается и помечается «НЕТ ДАННЫХ»; в шапке всегда виден возраст данных.
61
78
  - **Узкая ячейка** (уже 440 px) — ленты скорости и высоты заменяются числами поверх горизонта.
62
79
 
@@ -4,6 +4,7 @@
4
4
  ничего не отправляет и не знает про DeviceStore/WebSocket. Между пакетами ориентация сглаживается (Tween),
5
5
  устаревшие данные закрываются флагом «нет данных». В узкой ячейке ленты заменяются числами. -->
6
6
  <script lang="ts">
7
+ import { untrack } from "svelte"
7
8
  import { slide } from "svelte/transition"
8
9
  import { Tween } from "svelte/motion"
9
10
  import { cubicOut } from "svelte/easing"
@@ -165,6 +166,48 @@
165
166
  let battLow = $derived(limits?.battLow !== undefined && battRem >= 0 && battRem <= limits.battLow)
166
167
  let battCurrent = $derived(num(value?.BC, -1))
167
168
 
169
+ /* Навигация к точке маршрута: метки целей на лентах (flight director); нет маршрута — меток нет */
170
+ let wpNum = $derived(num(value?.WPNum))
171
+ let wp = $derived(
172
+ wpNum > 0
173
+ ? {
174
+ num: wpNum,
175
+ dist: num(value?.WPDist),
176
+ bearing: wrap360(num(value?.WPBrg)),
177
+ altTarget: alt + num(value?.AltErr),
178
+ spdErr: num(value?.SpdErr),
179
+ xte: num(value?.XTE),
180
+ }
181
+ : null,
182
+ )
183
+
184
+ /* Связь, точность GPS, исправность навигации */
185
+ let rssi = $derived(num(value?.RSSI, -1))
186
+ let remRssi = $derived(num(value?.RemRSSI, -1))
187
+ let drop = $derived(num(value?.Drop, -1))
188
+ let hdop = $derived(num(value?.HDOP, -1))
189
+ let sensErr = $derived(num(value?.SensErr))
190
+ let ekf = $derived(num(value?.EKF, -1))
191
+ /* Пороги дисперсии EKF — как в Mission Planner: от 0.5 внимание, от 0.8 тревога; неисправный датчик — тревога */
192
+ let navState: "ok" | "warn" | "bad" | "none" = $derived(sensErr > 0 || ekf >= 0.8 ? "bad" : ekf >= 0.5 ? "warn" : ekf >= 0 ? "ok" : "none")
193
+ const NAV_COLOR = { ok: "var(--green-color)", warn: "var(--orange-color)", bad: "var(--red-color)", none: "var(--faint-color)" }
194
+ const NAV_TEXT = { ok: "НАВ в норме", warn: "НАВ: внимание", bad: "НАВ: тревога", none: "НАВ: нет данных" }
195
+
196
+ /* Сообщения автопилота (STATUSTEXT): новое — по смене MsgSeq, последнее висит 10 с, история из 5 — в подсказке */
197
+ const SEVERITY = ["АВАРИЯ", "ТРЕВОГА", "КРИТИЧНО", "ОШИБКА", "ВНИМАНИЕ", "ЗАМЕЧАНИЕ", "ИНФО", "ОТЛАДКА"]
198
+ let messages: { text: string; sev: number; at: number }[] = $state([])
199
+ let lastSeq = -1
200
+ $effect(() => {
201
+ const seq = value?.MsgSeq
202
+ const text = value?.Msg
203
+ if (seq === undefined || seq === lastSeq || !text) return
204
+ lastSeq = seq
205
+ untrack(() => (messages = [{ text, sev: num(value?.MsgSev, 6), at: Date.now() }, ...messages].slice(0, 5)))
206
+ })
207
+ let banner = $derived(messages[0] && now - messages[0].at < 10_000 ? messages[0] : null)
208
+ const sevClass = (sev: number) => (sev <= 3 ? "bg-(--red-color) text-white" : sev === 4 ? "bg-(--orange-color) text-white" : "bg-black/70 text-white")
209
+ const clampTape = (y: number) => Math.min(Math.max(y, 22), HH - 8)
210
+
168
211
  /* Компактный режим — ленты заменяются числами поверх горизонта */
169
212
  let width = $state(600)
170
213
  let compact = $derived(width < 440)
@@ -206,17 +249,164 @@
206
249
  const fmt = (v: number, digits = 1) => (Number.isFinite(v) ? v.toFixed(digits) : "—")
207
250
  const signed = (v: number) => `${v >= 0 ? "+" : ""}${fmt(v)}`
208
251
 
209
- /* Всплывающая подсказка: элементы прибора и строк помечены data-tip, одна подсказка следует за курсором */
210
- let tip: { text: string; x: number; y: number } | null = $state(null)
252
+ /* Подсказка: элементы прибора и строк помечены data-tip, текст всегда в одном месте — информационная строка внизу горизонта */
253
+ /* Ключ подсказки элемента под курсором; текст берётся из tips — обновляется вместе с данными */
254
+ let tip: string | null = $state(null)
255
+ /* Галочка «Подсказки» в шапке — выбор оператора, общий для всех PFD в браузере */
256
+ const TIPS_KEY = "WidgetPFD.tips"
257
+ let tipsEnabled = $state(true)
258
+ $effect(() => {
259
+ try {
260
+ tipsEnabled = localStorage.getItem(TIPS_KEY) !== "0"
261
+ } catch {
262
+ /* хранилище недоступно — подсказки включены */
263
+ }
264
+ })
265
+ const setTips = (on: boolean) => {
266
+ tipsEnabled = on
267
+ if (!on) tip = null
268
+ try {
269
+ localStorage.setItem(TIPS_KEY, on ? "1" : "0")
270
+ } catch {
271
+ /* хранилище недоступно — выбор действует до перезагрузки */
272
+ }
273
+ }
211
274
  const onTip = (e: PointerEvent) => {
212
- const host = e.currentTarget as HTMLElement
275
+ if (!tipsEnabled) return
213
276
  const el = (e.target as Element | null)?.closest("[data-tip]")
214
- if (!el) return (tip = null)
215
- const r = host.getBoundingClientRect()
216
- tip = { text: el.getAttribute("data-tip") ?? "", x: Math.min(Math.max(e.clientX - r.left, 90), r.width - 90), y: e.clientY - r.top }
277
+ tip = el ? (el.getAttribute("data-tip") ?? null) : null
217
278
  }
218
279
  const fmtDistance = (m: number) => (m >= 1000 ? `${(m / 1000).toFixed(2)} км` : `${Math.round(m)} м`)
219
280
  const fmtAge = (s: number) => (Number.isFinite(s) ? `${s.toFixed(1)} с` : "нет связи")
281
+
282
+ /* Тексты подсказок: первая строка — величина, единица и значение, дальше — что означают знаки, цвета и пороги */
283
+ const lines = (...rows: (string | false | null | undefined)[]) => rows.filter(Boolean).join("\n")
284
+ let tips = $derived.by(() => {
285
+ const hdg = Math.round(wrap360(heading.current))
286
+ const homeLine = home ? `Расстояние: ${fmtDistance(home.distance)}` : null
287
+ return {
288
+ system: lines(
289
+ "Система",
290
+ "ARMED / DISARMED — двигатели взведены / не взведены",
291
+ "Режим — режим работы автопилота",
292
+ "НАВ — исправность навигации (фильтр EKF и датчики)",
293
+ ),
294
+ armed: armed
295
+ ? lines("Двигатели: ВЗВЕДЕНЫ (ARMED)", "Аппарат готов к движению — моторы могут вращаться")
296
+ : lines("Двигатели: НЕ ВЗВЕДЕНЫ (DISARMED)", "Моторы заблокированы"),
297
+ mode: lines(`Режим автопилота: ${modeName}`, "Название зависит от типа аппарата: самолёт, коптер, наземный"),
298
+ nav: lines(
299
+ `Навигация (НАВ): ${{ ok: "норма", warn: "внимание", bad: "тревога", none: "нет данных" }[navState]}`,
300
+ ekf >= 0 && `Дисперсия фильтра EKF: ${fmt(ekf, 2)}`,
301
+ "до 0.5 — норма",
302
+ "0.5…0.8 — внимание",
303
+ "от 0.8 — тревога, позиция может уплыть",
304
+ sensErr > 0 && `Неисправных датчиков: ${sensErr}`,
305
+ ),
306
+ gps: lines(
307
+ `GPS: ${FIX[fix] ?? fix} · ${num(value?.Sat)} спутников`,
308
+ "Фиксация ниже 3D — высота и позиция неточные",
309
+ hdop >= 0 && `HDOP (точность по горизонтали): ${fmt(hdop, 2)}`,
310
+ "до 1 — отлично",
311
+ "1…2 — хорошо",
312
+ "больше 2 — позиция неточная",
313
+ ),
314
+ link: lines(
315
+ `Связь, %: ${rssi >= 0 ? Math.round(rssi) : "—"} / ${remRssi >= 0 ? Math.round(remRssi) : "—"}`,
316
+ "Слева — сигнал на наземной стороне, справа — на аппарате",
317
+ "меньше 30 — связь на пределе",
318
+ drop >= 0 && `Потери пакетов, %: ${fmt(drop)}`,
319
+ ),
320
+ battery: lines(
321
+ `Батарея: ${fmt(num(value?.BV))} В${battCurrent >= 0 ? ` · ${fmt(battCurrent)} А` : ""}${battRem >= 0 ? ` · ${battRem} %` : ""}`,
322
+ "В — напряжение",
323
+ battCurrent >= 0 && "А — ток потребления",
324
+ battRem >= 0 && "% — остаток заряда",
325
+ limits?.battLow !== undefined && `Ниже ${limits.battLow} % — красным`,
326
+ ),
327
+ horizon: lines(
328
+ "Авиагоризонт",
329
+ `Крен, °: ${signed(roll.current)} ("+" — вправо, "−" — влево)`,
330
+ `Тангаж, °: ${signed(pitch.current)} ("+" — нос вверх, "−" — вниз)`,
331
+ "Синий — небо, коричневый — земля",
332
+ ),
333
+ rollPtr: lines(
334
+ `Крен, °: ${signed(roll.current)}`,
335
+ '"+" — вправо',
336
+ '"−" — влево',
337
+ limits?.bankMax !== undefined && `Предел ±${limits.bankMax}° — дальше дуга красная`,
338
+ ),
339
+ speedTape: lines(
340
+ `Путевая скорость, м/с: ${fmt(gs)}`,
341
+ "Скорость над землёй по GPS",
342
+ (limits?.speedMin !== undefined || limits?.speedMax !== undefined) &&
343
+ `Красная полоса — вне диапазона ${limits?.speedMin ?? "…"}…${limits?.speedMax ?? "…"} м/с`,
344
+ wp && "Бирюзовая метка — целевая скорость точки маршрута",
345
+ ),
346
+ speedBox: lines(`Путевая скорость, м/с: ${fmt(gs)}`, "Скорость над землёй по GPS"),
347
+ speedLabel: lines("Лента путевой скорости, м/с", "Скорость над землёй по GPS, текущее значение — в рамке"),
348
+ speedBug: wp
349
+ ? lines(`Целевая скорость точки ${wp.num}, м/с: ${fmt(as + wp.spdErr)}`, `Воздушная, сейчас: ${fmt(as)}`, "Метка у края — цель за пределами ленты")
350
+ : "",
351
+ altTape: lines(
352
+ `Высота над точкой взлёта, м: ${Math.round(alt)}`,
353
+ "Полоса у правого края — вертикальная скорость:",
354
+ "вверх, зелёная — набор",
355
+ "вниз, оранжевая — снижение",
356
+ limits?.altMax !== undefined && `Красная полоса — выше ${limits.altMax} м`,
357
+ wp && "Бирюзовая метка — целевая высота точки маршрута",
358
+ ),
359
+ altBox: lines(`Высота над точкой взлёта, м: ${Math.round(alt)}`, `Над уровнем моря, м: ${Math.round(altMsl)}`),
360
+ altLabel: lines("Лента высоты над точкой взлёта, м", "Текущее значение — в рамке"),
361
+ altBug: wp
362
+ ? lines(
363
+ `Целевая высота точки ${wp.num}, м: ${Math.round(wp.altTarget)}`,
364
+ `Осталось, м: ${signed(wp.altTarget - alt)}`,
365
+ '"+" — набрать',
366
+ '"−" — снизиться',
367
+ )
368
+ : "",
369
+ compactSpeed: lines(`Путевая скорость, м/с: ${fmt(gs)}`, `Воздушная скорость, м/с: ${fmt(as)}`),
370
+ compactAlt: lines(
371
+ `Высота над точкой взлёта, м: ${Math.round(alt)}`,
372
+ `Вертикальная скорость, м/с: ${signed(climb)}`,
373
+ '"+" — набор высоты',
374
+ '"−" — снижение',
375
+ ),
376
+ airspeed: lines(
377
+ `Воздушная скорость, м/с: ${fmt(as)}`,
378
+ "Скорость относительно воздуха",
379
+ "Отличается от путевой на ветер:",
380
+ "встречный — путевая меньше",
381
+ "попутный — путевая больше",
382
+ ),
383
+ vs: lines(`Вертикальная скорость, м/с: ${signed(climb)}`, '"+" — набор высоты', '"−" — снижение'),
384
+ heading: lines(`Курс, °: ${String(hdg % 360).padStart(3, "0")}`, "Куда направлен нос аппарата", "0 — север, 90 — восток, 180 — юг, 270 — запад"),
385
+ track:
386
+ track !== null
387
+ ? lines(`Трек, °: ${Math.round(track)} (▲ розовый)`, "Куда аппарат реально движется над землёй", "Отличается от курса на снос ветром")
388
+ : "",
389
+ wpBug: wp ? lines(`Курс на точку ${wp.num}, °: ${Math.round(wp.bearing)} (◆ бирюзовый)`, `Расстояние: ${fmtDistance(wp.dist)}`) : "",
390
+ homeMark: home ? lines(`Направление на точку взлёта, °: ${Math.round(home.bearing)} (● зелёный)`, homeLine) : "",
391
+ messages: lines(`Сообщения автопилота, последние ${messages.length}:`, ...messages.map((m) => `${SEVERITY[m.sev] ?? m.sev} — ${m.text}`)),
392
+ route: wp
393
+ ? lines(
394
+ `Маршрут: точка ${wp.num}`,
395
+ `Расстояние до точки: ${fmtDistance(wp.dist)}`,
396
+ `Откл. — боковое отклонение от линии маршрута: ${fmtDistance(Math.abs(wp.xte))}`,
397
+ )
398
+ : "",
399
+ home: home ? lines("Точка взлёта", homeLine, `Направление на неё, °: ${Math.round(home.bearing)} (● на ленте курса)`) : "",
400
+ position: lines(
401
+ "Позиция",
402
+ `Широта, долгота, °: ${lat ? `${lat.toFixed(6)}, ${lon.toFixed(6)}` : "—"}`,
403
+ `MSL — высота над уровнем моря, м: ${Math.round(altMsl)}`,
404
+ ),
405
+ air: lines("Воздух", `Давление, гПа: ${fmt(num(value?.Press))}`, `Температура, °C: ${fmt(num(value?.Temp))}`),
406
+ }
407
+ })
408
+ let tipText = $derived(tip ? (tips as Record<string, string>)[tip] || null : null)
409
+ let infoText = $derived(tipText ?? (banner ? `${SEVERITY[banner.sev] ?? ""} · ${banner.text}` : null))
220
410
  </script>
221
411
 
222
412
  <div
@@ -225,7 +415,13 @@
225
415
  >
226
416
  <WidgetHeader icon={PlaneIcon} label={label?.name ?? "Пилотажный дисплей"} bind:collapsed>
227
417
  {#snippet right()}
228
- <span class="text-[12px] font-semibold tabular-nums {stale ? 'text-(--red-color)' : 'text-(--muted-color)'}">{fmtAge(age)}</span>
418
+ <div class="flex items-center gap-4">
419
+ <label class="flex cursor-pointer items-center gap-1.5 text-[12px] font-semibold text-(--muted-color)">
420
+ <input type="checkbox" class="cursor-pointer accent-(--accent-color)" checked={tipsEnabled} onchange={(e) => setTips(e.currentTarget.checked)} />
421
+ Подсказки
422
+ </label>
423
+ <span class="text-[12px] font-semibold tabular-nums {stale ? 'text-(--red-color)' : 'text-(--muted-color)'}">{fmtAge(age)}</span>
424
+ </div>
229
425
  {/snippet}
230
426
  </WidgetHeader>
231
427
 
@@ -238,232 +434,274 @@
238
434
  onpointerleave={() => (tip = null)}
239
435
  role="presentation"
240
436
  >
241
- <!-- Строка состояния -->
437
+ <!-- Строка состояния: состояние систем аппарата, группами -->
242
438
  <div
243
- class="flex flex-wrap items-center gap-x-4 gap-y-1 rounded-[10px] border border-(--hairline-color) bg-(--back-color) px-3 py-2 text-[12px] font-semibold tabular-nums"
439
+ class="flex flex-wrap items-center justify-between gap-x-5 gap-y-1.5 rounded-[10px] border border-(--hairline-color) bg-(--back-color) px-3 py-2 text-[12px] font-semibold tabular-nums"
244
440
  >
245
- <span
246
- class="rounded-md px-1.5 py-0.5 {armed ? 'bg-(--red-color) text-white' : 'bg-(--container-color) text-(--muted-color)'}"
247
- data-tip={armed ? "Двигатели взведены — аппарат готов к полёту" : "Двигатели не взведены"}>{armed ? "ARMED" : "DISARMED"}</span
248
- >
249
- <span data-tip="Режим полёта автопилота">{modeName}</span>
250
- <span class={fix < 3 ? "text-(--orange-color)" : ""} data-tip="Тип фиксации GPS · число спутников; меньше 3D — позиция неточная"
251
- >GPS {FIX[fix] ?? fix} · {num(value?.Sat)}</span
252
- >
253
- <span class={battLow ? "text-(--red-color)" : ""} data-tip="Батарея: напряжение · ток · остаток заряда">
254
- {fmt(num(value?.BV))} В{battCurrent >= 0 ? ` · ${fmt(battCurrent)} А` : ""}{battRem >= 0 ? ` · ${battRem}%` : ""}
255
- </span>
256
- {#if home}
257
- <span data-tip="Точка взлёта: расстояние · направление на неё">Дом {fmtDistance(home.distance)} · {Math.round(home.bearing)}°</span>
441
+ <div class="flex min-w-0 items-center gap-2" data-tip="system">
442
+ <span class="shrink-0 text-[10px] tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Система</span>
443
+ <span class="rounded-md px-1.5 py-0.5 {armed ? 'bg-(--red-color) text-white' : 'bg-(--container-color) text-(--muted-color)'}" data-tip="armed"
444
+ >{armed ? "ARMED" : "DISARMED"}</span
445
+ >
446
+ <span data-tip="mode">{modeName}</span>
447
+ <span class="flex items-center gap-1" style="color: {NAV_COLOR[navState]}" data-tip="nav">
448
+ <span class="size-2 rounded-full" style="background: {NAV_COLOR[navState]}"></span>НАВ{sensErr > 0 ? ` · датчики ${sensErr}` : ""}
449
+ </span>
450
+ </div>
451
+ <div class="flex min-w-0 items-center gap-2" data-tip="gps">
452
+ <span class="shrink-0 text-[10px] tracking-wide whitespace-nowrap text-(--faint-color) uppercase">GPS</span>
453
+ <span class={fix < 3 ? "text-(--orange-color)" : ""}>{FIX[fix] ?? fix} · {num(value?.Sat)} сп.</span>
454
+ {#if hdop >= 0}<span class={hdop > 2 ? "text-(--orange-color)" : ""}>HDOP {fmt(hdop, 2)}</span>{/if}
455
+ </div>
456
+ {#if rssi >= 0 || remRssi >= 0 || drop >= 0}
457
+ <div class="flex min-w-0 items-center gap-2" data-tip="link">
458
+ <span class="shrink-0 text-[10px] tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Связь</span>
459
+ <span class={Math.min(rssi, remRssi) >= 0 && Math.min(rssi, remRssi) < 30 ? "text-(--red-color)" : ""}
460
+ >{rssi >= 0 ? Math.round(rssi) : "—"} / {remRssi >= 0 ? Math.round(remRssi) : "—"} %</span
461
+ >
462
+ {#if drop >= 0}<span class={drop > 5 ? "text-(--orange-color)" : ""}>потери {fmt(drop)} %</span>{/if}
463
+ </div>
258
464
  {/if}
465
+ <div class="flex items-center gap-2 {battLow ? 'text-(--red-color)' : ''}" data-tip="battery">
466
+ <span class="shrink-0 text-[10px] tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Батарея</span>
467
+ <span>{fmt(num(value?.BV))} В{battCurrent >= 0 ? ` · ${fmt(battCurrent)} А` : ""}{battRem >= 0 ? ` · ${battRem} %` : ""}</span>
468
+ </div>
259
469
  </div>
260
470
 
261
471
  <!-- Прибор -->
262
- <svg viewBox="0 0 {VW} 320" class="w-full rounded-[10px] bg-(--back-color)" role="img" aria-label="Пилотажный дисплей">
263
- <defs>
264
- <clipPath id="pfd-horizon-{uid}"><rect x={HX} y="0" width={HW} height={HH} rx="8" /></clipPath>
265
- <clipPath id="pfd-speed-{uid}"><rect x="0" y="0" width="70" height={HH} rx="8" /></clipPath>
266
- <clipPath id="pfd-alt-{uid}"><rect x={VW - 70} y="0" width="70" height={HH} rx="8" /></clipPath>
267
- <clipPath id="pfd-hdg-{uid}"><rect x={HX} y={HH + 6} width={HW} height="44" rx="8" /></clipPath>
268
- </defs>
269
-
270
- <!-- Авиагоризонт -->
271
- <g clip-path="url(#pfd-horizon-{uid})" data-tip="Авиагоризонт: крен {fmt(roll.current)}°, тангаж {fmt(pitch.current)}°">
272
- <g transform="rotate({-roll.current} {CX} {CY}) translate(0 {pitch.current * PPD})">
273
- <rect x={CX - 600} y={CY - 1000} width="1200" height="1000" fill="#3a86c8" />
274
- <rect x={CX - 600} y={CY} width="1200" height="1000" fill="#8a5a2b" />
275
- <line x1={CX - 600} y1={CY} x2={CX + 600} y2={CY} stroke="#fff" stroke-width="2" />
276
- {#each pitchLadder as d (d)}
277
- {@const y = CY - d * PPD}
278
- {@const half = d % 10 === 0 ? 40 : 18}
279
- <line x1={CX - half} y1={y} x2={CX + half} y2={y} stroke="#fff" stroke-width="1.5" />
280
- {#if d % 10 === 0}
281
- <text x={CX - half - 6} y={y + 4} fill="#fff" font-size="11" text-anchor="end">{Math.abs(d)}</text>
282
- <text x={CX + half + 6} y={y + 4} fill="#fff" font-size="11">{Math.abs(d)}</text>
283
- {/if}
472
+ <div class="relative">
473
+ <svg viewBox="0 0 {VW} 320" class="w-full rounded-[10px] bg-(--back-color)" role="img" aria-label="Пилотажный дисплей">
474
+ <defs>
475
+ <clipPath id="pfd-horizon-{uid}"><rect x={HX} y="0" width={HW} height={HH} rx="8" /></clipPath>
476
+ <clipPath id="pfd-speed-{uid}"><rect x="0" y="0" width="70" height={HH} rx="8" /></clipPath>
477
+ <clipPath id="pfd-alt-{uid}"><rect x={VW - 70} y="0" width="70" height={HH} rx="8" /></clipPath>
478
+ <clipPath id="pfd-hdg-{uid}"><rect x={HX} y={HH + 6} width={HW} height="44" rx="8" /></clipPath>
479
+ </defs>
480
+
481
+ <!-- Авиагоризонт -->
482
+ <g clip-path="url(#pfd-horizon-{uid})" data-tip="horizon">
483
+ <g transform="rotate({-roll.current} {CX} {CY}) translate(0 {pitch.current * PPD})">
484
+ <rect x={CX - 600} y={CY - 1000} width="1200" height="1000" fill="#3a86c8" />
485
+ <rect x={CX - 600} y={CY} width="1200" height="1000" fill="#8a5a2b" />
486
+ <line x1={CX - 600} y1={CY} x2={CX + 600} y2={CY} stroke="#fff" stroke-width="2" />
487
+ {#each pitchLadder as d (d)}
488
+ {@const y = CY - d * PPD}
489
+ {@const half = d % 10 === 0 ? 40 : 18}
490
+ <line x1={CX - half} y1={y} x2={CX + half} y2={y} stroke="#fff" stroke-width="1.5" />
491
+ {#if d % 10 === 0}
492
+ <text x={CX - half - 6} y={y + 4} fill="#fff" font-size="11" text-anchor="end">{Math.abs(d)}</text>
493
+ <text x={CX + half + 6} y={y + 4} fill="#fff" font-size="11">{Math.abs(d)}</text>
494
+ {/if}
495
+ {/each}
496
+ </g>
497
+
498
+ <!-- Шкала крена: зона за пределом bankMax — красная -->
499
+ <path d={arcPath(-60, 60, ROLL_R)} fill="none" stroke="#fff" stroke-width="1.5" />
500
+ {#if limits?.bankMax !== undefined && limits.bankMax < 60}
501
+ <path d={arcPath(-60, -limits.bankMax, ROLL_R)} fill="none" stroke="#ef4444" stroke-width="4" />
502
+ <path d={arcPath(limits.bankMax, 60, ROLL_R)} fill="none" stroke="#ef4444" stroke-width="4" />
503
+ {/if}
504
+ {#each ROLL_TICKS as t (t)}
505
+ {@const a = arcPoint(t, ROLL_R)}
506
+ {@const b = arcPoint(t, ROLL_R + (t % 30 === 0 ? 12 : 7))}
507
+ <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="#fff" stroke-width="1.5" />
284
508
  {/each}
285
- </g>
509
+ <g transform="rotate({-roll.current} {CX} {CY})" data-tip="rollPtr">
510
+ <polygon
511
+ points="{CX},{CY - ROLL_R + 2} {CX - 7},{CY - ROLL_R + 14} {CX + 7},{CY - ROLL_R + 14}"
512
+ fill={limits?.bankMax !== undefined && Math.abs(roll.current) > limits.bankMax ? "#ef4444" : "#facc15"}
513
+ />
514
+ </g>
286
515
 
287
- <!-- Шкала крена: зона за пределом bankMax — красная -->
288
- <path d={arcPath(-60, 60, ROLL_R)} fill="none" stroke="#fff" stroke-width="1.5" />
289
- {#if limits?.bankMax !== undefined && limits.bankMax < 60}
290
- <path d={arcPath(-60, -limits.bankMax, ROLL_R)} fill="none" stroke="#ef4444" stroke-width="4" />
291
- <path d={arcPath(limits.bankMax, 60, ROLL_R)} fill="none" stroke="#ef4444" stroke-width="4" />
292
- {/if}
293
- {#each ROLL_TICKS as t (t)}
294
- {@const a = arcPoint(t, ROLL_R)}
295
- {@const b = arcPoint(t, ROLL_R + (t % 30 === 0 ? 12 : 7))}
296
- <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="#fff" stroke-width="1.5" />
297
- {/each}
298
- <g
299
- transform="rotate({-roll.current} {CX} {CY})"
300
- data-tip="Указатель крена: {fmt(roll.current)}°{limits?.bankMax !== undefined ? `, предел ±${limits.bankMax}°` : ''}"
301
- >
302
- <polygon
303
- points="{CX},{CY - ROLL_R + 2} {CX - 7},{CY - ROLL_R + 14} {CX + 7},{CY - ROLL_R + 14}"
304
- fill={limits?.bankMax !== undefined && Math.abs(roll.current) > limits.bankMax ? "#ef4444" : "#facc15"}
305
- />
306
- </g>
516
+ <!-- Символ самолёта -->
517
+ <g stroke="#facc15" stroke-width="4" fill="none" stroke-linecap="round">
518
+ <polyline points="{CX - 70},{CY} {CX - 25},{CY} {CX - 15},{CY + 10}" />
519
+ <polyline points="{CX + 70},{CY} {CX + 25},{CY} {CX + 15},{CY + 10}" />
520
+ </g>
521
+ <circle cx={CX} cy={CY} r="3" fill="#facc15" />
307
522
 
308
- <!-- Символ самолёта -->
309
- <g stroke="#facc15" stroke-width="4" fill="none" stroke-linecap="round">
310
- <polyline points="{CX - 70},{CY} {CX - 25},{CY} {CX - 15},{CY + 10}" />
311
- <polyline points="{CX + 70},{CY} {CX + 25},{CY} {CX + 15},{CY + 10}" />
523
+ <!-- Компактный режим: числа вместо лент -->
524
+ {#if compact}
525
+ <g font-size="15" font-weight="600" fill="#fff">
526
+ <g data-tip="compactSpeed">
527
+ <rect x="8" y={CY - 16} width="76" height="32" rx="6" fill="#000" fill-opacity="0.55" />
528
+ <text x="46" y={CY + 6} text-anchor="middle">{fmt(gs)}</text>
529
+ <text x="46" y={CY + 32} text-anchor="middle" font-size="11">м/с</text>
530
+ </g>
531
+ <g data-tip="compactAlt">
532
+ <rect x={VW - 84} y={CY - 16} width="76" height="32" rx="6" fill="#000" fill-opacity="0.55" />
533
+ <text x={VW - 46} y={CY + 6} text-anchor="middle">{Math.round(alt)}</text>
534
+ <text x={VW - 46} y={CY + 32} text-anchor="middle" font-size="11">м · {climb >= 0 ? "+" : ""}{fmt(climb)}</text>
535
+ </g>
536
+ </g>
537
+ {/if}
312
538
  </g>
313
- <circle cx={CX} cy={CY} r="3" fill="#facc15" />
314
-
315
- <!-- Компактный режим: числа вместо лент -->
316
- {#if compact}
317
- <g font-size="15" font-weight="600" fill="#fff">
318
- <rect x="8" y={CY - 16} width="76" height="32" rx="6" fill="#000" fill-opacity="0.55" />
319
- <text x="46" y={CY + 6} text-anchor="middle">{fmt(gs)}</text>
320
- <rect x={VW - 84} y={CY - 16} width="76" height="32" rx="6" fill="#000" fill-opacity="0.55" />
321
- <text x={VW - 46} y={CY + 6} text-anchor="middle">{Math.round(alt)}</text>
322
- <text x="46" y={CY + 32} text-anchor="middle" font-size="11">м/с</text>
323
- <text x={VW - 46} y={CY + 32} text-anchor="middle" font-size="11">м · {climb >= 0 ? "+" : ""}{fmt(climb)}</text>
539
+
540
+ {#if !compact}
541
+ <!-- Лента скорости (путевая), красные зоны вне speedMin/speedMax -->
542
+ <g clip-path="url(#pfd-speed-{uid})" data-tip="speedTape">
543
+ <rect x="0" y="0" width="70" height={HH} fill="#000" fill-opacity="0.72" />
544
+ {#if limits?.speedMin !== undefined}
545
+ {@const y = CY - (limits.speedMin - gs) * SPEED_PPU}
546
+ <rect x="62" {y} width="8" height={Math.max(0, HH - y)} fill="#ef4444" />
547
+ {/if}
548
+ {#if limits?.speedMax !== undefined}
549
+ {@const y = CY - (limits.speedMax - gs) * SPEED_PPU}
550
+ <rect x="62" y="0" width="8" height={Math.max(0, y)} fill="#ef4444" />
551
+ {/if}
552
+ {#each speedTicks as v (v)}
553
+ {@const y = CY - (v - gs) * SPEED_PPU}
554
+ <line x1={v % 5 === 0 ? 48 : 56} y1={y} x2="62" y2={y} stroke="#fff" stroke-width="1" />
555
+ {#if v % 5 === 0}<text x="42" y={y + 4} fill="#fff" font-size="11" text-anchor="end">{v}</text>{/if}
556
+ {/each}
324
557
  </g>
325
- {/if}
326
- </g>
327
-
328
- {#if !compact}
329
- <!-- Лента скорости (путевая), красные зоны вне speedMin/speedMax -->
330
- <g
331
- clip-path="url(#pfd-speed-{uid})"
332
- data-tip="Путевая скорость (над землёй, по GPS): {fmt(gs)} м/с{limits?.speedMin !== undefined || limits?.speedMax !== undefined
333
- ? ' — красное: вне допустимого диапазона'
334
- : ''}"
335
- >
336
- <rect x="0" y="0" width="70" height={HH} fill="#000" fill-opacity="0.72" />
337
- {#if limits?.speedMin !== undefined}
338
- {@const y = CY - (limits.speedMin - gs) * SPEED_PPU}
339
- <rect x="62" {y} width="8" height={Math.max(0, HH - y)} fill="#ef4444" />
558
+ <g data-tip="speedBox">
559
+ <rect x="4" y={CY - 14} width="58" height="28" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
560
+ <text x="33" y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">{fmt(gs)}</text>
561
+ </g>
562
+ <g data-tip="speedLabel">
563
+ <rect x="0" y="0" width="70" height="20" rx="8" fill="#000" />
564
+ <text x="35" y="14" fill="#fff" font-size="10" text-anchor="middle" fill-opacity="0.7">СКОР м/с</text>
565
+ </g>
566
+ {#if wp}
567
+ {@const y = clampTape(CY - wp.spdErr * SPEED_PPU)}
568
+ <polygon points="70,{y - 7} 60,{y} 70,{y + 7}" fill="#22d3ee" data-tip="speedBug" />
340
569
  {/if}
341
- {#if limits?.speedMax !== undefined}
342
- {@const y = CY - (limits.speedMax - gs) * SPEED_PPU}
343
- <rect x="62" y="0" width="8" height={Math.max(0, y)} fill="#ef4444" />
570
+
571
+ <!-- Лента высоты над точкой взлёта, красная зона выше altMax; вариометр справа -->
572
+ <g clip-path="url(#pfd-alt-{uid})" data-tip="altTape">
573
+ <rect x={VW - 70} y="0" width="70" height={HH} fill="#000" fill-opacity="0.72" />
574
+ {#if limits?.altMax !== undefined}
575
+ {@const y = CY - (limits.altMax - alt) * ALT_PPU}
576
+ <rect x={VW - 70} y="0" width="8" height={Math.max(0, y)} fill="#ef4444" />
577
+ {/if}
578
+ {#each altTicks as v (v)}
579
+ {@const y = CY - (v - alt) * ALT_PPU}
580
+ <line x1={VW - 62} y1={y} x2={VW - (v % 10 === 0 ? 48 : 54)} y2={y} stroke="#fff" stroke-width="1" />
581
+ {#if v % 10 === 0}<text x={VW - 44} y={y + 4} fill="#fff" font-size="11">{v}</text>{/if}
582
+ {/each}
583
+ <rect x={VW - 6} y={Math.min(CY, CY - vsi)} width="4" height={Math.abs(vsi)} fill={climb >= 0 ? "#22c55e" : "#f97316"} />
584
+ </g>
585
+ <g data-tip="altBox">
586
+ <rect x={VW - 66} y={CY - 14} width="58" height="28" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
587
+ <text x={VW - 37} y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">{Math.round(alt)}</text>
588
+ </g>
589
+ <g data-tip="altLabel">
590
+ <rect x={VW - 70} y="0" width="70" height="20" rx="8" fill="#000" />
591
+ <text x={VW - 35} y="14" fill="#fff" font-size="10" text-anchor="middle" fill-opacity="0.7">ВЫС м</text>
592
+ </g>
593
+ {#if wp}
594
+ {@const y = clampTape(CY - (wp.altTarget - alt) * ALT_PPU)}
595
+ <polygon points="{VW - 70},{y - 7} {VW - 60},{y} {VW - 70},{y + 7}" fill="#22d3ee" data-tip="altBug" />
344
596
  {/if}
345
- {#each speedTicks as v (v)}
346
- {@const y = CY - (v - gs) * SPEED_PPU}
347
- <line x1={v % 5 === 0 ? 48 : 56} y1={y} x2="62" y2={y} stroke="#fff" stroke-width="1" />
348
- {#if v % 5 === 0}<text x="42" y={y + 4} fill="#fff" font-size="11" text-anchor="end">{v}</text>{/if}
597
+
598
+ <!-- Углы под лентами: воздушная и вертикальная скорость -->
599
+ <g data-tip="airspeed">
600
+ <rect x="0" y={HH + 6} width={HX - 6} height="44" rx="8" fill="#000" fill-opacity="0.72" />
601
+ <text x={(HX - 6) / 2} y={HH + 21} fill="#fff" fill-opacity="0.7" font-size="9" text-anchor="middle">ВОЗД. СКОР</text>
602
+ <text x={(HX - 6) / 2} y={HH + 41} fill="#fff" font-size="14" font-weight="600" text-anchor="middle">{fmt(as)}</text>
603
+ </g>
604
+ <g data-tip="vs">
605
+ <rect x={HX + HW + 6} y={HH + 6} width={VW - HX - HW - 6} height="44" rx="8" fill="#000" fill-opacity="0.72" />
606
+ <text x={(HX + HW + 6 + VW) / 2} y={HH + 21} fill="#fff" fill-opacity="0.7" font-size="9" text-anchor="middle">ВЕРТ. СКОР</text>
607
+ <text
608
+ x={(HX + HW + 6 + VW) / 2}
609
+ y={HH + 41}
610
+ fill={climb > 0.2 ? "#22c55e" : climb < -0.2 ? "#f97316" : "#fff"}
611
+ font-size="14"
612
+ font-weight="600"
613
+ text-anchor="middle">{signed(climb)}</text
614
+ >
615
+ </g>
616
+ {/if}
617
+
618
+ <!-- Лента курса: трек (розовый), направление на дом (зелёный) -->
619
+ <g clip-path="url(#pfd-hdg-{uid})" data-tip="heading">
620
+ <rect x={HX} y={HH + 6} width={HW} height="44" fill="#000" fill-opacity="0.72" />
621
+ {#each hdgTicks as d (d)}
622
+ {@const x = CX + hdgOffset(d)}
623
+ <line x1={x} y1={HH + 6} x2={x} y2={HH + (d % 10 === 0 ? 18 : 12)} stroke="#fff" stroke-width="1" />
624
+ {#if d % 10 === 0}<text {x} y={HH + 34} fill="#fff" font-size="11" text-anchor="middle">{hdgLabel(d)}</text>{/if}
349
625
  {/each}
350
- </g>
351
- <rect x="4" y={CY - 14} width="58" height="28" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
352
- <text x="33" y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">{fmt(gs)}</text>
353
- <rect x="0" y="0" width="70" height="20" rx="8" fill="#000" />
354
- <text x="35" y="14" fill="#fff" font-size="10" text-anchor="middle" fill-opacity="0.7">СКОР м/с</text>
355
-
356
- <!-- Лента высоты над точкой взлёта, красная зона выше altMax; вариометр справа -->
357
- <g
358
- clip-path="url(#pfd-alt-{uid})"
359
- data-tip="Высота над точкой взлёта: {Math.round(alt)} м; полоса у края — вертикальная скорость{limits?.altMax !== undefined
360
- ? ', красное — выше предела'
361
- : ''}"
362
- >
363
- <rect x={VW - 70} y="0" width="70" height={HH} fill="#000" fill-opacity="0.72" />
364
- {#if limits?.altMax !== undefined}
365
- {@const y = CY - (limits.altMax - alt) * ALT_PPU}
366
- <rect x={VW - 70} y="0" width="8" height={Math.max(0, y)} fill="#ef4444" />
626
+ {#if track !== null}
627
+ {@const x = CX + hdgOffset(track)}
628
+ <polygon points="{x},{HH + 8} {x - 6},{HH + 18} {x + 6},{HH + 18}" fill="#ec4899" data-tip="track" />
629
+ {/if}
630
+ {#if wp}
631
+ {@const x = CX + hdgOffset(wp.bearing)}
632
+ <polygon points="{x},{HH + 7} {x + 6},{HH + 13} {x},{HH + 19} {x - 6},{HH + 13}" fill="none" stroke="#22d3ee" stroke-width="2" data-tip="wpBug" />
633
+ {/if}
634
+ {#if home}
635
+ {@const x = CX + hdgOffset(home.bearing)}
636
+ <circle cx={x} cy={HH + 44} r="4" fill="#22c55e" data-tip="homeMark" />
367
637
  {/if}
368
- {#each altTicks as v (v)}
369
- {@const y = CY - (v - alt) * ALT_PPU}
370
- <line x1={VW - 62} y1={y} x2={VW - (v % 10 === 0 ? 48 : 54)} y2={y} stroke="#fff" stroke-width="1" />
371
- {#if v % 10 === 0}<text x={VW - 44} y={y + 4} fill="#fff" font-size="11">{v}</text>{/if}
372
- {/each}
373
- <rect x={VW - 6} y={Math.min(CY, CY - vsi)} width="4" height={Math.abs(vsi)} fill={climb >= 0 ? "#22c55e" : "#f97316"} />
374
- </g>
375
- <rect x={VW - 66} y={CY - 14} width="58" height="28" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
376
- <text x={VW - 37} y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">{Math.round(alt)}</text>
377
- <rect x={VW - 70} y="0" width="70" height="20" rx="8" fill="#000" />
378
- <text x={VW - 35} y="14" fill="#fff" font-size="10" text-anchor="middle" fill-opacity="0.7">ВЫС м</text>
379
-
380
- <!-- Углы под лентами: воздушная и вертикальная скорость -->
381
- <g data-tip="Воздушная скорость — относительно воздуха; отличается от путевой на ветер">
382
- <rect x="0" y={HH + 6} width={HX - 6} height="44" rx="8" fill="#000" fill-opacity="0.72" />
383
- <text x={(HX - 6) / 2} y={HH + 21} fill="#fff" fill-opacity="0.7" font-size="9" text-anchor="middle">ВОЗД. СКОР</text>
384
- <text x={(HX - 6) / 2} y={HH + 41} fill="#fff" font-size="14" font-weight="600" text-anchor="middle">{fmt(as)}</text>
385
638
  </g>
386
- <g data-tip="Вертикальная скорость: + набор высоты, − снижение, м/с">
387
- <rect x={HX + HW + 6} y={HH + 6} width={VW - HX - HW - 6} height="44" rx="8" fill="#000" fill-opacity="0.72" />
388
- <text x={(HX + HW + 6 + VW) / 2} y={HH + 21} fill="#fff" fill-opacity="0.7" font-size="9" text-anchor="middle">ВЕРТ. СКОР</text>
389
- <text
390
- x={(HX + HW + 6 + VW) / 2}
391
- y={HH + 41}
392
- fill={climb > 0.2 ? "#22c55e" : climb < -0.2 ? "#f97316" : "#fff"}
393
- font-size="14"
394
- font-weight="600"
395
- text-anchor="middle">{signed(climb)}</text
639
+ <g data-tip="heading">
640
+ <rect x={CX - 26} y={HH + 20} width="52" height="24" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
641
+ <text x={CX} y={HH + 37} fill="#fff" font-size="14" font-weight="600" text-anchor="middle"
642
+ >{String(Math.round(wrap360(heading.current)) % 360).padStart(3, "0")}</text
396
643
  >
397
644
  </g>
398
- {/if}
399
645
 
400
- <!-- Лента курса: трек (розовый), направление на дом (зелёный) -->
401
- <g clip-path="url(#pfd-hdg-{uid})" data-tip="Курс — куда направлен нос аппарата: {Math.round(wrap360(heading.current))}°">
402
- <rect x={HX} y={HH + 6} width={HW} height="44" fill="#000" fill-opacity="0.72" />
403
- {#each hdgTicks as d (d)}
404
- {@const x = CX + hdgOffset(d)}
405
- <line x1={x} y1={HH + 6} x2={x} y2={HH + (d % 10 === 0 ? 18 : 12)} stroke="#fff" stroke-width="1" />
406
- {#if d % 10 === 0}<text {x} y={HH + 34} fill="#fff" font-size="11" text-anchor="middle">{hdgLabel(d)}</text>{/if}
407
- {/each}
408
- {#if track !== null}
409
- {@const x = CX + hdgOffset(track)}
410
- <polygon
411
- points="{x},{HH + 8} {x - 6},{HH + 18} {x + 6},{HH + 18}"
412
- fill="#ec4899"
413
- data-tip="Трек — куда аппарат реально движется над землёй (по скоростям на север и восток): {Math.round(track)}°"
414
- />
415
- {/if}
416
- {#if home}
417
- {@const x = CX + hdgOffset(home.bearing)}
418
- <circle
419
- cx={x}
420
- cy={HH + 44}
421
- r="4"
422
- fill="#22c55e"
423
- data-tip="Направление на точку взлёта: {Math.round(home.bearing)}°, {fmtDistance(home.distance)}"
424
- />
646
+ <!-- Нет данных: флаг поверх прибора -->
647
+ {#if stale}
648
+ <g>
649
+ <rect x="0" y="0" width={VW} height="320" rx="10" fill="#000" fill-opacity="0.45" />
650
+ <line x1={HX + 20} y1="20" x2={HX + HW - 20} y2={HH - 20} stroke="#ef4444" stroke-width="3" />
651
+ <line x1={HX + HW - 20} y1="20" x2={HX + 20} y2={HH - 20} stroke="#ef4444" stroke-width="3" />
652
+ <rect x={CX - 70} y={CY - 16} width="140" height="32" rx="6" fill="#ef4444" />
653
+ <text x={CX} y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">НЕТ ДАННЫХ</text>
654
+ </g>
425
655
  {/if}
426
- </g>
427
- <rect x={CX - 26} y={HH + 20} width="52" height="24" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
428
- <text x={CX} y={HH + 37} fill="#fff" font-size="14" font-weight="600" text-anchor="middle"
429
- >{String(Math.round(wrap360(heading.current)) % 360).padStart(3, "0")}</text
430
- >
431
-
432
- <!-- Нет данных: флаг поверх прибора -->
433
- {#if stale}
434
- <g>
435
- <rect x="0" y="0" width={VW} height="320" rx="10" fill="#000" fill-opacity="0.45" />
436
- <line x1={HX + 20} y1="20" x2={HX + HW - 20} y2={HH - 20} stroke="#ef4444" stroke-width="3" />
437
- <line x1={HX + HW - 20} y1="20" x2={HX + 20} y2={HH - 20} stroke="#ef4444" stroke-width="3" />
438
- <rect x={CX - 70} y={CY - 16} width="140" height="32" rx="6" fill="#ef4444" />
439
- <text x={CX} y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">НЕТ ДАННЫХ</text>
440
- </g>
656
+ </svg>
657
+ <!-- Информационная строка внизу горизонта: подсказка к элементу под курсором, иначе последнее сообщение автопилота -->
658
+ {#if infoText}
659
+ {@const [head, ...rest] = infoText.split("\n")}
660
+ <div
661
+ data-tip={messages.length ? "messages" : null}
662
+ class="absolute -translate-x-1/2 -translate-y-full rounded-md px-3 py-1.5 text-left text-[12px] leading-snug {tipText
663
+ ? 'bg-black/75 text-white'
664
+ : sevClass(banner?.sev ?? 6)}"
665
+ style="left: {(CX / VW) * 100}%; top: {((HH - 8) / 320) * 100}%; width: {((HW * 0.9) / VW) * 100}%"
666
+ >
667
+ <div class="font-semibold">{head}</div>
668
+ {#each rest as row, i (i)}
669
+ <div class="opacity-90">{row}</div>
670
+ {/each}
671
+ </div>
441
672
  {/if}
442
- </svg>
443
-
444
- {#if tip}
445
- <div
446
- class="pointer-events-none absolute z-10 max-w-64 -translate-x-1/2 -translate-y-full rounded-lg border border-(--hairline-color) bg-(--back-color) px-2.5 py-1.5 text-[12px] text-(--font-color) shadow-(--elevation-2)"
447
- style="left: {tip.x}px; top: {tip.y - 10}px"
448
- >
449
- {tip.text}
450
- </div>
451
- {/if}
673
+ </div>
452
674
 
453
- <!-- Значения под прибором -->
675
+ <!-- Значения под прибором: куда движемся и где находимся, группами -->
454
676
  <div
455
- class="flex flex-wrap gap-x-5 gap-y-1 rounded-[10px] border border-(--hairline-color) bg-(--back-color) px-3 py-2 text-[12px] whitespace-nowrap tabular-nums"
677
+ class="flex flex-wrap items-center justify-between gap-x-5 gap-y-1.5 rounded-[10px] border border-(--hairline-color) bg-(--back-color) px-3 py-2 text-[12px] tabular-nums"
456
678
  >
457
- {#if track !== null}<span data-tip="Направление движения над землёй"
458
- ><span style="color:#ec4899">▲</span> <span class="text-(--muted-color)">Трек</span> {Math.round(track)}°</span
459
- >{/if}
460
- {#if home}<span data-tip="Направление на точку взлёта"
461
- ><span style="color:#22c55e">●</span> <span class="text-(--muted-color)">Дом</span> {Math.round(home.bearing)}°</span
462
- >{/if}
463
- <span><span class="text-(--muted-color)">Координаты</span> {lat ? `${lat.toFixed(6)}, ${lon.toFixed(6)}` : "—"}</span>
464
- <span data-tip="Высота над уровнем моря"><span class="text-(--muted-color)">Высота MSL</span> {Math.round(altMsl)} м</span>
465
- <span><span class="text-(--muted-color)">Давление</span> {fmt(num(value?.Press))} гПа</span>
466
- <span><span class="text-(--muted-color)">Температура</span> {fmt(num(value?.Temp))} °C</span>
679
+ {#if wp}
680
+ <div class="flex min-w-0 items-center gap-2" data-tip="route">
681
+ <span class="shrink-0 text-[10px] font-semibold tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Маршрут</span>
682
+ <span><span style="color:#22d3ee">◆</span> точка {wp.num} · {fmtDistance(wp.dist)} · откл. {fmtDistance(Math.abs(wp.xte))}</span>
683
+ </div>
684
+ {/if}
685
+ {#if home}
686
+ <div class="flex min-w-0 items-center gap-2" data-tip="home">
687
+ <span class="shrink-0 text-[10px] font-semibold tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Дом</span>
688
+ <span><span style="color:#22c55e">●</span> {fmtDistance(home.distance)} · {Math.round(home.bearing)}°</span>
689
+ </div>
690
+ {/if}
691
+ {#if track !== null}
692
+ <div class="flex items-center gap-2" data-tip="track">
693
+ <span class="shrink-0 text-[10px] font-semibold tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Трек</span>
694
+ <span><span style="color:#ec4899">▲</span> {Math.round(track)}°</span>
695
+ </div>
696
+ {/if}
697
+ <div class="flex min-w-0 items-center gap-2" data-tip="position">
698
+ <span class="shrink-0 text-[10px] font-semibold tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Позиция</span>
699
+ <span>{lat ? `${lat.toFixed(6)}, ${lon.toFixed(6)}` : "—"} · MSL {Math.round(altMsl)} м</span>
700
+ </div>
701
+ <div class="flex min-w-0 items-center gap-2" data-tip="air">
702
+ <span class="shrink-0 text-[10px] font-semibold tracking-wide whitespace-nowrap text-(--faint-color) uppercase">Воздух</span>
703
+ <span>{fmt(num(value?.Press))} гПа · {fmt(num(value?.Temp))} °C</span>
704
+ </div>
467
705
  </div>
468
706
  </div>
469
707
  {/if}
@@ -119,9 +119,9 @@ const translations = {
119
119
  "🎯 **用途**\n控制可寻址灯带:颜色、效果、速度以及灯带本身的参数。\n\n📦 **数据包**\n- `RGB_Strip` 驱动的 `LST` 分组字段(可通过设置中的前缀更改),无需绑定变量\n- 颜色、效果和速度立即发送\n- 「硬件」区块在本地编辑,点击「保存」一次性发送 —— 灯带重启\n\n🔑 **键**\n- `Color` —— 数组 `[R, G, B, W]`,0–255;`W` 仅用于 SK6812\n- `Effect` —— 数字:`0` 静态、`1` 彩虹、`2` 呼吸、`3` 调色板、`4` 彗星、`5` 闪烁、`6` 火焰、`7` 跑马灯\n- `Speed` —— 0–10 的数字(非线性),`0` —— 暂停\n- `Count` —— LED 数量,1–`MaxCount`\n- `Model` —— 数字:`0` WS2812、`1` SK6812(RGBW)、`2` WS2811\n- `Order` —— 颜色顺序:`0` GRB、`1` RGB、`2` BRG、`3` RBG、`4` GBR、`5` BGR\n- `MaxCount` —— 数字:固件中的容量,只读\n\n✨ **特点**\n- 彩虹、调色板和火焰不使用颜色\n- 颜色不对(红色变绿色)—— 更改顺序:WS2812 通常为 GRB,部分 WS2811 为 RGB 或 BRG\n- 数据引脚和传输方式(RMT/SPI)在固件中设定\n\n⚙️ **设置**\n- 分组前缀(ID)、主色",
120
120
  ],
121
121
  "component.info.WidgetPFD": [
122
- "🎯 **Назначение**\nПилотажный дисплей (PFD) — главный прибор контроля полёта: пространственное положение, скорость, высота, курс и состояние систем.\n\n📦 **Пакет данных**\n- Поля группы `MAV` — телеметрия автопилота MAVLink (меняется префиксом в настройках), привязывать переменные не нужно\n- Поток телеметрии: `Roll`, `Pitch`, `Heading` — ~20 раз в секунду, остальное — 1–2 раза в секунду\n- Только отображение, ничего не отправляет\n\n🔑 **Ключи**\n- `Roll`, `Pitch` — числа, °: крен (вправо +) и тангаж (вверх +)\n- `Heading` — число, 0–360°: курс\n- `GSpeed`, `ASpeed` — числа, м/с: путевая и воздушная скорость\n- `Alt` — число, м: высота над точкой взлёта; `AltMSL` — над уровнем моря\n- `Climb` — число, м/с: вертикальная скорость (вверх +)\n- `Lat`, `Lon` — числа, °: координаты\n- `Vx`, `Vy` — числа, м/с: скорость на север и восток — для трека\n- `Fix` — число: тип фиксации GPS, `0`–`1` нет, `2` 2D, `3` 3D, `4`–`6` DGPS/RTK; `Sat` — число спутников\n- `BV` — В; `BC` — А (`-1` не измеряется); `BRem` — % (`-1` неизвестно)\n- `Mode` — число: режим автопилота; `VType` — тип аппарата (по нему — названия режимов); `Armed` — `1` взведён\n- `HomeLat`, `HomeLon` — числа, °: точка взлёта, `0` — неизвестна\n- `Press` — гПа, `Temp` — °C\n\n✨ **Особенности**\n- Ориентация сглаживается между пакетами\n- Наведите курсор на любой элемент прибора или строки — всплывающая подсказка с расшифровкой и текущим значением\n- Углы под лентами: воздушная скорость (относительно воздуха) и вертикальная скорость (+ набор, − снижение)\n- Розовый ▲ на ленте курса — трек: куда аппарат реально движется над землёй (по `Vx`, `Vy`); зелёный ● — направление на точку взлёта; легенда с градусами — под прибором\n- Нет данных дольше заданного времени — прибор перечёркнут и помечен «НЕТ ДАННЫХ»\n- В узкой ячейке ленты скорости и высоты заменяются числами поверх горизонта\n- Ключи `Lat`, `Lon`, `Alt`, `Heading`, `Sat` общие с «Картой» — одна группа питает оба виджета\n\n⚙️ **Настройки**\n- Префикс группы (ID), мастер-цвет\n- Границы: мин./макс. скорость и макс. высота — красные зоны на лентах; предельный крен — красная дуга; низкий заряд; время до «НЕТ ДАННЫХ»",
123
- '🎯 **Purpose**\nPrimary flight display (PFD) — the main flight instrument: attitude, speed, altitude, heading and system status.\n\n📦 **Data packet**\n- Fields of the `MAV` group — MAVLink autopilot telemetry (changed by the prefix in settings), no variable binding needed\n- Telemetry stream: `Roll`, `Pitch`, `Heading` ~20 times a second, the rest 1–2 times a second\n- Display only, sends nothing\n\n🔑 **Keys**\n- `Roll`, `Pitch` — numbers, °: bank (right +) and pitch (up +)\n- `Heading` — number, 0–360°: heading\n- `GSpeed`, `ASpeed` — numbers, m/s: ground speed and airspeed\n- `Alt` — number, m: altitude above home; `AltMSL` — above sea level\n- `Climb` — number, m/s: vertical speed (up +)\n- `Lat`, `Lon` — numbers, °: coordinates\n- `Vx`, `Vy` — numbers, m/s: north and east velocity — for the track\n- `Fix` — number: GPS fix type, `0`–`1` none, `2` 2D, `3` 3D, `4`–`6` DGPS/RTK; `Sat` — satellite count\n- `BV` — V; `BC` — A (`-1` not measured); `BRem` — % (`-1` unknown)\n- `Mode` — number: autopilot mode; `VType` — vehicle type (mode names depend on it); `Armed` — `1` armed\n- `HomeLat`, `HomeLon` — numbers, °: home position, `0` — unknown\n- `Press` — hPa, `Temp` — °C\n\n✨ **Features**\n- Attitude is smoothed between packets\n- Hover any instrument element or row item — a tooltip with its meaning and current value\n- Corners under the tapes: airspeed (relative to the air) and vertical speed (+ climb, − descent)\n- Magenta ▲ on the heading tape is the track: where the vehicle actually moves over the ground (from `Vx`, `Vy`); green ● — direction to home; a legend with degrees is below the instrument\n- No data for longer than the set time — the instrument is crossed out and marked "NO DATA"\n- In a narrow cell the speed and altitude tapes become numbers over the horizon\n- Keys `Lat`, `Lon`, `Alt`, `Heading`, `Sat` are shared with the "Map" — one group feeds both widgets\n\n⚙️ **Settings**\n- Group prefix (ID), master color\n- Limits: min/max speed and max altitude — red bands on the tapes; bank limit — red arc; low battery; time until "NO DATA"',
124
- "🎯 **用途**\n主飞行显示器(PFD)—— 飞行监控的主要仪表:姿态、速度、高度、航向和系统状态。\n\n📦 **数据包**\n- `MAV` 分组的字段 —— MAVLink 自驾仪遥测(可通过设置中的前缀更改),无需绑定变量\n- 遥测流:`Roll`、`Pitch`、`Heading` 约每秒 20 次,其余每秒 1–2 次\n- 仅显示,不发送任何数据\n\n🔑 **键**\n- `Roll`、`Pitch` —— 数字,°:坡度(向右 +)和俯仰(向上 +)\n- `Heading` —— 数字,0–360°:航向\n- `GSpeed`、`ASpeed` —— 数字,m/s:地速和空速\n- `Alt` —— 数字,m:相对起飞点高度;`AltMSL` —— 海拔高度\n- `Climb` —— 数字,m/s:升降速度(向上 +)\n- `Lat`、`Lon` —— 数字,°:坐标\n- `Vx`、`Vy` —— 数字,m/s:向北和向东的速度 —— 用于航迹\n- `Fix` —— 数字:GPS 定位类型,`0`–`1` 无、`2` 2D、`3` 3D、`4`–`6` DGPS/RTK;`Sat` —— 卫星数\n- `BV` —— V;`BC` —— A(`-1` 未测量);`BRem` —— %(`-1` 未知)\n- `Mode` —— 数字:自驾仪模式;`VType` —— 机型(决定模式名称);`Armed` —— `1` 已解锁\n- `HomeLat`、`HomeLon` —— 数字,°:起飞点,`0` —— 未知\n- `Press` —— hPa,`Temp` —— °C\n\n✨ **特点**\n- 数据包之间的姿态经过平滑\n- 将鼠标悬停在仪表或行中的任意元素上 —— 显示含义和当前值的提示\n- 速度带和高度带下方的角落:空速(相对空气)和升降速度(+ 爬升、− 下降)\n- 航向带上的粉色 ▲ 为航迹:载具相对地面的实际运动方向(由 `Vx`、`Vy` 计算);绿色 ● —— 起飞点方向;仪表下方有带角度的图例\n- 超过设定时间无数据 —— 仪表被划叉并标记「无数据」\n- 在窄单元格中,速度带和高度带变为地平线上的数字\n- 键 `Lat`、`Lon`、`Alt`、`Heading`、`Sat` 与「地图」通用 —— 一个分组同时供两个组件使用\n\n⚙️ **设置**\n- 分组前缀(ID)、主色\n- 限值:最小/最大速度和最大高度 —— 速度带和高度带上的红色区域;最大坡度 —— 红色弧线;低电量;「无数据」前的时间",
122
+ "🎯 **Назначение**\nПилотажный дисплей (PFD) — главный прибор контроля полёта: пространственное положение, скорость, высота, курс и состояние систем.\n\n📦 **Пакет данных**\n- Поля группы `MAV` — телеметрия автопилота MAVLink (меняется префиксом в настройках), привязывать переменные не нужно\n- Поток телеметрии: `Roll`, `Pitch`, `Heading` — ~20 раз в секунду, остальное — 1–2 раза в секунду\n- Только отображение, ничего не отправляет\n\n🔑 **Ключи**\n- `Roll`, `Pitch` — числа, °: крен (вправо +) и тангаж (вверх +)\n- `Heading` — число, 0–360°: курс\n- `GSpeed`, `ASpeed` — числа, м/с: путевая и воздушная скорость\n- `Alt` — число, м: высота над точкой взлёта; `AltMSL` — над уровнем моря\n- `Climb` — число, м/с: вертикальная скорость (вверх +)\n- `Lat`, `Lon` — числа, °: координаты\n- `Vx`, `Vy` — числа, м/с: скорость на север и восток — для трека\n- `Fix` — число: тип фиксации GPS, `0`–`1` нет, `2` 2D, `3` 3D, `4`–`6` DGPS/RTK; `Sat` — число спутников\n- `BV` — В; `BC` — А (`-1` не измеряется); `BRem` — % (`-1` неизвестно)\n- `Mode` — число: режим автопилота; `VType` — тип аппарата (по нему — названия режимов); `Armed` — `1` взведён\n- `HomeLat`, `HomeLon` — числа, °: точка взлёта, `0` — неизвестна\n- `Press` — гПа, `Temp` — °C\n- `WPNum` — номер точки маршрута (`0` — маршрута нет); `WPDist`, `WPBrg` — расстояние, м, и курс на неё, °\n- `AltErr`, `SpdErr` — цель минус текущее: высота, м, и воздушная скорость, м/с; `XTE` — боковое отклонение от маршрута, м\n- `RSSI`, `RemRSSI` — сигнал своей и удалённой стороны, % (`-1` неизвестно); `Drop` — потери пакетов, %\n- `HDOP` — точность GPS (до 1 отлично, больше 2 неточно); `EKF` — дисперсия навигационного фильтра (от 0.5 внимание, от 0.8 тревога); `SensErr` — неисправных датчиков\n- `Msg`, `MsgSev`, `MsgSeq` — сообщение автопилота, важность (`0`–`3` ошибка, `4` предупреждение, `5`–`7` информация), номер\n\n✨ **Особенности**\n- Ориентация сглаживается между пакетами\n- Наведите курсор на любой элемент прибора или строки — расшифровка с текущим значением в строке внизу горизонта; выключается галочкой «Подсказки» в шапке\n- Углы под лентами: воздушная скорость (относительно воздуха) и вертикальная скорость (+ набор, − снижение)\n- Розовый ▲ на ленте курса — трек: куда аппарат реально движется над землёй (по `Vx`, `Vy`); зелёный ● — направление на точку взлёта; легенда с градусами — под прибором\n- Нет данных дольше заданного времени — прибор перечёркнут и помечен «НЕТ ДАННЫХ»\n- В узкой ячейке ленты скорости и высоты заменяются числами поверх горизонта\n- Строки сгруппированы: сверху «Система / GPS / Связь / Батарея» — состояние, снизу «Маршрут / Дом / Трек / Позиция / Воздух» — куда движемся и где находимся\n- Бирюзовые метки — цели текущей точки маршрута: скорость и высота на лентах, курс ◆ на ленте курса\n- Сообщения автопилота — в той же строке внизу горизонта на 10 с, цвет по важности; наведение на строку — история\n- Ключи `Lat`, `Lon`, `Alt`, `Heading`, `Sat` общие с «Картой» — одна группа питает оба виджета\n\n⚙️ **Настройки**\n- Префикс группы (ID), мастер-цвет\n- Границы: мин./макс. скорость и макс. высота — красные зоны на лентах; предельный крен — красная дуга; низкий заряд; время до «НЕТ ДАННЫХ»",
123
+ '🎯 **Purpose**\nPrimary flight display (PFD) — the main flight instrument: attitude, speed, altitude, heading and system status.\n\n📦 **Data packet**\n- Fields of the `MAV` group — MAVLink autopilot telemetry (changed by the prefix in settings), no variable binding needed\n- Telemetry stream: `Roll`, `Pitch`, `Heading` ~20 times a second, the rest 1–2 times a second\n- Display only, sends nothing\n\n🔑 **Keys**\n- `Roll`, `Pitch` — numbers, °: bank (right +) and pitch (up +)\n- `Heading` — number, 0–360°: heading\n- `GSpeed`, `ASpeed` — numbers, m/s: ground speed and airspeed\n- `Alt` — number, m: altitude above home; `AltMSL` — above sea level\n- `Climb` — number, m/s: vertical speed (up +)\n- `Lat`, `Lon` — numbers, °: coordinates\n- `Vx`, `Vy` — numbers, m/s: north and east velocity — for the track\n- `Fix` — number: GPS fix type, `0`–`1` none, `2` 2D, `3` 3D, `4`–`6` DGPS/RTK; `Sat` — satellite count\n- `BV` — V; `BC` — A (`-1` not measured); `BRem` — % (`-1` unknown)\n- `Mode` — number: autopilot mode; `VType` — vehicle type (mode names depend on it); `Armed` — `1` armed\n- `HomeLat`, `HomeLon` — numbers, °: home position, `0` — unknown\n- `Press` — hPa, `Temp` — °C\n- `WPNum` — waypoint number (`0` — no mission); `WPDist`, `WPBrg` — distance, m, and bearing to it, °\n- `AltErr`, `SpdErr` — target minus current: altitude, m, and airspeed, m/s; `XTE` — cross-track error, m\n- `RSSI`, `RemRSSI` — local and remote signal, % (`-1` unknown); `Drop` — packet loss, %\n- `HDOP` — GPS precision (up to 1 excellent, over 2 poor); `EKF` — navigation filter variance (from 0.5 warning, from 0.8 alarm); `SensErr` — unhealthy sensors\n- `Msg`, `MsgSev`, `MsgSeq` — autopilot message, severity (`0`–`3` error, `4` warning, `5`–`7` info), number\n\n✨ **Features**\n- Attitude is smoothed between packets\n- Hover any instrument element or row item — its meaning and current value in the line at the bottom of the horizon; turned off with the \"Hints\" checkbox in the header\n- Corners under the tapes: airspeed (relative to the air) and vertical speed (+ climb, − descent)\n- Magenta ▲ on the heading tape is the track: where the vehicle actually moves over the ground (from `Vx`, `Vy`); green ● — direction to home; a legend with degrees is below the instrument\n- No data for longer than the set time — the instrument is crossed out and marked "NO DATA"\n- In a narrow cell the speed and altitude tapes become numbers over the horizon\n- Rows are grouped: top \"System / GPS / Link / Battery\" — status, bottom \"Route / Home / Track / Position / Air\" — where we go and where we are\n- Cyan marks — targets of the current waypoint: speed and altitude on the tapes, bearing ◆ on the heading tape\n- Autopilot messages — in the same line at the bottom of the horizon for 10 s, colored by severity; hover the line for history\n- Keys `Lat`, `Lon`, `Alt`, `Heading`, `Sat` are shared with the "Map" — one group feeds both widgets\n\n⚙️ **Settings**\n- Group prefix (ID), master color\n- Limits: min/max speed and max altitude — red bands on the tapes; bank limit — red arc; low battery; time until "NO DATA"',
124
+ "🎯 **用途**\n主飞行显示器(PFD)—— 飞行监控的主要仪表:姿态、速度、高度、航向和系统状态。\n\n📦 **数据包**\n- `MAV` 分组的字段 —— MAVLink 自驾仪遥测(可通过设置中的前缀更改),无需绑定变量\n- 遥测流:`Roll`、`Pitch`、`Heading` 约每秒 20 次,其余每秒 1–2 次\n- 仅显示,不发送任何数据\n\n🔑 **键**\n- `Roll`、`Pitch` —— 数字,°:坡度(向右 +)和俯仰(向上 +)\n- `Heading` —— 数字,0–360°:航向\n- `GSpeed`、`ASpeed` —— 数字,m/s:地速和空速\n- `Alt` —— 数字,m:相对起飞点高度;`AltMSL` —— 海拔高度\n- `Climb` —— 数字,m/s:升降速度(向上 +)\n- `Lat`、`Lon` —— 数字,°:坐标\n- `Vx`、`Vy` —— 数字,m/s:向北和向东的速度 —— 用于航迹\n- `Fix` —— 数字:GPS 定位类型,`0`–`1` 无、`2` 2D、`3` 3D、`4`–`6` DGPS/RTK;`Sat` —— 卫星数\n- `BV` —— V;`BC` —— A(`-1` 未测量);`BRem` —— %(`-1` 未知)\n- `Mode` —— 数字:自驾仪模式;`VType` —— 机型(决定模式名称);`Armed` —— `1` 已解锁\n- `HomeLat`、`HomeLon` —— 数字,°:起飞点,`0` —— 未知\n- `Press` —— hPa,`Temp` —— °C\n- `WPNum` —— 航点编号(`0` —— 无任务);`WPDist`、`WPBrg` —— 到航点的距离,m,和方位,°\n- `AltErr`、`SpdErr` —— 目标减当前:高度,m,空速,m/s;`XTE` —— 偏航距离,m\n- `RSSI`、`RemRSSI` —— 本地和远端信号,%(`-1` 未知);`Drop` —— 丢包率,%\n- `HDOP` —— GPS 精度(1 以下优秀,大于 2 不准确);`EKF` —— 导航滤波器方差(0.5 起注意,0.8 起告警);`SensErr` —— 故障传感器数\n- `Msg`、`MsgSev`、`MsgSeq` —— 自驾仪消息、严重程度(`0`–`3` 错误、`4` 警告、`5`–`7` 信息)、编号\n\n✨ **特点**\n- 数据包之间的姿态经过平滑\n- 将鼠标悬停在仪表或行中的任意元素上 —— 在地平线底部的信息行中显示含义和当前值;可通过标题栏中的「提示」复选框关闭\n- 速度带和高度带下方的角落:空速(相对空气)和升降速度(+ 爬升、− 下降)\n- 航向带上的粉色 ▲ 为航迹:载具相对地面的实际运动方向(由 `Vx`、`Vy` 计算);绿色 ● —— 起飞点方向;仪表下方有带角度的图例\n- 超过设定时间无数据 —— 仪表被划叉并标记「无数据」\n- 在窄单元格中,速度带和高度带变为地平线上的数字\n- 行已分组:上方「系统 / GPS / 链路 / 电池」—— 状态,下方「航线 / 起飞点 / 航迹 / 位置 / 空气」—— 去向与位置\n- 青色标记 —— 当前航点的目标:速度带和高度带上的速度与高度,航向带上的方位 ◆\n- 自驾仪消息 —— 在地平线底部同一信息行显示 10 秒,颜色按严重程度;悬停该行可查看历史\n- 键 `Lat`、`Lon`、`Alt`、`Heading`、`Sat` 与「地图」通用 —— 一个分组同时供两个组件使用\n\n⚙️ **设置**\n- 分组前缀(ID)、主色\n- 限值:最小/最大速度和最大高度 —— 速度带和高度带上的红色区域;最大坡度 —— 红色弧线;低电量;「无数据」前的时间",
125
125
  ],
126
126
  "component.info.WidgetMap": [
127
127
  "🎯 **Назначение**\nКарта объектов в реальном времени: положение, курс и траектория каждого объекта.\n\n📦 **Пакет данных**\n- Поля групп устройства, по группе на объект: по умолчанию `MAV`, несколько — через пробел (`MAV GNSS`); привязывать переменные не нужно\n- Поток телеметрии: координаты — раз в 0.5–1 с, имя и серийный номер — реже\n- Только отображение, ничего не отправляет\n\n🔑 **Ключи**\n- `Lat` — число, °: широта, обязательный; `Lat` и `Lon` = 0 — нет фикса, объект не рисуется\n- `Lon` — число, °: долгота, обязательный\n- `Alt` — число, м: высота\n- `Heading` — число, 0–360°: курс, поворачивает маркер\n- `Sat` — число спутников\n- `DevName` — строка: имя объекта, подпись маркера; без него — имя группы\n- `DevSN` — строка: серийный номер\n\n✨ **Особенности**\n- Ключи общие с «Пилотажным дисплеем» и «Об устройстве» — один пакет питает несколько виджетов\n- Клик по объекту — карточка со всеми значениями, кнопки «Копировать» и «Скрыть»\n- Траекторию, число её точек и таймаут маркеров оператор переключает на карте; объект без новых данных краснеет, ещё через таймаут скрывается\n- «Показать все» в шапке вписывает объекты в экран; сама карта вид не меняет\n- В шапке — сколько объектов на связи из заданных\n- Только 2D: правой кнопкой мыши карта поворачивается, компас под зумом возвращает её на север\n\n⚙️ **Настройки**\n- Группы полей, мастер-цвет\n- Высота карты `S`/`M`/`L`/`XL`, иконка маркера\n- Начальные значения: траектория и число её точек",
package/dist/types.d.ts CHANGED
@@ -648,6 +648,21 @@ export interface IWidgetPFDData {
648
648
  HomeLon: number;
649
649
  Press: number;
650
650
  Temp: number;
651
+ WPNum: number;
652
+ WPDist: number;
653
+ WPBrg: number;
654
+ AltErr: number;
655
+ SpdErr: number;
656
+ XTE: number;
657
+ RSSI: number;
658
+ RemRSSI: number;
659
+ Drop: number;
660
+ HDOP: number;
661
+ EKF: number;
662
+ SensErr: number;
663
+ Msg: string;
664
+ MsgSev: number;
665
+ MsgSeq: number;
651
666
  }
652
667
  export interface IWidgetPFDLimits {
653
668
  speedMin?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"