ESP32 OTA firmware updates via WiFi mesh network.
https://hendrikschutter.com
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
386 lines
17 KiB
386 lines
17 KiB
/** |
|
* @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 |
|
*/ |
|
|
|
#include "Mesh_OTA.h" |
|
#include "Mesh_OTA_Util.h" |
|
#include "Mesh_OTA_Globals.h" |
|
#include "Mesh_OTA_Partition_Access.h" |
|
|
|
static const char *LOG_TAG = "mesh_ota"; |
|
|
|
/** |
|
* @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) |
|
{ |
|
esp_err_t err = ESP_OK; |
|
BaseType_t xReturned; |
|
bWantReboot = false; |
|
|
|
//create queue to store nodes for ota worker task |
|
queueNodes = xQueueCreate(QUEUE_NODES_SIZE, sizeof(mesh_addr_t)); |
|
if (queueNodes == 0) // Queue not created |
|
{ |
|
ESP_LOGE(LOG_TAG, "Unable to create queue for nodes"); |
|
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 |
|
{ |
|
ESP_LOGE(LOG_TAG, "Unable to create queue for OTA messages"); |
|
err = ESP_FAIL; |
|
} |
|
} |
|
|
|
if(err == ESP_OK) |
|
{ |
|
bsStartStopServerWorker = xSemaphoreCreateBinary(); |
|
if( bsStartStopServerWorker == NULL ) |
|
{ |
|
ESP_LOGE(LOG_TAG, "Unable to create mutex to represent state of server worker"); |
|
err = ESP_FAIL; |
|
} |
|
} |
|
|
|
if(err == ESP_OK) |
|
{ |
|
bsOTAProcess = xSemaphoreCreateBinary(); |
|
if( bsOTAProcess == NULL ) |
|
{ |
|
ESP_LOGE(LOG_TAG, "Unable to create mutex to grant access to OTA process"); |
|
err = ESP_FAIL; |
|
} |
|
} |
|
|
|
if(err == ESP_OK) |
|
{ |
|
xSemaphoreGive(bsOTAProcess); //unlock binary semaphore |
|
if( bsOTAProcess == NULL ) |
|
{ |
|
ESP_LOGE(LOG_TAG, "Unable to unlock mutex to grant access to OTA process"); |
|
err = ESP_FAIL; |
|
} |
|
} |
|
|
|
//register callbacks in network |
|
ERROR_CHECK(errMeshNetworkSetChildConnectedHandle(vMeshOtaUtilAddNodeToPossibleUpdatableQueue)); |
|
ERROR_CHECK(errMeshNetworkSetOTAMessageHandleHandle(vMeshOtaUtilAddOtaMessageToQueue)); |
|
ERROR_CHECK(errMeshNetworkSetChangeStateOfServerWorkerHandle(vMeshOtaUtilChangeStateOfServerWorker)); |
|
|
|
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"); |
|
} |
|
} |
|
|
|
if(err == ESP_OK) |
|
{ |
|
xReturned = xTaskCreate(vMeshOtaTaskServerWorker, "vMeshOtaTaskServerWorker", 8192, NULL, 5, NULL); |
|
if(xReturned != pdPASS) |
|
{ |
|
ESP_LOGE(LOG_TAG, "Unable to create the server worker task"); |
|
err = ESP_FAIL; |
|
} |
|
} |
|
|
|
|
|
if(err == ESP_OK) |
|
{ |
|
xReturned = xTaskCreate(vMeshOtaTaskOTAWorker, "vMeshOtaTaskOTAWorker", 8192, NULL, 5, NULL); |
|
if(xReturned != pdPASS) |
|
{ |
|
ESP_LOGE(LOG_TAG, "Unable to create the OTA worker task"); |
|
err = ESP_FAIL; |
|
} |
|
} |
|
return err; |
|
} |
|
|
|
/** |
|
* @fn void vMeshOtaTaskServerWorker(void *arg) |
|
* @brief Task for updating from server via HTTPS |
|
* @param arg |
|
* @return void |
|
* @author Hendrik Schutter |
|
* @date 21.01.2021 |
|
*/ |
|
void vMeshOtaTaskServerWorker(void *arg) |
|
{ |
|
esp_err_t err = ESP_OK; |
|
bool bNewOTAImage; //true if a new ota image was downloaded and validated |
|
bool bFirstRun = true; |
|
|
|
while(true) |
|
{ |
|
err = ESP_OK; |
|
bNewOTAImage = false; |
|
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 |
|
{ |
|
ESP_LOGI(LOG_TAG, "Checking firmware image on server"); |
|
|
|
if(bFirstRun == true) |
|
{ |
|
//init on first run |
|
ERROR_CHECK(errHTTPSClientInitialize()); |
|
bFirstRun = false; |
|
} |
|
|
|
ERROR_CHECK(errHTTPSClientConnectToServer()); |
|
ERROR_CHECK(errHTTPSClientValidateServer()); |
|
ERROR_CHECK(errHTTPSClientSendRequest()); |
|
|
|
ERROR_CHECK(errMeshOtaPartitionAccessHttps(&bNewOTAImage)); |
|
errHTTPSClientReset(); |
|
|
|
if(bNewOTAImage == true) |
|
{ |
|
//set want reboot |
|
ESP_LOGI(LOG_TAG, "Updated successfully via HTTPS, set pending reboot"); |
|
bWantReboot = true; |
|
} |
|
vTaskDelay( (SERVER_CHECK_INTERVAL*1000) / portTICK_PERIOD_MS); //sleep till next server checks |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* @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 |
|
*/ |
|
void vMeshOtaTaskOTAWorker(void *arg) |
|
{ |
|
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 |
|
BaseType_t xReturned; |
|
|
|
while(true) |
|
{ |
|
err = ESP_OK; |
|
bNewOTAImage = false; |
|
|
|
if((uxQueueSpacesAvailable(queueNodes) - QUEUE_NODES_SIZE) == 0) |
|
{ |
|
//nodes queue is empty |
|
|
|
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 ..."); |
|
vTaskDelay( (1000) / portTICK_PERIOD_MS); |
|
esp_restart(); |
|
} |
|
|
|
xSemaphoreGive(bsOTAProcess); //free binary semaphore, this allows other tasks to start the OTA process |
|
ERROR_CHECK(errMeshOtaSlaveEndpoint(&bNewOTAImage, &meshNodeAddr)); |
|
} |
|
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; |
|
} |
|
|
|
ERROR_CHECK(errMeshOtaMasterEndpoint(&bNewOTAImage, &meshNodeAddr)); |
|
|
|
if (err != ESP_OK) |
|
{ |
|
//OTA process faild --> add back to queue |
|
vMeshOtaUtilAddNodeToPossibleUpdatableQueue(meshNodeAddr.addr); |
|
} |
|
else |
|
{ |
|
vMeshOtaUtilClearNeighboursQueue(&meshNodeAddr); //remove this node from queue |
|
} |
|
} |
|
|
|
if(bNewOTAImage == true) |
|
{ |
|
//set want reboot |
|
ESP_LOGI(LOG_TAG, "Updated successfully via Mesh, set pending reboot"); |
|
bWantReboot = true; |
|
|
|
vMeshOtaUtilAddAllNeighboursToQueue(&meshNodeAddr); //add all existing neighbours to queue |
|
} |
|
vTaskDelay( (1000) / portTICK_PERIOD_MS); |
|
} |
|
} |
|
|
|
/** |
|
* @fn esp_err_t errMeshOtaSlaveEndpoint(bool* const cpbNewOTAImage) |
|
* @brief Endpoint for OTA process that is called from remote node |
|
* @param cpbNewOTAImage pointer to boolean to signal if a new image was successfully received |
|
* @param cpcMeshNodeAddr pointer to mesh node that transmitted the update |
|
* @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 |
|
*/ |
|
esp_err_t errMeshOtaSlaveEndpoint(bool* const cpbNewOTAImage, mesh_addr_t* const cpcMeshNodeAddr) |
|
{ |
|
esp_err_t err = ESP_OK; |
|
MESH_PACKET_t sOTAMessage; |
|
const esp_partition_t* cpBootPartition = NULL; //pointer to boot partition (that will booted after reset) |
|
esp_app_desc_t bootPartitionDesc; //Metadate from boot partition |
|
*cpbNewOTAImage = false; //set default false |
|
|
|
//read OTAMessages queue |
|
if(uxQueueSpacesAvailable(queueMessageOTA) < QUEUE_MESSAGE_OTA_SIZE) |
|
{ |
|
//queue not empty |
|
if (xQueueReceive(queueMessageOTA, &sOTAMessage, ((100) / portTICK_PERIOD_MS)) != pdTRUE) |
|
{ |
|
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 |
|
|
|
memcpy(cpcMeshNodeAddr, &sOTAMessage.meshSenderAddr, sizeof(mesh_addr_t)); |
|
|
|
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 |
|
|
|
//send OTA_Version_Response to sender of OTA_Version_Request packet wirh version in payload |
|
ERROR_CHECK(errMeshOtaUtilSendOtaVersionResponse(&sOTAMessage.meshSenderAddr)); |
|
|
|
if((bMeshOtaUtilNewerVersion((bootPartitionDesc).version, (char*) sOTAMessage.au8Payload)) && (err == ESP_OK)) //compare local and remote version |
|
{ |
|
//remote newer as local |
|
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 |
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshReceive(cpbNewOTAImage, &sOTAMessage.meshSenderAddr)); |
|
} |
|
|
|
if((bMeshOtaUtilNewerVersion((char*) sOTAMessage.au8Payload, (bootPartitionDesc).version)) && (err == ESP_OK)) //compare remote and local version |
|
{ |
|
//local newer as remote |
|
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 |
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshTransmit(&sOTAMessage.meshSenderAddr)); |
|
} |
|
xSemaphoreGive(bsOTAProcess); //free binary semaphore, this allows other tasks to start the OTA process |
|
} |
|
} |
|
return err; |
|
} |
|
|
|
/** |
|
* @fn esp_err_t errMeshOtaMasterEndpoint(bool* const cpbNewOTAImage, const mesh_addr_t* const cpcMeshNodeAddr) |
|
* @brief Endpoint for OTA process that calls remote node |
|
* @param cpbNewOTAImage pointer to boolean to signal if a new image was successfully received |
|
* @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 |
|
*/ |
|
esp_err_t errMeshOtaMasterEndpoint(bool* const cpbNewOTAImage, const mesh_addr_t* const cpcMeshNodeAddr) |
|
{ |
|
esp_err_t err = ESP_OK; |
|
MESH_PACKET_t sOTAMessage; |
|
const esp_partition_t* cpBootPartition = NULL; //pointer to boot partition (that will booted after reset) |
|
esp_app_desc_t bootPartitionDesc; //Metadata from boot partition |
|
bool bNodeIsConnected = false; |
|
bool bNodeIsResponding = false; |
|
bool bSameVersion = false; |
|
|
|
*cpbNewOTAImage = false; //set default false |
|
|
|
if(bMeshNetworkIsNodeNeighbour(cpcMeshNodeAddr) == true) //check if node is still connected |
|
{ |
|
bNodeIsConnected = true; //node is one of the neighbours |
|
xSemaphoreTake(bsOTAProcess, portMAX_DELAY); //wait for binary semaphore that allows to start the OTA process |
|
|
|
ERROR_CHECK(errMeshOtaUtilSendOtaVersionRequest(cpcMeshNodeAddr)); //send OTA_VERSION_REQUEST with local version in payload |
|
|
|
for (uint32_t u32Index = 0; ((u32Index < QUEUE_MESSAGE_OTA_SIZE) && (bSameVersion == false)); 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; |
|
} |
|
|
|
if((err == ESP_OK) && (sOTAMessage.type == OTA_Version_Response) && (bMeshNetworkCheckMacEquality(sOTAMessage.meshSenderAddr.addr, cpcMeshNodeAddr->addr))) //if OTA_Version_Request |
|
{ |
|
bSameVersion = true; |
|
bNodeIsResponding = true; |
|
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 |
|
|
|
if((bMeshOtaUtilNewerVersion((bootPartitionDesc).version, (char*) sOTAMessage.au8Payload)) && (err == ESP_OK)) //compare local and remote version |
|
{ |
|
//remote newer as local |
|
bSameVersion = false; |
|
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 |
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshReceive(cpbNewOTAImage, &sOTAMessage.meshSenderAddr)); |
|
u32Index = QUEUE_MESSAGE_OTA_SIZE; |
|
} |
|
|
|
if((bMeshOtaUtilNewerVersion((char*) sOTAMessage.au8Payload, (bootPartitionDesc).version)) && (err == ESP_OK)) //compare remote and local version |
|
{ |
|
//local newer as remote |
|
bSameVersion = false; |
|
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 |
|
ERROR_CHECK(errMeshOtaPartitionAccessMeshTransmit(&sOTAMessage.meshSenderAddr)); |
|
u32Index = QUEUE_MESSAGE_OTA_SIZE; |
|
} |
|
} |
|
else |
|
{ |
|
// OTA Message queue is empty --> wait some time |
|
vTaskDelay( (1000/QUEUE_MESSAGE_OTA_SIZE) / portTICK_PERIOD_MS); |
|
} |
|
}//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 |
|
ESP_LOGI(LOG_TAG, "OTA-Master: connected and NOT responding --> add node back to queue "); |
|
vMeshOtaUtilAddNodeToPossibleUpdatableQueue(cpcMeshNodeAddr->addr); |
|
} |
|
return err; |
|
}
|
|
|