From 4035c812878db045c5e5ce7c5380995fced32be9 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Fri, 13 Dec 2024 16:15:57 +0000 Subject: [PATCH] add db2autosave feature --- pyproject.toml | 1 + src/builder2ibek/db2autosave.py | 87 +++++++++ tests/samples/motor.template | 313 ++++++++++++++++++++++++++++++++ tests/test_autosave.py | 15 ++ tests/test_file_conversion.py | 2 +- 5 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/builder2ibek/db2autosave.py create mode 100644 tests/samples/motor.template create mode 100644 tests/test_autosave.py diff --git a/pyproject.toml b/pyproject.toml index 23aa299..0417c77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dev = [ [project.scripts] builder2ibek = "builder2ibek.__main__:cli" +db2autosave = "builder2ibek.autosave:cli" [project.urls] GitHub = "https://github.com/epics-containers/builder2ibek" diff --git a/src/builder2ibek/db2autosave.py b/src/builder2ibek/db2autosave.py new file mode 100644 index 0000000..3e25e6c --- /dev/null +++ b/src/builder2ibek/db2autosave.py @@ -0,0 +1,87 @@ +import re +from pathlib import Path + +import typer + +from builder2ibek import __version__ + +cli = typer.Typer() + + +def version_callback(value: bool): + if value: + typer.echo(__version__) + raise typer.Exit() + + +@cli.callback() +def main( + version: bool | None = typer.Option( + None, + "--version", + callback=version_callback, + is_eager=True, + help="Print the version of builder2ibek and exit", + ), +): + """ + Convert DLS autosave DB template comments into autosave req files + """ + + +regex_autosave = [ + re.compile(rf'# *% *autosave *{n} *(.*)[\s\S]*?record *\(.*, *"?([^"]*)"?\)') + for n in range(3) +] + + +@cli.command() +def db_files( + out_folder: Path = typer.Option( + ".", help="Output folder to write autosave request files" + ), + db_list: list[Path] = typer.Argument( + ..., help="List of autosave req files to link " + ), +): + """ + Convert DLS autosave DB template comments into autosave req files + """ + parse_templates(out_folder, db_list) + + +def parse_templates(out_folder: Path, db_list: list[Path]): + """ + DLS has 3 autosave levels + 0 = save on pass 0 + 1 = save on pass 0 and 1 + 2 = save on pass 1 only + + Areadetector and other support modules use: + template_name_positions.req for save on pass 0 + template_name_settings.req for save on pass 0 and pass 1 + the autosave docs seem to back the idea that pass 1 only is not used + + So this translation will make + 0 => template_name_positions.req + 1 => template_name_settings.req + """ + for db in db_list: + text = db.read_text() + + positions = set() + settings = set() + for n in range(3): + match n: + case 0: + this_set = positions + case 1 | 2: + this_set = settings + for match in regex_autosave[n].finditer(text): + this_set.add(f"{match.group(2)} {match.group(1)}") + + stem = db.stem + req_file = out_folder / f"{stem}_positions.req" + req_file.write_text("\n".join(positions)) + req_file = out_folder / f"{stem}_settings.req" + req_file.write_text("\n".join(settings)) diff --git a/tests/samples/motor.template b/tests/samples/motor.template new file mode 100644 index 0000000..2b5e4ee --- /dev/null +++ b/tests/samples/motor.template @@ -0,0 +1,313 @@ +#% macro, __doc__, Basic template, including motor record and associated tags +#% macro, P , Device Prefix +#% macro, M , Device Suffix +#% macro, PORT , Asyn port for motor record +#% macro, ADDR , Address on controller +#% macro, DESC , Description, displayed on EDM screen +#% macro, MRES , Motor Step Size (EGU) +#% macro, DTYP , DTYP of record +#% macro, DIR , User Direction +#% macro, VBAS , Base Velocity (EGU/s) +#% macro, VELO , Velocity (EGU/s) +#% macro, VMAX , Max Velocity (EGU/s), defaults to VELO +#% macro, ACCL , Seconds to Velocity +#% macro, BDST , BL Distance (EGU) +#% macro, BVEL , BL Velocity (EGU/s) +#% macro, BACC , BL Seconds to Veloc. +#% macro, PREC , Display Precision +#% macro, EGU , Engineering Units +#% macro, DHLM , Dial High Limit +#% macro, DLLM , Dial Low Limit +#% macro, HLM , User High Limit +#% macro, LLM , User Low Limit +#% macro, HLSV , HW Lim. Violation Svr +#% macro, INIT , Startup commands +#% macro, SREV , Steps per Revolution +#% macro, RRES , Readback Step Size (EGU) +#% macro, TWV , Tweak Step Size (EGU) +#% macro, ERES , Encoder Step Size (EGU) +#% macro, JAR , Jog Acceleration (EGU/s^2) +#% macro, UEIP , Use Encoder If Present +#% macro, URIP , Use RDBL If Present +#% macro, RDBL , Readback Location, set URIP = 1 if you specify this +#% macro, RLNK , Readback output link +#% macro, RTRY , Max retry count +#% macro, DLY , Readback settle time (s) +#% macro, OFF , User Offset (EGU) +#% macro, RDBD , Retry Deadband (EGU) +#% macro, FOFF , Freeze Offset, 0=variable, 1=frozen +#% macro, ADEL , Alarm monitor deadband (EGU) +#% macro, NTM , New Target Monitor, only set to 0 for soft motors +#% macro, FEHIGH , HIGH limit for following error +#% macro, FEHIHI , HIHI limit for following error +#% macro, FEHHSV , HIHI alarm severity for following error +#% macro, FEHSV , HIGH alarm severity for following error +#% macro, SCALE , Scale factor, if pmacSetAxisScale is used this should be set +#% macro, HOMEVIS, If 1 then home is visible on the gui +#% macro, HOMEVISSTR, If HOMEVIS=0, then display this text on the gui instead +#% macro, name , Object name and gui association name +#% macro, alh , Set this to alh to add the motor to the alarm handler and send emails, +# set to '#' to comment out alh lines +#% macro, gda_name, Name to export this as to GDA +#% macro, gda_desc, Description to export this as to GDA + +# AUTOSAVE: level 0 = before record init, level 1 = before AND after record init +#% autosave 0 DVAL OFF +#% autosave 1 DIR DHLM DLLM TWV VBAS VELO ACCL BDST BVEL BACC RDBD EGU RTRY UEIP URIP DLY PREC DISA DISP FOFF OFF FRAC OMSL JVEL JAR ADEL MDEL +#% archiver 0.5 Monitor +#% archiver 0.5 Monitor RBV +#% archiver 10 Monitor OFF +#% archiver 10 Monitor MRES +#% gdatag,template,simpleMotor,$(gda_name=),$(gda_desc=$(DESC)) +#% gdatag,motor,rw,$(gda_name=),RECORD,Motor +#% alh +#% $(alh=None) $SEVRCOMMAND UP_ANY dls-alh-handler.py $(P)$(M) + +# This associates an edm screen with the template +# % gui, $(name=), edm, motor.edl, motor=$(P)$(M) +# % gui, $(name=), edmembed, motor-embed-small.edl, motor=$(P)$(M),filename=motor.edl,box-label=$(DESC) + +# This associates a BOY screen with the template +# % gui, $(name=), boydetail, motorApp_opi/motor_detail.opi, P=$(P),M=$(M),DESC=$(DESC), name=$(name=) +# % gui, $(name=), boyembed, motorApp_opi/motor_embed_box.opi, P=$(P),M=$(M),DESC=$(DESC), name=$(name=) +# % gui, $(name=), boyembed, motorApp_opi/motor_embed.opi, P=$(P),M=$(M),DESC=$(DESC), name=$(name=) +# FIXME: this should be in pmacUtil or tpmac +# % gui, $(name=), boyembed, motorApp_opi/motor_homed_embed.opi, P=$(P),M=$(M) + +# This makes the component icon reflect the status and severity +# % gui, $(name=), status, .MOVN +# % gui, $(name=), sevr + +# These define what PVs a motor detail screen should contain +# % gui, $(name=), statusbits, Status, .MSTA, Direction Positive, Done, High Limit, Home Limit, Unused, Closed Loop, Following Error, At Home, Encoder Present, Problem, Moving, Gain Support, Comms Error, Low Limit, Homed +# % gui, $(name=), statusbits, ELoss, :ELOSS, Encoder Loss, Amplifier Loss, Sys Fail +# % gui, $(name=), command, ELoss Clear, :ELOSSRC.A +# % gui, $(name=), statusbits, Limit Violation, .LVIO, Lim +# % gui, $(name=), statusbits, At High Limit, .HLS, High +# % gui, $(name=), statusbits, At Low Limit, .LLS, Low +# % gui, $(name=), demand, User High Limit, .HLM +# % gui, $(name=), demand, User Low Limit, .LLM +# % gui, $(name=), demand, Dial High Limit, .DHLM +# % gui, $(name=), demand, Dial Low Limit, .DLLM + +# % gui, $(name=), demand, Motor demand, .VAL +# % gui, $(name=), readback, Motor readback, .RBV +# % gui, $(name=), command, Stop, .STOP +# % gui, $(name=), command, Home Forward, .HOMF +# % gui, $(name=), command, Home Reverse, .HOMR +# % gui, $(name=), command, Jog Forward, .JOGF +# % gui, $(name=), command, Jog Reverse, .JOGR +# % gui, $(name=), command, Tweak Forward, .TWF +# % gui, $(name=), command, Tweak Reverse, .TWR +# % gui, $(name=), demand, Tweak Step, .TWV +# % gui, $(name=), command, Kill, :KILL.PROC, Kill +# % gui, $(name=), command, Sync VAL=RBV, .SYNC + +# % gui, $(name=), enum, Direction, .DIR +# % gui, $(name=), demand, User Offset, .OFF +# % gui, $(name=), enum, Set/Use, .SET +# % gui, $(name=), enum, Offset, .FOFF +# % gui, $(name=), enum, Use Encoder, .UEIP + +# % gui, $(name=), demand, Motor Step Size, .MRES +# % gui, $(name=), readback, Steps per Rev, .SREV +# % gui, $(name=), readback, EGUs per Rev, .UREV +# % gui, $(name=), demand, Encoder Step Size, .ERES +# % gui, $(name=), readback, Readback Step Size, .RRES +# % gui, $(name=), readback, Use Encoder if Present, .UEIP + +# % gui, $(name=), demand, Max Velocity, .VMAX +# % gui, $(name=), demand, Base Velocity, .VBAS +# % gui, $(name=), demand, Velocity, .VELO +# % gui, $(name=), demand, Secs to Velocity, .ACCL +# % gui, $(name=), demand, JVEL, .JVEL +# % gui, $(name=), demand, Jog Acceleration, .JAR +# % gui, $(name=), demand, Backlash Distance, .BDST +# % gui, $(name=), demand, Backlash Velocity, .BVEL +# % gui, $(name=), demand, Backlash Secs to Vel, .BACC +# % gui, $(name=), demand, Move Fraction, .FRAC +# % gui, $(name=), demand, Retry Deadband, .RDBD +# % gui, $(name=), demand, Max Retries, .RTRY + +# % gui, $(name=), demand, PREC, .PREC +# % gui, $(name=), demand, EGU, .EGU + +# % gui, $(name=), demand, Output Specification, .OUT +# % gui, $(name=), readback, Readback Location, .RDBL +# % gui, $(name=), readback, Desired Output Loc, .DOL +# % gui, $(name=), readback, Output Mode Select, .OMSL +# % gui, $(name=), readback, Readback Out Link, .RLNK +# % gui, $(name=), demand, DMOV Input Link, .DINP +# % gui, $(name=), demand, RMP Input Link, .RINP +# % gui, $(name=), demand, Stop Out Link, .STOO + + +record(motor,"$(P)$(M)) +{ + field(DESC,"$(DESC)") + field(DTYP,"$(DTYP=asynMotor)") + field(DIR,"$(DIR=0)") + field(VELO,"$(VELO)") + field(VBAS,"$(VBAS=0)") + field(ACCL,"$(ACCL=0.5)") + field(BDST,"$(BDST=0)") + field(BVEL,"$(BVEL=0)") + field(BACC,"$(BACC=)") + field(OUT,"@asyn($(PORT),$(ADDR))") + field(MRES,"$(MRES)") + field(PREC,"$(PREC)") + field(EGU,"$(EGU)") + field(DHLM,"$(DHLM=)") + field(DLLM,"$(DLLM=)") + field(HLM,"$(HLM=)") + field(LLM,"$(LLM=)") + field(HLSV,"$(HLSV=MAJOR)") + field(INIT,"$(INIT=)") + field(RTRY,"$(RTRY=0)") + field(DLY,"$(DLY=0)") + field(HVEL,"0") + field(SREV, "$(SREV=1000)") + field(RRES, "$(RRES=)") + field(TWV, "$(TWV)") + field(ERES, "$(ERES=)") + field(JVEL, "$(VELO)") + field(JAR, "$(JAR=)") + field(UEIP, "$(UEIP=0)") + field(URIP, "$(URIP=0)") + field(RDBL, "$(RDBL=)") + field(VMAX, "$(VMAX=$(VELO))") + field(OFF, "$(OFF=0)") + field(RDBD, "$(RDBD=)") + field(FOFF, "$(FOFF=0)") + field(ADEL, "$(ADEL=0)") + field(NTM, "$(NTM=1)") + field(SDIS, "$(P)$(M):SDIS.VAL") + field(RLNK, "$(RLNK=)") +} + +# record that holds the ADDR +record(ao, "$(P)$(M):ADDR") { + field(PINI, "YES") + field(VAL, "$(ADDR)") +} + +# record that holds the PORT +record(stringout, "$(P)$(M):PORT") { + field(PINI, "YES") + field(VAL, "$(PORT)") +} + +# record that holds the SCALE +record(ao, "$(P)$(M):SCALE") { + field(PINI, "YES") + field(VAL, "$(SCALE=1)") +} + +# Write a 1 to this record over Channel Access to disable the motor record and prevent CA writes to any field +# including the demand field. +record(ai, "$(P)$(M):USER:SDIS") { + field(VAL, "0") + field(FLNK, "$(P)$(M):SDIS") +} + +# Note: This record forms part of the interface to auto-homing, which uses the "A" field to disable the motor record +# when homing. +# record that will disable and re-enable record if any of its inputs are non-zero +record(calcout, "$(P)$(M):SDIS") { + field(DESC, "Disable on non-zero input") + field(INPL, "$(P)$(M):USER:SDIS") + field(CALC, "(A|B|C|D|E|F|G|H|I|J|K|L)>0") + field(OUT, "$(P)$(M).DISP PP") +} + +# dummy record that looks like a kill command +record(ao, "$(P)$(M):KILL") { + field(VAL, "1") +} + +# dummy record that looks like eloss status +#% archiver 10 Monitor VAL +#% $(alh=None) $SEVRCOMMAND UP_ANY dls-alh-handler.py $(P)$(M):ELOSS +record(ai, "$(P)$(M):ELOSS") { + field(PINI, "YES") + field(HIHI, "1") + field(HHSV, "MAJOR") +} + +# dummy record that looks like an eloss reset +record(calcout, "$(P)$(M):ELOSSRC") { +} + +# check if SYNC has been pressed +record(calcout, "$(P)$(M):CHECK_SYNC") { + field(INPA, "$(P)$(M).SYNC CP") + field(CALC, "A>0") + field(OOPT, "When Non-zero") + field(OUT, "$(P)$(M):MR_MOVE_WRITE.PROC PP") +} + +# dummy record that looks like a record to notify a CS about a raw motor move +record(longout, "$(P)$(M):MR_MOVE_WRITE") { +} + +#Record to calculate the following error +#% archiver 1 Monitor +record(calc, "$(P)$(M):FERROR") +{ + field(DESC,"Following Error") + field(INPA,"$(P)$(M).RMP CP") + field(INPB,"$(P)$(M).REP NPP") + field(INPC,"$(P)$(M).MRES NPP") + field(INPD,"$(P)$(M).ERES NPP") + field(CALC,"ABS((A*C)-(B*D))") + field(FLNK,"$(P)$(M):FERRORMAX") + field(PREC,"$(PREC)") + field(EGU, "$(EGU)") +} + +#Record to store the maximum following error +#% archiver 10 Monitor VAL +#% autosave 1 VAL +#% $(alh=None) $SEVRCOMMAND UP_ANY dls-alh-handler.py $(P)$(M):FERRORMAX +record(calc, "$(P)$(M):FERRORMAX") +{ + field(DESC,"Following Error Max") + field(INPA,"$(P)$(M):FERROR.VAL") + field(INPB,"$(P)$(M):FERRORMAX.VAL") + field(CALC,"(A>B)?A:B") + field(HIGH,"$(FEHIGH=0)") + field(HIHI,"$(FEHIHI=0)") + field(HHSV,"$(FEHHSV=NO_ALARM)") + field(HSV, "$(FEHSV=NO_ALARM)") + field(PREC,"$(PREC)") + field(EGU, "$(EGU)") +} + +#Record to reset the maximum following error +record(bo, "$(P)$(M):FEMAXRESET") +{ + field(DESC,"Reset max following error") + field(DTYP,"Soft Channel") + field(OUT, "$(P)$(M):FERRORMAX.VAL") + field(VAL, "0") +} + + +#Record to determin HOME button visibility on the motor.edl screen. +record(bo, "$(P)$(M):HOMEVIS") +{ + field(DESC,"Home button visibility") + field(DTYP,"Soft Channel") + field(VAL, "$(HOMEVIS=1)") + field(ZNAM, "Invisible") + field(ONAM, "Visible") +} + +#Record to display a message if the HOME buttons are not visible (if HOMEVIS=0). +record(stringout, "$(P)$(M):HOMEVISSTR") +{ + field(DESC, "Home visibility string") + field(DTYP,"Soft Channel") + field(VAL, "$(HOMEVISSTR=Use motor summary screen)") +} + diff --git a/tests/test_autosave.py b/tests/test_autosave.py new file mode 100644 index 0000000..7541bae --- /dev/null +++ b/tests/test_autosave.py @@ -0,0 +1,15 @@ +import shutil +from pathlib import Path + +from builder2ibek.autosave import parse_templates + + +def test_autosave(): + conversion_samples = [ + Path("tests/samples/motor.template"), + ] + + shutil.rmtree("/tmp/autosave", ignore_errors=True) + Path("/tmp/autosave").mkdir() + + parse_templates("/tmp/autosave", conversion_samples) diff --git a/tests/test_file_conversion.py b/tests/test_file_conversion.py index 49ffc43..d9630d9 100644 --- a/tests/test_file_conversion.py +++ b/tests/test_file_conversion.py @@ -2,7 +2,7 @@ import sys -def test_cli_version(): +def test_file_conversion(): conversion_samples = [ "tests/samples/BL45P-MO-IOC-01.xml", "tests/samples/BL99P-EA-IOC-05.xml",