AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Sprachen und Entwicklungsumgebungen Object-Pascal / Delphi-Language Delphi Serielle Kommunikation in eine Art queue auslagern
Thema durchsuchen
Ansicht
Themen-Optionen

Serielle Kommunikation in eine Art queue auslagern

Ein Thema von ERBITUX · begonnen am 7. Mär 2019 · letzter Beitrag vom 7. Mär 2019
Antwort Antwort
ERBITUX

Registriert seit: 7. Mär 2019
12 Beiträge
 
#1

Serielle Kommunikation in eine Art queue auslagern

  Alt 7. Mär 2019, 10:14
Delphi-Version: 10.2 Tokyo
Hallo,

ich will mich hier mal kurz vorstellen. Ich heiße Thomas, komme aus der Nähe von Braunschweig und bin noch Schüler (Klasse 10).
Ich bin noch ein ziemlicher Anfänger was Delphi angeht, habe jedoch viel Erfahrung mit Mikrocontrollern (STM und AVR)

Ich habe hier in den letzten Wochen viel nachgelesen und habe die ersten Schritte hinter mir.

Nun zu meinem Problem, für das ich keine Lösung finden konnte.

Für ein Schulprojekt haben wir eine "Maschine" gebaut, die Vektorgrafiken malen kann. Gesteuert wird das Ding von einem Arduino für den ich eine Art stark abgespeckten G-Code Interpreter geschrieben habe. Die Steuerung läuft auf einem Arduino UNO. Die Befehle werden über USB (virtuelle serielle Schnittstelle) an den Arduino geschickt. Das funktioniert einwandfrei. Ich habe nun ein Delphiprogramm geschrieben, welches eine HPGL Datei in seine Einzelteile zerlegt, in die entsprechenden Befehle umwandelt und an die Maschine schickt. Auch das funktioniert im Grunde ohne Probleme, aber...

Ich schicke die Befehle im gleichen Thread an die Maschine. Die Abarbeitung dauert unterschiedlich lange. Ich muss im Programm warten bis die Antwort von der Maschine kommt um dann auf der Obfläche die Position des Stiftes zu aktualisieren. Dann wird der nächste Befehl an die Maschine geschickt. Während dieser Wartezeit kann das Programm leider nichts anderes machen. Ich denke hier z.b an eine aufwendigere Visualisierung.

Ich dachte jetzt daran die serielle Kommunikation in einen Thread auszulagern. Dazu will ich die Befehle der Reihe nach an den Thread übergeben und dieser führt sie selbstständig aus und aktualisiert nach jedem Befehl das Hauptformular. Allerdings muss ich gestehen das ich keinen Schimmer habe wie ich das realisieren kann. Kann mir da vielleicht jemand helfen? Gibt es vielleicht irgendwo ein Tutorial oder so was?

Ich mache das Ganze übrigens mit Delphi 10.2 Community Edition und FMX.

Gruß Thomas
  Mit Zitat antworten Zitat
peterbelow

Registriert seit: 12. Jan 2019
Ort: Hessen
672 Beiträge
 
Delphi 11 Alexandria
 
#2

AW: Serielle Kommunikation in eine Art queue auslagern

  Alt 7. Mär 2019, 13:27
Thomas, Dein Ansatz (ein Thread mit einer Queue von abzuarbeitenden Kommandos) ist genau richtig für das Problem.

Zu beachten dabei ist folgendes:

* Die Queue muss thread-safe sein, da der UI-Thread Kommandos hineinschreibt und der Arbeitsthread sie herausholt.
* Es muss einen Mechanismus geben, der dem Thread mitteilt, das Arbeit in der Queue ist, wenn der UI-Thread etwas hinzufügt.
* Die Interaktion mit der seriellen Schnittstelle sollte vollständig innerhalb des Arbeitsthreads erfolgen.
* Der Arbeitsthread sollte kontrolliert beendet werden können, wenn das Programm die Kommunikation mit dem externen Partner beenden will.
* Alle Daten (Fortschritt, Fehler), die der Arbeitsthread an den UI-Thread schicken will müssen per Synchronize übermittelt werden, so dass die Daten im Kontext des UI-Threads verarbeitet werden.

Fangen wir also mal mit einer von TThread abgeleiteten Klasse an. Wir überschrieben die Execute-Methode, deren Kode innerhalb des sekundären Threads abgearbeitet werden soll.

