[gupnp/wip/gi-docgen: 10/14] wip
- From: Jens Georg <jensgeorg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gupnp/wip/gi-docgen: 10/14] wip
- Date: Thu, 30 Dec 2021 09:18:06 +0000 (UTC)
commit ab783c0b23e99eea39b3520c08e6dab398088833
Author: Jens Georg <mail jensge org>
Date: Sun Jul 25 11:49:42 2021 +0200
wip
doc/client-tutorial.md | 215 +++++++++++++++++++++++
doc/gupnp.toml.in | 52 ++++++
doc/images/gupnp-logo-short.svg | 126 ++++++++++++++
doc/meson.build | 94 ++++++----
doc/server-tutorial.md | 372 ++++++++++++++++++++++++++++++++++++++++
doc/urlmap.js | 4 +
meson.build | 6 +
subprojects/gi-docgen.wrap | 2 +
8 files changed, 840 insertions(+), 31 deletions(-)
---
diff --git a/doc/client-tutorial.md b/doc/client-tutorial.md
new file mode 100644
index 0000000..429e29e
--- /dev/null
+++ b/doc/client-tutorial.md
@@ -0,0 +1,215 @@
+---
+Title: UPnP Client Tutorial
+---
+
+# 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/gupnp.toml.in b/doc/gupnp.toml.in
new file mode 100644
index 0000000..efba00f
--- /dev/null
+++ b/doc/gupnp.toml.in
@@ -0,0 +1,52 @@
+[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"
+]
+
+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 2fd0106..3cf2a38 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -1,41 +1,73 @@
entities = configuration_data()
entities.set('VERSION', meson.project_version())
+
version_xml = configure_file(input: 'version.xml.in',
output: 'version.xml', configuration:
entities)
if get_option('gtk_doc')
- gnome.gtkdoc('gupnp',
- content_files : files(
- 'client-tutorial.xml',
- 'fdl-1.1.xml',
- 'glossary.xml',
- 'gupnp-binding-tool.xml',
- 'gupnp-docs.xml',
- 'overview.xml',
- 'server-tutorial.xml'
- ),
- main_xml : 'gupnp-docs.xml',
- src_dir : ['libgupnp'],
- dependencies : libgupnp,
- 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'],
+ build_by_default: true,
+ install: true,
+ install_dir : docs_dir,
+ )
+
+# gnome.gtkdoc('gupnp',
+# content_files : files(
+# 'client-tutorial.xml',
+# 'fdl-1.1.xml',
+# 'glossary.xml',
+# 'gupnp-binding-tool.xml',
+# 'gupnp-docs.xml',
+# 'overview.xml',
+# 'server-tutorial.xml'
+# ),
+# main_xml : 'gupnp-docs.xml',
+# src_dir : ['libgupnp'],
+# dependencies : libgupnp,
+# 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)
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..ca8694c
--- /dev/null
+++ b/doc/server-tutorial.md
@@ -0,0 +1,372 @@
+---
+Title: UPnP Server Tutorial
+---
+
+# UPnP Server Tutorial
+This chapter explains how to implement a 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/' ],
+]
diff --git a/meson.build b/meson.build
index 545dd01..d9b9ab8 100644
--- a/meson.build
+++ b/meson.build
@@ -38,6 +38,12 @@ dependencies = [
subdir('libgupnp')
subdir('tests')
subdir('tools')
+
+gidocgen_dep = dependency('gi-docgen', version: '>= 2021.1',
+ fallback: ['gi-docgen', 'dummy_dep'],
+ required: get_option('gtk_doc') and get_option('introspection')
+ )
+
subdir('doc')
if get_option('vapi') and get_option('introspection')
diff --git a/subprojects/gi-docgen.wrap b/subprojects/gi-docgen.wrap
new file mode 100644
index 0000000..c29b801
--- /dev/null
+++ b/subprojects/gi-docgen.wrap
@@ -0,0 +1,2 @@
+[wrap-redirect]
+filename = gssdp-1.2/subprojects/gi-docgen.wrap
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]