Skip to content

Better open loop handling; add a simulator - #34

Open
tboegi wants to merge 73 commits into
epics-motor:masterfrom
EuropeanSpallationSource:ess-master
Open

Better open loop handling; add a simulator#34
tboegi wants to merge 73 commits into
epics-motor:masterfrom
EuropeanSpallationSource:ess-master

Conversation

@tboegi

@tboegi tboegi commented May 12, 2026

Copy link
Copy Markdown

Improve the open loop handling:
The step size forward and backward can be configured,
which allows better to keep track of the position when switching open/closed loop
Various other small improvements.

Add a simulator to test this driver

tboegi added 30 commits June 11, 2024 10:34
Update the .ci submodule to the latest release
Make the source code compile without warnings.
- Initialize stepTarget_ inside the constructor, not the .h files
- Comment out not used "static const char *functionName"
- Comment out a not used "asynStatus comStatus_"
- Remove not needed helper variables in C++ constructors
No intended code changes.
Remove all conversion warnings, found by the Windows compiler.
When e.g. a double is assigned to an int, cast it.
Clean up the whithe space: No more TAB or trailing whithespace
Make sure that further whitespace damage is detected in ci:

Changes to be committed:
    modified:   .github/workflows/ci-scripts-build.yml
    new file:   checkws.sh
The "positioner name" was garbage:
strcpy(dst, src) it is.
Fix this, and while there, add a memset() and use strncpy() instead.
Add more variables to be retrieved and printed, range limit and thresholds.
To avoid too much code-duplication, add and use 2 helper functions to
simplify the code:
reportHelperInteger() and reportHelperDouble()
Improve the poll() function with 3 changes:
The setting of motorStatusProblem_ was wrong:
  // This is correct and not changed:
  In MCS2Controller::clearErrors()
    skip:
      setIntegerParam(this->motorStatusProblem_, comStatus ? 1:0);

  In MCS2Axis::poll()
    // This is changed, see below.
    setIntegerParam(pC_->motorStatusProblem_, movementFailed);
    // This is wrong: error is cleared, regardless.
    setIntegerParam(pC_->motorStatusProblem_, 0);

    // And, at the end of poll(), the Problem_ is set to 0,
    // unless there was a communication error.
    skip:
    setIntegerParam(this->motorStatusProblem_, comStatus ? 1:0);

  Clean up the code, and use a single call to set motorStatusProblem_:
    skip:
    setIntegerParam(pC_->motorStatusProblem_, comStatus ? 1:0);

Change even the the handling of movementFailed:
  It is not a problem (of the next motion), but rather an indication
  that the current/last commanded movement failed.
  Use the motorStatusFollowingError_ to indicate that the last movement failed:
    setIntegerParam(pC_->motorStatusFollowingError_,
                    followLimitReached || movementFailed);

Rename Channel State Bits:
  Rename the definition of the "channel state bits" to make more clear,
  where they belong to.
  Match MCS2ProgrammersGuide.pdf, page 71.
  Example: ACTIVELY_MOVING -> CH_STATE_ACTIVELY_MOVING
  Make the a "#define", which is a better choice to match an integer
  anyway.
  Add new definition bits, CH_STATE_IS_PHASED..CH_STATE_BRAKE_ENABLED
  No intended code changes.
Do not set the problem bit, but rather the motorStatusCommsError_ bit,
when the communication fails in poll().

This will set the appropriate alarm on the motorRecord:
COMM_INVALID
Add a printout to hold_time in report().
While there, remove code in report() that is not working:
  REPORTHELPERCHECKERROR(":TUN:THR:THO?", buf.target_hold_threshold);
Make the HOLD paramater configurable:
Add it as an asyn parameter and add a longout record for it.
The default is still -1.
Add a new method, that will be caused when polling starts,
either when the IOC starts, or when the connection is lost and
now re-established.

