![]() |
Delphi-Version: 10.2 Tokyo
Serielle Kommunikation in eine Art queue auslagern
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 |
AW: Serielle Kommunikation in eine Art queue auslagern
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:
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.
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; 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:
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.
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 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. |
AW: Serielle Kommunikation in eine Art queue auslagern
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 |
AW: Serielle Kommunikation in eine Art queue auslagern
@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 |
AW: Serielle Kommunikation in eine Art queue auslagern
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! |
Alle Zeitangaben in WEZ +1. Es ist jetzt 11:36 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz