Modbus RTU Master

rage
Benutzer
Avatar
Gender: n/a
Age: 64
Homepage: processanalytik.de
Posts: 235
Registered: 02 / 2007
Subject:

Modbus RTU Master

 · 
Posted: 14.10.2022 - 08:57  ·  #1
Guten Morgen,

ich muss einen Sensor an unsere Hardware anschließen und halt auch Daten davon lesen. Zwar sieht Modbus RTU nicht so wirklich dramatisch aus, aber ich frage trotzdem mal. Hat irgendwer das schon mal erledigt und könnte den Sourcecode zur Verfügung stellen?

Ein schönen Tag noch
Ralf
pvs-deck
PowerUser
Avatar
Gender:
Age: 53
Homepage: pvs-deck.de
Posts: 1340
Registered: 02 / 2009
Subject:

Re: Modbus RTU Master

 · 
Posted: 14.10.2022 - 19:21  ·  #2
Quote by rage

Guten Morgen,

ich muss einen Sensor an unsere Hardware anschließen und halt auch Daten davon lesen. Zwar sieht Modbus RTU nicht so wirklich dramatisch aus, aber ich frage trotzdem mal. Hat irgendwer das schon mal erledigt und könnte den Sourcecode zur Verfügung stellen?

Ein schönen Tag noch
Ralf


Hallo Ralf,

also hast Du einen Sensor der Modbus-RTU liefert und willst diesen verwenden?

Also ich habe schon mit AVRco einige Steuerungen mit Modbus-RTU (Slave) im Einsatz.

Als Master und Slave habe ich Modbus-ASCII im Einsatz, ist einfacher zu verarbeiten und ist nicht so zeitkritisch wie das RTU. Beim Slave nutze ich die vorhandenen Modbusfunktionen vom AVRco. Beim Master habe ich mir mal einge Routinen gebaut um mir diese entsprechend zu benutzen.

Im Prinzip recht einfach du sendest ein Telegram an die Slave Adr und wartest auf die Antwort.
Sobald die Anwort kommt, Prüfst Du die Gültigkeit, Länge und Checksumme und verarbeitest die Daten.

Die Routinen die ich mir gebaut habe sind aus 2015/2016 und eigentlich genau auf meine eigenen Slaves aufgebaut. Aber diese könntest Du Dir dann für Deinen Slave anpassen, aber es sind nur MODBUS-ASCII Funktionen.

Diese Steuerungen laufen schon seit 2015/2016 im 24h Betrieb ohne einen einzigen Ausfall, schaue Dir die Routinen einfach mal an da kannst Du bestimmt was passendes ableiten.
Mittlerweile würde ich einige Sachen anderst aufbauen und programmieren, aber der Code funktioniert so.

Die Meisten Endgeräte/Sensoren können RTU und ASCII, meistens kann man diese umschalten.

Im Mainloop:
Code
...
loop
 SetData; // Daten für ModbusSlave

 ReadInput;

// COMTimer, TimeOut, RestartTimer
 SlaveCom(6, 8, 8);

// Schreibe Ausgänge an Hardware
 WriteOutput;

 endloop;
...


Im SlaveCom ist die eigentliche Verbindung und Verarbeitung drinnen:
Code

procedure SlaveCom( bComTime : word; bTOTime : byte; bStartTime : word);
var
 strTest : String[100];
 bCh     : char;
begin
// Sende an Adresse 1
//

if ((ComState=0)AND isSysTimerZero( ComTimer ))then
 ComState:= 1;
 Toggle( OUT_LED_COM );
  SetSysTimer( ComTimer, bComTime);

endif;

