Merge pull request 'master' (#3) from master into develop
Reviewed-on: #3
This commit is contained in:
commit
9d23788ac8
|
@ -60,3 +60,4 @@ test/build/
|
||||||
|
|
||||||
*.orig
|
*.orig
|
||||||
|
|
||||||
|
Doxygen/html/
|
||||||
|
|
15
README.md
15
README.md
|
@ -1,19 +1,4 @@
|
||||||
# ESP32-Mesh-OTA
|
# ESP32-Mesh-OTA
|
||||||
|
|
||||||
## Work in progress
|
|
||||||
<img src="https://patenschaft.bienenweide.org/img/loading.gif" alt="code_example_output" width="50"/>
|
|
||||||
|
|
||||||
## Todo for first release
|
|
||||||
|
|
||||||
### Refactoring
|
|
||||||
- architecture
|
|
||||||
- full error handling through all functions
|
|
||||||
- export as a component library
|
|
||||||
|
|
||||||
### Features
|
|
||||||
- root node: Download new Firmware from HTTPS server instead using newest ota partition
|
|
||||||
- add example main app
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,6 @@ esp_err_t errMeshOTAInitialize(void)
|
||||||
err = ESP_FAIL;
|
err = ESP_FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +161,6 @@ void vMeshOtaTaskServerWorker(void *arg)
|
||||||
//set want reboot
|
//set want reboot
|
||||||
ESP_LOGI(LOG_TAG, "Updated successfully via HTTPS, set pending reboot");
|
ESP_LOGI(LOG_TAG, "Updated successfully via HTTPS, set pending reboot");
|
||||||
bWantReboot = true;
|
bWantReboot = true;
|
||||||
vMeshOtaUtilAddAllNeighboursToQueue(); //add all existing neighbours to queue (aparent will not be added because this node is the root)
|
|
||||||
}
|
}
|
||||||
vTaskDelay( (SERVER_CHECK_INTERVAL*1000) / portTICK_PERIOD_MS); //sleep till next server checks
|
vTaskDelay( (SERVER_CHECK_INTERVAL*1000) / portTICK_PERIOD_MS); //sleep till next server checks
|
||||||
}
|
}
|
||||||
|
@ -182,6 +180,7 @@ void vMeshOtaTaskOTAWorker(void *arg)
|
||||||
esp_err_t err = ESP_OK;
|
esp_err_t err = ESP_OK;
|
||||||
bool bNewOTAImage; //true if a new ota image was downloaded and validated
|
bool bNewOTAImage; //true if a new ota image was downloaded and validated
|
||||||
mesh_addr_t meshNodeAddr; //node that should be checked for ota update
|
mesh_addr_t meshNodeAddr; //node that should be checked for ota update
|
||||||
|
BaseType_t xReturned;
|
||||||
|
|
||||||
while(true)
|
while(true)
|
||||||
{
|
{
|
||||||
|
@ -190,25 +189,30 @@ void vMeshOtaTaskOTAWorker(void *arg)
|
||||||
|
|
||||||
if((uxQueueSpacesAvailable(queueNodes) - QUEUE_NODES_SIZE) == 0)
|
if((uxQueueSpacesAvailable(queueNodes) - QUEUE_NODES_SIZE) == 0)
|
||||||
{
|
{
|
||||||
//nodes queue is empty
|
ESP_LOGI(LOG_TAG, "//nodes queue is empty");
|
||||||
if((bWantReboot == true) && (OTA_ALLOW_REBOOT == 1))
|
|
||||||
|
xReturned = xSemaphoreTake(bsOTAProcess, portMAX_DELAY); //wait for binary semaphore that allows to start the OTA process
|
||||||
|
if((xReturned == pdTRUE) && (bWantReboot == true) && (OTA_ALLOW_REBOOT == 1))
|
||||||
{
|
{
|
||||||
ESP_LOGE(LOG_TAG, "ESP32 Reboot ...");
|
ESP_LOGE(LOG_TAG, "ESP32 Reboot ...");
|
||||||
vTaskDelay( (1000) / portTICK_PERIOD_MS);
|
vTaskDelay( (1000) / portTICK_PERIOD_MS);
|
||||||
esp_restart();
|
esp_restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xSemaphoreGive(bsOTAProcess); //free binary semaphore, this allows other tasks to start the OTA process
|
||||||
ERROR_CHECK(errMeshOtaSlaveEndpoint(&bNewOTAImage));
|
ERROR_CHECK(errMeshOtaSlaveEndpoint(&bNewOTAImage));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//queue not empty
|
ESP_LOGI(LOG_TAG, "//queue not empty %i", (uxQueueSpacesAvailable(queueNodes) - QUEUE_NODES_SIZE));
|
||||||
if (xQueueReceive(queueNodes, &meshNodeAddr, ((100) / portTICK_PERIOD_MS)) != pdTRUE)
|
if (xQueueReceive(queueNodes, &meshNodeAddr, ((100) / portTICK_PERIOD_MS)) != pdTRUE)
|
||||||
{
|
{
|
||||||
ESP_LOGE(LOG_TAG, "Unable to receive OTA Messages from Queue");
|
ESP_LOGE(LOG_TAG, "Unable to receive OTA Messages from Queue");
|
||||||
err = ESP_FAIL;
|
err = ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(LOG_TAG, "//handle node %x", meshNodeAddr.addr[5]);
|
||||||
|
|
||||||
ERROR_CHECK(errMeshOtaMasterEndpoint(&bNewOTAImage, &meshNodeAddr));
|
ERROR_CHECK(errMeshOtaMasterEndpoint(&bNewOTAImage, &meshNodeAddr));
|
||||||
|
|
||||||
if (err != ESP_OK)
|
if (err != ESP_OK)
|
||||||
|
@ -275,7 +279,7 @@ esp_err_t errMeshOtaSlaveEndpoint(bool* const cpbNewOTAImage)
|
||||||
if((bMeshOtaUtilNewerVersion((bootPartitionDesc).version, (char*) sOTAMessage.au8Payload)) && (err == ESP_OK)) //compare local and remote version
|
if((bMeshOtaUtilNewerVersion((bootPartitionDesc).version, (char*) sOTAMessage.au8Payload)) && (err == ESP_OK)) //compare local and remote version
|
||||||
{
|
{
|
||||||
//remote newer as local
|
//remote newer as local
|
||||||
ESP_LOGI(LOG_TAG, "remote image on node is newer --> OTA update required");
|
ESP_LOGI(LOG_TAG, "remote image on node is newer --> OTA update required from node \"%x:%x:%x:%x:%x:%x\"", sOTAMessage.meshSenderAddr.addr[0], sOTAMessage.meshSenderAddr.addr[1], sOTAMessage.meshSenderAddr.addr[2], sOTAMessage.meshSenderAddr.addr[3], sOTAMessage.meshSenderAddr.addr[4], sOTAMessage.meshSenderAddr.addr[5]);
|
||||||
// --> this version older --> start OTA_Rx --> set cpbNewOTAImage true
|
// --> this version older --> start OTA_Rx --> set cpbNewOTAImage true
|
||||||
ERROR_CHECK(errMeshOtaPartitionAccessMeshReceive(cpbNewOTAImage, &sOTAMessage.meshSenderAddr));
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshReceive(cpbNewOTAImage, &sOTAMessage.meshSenderAddr));
|
||||||
}
|
}
|
||||||
|
@ -283,7 +287,7 @@ esp_err_t errMeshOtaSlaveEndpoint(bool* const cpbNewOTAImage)
|
||||||
if((bMeshOtaUtilNewerVersion((char*) sOTAMessage.au8Payload, (bootPartitionDesc).version)) && (err == ESP_OK)) //compare remote and local version
|
if((bMeshOtaUtilNewerVersion((char*) sOTAMessage.au8Payload, (bootPartitionDesc).version)) && (err == ESP_OK)) //compare remote and local version
|
||||||
{
|
{
|
||||||
//local newer as remote
|
//local newer as remote
|
||||||
ESP_LOGI(LOG_TAG, "remote image on node is older --> OTA send required");
|
ESP_LOGI(LOG_TAG, "remote image on node is older --> OTA send required to node \"%x:%x:%x:%x:%x:%x\"", sOTAMessage.meshSenderAddr.addr[0], sOTAMessage.meshSenderAddr.addr[1], sOTAMessage.meshSenderAddr.addr[2], sOTAMessage.meshSenderAddr.addr[3], sOTAMessage.meshSenderAddr.addr[4], sOTAMessage.meshSenderAddr.addr[5]);
|
||||||
// --> this version newer --> start OTA_Tx
|
// --> this version newer --> start OTA_Tx
|
||||||
ERROR_CHECK(errMeshOtaPartitionAccessMeshTransmit(&sOTAMessage.meshSenderAddr));
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshTransmit(&sOTAMessage.meshSenderAddr));
|
||||||
}
|
}
|
||||||
|
@ -343,7 +347,7 @@ esp_err_t errMeshOtaMasterEndpoint(bool* const cpbNewOTAImage, const mesh_addr_t
|
||||||
if((bMeshOtaUtilNewerVersion((bootPartitionDesc).version, (char*) sOTAMessage.au8Payload)) && (err == ESP_OK)) //compare local and remote version
|
if((bMeshOtaUtilNewerVersion((bootPartitionDesc).version, (char*) sOTAMessage.au8Payload)) && (err == ESP_OK)) //compare local and remote version
|
||||||
{
|
{
|
||||||
//remote newer as local
|
//remote newer as local
|
||||||
ESP_LOGI(LOG_TAG, "Mesh: remote image on node is newer --> OTA update required");
|
ESP_LOGI(LOG_TAG, "remote image on node is newer --> OTA update required from node \"%x:%x:%x:%x:%x:%x\"", sOTAMessage.meshSenderAddr.addr[0], sOTAMessage.meshSenderAddr.addr[1], sOTAMessage.meshSenderAddr.addr[2], sOTAMessage.meshSenderAddr.addr[3], sOTAMessage.meshSenderAddr.addr[4], sOTAMessage.meshSenderAddr.addr[5]);
|
||||||
// --> this version older --> start OTA_Rx --> set cpbNewOTAImage true
|
// --> this version older --> start OTA_Rx --> set cpbNewOTAImage true
|
||||||
ERROR_CHECK(errMeshOtaPartitionAccessMeshReceive(cpbNewOTAImage, &sOTAMessage.meshSenderAddr));
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshReceive(cpbNewOTAImage, &sOTAMessage.meshSenderAddr));
|
||||||
}
|
}
|
||||||
|
@ -351,7 +355,7 @@ esp_err_t errMeshOtaMasterEndpoint(bool* const cpbNewOTAImage, const mesh_addr_t
|
||||||
if((bMeshOtaUtilNewerVersion((char*) sOTAMessage.au8Payload, (bootPartitionDesc).version)) && (err == ESP_OK)) //compare remote and local version
|
if((bMeshOtaUtilNewerVersion((char*) sOTAMessage.au8Payload, (bootPartitionDesc).version)) && (err == ESP_OK)) //compare remote and local version
|
||||||
{
|
{
|
||||||
//local newer as remote
|
//local newer as remote
|
||||||
ESP_LOGI(LOG_TAG, "Mesh: remote image on node is older --> OTA send required");
|
ESP_LOGI(LOG_TAG, "remote image on node is older --> OTA send required to node \"%x:%x:%x:%x:%x:%x\"", sOTAMessage.meshSenderAddr.addr[0], sOTAMessage.meshSenderAddr.addr[1], sOTAMessage.meshSenderAddr.addr[2], sOTAMessage.meshSenderAddr.addr[3], sOTAMessage.meshSenderAddr.addr[4], sOTAMessage.meshSenderAddr.addr[5]);
|
||||||
// --> this version newer --> start OTA_Tx
|
// --> this version newer --> start OTA_Tx
|
||||||
ERROR_CHECK(errMeshOtaPartitionAccessMeshTransmit(&sOTAMessage.meshSenderAddr));
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshTransmit(&sOTAMessage.meshSenderAddr));
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,7 @@ esp_err_t errMeshOtaPartitionAccessHttps(bool* const cpbNewOTAImage)
|
||||||
if(err == ESP_OK)
|
if(err == ESP_OK)
|
||||||
{
|
{
|
||||||
*cpbNewOTAImage = true; //image validated
|
*cpbNewOTAImage = true; //image validated
|
||||||
|
vMeshOtaUtilAddAllNeighboursToQueue(); //add all existing neighbours to queue (aparent will not be added because this node is the root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -328,17 +328,24 @@ void vMeshOtaUtilClearOtaMessageQueue(const mesh_addr_t* const cpcMeshNodeAddr)
|
||||||
void vMeshOtaUtilClearNeighboursQueue(const mesh_addr_t* const cpcMeshNodeAddr)
|
void vMeshOtaUtilClearNeighboursQueue(const mesh_addr_t* const cpcMeshNodeAddr)
|
||||||
{
|
{
|
||||||
mesh_addr_t sNode; //packet for sending and receiving
|
mesh_addr_t sNode; //packet for sending and receiving
|
||||||
for (uint32_t u32Index = 0; (u32Index < QUEUE_MESSAGE_OTA_SIZE); u32Index++) //loop through all OTA messages
|
for (uint32_t u32Index = 0; (u32Index < QUEUE_NODES_SIZE); u32Index++) //loop through all node queue
|
||||||
{
|
{
|
||||||
if (xQueueReceive(queueNodes, &sNode, 0) == pdTRUE)
|
if (xQueueReceive(queueNodes, &sNode, 0) == pdTRUE)
|
||||||
{
|
{
|
||||||
if(!(bMeshNetworkCheckMacEquality(sNode.addr, cpcMeshNodeAddr->addr)))
|
if((bMeshNetworkCheckMacEquality(sNode.addr, cpcMeshNodeAddr->addr)))
|
||||||
{
|
{
|
||||||
//node is NOT cpcMeshNodeAddr --> keep it in queue
|
ESP_LOGI(LOG_TAG, "REMOVE_NODE: remove node %x", sNode.addr[5]);
|
||||||
vMeshOtaUtilAddNodeToPossibleUpdatableQueue(cpcMeshNodeAddr->addr);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGI(LOG_TAG, "//node is NOT cpcMeshNodeAddr: %i --> keep it in queue", sNode.addr[5]);
|
||||||
|
vMeshOtaUtilAddNodeToPossibleUpdatableQueue(sNode.addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
}else{
|
||||||
|
ESP_LOGI(LOG_TAG, "REMOVE_NODE: queue leer");
|
||||||
}
|
}
|
||||||
}//end OTA message loop
|
}//end nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -361,7 +368,7 @@ void vMeshOtaUtilAddNodeToPossibleUpdatableQueue(const uint8_t* const cpcu8MAC)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ESP_LOGD(LOG_TAG, "added node \"%x:%x:%x:%x:%x:%x\" to possible updatable queue", addrNode.addr[0], addrNode.addr[1], addrNode.addr[2], addrNode.addr[3], addrNode.addr[4], addrNode.addr[5]);
|
ESP_LOGI(LOG_TAG, "added node \"%x:%x:%x:%x:%x:%x\" to possible updatable queue", addrNode.addr[0], addrNode.addr[1], addrNode.addr[2], addrNode.addr[3], addrNode.addr[4], addrNode.addr[5]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,14 +18,14 @@
|
||||||
#include "HTTPS_Client.h"
|
#include "HTTPS_Client.h"
|
||||||
|
|
||||||
#define ERASE_NVS //erase non volatile storage if full
|
#define ERASE_NVS //erase non volatile storage if full
|
||||||
#define QUEUE_NODES_SIZE 10
|
#define QUEUE_NODES_SIZE 20
|
||||||
#define QUEUE_MESSAGE_OTA_SIZE 10
|
#define QUEUE_MESSAGE_OTA_SIZE 20
|
||||||
#define SERVER_CHECK_INTERVAL 30 //in seconds
|
#define SERVER_CHECK_INTERVAL 30 //in seconds
|
||||||
#define OTA_HTTPS_SEGMENT_SIZE 2048U
|
#define OTA_HTTPS_SEGMENT_SIZE 2048U
|
||||||
#define OTA_PROGRESS_LOG_INTERVAL 7U
|
#define OTA_PROGRESS_LOG_INTERVAL 7U
|
||||||
#define OTA_MESH_SEGMENT_SIZE MESH_NETWORK_PAYLOAD_SIZE
|
#define OTA_MESH_SEGMENT_SIZE MESH_NETWORK_PAYLOAD_SIZE
|
||||||
#define OTA_MESH_TIMEOUT 20000U //in ms
|
#define OTA_MESH_TIMEOUT 20000U //in ms
|
||||||
#define OTA_ALLOW_REBOOT 0
|
#define OTA_ALLOW_REBOOT 1
|
||||||
|
|
||||||
#define ERROR_CHECK(x) if (err == ESP_OK) \
|
#define ERROR_CHECK(x) if (err == ESP_OK) \
|
||||||
{ \
|
{ \
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include "unity.h"
|
#include "unity.h"
|
||||||
|
|
||||||
#include "Mesh_OTA.h"
|
#include "Mesh_OTA_Util.h"
|
||||||
#include "test_image_hex.h"
|
#include "test_image_hex.h"
|
||||||
|
|
||||||
// ### ### ### distinguish newer image version ### ### ###
|
// ### ### ### distinguish newer image version ### ### ###
|
||||||
|
|
|
@ -61,6 +61,11 @@ esp_err_t errBlinkyLEDInitialize(void)
|
||||||
err = ESP_FAIL;
|
err = ESP_FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef NEW_VERSION
|
||||||
|
gpio_set_level(GPIO_LED_GREEN, 1); //switch on
|
||||||
|
#endif
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,8 +84,13 @@ void vGPIOInitialize(void)
|
||||||
gpio_config_t gpioConf;
|
gpio_config_t gpioConf;
|
||||||
|
|
||||||
//LED as Output
|
//LED as Output
|
||||||
gpio_reset_pin(GPIO_LED);
|
gpio_reset_pin(GPIO_LED_BLUE);
|
||||||
gpio_set_direction(GPIO_LED, GPIO_MODE_OUTPUT);
|
gpio_set_direction(GPIO_LED_BLUE, GPIO_MODE_OUTPUT);
|
||||||
|
|
||||||
|
#ifdef NEW_VERSION
|
||||||
|
gpio_reset_pin(GPIO_LED_GREEN);
|
||||||
|
gpio_set_direction(GPIO_LED_GREEN, GPIO_MODE_OUTPUT);
|
||||||
|
#endif
|
||||||
|
|
||||||
//BTN as Input
|
//BTN as Input
|
||||||
gpioConf.intr_type = GPIO_INTR_DISABLE;
|
gpioConf.intr_type = GPIO_INTR_DISABLE;
|
||||||
|
@ -212,19 +222,19 @@ void vTaskReceiveData(void *arg)
|
||||||
{
|
{
|
||||||
case LED_ON:
|
case LED_ON:
|
||||||
bLEDisOn = true;
|
bLEDisOn = true;
|
||||||
gpio_set_level(GPIO_LED, 1); //switch on
|
gpio_set_level(GPIO_LED_BLUE, 1); //switch on
|
||||||
ESP_LOGI(LOG_TAG,"switch LED ON");
|
ESP_LOGI(LOG_TAG,"switch LED ON");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LED_OFF:
|
case LED_OFF:
|
||||||
bLEDisOn = false;
|
bLEDisOn = false;
|
||||||
gpio_set_level(GPIO_LED, 0); //switch off
|
gpio_set_level(GPIO_LED_BLUE, 0); //switch off
|
||||||
ESP_LOGI(LOG_TAG,"switch LED OFF");
|
ESP_LOGI(LOG_TAG,"switch LED OFF");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
bLEDisOn = false;
|
bLEDisOn = false;
|
||||||
gpio_set_level(GPIO_LED, 0); //switch off
|
gpio_set_level(GPIO_LED_BLUE, 0); //switch off
|
||||||
ESP_LOGI(LOG_TAG,"switch LED OFF");
|
ESP_LOGI(LOG_TAG,"switch LED OFF");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,13 @@
|
||||||
|
|
||||||
#include "Mesh_OTA.h"
|
#include "Mesh_OTA.h"
|
||||||
|
|
||||||
|
//#define NEW_VERSION
|
||||||
|
|
||||||
#define GPIO_BOOT_BTN 0 //GPIO0 (Boot BTN)
|
#define GPIO_BOOT_BTN 0 //GPIO0 (Boot BTN)
|
||||||
#define GPIO_LED 2 //GPIO2 (internal blue LED in DevKit V1.0)
|
#define GPIO_LED_BLUE 2 //GPIO2 (internal blue LED in DevKit V1.0)
|
||||||
|
#ifdef NEW_VERSION
|
||||||
|
#define GPIO_LED_GREEN 13 //GPIO13
|
||||||
|
#endif
|
||||||
|
|
||||||
#define GPIO_INPUT_PIN_SEL (1ULL<<GPIO_BOOT_BTN)
|
#define GPIO_INPUT_PIN_SEL (1ULL<<GPIO_BOOT_BTN)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ static const char *LOG_TAG = "esp_main";
|
||||||
void app_main(void)
|
void app_main(void)
|
||||||
{
|
{
|
||||||
esp_err_t err = ESP_OK;
|
esp_err_t err = ESP_OK;
|
||||||
ESP_LOGI(LOG_TAG, "hardcoded version: 0.0.1");
|
|
||||||
|
|
||||||
ESP_LOGI(LOG_TAG, "start mesh network");
|
ESP_LOGI(LOG_TAG, "start mesh network");
|
||||||
err = errMeshNetworkInitialize();
|
err = errMeshNetworkInitialize();
|
||||||
|
|
|
@ -32,7 +32,7 @@ CONFIG_APP_COMPILE_TIME_DATE=y
|
||||||
# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set
|
# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set
|
||||||
# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set
|
# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set
|
||||||
CONFIG_APP_PROJECT_VER_FROM_CONFIG=y
|
CONFIG_APP_PROJECT_VER_FROM_CONFIG=y
|
||||||
CONFIG_APP_PROJECT_VER="0.0.1"
|
CONFIG_APP_PROJECT_VER="0.0.26"
|
||||||
CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16
|
CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16
|
||||||
# end of Application manager
|
# end of Application manager
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue