Obsidean_VM/04-SIDEL/09 - SAE452 - Diet as Regul.../FB2120 - MasseliTCP/FB2120 - MasseliTCP Read - ...

53 KiB


Ho creato l'FB2120 per leggere direttamente i valori inviati dai masselli UR62 o UR29 utilizzando il protocollo ADAM tramite un gateway 485 a Ethernet. Ho scelto TCP sul gateway trasparente 485-Ethernet per migliorare la resilienza dei dati. Il protocollo TCP gestisce automaticamente le ritrasmissioni in caso di perdita di un frame, mantenendo l'ordine dei dati. Questa gestione è ideale per la velocità richiesta, poiché Ethernet opera a velocità molto superiori e permette di evitare la perdita di trame in caso di collisioni.

Il sistema di riconnessione gestisce diversi timeout per ristabilire la connessione in varie situazioni. Il valore di TimeBetweenFramesMs è stato creato per misurare la velocità con cui i Masselli inviano i dati. Altri contatori monitorano diversi tipi di errori per valutare l'affidabilità del sistema.

Per potere testare il sistema ho creato un simulatore che riesce a creare via 485 attraverso il gateway le stesse protocollo ADAM e ho testato sul ADAM che ci ha prestato Masselli e funziona correttamente. Una volta collaudata la simulazione dei dati digitali ho creato un server TCP che permette simulare il funzionamento e dal PLC prendere i valori como se ci fosse il Masseli colegato. Con questo setup ho potuto simulare diversi problemi che possono accadere nella comunicazione tra il PLC e il Gateway. Quando arrivi il misuratore dorerebbe essere funzionante.

Ho utilizzato un Gateway Waveshare WIFI 485/Ethernet (Waveshare Industrial Grade Serial Server RS232/485 to WiFi And Ethernet, Modbus Gateway, MQTT Gateway, Metal Case, Wail-Mount and Rail-Mount Support : Amazon.it: Informatica ), ma in realtà si può usare qualsiasi gateway 485 a Ethernet che diventa un server TCP trasparente. Sul lato PLC, è sufficiente un'istanza di FB/DB per misuratore, utilizzando solo le FB Siemens TCON e TRCV, già impiegate per la FB1. Non è stato necessario utilizzare SFC extra o modificare la configurazione hardware, poiché TCON gestisce la comunicazione senza bisogno di creare il link con NetPro. Per semplicità, ho inserito l'IP del server direttamente nel codice per rendere la chiamata alla FB più pulita.

La FB fornisce stati per gestire problemi di comunicazione e calcola il Brix e la corrente ricevuta digitalmente. Utilizzando questo sistema, si evita la doppia conversione DAC-ADC, riducendo la perdita di risoluzione; questo include la perdita del 25% di risoluzione per il discorso che il ADAM lavora a 0..20mA e solo se utilizzano 4..20mA. La FB legge i valori digitali direttamente dall'uscita del Masselli, senza perdite, e può essere usata in parallelo al ADAM come opzione. Il flusso via 485 può essere letto sia dal ADAM che dal Gateway contemporaneamente, sfruttando la comunicazione unidirezionale.

Video da i test: Microsoft OneDrive Link dal software S7 utlizato per Test: Microsoft OneDrive


La chiamata della FB sarebbe:

!Pasted image 20250530095053.png

!Pasted image 20250530095111.png

Documentazione del Blocco Funzione: MaselliTCP


Versione: 1.8 Descrizione: Questa FB è progettata per leggere dati da un sensore Maselli tramite una connessione TCP/IP in modo continuo e ottimizzato. Il sensore invia frame di dati con un formato fisso di 12 byte: #XX12.345CKCR via RS485, questi dati vengono catturati con un gateway Waveshare che funziona da server TCP trasparente. L'FB gestisce la connessione, la ricezione ottimizzata tramite burst reading, il parsing dei dati con validazione ID e checksum, e il calcolo del valore Brix.

Principali Miglioramenti v1.8 verso v1.3

  • Burst Reading: Sistema di lettura ottimizzato che riduce il carico di ciclo PLC alternando fasi di lettura intensiva e pause
  • Controllo ID Dispositivo: Validazione opzionale dell'ID del dispositivo Adam (posizioni 1-2 del frame)
  • Validazione Checksum: Controllo checksum configurabile per garantire l'integrità dei dati
  • Buffer Ottimizzato: Buffer di ricezione ridotto da 128 a 23 byte per maggiore efficienza
  • Organizzazione Dati Strutturata: Uso di strutture per configurazione, statistiche, timing e diagnostica
  • Gestione Errori Avanzata: Sistema di codici errore con priorità (0-12, maggiore = più critico)
  • Tabelle FIFO: Storia opzionale degli ultimi 10 valori per debug (newest=[0], oldest=[9])

Funzionamento di Base

L'FB opera attraverso una macchina a stati principale e una macchina a stati di lettura ottimizzata:

Stati Principali (iState):

  1. IDLE (0): Attesa abilitazione con ritardo anti-riconnessione rapida
  2. CONNECT (1): Preparazione richiesta connessione TCP
  3. WAIT CONNECT (2): Tentativo di connessione con timeout configurabile
  4. READING (3): Lettura continua con sistema burst reading ottimizzato
  5. DISCONNECT (4): Chiusura controllata della connessione

Stati di Lettura Ottimizzata (iReadingState):

  • ACTIVE_READING (0): Lettura intensiva durante finestra di ricezione dati
  • COMPLETED (1): Frame valido processato, calcolo timing per frame successivo
  • PAUSED (2): Pausa calcolata tra frame per ridurre carico ciclo PLC

Il sistema burst reading ottimizza i tempi di ciclo alternando:

  • Fasi di lettura intensiva quando sono attesi i dati
  • Pause calcolate per ridurre il carico computazionale
  • Fase di inizializzazione per sincronizzazione con il sensore

Parametri di Ingresso (VAR_INPUT)

Nome Tipo Valore Predefinito Descrizione
xEnable BOOL Abilita (TRUE) o disabilita (FALSE) la comunicazione con il sensore
aRemoteIP ARRAY[1..4] OF BYTE [10,1,33,100] Indirizzo IP del server Gateway Maselli
iRemotePort INT 8899 Porta TCP del server Gateway Maselli
rBrixMax REAL 80.0 Valore Brix massimo che corrisponde a 20mA del segnale
wConnectionID WORD W#16#10 ID univoco per la connessione TCP (configurato nell'hardware del PLC)
xEnableChecksum BOOL TRUE NUOVO: Abilita validazione checksum dei frame ricevuti
rFramesPerSecond REAL 3.0 NUOVO: Frame attesi per secondo (per calcolo timing ottimizzato)
iConnTimeoutSec INT 10 NUOVO: Timeout connessione in secondi
iDataTimeoutSec INT 30 NUOVO: Timeout ricezione dati in secondi
iRetryDelaySec INT 2 NUOVO: Ritardo tra tentativi di riconnessione
xEnableFifoDebug BOOL FALSE NUOVO: Abilita tabelle FIFO per debug (ultimi 10 valori)
xDisableBurstRead BOOL FALSE NUOVO: Disabilita burst reading (TRCV sempre attivo)
iDeviceID INT 0 NUOVO: ID dispositivo da validare (0=disabilitato, 1-99=abilitato)