if ((ComState = 2) ) then

    if SerInp_TO2( bCh, bTOTime ) then

      if  bCh=CHAR($0A) then

      elsif bCh=CHAR($0D) then
      
      else
           strTest:=strTest+bCh;
       endif;

         if (bCh=CHAR($0A)) then
       //    DebugOut('Read vom Slave : '+strTest);
             ComErr1:=0;
            ComState:= 3;
             SetSysTimer( ComTimer, bComTime);
               Toggle( OUT_LED_COM );
                bComErr1:=false;
            
        if ModWriteAnsw( SlaveData.Slave[0].Adr, SlaveData.Slave[0].FuncWrite, SlaveData.Slave[0].WriteRegister, SlaveData.Slave[0].WriteBuffer, strTest) then
          //DebugOut('1.OK, von Adr. '+ ByteToStr( SlaveData.Slave[0].Adr));
         else
           bComErr1:=true;
            DebugOut('1.Fehler, von Adr. '+ ByteToStr(SlaveData.Slave[0].Adr));
         endif;

         strTest:='';
         
            
         endif; // Ende Telegramm
     else
      if ComErr1 < 1 then
       ComState:= 1;
        SetSysTimer( ComTimer, bStartTime); //+50
         ComErr1:=ComErr1+1;
      else
       ComState:= 3;
        ComErr1:= 0;
        SetSysTimer( ComTimer, bStartTime); //+50
         bComErr1:=true;
        DebugOut('1.TimneOut, von Adr. '+ ByteToStr( SlaveData.Slave[0].Adr)+' => '+strTest);
      endif;
      
     endif; // Ende SerInp

endif; // ComState2


if ((ComState = 1) AND isSysTimerZero( ComTimer ))  then

    //DebugOut('1.Write zum Slave: '+ModWriteTel( SlaveData.Slave[0].Adr, SlaveData.Slave[0].FuncWrite, SlaveData.Slave[0].WriteRegister, SlaveData.Slave[0].WriteBuffer));
     Ser_Enable2 (true);
      WriteLn (SerOut2, ModWriteTel( SlaveData.Slave[0].Adr, SlaveData.Slave[0].FuncWrite, SlaveData.Slave[0].WriteRegister, SlaveData.Slave[0].WriteBuffer));
       Ser_Enable2 (false);

        strTest:='';
         ComState:= 2;
           Toggle( OUT_LED_COM );
         //  SetSysTimer( ComTimer, bComTime);
endif;

// Teil 2

if ((ComState = 4) ) then

    if SerInp_TO2( bCh, bTOTime ) then

      if  bCh=CHAR($0A) then

      elsif bCh=CHAR($0D) then

      else
           strTest:=strTest+bCh;
       endif;
       
         if (bCh=CHAR($0A)) then
           //DebugOut('Read vom Slave : '+strTest);
            ComState:= 5;
             ComErr2:= 0;
               bComErr2:=false;
             SetSysTimer( ComTimer, bComTime);
               Toggle( OUT_LED_COM );

        if ModWriteAnsw( SlaveData.Slave[1].Adr, SlaveData.Slave[1].FuncWrite, SlaveData.Slave[1].WriteRegister, SlaveData.Slave[1].WriteBuffer, strTest) then
          //DebugOut('2.OK, von Adr. '+ ByteToStr( SlaveData.Slave[0].Adr));
         else
           DebugOut('2.Fehler, von Adr. '+ ByteToStr(SlaveData.Slave[1].Adr));
            bComErr2:=true;
         endif;

         strTest:='';

         endif; // Ende Telegramm
     else
     
     
       if ComErr2 < 1 then // Fehler
       ComState:= 3;
        SetSysTimer( ComTimer, bStartTime); //+50
         ComErr2:=ComErr2+1;
      else             //2. Fehler
       ComState:= 5;
        ComErr2:= 0;
        SetSysTimer( ComTimer, bStartTime); //+50
        DebugOut('2.TimneOut, von Adr. '+ ByteToStr( SlaveData.Slave[1].Adr)+' => '+strTest);
         bComErr2:=true;
      endif;

     endif; // Ende SerInp

endif; // ComState4


if ((ComState = 3) AND isSysTimerZero( ComTimer ))  then
    //DebugOut('2.Write zum Slave: '+ModWriteTel( SlaveData.Slave[1].Adr, SlaveData.Slave[1].FuncWrite, SlaveData.Slave[1].WriteRegister, SlaveData.Slave[1].WriteBuffer));
     Ser_Enable2 (true);
      WriteLn (SerOut2, ModWriteTel( SlaveData.Slave[1].Adr, SlaveData.Slave[1].FuncWrite, SlaveData.Slave[1].WriteRegister, SlaveData.Slave[1].WriteBuffer));
       Ser_Enable2 (false);
        strTest:='';
         ComState:= 4;
           Toggle( OUT_LED_COM );
        //   SetSysTimer( ComTimer, bComTime);

endif;

// ab hier Read
//--------------------------------------------------------------------------
//
//    ´Knoten 1
//

