FUNCTION_BLOCK "CP340_Poll"
TITLE='485 Polling'
VERSION:'0.9'
KNOW_HOW_PROTECT
AUTHOR:Goosy
NAME:SC
FAMILY:GoosyLib

VAR_INPUT
    module_addr : INT;           // Module address
    try_times : INT := 5;        // The maximum number of inquiry failures for a device, if the number exceeds this, the data of the device is marked as invalid.
    retry_times : INT := 50;     // The number of inquiry failures a device must experience before retrying
    data : ANY;                  // The data areas of the polling defines
    data_P AT data : STRUCT      // Define ANY structure
        SyntaxID: BYTE;
        DataType: BYTE;
        DataCount: INT;
        DB_Nummer: INT;
        Byte_Pointer: DWORD;
    END_STRUCT;
END_VAR

VAR_IN_OUT
    reset : BOOL := TRUE;        // Reinitialize
END_VAR

VAR
    send_trigger : BOOL ;        // send trigger
    recv_trigger : BOOL := TRUE; // receive trigger
    is_modbus : BOOL;            // Whether it is modbus protocol
    is_polling : BOOL ;          // In the middle of a single inquiry
    poll_enable : BOOL;          // Polling enable flag
    continuous : BOOL;           // Whether to inquire continuously
    periodicity : BOOL;          // Whether to inquire periodically
    custom_trigger : BOOL;       // User-defined trigger
    poll_index : INT;            // Which round of inquiry
    poll_length : INT;           // Total number of inquiries
    poll_base : INT;             // Start offset of the polling area
    poll_DB : INT;               // Polling DB number
    w_poll_DB AT poll_DB: WORD ;
    send_DB : INT;               // Current polling send block number
    w_send_DB AT send_DB: WORD ;
    send_start : INT;            // Current polling send offset
    send_length : INT;
    recv_DB : INT;               // Current polling receive block number (i.e. current send corresponding receive block)
    w_recv_DB AT recv_DB: WORD ;
    recv_start : INT;            // Current polling receive offset
    recv_length : INT;
    timeout : DINT;              // Maximum waiting time for a single inquiry, timeout means failure and switch to the next inquiry
    PT AT timeout : TIME;
    send : P_SEND;
    receive : P_RCV;
    time_over : TON;
END_VAR

VAR_TEMP
    poll_pause : BOOL;           // Polling pause flag
    poll_start : BOOL;           // Send rising edge
    poll_end : BOOL;             // Send falling edge
    on_receive : BOOL;           // Receive success flag
    request : BOOL;              // Inquiry request
    tmp_int : INT;
    offset : INT;                // Polling offset
    send_byte : BYTE;
    recv_byte : BYTE;
    CRC_H : BYTE;
    CRC_L : BYTE;
END_VAR