Rigth now, the HOLD parameter is send to the controller.
More functions may be added later.
Detect network problems better when there is a query to the controller
and an answer is expected, but none is received.
It may happen, the the answer is stuck inside the network,
to be more exact inside the TCP layer, when there is a temporary network
problem.
To avoid that this answer is delivered later and treated as the
response to the next poll() and everyrhing is mixed up,
disconnect the socket (TCP connection).
asyn will the re-establish a new one without having old data floating
around.
Partly revert the last commit:
The TCP/IP stack in the controller is only allowing one TCP connection
at a time. Trying to re-establish a new connection while the old one
is still not "fully closed" does not work.
This means that the "disconnect" from the previous commit may hang
the whold communication, if there is a short glitch in the network.
And the controller needs a power-cycle to get out of this situation.
The new solution will not close the TCP connection, but wait for
the controller to do so.
However, when polling one axis fails, all axes are put into the
disconnected state.
Where the initialPoll() will do the re-establishment.
Add logic to make it possible to supply a user with an
"error message text". From the possible errors and problems,
try to find the most important one and display that one.
Note1: There is no record yet.
Note2: The ordering of priorities may need to change, once we
       get more experience in what the controller reports when.
Support the "message from driver" record found in the ESS-motor:
There is an overall "message text" record, which either displays
the message from the driver (this module), if there is something
important, or the messages from the generic driver.
Add the readback in picometers:
Store it in a new asynParameter
A record will be added to this later
When we have pvxs defined in our worktree, enable it.
Otherwise, when compiling/linking against EPICS base 7,
enable pva.
Split the READBACK asynparamter into 2:
- FREADBACK floating point, double, used for an "ai" record
- IREADBACK integer 64 bit, used for an "int64" record

Add needed code and #ifdefs to be able to compile against
older versions of asyn, which do not have the int64 support.
Improve the printing situation when a timeout occurs:
Normally the printouts are are on ASYN_TRACEIO_DRIVER level and
switched off.
When a timeout occurs, this is serious. Print it on ERROR level.
Add support for a probably unusual configuration:
The positioner has a sensor, but it should not be used.
The aim is to use an external readback to get the position
into motorRecord and use openloop movements in the MCS2.

Retries are handled by the motorRecord:
The fields RDBL, RTRY RMOD and URIP are used.
In the first phase of the project we can compare the external
readback with the sensor and see how good that works.
In the seconds phase the function of the sensor will be lost,
it is going to be damaged by radiation and stops working.

Add a new asyn parameter to be able to "connect" a record to it,
and use a local variable as a copy inside MCS2Axis::move().

This commit does not use the CNEN field from the motorRecord
for 2 reasons:
The default for the driver is still closed loop. The special case
open loop needs to be configured, when needed.
The CNEN field and specially the setClosedLoop() method in the
generic model 3 driver has been used to power on- and off the
motor. Espially the auto-power-on (and off) feature relies
on the fact that setClosedLoop() inside the driver enables the power
and nothing else.
The dilemma of CNEN used for different things has been realized
by the community, and my take away from this is to keep CNEN away
from closed loop.
Add more values in asynReport:

step_freq, step_ampl, diag_clf_max, diag_clf_aver
The current implementation uses velocity (maxVelocity) in move() as the
directly as the frequency.
This has 2 disadvantages:
- The velocity from motorRecord (.VELO) is now longer reflected
  in EGU/second.
- Switching between closed- and open-loop gives a mess:
  A velocity of 1 mm/sec needs e.g a frequency of 300 Hz,
  as observed at one positioner.

To somewhat improve the situation, add an optional factor to calculate the
frequency from the velocity. This can be connected to an ao record.
When the new factor is 0.0, it is ignored, making the code backward
compatible with existing databases.
Add 2 parameters (no records yet) that allow the driver to issue
a "step move" in open loop.
It turns out that all open loop step moves are using steps,
not picometers.

A better handling of open loop movements is wanted:
On the used hardware we can find that running +1000 moves the positioner
2.4 mm, and stepping backwards with -1000 moves -3.4 mm.

