8 Commits

Author SHA1 Message Date
9cc9b1d144 Added explanation why the return status of doPoll is not used in the
enable function.
2025-05-13 14:44:49 +02:00
2e42cbc6da Fixed some inaccuracies in the description 2025-05-12 17:25:30 +02:00
c5f5cf3065 Specified new version for sinqMotor 2025-05-12 12:04:09 +02:00
468ca46010 Static linking of sinqMotor into masterMacs 2025-05-12 09:04:26 +02:00
cd57409f3c Added motorConnected logic 2025-04-25 15:58:03 +02:00
21ffcba8be Back to dev version 2025-04-22 15:07:45 +02:00
3bfc2414b6 Fixed sinqMotor version for tagging 2025-04-22 15:05:16 +02:00
60053244a1 Fixed moving after enable bug
When a MasterMacs motor is powered for the first time, it does not have
a target set. Therefore, the targetReached bit is 0, which the driver
used to interpret as "moving". This is solved now by an additional flag
which checks if the motor did a handshake.

Additionally, the communication module was simplified and new utility
scripts were added. It is now made sure that the communication timeout
for enabling and sending move commands is now at least equal to
PowerCycleTimeout defined in src/masterMacsAxis.cpp.
2025-04-17 16:50:42 +02:00
9 changed files with 449 additions and 198 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "sinqMotor"]
path = sinqMotor
url = https://gitea.psi.ch/lin-epics-modules/sinqMotor

View File

@ -12,18 +12,23 @@ REQUIRED+=sinqMotor
# Specify the version of asynMotor we want to build against
motorBase_VERSION=7.2.2
# Specify the version of sinqMotor we want to build against
sinqMotor_VERSION=0.12.0
# These headers allow to depend on this library for derived drivers.
HEADERS += src/masterMacsAxis.h
HEADERS += src/masterMacsController.h
# Source files to build
SOURCES += sinqMotor/src/msgPrintControl.cpp
SOURCES += sinqMotor/src/sinqAxis.cpp
SOURCES += sinqMotor/src/sinqController.cpp
SOURCES += src/masterMacsAxis.cpp
SOURCES += src/masterMacsController.cpp
# Store the record files
TEMPLATES += sinqMotor/db/asynRecord.db
TEMPLATES += sinqMotor/db/sinqMotor.db
# This file registers the motor-specific functions in the IOC shell.
DBDS += sinqMotor/src/sinqMotor.dbd
DBDS += src/masterMacs.dbd
USR_CFLAGS += -Wall -Wextra -Weffc++ -Wunused-result -Wpedantic -Wextra -Werror

View File

