Einlesen einer CSV Datei in eine Wago Steuerung

Zuviel Werbung?
-> Hier kostenlos registrieren
Die Formatierung des Zeitstempels hat sich bei Deinen Angaben verändert.

Für das erstgenannte Format mit dd.mm.jjjj HH:MM,Wert

Code:
// split a string in CSV-format into an array of data values
// return TRUE on success, FALSE on CSV format error
// 2024-05-17    KLM        created
FUNCTION FC_ExtractCsvData : BOOL

VAR CONSTANT
    c_bSeparator_LF                : BYTE := WagoTypes.eChar.LineFeed;
END_VAR

VAR_INPUT
    ptsRawString            : POINTER TO STRING(1);
    udiSize                    : UDINT;
    bDTFormat                : BYTE; // time stamp format - see WagoAppString.FuStringToDT.bFormat, e.g. 3: 'dd.mm.jjjj HH:MM'
    bSeparator_Data            : BYTE := WagoTypes.eChar.Semicolon;
    xHeaderIsIncluded        : bool;
END_VAR

VAR_IN_OUT
    atypResultData            : ARRAY[*] OF typDataSet;
END_VAR

VAR_OUTPUT
    xOverFlow                : BOOL; // more line in CSV then array fields
    diNumOfDataLines        : DINT;
END_VAR

VAR
    sExtraction_Line        : WagoAppString.MaxString;
    sExtraction_Value        : WagoAppString.MaxString;
    udiExtractIndex_Line    : UDINT;
    udiExtractIndex_Value    : UDINT;
    diOutputIndex            : DINT;
    usiDataIndex            : USINT;
    udiLen_Line                : UDINT;
    ptbChar                    : POINTER TO BYTE;
    xIsHeaderLine            : bool;
END_VAR

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

diOutputIndex := LOWER_BOUND(atypResultData, 1) - 1;
diNumOfDataLines := 0;

udiExtractIndex_Line := 1;
WHILE (udiExtractIndex_Line <> 0) DO
    xIsHeaderLine := xHeaderIsIncluded AND (udiExtractIndex_Line = 1);
    sExtraction_Line := WagoAppString.StrExtractToken(
            sBuffer        := ptsRawString^,
            udiSize        := udiSize,
            bSeparator    := c_bSeparator_LF,
            udiBegin    := udiExtractIndex_Line,
            udiNext        => udiExtractIndex_Line);
            
    // skip header
    IF xIsHeaderLine THEN
        CONTINUE;
    END_IF
            
    // cut most right padded '$R' (CarriageReturn)
    udiLen_Line := WagoAppString.StrLength(
                    sBuffer    := sExtraction_Line,
                    udiSize    := SIZEOF(sExtraction_Line));
    ptbChar := ADR(sExtraction_Line) + udiLen_Line - 1;
    IF ptbChar^ = WagoTypes.eChar.CarriageReturn THEN
        ptbChar^ := 0; // override by string termination char
    END_IF
    
    // CSV line to data array index
    diOutputIndex := diOutputIndex + 1;
    IF diOutputIndex > UPPER_BOUND(atypResultData, 1) THEN
        xOverFlow    := TRUE;
        FC_ExtractCsvData := TRUE;
        RETURN;
    END_IF
    
    udiExtractIndex_Value := 1;
    usiDataIndex := 0;
    WHILE (udiExtractIndex_Value <> 0) DO
        sExtraction_Value := WagoAppString.StrExtractToken(
                sBuffer        := sExtraction_Line,
                udiSize        := udiSize,
                bSeparator    := bSeparator_Data,
                udiBegin    := udiExtractIndex_Value,
                udiNext        => udiExtractIndex_Value);
    
        usiDataIndex := usiDataIndex + 1;
        CASE usiDataIndex OF
        1: // time stamp
            atypResultData[ diOutputIndex ].dtTimeStamp := WagoAppString.FuStringToDT(bFormat:=bDTFormat, sInput:= sExtraction_Value);
            IF atypResultData[ diOutputIndex ].dtTimeStamp = TO_DT(0) THEN
                JMP FORMAT_ERROR;
            END_IF
        2: // 1st value
            atypResultData[ diOutputIndex ].rValue_1 := TO_REAL(sExtraction_Value);
        3: // 2nd value
            JMP FORMAT_ERROR;
        4: // 3td value
            JMP FORMAT_ERROR;
        ELSE
            JMP FORMAT_ERROR;
        END_CASE
        
    END_WHILE
    diNumOfDataLines := diNumOfDataLines + 1;
    
