- Rearranged directory structure for forking out ANSTO
- Refactored site specific stuff into a site module - PSI specific stuff is now in the PSI directory. - The old version has been tagged with pre-ansto
This commit is contained in:
682
sanscook.c
Normal file
682
sanscook.c
Normal file
@ -0,0 +1,682 @@
|
||||
/*--------------------------------------------------------------------------
|
||||
|
||||
S A N S C O O K
|
||||
|
||||
This is a controller driver for a heaiting device developed especially
|
||||
for use with SANS at SINQ by somebody at the Some God Forsaken University
|
||||
(SGFU). As this device comes with two motors in addition to the heater
|
||||
the general controller mechanism as described in choco.tex is used.
|
||||
|
||||
copyright: see copyright.h
|
||||
|
||||
Mark Koennecke, July 2000
|
||||
---------------------------------------------------------------------------*/
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
#include <time.h>
|
||||
#include <fortify.h>
|
||||
#include <sics.h>
|
||||
#include <stringdict.h>
|
||||
#include "hardsup/serialsinq.h"
|
||||
#include "hardsup/el734_errcodes.h"
|
||||
#include "hardsup/el734fix.h"
|
||||
#include <codri.h>
|
||||
|
||||
/*-----------------------------------------------------------------------
|
||||
A private data structure for the SANS cooker
|
||||
-------------------------------------------------------------------------*/
|
||||
typedef struct {
|
||||
char *pHost;
|
||||
int iPort;
|
||||
int iChannel;
|
||||
void *pData;
|
||||
int iStop;
|
||||
int iError;
|
||||
} SANSCook, *pSANSCook;
|
||||
/*
|
||||
pHost, iPort and iChannel combined are the adress of the cooker
|
||||
controller at the Macintosh terminal server. pData is the serial
|
||||
port connection data structure needed and managed by the SerialIO
|
||||
functions.
|
||||
|
||||
iError is the last error reported on this device. If no error: 0
|
||||
|
||||
-----------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
ERROR CODES:
|
||||
*/
|
||||
|
||||
#define NOTINIT -9001
|
||||
#define POWEROFF -9002
|
||||
#define LOWRANGE -9003
|
||||
#define HIGHRANGE -9004
|
||||
#define CONTIMEOUT -9005
|
||||
#define BADCOMMAND -9006
|
||||
#define NOANSWER -9007
|
||||
#define BADINPUT -9008
|
||||
#define MOTERROR -9009
|
||||
#define CONBUSY -9010
|
||||
#define BADREPLY -9011
|
||||
|
||||
/*-------------------------------------------------------------------------*/
|
||||
static int CookerInit(pCodri self)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
int iRet;
|
||||
char pReply[131];
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
pPriv->iError = 0;
|
||||
|
||||
/* first open the connection to the serial port server and channel */
|
||||
iRet = SerialOpen(&(pPriv->pData),pPriv->pHost,pPriv->iPort,
|
||||
pPriv->iChannel);
|
||||
if(iRet <= 0)
|
||||
{
|
||||
pPriv->iError = iRet;
|
||||
return 0;
|
||||
}
|
||||
/* configure the connection */
|
||||
SerialATerm(&(pPriv->pData),"1\r\n");
|
||||
SerialSendTerm(&(pPriv->pData),"\r");
|
||||
|
||||
pPriv->iStop = 0;
|
||||
|
||||
/* switch everything on, but do not deal with errors here */
|
||||
/*
|
||||
SerialWriteRead(&(pPriv->pData),"mpon",pReply,131);
|
||||
SerialWriteRead(&(pPriv->pData),"mzon",pReply,131);
|
||||
SerialWriteRead(&(pPriv->pData),"ton",pReply,131);
|
||||
*/
|
||||
|
||||
return 1;
|
||||
}
|
||||
/*------------------------------------------------------------------------*/
|
||||
static int CookerClose(pCodri self)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
int iRet;
|
||||
long lVal;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
|
||||
if(pPriv->pData)
|
||||
{
|
||||
SerialClose(&(pPriv->pData));
|
||||
pPriv->pData = NULL;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
/*-----------------------------------------------------------------------*/
|
||||
static int CookerDelete(pCodri self)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
|
||||
if(pPriv->pData)
|
||||
{
|
||||
SerialClose(&(pPriv->pData));
|
||||
pPriv->pData = NULL;
|
||||
}
|
||||
|
||||
if(pPriv->pHost)
|
||||
free(pPriv->pHost);
|
||||
|
||||
free(pPriv);
|
||||
|
||||
return 1;
|
||||
}
|
||||
/*-----------------------------------------------------------------------*/
|
||||
static int CookerSetPar(pCodri self, char *parname, float fValue)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
char pCommand[80], pReply[132];
|
||||
int iRet, iValue;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
pPriv->iError = 0;
|
||||
|
||||
/* handle the numeric parameters */
|
||||
iValue = (int)fValue;
|
||||
|
||||
if(strcmp(parname,"mp") == 0)
|
||||
{
|
||||
sprintf(pCommand,"mps%3.3d",iValue);
|
||||
}
|
||||
else if(strcmp(parname,"mz") == 0)
|
||||
{
|
||||
sprintf(pCommand,"mzs%3.3d",iValue);
|
||||
}
|
||||
else if(strcmp(parname,"ts") == 0)
|
||||
{
|
||||
sprintf(pCommand,"ts%3.3d",iValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
pPriv->iError = BADINPUT;
|
||||
return 0;
|
||||
}
|
||||
/* send command and check for errors right here */
|
||||
iRet = SerialWriteRead(&(pPriv->pData),pCommand,pReply,131);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = iRet;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(strstr(pReply,"Error") != NULL)
|
||||
{
|
||||
pPriv->iError = MOTERROR;
|
||||
return 0;
|
||||
}
|
||||
if(strstr(pReply,"Power OFF") != NULL)
|
||||
{
|
||||
pPriv->iError = POWEROFF;
|
||||
return 0;
|
||||
}
|
||||
if(strstr(pReply,"not init.") != NULL)
|
||||
{
|
||||
pPriv->iError = NOTINIT ;
|
||||
return 0;
|
||||
}
|
||||
if(strstr(pReply,"Timeout") != NULL)
|
||||
{
|
||||
pPriv->iError = CONTIMEOUT ;
|
||||
return 0;
|
||||
}
|
||||
if(strstr(pReply,"Busy") != NULL)
|
||||
{
|
||||
pPriv->iError = CONBUSY ;
|
||||
return 0;
|
||||
}
|
||||
if( (strstr(pReply,"Max") != NULL) || (strstr(pReply,"high") != NULL) )
|
||||
{
|
||||
pPriv->iError = HIGHRANGE;
|
||||
return 0;
|
||||
}
|
||||
if( (strstr(pReply,"Min") != NULL) || (strstr(pReply,"down") != NULL) )
|
||||
{
|
||||
pPriv->iError = LOWRANGE;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
/*------------------------------------------------------------------------*/
|
||||
static int CookerSetPar2(pCodri self, char *parname, char *pValue)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
char pCommand[80], pReply[132];
|
||||
int iRet, iAct = 0;
|
||||
float fValue;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
pPriv->iError = 0;
|
||||
|
||||
/* handle our parameters. The first set is the power settings */
|
||||
if(strcmp(parname,"mp.power") == 0)
|
||||
{
|
||||
if(strstr(pValue,"on") != NULL)
|
||||
{
|
||||
strcpy(pCommand,"mpon");
|
||||
iAct = 1;
|
||||
}
|
||||
else if(strstr(pValue,"off") != NULL)
|
||||
{
|
||||
strcpy(pCommand,"mpoff");
|
||||
iAct = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
pPriv->iError = BADINPUT;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if(strcmp(parname,"mz.power") == 0)
|
||||
{
|
||||
if(strstr(pValue,"on") != NULL)
|
||||
{
|
||||
strcpy(pCommand,"mzon");
|
||||
iAct = 1;
|
||||
}
|
||||
else if(strstr(pValue,"off") != NULL)
|
||||
{
|
||||
strcpy(pCommand,"mzoff");
|
||||
iAct = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
pPriv->iError = BADINPUT;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if(strcmp(parname,"ts.power") == 0)
|
||||
{
|
||||
if(strstr(pValue,"on") != NULL)
|
||||
{
|
||||
iAct = 1;
|
||||
strcpy(pCommand,"ton");
|
||||
}
|
||||
else if(strstr(pValue,"off") != NULL)
|
||||
{
|
||||
iAct = 1;
|
||||
strcpy(pCommand,"toff");
|
||||
}
|
||||
else
|
||||
{
|
||||
pPriv->iError = BADINPUT;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if(iAct == 1)
|
||||
{
|
||||
/* send command and check for errors right here */
|
||||
iRet = SerialWriteRead(&(pPriv->pData),pCommand,pReply,131);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = iRet;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(strstr(pReply,"Error") != NULL)
|
||||
{
|
||||
pPriv->iError = MOTERROR;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* second set: init */
|
||||
if(strcmp(parname,"mp.init") == 0)
|
||||
{
|
||||
strcpy(pCommand,"mpi");
|
||||
iAct = 1;
|
||||
}
|
||||
else if(strcmp(parname,"mz.init") == 0)
|
||||
{
|
||||
strcpy(pCommand,"mzi");
|
||||
iAct = 1;
|
||||
}
|
||||
if(iAct)
|
||||
{
|
||||
/* send command and check for errors right here */
|
||||
iRet = SerialWriteRead(&(pPriv->pData),pCommand,pReply,131);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = iRet;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(strstr(pReply,"Error") != NULL)
|
||||
{
|
||||
pPriv->iError = MOTERROR;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
what is left here is either the para's themselves which will be
|
||||
handled in the second function after convertion to float or an
|
||||
error
|
||||
*/
|
||||
iRet = sscanf(pValue,"%f",&fValue);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = BADINPUT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return CookerSetPar(self,parname,fValue);
|
||||
}
|
||||
/*-----------------------------------------------------------------------*/
|
||||
static int CookerHalt(pCodri self)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
pPriv->iError = 0;
|
||||
|
||||
/* no commands are defined to halt a thing! */
|
||||
return 1;
|
||||
}
|
||||
/*----------------------------------------------------------------------*/
|
||||
extern char *stptok(const char *s, char *tok, size_t toklen, char *brk);
|
||||
|
||||
static int CookerGetPar(pCodri self, char *parname,
|
||||
char *pBuffer, int iLen)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
char pBueffel[256], pCommand[80], pReply[80];
|
||||
float fVal;
|
||||
char *pPtr;
|
||||
int iAct = 0, iRet;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
pPriv->iError = 0;
|
||||
|
||||
/* first the power stuff */
|
||||
if(strcmp(parname,"mp.power") == 0 || strcmp(parname,"mp.init") == 0)
|
||||
{
|
||||
strcpy(pBueffel,parname);
|
||||
strcpy(pCommand,"mps");
|
||||
iAct = 1;
|
||||
}
|
||||
else if(strcmp(parname,"mz.power") == 0 || strcmp(parname,"mz.init") ==0 )
|
||||
{
|
||||
strcpy(pBueffel,parname);
|
||||
strcpy(pCommand,"mzs");
|
||||
iAct = 1;
|
||||
}
|
||||
else if(strcmp(parname,"ts.power") == 0)
|
||||
{
|
||||
strcpy(pBueffel,parname);
|
||||
strcpy(pCommand,"ts");
|
||||
iAct = 1;
|
||||
}
|
||||
if(iAct)
|
||||
{
|
||||
iRet = SerialWriteRead(&(pPriv->pData),pCommand,pReply,80);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = iRet;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if( strstr(pReply,"OFF") != NULL || strstr(pReply,"Off") != NULL)
|
||||
{
|
||||
strcat(pBueffel," off");
|
||||
}
|
||||
else if(strstr(pReply,"not init") != NULL)
|
||||
{
|
||||
strcat(pBueffel," not");
|
||||
}
|
||||
else
|
||||
{
|
||||
strcat(pBueffel," on");
|
||||
}
|
||||
|
||||
}
|
||||
strncpy(pBuffer,pBueffel,iLen);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* now request values for the parameter */
|
||||
if(strcmp(parname,"mp") == 0)
|
||||
{
|
||||
strcpy(pCommand,"mps");
|
||||
}
|
||||
else if(strcmp(parname,"mz") == 0)
|
||||
{
|
||||
strcpy(pCommand,"mzs");
|
||||
}
|
||||
else if(strcmp(parname,"ts") == 0)
|
||||
{
|
||||
strcpy(pCommand,"ts");
|
||||
}
|
||||
else
|
||||
{
|
||||
pPriv->iError = BADINPUT;
|
||||
return 0;
|
||||
}
|
||||
iRet = SerialWriteRead(&(pPriv->pData),pCommand,pReply,80);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = iRet;
|
||||
return 0;
|
||||
}
|
||||
/* decode value */
|
||||
pPtr = pReply;
|
||||
pPtr = stptok(pPtr,pBueffel,255,"/\0");
|
||||
pPtr = stptok(pPtr,pBueffel,255,"/\0");
|
||||
iRet = sscanf(pBueffel,"%f",&fVal);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = BADREPLY;
|
||||
return 0;
|
||||
}
|
||||
sprintf(pBueffel,"%f",fVal);
|
||||
strncpy(pBuffer,pBueffel,iLen);
|
||||
return 1;
|
||||
}
|
||||
/*-----------------------------------------------------------------------*/
|
||||
static int CookerCheckPar(pCodri self, char *parname)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
char pCommand[80], pReply[80];
|
||||
int iRet;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
|
||||
if(strcmp(parname,"mp") == 0)
|
||||
{
|
||||
strcpy(pCommand,"mps");
|
||||
}
|
||||
else if(strcmp(parname,"mz"))
|
||||
{
|
||||
strcpy(pCommand,"mzs");
|
||||
}
|
||||
else
|
||||
{
|
||||
pPriv->iError = BADINPUT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* get a status reply */
|
||||
iRet = SerialWriteRead(&(pPriv->pData),pCommand,pReply,80);
|
||||
if(iRet != 1)
|
||||
{
|
||||
pPriv->iError = iRet;
|
||||
return HWFault;
|
||||
}
|
||||
|
||||
if( strstr(pReply,"Start") != NULL || strstr(pReply,"Move") != NULL )
|
||||
{
|
||||
return HWBusy;
|
||||
}
|
||||
else
|
||||
{
|
||||
return HWIdle;
|
||||
}
|
||||
return HWIdle;
|
||||
}
|
||||
/*---------------------------------------------------------------------*/
|
||||
static int CookerError(pCodri self, int *iCode, char *pError, int iLen)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
char pCommand[80], pReply[80];
|
||||
int iRet;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
|
||||
*iCode = pPriv->iError;
|
||||
switch(pPriv->iError)
|
||||
{
|
||||
case NOTINIT:
|
||||
strncpy(pError,"ERROR: NOT Initialized!",iLen);
|
||||
break;
|
||||
case POWEROFF:
|
||||
strncpy(pError,"ERROR: Power is OFF",iLen);
|
||||
break;
|
||||
case LOWRANGE:
|
||||
strncpy(pError,"ERROR: Lower limit violated",iLen);
|
||||
break;
|
||||
case HIGHRANGE:
|
||||
strncpy(pError,"ERROR: Upper limit violated",iLen);
|
||||
break;
|
||||
case CONTIMEOUT:
|
||||
strncpy(pError,"ERROR: Internal controller timeout ",iLen);
|
||||
break;
|
||||
case BADCOMMAND:
|
||||
strncpy(pError,"ERROR: Controller did not understand command",iLen);
|
||||
break;
|
||||
case BADINPUT:
|
||||
strncpy(pError,"A bad parameter was entered",iLen);
|
||||
break;
|
||||
case MOTERROR:
|
||||
strncpy(pError,"ERROR: Iternal motor error in controller",iLen);
|
||||
break;
|
||||
case CONBUSY:
|
||||
strncpy(pError,"ERROR: Controller is busy",iLen);
|
||||
break;
|
||||
default:
|
||||
SerialError(pPriv->iError,pError,iLen);
|
||||
break;
|
||||
}
|
||||
pPriv->iError = 0;
|
||||
return 1;
|
||||
}
|
||||
/*----------------------------------------------------------------------*/
|
||||
static int CookerFix(pCodri self, int iCode)
|
||||
{
|
||||
pSANSCook pPriv = NULL;
|
||||
int iRet;
|
||||
|
||||
assert(self);
|
||||
pPriv = (pSANSCook)self->pPrivate;
|
||||
assert(pPriv);
|
||||
|
||||
switch(iCode)
|
||||
{
|
||||
case NOTINIT:
|
||||
case POWEROFF:
|
||||
case LOWRANGE:
|
||||
case HIGHRANGE:
|
||||
case BADCOMMAND:
|
||||
case BADINPUT:
|
||||
case MOTERROR:
|
||||
return CHFAIL;
|
||||
case CONTIMEOUT:
|
||||
case CONBUSY:
|
||||
return CHREDO;
|
||||
/* network errors */
|
||||
case EL734__BAD_FLUSH:
|
||||
case EL734__BAD_RECV:
|
||||
case EL734__BAD_RECV_NET:
|
||||
case EL734__BAD_RECV_UNKN:
|
||||
case EL734__BAD_RECVLEN:
|
||||
case EL734__BAD_RECV1:
|
||||
case EL734__BAD_RECV1_PIPE:
|
||||
case EL734__BAD_RNG:
|
||||
case EL734__BAD_SEND:
|
||||
case EL734__BAD_SEND_PIPE:
|
||||
case EL734__BAD_SEND_NET:
|
||||
case EL734__BAD_SEND_UNKN:
|
||||
case EL734__BAD_SENDLEN:
|
||||
case NOCONNECTION:
|
||||
SerialForceClose(&(pPriv->pData));
|
||||
pPriv->pData = NULL;
|
||||
iRet = SerialOpen(&(pPriv->pData),pPriv->pHost,
|
||||
pPriv->iPort,pPriv->iChannel);
|
||||
if(iRet == 1 )
|
||||
{
|
||||
return CHREDO;
|
||||
}
|
||||
else
|
||||
{
|
||||
return CHFAIL;
|
||||
}
|
||||
break;
|
||||
case EL734__FORCED_CLOSED:
|
||||
iRet = CookerInit(self);
|
||||
if(iRet)
|
||||
{
|
||||
return CHREDO;
|
||||
}
|
||||
else
|
||||
{
|
||||
return CHFAIL;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return CHFAIL;
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
/*------------------------------------------------------------------------*/
|
||||
pCodri MakeCookerDriver(char *pHost, int iPort, int iChannel)
|
||||
{
|
||||
pCodri pNew = NULL;
|
||||
pSANSCook pPriv = NULL;
|
||||
char *pText;
|
||||
|
||||
/* allocate memory */
|
||||
pText = (char *)malloc(1024*sizeof(char));
|
||||
pNew = (pCodri)malloc(sizeof(Codri));
|
||||
pPriv = (pSANSCook)malloc(sizeof(SANSCook));
|
||||
if( !pText || !pNew || !pPriv)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
memset(pText,0,1024);
|
||||
memset(pNew,0,sizeof(Codri));
|
||||
memset(pPriv,0,sizeof(SANSCook));
|
||||
|
||||
/* initialize private data structure */
|
||||
pPriv->pHost = strdup(pHost);
|
||||
pPriv->iPort = iPort;
|
||||
pPriv->iChannel = iChannel;
|
||||
pPriv->pData = NULL;
|
||||
|
||||
/* set known parameter names */
|
||||
strcpy(pText,"mp");
|
||||
strcat(pText,",mp.power");
|
||||
strcat(pText,",mp.init");
|
||||
strcat(pText,",mz");
|
||||
strcat(pText,",mz.power");
|
||||
strcat(pText,",mz.init");
|
||||
strcat(pText,",ts");
|
||||
strcat(pText,",ts.power");
|
||||
|
||||
|
||||
/* install codri */
|
||||
pNew->Init = CookerInit;
|
||||
pNew->Close = CookerClose;
|
||||
pNew->Delete = CookerDelete;
|
||||
pNew->SetPar = CookerSetPar;
|
||||
pNew->SetPar2 = CookerSetPar2;
|
||||
pNew->GetPar = CookerGetPar;
|
||||
pNew->CheckPar = CookerCheckPar;
|
||||
pNew->GetError = CookerError;
|
||||
pNew->TryFixIt = CookerFix;
|
||||
pNew->Halt = CookerHalt;
|
||||
pNew->pParList = pText;
|
||||
pNew->pPrivate = pPriv;
|
||||
|
||||
return pNew;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user