如何在 TwinCAT 中以 Excel 或 CSV 格式保存来自 PLC 的数据?

问题描述 投票:0回答:2

我必须从 Beckhoff PLC 管理的模拟器中记录 JobNo、处理时间、JobID、过程编号等数据,并将这些数据以 CSV 或 excel 文件的形式写入。 例如,如果 JobNo 为 1,则在处理完所有数据后,应将其记录在相应的列下,当遇到下一个作业时,将数据记录在下一行。以下应该是 excel 文件的样子

职位号 处理时间 职位编号 工艺编号
1 20 100 200
2 40 101 320

到目前为止,我已经关注了 Beckhoff 的在线资源,甚至联系了他们的支持团队,但是我从他们那里得到的代码导致了某种错误。我也在一些论坛上写下了我的问题,但我在那里得到的答案太复杂和高级了。如果你能提供一个文件写入程序的例子会很有帮助 以下是我为此目的使用的所有 DUT 和 GVL 以及主要代码: 名为 ST_data

的 DUT
TYPE ST_data :
STRUCT
    JOB_ID          : STRING(255);
    Processing_Time : STRING(255);
    Setup_Time      : STRING(255);
    Due_Date        : STRING(255);
END_STRUCT
END_TYPE

GVL 名为 GVL

{attribute 'qualified_only'}
VAR_GLOBAL
    MACHINE : ARRAY[1..50] OF ST_data;
    sen5 : BOOL;
    sen2 : BOOL;
    state : BOOL;
    count :INT;
    hit: BOOL;
    tdelta : ULINT;
    tstart : ULINT;
    tend : ULINT;
    setuptime : LREAL;
END_VAR

MAIN 程序代码 局部变量--------------------------------------------

PROGRAM MAIN
VAR

    FB_FileOpen: FB_FileOpen;
    FB_FileWrite: FB_FileWrite;
    FB_FileClose: FB_FileClose;
    
    hFile: UINT;
    sPathName: T_MaxString;
    sWriteBuffer : STRING(5000);
    sBufferTemp : STRING(1000); // Temporary string that will hold the string that needs to be added to the full string
    bBuffTemp : BOOL ; //check if the strings have been concatinated
    sFormat : STRING(255);
    
    state: INT;
    bWrite: BOOL;
    NT_GetTime: NT_GetTime;
    
    bFill: BOOL;
    i: INT;
    FB_FormatString2: FB_FormatString2;
    
    trigger: BOOL;
END_VAR

代码-------------------------------------------- ------------------

//counter code
//==========================================================================================
IF GVL.sen5 = FALSE AND GVL.hit = FALSE THEN
    GVL.count := GVL.count +1;
    GVL.hit := TRUE;
ELSIF GVL.sen5 = TRUE AND GVL.hit = TRUE THEN
    GVL.hit := FALSE;
END_IF

IF gvl.sen5= TRUE THEN
    GVL.tstart := F_GetSystemTime();
END_IF
IF GVL.sen2 = TRUE THEN
    GVL.tend := F_GetsystemTime();
    //trigger := TRUE; // this will trigger the file writing commands
END_IF
GVL.tdelta := GVL.tend - GVL.tstart;
GVL.setuptime := GVL.tdelta*EXPT(10,-7);
//Entering some value in array
//==========================================================================================
IF bFill THEN
    FOR i := 1 TO 50 BY 1 DO
        GVL.MACHINE[i].Due_Date :='10823'; //WORD_TO_STRING(NT_GetTime.TIMESTR.wDayOfWeek);
        GVL.MACHINE[i].JOB_ID := INT_TO_STRING(GVL.count) ;//INT_TO_STRING(i);
        GVL.MACHINE[i].Processing_Time := '125';//WORD_TO_STRING(NT_GetTime.TIMESTR.wMinute);
        GVL.MACHINE[i].Setup_Time := LREAL_TO_STRING(GVL.setuptime);//WORD_TO_STRING(NT_GetTime.TIMESTR.wSecond);
    END_FOR
    bFill := FALSE;
END_IF


