--- a
+++ b/src/myrdcvolume.cpp
@@ -0,0 +1,267 @@
+// This libupnpp sample does about the same thing as rdcvolume, but it defines
+// its own control classes instead of the ones predefined by libupnpp.
+
+#include <termios.h>
+
+#include <string>
+#include <iostream>
+#include <mutex>
+#include <condition_variable>
+#include <functional>
+
+#include "libupnpp/upnpplib.hxx"
+#include "libupnpp/soaphelp.hxx"
+#include "libupnpp/control/discovery.hxx"
+#include "libupnpp/control/device.hxx"
+#include "libupnpp/control/service.hxx"
+
+using namespace std;
+using namespace std::placeholders;
+
+//using namespace UPnPClient;
+//using namespace UPnPP;
+
+
+// Locally defined control class for the Rendering Control
+// service. We're just copying code from the libupnpp actually, this is
+// just to show that it can be done outside the library.
+class MyRDC : public UPnPClient::Service {
+public:
+
+    /* Construct by copying data from device and service objects.*/
+    MyRDC(const UPnPClient::UPnPDeviceDesc& device,
+          const UPnPClient::UPnPServiceDesc& service)
+        : UPnPClient::Service(device, service) {
+
+        // We want to have a look at our service description file
+        // (xml) to retrieve the min/max/step values for the
+        // volume. Not all services need to do this.
+        UPnPClient::UPnPServiceDesc::Parsed sdesc;
+        if (service.fetchAndParseDesc(device.URLBase, sdesc)) {
+            auto it = sdesc.stateTable.find("Volume");
+            if (it != sdesc.stateTable.end() && it->second.hasValueRange) {
+                m_volmin = it->second.minimum;
+                m_volmax = it->second.maximum;
+                m_volstep = it->second.step;
+            } else {
+                // ??
+                m_volmin = 0;
+                m_volmax = 100;
+                m_volstep = 1;
+            }
+        }
+
+        // Say that we want to receive events. This is not mandatory
+        registerCallback();
+    }
+
+    virtual ~MyRDC() {
+        unregisterCallback();
+    }
+
+    /* Test that a service type matches ours. This can be used
+       with the directory traversal routine */
+    static bool isRDCService(const std::string& st) {
+        // Note that we do not care about the version
+        return st.find("urn:schemas-upnp-org:service:RenderingControl") == 0;
+    }
+
+    int setVolume(int volume, const std::string& channel = "Master");
+    int getVolume(const std::string& channel = "Master");
+
+    /* Volume settings params */
+    int m_volmin;
+    int m_volmax;
+    int m_volstep;
+
+private:
+
+    void evtCallback(const unordered_map<string, string>& props) {
+        // The callback gets a map of changed properties as
+        // parameter. In turn, the classes defined by libupnpp
+        // (e.g. RenderingControl) call a client event reporter in an
+        // uniform way, and after massaging the data a bit, but you
+        // can do whatever you like here. UPnP AV is special
+        // because it coalesces the values inside a LastChange XML
+        // string. Many services just report them individually.
+        cerr << "evtCallback: props size " << props.size() << endl;
+        for (const auto& ent : props) {
+            cout << ent.first << " -> " << ent.second << endl;
+        }
+    }
+
+    // Register our member function callback. It's just an
+    // std::function, other approaches may be possible.
+    void registerCallback() {
+        UPnPClient::Service::registerCallback(
+            std::bind(&MyRDC::evtCallback, this, _1));
+    }
+};
+
+// The libupnpp equivalent checks and converts the range, and also
+// that a volume change is actually required, and does appropriate
+// rounding. We're just showing how to send a parameter here.  The arg
+// names are defined by the service description XML file, so it would
+// be possible to construct the call after the XML data (a la
+// upnp-inspector), there is nothing in libupnpp to prevent it.
+int MyRDC::setVolume(int ivol, const string& channel)
+{
+    // Outgoing parameters. The object is constructed with the service
+    // type (comes from the description we were built on), and the
+    // action name. This is sufficient for some actions (ie stop())
+    UPnPP::SoapOutgoing args(getServiceType(), "SetVolume");
+
+    // This call needs further outgoing arguments, which goes in there
+    // through an operator() overload
+    args("InstanceID", "0")("Channel", channel)
+        ("DesiredVolume", UPnPP::SoapHelp::i2s(ivol));
+
+    // We have to declare a return parameter, even if we don't care
+    // about the contents.
+    UPnPP::SoapIncoming data;
+
+    return runAction(args, data);
+}
+
+// Same as setVolume really, except that we look at the return data.
+int MyRDC::getVolume(const string& channel)
+{
+    UPnPP::SoapOutgoing args(getServiceType(), "GetVolume");
+    args("InstanceID", "0")("Channel", channel);
+    UPnPP::SoapIncoming data;
+    int ret = runAction(args, data);
+    if (ret != UPNP_E_SUCCESS) {
+        return ret;
+    }
+    int volume;
+    if (!data.get("CurrentVolume", &volume)) {
+        cerr << "MyRDC:getVolume: missing CurrentVolume in response\n";
+        return UPNP_E_BAD_RESPONSE;
+    }
+
+    return volume;
+}
+
+
+// Device discovery part. We do it the easy way here: use a blocking
+// call which will wait for the initial window to complete.  We could
+// traverse the device directory in search, for example of a device of
+// a specific kind instead of using a device name like we do here (there is
+// an example of UPnPDeviceDirectory::traverse() usage in uplistdir.cpp).
+//
+// See rdcvolume.cpp for a version using callbacks to get the device asap
+shared_ptr<MyRDC> getService(const string& name)
+{
+    // Initialize and get a discovery directory handle.
+    auto *superdir = UPnPClient::UPnPDeviceDirectory::getTheDir();
+    if (nullptr == superdir) {
+        cerr << "Discovery init failed\n";
+        return shared_ptr<MyRDC>();
+    }
+
+    UPnPClient::UPnPDeviceDesc devicedesc;
+    // We look-up the device by either friendlyname or udn as the 2
+    // namespaces are unlikely to overlap, no need to complicate things
+    if (!superdir->getDevByUDN(name, devicedesc) && 
+        !superdir->getDevByFName(name, devicedesc)) {
+        cerr << "Can't connect to " << name << endl;
+        return shared_ptr<MyRDC>();
+    }
+
+    // UPnPClient::Device does nothing really interesting actually. It
+    // just holds the device description. Derived device
+    // implementations, for example for a MediaRenderer, add a bit of
+    // value by creating objects for the well-known services. Here we
+    // just dispense with the device creation, and directly create a
+    // service object.
+    
+    // Walk the device description service list, looking for ours
+    for (const auto& ent : devicedesc.services) {
+        if (MyRDC::isRDCService(ent.serviceType)) {
+            cout << ent.dump() << endl;
+            return make_shared<MyRDC>(devicedesc, ent);
+        }
+    }
+    cerr << name << " has no rendering control service\n";
+    return shared_ptr<MyRDC>();
+}
+
+// nothing to see here: character reading, one at a time.
+int mygetch()
+{
+    struct termios oldt, newt;
+    int ch;
+    tcgetattr(STDIN_FILENO, &oldt);
+    newt = oldt;
+    newt.c_lflag &= ~(ICANON | ECHO);
+    tcsetattr(STDIN_FILENO, TCSANOW, &newt);
+    ch = getchar();
+    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
+    return ch;
+}
+
+int main(int argc, char **argv)
+{
+    argv++;argc--;
+    if (argc != 1) {
+        cerr << "Usage: rdcvolume rendererNameOrUid\n";
+        return 1;
+    }
+    string devname(*argv++); 
+    argc--;
+    
+    // Initialize libupnpp logging
+    Logger::getTheLog("")->setLogLevel(Logger::LLERR);
+
+    // Explicitely initialize libupnpp so that we can display a
+    // possible error
+    UPnPP::LibUPnP *mylib = UPnPP::LibUPnP::getLibUPnP();
+    if (!mylib) {
+        cerr << "Can't get LibUPnP" << endl;
+        return 1;
+    }
+    if (!mylib->ok()) {
+        cerr << "Lib init failed: " <<
+            mylib->errAsString("main", mylib->getInitError()) << endl;
+        return 1;
+    }
+
+
+    shared_ptr<MyRDC> rdc = getService(devname);
+    if (!rdc) {
+        cerr << "Device " << devname << 
+            " has no RenderingControl service" << endl;
+        return 1;
+    }
+
+    cout << "q = quit, 'u' = up, 'd' = down\n";
+    for (;;) {
+        int vol = rdc->getVolume();
+        cout << "Volume now " << vol << endl;
+        int key = mygetch();
+
+        if (key == 'q') {
+            cout << "QUIT\n";
+            break;
+        } else if (key == 'u') {
+            vol += 5;
+            if (vol > 100) {
+                vol = 100;
+            }
+        } else if (key == 'd') {
+            vol -= 5;
+            if (vol < 0) {
+                vol = 0;
+            }
+        } else {
+            cout << "Bad key: " << (char)key << endl;
+            continue;
+        }
+        if (rdc->setVolume(vol)) {
+            cerr << "setVolume(" << vol << ") failed\n";
+        }
+    }
+
+    return 0;
+}
+