Der generelle Aufbau (inklusive Fehlerbehandlung) sieht so aus:
Delphi-Quellcode:
procedure TSerialComThread.Execute;
begin
  try
    InitializeSerialPort;
    try
      while not Terminated do begin
        WaitForWork;
        while not Terminated and MoreWorkAvailable do
          ProcessOneCommand;
      end;
    finally
      CloseSerialPort;
    end;
  except
    On E: Exception do
      ReportError(MakeErrorMsg(E), ERROR_IS_FATAL);
  end;
end;
Der Plan ist, dass der Thread sofort nachdem er erzeugt wurde den seriellen Port öffnet und dann wartet, bis ein Kommando zu Bearbeitung ansteht. Wenn das der Fall ist arbeitet er alle Kommandos ab, die in der Queue stehen, bis die Queue leer ist, dann geht er wieder in den Wartezustand.

Jetzt müssen wir nur noch die ganzen Methoden implementieren, die in Execute aufgerufen werden. Als erstes brauchen wir natürlich eine dem Problem angemessene Queue-Klasse. Da deine Kommandos vermutlich in Strings abgelegt sind können wir mit einer TQueue<string> aus der mitgelieferten Unit von generics beginnen. Da die Queue aber thread-safe sein soll verpacken wir sie in eine eigene Klasse, die den Zugriff auf die interne Queue regelt.

Delphi-Quellcode:
type
  TCommandQueue = class
  strict private
    FCommands: TQueue<string>;
    FSignal: TSimpleEvent;
  strict protected
    property Commands: TQueue<string> read FCommands;
    property Signal: TSimpleEvent read FSignal;
  public
    constructor Create;
    destructor Destroy; override;
    function HasCommands: boolean;
    function Pop: string;
    procedure Push(const aCommand: string);
    procedure SetEvent;
    procedure WaitFor;
  end;
Die Push, Pop und HasCommands-Methoden müssen thread-safe sein, Push löst das Signal aus, auf das WaitFor (in einem anderen Thread) wartet. SetEvent erlaubt es, das Signal von aussen auszulösen, wenn der Thread beendet werden soll.

Die fertige Unit (völlig ungetestet!) sieht dann etwa wie folgt aus. Die TODO-Sachen sind dann dein Bier.

Delphi-Quellcode:

unit SerialComThreadU;

interface

uses
  System.Sysutils, System.Generics.Collections,
  System.Classes, System.SyncObjs;

type
  TCommandQueue = class
  strict private
    FCommands: TQueue<string>;
    FSignal: TSimpleEvent;
  strict protected
    property Commands: TQueue<string> read FCommands;
    property Signal: TSimpleEvent read FSignal;
  public
    constructor Create;
    destructor Destroy; override;
    function HasCommands: boolean;
    function Pop: string;
    procedure Push(const aCommand: string);
    procedure SetEvent;
    procedure WaitFor;
  end;

  TSerialComThread = class(TThread)
  strict private
    type
      TThreadErrorEvent = procedure (sender: TSerialComThread;
         const ErrMsg: string; isFatal: boolean);
      TThreadProgress = procedure (sender: TSerialComThread;
         const Command: string);
    var
    FCommands: TCommandQueue;
    FOnProgress: TThreadProgress;
    FOnThreadError: TThreadErrorEvent;
    procedure CloseSerialPort;
    procedure InitializeSerialPort;
    function MakeErrorMsg(aException: Exception): string;
    function MoreWorkAvailable: boolean;
    procedure ProcessOneCommand;
    procedure ReportError(const aErrorMsg: string;
      aIsFatalError: boolean = false);
    procedure WaitForWork;
  strict protected
    procedure DoProgress(const aCommand: string);
    procedure DoThreadError(const aErrorMsg: string; aIsFatalError: boolean =
        false);
    property Commands: TCommandQueue read FCommands;
  protected
    procedure Execute; override;
    procedure TerminatedSet; override;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AddCommand(const aCommand: string);
    property OnProgress: TThreadProgress read FOnProgress write FOnProgress;
    property OnThreadError: TThreadErrorEvent read FOnThreadError write
        FOnThreadError;
  end;



implementation

resourcestring
  SThreadErrorMask =
    '%s ist auf eine Ausnahme vom Type %s gelaufen.'+SLinebreak+'%s';

const
  ERROR_IS_FATAL = true;

{== TSerialComThread ==================================================}


