Fragensteller
RS232 Bild empfangen und speichern

Frage
-
Hallo,
Ich empfange über RS232 ein BMP-Bild von einem Messgerät
Dieses will ich mit einem selber geschriebenen Programm speichern.Die Übertragung funktioniert ansich schon, allerdings kommt kein brauchbares Bild dabei raus.
Hier mal der Teil des Programms:private void serialport_DataReceived(object sender, SerialDataReceivedEventArgs e) { RxData = serialPort1.ReadExisting(); this.Invoke(new EventHandler(Schreiben)); }
private void Schreiben(object sender, EventArgs e) { using (StreamWriter sw = File.AppendText("temp.bmp")) { sw.Write(RxData); RxData = null; } }
Als Ergebnis bekomme ich ein Bitmap, allerdings ist nur ein verfärbter Teil vom Gesamtbild zu erkennen.
Ich habe zum Test das Bild auch mit dem HyperTerminal mitgeschnitten, da kommt das korrekte Bild dabei raus.
Ich habe dann mittels Software den Code der beiden Bitmaps(die genau die gleiche Dateigröße haben) verglichen.. Sie sind anscheinend total identisch.Wäre toll, wenn mir da jemand helfen könnte :)
Danke schonmal.
Gruß
Matze
Alle Antworten
-
Hallo Matze,
eine Bitmap ist kein Text, sondern enthält binäre Daten. Und so funktioniert weder ReadExisting, das eine Zeichenkette liefert (und die Daten dabei verstümmeln kann) noch ein StreamWriter. Nebenbei gibt es auch keinen Grund das Schreiben auf dem Vordergrund-Thread zu machen, das kann durchaus im Hintergrund in DataReceived erfolgen.
Grob könnte der Code wie folgt aussehen:
private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { SerialPort sp = (SerialPort)sender; using (var outputStream = File.OpenWrite(@"C:\TEMP\TEMP.BMP")) { byte[] buffer = new byte[sp.BytesToRead]; int readLength; // Ans Ende outputStream.Seek(0, SeekOrigin.End); // verfügbare Daten lesen und an Datei anfügen while ((readLength = sp.Read(buffer, 0, buffer.Length)) > 0) { outputStream.Write(buffer, 0, readLength); } } }
Voraussetzung ist a) die Datei ist anfangs nicht vorhanden bzw. leer. Und b) es kommen nur die Bilddaten, danach endet die Übertragung. Falls zusätzliche Daten kommen, z. B. um den Anfang oder das Ende des Bildes markieren, musst Du die gelesen Daten vorher "untersuchen", um nur den relevanten Teil in die Datei zu schreiben.
Gruß Elmar
-
Hallo Matze,
wie schon geschrieben: Voraussetzung das nur die Bilddaten, und wirklich nichts weiter als die Bilddaten kommen. Was aber unüblich wäre, normalerweise wird man ein Protokoll definieren, was z. B. Informationen über die gelieferten Daten und ihre Länge, manchmal auch eine Prüfziffer (am Ende9 liefert.
Wenn es eine Bitmap sein soll, schau bitte mal mit einem Hex-Editor in die geschriebene Datei. Eine Bitmap Datei fängt mit "BM" an; andere übliche Formate wie JPG oder PNG verwenden ebenfalls eine Kennung am Anfang.
Gruß Elmar
-
Hallo Elmar,
Es wird wirklich nur das Bild gesendet...
Wenn ich im HyperTerminal das Empfangene in eine Textdatei mitschreibe, und dieser die Endung .bmp gebe, habe ich ein komplettes Bild.
Am Anfang der Datei steht BM.
Eigentlich kommt an den RS232 Port vom Messgerät ein Drucker, ich kann im Messgerät das Dateiformat auf BMP stellen.
Gruß
Matze- Bearbeitet M_tz3 Sonntag, 28. September 2014 18:31
-
Hallo,
hast Du denn den richtigen Handshake und DTREnable eingestellt - identisch zu HyperTerminal - Drucker arbeiten z. B. in der Regel mit DTR/RTS? Wenn nicht wäre es eine Erklärung, denn so könnten einige Daten verloren gehen.
Ansonsten, was steht in den empfangenen Daten drin? Mindestens der Header (siehe Wikipedia) sollte passen, sonst kann die Datei nicht angezeigt werden. Ist später etwas kaputt, gibt es Streifen oder bunte Flächen.
Gruß Elmar
-
Hallo Elmar,
ich bin in der seriellen Datenübertragung noch Anfänger.. auch in C# arbeite ich mich gerade wieder ein ;)
Ich habe mir die Dateien mit einem Hex-Editor angeschaut.. Sie unterscheiden sich tatsächlich ein wenig.
Hier Screenshots vom Hex-Editor:
Programm:
http://s14.directupload.net/images/140928/vv73dj2c.jpg
Hyperterminal:
http://s7.directupload.net/images/140928/o2ct2l2b.jpg
Hier noch die Daten zum COM-Port:
//Hyperterminal COM ist offen Schlangengröße In/Out 8192/8192 Baud-Rate 19200 DTR an Data Bits=8, Stop Bits=1, Parity=None Zeichen setzen: Eof=0x1A, Error=0x00, Break=0x00, Event=0x1A, Xon=0x11, Xoff=0x13 Handflow: ControlHandShake=(DTR_CONTROL, CTS_HANDSHAKE, ERROR_ABORT), FlowReplace=(TRANSMIT_TOGGLE, RTS_HANDSHAKE, XOFF_CONTINUE), XonLimit=80, XoffLimit=200 Zeitüberschreitungen: ReadInterval=10, ReadTotalTimeoutMultiplier=0, ReadTotalTimeoutConstant=0, WriteTotalTimeoutMultiplier=0, WriteTotalTimeoutConstant=5000 // Programm COM ist offen Baud-Rate 19200 DTR an Data Bits=8, Stop Bits=1, Parity=None Zeichen setzen: Eof=0x1A, Error=0x00, Break=0x00, Event=0x1A, Xon=0x11, Xoff=0x13 Handflow: ControlHandShake=(DTR_CONTROL, CTS_HANDSHAKE, ERROR_ABORT), FlowReplace=(AUTO_TRANSMIT, AUTO_RECEIVE, TRANSMIT_TOGGLE, RTS_HANDSHAKE, XOFF_CONTINUE), XonLimit=1024, XoffLimit=1024 DTR an Zeitüberschreitungen: ReadInterval=-1, ReadTotalTimeoutMultiplier=-1, ReadTotalTimeoutConstant=-2, WriteTotalTimeoutMultiplier=0, WriteTotalTimeoutConstant=0 Schlangengröße In/Out 4096/2048
Gruß Matze
-
Hallo Matze,
wenn man nicht wirklich eine Ahnung hat sollte man eigentlich seinen Mund halten -> auf mich bezogen. Ich habe nie etwas mit RS232 programmiert. Dennoch fällt mir etwas auf.
Hyperterminal hat eine Constante WriteTotalTimeoutConstant = 5000, bei dir im Programm steht diese auf 0. Weiterhin kann man sehen das bei dir viel weniger Daten im IN/OUT zu sehen sind. Ich kann dir den Fehler jetzt leider nicht benennen aber es sieht doch stark aus als ob auf Grund des zu niedrigen Timeout's bei dir nicht alle Daten ankommen. Leider keine Lösung aber vielleicht ein Ansatzpunkt...
Versuche doch mal die serialport WriteTotalTimeoutConstant und WriteTotalTimeoutMultiplier Eigenschaften anzupassen.
Gruß
Jens Gerber
-
Hallo Matze,
macht nichts - ich habe davon (vor gut zwei Jahrzehnten) soviel damit zu tun gehabt, dass es für mehrere reicht ((;
Das Problem dürfte hier XON / XOFF sein. Wenn Du die auf die Hexadezimal Daten schaust, so wirst Du feststellen, dass die "11" Bytes fehlen; das wäre ein XON - und das wird bei aktiviertem XON/XOFF als Protokollzeichen verwendet, ist somit wie XOFF (0x13) nicht in den Daten enthalten - weswegen sich das Protokoll nicht für binäre Daten wie Bilder eignet.
Schalte die XON/XOFF Kontrolle ab - also nur RequestToSend, dann sollte es (besser) funktionieren.
DTR / RTS Handshake ist zuverlässiger, weil es eigene Steuerleitungen verwendet - XON/XOFF nimmt man nur wenn es nicht anders geht (mir damals bei Terminals zu oft passiert, weil die Leitungen miserabel verdrahtet waren (;
Gruß Elmar
@Jens: Timeouts können eine Rolle spielen, da sollte man erst dran, wenn nichts funktioniert - aber anscheinend kriegt es der Drucker ja hin. Für Neugierige siehe z. B. http://www.lookrs232.com/com_port_programming/api_commtimeouts.htm
- Als Antwort vorgeschlagen Jens Gerber Sonntag, 28. September 2014 22:18
-
Hallo Elmar,
Jetzt funktioniert das schon ganz gut :)
Doch zwei Fragen dazu habe ich noch...
Wie kann mein Programm jetzt erkennen, dass alle Daten angekommen sind? Denn das Messgerät sendet ja kein Ende-Zeichen, etc..Dies muss ich ja irgendwie erkennen, um dann die Datei zu schließen und dem Benutzer zu signalisieren, dass die Übertragung abgeschlossen ist.
Das wäre schon meine zweite Frage. Mit welchem Befehl schließe ich die TEMP.BMP wieder? Denn sie kann momentan erst verwendet werden, wenn ich das Programm schließe.Gruß
Matze
-
Hallo Matze,
wie oben weiter geschrieben, gibt es normalerweise ein Protokoll, das etwas definiert wie: jetzt kommt ein Block mit einem Bild und das ist n Bytes groß.
Gibt es so etwas nicht, wird es mehr oder weniger kniffelig. Wenn zwischen den Bildern eine größere Pause liegt, so kann man versuchen die über einen Timer zu erkennen.
Kommen die Daten unregelmäßig bzw. sind die Pausen ziemlich kurz, wäre der (deutlich aufwändigere) Weg, die empfangenen Daten zu untersuchen. Wie oben in den Links nachzulesen, hat eine Bitmap einen Header und man kann daraus die Größe ermitteln. Wobei das Gucken nach "BM" als Indikator nicht ausreicht, da das auch innerhalb der Bilddaten vorkommen kann.
Idealer wäre, wenn Du das Senden genau einer Datei anstossen kannst oder etwas befehlen kannst wie Sende die Datei zu Ende und warte bis ich die nächste haben möchte.
Zur offenen Datei: Die wird beim Verlassen des using Blocks wieder geschlossen. Wenn sie jedoch während des gesamten Programmablaufs als geöffnet erscheint, liegt das daran, dass sich das Programm die meiste Zeit in dem Empfangsblock befindet.
Damit man auf die Datei während des Ablaufs zugreifen kann, ohne dass sich der Empfang mit dem Zugriff ins Gehege kommt brauchst Du eine Sperre, siehe lock Anweisung.
Das obige erweitert könnte das so aussehen:
object fileLocked = new object(); string receivedFileName = @"C:\TEMP\TEMP.BMP"; DateTime receivedTime = DateTime.MinValue; private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { var sp = (System.IO.Ports.SerialPort)sender; lock (fileLocked) { using (var outputStream = File.OpenWrite(receivedFileName)) { byte[] buffer = new byte[sp.BytesToRead]; int readLength; // Ans Ende outputStream.Seek(0, SeekOrigin.End); // verfügbare Daten lesen und an Datei anfügen while ((readLength = sp.Read(buffer, 0, buffer.Length)) > 0) { outputStream.Write(buffer, 0, readLength); receivedTime = DateTime.Now; // Letzte Zeit (nicht sehr genau) } } } }
Und dann kann man - z. b. im Click-Ereignis einer Schaltfläche:
// Um die Datei während des Laufs umzubenennen string saveFileName = @"C:\TEMP\FERTIG.BMP"; lock (fileLocked) { // Verschieben der empfangenen Datei File.Move(receivedFileName, saveFileName); receivedTime = DateTime.MinValue; }
Wobei der knifflige Teil fehlt, nämlich wann eine Datei komplett ist - ansonsten bekommst womöglich nur halbe oder auch anderthalb Bilder.
Ich habe eine Zeit für den letzten Empfang eingebaut, das mag für den Anfang zum Testen in einem Timer verwendet werden - auch dort mit lock arbeiten, sonst kann sich der Wert zwischenzeitlich ändern.
Gruß Elmar
- Bearbeitet Elmar BoyeEditor Dienstag, 30. September 2014 08:43
-
Hallo Elmar,
Es wird immer nur ein Bild gesendet. Wenn ich einen Button drücke, sende ich dem Gerät einen Befehl, der den Auftrag gibt, ein Bild zu "drucken".
Die Datei wird nach dem "using" leider nicht geschlossen. Nach dem Empfang ist die Datei für den Windows Explorer nicht verwendbar, da sie von der Anwendung noch verwendet wird.
Ich könnte ja einen Timer laufen lassen, der z.B. auf 3 Sekunden zählt, und dann eine Methode aufruft.
Und immer nach einem Schreibdurchgang, wird der aktuelle Wert des Timers auf 0 zurückgesetzt. Wenn keine Daten mehr ankommen, passiert das nicht, und der Timer zählt bis 3, wird die Methode ausgeführt.
Edit: Ich habe das mit dem Timer probiert. Könnte theoretisch auch funktionieren. Nur sieht es so aus, als ob dein Empfangscode im letzten Durchgang hängen bleiben würde. Dadurch wird vlt auch die Datei nicht mehr freigegeben und das Timer-Event wird nicht ausgelöst.
Gruß Matze
- Bearbeitet M_tz3 Dienstag, 30. September 2014 13:31
-
Hallo Matze,
entschuldige, es hat etwas länger gedauert.
Wenn Du das Senden selbst anstößt, ist zumindest das Erkennen einfacher, als bei den anderen von mir aufgeführten Varianten.
Das Blockieren der Datei dürfte an meinem Code liegen ;( Denn ich habe dem eine Schleife verpasst, die zum Dauer öffnen führt. Das passiert, wenn man blind codiert und nicht genug darüber nachdenkt.
Auch das Folgende ist vollkommen blind programmiert - und somit mit Vorsicht zu genießen, da nur die Syntax vom Compiler abgenommen:using System; using System.IO; using System.Windows.Forms; namespace ElmarBoye.Samples.Forms { public partial class ReadPictureForm : Form { const int TIMER_WAITTIME = 5000; // Wartezeit in ms für Timer (je nach Tempo kleiner) object receiveLock = new object(); // Sperre bei Zugriff auf Empfangsdatei bool isReceiving; // Wahr wenn Empfang läuft bool hasReadTimeout; // Wahr, wenn Timeout im Empfang DateTime receivedTime = DateTime.MinValue; // Zeit des letzten Empfangs string receivedFileName = @"C:\TEMP\TEMP.BMP"; // temporäre Empfangsdatei string bitmapFileName = @"C:\TEMP\BILD.BMP"; // Basis Name der endgültigen Datei string saveFileName; // aktueller Empfangsname, siehe GetNextBitmapFileName public ReadPictureForm() { // SerialPort (serialPort1), Forms.Timer (receivedTimer), // Schaltfläche (receiveButton), Label (InfoLabel) InitializeComponent(); } /// <summary>Starten Bildempfang</summary> private void receiveBitmapButton_Click(object sender, EventArgs e) { // eindeutigen Dateinamen für Enddatei ermitteln saveFileName = GetNextBitmapFileName(bitmapFileName); if (saveFileName == null) { MessageBox.Show("Keine Datei verfügbar"); return; } // Optional: Lesevorgang nach Wartezeit abbrechen // probieren ob und welche Zeit sinnvoll, ansonsten streichen this.serialPort1.ReadTimeout = 5000; // this.serialPort1.Open(); this.CleanupReceiveFile(); // Löschen der Empfangsdatei isReceiving = true; // Jetzt kommt die Datei (hoffentlich ;) // **** Hier einfügen Bild anfordern *** this.serialPort1.Write("<Ich möchte ein Bild>"); // Überwachung durch Timer Starten this.receiveTimer.Interval = 1000; // Wert sollte in etwa teilbar durch TIMER_WAITTIME sein this.receiveTimer.Enabled = true; } /// <summary>Ereignis-Handler für den Daten-Empfang.</summary> private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { var sp = (System.IO.Ports.SerialPort)sender; if (sp.BytesToRead > 0) { // Sperren Zugriff auf Datei, sowie isReceiving und hasReadTimeout lock (receiveLock) { // Nur wenn im Empfang und kein Timeout if (!isReceiving || hasReadTimeout) { // Wirft Daten weg (optional was anderes damit machen) sp.DiscardInBuffer(); return; } byte[] buffer = new byte[sp.BytesToRead]; int readLength = -1; try { readLength = sp.Read(buffer, 0, buffer.Length); } catch (TimeoutException) // Zeitablauf (als Ende interpretiert) { hasReadTimeout = true; return; } if (readLength > 0) { // Daten ans Ende anfügen using (var outputStream = File.OpenWrite(receivedFileName)) { outputStream.Seek(0, SeekOrigin.End); outputStream.Write(buffer, 0, readLength); ShowProgress(String.Format("{0} Bytes geschrieben", outputStream.Position)); } // letzte Empfangszeit für Timer Vergleich merken receivedTime = DateTime.UtcNow; } } } } private void ShowProgress(string info) { // Anzeige des Fortschritts auf einem Label namens InfoLabel if (this.InvokeRequired) { // aus DataReceived this.BeginInvoke((MethodInvoker)delegate { this.InfoLabel.Text = info; }); } else { this.InfoLabel.Text = info; } } bool timerIsWaiting = false; /// <summary>Ereignis-Handler für den Empfangs-Timer.</summary> private void receiveTimer_Tick(object sender, EventArgs e) { // Mehrfache Ticks ignorieren, nur zur Sicherheit // denn Interval sollte gleich gross genug sein if (timerIsWaiting) return; try { timerIsWaiting = true; // Sperren Zugriff auf Datei, sowie isReceiving und hasReadTimeout lock (receiveLock) { // Abbruch durch Zeitüberlauf im Empfang if (hasReadTimeout) { receiveTimer.Enabled = false; isReceiving = false; // Datei kann defekt sein, wenn "richtiger" Abbruch RenameReceiveFile(saveFileName); } // Empfang aktiv else if (isReceiving) { TimeSpan idleTime = DateTime.UtcNow - receivedTime; // Keine Übertragung innerhalb der Wartezeit if (idleTime.TotalMilliseconds > TIMER_WAITTIME) { receiveTimer.Enabled = false; isReceiving = false; RenameReceiveFile(saveFileName); } } else { receiveTimer.Enabled = false; } } } finally { timerIsWaiting = false; } } private void RenameReceiveFile(string targetFileName) { try { File.Delete(targetFileName); File.Move(receivedFileName, targetFileName); } catch (Exception ex) { Console.WriteLine("RenameReceiveFile:" + ex.Message); } } private void CleanupReceiveFile() { File.Delete(receivedFileName); receivedTime = DateTime.UtcNow; isReceiving = false; hasReadTimeout = false; } private static string GetNextBitmapFileName(string fileName) { string fileDirectory = Path.GetDirectoryName(fileName); string baseFileName = Path.GetFileNameWithoutExtension(fileName); string fileExt = Path.GetExtension(fileName); // Ergänzt um Datum/Zeit sortierbar baseFileName += DateTime.UtcNow.ToString("'-'yyyyMMdd'-'HHmmss"); string dateFileName = Path.Combine(fileDirectory, baseFileName + fileExt); if (!File.Exists(dateFileName)) return dateFileName; // notfalls ergänzt um 01...99 for (int bitmapIndex = 1; bitmapIndex < 100; bitmapIndex++) { dateFileName = Path.Combine(fileDirectory, String.Format("{0}-{1:00}{2}", baseFileName, bitmapIndex, fileExt)); if (!File.Exists(dateFileName)) return dateFileName; } return null; // Zuviel Gleiche des guten } } }
Mindestens einfügen müsstest Du das Anstoßen der Bildübertragung. Die Werte für Zeiten sind rein fiktiv, das Optimum müsstest Du anhand Deiner Gegebenheiten rausfinden - und natürlich die vermutlich vorhandenen (Denk-)Fehler meinerseits ;(
Schau mal, ob Du damit etwas anfangen kannst...
Gruß Elmar
-
Hallo Elmar,
Jetzt sieht es schon fast gut aus ;)
Das Bild wird übertragen und ist danach auch zugänglich wärend das Programm läuft.
Ich habe beim "Start"-Button einen Timer gestartet, der auf 5sec hochzählt und dann ein Event auslöst, in dem dem Benutzer angezeigt wird, dass die Übertragung fertig ist, und das Bild anzeigt.Im serialPort1_DataReceived-Event setze ich den Timer über timer.Stop(); und timer.Start(); zurück, damit er wieder von vorne anfängt zu zählen, solange noch Daten empfangen werden.
Aber der Timer löst, wenn das Bild fertig übertragen ist, leider nicht aus. Wenn ich das Timer-Event "von Hand" auslöse, funktioniert alles, aber der Timer spielt nicht mit.
Weist du woran das liegen könnte?
Gruß Matze
-
Hallo,
Ich habe den Timer in meinem Beispiel nicht ohne Grund unabhängig vom DataReceived Ereignis erstellt.
Abgesehen davon, dass Du das Starten evtl. falsch platziert hast...
Der Windows Forms Timer[1] ist abhängig von der Windows Meldungsschleife, wird die nicht bedient, so passiert gar nichts - etwas wie Application.DoEvents will niemand wirklich, schon gar nicht in Hintergrund-Threads.
Das DataReceived-Ereignis tritt nur ein, wenn bereits Daten angekommen sind. Auch kann der Lesevorgang u. U. länger dauern als erwartet, weil etwas klemmt - hängt von der Hardware am anderen Ende ab (hoffentlich nicht).
Weswegen man als Testfall verwenden sollte: Trenne das Kabel während der Übertragung, um festzustellen, wie das Programm damit klarkommt.
Versuche mal die Variante, die ich zusammengestellt habe (am besten in einem neuen Formular / Programm).
Gruß Elmar
[1] Es gibt zwar andere Timer, die nicht damit arbeiten. Damit wird es aber kniffliger, weil Du gleich drei Threads (GUI, SerialPort und Timer) in den Griff bekommen müsstest - was ich Dir für den Anfang ersparen wollte.