if ((ComState = 6) ) then

    if SerInp_TO2( bCh, bTOTime ) then

      if  bCh=CHAR($0A) then

      elsif bCh=CHAR($0D) then

      else
           strTest:=strTest+bCh;
       endif;

         if (bCh=CHAR($0A)) then
           //DebugOut('2. Read vom Slave : '+strTest);
            ComState:= 7;
             ComErr2:= 0;
               bComErr2:=False;
             SetSysTimer( ComTimer, bComTime);
               Toggle( OUT_LED_COM );


        if ModReadAnsw( SlaveData.Slave[1].Adr, SlaveData.Slave[1].FuncRead, SlaveData.Slave[1].ReadNum * 2, strTest, false, 1) then
          //DebugOut('3.OK, von Adr. '+ ByteToStr( SlaveData.Slave[0].Adr));
         else
           DebugOut('3.Fehler, von Adr. '+ ByteToStr(SlaveData.Slave[1].Adr));
            bComErr2:=true;
         endif;
         strTest:='';

         endif; // Ende Telegramm
     else


       if ComErr2 < 1 then
       ComState:= 5;
        SetSysTimer( ComTimer, bStartTime); //+50
         ComErr2:= ComErr2+1;
      else
       ComState:= 7;
        ComErr2:= 0;
        SetSysTimer( ComTimer, bStartTime); //+50
        DebugOut('3.TimneOut, von Adr. '+ ByteToStr( SlaveData.Slave[1].Adr)+' => '+strTest);
         bComErr2:=true;
      endif;

     endif; // Ende SerInp
endif; // ComState4

// Read Start
if ((ComState = 5) AND isSysTimerZero( ComTimer ))  then

    //DebugOut('3. Ausgabe:  '+ModReadTel( SlaveData.Slave[1].Adr, SlaveData.Slave[1].FuncRead, SlaveData.Slave[1].ReadRegister, SlaveData.Slave[1].ReadNum));
     Ser_Enable2 (true);
      WriteLn (SerOut2, ModReadTel( SlaveData.Slave[1].Adr, SlaveData.Slave[1].FuncRead, SlaveData.Slave[1].ReadRegister, SlaveData.Slave[1].ReadNum));
       Ser_Enable2 (false);
        strTest:='';
         ComState:= 6;
           Toggle( OUT_LED_COM );
         //  SetSysTimer( ComTimer, bComTime);

endif;

//--------------------------------------------------------------------------
//
//    ´Knoten 0
//
if ((ComState = 8) ) then

    if SerInp_TO2( bCh, bTOTime ) then

      if  bCh=CHAR($0A) then

      elsif bCh=CHAR($0D) then

      else
           strTest:=strTest+bCh;
       endif;

         if (bCh=CHAR($0A)) then
           //DebugOut('3. Read vom Slave : '+strTest);
            ComState:= 0;
             ComErr1:= 0;
               bComErr1:=false;
             SetSysTimer( ComTimer, bComTime);
               Toggle( OUT_LED_COM );

        if ModReadAnsw( SlaveData.Slave[0].Adr, SlaveData.Slave[0].FuncRead, SlaveData.Slave[0].ReadNum * 2, strTest, false,0 ) then
          //DebugOut('4.OK, von Adr. '+ ByteToStr( SlaveData.Slave[0].Adr));
         else
           DebugOut('4.Fehler, von Adr. '+ ByteToStr(SlaveData.Slave[0].Adr));
            bComErr1:=true;
         endif;
         strTest:='';

         endif; // Ende Telegramm
     else
       if ComErr1 < 1 then
       ComState:= 7;
        SetSysTimer( ComTimer, bStartTime); //+50
         ComErr1:= ComErr1+1;
      else
       ComState:= 0;
        ComErr1:= 0;
        SetSysTimer( ComTimer, bStartTime); //+50
        DebugOut('4.TimneOut, von Adr. '+ ByteToStr( SlaveData.Slave[0].Adr)+' => '+strTest);
         bComErr1:=true;
      endif;

     endif; // Ende SerInp


endif; // ComState4