END_WHILE
xOverFlow    := FALSE;
FC_ExtractCsvData := TRUE;

RETURN;
FORMAT_ERROR:
    FC_ExtractCsvData := FALSE;

Für das neue Format mit jjjj-mm-dd, HH:MM, Wert

Code:
// split a string in CSV-format into an array of data values
// return TRUE on success, FALSE on CSV format error
// use this function for format:
//        jjjj-mm-dd, HH:MM, value, value, value, ...
// 2024-05-17    KLM        created
FUNCTION FC_ExtractCsvData_D_T : BOOL

VAR CONSTANT
    c_bSeparator_LF                : BYTE := WagoTypes.eChar.LineFeed;
    c_bSeparator_Data            : BYTE := WagoTypes.eChar.Comma;
END_VAR

VAR_INPUT
    ptsRawString            : POINTER TO STRING(1);
    udiSize                    : UDINT;
    xHeaderIsIncluded        : BOOL;
END_VAR

VAR_IN_OUT
    atypResultData            : ARRAY[*] OF typDataSet;
END_VAR

VAR_OUTPUT
    xOverFlow                : BOOL; // more line in CSV then array fields
    diNumOfDataLines        : dint;
END_VAR

VAR
    sExtraction_Line        : WagoAppString.MaxString;
    sExtraction_Value        : WagoAppString.MaxString;
    udiExtractIndex_Line    : UDINT;
    udiExtractIndex_Value    : UDINT;
    diOutputIndex            : DINT;
    usiDataIndex            : USINT;
    udiLen_Line                : UDINT;
    ptbChar                    : POINTER TO BYTE;
    xIsHeaderLine            : bool;
    dDate                    : DATE;
    todTime                    : TOD;
END_VAR

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

diOutputIndex := LOWER_BOUND(atypResultData, 1) - 1;
diNumOfDataLines := 0;

udiExtractIndex_Line := 1;
WHILE (udiExtractIndex_Line <> 0) DO
    xIsHeaderLine := xHeaderIsIncluded AND (udiExtractIndex_Line = 1);
    sExtraction_Line := WagoAppString.StrExtractToken(
            sBuffer        := ptsRawString^,
            udiSize        := udiSize,
            bSeparator    := c_bSeparator_LF,
            udiBegin    := udiExtractIndex_Line,
            udiNext        => udiExtractIndex_Line);
            
    // skip header
    IF xIsHeaderLine THEN
        CONTINUE;
    END_IF
            
    // cut most right padded '$R' (CarriageReturn)
    udiLen_Line := WagoAppString.StrLength(
                    sBuffer    := sExtraction_Line,
                    udiSize    := SIZEOF(sExtraction_Line));
    ptbChar := ADR(sExtraction_Line) + udiLen_Line - 1;
    IF ptbChar^ = WagoTypes.eChar.CarriageReturn THEN
        ptbChar^ := 0; // override by string termination char
    END_IF
    
    // CSV line to data array index
    diOutputIndex := diOutputIndex + 1;
    IF diOutputIndex > UPPER_BOUND(atypResultData, 1) THEN
        xOverFlow    := TRUE;
        FC_ExtractCsvData_D_T := TRUE;
        RETURN;
    END_IF
    
    udiExtractIndex_Value := 1;
    usiDataIndex := 0;
    WHILE (udiExtractIndex_Value <> 0) DO
        sExtraction_Value := WagoAppString.StrExtractToken(
                sBuffer        := sExtraction_Line,
                udiSize        := udiSize,
                bSeparator    := c_bSeparator_Data,
                udiBegin    := udiExtractIndex_Value,
                udiNext        => udiExtractIndex_Value);
    
        usiDataIndex := usiDataIndex + 1;
        CASE usiDataIndex OF
        1: // time stamp: date as jjjj-mm-dd
            dDate := FuStringToDate(bFormat:= 0, sInput:= sExtraction_Value);
            IF dDate = TO_DATE(0) THEN
                JMP FORMAT_ERROR;
            END_IF
        2: // time stamp: time as HH:MM
            todTime := FuStringToTod(sInput:= sExtraction_Value);
            IF todTime= TO_TOD(0) THEN
                JMP FORMAT_ERROR;
            END_IF
            atypResultData[ diOutputIndex ].dtTimeStamp := FuSetDT(datDate:= dDate, tdTimeOfDay:= todTime);
            
        3: // 1st value
            atypResultData[ diOutputIndex ].rValue_1 := TO_REAL(sExtraction_Value);
        4: // 2nd value
            JMP FORMAT_ERROR;
        5: // 3td value
            JMP FORMAT_ERROR;
        ELSE
            JMP FORMAT_ERROR;
        END_CASE
        
    END_WHILE
    diNumOfDataLines := diNumOfDataLines + 1;
    