Parametri di Uscita (VAR_OUTPUT)

Nome Tipo Descrizione
xConnected BOOL Indica lo stato della connessione TCP
xDataValid BOOL Impulso quando un nuovo set di dati validi è stato ricevuto e parsato
xError BOOL TRUE se si è verificato un errore
wErrorCode WORD Codice di errore legacy (per compatibilità)
iErrorCode INT NUOVO: Codice errore strutturato (0=OK, maggiore=più critico)
rCurrentMA REAL Valore di corrente in milliampere letto dal sensore
rBrixValue REAL Valore Brix calcolato
xInitPhaseFinish BOOL NUOVO: Flag completamento fase inizializzazione
diTimeBetweenFramesMs DINT Tempo trascorso tra gli ultimi due frame validi
diReadingState INT NUOVO: Stato corrente del sistema di lettura (0=Attivo, 1=Completato, 2=Pausa)

Strutture Dati Organizzate

Configurazione (stConfig)

Parametri di configurazione consolidati per migliore organizzazione dei dati del DB.

Statistiche (stStats)

  • diFrameCount: Frame totali ricevuti
  • diErrorFrameCount: Frame con errori di formato
  • diSuccessfulReconCount: Riconnessioni riuscite
  • diChecksumErrorCount: NUOVO: Errori checksum
  • diLostFramesCount: NUOVO: Frame persi stimati
  • iConsecutiveBadFrames: NUOVO: Frame consecutivi errati

Timing (stTiming)

  • Calcolo automatico intervalli frame basato su rFramesPerSecond
  • Ottimizzazione dinamica del timing di lettura
  • Rilevamento jitter e variazioni timing

Diagnostica (stDiag)

Stati e informazioni di debug consolidate per facilitare la manutenzione.

Codici Errore Strutturati (iErrorCode)

Priorità crescente (0 = nessun errore, 12 = più critico):

Codice Descrizione
0 Nessun errore
1 Errore checksum
2 Formato frame non valido
3 Caratteri non numerici
4 Dati fuori range (4-20mA)
5 Frame errati consecutivi multipli
6 Frame persi rilevati
7 Buffer ricezione TRCV troppo piccolo
8 Errore comunicazione TRCV
9 Timeout ricezione dati
10 Timeout connessione
11 Connessione TCP fallita
12 Connessione TCP terminata

Tabelle FIFO (Debug Opzionale)

Quando xEnableFifoDebug = TRUE:

  • arBrixHistory[0..9]: Ultimi 10 valori Brix ([0]=più recente, [9]=più vecchio)
  • auiTimeHistory[0..9]: Ultimi 10 timestamp ([0]=più recente, [9]=più vecchio)

Note Aggiuntive


  • Buffer Ottimizzato: Buffer di ricezione ridotto da 128 a 23 byte per maggiore efficienza memoria
  • Burst Reading: Riduce significativamente il carico di ciclo PLC alternando lettura intensiva e pause calcolate
  • Validazione ID: Controllo opzionale dell'ID dispositivo Adam nelle posizioni 1-2 del frame
  • Checksum: Validazione hex checksum configurabile per garantire integrità dati
  • Timing Adattivo: Calcolo automatico degli intervalli di lettura basato sulla frequenza frame configurata
  • Rilevamento Frame Persi: Stima automatica dei frame persi basata su analisi timing
  • Fase Inizializzazione: Gestione ottimizzata della sincronizzazione iniziale con il sensore
  • Reset Contatori: I contatori si resettano automaticamente per prevenire overflow

Source:


//******************************************************************************
// Function Block: FB_MaselliTCP
// Description: Enhanced Maselli sensor TCP reader with optimized buffer and FIFO data
// Frame format: #XX12.345CKCR (12 fixed bytes)
//
// |  Field  | Start (1 char) | Adam address (2 chars) | Brix or Temperature value in mA (XX.YYY: 6 chars) | Checksum (2 chars) | Carriage return (1 char) |
// | :-----: | :------------: | :--------------------: | :-----------------------------------------------: | :----------------: | :----------------------: |
// | Example |       #        |           01           |                      02.145                       |        CkCk        |            Cr            |
//
// Version: 1.8 - Enhanced with structured data and optimized buffer management
// Platform: Siemens S7-300
//
// IMPROVEMENTS v1.8:
// - Structured data organization for better DB readability
// - Optimized buffer size (23 bytes instead of 128)
// - Enhanced reading state machine with initialization phase
// - Ordered FIFO tables for last 10 readings (newest at [0], oldest at [9])
// - Optional FIFO debug mode (disabled by default)
// - Improved synchronization logic with xInitPhaseFinish flag
// - Limited diAccumCycleMs accumulation only during iState = 3
//
// READING STATES:
//  0 = ACTIVE_READING - Receiving data burst, read intensively
//  1 = COMPLETED - Finished reading a valid frame, return to paused
//  2 = PAUSED - Not reading, waiting for next frame window
//
// FIFO TABLES (when xEnableFifoDebug = TRUE):
//  arBrixHistory[0] = newest Brix value, arBrixHistory[9] = oldest
//  auiTimeHistory[0] = newest timestamp, auiTimeHistory[9] = oldest
//
// ERROR CODES (iErrorCode) - Higher numbers = Higher priority:
//  0 = No error, 1 = Checksum error, 2 = Invalid frame format
//  3 = Invalid digit characters, 4 = Data out of range (4-20mA)
//  5 = Multiple consecutive bad frames, 6 = Lost frames detected
//  7 = TRCV receive buffer too small, 8 = TRCV communication error
//  9 = Data reception timeout, 10 = Connection timeout
// 11 = TCP connection failed, 12 = TCP connection terminated
//******************************************************************************

FUNCTION_BLOCK MaselliTCP
TITLE = 'Maselli TCP Reader - Enhanced Buffer Management'
VERSION : '1.8'

VAR_INPUT
    xEnable         : BOOL;         // Enable communication
    aRemoteIP       : ARRAY[1..4] OF BYTE := [10,1,33,100]; // Remote server IP
    iRemotePort     : INT := 8899;  // Server port
    rBrixMax        : REAL := 80.0; // Maximum Brix value at 20mA
    wConnectionID   : WORD := W#16#10; // TCP Connection ID
    xEnableChecksum : BOOL := TRUE; // Enable checksum validation
    rFramesPerSecond : REAL := 3.0; // Expected frames per second
    iConnTimeoutSec : INT := 10;    // Connection timeout in seconds
    iDataTimeoutSec : INT := 30;    // Data reception timeout in seconds
    iRetryDelaySec  : INT := 2;     // Delay between reconnection attempts
    xEnableFifoDebug : BOOL := FALSE; // Enable FIFO tables update (debug only)
    xDisableBurstRead : BOOL := FALSE; // Disable burst reading (TRCV always active in reading state if TRUE)
    iDeviceID       : INT := 0;     // Device ID to validate (0 = disabled, 1-99 = enabled). Checks aFrame[1] and aFrame[2].
