(*#######################################################################################

UpdateLoader - Hauptprogramm

Standardeinstellungen können in protocol.pas angepasst werden

https://luani.de/

Copyright (C) 2012-2023 Leo-Andres Hofmann


This file is part of UpdateLoader.

UpdateLoader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

UpdateLoader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with UpdateLoader. If not, see <http://www.gnu.org/licenses/>.

#######################################################################################*)

unit main;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ComCtrls, FileInfo,
  Grids, StdCtrls, ExtCtrls, Buttons, LCLType, IniFiles,
  LCLIntf, Tools, Protocol, SynaSer, LCLTranslator, DefaultTranslator;

type

  { TMainWindow }

  TMainWindow = class(TForm)
    btnUpdateControl: TButton;
    btnSelectFirmwareFilename: TButton;
    btnClearSettings: TButton;
    btnSavePaketSettings: TButton;
    ckbSaveLog: TCheckBox;
    ckbInvertedCom: TCheckBox;
    ckbFWKey: TCheckBox;
    ckbEnablePaketMode: TCheckBox;
    ckbPakMiniGUI: TCheckBox;
    ckbPakOnTop: TCheckBox;
    ckbPakLockFile: TCheckBox;
    ckbPakLockPass: TCheckBox;
    ckbPakHidePass: TCheckBox;
    ckbPakHideAdv: TCheckBox;
    ckbToggleDTR: TCheckBox;
    ckbDetectOneWire: TCheckBox;
    ckbProgramStart: TCheckBox;
    ckbShowMemory: TCheckBox;
    ckbReloadFile: TCheckBox;
    ckbProgramWrite: TCheckBox;
    ckbProgramVerify: TCheckBox;
    ckbHideMessages: TCheckBox;
    ckbAdvancedMode: TCheckBox;
    cmbComPort: TComboBox;
    cmbBaudRate: TComboBox;
    cmbFWKeyParseMode: TComboBox;
    edtFWKey: TEdit;
    edtVersionInfo: TEdit;
    gpbMemoryView: TGroupBox;
    gpbStatus: TGroupBox;
    gpbAdvancedConnection: TGroupBox;
    gpbConnection: TGroupBox;
    gpbUpdate: TGroupBox;
    gpbInfo: TGroupBox;
    gpbAbout: TGroupBox;
    gpbSettingsFile: TGroupBox;
    gpbAdvancedUpdate: TGroupBox;
    lblComVersion: TLabel;
    lblInfo_1: TLabel;
    lblInfo_2: TLabel;
    lblBootloaderVersion: TLabel;
    lblLicense: TLabel;
    lblCopyright: TLabel;
    lblProgramConnect: TLabel;
    lblProgramWrite: TLabel;
    lblProgramVerify: TLabel;
    lblUpdateProgress: TLabel;
    lblFlashNotice: TLabel;
    lblBaudRate: TLabel;
    lblComPort: TLabel;
    edtBootloaderPassword: TLabeledEdit;
    edtFirmwareFilename: TLabeledEdit;
    edtConnectTries: TLabeledEdit;
    memUpdateStatus: TMemo;
    memUpdateLog: TMemo;
    OpenDialog: TOpenDialog;
    PageControl: TPageControl;
    pnlProgressInfo: TPanel;
    pgbProgramConnect: TProgressBar;
    pgbProgramWrite: TProgressBar;
    pgbUpdateProgress: TProgressBar;
    pgbProgramVerify: TProgressBar;
    rdbFWKeyOnce: TRadioButton;
    rdbFWKeyTenth: TRadioButton;
    StatusBar: TStatusBar;
    stgMemoryView: TStringGrid;
    TabUpdate: TTabSheet;
    TabAdvanced: TTabSheet;
    TabInfo: TTabSheet;
    tmrFlashNotice: TTimer;
    procedure btnSavePaketSettingsClick(Sender: TObject);
    procedure btnSelectFirmwareFilenameClick(Sender: TObject);
    procedure btnClearSettingsClick(Sender: TObject);
    procedure btnUpdateControlClick(Sender: TObject);
    procedure ckbFWKeyChange(Sender: TObject);
    procedure ckbAdvancedModeChange(Sender: TObject);
    procedure ckbPakHidePassChange(Sender: TObject);
    procedure ckbPakMiniGUIChange(Sender: TObject);
    procedure ckbEnablePaketModeChange(Sender: TObject);
    procedure ckbShowMemoryChange(Sender: TObject);
    procedure cmbComPortChange(Sender: TObject);
    procedure edtConnectTriesChange(Sender: TObject);
    procedure edtFirmwareFilenameChange(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCloseQuery(Sender: TObject; var CanClose: boolean);
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure lblCopyrightClick(Sender: TObject);
    procedure PageControlChanging(Sender: TObject; var AllowChange: boolean);
    procedure stgMemoryViewDrawCell(Sender: TObject; aCol, aRow: integer;
      aRect: TRect; aState: TGridDrawState);
    procedure TabAdvancedShow(Sender: TObject);
    procedure tmrFlashNoticeTimer(Sender: TObject);
  private
    { private declarations }
    type
      TUpdateSettings = record
        programWrite: boolean;
        programVerify: boolean;
        programStart: boolean;
        showPrompts: boolean;
        reloadFile: boolean;
        detectWireNum: boolean;
        toggleDTR: boolean;
        connectTries: integer;
        baudrate: integer;
        invertCom: boolean;
        fwKeyMode: TFWKeyMode;
        portNum: integer;
        fwKeyParsed: AnsiString;
        saveLog: boolean;
      end;

    var
      firmwareBuffer: TMemoryBuffer;
      processingUpload: boolean;

      configFileFQN: string;   //Ini-Datei
      configValid: boolean;

      paketMode, hideAdvMode: boolean;

    procedure logMessage(message: string; openMsgBox: boolean = False;
      addToLog: boolean = True; windowIcon: integer = MB_ICONEXCLAMATION);
    procedure logMsgFmt(message: string; const args: array of const;
      openMsgBox: boolean = False; addToLog: boolean = True; windowIcon: integer = MB_ICONEXCLAMATION);
    procedure infoMsg(message: string);
    procedure infoMsgFmt(message: string; const args: array of const);
    procedure flashNotice(showNotice: boolean; message: string = '';
      flashSpeed: integer = 500);

    function uploadFirmware(connection: TBlockSerial; routines: TBootloaderRoutines; settings: TUpdateSettings): boolean;
    procedure sleepActive(duration: integer);

    //Einstellungen laden & sichern: Vollständiger Pfad!
    procedure loadSettings(fileFQN: string);
    procedure saveSettings(fileFQN: string; enablePaketMode: boolean = False);
  public
    { public declarations }
  end;

var
  MainWindow: TMainWindow;

implementation

uses Translations, GetText, Math, i18n;

{$R *.lfm}

{ TMainWindow }
{$REGION 'Programmstart/Ende'}
procedure TMainWindow.loadSettings(fileFQN: string);
var
  iniFile: TMemIniFile;
  lastPage: integer;
  itemIndex: integer;
  miniGUI: boolean;
begin
  assert(fileFQN <> '', 'fileFQN empty');

  iniFile := TMemIniFile.Create(fileFQN);

  try
    if not Assigned(iniFile) then
      raise Exception.Create(rsErrFileLoad);

    paketMode := iniFile.ReadBool('PaketMode', 'Enabled', False);

    //cmbComPort.Items speichert die Portnummer als Integer-Objekt zusätzlich zum Text
    //Im Paketmodus wird der erste verfügbare Port gewählt
    itemIndex := cmbComPort.Items.IndexOfObject(TObject(iniFile.ReadInteger('UpdateLoader', 'ComPort', -1)));
    if (itemIndex <= cmbComPort.Items.Count) and (itemIndex >= 0) and (not paketMode) then
      cmbComPort.ItemIndex := itemIndex;

    //genauso für cmbBaudRate
    itemIndex := cmbBaudRate.Items.IndexOfObject(TObject(iniFile.ReadInteger('Advanced', 'BaudRate', defaultBaudrate)));
    if (itemIndex <= cmbBaudRate.Items.Count) and (itemIndex >= 0) then
        cmbBaudRate.ItemIndex := itemIndex;

    lastPage := iniFile.ReadInteger('UpdateLoader', 'Page', 0);

    ckbAdvancedMode.Checked := iniFile.ReadBool('Advanced', 'Enabled', False);
    ckbProgramWrite.Checked := iniFile.ReadBool('Advanced', 'ProgramWrite', True);
    ckbProgramVerify.Checked := iniFile.ReadBool('Advanced', 'ProgramVerify', True);
    ckbProgramStart.Checked := iniFile.ReadBool('Advanced', 'ProgramStart', True);
    ckbReloadFile.Checked := iniFile.ReadBool('Advanced', 'ReloadFile', True);
    ckbHideMessages.Checked := iniFile.ReadBool('Advanced', 'HideMessages', False);
    ckbShowMemory.Checked := iniFile.ReadBool('Advanced', 'ShowMemory', False);
    ckbDetectOneWire.Checked := iniFile.ReadBool('Advanced', 'DetectOneWire', False);
    ckbToggleDTR.Checked := iniFile.ReadBool('Advanced', 'ToggleDTR', True);
    ckbInvertedCom.Checked := iniFile.ReadBool('Advanced', 'InvertCom', False);
    ckbFWKey.Checked := iniFile.ReadBool('Advanced', 'EnableFirmwareKey', False);
    ckbSaveLog.Checked := iniFile.ReadBool('Advanced', 'SaveLog', True);

    edtFWKey.Text := iniFile.ReadString('Advanced', 'FirmwareKey', '');

    itemIndex := iniFile.ReadInteger('Advanced', 'FirmwareKeyParseMode', 0);
    if (itemIndex <= cmbFWKeyParseMode.Items.Count) and (itemIndex >= 0) then
      cmbFWKeyParseMode.ItemIndex := itemIndex;

    if iniFile.ReadBool('Advanced', 'FirmwareKeyOnce', True) then
      rdbFWKeyOnce.Checked := true
    else
      rdbFWKeyTenth.Checked := true;

    edtBootloaderPassword.Text := iniFile.ReadString('UpdateLoader', 'BootloaderPassword', defaultBootloaderPassword);

    //Im Paketmodus immer den Dateinamen aus der Datei laden (überschreibt ggf. Startparameter)
    if (edtFirmwareFilename.Text = '') or paketMode then
      edtFirmwareFilename.Text := TrimFilename(iniFile.ReadString('UpdateLoader', 'FirmwareFilename', ''));

    if iniFile.ReadInteger('Advanced', 'ConnectTries', defaultConnectTries) > 0 then
      edtConnectTries.Text := IntToStr(iniFile.ReadInteger('Advanced', 'ConnectTries', defaultConnectTries))
    else
      edtConnectTries.Text := IntToStr(defaultConnectTries);

    //Spezialmodus zur Auslieferung (Programmpaket)
    if paketMode then
    begin
      gpbSettingsFile.Hide; //Paketmodus wurde schon erstellt

      miniGUI := iniFile.ReadBool('PaketMode', 'MiniGUI', False); //minimale Oberfläche
      hideAdvMode := iniFile.ReadBool('PaketMode', 'HideAdvancedMode', False) or miniGUI; //Erweitertes in minimaler GUI ausblenden

      if iniFile.ReadBool('PaketMode', 'ModalGUI', False) then
        MainWindow.FormStyle := fsSystemStayOnTop;

      if iniFile.ReadBool('PaketMode', 'LockPassword', False) or miniGUI then
        edtBootloaderPassword.ReadOnly := True;

      if iniFile.ReadBool('PaketMode', 'HidePassword', False) or miniGUI then
        edtBootloaderPassword.PasswordChar := '*';

      if iniFile.ReadBool('PaketMode', 'LockFilename', False) or miniGUI then
      begin
        edtFirmwareFilename.Width := 305;

        btnSelectFirmwareFilename.Enabled := False;
        btnSelectFirmwareFilename.Visible := False;
      end;

      if hideAdvMode or miniGUI then
      begin
        ckbAdvancedMode.Enabled := False;
        ckbAdvancedMode.Visible := False;

        edtBootloaderPassword.Width := 305;
      end;

      //Fehlende FW-Datei abfangen, Auswahlmöglichkeit ist nicht vorhanden/ausgeblendet
      if miniGUI or iniFile.ReadBool('PaketMode', 'LockFilename', False) then
      begin
        if (not FileExistsUTF8(edtFirmwareFilename.Text)) or
          (length(edtFirmwareFilename.Text) <= 0) then
        begin
          logMessage(rsPaketErrFirmwareMissing + sLineBreak + rsPaketInfoFirmwareMissing +
            sLineBreak + sLineBreak + rsExiting,
            True, False, MB_ICONSTOP);

          MainWindow.Hide;
          Application.Terminate;
        end;
      end;

      //Passwort & erweiterter Modus gesperrt und versteckt: Felder komplett ausblenden
      if (not miniGUI) and
        iniFile.ReadBool('PaketMode', 'LockPassword', False) and
        iniFile.ReadBool('PaketMode', 'HidePassword', False) and
        iniFile.ReadBool('PaketMode', 'HideAdvancedMode', False) then
      begin
        gpbConnection.Height := gpbConnection.Height - 45;

        edtBootloaderPassword.Enabled := False;
        edtBootloaderPassword.Visible := False;

        gpbUpdate.Top := gpbUpdate.Top - 50;
        gpbInfo.Height := gpbInfo.Height - 50;
        memUpdateLog.Height := memUpdateLog.Height - 50;
        PageControl.Height := PageControl.Height - 50;
        gpbStatus.Top := gpbStatus.Top - 50;
        pnlProgressInfo.Top := pnlProgressInfo.Top - 50;
        MainWindow.Height := MainWindow.Height - 50;
      end;

      //Verkleinerte GUI
      if miniGUI then
      begin
        edtFirmwareFilename.Enabled := False;
        edtFirmwareFilename.Visible := False;
        edtBootloaderPassword.Enabled := False;
        edtBootloaderPassword.Visible := False;

        PageControl.Width := 345;
        PageControl.Height := 210;

        gpbAbout.Width := 325;

        gpbConnection.Height := 75;

        gpbInfo.Visible := False;
        gpbStatus.Visible := False;

        gpbUpdate.Height := 90;
        gpbUpdate.Top := 85;
        btnUpdateControl.Top := 12;

        pnlProgressInfo.Top := 220;
        pnlProgressInfo.Width := 345;
        pnlProgressInfo.Height := 32;
        lblUpdateProgress.Font.Height := 26;

        MainWindow.Width := 355;
        MainWindow.Height := 280;
      end;
    end;

  finally
    iniFile.Free;
  end;

  //Events auslösen um Einstellungen zu übernehmen
  ckbAdvancedModeChange(Self);
  ckbEnablePaketModeChange(Self);
  ckbFWKeyChange(Self);
  cmbComPortChange(Self);

  //Letzten Tab laden, Rückfall auf Update-Tab
  if (lastPage = 0) or (not TabAdvanced.TabVisible) or paketMode then
    PageControl.ActivePage := TabUpdate
  else if lastPage = 1 then
    PageControl.ActivePage := TabAdvanced;
end;

procedure TMainWindow.saveSettings(fileFQN: string; enablePaketMode: boolean = False);
var
  iniFile: TMemIniFile;
begin
  assert(fileFQN <> '', 'fileFQN empty');

  iniFile := TMemIniFile.Create(fileFQN);

  try
    if not Assigned(iniFile) then
      raise Exception.Create(rsErrFileSave);

    iniFile.Clear;

    //Sektion "UpdateLoader"
    if cmbComPort.ItemIndex >= 0 then
      iniFile.WriteInteger('UpdateLoader', 'ComPort', integer(cmbComPort.Items.Objects[cmbComPort.ItemIndex]))
    else
      iniFile.WriteInteger('UpdateLoader', 'ComPort', -1);

    iniFile.WriteString('UpdateLoader', 'BootloaderPassword', edtBootloaderPassword.Text);
    iniFile.WriteString('UpdateLoader', 'FirmwareFilename', edtFirmwareFilename.Text);

    iniFile.WriteInteger('UpdateLoader', 'Page', PageControl.ActivePageIndex);

    //Sektion "Advanced"
    iniFile.WriteInteger('Advanced', 'BaudRate', integer(cmbBaudRate.Items.Objects[cmbBaudRate.ItemIndex]));
    iniFile.WriteInteger('Advanced', 'FirmwareKeyParseMode', cmbFWKeyParseMode.ItemIndex);

    iniFile.WriteBool('Advanced', 'ShowMemory', ckbShowMemory.Checked);
    iniFile.WriteBool('Advanced', 'HideMessages', ckbHideMessages.Checked);

    iniFile.WriteBool('Advanced', 'Enabled', ckbAdvancedMode.Checked);
    iniFile.WriteBool('Advanced', 'ProgramWrite', ckbProgramWrite.Checked);
    iniFile.WriteBool('Advanced', 'ProgramVerify', ckbProgramVerify.Checked);
    iniFile.WriteBool('Advanced', 'ProgramStart', ckbProgramStart.Checked);
    iniFile.WriteBool('Advanced', 'ReloadFile', ckbReloadFile.Checked);
    iniFile.WriteBool('Advanced', 'DetectOneWire', ckbDetectOneWire.Checked);
    iniFile.WriteBool('Advanced', 'ToggleDTR', ckbToggleDTR.Checked);
    iniFile.WriteBool('Advanced', 'InvertCom', ckbInvertedCom.Checked);
    iniFile.WriteBool('Advanced', 'EnableFirmwareKey', ckbFWKey.Checked);
    iniFile.WriteBool('Advanced', 'FirmwareKeyOnce', rdbFWKeyOnce.Checked);
    iniFile.WriteBool('Advanced', 'SaveLog', ckbSaveLog.Checked);

    iniFile.WriteString('Advanced', 'FirmwareKey', edtFWKey.Text);

    if StrToInt(edtConnectTries.Text) > 0 then
      iniFile.WriteInteger('Advanced', 'ConnectTries',
        StrToInt(edtConnectTries.Text))
    else
      iniFile.WriteInteger('Advanced', 'ConnectTries', defaultConnectTries);

    //Sektion "PaketMode"
    iniFile.WriteBool('PaketMode', 'Enabled', enablePaketMode);

    iniFile.WriteBool('PaketMode', 'MiniGUI', ckbPakMiniGUI.Checked);
    iniFile.WriteBool('PaketMode', 'ModalGUI', ckbPakOnTop.Checked);
    iniFile.WriteBool('PaketMode', 'HideAdvancedMode', ckbPakHideAdv.Checked);
    iniFile.WriteBool('PaketMode', 'LockFilename', ckbPakLockFile.Checked);
    iniFile.WriteBool('PaketMode', 'LockPassword', ckbPakLockPass.Checked);
    iniFile.WriteBool('PaketMode', 'HidePassword', ckbPakHidePass.Checked);

    //Pfad zur Firmware im Paketmodus entfernen (Datei liegt immer neben Anwendung)
    if enablePaketMode then
      iniFile.WriteString('UpdateLoader', 'FirmwareFilename', ExtractFileName(edtFirmwareFilename.Text));

    iniFile.UpdateFile;

  finally
    iniFile.Free;
  end;
end;

procedure TMainWindow.FormShow(Sender: TObject);
var
  fileVersion: TFileVersionInfo;
  portCount, index, dropDownWidth: integer;
begin
  try
    portCount := getComportCount;
  except
  end;

  if portCount <= 0 then
  begin
    logMessage(rsCOMErrNoPort + sLineBreak + rsCOMInfoNoPort + sLineBreak + sLineBreak + rsExiting,
      True, False, MB_ICONSTOP);

    MainWindow.Hide;
    Application.Terminate;
  end;

  //Versioninformationen für Fenstertitel & Info-Seite
  try
    try
      fileVersion := TFileVersionInfo.Create(Application);
      fileVersion.fileName := Application.ExeName;

      edtVersionInfo.Text := 'Version: ' + fileversion.getVersionSetting('ProductName') +
        ' ' + fileversion.getVersionSetting('ProductVersion') +
        ' (Build ' + fileversion.getVersionSetting('FileVersion') + ', ' +
{$I %DATE%}
        +')';

      Application.Title := fileversion.getVersionSetting('ProductName') +
        ' ' + fileversion.getVersionSetting('ProductVersion');

      MainWindow.Caption := fileversion.getVersionSetting('ProductName') +
        ' ' + fileversion.getVersionSetting('ProductVersion') +
        ' (Build ' + fileversion.getVersionSetting('FileVersion') + ')';

      lblBootloaderVersion.Caption := 'Bootloader-Version: ' + TBootloaderRoutines.getBootVersion;
      lblComVersion.Caption := 'COM: ' + TBlockSerial.GetVersion;
    finally
      fileVersion.Free;
    end;
  except
  end;

  //Copyright Info
  lblCopyright.Caption := '© 2012-2023 Leo-Andres Hofmann (LUANI.de)';

  //Verfügbare Baudraten auflisten, Voreinstellung laden
  getBaudrates(cmbBaudRate.Items);
  cmbBaudRate.DropDownCount := cmbBaudRate.Items.Count;
  cmbBaudRate.ItemIndex := cmbBaudRate.Items.IndexOfObject(TObject(defaultBaudrate));

  //Verfügbare Ports auflisten
  try
    getComportNames(cmbComPort.Items);
  except
  end;
  cmbComPort.ItemIndex := 0; //Ersten verfügbaren Port vorauswählen

  //Größe des Dropdown-Felds erweitern, ggf. um Breite der Scrollbar erweitert
  dropDownWidth := cmbComPort.Width;
  for index := 0 to pred(cmbComPort.Items.Count) do
    dropDownWidth := Max(dropDownWidth, cmbComPort.Canvas.TextWidth(cmbComPort.Items[index]) + 8);

  //Verbreitern wenn gescrollt werden muss
  if cmbComPort.DropDownCount < cmbComPort.Items.Count then
    Inc(dropDownWidth, GetSystemMetrics(SM_CXVSCROLL));
  SendMessage(cmbComPort.Handle, 352, EnsureRange(dropDownWidth, cmbComPort.Width, MainWindow.Width), 0); //352/xH160 = CB_SETDROPPEDWIDTH

  StatusBar.Panels[0].Text := rsStatusReadyNoFile;
  stgMemoryView.RowCount := 0;

  //UpdateLoader.ini liegt neben Anwendung im Verzeichnis
  configFileFQN := ExtractFilePath(Application.ExeName) + 'UpdateLoader.ini';

  //Startparameter: .ini oder .hex-Datei (überschreibt Voreinstellung)
  if ParamCount > 0 then
  begin
    if FileExistsUTF8(ParamStr(1)) then
    begin
      if lowerCase(ExtractFileExt(ParamStr(1))) = '.ini' then
        configFileFQN := ParamStr(1)
      else if lowerCase(ExtractFileExt(ParamStr(1))) = '.hex' then
        edtFirmwareFilename.Text := ParamStr(1);
    end;
  end;

  //Einstellungen laden
  try
    loadSettings(configFileFQN);
    configValid := True;
  except
    on E: Exception do
      ShowMessage(rsErrSettingsLoad + sLineBreak + E.Message);
  end;

  memUpdateStatus.Clear;
  memUpdateLog.Clear;
end;

procedure TMainWindow.lblCopyrightClick(Sender: TObject);
begin
  OpenURL('https://luani.de');
end;

procedure TMainWindow.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  //Einstellungen speichern
  if configValid and (not paketMode) then
  begin
    try
      saveSettings(configFileFQN);
    except
      on E: Exception do
        ShowMessage(rsErrSettingsSave + sLineBreak + E.Message);
    end;
  end;

  CloseAction := caFree;
end;

procedure TMainWindow.FormCloseQuery(Sender: TObject; var CanClose: boolean);
begin
  CanClose := not processingUpload;
end;

procedure TMainWindow.FormCreate(Sender: TObject);
var
  dialogTranslation: TPOFile;
begin
  inherited;

  firmwareBuffer := TMemoryBuffer.Create(settingMaxBufferSize, true);

  processingUpload := False;

  configFileFQN := '';
  configValid := False;

  paketMode := False;
  hideAdvMode := False;

  //Schaltflächen übersetzen
  dialogTranslation := TPOFile.Create;

  dialogTranslation.Add('rsmbyes', '&Yes', rsDialogYes, '-', 'lclstrconsts', '', '');
  dialogTranslation.Add('rsmbno', '&No', rsDialogNo, '-', 'lclstrconsts', '', '');
  dialogTranslation.Add('rsmbok', '&OK', rsDialogOk, '-', 'lclstrconsts', '', '');
  dialogTranslation.Add('rsmbcancel', 'Cancel', rsDialogCancel, '-', 'lclstrconsts', '', '');

  Translations.TranslateUnitResourceStrings('LCLStrConsts', dialogTranslation);
end;
{$ENDREGION}

{$REGION 'Events (Button-Betätigung)'}
procedure TMainWindow.PageControlChanging(Sender: TObject; var AllowChange: boolean);
begin
  AllowChange := not processingUpload;
end;

procedure TMainWindow.TabAdvancedShow(Sender: TObject);
var
  oldCursor: TCursor;
begin
  //Neuer Speicherinhalt verfügbar
  if boolean(firmwareBuffer.Tag) and firmwareBuffer.Valid and ckbShowMemory.Checked then
  begin
    firmwareBuffer.Tag := integer(False);

    if TabAdvanced.IsVisible then
    begin
      //Sichtbar: Darstellung aufbauen
      oldCursor := Screen.Cursor;
      Screen.Cursor := crHourGlass;

      createMemoryView(stgMemoryView, firmwareBuffer, pgbUpdateProgress);
      gpbMemoryView.Caption := Format(rsCaptionProgramSize, [convertKiB2String(firmwareBuffer.Used)]);

      Screen.Cursor := oldCursor;
    end
    else if not TabAdvanced.IsVisible and (firmwareBuffer.Used > settingMaxAutoShowBuffer) then
    begin
      //Große Datei, Reiter nicht sichtbar: Nicht automatisch neu laden
      logMsgFmt(rsInfoLargeFile, [convertKiB2String(settingMaxAutoShowBuffer)]);
      ckbShowMemory.Checked := False;
    end
    else if not TabAdvanced.IsVisible and (firmwareBuffer.Used <= settingMaxAutoShowBuffer) then
    begin
      //Kleine Datei, Reiter nicht sichtbar: Beim Anzeigen laden
      firmwareBuffer.Tag := integer(True);
    end;
  end
  else if boolean(firmwareBuffer.Tag) then
  begin
    //Keine Datei geladen oder Anzeige deaktiviert
    stgMemoryView.RowCount := 0;
    gpbMemoryView.Caption := rsCaptionProgram;

    firmwareBuffer.Tag := integer(False);
  end;
end;

procedure TMainWindow.ckbShowMemoryChange(Sender: TObject);
begin
  firmwareBuffer.Tag := integer(True); //Neue Darstellung anfordern

  if TabAdvanced.TabVisible then   //Darstellung aktualisieren
    TabAdvancedShow(Self);
end;

procedure TMainWindow.btnClearSettingsClick(Sender: TObject);
var
  reply: integer;
begin
  reply := Application.MessageBox(PChar(rsConfirmSettingsClear),
    'UpdateLoader', MB_ICONQUESTION or MB_YESNO or MB_DEFBUTTON2);

  if reply = idYes then
  begin
    cmbComPort.ItemIndex := 0;
    cmbBaudRate.ItemIndex := cmbBaudRate.Items.IndexOfObject(TObject(defaultBaudrate));
    cmbFWKeyParseMode.ItemIndex := 0;
    PageControl.ActivePage := TabUpdate;

    ckbAdvancedMode.Checked := False;
    ckbProgramWrite.Checked := True;
    ckbProgramVerify.Checked := True;
    ckbProgramStart.Checked := True;
    ckbReloadFile.Checked := True;
    ckbHideMessages.Checked := False;
    ckbShowMemory.Checked := False;
    ckbDetectOneWire.Checked := False;
    ckbToggleDTR.Checked := True;
    ckbInvertedCom.Checked := False;
    ckbFWKey.Checked := False;
    rdbFWKeyOnce.Checked := True;
    ckbSaveLog.Checked := True;

    ckbEnablePaketMode.Checked := False;
    ckbPakMiniGUI.Checked := False;
    ckbPakOnTop.Checked := False;
    ckbPakHideAdv.Checked := False;
    ckbPakLockFile.Checked := False;
    ckbPakLockPass.Checked := False;
    ckbPakHidePass.Checked := False;

    edtBootloaderPassword.Text := defaultBootloaderPassword;
    edtFirmwareFilename.Text := '';
    edtConnectTries.Text := IntToStr(defaultConnectTries);
    edtFWKey.Text := '';

    ckbAdvancedModeChange(Self);
    ckbEnablePaketModeChange(Self);

    try
      saveSettings(configFileFQN);
    except
      on E: Exception do
        ShowMessage(rsErrSettingsSave + sLineBreak + E.Message);
    end;
  end;
end;

procedure TMainWindow.btnSavePaketSettingsClick(Sender: TObject);
var
  reply: integer;
begin
  reply := Application.MessageBox(PChar(rsConfirmSettingsSave + sLineBreak + sLineBreak + rsPaketInfoSaveAbsPath),
    'UpdateLoader', MB_ICONQUESTION or MB_YESNO or MB_DEFBUTTON2);

  if reply = idYes then
  begin
    //Einstellungen speichern
    try
      if configValid and (not paketMode) then
        saveSettings(configFileFQN, True)
      else
        raise Exception.Create(rsErrSettingsLocked);
    except
      on E: Exception do
      begin
        ShowMessage(rsErrSettingsSave + sLineBreak + E.Message);
        Exit;
      end;
    end;

    reply := Application.MessageBox(PChar(rsPaketConfigSaved +
      sLineBreak + rsPaketReady +
      sLineBreak + sLineBreak + rsConfirmExit), 'UpdateLoader',
      MB_ICONINFORMATION or MB_YESNO or MB_DEFBUTTON1);

    if reply = idYes then
      Application.Terminate;
  end;
end;

procedure TMainWindow.cmbComPortChange(Sender: TObject);
begin
  if cmbComPort.ItemIndex >= 0 then
  begin
    cmbComPort.Hint := cmbComPort.Items[cmbComPort.ItemIndex];
    cmbComPort.ShowHint := true;
  end
  else
    cmbComPort.ShowHint := false;
end;

procedure TMainWindow.btnSelectFirmwareFilenameClick(Sender: TObject);
begin
  //Dialog im order der Firmware-Datei oder Anwendung öffnen
  if edtFirmwareFilename.Text <> '' then
    OpenDialog.Filename := edtFirmwareFilename.Text
  else
    OpenDialog.Filename := ExtractFilePath(Application.ExeName);

  if OpenDialog.Execute then
  begin
    if edtFirmwareFilename.Text = OpenDialog.FileName then
      edtFirmwareFilenameChange(Self)   //Change-Event auslösen um Datei neu zu laden
    else
      edtFirmwareFilename.Text := OpenDialog.FileName;
  end;
end;

procedure TMainWindow.ckbAdvancedModeChange(Sender: TObject);
begin
  TabAdvanced.TabVisible := ckbAdvancedMode.Checked and (not hideAdvMode);
  if (not TabAdvanced.TabVisible) and (PageControl.ActivePage = TabAdvanced) then
    PageControl.ActivePage := TabUpdate;
end;

procedure TMainWindow.ckbPakHidePassChange(Sender: TObject);
begin
  if ckbPakHidePass.Checked then
  begin
    ckbPakLockPass.Tag := integer(ckbPakLockPass.Checked);
    ckbPakLockPass.Checked := True;
    ckbPakLockPass.Enabled := False;
  end
  else
  begin
    ckbPakLockPass.Checked := boolean(ckbPakLockPass.Tag);
    ckbPakLockPass.Enabled := True;
  end;
end;

procedure TMainWindow.ckbPakMiniGUIChange(Sender: TObject);
begin
  if ckbPakMiniGUI.Checked then
  begin
    ckbPakHideAdv.Tag := integer(ckbPakHideAdv.Checked);
    ckbPakLockFile.Tag := integer(ckbPakLockFile.Checked);
    ckbPakLockPass.Tag := integer(ckbPakLockPass.Checked);
    ckbPakHidePass.Tag := integer(ckbPakHidePass.Checked);

    ckbPakHideAdv.Checked := True;
    ckbPakLockFile.Checked := True;
    ckbPakLockPass.Checked := True;
    ckbPakHidePass.Checked := True;

    ckbPakHideAdv.Enabled := False;
    ckbPakLockFile.Enabled := False;
    ckbPakLockPass.Enabled := False;
    ckbPakHidePass.Enabled := False;
  end
  else
  begin
    ckbPakHideAdv.Checked := boolean(ckbPakHideAdv.Tag);
    ckbPakLockFile.Checked := boolean(ckbPakLockFile.Tag);
    ckbPakLockPass.Checked := boolean(ckbPakLockPass.Tag);
    ckbPakHidePass.Checked := boolean(ckbPakHidePass.Tag);

    ckbPakHideAdv.Enabled := True;
    ckbPakLockFile.Enabled := True;
    ckbPakLockPass.Enabled := True;
    ckbPakHidePass.Enabled := True;
  end;
end;

procedure TMainWindow.ckbEnablePaketModeChange(Sender: TObject);
begin
  ckbPakMiniGUI.Enabled := ckbEnablePaketMode.Checked;
  ckbPakOnTop.Enabled := ckbEnablePaketMode.Checked;
  ckbPakHideAdv.Enabled := ckbEnablePaketMode.Checked;
  ckbPakLockFile.Enabled := ckbEnablePaketMode.Checked;
  ckbPakLockPass.Enabled := ckbEnablePaketMode.Checked;
  ckbPakHidePass.Enabled := ckbEnablePaketMode.Checked;

  btnSavePaketSettings.Enabled := ckbEnablePaketMode.Checked;
end;

procedure TMainWindow.ckbFWKeyChange(Sender: TObject);
begin
  rdbFWKeyOnce.Enabled := ckbFWKey.Checked;
  rdbFWKeyTenth.Enabled := ckbFWKey.Checked;
  cmbFWKeyParseMode.Enabled := ckbFWKey.Checked;
  edtFWKey.Enabled := ckbFWKey.Checked;
end;

procedure TMainWindow.edtConnectTriesChange(Sender: TObject);
var
  convert: integer;
begin
  if edtConnectTries.Text = '' then
    exit;

  if not TryStrToInt(edtConnectTries.Text, convert) then
    edtConnectTries.Text := IntToStr(defaultConnectTries)
  else if StrToInt(edtConnectTries.Text) <= 0 then
    edtConnectTries.Text := IntToStr(defaultConnectTries);

  edtConnectTries.SelStart := Length(edtConnectTries.Text);
end;

procedure TMainWindow.edtFirmwareFilenameChange(Sender: TObject);
var
  oldCursor: TCursor;
begin
  if (FileExistsUTF8(edtFirmwareFilename.Text)) and
    (length(edtFirmwareFilename.Text) > 0) then
  begin
    oldCursor := Screen.Cursor;
    Screen.Cursor := crHourGlass;
    pgbUpdateProgress.Style := pbstMarquee;

    try
      loadFirmwareFile(edtFirmwareFilename.Text, firmwareBuffer);
    except
      on E: Exception do
      begin
        logMessage(E.Message + ' (' + ExtractFileName(edtFirmwareFilename.Text) +
          ')', False, True);
        logMessage(rsErrLoadFirmware + sLineBreak + E.Message +
          sLineBreak + sLineBreak + ExtractFileName(edtFirmwareFilename.Text),
          True, False, MB_ICONSTOP);
      end;
    end;

    if firmwareBuffer.Valid and not (Sender = btnUpdateControl) then
    begin
      logMsgFmt(rsInfoFirmwareLoaded, [ExtractFileName(edtFirmwareFilename.Text)]);
      StatusBar.Panels[0].Text := Format(rsStatusReadyLoaded, [ExtractFileName(edtFirmwareFilename.Text)]);
    end;

    Screen.Cursor := oldCursor;
    pgbUpdateProgress.Style := pbstNormal;
  end
  else
  begin
    firmwareBuffer.Clear;

    edtFirmwareFilename.Text := '';
  end;

  btnUpdateControl.Enabled := firmwareBuffer.Valid;
  ckbShowMemoryChange(Self); //Hex-Darstellung neu laden

  if not firmwareBuffer.Valid then
    StatusBar.Panels[0].Text := rsStatusReadyNoFile;
end;

procedure TMainWindow.stgMemoryViewDrawCell(Sender: TObject;
  aCol, aRow: integer; aRect: TRect; aState: TGridDrawState);
begin
  if gdFixed in aState then
    Exit;

  case aCol of
    0:
    begin
      stgMemoryView.Canvas.Font.Color := RGB(128, 0, 0);
    end;
    4:
    begin
      stgMemoryView.Canvas.Font.Color := RGB(128, 0, 128);
    end;
    else
    begin
      stgMemoryView.Canvas.Font.Color := RGB(0, 0, 128);
    end;
  end;

  stgMemoryView.Canvas.TextOut(aRect.Left + 2, aRect.Top + 2,
    stgMemoryView.Cells[aCol, aRow]);
  stgMemoryView.Canvas.TextRect(aRect, aRect.Left + 2, aRect.Top +
    2, stgMemoryView.Cells[aCol, aRow]);
end;
{$ENDREGION}

{$REGION 'Hilfsfunktionen für die Benutzerführung'}
procedure TMainWindow.logMessage(message: string; openMsgBox: boolean = False;
  addToLog: boolean = True; windowIcon: integer = MB_ICONEXCLAMATION);
begin
  if addToLog then
    memUpdateLog.Lines.Add(message);

  if openMsgBox then
    Application.MessageBox(PChar(message), PChar(rsMessageTitle),
      MB_OK + windowIcon);
end;

procedure TMainWindow.logMsgFmt(message: string; const args: array of const;
      openMsgBox: boolean = False; addToLog: boolean = True; windowIcon: integer = MB_ICONEXCLAMATION);
begin
  logMessage(Format(message, args), openMsgBox, addToLog, windowIcon);
end;

procedure TMainWindow.infoMsg(message: string);
begin
  memUpdateStatus.Lines.Add(message);
end;

procedure TMainWindow.infoMsgFmt(message: string; const args: array of const);
begin
  infoMsg(Format(message, args));
end;

procedure TMainWindow.flashNotice(showNotice: boolean; message: string = '';
  flashSpeed: integer = 500);
begin
  lblFlashNotice.Caption := message;
  lblFlashNotice.Visible := showNotice;
  lblFlashNotice.Left := round((gpbInfo.Width - lblFlashNotice.Width) / 2);

  tmrFlashNotice.Interval := flashSpeed;
  tmrFlashNotice.Enabled := showNotice;
end;

procedure TMainWindow.tmrFlashNoticeTimer(Sender: TObject);
begin
  lblFlashNotice.Visible := not lblFlashNotice.Visible;
end;

procedure TMainWindow.sleepActive(duration: integer);
var
  i: integer;
begin
  if duration <= 0 then
    Exit;

  for i := 0 to Round(duration / 100) do
  begin
    Sleep(100);
    Application.ProcessMessages;
  end;
end;
{$ENDREGION}

{$REGION 'Update-Routine'}
procedure TMainWindow.btnUpdateControlClick(Sender: TObject);
var
  updateConfig: TUpdateSettings;
  serialConnection: TBlockSerial;
  bootloaderRoutines: TBootloaderRoutines;
  startTime: TDateTime;
  oldCursor: TCursor;
  oldStatus: string;
  reply: integer;
  success: boolean;
  updataDur: string;
  logFile: string;
begin
  if self.processingUpload then
    Exit;

  //Konfiguration prüfen
  if cmbComPort.ItemIndex <= -1 then
  begin
    logMessage(rsErrNoComSel +
      sLineBreak + sLineBreak + rsIdle, True, False, MB_ICONINFORMATION);
    Exit;
  end;

  if ckbAdvancedMode.Checked and (cmbBaudRate.ItemIndex <= -1) then
  begin
    logMessage(rsErrNoBaudSel +
      sLineBreak + sLineBreak + rsIdle, True, False, MB_ICONINFORMATION);
    Exit;
  end;

  if edtBootloaderPassword.Text = '' then
  begin
    logMessage(rsErrNoPassword + sLineBreak + rsInfoNoPassword, True, False, MB_ICONSTOP);
    Exit;
  end;

  //Einstellungen zusammenstellen
  if ckbAdvancedMode.Checked then
  begin
    updateConfig.programWrite := ckbProgramWrite.Checked;
    updateConfig.programVerify := ckbProgramVerify.Checked;
    updateConfig.programStart := ckbProgramStart.Checked;
    updateConfig.showPrompts := not ckbHideMessages.Checked;
    updateConfig.reloadFile := ckbReloadFile.Checked;
    updateConfig.detectWireNum := ckbDetectOneWire.Checked;
    updateConfig.toggleDTR := ckbToggleDTR.Checked;
    updateConfig.invertCom := ckbInvertedCom.Checked;

    updateConfig.connectTries := StrToIntDef(edtConnectTries.Text, defaultConnectTries);
    updateConfig.baudrate := integer(cmbBaudRate.Items.Objects[cmbBaudRate.ItemIndex]);

    if updateConfig.connectTries <= 0 then
      updateConfig.connectTries := defaultConnectTries;

    if updateConfig.baudrate <= 0 then
      updateConfig.baudrate := defaultBaudrate;

    if ckbFWKey.Checked and rdbFWKeyOnce.Checked then
      updateConfig.fwKeyMode := KEYONCE
    else if ckbFWKey.Checked and rdbFWKeyTenth.Checked then
      updateConfig.fwKeyMode := KEYTENTH
    else
      updateConfig.fwKeyMode := NOKEY;

    updateConfig.saveLog := ckbSaveLog.Checked;
  end
  else //Standardeinstellungen verwenden
  begin
    updateConfig.programWrite := True;
    updateConfig.programVerify := True;
    updateConfig.programStart := True;
    updateConfig.showPrompts := True;
    updateConfig.reloadFile := True;
    updateConfig.detectWireNum := defaultDetectOneWire;
    updateConfig.toggleDTR := defaultToggleDTR;
    updateConfig.connectTries := defaultConnectTries;
    updateConfig.baudrate := defaultBaudrate;
    updateConfig.invertCom := False;
    updateConfig.fwKeyMode := NOKEY;
    updateConfig.saveLog := true;
  end;
  updateConfig.portNum := integer(cmbComPort.Items.Objects[cmbComPort.ItemIndex]);

  //Firmware Passwort konvertieren
  if (updateConfig.fwKeyMode <> NOKEY) and (cmbFWKeyParseMode.Text = 'ASCII') then
  begin
    updateConfig.fwKeyParsed := edtFWKey.Text;
  end
  else if (updateConfig.fwKeyMode <> NOKEY) and (cmbFWKeyParseMode.Text = 'HEX') then
  begin
    try
      updateConfig.fwKeyParsed := parseHexString(edtFWKey.Text);
    except
      on E: Exception do
      begin
        logMsgFmt(rsErrPasswordFormat + sLineBreak + '%s' + sLineBreak +
          sLineBreak + rsInfoConnectionImpossible,
          [E.Message], True, False, MB_ICONSTOP);
        Exit;
      end;
    end;
  end
  else
    updateConfig.fwKeyParsed := '';

  if not (updateConfig.programWrite or updateConfig.programVerify) then
  begin
    logMessage(rsInfoNoTask + sLineBreak + sLineBreak + rsIdle, True, False, MB_ICONINFORMATION);
    Exit;
  end;

  //Datei neu einlesen
  if updateConfig.reloadFile then
    edtFirmwareFilenameChange(btnUpdateControl);

  if not firmwareBuffer.Valid then
  begin
    logMessage(rsInfoNoFirmware +
      sLineBreak + sLineBreak + rsIdle, True, False, MB_ICONINFORMATION);
    Exit;
  end;

  //Benutzerführung
  if updateConfig.showPrompts then
  begin
    reply := Application.MessageBox(PChar(rsWarningNoDowngrade + sLineBreak + sLineBreak + rsConfirmStart),
      'UpdateLoader', MB_ICONWARNING or MB_YESNO or MB_DEFBUTTON2);
    if reply = idNo then
      Exit;
  end;

  //Initialisierung
  self.processingUpload := True;
  startTime := TDateTime(now);
  success := false;

  //GUI sperren
  btnSelectFirmwareFilename.Enabled := False;
  btnUpdateControl.Enabled := False;
  cmbComPort.Enabled := False;
  edtBootloaderPassword.Enabled := False;
  edtFirmwareFilename.Enabled := False;
  ckbAdvancedMode.Enabled := False;
  PageControl.ActivePage := TabUpdate;

  //Statusanzeigen setzen
  oldCursor := Screen.Cursor;
  Screen.Cursor := crHourGlass;
  oldStatus := StatusBar.Panels[0].Text;
  memUpdateStatus.Clear;
  lblUpdateProgress.Caption := rsProgress;
  lblUpdateProgress.Color := clNone;
  pgbUpdateProgress.Visible := True;

  if updateConfig.showPrompts then
    MainWindow.FormStyle := fsStayOnTop;

  btnUpdateControl.Caption := rsPleaseWait;
  StatusBar.Panels[0].Text := rsStatusBusy;
  logMsgFmt(rsLogUpdateStart, [convertKiB2String(firmwareBuffer.Used)]);
  infoMsgFmt(rsInfoUpdateStart, [ExtractFileName(edtFirmwareFilename.Text),
    convertKiB2String(firmwareBuffer.Used)]);

{------------------------------------------------------------------------------}
  //Update ausführen
  try
    serialConnection := TBlockSerial.Create;
    bootloaderRoutines := TBootloaderRoutines.Create(self.firmwareBuffer);

    try
      success := self.uploadFirmware(serialConnection, bootloaderRoutines, updateConfig);
    except
      on E: Exception do
      begin
        logMsgFmt(rsException + sLineBreak + '%s' + sLineBreak + '(COM: %s)',
          [E.Message, serialConnection.GetErrorDesc(serialConnection.LastError)], true, true, MB_ICONSTOP);
        logMessage(E.Message);
        infoMsg(E.Message);
      end;
    end;

    //Verbindung freigeben
    if serialConnection.InstanceActive then
    begin
      serialConnection.Purge;
      serialConnection.Flush;
      serialConnection.CloseSocket;
    end;
  finally
    serialConnection.Free;
    bootloaderRoutines.Free;
  end;
{------------------------------------------------------------------------------}

  //GUI freigeben
  btnSelectFirmwareFilename.Enabled := True;
  btnUpdateControl.Enabled := True;
  cmbComPort.Enabled := True;
  edtBootloaderPassword.Enabled := True;
  edtFirmwareFilename.Enabled := True;
  ckbAdvancedMode.Enabled := True;

  //Statusanzeige zurücksetzen
  flashNotice(False);
  pgbUpdateProgress.Visible := False;

  Screen.Cursor := oldCursor;
  StatusBar.Panels[0].Text := oldStatus;

  if updateConfig.showPrompts then
    MainWindow.FormStyle := fsNormal;

  btnUpdateControl.Caption := rsBtnStartUpdate;
  pgbProgramConnect.Position := 0;
  pgbProgramWrite.Position := 0;
  pgbProgramVerify.Position := 0;
  pgbUpdateProgress.Position := 0;

  //Meldung
  if success then
  begin
    logMessage(rsLogUpdateSuccess);

    DateTimeToString(updataDur, rsFormatTime, TDateTime(now) - startTime);
    logMsgFmt(rsLogDuration, [updataDur]);
    infoMsgFmt(rsLogDuration, [updataDur]);

    lblUpdateProgress.Caption := Format(rsStatusSuccess, [ExtractFileName(edtFirmwareFilename.Text)]);
    lblUpdateProgress.Color := clMoneyGreen;

    if paketMode or updateConfig.showPrompts then
    begin
      logMsgFmt(rsLogUpdateSuccess + sLineBreak +
        rsLogDuration, [updataDur], True, False, MB_ICONINFORMATION);
    end;
  end
  else
  begin
    infoMsg(rsInfoUpdateCancelled);
    logMessage(rsLogUpdateCancelled);

    lblUpdateProgress.Caption := rsStatusFailed;
    lblUpdateProgress.Color := clSkyBlue;
  end;

  //Log speichern
  if updateConfig.saveLog then
  begin
    try
      logFile := edtFirmwareFilename.Text + '_UpdateLog.txt';
      memUpdateStatus.Lines.SaveToFile(logFile);
    except
      on E: Exception do
      begin
        logMessage(rsErrFileSave + sLineBreak + E.Message);
      end;
    end;
  end;

  //Status zurücksetzen, Freigabe für nächsten Durchlauf
  self.processingUpload := False;
end;

function TMainWindow.uploadFirmware(connection: TBlockSerial; routines: TBootloaderRoutines; settings: TUpdateSettings): boolean;
var
  wireNum: TWireCount;
  reply: integer;
  supportsCRC: boolean;
  supportsVerify: boolean;
  deviceName, deviceSignature: string;
  deviceFlashSize: longint;
  deviceWriteBuffer: integer;
  deviceRevision: string;
begin
  assert(assigned(connection), 'connection not assigned');
  assert(assigned(routines), 'routines not assigned');
  assert(self.firmwareBuffer.Valid, 'Invalid firmware buffer');

  Result := False;

  pgbUpdateProgress.Position := 0;
  pgbUpdateProgress.Max := 12;
  pgbUpdateProgress.Step := 1;

{------------------------------------------------------------------------------}
  //Schnittstelle konfigurieren
  infoMsgFmt (rsCOMInfoStart, [settings.portNum]);

  connection.RaiseExcept := True; //Exceptions um belegten Port zu erkennen
  connection.LinuxLock := False;
  connection.InterPacketTimeout := False;

  //Com-Port öffnen
  try
    connection.Connect(Format('COM%d', [settings.portNum]));
    sleepActive(settingPortSetupDelay);
    connection.Config(settings.baudrate, 8, 'N', SB1, False, False);
    sleepActive(settingPortSetupDelay);
    connection.Purge;
    connection.Flush;
  except
    on E: Exception do
    begin
      logMsgFmt('%s' + sLineBreak + sLineBreak + rsCOMErrOpen +
        sLineBreak + rsCOMInfoBlocked,
        [connection.GetErrorDesc(connection.LastError)], True, False, MB_ICONSTOP);
      logMessage(connection.GetErrorDesc(connection.LastError));
      infoMsg(connection.GetErrorDesc(connection.LastError));
      Exit;
    end;
  end;

  //Exceptions deaktivieren für Verbindungsaufbau
  connection.RaiseExcept := False;

  //Geöffnete Verbindung und Einstellungen an Bootloader-Klasse übergeben
  routines.serialConnection := connection;

  routines.bootloaderPassword := edtBootloaderPassword.Text;
  routines.firmwarePassword := settings.fwKeyParsed;

  routines.toggleDTR := settings.toggleDTR;
  routines.invertConnection := settings.invertCom;
  routines.firmwarePasswordMode := settings.fwKeyMode;

{------------------------------------------------------------------------------}
  //Verbindungsaufbau
  pgbUpdateProgress.StepIt;
  Application.ProcessMessages;
  infoMsg(rsInfoConnecting);

  //Benutzerführung
  if settings.showPrompts then
  begin
    logMessage(rsLogShutdown +
      sLineBreak + sLineBreak + rsInfoDontDisconnect, True,
      False, MB_ICONINFORMATION);

    sleepActive(settingConnectMessageDelay);
    flashNotice(True, rsNoticeRestart, 500);
    sleepActive(settingConnectMessageDelay);
  end
  else
  begin
    flashNotice(True, rsNoticeStartBoot, 500);
    sleepActive(settingConnectMessageDelay);
  end;

  //Verbindung herstellen
  pgbUpdateProgress.StepIt;
  Application.ProcessMessages;

  try
    wireNum := routines.connect(settings.connectTries, pgbProgramConnect);
  except
    on E: Exception do
    begin
      logMsgFmt(rsLogConnecting + sLineBreak + '%s' +
        sLineBreak + sLineBreak + rsInfoCanceling,
        [E.Message], True, False, MB_ICONSTOP);
      logMessage(E.Message);
      infoMsg(E.Message);
      Exit;
    end;
  end;

  //Exceptions wieder aktivieren: Timeout muss Exception auslösen
  connection.RaiseExcept := True;

  if wireNum = UNKNOWNWIRE then
  begin
    logMessage(rsLogConnecting + sLineBreak + rsErrNoMode +
      sLineBreak + sLineBreak + rsInfoCanceling,
      True, False, MB_ICONSTOP);
    logMessage(rsLogConnectNoWire);
    infoMsg(rsLogConnectNoWire);
    Exit;
  end;

  //Keine 1-Draht Verbindung unterstützt: Fest auf 2-Draht setzen/überschreiben
  if not settings.detectWireNum then
    routines.wireCount := TWOWIRE;

  if routines.wireCount = ONEWIRE then
  begin
    if settings.connectTries <> 1 then
      infoMsgFmt(rsInfoConnect1Wp, [settings.connectTries])
    else
      infoMsg(rsInfoConnect1Ws);
  end
  else if routines.wireCount = TWOWIRE then
  begin
    if settings.connectTries <> 1 then
      infoMsgFmt(rsInfoConnect2Wp, [settings.connectTries])
    else
      infoMsg(rsInfoConnect2Ws);
  end;

  if (not settings.detectWireNum) and (wireNum = ONEWIRE) then
    raise Exception.Create(rsErr1WDisabled);

{------------------------------------------------------------------------------}
  //Unterstütze Funktionen ermitteln
  pgbUpdateProgress.StepIt;
  Application.ProcessMessages;
  infoMsg(rsInfoRetrieveCapabilities);

  //CRC-Unterstützung
  supportsCRC := routines.detectCRCSupport;
  routines.resetCRC;

  if supportsCRC then
    infoMsg(rsInfoCRCAvailable)
  else
    infoMsg(rsInfoCRCUnsupported);

  //Verify-Unterstützung ermitteln
  supportsVerify := routines.detectVerifySupport;
  if supportsVerify then
    infoMsg(rsInfoVerifyAvailable)
  else
    infoMsg(rsInfoVerifyUnsupported);

  //Im Paketmodus muss ein aktives "Verify" immer ausgeführt werden
  if (settings.programVerify) and (not supportsVerify) and paketMode then
  begin
    logMessage(rsPaketErrVerifyRequired +
      sLineBreak + sLineBreak + rsInfoCanceling,
      True, False, MB_ICONSTOP);
    infoMsg(rsPaketErrVerifyRequired);
    Exit;
  end;

{------------------------------------------------------------------------------}
  //Informationen auslesen
  pgbUpdateProgress.StepIt;
  Application.ProcessMessages;
  infoMsg(rsInfoRetrieveConfig);

  try
    //Bootloader-Version
    deviceRevision := routines.readRevision;
    if deviceRevision <> TBootloaderRoutines.getBootVersion then
    begin
      raise Exception.CreateFmt(rsErrUnsupportedVersion +
        sLineBreak + rsInfoUnsupportedVersion, [deviceRevision,
          TBootloaderRoutines.getBootVersion]);
    end;

    //Signatur
    deviceSignature := routines.readSignature;
    deviceName := routines.getDeviceName(deviceSignature);

    if deviceName = defaultDeviceName then
    begin
      if settings.showPrompts then
      begin
        reply := Application.MessageBox(PChar(
          rsInfoUnknownSignature +
          sLineBreak + rsConfirmUnknownSignature
          + sLineBreak + sLineBreak + rsConfirmForceUpdate),
          'UpdateLoader', MB_ICONWARNING or MB_YESNO or MB_DEFBUTTON2);

        if reply = idNo then
          Exit;
      end
      else
        logMessage(rsLogUnknownSignature);
    end;

    //Notiz Bootloader-Version und Chip-Signatur
    infoMsgFmt(rsInfoSignature, [deviceSignature]);
    infoMsgFmt(rsInfoDevice, [deviceRevision, deviceName]);

    //Flash-Größe
    deviceFlashSize := routines.readFlashSize;

    if deviceFlashSize > self.firmwareBuffer.Max then
    begin
      raise Exception.CreateFmt(rsErrFlashOversize +
        sLineBreak + rsInfoFlashOversize,
        [convertKiB2String(deviceFlashSize), convertKiB2String(self.firmwareBuffer.Max)]);
    end;

    if deviceFlashSize < self.firmwareBuffer.Used then
    begin
      raise Exception.CreateFmt(rsErrFlashUndersize +
        sLineBreak + rsInfoFlashUndersize, [deviceFlashSize,
          self.firmwareBuffer.Used]);
    end;

    //Puffer-Größe
    deviceWriteBuffer := routines.readBufferSize;

    if deviceWriteBuffer < settingMinWriteBuffer then
    begin
      raise Exception.CreateFmt(rsErrTxBufferUndersize + sLineBreak +
        rsInfoTxBufferUndersize, [deviceWriteBuffer, settingMinWriteBuffer]);
    end
    else if deviceWriteBuffer < settingSmallWriteBuffer then
      logMessage(rsInfoTxSmallBuffer);

    //Notiz Flash-Größe und Sendepuffer
    infoMsgFmt(rsInfoFlashSize, [convertKiB2String(deviceFlashSize),
      deviceWriteBuffer]);
  except
    on E: Exception do
    begin
      logMessage(rsLogReadSettings + sLineBreak + E.Message +
        sLineBreak + sLineBreak + rsInfoCanceling,
        True, False, MB_ICONSTOP);
      logMessage(E.Message);
      infoMsg(E.Message);
      Exit;
    end;
  end;

{------------------------------------------------------------------------------}
  //CRC Prüfung
  if supportsCRC then
  begin
    pgbUpdateProgress.StepIt;
    Application.ProcessMessages;
    infoMsg(rsCRCInfoInit);

    try
      if not routines.checkCRC then
        raise Exception.Create(rsCRCErrFailed)
      else
        infoMsg(rsCRCInfoSuccess);
    except
      on E: Exception do
      begin
        logMsgFmt(rsCRCLogSum + sLineBreak + '%s' + sLineBreak + sLineBreak +
          rsInfoCanceling,
          [E.Message], True, False, MB_ICONSTOP);
        logMessage(E.Message);
        infoMsg(E.Message);
        Exit;
      end;
    end;
  end
  else
    pgbUpdateProgress.Max := pgbUpdateProgress.Max - pgbUpdateProgress.Step;

{------------------------------------------------------------------------------}
  //Programm aufspielen
  flashNotice(True, rsNoticeRunning, 1000);

  //Programm aufspielen
  if settings.programWrite then
  begin
    pgbUpdateProgress.StepIt;
    Application.ProcessMessages;

    infoMsgFmt(rsUploadInfoProgress, [convertKiB2String(firmwareBuffer.Used),
      convertKiB2String(deviceFlashSize), (firmwareBuffer.Used / deviceFlashSize
      ) * 100]);

    try
      if not routines.writeFirmware(deviceWriteBuffer, pgbProgramWrite) then
        raise Exception.Create(rsUploadErrFailed)
      else
        infoMsg(rsUploadInfoSuccess);
    except
      on E: Exception do
      begin
        logMsgFmt(rsUploadLogProgress +
          sLineBreak + '%s' + sLineBreak + sLineBreak +
          rsInfoCanceling + sLineBreak +
          rsInfoCorruption,
          [routines.lastAddress, E.Message],True, False, MB_ICONSTOP);
        logMsgFmt('%s (0x%.6x)', [E.Message, routines.lastAddress]);
        infoMsgFmt('%s (0x%.6x)', [E.Message, routines.lastAddress]);
        Exit;
      end;
    end;

    pgbUpdateProgress.StepIt;
  end
  else
    pgbUpdateProgress.Max := pgbUpdateProgress.Max - pgbUpdateProgress.Step;

{------------------------------------------------------------------------------}
  //Programm prüfen

  //Abfrage auf optionales Überprüfen (nicht im Paketmodus)
  if settings.showPrompts and (not settings.programVerify) and supportsVerify and (not paketMode) then
  begin
    reply := Application.MessageBox(PChar(rsInfoVerifyRecommend1 + sLineBreak +
      rsInfoVerifyRecommend2 + sLineBreak + sLineBreak + rsConfirmVerify),
      'UpdateLoader', MB_ICONQUESTION or MB_YESNO or MB_DEFBUTTON1);

    if reply = idYes then
      settings.programVerify := True;
  end;

  if settings.programVerify and supportsVerify then
  begin
    pgbUpdateProgress.StepIt;
    Application.ProcessMessages;
    infoMsg(rsVerifyInfoStart);

    try
      if not routines.verifyFirmware(pgbProgramVerify) then
        raise Exception.Create(rsVerifyErrFailed)
      else
        infoMsg(rsVerifyInfoSuccess);
    except
      on E: Exception do
      begin
        logMsgFmt(rsVerifyLogProgress + sLineBreak +
          '%s' + sLineBreak + sLineBreak + rsInfoCanceling,
          [routines.lastAddress, E.Message], True, False, MB_ICONSTOP);
        logMsgFmt('%s (0x%.6x)', [E.Message, routines.lastAddress]);
        infoMsgFmt('%s (0x%.6x)', [E.Message, routines.lastAddress]);
        Exit;
      end;
    end;

    pgbUpdateProgress.StepIt;
  end
  else
    pgbUpdateProgress.Max := pgbUpdateProgress.Max - pgbUpdateProgress.Step;

{------------------------------------------------------------------------------}
  //CRC Prüfung
  if supportsCRC then
  begin
    pgbUpdateProgress.StepIt;
    Application.ProcessMessages;
    infoMsg(rsCRCInfoStart);

    try
      if not routines.checkCRC then
        raise Exception.Create(rsCRCErrFailed)
      else
        infoMsg(rsCRCInfoSuccess);
    except
      on E: Exception do
      begin
        logMsgFmt(rsCRCLogSum + sLineBreak + '%s' + sLineBreak + sLineBreak +
          rsInfoCanceling,
          [E.Message], True, False, MB_ICONSTOP);
        logMessage(E.Message);
        infoMsg(E.Message);
        Exit;
      end;
    end;
  end
  else
    pgbUpdateProgress.Max := pgbUpdateProgress.Max - pgbUpdateProgress.Step;

{------------------------------------------------------------------------------}
  //Programm starten
  if settings.programStart then
  begin
    pgbUpdateProgress.StepIt;
    Application.ProcessMessages;
    infoMsg(rsRebootInfoStart);

    if not routines.startFirmware then
    begin
      if settings.showPrompts then
      begin
        logMessage(rsRebootErrCommand
          + sLineBreak + sLineBreak +
          rsRebootInfoCommand,
          True, False, MB_ICONINFORMATION);
        infoMsg(rsRebootInfoFail);
      end
      else
        infoMsg(rsRebootInfoFail);
    end
    else
      infoMsg(rsRebootInfoSuccess);
  end
  else
    pgbUpdateProgress.Max := pgbUpdateProgress.Max - pgbUpdateProgress.Step;


{------------------------------------------------------------------------------}
  pgbUpdateProgress.Position := pgbUpdateProgress.Max;
  Application.ProcessMessages;
  infoMsg(rsLogUpdateSuccess);

  sleepActive(500);

  Result := True;
end;
{$ENDREGION}

end.