BEGIN
    // Initialize the polling list
    IF reset THEN
        IF (data_P.DB_Nummer = 0) // DB number cannot be 0
            OR (data_P.DataType <> B#16#2) // Must be a byte type
            OR ((data_P.Byte_Pointer AND DW#16#ff000000) <> DW#16#84000000)
        THEN
            RETURN;
        END_IF;

        poll_DB := data_P.DB_Nummer;
        poll_base := DWORD_TO_INT(SHR(IN := data_P.Byte_Pointer, N := 3));
        poll_length := data_P.DataCount/16;

        FOR tmp_int := poll_length - 1 TO 0 BY -1 DO
            offset := poll_base + tmp_int * 16;
            WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 1] := FALSE; // poll_pause
            WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 14] := 0; //waitCount
            w_send_DB := WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 4];
            IF send_DB = 0 THEN // Default send_DB is the poll_DB
                WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 4] := w_poll_DB;
            END_IF;
        END_FOR;
        reset := FALSE;

        // just for the first polling
        poll_index := tmp_int;
        w_recv_DB := WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 8];
        recv_start := WORD_TO_INT(WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 10]);
    END_IF;

    // Check request rising edge to set custom_trigger
    FOR tmp_int := 0 TO poll_length - 1 BY 1 DO
        offset := poll_base + tmp_int * 16;
        IF WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 2] OR WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 3] THEN
            CONTINUE; // continuous OR periodicity is false, skip this polling
        END_IF;
        request := WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 6];
        IF request AND NOT WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 7] THEN
            WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 5] := TRUE; // custom_trigger
        END_IF;
        WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 7] := request;
    END_FOR;

    // Range check
    IF poll_index < 0 OR poll_index >= poll_length THEN
        poll_index := 0;
    END_IF;
    offset := poll_index * 16 + poll_base;

    // Timeout TON
    time_over(IN := is_polling, PT := PT);

    // handle polling
    // polling lifetime: poll_start - poll_end
    poll_end := NOT periodicity AND (receive.NDR OR receive.ERROR) OR time_over.Q;
    poll_start := FALSE;
    IF poll_end THEN
        // End the current inquiry, ensuring a cycle time to process status
        // Also ensures time_over reset and poll_end reset after this cycle
        is_polling := FALSE;
        // Reset send trigger
        send_trigger := FALSE;
    ELSIF NOT is_polling THEN // Start a new round of inquiry when is_polling is false
        poll_start := TRUE;
        send_trigger := FALSE;
    ELSIF continuous OR periodicity OR custom_trigger THEN
        // Set send trigger in the next cycle after poll_start
        // User-defined trigger custom_trigger is set by external or request rising edge
        // custom_trigger uses only the value at poll_start
        send_trigger := TRUE;
    END_IF;

    // Handle new round
    IF poll_start THEN
        // handle previous round to end
        // Increment the counter which will be cleared when receiving correct data.
        // It must be incremented here because poll_end may not exist in the current round.
        tmp_int := WORD_TO_INT(WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 14]) + 1;
        IF NOT poll_enable THEN
            WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 14] := W#16#0; //wait_count
        ELSIF continuous OR periodicity OR custom_trigger THEN
            IF tmp_int < 0 THEN
                tmp_int := 0; // Avoid underflow
            ELSIF tmp_int > retry_times THEN // Retry after exceeding retry_times
                WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 1] := FALSE;   // poll_pause
                // Allow access to this poll once
                tmp_int := try_times; // Set to try_times - 1 to allow access once
            ELSIF tmp_int > try_times THEN // Inquire failure times exceed the try_times value
                WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 1] := TRUE;   // poll_pause
                WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 0] := FALSE; // Set device abnormal flag
                WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 1] := TRUE;  // Set device invalid flag
                WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 2] := FALSE; // Set device not received flag
            END_IF;
            WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 14] := INT_TO_WORD(tmp_int);
        END_IF;
        // Reset the receive flag
        WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 2] := FALSE; // Set the device receive flag

        // start new round
        poll_index := (poll_index + 1) MOD poll_length;
        offset := poll_index * 16 + poll_base;
        poll_pause := WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 1];

        // the data below is only prepared when a new round starts
        poll_enable := WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 0];
        continuous := WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 2];
        periodicity := WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 3] AND NOT continuous;
        is_modbus := WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 4];
        custom_trigger := WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 5];
        IF custom_trigger THEN // custom_trigger is reset only at poll_start
            WORD_TO_BLOCK_DB(w_poll_DB).DX[offset, 5] := FALSE; // Reset custom_trigger
        END_IF;
        timeout := WORD_TO_DINT(WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 2]);
        w_send_DB := WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 4];
        send_start := WORD_TO_INT(WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 6]) + 2;
        send_length := WORD_TO_INT(WORD_TO_BLOCK_DB(w_send_DB).DW[send_start - 2]);
        w_recv_DB := WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 8];
        recv_start := WORD_TO_INT(WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 10]);

        WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 3] := NOT poll_enable OR poll_pause; // Set the device pause flag

        // skip this round if not poll_enable or poll_pause
        // DON'T use REPEAT loop, which may cause an infinite loop.
        IF NOT poll_enable OR poll_pause OR (NOT continuous AND NOT periodicity AND NOT custom_trigger) THEN
            RETURN; // Ignore this round and proceed directly to the next round
        END_IF;
        // Turn is_polling to TRUE must be after ignoring check
        is_polling := TRUE;
        IF is_modbus THEN
            "CRC16"(
                DB_NO     := send_DB,
                DBB_Start := send_start,
                DBB_Counts:= send_length - 2,
                CRC_H     := WORD_TO_BLOCK_DB(w_send_DB).DB[send_start + send_length - 2],
                CRC_L     := WORD_TO_BLOCK_DB(w_send_DB).DB[send_start + send_length - 1]);
        END_IF;
    END_IF;

    // Send data
    send (
        REQ                      := send_trigger,
        LADDR                    := module_addr,
        DB_NO                    := send_DB,
        DBB_NO                   := send_start,
        LEN                      := send_length);

    // Receive function
    receive (
        EN_R                     := recv_trigger,
        LADDR                    := module_addr,
        DB_NO                    := recv_DB,
        DBB_NO                   := recv_start + 1);

    // Handle on receive success
    on_receive := receive.NDR; // Non-modbus receive success flag
    IF on_receive AND is_modbus THEN // Additional receive success flag check for modbus
        // check func_code
        send_byte := WORD_TO_BLOCK_DB(w_send_DB).DB[send_start + 1];
        recv_byte := WORD_TO_BLOCK_DB(w_recv_DB).DB[recv_start + 2];
        on_receive := send_byte = recv_byte;
        IF BYTE_TO_INT(send_byte) > 4 THEN // Functions 05 06 15 16
            recv_length := 6;
        ELSE // Functions 01 02 03 04
            recv_length := BYTE_TO_INT(WORD_TO_BLOCK_DB(w_recv_DB).DB[recv_start + 3]) + 3;
        END_IF;
        // check device_ID
        send_byte := WORD_TO_BLOCK_DB(w_send_DB).DB[send_start];
        recv_byte := WORD_TO_BLOCK_DB(w_recv_DB).DB[recv_start + 1];
        on_receive := on_receive AND send_byte = recv_byte;
        // CRC
        "CRC16"(
            DB_NO     := recv_DB,
            DBB_Start := recv_start + 1,
            DBB_Counts:= recv_length,
            CRC_H     := CRC_H,
            CRC_L     := CRC_L);
        on_receive := on_receive
            AND CRC_H = WORD_TO_BLOCK_DB(w_recv_DB).DB[recv_start + recv_length + 1]
            AND CRC_L = WORD_TO_BLOCK_DB(w_recv_DB).DB[recv_start + recv_length + 2];
    END_IF;
    WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start,2] := on_receive; // Set the device receive flag

    IF on_receive THEN // Receive success
        WORD_TO_BLOCK_DB(w_poll_DB).DW[offset + 14] := W#16#0; //wait_count
        WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 0] := TRUE; // Device normal flag
        WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 1] := FALSE; // Device error flag
    ELSIF receive.NDR THEN // Received data but not successfully
        WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 0] := FALSE; // Device normal flag
        WORD_TO_BLOCK_DB(w_recv_DB).DX[recv_start, 1] := TRUE; // Device error flag;
    END_IF;
END_FUNCTION_BLOCK
