├── .fake ├── lib ├── src │ ├── storage │ │ ├── redis_storage.dart │ │ └── web_storage.dart │ ├── responder │ │ ├── manager │ │ │ ├── permission_manager.dart │ │ │ ├── trace.dart │ │ │ └── storage.dart │ │ ├── response.dart │ │ ├── base_impl │ │ │ ├── def_node.dart │ │ │ ├── config_setting.dart │ │ │ └── local_node_impl.dart │ │ └── response │ │ │ └── invoke.dart │ ├── historian │ │ ├── adapter.dart │ │ ├── publish.dart │ │ ├── values.dart │ │ ├── main.dart │ │ ├── interval.dart │ │ ├── manage.dart │ │ ├── rollup.dart │ │ └── get_history.dart │ ├── requester │ │ ├── request │ │ │ ├── remove.dart │ │ │ ├── set.dart │ │ │ └── invoke.dart │ │ ├── request.dart │ │ └── default_defs.dart │ ├── utils │ │ ├── promise_timeout.dart │ │ ├── list.dart │ │ ├── dslink_json.dart │ │ ├── uri_component.dart │ │ ├── stream_controller.dart │ │ └── codec.dart │ ├── common │ │ ├── default_defs.dart │ │ ├── connection_channel.dart │ │ ├── table.dart │ │ ├── permission.dart │ │ └── connection_handler.dart │ ├── query │ │ ├── query_command.dart │ │ ├── query_manager.dart │ │ └── query_subscribe_command.dart │ ├── crypto │ │ ├── pk.dart │ │ └── dart │ │ │ └── isolate.dart │ ├── browser │ │ └── browser_user_link.dart │ └── nodes │ │ └── json.dart ├── socket_client.dart ├── socket_server.dart ├── query.dart ├── dslink.dart ├── requester.dart ├── historian.dart ├── server.dart ├── responder.dart ├── convert_consts.dart ├── convert_consts.dart1 └── broker_discovery.dart ├── .analysis_options ├── tool ├── experiment │ ├── certs │ │ ├── private_key.txt │ │ ├── key4.db │ │ ├── cert9.db │ │ └── pkcs11.txt │ ├── browser │ │ ├── styles │ │ │ └── main.css │ │ ├── lockerdemo.css │ │ ├── test_browser_link.dart │ │ ├── test_browser_link.html │ │ ├── test_browser_requester.html │ │ ├── test_browser_requester.dart │ │ ├── lockerdemo.html │ │ └── lockerdemo.dart │ ├── test_scheduler.dart │ ├── workers │ │ ├── controller.dart │ │ └── worker.dart │ ├── test_client_responder.dart │ ├── test_qos_requester.dart │ ├── test_client_requester.dart │ ├── test_key_generate.dart │ ├── test_qos_responder.dart │ ├── test_streamset.dart │ ├── dslink.json │ ├── lots_of_links.dart │ ├── sample_responder.dart │ ├── test_client_responder_locker.dart │ ├── test_ds_handshake.dart │ ├── large.dart │ ├── test_concurrent.dart │ ├── test_link_provider.dart │ └── test_concurrent2.dart ├── id_rsa.enc ├── test.sh ├── analyze.sh ├── docs.sh ├── travis.sh ├── toDart2.sh └── toDart1.sh ├── example ├── message.png ├── broker_discovery.dart ├── browser │ ├── worker.dart │ ├── worker.html │ ├── image.html │ ├── video.html │ ├── camera.html │ ├── browser.html │ ├── grid.html │ ├── image.dart │ ├── camera.dart │ ├── video.dart │ ├── grid.dart │ └── style.css ├── browser_link.dart ├── simple_requester.dart ├── worker_link.dart ├── simple_link.dart └── actions_link.dart ├── .gitattributes ├── test ├── utils │ ├── all.dart │ ├── logging_test.dart │ ├── base64_test.dart │ ├── action_test.dart │ └── scheduler_test.dart ├── all.dart ├── broker_discovery_test.dart ├── common.dart ├── worker_test.dart ├── simple_nodes_test.dart └── large_links_test.dart ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── CHANGELOG.md ├── bin └── beacon.dart ├── pubspec.yaml └── README.md /.fake: -------------------------------------------------------------------------------- 1 | Build Bump 2 | -------------------------------------------------------------------------------- /lib/src/storage/redis_storage.dart: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.analysis_options: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: true 3 | -------------------------------------------------------------------------------- /tool/experiment/certs/private_key.txt: -------------------------------------------------------------------------------- 1 | M6S41GAL0gH0I97Hhy7A2-icf8dHnxXPmYIRwem03HE -------------------------------------------------------------------------------- /tool/id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IOT-DSA/sdk-dslink-dart/HEAD/tool/id_rsa.enc -------------------------------------------------------------------------------- /example/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IOT-DSA/sdk-dslink-dart/HEAD/example/message.png -------------------------------------------------------------------------------- /lib/socket_client.dart: -------------------------------------------------------------------------------- 1 | /// Placeholder for future socket client support. 2 | library dslink.socket_client; 3 | -------------------------------------------------------------------------------- /lib/socket_server.dart: -------------------------------------------------------------------------------- 1 | /// Placeholder for future socket server support. 2 | library dslink.socket_server; 3 | -------------------------------------------------------------------------------- /tool/experiment/certs/key4.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IOT-DSA/sdk-dslink-dart/HEAD/tool/experiment/certs/key4.db -------------------------------------------------------------------------------- /tool/experiment/certs/cert9.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IOT-DSA/sdk-dslink-dart/HEAD/tool/experiment/certs/cert9.db -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.dart text eol=lf 2 | *.sh text eol=lf 3 | *.md text eol=lf 4 | *.json text eol=lf 5 | *.html text eol=lf 6 | -------------------------------------------------------------------------------- /tool/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ARGS="${@}" 4 | 5 | if [ -z "${ARGS}" ] 6 | then 7 | ARGS="test/ -p vm -j 6" 8 | fi 9 | 10 | pub run test ${ARGS} 11 | 12 | -------------------------------------------------------------------------------- /tool/experiment/browser/styles/main.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto); 2 | 3 | html, body { 4 | width: 100%; 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | font-family: 'Roboto', sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /example/broker_discovery.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | 3 | main() async { 4 | var client = new BrokerDiscoveryClient(); 5 | 6 | await client.init(); 7 | 8 | await for (var url in client.discover()) { 9 | print("Discovered Broker at ${url}"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tool/experiment/test_scheduler.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/utils.dart"; 2 | 3 | main() { 4 | Scheduler.after(new Duration(seconds: 5), () { 5 | print("It's 5 seconds later."); 6 | }); 7 | 8 | Scheduler.every(Interval.ONE_SECOND, () { 9 | print("One Second"); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /tool/experiment/workers/controller.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/worker.dart"; 2 | 3 | main() async { 4 | WorkerSocket worker; 5 | worker = await createWorkerScript("worker.dart").init(methods: { 6 | "stop": (_) => worker.stop() 7 | }); 8 | await worker.callMethod("hello"); 9 | } 10 | -------------------------------------------------------------------------------- /test/utils/all.dart: -------------------------------------------------------------------------------- 1 | import "action_test.dart" as ActionTests; 2 | import "base64_test.dart" as Base64Tests; 3 | import "logging_test.dart" as LoggingTests; 4 | import "scheduler_test.dart" as SchedulerTests; 5 | 6 | main() { 7 | ActionTests.main(); 8 | Base64Tests.main(); 9 | LoggingTests.main(); 10 | SchedulerTests.main(); 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | .pub 4 | packages 5 | build 6 | pubspec.lock 7 | .idea 8 | *.iml 9 | /test/experiment/.dslink.key 10 | /dslink.json 11 | /.dslink.key 12 | /broker.json 13 | /conns.json 14 | /nodes.json 15 | /tool/personal 16 | .packages 17 | doc/ 18 | /data.json 19 | /usernodes.json 20 | /defs.json 21 | /storage/ 22 | /tmp 23 | -------------------------------------------------------------------------------- /lib/src/responder/manager/permission_manager.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | abstract class IPermissionManager { 4 | int getPermission(String path, Responder resp); 5 | } 6 | 7 | class DummyPermissionManager implements IPermissionManager { 8 | int getPermission(String path, Responder resp) { 9 | return Permission.CONFIG; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/query.dart: -------------------------------------------------------------------------------- 1 | /// DSA Query Implementation 2 | library dslink.query; 3 | 4 | import "dart:async"; 5 | 6 | import "responder.dart"; 7 | import "common.dart"; 8 | import "utils.dart"; 9 | 10 | part "src/query/query_manager.dart"; 11 | part "src/query/query_command.dart"; 12 | part "src/query/query_list_command.dart"; 13 | part "src/query/query_subscribe_command.dart"; 14 | -------------------------------------------------------------------------------- /example/browser/worker.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/worker.dart"; 2 | 3 | main() async { 4 | var worker = await createWorker(transformStringWorker).init(); 5 | print(await worker.callMethod("transform", "Hello World")); 6 | } 7 | 8 | transformStringWorker(Worker worker) async { 9 | await worker.init(methods: { 10 | "transform": (/*String*/ input) => input.toLowerCase() 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /tool/experiment/certs/pkcs11.txt: -------------------------------------------------------------------------------- 1 | library= 2 | name=NSS Internal PKCS #11 Module 3 | parameters=configdir='sql:.' certPrefix='' keyPrefix='' secmod='secmod.db' flags= updatedir='' updateCertPrefix='' updateKeyPrefix='' updateid='' updateTokenDescription='' 4 | NSS=Flags=internal,critical trustOrder=75 cipherOrder=100 slotParams=(1={slotFlags=[RSA,DSA,DH,RC2,RC4,DES,RANDOM,SHA1,MD5,MD2,SSL,TLS,AES,Camellia,SEED,SHA256,SHA512] askpw=any timeout=30}) 5 | 6 | -------------------------------------------------------------------------------- /example/browser/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DSA Browser Workers 5 | 6 | 8 | 9 | 10 | 11 |
12 |

13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/utils/logging_test.dart: -------------------------------------------------------------------------------- 1 | library dslink.test.utils.logger; 2 | 3 | import "package:test/test.dart"; 4 | import "package:logging/logging.dart"; 5 | 6 | import "package:dslink/utils.dart"; 7 | 8 | main() => group("Logger", loggingTests); 9 | 10 | loggingTests() { 11 | test("level update works as expected", () { 12 | expect(logger.level, equals(Level.INFO)); 13 | updateLogLevel("FINE"); 14 | expect(logger.level, equals(Level.FINE)); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | script: "./tool/travis.sh" 3 | sudo: true 4 | dart: 5 | - 1.24.3 6 | - 1.17.1 7 | env: 8 | global: 9 | - secure: TzlLtsKoHCPotSBXkQZwobmEwMXzTioS3uG+Z+0ablygm+Ba+f9vja/PkT1odHcK39H/el2T4v6aOyU3hl52nMvKOsUvCtD1RTHPgK/ssQAaKhwb0Prx8qzK/0sv33Bo7k1Zqqu0CiKX6NWGrRxV2wraQehN5YDxpDr76P6wFxA= 10 | - secure: omDxdGHkFzF6nuRTrjJ5I5xymdXlkb4NcRXIQ7wt+OYh3OTKz7pkGlvCOd6liUFYpiRbjApZRs4SMh7UViYyrMNSX9j2jPTiGSpvPw5X1RtdeAHuYKD/N+p11NlhNueaAT8wyr4yMrG5dR0Yby+O0XunFYcb4kp//L8pUEgdvLo= 11 | -------------------------------------------------------------------------------- /tool/experiment/workers/worker.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/utils.dart" show Scheduler; 2 | import "package:dslink/worker.dart"; 3 | 4 | main(List args, message) async { 5 | Worker worker = buildWorkerForScript(message as Map); 6 | WorkerSocket socket = await worker.init(methods: { 7 | "hello": (_) => print("Hello World") 8 | }); 9 | 10 | print("Worker Started"); 11 | 12 | Scheduler.after(new Duration(seconds: 2), () { 13 | socket.callMethod("stop"); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/all.dart: -------------------------------------------------------------------------------- 1 | import "simple_links_test.dart" as SimpleLinksTests; 2 | import "simple_nodes_test.dart" as SimpleNodesTests; 3 | import "large_links_test.dart" as LargeLinksTests; 4 | 5 | import "worker_test.dart" as WorkerTests; 6 | import "utils/all.dart" as UtilsTests; 7 | import "broker_discovery_test.dart" as BrokerDiscoveryTests; 8 | 9 | main() { 10 | UtilsTests.main(); 11 | SimpleLinksTests.main(); 12 | SimpleNodesTests.main(); 13 | LargeLinksTests.main(); 14 | WorkerTests.main(); 15 | BrokerDiscoveryTests.main(); 16 | } 17 | -------------------------------------------------------------------------------- /tool/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | FILES=( 5 | bin/beacon.dart 6 | lib/browser_client.dart 7 | lib/client.dart 8 | lib/common.dart 9 | lib/dslink.dart 10 | lib/nodes.dart 11 | lib/requester.dart 12 | lib/responder.dart 13 | lib/server.dart 14 | lib/socket_client.dart 15 | lib/socket_server.dart 16 | lib/utils.dart 17 | lib/worker.dart 18 | lib/io.dart 19 | lib/historian.dart 20 | ) 21 | 22 | for FILE in ${FILES[@]} 23 | do 24 | dartanalyzer ${FILE} 25 | done 26 | -------------------------------------------------------------------------------- /tool/experiment/test_client_responder.dart: -------------------------------------------------------------------------------- 1 | import 'package:dslink/client.dart'; 2 | import 'package:dslink/src/crypto/pk.dart'; 3 | import 'sample_responder.dart'; 4 | 5 | main() async { 6 | PrivateKey key = new PrivateKey.loadFromString('9zaOwGO2iXimn4RXTNndBEpoo32qFDUw72d8mteZP9I BJSgx1t4pVm8VCs4FHYzRvr14BzgCBEm8wJnMVrrlx1u1dnTsPC0MlzAB1LhH2sb6FXnagIuYfpQUJGT_yYtoJM'); 7 | 8 | var link = new HttpClientLink('http://localhost:8080/conn', 'rick-req-', key, 9 | isResponder: true, nodeProvider: new TestNodeProvider()); 10 | 11 | link.connect(); 12 | } 13 | -------------------------------------------------------------------------------- /lib/dslink.dart: -------------------------------------------------------------------------------- 1 | /// Entry Point for the DSLink SDK for the Dart VM 2 | library dslink; 3 | 4 | export "package:dslink/common.dart"; 5 | export "package:dslink/requester.dart"; 6 | export "package:dslink/responder.dart"; 7 | export "package:dslink/client.dart"; 8 | export "package:dslink/broker_discovery.dart" show BrokerDiscoveryClient, BrokerDiscoverRequest; 9 | export "package:dslink/utils.dart" 10 | show 11 | Scheduler, 12 | Interval, 13 | DSLinkJSON, 14 | updateLogLevel, 15 | buildEnumType, 16 | buildActionIO, 17 | ByteDataUtil; 18 | -------------------------------------------------------------------------------- /tool/experiment/browser/lockerdemo.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: #F8F8F8; 4 | font-family: 'Open Sans', sans-serif; 5 | font-size: 14px; 6 | font-weight: normal; 7 | line-height: 1.2em; 8 | margin: 15px; 9 | } 10 | 11 | h1, p { 12 | color: #333; 13 | } 14 | 15 | .sample_container { 16 | width: 300px; 17 | height: 300px; 18 | border: 1px solid #ccc; 19 | background-color: #fff; 20 | } 21 | 22 | .sample_text { 23 | font-size: 24pt; 24 | text-align: center; 25 | margin-top: 140px; 26 | -webkit-user-select: none; 27 | user-select: none; 28 | } 29 | input { 30 | margin-left:70px; 31 | } -------------------------------------------------------------------------------- /tool/docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | if [ -d "build" ] 4 | then 5 | rm -rf build 6 | fi 7 | pub global activate -sgit https://github.com/dart-lang/dartdoc.git > /dev/null 8 | mkdir -p build/docs 9 | pub global run dartdoc --output build/docs --add-crossdart 10 | if [ "$1" == "--upload" ] 11 | then 12 | git clone git@github.com:IOT-DSA/docs.git -b gh-pages --depth 1 build/tmp 13 | rm -rf build/tmp/sdks/dart 14 | mkdir -p build/tmp/sdks/dart 15 | cp -R build/docs/* build/tmp/sdks/dart/ 16 | cd build/tmp 17 | set +e 18 | git add -A . 19 | git commit -m "Update Docs for Dart SDK" 20 | git push origin gh-pages 21 | fi 22 | -------------------------------------------------------------------------------- /example/browser_link.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/browser.dart"; 2 | 3 | LinkProvider link; 4 | 5 | main() async { 6 | link = new LinkProvider( 7 | "http://127.0.0.1:8080/conn", // Broker URL 8 | "BrowserExample-", // Link Prefix 9 | defaultNodes: { 10 | "Message": { 11 | r"$type": "string", // The type of the node is a string. 12 | r"$writable": "write", // This node's value can be set by a requester. 13 | "?value": "Hello World" // The default message value. 14 | } 15 | } 16 | ); 17 | 18 | await link.init(); // Initialize the Link 19 | 20 | link.connect(); // Connect to the Broker 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ``` 2 | Copyright 2014 DGLogik Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ``` 16 | -------------------------------------------------------------------------------- /lib/requester.dart: -------------------------------------------------------------------------------- 1 | /// DSA Requester API 2 | library dslink.requester; 3 | 4 | import "dart:async"; 5 | import "dart:collection"; 6 | 7 | import "common.dart"; 8 | import "utils.dart"; 9 | import 'package:dslink/convert_consts.dart'; 10 | 11 | export "package:dslink/utils.dart" show parseEnumType; 12 | 13 | part "src/requester/requester.dart"; 14 | part "src/requester/request.dart"; 15 | part "src/requester/node_cache.dart"; 16 | part "src/requester/request/list.dart"; 17 | part "src/requester/request/subscribe.dart"; 18 | part "src/requester/request/invoke.dart"; 19 | part "src/requester/request/set.dart"; 20 | part "src/requester/request/remove.dart"; 21 | 22 | part "src/requester/default_defs.dart"; 23 | -------------------------------------------------------------------------------- /example/browser/image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DSA Image Display 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/browser/video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DSA Video Player 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/src/historian/adapter.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | abstract class HistorianAdapter { 4 | Future getDatabase(Map config); 5 | 6 | List> getCreateDatabaseParameters(); 7 | } 8 | 9 | abstract class HistorianDatabaseAdapter { 10 | Future getSummary(String group, String path); 11 | Future store(List entries); 12 | Stream fetchHistory(String group, String path, TimeRange range); 13 | Future purgePath(String group, String path, TimeRange range); 14 | Future purgeGroup(String group, TimeRange range); 15 | 16 | Future close(); 17 | 18 | addWatchPathExtensions(WatchPathNode node) {} 19 | addWatchGroupExtensions(WatchGroupNode node) {} 20 | } 21 | -------------------------------------------------------------------------------- /example/browser/camera.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DSA Camera Stream 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/simple_requester.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | 3 | LinkProvider link; 4 | 5 | main(List args) async { 6 | link = new LinkProvider( 7 | args, 8 | "Simple-Requester-", // DSLink Prefix 9 | isResponder: false // We are just a requester. 10 | ); 11 | 12 | link.connect(); // Connect to the broker. 13 | Requester requester = await link.onRequesterReady; // Wait for the requester to be ready. 14 | 15 | await for (RequesterListUpdate update in requester.list("/")) { 16 | // List the nodes in / 17 | print("- ${update.node.remotePath}"); // Print the path of each node. 18 | } // This will not end until you break the for loop. Whenever a node is added or removed to/from the given path, it will receive an update. 19 | } 20 | -------------------------------------------------------------------------------- /tool/experiment/test_qos_requester.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | 3 | LinkProvider link; 4 | int lastNum; 5 | SimpleNode valueNode; 6 | 7 | 8 | main(List args) { 9 | var defaultNodes = { 10 | 'node': { 11 | r'$type':'string' 12 | } 13 | }; 14 | 15 | link = new LinkProvider( 16 | ['-b', 'localhost:8080/conn', '--log', 'finest'], 'qos-req', 17 | defaultNodes: defaultNodes, isResponder: false, isRequester: true); 18 | if (link.link == null) { 19 | // initialization failed 20 | return; 21 | } 22 | 23 | link.connect(); 24 | link.onRequesterReady.then((Requester req) { 25 | req.subscribe('/downstream/qos-resp/node', (update) { 26 | print(update.value); 27 | }, 3); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /tool/experiment/browser/test_browser_link.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, . All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import "dart:html"; 5 | import "package:dslink/src/crypto/pk.dart"; 6 | import "package:dslink/browser_client.dart"; 7 | import "../sample_responder.dart"; 8 | 9 | void main() { 10 | querySelector('#output').text = 'Your Dart app is running.'; 11 | 12 | PrivateKey key = new PrivateKey.loadFromString('M6S41GAL0gH0I97Hhy7A2-icf8dHnxXPmYIRwem03HE'); 13 | 14 | var link = new BrowserECDHLink('http://localhost:8080/conn', 'test-browser-responder-', key, 15 | isResponder: true, nodeProvider: new TestNodeProvider()); 16 | 17 | var f = link.connect(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lib/historian.dart: -------------------------------------------------------------------------------- 1 | library dslink.historian; 2 | 3 | import "dart:async"; 4 | import "dart:math"; 5 | 6 | import "package:dslink/dslink.dart"; 7 | import "package:dslink/nodes.dart"; 8 | import "package:dslink/utils.dart"; 9 | 10 | import 'package:dslink/convert_consts.dart'; 11 | 12 | part "src/historian/interval.dart"; 13 | part "src/historian/rollup.dart"; 14 | part "src/historian/get_history.dart"; 15 | part "src/historian/manage.dart"; 16 | part "src/historian/values.dart"; 17 | part "src/historian/adapter.dart"; 18 | part "src/historian/container.dart"; 19 | part "src/historian/publish.dart"; 20 | part "src/historian/main.dart"; 21 | 22 | LinkProvider _link; 23 | HistorianAdapter _historian; 24 | 25 | HistorianAdapter get historian => _historian; 26 | LinkProvider get link => _link; 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | * v1.0.7 - Handle mistyped upstream broker addresses. 4 | * v1.0.6 - Fix a logic error in removeChild method of Nodes. 5 | * v1.0.5 - Ensure that QOS downgrades are propagated. 6 | * v1.0.4+1 - Map logger names to match DSA log levels. 7 | * v1.0.4 - Forward any errors generated by a `list` request to the requester. 8 | * v1.0.3 - Update packages to use hosted rather than repository sources. 9 | * v1.0.2 - Properly forward QOS level changes to nodes when changing an existing subscription QOS level. 10 | * v1.0.1 - Fix bug in list subscriptions for nodes which have been removed and recreated throughout subscription's life. 11 | This may have prevented list subscriptions from updating full entries. 12 | * v1.0.0 - Start of changelog tracking. Too many existing changes to list. -------------------------------------------------------------------------------- /lib/src/responder/manager/trace.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | typedef ResponseTraceCallback(ResponseTrace update); 4 | 5 | class ResponseTrace { 6 | /// data path for trace 7 | String path; 8 | /// 'list' 'subscribe' 'invoke' 9 | String type; 10 | 11 | /// value is + - or blank string 12 | String change; 13 | 14 | /// action name, only needed by invoke 15 | String action; 16 | /// rid, only needed by invoke 17 | int rid; 18 | 19 | // {'name': 'path', 'type': 'string'}, 20 | // {'name': 'type', 'type': 'string'}, 21 | // {'name': 'rid', 'type': 'number'}, 22 | // {'name': 'action', 'type': 'string'}, 23 | // {'name': 'change', 'type': 'string'}, 24 | List get rowData => [path, type, rid, action, change]; 25 | 26 | ResponseTrace(this.path, this.type, this.rid, [this.change = '', this.action]); 27 | } 28 | -------------------------------------------------------------------------------- /example/browser/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DSA Browser Link 5 | 6 | 24 | 25 | 26 | 27 |
28 |

29 |
30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/src/requester/request/remove.dart: -------------------------------------------------------------------------------- 1 | part of dslink.requester; 2 | 3 | class RemoveController implements RequestUpdater { 4 | final Completer completer = new Completer(); 5 | Future get future => completer.future; 6 | 7 | final Requester requester; 8 | final String path; 9 | Request _request; 10 | 11 | RemoveController(this.requester, this.path) { 12 | var reqMap = { 13 | 'method': 'remove', 14 | 'path': path 15 | }; 16 | 17 | _request = requester._sendRequest(reqMap, this); 18 | } 19 | 20 | void onUpdate(String status, List updates, List columns, Map meta, DSError error) { 21 | // TODO implement error 22 | completer.complete(new RequesterUpdate(status)); 23 | } 24 | 25 | void onDisconnect() {} 26 | 27 | void onReconnect() {} 28 | } 29 | -------------------------------------------------------------------------------- /test/broker_discovery_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("vm") 2 | @Timeout(const Duration(seconds: 10)) 3 | library dslink.test.vm.discovery; 4 | 5 | import "package:test/test.dart"; 6 | 7 | main() => group("Broker Discovery", brokerDiscoveryTests); 8 | 9 | brokerDiscoveryTests() { 10 | // test("works with a single broker", () async { 11 | // var host = new BrokerDiscoveryClient(); 12 | // await host.init(true); 13 | // host.requests.listen((request) { 14 | // request.reply("http://127.0.0.1:8080"); 15 | // }); 16 | // 17 | // var client = new BrokerDiscoveryClient(); 18 | // await client.init(); 19 | // var urls = await client.discover().toList(); 20 | // expect(urls.length, equals(1)); 21 | // expect(urls.first, equals("http://127.0.0.1:8080")); 22 | // await host.close(); 23 | // await client.close(); 24 | // }, skip: true); 25 | } 26 | -------------------------------------------------------------------------------- /tool/experiment/browser/test_browser_link.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | delemmm 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tool/experiment/browser/test_browser_requester.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | delemmm 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tool/experiment/test_client_requester.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:dslink/client.dart'; 3 | import 'package:dslink/src/crypto/pk.dart'; 4 | import 'package:dslink/requester.dart'; 5 | import 'package:dslink/common.dart'; 6 | import 'dart:async'; 7 | import 'package:logging/logging.dart'; 8 | import 'package:dslink/utils.dart'; 9 | 10 | main() async { 11 | PrivateKey key = new PrivateKey.loadFromString('1aEqqRYk-yf34tcLRogX145szFsdVtrpywDEPuxRQtM BGt1WHhkwCn2nWSDXHTg-IxruXLrPPUlU--0ghiBIQC7HMWWcNQGAoO03l_BQYx7_DYn0sn2gWW9wESbixzWuKg'); 12 | 13 | var link = new HttpClientLink('http://rnd.iot-dsa.org/conn', 'rick-req-', key, 14 | isRequester: true); 15 | link.connect(); 16 | Requester requester = await link.onRequesterReady; 17 | updateLogLevel('debug'); 18 | 19 | // configure 20 | 21 | requester.subscribe('/upstream/benchmarks/conns/Benchmark-1/Node_1/Metric_1', (ValueUpdate update){print('${update.ts} : ${update.value}');}, 1); 22 | } 23 | -------------------------------------------------------------------------------- /tool/experiment/test_key_generate.dart: -------------------------------------------------------------------------------- 1 | import 'package:dslink/src/crypto/pk.dart'; 2 | import 'dart:io'; 3 | import '../../lib/utils.dart'; 4 | 5 | main() async{ 6 | String rslt; 7 | 8 | if (Platform.isWindows) { 9 | rslt = Process.runSync('getmac', []).stdout.toString(); 10 | } else { 11 | rslt = Process.runSync('ifconfig', []).stdout.toString(); 12 | } 13 | 14 | // randomize the PRNG with the system mac 15 | DSRandom.instance.addEntropy(rslt); 16 | 17 | var t1 = (new DateTime.now()).millisecondsSinceEpoch; 18 | PrivateKey key ; 19 | for (int i=0; i< 50; ++i) 20 | // generate private key 21 | key = await PrivateKey.generate(); 22 | 23 | var t2 = (new DateTime.now()).millisecondsSinceEpoch; 24 | 25 | print('takes ${t2-t1} ms to generate key'); 26 | print('dsaId: ${key.publicKey.getDsId('my-dsa-test-')}'); 27 | print('saved key:\n${key.saveToString()}'); 28 | print('public key:\n${key.publicKey.qBase64}'); 29 | //test token encrypt, decrypt 30 | } 31 | -------------------------------------------------------------------------------- /tool/experiment/browser/test_browser_requester.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, . All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:html'; 5 | import 'package:dslink/src/crypto/pk.dart'; 6 | import 'package:dslink/browser_client.dart'; 7 | import 'package:dslink/requester.dart'; 8 | import 'dart:async'; 9 | 10 | main() async { 11 | querySelector('#output').text = 'Your Dart app is running.'; 12 | 13 | PrivateKey key = new PrivateKey.loadFromString('M6S41GAL0gH0I97Hhy7A2-icf8dHnxXPmYIRwem03HE'); 14 | 15 | var link = new BrowserECDHLink('http://localhost:8080/conn', 'test-browser-responder-', key, 16 | isRequester:true); 17 | 18 | link.connect(); 19 | Requester requester = link.requester;//await link.onRequesterReady; 20 | 21 | Stream updates = requester.list('/conns/locker-a'); 22 | await for (RequesterListUpdate update in updates) { 23 | print(update.changes); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tool/experiment/browser/lockerdemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lockerdemo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Lockerdemo

16 | 17 |

Demo of 2 lockers

18 | 19 |
20 |

Closed

21 | 22 | 23 |
24 |
25 |

Closed

26 | 27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /example/browser/grid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DSA Grid 6 | 7 | 8 | 32 | 33 | 34 | 35 |
36 |
Clear Grid
37 |
38 |
39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/src/requester/request/set.dart: -------------------------------------------------------------------------------- 1 | part of dslink.requester; 2 | 3 | class SetController implements RequestUpdater { 4 | final Completer completer = new Completer(); 5 | Future get future => completer.future; 6 | final Requester requester; 7 | final String path; 8 | final Object value; 9 | Request _request; 10 | 11 | SetController(this.requester, this.path, this.value, 12 | [int maxPermission = Permission.CONFIG]) { 13 | var reqMap = { 14 | 'method': 'set', 15 | 'path': path, 16 | 'value': value 17 | }; 18 | 19 | if (maxPermission != Permission.CONFIG) { 20 | reqMap['permit'] = Permission.names[maxPermission]; 21 | } 22 | 23 | _request = requester._sendRequest(reqMap, this); 24 | } 25 | 26 | void onUpdate(String status, List updates, List columns, Map meta, DSError error) { 27 | completer.complete(new RequesterUpdate(status, error)); 28 | } 29 | 30 | void onDisconnect() {} 31 | 32 | void onReconnect() {} 33 | } 34 | -------------------------------------------------------------------------------- /test/common.dart: -------------------------------------------------------------------------------- 1 | library dslink.test.common; 2 | 3 | import "dart:async"; 4 | 5 | import "package:test/test.dart"; 6 | 7 | import "package:dslink/dslink.dart"; 8 | 9 | Future firstValueUpdate(Requester requester, String path) async { 10 | return await requester.getNodeValue(path); 11 | } 12 | 13 | Future gap() async { 14 | await new Future.delayed(const Duration(milliseconds: 300)); 15 | } 16 | 17 | expectNodeValue(from, String path, dynamic value) { 18 | if (from is Requester) { 19 | return new Future(() async { 20 | var update = await firstValueUpdate(from, path); 21 | expect(update.value, equals(value)); 22 | }); 23 | } else if (from is SimpleNodeProvider) { 24 | expect(from.getNode(path).lastValueUpdate.value, equals(value)); 25 | } else { 26 | throw new Exception("What is the from input? I don't understand."); 27 | } 28 | } 29 | 30 | SimpleNodeProvider createSimpleNodeProvider({ 31 | Map nodes, 32 | Map profiles}) { 33 | return new SimpleNodeProvider(nodes, profiles); 34 | } 35 | -------------------------------------------------------------------------------- /bin/beacon.dart: -------------------------------------------------------------------------------- 1 | import "dart:io"; 2 | 3 | import "package:args/args.dart"; 4 | 5 | import "package:dslink/dslink.dart" 6 | show BrokerDiscoveryClient, BrokerDiscoverRequest; 7 | 8 | main(List args) async { 9 | var argp = new ArgParser(allowTrailingOptions: true); 10 | var discovery = new BrokerDiscoveryClient(); 11 | 12 | argp.addFlag("help", 13 | abbr: "h", help: "Display Help Message", negatable: false); 14 | 15 | var opts = argp.parse(args); 16 | 17 | if (opts["help"] || opts.rest.isEmpty) { 18 | print("Usage: beacon "); 19 | if (argp.usage.isNotEmpty) { 20 | print(argp.usage); 21 | } 22 | exit(1); 23 | } 24 | 25 | try { 26 | await discovery.init(true); 27 | discovery.requests.listen((BrokerDiscoverRequest request) { 28 | for (var url in opts.rest) { 29 | request.reply(url); 30 | } 31 | }); 32 | } catch (e) { 33 | print( 34 | "Error: Failed to start the beacon service. Are you running another beacon or broker on this machine?"); 35 | exit(1); 36 | } 37 | 38 | print("Beacon Service Started."); 39 | } 40 | -------------------------------------------------------------------------------- /test/worker_test.dart: -------------------------------------------------------------------------------- 1 | library dslink.test.workers; 2 | 3 | import "package:test/test.dart"; 4 | import "package:dslink/worker.dart"; 5 | 6 | void main() { 7 | group("Function Worker", () { 8 | workerTests((m, [x]) => createWorker(m, metadata: x)); 9 | }); 10 | } 11 | 12 | void workerTests(WorkerSocket factory(WorkerFunction func, [Map metadata])) { 13 | test("calls a simple method that returns a result", () async { 14 | for (int i = 1; i <= 10; i++) { 15 | WorkerSocket socket = await factory(transformStringWorker).init(); 16 | 17 | try { 18 | for (int x = 1; x <= 5; x++) { 19 | var result = await socket.callMethod("transform", "Hello World") 20 | .timeout(const Duration(seconds: 3), onTimeout: () => null); 21 | 22 | expect(result, equals("hello world")); 23 | } 24 | } finally { 25 | await socket.close(); 26 | } 27 | } 28 | }); 29 | } 30 | 31 | transformStringWorker(Worker worker) async { 32 | await worker.init(methods: { 33 | "transform": (/*String*/ input) => input.toLowerCase() 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/historian/publish.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | class PublishValueAction extends SimpleNode { 4 | PublishValueAction(String path) : super(path); 5 | 6 | @override 7 | onInvoke(Map params) { 8 | var inputPath = params["Path"]; 9 | dynamic val = params["Value"]; 10 | String ts = params["Timestamp"]; 11 | 12 | if (ts == null) { 13 | ts = ValueUpdate.getTs(); 14 | } 15 | 16 | if (inputPath is! String) { 17 | throw "Path not provided."; 18 | } 19 | 20 | Path p = new Path(path); 21 | String tp = p 22 | .parent 23 | .child(NodeNamer.createName(inputPath)) 24 | .path; 25 | SimpleNode node = _link[tp]; 26 | 27 | WatchPathNode pn; 28 | if (node is! WatchPathNode) { 29 | pn = _link.addNode(tp, { 30 | r"$name": inputPath, 31 | r"$is": "watchPath", 32 | r"$publish": true, 33 | r"$type": "dynamic", 34 | r"$path": inputPath 35 | }); 36 | _link.save(); 37 | } else { 38 | pn = node; 39 | } 40 | 41 | pn.doUpdate(new ValueUpdate(val, ts: ts)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/utils/promise_timeout.dart: -------------------------------------------------------------------------------- 1 | part of dslink.utils; 2 | 3 | Future awaitWithTimeout(Future future, int timeoutMs, 4 | {Function onTimeout: null, 5 | Function onSuccessAfterTimeout: null, 6 | Function onErrorAfterTimeout: null}) { 7 | Completer completer = new Completer(); 8 | 9 | Timer timer = new Timer(new Duration(milliseconds: timeoutMs), () { 10 | if (!completer.isCompleted) { 11 | if (onTimeout != null) { 12 | onTimeout(); 13 | } 14 | completer.completeError(new Exception('Future timeout before complete')); 15 | } 16 | }); 17 | future.then((t) { 18 | if (completer.isCompleted) { 19 | if (onSuccessAfterTimeout != null) { 20 | onSuccessAfterTimeout(t); 21 | } 22 | } else { 23 | timer.cancel(); 24 | completer.complete(t); 25 | } 26 | }).catchError((err) { 27 | if (completer.isCompleted) { 28 | if (onErrorAfterTimeout != null) { 29 | onErrorAfterTimeout(err); 30 | } 31 | } else { 32 | timer.cancel(); 33 | completer.completeError(err); 34 | } 35 | }); 36 | 37 | return completer.future; 38 | } 39 | -------------------------------------------------------------------------------- /test/utils/base64_test.dart: -------------------------------------------------------------------------------- 1 | library dslink.test.common.base64; 2 | 3 | import "dart:convert"; 4 | 5 | import "package:test/test.dart"; 6 | import "package:dslink/utils.dart" show Base64; 7 | 8 | import "package:dslink/convert_consts.dart"; 9 | 10 | void main() { 11 | group("Base64", base64Tests); 12 | } 13 | 14 | const Map inputs = const { 15 | "Hello World": "SGVsbG8gV29ybGQ", 16 | "Goodbye World": "R29vZGJ5ZSBXb3JsZA" 17 | }; 18 | 19 | void base64Tests() { 20 | test("successfully encodes and decodes strings", () { 21 | for (var key in inputs.keys) { 22 | var encoded = Base64.encodeString(key); 23 | expect(encoded, equals(inputs[key])); 24 | var decoded = Base64.decodeString(inputs[key]); 25 | expect(decoded, equals(key)); 26 | } 27 | }); 28 | 29 | test("successfully encodes and decodes bytes", () { 30 | for (var key in inputs.keys) { 31 | var encoded = Base64.encode(UTF8.encode(key)); 32 | expect(encoded, equals(inputs[key])); 33 | var decoded = UTF8.decode(Base64.decode(inputs[key])); 34 | expect(decoded, equals(key)); 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /tool/experiment/test_qos_responder.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | 3 | import "package:dslink/dslink.dart"; 4 | import "package:dslink/src/storage/simple_storage.dart"; 5 | 6 | LinkProvider link; 7 | int lastNum; 8 | SimpleNode valueNode; 9 | 10 | 11 | main(List args) async { 12 | var defaultNodes = { 13 | 'node':{ 14 | r'$type':'string' 15 | } 16 | }; 17 | 18 | SimpleResponderStorage storage = new SimpleResponderStorage('storage'); 19 | 20 | List storedNodes = await storage.load(); 21 | 22 | link = new LinkProvider( 23 | ['-b', 'localhost:8080/conn', '--log', 'finest'], 24 | 'qos-resp', 25 | defaultNodes: defaultNodes 26 | ); 27 | 28 | if (link.link == null) { 29 | // initialization failed 30 | return; 31 | } 32 | 33 | link.link.responder.initStorage(storage, storedNodes); 34 | 35 | valueNode = link.getNode('/node'); 36 | 37 | new Timer.periodic(new Duration(seconds: 1), (t) { 38 | DateTime d = new DateTime.now(); 39 | valueNode.updateValue('${d.hour}:${d.minute}:${d.second}'); 40 | }); 41 | 42 | link.connect(); 43 | } 44 | -------------------------------------------------------------------------------- /tool/experiment/test_streamset.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | 3 | import "package:dslink/dslink.dart"; 4 | 5 | LinkProvider link; 6 | int lastNum; 7 | SimpleNode valueNode; 8 | 9 | main(List args) { 10 | var defaultNodes = { 11 | 'node': { 12 | r'$type':'string' 13 | } 14 | }; 15 | 16 | link = new LinkProvider( 17 | ['-b', 'localhost:8080/conn', '--log', 'finest'], 'streamset-req', 18 | defaultNodes: defaultNodes, isResponder: false, isRequester: true); 19 | if (link.link == null) { 20 | // initialization failed 21 | return; 22 | } 23 | 24 | link.connect(); 25 | link.onRequesterReady.then((Requester req) { 26 | Request rawreq; 27 | void fetchReq(Request v) { 28 | rawreq = v; 29 | int i = 0; 30 | new Timer.periodic(new Duration(seconds: 1), (Timer t) { 31 | rawreq.addReqParams({'Path':'/data/m1', 'Value':++i}); 32 | }); 33 | } 34 | req.invoke( 35 | '/data/streamingSet', {'Path':'/data/m1', 'Value':0}, Permission.CONFIG, 36 | fetchReq).listen((update) { 37 | print(update.updates); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/utils/action_test.dart: -------------------------------------------------------------------------------- 1 | library dslink.test.utils.actions; 2 | 3 | import "package:dslink/utils.dart"; 4 | import "package:test/test.dart"; 5 | import "package:dslink/historian.dart"; 6 | 7 | main() => group("Actions", actionTests); 8 | 9 | actionTests() { 10 | test("buildActionIO works", () { 11 | expect(buildActionIO({ 12 | "name": "string", 13 | "input": "string" 14 | }), equals([ 15 | { 16 | "name": "name", 17 | "type": "string" 18 | }, 19 | { 20 | "name": "input", 21 | "type": "string" 22 | } 23 | ])); 24 | }); 25 | 26 | test("buildEnumType works", () { 27 | expect(buildEnumType([ 28 | "A", 29 | "B", 30 | "C", 31 | "D" 32 | ]), equals("enum[A,B,C,D]")); 33 | }); 34 | 35 | test("test get_history", () { 36 | expect(parseInterval('1D'), equals(86400000)); 37 | MaxRollup rlp = new MaxRollup(); 38 | rlp.add(43); 39 | rlp.add("101"); 40 | expect(rlp.value, equals(101)); 41 | MinRollup mrlp = new MinRollup(); 42 | mrlp.add(43); 43 | mrlp.add("101"); 44 | expect(mrlp.value, equals(43)); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /example/worker_link.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | import "package:dslink/worker.dart"; 3 | 4 | LinkProvider link; 5 | 6 | main(List args) async { 7 | // Process the arguments and initializes the default nodes. 8 | link = new LinkProvider(args, "CounterWorker-", defaultNodes: { 9 | "Counter": { 10 | r"$type": "number", // The type of the node is a number. 11 | r"$writable": "write", // This node's value can be set by a requester. 12 | "?value": 0 // The default counter value. 13 | } 14 | }, encodePrettyJson: true); 15 | 16 | // Connect to the broker. 17 | link.connect(); 18 | 19 | SimpleNode counterNode = link["/Counter"]; 20 | counterNode.subscribe((update) => link.save()); 21 | 22 | WorkerSocket worker = await createWorker(counterWorker).init(); 23 | worker.addMethod("increment", (_) { 24 | counterNode.updateValue((counterNode.lastValueUpdate.value as int) + 1); 25 | }); 26 | } 27 | 28 | counterWorker(Worker worker) async { 29 | WorkerSocket socket = await worker.init(); 30 | Scheduler.every(Interval.ONE_SECOND, () async { 31 | await socket.callMethod("increment"); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /tool/travis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ./tool/analyze.sh 5 | ./tool/test.sh 6 | 7 | if [ "${TRAVIS_DART_VERSION}" == "dev" ] && [ "${TRAVIS_PULL_REQUEST}" == "false" ] && [ "${TRAVIS_BRANCH}" == "master" ] && [ "${TRAVIS_UPLOAD_DOCS}" == "true" ] 8 | then 9 | if [ ! -d ${HOME}/.ssh ] 10 | then 11 | mkdir ${HOME}/.ssh 12 | fi 13 | 14 | git config --global user.name "Travis CI" 15 | git config --global user.email "travis@iot-dsa.org" 16 | openssl aes-256-cbc -K $encrypted_afe27f9b0c58_key -iv $encrypted_afe27f9b0c58_iv -in tool/id_rsa.enc -out ${HOME}/.ssh/id_rsa -d 17 | chmod 600 ${HOME}/.ssh/id_rsa 18 | echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ${HOME}/.ssh/config 19 | ./tool/docs.sh --upload 20 | fi 21 | 22 | if [ "${TRAVIS_DART_VERSION}" == "stable" ] && [ "${TRAVIS_PULL_REQUEST}" == "false" ] && [ "${TRAVIS_BRANCH}" == "master" ] 23 | then 24 | if [ "${COVERALLS_TOKEN}" ] 25 | then 26 | pub global activate dart_coveralls 27 | pub global run dart_coveralls report \ 28 | --token ${COVERALLS_TOKEN} \ 29 | --retry 2 \ 30 | --exclude-test-files \ 31 | test/all.dart 32 | fi 33 | fi 34 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dslink 2 | version: 1.0.7 3 | description: "DSA IoT Platform - DSLink SDK for Dart" 4 | homepage: "http://iot-dsa.org" 5 | documentation: "https://iot-dsa.github.io/docs/sdks/dart/" 6 | dependencies: 7 | args: "^0.13.4" 8 | bignum: "^0.1.0" 9 | dscipher: "^0.7.2" 10 | # pointycastle: 1.0.2 11 | logging: "^0.11.3" 12 | crypto: ">=0.9.2 <3.0.0" 13 | path: "^1.3.7" 14 | msgpack: "^0.9.0" 15 | # msgpack: 16 | # git: https://github.com/tbelousova/msgpack.dart 17 | json_diff: '^0.1.2' 18 | # json_diff: 19 | # git: https://github.com/tbelousova/dart-json_diff 20 | authors: 21 | - Kenneth Endfinger 22 | - Rick Zhou 23 | - Michael Bullington 24 | - Matthew Butler 25 | - Joel Trottier-Hebert 26 | dev_dependencies: 27 | test: '0.12.15+8' 28 | # test: any 29 | dsbroker: "^1.1.1+1" 30 | # browser: '^0.10.0' 31 | # build_runner: any 32 | # build_test: any 33 | # build_web_compilers: any 34 | environment: 35 | # sdk: '>=2.0.0' 36 | sdk: '>=1.13.0 <2.0.0' 37 | executables: 38 | dsbeacon: beacon 39 | -------------------------------------------------------------------------------- /tool/experiment/dslink.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-link_provider", 3 | "version": "0.0.1", 4 | "description": "An example dslink to illustrate the usage of a dart dslink", 5 | "author": { 6 | "name": "Rick Zhou", 7 | "email": "z.zhou@dglogik.com" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Kenneth Endfinger", 12 | "email": "kaendfinger@gmail.com" 13 | } 14 | ], 15 | "main": "test_link_provider.dart", 16 | "engines": { 17 | "dart": ">=1.9.0" 18 | }, 19 | "getDependencies": [ 20 | "pub get" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/IOT-DSA/sdk-dslink-dart" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/IOT-DSA/sdk-dslink-dart/issues" 28 | }, 29 | "keywords": [ 30 | "test" 31 | ], 32 | "configs": { 33 | "broker": { 34 | "type": "url", 35 | "required": false 36 | }, 37 | "nodes": { 38 | "type": "path", 39 | "value": "nodes.json", 40 | "required": true 41 | }, 42 | "key": { 43 | "type": "path", 44 | "value": ".dslink.key", 45 | "required": true 46 | } 47 | }, 48 | "license": "Apache" 49 | } 50 | -------------------------------------------------------------------------------- /tool/experiment/lots_of_links.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | import "package:dslink/worker.dart"; 3 | 4 | int workers = 20; // Number of Workers 5 | 6 | main() async { 7 | WorkerPool pool = createWorkerPool(workers, linkWorker); // Create a Worker Pool 8 | await pool.init(); // Initialize the Worker Pool 9 | await pool.divide("spawn", 1000); // Divide 1000 calls to "spawn" over all the workers, which is 50 links per worker. 10 | } 11 | 12 | linkWorker(Worker worker) async { 13 | spawnLink(int i) async { 14 | updateLogLevel("OFF"); 15 | LinkProvider link = new LinkProvider([], "Worker-${i}-", defaultNodes: { // Create a Link Provider 16 | "string": { // Just a value so that things aren't empty. 17 | r"$name": "String Value", 18 | r"$type": "string", 19 | "?value": "Hello World" 20 | } 21 | }, autoInitialize: false); 22 | 23 | link.configure(); // Configure the Link 24 | link.init(); // Initialize the Link 25 | link.connect().then((_) { 26 | print("Link #${i} Connected."); 27 | }); // Connect to the Broker 28 | } 29 | 30 | await worker.init(methods: { // Initialize the Worker, and add a "spawn" method. 31 | "spawn": (i) { 32 | spawnLink(i); 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/responder/response.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | class Response implements ConnectionProcessor { 4 | final Responder responder; 5 | final int rid; 6 | String type; 7 | String _sentStreamStatus = StreamStatus.initialize; 8 | 9 | String get sentStreamStatus => _sentStreamStatus; 10 | 11 | Response(this.responder, this.rid, [this.type = null]); 12 | 13 | /// close the request from responder side and also notify the requester 14 | void close([DSError err = null]) { 15 | _sentStreamStatus = StreamStatus.closed; 16 | responder.closeResponse(rid, error: err, response: this); 17 | } 18 | 19 | /// close the response now, no need to send more response update 20 | void _close() {} 21 | 22 | void prepareSending() { 23 | if (!_pendingSending) { 24 | _pendingSending = true; 25 | responder.addProcessor(this); 26 | } 27 | } 28 | 29 | bool _pendingSending = false; 30 | 31 | void startSendingData(int currentTime, int waitingAckId) { 32 | _pendingSending = false; 33 | } 34 | 35 | void ackReceived(int receiveAckId, int startTime, int currentTime) { 36 | // TODO: implement ackReceived 37 | } 38 | 39 | /// for the broker trace action 40 | ResponseTrace getTraceData([String change = '+']) { 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/utils/list.dart: -------------------------------------------------------------------------------- 1 | part of dslink.utils; 2 | 3 | class ByteDataUtil { 4 | static Uint8List list2Uint8List(List input) { 5 | if (input is Uint8List) { 6 | return input; 7 | } 8 | return new Uint8List.fromList(input); 9 | } 10 | 11 | static ByteData mergeBytes(List bytesList) { 12 | if (bytesList.length == 1) { 13 | return bytesList[0]; 14 | } 15 | int totalLen = 0; 16 | for (ByteData bytes in bytesList) { 17 | totalLen += bytes.lengthInBytes; 18 | } 19 | ByteData output = new ByteData(totalLen); 20 | int pos = 0; 21 | for (ByteData bytes in bytesList) { 22 | output.buffer.asUint8List(pos).setAll(0, toUint8List(bytes)); 23 | pos += bytes.lengthInBytes; 24 | } 25 | return output; 26 | } 27 | 28 | static ByteData fromUint8List(Uint8List uintsList) { 29 | return uintsList.buffer 30 | .asByteData(uintsList.offsetInBytes, uintsList.lengthInBytes); 31 | } 32 | 33 | static Uint8List toUint8List(ByteData bytes) { 34 | return bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes); 35 | } 36 | 37 | static ByteData fromList(List input) { 38 | if (input is Uint8List) { 39 | return fromUint8List(input); 40 | } 41 | return fromUint8List(new Uint8List.fromList(input)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/server.dart: -------------------------------------------------------------------------------- 1 | /// DSA Broker Server 2 | library dslink.server; 3 | 4 | import "dart:io"; 5 | 6 | export "src/crypto/pk.dart"; 7 | 8 | abstract class IRemoteRequester { 9 | /// user when the requester is proxied to another responder 10 | String get responderPath; 11 | } 12 | 13 | ContentType _jsonContentType = new ContentType("application", "json", charset: "utf-8"); 14 | 15 | void updateResponseBeforeWrite(HttpRequest request, 16 | [int statusCode = HttpStatus.OK, 17 | ContentType contentType, 18 | bool noContentType = false]) { 19 | var response = request.response; 20 | 21 | if (statusCode != null) { 22 | response.statusCode = statusCode; 23 | } 24 | 25 | response.headers.set("Access-Control-Allow-Methods", "POST, OPTIONS, GET"); 26 | response.headers.set("Access-Control-Allow-Headers", "Content-Type"); 27 | String origin = request.headers.value("origin"); 28 | 29 | if (request.headers.value("x-proxy-origin") != null) { 30 | origin = request.headers.value("x-proxy-origin"); 31 | } 32 | 33 | if (origin == null) { 34 | origin = "*"; 35 | } 36 | 37 | response.headers.set("Access-Control-Allow-Origin", origin); 38 | 39 | if (!noContentType) { 40 | if (contentType == null) { 41 | contentType = _jsonContentType; 42 | } 43 | response.headers.contentType = contentType; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/utils/scheduler_test.dart: -------------------------------------------------------------------------------- 1 | library dslink.test.common.scheduler; 2 | 3 | import "dart:async"; 4 | 5 | import "package:test/test.dart"; 6 | import "package:dslink/utils.dart"; 7 | 8 | void main() { 9 | group("Scheduler", schedulerTests); 10 | } 11 | 12 | void schedulerTests() { 13 | test("runs after a 2 millisecond delay", () { 14 | var completer = new Completer.sync(); 15 | 16 | Scheduler.after(new Duration(milliseconds: 2), () { 17 | completer.complete(); 18 | }); 19 | 20 | return new Future.delayed(new Duration(milliseconds: 5), () { 21 | expect(completer.isCompleted, isTrue, reason: "Completer should be completed."); 22 | }); 23 | }); 24 | 25 | test("schedules a function on the event loop", () { 26 | var completer = new Completer.sync(); 27 | 28 | Scheduler.runLater(() { 29 | completer.complete(); 30 | }); 31 | 32 | return new Future(() { 33 | expect(completer.isCompleted, isTrue, reason: "Completer should be completed."); 34 | }); 35 | }); 36 | 37 | test("schedules a function later", () { 38 | var completer = new Completer.sync(); 39 | 40 | Scheduler.later(() { 41 | completer.complete(); 42 | }); 43 | 44 | return new Future(() { 45 | expect(completer.isCompleted, isTrue, reason: "Completer should be completed."); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /lib/responder.dart: -------------------------------------------------------------------------------- 1 | /// DSA Responder API 2 | library dslink.responder; 3 | 4 | import "dart:async"; 5 | import "dart:collection"; 6 | import "dart:typed_data"; 7 | 8 | import "common.dart"; 9 | import "utils.dart"; 10 | 11 | //FIXME:Dart1.0 12 | //*Dart1-open-block 13 | import 'package:dscipher/block/aes_fast.dart'; 14 | import 'package:dscipher/params/key_parameter.dart'; 15 | import 'dart:convert'; 16 | // Dart1-close-block*/ 17 | 18 | //FIXME:Dart2.0 19 | /*Dart2-open-block 20 | import "package:dslink/convert_consts.dart"; 21 | import "package:pointycastle/export.dart"; 22 | Dart2-close-block*/ 23 | /* 24 | import "package:pointycastle/block/aes_fast.dart"; 25 | import "package:pointycastle/api.dart"; 26 | */ 27 | 28 | 29 | part "src/responder/responder.dart"; 30 | part "src/responder/response.dart"; 31 | part "src/responder/node_provider.dart"; 32 | part "src/responder/response/subscribe.dart"; 33 | part "src/responder/response/list.dart"; 34 | part "src/responder/response/invoke.dart"; 35 | 36 | part "src/responder/base_impl/local_node_impl.dart"; 37 | part "src/responder/base_impl/config_setting.dart"; 38 | part "src/responder/base_impl/def_node.dart"; 39 | part "src/responder/simple/simple_node.dart"; 40 | 41 | part "src/responder/manager/permission_manager.dart"; 42 | part "src/responder/manager/trace.dart"; 43 | part "src/responder/manager/storage.dart"; 44 | -------------------------------------------------------------------------------- /lib/src/common/default_defs.dart: -------------------------------------------------------------------------------- 1 | part of dslink.common; 2 | 3 | Map defaultProfileMap = { 4 | "node": {}, 5 | "static": {}, 6 | "getHistory": { 7 | r"$invokable": "read", 8 | r"$result": "table", 9 | r"$params": [ 10 | {"name": "Timerange", "type": "string", "editor": "daterange"}, 11 | { 12 | "name": "Interval", 13 | "type": "enum", 14 | "editor": buildEnumType([ 15 | "default", 16 | "none", 17 | "1Y", 18 | "3N", 19 | "1N", 20 | "1W", 21 | "1D", 22 | "12H", 23 | "6H", 24 | "4H", 25 | "3H", 26 | "2H", 27 | "1H", 28 | "30M", 29 | "15M", 30 | "10M", 31 | "5M", 32 | "1M", 33 | "30S", 34 | "15S", 35 | "10S", 36 | "5S", 37 | "1S" 38 | ]) 39 | }, 40 | { 41 | "name": "Rollup", 42 | "type": buildEnumType([ 43 | "avg", 44 | "min", 45 | "max", 46 | "sum", 47 | "first", 48 | "last", 49 | "and", 50 | "or", 51 | "count", 52 | "auto" 53 | ]) 54 | } 55 | ], 56 | r"$columns": [ 57 | {"name": "timestamp", "type": "time"}, 58 | {"name": "value", "type": "dynamic"} 59 | ] 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /example/browser/image.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/browser.dart"; 2 | 3 | import "dart:html"; 4 | import "dart:typed_data"; 5 | 6 | LinkProvider link; 7 | Requester requester; 8 | ImageElement image; 9 | 10 | main() async { 11 | image = querySelector("#image"); 12 | 13 | var brokerUrl = await BrowserUtils.fetchBrokerUrlFromPath("broker_url", "http://localhost:8080/conn"); 14 | 15 | link = new LinkProvider(brokerUrl, "ImageDisplay-", isRequester: true); 16 | 17 | await link.connect(); 18 | requester = await link.onRequesterReady; 19 | window.onHashChange.listen((event) { 20 | setup(window.location.hash.substring(1)); 21 | }); 22 | 23 | setup(window.location.hash.isNotEmpty ? window.location.hash.substring(1) : "/data/image"); 24 | } 25 | 26 | setup(String path) { 27 | print("Displaying Image from ${path}"); 28 | 29 | if (listener != null) { 30 | listener.cancel(); 31 | listener = null; 32 | } 33 | 34 | listener = requester.subscribe(path, handleValueUpdate, 0); 35 | } 36 | 37 | String url; 38 | 39 | handleValueUpdate(ValueUpdate update) { 40 | if (update.value == null) { 41 | return; 42 | } 43 | 44 | if (url != null) { 45 | Url.revokeObjectUrl(url); 46 | } 47 | 48 | ByteData data = update.value; 49 | var bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); 50 | 51 | var blob = new Blob([bytes], "image/jpeg"); 52 | 53 | url = image.src = Url.createObjectUrl(blob); 54 | } 55 | 56 | ReqSubscribeListener listener; 57 | -------------------------------------------------------------------------------- /lib/src/responder/base_impl/def_node.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | typedef InvokeResponse InvokeCallback(Map params, Responder responder, 4 | InvokeResponse response, LocalNode parentNode); 5 | 6 | /// definition nodes are serializable node that won"t change 7 | /// the only change will be a global upgrade 8 | class DefinitionNode extends LocalNodeImpl { 9 | final NodeProvider provider; 10 | 11 | DefinitionNode(String path, this.provider) : super(path) { 12 | this.configs[r"$is"] = "static"; 13 | } 14 | 15 | InvokeCallback _invokeCallback; 16 | 17 | void setInvokeCallback(InvokeCallback callback) { 18 | _invokeCallback = callback; 19 | } 20 | 21 | @override 22 | InvokeResponse invoke( 23 | Map params, 24 | Responder responder, 25 | InvokeResponse response, 26 | Node parentNode, 27 | [int maxPermission = Permission.CONFIG]) { 28 | if (_invokeCallback == null) { 29 | return response..close(DSError.NOT_IMPLEMENTED); 30 | } 31 | 32 | String parentPath = parentNode is LocalNode ? parentNode.path : null; 33 | 34 | int permission = responder.nodeProvider.permissions.getPermission( 35 | parentPath, 36 | responder 37 | ); 38 | 39 | if (maxPermission < permission) { 40 | permission = maxPermission; 41 | } 42 | 43 | if (getInvokePermission() <= permission) { 44 | _invokeCallback(params, responder, response, parentNode); 45 | return response; 46 | } else { 47 | return response..close(DSError.PERMISSION_DENIED); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/common/connection_channel.dart: -------------------------------------------------------------------------------- 1 | part of dslink.common; 2 | 3 | class PassiveChannel implements ConnectionChannel { 4 | final StreamController onReceiveController = 5 | new StreamController(); 6 | Stream get onReceive => onReceiveController.stream; 7 | 8 | List _processors = []; 9 | 10 | final Connection conn; 11 | 12 | PassiveChannel(this.conn, [this.connected = false]) {} 13 | 14 | ConnectionHandler handler; 15 | void sendWhenReady(ConnectionHandler handler) { 16 | this.handler = handler; 17 | conn.requireSend(); 18 | } 19 | 20 | ProcessorResult getSendingData(int currentTime, int waitingAckId){ 21 | if (handler != null) { 22 | ProcessorResult rslt = handler.getSendingData(currentTime, waitingAckId); 23 | //handler = null; 24 | return rslt; 25 | } 26 | return null; 27 | } 28 | 29 | bool _isReady = false; 30 | bool get isReady => _isReady; 31 | void set isReady(bool val) { 32 | _isReady = val; 33 | } 34 | 35 | bool connected = true; 36 | 37 | final Completer onDisconnectController = 38 | new Completer(); 39 | Future get onDisconnected => onDisconnectController.future; 40 | 41 | final Completer onConnectController = 42 | new Completer(); 43 | Future get onConnected => onConnectController.future; 44 | 45 | void updateConnect() { 46 | if (connected) return; 47 | connected = true; 48 | onConnectController.complete(this); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/historian/values.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | class HistorySummary { 4 | final ValuePair first; 5 | final ValuePair last; 6 | 7 | HistorySummary(this.first, this.last); 8 | } 9 | 10 | class ValuePair { 11 | final String timestamp; 12 | final dynamic value; 13 | 14 | DateTime get time => DateTime.parse(timestamp); 15 | 16 | ValuePair(this.timestamp, this.value); 17 | 18 | List toRow() { 19 | return [timestamp, value]; 20 | } 21 | } 22 | 23 | class TimeRange { 24 | final DateTime start; 25 | final DateTime end; 26 | 27 | TimeRange(this.start, this.end); 28 | 29 | Duration get duration => end.difference(start); 30 | 31 | bool isWithin(DateTime time) { 32 | bool valid = (time.isAfter(start) || time.isAtSameMomentAs(start)); 33 | if (end != null) { 34 | valid = valid && (time.isBefore(end) || time.isAtSameMomentAs(end)); 35 | } 36 | return valid; 37 | } 38 | } 39 | 40 | class ValueEntry { 41 | final String group; 42 | final String path; 43 | final String timestamp; 44 | final dynamic value; 45 | 46 | ValueEntry(this.group, this.path, this.timestamp, this.value); 47 | 48 | ValuePair asPair() { 49 | return new ValuePair(timestamp, value); 50 | } 51 | 52 | DateTime get time => DateTime.parse(timestamp); 53 | } 54 | 55 | TimeRange parseTimeRange(String input) { 56 | TimeRange tr; 57 | if (input != null) { 58 | List l = input.split("/"); 59 | DateTime start = DateTime.parse(l[0]); 60 | DateTime end = DateTime.parse(l[1]); 61 | 62 | tr = new TimeRange(start, end); 63 | } 64 | return tr; 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/historian/main.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | historianMain(List args, String name, HistorianAdapter adapter) async { 4 | _historian = adapter; 5 | 6 | _link = new LinkProvider( 7 | args, 8 | "${name}-", 9 | isRequester: true, 10 | autoInitialize: false, 11 | nodes: { 12 | "addDatabase": { 13 | r"$name": "Add Database", 14 | r"$invokable": "write", 15 | r"$params": >[ 16 | { 17 | "name": "Name", 18 | "type": "string", 19 | "placeholder": "HistoryData" 20 | } 21 | ]..addAll(adapter.getCreateDatabaseParameters()), 22 | r"$is": "addDatabase" 23 | } 24 | }, 25 | profiles: { 26 | "createWatchGroup": (String path) => new CreateWatchGroupNode(path), 27 | "addDatabase": (String path) => new AddDatabaseNode(path), 28 | "addWatchPath": (String path) => new AddWatchPathNode(path), 29 | "watchGroup": (String path) => new WatchGroupNode(path), 30 | "watchPath": (String path) => new WatchPathNode(path), 31 | "database": (String path) => new DatabaseNode(path), 32 | "delete": (String path) => new DeleteActionNode.forParent( 33 | path, _link.provider as MutableNodeProvider, onDelete: () { 34 | _link.save(); 35 | }), 36 | "purgePath": (String path) => new PurgePathNode(path), 37 | "purgeGroup": (String path) => new PurgeGroupNode(path), 38 | "publishValue": (String path) => new PublishValueAction(path) 39 | }, 40 | encodePrettyJson: true 41 | ); 42 | _link.init(); 43 | _link.connect(); 44 | } 45 | -------------------------------------------------------------------------------- /example/simple_link.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | import "dart:io"; 3 | import "dart:typed_data"; 4 | 5 | LinkProvider link; 6 | 7 | main(List args) async { 8 | // Process the arguments and initializes the default nodes. 9 | link = new LinkProvider(args, "Simple-", defaultNodes: { 10 | "message": { 11 | r"$name": "Message", // The pretty name of this node. 12 | r"$type": "string", // The type of the node is a string. 13 | r"$writable": "write", // This node's value can be set by a requester. 14 | "?value": null, // The default message value. 15 | "@icon": "dart-example-simple/message" 16 | } 17 | }, encodePrettyJson: true, commandLineOptions: { 18 | "default-message": "Hello World" 19 | }); 20 | 21 | SimpleNodeProvider provider = link.provider; 22 | 23 | if (provider["/message"].value == null) { 24 | provider.updateValue("/message", link.parsedArguments["default-message"]); 25 | } 26 | 27 | provider.setIconResolver((String name) async { 28 | if (name == "dart-example-simple/message") { 29 | var file = new File(Platform.script.resolve("message.png").toFilePath()); 30 | if (await file.exists()) { 31 | Uint8List data = await file.readAsBytes(); 32 | return data.buffer.asByteData( 33 | data.offsetInBytes, 34 | data.lengthInBytes 35 | ); 36 | } 37 | } 38 | return null; 39 | }); 40 | 41 | // Connect to the broker. 42 | link.connect(); 43 | 44 | // Save the message when it changes. 45 | if (link.valuePersistenceEnabled) { 46 | link.onValueChange("/message").listen((_) => link.save()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/utils/dslink_json.dart: -------------------------------------------------------------------------------- 1 | part of dslink.utils; 2 | 3 | class DSLinkJSON { 4 | Map _json; 5 | 6 | Map get json => _json; 7 | 8 | String name; 9 | String version; 10 | String description; 11 | String main; 12 | Map engines = {}; 13 | Map> configs = {}; 14 | List getDependencies = []; 15 | 16 | DSLinkJSON(); 17 | 18 | factory DSLinkJSON.from(Map map) { 19 | var j = new DSLinkJSON(); 20 | j._json = map; 21 | j.name = map["name"]; 22 | j.version = map["version"]; 23 | j.description = map["description"]; 24 | j.main = map["main"]; 25 | j.engines = map["engines"] as Map; 26 | j.configs = map["configs"] as Map>; 27 | j.getDependencies = map["getDependencies"] as List; 28 | return j; 29 | } 30 | 31 | void verify() { 32 | if (name == null) { 33 | throw new Exception("DSLink Name is required."); 34 | } 35 | 36 | if (main == null) { 37 | throw new Exception("DSLink Main Script is required."); 38 | } 39 | } 40 | 41 | Map save() { 42 | verify(); 43 | 44 | var map = new Map.from(_json != null ? _json : {}); 45 | map["name"] = name; 46 | map["version"] = version; 47 | map["description"] = description; 48 | map["main"] = main; 49 | map["engines"] = engines; 50 | map["configs"] = configs; 51 | map["getDependencies"] = getDependencies; 52 | for (var key in map.keys.toList()) { 53 | if (map[key] == null) { 54 | map.remove(key); 55 | } 56 | } 57 | return map; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tool/experiment/sample_responder.dart: -------------------------------------------------------------------------------- 1 | library dslink.test.sampleresponder; 2 | 3 | import 'package:dslink/responder.dart'; 4 | import 'package:dslink/common.dart'; 5 | import 'dart:async'; 6 | 7 | class TestNodeProvider extends NodeProvider { 8 | TestNode onlyNode; 9 | 10 | TestNodeProvider() { 11 | onlyNode = new TestNode('/', this); 12 | } 13 | 14 | LocalNode getNode(String path) { 15 | return onlyNode; 16 | } 17 | 18 | IPermissionManager permissions = new DummyPermissionManager(); 19 | 20 | Responder createResponder(String dsId, String sessionId) { 21 | return new Responder(this, dsId); 22 | } 23 | 24 | LocalNode getOrCreateNode(String path, [bool addToTree = true]) { 25 | return onlyNode; 26 | } 27 | } 28 | 29 | class TestNode extends LocalNodeImpl { 30 | NodeProvider provider; 31 | 32 | TestNode(String path, this.provider) : super(path) { 33 | new Timer.periodic(const Duration(seconds: 5), updateTime); 34 | configs[r'$is'] = 'node'; 35 | configs[r'$test'] = 'hello world'; 36 | } 37 | 38 | int count = 0; 39 | 40 | void updateTime(Timer t) { 41 | updateValue(count++); 42 | } 43 | 44 | bool get exists => true; 45 | 46 | @override 47 | InvokeResponse invoke(Map params, 48 | Responder responder, 49 | InvokeResponse response, 50 | Node parentNode, 51 | [int maxPermission = Permission.CONFIG]) { 52 | response.updateStream( 53 | [[1, 2]], streamStatus: StreamStatus.closed, columns: [{ 54 | 'name': 'v1', 55 | 'type': 'number' 56 | }, { 57 | 'name': 'v2', 58 | 'type': 'number' 59 | } 60 | ]); 61 | return response; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/query/query_command.dart: -------------------------------------------------------------------------------- 1 | part of dslink.query; 2 | 3 | abstract class BrokerQueryCommand { 4 | final BrokerQueryManager _manager; 5 | BrokerQueryCommand(this._manager) {} 6 | 7 | BrokerQueryCommand base; 8 | Set nexts = new Set(); 9 | Set responses = new Set(); 10 | 11 | void addResponse(InvokeResponse response) { 12 | response.onClose = _onResponseClose; 13 | responses.add(response); 14 | } 15 | 16 | void _onResponseClose(InvokeResponse response) { 17 | responses.remove(response); 18 | if (responses.isEmpty && nexts.isEmpty) { 19 | destroy(); 20 | } 21 | } 22 | 23 | void addNext(BrokerQueryCommand next) { 24 | nexts.add(next); 25 | } 26 | 27 | void removeNext(BrokerQueryCommand next) { 28 | nexts.remove(next); 29 | if (nexts.isEmpty && responses.isEmpty) { 30 | destroy(); 31 | } 32 | } 33 | 34 | /// init after checking command is not a duplication 35 | void init() {} 36 | 37 | void updateFromBase(List updats); 38 | 39 | void destroy() { 40 | for (InvokeResponse resp in responses) { 41 | resp.close; 42 | } 43 | if (base != null) { 44 | base.removeNext(this); 45 | } 46 | _manager._dict.remove(getQueryId()); 47 | } 48 | 49 | String _cachedQueryId; 50 | 51 | /// return a unified String as the key of the map 52 | String getQueryId() { 53 | if (_cachedQueryId == null) { 54 | if (base != null) { 55 | _cachedQueryId = '$base|$this'; 56 | } else { 57 | _cachedQueryId = toString(); 58 | } 59 | } 60 | return _cachedQueryId; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/historian/interval.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | const Map, int> _intervalTypes = const { 4 | const ["ms", "millis", "millisecond", "milliseconds"]: 1, 5 | const ["s", "second", "seconds"]: 1000, 6 | const ["m", "min", "minute", "minutes"]: 60000, 7 | const ["h", "hr", "hour", "hours"]: 3600000, 8 | const ["d", "day", "days"]: 86400000, 9 | const ["wk", "week", "weeks"]: 604800000, 10 | const ["n", "month", "months"]: 2628000000, 11 | const ["year", "years", "y"]: 31536000000 12 | }; 13 | 14 | List __intervalAllTypes; 15 | 16 | List get _intervalAllTypes { 17 | if (__intervalAllTypes == null) { 18 | __intervalAllTypes = _intervalTypes 19 | .keys 20 | .expand((key) => key) 21 | .toList(); 22 | __intervalAllTypes.sort(); 23 | } 24 | return __intervalAllTypes; 25 | } 26 | 27 | final RegExp _intervalPattern = new RegExp( 28 | "^(\\d*?.?\\d*?)(${_intervalAllTypes.join('|')})\$" 29 | ); 30 | 31 | int parseInterval(String input) { 32 | if (input == null) { 33 | return 0; 34 | } 35 | 36 | /// Sanitize Input 37 | input = input.trim().toLowerCase().replaceAll(" ", ""); 38 | 39 | if (input == "none") { 40 | return 0; 41 | } 42 | 43 | if (input == "default") { 44 | return 0; 45 | } 46 | 47 | if (!_intervalPattern.hasMatch(input)) { 48 | throw new FormatException("Bad Interval Syntax: ${input}"); 49 | } 50 | 51 | var match = _intervalPattern.firstMatch(input); 52 | var multiplier = num.parse(match[1]); 53 | var typeName = match[2]; 54 | var typeKey = _intervalTypes.keys.firstWhere((x) => x.contains(typeName)); 55 | var type = _intervalTypes[typeKey]; 56 | return (multiplier * type).round(); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/common/table.dart: -------------------------------------------------------------------------------- 1 | part of dslink.common; 2 | 3 | class TableColumn { 4 | String type; 5 | String name; 6 | Object defaultValue; 7 | 8 | TableColumn(this.name, this.type, [this.defaultValue]); 9 | 10 | Map getData() { 11 | var rslt = { 12 | "type": type, 13 | "name": name 14 | }; 15 | 16 | if (defaultValue != null) { 17 | rslt["default"] = defaultValue; 18 | } 19 | return rslt; 20 | } 21 | 22 | /// convert tableColumns into List of Map 23 | static List> serializeColumns(List list) { 24 | var rslts = >[]; 25 | for (Object m in list) { 26 | if (m is Map) { 27 | rslts.add(m); 28 | } else if (m is TableColumn) { 29 | rslts.add(m.getData()); 30 | } 31 | } 32 | return rslts; 33 | } 34 | 35 | /// parse List of Map into TableColumn 36 | static List parseColumns(List list) { 37 | List rslt = []; 38 | for (Object m in list) { 39 | if (m is Map && m["name"] is String) { 40 | String type = "string"; 41 | if (m["type"] is String) { 42 | type = m["type"]; 43 | } 44 | rslt.add(new TableColumn(m["name"], type, m["default"])); 45 | } else if (m is TableColumn) { 46 | rslt.add(m); 47 | } else { 48 | // invalid column data 49 | return null; 50 | } 51 | } 52 | return rslt; 53 | } 54 | } 55 | 56 | class Table { 57 | List columns; 58 | List rows; 59 | Map meta; 60 | 61 | Table(this.columns, this.rows, {this.meta}); 62 | } 63 | 64 | class TableColumns { 65 | final List columns; 66 | 67 | TableColumns(this.columns); 68 | } 69 | 70 | class TableMetadata { 71 | final Map meta; 72 | 73 | TableMetadata(this.meta); 74 | } 75 | -------------------------------------------------------------------------------- /example/actions_link.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | 3 | LinkProvider link; 4 | 5 | main(List args) async { 6 | // Process the arguments and initializes the default nodes. 7 | link = new LinkProvider(args, "Actions-", defaultNodes: { 8 | "message": { 9 | r"$name": "Message", // The pretty name of this node. 10 | r"$type": "string", // The type of the node is a string. 11 | r"$writable": "write", // This node's value can be set by a requester. 12 | "?value": "Hello World", // The default message value. 13 | "reset": { // An action on the message node. 14 | r"$name": "Reset", // The pretty name of this action. 15 | r"$is": "reset", // This node takes on the 'reset' profile. 16 | r"$invokable": "write", // Invoking this action requires write permissions. 17 | r"$params": [], // This action does not have any parameters. 18 | r"$result": "values", // This action returns a single row of values. 19 | r"$columns": [] // This action does not return any actual values. 20 | } 21 | } 22 | }, profiles: { 23 | "reset": (String path) => new ResetNode(path) // The reset profile should use this function to create the node object. 24 | }, encodePrettyJson: true); 25 | 26 | // Connect to the broker. 27 | link.connect(); 28 | 29 | // Save the message when it changes. 30 | if (link.valuePersistenceEnabled) { 31 | link.onValueChange("/message").listen((update) => link.save()); 32 | } 33 | } 34 | 35 | // A simple node that resets the message value. 36 | class ResetNode extends SimpleNode { 37 | ResetNode(String path, [SimpleNodeProvider provider]) : super(path, provider) { 38 | print("========= CREATE RESET ========="); 39 | } 40 | 41 | @override 42 | onInvoke(Map params) { 43 | link.updateValue("/Message", "Hello World"); // Update the value of the message node. 44 | return {}; // Return an empty row of values. 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/requester/request.dart: -------------------------------------------------------------------------------- 1 | part of dslink.requester; 2 | 3 | /// request class handles raw response from responder 4 | class Request { 5 | final Requester requester; 6 | final int rid; 7 | final Map data; 8 | 9 | /// raw request callback 10 | final RequestUpdater updater; 11 | bool _isClosed = false; 12 | bool get isClosed => _isClosed; 13 | 14 | Request(this.requester, this.rid, this.updater, this.data); 15 | 16 | String streamStatus = StreamStatus.initialize; 17 | 18 | /// resend the data if previous sending failed 19 | void resend() { 20 | requester.addToSendList(data); 21 | } 22 | 23 | void addReqParams(Map m) { 24 | requester.addToSendList({'rid':rid, 'params':m}); 25 | } 26 | 27 | void _update(Map m) { 28 | if (m["stream"] is String) { 29 | streamStatus = m["stream"]; 30 | } 31 | List updates; 32 | List columns; 33 | Map meta; 34 | if (m["updates"] is List) { 35 | updates = m["updates"]; 36 | } 37 | if (m["columns"] is List) { 38 | columns = m["columns"]; 39 | } 40 | if (m["meta"] is Map) { 41 | meta = m["meta"]; 42 | } 43 | // remove the request from global Map 44 | if (streamStatus == StreamStatus.closed) { 45 | requester._requests.remove(rid); 46 | } 47 | DSError error; 48 | if (m.containsKey("error") && m["error"] is Map) { 49 | error = new DSError.fromMap(m["error"]); 50 | requester._errorController.add(error); 51 | } 52 | 53 | updater.onUpdate(streamStatus, updates, columns, meta, error); 54 | } 55 | 56 | /// close the request and finish data 57 | void _close([DSError error]) { 58 | if (streamStatus != StreamStatus.closed) { 59 | streamStatus = StreamStatus.closed; 60 | updater.onUpdate(StreamStatus.closed, null, null, null, error); 61 | } 62 | } 63 | 64 | /// close the request from the client side 65 | void close() { 66 | // _close will also be called later from the requester; 67 | requester.closeRequest(this); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/query/query_manager.dart: -------------------------------------------------------------------------------- 1 | part of dslink.query; 2 | 3 | class BrokerQueryManager { 4 | NodeProvider provider; 5 | 6 | BrokerQueryManager(this.provider) {} 7 | 8 | BrokerQueryCommand parseList(List str) { 9 | // TODO: implement this 10 | return null; 11 | } 12 | 13 | BrokerQueryCommand parseDql(String str) { 14 | if (str.startsWith('[')) { 15 | return parseList(DsJson.decode(str)); 16 | } 17 | // TODO: implement full dql spec 18 | // this is just a temp quick parser for basic /data node query 19 | List commands = str.split('|').map((x) => x.trim()).toList(); 20 | if (commands.length == 2 && 21 | commands[0].startsWith('list /data') && 22 | commands[1].startsWith('subscribe')) { 23 | String path = commands[0].substring(5); 24 | BrokerQueryCommand listcommand = new QueryCommandList(path, this); 25 | listcommand = _getOrAddCommand(listcommand); 26 | if (listcommand == null) { 27 | return null; 28 | } 29 | BrokerQueryCommand subcommand = new QueryCommandSubscribe(this); 30 | subcommand.base = listcommand; 31 | subcommand = _getOrAddCommand(subcommand); 32 | return subcommand; 33 | } 34 | return null; 35 | } 36 | 37 | Map _dict = new Map(); 38 | 39 | BrokerQueryCommand _getOrAddCommand(BrokerQueryCommand command) { 40 | String key = command.getQueryId(); 41 | if (_dict.containsKey(key)) { 42 | return _dict[key]; 43 | } 44 | try { 45 | command.init(); 46 | } catch (err) { 47 | command.destroy(); 48 | return null; 49 | } 50 | 51 | // add to base command's next 52 | if (command.base != null) { 53 | command.base.addNext(command); 54 | } else if (command is QueryCommandList) { 55 | // all list command start from root node 56 | command.updateFromBase([ 57 | [provider.getNode('/'), '+'] 58 | ]); 59 | } 60 | _dict[key] = command; 61 | return command; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/utils/uri_component.dart: -------------------------------------------------------------------------------- 1 | part of dslink.utils; 2 | 3 | /// a decoder class to decode malformed url encoded string 4 | class UriComponentDecoder { 5 | static const int _SPACE = 0x20; 6 | static const int _PERCENT = 0x25; 7 | static const int _PLUS = 0x2B; 8 | 9 | static String decode(String text) { 10 | List codes = new List(); 11 | List bytes = new List(); 12 | int len = text.length; 13 | for (int i = 0; i < len; i++) { 14 | var codeUnit = text.codeUnitAt(i); 15 | if (codeUnit == _PERCENT) { 16 | if (i + 3 > text.length) { 17 | bytes.add(_PERCENT); 18 | continue; 19 | } 20 | int hexdecoded = _hexCharPairToByte(text, i + 1); 21 | if (hexdecoded > 0) { 22 | bytes.add(hexdecoded); 23 | i += 2; 24 | } else { 25 | bytes.add(_PERCENT); 26 | } 27 | } else { 28 | if (!bytes.isEmpty) { 29 | codes.addAll( 30 | const Utf8Decoder(allowMalformed: true) 31 | .convert(bytes) 32 | .codeUnits); 33 | bytes.clear(); 34 | } 35 | if (codeUnit == _PLUS) { 36 | codes.add(_SPACE); 37 | } else { 38 | codes.add(codeUnit); 39 | } 40 | } 41 | } 42 | 43 | if (!bytes.isEmpty) { 44 | codes.addAll(const Utf8Decoder() 45 | .convert(bytes) 46 | .codeUnits); 47 | bytes.clear(); 48 | } 49 | return new String.fromCharCodes(codes); 50 | } 51 | 52 | static int _hexCharPairToByte(String s, int pos) { 53 | int byte = 0; 54 | for (int i = 0; i < 2; i++) { 55 | int charCode = s.codeUnitAt(pos + i); 56 | if (0x30 <= charCode && charCode <= 0x39) { 57 | byte = byte * 16 + charCode - 0x30; 58 | } else if ((charCode >= 0x41 && charCode <= 0x46) || 59 | (charCode >= 0x61 && charCode <= 0x66)) { 60 | // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66). 61 | charCode |= 0x20; 62 | byte = byte * 16 + charCode - 0x57; 63 | } else { 64 | return -1; 65 | } 66 | } 67 | return byte; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/simple_nodes_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("vm") 2 | @Timeout(const Duration(seconds: 10)) 3 | library dslink.test.vm.nodes.simple; 4 | 5 | import "package:dslink/dslink.dart"; 6 | import "package:test/test.dart"; 7 | 8 | import "common.dart"; 9 | 10 | main() { 11 | group("SimpleNode", simpleNodeTests); 12 | } 13 | 14 | simpleNodeTests() { 15 | test("retains configs", () { 16 | var provider = createSimpleNodeProvider(nodes: { 17 | "message": { 18 | r"$name": "A Message", 19 | r"$type": "string" 20 | } 21 | }); 22 | 23 | var msg = provider.getNode("/message"); 24 | 25 | expect(msg.configs[r"$is"], equals("node")); 26 | expect(msg.configs[r"$name"], equals("A Message")); 27 | expect(msg.configs[r"$type"], equals("string")); 28 | expect(msg.getConfig(r"$name"), equals("A Message")); 29 | expect(msg.getConfig(r"$type"), equals("string")); 30 | expect(msg.get(r"$name"), equals("A Message")); 31 | expect(msg.get(r"$type"), equals("string")); 32 | }); 33 | 34 | test("retains attributes", () { 35 | var provider = createSimpleNodeProvider(nodes: { 36 | "message": { 37 | "@name": "A Message", 38 | "@type": "string" 39 | } 40 | }); 41 | 42 | var msg = provider.getNode("/message"); 43 | 44 | expect(msg.attributes["@name"], equals("A Message")); 45 | expect(msg.attributes["@type"], equals("string")); 46 | expect(msg.getAttribute("@name"), equals("A Message")); 47 | expect(msg.getAttribute("@type"), equals("string")); 48 | expect(msg.get("@name"), equals("A Message")); 49 | expect(msg.get("@type"), equals("string")); 50 | }); 51 | 52 | test("retains children", () { 53 | var provider = createSimpleNodeProvider(nodes: { 54 | "container": { 55 | "message": { 56 | r"$type": "string" 57 | } 58 | } 59 | }); 60 | 61 | var c = provider.getNode("/container"); 62 | 63 | expect(c.configs, hasLength(1)); 64 | expect(c.children, hasLength(1)); 65 | var msg = c.children.values.first; 66 | expect(msg, new isInstanceOf()); 67 | expect(msg.configs, hasLength(2)); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DSLink SDK for Dart [![Slack](https://dsa-slack.herokuapp.com/badge.svg)](https://dsa-slack.herokuapp.com/) 2 | 3 | DSLink SDK for Dart 4 | 5 | ## Getting Started 6 | 7 | ### Prerequisites 8 | 9 | - [Git](https://git-scm.com/downloads) 10 | - [Dart SDK](https://www.dartlang.org/downloads/) 11 | 12 | ### Install 13 | 14 | ```bash 15 | pub global activate -sgit https://github.com/IOT-DSA/broker-dart.git # Globally install the DSA Broker 16 | ``` 17 | 18 | ### Start a Broker 19 | 20 | ```bash 21 | dsbroker # If you have the pub global executable path setup. 22 | pub global run dsbroker:broker # If you do not have the pub global executable path setup. 23 | ``` 24 | 25 | You can edit the server configuration using `broker.json`. For more information about broker configuration, see [this page](https://github.com/IOT-DSA/sdk-dslink-dart/wiki/Configuring-a-Broker). 26 | 27 | ### Create a Link 28 | 29 | For documentation, see [this page](http://iot-dsa.github.io/docs/sdks/dart/). 30 | For more examples, see [this page](https://github.com/IOT-DSA/sdk-dslink-dart/tree/master/example). 31 | 32 | ```dart 33 | import "package:dslink/dslink.dart"; 34 | 35 | LinkProvider link; 36 | 37 | main(List args) async { 38 | // Process the arguments and initializes the default nodes. 39 | link = new LinkProvider(args, "Simple-", defaultNodes: { 40 | "Message": { 41 | r"$type": "string", // The type of the node is a string. 42 | r"$writable": "write", // This node's value can be set by a responder link. 43 | "?value": "Hello World" // The default message value. 44 | } 45 | }); 46 | 47 | // Connect to the broker. 48 | link.connect(); 49 | 50 | // Save the message when it changes. 51 | link.onValueChange("/Message").listen((_) => link.save()); 52 | } 53 | ``` 54 | 55 | ### Start a Link 56 | 57 | ```bash 58 | dart path/to/link.dart # Start a link that connects to a broker at http://127.0.0.1:8080/conn 59 | dart path/to/link.dart --broker http://my.broker:8080/conn # Start a link that connects to the specified broker. 60 | ``` 61 | 62 | ## Links 63 | 64 | - [DSA Site](http://iot-dsa.org/) 65 | - [DSA Wiki](https://github.com/IOT-DSA/docs/wiki) 66 | - [Documentation](http://iot-dsa.github.io/docs/sdks/dart/) 67 | -------------------------------------------------------------------------------- /example/browser/camera.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/browser.dart"; 2 | 3 | import "dart:html"; 4 | import "dart:typed_data"; 5 | 6 | //FIXME:Dart1.0 7 | import "dart:convert"; 8 | 9 | //FIXME:Dart2.0 10 | //import "package:dslink/convert_consts.dart"; 11 | 12 | LinkProvider link; 13 | MediaStream stream; 14 | VideoElement video; 15 | CanvasElement canvas; 16 | String url; 17 | int width = 320; 18 | int height = 0; 19 | bool streaming = false; 20 | 21 | main() async { 22 | video = querySelector("#video"); 23 | canvas = querySelector("#canvas"); 24 | stream = await window.navigator.getUserMedia(video: true); 25 | 26 | url = Url.createObjectUrlFromStream(stream); 27 | 28 | video.src = url; 29 | video.play(); 30 | 31 | video.onCanPlay.listen((e) { 32 | if (!streaming) { 33 | height = video.videoHeight ~/ (video.videoWidth / width); 34 | 35 | if (height.isNaN) { 36 | height = width ~/ (4 / 3); 37 | } 38 | 39 | video.width = width; 40 | video.height = height; 41 | canvas.width = width; 42 | canvas.height = height; 43 | streaming = true; 44 | } 45 | }); 46 | 47 | var brokerUrl = await BrowserUtils.fetchBrokerUrlFromPath("broker_url", "http://localhost:8080/conn"); 48 | 49 | link = new LinkProvider(brokerUrl, "Webcam-", defaultNodes: { 50 | "Image": { 51 | r"$type": "binary" 52 | } 53 | }); 54 | 55 | await link.connect(); 56 | 57 | Scheduler.every(new Interval.forMilliseconds(16 * 4), () { 58 | takePicture(); 59 | }); 60 | } 61 | 62 | void takePicture() { 63 | CanvasRenderingContext2D context = canvas.getContext("2d"); 64 | if (width != 0 && height != 0) { 65 | canvas.width = width; 66 | canvas.height = height; 67 | context.drawImage(video, 0, 0); 68 | 69 | updateImage(); 70 | } else { 71 | clearPicture(); 72 | } 73 | } 74 | 75 | void clearPicture() { 76 | CanvasRenderingContext2D context = canvas.getContext("2d"); 77 | context.fillStyle = "#AAA"; 78 | context.fillRect(0, 0, canvas.width, canvas.height); 79 | updateImage(); 80 | } 81 | 82 | void updateImage() { 83 | link.val("/Image", captureImage()); 84 | } 85 | 86 | ByteData captureImage() { 87 | var stopwatch = new Stopwatch(); 88 | stopwatch.start(); 89 | var dataUrl = canvas.toDataUrl("image/webp", 0.2); 90 | stopwatch.stop(); 91 | var bytes = BASE64.decode(dataUrl.substring("data:image/webp;base64,".length)); 92 | var data = ByteDataUtil.fromList(bytes); 93 | print("Took ${stopwatch.elapsedMilliseconds} to create image."); 94 | return data; 95 | } 96 | -------------------------------------------------------------------------------- /tool/experiment/test_client_responder_locker.dart: -------------------------------------------------------------------------------- 1 | import 'package:dslink/client.dart'; 2 | import 'package:dslink/src/crypto/pk.dart'; 3 | import 'package:dslink/responder.dart'; 4 | import 'dart:async'; 5 | import 'dart:math'; 6 | 7 | SimpleNodeProvider nodeProvider; 8 | 9 | class OpenLockerAction extends SimpleNode { 10 | OpenLockerAction(String path) : super(path); 11 | 12 | Object onInvoke(Map params) { 13 | nodeProvider.updateValue('${path}ed', true); 14 | return {"value":"a"}; 15 | } 16 | } 17 | 18 | class ChangeLocker extends SimpleNode { 19 | ChangeLocker(String path) : super(path); 20 | 21 | Object onInvoke(Map params) { 22 | if (params['value'] is bool) { 23 | nodeProvider.updateValue('${path}ed', params['value']); 24 | } 25 | return {"value":"a"}; 26 | } 27 | } 28 | 29 | void main() { 30 | PrivateKey key = new PrivateKey.loadFromString( 31 | 't5YRKgaZyhXNNberpciIoYzz3S1isttspwc4QQhiaVk BJ403K-ND1Eau8UJA7stsYI2hdgiOKhNVDItwg7sS6MfG2iSRGqM2UodSF0mb8GbD8s2OAukQ03DFLULw72bklo'); 32 | 33 | 34 | var profiles = { 35 | 'openLocker':(String path) { 36 | return new OpenLockerAction(path); 37 | }, 38 | 'changeLocker':(String path) { 39 | return new ChangeLocker(path); 40 | }, 41 | }; 42 | 43 | nodeProvider = new SimpleNodeProvider({ 44 | 'locker1': { 45 | r'$is':'locker', 46 | 'open': { // an action to open the door 47 | r'$invokable': 'read', 48 | r'$is': 'openLocker' 49 | }, 50 | 'opened': { // the open status value 51 | r'$type': 'bool', 52 | '?value': false 53 | } 54 | }, 55 | 'locker2': { 56 | r'$is':'locker', 57 | 'open': { // an action to open the door 58 | r'$invokable': 'read', 59 | r'$params':[{"name":"value", "type":"bool", "default":true}], 60 | r'$is': 'changeLocker' 61 | }, 62 | 'opened': { // the open status value 63 | r'$type': 'bool', 64 | '?value': false 65 | }, 66 | 'test': { // the open status value 67 | r'$type': 'map', 68 | '?value': {"a":"hello", "b":[1, 2, 3], "c":{"d":"hi", "e":3}} 69 | } 70 | } 71 | }, profiles); 72 | 73 | Random rng = new Random(); 74 | new HttpClientLink( 75 | 'http://localhost:8080/conn', 'locker-', key, isResponder: true, 76 | nodeProvider: nodeProvider) 77 | ..connect(); 78 | new Timer.periodic(new Duration(seconds: 2), (v) { 79 | nodeProvider.updateValue('/locker2/test', 80 | {"a":"hello", "b":[1, 2, 3], "c":{"d":"hi", "e":rng.nextInt(500)}}); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /tool/toDart2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sed -i '' -e "/args/s/^#*/#/" ./../pubspec.yaml 3 | sed -i '' -e "/bignum/s/^#*/#/" ./../pubspec.yaml 4 | sed -i '' -e "/dscipher/s/^#*/#/" ./../pubspec.yaml 5 | sed -i '' -e "/dscipher.git/s/^#*/#/" ./../pubspec.yaml 6 | 7 | sed -i '' -e "/^#.*pointycastle/ s/^#*//" ./../pubspec.yaml 8 | 9 | sed -i '' -e "/msgpack: \"^0.9.0\"/s/^#*/#/" ./../pubspec.yaml 10 | sed -i '' -e "/msgpack:*$/s/^#*//" ./../pubspec.yaml 11 | 12 | sed -i '' -e "/tbelousova\/msgpack.dart/ s/^#*//" ./../pubspec.yaml 13 | 14 | sed -i '' -e "/json_diff: '^0.1.2'/s/^#*/#/" ./../pubspec.yaml 15 | 16 | sed -i '' -e "/json_diff:*$/s/^#*//" ./../pubspec.yaml 17 | 18 | sed -i '' -e "/build_runner/ s/^#*//" ./../pubspec.yaml 19 | sed -i '' -e "/build_test/ s/^#*//" ./../pubspec.yaml 20 | sed -i '' -e "/build_web_compilers/ s/^#*//" ./../pubspec.yaml 21 | 22 | sed -i '' -e "/dsbroker:/s/^#*/#/" ./../pubspec.yaml 23 | 24 | sed -i '' -e "/sdk: '>=2.0.0'/ s/^#*//" ./../pubspec.yaml 25 | sed -i '' -e "/sdk: '>=1.13.0/s/^#*/#/" ./../pubspec.yaml 26 | 27 | sed -i '' -e "/test: any/ s/^#*//" ./../pubspec.yaml 28 | sed -i '' -e "/test: '0.12.15+8'/s/^#*/#/" ./../pubspec.yaml 29 | 30 | 31 | sed -i '' -e '/\/\/Dart1-close-block/s,^\/\/*,,' ./../lib/src/crypto/dart/pk.dart 32 | sed -i '' -e '/Dart1-open-block/s,^\/\/,\/,' ./../lib/src/crypto/dart/pk.dart 33 | 34 | sed -i '' -e '/^Dart2-close-block/s,^,\/\/,' ./../lib/src/crypto/dart/pk.dart 35 | sed -i '' -e '/Dart2-open-block/s,^\/*,\/\/,' ./../lib/src/crypto/dart/pk.dart 36 | 37 | sed -i '' -e '/\/\/Dart1-close-block/s,^\/\/*,,' ./../lib/src/crypto/dart/isolate.dart 38 | sed -i '' -e '/Dart1-open-block/s,^\/\/,\/,' ./../lib/src/crypto/dart/isolate.dart 39 | 40 | sed -i '' -e '/\/\/Dart1-close-block/s,^\/\/*,,' ./../lib/responder.dart 41 | sed -i '' -e '/Dart1-open-block/s,^\/\/,\/,' ./../lib/responder.dart 42 | 43 | sed -i '' -e '/^Dart2-close-block/s,^,\/\/,' ./../lib/responder.dart 44 | sed -i '' -e '/Dart2-open-block/s,^\/*,\/\/,' ./../lib/responder.dart 45 | 46 | sed -i '' -e '/asFuture/s,\/\/,,' ./../lib/src/requester/request/subscribe.dart 47 | sed -i '' -e '/^[^\/].*asFuture\/\*/s,^,\/\/,' ./../lib/src/requester/request/subscribe.dart 48 | 49 | sed -i '' -e '/convert_consts/s,^\/\/*,,' ./../lib/nodes.dart 50 | sed -i '' -e '/convert_consts/s,^\/\/*,,' ./../example/browser/camera.dart 51 | sed -i '' -e '/convert_consts/s,^\/\/*,,' ./../lib/io.dart 52 | 53 | sed -i '' -e '/^[^\/].*dart:convert/s,^,\/\/,' ./../example/browser/camera.dart 54 | sed -i '' -e '/^[^\/].*dart:convert/s,^,\/\/,' ./../lib/broker_discovery.dart 55 | 56 | 57 | cp ./../lib/convert_consts.dart2 ./../lib/convert_consts.dart -------------------------------------------------------------------------------- /lib/src/historian/manage.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | class CreateWatchGroupNode extends SimpleNode { 4 | CreateWatchGroupNode(String path) : super(path, _link.provider); 5 | 6 | @override 7 | onInvoke(Map params) async { 8 | String name = params["Name"]; 9 | String realName = NodeNamer.createName(name); 10 | 11 | var p = new Path(path); 12 | 13 | _link.addNode("${p.parentPath}/${realName}", { 14 | r"$is": "watchGroup", 15 | r"$name": name 16 | }); 17 | _link.save(); 18 | } 19 | } 20 | 21 | class AddDatabaseNode extends SimpleNode { 22 | AddDatabaseNode(String path) : super(path, _link.provider); 23 | 24 | @override 25 | onInvoke(Map params) async { 26 | String name = params["Name"]; 27 | String realName = NodeNamer.createName(name); 28 | 29 | _link.addNode("/${realName}", { 30 | r"$is": "database", 31 | r"$name": name, 32 | r"$$db_config": params 33 | }); 34 | _link.save(); 35 | } 36 | } 37 | 38 | class AddWatchPathNode extends SimpleNode { 39 | AddWatchPathNode(String path) : super(path); 40 | 41 | @override 42 | onInvoke(Map params) async { 43 | String wp = params["Path"]; 44 | String rp = NodeNamer.createName(wp); 45 | var p = new Path(path); 46 | var targetPath = "${p.parentPath}/${rp}"; 47 | var node = await _link.requester.getRemoteNode(wp); 48 | _link.addNode(targetPath, { 49 | r"$name": wp, 50 | r"$path": wp, 51 | r"$is": "watchPath", 52 | r"$type": node.configs[r"$type"] 53 | }); 54 | 55 | _link.save(); 56 | } 57 | } 58 | 59 | class PurgePathNode extends SimpleNode { 60 | PurgePathNode(String path) : super(path); 61 | 62 | @override 63 | onInvoke(Map params) async { 64 | TimeRange tr = parseTimeRange(params["timeRange"]); 65 | if (tr == null) { 66 | return; 67 | } 68 | 69 | WatchPathNode watchPathNode = _link[new Path(path).parentPath]; 70 | await watchPathNode.group.db.database.purgePath( 71 | watchPathNode.group._watchName, 72 | watchPathNode.valuePath, 73 | tr 74 | ); 75 | } 76 | } 77 | 78 | class PurgeGroupNode extends SimpleNode { 79 | PurgeGroupNode(String path) : super(path); 80 | 81 | @override 82 | onInvoke(Map params) async { 83 | TimeRange tr = parseTimeRange(params["timeRange"]); 84 | if (tr == null) { 85 | return; 86 | } 87 | 88 | WatchGroupNode watchGroupNode = _link[new Path(path).parentPath]; 89 | await watchGroupNode.db.database.purgeGroup( 90 | watchGroupNode._watchName, 91 | tr 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /example/browser/video.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/browser.dart"; 2 | 3 | import "dart:html"; 4 | import "dart:typed_data"; 5 | import "dart:js"; 6 | 7 | LinkProvider link; 8 | Requester requester; 9 | VideoElement video; 10 | JsObject videoObject; 11 | 12 | String codec = 'video/webm; codecs="vorbis, vp8"'; 13 | 14 | main() async { 15 | //updateLogLevel("ALL"); 16 | video = querySelector("#video"); 17 | videoObject = new JsObject.fromBrowserObject(video); 18 | 19 | var brokerUrl = await BrowserUtils.fetchBrokerUrlFromPath("broker_url", "http://localhost:8080/conn"); 20 | 21 | link = new LinkProvider(brokerUrl, "VideoDisplay-", isRequester: true, isResponder: false); 22 | 23 | await link.connect(); 24 | requester = await link.onRequesterReady; 25 | 26 | String getHash() { 27 | if (window.location.hash.isEmpty) { 28 | return ""; 29 | } 30 | var h = window.location.hash.substring(1); 31 | if (h.startsWith("mpeg4:")) { 32 | codec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; 33 | h = h.substring("mpeg4:".length); 34 | } 35 | return h; 36 | } 37 | 38 | window.onHashChange.listen((event) { 39 | setup(getHash()); 40 | }); 41 | 42 | await setup(getHash().isNotEmpty ? getHash() : "/downstream/File/video"); 43 | } 44 | 45 | setup(String path) async { 46 | print("Displaying Video from ${path}"); 47 | 48 | var sizePath = path + "/size"; 49 | var getChunkPath = path + "/readBinaryChunk"; 50 | 51 | int size = (await requester.getNodeValue(sizePath)).value; 52 | 53 | print("Video Size: ${size} bytes"); 54 | 55 | var source = new MediaSource(); 56 | 57 | source.addEventListener("sourceopen", (e) async { 58 | CHUNK_COUNT = (size / 512000).round(); 59 | var chunkSize = (size / CHUNK_COUNT).ceil(); 60 | 61 | print("Chunk Size: ${chunkSize} bytes"); 62 | 63 | var buff = source.addSourceBuffer(codec); 64 | for (var i = 0; i < CHUNK_COUNT; ++i) { 65 | var start = chunkSize * i; 66 | var end = start + chunkSize; 67 | RequesterInvokeUpdate update = await requester.invoke(getChunkPath, { 68 | "start": start, 69 | "end": start + chunkSize 70 | }).first; 71 | 72 | Map map = update.updates[0]; 73 | ByteData data = map["data"]; 74 | 75 | print("Chunk #${i}"); 76 | 77 | print("${start}-${end}"); 78 | 79 | if (i + 1 == CHUNK_COUNT) { 80 | source.endOfStream(); 81 | } else { 82 | buff.appendBuffer(data.buffer); 83 | } 84 | 85 | await buff.on["updateend"].first; 86 | } 87 | 88 | source.endOfStream(); 89 | }); 90 | 91 | video.src = Url.createObjectUrlFromSource(source); 92 | video.autoplay = true; 93 | video.play(); 94 | } 95 | 96 | int CHUNK_COUNT = 200; 97 | -------------------------------------------------------------------------------- /tool/toDart1.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sed -i '' -e "/args/s/^#//" ./../pubspec.yaml 4 | sed -i '' -e "/bignum/s/^#//" ./../pubspec.yaml 5 | sed -i '' -e "/dscipher/s/^#//" ./../pubspec.yaml 6 | sed -i '' -e "/dscipher.git/s/^#//" ./../pubspec.yaml 7 | 8 | sed -i '' -e "/pointycastle/ s/^#*/#/" ./../pubspec.yaml 9 | 10 | sed -i '' -e "/^#.*msgpack: \"^0.9.0\"/s/^#//" ./../pubspec.yaml 11 | 12 | sed -i '' -e "/msgpack:*$/ s/^#*/#/" ./../pubspec.yaml 13 | sed -i '' -e "/tbelousova\/msgpack.dart/ s/^#*/#/" ./../pubspec.yaml 14 | 15 | sed -i '' -e "/^#.*json_diff: '^0.1.2'/s/^#//" ./../pubspec.yaml 16 | 17 | sed -i '' -e "/^json_diff:*$/s/^#//" ./../pubspec.yaml 18 | sed -i '' -e "/json_diff:*$/ s/^#*/#/" ./../pubspec.yaml 19 | sed -i '' -e "/tbelousova\/dart-json_diff/ s/^#*/#/" ./../pubspec.yaml 20 | 21 | sed -i '' -e "/build_runner/ s/^#*/#/" ./../pubspec.yaml 22 | sed -i '' -e "/build_test/ s/^#*/#/" ./../pubspec.yaml 23 | sed -i '' -e "/build_web_compilers/ s/^#*/#/" ./../pubspec.yaml 24 | 25 | sed -i '' -e "/dsbroker:/s/^#//" ./../pubspec.yaml 26 | 27 | sed -i '' -e "/sdk: '>=2.0.0'/ s/^#*/#/" ./../pubspec.yaml 28 | sed -i '' -e "/sdk: '>=1.13.0/s/^#//" ./../pubspec.yaml 29 | 30 | 31 | sed -i '' -e "/test: any/ s/^#*/#/" ./../pubspec.yaml 32 | sed -i '' -e "/test: '0.12.15+8'/s/^#//" ./../pubspec.yaml 33 | 34 | sed -i '' -e '/\/\/Dart2-close-block/s,^\/\/*,,' ./../lib/src/crypto/dart/pk.dart 35 | sed -i '' -e '/Dart2-open-block/s,^\/\/,\/,' ./../lib/src/crypto/dart/pk.dart 36 | 37 | sed -i '' -e '/^Dart1-close-block/s,^,\/\/,' ./../lib/src/crypto/dart/pk.dart 38 | sed -i '' -e '/Dart1-open-block/s,^\/*,\/\/,' ./../lib/src/crypto/dart/pk.dart 39 | 40 | sed -i '' -e '/^Dart1-close-block/s,^,\/\/,' ./../lib/src/crypto/dart/isolate.dart 41 | sed -i '' -e '/Dart1-open-block/s,^\/*,\/\/,' ./../lib/src/crypto/dart/isolate.dart 42 | 43 | 44 | sed -i '' -e '/\/\/Dart2-close-block/s,^\/\/*,,' ./../lib/responder.dart 45 | sed -i '' -e '/Dart2-open-block/s,^\/\/,\/,' ./../lib/responder.dart 46 | 47 | sed -i '' -e '/^Dart1-close-block/s,^,\/\/,' ./../lib/responder.dart 48 | sed -i '' -e '/Dart1-open-block/s,^\/*,\/\/,' ./../lib/responder.dart 49 | 50 | sed -i '' -e '/^[^\/].*asFuture/s,^,\/\/,' ./../lib/src/requester/request/subscribe.dart 51 | sed -i '' -e '/asFuture\/\*/s,\/\/,,' ./../lib/src/requester/request/subscribe.dart 52 | 53 | sed -i '' -e '/convert_consts/s,^\/*,\/\/,' ./../lib/nodes.dart 54 | sed -i '' -e '/convert_consts/s,^\/*,\/\/,' ./../example/browser/camera.dart 55 | sed -i '' -e '/convert_consts/s,^\/*,\/\/,' ./../lib/io.dart 56 | 57 | sed -i '' -e '/dart:convert/s,^\/\/*,,' ./../example/browser/camera.dart 58 | sed -i '' -e '/dart:convert/s,^\/\/*,,' ./../lib/broker_discovery.dart 59 | 60 | 61 | cp ./../lib/convert_consts.dart1 ./../lib/convert_consts.dart -------------------------------------------------------------------------------- /lib/src/requester/default_defs.dart: -------------------------------------------------------------------------------- 1 | part of dslink.requester; 2 | 3 | // TODO: merge with defaultProfileMap in common lib 4 | class DefaultDefNodes { 5 | static final Map _defaultDefs = { 6 | "node": {}, 7 | "static": {}, 8 | "getHistory": { 9 | r"$invokable": "read", 10 | r"$result": "table", 11 | r"$params": [ 12 | { 13 | "name": "Timerange", 14 | "type": "string", 15 | "editor": "daterange" 16 | }, 17 | { 18 | "name": "Interval", 19 | "type": "enum", 20 | "default": "none", 21 | "editor": buildEnumType([ 22 | "default", 23 | "none", 24 | "1Y", 25 | "3N", 26 | "1N", 27 | "1W", 28 | "1D", 29 | "12H", 30 | "6H", 31 | "4H", 32 | "3H", 33 | "2H", 34 | "1H", 35 | "30M", 36 | "15M", 37 | "10M", 38 | "5M", 39 | "1M", 40 | "30S", 41 | "15S", 42 | "10S", 43 | "5S", 44 | "1S" 45 | ]) 46 | }, 47 | { 48 | "name": "Rollup", 49 | "default": "none", 50 | "type": buildEnumType([ 51 | "none", 52 | "avg", 53 | "min", 54 | "max", 55 | "sum", 56 | "first", 57 | "last", 58 | "count", 59 | "delta" 60 | ]) 61 | } 62 | ], 63 | r"$columns": [ 64 | { 65 | "name": "timestamp", 66 | "type": "time" 67 | }, 68 | { 69 | "name": "value", 70 | "type": "dynamic" 71 | } 72 | ] 73 | } 74 | }; 75 | 76 | static final Map nameMap = () { 77 | var rslt = new Map(); 78 | _defaultDefs.forEach((/*String*/ k, /*Map*/ m) { 79 | String path = '/defs/profile/$k'; 80 | RemoteDefNode node = new RemoteDefNode(path); 81 | (m as Map).forEach((/*String*/ n, Object v) { 82 | if (n.startsWith(r'$')) { 83 | node.configs[n] = v; 84 | } else if (n.startsWith('@')) { 85 | node.attributes[n] = v; 86 | } 87 | }); 88 | node.listed = true; 89 | rslt[k] = node; 90 | }); 91 | return rslt; 92 | }(); 93 | 94 | static final Map pathMap = () { 95 | var rslt = new Map(); 96 | nameMap.forEach((k, node) { 97 | if (node is RemoteNode) { 98 | rslt[node.remotePath] = node; 99 | } 100 | }); 101 | return rslt; 102 | }(); 103 | } 104 | -------------------------------------------------------------------------------- /test/large_links_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("vm") 2 | @Timeout(const Duration(seconds: 60)) 3 | library dslink.test.vm.links.large; 4 | 5 | import "dart:async"; 6 | 7 | import "package:dsbroker/broker.dart"; 8 | import "package:dslink/dslink.dart"; 9 | import "package:dslink/io.dart"; 10 | import "package:test/test.dart"; 11 | 12 | import "common.dart"; 13 | 14 | main() { 15 | group("Large Links", largeLinksTest); 16 | } 17 | 18 | largeLinksTest() { 19 | DsHttpServer server; 20 | int port; 21 | setUp(() async { 22 | updateLogLevel("WARNING"); 23 | port = await getRandomSocketPort(); 24 | server = await startBrokerServer(port, persist: false); 25 | }); 26 | 27 | List _links = []; 28 | 29 | tearDown(() async { 30 | for (var link in _links) { 31 | link.close(); 32 | } 33 | 34 | await server.stop(); 35 | }); 36 | 37 | Future createLink( 38 | String name, { 39 | List args, 40 | bool isRequester: false, 41 | bool isResponder: true, 42 | Map nodes, 43 | Map profiles 44 | }) async { 45 | var margs = [ 46 | "--broker=http://127.0.0.1:${port}/conn" 47 | ]; 48 | 49 | if (args != null) { 50 | margs.addAll(args); 51 | } 52 | 53 | var link = new LinkProvider( 54 | margs, 55 | name, 56 | isRequester: isRequester, 57 | isResponder: isResponder, 58 | defaultNodes: nodes, 59 | profiles: profiles, 60 | autoInitialize: false, 61 | loadNodesJson: false, 62 | savePrivateKey: false 63 | ); 64 | 65 | _links.add(link); 66 | 67 | link.init(); 68 | link.connect(); 69 | return link; 70 | } 71 | 72 | test("are able to send large numbers of value updates", () async { 73 | LinkProvider host = await createLink("DataHost", nodes: { 74 | "number": { 75 | r"$type": "number", 76 | "?value": 1 77 | } 78 | }); 79 | 80 | var client = await createLink( 81 | "DataClient", 82 | isRequester: true, 83 | isResponder: false 84 | ); 85 | var requester = await client.onRequesterReady; 86 | await gap(); 87 | 88 | var sent = []; 89 | var received = []; 90 | var sub = requester.subscribe("/downstream/DataHost/number", (ValueUpdate update) { 91 | received.add(update.value); 92 | }, 1); 93 | 94 | await gap(); 95 | 96 | for (var i = 1; i <= 5000; i++) { 97 | host.val("/number", i); 98 | sent.add(i); 99 | await new Future.delayed(const Duration(milliseconds: 1)); 100 | } 101 | 102 | await gap(); 103 | 104 | sub.cancel(); 105 | 106 | expect(received, equals(sent)); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /lib/src/common/permission.dart: -------------------------------------------------------------------------------- 1 | part of dslink.common; 2 | 3 | class Permission { 4 | /// now allowed to do anything 5 | static const int NONE = 0; 6 | 7 | /// list node 8 | static const int LIST = 1; 9 | 10 | /// read node 11 | static const int READ = 2; 12 | 13 | /// write attribute and value 14 | static const int WRITE = 3; 15 | 16 | /// config the node 17 | static const int CONFIG = 4; 18 | 19 | /// something that can never happen 20 | static const int NEVER = 5; 21 | 22 | static const List names = const [ 23 | 'none', 24 | 'list', 25 | 'read', 26 | 'write', 27 | 'config', 28 | 'never' 29 | ]; 30 | 31 | static const Map nameParser = const { 32 | 'none': NONE, 33 | 'list': LIST, 34 | 'read': READ, 35 | 'write': WRITE, 36 | 'config': CONFIG, 37 | 'never': NEVER 38 | }; 39 | 40 | static int parse(Object obj, [int defaultVal = NEVER]) { 41 | if (obj is String && nameParser.containsKey(obj)) { 42 | return nameParser[obj]; 43 | } 44 | return defaultVal; 45 | } 46 | } 47 | 48 | class PermissionList { 49 | Map idMatchs = {}; 50 | Map groupMatchs = {}; 51 | int defaultPermission = Permission.NONE; 52 | 53 | void updatePermissions(List data) { 54 | idMatchs.clear(); 55 | groupMatchs.clear(); 56 | defaultPermission = Permission.NONE; 57 | for (Object obj in data) { 58 | if (obj is Map) { 59 | if (obj['id'] is String) { 60 | idMatchs[obj['id']] = Permission.nameParser[obj['permission']]; 61 | } else if (obj['group'] is String) { 62 | if (obj['group'] == 'default') { 63 | defaultPermission = Permission.nameParser[obj['permission']]; 64 | } else { 65 | groupMatchs[obj['group']] = 66 | Permission.nameParser[obj['permission']]; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | bool _FORCE_CONFIG = true; 74 | 75 | int getPermission(Responder responder) { 76 | // TODO Permission temp workaround before user permission is implemented 77 | if (_FORCE_CONFIG) { 78 | return Permission.CONFIG; 79 | } 80 | if (idMatchs.containsKey(responder.reqId)) { 81 | return idMatchs[responder.reqId]; 82 | } 83 | 84 | int rslt = Permission.NEVER; 85 | for (String group in responder.groups) { 86 | if (groupMatchs.containsKey(group)) { 87 | int v = groupMatchs[group]; 88 | if (v < rslt) { 89 | // choose the lowest permission from all matched group 90 | rslt = v; 91 | } 92 | } 93 | } 94 | 95 | if (rslt == Permission.NEVER) { 96 | return defaultPermission; 97 | } 98 | return rslt; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/convert_consts.dart: -------------------------------------------------------------------------------- 1 | library util.consts; 2 | 3 | import 'dart:math' as Math; 4 | import "dart:io"; 5 | 6 | import "package:bignum/bignum.dart"; 7 | const double_NAN = double.NAN; 8 | const double_NEGATIVE_INFINITY = double.NEGATIVE_INFINITY; 9 | const double_INFINITY = double.INFINITY; 10 | const Math_PI = Math.PI; 11 | const double_MAX_FINITE = double.MAX_FINITE; 12 | const Math_E = Math.E; 13 | const Math_LN2 = Math.LN2; 14 | const Math_LN10 = Math.LN10; 15 | const Math_LOG2E = Math.LOG2E; 16 | const Math_LOG10E = Math.LOG10E; 17 | const Math_SQRT2 = Math.SQRT2; 18 | const Math_SQRT1_2 = Math.SQRT1_2; 19 | const Duration_ZERO = Duration.ZERO; 20 | 21 | void socketJoinMulticast(RawDatagramSocket socket, InternetAddress group, [NetworkInterface interface]) { 22 | socket.joinMulticast(group, interface: interface); 23 | } 24 | 25 | List bigIntegerToByteArray(data) { 26 | return (data as BigInteger).toByteArray(); 27 | } 28 | 29 | String bigIntegerToRadix(value, int radix) { 30 | return (value as BigInteger).toRadix(radix); 31 | } 32 | 33 | dynamic newBigInteger([a, b, c]) { 34 | return new BigInteger(a,b,c); 35 | } 36 | 37 | dynamic newBigIntegerFromBytes(int signum, List magnitude) { 38 | return new BigInteger.fromBytes(signum, magnitude); 39 | } 40 | 41 | /// A type representing values that are either `Future` or `T`. 42 | /// 43 | /// This class declaration is a public stand-in for an internal 44 | /// future-or-value generic type. References to this class are resolved to the 45 | /// internal type. 46 | /// 47 | /// It is a compile-time error for any class to extend, mix in or implement 48 | /// `FutureOr`. 49 | /// 50 | /// Note: the `FutureOr` type is interpreted as `dynamic` in non strong-mode. 51 | /// 52 | /// # Examples 53 | /// ``` dart 54 | /// // The `Future.then` function takes a callback [f] that returns either 55 | /// // an `S` or a `Future`. 56 | /// Future then(FutureOr f(T x), ...); 57 | /// 58 | /// // `Completer.complete` takes either a `T` or `Future`. 59 | /// void complete(FutureOr value); 60 | /// ``` 61 | /// 62 | /// # Advanced 63 | /// The `FutureOr` type is actually the "type union" of the types `int` and 64 | /// `Future`. This type union is defined in such a way that 65 | /// `FutureOr` is both a super- and sub-type of `Object` (sub-type 66 | /// because `Object` is one of the types of the union, super-type because 67 | /// `Object` is a super-type of both of the types of the union). Together it 68 | /// means that `FutureOr` is equivalent to `Object`. 69 | /// 70 | /// As a corollary, `FutureOr` is equivalent to 71 | /// `FutureOr>`, `FutureOr>` is equivalent to 72 | /// `Future`. 73 | abstract class FutureOr { 74 | // Private generative constructor, so that it is not subclassable, mixable, or 75 | // instantiable. 76 | FutureOr._() { 77 | throw new UnsupportedError("FutureOr can't be instantiated"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/convert_consts.dart1: -------------------------------------------------------------------------------- 1 | library util.consts; 2 | 3 | import 'dart:math' as Math; 4 | import "dart:io"; 5 | 6 | import "package:bignum/bignum.dart"; 7 | const double_NAN = double.NAN; 8 | const double_NEGATIVE_INFINITY = double.NEGATIVE_INFINITY; 9 | const double_INFINITY = double.INFINITY; 10 | const Math_PI = Math.PI; 11 | const double_MAX_FINITE = double.MAX_FINITE; 12 | const Math_E = Math.E; 13 | const Math_LN2 = Math.LN2; 14 | const Math_LN10 = Math.LN10; 15 | const Math_LOG2E = Math.LOG2E; 16 | const Math_LOG10E = Math.LOG10E; 17 | const Math_SQRT2 = Math.SQRT2; 18 | const Math_SQRT1_2 = Math.SQRT1_2; 19 | const Duration_ZERO = Duration.ZERO; 20 | 21 | void socketJoinMulticast(RawDatagramSocket socket, InternetAddress group, [NetworkInterface interface]) { 22 | socket.joinMulticast(group, interface: interface); 23 | } 24 | 25 | List bigIntegerToByteArray(data) { 26 | return (data as BigInteger).toByteArray(); 27 | } 28 | 29 | String bigIntegerToRadix(value, int radix) { 30 | return (value as BigInteger).toRadix(radix); 31 | } 32 | 33 | dynamic newBigInteger([a, b, c]) { 34 | return new BigInteger(a,b,c); 35 | } 36 | 37 | dynamic newBigIntegerFromBytes(int signum, List magnitude) { 38 | return new BigInteger.fromBytes(signum, magnitude); 39 | } 40 | 41 | /// A type representing values that are either `Future` or `T`. 42 | /// 43 | /// This class declaration is a public stand-in for an internal 44 | /// future-or-value generic type. References to this class are resolved to the 45 | /// internal type. 46 | /// 47 | /// It is a compile-time error for any class to extend, mix in or implement 48 | /// `FutureOr`. 49 | /// 50 | /// Note: the `FutureOr` type is interpreted as `dynamic` in non strong-mode. 51 | /// 52 | /// # Examples 53 | /// ``` dart 54 | /// // The `Future.then` function takes a callback [f] that returns either 55 | /// // an `S` or a `Future`. 56 | /// Future then(FutureOr f(T x), ...); 57 | /// 58 | /// // `Completer.complete` takes either a `T` or `Future`. 59 | /// void complete(FutureOr value); 60 | /// ``` 61 | /// 62 | /// # Advanced 63 | /// The `FutureOr` type is actually the "type union" of the types `int` and 64 | /// `Future`. This type union is defined in such a way that 65 | /// `FutureOr` is both a super- and sub-type of `Object` (sub-type 66 | /// because `Object` is one of the types of the union, super-type because 67 | /// `Object` is a super-type of both of the types of the union). Together it 68 | /// means that `FutureOr` is equivalent to `Object`. 69 | /// 70 | /// As a corollary, `FutureOr` is equivalent to 71 | /// `FutureOr>`, `FutureOr>` is equivalent to 72 | /// `Future`. 73 | abstract class FutureOr { 74 | // Private generative constructor, so that it is not subclassable, mixable, or 75 | // instantiable. 76 | FutureOr._() { 77 | throw new UnsupportedError("FutureOr can't be instantiated"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/responder/base_impl/config_setting.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | class ConfigSetting { 4 | final String name; 5 | final String type; 6 | 7 | /// need permission to read 8 | final Object defaultValue; 9 | 10 | /// whether broker need to maintain the change of config value when ds link is offline 11 | // bool maintain 12 | 13 | ConfigSetting(this.name, this.type, {this.defaultValue}); 14 | ConfigSetting.fromMap(this.name, Map m) 15 | : type = m.containsKey('type') ? m['type'] : 'string', 16 | defaultValue = m.containsKey('default') ? m['default'] : null {} 17 | 18 | DSError setConfig(Object value, LocalNodeImpl node, Responder responder) { 19 | if (node.configs[name] != value) { 20 | node.configs[name] = value; 21 | node.updateList(name); 22 | } 23 | return null; 24 | } 25 | 26 | DSError removeConfig(LocalNodeImpl node, Responder responder) { 27 | if (node.configs.containsKey(name)) { 28 | node.configs.remove(name); 29 | node.updateList(name); 30 | } 31 | return null; 32 | } 33 | } 34 | 35 | class Configs { 36 | static const Map _globalConfigs = const { 37 | r'$is': const {'type': 'profile'}, 38 | r'$interface': const {'type': 'interface'}, 39 | 40 | /// list of permissions 41 | r'$permissions': const { 42 | 'type': 'list', 43 | 'require': Permission.CONFIG, 44 | 'writable': Permission.CONFIG, 45 | }, 46 | 47 | /// the display name 48 | r'$name': const {'type': 'string'}, 49 | 50 | /// type of subscription stream 51 | r'$type': const {'type': 'type'}, 52 | 53 | /// permission needed to invoke 54 | r'$invokable': const {'type': 'permission', 'default': 'read'}, 55 | 56 | /// permission needed to set 57 | r'$writable': const {'type': 'permission', 'default': 'never'}, 58 | 59 | /// config settings, only used by profile nodes 60 | r'$settings': const {'type': 'map'}, 61 | 62 | /// params of invoke method 63 | r'$params': const {'type': 'list'}, 64 | 65 | /// stream columns of invoke method 66 | r'$columns': const {'type': 'list'}, 67 | 68 | /// stream meta of invoke method 69 | r'$streamMeta': const {'type': 'list'} 70 | // not serializable 71 | }; 72 | 73 | static final Configs global = new Configs()..load(_globalConfigs); 74 | static final ConfigSetting defaultConfig = 75 | new ConfigSetting.fromMap('', const {}); 76 | 77 | static ConfigSetting getConfig(String name, Node profile) { 78 | if (global.configs.containsKey(name)) { 79 | return global.configs[name]; 80 | } 81 | if (profile is DefinitionNode && profile.configs.containsKey(name)) { 82 | return profile.configs[name]; 83 | } 84 | return defaultConfig; 85 | } 86 | 87 | Map configs = {}; 88 | void load(Map inputs) { 89 | inputs.forEach((name, m) { 90 | if (m is Map) { 91 | configs[name] = new ConfigSetting.fromMap(name, m); 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/common/connection_handler.dart: -------------------------------------------------------------------------------- 1 | part of dslink.common; 2 | 3 | abstract class ConnectionProcessor { 4 | static const int ACK_WAIT_COUNT = 16; 5 | static int defaultCacheSize = 256; 6 | 7 | void startSendingData(int waitingAckId, int currentTime); 8 | void ackReceived(int receiveAckId, int startTime, int currentTime); 9 | } 10 | 11 | abstract class ConnectionHandler { 12 | ConnectionChannel _conn; 13 | StreamSubscription _connListener; 14 | ConnectionChannel get connection => _conn; 15 | 16 | set connection(ConnectionChannel conn) { 17 | if (_connListener != null) { 18 | _connListener.cancel(); 19 | _connListener = null; 20 | _onDisconnected(_conn); 21 | } 22 | _conn = conn; 23 | _connListener = _conn.onReceive.listen(onData); 24 | _conn.onDisconnected.then(_onDisconnected); 25 | // resend all requests after a connection 26 | if (_conn.connected) { 27 | onReconnected(); 28 | } else { 29 | _conn.onConnected.then((conn) => onReconnected()); 30 | } 31 | } 32 | 33 | void _onDisconnected(ConnectionChannel conn) { 34 | if (_conn == conn) { 35 | if (_connListener != null) { 36 | _connListener.cancel(); 37 | _connListener = null; 38 | } 39 | onDisconnected(); 40 | _conn = null; 41 | } 42 | } 43 | 44 | void onDisconnected(); 45 | void onReconnected() { 46 | if (_pendingSend) { 47 | _conn.sendWhenReady(this); 48 | } 49 | } 50 | 51 | void onData(List m); 52 | 53 | List _toSendList = []; 54 | 55 | void addToSendList(Map m) { 56 | _toSendList.add(m); 57 | if (!_pendingSend) { 58 | if (_conn != null) { 59 | _conn.sendWhenReady(this); 60 | } 61 | _pendingSend = true; 62 | } 63 | } 64 | 65 | List _processors = []; 66 | 67 | /// a processor function that's called just before the data is sent 68 | /// same processor won't be added to the list twice 69 | /// inside processor, send() data that only need to appear once per data frame 70 | void addProcessor(ConnectionProcessor processor) { 71 | _processors.add(processor); 72 | if (!_pendingSend) { 73 | if (_conn != null) { 74 | _conn.sendWhenReady(this); 75 | } 76 | _pendingSend = true; 77 | } 78 | } 79 | 80 | bool _pendingSend = false; 81 | 82 | /// gather all the changes from 83 | ProcessorResult getSendingData(int currentTime, int waitingAckId) { 84 | _pendingSend = false; 85 | List processors = _processors; 86 | _processors = []; 87 | for (ConnectionProcessor proc in processors) { 88 | proc.startSendingData(currentTime, waitingAckId); 89 | } 90 | List rslt = _toSendList; 91 | _toSendList = []; 92 | return new ProcessorResult(rslt, processors); 93 | } 94 | 95 | void clearProcessors() { 96 | _processors.length = 0; 97 | _pendingSend = false; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/broker_discovery.dart: -------------------------------------------------------------------------------- 1 | library dslink.broker_discovery; 2 | 3 | import "dart:async"; 4 | import "dart:convert"; 5 | import "dart:io"; 6 | import "convert_consts.dart"; 7 | 8 | class BrokerDiscoveryClient { 9 | RawDatagramSocket _socket; 10 | 11 | BrokerDiscoveryClient(); 12 | 13 | Future init([bool broadcast = false]) async { 14 | _socket = await RawDatagramSocket.bind("0.0.0.0", broadcast ? 1900 : 0); 15 | 16 | _socket.multicastHops = 10; 17 | _socket.broadcastEnabled = true; 18 | _socket.listen((RawSocketEvent event) { 19 | if (event == RawSocketEvent.READ) { 20 | var packet = _socket.receive(); 21 | _socket.writeEventsEnabled = true; 22 | 23 | if (packet == null) { 24 | return; 25 | } 26 | 27 | var data = UTF8.decode(packet.data); 28 | _onMessage(packet, data); 29 | } else if (event == RawSocketEvent.CLOSED) { 30 | if (!_brokerController.isClosed) { 31 | _brokerController.close(); 32 | } 33 | } 34 | }); 35 | 36 | _socket.writeEventsEnabled = true; 37 | 38 | var interfaces = await NetworkInterface.list(); 39 | try { 40 | for (var interface in interfaces) { 41 | try { 42 | socketJoinMulticast(_socket, new InternetAddress("239.255.255.230"), /*interface:*/interface); 43 | } catch (e) { 44 | socketJoinMulticast(_socket, new InternetAddress("239.255.255.230"), /*interface:*/ interface); 45 | } 46 | } 47 | } catch (e) { 48 | socketJoinMulticast(_socket, new InternetAddress("239.255.255.230")); 49 | } 50 | } 51 | 52 | Stream discover({Duration timeout: const Duration(seconds: 5)}) { 53 | _send("DISCOVER", "239.255.255.230", 1900); 54 | Stream stream = _brokerController.stream; 55 | new Future.delayed(timeout, () { 56 | close(); 57 | }); 58 | return stream; 59 | } 60 | 61 | void _send(String content, String address, int port) { 62 | _socket.send(UTF8.encode(content), new InternetAddress(address), port); 63 | } 64 | 65 | Stream get requests => _discoverController.stream; 66 | 67 | void _onMessage(Datagram packet, String msg) { 68 | var parts = msg.split(" "); 69 | var type = parts[0]; 70 | var argument = parts.skip(1).join(" "); 71 | 72 | if (type == "BROKER") { 73 | _brokerController.add(argument); 74 | } else if (type == "DISCOVER") { 75 | _discoverController.add(new BrokerDiscoverRequest(this, packet)); 76 | } 77 | } 78 | 79 | StreamController _discoverController = 80 | new StreamController.broadcast(); 81 | StreamController _brokerController = new StreamController.broadcast(); 82 | 83 | void close() { 84 | _socket.close(); 85 | } 86 | } 87 | 88 | class BrokerDiscoverRequest { 89 | final BrokerDiscoveryClient client; 90 | final Datagram packet; 91 | 92 | BrokerDiscoverRequest(this.client, this.packet); 93 | 94 | void reply(String url) { 95 | client._send("BROKER ${url}", packet.address.address, packet.port); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/responder/manager/storage.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | /// general purpose storage class 4 | abstract class IStorageManager { 5 | /// general key/value pair storage 6 | IValueStorageBucket getOrCreateValueStorageBucket(String name); 7 | void destroyValueStorageBucket(String name); 8 | 9 | /// get subscription storage 10 | /// responder path point to a local responder node 11 | /// which means the dslink on the other side of the connection is a requester 12 | ISubscriptionResponderStorage getOrCreateSubscriptionStorage(String responderPath); 13 | 14 | /// destroy subscription storage 15 | void destroySubscriptionStorage(String responderPath); 16 | 17 | /// load all saved subscriptions 18 | /// should be called only during application initialization 19 | Future>> loadSubscriptions(); 20 | } 21 | 22 | /// a storage container for one dslink 23 | /// different dslink will have different ISubscriptionResponderStorage 24 | abstract class ISubscriptionResponderStorage { 25 | String get responderPath; 26 | 27 | ISubscriptionNodeStorage getOrCreateValue(String valuePath); 28 | void destroyValue(String valuePath); 29 | /// load all saved subscriptions 30 | /// should be called only during application initialization 31 | Future> load(); 32 | void destroy(); 33 | } 34 | 35 | /// the storage of one value 36 | abstract class ISubscriptionNodeStorage { 37 | final String path; 38 | final ISubscriptionResponderStorage storage; 39 | int qos; 40 | ISubscriptionNodeStorage(this.path, this.storage); 41 | 42 | /// add data to List of values 43 | void addValue(ValueUpdate value); 44 | 45 | /// set value to newValue and clear all existing values in the storage 46 | /// [removes] is only designed for database that can't directly remove all data in a key. 47 | /// ValueUpdate.storedData can be used to store any helper data for the storage class 48 | void setValue(Iterable removes, ValueUpdate newValue); 49 | 50 | /// for some database it's easier to remove data one by one 51 | /// removeValue and valueRemoved will be both called, either one can be used 52 | void removeValue(ValueUpdate value); 53 | 54 | /// for some database it's easier to remove multiple data together 55 | /// removeValue and valueRemoved will be both called, either one can be used 56 | /// [updates] are all the remaining value that are still in the list 57 | void valueRemoved(Iterable updates){} 58 | 59 | /// clear the values, but still leave the qos data in storage 60 | void clear(int qos); 61 | void destroy(); 62 | 63 | /// return the existing storage values 64 | /// should be called only during application initialization 65 | /// and value will only be available after parent's load() function is finished 66 | List getLoadedValues(); 67 | } 68 | 69 | /// a storage class for general purpose key/value pair 70 | abstract class IValueStorageBucket { 71 | IValueStorage getValueStorage(String key); 72 | Future load(); 73 | void destroy(); 74 | } 75 | 76 | /// basic value storage 77 | abstract class IValueStorage { 78 | String get key; 79 | void setValue(Object value); 80 | Future getValueAsync(); 81 | void destroy(); 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/crypto/pk.dart: -------------------------------------------------------------------------------- 1 | library dslink.pk; 2 | 3 | import 'dart:async'; 4 | import 'dart:typed_data'; 5 | 6 | import 'dart/pk.dart' show DartCryptoProvider; 7 | import '../../utils.dart'; 8 | 9 | CryptoProvider _CRYPTO_PROVIDER = DartCryptoProvider.INSTANCE; 10 | bool _isCryptoProviderLocked = false; 11 | 12 | setCryptoProvider(CryptoProvider provider) { 13 | if(_isCryptoProviderLocked) 14 | throw new StateError("crypto provider is locked"); 15 | _CRYPTO_PROVIDER = provider; 16 | _isCryptoProviderLocked = true; 17 | } 18 | 19 | lockCryptoProvider() => _isCryptoProviderLocked = true; 20 | 21 | abstract class CryptoProvider { 22 | static String sha256(List list){ 23 | Uint8List bytes = ByteDataUtil.list2Uint8List(list); 24 | return _CRYPTO_PROVIDER.base64_sha256(bytes); 25 | } 26 | 27 | DSRandom get random; 28 | 29 | Future assign(PublicKey publicKeyRemote, ECDH old); 30 | Future getSecret(PublicKey publicKeyRemote); 31 | 32 | Future generate(); 33 | PrivateKey generateSync(); 34 | 35 | PrivateKey loadFromString(String str); 36 | 37 | PublicKey getKeyFromBytes(Uint8List bytes); 38 | 39 | String base64_sha256(Uint8List bytes); 40 | } 41 | 42 | abstract class ECDH { 43 | String get encodedPublicKey; 44 | 45 | static Future assign(PublicKey publicKeyRemote, ECDH old) async => 46 | _CRYPTO_PROVIDER.assign(publicKeyRemote, old); 47 | 48 | String hashSalt(String salt); 49 | 50 | bool verifySalt(String salt, String hash) { 51 | return hashSalt(salt) == hash; 52 | } 53 | } 54 | 55 | abstract class PublicKey { 56 | String get qBase64; 57 | String get qHash64; 58 | 59 | PublicKey(); 60 | 61 | factory PublicKey.fromBytes(Uint8List bytes) => 62 | _CRYPTO_PROVIDER.getKeyFromBytes(bytes); 63 | 64 | String getDsId(String prefix) { 65 | return '$prefix$qHash64'; 66 | } 67 | 68 | bool verifyDsId(String dsId) { 69 | return (dsId.length >= 43 && dsId.substring(dsId.length - 43) == qHash64); 70 | } 71 | } 72 | 73 | abstract class PrivateKey { 74 | PublicKey get publicKey; 75 | 76 | static Future generate() async => 77 | _CRYPTO_PROVIDER.generate(); 78 | 79 | factory PrivateKey.generateSync() => 80 | _CRYPTO_PROVIDER.generateSync(); 81 | 82 | factory PrivateKey.loadFromString(String str) => 83 | _CRYPTO_PROVIDER.loadFromString(str); 84 | 85 | String saveToString(); 86 | /// get the secret from the remote public key 87 | Future getSecret(String tempKey); 88 | } 89 | 90 | abstract class DSRandom { 91 | static DSRandom get instance => _CRYPTO_PROVIDER.random; 92 | bool get needsEntropy; 93 | 94 | int nextUint16() { 95 | var data = new ByteData(2); 96 | data.setUint8(0, nextUint8()); 97 | data.setUint8(1, nextUint8()); 98 | 99 | return data.getUint16(0); 100 | } 101 | 102 | int nextUint8(); 103 | 104 | void addEntropy(String str); 105 | } 106 | 107 | class DummyECDH implements ECDH { 108 | final String encodedPublicKey = ""; 109 | 110 | const DummyECDH(); 111 | 112 | String hashSalt(String salt) { 113 | return ''; 114 | } 115 | 116 | bool verifySalt(String salt, String hash) { 117 | return true; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/src/historian/rollup.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | abstract class Rollup { 4 | dynamic get value; 5 | 6 | void add(dynamic input); 7 | 8 | void reset(); 9 | } 10 | 11 | class FirstRollup extends Rollup { 12 | @override 13 | void add(input) { 14 | if (set) { 15 | return; 16 | } 17 | value = input; 18 | set = true; 19 | } 20 | 21 | @override 22 | void reset() { 23 | set = false; 24 | } 25 | 26 | dynamic value; 27 | bool set = false; 28 | } 29 | 30 | class LastRollup extends Rollup { 31 | @override 32 | void add(input) { 33 | value = input; 34 | } 35 | 36 | @override 37 | void reset() { 38 | } 39 | 40 | dynamic value; 41 | } 42 | 43 | class AvgRollup extends Rollup { 44 | @override 45 | void add(input) { 46 | if (input is String) { 47 | input = num.parse(input, (e) => input.length); 48 | } 49 | 50 | if (input is! num) { 51 | return; 52 | } 53 | 54 | total += input; 55 | count++; 56 | } 57 | 58 | @override 59 | void reset() { 60 | total = 0.0; 61 | count = 0; 62 | } 63 | 64 | dynamic total = 0.0; 65 | 66 | dynamic get value => total / count; 67 | int count = 0; 68 | } 69 | 70 | class SumRollup extends Rollup { 71 | @override 72 | void add(input) { 73 | if (input is String) { 74 | input = num.parse(input, (e) => input.length); 75 | } 76 | 77 | if (input is! num) { 78 | return; 79 | } 80 | 81 | value += input; 82 | } 83 | 84 | @override 85 | void reset() { 86 | value = 0.0; 87 | } 88 | 89 | dynamic value = 0.0; 90 | } 91 | 92 | class CountRollup extends Rollup { 93 | @override 94 | void add(input) { 95 | value++; 96 | } 97 | 98 | @override 99 | void reset() { 100 | value = 0; 101 | } 102 | 103 | dynamic value = 0; 104 | } 105 | 106 | class MaxRollup extends Rollup { 107 | @override 108 | void add(input) { 109 | if (input is String) { 110 | input = num.parse(input, (e) => null); 111 | } 112 | 113 | if (input is! num) { 114 | return; 115 | } 116 | 117 | value = max(value == null ? double_NEGATIVE_INFINITY : value as num, input as num); 118 | } 119 | 120 | @override 121 | void reset() { 122 | value = null; 123 | } 124 | 125 | dynamic value; 126 | } 127 | 128 | class MinRollup extends Rollup { 129 | @override 130 | void add(input) { 131 | if (input is String) { 132 | input = num.parse(input, (e) => null); 133 | } 134 | 135 | if (input is! num) { 136 | return; 137 | } 138 | 139 | value = min(value == null ? double_INFINITY : value as num, input as num); 140 | } 141 | 142 | @override 143 | void reset() { 144 | value = null; 145 | } 146 | 147 | dynamic value; 148 | } 149 | 150 | typedef Rollup RollupFactory(); 151 | 152 | final Map _rollups = { 153 | "none": () => null, 154 | "delta": () => new FirstRollup(), 155 | "first": () => new FirstRollup(), 156 | "last": () => new LastRollup(), 157 | "max": () => new MaxRollup(), 158 | "min": () => new MinRollup(), 159 | "count": () => new CountRollup(), 160 | "sum": () => new SumRollup(), 161 | "avg": () => new AvgRollup() 162 | }; 163 | -------------------------------------------------------------------------------- /lib/src/responder/base_impl/local_node_impl.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | abstract class NodeProviderImpl extends NodeProvider { 4 | Map get nodes; 5 | } 6 | 7 | abstract class LocalNodeImpl extends LocalNode { 8 | LocalNodeImpl(String path) : super(path); 9 | 10 | Map serialize(bool withChildren) { 11 | var rslt = {}; 12 | configs.forEach((key, val) { 13 | rslt[key] = val; 14 | }); 15 | 16 | attributes.forEach((key, val) { 17 | rslt[key] = val; 18 | }); 19 | 20 | children.forEach((key, val) { 21 | if (withChildren) { 22 | if (val is LocalNodeImpl) { 23 | rslt[key] = val.serialize(true); 24 | } else { 25 | rslt[key] = val.getSimpleMap(); 26 | } 27 | } 28 | }); 29 | 30 | return rslt; 31 | } 32 | 33 | bool _loaded = false; 34 | 35 | bool get loaded => _loaded; 36 | 37 | void load(Map m) { 38 | if (_loaded) { 39 | configs.clear(); 40 | attributes.clear(); 41 | children.clear(); 42 | } 43 | String childPathPre; 44 | if (path == '/') { 45 | childPathPre = '/'; 46 | } else { 47 | childPathPre = '$path/'; 48 | } 49 | 50 | m.forEach((/*String*/ key, value) { 51 | if (key.startsWith(r'$')) { 52 | configs[key] = value; 53 | } else if (key.startsWith('@')) { 54 | attributes[key] = value; 55 | } else if (value is Map) { 56 | Node node = provider.getOrCreateNode('$childPathPre$key', false); 57 | if (node is LocalNodeImpl) { 58 | node.load(value); 59 | } 60 | children[key] = node; 61 | } 62 | }); 63 | _loaded = true; 64 | } 65 | 66 | void updateList(String name) { 67 | listChangeController.add(name); 68 | } 69 | 70 | Response setAttribute(String name, Object value, Responder responder, 71 | Response response) { 72 | if (!attributes.containsKey(name) || attributes[name] != value) { 73 | attributes[name] = value; 74 | updateList(name); 75 | 76 | if (provider is SerializableNodeProvider) { 77 | (provider as SerializableNodeProvider).persist(); 78 | } 79 | } 80 | return response..close(); 81 | } 82 | 83 | Response removeAttribute(String name, Responder responder, 84 | Response response) { 85 | if (attributes.containsKey(name)) { 86 | attributes.remove(name); 87 | updateList(name); 88 | 89 | if (provider is SerializableNodeProvider) { 90 | (provider as SerializableNodeProvider).persist(); 91 | } 92 | } 93 | return response..close(); 94 | } 95 | 96 | Response setConfig(String name, Object value, Responder responder, 97 | Response response) { 98 | var config = Configs.getConfig(name, profile); 99 | response.close(config.setConfig(value, this, responder)); 100 | return response; 101 | } 102 | 103 | Response removeConfig(String name, Responder responder, Response response) { 104 | var config = Configs.getConfig(name, profile); 105 | return response..close(config.removeConfig(this, responder)); 106 | } 107 | 108 | Response setValue( 109 | Object value, 110 | Responder responder, 111 | Response response, 112 | [int maxPermission = Permission.CONFIG]) { 113 | updateValue(value); 114 | // TODO: check value type 115 | return response..close(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/src/browser/browser_user_link.dart: -------------------------------------------------------------------------------- 1 | part of dslink.browser_client; 2 | 3 | /// a client link for both http and ws 4 | class BrowserUserLink extends ClientLink { 5 | Completer _onRequesterReadyCompleter = new Completer(); 6 | 7 | Future get onRequesterReady => _onRequesterReadyCompleter.future; 8 | 9 | static String session = DSRandom.instance.nextUint16().toRadixString(16) + 10 | DSRandom.instance.nextUint16().toRadixString(16) + 11 | DSRandom.instance.nextUint16().toRadixString(16) + 12 | DSRandom.instance.nextUint16().toRadixString(16); 13 | final Requester requester; 14 | final Responder responder; 15 | 16 | final ECDH nonce = const DummyECDH(); 17 | PrivateKey privateKey; 18 | 19 | WebSocketConnection _wsConnection; 20 | 21 | bool enableAck; 22 | 23 | static const Map saltNameMap = const {"salt": 0, "saltS": 1,}; 24 | 25 | updateSalt(String salt) { 26 | // TODO: implement updateSalt 27 | } 28 | 29 | String wsUpdateUri; 30 | String format = "json"; 31 | 32 | BrowserUserLink({NodeProvider nodeProvider, 33 | bool isRequester: true, 34 | bool isResponder: true, 35 | this.wsUpdateUri, 36 | this.enableAck: false, 37 | String format}) 38 | : requester = isRequester ? new Requester() : null, 39 | responder = (isResponder && nodeProvider != null) 40 | ? new Responder(nodeProvider) 41 | : null { 42 | if (wsUpdateUri.startsWith("http")) { 43 | wsUpdateUri = "ws${wsUpdateUri.substring(4)}"; 44 | } 45 | 46 | if (format != null) { 47 | this.format = format; 48 | } 49 | 50 | if (window.location.hash.contains("dsa_json")) { 51 | this.format = "json"; 52 | } 53 | } 54 | 55 | void connect() { 56 | lockCryptoProvider(); 57 | initWebsocket(false); 58 | } 59 | 60 | int _wsDelay = 1; 61 | 62 | initWebsocket([bool reconnect = true]) { 63 | var socket = new WebSocket("$wsUpdateUri?session=$session&format=$format"); 64 | _wsConnection = new WebSocketConnection( 65 | socket, this, enableAck: enableAck, useCodec: DsCodec.getCodec(format)); 66 | 67 | if (responder != null) { 68 | responder.connection = _wsConnection.responderChannel; 69 | } 70 | 71 | if (requester != null) { 72 | _wsConnection.onRequesterReady.then((channel) { 73 | requester.connection = channel; 74 | if (!_onRequesterReadyCompleter.isCompleted) { 75 | _onRequesterReadyCompleter.complete(requester); 76 | } 77 | }); 78 | } 79 | _wsConnection.onDisconnected.then((connection) { 80 | logger.info("Disconnected"); 81 | if (_wsConnection == null) { 82 | // connection is closed 83 | return; 84 | } 85 | if (_wsConnection._opened) { 86 | _wsDelay = 1; 87 | initWebsocket(false); 88 | } else if (reconnect) { 89 | DsTimer.timerOnceAfter(initWebsocket, _wsDelay * 1000); 90 | if (_wsDelay < 60) _wsDelay++; 91 | } else { 92 | _wsDelay = 5; 93 | DsTimer.timerOnceAfter(initWebsocket, 5000); 94 | } 95 | }); 96 | } 97 | void reconnect() { 98 | if (_wsConnection != null) { 99 | _wsConnection.socket.close(); 100 | } 101 | } 102 | void close() { 103 | if (_wsConnection != null) { 104 | _wsConnection.close(); 105 | _wsConnection = null; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tool/experiment/test_ds_handshake.dart: -------------------------------------------------------------------------------- 1 | import 'package:dslink/src/crypto/pk.dart'; 2 | import 'package:dslink/src/crypto/dart/pk.dart'; 3 | 4 | String clientPrivate = "M6S41GAL0gH0I97Hhy7A2-icf8dHnxXPmYIRwem03HE"; 5 | String clientPublic = "BEACGownMzthVjNFT7Ry-RPX395kPSoUqhQ_H_vz0dZzs5RYoVJKA16XZhdYd__ksJP0DOlwQXAvoDjSMWAhkg4"; 6 | String clientDsId = "test-s-R9RKdvC2VNkfRwpNDMMpmT_YWVbhPLfbIc-7g4cpc"; 7 | String serverTempPrivate = "rL23cF6HxmEoIaR0V2aORlQVq2LLn20FCi4_lNdeRkk"; 8 | String serverTempPublic = "BCVrEhPXmozrKAextseekQauwrRz3lz2sj56td9j09Oajar0RoVR5Uo95AVuuws1vVEbDzhOUu7freU0BXD759U"; 9 | String sharedSecret = "116128c016cf380933c4b40ffeee8ef5999167f5c3d49298ba2ebfd0502e74e3"; 10 | String hashedAuth = "V2P1nwhoENIi7SqkNBuRFcoc8daWd_iWYYDh_0Z01rs"; 11 | 12 | void main() { 13 | //testAlgorithm(); 14 | testApi(); 15 | print('All Tests Passed!'); 16 | } 17 | 18 | //void testAlgorithm() { 19 | // 20 | // /// Initialize connection , Client -> Server 21 | // Uint8List modulusHash = new SHA256Digest().process(bigintToUint8List(modulus)); 22 | // String dsId = 'test-${Base64.encode(modulusHash)}'; 23 | // __assertEqual(dsId, 'test-pTrfpbVWb3NNAhMIXr_FpmV3oObtMVxPcNu2mDksp0M', 'dsId'); 24 | // 25 | // /// Initialize connection , Server -> Client 26 | // 27 | // BigInteger A = new BigInteger.fromBytes(1, nonceBytes); 28 | // BigInteger E = A.modPow(new BigInteger(65537), modulus); 29 | // String encryptedNonce = Base64.encode(bigintToUint8List(E)); 30 | // __assertEqual(encryptedNonce, encryptedNonceCompare, 'encryptedNonce'); 31 | // 32 | // /// Start Connection (http or ws), Client -> Server 33 | // /// Decode 34 | // 35 | // BigInteger decodeE = new BigInteger.fromBytes(1, Base64.decode(encryptedNonce)); 36 | // __assertEqual(decodeE, E, 'decoded E'); 37 | // 38 | // BigInteger decryptedA = E.modPow(privateExp, modulus); 39 | // __assertEqual(decryptedA, A, 'decrypted A'); 40 | // 41 | // Uint8List decryptedNonce = bigintToUint8List(decryptedA); 42 | // __assertEqual(bytes2hex(decryptedNonce), bytes2hex(nonceBytes), 'decrypted Nonce'); 43 | // 44 | // /// Make Auth 45 | // 46 | // List authRaw = new Uint8List.fromList([] 47 | // ..addAll(UTF8.encode(salt)) 48 | // ..addAll(decryptedNonce)); 49 | // __assertEqual(bytes2hex(authRaw), '3078313030d26538aabf9a97bcfd8bc0dd1a727c92', 'auth raw'); 50 | // 51 | // Uint8List digest = new SHA256Digest().process(authRaw); 52 | // String auth = Base64.encode(digest); 53 | // __assertEqual(auth, authCompare, 'auth'); 54 | //} 55 | 56 | testApi() async { 57 | PrivateKey prikey = new PrivateKey.loadFromString(clientPrivate); 58 | PublicKey pubkey = prikey.publicKey; 59 | 60 | __assertEqual(pubkey.qBase64, clientPublic, 'API public key'); 61 | 62 | /// Initialize connection , Client -> Server 63 | 64 | String dsId = pubkey.getDsId('test-'); 65 | __assertEqual(dsId, clientDsId, 'API dsId'); 66 | 67 | /// Initialize connection , Server -> Client 68 | PrivateKey tPrikey = new PrivateKey.loadFromString(serverTempPrivate); 69 | PublicKey tPubkey = tPrikey.publicKey; 70 | 71 | __assertEqual(tPubkey.qBase64, serverTempPublic, 'API temp key'); 72 | 73 | 74 | /// Start Connection (http or ws), Client -> Server 75 | /// Decode 76 | ECDHImpl clientEcdh = await prikey.getSecret(tPubkey.qBase64); 77 | ECDHImpl serverEcdh = await tPrikey.getSecret(pubkey.qBase64); 78 | 79 | __assertEqual(bytes2hex(clientEcdh.bytes), sharedSecret, 'API client ECDH'); 80 | __assertEqual(bytes2hex(serverEcdh.bytes), sharedSecret, 'API server ECDH'); 81 | 82 | /// Make Auth 83 | String auth = serverEcdh.hashSalt('0000'); 84 | __assertEqual(auth, hashedAuth, 'API auth'); 85 | } 86 | void __assertEqual(a, b, String testName) { 87 | if (a != b) { 88 | print('$testName Test Failed\na: $a\nb: $b'); 89 | throw 0; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /example/browser/grid.dart: -------------------------------------------------------------------------------- 1 | import "dart:html"; 2 | 3 | import "package:dslink/browser.dart"; 4 | 5 | LinkProvider link; 6 | Requester r; 7 | 8 | main() async { 9 | var brokerUrl = await BrowserUtils.fetchBrokerUrlFromPath( 10 | "broker_url", "http://localhost:8080/conn"); 11 | 12 | link = new LinkProvider( 13 | brokerUrl, "HtmlGrid-", isRequester: true, isResponder: false); 14 | await link.connect(); 15 | 16 | r = link.requester; 17 | 18 | var dataNode = await r.getRemoteNode("/data"); 19 | if (!dataNode.children.containsKey("grid")) { 20 | await r.invoke("/data/addValue", { 21 | "Name": "grid", 22 | "Type": "array" 23 | }).firstWhere((x) => x.streamStatus == StreamStatus.closed); 24 | 25 | var generateList = (int i) => 26 | new List.generate(15, (x) => false); 27 | var list = new List>.generate(15, generateList); 28 | await r.set("/data/grid", list); 29 | } 30 | 31 | r.onValueChange("/data/grid").listen((ValueUpdate update) { 32 | if (update.value is! List) return; 33 | 34 | var isNew = _grid == null; 35 | 36 | loadGrid(update.value as List>); 37 | 38 | if (isNew) { 39 | resizeGrid(15, 15); 40 | } 41 | }); 42 | 43 | querySelector("#clear-btn").onClick.listen((e) { 44 | clearGrid(); 45 | }); 46 | } 47 | 48 | List> _grid = >[]; 49 | 50 | resizeGrid(int width, int height) { 51 | List> grid = deepCopy(_grid) as List>; 52 | 53 | print(grid); 54 | 55 | grid.length = height; 56 | for (var i = 0; i < height; i++) { 57 | var row = grid[i]; 58 | if (row == null) { 59 | row = grid[i] = new List(); 60 | } 61 | 62 | row.length = width; 63 | for (var x = 0; x < width; x++) { 64 | if (row[x] == null) { 65 | row[x] = false; 66 | } 67 | } 68 | } 69 | 70 | _grid = grid; 71 | r.set("/data/grid", _grid); 72 | } 73 | 74 | dynamic deepCopy(input) { 75 | if (input is List) { 76 | return input.map(deepCopy).toList(); 77 | } 78 | return input; 79 | } 80 | 81 | clearGrid() { 82 | for (var row in _grid) { 83 | row.fillRange(0, _grid.length, false); 84 | } 85 | r.set("/data/grid", _grid); 86 | } 87 | 88 | loadGrid(List> input) { 89 | _grid = input; 90 | 91 | var root = querySelector("#root"); 92 | 93 | for (var i = 1; i <= input.length; i++) { 94 | List row = input[i - 1]; 95 | 96 | DivElement rowe = querySelector("#row-${i}"); 97 | if (rowe == null) { 98 | rowe = new DivElement(); 99 | rowe.id = "row-${i}"; 100 | rowe.classes.add("row"); 101 | root.append(rowe); 102 | } 103 | 104 | for (var x = 1; x <= row.length; x++) { 105 | bool val = row[x - 1]; 106 | DivElement cow = querySelector("#block-${i}-${x}"); 107 | if (cow == null) { 108 | cow = new DivElement(); 109 | cow.id = "block-${i}-${x}"; 110 | cow.classes.add("block"); 111 | cow.style.transition = "background-color 0.2s"; 112 | cow.onClick.listen((e) { 113 | if (_grid[i - 1][x - 1]) { 114 | _grid[i - 1][x - 1] = false; 115 | } else { 116 | _grid[i - 1][x - 1] = true; 117 | } 118 | 119 | r.set("/data/grid", _grid); 120 | }); 121 | rowe.append(cow); 122 | } 123 | 124 | String color; 125 | 126 | if (val == true) { 127 | color = "red"; 128 | } else if (val == false) { 129 | color = "white"; 130 | } else { 131 | color = "gray"; 132 | } 133 | 134 | if (cow.style.backgroundColor != color) { 135 | cow.style.backgroundColor = color; 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tool/experiment/large.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | import "dart:math"; 3 | 4 | import "package:dslink/dslink.dart"; 5 | import "package:dslink/nodes.dart"; 6 | 7 | LinkProvider link; 8 | 9 | int current = 0; 10 | 11 | main(List args) { 12 | link = new LinkProvider(args, "Large-", defaultNodes: { 13 | "Generate": { 14 | r"$invokable": "write", 15 | r"$is": "generate", 16 | r"$params": [ 17 | { 18 | "name": "count", 19 | "type": "number", 20 | "default": 50 21 | } 22 | ] 23 | }, 24 | "Reduce": { 25 | r"$invokable": "write", 26 | r"$is": "reduce", 27 | r"$params": [ 28 | { 29 | "name": "target", 30 | "type": "number", 31 | "default": 1 32 | } 33 | ] 34 | }, 35 | "Tick_Rate": { 36 | r"$name": "Tick Rate", 37 | r"$type": "number", 38 | r"$writable": "write", 39 | "?value": 300 40 | }, 41 | "RNG_Maximum": { 42 | r"$name": "Maximum Random Number", 43 | r"$type": "number", 44 | r"$writable": "write", 45 | "?value": max 46 | } 47 | }, profiles: { 48 | "generate": (String path) => new SimpleActionNode(path, (Map params) { 49 | var count = params["count"] != null ? params["count"] : 50; 50 | generate(count); 51 | }), 52 | "reduce": (String path) => new SimpleActionNode(path, (Map params) { 53 | var target = params["target"] != null ? params["target"] : 1; 54 | for (var name in link["/"].children.keys.where((it) => it.startsWith("Node_")).toList()) { 55 | link.removeNode("/${name}"); 56 | } 57 | generate(target); 58 | }), 59 | "test": (String path) { 60 | CallbackNode node; 61 | 62 | node = new CallbackNode(path, onCreated: () { 63 | nodes.add(node); 64 | }, onRemoving: () { 65 | nodes.remove(node); 66 | }); 67 | 68 | return node; 69 | } 70 | }); 71 | 72 | link.onValueChange("/Tick_Rate").listen((ValueUpdate u) { 73 | if (schedule != null) { 74 | schedule.cancel(); 75 | schedule = null; 76 | } 77 | 78 | schedule = Scheduler.every(new Interval.forMilliseconds(u.value), update); 79 | }); 80 | 81 | link.onValueChange("/RNG_Maximum").listen((ValueUpdate u) { 82 | max = u.value; 83 | }); 84 | 85 | link.connect(); 86 | 87 | schedule = Scheduler.every(Interval.THREE_HUNDRED_MILLISECONDS, update); 88 | } 89 | 90 | Timer schedule; 91 | int max = 100; 92 | 93 | void update() { 94 | nodes.forEach((node) { 95 | var l = link["${node.path}/RNG/Value"]; 96 | if (l.hasSubscriber) { 97 | l.updateValue(random.nextInt(max)); 98 | } 99 | }); 100 | } 101 | 102 | Random random = new Random(); 103 | List nodes = []; 104 | 105 | void generate(int count) { 106 | for (var i = 1; i <= count; i++) { 107 | link.addNode("/Node_${i}", { 108 | r"$is": "test", 109 | r"$name": "Node ${i}", 110 | "Values": { 111 | "String_Value": { 112 | r"$name": "String Value", 113 | r"$type": "string", 114 | r"$writable": "write", 115 | "?value": "Hello World" 116 | }, 117 | "Number_Value": { 118 | r"$name": "Number Value", 119 | r"$type": "number", 120 | r"$writable": "write", 121 | "?value": 5.0 122 | }, 123 | "Integer_Value": { 124 | r"$name": "Integer Value", 125 | r"$type": "number", 126 | r"$writable": "write", 127 | "?value": 5 128 | } 129 | }, 130 | "RNG": { 131 | "Value": { 132 | r"$type": "number", 133 | "?value": 0.0 134 | } 135 | } 136 | }); 137 | current++; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/responder/response/invoke.dart: -------------------------------------------------------------------------------- 1 | part of dslink.responder; 2 | 3 | typedef void OnInvokeClosed(InvokeResponse response); 4 | typedef void OnInvokeSend(InvokeResponse response, Map m); 5 | 6 | /// return true if params are valid 7 | typedef bool OnReqParams(InvokeResponse resp, Map m); 8 | 9 | class _InvokeResponseUpdate { 10 | String status; 11 | List columns; 12 | List updates; 13 | Map meta; 14 | 15 | _InvokeResponseUpdate(this.status, this.updates, this.columns, this.meta); 16 | } 17 | 18 | class InvokeResponse extends Response { 19 | final LocalNode parentNode; 20 | final LocalNode node; 21 | final String name; 22 | 23 | InvokeResponse(Responder responder, int rid, this.parentNode, this.node, this.name) 24 | : super(responder, rid, 'invoke'); 25 | 26 | List<_InvokeResponseUpdate> pendingData = new List<_InvokeResponseUpdate>(); 27 | 28 | bool _hasSentColumns = false; 29 | 30 | /// update data for the responder stream 31 | void updateStream(List updates, 32 | {List columns, String streamStatus: StreamStatus.open, 33 | Map meta, bool autoSendColumns: true}) { 34 | if (meta != null && meta['mode'] == 'refresh') { 35 | pendingData.length = 0; 36 | } 37 | 38 | if (!_hasSentColumns) { 39 | if (columns == null && 40 | autoSendColumns && 41 | node != null && 42 | node.configs[r"$columns"] is List) { 43 | columns = node.configs[r"$columns"]; 44 | } 45 | } 46 | 47 | if (columns != null) { 48 | _hasSentColumns = true; 49 | } 50 | 51 | pendingData.add( 52 | new _InvokeResponseUpdate(streamStatus, updates, columns, meta) 53 | ); 54 | prepareSending(); 55 | } 56 | 57 | OnReqParams onReqParams; 58 | /// new parameter from the requester 59 | void updateReqParams(Map m) { 60 | if (onReqParams != null) { 61 | onReqParams(this, m); 62 | } 63 | } 64 | 65 | @override 66 | void startSendingData(int currentTime, int waitingAckId) { 67 | _pendingSending = false; 68 | if (_err != null) { 69 | responder.closeResponse(rid, response: this, error: _err); 70 | if (_sentStreamStatus == StreamStatus.closed) { 71 | _close(); 72 | } 73 | return; 74 | } 75 | 76 | for (_InvokeResponseUpdate update in pendingData) { 77 | List> outColumns; 78 | if (update.columns != null) { 79 | outColumns = TableColumn.serializeColumns(update.columns); 80 | } 81 | 82 | responder.updateResponse( 83 | this, 84 | update.updates, 85 | streamStatus: update.status, 86 | columns: outColumns, 87 | meta: update.meta, handleMap: (m) { 88 | if (onSendUpdate != null) { 89 | onSendUpdate(this, m); 90 | } 91 | }); 92 | 93 | if (_sentStreamStatus == StreamStatus.closed) { 94 | _close(); 95 | break; 96 | } 97 | } 98 | pendingData.length = 0; 99 | } 100 | 101 | /// close the request from responder side and also notify the requester 102 | void close([DSError err = null]) { 103 | if (err != null) { 104 | _err = err; 105 | } 106 | if (!pendingData.isEmpty) { 107 | pendingData.last.status = StreamStatus.closed; 108 | } else { 109 | pendingData.add( 110 | new _InvokeResponseUpdate(StreamStatus.closed, null, null, null) 111 | ); 112 | prepareSending(); 113 | } 114 | } 115 | 116 | DSError _err; 117 | 118 | OnInvokeClosed onClose; 119 | OnInvokeSend onSendUpdate; 120 | 121 | void _close() { 122 | if (onClose != null) { 123 | onClose(this); 124 | } 125 | } 126 | 127 | /// for the broker trace action 128 | ResponseTrace getTraceData([String change = '+']) { 129 | return new ResponseTrace(parentNode.path, 'invoke', rid, change, name); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tool/experiment/browser/lockerdemo.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dslink/src/crypto/pk.dart'; 3 | import 'package:dslink/browser_client.dart'; 4 | import 'package:dslink/responder.dart'; 5 | import 'package:dslink/common.dart'; 6 | 7 | // load private ECDH key 8 | // this can be replaced with other authentication method if it's implemented in broker 9 | PrivateKey key = new PrivateKey.loadFromString( 10 | 'J7wbaV2z-HDVDau2WrOf6goPgbZnj0xamPid1MNOuVc BC7EZK44i85VUr5LleLsLP-Bu6MkK2IbZWVHXBaUQlRKmfkT_488BW-KwOgoize4gaRVF1i0NarPeLgCXM6pGrE'); 11 | 12 | SimpleNodeProvider nodeProvider; 13 | 14 | class OpenLockerAction extends SimpleNode { 15 | OpenLockerAction(String path) : super(path); 16 | 17 | Object onInvoke(Map params) { 18 | nodeProvider.updateValue('${path}ed', true); 19 | return {"value":"a"}; 20 | } 21 | } 22 | 23 | class ChangeLocker extends SimpleNode { 24 | ChangeLocker(String path) : super(path); 25 | 26 | Object onInvoke(Map params) { 27 | if (params['value'] is bool) { 28 | nodeProvider.updateValue('${path}ed', params['value']); 29 | } 30 | return {"value":"a"}; 31 | } 32 | } 33 | 34 | void main() { 35 | var profiles = { 36 | 'openLocker':(String path) { 37 | return new OpenLockerAction(path); 38 | }, 39 | 'changeLocker':(String path) { 40 | return new ChangeLocker(path); 41 | }, 42 | }; 43 | 44 | nodeProvider = new SimpleNodeProvider({ 45 | 'locker1': { 46 | r'$is':'locker', 47 | 'open': { // an action to open the door 48 | r'$invokable': 'read', 49 | r'$function': 'openLocker' 50 | }, 51 | 'opened': { // the open status value 52 | r'$type': 'bool', 53 | '?value': false 54 | } 55 | }, 56 | 'locker2': { 57 | r'$is':'locker', 58 | 'open': { // an action to open the door 59 | r'$invokable': 'read', 60 | r'$params':[{"name":"value", "type":"bool"}], 61 | r'$function': 'changeLocker' 62 | }, 63 | 'opened': { // the open status value 64 | r'$type': 'bool', 65 | '?value': false 66 | } 67 | } 68 | }, profiles); 69 | 70 | // add locker at runtime 71 | nodeProvider.addNode('/locker3', { 72 | r'$is':'locker', 73 | 'open': { // an action to open the door 74 | r'$invokable': 'read', 75 | r'$params':[{"name":"value", "type":"bool"}], 76 | r'$function': 'openLocker' 77 | }, 78 | 'opened': { // the open status value 79 | r'$type': 'bool', 80 | '?value': false 81 | } 82 | }); 83 | 84 | new BrowserECDHLink( 85 | 'http://localhost:8080/conn', 'locker-', key, isResponder: true, 86 | nodeProvider: nodeProvider) 87 | ..connect(); 88 | 89 | initUI(); 90 | } 91 | 92 | void initUI() { 93 | // update label 94 | nodeProvider.getNode('/locker1/opened').subscribe((ValueUpdate update) { 95 | document 96 | .querySelector('#opentext1') 97 | .text = update.value == true ? 'Opened' : 'Closed'; 98 | }); 99 | nodeProvider.getNode('/locker2/opened').subscribe((ValueUpdate update) { 100 | document 101 | .querySelector('#opentext2') 102 | .text = update.value == true ? 'Opened' : 'Closed'; 103 | }); 104 | // buttons 105 | document 106 | .querySelector('#openbtn1') 107 | .onClick 108 | .listen((e) => nodeProvider.updateValue('/locker1/opened', true)); 109 | document 110 | .querySelector('#closebtn1') 111 | .onClick 112 | .listen((e) => nodeProvider.updateValue('/locker1/opened', false)); 113 | document 114 | .querySelector('#openbtn2') 115 | .onClick 116 | .listen((e) => nodeProvider.updateValue('/locker2/opened', true)); 117 | document 118 | .querySelector('#closebtn2') 119 | .onClick 120 | .listen((e) => nodeProvider.updateValue('/locker2/opened', false)); 121 | } 122 | -------------------------------------------------------------------------------- /lib/src/nodes/json.dart: -------------------------------------------------------------------------------- 1 | part of dslink.nodes; 2 | 3 | class DsaJsonNode extends SimpleNode { 4 | DsaJsonNode(String path, [SimpleNodeProvider provider]) : 5 | super(path, provider); 6 | 7 | dynamic _json; 8 | 9 | void init(value) { 10 | load(buildNodeMap(value)); 11 | _json = value; 12 | } 13 | 14 | void updateJsonValue(input) { 15 | if (input is! Map) { 16 | updateValue(input); 17 | _json = input; 18 | 19 | String type = _guessType(input); 20 | 21 | String lastType = configs[r"$type"]; 22 | 23 | if (lastType != type) { 24 | configs[r"$type"] = type; 25 | updateList(r"$type"); 26 | } 27 | 28 | return; 29 | } 30 | 31 | clearValue(); 32 | JsonDiff.JsonDiffer differ = new JsonDiff.JsonDiffer( 33 | JSON.encode(_json), 34 | JSON.encode(input) 35 | ); 36 | 37 | JsonDiff.DiffNode fullDiff = differ.diff(); 38 | 39 | void apply(JsonDiff.DiffNode diff, DsaJsonNode node) { 40 | for (String key in diff.added.keys) { 41 | var name = NodeNamer.createName(key); 42 | provider.addNode( 43 | "${node.path}/${name}", 44 | buildNodeMap(diff.added[key]) 45 | ); 46 | node.updateList(r"$is"); 47 | } 48 | 49 | for (String key in diff.removed.keys) { 50 | var name = NodeNamer.createName(key); 51 | 52 | provider.removeNode("${node.path}/${name}"); 53 | } 54 | 55 | for (String key in diff.changed.keys) { 56 | var name = NodeNamer.createName(key); 57 | 58 | DsaJsonNode child = node.getChild(name); 59 | 60 | if (child == null) { 61 | child = provider.addNode( 62 | "${node.path}/${name}", 63 | buildNodeMap(diff.changed[key][1]) 64 | ); 65 | } else { 66 | child.updateJsonValue(diff.changed[key][1]); 67 | } 68 | } 69 | 70 | for (String key in diff.node.keys) { 71 | var name = NodeNamer.createName(key); 72 | 73 | DsaJsonNode child = node.getChild(name); 74 | 75 | if (child == null) { 76 | child = provider.addNode("${node.path}/${name}", buildNodeMap({})); 77 | } 78 | 79 | apply(diff.node[key], child); 80 | } 81 | } 82 | 83 | apply(fullDiff, this); 84 | 85 | _json = input; 86 | } 87 | 88 | @override 89 | void load(Map m) { 90 | super.load(m); 91 | 92 | if (m["?json"] != null) { 93 | init(m["?json"]); 94 | } 95 | 96 | if (m["?_json"] != null) { 97 | updateJsonValue(m["?_json"]); 98 | } 99 | } 100 | 101 | @override 102 | Map save() { 103 | var data = super.save(); 104 | data["?json"] = _json; 105 | return data; 106 | } 107 | 108 | static String _guessType(input) { 109 | if (input is String) { 110 | return "string"; 111 | } else if (input is num) { 112 | return "number"; 113 | } else if (input is bool) { 114 | return "bool"; 115 | } else { 116 | return "dynamic"; 117 | } 118 | } 119 | 120 | static Map buildNodeMap(input) { 121 | Map create(value) { 122 | if (value is Map) { 123 | var m = { 124 | r"$is": "json" 125 | }; 126 | 127 | for (String key in value.keys) { 128 | m[NodeNamer.createName(key)] = create(value[key]); 129 | } 130 | 131 | return m; 132 | } else if (value is List && value.every((e) => e is Map || e is List)) { 133 | var m = {}; 134 | for (var i = 0; i < value.length; i++) { 135 | m[i.toString()] = create(value[i]); 136 | } 137 | return m; 138 | } else { 139 | return { 140 | r"$is": "json", 141 | r"$type": _guessType(value), 142 | "?_json": value 143 | }; 144 | } 145 | } 146 | 147 | return create(input); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tool/experiment/test_concurrent.dart: -------------------------------------------------------------------------------- 1 | import "dart:math"; 2 | 3 | import "package:dslink/dslink.dart"; 4 | import "package:dslink/utils.dart"; 5 | import "package:logging/logging.dart"; 6 | 7 | import "package:args/args.dart"; 8 | 9 | class TestNodeProvider extends NodeProvider { 10 | TestNode onlyNode; 11 | TestNodeProvider(){ 12 | onlyNode = new TestNode('/', this); 13 | } 14 | 15 | LocalNode getNode(String path) { 16 | return onlyNode; 17 | } 18 | IPermissionManager permissions = new DummyPermissionManager(); 19 | Responder createResponder(String dsId, String sessionId) { 20 | return new Responder(this, dsId); 21 | } 22 | LocalNode getOrCreateNode(String path, [bool addToTree = true]) { 23 | return onlyNode; 24 | } 25 | } 26 | 27 | class TestNode extends LocalNodeImpl { 28 | NodeProvider provider; 29 | TestNode(String path, this.provider) : super(path) { 30 | configs[r'$is'] = 'node'; 31 | configs[r'$test'] = 'hello world'; 32 | configs[r'$type'] = 'number'; 33 | children['node'] = this; 34 | } 35 | } 36 | 37 | int pairCount = 1000; 38 | 39 | Stopwatch stopwatch; 40 | Random random = new Random(); 41 | 42 | main(List args) async { 43 | var argp = new ArgParser(); 44 | argp.addOption("pairs", abbr: "p", help: "Number of Link Pairs", defaultsTo: "1000", valueHelp: "pairs"); 45 | var opts = argp.parse(args); 46 | 47 | try { 48 | pairCount = int.parse(opts["pairs"]); 49 | } catch (e) { 50 | print("Invalid Number of Pairs."); 51 | return; 52 | } 53 | 54 | logger.level = Level.WARNING; 55 | 56 | stopwatch = new Stopwatch(); 57 | 58 | await createLinks(); 59 | int mm = 0; 60 | bool ready = false; 61 | 62 | Scheduler.every(Interval.TWO_SECONDS, () { 63 | if (connectedCount != pairCount) { 64 | mm++; 65 | 66 | if (mm == 2) { 67 | print("${connectedCount} of ${pairCount} link pairs are ready."); 68 | mm = 0; 69 | } 70 | 71 | return; 72 | } 73 | 74 | if (!ready) { 75 | print("All link pairs are now ready. Subscribing requesters to values and starting value updates."); 76 | ready = true; 77 | } 78 | 79 | var pi = 1; 80 | 81 | while (pi <= 5) { 82 | var rpc = getRandomPair(); 83 | var n = random.nextInt(5000); 84 | changeValue(n, rpc); 85 | pi++; 86 | } 87 | }); 88 | } 89 | 90 | int getRandomPair() { 91 | return random.nextInt(pairCount - 1) + 1; 92 | } 93 | 94 | void changeValue(value, int idx) { 95 | (pairs[idx][2] as TestNodeProvider).getNode('/node').updateValue(value); 96 | } 97 | 98 | createLinks() async { 99 | print("Creating ${pairCount} link pairs."); 100 | while (true) { 101 | await createLinkPair(); 102 | if (pairIndex > pairCount) { 103 | return; 104 | } 105 | } 106 | } 107 | 108 | List pairs = [null]; 109 | int pairIndex = 1; 110 | 111 | PrivateKey key = 112 | new PrivateKey.loadFromString( 113 | '9zaOwGO2iXimn4RXTNndBEpoo32qFDUw72d8mteZP9I BJSgx1t4pVm8VCs4FHYzRvr14BzgCBEm8wJnMVrrlx1u1dnTsPC0MlzAB1LhH2sb6FXnagIuYfpQUJGT_yYtoJM'); 114 | 115 | createLinkPair() async { 116 | TestNodeProvider provider = new TestNodeProvider(); 117 | var linkResp = new HttpClientLink('http://localhost:8080/conn', 'responder-$pairIndex-', key, isRequester: false, isResponder: true, nodeProvider: provider); 118 | 119 | var linkReq = new HttpClientLink('http://localhost:8080/conn', 'requester-$pairIndex-', key, isRequester: true); 120 | linkReq.connect(); 121 | 122 | pairs.add([linkResp, linkReq, provider]); 123 | 124 | var mine = pairIndex; 125 | 126 | changeValue(0, pairIndex); 127 | pairIndex++; 128 | 129 | linkResp.connect().then((_) { 130 | print("Link Pair ${mine} is now ready."); 131 | connectedCount++; 132 | linkReq.requester.subscribe("/conns/responder-$mine/node", (ValueUpdate val) { 133 | }); 134 | }); 135 | } 136 | 137 | int connectedCount = 0; 138 | -------------------------------------------------------------------------------- /lib/src/crypto/dart/isolate.dart: -------------------------------------------------------------------------------- 1 | //FIXME:Dart1.0 2 | //*Dart1-open-block 3 | part of dslink.pk.dart; 4 | 5 | ECPrivateKey _cachedPrivate; 6 | ECPublicKey _cachedPublic; 7 | int _cachedTime = -1; 8 | String cachedPrivateStr; 9 | 10 | List generate(List publicKeyRemote, String oldPriKeyStr) { 11 | ECPoint publicPointRemote = _secp256r1.curve.decodePoint(publicKeyRemote); 12 | ECPrivateKey privateKey; 13 | ECPublicKey publicKey; 14 | int ts = (new DateTime.now()).millisecondsSinceEpoch; 15 | if (cachedPrivateStr == null || 16 | ts - _cachedTime > 60000 || 17 | oldPriKeyStr == cachedPrivateStr || 18 | oldPriKeyStr == '') { 19 | var gen = new ECKeyGenerator(); 20 | var rsapars = new ECKeyGeneratorParameters(_secp256r1); 21 | var params = new ParametersWithRandom(rsapars, 22 | DartCryptoProvider.INSTANCE.random); 23 | gen.init(params); 24 | var pair = gen.generateKeyPair(); 25 | privateKey = pair.privateKey; 26 | publicKey = pair.publicKey; 27 | if (oldPriKeyStr != '') { 28 | _cachedPrivate = pair.privateKey; 29 | _cachedPublic = pair.publicKey; 30 | _cachedTime = ts; 31 | } 32 | } else { 33 | privateKey = _cachedPrivate; 34 | publicKey = _cachedPublic; 35 | } 36 | 37 | var Q2 = publicPointRemote * privateKey.d; 38 | return [ 39 | /*privateKey.d.toByteArray()*/ 40 | bigIntegerToByteArray(privateKey.d), 41 | publicKey.Q.getEncoded(false), 42 | Q2.getEncoded(false) 43 | ]; 44 | } 45 | 46 | void _processECDH(SendPort initialReplyTo) { 47 | var response = new ReceivePort(); 48 | initialReplyTo.send(response.sendPort); 49 | response.listen((msg) { 50 | if (msg is List && msg.length == 2) { 51 | initialReplyTo.send(generate(msg[0] as List, msg[1].toString())); 52 | } 53 | }); 54 | } 55 | 56 | class ECDHIsolate { 57 | static bool get running => _ecdh_isolate != null; 58 | static Isolate _ecdh_isolate; 59 | static start() async { 60 | if (_ecdh_isolate != null) return; 61 | var response = new ReceivePort(); 62 | _ecdh_isolate = await Isolate.spawn(_processECDH, response.sendPort); 63 | response.listen(_processResult); 64 | _checkRequest(); 65 | } 66 | 67 | static SendPort _isolatePort; 68 | static void _processResult(message) { 69 | if (message is SendPort) { 70 | _isolatePort = message; 71 | } else if (message is List) { 72 | if (_waitingReq != null && message.length == 3) { 73 | var d1 = newBigIntegerFromBytes(1, message[0] as List); 74 | var Q1 = _secp256r1.curve.decodePoint(message[1] as List); 75 | var Q2 = _secp256r1.curve.decodePoint(message[2] as List); 76 | var ecdh = new ECDHImpl( 77 | new ECPrivateKey(d1, _secp256r1), new ECPublicKey(Q1, _secp256r1), 78 | Q2); 79 | _waitingReq._completer.complete(ecdh); 80 | _waitingReq = null; 81 | } 82 | } 83 | _checkRequest(); 84 | } 85 | 86 | static ECDHIsolateRequest _waitingReq; 87 | static void _checkRequest() { 88 | if (_waitingReq == null && _requests.length > 0) { 89 | _waitingReq = _requests.removeFirst(); 90 | _isolatePort.send([ 91 | _waitingReq.publicKeyRemote.ecPublicKey.Q.getEncoded(false), 92 | _waitingReq.oldPrivate 93 | ]); 94 | } 95 | } 96 | 97 | static ListQueue _requests = 98 | new ListQueue(); 99 | 100 | /// when oldprivate is '', don't use cache 101 | static Future _sendRequest( 102 | PublicKey publicKeyRemote, String oldprivate) { 103 | var req = new ECDHIsolateRequest(publicKeyRemote, oldprivate); 104 | _requests.add(req); 105 | _checkRequest(); 106 | return req.future; 107 | } 108 | } 109 | 110 | class ECDHIsolateRequest { 111 | PublicKeyImpl publicKeyRemote; 112 | String oldPrivate; 113 | 114 | ECDHIsolateRequest(this.publicKeyRemote, this.oldPrivate); 115 | 116 | Completer _completer = new Completer(); 117 | Future get future => _completer.future; 118 | } 119 | //Dart1-close-block*/ 120 | -------------------------------------------------------------------------------- /lib/src/storage/web_storage.dart: -------------------------------------------------------------------------------- 1 | library dslink.storage.web; 2 | 3 | import '../../responder.dart'; 4 | import '../../common.dart'; 5 | import 'dart:html'; 6 | import '../../utils.dart'; 7 | import 'dart:async'; 8 | 9 | class WebResponderStorage extends ISubscriptionResponderStorage { 10 | Map values = new Map(); 11 | 12 | final String prefix; 13 | 14 | String responderPath; 15 | 16 | WebResponderStorage([this.prefix = 'dsaValue:']); 17 | 18 | ISubscriptionNodeStorage getOrCreateValue(String path) { 19 | if (values.containsKey(path)) { 20 | return values[path]; 21 | } 22 | WebNodeStorage value = new WebNodeStorage(path, prefix, this); 23 | values[path] = value; 24 | return value; 25 | } 26 | 27 | Future> load() async { 28 | List rslt = []; 29 | for (String key in window.localStorage.keys) { 30 | if (key.startsWith(prefix)) { 31 | String path = key.substring(prefix.length); 32 | WebNodeStorage value = new WebNodeStorage(path, prefix, this); 33 | value.load(); 34 | if (value._cachedValue != null) { 35 | values[path] = value; 36 | rslt.add(value); 37 | } 38 | } 39 | } 40 | return new Future>.value(rslt); 41 | } 42 | 43 | void destroyValue(String path) { 44 | if (values.containsKey(path)) { 45 | values[path].destroy(); 46 | values.remove(path); 47 | } 48 | } 49 | 50 | void destroy() { 51 | values.forEach((String path, WebNodeStorage value) { 52 | value.destroy(); 53 | }); 54 | values.clear(); 55 | } 56 | } 57 | 58 | class WebNodeStorage extends ISubscriptionNodeStorage { 59 | String storePath; 60 | 61 | WebNodeStorage(String path, String prefix, WebResponderStorage storage) 62 | : super(path, storage) { 63 | storePath = '$prefix$path'; 64 | } 65 | 66 | /// add data to List of values 67 | void addValue(ValueUpdate value) { 68 | qos = 3; 69 | value.storedData = '${DsJson.encode(value.toMap())}\n'; 70 | if (window.localStorage.containsKey(storePath)) { 71 | window.localStorage[storePath] = 72 | window.localStorage[storePath] + value.storedData; 73 | } else { 74 | window.localStorage[storePath] = value.storedData; 75 | } 76 | } 77 | 78 | void setValue(Iterable removes, ValueUpdate newValue) { 79 | qos = 2; 80 | newValue.storedData = ' ${DsJson.encode(newValue.toMap())}\n'; 81 | // add a space when qos = 2 82 | window.localStorage[storePath] = newValue.storedData; 83 | } 84 | 85 | void removeValue(ValueUpdate value) { 86 | // do nothing, it's done in valueRemoved 87 | } 88 | 89 | void valueRemoved(Iterable updates) { 90 | window.localStorage[storePath] = updates.map((v) => v.storedData).join(); 91 | } 92 | 93 | void clear(int qos) { 94 | if (qos == 3) { 95 | window.localStorage[storePath] = ''; 96 | } else { 97 | window.localStorage[storePath] = ' '; 98 | } 99 | } 100 | 101 | void destroy() { 102 | window.localStorage.remove(storePath); 103 | } 104 | 105 | List _cachedValue; 106 | 107 | void load() { 108 | String str = window.localStorage[storePath]; 109 | if (str == null) { 110 | return; 111 | } 112 | List strs = str.split('\n'); 113 | if (str.startsWith(' ')) { 114 | // where there is space, it's qos 2 115 | qos = 2; 116 | } else { 117 | qos = 3; 118 | } 119 | List rslt = new List(); 120 | for (String s in strs) { 121 | if (s.length < 18) { 122 | // a valid data is always 18 bytes or more 123 | continue; 124 | } 125 | 126 | try { 127 | Map m = DsJson.decode(s); 128 | ValueUpdate value = new ValueUpdate(m['value'], ts: m['ts'], meta: m); 129 | rslt.add(value); 130 | } catch (err) {} 131 | } 132 | _cachedValue = rslt; 133 | } 134 | 135 | List getLoadedValues() { 136 | return _cachedValue; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/src/utils/stream_controller.dart: -------------------------------------------------------------------------------- 1 | part of dslink.utils; 2 | 3 | class BroadcastStreamController implements StreamController { 4 | StreamController _controller; 5 | CachedStreamWrapper _stream; 6 | Stream get stream => _stream; 7 | 8 | Function onStartListen; 9 | Function onAllCancel; 10 | 11 | BroadcastStreamController([ 12 | void onStartListen(), 13 | void onAllCancel(), 14 | void onListen(callback(T value)), 15 | bool sync = false 16 | ]) { 17 | _controller = new StreamController(sync: sync); 18 | _stream = new CachedStreamWrapper( 19 | _controller.stream 20 | .asBroadcastStream(onListen: _onListen, onCancel: _onCancel), 21 | onListen); 22 | this.onStartListen = onStartListen; 23 | this.onAllCancel = onAllCancel; 24 | } 25 | 26 | /// whether there is listener or not 27 | bool _listening = false; 28 | 29 | /// whether _onStartListen is called 30 | bool _listenState = false; 31 | void _onListen(StreamSubscription subscription) { 32 | if (!_listenState) { 33 | if (onStartListen != null) { 34 | onStartListen(); 35 | } 36 | _listenState = true; 37 | } 38 | _listening = true; 39 | } 40 | 41 | void _onCancel(StreamSubscription subscription) { 42 | _listening = false; 43 | if (onAllCancel != null) { 44 | if (!_delayedCheckCanceling) { 45 | _delayedCheckCanceling = true; 46 | DsTimer.callLater(delayedCheckCancel); 47 | } 48 | } else { 49 | _listenState = false; 50 | } 51 | } 52 | 53 | bool _delayedCheckCanceling = false; 54 | void delayedCheckCancel() { 55 | _delayedCheckCanceling = false; 56 | if (!_listening && _listenState) { 57 | onAllCancel(); 58 | _listenState = false; 59 | } 60 | } 61 | 62 | void add(T t) { 63 | _controller.add(t); 64 | _stream.lastValue = t; 65 | } 66 | 67 | void addError(Object error, [StackTrace stackTrace]) { 68 | _controller.addError(error, stackTrace); 69 | } 70 | 71 | Future addStream(Stream source, {bool cancelOnError: true}) { 72 | return _controller.addStream(source, cancelOnError: cancelOnError); 73 | } 74 | 75 | Future close() { 76 | return _controller.close(); 77 | } 78 | 79 | Future get done => _controller.done; 80 | 81 | bool get hasListener => _controller.hasListener; 82 | 83 | bool get isClosed => _controller.isClosed; 84 | 85 | bool get isPaused => _controller.isPaused; 86 | 87 | StreamSink get sink => _controller.sink; 88 | 89 | void set onCancel(onCancelHandler()) { 90 | throw('BroadcastStreamController.onCancel not implemented'); 91 | } 92 | 93 | void set onListen(void onListenHandler()) { 94 | throw('BroadcastStreamController.onListen not implemented'); 95 | } 96 | 97 | void set onPause(void onPauseHandler()) { 98 | throw('BroadcastStreamController.onPause not implemented'); 99 | } 100 | 101 | void set onResume(void onResumeHandler()) { 102 | throw('BroadcastStreamController.onResume not implemented'); 103 | } 104 | 105 | ControllerCancelCallback get onCancel => null; 106 | ControllerCallback get onListen => null; 107 | ControllerCallback get onPause => null; 108 | ControllerCallback get onResume => null; 109 | } 110 | 111 | class CachedStreamWrapper extends Stream { 112 | T lastValue; 113 | 114 | final Stream _stream; 115 | final Function _onListen; 116 | CachedStreamWrapper(this._stream, this._onListen); 117 | 118 | @override 119 | Stream asBroadcastStream( 120 | {void onListen(StreamSubscription subscription), 121 | void onCancel(StreamSubscription subscription)}) { 122 | return this; 123 | } 124 | 125 | bool get isBroadcast => true; 126 | 127 | @override 128 | StreamSubscription listen( 129 | void onData(T event), { 130 | Function onError, 131 | void onDone(), 132 | bool cancelOnError}) { 133 | if (_onListen != null) { 134 | _onListen(onData); 135 | } 136 | 137 | return _stream.listen( 138 | onData, 139 | onError: onError, 140 | onDone: onDone, 141 | cancelOnError: cancelOnError 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tool/experiment/test_link_provider.dart: -------------------------------------------------------------------------------- 1 | import "package:dslink/dslink.dart"; 2 | import "package:dslink/utils.dart" show ByteDataUtil, DsTimer; 3 | 4 | import "dart:async"; 5 | import "dart:math" as Math; 6 | 7 | LinkProvider link; 8 | int lastNum; 9 | SimpleNode addNode; 10 | SimpleNode rootNode; 11 | 12 | class AddNodeAction extends SimpleNode { 13 | AddNodeAction(String path) : super(path); 14 | 15 | Object onInvoke(Map params) { 16 | addNode.configs[r'$lastNum'] = ++lastNum; 17 | 18 | String nodeName = '/node%2F_$lastNum'; 19 | link.addNode(nodeName, { 20 | r'$type': 'bool[disable,enable]', 21 | r'$is': 'rng', 22 | '@unit': 'hit', 23 | '?value': '123.456', //ByteDataUtil.fromList([1,2,3,1,2,3]), 24 | 'remove': { 25 | // an action to delete the node 26 | r'$is': 'removeSelfAction', 27 | r'$invokable': 'write', 28 | }, 29 | r'$writable': 'write', 30 | r'$placeholder': 'abcc', 31 | }); 32 | link.save(); // save json 33 | 34 | AsyncTableResult tableRslt = new AsyncTableResult(); 35 | void closed(InvokeResponse resp) { 36 | print('closed'); 37 | } 38 | int colCount = 1; 39 | int tcount = 0; 40 | tableRslt.onClose = closed; 41 | tableRslt.columns = [{'name': 'a'}]; 42 | new Timer.periodic(new Duration(milliseconds: 50), (Timer t) { 43 | if (tcount++ > 5) { 44 | tableRslt.close(); 45 | t.cancel(); 46 | return; 47 | } 48 | tableRslt.update([[1], [2]], StreamStatus.initialize, {'a': 'abc'}); 49 | }); 50 | return tableRslt; //new SimpleTableResult([['0'], ['1']], [{"name":"name"}]); 51 | } 52 | } 53 | 54 | class RemoveSelfAction extends SimpleNode { 55 | RemoveSelfAction(String path) : super(path); 56 | 57 | Object onInvoke(Map params) { 58 | List p = path.split('/') 59 | ..removeLast(); 60 | String parentPath = p.join('/'); 61 | link.removeNode(parentPath); 62 | link.save(); 63 | return null; 64 | } 65 | } 66 | 67 | class RngNode extends SimpleNode { 68 | RngNode(String path) : super(path); 69 | 70 | static Math.Random rng = new Math.Random(); 71 | 72 | @override 73 | void onCreated() { 74 | //updateValue(rng.nextDouble()); 75 | } 76 | 77 | void updateRng() { 78 | if (!removed) { 79 | updateValue(ByteDataUtil.fromList([1, 2, 3, 1, 2, 3])); 80 | DsTimer.timerOnceAfter(updateRng, 1000); 81 | } 82 | } 83 | } 84 | 85 | main(List args) { 86 | var defaultNodes = { 87 | 'defs': { 'profile':{ 88 | 'addNodeAction': { 89 | r'$params': { 90 | "name": { 91 | "type": "string", 92 | "placeholder": 'ccc', 93 | "description": "abcd", 94 | "default": 123 95 | }, 96 | "source": {"type": "string", 'editor': "password"}, 97 | "destination": {"type": "string"}, 98 | "queueSize": {"type": "string"}, 99 | "pem": {"type": "string"}, 100 | "filePrefix": {"type": "bool[disable,enable]"}, 101 | "copyToPath": {"type": "enum[a,b,c]"} 102 | }, 103 | //r'$columns':[{'name':'name','type':'string'}], 104 | r'$lastNum': 0, 105 | r'$result': 'stream' 106 | }} 107 | }, 108 | 'add': {r'$is': 'addNodeAction', r'$invokable': 'write',} 109 | }; 110 | 111 | var profiles = { 112 | 'addNodeAction': (String path) { 113 | return new AddNodeAction(path); 114 | }, 115 | 'removeSelfAction': (String path) { 116 | return new RemoveSelfAction(path); 117 | }, 118 | 'rng': (String path) { 119 | return new RngNode(path); 120 | } 121 | }; 122 | 123 | link = new LinkProvider( 124 | ['-b', 'localhost:8080/conn', '--log', 'finest'], 'rick-resp-', 125 | defaultNodes: defaultNodes, 126 | profiles: profiles /*, home:'dgSuper'*/, 127 | linkData: {'a': 1}); 128 | if (link.link == null) { 129 | // initialization failed 130 | return; 131 | } 132 | 133 | addNode = link.getNode('/add'); 134 | rootNode = link.getNode('/'); 135 | lastNum = addNode.configs[r'$lastNum']; 136 | 137 | var node = link.provider.getOrCreateNode('/testpoint'); 138 | node.load({ 139 | r'$type': 'number', 140 | "?value": 1 141 | }); 142 | 143 | link.connect(); 144 | } 145 | -------------------------------------------------------------------------------- /example/browser/style.css: -------------------------------------------------------------------------------- 1 | .line { 2 | border-top: 1px; 3 | border-style: solid; 4 | } 5 | 6 | .box { 7 | background: linear-gradient(180deg, #e5e5e5 20%, #d1d1d1 80%); 8 | box-shadow: 0 2px 5px 0 rgba(50, 50, 50, 0.85); 9 | border-radius: 5px; 10 | word-wrap: break-word; 11 | margin: 5px; 12 | } 13 | 14 | .column { 15 | width: 50%; 16 | display: inline-block; 17 | } 18 | 19 | .center { 20 | display: block; 21 | text-align: center; 22 | margin-top: 0; 23 | margin-bottom: 0; 24 | padding: 0; 25 | } 26 | 27 | .left { 28 | float: left; 29 | } 30 | 31 | .right { 32 | float: right; 33 | } 34 | 35 | .middle { 36 | vertical-align: middle; 37 | } 38 | 39 | .container { 40 | display: flex; 41 | flex-direction: row; 42 | flex-wrap: wrap; 43 | } 44 | 45 | .item { 46 | justify-content: center; 47 | } 48 | 49 | .inline-block { 50 | display: inline-block; 51 | } 52 | 53 | .header { 54 | display: block; 55 | } 56 | 57 | .btn { 58 | display: inline-block; 59 | transition-timing-function: ease; 60 | transition-duration: 0.2s; 61 | transition-property: background-color, color, border-color; 62 | background-color: transparent; 63 | padding: 8px; 64 | border-radius: 3px; 65 | border: 1px solid #00AACC; 66 | color: #00AACC; 67 | font-family: "Helvetica Neue", Helvetica, sans-serif; 68 | -webkit-touch-callout: none; 69 | -webkit-user-select: none; 70 | -khtml-user-select: none; 71 | -moz-user-select: none; 72 | -ms-user-select: none; 73 | user-select: none; 74 | } 75 | 76 | .btn:active { 77 | color: white; 78 | border-color: #008EAB; 79 | background-color: #008EAB; 80 | } 81 | 82 | .btn:hover { 83 | background-color: #00AACC; 84 | color: white; 85 | border-color: #00AACC; 86 | cursor: pointer; 87 | } 88 | 89 | .btn-danger { 90 | display: inline-block; 91 | transition-timing-function: ease; 92 | transition-duration: 0.2s; 93 | transition-property: background-color, color, border-color; 94 | background-color: transparent; 95 | padding: 8px; 96 | border-radius: 3px; 97 | border: 1px solid #d9534f; 98 | color: #d9534f; 99 | font-family: "Helvetica Neue", Helvetica, sans-serif; 100 | -webkit-touch-callout: none; 101 | -webkit-user-select: none; 102 | -khtml-user-select: none; 103 | -moz-user-select: none; 104 | -ms-user-select: none; 105 | user-select: none; 106 | } 107 | 108 | .btn-danger:active { 109 | color: white; 110 | border-color: #008EAB; 111 | background-color: #008EAB; 112 | } 113 | 114 | .btn-danger:hover { 115 | background-color: #d9534f; 116 | color: white; 117 | border-color: #d9534f; 118 | cursor: pointer; 119 | } 120 | 121 | .btn-warning { 122 | display: inline-block; 123 | transition-timing-function: ease; 124 | transition-duration: 0.2s; 125 | transition-property: background-color, color, border-color; 126 | background-color: transparent; 127 | padding: 8px; 128 | border-radius: 3px; 129 | border: 1px solid #f0ad4e; 130 | color: #f0ad4e; 131 | font-family: "Helvetica Neue", Helvetica, sans-serif; 132 | -webkit-touch-callout: none; 133 | -webkit-user-select: none; 134 | -khtml-user-select: none; 135 | -moz-user-select: none; 136 | -ms-user-select: none; 137 | user-select: none; 138 | } 139 | 140 | .btn-warning:active { 141 | color: white; 142 | border-color: #008EAB; 143 | background-color: #008EAB; 144 | } 145 | 146 | .btn-warning:hover { 147 | background-color: #f0ad4e; 148 | color: white; 149 | border-color: #f0ad4e; 150 | cursor: pointer; 151 | } 152 | 153 | .input-box { 154 | display: inline-block; 155 | width: 256px; 156 | height: 20px; 157 | padding: 6px 12px; 158 | font-size: 14px; 159 | line-height: 1.42857143; 160 | color: #555; 161 | background: #fff none; 162 | border: 1px solid #ccc; 163 | border-radius: 4px; 164 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 165 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 166 | -webkit-transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s; 167 | -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 168 | transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 169 | } 170 | 171 | pre { 172 | display: block; 173 | padding: 10px; 174 | margin: 0 0 10px; 175 | font-size: 13px; 176 | line-height: 1.42857143; 177 | word-break: break-all; 178 | word-wrap: break-word; 179 | color: #333333; 180 | background-color: #f5f5f5; 181 | border: 1px solid #cccccc; 182 | border-radius: 4px; 183 | } 184 | 185 | .status-minor { 186 | color: yellow; 187 | } 188 | 189 | .status-good { 190 | color: green; 191 | } 192 | 193 | .status-major { 194 | color: red; 195 | } 196 | -------------------------------------------------------------------------------- /tool/experiment/test_concurrent2.dart: -------------------------------------------------------------------------------- 1 | /** 2 | * this is a modified version of the original test_concurrent.dart 3 | * it gives the broker more time to handler handshake and not blocking network traffic 4 | */ 5 | import "dart:math"; 6 | 7 | import "package:dslink/dslink.dart"; 8 | import "package:dslink/utils.dart"; 9 | import "package:logging/logging.dart"; 10 | 11 | import "package:args/args.dart"; 12 | import 'dart:async'; 13 | 14 | class TestNodeProvider extends NodeProvider { 15 | TestNode onlyNode; 16 | TestNodeProvider(){ 17 | onlyNode = new TestNode('/', this); 18 | } 19 | 20 | LocalNode getNode(String path) { 21 | return onlyNode; 22 | } 23 | IPermissionManager permissions = new DummyPermissionManager(); 24 | Responder createResponder(String dsId, String sessionId) { 25 | return new Responder(this, dsId); 26 | } 27 | LocalNode getOrCreateNode(String path, [bool addToTree = true]) { 28 | return onlyNode; 29 | } 30 | } 31 | 32 | class TestNode extends LocalNodeImpl { 33 | NodeProvider provider; 34 | TestNode(String path, this.provider) : super(path) { 35 | configs[r'$is'] = 'node'; 36 | configs[r'$test'] = 'hello world'; 37 | configs[r'$type'] = 'number'; 38 | children['node'] = this; 39 | } 40 | } 41 | 42 | int pairCount = 1000; 43 | String broker = "http://localhost:8080/conn"; 44 | Stopwatch stopwatch; 45 | Random random = new Random(); 46 | 47 | String prefix = ''; 48 | main(List args) async { 49 | var argp = new ArgParser(); 50 | argp.addOption("pairs", abbr: "p", help: "Number of Link Pairs", defaultsTo: "1000", valueHelp: "pairs"); 51 | argp.addOption("broker", abbr: "b", help: "Broker Url", defaultsTo: "http://localhost:8080/conn", valueHelp: "broker"); 52 | argp.addOption("prefix", abbr: "f", help: "Prefix on DsLink Id", defaultsTo: "", valueHelp: "previx"); 53 | 54 | var opts = argp.parse(args); 55 | 56 | try { 57 | pairCount = int.parse(opts["pairs"]); 58 | } catch (e) { 59 | print("Invalid Number of Pairs."); 60 | return; 61 | } 62 | try { 63 | broker = opts["broker"]; 64 | } catch (e) { 65 | print("Invalid broker"); 66 | return; 67 | } 68 | prefix = opts["prefix"]; 69 | 70 | logger.level = Level.WARNING; 71 | 72 | stopwatch = new Stopwatch(); 73 | 74 | createLinks(); 75 | createLinks(); 76 | } 77 | 78 | bool onCreatedRun = false; 79 | 80 | void onCreated() { 81 | if (onCreatedRun) return; 82 | onCreatedRun = true; 83 | 84 | int mm = 0; 85 | bool ready = false; 86 | 87 | Scheduler.every(Interval.TWO_SECONDS, () { 88 | if (connectedCount != pairCount) { 89 | mm++; 90 | 91 | if (mm == 2) { 92 | print("${connectedCount} of ${pairCount} link pairs are ready."); 93 | mm = 0; 94 | } 95 | 96 | return; 97 | } 98 | 99 | if (!ready) { 100 | print("All link pairs are now ready. Subscribing requesters to values and starting value updates."); 101 | ready = true; 102 | } 103 | 104 | var pi = 1; 105 | 106 | while (pi <= 5) { 107 | var rpc = getRandomPair(); 108 | var n = random.nextInt(5000); 109 | changeValue(n, rpc); 110 | pi++; 111 | } 112 | }); 113 | } 114 | 115 | int getRandomPair() { 116 | return random.nextInt(pairCount - 1) + 1; 117 | } 118 | 119 | void changeValue(value, int idx) { 120 | (pairs[idx][2] as TestNodeProvider).getNode('/node').updateValue(value); 121 | } 122 | 123 | createLinks() async { 124 | print("Creating ${pairCount} link pairs."); 125 | while (true) { 126 | await createLinkPair(); 127 | if (pairIndex > pairCount) { 128 | onCreated(); 129 | return; 130 | } 131 | } 132 | } 133 | 134 | List pairs = [null]; 135 | int pairIndex = 1; 136 | 137 | PrivateKey key = 138 | new PrivateKey.loadFromString( 139 | '9zaOwGO2iXimn4RXTNndBEpoo32qFDUw72d8mteZP9I BJSgx1t4pVm8VCs4FHYzRvr14BzgCBEm8wJnMVrrlx1u1dnTsPC0MlzAB1LhH2sb6FXnagIuYfpQUJGT_yYtoJM'); 140 | 141 | createLinkPair() async { 142 | TestNodeProvider provider = new TestNodeProvider(); 143 | var linkResp = new HttpClientLink(broker, '$prefix-resp-$pairIndex-', key, isRequester: false, isResponder: true, nodeProvider: provider); 144 | 145 | var linkReq = new HttpClientLink(broker, '$prefix-req--$pairIndex-', key, isRequester: true); 146 | linkReq.connect(); 147 | 148 | pairs.add([linkResp, linkReq, provider]); 149 | 150 | var mine = pairIndex; 151 | 152 | changeValue(0, pairIndex); 153 | pairIndex++; 154 | 155 | await linkResp.connect(); 156 | print("Link Pair ${mine} is now ready."); 157 | connectedCount++; 158 | linkReq.requester.subscribe("/conns/$prefix-resp-$mine/node", (ValueUpdate val) { 159 | }); 160 | } 161 | 162 | int connectedCount = 0; 163 | -------------------------------------------------------------------------------- /lib/src/query/query_subscribe_command.dart: -------------------------------------------------------------------------------- 1 | part of dslink.query; 2 | 3 | class _QuerySubscription { 4 | final QueryCommandSubscribe command; 5 | final LocalNode node; 6 | RespSubscribeListener listener; 7 | 8 | /// if removed, the subscription will be destroyed next frame 9 | bool removed = false; 10 | bool justAdded = true; 11 | _QuerySubscription(this.command, this.node) { 12 | if (node.valueReady) { 13 | valueCallback(node.lastValueUpdate); 14 | } 15 | listener = node.subscribe(valueCallback); 16 | } 17 | 18 | ValueUpdate lastUpdate; 19 | void valueCallback(ValueUpdate value) { 20 | lastUpdate = value; 21 | command.updateRow(getRowData()); 22 | } 23 | 24 | List getRowData() { 25 | // TODO: make sure node still in tree 26 | // because list remove node update could come one frame later 27 | if (!removed && lastUpdate != null) { 28 | if (justAdded) { 29 | justAdded = false; 30 | return [node.path, '+', lastUpdate.value, lastUpdate.ts]; 31 | } else { 32 | return [node.path, '', lastUpdate.value, lastUpdate.ts]; 33 | } 34 | } 35 | return null; 36 | } 37 | 38 | List getRowDataForNewResponse() { 39 | if (!removed && !justAdded && lastUpdate != null) { 40 | return [node.path, '+', lastUpdate.value, lastUpdate.ts]; 41 | } 42 | return null; 43 | } 44 | 45 | void destroy() { 46 | listener.cancel(); 47 | } 48 | } 49 | 50 | class QueryCommandSubscribe extends BrokerQueryCommand { 51 | static List columns = [ 52 | {'name': 'path', 'type': 'string'}, 53 | {'name': 'change', 'type': 'string'}, 54 | {'name': 'value', 'type': 'string'}, 55 | {'name': 'ts', 'type': 'string'}, 56 | ]; 57 | 58 | QueryCommandSubscribe(BrokerQueryManager manager) : super(manager); 59 | 60 | void addResponse(InvokeResponse response) { 61 | if (_pending) { 62 | // send all pending update to existing responses 63 | // so the new response can continue on a clear state 64 | _doUpdate(); 65 | } 66 | super.addResponse(response); 67 | List rows = []; 68 | subscriptions.forEach((String path, _QuerySubscription sub) { 69 | List data = sub.getRowDataForNewResponse(); 70 | if (data != null) { 71 | rows.add(data); 72 | } 73 | }); 74 | response.updateStream(rows, columns: columns); 75 | } 76 | 77 | Set _changes = new Set(); 78 | Map subscriptions = 79 | new Map(); 80 | 81 | bool _pending = false; 82 | void updatePath(String path) { 83 | _changes.add(path); 84 | if (!_pending) { 85 | _pending = true; 86 | DsTimer.callLater(_doUpdate); 87 | } 88 | } 89 | 90 | List _pendingRows = []; 91 | void updateRow(List row) { 92 | _pendingRows.add(row); 93 | if (!_pending) { 94 | _pending = true; 95 | DsTimer.callLater(_doUpdate); 96 | } 97 | } 98 | 99 | void _doUpdate() { 100 | if (!_pending) { 101 | return; 102 | } 103 | _pending = false; 104 | List rows = _pendingRows; 105 | _pendingRows = []; 106 | for (String path in _changes) { 107 | _QuerySubscription sub = subscriptions[path]; 108 | if (sub != null) { 109 | if (sub.removed) { 110 | if (!sub.justAdded) { 111 | rows.add([path, '-', null, ValueUpdate.getTs()]); 112 | } 113 | subscriptions.remove(path); 114 | sub.destroy(); 115 | } else { 116 | List data = sub.getRowData(); 117 | if (data != null) { 118 | rows.add(data); 119 | } 120 | } 121 | } 122 | } 123 | _changes.clear(); 124 | for (var resp in responses) { 125 | resp.updateStream(rows); 126 | } 127 | } 128 | 129 | // must be list result 130 | // new matched node [node,'+'] 131 | // remove matched node [node, '-'] 132 | void updateFromBase(List updates) { 133 | for (List data in updates) { 134 | if (data[0] is LocalNode) { 135 | LocalNode node = data[0]; 136 | if (data[1] == '+') { 137 | if (!subscriptions.containsKey(node.path)) { 138 | subscriptions[node.path] = new _QuerySubscription(this, node); 139 | } else { 140 | subscriptions[node.path].removed = false; 141 | } 142 | } else if (data[1] == '-') { 143 | if (subscriptions.containsKey(node.path)) { 144 | subscriptions[node.path].removed = true; 145 | updatePath(node.path); 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | String toString() { 153 | return r'subscribe $value'; 154 | } 155 | 156 | void destroy() { 157 | super.destroy(); 158 | subscriptions.forEach((String key, _QuerySubscription sub) { 159 | sub.destroy(); 160 | }); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lib/src/historian/get_history.dart: -------------------------------------------------------------------------------- 1 | part of dslink.historian; 2 | 3 | class GetHistoryNode extends SimpleNode { 4 | GetHistoryNode(String path) : super(path, _link.provider) { 5 | configs[r"$is"] = "getHistory"; 6 | configs[r"$name"] = "Get History"; 7 | configs[r"$invokable"] = "read"; 8 | configs[r"$params"] = [ 9 | { 10 | "name": "Timerange", 11 | "type": "string", 12 | "editor": "daterange" 13 | }, 14 | { 15 | "name": "Interval", 16 | "type": "enum", 17 | "editor": buildEnumType([ 18 | "default", 19 | "none", 20 | "1Y", 21 | "3N", 22 | "1N", 23 | "1W", 24 | "1D", 25 | "12H", 26 | "6H", 27 | "4H", 28 | "3H", 29 | "2H", 30 | "1H", 31 | "30M", 32 | "15M", 33 | "10M", 34 | "5M", 35 | "1M", 36 | "30S", 37 | "15S", 38 | "10S", 39 | "5S", 40 | "1S" 41 | ]), 42 | "default": "default" 43 | }, 44 | { 45 | "name": "Rollup", 46 | "type": buildEnumType([ 47 | "none", 48 | "avg", 49 | "min", 50 | "max", 51 | "sum", 52 | "first", 53 | "last", 54 | "count" 55 | ]) 56 | }, 57 | { 58 | "name": "Real Time", 59 | "type": "bool", 60 | "default": false 61 | }, 62 | { 63 | "name": "Batch Size", 64 | "type": "number", 65 | "default": 0 66 | } 67 | ]; 68 | 69 | configs[r"$columns"] = [ 70 | { 71 | "name": "timestamp", 72 | "type": "time" 73 | }, 74 | { 75 | "name": "value", 76 | "type": "dynamic" 77 | } 78 | ]; 79 | 80 | configs[r"$result"] = "stream"; 81 | } 82 | 83 | @override 84 | onInvoke(Map params) async* { 85 | String range = params["Timerange"]; 86 | String rollupName = params["Rollup"]; 87 | RollupFactory rollupFactory = _rollups[rollupName]; 88 | Rollup rollup = rollupFactory == null ? null : rollupFactory(); 89 | Duration interval = new Duration( 90 | milliseconds: parseInterval(params["Interval"])); 91 | num batchSize = params["Batch Size"]; 92 | 93 | if (batchSize == null) { 94 | batchSize = 0; 95 | } 96 | 97 | int batchCount = batchSize.toInt(); 98 | 99 | TimeRange tr = parseTimeRange(range); 100 | if (params["Real Time"] == true) { 101 | tr = new TimeRange(tr.start, null); 102 | } 103 | 104 | try { 105 | Stream pairs = await calculateHistory( 106 | tr, 107 | interval, 108 | rollup 109 | ); 110 | 111 | if (params["Real Time"] == true) { 112 | await for (ValuePair pair in pairs) { 113 | yield [pair.toRow()]; 114 | } 115 | } else { 116 | int count = 0; 117 | List> buffer = []; 118 | 119 | await for (ValuePair row in pairs) { 120 | count++; 121 | buffer.add(row.toRow()); 122 | if (count != 0 && count == batchCount) { 123 | yield buffer; 124 | buffer = []; 125 | count = 0; 126 | } 127 | } 128 | 129 | if (buffer.isNotEmpty) { 130 | yield buffer; 131 | buffer.length = 0; 132 | } 133 | } 134 | } catch (e) { 135 | rethrow; 136 | } 137 | } 138 | 139 | Stream fetchHistoryData(TimeRange range) { 140 | var p = new Path(path); 141 | var mn = p.parent; 142 | WatchPathNode pn = _link[mn.path]; 143 | 144 | return pn.fetchHistory(range); 145 | } 146 | 147 | Stream calculateHistory(TimeRange range, 148 | Duration interval, 149 | Rollup rollup) async* { 150 | if (interval.inMilliseconds <= 0) { 151 | yield* fetchHistoryData(range); 152 | return; 153 | } 154 | 155 | int lastTimestamp = -1; 156 | int totalTime = 0; 157 | 158 | ValuePair result; 159 | 160 | await for (ValuePair pair in fetchHistoryData(range)) { 161 | rollup.add(pair.value); 162 | if (lastTimestamp != -1) { 163 | totalTime += pair.time.millisecondsSinceEpoch - lastTimestamp; 164 | } 165 | lastTimestamp = pair.time.millisecondsSinceEpoch; 166 | if (totalTime >= interval.inMilliseconds) { 167 | totalTime = 0; 168 | result = new ValuePair( 169 | new DateTime.fromMillisecondsSinceEpoch( 170 | lastTimestamp 171 | ).toIso8601String(), 172 | rollup.value 173 | ); 174 | yield result; 175 | result = null; 176 | rollup.reset(); 177 | } 178 | } 179 | 180 | if (result != null) { 181 | yield result; 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /lib/src/utils/codec.dart: -------------------------------------------------------------------------------- 1 | part of dslink.utils; 2 | 3 | typedef Object _Encoder(Object input); 4 | typedef Object _Reviver(String key, Object input); 5 | 6 | class BinaryData { 7 | /// used when only partial data is received 8 | /// don"t merge them before it's finished 9 | List mergingList; 10 | 11 | ByteData bytes; 12 | 13 | BinaryData(ByteData bytes) { 14 | this.bytes = bytes; 15 | } 16 | 17 | BinaryData.fromList(List list) { 18 | bytes = ByteDataUtil.fromList(list); 19 | } 20 | } 21 | 22 | abstract class DsCodec { 23 | static final Map _codecs = { 24 | "json": DsJson.instance, 25 | "msgpack": DsMsgPackCodecImpl.instance 26 | }; 27 | 28 | static final DsCodec defaultCodec = DsJson.instance; 29 | 30 | static void register(String name, DsCodec codec) { 31 | if (name != null && codec != null) { 32 | _codecs[name] = codec; 33 | } 34 | } 35 | 36 | static DsCodec getCodec(String name) { 37 | DsCodec rslt = _codecs[name]; 38 | if (rslt == null) { 39 | return defaultCodec; 40 | } 41 | return rslt; 42 | } 43 | 44 | Object _blankData; 45 | 46 | Object get blankData { 47 | if (_blankData == null) { 48 | _blankData = encodeFrame({}); 49 | } 50 | return _blankData; 51 | } 52 | 53 | /// output String or List 54 | Object encodeFrame(Map val); 55 | 56 | /// input can be String or List 57 | Map decodeStringFrame(String input); 58 | 59 | Map decodeBinaryFrame(List input); 60 | } 61 | 62 | abstract class DsJson { 63 | static DsJsonCodecImpl instance = new DsJsonCodecImpl(); 64 | 65 | static String encode(Object val, {bool pretty: false}) { 66 | return instance.encodeJson(val, pretty: pretty); 67 | } 68 | 69 | static dynamic decode(String str) { 70 | return instance.decodeJson(str); 71 | } 72 | 73 | String encodeJson(Object val, {bool pretty: false}); 74 | 75 | dynamic decodeJson(String str); 76 | } 77 | 78 | class DsJsonCodecImpl extends DsCodec implements DsJson { 79 | static dynamic _safeEncoder(value) { 80 | return null; 81 | } 82 | 83 | JsonEncoder encoder = new JsonEncoder(_safeEncoder); 84 | 85 | JsonDecoder decoder = new JsonDecoder(); 86 | JsonEncoder _prettyEncoder; 87 | 88 | dynamic decodeJson(String str) { 89 | return decoder.convert(str); 90 | } 91 | 92 | String encodeJson(val, {bool pretty: false}) { 93 | JsonEncoder e = encoder; 94 | if (pretty) { 95 | if (_prettyEncoder == null) { 96 | _prettyEncoder = 97 | encoder = new JsonEncoder.withIndent(" ", _safeEncoder); 98 | } 99 | e = _prettyEncoder; 100 | } 101 | return e.convert(val); 102 | } 103 | 104 | JsonDecoder _unsafeDecoder; 105 | 106 | Map decodeBinaryFrame(List bytes) { 107 | return decodeStringFrame(const Utf8Decoder().convert(bytes)); 108 | } 109 | 110 | Map decodeStringFrame(String str) { 111 | if (_reviver == null) { 112 | _reviver = (key, value) { 113 | if (value is String && value.startsWith("\u001Bbytes:")) { 114 | try { 115 | return ByteDataUtil.fromUint8List( 116 | Base64.decode(value.substring(7))); 117 | } catch (err) { 118 | return null; 119 | } 120 | } 121 | return value; 122 | }; 123 | } 124 | 125 | if (_unsafeDecoder == null) { 126 | _unsafeDecoder = new JsonDecoder(_reviver); 127 | } 128 | 129 | var result = _unsafeDecoder.convert(str); 130 | return result; 131 | } 132 | 133 | _Reviver _reviver; 134 | _Encoder _encoder; 135 | 136 | Object encodeFrame(Object val) { 137 | if (_encoder == null) { 138 | _encoder = (value) { 139 | if (value is ByteData) { 140 | return "\u001Bbytes:${Base64.encode( 141 | ByteDataUtil.toUint8List(value))}"; 142 | } 143 | return null; 144 | }; 145 | } 146 | 147 | JsonEncoder c; 148 | 149 | if (_unsafeEncoder == null) { 150 | _unsafeEncoder = new JsonEncoder(_encoder); 151 | } 152 | c = _unsafeEncoder; 153 | 154 | var result = c.convert(val); 155 | return result; 156 | } 157 | 158 | JsonEncoder _unsafeEncoder; 159 | } 160 | 161 | class DsMsgPackCodecImpl extends DsCodec { 162 | static DsMsgPackCodecImpl instance = new DsMsgPackCodecImpl(); 163 | 164 | Map decodeBinaryFrame(List input) { 165 | Uint8List data = ByteDataUtil.list2Uint8List(input); 166 | if (_unpacker == null) { 167 | _unpacker = new Unpacker(data.buffer, data.offsetInBytes); 168 | } else { 169 | _unpacker.reset(data.buffer, 0); 170 | _unpacker.offset = data.offsetInBytes; 171 | } 172 | Object rslt = _unpacker.unpack(); 173 | if (rslt is Map) { 174 | return rslt; 175 | } 176 | _unpacker.data = null; 177 | return {}; 178 | } 179 | 180 | Unpacker _unpacker; 181 | 182 | Map decodeStringFrame(String input) { 183 | // not supported 184 | return {}; 185 | } 186 | 187 | Object encodeFrame(Map val) { 188 | return pack(val); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/src/requester/request/invoke.dart: -------------------------------------------------------------------------------- 1 | part of dslink.requester; 2 | 3 | class RequesterInvokeUpdate extends RequesterUpdate { 4 | List rawColumns; 5 | List columns; 6 | List updates; 7 | Map meta; 8 | 9 | RequesterInvokeUpdate(this.updates, this.rawColumns, this.columns, 10 | String streamStatus, 11 | {this.meta, error}) 12 | : super(streamStatus, error); 13 | 14 | List _rows; 15 | 16 | List get rows { 17 | int colLen = -1; 18 | if (columns != null) { 19 | colLen = columns.length; 20 | } 21 | if (_rows == null) { 22 | _rows = []; 23 | if (updates == null) { 24 | return _rows; 25 | } 26 | for (Object obj in updates) { 27 | List row; 28 | if (obj is List) { 29 | if (obj.length < colLen) { 30 | row = obj.toList(); 31 | for (int i = obj.length; i < colLen; ++i) { 32 | row.add(columns[i].defaultValue); 33 | } 34 | } else if (obj.length > colLen) { 35 | if (colLen == -1) { 36 | // when column is unknown, just return all values 37 | row = obj.toList(); 38 | } else { 39 | row = obj.sublist(0, colLen); 40 | } 41 | } else { 42 | row = obj; 43 | } 44 | } else if (obj is Map) { 45 | row = []; 46 | if (columns == null) { 47 | Map map = obj; 48 | List keys = map.keys.map((k) => k.toString()).toList(); 49 | columns = keys.map((x) => new TableColumn(x, "dynamic")).toList(); 50 | } 51 | 52 | if (columns != null) { 53 | for (TableColumn column in columns) { 54 | if (obj.containsKey(column.name)) { 55 | row.add(obj[column.name]); 56 | } else { 57 | row.add(column.defaultValue); 58 | } 59 | } 60 | } 61 | } 62 | _rows.add(row); 63 | } 64 | } 65 | return _rows; 66 | } 67 | } 68 | 69 | class InvokeController implements RequestUpdater { 70 | static List getNodeColumns(RemoteNode node) { 71 | Object columns = node.getConfig(r'$columns'); 72 | if (columns is! List && node.profile != null) { 73 | columns = node.profile.getConfig(r'$columns'); 74 | } 75 | if (columns is List) { 76 | return TableColumn.parseColumns(columns); 77 | } 78 | return null; 79 | } 80 | 81 | final RemoteNode node; 82 | final Requester requester; 83 | 84 | StreamController _controller; 85 | Stream _stream; 86 | Request _request; 87 | List _cachedColumns; 88 | 89 | String mode = 'stream'; 90 | String lastStatus = StreamStatus.initialize; 91 | 92 | InvokeController(this.node, this.requester, Map params, 93 | [int maxPermission = Permission.CONFIG, RequestConsumer fetchRawReq]) { 94 | _controller = new StreamController(); 95 | _controller.done.then(_onUnsubscribe); 96 | _stream = _controller.stream; 97 | var reqMap = { 98 | 'method': 'invoke', 99 | 'path': node.remotePath, 100 | 'params': params 101 | }; 102 | 103 | if (maxPermission != Permission.CONFIG) { 104 | reqMap['permit'] = Permission.names[maxPermission]; 105 | } 106 | // TODO: update node before invoke to load columns 107 | // if(!node.isUpdated()) { 108 | // node._list().listen(_onNodeUpdate) 109 | // } else { 110 | 111 | _request = requester._sendRequest(reqMap, this); 112 | 113 | if (fetchRawReq != null) { 114 | fetchRawReq(_request); 115 | } 116 | // } 117 | } 118 | 119 | void _onUnsubscribe(obj) { 120 | if (_request != null && _request.streamStatus != StreamStatus.closed) { 121 | _request.close(); 122 | } 123 | } 124 | 125 | void onUpdate(String streamStatus, List updates, List columns, Map meta, 126 | DSError error) { 127 | if (meta != null && meta['mode'] is String) { 128 | mode = meta['mode']; 129 | } 130 | // TODO: implement error 131 | if (columns != null) { 132 | if (_cachedColumns == null || mode == 'refresh') { 133 | _cachedColumns = TableColumn.parseColumns(columns); 134 | } else { 135 | _cachedColumns.addAll(TableColumn.parseColumns(columns)); 136 | } 137 | } else if (_cachedColumns == null) { 138 | _cachedColumns = getNodeColumns(node); 139 | } 140 | 141 | if (error != null) { 142 | streamStatus = StreamStatus.closed; 143 | _controller.add( 144 | new RequesterInvokeUpdate( 145 | null, null, null, streamStatus, error: error, meta: meta)); 146 | } else if (updates != null || meta != null || streamStatus != lastStatus) { 147 | _controller.add(new RequesterInvokeUpdate( 148 | updates, columns, _cachedColumns, streamStatus, meta: meta)); 149 | } 150 | lastStatus = streamStatus; 151 | if (streamStatus == StreamStatus.closed) { 152 | _controller.close(); 153 | } 154 | } 155 | 156 | void onDisconnect() {} 157 | 158 | void onReconnect() {} 159 | } 160 | --------------------------------------------------------------------------------