// Read Start
if ((ComState = 7) AND isSysTimerZero( ComTimer ))  then

    //DebugOut('4. Ausgabe:  '+ModReadTel( SlaveData.Slave[0].Adr, SlaveData.Slave[0].FuncRead, SlaveData.Slave[0].ReadRegister, SlaveData.Slave[0].ReadNum));
     Ser_Enable2 (true);
      WriteLn (SerOut2, ModReadTel( SlaveData.Slave[0].Adr, SlaveData.Slave[0].FuncRead, SlaveData.Slave[0].ReadRegister, SlaveData.Slave[0].ReadNum));
       Ser_Enable2 (false);
        strTest:='';
         ComState:= 8;
           Toggle( OUT_LED_COM );
           SetSysTimer( ComTimer, bComTime);

endif;

end; // Ende Slave Com


Die Funktion ModWriteAnsw:
Code
// :01 10 001E 0001 D0
function ModWriteAnsw( Adr: byte; Func : byte; Register : word; wWrite : word; strAntwort : String[100]  ) : boolean;
var
 StrBufferTel: String[100];
 LRC : word;
 inBufferArr   : Array[0..99] of integer;
 ix : integer;
 str2 : string[2];
 str4 : string[4];

 
begin
// clear
 for ix := 0 to 99 do
  inBufferArr[ix]:=0;
 endfor;

if Length(strAntwort) < 14 then // Antwort zu klein -> Raus
   DebugOut('Telegram länge: '+ strAntwort);
    return (false);
     endif;

 if strAntwort[1]<>':' then // Keine : am Anfang, dann mit false raus
   DebugOut('Telegram Falsch: '+ strAntwort);
   return (false);
    endif;
    
//Berechne wie Antwort aussehen sollte
 LRC:= 0;
  LRC:= LRC+ Word(Adr);
   LRC:= LRC+ Word(Func);
    LRC:= LRC+ Word(Register);
     LRC:= LRC+ Word(1);
 LRC:= (LRC XOR $FF) +1;
  LRC:= LRC AND $FF;

 StrBufferTel:=':'+ByteToHex(Adr)+ByteToHex(Func)+IntToHex(Register)+IntToHex(1)+ByteToHex( Byte(LRC));
   //DebugOut('Telegram SOLL: '+ StrBufferTel);
   //DebugOut('Telegram:      '+ strAntwort);

 if StrBufferTel =  strAntwort then // Telegramm gleich?
  return (true);
 else
  return (false);
 endif;

end;


Antwort vom Slave verarbeiten:
Code
//------------------------------------------------------------------------------
//
// Antwort vom Slave:
// HEX:
// 3A 30 31 30 33 30 38 30 30 30 30 30 30 30 30 30 30 30 33 30 30 38 33 36 45 0D 0A
//
// ASCII:
// :01 03 08 0000  0000  0003  0081 70 CR LF
//  01 03 08 0000  0000  0003  0081 70 CR LF
// :01 03 08 0000  0000  0003  0083 6E CR LF
//   |  |  |  ---   ---   ---   ---   |
//   |  |  |   |     |     |     |    |- LRC
//   |  |  |   |     |     |     |-- 4. Register
//   |  |  |   |     |     |-- 3. Register
//   |  |  |   |     |-- 2. Register
//   |  |  |   |--- 1. Register
//   |  |  |---Anzahl der Bytes
//   |  |---Functionscode
//   |---Adresse
//
function ModReadAnsw( Adr: byte; Func : byte; Count : Byte; strAntwort : String[100]; Print : boolean; Knoten : byte  ) : boolean;
var
 StrBufferTel: String[100];
 LRC : word;
// inBufferArr   : Array[0..99] of integer;
 ix : integer;
 str2 : string[2];
 str4 : string[4];
 str20 : string[20];
 iBuffer : WORD;
 bBuffer : byte;
 iZiel : integer;
 wReg1 : word;
 wReg2 : word;
 wReg3 : word;
 wReg4 : word;

begin
// clear
// for ix := 0 to 99 do
//  inBufferArr[ix]:=0;
// endfor;

if Length(strAntwort) < 25 then // Antwort zu klein -> Raus
   DebugOut('Telegram länge read: '+ strAntwort);
    return (false);
     endif;

 if strAntwort[1]<>':' then // Keine : am Anfang, dann mit false raus
   DebugOut('Telegram falsch Read: '+ strAntwort);
   return (false);
    endif;

// Checke ob richtige Adresse
str2:= strAntwort[2]+strAntwort[3];
  bBuffer:= HexToInt( str2 );
 if  bBuffer <> Adr then // Antwort von falscher Adresse
   DebugOut('Telegram falsche Adresse: '+ strAntwort);
   return (false);
    endif;

