# Probleme beim CSV Datei schreiben mit ByteArray auf WAGO 750-8202



## TiI (6 Dezember 2018)

Hey zusammen,
ich bin relativ neu in der SPS Welt und versuche gerade mit Hilfe der WAGO Beispiel Syslibfile.lib Projekte eigene CSV Dateien schreiben zu lassen.
Umgesetzt wird das Projekt auf einem WAGO 750-8202 und in CoDeSys 2.3 programmiert.

*tl;dr:*
In den erfolgreich geschriebenen Dateien finde ich nach dem Block aus Zeitangabe, Werten und Zeilenabschluss-Steuerzeichen zusätzliche Zeichen, welche dort ungewollt geschrieben wurden.
Könnte der Fehler in meiner Zeigerarithmetik liegen?


* Langfassung:*
In einer WerteGenerieren()-Funktion wird mir ein selbstdefinierter Datentyp befüllt: Darin ein eindimensionales Array mit 10 Werten (gc_ColumnCount := 9 global definiert) und die aktuelle Systemzeit:

```
TYPE typCSV :
    STRUCT
        dtTageszeit:DT;
        Wert:ARRAY[0..gc_ColumnCount] OF REAL;
    END_STRUCT
END_TYPE
```

Danach wird in einem Konvertierungs-Funktionsblock die Umwandlung von typCSV (EingangsArray) in ein ByteArray vorgenommen.
Dazu lasse ich das ByteArray: ARRAY[0..gc_RawDataSize] OF BYTE vor jeder Ausführung löschen und nutze im Anschluss die Zeigerarithmetik aus dem WAGO Beispiel Projekt:

```
FOR z:=0 TO gc_RawDataSize BY 1 DO                                       (* ByteArray löschen *)
    ByteArray[z] := 0;
END_FOR

ptString := ADR(ByteArray);                                                        (* Pointer auf Anfang des Bytearrays *)
index := 0;

ptString^ := DT_TO_STRING(EingangsArray.dtTageszeit);                        (* Zeitstempel --> Bytefeld *)
index := index + LEN(ptString^);                                                    (* feststellen wieviel Byte benutzt sind *)
ptString := ADR(ByteArray[index]);                                                    (* Pointer auf Ende des Wertes *)

FOR i:=0 TO gc_ColumnCount BY 1 DO
    ptString^:= trenner;                                                            (* Trennzeichen hinter letzten Wert *)
    index := index + LEN(ptString^);
    ptString := ADR(ByteArray[index]);

    ptString^ := REAL_TO_STRING(EingangsArray.Wert[i]);                        (* Wert in ByteArray schreiben *)
    index := index + LEN(ptString^);
    ptString := ADR(ByteArray[index]);
END_FOR

ptString^:= '$r$n';                                                                    (* new line *)
index := index + LEN(ptString^);
ptString := ADR(ByteArray[index]);
```
Das ByteArray ist natürlich um einiges größer (durch gc_RawDataSize := 100 global festgelegt) als die Summer aller zu schreibenden Werte (usw.) in Byte.

Der Konvertierungs-Funktionsblock wird im Programm Dateischreiben aufgerufen. Beim Aufruf der Konvertierungsinstanz wird auch die WerteGenerieren()-Funktion aufgerufen.
Das zurückgegebene ByteArray wird nun wie folgt geschrieben:

```
Konvertierungsinstanz(trenner:= ';', EingangsArray:= WerteGenerieren(), convert_ready=> , ByteArray=> );

groesse := SysFileGetSize(g_sDateiname);                                            (* Größe der Datei feststellen *)
IF Konvertierungsinstanz.convert_ready THEN
    handle := SysFileOpen(g_sDateiname, g_zugriff);                                    (* g_zugriff wird mit 'a' initialisiert *)                                                                                           
    groesse := groesse + SysFileWrite(handle, ADR(Konvertierungsinstanz.ByteArray),SIZEOF(Konvertierungsinstanz.ByteArray));
    file_ok := SysFileClose(handle);                                                    (* Datei schließen *)
END_IF
```
Nach dem Schreiben steht alles Gewünschte in der erzeugten / erweiterten Datei.