END_WHILE
xOverFlow    := FALSE;
FC_ExtractCsvData_D_T := TRUE;

RETURN;
FORMAT_ERROR:
    FC_ExtractCsvData_D_T := FALSE;
 
Das ist dann gegenüber Beckhoff TwinCAT und anderen Codesys Derivaten echt ein Pluspunkt, da sind String Funktionen immer auf 255 Zeichen begrenzt.
WAGO verwendet die native CODESYS, womit die Standard String-Funktionen auch auf 255 Zeichen begrenzt sind. Aber in der Bibliothek WagoAppString sind Funktionen, die keine Limitierung haben und welche, die über den änderbaren Parameter begrenzt sind.
 
WAGO verwendet die native CODESYS, womit die Standard String-Funktionen auch auf 255 Zeichen begrenzt sind. Aber in der Bibliothek WagoAppString sind Funktionen, die keine Limitierung haben und welche, die über den änderbaren Parameter begrenzt sind.
Danke, habe jetzt mal einen Blick in die Doku geworfen. Echt genial das Teil, das da keiner vorher auf die Idee gekommen ist.
Auch die Befehlsvarianten, zum Beispiel für CONCAT mit 3, 4, 5, usw Strings sind klasse.
 
Das eigentliche Problem liegt komischerweise in den Real-Werten. Da bekomme ich im Moment nur zwei. Es waren mal mehr (bis zur 10)
Hier wird weder das Token $R$N noch eChar.LineFeed (zuverlässig ;-) erkannt.
 
Zuviel Werbung?
-> Hier kostenlos registrieren
Schau Dir doch mal die CSV als ASCII-Bytes an. Dann bekommst Du einen guten Überblick welche nicht darstellbaren Zeichen enthalten sind.

Code:
ptsText            : POINTER TO STRING(1);
ptabASCII        : POINTER TO ARRAY[0..1024] OF BYTE;
-----------------
ptsText := ADR(sText);
ptabASCII := ptsText;
 
Vielen Dank erst mal.
die WagoTypes.eChar sind Klasse, nur konnte ich keinen Fehler inder csv finden.
Der Fehler liegt immer noch in MAX_STRING_LENGTH. Den hab ich auf 2002 geändert.
WagoAppString.StrExtractToken sucht aber nur bis 255.
 
Zuviel Werbung?
-> Hier kostenlos registrieren
Vielen Dank erst mal.
die WagoTypes.eChar sind Klasse, nur konnte ich keinen Fehler inder csv finden.
Der Fehler liegt immer noch in MAX_STRING_LENGTH. Den hab ich auf 2002 geändert.
WagoAppString.StrExtractToken sucht aber nur bis 255.
Zeig doch bitte einmal Deinen Aufruf von StrExtractToken. Laut Doku ist die einzige Begrenzung, dass das zu findende Token nicht größer als 254 Zeichen sein darf.
 
ich häng das Programm im aktuellen Zusatand mal dran. Bis 12 Zeilen gehts. Ab 13 fehlen die Zeichen. (Visu)
 

Anhänge

  • CSV_Projekt_02.zip
    99,3 KB · Aufrufe: 5
Ich habe mir Dein Programm mal angesehen.
Bei einer Funktion behalten Variablen ihren Wert nur während der Ausführung, wenn es also erforderlich ist, dass Du Deine Funktion mehrfach aufrufst kannst Du nicht auf "alte" Werte zugreifen. Das ist Dir bewusst?
Die WHILE Schleife ist sehr bedenklich. Ein SPS-Programm wird zyklisch ausgeführt, wenn die WHILE Schleife also zu lange braucht kommt es zu Problemen. Gibt es eventuell eine Fehlermeldung?
Bei der Nutzung von Schleifen in SPS-Programmen muss man genau wissen was man tut.
 
Zuletzt bearbeitet:
Zuviel Werbung?
-> Hier kostenlos registrieren
Wie bereits gesagt hat die WagoAppString.StrExtractToken keine Begrenzung auf 255 oder einen Parameter. Nur der Return der Funktion ist auf den Parameter MAX_STRING_LENGTH (Default = 255) begrenzt. Deine Zeilen sind aber deutlich darunter. Du kannst die Funktion also verwenden und must nichts anpassen.