// Checke ob richtiger Functionscode
str2:= strAntwort[4]+strAntwort[5];
  bBuffer:= HexToInt( str2 );
 if  bBuffer <> Func then // Antwort von falscher Functionscode
   DebugOut('Telegram falscher Functionscode: '+ strAntwort);
   return (false);
    endif;

// Checke ob richtige Anzahl der Bytes
str2:= strAntwort[6]+strAntwort[7];
  bBuffer:= HexToInt( str2 );
 if  bBuffer <> Count then // Falsche Anzahl an Bytes
   DebugOut('Telegram falsche Anzahl der Bytes: '+ strAntwort);
   return (false);
    endif;


//Berechne LRC
//
 LRC:= 0;
 // Adresse
  str2:= strAntwort[2]+strAntwort[3];
   LRC:= LRC + word(HexToInt( str2 ));
 // Functionscode
  str2:= strAntwort[4]+strAntwort[5];
   LRC:= LRC + word(HexToInt( str2 ));
 // Anzahl der Bytes
  str2:= strAntwort[6]+strAntwort[7];
   LRC:= LRC + word(HexToInt( str2 ));
    iZiel:= HexToInt( str2 ); // Anzahl der Bytes für Schleife

 // Register
  str2:= strAntwort[8]+strAntwort[9];
   LRC:= LRC + word(HexToInt( str2 ));
 // Register
  str2:= strAntwort[10]+strAntwort[11];
   LRC:= LRC + word(HexToInt( str2 ));
   // Register zur Übertragung vorbereiten
    str4:=  strAntwort[8]+strAntwort[9]+strAntwort[10]+strAntwort[11];
    wReg1 :=  word(HexToInt( str4 ));

 // Register
  str2:= strAntwort[12]+strAntwort[13];
   LRC:= LRC + word(HexToInt( str2 ));
 // Register
  str2:= strAntwort[14]+strAntwort[15];
   LRC:= LRC + word(HexToInt( str2 ));
   // Register zur Übertragung vorbereiten
    str4:=  strAntwort[12]+strAntwort[13]+strAntwort[14]+strAntwort[15];
    wReg2 :=  word(HexToInt( str4 ));

 // Register
  str2:= strAntwort[16]+strAntwort[17];
   LRC:= LRC + word(HexToInt( str2 ));
 // Register
  str2:= strAntwort[18]+strAntwort[19];
   LRC:= LRC + word(HexToInt( str2 ));
   // Register zur Übertragung vorbereiten
    str4:=  strAntwort[16]+strAntwort[17]+strAntwort[18]+strAntwort[19];
    wReg3 :=  word(HexToInt( str4 ));

 // Register
  str2:= strAntwort[20]+strAntwort[21];
   LRC:= LRC + word(HexToInt( str2 ));
 // Register
  str2:= strAntwort[22]+strAntwort[23];
   LRC:= LRC + word(HexToInt( str2 ));
   // Register zur Übertragung vorbereiten
    str4:=  strAntwort[20]+strAntwort[21]+strAntwort[22]+strAntwort[23];
    wReg4 :=  word(HexToInt( str4 ));
 // End Berechnung
 LRC:= (LRC XOR $FF) +1;
  LRC:= LRC AND $FF;


  if Print then
   DebugOut('---------');
   ix:= 7 + (iZiel*2)+1;
    str2 := strAntwort[ix]+ strAntwort[ix+1];
    iBuffer :=  HexToInt( str2 );
     str20:= IntToStr( iBuffer );
    DebugOut('LRC in  : '+ str20);
   DebugOut('LRC calc: '+ IntToStr(  LRC ) );
  else
   ix:= 7 + (iZiel*2)+1;
    str2 := strAntwort[ix]+ strAntwort[ix+1];
    iBuffer :=  HexToInt( str2 );
     str20:= IntToStr( iBuffer );
  endif;


 if LRC = iBuffer   then // LRC OK ?
   // Nur wenn LRC ok, dann Daten einsortieren
   //
   SlaveData.Slave[Knoten].ReadBuffer1 := wReg1;
    SlaveData.Slave[Knoten].ReadBuffer2 := wReg2;
     SlaveData.Slave[Knoten].ReadBuffer3 := wReg3;
      SlaveData.Slave[Knoten].ReadBuffer4 := wReg4;
       return (true);
 else
  return (false);
 endif;


end;


ModWriteTel:
Code

// WRITE:
// Modbus Ascii alles EIN:
// HEX:
// 3A 30 31 31 30 30 30 31 45 30 30 30 31 30 32 30 30 46 46 43 46 0D 0A
// ASCII:
//  Test
// :01 10 00 1E 00 01 02 00 FF CF <CR><LF>
//   |  |     |     |  |  ---   |- LRC
//   |  |     |     |  |   | Register Inhalt
//   |  |     |     |  |- Anzahl der Bytes
//   |  |     |     |-- Anzahl der Register
//   |  |     |-- Ab Regsiter Hex
//   |  |--- Funktionscode
//   |--- Adresse

function ModWriteTel( Adr: byte; Func : byte; Register : word; wWrite : word  ) : String[100];
var
 StrBufferTel: String[100];
 LRC : word;

begin
 LRC:= 0;
  LRC:= LRC+ Word(Adr);
   LRC:= LRC+ Word(Func);
    LRC:= LRC+ Word(Register);
     LRC:= LRC+ Word(1);
      LRC:= LRC+ Word(2);
//     LRC:= LRC+ Word(wWrite);
        LRC:= LRC+ Word( WordByte(wWrite, true)  );
         LRC:= LRC+ Word( WordByte(wWrite, false)  );

 LRC:= (LRC XOR $FF) +1;
  LRC:= LRC AND $FF;

 StrBufferTel:=':'+ByteToHex(Adr)+ByteToHex(Func)+IntToHex(Register)+IntToHex(1)+ByteToHex(2)+IntToHex(wWrite)+ByteToHex( Byte(LRC));
return (StrBufferTel);

end;


ModReadTel:
Code
//---------------------------------------------------------------------
//HEX:
//3A 30 31 30 33 30 30 31 30 30 30 30 34 45 38 0D OA
//
//ASCII:
//:01 03 00 10 00 04 E8 CR LF
//  |  |     |     |  |- LRC
//  |  |     |     |-- Anzahl der Register
//  |  |     |-- Ab Regsiter Hex
//  |  |--- Funktionscode
//  |--- Adresse

function ModReadTel( Adr: byte; Func : byte; Register : word; Count : byte  ) : String[100];
var
 StrBufferTel: String[100];
 LRC : word;

begin
 LRC:= 0;
  LRC:= LRC+ Word(Adr);
   LRC:= LRC+ Word(Func);
    LRC:= LRC+ Word(Register);
     LRC:= LRC+ Word(Count);
 LRC:= (LRC XOR $FF) +1;
  LRC:= LRC AND $FF;

 StrBufferTel:=':'+ByteToHex(Adr)+ByteToHex(Func)+IntToHex(Register)+IntToHex( Word(Count))+ByteToHex( Byte(LRC));
return (StrBufferTel);

end;


Thorsten
pvs-deck
PowerUser
Avatar
Gender:
Age: 53
Homepage: pvs-deck.de
Posts: 1340
Registered: 02 / 2009
Subject:

Re: Modbus RTU Master

 · 
Posted: 25.10.2022 - 10:58  ·  #3
Hallo Ralf,

keine Fragen? Problem gelöst?

Thorsten
rage
Benutzer
Avatar
Gender: n/a
Age: 64
Homepage: processanalytik.de
Posts: 235
Registered: 02 / 2007
Subject:

Re: Modbus RTU Master

 · 
Posted: 04.11.2022 - 15:18  ·  #4
Hallo Thorsten,
erstmal Danke sehr für die Hilfe. Ich bin gerade oder war gerade am Hausumbau, daher war meine Zeit die letzten 3 Wochen so ziemlich bei null. Leider kann der Sensor nicht auf ASCII umgestellt werden. Ich werde wohl doch einen MODBUS-Master "basteln" müssen. Der erste Versuch war heute garnicht so negativ. Ich werde mal nach dem Wochenende sehen wo es hinführt.
Schöne Wochenende und Danke nochmal.
Selected quotes for multi-quoting:   0

Registered users in this topic

Currently no registered users in this section

The statistic shows who was online during the last 5 minutes. Updated every 90 seconds.
MySQL Queries: 15 · Cache Hits: 14   81   95 · Page-Gen-Time: 0.019264s · Memory Usage: 2 MB · GZIP: on · Viewport: SMXL-HiDPI