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

UpdateLoader - Protokollinformationen und Standardeinstellungen

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 protocol;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ComCtrls,
  ExtCtrls, Buttons, LCLType, LCLIntf, Tools, SynaSer;

resourcestring
  rsDefaultDevice = 'Unbekannt';
  rsDefaultPassword = 'Peda';
  rsErrBusy = 'Verbindungsaufbau durch laufende Kommunikation blockiert';
  rsErrInvalidRecord = 'Ungültiger Datensatz empfangen (%s)';
  rsErrInvalidResponse = 'Ungültige Antwort des Bootloaders (0x%.2x) ';
  rsErrNoAck = 'Abschluss der Übertragung nicht bestätigt';
  rsErrRxInvalid = 'Ungültige Daten empfangen';
  rsErrSyncFail = 'Verbindung aufgebaut, Synchronisation fehlgeschlagen';
  rsErrTimeoutSync = 'Zeitüberschreitung nach Verbindungsaufbau, Synchronisation fehlgeschlagen';

const  //Standardwerte für GUI Felder
  defaultConnectTries: integer = 250;
  defaultBootloaderPassword: string = rsDefaultPassword;
  defaultDeviceName: string = rsDefaultDevice;
  defaultBaudrate: integer = 115200;
  defaultDetectOneWire: boolean = false;
  defaultToggleDTR: boolean = true;

const  //Einstellungen Programmablauf
  settingPortSetupDelay: integer = 500; //Wartezeit beim Öffnen des COM-Ports (ms)
  settingConnectMessageDelay: integer = 150; //Wartezeit zwischen GUI-Anzeigen (ms)

  settingMaxBufferSize: longint = $40000; //256KiB (262144 Byte)
  settingMinWriteBuffer: integer = 8; //Minimaler Sendepuffer
  settingSmallWriteBuffer: integer = 128; //Warnmeldung "langsame Übertragung"
  settingMaxAutoShowBuffer: longint = 10240; //Puffer über 10kb nicht mehr automatisch anzeigen (für flüssige Bedienung)

