Phase 3 (batch 33+34): web device discovery/connect parity

Move discovery into the controller and add a full web device overlay
(discovery + connect + saved-device CRUD), reaching parity with the
desktop device dialog.

- RadioController: OnDeviceFound/Discover/DiscoverFinishedNone +
  rfDeviceList; TDiscoverThread moved in from MainForm.
- WebServer: device protocol (discover/connect/disconnect/dev_add/
  dev_remove/dev_autostart) + "devices" in BuildStateJson.
- MainForm: web handlers (marshalled), PushDeviceListToWeb, extracted
  DoStopAndDisconnect, detached WebOnRun from BtnStartStopClick;
  web connect resolves board type from discovered then saved by IP.
- DeviceForm: RefreshFound renders the found list from the store.
- WebPageHtml: DEV overlay; START opens it, STOP disconnects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-06-08 15:30:03 +03:00
co-authored by Claude Opus 4.8
parent 9dec47a6ca
commit c4981fcb19
5 changed files with 488 additions and 135 deletions
+79 -2
View File
@@ -70,7 +70,8 @@ type
rfChannel,
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
rfADCOverload, // ADC overload (рендер индикатора)
rfDevice // подключённое устройство сменилось
rfDevice, // подключённое устройство сменилось
rfDeviceList // список discovered устройств обновился
);
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
@@ -167,6 +168,7 @@ type
FAudioOutDevName: string;
FAudioInDevName: string;
FDevConnected: Boolean;
FDiscoverNoneFound: Boolean; // discovery завершился без устройств (для рендера)
// ---- DSP-фильтры шума / водопад ----
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
@@ -244,6 +246,10 @@ type
FHPpFwdW, FHPpSWR, FHPpSupplyV, FHPpSupplyA: Double;
FHPpPLL, FHPpHWPTT: Boolean;
FHPpOverload: Byte;
// Pending discovered-устройство (сетевой поток discovery → ApplyDeviceFound
// через Invoke; Synchronize блокирующий — устройства приходят по одному).
FPendingFoundDev: THPSDRDevice;
FPendingFoundEntry: string;
constructor Create;
destructor Destroy; override;
@@ -264,6 +270,9 @@ type
procedure OnHPStatus(const Status: THighPriorityStatus);
procedure ApplyHPStatus; // маршалится из OnHPStatus (сглаживание/overload/HWPTT)
procedure OnTXIQ(const Buf: array of Double; Count: Integer);
procedure OnDeviceFound(const Dev: THPSDRDevice); // сетевой поток discovery
procedure ApplyDeviceFound; // маршалится из OnDeviceFound (store + rfDeviceList)
procedure DiscoverFinishedNone; // маршалится из discover-потока: устройств нет
// Маршалинг команды в поток контроллера (GUI: TThread.Synchronize).
procedure Invoke(M: TThreadMethod);
@@ -2162,7 +2171,75 @@ begin
Changed(rfDevice); // UI: полная синхронизация device-виджетов из состояния
end;
// ---------------------------------------------------------------------------
// Discovery — поиск устройств в фоновом потоке. Найденные приходят через
// FNetwork.OnDeviceFound (=OnDeviceFound) и копятся в FDeviceStore.Discovered;
// по завершении без устройств — DiscoverFinishedNone. UI слушает rfDeviceList.
// ---------------------------------------------------------------------------
type
TDiscoverThread = class(TThread)
private
FCtrl: TRadioController;
protected
procedure Execute; override;
public
constructor Create(ACtrl: TRadioController);
end;
constructor TDiscoverThread.Create(ACtrl: TRadioController);
begin
FCtrl := ACtrl;
FreeOnTerminate := True;
inherited Create(False);
end;
procedure TDiscoverThread.Execute;
var
Devs: THPSDRDeviceArray;
begin
Devs := FCtrl.FNetwork.Discover(2000);
// Найденные устройства сигналятся колбэком OnDeviceFound во время Discover().
if Length(Devs) = 0 then
FCtrl.Invoke(FCtrl.DiscoverFinishedNone);
end;
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice);
// Сетевой поток: строим строку для списка → pending → маршалинг в ApplyDeviceFound.
var
BoardName: string;
begin
BoardName := BoardTypeName(Dev.BoardType);
FPendingFoundDev := Dev;
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d',
[Dev.IPAddress, BoardName,
Dev.FirmwareVersion, Dev.NumDDCs]);
Invoke(ApplyDeviceFound);
end;
procedure TRadioController.ApplyDeviceFound;
// Маршалится из OnDeviceFound (Synchronize). Дописываем в store и уведомляем UI.
begin
FDiscoverNoneFound := False;
FDeviceStore.AddDiscovered(FPendingFoundDev.IPAddress, FPendingFoundEntry,
FPendingFoundDev.BoardType, FPendingFoundDev.MAC);
Changed(rfDeviceList);
end;
procedure TRadioController.DiscoverFinishedNone;
// Маршалится из discover-потока, когда устройств не найдено.
begin
FDiscoverNoneFound := True;
Changed(rfDeviceList);
end;
procedure TRadioController.Discover;
begin { TODO wiring: FNetwork.Discover } end;
// Старт поиска: чистим прошлый список, сбрасываем direct-IP, запускаем поток.
begin
FDeviceStore.ClearDiscovered;
FDiscoverNoneFound := False;
FNetwork.DirectIP := '';
Changed(rfDeviceList);
TDiscoverThread.Create(Self);
end;
end.