|
|
Here, I'll walk you through the source code of the [Demo Controller](TODO), running on an ESP32 microcontroller.
|
|
|
|
|
|
The discussed file can be found [here](TODO)
|
|
|
|
|
|
```
|
|
|
#define ANIMATION1_SW 27
|
|
|
#define ANIMATION2_SW 26
|
|
|
#define ANIMATOR_STOP_SW 25
|
|
|
|
|
|
#define RED_LIGHT_SW 33
|
|
|
#define GREEN_LIGHT_SW 32
|
|
|
#define ALL_LIGHTS_SW 35
|
|
|
#define BLINK_SW 34
|
|
|
#define CONTROLLER_STOP_SW 22
|
|
|
|
|
|
#define UPDATE_SW 13
|
|
|
|
|
|
#define ANIMATOR_RED 5
|
|
|
#define ANIMATOR_GREEN 18
|
|
|
#define CONTROLLER_RED 19
|
|
|
#define CONTROLLER_GREEN 21
|
|
|
|
|
|
#define SERVER_RED 15
|
|
|
#define SERVER_GREEN 2
|
|
|
#define REGISTRY_RED 4
|
|
|
#define REGISTRY_GREEN 23
|
|
|
```
|
|
|
These Definitions link the GPIO ports of the ESP32 to readable names, according to their purpose. See [wiring up the demo](TODO) for more information.
|
|
|
|
|
|
```
|
|
|
#define RED_LED 4
|
|
|
#define GREEN_LED 2
|
|
|
```
|
|
|
Here, the GPIO pins used for the red and green LEDs attached to the ESP32 Dev Board are defined. If you were to diverge from the [default wiring of the demo](TODO), you'll need to adjust the GPIO numbers here.
|
|
|
|
|
|
```
|
|
|
#include <ESPmDNS.h>
|
|
|
#include <HTTPClient.h>
|
|
|
```
|
|
|
Here we include the headers for both the HTTPClient library and the ESPmDNS, which both come as part of espressif's standard library for the ESP32.
|
|
|
ESPmDNS is used to resolve the local address of the [AAS Server](TODO) `aas-server.local` and the [registry](TODO) `registry.local`. HTTPClient is used to make requests to the REST API endpoints exposed by BaSyx.
|
|
|
The separate resolution is neccessary, as `HTTPClient` doesn't resolve them itself.
|
|
|
|
|
|
```c++
|
|
|
const char* ssid = "";
|
|
|
const char* password = "";
|
|
|
```
|
|
|
Here, the SSID and password of the WiFi network are set. You'll need to set them according to the [secrets of the Demo](TODO).
|
|
|
|
|
|
```c++
|
|
|
unsigned long last_mDNS_refresh = 0;
|
|
|
```
|
|
|
This variable is used to keep track of when the last mDNS resolution of the names above happened.
|
|
|
While on the Raspberry Pis the underlying network stack takes care of this, on this platform, we need to handle this ourself.
|
|
|
|
|
|
```c++
|
|
|
IPAddress AASServerIP = IPAddress(0, 0, 0, 0);
|
|
|
IPAddress RegistryIP = IPAddress(0, 0, 0, 0);
|
|
|
```
|
|
|
In these `IPAddress`es, the resolved ip addresses will be stored in.
|
|
|
|
|
|
```c++
|
|
|
HTTPClient http;
|
|
|
```
|
|
|
This client will be used to make the necessary requests to various endpoints exposed by BaSyx
|
|
|
|
|
|
```c++
|
|
|
void resolve_mdns(String name, IPAddress* target)
|
|
|
{
|
|
|
```
|
|
|
This method resolves the given name into the space of the given `IPAddress`.
|
|
|
|
|
|
```c++
|
|
|
do {
|
|
|
*target = MDNS.queryHost(name);
|
|
|
Serial.println("Resolving for " + name + ".local...");
|
|
|
} while (*target == IPAddress(0, 0, 0, 0));
|
|
|
```
|
|
|
To do this, we use the `MDNS` object given by the library to resolve the given `name` into the given `IPAddress`. We continue to request resolution until the resolution yielded an IP different from zero, as `queryHost` will set the IP to zero if resolution fails.
|
|
|
|
|
|
```c++
|
|
|
Serial.print(name + ".local found at ");
|
|
|
Serial.println(AASServerIP);
|
|
|
```
|
|
|
When the resolution succeeded, we print it and conclude the method.
|
|
|
|
|
|
```c++
|
|
|
void setup()
|
|
|
{
|
|
|
pinMode(ANIMATION1_SW, INPUT);
|
|
|
pinMode(ANIMATION2_SW, INPUT);
|
|
|
...
|
|
|
```
|
|
|
The `setup()` function is called once on startup of the ESP32. Here, all one-time setup happens, including the initialization of the GPIO ports assigned names above. Switches are `INPUT`s and LEDs are `OUTPUT`s.
|
|
|
|
|
|
```c++
|
|
|
Serial.begin(115200);
|
|
|
```
|
|
|
This initializes the serial interface.. You can connect via USB, it communicates at 115200 baud. This is very handy for debugging and troubleshooting.
|
|
|
|
|
|
```c++
|
|
|
WiFi.begin(ssid, password);
|
|
|
```
|
|
|
This initializes the WiFi interface and attempts connecting to the specified network.
|
|
|
|
|
|
```c++
|
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
|
delay(1000);
|
|
|
Serial.println("Connecting to Wifi...");
|
|
|
}
|
|
|
```
|
|
|
Using `WiFi.status()`, one can inquire about the status of the wireless interface. Here, we'll check whether the connection to the network has been established every second while sending `Connecting to Wifi...` to the serial interface.
|
|
|
Once connection to the network is established, the loop will exit and execution will continue.
|
|
|
|
|
|
```c++
|
|
|
mdns_init();
|
|
|
```
|
|
|
Initializes the mDNS resolution for the ESP32
|
|
|
|
|
|
```c++
|
|
|
resolve_mdns("aas-server", &AASServerIP);
|
|
|
resolve_mdns("registry", &RegistryIP);
|
|
|
```
|
|
|
Lastly in the setup, we'll resolve the addresses for the first time.
|
|
|
|
|
|
Note, that the mDNS implementation on the ESP32 will add `.local` to addresses to be resolved via mDNS. This might cause a problem in some networks, see [this discussion](https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300). To my knowledge, this also differs from the behaviour of an ESP2866.
|
|
|
Per default configuration, this should not cause a problem in this demo though.
|
|
|
|
|
|
```c++
|
|
|
bool isAASServerUp()
|
|
|
{
|
|
|
```
|
|
|
We then begin to define a few methods used to incapsulate making the relevant requests to the BaSyx endpoints defined within.
|
|
|
This method in particular returns `true`, if the call to the AAS Servers root endpoint succeeded.
|
|
|
|
|
|
```c++
|
|
|
bool ret;
|
|
|
String request = "http://" + AASServerIP.toString() + ":4000/aasServer/shells/";
|
|
|
```
|
|
|
To do this, we need to define the Endpoint we want to query. We do so after declaring our return variable and using the resolved IP of the AAS Server.
|
|
|
The unresolved endpoint is `http://aas-server.local:4000/aasServer/shells/`
|
|
|
|
|
|
```c++
|
|
|
http.begin(request);
|
|
|
ret = (http.GET() == 200);
|
|
|
http.end();
|
|
|
```
|
|
|
After having defined our request endpoint, we initialize the connection using `http.begin`. We then send a `GET` request to the specified endpoint and store whether or not the request was successful in our return variable. `200` being the [HTTP Status Code] indicating success.
|
|
|
`http.end()` closes the connection again.
|
|
|
|
|
|
```c++
|
|
|
//Remember: LEDs are active LOW
|
|
|
digitalWrite(SERVER_GREEN, ret ? LOW : HIGH);
|
|
|
digitalWrite(SERVER_RED, ret ? HIGH : LOW);
|
|
|
return ret;
|
|
|
```
|
|
|
Keeping in mind that the LEDs are active LOW, we then set the output pins for the corresponding red and green status lights attached.
|
|
|
The usual way to do this is via either `digitalWrite(<PIN>, HIGH)` or `digitalWrite(<PIN>, LOW)`, with `<PIN>` being the corresponding GPIO number to be assigned digital `HIGH` or `LOW`. The number is resolved by our definitions above and the latter part (`ret ? LOW : HIGH`) is a shorthand for the below:
|
|
|
```c++
|
|
|
if (ret)
|
|
|
LOW
|
|
|
else
|
|
|
HIGH
|
|
|
```
|
|
|
If you get keep that shorthand in mind, code can sometimes become more readable and concise, which I think is the case here.
|
|
|
Note, that the assignments for the red and green status LEDs for the AAS Server status are complementary to each other.
|
|
|
Green is lit, when the AAS Server is up, red, when the request failed.
|
|
|
|
|
|
```c++
|
|
|
bool isRegistryUp()
|
|
|
{
|
|
|
```
|
|
|
The same is done for the registry and it's status LEDs. This time the request goes to `http://registry.local:4000/registry/api/v1/registry`
|
|
|
|
|
|
```c++
|
|
|
bool isLightControllerActive()
|
|
|
{
|
|
|
```
|
|
|
Testing wether or not the Light controller is active (ie. actively controlling the lights it knows) involves a bit more logic.
|
|
|
|
|
|
```c++
|
|
|
bool ret;
|
|
|
//Endpoint for the 'isActive' Property of the main Submodel of the LightController
|
|
|
String request = "http://" + AASServerIP.toString() + ":4000/aasServer/shells/urn:de.olipar.basyx:LightControllerAAS/aas/submodels/lightControllerSM/submodel/submodelElements/isActive/value";
|
|
|
http.begin(request);
|
|
|
ret = (http.GET() == 200);
|
|
|
```
|
|
|
The beginning should look familiar by now, this time the endpoint of our request is
|
|
|
`http://aas-server.local:4000/aasServer/shells/urn:de.olipar.basyx:LightControllerAAS/aas/submodels/lightControllerSM/submodel/submodelElements/isActive/value`
|
|
|
It pays to play around with the endpoints in a browser a bit. If you use Firefox, the json will be nicely formatted out of the box and you'll soon find your way round them. If you get stuck, there's always the [official API specification](TODO) available.
|
|
|
Let's walk through this URL backwards:
|
|
|
We want to get the `value` of the Property `isActive`, which is an element of the sumbodel `lightControllerSM` of the AAS, which is known as `urn:de.olipar.basyx:LightControllerAAS`.
|
|
|
|
|
|
Note, that if you use the AASX-Branch of this project, the GUID is different for the light controller AAS.
|
|
|
|
|
|
```c++
|
|
|
if (!ret) {
|
|
|
http.end();
|
|
|
digitalWrite(CONTROLLER_GREEN, ret ? LOW : HIGH);
|
|
|
digitalWrite(CONTROLLER_RED, ret ? HIGH : LOW);
|
|
|
return ret;
|
|
|
}
|
|
|
```
|
|
|
Should the request not be successfull (ie. the Status code of the response not be `200`) we'll switch the green LED off and the red one on. That means, that we assume a controller we can't reach doesn't control Lights, too, so we set our LEDs and return what we've learned.
|
|
|
|
|
|
```c++
|
|
|
ret = (http.getString() == "true");
|
|
|
http.end();
|
|
|
digitalWrite(CONTROLLER_GREEN, ret ? LOW : HIGH);
|
|
|
digitalWrite(CONTROLLER_RED, ret ? HIGH : LOW);
|
|
|
return ret;
|
|
|
```
|
|
|
The `http.GET()` we invoked to get the status code did also save the answer we got to the request into our http instance. With `http.getString()` we can get the response body and compare it to the value it would have, should the Controller be active, in this case the Endpoint returns either `true` or `false`.
|
|
|
We then close the connection, set the LEDs and return what we've learned.
|
|
|
|
|
|
```c++
|
|
|
bool isLightAnimatorActive()
|
|
|
{
|
|
|
```
|
|
|
The same is done for the Animator and its lights. The endpoint here is `http://aas-server.local:4000/aasServer/shells/urn:de.olipar.basyx:LightAnimatorAAS/aas/submodels/lightAnimatorSM/submodel/submodelElements/isActive/value`
|
|
|
|
|
|
```c++
|
|
|
void animation1(){
|
|
|
//Endpoint for the operation 'animation1' of the main Submodel of the LightAnimator
|
|
|
String request = "http://" + AASServerIP.toString() + ":4000/aasServer/shells/urn:de.olipar.basyx:LightAnimatorAAS/aas/submodels/lightAnimatorSM/submodel/submodelElements/animation1/invoke";
|
|
|
http.begin(request);
|
|
|
http.POST("");
|
|
|
http.end();
|
|
|
}
|
|
|
```
|
|
|
Invoking operations works similarly, difference being the endpoint, of course, and instead of sending a `GET` request, we send a `POST` request. The content of the `POST` request is irrelevant, so we just send an empty string.
|
|
|
|
|
|
The above is then defined for numerous wrappers, corresponding to the buttons present on the control breadboard.
|
|
|
|
|
|
```c++
|
|
|
void loop()
|
|
|
{
|
|
|
```
|
|
|
After `setup()`, this function is called endlessly.
|
|
|
|
|
|
```c++
|
|
|
if (millis() - last_mDNS_refresh > 60000) {
|
|
|
resolve_mdns("aas-server", &AASServerIP);
|
|
|
resolve_mdns("registry", &RegistryIP);
|
|
|
|
|
|
last_mDNS_refresh=millis();
|
|
|
}
|
|
|
```
|
|
|
Here, we check, whether the last refresh of the addresses resolved via mDNS happened more then a minute ago. If so, we resolve them again and set the time of the last mDNS refresh to the current time.
|
|
|
"Time" meaning here milliseconds since startup of the microcontroller
|
|
|
|
|
|
```c++
|
|
|
isRegistryUp();
|
|
|
isAASServerUp();
|
|
|
```
|
|
|
We first check the status of the Registry and AAS Server and update the status LEDs accordingly.
|
|
|
|
|
|
```c++
|
|
|
isLightControllerActive();
|
|
|
isLightAnimatorActive();
|
|
|
```
|
|
|
Then, we update the LEDs indicating, whether Animator and/or Controller are currently Active (ie. Controlling lights)
|
|
|
|
|
|
```c++
|
|
|
if(digitalRead(ANIMATION1_SW)){
|
|
|
Serial.println("invoke animation1");
|
|
|
animation1();
|
|
|
} else if
|
|
|
```
|
|
|
Then follows a long `if` - `else if `-segment, checking whether a button is currently being pressed and invoking the corresponding action.
|
|
|
`else if` is used to only act on one button being pressed, regardless of how many are being held down.
|
|
|
|
|
|
```c++
|
|
|
delay(100);
|
|
|
```
|
|
|
Lastly, we wait for 100ms before the next iteration. This is done to ease network load imposed by this controller a bit.
|
|
|
In the future a longer delay could be implemented using non-blocking code and keeping track of what button was pressed last, similar to what is done at the beginning of the `loop()`. |