Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
a4501b4517 | |||
8db787367d | |||
893badbada | |||
df698554c0 | |||
d5a4fd7fd6 | |||
8afb1fd4fb |
16
README.md
16
README.md
@ -6,7 +6,7 @@ This library offers base classes for EPICS motor drivers (`sinqAxis` and `sinqCo
|
||||
|
||||
## Features
|
||||
|
||||
sinqMotor offers a variety of additional methods for children classes to standardize certain patterns (e.g. writing messages to the IOC shell and the motor message PV). For a detailed description, please see the respective function documentation in the .h-file. All of these functions can be overwritten manually if e.g. a completely different implementation of `poll` is required
|
||||
sinqMotor offers a variety of additional methods for children classes to standardize certain patterns (e.g. writing messages to the IOC shell and the motor message PV). For a detailed description, please see the respective function documentation in the .h-file. All of these functions can be overwritten manually if e.g. a completely different implementation of `poll` is required.
|
||||
|
||||
### sinqController
|
||||
- `stringifyAsynStatus`: Convert the enum `asynStatus` into a human-readable string.
|
||||
@ -16,16 +16,28 @@ sinqMotor offers a variety of additional methods for children classes to standar
|
||||
### sinqAxis
|
||||
- `atFirstPoll`: This function is executed once before the first poll. If it returns anything but `asynSuccess`, it retries.
|
||||
- `poll`: This is a wrapper around `doPoll` which performs some bookkeeping tasks before and after calling `doPoll`:
|
||||
|
||||
Before calling `doPoll`:
|
||||
- Try to execute `atFirstPoll` once (and retry, if that failed)
|
||||
|
||||
After calling `doPoll`:
|
||||
- Reset `motorStatusProblem_`, `motorStatusCommsError_` and `motorMessageText_` if `doPoll` returned `asynSuccess`
|
||||
- Run `callParamCallbacks`
|
||||
- Return the status of `doPoll`
|
||||
- `doPoll`: This is an empty function which should be overwritten by concrete driver implementations.
|
||||
- `move`: This function sets the absolute target position in the parameter library and then calls `doMove`.
|
||||
- `doMove`: This is an empty function which should be overwritten by concrete driver implementations.
|
||||
- `movementTimeoutWatchdog`: Manages a watchdog mechanism for movement operations
|
||||
|
||||
|
||||
## Versioning
|
||||
|
||||
The versioning is done via git tags. Git tags are recognized by the PSI build system: If you tag a version as 1.0, it will be built into the directory /ioc/modules/sinqMotor/1.0. The tag is directly coupled to a commit so that it is always clear which source code was used to build which binary.
|
||||
|
||||
All existing tags can be listed with `git tag` in the sinqMotor directory. Detailed information (author, data, commit number, commit message) regarding a specific tag can be shown with `git show X.X`, where X.X is the name of your version. To create a new tag, use `git tag X.X`. If the tag `X.X` is already used by another commit, git will show a corresponding error.
|
||||
|
||||
## How to build it
|
||||
|
||||
The makefile in the top directory includes all necessary steps for compiling a shared library together with the header files into `/ioc/modules` (using the PSI EPICS build system).Therefore it is sufficient to run `make install -f Makefile` from the terminal. If you want to compile a new version, adjust the entry `LIBVERSION` accordingly.
|
||||
The makefile in the top directory includes all necessary steps for compiling a shared library together with the header files into `/ioc/modules` (using the PSI EPICS build system).Therefore it is sufficient to run `make install -f Makefile` from the terminal.
|
||||
|
||||
To use the library when writing a concrete motor driver, include it in the makefile of your application /library the same way as other libraries such as e.g. `asynMotor` by adding `REQUIRED+=sinqMotor` to your Makefile.
|
||||
|
49
src/Makefile
49
src/Makefile
@ -1,49 +0,0 @@
|
||||
TOP=../..
|
||||
|
||||
include $(TOP)/configure/CONFIG
|
||||
#----------------------------------------
|
||||
# ADD MACRO DEFINITIONS AFTER THIS LINE
|
||||
#=============================
|
||||
|
||||
#=============================
|
||||
# Build the IOC application
|
||||
|
||||
PROD_IOC = sinqEPICS
|
||||
# sinqEPICS.dbd will be created and installed
|
||||
DBD += sinqEPICS.dbd
|
||||
|
||||
# sinqEPICS.dbd will be made up from these files:
|
||||
sinqEPICS_DBD += base.dbd
|
||||
|
||||
# Include dbd files from all support applications:
|
||||
sinqEPICS_DBD += sinq.dbd
|
||||
#sinqEPICS_DBD += pmacAsynIPPort.dbd pmacAsynMotorPort.dbd
|
||||
|
||||
# Add all the support libraries needed by this IOC
|
||||
sinqEPICS_LIBS += motor asyn busy synAppsStd streamdevice pcre
|
||||
|
||||
# sinqEPICS_registerRecordDeviceDriver.cpp derives from sinqEPICS.dbd
|
||||
sinqEPICS_SRCS += sinqEPICS_registerRecordDeviceDriver.cpp
|
||||
sinqEPICS_SRCS += EL734Driver.cpp devScalerEL737.c pmacAsynIPPort.c sinqAxis.cpp sinqController.cpp
|
||||
sinqEPICS_SRCS += pmacController.cpp pmacAxis.cpp
|
||||
sinqEPICS_SRCS += NanotecDriver.cpp stptok.cpp
|
||||
sinqEPICS_SRCS += PhytronDriver.cpp
|
||||
sinqEPICS_SRCS += EuroMoveDriver.cpp
|
||||
|
||||
|
||||
# Build the main IOC entry point on workstation OSs.
|
||||
sinqEPICS_SRCS_DEFAULT += sinqEPICSMain.cpp
|
||||
sinqEPICS_SRCS_vxWorks += -nil-
|
||||
|
||||
# Add support from base/src/vxWorks if needed
|
||||
#sinqEPICS_OBJS_vxWorks += $(EPICS_BASE_BIN)/vxComLibrary
|
||||
|
||||
# Finally link to the EPICS Base libraries
|
||||
sinqEPICS_LIBS += $(EPICS_BASE_IOC_LIBS)
|
||||
|
||||
#===========================
|
||||
|
||||
include $(TOP)/configure/RULES
|
||||
#----------------------------------------
|
||||
# ADD RULES AFTER THIS LINE
|
||||
|
132
src/sinqAxis.cpp
132
src/sinqAxis.cpp
@ -1,12 +1,14 @@
|
||||
#include "sinqAxis.h"
|
||||
#include "sinqController.h"
|
||||
#include <math.h>
|
||||
#include <unistd.h>
|
||||
|
||||
sinqAxis::sinqAxis(class sinqController *pC, int axis)
|
||||
: asynMotorAxis((asynMotorController *)pC, axis), pC_(pC) {
|
||||
|
||||
bool initial_poll_ = true;
|
||||
int init_poll_counter_ = 0;
|
||||
initial_poll_ = true;
|
||||
watchdogMovActive_ = false;
|
||||
init_poll_counter_ = 0;
|
||||
}
|
||||
|
||||
asynStatus sinqAxis::atFirstPoll() { return asynSuccess; }
|
||||
@ -88,4 +90,128 @@ asynStatus sinqAxis::poll(bool *moving) {
|
||||
return poll_status;
|
||||
}
|
||||
|
||||
asynStatus sinqAxis::doPoll(bool *moving) { return asynSuccess; }
|
||||
asynStatus sinqAxis::doPoll(bool *moving) { return asynSuccess; }
|
||||
|
||||
asynStatus sinqAxis::move(double position, int relative, double minVelocity,
|
||||
double maxVelocity, double acceleration) {
|
||||
|
||||
double motorRecResolution = 0.0;
|
||||
double target_position = 0.0;
|
||||
|
||||
// Status of parameter library operations
|
||||
asynStatus pl_status = asynSuccess;
|
||||
|
||||
// =========================================================================
|
||||
|
||||
// Calculate the (absolute) target position
|
||||
if (relative) {
|
||||
|
||||
double motorPosition = 0.0;
|
||||
pl_status =
|
||||
pC_->getDoubleParam(axisNo_, pC_->motorPosition_, &motorPosition);
|
||||
if (pl_status != asynSuccess) {
|
||||
return pC_->paramLibAccessFailed(pl_status, "motorPosition_",
|
||||
__PRETTY_FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
target_position = position + motorPosition;
|
||||
} else {
|
||||
target_position = position;
|
||||
}
|
||||
|
||||
// Set the target position
|
||||
pl_status = setDoubleParam(pC_->motorTargetPosition_, target_position);
|
||||
if (pl_status != asynSuccess) {
|
||||
return pC_->paramLibAccessFailed(pl_status, "motorTargetPosition_",
|
||||
__PRETTY_FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
return doMove(position, relative, minVelocity, maxVelocity, acceleration);
|
||||
}
|
||||
|
||||
asynStatus sinqAxis::doMove(double position, int relative, double minVelocity,
|
||||
double maxVelocity, double acceleration) {
|
||||
return asynSuccess;
|
||||
}
|
||||
|
||||
asynStatus sinqAxis::movementTimeoutWatchdog(bool moving) {
|
||||
asynStatus pl_status;
|
||||
|
||||
// Not moving -> Watchdog inactive
|
||||
if (!moving) {
|
||||
watchdogMovActive_ = false;
|
||||
return asynSuccess;
|
||||
}
|
||||
|
||||
if (!watchdogMovActive_) {
|
||||
|
||||
// These parameters are only needed in this branch
|
||||
double motorPosition = 0.0;
|
||||
double motorTargetPosition = 0.0;
|
||||
double motorRecResolution = 0.0;
|
||||
double motorVelBase = 0.0;
|
||||
double motorAccel = 0.0;
|
||||
time_t timeContSpeed = 0;
|
||||
time_t timeAccel = 0;
|
||||
|
||||
// Activate the watchdog
|
||||
watchdogMovActive_ = true;
|
||||
|
||||
pl_status =
|
||||
pC_->getDoubleParam(axisNo_, pC_->motorPosition_, &motorPosition);
|
||||
if (pl_status != asynSuccess) {
|
||||
return pC_->paramLibAccessFailed(pl_status, "motorPosition",
|
||||
__PRETTY_FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
pl_status =
|
||||
pC_->getDoubleParam(axisNo_, pC_->motorVelBase_, &motorVelBase);
|
||||
// Only calculate timeContSpeed if the motorVelBase_ has been populated
|
||||
// with a sensible value (e.g. > 0)
|
||||
if (pl_status == asynSuccess && motorVelBase > 0.0) {
|
||||
|
||||
pl_status = pC_->getDoubleParam(axisNo_, pC_->motorTargetPosition_,
|
||||
&motorTargetPosition);
|
||||
if (pl_status == asynSuccess) {
|
||||
timeContSpeed =
|
||||
std::ceil(std::fabs(motorTargetPosition - motorPosition) /
|
||||
motorVelBase);
|
||||
}
|
||||
}
|
||||
|
||||
pl_status = pC_->getDoubleParam(axisNo_, pC_->motorAccel_, &motorAccel);
|
||||
if (pl_status == asynSuccess && motorVelBase > 0.0 &&
|
||||
motorAccel > 0.0) {
|
||||
timeAccel = 2 * std::ceil(motorVelBase / motorAccel);
|
||||
}
|
||||
|
||||
// Calculate the expected arrival time
|
||||
expectedArrivalTime_ =
|
||||
time(NULL) + offsetMovTimeout_ +
|
||||
scaleMovTimeout_ * (timeContSpeed + 2 * timeAccel);
|
||||
|
||||
} else if (expectedArrivalTime_ < time(NULL)) {
|
||||
// Check the watchdog
|
||||
asynPrint(pC_->pasynUserSelf, ASYN_TRACE_ERROR,
|
||||
"%s => line %d:\nAxis %d exceeded the expected arrival time "
|
||||
"(%ld).\n",
|
||||
__PRETTY_FUNCTION__, __LINE__, axisNo_, expectedArrivalTime_);
|
||||
|
||||
pl_status = setStringParam(
|
||||
pC_->motorMessageText_,
|
||||
"Exceeded expected arrival time. Check if the axis is blocked.");
|
||||
if (pl_status != asynSuccess) {
|
||||
return pC_->paramLibAccessFailed(pl_status, "motorMessageText_",
|
||||
__PRETTY_FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
pl_status = setIntegerParam(pC_->motorStatusProblem_, true);
|
||||
if (pl_status != asynSuccess) {
|
||||
pC_->paramLibAccessFailed(pl_status, "motorStatusProblem_",
|
||||
__PRETTY_FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
return asynError;
|
||||
}
|
||||
return asynSuccess;
|
||||
}
|
@ -18,6 +18,17 @@ class epicsShareClass sinqAxis : public asynMotorAxis {
|
||||
*/
|
||||
asynStatus atFirstPoll();
|
||||
|
||||
/**
|
||||
Wrapper around doPoll which performs the following operations;
|
||||
Before calling doPoll:
|
||||
- Try to execute atFirstPoll once (and retry, if that failed)
|
||||
|
||||
After calling doPoll:
|
||||
- Reset motorStatusProblem_, motorStatusCommsError_ and motorMessageText_ if
|
||||
doPoll returned asynSuccess
|
||||
- Run `callParamCallbacks`
|
||||
- Return the status of `doPoll`
|
||||
*/
|
||||
asynStatus poll(bool *moving);
|
||||
|
||||
/**
|
||||
@ -26,12 +37,69 @@ class epicsShareClass sinqAxis : public asynMotorAxis {
|
||||
*/
|
||||
asynStatus doPoll(bool *moving);
|
||||
|
||||
/**
|
||||
Wrapper around move which calculates the (absolute) target position and
|
||||
stores it in the parameter library. After that, it calls and returns doMove
|
||||
*/
|
||||
asynStatus move(double position, int relative, double minVelocity,
|
||||
double maxVelocity, double acceleration);
|
||||
|
||||
/**
|
||||
Implementation of the "proper", device-specific poll method. This method
|
||||
should be implemented by a child class of sinqAxis.
|
||||
*/
|
||||
asynStatus doMove(double position, int relative, double minVelocity,
|
||||
double maxVelocity, double acceleration);
|
||||
|
||||
/**
|
||||
Manages a timeout mechanism for the movement:
|
||||
If the movement takes too long, create an error message and return
|
||||
asynError. The watchdog is started when moving switches from "false" to
|
||||
"true" and stopped when moving switches from "true" to "false". At the
|
||||
watchdog start, the estimated movement time is calculated as
|
||||
|
||||
t = offsetMovTimeout_ + scaleMovTime_ * [timeContSpeed + 2*timeAccel]
|
||||
|
||||
with
|
||||
|
||||
timeContSpeed = abs(motorTargetPosition - motorPosition) / motorVelBase
|
||||
timeAcc = motorVelBase / motorAccel
|
||||
|
||||
The values motorTargetPosition, motorVelBase, motorAccel and
|
||||
positionAtMovementStart are taken from the parameter library. Therefore it
|
||||
is necessary to populate them before using this function. If they are not
|
||||
given, both speed and velocity are assumed to be infinity. This means that
|
||||
timeContSpeed and/or timeAcc are set to zero. motorTargetPosition is
|
||||
populated automatically when using the doMove function.
|
||||
|
||||
The values offsetMovTimeout_ and scaleMovTimeout_ can be set directly from
|
||||
the IOC shell with the functions setScaleMovTimeout and setOffsetMovTimeout,
|
||||
if sinqMotor is loaded via the "require" mechanism.
|
||||
*/
|
||||
asynStatus movementTimeoutWatchdog(bool moving);
|
||||
|
||||
// Setter for offsetMovTimeout
|
||||
asynStatus setOffsetMovTimeout(time_t offsetMovTimeout) {
|
||||
offsetMovTimeout_ = offsetMovTimeout;
|
||||
}
|
||||
|
||||
// Setter for scaleMovTimeout
|
||||
asynStatus setScaleMovTimeout(time_t scaleMovTimeout) {
|
||||
scaleMovTimeout_ = scaleMovTimeout;
|
||||
}
|
||||
|
||||
friend class sinqController;
|
||||
|
||||
protected:
|
||||
bool initial_poll_;
|
||||
int init_poll_counter_;
|
||||
|
||||
// Helper variables for movementTimeoutWatchdog
|
||||
time_t expectedArrivalTime_;
|
||||
time_t offsetMovTimeout_;
|
||||
double scaleMovTimeout_;
|
||||
bool watchdogMovActive_;
|
||||
|
||||
private:
|
||||
sinqController *pC_;
|
||||
};
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include "asynMotorController.h"
|
||||
#include "epicsExport.h"
|
||||
#include "iocsh.h"
|
||||
#include "sinqAxis.h"
|
||||
#include <errlog.h>
|
||||
|
||||
sinqController::sinqController(const char *portName, const char *SINQPortName,
|
||||
@ -27,9 +28,42 @@ sinqController::sinqController(const char *portName, const char *SINQPortName,
|
||||
1, // autoconnect
|
||||
0, 0) // Default priority and stack size
|
||||
{
|
||||
createParam(motorMessageIsFromDriverString, asynParamInt32,
|
||||
&motorMessageIsFromDriver_);
|
||||
createParam(motorMessageTextString, asynParamOctet, &motorMessageText_);
|
||||
|
||||
// Initialization of local variables
|
||||
asynStatus status = asynSuccess;
|
||||
|
||||
// =========================================================================;
|
||||
|
||||
// MOTOR_MESSAGE_TEXT corresponds to the PV definition inside
|
||||
// sinqn_asyn_motor.db. This text is used to forward status messages to
|
||||
// NICOS and in turn to the user.
|
||||
status =
|
||||
createParam("MOTOR_MESSAGE_TEXT", asynParamOctet, &motorMessageText_);
|
||||
if (status != asynSuccess) {
|
||||
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
|
||||
"%s => line %d:\nFATAL ERROR (creating a parameter failed "
|
||||
"with %s)\n. Terminating IOC",
|
||||
__PRETTY_FUNCTION__, __LINE__, stringifyAsynStatus(status));
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
// Internal parameter library entry which stores the movement target
|
||||
status = createParam("MOTOR_TARGET_POSITION", asynParamOctet,
|
||||
&motorTargetPosition_);
|
||||
if (status != asynSuccess) {
|
||||
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
|
||||
"%s => line %d:\nFATAL ERROR (creating a parameter failed "
|
||||
"with %s)\n. Terminating IOC",
|
||||
__PRETTY_FUNCTION__, __LINE__, stringifyAsynStatus(status));
|
||||
exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
sinqController::~sinqController(void) {
|
||||
/*
|
||||
Cleanup of the memory allocated in the asynMotorController constructor
|
||||
*/
|
||||
free(this->pAxes_);
|
||||
}
|
||||
|
||||
asynStatus sinqController::errMsgCouldNotParseResponse(const char *command,
|
||||
@ -120,4 +154,91 @@ const char *sinqController::stringifyAsynStatus(asynStatus status) {
|
||||
errlogPrintf("%s => line %d:\nReached unreachable code.",
|
||||
__PRETTY_FUNCTION__, __LINE__);
|
||||
return "unreachable code reached";
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Provide the setters to IOC shell
|
||||
extern "C" {
|
||||
|
||||
asynStatus setOffsetMovTimeout(const char *portName, int axisNo,
|
||||
double offsetMovTimeout) {
|
||||
|
||||
sinqController *pC;
|
||||
pC = (sinqController *)findAsynPortDriver(portName);
|
||||
if (pC == nullptr) {
|
||||
errlogPrintf("%s => line %d:\nPort %s not found.", __PRETTY_FUNCTION__,
|
||||
__LINE__, portName);
|
||||
return asynError;
|
||||
}
|
||||
|
||||
asynMotorAxis *asynAxis = pC->getAxis(axisNo);
|
||||
sinqAxis *axis = dynamic_cast<sinqAxis *>(asynAxis);
|
||||
if (axis == nullptr) {
|
||||
errlogPrintf("%s => line %d:\nPAxis %d does not exist or is not an "
|
||||
"instance of sinqAxis.",
|
||||
__PRETTY_FUNCTION__, __LINE__, portName, axisNo);
|
||||
}
|
||||
|
||||
return axis->setOffsetMovTimeout(offsetMovTimeout);
|
||||
}
|
||||
|
||||
static const iocshArg setOffsetMovTimeoutArg0 = {"Controller port name",
|
||||
iocshArgString};
|
||||
static const iocshArg setOffsetMovTimeoutArg1 = {"Axis number", iocshArgInt};
|
||||
static const iocshArg setOffsetMovTimeoutArg2 = {"Offset timeout for movement",
|
||||
iocshArgDouble};
|
||||
static const iocshArg *const setOffsetMovTimeoutArgs[] = {
|
||||
&setOffsetMovTimeoutArg0, &setOffsetMovTimeoutArg1,
|
||||
&setOffsetMovTimeoutArg2};
|
||||
static const iocshFuncDef setOffsetMovTimeoutDef = {"setOffsetMovTimeout", 3,
|
||||
setOffsetMovTimeoutArgs};
|
||||
|
||||
static void setOffsetMovTimeoutCallFunc(const iocshArgBuf *args) {
|
||||
setOffsetMovTimeout(args[0].sval, args[1].ival, args[1].dval);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
asynStatus setScaleMovTimeout(const char *portName, int axisNo,
|
||||
double scaleMovTimeout) {
|
||||
|
||||
sinqController *pC;
|
||||
pC = (sinqController *)findAsynPortDriver(portName);
|
||||
if (pC == nullptr) {
|
||||
errlogPrintf("%s => line %d:\nPort %s not found.", __PRETTY_FUNCTION__,
|
||||
__LINE__, portName);
|
||||
return asynError;
|
||||
}
|
||||
|
||||
asynMotorAxis *asynAxis = pC->getAxis(axisNo);
|
||||
sinqAxis *axis = dynamic_cast<sinqAxis *>(asynAxis);
|
||||
if (axis == nullptr) {
|
||||
errlogPrintf("%s => line %d:\nPAxis %d does not exist or is not an "
|
||||
"instance of sinqAxis.",
|
||||
__PRETTY_FUNCTION__, __LINE__, portName, axisNo);
|
||||
}
|
||||
|
||||
return axis->setScaleMovTimeout(scaleMovTimeout);
|
||||
}
|
||||
|
||||
static const iocshArg setScaleMovTimeoutArg0 = {"Controller port name",
|
||||
iocshArgString};
|
||||
static const iocshArg setScaleMovTimeoutArg1 = {"Axis number", iocshArgInt};
|
||||
static const iocshArg setScaleMovTimeoutArg2 = {
|
||||
"Multiplier for calculated move time", iocshArgDouble};
|
||||
static const iocshArg *const setScaleMovTimeoutArgs[] = {
|
||||
&setScaleMovTimeoutArg0, &setScaleMovTimeoutArg1, &setScaleMovTimeoutArg2};
|
||||
static const iocshFuncDef setScaleMovTimeoutDef = {"setScaleMovTimeout", 3,
|
||||
setScaleMovTimeoutArgs};
|
||||
|
||||
static void setScaleMovTimeoutCallFunc(const iocshArgBuf *args) {
|
||||
setScaleMovTimeout(args[0].sval, args[1].ival, args[1].dval);
|
||||
}
|
||||
|
||||
static void sinqControllerRegister(void) {
|
||||
iocshRegister(&setOffsetMovTimeoutDef, setOffsetMovTimeoutCallFunc);
|
||||
iocshRegister(&setOffsetMovTimeoutDef, setOffsetMovTimeoutCallFunc);
|
||||
}
|
||||
epicsExportRegistrar(sinqControllerRegister);
|
||||
|
||||
} // extern C
|
||||
|
@ -47,8 +47,8 @@ class epicsShareClass sinqController : public asynMotorController {
|
||||
|
||||
protected:
|
||||
asynUser *lowLevelPortUser_;
|
||||
int motorMessageIsFromDriver_;
|
||||
int motorMessageText_;
|
||||
int motorTargetPosition_;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
Reference in New Issue
Block a user