Compare commits

...

10 Commits
0.2.1 ... 0.5.0

Author SHA1 Message Date
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
3ee507086a Included the possibility to vary the motor speed. 2024-12-06 08:30:10 +01:00
2e2c24238b Prototype for v0.2 2024-12-04 13:39:36 +01:00
e967e65d33 Added support for (optional) variable speed drive mode and refactored
some records into sinqMotor
2024-11-29 14:54:05 +01:00
dc70b560f7 Improved the error message when the MCU response is printed and the IOC
shell constructor documentation.
2024-11-27 16:05:48 +01:00
a6227629ad Improved the error message when the MCU response is printed. 2024-11-27 15:51:03 +01:00
13 changed files with 690 additions and 273 deletions

View File

@ -11,7 +11,7 @@ REQUIRED+=asynMotor
REQUIRED+=sinqMotor
# Specify the version of sinqMotor we want to build against
sinqMotor_VERSION=0.3.0
sinqMotor_VERSION=0.6.3
# These headers allow to depend on this library for derived drivers.
HEADERS += src/pmacv3Axis.h

View File

@ -4,50 +4,44 @@
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.
## Usage in IOC shell
## 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.
The function arguments are documented directly within the source code or are available from the help function of the IOC shell.
## Database
The pmacV3 module provides additional PVs in the database template db/pmacv3.db. It can be parametrized with the `dbLoadTemplate` function from the IOC shell:
These functions are parametrized as follows:
```
require sinqMotor, y.y.y # The sinqMotor module is needed for the pmacv3 module. The version y.y.y is defined in the Makefile (line sinqMotor_VERSION=x.x.x)
require pmacv3, x.x.x # This is the three-digit version number of the pmacv3 module
dbLoadTemplate "motor.substitutions"
pmacv3Controller(
"$(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
0.05, # Busy poll period in seconds
1, # Idle poll period in seconds
0.05 # Communication timeout in seconds
);
```
```
pmacv3Axis(
"$(NAME)", # Name of the associated MCU, e.g. mcu1. This parameter should be provided by an environment variable.
1 # Index of the axis.
);
```
The substitutions file can be concatenated with that of sinqMotor:
```
file "$(sinqMotor_DB)/sinqMotor.db"
{
pattern
...
}
file "$(pmacv3_DB)/pmacv3.db"
{
pattern
{ AXIS, M}
{ 1, "lin1"}
{ 2, "rot1"}
}
```
The sinqMotor pattern "..." is documented in https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.
The other parameters have the following meaning:
- `AXIS`: Index of the axis, corresponds to the physical connection of the axis to the MCU
- `M`: Name of the motor as shown in EPICS
The axis name should correspond to that of the sinqMotor pattern with the same respective index.
## Versioning
### Versioning
Please see the documentation for the module sinqMotor: https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.
## How to build it
### How to build it
Please see the documentation for the module sinqMotor: https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.

View File

@ -1,59 +1,19 @@
# Encoder type
record(waveform, "$(P)$(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")
}
# reread encoder
record(longout, "$(P)$(M):Reread_Encoder") {
# 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):RereadEncoder") {
field(DTYP, "asynInt32")
field(OUT, "@asyn($(CONTROLLER),$(AXIS),1) REREAD_ENCODER_POSITION")
field(PINI, "NO")
}
# reread encoder
record(longout, "$(P)$(M):Read_Config") {
# The pmacV3 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):ReadConfig") {
field(DTYP, "asynInt32")
field(OUT, "@asyn($(CONTROLLER),$(AXIS),1) READ_CONFIG")
field(PINI, "NO")
}
# ===================================================================
# The following records read acceleration and velocity from the driver and
# copy those values into the corresponding fields of the main motor record.
# This strategy is described here: https://epics.anl.gov/tech-talk/2022/msg00464.php
# Helper record for the high limit which is filled in by the driver
record(ai, "$(P)$(M):MOTOR_VELOCITY-RBV")
{
field(DTYP, "asynFloat64")
field(INP, "@asyn($(CONTROLLER),$(AXIS)) MOTOR_VELOCITY_FROM_DRIVER")
field(SCAN, "I/O Intr")
field(FLNK, "$(P)$(M):PUSH_VELO_TO_FIELD")
}
# Push the value into the field of the main motor record
record(ao, "$(P)$(M):PUSH_VELO_TO_FIELD") {
field(DOL, "$(P)$(M):MOTOR_VELOCITY-RBV CP")
field(OUT, "$(P)$(M).VELO")
field(OMSL, "closed_loop") # This configuration keeps the PV and the field in sync
}
# Helper record for the low limit which is filled in by the driver
record(ai, "$(P)$(M):MOTOR_ACCL-RBV")
{
field(DTYP, "asynFloat64")
field(INP, "@asyn($(CONTROLLER),$(AXIS)) MOTOR_ACCEL_FROM_DRIVER")
field(SCAN, "I/O Intr")
field(FLNK, "$(P)$(M):PUSH_ACCL_TO_FIELD")
}
# Push the value into the field of the main motor record
record(ao, "$(P)$(M):PUSH_ACCL_TO_FIELD") {
field(DOL, "$(P)$(M):MOTOR_ACCL-RBV CP")
field(OUT, "$(P)$(M).ACCL")
field(OMSL, "closed_loop") # This configuration keeps the PV and the field in sync
}

View File

@ -50,7 +50,8 @@ pmacv3Axis::pmacv3Axis(pmacv3Controller *pC, int axisNo)
exit(-1);
}
status = pC_->setDoubleParam(axisNo_, pC_->motorPosition_, 0.0);
// pmacv3 motors can always be disabled
status = pC_->setIntegerParam(axisNo_, pC_->motorCanDisable_, 1);
if (status != asynSuccess) {
asynPrint(
pC_->pasynUserSelf, ASYN_TRACE_ERROR,
@ -73,12 +74,7 @@ pmacv3Axis::~pmacv3Axis(void) {
/**
Read the configuration at the first poll
*/
asynStatus pmacv3Axis::atFirstPoll() { return readConfig(); }
/*
Read the configuration from the motor control unit and the parameter library.
*/
asynStatus pmacv3Axis::readConfig() {
asynStatus pmacv3Axis::atFirstPoll() {
// Local variable declaration
asynStatus status = asynSuccess;
@ -87,6 +83,7 @@ asynStatus pmacv3Axis::readConfig() {
double motorRecResolution = 0.0;
double motorPosition = 0.0;
double motorVelocity = 0.0;
double motorVmax = 0.0;
double motorAccel = 0.0;
int acoDelay = 0.0; // Offset time for the movement watchdog caused by
// the air cushions in milliseconds.
@ -101,16 +98,19 @@ asynStatus pmacv3Axis::readConfig() {
__PRETTY_FUNCTION__, __LINE__);
}
// Software limits and current position
/*
Read out the axis status, the current position, current and maximum speed,
acceleration and the air cushion delay.
*/
snprintf(command, sizeof(command),
"P%2.2d00 Q%2.2d10 Q%2.2d04 Q%2.2d06 P%2.2d22", axisNo_, axisNo_,
axisNo_, axisNo_, axisNo_);
status = pC_->writeRead(axisNo_, command, response, 5);
"P%2.2d00 Q%2.2d10 Q%2.2d03 Q%2.2d04 Q%2.2d06 P%2.2d22", axisNo_,
axisNo_, axisNo_, axisNo_, axisNo_, axisNo_);
status = pC_->writeRead(axisNo_, command, response, 6);
if (status != asynSuccess) {
return status;
}
nvals = sscanf(response, "%d %lf %lf %lf %d", &axStatus, &motorPosition,
&motorVelocity, &motorAccel, &acoDelay);
nvals = sscanf(response, "%d %lf %lf %lf %lf %d", &axStatus, &motorPosition,
&motorVmax, &motorVelocity, &motorAccel, &acoDelay);
// The acoDelay is given in milliseconds -> Convert to seconds, rounded up
offsetMovTimeout_ = std::ceil(acoDelay / 1000.0);
@ -120,7 +120,7 @@ asynStatus pmacv3Axis::readConfig() {
// here to mm/s^2.
motorAccel = motorAccel * 1000;
if (nvals != 5) {
if (nvals != 6) {
return pC_->errMsgCouldNotParseResponse(command, response, axisNo_,
__PRETTY_FUNCTION__, __LINE__);
}
@ -135,25 +135,14 @@ asynStatus pmacv3Axis::readConfig() {
__PRETTY_FUNCTION__, __LINE__);
}
status =
pC_->setDoubleParam(axisNo_, pC_->motorVelocityRBV_, motorVelocity);
// Write to the motor record fields
status = setVeloFields(motorVelocity, 0.0, motorVmax);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorVelocityRBV_",
__PRETTY_FUNCTION__, __LINE__);
return status;
}
status = pC_->setDoubleParam(axisNo_, pC_->motorAccelRBV_, motorAccel);
status = setAcclField(motorAccel);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorAccelRBV_",
__PRETTY_FUNCTION__, __LINE__);
}
// Set the initial enable based on the motor status value
status =
setIntegerParam(pC_->enableMotor_, (axStatus != -3 && axStatus != -5));
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "enableMotor_",
__PRETTY_FUNCTION__, __LINE__);
return status;
}
// Update the parameter library immediately
@ -251,7 +240,7 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
}
// Transform from EPICS to motor coordinates (see comment in
// pmacv3Axis::readConfig())
// pmacv3Axis::atFirstPoll)
previousPosition = previousPosition * motorRecResolution;
// Query the axis status
@ -274,13 +263,13 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
The axis limits are set as: ({[]})
where [] are the positive and negative limits set in EPICS/NICOS, {} are the
software limits set on the MCU and () are the hardware limit switches. In
other words, the EPICS/NICOS limits must be stricter than the software
other words, the EPICS/NICOS limits should be stricter than the software
limits on the MCU which in turn should be stricter than the hardware limit
switches. For example, if the hardware limit switches are at [-10, 10], the
software limits could be at [-9, 9] and the EPICS / NICOS limits could be at
[-8, 8]. Therefore, we cannot use the software limits read from the MCU
directly, but need to shrink them a bit. In this case, we're shrinking them
by 0.1 mm or 0.1 degree (depending on the axis type) on both sides.
by limitsOffset on both sides.
*/
pl_status =
pC_->getDoubleParam(axisNo_, pC_->motorLimitsOffset_, &limitsOffset);
@ -294,6 +283,14 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
// Store the axis status
axisStatus_ = axStatus;
// Update the enablement PV
pl_status = setIntegerParam(pC_->motorEnableRBV_,
(axStatus != -3 && axStatus != -5));
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorEnableRBV_",
__PRETTY_FUNCTION__, __LINE__);
}
// Intepret the status
switch (axStatus) {
case -6:
@ -524,7 +521,7 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
snprintf(command, sizeof(command),
"Maximum allowed following error exceeded (P%2.2d01 = %d). "
"Check if movement range is blocked."
"Check if movement range is blocked. "
"Otherwise please call the support.",
axisNo_, error);
pl_status = setStringParam(pC_->motorMessageText_, command);
@ -618,13 +615,6 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
}
}
pl_status = setIntegerParam(pC_->enableMotorRBV_,
(axStatus != -3 && axStatus != -5));
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "enableMotorRBV_",
__PRETTY_FUNCTION__, __LINE__);
}
pl_status = setIntegerParam(pC_->motorStatusMoving_, *moving);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorStatusMoving_",
@ -658,7 +648,7 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
}
// Transform from motor to EPICS coordinates (see comment in
// pmacv3Axis::readConfig())
// pmacv3Axis::atFirstPoll())
currentPosition = currentPosition / motorRecResolution;
pl_status = setDoubleParam(pC_->motorPosition_, currentPosition);
@ -681,12 +671,15 @@ asynStatus pmacv3Axis::doMove(double position, int relative, double minVelocity,
char command[pC_->MAXBUF_], response[pC_->MAXBUF_];
double motorCoordinatesPosition = 0.0;
int enabled = 0;
double motorRecResolution = 0.0;
double motorVelocity = 0.0;
int enabled = 0;
int motorCanSetSpeed = 0;
int writeOffset = 0;
// =========================================================================
pl_status = pC_->getIntegerParam(axisNo_, pC_->enableMotorRBV_, &enabled);
pl_status = pC_->getIntegerParam(axisNo_, pC_->motorEnableRBV_, &enabled);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "enableMotorRBV_",
__PRETTY_FUNCTION__, __LINE__);
@ -708,18 +701,43 @@ asynStatus pmacv3Axis::doMove(double position, int relative, double minVelocity,
// Convert from EPICS to user / motor units
motorCoordinatesPosition = position * motorRecResolution;
motorVelocity = maxVelocity * motorRecResolution;
asynPrint(pC_->pasynUserSelf, ASYN_TRACE_FLOW,
"%s => line %d:\nStart of axis %d to position %lf.\n",
__PRETTY_FUNCTION__, __LINE__, axisNo_, position);
// Perform handshake, Set target position and start the move command
// Check if the speed is allowed to be changed
pl_status = pC_->getIntegerParam(axisNo_, pC_->motorCanSetSpeed_,
&motorCanSetSpeed);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorCanSetSpeed_",
__PRETTY_FUNCTION__, __LINE__);
}
// Prepend the new motor speed, if the user is allowed to set the speed.
// Mind the " " (space) before the closing "", as the command created here
// is prepended to the one down below.
if (motorCanSetSpeed != 0) {
snprintf(command, sizeof(command), "Q%2.2d04=%lf ", axisNo_,
motorVelocity);
writeOffset = strlen(command);
asynPrint(pC_->pasynUserSelf, ASYN_TRACE_FLOW,
"%s => line %d:\nSetting speed of axis %d to %lf.\n",
__PRETTY_FUNCTION__, __LINE__, axisNo_, motorVelocity);
}
// Perform handshake, Set target position (and speed, if allowed) and start
// the move command
if (relative) {
snprintf(command, sizeof(command), "P%2.2d23=0 Q%2.2d02=%lf M%2.2d=2",
axisNo_, axisNo_, motorCoordinatesPosition, axisNo_);
snprintf(&command[writeOffset], sizeof(command) - writeOffset,
"P%2.2d23=0 Q%2.2d02=%lf M%2.2d=2", axisNo_, axisNo_,
motorCoordinatesPosition, axisNo_);
} else {
snprintf(command, sizeof(command), "P%2.2d23=0 Q%2.2d01=%lf M%2.2d=1",
axisNo_, axisNo_, motorCoordinatesPosition, axisNo_);
snprintf(&command[writeOffset], sizeof(command) - writeOffset,
"P%2.2d23=0 Q%2.2d01=%lf M%2.2d=1", axisNo_, axisNo_,
motorCoordinatesPosition, axisNo_);
}
// We don't expect an answer
@ -932,25 +950,18 @@ asynStatus pmacv3Axis::rereadEncoder() {
}
// Abort if the axis is incremental
if (strcmp(encoderType, IncrementalEncoder) == 1) {
asynPrint(pC_->pasynUserSelf, ASYN_TRACE_WARNING,
"%s => line %d:\nTrying to reread absolute encoder of "
"axis %d on controller %s, but it is a relative encoder.\n",
__PRETTY_FUNCTION__, __LINE__, axisNo_, pC_->portName);
pl_status = setStringParam(pC_->motorMessageText_,
"Cannot reread an incremental encoder.");
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorMessageText_",
__PRETTY_FUNCTION__, __LINE__);
}
return asynError;
if (strcmp(encoderType, IncrementalEncoder) == 0) {
asynPrint(pC_->pasynUserSelf, ASYN_TRACE_FLOW,
"%s => line %d:\nEncoder of axis %d is not reread because it "
"is incremental.\n",
__PRETTY_FUNCTION__, __LINE__, axisNo_);
return asynSuccess;
}
// Check if the axis is disabled. If not, inform the user that this
// is necessary
int enabled = 0;
pl_status = pC_->getIntegerParam(axisNo_, pC_->enableMotorRBV_, &enabled);
pl_status = pC_->getIntegerParam(axisNo_, pC_->motorEnableRBV_, &enabled);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "enableMotorRBV_",
__PRETTY_FUNCTION__, __LINE__);
@ -1031,13 +1042,6 @@ asynStatus pmacv3Axis::enable(bool on) {
__PRETTY_FUNCTION__, __LINE__);
}
// Reset the value in the param lib.
pl_status = setIntegerParam(pC_->enableMotor_, 1);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "enableMotor_",
__PRETTY_FUNCTION__, __LINE__);
}
return asynError;
}
@ -1051,6 +1055,14 @@ asynStatus pmacv3Axis::enable(bool on) {
return asynSuccess;
}
// Reread the encoder, if the axis is going to be enabled
if (on != 0) {
rw_status = rereadEncoder();
if (rw_status != asynSuccess) {
return rw_status;
}
}
// Enable / disable the axis if it is not moving
snprintf(command, sizeof(command), "M%2.2d14=%d", axisNo_, on);
asynPrint(pC_->pasynUserSelf, ASYN_TRACE_FLOW,
@ -1066,7 +1078,6 @@ asynStatus pmacv3Axis::enable(bool on) {
return pC_->paramLibAccessFailed(pl_status, "motorMessageText_",
__PRETTY_FUNCTION__, __LINE__);
}
rw_status = pC_->writeRead(axisNo_, command, response, 0);
if (rw_status != asynSuccess) {
return rw_status;
@ -1116,8 +1127,3 @@ asynStatus pmacv3Axis::enable(bool on) {
}
return asynError;
}
asynStatus pmacv3Axis::isEnabled(bool *on) {
*on = (axisStatus_ != -3 && axisStatus_ != -5);
return asynSuccess;
}

View File

@ -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.
@ -115,7 +106,6 @@ class pmacv3Axis : public sinqAxis {
protected:
pmacv3Controller *pC_;
asynStatus readConfig();
bool initial_poll_;
bool waitForHandshake_;
time_t timeAtHandshake_;

View File

@ -1,9 +1,7 @@
#include "pmacv3Controller.h"
#include "asynMotorController.h"
#include "asynOctetSyncIO.h"
#include "pmacv3Axis.h"
#include <cstring>
#include <epicsExport.h>
#include <errlog.h>
#include <iocsh.h>
@ -13,16 +11,19 @@
#include <unistd.h>
/**
* @brief Copy src into dst and replace all carriage returns with spaces
* @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) {
strcpy(dst, src);
for (size_t i = 0; i < strlen(dst); i++) {
if (dst[i] == '\r') {
dst[i] = '_';
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];
}
}
}
@ -47,13 +48,10 @@ pmacv3Controller::pmacv3Controller(const char *portName,
portName, ipPortConfigName, numAxes, movingPollPeriod, idlePollPeriod,
/*
The following parameter library entries are added in this driver:
- ENCODER_TYPE
- REREAD_ENCODER_POSITION
- READ_CONFIG
- MOTOR_VELOCITY_FROM_DRIVER
- MOTOR_ACCEL_FROM_DRIVER
*/
5)
NUM_PMACV3_DRIVER_PARAMS)
{
@ -82,15 +80,6 @@ pmacv3Controller::pmacv3Controller(const char *portName,
// =========================================================================
// 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) {
@ -110,26 +99,6 @@ pmacv3Controller::pmacv3Controller(const char *portName,
exit(-1);
}
status = createParam("MOTOR_VELOCITY_FROM_DRIVER", asynParamFloat64,
&motorVelocityRBV_);
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("MOTOR_ACCEL_FROM_DRIVER", asynParamFloat64,
&motorAccelRBV_);
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
@ -324,15 +293,14 @@ asynStatus pmacv3Controller::writeRead(int axisNo, const char *command,
// Second check: If this fails, give up and propagate the error.
if (numExpectedResponses != numReceivedResponses) {
adjustResponseForPrint(modResponse, response);
adjustResponseForPrint(modResponse, response, MAXBUF_);
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nUnexpected response %s (_ are "
"carriage returns) for command %s\n",
"%s => line %d:\nUnexpected response '%s' (carriage "
"returns are replaced with spaces) for command %s\n",
__PRETTY_FUNCTION__, __LINE__, modResponse, command);
snprintf(drvMessageText, sizeof(drvMessageText),
"Received unexpected response %s (_ are "
"carriage returns) for command %s. "
"Received unexpected response '%s' (carriage returns "
"are replaced with spaces) for command %s. "
"Please call the support",
modResponse, command);
pl_status = setStringParam(motorMessageText_, drvMessageText);
@ -374,29 +342,6 @@ asynStatus pmacv3Controller::writeRead(int axisNo, const char *command,
}
}
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) {
@ -406,21 +351,43 @@ asynStatus pmacv3Controller::writeRead(int axisNo, const char *command,
__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);
}
asynPrint(lowLevelPortUser_, ASYN_TRACE_ERROR,
"%s => line %d:\nCommunication failed for command %s (%s)\n",
__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
pl_status =
getIntegerParam(axisNo, motorStatusProblem_, &motorStatusProblem);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusCommsError_",
return paramLibAccessFailed(pl_status, "motorStatusProblem_",
__PRETTY_FUNCTION__, __LINE__);
}
if (motorStatusProblem == 0) {
pl_status = axis->setStringParam(motorMessageText_, drvMessageText);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorMessageText_",
__PRETTY_FUNCTION__, __LINE__);
}
pl_status = axis->setIntegerParam(motorStatusProblem_, 1);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusProblem",
__PRETTY_FUNCTION__, __LINE__);
}
pl_status = axis->setIntegerParam(motorStatusProblem_, 1);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusCommsError_",
__PRETTY_FUNCTION__, __LINE__);
}
}
}
return asynSuccess;
return status;
}
asynStatus pmacv3Controller::writeInt32(asynUser *pasynUser, epicsInt32 value) {
@ -438,17 +405,27 @@ asynStatus pmacv3Controller::writeInt32(asynUser *pasynUser, epicsInt32 value) {
if (function == rereadEncoderPosition_) {
return axis->rereadEncoder();
} else if (function == readConfig_) {
return axis->readConfig();
return axis->atFirstPoll();
} 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 pmacv3Controller::errMsgCouldNotParseResponse(
const char *command, const char *response, int axisNo,
const char *functionName, int lineNumber) {
char modifiedResponse[MAXBUF_] = {0};
adjustResponseForPrint(modifiedResponse, response);
adjustResponseForPrint(modifiedResponse, response, MAXBUF_);
return sinqController::errMsgCouldNotParseResponse(
command, modifiedResponse, axisNo, functionName, lineNumber);
}
@ -464,7 +441,7 @@ C wrapper for the controller constructor. Please refer to the pmacv3Controller
constructor documentation.
*/
asynStatus pmacv3CreateController(const char *portName,
const char *lowLevelPortName, int numAxes,
const char *ipPortConfigName, int numAxes,
double movingPollPeriod,
double idlePollPeriod, double comTimeout) {
/*
@ -479,7 +456,7 @@ asynStatus pmacv3CreateController(const char *portName,
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-variable"
pmacv3Controller *pController =
new pmacv3Controller(portName, lowLevelPortName, numAxes,
new pmacv3Controller(portName, ipPortConfigName, numAxes,
movingPollPeriod, idlePollPeriod, comTimeout);
return asynSuccess;
@ -566,9 +543,9 @@ types and then providing "factory" functions
(configCreateControllerCallFunc). These factory functions are used to
register the constructors during compilation.
*/
static const iocshArg CreateControllerArg0 = {"Controller port name",
static const iocshArg CreateControllerArg0 = {"Controller name (e.g. mcu1)",
iocshArgString};
static const iocshArg CreateControllerArg1 = {"Low level port name",
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)",
@ -591,7 +568,8 @@ static void configPmacV3CreateControllerCallFunc(const iocshArgBuf *args) {
Same procedure as for the CreateController function, but for the axis
itself.
*/
static const iocshArg CreateAxisArg0 = {"Controller port name", iocshArgString};
static const iocshArg CreateAxisArg0 = {"Controller name (e.g. mcu1)",
iocshArgString};
static const iocshArg CreateAxisArg1 = {"Axis number", iocshArgInt};
static const iocshArg *const CreateAxisArgs[] = {&CreateAxisArg0,
&CreateAxisArg1};

View File

@ -12,9 +12,6 @@
#include "sinqAxis.h"
#include "sinqController.h"
#define IncrementalEncoder "Incremental encoder"
#define AbsoluteEncoder "Absolute encoder"
class pmacv3Controller : public sinqController {
public:
@ -51,10 +48,9 @@ class pmacv3Controller : public sinqController {
pmacv3Axis *getAxis(int axisNo);
/**
* @brief Overloaded function of asynMotorController
* @brief Overloaded function of sinqController
*
* The function is overloaded to allow enabling / disabling the motor and
* rereading the encoder.
* The function is overloaded to allow rereading the encoder and config.
*
* @param pasynUser Specify the axis via the asynUser
* @param value New value
@ -128,18 +124,13 @@ class pmacv3Controller : public sinqController {
double comTimeout_;
// Indices of additional PVs
#define FIRST_PMACV3_PARAM rereadEncoderPosition_
int rereadEncoderPosition_;
int readConfig_;
int encoderType_;
/*
Same strategy as with the limits in sinqController -> Use additional PVs to
write speed and acceleration from the driver to the record.
*/
int motorVelocityRBV_;
int motorAccelRBV_;
#define LAST_PMACV3_PARAM readConfig_
friend class pmacv3Axis;
};
#define NUM_PMACV3_DRIVER_PARAMS (&LAST_PMACV3_PARAM - &FIRST_PMACV3_PARAM + 1)
#endif /* pmacv3Controller_H */

View File

@ -0,0 +1,200 @@
#! 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 = int(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>
<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.
""")