Die Funktion extrahiert bereits den relevanten Teil und Du must nicht anschließend noch mit MID arbeiten.
Dein Fehler liegt aber in der Verkettung der StrExtractToken. Nachdem Du immer in einer Zeile nach dem Komma suchst, musst Du das Positionsergebnis NEXT als START für den nächsten Aufruf verwenden.

Die Verwendung der FOR-Schleife mit statischer Zeilenzahl finde ich zwar merkwürdig, aber so soll es eben sein.

Anmerkung: Du mischst bei den Variablenbezeichnungen Deutsch und Englisch, verwendest Präfixe für Datentypen nicht konsistent und verwendet CamelCase, mal SnakeCase und mal mischt du es. Einrücken und generelle Formatierung von Code ist auch nicht Deine Stärke. In Summe nicht gerade förderlich zum Lesen Deines Codes und für Dich auch unübersichtlich bei der Fehlersuche.

Habe Deine Code überarbeitet und kommentiert. Sollte jetzt gehen. Hättest aber nur meine Funktion von oben nehmen brauchen, die macht das gleiche.
 

Anhänge

  • CSV_Projekt_02_KLM.zip
    3 KB · Aufrufe: 2
Die WHILE Schleife ist sehr bedenklich. Ein SPS-Programm wird zyklisch ausgeführt, wenn die WHILE Schleife also zu lange braucht kommt es zu Problemen.
Er verwendet die FOR-Schleife. Die WHILE-Schleife ist mein Vorschlag. Und ja, das sollte einem bewusst sein, aber gerade bei STRING-Verarbeitungen ist die WHILE immer wieder recht nützlich.
 
Code:
PROGRAM prgKLM
VAR
    sFileName                 : STRING := '/media/sd/forecast.csv';
    oReadCfg                : WagoAppFileDir.FbReadWholeFile_cpt;
    sReadErrorInfo            : STRING;
    xRead                    : BOOL;
    xReadError                : BOOL;
    
    sCfg                    : STRING(2000);
    sLine                    : STRING;
    sDate                    : STRING;
    sTime                    : STRING;
    sValue                    : STRING;
    
    // oToken_sperator             :  WagoAppString.StrExtractToken;
    udiPosStart_Zeile        : UDINT;
    udiPosNext_Zeile        : UDINT;
    udiPosStart_Time        : UDINT;
    udiPosStart_Value        : UDINT;

    sZeileEnde                : STRING := '$R$N';
    
    iZeile                    : INT;
    
    iAnzahlZeilen_in        : INT := 18;
    xLadeVisuVonCsv            : BOOL;
END_VAR




(*

1. WagoAppFileDir.FbReadWholeFile_cpt
Den Eingang udiRxBufferSize belegst Du mit einem ADR(sReadBuffer) String in ausreichender Größe, z.B. STRING(2048).
Der Eingang udiRxNBytes ist dann nur diese Größe, also SIZEOF(sReadBuffer).
2. WagoAppString.StrExtractToken in einer While-Schleife.
Hiermit extrahierst Du erst eine ganze Zeile der CSV indem Du als bSeparator WagoTypes.eChar.LineFeed verwendest.
Dann nimmst Du die die gleiche Funktion um Zeitstempel und den Prozesswert zu trennen, also als bSeparator WagoTypes.eChar.Semicolon.
3. Umwandeln des Zeitstempels und in ein Array deines Datentyps aus DT und INT ablegen.
Umwandlung z.B. mit WagoAppString.FuStringToDT(bFormat:=3, sInput:= '02.05.2024 01:15')

*)
----------------------------------------------------

oReadCfg(
    xTrigger        := xRead,
    sName            := sFileName,
    pRxBuffer        := ADR(sCfg),
    udiRxBuffersize    := SIZEOF(sCfg) );
sReadErrorInfo := oReadCfg.oStatus.GetDescription();

// Simulation
sCfg := gvlSimulation.sCsvContent;

