Laden...

SerialPort Kommunikation mit Controllino (Arduino): Senden bis Antwort okay ist

Erstellt von Elchi vor 7 Jahren Letzter Beitrag vor 7 Jahren 5.093 Views
E
Elchi Themenstarter:in
26 Beiträge seit 2016
vor 7 Jahren
SerialPort Kommunikation mit Controllino (Arduino): Senden bis Antwort okay ist

Halli hallo,

ihr habt mir jetzt schon mehrfachw super weiter geholfen und jetzt gerade komme ich mit der MSDN nicht weiter, also möchte ich mich nochmal an euch wenden, denn in anderen Forenbeiträgen habe ich auch noch nicht die passende Lösung / Tipp gefunden.

Unser Techniker-Abschlussprojekt geht in die finale Phase, daher stand die Verheiratung meines Hauptprogramms (C#) und das des Partners (Controllino -> Arduinobasis) an. Prinzipiell hatten wir das schon getestet, aber aktuell tauchen Probleme auf. Wir benötigen angesichts unseres Zeitplans (Abgabetermin etc.) keine ideale-multifunktions-Lösung, es soll einfach funktionieren und uns nicht so lange beschäftigen 😉

Hintergrund:
Wir schicken uns gegenseitig Daten, in der Regel frage ich sie vorher an. Nur der Not-Aus (Interruptroutine auf dem Controllino) wird direkt an den PC (serielle Schnittstelle) gesendet.

Theoretischer Ablauf (vereinfacht):
C# an Contro: sende "Befehl:"
Contro merkt sich "Befehl:"
C# an Contro: sende "1;"
Contro merkt sich "1;"
Contro verarbeitet
Contro an C#: sende "OK:Befehl:1;"

Der Controllino antwortet mir also immer mit einem "OK:" + meinem gesendeten Befel
Da der Controllino nur in seiner "Freizeit" den SerialPort liest, muss ich den Befehl durchaus 3-4 mal schicken. Das habe ich jetzt mit if-Schleifen etc. gelöst und das funktioniert auch gut.
4 Befehle + Antworten sind so in 1-2 Sekunden verarbeitet, was für uns akzeptabel ist. Ich befürchte zwar das das so nicht 100% sauber und ideal ist, aber es funktioniert erstmal.

Ich hatte Anfangs mit dem "DataRecieved"-Event experimentiert, aber das hat nicht geklappt. Ich bekam immer Fehlermeldungen mit dem Sinn "Textbox wird aus anderem Thread gefüllt -> nicht zulässig". Das habe ich nicht in den Griff bekommen und mir so meine eigene Abfrage gebastelt. Da wird es dann allerdings mit dem Not-Aus schwer, das muss ich noch abfragen.

Das jetzige Problem ist:
EIne Methode stellt ebenfalls Anfragen an den Controllino, wiederholt diese jedoch nicht in einer Scheife sondern wird jede Sekunde durch einen Timer aufgerufen (es handelt sich um Sensordaten die abgefragt werden müssen). Mal alle 3, mal alle 20 Sendezyklen versteht der Controllino den Befehl. Das geht so natürlich nicht.

Ich habe da jetzt eine Bitte an euch:
Falls ihr Lust und Zeit habt, schaut doch mal ins Programm rein und sagt mir was ihr denkt.
Link zur Dropbox: https://www.dropbox.com/sh/1wudtcerdr2y4jo/AAA-IEIXvufSsY6t-0uucsbAa?dl=0
Die Kommunikation ist in "Hauptprogramm.cs" in den Funktionen "cont..." realisiert.

Ich weiß ihr braucht mehr Infos etc aber damit kann ich erst Morgen wieder dienen. Vielleicht hat ja schon jemand Lust drauf rein zu schauen.
An dieser Stelle erstmal vielen lieben Dank und ganz liebe Grüße
Euer Elchi, der sich so langsam in C# und dem Programmieren allgemein zurecht findet und stolz auf sein Progrmam ist. Ja es ist nicht perfekt aber es läuft und ich habs selbst gemacht 😃

187 Beiträge seit 2009
vor 7 Jahren

Eine Offtopic-Frage von mir weil es mich einfach interessiert. Hast Du den Controllino mit der Arduino-IDE programmiert oder vielleicht sogar mit Atmel Studio?

E
Elchi Themenstarter:in
26 Beiträge seit 2016
vor 7 Jahren

Grüß dich,

das wurde mit der Arduino-IDE gemacht 😉

LG

T
708 Beiträge seit 2008
vor 7 Jahren

Hallo Elchi,

da besteht ein grundsätzliches Verständnisproblem. Auch ohne dass ich in den Code geschaut habe.

Da der Controllino nur in seiner "Freizeit" den SerialPort liest, muss ich den Befehl durchaus 3-4 mal schicken.

Das ist das erste Design-Problem. Der Arduino verfügt über eine hardwareseitige Serielle-Schnittstelle. Das bedeutet, die Daten werden gebuffert und gehen nicht verloren.
Es sei denn: Du rufst im Code flush() auf oder die Zeichen überschreiten die Größe von 64byte.

Wenn der Arduino sauber entwickelt wäre, müsstest Du auch keine Befehle mehrfach versenden.

Ich hatte Anfangs mit dem "DataRecieved"-Event experimentiert, aber das hat nicht geklappt. Ich bekam immer Fehlermeldungen mit dem Sinn "Textbox wird aus anderem Thread gefüllt -> nicht zulässig". Das habe ich nicht in den Griff bekommen und mir so meine eigene Abfrage gebastelt. Da wird es dann allerdings mit dem Not-Aus schwer,

Auch hier gibt es ein Verständnis-Problem.
Das DataReceived-Event wird aufgerufen, sobald Daten bereitstehen. Das kann zu jeder Zeit passieren und läuft daher nicht im Thread Deiner Form, sondern separat.
Um nun den Wert aus dem Event in dem Thread der Form anzeigen zu können, solltest Du Dich mit Invoke beschäftigen.

Habe gerade leider nicht mehr Zeit, sonst würde ich mir den Code mal anschauen.

E
Elchi Themenstarter:in
26 Beiträge seit 2016
vor 7 Jahren

Hallo Elchi,
Wenn der Arduino sauber entwickelt wäre, müsstest Du auch keine Befehle mehrfach versenden.

Danke für die Erklärung. Da kann ich jetzt leider nicht mehr viel dran machen, ich kann den Kollegen das aber mal überprüfen lassen.

Auch hier gibt es ein Verständnis-Problem.
Das DataReceived-Event wird aufgerufen, sobald Daten bereitstehen. Das kann zu jeder Zeit passieren und läuft daher nicht im Thread Deiner Form, sondern separat.
Um nun den Wert aus dem Event in dem Thread der Form anzeigen zu können, solltest Du Dich mit Invoke beschäftigen.

Inzwischen ist mir da einiges klarer geworden denn es wird letztendlich nur via DataRecieved funktionieren, denke ich zumindestens aktuell.

Habe gerade leider nicht mehr Zeit, sonst würde ich mir den Code mal anschauen.

Kein Problem. Falls du irgendwann Zeit und Lust findest, sag Bescheid dann schicke ich dir mal den aktuellen Stand bzw. lade ihn hoch und verlinke ihn.

Aktueller Stand:
Ich bekomme die Daten via DataRecieved ausgelesen. Allerdings tuhe ich mich schwer damit die Überprüfung und ggf. Neusendung der Befehle anzugehen. Aktuell klappt das einfach noch nicht wie gewünscht, zudem kann ich nicht 100%tig ausschließen das es am Controllino liegt.
So langsam geht mir der Hintern auf Grundeis.

LG Elchi

D
985 Beiträge seit 2014
vor 7 Jahren

Das Protokoll ist natürlich etwas mau (freundlich ausgedrückt).

Wenn der Controllino bei jedem Senden eine Zeichenfolge mit einer bestimmten Minimallänge senden würde, dann könnte man da wesentlich besser darauf reagieren.


CRP: => Antwort auf einen Befehl (Command Response)
EVT: => Eine Nachricht vom Controllino (z.B. der Notaus-Event)

Jetzt kann man nach dem Lesen dieser 4 Zeichen wunderbar unterscheiden, was man als nächstes zu erwarten hat.

E
Elchi Themenstarter:in
26 Beiträge seit 2016
vor 7 Jahren

Grüß dich,

angenommen er ändert das noch ab, wie würdest du da rangehen?
Es wäre super wenn du mir da deinen Ansatz erklären könntest, dann habe ich eine Argumentationsgrundlage 😃

LG Elchi

1.029 Beiträge seit 2010
vor 7 Jahren

Hi,

naja - selbst das Protokoll wie es ist (sofern tatsächlich immer ein Semikolon am Schluss ist) lässt sich doch recht leicht handeln - oder nicht?

Hatte mal etwas ähnliches machen müssen und das dann folgendermaßen aufgezogen:

Erstmal ein Interface "IMessageHandler" definiert, welches:

  • Messages senden kann
  • ein Event anbietet, welches bei gelesenen Messages ausgelöst wird
  • Mehrere Parser verwaltet

Dann das Interface "IMessageParser" definiert, welches:

  • prüfen kann ob ein gegebener Input (string) verarbeitet werden kann vom Parser
  • eine Verarbeitungsmethode anbietet, welche eine Message zurückgibt

Klasse Message erstellen, die:

  • einen CommandType zur leichteren Prüfung hat (z.B. ein Enum)
  • eine Mehode zur Wandlung in einen sendefähigen string bietet (.ToCommand() oder so)

Die Implementierung vom MessageHandler kann man dann SeriamMessageHandler nennen - die liest über das DataReceived-Event kontinuierlich als String in einen Buffer und hängt dort immer an. Immer wenn das geschehen ist - werden alle MessageParser gefragt, ob der vorhandene Input (gelesen bis zum ersten ";" verarbeitet werden kann - falls ein Parser darunter ist - wird der gebeten den Teil vom Command zu verabeiten, wobei der Buffer bis zum Semikolon geleert wird und entsprechend das Event ausgelöst wird.

Die Klasse die den MessageHandler verwendet kann dann recht einfach werden, indem dort einfach Messages erzeugt werden, die gesendet werden bzw. Funktionen eingebaut werden, die auf ein bestimmtes Ereignis vom IMessageHandler "warten" um eine neue Message zu senden oder irgendetwas in der Oberfläche zu aktualisieren.

Nachfolgend etwas Code, der die Struktur verdeutlichen soll - weder vollständig noch funktionsfähig - :


using System;

namespace ConsoleApplication1
{
	public interface IMessageHandler
	{
		event EventHandler<Message> MessageReceived;

 		void Send(Message message);

		void AddParser(IMessageParser parser);
	}

	public interface IMessageParser
	{
		bool CanProcess(string message);

		Message Process(string message);
	}

	public class SerialMessageHandler : IMessageHandler
	{
		// In dieser Klasse könnte SerialPort und dessen
		// DataReceived-Ereignis genutzt werden um
		// IMessageParser zur Auslösung des MessageReceived
		// Ereignisses zu nutzen
		public event EventHandler<Message> MessageReceived;

		public void Send(Message message)
		{
			throw new NotImplementedException();
		}

		public void AddParser(IMessageParser parser)
		{
			throw new NotImplementedException();
		}
	}

	public enum MessageType
	{
		InitiateCommand,
		Command,
		CommandComplete,
		Exit
	}

	public abstract class Message
	{
		public MessageType Type { get; set; }

		public abstract string ToMessage();
	}

	public class CommandMessage : Message
	{
		private readonly string _commandName;

		public CommandMessage(string commandName)
		{
			_commandName = commandName;
		}

		public override string ToMessage()
		{
			return $"{_commandName};";
		}
	}

	public class ExitMessage : Message
	{
		public ExitMessage()
		{
			Type = MessageType.Exit;
		}

		public override string ToMessage()
		{
			return ToString();
		}
	}

	public class ExitMessageParser : IMessageParser
	{
		public bool CanProcess(string message)
		{
			return message == "notaus;";
		}

		public Message Process(string message)
		{
			return new ExitMessage();
		}
	}
}

E
Elchi Themenstarter:in
26 Beiträge seit 2016
vor 7 Jahren

Hallo Taipi,

danke für deine Antwort! Da muss ich mir erstmal ganz viel nachschauen, da ist soviel Neues für mich dabei.
Aber irgendwie hört sich das Grundkonstrukt vertraut an, das habe ich so ähnlich ja auch versucht zu lösen, nur eben wie ein blutiger Anfänger mit If usw. 😉

Hast du einen Blick in mein Projekt werfen können? 😃

Hier mal ein Gehversuch von vorhin, betroffen sind in "Hauptfenster":

  • contSendenStart()
  • contSendenStop()
  • contEmpfangskontrolle()
  • contVerbindungAufbauen()
    Die habe ich alle nach oben geschoben, sollten also schnell zu finden sein 😉

https://www.dropbox.com/sh/cekjdrded3sgpur/AAAlYR_TOVbpvg6DzSARfGBUa?dl=0

Jetzt muss ich erst an einer anderen Baustelle ran, schaue mir dein Beispiel Morgen also nochmal ganz genau an. Danke dir dafür!!

Ganz liebe Grüße
Elchi

1.029 Beiträge seit 2010
vor 7 Jahren

Hi,

nun - was soll ich sagen - wenn man das Projekt öffnet merkt man direkt, dass es sich um eines deiner ersten Projekte handelt... Das ist jetzt auch nicht bös gemeint - ich denke beim selbst beibringen startet fast jeder so.

Aber mal so als Grundsatz:
In einer guten Anwendung ist die Logik von der Oberfläche getrennt.

In deiner Anwendung gibt es allerdings bis auf die vordefinierte Program.cs, XML.cs und 3 Forms keine einzige Klasse - du hast alles ohne Ausnahme mit deinen Forms verschmolzen.

Man sieht zwar du hast dir redlich Mühe gegeben alles zu beschreiben - nur bei der Übersicht und isolierten Betrachtung von Problemen hilft das halt nicht wirklich.

Um dein Problem mal in Worte zu fassen:
Du sendest Daten - und möchtest derzeit in der selben Funktion wissen, ob vom Controllino die die richtige Antwort kam - und verwendest hierfür diverse notdürftige Konstrukte, die irgendwelche Buffer leer mnachen, den kompletten Thread blockieren, etc. pp.
Mit etwas mehr Rumspielen wird das sicher irgendwann mehr oder minder funktionieren.

Da dir offenbar die Zeit ausgeht halte ich mich jetzt einfach mal zurück und geb dir folgenden Tipp:
Dein Problem kommt daher, dass du mit einem Funktionsaufruf nicht nur alles startest - sondern auch alles sequenziell nacheinander vereinbaren möchtest. Das funktioniert in WindowsForms und dem SerialPort so nicht - das komplette System basiert auf Ereignissen.
Wenn du das für die relevanten Funktionen schaffst - dann kriegst das auch ans Laufen.
(Wirklich behilflich kann ich dir hier jedenfalls nicht sein - habe weder ein COM-fähiges Gerät hier - noch finde ich mich im Source wirklich zurecht)

LG

E
Elchi Themenstarter:in
26 Beiträge seit 2016
vor 7 Jahren

Grüß dich,

ersteinmal: Danke das du dir die Zeit genommen hast und versuchst mir weiter zu helfen, ich weiß das sehr zu schätzen!

Ja ich lerne gerade erst, hatte bisher nur C++ Grundlagen und in C# haben wir gerade mal ~ 50 Unterrichtsstunden inverstiert bevor es an die Projektphase geht. Die zugesagte Unterstützung eines Betreuers bekommen wir nicht, der hat jahrelange Erfahrung in C#. Daher versuche ich mich da durchzuwühlen, dadurch scheitert es natürlich an diversen Sachen. Erst einen richtigen Kurs zu belegen, das war zeitlich nicht drin anhand der Größe des Projektes. Nunja, alles nicht ideal aber ich habe es ja so gewollt, also Augen zu und durch.

Aktuell werde ich das nicht alles "outsourcen" können, das werde ich zeitlich wohl nicht mehr schaffen, daher stelle ich das mal zurück.

Ich habe mir deine vorherige Antwort nochmal ganz genau zu Gemüte geführt und versucht alles nachzuvollziehen. Wenn ich jetzt deine letzte Antwort lese, ergibt sich für mich folgender Lösungsweg meines Problems:

  • extra Klasse erstellen die für den Serial Port zuständig ist
  • diese enthält:
    • eine universelle Funktion zum senden
    • ein Event DataRecievent
    • eine Interpretation der empfangenen Daten (Parser)
    • ein Event "Befehl verstanden & bestätigt" das mir in meiner Form.cs sagt das der nächste Befehl an die universelle Funktion "senden" gegeben werden kann
    • ein Event "Rückmeldung Istwerte, Not-Aus etc" das mir sagt was mir der Controllino geschickt hat

Damit sollte es dann laufen, korrekt?
Das heißt die Form.cs sagt der extra Klasse das sie senden soll und wartet auf das Event der korrekten Rückmeldung. Ebenso wartet die Form.cs auf Events die mir sagen was der Controllino geschickt hat (Not-Aus etc).

Habe ich dich da richtig verstanden?

Vielen ganz lieben Dank!
Elchi

1.029 Beiträge seit 2010
vor 7 Jahren

Hi,

grundsätzlich hatte ich zwar deutlich mehr Schnittstellen und Klassen angedacht - aber mit einer SerialPortHandler-Klasse sollte es auch gehen.

Wie du das ganze in deiner Form am besten verarbeitest weiß ich nicht, da ich den Ablauf mit dem SerialPort bislang nicht ganz verstehe. (Welches Kommando, wie viele und warum)

Aber für den Fall, dass du einfach mehrere Kommandos nacheinander senden möchtest kann man das schon so ähnlich machen. (Unter Berücksichtigung der Bearbeitung im Controllino)

Mit deinen Gedanken würde ich:
a) Eine Klasse SerielMessageHandler schreiben
b) eine Klasse Message schreiben
c) einen Enum MessageType schreiben
d) Der Klasse SerialMessageHandler beibringen:

  • Messages von anderen Komponenten entgegenzunehmen und zu senden
  • Ein Event zu feuern, wenn der Controllino was gesendet hat - eigentlich reicht hier
    ein einziges Event, dass mitsamt einer Nachricht gefeuert wird, die ja dann wiederum den
    MessageType hat
    Entweder in der Form oder in der SerialMessageHandler-Klasse kannst du ja dann einfach eine Liste der zu sendenden Kommandos haben - und wenn ein Kommando erst eine Bestätigung des vorherigen Kommandos braucht - musst du ja einfach nur richtig auf das von dir selbst ausgelöste Event reagieren.

