From bcb8b25688e21eebdaff270572c2d78c1a53286d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:03:42 +0000 Subject: [PATCH] deploy: 4fdfee34f5fab703534c2d7c23f8d0e4e07d2095 --- 404.html | 4 ++-- assets/js/74876495.6d8d7f76.js | 1 - assets/js/74876495.cd08c5dc.js | 1 + .../{runtime~main.cbfafcd9.js => runtime~main.fc78f968.js} | 2 +- blog.html | 4 ++-- blog/archive.html | 4 ++-- blog/first-blog-post.html | 4 ++-- blog/long-blog-post.html | 4 ++-- blog/mdx-blog-post.html | 4 ++-- blog/tags.html | 4 ++-- blog/tags/docusaurus.html | 4 ++-- blog/tags/facebook.html | 4 ++-- blog/tags/hello.html | 4 ++-- blog/tags/hola.html | 4 ++-- blog/welcome.html | 4 ++-- docs/category/examples.html | 4 ++-- docs/category/guides.html | 4 ++-- docs/category/installation.html | 4 ++-- docs/category/overview.html | 4 ++-- docs/category/raspberry-pi.html | 4 ++-- docs/category/unity-visualization.html | 4 ++-- docs/examples/ball-example.html | 4 ++-- docs/examples/raspberry-example.html | 4 ++-- docs/examples/raspberry-example/sending-data.html | 4 ++-- docs/examples/string-example.html | 4 ++-- docs/guides/dt-schema-creation.html | 4 ++-- docs/guides/type-creation.html | 4 ++-- docs/guides/unity/creating-unity-build.html | 4 ++-- docs/guides/unity/using-grafana-plugin.html | 4 ++-- docs/installation/manual.html | 4 ++-- docs/installation/requirements.html | 4 ++-- docs/installation/using-helm.html | 4 ++-- docs/overview/architecture.html | 4 ++-- docs/overview/concepts.html | 4 ++-- docs/overview/purpose.html | 4 ++-- docs/quickstart.html | 6 +++--- index.html | 4 ++-- markdown-page.html | 4 ++-- 38 files changed, 73 insertions(+), 73 deletions(-) delete mode 100644 assets/js/74876495.6d8d7f76.js create mode 100644 assets/js/74876495.cd08c5dc.js rename assets/js/{runtime~main.cbfafcd9.js => runtime~main.fc78f968.js} (98%) diff --git a/404.html b/404.html index 4a976cc..1cf7246 100644 --- a/404.html +++ b/404.html @@ -5,13 +5,13 @@ Page Not Found | OpenTwins - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/assets/js/74876495.6d8d7f76.js b/assets/js/74876495.6d8d7f76.js deleted file mode 100644 index aa0b703..0000000 --- a/assets/js/74876495.6d8d7f76.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5049],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>m});var i=n(7294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function r(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var l=i.createContext({}),c=function(e){var t=i.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},p=function(e){var t=c(e.components);return i.createElement(l.Provider,{value:t},e.children)},d="mdxType",h={inlineCode:"code",wrapper:function(e){var t=e.children;return i.createElement(i.Fragment,{},t)}},u=i.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,l=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),d=c(n),u=a,m=d["".concat(l,".").concat(u)]||d[u]||h[u]||o;return n?i.createElement(m,r(r({ref:t},p),{},{components:n})):i.createElement(m,r({ref:t},p))}));function m(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,r=new Array(o);r[0]=u;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[d]="string"==typeof e?e:a,r[1]=s;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var i=n(7462),a=(n(7294),n(3905));n(4996),n(941);const o={sidebar_position:1},r="Quickstart",s={unversionedId:"quickstart",id:"quickstart",title:"Quickstart",description:"Welcome to OpenTwins, a flexible platform adapted to your needs! Although OpenTwins offers extensive customization options, we understand the importance of simplicity for beginners. Therefore, let's embark on a short journey together, showing you the quickest route to deploy the platform and develop a functional digital twin.",source:"@site/docs/quickstart.mdx",sourceDirName:".",slug:"/quickstart",permalink:"/opentwins/docs/quickstart",draft:!1,editUrl:"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/quickstart.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",next:{title:"Overview",permalink:"/opentwins/docs/category/overview"}},l={},c=[{value:"Installation",id:"installation",level:2},{value:"Prerequisites",id:"prerequisites",level:3},{value:"Deploy",id:"deploy",level:3},{value:"Configuration",id:"configuration",level:3},{value:"Create your first digital twin",id:"create-your-first-digital-twin",level:2},{value:"Design",id:"design",level:3},{value:"Definition",id:"definition",level:3},{value:"Create Car type",id:"create-car-type",level:4},{value:"Create Wheel type",id:"create-wheel-type",level:4},{value:"Create the digital twins",id:"create-the-digital-twins",level:4},{value:"Connection",id:"connection",level:3},{value:"Visualization",id:"visualization",level:3}],p={toc:c},d="wrapper";function h(e){let{components:t,...o}=e;return(0,a.kt)(d,(0,i.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"quickstart"},"Quickstart"),(0,a.kt)("p",null,"Welcome to OpenTwins, a flexible platform adapted to your needs! Although OpenTwins offers extensive customization options, we understand the importance of simplicity for beginners. Therefore, let's embark on a short journey together, showing you the quickest route to deploy the platform and develop a functional digital twin."),(0,a.kt)("h2",{id:"installation"},"Installation"),(0,a.kt)("h3",{id:"prerequisites"},"Prerequisites"),(0,a.kt)("p",null,"Please be sure you have the following utilities installed on your host machine:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://www.docker.com/"},"Docker")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://kubernetes.io/releases/download/"},"Kubernetes")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://helm.sh/docs/intro/install/"},"Helm")," v3")),(0,a.kt)("p",null,"If you don't have a Kubernetes cluster, you can set one up on local using ",(0,a.kt)("a",{parentName:"p",href:"https://minikube.sigs.k8s.io/docs/"},"minikube"),". For a smooth deployment experience, we suggest you use the following minimum configuration values."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"minikube start --cpus 4 --disk-size 40gb --memory 8192\n")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"kubectl config use-context minikube\n")),(0,a.kt)("h3",{id:"deploy"},"Deploy"),(0,a.kt)("p",null,"The quickest way to deploy OpenTwins is ",(0,a.kt)("a",{parentName:"p",href:"https://helm.sh/docs/intro/using_helm/"},"using Helm"),"."),(0,a.kt)("p",null,"The following command adds the ERTIS repository where the OpenTwins helm chart is located."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"helm repo add ertis https://ertis-research.github.io/Helm-charts/\n")),(0,a.kt)("p",null,"To deploy the platform with recommended functionality, use the command below:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"helm upgrade --install opentwins ertis/OpenTwins -n opentwins --wait --dependency-update\n")),(0,a.kt)("p",null,"To modify the components to be deployed and connected during the installation, you can check the ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/installation/using-helm"},"installation via Helm"),"."),(0,a.kt)("h3",{id:"configuration"},"Configuration"),(0,a.kt)("p",null,"If you've correctly installed OpenTwins Helm using the default settings, all connections should be established.\nThe final step involves ",(0,a.kt)("strong",{parentName:"p"},"configuring the platform interface plugin")," by adding the addresses of the Eclipse Ditto nginx service and the Ditto Extended API component into the plugin's configuration section.\nCheck the ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/installation/using-helm#configuration"},"installation documentation")," for more details."),(0,a.kt)("h2",{id:"create-your-first-digital-twin"},"Create your first digital twin"),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(2979).Z,alt:"Create digital twins",style:{width:700,margin:40}})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"A digital twin must be at least a synchronized replica of a real system or object"),". To create it, the first step involves understanding the purpose of the digital twin, designing its structure and defining its most relevant characteristics. Next, it is necessary to define this information in OpenTwins and then connect the data sources that will feed the model. Finally, it is necessary to represent the data in a way that is understandable to any user."),(0,a.kt)("p",null,"Optionally, other useful functionalities can be added to the digital twin. In OpenTwins, we offer the integration of AI/ML models, the addition of 3D models and the execution of FMI or containerized simulations. However, this tutorial will not cover these extra functionalities, so we recommend consulting their respective guides for more information."),(0,a.kt)("p",null,"Following these steps, we will use OpenTwins to develop the ",(0,a.kt)("strong",{parentName:"p"},"digital twin of a car"),". In this case, for simplicity, we will focus only on the speed and direction of the car's four wheels. In addition, we will record the GPS location of the vehicle for tracking."),(0,a.kt)("h3",{id:"design"},"Design"),(0,a.kt)("p",null,"Taking advantage of the platform's functionalities, we will create a ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/overview/concepts#digital-twins-composition"},"composite digital twin"),".\nFor this purpose, we will define ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/overview/concepts#digital-twin-type"},"types"),' "car" and "wheel", which will abstract information about the car and the wheel, respectively.\nThese types will be linked by a composition relation, which means that a car comprises four wheels. Once all this is set up, instantiating the car as a digital twin will automatically generate twins for all four wheels.\nIn this way, we can independently access the data for each wheel and easily add digital twins for other cars or other contexts.'),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(4347).Z,alt:"Create digital twins",style:{width:700}})),(0,a.kt)("admonition",{type:"info"},(0,a.kt)("p",{parentName:"admonition"},"The creation of ",(0,a.kt)("strong",{parentName:"p"},"types is not essential")," to create a digital twin, but ",(0,a.kt)("strong",{parentName:"p"},"it is recommended")," to facilitate future work. You can create digital twins directly without defining a type, just select the ",(0,a.kt)("em",{parentName:"p"},"from scratch")," option in the twin creation form.")),(0,a.kt)("h3",{id:"definition"},"Definition"),(0,a.kt)("h4",{id:"create-car-type"},"Create Car type"),(0,a.kt)("p",null,"First, we will create the car type. To do so, we navigate to the ",(0,a.kt)("em",{parentName:"p"},"Types")," section in the interface and click on the blue ",(0,a.kt)("em",{parentName:"p"},"Create new type")," button. In this form, we must fill in the ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/overview/concepts#digital-twin-content"},"digital twin information")," that will be common for all instances. This includes the name, description and image of the twin, along with the values it will store, in this case ",(0,a.kt)("strong",{parentName:"p"},"gps"),".\nWe will define this type within a namespace and assign it a name.\nThe combination of the namespace and the name will be referred to as the ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/basic-thing.html#thing-id"},"thingId"),"."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Screenshots of the filled form"),(0,a.kt)("div",null,(0,a.kt)("center",null,(0,a.kt)("img",{src:n(9636).Z,alt:"Create type - Identification and type information"}),(0,a.kt)("img",{src:n(2748).Z,alt:"Create type - attributes and features"})))),(0,a.kt)("p",null,"The JSON generated in ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/basic-thing.html#model-specification"},"Ditto Thing")," model is shown to the right of the form, which in this case corresponds to the following JSON:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "thingId": "example:car",\n "policyId": "default:basic_policy",\n "attributes": {\n "name": "Car",\n "description": "Digital twin example for quickstart",\n "image": "https://images.pexels.com/photos/119435/pexels-photo-119435.jpeg"\n },\n "features": {\n "gps": {\n "properties": {\n "value": null\n }\n }\n }\n}\n')),(0,a.kt)("p",null,"Click on the blue ",(0,a.kt)("em",{parentName:"p"},"Create type")," button to create the type. A message should appear indicating that the type has been successfully created. You can close this message and return to the main screen."),(0,a.kt)("admonition",{type:"info"},(0,a.kt)("p",{parentName:"admonition"},"If you have installed OpenTwins with Helm, you should have a basic policy. Currently, we do not take into account the restriction of access to digital twins by ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/basic-policy.html"},"policy"),", so we always use the same one. Still, using this functionality is possible with OpenTwins, although you must interact directly with Eclipse Ditto. "),(0,a.kt)("p",{parentName:"admonition"},"For more details see the ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/http-api-doc.html#/Policies"},"Eclipse Ditto documentation"),".")),(0,a.kt)("h4",{id:"create-wheel-type"},"Create Wheel type"),(0,a.kt)("p",null,"Next we will create the type for the wheels.\nIn the list of types, we will access the type of the car we have just created to see its information.\nHere we select the ",(0,a.kt)("em",{parentName:"p"},"children")," tab and click on the ",(0,a.kt)("em",{parentName:"p"},"Create new type")," button.\nThis form is almost identical to the previous one, with the difference that we can directly specify the number of instances of this new type to be created when a car type is instantiated.\nIn our example we will have to indicate a 4 in this section and fill in the rest of the form as before.\nThis type, in addition to its identification and basic information, will have as features the ",(0,a.kt)("strong",{parentName:"p"},"velocity")," and ",(0,a.kt)("strong",{parentName:"p"},"direction")," of the wheel."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Screenshots of the filled form"),(0,a.kt)("div",null,(0,a.kt)("center",null,(0,a.kt)("img",{src:n(8775).Z,alt:"Create type - Identification and type information"}),(0,a.kt)("img",{src:n(8226).Z,alt:"Create type - attributes and features"})))),(0,a.kt)("p",null,"In this case the JSON of the generated Ditto Thing is the following:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "thingId": "example:wheel",\n "policyId": "default:basic_policy",\n "attributes": {\n "name": "Wheel",\n "description": "Digital twin example for quickstart",\n "image": "https://images.pexels.com/photos/111766/pexels-photo-111766.jpeg"\n },\n "features": {\n "velocity": {\n "properties": {\n "value": null\n }\n },\n "direction": {\n "properties": {\n "value": null\n }\n }\n }\n}\n')),(0,a.kt)("p",null,"After clicking the ",(0,a.kt)("em",{parentName:"p"},"Create type")," button, a confirmation message will appear.\nIf we now navigate to the children tab of the car type, it will show that the wheel type is one of its children and will be instantiated four times."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(7390).Z,alt:"Children of car type",style:{width:600}})),(0,a.kt)("h4",{id:"create-the-digital-twins"},"Create the digital twins"),(0,a.kt)("p",null,"All that remains is to instantiate the car type so that all the twins are created.\nTo do this, navigate to the ",(0,a.kt)("em",{parentName:"p"},"Twins")," section and click on ",(0,a.kt)("em",{parentName:"p"},"Create a new twin"),".\nIn the form, specify the identification of the twin and select the car type.\nAll data will be filled in automatically, though you can modify it if desired by activating the customization switch."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Screenshots of the filled form"),(0,a.kt)("div",null,(0,a.kt)("center",null,(0,a.kt)("img",{src:n(9726).Z,alt:"Create twin form"})))),(0,a.kt)("p",null,"After clicking on ",(0,a.kt)("em",{parentName:"p"},"Create twin"),", the 5 digital twins will be generated automatically. A successfull message will appear when the process is finished.\nIf we check the list of twins, we will see our twin example:car.\nBy clicking on it and accessing the children tab, we will find four twins corresponding to its wheels, each with the features specified in their respective type."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(4008).Z,alt:"Children of car type"})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"The composite digital twin has already been defined.")),(0,a.kt)("h3",{id:"connection"},"Connection"),(0,a.kt)("p",null,"Once we have the digital twin defined, we need to feed it with data.\nEclipse Ditto requires data updates to be sent in ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/protocol-specification.html#dittoProtocolEnvelope"},"Ditto Protocol"),", which is a JSON format that indicates which parts of the digital twin we want to update and how to do it."),(0,a.kt)("admonition",{type:"info"},(0,a.kt)("p",{parentName:"admonition"},"It is also possible to connect Eclipse Ditto with messaging brokers that use other message formats.\nWe can add a ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/connectivity-mapping.html#javascript-examples"},"JavaScript mapping")," to the Eclipse Ditto connection, which will automatically transform the messages to the Ditto Protocol format.")),(0,a.kt)("p",null,"For Eclipse Ditto to collect the data, it is necessary to establish a ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/connectivity-overview.html"},"connection")," through MQTT, AMQP or Kafka, providing all the required information (address, port, credentials, etc.).\nIn this example, we will take advantage of the source connection that is automatically created with the installation of Helm (",(0,a.kt)("inlineCode",{parentName:"p"},"mosquitto-source-connection"),").\nThis connection pulls messages via MQTT from the Mosquitto deployed with the platform, using any subtopic within the telemetry topic (i.e. ",(0,a.kt)("inlineCode",{parentName:"p"},"telemetry/#"),").\nBy default, it does not include any JavaScript mapping, so we will send messages directly in Ditto Protocol.\nIf you need to use another connection, see the ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/http-api-doc.html#/Connections"},"Eclipse Ditto documentation"),". "),(0,a.kt)("p",null,"We will need to get the address of Mosquitto, which will depend on your installation."),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"If you are using a cluster in your network"),", the address will be your cluster address and the port can be obtained by running ",(0,a.kt)("inlineCode",{parentName:"li"},"kubectl get services")," and looking up the ",(0,a.kt)("a",{parentName:"li",href:"https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport"},(0,a.kt)("em",{parentName:"a"},"NodePort"))," of the Mosquitto service (default should be 30511)."),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"If you are using Minikube"),", you must expose the Mosquitto service in order to access it from your ",(0,a.kt)("em",{parentName:"li"},"localhost"),".\nTo do this, find the name of the service with ",(0,a.kt)("inlineCode",{parentName:"li"},"kubectl get services")," and then run ",(0,a.kt)("inlineCode",{parentName:"li"},"minikube service --url"),".\nThis will return a URL with the address and port to connect to.")),(0,a.kt)("p",null,"Since we don't have real data, we are going to create a Python script that generates random data from the car and its wheels every 5 seconds and sends it in Ditto Protocol to Mosquitto.\nTo run the script we will need to install the ",(0,a.kt)("a",{parentName:"p",href:"https://pypi.org/project/paho-mqtt/"},"Paho library for MQTT")," (typing_extensions is one of its dependencies)."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"pip install paho-mqtt\npip install typing_extensions\n")),(0,a.kt)("p",null,"In the following script you must ",(0,a.kt)("strong",{parentName:"p"},"change the MQTT broker address and port")," to your own."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-python"},'import paho.mqtt.client as mqtt\nimport random\nimport time\nimport json\n\n# Digital twin info\nnamespace = "example"\ncar_name = "mycar"\nwheels_name = "mycar:wheel_"\n\n# MQTT info\nbroker = "localhost" # MQTT broker address\nport = 1883 # MQTT port\ntopic = "telemetry/" # Topic where data will be published\n\n# MQTT connection\ndef on_connect(client, userdata, flags, rc):\n if rc == 0:\n print("Successful connection")\n else:\n print(f"Connection failed with code {rc}")\n\nclient = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)\nclient.on_connect = on_connect\nclient.username_pw_set(username, password)\nclient.connect(broker, port, 60)\n\n# Data generator\ndef generate_wheel_data():\n velocity = random.uniform(0, 100) # Generate random velocity (between 0 and 100 km/h)\n direction = random.uniform(-45, 45) # Generate random direction (between -45 and 45 degrees)\n return velocity, direction\n\ndef generate_gps_data():\n latitude = random.uniform(-90.0, 90.0)\n longitude = random.uniform(-180.0, 180.0)\n return latitude, longitude\n\n# Ditto Protocol\ndef get_ditto_protocol_value_car(time, latitude, longitude):\n return {\n "gps" : {\n "properties": {\n "latitude": latitude,\n "longitude": longitude,\n "time": time\n }\n }\n }\n\ndef get_ditto_protocol_value_wheel(time, velocity, direction):\n return {\n "velocity" : {\n "properties": {\n "value": velocity,\n "time": time\n }\n },\n "direction": {\n "properties" : {\n "value": direction,\n "time" : time\n }\n }\n }\n\ndef get_ditto_protocol_msg(name, value):\n return {\n "topic": "{}/{}/things/twin/commands/merge".format(namespace, name),\n "headers": {\n "content-type": "application/merge-patch+json"\n },\n "path": "/features",\n "value": value\n }\n\n# Send data\ntry:\n while True:\n t = round(time.time() * 1000) # Unix ms\n \n # Car twin\n latitude, longitude = generate_gps_data()\n msg = get_ditto_protocol_msg(car_name, get_ditto_protocol_value_car(t, latitude, longitude))\n client.publish(topic + namespace + "/" + car_name, json.dumps(msg))\n print(car_name + " data published")\n \n # Wheels twins\n for i in range(1, 5):\n name = wheels_name+str(i)\n velocity, direction = generate_wheel_data()\n msg = get_ditto_protocol_msg(name, get_ditto_protocol_value_wheel(t, velocity, direction))\n client.publish(topic + namespace + "/" + name, json.dumps(msg))\n print(name + " data published")\n \n time.sleep(5)\n \nexcept KeyboardInterrupt:\n client.disconnect()\n')),(0,a.kt)("p",null,"When you ",(0,a.kt)("strong",{parentName:"p"},"run the script"),", the data should start to be stored in the digital twins.\nYou can see if messages are being received by checking the information of the twins, where the values of their features will now have data."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(8606).Z,alt:"Children of car type",style:{width:700}})),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Are the twins not being updated?"),(0,a.kt)("div",null,(0,a.kt)("p",null,"You can check if messages arrive to Mosquitto by using ",(0,a.kt)("a",{parentName:"p",href:"https://mqtt-explorer.com/"},"MQTT explorer"),".\nIf they are sending correctly, you should see something like the following image:"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"MQTT explorer",src:n(1845).Z,width:"1140",height:"628"})),(0,a.kt)("p",null,"And the messages being sent should be like these:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "topic": "example/mycar:wheel_2/things/twin/commands/merge",\n "headers": {\n "content-type": "application/merge-patch+json"\n },\n "path": "/features",\n "value": {\n "velocity": {\n "properties": {\n "value": 44.07908610511725,\n "time": 1715943644787\n }\n },\n "direction": {\n "properties": {\n "value": 37.92163063527694,\n "time": 1715943644787\n }\n }\n }\n}\n')),(0,a.kt)("p",null,"If the ",(0,a.kt)("strong",{parentName:"p"},"messages are not being received"),", debug the code to see what might be happening. Most likely you are not connecting correctly to Mosquitto (wrong address or port)."),(0,a.kt)("p",null,"If the ",(0,a.kt)("strong",{parentName:"p"},"messages are being received"),", then the problem is in the source connection to Eclipse Ditto. Check the connection logs with ",(0,a.kt)("inlineCode",{parentName:"p"},"http://:/api/2/connections/mosquitto-source-connection/logs"),".\nThe Eclipse Ditto ip and port are obtained the same as mosquitto's, but since Ditto has more than one service, you have to query/expose the ",(0,a.kt)("strong",{parentName:"p"},"nginx")," one."))),(0,a.kt)("h3",{id:"visualization"},"Visualization"),(0,a.kt)("p",null,"Finally, we need to present the data in a user-friendly and meaningful way for the users of the digital twin.\nTo achieve this, we will create a new ",(0,a.kt)("a",{parentName:"p",href:"https://grafana.com/docs/grafana/latest/dashboards/"},"dashboard")," in Grafana and add ",(0,a.kt)("a",{parentName:"p",href:"https://grafana.com/docs/grafana/latest/panels-visualizations/"},"panels")," to display the relevant digital twin information."),(0,a.kt)("p",null,"The digital twin data is stored in an ",(0,a.kt)("a",{parentName:"p",href:"https://docs.influxdata.com/influxdb/v2/"},"InfluxDB2")," database, so we will have to query the information using ",(0,a.kt)("a",{parentName:"p",href:"https://docs.influxdata.com/flux/v0/"},"Flux")," language.\nIf OpenTwins has been installed via Helm with default values, the ",(0,a.kt)("a",{parentName:"p",href:"https://grafana.com/docs/grafana/latest/datasources/"},"connection")," between InfluxDB and Grafana should already be established, so it will only be necessary to select it as data source when creating a panel."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(159).Z,alt:"Children of car type",style:{width:400}})),(0,a.kt)("p",null,"In this example, we will demonstrate a basic visualization. However, ",(0,a.kt)("strong",{parentName:"p"},"you can use any of Grafana's functionalities and plugins to customize it according to your specific objectives"),".\nWe will create four panels: one displaying the most recent GPS data of the car, another showing the evolution of the GPS data, a third panel indicating the current direction of all the wheels, and a fourth comparing the velocity of each wheel.\nThe result would look something like this:"),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(589).Z,alt:"Grafana dashboard"})),(0,a.kt)("p",null,"For each of the four panels, we have selected the most convenient chart type, kept the default settings and added the related query in the Query section."),(0,a.kt)("p",null,"The panel displaying the ",(0,a.kt)("strong",{parentName:"p"},"current GPS")," data extracts the longitude and latitude information from the digital twin ",(0,a.kt)("em",{parentName:"p"},"example:mycar"),".\nIt renames the fields for proper display, retains the relevant fields, sorts the results by time, and keeps only the most recent entry."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => r["thingId"] == "example:mycar")\n |> filter(fn: (r) => r["_field"] == "value_gps_properties_latitude" or r["_field"] == "value_gps_properties_longitude")\n |> map(fn: (r) => ({ r with _field: strings.replace(v: r["_field"], t: "value_gps_properties_", u: "", i: 2) }))\n |> keep (columns: ["_value", "_field", "_time"])\n |> sort(columns: ["_time"], desc: false) \n |> last() \n')),(0,a.kt)("p",null,"The panel for show the ",(0,a.kt)("strong",{parentName:"p"},"GPS evolution")," also extracts the latitude and longitude data from the digital twin ",(0,a.kt)("em",{parentName:"p"},"example:mycar"),", but keeps all the entries instead of just the last one."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => r["thingId"] == "example:mycar")\n |> filter(fn: (r) => r["_field"] == "value_gps_properties_latitude" or r["_field"] == "value_gps_properties_longitude")\n |> map(fn: (r) => ({ r with _field: strings.replace(v: r["_field"], t: "value_gps_properties_", u: "", i: 2) }))\n |> keep (columns: ["_value", "_field", "_time"])\n')),(0,a.kt)("p",null,"The panel displaying the ",(0,a.kt)("strong",{parentName:"p"},"current direction of wheels")," extracts the direction data of the four twins corresponding to the wheels, identified by starting with ",(0,a.kt)("em",{parentName:"p"},"example:mycar:wheel_"),".\nIt modifies the identifiers of the twins for a more readable display and retains the most recent value based on time."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => strings.hasPrefix(v: r["thingId"], prefix: "example:mycar:wheel_"))\n |> filter(fn: (r) => r["_field"] == "value_direction_properties_value")\n |> map(fn: (r) => ({ r with thingId: strings.replace(v: r["thingId"], t: "example:mycar:", u: "", i: 2) }))\n |> keep (columns: ["thingId", "_value", "_time"])\n |> sort(columns: ["_time"], desc: false) \n |> last()\n')),(0,a.kt)("p",null,"Finally, the panel that makes a ",(0,a.kt)("strong",{parentName:"p"},"wheels velocity comparison")," is similar to the previous one, although extracting the velocity data from the 4 twins instead of the direction and keeping all the entries."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => strings.hasPrefix(v: r["thingId"], prefix: "example:mycar:wheel_"))\n |> filter(fn: (r) => r["_field"] == "value_velocity_properties_value")\n |> map(fn: (r) => ({ r with thingId: strings.replace(v: r["thingId"], t: "example:mycar:", u: "", i: 2) }))\n |> keep (columns: ["thingId", "_value", "_time"])\n')),(0,a.kt)("p",null,"This satisfies the basic requirements to consider a system as a digital twin.\nHowever, to take full advantage of its capabilities, we recommend including other functionalities or additional data sources.\nThis will allow you to obtain a more complete and accurate view of the real system.\nYou can check our ",(0,a.kt)("a",{parentName:"p",href:"./category/guides"},"guides")," for more information"))}h.isMDXComponent=!0},4008:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/children-car-twin-ca2cf91c075a327581d6ba641943742c.png"},7390:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/children-car-d181addcc20bcf1a9a75715cf3353c15.png"},2979:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-digital-twins-bd76130c9ab23c157cdfcd9dcb0fdc5b.png"},9726:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-twin-car-4a9968f0a40982680599c72e2f6d3b23.png"},9636:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-car-1-83399364c854df3e8b229885140649ba.png"},2748:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-car-2-c0b3b0d20c5b4291c77340e8a685d0c6.png"},8775:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-wheel-1-d71b44232fb9dfeebc601958e43d19fd.png"},8226:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-wheel-2-01daddbffbb636b06904956113d446fc.png"},589:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/grafana-dashboard-d0dbd9d8646a93fa599ba324ce583338.png"},159:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/grafana-datasource-e1e06ed3137227fd650fb894928415db.png"},4347:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/model-car-example-2b119ad662f43ec5caa875a83d9d7b7c.jpg"},8606:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/wheel-data-30d7b695d58b99fcec8f817d5d5b45e1.png"},1845:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/mqtt-explorer-ebaae1985874581cb44c024060bef150.png"}}]); \ No newline at end of file diff --git a/assets/js/74876495.cd08c5dc.js b/assets/js/74876495.cd08c5dc.js new file mode 100644 index 0000000..8ed9e17 --- /dev/null +++ b/assets/js/74876495.cd08c5dc.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5049],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>m});var i=n(7294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function r(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var l=i.createContext({}),c=function(e){var t=i.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},p=function(e){var t=c(e.components);return i.createElement(l.Provider,{value:t},e.children)},d="mdxType",h={inlineCode:"code",wrapper:function(e){var t=e.children;return i.createElement(i.Fragment,{},t)}},u=i.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,l=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),d=c(n),u=a,m=d["".concat(l,".").concat(u)]||d[u]||h[u]||o;return n?i.createElement(m,r(r({ref:t},p),{},{components:n})):i.createElement(m,r({ref:t},p))}));function m(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,r=new Array(o);r[0]=u;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[d]="string"==typeof e?e:a,r[1]=s;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var i=n(7462),a=(n(7294),n(3905));n(4996),n(941);const o={sidebar_position:1},r="Quickstart",s={unversionedId:"quickstart",id:"quickstart",title:"Quickstart",description:"Welcome to OpenTwins, a flexible platform adapted to your needs! Although OpenTwins offers extensive customization options, we understand the importance of simplicity for beginners. Therefore, let's embark on a short journey together, showing you the quickest route to deploy the platform and develop a functional digital twin.",source:"@site/docs/quickstart.mdx",sourceDirName:".",slug:"/quickstart",permalink:"/opentwins/docs/quickstart",draft:!1,editUrl:"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/quickstart.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",next:{title:"Overview",permalink:"/opentwins/docs/category/overview"}},l={},c=[{value:"Installation",id:"installation",level:2},{value:"Prerequisites",id:"prerequisites",level:3},{value:"Deploy",id:"deploy",level:3},{value:"Configuration",id:"configuration",level:3},{value:"Create your first digital twin",id:"create-your-first-digital-twin",level:2},{value:"Design",id:"design",level:3},{value:"Definition",id:"definition",level:3},{value:"Create Car type",id:"create-car-type",level:4},{value:"Create Wheel type",id:"create-wheel-type",level:4},{value:"Create the digital twins",id:"create-the-digital-twins",level:4},{value:"Connection",id:"connection",level:3},{value:"Visualization",id:"visualization",level:3}],p={toc:c},d="wrapper";function h(e){let{components:t,...o}=e;return(0,a.kt)(d,(0,i.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"quickstart"},"Quickstart"),(0,a.kt)("p",null,"Welcome to OpenTwins, a flexible platform adapted to your needs! Although OpenTwins offers extensive customization options, we understand the importance of simplicity for beginners. Therefore, let's embark on a short journey together, showing you the quickest route to deploy the platform and develop a functional digital twin."),(0,a.kt)("h2",{id:"installation"},"Installation"),(0,a.kt)("h3",{id:"prerequisites"},"Prerequisites"),(0,a.kt)("p",null,"Please be sure you have the following utilities installed on your host machine:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://www.docker.com/"},"Docker")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://kubernetes.io/releases/download/"},"Kubernetes")),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://helm.sh/docs/intro/install/"},"Helm")," v3")),(0,a.kt)("p",null,"If you don't have a Kubernetes cluster, you can set one up on local using ",(0,a.kt)("a",{parentName:"p",href:"https://minikube.sigs.k8s.io/docs/"},"minikube"),". For a smooth deployment experience, we suggest you use the following minimum configuration values."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"minikube start --cpus 4 --disk-size 40gb --memory 8192\n")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"kubectl config use-context minikube\n")),(0,a.kt)("h3",{id:"deploy"},"Deploy"),(0,a.kt)("p",null,"The quickest way to deploy OpenTwins is ",(0,a.kt)("a",{parentName:"p",href:"https://helm.sh/docs/intro/using_helm/"},"using Helm"),"."),(0,a.kt)("p",null,"The following command adds the ERTIS repository where the OpenTwins helm chart is located."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"helm repo add ertis https://ertis-research.github.io/Helm-charts/\n")),(0,a.kt)("p",null,"To deploy the platform with recommended functionality, use the command below:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"helm upgrade --install opentwins ertis/OpenTwins --wait --dependency-update\n")),(0,a.kt)("p",null,"To modify the components to be deployed and connected during the installation, you can check the ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/installation/using-helm"},"installation via Helm"),"."),(0,a.kt)("h3",{id:"configuration"},"Configuration"),(0,a.kt)("p",null,"If you've correctly installed OpenTwins Helm using the default settings, all connections should be established.\nThe final step involves ",(0,a.kt)("strong",{parentName:"p"},"configuring the platform interface plugin")," by adding the addresses of the Eclipse Ditto nginx service and the Ditto Extended API component into the plugin's configuration section.\nCheck the ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/installation/using-helm#configuration"},"installation documentation")," for more details."),(0,a.kt)("h2",{id:"create-your-first-digital-twin"},"Create your first digital twin"),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(2979).Z,alt:"Create digital twins",style:{width:700,margin:40}})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"A digital twin must be at least a synchronized replica of a real system or object"),". To create it, the first step involves understanding the purpose of the digital twin, designing its structure and defining its most relevant characteristics. Next, it is necessary to define this information in OpenTwins and then connect the data sources that will feed the model. Finally, it is necessary to represent the data in a way that is understandable to any user."),(0,a.kt)("p",null,"Optionally, other useful functionalities can be added to the digital twin. In OpenTwins, we offer the integration of AI/ML models, the addition of 3D models and the execution of FMI or containerized simulations. However, this tutorial will not cover these extra functionalities, so we recommend consulting their respective guides for more information."),(0,a.kt)("p",null,"Following these steps, we will use OpenTwins to develop the ",(0,a.kt)("strong",{parentName:"p"},"digital twin of a car"),". In this case, for simplicity, we will focus only on the speed and direction of the car's four wheels. In addition, we will record the GPS location of the vehicle for tracking."),(0,a.kt)("h3",{id:"design"},"Design"),(0,a.kt)("p",null,"Taking advantage of the platform's functionalities, we will create a ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/overview/concepts#digital-twins-composition"},"composite digital twin"),".\nFor this purpose, we will define ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/overview/concepts#digital-twin-type"},"types"),' "car" and "wheel", which will abstract information about the car and the wheel, respectively.\nThese types will be linked by a composition relation, which means that a car comprises four wheels. Once all this is set up, instantiating the car as a digital twin will automatically generate twins for all four wheels.\nIn this way, we can independently access the data for each wheel and easily add digital twins for other cars or other contexts.'),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(4347).Z,alt:"Create digital twins",style:{width:700}})),(0,a.kt)("admonition",{type:"info"},(0,a.kt)("p",{parentName:"admonition"},"The creation of ",(0,a.kt)("strong",{parentName:"p"},"types is not essential")," to create a digital twin, but ",(0,a.kt)("strong",{parentName:"p"},"it is recommended")," to facilitate future work. You can create digital twins directly without defining a type, just select the ",(0,a.kt)("em",{parentName:"p"},"from scratch")," option in the twin creation form.")),(0,a.kt)("h3",{id:"definition"},"Definition"),(0,a.kt)("h4",{id:"create-car-type"},"Create Car type"),(0,a.kt)("p",null,"First, we will create the car type. To do so, we navigate to the ",(0,a.kt)("em",{parentName:"p"},"Types")," section in the interface and click on the blue ",(0,a.kt)("em",{parentName:"p"},"Create new type")," button. In this form, we must fill in the ",(0,a.kt)("a",{parentName:"p",href:"/opentwins/docs/overview/concepts#digital-twin-content"},"digital twin information")," that will be common for all instances. This includes the name, description and image of the twin, along with the values it will store, in this case ",(0,a.kt)("strong",{parentName:"p"},"gps"),".\nWe will define this type within a namespace and assign it a name.\nThe combination of the namespace and the name will be referred to as the ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/basic-thing.html#thing-id"},"thingId"),"."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Screenshots of the filled form"),(0,a.kt)("div",null,(0,a.kt)("center",null,(0,a.kt)("img",{src:n(9636).Z,alt:"Create type - Identification and type information"}),(0,a.kt)("img",{src:n(2748).Z,alt:"Create type - attributes and features"})))),(0,a.kt)("p",null,"The JSON generated in ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/basic-thing.html#model-specification"},"Ditto Thing")," model is shown to the right of the form, which in this case corresponds to the following JSON:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "thingId": "example:car",\n "policyId": "default:basic_policy",\n "attributes": {\n "name": "Car",\n "description": "Digital twin example for quickstart",\n "image": "https://images.pexels.com/photos/119435/pexels-photo-119435.jpeg"\n },\n "features": {\n "gps": {\n "properties": {\n "value": null\n }\n }\n }\n}\n')),(0,a.kt)("p",null,"Click on the blue ",(0,a.kt)("em",{parentName:"p"},"Create type")," button to create the type. A message should appear indicating that the type has been successfully created. You can close this message and return to the main screen."),(0,a.kt)("admonition",{type:"info"},(0,a.kt)("p",{parentName:"admonition"},"If you have installed OpenTwins with Helm, you should have a basic policy. Currently, we do not take into account the restriction of access to digital twins by ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/basic-policy.html"},"policy"),", so we always use the same one. Still, using this functionality is possible with OpenTwins, although you must interact directly with Eclipse Ditto. "),(0,a.kt)("p",{parentName:"admonition"},"For more details see the ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/http-api-doc.html#/Policies"},"Eclipse Ditto documentation"),".")),(0,a.kt)("h4",{id:"create-wheel-type"},"Create Wheel type"),(0,a.kt)("p",null,"Next we will create the type for the wheels.\nIn the list of types, we will access the type of the car we have just created to see its information.\nHere we select the ",(0,a.kt)("em",{parentName:"p"},"children")," tab and click on the ",(0,a.kt)("em",{parentName:"p"},"Create new type")," button.\nThis form is almost identical to the previous one, with the difference that we can directly specify the number of instances of this new type to be created when a car type is instantiated.\nIn our example we will have to indicate a 4 in this section and fill in the rest of the form as before.\nThis type, in addition to its identification and basic information, will have as features the ",(0,a.kt)("strong",{parentName:"p"},"velocity")," and ",(0,a.kt)("strong",{parentName:"p"},"direction")," of the wheel."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Screenshots of the filled form"),(0,a.kt)("div",null,(0,a.kt)("center",null,(0,a.kt)("img",{src:n(8775).Z,alt:"Create type - Identification and type information"}),(0,a.kt)("img",{src:n(8226).Z,alt:"Create type - attributes and features"})))),(0,a.kt)("p",null,"In this case the JSON of the generated Ditto Thing is the following:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "thingId": "example:wheel",\n "policyId": "default:basic_policy",\n "attributes": {\n "name": "Wheel",\n "description": "Digital twin example for quickstart",\n "image": "https://images.pexels.com/photos/111766/pexels-photo-111766.jpeg"\n },\n "features": {\n "velocity": {\n "properties": {\n "value": null\n }\n },\n "direction": {\n "properties": {\n "value": null\n }\n }\n }\n}\n')),(0,a.kt)("p",null,"After clicking the ",(0,a.kt)("em",{parentName:"p"},"Create type")," button, a confirmation message will appear.\nIf we now navigate to the children tab of the car type, it will show that the wheel type is one of its children and will be instantiated four times."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(7390).Z,alt:"Children of car type",style:{width:600}})),(0,a.kt)("h4",{id:"create-the-digital-twins"},"Create the digital twins"),(0,a.kt)("p",null,"All that remains is to instantiate the car type so that all the twins are created.\nTo do this, navigate to the ",(0,a.kt)("em",{parentName:"p"},"Twins")," section and click on ",(0,a.kt)("em",{parentName:"p"},"Create a new twin"),".\nIn the form, specify the identification of the twin and select the car type.\nAll data will be filled in automatically, though you can modify it if desired by activating the customization switch."),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Screenshots of the filled form"),(0,a.kt)("div",null,(0,a.kt)("center",null,(0,a.kt)("img",{src:n(9726).Z,alt:"Create twin form"})))),(0,a.kt)("p",null,"After clicking on ",(0,a.kt)("em",{parentName:"p"},"Create twin"),", the 5 digital twins will be generated automatically. A successfull message will appear when the process is finished.\nIf we check the list of twins, we will see our twin example:car.\nBy clicking on it and accessing the children tab, we will find four twins corresponding to its wheels, each with the features specified in their respective type."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(4008).Z,alt:"Children of car type"})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"The composite digital twin has already been defined.")),(0,a.kt)("h3",{id:"connection"},"Connection"),(0,a.kt)("p",null,"Once we have the digital twin defined, we need to feed it with data.\nEclipse Ditto requires data updates to be sent in ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/protocol-specification.html#dittoProtocolEnvelope"},"Ditto Protocol"),", which is a JSON format that indicates which parts of the digital twin we want to update and how to do it."),(0,a.kt)("admonition",{type:"info"},(0,a.kt)("p",{parentName:"admonition"},"It is also possible to connect Eclipse Ditto with messaging brokers that use other message formats.\nWe can add a ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/connectivity-mapping.html#javascript-examples"},"JavaScript mapping")," to the Eclipse Ditto connection, which will automatically transform the messages to the Ditto Protocol format.")),(0,a.kt)("p",null,"For Eclipse Ditto to collect the data, it is necessary to establish a ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/connectivity-overview.html"},"connection")," through MQTT, AMQP or Kafka, providing all the required information (address, port, credentials, etc.).\nIn this example, we will take advantage of the source connection that is automatically created with the installation of Helm (",(0,a.kt)("inlineCode",{parentName:"p"},"mosquitto-source-connection"),").\nThis connection pulls messages via MQTT from the Mosquitto deployed with the platform, using any subtopic within the telemetry topic (i.e. ",(0,a.kt)("inlineCode",{parentName:"p"},"telemetry/#"),").\nBy default, it does not include any JavaScript mapping, so we will send messages directly in Ditto Protocol.\nIf you need to use another connection, see the ",(0,a.kt)("a",{parentName:"p",href:"https://eclipse.dev/ditto/3.3/http-api-doc.html#/Connections"},"Eclipse Ditto documentation"),". "),(0,a.kt)("p",null,"We will need to get the address of Mosquitto, which will depend on your installation."),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"If you are using a cluster in your network"),", the address will be your cluster address and the port can be obtained by running ",(0,a.kt)("inlineCode",{parentName:"li"},"kubectl get services")," and looking up the ",(0,a.kt)("a",{parentName:"li",href:"https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport"},(0,a.kt)("em",{parentName:"a"},"NodePort"))," of the Mosquitto service (default should be 30511)."),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"If you are using Minikube"),", you must expose the Mosquitto service in order to access it from your ",(0,a.kt)("em",{parentName:"li"},"localhost"),".\nTo do this, find the name of the service with ",(0,a.kt)("inlineCode",{parentName:"li"},"kubectl get services")," and then run ",(0,a.kt)("inlineCode",{parentName:"li"},"minikube service --url"),".\nThis will return a URL with the address and port to connect to.")),(0,a.kt)("p",null,"Since we don't have real data, we are going to create a Python script that generates random data from the car and its wheels every 5 seconds and sends it in Ditto Protocol to Mosquitto.\nTo run the script we will need to install the ",(0,a.kt)("a",{parentName:"p",href:"https://pypi.org/project/paho-mqtt/"},"Paho library for MQTT")," (typing_extensions is one of its dependencies)."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-shell"},"pip install paho-mqtt\npip install typing_extensions\n")),(0,a.kt)("p",null,"In the following script you must ",(0,a.kt)("strong",{parentName:"p"},"change the MQTT broker address and port")," to your own."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-python"},'import paho.mqtt.client as mqtt\nimport random\nimport time\nimport json\n\n# Digital twin info\nnamespace = "example"\ncar_name = "mycar"\nwheels_name = "mycar:wheel_"\n\n# MQTT info\nbroker = "localhost" # MQTT broker address\nport = 1883 # MQTT port\ntopic = "telemetry/" # Topic where data will be published\n\n# MQTT connection\ndef on_connect(client, userdata, flags, rc):\n if rc == 0:\n print("Successful connection")\n else:\n print(f"Connection failed with code {rc}")\n\nclient = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)\nclient.on_connect = on_connect\nclient.username_pw_set(username, password)\nclient.connect(broker, port, 60)\n\n# Data generator\ndef generate_wheel_data():\n velocity = random.uniform(0, 100) # Generate random velocity (between 0 and 100 km/h)\n direction = random.uniform(-45, 45) # Generate random direction (between -45 and 45 degrees)\n return velocity, direction\n\ndef generate_gps_data():\n latitude = random.uniform(-90.0, 90.0)\n longitude = random.uniform(-180.0, 180.0)\n return latitude, longitude\n\n# Ditto Protocol\ndef get_ditto_protocol_value_car(time, latitude, longitude):\n return {\n "gps" : {\n "properties": {\n "latitude": latitude,\n "longitude": longitude,\n "time": time\n }\n }\n }\n\ndef get_ditto_protocol_value_wheel(time, velocity, direction):\n return {\n "velocity" : {\n "properties": {\n "value": velocity,\n "time": time\n }\n },\n "direction": {\n "properties" : {\n "value": direction,\n "time" : time\n }\n }\n }\n\ndef get_ditto_protocol_msg(name, value):\n return {\n "topic": "{}/{}/things/twin/commands/merge".format(namespace, name),\n "headers": {\n "content-type": "application/merge-patch+json"\n },\n "path": "/features",\n "value": value\n }\n\n# Send data\ntry:\n while True:\n t = round(time.time() * 1000) # Unix ms\n \n # Car twin\n latitude, longitude = generate_gps_data()\n msg = get_ditto_protocol_msg(car_name, get_ditto_protocol_value_car(t, latitude, longitude))\n client.publish(topic + namespace + "/" + car_name, json.dumps(msg))\n print(car_name + " data published")\n \n # Wheels twins\n for i in range(1, 5):\n name = wheels_name+str(i)\n velocity, direction = generate_wheel_data()\n msg = get_ditto_protocol_msg(name, get_ditto_protocol_value_wheel(t, velocity, direction))\n client.publish(topic + namespace + "/" + name, json.dumps(msg))\n print(name + " data published")\n \n time.sleep(5)\n \nexcept KeyboardInterrupt:\n client.disconnect()\n')),(0,a.kt)("p",null,"When you ",(0,a.kt)("strong",{parentName:"p"},"run the script"),", the data should start to be stored in the digital twins.\nYou can see if messages are being received by checking the information of the twins, where the values of their features will now have data."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(8606).Z,alt:"Children of car type",style:{width:700}})),(0,a.kt)("details",null,(0,a.kt)("summary",null,"Are the twins not being updated?"),(0,a.kt)("div",null,(0,a.kt)("p",null,"You can check if messages arrive to Mosquitto by using ",(0,a.kt)("a",{parentName:"p",href:"https://mqtt-explorer.com/"},"MQTT explorer"),".\nIf they are sending correctly, you should see something like the following image:"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"MQTT explorer",src:n(1845).Z,width:"1140",height:"628"})),(0,a.kt)("p",null,"And the messages being sent should be like these:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "topic": "example/mycar:wheel_2/things/twin/commands/merge",\n "headers": {\n "content-type": "application/merge-patch+json"\n },\n "path": "/features",\n "value": {\n "velocity": {\n "properties": {\n "value": 44.07908610511725,\n "time": 1715943644787\n }\n },\n "direction": {\n "properties": {\n "value": 37.92163063527694,\n "time": 1715943644787\n }\n }\n }\n}\n')),(0,a.kt)("p",null,"If the ",(0,a.kt)("strong",{parentName:"p"},"messages are not being received"),", debug the code to see what might be happening. Most likely you are not connecting correctly to Mosquitto (wrong address or port)."),(0,a.kt)("p",null,"If the ",(0,a.kt)("strong",{parentName:"p"},"messages are being received"),", then the problem is in the source connection to Eclipse Ditto. Check the connection logs with ",(0,a.kt)("inlineCode",{parentName:"p"},"http://:/api/2/connections/mosquitto-source-connection/logs"),".\nThe Eclipse Ditto ip and port are obtained the same as mosquitto's, but since Ditto has more than one service, you have to query/expose the ",(0,a.kt)("strong",{parentName:"p"},"nginx")," one."))),(0,a.kt)("h3",{id:"visualization"},"Visualization"),(0,a.kt)("p",null,"Finally, we need to present the data in a user-friendly and meaningful way for the users of the digital twin.\nTo achieve this, we will create a new ",(0,a.kt)("a",{parentName:"p",href:"https://grafana.com/docs/grafana/latest/dashboards/"},"dashboard")," in Grafana and add ",(0,a.kt)("a",{parentName:"p",href:"https://grafana.com/docs/grafana/latest/panels-visualizations/"},"panels")," to display the relevant digital twin information."),(0,a.kt)("p",null,"The digital twin data is stored in an ",(0,a.kt)("a",{parentName:"p",href:"https://docs.influxdata.com/influxdb/v2/"},"InfluxDB2")," database, so we will have to query the information using ",(0,a.kt)("a",{parentName:"p",href:"https://docs.influxdata.com/flux/v0/"},"Flux")," language.\nIf OpenTwins has been installed via Helm with default values, the ",(0,a.kt)("a",{parentName:"p",href:"https://grafana.com/docs/grafana/latest/datasources/"},"connection")," between InfluxDB and Grafana should already be established, so it will only be necessary to select it as data source when creating a panel."),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(159).Z,alt:"Children of car type",style:{width:400}})),(0,a.kt)("p",null,"In this example, we will demonstrate a basic visualization. However, ",(0,a.kt)("strong",{parentName:"p"},"you can use any of Grafana's functionalities and plugins to customize it according to your specific objectives"),".\nWe will create four panels: one displaying the most recent GPS data of the car, another showing the evolution of the GPS data, a third panel indicating the current direction of all the wheels, and a fourth comparing the velocity of each wheel.\nThe result would look something like this:"),(0,a.kt)("center",null,(0,a.kt)("img",{src:n(589).Z,alt:"Grafana dashboard"})),(0,a.kt)("p",null,"For each of the four panels, we have selected the most convenient chart type, kept the default settings and added the related query in the Query section."),(0,a.kt)("p",null,"The panel displaying the ",(0,a.kt)("strong",{parentName:"p"},"current GPS")," data extracts the longitude and latitude information from the digital twin ",(0,a.kt)("em",{parentName:"p"},"example:mycar"),".\nIt renames the fields for proper display, retains the relevant fields, sorts the results by time, and keeps only the most recent entry."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => r["thingId"] == "example:mycar")\n |> filter(fn: (r) => r["_field"] == "value_gps_properties_latitude" or r["_field"] == "value_gps_properties_longitude")\n |> map(fn: (r) => ({ r with _field: strings.replace(v: r["_field"], t: "value_gps_properties_", u: "", i: 2) }))\n |> keep (columns: ["_value", "_field", "_time"])\n |> sort(columns: ["_time"], desc: false) \n |> last() \n')),(0,a.kt)("p",null,"The panel for show the ",(0,a.kt)("strong",{parentName:"p"},"GPS evolution")," also extracts the latitude and longitude data from the digital twin ",(0,a.kt)("em",{parentName:"p"},"example:mycar"),", but keeps all the entries instead of just the last one."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => r["thingId"] == "example:mycar")\n |> filter(fn: (r) => r["_field"] == "value_gps_properties_latitude" or r["_field"] == "value_gps_properties_longitude")\n |> map(fn: (r) => ({ r with _field: strings.replace(v: r["_field"], t: "value_gps_properties_", u: "", i: 2) }))\n |> keep (columns: ["_value", "_field", "_time"])\n')),(0,a.kt)("p",null,"The panel displaying the ",(0,a.kt)("strong",{parentName:"p"},"current direction of wheels")," extracts the direction data of the four twins corresponding to the wheels, identified by starting with ",(0,a.kt)("em",{parentName:"p"},"example:mycar:wheel_"),".\nIt modifies the identifiers of the twins for a more readable display and retains the most recent value based on time."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => strings.hasPrefix(v: r["thingId"], prefix: "example:mycar:wheel_"))\n |> filter(fn: (r) => r["_field"] == "value_direction_properties_value")\n |> map(fn: (r) => ({ r with thingId: strings.replace(v: r["thingId"], t: "example:mycar:", u: "", i: 2) }))\n |> keep (columns: ["thingId", "_value", "_time"])\n |> sort(columns: ["_time"], desc: false) \n |> last()\n')),(0,a.kt)("p",null,"Finally, the panel that makes a ",(0,a.kt)("strong",{parentName:"p"},"wheels velocity comparison")," is similar to the previous one, although extracting the velocity data from the 4 twins instead of the direction and keeping all the entries."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},'import "strings"\nfrom(bucket: "opentwins")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer")\n |> filter(fn: (r) => strings.hasPrefix(v: r["thingId"], prefix: "example:mycar:wheel_"))\n |> filter(fn: (r) => r["_field"] == "value_velocity_properties_value")\n |> map(fn: (r) => ({ r with thingId: strings.replace(v: r["thingId"], t: "example:mycar:", u: "", i: 2) }))\n |> keep (columns: ["thingId", "_value", "_time"])\n')),(0,a.kt)("p",null,"This satisfies the basic requirements to consider a system as a digital twin.\nHowever, to take full advantage of its capabilities, we recommend including other functionalities or additional data sources.\nThis will allow you to obtain a more complete and accurate view of the real system.\nYou can check our ",(0,a.kt)("a",{parentName:"p",href:"./category/guides"},"guides")," for more information"))}h.isMDXComponent=!0},4008:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/children-car-twin-ca2cf91c075a327581d6ba641943742c.png"},7390:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/children-car-d181addcc20bcf1a9a75715cf3353c15.png"},2979:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-digital-twins-bd76130c9ab23c157cdfcd9dcb0fdc5b.png"},9726:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-twin-car-4a9968f0a40982680599c72e2f6d3b23.png"},9636:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-car-1-83399364c854df3e8b229885140649ba.png"},2748:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-car-2-c0b3b0d20c5b4291c77340e8a685d0c6.png"},8775:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-wheel-1-d71b44232fb9dfeebc601958e43d19fd.png"},8226:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/create-type-wheel-2-01daddbffbb636b06904956113d446fc.png"},589:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/grafana-dashboard-d0dbd9d8646a93fa599ba324ce583338.png"},159:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/grafana-datasource-e1e06ed3137227fd650fb894928415db.png"},4347:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/model-car-example-2b119ad662f43ec5caa875a83d9d7b7c.jpg"},8606:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/wheel-data-30d7b695d58b99fcec8f817d5d5b45e1.png"},1845:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/mqtt-explorer-ebaae1985874581cb44c024060bef150.png"}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.cbfafcd9.js b/assets/js/runtime~main.fc78f968.js similarity index 98% rename from assets/js/runtime~main.cbfafcd9.js rename to assets/js/runtime~main.fc78f968.js index c9796a3..24dfa54 100644 --- a/assets/js/runtime~main.cbfafcd9.js +++ b/assets/js/runtime~main.fc78f968.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,c,t,r,f={},d={};function b(e){var a=d[e];if(void 0!==a)return a.exports;var c=d[e]={id:e,loaded:!1,exports:{}};return f[e].call(c.exports,c,c.exports,b),c.loaded=!0,c.exports}b.m=f,b.c=d,e=[],b.O=(a,c,t,r)=>{if(!c){var f=1/0;for(i=0;i=r)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(d=!1,r0&&e[i-1][2]>r;i--)e[i]=e[i-1];e[i]=[c,t,r]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var r=Object.create(null);b.r(r);var f={};a=a||[null,c({}),c([]),c(c)];for(var d=2&t&&e;"object"==typeof d&&!~a.indexOf(d);d=c(d))Object.getOwnPropertyNames(d).forEach((a=>f[a]=()=>e[a]));return f.default=()=>e,b.d(r,f),r},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({53:"935f2afb",114:"908ba98b",788:"b0bae498",799:"c0eb0ada",948:"8717b14a",1144:"40b0d055",1199:"ac75af2e",1368:"4899252d",1683:"26bc6599",1761:"acac1da9",1826:"5a7456e0",1914:"d9f32620",2267:"59362658",2362:"e273c56f",2535:"814f3328",2614:"32191809",2690:"8dd02d2f",3085:"1f391b9e",3089:"a6aa9e1f",3237:"1df93b7f",3514:"73664a40",3608:"9e4087bc",4013:"01a85c17",4283:"5eb90766",4397:"e9a1c9e5",4576:"0598cbc5",4699:"85531627",5041:"aa3c268d",5049:"74876495",5391:"9281dd35",5466:"70066871",5998:"eb58dad4",6103:"ccc49370",6216:"68c71cca",6513:"d72ac48e",6933:"4c455ca7",7063:"ec3c7536",7414:"393be207",7431:"1c4bf583",7557:"928e06c2",7632:"0964aedb",7826:"8ee96214",7918:"17896441",7979:"118e913f",8364:"96e1810e",8610:"6875c492",8636:"f4f34a3a",8906:"1ba72f0d",9003:"925b3f96",9048:"e677b25a",9514:"1be78505",9521:"3c0fcc1c",9534:"3fb959cb",9642:"7661071f",9817:"14eb3368",9850:"dcc71bcc"}[e]||e)+"."+{53:"2f63e229",114:"a7d9cf5e",210:"6e5e9f1a",788:"5ee92163",799:"b281447b",948:"74619e7a",1144:"4e4684a7",1199:"e26b80a9",1368:"b3e32514",1683:"3b7fe1d7",1761:"05f2b0af",1826:"a5334c36",1914:"b045544d",2267:"22a5d353",2362:"c362744b",2529:"cec79ce1",2535:"0c1d9999",2614:"bb78a4f4",2690:"7fc106b4",3085:"8fc7c9b3",3089:"845cad8c",3237:"20e07858",3514:"d45d5659",3608:"472c889f",4013:"75a76f22",4283:"49b2da36",4397:"f065f0d0",4576:"b9f9eae9",4699:"61a331f6",4972:"b60a5582",5041:"11260222",5049:"6d8d7f76",5391:"056d9a3c",5466:"508587da",5998:"f93dfa8b",6103:"d9c41d1e",6216:"704c8102",6513:"cd06afaa",6933:"946c6245",7063:"adf7bfc2",7414:"6ad33c11",7431:"58dbf46d",7557:"fee88172",7632:"f08e1154",7826:"92b136ff",7918:"4f945c03",7979:"b956ff9a",8364:"a859fe5f",8610:"f37b7b5c",8636:"77d55ebe",8906:"aa79666e",9003:"a62a82fe",9048:"4140a2d3",9514:"685933da",9521:"e76176e0",9534:"7f1cc7ba",9642:"77dfe874",9817:"a5d80bcb",9850:"e2c3cca1"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),t={},r="docs:",b.l=(e,a,c,f)=>{if(t[e])t[e].push(a);else{var d,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{d.onerror=d.onload=null,clearTimeout(s);var r=t[e];if(delete t[e],d.parentNode&&d.parentNode.removeChild(d),r&&r.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=l.bind(null,d.onerror),d.onload=l.bind(null,d.onload),o&&document.head.appendChild(d)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/opentwins/",b.gca=function(e){return e={17896441:"7918",32191809:"2614",59362658:"2267",70066871:"5466",74876495:"5049",85531627:"4699","935f2afb":"53","908ba98b":"114",b0bae498:"788",c0eb0ada:"799","8717b14a":"948","40b0d055":"1144",ac75af2e:"1199","4899252d":"1368","26bc6599":"1683",acac1da9:"1761","5a7456e0":"1826",d9f32620:"1914",e273c56f:"2362","814f3328":"2535","8dd02d2f":"2690","1f391b9e":"3085",a6aa9e1f:"3089","1df93b7f":"3237","73664a40":"3514","9e4087bc":"3608","01a85c17":"4013","5eb90766":"4283",e9a1c9e5:"4397","0598cbc5":"4576",aa3c268d:"5041","9281dd35":"5391",eb58dad4:"5998",ccc49370:"6103","68c71cca":"6216",d72ac48e:"6513","4c455ca7":"6933",ec3c7536:"7063","393be207":"7414","1c4bf583":"7431","928e06c2":"7557","0964aedb":"7632","8ee96214":"7826","118e913f":"7979","96e1810e":"8364","6875c492":"8610",f4f34a3a:"8636","1ba72f0d":"8906","925b3f96":"9003",e677b25a:"9048","1be78505":"9514","3c0fcc1c":"9521","3fb959cb":"9534","7661071f":"9642","14eb3368":"9817",dcc71bcc:"9850"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var t=b.o(e,a)?e[a]:void 0;if(0!==t)if(t)c.push(t[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var r=new Promise(((c,r)=>t=e[a]=[c,r]));c.push(t[2]=r);var f=b.p+b.u(a),d=new Error;b.l(f,(c=>{if(b.o(e,a)&&(0!==(t=e[a])&&(e[a]=void 0),t)){var r=c&&("load"===c.type?"missing":c.type),f=c&&c.target&&c.target.src;d.message="Loading chunk "+a+" failed.\n("+r+": "+f+")",d.name="ChunkLoadError",d.type=r,d.request=f,t[1](d)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var t,r,f=c[0],d=c[1],o=c[2],n=0;if(f.some((a=>0!==e[a]))){for(t in d)b.o(d,t)&&(b.m[t]=d[t]);if(o)var i=o(b)}for(a&&a(c);n{"use strict";var e,a,c,t,r,f={},d={};function b(e){var a=d[e];if(void 0!==a)return a.exports;var c=d[e]={id:e,loaded:!1,exports:{}};return f[e].call(c.exports,c,c.exports,b),c.loaded=!0,c.exports}b.m=f,b.c=d,e=[],b.O=(a,c,t,r)=>{if(!c){var f=1/0;for(i=0;i=r)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(d=!1,r0&&e[i-1][2]>r;i--)e[i]=e[i-1];e[i]=[c,t,r]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var r=Object.create(null);b.r(r);var f={};a=a||[null,c({}),c([]),c(c)];for(var d=2&t&&e;"object"==typeof d&&!~a.indexOf(d);d=c(d))Object.getOwnPropertyNames(d).forEach((a=>f[a]=()=>e[a]));return f.default=()=>e,b.d(r,f),r},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({53:"935f2afb",114:"908ba98b",788:"b0bae498",799:"c0eb0ada",948:"8717b14a",1144:"40b0d055",1199:"ac75af2e",1368:"4899252d",1683:"26bc6599",1761:"acac1da9",1826:"5a7456e0",1914:"d9f32620",2267:"59362658",2362:"e273c56f",2535:"814f3328",2614:"32191809",2690:"8dd02d2f",3085:"1f391b9e",3089:"a6aa9e1f",3237:"1df93b7f",3514:"73664a40",3608:"9e4087bc",4013:"01a85c17",4283:"5eb90766",4397:"e9a1c9e5",4576:"0598cbc5",4699:"85531627",5041:"aa3c268d",5049:"74876495",5391:"9281dd35",5466:"70066871",5998:"eb58dad4",6103:"ccc49370",6216:"68c71cca",6513:"d72ac48e",6933:"4c455ca7",7063:"ec3c7536",7414:"393be207",7431:"1c4bf583",7557:"928e06c2",7632:"0964aedb",7826:"8ee96214",7918:"17896441",7979:"118e913f",8364:"96e1810e",8610:"6875c492",8636:"f4f34a3a",8906:"1ba72f0d",9003:"925b3f96",9048:"e677b25a",9514:"1be78505",9521:"3c0fcc1c",9534:"3fb959cb",9642:"7661071f",9817:"14eb3368",9850:"dcc71bcc"}[e]||e)+"."+{53:"2f63e229",114:"a7d9cf5e",210:"6e5e9f1a",788:"5ee92163",799:"b281447b",948:"74619e7a",1144:"4e4684a7",1199:"e26b80a9",1368:"b3e32514",1683:"3b7fe1d7",1761:"05f2b0af",1826:"a5334c36",1914:"b045544d",2267:"22a5d353",2362:"c362744b",2529:"cec79ce1",2535:"0c1d9999",2614:"bb78a4f4",2690:"7fc106b4",3085:"8fc7c9b3",3089:"845cad8c",3237:"20e07858",3514:"d45d5659",3608:"472c889f",4013:"75a76f22",4283:"49b2da36",4397:"f065f0d0",4576:"b9f9eae9",4699:"61a331f6",4972:"b60a5582",5041:"11260222",5049:"cd08c5dc",5391:"056d9a3c",5466:"508587da",5998:"f93dfa8b",6103:"d9c41d1e",6216:"704c8102",6513:"cd06afaa",6933:"946c6245",7063:"adf7bfc2",7414:"6ad33c11",7431:"58dbf46d",7557:"fee88172",7632:"f08e1154",7826:"92b136ff",7918:"4f945c03",7979:"b956ff9a",8364:"a859fe5f",8610:"f37b7b5c",8636:"77d55ebe",8906:"aa79666e",9003:"a62a82fe",9048:"4140a2d3",9514:"685933da",9521:"e76176e0",9534:"7f1cc7ba",9642:"77dfe874",9817:"a5d80bcb",9850:"e2c3cca1"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),t={},r="docs:",b.l=(e,a,c,f)=>{if(t[e])t[e].push(a);else{var d,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{d.onerror=d.onload=null,clearTimeout(s);var r=t[e];if(delete t[e],d.parentNode&&d.parentNode.removeChild(d),r&&r.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=l.bind(null,d.onerror),d.onload=l.bind(null,d.onload),o&&document.head.appendChild(d)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/opentwins/",b.gca=function(e){return e={17896441:"7918",32191809:"2614",59362658:"2267",70066871:"5466",74876495:"5049",85531627:"4699","935f2afb":"53","908ba98b":"114",b0bae498:"788",c0eb0ada:"799","8717b14a":"948","40b0d055":"1144",ac75af2e:"1199","4899252d":"1368","26bc6599":"1683",acac1da9:"1761","5a7456e0":"1826",d9f32620:"1914",e273c56f:"2362","814f3328":"2535","8dd02d2f":"2690","1f391b9e":"3085",a6aa9e1f:"3089","1df93b7f":"3237","73664a40":"3514","9e4087bc":"3608","01a85c17":"4013","5eb90766":"4283",e9a1c9e5:"4397","0598cbc5":"4576",aa3c268d:"5041","9281dd35":"5391",eb58dad4:"5998",ccc49370:"6103","68c71cca":"6216",d72ac48e:"6513","4c455ca7":"6933",ec3c7536:"7063","393be207":"7414","1c4bf583":"7431","928e06c2":"7557","0964aedb":"7632","8ee96214":"7826","118e913f":"7979","96e1810e":"8364","6875c492":"8610",f4f34a3a:"8636","1ba72f0d":"8906","925b3f96":"9003",e677b25a:"9048","1be78505":"9514","3c0fcc1c":"9521","3fb959cb":"9534","7661071f":"9642","14eb3368":"9817",dcc71bcc:"9850"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var t=b.o(e,a)?e[a]:void 0;if(0!==t)if(t)c.push(t[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var r=new Promise(((c,r)=>t=e[a]=[c,r]));c.push(t[2]=r);var f=b.p+b.u(a),d=new Error;b.l(f,(c=>{if(b.o(e,a)&&(0!==(t=e[a])&&(e[a]=void 0),t)){var r=c&&("load"===c.type?"missing":c.type),f=c&&c.target&&c.target.src;d.message="Loading chunk "+a+" failed.\n("+r+": "+f+")",d.name="ChunkLoadError",d.type=r,d.request=f,t[1](d)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var t,r,f=c[0],d=c[1],o=c[2],n=0;if(f.some((a=>0!==e[a]))){for(t in d)b.o(d,t)&&(b.m[t]=d[t]);if(o)var i=o(b)}for(a&&a(c);n Blog | OpenTwins - +

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

Simply add Markdown files (or folders) to the blog directory.

Regular blog authors can be added to authors.yml.

The blog post date can be extracted from filenames, such as:

  • 2019-05-30-welcome.md
  • 2019-05-30-welcome/index.md

A blog post folder can be convenient to co-locate blog post images:

Docusaurus Plushie

The blog supports tags as well!

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

- + \ No newline at end of file diff --git a/blog/archive.html b/blog/archive.html index 83ba9b0..5cb276e 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -5,13 +5,13 @@ Archive | OpenTwins - + - + \ No newline at end of file diff --git a/blog/first-blog-post.html b/blog/first-blog-post.html index 0a71a29..5e37db2 100644 --- a/blog/first-blog-post.html +++ b/blog/first-blog-post.html @@ -5,13 +5,13 @@ First Blog Post | OpenTwins - +

First Blog Post

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

- + \ No newline at end of file diff --git a/blog/long-blog-post.html b/blog/long-blog-post.html index c62247c..2e37d5a 100644 --- a/blog/long-blog-post.html +++ b/blog/long-blog-post.html @@ -5,13 +5,13 @@ Long Blog Post | OpenTwins - +

Long Blog Post

· 3 min read
Endilie Yacop Sucipto

This is the summary of a very long blog post,

Use a <!-- truncate --> comment to limit blog post size in the list view.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

- + \ No newline at end of file diff --git a/blog/mdx-blog-post.html b/blog/mdx-blog-post.html index 2abdc75..c61410d 100644 --- a/blog/mdx-blog-post.html +++ b/blog/mdx-blog-post.html @@ -5,13 +5,13 @@ MDX Blog Post | OpenTwins - +
- + \ No newline at end of file diff --git a/blog/tags.html b/blog/tags.html index da7ad2c..55c4eba 100644 --- a/blog/tags.html +++ b/blog/tags.html @@ -5,13 +5,13 @@ Tags | OpenTwins - +
- + \ No newline at end of file diff --git a/blog/tags/docusaurus.html b/blog/tags/docusaurus.html index 89488b4..65a49cb 100644 --- a/blog/tags/docusaurus.html +++ b/blog/tags/docusaurus.html @@ -5,13 +5,13 @@ 4 posts tagged with "docusaurus" | OpenTwins - +

4 posts tagged with "docusaurus"

View All Tags

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

Simply add Markdown files (or folders) to the blog directory.

Regular blog authors can be added to authors.yml.

The blog post date can be extracted from filenames, such as:

  • 2019-05-30-welcome.md
  • 2019-05-30-welcome/index.md

A blog post folder can be convenient to co-locate blog post images:

Docusaurus Plushie

The blog supports tags as well!

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

- + \ No newline at end of file diff --git a/blog/tags/facebook.html b/blog/tags/facebook.html index 51739e8..e4ebd72 100644 --- a/blog/tags/facebook.html +++ b/blog/tags/facebook.html @@ -5,13 +5,13 @@ One post tagged with "facebook" | OpenTwins - +

One post tagged with "facebook"

View All Tags

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

Simply add Markdown files (or folders) to the blog directory.

Regular blog authors can be added to authors.yml.

The blog post date can be extracted from filenames, such as:

  • 2019-05-30-welcome.md
  • 2019-05-30-welcome/index.md

A blog post folder can be convenient to co-locate blog post images:

Docusaurus Plushie

The blog supports tags as well!

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

- + \ No newline at end of file diff --git a/blog/tags/hello.html b/blog/tags/hello.html index 8426604..c24e814 100644 --- a/blog/tags/hello.html +++ b/blog/tags/hello.html @@ -5,13 +5,13 @@ 2 posts tagged with "hello" | OpenTwins - +

2 posts tagged with "hello"

View All Tags

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

Simply add Markdown files (or folders) to the blog directory.

Regular blog authors can be added to authors.yml.

The blog post date can be extracted from filenames, such as:

  • 2019-05-30-welcome.md
  • 2019-05-30-welcome/index.md

A blog post folder can be convenient to co-locate blog post images:

Docusaurus Plushie

The blog supports tags as well!

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

- + \ No newline at end of file diff --git a/blog/tags/hola.html b/blog/tags/hola.html index 9e77d02..364705c 100644 --- a/blog/tags/hola.html +++ b/blog/tags/hola.html @@ -5,13 +5,13 @@ One post tagged with "hola" | OpenTwins - +

One post tagged with "hola"

View All Tags

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

- + \ No newline at end of file diff --git a/blog/welcome.html b/blog/welcome.html index 4618389..6498768 100644 --- a/blog/welcome.html +++ b/blog/welcome.html @@ -5,13 +5,13 @@ Welcome | OpenTwins - +

Welcome

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

Simply add Markdown files (or folders) to the blog directory.

Regular blog authors can be added to authors.yml.

The blog post date can be extracted from filenames, such as:

  • 2019-05-30-welcome.md
  • 2019-05-30-welcome/index.md

A blog post folder can be convenient to co-locate blog post images:

Docusaurus Plushie

The blog supports tags as well!

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

- + \ No newline at end of file diff --git a/docs/category/examples.html b/docs/category/examples.html index ae38c75..b4aa2c1 100644 --- a/docs/category/examples.html +++ b/docs/category/examples.html @@ -7,14 +7,14 @@ It is recommended using Postman to make all requests but youy can use your own method."> - +
- + \ No newline at end of file diff --git a/docs/category/guides.html b/docs/category/guides.html index d65e196..e573c1a 100644 --- a/docs/category/guides.html +++ b/docs/category/guides.html @@ -5,13 +5,13 @@ Guides | OpenTwins - + - + \ No newline at end of file diff --git a/docs/category/installation.html b/docs/category/installation.html index a891e01..33c2609 100644 --- a/docs/category/installation.html +++ b/docs/category/installation.html @@ -5,13 +5,13 @@ Installation | OpenTwins - + - + \ No newline at end of file diff --git a/docs/category/overview.html b/docs/category/overview.html index deff1f8..12a55e5 100644 --- a/docs/category/overview.html +++ b/docs/category/overview.html @@ -5,13 +5,13 @@ Overview | OpenTwins - + - + \ No newline at end of file diff --git a/docs/category/raspberry-pi.html b/docs/category/raspberry-pi.html index 38cd9b1..190be3b 100644 --- a/docs/category/raspberry-pi.html +++ b/docs/category/raspberry-pi.html @@ -5,13 +5,13 @@ Raspberry Pi | OpenTwins - + - + \ No newline at end of file diff --git a/docs/category/unity-visualization.html b/docs/category/unity-visualization.html index c6a3d7c..d1d6916 100644 --- a/docs/category/unity-visualization.html +++ b/docs/category/unity-visualization.html @@ -5,13 +5,13 @@ Unity visualization | OpenTwins - + - + \ No newline at end of file diff --git a/docs/examples/ball-example.html b/docs/examples/ball-example.html index 26b63a6..4d7d998 100644 --- a/docs/examples/ball-example.html +++ b/docs/examples/ball-example.html @@ -5,13 +5,13 @@ Bouncing ball | OpenTwins - + - + \ No newline at end of file diff --git a/docs/examples/raspberry-example.html b/docs/examples/raspberry-example.html index 0acbdc3..b4dc64c 100644 --- a/docs/examples/raspberry-example.html +++ b/docs/examples/raspberry-example.html @@ -5,7 +5,7 @@ Raspberry | OpenTwins - + @@ -14,7 +14,7 @@ A twin has two main components:

  • attributes. It contains the basic information of the twin, such as the name, location, etc.
  • features. It contains the variables of the twin. Imagine a twin of a sensor that measures humidity and temperature. You will have two features: humidity and temperature. Each feature must contain a field called properties that contains, as its name says, every property of the feature, for example, the value of the temperature and the time the value has been measured.

Once we know wich data will store our twin, it is time to create it. To create a twin, we need to make HTTP requests, we recommend you to use Postman. We need to create a PUT request to the Ditto url with the next pattern and a specific payload.

PUT http://{DITTO_IP}:{PORT}/api/2/things/{nameOfThing}

The payload has the attributes and features of the twin mentioned above. As attributes we have the location, in this case "Spain".

As features we have temperature and humidity. In this case both features has the same properties, value and timestamp, but they dont have to fit.

{
"attributes": {
"location": "Spain"
},
"features": {
"temperature": {
"properties": {
"value": null,
"timestamp": null
}
},
"humidity": {
"properties": {
"value": null,
"timestamp": null
}
}
}
}

Once we have checked that all the data is correct, just click send. You should recieve a 200 code of a correct execution.

To check if the twin has been created properly, just send a GET request to the same url.

GET http://{DITTO_IP}:{PORT}/api/2/things/{nameOfThing}

You should be granted with the schema of the new twin.

Second step. Recieving the data

A digital twin is a copy of a real object or process, but we just have a schema, so we need to feed it with data. To achieve this we can use both the Kafka or MQTT broker that are installed with the platform.

Ditto needs to recieve the data in a specific format called Ditto Protocol, so we need the data to be sent in that format. But don't worry if you recieve the data on other format, Ditto gives us the chance to create a mapping with Javascript to change the format when the data arrives to Ditto(We will always recommend you to send the data on Ditto protocol).

Asuming that we recieve that data in Ditto protocol we can configure the connection with one of the two brokers, Kafka or MQTT. To create a connection you can proceed with the same steps as creating the twins, make a POST request to the url and a payload that contains the connection information.

POST http://{DITTO_IP}:{PORT}/api/2/connections
  {
"name": "{NAME OF THE CONNECTION}",
"connectionType": "kafka",
"connectionStatus": "open",
"uri": "tcp://KAFKA_BROKER_IP",
"sources": [
{
"addresses": [
{"list Of topics to read"}
],
"consumerCount": 1,
"qos": 1,
"authorizationContext": [
"nginx:ditto"
],
"headerMapping": {
"correlation-id": "{{header:correlation-id}}",
"namespace": "{{ entity:namespace }}",
"content-type": "{{header:content-type}}",
"connection": "{{ connection:id }}",
"id": "{{ entity:id }}",
"reply-to": "{{header:reply-to}}"
},
"replyTarget": {
"address": "{{header:reply-to}}",
"headerMapping": {
"content-type": "{{header:content-type}}",
"correlation-id": "{{header:correlation-id}}"
},
"expectedResponseTypes": [
"response",
"error"
],
"enabled": true
}
}
],
"targets": [],
"clientCount": 5,
"failoverEnabled": true,
"validateCertificates": true,
"processorPoolSize": 1,
"specificConfig": {
"saslMechanism": "plain",
"bootstrapServers": "KAFKA_BROKER_IP"
},
"tags": []
}

Once we have checked that all the data is correct, just click send. You should recieve a 200 code of a correct execution.

To check if the twin has been created properly, just send a GET request to the same url adding the if of the new connection

GET http://{DITTO_IP}:{PORT}/api/2/connections/{connectionID}

You should be granted with the information of the connection.

With all this setup, the configuration should be already done, and Ditto should be recieving the data from the broker. If you want to create an example script to send the data, just click on the next link.

- + \ No newline at end of file diff --git a/docs/examples/raspberry-example/sending-data.html b/docs/examples/raspberry-example/sending-data.html index 9d6088e..9ed3c45 100644 --- a/docs/examples/raspberry-example/sending-data.html +++ b/docs/examples/raspberry-example/sending-data.html @@ -5,13 +5,13 @@ Sending data to Ditto | OpenTwins - +

Sending data to Ditto

In this case we will use a Raspberry Pi 3B with Raspbian buster OS connected to a DHT22 temperature and humidity sensor.

Setting up the Raspberry Pi

In the following image the pins of the Raspberry used are shown.

We will use pins 2, 6, 23 and 24.

Obtaining sensor data

To get the data from the sensor it is necessary to install its library.

sudo pip3 install Adafruit_DHT

We can test the operation of the sensor by creating a .py file with the following code (in our case it is called dht_code.py and I have placed it on the desktop).

import Adafruit_DHT
import time

SENSOR_DHT = Adafruit_DHT.DHT22
PIN_DHT = 24

while True:
humedad, temperatura = Adafruit_DHT.read(SENSOR_DHT, PIN_DHT)
if humedad is not None and temperatura is not None:
print("Temp={0:0.1f}C Hum={1:0.1f}%".format(temperatura, humedad))
else:
print("Lecture fails, chech connection");
time.sleep(3);

And we run it as follows:

cd Desktop/
python3 dht_code.py

Installing Mosquitto on Raspberry

To send the data to DITTO we will use MQTT with the Mosquitto broker.

sudo wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
sudo apt-key add mosquitto-repo.gpg.key
cd /etc/apt/sources.list.d/
sudo wget http://repo.mosquitto.org/debian/mosquitto-buster.list
sudo -i
apt-get update
apt-get install mosquitto
apt-get install mosquitto-clients

With this we would already have Mosquitto installed on our Raspberry. To test it we can open two terminals, subscribe to a topic with one and publish to that topic with another.

mosquitto_sub -h localhost -t casa/comedor/temperatura
mosquitto_pub -h localhost -t casa/comedor/temperatura -m "Temperatura: 25ºC"

Configuring Mosquitto on raspberry

If we wanted to try to send and receive messages by MQTT between the raspberry and another device, we would have to configure the following.

  1. From the main route of the Raspberry edit the Mosquitto configuration file.
sudo nano /etc/mosquitto/mosquitto.conf
  1. Write these three lines at the end of the file to enable connections with any IP through port 1883 and configure authentication.
listener 1883 0.0.0.0

password_file /etc/mosquitto/passwd
allow_anonymous true

So that mosquito.conf would look like this:

# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.gz

pid_file /run/mosquitto/mosquitto.pid

persistence true
persistence_location /var/lib/mosquitto/

log_dest file /var/log/mosquitto/mosquitto.log
log_type all
log_timestamp true

include_dir /etc/mosquitto/conf.d

listener 1883 0.0.0.0

password_file /etc/mosquitto/passwd
allow_anonymous true
  1. Save the file with Ctrl-O, Enter and Ctrl-X.
  2. Create a user with password using the following command. Replace USERNAME with the username you want. When you run it, it will ask you to enter a password, which will not be visible while you type it.
sudo mosquitto_passwd -c /etc/mosquitto/passwd USERNAME
  1. Restart Mosquitto with the following command:
sudo systemctl restart mosquitto

Finally, we would have Mosquitto configured to receive and send from other IPs. To do this you have to add -u "USERNAME" and -P "PASSWORD" (including quotes) to the respective command.

For example (in this case being user both the user and the password):

mosquitto_sub -h 192.168.0.27 -u "usuario" -P "usuario" -t "/Raspberry/Sensores/DHT22"

Sending data to MQTT from Raspberry

To work with MQTT in python we will need to make use of Eclipse Paho.

sudo pip3 install paho-mqtt

Now, we will create a .py file that publishes the sensor data in the corresponding topic of MQTT. For this we have adapted the code example exposed in the following link to the DHT22 sensor with the Adafruit_DHT library and the requirements of MQTT.

How to use MQTT in Python (Paho)

In addition, the message sent by MQTT regarding the Ditto Protocol has been made following both the documentation and an example of use.

Things - Create-Or-Modify protocol specification

  • Code to send sensor data to MQTT and Eclipse Ditto
    from paho.mqtt import client as mqtt_client
    import time
    import random
    import Adafruit_DHT
    import json

    #Constants to connect to MQTT
    broker = "IP OF MQTT"
    port = POR OF MQTT
    topic = "telemetry"
    client_id = f'python-mqtt-{random.randint(0, 1000)}'
    username = "raspberry_DHT22_1@ditto"
    password = "password"

    #Constantes para obtener información del sensor
    SENSOR_DHT = Adafruit_DHT.DHT22
    PIN_DHT = 24

    #Constantes para crear el mensaje de Eclipse Ditto
    DITTO_NAMESPACE = "raspberry";
    DITTO_THING_ID = "DHT22_1";

    def connect_mqtt():
    def on_connect(client, userdata, flags, rc):
    if rc == 0:
    print("Connected to MQTT Broker!")
    else:
    print("Failed to connect, return code %d\n", rc)
    # Set Connecting Client ID
    client = mqtt_client.Client(client_id)
    client.username_pw_set(username, password)
    client.on_connect = on_connect
    client.connect(broker, port)
    return client

    def publish(client):
    while True:
    time.sleep(1)
    msg = getValues();
    if msg is not None:
    result = client.publish(topic, msg)
    status = result[0]
    if status == 0:
    print(f"Send '{msg}' to topic '{topic}'")
    else:
    print(f"Failed to send message to topic {topic}")

    def getValues():
    humedad, temperatura = Adafruit_DHT.read(SENSOR_DHT, PIN_DHT)
    if humedad is not None and temperatura is not None:
    temp = "{0:0.1f}".format(temperatura)
    hum = "{0:0.1f}".format(humedad)
    output = "{\"topic\": \""
    output += DITTO_NAMESPACE
    output += "/"
    output += DITTO_THING_ID
    output += "/things/twin/commands/modify\",\"headers\":{\"response-required\":false, \"content-type\":\"application/vnd.eclipse.ditto+json\"},"
    output += "\"path\": \"/features\", \"value\":{"
    output += sensorString("temperature", temp)
    output += ","
    output += sensorString("humidity", hum)
    output += "}}"
    return output
    else:
    print("Failed on lecture, check circuit")
    return None

    def sensorString(name, value):
    return "\"" + name + "\": { \"properties\": { \"value\": " + value + "}}";

    def run():
    client = connect_mqtt()
    client.loop_start()
    publish(client)

    if __name__ == '__main__':
    run()

This code has been saved in a .py file with the name of dht22publisher.py and have saved it on the desktop. To execute it we use:

cd Desktop/
python3 dht22publisher.py
- + \ No newline at end of file diff --git a/docs/examples/string-example.html b/docs/examples/string-example.html index cf501ff..3c14abc 100644 --- a/docs/examples/string-example.html +++ b/docs/examples/string-example.html @@ -5,13 +5,13 @@ String and number | OpenTwins - +

String and number

This is a very simple example of creating a ONE way digital twin for monitoring a device. In this case, the stored information will be a string and a number, both of them have a timestamp asociated.

To create a digital twin, we must first know the schema used by Eclipse Ditto called Ditto Protocol.

- + \ No newline at end of file diff --git a/docs/guides/dt-schema-creation.html b/docs/guides/dt-schema-creation.html index 462aeeb..5ebd694 100644 --- a/docs/guides/dt-schema-creation.html +++ b/docs/guides/dt-schema-creation.html @@ -5,7 +5,7 @@ Create a digital twin | OpenTwins - + @@ -13,7 +13,7 @@

Create a digital twin

The way to interact with Eclipse Ditto and therefore create not only digital twins, but connections, etc. is through http requests and methods. Although the graphical interface of OpenTwins makes it unnecessary to go so low level, the option to communicate directly with Eclipse Ditto is still available.

To create a new digital twin schema using OpenTwins plugin in Grafana just select "Create new twin" button in Twins tab. CreateTwin

- + \ No newline at end of file diff --git a/docs/guides/type-creation.html b/docs/guides/type-creation.html index 3794bfc..efa2a0e 100644 --- a/docs/guides/type-creation.html +++ b/docs/guides/type-creation.html @@ -5,7 +5,7 @@ Create a type | OpenTwins - + @@ -13,7 +13,7 @@

Create a type

The way to interact with Eclipse Ditto and therefore create not only digital twins, but connections, etc. is through http requests and methods. Although the graphical interface of OpenTwins makes it unnecessary to go so low level, the option to communicate directly with Eclipse Ditto is still available.

As explained in TWINS WIP, OpenTwins has two types of DT schemas. One for creating a single DT and other for creating a type to create multiple instances of a DT.

To create a new DT type using OpenTwins plugin in Grafana, just select "Create new type in" button in "Types" tab. CreateType

A new window with a form that will define the DT and a viewer of the produced JSON schema will have appeared.

The first required information is the identification of the twin. There are two required field.

  • Namespace: Is the name of the context to which the type belongs.
  • ID: This must be unique within the scope of the type. The name of the type will precede it automatically.

Identification

Next is type information. This basic static information about the type for description. There are several fields, but just one is required:

  • Policy* : We must select a policy.
  • Name.
  • Description.
  • Image: You can paste a image url to show in the type information.

Information

In addition to the above information, new custom attributes can be defined, normally used as static information. By simply filling in the attribute name and its value, click on the "add" button to add a new attribute.

Attributes

Finally, the features section is used to create the variables to be collected by the DT. Simply type the name and click on the "add" button. This will add a new variable to the twin schema.

Features

An example of a schema of a DT of an abstract vehicle can be seen in the following JSON:

{
"thingId": "benchmark:vehicle",
"policyId": "default:basic_policy",
"attributes": {
"name": "Vehicle",
"description": "Vehicle type for generating new vehicles.",
"image": "ImageLink",
"Brand": "EMPTY",
"Subtype": "EMPTY"
},
"features": {
"wheels": {
"properties": {
"value": null
}
},
"power": {
"properties": {
"value": null
}
},
"capacity": {
"properties": {
"value": null
}
}
}
}

- + \ No newline at end of file diff --git a/docs/guides/unity/creating-unity-build.html b/docs/guides/unity/creating-unity-build.html index e343058..2e4cf4a 100644 --- a/docs/guides/unity/creating-unity-build.html +++ b/docs/guides/unity/creating-unity-build.html @@ -5,7 +5,7 @@ Creation of Unity WebGL build | OpenTwins - + @@ -18,7 +18,7 @@ First step is to create a .jslib file inside Assets/Plugins/WebGL, where Assets is our project Assets folder. It is very important that the folders have these names (if one does not exist, it is necessary to create it) because otherwise the plugin will not work. The following script must be added into the created file:


mergeInto(LibraryManager.library, {
GetData: function (deviceId) {
dispatchReactUnityEvent(
"GetData",
Pointer_stringify(deviceId));
}
});

Once created the .jslib file, we need to add the following code to a script in Unity inside a class but outside Start() and Update() funcions:

#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void GetData(string deviceId);
#else
private static void GetData(string deviceId){
Debug.Log("ERROR");
}
#endif

By adding it, we are enabling the execution of events just by calling GetData() function. For example the following script, sends the name of the clicked item in Unity as event to be catched by Grafana:


using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

public class ObjectClicker : MonoBehaviour
{
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void GetData(string deviceId);
#else
private static void GetData(string deviceId){
Debug.Log("ERROR");
}
#endif

// Update is called once per frame
void Update(){
if (Input.GetMouseButtonDown(0)){
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

if (Physics.Raycast(ray, out hit, 100.0f)){
if (hit.transform != null){
GameObject go = hit.transform.gameObject;
SendEventWithId(go);
}
}
}
}

void SendEventWithId(GameObject go){
if(go != null){
Debug.Log("Nombre del objeto: " + go.name);
GetData(go.name);
Debug.Log("Mensaje enviado");
}
else{
Debug.Log("nulo");
}
}
}

- + \ No newline at end of file diff --git a/docs/guides/unity/using-grafana-plugin.html b/docs/guides/unity/using-grafana-plugin.html index eded4bf..92ab0d2 100644 --- a/docs/guides/unity/using-grafana-plugin.html +++ b/docs/guides/unity/using-grafana-plugin.html @@ -5,13 +5,13 @@ Using Grafana Unity Plugin | OpenTwins - + - + \ No newline at end of file diff --git a/docs/installation/manual.html b/docs/installation/manual.html index 8f40928..45a785c 100644 --- a/docs/installation/manual.html +++ b/docs/installation/manual.html @@ -5,13 +5,13 @@ Manual | OpenTwins - +

Manual

danger

The documentation of this method is being written right now. We recommend using helm installation.

This section will explain how to deploy the platform manually. Basically, you will have to deploy or install the different components and then connect them. The procedure explained below is the one followed to deploy them in Kubernetes using in most cases the Helm option, but any other installation in which all the components are correctly installed and there is some kind of network between them to be able to communicate can be used.

It is not necessary to deploy all components if not all functionalities are to be used. Check the architecture section to find out which ones are essential and what functionality is covered by each of them.

Essential functionality

Deploy

tip

Note that the values files have the variables that we recommend for the installation of each Helm Chart, but they can be extended or modified according to your needs (please consult the Helm Chart documentation for each component).

We recommend installing all components in the same Kubernetes namespace to make it easier to identify and control them all. In our case the namespace will be opentwins.

kubectl create namespace opentwins

We installed all the components with their Helm versions and kept most of the values in their default configuration, except for those that are important for the interconnection of the components. In addition, we configure the services as NodePort to facilitate external access and set a specific port for each one.

danger

Depending on how you have persistence configured in your cluster, you may need to deploy persistent volumes for MongoDB, InfluxDB and Grafana. The values for MongoDB are shown below, but they all follow the same template.

apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-opentwins-mongodb
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 8Gi
hostPath:
path: /mnt/opentwins/mongodb
type: DirectoryOrCreate

Listed below are the essential components of the architecture along with their versions used, their Helm values and a link to the repository explaining their installation.

MongoDB v6.0

helm install mongodb -n opentwins oci://registry-1.docker.io/bitnamicharts/mongodb --version 13.8.3 -f values.yaml
values.yaml
service:
type: NodePort
nodePorts:
mongodb: 30717
persistence:
enabled: true
volumePermissions:
enabled: true
auth:
enabled: false

Eclipse Ditto v3.3

helm install --dependency-update -n opentwins ditto oci://registry-1.docker.io/eclipse/ditto --version 3.3.7 --wait -f values.yaml
danger
  • We advise not to modify any authentication configuration due to a bug in Eclipse Ditto that may cause access errors.
  • In the following values you have to replace mongodb-service-name by the MongoDB service name
values.yaml
global:
hashedBasicAuthUsers: false
basicAuthUsers:
ditto:
user: ditto
password: ditto
devops:
user: devops
password: foobar
nginx:
service:
type: NodePort
nodePort: 30525
swaggerui:
enabled: false
dittoui:
enabled: false
mongodb:
enabled: false
dbconfig:
policies:
uri: 'mongodb://<mongodb-service-name>:27017/ditto'
things:
uri: 'mongodb://<mongodb-service-name>:27017/ditto'
connectivity:
uri: 'mongodb://<mongodb-service-name>:27017/ditto'
thingsSearch:
uri: 'mongodb://<mongodb-service-name>:27017/ditto'
gateway:
config:
authentication:
enablePreAuthentication: true
devops:
devopsPassword: foobar
statusPassword: foobar

InfluxDB v2

helm repo add influxdata https://helm.influxdata.com/
helm repo update
helm install -n opentwins influxdb influxdata/influxdb2 --version 2.1.1 -f values.yaml
values.yaml
persistence:
enabled: true
service:
type: NodePort
nodePort: 30716
image:
pullPolicy: Always

Mosquitto v2.0

tip

OpenTwins supports the use of Mosquitto and Kafka as intermediaries, but we recommend using Mosquitto due to its simpler configuration. Since there is no official Helm chart for Mosquitto, we have created one of our own that works fine, although there is no documentation yet. However, you can install Mosquitto in any of the available ways.

helm repo add ertis https://ertis-research.github.io/Helm-charts/
helm repo update
helm install mosquitto ertis/mosquitto -n opentwins --wait --dependency-update -f values.yaml
values.yaml
service:
type: NodePort
nodePort: 30511
configuration:
authentication:
enabled: false

Apache Kafka v3.4

helm install kafka oci://registry-1.docker.io/bitnamicharts/kafka --version 22.0.0 -f values.yaml
values.yaml
autoCreateTopicsEnable: true

Grafana v9.5

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install grafana grafana/grafana -n opentwins --version 6.56.1 -f values.yaml
values.yaml
persistence:
enabled: true
service:
type: NodePort
nodePort: 30718
grafana.ini:
plugins:
plugin_admin_enabled: true
allow_loading_unsigned_plugins: 'ertis-opentwins,ertis-unity-panel'
extraInitContainers:
- name: install-opentwins-plugins
image: busybox
command:
- /bin/sh
- -c
- |
#!/bin/sh
set -euo pipefail
mkdir -p /grafana-storage/plugins
cd /grafana-storage/plugins
wget --no-check-certificate -O ertis-opentwins.zip https://github.com/ertis-research/opentwins-in-grafana/releases/download/latest/ertis-opentwins.zip
unzip -o ertis-opentwins.zip
rm ertis-opentwins.zip
wget --no-check-certificate -O ertis-unity-panel.zip https://github.com/ertis-research/grafana-panel-unity/releases/download/latest/ertis-unity-panel.zip
unzip -o ertis-unity-panel.zip
rm ertis-unity-panel.zip
volumeMounts:
- name: storage
mountPath: /grafana-storage

Eclipse Hono v2.4

danger

This component is completely optional. We maintain support for its connection to OpenTwins, but we do not recommend its use. For a large number of devices or messages it increases considerably the latency of the platform.

helm repo add eclipse-iot https://eclipse.org/packages/charts
helm repo update
helm install hono eclipse-iot/hono -n opentwins -f values.yaml --version=2.5.5
values.yaml
prometheus:
createInstance: false
grafana:
enabled: false
useLoadBalancer: false
probes:
livenessProbe:
initialDelaySeconds: 900
readinessProbe:
initialDelaySeconds: 45
messagingNetworkTypes:
- amqp
kafkaMessagingClusterExample:
enabled: false
amqpMessagingNetworkExample:
enabled: true
deviceRegistryExample:
type: mongodb
addExampleData: false
mongoDBBasedDeviceRegistry:
mongodb:
host: '{{ .Release.Name }}-mongodb'
port: 27017
dbName: hono
hono:
registry:
http:
insecurePortEnabled: true
adapters:
mqtt:
hono:
mqtt:
insecurePortEnabled: true
http:
hono:
http:
insecurePortEnabled: true
amqp:
hono:
amqp:
insecurePortEnabled: true

Connect

tip

Check architecture to see which connections you need to set up

Eclipse Ditto and InfluxDB

The process to connect Eclipse Ditto and InfluxDB will depend on Mosquitto or Apache Kafka. Choose the option you have selected in each step.

  1. You have to add an output connection in Eclipse Ditto that publishes the events of the twins in the intermediary. This is done with a POST request to the URL http://DITTO_NGINX_URL/api/2/connections with the following body and the basic credentials: user "devops" and password "foobar". Remember to replace DITTO_NGINX_URL by a URL that allows access to the Eclipse Ditto Nginx service, you can check how to do it here.

    You can check if the connection is working properly by reading the opentwins topic in the selected broker with some tool or script and sending updates to some twin in Ditto Protocol format. To create the twin check here and to see an example of an update message check here.

danger

Change MOSQUITTO_SERVICE_NAME to the name of the Mosquitto service. You can check it with kubectl get services.

PUT http://DITTO_NGINX_URL/api/2/connections
{
"name": "mosquitto-target-connection",
"connectionType": "mqtt-5",
"connectionStatus": "open",
"uri": "tcp://MOSQUITTO_SERVICE_NAME:1883",
"clientCount": 1,
"failoverEnabled": true,
"sources": [],
"targets": [
{
"address": "opentwins/{{ topic:channel }}/{{ topic:criterion }}/{{ thing:namespace }}/{{ thing:name }}",
"topics": [
"_/_/things/twin/events?extraFields=thingId,attributes/_parents,features/idSimulationRun/properties/value",
"_/_/things/live/messages",
"_/_/things/live/commands"
],
"qos": 1,
"authorizationContext": [
"nginx:ditto"
]
}
]
}
  1. Now we will need to obtain a token in InfluxDB with write permissions. We will then access from a browser to the InfluxDB interface and create an opentwins organization. Then, follow the instructions in their documentation to create an API token in the organization. Save this token because we will use it next.

  2. An instance of Telegraf must be deployed to read the events written in the intermediary broker and write them to the database. For this we will use the Telegraf Helm and add the necessary configuration in its values. You can check to Telegraf v1 documentation for both the application and Helm for more information.

    The commands to deploy it are the following, using the necessary values file in each case.

helm repo add influxdata https://helm.influxdata.com/
helm repo update
helm install -n opentwins telegraf influxdata/telegraf -f values.yaml --version=1.8.27 --set tplVersion=2
values.yaml
service:
enabled: false
config:
agent:
debug: true
processors:
- rename:
replace:
- tag: "extra_attributes__parents"
dest: "parent"
- tag: "headers_ditto-originator"
dest: "originator"
- tag: "extra_features_idSimulationRun_properties_value"
dest: "idSimulationRun"
- tag: "extra_thingId"
dest: "thingId"
outputs:
- influxdb_v2:
urls:
- "http://INFLUX_SERVICE_NAME:INFLUX_PORT"
token: "INFLUXDB_TOKEN"
organization: "opentwins"
bucket: "default"
inputs:
- mqtt_consumer:
servers:
- "tcp://MOSQUITTO_SERVICE_NAME:1883"
topics:
- "opentwins/#"
qos: 1
tag_keys:
- "extra_attributes__parents"
- "extra_thingId"
- "headers_ditto-originator"
- "extra_features_idSimulationRun_properties_value"
- "value_time_properties_value"
data_format: "json"
metrics:
internal:
enabled: false

With this Eclipse Ditto and InfluxDB should be connected. You can check this by sending update messages to Eclipse Ditto and verifying if they are correctly written to the InfluxDB bucket. If not, check if the messages are arriving correctly to the intermediate broker and, if so, check the logs of the Telegraf pod to see if there is any error in the configuration (usually connection problems).

InfluxDB and Grafana

  1. Obtain a read access token in InfluxDB for Grafana.
  2. Access Configuration > Data sources on the Grafana interface and click on Add data source.
  3. Select InfluxDB from the list. In the setup form it is very important to select Flux as query language. It will be necessary to fill in the URL section with the one that corresponds to InfluxDB service. You will also have to activate Auth Basic and fill in the fields (in our case we have set the default admin of InfluxDB, but you can create a new user and fill in these fields). In the InfluxDB details you should indicate the organization, the bucket (default is default) and the token you have generated.
  4. When saving and testing, it should come out that at least one bucket has been found, indicating that they are already connected.

Eclipse Ditto and Eclipse Hono

In the following diagram you can see how Eclipse Hono and Eclipse Ditto are related in OpenTwins.

Ditto and Hono relationship

Basically, you will need to create a connection between both for each Eclipse Hono tenant you want to use. Tenants basically act as device containers, so you could simply create a single tenant connected to Eclipse Ditto and store all the devices you need there. In this case we will do it this way, but you could create as many tenants and connections as your needs require.

The first thing to do is to check the IPs and ports to use with kubectl get services -n $NS. At this point we are interested in the dt-service-device-registry-ext and dt-ditto-nginx services, which correspond to Eclipse Hono and Eclipse Ditto respectively (if you have followed these instructions and services are NodePort, you will have to use port 3XXXX).

We will then create a Hono tenant called, for example, ditto (you must override the variable HONO_TENANT if you have chosen another name).

HONO_TENANT=ditto
curl -i -X POST http://$HONO_IP:$HONO_PORT/v1/tenants/$HONO_TENANT

Now we will create the connection from Eclipse Ditto, which will act as a consumer of the AMQP endpoint of that tenant. To do this you will need to know the Eclipse Ditto devops password with the following command (the variable RELEASE is the name we gave to the Helm release when installing cloud2edge, if you have followed these instructions it should be dt).

RELEASE=dt
DITTO_DEVOPS_PWD=$(kubectl --namespace ${NS} get secret ${RELEASE}-ditto-gateway-secret -o jsonpath="{.data.devops-password}" | base64 --decode)

Now we create the connection from Eclipse Ditto with the following command.

curl -i -X POST -u devops:${DITTO_DEVOPS_PWD} -H 'Content-Type: application/json' --data '{
"targetActorSelection": "/system/sharding/connection",
"headers": {
"aggregate": false
},
"piggybackCommand": {
"type": "connectivity.commands:createConnection",
"connection": {
"id": "hono-connection-for-'"${HONO_TENANT}"'",
"connectionType": "amqp-10",
"connectionStatus": "open",
"uri": "amqp://consumer%40HONO:verysecret@'"${RELEASE}"'-dispatch-router-ext:15672",
"failoverEnabled": true,
"sources": [
{
"addresses": [
"telemetry/'"${HONO_TENANT}"'",
"event/'"${HONO_TENANT}"'"
],
"authorizationContext": [
"pre-authenticated:hono-connection"
],
"enforcement": {
"input": "{{ header:device_id }}",
"filters": [
"{{ entity:id }}"
]
},
"headerMapping": {
"hono-device-id": "{{ header:device_id }}",
"content-type": "{{ header:content-type }}"
},
"replyTarget": {
"enabled": true,
"address": "{{ header:reply-to }}",
"headerMapping": {
"to": "command/'"${HONO_TENANT}"'/{{ header:hono-device-id }}",
"subject": "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response",
"correlation-id": "{{ header:correlation-id }}",
"content-type": "{{ header:content-type | fn:default('"'"'application/vnd.eclipse.ditto+json'"'"') }}"
},
"expectedResponseTypes": [
"response",
"error"
]
},
"acknowledgementRequests": {
"includes": [],
"filter": "fn:filter(header:qos,'"'"'ne'"'"','"'"'0'"'"')"
}
},
{
"addresses": [
"command_response/'"${HONO_TENANT}"'/replies"
],
"authorizationContext": [
"pre-authenticated:hono-connection"
],
"headerMapping": {
"content-type": "{{ header:content-type }}",
"correlation-id": "{{ header:correlation-id }}",
"status": "{{ header:status }}"
},
"replyTarget": {
"enabled": false,
"expectedResponseTypes": [
"response",
"error"
]
}
}
],
"targets": [
{
"address": "command/'"${HONO_TENANT}"'",
"authorizationContext": [
"pre-authenticated:hono-connection"
],
"topics": [
"_/_/things/live/commands",
"_/_/things/live/messages"
],
"headerMapping": {
"to": "command/'"${HONO_TENANT}"'/{{ thing:id }}",
"subject": "{{ header:subject | fn:default(topic:action-subject) }}",
"content-type": "{{ header:content-type | fn:default('"'"'application/vnd.eclipse.ditto+json'"'"') }}",
"correlation-id": "{{ header:correlation-id }}",
"reply-to": "{{ fn:default('"'"'command_response/'"${HONO_TENANT}"'/replies'"'"') | fn:filter(header:response-required,'"'"'ne'"'"','"'"'false'"'"') }}"
}
},
{
"address": "command/'"${HONO_TENANT}"'",
"authorizationContext": [
"pre-authenticated:hono-connection"
],
"topics": [
"_/_/things/twin/events",
"_/_/things/live/events"
],
"headerMapping": {
"to": "command/'"${HONO_TENANT}"'/{{ thing:id }}",
"subject": "{{ header:subject | fn:default(topic:action-subject) }}",
"content-type": "{{ header:content-type | fn:default('"'"'application/vnd.eclipse.ditto+json'"'"') }}",
"correlation-id": "{{ header:correlation-id }}"
}
}
]
}
}
}' http://$DITTO_IP:$DITTO_PORT/devops/piggyback/connectivity

This connection is configured so that if an Eclipse Hono device has the ThingId of an Eclipse Ditto twin as its identifier, its messages will be redirected to that twin directly (explained in more detail in the usage section).

Compositional support

Data prediction with machine learning

3D representation

- + \ No newline at end of file diff --git a/docs/installation/requirements.html b/docs/installation/requirements.html index de95611..33763c0 100644 --- a/docs/installation/requirements.html +++ b/docs/installation/requirements.html @@ -5,13 +5,13 @@ Requirements | OpenTwins - +
- + \ No newline at end of file diff --git a/docs/installation/using-helm.html b/docs/installation/using-helm.html index 015562b..840ea45 100644 --- a/docs/installation/using-helm.html +++ b/docs/installation/using-helm.html @@ -5,14 +5,14 @@ Helm | OpenTwins - +

Helm

Standard version

Installation

First of all, you have to add ERTIS Research group helm repository to your helm repository list:

helm repo add ertis https://ertis-research.github.io/Helm-charts/

Once done, the next step is installing the chart by executing this line on your terminal (in our case, we will use opentwins as release name and opentwins as namespace, but you can choose the one that you prefeer). To customize the installation, please refer to Helm's values file.

danger

We recommend to modify the default passwords and tokens for Grafana and InfluxDB before deploying the platform. Currently, to avoid potential problems, we do not recommend changing Eclipse Ditto's username and password.

helm upgrade --install opentwins ertis/OpenTwins --wait --dependency-update --debug

After waiting some time, the installation will be ready for use.

Configuration

If you have kept the default values of the Helm chart, you will only need to configure the OpenTwins interface plugin for Grafana.

Obtain external URLs for Eclipse Ditto, Ditto extended API and Grafana.

Get the name of the services with kubectl get services. The result will look something similar to the following image. If you have changed the name of the release, the names will not be preceded by opentwins, but by the name you have assigned. We are interested in the services opentwins-grafana, opentwins-ditto-nginx and opentwins-ditto-extended-api.

Kubectl get services

The method to obtain the URL may vary depending on the configuration of your cluster. Generally, the URL for each service will match the cluster IP and the NodePort (the number after the colon). For example, if our cluster IP is 192.168.32.25, the URL for Grafana would be 192.168.32.25:30718.

Are you using Minikube to deploy OpenTwins?

As Minikube is a local cluster, you cannot directly use the IP of the cluster. Therefore, you will have to expose the services that you want to use externally with a command.

Open three terminals, one for each service, and run the following command on each terminal with a different service name. These will return a URL of your localhost with a port that will forward all traffic to the specified service. These are the URLs you should use.

minikube service <service-name> --url

Add URLs to OpenTwins plugin configuration

Access Grafana in any browser with the URL you have obtained. The credentials must match those indicated in the Helm values, which by default are user admin and password admin.

Access the left drop-down menu and select Administration > Plugins. Once there, find the OpenTwins plugin and activate it by clicking Enable. Then, go to the Configuration tab where you will need to enter the Eclipse Ditto and Extended API URLs in the corresponding fields. Use ditto for both the Eclipse Ditto username and password for the moment. Then click on Save settings to complete the plugin configuration.

note

If you are using the latest version of the interface, you may find two fields intended for an agent service. This functionality is currently under development and is not yet available, so leave them empty and disregard them for now.

Find the available application in the App > OpenTwins section of the left drop-down menu.

You can now start using OpenTwins.

Screenshots
Plugin
Configuration
Configuration

Lightweight version

OpenTwins has it's own lightweight version that aims to run on IoT devices such as Raspberry Pi devices. To install this versión, you have to follow the first step in order to add ERTIS repository to your repository list and then install the platform using the command bellow:

helm install ot ertis/OpenTwins-Lightweight -n opentwins

In this case connections still need to be made for the platform to work properly.

- + \ No newline at end of file diff --git a/docs/overview/architecture.html b/docs/overview/architecture.html index fca0ff0..64d8be3 100644 --- a/docs/overview/architecture.html +++ b/docs/overview/architecture.html @@ -5,14 +5,14 @@ Architecture | OpenTwins - +

Architecture

OpenTwins is built on a open source microservices architecture, designed to enhance scalability, flexibility and efficiency in the development, extension, deployment and maintenance of the platform. All the components that make up this architecture are encapsulated in Docker containers, ideally managed through Kubernetes, which ensures efficient portability and management.

note

Although it is possible to deploy and connect the different components without containerization, this approach is not recommended due to the difficulties involved in terms of installation and management. However, it is important to note that OpenTwins could be manually connected to non-containerized components, such as a local instance of Grafana.

The following image illustrates the current architecture of OpenTwins, in which each color of the boxes represents the functionality covered by each component. Most of these components are external projects to our organization, however, we also include certain services specifically designed to enrich the functionality of the platform. Both the code and documentation of the components are available in their respective repositories.

Architecture

Essential functionality

The elements highlighted in blue form the heart of OpenTwins, as they provide the essential functionalities of a digital twin development platform: the definition of digital twins, the connection to IoT devices, the storage of information and the user-friendly visualisation of data. The tools used in this case include:

  • Eclipse Ditto. This is the core component of OpenTwins, an open-source framework for digital twins developed by the Eclipse Foundation. Eclipse Ditto provides an abstract entity "Thing", which allows describing digital twins through JSON schemas that include both static and dynamic data of the entity. The framework stores the current state of the "Thing" entity and facilitates its connection to input and output data sources through various IoT protocols. In a typical scenario, the Thing entity will update its information via a source connection, generating events that are sent to the indicated target connections. In addition, the tool provides an API that allows querying the current state of the entity and managing its schema and connections.

  • Eclipse Hono. This component facilitates the reception of data through various IoT protocols and centralizes it into a single endpoint, either AMQP 1.0 or Kafka. This output connects directly to Eclipse Ditto, eliminating the need for users to manually connect to an external broker to extract data. This allows the platform to receive data through the most common IoT protocols, giving devices the flexibility to connect to the most appropriate protocol for their particular case.

    danger

    Despite its advantages, we have observed that Eclipse Hono does not scale correctly when the message frequency is high, so we do not recommend its use in these cases. For this reason, or if it is not necessary to offer different input protocols, you can choose to connect Eclipse Ditto to one or more specific messaging brokers, such as Mosquitto or RabbitMQ.

  • MongoDB. This tool is the internal database used by Eclipse Hono and Eclipse Ditto. Eclipse Ditto stores data about the current state of digital twins ("things"), policies, connections and recent events, while Eclipse Hono stores information about defined devices and groups.

  • InfluxDB v2. This database provides an optimized architecture for time series, which guarantees superior performance in storing and querying digital twin data. Its high scalability and simplicity of use allow it to efficiently handle large volumes of data, facilitating the integration and analysis of information in real time. In addition, it is one of the most popular options in the field of the Internet of Things (IoT), generating an active community that consolidates its position as a robust solution.

  • Telegraf. This server-based agent for collecting and sending metrics offers easy configuration and a wide range of plugins to integrate various data sources and destinations. It is the recommended choice for data ingestion into InfluxDB. Its role in the platform is to capture digital twin updates, presented as Eclipse Ditto events, processing the data as time series for storage in the database.

  • Apache Kafka or Eclipse Mosquitto. An intermediary messaging broker is required for Telegraf to collect event data from Eclipse Ditto, since none of these technologies provide this role and do not have a direct connection. For this purpose, any messaging broker where Eclipse Ditto can publish and read Telegraf is valid. The options available on the platform include Apache Kafka, known for its scalability and error tolerance in processing large volumes of data, and Mosquitto, recognized for its efficiency in messaging and its flexibility in IoT environments.

  • Grafana. This solution acts as the platform's main front-end, providing a highly adaptable data visualization that allows users to create intuitive and easily understandable dashboards. Its ability to integrate with a wide variety of data sources and its active community of users and developers make it a powerful tool for monitoring and analyzing complex systems, such as digital twins. In addition, it allows users to expand its functionality by creating custom plugins, giving them the ability to integrate new visualizations, use-case specific panels and connectors to additional data sources.

Compositional support

The composition of digital twins represents one of the main contributions of this platform, distinguishing it from other similar solutions. In addition, OpenTwins provides the ability to define and compose "types" of digital twins, making development simpler. The services marked in green in the architecture are responsible for integrating these functionalities.

  • Extended API for Eclipse Ditto. The Thing entity provided by Eclipse Ditto must follow a specific JSON schema, although it offers great flexibility within it. Our goal is to simplify type definition and entity composition by taking advantage of the flexibility of this schema. This "extended API" acts as a layer on top of the Eclipse Ditto API, distinguishing between the management of twins and types, and applying all the necessary constraints and checks to ensure the composition of these entities according to the constraints imposed by each (types form graphs, while twins form trees).

  • OpenTwins app plugin for Grafana. To have a pleasant and usable platform for as many users as possible, it is important to have a simple interface capable of performing the functionalities available. Therefore, an app plugin is included for Grafana that uses the extended API to query and manage twins, types and their composition in a user-friendly way. Moreover, this approach keeps the entire platform front-end within a single tool, making it easy to use and accessible.

Data prediction with machine learning

The architecture highlights in yellow the components that facilitate the integration of digital twins with Machine Learning models. Providing this support represents a crucial aspect in the development of a digital twin, since it provides a complementary or comparative perspective with real data, enriching the understanding of the replicated object. To achieve this goal, the following tools are used:

  • Kafka-ML. This open source framework manages the lifecycle of ML/AI applications in production environments through continuous data streams. Unlike traditional frameworks that work with static data sets, Kafka-ML enables both training and inference with continuous data streams, allowing users to have fine-grained control over ingestion data. Currently, Kafka-ML is compatible with the most popular ML frameworks, TensorFlow and PyTorch, and enables the management and deployment of ML models, from definition to final deployment for inference. This component operates as a black box in OpenTwins, receiving input data for a deployed model through a Kafka topic and sending the predicted result to another topic, which is connected to Eclipse Ditto in a way that updates to the corresponding digital twin.

  • Eclipse Hono to Kafka-ML. Kafka-ML can receive input data from any source that is able to publish to a Kafka topic. However, at OpenTwins we have decided to simplify this process when the data comes from Eclipse Hono. Therefore, we have developed an optional service that automates the data feed of ML models deployed in Kafka-ML. This service automatically sends the data needed to make a prediction every time a new data is received from any of the devices required by the model. To use this tool, we provide an API that allows you to specify which devices should be taken into account, what data is required from these devices and how they should be formatted to work correctly as input for Kafka-ML.

  • Error detection for Eclipse Hono with Kafka-ML. An ML model useful in the construction of a digital twin is one capable of generating data that a sensor should produce in case it loses connection or experiences a failure. To automate this, we have developed an optional service with similar functionalities to the one mentioned above, but with an important particularity: it will only invoke the model when an interruption in data reception by the device is detected. This service takes into account the frequency with which the data is emitted by the device. As soon as an anomaly is identified, the service will format and send the last data received following the expected frequency until the connection is restored and real data is received again. In this way, the normal operation of the device is simulated, ensuring continuity of information for the digital twin.

3D representation

note

Although Unity is not open source, at the moment it is the only graphics engine we have tested the plugin with. However, it could be replaced by any other graphics engine that compiles in WebGL, such as Godot, since the plugin is expected to be able to render any WebGL compilation. However, since we have not yet tested with other graphics engines, we cannot guarantee this.

Although it may seem a secondary aspect, the representation of the digital twin makes a major difference in its utility and adoption. An intuitive and attractive visual interface for users will facilitate the understanding and accessibility of the data received, which simplifies the optimization of the actual system. 3D representations stand out as one of the most effective options in this sense, which motivates most private digital twin platforms to offer support for them. For this reason, OpenTwins allows adding an interactive 3D representation of the digital twin that reacts dynamically to data received from any source (real, predicted or simulated). The components highlighted in red are the ones that enable this functionality.

  • Unity This powerful graphics engine, versatile in both 3D and 2D development, is widely used in the creation of video games, simulations and engineering. It is recognized as one of the most popular, supported by a large and active community. Although it is not open source software, it can be used free of charge for personal or low-profit projects. The technology of this engine allows to assign behaviors to 3D objects by means of scripts, which facilitates interaction both with the user and with other elements of the environment.

    info

    Unity offers a wide range of formats for importing 3D models, but we emphasize its integration with Blender, an open source 3D creation suite that is equally popular and supported by an active community.

    On the other hand, Unity provides the possibility to compile projects in the Unity WebGL format, perfect for web rendering. This option is the choice of OpenTwins, as it allows easy integration with existing web technologies, ensuring accessibility and distribution without the need for additional installations.

  • Unity panel plugin for Grafana. Since Grafana serves as the front-end of OpenTwins, it is convenient that the 3D representations are embedded directly into this tool, providing a unified management and visualization of the digital twin. To achieve this, a plugin capable of rendering WebGL compilations within a Grafana panel has been developed. This plugin is able to send the digital twin data from Grafana to the compilation, allowing it to influence the rendering in real time. In addition, this plugin enables direct user interaction with the 3D model, allowing actions on 3D elements to affect other panels of the dashboard. For example, by clicking on a 3D element, another Grafana panel can automatically display data related to it.

