[gupnp/wip/gi-docgen] wip




commit d830e759dda0f408a3248fb6654366caae1eca8a
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&apos;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: &apos;%s&apos;\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, &amp;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, &amp;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 68b43f5..ea965f5 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]