END_VAR

VAR_OUTPUT
    xConnected      : BOOL;         // Connection status
    xDataValid      : BOOL;         // Pulse when valid data is available
    xError          : BOOL;         // Active error
    wErrorCode      : WORD;         // Legacy error code (for compatibility)
    iErrorCode      : INT;          // Structured error code (0=OK, higher=more critical)
    rCurrentMA      : REAL;         // Current mA value
    rBrixValue      : REAL;         // Current Brix value
    xInitPhaseFinish : BOOL;        // Initialization phase completed flag
    
    // Current frame timing
    diTimeBetweenFramesMs : DINT;   // Time elapsed between last two valid frames
    diReadingState  : INT;          // Current reading state for diagnostics    
END_VAR

// ============================================================================
// CONTROL VARIABLES
// ============================================================================

VAR // State Machine Control
    iState          : INT := 0;     // Main state: 0=Idle, 1=Connect, 2=WaitConnect, 3=Reading, 4=Disconnect
    iReadingState   : INT := 0;     // Reading state: 0=Active, 1=Completed, 2=Paused
    iCurrentErrorCode : INT := 0;   // Current error being evaluated
    diAccumCycleMs  : DINT := 0;    // Accumulated cycle time (only in iState=3)
    iConsecutiveGoodFrames : INT := 0; // Counter for consecutive structurally good frames
    xFirstScan      : BOOL := TRUE;
    diInitPhaseTimerMs : DINT := 0; // Timer for initialization phase duration
END_VAR

VAR // Timing Control
    tCurrentTime    : TIME;         // Current system time
    tLastTime       : TIME;         // Previous time
    diElapsedMs     : DINT;         // Elapsed time in ms
    xTrcvTimeToCall : BOOL := TRUE; // TRCV call control
END_VAR

VAR // FIFO Tables - Last 10 readings (ordered: [0]=newest, [9]=oldest)
    arBrixHistory   : ARRAY[0..9] OF REAL;     // Last 10 Brix values (newest first)
    auiTimeHistory  : ARRAY[0..9] OF INT;     // Last 10 timestamps in ms (newest first)    
END_VAR

// ============================================================================
// STRUCTURED DATA ORGANIZATION
// ============================================================================

VAR // Configuration Structure
    stConfig : STRUCT
        rFramesPerSecond : REAL := 3.0;     // Expected frames per second
        iConnTimeoutSec : INT := 10;        // Connection timeout
        iDataTimeoutSec : INT := 30;        // Data timeout
        iRetryDelaySec  : INT := 2;         // Retry delay
        rBrixMax        : REAL := 80.0;     // Maximum Brix at 20mA
        xEnableChecksum : BOOL := TRUE;     // Checksum validation
        iDeviceID       : INT := 0;         // Device ID to validate
        xDisableBurstRead : BOOL;           // Disable burst reading
    END_STRUCT;
END_VAR

VAR // Statistics Structure
    stStats : STRUCT
        diFrameCount        : DINT;         // Total received frames
        diErrorFrameCount   : DINT;         // Frames with format errors
        diSuccessfulReconCount : DINT;      // Successful reconnections
        diChecksumErrorCount : DINT;        // Checksum errors
        diLostFramesCount   : DINT;         // Estimated lost frames
        iConsecutiveBadFrames : INT;        // Consecutive bad frames counter
    END_STRUCT;
END_VAR

VAR // Timing Structure
    stTiming : STRUCT
        diFrameIntervalMs   : DINT := 333;  // Auto-calculated frame interval
        iPreReadOffsetMs    : INT := 50;    // Pre-read offset
        iMaxActiveReadingMs : INT := 200;   // Max active reading time
        diEstFrameMs        : DINT := 1000; // Estimated frame interval
        diNextFrameMs       : DINT := 0;    // Next expected frame time
        diReadingStartMs    : DINT := 0;    // Reading start time
        diMinFrameIntervalMs : DINT := DINT#999999; // Minimum observed interval
        diMaxFrameIntervalMs : DINT := 0;   // Maximum observed interval
        diFrameJitterMs     : DINT := 0;    // Frame jitter
    END_STRUCT;
END_VAR

VAR // Diagnostics Structure
    stDiag : STRUCT
        diState         : INT;              // Current state for debug
        diTconStatus    : WORD;             // TCON status
        diTrcvStatus    : WORD;             // TRCV status
        diNextExpectedMs : DINT;            // Next expected frame time
        diCalcFrameIntervalMs : DINT;       // Calculated frame interval
        iCalcPreReadOffsetMs : INT;         // Calculated pre-read offset
        iCalcMaxActiveMs : INT;             // Calculated max active time
        rAvgCurrentMA   : REAL;             // Moving average of current
    END_STRUCT;
END_VAR

// ============================================================================
// CONNECTION PARAMETERS
// ============================================================================

VAR // TCP Connection Parameters
    stConnParams    : STRUCT
        block_length    : WORD := W#16#40;
        id              : WORD;
        connection_type : BYTE := B#16#11;  // TCP
        active_est      : BOOL := TRUE;     // Active client
        local_device_id : BYTE := B#16#2;   // Integrated PN
        local_tsap_id_len : BYTE := B#16#0;
        rem_subnet_id_len : BYTE := B#16#0;
        rem_staddr_len  : BYTE := B#16#4;
        rem_tsap_id_len : BYTE := B#16#2;
        next_staddr_len : BYTE := B#16#0;
        local_tsap_id   : ARRAY[1..16] OF BYTE;
        rem_subnet_id   : ARRAY[1..6] OF BYTE;
        rem_staddr      : ARRAY[1..6] OF BYTE;
        rem_tsap_id     : ARRAY[1..16] OF BYTE;
        next_staddr     : ARRAY[1..6] OF BYTE;
        spare           : WORD := W#16#0;
    END_STRUCT;
END_VAR

// ============================================================================
// FUNCTION BLOCK INSTANCES AND INTERNAL DATA (Bottom of DB)
// ============================================================================

VAR // Frame Processing
    i               : INT;
    j               : INT;
    iStartPos       : INT;
    aFrame          : ARRAY[0..11] OF BYTE;  // Current frame
    iChecksumSum    : INT;
    bCalculatedChecksum : BYTE;
    bReceivedChecksum : BYTE;
END_VAR