constructor TSerialComThread.Create;
begin
  FCommands := TCommandQueue.Create();
  inherited Create(false);
end;

destructor TSerialComThread.Destroy;
begin
  FCommands.Free;
  inherited Destroy;
end;

procedure TSerialComThread.AddCommand(const aCommand: string);
begin
  Commands.Push(aCommand);
end;

procedure TSerialComThread.CloseSerialPort;
begin
  // TODO -cMM: TSerialComThread.CloseSerialPort implement
end;

procedure TSerialComThread.DoProgress(const aCommand: string);
begin
  if Assigned(FOnProgress) then
    Synchronize(
      procedure begin
        FOnProgress(Self, aCommand);
      end);
end;

procedure TSerialComThread.DoThreadError(const aErrorMsg: string;
    aIsFatalError: boolean = false);
begin
  if Assigned(FOnThreadError) then
    Synchronize(
      procedure begin
        FOnThreadError(Self, aErrorMsg, aIsFatalError);
      end);
end;

procedure TSerialComThread.Execute;
begin
  try
    InitializeSerialPort;
    try
      while not Terminated do begin
        WaitForWork;
        while not Terminated and MoreWorkAvailable do
          ProcessOneCommand;
      end;
    finally
      CloseSerialPort;
    end;
  except
    On E: Exception do
      ReportError(MakeErrorMsg(E), ERROR_IS_FATAL);
  end;
end;

procedure TSerialComThread.InitializeSerialPort;
begin
  // TODO -cMM: TSerialComThread.InitializeSerialPort implement
end;

function TSerialComThread.MakeErrorMsg(aException: Exception): string;
begin
  Result := Format(SThreadErrorMask,
    [Classname, aException.ClassName, aException.Message]);
end;

function TSerialComThread.MoreWorkAvailable: boolean;
begin
  Result := Commands.HasCommands;
end;

procedure TSerialComThread.ProcessOneCommand;
var
  LCommand: string;
begin
  LCommand := Commands.Pop;
  try
    // TODO : send command over the port
    DoProgress(LCommand);
  except
    on E: Exception do
      ReportError(MakeErrorMsg(E), not ERROR_IS_FATAL);
  end;
end;

procedure TSerialComThread.ReportError(const aErrorMsg: string; aIsFatalError:
    boolean = false);
begin
  DoThreadError(aErrorMsg, aIsFatalError);
end;

procedure TSerialComThread.TerminatedSet;
begin
  inherited;
  Commands.SetEvent;
end;

procedure TSerialComThread.WaitForWork;
begin
  Commands.Waitfor;
end;

{== TCommandQueue =====================================================}


constructor TCommandQueue.Create;
begin
  inherited Create;
  FCommands := TQueue<string>.Create();
  FSignal := TSimpleEvent.Create();
end;

destructor TCommandQueue.Destroy;
begin
  FreeAndNil(FSignal);
  FreeAndNil(FCommands);
  inherited Destroy;
end;

function TCommandQueue.HasCommands: boolean;
begin
  MonitorEnter(self);
  try
    Result:= Commands.Count > 0;
  finally
    MonitorExit(self);
  end;
end;

function TCommandQueue.Pop: string;
begin
  MonitorEnter(self);
  try
    Result := Commands.Dequeue;
  finally
    MonitorExit(self);
  end;
end;

procedure TCommandQueue.Push(const aCommand: string);
begin
  MonitorEnter(self);
  try
    Commands.Enqueue(aCommand);
    Signal.SetEvent;
  finally
    MonitorExit(self);
  end;
end;

procedure TCommandQueue.SetEvent;
begin
  Signal.SetEvent;
end;

procedure TCommandQueue.WaitFor;
begin
  Signal.WaitFor;
  Signal.ResetEvent;
end;

end.
Peter Below
  Mit Zitat antworten Zitat
matashen

Registriert seit: 29. Jan 2007
Ort: daheim
460 Beiträge
 
Delphi XE2 Enterprise
 
#3

AW: Serielle Kommunikation in eine Art queue auslagern

  Alt 7. Mär 2019, 13:44
Achtung etwas OT

also in der 10. Klasse waren für mich andere Dinge interessanter also hätte ich das in dem Alter vermutlich nicht hinbekommen.

also mal an beide ein Respekt unterstrichen mit einem Fetten WOW
Matthias
Das Leben ist eines der härtesten.
  Mit Zitat antworten Zitat