//Functioin block to get local time
//===========================================
IF NT_GetTime.start AND NOT NT_GetTime.BUSY THEN
    NT_GetTime.START :=FALSE;
ELSE
    NT_GetTime.START := TRUE;
END_IF
NT_GetTime(
    NETID:='' , 
    START:= , 
    TMOUT:= , 
    BUSY=> , 
    ERR=> , 
    ERRID=> , 
    TIMESTR=> );
//CASE STATEMENT to handle writing
//===========================================
CASE state OF
    0:
        IF bWrite THEN // switch to true or false to control the execution
            State :=5;
            bWrite := FALSE;
        END_IF
    5:// creating the file 
        sPathName := CONCAT('C:\Users\Manjot Sanghera\Desktop\CSVRECORD',WORD_TO_STRING(NT_GetTime.TIMESTR.wDay));
        sPathName := CONCAT(sPathName,'.');
        sPathName := CONCAT(sPathName,WORD_TO_STRING(NT_GetTime.TIMESTR.wHour));
        //sPathName := CONCAT(sPathName,'.');
        //sPathName := CONCAT(sPathName,WORD_TO_STRING(NT_GetTime.TIMESTR.wMinute));
        sPathName := CONCAT(sPathName,'_TestFile.csv');
        State := 10;
        FB_FileOpen.bExecute := TRUE;
        
    10:
        FB_FileOpen.bExecute := TRUE;
        IF NOT FB_FileOpen.bBusy AND NOT FB_FileOpen.bError THEN
            FB_FileOpen.bExecute := FALSE;
            State := 15;
        END_IF
    15 :
        sWriteBuffer :='Due_Date, Job_ID, Processin_Time,Setup_Time $n';
        sFormat :='%s, %s, %s, %s $n';
        FOR i := 1 TO 50 BY 1 DO // loop is required so that you can loop through each line/row/arrayelement
            FB_FormatString2(
                pFormatString:=  ADR(sFormat), 
                arg1:= F_STRING(GVL.MACHINE[i].Due_Date), 
                arg2:= F_STRING(GVL.MACHINE[i].JOB_ID), 
                arg3:= F_STRING(GVL.MACHINE[i].Processing_Time), 
                arg4:= F_STRING(GVL.MACHINE[i].Setup_Time),  
                pDstString:= ADR(sBufferTemp) , 
                nDstSize:= SIZEOF(sBufferTemp), 
                bError=> , 
                nErrId=> );
            bBuffTemp := CONCAT2(   pSrcString1 := ADR(sWriteBuffer),//main string 
                                    pSrcString2 := ADR(sBufferTemp), //String to be added
                                    pDstString  := ADR(sWriteBuffer),//destinatioin of the string
                                    nDstSize := SIZEOF(sWriteBuffer));//size of the final string
        END_FOR
        //sWriteBuffer := 'Job_ID__|__Processing_Time__|__SetupTime';
        State := 20;
        FB_FileWrite.bExecute := TRUE;
    20:
        FB_FileWrite.bExecute := TRUE;
        IF NOT FB_FileWrite.bBusy AND NOT FB_FileWrite.bError THEN
            FB_FileWrite.bExecute := FALSE;
            State := 30;
            FB_FileClose.bExecute := TRUE;
        END_IF
    30:
        IF NOT FB_FileClose.bBusy AND NOT FB_FileClose.bError THEN
            FB_FileClose.bExecute := FALSE;
            State:= 0;
        END_IF      
END_CASE

//OPEN, WRITE, CLOSE FILE
//===========================================
FB_FileOpen(
    sNetId:= '', 
    sPathName:= sPathName , 
    nMode:= FOPEN_MODEWRITE OR FOPEN_MODEPLUS, 
    ePath:= PATH_GENERIC, 
    bExecute:= , 
    tTimeout:= , 
    bBusy=> , 
    bError=> , 
    nErrId=> , 
    hFile=> hFile);
    