Mein Problem ist, dass nach einem Null-Byte hinter dem Zeilenende-Steuerzeichen ein abgehacktes Datum (15 Byte lang) vom Format DATE_AND_TIME folgt.
Am Besten zu sehen in einem Hex-Editor (Das Markierte ist der unbewusst geschriebene Datumsteil):



Es ist hinter jedem geschriebenen Daten(ByteArray)block. Manchmal fehlen die Zeichen, manchmal stehen dort irgendwelche Zeichen, aber kein Datum... dann sogar im Null-Teil nach hinten verschoben.
Habe ich bei meiner Zeigerarithmetik einen Fehler gemacht oder ist es ein anderer Fehler?
Könnte es auch eine WAGO/Controller spezifische Beschränkung geben, welche ich übersehe?

Ich lasse extra bei jedem Konvertierungsaufruf das ByteArray aufs neue mit Nullen beschreiben, von daher kann ich mir nicht erklären, warum hinter den Zeilenende-Steuerzeichen keine Nullen stehen...

Gibt es vielleicht eine andere Methode mein ByteArray zu füllen/ zu löschen oder sollte ich es mit Strings schreiben versuchen?
Ich will allerdings später viel mehr Werte schreiben, sodass die allgemeine String Längenbeschränkung ein Problem darstellt.

Vielen Dank für eure Hilfe.
MfG
TiI


----------



## oliver.tonn (6 Dezember 2018)

Du führst die einzelnen File-Befehle zu schnell aus. Die einzelnen FBs haben Rückmeldungen, wann sie fertig sind und welchen Status sie haben. Den nächsten Befehl darf man erst ausführen wenn der vorhergehende beendet ist. Der Schreiben Befehl benötigt z.B. meist mehr als einen Zyklus, deswegen kommt bei Dir einiges durcheinander.


----------



## PN/DP (6 Dezember 2018)

Deine Zeigerarithmetik ist korrekt. Die unerwarteten Zeichen in der Datei entstehen vermutlich erst beim Datei-Schreiben, was bei Dir zu früh "abgewürgt" wird, wie oliver.tonn schon anmerkte.

Man könnte die csv-Zeile verständlicher mit CONCAT zusammensetzen. Dann braucht auch nicht das ganze Array vorher gelöscht werden sondern nur das erste Byte ( ByteArray[0] := 0; )
(Wieviele Zeichen darf ein String lang sein für CONCAT bei WAGO? Sind bei WAGO die String-Funktionen "thread-safe"? Wenn nicht, dann darf CONCAT nicht in verschiedenen Task verwendet werden.)




TiI schrieb:


> Das ByteArray ist natürlich um einiges größer (durch gc_RawDataSize := 100 global festgelegt) als die Summer aller zu schreibenden Werte (usw.) in Byte.


Vorsicht, ich denke für reale Zahlenwerte ist Dein Bytearray zu klein! Echte REAL-Werte können/werden mehr Nachkommastellen haben. Im ungünstigsten Fall können schon allein die 10 Real-Werte und Semikolons mehr als 100 Zeichen ergeben, dazu kommen noch 22 Zeichen Zeitstempel und 2 Zeichen Zeilenende.

Gibt es eine genaue Dokumentation, wie bei WAGO die Ausgabe von REAL_TO_STRING aussieht?
Gibt es bei WAGO eine Funktion um einen Real mit Formatvorgabe in einen String zu konvertieren (z.B. REAL_TO_FMTSTR?)?

Harald


----------



## TiI (7 Dezember 2018)

Hey, danke erstmal an euch für die Anregungen:

in der CoDeSys Programmierungsanleitung von 3S - Smart Software Solutions und in der Beschreibung zur SysFileLib steht leider nichts von einer Methode, wie ich auf das Ende des Schreibvorgangs warten könnte.
Die SysFileWrite Funktion gibt (nach Erfolg?) nur einen DWORD zurück, welcher die Anzahl der geschriebenen Bytes enthält.

