Added adaptive polling

See documentation in README.md for parameter ADAPTPOLL
This commit is contained in:
2025-04-16 13:05:48 +02:00
parent 4c3254687d
commit db03ffea0e
6 changed files with 126 additions and 46 deletions

View File

@ -29,15 +29,15 @@ An EPICS IOC for motor control at SINQ is started by executing a script with the
# Load libraries needed for the IOC
require sinqMotor, 1.0.0
require turboPmac, 1.2.0
require actualDriver, 1.2.0
# Define environment variables used later to parametrize the individual controllers
epicsEnvSet("TOP","/ioc/sinq-ioc/sinqtest-ioc/")
epicsEnvSet("INSTR","SQ:SINQTEST:")
# Include other scripts for the controllers 1 and 2
< turboPmac1.cmd
< turboPmac2.cmd
< actualDriver.cmd
< actualDriver.cmd
iocInit()
```
@ -46,7 +46,7 @@ The script for controller 1 ("turboPmac1.cmd") for a Turbo PMAC (see https://git
```
# Define the name of the controller and the corresponding port
epicsEnvSet("NAME","turboPmac1")
epicsEnvSet("NAME","actualDriver1")
epicsEnvSet("ASYN_PORT","p$(NAME)")
# Create the TCP/IP socket used to talk with the controller. The socket can be adressed from within the IOC shell via the port name
@ -58,12 +58,12 @@ drvAsynIPPortConfigure("$(ASYN_PORT)","172.28.101.24:1025")
# 0.05: Busy poll period in seconds
# 1: Idle poll period in seconds
# 1: Socket communication timeout in seconds
turboPmacController("$(NAME)", "$(ASYN_PORT)", 8, 0.05, 1, 1);
actualDriverController("$(NAME)", "$(ASYN_PORT)", 8, 0.05, 1, 1);
# Define some axes for the specified motor controller at the given slot (1, 2 and 5). No slot may be used twice!
turboPmacAxis("$(NAME)",1);
turboPmacAxis("$(NAME)",2);
turboPmacAxis("$(NAME)",5);
actualDriverAxis("$(NAME)",1);
actualDriverAxis("$(NAME)",2);
actualDriverAxis("$(NAME)",5);
# Set the number of subsequent timeouts
setMaxSubsequentTimeouts("$(NAME)", 20);
@ -74,7 +74,7 @@ setThresholdComTimeout("$(NAME)", 300, 10);
# Parametrize the EPICS record database with the substitution file named after the motor controller.
epicsEnvSet("SINQDBPATH","$(sinqMotor_DB)/sinqMotor.db")
dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
epicsEnvSet("SINQDBPATH","$(turboPmac_DB)/turboPmac.db")
epicsEnvSet("SINQDBPATH","$(actualDriver_DB)/turboPmac.db")
dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
dbLoadRecords("$(sinqMotor_DB)/asynRecord.db","P=$(INSTR)$(NAME),PORT=$(ASYN_PORT)")
```
@ -87,11 +87,11 @@ To work with sinqMotor, "mcu1.substitutions" needs to look like this (the order
file "$(SINQDBPATH)"
{
pattern
{ AXIS, M, DESC, EGU, DIR, MRES, MSGTEXTSIZE, ENABLEMOVWATCHDOG, LIMITSOFFSET, CANSETSPEED }
{ 1, "lin1", "Linear motor doing whatever", mm, Pos, 0.001, 200, 1, 1.0, 1 }
{ 2, "rot1", "First rotary motor", degree, Neg, 0.001, 200, 0, 1.0, 0 }
{ 3, "rot2", "Second rotary motor", degree, Pos, 0.001, 200, 0, 0.0, 1 }
{ 5, "rot3", "Surprise: Third rotary motor", degree, Pos, 0.001, 200, 1, 2.0, 0 }
{ AXIS, M, DESC, EGU, DIR, MRES, MSGTEXTSIZE, ENABLEMOVWATCHDOG, LIMITSOFFSET, CANSETSPEED, ADAPTPOLL }
{ 1, "lin1", "Linear motor doing whatever", mm, Pos, 0.001, 200, 1, 1.0, 1, 1 }
{ 2, "rot1", "First rotary motor", degree, Neg, 0.001, 200, 0, 1.0, 0, 1 }
{ 3, "rot2", "Second rotary motor", degree, Pos, 0.001, 200, 0, 0.0, 1, 0 }
{ 5, "rot3", "Surprise: Third rotary motor", degree, Pos, 0.001, 200, 1, 2.0, 0, 0 }
}
```
The variable `SINQDBPATH` has been set in "mcu1.cmd" before calling `dbLoadTemplate`.
@ -114,6 +114,7 @@ be further reduced by this offset in order to avoid errors due to slight oversho
on the motor controller. For example, if this value is 1.0 and the read-out limits
are [-10.0 10.0], the EPICS limits are set to [-9.0 9.0]. This parameter uses engineering units (EGU). Defaults to 0.0.
- `CANSETSPEED`: If set to 1, the motor speed can be modified by the user. Defaults to 0.
- `ADAPTPOLL`: If set to any value other than 0, adaptive polling is enabled for this particular axis. Adaptive polling is designed to reduce the communication load in case some axis is moving. By default, if at least one axis is moving, all axes are polled using the busy / moving poll period (see [IOC startup script](#ioc-startup-script)). Adaptive polling modifies this behaviour so that the affected axis is only polled with the busy / moving poll period if it itself is moving. Defaults to 1.
### Motor record resolution MRES

View File

@ -140,6 +140,18 @@ record(longout, "$(INSTR)$(M):CanSetSpeed") {
field(VAL, "$(CANSETSPEED=0)")
}
# If this PV has a value other than 0, adaptive polling for this axis is enabled.
# The standard motor record behaviour is to poll all axis with the busy / move poll
# period if at least one of the axes is moving. Adaptive polling changes this so
# that only axes which were moving in the last poll are polled with the busy / move poll
# period and all other axes are polled with the idle poll period.
record(longout, "$(INSTR)$(M):AdaptivePolling") {
field(DTYP, "asynInt32")
field(OUT, "@asyn($(CONTROLLER),$(AXIS),1) ADAPTIVE_POLLING")
field(PINI, "YES")
field(VAL, "$(ADAPTPOLL=1)")
}
# The timeout mechanism for movements can be enabled / disabled by setting
# this PV to 1 / 0.
# This record is coupled to the parameter library via motorEnableMovWatchdog -> MOTOR_ENABLE_MOV_WATCHDOG.

View File

@ -15,6 +15,8 @@ sinqAxis::sinqAxis(class sinqController *pC, int axisNo)
offsetMovTimeout_ = 30;
targetPosition_ = 0.0;
epicsTimeGetCurrent(&lastPollTime_);
// This check is also done in asynMotorAxis, but there the IOC continues
// running even though the configuration is incorrect. When failing this
// check, the IOC is stopped, since this is definitely a configuration
@ -115,6 +117,46 @@ asynStatus sinqAxis::poll(bool *moving) {
asynStatus poll_status = asynSuccess;
int homing = 0;
int homed = 0;
int adaptivePolling = 0;
/*
If adaptive polling is enabled:
- Check if the motor was moving during the last poll
- If yes, perform the poll
- If no, check if the last poll was at least an idlePollPeriod ago
- If yes, perform the poll
- If no, skip it
*/
pl_status =
pC_->getIntegerParam(axisNo_, pC_->adaptivePolling(), &adaptivePolling);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "adaptivePolling", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
};
// Using the EPICS timestamp here, see
// https://docs.epics-controls.org/projects/base/en/latest/epicsTime_h.html#_CPPv414epicsTimeStamp
// Get the current time
epicsTimeStamp ts;
epicsTimeGetCurrent(&ts);
if (adaptivePolling != 0) {
// Motor wasn't moving during the last poll
if (getWasMovingFlag() == 0) {
// Add the idle poll period
epicsTimeStamp earliestTimeNextPoll = lastPollTime_;
epicsTimeAddSeconds(&earliestTimeNextPoll, pC_->idlePollPeriod());
if (epicsTimeLessThan(&earliestTimeNextPoll, &ts) == 1) {
*moving = false;
return asynSuccess;
}
}
}
// Update the start time of the last poll
lastPollTime_ = ts;
/*
At the beginning of the poll, it is assumed that the axis has no status
@ -223,36 +265,42 @@ asynStatus sinqAxis::move(double position, int relative, double minVelocity,
double maxVelocity, double acceleration) {
// Status of parameter library operations
asynStatus pl_status = asynSuccess;
asynStatus status = asynSuccess;
double motorRecResolution = 0.0;
// =========================================================================
// When a new move is done, the motor is not homed anymore
pl_status = setIntegerParam(pC_->motorStatusHomed(), 0);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorStatusHomed_",
axisNo_, __PRETTY_FUNCTION__,
__LINE__);
}
pl_status = setIntegerParam(pC_->motorStatusAtHome(), 0);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorStatusAtHome_",
axisNo_, __PRETTY_FUNCTION__,
__LINE__);
}
pl_status = pC_->getDoubleParam(axisNo_, pC_->motorRecResolution(),
&motorRecResolution);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorRecResolution_",
axisNo_, __PRETTY_FUNCTION__,
__LINE__);
}
// Store the target position internally
targetPosition_ = position * motorRecResolution;
return doMove(position, relative, minVelocity, maxVelocity, acceleration);
status = doMove(position, relative, minVelocity, maxVelocity, acceleration);
if (status != asynSuccess) {
return status;
}
// Since the move command was successfull, we assume that the motor has
// started its movement.
status = setIntegerParam(pC_->motorStatusHomed(), 0);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorStatusHomed_", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
}
status = setIntegerParam(pC_->motorStatusAtHome(), 0);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorStatusAtHome_", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
}
status = pC_->getDoubleParam(axisNo_, pC_->motorRecResolution(),
&motorRecResolution);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorRecResolution_", axisNo_,
__PRETTY_FUNCTION__, __LINE__);
}
// Needed for adaptive polling
setWasMovingFlag(1);
return pC_->callParamCallbacks();
}
asynStatus sinqAxis::doMove(double position, int relative, double minVelocity,

View File

@ -7,6 +7,7 @@ Stefan Mathis, November 2024
#ifndef sinqAxis_H
#define sinqAxis_H
#include "asynMotorAxis.h"
#include <epicsTime.h>
class epicsShareClass sinqAxis : public asynMotorAxis {
public:
@ -351,11 +352,17 @@ class epicsShareClass sinqAxis : public asynMotorAxis {
// Internal variables used in the movement timeout watchdog
time_t expectedArrivalTime_;
time_t offsetMovTimeout_;
double scaleMovTimeout_;
bool watchdogMovActive_;
// Store the motor target position for the movement time calculation
double targetPosition_;
/*
Store the time since the last poll
*/
epicsTimeStamp lastPollTime_;
private:
sinqController *pC_;
};

View File

@ -53,7 +53,7 @@ sinqController::sinqController(const char *portName,
asynStatus status = asynSuccess;
// Handle to the asynUser of the IP port asyn driver
ipPortAsynOctetSyncIO_ = nullptr;
pasynOctetSyncIOipPort_ = nullptr;
// Initial values for the average timeout mechanism, can be overwritten
// later by a FFI function
@ -81,9 +81,9 @@ sinqController::sinqController(const char *portName,
We try to connect to the port via the port name provided by the constructor.
If this fails, the function is terminated via exit.
*/
pasynOctetSyncIO->connect(ipPortConfigName, 0, &ipPortAsynOctetSyncIO_,
pasynOctetSyncIO->connect(ipPortConfigName, 0, &pasynOctetSyncIOipPort_,
NULL);
if (status != asynSuccess || ipPortAsynOctetSyncIO_ == nullptr) {
if (status != asynSuccess || pasynOctetSyncIOipPort_ == nullptr) {
errlogPrintf("Controller \"%s\" => %s, line %d:\nFATAL ERROR (cannot "
"connect to MCU controller).\n"
"Terminating IOC",
@ -252,6 +252,16 @@ sinqController::sinqController(const char *portName,
exit(-1);
}
status = createParam("ADAPTIVE_POLLING", asynParamInt32, &adaptivePolling_);
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\" => %s, line %d:\nFATAL ERROR (creating a "
"parameter failed with %s).\nTerminating IOC",
portName, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
exit(-1);
}
status = createParam("ENCODER_TYPE", asynParamOctet, &encoderType_);
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
@ -356,7 +366,7 @@ asynStatus sinqController::couldNotParseResponse(const char *command,
int line) {
asynStatus pl_status = asynSuccess;
asynPrint(ipPortAsynOctetSyncIO_, ASYN_TRACE_ERROR,
asynPrint(pasynOctetSyncIOipPort_, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nCould not interpret "
"response \"%s\" for command \"%s\".\n",
portName, axisNo, functionName, line, response, command);
@ -385,7 +395,7 @@ asynStatus sinqController::paramLibAccessFailed(asynStatus status,
int line) {
if (status != asynSuccess) {
asynPrint(ipPortAsynOctetSyncIO_, ASYN_TRACE_ERROR,
asynPrint(pasynOctetSyncIOipPort_, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\n Accessing the "
"parameter library failed for parameter %s with error %s.\n",
portName, axisNo, functionName, line, parameter,
@ -654,9 +664,9 @@ asynStatus setMaxSubsequentTimeouts(const char *portName,
if (ptr == nullptr) {
/*
We can't use asynPrint here since this macro would require us
to get a ipPortAsynOctetSyncIO_ from a pointer to an asynPortDriver.
to get a pasynOctetSyncIOipPort_ from a pointer to an asynPortDriver.
However, the given pointer is a nullptr and therefore doesn't
have a ipPortAsynOctetSyncIO_! printf is an EPICS alternative which
have a pasynOctetSyncIOipPort_! printf is an EPICS alternative which
works w/o that, but doesn't offer the comfort provided
by the asynTrace-facility
*/

View File

@ -291,6 +291,7 @@ class epicsShareClass sinqController : public asynMotorController {
int motorAcclFromDriver() { return motorAcclFromDriver_; }
int motorHighLimitFromDriver() { return motorHighLimitFromDriver_; }
int motorLowLimitFromDriver() { return motorLowLimitFromDriver_; }
int adaptivePolling() { return adaptivePolling_; }
int encoderType() { return encoderType_; }
// Additional members
@ -298,14 +299,14 @@ class epicsShareClass sinqController : public asynMotorController {
double idlePollPeriod() { return idlePollPeriod_; }
double movingPollPeriod() { return movingPollPeriod_; }
asynUser *asynUserSelf() { return pasynUserSelf; }
asynUser *ipPortAsynOctetSyncIO() { return ipPortAsynOctetSyncIO_; }
asynUser *pasynOctetSyncIOipPort() { return pasynOctetSyncIOipPort_; }
// =========================================================================
protected:
// Pointer to the port user which is specified by the char array
// `ipPortConfigName` in the constructor
asynUser *ipPortAsynOctetSyncIO_;
asynUser *pasynOctetSyncIOipPort_;
double movingPollPeriod_;
double idlePollPeriod_;
msgPrintControl msgPrintControl_;
@ -345,6 +346,7 @@ class epicsShareClass sinqController : public asynMotorController {
int motorAcclFromDriver_;
int motorHighLimitFromDriver_;
int motorLowLimitFromDriver_;
int adaptivePolling_;
int encoderType_;
#define LAST_SINQMOTOR_PARAM encoderType_