[Major] Updated PLC example setup doc

This commit is contained in:
Robert von Burg 2022-08-17 15:36:04 +02:00
parent 846548f01c
commit b525b0f5e4
5 changed files with 88 additions and 467 deletions

View File

@ -6,8 +6,11 @@ weight: 40
## Maven Archetypes
Maven offers archetypes to generate new projects. Strolch offers the following archetypes, to create new projects:
* [li.strolch.mvn.archetype.main](/development/main-class-app) for Java main class applications
* [li.strolch.mvn.archetype.webapp](/development/web-app) for Java Web based applications using REST and Polymer 1.x as the frontend.
* [li.strolch.mvn.archetype.webapp](/development/web-app) for Java Web based applications using REST and Polymer 1.x as
the frontend.
* [li.strolch.mvn.archetype.plc](/plc/example-set-up) for Strolch PLC projects.
To use the archetypes, clone the archetypes repository and install it locally:
@ -17,4 +20,4 @@ cd strolch-maven-archetypes
mvn clean install
```
Then follow one of the next steps to create the type of application you want.
Then follow one of the next steps to create the type of application you want.

View File

@ -19,7 +19,7 @@ description in how to set up your own Strolch based PLC.
Checkout the code at [GitHub](https://github.com/strolch-li/strolch-plc)
Strolch PLC is also deployed to Maven Central. Current version is 1.2.1.
Strolch PLC is also deployed to Maven Central. Current version is 1.2.2 and is using Strolch version 1.8.5.
Currently, we have the following topics of discussion:

View File

@ -20,362 +20,60 @@ business logic and the PLC controls the I/Os.
![Strolch PLC Example](/assets/images/Strolch-Plc-Example.png)
## New Project
1. First create a new Strolch Web project using the Strolch Maven archetype
2. Now add the following Maven dependencies:
```xml
<project>
<properties>
<strolch.version>1.8.4</strolch.version>
<strolch.plc.version>1.2.2</strolch.plc.version>
</properties>
<dependencyManagement>
<dependency>
<groupId>li.strolch</groupId>
<artifactId>li.strolch.bom</artifactId>
<type>pom</type>
<version>${strolch.version}</version>
<scope>import</scope>
</dependency>
<dependency>
<groupId>li.strolch</groupId>
<artifactId>strolch-plc-bom</artifactId>
<type>pom</type>
<version>${strolch.plc.version}</version>
<scope>import</scope>
</dependency>
</dependencyManagement>
<dependencies>
<!-- PLC -->
<dependency>
<groupId>li.strolch</groupId>
<artifactId>strolch-plc-core</artifactId>
</dependency>
<dependency>
<groupId>li.strolch</groupId>
<artifactId>strolch-plc-rest</artifactId>
</dependency>
<dependency>
<groupId>li.strolch</groupId>
<artifactId>strolch-plc-gw-client</artifactId>
</dependency>
</dependencies>
</project>
Create a new project using the PLC Strolch Maven Archetype:
```shell
mvn archetype:generate \
-DarchetypeGroupId=li.strolch \
-DarchetypeArtifactId=li.strolch.mvn.archetype.plc \
-DarchetypeVersion=0.1.0-SNAPSHOT \
-DgroupId=<my.groupid> \
-DartifactId=<my-artifactId> \
-Dversion=<my.version> \
-Dpackage=<my.package> \
-DappName="<my app name>"
```
3. Add a bower dependency: `"strolch-wc-plc": "strolch-li/strolch-wc-plc#^0.3.20"`
to `src/main/webapp/bower.json`
This will create a multi-module project:
After adding the dependency, run `gulp` in the webapp directory. Gulp should
have been installed through the instructions from
the [development page](/development).
- The `<my-artifactId>-web` module contains the server code, which handles notifications from the PLC, and can send
telegrams to the PLC.
- The `<my-artifactId>-plc-web` module contains the PLC code, which connects to the server and communicates with the
low-level hardware.
- The `shared` modules contains classes shared by both projects (e.g. constants, etc.).
4. Now we need to add the PLC web views to our new project. This is added in
the `src/main/webapp/app/src/c-app.html` file. Add the following:
This project already contains a default PLC model in `<my-artifactId>-plc-web/runtime/data/`.
```html
<!-- HTML Imports -->
<link rel="import" href="../bower_components/strolch-wc-plc/strolch-wc-plc-connections.html">
<link rel="import" href="../bower_components/strolch-wc-plc/strolch-wc-plc-logical-devices.html">
The following sections explains these files:
<!-- Change default-page to plcLogicalDevices -->
<c-app-routing id="appRouting"
login-page="login"
default-page="plcLogicalDevices"
auth-valid="[[authTokenValid]]"
page="{{page}}"
route-tail="{{routeTail}}"
use-hash-as-path></c-app-routing>
### strolch-plc-example-connections.xml
<!-- Add the new pages in the iron-pages element: -->
<template is="dom-if" if="[[equal(page, 'plcConnections')]]" restamp>
<strolch-wc-plc-connections id="plcConnections"
base-path="../"
base-rest-path="[[baseRestPath]]"
route="{{subroute}}"></strolch-wc-plc-connections>
</template>
<template is="dom-if" if="[[equal(page, 'plcLogicalDevices')]]" restamp>
<strolch-wc-plc-logical-devices id="plcLogicalDevices"
base-path="../"
base-rest-path="[[baseRestPath]]"
base-ws-path="[[baseWsPath]]"
route="{{subroute}}"></strolch-wc-plc-logical-devices>
</template>
This file defines the hardware connections. The following connections are implemented:
// add a new property to the WebSocket path for observing changes on the PLC
wsObserverPath: {
type: String,
value: function () {
return CustomWeb.baseWsPath + "/plc/observer";
}
}
```
* li.strolch.plc.core.hw.i2c.RSL366OverHorterI2c
* li.strolch.plc.core.hw.i2c.PCF8574InputConnection
* li.strolch.plc.core.hw.gpio.RaspiBcmGpioInputConnection
* li.strolch.plc.core.hw.gpio.RaspiBcmGpioOutputConnection
* li.strolch.plc.core.hw.i2c.Multi8BitI2cOutputConnection
* li.strolch.plc.core.hw.connections.DataLogicScannerConnection
* li.strolch.plc.core.hw.connections.LoggerOutConnection
* li.strolch.plc.core.hw.connections.RandomStringConnection
5. Don't forget to add the PLC Rest classes to your `ResourceConfig`
See their respective classes for details.
```java
@ApplicationPath("rest")
public class RestfulApplication extends ResourceConfig {
### strolch-plc-example.csv
public RestfulApplication() {
This file maps I/Os to Resources and Actions and is converted into Strolch `Resource` objects using
the `PlcAddressGenerator` class.
...
In this example we will use simple Raspberry Pi GPIOs. For convenience, and also when sharing I/O definitions with
external partners, it is easier to use a CSV file to define the I/Os and then use the `PlcAddressGenerator` to generate
and validate the model.
// strolch plc services
packages(PlcConnectionsResource.class.getPackage().getName());
...
}
}
```
6. Now we need to configure the PLC's runtime by
modifying `runtime/StrolchConfiguration.xml` and adding the following:
```xml
<StrolchConfiguration>
<env id="dev">
<!--
This component configures the PlcHandler by
loading the PlcConnections, PlcAddresses and PlcTelegrams
-->
<Component>
<name>PlcHandler</name>
<api>li.strolch.plc.core.PlcHandler</api>
<impl>li.strolch.plc.core.DefaultPlcHandler</impl>
<depends>RealmHandler</depends>
<Properties>
<!-- The component handling the low level connections -->
<plcClass>li.strolch.plc.core.hw.DefaultPlc</plcClass>
</Properties>
</Component>
<!--
This component handles registrations of the PlcServices, i.e. your PLC business logic
-->
<Component>
<name>PlcServiceInitializer</name>
<api>li.strolch.plc.core.PlcServiceInitializer</api>
<impl>li.strolch.plc.example.CustomPlcServiceInitializer</impl>
<depends>PlcHandler</depends>
<Properties>
</Properties>
</Component>
<!--
This component notifies a Strolch agent of changes on the PLC
only if you have a Strolch server with a configured
li.strolch.plc.gw.server.PlcServerWebSocketEndpoint ready to accept connections
-->
<Component>
<name>PlcGwClientHandler</name>
<api>li.strolch.plc.gw.client.PlcGwClientHandler</api>
<impl>li.strolch.plc.gw.client.PlcGwClientHandler</impl>
<depends>PlcHandler</depends>
<depends>PlcServiceInitializer</depends>
<Properties>
<plcId>plc-01</plcId>
<gwUsername>plc-01</gwUsername>
<gwPassword>plc-01</gwPassword>
<gwServerUrl>ws://localhost:8080/agent/websocket/strolch/plc
</gwServerUrl>
</Properties>
</Component>
</env>
</StrolchConfiguration>
```
7. Now we add the custom classes we just declared.
**PlcServiceInitializer**
```java
import java.util.ArrayList;
import java.util.List;
import li.strolch.plc.example.services.*;
import li.strolch.agent.api.ComponentContainer;
import li.strolch.plc.core.PlcHandler;
import li.strolch.plc.core.PlcService;
import li.strolch.plc.core.PlcServiceInitializer;
public class CustomPlcServiceInitializer extends PlcServiceInitializer {
public CustomPlcServiceInitializer(ComponentContainer container, String componentName) {
super(container, componentName);
}
@Override
protected List<PlcService> getPlcServices(PlcHandler plcHandler) {
ArrayList<PlcService> plcServices = new ArrayList<>();
StartupPlcService startupPlcService = new StartupPlcService(plcHandler);
ConveyorPlcService conveyorPlcService = new ConveyorPlcService(plcHandler);
plcServices.add(conveyorPlcService);
plcServices.add(startupPlcService);
return plcServices;
}
}
```
**PlcPostInitializer**
```java
import li.strolch.agent.api.ComponentContainer;
import li.strolch.plc.core.PlcPostInitializer;
public class CustomPostInitializer extends PlcPostInitializer {
public CustomPostInitializer(ComponentContainer container, String componentName) {
super(container, componentName);
}
// override the initialize(), start(), stop() and destroy() methods as needed
}
```
8. In the `CustomPlcServiceInitializer` we added two PlcServices, for which the
code is missing. The following are simple examples:
**StartupPlcService**
```java
import li.strolch.persistence.api.StrolchTransaction;
import li.strolch.plc.core.PlcHandler;
import li.strolch.plc.core.PlcService;
public class StartupPlcService extends PlcService {
public static final String PLC = "PLC";
public static final String STARTED = "Started";
public static final String STOPPED = "Stopped";
public StartupPlcService(PlcHandler plcHandler) {
super(plcHandler);
}
@Override
public void start(StrolchTransaction tx) {
send(PLC, STARTED);
super.start(tx);
}
@Override
public void stop() {
send(PLC, STOPPED);
super.stop();
}
}
```
**ConveyorPlcService**
```java
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import li.strolch.plc.core.PlcHandler;
import li.strolch.plc.core.PlcService;
import li.strolch.plc.model.PlcAddress;
public class ConveyorPlcService extends PlcService {
public static final int BOX_TRANSFER_DURATION = 30;
private static final String R_CONVEYOR_01 = "Conveyor01";
private static final String A_START_BUTTON = "StartButton";
private static final String T_MOTOR_ON = "MotorOn";
private static final String T_MOTOR_OFF = "MotorOff";
private static final String A_BOX_DETECTED = "BoxDetected";
private boolean motorOn;
private ScheduledFuture<?> motorStopTask;
public ConveyorPlcService(PlcHandler plcHandler) {
super(plcHandler);
}
@Override
public void handleNotification(PlcAddress address, Object value) {
String resource = address.resource;
String action = address.action;
if (!resource.equals("Conveyor01"))
throw new IllegalStateException("Unexpected resource " + resource);
boolean active = (boolean) value;
if (action.equals(A_START_BUTTON)) {
if (active) {
logger.info("Start button pressed. Starting motors...");
send(R_CONVEYOR_01, T_MOTOR_ON);
this.motorOn = true;
scheduleStopTask();
}
} else if (action.equals(A_BOX_DETECTED)) {
if (active && this.motorOn) {
logger.info("Container detected, refreshing stop task...");
scheduleStopTask();
}
} else {
logger.info("Unhandled notification " + address.toKeyAddress());
}
}
private void scheduleStopTask() {
if (this.motorStopTask != null)
this.motorStopTask.cancel(false);
this.motorStopTask = schedule(this::stopMotor, BOX_TRANSFER_DURATION, TimeUnit.SECONDS);
}
private void stopMotor() {
send(R_CONVEYOR_01, T_MOTOR_OFF);
}
@Override
public void register() {
this.plcHandler.register(R_CONVEYOR_01, A_START_BUTTON, this);
this.plcHandler.register(R_CONVEYOR_01, A_BOX_DETECTED, this);
super.register();
}
@Override
public void unregister() {
this.plcHandler.unregister(R_CONVEYOR_01, A_START_BUTTON, this);
this.plcHandler.unregister(R_CONVEYOR_01, A_BOX_DETECTED, this);
super.unregister();
}
}
```
9. Now the last part is to add the model, i.e. PlcConnections, PlcAddresses and
PlcTelegrams. To have less configuration files and make it easier to
reconfigure at runtime, this data is stored in normal Strolch `Resources`.
In this example we will use simple Raspberry Pi GPIOs. For convenience, and
also when sharing I/O definitions with external partners, it is easier to use
a CSV file to define the I/Os and then use the `PlcAddressGenerator` to
generate and validate the model.
For this purpose in this example, we will use one conveyor with 2 inputs and
1 output. The CSV file should have the following content:
```csv
Description,Type,SubType,Device,Pin,Resource,Action1,Action2,Connection,DeviceId
Material Flow,Group,,,,,,,,MaterialFlow
Conveyor 1,Input,Pin,,4,Conveyor,Occupied,,raspiBcmGpioInput
Conveyor 1,Input,Pin,,17,Conveyor,BoxDetected,,raspiBcmGpioInput
Conveyor 1,Output,Pin,,18,Conveyor,MotorOn,MotorOff,raspiBcmGpioOutput
```
For easier handling, use the following Google Sheet as a starting
point: https://docs.google.com/spreadsheets/d/10fgTfR3FZCVbQ5bbh0xB1u8rLIaw2KEyO45VMv7y5ho/edit?usp=sharing
The CSV headers are as follows:
@ -392,13 +90,14 @@ The CSV headers are as follows:
Scanner. The actions must be left empty as the keys Barcode (address), On
and Off (telegrams) will be generated.
* SubType →
* For Input and Output types →
* DevPin, DevPin0 → Generates the address as `<Connection>.<Device>.<Pin>`.
DevPin0 decrements the Device and Pin values by one.
* Pin → Generates the address as `<Connection>.<Pin>`.
* For Virtual types →
* Boolean
* String
* For Input and Output types →
* DevPin, DevPin0 → Generates the address as `<Connection>.<Device>.<Pin>`.
DevPin0 decrements the Device and Pin values by one for zero-indexed addressing.
* Pin → Generates the address as `<Connection>.<Pin>`.
* For Virtual types →
* Boolean
* String
* Integer
* Device → Device number
* Pin → The pin number on the device
* Resource → The resource ID with which to notify the agent
@ -406,134 +105,53 @@ The CSV headers are as follows:
* Action2 → The second action ID if required
* Connection → The ID of the PlcConnection with which this I/O is attached
* DeviceId → For type Group: Set the ID of this PlcLogicalDevice being generated
* Interted → For boolean inputs or outputs, if true, inverts the value
* Value → A default value, often used for virtual integer addresses
* Remote → if true, then the server will be notified of changes to this address
When you use this file as input for the `PlcAddressGenerator`, then it will
generate PlcLogicalDevice, PlcAddress and PlcTelegram elements:
generate PlcLogicalDevice, PlcAddress and PlcTelegram elements. See the file `strolch-plc-example.xml` for an example.
```xml
<StrolchModel>
<Resource Id="D_MaterialFlow" Name="MaterialFlow" Type="PlcLogicalDevice">
<ParameterBag Id="parameters" Name="Parameters" Type="Parameters">
<Parameter Id="description" Name="Description" Type="String"
Value="Material Flow"/>
<Parameter Id="group" Name="Group" Type="String"
Value="01 Material Flow"/>
<Parameter Id="index" Name="Index" Type="Integer" Value="10"/>
</ParameterBag>
<ParameterBag Id="relations" Name="Relations" Type="Relations">
<Parameter Id="addresses" Name="Addresses" Type="StringList"
Interpretation="Resource-Ref" Uom="PlcAddress"
Value="A_Conveyor-Occupied, A_Conveyor-BoxDetected, A_Conveyor-MotorOn"/>
<Parameter Id="telegrams" Name="Telegrams" Type="StringList"
Interpretation="Resource-Ref" Uom="PlcTelegram"
Value="T_Conveyor-MotorOn, T_Conveyor-MotorOff"/>
</ParameterBag>
</Resource>
<Resource Id="A_Conveyor-Occupied" Name="Conveyor - Occupied"
Type="PlcAddress">
<ParameterBag Id="parameters" Name="Parameters" Type="Parameters">
<Parameter Id="description" Name="Description" Type="String"
Index="5"
Value="Conveyor 1"/>
<Parameter Id="address" Name="HW Address" Type="String"
Interpretation="PlcConnection" Index="10"
Value="raspiBcmGpioInput.4"/>
<Parameter Id="resource" Name="Resource ID for PlcAddress"
Type="String"
Index="20" Value="Conveyor"/>
<Parameter Id="action" Name="Action ID for PlcAddress" Type="String"
Index="30" Value="Occupied"/>
<Parameter Id="index" Name="Index" Type="Integer" Index="40"
Value="10"/>
<Parameter Id="value" Name="Value" Type="Boolean" Index="100"
Value="false"/>
</ParameterBag>
</Resource>
<Resource Id="T_Conveyor-MotorOn" Name="Conveyor - MotorOn"
Type="PlcTelegram">
<ParameterBag Id="parameters" Name="Parameters" Type="Parameters">
<Parameter Id="description" Name="Description" Type="String"
Index="5"
Value="Conveyor 1"/>
<Parameter Id="address" Name="HW Address" Type="String"
Interpretation="PlcConnection" Index="10"
Value="raspiBcmGpioOutput.18"/>
<Parameter Id="resource" Name="Resource ID for PlcAddress"
Type="String"
Index="20" Value="Conveyor"/>
<Parameter Id="action" Name="Action ID for PlcAddress" Type="String"
Index="30" Value="MotorOn"/>
<Parameter Id="index" Name="Index" Type="Integer" Index="40"
Value="10"/>
<Parameter Id="value" Name="Value" Type="Boolean" Index="100"
Value="true"/>
</ParameterBag>
</Resource>
</StrolchModel>
```
The PlcLogicalDevice references the PlcAddress and PlcTelegram objects, and is
The `PlcLogicalDevice` references the `PlcAddress` and `PlcTelegram` objects, and is
then used in the UI for grouping.
The PlcAddress is used to store the current value and defines the keys with
The `PlcAddress` is used to store the current value and defines the keys with
which the agent will be notified
The PlcTelegram is used to store default values to send, for specific keys. E.g.
The `PlcTelegram` is used to store default values to send, for specific keys. E.g.
The action `On` would send true, and `Off` would send false. This is semantics, and
is defined in each project depending on the hardware.
10. Copy the
file [plc-state.xml](https://github.com/strolch-li/strolch-plc/blob/develop/example/plc-state.xml)
to your runtime and reference it by use of
a `<IncludeFile file="plc-state.xml" />` element. Modify the `PlcId` to be the
same as the one you defined in the `StrolchConfiguration.xml`.
### Running the example
11. Now that we have a model, the `PlcConnections` are to be defined. In the
previous example we used a Raspberry Pi's GPIOs. This needs to be defined as
a `PlcConnection`:
Once you have both the server and PLC instances running, you can login. The default username and password are `admin`
/ `admin`.
```xml
After logging in to the PLC you should be greeted with the following
screen:
![PLC UI](/assets/images/plc.png)
<StrolchModel>
<Resource Id="raspiBcmGpioOutput" Name="Raspi BCM GPIO Output"
Type="PlcConnection">
<ParameterBag Id="parameters" Name="Parameters" Type="Parameters">
<Parameter Id="className" Name="Connection Class" Type="String"
Value="li.strolch.plc.core.hw.gpio.RaspiBcmGpioOutputConnection"/>
<Parameter Id="state" Name="Connection State" Type="String"
Interpretation="Enumeration" Uom="ConnectionState"
Value="Disconnected"/>
<Parameter Id="stateMsg" Name="Connection State Msg" Type="String"
Interpretation="Enumeration" Uom="ConnectionState"
Value=""/>
<Parameter Id="inverted" Name="Inverted" Type="Boolean"
Value="false"/>
<Parameter Id="bcmOutputPins" Name="BCM Output Pins"
Type="IntegerList" Value="27"/>
</ParameterBag>
</Resource>
<Resource Id="raspiBcmGpioInput" Name="Raspi BCM GPIO Input"
Type="PlcConnection">
<ParameterBag Id="parameters" Name="Parameters" Type="Parameters">
<Parameter Id="className" Name="Connection Class" Type="String"
Value="li.strolch.plc.core.hw.gpio.RaspiBcmGpioInputConnection"/>
<Parameter Id="state" Name="Connection State" Type="String"
Interpretation="Enumeration" Uom="ConnectionState"
Value="Disconnected"/>
<Parameter Id="stateMsg" Name="Connection State Msg" Type="String"
Interpretation="Enumeration" Uom="ConnectionState"
Value=""/>
<Parameter Id="inverted" Name="Inverted" Type="Boolean"
Value="true"/>
<Parameter Id="bcmInputPins" Name="BCM Input Pins"
Type="IntegerList" Value="4"/>
</ParameterBag>
</Resource>
</StrolchModel>
```
And after logging in to the server you should be greeted with the following
screen:
![Server UI](/assets/images/plc-server.png)
See [strolch-plc-example-connections.xml](https://github.com/strolch-li/strolch-plc/blob/develop/example/strolch-plc-example-connections.xml) for further examples.
If the PLC could connect to the server, then the `PLC Control` icon should be activated. The actions Enable, Disable and
Stop All send telegrams to the PLC, thus showing how the server would communicate with the PLC.
### Customization
#### PLC
Now that the server and PLC are running, we can customize the code. On the PLC side you will want to create
new `li.strolch.plc.core.PlcService` services by extending the class and then registering the service
in `CustomPlcServiceInitializer`.
See the example `StartupPlcService`.
#### Server
On the server side, to register for notifications from the PLC, one would
implement `li.strolch.plc.gw.server.PlcGwService` services and register them on the `PlcHandler` in
the `PostInitializer` class.
See the example `ModePlcSrvService`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB