Repository: DiamondLightSource/pythonSoftIOC (language: python)

Issue:
Waveforms with empty array initial values do not update as expected
When using a `WaveformOut` record and doing `.set()` and `.get()`, the behaviour is not as expected when the `initial_value` is an empty numpy array:

```
# Import the basic framework components.
from softioc import softioc, builder, asyncio_dispatcher
import numpy
# Create an asyncio dispatcher, the event loop is now running
dispatcher = asyncio_dispatcher.AsyncioDispatcher()

# Set the record prefix
builder.SetDeviceName("PREFIX")

# Create some records
wo = builder.WaveformOut("TEST", initial_value=numpy.array([]))

# Boilerplate get the IOC started
builder.LoadDatabase()
softioc.iocInit(dispatcher)

print(wo.get())
wo.set(numpy.array([10]))
print(wo.get())

# Finally leave the IOC running with an interactive shell.
softioc.interactive_ioc(globals())

```

This program prints:
```
...
iocRun: All initialization complete
[]
[]
```


If you change the `initial_value` to `numpy.array([1])` then the program prints out the expected results:
```
...
iocRun: All initialization complete
[1]
[10]
```

Apologies if this is a duplicate of an existing issue, but it certainly is a novel symptom.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml
index 3d90a6c2..df3ff6c0 100644
--- a/.github/workflows/code.yml
+++ b/.github/workflows/code.yml
@@ -2,7 +2,15 @@ name: Code CI
 
 on:
   push:
+    branches:
+      - master
+      - main
+    tags:
+      - "*"
   pull_request:
+  schedule:
+    # Run every Monday at 8am
+    - cron: '0 8 * * MON'
 
 jobs:
   lint:
@@ -30,15 +38,18 @@ jobs:
         python: [cp36, cp37, cp38, cp39]
 
         include:
-          # Put coverage in the project directory for mac
+          # Put coverage and results files in the project directory for mac
           - os: macos-latest
             cov_file: "{project}/dist/coverage.xml"
+            results_file: "{project}/dist/pytest-results.xml"
           # And for windows
           - os: windows-latest
             cov_file: "{project}/dist/coverage.xml"
-          # But put coverage in the output dir mounted in docker for linux
+            results_file: "{project}/dist/pytest-results.xml"
+          # But put coverage and results files in the output dir mounted in docker for linux
           - os: ubuntu-latest
             cov_file: /output/coverage.xml
+            results_file: /output/pytest-results.xml
           # Build an sdist on linux so it has the right line endings
           - os: ubuntu-latest
             sdist: true
@@ -60,7 +71,8 @@ jobs:
           python-version: "3.7"
 
       - name: Install Python Dependencies
-        run: pip install build cibuildwheel==1.* # The 2.* releases dropped Python2 support.
+        # Pin cibuildwheel due to https://github.com/pypa/cibuildwheel/issues/962
+        run: pip install build cibuildwheel>=2.3.1
 
       - name: Build Sdist
         if: matrix.sdist
@@ -69,14 +81,17 @@ jobs:
       - name: Build Wheel
         run: cibuildwheel --output-dir dist
         env:
+          # Force old behaviour of pip - see https://github.com/pypa/cibuildwheel/issues/962
+          CIBW_ENVIRONMENT: PIP_USE_DEPRECATED=out-of-tree-build
           CIBW_BUILD: ${{ matrix.python }}*64
           CIBW_TEST_EXTRAS: dev
-          CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }}
+          CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{ matrix.results_file }}
           # Disable auditwheel as it isn't compatible with setuptools_dso approach
           # https://github.com/mdavidsaver/setuptools_dso/issues/17
           CIBW_REPAIR_WHEEL_COMMAND: ""
-          CIBW_MANYLINUX_X86_64_IMAGE: manylinux1
-          CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64
+          CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
+          CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux2014_x86_64
+          CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms
 
       - name: Upload Wheel and Sdist
         uses: actions/upload-artifact@v2
@@ -90,6 +105,30 @@ jobs:
           name: ${{ matrix.os }}/${{ matrix.python }}
           directory: dist
 
+      - name: Upload Unit Test Results
+        if: always()
+        uses: actions/upload-artifact@v2
+        with:
+          name: Unit Test Results (${{ matrix.os }}-${{ matrix.python }})
+          path: dist/pytest-results.xml
+
+  publish-test-results:
+    name: Publish Unit Tests Results
+    needs: build
+    runs-on: ubuntu-latest
+    if: always()
+
+    steps:
+      - name: Download Artifacts
+        uses: actions/download-artifact@v2
+        with:
+          path: artifacts
+
+      - name: Publish Unit Test Results
+        uses: EnricoMi/publish-unit-test-result-action@v1
+        with:
+          files: artifacts/**/*.xml
+
   release:
     needs: [build]
     runs-on: ubuntu-latest
@@ -115,4 +154,4 @@ jobs:
         env:
           TWINE_USERNAME: __token__
           TWINE_PASSWORD: ${{ secrets.pypi_token }}
