|
|
Here, I'll walk you through the java source code, responsible for starting the [Light Hosts](What-am-I-looking-at-here#light-hosts). Additional startup procedures are explained in the documentation for the `start_component.sh` and the `run_demo.sh` scripts.
|
|
|
|
|
|
# Lightstrand
|
|
|
|
|
|
The discussed file can be found [here](https://gitlab.hrz.tu-chemnitz.de/s6869070--tu-dresden.de/vws-spielwiese/-/blob/6d65d24412bda2e555e70a4f9581eb142096bd49/basyx.lichterkette/src/main/java/de/olipar/basyx/lichterkette/Lightstrand.java).
|
|
|
|
|
|
We'll skip the license headers, imports and comments, as they are either pretty much self explanatory or not the product of conscious development effort.
|
|
|
|
|
|
```java
|
|
|
public class Lightstrand {
|
|
|
```
|
|
|
This class creates, registers and hosts the Submodels of the array of lights defined by environment variables, as well as uploads their AAS.
|
|
|
|
|
|
```java
|
|
|
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Lightstrand.class);
|
|
|
```
|
|
|
At the start of the class, we acquire a logger object, to which all console output will be passed. This is not only used internally by BaSyx too, but allows us to easily utilize different loglevels, such as `INFO`, `WARN` and `ERROR`, indicating severity.
|
|
|
|
|
|
```java
|
|
|
private static int redLights;
|
|
|
private static int greenLights;
|
|
|
private static int[] redPins;
|
|
|
private static int[] greenPins;
|
|
|
```
|
|
|
Here, we declare variables for the number of red and green lights, as well as arrays for the individual GPIO pins they correspond to.
|
|
|
|
|
|
```java
|
|
|
private static int getLightAmount(String color) {
|
|
|
```
|
|
|
This method returns the amount of lights of a given color to be created from the environment variables. It is parametrized, because the procedure is the same for red and green lights, except substituting either "red" or "green" (sometimes in caps) where appropriate.
|
|
|
|
|
|
```java
|
|
|
color = color.toUpperCase();
|
|
|
```
|
|
|
First, the given color string is converted to upper case. This allows us to be sure of the capitalization.
|
|
|
|
|
|
```java
|
|
|
String uncheckedAmount = System.getenv(color + "_LIGHTS");
|
|
|
```
|
|
|
We then get the content of the environment variable corresponding to the amount of lights to be created of a given color, usually either `RED_LIGHTS` or `GREEN_LIGHTS`.
|
|
|
|
|
|
```java
|
|
|
if (uncheckedAmount == null || uncheckedAmount.isEmpty()) {
|
|
|
logger.warn("No valid amount for " + color.toLowerCase() + " lamps set.");
|
|
|
logger.warn("You might want to explicitly set a " + color + "_LIGHTS environment variable, eg:");
|
|
|
logger.warn("$ export " + color + "_LIGHTS=4");
|
|
|
logger.warn("No " + color.toLowerCase() + " lights will be created.");
|
|
|
return 0;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
If no or an empty environment variable for the amount of lights of the color in question is set, no lights of that color will be created and a warning is issued, advising the setting of such a variable.
|
|
|
|
|
|
```java
|
|
|
try {
|
|
|
int uncheckedIntegerAmount = Integer.parseUnsignedInt(uncheckedAmount);
|
|
|
if (uncheckedIntegerAmount < 0) {
|
|
|
throw new IllegalArgumentException(color + "_LIGHTS is negative");
|
|
|
}
|
|
|
return uncheckedIntegerAmount;
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
logger.warn("No valid amount for " + color.toLowerCase() + " lamps set.");
|
|
|
logger.warn("Make sure " + color.toLowerCase() + " is a positive or zero integer value, eg:");
|
|
|
logger.warn("$ export " + color + "_LIGHTS=4");
|
|
|
logger.warn("No " + color.toLowerCase() + " lights will be created.");
|
|
|
return 0;
|
|
|
}
|
|
|
```
|
|
|
If a value is set, conversion to an unsigned Integer is attempted. If it's not a valid unsigned Integer the same warning as above is issued, advising of the correct setting of the corresponding environment variable.
|
|
|
|
|
|
```java
|
|
|
private static int[] getGPIOPins(String color) {
|
|
|
```
|
|
|
This method returns the array of GPIO pins corresponding to the lights of a given color to be created from the environment variables. It is parametrized, because the procedure is the same for red and green lights, except substituting either "red" or "green" (sometimes in caps) where appropriate, just like the method above.
|
|
|
|
|
|
```java
|
|
|
color = color.toUpperCase();
|
|
|
String uncheckedPins = System.getenv(color + "_PINS");
|
|
|
```
|
|
|
Just like above, we capitalize the given color string and acquire the contents of the resulting environment variable.
|
|
|
|
|
|
```java
|
|
|
if (uncheckedPins == null || uncheckedPins.isEmpty()) {
|
|
|
logger.warn("No valid GPIO pins for " + color.toLowerCase() + " lamps set.");
|
|
|
logger.warn("You might want to explicitly set a " + color + "_PINS environment variable, eg:");
|
|
|
logger.warn("$ export " + color + "_PINS=[2,3,4,17]");
|
|
|
logger.warn("State of virtual " + color.toLowerCase() + " lights will not be mirrored in the real world.");
|
|
|
return new int[0];
|
|
|
```
|
|
|
Similar to above, if the environment variable is not set or empty, a warning is issued regarding the setting of it and advising doing so. an empty Array is returned, indicating that no GPIO ports will be utilized.
|
|
|
|
|
|
We then enter a try block, assuming to have a valid environment variable set.
|
|
|
|
|
|
```java
|
|
|
int[] unfiltered_arr = Arrays.stream(uncheckedPins.substring(1, uncheckedPins.length() - 1).split(",")).map(String::trim).mapToInt(Integer::parseInt).toArray();
|
|
|
```
|
|
|
Here, we convert the given array in the environment variable into a int Array.
|
|
|
To do this, ignoring the first and last character (which would be square brackets), the string is split at the commas. The such created segments are freed of whitespace and then attempted to be converted to integer values and then put into an Array.
|
|
|
|
|
|
```java
|
|
|
int[] unique_arr = IntStream.of(unfiltered_arr).distinct().toArray();
|
|
|
```
|
|
|
We then remove all duplicate numbers from the just created array and store the resulting array into a new local variable.
|
|
|
|
|
|
```java
|
|
|
if (unfiltered_arr.length > unique_arr.length) {
|
|
|
logger.warn("There are non-unique " + color + "_PINS set");
|
|
|
}
|
|
|
```
|
|
|
If the lengths of the unfiltered Array and the Array containing only distinct values differs, a warning is issued, that the environment variable contains duplicate pins.
|
|
|
|
|
|
```java
|
|
|
IntPredicate isInRange = argument -> argument >= 0 && argument <= 27;
|
|
|
int[] arr = IntStream.of(unique_arr).filter(isInRange).toArray();
|
|
|
```
|
|
|
Then, we filter out all invalid [GPIO ports](https://pinout.xyz/). The first line defines a boolean expression dependent on `argument`. It returns true, if `argument` is inside [0,27] and false otherwise.
|
|
|
This predicate is then used to filter the Array of unique values created above, the result is again stored in a new local variable.
|
|
|
|
|
|
```java
|
|
|
if (unique_arr.length > arr.length) {
|
|
|
logger.warn("There are invalid " + color + "_PINS set.");
|
|
|
logger.warn("Please make sure " + color + "_PIN values are within [0;27]");
|
|
|
}
|
|
|
```
|
|
|
The lengths of the Array containing unique values and the Array containing unique valid values for GPIO pins are compared. If they differ, there are invalid values for GPIO ports present and a warning regarding this is issued.
|
|
|
|
|
|
```java
|
|
|
if ((Objects.equals(color, "RED") && arr.length < redLights)
|
|
|
|| (Objects.equals(color, "GREEN") && arr.length < greenLights)) {
|
|
|
logger.warn("Not enough valid " + color + "_PINS set.");
|
|
|
logger.warn("Only the first " + arr.length + " virtual " + color.toLowerCase() + " lights will be mirrored in the real world.");
|
|
|
}
|
|
|
```
|
|
|
Then we check, whether or not enough GPIO ports have been defined for either green or red lights, to cover all red or green lights to be created respectively.
|
|
|
|
|
|
```java
|
|
|
return arr;
|
|
|
```
|
|
|
All going well, the cleaned array is returned.
|
|
|
|
|
|
```java
|
|
|
catch (NumberFormatException e) {
|
|
|
logger.warn("Invalid value in/for" + color + "_PINS set");
|
|
|
logger.warn("Please make sure the values are integers separated by commas, enclosed in brackets, eg:");
|
|
|
logger.warn("$ export " + color + "_PINS=[2,3,4,17]");
|
|
|
logger.warn("State of virtual " + color.toLowerCase() + " lights will not be mirrored on the output.");
|
|
|
return new int[0];
|
|
|
}
|
|
|
```
|
|
|
Things however, might not be going well. In the case of invalid substrings present (ie. such which can't be converted to integers), a warning is issued, advising of the correct format and a empty Array is returned.
|
|
|
|
|
|
```java
|
|
|
private static void setGPIOOutputMode(int[] pins) {
|
|
|
for (int pin : pins) {
|
|
|
try {
|
|
|
Runtime.getRuntime().exec("raspi-gpio set " + pin + " op").waitFor();
|
|
|
} catch (IOException | InterruptedException e) {
|
|
|
logger.warn("Could not set GPIO mode OUTPUT on port " + pin + ": " + e.getMessage());
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
This method calls `raspi-gpio set <PIN> op` for every integer inside the given `pins` Array. This is to set the given GPIO ports as output ports.
|
|
|
|
|
|
```java
|
|
|
public static void main(String[] args) {
|
|
|
```
|
|
|
Finally, we get to the main method.
|
|
|
|
|
|
```java
|
|
|
redLights = getLightAmount("red");
|
|
|
greenLights = getLightAmount("green");
|
|
|
```
|
|
|
Here, we call the `getLightAmount` method encountered above to get the amount of red and green lights to be created.
|
|
|
|
|
|
```java
|
|
|
redPins = getGPIOPins("red");
|
|
|
int[] unfilteredGreenPins = getGPIOPins("green");
|
|
|
```
|
|
|
Then, the `redPins` Array is set with the `getGPIOPins` method discussed above. The green Pins get a bit of special treatment, as we need to check duplicates between the set red and green GPIO ports:
|
|
|
|
|
|
```java
|
|
|
greenPins = Arrays.stream(unfilteredGreenPins).filter(x -> !Arrays.asList(redPins).contains(x)).toArray();
|
|
|
```
|
|
|
To do this, we stream the unfiltered Array of green pins through a filter. this filter only passes elements, that are not contained in `redPins` and puts those that make it into a new Array.
|
|
|
|
|
|
```java
|
|
|
if (unfilteredGreenPins.length > greenPins.length) {
|
|
|
logger.warn("There are duplicate pins defined in GREEN_PINS and RED_PINS.");
|
|
|
logger.warn("The offending values have been filtered from GREEN_PINS.");
|
|
|
}
|
|
|
```
|
|
|
Similar as to how we did it before, we compare the lenghts of the filtered and unfiltered arrays to determine, whether elements did not make the filter. If so, a warning stating the issue is issued.
|
|
|
|
|
|
```java
|
|
|
if (greenPins.length < greenLights) {
|
|
|
logger.warn("Not enough valid GREEN_PINS set.");
|
|
|
logger.warn("Only the first " + greenPins.length + " virtual green lights will be mirrored in the real world.");
|
|
|
}
|
|
|
```
|
|
|
We then need to check again, if enough green pins are provided and issue a warning if not the case.
|
|
|
|
|
|
```java
|
|
|
setGPIOOutputMode(redPins);
|
|
|
setGPIOOutputMode(greenPins);
|
|
|
```
|
|
|
Now that all our GPIO ports for both red and green lights are unique and valid, we set the corresponding GPIO ports to be outputs, using the method described previously.
|
|
|
|
|
|
```java
|
|
|
final int HOSTPORT = Common.getHostPort();
|
|
|
logger.info("Local host port is set to " + HOSTPORT);
|
|
|
logger.info("Ports " + HOSTPORT + " through including " + (HOSTPORT + redLights + greenLights - 1) + " will be used for lights.");
|
|
|
```
|
|
|
`getHostPort()`, part of the [Common class](Common-class-walkthrough) is used to get the port to host the soon to be created Submodels on.
|
|
|
Since we're creating multiple Submodels, each requiring an own port, the `HOSTPORT` value will be used as the start of a sequence of ports used, the length of which depends on the amount of submodels to be created.
|
|
|
|
|
|
```java
|
|
|
for (int i = 0; i < redLights; i++) {
|
|
|
```
|
|
|
Then we enter a `for` loop, running once for each red light to be created.
|
|
|
|
|
|
```java
|
|
|
RedLight light;
|
|
|
if (i < redPins.length) {
|
|
|
light = new RedLight(Integer.toString(i), redPins[i]);
|
|
|
} else {
|
|
|
light = new RedLight(Integer.toString(i));
|
|
|
}
|
|
|
```
|
|
|
a new RedLight is created. If we still have red GPIO ports left, we hand the GPIO port for this light over on creation, if not the constructor without will be used. See [here](Walkthrough-for-the-general-light-classes) for the declaration.
|
|
|
|
|
|
```java
|
|
|
LightSwitchSubmodelProvider lssp = new LightSwitchSubmodelProvider("Lightstrand:redLight_" + light.getID());
|
|
|
```
|
|
|
The `LightSwitchSubmodelProvider` class handles the creation of the light submodels, hosting them, registering them and uploading the AAS of the lights created. On Construction, we hand it a locally unique ID for the Light for this host.
|
|
|
|
|
|
```java
|
|
|
lssp.hostUploadAndRegister(light, "redLight_" + light.getID(), "redLight_" + light.getID(), HOSTPORT + i)
|
|
|
```
|
|
|
Then, we let the just created instance handle all the linking with BaSyx required. The class is explained below.
|
|
|
|
|
|
Finally, the same is done with the green lights. Only non-trivial difference is, that the ports passed to the Provider start at `HOSTPORT+redLights`.
|
|
|
|
|
|
# LightSwitchSubmodelProvider
|
|
|
|
|
|
The discussed file can be found [here](https://gitlab.hrz.tu-chemnitz.de/s6869070--tu-dresden.de/vws-spielwiese/-/blob/6d65d24412bda2e555e70a4f9581eb142096bd49/basyx.lichterkette/src/main/java/de/olipar/basyx/lichterkette/LightSwitchSubmodelProvider.java).
|
|
|
|
|
|
We'll skip the license headers, imports and comments, as they are either pretty much self explanatory or not the product of conscious development effort.
|
|
|
It is advisable to take a look at the file, as it is heavily commented.
|
|
|
|
|
|
```java
|
|
|
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(LightSwitchSubmodelProvider.class);
|
|
|
```
|
|
|
Just like above, this class (as does any, really) uses a logger instance.
|
|
|
|
|
|
```java
|
|
|
private final String uniqueID;
|
|
|
```
|
|
|
This is the locally unique ID of the submodel to be created, It is required and set by the Constructor:
|
|
|
```java
|
|
|
public LightSwitchSubmodelProvider(String uniqueID) {
|
|
|
this.uniqueID = uniqueID;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
```java
|
|
|
public Submodel createMyLightSwitchModel(ILight light) {
|
|
|
```
|
|
|
This method creates Submodel for controlling the light its AAS represents. It requires an implementation of [`ILight`](Walkthrough-for-the-general-light-classes#ilight), to which it will bind the Operations.
|
|
|
|
|
|
```java
|
|
|
Submodel lightSwitchSubmodel = new Submodel();
|
|
|
lightSwitchSubmodel.setIdShort("lightSwitch");
|
|
|
lightSwitchSubmodel.setIdentification(new ModelUrn("LightSwitch" + uniqueID));
|
|
|
```
|
|
|
First, a new Submodel is created. Then it's short ID is set, which must be locally unique in the context of the AAS this submodel will be part of. Then a unique identifier is also set, but from my testing it appears, that this is neither strictly neccessary, nor a problem, if it's not globally unique.
|
|
|
|
|
|
```java
|
|
|
Function<Object[], Object> activateFunction = (args) -> {
|
|
|
light.activate();
|
|
|
return null;
|
|
|
};
|
|
|
```
|
|
|
In order to bind the `activate` and `deactivate` methods of the light, we need to create a `Function` object. This function here only activates the light and returns `null`. Upon invocation of the method, this return value is returned as the response body.
|
|
|
|
|
|
```java
|
|
|
Operation activateOperation = new Operation(activateFunction);
|
|
|
activateOperation.setIdShort("activateLight");
|
|
|
```
|
|
|
We then create a new `Operation` object, which also requires a short ID, locally unique to the submodel it's part of.
|
|
|
|
|
|
```java
|
|
|
lightSwitchSubmodel.addSubmodelElement(activateOperation);
|
|
|
```
|
|
|
Here, we add the newly created Operation to the Submodel instantiated earlier.
|
|
|
|
|
|
We then do the same for the deactivation Function and finally return the created Submodel.
|
|
|
|
|
|
```java
|
|
|
public void hostUploadAndRegister(ILight light, String AASID, String AASShortID, int port) {
|
|
|
```
|
|
|
This method does quire a few things. It invokes the creation of a Sumbodel for the light and hosts it, then creates an AAS, adds the submodel uploads it to the [AAS Server](What-am-I-looking-at-here#aas-server) and registers the AAS and its submodel.
|
|
|
|
|
|
```java
|
|
|
final String HOSTADDRESS = Common.getHostAddress();
|
|
|
logger.info("Local host address is set to " + HOSTADDRESS);
|
|
|
logger.info("Local host port is set to " + port);
|
|
|
final String REGISTRYPATH = Common.getRegistryPath();
|
|
|
logger.info("Registry path set to " + REGISTRYPATH);
|
|
|
final String AASSERVERPATH = Common.getAASServerPath();
|
|
|
logger.info("AAS server path set to " + AASSERVERPATH);
|
|
|
```
|
|
|
We start off by getting all three values the [`Common`-class](Common-class-walkthrough) has to offer and log them.
|
|
|
|
|
|
```java
|
|
|
Submodel lightSwitchModel = createMyLightSwitchModel(light);
|
|
|
```
|
|
|
Then, we create a Submodel for the light given.
|
|
|
|
|
|
```java
|
|
|
IModelProvider modelProvider = new SubmodelProvider(lightSwitchModel);
|
|
|
BaSyxContext context = new BaSyxContext("/handson", "", HOSTADDRESS, port);
|
|
|
```
|
|
|
Then, a provider for the submodel is created and the host context defined. This context applies to all Servlets hosted later on, but we'll only host the one created next:
|
|
|
|
|
|
```java
|
|
|
HttpServlet modelServlet = new VABHTTPInterface<IModelProvider>(modelProvider);
|
|
|
logger.info("Created a servlet for the lightswitch model");
|
|
|
```
|
|
|
|
|
|
```java
|
|
|
context.addServletMapping("/lightSwitch/*", modelServlet);
|
|
|
```
|
|
|
We then add the Servlet to the context set above.
|
|
|
|
|
|
We then create and start the server:
|
|
|
```java
|
|
|
BaSyxHTTPServer server = new BaSyxHTTPServer(context);
|
|
|
server.start();
|
|
|
logger.info("HTTP server started");
|
|
|
```
|
|
|
The submodel can then be reached at `http://<HOSTADDRESS>:<port>/handson/lightSwitch/submodel`
|
|
|
|
|
|
We then continue to create the AAS for the light:
|
|
|
```java
|
|
|
ModelUrn aasURN = new ModelUrn("urn:de.olipar.basyx:" + HOSTADDRESS + ":" + AASID);
|
|
|
```
|
|
|
First, we create a URN for the AAS, this will serve as the GUID of the AAS, which has to be globally unique. We use the AASID passed to the method as well as the HOSTADDRESS for this.
|
|
|
|
|
|
```java
|
|
|
Asset asset = new Asset("lightSwitchAsset", aasURN, AssetKind.INSTANCE);
|
|
|
AssetAdministrationShell aas = new AssetAdministrationShell(AASShortID, aasURN, asset);
|
|
|
aas.addSubmodel(lightSwitchModel);
|
|
|
```
|
|
|
Then we create a new digital representation of the light and a AAS for it and add the previously created Submodel to the AAS.
|
|
|
|
|
|
```java
|
|
|
SubmodelDescriptor lightSwitchSMDescriptor = new SubmodelDescriptor(lightSwitchModel, "http://" + HOSTADDRESS + ":" + Integer.toString(port) + "/handson/lightSwitch/submodel");
|
|
|
```
|
|
|
We then create an descriptor for the submodel, linking the submodel object to the hosted endpoint for it.
|
|
|
|
|
|
```java
|
|
|
AASRegistryProxy registry = new AASRegistryProxy(REGISTRYPATH);
|
|
|
ConnectedAssetAdministrationShellManager manager = new ConnectedAssetAdministrationShellManager(registry);
|
|
|
```
|
|
|
Then, we create a proxy object for the registry and create an AAS manager. This manager is a very powerful thing to have, as it allows us to upload AAS and even submodels (without Operations) to the AAS server, also taking care of registering them. Submodels with Operations can be uploaded, too but they will cease to work.
|
|
|
|
|
|
```java
|
|
|
manager.createAAS(aas, AASSERVERPATH);
|
|
|
```
|
|
|
Just like that, the manager uploads the just created AAS to the AAS Server and registers it at the registry.
|
|
|
|
|
|
```java
|
|
|
registry.register(aas.getIdentification(), lightSwitchSMDescriptor);
|
|
|
```
|
|
|
We then register the Submodel hosted here at the registry, supplying the rest of the BaSyx System with means to lookup the capabilities and the Endpoints.
|
|
|
|
|
|
```java
|
|
|
Runtime.getRuntime().addShutdownHook(new Thread() {
|
|
|
public void run() {
|
|
|
System.out.println("Shutting down Light Switch...");
|
|
|
manager.deleteSubmodel(aas.getIdentification(), lightSwitchSMDescriptor.getIdentifier());
|
|
|
manager.deleteAAS(aas.getIdentification());
|
|
|
}
|
|
|
});
|
|
|
```
|
|
|
|
|
|
Here, we add a hook to be executed, when the application is to be shut down. It deletes the AAS from the AAS Server and deregisters both AAS and Submodel created before the application exits. |