diff --git a/applications/helpers/ota_orchestrator/src/ota_orchestrator.c b/applications/helpers/ota_orchestrator/src/ota_orchestrator.c index 51364f7f..0b45552a 100644 --- a/applications/helpers/ota_orchestrator/src/ota_orchestrator.c +++ b/applications/helpers/ota_orchestrator/src/ota_orchestrator.c @@ -71,7 +71,7 @@ #define NUM_OF_BLOCKS_REQUESTED 1U #define START_JOB_MSG_LENGTH 147U #define MAX_JOB_ID_LENGTH 64U -#define UPDATE_JOB_MSG_LENGTH 48U +#define UPDATE_JOB_MSG_LENGTH 128U extern void vOtaNotActiveHook( void ); extern void vOtaActiveHook( void ); @@ -178,6 +178,8 @@ char globalJobId[ MAX_JOB_ID_LENGTH ] = { 0 }; */ static AfrOtaJobDocumentFields_t jobFields = { 0 }; +static AfrOtaJobDocumentStatusDetails_t jobStatusDetails = { 0 }; + /** * @brief Structure used to hold data from a job document. */ @@ -252,9 +254,26 @@ STATIC bool closeFile( void ); STATIC bool activateImage( void ); /** - * @brief Send a message to notify that the firmware image was accepted. + * @brief Send a message to the cloud to update the statusDetails field of the + * job. + */ +STATIC void sendStatusDetailsMessage( void ); + +/** + * @brief Send a message to notify the cloud of the job's final status (i.e. + * accepted or failed). */ -STATIC bool sendSuccessMessage( void ); +STATIC void sendFinalJobStatusMessage( JobCurrentStatus_t status ); + +/** + * @brief Print the OTA job document metadata. + * + * @param[in] jobId String containing the job ID. + * @param[in] jobFields Pointer to the parameters extracted from the OTA job + * document. + */ +STATIC void printJobParams( const char * jobId, + AfrOtaJobDocumentFields_t jobFields ); /** * @brief Send the necessary message to request a job document. @@ -439,7 +458,7 @@ STATIC bool activateImage( void ) return otaPal_ActivateNewImage( &jobFields ); } -STATIC bool sendSuccessMessage( void ) +STATIC void sendStatusDetailsMessage( void ) { char topicBuffer[ TOPIC_BUFFER_SIZE + 1 ] = { 0 }; size_t topicBufferLength = 0U; @@ -457,20 +476,119 @@ STATIC bool sendSuccessMessage( void ) ( uint16_t ) app_strnlen( globalJobId, 1000U ), &topicBufferLength ); + /* + * Convert the current app firmware version to a string and send it to the + * the cloud + */ + + /* + * Calling snprintf() with NULL and 0 as first two parameters gives the + * required length of the destination string. + */ + int updatedByBufferLength = snprintf( NULL, + 0, + "%u", + appFirmwareVersion.u.x.build ); + + char updatedByBuffer[ updatedByBufferLength + 1 ]; + + ( void ) snprintf( updatedByBuffer, + updatedByBufferLength + 1, + "%u", + appFirmwareVersion.u.x.build ); + /* * AWS IoT Jobs library: * Creating the message which contains the status of OTA job. * It will be published on the topic created in the previous step. */ - size_t messageBufferLength = Jobs_UpdateMsg( Succeeded, + size_t messageBufferLength = Jobs_UpdateMsg( InProgress, "2", 1U, + updatedByBuffer, + updatedByBufferLength, + messageBuffer, + UPDATE_JOB_MSG_LENGTH ); + + prvMQTTPublish( topicBuffer, + topicBufferLength, + messageBuffer, + messageBufferLength, + 0 ); +} + +STATIC void sendFinalJobStatusMessage( JobCurrentStatus_t status ) +{ + char topicBuffer[ TOPIC_BUFFER_SIZE + 1 ] = { 0 }; + size_t topicBufferLength = 0U; + char messageBuffer[ UPDATE_JOB_MSG_LENGTH ] = { 0 }; + + /* + * AWS IoT Jobs library: + * Creating the MQTT topic to update the status of OTA job. + */ + Jobs_Update( topicBuffer, + TOPIC_BUFFER_SIZE, + OTA_THING_NAME, + ( uint16_t ) app_strnlen( OTA_THING_NAME, 1000U ), + globalJobId, + ( uint16_t ) app_strnlen( globalJobId, 1000U ), + &topicBufferLength ); + + /* + * AWS IoT Jobs library: + * Creating the message which contains the status of OTA job. + * It will be published on the topic created in the previous step. + */ + size_t messageBufferLength = Jobs_UpdateMsg( status, + "3", + 1U, + jobStatusDetails.updatedBy, + jobStatusDetails.updatedByLen, messageBuffer, UPDATE_JOB_MSG_LENGTH ); prvMQTTPublish( topicBuffer, topicBufferLength, messageBuffer, messageBufferLength, 0 ); - LogInfo( ( "OTA update completed successfully.\n" ) ); globalJobId[ 0 ] = 0U; + + if( status == Succeeded ) + { + LogInfo( ( "OTA update completed successfully.\n" ) ); + } +} + +STATIC void printJobParams( const char * jobId, + AfrOtaJobDocumentFields_t jobFields ) +{ + char streamName[ jobFields.imageRefLen ]; + char filePath[ jobFields.filepathLen ]; + char certFile[ jobFields.certfileLen ]; + char sig[ jobFields.signatureLen ]; + + LogInfo( ( "Extracted parameter: [jobid: %s]\n", jobId ) ); + + /* + * Strings in the jobFields structure are not null terminated so copy them + * to a buffer and null terminate them for printing. + */ + ( void ) memcpy( streamName, jobFields.imageRef, jobFields.imageRefLen ); + streamName[ jobFields.imageRefLen ] = '\0'; + LogInfo( ( "Extracted parameter: [streamname: %s]\n", streamName ) ); + + ( void ) memcpy( filePath, jobFields.filepath, jobFields.filepathLen ); + filePath[ jobFields.filepathLen ] = '\0'; + LogInfo( ( "Extracted parameter: [filepath: %s]\n", filePath ) ); + + LogInfo( ( "Extracted parameter: [filesize: %u]\n", jobFields.fileSize ) ); + LogInfo( ( "Extracted parameter: [fileid: %u]\n", jobFields.fileId ) ); + + ( void ) memcpy( certFile, jobFields.certfile, jobFields.certfileLen ); + certFile[ jobFields.certfileLen ] = '\0'; + LogInfo( ( "Extracted parameter: [certfile: %s]\n", certFile ) ); + + ( void ) memcpy( sig, jobFields.signature, jobFields.signatureLen ); + sig[ jobFields.signatureLen ] = '\0'; + LogInfo( ( "Extracted parameter: [sig-sha256-rsa: %s]\n", sig ) ); } /* -------------------------------------------------------------------------- */ @@ -665,29 +783,83 @@ STATIC OtaPalJobDocProcessingResult_t receivedJobDocumentHandler( OtaJobEventDat { bool handled = jobDocumentParser( ( char * ) jobDoc->jobData, jobDoc->jobDataLength, &jobFields ); - if( otaPal_GetPlatformImageState( &jobFields ) == OtaPalImageStatePendingCommit ) - { - ( void ) sendSuccessMessage(); - - otaAgentShutdown(); - } + populateJobStatusDetailsFields( ( char * ) jobDoc->jobData, jobDoc->jobDataLength, &jobStatusDetails ); if( handled ) { - initMqttDownloader( &jobFields ); + printJobParams( globalJobId, jobFields ); - /* AWS IoT core returns the signature in a PEM format. We need to - * convert it to DER format for image signature verification. */ + /*In pending commit state, the device is in self test mode */ + if( otaPal_GetPlatformImageState( &jobFields ) == OtaPalImageStatePendingCommit ) + { + /* + * Convert the updatedBy string to an integer so the updatedBy + * version can be compared to the update firmware version. + */ + char updatedByBuffer[ jobStatusDetails.updatedByLen ]; + char * endPtr; + + /* + * updatedBy string is not null terminated so copy it to a + * temporary string and null terminate. + */ + ( void ) memcpy( updatedByBuffer, + jobStatusDetails.updatedBy, + jobStatusDetails.updatedByLen ); + + updatedByBuffer[ jobStatusDetails.updatedByLen ] = '\0'; + + uint16_t updatedByVer = ( uint16_t ) strtoul( updatedByBuffer, + &endPtr, + 10 ); + + if( updatedByVer < appFirmwareVersion.u.x.build ) + { + LogInfo( ( "New image has a higher version number than the current image: " + "New image version=%u" + ", Previous image version=%u", + appFirmwareVersion.u.x.build, + updatedByVer ) ); - handled = convertSignatureToDER( OtaImageSignatureDecoded, sizeof( OtaImageSignatureDecoded ), &jobFields ); + otaPal_SetPlatformImageState( &jobFields, OtaImageStateAccepted ); + ( void ) sendFinalJobStatusMessage( Succeeded ); - if( handled ) - { - xResult = otaPal_CreateFileForRx( &jobFields ); + xResult = OtaPalNewImageBooted; + } + else + { + LogInfo( ( "Application version of the new image is not higher than the current image: " + "New images are expected to have a higher version number." ) ); + + otaPal_SetPlatformImageState( &jobFields, OtaImageStateRejected ); + + /* + * Mark the job as FAILED (AWS Job Service will not allow + * the job to be set to REJECTED if the job has been + * started already). + */ + ( void ) sendFinalJobStatusMessage( Failed ); + + xResult = OtaPalNewImageBootFailed; + } } else { - LogError( ( "Failed to decode the image signature to DER format." ) ); + initMqttDownloader( &jobFields ); + + /* AWS IoT core returns the signature in a PEM format. We need to + * convert it to DER format for image signature verification. */ + + handled = convertSignatureToDER( OtaImageSignatureDecoded, sizeof( OtaImageSignatureDecoded ), &jobFields ); + + if( handled ) + { + xResult = otaPal_CreateFileForRx( &jobFields ); + } + else + { + LogError( ( "Failed to decode the image signature to DER format." ) ); + } } } } @@ -862,11 +1034,15 @@ STATIC void processOTAEvents() vOtaActiveHook(); break; + case OtaPalNewImageBooted: + LogInfo( ( "New firmware image booted.\n" ) ); + vOtaNotActiveHook(); + break; + case OtaPalJobDocFileCreateFailed: case OtaPalNewImageBootFailed: case OtaPalJobDocProcessingStateInvalid: LogInfo( ( "No OTA job available. \n" ) ); - otaAgentShutdown(); break; } @@ -958,6 +1134,8 @@ STATIC void processOTAEvents() case OtaAgentEventActivateImage: LogInfo( ( "Attempting to activate image.\n" ) ); + sendStatusDetailsMessage(); + if( activateImage() == true ) { LogInfo( ( "Activated image.\n" ) ); diff --git a/components/aws_iot/jobs_for_aws_iot_embedded_sdk/CMakeLists.txt b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/CMakeLists.txt index 250839f4..b2ff66be 100644 --- a/components/aws_iot/jobs_for_aws_iot_embedded_sdk/CMakeLists.txt +++ b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/CMakeLists.txt @@ -17,6 +17,7 @@ else() set(PATCH_FILES "${PATCH_FILES_DIRECTORY}/0001-Check-for-RSA-signature-instead-of-ECDSA.patch" "${PATCH_FILES_DIRECTORY}/0002-Use-custom-strnlen-implementation.patch" + "${PATCH_FILES_DIRECTORY}/0003-Add-functionality-for-sending-and-retrieving-updated.patch" ) iot_reference_arm_corstone3xx_apply_patches("${jobs-for-aws-iot-embedded-sdk_SOURCE_DIR}" "${PATCH_FILES}") diff --git a/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0001-Check-for-RSA-signature-instead-of-ECDSA.patch b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0001-Check-for-RSA-signature-instead-of-ECDSA.patch index ead71932..9ff2cc3d 100644 --- a/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0001-Check-for-RSA-signature-instead-of-ECDSA.patch +++ b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0001-Check-for-RSA-signature-instead-of-ECDSA.patch @@ -1,7 +1,7 @@ From 6ea98c733a2ea3c9d8cfdf4d3b689598ffdf8d54 Mon Sep 17 00:00:00 2001 From: Chuyue Luo Date: Wed, 4 Dec 2024 15:20:34 +0000 -Subject: [PATCH 1/2] Check for RSA signature instead of ECDSA +Subject: [PATCH 1/3] Check for RSA signature instead of ECDSA The Jobs-for-AWS-IoT-embedded-sdk library assumes the OTA job is signed using ECDSA, but we currently use RSA. Thus, change the check for an diff --git a/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0002-Use-custom-strnlen-implementation.patch b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0002-Use-custom-strnlen-implementation.patch index 5bb873fb..2677cad4 100644 --- a/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0002-Use-custom-strnlen-implementation.patch +++ b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0002-Use-custom-strnlen-implementation.patch @@ -1,7 +1,7 @@ From 02b777ed41b393163c3f793d8ff3b608ac0b9634 Mon Sep 17 00:00:00 2001 From: Chuyue Luo Date: Wed, 4 Dec 2024 15:24:44 +0000 -Subject: [PATCH 2/2] Use custom `strnlen` implementation +Subject: [PATCH 2/3] Use custom `strnlen` implementation The Arm Compiler for Embedded (v6.21) does not support the `strnlen` function. Therefore, use our own implementation (`app_strnlen`) instead. diff --git a/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0003-Add-functionality-for-sending-and-retrieving-updated.patch b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0003-Add-functionality-for-sending-and-retrieving-updated.patch new file mode 100644 index 00000000..0ef1b506 --- /dev/null +++ b/components/aws_iot/jobs_for_aws_iot_embedded_sdk/integration/patches/0003-Add-functionality-for-sending-and-retrieving-updated.patch @@ -0,0 +1,183 @@ +From b2eb23684950fb835cc77ad852a3a4e37ac43f39 Mon Sep 17 00:00:00 2001 +From: Chuyue Luo +Date: Fri, 10 Jan 2025 11:50:45 +0000 +Subject: [PATCH 3/3] Add functionality for sending and retrieving updatedBy + version + +An OTA image should be accepted only if the update firmware version is +higher than the current firmware version. This patch adds functionality +for sending the updatedBy version to the cloud and retrieving it when +the device reboots. + +Signed-off-by: Chuyue Luo +--- + source/include/jobs.h | 6 +++++ + source/jobs.c | 6 ++++- + source/otaJobParser/include/job_parser.h | 33 ++++++++++++++++++++++++ + source/otaJobParser/job_parser.c | 32 +++++++++++++++++++++++ + 4 files changed, 76 insertions(+), 1 deletion(-) + +diff --git a/source/include/jobs.h b/source/include/jobs.h +index 11caba6..9a04cb9 100644 +--- a/source/include/jobs.h ++++ b/source/include/jobs.h +@@ -1,6 +1,8 @@ + /* + * AWS IoT Jobs v1.5.1 + * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. ++ * Copyright 2024 Arm Limited and/or its affiliates ++ * + * + * SPDX-License-Identifier: MIT + * +@@ -841,6 +843,8 @@ JobsStatus_t Jobs_Update( char * buffer, + * @param status Current status of the job + * @param expectedVersion The version that is expected + * @param expectedVersionLength The length of the expectedVersion string ++ * @param updatedBy The app firmware version before the update ++ * @param updatedByLength The length of the updatedBy string + * @param buffer The buffer to be written to + * @param bufferSize the size of the buffer + * +@@ -878,6 +882,8 @@ JobsStatus_t Jobs_Update( char * buffer, + size_t Jobs_UpdateMsg( JobCurrentStatus_t status, + const char * expectedVersion, + size_t expectedVersionLength, ++ const char * updatedBy, ++ size_t updatedByLength, + char * buffer, + size_t bufferSize ); + /* @[declare_jobs_updatemsg] */ +diff --git a/source/jobs.c b/source/jobs.c +index 0f83c27..0281008 100644 +--- a/source/jobs.c ++++ b/source/jobs.c +@@ -820,6 +820,8 @@ JobsStatus_t Jobs_Update( char * buffer, + size_t Jobs_UpdateMsg( JobCurrentStatus_t status, + const char * expectedVersion, + size_t expectedVersionLength, ++ const char * updatedBy, ++ size_t updatedByLength, + char * buffer, + size_t bufferSize ) + { +@@ -853,7 +855,9 @@ size_t Jobs_UpdateMsg( JobCurrentStatus_t status, + ( void ) strnAppend( buffer, &start, bufferSize, jobStatusString[ status ], jobStatusStringLengths[ status ] ); + ( void ) strnAppend( buffer, &start, bufferSize, JOBS_API_EXPECTED_VERSION, JOBS_API_EXPECTED_VERSION_LENGTH ); + ( void ) strnAppend( buffer, &start, bufferSize, expectedVersion, expectedVersionLength ); +- ( void ) strnAppend( buffer, &start, bufferSize, "\"}", ( CONST_STRLEN( "\"}" ) ) ); ++ ( void ) strnAppend( buffer, &start, bufferSize, "\",\"statusDetails\":{\"updatedBy\":\"", ( CONST_STRLEN( "\",\"statusDetails\":{\"updatedBy\":\"" ) )); ++ ( void ) strnAppend( buffer, &start, bufferSize, updatedBy, updatedByLength ); ++ ( void ) strnAppend( buffer, &start, bufferSize, "\"}}", ( CONST_STRLEN( "\"}}" ) ) ); + } + + return start; +diff --git a/source/otaJobParser/include/job_parser.h b/source/otaJobParser/include/job_parser.h +index 3fcf537..7e87d3b 100644 +--- a/source/otaJobParser/include/job_parser.h ++++ b/source/otaJobParser/include/job_parser.h +@@ -1,6 +1,9 @@ + /* + * AWS IoT Jobs v1.5.1 + * Copyright (C) 2023 Amazon.com, Inc. and its affiliates. All Rights Reserved. ++ * Copyright 2024 Arm Limited and/or its affiliates ++ * ++ * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License. See the LICENSE accompanying this file +@@ -61,6 +64,20 @@ typedef struct + uint32_t fileType; + } AfrOtaJobDocumentFields_t; + ++/** ++ * @ingroup jobs_structs ++ * @brief struct containing the entries within the statusDetails field of an AFR ++ * OTA Job Document ++ */ ++typedef struct ++{ ++ /** @brief The app firmware version prior to the update */ ++ const char * updatedBy; ++ ++ /** @brief Length of updatedBy string */ ++ size_t updatedByLen; ++} AfrOtaJobDocumentStatusDetails_t; ++ + /** + * @brief Populate the fields of 'result', returning + * true if successful. +@@ -79,4 +96,20 @@ bool populateJobDocFields( const char * jobDoc, + AfrOtaJobDocumentFields_t * result ); + /* @[declare_populatejobdocfields] */ + ++/** ++ * @brief Populate the fields of 'result', returning true if successful. ++ * ++ * @param jobDoc FreeRTOS OTA job document ++ * @param jobDocLength OTA job document length ++ * @param result Job document statusDetails structure to populate ++ * @return true Job document statusDetails fields were parsed from the document ++ * @return false Job document statusDetails fields were not parsed from the ++ * document ++ */ ++/* @[declare_populatejobstatusdetailsfields] */ ++bool populateJobStatusDetailsFields( const char * jobDoc, ++ const size_t jobDocLength, ++ AfrOtaJobDocumentStatusDetails_t * result ); ++/* @[declare_populatejobstatusdetailsfields] */ ++ + #endif /* JOB_PARSER_H */ +diff --git a/source/otaJobParser/job_parser.c b/source/otaJobParser/job_parser.c +index 8638dc0..eb63777 100644 +--- a/source/otaJobParser/job_parser.c ++++ b/source/otaJobParser/job_parser.c +@@ -19,6 +19,11 @@ + #include "core_json.h" + #include "job_parser.h" + ++/** ++ * @brief Get the length of a string literal. ++ */ ++#define CONST_STRLEN( x ) ( sizeof( ( x ) ) - 1U ) ++ + /** + * @brief Populates common job document fields in result + * +@@ -184,6 +189,33 @@ bool populateJobDocFields( const char * jobDoc, + return populatedJobDocFields; + } + ++bool populateJobStatusDetailsFields( const char * jobDoc, ++ const size_t jobDocLength, ++ AfrOtaJobDocumentStatusDetails_t * result ) ++{ ++ bool populatedJobStatusDetailsFields = false; ++ JSONStatus_t jsonResult = JSONNotFound; ++ const char * jsonValue = NULL; ++ size_t jsonValueLength = 0U; ++ ++ jsonResult = JSON_SearchConst( jobDoc, ++ jobDocLength, ++ "execution.statusDetails.updatedBy", ++ CONST_STRLEN("execution.statusDetails.updatedBy"), ++ &jsonValue, ++ &jsonValueLength, ++ NULL ); ++ ++ if( jsonResult == JSONSuccess ) ++ { ++ result->updatedBy = jsonValue; ++ result->updatedByLen = ( uint32_t ) jsonValueLength; ++ populatedJobStatusDetailsFields = true; ++ } ++ ++ return populatedJobStatusDetailsFields; ++} ++ + static JSONStatus_t populateCommonFields( const char * jobDoc, + const size_t jobDocLength, + int32_t fileIndex, +-- +2.47.1 + diff --git a/release_changes/202501081605.change.md b/release_changes/202501081605.change.md new file mode 100644 index 00000000..fdd69f9e --- /dev/null +++ b/release_changes/202501081605.change.md @@ -0,0 +1,2 @@ +Modular OTA improvements: Print job metadata and check if new firmware version +higher than previous version