blob: 8efdf416e97495c6e6ac7206a7ff566f201d08ce [file] [log] [blame]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>PLC4X &#x2013; </title>
<script src="../../js/jquery.slim.min.js" type="text/javascript"></script>
<!--script src="../../js/popper.min.js" type="javascript"></script-->
<script src="../../js/bootstrap.bundle.min.js" type="text/javascript"></script>
<!-- The tooling for adding images and links to Apache events -->
<script src="https://www.apachecon.com/event-images/snippet.js" type="text/javascript"></script>
<!-- FontAwesome -->
<link rel="stylesheet" href="../../css/all.min.css" type="text/css"/>
<!-- Bootstrap -->
<link rel="stylesheet" href="../../css/bootstrap.min.css" type="text/css"/>
<!-- Some Maven Site defaults -->
<link rel="stylesheet" href="../../css/maven-base.css" type="text/css"/>
<link rel="stylesheet" href="../../css/maven-theme.css" type="text/css"/>
<!-- The PLC4X version of a bootstrap theme -->
<link rel="stylesheet" href="../../css/themes/plc4x.css" type="text/css" id="pagestyle"/>
<!-- A custom style for printing content -->
<link rel="stylesheet" href="../../css/print.css" type="text/css" media="print"/>
<meta http-equiv="Content-Language" content="en"/>
</head>
<body class="composite">
<nav class="navbar navbar-light navbar-expand-md bg-faded justify-content-center border-bottom">
<!--a href="/" class="navbar-brand d-flex w-50 mr-auto">Navbar 3</a-->
<a href="https://plc4x.apache.org/" id="bannerLeft"><img src="../../images/apache_plc4x_logo_small.png" alt="Apache PLC4X"/></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#collapsingNavbar3">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse w-100" id="collapsingNavbar3">
<ul class="navbar-nav w-100 justify-content-center">
<li class="nav-item">
<a class="nav-link" href="../../index.html">Home</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="../../users/index.html">Users</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../../developers/index.html">Developers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../../apache/index.html">Apache</a>
</li>
</ul>
<ul class="nav navbar-nav ml-auto justify-content-end">
<li class="nav-item row valign-middle">
<a class="acevent" data-format="wide" data-mode="light" data-event="random" style="width:240px;height:60px;"></a>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<div class="row h-100">
<nav class="col-sm-push col-md-2 pt-3 sidebar">
<div class="sidebar-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<a href="../../users/index.html" class="nav-link">Section Home</a>
</li>
<li class="nav-item">
<a href="../../users/download.html" class="nav-link">Download</a>
</li>
<li class="nav-item">
<a href="../../users/adopters.html" class="nav-link">Adopters</a>
</li>
<li class="nav-item">
<a href="../../users/commercial-support.html" class="nav-link">Commercial support</a>
</li>
<li class="nav-item">
<a href="../../users/gettingstarted.html" class="nav-link">Getting Started</a>
<ul class="flex-column pl-4 nav">
<li class="nav-item">
<a href="../../users/getting-started/plc4go.html" class="nav-link">Go</a>
</li>
<li class="nav-item">
<strong class="nav-link">Java</strong>
</li>
<li class="nav-item">
<a href="../../users/getting-started/using-snapshots.html" class="nav-link">Using SNAPSHOTS</a>
</li>
<li class="nav-item">
<a href="../../users/getting-started/general-concepts.html" class="nav-link">General Concepts</a>
</li>
<li class="nav-item">
<a href="../../users/getting-started/virtual-modbus.html" class="nav-link">Virtual Modbus</a>
</li>
</ul>
</li>
<li class="nav-item">
<a href="../../users/blogs-videos-and-slides.html" class="nav-link">Blogs, Videos and Slides</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/index.html" class="nav-link">Protocols</a>
<ul class="flex-column pl-4 nav">
<li class="nav-item">
<a href="../../users/protocols/ab-eth.html" class="nav-link">AB-ETH</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/ads.html" class="nav-link">ADS/AMS</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/bacnetip.html" class="nav-link">BACnet/IP</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/canopen.html" class="nav-link">CANopen</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/deltav.html" class="nav-link">DeltaV</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/df1.html" class="nav-link">DF1</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/ethernet-ip.html" class="nav-link">EtherNet/IP</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/firmata.html" class="nav-link">Firmata</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/knxnetip.html" class="nav-link">KNXnet/IP</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/modbus.html" class="nav-link">Modbus</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/opc-ua.html" class="nav-link">OPC UA</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/plc4x.html" class="nav-link">PLC4X (Proxy)</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/s7.html" class="nav-link">S7 (Step7)</a>
</li>
<li class="nav-item">
<a href="../../users/protocols/simulated.html" class="nav-link">Simulated</a>
</li>
</ul>
</li>
<li class="nav-item">
<a href="../../users/transports/index.html" class="nav-link">Transports</a>
<ul class="flex-column pl-4 nav">
<li class="nav-item">
<a href="../../users/transports/tcp.html" class="nav-link">TCP</a>
</li>
<li class="nav-item">
<a href="../../users/transports/udp.html" class="nav-link">UDP</a>
</li>
<li class="nav-item">
<a href="../../users/transports/serial.html" class="nav-link">Serial</a>
</li>
<li class="nav-item">
<a href="../../users/transports/socketcan.html" class="nav-link">SocketCAN</a>
</li>
<li class="nav-item">
<a href="../../users/transports/raw-socket.html" class="nav-link">Raw Socket</a>
</li>
<li class="nav-item">
<a href="../../users/transports/pcap-replay.html" class="nav-link">PCAP Replay</a>
</li>
</ul>
</li>
<li class="nav-item">
<a href="../../users/integrations/index.html" class="nav-link">Integrations</a>
<ul class="flex-column pl-4 nav">
<li class="nav-item">
<a href="../../users/integrations/apache-calcite.html" class="nav-link">Apache Calcite</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/apache-camel.html" class="nav-link">Apache Camel</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/apache-edgent.html" class="nav-link">Apache Edgent</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/apache-iotdb.html" class="nav-link">Apache IoTDB</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/apache-kafka.html" class="nav-link">Apache Kafka</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/apache-nifi.html" class="nav-link">Apache NiFi</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/apache-streampipes.html" class="nav-link">Apache StreamPipes</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/eclipse-ditto.html" class="nav-link">Eclipse Ditto</a>
</li>
<li class="nav-item">
<a href="../../users/integrations/eclipse-milo.html" class="nav-link">Eclipse Milo OPC UA Server</a>
</li>
</ul>
</li>
<li class="nav-item">
<a href="../../users/tools/index.html" class="nav-link">Tools</a>
<ul class="flex-column pl-4 nav">
<li class="nav-item">
<a href="../../users/tools/capture-replay.html" class="nav-link">Capture Replay</a>
</li>
<li class="nav-item">
<a href="../../users/tools/connection-pool.html" class="nav-link">Connection Pool</a>
</li>
<li class="nav-item">
<a href="../../users/tools/connection-cache.html" class="nav-link">Connection Cache</a>
</li>
<li class="nav-item">
<a href="../../users/tools/opm.html" class="nav-link">Object PLC Mapping (OPM)</a>
</li>
<li class="nav-item">
<a href="../../users/tools/scraper.html" class="nav-link">Scraper</a>
</li>
<li class="nav-item">
<a href="../../users/tools/testing.html" class="nav-link">PLC4X without a PLC and Unit Testing</a>
</li>
</ul>
</li>
<li class="nav-item">
<a href="../../users/industry40.html" class="nav-link">Industry 4.0 with Apache</a>
</li>
<li class="nav-item">
<a href="../../users/security.html" class="nav-link">Security</a>
</li>
</ul>
</div>
</nav>
<main role="main" class="ml-sm-auto px-4 col-sm-pull col-md-9 col-lg-10 h-100">
<div class="sect1">
<h2 id="getting_started">Getting Started</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="using_the_plc4j_api_directly">Using the PLC4J API directly</h3>
<div class="paragraph">
<p>In order to write a valid PLC4X Java application, all you need, is to add a dependency to the <code>api module</code>.
When using Maven, all you need to do is add this dependency:</p>
</div>
<div class="listingblock">
<div class="content">
<pre> &lt;dependency&gt;
&lt;groupId&gt;org.apache.plc4x&lt;/groupId&gt;
&lt;artifactId&gt;plc4j-api&lt;/artifactId&gt;
&lt;version&gt;0.9.1&lt;/version&gt;
&lt;/dependency&gt;</pre>
</div>
</div>
<div class="paragraph">
<p>This will allow you to write a valid application, that compiles fine.
However, in order to actually connect to a device using a given protocol, you need to add this protocol implementation to the classpath.</p>
</div>
<div class="paragraph">
<p>For example in order to communicate with an <code>S7 device</code> using the <code>S7 Protocol</code>, you would need to add the following dependency:</p>
</div>
<div class="listingblock">
<div class="content">
<pre> &lt;dependency&gt;
&lt;groupId&gt;org.apache.plc4x&lt;/groupId&gt;
&lt;artifactId&gt;plc4j-driver-s7&lt;/artifactId&gt;
&lt;version&gt;0.9.1&lt;/version&gt;
&lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;</pre>
</div>
</div>
<div class="paragraph">
<p>So as soon as your project has the API and a driver implementation available, you first need to get a <code>PlcConnection</code> instance.
This is done via the <code>PlcDriverManager</code> by asking this to create an instance for a given <code>PLC4X connection string</code>.</p>
</div>
<div class="listingblock">
<div class="content">
<pre>String connectionString = "s7://10.10.64.20";
try (PlcConnection plcConnection = new PlcDriverManager().getConnection(connectionString)) {
... do something with the connection here ...
}</pre>
</div>
</div>
<div class="paragraph">
<p>PLC4X generally supports a very limited set of functions, which is not due to the fact, that we didn&#8217;t implement things, but that PLCs generally support a very limited set of functions.</p>
</div>
<div class="paragraph">
<p>The basic functions supported by PLCs and therefore supported by PLC4X are:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Read data</p>
</li>
<li>
<p>Write data</p>
</li>
<li>
<p>Subscribe for data</p>
</li>
<li>
<p>Execute functions in the PLC</p>
</li>
<li>
<p>List resources in the PLC</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>In general we will try to offer as many features as possible.
So if a protocol doesn&#8217;t support subscription based communication it is our goal to simulate this by polling in the background so it is transparent for the users.</p>
</div>
<div class="paragraph">
<p>But there are some cases in which we can&#8217;t simulate or features are simply disabled intentionally:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>If a PLC and/or protocol don&#8217;t support executing of functions, we simply can&#8217;t provide this functionality.</p>
</li>
<li>
<p>We will be providing stripped down versions of drivers, that for example intentionally don&#8217;t support any writing of data and executing of functions.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Therefore we use metadata to check programmatically, if a given feature is available.</p>
</div>
<div class="sect3">
<h4 id="reading_data">Reading Data</h4>
<div class="listingblock">
<div class="content">
<pre>// Check if this connection support reading of data.
if (!plcConnection.getMetadata().canRead()) {
logger.error("This connection doesn't support reading.");
return;
}</pre>
</div>
</div>
<div class="paragraph">
<p>As soon as you have ensured that a feature is available, you are ready to build a first request.
This is done by getting a <code>PlcReadRequest.Builder</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>// Create a new read request:
// - Give the single item requested an alias name
PlcReadRequest.Builder builder = plcConnection.readRequestBuilder();
builder.addItem("value-1", "%Q0.4:BOOL");
builder.addItem("value-2", "%Q0:BYTE");
builder.addItem("value-3", "%I0.2:BOOL");
builder.addItem("value-4", "%DB.DB1.4:INT");
PlcReadRequest readRequest = builder.build();</pre>
</div>
</div>
<div class="paragraph">
<p>So, as you can see, you prepare a request, by adding items to the request and in the end by calling the <code>build</code> method.</p>
</div>
<div class="paragraph">
<p>The request is sent to the PLC by issuing the <code>execute</code> method on the request object:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>CompletableFuture&lt;? extends PlcReadResponse&gt; asyncResponse = readRequest.execute();
asyncResponse.whenComplete((response, throwable) -&gt; {
try {
... process the response ...
} catch (Exception e) {
... Handle any errors ...
}
});</pre>
</div>
</div>
<div class="paragraph">
<p>In general all requests are executed asynchronously.
So as soon as the request is fully processed, the callback gets called and will contain a <code>readResponse</code>, if everything went right or a <code>throwable</code> if there were problems.</p>
</div>
<div class="paragraph">
<p>However if you want to write your code in a more synchronous fashion, the following alternative will provide this:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>PlcReadResponse response = readRequest.execute().get(5000, TimeUnit.MILLISECONDS);</pre>
</div>
</div>
<div class="paragraph">
<p>Processing of the responses is identical in both cases.
The following example will demonstrate some of the options you have:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>for (String fieldName : response.getFieldNames()) {
if(response.getResponseCode(fieldName) == PlcResponseCode.OK) {
int numValues = response.getNumberOfValues(fieldName);
// If it's just one element, output just one single line.
if(numValues == 1) {
logger.info("Value[" + fieldName + "]: " + response.getObject(fieldName));
}
// If it's more than one element, output each in a single row.
else {
logger.info("Value[" + fieldName + "]:");
for(int i = 0; i &lt; numValues; i++) {
logger.info(" - " + response.getObject(fieldName, i));
}
}
}
// Something went wrong, to output an error message instead.
else {
logger.error("Error[" + fieldName + "]: " + response.getResponseCode(fieldName).name());
}
}</pre>
</div>
</div>
<div class="paragraph">
<p>In the for loop, we are demonstrating how the user can iterate over the address aliases in the response.
In case of an ordinary read request, this will be predefined by the items in the request, however in case of a subscription response, the response might only contain some of the items that were subscribed.</p>
</div>
<div class="paragraph">
<p>Before accessing the data, it is advisable to check if an item was correctly returned.
This is done by the <code>getResponseCode</code> method for a given alias.
If this is <code>PlcResponseCode.OK</code>, everything is ok, however it could be one of the following:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>NOT_FOUND</p>
</li>
<li>
<p>ACCESS_DENIED</p>
</li>
<li>
<p>INVALID_ADDRESS</p>
</li>
<li>
<p>INVALID_DATATYPE</p>
</li>
<li>
<p>INTERNAL_ERROR</p>
</li>
<li>
<p>RESPONSE_PENDING</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Assuming the return code was <code>OK</code>, we can continue accessing the data.</p>
</div>
<div class="paragraph">
<p>As some addresses support reading arrays, with the method <code>getNumberOfValues</code> the user can check how many items of a given type are returned.
For convenience the response object has single-argument methods for accessing the data, which default to returning the first element.</p>
</div>
<div class="literalblock">
<div class="content">
<pre>response.getObject(fieldName)</pre>
</div>
</div>
<div class="paragraph">
<p>If you want to access a given element number, please use the two-argument version instead:</p>
</div>
<div class="literalblock">
<div class="content">
<pre>response.getObject(fieldName, 42)</pre>
</div>
</div>
<div class="paragraph">
<p>PLC4X provides getters and setters for a wide variety of Java types and automatically handles the type conversion.
However when for example trying to get a long-value as a byte and the long-value exceeds the range supported by the smaller type, a <code>RuntimeException</code> of type <code>PlcIncompatibleDatatypeException</code>.
In order to avoid causing this exception to be thrown, however there are <code>isValid{TypeName}</code> methods that you can use to check if the value is compatible.</p>
</div>
</div>
<div class="sect3">
<h4 id="writing_data">Writing Data</h4>
<div class="paragraph">
<p>In general the structure of code for writing data is extremely similar to that of reading data.</p>
</div>
<div class="paragraph">
<p>So first it is advisable to check if this connection is even able to write data:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>// Check if this connection support writing of data.
if (!plcConnection.getMetadata().canWrite()) {
logger.error("This connection doesn't support writing.");
return;
}</pre>
</div>
</div>
<div class="paragraph">
<p>As soon as we are sure that we can write, we create a new <code>PlcWriteRequest.Builder</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>// Create a new write request:
// - Give the single item requested an alias name
// - Pass in the data you want to write (for arrays, pass in one value for every element)
PlcWriteRequest.Builder builder = plcConnection.writeRequestBuilder();
builder.addItem("value-1", "%Q0.4:BOOL", true);
builder.addItem("value-2", "%Q0:BYTE", (byte) 0xFF);
builder.addItem("value-4", "%DB.DB1.4:INT[3]", 7, 23, 42);
PlcWriteRequest writeRequest = builder.build();</pre>
</div>
</div>
<div class="paragraph">
<p>The same way read requests are sent to the PLC by issuing the <code>execute</code> method on the request object:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>CompletableFuture&lt;? extends PlcWriteResponse&gt; asyncResponse = writeRequest.execute();
asyncResponse.whenComplete((response, throwable) -&gt; {
... process the response ...
});</pre>
</div>
</div>
<div class="paragraph">
<p>You could here also use the blocking option:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>PlcWriteResponse response = writeRequest.execute().get();</pre>
</div>
</div>
<div class="paragraph">
<p>As we don&#8217;t have to process the data itself, for the write request, it&#8217;s enough to simply check the return code for each field.</p>
</div>
<div class="listingblock">
<div class="content">
<pre>for (String fieldName : response.getFieldNames()) {
if(response.getResponseCode(fieldName) == PlcResponseCode.OK) {
logger.info("Value[" + fieldName + "]: updated");
}
// Something went wrong, to output an error message instead.
else {
logger.error("Error[" + fieldName + "]: " + response.getResponseCode(fieldName).name());
}
}</pre>
</div>
</div>
</div>
<div class="sect3">
<h4 id="subscribing_to_data">Subscribing to Data</h4>
<div class="paragraph">
<p>Subscribing to data can be considered similar to reading data, at least the subscription itself if very similar to reading of data.</p>
</div>
<div class="paragraph">
<p>First of all we first have to check if the connection supports this:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>// Check if this connection support subscribing to data.
if (!plcConnection.getMetadata().canSubscribe()) {
logger.error("This connection doesn't support subscribing.");
return;
}</pre>
</div>
</div>
<div class="paragraph">
<p>Now we&#8217;ll create the subscription request.</p>
</div>
<div class="paragraph">
<p>The main difference is that while reading there is only one form how you could read, with subscriptions there are different forms of subscriptons:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Change of state (Event is sent as soon as a value changes)</p>
</li>
<li>
<p>Cyclic (The Event is sent in regular cyclic intervals)</p>
</li>
<li>
<p>Event (The Event is usually explicitly sent form the PLC as a signal)</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Therefore instead of using a normal <code>addItem</code>, there are tree different methods as you can see in the following examples.</p>
</div>
<div class="listingblock">
<div class="content">
<pre>// Create a new subscription request:
// - Give the single item requested an alias name
PlcSubscriptionRequest.Builder builder = plcConnection.subscriptionRequestBuilder();
builder.addChangeOfStateField("value-1", "{some address}");
builder.addCyclicField("value-2", "{some address}", Duration.ofMillis(1000));
builder.addEventField("value-3", "{some alarm address}");
PlcSubscriptionRequest subscriptionRequest = builder.build();</pre>
</div>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
The <code>addCyclicField</code> method requires a third parameter <code>duration</code>.
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>The request itself is executed exactly the same way the read and write operations are executed, using the <code>execute</code> method, therefore just the short synchronous version here (The async version works just as good)</p>
</div>
<div class="listingblock">
<div class="content">
<pre>PlcSubscriptionResponse response = subscriptionRequest.execute().get();</pre>
</div>
</div>
<div class="paragraph">
<p>Now comes the little more tricky part, as subscriptions are always asynchronous, we have to register a callback for the connection to call as soon as there is news available:</p>
</div>
<div class="paragraph">
<p>In general you can&#8217;t say how many of your subscribed fields will be available in every callback so it is double important to check or iterate over the field names.</p>
</div>
<div class="listingblock">
<div class="content">
<pre>for (String subscriptionName : response.getFieldNames()) {
final PlcSubscriptionHandle subscriptionHandle = response.getSubscriptionHandle(subscriptionName);
subscriptionHandle.register(plcSubscriptionEvent -&gt; {
for (String fieldName : plcSubscriptionEvent.getFieldNames()) {
System.out.println(plcSubscriptionEvent.getPlcValue(fieldName));
}
});
}</pre>
</div>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
Here there currently is a double iteration over the field names, this will probably change soon.
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="pt-4 my-md-5 pt-md-5 w-100 border-top">
<div class="row justify-content-md-center" style="font-size: 13px">
<div class="col col-6 text-center">
Copyright &#169; 2017&#x2013;2022 <a href="https://www.apache.org/">The Apache Software Foundation</a>.
All rights reserved.<br/>
Apache PLC4X, PLC4X, Apache, the Apache feather logo, and the Apache PLC4X project logo are either registered trademarks or trademarks of The Apache Software Foundation in the United States and other countries. All other marks mentioned may be trademarks or registered trademarks of their respective owners.
<br/><div style="text-align:center;">Home screen image taken from <a
href="https://flic.kr/p/chEftd">Flickr</a>, "Tesla Robot Dance" by Steve Jurvetson, licensed
under <a href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0 Generic</a>, image cropped
and blur effect added.</div>
</div>
</div>
</footer>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="../../js/jquery.slim.min.js"></script>
<script src="../../js/popper.min.js"></script>
<script src="../../js/bootstrap.min.js"></script>
<script type="text/javascript">
$('.carousel .carousel-item').each(function(){
var next = $(this).next();
if (!next.length) {
next = $(this).siblings(':first');
}
next.children(':first-child').clone().appendTo($(this));
for (let i = 0; i < 3; i++) {
next=next.next();
if (!next.length) {
next = $(this).siblings(':first');
}
next.children(':first-child').clone().appendTo($(this));
}
});
</script>
</body>
</html>