libnpupnp: libnpupnp

libnpupnp 4.1.0 An almost compatible successor for the Portable UPnP reference library

libnpupnp

# Introduction

libpnupnp is a C++ reimplementation of the venerable libupnp, a.k.a. Portable UPnP* library. Most of the API is unchanged, except where libupnp exposed its internal XML ixml library DOM objects.

One principal objective of the evolution was to make the transition as easy as possible for a program currently using libupnp. The structure of the C API is conserved.

Beyond issues of reliability and maintability, the main evolution in the new library is the support for multiple network interfaces. Also, the library exposes helper code for parsing an UPnP device description document and for accessing the machine network interfaces.

libnupnp implements a low level interface to the UPnP protocol. It has a separate set of functions for client and device implementations, with a few common functions, mostly for initialization and termination.

This document is a high level overview, with references to the detailed documentation. It largely supposes that you have a working knowledge of UPnP concepts, and you will probably also need to read parts of the UPnP architecture standard.

We present the programming interface in three sections:

- Common functions (initialization and termination mostly).
- Client interface.
- Device interface.

It is possible, but uncommon, to use both the client and device interfaces in the same instance.

You will find a tar archive of the current released version on the upmpdcli downloads page.

Some code examples are present in the document. In addition, it may be useful to clone the npupnp samples repository, which contain buildable and runnable examples, and will also referenced further down.

# Common functions

## Configuring the message log

The library has an internal error/debug messages system with settable verbosity and output destination.

By default, only error messages will be logged, to stderr.

You can use the functions in upnpdebug.h to configure logging. This can be done before or after the main initialisation of the library, but is best done before, to ensure that the initialisation messages behave as desired.

Example:

#include <upnp/upnp.h>
#include <upnp/upnpdebug.h>
int main(int argc, char **argv)
{
UpnpSetLogFileNames("/tmp/mylogfilename.txt", "");
UpnpSetLogLevel(UPNP_INFO);
UpnpInitLog();
UpnpPrintf(UPNP_INFO, DOM, __FILE__, __LINE__, "my log message\n");
int success = UpnpInit2("", ...
UpnpInit2
EXPORT_SPEC int UpnpInit2(const char *IfName, unsigned short DestPort)
Initializes the library, passing the interface spec as a single string.
UpnpSetLogFileNames
EXPORT_SPEC void UpnpSetLogFileNames(const char *fileName, const char *Ignored)
Set the name for the log file. You will then need to call UpnpInitLog to close the old file if needed...
UpnpPrintf
EXPORT_SPEC void UpnpPrintf(Upnp_LogLevel DLevel, Dbg_Module Module, const char *DbgFileName, int DbgLineNo, const char *FmtStr,...)
Prints the debug statement to the current output.
UpnpSetLogLevel
EXPORT_SPEC void UpnpSetLogLevel(Upnp_LogLevel log_level)
Set the log verbosity level.
UPNP_INFO
@ UPNP_INFO
Definition upnpdebug.h:76
UpnpInitLog
EXPORT_SPEC int UpnpInitLog(void)
Initialize the log output. Can be called before UpnpInit2.

## Common library initialisation

The library initialization mostly consists in selecting one or several network interfaces and addresses, and starting a listener on them.

It should be noted that both client and device instanciations (which are identical at this point) start a network listener on the UPnP multicast UDP address and port, and on a TCP port specific to the library (49152 for the first instance by default).

The listener is used for UPnP discovery operations on both sides, for event reception on the client side, and for SOAP action request reception on the device side, and the operation of both sides is largely symmetric. In other words, UPnP clients also run a network server.

For historical and compatibility reasons there are several possible ways to initialize the library:

- UpnpInit() is deprecated, and works only for IPV4.
- UpnpInit2() can handle multiple interfaces and will enable IPV6 operation (except if disabled at compile time). This is mostly kept for compatibility with the old libupnp. Calling UpnpInit2(ifname, port) is exactly equivalent to calling UpnpInitWithOptions(ifname, port, UPNP_FLAG_IPV6, UPNP_OPTION_END).
- UpnpInitWithOptions() is the new npupnp way and has a more adaptable interface.

For the two last functions, the network adapter name can be specified as follows:

- Empty (nullptr or empty string): the library will choose the first non loopback adapter (and having an IPV6 address if the flag is set).
- "*": the library will use all possible network adapters.
- Space-separated adapter names: the library will try to use the specified adapters. Under Windows, the names can be either the friendly or the system adapter names. If an adapter name contains space characters, you will need to quote it with double quotes.

Example:

#include <upnp/upnp.h>
#include <upnp/upnpdebug.h>
#include <upnp/upnptools.h>
#include <iostream>
int main(int argc, char **argv)
{
if (argc != 2) {
std::cerr << "Usage: npupnp_init <ifname>\n";
return 1;
}
const char *ifname = argv[1];
UpnpSetLogFileNames("", "");
UpnpSetLogLevel(UPNP_INFO);
UpnpInitLog();
int port = 0;
unsigned int flags = UPNP_FLAG_IPV6;
int success = UpnpInitWithOptions(
ifname, port, flags, UPNP_OPTION_NETWORK_WAIT, 5, UPNP_OPTION_END);
if (success != UPNP_E_SUCCESS) {
std::cerr << "init failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
return 1;
}
std::cout << "IPV4 address: " << UpnpGetServerIpAddress() << "\n";
std::cout << "IPV6 address: " << UpnpGetServerIp6Address() << "\n";
std::cout << "Port: " << UpnpGetServerPort() << "\n";
UpnpFinish();
return 0;
}
UPNP_OPTION_NETWORK_WAIT
@ UPNP_OPTION_NETWORK_WAIT
Max wait seconds for an IP address to be found, int arg follows.
Definition upnp.h:452
UPNP_OPTION_END
@ UPNP_OPTION_END
Terminate the VARARGs list.
Definition upnp.h:450
UpnpInitWithOptions
EXPORT_SPEC int UpnpInitWithOptions(const char *IfNames, unsigned short DestPort, unsigned int flags,...)
Initializes the library, passing the interface spec as a string.
UpnpGetServerPort
EXPORT_SPEC unsigned short UpnpGetServerPort(void)
Returns the internal server IPv4 UPnP listening port.
UPNP_E_SUCCESS
#define UPNP_E_SUCCESS
The operation completed successfully.
Definition upnp.h:96
UpnpGetServerIpAddress
EXPORT_SPEC const char * UpnpGetServerIpAddress(void)
Returns a local IPv4 listening ip address.
UpnpFinish
EXPORT_SPEC int UpnpFinish(void)
Terminate and clean up the library.
UPNP_FLAG_IPV6
@ UPNP_FLAG_IPV6
Definition upnp.h:436

# Client side operation

## Client: registration

A client (Control Point) program just needs to register its callback function to the library, and will immediately begin to receive network events (broadcast discovery messages):

int success = UpnpRegisterClient(mycallback, (void *)mycontext, &myhandle);
UpnpRegisterClient
EXPORT_SPEC int UpnpRegisterClient(Upnp_FunPtr Fun, const void *Cookie, UpnpClient_Handle *Hnd)
Registers a control point application with the UPnP Library.

This register the Upnp_FunPtr mycallback function to receive events. The mycontext cookie will be passed with each call. The return value UpnpClient_Handle myhandle can be used to reference the session (e.g. to stop it with UpnpUnRegisterClient).

## Client: discovery

A client could passively wait for network events to reveal the presence of UPnP devices on the network, but this might be rather slow, so you will normally want to trigger an UPnP search. This is done with the UpnpSearchAsync function.

int status = UpnpSearchAsync(hnd, windowsecs, "upnp:rootdevice", &mycontext);
UpnpSearchAsync
EXPORT_SPEC int UpnpSearchAsync(UpnpClient_Handle Hnd, int Mx, const char *Target, const void *cookie)
Searches for devices matching the given search target.

The first parameter is the client handle obtained with UpnpRegisterClient.

The second one is an indication to the network devices of the maximum period of time within with they should respond (they will use a random delay within the window to avoid collisions between answers).

The third parameter is the UPnP SSDP search type. Check the UPnP standard for possible values.

The last parameter is the value which will be passed to the callback function.

The same function is used for all callbacks from the library. Only the calls relative to the search (e.g. UPNP_DISCOVERY_SEARCH_RESULT) will use the search context value. Calls relative, for example, to spontaneous advertisements (e.g. UPNP_DISCOVERY_ADVERTISEMENT_ALIVE) will use the generic client context.

When the search window is expired, the last call relative to the search will be of type UPNP_DISCOVERY_SEARCH_TIMEOUT, with a NULL event parameter.

The important value inside the Upnp_Discovery structure received by the callback function is the Location field, which contains the URL of the description document for the device. You can download this document with the UpnpDownloadUrlItem helper function.

std::string data;
std::string content_type;
success = UpnpDownloadUrlItem(url, data, content_type);
UpnpDownloadUrlItem
EXPORT_SPEC int UpnpDownloadUrlItem(const char *url, char **outBuf, char *contentType)
Downloads a text file specified in a URL.

Once downloaded, the XML description document will need to be parsed to access the list of services and other information elements.

The following example uses the XML device description parser which comes with libnpupnp. This was not available in libupnp, so this sample is a bit farther from libupnp compatibility.

Note that, for a trivial program like the following, we could just pass nullptr for the context cookie addresses, which are not used.

The discovery results will usually be filtered by desired service availability, for presenting a choice to the user, or against a supplied device name.

#include <unistd.h>
#include <set>
#include <mutex>
#include <condition_variable>
#include <libupnpp/control/description.hxx>
#include <upnp/upnp.h>
#include <upnp/upnpdebug.h>
#include <upnp/upnptools.h>
#include <iostream>
static int clctxt;
std::set<std::string> locations;
std::mutex locations_mutex;
std::condition_variable locations_cv;
static bool searchdone;
static int mycallback(Upnp_EventType etyp, const void *evt, void *ctxt)
{
UpnpDiscovery *dsp = (UpnpDiscovery*)evt;
switch (etyp) {
case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
goto dldesc;
case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
break;
case UPNP_DISCOVERY_SEARCH_RESULT:
goto dldesc;
case UPNP_DISCOVERY_SEARCH_TIMEOUT:
{
std::unique_lock<std::mutex> loclock;
searchdone = true;
locations_cv.notify_all();
}
break;
default:
std::cerr << "Received surprising event type " << etyp << "\n";
break;
}
return 0;
dldesc:
std::unique_lock<std::mutex> loclock;
locations.insert(dsp->Location);
locations_cv.notify_all();
return 0;
}
int main(int argc, char **argv)
{
UpnpSetLogFileNames("", "");
UpnpSetLogLevel(UPNP_ERROR);
UpnpInitLog();
int success = UpnpInitWithOptions("*", 0, UPNP_FLAG_IPV6, UPNP_OPTION_END);
if (success != UPNP_E_SUCCESS) {
std::cerr << "init failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
return 1;
}
UpnpClient_Handle chdl;
success = UpnpRegisterClient(mycallback, &clctxt, &chdl);
if (success != UPNP_E_SUCCESS) {
std::cerr << "register client failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
return 1;
}
int searchctxt;
success = UpnpSearchAsync(chdl, 3, "upnp:rootdevice", &searchctxt);
std::unique_lock<std::mutex> locklocs(locations_mutex);
for (;;) {
locations_cv.wait(locklocs);
if (searchdone) {
break;
}
for (auto it = locations.begin(); it != locations.end();) {
std::string data;
std::string ct;
int success = UpnpDownloadUrlItem(*it, data, ct);
if (success != UPNP_E_SUCCESS) {
std::cerr << "UpnpDownloadUrlItem failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
continue;
}
UPnPClient::UPnPDeviceDesc desc(*it, data);
if (!desc.ok) {
std::cout << "Description parse failed for " << *it << "\n";
} else {
std::cout << "Got " << data.size() <<
" bytes of description data. " << " friendly name: " <<
desc.friendlyName << "\n";
}
it = locations.erase(it);
}
}
UpnpFinish();
return 0;
}
Upnp_Discovery
Definition upnp.h:547
Upnp_Discovery::Location
char Location[LINE_SIZE]
The URL to the UPnP description document for the device.
Definition upnp.h:567
Upnp_EventType
enum Upnp_EventType_e Upnp_EventType
UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE
@ UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE
Definition upnp.h:362
UPNP_DISCOVERY_ADVERTISEMENT_ALIVE
@ UPNP_DISCOVERY_ADVERTISEMENT_ALIVE
Definition upnp.h:357
UPNP_DISCOVERY_SEARCH_RESULT
@ UPNP_DISCOVERY_SEARCH_RESULT
Definition upnp.h:368
UPNP_DISCOVERY_SEARCH_TIMEOUT
@ UPNP_DISCOVERY_SEARCH_TIMEOUT
Definition upnp.h:373
UpnpClient_Handle
int UpnpClient_Handle
Definition upnp.h:325
UPNP_ERROR
@ UPNP_ERROR
Definition upnpdebug.h:74

## Client: control

Among the interesting fields in the description document which was parsed above are the controlURL ones, which define for each service the endpoint for action messages.

Once this is determined, and if you know the actions and parameters supported by the service, you can use the UpnpSendAction call to request that the device does something for you:

status = UpnpSendAction(handle, headerString, actionURL, serviceType,
actionName, actionParams, responseData, errorCodep, errorDescr);
UpnpSendAction
EXPORT_SPEC int UpnpSendAction(UpnpClient_Handle Hnd, const std::string &headerString, const std::string &actionURL, const std::string &serviceType, const std::string &actionName, const std::vector< std::pair< std::string, std::string > > &actionParams, std::vector< std::pair< std::string, std::string > > &responseData, int *errcodep, std::string &errdesc)
Sends a message to change a state variable in a service.

Refer to the UpnpSendAction reference documentation for details on the parameters, we will just add a few remarks here.

headerString is a value for the SOAP header, and is generally not needed and empty.

actionURL is the field named controlURL in the XML description document and its libupnpp parsed version.

serviceType also comes from the parsed description.

actionParams and responseData are the call arguments and return data as C++ vectors of name/value pairs. The arguments should be as defined by the service. For dynamic interface programs, this can be determined by downloading and parsing the service description document, the URL of which is found in the device description. However, for known services, you will usually just hard-code the argument list.

## Client: events

A client can opt to be informed of the state changes in a service by subscribing to its events, using the UpnpSubscribe function.

int status = UpnpSubscribe(handle, eventURL, timeoutp, subs_id);
UpnpSubscribe
EXPORT_SPEC int UpnpSubscribe(UpnpClient_Handle Hnd, const char *EvtUrl, int *TimeOut, Upnp_SID &SubsId)
Registers a control point to receive event notifications from another device.

The eventURL value comes from the device description document.

timeoutp points to an integer containing the subscription timeout in seconds. This is the time in seconds, typically a fraction of an hour to a few hours, after which the device will cancel the subscription if it has not been renewed. The library can change the value passed by the client code.

The library performs automatic renewals of subscriptions internally, the client code does not need to deal with them.

subs_id is an Upnp_SID character array, which will contain the subscription identifier (a UUID) after the call. This value will be passed with the event callbacks, and can be used to cancel the subscription, using UpnpUnSubscribe.

Once the client is subscribed to the events fro a service, its callback function will be called with an event type of UPNP_EVENT_RECEIVED and a Upnp_Event data structure. The latter contains a map of the changed variables names and the new values.

The first callback, during or just after the UnpnSubscribe call will contain the name and values of all the service eventable state variables.

# Device side operation

## Device: registration

Registering a root device is done by calling the UpnpRegisterRootDevice2 function.

int status = UpnpRegisterRootDevice2(
descriptionType, description, 0, 0, mycallback, mycontext, myhandle)
UpnpRegisterRootDevice2
EXPORT_SPEC int UpnpRegisterRootDevice2(Upnp_DescType descriptionType, const char *description, size_t bufferLen, int ignored, Upnp_FunPtr Fun, const void *Cookie, UpnpDevice_Handle *Hnd)
Registers a device application with the UPnP Library. Similar to UpnpRegisterRootDevice,...

descriptionType is a Upnp_DescType member, and indicates if the description character pointer contains an URL, an inline character string, or a file name. In the two last cases, the description will be served from an internal buffer, with an URL path in the root of the WEB server tree. The file name will be the same as the input file name, or description.xml if the description is passed as a buffer.

Multiple root devices can be registered in a single instance of the library. You can't pass the description documents as memory buffers in this case (because of the fixed file name).

The two parameters after the description pointer are unused.

mycallback and mycontext are the callback and callback context parameter which will be used when the library calls the device code.

myhandle can be used to reference this device registration, for example for unsetting it with UpnpUnRegisterRootDevice

## Device: discovery

UPnP device send advertisements about their presence when they start up, and later, at regular intervals. For a libnpupnp device, this process is started by a call to UpnpSendAdvertisement.

int status = UpnpSendAdvertisement(handle, expiration);
UpnpSendAdvertisement
EXPORT_SPEC int UpnpSendAdvertisement(UpnpDevice_Handle Hnd, int Exp)
Sends out the discovery announcements for all devices and services for a device.

expiration is the time in seconds after which clients should consider the advertisement as expired. Set it to 0 to use the library default (1800 S). While the process lives, the library will automatically renew the advertisement before the expiration time, so you do not need to make further calls.

Without this call, the device will not advertise itself spontaneously on the network, but it will still answer search requests, so it is not invisible.

## Device: WEB server and Virtual Directories

### Device: WEB server: local file system

You can set the internal HTTP server to serve files from the local file system by calling UpnpSetWebServerRootDir.

int status = UpnpSetWebServerRootDir(rootDir);
UpnpSetWebServerRootDir
EXPORT_SPEC int UpnpSetWebServerRootDir(const char *rootDir)
Sets the document root directory for the internal WEB server.

If this is not called, the internal HTTP server will not allow any access to the local file system. In this case, you will need to set up a Virtual Directory to hold the service description files, which is actually the common way to do things.

The call will accept a relative rootDir parameter, but using one would be a bad idea in most cases.

See here for a small sample device storing its description files in the file system.

### Device: Virtual Directories

The integrated WEB server has the capability to serve documents from application-provided data. This is performed by:

- Defining a set of callbacks to perform the needed operations on virtual files
- Defining directory paths where file operations should be virtual.

All subpaths of a defined virtual path are considered virtual, not only the immediate file children.

Virtual Paths are checked before file system paths and will hide the whole subtree (in case UpnpSetWebServerRootDir() has been called in a way which may result in collisions).

#### Virtual Directories: defining the callbacks

Let us see for example how to set the VDCallback_GetInfo function which is always the first one that the WEB server calls when processing a document request. The function prototype is as follows:

int (*VDCallback_GetInfo)(
const char *filepath,
struct File_Info *info,
const void *cookie,
const void **request_cookiep
);
File_Info
Definition upnp.h:643
VDCallback_GetInfo
int(* VDCallback_GetInfo)(const char *filename, struct File_Info *info, const void *cookie, const void **request_cookiep)
Virtual Directory function prototype for the "get file information" callback. This is guaranteed to ...
Definition upnp.h:1970

The filepath parameter is an absolute file system path.

The File_Info info pointer is used both to supply information about the HTTP request (its headers), and to return information about the file and possibly additional headers to set in the response.

cookie is an application context pointer set when defining the path as a Virtual Directory.

request_cookiep is specific of this request and can be set the VDCallback_GetInfo to track the different calls associated with this specific request.

The callback is defined through the following call:

int status = UpnpVirtualDir_set_GetInfoCallback(callback);
UpnpVirtualDir_set_GetInfoCallback
EXPORT_SPEC int UpnpVirtualDir_set_GetInfoCallback(VDCallback_GetInfo callback)
Sets the get_info callback function to be used to access a virtual directory.

There are similar definitions for open, read, write (unused at the moment), seek and close.

It is also possible to set all the callbacks in one call by using the UpnpVirtualDirCallbacks structure and UpnpSetVirtualDirCallbacks().

#### Virtual Directories: defining a virtual path

You associate a path with the Virtual Directory system by using UpnpAddVirtualDir :

int status = UpnpAddVirtualDir(dirpath, cookie, oldcookiep);
UpnpAddVirtualDir
EXPORT_SPEC int UpnpAddVirtualDir(const char *dirName, const void *cookie, const void **oldcookie)
Adds a virtual directory mapping.

The cookie will be set in further file-related calls under this path. The pointer-to-pointer oldcookiep can be set to retrieve the old value of the cookie in case the path was already associated. You can set it to nullptr if you're not interested.

You can also erase an association with UpnpRemoveVirtualDir, and get rid of them all with UpnpRemoveAllVirtualDirs.

See this file in the samples for an example of a small bogus but functional device using a virtual directory to store its description files.

The virtual directory implementation in the sample is actually file-system-backed to make things simple, but this does not change the principle.

## Device: actions

Actions requested by a Control Point to a specific device service action will be received by the device callback with a type of UPNP_CONTROL_ACTION_REQUEST, and an associated Upnp_Action_Request data structure, used both to define what is requested, and the data which should be returned.

The main input fields of the structure are the following:

- DevUDN, ServiceID, and ActionName define the entry point.
- args, a vector of (name,value) pairs holds the input arguments parsed from the XML payload. The latter is also accessible as xmlAction in case the parser missed something.
- CtrlPtIPAddr (a_sockaddr_ pointer), and Os (client string) describe the remote client.

On output:

- ErrCode (HTTP error status) and ErrStr can be used to describe an error.
- resdata is a vector of (name,value) pairs to hold the response values.
- xmlResponse is an alternative way to return the data as an XML SOAP payload. If this is set, resdata is ignored. This may be easier to use for some applications when transitionning from pupnp.

See [this file in the samples] (https://framagit.org/medoc92/npupnp-samples/-/tree/master/src/device.cpp) for an example of a small bogus but functional device processing some actions.

## Device: events

### Device: events: subscription

A device implementation will be informed of a subscription request by a callback of type UPNP_EVENT_SUBSCRIPTION_REQUEST. The associated Upnp_Subscription_Request data structure holds the device and service identifiers, and the subscription ID set by the library.

The subscription can be rejected by returning an error, for example if there is an inconsistency in the device/service data, but will more commonly be accepted, in which case the initial set of state variable values should be returned by a call to UpnpAcceptSubscription or UpnpAcceptSubscriptionXML which only differ by the format of the returned state data.

This call should be performed from the UPNP_EVENT_SUBSCRIPTION_REQUEST callback.

int ret = UpnpAcceptSubscription(handle, UDN, serviceId, names, values, cnt, SID);
int ret UpnpAcceptSubscriptionXML(handle, UDN, serviceId, propertyset, SID);
UpnpAcceptSubscription
EXPORT_SPEC int UpnpAcceptSubscription(UpnpDevice_Handle Hnd, const char *DevID, const char *ServName, const char **VarName, const char **NewVal, int cVariables, const Upnp_SID &SubsId)
Accepts a subscription request and sends out the current state of the eventable variables for a servi...
UpnpAcceptSubscriptionXML
EXPORT_SPEC int UpnpAcceptSubscriptionXML(UpnpDevice_Handle Hnd, const char *DevID, const char *ServName, const std::string &propertyset, const Upnp_SID &SubsId)

UDN, serviceId, and SID come from the input Upnp_Subscription_Request structure.

names, values are parallel arrays of C string pointers, of size cnt.

Alternatively, propertyset is an XML GENA property set ready for sending back (see the UPnP standard).

### Device: events: eventing

A device generates UPnP events by calls to UpnpNotify or UpnpNotifyXML, which only differ in the returned data format:

int ret = UpnpNotify(handle, UDN, serviceId, names, values, count);
int ret UpnpNotifyXML(handle, UDN, serviceId, propset)
UpnpNotifyXML
EXPORT_SPEC int UpnpNotifyXML(UpnpDevice_Handle, const char *DevID, const char *ServName, const std::string &propset)
UpnpNotify
EXPORT_SPEC int UpnpNotify(UpnpDevice_Handle, const char *DevID, const char *ServName, const char **VarName, const char **NewVal, int cVariables)
Sends out an event change notification to all control points subscribed to a particular service.

The device application code only has to call one of these once for a state event. The library will arrange to send the event to all the currently suscribed control points.

Our bogus [sample device] (https://framagit.org/medoc92/npupnp-samples/-/tree/master/src/device.cpp) also has eventing code, which can be triggered by it [associated client] (https://framagit.org/medoc92/npupnp-samples/-/tree/master/src/ctl.cpp) using an action to change a variable value.