FB_FileWrite(
    sNetId:= '', 
    hFile:= hFile, 
    pWriteBuff:= ADR(sWriteBuffer) , 
    cbWriteLen:= SIZEOF(sWriteBuffer), 
    bExecute:= , 
    tTimeout:= , 
    bBusy=> , 
    bError=> , 
    nErrId=> , 
    cbWrite=> );
    
FB_FileClose(
    sNetId:= '', 
    hFile:= hFile  , 
    bExecute:= , 
    tTimeout:= , 
    bBusy=> , 
    bError=> , 
    nErrId=> );

程序的预期功能 counter 对对象进行计数,并将 JobID、Process No 和其他值的值填充到表的第一行中,并且仅在第二个作业的这些值可用后才起作用

plc twincat
2个回答
1
投票

根据评论,这是一个程序,其中包含用于写入文件的 FB 和实现演示以将新数据发送到文件的 MAIN。数据以 CSV 格式写入。您可以修改要写入文件的内容,什么文件,数据格式是什么,......我希望这应该足以解决问题。

由于你是新手,这里是文件夹结构的图像,因为我使用了方法。以下是关于 Object Oriented Programming with TwinCAT 3

的更多信息

文件写入功能块实现: 变量声明:

FUNCTION_BLOCK FB_FileWriter
VAR_INPUT
    sFilePath   : T_MaxString;
    sFileName   : T_MaxString;
    sNetId      : STRING;   // NetId of the target, leave empty for local
END_VAR
VAR
    fbFileOpen  : FB_FileOpen;
    fbFilePuts  : FB_FilePuts;
    fbFileClose : FB_FileClose;
    nFileHandle : UINT;
    
    arrBuffer   : ARRAY[0..100] OF T_MaxString;
    eFileWriteState : (IDLE, OPEN_FILE, WRITE_TO_FILE, CLOSE_FILE, ERROR);
END_VAR

身体:

CASE eFileWriteState OF
    IDLE:
        // Make sure all the file writing FBs are ready for use (trigger on execute)
        Init();
        
        // We have pending data to be written
        IF arrBuffer[0] <> '' THEN
            eFileWriteState := OPEN_FILE;
        END_IF
        
    OPEN_FILE:
        // Opens a file for writing at the end of the file (append). 
        // If the file does not exist, a new file is created.
        fbFileOpen(
            bExecute    := TRUE,
            sNetId      := sNetId,
            sPathName   := CONCAT(sFilePath, sFileName),
            nMode       := FOPEN_MODEAPPEND);
            
        IF fbFileOpen.bError THEN
            eFileWriteState := ERROR;
        ELSIF NOT fbFileOpen.bBusy THEN
            nFileHandle := fbFileOpen.hFile;
            fbFileOpen(bExecute := FALSE);
            eFileWriteState := WRITE_TO_FILE;
        END_IF
    
    WRITE_TO_FILE:
        fbFilePuts(
            bExecute    := TRUE,
            sNetId      := sNetId,
            hFile       := nFileHandle,
            sLine       := arrBuffer[0]);
        IF fbFilePuts.bError THEN
            eFileWriteState := ERROR;
        ELSIF NOT fbFileClose.bBusy THEN
            // Sucess, data was written
            // Rotate the buffer and close the file
            RotateBuffer();
            eFileWriteState := CLOSE_FILE; 
        END_IF
    
    CLOSE_FILE:
        fbFileClose(
            bExecute := TRUE,
            sNetId      := sNetId,
            hFile       := nFileHandle);
            
        IF fbFileClose.bError THEN
            eFileWriteState := ERROR;
        ELSIF NOT fbFileClose.bBusy THEN
            nFileHandle := 0;
            eFileWriteState := IDLE;
        END_IF      
    
    ERROR:
        // Error, clear the handle and go back to idle
        nFileHandle := 0;
        eFileWriteState := IDLE;
    END_CASE

方法:

初始化所有FB的方法:

METHOD PRIVATE Init : BOOL

fbFileClose(bExecute := FALSE);
fbFileOpen(bExecute := FALSE);
fbFilePuts(bExecute := FALSE);

向缓冲区插入新数据的方法

