diff --git a/DeviceStore.pas b/DeviceStore.pas index 1bc892a..e7eab44 100644 --- a/DeviceStore.pas +++ b/DeviceStore.pas @@ -62,6 +62,9 @@ type procedure SetAutoStart(Idx: Integer); // эксклюзивно: только одно autostart function AutoStartIP: string; function AutoStartBoardType: Integer; + // Полная autostart-запись (Kind/URI/Serial) — для бэкенд-агностичного автозапуска + // (Pluto открывается по URI, у USB-Pluto IPAddress пуст). False — нет autostart. + function AutoStartDevice(out Dev: TSavedDevice): Boolean; function SavedBoardType(Idx: Integer): Integer; function SavedDisplay(Idx: Integer): string; // строка для списка (с * и board) @@ -242,6 +245,19 @@ begin end; end; +function TDeviceStore.AutoStartDevice(out Dev: TSavedDevice): Boolean; +var + I: Integer; +begin + Result := False; + for I := 0 to FSavedCount - 1 do + if FSaved[I].AutoStart then + begin + Dev := FSaved[I]; + Exit(True); + end; +end; + function TDeviceStore.SavedBoardType(Idx: Integer): Integer; begin if (Idx >= 0) and (Idx < FSavedCount) then diff --git a/MainForm.pas b/MainForm.pas index 71ee8e5..5549546 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -3578,41 +3578,9 @@ end; function TMainForm.ResolveDevice(const IP: string): THPSDRDevice; // По IP/URI ищет устройство среди discovered, затем saved, чтобы восстановить -// Kind/URI/Serial/MAC (нужно для Pluto). Если не найдено — простой HPSDR по IP. -var - i: Integer; - dd: TDiscoveredDevice; - sd: TSavedDevice; +// Kind/URI/Serial/MAC (нужно для Pluto). Логика общая с демоном — в контроллере. begin - // Result — managed-поля (строки) при входе nil, FillChar безопасен. - FillChar(Result, SizeOf(Result), 0); - Result.Kind := bkHPSDR; - Result.IPAddress := IP; - - i := FController.FDeviceStore.FindDiscoveredByIP(IP); - if i >= 0 then - begin - dd := FController.FDeviceStore.Discovered(i); - Result.Kind := dd.Kind; - Result.URI := dd.URI; - Result.Serial := dd.Serial; - Result.BoardType := dd.BoardType; - Move(dd.MAC[0], Result.MAC[0], 6); - Exit; - end; - - for i := 0 to FController.FDeviceStore.SavedCount - 1 do - begin - sd := FController.FDeviceStore.Saved(i); - if SameText(sd.IPAddress, IP) or SameText(sd.URI, IP) then - begin - Result.Kind := sd.Kind; - Result.URI := sd.URI; - Result.Serial := sd.Serial; - Result.BoardType := sd.BoardType; - Exit; - end; - end; + Result := FController.ResolveDevice(IP); end; procedure TMainForm.DoStopAndDisconnect; @@ -5012,7 +4980,7 @@ end; procedure TMainForm.SyncWebDevAdd; begin - FController.FDeviceStore.AddSaved(FWebSyncStr1, FWebSyncStr2, 0); + FController.AddSavedDevice(FWebSyncStr1, FWebSyncStr2); // Pluto по схеме URI PushDeviceListToWeb; end; diff --git a/RadioController.pas b/RadioController.pas index a25c2f0..d103ee0 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -129,6 +129,7 @@ type FOnStateChanged: TRadioStateEvent; FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент) + FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView) FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер) procedure Changed(Field: TRadioField); @@ -528,6 +529,14 @@ type function ConnectDevice(const Dev: THPSDRDevice): Boolean; // Connect + load-by-MAC; рендер через rfDevice function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless) + // По IP/URI ищет устройство в store (discovered→saved), восстанавливая + // Kind/URI/Serial/BoardType/MAC. Нужно для Pluto (открывается по URI, выбор + // бэкенда — по Kind). Не найдено → простой HPSDR по IP. Общий путь GUI+демон. + function ResolveDevice(const IP: string): THPSDRDevice; + // Сохраняет устройство по введённому адресу. Схема URI ('ip:'/'usb:'/'local:') + // → Pluto (AddSavedPluto), иначе HPSDR-IP. Общий путь web-dev_add для GUI и + // демона (десктоп-диалог сохраняет Pluto своим путём через AddSavedPluto). + procedure AddSavedDevice(const AName, AAddr: string); procedure Discover; // Гарантирует, что FNetwork — бэкенд нужного типа (HPSDR/Pluto): при // несовпадении пересоздаёт его, перенося колбэки данных. Вызывается из @@ -547,6 +556,9 @@ type // Список URI для probe сетевых Pluto (mDNS недоступен) — задаёт фронтенд // перед Discover (сохранённые Pluto + ручной ввод IP). procedure SetPlutoProbeURIs(const URIs: array of string); + // Сидирует probe-список из сохранённых Pluto (для бэкенд-агностичного Discover + // без UI-диалога — демон). GUI добавляет ручной IP поверх в BuildProbeURIs. + procedure SeedPlutoProbeFromSaved; // ---- События наружу ---- property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke; @@ -556,6 +568,9 @@ type // чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless. property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune; property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume; + // Локальный звук ПК (динамик RX / микрофон TX). Демон ставит False: аудио + // только через web (OnAudioConsume), локальная звуковая карта не трогается. + property LocalAudioEnabled: Boolean read FLocalAudio write FLocalAudio; property OnSpectrumData: TPixelDataEvent read FOnSpectrumData write FOnSpectrumData; property OnWaterfallData: TPixelDataEvent read FOnWaterfallData write FOnWaterfallData; end; @@ -580,6 +595,7 @@ begin FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True; FFMStepOn := True; + FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE; FPlutoTeleTick := 0; FPlutoTempBad := 0; @@ -896,7 +912,10 @@ begin FNetwork.SendSpeakerAudio(Left, Right, Count); if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then Exit; - FAudioOut.Write(Left, Right, Count); + // Без локального звука (демон) на этом маршрут заканчивается: если web-клиент + // не активен, аудио просто отбрасывается, а не идёт на звуковую карту хоста. + if FLocalAudio then + FAudioOut.Write(Left, Right, Count); end; procedure TRadioController.OnSpectrumReady(const Pixels: array of Single; @@ -2790,6 +2809,58 @@ begin Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP) end; +function TRadioController.ResolveDevice(const IP: string): THPSDRDevice; +// discovered→saved по IP/URI: восстанавливает Kind/URI/Serial/BoardType/MAC, +// иначе простой HPSDR по IP. См. одноимённую обёртку в GUI (TMainForm). +var + i: Integer; + dd: TDiscoveredDevice; + sd: TSavedDevice; +begin + FillChar(Result, SizeOf(Result), 0); + Result.Kind := bkHPSDR; + Result.IPAddress := IP; + + i := FDeviceStore.FindDiscoveredByIP(IP); + if i >= 0 then + begin + dd := FDeviceStore.Discovered(i); + Result.Kind := dd.Kind; + Result.URI := dd.URI; + Result.Serial := dd.Serial; + Result.BoardType := dd.BoardType; + Move(dd.MAC[0], Result.MAC[0], 6); + Exit; + end; + + for i := 0 to FDeviceStore.SavedCount - 1 do + begin + sd := FDeviceStore.Saved(i); + if SameText(sd.IPAddress, IP) or SameText(sd.URI, IP) then + begin + Result.Kind := sd.Kind; + Result.URI := sd.URI; + Result.Serial := sd.Serial; + Result.BoardType := sd.BoardType; + Exit; + end; + end; +end; + +procedure TRadioController.AddSavedDevice(const AName, AAddr: string); +// web-dev_add: Pluto по схеме URI ('ip:'/'usb:'/'local:'), иначе HPSDR-IP. +// Serial при ручном добавлении неизвестен (допишется при connect/probe). +var + A: string; +begin + A := Trim(AAddr); + if A = '' then Exit; + if (Pos('ip:', A) = 1) or (Pos('usb:', A) = 1) or (Pos('local:', A) = 1) then + FDeviceStore.AddSavedPluto(AName, A, '') + else + FDeviceStore.AddSaved(AName, A, 0); +end; + function TRadioController.ConnectDevice(const Dev: THPSDRDevice): Boolean; // Подключение + загрузка per-device настроек по MAC в поля контроллера + конфиг // движка (DSP display, audio). Возвращает False, если Connect не удался (UI @@ -2884,33 +2955,39 @@ begin FDSPEngine.SetWaterfallDisplay(FLoadedGlobal.WfDetector, FLoadedGlobal.WfAvgMode, FLoadedGlobal.WfAvgTimeMS); end; - // Аудио устройство вывода - FAudioOut.Close; - if FLoadedGlobal.AudioSampleRate > 0 then - FAudioOut.SampleRate := FLoadedGlobal.AudioSampleRate; - if FLoadedGlobal.AudioOutDevice <> '' then + // Локальная звуковая карта (динамик RX / микрофон TX). В демоне FLocalAudio= + // False: карту не открываем (иначе headless-хост хватает ALSA-устройство и + // играет звук). Web-аудио идёт через OnAudioConsume и от FAudioOut не зависит. + if FLocalAudio then begin - FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(FLoadedGlobal.AudioOutDevice); - FAudioOutDevName := FLoadedGlobal.AudioOutDevice; - end - else - begin - FAudioOut.DeviceIndex := -1; - FAudioOutDevName := ''; - end; - FAudioOut.Open; - // Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT — - // здесь только открываем устройство. - if FLoadedGlobal.AudioInDevice <> '' then - begin - FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice); - FAudioInDevName := FLoadedGlobal.AudioInDevice; - FAudioIn.Open; - end - else - begin - FAudioIn.DeviceIndex := -1; - FAudioInDevName := ''; + // Аудио устройство вывода + FAudioOut.Close; + if FLoadedGlobal.AudioSampleRate > 0 then + FAudioOut.SampleRate := FLoadedGlobal.AudioSampleRate; + if FLoadedGlobal.AudioOutDevice <> '' then + begin + FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(FLoadedGlobal.AudioOutDevice); + FAudioOutDevName := FLoadedGlobal.AudioOutDevice; + end + else + begin + FAudioOut.DeviceIndex := -1; + FAudioOutDevName := ''; + end; + FAudioOut.Open; + // Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT — + // здесь только открываем устройство. + if FLoadedGlobal.AudioInDevice <> '' then + begin + FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice); + FAudioInDevName := FLoadedGlobal.AudioInDevice; + FAudioIn.Open; + end + else + begin + FAudioIn.DeviceIndex := -1; + FAudioInDevName := ''; + end; end; end; @@ -3153,6 +3230,30 @@ begin for i := 0 to High(URIs) do FPlutoProbeURIs[i] := URIs[i]; end; +procedure TRadioController.SeedPlutoProbeFromSaved; +// Заполняет probe-список URI всех сохранённых Pluto (mDNS нет → сетевой Pluto +// находится только прямым probe его URI). Демон зовёт это перед Discover; GUI +// добавляет ещё и ручной IP из диалога. Дедупликация по URI. +var + URIs: array of string; + i, k: Integer; + U: string; +begin + SetLength(URIs, 0); + for i := 0 to FDeviceStore.SavedCount - 1 do + if FDeviceStore.Saved(i).Kind = bkPluto then + begin + U := FDeviceStore.Saved(i).URI; + if U = '' then Continue; + for k := 0 to High(URIs) do + if SameText(URIs[k], U) then begin U := ''; Break; end; + if U = '' then Continue; + SetLength(URIs, Length(URIs) + 1); + URIs[High(URIs)] := U; + end; + SetPlutoProbeURIs(URIs); +end; + procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice); // Сетевой/scan-поток: строим строку для списка → pending → маршалинг в // ApplyDeviceFound. Строка зависит от типа бэкенда (HPSDR vs Pluto). diff --git a/ewsdrd.lpr b/ewsdrd.lpr index 303296c..d9cd469 100644 --- a/ewsdrd.lpr +++ b/ewsdrd.lpr @@ -28,7 +28,7 @@ uses {$IFDEF UNIX}cthreads, BaseUnix,{$ENDIF} Classes, SysUtils, RadioController, WebServer, WebAdapter, - HPSDRNetwork, Settings, WDSP, PlatformUtils; + HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore; const DAEMON_SPEC_WIDTH = 1024; // ширина спектра для StartRunning (web-буфер = 1024) @@ -66,6 +66,7 @@ type // Sync* — выполняются в главном потоке через DoInvoke. procedure SyncConnect; procedure SyncDisconnect; + procedure SyncDiscover; procedure SyncRun; procedure SyncDevAdd; procedure SyncDevRemove; @@ -112,6 +113,7 @@ var begin inherited Create; FCtrl := TRadioController.Create; // создаёт FSettings + FDeviceStore (LoadSaved) + FCtrl.LocalAudioEnabled := False; // headless: звук только в web, не на карту хоста FCtrl.OnInvoke := @DoInvoke; FCtrl.OnStateChanged := @OnState; @@ -298,7 +300,7 @@ procedure THeadlessHost.WebConnect(const IP: string); begin FsStr1 := IP; FCtrl.Invoke(@SyncConnect); end; procedure THeadlessHost.WebDiscover; -begin FCtrl.Invoke(@FCtrl.Discover); end; // Discover уже headless +begin FCtrl.Invoke(@SyncDiscover); end; procedure THeadlessHost.WebDisconnect; begin FCtrl.Invoke(@SyncDisconnect); end; @@ -315,25 +317,11 @@ begin FsInt := Idx; FCtrl.Invoke(@SyncDevAutoStart); end; // --- Sync* (главный поток) --- procedure THeadlessHost.SyncConnect; -var - Dev: THPSDRDevice; - i: Integer; begin if FCtrl.FNetwork.Connected then Exit; - FillChar(Dev, SizeOf(Dev), 0); - Dev.IPAddress := FsStr1; - // BoardType не приходит при Connect — берём из discovery, иначе из saved по IP. - i := FCtrl.FDeviceStore.FindDiscoveredByIP(FsStr1); - if i >= 0 then - Dev.BoardType := FCtrl.FDeviceStore.Discovered(i).BoardType - else - for i := 0 to FCtrl.FDeviceStore.SavedCount - 1 do - if FCtrl.FDeviceStore.Saved(i).IPAddress = FsStr1 then - begin - Dev.BoardType := FCtrl.FDeviceStore.Saved(i).BoardType; - Break; - end; - ConnectTo(Dev); + // Kind/URI/Serial/BoardType при Connect не приходят — восстанавливаем из store + // (discovered→saved). Без Kind/URI бэкенд бы выбрался HPSDR, и Pluto не открылся. + ConnectTo(FCtrl.ResolveDevice(FsStr1)); end; procedure THeadlessHost.SyncDisconnect; @@ -345,6 +333,15 @@ begin end; end; +procedure THeadlessHost.SyncDiscover; +begin + // Сетевой Pluto без mDNS находится только прямым probe URI — сидируем список из + // сохранённых Pluto (в GUI это делает BuildProbeURIs). Без этого Discover видит + // лишь HPSDR-broadcast и USB-Pluto, а сетевые Pluto — никогда. + FCtrl.SeedPlutoProbeFromSaved; + FCtrl.Discover; +end; + procedure THeadlessHost.SyncRun; begin // set_run:true бессмыслен без выбранного устройства (web стартует через connect); @@ -357,7 +354,7 @@ begin end; procedure THeadlessHost.SyncDevAdd; -begin FCtrl.FDeviceStore.AddSaved(FsStr1, FsStr2, 0); PushDeviceList; end; +begin FCtrl.AddSavedDevice(FsStr1, FsStr2); PushDeviceList; end; procedure THeadlessHost.SyncDevRemove; begin FCtrl.FDeviceStore.RemoveSaved(FsInt); PushDeviceList; end; @@ -374,7 +371,7 @@ procedure THeadlessHost.Run; var WebCfg: TWebSettings; Dev: THPSDRDevice; - AutoIP: string; + AutoDev: TSavedDevice; lastPush, nowMs: QWord; begin FCtrl.FSettings.LoadWebSettings(WebCfg); @@ -391,13 +388,17 @@ begin // не перетрётся обратно в True. FRunning := True; - // Autostart — подключаемся сразу к отмеченному устройству. - AutoIP := FCtrl.FDeviceStore.AutoStartIP; - if AutoIP <> '' then + // Autostart — подключаемся сразу к отмеченному устройству. Берём полную запись + // (Kind/URI/Serial), иначе Pluto не подключить: бэкенд выбирается по Kind, USB- + // Pluto открывается по URI ('usb:..') и не имеет IPAddress. + if FCtrl.FDeviceStore.AutoStartDevice(AutoDev) then begin FillChar(Dev, SizeOf(Dev), 0); - Dev.IPAddress := AutoIP; - Dev.BoardType := FCtrl.FDeviceStore.AutoStartBoardType; + Dev.Kind := AutoDev.Kind; + Dev.IPAddress := AutoDev.IPAddress; + Dev.URI := AutoDev.URI; + Dev.Serial := AutoDev.Serial; + Dev.BoardType := AutoDev.BoardType; ConnectTo(Dev); end else