Da ich in ST schreibe, werden die Befehle doch generell nacheinander abgearbeitet. Von welchem Zyklus reden wir also, wenn mein Schreibvorgang eurer Meinung nach in der Mitte unterbrochen wird? Ich habe dem Task zum Schreiben meiner Datei mehr als 10 sek gegeben, das Ergebnis bleibt leider gleich... also mit den ungewollten abgehackten Datum. Egal ob ich nur 10 Werte und ein 100 Byte Array schreibe oder 400 Werte in ein 3000 Byte Array.

Wie könnte ich den SysFileWrite Vorgang (in einer Task) "auslagern", sodass sein Schreibvorgang nicht unterbrochen wird?
Für mich sieht es so aus, als würde der Schreibvorgang keinerlei Probleme haben fertig zu werden.
Ich überprüfe jetzt vor jedem Schreibvorgang ob mir vorher die SysFileClose Funktion ein TRUE lieferte.
Nur in diesem Fall erlaube ich beim nächsten zyklischen Aufruf des Schreib-Tasks die SysFileWrite Funktion.
Laut der der CoDeSys Programmierungsanleitung wird mit dem SysFileClose die Datei endgültig geschrieben und abgelegt.
Es sollte mir laut meinem Verständnis ein TRUE liefern, wenn der Schreibvorgang geglückt ist und die Datei zum schließen nicht mehr gelockt ist.
Das deckt sich auch mit dem Dateiinhalt danach.

Ich bin ein wenig ratlos worauf ich noch prüfen kann oder welche Funktionen ich nutzen sollte 
Würden mir vielleicht die Async Funktionen (SysFileOpenAsync, SysFileWriteAsync, SysFileCloseAsync) weiterhelfen?

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Um mal das ganze mal mit Strings zu testen, habe ich folgende Konvertierung geschrieben:

```
WriteBuffer:            STRING(2000);                                        (*  STRING *)

========================================================
WriteBuffer := '';
WriteBuffer := CONCAT(WriteBuffer, DT_TO_STRING(EingangsArray.dtTageszeit));            (* Zeitstempel --> Bytefeld *)

FOR i:=0 TO gc_ColumnCount BY 1 DO
    WriteBuffer := CONCAT( WriteBuffer, trenner );
    WriteBuffer := CONCAT( WriteBuffer, REAL_TO_STRING(EingangsArray.Wert[i]) );
END_FOR

WriteBuffer := CONCAT( WriteBuffer, '$R$N' );                                                    (* new line *)
```
Das hat leider nichts gebracht. Es gibt keine dynamischen Strings in CoDeSys 2.3.
Alle String Funktionen können nur mit 250 Zeichen umgehen, sodass CONCAT bei größeren Strings nicht mehr funktioniert und ab 255 Zeichen nichts weiter an WriteBuffer anfügen will 

Habt ihr noch weitere Tipps für mich?
Würden mir die OSCAT Bibliotheken in diesem Fall etwas bringen? Gibt es dort erweiterte / dynamische Datentypen?

Vielen Dank für eure Hilfe 
VG
TiI


----------



## TiI (14 Dezember 2018)

```
ptString^:= '$r$n';                                                                    (* new line *)

___________________________________
index := index + LEN(ptString^);       <-- gelöscht
ptString := ADR(ByteArray[index]);    <-- gelöscht
```
Da sonst keine weiteren Hinweise kamen, habe ich zur Probe aus dem WAGO Beispiel Programm einfach die letzten 2 Zeilen herausgelöscht.
Sie erschienen mir recht sinnfrei, denn danach war die Ausführung des Funktionsblocks zuende und es wurde nicht mehr mit dem String-Pointer gearbeitet.

Das brachte den entscheidenden Erfolg und es tauchen keine ungewollten Zeichen mehr in meinem ByteArray auf.

