*** 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](https://www.amazon.it/dp/B0CFXX5SKS?ref=ppx_yo2ov_dt_b_fed_asin_title) ), 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](https://1drv.ms/v/c/129c0e590bbc3a9e/EedcDsWnAwFKkzfMdsEloHoBJoapLe0CjJq9dpG5ch6qzw?e=UpMASB) Link dal software S7 utlizato per Test: [Microsoft OneDrive](https://1drv.ms/u/c/129c0e590bbc3a9e/ETpiLUQCwNRLn4rQFx8ygkcBDFsQ6HRxXy7o0FGd15FBcA?e=5fld6z) *** #### 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: *** ```pascal //****************************************************************************** // 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 ```