METHOD PRIVATE InsertToBuffer
VAR_INPUT
    value   : STRING;
END_VAR
VAR
    nBufferIndex    : INT;
END_VAR

FOR nBufferIndex := 0 TO 100 BY 1 DO
    IF arrBuffer[nBufferIndex] = '' THEN
        // We found the free spot in the buffer, insert the value to this place
        arrBuffer[nBufferIndex] := value;
        EXIT;
    END_IF
END_FOR

当前数据片写入后轮换缓冲区中数据的方法。

METHOD PRIVATE RotateBuffer

VAR
    nIndex  : int := 0;
END_VAR

FOR nIndex := 0 TO 99 BY 1 DO
    // We can exit when we reached the empty slot in the buffer
    IF arrBuffer[nIndex] = '' THEN
        EXIT;
    END_IF
    // FIFO, removing first element in the array copying the next, etc...
    arrBuffer[nIndex] := arrBuffer[nIndex+1];
END_FOR

被调用以写入目标文件的公共方法

// Write to the destination file
METHOD WriteToFile : BOOL
VAR_INPUT
    message : T_MaxString;
END_VAR

InsertToBuffer(value := message);

结构定义:

TYPE ST_MyData :
STRUCT
    Id          : DINT;
    Value       : DINT;
    Timestamp   : T_MaxString; 
END_STRUCT
END_TYPE

主要变量减速:

PROGRAM MAIN
VAR
    fbWriteFile     : FB_FileWriter;
    
    arrTestData     : ARRAY[0..10] OF ST_MyData;
    nIteration      : INT;
    bStartNewJob    : BOOL;
    fbFormatString  : FB_FormatString;
    
    fbGetCurrentTaskIndex   : GETCURTASKINDEX;
END_VAR

VAR CONSTANT
    cHeader : STRING := 'ID;VALUE;TIMESTAMP';
END_VAR

身体:

fbWriteFile(
    sFileName   := 'Test.txt',
    sFilePath   := 'C:\',
    sNetId      := '');
    
// Handle this differently, but just creating the header on first PLC cycle for demo purpose
fbGetCurrentTaskIndex();
IF _TaskInfo[fbGetCurrentTaskIndex.index].FirstCycle THEN
    fbWriteFile.WriteToFile(message := cHeader);
END_IF
    
IF bStartNewJob THEN
    // Making up some fake data to store it
    arrTestData[nIteration].Id := nIteration;                   // just some made up id
    arrTestData[nIteration].Value := 5*nIteration;              // just some made up value
    arrTestData[nIteration].Timestamp := '2023.27.04_10:00';    // just some fake timestamp
    
    // Create the full line by using the FB_FormatString, way easier than CONCAT
    fbFormatString(
        sFormat := '%d;%d;%s$n',
        arg1    := F_DINT(arrTestData[nIteration].Id),
        arg2    := F_DINT(arrTestData[nIteration].Value),
        arg3    := F_STRING(arrTestData[nIteration].Timestamp));

    // Send the created line to the file writer
    fbWriteFile.WriteToFile(message := fbFormatString.sOut);

    // Increasing the counter for new, different data
    IF nIteration <10 THEN
        nIteration := nIteration + 1;
    ELSE
        nIteration := 0;
    END_IF
    
    // And finally, clear the trigger
    bStartNewJob := FALSE;      
END_IF

最终结果。

第一次循环后的初始状态,创建了标题:


0
投票

作为替代方案,您可以尝试这个来自 benhar 的 CSV 编写器。你可以从example看到如何使用它:

PROGRAM ExampleApplication
VAR
    // application
    fileName : STRING := 'C:\myLog.csv';
    targetLogCount : UDINT := 10000; // how many records will be record in the logging session.
    loggingState: (IDLE, INITIALISE, CREATE_HEADER, CYCLIC_LOG_VALUES, COMPLETE_LOGGING) := IDLE;
    maxiumumBufferUsage : UDINT; // you must check this value to make sure it does not exceed GVL_ByteBufferConstants.FILE_WRITE_BUFFER_SIZE
    
    {attribute 'hidden'}
    fileBasedByteBuffer : FileBasedByteBuffer;
    {attribute 'hidden'}
    csvDocumentWriter : CsvByteDocumentWriter(FileBasedByteBuffer);
    {attribute 'hidden'}
    xfcChannel: INT;
    // example data
    SensorName : STRING := 'Sensor1';
    SerialNumber : INT := 123;
    SensorActive : BOOL;
    SensorValue : ARRAY [0..49] OF INT := [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50];
    
    bDutDefined: BOOL;