@ -14,6 +14,8 @@ This driver is a standard sinqMotor-derived driver and does not need any specifi
The folder "utils" contains utility scripts for working with masterMacs motor controllers:
- decodeStatus.py: Take the return message of a R10 (read status) command and print it in human-readable form.
- decodeError.py: Take the return message of a R11 (read error) command and print it in human-readable form.
- writeRead.py: Send messages to the controller and receive answers.
## Developer guide
@ -53,11 +55,9 @@ setMaxSubsequentTimeouts("$(NAME)", 20);
setThresholdComTimeout("$(NAME)", 100, 1);
# Parametrize the EPICS record database with the substitution file named after the MCU.
epicsEnvSet("SINQDBPATH","$(sinqMotor_DB)/sinqMotor.db")
epicsEnvSet("SINQDBPATH","$(masterMacs_DB)/sinqMotor.db")
dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
epicsEnvSet("SINQDBPATH","$(masterMacs_DB)/masterMacs.db")
dbLoadTemplate("$(TOP)/$(NAME).substitutions", "INSTR=$(INSTR)$(NAME):,CONTROLLER=$(NAME)")
dbLoadRecords("$(sinqMotor_DB)/asynRecord.db","P=$(INSTR)$(NAME),PORT=$(ASYN_PORT)")
dbLoadRecords("$(masterMacs_DB)/asynRecord.db","P=$(INSTR)$(NAME),PORT=$(ASYN_PORT)")
```
### Versioning
@ -66,4 +66,4 @@ Please see the documentation for the module sinqMotor: https://git.psi.ch/sinq-e
### How to build it
Please see the documentation for the module sinqMotor: https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.
This driver can be compiled and installed by running `make install` from the same directory where the Makefile is located. However, since it uses the git submodule sinqMotor, make sure that the correct version of the submodule repository is checked out AND the change is commited (`git status` shows no non-committed changes). Please see the section "Usage as static dependency" in https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md for more details.

1
sinqMotor Submodule

Submodule sinqMotor added at 5689402375

View File

@ -11,6 +11,14 @@
#include <string.h>
#include <unistd.h>
/*
A special communication timeout is used in the following two cases:
1) Enable command
2) First move command after enabling the motor
This is due to MasterMACS running a powerup cycle, which can delay the answer.
*/
#define PowerCycleTimeout 10.0 // Value in seconds
/*
Contains all instances of turboPmacAxis which have been created and is used in
the initialization hook function.
@ -52,9 +60,9 @@ void appendErrorMessage(char *fullMessage, size_t capacityFullMessage,
// fullMessage suffices. We need capacity for one additional character
// because of the linebreak.
if (lenFullMessage + lenToBeAppended + 1 < capacityFullMessage) {
// Append the linebreak and readd the null terminator behind it
// fullMessage[lenFullMessage] = '\n';
// fullMessage[lenFullMessage + 1] = '\0';
// Append the linebreak and set the null terminator behind it
fullMessage[lenFullMessage] = '\n';
fullMessage[lenFullMessage + 1] = '\0';
// We check before that the capacity of fullMessage is sufficient
strcat(fullMessage, toBeAppended);
@ -79,7 +87,7 @@ masterMacsAxis::masterMacsAxis(masterMacsController *pC, int axisNo)
stop completely, since this is a configuration error.
*/
if (axisNo >= pC->numAxes()) {
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:: FATAL ERROR: "
"Axis index %d must be smaller than the total number of axes "
"%d. Call the support.",
@ -106,12 +114,13 @@ masterMacsAxis::masterMacsAxis(masterMacsController *pC, int axisNo)
// Flag for handshake waiting
waitForHandshake_ = false;
timeAtHandshake_ = 0;
targetReachedUninitialized_ = true;
// masterMacs motors can always be disabled
status = pC_->setIntegerParam(axisNo_, pC_->motorCanDisable(), 1);
if (status != asynSuccess) {
asynPrint(
pC_->asynUserSelf(), ASYN_TRACE_ERROR,
pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nFATAL ERROR "
"(setting a parameter value failed with %s)\n. Terminating IOC",
pC_->portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
@ -123,7 +132,7 @@ masterMacsAxis::masterMacsAxis(masterMacsController *pC, int axisNo)
status = pC_->setIntegerParam(axisNo_, pC_->motorStatusMoving(), false);
if (status != asynSuccess) {
asynPrint(
pC_->asynUserSelf(), ASYN_TRACE_ERROR,
pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nFATAL ERROR "
"(setting a parameter value failed with %s)\n. Terminating IOC",
pC_->portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
@ -148,7 +157,7 @@ asynStatus masterMacsAxis::init() {
// Local variable declaration
asynStatus pl_status = asynSuccess;
char response[pC_->MAXBUF_];
char response[pC_->MAXBUF_] = {0};
int nvals = 0;
double motorRecResolution = 0.0;
double motorPosition = 0.0;
@ -167,7 +176,7 @@ asynStatus masterMacsAxis::init() {
&motorRecResolution);
if (pl_status == asynParamUndefined) {
if (now + maxInitTime < time(NULL)) {
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d\nInitializing the parameter library failed.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__,
@ -259,7 +268,7 @@ asynStatus masterMacsAxis::init() {
// If we can't communicate with the parameter library, it doesn't
// make sense to try and upstream this to the user -> Just log the
// error
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d:\ncallParamCallbacks failed with %s.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__,
@ -281,7 +290,7 @@ asynStatus masterMacsAxis::doPoll(bool *moving) {
// Status of parameter library operations
asynStatus pl_status = asynSuccess;
char response[pC_->MAXBUF_];
char response[pC_->MAXBUF_] = {0};
int nvals = 0;
int direction = 0;
@ -306,8 +315,8 @@ asynStatus masterMacsAxis::doPoll(bool *moving) {
if (pC_->getMsgPrintControl().shouldBePrinted(
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__, timedOut,
pC_->asynUserSelf())) {
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
pC_->pasynUser())) {
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d\nAsked for a "
"handshake at %ld s and didn't get a positive reply yet "
"(current time is %ld s).\n",
@ -341,6 +350,7 @@ asynStatus masterMacsAxis::doPoll(bool *moving) {
// Handshake has been performed successfully -> Continue with the
// poll
waitForHandshake_ = false;
targetReachedUninitialized_ = false;
} else {
// Still waiting for the handshake - try again in the next busy
// poll. This is already part of the movement procedure.
@ -385,12 +395,18 @@ asynStatus masterMacsAxis::doPoll(bool *moving) {
return rw_status;
}
// If the motor is switched off or if it reached its target, it is not
// moving.
if (targetReached() || !switchedOn()) {
if (targetReachedUninitialized_) {
*moving = false;
} else {
*moving = true;
if (targetReached() || !switchedOn()) {
*moving = false;
} else {
*moving = true;
}
}
if (targetReached()) {
targetReachedUninitialized_ = false;
}
// Read the current position
@ -420,9 +436,9 @@ asynStatus masterMacsAxis::doPoll(bool *moving) {
since this information is not reliable.
*/
if (communicationError()) {
if (pC_->getMsgPrintControl().shouldBePrinted(
keyError, true, pC_->asynUserSelf())) {
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
if (pC_->getMsgPrintControl().shouldBePrinted(keyError, true,
pC_->pasynUser())) {
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d\nCommunication error.%s\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__,
@ -579,8 +595,8 @@ asynStatus masterMacsAxis::doPoll(bool *moving) {
if (strlen(shellMessage) > 0) {
if (pC_->getMsgPrintControl().shouldBePrinted(
keyError, true, pC_->asynUserSelf())) {
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
keyError, true, pC_->pasynUser())) {
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d\n%s%s\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__,
@ -597,7 +613,7 @@ asynStatus masterMacsAxis::doPoll(bool *moving) {
}
}
} else {
pC_->getMsgPrintControl().resetCount(keyError, pC_->asynUserSelf());
pC_->getMsgPrintControl().resetCount(keyError, pC_->pasynUser());
}
// Read out the limits, if the motor is not moving
@ -750,7 +766,7 @@ asynStatus masterMacsAxis::doMove(double position, int relative,
}
if (enabled == 0) {
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nAxis is "
"disabled.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__);
@ -762,7 +778,7 @@ asynStatus masterMacsAxis::doMove(double position, int relative,
motorVelocity = maxVelocity * motorRecResolution;
asynPrint(
pC_->asynUserSelf(), ASYN_TRACE_FLOW,
pC_->pasynUser(), ASYN_TRACE_FLOW,
"Controller \"%s\", axis %d => %s, line %d:\nMove to position %lf.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__, position);
@ -805,7 +821,7 @@ asynStatus masterMacsAxis::doMove(double position, int relative,
return rw_status;
}
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_FLOW,
asynPrint(pC_->pasynUser(), ASYN_TRACE_FLOW,
"Controller \"%s\", axis %d => %s, line %d:\nSetting speed "
"to %lf.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__,
@ -825,11 +841,17 @@ asynStatus masterMacsAxis::doMove(double position, int relative,
return rw_status;
}
// If the motor has just been enabled, use Enable
double timeout = pC_->comTimeout();
if (targetReachedUninitialized_ && timeout < PowerCycleTimeout) {
timeout = PowerCycleTimeout;
}
// Start the move
if (relative) {
rw_status = pC_->write(axisNo_, 00, "2");
rw_status = pC_->write(axisNo_, 00, "2", timeout);
} else {
rw_status = pC_->write(axisNo_, 00, "1");
rw_status = pC_->write(axisNo_, 00, "1", timeout);
}
if (rw_status != asynSuccess) {
pl_status = setIntegerParam(pC_->motorStatusProblem(), true);
@ -897,6 +919,16 @@ asynStatus masterMacsAxis::doReset() {
}
}
rw_status = pC_->write(axisNo_, 85, "");
if (rw_status != asynSuccess) {
pl_status = setIntegerParam(pC_->motorStatusProblem(), true);
if (pl_status != asynSuccess) {
return pC_->paramLibAccessFailed(pl_status, "motorStatusProblem_",
axisNo_, __PRETTY_FUNCTION__,
__LINE__);
}
}
return rw_status;
}
@ -912,7 +944,7 @@ asynStatus masterMacsAxis::doHome(double min_velocity, double max_velocity,
// Status of parameter library operations
asynStatus pl_status = asynSuccess;
char response[pC_->MAXBUF_];
char response[pC_->MAXBUF_] = {0};
// =========================================================================
@ -964,7 +996,8 @@ asynStatus masterMacsAxis::readEncoderType() {
// Status of parameter library operations
asynStatus pl_status = asynSuccess;
char command[pC_->MAXBUF_], response[pC_->MAXBUF_];
char command[pC_->MAXBUF_] = {0};
char response[pC_->MAXBUF_] = {0};
int nvals = 0;
int encoder_id = 0;
@ -1012,10 +1045,22 @@ asynStatus masterMacsAxis::enable(bool on) {
// Status of parameter library operations
asynStatus pl_status = asynSuccess;
bool moving = false;
// =========================================================================
/*
When the motor is changing its enable state, its targetReached bit is set to
0. In order to prevent the poll method in interpreting the motor state as
"moving", this flag is used. It is reset in the handshake.
*/
targetReachedUninitialized_ = true;
/*
Continue regardless of the status returned by the poll; we just want to
find out whether the motor is currently moving or not. If the poll
function fails before it can determine that, it is assumed that the motor
is not moving.
*/
bool moving = false;
doPoll(&moving);
// If the axis is currently moving, it cannot be disabled. Ignore the
@ -1023,7 +1068,7 @@ asynStatus masterMacsAxis::enable(bool on) {
// axis instead of "moving", since status -6 is also moving, but the
// motor can actually be disabled in this state!
if (moving) {
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nAxis is not "
"idle and can therefore not be disabled.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__);
@ -1045,7 +1090,7 @@ asynStatus masterMacsAxis::enable(bool on) {
if ((readyToBeSwitchedOn() && switchedOn()) == on) {
asynPrint(
pC_->asynUserSelf(), ASYN_TRACE_WARNING,
pC_->pasynUser(), ASYN_TRACE_WARNING,
"Controller \"%s\", axis %d => %s, line %d:\nAxis is already %s.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__,
on ? "enabled" : "disabled");
@ -1054,7 +1099,7 @@ asynStatus masterMacsAxis::enable(bool on) {
// Enable / disable the axis if it is not moving
snprintf(value, sizeof(value), "%d", on);
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_FLOW,
asynPrint(pC_->pasynUser(), ASYN_TRACE_FLOW,
"Controller \"%s\", axis %d => %s, line %d:\n%s axis.\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__,
on ? "Enable" : "Disable");
@ -1072,7 +1117,12 @@ asynStatus masterMacsAxis::enable(bool on) {
// The answer to the enable command on MasterMACS might take some time,
// hence we wait for a custom timespan in seconds instead of
// pC_->comTimeout_
rw_status = pC_->write(axisNo_, 04, value, 1.0);
double timeout = pC_->comTimeout();
if (targetReachedUninitialized_ && timeout < PowerCycleTimeout) {
timeout = PowerCycleTimeout;
}
rw_status = pC_->write(axisNo_, 04, value, timeout);
if (rw_status != asynSuccess) {
return rw_status;
}
@ -1099,7 +1149,7 @@ asynStatus masterMacsAxis::enable(bool on) {
// Failed to change axis status within timeout_enable_disable => Send a
// corresponding message
asynPrint(pC_->asynUserSelf(), ASYN_TRACE_ERROR,
asynPrint(pC_->pasynUser(), ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nFailed to %s axis "
"within %d seconds\n",
pC_->portName, axisNo_, __PRETTY_FUNCTION__, __LINE__,
@ -1132,7 +1182,7 @@ std::bitset<16> toBitset(float val) {
}
asynStatus masterMacsAxis::readAxisStatus() {
char response[pC_->MAXBUF_];
char response[pC_->MAXBUF_] = {0};
// =========================================================================
@ -1153,7 +1203,7 @@ asynStatus masterMacsAxis::readAxisStatus() {
}
asynStatus masterMacsAxis::readAxisError() {
char response[pC_->MAXBUF_];
char response[pC_->MAXBUF_] = {0};
// =========================================================================

View File

@ -111,6 +111,8 @@ class masterMacsAxis : public sinqAxis {
bool waitForHandshake_;
time_t timeAtHandshake_;
bool targetReachedUninitialized_;
asynStatus readConfig();
/*

View File

@ -70,7 +70,7 @@ masterMacsController::masterMacsController(const char *portName,
the message length is encoded in the message header.
*/
const char *message_from_device = "\x0D"; // Hex-code for CR
status = pasynOctetSyncIO->setInputEos(ipPortAsynOctetSyncIO_,
status = pasynOctetSyncIO->setInputEos(pasynOctetSyncIOipPort(),
message_from_device,
strlen(message_from_device));
if (status != asynSuccess) {
@ -79,7 +79,7 @@ masterMacsController::masterMacsController(const char *portName,
"input EOS failed with %s).\nTerminating IOC",
portName, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
pasynOctetSyncIO->disconnect(ipPortAsynOctetSyncIO_);
pasynOctetSyncIO->disconnect(pasynOctetSyncIOipPort());
exit(-1);
}
@ -90,7 +90,7 @@ masterMacsController::masterMacsController(const char *portName,
"ParamLib callbacks failed with %s).\nTerminating IOC",
portName, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status));
pasynOctetSyncIO->disconnect(ipPortAsynOctetSyncIO_);
pasynOctetSyncIO->disconnect(pasynOctetSyncIOipPort());
exit(-1);
}
}
@ -133,8 +133,6 @@ asynStatus masterMacsController::writeRead(int axisNo, int tcpCmd,
asynStatus pl_status = asynSuccess;
char fullCommand[MAXBUF_] = {0};
char fullResponse[MAXBUF_] = {0};
char printableCommand[MAXBUF_] = {0};
char printableResponse[MAXBUF_] = {0};
char drvMessageText[MAXBUF_] = {0};
int motorStatusProblem = 0;
@ -158,7 +156,7 @@ asynStatus masterMacsController::writeRead(int axisNo, int tcpCmd,
// =========================================================================
// Check if a timeout has been given
// Check if a custom timeout has been given
if (comTimeout < 0.0) {
comTimeout = comTimeout_;
}
@ -169,6 +167,7 @@ asynStatus masterMacsController::writeRead(int axisNo, int tcpCmd,
return asynError;
}
// Build the full command depending on the inputs to this function
if (isRead) {
snprintf(fullCommand, MAXBUF_ - 1, "%dR%02d\x0D", axisNo, tcpCmd);
} else {
@ -183,95 +182,39 @@ asynStatus masterMacsController::writeRead(int axisNo, int tcpCmd,
// Calculate the command length
const size_t fullCommandLength = strlen(fullCommand);
adjustForPrint(printableCommand, fullCommand, MAXBUF_);
// Flush the IOC-side socket, then write the command and wait for the
// response.
status = pasynOctetSyncIO->writeRead(
pasynOctetSyncIOipPort(), fullCommand, fullCommandLength, fullResponse,
MAXBUF_, comTimeout, &nbytesOut, &nbytesIn, &eomReason);
// Send out the command
status = pasynOctetSyncIO->write(ipPortAsynOctetSyncIO_, fullCommand,
fullCommandLength, comTimeout, &nbytesOut);
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));
}
msgPrintControlKey writeKey =
// If a communication error occured, print this message to the
msgPrintControlKey comKey =
msgPrintControlKey(portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
if (status == asynSuccess) {
msgPrintControl_.resetCount(writeKey, pasynUserSelf);
// Try to read the answer repeatedly
int maxTrials = 2;
for (int i = 0; i < maxTrials; i++) {
/*
A typical response of the MasterMacs controller looks like this:
(.. TCP Header ...) 31 20 52 20 31 31 3d 35 31 32 2e 30 30 30 30 06
0d 00 00 00 00 00 00 00 00 00 00 00 00 00
The message terminator is the carriage return (0d), which is
specified in the controller constructor as the end-of-string (EOS)
character. However, we also need to remove the buffer zeros at the
end, because they will otherwise confuse the
pasynOctetSyncIO->read() during the next call. The
pasynOctetSyncIO->flush() method does exactly that: it takes all
bytes it can find in the socket and throws them away. We don't check
the return value of flush(), because it is always asynSuccess (see
https://www.slac.stanford.edu/grp/lcls/controls/global/doc/epics-modules/R3-14-12/asyn/asyn-R4-18-lcls2/asyn/interfaces/asynOctetBase.c)
*/
status = pasynOctetSyncIO->read(ipPortAsynOctetSyncIO_,
fullResponse, MAXBUF_, comTimeout,
&nbytesIn, &eomReason);
pasynOctetSyncIO->flush(ipPortAsynOctetSyncIO_);
if (status == asynSuccess) {
status = parseResponse(fullCommand, fullResponse,
drvMessageText, &valueStart, &valueStop,
axisNo, tcpCmd, isRead);
if (status == asynSuccess) {
// Received the correct message
break;
}
} else {
if (status != asynTimeout) {
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));
break;
}
}
if (i + 1 == maxTrials && status == asynError) {
asynPrint(
this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nFailed "
"%d times to get the correct response. Aborting read.\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__, maxTrials);
}
if (status != asynSuccess) {
if (msgPrintControl_.shouldBePrinted(comKey, true, pasynUserSelf)) {
char printableCommand[MAXBUF_] = {0};
adjustForPrint(printableCommand, fullCommand, MAXBUF_);
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nError "
"%s while sending command %s to the controller\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status), printableCommand);
}
} else {
if (msgPrintControl_.shouldBePrinted(writeKey, true, pasynUserSelf)) {
asynPrint(
this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nError %s while "
"writing to the controller.%s\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
stringifyAsynStatus(status), msgPrintControl_.getSuffix());
}
msgPrintControl_.resetCount(comKey, pasynUserSelf);
}
// Create custom error messages for different failure modes
switch (status) {
case asynSuccess:
// We did get a response, but does it make sense and is it designated as
// OK from the controller? This is checked here.
status = parseResponse(fullCommand, fullResponse, drvMessageText,
&valueStart, &valueStop, axisNo, tcpCmd, isRead);
if (isRead) {
// Read out the important information from the response
if (status == asynSuccess && isRead) {
/*
If a property has been read, we need just the part between the
"=" (0x3D) and the [ACK] (0x06). Therefore, we remove all
@ -282,33 +225,39 @@ asynStatus masterMacsController::writeRead(int axisNo, int tcpCmd,
response[i] = fullResponse[i + valueStart];
}
}
break;
case asynTimeout:
snprintf(drvMessageText, sizeof(drvMessageText),
"connection timeout for axis %d", axisNo);
"Connection timeout. Please call the support.");
break;
case asynDisconnected:
snprintf(drvMessageText, sizeof(drvMessageText),
"axis is not connected");
"Axis is not connected.");
break;
case asynDisabled:
snprintf(drvMessageText, sizeof(drvMessageText), "axis is disabled");
snprintf(drvMessageText, sizeof(drvMessageText), "Axis is disabled.");
break;
case asynError:
// Do nothing - error message drvMessageText has already been set.
break;
default:
snprintf(drvMessageText, sizeof(drvMessageText),
"Communication failed (%s)", stringifyAsynStatus(status));
"Communication failed (%s). Please call the support.",
stringifyAsynStatus(status));
break;
}
// Log the overall status (communication successfull or not)
if (status == asynSuccess) {
adjustForPrint(printableResponse, fullResponse, MAXBUF_);
pl_status = axis->setIntegerParam(this->motorStatusCommsError_, 0);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusCommsError_",
axisNo, __PRETTY_FUNCTION__, __LINE__);
}
} else if (status == asynDisconnected) {
// Do nothing
} else {
// Set the error status bits only if the axis is not disconnected
// Check if the axis already is in an error communication mode. If
// it is not, upstream the error. This is done to avoid "flooding"
@ -361,14 +310,22 @@ asynStatus masterMacsController::parseResponse(
const char *fullCommand, const char *fullResponse, char *drvMessageText,
int *valueStart, int *valueStop, int axisNo, int tcpCmd, bool isRead) {
bool responseValid = false;
int responseStart = 0;
asynStatus status = asynSuccess;
int prevConnected = 0;
char printableCommand[MAXBUF_] = {0};
char printableResponse[MAXBUF_] = {0};
msgPrintControlKey parseKey =
msgPrintControlKey(portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
// Was the motor previously connected?
status = getIntegerParam(axisNo, motorConnected(), &prevConnected);
if (status != asynSuccess) {
return paramLibAccessFailed(status, "motorConnected", axisNo,
__PRETTY_FUNCTION__, __LINE__);
}
// We don't use strlen here since the C string terminator 0x00
// occurs in the middle of the char array.
for (uint32_t i = 0; i < MAXBUF_; i++) {
@ -377,22 +334,120 @@ asynStatus masterMacsController::parseResponse(
} else if (fullResponse[i] == '=') {
*valueStart = i + 1;
} else if (fullResponse[i] == '\x06') {
// ACK
*valueStop = i;
responseValid = true;
break;
} else if (fullResponse[i] == '\x15') {
// NAK
snprintf(drvMessageText, MAXBUF_, "Communication failed.");
if (msgPrintControl_.shouldBePrinted(parseKey, true,
pasynUserSelf)) {
// Motor wasn't connected before -> Update the paramLib entry and PV
// to show it is now connected.
if (prevConnected == 0) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d:\nCommunication failed.%s\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
msgPrintControl_.getSuffix());
"%d:\nAxis connection status has changed to "
"connected.\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
masterMacsAxis *axis = getMasterMacsAxis(axisNo);
if (axis == nullptr) {
return asynError;
}
status = axis->setIntegerParam(motorConnected(), 1);
if (status != asynSuccess) {
return paramLibAccessFailed(status, "motorConnected",
axisNo, __PRETTY_FUNCTION__,
__LINE__);
}
status = callParamCallbacks();
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d:\nCould not update parameter library\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
return status;
}
}
break;
msgPrintControl_.resetCount(parseKey, pasynUserSelf);
// Check if the response matches the expectations. Each response
// contains the string "axisNo R tcpCmd" (including the spaces)
char expectedResponseSubstring[MAXBUF_] = {0};
// The response does not contain a leading 0 if tcpCmd only has
// a single digit!
if (isRead) {
snprintf(expectedResponseSubstring, MAXBUF_ - 4, "%d R %d",
axisNo, tcpCmd);
} else {
snprintf(expectedResponseSubstring, MAXBUF_ - 4, "%d S %d",
axisNo, tcpCmd);
}
msgPrintControlKey responseMatchKey = msgPrintControlKey(
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
if (strstr(&fullResponse[responseStart],
expectedResponseSubstring) == NULL) {
adjustForPrint(printableCommand, fullCommand, MAXBUF_);
adjustForPrint(printableResponse, fullResponse, MAXBUF_);
if (msgPrintControl_.shouldBePrinted(parseKey, true,
pasynUserSelf)) {
asynPrint(this->pasynUserSelf, ASYN_TRACEIO_DRIVER,
"Controller \"%s\", axis %d => %s, line "
"%d:\nMismatched "
"response %s to command %s.%s\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
printableResponse, printableCommand,
msgPrintControl_.getSuffix());
}
snprintf(
drvMessageText, MAXBUF_,
"Mismatched response %s to command %s. Please call the "
"support.",
printableResponse, printableCommand);
return asynError;
} else {
msgPrintControl_.resetCount(responseMatchKey, pasynUserSelf);
}
return asynSuccess;
} else if (fullResponse[i] == '\x15') {
/*
NAK
This indicates that the axis is not connected. This is not an error!
*/
snprintf(drvMessageText, MAXBUF_, "Axis not connected.");
// Motor was connected before -> Update the paramLib entry and PV
// to show it is now disconnected.
if (prevConnected == 1) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d:\nAxis connection status has changed to "
"disconnected.\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
masterMacsAxis *axis = getMasterMacsAxis(axisNo);
if (axis == nullptr) {
return asynError;
}
status = axis->setIntegerParam(motorConnected(), 0);
if (status != asynSuccess) {
return paramLibAccessFailed(status, "motorConnected",
axisNo, __PRETTY_FUNCTION__,
__LINE__);
}
status = callParamCallbacks();
if (status != asynSuccess) {
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line "
"%d:\nCould not update parameter library\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
return status;
}
}
return asynDisconnected;
} else if (fullResponse[i] == '\x18') {
// CAN
snprintf(drvMessageText, MAXBUF_,
@ -401,6 +456,7 @@ asynStatus masterMacsController::parseResponse(
if (msgPrintControl_.shouldBePrinted(parseKey, true,
pasynUserSelf)) {
adjustForPrint(printableCommand, fullCommand, MAXBUF_);
asynPrint(
this->pasynUserSelf, ASYN_TRACE_ERROR,
"Controller \"%s\", axis %d => %s, line %d:\nTried to "
@ -408,60 +464,14 @@ asynStatus masterMacsController::parseResponse(
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
printableCommand, msgPrintControl_.getSuffix());
}
responseValid = false;
break;
}
}
if (responseValid) {
msgPrintControl_.resetCount(parseKey, pasynUserSelf);
// Check if the response matches the expectations. Each response
// contains the string "axisNo R tcpCmd" (including the spaces)
char expectedResponseSubstring[MAXBUF_] = {0};
// The response does not contain a leading 0 if tcpCmd only has
// a single digit!
if (isRead) {
snprintf(expectedResponseSubstring, MAXBUF_ - 4, "%d R %d", axisNo,
tcpCmd);
} else {
snprintf(expectedResponseSubstring, MAXBUF_ - 4, "%d S %d", axisNo,
tcpCmd);
}
msgPrintControlKey responseMatchKey =
msgPrintControlKey(portName, axisNo, __PRETTY_FUNCTION__, __LINE__);
if (strstr(&fullResponse[responseStart], expectedResponseSubstring) ==
NULL) {
adjustForPrint(printableCommand, fullCommand, MAXBUF_);
adjustForPrint(printableResponse, fullResponse, MAXBUF_);
if (msgPrintControl_.shouldBePrinted(parseKey, true,
pasynUserSelf)) {
asynPrint(
this->pasynUserSelf, ASYN_TRACEIO_DRIVER,
"Controller \"%s\", axis %d => %s, line %d:\nMismatched "
"response %s to command %s.%s\n",
portName, axisNo, __PRETTY_FUNCTION__, __LINE__,
printableResponse, printableCommand,
msgPrintControl_.getSuffix());
}
snprintf(drvMessageText, MAXBUF_,
"Mismatched response %s to command %s. Please call the "
"support.",
printableResponse, printableCommand);
return asynError;
} else {
msgPrintControl_.resetCount(responseMatchKey, pasynUserSelf);
}
}
return asynSuccess;
return asynError;
}
asynStatus sinqController::readInt32(asynUser *pasynUser, epicsInt32 *value) {
asynStatus masterMacsController::readInt32(asynUser *pasynUser,
epicsInt32 *value) {
// masterMacs can be disabled
if (pasynUser->reason == motorCanDisable_) {
*value = 1;

View File

@ -118,6 +118,15 @@ class masterMacsController : public sinqController {
// responses from it.
static const uint32_t MAXBUF_ = 200;
/**
* @brief Get the communication timeout used in the writeRead command
*
* @return double Timeout in seconds
*/
double comTimeout() { return comTimeout_; }
asynStatus readInt32(asynUser *pasynUser, epicsInt32 *value);
private:
/*
Stores the constructor input comTimeout

171
utils/writeRead.py Normal file
View File

@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
This script allows direct interaction with a MasterMACS-Controller over an ethernet connection.
To read the manual, simply run this script without any arguments.
Stefan Mathis, April 2025
"""
import struct
import socket
import curses
def packMasterMacsCommand(command):
# 0x0D = Carriage return
buf = struct.pack('B',0x0D)
buf = bytes(command,'utf-8') + buf
return bytes(command,'utf-8')
def readMasterMacsReply(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 = packMasterMacsCommand(argv[2])
s.send(buf)
reply = readMasterMacsReply(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 = packMasterMacsCommand(history[ptr])
s.send(buf)
reply = readMasterMacsReply(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.
""")