VAR // Connection Control
    xTdisconReq     : BOOL;
    xTconReq        : BOOL;
    xTconReqOld     : BOOL;
    xTconPulse      : BOOL;
    iCyclesToWait   : INT;
END_VAR

VAR // Timer Instances
    tonConnTimeout  : TON;          // Connection timeout
    tonRetryDelay   : TON;          // Retry delay
    tonNoDataTimeout: TON;          // No data timeout
    tonDisconTimeout: TON;          // Disconnect timeout
END_VAR

VAR // Communication Block Instances
    TCON_DB         : TCON;         // TCP connection
    TRCV_DB         : TRCV;         // TCP receive
    TDISCON_DB      : TDISCON;      // TCP disconnect
END_VAR

VAR // Optimized Reception Buffer (23 bytes instead of 128)
    aRxBuffer       : ARRAY[0..22] OF BYTE;  // Optimized buffer size
END_VAR

VAR // Buffer Management Structure
    stBuffer : STRUCT
        aCircBuffer     : ARRAY[0..255] OF BYTE; // Circular buffer
        iWritePtr       : INT := 0;         // Write pointer
        iReadPtr        : INT := 0;         // Read pointer
        iDataInBuffer   : INT := 0;         // Data in buffer
        iRxLength       : INT;              // Last received length
    END_STRUCT;
END_VAR


VAR_TEMP
    tempByte        : BYTE;
    tempInt         : INT;
    tempIntValue    : INT;
    tempWord        : WORD;
    tempReal        : REAL;
    tempTime        : TIME;
    iAvailable      : INT;
    iBytesToRead    : INT;
    tempActualIntervalMs : DINT;    // Stores actual interval for current frame processing cycle
    tempReceivedID_Char1 : BYTE;
    tempDiffDint    : DINT;         // Temporary DINT for offset adjustment calculation
    tempReceivedID_Char2 : BYTE;
    tempReceivedID_Val   : INT;
END_VAR

