diff --git a/ADApp/Makefile b/ADApp/Makefile
index 0b2f54e2b..32f440b0a 100644
--- a/ADApp/Makefile
+++ b/ADApp/Makefile
@@ -12,7 +12,8 @@ pluginSrc_DEPEND_DIRS += ADSrc
 DIRS += pluginTests
 pluginTests_DEPEND_DIRS += pluginSrc
 
-ifeq ($(WITH_PVA), YES)
+# if WITH_PVA or WITH_PVA = YES
+ifeq ($(findstring YES,$(WITH_PVA) $(WITH_PVXS)), YES)
   DIRS += ntndArrayConverterSrc
   ntndArrayConverterSrc_DEPEND_DIRS += ADSrc
   pluginSrc_DEPEND_DIRS += ntndArrayConverterSrc
diff --git a/ADApp/commonDriverMakefile b/ADApp/commonDriverMakefile
index 1347f01e5..872aa2edd 100644
--- a/ADApp/commonDriverMakefile
+++ b/ADApp/commonDriverMakefile
@@ -37,6 +37,12 @@ ifeq ($(WITH_PVA),YES)
   endif
 endif
 
+ifeq ($(WITH_PVXS),YES)
+  $(DBD_NAME)_DBD += NDPluginPvxs.dbd
+  PROD_LIBS += pvxs
+  PROD_LIBS += ntndArrayConverterPvxs
+endif
+
 ifeq ($(WITH_NETCDF),YES)
   $(DBD_NAME)_DBD += NDFileNetCDF.dbd
   ifeq ($(NETCDF_EXTERNAL),NO)
diff --git a/ADApp/commonLibraryMakefile b/ADApp/commonLibraryMakefile
index a159db671..99b99a74c 100644
--- a/ADApp/commonLibraryMakefile
+++ b/ADApp/commonLibraryMakefile
@@ -11,6 +11,12 @@ ifeq ($(WITH_PVA),YES)
   LIB_LIBS += pvData
 endif
 
+ifeq ($(WITH_PVXS),YES)
+  LIB_LIBS += ntndArrayConverterPvxs
+  LIB_LIBS += pvxs
+  LIB_LIBS += pvxsIoc
+endif
+
 ifeq ($(WITH_NETCDF),YES)
   ifeq ($(NETCDF_EXTERNAL),NO)
     LIB_LIBS += netCDF
diff --git a/ADApp/ntndArrayConverterSrc/Makefile b/ADApp/ntndArrayConverterSrc/Makefile
index 662520807..07695450d 100644
--- a/ADApp/ntndArrayConverterSrc/Makefile
+++ b/ADApp/ntndArrayConverterSrc/Makefile
@@ -4,19 +4,26 @@ include $(TOP)/configure/CONFIG
 #  ADD MACRO DEFINITIONS AFTER THIS LINE
 #=============================
 
-LIBRARY_IOC += ntndArrayConverter
-INC += ntndArrayConverterAPI.h
-INC += ntndArrayConverter.h
-LIB_SRCS += ntndArrayConverter.cpp
+ifeq ($(WITH_PVA), YES)
+	LIBRARY_IOC += ntndArrayConverter
+	INC += ntndArrayConverter.h
+	LIB_SRCS += ntndArrayConverter.cpp
+	LIB_LIBS += pvData
+	LIB_LIBS += nt
+endif
 
-USR_CPPFLAGS += -DBUILDING_ntndArrayConverter_API
+ifeq ($(WITH_PVXS), YES)
+	LIBRARY_IOC += ntndArrayConverterPvxs
+	INC += ntndArrayConverterPvxs.h
+	LIB_SRCS += ntndArrayConverterPvxs.cpp
+	LIB_LIBS += pvxs
+endif
 
+INC += ntndArrayConverterAPI.h
+USR_CPPFLAGS += -DBUILDING_ntndArrayConverter_API
 LIB_LIBS              += ADBase
-LIB_LIBS              += pvData
-LIB_LIBS              += nt
 LIB_LIBS              += asyn
 LIB_LIBS              += $(EPICS_BASE_IOC_LIBS)
-
 #=============================
 
 include $(TOP)/configure/RULES
diff --git a/ADApp/ntndArrayConverterSrc/ntndArrayConverterPvxs.cpp b/ADApp/ntndArrayConverterSrc/ntndArrayConverterPvxs.cpp
new file mode 100644
index 000000000..37a33921a
--- /dev/null
+++ b/ADApp/ntndArrayConverterSrc/ntndArrayConverterPvxs.cpp
@@ -0,0 +1,452 @@
+#include "ntndArrayConverterPvxs.h"
+#include <stdio.h>
+#include <string.h>
+#include <iostream>
+using namespace std;
+
+NTNDArrayConverterPvxs::NTNDArrayConverterPvxs (pvxs::Value value) : m_value(value) {
+    m_typeMap = {
+        {typeid(int8_t), NDAttrDataType_t::NDAttrInt8},
+        {typeid(uint8_t), NDAttrDataType_t::NDAttrUInt8},
+        {typeid(int16_t), NDAttrDataType_t::NDAttrInt16},
+        {typeid(uint16_t), NDAttrDataType_t::NDAttrUInt16},
+        {typeid(int32_t), NDAttrDataType_t::NDAttrInt32},
+        {typeid(uint32_t), NDAttrDataType_t::NDAttrUInt32},
+        {typeid(int64_t), NDAttrDataType_t::NDAttrInt64},
+        {typeid(uint64_t), NDAttrDataType_t::NDAttrUInt64},
+        {typeid(float_t), NDAttrDataType_t::NDAttrFloat32},
+        {typeid(double_t), NDAttrDataType_t::NDAttrFloat64}
+    };
+
+    m_fieldNameMap = {
+        {typeid(int8_t), "value->byteValue"},
+        {typeid(uint8_t), "value->ubyteValue"},
+        {typeid(int16_t), "value->shortValue"},
+        {typeid(uint16_t), "value->ushortValue"},
+        {typeid(int32_t), "value->intValue"},
+        {typeid(uint32_t), "value->uintValue"},
+        {typeid(int64_t), "value->longValue"},
+        {typeid(uint64_t), "value->ulongValue"},
+        {typeid(float_t), "value->floatValue"},
+        {typeid(double_t), "value->doubleValue"}
+    };
+}
+
+NDColorMode_t NTNDArrayConverterPvxs::getColorMode (void)
+{
+    auto attributes = m_value["attribute"].as<pvxs::shared_array<const pvxs::Value>>();
+    NDColorMode_t colorMode = NDColorMode_t::NDColorModeMono;
+    for (auto &attribute : attributes) {
+        if (attribute["name"].as<std::string>() == "ColorMode") {
+            colorMode = (NDColorMode_t) attribute["value"].as<int32_t>();
+            break;
+        }
+    }
+    return colorMode;
+}
+
+NTNDArrayInfo_t NTNDArrayConverterPvxs::getInfo (void)
+{
+    NTNDArrayInfo_t info = {0};
+
+    auto dims = m_value["dimension"].as<pvxs::shared_array<const pvxs::Value>>();
+    info.ndims = (int) dims.size();
+    info.nElements = 1;
+
+    for(int i = 0; i < info.ndims; ++i)
+    {
+        info.dims[i]    = dims[i]["size"].as<size_t>();
+        info.nElements *= info.dims[i];
+    }
+
+    // does not update info.codec if codec.name not found in Value
+    m_value["codec.name"].as<std::string>(info.codec);
+
+    NDDataType_t dt;
+    int bpe;
+
+    if (info.codec.empty()) {
+        switch (m_value["value->"].type().code) {
+            case pvxs::TypeCode::Int8A:      {dt = NDInt8;      break;}
+            case pvxs::TypeCode::UInt8A:     {dt = NDUInt8;     break;}
+            case pvxs::TypeCode::Int16A:     {dt = NDInt16;     break;}
+            case pvxs::TypeCode::UInt16A:    {dt = NDUInt16;    break;}
+            case pvxs::TypeCode::Int32A:     {dt = NDInt32;     break;}
+            case pvxs::TypeCode::UInt32A:    {dt = NDUInt32;    break;}
+            case pvxs::TypeCode::Int64A:     {dt = NDInt64;     break;}
+            case pvxs::TypeCode::UInt64A:    {dt = NDUInt64;    break;}
+            case pvxs::TypeCode::Float32A:   {dt = NDFloat32;   break;}
+            case pvxs::TypeCode::Float64A:   {dt = NDFloat64;   break;}
+            default: throw std::runtime_error("invalid value data type");
+        }
+    } else dt = (NDDataType_t) m_value["codec.parameters"].as<int32_t>();
+    switch (dt) {
+        case NDInt8:      {bpe = sizeof(int8_t);   break;}
+        case NDUInt8:     {bpe = sizeof(uint8_t);  break;}
+        case NDInt16:     {bpe = sizeof(int16_t);  break;}
+        case NDUInt16:    {bpe = sizeof(uint16_t); break;}
+        case NDInt32:     {bpe = sizeof(int32_t);  break;}
+        case NDUInt32:    {bpe = sizeof(uint32_t); break;}
+        case NDInt64:     {bpe = sizeof(int64_t);  break;}
+        case NDUInt64:    {bpe = sizeof(uint64_t); break;}
+        case NDFloat32:   {bpe = sizeof(float_t);  break;}
+        case NDFloat64:   {bpe = sizeof(double_t); break;}
+        default: throw std::runtime_error("Could not determine element size.");
+    }
+
+    info.dataType        = dt;
+    info.bytesPerElement = bpe;
+    info.totalBytes      = info.nElements*info.bytesPerElement;
+    info.colorMode       = getColorMode();
+
+    if(info.ndims > 0)
+    {
+        info.x.dim    = 0;
+        info.x.stride = 1;
+        info.x.size   = info.dims[0];
+    }
+
+    if(info.ndims > 1)
+    {
+        info.y.dim    = 1;
+        info.y.stride = 1;
+        info.y.size   = info.dims[1];
+    }
+
+    if(info.ndims == 3)
+    {
+        switch(info.colorMode)
+        {
+        case NDColorModeRGB1:
+            info.x.dim        = 1;
+            info.y.dim        = 2;
+            info.color.dim    = 0;
+            info.x.stride     = info.dims[0];
+            info.y.stride     = info.dims[0]*info.dims[1];
+            info.color.stride = 1;
+            break;
+
+        case NDColorModeRGB2:
+            info.x.dim        = 0;
+            info.y.dim        = 2;
+            info.color.dim    = 1;
+            info.x.stride     = 1;
+            info.y.stride     = info.dims[0]*info.dims[1];
+            info.color.stride = info.dims[0];
+            break;
+
+        case NDColorModeRGB3:
+            info.x.dim        = 1;
+            info.y.dim        = 2;
+            info.color.dim    = 0;
+            info.x.stride     = info.dims[0];
+            info.y.stride     = info.dims[0]*info.dims[1];
+            info.color.stride = 1;
+            break;
+
+        default:
+            info.x.dim        = 0;
+            info.y.dim        = 1;
+            info.color.dim    = 2;
+            info.x.stride     = 1;
+            info.y.stride     = info.dims[0];
+            info.color.stride = info.dims[0]*info.dims[1];
+            break;
+        }
+
+        info.x.size     = info.dims[info.x.dim];
+        info.y.size     = info.dims[info.y.dim];
+        info.color.size = info.dims[info.color.dim];
+    }
+
+    return info;
+}
+
+void NTNDArrayConverterPvxs::toArray (NDArray *dest)
+{
+    toValue(dest);
+    toDimensions(dest);
+    toTimeStamp(dest);
+    toDataTimeStamp(dest);
+    toAttributes(dest);
+
+    dest->uniqueId = m_value["uniqueId"].as<int32_t>();
+}
+
+void NTNDArrayConverterPvxs::fromArray (NDArray *src)
+{
+    fromValue(src);
+    fromDimensions(src);
+    fromTimeStamp(src);
+    fromDataTimeStamp(src);
+    fromAttributes(src);
+
+    m_value["uniqueId"] = src->uniqueId;
+}
+
+template <typename arrayType>
+void NTNDArrayConverterPvxs::toValue (NDArray *dest)
+{
+    NTNDArrayInfo_t info = getInfo();
+    dest->codec.name = info.codec;
+    dest->dataType = info.dataType;
+
+
+    std::string fieldName = m_fieldNameMap[typeid(arrayType)];
+
+    auto value = m_value[fieldName].as<pvxs::shared_array<const arrayType>>();
+    memcpy(dest->pData, value.data(), info.totalBytes);
+
+    if (!info.codec.empty())
+        dest->compressedSize = info.totalBytes;
+}
+
+void NTNDArrayConverterPvxs::toValue (NDArray *dest)
+{
+    switch (m_value["value->"].type().code) {
+    case pvxs::TypeCode::Int8A:     {toValue<int8_t>(dest); break;}
+    case pvxs::TypeCode::UInt8A:    {toValue<uint8_t>(dest); break;}
+    case pvxs::TypeCode::Int16A:    {toValue<int16_t>(dest); break;}
+    case pvxs::TypeCode::UInt16A:   {toValue<uint16_t>(dest); break;}
+    case pvxs::TypeCode::Int32A:    {toValue<int32_t>(dest); break;}
+    case pvxs::TypeCode::UInt32A:   {toValue<uint32_t>(dest); break;}
+    case pvxs::TypeCode::Int64A:    {toValue<int64_t>(dest); break;}
+    case pvxs::TypeCode::UInt64A:   {toValue<uint64_t>(dest); break;}
+    case pvxs::TypeCode::Float32A:  {toValue<float_t>(dest); break;}
+    case pvxs::TypeCode::Float64A:  {toValue<double_t>(dest); break;}
+    default: throw std::runtime_error("invalid value data type");
+    }
+}
+
+void NTNDArrayConverterPvxs::toDimensions (NDArray *dest)
+{
+    auto dims = m_value["dimension"].as<pvxs::shared_array<const pvxs::Value>>();
+    dest->ndims = (int)dims.size();
+
+    for(int i = 0; i < dest->ndims; ++i)
+    {
+        NDDimension_t *d = &dest->dims[i];
+        d->size    = dims[i]["size"].as<int32_t>();
+        d->offset  = dims[i]["offset"].as<int32_t>();
+        d->binning = dims[i]["binning"].as<int32_t>();
+        d->reverse = dims[i]["reverse"].as<bool>();
+    }
+}
+
+void NTNDArrayConverterPvxs::toTimeStamp (NDArray *dest)
+{
+    // NDArray uses EPICS time, pvAccess uses Posix time, need to convert
+    dest->epicsTS.secPastEpoch = (epicsUInt32)
+        m_value["timeStamp.secondsPastEpoch"].as<uint32_t>() - POSIX_TIME_AT_EPICS_EPOCH;
+    dest->epicsTS.nsec = (epicsUInt32)
+        m_value["timeStamp.nanoseconds"].as<uint32_t>();
+}
+
+void NTNDArrayConverterPvxs::toDataTimeStamp (NDArray *dest)
+{
+    // NDArray uses EPICS time, pvAccess uses Posix time, need to convert
+    dest->timeStamp = (epicsFloat64) 
+        (m_value["dataTimeStamp.nanoseconds"].as<double_t>() / 1e9) 
+        + m_value["dataTimeStamp.secondsPastEpoch"].as<uint32_t>() 
+        - POSIX_TIME_AT_EPICS_EPOCH;
+}
+
+template <typename valueType>
+void NTNDArrayConverterPvxs::toAttribute (NDArray *dest, pvxs::Value attribute)
+{
+    auto name = attribute["name"].as<std::string>();
+    auto desc = attribute["descriptor"].as<std::string>();
+    auto source = attribute["source"].as<std::string>();
+    NDAttrSource_t sourceType = (NDAttrSource_t) attribute["sourceType"].as<int32_t>();
+    valueType value = attribute["value"].as<valueType>();
+    NDAttrDataType_t dataType = m_typeMap[typeid(valueType)];
+
+    NDAttribute *attr = new NDAttribute(name.c_str(), desc.c_str(), sourceType, source.c_str(), dataType, (void*)&value);
+    dest->pAttributeList->add(attr);
+}
+
+void NTNDArrayConverterPvxs::toStringAttribute (NDArray *dest, pvxs::Value attribute)
+{
+    auto name = attribute["name"].as<std::string>();
+    auto desc = attribute["descriptor"].as<std::string>();
+    auto source = attribute["source"].as<std::string>();
+    NDAttrSource_t sourceType = (NDAttrSource_t) attribute["sourceType"].as<int32_t>();
+    auto value = attribute["value"].as<std::string>();
+
+    NDAttribute *attr = new NDAttribute(name.c_str(), desc.c_str(), sourceType, source.c_str(), NDAttrDataType_t::NDAttrString, (void*)value.c_str());
+    dest->pAttributeList->add(attr);
+}
+
+void NTNDArrayConverterPvxs::toUndefinedAttribute (NDArray *dest, pvxs::Value attribute)
+{
+    auto name = attribute["name"].as<std::string>();
+    auto desc = attribute["descriptor"].as<std::string>();
+    auto source = attribute["source"].as<std::string>();
+    NDAttrSource_t sourceType = (NDAttrSource_t) attribute["sourceType"].as<int32_t>();
+
+    NDAttribute *attr = new NDAttribute(name.c_str(), desc.c_str(), sourceType, source.c_str(), NDAttrDataType_t::NDAttrUndefined, NULL);
+    dest->pAttributeList->add(attr);
+}
+
+void NTNDArrayConverterPvxs::toAttributes (NDArray *dest)
+{
+    auto attributes = m_value["attribute"].as<pvxs::shared_array<const pvxs::Value>>();
+    for (size_t i=0; i < attributes.size(); i++) {
+        pvxs::Value value = attributes[i]["value"];
+        switch (attributes[i]["value->"].type().code) {
+            // use indirection on Any container to get specified type
+            case pvxs::TypeCode::Int8:        toAttribute<int8_t>    (dest, attributes[i]); break;
+            case pvxs::TypeCode::UInt8:       toAttribute<uint8_t>   (dest, attributes[i]); break;
+            case pvxs::TypeCode::Int16:       toAttribute<int16_t>   (dest, attributes[i]); break;
+            case pvxs::TypeCode::UInt16:      toAttribute<uint16_t>  (dest, attributes[i]); break;
+            case pvxs::TypeCode::Int32:       toAttribute<int32_t>   (dest, attributes[i]); break;
+            case pvxs::TypeCode::UInt32:      toAttribute<uint32_t>  (dest, attributes[i]); break;
+            case pvxs::TypeCode::Int64:       toAttribute<int64_t>   (dest, attributes[i]); break;
+            case pvxs::TypeCode::UInt64:      toAttribute<uint64_t>  (dest, attributes[i]); break;
+            case pvxs::TypeCode::Float32:     toAttribute<float_t>   (dest, attributes[i]); break;
+            case pvxs::TypeCode::Float64:     toAttribute<double_t>  (dest, attributes[i]); break;
+            case pvxs::TypeCode::String:      toStringAttribute      (dest, attributes[i]); break;
+            case pvxs::TypeCode::Null:        toUndefinedAttribute   (dest, attributes[i]); break;
+            default: throw std::runtime_error("invalid value data type");
+        }
+    }
+}
+
+template <typename dataType>
+struct freeNDArray {
+    NDArray *array;
+    // increase reference count of array, release on destructor
+    freeNDArray(NDArray *array) : array(array) { array->reserve(); }
+    void operator()(dataType *data) {
+        assert (array->pData == data);
+        array->release();
+    }
+};
+
+template <typename dataType>
+void NTNDArrayConverterPvxs::fromValue(NDArray *src) {
+    NDArrayInfo_t arrayInfo;
+    src->getInfo(&arrayInfo);
+
+    m_value["compressedSize"] = src->compressedSize;
+    m_value["uncompressedSize"] = arrayInfo.totalBytes;
+
+    std::string fieldName = m_fieldNameMap[typeid(dataType)];
+    auto val = pvxs::shared_array<dataType>(
+        (dataType*)src->pData,
+        // custom deletor
+        freeNDArray<dataType>(src),
+        arrayInfo.nElements);
+    m_value[fieldName] = val.freeze();
+
+    m_value["codec.name"] = src->codec.name; // compression codec
+    // The uncompressed data type would be lost when converting to NTNDArray,
+    // so we must store it somewhere. codec.parameters seems like a good place.
+    m_value["codec.parameters"] = (int32_t) src->dataType;
+}
+
+void NTNDArrayConverterPvxs::fromValue (NDArray *src) {
+    switch(src->dataType) {
+        case NDInt8:      {fromValue<int8_t>(src); break;};
+        case NDUInt8:     {fromValue<uint8_t>(src); break;};
+        case NDInt16:     {fromValue<int16_t>(src); break;};
+        case NDUInt16:    {fromValue<uint16_t>(src); break;};
+        case NDInt32:     {fromValue<int32_t>(src); break;};
+        case NDUInt32:    {fromValue<uint32_t>(src); break;};
+        case NDInt64:     {fromValue<int64_t>(src); break;};
+        case NDUInt64:    {fromValue<uint64_t>(src); break;};
+        case NDFloat32:   {fromValue<float_t>(src); break;};
+        case NDFloat64:   {fromValue<double_t>(src); break;};
+        default: {
+            throw std::runtime_error("invalid value data type");
+            break;
+        }
+    }
+}
+
+void NTNDArrayConverterPvxs::fromDimensions (NDArray *src) {
+    pvxs::shared_array<pvxs::Value> dims;
+    dims.resize(src->ndims);
+
+    for (int i = 0; i < src->ndims; i++) {
+        dims[i] = m_value["dimension"].allocMember()
+        .update("size", src->dims[i].size)
+        .update("offset", src->dims[i].offset)
+        .update("fullSize", src->dims[i].size)
+        .update("binning", src->dims[i].binning)
+        .update("reverse", src->dims[i].reverse);
+    }
+    m_value["dimension"] = dims.freeze();
+}
+
+void NTNDArrayConverterPvxs::fromDataTimeStamp (NDArray *src) {
+    double seconds = floor(src->timeStamp);
+    double nanoseconds = (src->timeStamp - seconds)*1e9;
+    // pvAccess uses Posix time, NDArray uses EPICS time, need to convert
+    seconds += POSIX_TIME_AT_EPICS_EPOCH;
+    m_value["dataTimeStamp.secondsPastEpoch"] = seconds;
+    m_value["dataTimeStamp.nanoseconds"] = nanoseconds;
+}
+
+void NTNDArrayConverterPvxs::fromTimeStamp (NDArray *src) {
+    // pvAccess uses Posix time, NDArray uses EPICS time, need to convert
+    m_value["timeStamp.secondsPastEpoch"] = src->epicsTS.secPastEpoch + POSIX_TIME_AT_EPICS_EPOCH;
+    m_value["timeStamp.nanoseconds"] = src->epicsTS.nsec;
+}
+
+template <typename valueType>
+void NTNDArrayConverterPvxs::fromAttribute (pvxs::Value destValue, NDAttribute *src)
+{
+    valueType value;
+    src->getValue(src->getDataType(), (void*)&value);
+    destValue["value"] = value;
+}
+
+void NTNDArrayConverterPvxs::fromStringAttribute (pvxs::Value destValue, NDAttribute *src)
+{
+    const char *value;
+    src->getValue(src->getDataType(), (void*)&value);
+    destValue["value"] = std::string(value);
+}
+
+void NTNDArrayConverterPvxs::fromAttributes (NDArray *src)
+{
+    NDAttributeList *srcList = src->pAttributeList;
+    NDAttribute *attr = NULL;
+    size_t i = 0;
+    pvxs::shared_array<pvxs::Value> attrs;
+    attrs.resize(src->pAttributeList->count());
+    while((attr = srcList->next(attr)))
+    {
+        NDAttrSource_t sourceType;
+        attr->getSourceInfo(&sourceType);
+        attrs[i] = m_value["attribute"].allocMember()
+        .update("name", attr->getName())
+        .update("descriptor", attr->getDescription())
+        .update("source", attr->getSource())
+        .update("sourceType", sourceType);
+
+        switch(attr->getDataType())
+        {
+        case NDAttrInt8:      fromAttribute<int8_t>(attrs[i], attr);       break;
+        case NDAttrUInt8:     fromAttribute<uint8_t>(attrs[i], attr);      break;
+        case NDAttrInt16:     fromAttribute<int16_t>(attrs[i], attr);      break;
+        case NDAttrUInt16:    fromAttribute<uint16_t>(attrs[i], attr);     break;
+        case NDAttrInt32:     fromAttribute<int32_t>(attrs[i], attr);      break;
+        case NDAttrUInt32:    fromAttribute<uint32_t>(attrs[i], attr);     break;
+        case NDAttrInt64:     fromAttribute<int64_t>(attrs[i], attr);      break;
+        case NDAttrUInt64:    fromAttribute<uint64_t>(attrs[i], attr);     break;
+        case NDAttrFloat32:   fromAttribute<float>(attrs[i], attr);        break;
+        case NDAttrFloat64:   fromAttribute<double>(attrs[i], attr);       break;
+        case NDAttrString:    fromStringAttribute(attrs[i], attr);         break;
+        case NDAttrUndefined: break;  // No need to assign value, leave as undefined
+        default:              throw std::runtime_error("invalid attribute data type");
+        }
+        ++i;
+    }
+    m_value["attribute"] = attrs.freeze();
+}
+
+
+
+
diff --git a/ADApp/ntndArrayConverterSrc/ntndArrayConverterPvxs.h b/ADApp/ntndArrayConverterSrc/ntndArrayConverterPvxs.h
new file mode 100644
index 000000000..a69dded5a
--- /dev/null
+++ b/ADApp/ntndArrayConverterSrc/ntndArrayConverterPvxs.h
@@ -0,0 +1,69 @@
+#include <math.h>
+
+#include <ntndArrayConverterAPI.h>
+#include <NDArray.h>
+#include <pvxs/data.h>
+#include <typeindex>
+#include <typeinfo>
+#include <unordered_map>
+
+typedef struct NTNDArrayInfo
+{
+    int ndims;
+    size_t dims[ND_ARRAY_MAX_DIMS];
+    size_t nElements, totalBytes;
+    int bytesPerElement;
+    NDColorMode_t colorMode;
+    NDDataType_t dataType;
+    std::string codec;
+
+    struct
+    {
+        int dim;
+        size_t size, stride;
+    }x, y, color;
+}NTNDArrayInfo_t;
+
+class NTNDARRAYCONVERTER_API NTNDArrayConverterPvxs
+{
+public:
+    NTNDArrayConverterPvxs(pvxs::Value value);
+    NTNDArrayInfo_t getInfo (void);
+    void toArray (NDArray *dest);
+    void fromArray (NDArray *src);
+
+private:
+    pvxs::Value m_value;
+    std::unordered_map<std::type_index, NDAttrDataType_t> m_typeMap;
+    std::unordered_map<std::type_index, std::string> m_fieldNameMap;
+    NDColorMode_t getColorMode (void);
+
+    template <typename arrayType>
+    void toValue (NDArray *dest);
+    void toValue (NDArray *dest);
+
+    void toDimensions (NDArray *dest);
+    void toTimeStamp (NDArray *dest);
+    void toDataTimeStamp (NDArray *dest);
+
+    template <typename valueType>
+    void toAttribute (NDArray *dest, pvxs::Value attribute);
+    void toStringAttribute (NDArray *dest, pvxs::Value attribute);
+    void toUndefinedAttribute (NDArray *dest, pvxs::Value attribute);
+    void toAttributes (NDArray *dest);
+
+    template <typename arrayType>
+    void fromValue (NDArray *src);
+    void fromValue (NDArray *src);
+    
+    void fromDimensions (NDArray *src);
+    void fromTimeStamp (NDArray *src);
+    void fromDataTimeStamp (NDArray *src);
+
+    template <typename valueType>
+    void fromAttribute (pvxs::Value destValue, NDAttribute *src);
+    void fromStringAttribute (pvxs::Value destValue, NDAttribute *src);
+    void fromAttributes (NDArray *src);
+};
+
+typedef std::shared_ptr<NTNDArrayConverterPvxs> NTNDArrayConverterPvxsPtr;
diff --git a/ADApp/pluginSrc/Makefile b/ADApp/pluginSrc/Makefile
index a6d95710e..a00a81d00 100644
--- a/ADApp/pluginSrc/Makefile
+++ b/ADApp/pluginSrc/Makefile
@@ -196,6 +196,12 @@ ifeq ($(WITH_PVA), YES)
   LIB_SRCS += NDPluginPva.cpp
 endif
 
