diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..9585212
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,29 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Explicitly declare text files you want to always be normalized and converted
+# to native line endings on checkout.
+*.c text
+*.cpp text
+*.h text
+*.hpp text
+*.java text
+*.sh text eol=lf
+*.bat text
+*.cmd text
+*.db text
+*.dbd text
+*.template text
+*.substitutions text
+*.py text
+*.rst text
+*.uxf text
+
+# Declare files that will always have CRLF line endings on checkout.
+*.sln text eol=crlf
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
+*.class binary
+*.vi binary
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..43df806
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+O.*/
+/db
+/bin
+/dbd
+/include
+/lib
+/templates
+test-reports
+envPaths
+cdCommands
+dllPath.bat
+runIOC.bat
+runIOC.sh
+relPaths.sh
+*.tag
+/data/
+/doc/
+*_info_positions.req
+*_info_settings.req
+*.py[cod]
+__pycache__/
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..45589b4
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,30 @@
+BSD 3-Clause License
+
+Copyright (c) 2023, Science and Technology Facilities Council
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..12a7415
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,37 @@
+# Makefile for Asyn rknmntr support
+#
+# Created by wtn43451 on Wed Sep 27 14:08:57 2023
+# Based on the Asyn streamSCPI template
+
+TOP = .
+include $(TOP)/configure/CONFIG
+
+DIRS += configure
+DIRS += $(wildcard *Sup)
+DIRS += $(wildcard *App)
+DIRS += $(wildcard *Top)
+DIRS += $(wildcard iocBoot)
+
+# The build order is controlled by these dependency rules:
+
+# All dirs except configure depend on configure
+$(foreach dir, $(filter-out configure, $(DIRS)), \
+ $(eval $(dir)_DEPEND_DIRS += configure))
+
+# Any *App dirs depend on all *Sup dirs
+$(foreach dir, $(filter %App, $(DIRS)), \
+ $(eval $(dir)_DEPEND_DIRS += $(filter %Sup, $(DIRS))))
+
+# Any *Top dirs depend on all *Sup and *App dirs
+$(foreach dir, $(filter %Top, $(DIRS)), \
+ $(eval $(dir)_DEPEND_DIRS += $(filter %Sup %App, $(DIRS))))
+
+# iocBoot depends on all *App dirs
+iocBoot_DEPEND_DIRS += $(filter %App,$(DIRS))
+
+# Add any additional dependency rules here:
+
+include $(TOP)/configure/RULES_TOP
+
+ioctests:
+ .\system_tests\run_tests.bat
diff --git a/configure/CONFIG b/configure/CONFIG
new file mode 100644
index 0000000..c1a4703
--- /dev/null
+++ b/configure/CONFIG
@@ -0,0 +1,29 @@
+# CONFIG - Load build configuration data
+#
+# Do not make changes to this file!
+
+# Allow user to override where the build rules come from
+RULES = $(EPICS_BASE)
+
+# RELEASE files point to other application tops
+include $(TOP)/configure/RELEASE
+-include $(TOP)/configure/RELEASE.$(EPICS_HOST_ARCH).Common
+ifdef T_A
+-include $(TOP)/configure/RELEASE.Common.$(T_A)
+-include $(TOP)/configure/RELEASE.$(EPICS_HOST_ARCH).$(T_A)
+endif
+
+CONFIG = $(RULES)/configure
+include $(CONFIG)/CONFIG
+
+# Override the Base definition:
+INSTALL_LOCATION = $(TOP)
+
+# CONFIG_SITE files contain other build configuration settings
+include $(TOP)/configure/CONFIG_SITE
+-include $(TOP)/configure/CONFIG_SITE.$(EPICS_HOST_ARCH).Common
+ifdef T_A
+ -include $(TOP)/configure/CONFIG_SITE.Common.$(T_A)
+ -include $(TOP)/configure/CONFIG_SITE.$(EPICS_HOST_ARCH).$(T_A)
+endif
+
diff --git a/configure/CONFIG_SITE b/configure/CONFIG_SITE
new file mode 100644
index 0000000..212485e
--- /dev/null
+++ b/configure/CONFIG_SITE
@@ -0,0 +1,43 @@
+# CONFIG_SITE
+
+# Make any application-specific changes to the EPICS build
+# configuration variables in this file.
+#
+# Host/target specific settings can be specified in files named
+# CONFIG_SITE.$(EPICS_HOST_ARCH).Common
+# CONFIG_SITE.Common.$(T_A)
+# CONFIG_SITE.$(EPICS_HOST_ARCH).$(T_A)
+
+# CHECK_RELEASE controls the consistency checking of the support
+# applications pointed to by the RELEASE* files.
+# Normally CHECK_RELEASE should be set to YES.
+# Set CHECK_RELEASE to NO to disable checking completely.
+# Set CHECK_RELEASE to WARN to perform consistency checking but
+# continue building even if conflicts are found.
+CHECK_RELEASE = YES
+
+# Set this when you only want to compile this application
+# for a subset of the cross-compiled target architectures
+# that Base is built for.
+#CROSS_COMPILER_TARGET_ARCHS = vxWorks-ppc32
+
+# To install files into a location other than $(TOP) define
+# INSTALL_LOCATION here.
+#INSTALL_LOCATION=
+
+# Set this when the IOC and build host use different paths
+# to the install location. This may be needed to boot from
+# a Microsoft FTP server say, or on some NFS configurations.
+#IOCS_APPL_TOP =
+
+# For application debugging purposes, override the HOST_OPT and/
+# or CROSS_OPT settings from base/configure/CONFIG_SITE
+#HOST_OPT = NO
+#CROSS_OPT = NO
+
+# These allow developers to override the CONFIG_SITE variable
+# settings without having to modify the configure/CONFIG_SITE
+# file itself.
+-include $(TOP)/../CONFIG_SITE.local
+-include $(TOP)/configure/CONFIG_SITE.local
+
diff --git a/configure/Makefile b/configure/Makefile
new file mode 100644
index 0000000..9254309
--- /dev/null
+++ b/configure/Makefile
@@ -0,0 +1,8 @@
+TOP=..
+
+include $(TOP)/configure/CONFIG
+
+TARGETS = $(CONFIG_TARGETS)
+CONFIGS += $(subst ../,,$(wildcard $(CONFIG_INSTALLS)))
+
+include $(TOP)/configure/RULES
diff --git a/configure/RELEASE b/configure/RELEASE
new file mode 100644
index 0000000..7fc8365
--- /dev/null
+++ b/configure/RELEASE
@@ -0,0 +1,5 @@
+# optional extra local definitions here
+-include $(TOP)/configure/RELEASE.private
+
+include $(TOP)/../../../ISIS_CONFIG
+-include $(TOP)/../../../ISIS_CONFIG.$(EPICS_HOST_ARCH)
diff --git a/configure/RULES b/configure/RULES
new file mode 100644
index 0000000..6d56e14
--- /dev/null
+++ b/configure/RULES
@@ -0,0 +1,6 @@
+# RULES
+
+include $(CONFIG)/RULES
+
+# Library should be rebuilt because LIBOBJS may have changed.
+$(LIBNAME): ../Makefile
diff --git a/configure/RULES_DIRS b/configure/RULES_DIRS
new file mode 100644
index 0000000..3ba269d
--- /dev/null
+++ b/configure/RULES_DIRS
@@ -0,0 +1,2 @@
+#RULES_DIRS
+include $(CONFIG)/RULES_DIRS
diff --git a/configure/RULES_TOP b/configure/RULES_TOP
new file mode 100644
index 0000000..d09d668
--- /dev/null
+++ b/configure/RULES_TOP
@@ -0,0 +1,3 @@
+#RULES_TOP
+include $(CONFIG)/RULES_TOP
+
diff --git a/documentation/devrknmntr.html b/documentation/devrknmntr.html
new file mode 100644
index 0000000..8640236
--- /dev/null
+++ b/documentation/devrknmntr.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ rknmntr Instrument Support
+
+
+
+
+Using rknmntr instrument support in an application
+
+Several files need minor modifications to use rknmntr instrument support in
+an application.
+
+ - Add the full path to the rknmntr support directory to the
+ application configure/RELEASE file:
+ rknmntr=xxxx/modules/instrument/rknmntr/<release>
+Where <release> is the release number of of the rknmntr support.
+ - Add stream and asyn support to application database definition file
+ The application database definition file must include the database
+ definition files for the stream package and for any needed ASYN
+ drivers. There are two ways that this can be done:
+
+ - If you are building your application database definition file from
+ an xxxInclude.dbd file you include the additional database
+ definitions in that file:
+ include "base.dbd"
+ include "stream.dbd"
+ include "drvAsynIPPort.dbd"
+ - If you are building your application database definition file from
+ the application Makefile you specify the aditional database
+ definitions there:
+ xxx_DBD += base.dbd
+ xxx_DBD += stream.dbd
+ xxx_DBD += drvAsynIPPort.dbd
+
+
+ - Add the stream and asyn support libraries to the application
+ You must link the stream support library and the ASYN support library
+ with the application. Add the following lines:
+ xxx_LIBS += stream
+ xxx_LIBS += asyn
+ before the
+ xxx_LIBS += $(EPICS_BASE_IOC_LIBS)
+ in the application Makefile.
+ - Load the rknmntr support database records in the application startup script:
+ cd $(rknmntr) (cd rknmntr if using the vxWorks shell)
+ dbLoadRecords("db/devrknmntr.db,"P=<P>,R=<R>,PORT=<PORT>,A=<A>")
+ You'll have to provide appropriate values for the PV name prefixes
+ (<P> and <R>), the port name (<PORT>) and the device address
+ (<A>). The port name must match the value specified in
+ an ASYN drvxxxxxConfigure command.
+
+
+Installation and Building
+After obtaining a copy of the distribution, it must be installed and built
+for use at your site.
+
+ - Create an installation directory for the module. The path name
+ of this directory should end with modules/instrument/rknmntr.
+ - Place the distribution file into this directory.
+ - Execute the following commands:
+ cd modules/instrument/rknmntr
+ gunzip rknmntr<release>.tar.gz
+ tar xvf rknmntr<release>.tar
+ cd <release>
+Where <release> is the release number of of the rknmntr support.
+
+ - Edit the configure/RELEASE file and set the paths to your
+ installation of EPICS base, stream and ASYN support modules.
+ - Execute make in the top level directory.
+
+
+
+
+
diff --git a/rknmntrSup/Makefile b/rknmntrSup/Makefile
new file mode 100644
index 0000000..11d94da
--- /dev/null
+++ b/rknmntrSup/Makefile
@@ -0,0 +1,12 @@
+TOP=..
+include $(TOP)/configure/CONFIG
+#=======================================
+
+# Install .dbd and .db files
+DB += rknmntr.db
+DB += RIKEN_TEMP_CALC.db
+DB += test_PSU_PVs.db
+DB += test_block_curr_PVs.db
+
+#=======================================
+include $(TOP)/configure/RULES
diff --git a/rknmntrSup/RIKEN_TEMP_CALC.substitutions b/rknmntrSup/RIKEN_TEMP_CALC.substitutions
new file mode 100644
index 0000000..e2a0f56
--- /dev/null
+++ b/rknmntrSup/RIKEN_TEMP_CALC.substitutions
@@ -0,0 +1,86 @@
+# Macros:
+#
+# MAG - magnet name
+# TAP - tap name
+# GAIN - gain for the tap (needed for calculations)
+# INITIAL_RES - initial resistance (needed for the calculations)
+
+file RIKEN_TEMP_CALC.template {
+ pattern { MAGNET, TAP, GAIN, INITIAL_RES }
+
+ { "RQ1", "TAP01", "2", "7.7" }
+ { "RQ1", "TAP02", "2", "7.2" }
+ { "RQ1", "TAP03", "2", "7.1" }
+ { "RQ1", "TAP04", "2", "5.9" }
+ { "RQ1", "TAP05", "2", "6.0" }
+ { "RQ1", "TAP06", "2", "6.4" }
+ { "RQ1", "TAP07", "2", "6.5" }
+ { "RQ1", "TAP08", "2", "6.2" }
+ { "RQ1", "TAP09", "2", "5.8" }
+ { "RQ1", "TAP10", "2", "7.6" }
+ { "RQ1", "TAP11", "2", "7.0" }
+ { "RQ1", "TAP12", "2", "7.8" }
+
+ { "RQ1", "TAP13", "2", "8.0" }
+ { "RQ1", "TAP14", "2", "6.9" }
+ { "RQ1", "TAP15", "2", "6.8" }
+ { "RQ1", "TAP16", "2", "6.5" }
+ { "RQ1", "TAP17", "2", "6.2" }
+ { "RQ1", "TAP18", "2", "6.1" }
+ { "RQ1", "TAP19", "2", "6.0" }
+ { "RQ1", "TAP20", "2", "6.4" }
+ { "RQ1", "TAP21", "2", "6.3" }
+ { "RQ1", "TAP22", "2", "6.8" }
+ { "RQ1", "TAP23", "2", "7.5" }
+ { "RQ1", "TAP24", "2", "7.5" }
+
+
+ { "RQ2", "TAP01", "5", "5.5" }
+ { "RQ2", "TAP02", "5", "3.7" }
+ { "RQ2", "TAP03", "5", "5.3" }
+ { "RQ2", "TAP04", "5", "3.8" }
+ { "RQ2", "TAP05", "5", "5.2" }
+ { "RQ2", "TAP06", "5", "4.8" }
+ { "RQ2", "TAP07", "5", "5.1" }
+ { "RQ2", "TAP08", "5", "4.9" }
+ { "RQ2", "TAP09", "5", "5.5" }
+ { "RQ2", "TAP10", "5", "5.1" }
+ { "RQ2", "TAP11", "5", "4.8" }
+ { "RQ2", "TAP12", "5", "5.3" }
+
+ { "RQ2", "TAP13", "5", "3.8" }
+ { "RQ2", "TAP14", "5", "5.3" }
+ { "RQ2", "TAP15", "5", "3.7" }
+ { "RQ2", "TAP16", "5", "5.4" }
+ { "RQ2", "TAP17", "5", "6.1" }
+ { "RQ2", "TAP18", "5", "3.7" }
+ { "RQ2", "TAP19", "5", "5.3" }
+ { "RQ2", "TAP20", "5", "3.8" }
+ { "RQ2", "TAP21", "5", "5.2" }
+ { "RQ2", "TAP22", "5", "4.9" }
+ { "RQ2", "TAP23", "5", "5.0" }
+ { "RQ2", "TAP24", "5", "5.0" }
+
+ { "RQ2", "TAP25", "5", "5.8" }
+ { "RQ2", "TAP26", "5", "5.0" }
+ { "RQ2", "TAP27", "5", "4.8" }
+ { "RQ2", "TAP28", "5", "5.2" }
+ { "RQ2", "TAP29", "5", "3.8" }
+ { "RQ2", "TAP30", "5", "5.4" }
+ { "RQ2", "TAP31", "5", "3.7" }
+ { "RQ2", "TAP32", "5", "5.4" }
+
+
+ { "RB1", "TAP01", "1.25", "16.0"}
+ { "RB1", "TAP02", "1.25", "16.0"}
+ { "RB1", "TAP03", "1.25", "16.1"}
+ { "RB1", "TAP04", "1.25", "16.0"}
+ { "RB1", "TAP05", "1.25", "16.1"}
+ { "RB1", "TAP06", "1.25", "16.0"}
+ { "RB1", "TAP07", "1.25", "16.0"}
+ { "RB1", "TAP08", "1.25", "16.1"}
+ { "RB1", "TAP09", "1.25", "16.0"}
+ { "RB1", "TAP10", "1.25", "16.1"}
+ { "RB1", "TAP11", "1.25", "16.0"}
+ { "RB1", "TAP12", "1.25", "16.0"}
+}
diff --git a/rknmntrSup/RIKEN_TEMP_CALC.template b/rknmntrSup/RIKEN_TEMP_CALC.template
new file mode 100644
index 0000000..bb73b6e
--- /dev/null
+++ b/rknmntrSup/RIKEN_TEMP_CALC.template
@@ -0,0 +1,65 @@
+
+# Calculations for $(MAGNET) $(TAP)
+
+record(calc, "$(P)$(MAGNET):$(TAP):VOLT:RAW") {
+ field(DESC, "Raw Voltage at magnet $(MAGNET) at $(TAP)")
+ field(VAL, 0)
+ # This below only works if P prefix is the same machine as the one running SCHNDR IOC (must be RIKENFE)
+ field(INPA, "$(HOST)SCHNDR_01:$(MAGNET):TEMPMON:$(TAP) CP")
+ field(CALC, "A")
+ field(PREC, "2")
+ field(FLNK, "$(P)$(MAGNET):$(TAP):VOLT:ADC")
+ field(SDIS, "$(P)DISABLE")
+}
+
+record(calc, "$(P)$(MAGNET):$(TAP):VOLT:ADC") {
+ field(DESC, "Digital Voltage at magnet $(MAGNET) at $(TAP)")
+ field(VAL, 0)
+ field(INPA, "$(P)$(MAGNET):$(TAP):VOLT:RAW")
+ # The tap value is a signed 12-bit integer measured by the PLC ADC which
+ # has a 10V reference. This needs to be converted back to a voltage
+ field(CALC, "((A/((2**12)-1))*10)")
+ field(PREC, "2")
+ field(EGU, "V")
+ field(FLNK, "$(P)$(MAGNET):$(TAP):VOLT")
+ field(SDIS, "$(P)DISABLE")
+}
+
+record(calc, "$(P)$(MAGNET):$(TAP):VOLT") {
+ field(DESC, "Actual Voltage at magnet $(MAGNET) at $(TAP)")
+ field(VAL, 0)
+ field(INPA, "$(P)$(MAGNET):$(TAP):VOLT:ADC")
+ field(INPB, "$(GAIN)")
+ # The PLC measures the signal conditioned voltage and so needs to be divided
+ # by a gain to get the actual voltage at the magnet terminals
+ field(CALC, "(B#0)?A/B:0")
+ field(PREC, "2")
+ field(EGU, "V")
+ field(FLNK, "$(P)$(MAGNET):$(TAP):RES")
+ field(SDIS, "$(P)DISABLE")
+}
+
+record(calc, "$(P)$(MAGNET):$(TAP):RES") {
+ field(DESC, "Resistance at magnet $(MAGNET) at $(TAP)")
+ field(VAL, 0)
+ field(INPA, "$(P)$(MAGNET):$(TAP):VOLT")
+ # This PV below references the block corresponding to magnet so it depends on correct block configuration
+ field(INPB, "$(HOST)CS:SB:$(MAGNET)_CURR")
+ field(CALC, "(B#0)?(A/B)*1000:0") # Convert to milliohm
+ field(PREC, "2")
+ field(EGU, "mOhm")
+ field(FLNK, "$(P)$(MAGNET):$(TAP):TEMP")
+ field(SDIS, "$(P)DISABLE")
+}
+
+record(calc, "$(P)$(MAGNET):$(TAP):TEMP") {
+ field(DESC, "Temperature of magnet $(MAGNET) at $(TAP)")
+ field(VAL, 0)
+ field(INPA, "$(P)$(MAGNET):$(TAP):RES")
+ field(INPB, "$(INITIAL_RES)")
+ field(CALC, "(A#0&&B#0)?(((A/B)-1)/0.004041)+23:0")
+ field(PREC, "2")
+ field(EGU, "C")
+ info(archive, "VAL")
+ field(SDIS, "$(P)DISABLE")
+}
diff --git a/rknmntrSup/rknmntr.db b/rknmntrSup/rknmntr.db
new file mode 100644
index 0000000..a737e7f
--- /dev/null
+++ b/rknmntrSup/rknmntr.db
@@ -0,0 +1,9 @@
+record(bo, "$(P)DISABLE")
+{
+ field(DESC, "Disable comms")
+ field(PINI, "YES")
+ field(VAL, "$(DISABLE=0)")
+ field(OMSL, "supervisory")
+ field(ZNAM, "COMMS ENABLED")
+ field(ONAM, "COMMS DISABLED")
+}
diff --git a/rknmntrSup/test_PSU_PVs.substitutions b/rknmntrSup/test_PSU_PVs.substitutions
new file mode 100644
index 0000000..39e1964
--- /dev/null
+++ b/rknmntrSup/test_PSU_PVs.substitutions
@@ -0,0 +1,79 @@
+file test_PSU_PVs.template {
+ pattern { MAGNET, TAP }
+
+ { "RQ1", "TAP01" }
+ { "RQ1", "TAP02" }
+ { "RQ1", "TAP03" }
+ { "RQ1", "TAP04" }
+ { "RQ1", "TAP05" }
+ { "RQ1", "TAP06" }
+ { "RQ1", "TAP07" }
+ { "RQ1", "TAP08" }
+ { "RQ1", "TAP09" }
+ { "RQ1", "TAP10" }
+ { "RQ1", "TAP11" }
+ { "RQ1", "TAP12" }
+
+ { "RQ1", "TAP13" }
+ { "RQ1", "TAP14" }
+ { "RQ1", "TAP15" }
+ { "RQ1", "TAP16" }
+ { "RQ1", "TAP17" }
+ { "RQ1", "TAP18" }
+ { "RQ1", "TAP19" }
+ { "RQ1", "TAP20" }
+ { "RQ1", "TAP21" }
+ { "RQ1", "TAP22" }
+ { "RQ1", "TAP23" }
+ { "RQ1", "TAP24" }
+
+
+ { "RQ2", "TAP01" }
+ { "RQ2", "TAP02" }
+ { "RQ2", "TAP03" }
+ { "RQ2", "TAP04" }
+ { "RQ2", "TAP05" }
+ { "RQ2", "TAP06" }
+ { "RQ2", "TAP07" }
+ { "RQ2", "TAP08" }
+ { "RQ2", "TAP09" }
+ { "RQ2", "TAP10" }
+ { "RQ2", "TAP11" }
+ { "RQ2", "TAP12" }
+
+ { "RQ2", "TAP13" }
+ { "RQ2", "TAP14" }
+ { "RQ2", "TAP15" }
+ { "RQ2", "TAP16" }
+ { "RQ2", "TAP17" }
+ { "RQ2", "TAP18" }
+ { "RQ2", "TAP19" }
+ { "RQ2", "TAP20" }
+ { "RQ2", "TAP21" }
+ { "RQ2", "TAP22" }
+ { "RQ2", "TAP23" }
+ { "RQ2", "TAP24" }
+
+ { "RQ2", "TAP25" }
+ { "RQ2", "TAP26" }
+ { "RQ2", "TAP27" }
+ { "RQ2", "TAP28" }
+ { "RQ2", "TAP29" }
+ { "RQ2", "TAP30" }
+ { "RQ2", "TAP31" }
+ { "RQ2", "TAP32" }
+
+
+ { "RB1", "TAP01" }
+ { "RB1", "TAP02" }
+ { "RB1", "TAP03" }
+ { "RB1", "TAP04" }
+ { "RB1", "TAP05" }
+ { "RB1", "TAP06" }
+ { "RB1", "TAP07" }
+ { "RB1", "TAP08" }
+ { "RB1", "TAP09" }
+ { "RB1", "TAP10" }
+ { "RB1", "TAP11" }
+ { "RB1", "TAP12" }
+}
diff --git a/rknmntrSup/test_PSU_PVs.template b/rknmntrSup/test_PSU_PVs.template
new file mode 100644
index 0000000..459358e
--- /dev/null
+++ b/rknmntrSup/test_PSU_PVs.template
@@ -0,0 +1,6 @@
+# Dummy records for testing $(MAGNET), $(TAP)
+
+record(ai, "$(P)SCHNDR_01:$(MAGNET):TEMPMON:$(TAP)") {
+ field(DESC, "Dummy PV for testing")
+}
+
diff --git a/rknmntrSup/test_block_curr_PVs.substitutions b/rknmntrSup/test_block_curr_PVs.substitutions
new file mode 100644
index 0000000..185ff1c
--- /dev/null
+++ b/rknmntrSup/test_block_curr_PVs.substitutions
@@ -0,0 +1,7 @@
+file test_block_curr_PVs.template {
+ pattern { MAGNET }
+
+ { "RQ1" }
+ { "RQ2" }
+ { "RB1" }
+}
diff --git a/rknmntrSup/test_block_curr_PVs.template b/rknmntrSup/test_block_curr_PVs.template
new file mode 100644
index 0000000..4921518
--- /dev/null
+++ b/rknmntrSup/test_block_curr_PVs.template
@@ -0,0 +1,4 @@
+
+record(ai, "$(P)CS:SB:$(MAGNET)_CURR") {
+ field(DESC, "Dummy PV for testing")
+}
diff --git a/system_tests/lewis_emulators/Rknmntr/__init__.py b/system_tests/lewis_emulators/Rknmntr/__init__.py
new file mode 100644
index 0000000..ab27daf
--- /dev/null
+++ b/system_tests/lewis_emulators/Rknmntr/__init__.py
@@ -0,0 +1,5 @@
+from .device import SimulatedRknmntr
+from ..lewis_versions import LEWIS_LATEST
+
+framework_version = LEWIS_LATEST
+__all__ = ['SimulatedRknmntr']
diff --git a/system_tests/lewis_emulators/Rknmntr/device.py b/system_tests/lewis_emulators/Rknmntr/device.py
new file mode 100644
index 0000000..62060ed
--- /dev/null
+++ b/system_tests/lewis_emulators/Rknmntr/device.py
@@ -0,0 +1,25 @@
+from collections import OrderedDict
+from .states import DefaultState
+from lewis.devices import StateMachineDevice
+
+
+class SimulatedRknmntr(StateMachineDevice):
+
+ def _initialize_data(self):
+ """
+ Initialize all of the device's attributes.
+ """
+ pass
+
+ def _get_state_handlers(self):
+ return {
+ 'default': DefaultState(),
+ }
+
+ def _get_initial_state(self):
+ return 'default'
+
+ def _get_transition_handlers(self):
+ return OrderedDict([
+ ])
+
diff --git a/system_tests/lewis_emulators/Rknmntr/interfaces/__init__.py b/system_tests/lewis_emulators/Rknmntr/interfaces/__init__.py
new file mode 100644
index 0000000..2425422
--- /dev/null
+++ b/system_tests/lewis_emulators/Rknmntr/interfaces/__init__.py
@@ -0,0 +1,3 @@
+from .stream_interface import RknmntrStreamInterface
+
+__all__ = ['RknmntrStreamInterface']
diff --git a/system_tests/lewis_emulators/Rknmntr/interfaces/stream_interface.py b/system_tests/lewis_emulators/Rknmntr/interfaces/stream_interface.py
new file mode 100644
index 0000000..a5676f2
--- /dev/null
+++ b/system_tests/lewis_emulators/Rknmntr/interfaces/stream_interface.py
@@ -0,0 +1,32 @@
+from lewis.adapters.stream import StreamInterface, Cmd
+from lewis.utils.command_builder import CmdBuilder
+from lewis.core.logging import has_log
+from lewis.utils.replies import conditional_reply
+
+
+@has_log
+class RknmntrStreamInterface(StreamInterface):
+
+ in_terminator = "\r\n"
+ out_terminator = "\r\n"
+
+ def __init__(self):
+ super(RknmntrStreamInterface, self).__init__()
+ # Commands that we expect via serial during normal operation
+ self.commands = {
+ CmdBuilder(self.catch_all).arg("^#9.*$").build() # Catch-all command for debugging
+ }
+
+ def handle_error(self, request, error):
+ """
+ If command is not recognised print and error
+
+ Args:
+ request: requested string
+ error: problem
+
+ """
+ self.log.error("An error occurred at request " + repr(request) + ": " + repr(error))
+
+ def catch_all(self, command):
+ pass
diff --git a/system_tests/lewis_emulators/Rknmntr/states.py b/system_tests/lewis_emulators/Rknmntr/states.py
new file mode 100644
index 0000000..e4ca48e
--- /dev/null
+++ b/system_tests/lewis_emulators/Rknmntr/states.py
@@ -0,0 +1,5 @@
+from lewis.core.statemachine import State
+
+
+class DefaultState(State):
+ pass
diff --git a/system_tests/lewis_emulators/__init__.py b/system_tests/lewis_emulators/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/system_tests/lewis_emulators/lewis_versions.py b/system_tests/lewis_emulators/lewis_versions.py
new file mode 100644
index 0000000..0f6c45a
--- /dev/null
+++ b/system_tests/lewis_emulators/lewis_versions.py
@@ -0,0 +1,2 @@
+LEWIS_1_3_0 = "1.3.0"
+LEWIS_LATEST = LEWIS_1_3_0
diff --git a/system_tests/run_tests.bat b/system_tests/run_tests.bat
new file mode 100644
index 0000000..56f9c25
--- /dev/null
+++ b/system_tests/run_tests.bat
@@ -0,0 +1,13 @@
+@echo off
+REM Run this directory's tests using the IOC Testing Framework
+
+SET CurrentDir=%~dp0
+
+call "%~dp0..\..\..\..\config_env.bat"
+
+set "PYTHONUNBUFFERED=1"
+
+REM Command line arguments always passed to the test script
+SET ARGS=--test_and_emulator %~dp0
+call %PYTHON3% "%EPICS_KIT_ROOT%\support\IocTestFramework\master\run_tests.py" %ARGS% %*
+IF %ERRORLEVEL% NEQ 0 EXIT /b %errorlevel%
diff --git a/system_tests/tests/__init__.py b/system_tests/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/system_tests/tests/rknmntr.py b/system_tests/tests/rknmntr.py
new file mode 100644
index 0000000..764407e
--- /dev/null
+++ b/system_tests/tests/rknmntr.py
@@ -0,0 +1,95 @@
+import unittest
+
+from utils.channel_access import ChannelAccess
+from utils.ioc_launcher import get_default_ioc_dir
+from utils.test_modes import TestModes
+from utils.testing import get_running_lewis_and_ioc, parameterized_list
+
+from parameterized import parameterized
+import random
+
+DEVICE_PREFIX = "RKNMNTR_01"
+
+
+IOCS = [
+ {
+ "name": DEVICE_PREFIX,
+ "directory": get_default_ioc_dir("RKNMNTR"),
+ "macros": {},
+ "emulator": "Rknmntr",
+ },
+]
+
+
+TEST_MODES = [TestModes.RECSIM]
+
+MAGNET_TAP_PAIRS = {
+ "RQ1": [f"TAP{i:02d}" for i in range(1, 25)],
+ "RQ2": [f"TAP{i:02d}" for i in range(1, 25)],
+ "RB1": [f"TAP{i:02d}" for i in range(1, 13)],
+}
+
+class RknmntrTests(unittest.TestCase):
+ """
+ Tests for the Rknmntr IOC.
+ """
+ def setUp(self):
+ self._lewis, self._ioc = get_running_lewis_and_ioc("Rknmntr", DEVICE_PREFIX)
+ self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
+
+ def _get_pv_for_magnet_tap(self, magnet, tap):
+ return f"{magnet}:{tap}:"
+
+ def test_GIVEN_ioc_running_THEN_all_pvs_exist(self):
+ for magnet in MAGNET_TAP_PAIRS:
+ for tap in MAGNET_TAP_PAIRS[magnet]:
+ pv_magnet_tap = self._get_pv_for_magnet_tap(magnet, tap)
+ self.ca.assert_that_pv_exists(f"{pv_magnet_tap}VOLT:RAW")
+ self.ca.assert_that_pv_exists(f"{pv_magnet_tap}VOLT:ADC")
+ self.ca.assert_that_pv_exists(f"{pv_magnet_tap}VOLT")
+ self.ca.assert_that_pv_exists(f"{pv_magnet_tap}RES")
+ self.ca.assert_that_pv_exists(f"{pv_magnet_tap}TEMP")
+
+ @parameterized.expand(parameterized_list([
+ "RQ1", "RQ2", "RB1"
+ ]))
+ def test_WHEN_raw_voltage_THEN_values_calculated(self, _, magnet):
+ # Remove IOC prefix from prefix, leaving only the host machine prefix
+ self.ca.prefix = self.ca.host_prefix
+
+ # Set magnet current to a non-zero value
+ pv_magnet_curr = f"CS:SB:{magnet}_CURR"
+ self.ca.set_pv_value(pv_magnet_curr, 1)
+
+ for tap in MAGNET_TAP_PAIRS[magnet]:
+ # WHEN
+ # Simulate a raw voltage on each tap
+ volt_raw = random.randrange(10, 1000)
+ pv = f"SCHNDR_01:{magnet}:TEMPMON:{tap}"
+ self.ca.set_pv_value(pv, volt_raw)
+
+ pv_magnet_tap = f"RKNMNTR_01:{magnet}:{tap}:"
+ pv_raw = f"{pv_magnet_tap}VOLT:RAW"
+ pv_volt_adc = f"{pv_magnet_tap}VOLT:ADC"
+ pv_volt = f"{pv_magnet_tap}VOLT"
+ pv_res = f"{pv_magnet_tap}RES"
+ pv_temp = f"{pv_magnet_tap}TEMP"
+
+ gain = self.ca.get_pv_value(f"{pv_volt}.B") # Retrieve gain for magnet from calc record B field (loaded in there from macro)
+ curr = self.ca.get_pv_value(pv_magnet_curr)
+ initial_res = self.ca.get_pv_value(f"{pv_temp}.B") # Retrieve initial resistance for magnet from calc record B field (loaded in there from macro)
+
+ #These calculations convert raw/analogue voltages into digital. The conversion calculations were provided by instrument scientists,
+ #and can be found here: https://github.com/ISISComputingGroup/IBEX/issues/7975
+ expected_volt_adc = volt_raw / ((2**12)-1)*10
+ expected_volt = expected_volt_adc / gain
+ expected_res = expected_volt / curr * 1000
+ expected_temp = (((expected_res / initial_res) - 1) / 0.004041) + 23
+
+ # ASSERT
+ # That calculations all happen
+ self.ca.assert_that_pv_is(pv_raw, volt_raw)
+ self.ca.assert_that_pv_is(pv_volt_adc, expected_volt_adc)
+ self.ca.assert_that_pv_is(pv_volt, expected_volt)
+ self.ca.assert_that_pv_is(pv_res, expected_res)
+ self.ca.assert_that_pv_is(pv_temp, expected_temp)