BEGIN

    // --- Read system time ---
    tCurrentTime := TIME_TCK();

    // --- Initialization on first scan ---
    IF xFirstScan THEN
        xFirstScan := FALSE;
        tLastTime := tCurrentTime;
        
        // Initialize configuration from inputs
        stConfig.rFramesPerSecond := rFramesPerSecond;
        stConfig.iConnTimeoutSec := iConnTimeoutSec;
        stConfig.iDataTimeoutSec := iDataTimeoutSec;
        stConfig.iRetryDelaySec := iRetryDelaySec;
        stConfig.rBrixMax := rBrixMax;
        stConfig.xEnableChecksum := xEnableChecksum;
        stConfig.iDeviceID := iDeviceID;
        stConfig.xDisableBurstRead := xDisableBurstRead;
        
        // Clear connection parameters arrays
        FOR i := 1 TO 16 DO
            stConnParams.local_tsap_id[i] := B#16#00;
            stConnParams.rem_tsap_id[i] := B#16#00;
        END_FOR;
        FOR i := 1 TO 6 DO
            stConnParams.rem_subnet_id[i] := B#16#00;
            stConnParams.rem_staddr[i] := B#16#00;
            stConnParams.next_staddr[i] := B#16#00;
        END_FOR;
        
        // Configure connection parameters
        stConnParams.id := wConnectionID;
        stConnParams.rem_staddr[1] := aRemoteIP[1];
        stConnParams.rem_staddr[2] := aRemoteIP[2];
        stConnParams.rem_staddr[3] := aRemoteIP[3];
        stConnParams.rem_staddr[4] := aRemoteIP[4];
        
        // Remote port configuration
        tempWord := INT_TO_WORD(iRemotePort);
        stConnParams.rem_tsap_id[1] := WORD_TO_BYTE(SHR(IN:= tempWord, N:= 8));
        stConnParams.rem_tsap_id[2] := WORD_TO_BYTE(tempWord AND W#16#FF);
        
        // Reset all control variables
        xTconReq := FALSE;
        xTconReqOld := FALSE;
        iCyclesToWait := 0;
        stBuffer.iWritePtr := 0;
        stBuffer.iReadPtr := 0;
        stBuffer.iDataInBuffer := 0;
        
        // Reset statistics
        stStats.diFrameCount := 0;
        stStats.diErrorFrameCount := 0;
        stStats.diSuccessfulReconCount := 0;
        stStats.diChecksumErrorCount := 0;
        stStats.diLostFramesCount := 0;
        stStats.iConsecutiveBadFrames := 0;
        
        // Initialize timing parameters
        IF stConfig.rFramesPerSecond > 0.1 THEN
            stTiming.diFrameIntervalMs := REAL_TO_DINT(1000.0 / stConfig.rFramesPerSecond);
            stTiming.iPreReadOffsetMs := DINT_TO_INT(stTiming.diFrameIntervalMs / 6);
            stTiming.iMaxActiveReadingMs := DINT_TO_INT(stTiming.diFrameIntervalMs / 3);
            IF stTiming.iPreReadOffsetMs < 20 THEN stTiming.iPreReadOffsetMs := 20; END_IF;
            IF stTiming.iMaxActiveReadingMs < 50 THEN stTiming.iMaxActiveReadingMs := 50; END_IF;
        ELSE
            stTiming.diFrameIntervalMs := 1000;
            stTiming.iPreReadOffsetMs := 50;
            stTiming.iMaxActiveReadingMs := 200;
        END_IF;
        
        stTiming.diEstFrameMs := stTiming.diFrameIntervalMs;
        stTiming.diNextFrameMs := 0;
        stTiming.diReadingStartMs := 0;
        
        // Initialize FIFO tables (ordered arrays)
        FOR i := 0 TO 9 DO
            arBrixHistory[i] := 0.0;
            auiTimeHistory[i] := 0;
        END_FOR;
        
        // Initialize other variables    
        diTimeBetweenFramesMs := 0;
        diAccumCycleMs := 0;
        iReadingState := 0; // Start in ACTIVE_READING
        xTrcvTimeToCall := TRUE;
        iCurrentErrorCode := 0;
        iConsecutiveGoodFrames := 0;
        diInitPhaseTimerMs := 0;
        xInitPhaseFinish := FALSE;
    END_IF;
    
    // Calculate elapsed time since last call
    IF tCurrentTime >= tLastTime THEN
        diElapsedMs := TIME_TO_DINT(tCurrentTime - tLastTime);
        if diElapsedMs > 1000 THEN
            diElapsedMs := 1000; // Cap at 1 second
        END_IF;
    ELSE
        diElapsedMs := 1; // Handle wrap-around
    END_IF;
    tLastTime := tCurrentTime;

    // --- Diagnostic outputs ---
    stDiag.diState := iState;
    stDiag.diTconStatus := TCON_DB.STATUS;
    stDiag.diTrcvStatus := TRCV_DB.STATUS;
    diReadingState := iReadingState;
    stDiag.diNextExpectedMs := stTiming.diNextFrameMs;
    stDiag.diCalcFrameIntervalMs := stTiming.diFrameIntervalMs;
    stDiag.iCalcPreReadOffsetMs := stTiming.iPreReadOffsetMs;
    stDiag.iCalcMaxActiveMs := stTiming.iMaxActiveReadingMs;
    
    // --- Error code evaluation (higher number = higher priority) ---
    iCurrentErrorCode := 0;
    
    // Priority 1: Checksum errors
    IF stStats.diChecksumErrorCount > 0 AND diAccumCycleMs < 5000 THEN
        iCurrentErrorCode := 1;
    END_IF;
    
    // Priority 2: Frame format errors
    IF stStats.diErrorFrameCount > 0 AND diAccumCycleMs < 5000 THEN
        iCurrentErrorCode := 2;
    END_IF;
    
    // Priority 4: Data out of range
    IF rCurrentMA > 0.0 AND (rCurrentMA < 4.0 OR rCurrentMA > 20.0) AND 
       diAccumCycleMs < 2000 THEN
        iCurrentErrorCode := 4;
    END_IF;
    
    // Priority 5: Multiple consecutive bad frames
    IF stStats.iConsecutiveBadFrames >= 5 THEN
        iCurrentErrorCode := 5;
    END_IF;
    
    // Priority 6: Lost frames
    IF stStats.diLostFramesCount > 0 AND stTiming.diEstFrameMs > 0 AND
       diAccumCycleMs > (stTiming.diEstFrameMs * 2) THEN
        iCurrentErrorCode := 6;
    END_IF;
    
    // Priority 7-8: TRCV errors
    IF TRCV_DB.ERROR THEN
        IF TRCV_DB.STATUS = W#16#8088 THEN
            iCurrentErrorCode := 7;
        ELSE
            iCurrentErrorCode := 8;
        END_IF;
    END_IF;
    
    // Priority 9: Data timeout
    IF tonNoDataTimeout.Q THEN
        iCurrentErrorCode := 9;
    END_IF;
    
    // Priority 10: Connection timeout
    IF tonConnTimeout.Q THEN
        iCurrentErrorCode := 10;
    END_IF;
    
    // Priority 11: Connection failed
    IF TCON_DB.ERROR THEN
        iCurrentErrorCode := 11;
    END_IF;
    
    // Priority 12: Connection terminated
    IF (TRCV_DB.STATUS = W#16#80A3) OR (TRCV_DB.STATUS = W#16#80C4) THEN
        iCurrentErrorCode := 12;
    END_IF;
    
    // Set final error outputs
    iErrorCode := iCurrentErrorCode;
    xError := (iCurrentErrorCode > 0);
    
    // --- Reset pulse outputs ---
    xDataValid := FALSE;
    
    // --- Pulse control for TCON ---
    xTconPulse := xTconReq AND NOT xTconReqOld;
    xTconReqOld := xTconReq;
          
    // Enhanced Reading State Machine with Initialization Phase
    IF iState <> 3 THEN // Only run reading states when in READING main state
        iReadingState := 0; // Reset reading state
        xInitPhaseFinish := FALSE; // Reset initialization phase finish
        diAccumCycleMs := 0; // Reset cycle accumulator
        diInitPhaseTimerMs := 0; // Reset init phase timer
        iConsecutiveGoodFrames := 0; // Reset counter when not in active reading cycle
        stTiming.diEstFrameMs := stTiming.diFrameIntervalMs;
    ELSE // iState = 3: READING
        
        IF stStats.iConsecutiveBadFrames <= 5 THEN
            diAccumCycleMs := diAccumCycleMs + diElapsedMs;
        END_IF;

        IF NOT xInitPhaseFinish THEN
            diInitPhaseTimerMs := diInitPhaseTimerMs + diElapsedMs;
        END_IF;

        CASE iReadingState OF
            0: // ACTIVE_READING - Receiving data burst, read intensively
                xTrcvTimeToCall := TRUE; // Read every cycle
                
                // During initialization phase, skip PAUSED state
                IF xInitPhaseFinish THEN
                    // Normal operation - can transition to PAUSED
                    IF (stTiming.diNextFrameMs - diAccumCycleMs) > INT_TO_DINT(stTiming.iMaxActiveReadingMs) THEN
                        iReadingState := 2; // Go to PAUSED
                        xTrcvTimeToCall := FALSE;
                    END_IF;
                END_IF;
                
            1: // COMPLETED - Finished reading a valid frame
                xTrcvTimeToCall := FALSE;

                // Capture the actual time elapsed for this frame cycle before resetting diAccumCycleMs
                tempActualIntervalMs := diAccumCycleMs;

                // --- Time Between Frames Calculation ---
                // Only update diTimeBetweenFramesMs if the current and previous frames were good.
                IF iConsecutiveGoodFrames >= 2 THEN
                    diTimeBetweenFramesMs := tempActualIntervalMs;
                END_IF;
                // Always reset diAccumCycleMs for the next interval measurement.
                diAccumCycleMs := 0; 
                
                // Update estimated frame interval (stTiming.diEstFrameMs)
                IF NOT xInitPhaseFinish THEN
                    // During initialization, if we have a fresh reliable interval, use it directly.
                    IF iConsecutiveGoodFrames >= 2 AND diTimeBetweenFramesMs > 0 THEN
                        stTiming.diEstFrameMs := diTimeBetweenFramesMs;
                    END_IF;
                    // ELSE stTiming.diEstFrameMs remains its initial value (from config)
                ELSE
                    // After initialization, smooth the estimate if diTimeBetweenFramesMs is valid.
                    IF diTimeBetweenFramesMs > 0 THEN
                        stTiming.diEstFrameMs := (stTiming.diEstFrameMs * 3 + diTimeBetweenFramesMs) / 4; // Smoothing
                    ELSE
                        // If diTimeBetweenFramesMs is not positive (e.g. never set, or problem), revert to default.
                        stTiming.diEstFrameMs := stTiming.diFrameIntervalMs;
                    END_IF;
                END_IF;
                
                // Set next expected frame time
                stTiming.diNextFrameMs := stTiming.diEstFrameMs;

                // --- Dynamically adjust iPreReadOffsetMs (User Optimization) ---
                // Adjust only after initialization and with stable timing data.
                // Limits for iPreReadOffsetMs: 5ms to 50ms.
                IF xInitPhaseFinish AND (iConsecutiveGoodFrames >= 2) THEN
                    // tempDiffDint = Estimated Interval for Next Frame - Actual Interval of Last Frame
                    tempDiffDint := stTiming.diNextFrameMs - diTimeBetweenFramesMs;

                    IF tempDiffDint < INT_TO_DINT(stTiming.iPreReadOffsetMs) THEN
                        IF stTiming.iPreReadOffsetMs > 5 THEN
                            stTiming.iPreReadOffsetMs := stTiming.iPreReadOffsetMs - 1;
                        END_IF;
                    ELSIF tempDiffDint > INT_TO_DINT(stTiming.iPreReadOffsetMs) THEN
                        IF stTiming.iPreReadOffsetMs < 50 THEN
                            stTiming.iPreReadOffsetMs := stTiming.iPreReadOffsetMs + 1;
                        END_IF;
                    END_IF;
                END_IF;

                // Detect lost frames based on the *actual* interval for the just-completed frame cycle
                // compared to the current estimate of frame interval.
                IF stTiming.diEstFrameMs > 0 AND tempActualIntervalMs > (stTiming.diEstFrameMs * 3 / 2) THEN
                    tempInt := DINT_TO_INT(tempActualIntervalMs / stTiming.diEstFrameMs) - 1;
                    IF tempInt > 0 THEN
                        stStats.diLostFramesCount := stStats.diLostFramesCount + INT_TO_DINT(tempInt);
                    END_IF;
                END_IF;
                iReadingState := 2;
                
            2: // PAUSED - Not reading, waiting for next frame window                
                // Ensure iConsecutiveGoodFrames is reset if we are paused for a long time or before starting a new read attempt.
                // This is mainly handled by the iState <> 3 check, but can be reinforced if needed.
                IF NOT xInitPhaseFinish OR stConfig.xDisableBurstRead THEN
                    iReadingState := 0;
                ELSE // Only used when xInitPhaseFinish = TRUE
                    IF (stTiming.diNextFrameMs - diAccumCycleMs) <= INT_TO_DINT(stTiming.iPreReadOffsetMs) THEN
                        iReadingState := 0; // Go to ACTIVE_READING
                        stTiming.diReadingStartMs := diAccumCycleMs;
                    ELSE
                        xTrcvTimeToCall := FALSE;
                    END_IF;                
                END_IF;
                
        END_CASE;
    END_IF;

    // --- Main State Machine ---
    CASE iState OF
        0:  // IDLE
            xConnected := FALSE;
            wErrorCode := W#16#0000;
                        
            // Retry delay
            tempTime := DINT_TO_TIME(INT_TO_DINT(stConfig.iRetryDelaySec) * 1000);
            tonRetryDelay(IN := TRUE, PT := tempTime);
            
            IF xEnable AND tonRetryDelay.Q THEN
                tonRetryDelay(IN := FALSE);
                
                // Intentar desconectar primero por seguridad
                xTdisconReq := TRUE;
                TDISCON_DB(REQ := xTdisconReq, ID := wConnectionID);
                
                // Resetear variables de control
                xTconReq := FALSE;
                xTconReqOld := FALSE;
                iCyclesToWait := 0;
                stBuffer.iWritePtr := 0;
                stBuffer.iReadPtr := 0;
                stBuffer.iDataInBuffer := 0;
                iState := 1;
            ELSIF NOT xEnable THEN
                tonRetryDelay(IN := FALSE);
                xTdisconReq := FALSE;
                TDISCON_DB(REQ := FALSE, ID := wConnectionID);
            END_IF;
            
        1:  // CONNECT - Prepare connection
            iCyclesToWait := iCyclesToWait + 1;
            IF iCyclesToWait >= 2 THEN
                iCyclesToWait := 0;
                xTconReq := TRUE;
                xTconReqOld := FALSE;  // ← AGREGAR ESTA LÍNEA
                iState := 2;
            END_IF;
            
        2:  // WAIT CONNECT - Wait for connection
            TCON_DB(
                REQ := xTconReq, // xTconPulse,
                ID := wConnectionID,
                CONNECT := stConnParams    
            );
            
            tempTime := DINT_TO_TIME(INT_TO_DINT(stConfig.iConnTimeoutSec) * 1000);
            tonConnTimeout(IN := TRUE, PT := tempTime);
            tonNoDataTimeout(IN := FALSE);
            
            IF TCON_DB.DONE THEN
                xTconReq := FALSE;
                xConnected := TRUE;
                tonConnTimeout(IN := FALSE);
                stStats.diSuccessfulReconCount := stStats.diSuccessfulReconCount + 1;
                
                // Start initialization phase
                diAccumCycleMs := 0; // Reset cycle accumulator when entering reading state
                iReadingState := 0;  // Start in ACTIVE_READING
                diInitPhaseTimerMs := 0; // Reset init phase timer
                stTiming.diReadingStartMs := 0;
                
                iState := 3;  // Go to continuous reading
            ELSIF TCON_DB.ERROR THEN
                IF TCON_DB.STATUS = W#16#80A3 THEN
                    // Conexión ya existe - ir a disconnect primero
                    xTconReq := FALSE;
                    xTconReqOld := FALSE;
                    iState := 4;
                ELSE
                    xTconReq := FALSE;
                    xTconReqOld := FALSE;
                    wErrorCode := TCON_DB.STATUS;
                    tonConnTimeout(IN := FALSE);
                    iState := 4;
                END_IF;
            ELSIF tonConnTimeout.Q THEN
                xTconReq := FALSE;
                xTconReqOld := FALSE;
                wErrorCode := W#16#8001;
                tonConnTimeout(IN := FALSE);
                iState := 4;
            END_IF;
            
            IF NOT xEnable THEN
                xTconReq := FALSE;
                tonConnTimeout(IN := FALSE);
                iState := 4;
            END_IF;
            
        3:  // READING - Enhanced reading with initialization phase
            
            // Call TRCV based on reading state
            IF xTrcvTimeToCall THEN
                TRCV_DB(
                    EN_R := TRUE,
                    ID := wConnectionID,
                    LEN := 0,  // Ad-hoc mode
                    DATA := aRxBuffer  // Optimized 23-byte buffer
                );
            ELSE
                TRCV_DB(
                    EN_R := FALSE,
                    ID := wConnectionID,
                    LEN := 0,
                    DATA := aRxBuffer
                );
            END_IF;
            
            // Process TRCV result
            IF TRCV_DB.NDR THEN
                stBuffer.iRxLength := TRCV_DB.RCVD_LEN;
                tonNoDataTimeout(IN := FALSE);
                
                // Copy data to circular buffer
                FOR i := 0 TO (stBuffer.iRxLength - 1) DO
                    stBuffer.aCircBuffer[stBuffer.iWritePtr] := aRxBuffer[i];
                    stBuffer.iWritePtr := (stBuffer.iWritePtr + 1) MOD 256;
                    stBuffer.iDataInBuffer := stBuffer.iDataInBuffer + 1;
                    IF stBuffer.iDataInBuffer > 256 THEN
                        stBuffer.iDataInBuffer := 256;
                        stBuffer.iReadPtr := (stBuffer.iReadPtr + 1) MOD 256;
                    END_IF;
                END_FOR;
                
                // --- Process frames in circular buffer ---
                WHILE stBuffer.iDataInBuffer >= 12 DO
                    // Find first '#'
                    iStartPos := -1;
                    FOR j := 0 TO (stBuffer.iDataInBuffer - 1) DO
                        tempInt := (stBuffer.iReadPtr + j) MOD 256;
                        IF stBuffer.aCircBuffer[tempInt] = B#16#23 THEN
                            iStartPos := j;
                            EXIT;
                        END_IF;
                    END_FOR;

                    IF iStartPos >= 0 THEN
                        // Discard bytes before '#'
                        IF iStartPos > 0 THEN
                            stBuffer.iReadPtr := (stBuffer.iReadPtr + iStartPos) MOD 256;
                            stBuffer.iDataInBuffer := stBuffer.iDataInBuffer - iStartPos;
                        END_IF;

                        IF stBuffer.iDataInBuffer >= 12 THEN
                            // Check format: '.' at position 5
                            IF stBuffer.aCircBuffer[(stBuffer.iReadPtr + 5) MOD 256] = B#16#2E THEN
                                // Copy frame
                                FOR i := 0 TO 11 DO
                                    aFrame[i] := stBuffer.aCircBuffer[(stBuffer.iReadPtr + i) MOD 256];
                                END_FOR;

                                // --- Validate Device ID (if enabled) ---
                                // Adam address is in aFrame[1] and aFrame[2]
                                IF stConfig.iDeviceID > 0 AND stConfig.iDeviceID <= 99 THEN
                                    tempReceivedID_Char1 := aFrame[1];
                                    tempReceivedID_Char2 := aFrame[2];

                                    // Check if ID characters are digits
                                    IF (BYTE_TO_INT(tempReceivedID_Char1) >= 16#30 AND BYTE_TO_INT(tempReceivedID_Char1) <= 16#39) AND
                                       (BYTE_TO_INT(tempReceivedID_Char2) >= 16#30 AND BYTE_TO_INT(tempReceivedID_Char2) <= 16#39) THEN
                                        
                                        tempReceivedID_Val := (BYTE_TO_INT(tempReceivedID_Char1) - 16#30) * 10 + 
                                                              (BYTE_TO_INT(tempReceivedID_Char2) - 16#30);

                                        IF tempReceivedID_Val <> stConfig.iDeviceID THEN
                                            // ID Mismatch
                                            stStats.diErrorFrameCount := stStats.diErrorFrameCount + 1;
                                            stStats.iConsecutiveBadFrames := stStats.iConsecutiveBadFrames + 1;
                                            iConsecutiveGoodFrames := 0;
                                            stBuffer.iReadPtr := (stBuffer.iReadPtr + 12) MOD 256;
                                            stBuffer.iDataInBuffer := stBuffer.iDataInBuffer - 12;
                                            CONTINUE; 
                                        END_IF;
                                    ELSE
                                        // Invalid characters for ID in frame
                                        stStats.diErrorFrameCount := stStats.diErrorFrameCount + 1;
                                        stStats.iConsecutiveBadFrames := stStats.iConsecutiveBadFrames + 1;
                                        iConsecutiveGoodFrames := 0;
                                        stBuffer.iReadPtr := (stBuffer.iReadPtr + 12) MOD 256;
                                        stBuffer.iDataInBuffer := stBuffer.iDataInBuffer - 12;
                                        CONTINUE;
                                    END_IF;
                                END_IF;

                                // Validate digit characters
                                IF (BYTE_TO_INT(aFrame[3]) >= 16#30 AND BYTE_TO_INT(aFrame[3]) <= 16#39) AND
                                   (BYTE_TO_INT(aFrame[4]) >= 16#30 AND BYTE_TO_INT(aFrame[4]) <= 16#39) AND
                                   (BYTE_TO_INT(aFrame[6]) >= 16#30 AND BYTE_TO_INT(aFrame[6]) <= 16#39) AND
                                   (BYTE_TO_INT(aFrame[7]) >= 16#30 AND BYTE_TO_INT(aFrame[7]) <= 16#39) AND
                                   (BYTE_TO_INT(aFrame[8]) >= 16#30 AND BYTE_TO_INT(aFrame[8]) <= 16#39) THEN

                                    // Checksum validation
                                    IF stConfig.xEnableChecksum THEN
                                        iChecksumSum := 0;
                                        FOR i := 0 TO 8 DO
                                            iChecksumSum := iChecksumSum + BYTE_TO_INT(aFrame[i]);
                                        END_FOR;
                                        bCalculatedChecksum := INT_TO_BYTE(iChecksumSum MOD 256);
                                        
                                        // Extract received checksum
                                        tempInt := BYTE_TO_INT(aFrame[9]);
                                        IF tempInt >= 16#30 AND tempInt <= 16#39 THEN
                                            tempInt := tempInt - 16#30;
                                        ELSIF tempInt >= 16#41 AND tempInt <= 16#46 THEN
                                            tempInt := tempInt - 16#37;
                                        ELSIF tempInt >= 16#61 AND tempInt <= 16#66 THEN
                                            tempInt := tempInt - 16#57;
                                        ELSE
                                            tempInt := 0;
                                        END_IF;
                                        bReceivedChecksum := INT_TO_BYTE(tempInt * 16);
                                        
                                        tempInt := BYTE_TO_INT(aFrame[10]);
                                        IF tempInt >= 16#30 AND tempInt <= 16#39 THEN
                                            tempInt := tempInt - 16#30;
                                        ELSIF tempInt >= 16#41 AND tempInt <= 16#46 THEN
                                            tempInt := tempInt - 16#37;
                                        ELSIF tempInt >= 16#61 AND tempInt <= 16#66 THEN
                                            tempInt := tempInt - 16#57;
                                        ELSE
                                            tempInt := 0;
                                        END_IF;
                                        bReceivedChecksum := bReceivedChecksum OR INT_TO_BYTE(tempInt);
                                        
                                        IF bCalculatedChecksum <> bReceivedChecksum THEN
                                            stStats.diChecksumErrorCount := stStats.diChecksumErrorCount + 1;
                                            stStats.iConsecutiveBadFrames := stStats.iConsecutiveBadFrames + 1;
                                            iConsecutiveGoodFrames := 0; // Reset consecutive good frames counter
                                            stBuffer.iReadPtr := (stBuffer.iReadPtr + 12) MOD 256;
                                            stBuffer.iDataInBuffer := stBuffer.iDataInBuffer - 12;
                                            CONTINUE;
                                        END_IF;
                                    END_IF;

                                    // Parse valid frame
                                    // At this point, frame is structurally good (format, digits, checksum OK or disabled)
                                    iConsecutiveGoodFrames := iConsecutiveGoodFrames + 1;
                                    IF iConsecutiveGoodFrames > 2 THEN // Cap at 2 for "last two frames" logic
                                        iConsecutiveGoodFrames := 2;
                                    END_IF;
                                    stStats.iConsecutiveBadFrames := 0; // Reset consecutive bad *structural* frames counter
                                    tempIntValue := 0;
                                    tempInt := BYTE_TO_INT(aFrame[3]);
                                    tempIntValue := (tempInt - 16#30) * 10;
                                    tempInt := BYTE_TO_INT(aFrame[4]);
                                    tempIntValue := tempIntValue + (tempInt - 16#30);
                                    rCurrentMA := INT_TO_REAL(tempIntValue);
                                    
                                    tempInt := BYTE_TO_INT(aFrame[6]);
                                    rCurrentMA := rCurrentMA + INT_TO_REAL(tempInt - 16#30) / 10.0;
                                    tempInt := BYTE_TO_INT(aFrame[7]);
                                    rCurrentMA := rCurrentMA + INT_TO_REAL(tempInt - 16#30) / 100.0;
                                    tempInt := BYTE_TO_INT(aFrame[8]);
                                    rCurrentMA := rCurrentMA + INT_TO_REAL(tempInt - 16#30) / 1000.0;

                                    // Calculate Brix
                                    IF (rCurrentMA >= 4.0) AND (rCurrentMA <= 20.0) THEN
                                        rBrixValue := (rCurrentMA - 4.0) * stConfig.rBrixMax / 16.0;
                                        
                                        // *** UPDATE FIFO TABLES (if enabled) ***
                                        IF xEnableFifoDebug THEN
                                            // Shift all elements up (newest at [0], oldest discarded at [9])
                                            FOR tempInt := 9 TO 1 BY -1 DO
                                                arBrixHistory[tempInt] := arBrixHistory[tempInt - 1];
                                                auiTimeHistory[tempInt] := auiTimeHistory[tempInt - 1];
                                            END_FOR;
                                            // Insert new values at [0]
                                            arBrixHistory[0] := rBrixValue;
                                            auiTimeHistory[0] := DINT_TO_INT(diAccumCycleMs MOD 32767); // Convert to UINT
                                        END_IF;
                                        
                                        // Set data valid pulse
                                        xDataValid := TRUE;

                                        // Check if initialization phase should finish
                                        IF NOT xInitPhaseFinish AND diInitPhaseTimerMs > (stTiming.diFrameIntervalMs * 2) THEN
                                            IF stBuffer.iRxLength = 12 THEN
                                                xInitPhaseFinish := TRUE; // Synchronized and clean buffer
                                            END_IF;
                                        END_IF;
                                        
                                        // Update moving average
                                        IF stDiag.rAvgCurrentMA = 0.0 THEN
                                            stDiag.rAvgCurrentMA := rCurrentMA;
                                        ELSE
                                            stDiag.rAvgCurrentMA := stDiag.rAvgCurrentMA * 0.9 + rCurrentMA * 0.1;
                                        END_IF;
                                        
                                        // stStats.iConsecutiveBadFrames already reset as frame is structurally good
                                    END_IF;
                                    
                                    // Frame processing complete - transition to COMPLETED
                                    iReadingState := 1;
                                    
                                    // Increment frame counter
                                    stStats.diFrameCount := stStats.diFrameCount + 1;
                                    IF stStats.diFrameCount >= DINT#2000000000 THEN
                                        stStats.diFrameCount := 0;
                                    END_IF;

                                ELSE // Invalid digits
                                    stStats.diErrorFrameCount := stStats.diErrorFrameCount + 1;
                                    stStats.iConsecutiveBadFrames := stStats.iConsecutiveBadFrames + 1;
                                    iConsecutiveGoodFrames := 0; // Reset consecutive good frames counter
                                END_IF;

                            ELSE // Invalid format (no '.' at position 5)
                                stStats.diErrorFrameCount := stStats.diErrorFrameCount + 1;
                                stStats.iConsecutiveBadFrames := stStats.iConsecutiveBadFrames + 1;
                                iConsecutiveGoodFrames := 0; // Reset consecutive good frames counter
                            END_IF;

                            // Discard processed frame
                            stBuffer.iReadPtr := (stBuffer.iReadPtr + 12) MOD 256;
                            stBuffer.iDataInBuffer := stBuffer.iDataInBuffer - 12;

                        ELSE // Not enough bytes for full frame
                            EXIT;
                        END_IF;

                    ELSE // No '#' found
                        stBuffer.iReadPtr := (stBuffer.iReadPtr + stBuffer.iDataInBuffer) MOD 256;
                        stBuffer.iDataInBuffer := 0;
                        EXIT;
                    END_IF;
                END_WHILE;
                
            ELSIF TRCV_DB.ERROR THEN
                IF (TRCV_DB.STATUS = W#16#80A3) OR (TRCV_DB.STATUS = W#16#80C4) THEN 
                    xConnected := FALSE;
                    TCON_DB(REQ := FALSE, ID := wConnectionID, CONNECT := stConnParams);
                    TRCV_DB(EN_R := FALSE, ID := wConnectionID, LEN := 0, DATA := aRxBuffer);
                    IF xTdisconReq THEN
                         xTdisconReq := FALSE; 
                         TDISCON_DB(REQ := FALSE, ID := wConnectionID);
                    END_IF;
                    tonNoDataTimeout(IN := FALSE);
                    iState := 0;
                ELSIF TRCV_DB.STATUS = W#16#8088 THEN
                    tonNoDataTimeout(IN := FALSE);
                ELSE
                    wErrorCode := TRCV_DB.STATUS;
                    xConnected := FALSE;
                    iState := 4;
                END_IF;
            END_IF;
            
            // Data timeout check - limited by diAccumCycleMs max = iDataTimeoutSec
            tempTime := DINT_TO_TIME(INT_TO_DINT(stConfig.iDataTimeoutSec) * 1000);
            tonNoDataTimeout(IN := TRUE, PT := tempTime);
            IF tonNoDataTimeout.Q THEN
                wErrorCode := W#16#8002;
                iState := 4;
            END_IF;
            
            IF NOT xEnable THEN
                iState := 4;
            END_IF;
            
        4:  // DISCONNECT
            xTdisconReq := TRUE;
            TDISCON_DB(
                REQ := xTdisconReq,
                ID := wConnectionID
            );
            
            tonDisconTimeout(IN := xTdisconReq, PT := T#2S);
            
            IF TDISCON_DB.DONE OR TDISCON_DB.ERROR OR tonDisconTimeout.Q THEN
                xTdisconReq := FALSE;
                TDISCON_DB(REQ := xTdisconReq, ID := wConnectionID);
                xConnected := FALSE;
                tonDisconTimeout(IN := FALSE);
                TCON_DB(REQ := FALSE, ID := wConnectionID, CONNECT := stConnParams);
                TRCV_DB(EN_R := FALSE, ID := wConnectionID, LEN := 0, DATA := aRxBuffer);
                iState := 0;
            END_IF;
            
    END_CASE;
    
END_FUNCTION_BLOCK