feat: Pluto board name + hardware telemetry in status bar

- Status bar / web now show Pluto/LibreSDR model + serial instead of
  "Unknown Board" (BoardDisplayName in controller; UI just renders it).
- Field 3 shows AD936x chip temperature + RX RSSI for Pluto (no PA →
  Supply only for openHPSDR). Polled via backend.ReadTelemetry (~2 Hz),
  filtered for sensor glitches (plausibility window + slew-limit, with
  anti-stick) — fixes rare bogus -15°C readings.
- Add libiio binding iio_channel_attr_read_double.
- Remove stale root pluto_integration.MD (superseded by
  doc/PLUTO_INTEGRATION_PLAN.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 11:31:27 +03:00
co-authored by Claude Opus 4.8
parent 4d253909ca
commit 1321e415f7
8 changed files with 199 additions and 522 deletions
+80
View File
@@ -272,6 +272,11 @@ type
// ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ----
FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double;
FLastPLLLock: Boolean;
// Pluto-телеметрия (нет HP-status): температура чипа °C / RSSI приёмника дБ.
// Опрашиваются ServicePlutoTelemetry с UI-таймера; TELEMETRY_NONE = нет данных.
FLastPlutoTempC, FLastPlutoRSSIdB: Double;
FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг)
FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание)
// ---- RX/seq телеметрия data-path (пишется из сетевого колбэка OnDDCIQ) ----
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
@@ -515,6 +520,13 @@ type
function BackendCaps: TBackendCaps;
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
function IsPluto: Boolean;
// Человекочитаемое имя подключённой платы для статус-строки (UI/Web).
// HPSDR → тип платы; Pluto/LibreSDR → hw_model + серийник (если есть).
function BoardDisplayName: string;
// Опрос аппаратной телеметрии Pluto (температура/RSSI) с троттлингом — зовётся
// с UI-таймера (MeterTimer @10 Гц), как ServiceBeaconLock. No-op для HPSDR.
// Результат — в FLastPlutoTempC/FLastPlutoRSSIdB (UI читает и форматирует).
procedure ServicePlutoTelemetry;
// Список URI для probe сетевых Pluto (mDNS недоступен) — задаёт фронтенд
// перед Discover (сохранённые Pluto + ручной ввод IP).
procedure SetPlutoProbeURIs(const URIs: array of string);
@@ -551,6 +563,8 @@ begin
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
FFMStepOn := True;
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE;
FPlutoTeleTick := 0; FPlutoTempBad := 0;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
end;
@@ -2727,6 +2741,8 @@ begin
end;
FRXPacketCount := 0;
FActiveDDC := 0;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE;
FPlutoTeleTick := 0; FPlutoTempBad := 0;
Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP)
end;
@@ -3021,6 +3037,70 @@ begin
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
end;
function TRadioController.BoardDisplayName: string;
var
Dev: THPSDRDevice;
begin
if FNetwork = nil then Exit('');
Dev := FNetwork.Device;
if Dev.Kind = bkPluto then
begin
Result := PlutoModelName(Dev.Model);
// Серийник дописываем, только если это реальный hw_serial, а не URI-fallback
// ('usb:1.5.5' / 'ip:…'), который PlutoBackend кладёт при отсутствии серийника.
if (Dev.Serial <> '') and (Pos('usb:', Dev.Serial) = 0)
and (Pos('ip:', Dev.Serial) = 0) then
Result := Result + ' SN:' + Dev.Serial;
end
else
Result := BoardTypeName(Dev.BoardType);
end;
procedure TRadioController.ServicePlutoTelemetry;
const
POLL_MS = 500; // опрос ~2 Гц: чтение attr — USB round-trip, чаще ни к чему
TEMP_MIN_C = 0.0; // работающий AD936x всегда теплее окружающей (само-нагрев)
TEMP_MAX_C = 120.0; // абсолютный потолок (датчик исправен)
TEMP_STEP_C = 12.0; // макс. физичный шаг между опросами (тепловая инерция чипа)
TEMP_FORCE = 3; // принять «выброс», если держится N раз подряд (не залипаем)
var
t, r: Double;
now_: QWord;
begin
if (FNetwork = nil) or (not FRunning) or (FNetwork.Caps.Kind <> bkPluto) then Exit;
now_ := GetTickCount64;
if (FPlutoTeleTick <> 0) and (now_ - FPlutoTeleTick < POLL_MS) then Exit;
FPlutoTeleTick := now_;
if not FNetwork.ReadTelemetry(t, r) then Exit;
// Температура: датчик AD9361 иногда отдаёт мусор (мид-конверсия / контеншн на
// USB-контексте с RX-потоком) — наблюдалось редкое -15 °C. Фильтруем: жёсткое
// окно правдоподобия + slew-limit по тепловой инерции; устойчивый «выброс»
// (реальный скачок) всё же принимаем через TEMP_FORCE опросов, чтобы не залипнуть.
if t > TELEMETRY_NONE then
begin
if (t >= TEMP_MIN_C) and (t <= TEMP_MAX_C) then
begin
if (FLastPlutoTempC <= TELEMETRY_NONE) or (Abs(t - FLastPlutoTempC) <= TEMP_STEP_C) then
begin
FLastPlutoTempC := t; FPlutoTempBad := 0;
end
else
begin
Inc(FPlutoTempBad);
if FPlutoTempBad >= TEMP_FORCE then
begin FLastPlutoTempC := t; FPlutoTempBad := 0; end;
end;
end;
// вне окна правдоподобия — молча отбрасываем, держим прошлое валидное значение
end;
// RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден.
if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r;
Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже
end;
procedure TRadioController.SetPlutoProbeURIs(const URIs: array of string);
var i: Integer;
begin