That will be addressed in the next commits.
Running the piezo in open-loop means that we use "(full) steps".
A step has a specific size, and the positioner even moves a slightly
different distance forward compared to reverse.
The the achieved step size may even be dependend on other things,
I don't know yet.
Anyway, make it possible to configure the step size into the driver,
so that the move() command achieves a good movement.
Example for records will be added later.
Improve the calculation of the frequency when running open loop
and there is are step sizes defined.
Re-rorder the code to limit the scope of temporary variables.
Remove the parameter vel2clf because that is no longer needed.
Fix a bug: stepcnt is an integer to move the positioner a number of
steps, so create it as asynParamInt32, as it should be.
The open loop logic of move() has 4 different use cases:
1) move absolute, closed loop
2) move relative, closed loop
3) move absolute, open loop
4) move relative, open loop

3) was not working as it should when stepsizef and stepsizer had
been used. Instead of converting the position into steps,
it was forwared "as is"
When retry-modus 3 was used (motorRecord.RMOD =3, in position),
the open loop move() commands are absolute, not relative.
(If this modus makes sense, is a different question).

However, in order to keep track of the commanded position,
update stepTargetPos_nm_ both for relative and absolute moves.
The variable channel_ is not needed:
We can use axisNo_ from asynAxisMotor, which does the same thing.
tboegi added 30 commits May 23, 2025 07:45
Adding a folder smaractExApp without adding a Makefile
makes `make` fail.

Solution:
Specify "smarActApp" instead of "*App" as a wildcard.
Add support for "SensorPowerMode" and "SensorDelay",
":CHAN%d:SENS:MOD" and ":CHAN%d:SENS:DEL".

The power mode has 3 values:
0 == disabled,
1 == enabled,
2 == power save.
When disabled, the controller will report error 260.
Since we read the sensor power mode and react on it
by setting sensorIsDisabled_ to 1 we can ignore this error.

It may happen that the device is configured by a vendor tool
outside EPICS.
To be able to follow what is inside tha controller:
Add the reading of sensor power mode and sensor delay in poll();

Improve report():
 - decode STAT in report()
 - print sensorIsDisabld

An observation:
Reading the position via ':CHAN0:POS?' runs into a timeout when
the sensor is disabled. So don't do that.
Since the numerical range of positions is more than what a 32 bit
value can hold, the controller uses 64 bit (signed) integers.
However, we don't need the full 64 bit range.
For the real existing hardware we can use a double.
So cleanup and remove the SMARACT_ASYN_ASYNPARAMINT64 and all code
around "ireadback" and keep "freadback" as is.
The documentation says that the range of a "step move" is limited:
-100000..-1 or +1..+100000
Moving 0 steps doesn't make sense.
Clean up the handling:
There is no need for "long long PositionType" or steps_to_go_i
Use double instead. Add a range check.
When there is no enabled sensor and a move is done by
commanding n steps try to calculate the theoretical position
according to the direction and step size forward/backward.

Site note: This makes motorPosition jump at the beginning
of the movement. Should be better at the end.

Add some error handling as well.
When there is a powered-on sensor, read ":CHAN%d:POS:TARG?",
both open- and closed- loop.
The controller will tell us where the positioner should be.
Remove the stale definition:
#define MCS2IReadbackString "IREADBACK"
Move "global" variables inside poll() into local blocks.
Rename theoretical, because it is the internal setpoint.
Add a helper variable which is updated AFTER the move:
When running open loop without sensor, the motor position
is calculated according to the step size.
And this value is put into motorPosition before the move.
This may confuse users/3rd party systems.
Update the new PV after the move is done.

Changes to be committed:
    modified:   smarActApp/src/smarActMCS2MotorDriver.cpp
    modified:   smarActApp/src/smarActMCS2MotorDriver.h
Inside the driver we use somthing like

if (sensorPresent_ && !sensorIsDisabled_ && !openLoop_)

This means that a disabled sensor will turn a move() into an open loop
movement.
Make that visible to the user by setting openloop to 1
…eps_