Ansonsten kriegst du das so sicher hin.

LG

E
Elchi Themenstarter:in
26 Beiträge seit 2016
vor 7 Jahren

Grüß dich,

bitte entschuldige das ich mich jetzt erst melde, aber wie du ja bereits gemerkt hast hatten wir es ziemlich eilig 😄
Jetzt hier eine kleine Beschreibung wie wir es gelöst haben:

  • wir haben uns insgesamt 3 State-Machines gebastelt: 1x Start, 1x Stop, 1x Istwerte
  • jeweils gab es ein passendes eNum dazu, jewels mit "Restart" & "Fehler"
  • am Serialport benutzten wir eine Art Ringpuffer
  • eine Vorstufe der Verarbeitung indem besondere Events (Not-Aus & Fehlermeldungen) behandelt werden
  • falls es ein Befehl ist der unserem Schema entspricht wird er weitergeleitet und dann zugeordnet

Bekommen wir während die Start-machine ausgeführt wird einen Fehler der uns sagt das der Befehl nicht korrekt gesendet wurde, wiederholen wir den Vorgang. Ansonsten springen wir in die Stop-machine die die Stoppbefehle zur Sicherheit nochmal überträgt damit auch wirklich alle Aktoren aus sind

Zusätzlich haben wir einen Art Ping integriert, welcher mittels eines TImers überprüft ob die Verbindung noch steht.

Das Einzige was mir da nicht gefällt ist das wir, wenn der SerialPort mal nicht mit dem Controllino verbunden ist, eine harte Fehlermeldung rauswirft welche das Programm meist nicht "überlebt" und sich aufhängt. Aber da dies ohnehin nur in Kombination mit dem NotAus stattfindet und die Aktoren dabei durch die Hardware direkt zurückgesetz weden, finde ich das aktuell verschmerzbar.

An dieser Stelle möchte ich mich nochmal ganz ganz herzlich bei euch und voralldingen dir Taipi bedanken. Es ist immer sehr schön zu wissen das man nicht ganz alleine vor solchen Aufgaben steht. Ich werde mir in der Zukunft nochmal deine Ratschläge zur Strukturiereung etc. vornehmen und versuchen das dann in die nächsten Programm einfließen zu lassen.

Also, ganz liebe Grüße und ein großes Dankeschön!
Euer Elchi

PS: Hier kann dann meinetwegen "Gelöst" werden