type
  TBootloaderRoutines = class
    public
      class function getBootVersion: string;

    private
      const
        supportedBootVer: string = '2.1';  //Unterstützte Bootloader Version

      const  //Einstellungen für Datenübertragung
        settingConnectTryTimeout: integer = 25; //Wartezeit auf Antwort beim Verbinden (ms)
        settingResetToggleDelay: integer = 50; //Wartezeit zur Reset-Auslösung über DTR (ms)
        settingReadTimeout: integer = 250; //Allgemeine Wartezeit auf serielle Daten (ms)
        settingReadLongTimeout: integer = 750; //Wartezeit auf "Continue" beim Flashen, "Success" beim Verbinden (ms)
        settingConnectLoopTimeout: integer = 100; //Maximale Laufzeit der Empfangsschleife während Verbindungsaufbau (ms)
        settingSyncDelay: integer = 100; //Wartezeit vor Synchronisations-Byte nach Passwort
        settingSyncLoopTimeout: integer = 3000; //Maximale Laufzeit Synchronisation nach Passwort (ms)
        settingVerifySectionLength: integer = 512; //Abschnittlänge

      const  //Übersetzungstabelle Signatur -> Name
        deviceSignatures: array[0..29] of string = (
           '1E 90 07:ATtiny13',
           '1E 91 0A:ATtiny2313',
           '1E 92 05:ATmega48',
           '1E 92 06:ATtiny45',
           '1E 92 07:ATtiny44',
           '1E 92 08:ATtiny461',
           '1E 93 06:ATmega8515',
           '1E 93 07:ATmega8',
           '1E 93 08:ATmega8535',
           '1E 93 0A:ATmega88',
           '1E 93 0B:ATtiny85',
           '1E 93 0C:ATtiny84',
           '1E 93 0D:ATtiny861',
           '1E 93 0F:ATmega88P',
           '1E 94 03:ATmega16',
           '1E 94 04:ATmega162',
           '1E 94 06:ATmega168',
           '1E 94 0B:ATmega168P',
           '1E 95 01:ATmega323',
           '1E 95 02:ATmega32',
           '1E 95 14:ATmega328P',
           '1E 95 0F:ATmega328P',
           '1E 95 16:ATmega328PB',
           '1E 96 02:ATmega64',
           '1E 96 09:ATmega644',
           '1E 98 02:ATmega2561',
           '1E 94 15:Atmega168PB',
           '1E A6 02:ATmega64RFR2',
           '1E A7 02:ATmega128RFR2',
           '1E A8 02:ATmega256RFR2');

      const  //Bootloader-Kommunikation (siehe FBoot Quellen)
        blcIdentifierCommand: byte = $A5;
        blcIdentifierEscape: byte = $A5;
        blcIdentifierAnswer: byte = $A8;
        blcIdentifierContinue: byte = $A9;

        blcReadRevision: byte = $0;
        blcReadBufferSize: byte = $1;
        blcReadSignature: byte = $2;
        blcReadFlashSize: byte = $3;

        blcCommandProgramWrite: byte = $4;
        blcCommandProgramStart: byte = $5;
        blcCommandProgramCheckCRC: byte = $6;
        blcCommandProgramVerify: byte = $7;
        blcCommandProgramFinish: byte = $80;

        blcStatusConnected: byte = $A6;
        blcStatusBadCommand: byte = $A7;
        blcStatusSuccess: byte = $AA;
        blcStatusFail: byte = $AB;

        blcDataIllegalChar: byte = $13;
        blcDataIllegalCharShift: byte = $80;
        blcDataAutobaudLeader: byte = $0D; //Steuerzeichen Baudraten-Erkennung, einmalig vor Passwort
        blcDataPasswordTrailer: byte = $FF;

      var
        serialHandle: TBlockSerial;
        firmwareBuffer: TMemoryBuffer;
        wireNum: TWireCount;

        bootKey: AnsiString;
        firmwareKey: AnsiString;

        sendHWReset: boolean;
        fwKeyMode: TFWKeyMode;
        invertCom: boolean;

        movingTxCRC: word;
        currentAddress: longint;

      procedure setBootKey(key: AnsiString);

      procedure comSendByte(data: byte);
      procedure comSendString(data: AnsiString);
      function comReceiveByte(timeout: integer): byte;

      procedure updateTxCRC(data: byte);
      procedure dropLoopback(count: integer);

      function detectSupport(command: byte): boolean;
      function readInfo(command: integer): TByteList;
      function uploadData(command: byte; waitContinue: boolean; sectionLength: longint; progressBar: TProgressBar = nil): boolean;

      function escapeBufferSection(var offset: longint; byteCount: longint): AnsiString;

    public
      constructor Create; overload;
      constructor Create(serialConnection: TBlockSerial); overload;
      constructor Create(firmware: TMemoryBuffer); overload;
      destructor Destroy; override;

      function connect(var connectTries: integer; progressBar: TProgressBar = nil): TWireCount;
      function writeFirmware(sectionLength: longint; progressBar: TProgressBar = nil): boolean;
      function verifyFirmware(progressBar: TProgressBar = nil): boolean;
      function startFirmware: boolean;

      procedure resetCRC;
      function checkCRC: boolean;

      function detectVerifySupport: boolean;
      function detectCRCSupport: boolean;

      function readRevision: string;
      function readSignature: string;
      function readBufferSize: integer;
      function readFlashSize: longint;

      function getDeviceName(signature: string): string;

      property serialConnection: TBlockSerial read serialHandle write serialHandle;
      property firmware: TMemoryBuffer read firmwareBuffer write firmwareBuffer;
      property bootloaderPassword: AnsiString read bootKey write setBootKey;
      property firmwarePassword: AnsiString read firmwareKey write firmwareKey;
      property firmwarePasswordMode: TFWKeyMode read fwKeyMode write fwKeyMode;
      property wireCount: TWireCount read wireNum write wireNum;
      property toggleDTR: boolean read sendHWReset write sendHWReset;
      property lastAddress: longint read currentAddress;
      property invertConnection: boolean read invertCom write invertCom;
  end;

implementation