END_VAR

//  Set logging state to INITIALISE to run the example code. 
CASE loggingState OF
        
    IDLE: 
        // The code will wait here.  You must manually set the loggingState to INITIALISE using the online write values.
    
    INITIALISE:
    
        maxiumumBufferUsage := 0; // this example tracks the maximum buffer usage.  The default buffer size is 200KB.
        
        // The csv document requires a buffer to save the data to.  
        // Therefore before starting to write csv records we must enable the buffer by calling start, plus the file name. 
        
        IF fileBasedByteBuffer.Start(fileName) THEN
            loggingState := CREATE_HEADER;
        END_IF
        
    CREATE_HEADER: 
        
        // This section creates your header information.
        // The information will be stored as bytes.  This example also shows the conversion of an int to string. 
        csvDocumentWriter
            .StartRecord()
            .AddStringLiteral('Sensor Name')
            .AddString(SensorName)
            .EndRecord();   
        csvDocumentWriter
            .StartRecord()
            .AddStringLiteral('Sensor Serial Number')
            .AddInt(SerialNumber)
            .EndRecord();
        csvDocumentWriter
            .StartRecord()
            .AddStringLiteral('Column 1')
            .AddStringLiteral('Column 2')
            .AddStringLiteral('Column 3')
            .AddStringLiteral('Column 4')
            .AddInt(SerialNumber)
            .EndRecord();
        loggingState := CYCLIC_LOG_VALUES;
                
    CYCLIC_LOG_VALUES:
            
        // This is the logging section of the code.  You can either make a single record per cycle, or 
        // record multiples based on the array given by our XFC terminals.  In this example I save 50 XFC values plus a few other details
        // Remember, the data is stored as binary.  Use xxx_TO_STRING converters to provide strings. 
        FOR xfcChannel := 0 TO 49 DO   
                csvDocumentWriter
                    .StartRecord()
                    .AddBool(SensorActive)
                    .AddBoolLiteral(TRUE)
                    .AddInt(SensorValue[xfcChannel]) 
                    .AddIntLiteral(0)
                    .AddStringLiteral('Logging')
                    .EndRecord();
                    
        END_FOR
        
        // This controls how many records are written.  You can either use a timer countdown or a record count.  
        // in this example I check the records created and once I have over 300 (including the header) then I complete.
        IF csvDocumentWriter.RecordCount > targetLogCount THEN
            loggingState := COMPLETE_LOGGING;
        END_IF
        
    COMPLETE_LOGGING: 
        
        // You must stop the file based buffer to close and release the file.  Once the buffer has stopped we can then finish
        // by resetting the csvDocument.  This resets the internal counters.
        fileBasedByteBuffer.Stop();
        
        IF fileBasedByteBuffer.IsStopped THEN
            csvDocumentWriter.Reset();
            loggingState := IDLE;
        END_IF
    
END_CASE
// You must cyclic call this fileBasedByteBuffer to give it procesing time.  There is no requirement to call the csvDocumentWriter as this is 
// all event based. 
fileBasedByteBuffer();
// maxiumumBufferUsage must remain under GVL_ByteBufferConstants.FILE_WRITE_BUFFER_SIZE.  If it exceeds then you will drop values.  In this instance
// make GVL_ByteBufferConstants.FILE_WRITE_BUFFER_SIZE larger. 
IF fileBasedByteBuffer.Size > maxiumumBufferUsage THEN
    maxiumumBufferUsage := fileBasedByteBuffer.Size;
END_IF
© www.soinside.com 2019 - 2024. All rights reserved.