diff options
author | Jens Georg <mail@jensge.org> | 2022-01-03 11:07:59 +0100 |
---|---|---|
committer | Jens Georg <mail@jensge.org> | 2022-01-03 19:10:51 +0100 |
commit | d2ad83d588759d005c7a3bae87f7523666e9a733 (patch) | |
tree | 35de8ceeb48fe817dfc03c5026150b7c35af1c8f /doc | |
parent | 79a2cb24cd0079e9a025809d11dce13edce68bdd (diff) | |
download | gupnp-d2ad83d588759d005c7a3bae87f7523666e9a733.tar.gz |
docs: Use gi-docgen instead of gtk-doc
Diffstat (limited to 'doc')
-rw-r--r-- | doc/choosing-a-context-manager.md | 20 | ||||
-rw-r--r-- | doc/client-tutorial.md | 215 | ||||
-rw-r--r-- | doc/glossary.md | 115 | ||||
-rw-r--r-- | doc/gupnp.toml.in | 54 | ||||
-rw-r--r-- | doc/images/gupnp-logo-short.svg | 126 | ||||
-rw-r--r-- | doc/meson.build | 67 | ||||
-rw-r--r-- | doc/server-tutorial.md | 373 | ||||
-rw-r--r-- | doc/urlmap.js | 4 |
8 files changed, 944 insertions, 30 deletions
diff --git a/doc/choosing-a-context-manager.md b/doc/choosing-a-context-manager.md new file mode 100644 index 0000000..bc98595 --- /dev/null +++ b/doc/choosing-a-context-manager.md @@ -0,0 +1,20 @@ +----- +Title: Choosing a Context Manager Implementation +----- + +# Choosing a Context Manager Implementation + +Ususally it is fine to trust the auto-detection. If the operating system is not Linux, +there is only one choice anyway. + +For Linux, four different implementations exist: + + - A basic polling implementation, the fall-back if nothing else works. + - A Netlink-based implementation + - Using NetworkManager to identify available network interfaces + - Using Connman to identify the available interfaces + - An Android-specific implementation + +With the exception of Android, It is generally recommended to use the Netlink-based implementation. +It should co-exist with any other network management implementation. + diff --git a/doc/client-tutorial.md b/doc/client-tutorial.md new file mode 100644 index 0000000..5d0b71e --- /dev/null +++ b/doc/client-tutorial.md @@ -0,0 +1,215 @@ +--- +Title: Interacting with remote UPnP devices +--- + +# UPnP Client Tutorial + +This chapter explains how to write an application which fetches the external IP address +from an UPnP-compliant modem. To do this, a Control Point is created, which searches for +services of the type `urn:schemas-upnp-org:service:WANIPConnection:1` which is part of +the Internet Gateway Devce specification. + +As services are discovered, ServiceProxy objects are created by GUPnP to allow interaction +with the service, on which we can invoke the action `GetExternalIPAddress` to fetch the +external IP address. + +## Finding Services + +First, we initialize GUPnP and create a control point targeting the service type. +Then we connect a signal handler so that we are notified when services we are interested in +are found. + + +```c +#include <ibgupnp/gupnp-control-point.h> + +static GMainLoop *main_loop; + +static void +service_proxy_available_cb (GUPnPControlPoint *cp, + GUPnPServiceProxy *proxy, + gpointer userdata) +{ + /* ... */ +} + +int +main (int argc, char **argv) +{ + GUPnPContext *context; + GUPnPControlPoint *cp; + + /* Create a new GUPnP Context. By here we are using the default GLib main + context, and connecting to the current machine's default IP on an + automatically generated port. */ + context = gupnp_context_new (NULL, 0, NULL); + + /* Create a Control Point targeting WAN IP Connection services */ + cp = gupnp_control_point_new + (context, "urn:schemas-upnp-org:service:WANIPConnection:1"); + + /* The service-proxy-available signal is emitted when any services which match + our target are found, so connect to it */ + g_signal_connect (cp, + "service-proxy-available2, + G_CALLBACK (service_proxy_available_cb), + NULL); + + /* Tell the Control Point to start searching */ + gssdp_resource_browser_set_active (GSSDP_RESOURCE_BROWSER (cp), TRUE); + + /* Enter the main loop. This will start the search and result in callbacks to + service_proxy_available_cb. */ + main_loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (main_loop); + + /* Clean up */ + g_main_loop_unref (main_loop); + g_object_unref (cp); + g_object_unref (context); + + return 0; +} +``` + +## Invoking Actions +Now we have an application which searches for the service we specified and +calls `service_proxy_available_cb` for each one it +found. To get the external IP address we need to invoke the +`GetExternalIPAddress` action. This action takes no in +arguments, and has a single out argument called "NewExternalIPAddress". +GUPnP has a set of methods to invoke actions where you pass a +`NULL`-terminated varargs list of (name, GType, value) +tuples for the in arguments, then a `NULL`-terminated +varargs list of (name, GType, return location) tuples for the out +arguments. + +```c +static void +service_proxy_available_cb (GUPnPControlPoint *cp, + GUPnPServiceProxy *proxy, + gpointer userdata) +{ + GError *error = NULL; + char *ip = NULL; + GUPnPServiceProxyAction *action = NULL; + + action = gupnp_service_proxy_action_new ( + /* Action name */ + "GetExternalIPAddress", + /* IN args */ + NULL); + gupnp_service_proxy_call_action (proxy, + action, + NULL, + &error); + if (error != NULL) { + goto out; + } + + gupnp_service_proxy_action_get_result (action, + /* Error location */ + &error, + /* OUT args */ + "NewExternalIPAddress", + G_TYPE_STRING, &ip, + NULL); + + if (error == NULL) { + g_print ("External IP address is %s\n", ip); + g_free (ip); + } + +out: + if (error != NULL) { + g_printerr ("Error: %s\n", error->message); + g_error_free (error); + } + + gupnp_service_proxy_action_unref (action); + g_main_loop_quit (main_loop); +} +``` + +Note that `gupnp_service_proxy_call_action()` blocks until the service has +replied. If you need to make non-blocking calls then use +`gupnp_service_proxy_call_action_async()`, which takes a callback that will be +called from the mainloop when the reply is received. + + +## Subscribing to state variable change notifications +It is possible to get change notifications for the service state variables +that have attribute `sendEvents="yes"`. We'll demonstrate +this by modifying `service_proxy_available_cb` and using +`gupnp_service_proxy_add_notify()` to setup a notification callback: + + +```c +static void +external_ip_address_changed (GUPnPServiceProxy *proxy, + const char *variable, + GValue *value, + gpointer userdata) +{ + g_print ("External IP address changed: %s\n", g_value_get_string (value)); +} + +static void +service_proxy_available_cb (GUPnPControlPoint *cp, + GUPnPServiceProxy *proxy, + gpointer userdata) +{ + g_print ("Found a WAN IP Connection service\n"); + + gupnp_service_proxy_set_subscribed (proxy, TRUE); + if (!gupnp_service_proxy_add_notify (proxy, + "ExternalIPAddress", + G_TYPE_STRING, + external_ip_address_changed, + NULL)) { + g_printerr ("Failed to add notify"); + } +} +``` + +## Generating wrappers + +Using `gupnp_service_proxy_call_action()` and `gupnp_service_proxy_add_notify()` +can become tedious, because of the requirement to specify the types and deal +with GValues. An +alternative is to use `gupnp-binding-tool`, which +generates wrappers that hide the boilerplate code from you. Using a +wrapper generated with prefix "ipconn" would replace +`gupnp_service_proxy_call_action()` with this code: + +```c +ipconn_get_external_ip_address (proxy, &ip, &error); +``` + +State variable change notifications are friendlier with wrappers as well: + +```c +static void +external_ip_address_changed (GUPnPServiceProxy *proxy, + const gchar *external_ip_address, + gpointer userdata) +{ + g_print ("External IP address changed: '%s'\n", external_ip_address); +} + +static void +service_proxy_available_cb (GUPnPControlPoint *cp, + GUPnPServiceProxy *proxy + gpointer userdata) +{ + g_print ("Found a WAN IP Connection service\n"); + + gupnp_service_proxy_set_subscribed (proxy, TRUE); + if (!ipconn_external_ip_address_add_notify (proxy, + external_ip_address_changed, + NULL)) { + g_printerr ("Failed to add notify"); + } +} +``` + diff --git a/doc/glossary.md b/doc/glossary.md new file mode 100644 index 0000000..0b9dbb2 --- /dev/null +++ b/doc/glossary.md @@ -0,0 +1,115 @@ +--- +Title: UPnP Glossary +--- + +# Action + +> An Action is a method call on a Service, which encapsulated a single piece of +functionality. Actions can have multiple input and output arguments, and +can return error codes. UPnP allows one of the output arguments to be +marked as the return value, but GUPnP doesn't treat return values specially. + +> Every action argument has a related State Variable, +which determines the type of the argument. Note that even if the argument +wouldn't need a state variable it is still required, due to historical +reasons. + +# Control Point + +> A Control Point is an entity on the network which +communicates with other Devices and +Services. In the client/server model the control +point is a client and the Service is a server, +although it is common for devices to also be a control point because +whilst a single control point/service connection is client/server, the +UPnP network as whole is peer-to-peer. + +# Device +> A Device is an entity on the network which +communicates using the UPnP standards. This can be a dedicated physical +device such as a router or printer, or a PC which is running software +implementing the UPnP standards. + +> A Device can contain sub-devices, for example a combination +printer/scanner could appear as a general device with a printer +sub-device and a scanner sub-device. + +> Every device has zero or more Services. UPnP defines many standard +device types, which specify services which are required to be implemented. +Alternatively, a non-standard device type could be used. Examples of +standard device types are `MediaRenderer` or +`InternetGatewayDevice`. + +# DIDL-Lite + +> Digital Item Declaration Language - Lite + +> An XML schema used to represent digital content metadata. Defined by +the UPnP Forum. + +# Service + +> A Service is a collection of related methods +(called Actions) and public variables (called +State Variables) which together form a logical interface. +> UPnP defines standard services that define actions and variables which +must be present and their semantics. Examples of these are +`AVTransport` and `WANIPConnection`. + +See also: + +- [Action](#action) +- [Device](#device) +- [State Variable](#state-variable) + +# SCDP +> Service Control Protocol Document + + +> An XML document which defines the set of <glossterm>Actions</glossterm> +and <glossterm>State Variables</glossterm> that a +<glossterm>Service</glossterm> implements. + +See also: + +- [Action](#action) +- [Device](#device) +- [State Variable](#state-variable) + +# SSDP +> <glossterm>Simple Service Discovery Protocol</glossterm> + +> UPnP device discovery protocol. Specifies how <glossterm>Devices</glossterm> +advertise their <glossterm>Services</glossterm> in the network and also how +<glossterm>Control Points</glossterm> search for +services and devices respond. + +See also: + +- [Device](#device) +- [Action](#controlpoint) +- [Service](#service) + +# State Variable + +> A <firstterm>State Variable</firstterm> is a public variable exposing some +aspect of the service's state. State variables are typed and optionally +are <firstterm>evented</firstterm>, which means that any changes will be +notified. Control points are said to <firstterm>subscribe</firstterm> to +a state variable to receive change notifications. + + +# UDN +> Unique Device Name + +> An unique identifier which is <emphasis>unique</emphasis> for every +device but <emphasis>never changes</emphasis> for each particular +device. + +> A common practise is to generate a unique UDN on first boot from a +random seed, or use some unique and persistent property such as the +device's MAC address to create the UDN. + +See also: + +- [Device](#device) diff --git a/doc/gupnp.toml.in b/doc/gupnp.toml.in new file mode 100644 index 0000000..e416445 --- /dev/null +++ b/doc/gupnp.toml.in @@ -0,0 +1,54 @@ +[library] +namespace = "GUPnP" +version = "@VERSION@" +browse_url = "https://gitlab.gnome.org/GNOME/gssdp/" +repository_url = "https://gitlab.gnome.org/GNOME/gssdp.git" +website_url = "https://gupnp.org" +logo_url = "gupnp-logo-short.svg" +license = "LGPL-2.1-or-later" +description = "UPnP implementation using GObject" +dependencies = [ "GObject-2.0", "GSSDP-1.2", "Soup-2.4", "libxml2-2.0" ] +devhelp = true +search_index = true +authors = "The GUPnP developers" + +[theme] +name="basic" +show_index_summary = true + +[source-location] +base_url = "https://gitlab.gnome.org/GNOME/gupnp/-/blob/master" + +[dependencies."GObject-2.0"] +name = "GObject" +description = "The base type system library" +docs_url = "https://developer.gnome.org/gobject/stable" + +[dependencies."GSSDP-1.2"] +name = "GSSDP" +description = "SSDP implementation using GObject" +docs_url = "https://gnome.pages.gitlab.gnome.org/gssdp/docs/" + +[dependencies."Soup-2.4"] +name = "Soup" +description = "A HTTP handling library" +docs_url = "https://developer.gnome.org/libsoup/stable" + +[dependencies."libxml2-2.0"] +name = "LibXML2" +description = "A XML handling library" +docs_url = "http://www.xmlsoft.org/html/index.html" + +[extra] +content_files = [ + "client-tutorial.md", + "server-tutorial.md", + "choosing-a-context-manager.md", + "glossary.md" +] + +content_images = [ + "images/gupnp-logo-short.svg" +] + +urlmap_file = "urlmap.js" diff --git a/doc/images/gupnp-logo-short.svg b/doc/images/gupnp-logo-short.svg new file mode 100644 index 0000000..6386c74 --- /dev/null +++ b/doc/images/gupnp-logo-short.svg @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + id="svg4194" + version="1.1" + inkscape:version="1.1 (c68e22c387, 2021-05-23)" + width="89.339256mm" + height="37.64888mm" + viewBox="0 0 316.55642 133.40154" + sodipodi:docname="gupnp-logo-short.svg" + inkscape:export-filename="/home/jgeorg/gupnp-logo-v1.svg.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <metadata + id="metadata4200"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <cc:license + rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" /> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/publicdomain/zero/1.0/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + </cc:License> + </rdf:RDF> + </metadata> + <defs + id="defs4198" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1011" + id="namedview4196" + showgrid="false" + showguides="true" + inkscape:snap-page="false" + inkscape:zoom="1.6897519" + inkscape:cx="106.82042" + inkscape:cy="36.691777" + inkscape:window-x="0" + inkscape:window-y="32" + inkscape:window-maximized="1" + inkscape:current-layer="svg4194" + inkscape:guide-bbox="true" + inkscape:pagecheckerboard="0" + units="mm" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:document-units="mm"> + <inkscape:grid + type="xygrid" + id="grid5354" + originx="-2.7774772" + originy="-2.2013303" + spacingx="1" + spacingy="1" /> + <sodipodi:guide + position="311.44403,45.237576" + orientation="0,1" + id="guide4237" + inkscape:locked="false" /> + <sodipodi:guide + position="316.24124,64.031713" + orientation="1,0" + id="guide14" /> + </sodipodi:namedview> + <path + inkscape:connector-curvature="0" + id="path4222" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:111.914px;line-height:125%;font-family:moderna;-inkscape-font-specification:moderna;letter-spacing:0px;word-spacing:0px;fill:#204a87;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 59.874011,60.321668 q 0,12.7582 -9.176951,21.487495 9.288865,8.841209 9.288865,21.711327 0,16.11562 -13.205856,24.84491 -7.722069,5.03614 -16.675192,5.03614 -16.003708,0 -24.8449171,-13.31778 -5.1480458,-7.61015 -5.1480458,-16.56327 0,-5.819533 2.2382807,-11.191407 L 16.33945,97.92479 q -1.11914,2.68593 -1.11914,5.5957 0,6.15527 4.364647,10.408 4.476562,4.36465 10.51992,4.36465 6.043358,0 10.408006,-4.36465 4.364647,-4.36464 4.364647,-10.408 0,-9.848438 -9.288865,-13.76543 -2.797851,0.55957 -5.595702,0.55957 -12.534373,0 -21.2636682,-8.729295 Q 0,72.85604 0,60.321668 0,47.899209 8.7292948,39.169914 17.570504,30.440619 29.992963,30.440619 l 8.729295,-20.7040974 11.862888,5.0361324 -7.833983,18.57773 q 7.833983,3.805078 12.422459,11.07949 4.700389,7.274413 4.700389,15.891794 z m -15.108395,0 q 0,-6.043358 -4.364647,-10.408006 -4.364648,-4.364647 -10.408006,-4.364647 -6.155272,0 -10.51992,4.364647 -4.364647,4.252734 -4.364647,10.408006 0,6.155272 4.364647,10.519919 4.364648,4.364648 10.51992,4.364648 6.155272,0 10.408006,-4.364648 4.364647,-4.364647 4.364647,-10.519919 z" /> + <path + inkscape:connector-curvature="0" + id="path4224" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:111.914px;line-height:125%;font-family:moderna;-inkscape-font-specification:moderna;letter-spacing:0px;word-spacing:0px;fill:#204a87;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 97.753413,73.527524 q -6.15527,0 -10.519918,-4.252733 -4.252733,-4.364648 -4.252733,-10.51992 L 82.868848,0 H 67.760452 v 58.754871 q 0,12.422459 8.729296,21.151754 8.841209,8.729295 21.263665,8.729295 12.422457,0 21.151757,-8.729295 8.8412,-8.729295 8.8412,-21.151754 V 0 h -15.10839 v 58.754871 q 0,6.155272 -4.36465,10.51992 -4.36465,4.252733 -10.519917,4.252733 z" /> + <path + inkscape:connector-curvature="0" + id="path4226" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:111.914px;line-height:125%;font-family:moderna;-inkscape-font-specification:moderna;letter-spacing:0px;word-spacing:0px;fill:#204a87;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 190.13495,30.216791 Q 190.24687,17.682419 181.51757,9.0650376 172.78828,0.3357421 160.2539,0.2238281 L 135.5209,0.111914 V 88.63592 h 15.22031 V 59.985926 h 9.40078 q 12.42246,0 21.15175,-8.617381 8.7293,-8.729296 8.84121,-21.151754 z m -15.10839,-0.111914 q -0.11192,6.267186 -4.36465,10.51992 -4.25273,4.252733 -10.51992,4.252733 H 150.6293 V 15.22031 l 9.6246,0.111914 q 6.26719,0.111914 10.51992,4.364647 4.36465,4.252734 4.25274,10.408006 z" /> + <path + inkscape:connector-curvature="0" + id="path4228" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:111.914px;line-height:125%;font-family:moderna;-inkscape-font-specification:moderna;letter-spacing:0px;word-spacing:0px;fill:#204a87;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 221.80976,45.549015 q 6.04336,0 10.40801,4.364647 4.36465,4.364648 4.36465,10.408006 l 0.11191,28.314252 h 15.1084 V 60.321668 q 0,-12.422459 -8.84121,-21.151754 -8.7293,-8.729295 -21.15176,-8.729295 -12.42246,0 -21.26366,8.729295 -8.7293,8.729295 -8.7293,21.151754 V 88.63592 h 15.1084 V 60.321668 q 0,-6.155272 4.36464,-10.408006 4.36465,-4.364647 10.51992,-4.364647 z" /> + <path + inkscape:connector-curvature="0" + id="path4230" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:111.914px;line-height:125%;font-family:moderna;-inkscape-font-specification:moderna;letter-spacing:0px;word-spacing:0px;fill:#204a87;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 314.19132,30.216791 Q 314.30323,17.682419 305.57394,9.0650376 296.84464,0.3357421 284.31027,0.2238281 L 259.57727,0.111914 V 88.63592 h 15.22031 V 59.985926 h 9.40078 q 12.42245,0 21.15175,-8.617381 8.7293,-8.729296 8.84121,-21.151754 z m -15.1084,-0.111914 q -0.11191,6.267186 -4.36464,10.51992 -4.25274,4.252733 -10.51992,4.252733 h -9.5127 V 15.22031 l 9.62461,0.111914 q 6.26719,0.111914 10.51992,4.364647 4.36465,4.252734 4.25273,10.408006 z" /> + <rect + style="fill:#204a87;fill-opacity:1;stroke:#204a87;stroke-width:0.630386;stroke-linejoin:miter;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1" + id="rect5352" + width="248.92432" + height="15.959336" + x="67.316925" + y="104.33861" + ry="0" /> +</svg> diff --git a/doc/meson.build b/doc/meson.build index c3b8fb3..c5ebbc0 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,5 +1,6 @@ entities = configuration_data() entities.set('VERSION', meson.project_version()) + version_xml = configure_file(input: 'version.xml.in', output: 'version.xml', configuration: entities) @@ -12,36 +13,42 @@ docbook_man_page = configure_file( ) if get_option('gtk_doc') - gnome.gtkdoc('gupnp', - content_files : files( - 'client-tutorial.xml', - 'fdl-1.1.xml', - 'glossary.xml', - 'gupnp-docs.xml', - 'overview.xml', - 'server-tutorial.xml' - ), - main_xml : 'gupnp-docs.xml', - src_dir : ['libgupnp'], - dependencies : [libgupnp, version_xml, docbook_man_page], - scan_args : ['--ignore-decorators', 'G_DEPRECATED|G_GNUC_DEPRECATED,G_DEPRECATED_FOR'], - ignore_headers : [ - 'gena-protocol.h', - 'xml-util.h', - 'gvalue-util.h', - 'http-headers.h', - 'gupnp-context-private.h', - 'gupnp-linux-context-manager.h', - 'gupnp-network-manager.h', - 'gupnp-unix-context-manager.h', - 'gupnp-device-info-private.h', - 'gupnp-error-private.h', - 'gupnp-resource-factory-private.h', - 'gupnp-service-introspection-private.h', - 'gupnp-service-proxy-action-private.h', - 'gupnp-types-private.h' - ], - install : true) + gidocgen = find_program('gi-docgen', required: true) + + gupnp_toml = configure_file ( + input: 'gupnp.toml.in', + output: 'gupnp.toml', + configuration: entities + ) + + docs_dir = join_paths(get_option('prefix'), get_option('datadir')) / 'doc/gupnp-1.2/reference' + + custom_target( + 'gupnp-doc', + input: [ gupnp_toml, gir[0] ], + output: 'GUPnP', + command : [ + gidocgen, + 'generate', + '--quiet', + '--add-include-path=@0@'.format(meson.current_build_dir() / '../libgupnp'), + '--config', gupnp_toml, + '--output-dir=@OUTPUT@', + '--no-namespace-dir', + '--content-dir=@0@'.format(meson.current_source_dir()), + '@INPUT1@', + ], + depend_files : [ + gupnp_toml, + 'client-tutorial.md', + 'server-tutorial.md', + 'choosing-a-context-manager.md', + 'glossary.md', + ], + build_by_default: true, + install: true, + install_dir : docs_dir, + ) endif xsltproc = find_program('xsltproc', required: false) diff --git a/doc/server-tutorial.md b/doc/server-tutorial.md new file mode 100644 index 0000000..d0c0ce2 --- /dev/null +++ b/doc/server-tutorial.md @@ -0,0 +1,373 @@ +--- +Title: Implementing UPnP devices +--- + +# UPnP Server Tutorial + +This chapter explains how to implement an UPnP service using GUPnP. For +this example we will create a virtual UPnP-enabled light bulb. + +Before any code can be written, the device and services that it implement +need to be described in XML. Although this can be frustrating, if you are +implementing a standardised service then the service description is +already written for you and the device description is trivial. UPnP has +standardised Lighting Controls, so we'll be using the device and service types defined +there. + +## Defining the Device + +The first step is to write the _device description_ +file. This is a short XML document which describes the device and what +services it provides (for more details see the [UPnP Device Architecture specification](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf), section 2.1). We'll be using +the `BinaryLight1` device type, but if none of the +existing device types are suitable a custom device type can be created. + +```xml +<?xml version="1.0" encoding="utf-8"?> +<root xmlns="urn:schemas-upnp-org:device-1-0"> + <specVersion> + <major>1</major> + <minor>0</minor> + </specVersion> + + <device> + <deviceType>urn:schemas-upnp-org:device:BinaryLight:1</deviceType> + <friendlyName>Kitchen Lights</friendlyName> + <manufacturer>OpenedHand</manufacturer> + <modelName>Virtual Light</modelName> + <UDN>uuid:cc93d8e6-6b8b-4f60-87ca-228c36b5b0e8</UDN> + + <serviceList> + <service> + <serviceType>urn:schemas-upnp-org:service:SwitchPower:1</serviceType> + <serviceId>urn:upnp-org:serviceId:SwitchPower:1</serviceId> + <SCPDURL>/SwitchPower1.xml</SCPDURL> + <controlURL>/SwitchPower/Control</controlURL> + <eventSubURL>/SwitchPower/Event</eventSubURL> + </service> + </serviceList> + </device> +</root> +``` +The `<specVersion>` tag defines what version of the UPnP +Device Architecture the document conforms to. At the time of writing the +only version is 1.0. + +Next there is the root `<device>` tag. This contains +metadata about the device, lists the services it provides and any +sub-devices present (there are none in this example). The +`<deviceType>` tag specifies the type of the device. + +Next we have `<friendlyName>`, `<manufacturer>` and `<modelName>`. The +friendly name is a human-readable name for the device, the manufacturer +and model name are self-explanatory. + +Next there is the UDN, or _Unique Device Name_. This +is an identifier which is unique for each device but persistent for each +particular device. Although it has to start with `uuid:` +note that it doesn't have to be an UUID. There are several alternatives +here: for example it could be computed at built-time if the software will +only be used on a single machine, or it could be calculated using the +device's serial number or MAC address. + +Finally we have the `<serviceList>` which describes the +services this device provides. Each service has a service type (again +there are types defined for standardised services or you can create your +own), service identifier, and three URLs. As a service type we're using +the standard `SwitchPower1` service. The +`<SCPDURL>` field specifies where the _Service +Control Protocol Document_ can be found, this describes the +service in more detail and will be covered next. Finally there are the +control and event URLs, which need to be unique on the device and will be +managed by GUPnP. + +## Defining Services + +Because we are using a standard service we can use the service description +from the specification. This is the `SwitchPower1` +service description file: + +```xml +<?xml version="1.0" encoding="utf-8"?> +<scpd xmlns="urn:schemas-upnp-org:service-1-0"> + <specVersion> + <major>1</major> + <minor>0</minor> + </specVersion> + <actionList> + <action> + <name>SetTarget</name> + <argumentList> + <argument> + <name>newTargetValue</name> + <relatedStateVariable>Target</relatedStateVariable> + <direction>in</direction> + </argument> + </argumentList> + </action> + <action> + <name>GetTarget</name> + <argumentList> + <argument> + <name>RetTargetValue</name> + <relatedStateVariable>Target</relatedStateVariable> + <direction>out</direction> + </argument> + </argumentList> + </action> + <action> + <name>GetStatus</name> + <argumentList> + <argument> + <name>ResultStatus</name> + <relatedStateVariable>Status</relatedStateVariable> + <direction>out</direction> + </argument> + </argumentList> + </action> + </actionList> + <serviceStateTable> + <stateVariable sendEvents="no"> + <name>Target</name> + <dataType>boolean</dataType> + <defaultValue>0</defaultValue> + </stateVariable> + <stateVariable sendEvents="yes"> + <name>Status</name> + <dataType>boolean</dataType> + <defaultValue>0</defaultValue> + </stateVariable> + </serviceStateTable> +</scpd> +``` + +Again, the `<specVersion>` tag defines the UPnP version +that is being used. The rest of the document consists of an +`<actionList>` defining the actions available and a +`<serviceStateTable>` defining the state variables. + +Every `<action>` has a `<name>` and a list +of `<argument>`s. Arguments also have a name, a direction +(`in` or `out` for input or output + arguments) and a related state variable. The state variable is used to +determine the type of the argument, and as such is a required element. +This can lead to the creation of otherwise unused state variables to +define the type for an argument (the `WANIPConnection` +service is a good example of this), thanks to the legacy behind UPnP. + +`<stateVariable>`s need to specify their +`<name>` and `<dataType>`. State variables +by default send notifications when they change, to specify that a variable +doesn't do this set the `<sendEvents>` attribute to +`no`. Finally there are optional +`<defaultValue>`, `<allowedValueList>` and +`<allowedValueRange>` elements which specify what the +default and valid values for the variable. + +For the full specification of the service definition file, including a +complete list of valid `<dataType>`s, see section 2.3 of +the [UPnP Device Architecture](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf) + +## Implementing the Device + +Before starting to implement the device, some boilerplate code is needed +to initialise GUPnP. A GUPnP context can be created using `gupnp_context_new()`. + +```c +GUPnPContext *context; +/* Create the GUPnP context with default host and port */ +context = gupnp_context_new (NULL, 0, NULL); +``` +Next the root device can be created. The name of the device description +file can be passed as an absolute file path or a relative path to the +second parameter of `gupnp_root_device_new()`. The service description +files referenced in the device description are expected to be at the path +given there as well. + +```c +GUPnPRootDevice *dev; +/* Create the root device object */ +dev = gupnp_root_device_new (context, "BinaryLight1.xml", "."); +/* Activate the root device, so that it announces itself */ +gupnp_root_device_set_available (dev, TRUE); +``` + +GUPnP scans the device description and any service description files it +refers to, so if the main loop was entered now the device and service +would be available on the network, albeit with no functionality. The +remaining task is to implement the services. + +## Implementing a Service + +To implement a service we first fetch the #GUPnPService from the root +device using gupnp_device_info_get_service() (#GUPnPRootDevice is a +subclass of #GUPnPDevice, which implements #GUPnPDeviceInfo). This +returns a #GUPnPServiceInfo which again is an interface, implemented by +#GUPnPService (on the server) and #GUPnPServiceProxy (on the client). + +```c +GUPnPServiceInfo *service; +service = gupnp_device_info_get_service (GUPNP_DEVICE_INFO (dev), "urn:schemas-upnp-org:service:SwitchPower:1"); +``` + +GUPnPService handles interacting with the network itself, leaving the +implementation of the service itself to signal handlers that we need to +connect. There are two signals: #GUPnPService::action-invoked and +#GUPnPService::query-variable. #GUPnPService::action-invoked is emitted +when a client invokes an action: the handler is passed a +#GUPnPServiceAction object that identifies which action was invoked, and +is used to return values using `gupnp_service_action_set()`. +#GUPnPService::query-variable is emitted for evented variables when a +control point subscribes to the service (to announce the initial value), +or whenever a client queries the value of a state variable (note that this +is now deprecated behaviour for UPnP control points): the handler is +passed the variable name and a #GValue which should be set to the current +value of the variable. + +Handlers should be targetted at specific actions or variables by using +the signal detail when connecting. For example, +this causes `on_get_status_action` to be called when +the `GetStatus` action is invoked: + +```c +static void on_get_status_action (GUPnPService *service, GUPnPServiceAction *action, gpointer user_data); +// ... +g_signal_connect (service, "action-invoked::GetStatus", G_CALLBACK (on_get_status_action), NULL); +``` + +The implementation of action handlers is quite simple. The handler is +passed a #GUPnPServiceAction object which represents the in-progress +action. If required it can be queried using +gupnp_service_action_get_name() to identify the action (this isn't +required if detailed signals were connected). Any +in arguments can be retrieving using +`gupnp_service_action_get()`, and then return values can be set using +`gupnp_service_action_set()`. Once the action has been performed, either +`gupnp_service_action_return()` or `gupnp_service_action_return_error()` +should be called to either return successfully or return an error code. + +If any evented state variables were modified during the action then a +notification should be emitted using `gupnp_service_notify()`. This is an +example implementation of `GetStatus` and `SetTarget` + +```c +static gboolean status; + +static void +get_status_cb (GUPnPService *service, GUPnPServiceAction *action, gpointer user_data) +{ + gupnp_service_action_set (action, + "ResultStatus", G_TYPE_BOOLEAN, status, + NULL); + gupnp_service_action_return (action); +} + +void +set_target_cb (GUPnPService *service, GUPnPServiceAction *action, gpointer user_data) +{ + gupnp_service_action_get (action, + "NewTargetValue", G_TYPE_BOOLEAN, &status, + NULL); + gupnp_service_action_return (action); + gupnp_service_notify (service, "Status", G_TYPE_STRING, status, NULL); +} + +//... + +g_signal_connect (service, "action-invoked::GetStatus", G_CALLBACK (get_status_cb), NULL); +g_signal_connect (service, "action-invoked::SetTarget", G_CALLBACK (set_target_cb), NULL); +``` + +State variable query handlers are called with the name of the variable and +a #GValue. This value should be initialized with the relevant type and +then set to the current value. Again signal detail can be used to connect +handlers to specific state variable callbacks. + +```c +static gboolean status; + +static void +query_status_cb (GUPnPService *service, char *variable, GValue *value, gpointer user_data) +{ + g_value_init (value, G_TYPE_BOOLEAN); + g_value_set_boolean (value, status); +} + +// ... + +g_signal_connect (service, "query-variable::Status", G_CALLBACK (query_status_cb), NULL); +``` + +The service is now fully implemented. To complete it, enter a GLib main +loop and wait for a client to connect. The complete source code for this +example is available as [examples/light-server.c](https://gitlab.gnome.org/GNOME/gupnp/-/blob/master/examples/light-server.c) in +the GUPnP sources. + +For services which have many actions and variables there is a convenience +method [method@GUPnP.Service.signals_autoconnect] which will automatically +connect specially named handlers to signals. See the documentation for +full details on how it works. + +## Generating Service-specific Wrappers + +Using service-specific wrappers can simplify the implementation of a service. +Wrappers can be generated with gupnp-binding-tool +using the option `--mode server`. + +In the following examples the wrapper has been created with + `--mode server --prefix switch`. Please note that the callback handlers + (`get_status_cb` and `set_target_cb`) are not automatically + generated by gupnp-binding-tool for you. + +```c +static gboolean status; + +static void +get_status_cb (GUPnPService *service, + GUPnPServiceAction *action, + gpointer user_data) +{ + switch_get_status_action_set (action, status); + + gupnp_service_action_return (action); +} + +static void +set_target_cb (GUPnPService *service, + GUPnPServiceAction *action, + gpointer user_data) +{ + switch_set_target_action_get (action, &status); + switch_status_variable_notify (service, status); + + gupnp_service_action_return (action); +} + +// ... + +switch_get_status_action_connect (service, G_CALLBACK(get_status_cb), NULL); +switch_set_target_action_connect (service, G_CALLBACK(set_target_cb), NULL); +``` + +Note how many possible problem situations that were run-time errors are +actually compile-time errors when wrappers are used: Action names, +argument names and argument types are easier to get correct (and available +in editor autocompletion). + +State variable query handlers are implemented in a similar manner, but +they are even simpler as the return value of the handler is the state +variable value. + +```c +static gboolean +query_status_cb (GUPnPService *service, + gpointer user_data) +{ + return status; +} + +// ... + + +switch_status_query_connect (service, query_status_cb, NULL); +``` diff --git a/doc/urlmap.js b/doc/urlmap.js new file mode 100644 index 0000000..4e19607 --- /dev/null +++ b/doc/urlmap.js @@ -0,0 +1,4 @@ +// A map between namespaces and base URLs for their online documentation +baseURLs = [ + [ 'GSSDP', 'https://gnome.pages.gitlab.gnome.org/gssdp/docs/' ], +] |