uses DateUtils, i18n;

{$REGION 'Initialisierung'}
constructor TBootloaderRoutines.Create;
begin
  inherited;

  serialHandle := nil;
  firmwareBuffer := nil;
  wireNum := UNKNOWNWIRE;

  bootKey := defaultBootloaderPassword;
  firmwareKey := '';

  sendHWReset := false;
  fwKeyMode := NOKEY;
  invertCom := false;

  movingTxCRC := 0;
  currentAddress := 0;
end;

constructor TBootloaderRoutines.Create(serialConnection: TBlockSerial);
begin
  self.Create;
  self.serialHandle := serialConnection;
end;

constructor TBootloaderRoutines.Create(firmware: TMemoryBuffer);
begin
  self.Create;
  self.firmwareBuffer := firmware;
end;

destructor TBootloaderRoutines.Destroy;
begin
  inherited;
end;

class function TBootloaderRoutines.getBootVersion: string;
begin
  Result := self.supportedBootVer;
end;

procedure TBootloaderRoutines.setBootKey(key: AnsiString);
begin
  if key = '' then
    raise Exception.Create(rsErrPasswordEmpty);

  //Steuerzeichen "Connected" und "Success" dürfen nicht im Passwort vorkommen
  //TODO: Fix für Unicode!!!
  if (pos(AnsiChar(blcStatusConnected), key) <> 0) or (pos(AnsiChar(blcStatusSuccess), key) <> 0) then
    raise Exception.Create(rsErrPasswordPolicy);

  self.bootKey := key;
end;

{$ENDREGION}

{$REGION 'Hauptfunktionen (Übertragen, Verbinden)'}
function TBootloaderRoutines.connect(var connectTries: integer; progressBar: TProgressBar = nil): TWireCount;
var
  loopbackDetect: byte;
  connected: boolean;
  detectedWireNum: TWireCount;
  connectTry: integer;
  reply: byte;
  loopTimeout: boolean;
  startTime: TDateTime;
begin
  assert(assigned(self.serialHandle), 'serialHandle not assigned');
  assert(connectTries > 0, 'connectTries = 0');

  if bootloaderPassword = '' then
    raise Exception.Create(rsErrPasswordEmpty);

  connected := false;
  detectedWireNum := UNKNOWNWIRE;
  loopbackDetect := ord(self.bootKey[1]); //Erstes Zeichen im Passwort (Loopback-Erkennung bei Eindrahtverbindung)

  //Exceptions deaktivieren: Auswertung erfolgt auf Protokollebene
  serialConnection.RaiseExcept := False;

  if Assigned(progressBar) then
  begin
    progressBar.Position := 0;
    progressBar.Max := connectTries;
    progressBar.Step := 1;
  end;

  //Reset-Pin Ansteuerung über DTR
  //Fallende Flanke am DTR-Pin erzeugen (Reset auslösen), Pin danach auf High-Pegel halten
  //Pin gesetzt (true) = Low-Pegel!
  if toggleDTR then
  begin
    serialConnection.DTR := false;
    Sleep(settingResetToggleDelay);
    serialConnection.DTR := true;
    Sleep(settingResetToggleDelay);
    serialConnection.DTR := false;
  end;

  //Firmware Passwort vor Verbindungsaufbau senden
  if self.fwKeyMode = KEYONCE then
    comSendString(self.firmwareKey);

  //Puffer leeren
  serialConnection.Purge;
  serialConnection.Flush;

  reply := 0;
  loopTimeout := false;

  for connectTry := 0 to connectTries do
  begin
    //Firmware Passwort alle 10 Verbindungsversuche senden
    if (self.fwKeyMode = KEYTENTH) and ((connectTry mod 10) = 0) then
    begin
      comSendString(self.firmwareKey);

      if detectedWireNum = ONEWIRE then
        dropLoopback(Length(self.firmwareKey));
    end;

{$IF Declared(blcDataAutobaudLeader)}
    //0x0D zur Baudraten-Erkennung senden
    comSendByte(blcDataAutobaudLeader);
{$ENDIF}

    //Passwort + 0xFF senden
    comSendString(bootloaderPassword);
    comSendByte(blcDataPasswordTrailer);

    try
      //Gesamten empfangenen Puffer durchlaufen
      //Schleife mit Timeout, permante Übertragungen blockieren die Empfangsschleife
      startTime := TDateTime(now);
      repeat
        reply := comReceiveByte(settingConnectTryTimeout div 2);

        if (reply = loopbackDetect) and (detectedWireNum = UNKNOWNWIRE) {and (detectWireCount)} then    //TODO: Immer erkennen?!
           detectedWireNum := ONEWIRE;
        if reply = blcStatusConnected then
           connected := true;

        if MilliSecondsBetween(now, startTime) > settingConnectLoopTimeout then
        begin
          loopTimeout := true;
          Break;
        end;
      until not serialConnection.CanReadEx(settingConnectTryTimeout div 2);
    except
    end;

    if connected then
      Break;

    if Assigned(progressBar) then
      progressBar.StepIt;
    Application.ProcessMessages;
  end;

  //Genutzte Anzahl Verbindungsversuche zurückmelden
  connectTries := succ(connectTry);

  //Puffer leeren
  serialConnection.Purge;
  serialConnection.Flush;

  //Passwort wurde akzeptiert, Umschaltung in Kommandomodus:
  //Nach dem Empfang von blcStatusSuccess ist die Verbindung synchron
  if connected then
  begin
    //keine 1-Draht Verbindung oder Erkennung deaktiviert -> 2-Draht
    //if (not detectWireCount) or (detectWireCount and (detectedWireNum <> ONEWIRE)) then
    if detectedWireNum <> ONEWIRE then        //TODO: Immer erkennen?!
      detectedWireNum := TWOWIRE;

    //Verbindungsmodus speichern
    self.wireNum := detectedWireNum;

    //0xA5 senden, Bootloader antwortet mit Status "Success"
    //Nach dem Verbindungsaufbau wird das Sychronisationsbyte verzögert
    //gesendet um es eindeutig vom Passwort zu trennen
    Sleep(settingSyncDelay);
    comSendByte(blcIdentifierCommand);

    reply := 0;
    loopTimeout := false;

    try
      //Gesamten empfangenen Puffer durchlaufen, 1-Wire Daten werden ignoriert
      //Schleife mit Timeout, permante Übertragungen blockieren die Empfangsschleife
      startTime := TDateTime(now);
      repeat
        reply := serialConnection.RecvByte(settingReadTimeout);

        if MilliSecondsBetween(now, startTime) > settingSyncLoopTimeout then
        begin
          loopTimeout := true;
          Break;
        end;
      until reply = blcStatusSuccess;
    except
    end;

    //Puffer leeren
    serialConnection.Purge;
    serialConnection.Flush;

    if loopTimeout then
      raise Exception.Create(rsErrTimeoutSync)
    else if reply <> blcStatusSuccess then
      raise Exception.CreateFmt(rsErrInvalidResponse + sLineBreak +
        rsErrSyncFail, [reply]);
  end
  else
  begin
    detectedWireNum := UNKNOWNWIRE;

    if loopTimeout then
      raise Exception.Create(rsTimeout + sLineBreak + rsErrBusy)
    else
      raise Exception.Create(rsTimeout);
  end;

  Result := detectedWireNum;

  if Assigned(progressBar) then
    progressBar.Position := progressBar.Max;
  Application.ProcessMessages;
end;

function TBootloaderRoutines.writeFirmware(sectionLength: longint; progressBar: TProgressBar = nil): boolean;
begin
  assert(assigned(firmwareBuffer), 'firmwareBuffer not assigned');

  Result := self.uploadData(blcCommandProgramWrite, true, sectionLength, progressBar);
end;

function TBootloaderRoutines.verifyFirmware(progressBar: TProgressBar = nil): boolean;
begin
  assert(assigned(firmwareBuffer), 'firmwareBuffer not assigned');

  Result := self.uploadData(blcCommandProgramVerify, false, settingVerifySectionLength, progressBar);
end;

function TBootloaderRoutines.startFirmware: boolean;
begin
  Result := true;

  try
    comSendByte(blcIdentifierCommand);
    comSendByte(blcCommandProgramStart);

    dropLoopback(2);

    try
      if comReceiveByte(settingReadTimeout) = blcStatusBadCommand then
        Result := false;
    except
    end;

  finally
    serialConnection.Purge;
    serialConnection.Flush;
  end;
end;
{$ENDREGION}

{$REGION 'CRC-Prüfsumme'}
procedure TBootloaderRoutines.resetCRC;
begin
  self.movingTxCRC := 0;
end;

function TBootloaderRoutines.checkCRC: boolean;
var
  reply: byte;
begin
  Result := false;

  serialConnection.RaiseExcept := True;

  try
    comSendByte(blcIdentifierCommand);
    comSendByte(blcCommandProgramCheckCRC);
    comSendString(AnsiChar(lo(self.movingTxCRC)) + AnsiChar(hi(self.movingTxCRC)));

    dropLoopback(4);

    try
      reply := comReceiveByte(settingReadTimeout);
    except
      raise Exception.Create(rsTimeout);
    end;

  finally
    serialConnection.Purge;
    serialConnection.Flush;
  end;

  if not (reply in [blcStatusSuccess, blcStatusFail]) then
    raise Exception.Create(rsErrNoAck);

  if reply = blcStatusSuccess then
    Result := true;
end;
{$ENDREGION}

{$REGION 'Informationen auslesen'}
function TBootloaderRoutines.detectVerifySupport: boolean;
begin
  Result := detectSupport(blcCommandProgramVerify);
end;

function TBootloaderRoutines.detectCRCSupport: boolean;
begin
  Result := detectSupport(blcCommandProgramCheckCRC);
end;

function TBootloaderRoutines.readRevision: string;
var
  revStr: string;
begin
  revStr := convertByteList2String(readInfo(blcReadRevision));

  if Length(revStr) = 2 then
    revStr := revStr[1] + '.' + revStr[2]
  else
    raise Exception.CreateFmt(rsErrInvalidRecord + sLineBreak + '"%s"', [rsRevision, revStr]);

  Result := revStr;
end;

function TBootloaderRoutines.readSignature: string;
begin
  Result := convertByteList2HexString(readInfo(blcReadSignature), true);
end;

function TBootloaderRoutines.readBufferSize: integer;
var
  readBytes: TByteList;
begin
  Result := 0;

  readBytes := readInfo(blcReadBufferSize);

  if High(readBytes) = 1 then
    Result := readBytes[1] + (readBytes[0] * 256)
  else
    raise Exception.CreateFmt(rsErrInvalidRecord, [rsTxBufferSize]);
end;

function TBootloaderRoutines.readFlashSize: longint;
var
  readBytes: TByteList;
begin
  Result := 0;

  readBytes := readInfo(blcReadFlashSize);

  if High(readBytes) = 2 then
    Result := readBytes[2] + (readBytes[1] * 256) + (readBytes[0] * 65536)
  else
    raise Exception.CreateFmt(rsErrInvalidRecord, [rsFlashSize]);
end;

function TBootloaderRoutines.getDeviceName(signature: string): string;
var
  dbPair: string;
begin
  Result := defaultDeviceName;

  for dbPair in deviceSignatures do
  begin
    if signature = copy(dbPair, 1, 8) then  //Erste 8 Zeichen: Signatur
    begin
      Result := copy(dbPair, 10, length(dbPair));
      Break;
    end;
  end;
end;
{$ENDREGION}

{$REGION 'Private Funktionen'}
{$REGION 'Bitübertragung'}
procedure TBootloaderRoutines.comSendByte(data: byte);
begin
  assert(assigned(self.serialHandle), 'serialHandle not assigned');

  updateTxCRC(data);

  if self.invertCom then
    invertByte(data);
  serialHandle.SendByte(data);
end;

procedure TBootloaderRoutines.comSendString(data: AnsiString);
var
  aC: AnsiChar;
  send: AnsiString;
begin
  assert(assigned(self.serialHandle), 'serialHandle not assigned');

  send := '';

  for aC in data do
  begin
    updateTxCRC(ord(aC));

    if self.invertCom then
      invertByte(ord(aC));
    send := send + aC;
  end;

  serialHandle.SendString(send);
end;

function TBootloaderRoutines.comReceiveByte(timeout: integer): byte;
var
  data: byte;
begin
  assert(assigned(self.serialHandle), 'serialHandle not assigned');

  data := serialHandle.RecvByte(timeout);
  if self.invertCom then
    invertByte(data);

  Result := data;
end;

procedure TBootloaderRoutines.updateTxCRC(data: byte);
var
  count: integer;
begin
  for count := 0 to 7 do
  begin
    if ((data and $01) xor (self.movingTxCRC and $0001) <> 0) then
    begin
      self.movingTxCRC := self.movingTxCRC shr 1;
      self.movingTxCRC := self.movingTxCRC xor $A001;
    end
    else
      self.movingTxCRC := self.movingTxCRC shr 1;

    data := data shr 1;
  end;
end;

procedure TBootloaderRoutines.dropLoopback(count: integer);
begin
  assert(assigned(self.serialHandle), 'serialHandle not assigned');

  if self.wireNum <> ONEWIRE then
    exit;

  try
    serialConnection.RecvBufferStr(count, settingReadTimeout);
  except
  end;
end;
{$ENDREGION}

function TBootloaderRoutines.detectSupport(command: byte): boolean;
var
  reply: byte;
begin
  Result := false;

  try
    comSendByte(blcIdentifierCommand);
    comSendByte(command);

    dropLoopback(2);

    //Unbekannte Kommandos lehnt der Bootloader sofort ab
    //Bei einem gültigen Kommando läuft die Empfangsfunktion ins Timeout, der Bootloader
    //erwartet jetzt weitere Daten zum aktivierten Kommando
    try
      if comReceiveByte(settingReadTimeout) = blcStatusBadCommand then
        Exit;
    except
    end;

    //Kommando sofort wieder beenden, hier nur prüfen
    comSendByte(blcIdentifierCommand);
    comSendByte(blcCommandProgramFinish);

    dropLoopback(2);

    try
      reply := comReceiveByte(settingReadTimeout);
      if (reply in [blcStatusSuccess, blcStatusFail]) then
        Result := true;
    except
    end;

  finally
    serialConnection.Purge;
    serialConnection.Flush;
  end;
end;

function TBootloaderRoutines.readInfo(command: integer): TByteList;
var
  awLen: byte;
  awPos: byte;
  readAnswer: TByteList;
  readLeader: byte;
  readTrailer: byte;
begin
  //Daten anfordern
  comSendByte(blcIdentifierCommand);
  comSendByte(command);

  dropLoopback(2);

  serialConnection.RaiseExcept := True;

  try
    try
      readLeader := comReceiveByte(settingReadTimeout);
      awLen := comReceiveByte(settingReadTimeout);

      if awLen > 0 then
      begin
        //Länge abzüglich Bestätigungscode
        dec(awLen);
        SetLength(readAnswer, awLen);

        for awPos := 0 to pred(awLen) do
          readAnswer[awPos] := comReceiveByte(settingReadTimeout);
      end;

      readTrailer := comReceiveByte(settingReadTimeout);
    except
      raise Exception.Create(rsTimeout);
    end;

    if (readLeader <> blcIdentifierAnswer) or (readTrailer <> blcStatusSuccess) then
      raise Exception.Create(rsErrRxInvalid);
  finally
    serialConnection.Purge;
    serialConnection.Flush;
  end;

  Result := readAnswer;
end;

function TBootloaderRoutines.uploadData(command: byte; waitContinue: boolean; sectionLength: longint; progressBar: TProgressBar = nil): boolean;
var
  offset: longint;
  section: longint;
  sectionNum: longint;
  sectionData: AnsiString;
  bufferLen: longint;
  reply: byte;
begin
  assert(assigned(self.serialHandle), 'serialHandle not assigned');
  assert(assigned(self.firmwareBuffer), 'firmwareBuffer not assigned');
  assert(command <> 0, 'invalid upload command');

  section := 0;
  offset := self.firmwareBuffer.Min;
  bufferLen := self.firmwareBuffer.Used;

  //Anzahl zu übertragender Abschnitte bestimmen (+1 für Rest, wenn nicht genau teilbar)
  sectionNum := bufferLen div sectionLength;
  if (bufferLen - (sectionNum * sectionLength)) <> 0 then
    inc(sectionNum);

  if Assigned(progressBar) then
  begin
    progressBar.Position := 0;
    progressBar.Max := sectionNum;
    progressBar.Step := 1;
  end;

  //Programmieren starten
  comSendByte(blcIdentifierCommand);
  comSendByte(command);

  dropLoopback(2);

  serialConnection.RaiseExcept := True;

  try
    while section < sectionNum do
    begin
      //Datenblock laden (erhöht Offset um Anzahl geladener Byte)
      sectionData := self.escapeBufferSection(offset, sectionLength);
      self.currentAddress := offset;

      //Datenblock senden
      comSendString(sectionData);

      dropLoopback(Length(sectionData));

      //Abschnitt übertragen
      inc(section);

      //Nach der Übertragung aller Abschnitte prüfen, ob alle Daten gesendet wurden
      //Ja: Schleife beenden, Antwort wird nicht abgewartet
      //    Sonderfall: Wenn Firmware genau in Sendepuffer passt trotzdem ein "Continue" abwarten
      //Nein: Durch Escape-Zeichen stimmte die Berechnung nicht, noch einen Abschnitt senden
      if section = sectionNum then
      begin
        if offset < bufferLen then
          inc(sectionNum)
        else if (not waitContinue) or ((bufferLen mod sectionLength) <> 0) then
          Break;
      end;

      //Auf "Fortsetzen"-Kommando warten
      if waitContinue then
      begin
        try
          reply := comReceiveByte(settingReadLongTimeout);
        except
          raise Exception.Create(rsTimeout);
        end;

        if reply <> blcIdentifierContinue then
          raise Exception.Create(rsErrRxInvalid);
      end;

      if Assigned(progressBar) then
        progressBar.StepIt;
      Application.ProcessMessages;
    end;

    //Programmieren beenden
    comSendByte(blcIdentifierCommand);
    comSendByte(blcCommandProgramFinish);
    dropLoopback(2);

    try
      reply := comReceiveByte(settingReadLongTimeout);
    except
      raise Exception.Create(rsTimeout);
    end;

    if not (reply in [blcStatusSuccess, blcStatusFail]) then
      raise Exception.Create(rsErrNoAck);
  finally
    serialConnection.Purge;
    serialConnection.Flush;
  end;

  Result := (reply = blcStatusSuccess);

  if Assigned(progressBar) then
    progressBar.Position := progressBar.Max;
  Application.ProcessMessages;
end;

function TBootloaderRoutines.escapeBufferSection(var offset: longint; byteCount: longint): AnsiString;
var
  res: AnsiString;
begin
  assert(assigned(self.firmwareBuffer), 'firmwareBuffer not assigned');

  if (offset < 0) or (offset < self.firmwareBuffer.Min) then
    offset := self.firmwareBuffer.Min;
  if offset > self.firmwareBuffer.Used then
    offset := self.firmwareBuffer.Used;
  if (byteCount + offset) > self.firmwareBuffer.Used then
    byteCount := self.firmwareBuffer.Used - offset;
  if (byteCount <= 0) then
    Exit;

  res := '';

  while byteCount > 0 do
  begin
    if (self.firmwareBuffer.Memory[offset] in [blcDataIllegalChar, blcIdentifierEscape]) then
    begin
      res := res + AnsiChar(blcIdentifierEscape);
      res := res + AnsiChar(self.firmwareBuffer.Memory[offset] + blcDataIllegalCharShift);
    end
    else
      res := res + AnsiChar(self.firmwareBuffer.Memory[offset]);

    inc(offset);
    dec(byteCount);

    if offset > self.firmwareBuffer.TopWrite then
      Break;
  end;

  Result := res;
end;

{$ENDREGION}

end.