ERBITUX

Registriert seit: 7. Mär 2019
12 Beiträge
 
#4

AW: Serielle Kommunikation in eine Art queue auslagern

  Alt 7. Mär 2019, 16:18
@peterbelow

Wow... Vielen Dank. Das muss ich erst mal verdauen und verstehen.

Ich werde mich da am Wochenende einmal durch kämpfen. Vielen Dank noch einmal.

@matashen

Leider sind die Dinge nicht immer unbedingt so wie man es sich wünscht. Ich wäre froh, wenn ich mich mit anderen Dingen beschäftigen könnte. Manchmal wird man aber durch das Schicksal etwas ausgebremst. Trotzdem Danke.

Gruß Frank
  Mit Zitat antworten Zitat
mensch72

Registriert seit: 6. Feb 2008
838 Beiträge
 
#5

AW: Serielle Kommunikation in eine Art queue auslagern

  Alt 7. Mär 2019, 19:05
Nach dem "praktischen" Vorschlag, hier noch etwas sinnvolle Theorie:

Um sowas ganz allgemein zu beschreiben, bietet sich die Orientierung am OSI Sichtenmodel an. Wenn dir das noch nix sagt, da lohnt egal ob vorweg oder hinterher mal ein Blick ins INET.

Grob gesagt, beschreibst du hier folgendes:

Übertragungsschicht: "seriell" via USB
Protokollschicht: du bist der Master und sendest Daten(Komandos) an den Slave(die Maschine), und wenn fertig vom Slave eine Quittung empfangen wird (bzw. nach WorstCase nach Zeit X ein Timeout ist)
Komandoschicht: hier werden in Abhängigkeit vom aktuellem Status synchron die passenden Befehle bei Bedarf einzeln oder aus einer JobQueue via Protokollschicht gesendet/empfangen
Applikationsschicht: hier wird alles Initialisiert, die alle Stati visualisiert und es werden Einzelbefehle oder ganze Befehlsjobs asynchron der Komandoschicht übergeben

Das hier vorgeschlagene Delphikonzept fasst Protokollschicht und Komandoschicht quasi zu einer "EventDriven-Statemaschine" zusammen. Das ist so möglich und spart etwas Overhead im Konzept. Das OSI Modell besteht eh aus noch ein paar mehr Schichten und Zusammenfassung von Funktionalität ist durchaus üblich.

Um Sinn oder Unsinn dieser Standardvariante besser zu beurteilen, überlege dir folgendes:
- du willst wahlweise nicht USBseriell sondern auch per Ethernet TCPIP oder UDP mit deiner oder einer anderen Maschine kommunizieren
- du willst mit deinem Programm was HPGL einliest und den Bearbeitungsverlauf anzeigt, künftig auch weitere Maschinentypen(mit eben anderen Komandos für vergleichbare Funktionalität) bedienen

=> Dann helfen die die "Schichten" stets nur das zu ergänzen oder neu zu schreiben was fehlt, der Großteil deiner Software bleibt möglichst unangetastet gleich.

Es ist durchaus in Ordnung, wenn du sowas erstmal auf Funktion programmierst, nur spiele gedanklich einfach mal was wäre wenn... und über lege wie du wo mögliche Erweiterungen so einbauen könntest, das es keine unendlche "If/Then/Case" Orgie entsteht.
Es klinkt verrückt, aber nicht das einbauen einer zusätzlichen Variante wird (künftig) ein Problem, sondern das möglichst einfache optionale wieder weglassen einer solchen Erweiterung.



Programmierer für Datenbanken, GUIs, WEB, MiddleWare gibt es in zig Sprachen native wie Scripts wie Sand am Meer.
Leute mit technischem Verständnis die von Frontend-Programmierung bis LowLevel in Hardware nochmit einzenlen Bytes umgehen können, sind sehr rar!
=> Für 10.Klasse ist es toll, wenn dir HW nahe praktische Microcontroler&Embedded-Programmierung UND PC-GUI-Software via Standard-Interfaces und -Protokollen liegt.
Wenn du dazu noch etwas löten und Schaltpläne lesen kannst/lernst, stehen dir künftig sehr viele Türe offen... wünsche weiter viel Erfolg!

Geändert von mensch72 ( 7. Mär 2019 um 19:08 Uhr)
  Mit Zitat antworten Zitat
Antwort Antwort


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 22:09 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz