From 4e890e03033ee451573f4afdd1d2752d7f8a0af8 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 9 Nov 2023 15:17:17 +0100 Subject: [PATCH 1/5] feat: implement requestThingDescription method --- lib/src/binding_coap/coap_client.dart | 8 ++++++++ lib/src/binding_http/http_client.dart | 14 ++++++++++++++ lib/src/binding_mqtt/mqtt_client.dart | 6 ++++++ .../protocol_interfaces/protocol_client.dart | 3 +++ lib/src/core/servient.dart | 17 +++++++++++++++++ lib/src/core/wot.dart | 5 +++++ lib/src/scripting_api/wot.dart | 3 +++ 7 files changed, 56 insertions(+) diff --git a/lib/src/binding_coap/coap_client.dart b/lib/src/binding_coap/coap_client.dart index a8b38630..a4e1e0b7 100644 --- a/lib/src/binding_coap/coap_client.dart +++ b/lib/src/binding_coap/coap_client.dart @@ -538,4 +538,12 @@ final class CoapClient implements ProtocolClient { yield content; } } + + @override + Future requestThingDescription(Uri url) async => _sendRequest( + url, + coap.RequestMethod.get, + form: null, + accept: coap.CoapMediaType.applicationTdJson, + ); } diff --git a/lib/src/binding_http/http_client.dart b/lib/src/binding_http/http_client.dart index 67cea18b..a44fc858 100644 --- a/lib/src/binding_http/http_client.dart +++ b/lib/src/binding_http/http_client.dart @@ -332,4 +332,18 @@ final class HttpClient implements ProtocolClient { yield encodedLinks; } + + @override + Future requestThingDescription(Uri url) async { + final request = Request(HttpRequestMethod.get.methodName, url); + const tdContentType = 'application/td+json'; + request.headers['Accept'] = tdContentType; + + final response = await _client.send(request); + + return Content( + response.headers['Content-Type'] ?? tdContentType, + response.stream, + ); + } } diff --git a/lib/src/binding_mqtt/mqtt_client.dart b/lib/src/binding_mqtt/mqtt_client.dart index 756634e5..e0ce0f26 100644 --- a/lib/src/binding_mqtt/mqtt_client.dart +++ b/lib/src/binding_mqtt/mqtt_client.dart @@ -251,4 +251,10 @@ final class MqttClient implements ProtocolClient { // TODO: implement discoverWithCoreLinkFormat throw UnimplementedError(); } + + @override + Future requestThingDescription(Uri url) { + // TODO: implement requestThingDescription + throw UnimplementedError(); + } } diff --git a/lib/src/core/protocol_interfaces/protocol_client.dart b/lib/src/core/protocol_interfaces/protocol_client.dart index 06298c26..38d91b5f 100644 --- a/lib/src/core/protocol_interfaces/protocol_client.dart +++ b/lib/src/core/protocol_interfaces/protocol_client.dart @@ -62,4 +62,7 @@ abstract interface class ProtocolClient { void Function(Exception error)? error, required void Function() complete, }); + + /// Requests a Thing Description as [Content] from a [url]. + Future requestThingDescription(Uri url); } diff --git a/lib/src/core/servient.dart b/lib/src/core/servient.dart index 40544724..fc9580bc 100644 --- a/lib/src/core/servient.dart +++ b/lib/src/core/servient.dart @@ -15,6 +15,7 @@ import 'exposed_thing.dart'; import 'protocol_interfaces/protocol_client.dart'; import 'protocol_interfaces/protocol_client_factory.dart'; import 'protocol_interfaces/protocol_server.dart'; +import 'thing_discovery.dart'; import 'wot.dart'; /// Exception that is thrown by a [Servient]. @@ -225,4 +226,20 @@ class Servient { return clientFactory.createClient(); } + + /// Requests a [ThingDescription] from a [url]. + Future requestThingDescription(Uri url) async { + final client = clientFor(url.scheme); + final content = await client.requestThingDescription(url); + + final value = await contentSerdes.contentToValue(content, null); + + if (value is! Map) { + throw DiscoveryException( + 'Could not parse Thing Description obtained from $url', + ); + } + + return ThingDescription.fromJson(value); + } } diff --git a/lib/src/core/wot.dart b/lib/src/core/wot.dart index 7408e17e..1bdf8fc0 100644 --- a/lib/src/core/wot.dart +++ b/lib/src/core/wot.dart @@ -90,4 +90,9 @@ class WoT implements scripting_api.WoT { }) { return ThingDiscovery(url, thingFilter, _servient, method: method); } + + @override + Future requestThingDescription(Uri url) { + return _servient.requestThingDescription(url); + } } diff --git a/lib/src/scripting_api/wot.dart b/lib/src/scripting_api/wot.dart index 5efc82eb..207e0723 100644 --- a/lib/src/scripting_api/wot.dart +++ b/lib/src/scripting_api/wot.dart @@ -31,6 +31,9 @@ abstract interface class WoT { /// based on the underlying impementation. Future produce(ExposedThingInit exposedThingInit); + /// Requests a [ThingDescription] from the given [url]. + Future requestThingDescription(Uri url); + /// Discovers [ThingDescription]s from a given [url] using the specified /// [method]. /// From 83b256cc38c9991cdf4995bc15399e4fc6ebe2bc Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 20 Dec 2023 09:19:34 +0100 Subject: [PATCH 2/5] docs: simplify main example --- README.md | 63 ++++++++++------------ example/example.dart | 122 +++++-------------------------------------- 2 files changed, 41 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 2537f107..32c44d55 100644 --- a/README.md +++ b/README.md @@ -36,54 +36,49 @@ You can then use the package in your project by adding ## Usage -Below you can find a very basic example for reading a status from a Thing (using the -`coap.me` test server). -To do so, a Thing Description JSON string is first parsed and turned into a -`ThingDescription` object, which is then passed to a WoT runtime created by a -`Servient` with CoAP support. +Below you can find a basic example for incrementing and reading the value of a +counter Thing, which is part of the +[Thingweb Online Things](https://www.thingweb.io/services). + +In the example, we first create a WoT runtime using a `Servient` with CoAP +support. +With the runtime, we then retrieve a TD (using the `requestThingDescription()` +method) and consume it (using the `consume()` method), creating a +`ConsumedThing` object, +Afterward, the actual interactions with the counter are performed by calling the +`invokeAction()` and `readProperty()` methods on the `ConsumedThing`. ```dart import 'package:dart_wot/dart_wot.dart'; Future main(List args) async { - final CoapClientFactory coapClientFactory = CoapClientFactory(); final servient = Servient( - protocolClients: [coapClientFactory] + clientFactories: [ + CoapClientFactory(), + ], ); final wot = await servient.start(); - final thingDescriptionJson = ''' - { - "@context": "http://www.w3.org/ns/td", - "title": "Test Thing", - "base": "coap://coap.me", - "security": ["nosec_sc"], - "securityDefinitions": { - "nosec_sc": { - "scheme": "nosec" - } - }, - "properties": { - "status": { - "forms": [ - { - "href": "/hello" - } - ] - } - } - } - '''; - - final thingDescription = ThingDescription(thingDescriptionJson); + final url = Uri.parse('coap://plugfest.thingweb.io/counter'); + print('Requesting TD from $url ...'); + final thingDescription = await wot.requestThingDescription(url); + final consumedThing = await wot.consume(thingDescription); - final status = await consumedThing.readProperty("status"); + print( + 'Successfully retrieved and consumed TD with title ' + '"${thingDescription.title}"!', + ); + + print('Incrementing counter ...'); + await consumedThing.invokeAction('increment'); + + final status = await consumedThing.readProperty('count'); final value = await status.value(); - print(value); + print('New counter value: $value'); } ``` -A more complex example can be found in the `example` directory. +More complex examples can be found in the `example` directory. ## Additional information diff --git a/example/example.dart b/example/example.dart index 6afe1dba..c009bafa 100644 --- a/example/example.dart +++ b/example/example.dart @@ -8,126 +8,28 @@ import 'package:dart_wot/dart_wot.dart'; -final Map basicCredentials = { - 'urn:test': BasicCredentials('rw', 'readwrite'), -}; - -Future basicCredentialsCallback( - Uri uri, - Form? form, [ - BasicCredentials? invalidCredentials, -]) async { - final id = form?.thingDescription.identifier; - - return basicCredentials[id]; -} - Future main(List args) async { - final coapClientFactory = CoapClientFactory(); - final httpClientFactory = - HttpClientFactory(basicCredentialsCallback: basicCredentialsCallback); - final mqttClientFactory = MqttClientFactory(); - final servient = Servient( clientFactories: [ - coapClientFactory, - httpClientFactory, - mqttClientFactory, + CoapClientFactory(), ], ); - final wot = await servient.start(); - const thingDescriptionJson = ''' - { - "@context": "http://www.w3.org/ns/td", - "title": "Test Thing", - "id": "urn:test", - "base": "coap://coap.me", - "security": ["auto_sc"], - "securityDefinitions": { - "auto_sc": { - "scheme": "auto" - } - }, - "properties": { - "status": { - "forms": [ - { - "href": "/hello" - } - ] - }, - "status2": { - "observable": true, - "forms": [ - { - "href": "mqtt://test.mosquitto.org:1884", - "mqv:filter": "test", - "op": ["readproperty", "observeproperty"], - "contentType": "text/plain" - } - ] - } - }, - "actions": { - "toggle": { - "forms": [ - { - "href": "mqtt://test.mosquitto.org:1884", - "mqv:topic": "test", - "mqv:retain": true - } - ] - } - } - } - '''; + final url = Uri.parse('coap://plugfest.thingweb.io/counter'); + print('Requesting TD from $url ...'); + final thingDescription = await wot.requestThingDescription(url); - final thingDescription = ThingDescription(thingDescriptionJson); final consumedThing = await wot.consume(thingDescription); - final status = await consumedThing.readProperty('status'); - final value = await status.value(); - print(value); - final subscription = await consumedThing.observeProperty( - 'status2', - (data) async { - final value = await data.value(); - print(value); - }, + print( + 'Successfully retrieved and consumed TD with title ' + '"${thingDescription.title}"!', ); - await consumedThing.invokeAction('toggle', 'Hello World!'); - await consumedThing.invokeAction('toggle', 'Hello World!'); - await consumedThing.invokeAction('toggle', 'Hello World!'); - await consumedThing.invokeAction('toggle', 'Hello World!'); - await subscription.stop(); + print('Incrementing counter ...'); + await consumedThing.invokeAction('increment'); - final thingUri = Uri.parse( - 'https://raw.githubusercontent.com/w3c/wot-testing' - '/b07fa6124bca7796e6ca752a3640fac264d3bcbc/events/2021.03.Online/TDs' - '/Oracle/oracle-Festo_Shared.td.jsonld', - ); - - final thingDiscovery = wot.discover(thingUri); - - await for (final thingDescription in thingDiscovery) { - final consumedDiscoveredThing = await wot.consume(thingDescription); - print( - 'The title of the fetched TD is ' - '${consumedDiscoveredThing.thingDescription.title}.', - ); - } - - await consumedThing.invokeAction('toggle', 'Bye World!'); - await consumedThing.readAndPrintProperty('status2'); - print('Done!'); -} - -extension ReadAndPrintExtension on ConsumedThing { - Future readAndPrintProperty(String propertyName) async { - final output = await readProperty(propertyName); - final value = await output.value(); - print(value); - } + final status = await consumedThing.readProperty('count'); + final value = await status.value(); + print('New counter value: $value'); } From 86d18d3cee39dc0245359aba673c8906f35639b3 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 20 Dec 2023 09:20:31 +0100 Subject: [PATCH 3/5] docs: create seperate MQTT example --- example/mqtt_example.dart | 105 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 example/mqtt_example.dart diff --git a/example/mqtt_example.dart b/example/mqtt_example.dart new file mode 100644 index 00000000..15358070 --- /dev/null +++ b/example/mqtt_example.dart @@ -0,0 +1,105 @@ +// Copyright 2023 Contributors to the Eclipse Foundation. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// SPDX-License-Identifier: BSD-3-Clause + +// ignore_for_file: avoid_print + +import 'package:dart_wot/dart_wot.dart'; + +const thingDescriptionJson = ''' + { + "@context": "https://www.w3.org/2022/wot/td/v1.1", + "title": "Test Thing", + "id": "urn:test", + "base": "coap://coap.me", + "security": ["auto_sc"], + "securityDefinitions": { + "auto_sc": { + "scheme": "auto" + } + }, + "properties": { + "status": { + "observable": true, + "forms": [ + { + "href": "mqtt://test.mosquitto.org:1884", + "mqv:filter": "test", + "op": ["readproperty", "observeproperty"], + "contentType": "text/plain" + } + ] + } + }, + "actions": { + "toggle": { + "input": { + "type": "string" + }, + "forms": [ + { + "href": "mqtt://test.mosquitto.org:1884", + "mqv:topic": "test", + "mqv:retain": true + } + ] + } + } + } + '''; + +final Map basicCredentials = { + 'urn:test': BasicCredentials('rw', 'readwrite'), +}; + +Future basicCredentialsCallback( + Uri uri, + Form? form, [ + BasicCredentials? invalidCredentials, +]) async { + final id = form?.thingDescription.identifier; + + return basicCredentials[id]; +} + +Future main(List args) async { + final servient = Servient( + clientFactories: [ + MqttClientFactory(basicCredentialsCallback: basicCredentialsCallback), + ], + ); + + final wot = await servient.start(); + + final thingDescription = ThingDescription(thingDescriptionJson); + final consumedThing = await wot.consume(thingDescription); + await consumedThing.readAndPrintProperty('status'); + + final subscription = await consumedThing.observeProperty( + 'status', + (data) async { + final value = await data.value(); + print(value); + }, + ); + + await consumedThing.invokeAction('toggle', 'Hello World!'); + await consumedThing.invokeAction('toggle', 'Hello World!'); + await consumedThing.invokeAction('toggle', 'Hello World!'); + await consumedThing.invokeAction('toggle', 'Hello World!'); + await subscription.stop(); + + await consumedThing.invokeAction('toggle', 'Bye World!'); + await consumedThing.readAndPrintProperty('status'); + print('Done!'); +} + +extension ReadAndPrintExtension on ConsumedThing { + Future readAndPrintProperty(String propertyName) async { + final output = await readProperty(propertyName); + final value = await output.value(); + print(value); + } +} From 086ad2c4208e0c48eb9f677da1e999910bf9127a Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 20 Dec 2023 09:20:59 +0100 Subject: [PATCH 4/5] chore: deprecate old `discover` method implementation --- lib/src/scripting_api/wot.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/scripting_api/wot.dart b/lib/src/scripting_api/wot.dart index 207e0723..eb8b4c71 100644 --- a/lib/src/scripting_api/wot.dart +++ b/lib/src/scripting_api/wot.dart @@ -52,7 +52,13 @@ abstract interface class WoT { /// using the `await for` syntax. /// It also allows for stopping the Discovery process prematurely and /// for retrieving information about its current state (i.e., whether it is - /// still `active`). + /// still [ThingDiscovery.active]). + @Deprecated( + 'The discover method is curently in the process of being adjusted to the ' + 'latest Scripting API version and is therefore subject to change. ' + 'For direct and directory discovery, please refer to the ' + 'requestThingDescription and exploreDirectory methods instead.', + ) ThingDiscovery discover( Uri url, { ThingFilter? thingFilter, From e5b57454d90d77fb5dcc5d263e8d35d5ffd779d4 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 20 Dec 2023 10:20:02 +0100 Subject: [PATCH 5/5] test: add tests for requestThingDescription method --- test/core/discovery_test.dart | 165 ++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 test/core/discovery_test.dart diff --git a/test/core/discovery_test.dart b/test/core/discovery_test.dart new file mode 100644 index 00000000..ad6780b5 --- /dev/null +++ b/test/core/discovery_test.dart @@ -0,0 +1,165 @@ +// Copyright 2023 Contributors to the Eclipse Foundation. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'dart:convert'; + +import 'package:dart_wot/dart_wot.dart'; +import 'package:dart_wot/src/core/content.dart'; +import 'package:dart_wot/src/core/thing_discovery.dart'; +import 'package:test/test.dart'; + +const testUriScheme = 'test'; +final validTestDiscoveryUri = + Uri.parse('$testUriScheme://[::1]/.well-known/wot'); +final invalidTestDiscoveryUri = + Uri.parse('$testUriScheme://[::2]/.well-known/wot'); + +const validTestTitle = 'Test TD'; +const validTestThingDescription = ''' + { + "@context": "https://www.w3.org/2022/wot/td/v1.1", + "title": "$validTestTitle", + "security": "nosec_sc", + "securityDefinitions": { + "nosec_sc": {"scheme": "nosec"} + } + } +'''; + +const invalidTestThingDescription = '"Hi there!"'; + +void main() { + group('Discovery Tests', () { + test('Should be able to use the requestThingDescription method', () async { + final servient = Servient( + clientFactories: [ + _MockedProtocolClientFactory(), + ], + ); + + final wot = await servient.start(); + final thingDescription = + await wot.requestThingDescription(validTestDiscoveryUri); + + expect(thingDescription.title, validTestTitle); + }); + + test( + 'Should throw an exception if an invalid TD results from the ' + 'requestThingDescription method', + () async { + final servient = Servient( + clientFactories: [ + _MockedProtocolClientFactory(), + ], + ); + + final wot = await servient.start(); + await expectLater( + wot.requestThingDescription(invalidTestDiscoveryUri), + // TODO: Refine error handling + throwsA(isA()), + ); + }, + ); + }); +} + +class _MockedProtocolClient implements ProtocolClient { + @override + Stream discoverWithCoreLinkFormat(Uri uri) { + // TODO: implement discoverWithCoreLinkFormat + throw UnimplementedError(); + } + + @override + Future invokeResource(Form form, Content content) { + // TODO: implement invokeResource + throw UnimplementedError(); + } + + @override + Future readResource(Form form) { + // TODO: implement readResource + throw UnimplementedError(); + } + + @override + Future requestThingDescription(Uri url) async { + if (url == validTestDiscoveryUri) { + return validTestThingDescription.toDiscoveryContent(url); + } + + if (url == invalidTestDiscoveryUri) { + return invalidTestThingDescription.toDiscoveryContent(url); + } + + throw StateError('Encountered invalid URL.'); + } + + @override + Future start() async { + // Do nothing + } + + @override + Future stop() async { + // Do nothing + } + + @override + Future subscribeResource( + Form form, { + required void Function(Content content) next, + void Function(Exception error)? error, + required void Function() complete, + }) { + // TODO: implement subscribeResource + throw UnimplementedError(); + } + + @override + Future writeResource(Form form, Content content) { + // TODO: implement writeResource + throw UnimplementedError(); + } + + @override + Stream discoverDirectly( + Uri uri, { + bool disableMulticast = false, + }) { + // TODO: implement discoverDirectly + throw UnimplementedError(); + } +} + +class _MockedProtocolClientFactory implements ProtocolClientFactory { + @override + ProtocolClient createClient() { + return _MockedProtocolClient(); + } + + @override + bool destroy() { + return true; + } + + @override + bool init() { + return true; + } + + @override + Set get schemes => {testUriScheme}; +} + +extension _DiscoveryContentCreationExtension on String { + DiscoveryContent toDiscoveryContent(Uri url) { + final body = Stream.fromIterable([utf8.encode(this)]); + return DiscoveryContent('application/td+json', body, url); + } +}