diff --git a/dg645Sup/Makefile b/dg645Sup/Makefile index ca08d77..e1cf4a7 100644 --- a/dg645Sup/Makefile +++ b/dg645Sup/Makefile @@ -8,6 +8,7 @@ DB += dg645_logic.db DB += dg645_delay.db DB += dg645_width.db DB += dg645_delay_width_shared.db +DB += litron.db #======================================= include $(TOP)/configure/RULES diff --git a/dg645Sup/litron.db b/dg645Sup/litron.db new file mode 100644 index 0000000..14b7408 --- /dev/null +++ b/dg645Sup/litron.db @@ -0,0 +1,144 @@ +record(calc, "$(P)$(R)$(chan1=A)DLAYSCAL") +{ + field(DESC, "Scaled raw value of ADELAY") + field(INPA, "$(P)$(R)$(chan1=A)DelayAI CP MS") # This is raw value scaled to 'us' + field(INPB, "1e6") + field(CALC, "A*B") + field(EGU, "us") +} + +record(calc, "$(P)$(R)$(chan2=C)DLAYSCAL") +{ + field(DESC, "Scaled raw value of CDELAY") + field(INPA, "$(P)$(R)$(chan2=C)DelayAI CP MS") + field(INPB, "1e6") + field(CALC, "A*B") + field(EGU, "us") +} + +record(calc, "$(P)$(R)SUMMED_DELAY") +{ + field(DESC, "Add dA & dC") + field(INPA, "$(P)$(R)$(chan1=A)DLAYSCAL CP MS") + field(INPB, "$(P)$(R)$(chan2=C)DLAYSCAL CP MS") + field(CALC, "A+B") # Add ADELAY & CDELAY + field(EGU, "us") +} + +record(ai, "$(P)$(R)OFFSET") +{ + field(DESC, "User Offset") + field(VAL, "0") + field(EGU, "us") +} + +record(ai, "$(P)$(R)DELAY") +{ + field(DESC, "User Delay") + field(VAL, "0") + field(EGU, "us") +} + +record(calc, "$(P)$(R)SUMMED_VALUE") +{ + field(DESC, "Add Delay Offset") + field(INPA, "$(P)$(R)DELAY CP") + field(INPB, "$(P)$(R)OFFSET CP") + field(CALC, "A+B") # Add DELAY & OFFSET + field(EGU, "us") +} + +record(mbbo, "$(P)$(R)MODE") +{ + field(DESC, "Device Mode. auto|1|2") + field(ZRST, "auto") + field(ONST, "1") + field(TWST, "2") + field(FLNK, "$(P)$(R)_MODEAUTOCHECK") # When mode changes then check if in mode auto + field(PINI, "YES") + field(VAL, "0") +} + +record(calcout, "$(P)$(R)_MODEAUTOCHECK") +{ + field(DESC, "Mode Auto Error Check") + field(INPA, "$(P)$(R)MODE") + field(INPB, "$(P)$(R)DELAY") + field(INPC, "$(P)$(R)OFFSET") + field(CALC, "A=1?A:(A=2?A:(B>ABS(C)?1:2))") + # If mode is 1 or 2 then return 1 or 2 + # Else if DELAY > OFFSET return 1 else 2 + field(OUT, "$(P)$(R)SET_MODE PP") +} + +record(longout, "$(P)$(R)SET_MODE") # Send command to device to load settings from specified slot +{ + field(DESC, "Load Settings from slot 0(default) - 9") + field(EGU, "") + field(DTYP, "asynInt32") + field(OUT, "@asyn($(PORT)) RECALL") # *RCL +} + +record(calc, "$(P)$(R)_MODE1CHECK") +{ + field(DESC, "Mode 1 Error Check") + field(INPA, "$(P)$(R)$(chan2=C)DLAYSCAL CP") + field(INPB, "$(P)$(R)SUMMED_VALUE CP") + field(CALC, "(A+B)>39900||B<0") + # If delay + summed_val bigger than 39900 or summed_val less than 0 then error +} + +record(calc, "$(P)$(R)_MODE2CHECK") +{ + field(DESC, "Mode 2 Error Check") + field(INPA, "$(P)$(R)$(chan2=C)DLAYSCAL CP") + field(INPB, "$(P)$(R)SUMMED_VALUE CP") + field(CALC, "B>39900||(A+B)<=0") + # If delay + summed_val less than or equal to 0 or summed_val bigger than 39900 then error +} + +record(calc, "$(P)$(R)_BOTHCHECKS") +{ + field(DESC, "Mode 1 & 2 Error Check") + field(INPA, "$(P)$(R)SET_MODE CP") + field(INPB, "$(P)$(R)_MODE1CHECK CP") + field(INPC, "$(P)$(R)_MODE2CHECK CP") + field(CALC, "(A=1)?B:C") + # If mode is 1 return _MODE1CHECK else _MODE2CHECK +} + +record(calcout, "$(P)$(R)ERROR") +{ + field(DESC, "Check Modes") + field(INPA, "$(P)$(R)SET_MODE CP") + field(INPB, "$(P)$(R)_BOTHCHECKS") + field(CALC, "A=0||B") # If no error then write delay when set + field(OOPT, "When Zero") + field(OUT, "$(P)$(R)_WRITE_DELAY PP") + field(DOPT, "Use OCAL") + field(OCAL, "1") +} + +record(seq, "$(P)$(R)_WRITE_DELAY") +{ + field(DESC, "Update Delay") + field(VAL, "0") + + # 1) Units set + field(LNK1, "$(P)$(R)$(chan1=A)DELAYUNIT:SP") + field(DO1, "2") + # 2) Set t0 + field(LNK2, "$(P)$(R)$(chan1=A)REFERENCE:SP") + field(DO2, "0") + # 3) Set delay A + field(LNK3, "$(P)$(R)$(chan1=A)DelayWriteAO") + field(DOL3, "$(P)$(R)SUMMED_VALUE") + # 4) Send + field(LNK4, "$(P)$(R)$(chan1=A)DELAYBUTTON") + field(DO4, "1") + # 5) *CLS (StatusClearBO) + field(LNK5, "$(P)$(R)StatusClearBO") + field(DO5, "1") + + field(SELM, "All") +} diff --git a/system_tests/lewis_emulators/Dg645/interfaces/stream_interface.py b/system_tests/lewis_emulators/Dg645/interfaces/stream_interface.py index b0bf9f8..d150bfb 100644 --- a/system_tests/lewis_emulators/Dg645/interfaces/stream_interface.py +++ b/system_tests/lewis_emulators/Dg645/interfaces/stream_interface.py @@ -65,7 +65,12 @@ class Dg645StreamInterface(StreamInterface): # Commands below are only defined but not implemented because without it, the Delaygen # ASYN driver would crash CmdBuilder("get_prescale_factor").escape("PRES?").spaces().int().eos().build(), - CmdBuilder("get_prescale_phase_factor").escape("PHAS?").spaces().int().eos().build(), + CmdBuilder("get_prescale_phase_factor") + .escape("PHAS?") + .spaces() + .int() + .eos() + .build(), CmdBuilder("get_interface_config").escape("IFCF?").spaces().int().eos().build(), CmdBuilder("get_trigger_rate").escape("TRAT?").eos().build(), CmdBuilder("get_advanced_triggering_mode").escape("ADVT?").eos().build(), diff --git a/system_tests/tests/dg645.py b/system_tests/tests/dg645.py index e8f5f01..d78c841 100644 --- a/system_tests/tests/dg645.py +++ b/system_tests/tests/dg645.py @@ -1,3 +1,4 @@ +# pyright: reportMissingImports=false import unittest from parameterized import parameterized @@ -89,19 +90,29 @@ def test_WHEN_logic_button_pressed_THEN_correct_logic_set_NR(self, _, channel): self.ca.assert_that_pv_is(channel + "OutputOffsetAI", -0) @parameterized.expand(parameterized_list(OUTPUT_CHANNELS)) - def test_WHEN_polarity_button_pressed_THEN_correct_polarity_set_positive(self, _, channel): + def test_WHEN_polarity_button_pressed_THEN_correct_polarity_set_positive( + self, _, channel + ): self.ca.set_pv_value(channel + "OUTPUTPOLARITY:SP", 1) self.ca.assert_that_pv_is(channel + "OutputPolarityBI.RVAL", 1) self.ca.assert_that_pv_is(channel + "OUTPUTPOLARITY_OFF", 0) @parameterized.expand(parameterized_list(OUTPUT_CHANNELS)) - def test_WHEN_polarity_button_pressed_THEN_correct_polarity_set_negative(self, _, channel): + def test_WHEN_polarity_button_pressed_THEN_correct_polarity_set_negative( + self, _, channel + ): self.ca.set_pv_value(channel + "OUTPUTPOLARITY:SP", 0) self.ca.assert_that_pv_is(channel + "OutputPolarityBI.RVAL", 0) self.ca.assert_that_pv_is(channel + "OUTPUTPOLARITY_OFF", 1) def calculate_delay(self, count, unit): - units_map = {"s": 1, "ms": 0.001, "us": 0.000001, "ns": 0.000000001, "ps": 0.000000000001} + units_map = { + "s": 1, + "ms": 0.001, + "us": 0.000001, + "ns": 0.000000001, + "ps": 0.000000000001, + } self.assertIn(unit, units_map, "Unexpected unit: " + str(unit)) return round(count * units_map[unit], 12) @@ -130,7 +141,9 @@ def check_channel_delay(self, chan, ref, dlay, unit): # Returns max delay of all channels set def set_all_channels(self, dataset): channels_to_set = DEVICE_CHANNELS[2:] - self.assertEqual(len(dataset), len(channels_to_set), "Incorrect dataset provided") + self.assertEqual( + len(dataset), len(channels_to_set), "Incorrect dataset provided" + ) current_max = 0 for i in range(len(dataset)): current_max = max( @@ -156,7 +169,9 @@ def set_all_channels(self, dataset): ("H", "T0", 99, "ms"), ] ) - def test_WHEN_delay_set_THEN_readback_correct(self, channel, reference, delay, unit): + def test_WHEN_delay_set_THEN_readback_correct( + self, channel, reference, delay, unit + ): self.set_channel_delay(channel, reference, delay, unit, True) # T1_delay = T0_delay + current_max_delay @@ -226,7 +241,9 @@ def get_channel_width(self, channel_data, which, width=0, depth=0): # on valid settings, the depth should never reach 8 self.assertLess( - depth, 8, "Endless reference loop detected in channel data. Test data invalid." + depth, + 8, + "Endless reference loop detected in channel data. Test data invalid.", ) # referencing T0 stops the search @@ -245,7 +262,10 @@ def check_channel_width_matches_settings(self, channel_settings, channel): self.assertEqual( expected, received, - "Delay width incorrect, expected: " + str(expected) + ", received: " + str(received), + "Delay width incorrect, expected: " + + str(expected) + + ", received: " + + str(received), ) # Each channel has a width which is equal to this channel's delay plus delay of referenced channel @@ -294,7 +314,9 @@ def check_total_channel_width(self, channel_a, channel_b): expected = abs(round(channel_a[1] - channel_b[1], 12)) received = self.calculate_delay( self.ca.get_pv_value(channel_a[0] + channel_b[0] + "DELAYWIDTH:RB"), - self.ca.get_pv_value(channel_a[0] + channel_b[0] + "DELAYWIDTHUNIT" ":RB.SVAL"), + self.ca.get_pv_value( + channel_a[0] + channel_b[0] + "DELAYWIDTHUNIT" ":RB.SVAL" + ), ) self.assertEqual( expected, diff --git a/system_tests/tests/dg645_llt.py b/system_tests/tests/dg645_llt.py new file mode 100644 index 0000000..7ee6ae5 --- /dev/null +++ b/system_tests/tests/dg645_llt.py @@ -0,0 +1,91 @@ +# pyright: reportMissingImports=false +import unittest + +from parameterized import parameterized +from utils.channel_access import ChannelAccess +from utils.ioc_launcher import ProcServLauncher, get_default_ioc_dir +from utils.test_modes import TestModes +from utils.testing import get_running_lewis_and_ioc + +DEVICE_PREFIX = "DG645_01" +EMULATOR_NAME = "Dg645" + +IOCS = [ + { + "name": DEVICE_PREFIX, + "directory": get_default_ioc_dir("DG645"), + "macros": { + "APPLICATION": "LITRON", + }, + "emulator": EMULATOR_NAME, + "ioc_launcher_class": ProcServLauncher, + }, +] + +TEST_MODES = [TestModes.DEVSIM] + + +class Dg645LLTTests(unittest.TestCase): + """ + Tests for the Dg645 Litron Laser Timing Control IOC. + """ + + def setUp(self): + self._lewis, self._ioc = get_running_lewis_and_ioc(EMULATOR_NAME, DEVICE_PREFIX) + self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) + + def test_Summed_Value(self): + self.ca.set_pv_value("OFFSET", 10) + self.ca.set_pv_value("DELAY", 20) + self.ca.assert_that_pv_is("SUMMED_VALUE", 30) + + def test_Summed_Delay(self): + self.ca.set_pv_value("ADLAYSCAL", 10) + self.ca.set_pv_value("CDLAYSCAL", 20) + self.ca.set_pv_value("ADELAYBUTTON", 1) + self.ca.set_pv_value("CDELAYBUTTON", 1) + self.ca.assert_that_pv_is("SUMMED_DELAY", 30) + + def test_Mode_Zero_Change_Mode_To_One(self): + self.ca.set_pv_value("DELAY", 10) + self.ca.set_pv_value("OFFSET", 5) + self.ca.set_pv_value("MODE", "auto") + self.ca.assert_that_pv_is("SET_MODE", 1) + + def test_Mode_Zero_Change_Mode_To_Two(self): + self.ca.set_pv_value("DELAY", 5) + self.ca.set_pv_value("OFFSET", 5) + self.ca.set_pv_value("MODE", "auto") + self.ca.assert_that_pv_is("SET_MODE", 2) + + @parameterized.expand( + [(0, 39900, 100, 1), (40000, 0, 0, 1), (10, -1, 0, 1), (10, -5, 5, 0)] + ) + def test_Mode_One_Error_Check(self, cdelay, delay, offset, err): + self.ca.set_pv_value("CDELAY:SP", cdelay) + self.ca.set_pv_value("CDELAYUNIT:SP", "us") + self.ca.set_pv_value("CDELAYBUTTON", 1) + self.ca.set_pv_value("OFFSET", offset) + self.ca.set_pv_value("DELAY", delay) + self.ca.set_pv_value("MODE", "1") + self.ca.set_pv_value("ERROR.PROC", "1") + self.ca.assert_that_pv_is("ERROR", err) + + @parameterized.expand( + [ + (0, 40000, 0, 1), + (40000, 0, 0, 0), + (0, 0, 0, 1), + (10, -1, 0, 0), + (10, -5, -6, 1), + ] + ) + def test_Mode_Two_Error_Check(self, cdelay, delay, offset, err): + self.ca.set_pv_value("CDELAY:SP", cdelay) + self.ca.set_pv_value("CDELAYUNIT:SP", "us") + self.ca.set_pv_value("CDELAYBUTTON", 1) + self.ca.set_pv_value("OFFSET", offset) + self.ca.set_pv_value("DELAY", delay) + self.ca.set_pv_value("MODE", "2") + self.ca.set_pv_value("ERROR.PROC", "1") + self.ca.assert_that_pv_is("ERROR", err)