- + \ No newline at end of file diff --git a/docs/overview/concepts.html b/docs/overview/concepts.html index e6c0b02..336d818 100644 --- a/docs/overview/concepts.html +++ b/docs/overview/concepts.html @@ -5,13 +5,13 @@ Concepts | OpenTwins - +

Concepts

In this section, we will explore in depth the concept of a digital twin as defined by the platform. We will detail the information it can contain, explain the idea of a "digital twin type", and discuss how the composition works.

Digital twin definition

In the platform, a digital twin is defined as a replica of a real entity, whether tangible or not. This replica can be considered as an enhancement to monitoring the entity because, although it is not strictly necessary to be classified as a digital twin, it is beneficial to connect the real data of the entity with those generated by means of mathematical simulations or artificial intelligence. In this way, the digital twin becomes a central point that integrates all available sources of information on the entity, facilitating a unified, fast and effective query that promotes decision-making and, therefore, the optimization of the real entity.

Digital twin content

A digital twin is composed of static and dynamic data.

  • Static data. Information relevant to the digital twin that is expected to remain constant, such as the model, the date of acquisition or the location of the machine we are replicating.

  • Dynamic data. Data that changes over time and that we will record in time series, such as the position of a mobile robot or the values measured by a sensor.

For example, consider a DHT22 temperature and humidity sensor. Its digital twin, represented in JSON format following the schema provided by Eclipse Ditto, would look like this:

{
"policyId": "example:DHT22",
"attributes": {
"location": "Spain"
},
"features": {
"temperature": {
"properties": {
"value": null
}
},
"humidity": {
"properties": {
"value": null
}
}
}
}

Digital twin type

Digital twins composition

- + \ No newline at end of file diff --git a/docs/overview/purpose.html b/docs/overview/purpose.html index 7e18be5..230d632 100644 --- a/docs/overview/purpose.html +++ b/docs/overview/purpose.html @@ -5,13 +5,13 @@ Purpose | OpenTwins - +

Purpose

This platform has been designed to facilitate the development of digital twins and is characterised by the exclusive use of open source components. The aim is to achieve a platform that covers all the functionalities that a digital twin may require, from the most basic ones, such as simply checking its real-time state, to more advanced ones, such as the inclusion of predicted or simulated data or visualisation of 3D models of the twins.

Take care

This platform is currently under development, so its use in production environments is not recommended at this stage.

- + \ No newline at end of file diff --git a/docs/quickstart.html b/docs/quickstart.html index 33b3e0b..f35f513 100644 --- a/docs/quickstart.html +++ b/docs/quickstart.html @@ -5,12 +5,12 @@ Quickstart | OpenTwins - +
-

Quickstart

Welcome to OpenTwins, a flexible platform adapted to your needs! Although OpenTwins offers extensive customization options, we understand the importance of simplicity for beginners. Therefore, let's embark on a short journey together, showing you the quickest route to deploy the platform and develop a functional digital twin.

Installation

Prerequisites

Please be sure you have the following utilities installed on your host machine:

If you don't have a Kubernetes cluster, you can set one up on local using minikube. For a smooth deployment experience, we suggest you use the following minimum configuration values.

minikube start --cpus 4 --disk-size 40gb --memory 8192
kubectl config use-context minikube

Deploy

The quickest way to deploy OpenTwins is using Helm.

The following command adds the ERTIS repository where the OpenTwins helm chart is located.

helm repo add ertis https://ertis-research.github.io/Helm-charts/

To deploy the platform with recommended functionality, use the command below:

helm upgrade --install opentwins ertis/OpenTwins -n opentwins --wait --dependency-update

To modify the components to be deployed and connected during the installation, you can check the installation via Helm.

Configuration

If you've correctly installed OpenTwins Helm using the default settings, all connections should be established. +

Quickstart

Welcome to OpenTwins, a flexible platform adapted to your needs! Although OpenTwins offers extensive customization options, we understand the importance of simplicity for beginners. Therefore, let's embark on a short journey together, showing you the quickest route to deploy the platform and develop a functional digital twin.

Installation

Prerequisites

Please be sure you have the following utilities installed on your host machine:

If you don't have a Kubernetes cluster, you can set one up on local using minikube. For a smooth deployment experience, we suggest you use the following minimum configuration values.

minikube start --cpus 4 --disk-size 40gb --memory 8192
kubectl config use-context minikube

Deploy

The quickest way to deploy OpenTwins is using Helm.

The following command adds the ERTIS repository where the OpenTwins helm chart is located.

helm repo add ertis https://ertis-research.github.io/Helm-charts/

To deploy the platform with recommended functionality, use the command below:

helm upgrade --install opentwins ertis/OpenTwins --wait --dependency-update

To modify the components to be deployed and connected during the installation, you can check the installation via Helm.

Configuration

If you've correctly installed OpenTwins Helm using the default settings, all connections should be established. The final step involves configuring the platform interface plugin by adding the addresses of the Eclipse Ditto nginx service and the Ditto Extended API component into the plugin's configuration section. Check the installation documentation for more details.

Create your first digital twin

Create digital twins

A digital twin must be at least a synchronized replica of a real system or object. To create it, the first step involves understanding the purpose of the digital twin, designing its structure and defining its most relevant characteristics. Next, it is necessary to define this information in OpenTwins and then connect the data sources that will feed the model. Finally, it is necessary to represent the data in a way that is understandable to any user.

Optionally, other useful functionalities can be added to the digital twin. In OpenTwins, we offer the integration of AI/ML models, the addition of 3D models and the execution of FMI or containerized simulations. However, this tutorial will not cover these extra functionalities, so we recommend consulting their respective guides for more information.

Following these steps, we will use OpenTwins to develop the digital twin of a car. In this case, for simplicity, we will focus only on the speed and direction of the car's four wheels. In addition, we will record the GPS location of the vehicle for tracking.

Design

Taking advantage of the platform's functionalities, we will create a composite digital twin. For this purpose, we will define types "car" and "wheel", which will abstract information about the car and the wheel, respectively. @@ -50,7 +50,7 @@ However, to take full advantage of its capabilities, we recommend including other functionalities or additional data sources. This will allow you to obtain a more complete and accurate view of the real system. You can check our guides for more information

- + \ No newline at end of file diff --git a/index.html b/index.html index ab6b272..b8d5d27 100644 --- a/index.html +++ b/index.html @@ -5,13 +5,13 @@ OpenTwins - +
Docusaurus themed imageDocusaurus themed image
opentwins

Innovative open-source platform that specializes in
developing next-gen compositional digital twins

ertis logoertis logoitis logoitis logouma logouma logo
- + \ No newline at end of file diff --git a/markdown-page.html b/markdown-page.html index ba946ac..1c46dee 100644 --- a/markdown-page.html +++ b/markdown-page.html @@ -5,13 +5,13 @@ Markdown page example | OpenTwins - +

Markdown page example

You don't need React to write simple standalone pages.

- + \ No newline at end of file