-        run: pipx run twine upload dist/*
\ No newline at end of file
+        run: pipx run twine upload dist/*
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 10e35589..e3866cf4 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -2,6 +2,12 @@ name: Docs CI
 
 on:
   push:
+    branches:
+      # Add more branches here to publish docs from other branches
+      - master
+      - main
+    tags:
+      - "*"
   pull_request:
 
 jobs:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index cdadaac3..d6c29451 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -13,6 +13,10 @@ Changed:
 
 - `Remove python2 support <../../pull/64>`
 - `Default DISP to TRUE for all In records <../../pull/74>`
+- `Overhaul record value setting/getting <../../pull/60>`
+
+Added:
+- New longStringIn/longStringOut record types
 
 3.2.1_ - 2021-11-25
 -------------------
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 2606bfb3..a9ba7ece 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -26,6 +26,21 @@ While 100% code coverage does not make a library bug-free, it significantly
 reduces the number of easily caught bugs! Please make sure coverage remains the
 same or is improved by a pull request!
 
+
+Updating Dependencies
+---------------------
+
+pythonSoftIoc has a variety of types of dependencies, from Python module to
+C files and built libraries. If it is necessary to update any of them, the
+safest way to do that is::
+
+    $ Ensure all changes are committed/saved/backed up
+    $ pipenv --rm
+    $ pipenv run gitclean
+    $ rm Pipfile.lock
+    $ Update dependencies in setup.cfg / pyproject.toml
+    $ pipenv install --dev
+
 Code Styling
 ------------
 
diff --git a/Pipfile.lock b/Pipfile.lock
index a057c09d..1794c8eb 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -16,10 +16,10 @@
     "default": {
         "aioca": {
             "hashes": [
-                "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0",
-                "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae"
+                "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae",
+                "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d"
             ],
-            "version": "==1.2"
+            "version": "==1.3"
         },
         "cothread": {
             "hashes": [
@@ -29,69 +29,78 @@
         },
         "epicscorelibs": {
             "hashes": [
-                "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870",
-                "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c",
-                "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800",
-                "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4",
-                "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1",
-                "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be",
-                "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88",
-                "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab",
-                "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c",
-                "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351",
-                "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120",
-                "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3",
-                "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4",
-                "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92",
-                "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb",
-                "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0",
-                "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3",
-                "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358",
-                "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76",
-                "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3",
-                "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131",
-                "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552",
-                "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd",
-                "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681",
-                "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4"
-            ],
-            "version": "==7.0.6.99.1.0"
+                "sha256:01d45ae25d844afe71893838f36ea649ccc3e1e72a96076b33242437efb1ce13",
+                "sha256:0269ba469aa8117d86e865b09e9f3b3afdb891f5f544a90d8af037039015b995",
+                "sha256:1c6f00360ab2e89081c031ff499f5695bd687102a561160cf6304b64cf325ca5",
+                "sha256:1cbed1cb9b7fa3744723dfb78a1aaa7ff9d475e9dee6e6e21ebb1e682421cd35",
+                "sha256:223515de1da16fd7a06892e41a1e5bc870b57bbf1a0a4ac3dc3e069b647d7313",
+                "sha256:329179971288722eb7f7d085fced198ff30468cc225d9677f08175074680b4ef",
+                "sha256:32f91502afa9639fe576fa5bfa137352ee04f929d2887a2c7c169c24c0658808",
+                "sha256:3c8958b7c3385dc190948af429a7c10d18e71142d749776250341e1db6b3d05b",
+                "sha256:3eb16a21760110e0d159e7ee0f97a36250a83abc0ca6ec08806d4ad148c48761",
+                "sha256:3f07babd8ff1c7499cbe9f95dd80a8cbee45f9f38bda2f6c72855c4c647d2dc7",
+                "sha256:5766a0dafcd02af7ecead064d5502282eac1c66afdddf7235ae822228eedac11",
+                "sha256:5a18ab11d8a565b9ea9e035218b5ac44b86d21162d7260ecb54fad43f506d70f",
+                "sha256:64dd24d80b8dd1bca73ce8ffe0673306cf7018dba61883bd1b7b4ff179ac0623",
+                "sha256:773d51ade8a9c44333a56aae1224afe4c238750643c5a05a461186d9ce7f4cf6",
+                "sha256:86921315b855993c0316eb938586c8b99acc21ba5ed26ac0cb66d32e1682f89b",
+                "sha256:941643c2e90665305deddc4a7fe99dceb5488c80d1c31bd4a96169a92adea73f",
+                "sha256:9a4b401e22a96b9cacc2429f67179fed4a1414e5629f78865d6c63d92ec19439",
+                "sha256:9be6cf03ad466812cd659fa2a6af1c8efd33946abdf0a2c5f8901df2289f2dda",
+                "sha256:a9e07998cca913e6cf5f448c37dc1c2f29acc5fec4aca19ed80cd127b65e5d69",
+                "sha256:ac260dbd4b28a53c8ebac4f902d03448432b46d3fff7809488a63263b7efbd05",
+                "sha256:b0d606a0c0c020387136b1b19091d661b1b2f5763377d6a0b13152351f42ad53",
+                "sha256:b64b38b4f48601054a7bd0423d1559c1a2281cb962fd4cd4c1088f49ac29b2b7",
+                "sha256:b69e91fe38f818a71fd4fbf22ac17d577bdce914d4940883586e57d629030328",
+                "sha256:c3deebf918866f5bdd4fbc5f52b1c89c96121619573698442876eecdf1f9a985",
+                "sha256:cbb5a14534e68eb2a10904ba020191c858860f2dfebbdff969d160a29020724e",
+                "sha256:d729c35c29681acc3b666e59f041e6bd228c931b8303507bb046e331e5f11f67",
+                "sha256:f165e4754249ee12bd38ba68ed72dc27764b28a02654c224c690f68b95519455",
+                "sha256:f40ae0290be4d84c560f848f4b0340b3c9ff466bd52fcbd11899199fa0be9b4b"
+            ],
+            "version": "==7.0.6.99.2.0"
         },
         "epicsdbbuilder": {
             "hashes": [
-                "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308",
-                "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c"
+                "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283",
+                "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74"
             ],
-            "version": "==1.4.3"
+            "version": "==1.5"
         },
         "numpy": {
             "hashes": [
-                "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727",
-                "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6",
-                "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98",
-                "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7",
-                "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d",
-                "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2",
-                "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9",
-                "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935",
-                "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff",
-                "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee",
-                "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb",
-                "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042",
-                "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3",
-                "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5",
-                "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6",
-                "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f",
-                "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4",
-                "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737",
-                "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931",
-                "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6",
-                "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677",
-                "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576",
-                "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935",
-                "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd"
-            ],
-            "version": "==1.20.2"
+                "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb",
+                "sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939",
+                "sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc",
+                "sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced",
+                "sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441",
+                "sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b",
+                "sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166",
+                "sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640",
+                "sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13",
+                "sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8",
+                "sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5",
+                "sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611",
+                "sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76",
+                "sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c",
+                "sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7",
+                "sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a",
+                "sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5",
+                "sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee",
+                "sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449",
+                "sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02",
+                "sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a",
+                "sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815",
+                "sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593",
+                "sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e",
+                "sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa",
+                "sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954",
+                "sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354",
+                "sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9",
+                "sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a",
+                "sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089"
+            ],
+            "version": "==1.21.5"
         },
         "scipy": {
             "hashes": [
@@ -133,20 +142,19 @@
         },
         "typing-extensions": {
             "hashes": [
-                "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
-                "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
-                "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
+                "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
+                "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
             ],
-            "version": "==3.10.0.0"
+            "version": "==4.0.1"
         }
     },
     "develop": {
         "aioca": {
             "hashes": [
-                "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0",
-                "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae"
+                "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae",
+                "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d"
             ],
-            "version": "==1.2"
+            "version": "==1.3"
         },
         "alabaster": {
             "hashes": [
@@ -171,17 +179,18 @@
         },
         "certifi": {
             "hashes": [
-                "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
-                "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
+                "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
+                "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
             ],
-            "version": "==2021.5.30"
+            "version": "==2021.10.8"
         },
-        "chardet": {
+        "charset-normalizer": {
             "hashes": [
-                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
-                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
+                "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0",
+                "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"
             ],
-            "version": "==4.0.0"
+            "markers": "python_version >= '3'",
+            "version": "==2.0.7"
         },
         "cothread": {
             "hashes": [
@@ -190,119 +199,120 @@
             "version": "==2.17"
         },
         "coverage": {
-            "hashes": [
-                "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
-                "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
-                "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
-                "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
-                "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
-                "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
-                "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
-                "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
-                "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
-                "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
-                "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
-                "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
-                "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
-                "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
-                "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
-                "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
-                "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
-                "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
-                "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
-                "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
-                "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
-                "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
-                "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
-                "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
-                "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
-                "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
-                "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
-                "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
-                "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
-                "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
-                "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
-                "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
-                "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
-                "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
-                "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
-                "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
-                "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
-                "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
-                "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
-                "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
-                "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
-                "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
-                "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
-                "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
-                "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
-                "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
-                "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
-                "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
-                "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
-                "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
-                "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
-                "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
-            ],
-            "version": "==5.5"
+            "extras": [
+                "toml"
+            ],
+            "hashes": [
+                "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9",
+                "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b",
+                "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e",
+                "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a",
+                "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2",
+                "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67",
+                "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685",
+                "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f",
+                "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7",
+                "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717",
+                "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5",
+                "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b",
+                "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef",
+                "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489",
+                "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b",
+                "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e",
+                "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926",
+                "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271",
+                "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54",
+                "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026",
+                "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad",
+                "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533",
+                "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6",
+                "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8",
+                "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26",
+                "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b",
+                "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8",
+                "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9",
+                "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271",
+                "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003",
+                "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a",
+                "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0",
+                "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d",
+                "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a",
+                "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0",
+                "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9",
+                "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f",
+                "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33",
+                "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a",
+                "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6",
+                "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201",
+                "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336",
+                "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce",
+                "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4",
+                "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9",
+                "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07"
+            ],
+            "version": "==6.1.1"
         },
         "docutils": {
             "hashes": [
-                "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
-                "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
+                "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
+                "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
             ],
-            "version": "==0.16"
+            "version": "==0.17.1"
         },
         "epicscorelibs": {
             "hashes": [
-                "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870",
-                "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c",
-                "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800",
-                "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4",
-                "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1",
-                "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be",
-                "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88",
-                "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab",
-                "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c",
-                "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351",
-                "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120",
-                "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3",
-                "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4",
-                "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92",
-                "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb",
-                "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0",
-                "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3",
-                "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358",
-                "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76",
-                "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3",
-                "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131",
-                "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552",
-                "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd",
-                "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681",
-                "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4"
-            ],
-            "version": "==7.0.6.99.1.0"
+                "sha256:01d45ae25d844afe71893838f36ea649ccc3e1e72a96076b33242437efb1ce13",
+                "sha256:0269ba469aa8117d86e865b09e9f3b3afdb891f5f544a90d8af037039015b995",
+                "sha256:1c6f00360ab2e89081c031ff499f5695bd687102a561160cf6304b64cf325ca5",
+                "sha256:1cbed1cb9b7fa3744723dfb78a1aaa7ff9d475e9dee6e6e21ebb1e682421cd35",
+                "sha256:223515de1da16fd7a06892e41a1e5bc870b57bbf1a0a4ac3dc3e069b647d7313",
+                "sha256:329179971288722eb7f7d085fced198ff30468cc225d9677f08175074680b4ef",
+                "sha256:32f91502afa9639fe576fa5bfa137352ee04f929d2887a2c7c169c24c0658808",
+                "sha256:3c8958b7c3385dc190948af429a7c10d18e71142d749776250341e1db6b3d05b",
+                "sha256:3eb16a21760110e0d159e7ee0f97a36250a83abc0ca6ec08806d4ad148c48761",
+                "sha256:3f07babd8ff1c7499cbe9f95dd80a8cbee45f9f38bda2f6c72855c4c647d2dc7",
+                "sha256:5766a0dafcd02af7ecead064d5502282eac1c66afdddf7235ae822228eedac11",
+                "sha256:5a18ab11d8a565b9ea9e035218b5ac44b86d21162d7260ecb54fad43f506d70f",
+                "sha256:64dd24d80b8dd1bca73ce8ffe0673306cf7018dba61883bd1b7b4ff179ac0623",
+                "sha256:773d51ade8a9c44333a56aae1224afe4c238750643c5a05a461186d9ce7f4cf6",
+                "sha256:86921315b855993c0316eb938586c8b99acc21ba5ed26ac0cb66d32e1682f89b",
+                "sha256:941643c2e90665305deddc4a7fe99dceb5488c80d1c31bd4a96169a92adea73f",
+                "sha256:9a4b401e22a96b9cacc2429f67179fed4a1414e5629f78865d6c63d92ec19439",
+                "sha256:9be6cf03ad466812cd659fa2a6af1c8efd33946abdf0a2c5f8901df2289f2dda",
+                "sha256:a9e07998cca913e6cf5f448c37dc1c2f29acc5fec4aca19ed80cd127b65e5d69",
+                "sha256:ac260dbd4b28a53c8ebac4f902d03448432b46d3fff7809488a63263b7efbd05",
+                "sha256:b0d606a0c0c020387136b1b19091d661b1b2f5763377d6a0b13152351f42ad53",
+                "sha256:b64b38b4f48601054a7bd0423d1559c1a2281cb962fd4cd4c1088f49ac29b2b7",
+                "sha256:b69e91fe38f818a71fd4fbf22ac17d577bdce914d4940883586e57d629030328",
+                "sha256:c3deebf918866f5bdd4fbc5f52b1c89c96121619573698442876eecdf1f9a985",
+                "sha256:cbb5a14534e68eb2a10904ba020191c858860f2dfebbdff969d160a29020724e",
+                "sha256:d729c35c29681acc3b666e59f041e6bd228c931b8303507bb046e331e5f11f67",
+                "sha256:f165e4754249ee12bd38ba68ed72dc27764b28a02654c224c690f68b95519455",
+                "sha256:f40ae0290be4d84c560f848f4b0340b3c9ff466bd52fcbd11899199fa0be9b4b"
+            ],
+            "version": "==7.0.6.99.2.0"
         },
         "epicsdbbuilder": {
             "hashes": [
-                "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308",
-                "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c"
+                "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283",
+                "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74"
             ],
-            "version": "==1.4.3"
+            "version": "==1.5"
         },
         "flake8": {
             "hashes": [
-                "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378",
-                "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"
+                "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
+                "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
             ],
-            "version": "==3.9.1"
+            "version": "==4.0.1"
         },
         "idna": {
             "hashes": [
-                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
-                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+                "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
+                "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
             ],
-            "version": "==2.10"
+            "markers": "python_version >= '3'",
+            "version": "==3.3"
         },
         "imagesize": {
             "hashes": [
@@ -313,11 +323,11 @@
         },
         "importlib-metadata": {
             "hashes": [
-                "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb",
-                "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33"
+                "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581",
+                "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"
             ],
             "markers": "python_version < '3.8' and python_version < '3.8'",
-            "version": "==4.6.0"
+            "version": "==4.0.1"
         },
         "iniconfig": {
             "hashes": [
@@ -328,67 +338,69 @@
         },
         "jinja2": {
             "hashes": [
-                "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
-                "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
+                "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45",
+                "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"
             ],
-            "version": "==2.11.3"
+            "version": "==3.0.2"
         },
         "markupsafe": {
             "hashes": [
-                "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
-                "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
-                "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
-                "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
-                "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
-                "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
-                "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
-                "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
-                "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
-                "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
-                "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
-                "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
-                "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
-                "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
-                "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
-                "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
-                "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
-                "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
-                "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
-                "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
-                "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
-                "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
-                "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
-                "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
-                "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
-                "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
-                "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
-                "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
-                "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
-                "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
-                "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
-                "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
-                "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
-                "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
-                "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
-                "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
-                "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
-                "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
-                "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
-                "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
-                "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
-                "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
-                "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
-                "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
-                "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
-                "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
-                "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
-                "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
-                "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
-                "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
-                "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
-                "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
-            ],
-            "version": "==1.1.1"
+                "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
+                "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
+                "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
+                "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
+                "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
+                "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
+                "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
+                "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
+                "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
+                "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
+                "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
+                "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
+                "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
+                "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38",
+                "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac",
+                "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
+                "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6",
+                "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
+                "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
+                "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
+                "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
+                "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a",
+                "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
+                "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9",
+                "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864",
+                "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
+                "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
+                "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
+                "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
+                "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
+                "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b",
+                "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
+                "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
+                "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
+                "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
+                "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
+                "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
+                "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
+                "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
+                "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
+                "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
+                "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145",
+                "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
+                "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
+                "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
+                "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
+                "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
+                "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
+                "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
+                "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
+                "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
+                "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
+                "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
+                "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
+            ],
+            "version": "==2.0.1"
         },
         "mccabe": {
             "hashes": [
@@ -397,86 +409,91 @@
             ],
             "version": "==0.6.1"
         },
-        "nose": {
+        "nose2": {
             "hashes": [
-                "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac",
-                "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a",
-                "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"
+                "sha256:886ba617a96de0130c54b24479bd5c2d74d5c940d40f3809c3a275511a0c4a60",
+                "sha256:aa620e759f2c5018d9ba041340391913e282ecebd3c392027f1575847b093ec6"
             ],
-            "version": "==1.3.7"
+            "version": "==0.10.0"
         },
         "numpy": {
             "hashes": [
-                "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727",
-                "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6",
-                "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98",
-                "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7",
-                "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d",
-                "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2",
-                "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9",
-                "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935",
-                "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff",
-                "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee",
-                "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb",
-                "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042",
-                "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3",
-                "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5",
-                "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6",
-                "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f",
-                "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4",
-                "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737",
-                "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931",
-                "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6",
-                "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677",
-                "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576",
-                "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935",
-                "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd"
-            ],
-            "version": "==1.20.2"
+                "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb",
+                "sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939",
+                "sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc",
+                "sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced",
+                "sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441",
+                "sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b",
+                "sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166",
+                "sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640",
+                "sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13",
+                "sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8",
+                "sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5",
+                "sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611",
+                "sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76",
+                "sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c",
+                "sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7",
+                "sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a",
+                "sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5",
+                "sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee",
+                "sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449",
+                "sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02",
+                "sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a",
+                "sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815",
+                "sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593",
+                "sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e",
+                "sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa",
+                "sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954",
+                "sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354",
+                "sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9",
+                "sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a",
+                "sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089"
+            ],
+            "version": "==1.21.5"
         },
         "p4p": {
             "hashes": [
-                "sha256:0143a3a3d29bf03f7c828c28d025db81961e9933c750bd9d3d8e8b59cb8b71a3",
-                "sha256:160aa3dd5ca55583e929bcc939e0479dfca655fba440dc9b931373e733848959",
-                "sha256:1c688e35e30294c79bfebbda8c460ffb5486cdb64ccbeae93060e6de667942a4",
-                "sha256:1d58c9149b85e0fd19e00167fb047a04c1e649d394cf8fdcb7a172be73de3f6d",
-                "sha256:25e98f73ae4cebc3c305f2b11b0e6689d002594fda9330bf99c6414d0a01753f",
-                "sha256:2b4e193737cb69a20fbed2f86360275ce5dcce5ca83439e1f98b3af5f1aed1cc",
-                "sha256:30d0e083b5dfecee7693d197125daac3e5da9466e50dea0d01d5356abf5c6143",
-                "sha256:34ab524b4ce32dc67ef33421db644d6713ed677f601cbbef7007957f9cb6eb9c",
-                "sha256:3e257761347bf14e66f7a07bb4fda803c1293c3ca8b26baf5818ad66e2d46abb",
-                "sha256:3ec002d4e04ea5c99b33840f71c1042c33d4b13b20e32980b7a0a7eb71195ec4",
-                "sha256:79bd0bcb586d043a3565238be2f002ddf125bb710b4a46b330632a706d36ba08",
-                "sha256:7a004c518ba6abbb4ff96647e5ced0e1329d965448cbdc23709a8b6a3d040b26",
-                "sha256:977dc659a16481603716c655b7cae406e79d7788bd387b64994c198d3a27e19f",
-                "sha256:99c9dc61af4e8323bb77418d0c868717a838d424081156ac31b95957965f901f",
-                "sha256:a922287fd85973bbc900f629f5d475fe9be37eb2d3929fe81405c3ebcd950807",
-                "sha256:bf58b00da603378027cb1af265d0d0ccfa40cdc02263bdebbb4f56837d809512",
-                "sha256:c20592bb15a087add723d84484d95a9e428045a20afc7a791ccdad3e48113a39",
-                "sha256:c46c615896fa0daea5a5942c4b7b51be2bf3f8dfdcc2c05e256f2c957e3353eb",
-                "sha256:c745d18146bf48762806d724b8f265c046196ddb0a1dd15c7aee0919362cbb63",
-                "sha256:c9d5769c31b2700ece9c04aa8c0fc2efe95af8d473b35307d03210ff31d28831",
-                "sha256:cfd500cfa8d3f227704e8f4ff6bafee66bc5459fc56ece7792c334b92be2d35a",
-                "sha256:dda7008629e8232a372b798197dcebc5583eadc0ed643568543ee4afd0387928",
-                "sha256:f014907d2fd51ce132a29b1e869ea4058ead0cc14bcc2ed1690a08752c72ba62",
-                "sha256:f4677018469c7b98434dd465f1210f2ebd13a23d7084187666393a45b2ff7f26",
-                "sha256:f4ce7f6dd0e45f1acc05e0a633b3e18045b576c43394790719dcafb71a5daa13"
-            ],
-            "version": "==3.5.4"
+                "sha256:06c95f97b531e6dee05cf1080192c0310b72bcc6bb1a5da5bbe0295567abd1ae",
+                "sha256:0a817f4385c1d119f68bf701b4a73fdad7a273dfa7c7825db63930eda64ce8e7",
+                "sha256:0fc35bb3ad6b8e2c48d5f94ca1a18d60a763993384d03204d814ac63b7aa7ee1",
+                "sha256:1779a995e07ba561b8bb0b9474b9043861fc304c19afdcfc552fb758b783277f",
+                "sha256:178bd8e66b6771be6d270230c8169420ed05c5fb506fb05fc1983b2db469119b",
+                "sha256:270966fb4ecbf532a09c375c23404d171bb2d05169c7f2d00aa74d791091c21f",
+                "sha256:2951deb6d551d1a91535a7eaf91962834404218b97cfd5439b22ed9c71dd1214",
+                "sha256:325f888902e03a443090bd19fd8bd3f8a064989a9cdab0389fb60d6b461a189d",
+                "sha256:38c849f27d0acd042113cbde4d4d9682b1c45c62ca9c569201e70f128268b5d2",
+                "sha256:3950bef6486b2cf5690a7f0efbea233540582ebb5ac72736ff76e55048b5bed0",
+                "sha256:3da53fcc002f2d7b855f7fa075f74cba3fbbeb41f0a81958f45559334440819b",
+                "sha256:7466c57f66ba39f5c6c17425b0003d5eda2be98ddc5342aca99c02986a0bf185",
+                "sha256:84dcd9ca598665069bfdb3df1bef3fce98ea1a7ca5ef377cc6f55b1391daec1d",
+                "sha256:8f7e0577628c15e10ffd8da21e3699391bf5a25a28c5a531bccf614618bbfe3d",
+                "sha256:98325753e7d38dac9e0ad643814e37162e451c55bf9cd3ff3c5b94c2e6aff1df",
+                "sha256:9dc3cb1119efd1c29cf5e7a20ecd17dc184fdf66fa7b39e87d10344ecb8001fd",
+                "sha256:a4f65b20fccc77608591c90675bcc22aaaf128f20b9a648aea00afd592bccfe9",
+                "sha256:a5fddf5eedfed9d577f651ad53947d31738d893a485e3981ce710ca7c63ab547",
+                "sha256:b338048f1e96f4c3de6d58a668eeab3667276879db5589838118c174272358bd",
+                "sha256:b3dd72c29d1a89e9f96de0b3d67fc2dd824acfb026f3ff1603a024d229f7f404",
+                "sha256:bae9918e7793198b20507cc4a7b4ec4428c51ee33b28db96232ac178ea12400f",
+                "sha256:d714d0bf14ed9e0ca7cc5e9d4e6cb390c1ddcc3d8b7aeff23c09b0b3f6635e2c",
+                "sha256:d72f2951878c53e2f3f2e349bc8788f3e11a980e1f044f9c69153494318b340a",
+                "sha256:e69211b5f9e6aa93cf552491144aec561c3cdf6bf59c1f4013329556f13461f0",
+                "sha256:fd787fa25b5536d054bebc5e15f80da1948d0d6e9fe83848050eb557f5d35fc8"
+            ],
+            "version": "==3.5.5"
         },
         "packaging": {
             "hashes": [
-                "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
-                "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
+                "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966",
+                "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"
             ],
-            "version": "==20.9"
+            "version": "==21.2"
         },
         "pluggy": {
             "hashes": [
-                "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
-                "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
+                "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
+                "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
             ],
-            "version": "==0.13.1"
+            "version": "==1.0.0"
         },
         "ply": {
             "hashes": [
@@ -494,24 +511,24 @@
         },
         "pycodestyle": {
             "hashes": [
-                "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
-                "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
+                "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
+                "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
             ],
-            "version": "==2.7.0"
+            "version": "==2.8.0"
         },
         "pyflakes": {
             "hashes": [
-                "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3",
-                "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"
+                "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
+                "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"
             ],
-            "version": "==2.3.1"
+            "version": "==2.4.0"
         },
         "pygments": {
             "hashes": [
-                "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
-                "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
+                "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
+                "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
             ],
-            "version": "==2.9.0"
+            "version": "==2.10.0"
         },
         "pyparsing": {
             "hashes": [
@@ -522,25 +539,24 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634",
-                "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"
+                "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
+                "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
             ],
-            "version": "==6.2.3"
+            "version": "==6.2.5"
         },
         "pytest-asyncio": {
             "hashes": [
-                "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d",
-                "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"
+                "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4",
+                "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"
             ],
-            "markers": "python_version > '3.0'",
-            "version": "==0.14.0"
+            "version": "==0.17.2"
         },
         "pytest-cov": {
             "hashes": [
-                "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7",
-                "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"
+                "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
+                "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
             ],
-            "version": "==2.11.1"
+            "version": "==3.0.0"
         },
         "pytest-flake8": {
             "hashes": [
@@ -551,17 +567,17 @@
         },
         "pytz": {
             "hashes": [
-                "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
-                "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
+                "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
+                "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
             ],
-            "version": "==2021.1"
+            "version": "==2021.3"
         },
         "requests": {
             "hashes": [
-                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
-                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
+                "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
+                "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
             ],
-            "version": "==2.25.1"
+            "version": "==2.26.0"
         },
         "setuptools-dso": {
             "hashes": [
@@ -570,6 +586,13 @@
             ],
             "version": "==2.1"
         },
+        "six": {
+            "hashes": [
+                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+            ],
+            "version": "==1.16.0"
+        },
         "snowballstemmer": {
             "hashes": [
                 "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2",
@@ -586,25 +609,24 @@
         },
         "sphinx": {
             "hashes": [
-                "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13",
-                "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"
+                "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c",
+                "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851"
             ],
-            "version": "==4.1.2"
+            "version": "==4.3.2"
         },
         "sphinx-rtd-theme": {
             "hashes": [
-                "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a",
-                "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"
+                "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8",
+                "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"
             ],
-            "version": "==0.5.2"
+            "version": "==1.0.0"
         },
         "sphinx-rtd-theme-github-versions": {
             "hashes": [
-                "sha256:ad2822c08b0f075c051e25b9d291152d846c21ed052db3b458d5ef30efac59b3",
-                "sha256:f84b45baf536c01c2e62f26e8206dd69f0375d9c9b3769d56295292e6e00f43b"
+                "sha256:0df27ae9a9cd902468c808dbee5a43f4db8ce43cbcf2ecc78d2fe47698bb0ded",
+                "sha256:23018e51a5d27ef4f69dd86314f73b19088f2cfd91c74a24db1517832233dc07"
             ],
-            "markers": "python_version > '3.0'",
-            "version": "==0.2"
+            "version": "==1.1"
         },
         "sphinxcontrib-applehelp": {
             "hashes": [
@@ -655,27 +677,33 @@
             ],
             "version": "==0.10.2"
         },
+        "tomli": {
+            "hashes": [
+                "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee",
+                "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"
+            ],
+            "version": "==1.2.2"
+        },
         "typing-extensions": {
             "hashes": [
-                "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
-                "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
-                "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
+                "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
+                "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
             ],
-            "version": "==3.10.0.0"
+            "version": "==4.0.1"
         },
         "urllib3": {
             "hashes": [
-                "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
-                "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
+                "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
+                "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
             ],
-            "version": "==1.26.5"
+            "version": "==1.26.7"
         },
         "zipp": {
             "hashes": [
-                "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
-                "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
+                "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832",
+                "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"
             ],
-            "version": "==3.4.1"
+            "version": "==3.6.0"
         }
     }
 }
diff --git a/docs/reference/api.rst b/docs/reference/api.rst
index 7efab9e0..20eb6de3 100644
--- a/docs/reference/api.rst
+++ b/docs/reference/api.rst
@@ -267,7 +267,7 @@ All functions return a wrapped `ProcessDeviceSupportIn` or
         mbbOut(name, *options, **fields)
 
     Create ``mbbi`` and ``mbbo`` records.  Up to 16 options can be specified as
-    either an option name or a tuple of tw fields.  The name or first field of
+    either an option name or a tuple of two fields.  The name or first field of
     the tuple names the option, and the second optional field is the option
     severity.  For example::
 
@@ -308,13 +308,28 @@ All functions return a wrapped `ProcessDeviceSupportIn` or
     if given, or defaults to ``'FLOAT'``.
 
 
-The following function generates a specialised record.
+The following functions generates specialised records.
 
 ..  function:: Action(name, **fields)
 
     Creates a record (using `boolOut`) which will always call the `on_update`
-    method when processed.  Used for action records.  The `on_update` keyword
-    should always be passed.
+    method when processed.  Used for action records.  Either the `on_update` or
+    `on_update_name` keyword should always be passed.
+
+..  function::
+        longStringIn(name, **fields)
+        longStringOut(name, **fields)
+
+    Creates a Waveform record specialised to contain Python (Unicode) strings.
+    Only accepts string values, and only returns string values. `initial_value`
+    and ``length`` keywords accepted as per Waveform records.
+
+    If no ``length`` or `initial_value` is provided a default length of 256 is used.
+
+    .. note::
+        If you wish to store byte strings, use a ``WaveformIn/Out`` record
+
+
 
 
 The following functions manage record names.  The record device name must be
@@ -328,8 +343,7 @@ record creation function.
     Sets up the prefix part of the record name, referred to here as the "device"
     part.  This function must be called before creating any records.
     Note that
-    only this function need be used, the three other functions below are
-    entirely optional.
+    only this function need be used, function below is entirely optional.
 
 ..  function:: UnsetDevice()
 
diff --git a/softioc/__init__.py b/softioc/__init__.py
index b9c186b0..d826e87d 100644
--- a/softioc/__init__.py
+++ b/softioc/__init__.py
@@ -18,10 +18,10 @@
 # Need to do this before calling anything in device.py
 iocshRegisterCommon()
 for dbd in ('base.dbd', 'PVAServerRegister.dbd', 'qsrv.dbd'):
-    dbLoadDatabase(dbd, os.path.join(path.base_path, "dbd"), None)
-dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__), None)
+    dbLoadDatabase(dbd, os.path.join(path.base_path, 'dbd'), None)
+dbLoadDatabase('devIocStats.dbd', os.path.dirname(__file__), None)
 
 if registerRecordDeviceDriver(pdbbase):
     raise RuntimeError('Error registering')
 
-__all__ = ["__version__"]
+__all__ = ['__version__']
diff --git a/softioc/builder.py b/softioc/builder.py
index a7c7bd1b..679c4c6c 100644
--- a/softioc/builder.py
+++ b/softioc/builder.py
@@ -7,7 +7,8 @@
 InitialiseDbd()
 LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd'))
 
-from . import pythonSoftIoc  # noqa
+from . import device, pythonSoftIoc  # noqa
+
 PythonDevice = pythonSoftIoc.PythonDevice()
 
 
@@ -64,7 +65,7 @@ def longOut(name, DRVL=None, DRVH=None, EGU=None, **fields):
     'EI', 'NI', 'TE', 'EL', 'TV', 'TT', 'FT', 'FF']     # 8-15
 
 # All the severity strings supported by <prefix>SV
-_severityStrings = ["NO_ALARM", "MINOR", "MAJOR", "INVALID"]
+_severityStrings = ['NO_ALARM', 'MINOR', 'MAJOR', 'INVALID']
 
 # Converts a list of (option [,severity]) values or tuples into field settings
 # suitable for mbbi and mbbo records.
@@ -78,7 +79,7 @@ def process_value(prefix, value, option, severity=None):
                 severity = _severityStrings[severity]
             fields[prefix + 'SV'] = severity
     # zip() silently ignores extra values in options, so explicitly check length
-    assert len(options) <= 16, "May not specify more than 16 enum values"
+    assert len(options) <= 16, 'May not specify more than 16 enum values'
     for prefix, (value, option) in zip(_mbbPrefixes, enumerate(options)):
         if isinstance(option, tuple):
             # The option is tuple consisting of the option name and an optional
@@ -108,31 +109,28 @@ def Action(name, **fields):
     return boolOut(name, always_update = True, **fields)
 
 
-# Converts numpy character code to FTVL value.
-NumpyCharCodeToFtvl = {
-    # The following type codes are supported directly:
-    'B':    'UCHAR',        # ubyte
-    'b':    'CHAR',         # byte
-    'H':    'USHORT',       # ushort
-    'h':    'SHORT',        # short
-    'i':    'LONG',         # intc
-    'L':    'ULONG',        # uint
-    'l':    'LONG',         # int_
-    'f':    'FLOAT',        # single
-    'd':    'DOUBLE',       # float_
-    'S':    'STRING',       # str_
-
-    # The following type codes are weakly supported by pretending that
-    # they're related types.
-    '?':    'CHAR',         # bool_
-    'p':    'LONG',         # intp
-    'I':    'ULONG',        # uintc
-    'P':    'ULONG',        # uintp
-
-    # The following type codes are not supported at all:
-    #   q   longlong        Q   ulonglong       g   longfloat
-    #   F   csingle         D   complex_        G   clongfloat
-    #   O   object_         U   unicode_        V   void
+# Converts numpy dtype name to FTVL value.
+NumpyDtypeToDbf = {
+    'int8':     'CHAR',
+    'uint8':    'UCHAR',
+    'int16':    'SHORT',
+    'uint16':   'USHORT',
+    'int32':    'LONG',
+    'uint32':   'ULONG',
+    'float32':  'FLOAT',
+    'float64':  'DOUBLE',
+}
+
+# Coverts FTVL string to numpy type
+DbfStringToNumpy = {
+    'CHAR':     'int8',
+    'UCHAR':    'uint8',
+    'SHORT':    'int16',
+    'USHORT':   'uint16',
+    'LONG':     'int32',
+    'ULONG':    'uint32',
+    'FLOAT':    'float32',
+    'DOUBLE':   'float64',
 }
 
 
@@ -146,29 +144,49 @@ def _waveform(value, fields):
         assert not value, 'Can\'t specify initial value twice!'
         value = (fields.pop('initial_value'),)
 
+    # Datatype can be specified as keyword argument, taken from FTVL, or derived
+    # from the initial value
+    if 'datatype' in fields:
+        assert 'FTVL' not in fields, \
+            'Can\'t specify FTVL and datatype together'
+        datatype = fields.pop('datatype')
+        if datatype == int or datatype == 'int':
+            # Convert Python int to 32-bit integer, it's all we can handle
+            datatype = numpy.dtype('int32')
+        else:
+            datatype = numpy.dtype(datatype)
+    elif 'FTVL' in fields:
+        datatype = numpy.dtype(DbfStringToNumpy[fields['FTVL']])
+    else:
+        # No datatype specified, will have to infer from initial value
+        datatype = None
+
     if value:
-        # If a value is specified it should be the *only* non keyword
-        # argument.
+        # If a value is specified it should be the *only* non keyword argument.
         value, = value
-        value = numpy.array(value)
-        fields['initial_value'] = value
-
-        # Pick up default length and datatype from initial value
-        length = len(value)
-        FTVL = NumpyCharCodeToFtvl[value.dtype.char]
+        initial_value = device._require_waveform(value, datatype)
+        length = fields.pop('length', len(initial_value))
+
+        # Special case for [u]int64: if the initial value comes in as 64 bit
+        # integers we cannot represent that, so recast it as [u]int32
+        if datatype is None:
+            if initial_value.dtype == numpy.int64:
+                initial_value = numpy.require(initial_value, numpy.int32)
+            elif initial_value.dtype == numpy.uint64:
+                initial_value = numpy.require(initial_value, numpy.uint32)
     else:
-        # No value specified, so require length and datatype to be specified.
+        initial_value = numpy.array([], dtype = datatype)
         length = fields.pop('length')
-        FTVL = 'FLOAT'
+    datatype = initial_value.dtype
 
-    datatype = fields.pop('datatype', None)
-    if datatype is not None:
-        assert 'FTVL' not in fields, \
-            'Can\'t specify FTVL and datatype together'
-        FTVL = NumpyCharCodeToFtvl[numpy.dtype(datatype).char]
+    assert length > 0, 'Array cannot be of zero length'
+
+    fields['initial_value'] = initial_value
+    fields['_wf_nelm'] = length
+    fields['_wf_dtype'] = datatype
 
     fields['NELM'] = length
-    fields.setdefault('FTVL', FTVL)
+    fields['FTVL'] = NumpyDtypeToDbf[datatype.name]
 
 
 def Waveform(name, *value, **fields):
@@ -182,6 +200,32 @@ def WaveformOut(name, *value, **fields):
     return PythonDevice.waveform_out(name, **fields)
 
 
+def _long_string(fields):
+    if 'length' in fields:
+        length = fields.pop('length')
+    elif 'initial_value' in fields:
+        length = len(fields['initial_value'].encode(errors = 'replace')) + 1
+    else:
+        # Default length of 256
+        length = 256
+
+    fields.setdefault('initial_value', '')
+    fields['_wf_nelm'] = length
+    fields['_wf_dtype'] = numpy.dtype('uint8')
+
+    fields['NELM'] = length
+    fields['FTVL'] = 'UCHAR'
+
+
+def longStringIn(name, **fields):
+    _long_string(fields)
+    return _in_record('long_stringin', name, **fields)
+
+def longStringOut(name, **fields):
+    _long_string(fields)
+    return PythonDevice.long_stringout(name, **fields)
+
+
 
 # ----------------------------------------------------------------------------
 #  Support routines for builder
@@ -227,6 +271,7 @@ def UnsetDevice():
     'stringIn', 'stringOut',
     'mbbIn',    'mbbOut',
     'Waveform', 'WaveformOut',
+    'longStringIn', 'longStringOut',
     'Action',
     # Other builder support functions
     'LoadDatabase',
diff --git a/softioc/device.dbd b/softioc/device.dbd
index 6c01d590..766d508e 100644
--- a/softioc/device.dbd
+++ b/softioc/device.dbd
@@ -10,5 +10,7 @@ device(waveform,  INST_IO, devPython_waveform,  "Python")
 device(mbbi,      INST_IO, devPython_mbbi,      "Python")
 device(mbbo,      INST_IO, devPython_mbbo,      "Python")
 
-# Waveforms as "out" devices need special treatment.
+# A variety of specialised versions of waveform need special treatment.
 device(waveform,  INST_IO, devPython_waveform_out, "PythonWfOut")
+device(waveform,  INST_IO, devPython_long_stringin, "PythonLongStringIn")
+device(waveform,  INST_IO, devPython_long_stringout, "PythonLongStringOut")
diff --git a/softioc/device.py b/softioc/device.py
index 7d125345..3ff8d254 100644
--- a/softioc/device.py
+++ b/softioc/device.py
@@ -1,11 +1,11 @@
 import os
 import time
-import inspect
+import ctypes
 from ctypes import *
 import numpy
 
 from . import alarm
-from .fields import DbfCodeToNumpy, DbrToDbfCode
+from . import fields
 from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field
 from .device_core import DeviceSupportCore, RecordLookup
 
@@ -33,19 +33,46 @@ class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup):
 
     # For some record types we want to return a different return code either
     # from record init or processing
-    _epics_rc = EPICS_OK
+    _epics_rc_ = EPICS_OK
 
-    # Default implementations of read and write, overwritten where necessary.
+
+    # Most subclasses (all except waveforms) define a ctypes constructor for the
+    # underlying EPICS compatible value.
+    def _value_to_epics(self, value):
+        return self._ctype_(value)
+
+    def _epics_to_value(self, epics):
+        return epics.value
+
+    def _default_value(self):
+        return self._ctype_()
+
+    def _compare_values(self, value1, value2):
+        return value1.value == value2.value
+
+
+    # This method is called during Out record processing to return the
+    # underlying value in EPICS format.
     def _read_value(self, record):
-        return getattr(record, 'VAL')
+        return record.read_val()
+
+    # This method is called during In record processing to update the
+    # underlying value (the value must be in EPICS compatible format).  This is
+    # also called during Out record initialisation and value reversion when
+    # required.
     def _write_value(self, record, value):
-        setattr(record, 'VAL', value)
+        record.write_val(value)
 
 
 class ProcessDeviceSupportIn(ProcessDeviceSupportCore):
     _link_ = 'INP'
 
     def __init__(self, name, **kargs):
+        if 'initial_value' in kargs:
+            value = self._value_to_epics(kargs.pop('initial_value'))
+        else:
+            value = self._default_value()
+
         # We implement update locking via a simple trick which relies on the
         # Python global interpreter lock: this ensures that assigning or
         # reading a single value is atomic.  We therefore cluster all our
@@ -53,28 +80,26 @@ def __init__(self, name, **kargs):
         # to be processed.
         #    The tuple contains everything needed to be written: the value,
         # severity, alarm and optional timestamp.
-        self._value = (
-            kargs.pop('initial_value', self._default_),
-            alarm.NO_ALARM, alarm.UDF_ALARM, None)
+        self._value = (value, alarm.NO_ALARM, alarm.UDF_ALARM, None)
         self.__super.__init__(name, **kargs)
 
-    def _process(self, record, _value=None):
+    def _process(self, record):
         # For input process we copy the value stored in the instance to the
         # record.  The alarm status is also updated, and a custom timestamp
         # can also be set.
-        if _value is None:
-            _value = self._value
-        value, severity, alarm, timestamp = _value
+        value, severity, alarm, timestamp = self._value
         self._write_value(record, value)
         self.process_severity(record, severity, alarm)
         if timestamp is not None:
             record.TIME = timestamp
-        return self._epics_rc
+        record.UDF = 0
+        return self._epics_rc_
 
     def set(self, value,
             severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None):
         '''Updates the stored value and triggers an update.  The alarm
         severity and timestamp can also be specified if appropriate.'''
+        value = self._value_to_epics(value)
         self._value = (value, severity, alarm, timestamp)
         self.trigger()
 
@@ -86,7 +111,7 @@ def set_alarm(self, severity, alarm, timestamp=None):
 
     def get(self):
         '''Returns the last written value.'''
-        return self._value[0]
+        return self._epics_to_value(self._value[0])
 
 
 class ProcessDeviceSupportOut(ProcessDeviceSupportCore):
@@ -107,101 +132,65 @@ def __init__(self, name, **kargs):
 
         self.__validate = kargs.pop('validate', None)
         self.__always_update = kargs.pop('always_update', False)
-        self._value = kargs.pop('initial_value', None)
         self.__enable_write = True
+
+        if 'initial_value' in kargs:
+            self._value = self._value_to_epics(kargs.pop('initial_value'))
+        else:
+            self._value = None
+
         self.__super.__init__(name, **kargs)
 
     def init_record(self, record):
         '''Special record initialisation for out records only: implements
         special record initialisation if an initial value has been specified,
         allowing out records to have a sensible initial value.'''
-        if self._value is not None:
+        if self._value is None:
+            # Cannot set in __init__ (like we do for In records), as we want
+            # the record alarm status to be set if no value was provided
+            # Probably related to PythonSoftIOC issue #53
+            self._value = self._default_value()
+        else:
             self._write_value(record, self._value)
             if 'MLST' in self._fields_:
                 record.MLST = self._value
             record.TIME = time.time()
             record.UDF = 0
             recGblResetAlarms(record)
-        return self._epics_rc
+        return self._epics_rc_
 
     def _process(self, record):
         '''Processing suitable for output records.  Performs immediate value
         validation and asynchronous update notification.'''
         value = self._read_value(record)
-        if numpy.all(value == self._value) and not self.__always_update:
+        if not self.__always_update and \
+                self._compare_values(value, self._value):
             # If the value isn't making a change then don't do anything.
             return EPICS_OK
+
+        python_value = self._epics_to_value(value)
         if self.__enable_write and self.__validate and \
-                not self.__validate(self, value):
-            # Asynchronous validation rejects value.  It's up to the
-            # validation routine to do any logging.  In this case restore the
-            # last good value.
-            if self._value is not None:
-                self._write_value(record, self._value)
+                not self.__validate(self, python_value):
+            # Asynchronous validation rejects value, so restore the last good
+            # value.
+            self._write_value(record, self._value)
             return EPICS_ERROR
+        else:
+            # Value is good.  Hang onto it, let users know the value has changed
+            self._value = value
+            record.UDF = 0
+            if self.__on_update and self.__enable_write:
+                dispatcher(self.__on_update, python_value)
+            return EPICS_OK
 
-        self._value = value
-        if self.__on_update and self.__enable_write:
-            dispatcher(self.__on_update, value)
-        return EPICS_OK
-
-
-    NumpyCharCodeToDbr = {
-        # The following type codes are supported directly:
-        'S':    0,  # DBR_STRING     str_
-        'h':    1,  # DBR_SHORT      short  = int16
-        'f':    2,  # DBR_FLOAT      single = float32
-        'b':    4,  # DBR_CHAR       byte   = int8
-        'i':    5,  # DBR_LONG       intc   = int32
-        'd':    6,  # DBR_DOUBLE     float_ = float64
-        # These are supported as related types
-        'H':    1,  # DBR_SHORT      ushort = uint16
-        '?':    4,  # DBR_CHAR       bool_
-        'B':    4,  # DBR_CHAR       ubyte  = uint8
-        'I':    5,  # DBR_LONG       uintc  = uint32
-    }
-    if numpy.int_().itemsize == 4:
-        NumpyCharCodeToDbr.update({'l': 5, 'L': 5})   # int_, uint
-
-    # Converts a Python value into a form suitable for sending over channel
-    # access.  Derived from the corresponding cothread implementation.  Returns
-    # computed dbrcode, waveform length, raw data pointer and pointer to
-    # underlying data (for lifetime management).
-    def value_to_dbr(self, value):
-        # First convert the data directly into an array.  This will help in
-        # subsequent processing: this does most of the type coercion.
-        value = numpy.require(value, requirements = 'C')
-        if value.shape == ():
-            value.shape = (1,)
-        assert value.ndim == 1, 'Can\'t put multidimensional arrays!'
-
-        if value.dtype.char == 'S':
-            # Need special processing to hack the array so that strings are
-            # actually 40 characters long.
-            new_value = numpy.empty(value.shape, 'S40')
-            new_value[:] = value
-            value = new_value
 
-        try:
-            dbrtype = self.NumpyCharCodeToDbr[value.dtype.char]
-        except KeyError:
-            # One more special case.  caput() of a list of integers on a 64-bit
-            # system will fail at this point because they were automatically
-            # converted to 64-bit integers.  Catch this special case and fix it
-            # up by silently converting to 32-bit integers.  Not really the
-            # right thing to do (as data can be quietly lost), but the
-            # alternative isn't nice to use either.
-            if value.dtype.char == 'l':
-                value = numpy.require(value, dtype = numpy.int32)
-                dbrtype = 5
-            else:
-                raise
-
-        return dbrtype, len(value), value.ctypes.data, value
+    def _value_to_dbr(self, value):
+        return self._dbf_type_, 1, addressof(value), value
 
 
     def set(self, value, process=True):
         '''Special routine to set the value directly.'''
+        value = self._value_to_epics(value)
         try:
             _record = self._record
         except AttributeError:
@@ -209,24 +198,30 @@ def set(self, value, process=True):
             # initialisation occurs
             self._value = value
         else:
-            datatype, length, data, array = self.value_to_dbr(value)
+            # The array parameter is used to keep the raw pointer alive
+            dbf_code, length, data, array = self._value_to_dbr(value)
             self.__enable_write = process
-            db_put_field(
-                _record.NAME, DbrToDbfCode[datatype], data, length)
+            db_put_field(_record.NAME, dbf_code, data, length)
             self.__enable_write = True
 
     def get(self):
-        return self._value
+        if self._value is None:
+            # Before startup complete if no value set return default value
+            value = self._default_value()
+        else:
+            value = self._value
+        return self._epics_to_value(value)
 
 
-def _Device(Base, record_type, mlst=False, default=0, convert=True):
+def _Device(Base, record_type, ctype, dbf_type, epics_rc, mlst = False):
     '''Wrapper for generating simple records.'''
     class GenericDevice(Base):
         _record_type_ = record_type
         _device_name_ = 'devPython_' + record_type
-        _default_ = default
         _fields_ = ['UDF', 'VAL']
-        _epics_rc = EPICS_OK if convert else NO_CONVERT
+        _epics_rc_ = epics_rc
+        _ctype_ = staticmethod(ctype)
+        _dbf_type_ = dbf_type
         if mlst:
             _fields_.append('MLST')
 
@@ -236,20 +231,59 @@ class GenericDevice(Base):
 _In = ProcessDeviceSupportIn
 _Out = ProcessDeviceSupportOut
 
-def _Device_In(type, **kargs):
-    return _Device(_In,  type, **kargs)
+def _Device_In(*args, **kargs):
+    return _Device(_In, mlst = False, *args, **kargs)
+
+def _Device_Out(*args, **kargs):
+    return _Device(_Out, mlst = True, *args, **kargs)
+
+longin = _Device_In('longin', c_int32, fields.DBF_LONG, EPICS_OK)
+longout = _Device_Out('longout', c_int32, fields.DBF_LONG, EPICS_OK)
+bi = _Device_In('bi', c_uint16, fields.DBF_CHAR, NO_CONVERT)
+bo = _Device_Out('bo', c_uint16, fields.DBF_CHAR, NO_CONVERT)
+mbbi = _Device_In('mbbi', c_uint16, fields.DBF_SHORT, NO_CONVERT)
+mbbo = _Device_Out('mbbo', c_uint16, fields.DBF_SHORT, NO_CONVERT)
+
+
+def _string_at(value, count):
+    # Need string_at() twice to ensure string is size limited *and* null
+    # terminated.
+    value = ctypes.string_at(ctypes.string_at(value, count))
+    # Convert bytes to unicode string
+    return value.decode(errors = 'replace')
+
+class EpicsString:
+    _fields_ = ['UDF', 'VAL']
+    _epics_rc_ = EPICS_OK
+    _ctype_ = c_char * 40
+    _dbf_type_ = fields.DBF_STRING
+
+    def _value_to_epics(self, value):
+        # It's a little odd: we can't simply construct a value from the byte
+        # string, but we can update the array in an existing value.
+        # Value being written must be a string, and will be automatically null
+        # terminated where possible.
+        result = self._ctype_()
+        result.value = value.encode()
+        return result
+
+    def _epics_to_value(self, epics):
+        return _string_at(epics, 40)
+
+    def _read_value(self, record):
+        # For strings we need to take a copy of the value read
+        result = self._ctype_()
+        result.value = record.read_val().value
+        return result
+
 
-def _Device_Out(type, convert=True, mlst=True):
-    return _Device(_Out, type, convert=convert, mlst=mlst, default=None)
+class stringin(EpicsString, ProcessDeviceSupportIn):
+    _record_type_ = 'stringin'
+    _device_name_ = 'devPython_stringin'
 
-longin = _Device_In('longin')
-longout = _Device_Out('longout')
-bi = _Device_In('bi', convert=False)
-bo = _Device_Out('bo', convert=False)
-stringin = _Device_In('stringin', mlst=False, default='')
-stringout = _Device_Out('stringout', mlst=False)
-mbbi = _Device_In('mbbi', convert=False)
-mbbo = _Device_Out('mbbo', convert=False)
+class stringout(EpicsString, ProcessDeviceSupportOut):
+    _record_type_ = 'stringout'
+    _device_name_ = 'devPython_stringout'
 
 
 dset_process_linconv = (
@@ -263,26 +297,37 @@ def _Device_Out(type, convert=True, mlst=True):
 class ai(ProcessDeviceSupportIn):
     _record_type_ = 'ai'
     _device_name_ = 'devPython_ai'
-    _default_ = 0.0
     _fields_ = ['UDF', 'VAL']
     _dset_extra_ = dset_process_linconv
-    _epics_rc = NO_CONVERT
+    _epics_rc_ = NO_CONVERT
+    _ctype_ = c_double
+    _dbf_type_ = fields.DBF_DOUBLE
 
     def _process(self, record):
-        _value = self._value
-        self.__super._process(record, _value)
         # Because we're returning NO_CONVERT we need to do the .UDF updating
         # ourself (otherwise the record support layer does this).
-        record.UDF = int(numpy.isnan(_value[0]))
-        return NO_CONVERT
+        record.UDF = int(numpy.isnan(self._value[0]))
+        return self.__super._process(record)
 
 class ao(ProcessDeviceSupportOut):
     _record_type_ = 'ao'
     _device_name_ = 'devPython_ao'
     _fields_ = ['UDF', 'VAL', 'MLST']
     _dset_extra_ = dset_process_linconv
-    _epics_rc = NO_CONVERT
+    _epics_rc_ = NO_CONVERT
+    _ctype_ = c_double
+    _dbf_type_ = fields.DBF_DOUBLE
+
 
+def _require_waveform(value, dtype):
+    if isinstance(value, bytes):
+        # Special case hack for byte arrays.  Surprisingly tricky:
+        value = numpy.frombuffer(value, dtype = numpy.uint8)
+    value = numpy.require(value, dtype = dtype)
+    if value.shape == ():
+        value.shape = (1,)
+    assert value.ndim == 1, 'Can\'t write multidimensional arrays'
+    return value
 
 
 class WaveformBase(ProcessDeviceSupportCore):
@@ -293,70 +338,88 @@ class WaveformBase(ProcessDeviceSupportCore):
     #   NELM    Length of allocated array in number of elements
     #   NORD    Currently reported length of array (0 <= NORD <= NELM)
     _fields_ = ['UDF', 'FTVL', 'BPTR', 'NELM', 'NORD']
-    # Allow set() to be called before init_record:
-    dtype = None
+
+
+    def __init__(self, name, _wf_nelm, _wf_dtype, **kargs):
+        self._dtype = _wf_dtype
+        self._nelm = _wf_nelm
+        self.__super.__init__(name, **kargs)
 
     def init_record(self, record):
-        self.dtype = DbfCodeToNumpy[record.FTVL]
+        self._dbf_type_ = record.FTVL
         return self.__super.init_record(record)
 
     def _read_value(self, record):
         nord = record.NORD
-        result = numpy.empty(nord, dtype = self.dtype)
+        result = numpy.empty(nord, dtype = self._dtype)
         memmove(
             result.ctypes.data_as(c_void_p), record.BPTR,
-            self.dtype.itemsize * nord)
+            self._dtype.itemsize * nord)
         return result
 
     def _write_value(self, record, value):
-        value = numpy.require(value, dtype = self.dtype)
-        if value.shape == ():
-            value.shape = (1,)
-        assert value.ndim == 1, 'Can\'t write multidimensional arrays'
-
-        nelm = record.NELM
         nord = len(value)
-        if nord > nelm:
-            nord = nelm
         memmove(
             record.BPTR, value.ctypes.data_as(c_void_p),
-            self.dtype.itemsize * nord)
+            self._dtype.itemsize * nord)
         record.NORD = nord
 
+    def _compare_values(self, value, other):
+        return numpy.array_equal(value, other)
+
+    def _value_to_epics(self, value):
+        # Ensure we always convert incoming value into numpy array, regardless
+        # of whether the record has been initialised or not
+        value = _require_waveform(value, self._dtype)
+        # Because arrays are mutable values it's ever so easy to accidentially
+        # call set() with a value which subsequently changes.  To avoid this
+        # common class of bug, at the cost of duplicated code and data, here we
+        # ensure a copy is taken of the value.
+        assert len(value) <= self._nelm, 'Value too long for waveform'
+        return +value
+
+    def _epics_to_value(self, value):
+        return value
+
+    def _value_to_dbr(self, value):
+        return self._dbf_type_, len(value), value.ctypes.data, value
+
 
 class waveform(WaveformBase, ProcessDeviceSupportIn):
     _record_type_ = 'waveform'
     _device_name_ = 'devPython_waveform'
-    _default_ = ()
 
-    # Because arrays are mutable values it's ever so easy to accidentially call
-    # set() with a value which subsequently changes.  To avoid this common class
-    # of bug, at the cost of duplicated code and data, here we ensure a copy is
-    # taken of the value.
-    def set(self, value,
-            severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None):
-        '''Updates the stored value and triggers an update.  The alarm
-        severity and timestamp can also be specified if appropriate.'''
-        if isinstance(value, str):
-            # Convert a string into an array of characters.  This will produce
-            # the correct behaviour when treating a character array as a string.
-            # Note that the trailing null is needed to work around problems with
-            # some clients.
-            value = numpy.fromstring(value + '\0', dtype = 'uint8')
-
-        value = numpy.require(value, dtype = self.dtype)
-        self._value = (+value, severity, alarm, timestamp)
-        if value.shape == ():
-            value.shape = (1,)
-        assert value.ndim == 1, 'Can\'t write multidimensional arrays'
+class waveform_out(WaveformBase, ProcessDeviceSupportOut):
+    _record_type_ = 'waveform'
+    _device_name_ = 'devPython_waveform_out'
 
-        self.trigger()
 
+class LongStringBase(WaveformBase):
+    _dtype = numpy.dtype('uint8')
 
-class waveform_out(WaveformBase, ProcessDeviceSupportOut):
+    def _value_to_epics(self, value):
+        value = value.encode(errors = 'replace')
+        # Convert a string into an array of characters.  This will produce
+        # the correct behaviour when treating a character array as a string.
+        # Note that the trailing null is needed to work around problems with
+        # some clients.
+        value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8)
+        # Ensure string isn't too long to fit into waveform
+        assert len(value) <= self._nelm, 'Value too long for waveform'
+        return value
+
+    def _epics_to_value(self, value):
+        return _string_at(value.ctypes, len(value))
+
+
+class long_stringin(LongStringBase, ProcessDeviceSupportIn):
     _record_type_ = 'waveform'
-    _device_name_ = 'devPython_waveform_out'
+    _device_name_ = 'devPython_long_stringin'
+
+class long_stringout(LongStringBase, ProcessDeviceSupportOut):
+    _record_type_ = 'waveform'
+    _device_name_ = 'devPython_long_stringout'
 
 
 # Ensure the .dbd file is loaded.
-dbLoadDatabase("device.dbd", os.path.dirname(__file__), None)
+dbLoadDatabase('device.dbd', os.path.dirname(__file__), None)
diff --git a/softioc/fields.py b/softioc/fields.py
index d44dfb69..63fa476e 100644
--- a/softioc/fields.py
+++ b/softioc/fields.py
@@ -26,6 +26,7 @@
 # downstream for no good purpose -- for example, enums are of type DBF_ULONG,
 # but this cannot be written with caput.)
 DbfCodeToCtypes = {
+    DBF_STRING: c_char * 40,
     DBF_CHAR: c_byte,
     DBF_UCHAR: c_ubyte,
     DBF_SHORT: c_int16,
@@ -41,19 +42,6 @@
     DBF_NOACCESS: c_void_p,
 }
 
-# Mapping for record field type to numpy type.
-DbfCodeToNumpy = {
-    DBF_STRING: numpy.dtype('S40'),
-    DBF_CHAR: numpy.dtype('int8'),
-    DBF_UCHAR: numpy.dtype('uint8'),
-    DBF_SHORT: numpy.dtype('int16'),
-    DBF_USHORT: numpy.dtype('uint16'),
-    DBF_LONG: numpy.dtype('int32'),
-    DBF_ULONG: numpy.dtype('uint32'),
-    DBF_FLOAT: numpy.dtype('float32'),
-    DBF_DOUBLE: numpy.dtype('float64'),
-}
-
 # Mapping from basic DBR_ codes to DBF_ values
 DbrToDbfCode = {
     DBR_STRING: DBF_STRING,
@@ -73,7 +61,7 @@ def __init__(self, record_type, fields):
         self.fields = get_field_offsets(record_type)
         missing = set(fields) - set(self.fields)
         assert not missing, \
-            "Fields not supported by %s: %s" % (record_type, sorted(missing))
+            'Fields not supported by %s: %s' % (record_type, sorted(missing))
 
     def __call__(self, record):
         '''Converts a raw pointer to a record structure into a _Record object
@@ -98,9 +86,11 @@ def __getattr__(self, field):
         if field == 'TIME':
             return self.__get_time(address)
         elif field_type == DBF_STRING:
-            return string_at(cast(address, c_char_p)).decode()
+            raw_string = string_at(cast(address, c_char_p))
+            return raw_string.decode(errors = 'replace')
         elif field_type in [DBF_INLINK, DBF_OUTLINK]:
-            return cast(address, POINTER(c_char_p))[0].decode()
+            raw_string = cast(address, POINTER(c_char_p))[0]
+            return raw_string.decode(errors = 'replace')
         else:
             ctypes_type = DbfCodeToCtypes[field_type]
             return cast(address, POINTER(ctypes_type))[0]
@@ -121,6 +111,23 @@ def __setattr__(self, field, value):
             ctypes_type = DbfCodeToCtypes[field_type]
             cast(address, POINTER(ctypes_type))[0] = value
 
+    # Directly write EPICS compatible value to .VAL field
+    def write_val(self, value):
+        offset, size, field_type = self.fields['VAL']
+        address = c_void_p(offset + self.record.value)
+
+        ctypes_type = DbfCodeToCtypes[field_type]
+        assert sizeof(value) == size, \
+            'Incompatible field and value: %d %d' % (sizeof(value), size)
+        cast(address, POINTER(ctypes_type))[0] = value
+
+    def read_val(self):
+        offset, size, field_type = self.fields['VAL']
+        address = c_void_p(offset + self.record.value)
+        ctypes_type = DbfCodeToCtypes[field_type]
+        return cast(address, POINTER(ctypes_type)).contents
+
+
     def __get_time(self, address):
         timestamp = cast(address, POINTER(ca_timestamp))[0]
         timestamp.secs += EPICS_epoch
diff --git a/softioc/imports.py b/softioc/imports.py
index eb525fb8..699f2302 100644
--- a/softioc/imports.py
+++ b/softioc/imports.py
@@ -12,19 +12,19 @@
 
 # These are in the extension
 def get_DBF_values():
-    """Return {DBF_name: DBF_int_value} mapping"""
+    '''Return {DBF_name: DBF_int_value} mapping'''
     return _extension.get_DBF_values()
 
 def get_field_offsets(record_type):
-    """Return {field_name: (offset, size, field_type)}"""
+    '''Return {field_name: (offset, size, field_type)}'''
     return _extension.get_field_offsets(record_type)
 
 def db_put_field(name, dbr_type, pbuffer, length):
-    """Put field where pbuffer is void* pointer. Returns RC"""
+    '''Put field where pbuffer is void* pointer. Returns RC'''
     return _extension.db_put_field(name, dbr_type, pbuffer, length)
 
 def install_pv_logging(acf_file):
-    """Install pv logging"""
+    '''Install pv logging'''
     _extension.install_pv_logging(acf_file)
 
 def expect_success(status, function, args):
diff --git a/softioc/pythonSoftIoc.py b/softioc/pythonSoftIoc.py
index 84880c3e..8d79627f 100644
--- a/softioc/pythonSoftIoc.py
+++ b/softioc/pythonSoftIoc.py
@@ -23,8 +23,8 @@ def __init__(self, builder, device, name, **fields):
         # remaining arguments are passed to the builder.  It's a shame we
         # have to maintain this separately from the corresponding device list.
         DeviceKeywords = [
-            'on_update', 'on_update_name', 'validate',
-            'initial_value', 'always_update']
+            'on_update', 'on_update_name', 'validate', 'always_update',
+            'initial_value', '_wf_nelm', '_wf_dtype']
         device_kargs = {}
         for keyword in DeviceKeywords:
             if keyword in fields:
@@ -92,6 +92,12 @@ def __init_class__(cls):
         cls.waveform_out = cls.makeRecord(
             epicsdbbuilder.records.waveform, device.waveform_out,
             'PythonWfOut')
+        cls.long_stringin = cls.makeRecord(
+            epicsdbbuilder.records.waveform, device.long_stringin,
+            'PythonLongStringIn')
+        cls.long_stringout = cls.makeRecord(
+            epicsdbbuilder.records.waveform, device.long_stringout,
+            'PythonLongStringOut')
 
     class makeRecord:
         def __init__(self, builder, record, dtyp = 'Python'):
diff --git a/softioc/softioc.py b/softioc/softioc.py
index c9214142..017db501 100644
--- a/softioc/softioc.py
+++ b/softioc/softioc.py
@@ -18,7 +18,7 @@ def epicsAtPyExit():
 
 
 def iocInit(dispatcher=None):
-    """This must be called exactly once after loading all EPICS database files.
+    '''This must be called exactly once after loading all EPICS database files.
     After this point the EPICS IOC is running and serving PVs.
 
     Args:
@@ -28,7 +28,7 @@ def iocInit(dispatcher=None):
 
     See Also:
         `softioc.asyncio_dispatcher` is a dispatcher for `asyncio` applications
-    """
+    '''
     if dispatcher is None:
         # Fallback to cothread
         import cothread
@@ -263,7 +263,7 @@ def _add_records_from_file(dirname, file, substitutions):
     # possibly can put them out of order, but it works well enough for
     # devIocStats
     with open(os.path.join(dirname, file)) as f:
-        lines, include_subs = [], ""
+        lines, include_subs = [], ''
         for line in f.readlines():
             line = line.rstrip()
             if line.startswith('substitute'):
@@ -274,7 +274,7 @@ def _add_records_from_file(dirname, file, substitutions):
                 # include "iocQueue.db"
                 subs = substitutions
                 if substitutions and include_subs:
-                    subs = substitutions + ", " + include_subs
+                    subs = substitutions + ', ' + include_subs
                 else:
                     subs = substitutions + include_subs
                 _add_records_from_file(dirname, line.split('"')[1], subs)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 3479fe5c..94faccf8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -7,6 +7,14 @@
 
 import pytest
 
+# Must import softioc before epicsdbbuilder
+from softioc.device_core import RecordLookup
+from epicsdbbuilder import ResetRecords
+
+requires_cothread = pytest.mark.skipif(
+    sys.platform.startswith("win"), reason="Cothread doesn't work on windows"
+)
+
 class SubprocessIOC:
     def __init__(self, ioc_py):
         self.pv_prefix = "".join(
@@ -59,3 +67,29 @@ def asyncio_ioc_override():
     yield ioc
     ioc.kill()
     aioca_cleanup()
+
+def _clear_records():
+    # Remove any records created at epicsdbbuilder layer
+    ResetRecords()
+    # And at pythonSoftIoc level
+    # TODO: Remove this hack and use use whatever comes out of
+    # https://github.com/dls-controls/pythonSoftIOC/issues/56
+    RecordLookup._RecordDirectory.clear()
+
+@pytest.fixture
+def clear_records():
+    """Fixture to delete all records before and after a test."""
+    _clear_records()
+    yield
+    _clear_records()
+
+def enable_code_coverage():
+    """Ensure code coverage works as expected for `multiprocesses` tests.
+    This method should be called just before any `multiprocess.Process` call
+    is made."""
+    try:
+        from pytest_cov.embed import cleanup_on_sigterm
+    except ImportError:
+        pass
+    else:
+        cleanup_on_sigterm()
diff --git a/tests/expected_records.db b/tests/expected_records.db
index c210edc1..ab764e2f 100644
--- a/tests/expected_records.db
+++ b/tests/expected_records.db
@@ -53,10 +53,11 @@ record(longout, "TS-DI-TEST-01:LONGOUT")
 record(waveform, "TS-DI-TEST-01:LONGSTRING")
 {
     field(DISP, "1")
-    field(DTYP, "Python")
+    field(DTYP, "PythonLongStringIn")
     field(FTVL, "UCHAR")
     field(INP,  "@TS-DI-TEST-01:LONGSTRING")
     field(NELM, "256")
+    field(PINI, "YES")
     field(SCAN, "I/O Intr")
 }
 
@@ -97,6 +98,7 @@ record(waveform, "TS-DI-TEST-01:SIN")
     field(FTVL, "DOUBLE")
     field(INP,  "@TS-DI-TEST-01:SIN")
     field(NELM, "1024")
+    field(PINI, "YES")
     field(SCAN, "I/O Intr")
 }
 
@@ -145,9 +147,10 @@ record(waveform, "TS-DI-TEST-01:WAVEFORM2")
 {
     field(DISP, "1")
     field(DTYP, "Python")
-    field(FTVL, "FLOAT")
+    field(FTVL, "DOUBLE")
     field(INP,  "@TS-DI-TEST-01:WAVEFORM2")
     field(NELM, "10")
+    field(PINI, "YES")
     field(SCAN, "I/O Intr")
 }
 
diff --git a/tests/sim_records.py b/tests/sim_records.py
index 3009e356..ee712601 100644
--- a/tests/sim_records.py
+++ b/tests/sim_records.py
@@ -22,67 +22,54 @@ def on_update(value):
 def on_update_name(value, name):
     print('on_update', name, ':', repr(value))
 
-t_ai = aIn('AI', initial_value=12.34)
-t_boolin = boolIn('BOOLIN', 'True', 'False', initial_value=False)
-t_longin = longIn('LONGIN', initial_value=33)
-t_stringin = stringIn('STRINGIN', initial_value="Testing string")
-t_mbbi = mbbIn(
-    'MBBI', ('One', alarm.MAJOR_ALARM), 'Two', ('Three', "MINOR"),
-    initial_value=2)
-
-t_ao = aOut('AO', initial_value=12.45, on_update_name=on_update_name)
-t_boolout = boolOut(
-    'BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update)
-t_longout = longOut('LONGOUT', initial_value=2008, on_update=on_update)
-t_stringout = stringOut(
-    'STRINGOUT', initial_value='watevah', on_update=on_update)
-t_mbbo = mbbOut(
-    'MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1)
-
-def update_sin_wf(value):
-    print('update_sin_wf', value)
-    sin_wf.set(numpy.sin(
-        numpy.linspace(0, 2*numpy.pi*sin_ph.get(), sin_len.get())))
-sin_wf = Waveform('SIN', datatype = float, length = 1024)
-# Check we can update its value before iocInit as per #22
-sin_wf.set([1, 2, 3])
-sin_len = longOut(
-    'SINN', 0, 1024, initial_value=1024, on_update=update_sin_wf)
-sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf)
-
-
-wf_len = 32
-wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len))
-t_waveform_in = Waveform('WAVEFORM', wf)
-t_waveform_out = WaveformOut('WAVEFORM_OUT', wf, on_update = on_update)
-t_waveform_in2 = Waveform('WAVEFORM2', length = 10)
-
-t_longstring_in = Waveform('LONGSTRING', length = 256, datatype = numpy.uint8)
-
-
-def Update():
-    t_ai.set(3.14159)
-    t_boolin.set(True)
-    t_longin.set(365)
-    t_stringin.set('Another different string')
-    t_mbbi.set(0)
-
-def UpdateOut():
-    t_ao.set(3.14159)
-    t_boolout.set(True)
-    t_longout.set(365)
-    t_stringout.set('Another different string')
-    t_mbbo.set(2)
+# The publicly accessible records.
+t_ai = None
+t_ao = None
+
+def create_records():
+    global t_ai, t_ao
+
+    t_ai = aIn('AI', initial_value=12.34)
+
+    boolIn('BOOLIN', 'True', 'False', initial_value=False)
+    longIn('LONGIN', initial_value=33)
+    stringIn('STRINGIN', initial_value="Testing string")
+    mbbIn(
+        'MBBI', ('One', alarm.MAJOR_ALARM), 'Two', ('Three', "MINOR"),
+        initial_value=2)
+
+    t_ao = aOut('AO', initial_value=12.45, on_update_name=on_update_name)
+
+    boolOut('BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update)
+    longOut('LONGOUT', initial_value=2008, on_update=on_update)
+    stringOut('STRINGOUT', initial_value='watevah', on_update=on_update)
+    mbbOut('MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1)
+
+    def update_sin_wf(value):
+        print('update_sin_wf', value)
+        sin_wf.set(numpy.sin(
+            numpy.linspace(0, 2*numpy.pi*sin_ph.get(), sin_len.get())))
+    sin_wf = Waveform('SIN', datatype = float, length = 1024)
+    # Check we can update its value before iocInit as per #22
+    sin_wf.set([1, 2, 3])
+    sin_len = longOut(
+        'SINN', 0, 1024, initial_value=1024, on_update=update_sin_wf)
+    sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf)
+
+
+    wf_len = 32
+    wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len))
+    Waveform('WAVEFORM', wf)
+    WaveformOut('WAVEFORM_OUT', wf, on_update = on_update)
+    Waveform('WAVEFORM2', length = 10)
+
+    longStringIn('LONGSTRING', length = 256)
+
+create_records()
 
 softioc.devIocStats(ioc_name)
 
-__all__ = [
-    't_ai', 't_boolin',  't_longin',  't_stringin',  't_mbbi',
-    't_ao', 't_boolout', 't_longout', 't_stringout', 't_mbbo',
-    't_waveform_in', 't_waveform_in2', 't_waveform_out', 't_longstring_in',
-    'wf_len',
-    'Update', 'UpdateOut'
-]
+__all__ = ['t_ai', 't_ao', ]
 
 if __name__ == "__main__":
     WriteRecords("expected_records.db")
diff --git a/tests/test_record_values.py b/tests/test_record_values.py
new file mode 100644
index 00000000..17742c00
--- /dev/null
+++ b/tests/test_record_values.py
@@ -0,0 +1,797 @@
+import multiprocessing
+from typing import List
+import numpy
+import pytest
+import asyncio
+
+from enum import Enum
+from math import isnan, inf, nan
+
+from conftest import requires_cothread, enable_code_coverage
+
+from softioc import asyncio_dispatcher, builder, softioc
+from softioc.pythonSoftIoc import RecordWrapper
+
+# Test file for anything related to valid record values getting/setting
+
+
+# Test parameters
+DEVICE_NAME = "RECORD-VALUE-TESTS"
+TIMEOUT = 5  # Seconds
+
+VERY_LONG_STRING = "This is a fairly long string, the kind that someone " \
+    "might think to put into a record that can theoretically hold a huge " \
+    "string and so lets test it and prove that shall we?"
+
+
+
+def record_func_names(fixture_value):
+    """Provide a nice name for the record_func fixture"""
+    return fixture_value.__name__
+
+
+@pytest.fixture(
+    params=[
+        builder.aIn,
+        builder.aOut,
+        builder.longIn,
+        builder.longOut,
+        builder.boolIn,
+        builder.boolOut,
+        builder.Action,
+        builder.stringIn,
+        builder.stringOut,
+        builder.mbbIn,
+        builder.mbbOut,
+        builder.WaveformIn,
+        builder.WaveformOut,
+        builder.longStringIn,
+        builder.longStringOut,
+    ],
+    ids=record_func_names,
+)
+def record_func(request):
+    """The list of record creation functions"""
+    return request.param
+
+# A list of all In records, used to filter out various tests
+in_records = [
+    builder.aIn,
+    builder.boolIn,
+    builder.longIn,
+    builder.mbbIn,
+    builder.stringIn,
+    builder.WaveformIn,
+    builder.longStringIn,
+]
+
+def record_values_names(fixture_value):
+    """Provide a nice name for the tests in the record_values fixture"""
+    return (
+        fixture_value[1].__name__
+        + "-"
+        + type(fixture_value[2]).__name__
+        + "-"
+        + str(fixture_value[3])
+    )
+
+
+record_values_list = [
+    ("aIn_float", builder.aIn, 5.5, 5.5, float),
+    ("aOut_float", builder.aOut, 5.5, 5.5, float),
+    ("aIn_int", builder.aIn, 3, 3.0, float),
+    ("aOut_int", builder.aOut, 3, 3.0, float),
+    ("aIn_inf", builder.aIn, inf, inf, float),
+    ("aOut_inf", builder.aOut, inf, inf, float),
+    ("aIn_neginf", builder.aIn, -inf, -inf, float),
+    ("aOut_neginf", builder.aOut, -inf, -inf, float),
+    ("aIn_nan", builder.aIn, nan, nan, float),
+    ("aOut_nan", builder.aOut, nan, nan, float),
+    ("longIn_int", builder.longIn, 5, 5, int),
+    ("longOut_int", builder.longOut, 5, 5, int),
+    ("boolIn_int", builder.boolIn, 1, 1, int),
+    ("boolOut_int", builder.boolOut, 1, 1, int),
+    ("boolIn_true", builder.boolIn, True, 1, int),
+    ("boolOut_true", builder.boolOut, True, 1, int),
+    ("boolIn_false", builder.boolIn, False, 0, int),
+    ("boolOut_false", builder.boolOut, False, 0, int),
+    ("Action_int", builder.Action, 1, 1, int),
+    ("Action_true", builder.Action, True, 1, int),
+    ("Action_false", builder.Action, False, 0, int),
+    ("mbbIn_int", builder.mbbIn, 1, 1, int),
+    ("mbbOut_int", builder.mbbOut, 1, 1, int),
+    ("strIn_abc", builder.stringIn, "abc", "abc", str),
+    ("strOut_abc", builder.stringOut, "abc", "abc", str),
+    ("strIn_empty", builder.stringIn, "", "", str),
+    ("strOut_empty", builder.stringOut, "", "", str),
+    ("strin_utf8", builder.stringIn, "%a€b", "%a€b", str),  # Valid UTF-8
+    ("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str),  # Valid UTF-8
+    (
+        "wIn_list_int",
+        builder.WaveformIn,
+        [1, 2, 3],
+        numpy.array([1, 2, 3], dtype=numpy.int32),
+        numpy.ndarray,
+    ),
+    (
+        "wOut_list_int",
+        builder.WaveformOut,
+        [1, 2, 3],
+        numpy.array([1, 2, 3], dtype=numpy.int32),
+        numpy.ndarray,
+    ),
+    (
+        "wIn_list_float",
+        builder.WaveformIn,
+        [1.5, 2.6, 3.7],
+        numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64),
+        numpy.ndarray,
+    ),
+    (
+        "wOut_list_float",
+        builder.WaveformOut,
+        [1.5, 2.6, 3.7],
+        numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64),
+        numpy.ndarray,
+    ),
+    (
+        "wIn_int",
+        builder.WaveformIn,
+        567,
+        numpy.array([567.], dtype=numpy.float64),
+        numpy.ndarray,
+    ),
+    (
+        "wOut_int",
+        builder.WaveformOut,
+        567,
+        numpy.array([567.], dtype=numpy.float64),
+        numpy.ndarray,
+    ),
+    (
+        "wIn_float",
+        builder.WaveformIn,
+        12.345,
+        numpy.array([12.345], dtype=numpy.float64),
+        numpy.ndarray,
+    ),
+    (
+        "wOut_float",
+        builder.WaveformOut,
+        12.345,
+        numpy.array([12.345], dtype=numpy.float64),
+        numpy.ndarray,
+    ),
+    (
+        "wIn_bytes",
+        builder.WaveformIn,
+        b"HELLO\0WORLD",
+        numpy.array(
+            [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8
+        ),
+        numpy.ndarray,
+    ),
+    (
+        "wOut_bytes",
+        builder.WaveformOut,
+        b"HELLO\0WORLD",
+        numpy.array(
+            [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8
+        ),
+        numpy.ndarray,
+    ),
+    (
+        "longStringIn_str",
+        builder.longStringIn,
+        "ABC",
+        "ABC",
+        str,
+    ),
+    (
+        "longStringOut_str",
+        builder.longStringOut,
+        "ABC",
+        "ABC",
+        str,
+    ),
+    (
+        "longStringIn_long_str",
+        builder.longStringIn,
+        VERY_LONG_STRING,
+        VERY_LONG_STRING,
+        str,
+    ),
+    (
+        "longStringOut_long_str",
+        builder.longStringOut,
+        VERY_LONG_STRING,
+        VERY_LONG_STRING,
+        str,
+    ),
+    (
+        "longStringIn_unicode",
+        builder.longStringIn,
+        "%a€b",
+        "%a€b",
+        str
+    ),
+    (
+        "longStringOut_unicode",
+        builder.longStringOut,
+        "%a€b",
+        "%a€b",
+        str
+    ),
+]
+
+
+@pytest.fixture(params=record_values_list, ids=record_values_names)
+def record_values(request):
+    """A list of parameters for record value setting/getting tests.
+
+    Fields are:
+    - Record builder function
+    - Input value passed to .set()/initial_value/caput
+    - Expected output value after doing .get()/caget
+    - Expected type of the output value from .get()/caget"""
+    return request.param  # One item from the params list
+
+
+
+def record_value_asserts(
+    creation_func, actual_value, expected_value, expected_type
+):
+    """Asserts that the expected value and expected type are matched with
+    the actual value. Handles both scalar and waveform data"""
+    try:
+        if type(expected_value) == float and isnan(expected_value):
+            assert isnan(actual_value)  # NaN != Nan, so needs special case
+        elif creation_func in [builder.WaveformOut, builder.WaveformIn]:
+            assert numpy.array_equal(actual_value, expected_value)
+            assert type(actual_value) == expected_type
+        else:
+            assert actual_value == expected_value
+            assert type(actual_value) == expected_type
+    except AssertionError as e:
+        msg = (
+            "Failed with parameters: "
+            + str(creation_func)
+            + ", "
+            + str(actual_value)
+            + ", "
+            + str(expected_value)
+            + ", "
+            + str(expected_type)
+        )
+        # Add the parameters into the exception message. Seems to be the
+        # cleanest place to add it, as doing a print() or a logging.error()
+        # both cause the message to appear in a separate section of the pytest
+        # results, which can be difficult to spot
+        e.args += (msg,)
+        raise e
+
+
+class SetValueEnum(Enum):
+    """Enum to control when and how the record's value should be set"""
+
+    INITIAL_VALUE = 1
+    SET_BEFORE_INIT = 2
+    SET_AFTER_INIT = 3
+    NO_SET = 4
+    CAPUT = 5
+
+
+class GetValueEnum(Enum):
+    """Enum to control whether values are retrieved using .get() or caget()"""
+
+    GET = 1
+    CAGET = 2
+
+
+def run_ioc(record_configurations: list, conn, set_enum, get_enum):
+    """Creates a record and starts the IOC. `initial_value` will be set on
+    the record at different times based on the `set_enum` parameter."""
+
+    builder.SetDeviceName(DEVICE_NAME)
+
+    records: List[RecordWrapper] = []
+
+    # Create records from the given list
+    for configuration in record_configurations:
+        kwarg = {}
+
+        (
+            record_name,
+            creation_func,
+            initial_value,
+            expected_value,
+            expected_type,
+        ) = configuration
+
+        if set_enum == SetValueEnum.INITIAL_VALUE:
+            kwarg.update({"initial_value": initial_value})
+        elif creation_func in [builder.WaveformIn, builder.WaveformOut]:
+            kwarg = {"length": 50}  # Required when no value on creation
+            # Related to this issue:
+            # https://github.com/dls-controls/pythonSoftIOC/issues/37
+
+        out_rec = creation_func(record_name, **kwarg)
+
+        if set_enum == SetValueEnum.SET_BEFORE_INIT:
+            out_rec.set(initial_value)
+
+        records.append(out_rec)
+
+    dispatcher = asyncio_dispatcher.AsyncioDispatcher()
+    builder.LoadDatabase()
+    softioc.iocInit(dispatcher)
+    conn.send("IOC started")
+
+    # Record list and record config list should always be in line
+    for record, configuration in zip(records, record_configurations):
+        (
+            record_name,
+            creation_func,
+            initial_value,
+            expected_value,
+            expected_type,
+        ) = configuration
+
+        if set_enum == SetValueEnum.SET_AFTER_INIT:
+            record.set(initial_value)
+
+    for record in records:
+        if get_enum == GetValueEnum.GET:
+            conn.send(record.get())
+            # Some tests do a caput and require another .get()
+            if set_enum == SetValueEnum.CAPUT:
+                if conn.poll(TIMEOUT):
+                    val = conn.recv()
+                    if val is not None:
+                        conn.send(record.get())
+
+    conn.close()
+
+    # Keep process alive while main thread works.
+    # This is most applicable to CAGET tests.
+    asyncio.run_coroutine_threadsafe(
+        asyncio.sleep(TIMEOUT), dispatcher.loop
+    ).result()
+
+
+def run_test_function(
+    record_configurations: list, set_enum: SetValueEnum, get_enum: GetValueEnum
+):
+    """Run the test function using multiprocessing and check returned value is
+    expected value. set_enum and get_enum determine when the record's value is
+    set and how the value is retrieved, respectively."""
+
+    parent_conn, child_conn = multiprocessing.Pipe()
+
+    enable_code_coverage()
+
+    ioc_process = multiprocessing.Process(
+        target=run_ioc,
+        args=(record_configurations, child_conn, set_enum, get_enum),
+    )
+
+    ioc_process.start()
+
+    # Wait for message that IOC has started
+    if parent_conn.poll(TIMEOUT):
+        parent_conn.recv()
+    else:
+        pytest.fail("IOC process did not start before TIMEOUT expired")
+
+    try:
+        # Cannot do these imports before the subprocess starts, as cothread
+        # isn't threadsafe (in the way we require)
+        from cothread import Yield
+        from cothread.catools import caget, caput, _channel_cache
+        from cothread.dbr import DBR_CHAR_STR
+
+        # cothread remembers connected IOCs. As we potentially restart the same
+        # named IOC multiple times, we have to purge the cache else the
+        # result from caget/caput cache would be a DisconnectError during the
+        # second test
+        _channel_cache.purge()
+
+        for configuration in record_configurations:
+            (
+                record_name,
+                creation_func,
+                initial_value,
+                expected_value,
+                expected_type,
+            ) = configuration
+
+            # Infer some required keywords from parameters
+            kwargs = {}
+            put_kwarg = {}
+            if creation_func in [builder.longStringIn, builder.longStringOut]:
+                kwargs.update({"datatype": DBR_CHAR_STR})
+
+            if (creation_func in [builder.WaveformIn, builder.WaveformOut]
+                    and type(initial_value) is bytes):
+                # There's a bug in caput that means DBR_CHAR_STR doesn't
+                # truncate the array of the target record, meaning .get()
+                # returns all NELM rather than just NORD elements. Instead we
+                # encode the data ourselves
+                initial_value = numpy.frombuffer(
+                    initial_value, dtype = numpy.uint8)
+
+            if set_enum == SetValueEnum.CAPUT:
+                if get_enum == GetValueEnum.GET:
+                    if parent_conn.poll(TIMEOUT):
+                        parent_conn.recv()
+                    else:
+                        pytest.fail("IOC did not provide initial record value")
+                caput(
+                    DEVICE_NAME + ":" + record_name,
+                    initial_value,
+                    wait=True,
+                    **kwargs,
+                    **put_kwarg,
+                )
+
+                if get_enum == GetValueEnum.GET:
+                    parent_conn.send("Do another get!")
+                # Ensure IOC process has time to execute.
+                # I saw failures on MacOS where it appeared the IOC had not
+                # processed the put'ted value as the caget returned the same
+                # value as was originally passed in.
+                Yield(timeout=TIMEOUT)
+
+            if get_enum == GetValueEnum.GET:
+                if parent_conn.poll(TIMEOUT):
+                    rec_val = parent_conn.recv()
+                else:
+                    pytest.fail("IOC did not provide record value in queue")
+            else:
+                rec_val = caget(
+                    DEVICE_NAME + ":" + record_name,
+                    timeout=TIMEOUT,
+                    **kwargs,
+                )
+                # '+' operator used to convert cothread's types into Python
+                # native types e.g. "+ca_int" -> int
+                rec_val = +rec_val
+
+                if (
+                    creation_func in [builder.WaveformOut, builder.WaveformIn]
+                    and expected_value.dtype in [numpy.float64, numpy.int32]
+                ):
+                    print(
+                        "caget cannot distinguish between a waveform with 1"
+                        "element and a scalar value, and so always returns a "
+                        "scalar. Therefore we skip this check.")
+                    continue
+
+
+            record_value_asserts(
+                creation_func, rec_val, expected_value, expected_type
+            )
+    finally:
+        # Purge cache to suppress spurious "IOC disconnected" exceptions
+        _channel_cache.purge()
+
+        ioc_process.terminate()
+        ioc_process.join(timeout=TIMEOUT)
+        if ioc_process.exitcode is None:
+            pytest.fail("Process did not terminate")
+
+
+def skip_long_strings(record_values):
+    if (
+        record_values[0] in [builder.stringIn, builder.stringOut]
+        and len(record_values[1]) > 40
+    ):
+        pytest.skip("CAPut blocks strings > 40 characters.")
+
+
+class TestGetValue:
+    """Tests that use .get() to check whether values applied with .set(),
+    initial_value, or caput return the expected value"""
+
+    def test_value_pre_init_set(self, clear_records, record_values):
+        """Test that records provide the expected values on get calls when using
+        .set() and .get() before IOC initialisation occurs"""
+
+        (
+            record_name,
+            creation_func,
+            initial_value,
+            expected_value,
+            expected_type,
+        ) = record_values
+
+        kwarg = {}
+        if creation_func in [builder.WaveformIn, builder.WaveformOut]:
+            kwarg = {"length": 50}  # Required when no value on creation
+
+        out_rec = creation_func(record_name, **kwarg)
+        out_rec.set(initial_value)
+
+        record_value_asserts(
+            creation_func, out_rec.get(), expected_value, expected_type
+        )
+
+    def test_value_pre_init_initial_value(self, clear_records, record_values):
+        """Test that records provide the expected values on get calls when using
+        initial_value and .get() before IOC initialisation occurs"""
+
+        (
+            record_name,
+            creation_func,
+            initial_value,
+            expected_value,
+            expected_type,
+        ) = record_values
+
+        out_rec = creation_func(record_name, initial_value=initial_value)
+
+        record_value_asserts(
+            creation_func, out_rec.get(), expected_value, expected_type
+        )
+
+    @requires_cothread
+    def test_value_post_init_set(self):
+        """Test that records provide the expected values on get calls when using
+        .set() before IOC initialisation and .get() after initialisation"""
+        run_test_function(
+            record_values_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.GET
+        )
+
+    @requires_cothread
+    def test_value_post_init_initial_value(self):
+        """Test that records provide the expected values on get calls when using
+        initial_value during record creation and .get() after IOC initialisation
+        """
+        run_test_function(
+            record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.GET
+        )
+
+    @requires_cothread
+    def test_value_post_init_set_after_init(self):
+        """Test that records provide the expected values on get calls when using
+        .set() and .get() after IOC initialisation"""
+        run_test_function(
+            record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET
+        )
+
+    @requires_cothread
+    def test_value_post_init_caput(self):
+        """Test that records provide the expected values on get calls when using
+        caput and .get() after IOC initialisation"""
+
+        # Various conditions mean we cannot use the entire list of cases
+        filtered_list = []
+        for item in record_values_list:
+            if (
+                item[1] in [builder.stringIn, builder.stringOut]
+                and len(item[2]) > 40
+            ):
+                # caput blocks strings longer than 40 characters
+                continue
+
+            if item[1] not in in_records:
+                # In records block caput
+                filtered_list.append(item)
+
+        run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.GET)
+
+
+@requires_cothread
+class TestCagetValue:
+    """Tests that use Caget to check whether values applied with .set(),
+    initial_value, or caput return the expected value"""
+
+    def test_value_post_init_set(self):
+        """Test that records provide the expected values on get calls when using
+        .set() before IOC initialisation and caget after initialisation"""
+
+        # Various conditions mean we cannot use the entire list of cases
+        filtered_list = []
+        for item in record_values_list:
+            # .set() on In records doesn't update correctly.
+            # pythonSoftIOC issue #67
+            if item[1] not in in_records:
+                filtered_list.append(item)
+
+        run_test_function(
+            filtered_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.CAGET
+        )
+
+    @requires_cothread
+    def test_value_post_init_initial_value(self):
+        """Test that records provide the expected values on get calls when using
+        initial_value during record creation and caget after IOC initialisation
+        """
+
+        run_test_function(
+            record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.CAGET
+        )
+
+    @requires_cothread
+    def test_value_post_init_set_after_init(self):
+        """Test that records provide the expected values on get calls when using
+        .set() and caget after IOC initialisation"""
+
+        run_test_function(
+            record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.CAGET
+        )
+
+    def test_value_post_init_caput(self):
+        """Test that records provide the expected values on get calls when using
+        .set() before IOC initialisation and caget after initialisation"""
+
+        # Various conditions (e.g.) mean we cannot use the entire list of cases
+        filtered_list = []
+        for item in record_values_list:
+            # In records block caputs
+            if item[1] not in in_records:
+                filtered_list.append(item)
+
+        run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET)
+
+
+default_values_list = [
+    ("default_aOut", builder.aOut, None, 0.0, float),
+    ("default_aIn", builder.aIn, None, 0.0, float),
+    ("default_longOut", builder.longOut, None, 0, int),
+    ("default_longIn", builder.longIn, None, 0, int),
+    ("default_boolOut", builder.boolOut, None, 0, int),
+    ("default_boolIn", builder.boolIn, None, 0, int),
+    ("default_Action", builder.Action, None, 0, int),
+    ("default_stringOut", builder.stringOut, None, "", str),
+    ("default_stringIn", builder.stringIn, None, "", str),
+    ("default_mbbOut", builder.mbbOut, None, 0, int),
+    ("default_mbbIn", builder.mbbIn, None, 0, int),
+    ("default_wOut", builder.WaveformOut, None, numpy.empty(0), numpy.ndarray),
+    ("default_wIn", builder.WaveformIn, None, numpy.empty(0), numpy.ndarray),
+    ("default_longStringOut", builder.longStringOut, None, "", str),
+    ("default_longStringIn", builder.longStringIn, None, "", str),
+]
+
+
+class TestDefaultValue:
+    """Tests related to default values"""
+
+    @pytest.mark.parametrize(
+        "creation_func,expected_value,expected_type",
+        [
+            (builder.aOut, 0.0, float),
+            (builder.aIn, 0.0, float),
+            (builder.longOut, 0, int),
+            (builder.longIn, 0, int),
+            (builder.boolOut, 0, int),
+            (builder.boolIn, 0, int),
+            (builder.Action, 0, int),
+            (builder.stringOut, "", str),
+            (builder.stringIn, "", str),
+            (builder.mbbOut, 0, int),
+            (builder.mbbIn, 0, int),
+            (builder.WaveformOut, numpy.empty(0), numpy.ndarray),
+            (builder.WaveformIn, numpy.empty(0), numpy.ndarray),
+            (builder.longStringOut, "", str),
+            (builder.longStringIn, "", str),
+        ],
+    )
+    def test_value_default_pre_init(
+        self, creation_func, expected_value, expected_type, clear_records
+    ):
+        """Test that the correct default values are returned from .get() (before
+        record initialisation) when no initial_value or .set() is done"""
+        # Out records do not have default values until records are initialized
+
+        kwarg = {}
+        if creation_func in [builder.WaveformIn, builder.WaveformOut]:
+            kwarg = {"length": 50}  # Required when no value on creation
+
+        out_rec = creation_func("out-record", **kwarg)
+        record_value_asserts(
+            creation_func, out_rec.get(), expected_value, expected_type
+        )
+
+    @requires_cothread
+    def test_value_default_post_init(self):
+        """Test that records return the expected default value from .get after
+        they are initialised, having never had a value set"""
+
+        run_test_function(
+            default_values_list, SetValueEnum.NO_SET, GetValueEnum.GET
+        )
+
+    @requires_cothread
+    def test_value_default_post_init_caget(self):
+        """Test that records return the expected default value from CAGet after
+        they are initialised, having never had a value set"""
+
+        run_test_function(
+            default_values_list, SetValueEnum.NO_SET, GetValueEnum.CAGET
+        )
+
+
+class TestNoneValue:
+    """Various tests regarding record value of None"""
+
+    # We expect using None as a value will trigger one of these exceptions
+    expected_exceptions = (ValueError, TypeError, AttributeError)
+
+
+    @pytest.fixture
+    def record_func_reject_none(self, record_func):
+        """Parameterized fixture for all records that reject None"""
+        if record_func in [builder.WaveformIn, builder.WaveformOut]:
+            pytest.skip("None is accepted by Waveform records, as numpy "
+                        "treats it as NaN")
+        return record_func
+
+    def test_value_none_rejected_initial_value(
+        self, clear_records, record_func_reject_none
+    ):
+        """Test setting \"None\" as the initial_value raises an exception"""
+
+        kwarg = {}
+        if record_func_reject_none in [
+            builder.WaveformIn,
+            builder.WaveformOut,
+        ]:
+            kwarg = {"length": 50}  # Required when no value on creation
+
+        with pytest.raises(self.expected_exceptions):
+            record_func_reject_none("SOME-NAME", initial_value=None, **kwarg)
+
+    def test_value_none_rejected_set_before_init(
+        self, clear_records, record_func_reject_none
+    ):
+        """Test that setting \"None\" using .set() raises an exception"""
+
+        kwarg = {}
+        if record_func_reject_none in [builder.WaveformIn, builder.WaveformOut]:
+            kwarg = {"length": 50}  # Required when no value on creation
+
+        with pytest.raises(self.expected_exceptions):
+            record = record_func_reject_none("SOME-NAME", **kwarg)
+            record.set(None)
+
+    def none_value_test_func(self, record_func, queue):
+        """Start the IOC and catch the expected exception"""
+        kwarg = {}
+        if record_func in [builder.WaveformIn, builder.WaveformOut]:
+            kwarg = {"length": 50}  # Required when no value on creation
+
+        record = record_func("SOME-NAME", **kwarg)
+
+        builder.LoadDatabase()
+        softioc.iocInit()
+
+        try:
+            record.set(None)
+        except Exception as e:
+            queue.put(e)
+
+        queue.put(Exception("FAIL:Test did not raise exception during .set()"))
+
+    @requires_cothread
+    def test_value_none_rejected_set_after_init(self, record_func_reject_none):
+        """Test that setting \"None\" using .set() after IOC init raises an
+        exception"""
+        queue = multiprocessing.Queue()
+
+        enable_code_coverage()
+
+        process = multiprocessing.Process(
+            target=self.none_value_test_func,
+            args=(record_func_reject_none, queue),
+        )
+
+        process.start()
+
+        try:
+            exception = queue.get(timeout=5)
+
+            assert isinstance(exception, self.expected_exceptions)
+        finally:
+            process.terminate()
+            process.join(timeout=3)
diff --git a/tests/test_records.py b/tests/test_records.py
index 1d7af3d8..95d91013 100644
--- a/tests/test_records.py
+++ b/tests/test_records.py
@@ -1,23 +1,55 @@
+import multiprocessing
+import numpy
 import os
 import pytest
+import asyncio
 
-from softioc import builder
+from conftest import requires_cothread, _clear_records, enable_code_coverage
 
-import sim_records
+from softioc import asyncio_dispatcher, builder, softioc
 
+# Test file for miscellaneous tests related to records
+
+# Test parameters
+DEVICE_NAME = "RECORD-TESTS"
+TIMEOUT = 5  # Seconds
+
+def test_records(tmp_path, clear_records):
+    # Ensure we definitely unload all records that may be hanging over from
+    # previous tests, then create exactly one instance of expected records.
+    from sim_records import create_records
+    _clear_records()
+    create_records()
 
-def test_records(tmp_path):
     path = str(tmp_path / "records.db")
     builder.WriteRecords(path)
     expected = os.path.join(os.path.dirname(__file__), "expected_records.db")
     assert open(path).readlines()[5:] == open(expected).readlines()
 
+
 def test_enum_length_restriction():
     with pytest.raises(AssertionError):
         builder.mbbIn(
-                "ManyLabels", "one", "two", "three", "four", "five", "six",
-                "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen",
-                "fourteen", "fifteen", "sixteen", "seventeen")
+            "ManyLabels",
+            "one",
+            "two",
+            "three",
+            "four",
+            "five",
+            "six",
+            "seven",
+            "eight",
+            "nine",
+            "ten",
+            "eleven",
+            "twelve",
+            "thirteen",
+            "fourteen",
+            "fifteen",
+            "sixteen",
+            "seventeen",
+        )
+
 
 def test_DISP_defaults_on():
     """Test that all IN record types have DISP=1 set by default"""
@@ -45,9 +77,172 @@ def test_DISP_defaults_on():
         # Note: DISP attribute won't exist if field not specified
         assert record.DISP.Value() == 1
 
+
 def test_DISP_can_be_overridden():
     """Test that DISP can be forced off for In records"""
 
     record = builder.longIn("DISP-OFF", DISP=0)
     # Note: DISP attribute won't exist if field not specified
     assert record.DISP.Value() == 0
+
+def test_waveform_disallows_zero_length(clear_records):
+    """Test that WaveformIn/Out records throw an exception when being
+    initialized with a zero length array"""
+    with pytest.raises(AssertionError):
+        builder.WaveformIn("W_IN", [])
+    with pytest.raises(AssertionError):
+        builder.WaveformOut("W_OUT", [])
+
+def test_waveform_disallows_too_long_values(clear_records):
+    """Test that Waveform and longString records throw an exception when
+    an overlong value is written"""
+    w_in = builder.WaveformIn("W_IN", [1, 2, 3])
+    w_out = builder.WaveformOut("W_OUT", [1, 2, 3])
+
+    ls_in = builder.longStringIn("LS_IN", initial_value="ABC")
+    ls_out = builder.longStringOut("LS_OUT", initial_value="ABC")
+
+    with pytest.raises(AssertionError):
+        w_in.set([1, 2, 3, 4])
+    with pytest.raises(AssertionError):
+        w_out.set([1, 2, 3, 4])
+    with pytest.raises(AssertionError):
+        ls_in.set("ABCD")
+    with pytest.raises(AssertionError):
+        ls_out.set("ABCD")
+
+def validate_fixture_names(params):
+    """Provide nice names for the out_records fixture in TestValidate class"""
+    return params[0].__name__
+class TestValidate:
+    """Tests related to the validate callback"""
+
+    @pytest.fixture(
+        params=[
+            (builder.aOut, 7.89, 0),
+            (builder.boolOut, 1, 0),
+            (builder.Action, 1, 0),
+            (builder.longOut, 7, 0),
+            (builder.stringOut, "HI", ""),
+            (builder.mbbOut, 2, 0),
+            (builder.WaveformOut, [10, 11, 12], []),
+            (builder.longStringOut, "A LONGER HELLO", ""),
+        ],
+        ids=validate_fixture_names
+    )
+    def out_records(self, request):
+        """The list of Out records and an associated value to set """
+        return request.param
+
+    def validate_always_pass(self, record, new_val):
+        """Validation method that always allows changes"""
+        return True
+
+    def validate_always_fail(self, record, new_val):
+        """Validation method that always rejects changes"""
+        return False
+
+    def validate_ioc_test_func(self, record_func, queue, validate_pass: bool):
+        """Start the IOC with the specified validate method"""
+
+        builder.SetDeviceName(DEVICE_NAME)
+
+        kwarg = {}
+        if record_func in [builder.WaveformIn, builder.WaveformOut]:
+            kwarg = {"length": 50}  # Required when no value on creation
+
+        kwarg.update(
+            {
+                "validate": self.validate_always_pass
+                if validate_pass
+                else self.validate_always_fail
+            }
+        )
+
+        record_func("VALIDATE-RECORD", **kwarg)
+
+        dispatcher = asyncio_dispatcher.AsyncioDispatcher()
+        builder.LoadDatabase()
+        softioc.iocInit(dispatcher)
+
+        queue.put("IOC ready")
+
+        # Keep process alive while main thread works.
+        # This is most applicable to CAGET tests.
+        asyncio.run_coroutine_threadsafe(
+            asyncio.sleep(TIMEOUT), dispatcher.loop
+        ).result()
+
+    def validate_test_runner(
+            self,
+            creation_func,
+            new_value,
+            expected_value,
+            validate_pass: bool):
+
+        queue = multiprocessing.Queue()
+
+        enable_code_coverage()
+
+        process = multiprocessing.Process(
+            target=self.validate_ioc_test_func,
+            args=(creation_func, queue, validate_pass),
+        )
+
+        process.start()
+
+        try:
+            queue.get(timeout=5)  # Get the expected IOC initialised message
+
+            from cothread.catools import caget, caput, _channel_cache
+
+            # See other places in this file for why we call it
+            _channel_cache.purge()
+
+            kwargs = {}
+            if creation_func in [builder.longStringIn, builder.longStringOut]:
+                from cothread.dbr import DBR_CHAR_STR
+                kwargs.update({"datatype": DBR_CHAR_STR})
+
+            put_ret = caput(
+                DEVICE_NAME + ":" + "VALIDATE-RECORD",
+                new_value,
+                wait=True,
+                **kwargs,
+            )
+            assert put_ret.ok, "caput did not succeed"
+
+            ret_val = caget(
+                DEVICE_NAME + ":" + "VALIDATE-RECORD",
+                timeout=3,
+                **kwargs
+            )
+
+            if creation_func in [builder.WaveformOut, builder.WaveformIn]:
+                assert numpy.array_equal(ret_val, expected_value)
+            else:
+                assert ret_val == expected_value
+
+        finally:
+            process.terminate()
+            process.join(timeout=3)
+
+
+    @requires_cothread
+    def test_validate_allows_updates(self, out_records):
+        """Test that record values are updated correctly when validate
+        method allows it """
+
+        creation_func, value, _ = out_records
+
+        self.validate_test_runner(creation_func, value, value, True)
+
+
+    @requires_cothread
+    def test_validate_blocks_updates(self, out_records):
+        """Test that record values are not updated when validate method
+        always blocks updates"""
+
+        creation_func, value, default = out_records
+
+        self.validate_test_runner(creation_func, value, default, False)