I think I have something that will work for you. This is my most recent version of a 16 position ‘positioner’ menu:
There are two database templates. The first one includes the second one multiple times, once for each position:
############################################################################
#
# Positioner template for 16 positions.
# This is used for the sample stages and typically will be populated with pre-defined
# sets of positions.
#
# It makes use of the common positioner records in positioner_position.template.
#
# Macros:
# S - base PV name (eg. CG3:Mot:Sample)
# MOT - real motor PV (eg. CG3:Mot:scx)
# P0 to P15 - the 16 positions (optional, default is -1000.0)
# D0 to D15 - the 16 descriptions (optional, default is '')
# PREC - display precision
# TOLERANCE - used to determine position
# EGU - mm or deg
# POS_ASG - the ASG level for the position and descriptions (default is DEFAULT)
# AUTOSAVE - set this to " " to enable autosave of the positions and descriptions
# ///
# /// Instantiate the position records (0-15)
# ///
substitute "POS=0"
substitute "NEXTPOS=1"
substitute "POSITION=$(P0=-1000.0)"
substitute "DESC=$(D0='')"
include "positioner_position.template"
substitute "POS=1"
substitute "NEXTPOS=2"
substitute "POSITION=$(P1=-1000.0)"
substitute "DESC=$(D1='')"
include "positioner_position.template"
substitute "POS=2"
substitute "NEXTPOS=3"
substitute "POSITION=$(P2=-1000.0)"
substitute "DESC=$(D2='')"
include "positioner_position.template"
substitute "POS=3"
substitute "NEXTPOS=4"
substitute "POSITION=$(P3=-1000.0)"
substitute "DESC=$(D3='')"
include "positioner_position.template"
substitute "POS=4"
substitute "NEXTPOS=5"
substitute "POSITION=$(P4=-1000.0)"
substitute "DESC=$(D4='')"
include "positioner_position.template"
substitute "POS=5"
substitute "NEXTPOS=6"
substitute "POSITION=$(P5=-1000.0)"
substitute "DESC=$(D5='')"
include "positioner_position.template"
substitute "POS=6"
substitute "NEXTPOS=7"
substitute "POSITION=$(P6=-1000.0)"
substitute "DESC=$(D6='')"
include "positioner_position.template"
substitute "POS=7"
substitute "NEXTPOS=8"
substitute "POSITION=$(P7=-1000.0)"
substitute "DESC=$(D7='')"
include "positioner_position.template"
substitute "POS=8"
substitute "NEXTPOS=9"
substitute "POSITION=$(P8=-1000.0)"
substitute "DESC=$(D8='')"
include "positioner_position.template"
substitute "POS=9"
substitute "NEXTPOS=10"
substitute "POSITION=$(P9=-1000.0)"
substitute "DESC=$(D9='')"
include "positioner_position.template"
substitute "POS=10"
substitute "NEXTPOS=11"
substitute "POSITION=$(P10=-1000.0)"
substitute "DESC=$(D10='')"
include "positioner_position.template"
substitute "POS=11"
substitute "NEXTPOS=12"
substitute "POSITION=$(P11=-1000.0)"
substitute "DESC=$(D11='')"
include "positioner_position.template"
substitute "POS=12"
substitute "NEXTPOS=13"
substitute "POSITION=$(P12=-1000.0)"
substitute "DESC=$(D12='')"
include "positioner_position.template"
substitute "POS=13"
substitute "NEXTPOS=14"
substitute "POSITION=$(P13=-1000.0)"
substitute "DESC=$(D13='')"
include "positioner_position.template"
substitute "POS=14"
substitute "NEXTPOS=15"
substitute "POSITION=$(P14=-1000.0)"
substitute "DESC=$(D14='')"
include "positioner_position.template"
substitute "POS=15"
substitute "NEXTPOS=16"
substitute "POSITION=$(P15=-1000.0)"
substitute "DESC=$(D15='')"
include "positioner_position.template"
# ///
# /// Record to check which position the motor is at.
# /// -1 will mean 'Undefined'
# ///
record(longin, "$(S):Pos") {
field(LOW, "-1")
field(LSV, "MINOR")
info(archive, "Monitor, 00:00:01, VAL")
}
# ///
# /// Record to hold the description of the current position
# ///
record(stringin, "$(S):Desc") {
field(PINI, "YES")
field(VAL, " ")
}
# ///
# /// Description for this current positioner set. This may be
# /// manually set or set when populating the positioner from a
# /// table_position template.
# ///
record(stringout, "$(S):Description") {
field(PINI, "YES")
field(VAL, " ")
info(autosaveFields, "VAL")
}
# ///
# /// Calculate which position (and the corresponding description) we are at (if any)
# ///
record(bo, "$(S):Recalculate") {
field(FLNK, "$(S):TriggerCalcs")
}
record(bi, "$(S):TriggerCalcs") {
field(INP, "$(MOT).RBV CP MS")
field(PINI, "YES")
field(FLNK, "$(S):SetTempPos")
}
record(longout, "$(S):SetTempPos") {
field(VAL, "-1")
field(OUT, "$(S):TempPos PP")
field(FLNK, "$(S):SetTempDesc")
}
record(stringout, "$(S):SetTempDesc") {
field(VAL, " ")
field(OUT, "$(S):TempDesc PP")
field(FLNK, "$(S):Pos0Calc")
}
record(longin, "$(S):TempPos") {
field(VAL, "-1")
}
record(stringin, "$(S):TempDesc") {
field(VAL, " ")
}
record(calcout, "$(S):Pos15Calc") {
field(DESC, "Trigger WritePos")
field(FLNK, "$(S):WritePos")
}
record(dfanout, "$(S):WritePos") {
field(DOL, "$(S):TempPos MS")
field(OMSL, "closed_loop")
field(OUTA, "$(S):Pos.VAL PP")
field(FLNK, "$(S):WriteDesc")
}
record(stringout, "$(S):WriteDesc") {
field(OMSL, "closed_loop")
field(DOL, "$(S):TempDesc MS")
field(OUT, "$(S):Desc.VAL PP")
}
# ///
# /// Select among preset positions and move $(MOT) there
# ///
record(mbbo, "$(S):Menu") {
field(ZRVL, "0")
field(ONVL, "1")
field(TWVL, "2")
field(THVL, "3")
field(FRVL, "4")
field(FVVL, "5")
field(SXVL, "6")
field(SVVL, "7")
field(EIVL, "8")
field(NIVL, "9")
field(TEVL, "10")
field(ELVL, "11")
field(TVVL, "12")
field(TTVL, "13")
field(FTVL, "14")
field(FFVL, "15")
field(ZRST, "Out")
field(ONST, "Pos 1")
field(TWST, "Pos 2")
field(THST, "Pos 3")
field(FRST, "Pos 4")
field(FVST, "Pos 5")
field(SXST, "Pos 6")
field(SVST, "Pos 7")
field(EIST, "Pos 8")
field(NIST, "Pos 9")
field(TEST, "Pos 10")
field(ELST, "Pos 11")
field(TVST, "Pos 12")
field(TTST, "Pos 13")
field(FTST, "Pos 14")
field(FFST, "Pos 15")
field(FLNK, "$(S):MenuSel")
info(autosaveFields_pass0, "VAL")
info(archive, "Monitor, 00:00:01, VAL")
}
record(calcout, "$(S):MenuSel") {
field(INPA, "$(S):Menu NPP MS")
field(CALC, "(A>=0)&&(A<=9)?A+1:0")
field(DOPT, "Use CALC")
field(OOPT, "When Non-zero")
field(OUT, "$(S):PosGoList.SELN PP")
field(FLNK, "$(S):MenuSel2")
}
record(calcout, "$(S):MenuSel2") {
field(INPA, "$(S):Menu NPP MS")
field(CALC, "(A>=10)&&(A<=15)?(A-9):0")
field(DOPT, "Use CALC")
field(OOPT, "When Non-zero")
field(OUT, "$(S):PosGoList2.SELN PP")
}
record(sseq, "$(S):PosGoList") {
field(SELM, "Specified")
field(WAIT1, "Wait")
field(WAIT2, "Wait")
field(WAIT3, "Wait")
field(WAIT4, "Wait")
field(WAIT5, "Wait")
field(WAIT6, "Wait")
field(WAIT7, "Wait")
field(WAIT8, "Wait")
field(WAIT9, "Wait")
field(WAITA, "Wait")
field(DOL1, "1")
field(DOL2, "1")
field(DOL3, "1")
field(DOL4, "1")
field(DOL5, "1")
field(DOL6, "1")
field(DOL7, "1")
field(DOL8, "1")
field(DOL9, "1")
field(DOLA, "1")
field(LNK1, "$(S):Pos0Go.PROC CA")
field(LNK2, "$(S):Pos1Go.PROC CA")
field(LNK3, "$(S):Pos2Go.PROC CA")
field(LNK4, "$(S):Pos3Go.PROC CA")
field(LNK5, "$(S):Pos4Go.PROC CA")
field(LNK6, "$(S):Pos5Go.PROC CA")
field(LNK7, "$(S):Pos6Go.PROC CA")
field(LNK8, "$(S):Pos7Go.PROC CA")
field(LNK9, "$(S):Pos8Go.PROC CA")
field(LNKA, "$(S):Pos9Go.PROC CA")
field(FLNK, "$(S):Recalculate")
}
record(sseq, "$(S):PosGoList2") {
field(SELM, "Specified")
field(WAIT1, "Wait")
field(WAIT2, "Wait")
field(WAIT3, "Wait")
field(WAIT4, "Wait")
field(WAIT5, "Wait")
field(WAIT6, "Wait")
field(DOL1, "1")
field(DOL2, "1")
field(DOL3, "1")
field(DOL4, "1")
field(DOL5, "1")
field(DOL6, "1")
field(LNK1, "$(S):Pos10Go.PROC CA")
field(LNK2, "$(S):Pos11Go.PROC CA")
field(LNK3, "$(S):Pos12Go.PROC CA")
field(LNK4, "$(S):Pos13Go.PROC CA")
field(LNK5, "$(S):Pos14Go.PROC CA")
field(LNK6, "$(S):Pos15Go.PROC CA")
field(FLNK, "$(S):Recalculate")
}
and the second template is:
# This template is meant to be instantiated in a higher level template
# that builds a multiple position table.
# It simply has records that repeat for each position in the table.
# We have to override the FLNK in the last PosCalc record in the higher level template.
# ///
# /// Table position record and description
# ///
record(ao, "$(S):Pos$(POS)") {
field(DESC, "Pos for p$(POS)")
field(VAL, "$(POSITION=-1000)")
field(PREC, "$(PREC)")
field(PINI, "YES")
field(EGU, "$(EGU)")
field(ASG, "$(POS_ASG=)")
$(AUTOSAVE=#) info(autosaveFields, "VAL")
info(archive, "Monitor, 00:00:01, VAL")
}
record(stringout, "$(S):Desc$(POS)") {
field(DESC, "Desc for p$(POS)")
field(VAL, "$(DESC= )")
field(PINI, "YES")
field(ASG, "$(POS_ASG=)")
$(AUTOSAVE=#) info(autosaveFields, "VAL")
}
# ///
# /// Record to provide an 'in-position' LED
# ///
record(bi, "$(S):InPos$(POS)") {
field(VAL, "0")
field(ZNAM, "Not In Position")
field(ONAM, "In Position")
info(archive, "Monitor, 00:00:01, VAL")
}
# ///
# /// Records to move motor to preset Positions
# ///
record(calcout, "$(S):Pos$(POS)Go") {
field(INPA, "$(S):Pos$(POS).VAL NPP MS")
field(CALC, "A")
field(OUT, "$(MOT).VAL PP")
}
# ///
# /// Set the in position flag and also write the current position
# /// number into TempPosition. Also copy the current description into
# /// TempDescription.
# ///
record(bo, "$(S):Pos$(POS)Set") {
field(VAL, "1")
field(OUT, "$(S):InPos$(POS) PP")
field(FLNK, "$(S):InPos$(POS)SetTemp")
}
record(calcout, "$(S):InPos$(POS)SetTemp") {
field(INPA, "$(S):Pos$(POS)Set.VAL")
field(CALC, "A")
field(DOPT, "Use OCAL")
field(OOPT, "When Non-zero")
field(OCAL, "$(POS)")
field(OUT, "$(S):TempPos PP")
field(FLNK, "$(S):InPos$(POS)SetTempDesc")
}
record(calcout, "$(S):InPos$(POS)SetTempDesc") {
field(INPA, "$(S):Pos$(POS)Set.VAL")
field(CALC, "A")
field(DOPT, "Use CALC")
field(OOPT, "When Non-zero")
field(OUT, "$(S):InPos$(POS)WriteTempDesc.PROC PP")
}
record(stringout, "$(S):InPos$(POS)WriteTempDesc") {
field(OMSL, "closed_loop")
field(DOL, "$(S):Desc$(POS)")
field(OUT, "$(S):TempDesc PP")
}
# ///
# /// This position calculation is done as part of the TriggerCalcs
# ///
record(calcout, "$(S):Pos$(POS)Calc") {
field(INPA, "$(MOT).RBV NPP")
field(INPB, "$(TOLERANCE)")
field(INPC, "$(S):Pos$(POS)")
field(DOPT, "Use CALC")
field(OOPT, "Every Time")
field(CALC, "((A>(C-B))&&(A<(C+B)))?1:0")
field(OUT, "$(S):Pos$(POS)Set.VAL PP")
field(FLNK, "$(S):Pos$(NEXTPOS)Calc")
}
The second template is basically all the records common to each position. In the first template I just override the FLNK of the last instance, to provide an end point to the processing chain.
To instantiate all this in a substitution file, it’s simply:
file positioner_16.template
{
pattern {S, MOT, PREC, TOLERANCE, EGU, AUTOSAVE, POS_ASG}
{CG2:Mot:SamplePos, CG2:Mot:Sample:SCX, 3, 0.1, mm, " ", DEFAULT}
}
where CG2:Mot:SamplePos is the base part of the PV names for all the positioner table logic, and CG2:Mot:Sample:SCX is the motor record that will be controlled via the positioner logic.
You have the choice to not use autosave, in which case you would list all the positions and descriptions in the substitutions file (or hard code in the first template file). Or you can use autosave and let the users change the positions and descriptions.
This can easily be expanded to more than 16 positions, but then the first template needs to be modified so that the mbbo Menu record changes to a longout record, and the sseq records that select the position would need to be expanded.
I’ve also attached a screenshot showing what it looks like (there’s a separate screen allowing edits to the positions and descriptions).
On the screenshot, the ‘Current Position’ is a integer (not the motor position) that matches the numbers written to the ‘Move To’ menu, and the logic supports put_callback so it plays nice with clients that immediately check ‘Current Position’
after the end of the move. The ‘Current Description’ is a string record that shows whatever the description is for the current position.
The ‘Current Position’ is updated whenever the motor moves, and at the end of the move. If someone manually edits one of the table positions, then the ‘Current Position’ can be recalculated by writing 1 to the ‘Recalculate’ record in the top level
template.
Cheers,
Matt
Data Acquisition and Controls Engineer
Spallation Neutron Source
Oak Ridge National Lab
On Oct 21, 2019, at 2:04 PM, Davis, Mark via Tech-talk <[email protected]> wrote:
Hi all,
I have a new task that I figured those of you with much more experience
crafting clever record logic could help with:
The components that need to be supported:
A record for some device to which setpoints stored in other
records are to be written (e.g. a motor record).
A string value that provides a description of the current readback
of the device record.
Pairs of numeric and string values. For each pair, the number
represents one of the setpoints an operator can chose to have written to
the device record and the string represents a user-readable description
of what the associated number represents (e.g. the thickness or type of
material to place in the path of the beam, the size of a whole in a
metal plate, etc).
The operators can change the string (and possibly the numeric)
values whenever they want. Changes must be persistent (i.e. changes are
saved to a file and restored when the IOC restarts, probably using
autosave).
Logic that monitors the current readback of the device record.
Whenever the position is close to (within some specified deadband) of
the numeric value in one of the numeric/string pairs, it copies the
associated string to the one that describes the current readback. When
the readback is NOT close to one of the numeric values, it will write
something like "Invalid position" to the description string.
And of course it has to be relatively simple for the person
configuring the IOC to change the # of numeric/string pairs supported
for a device.
No doubt I can cobble something together that will do the job, but I
figured someone out there will have already dealt with a similar need
and can provide something much simpler, more flexible, and less
cumbersome than what I am likely to create on my first attempt.
Any tips or suggestions would be much appreciated.
Thanks,
Mark Davis
NSCL/FRIB