+ifeq ($(WITH_PVXS), YES)
+  DBD += NDPluginPvxs.dbd
+  INC += NDPluginPvxs.h
+  LIB_SRCS += NDPluginPvxs.cpp
+endif
+
 ifeq ($(WITH_BLOSC), YES)
   USR_CXXFLAGS += -DHAVE_BLOSC
 endif
diff --git a/ADApp/pluginSrc/NDPluginPvxs.cpp b/ADApp/pluginSrc/NDPluginPvxs.cpp
new file mode 100644
index 000000000..36b3f3bc8
--- /dev/null
+++ b/ADApp/pluginSrc/NDPluginPvxs.cpp
@@ -0,0 +1,195 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <iostream>
+
+#include <pvxs/server.h>
+#include <pvxs/sharedpv.h>
+#include <pvxs/nt.h>
+#include <pvxs/iochooks.h>
+
+#include <iocsh.h>
+
+#include <ntndArrayConverterPvxs.h>
+
+#include "NDPluginPvxs.h"
+
+#include <epicsExport.h>
+
+static const char *driverName="NDPluginPvxs";
+
+using namespace std;
+
+class NDPLUGIN_API NTNDArrayRecordPvxs {
+
+private:
+    NTNDArrayRecordPvxs(string const & name, pvxs::Value value) : m_name(name), m_value(value) {};
+    NTNDArrayConverterPvxsPtr m_converter;
+    string m_name;
+    pvxs::Value m_value;
+    pvxs::server::SharedPV m_pv;
+
+public:
+    virtual ~NTNDArrayRecordPvxs () {};
+    static NTNDArrayRecordPvxsPtr create (string const & name);
+    virtual bool init ();
+    virtual void process () {}
+    void update (NDArray *pArray);
+};
+
+NTNDArrayRecordPvxsPtr NTNDArrayRecordPvxs::create (string const & name)
+{
+    pvxs::Value value = pvxs::nt::NTNDArray{}.build().create();
+    NTNDArrayRecordPvxsPtr pvRecord(new NTNDArrayRecordPvxs(name, value));
+    
+    if(!pvRecord->init())
+        pvRecord.reset();
+    
+    return pvRecord;
+}
+
+bool NTNDArrayRecordPvxs::init ()
+{
+    m_pv = pvxs::server::SharedPV(pvxs::server::SharedPV::buildReadonly());
+    m_pv.open(m_value);
+    // use singleton pvxs server
+    pvxs::ioc::server().addPV(m_name, m_pv);
+    m_converter.reset(new NTNDArrayConverterPvxs(m_value));
+    return true;
+}
+
+void NTNDArrayRecordPvxs::update(NDArray *pArray)
+{
+    m_converter->fromArray(pArray);
+    m_pv.post(m_value);
+}
+
+/** Callback function that is called by the NDArray driver with new NDArray
+  * data.
+  * \param[in] pArray  The NDArray from the callback.
+  */
+void NDPluginPvxs::processCallbacks(NDArray *pArray)
+{
+    static const char *functionName = "processCallbacks";
+
+    NDPluginDriver::beginProcessCallbacks(pArray);   // Base class method
+
+    // Most plugins can rely on endProcessCallbacks() to check for throttling, but this one cannot
+    // because the output is not an NDArray but a pvAccess server.  Need to check here.
+    if (throttled(pArray)) {
+        int droppedOutputArrays;
+        int arrayCounter;
+        getIntegerParam(NDPluginDriverDroppedOutputArrays, &droppedOutputArrays);
+        asynPrint(pasynUserSelf, ASYN_TRACE_WARNING,
+            "%s::%s maximum byte rate exceeded, dropped array uniqueId=%d\n",
+            driverName, functionName, pArray->uniqueId);
+        droppedOutputArrays++;
+        setIntegerParam(NDPluginDriverDroppedOutputArrays, droppedOutputArrays);
+        // Since this plugin has done no useful work we also decrement ArrayCounter
+        getIntegerParam(NDArrayCounter, &arrayCounter);
+        arrayCounter--;
+        setIntegerParam(NDArrayCounter, arrayCounter);
+    }
+    m_record->update(pArray);
+
+    // Do NDArray callbacks.  We need to copy the array and get the attributes
+    NDPluginDriver::endProcessCallbacks(pArray, true, true);
+
+    callParamCallbacks();
+}
+
+/** Constructor for NDPluginPvxs
+  * This plugin cannot block (ASYN_CANBLOCK=0) and is not multi-device (ASYN_MULTIDEVICE=0).
+  * \param[in] portName The name of the asyn port driver to be created.
+  * \param[in] queueSize The number of NDArrays that the input queue for this
+  *            plugin can hold when NDPluginDriverBlockingCallbacks=0.
+  *            Larger queues can decrease the number of dropped arrays, at the
+  *            expense of more NDArray buffers being allocated from the
+  *            underlying driver's NDArrayPool.
+  * \param[in] blockingCallbacks Initial setting for the
+  *            NDPluginDriverBlockingCallbacks flag. 0=callbacks are queued and
+  *            executed by the callback thread; 1 callbacks execute in the
+  *            thread of the driver doing the callbacks.
+  * \param[in] NDArrayPort Name of asyn port driver for initial source of
+  *            NDArray callbacks.
+  * \param[in] NDArrayAddr asyn port driver address for initial source of
+  *            NDArray callbacks.
+  * \param[in] pvName Name of the PV that will be served by the EPICSv4 server.
+  * \param[in] maxBuffers The maximum number of NDArray buffers that the NDArrayPool for this driver is
+  *            allowed to allocate. Set this to 0 to allow an unlimited number of buffers.
+  * \param[in] maxMemory The maximum amount of memory that the NDArrayPool for this driver is
+  *            allowed to allocate. Set this to 0 to allow an unlimited amount of memory.
+  * \param[in] priority The thread priority for the asyn port driver thread if ASYN_CANBLOCK is set in asynFlags.
+  *            This value should also be used for any other threads this object creates.
+  * \param[in] stackSize The stack size for the asyn port driver thread if ASYN_CANBLOCK is set in asynFlags.
+  *            This value should also be used for any other threads this object creates.
+  */
+NDPluginPvxs::NDPluginPvxs(const char *portName, int queueSize,
+        int blockingCallbacks, const char *NDArrayPort, int NDArrayAddr,
+        const char *pvName, int maxBuffers, size_t maxMemory, int priority, int stackSize)
+    /* Invoke the base class constructor */
+    : NDPluginDriver(portName, queueSize, blockingCallbacks,
+            NDArrayPort, NDArrayAddr, 1, maxBuffers, maxMemory, 0, 0,
+            0, 1, priority, stackSize, 1, true),
+      m_record(NTNDArrayRecordPvxs::create(pvName))
+{
+    createParam(NDPluginPvxsPvNameString, asynParamOctet, &NDPluginPvxsPvName);
+
+    /* Set the plugin type string */
+    setStringParam(NDPluginDriverPluginType, "NDPluginPvxs");
+
+    /* Set PvName */
+    setStringParam(NDPluginPvxsPvName, pvName);
+
+    /* Try to connect to the NDArray port */
+    connectToArrayPort();
+}
+
+/* Configuration routine.  Called directly, or from the iocsh function */
+extern "C" int NDPvxsConfigure(const char *portName, int queueSize,
+        int blockingCallbacks, const char *NDArrayPort, int NDArrayAddr,
+        const char *pvName, int maxBuffers, size_t maxMemory, int priority, int stackSize)
+{
+    NDPluginPvxs *pPlugin = new NDPluginPvxs(portName, queueSize, blockingCallbacks, NDArrayPort,
+                                           NDArrayAddr, pvName, maxBuffers, maxMemory, priority, stackSize);
+    return pPlugin->start();
+}
+
+/* EPICS iocsh shell commands */
+static const iocshArg initArg0 = { "portName",iocshArgString};
+static const iocshArg initArg1 = { "frame queue size",iocshArgInt};
+static const iocshArg initArg2 = { "blocking callbacks",iocshArgInt};
+static const iocshArg initArg3 = { "NDArrayPort",iocshArgString};
+static const iocshArg initArg4 = { "NDArrayAddr",iocshArgInt};
+static const iocshArg initArg5 = { "pvName",iocshArgString};
+static const iocshArg initArg6 = { "maxBuffers",iocshArgInt};
+static const iocshArg initArg7 = { "maxMemory",iocshArgInt};
+static const iocshArg initArg8 = { "priority",iocshArgInt};
+static const iocshArg initArg9 = { "stack size",iocshArgInt};
+static const iocshArg * const initArgs[] = {&initArg0,
+                                            &initArg1,
+                                            &initArg2,
+                                            &initArg3,
+                                            &initArg4,
+                                            &initArg5,
+                                            &initArg6,
+                                            &initArg7,
+                                            &initArg8,
+                                            &initArg9,};
+static const iocshFuncDef initFuncDef = {"NDPvxsConfigure",10,initArgs};
+static void initCallFunc(const iocshArgBuf *args)
+{
+    NDPvxsConfigure(args[0].sval, args[1].ival, args[2].ival,
+                   args[3].sval, args[4].ival, args[5].sval,
+                   args[6].ival, args[7].ival, args[8].ival,
+                   args[9].ival);
+}
+
+extern "C" void NDPvxsRegister(void)
+{
+    iocshRegister(&initFuncDef,initCallFunc);
+}
+
+extern "C" {
+epicsExportRegistrar(NDPvxsRegister);
+}
diff --git a/ADApp/pluginSrc/NDPluginPvxs.dbd b/ADApp/pluginSrc/NDPluginPvxs.dbd
new file mode 100644
index 000000000..8fe934c1b
--- /dev/null
+++ b/ADApp/pluginSrc/NDPluginPvxs.dbd
@@ -0,0 +1 @@
+registrar("NDPvxsRegister")
diff --git a/ADApp/pluginSrc/NDPluginPvxs.h b/ADApp/pluginSrc/NDPluginPvxs.h
new file mode 100644
index 000000000..a03e35973
--- /dev/null
+++ b/ADApp/pluginSrc/NDPluginPvxs.h
@@ -0,0 +1,33 @@
+#ifndef NDPluginPvxs_H
+#define NDPluginPvxs_H
+
+
+#include "NDPluginDriver.h"
+#include <vector>
+
+#define NDPluginPvxsPvNameString "PV_NAME"
+
+class NTNDArrayRecordPvxs;
+typedef std::shared_ptr<NTNDArrayRecordPvxs> NTNDArrayRecordPvxsPtr;
+
+/** Converts NDArray callback data into EPICS V4 NTNDArray data and exposes it
+  * as an EPICS V4 PV  */
+class NDPLUGIN_API NDPluginPvxs : public NDPluginDriver,
+                     public std::enable_shared_from_this<NDPluginPvxs>
+{
+public:
+    NDPluginPvxs(const char *portName, int queueSize, int blockingCallbacks,
+                 const char *NDArrayPort, int NDArrayAddr, const char *pvName,
+                 int maxBuffers, size_t maxMemory, int priority, int stackSize);
+
+    /* These methods override the virtual methods in the base class */
+    void processCallbacks(NDArray *pArray);
+
+protected:
+    int NDPluginPvxsPvName;
+
+private:
+    NTNDArrayRecordPvxsPtr m_record;
+};
+
+#endif
diff --git a/docs/ADCore/NDPluginPvxs.rst b/docs/ADCore/NDPluginPvxs.rst
new file mode 100644
index 000000000..1f9cba869
--- /dev/null
+++ b/docs/ADCore/NDPluginPvxs.rst
@@ -0,0 +1,66 @@
+NDPluginPvxs
+===========
+:author: James Souter, Diamond Light Source
+
+.. contents:: Contents
+
+Overview
+--------
+
+This plugin reimplements the functionality of :ref:`NDPluginPva`, but requires the linking
+of the pvxs support module, with no requirement for the pvAccessCPP, pvDatabaseCPP, pvDataCPP or
+normativeTypesCPP support modules. The plugin wraps a pvxs SharedPV, which is added to
+the singleton PVXS server that is started when the IOC starts. 
+
+NDPluginPvxs defines the following parameters.
+
+.. cssclass:: table-bordered table-striped table-hover
+.. flat-table::
+  :header-rows: 2
+  :widths: 5 5 5 70 5 5 5
+
+  * -
+    -
+    - **Parameter Definitions in NDPluginPvxs.h and EPICS Record Definitions in NDPvxs.template**
+  * - Parameter index variable
+    - asyn interface
+    - Access
+    - Description
+    - drvInfo string
+    - EPICS record name
+    - EPICS record type
+  * - NDPluginPvxsPvName
+    - asynOctet
+    - r/o
+    - Name of the EPICSv4 PV being served
+    - PV_NAME
+    - $(P)$(R)PvName_RBV
+    - waveform
+
+
+Configuration
+-------------
+
+The NDPluginPvxs plugin is created with the ``NDPvxsConfigure`` command,
+either from C/C++ or from the EPICS IOC shell.
+
+::
+
+   NDPvxsConfigure (const char *portName, int queueSize, int blockingCallbacks,
+                         const char *NDArrayPort, int NDArrayAddr, const char *pvName,
+                         size_t maxMemory, int priority, int stackSize)
+     
+
+For details on the meaning of the parameters to this function refer to
+the detailed documentation on the NDPvxsConfigure function in the
+`NDPluginPvxs.cpp
+documentation <../areaDetectorDoxygenHTML/_n_d_plugin_pvxs_8cpp.html>`__ and
+in the documentation for the constructor for the `NDPluginPvxs
+class <../areaDetectorDoxygenHTML/class_n_d_plugin_pvxs.html>`__.
+
+Starting the pvxs server
+----------------------------
+
+Unlike NDPluginPva, the EPICSv4 PV will be automatically served in an IOC if the plugin
+is configured, however the 
+`IOC must be built with pvxsIoc.dbd and the pvxs and pvxsIoc libraries. <https://epics-base.github.io/pvxs/building.html#including-pvxs-in-your-application>`