[hamradio-commits] [soapyremote] 01/02: New upstream version 0.3.1

Andreas E. Bombe aeb at moszumanska.debian.org
Thu Oct 20 00:55:49 UTC 2016


This is an automated email from the git hooks/post-receive script.

aeb pushed a commit to branch master
in repository soapyremote.

commit e30cf278eb1c98321bf3d48d1431354b3c8c2a10
Author: Andreas Bombe <aeb at debian.org>
Date:   Mon Sep 19 20:31:13 2016 +0200

    New upstream version 0.3.1
---
 .travis.yml                                |   42 +
 CMakeLists.txt                             |   44 +
 Changelog.txt                              |   41 +
 LICENSE_1_0.txt                            |   23 +
 README.md                                  |   19 +
 client/CMakeLists.txt                      |   14 +
 client/ClientStreamData.cpp                |  167 ++++
 client/ClientStreamData.hpp                |   52 ++
 client/LogAcceptor.cpp                     |  181 ++++
 client/LogAcceptor.hpp                     |   21 +
 client/Registration.cpp                    |  173 ++++
 client/Settings.cpp                        | 1360 ++++++++++++++++++++++++++++
 client/SoapyClient.hpp                     |  357 ++++++++
 client/Streaming.cpp                       |  466 ++++++++++
 common/CMakeLists.txt                      |   50 +
 common/SoapyHTTPUtils.cpp                  |   52 ++
 common/SoapyHTTPUtils.hpp                  |   42 +
 common/SoapyInfoUtils.hpp                  |   22 +
 common/SoapyInfoUtils.in.cpp               |   87 ++
 common/SoapyRPCPacker.cpp                  |  206 +++++
 common/SoapyRPCPacker.hpp                  |  117 +++
 common/SoapyRPCSocket.cpp                  |  448 +++++++++
 common/SoapyRPCSocket.hpp                  |  157 ++++
 common/SoapyRPCUnpacker.cpp                |  282 ++++++
 common/SoapyRPCUnpacker.hpp                |  115 +++
 common/SoapyRemoteConfig.hpp               |   19 +
 common/SoapyRemoteDefs.hpp                 |  285 ++++++
 common/SoapySSDPEndpoint.cpp               |  359 ++++++++
 common/SoapySSDPEndpoint.hpp               |   87 ++
 common/SoapySocketDefs.in.hpp              |  120 +++
 common/SoapyStreamEndpoint.cpp             |  380 ++++++++
 common/SoapyStreamEndpoint.hpp             |  159 ++++
 common/SoapyURLUtils.cpp                   |  217 +++++
 common/SoapyURLUtils.hpp                   |   87 ++
 debian/changelog                           |   29 +
 debian/compat                              |    1 +
 debian/control                             |   35 +
 debian/copyright                           |   32 +
 debian/docs                                |    1 +
 debian/rules                               |   17 +
 debian/soapysdr-server.install             |    1 +
 debian/soapysdr0.5-2-module-remote.install |    1 +
 debian/source/format                       |    1 +
 server/CMakeLists.txt                      |   36 +
 server/ClientHandler.cpp                   | 1358 +++++++++++++++++++++++++++
 server/ClientHandler.hpp                   |   44 +
 server/LogForwarding.cpp                   |   59 ++
 server/LogForwarding.hpp                   |   18 +
 server/ServerListener.cpp                  |  108 +++
 server/ServerStreamData.cpp                |  223 +++++
 server/ServerStreamData.hpp                |   65 ++
 server/SoapyServer.cpp                     |  121 +++
 server/SoapyServer.hpp                     |   41 +
 server/ThreadPrioHelper.hpp                |   13 +
 server/ThreadPrioUnix.cpp                  |   38 +
 server/ThreadPrioWindows.cpp               |   28 +
 server/msvc/getopt.h                       |  607 +++++++++++++
 57 files changed, 9128 insertions(+)

diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..5e33312
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,42 @@
+########################################################################
+## Travis CI config for SoapyRemote
+##
+## * installs SoapySDR from source
+## * confirms build and install
+## * checks that drivers load
+########################################################################
+
+sudo: required
+dist: trusty
+
+language: cpp
+compiler: gcc
+
+env:
+  global:
+    - INSTALL_PREFIX=/usr/local
+    - SOAPY_SDR_BRANCH=master
+  matrix:
+    - BUILD_TYPE=Debug
+    - BUILD_TYPE=Release
+
+install:
+  # install SoapySDR from source
+  - git clone https://github.com/pothosware/SoapySDR.git
+  - pushd SoapySDR
+  - git checkout ${SOAPY_SDR_BRANCH}
+  - mkdir build && cd build
+  - cmake ../ -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DENABLE_PYTHON=OFF -DENABLE_PYTHON3=OFF
+  - make && sudo make install
+  - popd
+
+script:
+  - mkdir build && cd build
+  - cmake ../ -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
+  - make && sudo make install
+  # print info about the install
+  - export LD_LIBRARY_PATH=${INSTALL_PREFIX}/lib:${LD_LIBRARY_PATH}
+  - export PATH=${INSTALL_PREFIX}/bin:${PATH}
+  - SoapySDRUtil --info
+  - SoapySDRUtil --check=remote
+  - SoapySDRServer --help
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..f74ce3c
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,44 @@
+########################################################################
+# Build Soapy SDR remote support
+########################################################################
+cmake_minimum_required(VERSION 2.8.7)
+project(SoapyRemote CXX C)
+
+# select build type to get optimization flags
+if(NOT CMAKE_BUILD_TYPE)
+   set(CMAKE_BUILD_TYPE "Release")
+   message(STATUS "Build type not specified: defaulting to release.")
+endif(NOT CMAKE_BUILD_TYPE)
+set(CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE} CACHE STRING "")
+
+#find soapy sdr
+find_package(SoapySDR "0.4.0" NO_MODULE)
+
+#enable c++11 features
+if(CMAKE_COMPILER_IS_GNUCXX)
+
+    #C++11 is a required language feature for this project
+    include(CheckCXXCompilerFlag)
+    CHECK_CXX_COMPILER_FLAG("-std=c++11" HAS_STD_CXX11)
+    if(HAS_STD_CXX11)
+        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
+    else(HAS_STD_CXX11)
+        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
+    endif()
+
+    #Thread support enabled (not the same as -lpthread)
+    list(APPEND SoapySDR_LIBRARIES -pthread)
+
+endif(CMAKE_COMPILER_IS_GNUCXX)
+
+#enable c++11 extensions for OSX
+if (APPLE)
+   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wc++11-extensions")
+endif(APPLE)
+
+#common headers used by client and server
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/common)
+
+add_subdirectory(common)
+add_subdirectory(client)
+add_subdirectory(server)
diff --git a/Changelog.txt b/Changelog.txt
new file mode 100644
index 0000000..5a58ccf
--- /dev/null
+++ b/Changelog.txt
@@ -0,0 +1,41 @@
+Release 0.3.1 (2016-09-01)
+==========================
+
+- Update debian files for SoapySDR module ABI format
+- Preserve packet+burst boundaries in server readStream() endpoint
+
+Release 0.3.0 (2016-07-10)
+==========================
+
+- Support for named register interface API
+- Support for getChannelInfo() API call
+- Moved time source calls to time API section
+- Support for getBandwidthRange() API call
+- Support for channel-specific settings API
+
+Release 0.2.1 (2016-04-21)
+==========================
+
+- Fixed element send size when using multi-channel streams
+- Fixed bug in SoapyRPCUnpacker for std::vector<size_t>
+- Fix use of error code in reportError() with strerror_r
+
+Release 0.2.0 (2015-11-21)
+==========================
+
+- Use formatToSize() from SoapySDR library
+- Support CU8 native conversion to CF32
+- Implement SSDP for automatic server discovery
+- Use native format for automatic selection
+- Support API to query tune argument info
+- Support API to query sensors meta info
+- Support API to query setting argument info
+- Support API to query stream argument info
+- Support API to query stream format types
+- Support API call to query clock rates
+- Support API call to query AGC mode
+
+Release 0.1.0 (2015-10-10)
+==========================
+
+- First release of SoapyRemote plugin module and server application
diff --git a/LICENSE_1_0.txt b/LICENSE_1_0.txt
new file mode 100644
index 0000000..36b7cd9
--- /dev/null
+++ b/LICENSE_1_0.txt
@@ -0,0 +1,23 @@
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6c16ff5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# Use any Soapy SDR remotely
+
+##Build Status
+
+- Travis: [![Travis Build Status](https://travis-ci.org/pothosware/SoapyRemote.svg?branch=master)](https://travis-ci.org/pothosware/SoapyRemote)
+
+## Dependencies
+
+* SoapySDR - https://github.com/pothosware/SoapySDR/wiki
+
+## Documentation
+
+* https://github.com/pothosware/SoapyRemote/wiki
+
+## Licensing information
+
+Use, modification and distribution is subject to the Boost Software
+License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
+http://www.boost.org/LICENSE_1_0.txt)
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
new file mode 100644
index 0000000..9fac9bf
--- /dev/null
+++ b/client/CMakeLists.txt
@@ -0,0 +1,14 @@
+########################################################################
+# Build the client plugin module
+########################################################################
+SOAPY_SDR_MODULE_UTIL(
+    TARGET remoteSupport
+    SOURCES
+        Registration.cpp
+        Settings.cpp
+        Streaming.cpp
+        LogAcceptor.cpp
+        ClientStreamData.cpp
+    LIBRARIES
+        SoapySDRRemoteCommon
+)
diff --git a/client/ClientStreamData.cpp b/client/ClientStreamData.cpp
new file mode 100644
index 0000000..7f46170
--- /dev/null
+++ b/client/ClientStreamData.cpp
@@ -0,0 +1,167 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "ClientStreamData.hpp"
+#include "SoapyStreamEndpoint.hpp"
+#include <cstring> //memcpy
+#include <cassert>
+#include <cstdint>
+
+ClientStreamData::ClientStreamData(void):
+    streamId(-1),
+    endpoint(nullptr),
+    readHandle(0),
+    readElemsLeft(0),
+    scaleFactor(0.0),
+    convertType(CONVERT_MEMCPY)
+{
+    return;
+}
+
+void ClientStreamData::convertRecvBuffs(void * const *buffs, const size_t numElems)
+{
+    assert(endpoint != nullptr);
+    assert(endpoint->getElemSize() != 0);
+    assert(endpoint->getNumChans() != 0);
+    assert(not recvBuffs.empty());
+
+    switch (convertType)
+    {
+    ///////////////////////////
+    case CONVERT_MEMCPY:
+    ///////////////////////////
+    {
+        size_t elemSize = endpoint->getElemSize();
+        for (size_t i = 0; i < recvBuffs.size(); i++)
+        {
+            std::memcpy(buffs[i], recvBuffs[i], numElems*elemSize);
+        }
+    }
+    break;
+
+    ///////////////////////////
+    case CONVERT_CF32_CS16:
+    ///////////////////////////
+    {
+        const float scale = float(1.0/scaleFactor);
+        for (size_t i = 0; i < recvBuffs.size(); i++)
+        {
+            auto in = (short *)recvBuffs[i];
+            auto out = (float *)buffs[i];
+            for (size_t j = 0; j < numElems*2; j++)
+            {
+                out[j] = float(in[j])*scale;
+            }
+        }
+    }
+    break;
+
+    ///////////////////////////
+    case CONVERT_CF32_CS8:
+    ///////////////////////////
+    {
+        const float scale = float(1.0/scaleFactor);
+        for (size_t i = 0; i < recvBuffs.size(); i++)
+        {
+            auto in = (int8_t *)recvBuffs[i];
+            auto out = (float *)buffs[i];
+            for (size_t j = 0; j < numElems*2; j++)
+            {
+                out[j] = float(in[j])*scale;
+            }
+        }
+    }
+    break;
+
+    ///////////////////////////
+    case CONVERT_CF32_CU8:
+    ///////////////////////////
+    {
+        const float scale = float(1.0/scaleFactor);
+        for (size_t i = 0; i < recvBuffs.size(); i++)
+        {
+            auto in = (int8_t *)recvBuffs[i];
+            auto out = (float *)buffs[i];
+            for (size_t j = 0; j < numElems*2; j++)
+            {
+                out[j] = float(in[j]-127)*scale;
+            }
+        }
+    }
+    break;
+    }
+}
+
+void ClientStreamData::convertSendBuffs(const void * const *buffs, const size_t numElems)
+{
+    assert(endpoint != nullptr);
+    assert(endpoint->getElemSize() != 0);
+    assert(endpoint->getNumChans() != 0);
+    assert(not sendBuffs.empty());
+
+    switch (convertType)
+    {
+    ///////////////////////////
+    case CONVERT_MEMCPY:
+    ///////////////////////////
+    {
+        size_t elemSize = endpoint->getElemSize();
+        for (size_t i = 0; i < sendBuffs.size(); i++)
+        {
+            std::memcpy(sendBuffs[i], buffs[i], numElems*elemSize);
+        }
+    }
+    break;
+
+    ///////////////////////////
+    case CONVERT_CF32_CS16:
+    ///////////////////////////
+    {
+        float scale = float(scaleFactor);
+        for (size_t i = 0; i < sendBuffs.size(); i++)
+        {
+            auto in = (float *)buffs[i];
+            auto out = (short *)sendBuffs[i];
+            for (size_t j = 0; j < numElems*2; j++)
+            {
+                out[j] = short(in[j]*scale);
+            }
+        }
+    }
+    break;
+
+    ///////////////////////////
+    case CONVERT_CF32_CS8:
+    ///////////////////////////
+    {
+        float scale = float(scaleFactor);
+        for (size_t i = 0; i < sendBuffs.size(); i++)
+        {
+            auto in = (float *)buffs[i];
+            auto out = (int8_t *)sendBuffs[i];
+            for (size_t j = 0; j < numElems*2; j++)
+            {
+                out[j] = int8_t(in[j]*scale);
+            }
+        }
+    }
+    break;
+
+    ///////////////////////////
+    case CONVERT_CF32_CU8:
+    ///////////////////////////
+    {
+        float scale = float(scaleFactor);
+        for (size_t i = 0; i < sendBuffs.size(); i++)
+        {
+            auto in = (float *)buffs[i];
+            auto out = (int8_t *)sendBuffs[i];
+            for (size_t j = 0; j < numElems*2; j++)
+            {
+                out[j] = int8_t(in[j]*scale) + 127;
+            }
+        }
+    }
+    break;
+    }
+}
diff --git a/client/ClientStreamData.hpp b/client/ClientStreamData.hpp
new file mode 100644
index 0000000..84c12e5
--- /dev/null
+++ b/client/ClientStreamData.hpp
@@ -0,0 +1,52 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRPCSocket.hpp"
+#include <vector>
+#include <string>
+
+class SoapyStreamEndpoint;
+
+enum ConvertTypes
+{
+    CONVERT_MEMCPY,
+    CONVERT_CF32_CS16,
+    CONVERT_CF32_CS8,
+    CONVERT_CF32_CU8,
+};
+
+struct ClientStreamData
+{
+    ClientStreamData(void);
+
+    //string formats in use
+    std::string localFormat;
+    std::string remoteFormat;
+
+    //this ID identifies the stream to the remote host
+    int streamId;
+
+    //datagram socket for stream endpoint
+    SoapyRPCSocket streamSock;
+
+    //datagram socket for status endpoint
+    SoapyRPCSocket statusSock;
+
+    //local side of the stream endpoint
+    SoapyStreamEndpoint *endpoint;
+
+    //buffer pointers to read/write API
+    std::vector<const void *> recvBuffs;
+    std::vector<void *> sendBuffs;
+
+    //read stream remainder tracking
+    size_t readHandle;
+    size_t readElemsLeft;
+
+    //converter implementations
+    double scaleFactor;
+    ConvertTypes convertType;
+    void convertRecvBuffs(void * const *buffs, const size_t numElems);
+    void convertSendBuffs(const void * const *buffs, const size_t numElems);
+};
diff --git a/client/LogAcceptor.cpp b/client/LogAcceptor.cpp
new file mode 100644
index 0000000..9ba2520
--- /dev/null
+++ b/client/LogAcceptor.cpp
@@ -0,0 +1,181 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "LogAcceptor.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyRPCSocket.hpp"
+#include "SoapyRPCPacker.hpp"
+#include "SoapyRPCUnpacker.hpp"
+#include <SoapySDR/Logger.hpp>
+#include <csignal> //sig_atomic_t
+#include <cassert>
+#include <mutex>
+#include <thread>
+#include <map>
+
+/***********************************************************************
+ * Log acceptor thread implementation
+ **********************************************************************/
+struct LogAcceptorThreadData
+{
+    LogAcceptorThreadData(void):
+        done(true),
+        thread(nullptr),
+        useCount(0)
+    {
+        return;
+    }
+
+    ~LogAcceptorThreadData(void)
+    {
+        this->shutdown();
+    }
+
+    void activate(void);
+
+    void shutdown(void);
+
+    void handlerLoop(void);
+
+    SoapyRPCSocket client;
+    std::string url;
+    sig_atomic_t done;
+    std::thread *thread;
+    sig_atomic_t useCount;
+};
+
+void LogAcceptorThreadData::activate(void)
+{
+    client = SoapyRPCSocket();
+    int ret = client.connect(url);
+    if (ret != 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyLogAcceptor::connect() FAIL: %s", client.lastErrorMsg());
+        done = true;
+        return;
+    }
+
+    try
+    {
+        //startup forwarding
+        SoapyRPCPacker packerStart(client);
+        packerStart & SOAPY_REMOTE_START_LOG_FORWARDING;
+        packerStart();
+        SoapyRPCUnpacker unpackerStart(client);
+        done = false;
+        thread = new std::thread(&LogAcceptorThreadData::handlerLoop, this);
+    }
+    catch (const std::exception &ex)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyLogAcceptor::reactivate() ", ex.what());
+        done = true;
+    }
+}
+
+void LogAcceptorThreadData::shutdown(void)
+{
+    try
+    {
+        //shutdown forwarding (ignore reply)
+        SoapyRPCPacker packerStop(client);
+        packerStop & SOAPY_REMOTE_STOP_LOG_FORWARDING;
+        packerStop();
+
+        //graceful disconnect (ignore reply)
+        SoapyRPCPacker packerHangup(client);
+        packerHangup & SOAPY_REMOTE_HANGUP;
+        packerHangup();
+    }
+    catch (const std::exception &ex)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyLogAcceptor::shutdown() ", ex.what());
+    }
+
+    //the thread will exit due to the requests above
+    assert(thread != nullptr);
+    thread->join();
+    delete thread;
+    done = true;
+    client = SoapyRPCSocket();
+}
+
+void LogAcceptorThreadData::handlerLoop(void)
+{
+    try
+    {
+        //loop while active to relay messages to logger
+        while (true)
+        {
+            SoapyRPCUnpacker unpackerLogMsg(client);
+            if (unpackerLogMsg.done()) break; //got stop reply
+            char logLevel = 0;
+            std::string message;
+            unpackerLogMsg & logLevel;
+            unpackerLogMsg & message;
+            SoapySDR::log(SoapySDR::LogLevel(logLevel), message);
+        }
+    }
+    catch (const std::exception &ex)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyLogAcceptor::handlerLoop() ", ex.what());
+    }
+
+    done = true;
+}
+
+/***********************************************************************
+ * log acceptor threads and subscription tracking
+ **********************************************************************/
+static std::mutex logMutex;
+
+//unique server id to thread data
+static std::map<std::string, LogAcceptorThreadData> handlers;
+
+//cleanup completed and restart on errors
+static void threadMaintenance(void)
+{
+    auto it = handlers.begin();
+    while (it != handlers.end())
+    {
+        auto &data = it->second;
+
+        //first time, or error occurred
+        if (data.done) data.activate();
+
+        //no subscribers, erase
+        if (data.useCount == 0) handlers.erase(it++);
+
+        //next element
+        else ++it;
+    }
+}
+
+/***********************************************************************
+ * client subscription hooks
+ **********************************************************************/
+SoapyLogAcceptor::SoapyLogAcceptor(const std::string &url, SoapyRPCSocket &sock)
+{
+    SoapyRPCPacker packer(sock);
+    packer & SOAPY_REMOTE_GET_SERVER_ID;
+    packer();
+    SoapyRPCUnpacker unpacker(sock);
+    unpacker & _serverId;
+
+    std::lock_guard<std::mutex> lock(logMutex);
+
+    auto &data = handlers[_serverId];
+    data.useCount++;
+    data.url = url;
+
+    threadMaintenance();
+}
+
+SoapyLogAcceptor::~SoapyLogAcceptor(void)
+{
+    std::lock_guard<std::mutex> lock(logMutex);
+
+    auto &data = handlers.at(_serverId);
+    data.useCount--;
+
+    threadMaintenance();
+}
diff --git a/client/LogAcceptor.hpp b/client/LogAcceptor.hpp
new file mode 100644
index 0000000..77a868f
--- /dev/null
+++ b/client/LogAcceptor.hpp
@@ -0,0 +1,21 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include <string>
+
+class SoapyRPCSocket;
+
+/*!
+ * Create a log acceptor to subscribe to log events from the remote server.
+ * The acceptor avoids redundant threads by reference counting subscribers.
+ */
+class SoapyLogAcceptor
+{
+public:
+    SoapyLogAcceptor(const std::string &url, SoapyRPCSocket &sock);
+    ~SoapyLogAcceptor(void);
+
+private:
+    std::string _serverId;
+};
diff --git a/client/Registration.cpp b/client/Registration.cpp
new file mode 100644
index 0000000..6e66f6d
--- /dev/null
+++ b/client/Registration.cpp
@@ -0,0 +1,173 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapyClient.hpp"
+#include "LogAcceptor.hpp"
+#include "SoapySSDPEndpoint.hpp"
+#include "SoapyURLUtils.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyRPCPacker.hpp"
+#include "SoapyRPCUnpacker.hpp"
+#include <SoapySDR/Registry.hpp>
+#include <SoapySDR/Logger.hpp>
+#include <thread>
+
+/***********************************************************************
+ * Args translator for nested keywords
+ **********************************************************************/
+static SoapySDR::Kwargs translateArgs(const SoapySDR::Kwargs &args)
+{
+    SoapySDR::Kwargs argsOut;
+
+    //stop infinite loops with special keyword
+    argsOut[SOAPY_REMOTE_KWARG_STOP] = "";
+
+    //copy all non-remote keys
+    for (auto &pair : args)
+    {
+        if (pair.first == "driver") continue; //don't propagate local driver filter
+        if (pair.first == "type") continue; //don't propagate local sub-type filter
+        if (pair.first.find(SOAPY_REMOTE_KWARG_PREFIX) == std::string::npos)
+        {
+            argsOut[pair.first] = pair.second;
+        }
+    }
+
+    //write all remote keys with prefix stripped
+    for (auto &pair : args)
+    {
+        if (pair.first.find(SOAPY_REMOTE_KWARG_PREFIX) == 0)
+        {
+            static const size_t offset = std::string(SOAPY_REMOTE_KWARG_PREFIX).size();
+            argsOut[pair.first.substr(offset)] = pair.second;
+        }
+    }
+
+    return argsOut;
+}
+
+/***********************************************************************
+ * Discovery routine -- connect to server when key specified
+ **********************************************************************/
+static std::vector<SoapySDR::Kwargs> findRemote(const SoapySDR::Kwargs &args)
+{
+    std::vector<SoapySDR::Kwargs> result;
+
+    if (args.count(SOAPY_REMOTE_KWARG_STOP) != 0) return result;
+
+    //no remote specified, use the discovery protocol
+    if (args.count("remote") == 0)
+    {
+        //On non-windows platforms the endpoint instance can last the
+        //duration of the process because it can be cleaned up safely.
+        //Windows has issues cleaning up threads and sockets on exit.
+        #ifndef _MSC_VER
+        static
+        #endif //_MSC_VER
+        auto ssdpEndpoint = SoapySSDPEndpoint::getInstance();
+
+        //enable forces new search queries
+        ssdpEndpoint->enablePeriodicSearch(true);
+
+        //wait maximum timeout for replies
+        std::this_thread::sleep_for(std::chrono::microseconds(SOAPY_REMOTE_SOCKET_TIMEOUT_US));
+
+        for (const auto &url : SoapySSDPEndpoint::getInstance()->getServerURLs())
+        {
+            auto argsWithURL = args;
+            argsWithURL["remote"] = url;
+            const auto subResult = findRemote(argsWithURL);
+            result.insert(result.end(), subResult.begin(), subResult.end());
+        }
+
+        return result;
+    }
+
+    //otherwise connect to a specific url and enumerate
+    auto url = SoapyURL(args.at("remote"));
+
+    //default url parameters when not specified
+    if (url.getScheme().empty()) url.setScheme("tcp");
+    if (url.getService().empty()) url.setService(SOAPY_REMOTE_DEFAULT_SERVICE);
+
+    //try to connect to the remote server
+    SoapySocketSession sess;
+    SoapyRPCSocket s;
+    int ret = s.connect(url.toString());
+    if (ret != 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyRemote::find() -- connect(%s) FAIL: %s", url.toString().c_str(), s.lastErrorMsg());
+        return result;
+    }
+
+    //find transaction
+    try
+    {
+        SoapyLogAcceptor logAcceptor(url.toString(), s);
+
+        SoapyRPCPacker packer(s);
+        packer & SOAPY_REMOTE_FIND;
+        packer & translateArgs(args);
+        packer();
+        SoapyRPCUnpacker unpacker(s);
+        unpacker & result;
+
+        //graceful disconnect
+        SoapyRPCPacker packerHangup(s);
+        packerHangup & SOAPY_REMOTE_HANGUP;
+        packerHangup();
+        SoapyRPCUnpacker unpackerHangup(s);
+    }
+    catch (const std::exception &ex)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyRemote::find() -- transact FAIL: %s", ex.what());
+    }
+
+    //remove instances of the stop key from the result
+    for (auto &resultArgs : result)
+    {
+        resultArgs.erase(SOAPY_REMOTE_KWARG_STOP);
+        if (resultArgs.count("driver") != 0)
+        {
+            resultArgs["remote:driver"] = resultArgs.at("driver");
+            resultArgs.erase("driver");
+        }
+        if (resultArgs.count("type") != 0)
+        {
+            resultArgs["remote:type"] = resultArgs.at("type");
+            resultArgs.erase("type");
+        }
+        resultArgs["remote"] = url.toString();
+    }
+
+    return result;
+}
+
+/***********************************************************************
+ * Factory routine -- connect to server and create remote device
+ **********************************************************************/
+static SoapySDR::Device *makeRemote(const SoapySDR::Kwargs &args)
+{
+    if (args.count(SOAPY_REMOTE_KWARG_STOP) != 0) //probably wont happen
+    {
+        throw std::runtime_error("SoapyRemoteDevice() -- factory loop");
+    }
+
+    if (args.count("remote") == 0)
+    {
+        throw std::runtime_error("SoapyRemoteDevice() -- missing URL");
+    }
+
+    auto url = SoapyURL(args.at("remote"));
+
+    //default url parameters when not specified
+    if (url.getScheme().empty()) url.setScheme("tcp");
+    if (url.getService().empty()) url.setService(SOAPY_REMOTE_DEFAULT_SERVICE);
+
+    return new SoapyRemoteDevice(url.toString(), translateArgs(args));
+}
+
+/***********************************************************************
+ * Registration
+ **********************************************************************/
+static SoapySDR::Registry registerRemote("remote", &findRemote, &makeRemote, SOAPY_SDR_ABI_VERSION);
diff --git a/client/Settings.cpp b/client/Settings.cpp
new file mode 100644
index 0000000..3e1fd93
--- /dev/null
+++ b/client/Settings.cpp
@@ -0,0 +1,1360 @@
+// Copyright (c) 2015-2016 Josh Blum
+// Copyright (c) 2016-2016 Bastille Networks
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapyClient.hpp"
+#include "LogAcceptor.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyRPCPacker.hpp"
+#include "SoapyRPCUnpacker.hpp"
+#include <SoapySDR/Logger.hpp>
+#include <stdexcept>
+
+//lazy fix for the const call issue -- FIXME
+#define _mutex const_cast<std::mutex &>(_mutex)
+#define _sock const_cast<SoapyRPCSocket &>(_sock)
+
+/*******************************************************************
+ * Constructor
+ ******************************************************************/
+
+SoapyRemoteDevice::SoapyRemoteDevice(const std::string &url, const SoapySDR::Kwargs &args):
+    _logAcceptor(nullptr)
+{
+    //try to connect to the remote server
+    int ret = _sock.connect(url);
+    if (ret != 0)
+    {
+        throw std::runtime_error("SoapyRemoteDevice("+url+") -- connect FAIL: " + _sock.lastErrorMsg());
+    }
+
+    //connect the log acceptor
+    _logAcceptor = new SoapyLogAcceptor(url, _sock);
+
+    //acquire device instance
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_MAKE;
+    packer & args;
+    packer();
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+SoapyRemoteDevice::~SoapyRemoteDevice(void)
+{
+    //cant throw in the destructor
+    try
+    {
+        //release device instance
+        SoapyRPCPacker packer(_sock);
+        packer & SOAPY_REMOTE_UNMAKE;
+        packer();
+        SoapyRPCUnpacker unpacker(_sock);
+
+        //graceful disconnect
+        SoapyRPCPacker packerHangup(_sock);
+        packerHangup & SOAPY_REMOTE_HANGUP;
+        packerHangup();
+        SoapyRPCUnpacker unpackerHangup(_sock);
+    }
+    catch (const std::exception &ex)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "~SoapyRemoteDevice() FAIL: %s", ex.what());
+    }
+
+    //disconnect the log acceptor (does not throw)
+    delete _logAcceptor;
+}
+
+/*******************************************************************
+ * Identification API
+ ******************************************************************/
+
+std::string SoapyRemoteDevice::getDriverKey(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_DRIVER_KEY;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+std::string SoapyRemoteDevice::getHardwareKey(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_HARDWARE_KEY;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::Kwargs SoapyRemoteDevice::getHardwareInfo(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_HARDWARE_INFO;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::Kwargs result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Channels API
+ ******************************************************************/
+
+void SoapyRemoteDevice::setFrontendMapping(const int direction, const std::string &mapping)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_FRONTEND_MAPPING;
+    packer & char(direction);
+    packer & mapping;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::getFrontendMapping(const int direction) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_FRONTEND_MAPPING;
+    packer & char(direction);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+size_t SoapyRemoteDevice::getNumChannels(const int direction) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_NUM_CHANNELS;
+    packer & char(direction);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::Kwargs SoapyRemoteDevice::getChannelInfo(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_CHANNEL_INFO;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::Kwargs result;
+    unpacker & result;
+    return result;
+}
+
+bool SoapyRemoteDevice::getFullDuplex(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_FULL_DUPLEX;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Antenna API
+ ******************************************************************/
+
+std::vector<std::string> SoapyRemoteDevice::listAntennas(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_ANTENNAS;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setAntenna(const int direction, const size_t channel, const std::string &name)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_ANTENNA;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::getAntenna(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_ANTENNA;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Frontend corrections API
+ ******************************************************************/
+
+bool SoapyRemoteDevice::hasDCOffsetMode(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_HAS_DC_OFFSET_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setDCOffsetMode(const int direction, const size_t channel, const bool automatic)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_DC_OFFSET_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer & automatic;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+bool SoapyRemoteDevice::getDCOffsetMode(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_DC_OFFSET_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+bool SoapyRemoteDevice::hasDCOffset(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_HAS_DC_OFFSET;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setDCOffset(const int direction, const size_t channel, const std::complex<double> &offset)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_DC_OFFSET;
+    packer & char(direction);
+    packer & int(channel);
+    packer & offset;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::complex<double> SoapyRemoteDevice::getDCOffset(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_DC_OFFSET;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::complex<double> result;
+    unpacker & result;
+    return result;
+}
+
+bool SoapyRemoteDevice::hasIQBalance(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_HAS_IQ_BALANCE_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setIQBalance(const int direction, const size_t channel, const std::complex<double> &balance)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_IQ_BALANCE_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer & balance;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::complex<double> SoapyRemoteDevice::getIQBalance(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_IQ_BALANCE_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::complex<double> result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Gain API
+ ******************************************************************/
+
+std::vector<std::string> SoapyRemoteDevice::listGains(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_GAINS;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+bool SoapyRemoteDevice::hasGainMode(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_HAS_GAIN_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setGainMode(const int direction, const size_t channel, const bool automatic)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_GAIN_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer & automatic;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+bool SoapyRemoteDevice::getGainMode(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_GAIN_MODE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setGain(const int direction, const size_t channel, const double value)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_GAIN;
+    packer & char(direction);
+    packer & int(channel);
+    packer & value;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+void SoapyRemoteDevice::setGain(const int direction, const size_t channel, const std::string &name, const double value)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_GAIN_ELEMENT;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer & value;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+double SoapyRemoteDevice::getGain(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_GAIN;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    double result;
+    unpacker & result;
+    return result;
+}
+
+double SoapyRemoteDevice::getGain(const int direction, const size_t channel, const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_GAIN_ELEMENT;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    double result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::Range SoapyRemoteDevice::getGainRange(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_GAIN_RANGE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::Range result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::Range SoapyRemoteDevice::getGainRange(const int direction, const size_t channel, const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_GAIN_RANGE_ELEMENT;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::Range result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Frequency API
+ ******************************************************************/
+
+void SoapyRemoteDevice::setFrequency(const int direction, const size_t channel, const double frequency, const SoapySDR::Kwargs &args)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_FREQUENCY;
+    packer & char(direction);
+    packer & int(channel);
+    packer & frequency;
+    packer & args;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+void SoapyRemoteDevice::setFrequency(const int direction, const size_t channel, const std::string &name, const double frequency, const SoapySDR::Kwargs &args)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_FREQUENCY_COMPONENT;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer & frequency;
+    packer & args;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+double SoapyRemoteDevice::getFrequency(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_FREQUENCY;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    double result;
+    unpacker & result;
+    return result;
+}
+
+double SoapyRemoteDevice::getFrequency(const int direction, const size_t channel, const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_FREQUENCY_COMPONENT;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    double result;
+    unpacker & result;
+    return result;
+}
+
+std::vector<std::string> SoapyRemoteDevice::listFrequencies(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_FREQUENCIES;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::RangeList SoapyRemoteDevice::getFrequencyRange(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_FREQUENCY_RANGE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::RangeList result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::RangeList SoapyRemoteDevice::getFrequencyRange(const int direction, const size_t channel, const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_FREQUENCY_RANGE_COMPONENT;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::RangeList result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::ArgInfoList SoapyRemoteDevice::getFrequencyArgsInfo(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_FREQUENCY_ARGS_INFO;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::ArgInfoList result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Sample Rate API
+ ******************************************************************/
+
+void SoapyRemoteDevice::setSampleRate(const int direction, const size_t channel, const double rate)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_SAMPLE_RATE;
+    packer & char(direction);
+    packer & int(channel);
+    packer & rate;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+double SoapyRemoteDevice::getSampleRate(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_SAMPLE_RATE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    double result;
+    unpacker & result;
+    return result;
+}
+
+std::vector<double> SoapyRemoteDevice::listSampleRates(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_SAMPLE_RATES;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<double> result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Bandwidth API
+ ******************************************************************/
+
+void SoapyRemoteDevice::setBandwidth(const int direction, const size_t channel, const double bw)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_BANDWIDTH;
+    packer & char(direction);
+    packer & int(channel);
+    packer & bw;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+double SoapyRemoteDevice::getBandwidth(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_BANDWIDTH;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    double result;
+    unpacker & result;
+    return result;
+}
+
+std::vector<double> SoapyRemoteDevice::listBandwidths(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_BANDWIDTHS;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<double> result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::RangeList SoapyRemoteDevice::getBandwidthRange(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_BANDWIDTH_RANGE;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::RangeList result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Clocking API
+ ******************************************************************/
+
+void SoapyRemoteDevice::setMasterClockRate(const double rate)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_MASTER_CLOCK_RATE;
+    packer & rate;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+double SoapyRemoteDevice::getMasterClockRate(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_MASTER_CLOCK_RATE;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    double result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::RangeList SoapyRemoteDevice::getMasterClockRates(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_MASTER_CLOCK_RATES;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::RangeList result;
+    unpacker & result;
+    return result;
+}
+
+std::vector<std::string> SoapyRemoteDevice::listClockSources(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_CLOCK_SOURCES;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setClockSource(const std::string &source)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_CLOCK_SOURCE;
+    packer & source;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::getClockSource(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_CLOCK_SOURCE;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Time API
+ ******************************************************************/
+
+std::vector<std::string> SoapyRemoteDevice::listTimeSources(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_TIME_SOURCES;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setTimeSource(const std::string &source)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_TIME_SOURCE;
+    packer & source;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::getTimeSource(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_TIME_SOURCE;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+bool SoapyRemoteDevice::hasHardwareTime(const std::string &what) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_HAS_HARDWARE_TIME;
+    packer & what;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    bool result;
+    unpacker & result;
+    return result;
+}
+
+long long SoapyRemoteDevice::getHardwareTime(const std::string &what) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_HARDWARE_TIME;
+    packer & what;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    long long result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::setHardwareTime(const long long timeNs, const std::string &what)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_HARDWARE_TIME;
+    packer & timeNs;
+    packer & what;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+void SoapyRemoteDevice::setCommandTime(const long long timeNs, const std::string &what)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SET_COMMAND_TIME;
+    packer & timeNs;
+    packer & what;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+/*******************************************************************
+ * Sensor API
+ ******************************************************************/
+
+std::vector<std::string> SoapyRemoteDevice::listSensors(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_SENSORS;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::ArgInfo SoapyRemoteDevice::getSensorInfo(const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_SENSOR_INFO;
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::ArgInfo result;
+    unpacker & result;
+    return result;
+}
+
+std::string SoapyRemoteDevice::readSensor(const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_SENSOR;
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+std::vector<std::string> SoapyRemoteDevice::listSensors(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_CHANNEL_SENSORS;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::ArgInfo SoapyRemoteDevice::getSensorInfo(const int direction, const size_t channel, const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_CHANNEL_SENSOR_INFO;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::ArgInfo result;
+    unpacker & result;
+    return result;
+}
+
+std::string SoapyRemoteDevice::readSensor(const int direction, const size_t channel, const std::string &name) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_CHANNEL_SENSOR;
+    packer & char(direction);
+    packer & int(channel);
+    packer & name;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * Register API
+ ******************************************************************/
+
+std::vector<std::string> SoapyRemoteDevice::listRegisterInterfaces(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_REGISTER_INTERFACES;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::writeRegister(const std::string &name, const unsigned addr, const unsigned value)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_REGISTER_NAMED;
+    packer & name;
+    packer & int(addr);
+    packer & int(value);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+unsigned SoapyRemoteDevice::readRegister(const std::string &name, const unsigned addr) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_REGISTER_NAMED;
+    packer & name;
+    packer & int(addr);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result;
+    unpacker & result;
+    return unsigned(result);
+}
+
+void SoapyRemoteDevice::writeRegister(const unsigned addr, const unsigned value)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_REGISTER;
+    packer & int(addr);
+    packer & int(value);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+unsigned SoapyRemoteDevice::readRegister(const unsigned addr) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_REGISTER;
+    packer & int(addr);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result;
+    unpacker & result;
+    return unsigned(result);
+}
+
+/*******************************************************************
+ * Settings API
+ ******************************************************************/
+
+SoapySDR::ArgInfoList SoapyRemoteDevice::getSettingInfo(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_SETTING_INFO;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::ArgInfoList result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::writeSetting(const std::string &key, const std::string &value)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_SETTING;
+    packer & key;
+    packer & value;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::readSetting(const std::string &key) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_SETTING;
+    packer & key;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+SoapySDR::ArgInfoList SoapyRemoteDevice::getSettingInfo(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_CHANNEL_SETTING_INFO;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapySDR::ArgInfoList result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::writeSetting(const int direction, const size_t channel, const std::string &key, const std::string &value)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_CHANNEL_SETTING;
+    packer & char(direction);
+    packer & int(channel);
+    packer & key;
+    packer & value;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::readSetting(const int direction, const size_t channel, const std::string &key) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_CHANNEL_SETTING;
+    packer & char(direction);
+    packer & int(channel);
+    packer & key;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * GPIO API
+ ******************************************************************/
+
+std::vector<std::string> SoapyRemoteDevice::listGPIOBanks(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_GPIO_BANKS;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::writeGPIO(const std::string &bank, const unsigned value)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_GPIO;
+    packer & bank;
+    packer & int(value);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+void SoapyRemoteDevice::writeGPIO(const std::string &bank, const unsigned value, const unsigned mask)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_GPIO_MASKED;
+    packer & bank;
+    packer & int(value);
+    packer & int(mask);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+unsigned SoapyRemoteDevice::readGPIO(const std::string &bank) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_GPIO;
+    packer & bank;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result;
+    unpacker & result;
+    return unsigned(result);
+}
+
+void SoapyRemoteDevice::writeGPIODir(const std::string &bank, const unsigned dir)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_GPIO_DIR;
+    packer & bank;
+    packer & int(dir);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+void SoapyRemoteDevice::writeGPIODir(const std::string &bank, const unsigned dir, const unsigned mask)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_GPIO_DIR_MASKED;
+    packer & bank;
+    packer & int(dir);
+    packer & int(mask);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+unsigned SoapyRemoteDevice::readGPIODir(const std::string &bank) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_GPIO_DIR;
+    packer & bank;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result;
+    unpacker & result;
+    return unsigned(result);
+}
+
+/*******************************************************************
+ * I2C API
+ ******************************************************************/
+
+void SoapyRemoteDevice::writeI2C(const int addr, const std::string &data)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_I2C;
+    packer & int(addr);
+    packer & data;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::readI2C(const int addr, const size_t numBytes)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_I2C;
+    packer & int(addr);
+    packer & int(numBytes);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
+
+/*******************************************************************
+ * SPI API
+ ******************************************************************/
+
+unsigned SoapyRemoteDevice::transactSPI(const int addr, const unsigned data, const size_t numBits)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_TRANSACT_SPI;
+    packer & int(addr);
+    packer & int(data);
+    packer & int(numBits);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result;
+    unpacker & result;
+    return unsigned(result);
+}
+
+/*******************************************************************
+ * UART API
+ ******************************************************************/
+
+std::vector<std::string> SoapyRemoteDevice::listUARTs(void) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_LIST_UARTS;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+void SoapyRemoteDevice::writeUART(const std::string &which, const std::string &data)
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_WRITE_UART;
+    packer & which;
+    packer & data;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+}
+
+std::string SoapyRemoteDevice::readUART(const std::string &which, const long timeoutUs) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_READ_UART;
+    packer & which;
+    packer & int(timeoutUs);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    return result;
+}
diff --git a/client/SoapyClient.hpp b/client/SoapyClient.hpp
new file mode 100644
index 0000000..8e5eb62
--- /dev/null
+++ b/client/SoapyClient.hpp
@@ -0,0 +1,357 @@
+// Copyright (c) 2015-2016 Josh Blum
+// Copyright (c) 2016-2016 Bastille Networks
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRPCSocket.hpp"
+#include <SoapySDR/Device.hpp>
+#include <mutex>
+
+class SoapyLogAcceptor;
+
+class SoapyRemoteDevice : public SoapySDR::Device
+{
+public:
+    SoapyRemoteDevice(const std::string &url, const SoapySDR::Kwargs &args);
+
+    ~SoapyRemoteDevice(void);
+
+    /*******************************************************************
+     * Identification API
+     ******************************************************************/
+
+    std::string getDriverKey(void) const;
+
+    std::string getHardwareKey(void) const;
+
+    SoapySDR::Kwargs getHardwareInfo(void) const;
+
+    /*******************************************************************
+     * Channels API
+     ******************************************************************/
+
+    void setFrontendMapping(const int direction, const std::string &mapping);
+
+    std::string getFrontendMapping(const int direction) const;
+
+    size_t getNumChannels(const int direction) const;
+
+    SoapySDR::Kwargs getChannelInfo(const int direction, const size_t channel) const;
+
+    bool getFullDuplex(const int direction, const size_t channel) const;
+
+    /*******************************************************************
+     * Stream API
+     ******************************************************************/
+
+    std::vector<std::string> __getRemoteOnlyStreamFormats(const int direction, const size_t channel) const;
+
+    std::vector<std::string> getStreamFormats(const int direction, const size_t channel) const;
+
+    std::string getNativeStreamFormat(const int direction, const size_t channel, double &fullScale) const;
+
+    SoapySDR::ArgInfoList getStreamArgsInfo(const int direction, const size_t channel) const;
+
+    SoapySDR::Stream *setupStream(
+        const int direction,
+        const std::string &format,
+        const std::vector<size_t> &channels,
+        const SoapySDR::Kwargs &args);
+
+    void closeStream(SoapySDR::Stream *stream);
+
+    size_t getStreamMTU(SoapySDR::Stream *stream) const;
+
+    int activateStream(
+        SoapySDR::Stream *stream,
+        const int flags,
+        const long long timeNs,
+        const size_t numElems);
+
+    int deactivateStream(
+        SoapySDR::Stream *stream,
+        const int flags,
+        const long long timeNs);
+
+    int readStream(
+        SoapySDR::Stream *stream,
+        void * const *buffs,
+        const size_t numElems,
+        int &flags,
+        long long &timeNs,
+        const long timeoutUs);
+
+    int writeStream(
+        SoapySDR::Stream *stream,
+        const void * const *buffs,
+        const size_t numElems,
+        int &flags,
+        const long long timeNs,
+        const long timeoutUs);
+
+    int readStreamStatus(
+        SoapySDR::Stream *stream,
+        size_t &chanMask,
+        int &flags,
+        long long &timeNs,
+        const long timeoutUs);
+
+    /*******************************************************************
+     * Direct buffer access API
+     ******************************************************************/
+
+    size_t getNumDirectAccessBuffers(SoapySDR::Stream *stream);
+
+    int getDirectAccessBufferAddrs(SoapySDR::Stream *stream, const size_t handle, void **buffs);
+
+    int acquireReadBuffer(
+        SoapySDR::Stream *stream,
+        size_t &handle,
+        const void **buffs,
+        int &flags,
+        long long &timeNs,
+        const long timeoutUs);
+
+    void releaseReadBuffer(
+        SoapySDR::Stream *stream,
+        const size_t handle);
+
+    int acquireWriteBuffer(
+        SoapySDR::Stream *stream,
+        size_t &handle,
+        void **buffs,
+        const long timeoutUs);
+
+    void releaseWriteBuffer(
+        SoapySDR::Stream *stream,
+        const size_t handle,
+        const size_t numElems,
+        int &flags,
+        const long long timeNs);
+
+    /*******************************************************************
+     * Antenna API
+     ******************************************************************/
+
+    std::vector<std::string> listAntennas(const int direction, const size_t channel) const;
+
+    void setAntenna(const int direction, const size_t channel, const std::string &name);
+
+    std::string getAntenna(const int direction, const size_t channel) const;
+
+    /*******************************************************************
+     * Frontend corrections API
+     ******************************************************************/
+
+    bool hasDCOffsetMode(const int direction, const size_t channel) const;
+
+    void setDCOffsetMode(const int direction, const size_t channel, const bool automatic);
+
+    bool getDCOffsetMode(const int direction, const size_t channel) const;
+
+    bool hasDCOffset(const int direction, const size_t channel) const;
+
+    void setDCOffset(const int direction, const size_t channel, const std::complex<double> &offset);
+
+    std::complex<double> getDCOffset(const int direction, const size_t channel) const;
+
+    bool hasIQBalance(const int direction, const size_t channel) const;
+
+    void setIQBalance(const int direction, const size_t channel, const std::complex<double> &balance);
+
+    std::complex<double> getIQBalance(const int direction, const size_t channel) const;
+
+    /*******************************************************************
+     * Gain API
+     ******************************************************************/
+
+    std::vector<std::string> listGains(const int direction, const size_t channel) const;
+
+    bool hasGainMode(const int direction, const size_t channel) const;
+
+    void setGainMode(const int direction, const size_t channel, const bool automatic);
+
+    bool getGainMode(const int direction, const size_t channel) const;
+
+    void setGain(const int direction, const size_t channel, const double value);
+
+    void setGain(const int direction, const size_t channel, const std::string &name, const double value);
+
+    double getGain(const int direction, const size_t channel) const;
+
+    double getGain(const int direction, const size_t channel, const std::string &name) const;
+
+    SoapySDR::Range getGainRange(const int direction, const size_t channel) const;
+
+    SoapySDR::Range getGainRange(const int direction, const size_t channel, const std::string &name) const;
+
+    /*******************************************************************
+     * Frequency API
+     ******************************************************************/
+
+    void setFrequency(const int direction, const size_t channel, const double frequency, const SoapySDR::Kwargs &args);
+
+    void setFrequency(const int direction, const size_t channel, const std::string &name, const double frequency, const SoapySDR::Kwargs &args);
+
+    double getFrequency(const int direction, const size_t channel) const;
+
+    double getFrequency(const int direction, const size_t channel, const std::string &name) const;
+
+    std::vector<std::string> listFrequencies(const int direction, const size_t channel) const;
+
+    SoapySDR::RangeList getFrequencyRange(const int direction, const size_t channel) const;
+
+    SoapySDR::RangeList getFrequencyRange(const int direction, const size_t channel, const std::string &name) const;
+
+    SoapySDR::ArgInfoList getFrequencyArgsInfo(const int direction, const size_t channel) const;
+
+    /*******************************************************************
+     * Sample Rate API
+     ******************************************************************/
+
+    void setSampleRate(const int direction, const size_t channel, const double rate);
+
+    double getSampleRate(const int direction, const size_t channel) const;
+
+    std::vector<double> listSampleRates(const int direction, const size_t channel) const;
+
+    /*******************************************************************
+     * Bandwidth API
+     ******************************************************************/
+
+    void setBandwidth(const int direction, const size_t channel, const double bw);
+
+    double getBandwidth(const int direction, const size_t channel) const;
+
+    std::vector<double> listBandwidths(const int direction, const size_t channel) const;
+
+    SoapySDR::RangeList getBandwidthRange(const int direction, const size_t channel) const;
+
+    /*******************************************************************
+     * Clocking API
+     ******************************************************************/
+
+    void setMasterClockRate(const double rate);
+
+    double getMasterClockRate(void) const;
+
+    SoapySDR::RangeList getMasterClockRates(void) const;
+
+    std::vector<std::string> listClockSources(void) const;
+
+    void setClockSource(const std::string &source);
+
+    std::string getClockSource(void) const;
+
+    /*******************************************************************
+     * Time API
+     ******************************************************************/
+
+    std::vector<std::string> listTimeSources(void) const;
+
+    void setTimeSource(const std::string &source);
+
+    std::string getTimeSource(void) const;
+
+    bool hasHardwareTime(const std::string &what) const;
+
+    long long getHardwareTime(const std::string &what) const;
+
+    void setHardwareTime(const long long timeNs, const std::string &what);
+
+    void setCommandTime(const long long timeNs, const std::string &what);
+
+    /*******************************************************************
+     * Sensor API
+     ******************************************************************/
+
+    std::vector<std::string> listSensors(void) const;
+
+    SoapySDR::ArgInfo getSensorInfo(const std::string &name) const;
+
+    std::string readSensor(const std::string &name) const;
+
+    std::vector<std::string> listSensors(const int direction, const size_t channel) const;
+
+    SoapySDR::ArgInfo getSensorInfo(const int direction, const size_t channel, const std::string &name) const;
+
+    std::string readSensor(const int direction, const size_t channel, const std::string &name) const;
+
+    /*******************************************************************
+     * Register API
+     ******************************************************************/
+
+    std::vector<std::string> listRegisterInterfaces(void) const;
+
+    void writeRegister(const std::string &name, const unsigned addr, const unsigned value);
+
+    unsigned readRegister(const std::string &name, const unsigned addr) const;
+
+    void writeRegister(const unsigned addr, const unsigned value);
+
+    unsigned readRegister(const unsigned addr) const;
+
+    /*******************************************************************
+     * Settings API
+     ******************************************************************/
+
+    SoapySDR::ArgInfoList getSettingInfo(void) const;
+
+    void writeSetting(const std::string &key, const std::string &value);
+
+    std::string readSetting(const std::string &key) const;
+
+    SoapySDR::ArgInfoList getSettingInfo(const int direction, const size_t channel) const;
+
+    void writeSetting(const int direction, const size_t channel, const std::string &key, const std::string &value);
+
+    std::string readSetting(const int direction, const size_t channel, const std::string &key) const;
+
+    /*******************************************************************
+     * GPIO API
+     ******************************************************************/
+
+    std::vector<std::string> listGPIOBanks(void) const;
+
+    void writeGPIO(const std::string &bank, const unsigned value);
+
+    void writeGPIO(const std::string &bank, const unsigned value, const unsigned mask);
+
+    unsigned readGPIO(const std::string &bank) const;
+
+    void writeGPIODir(const std::string &bank, const unsigned dir);
+
+    void writeGPIODir(const std::string &bank, const unsigned dir, const unsigned mask);
+
+    unsigned readGPIODir(const std::string &bank) const;
+
+    /*******************************************************************
+     * I2C API
+     ******************************************************************/
+
+    void writeI2C(const int addr, const std::string &data);
+
+    std::string readI2C(const int addr, const size_t numBytes);
+
+    /*******************************************************************
+     * SPI API
+     ******************************************************************/
+
+    unsigned transactSPI(const int addr, const unsigned data, const size_t numBits);
+
+    /*******************************************************************
+     * UART API
+     ******************************************************************/
+
+    std::vector<std::string> listUARTs(void) const;
+
+    void writeUART(const std::string &which, const std::string &data);
+
+    std::string readUART(const std::string &which, const long timeoutUs) const;
+
+private:
+    SoapySocketSession _sess;
+    SoapyRPCSocket _sock;
+    SoapyLogAcceptor *_logAcceptor;
+    std::mutex _mutex;
+};
diff --git a/client/Streaming.cpp b/client/Streaming.cpp
new file mode 100644
index 0000000..c143368
--- /dev/null
+++ b/client/Streaming.cpp
@@ -0,0 +1,466 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include <SoapySDR/Logger.hpp>
+#include <SoapySDR/Formats.hpp>
+#include "SoapyClient.hpp"
+#include "ClientStreamData.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyURLUtils.hpp"
+#include "SoapyRPCPacker.hpp"
+#include "SoapyRPCUnpacker.hpp"
+#include "SoapyStreamEndpoint.hpp"
+#include <algorithm> //std::min, std::find
+
+//lazy fix for the const call issue -- FIXME
+#define _mutex const_cast<std::mutex &>(_mutex)
+#define _sock const_cast<SoapyRPCSocket &>(_sock)
+
+std::vector<std::string> SoapyRemoteDevice::__getRemoteOnlyStreamFormats(const int direction, const size_t channel) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_STREAM_FORMATS;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::vector<std::string> result;
+    unpacker & result;
+    return result;
+}
+
+std::vector<std::string> SoapyRemoteDevice::getStreamFormats(const int direction, const size_t channel) const
+{
+    auto formats = __getRemoteOnlyStreamFormats(direction, channel);
+
+    //add complex floats when a conversion is possible
+    const bool hasCF32 = std::find(formats.begin(), formats.end(), SOAPY_SDR_CF32) != formats.end();
+    const bool hasCS16 = std::find(formats.begin(), formats.end(), SOAPY_SDR_CS16) != formats.end();
+    const bool hasCS8 = std::find(formats.begin(), formats.end(), SOAPY_SDR_CS8) != formats.end();
+    const bool hasCU8 = std::find(formats.begin(), formats.end(), SOAPY_SDR_CU8) != formats.end();
+    if (not hasCF32 and (hasCS16 or hasCS8 or hasCU8)) formats.push_back(SOAPY_SDR_CF32);
+
+    return formats;
+}
+
+std::string SoapyRemoteDevice::getNativeStreamFormat(const int direction, const size_t channel, double &fullScale) const
+{
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_GET_NATIVE_STREAM_FORMAT;
+    packer & char(direction);
+    packer & int(channel);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string result;
+    unpacker & result;
+    unpacker & fullScale;
+    return result;
+}
+
+SoapySDR::ArgInfoList SoapyRemoteDevice::getStreamArgsInfo(const int direction, const size_t channel) const
+{
+    //get the remote arguments first (careful with lock scope)
+    SoapySDR::ArgInfoList result;
+    {
+        std::lock_guard<std::mutex> lock(_mutex);
+        SoapyRPCPacker packer(_sock);
+        packer & SOAPY_REMOTE_GET_STREAM_ARGS_INFO;
+        packer & char(direction);
+        packer & int(channel);
+        packer();
+
+        SoapyRPCUnpacker unpacker(_sock);
+        unpacker & result;
+    }
+
+    //insert SoapyRemote stream arguments
+    double fullScale = 0.0;
+    SoapySDR::ArgInfo formatArg;
+    formatArg.key = "remote:format";
+    formatArg.value = this->getNativeStreamFormat(direction, channel, fullScale);
+    formatArg.name = "Remote Format";
+    formatArg.description = "The stream format used on the remote device.";
+    formatArg.type = SoapySDR::ArgInfo::STRING;
+    formatArg.options = __getRemoteOnlyStreamFormats(direction, channel);
+    result.push_back(formatArg);
+
+    SoapySDR::ArgInfo scaleArg;
+    scaleArg.key = "remote:scale";
+    scaleArg.value = std::to_string(fullScale);
+    scaleArg.name = "Remote Scale";
+    scaleArg.description = "The factor used to scale remote samples to full-scale floats.";
+    scaleArg.type = SoapySDR::ArgInfo::FLOAT;
+    result.push_back(scaleArg);
+
+    SoapySDR::ArgInfo mtuArg;
+    mtuArg.key = "remote:mtu";
+    mtuArg.value = std::to_string(SOAPY_REMOTE_DEFAULT_ENDPOINT_MTU);
+    mtuArg.name = "Remote MTU";
+    mtuArg.units = "bytes";
+    mtuArg.description = "The maximum datagram transfer size in bytes.";
+    mtuArg.type = SoapySDR::ArgInfo::INT;
+    result.push_back(mtuArg);
+
+    SoapySDR::ArgInfo windowArg;
+    windowArg.key = "remote:window";
+    windowArg.value = std::to_string(SOAPY_REMOTE_DEFAULT_ENDPOINT_WINDOW);
+    windowArg.name = "Remote Window";
+    windowArg.units = "bytes";
+    windowArg.description = "The size of the kernel socket buffer in bytes.";
+    windowArg.type = SoapySDR::ArgInfo::INT;
+    result.push_back(windowArg);
+
+    SoapySDR::ArgInfo priorityArg;
+    priorityArg.key = "remote:priority";
+    priorityArg.value = std::to_string(SOAPY_REMOTE_DEFAULT_THREAD_PRIORITY);
+    priorityArg.name = "Remote Priority";
+    priorityArg.description = "Specify the scheduling priority of the server forwarding threads.";
+    priorityArg.type = SoapySDR::ArgInfo::FLOAT;
+    priorityArg.range = SoapySDR::Range(-1.0, 1.0);
+    result.push_back(priorityArg);
+
+    return result;
+}
+
+SoapySDR::Stream *SoapyRemoteDevice::setupStream(
+    const int direction,
+    const std::string &localFormat,
+    const std::vector<size_t> &channels_,
+    const SoapySDR::Kwargs &args)
+{
+    //default to channel 0 when not specified
+    //the channels vector cannot be empty
+    //its used for stream endpoint allocation
+    auto channels = channels_;
+    if (channels.empty()) channels.push_back(0);
+
+    //use the remote device's native stream format and scale factor when the conversion is supported
+    double nativeScaleFactor = 0.0;
+    auto nativeFormat = this->getNativeStreamFormat(direction, channels.front(), nativeScaleFactor);
+    const bool useNative = (localFormat == nativeFormat) or
+        (localFormat == SOAPY_SDR_CF32 and nativeFormat == SOAPY_SDR_CS16) or
+        (localFormat == SOAPY_SDR_CF32 and nativeFormat == SOAPY_SDR_CS8) or
+        (localFormat == SOAPY_SDR_CF32 and nativeFormat == SOAPY_SDR_CU8);
+
+    //use the native format when the conversion is supported,
+    //otherwise use the client's local format for the default
+    auto remoteFormat = useNative?nativeFormat:localFormat;
+    const auto remoteFormatIt = args.find(SOAPY_REMOTE_KWARG_FORMAT);
+    if (remoteFormatIt != args.end()) remoteFormat = remoteFormatIt->second;
+
+    //use the native scale factor when the remote format is native,
+    //otherwise the default scale factor is the max signed integer
+    double scaleFactor = (remoteFormat == nativeFormat)?nativeScaleFactor:double(1 << ((SoapySDR::formatToSize(remoteFormat)*4)-1));
+    const auto scaleFactorIt = args.find(SOAPY_REMOTE_KWARG_SCALAR);
+    if (scaleFactorIt != args.end()) scaleFactor = std::stod(scaleFactorIt->second);
+
+    size_t mtu = SOAPY_REMOTE_DEFAULT_ENDPOINT_MTU;
+    const auto mtuIt = args.find(SOAPY_REMOTE_KWARG_MTU);
+    if (mtuIt != args.end()) mtu = size_t(std::stod(mtuIt->second));
+
+    size_t window = SOAPY_REMOTE_DEFAULT_ENDPOINT_WINDOW;
+    const auto windowIt = args.find(SOAPY_REMOTE_KWARG_WINDOW);
+    if (windowIt != args.end()) window = size_t(std::stod(windowIt->second));
+
+    SoapySDR::logf(SOAPY_SDR_INFO, "SoapyRemote::setup%sStream(remoteFormat=%s, localFormat=%s, scaleFactor=%g, mtu=%d, window=%d)",
+        (direction == SOAPY_SDR_RX)?"Rx":"Tx", remoteFormat.c_str(), localFormat.c_str(), scaleFactor, int(mtu), int(window));
+
+    //check supported formats
+    ConvertTypes convertType = CONVERT_MEMCPY;
+    if (localFormat == remoteFormat) convertType = CONVERT_MEMCPY;
+    else if (localFormat == SOAPY_SDR_CF32 and remoteFormat == SOAPY_SDR_CS16) convertType = CONVERT_CF32_CS16;
+    else if (localFormat == SOAPY_SDR_CF32 and remoteFormat == SOAPY_SDR_CS8) convertType = CONVERT_CF32_CS8;
+    else if (localFormat == SOAPY_SDR_CF32 and remoteFormat == SOAPY_SDR_CU8) convertType = CONVERT_CF32_CU8;
+    else throw std::runtime_error(
+        "SoapyRemote::setupStream() conversion not supported;"
+        "localFormat="+localFormat+", remoteFormat="+remoteFormat);
+
+    //allocate new local stream data
+    ClientStreamData *data = new ClientStreamData();
+    data->localFormat = localFormat;
+    data->remoteFormat = remoteFormat;
+    data->recvBuffs.resize(channels.size());
+    data->sendBuffs.resize(channels.size());
+    data->convertType = convertType;
+    data->scaleFactor = scaleFactor;
+
+    //extract socket node information
+    const auto localNode = SoapyURL(_sock.getsockname()).getNode();
+    const auto remoteNode = SoapyURL(_sock.getpeername()).getNode();
+
+    //bind the stream socket to an automatic port
+    const auto bindURL = SoapyURL("udp", localNode, "0").toString();
+    int ret = data->streamSock.bind(bindURL);
+    if (ret != 0)
+    {
+        const std::string errorMsg = data->streamSock.lastErrorMsg();
+        delete data;
+        throw std::runtime_error("SoapyRemote::setupStream("+bindURL+") -- bind FAIL: " + errorMsg);
+    }
+    SoapySDR::logf(SOAPY_SDR_INFO, "Client side stream bound to %s", data->streamSock.getsockname().c_str());
+    const auto clientBindPort = SoapyURL(data->streamSock.getsockname()).getService();
+
+    //bind the status socket to an automatic port
+    ret = data->statusSock.bind(bindURL);
+    if (ret != 0)
+    {
+        const std::string errorMsg = data->statusSock.lastErrorMsg();
+        delete data;
+        throw std::runtime_error("SoapyRemote::setupStream("+bindURL+") -- bind FAIL: " + errorMsg);
+    }
+    SoapySDR::logf(SOAPY_SDR_INFO, "Client side status bound to %s", data->streamSock.getsockname().c_str());
+    const auto statusBindPort = SoapyURL(data->statusSock.getsockname()).getService();
+
+    //setup the remote end of the stream
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_SETUP_STREAM;
+    packer & char(direction);
+    packer & remoteFormat;
+    packer & channels;
+    packer & args;
+    packer & clientBindPort;
+    packer & statusBindPort;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    std::string serverBindPort;
+    unpacker & data->streamId;
+    unpacker & serverBindPort;
+
+    //connect the stream socket to the specified port
+    const auto connectURL = SoapyURL("udp", remoteNode, serverBindPort).toString();
+    ret = data->streamSock.connect(connectURL);
+    if (ret != 0)
+    {
+        const std::string errorMsg = data->streamSock.lastErrorMsg();
+        delete data;
+        throw std::runtime_error("SoapyRemote::setupStream("+connectURL+") -- connect FAIL: " + errorMsg);
+    }
+    SoapySDR::logf(SOAPY_SDR_INFO, "Client side stream connected to %s", data->streamSock.getpeername().c_str());
+
+    //create endpoint
+    data->endpoint = new SoapyStreamEndpoint(data->streamSock, data->statusSock,
+        direction == SOAPY_SDR_RX, channels.size(), SoapySDR::formatToSize(remoteFormat), mtu, window);
+
+    return (SoapySDR::Stream *)data;
+}
+
+void SoapyRemoteDevice::closeStream(SoapySDR::Stream *stream)
+{
+    auto data = (ClientStreamData *)stream;
+
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_CLOSE_STREAM;
+    packer & data->streamId;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+
+    //cleanup local stream data
+    delete data->endpoint;
+    delete data;
+}
+
+size_t SoapyRemoteDevice::getStreamMTU(SoapySDR::Stream *stream) const
+{
+    auto data = (ClientStreamData *)stream;
+    return data->endpoint->getBuffSize();
+    return SoapySDR::Device::getStreamMTU(stream);
+}
+
+int SoapyRemoteDevice::activateStream(
+    SoapySDR::Stream *stream,
+    const int flags,
+    const long long timeNs,
+    const size_t numElems)
+{
+    auto data = (ClientStreamData *)stream;
+
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_ACTIVATE_STREAM;
+    packer & data->streamId;
+    packer & flags;
+    packer & timeNs;
+    packer & int(numElems);
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result = 0;
+    unpacker & result;
+    return result;
+}
+
+int SoapyRemoteDevice::deactivateStream(
+    SoapySDR::Stream *stream,
+    const int flags,
+    const long long timeNs)
+{
+    auto data = (ClientStreamData *)stream;
+
+    std::lock_guard<std::mutex> lock(_mutex);
+    SoapyRPCPacker packer(_sock);
+    packer & SOAPY_REMOTE_DEACTIVATE_STREAM;
+    packer & data->streamId;
+    packer & flags;
+    packer & timeNs;
+    packer();
+
+    SoapyRPCUnpacker unpacker(_sock);
+    int result = 0;
+    unpacker & result;
+    return result;
+}
+
+int SoapyRemoteDevice::readStream(
+    SoapySDR::Stream *stream,
+    void * const *buffs,
+    const size_t numElems,
+    int &flags,
+    long long &timeNs,
+    const long timeoutUs)
+{
+    auto data = (ClientStreamData *)stream;
+
+    //call into direct buffer access (when there is no remainder)
+    if (data->readElemsLeft == 0)
+    {
+        int ret = this->acquireReadBuffer(stream, data->readHandle, data->recvBuffs.data(), flags, timeNs, timeoutUs);
+        if (ret < 0) return ret;
+        data->readElemsLeft = size_t(ret);
+    }
+
+    //convert the buffer
+    size_t numSamples = std::min(numElems, data->readElemsLeft);
+    data->convertRecvBuffs(buffs, numSamples);
+    data->readElemsLeft -= numSamples;
+
+    //completed the buffer, release its handle
+    if (data->readElemsLeft == 0)
+    {
+        this->releaseReadBuffer(stream, data->readHandle);
+    }
+
+    //increment pointers for the remainder conversion
+    else
+    {
+        flags |= SOAPY_SDR_MORE_FRAGMENTS;
+        const size_t offsetBytes = data->endpoint->getElemSize()*numSamples;
+        for (size_t i = 0; i < data->recvBuffs.size(); i++)
+        {
+            data->recvBuffs[i] = ((char *)data->recvBuffs[i]) + offsetBytes;
+        }
+    }
+
+    return numSamples;
+}
+
+int SoapyRemoteDevice::writeStream(
+    SoapySDR::Stream *stream,
+    const void * const *buffs,
+    const size_t numElems,
+    int &flags,
+    const long long timeNs,
+    const long timeoutUs)
+{
+    auto data = (ClientStreamData *)stream;
+
+    //acquire from direct buffer access
+    size_t handle = 0;
+    int ret = this->acquireWriteBuffer(stream, handle, data->sendBuffs.data(), timeoutUs);
+    if (ret < 0) return ret;
+
+    //only end burst if the last sample can be released
+    const size_t numSamples = std::min<size_t>(ret, numElems);
+    if (numSamples < numElems) flags &= ~(SOAPY_SDR_END_BURST);
+
+    //convert the samples
+    data->convertSendBuffs(buffs, numSamples);
+
+    //release to direct buffer access
+    this->releaseWriteBuffer(stream, handle, numSamples, flags, timeNs);
+    return numSamples;
+}
+
+int SoapyRemoteDevice::readStreamStatus(
+    SoapySDR::Stream *stream,
+    size_t &chanMask,
+    int &flags,
+    long long &timeNs,
+    const long timeoutUs)
+{
+    auto data = (ClientStreamData *)stream;
+    auto ep = data->endpoint;
+    if (not ep->waitStatus(timeoutUs)) return SOAPY_SDR_TIMEOUT;
+    return ep->readStatus(chanMask, flags, timeNs);
+}
+
+/*******************************************************************
+ * Direct buffer access API
+ ******************************************************************/
+
+size_t SoapyRemoteDevice::getNumDirectAccessBuffers(SoapySDR::Stream *stream)
+{
+    auto data = (ClientStreamData *)stream;
+    return data->endpoint->getNumBuffs();
+}
+
+int SoapyRemoteDevice::getDirectAccessBufferAddrs(SoapySDR::Stream *stream, const size_t handle, void **buffs)
+{
+    auto data = (ClientStreamData *)stream;
+    data->endpoint->getAddrs(handle, buffs);
+    return 0;
+}
+
+int SoapyRemoteDevice::acquireReadBuffer(
+    SoapySDR::Stream *stream,
+    size_t &handle,
+    const void **buffs,
+    int &flags,
+    long long &timeNs,
+    const long timeoutUs)
+{
+    auto data = (ClientStreamData *)stream;
+    auto ep = data->endpoint;
+    if (not ep->waitRecv(timeoutUs)) return SOAPY_SDR_TIMEOUT;
+    return ep->acquireRecv(handle, buffs, flags, timeNs);
+}
+
+void SoapyRemoteDevice::releaseReadBuffer(
+    SoapySDR::Stream *stream,
+    const size_t handle)
+{
+    auto data = (ClientStreamData *)stream;
+    auto ep = data->endpoint;
+    return ep->releaseRecv(handle);
+}
+
+int SoapyRemoteDevice::acquireWriteBuffer(
+    SoapySDR::Stream *stream,
+    size_t &handle,
+    void **buffs,
+    const long timeoutUs)
+{
+    auto data = (ClientStreamData *)stream;
+    auto ep = data->endpoint;
+    if (not ep->waitSend(timeoutUs)) return SOAPY_SDR_TIMEOUT;
+    return ep->acquireSend(handle, buffs);
+}
+
+void SoapyRemoteDevice::releaseWriteBuffer(
+    SoapySDR::Stream *stream,
+    const size_t handle,
+    const size_t numElems,
+    int &flags,
+    const long long timeNs)
+{
+    auto data = (ClientStreamData *)stream;
+    auto ep = data->endpoint;
+    return ep->releaseSend(handle, numElems, flags, timeNs);
+}
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
new file mode 100644
index 0000000..8967291
--- /dev/null
+++ b/common/CMakeLists.txt
@@ -0,0 +1,50 @@
+########################################################################
+# Build the remote support library
+########################################################################
+set(COMMON_SOURCES
+    SoapyURLUtils.cpp
+    SoapyRPCSocket.cpp
+    SoapyRPCPacker.cpp
+    SoapyRPCUnpacker.cpp
+    SoapyStreamEndpoint.cpp
+    SoapyHTTPUtils.cpp
+    SoapySSDPEndpoint.cpp
+)
+
+#configure ssdp defines
+configure_file(
+    ${CMAKE_CURRENT_SOURCE_DIR}/SoapyInfoUtils.in.cpp
+    ${CMAKE_CURRENT_BINARY_DIR}/SoapyInfoUtils.cpp
+ at ONLY)
+list(APPEND COMMON_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/SoapyInfoUtils.cpp)
+
+#check for platform-specific network header
+include(CheckIncludeFiles)
+CHECK_INCLUDE_FILES(winsock2.h HAS_WINSOCK2_H)
+CHECK_INCLUDE_FILES(ws2tcpip.h HAS_WS2TCPIP_H)
+CHECK_INCLUDE_FILES(netdb.h HAS_NETDB_H)
+CHECK_INCLUDE_FILES(unistd.h HAS_UNISTD_H)
+CHECK_INCLUDE_FILES(netinet/in.h HAS_NETINET_IN_H)
+CHECK_INCLUDE_FILES(netinet/tcp.h HAS_NETINET_TCP_H)
+CHECK_INCLUDE_FILES(sys/types.h HAS_SYS_TYPES_H)
+CHECK_INCLUDE_FILES(sys/socket.h HAS_SYS_SOCKET_H)
+CHECK_INCLUDE_FILES(arpa/inet.h HAS_ARPA_INET_H)
+CHECK_INCLUDE_FILES(ifaddrs.h HAS_IFADDRS_H)
+CHECK_INCLUDE_FILES(net/if.h HAS_NET_IF_H)
+
+#network libraries
+if (WIN32)
+    list(APPEND SoapySDR_LIBRARIES ws2_32)
+endif (WIN32)
+
+#create private include header for network compatibility
+include_directories(${CMAKE_CURRENT_BINARY_DIR})
+configure_file(
+    ${CMAKE_CURRENT_SOURCE_DIR}/SoapySocketDefs.in.hpp
+    ${CMAKE_CURRENT_BINARY_DIR}/SoapySocketDefs.hpp)
+
+#build a static library
+include_directories(${SoapySDR_INCLUDE_DIRS})
+add_library(SoapySDRRemoteCommon STATIC ${COMMON_SOURCES})
+target_link_libraries(SoapySDRRemoteCommon ${SoapySDR_LIBRARIES})
+set_property(TARGET SoapySDRRemoteCommon PROPERTY POSITION_INDEPENDENT_CODE TRUE)
diff --git a/common/SoapyHTTPUtils.cpp b/common/SoapyHTTPUtils.cpp
new file mode 100644
index 0000000..388ba88
--- /dev/null
+++ b/common/SoapyHTTPUtils.cpp
@@ -0,0 +1,52 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapyHTTPUtils.hpp"
+#include <cctype>
+
+SoapyHTTPHeader::SoapyHTTPHeader(const std::string &line0)
+{
+    _storage = line0 + "\r\n";
+}
+
+void SoapyHTTPHeader::addField(const std::string &key, const std::string &value)
+{
+    _storage += key + ": " + value + "\r\n";
+}
+
+void SoapyHTTPHeader::finalize(void)
+{
+    _storage += "\r\n";
+}
+
+SoapyHTTPHeader::SoapyHTTPHeader(const void *buff, const size_t length)
+{
+    _storage = std::string((const char *)buff, length);
+}
+
+std::string SoapyHTTPHeader::getLine0(void) const
+{
+    const auto pos = _storage.find("\r\n");
+    if (pos == std::string::npos) return "";
+    return _storage.substr(0, pos);
+}
+
+std::string SoapyHTTPHeader::getField(const std::string &key) const
+{
+    //find the field start
+    const std::string fieldStart("\r\n"+key+":");
+    auto pos = _storage.find(fieldStart);
+    if (pos == std::string::npos) return "";
+
+    //offset from field start
+    pos += fieldStart.length();
+
+    //offset from whitespace
+    while (std::isspace(_storage.at(pos))) pos++;
+
+    //find the field end
+    const auto end = _storage.find("\r\n", pos);
+    if (end == std::string::npos) return "";
+
+    return _storage.substr(pos, end-pos);
+}
diff --git a/common/SoapyHTTPUtils.hpp b/common/SoapyHTTPUtils.hpp
new file mode 100644
index 0000000..4cb4e7a
--- /dev/null
+++ b/common/SoapyHTTPUtils.hpp
@@ -0,0 +1,42 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+#include <string>
+
+class SOAPY_REMOTE_API SoapyHTTPHeader
+{
+public:
+
+    //! Create an HTTP header given request/response line
+    SoapyHTTPHeader(const std::string &line0);
+
+    //! Add a key/value field to the header
+    void addField(const std::string &key, const std::string &value);
+
+    //! Done adding fields to the header
+    void finalize(void);
+
+    //! Create an HTTP from a received datagram
+    SoapyHTTPHeader(const void *buff, const size_t length);
+
+    //! Get the request/response line
+    std::string getLine0(void) const;
+
+    //! Read a field from the HTTP header (empty when missing)
+    std::string getField(const std::string &key) const;
+
+    const void *data(void) const
+    {
+        return _storage.data();
+    }
+
+    size_t size(void) const
+    {
+        return _storage.size();
+    }
+
+private:
+    std::string _storage;
+};
diff --git a/common/SoapyInfoUtils.hpp b/common/SoapyInfoUtils.hpp
new file mode 100644
index 0000000..4c3428b
--- /dev/null
+++ b/common/SoapyInfoUtils.hpp
@@ -0,0 +1,22 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+#include <string>
+
+namespace SoapyInfo
+{
+    //! Get the hostname
+    SOAPY_REMOTE_API std::string getHostName(void);
+
+    /*!
+     * Generate a type1 UUID based on the current time and host ID.
+     */
+    SOAPY_REMOTE_API std::string generateUUID1(void);
+
+    /*!
+     * Get the user agent string for this build.
+     */
+    SOAPY_REMOTE_API std::string getUserAgent(void);
+};
diff --git a/common/SoapyInfoUtils.in.cpp b/common/SoapyInfoUtils.in.cpp
new file mode 100644
index 0000000..a29e684
--- /dev/null
+++ b/common/SoapyInfoUtils.in.cpp
@@ -0,0 +1,87 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapySocketDefs.hpp"
+#include "SoapyInfoUtils.hpp"
+#include <cstdlib> //rand
+#include <chrono>
+
+#ifdef _MSC_VER
+
+#define getpid() GetCurrentProcessId()
+
+static DWORD gethostid(void)
+{
+    char szVolName[MAX_PATH];
+    char szFileSysName[80];
+    DWORD dwSerialNumber;
+    DWORD dwMaxComponentLen;
+    DWORD dwFileSysFlags;
+    GetVolumeInformation(
+        "C:\\", szVolName, MAX_PATH,
+        &dwSerialNumber, &dwMaxComponentLen,
+        &dwFileSysFlags, szFileSysName, sizeof(szFileSysName));
+    return dwSerialNumber;
+}
+
+#endif //_MSC_VER
+
+std::string SoapyInfo::getHostName(void)
+{
+    std::string hostname;
+    char hostnameBuff[128];
+    int ret = gethostname(hostnameBuff, sizeof(hostnameBuff));
+    if (ret == 0) hostname = std::string(hostnameBuff);
+    else hostname = "unknown";
+    return hostname;
+}
+
+std::string SoapyInfo::generateUUID1(void)
+{
+    //64-bit timestamp in nanoseconds
+    const auto timeSinceEpoch = std::chrono::high_resolution_clock::now().time_since_epoch();
+    const auto timeNanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(timeSinceEpoch);
+    const unsigned long long timeNs64 = timeNanoseconds.count();
+
+    //clock sequence (random)
+    const unsigned short clockSeq16 = short(std::rand());
+
+    //rather than node, use the host id and pid
+    const unsigned short pid16 = short(getpid());
+    const unsigned int hid32 = int(gethostid());
+
+    //load up the UUID bytes
+    unsigned char bytes[16];
+    bytes[0] = (unsigned char)(timeNs64 >> 24);
+    bytes[1] = (unsigned char)(timeNs64 >> 16);
+    bytes[2] = (unsigned char)(timeNs64 >> 8);
+    bytes[3] = (unsigned char)(timeNs64 >> 0);
+    bytes[4] = (unsigned char)(timeNs64 >> 40);
+    bytes[5] = (unsigned char)(timeNs64 >> 32);
+    bytes[6] = (unsigned char)(((timeNs64 >> 56) & 0x0F) | 0x10); //variant
+    bytes[7] = (unsigned char)(timeNs64 >> 48);
+    bytes[8] = (unsigned char)(((clockSeq16 >> 8) & 0x3F) | 0x80); //reserved
+    bytes[9] = (unsigned char)(clockSeq16 >> 0);
+    bytes[10] = (unsigned char)(pid16 >> 8);
+    bytes[11] = (unsigned char)(pid16 >> 0);
+    bytes[12] = (unsigned char)(hid32 >> 24);
+    bytes[13] = (unsigned char)(hid32 >> 16);
+    bytes[14] = (unsigned char)(hid32 >> 8);
+    bytes[15] = (unsigned char)(hid32 >> 0);
+
+    //load fields into the buffer
+    char buff[37];
+    const int ret = sprintf(buff,
+        "%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx-"
+        "%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx",
+        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
+        bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]);
+
+    if (ret > 0) return std::string(buff, size_t(ret));
+    return ""; //failed
+}
+
+SOAPY_REMOTE_API std::string SoapyInfo::getUserAgent(void)
+{
+    return "@CMAKE_SYSTEM_NAME@ UPnP/1.1 SoapyRemote/@SoapySDR_VERSION@";
+}
diff --git a/common/SoapyRPCPacker.cpp b/common/SoapyRPCPacker.cpp
new file mode 100644
index 0000000..e768aff
--- /dev/null
+++ b/common/SoapyRPCPacker.cpp
@@ -0,0 +1,206 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapySocketDefs.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyRPCSocket.hpp"
+#include "SoapyRPCPacker.hpp"
+#include <cfloat> //DBL_MANT_DIG
+#include <cmath> //frexp
+#include <cstring> //memcpy
+#include <cstdlib> //malloc
+#include <algorithm> //min, max
+#include <stdexcept>
+
+SoapyRPCPacker::SoapyRPCPacker(SoapyRPCSocket &sock):
+    _sock(sock),
+    _message(NULL),
+    _size(0),
+    _capacity(0)
+{
+    //default allocation
+    this->ensureSpace(512);
+
+    //allot space for the header (filled in by send)
+    SoapyRPCHeader header;
+    this->pack(&header, sizeof(header));
+}
+
+SoapyRPCPacker::~SoapyRPCPacker(void)
+{
+    free(_message);
+    _message = NULL;
+}
+
+void SoapyRPCPacker::send(void)
+{
+    //load the trailer
+    SoapyRPCTrailer trailer;
+    trailer.trailerWord = htonl(SoapyRPCTrailerWord);
+    this->pack(&trailer, sizeof(trailer));
+
+    //load the header
+    SoapyRPCHeader *header = (SoapyRPCHeader *)_message;
+    header->headerWord = htonl(SoapyRPCHeaderWord);
+    header->version = htonl(SoapyRPCVersion);
+    header->length = htonl(_size);
+
+    //send the entire message
+    size_t bytesSent = 0;
+    while (bytesSent != _size)
+    {
+        const size_t toSend = std::min<size_t>(SOAPY_REMOTE_SOCKET_BUFFMAX, _size-bytesSent);
+        int ret = _sock.send(_message+bytesSent, toSend);
+        if (ret < 0)
+        {
+            throw std::runtime_error("SoapyRPCPacker::send() FAIL: "+std::string(_sock.lastErrorMsg()));
+        }
+        bytesSent += ret;
+    }
+}
+
+void SoapyRPCPacker::ensureSpace(const size_t length)
+{
+    if (_size+length <= _capacity) return;
+    const size_t newSize = std::max(_capacity*2, _size+length);
+    _message = (char *)realloc(_message, newSize);
+}
+
+void SoapyRPCPacker::pack(const void *buff, const size_t length)
+{
+    this->ensureSpace(length);
+    std::memcpy(_message+_size, buff, length);
+    _size += length;
+}
+
+void SoapyRPCPacker::operator&(const char value)
+{
+    *this & SOAPY_REMOTE_CHAR;
+    this->pack(value);
+}
+
+void SoapyRPCPacker::operator&(const bool value)
+{
+    *this & SOAPY_REMOTE_BOOL;
+    char out = value?1:0;
+    this->pack(out);
+}
+
+void SoapyRPCPacker::operator&(const int value)
+{
+    *this & SOAPY_REMOTE_INT32;
+    int out = htonl(value);
+    this->pack(&out, sizeof(out));
+}
+
+void SoapyRPCPacker::operator&(const long long value)
+{
+    *this & SOAPY_REMOTE_INT64;
+    long long out = htonll(value);
+    this->pack(&out, sizeof(out));
+}
+
+void SoapyRPCPacker::operator&(const double value)
+{
+    *this & SOAPY_REMOTE_FLOAT64;
+    int exp = 0;
+    const double x = std::frexp(value, &exp);
+    const long long man = (long long)std::ldexp(x, DBL_MANT_DIG);
+    *this & exp;
+    *this & man;
+}
+
+void SoapyRPCPacker::operator&(const std::complex<double> &value)
+{
+    *this & SOAPY_REMOTE_COMPLEX128;
+    *this & value.real();
+    *this & value.imag();
+}
+
+void SoapyRPCPacker::operator&(const std::string &value)
+{
+    *this & SOAPY_REMOTE_STRING;
+    *this & int(value.size());
+    this->pack(value.c_str(), value.size());
+}
+
+void SoapyRPCPacker::operator&(const SoapySDR::Range &value)
+{
+    *this & SOAPY_REMOTE_RANGE;
+    *this & value.minimum();
+    *this & value.maximum();
+}
+
+void SoapyRPCPacker::operator&(const SoapySDR::RangeList &value)
+{
+    *this & SOAPY_REMOTE_RANGE_LIST;
+    *this & int(value.size());
+    for (size_t i = 0; i < value.size(); i++) *this & value[i];
+}
+
+void SoapyRPCPacker::operator&(const std::vector<std::string> &value)
+{
+    *this & SOAPY_REMOTE_STRING_LIST;
+    *this & int(value.size());
+    for (size_t i = 0; i < value.size(); i++) *this & value[i];
+}
+
+void SoapyRPCPacker::operator&(const std::vector<double> &value)
+{
+    *this & SOAPY_REMOTE_FLOAT64_LIST;
+    *this & int(value.size());
+    for (size_t i = 0; i < value.size(); i++) *this & value[i];
+}
+
+void SoapyRPCPacker::operator&(const SoapySDR::Kwargs &value)
+{
+    *this & SOAPY_REMOTE_KWARGS;
+    *this & int(value.size());
+    for (auto it = value.begin(); it != value.end(); ++it)
+    {
+        *this & it->first;
+        *this & it->second;
+    }
+}
+
+void SoapyRPCPacker::operator&(const SoapySDR::KwargsList &value)
+{
+    *this & SOAPY_REMOTE_KWARGS_LIST;
+    *this & int(value.size());
+    for (size_t i = 0; i < value.size(); i++) *this & value[i];
+}
+
+void SoapyRPCPacker::operator&(const std::vector<size_t> &value)
+{
+    *this & SOAPY_REMOTE_SIZE_LIST;
+    *this & int(value.size());
+    for (size_t i = 0; i < value.size(); i++) *this & int(value[i]);
+}
+
+void SoapyRPCPacker::operator&(const SoapySDR::ArgInfo &value)
+{
+    *this & SOAPY_REMOTE_ARG_INFO;
+    *this & value.key;
+    *this & value.value;
+    *this & value.name;
+    *this & value.description;
+    *this & value.units;
+    *this & int(value.type);
+    *this & value.range;
+    *this & value.options;
+    *this & value.optionNames;
+}
+
+void SoapyRPCPacker::operator&(const SoapySDR::ArgInfoList &value)
+{
+    *this & SOAPY_REMOTE_ARG_INFO_LIST;
+    *this & int(value.size());
+    for (size_t i = 0; i < value.size(); i++) *this & value[i];
+}
+
+void SoapyRPCPacker::operator&(const std::exception &value)
+{
+    *this & SOAPY_REMOTE_EXCEPTION;
+    std::string msg(value.what());
+    *this & msg;
+}
diff --git a/common/SoapyRPCPacker.hpp b/common/SoapyRPCPacker.hpp
new file mode 100644
index 0000000..d6b0734
--- /dev/null
+++ b/common/SoapyRPCPacker.hpp
@@ -0,0 +1,117 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+#include <SoapySDR/Types.hpp>
+#include <vector>
+#include <complex>
+#include <string>
+#include <stdexcept>
+
+class SoapyRPCSocket;
+
+/*!
+ * The packer object accepts primitive Soapy SDR types
+ * and encodes them into a portable network RPC format.
+ */
+class SOAPY_REMOTE_API SoapyRPCPacker
+{
+public:
+    SoapyRPCPacker(SoapyRPCSocket &sock);
+
+    ~SoapyRPCPacker(void);
+
+    //! Shortcut operator for send
+    void operator()(void)
+    {
+        this->send();
+    }
+
+    //! Send the message when packing is complete
+    void send(void);
+
+    //! Pack a binary blob
+    void pack(const void *buff, const size_t length);
+
+    //! Pack a single byte
+    void pack(const char byte)
+    {
+        this->ensureSpace(1);
+        _message[_size] = byte;
+        _size++;
+    }
+
+    //! Pack the call
+    void operator&(const SoapyRemoteCalls value)
+    {
+        *this & SOAPY_REMOTE_CALL;
+        *this & int(value);
+    }
+
+    //! Pack the type
+    void operator&(const SoapyRemoteTypes value)
+    {
+        this->pack(char(value));
+    }
+
+    //! Pack a character
+    void operator&(const char value);
+
+    //! Pack a boolean
+    void operator&(const bool value);
+
+    //! Pack a 32-bit integer
+    void operator&(const int value);
+
+    //! Pack a 64-bit integer
+    void operator&(const long long value);
+
+    //! Pack a double float
+    void operator&(const double value);
+
+    //! Pack a complex double float
+    void operator&(const std::complex<double> &value);
+
+    //! Pack a string
+    void operator&(const std::string &value);
+
+    //! Pack a range
+    void operator&(const SoapySDR::Range &value);
+
+    //! Pack a list of ranges
+    void operator&(const SoapySDR::RangeList &value);
+
+    //! Pack a list of strings
+    void operator&(const std::vector<std::string> &value);
+
+    //! Pack a list of double floats
+    void operator&(const std::vector<double> &value);
+
+    //! Pack a kwargs dictionary
+    void operator&(const SoapySDR::Kwargs &value);
+
+    //! Pack a list of kwargs
+    void operator&(const SoapySDR::KwargsList &value);
+
+    //! Pack a list of sizes
+    void operator&(const std::vector<size_t> &value);
+
+    //! Pack an arg info structure
+    void operator&(const SoapySDR::ArgInfo &value);
+
+    //! Pack a list of arg infos
+    void operator&(const SoapySDR::ArgInfoList &value);
+
+    //! Pack an exception
+    void operator&(const std::exception &value);
+
+private:
+
+    void ensureSpace(const size_t length);
+
+    SoapyRPCSocket &_sock;
+    char *_message;
+    size_t _size;
+    size_t _capacity;
+};
diff --git a/common/SoapyRPCSocket.cpp b/common/SoapyRPCSocket.cpp
new file mode 100644
index 0000000..7bdbad8
--- /dev/null
+++ b/common/SoapyRPCSocket.cpp
@@ -0,0 +1,448 @@
+// Copyright (c) 2015-2016 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapySocketDefs.hpp"
+#include "SoapyRPCSocket.hpp"
+#include "SoapyURLUtils.hpp"
+#include <SoapySDR/Logger.hpp>
+#include <cstring> //strerror
+#include <cerrno> //errno
+#include <mutex>
+
+static std::mutex sessionMutex;
+static size_t sessionCount = 0;
+
+SoapySocketSession::SoapySocketSession(void)
+{
+    std::lock_guard<std::mutex> lock(sessionMutex);
+    sessionCount++;
+    if (sessionCount > 1) return;
+
+    #ifdef _MSC_VER
+    WORD wVersionRequested;
+    WSADATA wsaData;
+    wVersionRequested = MAKEWORD(2, 2);
+    int ret = WSAStartup(wVersionRequested, &wsaData);
+    if (ret != 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapySocketSession::WSAStartup: %d", ret);
+    }
+    #endif
+}
+
+SoapySocketSession::~SoapySocketSession(void)
+{
+    std::lock_guard<std::mutex> lock(sessionMutex);
+    sessionCount--;
+    if (sessionCount > 0) return;
+
+    #ifdef _MSC_VER
+    WSACleanup();
+    #endif
+}
+
+void SoapyRPCSocket::setDefaultTcpSockOpts(void)
+{
+    if (this->null()) return;
+
+    int one = 1;
+    int ret = ::setsockopt(_sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&one, sizeof(one));
+    if (ret != 0)
+    {
+        this->reportError("setsockopt(TCP_NODELAY)");
+    }
+
+    #ifdef TCP_QUICKACK
+    ret = ::setsockopt(_sock, IPPROTO_TCP, TCP_QUICKACK, (const char *)&one, sizeof(one));
+    if (ret != 0)
+    {
+        this->reportError("setsockopt(TCP_QUICKACK)");
+    }
+    #endif //TCP_QUICKACK
+}
+
+SoapyRPCSocket::SoapyRPCSocket(void):
+    _sock(INVALID_SOCKET)
+{
+    return;
+}
+
+SoapyRPCSocket::SoapyRPCSocket(const std::string &url):
+    _sock(INVALID_SOCKET)
+{
+    SoapyURL urlObj(url);
+    SockAddrData addr;
+    const auto errorMsg = urlObj.toSockAddr(addr);
+
+    if (not errorMsg.empty())
+    {
+        this->reportError("getaddrinfo("+url+")", errorMsg);
+    }
+    else
+    {
+        _sock = ::socket(addr.addr()->sa_family, urlObj.getType(), 0);
+    }
+}
+
+SoapyRPCSocket::~SoapyRPCSocket(void)
+{
+    if (this->close() != 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyRPCSocket::~SoapyRPCSocket: %s", this->lastErrorMsg());
+    }
+}
+
+bool SoapyRPCSocket::null(void)
+{
+    return _sock == INVALID_SOCKET;
+}
+
+int SoapyRPCSocket::close(void)
+{
+    if (this->null()) return 0;
+    int ret = ::closesocket(_sock);
+    _sock = INVALID_SOCKET;
+    return ret;
+}
+
+int SoapyRPCSocket::bind(const std::string &url)
+{
+    SoapyURL urlObj(url);
+    SockAddrData addr;
+    const auto errorMsg = urlObj.toSockAddr(addr);
+    if (not errorMsg.empty())
+    {
+        this->reportError("getaddrinfo("+url+")", errorMsg);
+        return -1;
+    }
+
+    if (this->null()) _sock = ::socket(addr.addr()->sa_family, urlObj.getType(), 0);
+    if (this->null()) return -1;
+
+    //setup reuse address
+    int one = 1;
+    int ret = ::setsockopt(_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&one, sizeof(one));
+    if (ret != 0)
+    {
+        this->reportError("setsockopt(SO_REUSEADDR)");
+    }
+
+    #ifdef __APPLE__
+    ret = ::setsockopt(_sock, SOL_SOCKET, SO_REUSEPORT, (const char *)&one, sizeof(one));
+    if (ret != 0)
+    {
+        this->reportError("setsockopt(SO_REUSEPORT)");
+    }
+    #endif //__APPLE__
+
+    if (urlObj.getType() == SOCK_STREAM) this->setDefaultTcpSockOpts();
+
+    ret = ::bind(_sock, addr.addr(), addr.addrlen());
+    if (ret == -1) this->reportError("bind("+url+")");
+    return ret;
+}
+
+int SoapyRPCSocket::listen(int backlog)
+{
+    int ret = ::listen(_sock, backlog);
+    if (ret == -1) this->reportError("listen()");
+    return ret;
+}
+
+SoapyRPCSocket *SoapyRPCSocket::accept(void)
+{
+    struct sockaddr_storage addr;
+    socklen_t addrlen = sizeof(addr);
+    int client = ::accept(_sock, (struct sockaddr*)&addr, &addrlen);
+    if (client == INVALID_SOCKET) return NULL;
+    SoapyRPCSocket *clientSock = new SoapyRPCSocket();
+    clientSock->_sock = client;
+    clientSock->setDefaultTcpSockOpts();
+    return clientSock;
+}
+
+int SoapyRPCSocket::connect(const std::string &url)
+{
+    SoapyURL urlObj(url);
+    SockAddrData addr;
+    const auto errorMsg = urlObj.toSockAddr(addr);
+    if (not errorMsg.empty())
+    {
+        this->reportError("getaddrinfo("+url+")", errorMsg);
+        return -1;
+    }
+
+    if (this->null()) _sock = ::socket(addr.addr()->sa_family, urlObj.getType(), 0);
+    if (this->null()) return -1;
+    if (urlObj.getType() == SOCK_STREAM) this->setDefaultTcpSockOpts();
+
+    int ret = ::connect(_sock, addr.addr(), addr.addrlen());
+    if (ret == -1) this->reportError("connect("+url+")");
+    return ret;
+}
+
+/*!
+ * OSX doesn't support automatic ipv6mr_interface = 0.
+ * The following code attempts to work around this issue
+ * by manually selecting a multicast capable interface.
+ */
+static int getDefaultIfaceIndex(void)
+{
+    #ifdef __APPLE__
+
+    //find the first available multicast interfaces
+    int loIface = 0, enIface = 0;
+    struct ifaddrs *ifa = nullptr;
+    getifaddrs(&ifa);
+    while (ifa != nullptr)
+    {
+        const bool isIPv6 = ifa->ifa_addr->sa_family == AF_INET6;
+        const bool isUp = ((ifa->ifa_flags & IFF_UP) != 0);
+        const bool isLoopback = ((ifa->ifa_flags & IFF_LOOPBACK) != 0);
+        const bool isMulticast = ((ifa->ifa_flags & IFF_MULTICAST) != 0);
+        const int ifaceIndex = if_nametoindex(ifa->ifa_name);
+        SoapySDR::logf(SOAPY_SDR_DEBUG, "Interface: #%d(%s) ipv6=%d, up=%d, lb=%d, mcast=%d",
+            ifaceIndex, ifa->ifa_name, isIPv6, isUp, isLoopback, isMulticast);
+        if (isIPv6 and isUp and isLoopback and isMulticast and loIface == 0) loIface = ifaceIndex;
+        if (isIPv6 and isUp and not isLoopback and isMulticast and enIface == 0) enIface = ifaceIndex;
+        ifa = ifa->ifa_next;
+    }
+    freeifaddrs(ifa);
+    SoapySDR::logf(SOAPY_SDR_DEBUG, "Default loopback: #%d, default ethernet #%d", loIface, enIface);
+
+    //prefer discovered regular interface over loopback
+    if (enIface != 0) return enIface;
+    if (loIface != 0) return loIface;
+    #endif //__APPLE__
+
+    return 0;
+}
+
+int SoapyRPCSocket::multicastJoin(const std::string &group, const bool loop, const int ttl, int iface)
+{
+    /*
+     * Multicast join docs:
+     * http://www.tldp.org/HOWTO/Multicast-HOWTO-6.html
+     * http://www.tenouk.com/Module41c.html
+     */
+
+    //lookup group url
+    SoapyURL urlObj(group);
+    SockAddrData addr;
+    const auto errorMsg = urlObj.toSockAddr(addr);
+    if (not errorMsg.empty())
+    {
+        this->reportError("getaddrinfo("+group+")", errorMsg);
+        return -1;
+    }
+
+    //create socket if null
+    if (this->null()) _sock = ::socket(addr.addr()->sa_family, SOCK_DGRAM, 0);
+    if (this->null()) return -1;
+    int ret = 0;
+
+    int loopInt = loop?1:0;
+
+    switch(addr.addr()->sa_family)
+    {
+    case AF_INET: {
+
+        //setup IP_MULTICAST_LOOP
+        ret = ::setsockopt(_sock, IPPROTO_IP, IP_MULTICAST_LOOP, (const char *)&loopInt, sizeof(loopInt));
+        if (ret != 0)
+        {
+            this->reportError("setsockopt(IP_MULTICAST_LOOP)");
+            return -1;
+        }
+
+        //setup IP_MULTICAST_TTL
+        ret = ::setsockopt(_sock, IPPROTO_IP, IP_MULTICAST_TTL, (const char *)&ttl, sizeof(ttl));
+        if (ret != 0)
+        {
+            this->reportError("setsockopt(IP_MULTICAST_TTL)");
+            return -1;
+        }
+
+        //setup IP_ADD_MEMBERSHIP
+        auto *addr_in = (const struct sockaddr_in *)addr.addr();
+        struct ip_mreq mreq;
+        mreq.imr_multiaddr = addr_in->sin_addr;
+        mreq.imr_interface.s_addr = INADDR_ANY;
+        ret = ::setsockopt(_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&mreq, sizeof(mreq));
+        if (ret != 0)
+        {
+            this->reportError("setsockopt(IP_ADD_MEMBERSHIP)");
+            return -1;
+        }
+        break;
+    }
+    case AF_INET6: {
+
+        //setup IPV6_MULTICAST_LOOP
+        ret = ::setsockopt(_sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (const char *)&loopInt, sizeof(loopInt));
+        if (ret != 0)
+        {
+            this->reportError("setsockopt(IPV6_MULTICAST_LOOP)");
+            return -1;
+        }
+
+        //setup IPV6_MULTICAST_HOPS
+        ret = ::setsockopt(_sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (const char *)&ttl, sizeof(ttl));
+        if (ret != 0)
+        {
+            this->reportError("setsockopt(IPV6_MULTICAST_HOPS)");
+            return -1;
+        }
+
+        //setup IPV6_MULTICAST_IF
+        if (iface == 0) iface = getDefaultIfaceIndex();
+        if (iface != 0)
+        {
+            ret = ::setsockopt(_sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, (const char *)&iface, sizeof(iface));
+            if (ret != 0)
+            {
+                this->reportError("setsockopt(IPV6_MULTICAST_IF)");
+                return -1;
+            }
+        }
+
+        //setup IPV6_ADD_MEMBERSHIP
+        auto *addr_in6 = (const struct sockaddr_in6 *)addr.addr();
+        struct ipv6_mreq mreq6;
+        mreq6.ipv6mr_multiaddr = addr_in6->sin6_addr;
+        mreq6.ipv6mr_interface = iface;
+        ret = ::setsockopt(_sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (const char *)&mreq6, sizeof(mreq6));
+        if (ret != 0)
+        {
+            this->reportError("setsockopt(IPV6_ADD_MEMBERSHIP)");
+            return -1;
+        }
+        break;
+    }
+    default:
+        break;
+    }
+
+    return 0;
+}
+
+int SoapyRPCSocket::send(const void *buf, size_t len, int flags)
+{
+    int ret = ::send(_sock, (const char *)buf, int(len), flags);
+    if (ret == -1) this->reportError("send()");
+    return ret;
+}
+
+int SoapyRPCSocket::recv(void *buf, size_t len, int flags)
+{
+    int ret = ::recv(_sock, (char *)buf, int(len), flags);
+    if (ret == -1) this->reportError("recv()");
+    return ret;
+}
+
+int SoapyRPCSocket::sendto(const void *buf, size_t len, const std::string &url, int flags)
+{
+    SockAddrData addr; SoapyURL(url).toSockAddr(addr);
+    int ret = ::sendto(_sock, (char *)buf, int(len), flags, addr.addr(), addr.addrlen());
+    if (ret == -1) this->reportError("sendto("+url+")");
+    return ret;
+}
+
+int SoapyRPCSocket::recvfrom(void *buf, size_t len, std::string &url, int flags)
+{
+    struct sockaddr_storage addr;
+    socklen_t addrlen = sizeof(addr);
+    int ret = ::recvfrom(_sock, (char *)buf, int(len), flags, (struct sockaddr*)&addr, &addrlen);
+    if (ret == -1) this->reportError("recvfrom()");
+    else url = SoapyURL(SockAddrData((struct sockaddr *)&addr, addrlen)).toString();
+    return ret;
+}
+
+bool SoapyRPCSocket::selectRecv(const long timeoutUs)
+{
+    struct timeval tv;
+    tv.tv_sec = timeoutUs / 1000000;
+    tv.tv_usec = timeoutUs % 1000000;
+
+    fd_set readfds;
+    FD_ZERO(&readfds);
+    FD_SET(_sock, &readfds);
+
+    int ret = ::select(_sock+1, &readfds, NULL, NULL, &tv);
+    if (ret == -1) this->reportError("select()");
+    return ret == 1;
+}
+
+static std::string errToString(const int err)
+{
+    char buff[1024];
+    #ifdef _MSC_VER
+    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&buff, sizeof(buff), NULL);
+    return buff;
+    #else
+    //http://linux.die.net/man/3/strerror_r
+    #if ((_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600) && ! _GNU_SOURCE) || __APPLE__
+    strerror_r(err, buff, sizeof(buff));
+    #else
+    //this version may decide to use its own internal string
+    return strerror_r(err, buff, sizeof(buff));
+    #endif
+    return buff;
+    #endif
+}
+
+void SoapyRPCSocket::reportError(const std::string &what)
+{
+    const int err = SOCKET_ERRNO;
+    if (err == 0) _lastErrorMsg = what;
+    else this->reportError(what, std::to_string(err) + ": " + errToString(err));
+}
+
+void SoapyRPCSocket::reportError(const std::string &what, const std::string &errorMsg)
+{
+    _lastErrorMsg = what + " [" + errorMsg + "]";
+}
+
+std::string SoapyRPCSocket::getsockname(void)
+{
+    struct sockaddr_storage addr;
+    socklen_t addrlen = sizeof(addr);
+    int ret = ::getsockname(_sock, (struct sockaddr *)&addr, &addrlen);
+    if (ret == -1) this->reportError("getsockname()");
+    if (ret != 0) return "";
+    return SoapyURL(SockAddrData((struct sockaddr *)&addr, addrlen)).toString();
+}
+
+std::string SoapyRPCSocket::getpeername(void)
+{
+    struct sockaddr_storage addr;
+    socklen_t addrlen = sizeof(addr);
+    int ret = ::getpeername(_sock, (struct sockaddr *)&addr, &addrlen);
+    if (ret == -1) this->reportError("getpeername()");
+    if (ret != 0) return "";
+    return SoapyURL(SockAddrData((struct sockaddr *)&addr, addrlen)).toString();
+}
+
+int SoapyRPCSocket::setBuffSize(const bool isRecv, const size_t numBytes)
+{
+    int opt = int(numBytes);
+    int ret = ::setsockopt(_sock, SOL_SOCKET, isRecv?SO_RCVBUF:SO_SNDBUF, (const char *)&opt, sizeof(opt));
+    if (ret == -1) this->reportError("setsockopt("+std::string(isRecv?"SO_RCVBUF":"SO_SNDBUF")+")");
+    return ret;
+}
+
+int SoapyRPCSocket::getBuffSize(const bool isRecv)
+{
+    int opt = 0;
+    socklen_t optlen = sizeof(opt);
+    int ret = ::getsockopt(_sock, SOL_SOCKET, isRecv?SO_RCVBUF:SO_SNDBUF, (char *)&opt, &optlen);
+    if (ret == -1) this->reportError("getsockopt("+std::string(isRecv?"SO_RCVBUF":"SO_SNDBUF")+")");
+    if (ret != 0) return ret;
+
+    //adjustment for linux kernel socket buffer doubling for bookkeeping
+    #ifdef __linux
+    opt = opt/2;
+    #endif
+
+    return opt;
+}
diff --git a/common/SoapyRPCSocket.hpp b/common/SoapyRPCSocket.hpp
new file mode 100644
index 0000000..48d4361
--- /dev/null
+++ b/common/SoapyRPCSocket.hpp
@@ -0,0 +1,157 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+#include <cstddef>
+#include <string>
+
+class SockAddrData;
+
+/*!
+ * Create one instance of the session per process to use sockets.
+ */
+class SOAPY_REMOTE_API SoapySocketSession
+{
+public:
+    SoapySocketSession(void);
+    ~SoapySocketSession(void);
+};
+
+/*!
+ * A simple socket wrapper with a TCP-like socket API.
+ * The implementation may be swapped out in the future.
+ */
+class SOAPY_REMOTE_API SoapyRPCSocket
+{
+public:
+    SoapyRPCSocket(void);
+
+    /*!
+     * Make the underlying socket (but does not bind or connect).
+     * This function is called automatically by bind and connect,
+     * however it can be used to test if a protocol is possible.
+     */
+    SoapyRPCSocket(const std::string &url);
+
+    ~SoapyRPCSocket(void);
+
+    /*!
+     * Is the socket null?
+     * The default constructor makes a null socket.
+     * The socket is non null after bind or connect,
+     * and after accept returns a successful socket.
+     */
+    bool null(void);
+
+    /*!
+     * Explicit close the socket, also done by destructor.
+     */
+    int close(void);
+
+    /*!
+     * Server bind.
+     * URL examples:
+     * 0.0.0.0:1234
+     * [::]:1234
+     */
+    int bind(const std::string &url);
+
+    /*!
+     * Server listen.
+     */
+    int listen(int backlog);
+
+    /*!
+     * Server accept connection.
+     * Socket will be null on failure.
+     * Caller owns the client socket.
+     */
+    SoapyRPCSocket *accept(void);
+
+    /*!
+     * Client connect.
+     * URL examples:
+     * 10.10.1.123:1234
+     * [2001:db8:0:1]:1234
+     * hostname:1234
+     */
+    int connect(const std::string &url);
+
+    /*!
+     * Join a multi-cast group.
+     * \param group the url for the multicast group and port number
+     * \param loop specify to receive local loopback
+     * \param ttl specify time to live for send packets
+     * \param iface the IPv6 interface index or 0 for automatic
+     */
+    int multicastJoin(const std::string &group, const bool loop = true, const int ttl = 1, const int iface = 0);
+
+    /*!
+     * Send the buffer and return bytes sent or error.
+     */
+    int send(const void *buf, size_t len, int flags = 0);
+
+    /*!
+     * Receive into buffer and return bytes received or error.
+     */
+    int recv(void *buf, size_t len, int flags = 0);
+
+    /*!
+     * Send to a specific destination.
+     */
+    int sendto(const void *buf, size_t len, const std::string &url, int flags = 0);
+
+    /*!
+     * Receive from an unconnected socket.
+     */
+    int recvfrom(void *buf, size_t len, std::string &url, int flags = 0);
+
+    /*!
+     * Wait for recv to become ready with timeout.
+     * Return true for ready, false for timeout.
+     */
+    bool selectRecv(const long timeoutUs);
+
+    /*!
+     * Query the last error message as a string.
+     */
+    const char *lastErrorMsg(void) const
+    {
+        return _lastErrorMsg.c_str();
+    }
+
+    /*!
+     * Get the URL of the local socket.
+     * Return an empty string on error.
+     */
+    std::string getsockname(void);
+
+    /*!
+     * Get the URL of the remote socket.
+     * Return an empty string on error.
+     */
+    std::string getpeername(void);
+
+    /*!
+     * Set the socket buffer size in bytes.
+     * \param isRecv true for RCVBUF, false for SNDBUF
+     * \return 0 for success or negative error code.
+     */
+    int setBuffSize(const bool isRecv, const size_t numBytes);
+
+    /*!
+     * Get the socket buffer size in bytes.
+     * \param isRecv true for RCVBUF, false for SNDBUF
+     * \return the actual size set or negative error code.
+     */
+    int getBuffSize(const bool isRecv);
+
+private:
+    int _sock;
+    std::string _lastErrorMsg;
+
+    void reportError(const std::string &what, const std::string &errorMsg);
+    void reportError(const std::string &what);
+    void setDefaultTcpSockOpts(void);
+};
diff --git a/common/SoapyRPCUnpacker.cpp b/common/SoapyRPCUnpacker.cpp
new file mode 100644
index 0000000..7f7c2e1
--- /dev/null
+++ b/common/SoapyRPCUnpacker.cpp
@@ -0,0 +1,282 @@
+// Copyright (c) 2015-2016 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapySocketDefs.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyRPCSocket.hpp"
+#include "SoapyRPCUnpacker.hpp"
+#include <SoapySDR/Logger.hpp>
+#include <cfloat> //DBL_MANT_DIG
+#include <cmath> //ldexp
+#include <cstring> //memcpy
+#include <cstdlib> //malloc
+#include <algorithm> //min, max
+#include <stdexcept>
+
+SoapyRPCUnpacker::SoapyRPCUnpacker(SoapyRPCSocket &sock, const bool autoRecv):
+    _sock(sock),
+    _message(NULL),
+    _offset(0),
+    _capacity(0)
+{
+    if (autoRecv) this->recv();
+}
+
+SoapyRPCUnpacker::~SoapyRPCUnpacker(void)
+{
+    free(_message);
+    _message = NULL;
+    _offset += sizeof(SoapyRPCTrailer); //consume trailer
+    if (_offset != _capacity)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "~SoapyRPCUnpacker: Unconsumed payload bytes %d", int(_capacity-_offset));
+    }
+}
+
+void SoapyRPCUnpacker::recv(void)
+{
+    //receive the header
+    SoapyRPCHeader header;
+    int ret = _sock.recv(&header, sizeof(header), MSG_WAITALL);
+    if (ret != sizeof(header))
+    {
+        throw std::runtime_error("SoapyRPCUnpacker::recv(header) FAIL: "+std::string(_sock.lastErrorMsg()));
+    }
+
+    //inspect and parse the header
+    if (ntohl(header.headerWord) != SoapyRPCHeaderWord)
+    {
+        throw std::runtime_error("SoapyRPCUnpacker::recv() FAIL: header word");
+    }
+    //TODO ignoring the version for now
+    //the check may need to be delicate with the version major, minor vs patch number
+    const size_t length = ntohl(header.length);
+    if (length <= sizeof(SoapyRPCHeader) + sizeof(SoapyRPCTrailer))
+    {
+        throw std::runtime_error("SoapyRPCUnpacker::recv() FAIL: header length");
+    }
+
+    //receive the remaining payload
+    _capacity = length - sizeof(SoapyRPCHeader);
+    _message = (char *)malloc(_capacity);
+    size_t bytesReceived = 0;
+    while (bytesReceived != _capacity)
+    {
+        const size_t toRecv = std::min<size_t>(SOAPY_REMOTE_SOCKET_BUFFMAX, _capacity-bytesReceived);
+        ret = _sock.recv(_message+bytesReceived, toRecv);
+        if (ret < 0)
+        {
+            throw std::runtime_error("SoapyRPCUnpacker::recv(payload) FAIL: "+std::string(_sock.lastErrorMsg()));
+        }
+        bytesReceived += ret;
+    }
+
+    //check the trailer
+    SoapyRPCTrailer trailer;
+    std::memcpy(&trailer, _message + _capacity - sizeof(SoapyRPCTrailer), sizeof(trailer));
+    if (ntohl(trailer.trailerWord) != SoapyRPCTrailerWord)
+    {
+        throw std::runtime_error("SoapyRPCUnpacker::recv() FAIL: trailer word");
+    }
+
+    //auto-consume void
+    if (this->peekType() == SOAPY_REMOTE_VOID)
+    {
+        SoapyRemoteTypes type;
+        *this & type;
+    }
+
+    //check for exceptions
+    else if (this->peekType() == SOAPY_REMOTE_EXCEPTION)
+    {
+        SoapyRemoteTypes type;
+        std::string errorMsg;
+        *this & type;
+        *this & errorMsg;
+        throw std::runtime_error("RemoteError: "+errorMsg);
+    }
+}
+
+void SoapyRPCUnpacker::unpack(void *buff, const size_t length)
+{
+    std::memcpy(buff, this->unpack(length), length);
+}
+
+void *SoapyRPCUnpacker::unpack(const size_t length)
+{
+    if (_offset + length > _capacity - sizeof(SoapyRPCTrailer))
+    {
+        throw std::runtime_error("SoapyRPCUnpacker::unpack() OVER-CONSUME");
+    }
+    void *buff = _message+_offset;
+    _offset += length;
+    return buff;
+}
+
+bool SoapyRPCUnpacker::done(void) const
+{
+    return (_offset + sizeof(SoapyRPCTrailer)) == _capacity;
+}
+
+#define UNPACK_TYPE_HELPER(expected) \
+    SoapyRemoteTypes type; *this & type; \
+    if (type != expected) {throw std::runtime_error("SoapyRPCUnpacker type check FAIL:" #expected);} else {}
+
+void SoapyRPCUnpacker::operator&(SoapyRemoteCalls &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_CALL);
+    int call = 0;
+    *this & call;
+    value = SoapyRemoteCalls(call);
+}
+
+void SoapyRPCUnpacker::operator&(char &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_CHAR);
+    value = this->unpack();
+}
+
+void SoapyRPCUnpacker::operator&(bool &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_BOOL);
+    char in = this->unpack();
+    value = (in == 0)?false:true;
+}
+
+void SoapyRPCUnpacker::operator&(int &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_INT32);
+    this->unpack(&value, sizeof(value));
+    value = ntohl(value);
+}
+
+void SoapyRPCUnpacker::operator&(long long &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_INT64);
+    this->unpack(&value, sizeof(value));
+    value = ntohll(value);
+}
+
+void SoapyRPCUnpacker::operator&(double &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_FLOAT64);
+    int exp = 0;
+    long long man = 0;
+    *this & exp;
+    *this & man;
+    value = std::ldexp(double(man), exp-DBL_MANT_DIG);
+}
+
+void SoapyRPCUnpacker::operator&(std::complex<double> &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_COMPLEX128);
+    double r = 0.0, i = 0.0;
+    *this & r;
+    *this & i;
+    value = std::complex<double>(r, i);
+}
+
+void SoapyRPCUnpacker::operator&(std::string &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_STRING);
+    int size = 0;
+    *this & size;
+    value = std::string((const char *)this->unpack(size), size);
+}
+
+void SoapyRPCUnpacker::operator&(SoapySDR::Range &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_RANGE);
+    double minimum = 0.0, maximum = 0.0;
+    *this & minimum;
+    *this & maximum;
+    value = SoapySDR::Range(minimum, maximum);
+}
+
+void SoapyRPCUnpacker::operator&(SoapySDR::RangeList &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_RANGE_LIST);
+    int size = 0;
+    *this & size;
+    value.resize(size);
+    for (size_t i = 0; i < size_t(size); i++) *this & value[i];
+}
+
+void SoapyRPCUnpacker::operator&(std::vector<std::string> &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_STRING_LIST);
+    int size = 0;
+    *this & size;
+    value.resize(size);
+    for (size_t i = 0; i < size_t(size); i++) *this & value[i];
+}
+
+void SoapyRPCUnpacker::operator&(std::vector<double> &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_FLOAT64_LIST);
+    int size = 0;
+    *this & size;
+    value.resize(size);
+    for (size_t i = 0; i < size_t(size); i++) *this & value[i];
+}
+
+void SoapyRPCUnpacker::operator&(SoapySDR::Kwargs &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_KWARGS);
+    int size = 0;
+    *this & size;
+    value.clear();
+    for (size_t i = 0; i < size_t(size); i++)
+    {
+        std::string key, val;
+        *this & key;
+        *this & val;
+        value[key] = val;
+    }
+}
+
+void SoapyRPCUnpacker::operator&(SoapySDR::KwargsList &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_KWARGS_LIST);
+    int size = 0;
+    *this & size;
+    value.resize(size);
+    for (size_t i = 0; i < size_t(size); i++) *this & value[i];
+}
+
+void SoapyRPCUnpacker::operator&(std::vector<size_t> &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_SIZE_LIST);
+    int size = 0;
+    *this & size;
+    value.resize(size);
+    for (size_t i = 0; i < value.size(); i++)
+    {
+        *this & size;
+        value[i] = size;
+    }
+}
+
+void SoapyRPCUnpacker::operator&(SoapySDR::ArgInfo &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_ARG_INFO);
+    *this & value.key;
+    *this & value.value;
+    *this & value.name;
+    *this & value.description;
+    *this & value.units;
+    int intType = 0; *this & intType;
+    value.type = SoapySDR::ArgInfo::Type(intType);
+    *this & value.range;
+    *this & value.options;
+    *this & value.optionNames;
+}
+
+void SoapyRPCUnpacker::operator&(SoapySDR::ArgInfoList &value)
+{
+    UNPACK_TYPE_HELPER(SOAPY_REMOTE_ARG_INFO_LIST);
+    int size = 0;
+    *this & size;
+    value.resize(size);
+    for (size_t i = 0; i < size_t(size); i++) *this & value[i];
+}
diff --git a/common/SoapyRPCUnpacker.hpp b/common/SoapyRPCUnpacker.hpp
new file mode 100644
index 0000000..6fee795
--- /dev/null
+++ b/common/SoapyRPCUnpacker.hpp
@@ -0,0 +1,115 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+#include <SoapySDR/Types.hpp>
+#include <vector>
+#include <complex>
+#include <string>
+
+class SoapyRPCSocket;
+
+/*!
+ * The unpacker object receives a complete RPC message,
+ * and unpacks the message into primitive Soapy SDR types.
+ */
+class SOAPY_REMOTE_API SoapyRPCUnpacker
+{
+public:
+    SoapyRPCUnpacker(SoapyRPCSocket &sock, const bool autoRecv = true);
+
+    ~SoapyRPCUnpacker(void);
+
+    //! Receive a complete RPC message
+    void recv(void);
+
+    //! Unpack a binary blob of known size
+    void unpack(void *buff, const size_t length);
+
+    //! Copy-less version of unpack
+    void *unpack(const size_t length);
+
+    //! Unpack a single byte
+    char unpack(void)
+    {
+        char byte = _message[_offset];
+        _offset++;
+        return byte;
+    }
+
+    //! Done when no data is left to unpack
+    bool done(void) const;
+
+    //! View the next type without consuming
+    SoapyRemoteTypes peekType(void) const
+    {
+        return SoapyRemoteTypes(_message[_offset]);
+    }
+
+    //! Unpack the call
+    void operator&(SoapyRemoteCalls &value);
+
+    //! Unpack the type
+    void operator&(SoapyRemoteTypes &value)
+    {
+        value = SoapyRemoteTypes(this->unpack());
+    }
+
+    //! Unpack a character
+    void operator&(char &value);
+
+    //! Unpack a boolean
+    void operator&(bool &value);
+
+    //! Unpack a 32-bit integer
+    void operator&(int &value);
+
+    //! Unpack a 64-bit integer
+    void operator&(long long &value);
+
+    //! Unpack a double float
+    void operator&(double &value);
+
+    //! Unpack a complex double float
+    void operator&(std::complex<double> &value);
+
+    //! Unpack a string
+    void operator&(std::string &value);
+
+    //! Unpack a range
+    void operator&(SoapySDR::Range &value);
+
+    //! Unpack a list of ranges
+    void operator&(SoapySDR::RangeList &value);
+
+    //! Unpack a list of strings
+    void operator&(std::vector<std::string> &value);
+
+    //! Unpack a list of double floats
+    void operator&(std::vector<double> &value);
+
+    //! Unpack a kwargs dictionary
+    void operator&(SoapySDR::Kwargs &value);
+
+    //! Unpack a list of kwargs
+    void operator&(SoapySDR::KwargsList &value);
+
+    //! Unpack a list of sizes
+    void operator&(std::vector<size_t> &value);
+
+    //! Unpack an arg info structure
+    void operator&(SoapySDR::ArgInfo &value);
+
+    //! Unpack a list of arg infos
+    void operator&(SoapySDR::ArgInfoList &value);
+
+private:
+
+    void ensureSpace(const size_t length);
+
+    SoapyRPCSocket &_sock;
+    char *_message;
+    size_t _offset;
+    size_t _capacity;
+};
diff --git a/common/SoapyRemoteConfig.hpp b/common/SoapyRemoteConfig.hpp
new file mode 100644
index 0000000..ef2179c
--- /dev/null
+++ b/common/SoapyRemoteConfig.hpp
@@ -0,0 +1,19 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include <SoapySDR/Config.hpp>
+
+/***********************************************************************
+ * API export defines
+ **********************************************************************/
+#ifdef SOAPY_REMOTE_DLL // defined if SOAPY is compiled as a DLL
+  #ifdef SOAPY_REMOTE_DLL_EXPORTS // defined if we are building the SOAPY DLL (instead of using it)
+    #define SOAPY_REMOTE_API SOAPY_SDR_HELPER_DLL_EXPORT
+  #else
+    #define SOAPY_REMOTE_API SOAPY_SDR_HELPER_DLL_IMPORT
+  #endif // SOAPY_REMOTE_DLL_EXPORTS
+  #define SOAPY_REMOTE_LOCAL SOAPY_SDR_HELPER_DLL_LOCAL
+#else // SOAPY_REMOTE_DLL is not defined: this means SOAPY is a static lib.
+  #define SOAPY_REMOTE_API SOAPY_SDR_HELPER_DLL_EXPORT
+#endif // SOAPY_REMOTE_DLL
diff --git a/common/SoapyRemoteDefs.hpp b/common/SoapyRemoteDefs.hpp
new file mode 100644
index 0000000..23ebe16
--- /dev/null
+++ b/common/SoapyRemoteDefs.hpp
@@ -0,0 +1,285 @@
+// Copyright (c) 2015-2016 Josh Blum
+// Copyright (c) 2016-2016 Bastille Networks
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+
+/***********************************************************************
+ * Key-words and their defaults
+ **********************************************************************/
+//! Use this magic stop key in the server to prevent infinite loops
+#define SOAPY_REMOTE_KWARG_STOP "soapy_remote_no_deeper"
+
+//! Use this key prefix to pass in args that will become local
+#define SOAPY_REMOTE_KWARG_PREFIX "remote:"
+
+//! Stream args key to set the format on the remote server
+#define SOAPY_REMOTE_KWARG_FORMAT (SOAPY_REMOTE_KWARG_PREFIX "format")
+
+//! Stream args key to set the scalar for local float conversions
+#define SOAPY_REMOTE_KWARG_SCALAR (SOAPY_REMOTE_KWARG_PREFIX "scalar")
+
+//! Stream args key to set the buffer MTU bytes for network transfers
+#define SOAPY_REMOTE_KWARG_MTU (SOAPY_REMOTE_KWARG_PREFIX "mtu")
+
+/*!
+ * Default stream transfer size (under network MTU).
+ * Larger transfer sizes may not be supported in hardware
+ * or may require tweaks to the system configuration.
+ */
+#define SOAPY_REMOTE_DEFAULT_ENDPOINT_MTU 1500
+
+/*!
+ * Stream args key to set the very large socket buffer size in bytes.
+ * This sets the socket buffer size as well as the flow control window.
+ */
+#define SOAPY_REMOTE_KWARG_WINDOW (SOAPY_REMOTE_KWARG_PREFIX "window")
+
+/*!
+ * Default number of bytes in socket buffer.
+ * Larger buffer sizes may not be supported or
+ * may require tweaks to the system configuration.
+ */
+#ifdef __APPLE__ //large buffer size causes crash
+#define SOAPY_REMOTE_DEFAULT_ENDPOINT_WINDOW (16*1024)
+#else
+#define SOAPY_REMOTE_DEFAULT_ENDPOINT_WINDOW (42*1024*1024)
+#endif
+
+/*!
+ * Stream args key to set the priority of the forwarding threads.
+ * Priority ranges: -1.0 (low), 0.0 (normal), and 1.0 (high)
+ */
+#define SOAPY_REMOTE_KWARG_PRIORITY (SOAPY_REMOTE_KWARG_PREFIX "priority")
+
+//! Default thread priority is elevated for stream forwarding
+#define SOAPY_REMOTE_DEFAULT_THREAD_PRIORITY double(0.5)
+
+/***********************************************************************
+ * Socket defaults
+ **********************************************************************/
+
+//! The default bind port for the remote server
+#define SOAPY_REMOTE_DEFAULT_SERVICE "55132"
+
+//! Use this timeout for every socket poll loop
+#define SOAPY_REMOTE_SOCKET_TIMEOUT_US (50*1000) //50 ms
+
+//! Backlog count for the server socket listen
+#define SOAPY_REMOTE_LISTEN_BACKLOG 100
+
+/*!
+ * The number of buffers that can be acquired.
+ * This is the number of buffers for the direct access API.
+ * The socket is doing all of the actual buffering,
+ * this just allows the user to get some flexibility
+ * with the direct access API. Otherwise, most client code
+ * will acquire and immediately release the same handle.
+ */
+#define SOAPY_REMOTE_ENDPOINT_NUM_BUFFS 8
+
+/*!
+ * The maximum buffer size for single socket call.
+ * Use this in the packer and unpacker TCP code.
+ * Larger buffers may crash some socket implementations.
+ * The implementation should loop until completed.
+ */
+#define SOAPY_REMOTE_SOCKET_BUFFMAX 4096
+
+/***********************************************************************
+ * RPC structures and constants
+ **********************************************************************/
+//major, minor, patch when this was last updated
+//bump the version number when changes are made
+static const unsigned int SoapyRPCVersion = 0x000300;
+
+enum SoapyRemoteTypes
+{
+    SOAPY_REMOTE_CHAR            = 0,
+    SOAPY_REMOTE_BOOL            = 1,
+    SOAPY_REMOTE_INT32           = 2,
+    SOAPY_REMOTE_INT64           = 3,
+    SOAPY_REMOTE_FLOAT64         = 4,
+    SOAPY_REMOTE_COMPLEX128      = 5,
+    SOAPY_REMOTE_STRING          = 6,
+    SOAPY_REMOTE_RANGE           = 7,
+    SOAPY_REMOTE_RANGE_LIST      = 8,
+    SOAPY_REMOTE_STRING_LIST     = 9,
+    SOAPY_REMOTE_FLOAT64_LIST    = 10,
+    SOAPY_REMOTE_KWARGS          = 11,
+    SOAPY_REMOTE_KWARGS_LIST     = 12,
+    SOAPY_REMOTE_EXCEPTION       = 13,
+    SOAPY_REMOTE_VOID            = 14,
+    SOAPY_REMOTE_CALL            = 15,
+    SOAPY_REMOTE_SIZE_LIST       = 16,
+    SOAPY_REMOTE_ARG_INFO        = 17,
+    SOAPY_REMOTE_ARG_INFO_LIST   = 18,
+    SOAPY_REMOTE_TYPE_MAX        = 19,
+};
+
+enum SoapyRemoteCalls
+{
+    //factory
+    SOAPY_REMOTE_FIND            = 0,
+    SOAPY_REMOTE_MAKE            = 1,
+    SOAPY_REMOTE_UNMAKE          = 2,
+    SOAPY_REMOTE_HANGUP          = 3,
+
+    //logger
+    SOAPY_REMOTE_GET_SERVER_ID          = 20,
+    SOAPY_REMOTE_START_LOG_FORWARDING   = 21,
+    SOAPY_REMOTE_STOP_LOG_FORWARDING    = 22,
+
+    //identification
+    SOAPY_REMOTE_GET_DRIVER_KEY      = 100,
+    SOAPY_REMOTE_GET_HARDWARE_KEY    = 101,
+    SOAPY_REMOTE_GET_HARDWARE_INFO   = 102,
+
+    //channels
+    SOAPY_REMOTE_SET_FRONTEND_MAPPING      = 200,
+    SOAPY_REMOTE_GET_FRONTEND_MAPPING      = 201,
+    SOAPY_REMOTE_GET_NUM_CHANNELS          = 202,
+    SOAPY_REMOTE_GET_FULL_DUPLEX           = 203,
+    SOAPY_REMOTE_GET_CHANNEL_INFO          = 204,
+
+    //stream
+    SOAPY_REMOTE_SETUP_STREAM              = 300,
+    SOAPY_REMOTE_CLOSE_STREAM              = 301,
+    SOAPY_REMOTE_ACTIVATE_STREAM           = 302,
+    SOAPY_REMOTE_DEACTIVATE_STREAM         = 303,
+    SOAPY_REMOTE_GET_STREAM_FORMATS        = 304,
+    SOAPY_REMOTE_GET_NATIVE_STREAM_FORMAT  = 305,
+    SOAPY_REMOTE_GET_STREAM_ARGS_INFO      = 306,
+
+    //antenna
+    SOAPY_REMOTE_LIST_ANTENNAS      = 500,
+    SOAPY_REMOTE_SET_ANTENNA        = 501,
+    SOAPY_REMOTE_GET_ANTENNA        = 502,
+
+    //corrections
+    SOAPY_REMOTE_HAS_DC_OFFSET_MODE       = 600,
+    SOAPY_REMOTE_SET_DC_OFFSET_MODE       = 601,
+    SOAPY_REMOTE_GET_DC_OFFSET_MODE       = 602,
+    SOAPY_REMOTE_HAS_DC_OFFSET            = 603,
+    SOAPY_REMOTE_SET_DC_OFFSET            = 604,
+    SOAPY_REMOTE_GET_DC_OFFSET            = 605,
+    SOAPY_REMOTE_HAS_IQ_BALANCE_MODE      = 606,
+    SOAPY_REMOTE_SET_IQ_BALANCE_MODE      = 607,
+    SOAPY_REMOTE_GET_IQ_BALANCE_MODE      = 608,
+
+    //gain
+    SOAPY_REMOTE_LIST_GAINS               = 700,
+    SOAPY_REMOTE_SET_GAIN_MODE            = 701,
+    SOAPY_REMOTE_GET_GAIN_MODE            = 702,
+    SOAPY_REMOTE_SET_GAIN                 = 703,
+    SOAPY_REMOTE_SET_GAIN_ELEMENT         = 704,
+    SOAPY_REMOTE_GET_GAIN                 = 705,
+    SOAPY_REMOTE_GET_GAIN_ELEMENT         = 706,
+    SOAPY_REMOTE_GET_GAIN_RANGE           = 707,
+    SOAPY_REMOTE_GET_GAIN_RANGE_ELEMENT   = 708,
+    SOAPY_REMOTE_HAS_GAIN_MODE            = 709,
+
+    //frequency
+    SOAPY_REMOTE_SET_FREQUENCY                 = 800,
+    SOAPY_REMOTE_SET_FREQUENCY_COMPONENT       = 801,
+    SOAPY_REMOTE_GET_FREQUENCY                 = 802,
+    SOAPY_REMOTE_GET_FREQUENCY_COMPONENT       = 803,
+    SOAPY_REMOTE_LIST_FREQUENCIES              = 804,
+    SOAPY_REMOTE_GET_FREQUENCY_RANGE           = 805,
+    SOAPY_REMOTE_GET_FREQUENCY_RANGE_COMPONENT = 806,
+    SOAPY_REMOTE_GET_FREQUENCY_ARGS_INFO       = 807,
+
+    //sample rate
+    SOAPY_REMOTE_SET_SAMPLE_RATE               = 900,
+    SOAPY_REMOTE_GET_SAMPLE_RATE               = 901,
+    SOAPY_REMOTE_LIST_SAMPLE_RATES             = 902,
+
+    //bandwidth
+    SOAPY_REMOTE_SET_BANDWIDTH                 = 903,
+    SOAPY_REMOTE_GET_BANDWIDTH                 = 904,
+    SOAPY_REMOTE_LIST_BANDWIDTHS               = 905,
+    SOAPY_REMOTE_GET_BANDWIDTH_RANGE           = 906,
+
+    //clocking
+    SOAPY_REMOTE_SET_MASTER_CLOCK_RATE         = 1000,
+    SOAPY_REMOTE_GET_MASTER_CLOCK_RATE         = 1001,
+    SOAPY_REMOTE_LIST_CLOCK_SOURCES            = 1002,
+    SOAPY_REMOTE_SET_CLOCK_SOURCE              = 1003,
+    SOAPY_REMOTE_GET_CLOCK_SOURCE              = 1004,
+    SOAPY_REMOTE_GET_MASTER_CLOCK_RATES        = 1008,
+
+    //time
+    SOAPY_REMOTE_LIST_TIME_SOURCES             = 1005,
+    SOAPY_REMOTE_SET_TIME_SOURCE               = 1006,
+    SOAPY_REMOTE_GET_TIME_SOURCE               = 1007,
+    SOAPY_REMOTE_HAS_HARDWARE_TIME        = 1100,
+    SOAPY_REMOTE_GET_HARDWARE_TIME        = 1101,
+    SOAPY_REMOTE_SET_HARDWARE_TIME        = 1102,
+    SOAPY_REMOTE_SET_COMMAND_TIME         = 1103,
+
+    //sensors
+    SOAPY_REMOTE_LIST_SENSORS            = 1200,
+    SOAPY_REMOTE_READ_SENSOR             = 1201,
+    SOAPY_REMOTE_LIST_CHANNEL_SENSORS    = 1202,
+    SOAPY_REMOTE_READ_CHANNEL_SENSOR     = 1203,
+    SOAPY_REMOTE_GET_SENSOR_INFO         = 1204,
+    SOAPY_REMOTE_GET_CHANNEL_SENSOR_INFO = 1205,
+
+    //registers
+    SOAPY_REMOTE_WRITE_REGISTER            = 1300,
+    SOAPY_REMOTE_READ_REGISTER             = 1301,
+    SOAPY_REMOTE_LIST_REGISTER_INTERFACES  = 1302,
+    SOAPY_REMOTE_WRITE_REGISTER_NAMED      = 1303,
+    SOAPY_REMOTE_READ_REGISTER_NAMED       = 1304,
+
+    //settings
+    SOAPY_REMOTE_WRITE_SETTING            = 1400,
+    SOAPY_REMOTE_READ_SETTING             = 1401,
+    SOAPY_REMOTE_GET_SETTING_INFO         = 1402,
+    SOAPY_REMOTE_WRITE_CHANNEL_SETTING    = 1403,
+    SOAPY_REMOTE_READ_CHANNEL_SETTING     = 1404,
+    SOAPY_REMOTE_GET_CHANNEL_SETTING_INFO = 1405,
+
+    //gpio
+    SOAPY_REMOTE_LIST_GPIO_BANKS         = 1500,
+    SOAPY_REMOTE_WRITE_GPIO              = 1501,
+    SOAPY_REMOTE_WRITE_GPIO_MASKED       = 1502,
+    SOAPY_REMOTE_READ_GPIO               = 1503,
+    SOAPY_REMOTE_WRITE_GPIO_DIR          = 1504,
+    SOAPY_REMOTE_WRITE_GPIO_DIR_MASKED   = 1505,
+    SOAPY_REMOTE_READ_GPIO_DIR           = 1506,
+
+    //i2c
+    SOAPY_REMOTE_WRITE_I2C            = 1600,
+    SOAPY_REMOTE_READ_I2C             = 1601,
+
+    //spi
+    SOAPY_REMOTE_TRANSACT_SPI         = 1700,
+
+    //uart
+    SOAPY_REMOTE_LIST_UARTS            = 1801,
+    SOAPY_REMOTE_WRITE_UART            = 1802,
+    SOAPY_REMOTE_READ_UART             = 1803,
+};
+
+#define SOAPY_PACKET_WORD32(str) \
+    ((unsigned int)(str[0]) << 24) | \
+    ((unsigned int)(str[1]) << 16) | \
+    ((unsigned int)(str[2]) << 8) | \
+    ((unsigned int)(str[3]) << 0)
+
+static const unsigned int SoapyRPCHeaderWord = SOAPY_PACKET_WORD32("SRPC");
+static const unsigned int SoapyRPCTrailerWord = SOAPY_PACKET_WORD32("CPRS");
+
+struct SoapyRPCHeader
+{
+    unsigned int headerWord; //!< header word to identify this protocol
+    unsigned int version; //!< version number for protocol compatibility
+    unsigned int length; //!< complete packet length in bytes
+};
+
+struct SoapyRPCTrailer
+{
+    unsigned int trailerWord; //!< trailer word to identify this protocol
+};
diff --git a/common/SoapySSDPEndpoint.cpp b/common/SoapySSDPEndpoint.cpp
new file mode 100644
index 0000000..4aa4ab4
--- /dev/null
+++ b/common/SoapySSDPEndpoint.cpp
@@ -0,0 +1,359 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+/*
+ * Docs and examples:
+ * https://stackoverflow.com/questions/13382469/ssdp-protocol-implementation
+ * http://buildingskb.schneider-electric.com/view.php?AID=15197
+ * http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
+ */
+
+#include <SoapySDR/Logger.hpp>
+#include "SoapySSDPEndpoint.hpp"
+#include "SoapyURLUtils.hpp"
+#include "SoapyInfoUtils.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyHTTPUtils.hpp"
+#include "SoapyRPCSocket.hpp"
+#include <thread>
+#include <ctime>
+#include <cctype>
+#include <set>
+
+//! IPv4 multi-cast address for SSDP communications
+#define SSDP_MULTICAST_ADDR_IPV4 "239.255.255.250"
+
+//! IPv6 multi-cast address for SSDP communications
+#define SSDP_MULTICAST_ADDR_IPV6 "ff02::c"
+
+//! UDP service port number for SSDP communications
+#define SSDP_UDP_PORT_NUMBER "1900"
+
+//! service and notify target identification string
+#define SOAPY_REMOTE_TARGET "urn:schemas-pothosware-com:service:soapyRemote:1"
+
+//! How often search and notify packets are triggered
+#define TRIGGER_TIMEOUT_SECONDS 60
+
+//! The default duration of an entry in the USN cache
+#define CACHE_DURATION_SECONDS 120
+
+//! Service is active, use with multicast NOTIFY
+#define NTS_ALIVE "ssdp:alive"
+
+//! Service stopped, use with multicast NOTIFY
+#define NTS_BYEBYE "ssdp:byebye"
+
+struct SoapySSDPEndpointData
+{
+    SoapyRPCSocket sock;
+    std::string groupURL;
+    std::thread *thread;
+    std::chrono::high_resolution_clock::time_point lastTimeSearch;
+    std::chrono::high_resolution_clock::time_point lastTimeNotify;
+};
+
+static std::string timeNowGMT(void)
+{
+    char buff[128];
+    auto t = std::time(nullptr);
+    size_t len = std::strftime(buff, sizeof(buff), "%c %Z", std::localtime(&t));
+    return std::string(buff, len);
+}
+
+std::shared_ptr<SoapySSDPEndpoint> SoapySSDPEndpoint::getInstance(void)
+{
+    static std::mutex singletonMutex;
+    std::lock_guard<std::mutex> lock(singletonMutex);
+    static std::weak_ptr<SoapySSDPEndpoint> epWeak;
+    auto epShared = epWeak.lock();
+    if (not epShared) epShared.reset(new SoapySSDPEndpoint());
+    epWeak = epShared;
+    return epShared;
+}
+
+SoapySSDPEndpoint::SoapySSDPEndpoint(void):
+    serviceRegistered(false),
+    periodicSearchEnabled(false),
+    periodicNotifyEnabled(false),
+    done(false)
+{
+    const bool isIPv6Supported = not SoapyRPCSocket(SoapyURL("tcp", "::", "0").toString()).null();
+    this->spawnHandler("0.0.0.0", SSDP_MULTICAST_ADDR_IPV4);
+    if (isIPv6Supported) this->spawnHandler("::", SSDP_MULTICAST_ADDR_IPV6);
+}
+
+SoapySSDPEndpoint::~SoapySSDPEndpoint(void)
+{
+    done = true;
+    for (auto &data : handlers)
+    {
+        data->thread->join();
+        delete data->thread;
+        delete data;
+    }
+}
+
+void SoapySSDPEndpoint::registerService(const std::string &uuid, const std::string &service)
+{
+    std::lock_guard<std::mutex> lock(mutex);
+    this->serviceRegistered = true;
+    this->uuid = uuid;
+    this->service = service;
+}
+
+void SoapySSDPEndpoint::enablePeriodicSearch(const bool enable)
+{
+    std::lock_guard<std::mutex> lock(mutex);
+    periodicSearchEnabled = enable;
+    for (auto &data : handlers) this->sendSearchHeader(data);
+}
+
+void SoapySSDPEndpoint::enablePeriodicNotify(const bool enable)
+{
+    std::lock_guard<std::mutex> lock(mutex);
+    periodicNotifyEnabled = enable;
+    for (auto &data : handlers) this->sendNotifyHeader(data, NTS_ALIVE);
+}
+
+std::vector<std::string> SoapySSDPEndpoint::getServerURLs(void)
+{
+    std::lock_guard<std::mutex> lock(mutex);
+    std::vector<std::string> serverURLs;
+    for (auto &pair : usnToURL) serverURLs.push_back(pair.second.first);
+    return serverURLs;
+}
+
+void SoapySSDPEndpoint::spawnHandler(const std::string &bindAddr, const std::string &groupAddr)
+{
+    //static list of blacklisted groups
+    //if we fail to join a group, its blacklisted
+    //so future instances wont get the same error
+    //thread-safe protected by the get instance call
+    static std::set<std::string> blacklistedGroups;
+
+    //check the blacklist
+    if (blacklistedGroups.find(groupAddr) != blacklistedGroups.end())
+    {
+        SoapySDR::logf(SOAPY_SDR_DEBUG, "SoapySSDPEndpoint::spawnHandler(%s) group blacklisted due to previous error", groupAddr.c_str());
+        return;
+    }
+
+    auto data = new SoapySSDPEndpointData();
+    auto &sock = data->sock;
+
+    const auto groupURL = SoapyURL("udp", groupAddr, SSDP_UDP_PORT_NUMBER).toString();
+    int ret = sock.multicastJoin(groupURL);
+    if (ret != 0)
+    {
+        blacklistedGroups.insert(groupAddr);
+        SoapySDR::logf(SOAPY_SDR_WARNING, "SoapySSDPEndpoint failed join group %s\n  %s", groupURL.c_str(), sock.lastErrorMsg());
+        delete data;
+        return;
+    }
+
+    const auto bindURL = SoapyURL("udp", bindAddr, SSDP_UDP_PORT_NUMBER).toString();
+    ret = sock.bind(bindURL);
+    if (ret != 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapySSDPEndpoint::bind(%s) failed\n  %s", bindURL.c_str(), sock.lastErrorMsg());
+        delete data;
+        return;
+    }
+
+    data->groupURL = groupURL;
+    data->thread = new std::thread(&SoapySSDPEndpoint::handlerLoop, this, data);
+    handlers.push_back(data);
+}
+
+void SoapySSDPEndpoint::handlerLoop(SoapySSDPEndpointData *data)
+{
+    auto &sock = data->sock;
+
+    std::string recvAddr;
+    char recvBuff[SOAPY_REMOTE_DEFAULT_ENDPOINT_MTU];
+
+    while (not done)
+    {
+        //receive SSDP traffic
+        if (sock.selectRecv(SOAPY_REMOTE_SOCKET_TIMEOUT_US))
+        {
+            std::lock_guard<std::mutex> lock(mutex);
+            int ret = sock.recvfrom(recvBuff, sizeof(recvBuff), recvAddr);
+            if (ret < 0)
+            {
+                SoapySDR::logf(SOAPY_SDR_ERROR, "SoapySSDPEndpoint::recvfrom() = %d\n  %s", ret, sock.lastErrorMsg());
+                return;
+            }
+
+            //parse the HTTP header
+            SoapyHTTPHeader header(recvBuff, size_t(ret));
+            if (header.getLine0() == "M-SEARCH * HTTP/1.1") this->handleSearchRequest(data, header, recvAddr);
+            if (header.getLine0() == "HTTP/1.1 200 OK") this->handleSearchResponse(data, header, recvAddr);
+            if (header.getLine0() == "NOTIFY * HTTP/1.1") this->handleNotifyRequest(data, header, recvAddr);
+        }
+
+        //locked for all non-blocking routines below
+        std::lock_guard<std::mutex> lock(mutex);
+        const auto timeNow = std::chrono::high_resolution_clock::now();
+        const auto triggerExpired = timeNow + std::chrono::seconds(TRIGGER_TIMEOUT_SECONDS);
+
+        //remove old cache entries
+        auto it = usnToURL.begin();
+        while (it != usnToURL.end())
+        {
+            auto &expires = it->second.second;
+            if (expires > timeNow) ++it;
+            else usnToURL.erase(it++);
+        }
+
+        //check trigger for periodic search
+        if (periodicSearchEnabled and data->lastTimeSearch > triggerExpired)
+        {
+            this->sendSearchHeader(data);
+        }
+
+        //check trigger for periodic notify
+        if (periodicNotifyEnabled and data->lastTimeNotify > triggerExpired)
+        {
+            this->sendNotifyHeader(data, NTS_ALIVE);
+        }
+    }
+
+    //disconnect notification when done
+    if (done)
+    {
+        std::lock_guard<std::mutex> lock(mutex);
+        this->sendNotifyHeader(data, NTS_BYEBYE);
+    }
+}
+
+void SoapySSDPEndpoint::sendHeader(SoapyRPCSocket &sock, const SoapyHTTPHeader &header, const std::string &addr)
+{
+    int ret = sock.sendto(header.data(), header.size(), addr);
+    if (ret != int(header.size()))
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "SoapySSDPEndpoint::sendTo(%s) = %d\n  %s", addr.c_str(), ret, sock.lastErrorMsg());
+    }
+}
+
+void SoapySSDPEndpoint::sendSearchHeader(SoapySSDPEndpointData *data)
+{
+    auto hostURL = SoapyURL(data->groupURL);
+    hostURL.setScheme(""); //no scheme name
+
+    SoapyHTTPHeader header("M-SEARCH * HTTP/1.1");
+    header.addField("HOST", hostURL.toString());
+    header.addField("MAN", "\"ssdp:discover\"");
+    header.addField("MX", "2");
+    header.addField("ST", SOAPY_REMOTE_TARGET);
+    header.addField("USER-AGENT", SoapyInfo::getUserAgent());
+    header.finalize();
+    this->sendHeader(data->sock, header, data->groupURL);
+    data->lastTimeSearch = std::chrono::high_resolution_clock::now();
+}
+
+void SoapySSDPEndpoint::sendNotifyHeader(SoapySSDPEndpointData *data, const std::string &nts)
+{
+    if (not serviceRegistered) return; //do we have a service to advertise?
+
+    auto hostURL = SoapyURL(data->groupURL);
+    hostURL.setScheme(""); //no scheme name
+
+    SoapyHTTPHeader header("NOTIFY * HTTP/1.1");
+    header.addField("HOST", hostURL.toString());
+    if (nts == NTS_ALIVE)
+    {
+        header.addField("CACHE-CONTROL", "max-age=" + std::to_string(CACHE_DURATION_SECONDS));
+        header.addField("LOCATION", SoapyURL("tcp", SoapyInfo::getHostName(), service).toString());
+    }
+    header.addField("SERVER", SoapyInfo::getUserAgent());
+    header.addField("NT", SOAPY_REMOTE_TARGET);
+    header.addField("USN", "uuid:"+uuid+"::"+SOAPY_REMOTE_TARGET);
+    header.addField("NTS", nts);
+    header.finalize();
+    this->sendHeader(data->sock, header, data->groupURL);
+    data->lastTimeNotify = std::chrono::high_resolution_clock::now();
+}
+
+void SoapySSDPEndpoint::handleSearchRequest(SoapySSDPEndpointData *data, const SoapyHTTPHeader &request, const std::string &recvAddr)
+{
+    if (not serviceRegistered) return; //do we have a service to advertise?
+
+    if (request.getField("MAN") != "\"ssdp:discover\"") return;
+    const auto st = request.getField("ST");
+    const bool stForUs = (st == "ssdp:all" or st == SOAPY_REMOTE_TARGET or st == "uuid:"+uuid);
+    if (not stForUs) return;
+
+    //send a unicast response HTTP header
+    SoapyHTTPHeader response("HTTP/1.1 200 OK");
+    response.addField("CACHE-CONTROL", "max-age=" + std::to_string(CACHE_DURATION_SECONDS));
+    response.addField("DATE", timeNowGMT());
+    response.addField("EXT", "");
+    response.addField("LOCATION", SoapyURL("tcp", SoapyInfo::getHostName(), service).toString());
+    response.addField("SERVER", SoapyInfo::getUserAgent());
+    response.addField("ST", SOAPY_REMOTE_TARGET);
+    response.addField("USN", "uuid:"+uuid+"::"+SOAPY_REMOTE_TARGET);
+    response.finalize();
+    this->sendHeader(data->sock, response, recvAddr);
+
+    //The unicast response may not be received if the destination has multiple SSDP clients
+    //because only one client on the destination host will actually receive the datagram.
+    //To work around this limitation, a multicast notification packet is sent as well;
+    //which will be received by all clients at the destination as well as other hosts.
+    this->sendNotifyHeader(data, NTS_ALIVE);
+}
+
+static int getCacheDuration(const SoapyHTTPHeader &header)
+{
+    const auto cacheControl = header.getField("CACHE-CONTROL");
+    if (cacheControl.empty()) return CACHE_DURATION_SECONDS;
+
+    const auto maxAgePos = cacheControl.find("max-age");
+    const auto equalsPos = cacheControl.find("=");
+    if (maxAgePos == std::string::npos) return CACHE_DURATION_SECONDS;
+    if (equalsPos == std::string::npos) return CACHE_DURATION_SECONDS;
+    if (maxAgePos > equalsPos) return CACHE_DURATION_SECONDS;
+    auto valuePos = equalsPos + 1;
+    while (std::isspace(cacheControl.at(valuePos))) valuePos++;
+
+    const auto maxAge = cacheControl.substr(valuePos);
+    try {return std::stoul(maxAge);}
+    catch (...) {return CACHE_DURATION_SECONDS;}
+}
+
+void SoapySSDPEndpoint::handleSearchResponse(SoapySSDPEndpointData *data, const SoapyHTTPHeader &header, const std::string &recvAddr)
+{
+    if (header.getField("ST") != SOAPY_REMOTE_TARGET) return;
+    this->handleRegisterService(data, header, recvAddr);
+}
+
+void SoapySSDPEndpoint::handleNotifyRequest(SoapySSDPEndpointData *data, const SoapyHTTPHeader &header, const std::string &recvAddr)
+{
+    if (header.getField("NT") != SOAPY_REMOTE_TARGET) return;
+    this->handleRegisterService(data, header, recvAddr);
+}
+
+void SoapySSDPEndpoint::handleRegisterService(SoapySSDPEndpointData *, const SoapyHTTPHeader &header, const std::string &recvAddr)
+{
+    //extract usn
+    const auto usn = header.getField("USN");
+    if (usn.empty()) return;
+
+    //handle byebye from notification packets
+    if (header.getField("NTS") == NTS_BYEBYE)
+    {
+        usnToURL.erase(usn);
+        return;
+    }
+
+    //format the server's url
+    const auto location = header.getField("LOCATION");
+    if (location.empty()) return;
+    const SoapyURL serverURL("tcp", SoapyURL(recvAddr).getNode(), SoapyURL(location).getService());
+    SoapySDR::logf(SOAPY_SDR_DEBUG, "SoapyRemote discovered %s", serverURL.toString().c_str());
+
+    //register the server
+    const auto expires = std::chrono::high_resolution_clock::now() + std::chrono::seconds(getCacheDuration(header));
+    usnToURL[usn] = std::make_pair(serverURL.toString(), expires);
+}
diff --git a/common/SoapySSDPEndpoint.hpp b/common/SoapySSDPEndpoint.hpp
new file mode 100644
index 0000000..0203111
--- /dev/null
+++ b/common/SoapySSDPEndpoint.hpp
@@ -0,0 +1,87 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRPCSocket.hpp"
+#include <map>
+#include <string>
+#include <csignal> //sig_atomic_t
+#include <chrono>
+#include <mutex>
+#include <vector>
+#include <memory>
+
+class SoapyHTTPHeader;
+struct SoapySSDPEndpointData;
+
+/*!
+ * Service an SSDP endpoint to:
+ * keep track of discovered servers of interest,
+ * and to respond to discovery packets for us.
+ */
+class SoapySSDPEndpoint
+{
+public:
+
+    //! Get a singleton instance of the endpoint
+    static std::shared_ptr<SoapySSDPEndpoint> getInstance(void);
+
+    /*!
+     * Create a discovery endpoint
+     */
+    SoapySSDPEndpoint(void);
+
+    ~SoapySSDPEndpoint(void);
+
+    /*!
+     * Allow the endpoint to advertise that its running the RPC service
+     */
+    void registerService(const std::string &uuid, const std::string &service);
+
+    /*!
+     * Enable the client endpoint to search for running services.
+     */
+    void enablePeriodicSearch(const bool enable);
+
+    /*!
+     * Enable the server to send periodic notification messages.
+     */
+    void enablePeriodicNotify(const bool enable);
+
+    //! Get a list of all active server URLs
+    std::vector<std::string> getServerURLs(void);
+
+private:
+    SoapySocketSession sess;
+
+    //protection between threads
+    std::mutex mutex;
+
+    //discovered services
+    std::map<std::string, std::pair<std::string, std::chrono::high_resolution_clock::time_point>> usnToURL;
+
+    //service settings
+    bool serviceRegistered;
+    std::string uuid;
+    std::string service;
+
+    //configured messages
+    bool periodicSearchEnabled;
+    bool periodicNotifyEnabled;
+
+    //server data
+    std::vector<SoapySSDPEndpointData *> handlers;
+
+    //signal done to the thread
+    sig_atomic_t done;
+
+    void spawnHandler(const std::string &bindAddr, const std::string &groupAddr);
+    void handlerLoop(SoapySSDPEndpointData *data);
+    void sendHeader(SoapyRPCSocket &sock, const SoapyHTTPHeader &header, const std::string &addr);
+    void sendSearchHeader(SoapySSDPEndpointData *data);
+    void sendNotifyHeader(SoapySSDPEndpointData *data, const std::string &nts);
+    void handleSearchRequest(SoapySSDPEndpointData *data, const SoapyHTTPHeader &header, const std::string &addr);
+    void handleSearchResponse(SoapySSDPEndpointData *data, const SoapyHTTPHeader &header, const std::string &addr);
+    void handleNotifyRequest(SoapySSDPEndpointData *data, const SoapyHTTPHeader &header, const std::string &addr);
+    void handleRegisterService(SoapySSDPEndpointData *, const SoapyHTTPHeader &header, const std::string &recvAddr);
+};
diff --git a/common/SoapySocketDefs.in.hpp b/common/SoapySocketDefs.in.hpp
new file mode 100644
index 0000000..9356ed8
--- /dev/null
+++ b/common/SoapySocketDefs.in.hpp
@@ -0,0 +1,120 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+// ** This header should be included first, to avoid compile errors.
+// ** At least in the case of the windows header files.
+
+// This header helps to abstract network differences between platforms.
+// Including the correct headers for various network APIs.
+// And providing various typedefs and definitions when missing.
+
+#pragma once
+
+/***********************************************************************
+ * Windows socket headers
+ **********************************************************************/
+#cmakedefine HAS_WINSOCK2_H
+#ifdef HAS_WINSOCK2_H
+#include <winsock2.h> //htonll
+#endif //HAS_WINSOCK2_H
+
+#cmakedefine HAS_WS2TCPIP_H
+#ifdef HAS_WS2TCPIP_H
+#include <ws2tcpip.h> //addrinfo
+typedef int socklen_t;
+#endif //HAS_WS2TCPIP_H
+
+/***********************************************************************
+ * unix socket headers
+ **********************************************************************/
+#cmakedefine HAS_UNISTD_H
+#ifdef HAS_UNISTD_H
+#include <unistd.h> //close
+#define closesocket close
+#endif //HAS_UNISTD_H
+
+#cmakedefine HAS_NETDB_H
+#ifdef HAS_NETDB_H
+#include <netdb.h> //addrinfo
+#endif //HAS_NETDB_H
+
+#cmakedefine HAS_NETINET_IN_H
+#ifdef HAS_NETINET_IN_H
+#include <netinet/in.h>
+#endif //HAS_NETINET_IN_H
+
+#cmakedefine HAS_NETINET_TCP_H
+#ifdef HAS_NETINET_TCP_H
+#include <netinet/tcp.h>
+#endif //HAS_NETINET_TCP_H
+
+#cmakedefine HAS_SYS_TYPES_H
+#ifdef HAS_SYS_TYPES_H
+#include <sys/types.h>
+#endif //HAS_SYS_TYPES_H
+
+#cmakedefine HAS_SYS_SOCKET_H
+#ifdef HAS_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif //HAS_SYS_SOCKET_H
+
+#cmakedefine HAS_ARPA_INET_H
+#ifdef HAS_ARPA_INET_H
+#include <arpa/inet.h> //inet_ntop
+#endif //HAS_ARPA_INET_H
+
+#cmakedefine HAS_IFADDRS_H
+#ifdef HAS_IFADDRS_H
+#include <ifaddrs.h> //getifaddrs
+#endif //HAS_IFADDRS_H
+
+#cmakedefine HAS_NET_IF_H
+#ifdef HAS_NET_IF_H
+#include <net/if.h> //if_nametoindex
+#endif //HAS_NET_IF_H
+
+/***********************************************************************
+ * htonll and ntohll for GCC
+ **********************************************************************/
+#if defined(__GNUC__) && !defined(htonll)
+    #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+        #define htonll(x) __builtin_bswap64(x)
+    #else //big endian
+        #define htonll(x) (x)
+    #endif //little endian
+#endif //__GNUC__ and not htonll
+
+#if defined(__GNUC__) && !defined(ntohll)
+    #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+        #define ntohll(x) __builtin_bswap64(x)
+    #else //big endian
+        #define ntohll(x) (x)
+    #endif //little endian
+#endif //__GNUC__ and not ntohll
+
+/***********************************************************************
+ * socket type definitions
+ **********************************************************************/
+#ifndef INVALID_SOCKET
+#define INVALID_SOCKET -1
+#endif //INVALID_SOCKET
+
+/***********************************************************************
+ * socket errno
+ **********************************************************************/
+#ifdef _MSC_VER
+#define SOCKET_ERRNO WSAGetLastError()
+#else
+#define SOCKET_ERRNO errno
+#endif
+
+/***********************************************************************
+ * OSX compatibility
+ **********************************************************************/
+#if !defined(IPV6_ADD_MEMBERSHIP) && defined(IPV6_JOIN_GROUP)
+#define IPV6_ADD_MEMBERSHIP IPV6_JOIN_GROUP
+#endif
+
+#if !defined(IPV6_DROP_MEMBERSHIP) && defined(IPV6_LEAVE_GROUP)
+#define IPV6_DROP_MEMBERSHIP IPV6_LEAVE_GROUP
+#endif
diff --git a/common/SoapyStreamEndpoint.cpp b/common/SoapyStreamEndpoint.cpp
new file mode 100644
index 0000000..4705031
--- /dev/null
+++ b/common/SoapyStreamEndpoint.cpp
@@ -0,0 +1,380 @@
+// Copyright (c) 2015-2016 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include <SoapySDR/Errors.hpp>
+#include <SoapySDR/Logger.hpp>
+#include "SoapyStreamEndpoint.hpp"
+#include "SoapyRPCSocket.hpp"
+#include "SoapyURLUtils.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapySocketDefs.hpp"
+#include <cassert>
+#include <cstdint>
+
+#define HEADER_SIZE sizeof(StreamDatagramHeader)
+
+//use the larger IPv6 header size
+#define PROTO_HEADER_SIZE (40 + 8) //IPv6 + UDP
+
+struct StreamDatagramHeader
+{
+    uint32_t bytes; //!< total number of bytes in datagram
+    uint32_t sequence; //!< sequence count for flow control
+    uint32_t elems; //!< number of elements or error code
+    int flags; //!< flags associated with this datagram
+    long long time; //!< time associated with this datagram
+};
+
+SoapyStreamEndpoint::SoapyStreamEndpoint(
+    SoapyRPCSocket &streamSock,
+    SoapyRPCSocket &statusSock,
+    const bool isRecv,
+    const size_t numChans,
+    const size_t elemSize,
+    const size_t mtu,
+    const size_t window):
+    _streamSock(streamSock),
+    _statusSock(statusSock),
+    _xferSize(mtu-PROTO_HEADER_SIZE),
+    _numChans(numChans),
+    _elemSize(elemSize),
+    _buffSize(((_xferSize-HEADER_SIZE)/numChans)/elemSize),
+    _numBuffs(SOAPY_REMOTE_ENDPOINT_NUM_BUFFS),
+    _nextHandleAcquire(0),
+    _nextHandleRelease(0),
+    _numHandlesAcquired(0),
+    _lastSendSequence(0),
+    _lastRecvSequence(0),
+    _maxInFlightSeqs(0),
+    _receiveInitial(false),
+    _triggerAckWindow(0)
+{
+    assert(not _streamSock.null());
+
+    //allocate buffer data and default state
+    _buffData.resize(_numBuffs);
+    for (auto &data : _buffData)
+    {
+        data.acquired = false;
+        data.buff.resize(_xferSize);
+        data.buffs.resize(_numChans);
+        for (size_t i = 0; i < _numChans; i++)
+        {
+            size_t offsetBytes = HEADER_SIZE+(i*_buffSize*_elemSize);
+            data.buffs[i] = (void*)(data.buff.data()+offsetBytes);
+        }
+    }
+
+    //endpoints require a large socket buffer in the data direction
+    int ret = _streamSock.setBuffSize(isRecv, window);
+    if (ret != 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint resize socket buffer to %d KiB failed\n  %s", int(window/1024), _streamSock.lastErrorMsg());
+    }
+
+    //log when the size is not expected, users may have to tweak system parameters
+    int actualWindow = _streamSock.getBuffSize(isRecv);
+    if (actualWindow < 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint get socket buffer size failed\n  %s", _streamSock.lastErrorMsg());
+        actualWindow = window;
+    }
+    else if (size_t(actualWindow) < window)
+    {
+        SoapySDR::logf(SOAPY_SDR_WARNING, "StreamEndpoint resize socket buffer: set %d KiB, got %d KiB", int(window/1024), int(actualWindow/1024));
+    }
+
+    //print summary
+    SoapySDR::logf(SOAPY_SDR_INFO, "Configured %s endpoint: dgram=%d bytes, %d elements @ %d bytes, window=%d KiB",
+        isRecv?"receiver":"sender", int(_xferSize), int(_buffSize*_numChans), int(_elemSize), int(actualWindow/1024));
+
+    //calculate flow control window
+    if (isRecv)
+    {
+        //calculate maximum in-flight sequences allowed
+        _maxInFlightSeqs = actualWindow/mtu;
+
+        //calculate the flow control ACK conditions
+        _triggerAckWindow = _maxInFlightSeqs/_numBuffs;
+
+        //send gratuitous ack to set sender's window
+        this->sendACK();
+    }
+    else
+    {
+        //_maxInFlightSeqs set by flow control packet
+    }
+}
+
+SoapyStreamEndpoint::~SoapyStreamEndpoint(void)
+{
+    return;
+}
+
+void SoapyStreamEndpoint::sendACK(void)
+{
+    StreamDatagramHeader header;
+    header.bytes = htonl(sizeof(header));
+    header.sequence = htonl(_lastRecvSequence);
+    header.elems = htonl(_maxInFlightSeqs);
+    header.flags = htonl(0);
+    header.time = htonll(0);
+
+    //send the flow control ACK
+    int ret = _streamSock.send(&header, sizeof(header));
+    if (ret < 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::sendACK(), FAILED %s", _streamSock.lastErrorMsg());
+    }
+    else if (size_t(ret) != sizeof(header))
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::sendACK(%d bytes), FAILED %d", int(sizeof(header)), ret);
+    }
+
+    //update last flow control ACK state
+    _lastSendSequence = _lastRecvSequence;
+}
+
+void SoapyStreamEndpoint::recvACK(void)
+{
+    StreamDatagramHeader header;
+    int ret = _streamSock.recv(&header, sizeof(header));
+    if (ret < 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::recvACK(), FAILED %s", _streamSock.lastErrorMsg());
+    }
+    _receiveInitial = true;
+
+    //check the header
+    size_t bytes = ntohl(header.bytes);
+    if (bytes > size_t(ret))
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::recvACK(%d bytes), FAILED %d", int(bytes), ret);
+    }
+
+    _lastRecvSequence = ntohl(header.sequence);
+    _maxInFlightSeqs = ntohl(header.elems);
+}
+
+/***********************************************************************
+ * receive endpoint implementation
+ **********************************************************************/
+bool SoapyStreamEndpoint::waitRecv(const long timeoutUs)
+{
+    //send gratuitous ack until something is received
+    if (not _receiveInitial) this->sendACK();
+    return _streamSock.selectRecv(timeoutUs);
+}
+
+int SoapyStreamEndpoint::acquireRecv(size_t &handle, const void **buffs, int &flags, long long &timeNs)
+{
+    //no available handles, the user is hoarding them...
+    if (_numHandlesAcquired == _buffData.size())
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::acquireRecv() -- all buffers acquired");
+        return SOAPY_SDR_STREAM_ERROR;
+    }
+
+    //grab the current handle
+    handle = _nextHandleAcquire;
+    auto &data = _buffData[handle];
+
+    //receive into the buffer
+    assert(not _streamSock.null());
+    int ret = _streamSock.recv(data.buff.data(), data.buff.size());
+    if (ret < 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::acquireRecv(), FAILED %s", _streamSock.lastErrorMsg());
+        return SOAPY_SDR_STREAM_ERROR;
+    }
+    _receiveInitial = true;
+
+    //check the header
+    auto header = (const StreamDatagramHeader*)data.buff.data();
+    size_t bytes = ntohl(header->bytes);
+    if (bytes > size_t(ret))
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::acquireRecv(%d bytes), FAILED %d\n"
+            "This MTU setting may be unachievable. Check network configuration.", int(bytes), ret);
+        return SOAPY_SDR_STREAM_ERROR;
+    }
+    const int numElemsOrErr = int(ntohl(header->elems));
+
+    //dropped or out of order packets
+    //TODO return an error code, more than a notification
+    if (uint32_t(_lastRecvSequence) != uint32_t(ntohl(header->sequence)))
+    {
+        SoapySDR::log(SOAPY_SDR_SSI, "S");
+    }
+
+    //update flow control
+    _lastRecvSequence = ntohl(header->sequence)+1;
+
+    //has there been at least trigger window number of sequences since the last ACK?
+    if (uint32_t(_lastRecvSequence-_lastSendSequence) >= _triggerAckWindow)
+    {
+        this->sendACK();
+    }
+
+    //increment for next handle
+    if (numElemsOrErr >= 0)
+    {
+        data.acquired = true;
+        _nextHandleAcquire = (_nextHandleAcquire + 1)%_numBuffs;
+        _numHandlesAcquired++;
+    }
+
+    //set output parameters
+    this->getAddrs(handle, (void **)buffs);
+    flags = ntohl(header->flags);
+    timeNs = ntohll(header->time);
+    return numElemsOrErr;
+}
+
+void SoapyStreamEndpoint::releaseRecv(const size_t handle)
+{
+    auto &data = _buffData[handle];
+    data.acquired = false;
+
+    //actually release in order of handle index
+    while (_numHandlesAcquired != 0)
+    {
+        if (_buffData[_nextHandleRelease].acquired) break;
+        _nextHandleRelease = (_nextHandleRelease + 1)%_numBuffs;
+        _numHandlesAcquired--;
+    }
+}
+
+/***********************************************************************
+ * send endpoint implementation
+ **********************************************************************/
+bool SoapyStreamEndpoint::waitSend(const long timeoutUs)
+{
+    //are we within the allowed number of sequences in flight?
+    while (not _receiveInitial or uint32_t(_lastSendSequence-_lastRecvSequence) >= _maxInFlightSeqs)
+    {
+        //wait for a flow control ACK to arrive
+        if (not _streamSock.selectRecv(timeoutUs)) return false;
+
+        //exhaustive receive without timeout
+        while (_streamSock.selectRecv(0)) this->recvACK();
+    }
+
+    return true;
+}
+
+int SoapyStreamEndpoint::acquireSend(size_t &handle, void **buffs)
+{
+    //no available handles, the user is hoarding them...
+    if (_numHandlesAcquired == _buffData.size())
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::acquireSend() -- all buffers acquired");
+        return SOAPY_SDR_STREAM_ERROR;
+    }
+
+    //grab the current handle
+    handle = _nextHandleAcquire;
+    auto &data = _buffData[handle];
+
+    //increment for next handle
+    data.acquired = true;
+    _nextHandleAcquire = (_nextHandleAcquire + 1)%_numBuffs;
+    _numHandlesAcquired++;
+
+    //set output parameters
+    this->getAddrs(handle, buffs);
+    return int(_buffSize);
+}
+
+void SoapyStreamEndpoint::releaseSend(const size_t handle, const int numElemsOrErr, int &flags, const long long timeNs)
+{
+    auto &data = _buffData[handle];
+    data.acquired = false;
+
+    //The first N-1 channels must be complete buffSize sends
+    //due to the pointer allocation at initialization time.
+    //The last channel can be shortened to the available numElems.
+    const size_t totalElems = ((_numChans-1)*_buffSize) + numElemsOrErr;
+
+    //load the header
+    auto header = (StreamDatagramHeader*)data.buff.data();
+    size_t bytes = HEADER_SIZE + ((numElemsOrErr < 0)?0:(totalElems*_elemSize));
+    header->bytes = htonl(bytes);
+    header->sequence = htonl(_lastSendSequence++);
+    header->elems = htonl(numElemsOrErr);
+    header->flags = htonl(flags);
+    header->time = htonll(timeNs);
+
+    //send from the buffer
+    assert(not _streamSock.null());
+    int ret = _streamSock.send(data.buff.data(), bytes);
+    if (ret < 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::releaseSend(), FAILED %s", _streamSock.lastErrorMsg());
+    }
+    else if (size_t(ret) != bytes)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::releaseSend(%d bytes), FAILED %d", int(bytes), ret);
+    }
+
+    //actually release in order of handle index
+    while (_numHandlesAcquired != 0)
+    {
+        if (_buffData[_nextHandleRelease].acquired) break;
+        _nextHandleRelease = (_nextHandleRelease + 1)%_numBuffs;
+        _numHandlesAcquired--;
+    }
+}
+
+/***********************************************************************
+ * status endpoint implementation -- used by both directions
+ **********************************************************************/
+bool SoapyStreamEndpoint::waitStatus(const long timeoutUs)
+{
+    return _statusSock.selectRecv(timeoutUs);
+}
+
+int SoapyStreamEndpoint::readStatus(size_t &chanMask, int &flags, long long &timeNs)
+{
+    StreamDatagramHeader header;
+    //read the status
+    assert(not _statusSock.null());
+    int ret = _statusSock.recv(&header, sizeof(header));
+    if (ret < 0) return SOAPY_SDR_STREAM_ERROR;
+
+    //check the header
+    size_t bytes = ntohl(header.bytes);
+    if (bytes > size_t(ret))
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::readStatus(%d bytes), FAILED %d", int(bytes), ret);
+        return SOAPY_SDR_STREAM_ERROR;
+    }
+
+    //set output parameters
+    chanMask = ntohl(header.sequence);
+    flags = ntohl(header.flags);
+    timeNs = ntohll(header.time);
+    return int(ntohl(header.elems));
+}
+
+void SoapyStreamEndpoint::writeStatus(const int code, const size_t chanMask, const int flags, const long long timeNs)
+{
+    StreamDatagramHeader header;
+    header.bytes = htonl(sizeof(header));
+    header.sequence = htonl(chanMask);
+    header.flags = htonl(flags);
+    header.time = htonll(timeNs);
+    header.elems = htonl(code);
+
+    //send the status
+    assert(not _statusSock.null());
+    int ret = _statusSock.send(&header, sizeof(header));
+    if (ret < 0)
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::writeStatus(), FAILED %s", _statusSock.lastErrorMsg());
+    }
+    else if (size_t(ret) != sizeof(header))
+    {
+        SoapySDR::logf(SOAPY_SDR_ERROR, "StreamEndpoint::writeStatus(%d bytes), FAILED %d", int(sizeof(header)), ret);
+    }
+}
diff --git a/common/SoapyStreamEndpoint.hpp b/common/SoapyStreamEndpoint.hpp
new file mode 100644
index 0000000..93587ea
--- /dev/null
+++ b/common/SoapyStreamEndpoint.hpp
@@ -0,0 +1,159 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+#include <cstddef>
+#include <vector>
+
+class SoapyRPCSocket;
+
+/*!
+ * The stream endpoint supports a windowed link datagram protocol.
+ * This endpoint can be operated in only one mode: receive or send,
+ * and must be paired with another differently configured endpoint.
+ */
+class SOAPY_REMOTE_API SoapyStreamEndpoint
+{
+public:
+    SoapyStreamEndpoint(
+        SoapyRPCSocket &streamSock,
+        SoapyRPCSocket &statusSock,
+        const bool isRecv,
+        const size_t numChans,
+        const size_t elemSize,
+        const size_t mtu,
+        const size_t window);
+
+    ~SoapyStreamEndpoint(void);
+
+    //! How many channels configured
+    size_t getNumChans(void) const
+    {
+        return _numChans;
+    }
+
+    //! Element size in bytes
+    size_t getElemSize(void) const
+    {
+        return _elemSize;
+    }
+
+    //! Actual buffer size in elements
+    size_t getBuffSize(void) const
+    {
+        return _buffSize;
+    }
+
+    //! Actual number of buffers
+    size_t getNumBuffs(void) const
+    {
+        return _numBuffs;
+    }
+
+    //! Query handle addresses
+    void getAddrs(const size_t handle, void **buffs) const
+    {
+        for (size_t i = 0; i < _numChans; i++)
+        {
+            buffs[i] = _buffData[handle].buffs[i];
+        }
+    }
+
+    /*******************************************************************
+     * receive endpoint API
+     ******************************************************************/
+
+    /*!
+     * Wait for a datagram to arrive at the socket
+     * return true when ready for false for timeout.
+     */
+    bool waitRecv(const long timeoutUs);
+
+    /*!
+     * Acquire a receive buffer with metadata.
+     * return the number of elements or error code
+     */
+    int acquireRecv(size_t &handle, const void **buffs, int &flags, long long &timeNs);
+
+    /*!
+     * Release the buffer when done.
+     */
+    void releaseRecv(const size_t handle);
+
+    /*******************************************************************
+     * send endpoint API
+     ******************************************************************/
+
+    /*!
+     * Wait for the flow control to allow transmission.
+     * return true when ready for false for timeout.
+     */
+    bool waitSend(const long timeoutUs);
+
+    /*!
+     * Acquire a receive buffer with metadata.
+     */
+    int acquireSend(size_t &handle, void **buffs);
+
+    /*!
+     * Release the buffer when done.
+     * pass in the number of elements or error code
+     */
+    void releaseSend(const size_t handle, const int numElemsOrErr, int &flags, const long long timeNs);
+
+    /*******************************************************************
+     * status endpoint API -- used by both directions
+     ******************************************************************/
+
+    /*!
+     * Wait for a status message to arrive
+     */
+    bool waitStatus(const long timeoutUs);
+
+    /*!
+     * Read the stream status data.
+     * Return 0 or error code.
+     */
+    int readStatus(size_t &chanMask, int &flags, long long &timeNs);
+
+    /*!
+     * Write the stream status from the forwarder.
+     */
+    void writeStatus(const int code, const size_t chanMask, const int flags, const long long timeNs);
+
+private:
+    SoapyRPCSocket &_streamSock;
+    SoapyRPCSocket &_statusSock;
+    const size_t _xferSize;
+    const size_t _numChans;
+    const size_t _elemSize;
+    const size_t _buffSize;
+    const size_t _numBuffs;
+
+    struct BufferData
+    {
+        std::vector<char> buff; //actual POD
+        std::vector<void *> buffs; //pointers
+        bool acquired;
+    };
+    std::vector<BufferData> _buffData;
+
+    //acquire+release tracking
+    size_t _nextHandleAcquire;
+    size_t _nextHandleRelease;
+    size_t _numHandlesAcquired;
+
+    //sequence tracking
+    size_t _lastSendSequence;
+    size_t _lastRecvSequence;
+    size_t _maxInFlightSeqs;
+    bool _receiveInitial;
+
+    //how often to send a flow control ACK? (recv only)
+    size_t _triggerAckWindow;
+
+    //flow control helpers
+    void sendACK(void);
+    void recvACK(void);
+};
diff --git a/common/SoapyURLUtils.cpp b/common/SoapyURLUtils.cpp
new file mode 100644
index 0000000..bf45e0c
--- /dev/null
+++ b/common/SoapyURLUtils.cpp
@@ -0,0 +1,217 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapySocketDefs.hpp"
+#include "SoapyURLUtils.hpp"
+#include <cstring> //memset
+#include <string>
+#include <cassert>
+
+SockAddrData::SockAddrData(void)
+{
+    return;
+}
+
+SockAddrData::SockAddrData(const struct sockaddr *addr, const int addrlen)
+{
+    _storage.resize(addrlen);
+    std::memcpy(_storage.data(), addr, addrlen);
+}
+
+const struct sockaddr *SockAddrData::addr(void) const
+{
+    return (const struct sockaddr *)_storage.data();
+}
+
+size_t SockAddrData::addrlen(void) const
+{
+    return _storage.size();
+}
+
+SoapyURL::SoapyURL(void)
+{
+    return;
+}
+
+SoapyURL::SoapyURL(const std::string &scheme, const std::string &node, const std::string &service):
+    _scheme(scheme),
+    _node(node),
+    _service(service)
+{
+    return;
+}
+
+SoapyURL::SoapyURL(const std::string &url)
+{
+    //extract the scheme
+    std::string urlRest = url;
+    auto schemeEnd = url.find("://");
+    if (schemeEnd != std::string::npos)
+    {
+        _scheme = url.substr(0, schemeEnd);
+        urlRest = url.substr(schemeEnd+3);
+    }
+
+    //extract node name and service port
+    bool inBracket = false;
+    bool inService = false;
+    for (size_t i = 0; i < urlRest.size(); i++)
+    {
+        const char ch = urlRest[i];
+        if (inBracket and ch == ']')
+        {
+            inBracket = false;
+            continue;
+        }
+        if (not inBracket and ch == '[')
+        {
+            inBracket = true;
+            continue;
+        }
+        if (inBracket)
+        {
+            _node += ch;
+            continue;
+        }
+        if (inService)
+        {
+            _service += ch;
+            continue;
+        }
+        if (not inService and ch == ':')
+        {
+            inService = true;
+            continue;
+        }
+        if (not inService)
+        {
+            _node += ch;
+            continue;
+        }
+    }
+}
+
+SoapyURL::SoapyURL(const SockAddrData &addr)
+{
+    char *s = NULL;
+    switch(addr.addr()->sa_family)
+    {
+        case AF_INET: {
+            auto *addr_in = (const struct sockaddr_in *)addr.addr();
+            s = (char *)malloc(INET_ADDRSTRLEN);
+            inet_ntop(AF_INET, (void *)&(addr_in->sin_addr), s, INET_ADDRSTRLEN);
+            _node = s;
+            _service = std::to_string(ntohs(addr_in->sin_port));
+            break;
+        }
+        case AF_INET6: {
+            auto *addr_in6 = (const struct sockaddr_in6 *)addr.addr();
+            s = (char *)malloc(INET6_ADDRSTRLEN);
+            inet_ntop(AF_INET6, (void *)&(addr_in6->sin6_addr), s, INET6_ADDRSTRLEN);
+            _node = s;
+            //support scoped address node
+            if (addr_in6->sin6_scope_id != 0)
+            {
+                _node += "%" + std::to_string(addr_in6->sin6_scope_id);
+            }
+            _service = std::to_string(ntohs(addr_in6->sin6_port));
+            break;
+        }
+        default:
+            break;
+    }
+    free(s);
+}
+
+std::string SoapyURL::toSockAddr(SockAddrData &addr) const
+{
+    SockAddrData result;
+
+    //unspecified service, cant continue
+    if (_service.empty()) return "service not specified";
+
+    //configure the hint
+    struct addrinfo hints, *servinfo = NULL;
+    std::memset(&hints, 0, sizeof(hints));
+    hints.ai_family = AF_UNSPEC; // use AF_INET6 to force IPv6
+    hints.ai_socktype = this->getType();
+
+    //get address info
+    int ret = getaddrinfo(_node.c_str(), _service.c_str(), &hints, &servinfo);
+    if (ret != 0) return gai_strerror(ret);
+
+    //iterate through possible matches
+    struct addrinfo *p = NULL;
+    for (p = servinfo; p != NULL; p = p->ai_next)
+    {
+        //eliminate unsupported family types
+        if (p->ai_family != AF_INET and p->ai_family != AF_INET6) continue;
+
+        //found a match
+        assert(p->ai_family == p->ai_addr->sa_family);
+        addr = SockAddrData(p->ai_addr, p->ai_addrlen);
+        break;
+    }
+
+    //cleanup
+    freeaddrinfo(servinfo);
+
+    //no results
+    if (p == NULL) return "no lookup results";
+
+    return ""; //OK
+}
+
+std::string SoapyURL::toString(void) const
+{
+    std::string url;
+
+    //add the scheme
+    if (not _scheme.empty()) url += _scheme + "://";
+
+    //add the node with ipv6 escape brackets
+    if (_node.find(":") != std::string::npos) url += "[" + _node + "]";
+    else url += _node;
+
+    //and the service
+    if (not _service.empty()) url += ":" + _service;
+
+    return url;
+}
+
+std::string SoapyURL::getScheme(void) const
+{
+    return _scheme;
+}
+
+std::string SoapyURL::getNode(void) const
+{
+    return _node;
+}
+
+std::string SoapyURL::getService(void) const
+{
+    return _service;
+}
+
+void SoapyURL::setScheme(const std::string &scheme)
+{
+    _scheme = scheme;
+}
+
+void SoapyURL::setNode(const std::string &node)
+{
+    _node = node;
+}
+
+void SoapyURL::setService(const std::string &service)
+{
+    _service = service;
+}
+
+int SoapyURL::getType(void) const
+{
+    if (_scheme == "tcp") return SOCK_STREAM;
+    if (_scheme == "udp") return SOCK_DGRAM;
+    return SOCK_STREAM; //assume
+}
diff --git a/common/SoapyURLUtils.hpp b/common/SoapyURLUtils.hpp
new file mode 100644
index 0000000..aeeeb57
--- /dev/null
+++ b/common/SoapyURLUtils.hpp
@@ -0,0 +1,87 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRemoteConfig.hpp"
+#include <cstddef>
+#include <string>
+#include <vector>
+
+//forward declares
+struct sockaddr;
+
+//! A simple storage class for a sockaddr
+class SOAPY_REMOTE_API SockAddrData
+{
+public:
+    //! Create an empty socket address
+    SockAddrData(void);
+
+    //! Create a socket address from a pointer and length
+    SockAddrData(const struct sockaddr *addr, const int addrlen);
+
+    //! Get a pointer to the underlying data
+    const struct sockaddr *addr(void) const;
+
+    //! Length of the underlying structure
+    size_t addrlen(void) const;
+
+private:
+    std::vector<char> _storage;
+};
+
+/*!
+ * URL parsing, manipulation, lookup.
+ */
+class SOAPY_REMOTE_API SoapyURL
+{
+public:
+    //! Create empty url object
+    SoapyURL(void);
+
+    //! Create URL from components
+    SoapyURL(const std::string &scheme, const std::string &node, const std::string &service);
+
+    //! Parse from url markup string
+    SoapyURL(const std::string &url);
+
+    //! Create URL from socket address
+    SoapyURL(const SockAddrData &addr);
+
+    /*!
+     * Convert to socket address + resolve address.
+     * Return the error message on failure.
+     */
+    std::string toSockAddr(SockAddrData &addr) const;
+
+    /*!
+     * Convert to URL string markup.
+     */
+    std::string toString(void) const;
+
+    //! Get the scheme
+    std::string getScheme(void) const;
+
+    //! Get the node
+    std::string getNode(void) const;
+
+    //! Get the service
+    std::string getService(void) const;
+
+    //! Set the scheme
+    void setScheme(const std::string &scheme);
+
+    //! Set the node
+    void setNode(const std::string &node);
+
+    //! Set the service
+    void setService(const std::string &service);
+
+    //! Get the socket type from the scheme
+    int getType(void) const;
+
+private:
+    std::string _scheme;
+    std::string _node;
+    std::string _service;
+};
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..c46379c
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,29 @@
+soapyremote (0.3.1) unstable; urgency=low
+
+  * Release 0.3.1 (2016-09-01)
+
+ -- Josh Blum <josh at pothosware.com>  Thu, 01 Sep 2016 21:02:42 -0700
+
+soapyremote (0.3.0) unstable; urgency=low
+
+  * Release 0.3.0 (2016-07-10)
+
+ -- Josh Blum <josh at pothosware.com>  Sun, 10 Jul 2016 16:48:12 -0700
+
+soapyremote (0.2.1) unstable; urgency=low
+
+  * Release 0.2.1 (2016-04-21)
+
+ -- Josh Blum <josh at pothosware.com>  Thu, 21 Apr 2016 14:50:37 -0400
+
+soapyremote (0.2.0) unstable; urgency=low
+
+  * Release 0.2.0 (2015-11-21)
+
+ -- Josh Blum <josh at pothosware.com>  Thu, 15 Oct 2015 19:29:23 -0700
+
+soapyremote (0.1.0) unstable; urgency=low
+
+  * Release 0.1.0 (2015-10-10)
+
+ -- Josh Blum <josh at pothosware.com>  Sat, 10 Oct 2015 11:08:35 -0700
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..16c85cc
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,35 @@
+Source: soapyremote
+Section: libs
+Priority: optional
+Maintainer: Josh Blum <josh at pothosware.com>
+Build-Depends:
+    debhelper (>= 9.0.0),
+    cmake,
+    libsoapysdr-dev (>= 0.3.0)
+Standards-Version: 3.9.8
+Homepage: https://github.com/pothosware/SoapyRemote/wiki
+Vcs-Git: https://github.com/pothosware/SoapyRemote.git
+Vcs-Browser: https://github.com/pothosware/SoapyRemote
+
+Package: soapysdr0.5-2-module-remote
+Architecture: any
+Multi-Arch: same
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Description: Soapy Remote - Remote device support for Soapy SDR.
+ A Soapy module that supports remote devices within the Soapy API.
+
+Package: soapysdr-module-remote
+Architecture: all
+Depends: soapysdr0.5-2-module-remote, ${misc:Depends}
+Description: Soapy Remote - Remote device support for Soapy SDR.
+ A Soapy module that supports remote devices within the Soapy API.
+ .
+ This is an empty dependency package that pulls in the remote module
+ for the default version of libsoapysdr.
+
+Package: soapysdr-server
+Section: libs
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Description: Soapy Remote - Remote device support for Soapy SDR.
+ The SoapySDRServer server application for remote devices.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..372433f
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,32 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: soapyremote
+Source: https://github.com/pothosware/SoapyRemote/wiki
+
+Files: *
+Copyright:
+    Copyright (c) 2015-2016 Josh Blum <josh at pothosware.com>
+    Copyright (c) 2016-2016 Bastille Networks
+License: BSL-1.0
+ Boost Software License - Version 1.0 - August 17th, 2003
+ .
+ Permission is hereby granted, free of charge, to any person or organization
+ obtaining a copy of the software and accompanying documentation covered by
+ this license (the "Software") to use, reproduce, display, distribute,
+ execute, and transmit the Software, and to prepare derivative works of the
+ Software, and to permit third-parties to whom the Software is furnished to
+ do so, all subject to the following:
+ .
+ The copyright notices in the Software and this entire statement, including
+ the above license grant, this restriction and the following disclaimer,
+ must be included in all copies of the Software, in whole or in part, and
+ all derivative works of the Software, unless such copies or derivative
+ works are solely in the form of machine-executable object code generated by
+ a source language processor.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
diff --git a/debian/docs b/debian/docs
new file mode 100644
index 0000000..b43bf86
--- /dev/null
+++ b/debian/docs
@@ -0,0 +1 @@
+README.md
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..6eb429c
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,17 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+
+DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
+export DEB_HOST_MULTIARCH
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+%:
+	dh $@ --buildsystem=cmake --parallel
+
+override_dh_auto_configure:
+	dh_auto_configure -- -DLIB_SUFFIX="/$(DEB_HOST_MULTIARCH)"
+
+override_dh_installchangelogs:
+	dh_installchangelogs Changelog.txt
diff --git a/debian/soapysdr-server.install b/debian/soapysdr-server.install
new file mode 100644
index 0000000..c703cf8
--- /dev/null
+++ b/debian/soapysdr-server.install
@@ -0,0 +1 @@
+usr/bin/
diff --git a/debian/soapysdr0.5-2-module-remote.install b/debian/soapysdr0.5-2-module-remote.install
new file mode 100644
index 0000000..56a9b13
--- /dev/null
+++ b/debian/soapysdr0.5-2-module-remote.install
@@ -0,0 +1 @@
+usr/lib/*/SoapySDR/modules*
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt
new file mode 100644
index 0000000..4ad4bcd
--- /dev/null
+++ b/server/CMakeLists.txt
@@ -0,0 +1,36 @@
+########################################################################
+# Thread config support
+########################################################################
+find_library(
+    RT_LIBRARIES
+    NAMES rt
+    PATHS /usr/lib /usr/lib64
+)
+
+if (RT_LIBRARIES)
+    list(APPEND SoapySDR_LIBRARIES ${RT_LIBRARIES})
+endif()
+
+if(WIN32)
+    list(APPEND SOAPY_SERVER_SOURCES ThreadPrioWindows.cpp)
+elseif(UNIX)
+    list(APPEND SOAPY_SERVER_SOURCES ThreadPrioUnix.cpp)
+endif()
+
+########################################################################
+# Build the remote server application
+########################################################################
+list(APPEND SOAPY_SERVER_SOURCES
+    SoapyServer.cpp
+    ServerListener.cpp
+    ClientHandler.cpp
+    LogForwarding.cpp
+    ServerStreamData.cpp
+)
+if (MSVC)
+    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/msvc)
+endif ()
+include_directories(${SoapySDR_INCLUDE_DIRS})
+add_executable(SoapySDRServer ${SOAPY_SERVER_SOURCES})
+target_link_libraries(SoapySDRServer ${SoapySDR_LIBRARIES} SoapySDRRemoteCommon)
+install(TARGETS SoapySDRServer DESTINATION bin)
diff --git a/server/ClientHandler.cpp b/server/ClientHandler.cpp
new file mode 100644
index 0000000..f5e0a11
--- /dev/null
+++ b/server/ClientHandler.cpp
@@ -0,0 +1,1358 @@
+// Copyright (c) 2015-2016 Josh Blum
+// Copyright (c) 2016-2016 Bastille Networks
+// SPDX-License-Identifier: BSL-1.0
+
+#include "ClientHandler.hpp"
+#include "ServerStreamData.hpp"
+#include "LogForwarding.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyURLUtils.hpp"
+#include "SoapyRPCSocket.hpp"
+#include "SoapyRPCPacker.hpp"
+#include "SoapyRPCUnpacker.hpp"
+#include "SoapyStreamEndpoint.hpp"
+#include <SoapySDR/Device.hpp>
+#include <SoapySDR/Logger.hpp>
+#include <SoapySDR/Formats.hpp>
+#include <SoapySDR/Version.hpp>
+#include <iostream>
+#include <mutex>
+
+//! The device factory make and unmake requires a process-wide mutex
+static std::mutex factoryMutex;
+
+/***********************************************************************
+ * Client handler constructor
+ **********************************************************************/
+SoapyClientHandler::SoapyClientHandler(SoapyRPCSocket &sock, const std::string &uuid):
+    _sock(sock),
+    _uuid(uuid),
+    _dev(nullptr),
+    _logForwarder(nullptr),
+    _nextStreamId(0)
+{
+    return;
+}
+
+SoapyClientHandler::~SoapyClientHandler(void)
+{
+    //stop all stream threads and close streams
+    for (auto &data : _streamData)
+    {
+        data.second.stopThreads();
+        _dev->closeStream(data.second.stream);
+    }
+
+    //release the device handle if we have it
+    if (_dev != nullptr)
+    {
+        std::lock_guard<std::mutex> lock(factoryMutex);
+        SoapySDR::Device::unmake(_dev);
+        _dev = nullptr;
+    }
+
+    //finally stop and cleanup log forwarding
+    delete _logForwarder;
+}
+
+/***********************************************************************
+ * Transaction handler
+ **********************************************************************/
+bool SoapyClientHandler::handleOnce(void)
+{
+    if (not _sock.selectRecv(SOAPY_REMOTE_SOCKET_TIMEOUT_US)) return true;
+
+    //receive the client's request
+    SoapyRPCUnpacker unpacker(_sock);
+    SoapyRPCPacker packer(_sock);
+
+    //handle the client's request
+    bool again = true;
+    try
+    {
+        again = this->handleOnce(unpacker, packer);
+    }
+    catch (const std::exception &ex)
+    {
+        packer & ex;
+    }
+
+    //send the result back
+    packer();
+
+    return again;
+}
+
+/***********************************************************************
+ * Handler dispatcher implementation
+ **********************************************************************/
+bool SoapyClientHandler::handleOnce(SoapyRPCUnpacker &unpacker, SoapyRPCPacker &packer)
+{
+    SoapyRemoteCalls call;
+    unpacker & call;
+
+    switch (call)
+    {
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_FIND:
+    ////////////////////////////////////////////////////////////////////
+    {
+        SoapySDR::Kwargs args;
+        unpacker & args;
+        packer & SoapySDR::Device::enumerate(args);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_MAKE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        SoapySDR::Kwargs args;
+        unpacker & args;
+        std::lock_guard<std::mutex> lock(factoryMutex);
+        if (_dev == nullptr) _dev = SoapySDR::Device::make(args);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_UNMAKE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        //stop all stream threads and close streams
+        if (not _streamData.empty())
+        {
+            SoapySDR::log(SOAPY_SDR_WARNING, "Performing automatic closeStream() before Device unmake.");
+        }
+        for (auto &data : _streamData)
+        {
+            data.second.stopThreads();
+            _dev->closeStream(data.second.stream);
+        }
+        _streamData.clear();
+
+        std::lock_guard<std::mutex> lock(factoryMutex);
+        if (_dev != nullptr) SoapySDR::Device::unmake(_dev);
+        _dev = nullptr;
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_HANGUP:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_SERVER_ID:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _uuid;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_START_LOG_FORWARDING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        if (_logForwarder == nullptr) _logForwarder = new SoapyLogForwarder(_sock);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_STOP_LOG_FORWARDING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        if (_logForwarder != nullptr) delete _logForwarder;
+        _logForwarder = nullptr;
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_DRIVER_KEY:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getDriverKey();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_HARDWARE_KEY:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getHardwareKey();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_HARDWARE_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getHardwareInfo();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_FRONTEND_MAPPING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        std::string mapping;
+        unpacker & direction;
+        unpacker & mapping;
+        _dev->setFrontendMapping(direction, mapping);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_FRONTEND_MAPPING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        unpacker & direction;
+        packer & _dev->getFrontendMapping(direction);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_NUM_CHANNELS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        unpacker & direction;
+        packer & int(_dev->getNumChannels(direction));
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_FULL_DUPLEX:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getFullDuplex(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_CHANNEL_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        #ifdef SOAPY_SDR_API_HAS_GET_CHANNEL_INFO
+        packer & _dev->getChannelInfo(direction, channel);
+        #else
+        SoapySDR::Kwargs result;
+        packer & result;
+        #endif
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_STREAM_FORMATS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getStreamFormats(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_NATIVE_STREAM_FORMAT:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+
+        double fullScale = 0.0;
+        packer & _dev->getNativeStreamFormat(direction, channel, fullScale);
+        packer & fullScale;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_STREAM_ARGS_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getStreamArgsInfo(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SETUP_STREAM:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        std::string format;
+        std::vector<size_t> channels;
+        SoapySDR::Kwargs args;
+        std::string clientBindPort;
+        std::string statusBindPort;
+        unpacker & direction;
+        unpacker & format;
+        unpacker & channels;
+        unpacker & args;
+        unpacker & clientBindPort;
+        unpacker & statusBindPort;
+
+        //parse args for buffer configuration
+        size_t mtu = SOAPY_REMOTE_DEFAULT_ENDPOINT_MTU;
+        const auto mtuIt = args.find(SOAPY_REMOTE_KWARG_MTU);
+        if (mtuIt != args.end()) mtu = size_t(std::stod(mtuIt->second));
+
+        size_t window = SOAPY_REMOTE_DEFAULT_ENDPOINT_WINDOW;
+        const auto windowIt = args.find(SOAPY_REMOTE_KWARG_WINDOW);
+        if (windowIt != args.end()) window = size_t(std::stod(windowIt->second));
+
+        double priority = SOAPY_REMOTE_DEFAULT_THREAD_PRIORITY;
+        const auto priorityIt = args.find(SOAPY_REMOTE_KWARG_PRIORITY);
+        if (priorityIt != args.end()) priority = std::stod(priorityIt->second);
+
+        //create stream
+        auto stream = _dev->setupStream(direction, format, channels, args);
+
+        //load data structure
+        auto &data = _streamData[_nextStreamId];
+        data.streamId = _nextStreamId++;
+        data.device = _dev;
+        data.stream = stream;
+        data.format = format;
+        for (const auto chan : channels) data.chanMask |= (1 << chan);
+        data.priority = priority;
+
+        //extract socket node information
+        const auto localNode = SoapyURL(_sock.getsockname()).getNode();
+        const auto remoteNode = SoapyURL(_sock.getpeername()).getNode();
+
+        //bind the stream socket to an automatic port
+        const auto bindURL = SoapyURL("udp", localNode, "0").toString();
+        int ret = data.streamSock.bind(bindURL);
+        if (ret != 0)
+        {
+            const std::string errorMsg = data.streamSock.lastErrorMsg();
+            _streamData.erase(data.streamId);
+            throw std::runtime_error("SoapyRemote::setupStream("+bindURL+") -- bind FAIL: " + errorMsg);
+        }
+        SoapySDR::logf(SOAPY_SDR_INFO, "Server side stream bound to %s", data.streamSock.getsockname().c_str());
+        const auto serverBindPort = SoapyURL(data.streamSock.getsockname()).getService();
+
+        //connect the stream socket to the specified port
+        auto connectURL = SoapyURL("udp", remoteNode, clientBindPort).toString();
+        ret = data.streamSock.connect(connectURL);
+        if (ret != 0)
+        {
+            const std::string errorMsg = data.streamSock.lastErrorMsg();
+            _streamData.erase(data.streamId);
+            throw std::runtime_error("SoapyRemote::setupStream("+connectURL+") -- connect FAIL: " + errorMsg);
+        }
+        SoapySDR::logf(SOAPY_SDR_INFO, "Server side stream connected to %s", data.streamSock.getpeername().c_str());
+
+        //connect the status socket to the specified port
+        connectURL = SoapyURL("udp", remoteNode, statusBindPort).toString();
+        ret = data.statusSock.connect(connectURL);
+        if (ret != 0)
+        {
+            const std::string errorMsg = data.statusSock.lastErrorMsg();
+            _streamData.erase(data.streamId);
+            throw std::runtime_error("SoapyRemote::setupStream("+connectURL+") -- connect FAIL: " + errorMsg);
+        }
+        SoapySDR::logf(SOAPY_SDR_INFO, "Server side status connected to %s", data.statusSock.getpeername().c_str());
+
+        //create endpoint
+        data.endpoint = new SoapyStreamEndpoint(data.streamSock, data.statusSock,
+            direction == SOAPY_SDR_TX, channels.size(), SoapySDR::formatToSize(format), mtu, window);
+
+        //start worker thread, this is not backwards,
+        //receive from device means using a send endpoint
+        //transmit to device means using a recv endpoint
+        if (direction == SOAPY_SDR_RX) data.startSendThread();
+        if (direction == SOAPY_SDR_TX) data.startRecvThread();
+        data.startStatThread();
+
+        packer & data.streamId;
+        packer & serverBindPort;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_CLOSE_STREAM:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int streamId = 0;
+        unpacker & streamId;
+
+        //cleanup data and stop worker thread
+        auto &data = _streamData.at(streamId);
+        data.stopThreads();
+        _dev->closeStream(data.stream);
+        _streamData.erase(streamId);
+
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_ACTIVATE_STREAM:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int streamId = 0;
+        int flags = 0;
+        long long timeNs = 0;
+        int numElems = 0;
+        unpacker & streamId;
+        unpacker & flags;
+        unpacker & timeNs;
+        unpacker & numElems;
+
+        auto &data = _streamData.at(streamId);
+        packer & _dev->activateStream(data.stream, flags, timeNs, size_t(numElems));
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_DEACTIVATE_STREAM:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int streamId = 0;
+        int flags = 0;
+        long long timeNs = 0;
+        unpacker & streamId;
+        unpacker & flags;
+        unpacker & timeNs;
+
+        auto &data = _streamData.at(streamId);
+        packer & _dev->deactivateStream(data.stream, flags, timeNs);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_ANTENNAS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->listAntennas(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_ANTENNA:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        _dev->setAntenna(direction, channel, name);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_ANTENNA:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getAntenna(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_HAS_DC_OFFSET_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->hasDCOffsetMode(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_DC_OFFSET_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        bool automatic = false;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & automatic;
+        _dev->setDCOffsetMode(direction, channel, automatic);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_DC_OFFSET_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getDCOffsetMode(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_HAS_DC_OFFSET:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->hasDCOffset(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_DC_OFFSET:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::complex<double> offset;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & offset;
+        _dev->setDCOffset(direction, channel, offset);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_DC_OFFSET:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getDCOffset(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_HAS_IQ_BALANCE_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->hasIQBalance(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_IQ_BALANCE_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::complex<double> balance;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & balance;
+        _dev->setIQBalance(direction, channel, balance);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_IQ_BALANCE_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getIQBalance(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_GAINS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->listGains(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_HAS_GAIN_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->hasGainMode(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_GAIN_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        bool automatic = false;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & automatic;
+        _dev->setGainMode(direction, channel, automatic);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_GAIN_MODE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getGainMode(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_GAIN:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        double value = 0;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & value;
+        _dev->setGain(direction, channel, value);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_GAIN_ELEMENT:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        double value = 0;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        unpacker & value;
+        _dev->setGain(direction, channel, name, value);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_GAIN:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getGain(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_GAIN_ELEMENT:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        packer & _dev->getGain(direction, channel, name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_GAIN_RANGE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getGainRange(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_GAIN_RANGE_ELEMENT:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        packer & _dev->getGainRange(direction, channel, name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_FREQUENCY:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        double value = 0;
+        SoapySDR::Kwargs args;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & value;
+        unpacker & args;
+        _dev->setFrequency(direction, channel, value, args);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_FREQUENCY_COMPONENT:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        double value = 0;
+        SoapySDR::Kwargs args;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        unpacker & value;
+        unpacker & args;
+        _dev->setFrequency(direction, channel, name, value, args);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_FREQUENCY:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getFrequency(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_FREQUENCY_COMPONENT:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        packer & _dev->getFrequency(direction, channel, name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_FREQUENCIES:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->listFrequencies(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_FREQUENCY_RANGE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getFrequencyRange(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_FREQUENCY_RANGE_COMPONENT:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        packer & _dev->getFrequencyRange(direction, channel, name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_FREQUENCY_ARGS_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getFrequencyArgsInfo(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_SAMPLE_RATE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        double rate = 0;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & rate;
+        _dev->setSampleRate(direction, channel, rate);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_SAMPLE_RATE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getSampleRate(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_SAMPLE_RATES:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->listSampleRates(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_BANDWIDTH:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        double bw = 0;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & bw;
+        _dev->setBandwidth(direction, channel, bw);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_BANDWIDTH:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->getBandwidth(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_BANDWIDTHS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->listBandwidths(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_BANDWIDTH_RANGE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        #ifdef SOAPY_SDR_API_HAS_GET_BANDWIDTH_RANGE
+        packer & _dev->getBandwidthRange(direction, channel);
+        #else
+        SoapySDR::RangeList result;
+        packer & result;
+        #endif
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_MASTER_CLOCK_RATE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        double rate = 0;
+        unpacker & rate;
+        _dev->setMasterClockRate(rate);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_MASTER_CLOCK_RATE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getMasterClockRate();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_MASTER_CLOCK_RATES:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getMasterClockRates();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_CLOCK_SOURCES:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->listClockSources();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_CLOCK_SOURCE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string source;
+        unpacker & source;
+        _dev->setClockSource(source);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_CLOCK_SOURCE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getClockSource();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_TIME_SOURCES:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->listTimeSources();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_TIME_SOURCE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string source;
+        unpacker & source;
+        _dev->setTimeSource(source);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_TIME_SOURCE:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getTimeSource();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_HAS_HARDWARE_TIME:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string what;
+        unpacker & what;
+        packer & _dev->hasHardwareTime(what);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_HARDWARE_TIME:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string what;
+        unpacker & what;
+        packer & _dev->getHardwareTime(what);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_HARDWARE_TIME:
+    ////////////////////////////////////////////////////////////////////
+    {
+        long long timeNs = 0;
+        std::string what;
+        unpacker & timeNs;
+        unpacker & what;
+        _dev->setHardwareTime(timeNs, what);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_SET_COMMAND_TIME:
+    ////////////////////////////////////////////////////////////////////
+    {
+        long long timeNs = 0;
+        std::string what;
+        unpacker & timeNs;
+        unpacker & what;
+        _dev->setCommandTime(timeNs, what);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_SENSORS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->listSensors();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_SENSOR_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string name;
+        unpacker & name;
+        packer & _dev->getSensorInfo(name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_SENSOR:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string name;
+        unpacker & name;
+        packer & _dev->readSensor(name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_CHANNEL_SENSORS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        packer & _dev->listSensors(direction, channel);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_CHANNEL_SENSOR_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        packer & _dev->getSensorInfo(direction, channel, name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_CHANNEL_SENSOR:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string name;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & name;
+        packer & _dev->readSensor(direction, channel, name);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_REGISTER:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int addr = 0;
+        int value = 0;
+        unpacker & addr;
+        unpacker & value;
+        _dev->writeRegister(unsigned(addr), unsigned(value));
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_REGISTER:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int addr = 0;
+        unpacker & addr;
+        packer & int(_dev->readRegister(unsigned(addr)));
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_REGISTER_INTERFACES:
+    ////////////////////////////////////////////////////////////////////
+    {
+        #ifdef SOAPY_SDR_API_HAS_NAMED_REGISTER_API
+        packer & _dev->listRegisterInterfaces();
+        #else
+        std::vector<std::string> result;
+        packer & result;
+        #endif
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_REGISTER_NAMED:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string name;
+        int addr = 0;
+        int value = 0;
+        unpacker & name;
+        unpacker & addr;
+        unpacker & value;
+        #ifdef SOAPY_SDR_API_HAS_NAMED_REGISTER_API
+        _dev->writeRegister(name, unsigned(addr), unsigned(value));
+        #endif
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_REGISTER_NAMED:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string name;
+        int addr = 0;
+        unpacker & name;
+        unpacker & addr;
+        #ifdef SOAPY_SDR_API_HAS_NAMED_REGISTER_API
+        packer & int(_dev->readRegister(name, unsigned(addr)));
+        #else
+        int result = 0;
+        packer & result;
+        #endif
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_SETTING_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->getSettingInfo();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_SETTING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string key;
+        std::string value;
+        unpacker & key;
+        unpacker & value;
+        _dev->writeSetting(key, value);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_SETTING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string key;
+        unpacker & key;
+        packer & _dev->readSetting(key);
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_GET_CHANNEL_SETTING_INFO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        unpacker & direction;
+        unpacker & channel;
+        #ifdef SOAPY_SDR_API_HAS_CHANNEL_SETTINGS
+        packer & _dev->getSettingInfo(direction, channel);
+        #else
+        SoapySDR::ArgInfoList info;
+        packer & info;
+        #endif
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_CHANNEL_SETTING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string key;
+        std::string value;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & key;
+        unpacker & value;
+        #ifdef SOAPY_SDR_API_HAS_CHANNEL_SETTINGS
+        _dev->writeSetting(direction, channel, key, value);
+        #endif
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_CHANNEL_SETTING:
+    ////////////////////////////////////////////////////////////////////
+    {
+        char direction = 0;
+        int channel = 0;
+        std::string key;
+        unpacker & direction;
+        unpacker & channel;
+        unpacker & key;
+        #ifdef SOAPY_SDR_API_HAS_CHANNEL_SETTINGS
+        packer & _dev->readSetting(direction, channel, key);
+        #else
+        std::string value;
+        packer & value;
+        #endif
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_GPIO_BANKS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->listGPIOBanks();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_GPIO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string bank;
+        int value = 0;
+        unpacker & bank;
+        unpacker & value;
+        _dev->writeGPIO(bank, unsigned(value));
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_GPIO_MASKED:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string bank;
+        int value = 0;
+        int mask = 0;
+        unpacker & bank;
+        unpacker & value;
+        unpacker & mask;
+        _dev->writeGPIO(bank, unsigned(value), unsigned(mask));
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_GPIO:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string bank;
+        unpacker & bank;
+        packer & int(_dev->readGPIO(bank));
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_GPIO_DIR:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string bank;
+        int dir = 0;
+        unpacker & bank;
+        unpacker & dir;
+        _dev->writeGPIODir(bank, unsigned(dir));
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_GPIO_DIR_MASKED:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string bank;
+        int dir = 0;
+        int mask = 0;
+        unpacker & bank;
+        unpacker & dir;
+        unpacker & mask;
+        _dev->writeGPIODir(bank, unsigned(dir), unsigned(mask));
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_GPIO_DIR:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string bank;
+        unpacker & bank;
+        packer & int(_dev->readGPIODir(bank));
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_I2C:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int addr = 0;
+        std::string data;
+        unpacker & addr;
+        unpacker & data;
+        _dev->writeI2C(addr, data);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_I2C:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int addr = 0;
+        int numBytes = 0;
+        unpacker & addr;
+        unpacker & numBytes;
+        packer & _dev->readI2C(addr, unsigned(numBytes));
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_TRANSACT_SPI:
+    ////////////////////////////////////////////////////////////////////
+    {
+        int addr = 0;
+        int data = 0;
+        int numBits = 0;
+        unpacker & addr;
+        unpacker & data;
+        unpacker & numBits;
+        packer & int(_dev->transactSPI(addr, unsigned(data), size_t(numBits)));
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_LIST_UARTS:
+    ////////////////////////////////////////////////////////////////////
+    {
+        packer & _dev->listUARTs();
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_WRITE_UART:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string which;
+        std::string data;
+        unpacker & which;
+        unpacker & data;
+        _dev->writeUART(which, data);
+        packer & SOAPY_REMOTE_VOID;
+    } break;
+
+    ////////////////////////////////////////////////////////////////////
+    case SOAPY_REMOTE_READ_UART:
+    ////////////////////////////////////////////////////////////////////
+    {
+        std::string which;
+        int timeoutUs = 0;
+        unpacker & which;
+        unpacker & timeoutUs;
+        packer & _dev->readUART(which, long(timeoutUs));
+    } break;
+
+    default: throw std::runtime_error(
+        "SoapyClientHandler::handleOnce("+std::to_string(int(call))+") unknown call");
+    }
+
+    return call != SOAPY_REMOTE_HANGUP;
+}
diff --git a/server/ClientHandler.hpp b/server/ClientHandler.hpp
new file mode 100644
index 0000000..eb952f5
--- /dev/null
+++ b/server/ClientHandler.hpp
@@ -0,0 +1,44 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include <cstddef>
+#include <string>
+#include <map>
+
+class SoapyRPCSocket;
+class SoapyRPCPacker;
+class SoapyRPCUnpacker;
+class SoapyLogForwarder;
+class ServerStreamData;
+
+namespace SoapySDR
+{
+    class Device;
+}
+
+/*!
+ * The client handler manages a remote client.
+ */
+class SoapyClientHandler
+{
+public:
+    SoapyClientHandler(SoapyRPCSocket &sock, const std::string &uuid);
+
+    ~SoapyClientHandler(void);
+
+    //handle once, return true to call again
+    bool handleOnce(void);
+
+private:
+    bool handleOnce(SoapyRPCUnpacker &unpacker, SoapyRPCPacker &packer);
+
+    SoapyRPCSocket &_sock;
+    const std::string _uuid;
+    SoapySDR::Device *_dev;
+    SoapyLogForwarder *_logForwarder;
+
+    //stream tracking
+    int _nextStreamId;
+    std::map<int, ServerStreamData> _streamData;
+};
diff --git a/server/LogForwarding.cpp b/server/LogForwarding.cpp
new file mode 100644
index 0000000..71dc453
--- /dev/null
+++ b/server/LogForwarding.cpp
@@ -0,0 +1,59 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "LogForwarding.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyRPCPacker.hpp"
+#include <SoapySDR/Logger.hpp>
+#include <mutex>
+#include <set>
+
+/***********************************************************************
+ * socket subscribers for log forwarding
+ **********************************************************************/
+static std::mutex subscribersMutex;
+
+static std::set<SoapyRPCSocket *> subscribers;
+
+/***********************************************************************
+ * custom log handling
+ **********************************************************************/
+static void handleLogMessage(const SoapySDRLogLevel logLevel, const char *message)
+{
+    std::string strMessage(message);
+
+    std::lock_guard<std::mutex> lock(subscribersMutex);
+    for (auto sock : subscribers)
+    {
+        try
+        {
+            SoapyRPCPacker packer(*sock);
+            packer & char(logLevel);
+            packer & strMessage;
+            packer();
+        }
+        catch (...)
+        {
+            //ignored
+        }
+    }
+}
+
+/***********************************************************************
+ * subscriber reregistration entry points
+ **********************************************************************/
+SoapyLogForwarder::SoapyLogForwarder(SoapyRPCSocket &sock):
+    _sock(sock)
+{
+    std::lock_guard<std::mutex> lock(subscribersMutex);
+    subscribers.insert(&_sock);
+
+    //register the log handler, its safe to re-register every time
+    SoapySDR::registerLogHandler(&handleLogMessage);
+}
+
+SoapyLogForwarder::~SoapyLogForwarder(void)
+{
+    std::lock_guard<std::mutex> lock(subscribersMutex);
+    subscribers.erase(&_sock);
+}
diff --git a/server/LogForwarding.hpp b/server/LogForwarding.hpp
new file mode 100644
index 0000000..8d977f6
--- /dev/null
+++ b/server/LogForwarding.hpp
@@ -0,0 +1,18 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRPCSocket.hpp"
+
+/*!
+ * Create a log forwarder to subscribe to log events from the local logger.
+ */
+class SoapyLogForwarder
+{
+public:
+    SoapyLogForwarder(SoapyRPCSocket &sock);
+    ~SoapyLogForwarder(void);
+
+private:
+    SoapyRPCSocket &_sock;
+};
diff --git a/server/ServerListener.cpp b/server/ServerListener.cpp
new file mode 100644
index 0000000..16fd148
--- /dev/null
+++ b/server/ServerListener.cpp
@@ -0,0 +1,108 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapyServer.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "ClientHandler.hpp"
+#include "SoapyRPCSocket.hpp"
+#include <thread>
+#include <iostream>
+
+/***********************************************************************
+ * Server thread implementation
+ **********************************************************************/
+SoapyServerThreadData::SoapyServerThreadData(void):
+    done(false),
+    thread(nullptr),
+    client(nullptr)
+{
+    return;
+}
+
+SoapyServerThreadData::~SoapyServerThreadData(void)
+{
+    done = true;
+    if (thread != nullptr)
+    {
+        thread->join();
+    }
+    delete thread;
+    if (client != nullptr)
+    {
+        std::cout << "SoapyServerListener::close()" << std::endl;
+    }
+    delete client;
+}
+
+void SoapyServerThreadData::handlerLoop(void)
+{
+    SoapyClientHandler handler(*client, uuid);
+
+    try
+    {
+        while (handler.handleOnce())
+        {
+            if (done) break;
+        }
+    }
+    catch (const std::exception &ex)
+    {
+        std::cerr << "SoapyServerListener::handlerLoop() FAIL: " << ex.what() << std::endl;
+    }
+
+    done = true;
+}
+
+/***********************************************************************
+ * Socket listener constructor
+ **********************************************************************/
+SoapyServerListener::SoapyServerListener(SoapyRPCSocket &sock, const std::string &uuid):
+    _sock(sock),
+    _uuid(uuid),
+    _handlerId(0)
+{
+    return;
+}
+
+SoapyServerListener::~SoapyServerListener(void)
+{
+    auto it = _handlers.begin();
+    while (it != _handlers.end())
+    {
+        _handlers.erase(it++);
+    }
+}
+
+/***********************************************************************
+ * Client socket acceptor
+ **********************************************************************/
+void SoapyServerListener::handleOnce(void)
+{
+    //cleanup completed threads
+    auto it = _handlers.begin();
+    while (it != _handlers.end())
+    {
+        auto &data = it->second;
+        if (not data.done) ++it;
+        else _handlers.erase(it++);
+    }
+
+    //wait with timeout for the server socket to become ready to accept
+    if (not _sock.selectRecv(SOAPY_REMOTE_SOCKET_TIMEOUT_US)) return;
+
+    SoapyRPCSocket *client = _sock.accept();
+    if (client == NULL)
+    {
+        std::cerr << "SoapyServerListener::accept() FAIL:" << _sock.lastErrorMsg() << std::endl;
+        return;
+    }
+    std::cout << "SoapyServerListener::accept(" << client->getpeername() << ")" << std::endl;
+
+    //setup the thread data
+    auto &data = _handlers[_handlerId++];
+    data.client = client;
+    data.uuid = _uuid;
+
+    //spawn a new thread
+    data.thread = new std::thread(&SoapyServerThreadData::handlerLoop, &data);
+}
diff --git a/server/ServerStreamData.cpp b/server/ServerStreamData.cpp
new file mode 100644
index 0000000..a44e600
--- /dev/null
+++ b/server/ServerStreamData.cpp
@@ -0,0 +1,223 @@
+// Copyright (c) 2015-2015 Josh Blum
+// Copyright (c) 2016-2016 Bastille Networks
+// SPDX-License-Identifier: BSL-1.0
+
+#include "ServerStreamData.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyStreamEndpoint.hpp"
+#include <SoapySDR/Device.hpp>
+#include <SoapySDR/Logger.hpp>
+#include <algorithm> //min
+#include <thread>
+#include <vector>
+#include <cassert>
+
+template <typename T>
+void incrementBuffs(std::vector<T> &buffs, size_t numElems, size_t elemSize)
+{
+    for (auto &buff : buffs)
+    {
+        buff = T(size_t(buff) + (numElems*elemSize));
+    }
+}
+
+ServerStreamData::ServerStreamData(void):
+    device(nullptr),
+    stream(nullptr),
+    chanMask(0),
+    priority(0.0),
+    streamId(-1),
+    endpoint(nullptr),
+    streamThread(nullptr),
+    statusThread(nullptr),
+    done(true)
+{
+    return;
+}
+
+void ServerStreamData::startSendThread(void)
+{
+    assert(streamId != -1);
+    done = false;
+    streamThread = new std::thread(&ServerStreamData::sendEndpointWork, this);
+}
+
+void ServerStreamData::startRecvThread(void)
+{
+    assert(streamId != -1);
+    done = false;
+    streamThread = new std::thread(&ServerStreamData::recvEndpointWork, this);
+}
+
+void ServerStreamData::startStatThread(void)
+{
+    assert(streamId != -1);
+    done = false;
+    statusThread = new std::thread(&ServerStreamData::statEndpointWork, this);
+}
+
+void ServerStreamData::stopThreads(void)
+{
+    done = true;
+    assert(streamThread != nullptr);
+    assert(statusThread != nullptr);
+    streamThread->join();
+    statusThread->join();
+    delete streamThread;
+    delete statusThread;
+}
+
+static void setThreadPrioWithLogging(const double priority)
+{
+    const auto errorMsg = setThreadPrio(priority);
+    if (not errorMsg.empty()) SoapySDR::logf(SOAPY_SDR_WARNING,
+        "Set thread priority %g failed: %s", priority, errorMsg.c_str());
+}
+
+void ServerStreamData::recvEndpointWork(void)
+{
+    setThreadPrioWithLogging(priority);
+    assert(endpoint != nullptr);
+    assert(endpoint->getElemSize() != 0);
+    assert(endpoint->getNumChans() != 0);
+
+    //setup worker data structures
+    int ret = 0;
+    size_t handle = 0;
+    int flags = 0;
+    long long timeNs = 0;
+    const auto elemSize = endpoint->getElemSize();
+    std::vector<const void *> buffs(endpoint->getNumChans());
+
+    //loop forever until signaled done
+    //1) wait on the endpoint to become ready
+    //2) acquire the recv buffer from the endpoint
+    //3) write to the device stream from the endpoint buffer
+    //4) release the buffer back to the endpoint
+    while (not done)
+    {
+        if (not endpoint->waitRecv(SOAPY_REMOTE_SOCKET_TIMEOUT_US)) continue;
+        ret = endpoint->acquireRecv(handle, buffs.data(), flags, timeNs);
+        if (ret < 0)
+        {
+            SoapySDR::logf(SOAPY_SDR_ERROR, "Server-side receive endpoint: %s; worker quitting...", streamSock.lastErrorMsg());
+            return;
+        }
+
+        //loop to write to device
+        size_t elemsLeft = size_t(ret);
+        while (not done)
+        {
+            ret = device->writeStream(stream, buffs.data(), elemsLeft, flags, timeNs, SOAPY_REMOTE_SOCKET_TIMEOUT_US);
+            if (ret == SOAPY_SDR_TIMEOUT) continue;
+            if (ret < 0)
+            {
+                endpoint->writeStatus(ret, chanMask, flags, timeNs);
+                break; //discard after error, this may have been invalid flags or time
+            }
+            elemsLeft -= ret;
+            incrementBuffs(buffs, ret, elemSize);
+            if (elemsLeft == 0) break;
+            flags &= ~(SOAPY_SDR_HAS_TIME); //clear time for subsequent writes
+        }
+
+        //release the buffer back to the endpoint
+        endpoint->releaseRecv(handle);
+    }
+}
+
+void ServerStreamData::sendEndpointWork(void)
+{
+    setThreadPrioWithLogging(priority);
+    assert(endpoint != nullptr);
+    assert(endpoint->getElemSize() != 0);
+    assert(endpoint->getNumChans() != 0);
+
+    //setup worker data structures
+    int ret = 0;
+    size_t handle = 0;
+    int flags = 0;
+    long long timeNs = 0;
+    const auto elemSize = endpoint->getElemSize();
+    std::vector<void *> buffs(endpoint->getNumChans());
+    const size_t mtuElems = device->getStreamMTU(stream);
+
+    //loop forever until signaled done
+    //1) waits on the endpoint to become ready
+    //2) acquire the send buffer from the endpoint
+    //3) read from the device stream into the endpoint buffer
+    //4) release the buffer back to the endpoint (sends)
+    while (not done)
+    {
+        if (not endpoint->waitSend(SOAPY_REMOTE_SOCKET_TIMEOUT_US)) continue;
+        ret = endpoint->acquireSend(handle, buffs.data());
+        if (ret < 0)
+        {
+            SoapySDR::logf(SOAPY_SDR_ERROR, "Server-side send endpoint: %s; worker quitting...", streamSock.lastErrorMsg());
+            return;
+        }
+
+        //Read only up to MTU size with a timeout for minimal waiting.
+        //In the next section we will continue the read with non-blocking.
+        size_t elemsLeft = size_t(ret);
+        size_t elemsRead = 0;
+        while (not done)
+        {
+            flags = 0; //flags is an in/out parameter and must be cleared for consistency
+            const size_t numElems = std::min(mtuElems, elemsLeft);
+            ret = device->readStream(stream, buffs.data(), numElems, flags, timeNs, SOAPY_REMOTE_SOCKET_TIMEOUT_US);
+            if (ret == SOAPY_SDR_TIMEOUT) continue;
+            if (ret < 0)
+            {
+                //ret will be propagated to remote endpoint
+                break;
+            }
+            elemsLeft -= ret;
+            elemsRead += ret;
+            incrementBuffs(buffs, ret, elemSize);
+            break;
+        }
+
+        //fill remaining buffer with no timeout
+        //This is a latency optimization to forward to the host ASAP,
+        //but to use the full bandwidth when more data is available.
+        //Do not allow this optimization when end of burst or single packet mode to preserve boundaries
+        if (elemsRead != 0 and elemsLeft != 0 and (flags & (SOAPY_SDR_END_BURST | SOAPY_SDR_ONE_PACKET)) == 0)
+        {
+            int flags1 = 0;
+            long long timeNs1 = 0;
+            ret = device->readStream(stream, buffs.data(), elemsLeft, flags1, timeNs1, 0);
+            if (ret == SOAPY_SDR_TIMEOUT) ret = 0; //timeouts OK
+            if (ret > 0)
+            {
+                elemsLeft -= ret;
+                elemsRead += ret;
+            }
+        }
+
+        //release the buffer with flags and time from the first read
+        //if any read call returned an error, forward the error instead
+        endpoint->releaseSend(handle, (ret < 0)?ret:elemsRead, flags, timeNs);
+    }
+}
+
+void ServerStreamData::statEndpointWork(void)
+{
+    assert(endpoint != nullptr);
+
+    int ret = 0;
+    size_t chanMask = 0;
+    int flags = 0;
+    long long timeNs = 0;
+
+    while (not done)
+    {
+        ret = device->readStreamStatus(stream, chanMask, flags, timeNs, SOAPY_REMOTE_SOCKET_TIMEOUT_US);
+        if (ret == SOAPY_SDR_TIMEOUT) continue;
+        endpoint->writeStatus(ret, chanMask, flags, timeNs);
+
+        //exit the thread if stream status is not supported
+        //but only after reporting this to the local endpoint
+        if (ret == SOAPY_SDR_NOT_SUPPORTED) return;
+    }
+}
diff --git a/server/ServerStreamData.hpp b/server/ServerStreamData.hpp
new file mode 100644
index 0000000..c9f7ef2
--- /dev/null
+++ b/server/ServerStreamData.hpp
@@ -0,0 +1,65 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include "SoapyRPCSocket.hpp"
+#include "ThreadPrioHelper.hpp"
+#include <csignal> //sig_atomic_t
+#include <string>
+#include <thread>
+
+class SoapyStreamEndpoint;
+
+namespace SoapySDR
+{
+    class Device;
+    class Stream;
+}
+
+/*!
+ * Server-side stream data for client handler.
+ * This class manages a recv/send endpoint,
+ * and a thread to handle that endpoint.
+ */
+class ServerStreamData
+{
+public:
+    ServerStreamData(void);
+
+    SoapySDR::Device *device;
+    SoapySDR::Stream *stream;
+    std::string format;
+    size_t chanMask;
+    double priority;
+
+    //this ID identifies the stream to the remote host
+    int streamId;
+
+    //datagram socket for stream endpoint
+    SoapyRPCSocket streamSock;
+
+    //datagram socket for status endpoint
+    SoapyRPCSocket statusSock;
+
+    //remote side of the stream endpoint
+    SoapyStreamEndpoint *endpoint;
+
+    //hooks to start/stop work
+    void startSendThread(void);
+    void startRecvThread(void);
+    void startStatThread(void);
+    void stopThreads(void);
+
+    //worker implementations
+    void recvEndpointWork(void);
+    void sendEndpointWork(void);
+    void statEndpointWork(void);
+
+private:
+    //worker thread for this stream
+    std::thread *streamThread;
+    std::thread *statusThread;
+
+    //signal done to the thread
+    sig_atomic_t done;
+};
diff --git a/server/SoapyServer.cpp b/server/SoapyServer.cpp
new file mode 100644
index 0000000..22038bc
--- /dev/null
+++ b/server/SoapyServer.cpp
@@ -0,0 +1,121 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "SoapyServer.hpp"
+#include "SoapyRemoteDefs.hpp"
+#include "SoapyURLUtils.hpp"
+#include "SoapyInfoUtils.hpp"
+#include "SoapyRPCSocket.hpp"
+#include "SoapySSDPEndpoint.hpp"
+#include <cstdlib>
+#include <cstddef>
+#include <iostream>
+#include <getopt.h>
+#include <csignal>
+
+/***********************************************************************
+ * Print help message
+ **********************************************************************/
+static int printHelp(void)
+{
+    std::cout << "Usage SoapySDRServer [options]" << std::endl;
+    std::cout << "  Options summary:" << std::endl;
+    std::cout << "    --help \t\t\t\t Print this help message" << std::endl;
+    std::cout << "    --bind \t\t\t\t Bind and serve forever" << std::endl;
+    std::cout << std::endl;
+    return EXIT_SUCCESS;
+}
+
+/***********************************************************************
+ * Signal handler for Ctrl + C
+ **********************************************************************/
+static sig_atomic_t serverDone = false;
+void sigIntHandler(const int)
+{
+    std::cout << "Caught Ctrl+C, shutting down the server..." << std::endl;
+    serverDone = true;
+}
+
+/***********************************************************************
+ * Launch the server
+ **********************************************************************/
+static int runServer(void)
+{
+    SoapySocketSession sess;
+    const bool isIPv6Supported = not SoapyRPCSocket(SoapyURL("tcp", "::", "0").toString()).null();
+    const auto defaultBindNode = isIPv6Supported?"::":"0.0.0.0";
+
+    //extract url from user input or generate automatically
+    const bool optargHasURL = (optarg != NULL and not std::string(optarg).empty());
+    auto url = (optargHasURL)? SoapyURL(optarg) : SoapyURL("tcp", defaultBindNode, "");
+
+    //default url parameters when not specified
+    if (url.getScheme().empty()) url.setScheme("tcp");
+    if (url.getService().empty()) url.setService(SOAPY_REMOTE_DEFAULT_SERVICE);
+
+    //this UUID identifies the server process
+    const auto serverUUID = SoapyInfo::generateUUID1();
+    std::cout << serverUUID << std::endl;
+
+    std::cout << "Launching the server... " << url.toString() << std::endl;
+    SoapyRPCSocket s;
+    if (s.bind(url.toString()) != 0)
+    {
+        std::cerr << "Server socket bind FAIL: " << s.lastErrorMsg() << std::endl;
+        return EXIT_FAILURE;
+    }
+    std::cout << "Server bound to " << s.getsockname() << std::endl;
+    s.listen(SOAPY_REMOTE_LISTEN_BACKLOG);
+    auto serverListener = new SoapyServerListener(s, serverUUID);
+
+    std::cout << "Launching discovery server... " << std::endl;
+    auto ssdpEndpoint = SoapySSDPEndpoint::getInstance();
+    ssdpEndpoint->registerService(serverUUID, url.getService());
+    ssdpEndpoint->enablePeriodicNotify(true);
+
+    std::cout << "Press Ctrl+C to stop the server" << std::endl;
+    signal(SIGINT, sigIntHandler);
+    while (not serverDone) serverListener->handleOnce();
+    ssdpEndpoint->enablePeriodicNotify(false);
+    ssdpEndpoint.reset();
+
+    std::cout << "Shutdown client handler threads" << std::endl;
+    delete serverListener;
+    s.close();
+
+    std::cout << "Cleanup complete, exiting" << std::endl;
+    return EXIT_SUCCESS;
+}
+
+/***********************************************************************
+ * Parse and dispatch options
+ **********************************************************************/
+int main(int argc, char *argv[])
+{
+    std::cout << "######################################################" << std::endl;
+    std::cout << "## Soapy Server -- Use any Soapy SDR remotely" << std::endl;
+    std::cout << "######################################################" << std::endl;
+    std::cout << std::endl;
+
+    /*******************************************************************
+     * parse command line options
+     ******************************************************************/
+    static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"bind", optional_argument, 0, 'b'},
+        {0, 0, 0,  0}
+    };
+    int long_index = 0;
+    int option = 0;
+    while ((option = getopt_long_only(argc, argv, "", long_options, &long_index)) != -1)
+    {
+        switch (option)
+        {
+        case 'h': return printHelp();
+        case 'b': return runServer();
+        }
+    }
+
+    //unknown or unspecified options, do help...
+    return printHelp();
+}
diff --git a/server/SoapyServer.hpp b/server/SoapyServer.hpp
new file mode 100644
index 0000000..9f5fb0b
--- /dev/null
+++ b/server/SoapyServer.hpp
@@ -0,0 +1,41 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+#include <csignal> //sig_atomic_t
+#include <string>
+#include <thread>
+#include <map>
+
+class SoapyRPCSocket;
+
+//! Client handler data
+struct SoapyServerThreadData
+{
+    SoapyServerThreadData(void);
+    ~SoapyServerThreadData(void);
+    void handlerLoop(void);
+    sig_atomic_t done;
+    std::thread *thread;
+    SoapyRPCSocket *client;
+    std::string uuid;
+};
+
+/*!
+ * The server listener class accepts clients and spawns threads.
+ */
+class SoapyServerListener
+{
+public:
+    SoapyServerListener(SoapyRPCSocket &sock, const std::string &uuid);
+
+    ~SoapyServerListener(void);
+
+    void handleOnce(void);
+
+private:
+    SoapyRPCSocket &_sock;
+    const std::string _uuid;
+    size_t _handlerId;
+    std::map<size_t, SoapyServerThreadData> _handlers;
+};
diff --git a/server/ThreadPrioHelper.hpp b/server/ThreadPrioHelper.hpp
new file mode 100644
index 0000000..8c04642
--- /dev/null
+++ b/server/ThreadPrioHelper.hpp
@@ -0,0 +1,13 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#pragma once
+
+#include <string>
+
+/*! Set the thread priority:
+ * -1.0 (low), 0.0 (normal), and 1.0 (high)
+ * Return a empty string on success,
+ * otherwise an error message.
+ */
+std::string setThreadPrio(const double prio);
diff --git a/server/ThreadPrioUnix.cpp b/server/ThreadPrioUnix.cpp
new file mode 100644
index 0000000..edcf4ca
--- /dev/null
+++ b/server/ThreadPrioUnix.cpp
@@ -0,0 +1,38 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "ThreadPrioHelper.hpp"
+#include <cstring> //memset, strerror
+#include <cerrno> //errno
+#include <sched.h>
+
+#ifdef __APPLE__
+#include <thread>
+#endif
+
+std::string setThreadPrio(const double prio)
+{
+    //no negative priorities supported on this OS
+    if (prio <= 0.0) return "";
+
+    //determine priority bounds
+    const int policy(SCHED_RR);
+    const int maxPrio = sched_get_priority_max(policy);
+    if (maxPrio < 0) return strerror(errno);
+    const int minPrio = sched_get_priority_min(policy);
+    if (minPrio < 0) return strerror(errno);
+
+    //set realtime priority and prio number
+    struct sched_param param;
+    std::memset(&param, 0, sizeof(param));
+    param.sched_priority = minPrio + int(prio * (maxPrio-minPrio));
+
+#ifdef __APPLE__
+    pthread_t tID = pthread_self();  // ID of this thread
+    if (pthread_setschedparam(tID, policy, &param) != 0) return strerror(errno);
+#else
+    if (sched_setscheduler(0, policy, &param) != 0) return strerror(errno);
+#endif
+
+    return "";
+}
diff --git a/server/ThreadPrioWindows.cpp b/server/ThreadPrioWindows.cpp
new file mode 100644
index 0000000..cca4600
--- /dev/null
+++ b/server/ThreadPrioWindows.cpp
@@ -0,0 +1,28 @@
+// Copyright (c) 2015-2015 Josh Blum
+// SPDX-License-Identifier: BSL-1.0
+
+#include "ThreadPrioHelper.hpp"
+#include <windows.h>
+
+std::string setThreadPrio(const double prio)
+{
+    int nPriority(THREAD_PRIORITY_NORMAL);
+
+    if (prio > 0)
+    {
+        if      (prio > +0.75) nPriority = THREAD_PRIORITY_TIME_CRITICAL;
+        else if (prio > +0.50) nPriority = THREAD_PRIORITY_HIGHEST;
+        else if (prio > +0.25) nPriority = THREAD_PRIORITY_ABOVE_NORMAL;
+        else                   nPriority = THREAD_PRIORITY_NORMAL;
+    }
+    else
+    {
+        if      (prio < -0.75) nPriority = THREAD_PRIORITY_IDLE;
+        else if (prio < -0.50) nPriority = THREAD_PRIORITY_LOWEST;
+        else if (prio < -0.25) nPriority = THREAD_PRIORITY_BELOW_NORMAL;
+        else                   nPriority = THREAD_PRIORITY_NORMAL;
+    }
+
+    if (SetThreadPriority(GetCurrentThread(), nPriority)) return "";
+    return "SetThreadPriority() fail";
+}
diff --git a/server/msvc/getopt.h b/server/msvc/getopt.h
new file mode 100644
index 0000000..6c834b4
--- /dev/null
+++ b/server/msvc/getopt.h
@@ -0,0 +1,607 @@
+#ifndef __GETOPT_H__
+/**
+ * DISCLAIMER
+ * This file has no copyright assigned and is placed in the Public Domain.
+ * This file is part of the mingw-w64 runtime package.
+ *
+ * The mingw-w64 runtime package and its code is distributed in the hope that it 
+ * will be useful but WITHOUT ANY WARRANTY.  ALL WARRANTIES, EXPRESSED OR 
+ * IMPLIED ARE HEREBY DISCLAIMED.  This includes but is not limited to 
+ * warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+#pragma warning(disable:4996)
+
+#define __GETOPT_H__
+
+/* All the headers include this file. */
+#include <crtdefs.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <windows.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define	REPLACE_GETOPT		/* use this getopt as the system getopt(3) */
+
+#ifdef REPLACE_GETOPT
+int	opterr = 1;		/* if error message should be printed */
+int	optind = 1;		/* index into parent argv vector */
+int	optopt = '?';		/* character checked for validity */
+#undef	optreset		/* see getopt.h */
+#define	optreset		__mingw_optreset
+int	optreset;		/* reset getopt */
+char    *optarg;		/* argument associated with option */
+#endif
+
+//extern int optind;		/* index of first non-option in argv      */
+//extern int optopt;		/* single option character, as parsed     */
+//extern int opterr;		/* flag to enable built-in diagnostics... */
+//				/* (user may set to zero, to suppress)    */
+//
+//extern char *optarg;		/* pointer to argument of current option  */
+
+#define PRINT_ERROR	((opterr) && (*options != ':'))
+
+#define FLAG_PERMUTE	0x01	/* permute non-options to the end of argv */
+#define FLAG_ALLARGS	0x02	/* treat non-options as args to option "-1" */
+#define FLAG_LONGONLY	0x04	/* operate as getopt_long_only */
+
+/* return values */
+#define	BADCH		(int)'?'
+#define	BADARG		((*options == ':') ? (int)':' : (int)'?')
+#define	INORDER 	(int)1
+
+#ifndef __CYGWIN__
+#define __progname __argv[0]
+#else
+extern char __declspec(dllimport) *__progname;
+#endif
+
+#ifdef __CYGWIN__
+static char EMSG[] = "";
+#else
+#define	EMSG		""
+#endif
+
+static int getopt_internal(int, char * const *, const char *,
+			   const struct option *, int *, int);
+static int parse_long_options(char * const *, const char *,
+			      const struct option *, int *, int);
+static int gcd(int, int);
+static void permute_args(int, int, int, char * const *);
+
+static char *place = EMSG; /* option letter processing */
+
+/* XXX: set optreset to 1 rather than these two */
+static int nonopt_start = -1; /* first non option argument (for permute) */
+static int nonopt_end = -1;   /* first option after non options (for permute) */
+
+/* Error messages */
+static const char recargchar[] = "option requires an argument -- %c";
+static const char recargstring[] = "option requires an argument -- %s";
+static const char ambig[] = "ambiguous option -- %.*s";
+static const char noarg[] = "option doesn't take an argument -- %.*s";
+static const char illoptchar[] = "unknown option -- %c";
+static const char illoptstring[] = "unknown option -- %s";
+
+static void
+_vwarnx(const char *fmt,va_list ap)
+{
+  (void)fprintf(stderr,"%s: ",__progname);
+  if (fmt != NULL)
+    (void)vfprintf(stderr,fmt,ap);
+  (void)fprintf(stderr,"\n");
+}
+
+static void
+warnx(const char *fmt,...)
+{
+  va_list ap;
+  va_start(ap,fmt);
+  _vwarnx(fmt,ap);
+  va_end(ap);
+}
+
+/*
+ * Compute the greatest common divisor of a and b.
+ */
+static int
+gcd(int a, int b)
+{
+	int c;
+
+	c = a % b;
+	while (c != 0) {
+		a = b;
+		b = c;
+		c = a % b;
+	}
+
+	return (b);
+}
+
+/*
+ * Exchange the block from nonopt_start to nonopt_end with the block
+ * from nonopt_end to opt_end (keeping the same order of arguments
+ * in each block).
+ */
+static void
+permute_args(int panonopt_start, int panonopt_end, int opt_end,
+	char * const *nargv)
+{
+	int cstart, cyclelen, i, j, ncycle, nnonopts, nopts, pos;
+	char *swap;
+
+	/*
+	 * compute lengths of blocks and number and size of cycles
+	 */
+	nnonopts = panonopt_end - panonopt_start;
+	nopts = opt_end - panonopt_end;
+	ncycle = gcd(nnonopts, nopts);
+	cyclelen = (opt_end - panonopt_start) / ncycle;
+
+	for (i = 0; i < ncycle; i++) {
+		cstart = panonopt_end+i;
+		pos = cstart;
+		for (j = 0; j < cyclelen; j++) {
+			if (pos >= panonopt_end)
+				pos -= nnonopts;
+			else
+				pos += nopts;
+			swap = nargv[pos];
+			/* LINTED const cast */
+			((char **) nargv)[pos] = nargv[cstart];
+			/* LINTED const cast */
+			((char **)nargv)[cstart] = swap;
+		}
+	}
+}
+
+#ifdef REPLACE_GETOPT
+/*
+ * getopt --
+ *	Parse argc/argv argument vector.
+ *
+ * [eventually this will replace the BSD getopt]
+ */
+int
+getopt(int nargc, char * const *nargv, const char *options)
+{
+
+	/*
+	 * We don't pass FLAG_PERMUTE to getopt_internal() since
+	 * the BSD getopt(3) (unlike GNU) has never done this.
+	 *
+	 * Furthermore, since many privileged programs call getopt()
+	 * before dropping privileges it makes sense to keep things
+	 * as simple (and bug-free) as possible.
+	 */
+	return (getopt_internal(nargc, nargv, options, NULL, NULL, 0));
+}
+#endif /* REPLACE_GETOPT */
+
+//extern int getopt(int nargc, char * const *nargv, const char *options);
+
+#ifdef _BSD_SOURCE
+/*
+ * BSD adds the non-standard `optreset' feature, for reinitialisation
+ * of `getopt' parsing.  We support this feature, for applications which
+ * proclaim their BSD heritage, before including this header; however,
+ * to maintain portability, developers are advised to avoid it.
+ */
+# define optreset  __mingw_optreset
+extern int optreset;
+#endif
+#ifdef __cplusplus
+}
+#endif
+/*
+ * POSIX requires the `getopt' API to be specified in `unistd.h';
+ * thus, `unistd.h' includes this header.  However, we do not want
+ * to expose the `getopt_long' or `getopt_long_only' APIs, when
+ * included in this manner.  Thus, close the standard __GETOPT_H__
+ * declarations block, and open an additional __GETOPT_LONG_H__
+ * specific block, only when *not* __UNISTD_H_SOURCED__, in which
+ * to declare the extended API.
+ */
+#endif /* !defined(__GETOPT_H__) */
+
+#if !defined(__UNISTD_H_SOURCED__) && !defined(__GETOPT_LONG_H__)
+#define __GETOPT_LONG_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct option		/* specification for a long form option...	*/
+{
+  const char *name;		/* option name, without leading hyphens */
+  int         has_arg;		/* does it take an argument?		*/
+  int        *flag;		/* where to save its status, or NULL	*/
+  int         val;		/* its associated status value		*/
+};
+
+enum    		/* permitted values for its `has_arg' field...	*/
+{
+  no_argument = 0,      	/* option never takes an argument	*/
+  required_argument,		/* option always requires an argument	*/
+  optional_argument		/* option may take an argument		*/
+};
+
+/*
+ * parse_long_options --
+ *	Parse long options in argc/argv argument vector.
+ * Returns -1 if short_too is set and the option does not match long_options.
+ */
+static int
+parse_long_options(char * const *nargv, const char *options,
+	const struct option *long_options, int *idx, int short_too)
+{
+	char *current_argv, *has_equal;
+	size_t current_argv_len;
+	int i, ambiguous, match;
+
+#define IDENTICAL_INTERPRETATION(_x, _y)                                \
+	(long_options[(_x)].has_arg == long_options[(_y)].has_arg &&    \
+	 long_options[(_x)].flag == long_options[(_y)].flag &&          \
+	 long_options[(_x)].val == long_options[(_y)].val)
+
+	current_argv = place;
+	match = -1;
+	ambiguous = 0;
+
+	optind++;
+
+	if ((has_equal = strchr(current_argv, '=')) != NULL) {
+		/* argument found (--option=arg) */
+		current_argv_len = has_equal - current_argv;
+		has_equal++;
+	} else
+		current_argv_len = strlen(current_argv);
+
+	for (i = 0; long_options[i].name; i++) {
+		/* find matching long option */
+		if (strncmp(current_argv, long_options[i].name,
+		    current_argv_len))
+			continue;
+
+		if (strlen(long_options[i].name) == current_argv_len) {
+			/* exact match */
+			match = i;
+			ambiguous = 0;
+			break;
+		}
+		/*
+		 * If this is a known short option, don't allow
+		 * a partial match of a single character.
+		 */
+		if (short_too && current_argv_len == 1)
+			continue;
+
+		if (match == -1)	/* partial match */
+			match = i;
+		else if (!IDENTICAL_INTERPRETATION(i, match))
+			ambiguous = 1;
+	}
+	if (ambiguous) {
+		/* ambiguous abbreviation */
+		if (PRINT_ERROR)
+			warnx(ambig, (int)current_argv_len,
+			     current_argv);
+		optopt = 0;
+		return (BADCH);
+	}
+	if (match != -1) {		/* option found */
+		if (long_options[match].has_arg == no_argument
+		    && has_equal) {
+			if (PRINT_ERROR)
+				warnx(noarg, (int)current_argv_len,
+				     current_argv);
+			/*
+			 * XXX: GNU sets optopt to val regardless of flag
+			 */
+			if (long_options[match].flag == NULL)
+				optopt = long_options[match].val;
+			else
+				optopt = 0;
+			return (BADARG);
+		}
+		if (long_options[match].has_arg == required_argument ||
+		    long_options[match].has_arg == optional_argument) {
+			if (has_equal)
+				optarg = has_equal;
+			else if (long_options[match].has_arg ==
+			    required_argument) {
+				/*
+				 * optional argument doesn't use next nargv
+				 */
+				optarg = nargv[optind++];
+			}
+		}
+		if ((long_options[match].has_arg == required_argument)
+		    && (optarg == NULL)) {
+			/*
+			 * Missing argument; leading ':' indicates no error
+			 * should be generated.
+			 */
+			if (PRINT_ERROR)
+				warnx(recargstring,
+				    current_argv);
+			/*
+			 * XXX: GNU sets optopt to val regardless of flag
+			 */
+			if (long_options[match].flag == NULL)
+				optopt = long_options[match].val;
+			else
+				optopt = 0;
+			--optind;
+			return (BADARG);
+		}
+	} else {			/* unknown option */
+		if (short_too) {
+			--optind;
+			return (-1);
+		}
+		if (PRINT_ERROR)
+			warnx(illoptstring, current_argv);
+		optopt = 0;
+		return (BADCH);
+	}
+	if (idx)
+		*idx = match;
+	if (long_options[match].flag) {
+		*long_options[match].flag = long_options[match].val;
+		return (0);
+	} else
+		return (long_options[match].val);
+#undef IDENTICAL_INTERPRETATION
+}
+
+/*
+ * getopt_internal --
+ *	Parse argc/argv argument vector.  Called by user level routines.
+ */
+static int
+getopt_internal(int nargc, char * const *nargv, const char *options,
+	const struct option *long_options, int *idx, int flags)
+{
+	char *oli;				/* option letter list index */
+	int optchar, short_too;
+	static int posixly_correct = -1;
+
+	if (options == NULL)
+		return (-1);
+
+	/*
+	 * XXX Some GNU programs (like cvs) set optind to 0 instead of
+	 * XXX using optreset.  Work around this braindamage.
+	 */
+	if (optind == 0)
+		optind = optreset = 1;
+
+	/*
+	 * Disable GNU extensions if POSIXLY_CORRECT is set or options
+	 * string begins with a '+'.
+	 *
+	 * CV, 2009-12-14: Check POSIXLY_CORRECT anew if optind == 0 or
+	 *                 optreset != 0 for GNU compatibility.
+	 */
+	if (posixly_correct == -1 || optreset != 0)
+		posixly_correct = (getenv("POSIXLY_CORRECT") != NULL);
+	if (*options == '-')
+		flags |= FLAG_ALLARGS;
+	else if (posixly_correct || *options == '+')
+		flags &= ~FLAG_PERMUTE;
+	if (*options == '+' || *options == '-')
+		options++;
+
+	optarg = NULL;
+	if (optreset)
+		nonopt_start = nonopt_end = -1;
+start:
+	if (optreset || !*place) {		/* update scanning pointer */
+		optreset = 0;
+		if (optind >= nargc) {          /* end of argument vector */
+			place = EMSG;
+			if (nonopt_end != -1) {
+				/* do permutation, if we have to */
+				permute_args(nonopt_start, nonopt_end,
+				    optind, nargv);
+				optind -= nonopt_end - nonopt_start;
+			}
+			else if (nonopt_start != -1) {
+				/*
+				 * If we skipped non-options, set optind
+				 * to the first of them.
+				 */
+				optind = nonopt_start;
+			}
+			nonopt_start = nonopt_end = -1;
+			return (-1);
+		}
+		if (*(place = nargv[optind]) != '-' ||
+		    (place[1] == '\0' && strchr(options, '-') == NULL)) {
+			place = EMSG;		/* found non-option */
+			if (flags & FLAG_ALLARGS) {
+				/*
+				 * GNU extension:
+				 * return non-option as argument to option 1
+				 */
+				optarg = nargv[optind++];
+				return (INORDER);
+			}
+			if (!(flags & FLAG_PERMUTE)) {
+				/*
+				 * If no permutation wanted, stop parsing
+				 * at first non-option.
+				 */
+				return (-1);
+			}
+			/* do permutation */
+			if (nonopt_start == -1)
+				nonopt_start = optind;
+			else if (nonopt_end != -1) {
+				permute_args(nonopt_start, nonopt_end,
+				    optind, nargv);
+				nonopt_start = optind -
+				    (nonopt_end - nonopt_start);
+				nonopt_end = -1;
+			}
+			optind++;
+			/* process next argument */
+			goto start;
+		}
+		if (nonopt_start != -1 && nonopt_end == -1)
+			nonopt_end = optind;
+
+		/*
+		 * If we have "-" do nothing, if "--" we are done.
+		 */
+		if (place[1] != '\0' && *++place == '-' && place[1] == '\0') {
+			optind++;
+			place = EMSG;
+			/*
+			 * We found an option (--), so if we skipped
+			 * non-options, we have to permute.
+			 */
+			if (nonopt_end != -1) {
+				permute_args(nonopt_start, nonopt_end,
+				    optind, nargv);
+				optind -= nonopt_end - nonopt_start;
+			}
+			nonopt_start = nonopt_end = -1;
+			return (-1);
+		}
+	}
+
+	/*
+	 * Check long options if:
+	 *  1) we were passed some
+	 *  2) the arg is not just "-"
+	 *  3) either the arg starts with -- we are getopt_long_only()
+	 */
+	if (long_options != NULL && place != nargv[optind] &&
+	    (*place == '-' || (flags & FLAG_LONGONLY))) {
+		short_too = 0;
+		if (*place == '-')
+			place++;		/* --foo long option */
+		else if (*place != ':' && strchr(options, *place) != NULL)
+			short_too = 1;		/* could be short option too */
+
+		optchar = parse_long_options(nargv, options, long_options,
+		    idx, short_too);
+		if (optchar != -1) {
+			place = EMSG;
+			return (optchar);
+		}
+	}
+
+	if ((optchar = (int)*place++) == (int)':' ||
+	    (optchar == (int)'-' && *place != '\0') ||
+	    (oli = (char*)strchr(options, optchar)) == NULL) {
+		/*
+		 * If the user specified "-" and  '-' isn't listed in
+		 * options, return -1 (non-option) as per POSIX.
+		 * Otherwise, it is an unknown option character (or ':').
+		 */
+		if (optchar == (int)'-' && *place == '\0')
+			return (-1);
+		if (!*place)
+			++optind;
+		if (PRINT_ERROR)
+			warnx(illoptchar, optchar);
+		optopt = optchar;
+		return (BADCH);
+	}
+	if (long_options != NULL && optchar == 'W' && oli[1] == ';') {
+		/* -W long-option */
+		if (*place)			/* no space */
+			/* NOTHING */;
+		else if (++optind >= nargc) {	/* no arg */
+			place = EMSG;
+			if (PRINT_ERROR)
+				warnx(recargchar, optchar);
+			optopt = optchar;
+			return (BADARG);
+		} else				/* white space */
+			place = nargv[optind];
+		optchar = parse_long_options(nargv, options, long_options,
+		    idx, 0);
+		place = EMSG;
+		return (optchar);
+	}
+	if (*++oli != ':') {			/* doesn't take argument */
+		if (!*place)
+			++optind;
+	} else {				/* takes (optional) argument */
+		optarg = NULL;
+		if (*place)			/* no white space */
+			optarg = place;
+		else if (oli[1] != ':') {	/* arg not optional */
+			if (++optind >= nargc) {	/* no arg */
+				place = EMSG;
+				if (PRINT_ERROR)
+					warnx(recargchar, optchar);
+				optopt = optchar;
+				return (BADARG);
+			} else
+				optarg = nargv[optind];
+		}
+		place = EMSG;
+		++optind;
+	}
+	/* dump back option letter */
+	return (optchar);
+}
+
+/*
+ * getopt_long --
+ *	Parse argc/argv argument vector.
+ */
+int
+getopt_long(int nargc, char * const *nargv, const char *options,
+    const struct option *long_options, int *idx)
+{
+
+	return (getopt_internal(nargc, nargv, options, long_options, idx,
+	    FLAG_PERMUTE));
+}
+
+/*
+ * getopt_long_only --
+ *	Parse argc/argv argument vector.
+ */
+int
+getopt_long_only(int nargc, char * const *nargv, const char *options,
+    const struct option *long_options, int *idx)
+{
+
+	return (getopt_internal(nargc, nargv, options, long_options, idx,
+	    FLAG_PERMUTE|FLAG_LONGONLY));
+}
+
+//extern int getopt_long(int nargc, char * const *nargv, const char *options,
+//    const struct option *long_options, int *idx);
+//extern int getopt_long_only(int nargc, char * const *nargv, const char *options,
+//    const struct option *long_options, int *idx);
+/*
+ * Previous MinGW implementation had...
+ */
+#ifndef HAVE_DECL_GETOPT
+/*
+ * ...for the long form API only; keep this for compatibility.
+ */
+# define HAVE_DECL_GETOPT	1
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* !defined(__UNISTD_H_SOURCED__) && !defined(__GETOPT_LONG_H__) */

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-hamradio/soapyremote.git



More information about the pkg-hamradio-commits mailing list