Remove stepTargetPos_nm_ and stepTargetSteps_, all positions should stored
in the asyn parameter motorPosition_ .
Rename steps_to_go_f into steps_to_go (it is NOT forward, but floating point)
Simplify the open loop handling in move()
While debugging an open loop movement with retries (which had the wrong
values for stepsize) it turned out that the bits from PSTATE
should be easier to follow.
In other words: Print them in the IOC log
Unify the text(s) printed in poll() when one ore more bits change:
Printthe all in uppercase
The calculation of the "theoretical motor position" was wrong,
both for realative- and absolute- moves.
Correct it
It seems as if the sensor is working like an incremental encoder:
When it is disabled, it doesn't update.
Solution:
Update the controller, when we run open loop and the sensor is disabled,
with the position that we think we are.
Remove ErrTxt, which is not used, since all information is in MsgTxt
Ever since we have the feature
  info(asyn:READBACK,"1")
  info(asyn:INITIAL_READBACK,"1")

... there is no need to have 2 asyn parameters for the same thing.
Remove the asyn parameter for "postioner type readback".
Map the readback record towards the "PTYP" asyn parameter.
Add a (commented out) asyn:READBACK for the setPositionerType record.

Changes to be committed:
    modified:   smarActApp/Db/MCS2_Extra.db
    modified:   smarActApp/src/smarActMCS2MotorDriver.cpp
    modified:   smarActApp/src/smarActMCS2MotorDriver.h
Fix a small issue when printing chanState in poll()
:he function name was printed twice. Remove even the line number
and shorten the whole thing.
Do no longer declare "int" when parsing responses:
E.g.:

These 2 lines
  mclf = atoi(pC_->inString_);
  asynMotorAxis::setIntegerParam(pC_->mclf_, mclf);
collapse into one line:
  asynMotorAxis::setIntegerParam(pC_->mclf_, atoi(pC_->inString_));
Add a demo file with the new features.

Changes to be committed:
    modified:   smarActApp/Db/Makefile
    new file:   smarActApp/Db/ess-mcs2.template
Add a bunch of CSS Boy files.
They are a copy of
git@gitlab.esss.lu.se:torstenbogershausen/motorsmaract-css-boy.git
tag v1.0.0

(New changes will be done here, in the smarAct repo)

Changes to be committed:
    new file:   smarActExApp/op/CSS-Boy/IOC.opi
    new file:   smarActExApp/op/CSS-Boy/genopis/IOC-BIFROST-SpSl1.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/IOC.end
    new file:   smarActExApp/op/CSS-Boy/genopis/IOC.start
    new file:   smarActExApp/op/CSS-Boy/genopis/Makefile
    new file:   smarActExApp/op/CSS-Boy/genopis/checkws.sh
    new file:   smarActExApp/op/CSS-Boy/genopis/e-cnen-vis.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-cnen.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-errtxt.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-foff-vis.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-foff.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-homf-cal-homr.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-openloop.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-retry.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/e-urip.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/genExpertPiezo.sh
    new file:   smarActExApp/op/CSS-Boy/genopis/genExpertWithAuxBits.py
    new file:   smarActExApp/op/CSS-Boy/genopis/genIOC.sh
    new file:   smarActExApp/op/CSS-Boy/genopis/genMotor.sh
    new file:   smarActExApp/op/CSS-Boy/genopis/m-egu-rbv.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/m-motor.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/m-msgtxt.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/motorx-piezo.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/motorx.end
    new file:   smarActExApp/op/CSS-Boy/genopis/motorx.start
    new file:   smarActExApp/op/CSS-Boy/genopis/shiftopi.py
    new file:   smarActExApp/op/CSS-Boy/genopis/smaractAnalogInput.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/smaractAnalogOutput.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/smaractAxisExpert.end
    new file:   smarActExApp/op/CSS-Boy/genopis/smaractAxisExpert.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/smaractMotorRecordField.mid
    new file:   smarActExApp/op/CSS-Boy/genopis/smaractaxisExpert.start
    new file:   smarActExApp/op/CSS-Boy/genopis/smaractaxisPiezoPSTAT.mid
    new file:   smarActExApp/op/CSS-Boy/motor-1-piezo.opi
    new file:   smarActExApp/op/CSS-Boy/motor-2-piezo.opi
    new file:   smarActExApp/op/CSS-Boy/smaractAxisExpert.opi
