Compare commits

...

9 Commits
0.3.0 ... 0.6.0

Author SHA1 Message Date
4c104cb90c Exchanged asynMotor for motorBase as linkage target 2025-02-14 16:37:09 +01:00
b4e49a9d7a Applied various bugfixes to make this driver fully operational 2025-02-14 16:31:23 +01:00
fd4467ae54 Renamed from pmacV3 to turboPmac 2025-01-21 13:07:09 +01:00
df7bc07259 Fixed two small bugs in the analyzeTcpDump utility 2025-01-17 10:47:58 +01:00
83051e10c3 Added two utility scripts for working with PMAC motors 2025-01-09 13:26:12 +01:00
08d76d7953 bump sinqMotor version to 0.6.3 2025-01-08 16:04:58 +01:00
1f02001502 Various small improvements to documentation, error messages etc.
Also moved the initialization of some parameters to sinqMotor
2024-12-23 09:32:00 +01:00
2f2678546d Bumped the required version of sinqMotor 2024-12-11 09:50:22 +01:00
285fab7587 Refactored some code into sinqMotor:
- Enable, EnableRBV and CanDisable
- EncoderType
- Removed function isEnabled as it is no longer required from sinqMotor
0.5.0
2024-12-09 11:20:16 +01:00
16 changed files with 1607 additions and 883 deletions

View File

@ -47,11 +47,11 @@ build_module:
- sed -i 's/ARCH_FILTER=.*/ARCH_FILTER=linux%/' Makefile
- echo "LIBVERSION=${CI_COMMIT_TAG:-0.0.1}" >> Makefile
- make install
- cp -rT "/ioc/modules/pmacv3/$(ls -U /ioc/modules/pmacv3/ | head -1)" "./pmacv3-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}"
- cp -rT "/ioc/modules/turboPmac/$(ls -U /ioc/modules/turboPmac/ | head -1)" "./turboPmac-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}"
artifacts:
name: "pmacv3-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}"
name: "turboPmac-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}"
paths:
- "pmacv3-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}/*"
- "turboPmac-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}/*"
expire_in: 1 week
when: always
tags:

View File

@ -1,30 +1,33 @@
# Use the PSI build system
include /ioc/tools/driver.makefile
MODULE=pmacv3
MODULE=turboPmac
BUILDCLASSES=Linux
EPICS_VERSIONS=7.0.7
ARCH_FILTER=RHEL%
# Additional module dependencies
REQUIRED+=asynMotor
REQUIRED+=motorBase
REQUIRED+=sinqMotor
# Specify the version of motorBase we want to build against
motorBase_VERSION=7.2.2
# Specify the version of sinqMotor we want to build against
sinqMotor_VERSION=0.4.0
sinqMotor_VERSION=0.7.0
# These headers allow to depend on this library for derived drivers.
HEADERS += src/pmacv3Axis.h
HEADERS += src/pmacv3Controller.h
HEADERS += src/turboPmacAxis.h
HEADERS += src/turboPmacController.h
# Source files to build
SOURCES += src/pmacv3Axis.cpp
SOURCES += src/pmacv3Controller.cpp
SOURCES += src/turboPmacAxis.cpp
SOURCES += src/turboPmacController.cpp
# Store the record files
TEMPLATES += db/pmacv3.db
TEMPLATES += db/turboPmac.db
# This file registers the motor-specific functions in the IOC shell.
DBDS += src/pmacv3.dbd
DBDS += src/turboPmac.dbd
USR_CFLAGS += -Wall -Wextra -Weffc++ -Wunused-result # -Werror
USR_CFLAGS += -Wall -Wextra -Weffc++ -Wunused-result -Wextra -Werror # -Wpedantic // Does not work because EPICS macros trigger warnings

View File

@ -1,23 +1,28 @@
# pmacv3
# turboPmac
## Overview
This is a driver for the pmacV3 motion controller with the SINQ communication protocol. It is based on the sinqMotor shared library (https://git.psi.ch/sinq-epics-modules/sinqmotor). The header files contain detailed documentation for all public functions. The headers themselves are exported when building the library to allow other drivers to depend on this one.
This is a driver for the Turbo PMAC motion controller with the SINQ communication protocol. It is based on the sinqMotor shared library (https://git.psi.ch/sinq-epics-modules/sinqmotor). The header files contain detailed documentation for all public functions. The headers themselves are exported when building the library to allow other drivers to depend on this one.
## User guide
This driver is a standard sinqMotor-derived driver and does not need any specific configuration. For the general configuration, please see https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.
The folder "utils" contains utility scripts for working with pmac motor controllers. To read their manual, run the scripts without any arguments.
- writeRead.py: Allows sending commands to and receiving commands from a pmac controller over an ethernet connection.
- analyzeTcpDump.py: Parse the TCP communication between an IOC and a MCU and format it into a dictionary. "demo.py" shows how this data can be easily visualized for analysis.
## Developer guide
### Usage in IOC shell
pmacv3 exposes the following IOC shell functions (all in pmacv3Controller.cpp):
- `pmacv3Controller`: Create a new controller object.
- `pmacv3Axis`: Create a new axis object.
turboPmac exposes the following IOC shell functions (all in turboPmacController.cpp):
- `turboPmacController`: Create a new controller object.
- `turboPmacAxis`: Create a new axis object.
These functions are parametrized as follows:
```
pmacv3Controller(
turboPmacController(
"$(NAME)", # Name of the MCU, e.g. mcu1. This parameter should be provided by an environment variable.
"$(ASYN_PORT)", # IP-Port of the MCU. This parameter should be provided by an environment variable.
8, # Maximum number of axes
@ -27,7 +32,7 @@ pmacv3Controller(
);
```
```
pmacv3Axis(
turboPmacAxis(
"$(NAME)", # Name of the associated MCU, e.g. mcu1. This parameter should be provided by an environment variable.
1 # Index of the axis.
);

View File

@ -1,29 +1,18 @@
# Read out the encoder type in human-readable form. The output numbers can be
# interpreted as ASCII.
# This record is coupled to the parameter library via encoderType -> ENCODER_TYPE.
record(waveform, "$(INSTR)$(M):Encoder_Type") {
field(DTYP, "asynOctetRead")
field(INP, "@asyn($(CONTROLLER),$(AXIS),1) ENCODER_TYPE")
field(FTVL, "CHAR")
field(NELM, "80")
field(SCAN, "I/O Intr")
}
# Trigger a rereading of the encoder. This action is sometimes necessary for
# absolute encoders after enabling them. For incremental encoders, this is a no-op.
# This record is coupled to the parameter library via rereadEncoderPosition_ -> REREAD_ENCODER_POSITION.
record(longout, "$(INSTR)$(M):Reread_Encoder") {
record(longout, "$(INSTR)$(M):RereadEncoder") {
field(DTYP, "asynInt32")
field(OUT, "@asyn($(CONTROLLER),$(AXIS),1) REREAD_ENCODER_POSITION")
field(PINI, "NO")
}
# The pmacV3 driver reads certain configuration parameters (such as the velocity
# The turboPmac driver reads certain configuration parameters (such as the velocity
# and the acceleration) directly from the MCU. This reading procedure is performed
# once at IOC startup during atFirstPoll. However, it can be triggered manually
# by setting this record value to 1.
# This record is coupled to the parameter library via readConfig_ -> READ_CONFIG.
record(longout, "$(INSTR)$(M):Read_Config") {
record(longout, "$(INSTR)$(M):ReadConfig") {
field(DTYP, "asynInt32")
field(OUT, "@asyn($(CONTROLLER),$(AXIS),1) READ_CONFIG")
field(PINI, "NO")

View File

@ -1,598 +0,0 @@
// Needed to use strcpy_s from string.h
#define __STDC_WANT_LIB_EXT1__ 1
#include "pmacv3Controller.h"
#include "asynMotorController.h"
#include "asynOctetSyncIO.h"
#include "pmacv3Axis.h"
#include <epicsExport.h>
#include <errlog.h>
#include <iocsh.h>
#include <netinet/in.h>
#include <registryFunction.h>
#include <string.h>
#include <unistd.h>
/**
* @brief Copy src into dst and replace all carriage returns with spaces
*
* @param dst Buffer for the modified string
* @param src Original string
*/
void adjustResponseForPrint(char *dst, const char *src) {
// Needed to use strcpy_s from string.h
#ifdef __STDC_LIB_EXT1__
strcpy_s(dst, src);
for (size_t i = 0; i < strlen(dst); i++) {
if (dst[i] == '\r') {
dst[i] = '_';
}
}
#endif
}
/**
* @brief Construct a new pmacv3Controller::pmacv3Controller object
*
* @param portName See documentation of sinqController
* @param ipPortConfigName See documentation of sinqController
* @param numAxes See documentation of sinqController
* @param movingPollPeriod See documentation of sinqController
* @param idlePollPeriod See documentation of sinqController
* @param comTimeout Time after which a communication timeout error
* is declared in writeRead (in seconds)
* @param extraParams See documentation of sinqController
*/
pmacv3Controller::pmacv3Controller(const char *portName,
const char *ipPortConfigName, int numAxes,
double movingPollPeriod,
double idlePollPeriod, double comTimeout)
: sinqController(
portName, ipPortConfigName, numAxes, movingPollPeriod, idlePollPeriod,
/*
The following parameter library entries are added in this driver:
- ENCODER_TYPE
- REREAD_ENCODER_POSITION
- READ_CONFIG
- ACCEL_FROM_DRIVER
*/
4)
{
// Initialization of local variables
asynStatus status = asynSuccess;
// Initialization of all member variables
lowLevelPortUser_ = nullptr;
comTimeout_ = comTimeout;
// =========================================================================;
/*
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, &lowLevelPortUser_, NULL);
if (status != asynSuccess || lowLevelPortUser_ == nullptr) {
errlogPrintf(
"%s => line %d:\nFATAL ERROR (cannot connect to MCU controller).\n"
"Terminating IOC",
__PRETTY_FUNCTION__, __LINE__);
exit(-1);
}
// =========================================================================
// Create additional parameter library entries
status = createParam("ENCODER_TYPE", asynParamOctet, &encoderType_);
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nFATAL ERROR (creating a parameter failed "
"with %s).\nTerminating IOC",
__PRETTY_FUNCTION__, __LINE__, stringifyAsynStatus(status));
exit(-1);
}
status = createParam("REREAD_ENCODER_POSITION", asynParamInt32,
&rereadEncoderPosition_);
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nFATAL ERROR (creating a parameter failed "
"with %s).\nTerminating IOC",
__PRETTY_FUNCTION__, __LINE__, stringifyAsynStatus(status));
exit(-1);
}
status = createParam("READ_CONFIG", asynParamInt32, &readConfig_);
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nFATAL ERROR (creating a parameter failed "
"with %s).\nTerminating IOC",
__PRETTY_FUNCTION__, __LINE__, stringifyAsynStatus(status));
exit(-1);
}
/*
Define the end-of-string of a message coming from the device to EPICS.
It is not necessary to append a terminator to outgoing messages, since
the message length is encoded in the message header in the getSetResponse
method.
*/
const char *message_from_device =
"\006"; // Hex-code for ACK (acknowledge) -> Each message from the MCU
// is terminated by this value
status = pasynOctetSyncIO->setInputEos(
lowLevelPortUser_, message_from_device, strlen(message_from_device));
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nFATAL ERROR (setting input EOS failed "
"with %s).\nTerminating IOC",
__PRETTY_FUNCTION__, __LINE__, stringifyAsynStatus(status));
pasynOctetSyncIO->disconnect(lowLevelPortUser_);
exit(-1);
}
status = callParamCallbacks();
if (status != asynSuccess) {
asynPrint(
this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nFATAL ERROR (executing ParamLib callbacks failed "
"with %s).\nTerminating IOC",
__PRETTY_FUNCTION__, __LINE__, stringifyAsynStatus(status));
pasynOctetSyncIO->disconnect(lowLevelPortUser_);
exit(-1);
}
}
/*
Access one of the axes of the controller via the axis adress stored in asynUser.
If the axis does not exist or is not a Axis, a nullptr is returned and an
error is emitted.
*/
pmacv3Axis *pmacv3Controller::getAxis(asynUser *pasynUser) {
asynMotorAxis *asynAxis = asynMotorController::getAxis(pasynUser);
return pmacv3Controller::castToAxis(asynAxis);
}
/*
Access one of the axes of the controller via the axis index.
If the axis does not exist or is not a Axis, the function must return Null
*/
pmacv3Axis *pmacv3Controller::getAxis(int axisNo) {
asynMotorAxis *asynAxis = asynMotorController::getAxis(axisNo);
return pmacv3Controller::castToAxis(asynAxis);
}
pmacv3Axis *pmacv3Controller::castToAxis(asynMotorAxis *asynAxis) {
// =========================================================================
// If the axis slot of the pAxes_ array is empty, a nullptr must be returned
if (asynAxis == nullptr) {
return nullptr;
}
// Here, an error is emitted since asyn_axis is not a nullptr but also not
// an instance of Axis
pmacv3Axis *axis = dynamic_cast<pmacv3Axis *>(asynAxis);
if (axis == nullptr) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nAxis %d is not an instance of pmacv3Axis",
__PRETTY_FUNCTION__, __LINE__, axis->axisNo_);
}
return axis;
}
asynStatus pmacv3Controller::writeRead(int axisNo, const char *command,
char *response,
int numExpectedResponses) {
// Definition of local variables.
asynStatus status = asynSuccess;
asynStatus pl_status = asynSuccess;
char fullCommand[MAXBUF_] = {0};
char drvMessageText[MAXBUF_] = {0};
char modResponse[MAXBUF_] = {0};
int motorStatusProblem = 0;
int numReceivedResponses = 0;
// Send the message and block the thread until either a response has
// been received or the timeout is triggered
int eomReason = 0; // Flag indicating why the message has ended
// Number of bytes of the outgoing message (which is command + the
// end-of-string terminator defined in the constructor)
size_t nbytesOut = 0;
// Number of bytes of the incoming message (which is response + the
// end-of-string terminator defined in the constructor)
size_t nbytesIn = 0;
// =========================================================================
pmacv3Axis *axis = getAxis(axisNo);
if (axis == nullptr) {
// We already did the error logging directly in getAxis
return asynError;
}
/*
The message protocol of the pmacv3 used at PSI looks as follows (all
characters immediately following each other without a newline):
0x40 (ASCII value of @) -> Request for download
0xBF (ASCII value of ¿) -> Select mode "get_response"
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
[message length in network byte order] -> Use the htons function for this
value [Actual message] It is not necessary to append a terminator, since
this protocol encodes the message length at the beginning. See Turbo PMAC
User Manual, page 418 in VR_PMAC_GETRESPONSE
The message has to be build manually into the buffer fullCommand, since it
contains NULL terminators in its middle, therefore the string manipulation
methods of C don't work.
*/
// The entire message is equal to the command length
const size_t commandLength =
strlen(command) + 1; // +1 because of the appended /r
const int offset = 8;
// Positions 2 to 6 must have the value 0. Since fullCommand is initialized
// as an array of zeros, we don't need to set these bits manually.
fullCommand[0] = '\x40';
fullCommand[1] = '\xBF';
fullCommand[7] = commandLength;
snprintf((char *)fullCommand + offset, MAXBUF_ - offset, "%s\r", command);
asynPrint(this->pasynUserSelf, ASYN_TRACEIO_DRIVER,
"%s => line %d:\nSending command %s", __PRETTY_FUNCTION__,
__LINE__, fullCommand);
// Perform the actual writeRead
status = pasynOctetSyncIO->writeRead(
lowLevelPortUser_, fullCommand, commandLength + offset, response,
MAXBUF_, comTimeout_, &nbytesOut, &nbytesIn, &eomReason);
/*
Calculate the number of received responses by counting the number of
carriage returns "\r" in the response.
*/
for (size_t i = 0; i < strlen(response); i++) {
if (response[i] == '\r') {
numReceivedResponses++;
}
}
/*
Check if we got the expected amount of responses. If we didn't, flush the
PMAC and try again. If that fails as well, return an error.
*/
if (numExpectedResponses != numReceivedResponses) {
// Flush message as defined in Turbo PMAC User Manual, p. 430:
// \x40\xB3000
// VR_DOWNLOAD = \x40
// VR_PMAC_FLUSH = \xB3
char flush_msg[5] = {0};
flush_msg[0] = '\x40';
flush_msg[1] = '\xB3';
size_t nbytesOut = 0;
status = pasynOctetSyncIO->write(lowLevelPortUser_, flush_msg, 5,
comTimeout_, &nbytesOut);
// Wait after the flush so the MCU has time to prepare for the
// next command
usleep(100000);
if (status == asynSuccess) {
// If flushing the MCU succeded, try to send the command again
status = pasynOctetSyncIO->writeRead(
lowLevelPortUser_, fullCommand, commandLength + offset,
response, MAXBUF_, comTimeout_, &nbytesOut, &nbytesIn,
&eomReason);
// If the command returned a bad answer for the second time, give up
// and propagate the problem
numReceivedResponses = 0;
for (size_t i = 0; i < strlen(response); i++) {
if (response[i] == '\r') {
numReceivedResponses++;
}
}
// Second check: If this fails, give up and propagate the error.
if (numExpectedResponses != numReceivedResponses) {
adjustResponseForPrint(modResponse, response);
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nUnexpected response %s (_ are "
"carriage returns) for command %s\n",
__PRETTY_FUNCTION__, __LINE__, modResponse, command);
snprintf(drvMessageText, sizeof(drvMessageText),
"Received unexpected response %s (_ are "
"carriage returns) for command %s. "
"Please call the support",
modResponse, command);
pl_status = setStringParam(motorMessageText_, drvMessageText);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorMessageText_",
__PRETTY_FUNCTION__, __LINE__);
}
status = asynError;
}
} else {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nFlushing the MCU failed with %s\n",
__PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
}
}
// Create custom error messages for different failure modes
if (strlen(drvMessageText) == 0) {
switch (status) {
case asynSuccess:
break; // Communicate nothing
case asynTimeout:
snprintf(drvMessageText, sizeof(drvMessageText),
"connection timeout for axis %d", axisNo);
break;
case asynDisconnected:
snprintf(drvMessageText, sizeof(drvMessageText),
"axis is not connected");
break;
case asynDisabled:
snprintf(drvMessageText, sizeof(drvMessageText),
"axis is disabled");
break;
default:
snprintf(drvMessageText, sizeof(drvMessageText),
"Communication failed (%s)", stringifyAsynStatus(status));
break;
}
}
if (status != asynSuccess) {
// Check if the axis already is in an error communication mode. If it is
// not, upstream the error. This is done to avoid "flooding" the user
// with different error messages if more than one error ocurred before
// an error-free communication
pl_status =
getIntegerParam(axisNo, motorStatusProblem_, &motorStatusProblem);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusProblem_",
__PRETTY_FUNCTION__, __LINE__);
}
if (motorStatusProblem == 0) {
pl_status =
axis->setStringParam(this->motorMessageText_, drvMessageText);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorMessageText_",
__PRETTY_FUNCTION__, __LINE__);
}
}
}
// Log the overall status (communication successfull or not)
if (status == asynSuccess) {
asynPrint(lowLevelPortUser_, ASYN_TRACEIO_DRIVER,
"%s => line %d:\nDevice response: %s (_ are "
"carriage returns)\n",
__PRETTY_FUNCTION__, __LINE__, modResponse);
pl_status = axis->setIntegerParam(this->motorStatusCommsError_, 0);
} else {
if (status == asynSuccess) {
asynPrint(
lowLevelPortUser_, ASYN_TRACE_ERROR,
"%s => line %d:\nCommunication failed for command %s (%s)\n",
__PRETTY_FUNCTION__, __LINE__, fullCommand,
stringifyAsynStatus(status));
pl_status = axis->setIntegerParam(this->motorStatusCommsError_, 1);
}
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusCommsError_",
__PRETTY_FUNCTION__, __LINE__);
}
}
return asynSuccess;
}
asynStatus pmacv3Controller::writeInt32(asynUser *pasynUser, epicsInt32 value) {
int function = pasynUser->reason;
// =====================================================================
pmacv3Axis *axis = getAxis(pasynUser);
if (axis == nullptr) {
// We already did the error logging directly in getAxis
return asynError;
}
// Handle custom PVs
if (function == rereadEncoderPosition_) {
return axis->rereadEncoder();
} else if (function == readConfig_) {
return axis->readConfig();
} else {
return sinqController::writeInt32(pasynUser, value);
}
}
asynStatus pmacv3Controller::errMsgCouldNotParseResponse(
const char *command, const char *response, int axisNo,
const char *functionName, int lineNumber) {
char modifiedResponse[MAXBUF_] = {0};
adjustResponseForPrint(modifiedResponse, response);
return sinqController::errMsgCouldNotParseResponse(
command, modifiedResponse, axisNo, functionName, lineNumber);
}
/*************************************************************************************/
/** The following functions are C-wrappers, and can be called directly from
* iocsh */
extern "C" {
/*
C wrapper for the controller constructor. Please refer to the pmacv3Controller
constructor documentation.
*/
asynStatus pmacv3CreateController(const char *portName,
const char *lowLevelPortName, int numAxes,
double movingPollPeriod,
double idlePollPeriod, double comTimeout) {
/*
We create a new instance of the controller, using the "new" keyword to
allocate it on the heap while avoiding RAII.
https://github.com/epics-modules/motor/blob/master/motorApp/MotorSrc/asynMotorController.cpp
https://github.com/epics-modules/asyn/blob/master/asyn/asynPortDriver/asynPortDriver.cpp
The created object is registered in EPICS in its constructor and can safely
be "leaked" here.
*/
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-variable"
pmacv3Controller *pController =
new pmacv3Controller(portName, lowLevelPortName, numAxes,
movingPollPeriod, idlePollPeriod, comTimeout);
return asynSuccess;
}
/*
C wrapper for the axis constructor. Please refer to the pmacv3Axis constructor
documentation. The controller is read from the portName.
*/
asynStatus pmacv3CreateAxis(const char *portName, int axis) {
pmacv3Axis *pAxis;
/*
findAsynPortDriver is a asyn library FFI function which uses the C ABI.
Therefore it returns a void pointer instead of e.g. a pointer to a
superclass of the controller such as asynPortDriver. Type-safe upcasting
via dynamic_cast is therefore not possible directly. However, we do know
that the void pointer is either a pointer to asynPortDriver (if a driver
with the specified name exists) or a nullptr. Therefore, we first do a
nullptr check, then a cast to asynPortDriver and lastly a (typesafe)
dynamic_upcast to Controller
https://stackoverflow.com/questions/70906749/is-there-a-safe-way-to-cast-void-to-class-pointer-in-c
*/
void *ptr = findAsynPortDriver(portName);
if (ptr == nullptr) {
/*
We can't use asynPrint here since this macro would require us
to get a lowLevelPortUser_ from a pointer to an asynPortDriver.
However, the given pointer is a nullptr and therefore doesn't
have a lowLevelPortUser_! printf is an EPICS alternative which
works w/o that, but doesn't offer the comfort provided
by the asynTrace-facility
*/
errlogPrintf("%s => line %d:\nPort %s not found.", __PRETTY_FUNCTION__,
__LINE__, portName);
return asynError;
}
// Unsafe cast of the pointer to an asynPortDriver
asynPortDriver *apd = (asynPortDriver *)(ptr);
// Safe downcast
pmacv3Controller *pC = dynamic_cast<pmacv3Controller *>(apd);
if (pC == nullptr) {
errlogPrintf(
"%s => line %d:\ncontroller on port %s is not a pmacv3Controller.",
__PRETTY_FUNCTION__, __LINE__, portName);
return asynError;
}
// Prevent manipulation of the controller from other threads while we
// create the new axis.
pC->lock();
/*
We create a new instance of the axis, using the "new" keyword to
allocate it on the heap while avoiding RAII.
https://github.com/epics-modules/motor/blob/master/motorApp/MotorSrc/asynMotorController.cpp
https://github.com/epics-modules/asyn/blob/master/asyn/asynPortDriver/asynPortDriver.cpp
The created object is registered in EPICS in its constructor and can safely
be "leaked" here.
*/
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-variable"
pAxis = new pmacv3Axis(pC, axis);
// Allow manipulation of the controller again
pC->unlock();
return asynSuccess;
}
/*
This is boilerplate code which is used to make the FFI functions
CreateController and CreateAxis "known" to the IOC shell (iocsh).
*/
#ifdef vxWorks
#else
/*
Define name and type of the arguments for the CreateController function
in the iocsh. This is done by creating structs with the argument names and
types and then providing "factory" functions
(configCreateControllerCallFunc). These factory functions are used to
register the constructors during compilation.
*/
static const iocshArg CreateControllerArg0 = {"Controller name (e.g. mcu1)",
iocshArgString};
static const iocshArg CreateControllerArg1 = {"Asyn IP port name (e.g. pmcu1)",
iocshArgString};
static const iocshArg CreateControllerArg2 = {"Number of axes", iocshArgInt};
static const iocshArg CreateControllerArg3 = {"Moving poll rate (s)",
iocshArgDouble};
static const iocshArg CreateControllerArg4 = {"Idle poll rate (s)",
iocshArgDouble};
static const iocshArg CreateControllerArg5 = {"Communication timeout (s)",
iocshArgDouble};
static const iocshArg *const CreateControllerArgs[] = {
&CreateControllerArg0, &CreateControllerArg1, &CreateControllerArg2,
&CreateControllerArg3, &CreateControllerArg4, &CreateControllerArg5};
static const iocshFuncDef configPmacV3CreateController = {"pmacv3Controller", 6,
CreateControllerArgs};
static void configPmacV3CreateControllerCallFunc(const iocshArgBuf *args) {
pmacv3CreateController(args[0].sval, args[1].sval, args[2].ival,
args[3].dval, args[4].dval, args[5].dval);
}
/*
Same procedure as for the CreateController function, but for the axis
itself.
*/
static const iocshArg CreateAxisArg0 = {"Controller name (e.g. mcu1)",
iocshArgString};
static const iocshArg CreateAxisArg1 = {"Axis number", iocshArgInt};
static const iocshArg *const CreateAxisArgs[] = {&CreateAxisArg0,
&CreateAxisArg1};
static const iocshFuncDef configPmacV3CreateAxis = {"pmacv3Axis", 2,
CreateAxisArgs};
static void configPmacV3CreateAxisCallFunc(const iocshArgBuf *args) {
pmacv3CreateAxis(args[0].sval, args[1].ival);
}
// This function is made known to EPICS in pmacv3.dbd and is called by EPICS
// in order to register both functions in the IOC shell
static void pmacv3Register(void) {
iocshRegister(&configPmacV3CreateController,
configPmacV3CreateControllerCallFunc);
iocshRegister(&configPmacV3CreateAxis, configPmacV3CreateAxisCallFunc);
}
epicsExportRegistrar(pmacv3Register);
#endif
} // extern "C"

View File

@ -1,4 +1,5 @@
#---------------------------------------------
# SINQ specific DB definitions
#---------------------------------------------
registrar(pmacv3Register)
registrar(turboPmacControllerRegister)
registrar(turboPmacAxisRegister)

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,27 @@
#ifndef pmacv3AXIS_H
#define pmacv3AXIS_H
#ifndef turboPmacAXIS_H
#define turboPmacAXIS_H
#include "sinqAxis.h"
// Forward declaration of the controller class to resolve the cyclic dependency
// between C804Controller.h and C804Axis.h. See
// https://en.cppreference.com/w/cpp/language/class.
class pmacv3Controller;
class turboPmacController;
class pmacv3Axis : public sinqAxis {
class turboPmacAxis : public sinqAxis {
public:
/**
* @brief Construct a new pmacv3Axis
* @brief Construct a new turboPmacAxis
*
* @param pController Pointer to the associated controller
* @param axisNo Index of the axis
*/
pmacv3Axis(pmacv3Controller *pController, int axisNo);
turboPmacAxis(turboPmacController *pController, int axisNo);
/**
* @brief Destroy the pmacv3Axis
* @brief Destroy the turboPmacAxis
*
*/
virtual ~pmacv3Axis();
virtual ~turboPmacAxis();
/**
* @brief Implementation of the `stop` function from asynMotorAxis
@ -69,7 +69,7 @@ class pmacv3Axis : public sinqAxis {
double max_velocity, double acceleration);
/**
* @brief Implementation of the `atFirstPoll` function from sinqAxis.
* @brief Readout of some values from the controller at IOC startup
*
* The following steps are performed:
* - Read out the motor status, motor position, velocity and acceleration
@ -78,7 +78,7 @@ class pmacv3Axis : public sinqAxis {
*
* @return asynStatus
*/
asynStatus atFirstPoll();
asynStatus init();
/**
* @brief Enable / disable the axis.
@ -88,15 +88,6 @@ class pmacv3Axis : public sinqAxis {
*/
asynStatus enable(bool on);
/**
* @brief EThis function sets "on" to true, if the motor is enabled, and to
* false otherwise
*
* @param on
* @return asynStatus
*/
asynStatus isEnabled(bool *on);
/**
* @brief Read the encoder type (incremental or absolute) for this axis from
* the MCU and store the information in the PV ENCODER_TYPE.
@ -113,10 +104,8 @@ class pmacv3Axis : public sinqAxis {
asynStatus rereadEncoder();
protected:
pmacv3Controller *pC_;
turboPmacController *pC_;
asynStatus readConfig();
bool initial_poll_;
bool waitForHandshake_;
time_t timeAtHandshake_;
@ -124,7 +113,7 @@ class pmacv3Axis : public sinqAxis {
int axisStatus_;
private:
friend class pmacv3Controller;
friend class turboPmacController;
};
#endif

600
src/turboPmacController.cpp Normal file
View File

@ -0,0 +1,600 @@
#include "turboPmacController.h"
#include "asynMotorController.h"
#include "asynOctetSyncIO.h"
#include "turboPmacAxis.h"
#include <epicsExport.h>
#include <errlog.h>
#include <initHooks.h>
#include <iocsh.h>
#include <netinet/in.h>
#include <registryFunction.h>
#include <string.h>
#include <unistd.h>
/**
* @brief Copy src into dst and replace all carriage returns with spaces. This
* allows to print *dst with asynPrint.
*
*
* @param dst Buffer for the modified string
* @param src Original string
*/
void adjustResponseForPrint(char *dst, const char *src, size_t buf_length) {
for (size_t i = 0; i < buf_length; i++) {
if (src[i] == '\r') {
dst[i] = ' ';
} else {
dst[i] = src[i];
}
}
}
/**
* @brief Construct a new turboPmacController::turboPmacController object
*
* @param portName See documentation of sinqController
* @param ipPortConfigName See documentation of sinqController
* @param numAxes See documentation of sinqController
* @param movingPollPeriod See documentation of sinqController
* @param idlePollPeriod See documentation of sinqController
* @param comTimeout Time after which a communication timeout error
* is declared in writeRead (in seconds)
* @param extraParams See documentation of sinqController
*/
turboPmacController::turboPmacController(const char *portName,
const char *ipPortConfigName,
int numAxes, double movingPollPeriod,
double idlePollPeriod,
double comTimeout)
: sinqController(
portName, ipPortConfigName, numAxes, movingPollPeriod, idlePollPeriod,
/*
The following parameter library entries are added in this driver:
- REREAD_ENCODER_POSITION
- READ_CONFIG
*/
NUM_turboPmac_DRIVER_PARAMS)
{
// Initialization of local variables
asynStatus status = asynSuccess;
// Initialization of all member variables
lowLevelPortUser_ = nullptr;
comTimeout_ = comTimeout;
// Maximum allowed number of subsequent timeouts before the user is
// informed.
maxSubsequentTimeouts_ = 10;
// =========================================================================;
/*
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, &lowLevelPortUser_, NULL);
if (status != asynSuccess || lowLevelPortUser_ == nullptr) {
errlogPrintf("Controller \"%s\" => %s, line %d\nFATAL ERROR "
"(cannot connect to MCU controller).\nTerminating IOC",
portName, __PRETTY_FUNCTION__, __LINE__);
exit(-1);
}
// =========================================================================
// Create additional parameter library entries
status = createParam("REREAD_ENCODER_POSITION", asynParamInt32,
&rereadEncoderPosition_);
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("READ_CONFIG", asynParamInt32, &readConfig_);
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);
}
/*
Define the end-of-string of a message coming from the device to EPICS.
It is not necessary to append a terminator to outgoing messages, since
the message length is encoded in the message header in the getSetResponse
method.
*/
const char *message_from_device =
"\006"; // Hex-code for ACK (acknowledge) -> Each message from the MCU
// is terminated by this value
status = pasynOctetSyncIO->setInputEos(
lowLevelPortUser_, message_from_device, strlen(message_from_device));
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\" => %s, line %d\nFATAL ERROR "
"(setting input EOS failed with %s).\nTerminating IOC",
portName, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
pasynOctetSyncIO->disconnect(lowLevelPortUser_);
exit(-1);
}
status = callParamCallbacks();
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\" => %s, line %d\nFATAL ERROR "
"(executing ParamLib callbacks failed "
"with %s).\nTerminating IOC",
portName, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
pasynOctetSyncIO->disconnect(lowLevelPortUser_);
exit(-1);
}
}
/*
Access one of the axes of the controller via the axis adress stored in asynUser.
If the axis does not exist or is not a Axis, a nullptr is returned and an
error is emitted.
*/
turboPmacAxis *turboPmacController::getAxis(asynUser *pasynUser) {
asynMotorAxis *asynAxis = asynMotorController::getAxis(pasynUser);
return turboPmacController::castToAxis(asynAxis);
}
/*
Access one of the axes of the controller via the axis index.
If the axis does not exist or is not a Axis, the function must return Null
*/
turboPmacAxis *turboPmacController::getAxis(int axisNo) {
asynMotorAxis *asynAxis = asynMotorController::getAxis(axisNo);
return turboPmacController::castToAxis(asynAxis);
}
turboPmacAxis *turboPmacController::castToAxis(asynMotorAxis *asynAxis) {
// =========================================================================
// If the axis slot of the pAxes_ array is empty, a nullptr must be returned
if (asynAxis == nullptr) {
return nullptr;
}
// Here, an error is emitted since asyn_axis is not a nullptr but also not
// an instance of Axis
turboPmacAxis *axis = dynamic_cast<turboPmacAxis *>(asynAxis);
if (axis == nullptr) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nAxis is not "
"an instance of turboPmacAxis",
portName, axis->axisNo_, __PRETTY_FUNCTION__, __LINE__);
}
return axis;
}
asynStatus turboPmacController::writeRead(int axisNo, const char *command,
char *response,
int numExpectedResponses) {
// Definition of local variables.
asynStatus status = asynSuccess;
asynStatus paramLibStatus = asynSuccess;
asynStatus timeoutStatus = asynSuccess;
char fullCommand[MAXBUF_] = {0};
char drvMessageText[MAXBUF_] = {0};
char modResponse[MAXBUF_] = {0};
int motorStatusProblem = 0;
int numReceivedResponses = 0;
/*
asyn defines the following reasons for an end-of-message coming from the MCU
(https://epics.anl.gov/modules/soft/asyn/R4-14/asynDriver.pdf, p. 28):
0: Timeout
1: Request count reached
2: End of string detected -> In this driver, this is the "normal" case
4: End indicator detected
Combinations of reasons are also possible, e.g. eomReason = 5 would mean
that both the request count was reached and an end indicator was detected.
*/
int eomReason = 0;
// Number of bytes of the outgoing message (which is command + the
// end-of-string terminator defined in the constructor)
size_t nbytesOut = 0;
// Number of bytes of the incoming message (which is response + the
// end-of-string terminator defined in the constructor)
size_t nbytesIn = 0;
// =========================================================================
turboPmacAxis *axis = getAxis(axisNo);
if (axis == nullptr) {
// We already did the error logging directly in getAxis
return asynError;
}
/*
The message protocol of the turboPmac used at PSI looks as follows (all
characters immediately following each other without a newline):
0x40 (ASCII value of @) -> Request for download
0xBF (ASCII value of ¿) -> Select mode "get_response"
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
0x00 (ASCII value of 0)
[message length in network byte order] -> Use the htons function for this
value [Actual message] It is not necessary to append a terminator, since
this protocol encodes the message length at the beginning. See Turbo PMAC
User Manual, page 418 in VR_PMAC_GETRESPONSE
x0D (ASCII value of carriage return) -> The controller needs a carriage
return at the end of a "send" command (a command were we transmit data via
=). For "request" commands (e.g. read status or position), this is not
necessary, but it doesn't hurt either, therefore we always add a carriage
return.
The message has to be build manually into the buffer fullCommand, since it
contains NULL terminators in its middle, therefore the string manipulation
methods of C don't work.
*/
const size_t commandLength = strlen(command);
const int offset = 9;
// Positions 2 to 6 must have the value 0. Since fullCommand is initialized
// as an array of zeros, we don't need to set these bits manually.
fullCommand[0] = '\x40';
fullCommand[1] = '\xBF';
// The size of size_t is platform dependant (pointers-sized), while htons
// needs an unsigned int. The byte order is then converted from host to
// network order. The offset "+1" is for the carriage return.
u_int16_t len = htons(static_cast<u_int16_t>(commandLength + 1));
// Split up into the upper and the lower byte
fullCommand[7] = (char)(len >> 8); // Shift the 8 higher bits to the right
fullCommand[8] = (char)(len & 0xFF); // Mask the higher bits
// Write the actual command behind the protocol
for (size_t i = 0; i < commandLength; i++) {
fullCommand[i + offset] = command[i];
}
fullCommand[offset + commandLength] = '\x0D';
// +1 for the carriage return.
const size_t fullComandLength = offset + commandLength + 1;
asynPrint(this->pasynUserSelf, ASYN_TRACEIO_DRIVER,
"Controller \"%s\", axis %d => %s, line %d\nSending command %s",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__, fullCommand);
/*
We use separated write and read commands here, not the combined writeRead
method, because the latter is actually a flushWriteRead (see
https://epics.anl.gov/modules/soft/asyn/R4-14/asynDriver.pdf, p. 31) ->
Calls the flush command, then the write command, then the read command.
The flush itself reads repeatedly from the MCU until no messages are there
anymore. (The Diamond Light Source driver first send a PMAC flush command
and then does the same as the asyn flush). We don't want this behaviour.
(https://www.slac.stanford.edu/grp/lcls/controls/global/doc/epics-modules/R3-14-12/asyn/asyn-R4-18-lcls2/asyn/interfaces/asynOctetBase.c)
If a timeout occurs during writing or reading, inform the user that we're
trying to reconnect. If the problem persists, ask them to call the support
*/
status = pasynOctetSyncIO->write(lowLevelPortUser_, fullCommand,
fullComandLength, comTimeout_, &nbytesOut);
if (status == asynTimeout) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nTimeout while "
"writing to the MCU\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
timeoutStatus = checkComTimeoutWatchdog(axisNo, drvMessageText,
sizeof(drvMessageText));
int timeoutCounter = 0;
while (1) {
checkMaxSubsequentTimeouts(timeoutCounter, axis);
timeoutCounter += 1;
status = pasynOctetSyncIO->write(lowLevelPortUser_, fullCommand,
fullComandLength, comTimeout_,
&nbytesOut);
if (status != asynTimeout) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d\nReconnected after write timeout\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
break;
}
}
} else if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nError %s while "
"writing to the controller\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
}
// Read the response from the MCU buffer
status = pasynOctetSyncIO->read(lowLevelPortUser_, response, MAXBUF_,
comTimeout_, &nbytesIn, &eomReason);
if (status == asynTimeout) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nTimeout while "
"reading from the MCU\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
// Add this event to the back of the timeout event counter
timeoutStatus = checkComTimeoutWatchdog(axisNo, drvMessageText,
sizeof(drvMessageText));
int timeoutCounter = 0;
while (1) {
checkMaxSubsequentTimeouts(timeoutCounter, axis);
timeoutCounter += 1;
status =
pasynOctetSyncIO->read(lowLevelPortUser_, response, MAXBUF_,
comTimeout_, &nbytesIn, &eomReason);
if (status != asynTimeout) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d\nReconnected after read timeout\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
break;
}
}
} else if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nError %s while "
"reading from the controller\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
}
if (timeoutStatus == asynError) {
status = asynError;
}
// The message should only ever terminate due to reason 2
if (eomReason != 2) {
status = asynError;
snprintf(drvMessageText, sizeof(drvMessageText),
"Terminated message due to reason %d (should be 2).",
eomReason);
asynPrint(this->pasynUserSelf, ASYN_TRACEIO_DRIVER,
"Controller \"%s\", axis %d => %s, line %d\nMessage "
"terminated due to reason %i\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__, eomReason);
}
/*
Calculate the number of received responses by counting the number of
carriage returns "\r" in the response.
*/
for (size_t i = 0; i < strlen(response); i++) {
if (response[i] == '\r') {
numReceivedResponses++;
}
}
if (numExpectedResponses != numReceivedResponses) {
adjustResponseForPrint(modResponse, response, MAXBUF_);
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nUnexpected "
"response '%s' (carriage returns are replaced with spaces) "
"for command %s\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__, modResponse,
command);
snprintf(drvMessageText, sizeof(drvMessageText),
"Received unexpected response '%s' (carriage returns "
"are replaced with spaces) for command %s. "
"Please call the support",
modResponse, command);
status = asynError;
}
// Create custom error messages for different failure modes, if no error
// message has been set yet
if (strlen(drvMessageText) == 0) {
switch (status) {
case asynSuccess:
break; // Communicate nothing
case asynTimeout:
snprintf(drvMessageText, sizeof(drvMessageText),
"connection timeout for axis %d", axisNo);
break;
case asynDisconnected:
snprintf(drvMessageText, sizeof(drvMessageText),
"axis is not connected");
break;
case asynDisabled:
snprintf(drvMessageText, sizeof(drvMessageText),
"axis is disabled");
break;
default:
snprintf(drvMessageText, sizeof(drvMessageText),
"Communication failed (%s)", stringifyAsynStatus(status));
break;
}
}
// Log the overall status (communication successfull or not)
if (status == asynSuccess) {
asynPrint(lowLevelPortUser_, ASYN_TRACEIO_DRIVER,
"Controller \"%s\", axis %d => %s, line %d\nDevice "
"response: %s (carriage returns are replaced with spaces)\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__, modResponse);
paramLibStatus = axis->setIntegerParam(this->motorStatusCommsError_, 0);
} else {
asynPrint(lowLevelPortUser_, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nCommunication "
"failed for command %s (%s)\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__, fullCommand,
stringifyAsynStatus(status));
// Check if the axis already is in an error communication mode. If it is
// not, upstream the error. This is done to avoid "flooding" the user
// with different error messages if more than one error ocurred before
// an error-free communication
paramLibStatus =
getIntegerParam(axisNo, motorStatusProblem_, &motorStatusProblem);
if (paramLibStatus != asynSuccess) {
return paramLibAccessFailed(paramLibStatus, "motorStatusProblem_",
axisNo, __PRETTY_FUNCTION__, __LINE__);
}
if (motorStatusProblem == 0) {
paramLibStatus =
axis->setStringParam(motorMessageText_, drvMessageText);
if (paramLibStatus != asynSuccess) {
return paramLibAccessFailed(paramLibStatus, "motorMessageText_",
axisNo, __PRETTY_FUNCTION__,
__LINE__);
}
paramLibStatus = axis->setIntegerParam(motorStatusProblem_, 1);
if (paramLibStatus != asynSuccess) {
return paramLibAccessFailed(paramLibStatus,
"motorStatusProblem", axisNo,
__PRETTY_FUNCTION__, __LINE__);
}
paramLibStatus = axis->setIntegerParam(motorStatusProblem_, 1);
if (paramLibStatus != asynSuccess) {
return paramLibAccessFailed(paramLibStatus,
"motorStatusCommsError_", axisNo,
__PRETTY_FUNCTION__, __LINE__);
}
}
}
return status;
}
asynStatus turboPmacController::writeInt32(asynUser *pasynUser,
epicsInt32 value) {
int function = pasynUser->reason;
// =====================================================================
turboPmacAxis *axis = getAxis(pasynUser);
if (axis == nullptr) {
// We already did the error logging directly in getAxis
return asynError;
}
// Handle custom PVs
if (function == rereadEncoderPosition_) {
return axis->rereadEncoder();
} else if (function == readConfig_) {
return axis->init();
} else {
return sinqController::writeInt32(pasynUser, value);
}
}
asynStatus sinqController::readInt32(asynUser *pasynUser, epicsInt32 *value) {
// PMACs can be disabled
if (pasynUser->reason == motorCanDisable_) {
*value = 1;
return asynSuccess;
} else {
return asynMotorController::readInt32(pasynUser, value);
}
}
asynStatus turboPmacController::errMsgCouldNotParseResponse(
const char *command, const char *response, int axisNo,
const char *functionName, int lineNumber) {
char modifiedResponse[MAXBUF_] = {0};
adjustResponseForPrint(modifiedResponse, response, MAXBUF_);
return sinqController::errMsgCouldNotParseResponse(
command, modifiedResponse, axisNo, functionName, lineNumber);
}
/*************************************************************************************/
/** The following functions are C-wrappers, and can be called directly from
* iocsh */
extern "C" {
/*
C wrapper for the controller constructor. Please refer to the
turboPmacController constructor documentation.
*/
asynStatus turboPmacCreateController(const char *portName,
const char *ipPortConfigName, int numAxes,
double movingPollPeriod,
double idlePollPeriod, double comTimeout) {
/*
We create a new instance of the controller, using the "new" keyword to
allocate it on the heap while avoiding RAII.
https://github.com/epics-modules/motor/blob/master/motorApp/MotorSrc/asynMotorController.cpp
https://github.com/epics-modules/asyn/blob/master/asyn/asynPortDriver/asynPortDriver.cpp
The created object is registered in EPICS in its constructor and can safely
be "leaked" here.
*/
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-variable"
turboPmacController *pController =
new turboPmacController(portName, ipPortConfigName, numAxes,
movingPollPeriod, idlePollPeriod, comTimeout);
return asynSuccess;
}
/*
Define name and type of the arguments for the CreateController function
in the iocsh. This is done by creating structs with the argument names and
types and then providing "factory" functions
(configCreateControllerCallFunc). These factory functions are used to
register the constructors during compilation.
*/
static const iocshArg CreateControllerArg0 = {"Controller name (e.g. mcu1)",
iocshArgString};
static const iocshArg CreateControllerArg1 = {"Asyn IP port name (e.g. pmcu1)",
iocshArgString};
static const iocshArg CreateControllerArg2 = {"Number of axes", iocshArgInt};
static const iocshArg CreateControllerArg3 = {"Moving poll rate (s)",
iocshArgDouble};
static const iocshArg CreateControllerArg4 = {"Idle poll rate (s)",
iocshArgDouble};
static const iocshArg CreateControllerArg5 = {"Communication timeout (s)",
iocshArgDouble};
static const iocshArg *const CreateControllerArgs[] = {
&CreateControllerArg0, &CreateControllerArg1, &CreateControllerArg2,
&CreateControllerArg3, &CreateControllerArg4, &CreateControllerArg5};
static const iocshFuncDef configturboPmacCreateController = {
"turboPmacController", 6, CreateControllerArgs};
static void configTurboPmacCreateControllerCallFunc(const iocshArgBuf *args) {
turboPmacCreateController(args[0].sval, args[1].sval, args[2].ival,
args[3].dval, args[4].dval, args[5].dval);
}
// This function is made known to EPICS in turboPmac.dbd and is called by EPICS
// in order to register both functions in the IOC shell
static void turboPmacControllerRegister(void) {
iocshRegister(&configturboPmacCreateController,
configTurboPmacCreateControllerCallFunc);
}
epicsExportRegistrar(turboPmacControllerRegister);
} // extern "C"

View File

@ -1,25 +1,22 @@
/********************************************
* pmacv3Controller.h
* turboPmacController.h
*
* PMAC V3 controller driver based on the asynMotorController class
* Turbo PMAC controller driver based on the asynMotorController class
*
* Stefan Mathis, September 2024
********************************************/
#ifndef pmacv3Controller_H
#define pmacv3Controller_H
#include "pmacv3Axis.h"
#ifndef turboPmacController_H
#define turboPmacController_H
#include "sinqAxis.h"
#include "sinqController.h"
#include "turboPmacAxis.h"
#define IncrementalEncoder "Incremental encoder"
#define AbsoluteEncoder "Absolute encoder"
class pmacv3Controller : public sinqController {
class turboPmacController : public sinqController {
public:
/**
* @brief Construct a new pmacv3Controller object
* @brief Construct a new turboPmacController object
*
* @param portName See sinqController constructor
* @param ipPortConfigName See sinqController constructor
@ -30,25 +27,27 @@ class pmacv3Controller : public sinqController {
the underlying asynOctetSyncIO interface waits for a response until this
time (in seconds) has passed, then it declares a timeout.
*/
pmacv3Controller(const char *portName, const char *ipPortConfigName,
int numAxes, double movingPollPeriod,
double idlePollPeriod, double comTimeout);
turboPmacController(const char *portName, const char *ipPortConfigName,
int numAxes, double movingPollPeriod,
double idlePollPeriod, double comTimeout);
/**
* @brief Get the axis object
*
* @param pasynUser Specify the axis via the asynUser
* @return pmacv3Axis* If no axis could be found, this is a nullptr
* @return turboPmacAxis* If no axis could be found, this is a
* nullptr
*/
pmacv3Axis *getAxis(asynUser *pasynUser);
turboPmacAxis *getAxis(asynUser *pasynUser);
/**
* @brief Get the axis object
*
* @param axisNo Specify the axis via its index
* @return pmacv3Axis* If no axis could be found, this is a nullptr
* @return turboPmacAxis* If no axis could be found, this is a
* nullptr
*/
pmacv3Axis *getAxis(int axisNo);
turboPmacAxis *getAxis(int axisNo);
/**
* @brief Overloaded function of sinqController
@ -84,17 +83,17 @@ class pmacv3Controller : public sinqController {
int numExpectedResponses);
/**
* @brief Save cast of the given asynAxis pointer to a pmacv3Axis pointer.
* If the cast fails, this function returns a nullptr.
* @brief Save cast of the given asynAxis pointer to a turboPmacAxis
* pointer. If the cast fails, this function returns a nullptr.
*
* @param asynAxis
* @return pmacv3Axis*
* @return turboPmacAxis*
*/
pmacv3Axis *castToAxis(asynMotorAxis *asynAxis);
turboPmacAxis *castToAxis(asynMotorAxis *asynAxis);
/**
* @brief Specialized version of sinqController::errMsgCouldNotParseResponse
* for pmacv3
* for turboPmac
*
* This is an overloaded version of
* sinqController::errMsgCouldNotParseResponse which calls
@ -122,16 +121,21 @@ class pmacv3Controller : public sinqController {
static const uint32_t MAXBUF_ = 200;
/*
Stores the constructor input comTimeout
Timeout for the communication process in seconds
*/
double comTimeout_;
char lastResponse[MAXBUF_];
// Indices of additional PVs
#define FIRST_turboPmac_PARAM rereadEncoderPosition_
int rereadEncoderPosition_;
int readConfig_;
int encoderType_;
#define LAST_turboPmac_PARAM readConfig_
friend class pmacv3Axis;
friend class turboPmacAxis;
};
#define NUM_turboPmac_DRIVER_PARAMS \
(&LAST_turboPmac_PARAM - &FIRST_turboPmac_PARAM + 1)
#endif /* pmacv3Controller_H */
#endif /* turboPmacController_H */

View File

@ -0,0 +1,201 @@
#! venv/bin/python3
"""
This script can be used to format the communication between an EPICS IOC and a
PMAC MCU into JSON files. It does this by parsing PCAP files created by tcpdump
and rearranging the information in a more structured manner.
To read the manual, simply run this script without any arguments.
Stefan Mathis, January 2025
"""
from scapy.all import *
import json
import re
import codecs
import os
from datetime import datetime
def parse(fileAndPath):
try:
scapyCap = PcapReader(fileAndPath)
except:
print(f"Could not read file {fileAndPath} as PCAP file")
requests = []
sent = []
jsonDict = dict()
lastLayer = None
for packet in scapyCap:
layer = packet.getlayer(Raw)
if layer is None:
continue
# Skip the package if it is not a command or a response. A command ends
# with a carriage return (x0d), a response ends with ACKNOWLEDGE (x06)
last = layer.load[-1]
if last == 6:
isResponse = True
elif last == 13:
isResponse = False
else:
continue
# Store the info by the IP adress of the MCU
if isResponse:
ip = packet[IP].src
else:
ip = packet[IP].dst
if ip not in jsonDict:
jsonDict[ip] = dict()
# Convert to ASCII
ascii = layer.load.decode("unicode_escape")
# Convert the time to a float
time = float(packet.time)
if isResponse:
# A response is always a number followed by a carriage return
responses = re.findall("-?\d+\.\d+\r|-?\d+\r", ascii)
# Check if the number of responses matches the number of requests
valid = len(responses) == len(requests)
# Pair up the request-response pairs
for (request, response) in zip(requests, responses):
if request not in jsonDict[ip]:
jsonDict[ip][request] = dict()
if "." in response:
value = float(response)
else:
value = int(response)
lastLayer = lastPacket.getlayer(Raw)
lastTime = float(lastPacket.time)
data = {
'command': {
'hex': [format(value, '02x') for value in lastLayer.load],
'ascii': lastLayer.load.decode("unicode_escape"),
'timestamp': lastTime,
'timeiso': str(datetime.fromtimestamp(lastTime).isoformat()),
},
'response': {
'hex': [format(value, '02x') for value in layer.load],
'ascii': ascii,
'value': value,
'timestamp': time,
'timeiso': str(datetime.fromtimestamp(time).isoformat()),
'valid': valid
}
}
jsonDict[ip][request][time] = data
else:
requests.clear()
sent.clear()
# Store the packet for use in the response iteration
lastPacket = packet
# Parse the ASCII text via regex. A PMAC command usually has the
# format LDDDD(=<Number>), where L is a capital letter, the first
# two digits D are the axis number and the last two digits together
# with the letter form the command.
# Separate the commands into sent data (e.g. setting a position)
# and data requests (e.g. reading the axis status). Sent data always
# has an equal sign.
for command in re.findall("[A-Z]\d+=-?\d+|[A-Z]\d+", ascii):
if "=" in command:
sent.append(command)
else:
requests.append(command)
# Store the sent. The requests yfd stored together with the responses later.
for command in sent:
splitted = command.split("=")
key = splitted[0]
key = key + "="
if key not in jsonDict[ip]:
jsonDict[ip][key] = dict()
if "." in splitted[1]:
value = float(splitted[1])
else:
value = int(splitted[1])
data = {
'command': {
'hex': [format(value, '02x') for value in layer.load],
'ascii': ascii,
'value': value,
'timestamp': time,
'timeiso': str(datetime.fromtimestamp(time).isoformat()),
},
}
jsonDict[ip][key][time] = data
return jsonDict
if __name__ == "__main__":
isInstalled = False
try:
from scapy.all import *
isInstalled = True
except ImportError:
print("This script needs the Scapy package to run. In order to install a "
"suitable virtual environment, use the 'makevenv' script.")
if isInstalled:
from sys import argv
if len(argv) < 2:
print("""
This script can be used to format the communication between an EPICS IOC and a
PMAC MCU into JSON files. It does this by parsing PCAP files created by tcpdump
and rearranging the information in a more structured manner.
After a successfull parse run, the resulting JSON data looks like this:
<IP Adress MCU1>
<Request command type> (e.g. Q0100 to request the position of axis 1)
<Event timestamp>
Command
<Raw ASCII string>
<Actual command> (e.g. P0100)
<Timestamp in Epoch>
Response
<Raw ASCII string>
<Actual response (e.g. -3)
<Timestamp in Epoch>
<Set command type> (e.g. Q0100= to set the position of axis 1)
<Event timestamp>
Command
<Raw ASCII string>
<Actual command (e.g. P0100)
<Set value>
<Timestamp in Epoch>
<IP Adress MCU2>
""")
else:
for fileAndPath in argv[1:]:
jsonDict = parse(fileAndPath)
# Save the dict into a JSON
fileName = os.path.basename(fileAndPath)
jsonfileAndPath = f"{fileName}.json"
with open(jsonfileAndPath, 'w') as fp:
json.dump(jsonDict, fp, indent=4)
print(f"Stored parse result of {fileAndPath} in {fileName}")

Binary file not shown.

83
utils/analyzeTcpDump/demo.py Executable file
View File

@ -0,0 +1,83 @@
#! demovenv/bin/python3
"""
This demo script shows how the "parse" function of "analyzeTcpDump.py" can be
used to easily visualize data from a PCAP file created by the tcpdump tool /
wireshark. A suitable virtual environment can be created with the "makedemovenv"
script.
Stefan Mathis, January 2025
"""
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
from analyzeTcpDump import parse
if __name__ == "__main__":
data = parse("demo.pcap")
plt.figure(figsize=(12, 6))
# Plot the position of axis 5 over time
# Actual position
position_valid = []
dates_valid = []
position_all = []
dates_all = []
for (timestamp, item) in data["172.28.101.24"]["Q0510"].items():
date = datetime.fromtimestamp(timestamp)
value = item["response"]["value"]
dates_all.append(date)
position_all.append(value)
if item["response"]["valid"]:
dates_valid.append(date)
position_valid.append(value)
else:
command = item["command"]["ascii"]
response = item["response"]["ascii"]
# Replace non-renderable characters
command = command.replace("\0", "\\x00")
command = command.replace("\r", "\\x0d")
command = command.replace("\x12", "\\x12")
response = response.replace("\r", "\\x0d")
response = response.replace("\06", "\\x06")
# Shift the text a bit to the right
plt.text(date, value, f"Command: {command}\nResponse: {response}", horizontalalignment="right", verticalalignment="top")
# Target position
position_target = [position_valid[0]]
dates_target = [dates_valid[0]]
for (timestamp, item) in data["172.28.101.24"]["Q0501="].items():
date = datetime.fromtimestamp(timestamp)
value = item["command"]["value"]
dates_target.append(date)
position_target.append(position_target[-1])
dates_target.append(date)
position_target.append(value)
dates_target.append(dates_valid[-1])
position_target.append(position_target[-1])
plt.plot(dates_target, position_target, "k--", label="Target position")
plt.plot(dates_all, position_all, "r-", label="All responses")
plt.plot(dates_valid, position_valid, "b-", label="Valid responses")
plt.xlabel("Time (ISO 8601)")
plt.ylabel("Axis position in degree")
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%dT%H:%M:%S"))
plt.xticks(rotation=45)
plt.grid(True)
plt.legend(loc="lower left")
plt.title("Position of axis 5")
plt.tight_layout()
plt.show()

View File

@ -0,0 +1,22 @@
#!/bin/bash
#-------------------------------------------------------------------------
# Script which installs a virtual environment for PCAP file parsing
#
# Stefan Mathis, September 2024
#-------------------------------------------------------------------------
# Remove any previous testing environment
if [ -d "demovenv" ]; then
rm -r demovenv
fi
/usr/bin/python3.11 -m venv demovenv
source demovenv/bin/activate
pip install --upgrade pip
pip install "scapy>=2.5,<3.0"
pip install matplotlib
# Exit the virtual environment
exit

21
utils/analyzeTcpDump/makevenv Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
#-------------------------------------------------------------------------
# Script which installs a virtual environment for PCAP file parsing
#
# Stefan Mathis, September 2024
#-------------------------------------------------------------------------
# Remove any previous testing environment
if [ -d "venv" ]; then
rm -r venv
fi
/usr/bin/python3.11 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install "scapy>=2.5,<3.0"
# Exit the virtual environment
exit

172
utils/writeRead.py Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
This script allows direct interaction with a pmac-Controller over an ethernet connection.
To read the manual, simply run this script without any arguments.
Stefan Mathis, December 2024
"""
import struct
import socket
import curses
def packPmacCommand(command):
# 0x40 = VR_DOWNLOAD
# 0xBF = VR_PMAC_GETRESPONSE
buf = struct.pack('BBHHH',0x40,0xBF,0,0,socket.htons(len(command)))
buf = buf + bytes(command,'utf-8')
return buf
def readPmacReply(input):
msg = bytearray()
expectAck = True
while True:
b = input.recv(1)
bint = int.from_bytes(b,byteorder='little')
if bint == 2 or bint == 7: #STX or BELL
expectAck = False
continue
if expectAck and bint == 6: # ACK
return bytes(msg)
else:
if bint == 13 and not expectAck: # CR
return bytes(msg)
else:
msg.append(bint)
if __name__ == "__main__":
from sys import argv
try:
addr = argv[1].split(':')
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((addr[0],int(addr[1])))
if len(argv) == 3:
buf = packPmacCommand(argv[2])
s.send(buf)
reply = readPmacReply(s)
print(reply.decode('utf-8') + '\n')
else:
try:
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
stdscr.keypad(True)
stdscr.scrollok(True)
stdscr.addstr(">> ")
stdscr.refresh()
history = [""]
ptr = len(history) - 1
while True:
c = stdscr.getch()
if c == curses.KEY_RIGHT:
(y, x) = stdscr.getyx()
if x < len(history[ptr]) + 3:
stdscr.move(y, x+1)
stdscr.refresh()
elif c == curses.KEY_LEFT:
(y, x) = stdscr.getyx()
if x > 3:
stdscr.move(y, x-1)
stdscr.refresh()
elif c == curses.KEY_UP:
if ptr > 0:
ptr -= 1
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
elif c == curses.KEY_DOWN:
if ptr < len(history) - 1:
ptr += 1
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
elif c == curses.KEY_ENTER or c == ord('\n') or c == ord('\r'):
if history[ptr] == 'quit':
break
# because of arrow keys move back to the end of the line
(y, x) = stdscr.getyx()
stdscr.move(y, 3+len(history[ptr]))
if history[ptr]:
buf = packPmacCommand(history[ptr])
s.send(buf)
reply = readPmacReply(s)
stdscr.addstr("\n" + reply.decode('utf-8')[0:-1])
if ptr == len(history) - 1 and history[ptr] != "":
history += [""]
else:
history[-1] = ""
ptr = len(history) - 1
stdscr.addstr("\n>> ")
stdscr.refresh()
else:
if ptr < len(history) - 1: # Modifying previous input
if len(history[-1]) == 0:
history[-1] = history[ptr]
ptr = len(history) - 1
else:
history += [history[ptr]]
ptr = len(history) - 1
if c == curses.KEY_BACKSPACE:
if len(history[ptr]) == 0:
continue
(y, x) = stdscr.getyx()
history[ptr] = history[ptr][0:x-4] + history[ptr][x-3:]
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
stdscr.move(y, x-1)
stdscr.refresh()
else:
(y, x) = stdscr.getyx()
history[ptr] = history[ptr][0:x-3] + chr(c) + history[ptr][x-3:]
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
stdscr.move(y, x+1)
stdscr.refresh()
finally:
# to quit
curses.nocbreak()
stdscr.keypad(False)
curses.echo()
curses.endwin()
except:
print("""
Invalid Arguments
Option 1: Single Command
------------------------
Usage: writeRead.py pmachost:port command
This then returns the response for command.
Option 2: CLI Mode
------------------
Usage: writeRead.py pmachost:port
You can then type in a command, hit enter, and the response will see
the reponse, before being prompted to again enter a command. Type
'quit' to close prompt.
""")