2021-01-21 11:11:37 +01:00
/**
* @ file Mesh_OTA . c
* @ brief Start and implement OTA updates via HTTPS from server and other mesh nodes ( bidirectional )
* @ author Hendrik Schutter
* @ date 21.01 .2021
*/
2021-01-11 22:56:39 +01:00
# include "Mesh_OTA.h"
2021-01-20 20:55:33 +01:00
# include "Mesh_OTA_Util.h"
# include "Mesh_OTA_Globals.h"
2021-01-20 21:40:51 +01:00
# include "Mesh_OTA_Partition_Access.h"
2021-01-02 00:30:13 +01:00
2021-01-17 23:27:01 +01:00
static const char * LOG_TAG = " mesh_ota " ;
2021-01-21 11:11:37 +01:00
/**
* @ fn esp_err_t errMeshOTAInitialize ( void )
* @ brief Starts Mesh OTA functionality
* @ param void
* @ return ESP32 error code
* @ author Hendrik Schutter
* @ date 21.01 .2021
*
* Initialize queues and tasks
* Set callbacks
*/
esp_err_t errMeshOTAInitialize ( void )
2021-01-17 23:27:01 +01:00
{
esp_err_t err = ESP_OK ;
BaseType_t xReturned ;
2021-01-18 19:03:32 +01:00
bWantReboot = false ;
2021-01-17 23:27:01 +01:00
//create queue to store nodes for ota worker task
queueNodes = xQueueCreate ( QUEUE_NODES_SIZE , sizeof ( mesh_addr_t ) ) ;
if ( queueNodes = = 0 ) // Queue not created
{
2021-01-21 11:11:37 +01:00
ESP_LOGE ( LOG_TAG , " Unable to create queue for nodes " ) ;
2021-01-17 23:27:01 +01:00
err = ESP_FAIL ;
}
if ( err = = ESP_OK )
{
//create queue to store ota messages
queueMessageOTA = xQueueCreate ( QUEUE_MESSAGE_OTA_SIZE , sizeof ( MESH_PACKET_t ) ) ;
if ( queueMessageOTA = = 0 ) // Queue not created
{
2021-01-21 11:11:37 +01:00
ESP_LOGE ( LOG_TAG , " Unable to create queue for OTA messages " ) ;
2021-01-17 23:27:01 +01:00
err = ESP_FAIL ;
}
}
if ( err = = ESP_OK )
{
bsStartStopServerWorker = xSemaphoreCreateBinary ( ) ;
if ( bsStartStopServerWorker = = NULL )
{
2021-01-21 11:11:37 +01:00
ESP_LOGE ( LOG_TAG , " Unable to create mutex to represent state of server worker " ) ;
2021-01-17 23:27:01 +01:00
err = ESP_FAIL ;
}
}
2021-01-18 19:03:32 +01:00
if ( err = = ESP_OK )
{
bsOTAProcess = xSemaphoreCreateBinary ( ) ;
if ( bsOTAProcess = = NULL )
{
2021-01-21 11:11:37 +01:00
ESP_LOGE ( LOG_TAG , " Unable to create mutex to grant access to OTA process " ) ;
2021-01-18 19:03:32 +01:00
err = ESP_FAIL ;
}
}
if ( err = = ESP_OK )
{
xSemaphoreGive ( bsOTAProcess ) ; //unlock binary semaphore
if ( bsOTAProcess = = NULL )
{
2021-01-21 11:11:37 +01:00
ESP_LOGE ( LOG_TAG , " Unable to unlock mutex to grant access to OTA process " ) ;
2021-01-18 19:03:32 +01:00
err = ESP_FAIL ;
}
}
2021-01-21 11:11:37 +01:00
//register callbacks in network
2021-01-20 21:40:51 +01:00
ERROR_CHECK ( errMeshNetworkSetChildConnectedHandle ( vMeshOtaUtilAddNodeToPossibleUpdatableQueue ) ) ;
ERROR_CHECK ( errMeshNetworkSetOTAMessageHandleHandle ( vMeshOtaUtilAddOtaMessageToQueue ) ) ;
ERROR_CHECK ( errMeshNetworkSetChangeStateOfServerWorkerHandle ( vMeshOtaUtilChangeStateOfServerWorker ) ) ;
2021-01-17 23:27:01 +01:00
2021-01-18 17:38:08 +01:00
if ( err = = ESP_OK )
{
pOTAPartition = esp_ota_get_next_update_partition ( NULL ) ; //get ota partition
if ( pOTAPartition = = NULL )
{
err = ESP_FAIL ;
ESP_LOGE ( LOG_TAG , " unable to get next ota partition " ) ;
}
}
2021-01-17 23:27:01 +01:00
if ( err = = ESP_OK )
{
2021-01-20 21:40:51 +01:00
xReturned = xTaskCreate ( vMeshOtaTaskServerWorker , " vMeshOtaTaskServerWorker " , 8192 , NULL , 5 , NULL ) ;
2021-01-17 23:27:01 +01:00
if ( xReturned ! = pdPASS )
{
ESP_LOGE ( LOG_TAG , " Unable to create the server worker task " ) ;
err = ESP_FAIL ;
}
}
2021-01-18 22:56:42 +01:00
if ( err = = ESP_OK )
{
2021-01-20 21:40:51 +01:00
xReturned = xTaskCreate ( vMeshOtaTaskOTAWorker , " vMeshOtaTaskOTAWorker " , 8192 , NULL , 5 , NULL ) ;
2021-01-18 22:56:42 +01:00
if ( xReturned ! = pdPASS )
{
ESP_LOGE ( LOG_TAG , " Unable to create the OTA worker task " ) ;
err = ESP_FAIL ;
}
}
2021-01-17 23:27:01 +01:00
return err ;
}
2021-01-21 11:11:37 +01:00
/**
* @ fn void vMeshOtaTaskServerWorker ( void * arg )
* @ brief Task for updating from server via HTTPS
* @ param arg
* @ return void
* @ author Hendrik Schutter
* @ date 21.01 .2021
*/
2021-01-20 21:40:51 +01:00
void vMeshOtaTaskServerWorker ( void * arg )
2021-01-17 23:27:01 +01:00
{
2021-01-21 11:11:37 +01:00
esp_err_t err = ESP_OK ;
2021-01-18 19:03:32 +01:00
bool bNewOTAImage ; //true if a new ota image was downloaded and validated
2021-01-18 17:38:08 +01:00
bool bFirstRun = true ;
2021-01-17 23:47:59 +01:00
2021-01-18 12:49:52 +01:00
while ( true )
{
err = ESP_OK ;
2021-01-18 19:03:32 +01:00
bNewOTAImage = false ;
2021-01-18 12:49:52 +01:00
xSemaphoreTake ( bsStartStopServerWorker , portMAX_DELAY ) ; //wait for binary semaphore that allows to start the worker
xSemaphoreGive ( bsStartStopServerWorker ) ; //free binary semaphore, this allows the handler to change is to taken
if ( esp_mesh_is_root ( ) ) //check again that this node is the root node
{
2021-01-18 19:03:32 +01:00
ESP_LOGI ( LOG_TAG , " Checking firmware image on server " ) ;
2021-01-18 17:38:08 +01:00
if ( bFirstRun = = true )
{
2021-01-21 11:11:37 +01:00
//init on first run
2021-01-18 19:03:32 +01:00
ERROR_CHECK ( errHTTPSClientInitialize ( ) ) ;
2021-01-18 17:38:08 +01:00
bFirstRun = false ;
}
2021-01-18 12:49:52 +01:00
2021-01-18 19:03:32 +01:00
ERROR_CHECK ( errHTTPSClientConnectToServer ( ) ) ;
ERROR_CHECK ( errHTTPSClientValidateServer ( ) ) ;
ERROR_CHECK ( errHTTPSClientSendRequest ( ) ) ;
2021-01-18 12:49:52 +01:00
2021-01-20 21:40:51 +01:00
ERROR_CHECK ( errMeshOtaPartitionAccessHttps ( & bNewOTAImage ) ) ;
2021-01-18 19:03:32 +01:00
errHTTPSClientReset ( ) ;
2021-01-18 12:49:52 +01:00
if ( bNewOTAImage = = true )
{
//set want reboot
2021-01-18 19:03:32 +01:00
ESP_LOGI ( LOG_TAG , " Updated successfully via HTTPS, set pending reboot " ) ;
bWantReboot = true ;
2021-01-20 21:40:51 +01:00
vMeshOtaUtilAddAllNeighboursToQueue ( ) ; //add all existing neighbours to queue (aparent will not be added because this node is the root)
2021-01-18 12:49:52 +01:00
}
2021-01-18 22:56:42 +01:00
vTaskDelay ( ( SERVER_CHECK_INTERVAL * 1000 ) / portTICK_PERIOD_MS ) ; //sleep till next server checks
2021-01-18 12:49:52 +01:00
}
}
}
2021-01-21 11:11:37 +01:00
/**
* @ fn void vMeshOtaTaskServerWorker ( void * arg )
* @ brief Task for updating from nodes in mesh network
* @ param arg
* @ return void
* @ author Hendrik Schutter
* @ date 21.01 .2021
*/
2021-01-20 21:40:51 +01:00
void vMeshOtaTaskOTAWorker ( void * arg )
2021-01-19 12:36:21 +01:00
{
esp_err_t err = ESP_OK ;
bool bNewOTAImage ; //true if a new ota image was downloaded and validated
mesh_addr_t meshNodeAddr ; //node that should be checked for ota update
while ( true )
{
err = ESP_OK ;
bNewOTAImage = false ;
if ( ( uxQueueSpacesAvailable ( queueNodes ) - QUEUE_NODES_SIZE ) = = 0 )
{
//nodes queue is empty
2021-01-20 23:04:18 +01:00
if ( ( bWantReboot = = true ) & & ( OTA_ALLOW_REBOOT = = 1 ) )
2021-01-19 12:36:21 +01:00
{
2021-01-20 23:04:18 +01:00
ESP_LOGE ( LOG_TAG , " ESP32 Reboot ... " ) ;
vTaskDelay ( ( 1000 ) / portTICK_PERIOD_MS ) ;
esp_restart ( ) ;
2021-01-19 12:36:21 +01:00
}
2021-01-20 21:40:51 +01:00
ERROR_CHECK ( errMeshOtaSlaveEndpoint ( & bNewOTAImage ) ) ;
2021-01-19 12:36:21 +01:00
}
else
{
//queue not empty
if ( xQueueReceive ( queueNodes , & meshNodeAddr , ( ( 100 ) / portTICK_PERIOD_MS ) ) ! = pdTRUE )
{
ESP_LOGE ( LOG_TAG , " Unable to receive OTA Messages from Queue " ) ;
err = ESP_FAIL ;
}
2021-01-20 21:40:51 +01:00
ERROR_CHECK ( errMeshOtaMasterEndpoint ( & bNewOTAImage , & meshNodeAddr ) ) ;
2021-01-19 22:19:30 +01:00
if ( err ! = ESP_OK )
{
//OTA process faild --> add back to queue
2021-01-20 21:40:51 +01:00
vMeshOtaUtilAddNodeToPossibleUpdatableQueue ( meshNodeAddr . addr ) ;
2021-01-19 22:19:30 +01:00
}
2021-01-20 23:04:18 +01:00
else
{
vMeshOtaUtilClearNeighboursQueue ( & meshNodeAddr ) ; //remove this node from queue
}
2021-01-19 12:36:21 +01:00
}
if ( bNewOTAImage = = true )
{
//set want reboot
ESP_LOGI ( LOG_TAG , " Updated successfully via Mesh, set pending reboot " ) ;
bWantReboot = true ;
2021-01-20 21:40:51 +01:00
vMeshOtaUtilAddAllNeighboursToQueue ( ) ; //add all existing neighbours to queue
2021-01-19 12:36:21 +01:00
}
vTaskDelay ( ( 1000 ) / portTICK_PERIOD_MS ) ;
}
}
2021-01-21 11:11:37 +01:00
/**
* @ fn esp_err_t errMeshOtaSlaveEndpoint ( bool * const cpbNewOTAImage )
* @ brief Endpoint for OTA process that is called from remote node
2021-01-21 11:43:09 +01:00
* @ param cpbNewOTAImage pointer to boolean to signal if a new image was successfully received
2021-01-21 11:11:37 +01:00
* @ return ESP32 error code
* @ author Hendrik Schutter
* @ date 21.01 .2021
*
* Answers the OTA_Version_Request with OTA_Version_Response
* calls errMeshOtaPartitionAccessMeshReceive OR errMeshOtaPartitionAccessMeshTransmit based on version number
*/
2021-01-20 22:39:18 +01:00
esp_err_t errMeshOtaSlaveEndpoint ( bool * const cpbNewOTAImage )
2021-01-19 12:36:21 +01:00
{
esp_err_t err = ESP_OK ;
MESH_PACKET_t sOTAMessage ;
2021-01-21 11:11:37 +01:00
const esp_partition_t * cpBootPartition = NULL ; //pointer to boot partition (that will booted after reset)
2021-01-19 12:36:21 +01:00
esp_app_desc_t bootPartitionDesc ; //Metadate from boot partition
2021-01-20 22:39:18 +01:00
* cpbNewOTAImage = false ; //set default false
2021-01-19 12:36:21 +01:00
//read OTAMessages queue
if ( uxQueueSpacesAvailable ( queueMessageOTA ) < QUEUE_MESSAGE_OTA_SIZE )
{
//queue not empty
if ( xQueueReceive ( queueMessageOTA , & sOTAMessage , ( ( 100 ) / portTICK_PERIOD_MS ) ) ! = pdTRUE )
{
ESP_LOGE ( LOG_TAG , " Unable to receive OTA Messages from Queue " ) ;
err = ESP_FAIL ;
}
if ( ( err = = ESP_OK ) & & ( sOTAMessage . type = = OTA_Version_Request ) ) //if OTA_Version_Request
{
xSemaphoreTake ( bsOTAProcess , portMAX_DELAY ) ; //wait for binary semaphore that allows to start the OTA process
2021-01-21 11:11:37 +01:00
cpBootPartition = esp_ota_get_boot_partition ( ) ; //get boot partition (that will booted after reset), not the running partition
ERROR_CHECK ( esp_ota_get_partition_description ( cpBootPartition , & bootPartitionDesc ) ) ; //get metadata of partition
2021-01-19 12:36:21 +01:00
2021-01-19 17:20:16 +01:00
//send OTA_Version_Response to sender of OTA_Version_Request packet wirh version in payload
2021-01-21 11:43:09 +01:00
ERROR_CHECK ( errMeshOtaUtilSendOtaVersionResponse ( & sOTAMessage . meshSenderAddr ) ) ;
2021-01-19 12:36:21 +01:00
2021-01-20 21:40:51 +01:00
if ( ( bMeshOtaUtilNewerVersion ( ( bootPartitionDesc ) . version , ( char * ) sOTAMessage . au8Payload ) ) & & ( err = = ESP_OK ) ) //compare local and remote version
2021-01-19 12:36:21 +01:00
{
//remote newer as local
2021-01-26 17:37:12 +01:00
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 ] ) ;
2021-01-20 22:39:18 +01:00
// --> this version older --> start OTA_Rx --> set cpbNewOTAImage true
ERROR_CHECK ( errMeshOtaPartitionAccessMeshReceive ( cpbNewOTAImage , & sOTAMessage . meshSenderAddr ) ) ;
2021-01-19 12:36:21 +01:00
}
2021-01-20 21:40:51 +01:00
if ( ( bMeshOtaUtilNewerVersion ( ( char * ) sOTAMessage . au8Payload , ( bootPartitionDesc ) . version ) ) & & ( err = = ESP_OK ) ) //compare remote and local version
2021-01-19 12:36:21 +01:00
{
//local newer as remote
2021-01-26 17:37:12 +01:00
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 ] ) ;
2021-01-19 12:36:21 +01:00
// --> this version newer --> start OTA_Tx
2021-01-20 21:40:51 +01:00
ERROR_CHECK ( errMeshOtaPartitionAccessMeshTransmit ( & sOTAMessage . meshSenderAddr ) ) ;
2021-01-19 12:36:21 +01:00
}
xSemaphoreGive ( bsOTAProcess ) ; //free binary semaphore, this allows other tasks to start the OTA process
}
}
return err ;
}
2021-01-21 11:11:37 +01:00
/**
* @ fn esp_err_t errMeshOtaMasterEndpoint ( bool * const cpbNewOTAImage , const mesh_addr_t * const cpcMeshNodeAddr )
* @ brief Endpoint for OTA process that calls remote node
2021-01-21 11:43:09 +01:00
* @ param cpbNewOTAImage pointer to boolean to signal if a new image was successfully received
2021-01-21 11:11:37 +01:00
* @ param cpcMeshNodeAddr pointer to remote node addr
* @ return ESP32 error code
* @ author Hendrik Schutter
* @ date 21.01 .2021
*
* Sends the OTA_Version_Request to remote node
* calls errMeshOtaPartitionAccessMeshReceive OR errMeshOtaPartitionAccessMeshTransmit based on version number received
*/
2021-01-20 22:39:18 +01:00
esp_err_t errMeshOtaMasterEndpoint ( bool * const cpbNewOTAImage , const mesh_addr_t * const cpcMeshNodeAddr )
2021-01-19 12:36:21 +01:00
{
esp_err_t err = ESP_OK ;
MESH_PACKET_t sOTAMessage ;
2021-01-21 11:11:37 +01:00
const esp_partition_t * cpBootPartition = NULL ; //pointer to boot partition (that will booted after reset)
2021-01-19 17:20:16 +01:00
esp_app_desc_t bootPartitionDesc ; //Metadata from boot partition
bool bNodeIsConnected = false ;
bool bNodeIsResponding = false ;
2021-01-20 22:39:18 +01:00
* cpbNewOTAImage = false ; //set default false
2021-01-19 12:36:21 +01:00
2021-01-20 22:39:18 +01:00
if ( bMeshNetworkIsNodeNeighbour ( cpcMeshNodeAddr ) = = true ) //check if node is still connected
2021-01-19 17:20:16 +01:00
{
bNodeIsConnected = true ; //node is one of the neighbours
xSemaphoreTake ( bsOTAProcess , portMAX_DELAY ) ; //wait for binary semaphore that allows to start the OTA process
2021-01-21 11:43:09 +01:00
ERROR_CHECK ( errMeshOtaUtilSendOtaVersionRequest ( cpcMeshNodeAddr ) ) ; //send OTA_VERSION_REQUEST with local version in payload
2021-01-19 12:36:21 +01:00
2021-01-19 17:20:16 +01:00
for ( uint32_t u32Index = 0 ; u32Index < QUEUE_MESSAGE_OTA_SIZE ; u32Index + + ) //loop through all OTA messages
{
if ( uxQueueSpacesAvailable ( queueMessageOTA ) < QUEUE_MESSAGE_OTA_SIZE )
{
//queue not empty
if ( xQueueReceive ( queueMessageOTA , & sOTAMessage , ( ( 3000 ) / portTICK_PERIOD_MS ) ) ! = pdTRUE )
{
ESP_LOGE ( LOG_TAG , " Unable to receive OTA Messages from queue " ) ;
err = ESP_FAIL ;
}
2021-01-21 11:43:09 +01:00
if ( ( err = = ESP_OK ) & & ( sOTAMessage . type = = OTA_Version_Response ) & & ( bMeshNetworkCheckMacEquality ( sOTAMessage . meshSenderAddr . addr , cpcMeshNodeAddr - > addr ) ) ) //if OTA_Version_Request
2021-01-19 17:20:16 +01:00
{
bNodeIsResponding = true ;
2021-01-21 11:11:37 +01:00
cpBootPartition = esp_ota_get_boot_partition ( ) ; //get boot partition (that will booted after reset), not the running partition
ERROR_CHECK ( esp_ota_get_partition_description ( cpBootPartition , & bootPartitionDesc ) ) ; //get metadata of partition
2021-01-19 17:20:16 +01:00
2021-01-20 21:40:51 +01:00
if ( ( bMeshOtaUtilNewerVersion ( ( bootPartitionDesc ) . version , ( char * ) sOTAMessage . au8Payload ) ) & & ( err = = ESP_OK ) ) //compare local and remote version
2021-01-19 17:20:16 +01:00
{
//remote newer as local
2021-01-26 17:37:12 +01:00
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 ] ) ;
2021-01-20 22:39:18 +01:00
// --> this version older --> start OTA_Rx --> set cpbNewOTAImage true
ERROR_CHECK ( errMeshOtaPartitionAccessMeshReceive ( cpbNewOTAImage , & sOTAMessage . meshSenderAddr ) ) ;
2021-01-19 17:20:16 +01:00
}
2021-01-20 21:40:51 +01:00
if ( ( bMeshOtaUtilNewerVersion ( ( char * ) sOTAMessage . au8Payload , ( bootPartitionDesc ) . version ) ) & & ( err = = ESP_OK ) ) //compare remote and local version
2021-01-19 17:20:16 +01:00
{
//local newer as remote
2021-01-26 17:37:12 +01:00
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 ] ) ;
2021-01-19 17:20:16 +01:00
// --> this version newer --> start OTA_Tx
2021-01-20 21:40:51 +01:00
ERROR_CHECK ( errMeshOtaPartitionAccessMeshTransmit ( & sOTAMessage . meshSenderAddr ) ) ;
2021-01-19 17:20:16 +01:00
}
}
2021-01-19 22:19:30 +01:00
else if ( err = = ESP_OK )
2021-01-19 17:20:16 +01:00
{
2021-01-19 22:19:30 +01:00
//received from wrong node or type --> back to queue
2021-01-20 21:40:51 +01:00
vMeshOtaUtilAddOtaMessageToQueue ( & sOTAMessage ) ;
2021-01-19 17:20:16 +01:00
}
}
2021-01-19 22:19:30 +01:00
else
{
// OTA Message queue is empty --> wait some time
2021-01-21 11:11:37 +01:00
ESP_LOGD ( LOG_TAG , " OTA-Master: OTA Message queue is empty --> wait some time " ) ;
2021-01-19 22:19:30 +01:00
vTaskDelay ( ( 1000 / QUEUE_MESSAGE_OTA_SIZE ) / portTICK_PERIOD_MS ) ;
}
2021-01-19 17:20:16 +01:00
} //end loop
xSemaphoreGive ( bsOTAProcess ) ; //free binary semaphore, this allows other tasks to start the OTA process
}
if ( ( bNodeIsResponding = = false ) & & ( bNodeIsConnected = = true ) )
{
//add node back to queue if connected and NOT responding
2021-01-21 11:11:37 +01:00
ESP_LOGD ( LOG_TAG , " OTA-Master: connected and NOT responding --> add node back to queue " ) ;
2021-01-20 22:39:18 +01:00
vMeshOtaUtilAddNodeToPossibleUpdatableQueue ( cpcMeshNodeAddr - > addr ) ;
2021-01-19 17:20:16 +01:00
}
2021-01-19 12:36:21 +01:00
return err ;
}