Ich vermute der Fehler liegt in der nicht Beachtung der Funktionsweise des Funktionsblocks.
Der Funktionsblock behält bis zur erneuten Ausführung den Pointer (der vorherigen Instanz). Ohne Löschung der letzten zwei Zeilen liegt im Pointer die letzte (beschriebene) Adresse im ByteArray des vorherigen Schreibvorgangs.
Bei einem erneuten Aufruf des Funktionsblocks wird der Pointer wieder initialisiert und ihm die Start-Adresse des neuen ByteArrays zugewiesen.
Auch wenn ich nicht nachvollziehen kann, wann ich dadurch in den Bereich des alten ByteArrays schreibe, wird dort scheinbar ein Schreibvorgang ausgeführt?!
Das erscheint mir bisher als einzige Erklärung des Phänomens.

Oder hat jemand eine andere Erklärung dafür?

Vielen Dank für eure Hilfe.
TiI


----------



## PN/DP (14 Dezember 2018)

TiI schrieb:


> habe ich zur Probe aus dem WAGO Beispiel Programm einfach die letzten 2 Zeilen herausgelöscht.
> [...]
> Das brachte den entscheidenden Erfolg und es tauchen keine ungewollten Zeichen mehr in meinem ByteArray auf.
> 
> ...


Dein Erklärungsversuch passt nicht zum gezeigten Code. In dem Code werden ptString und index vor der Verwendung ordnungsgemäß initialisiert. Und außerhalb von dem gezeigten Konvertierungs-Funktionsblock nicht verwendet.

Daß die beiden Zeilen im gezeigten Code anscheinend überflüssig sind, hatte ich auch schon bemerkt. Das Weglassen der Zeilen sollte aber keinen Effekt auf das Datei-Schreiben haben. (Es ist oft so, daß in Schleifen beim letzten Durchlauf die Pointer nochmal "sinnfrei" erhöht werden - da stört das auch nicht.)

Oder werden ptString oder index noch woanders verwendet/ausgewertet? Du hast uns leider eine Menge wichtigen Code vorenthalten/weggekürzt.


Mir scheint, der Fehler liegt in der Längen-Angabe von SysFileWrite(...):


TiI schrieb:


> ```
> Konvertierungsinstanz(trenner:= ';', EingangsArray:= WerteGenerieren(), convert_ready=> , ByteArray=> );
> 
> groesse := SysFileGetSize(g_sDateiname);                                            (* Größe der Datei feststellen *)
> ...


Das SIZEOF(..Array) dürfte die gesamte Größe des Arrays liefern und nicht nur die von der erstellten Textzeile belegte Länge. Die korrekte Länge steht in der Variable index - wenn das letzte Erhöhen von index oben nicht weggelassen wird!

Ich würde mal die Programmzeile so ändern:

```
[COLOR="#800080"]groesse := groesse + [/COLOR]SysFileWrite(handle, ADR(Konvertierungsinstanz.ByteArray),[COLOR="#0000FF"]LEN[/COLOR](Konvertierungsinstanz.ByteArray));
```
btw.: hier könnte man ebenfalls sagen, das "groesse := groesse + " ist überflüssig, doch wir können ja nicht sehen, ob groesse irgendwo später nochmal verwendet wird.

Leider kenne ich die Wago-Bibliothek nicht. Es scheint mir so, daß man selber keine Schrittkette für das Dateischreiben erstellen muß, SysFileClose prüft anscheinend selber, ob das Schreiben in die Datei komplett beendet ist. Wie wird bei Dir sichergestellt, daß SysFileClose (und evtl. SysFileWrite?) so oft/so lange immer wieder aufgerufen wird, bis die Datei komplett geschrieben und geschlossen ist? Der Code sieht aus als ob alles nur 1x aufgerufen würde. Besonders das "groesse := groesse + " soll doch bestimmt nicht mehrmals ausgeführt werden?

Harald


----------



## TiI (14 Dezember 2018)

PN/DP schrieb:


> Oder werden ptString oder index noch woanders verwendet/ausgewertet? Du hast uns leider eine Menge wichtigen Code vorenthalten/weggekürzt.


Nein _ptrString_ wird nirgends außerhalb des Konvertierungs-Funktionsblock verwendet. Es ist im Deklarationsteil nur im Abschnitt für lokale Variablen VAR.



PN/DP schrieb:


> Mir scheint, der Fehler liegt in der Längen-Angabe von SysFileWrite(...):
> 
> Das SIZEOF(..Array) dürfte die gesamte Größe des Arrays liefern und nicht nur die von der erstellten Textzeile belegte Länge. Die korrekte Länge steht in der Variable index - wenn das letzte Erhöhen von index oben nicht weggelassen wird!
> 
> ...


Die String Funktion LEN() hat laut Dokumentation wieder das Problem nur mit max 255 String Zeichen umgehen zu können. Zudem muss ich bei SysFileWrite die Länge (Size) eines ByteArrays in Form eines DWORD angeben. Als Eingangstyp braucht LEN() eine Variable vom Typ String. Damit fällt das leider raus. (Außerdem sind die String Funktionen laut Doku nicht threadsafe)

Die _groesse_ Berechnung habe ich mittlerweile herraus gekürzt. Das hatte ich einfach aus dem Programmierbeispiel von WAGO übernommen, dafür aber keine Verwendung gefunden.



PN/DP schrieb:


> Leider kenne ich die Wago-Bibliothek nicht. Es scheint mir so, daß man selber keine Schrittkette für das Dateischreiben erstellen muß, SysFileClose prüft anscheinend selber, ob das Schreiben in die Datei komplett beendet ist. Wie wird bei Dir sichergestellt, daß SysFileClose (und evtl. SysFileWrite?) so oft/so lange immer wieder aufgerufen wird, bis die Datei komplett geschrieben und geschlossen ist? Der Code sieht aus als ob alles nur 1x aufgerufen würde. Besonders das "groesse := groesse + " soll doch bestimmt nicht mehrmals ausgeführt werden?
> Harald


Nach euren Hinweisen habe ich in der Dokumentations zu SysFileClose gefunden, dass erst nach der Ausgabe von TRUE bei der SysFileClose-Funktion die Datei tatsächlich geschrieben wurde.

Ich überprüfe in meinem Code nun den BOOL Wert der SysFileClose Funktion auf TRUE. Wenn der Schreibvorgang erfolgreich war, wird ein Zähler dafür hochgezählt.
Wird bei der Überprüfung ein FALSE festgestellt, wird ein Fehlschlag-Zähler hoch gesetzt.
Dieser Fehlschlagzähler steht immer auf 0. Demnach sollte der Schreibvorgang niemals fehlschlagen.

Deshalb hatte ich vermutet, dass eine andere Funktion(sblock) auf mein Array aus der Konvertierung zugreift, während ich das Dateischreiben schon in Auftrag gab.
Heißt es wird von etwas ins ByteArray geschrieben, auch wenn der Konvertierungs-Funktionsblock schon durchlaufen wurde...


----------



## PN/DP (14 Dezember 2018)

Die Stringfunktion LEN() verwendest Du schon oft in der Konvertierungsinstanz.
Deine Textzeile ist bisher noch viel kürzer als 255 Zeichen (Du wolltest sogar mal mit 101 Byte hinkommen )

Du könntest die Zeilenlänge (entspricht index am Ende) aus der Konvertierungsinstanz ausgeben:

```
VAR_OUTPUT
  ...
  TextLen : UDINT ; //oder DWORD (was allerdings nicht ganz korrekt wäre)
END_VAR

...
ptString^:= '$r$n';               (* new line *)

index := index + LEN(ptString^);
TextLen := INT_TO_UDINT(index); //oder INT_TO_DWORD
```

... und an SysFileWrite übergeben:

```
groesse := SysFileWrite(handle, ADR(Konvertierungsinstanz.ByteArray), Konvertierungsinstanz.TextLen);
```

Harald


----------