Add an(other) directory with IOC example files.
Note that these example does include "open loop"

Changes to be committed:
    new file:   iocs/smarActIOC/iocBoot/ioc-smaract-ymir-handheld/.gitattributes
    new file:   iocs/smarActIOC/iocBoot/ioc-smaract-ymir-handheld/.gitignore
    new file:   iocs/smarActIOC/iocBoot/ioc-smaract-ymir-handheld/run-ioc.sh
    new file:   iocs/smarActIOC/iocBoot/ioc-smaract-ymir-handheld/st-handheld.cmd
    new file:   iocs/smarActIOC/iocBoot/ioc-smaract-ymir-handheld/st-simulator.cmd
Rename the PVs to have the test system more compatible with
the production systems:
$(P)$(M)-something is now $(P)$(M)Something
Adapt the CSS Screens.

Changes to be committed:
    modified:   iocs/smarActIOC/iocBoot/ioc-smaract-ymir-handheld/st-handheld.cmd
    modified:   smarActApp/Db/ess-mcs2.template
    modified:   smarActExApp/op/CSS-Boy/genopis/e-homf-cal-homr.mid
    modified:   smarActExApp/op/CSS-Boy/genopis/e-openloop.mid
    modified:   smarActExApp/op/CSS-Boy/genopis/genExpertPiezo.sh
    modified:   smarActExApp/op/CSS-Boy/genopis/smaractaxisPiezoPSTAT.mid
    modified:   smarActExApp/op/CSS-Boy/smaractAxisExpert.opi
Add a macro to initialize the VAL field for STEPSIZEF and STEPSIZER
Remove superflous "setClosedLoop" when printing "setClosedLoop" changes
(which actually switches the stage on and off)
Do not use printf() in home(): use asynPrint() instead
Add a simulator written in rust.
But: The simulater is good enough to demonstrate and test the driver.

Know issues:
Disabling the sensor in the hardware puts the stage into not homed
after the next movement.
And looses the position, so it must be re-homed with HOMF or HOMR,
depending where we are compared to the home mark.

Changes to be committed:
file:   test/smaract-sim-rs/smaract-sim-rs/.gitignore
file:   test/smaract-sim-rs/smaract-sim-rs/Cargo.lock
file:   test/smaract-sim-rs/smaract-sim-rs/Cargo.toml
file:   test/smaract-sim-rs/smaract-sim-rs/Makefile
file:   test/smaract-sim-rs/smaract-sim-rs/log.txt
file:   test/smaract-sim-rs/smaract-sim-rs/run.sh
file:   test/smaract-sim-rs/smaract-sim-rs/src/Makefile
file:   test/smaract-sim-rs/smaract-sim-rs/src/main.rs
file:   test/smaract-sim-rs/smaract-sim-rs/src/n2
file:   test/smaract-sim-rs/smaract-sim-rs/src/netcommand.rs
file:   test/smaract-sim-rs/smaract-sim-rs/src/netcommand/mcs2axis.rs
Merge all upstream changes with one exception:

Take smarActApp/src/smarActMCS2MotorDriver.cpp and .h from us.

Conflicts:
    .github/workflows/ci-scripts-build-full.yml
    .github/workflows/ci-scripts-build.yml
    smarActApp/src/smarActMCS2MotorDriver.cpp
    smarActApp/src/smarActMCS2MotorDriver.h
    smarActApp/src/smarActMCSMotorDriver.cpp
    smarActApp/src/smarActMCSMotorDriver.h
    smarActApp/src/smarActSCUMotorDriver.cpp
Prepare for the next commit:
The position of the positioner is "lost" when the sensor is disabled.
In other words: we need to introduce another variable.
When the sensor is disabled, do not update the position reported
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant