unit DeviceForm; {$mode objfpc}{$H+} interface uses Classes, SysUtils, FlatButton, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, ComCtrls, IniFiles; // Декодирование типа платы (совпадает с MainForm.BoardTypeName) function BoardTypeName(BoardType: Integer): string; const DEVICE_CFG_FILE = 'hpsdr_devices.ini'; type // Запись о сохранённом устройстве TSavedDevice = record Name: string; // пользовательское имя IPAddress: string; BoardType: Integer; AutoStart: Boolean; // запускать автоматически при старте end; // Результат диалога TDeviceDialogResult = record Accepted: Boolean; IPAddress: string; SavedIdx: Integer; // -1 если выбрали из discovery, иначе индекс в SavedDevices end; { TDeviceDialog } TDeviceDialog = class(TForm) private // Сохранённые устройства FSavedDevices: array of TSavedDevice; FSavedCount: Integer; FResult: TDeviceDialogResult; // Discovered devices (IP strings) FDiscoveredIPs: array of string; FDiscoveredNames: array of string; FDiscoveredBoardTypes: array of Integer; FDiscoveredCount: Integer; // UI PanelTop: TPanel; PanelBottom: TPanel; PanelLeft: TPanel; PanelRight: TPanel; LblSaved: TLabel; LstSaved: TListBox; BtnAdd: TFlatButton; BtnRemove: TFlatButton; BtnSetAuto: TFlatButton; EdName: TEdit; EdIP: TEdit; LblName: TLabel; LblIP: TLabel; LblFound: TLabel; LstFound: TListBox; BtnDiscover: TFlatButton; BtnAddFound: TFlatButton; BtnConnect: TFlatButton; BtnCancel: TFlatButton; FOnDiscover: TNotifyEvent; // внешний callback для запуска discovery procedure BuildUI; procedure ApplyTheme; procedure LoadSaved; procedure SaveSaved; procedure RefreshSavedList; procedure BtnDiscoverClick(Sender: TObject); procedure BtnAddClick(Sender: TObject); procedure BtnRemoveClick(Sender: TObject); procedure BtnSetAutoClick(Sender: TObject); procedure BtnAddFoundClick(Sender: TObject); procedure BtnConnectClick(Sender: TObject); procedure BtnCancelClick(Sender: TObject); procedure LstSavedDblClick(Sender: TObject); procedure LstFoundDblClick(Sender: TObject); procedure LstSavedClick(Sender: TObject); function MakeBtn(AParent: TWinControl; const Cap: string; X, Y, W, H: Integer; AClick: TNotifyEvent): TFlatButton; function MakeLbl(AParent: TWinControl; const Cap: string; X, Y: Integer): TLabel; public constructor Create(AOwner: TComponent); override; // Добавить найденное устройство (вызывается из MainForm при discovery) procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer = 0); procedure ClearDiscovered; // Автозапуск: возвращает IP если есть устройство с AutoStart=True function GetAutoStartIP: string; function GetAutoStartBoardType: Integer; function GetSavedBoardType(Idx: Integer): Integer; // Получить результат property DialogResult: TDeviceDialogResult read FResult; property OnDiscover: TNotifyEvent read FOnDiscover write FOnDiscover; property SavedCount: Integer read FSavedCount; end; implementation function BoardTypeName(BoardType: Integer): string; begin case BoardType of 1: Result := 'HERMES (ANAN-10/100)'; 2: Result := 'HERMES-E (ANAN-10E/100B)'; 3: Result := 'ANGELIA (ANAN-100D)'; 4: Result := 'ORION (ANAN-200D)'; 5: Result := 'ORION MkII (ANAN-7000/8000)'; 6: Result := 'HERMES-LITE 2'; 10: Result := 'SATURN (G2)'; else Result := Format('Unknown Board #%d', [BoardType]); end; end; const CLR_BG = TColor($00121212); CLR_PANEL = TColor($001A1A1A); CLR_TEXT = TColor($00E0E0E0); CLR_TEXTDIM = TColor($00888888); CLR_BORDER = TColor($00303030); CLR_ACCENT = TColor($0040FF80); CLR_AUTO = TColor($0000CCFF); // цвет авто-устройства BTN_H = 24; { TDeviceDialog } constructor TDeviceDialog.Create(AOwner: TComponent); begin inherited CreateNew(AOwner); Caption := 'Device Selection'; Width := 660; Height := 420; Position := poScreenCenter; BorderStyle := bsDialog; Color := CLR_BG; Font.Name := 'Courier New'; Font.Size := 8; Font.Color := CLR_TEXT; FSavedCount := 0; FDiscoveredCount := 0; FResult.Accepted := False; BuildUI; LoadSaved; RefreshSavedList; end; function TDeviceDialog.MakeBtn(AParent: TWinControl; const Cap: string; X, Y, W, H: Integer; AClick: TNotifyEvent): TFlatButton; begin Result := TFlatButton.Create(Self); Result.Parent := AParent; Result.Caption := Cap; Result.Left := X; Result.Top := Y; Result.Width := W; Result.Height := H; Result.OnClick := AClick; Result.Font.Name := 'Courier New'; Result.Font.Size := 8; Result.Font.Color := CLR_TEXT; Result.ClrNorm := CLR_PANEL; Result.ClrBorder := TColor($00404040); Result.ClrHot := TColor($00303030); Result.ClrActive := TColor($00003300); Result.ClrText := CLR_TEXT; Result.ClrTextAct := TColor($0000FF88); end; function TDeviceDialog.MakeLbl(AParent: TWinControl; const Cap: string; X, Y: Integer): TLabel; begin Result := TLabel.Create(Self); Result.Parent := AParent; Result.Caption := Cap; Result.Left := X; Result.Top := Y; Result.Font.Name := 'Courier New'; Result.Font.Size := 8; Result.Font.Color := CLR_TEXTDIM; end; procedure TDeviceDialog.BuildUI; var LblHint: TLabel; Ed: TEdit; begin // --- Левая панель: сохранённые устройства --- PanelLeft := TPanel.Create(Self); PanelLeft.Parent := Self; PanelLeft.SetBounds(8, 8, 300, 360); PanelLeft.BevelOuter := bvNone; PanelLeft.Color := CLR_PANEL; MakeLbl(PanelLeft, 'SAVED DEVICES', 6, 6); LstSaved := TListBox.Create(Self); LstSaved.Parent := PanelLeft; LstSaved.SetBounds(4, 22, 292, 140); LstSaved.Color := CLR_BG; LstSaved.Font.Color:= CLR_TEXT; LstSaved.Font.Name := 'Courier New'; LstSaved.Font.Size := 8; LstSaved.OnClick := @LstSavedClick; LstSaved.OnDblClick := @LstSavedDblClick; MakeLbl(PanelLeft, 'Name:', 6, 170); EdName := TEdit.Create(Self); EdName.Parent := PanelLeft; EdName.SetBounds(50, 167, 140, BTN_H); EdName.Color := CLR_BG; EdName.Font.Color:= CLR_TEXT; EdName.Font.Name := 'Courier New'; EdName.Font.Size := 8; MakeLbl(PanelLeft, 'IP:', 6, 198); EdIP := TEdit.Create(Self); EdIP.Parent := PanelLeft; EdIP.SetBounds(50, 195, 140, BTN_H); EdIP.Color := CLR_BG; EdIP.Font.Color:= CLR_TEXT; EdIP.Font.Name := 'Courier New'; EdIP.Font.Size := 8; EdIP.TextHint := '192.168.1.x'; BtnAdd := MakeBtn(PanelLeft, 'ADD', 6, 225, 70, BTN_H, @BtnAddClick); BtnRemove := MakeBtn(PanelLeft, 'REMOVE', 80, 225, 70, BTN_H, @BtnRemoveClick); BtnSetAuto := MakeBtn(PanelLeft, 'SET AUTOSTART', 6, 255, 130, BTN_H, @BtnSetAutoClick); LblHint := MakeLbl(PanelLeft, '* = autostart', 150, 260); LblHint.Font.Color := CLR_AUTO; BtnConnect := MakeBtn(PanelLeft, 'CONNECT', 6, 295, 130, BTN_H+4, @BtnConnectClick); BtnConnect.ClrText := CLR_ACCENT; BtnConnect.ClrTextAct := CLR_ACCENT; // --- Правая панель: discovery --- PanelRight := TPanel.Create(Self); PanelRight.Parent := Self; PanelRight.SetBounds(320, 8, 330, 360); PanelRight.BevelOuter := bvNone; PanelRight.Color := CLR_PANEL; MakeLbl(PanelRight, 'DISCOVERED DEVICES', 6, 6); LstFound := TListBox.Create(Self); LstFound.Parent := PanelRight; LstFound.SetBounds(4, 22, 322, 190); LstFound.Color := CLR_BG; LstFound.Font.Color:= CLR_TEXT; LstFound.Font.Name := 'Courier New'; LstFound.Font.Size := 8; LstFound.OnDblClick := @LstFoundDblClick; BtnDiscover := MakeBtn(PanelRight, 'DISCOVER', 6, 220, 100, BTN_H, @BtnDiscoverClick); BtnAddFound := MakeBtn(PanelRight, 'SAVE DEVICE', 6, 250, 100, BTN_H, @BtnAddFoundClick); BtnCancel := MakeBtn(PanelRight, 'CANCEL', 220, 295, 100, BTN_H+4, @BtnCancelClick); end; procedure TDeviceDialog.ApplyTheme; begin // уже задано в BuildUI end; procedure TDeviceDialog.LoadSaved; var Ini: TIniFile; I, N: Integer; Section: string; begin FSavedCount := 0; if not FileExists(DEVICE_CFG_FILE) then Exit; Ini := TIniFile.Create(DEVICE_CFG_FILE); try N := Ini.ReadInteger('Devices', 'Count', 0); SetLength(FSavedDevices, N); for I := 0 to N - 1 do begin Section := 'Device' + IntToStr(I); FSavedDevices[I].Name := Ini.ReadString (Section, 'Name', 'HPSDR'); FSavedDevices[I].IPAddress := Ini.ReadString (Section, 'IP', ''); FSavedDevices[I].BoardType := Ini.ReadInteger(Section, 'BoardType', 0); FSavedDevices[I].AutoStart := Ini.ReadBool (Section, 'AutoStart', False); Inc(FSavedCount); end; finally Ini.Free; end; end; procedure TDeviceDialog.SaveSaved; var Ini: TIniFile; I: Integer; Section: string; begin Ini := TIniFile.Create(DEVICE_CFG_FILE); try Ini.WriteInteger('Devices', 'Count', FSavedCount); for I := 0 to FSavedCount - 1 do begin Section := 'Device' + IntToStr(I); Ini.WriteString (Section, 'Name', FSavedDevices[I].Name); Ini.WriteString (Section, 'IP', FSavedDevices[I].IPAddress); Ini.WriteInteger(Section, 'BoardType', FSavedDevices[I].BoardType); Ini.WriteBool (Section, 'AutoStart', FSavedDevices[I].AutoStart); end; finally Ini.Free; end; end; procedure TDeviceDialog.RefreshSavedList; var I: Integer; S: string; begin LstSaved.Items.Clear; for I := 0 to FSavedCount - 1 do begin S := FSavedDevices[I].Name + ' [' + FSavedDevices[I].IPAddress + ']'; if FSavedDevices[I].BoardType > 0 then S := S + ' ' + BoardTypeName(FSavedDevices[I].BoardType); if FSavedDevices[I].AutoStart then S := '* ' + S; LstSaved.Items.Add(S); end; end; procedure TDeviceDialog.LstSavedClick(Sender: TObject); var Idx: Integer; begin Idx := LstSaved.ItemIndex; if (Idx < 0) or (Idx >= FSavedCount) then Exit; EdName.Text := FSavedDevices[Idx].Name; EdIP.Text := FSavedDevices[Idx].IPAddress; end; procedure TDeviceDialog.LstSavedDblClick(Sender: TObject); begin BtnConnectClick(nil); end; procedure TDeviceDialog.LstFoundDblClick(Sender: TObject); begin BtnConnectClick(nil); end; procedure TDeviceDialog.BtnDiscoverClick(Sender: TObject); begin LstFound.Items.Clear; LstFound.Items.Add('Searching...'); if Assigned(FOnDiscover) then FOnDiscover(Self); end; procedure TDeviceDialog.ClearDiscovered; begin FDiscoveredCount := 0; SetLength(FDiscoveredIPs, 0); SetLength(FDiscoveredNames, 0); SetLength(FDiscoveredBoardTypes, 0); LstFound.Items.Clear; end; procedure TDeviceDialog.AddDiscovered(const IP, DisplayName: string; BoardType: Integer = 0); var Idx: Integer; S: string; begin if (LstFound.Items.Count = 1) and (LstFound.Items[0] = 'Searching...') then LstFound.Items.Clear; Idx := FDiscoveredCount; Inc(FDiscoveredCount); SetLength(FDiscoveredIPs, FDiscoveredCount); SetLength(FDiscoveredNames, FDiscoveredCount); SetLength(FDiscoveredBoardTypes, FDiscoveredCount); FDiscoveredIPs[Idx] := IP; FDiscoveredNames[Idx] := DisplayName; FDiscoveredBoardTypes[Idx] := BoardType; S := DisplayName; if BoardType > 0 then S := S + ' ' + BoardTypeName(BoardType); LstFound.Items.Add(S); end; procedure TDeviceDialog.BtnAddClick(Sender: TObject); var Idx: Integer; begin if Trim(EdIP.Text) = '' then begin ShowMessage('Enter IP address'); Exit; end; Idx := FSavedCount; Inc(FSavedCount); SetLength(FSavedDevices, FSavedCount); FSavedDevices[Idx].Name := Trim(EdName.Text); if FSavedDevices[Idx].Name = '' then FSavedDevices[Idx].Name := 'HPSDR'; FSavedDevices[Idx].IPAddress := Trim(EdIP.Text); FSavedDevices[Idx].BoardType := 0; FSavedDevices[Idx].AutoStart := False; SaveSaved; RefreshSavedList; LstSaved.ItemIndex := Idx; end; procedure TDeviceDialog.BtnRemoveClick(Sender: TObject); var Idx, I: Integer; begin Idx := LstSaved.ItemIndex; if (Idx < 0) or (Idx >= FSavedCount) then Exit; for I := Idx to FSavedCount - 2 do FSavedDevices[I] := FSavedDevices[I + 1]; Dec(FSavedCount); SetLength(FSavedDevices, FSavedCount); SaveSaved; RefreshSavedList; EdName.Text := ''; EdIP.Text := ''; end; procedure TDeviceDialog.BtnSetAutoClick(Sender: TObject); var Idx, I: Integer; begin Idx := LstSaved.ItemIndex; if (Idx < 0) or (Idx >= FSavedCount) then begin ShowMessage('Select a device first'); Exit; end; // Только одно устройство может быть AutoStart for I := 0 to FSavedCount - 1 do FSavedDevices[I].AutoStart := (I = Idx); SaveSaved; RefreshSavedList; LstSaved.ItemIndex := Idx; end; procedure TDeviceDialog.BtnAddFoundClick(Sender: TObject); var Idx: Integer; begin Idx := LstFound.ItemIndex; if (Idx < 0) or (Idx >= FDiscoveredCount) then begin ShowMessage('Select a discovered device first'); Exit; end; EdIP.Text := FDiscoveredIPs[Idx]; EdName.Text := FDiscoveredNames[Idx]; BtnAddClick(nil); // Обновляем BoardType только что добавленной записи if FSavedCount > 0 then begin FSavedDevices[FSavedCount - 1].BoardType := FDiscoveredBoardTypes[Idx]; SaveSaved; RefreshSavedList; LstSaved.ItemIndex := FSavedCount - 1; end; end; procedure TDeviceDialog.BtnConnectClick(Sender: TObject); var IP: string; Idx: Integer; begin IP := ''; // Приоритет: выбранное сохранённое > выбранное найденное > ручной IP Idx := LstSaved.ItemIndex; if (Idx >= 0) and (Idx < FSavedCount) then begin IP := FSavedDevices[Idx].IPAddress; FResult.SavedIdx := Idx; end else begin Idx := LstFound.ItemIndex; if (Idx >= 0) and (Idx < FDiscoveredCount) then begin IP := FDiscoveredIPs[Idx]; FResult.SavedIdx := -1; end else if Trim(EdIP.Text) <> '' then begin IP := Trim(EdIP.Text); FResult.SavedIdx := -1; end; end; if IP = '' then begin ShowMessage('Select or enter a device to connect'); Exit; end; FResult.Accepted := True; FResult.IPAddress := IP; ModalResult := mrOk; end; procedure TDeviceDialog.BtnCancelClick(Sender: TObject); begin FResult.Accepted := False; ModalResult := mrCancel; end; function TDeviceDialog.GetAutoStartIP: string; var I: Integer; begin Result := ''; for I := 0 to FSavedCount - 1 do if FSavedDevices[I].AutoStart then begin Result := FSavedDevices[I].IPAddress; Exit; end; end; function TDeviceDialog.GetAutoStartBoardType: Integer; var I: Integer; begin Result := 0; for I := 0 to FSavedCount - 1 do if FSavedDevices[I].AutoStart then begin Result := FSavedDevices[I].BoardType; Exit; end; end; function TDeviceDialog.GetSavedBoardType(Idx: Integer): Integer; begin if (Idx >= 0) and (Idx < FSavedCount) then Result := FSavedDevices[Idx].BoardType else Result := 0; end; end.