IF xLadeVisuVonCsv  THEN
    udiPosStart_Zeile := 1;
    FOR iZeile := 1 TO iAnzahlZeilen_in  DO // immer statische Anzahl Zeilen - was wenn es mehr oder weniger in der CSV sind?!
                    
        // CSV Zeile einlesen bis Umbruch
        sLine := WagoAppString.StrExtractToken(
                    sBuffer        := sCfg,
                    udiSize        := SIZEOF(sCfg),
                    bSeparator    := eChar.LineFeed,
                    udiBegin    := udiPosStart_Zeile,
                    udiNext        => udiPosNext_Zeile);
        
        // nicht erforderlich, da bereits als Return von StrExtractToken verfügbar
        //sLine := MID(sCfg, TO_INT(udiNextCSV - udiStartCSV), TO_INT(udiStartCSV)); // Text aus Zeile in String zum weiterzuverarbeiten
        
        // Datum bis ertses Komma in Zeile   
        sDate := WagoAppString.StrExtractToken(
                    sBuffer        := sLine,
                    udiSize        := SIZEOF(sLine),
                    bSeparator    := eChar.Comma,
                    udiBegin    := 1,
                    udiNext        => udiPosStart_Time);
    
        // nicht erforderlich, da bereits als Return von StrExtractToken verfügbar
        // udiDatumEnde := udiNextZ;                                 
        // sDate := MID(sLine, UDINT_TO_INT( udiNextZ -2 ), UDINT_TO_INT(udiStartZ ));
        
        // Datum-STRING in DATE    wandeln und in Array speichern (Zwischenvariable nicht erforderlich)       
        sDate := CONCAT('D#',sDate);
        GVL.ar_zeit_soll_visu[iZeile].d_DATE_0 := TO_DATE(sDate);
        
        // Uhrzeit zwischen 1. und 2. Komma
        sTime := WagoAppString.StrExtractToken(
                    sBuffer        := sLine,
                    udiSize        := SIZEOF(sLine),
                    bSeparator    := eChar.Comma,
                    udiBegin    := udiPosStart_Time,
                    udiNext        => udiPosStart_Value) ;           
                                        
        // nicht erforderlich, da bereits als Return von StrExtractToken verfügbar
        // udiZeitEnde     := udiNextZ;
        // sTime := MID(sLine, UDINT_TO_INT(udiNextZ - udiDatumEnde -1  ), UDINT_TO_INT(udiDatumEnde));            // -1 wegen Komma
        
        // Zeit-STRING in TIME_OF_DAY wandeln und in Array speichern (Zwischenvariable nicht erforderlich)   
        sTime    := CONCAT('TOD#',sTime);
        GVL.ar_zeit_soll_visu[iZeile].dt_time_0 := TO_TOD(sTime);
        
        // Wert zwischen Zeit Ende und Zeilenende
        sValue := WagoAppString.StrExtractToken(
                    sBuffer        := sLine,
                    udiSize        := SIZEOF(sLine),
                    bSeparator    := TO_BYTE(sZeileEnde),
                    udiBegin    := udiPosStart_Value,
                    udiNext        => );
                                                                            
        // nicht erforderlich, da bereits als Return von StrExtractToken verfügbar
        // sValue := MID(sLine, UDINT_TO_INT(udiNextZv - udiZeitEnde ), UDINT_TO_INT(udiZeitEnde));        //  Länge des Wertes spielt keine Rolle - bis ENDE Zeiel
        // rArbeit:= FPU.StrToReal(sValue);        // STRING ZU Real
        
        // Wert-STRING in REAL wandeln und ins ARRAY VISU speichern (Zwischenvariable nicht erforderlich)   
        GVL.ar_zeit_soll_visu[iZeile].r_arbeit := TO_REAL(sValue);
        
        udiPosStart_Zeile := udiPosNext_Zeile;   
                                
    END_FOR        // CSV Zeile
    
    xLadeVisuVonCsv := FALSE;
END_IF

Code:
gvlSimulation
--------------------
{attribute 'qualified_only'}
VAR_GLOBAL
    sCsvContent : STRING(3000) :=
'2024-05-11,04:00,0.1
2024-05-11,05:00,0.2
2024-05-11,06:00,0.3
2024-05-11,07:00,0.4
2024-05-11,08:00,8
2024-05-11,09:00,9
2024-05-11,10:00,10
2024-05-11,11:00,11
2024-05-11,12:00,12
2024-05-11,13:00,13
2024-05-11,14:00,14
2024-05-11,15:00,15
2024-05-11,16:00,16
2024-05-11,17:00,17
2024-05-11,18:00,18
2024-05-11,19:00,19
2024-05-11,20:00,20
2024-05-11,21:00,21';
END_VAR
 
Vielen Dank an Alle - besoders KLM! Habs erst mal eingebaut - und geht. "Schöner Code"
Jetzt versuche ich nur nich die csv direkt von der Festplatte in die Visu zu bekommen.

Danke!
 
Zurück
Oben