Simple interface for Mbed Cloud Client
Embed:
(wiki syntax)
Show/hide line numbers
arm_uc_pal_flashiap_implementation.c
00001 // ---------------------------------------------------------------------------- 00002 // Copyright 2016-2017 ARM Ltd. 00003 // 00004 // SPDX-License-Identifier: Apache-2.0 00005 // 00006 // Licensed under the Apache License, Version 2.0 (the "License"); 00007 // you may not use this file except in compliance with the License. 00008 // You may obtain a copy of the License at 00009 // 00010 // http://www.apache.org/licenses/LICENSE-2.0 00011 // 00012 // Unless required by applicable law or agreed to in writing, software 00013 // distributed under the License is distributed on an "AS IS" BASIS, 00014 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 00015 // See the License for the specific language governing permissions and 00016 // limitations under the License. 00017 // ---------------------------------------------------------------------------- 00018 00019 #if defined(TARGET_LIKE_MBED) 00020 00021 #define __STDC_FORMAT_MACROS 00022 00023 #include "update-client-pal-flashiap/arm_uc_pal_flashiap.h" 00024 00025 #include "update-client-pal-flashiap/arm_uc_pal_flashiap_platform.h" 00026 00027 #include "update-client-common/arm_uc_metadata_header_v2.h" 00028 #include "update-client-common/arm_uc_types.h" 00029 #include "update-client-common/arm_uc_utilities.h" 00030 00031 #define TRACE_GROUP "UCPI" 00032 #include "update-client-common/arm_uc_trace.h" 00033 #include <inttypes.h> 00034 #include <stddef.h> 00035 00036 #ifndef MBED_CONF_UPDATE_CLIENT_APPLICATION_DETAILS 00037 #define MBED_CONF_UPDATE_CLIENT_APPLICATION_DETAILS 0 00038 #endif 00039 00040 #ifndef MBED_CONF_UPDATE_CLIENT_BOOTLOADER_DETAILS 00041 #define MBED_CONF_UPDATE_CLIENT_BOOTLOADER_DETAILS 0 00042 #endif 00043 00044 #ifndef MBED_CONF_UPDATE_CLIENT_STORAGE_ADDRESS 00045 #define MBED_CONF_UPDATE_CLIENT_STORAGE_ADDRESS 0 00046 #endif 00047 00048 #ifndef MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE 00049 #define MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE 1 00050 #endif 00051 00052 #ifndef MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS 00053 #define MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS 1 00054 #endif 00055 00056 /* consistency check */ 00057 #if (MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE == 0) 00058 #error Update client storage page cannot be zero. 00059 #endif 00060 00061 #if (MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS == 0) 00062 #error Update client storage locations must be at least 1. 00063 #endif 00064 00065 /* Check that the statically allocated buffers are aligned with the block size */ 00066 #define ARM_UC_PAL_ONE_BUFFER (ARM_UC_BUFFER_SIZE / 2) 00067 #define ARM_UC_PAL_PAGES (ARM_UC_PAL_ONE_BUFFER / MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE) 00068 00069 #if !((ARM_UC_PAL_PAGES * MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE) == ARM_UC_PAL_ONE_BUFFER) 00070 #error Update client buffer must be divisible by the block page size 00071 #endif 00072 00073 /* Calculate aligned external header size */ 00074 #define ARM_UC_PAL_HEADER_SIZE (((ARM_UC_INTERNAL_HEADER_SIZE_V2 + MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE - 1) \ 00075 / MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE) * MBED_CONF_UPDATE_CLIENT_STORAGE_PAGE) 00076 00077 static void (*arm_uc_pal_flashiap_callback)(uint32_t) = NULL; 00078 00079 static void arm_uc_pal_flashiap_signal_internal(uint32_t event) 00080 { 00081 if (arm_uc_pal_flashiap_callback) 00082 { 00083 arm_uc_pal_flashiap_callback(event); 00084 } 00085 } 00086 00087 arm_uc_error_t ARM_UC_PAL_FlashIAP_Initialize(void (*callback)(uint32_t)) 00088 { 00089 arm_uc_error_t result = { .code = ERR_INVALID_PARAMETER }; 00090 00091 int32_t status = arm_uc_flashiap_init(); 00092 00093 if (status == ARM_UC_FLASHIAP_SUCCESS) 00094 { 00095 arm_uc_pal_flashiap_callback = callback; 00096 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_INITIALIZE_DONE); 00097 00098 result.code = ERR_NONE; 00099 } 00100 00101 return result; 00102 } 00103 00104 /** 00105 * @brief Get maximum number of supported storage locations. 00106 * 00107 * @return Number of storage locations. 00108 */ 00109 uint32_t ARM_UC_PAL_FlashIAP_GetMaxID(void) 00110 { 00111 return MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS; 00112 } 00113 00114 /** 00115 * @brief Prepare the storage layer for a new firmware image. 00116 * @details The storage location is set up to receive an image with 00117 * the details passed in the details struct. 00118 * 00119 * @param location Storage location ID. 00120 * @param details Pointer to a struct with firmware details. 00121 * @param buffer Temporary buffer for formatting and storing metadata. 00122 * @return Returns ERR_NONE on accept, and signals the event handler with 00123 * either DONE or ERROR when complete. 00124 * Returns ERR_INVALID_PARAMETER on reject, and no signal is sent. 00125 */ 00126 arm_uc_error_t ARM_UC_PAL_FlashIAP_Prepare(uint32_t location, 00127 const arm_uc_firmware_details_t* details, 00128 arm_uc_buffer_t* buffer) 00129 { 00130 arm_uc_error_t result = { .code = ERR_INVALID_PARAMETER }; 00131 00132 if (details && buffer && buffer->ptr) 00133 { 00134 UC_PAAL_TRACE("ARM_UC_PAL_FlashIAP_Prepare: %" PRIX32 " %" PRIX32, 00135 location, details->size); 00136 00137 /* encode firmware details in buffer */ 00138 result = arm_uc_create_internal_header_v2(details, buffer); 00139 00140 /* make space for new firmware */ 00141 if (result.error == ERR_NONE) 00142 { 00143 /* find location start address */ 00144 uint32_t slot_size = MBED_CONF_UPDATE_CLIENT_STORAGE_SIZE / 00145 MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS; 00146 uint32_t start_address = MBED_CONF_UPDATE_CLIENT_STORAGE_ADDRESS + 00147 location * slot_size; 00148 00149 /* find end address */ 00150 uint32_t end_address = start_address + 00151 ARM_UC_PAL_HEADER_SIZE + 00152 details->size; 00153 00154 uint32_t erase_address = start_address; 00155 00156 /* find exact erase size */ 00157 while (erase_address < end_address) 00158 { 00159 uint32_t sector_size = arm_uc_flashiap_get_sector_size(erase_address); 00160 erase_address += sector_size; 00161 } 00162 00163 if (erase_address > (start_address + slot_size)) 00164 { 00165 result.code = ERR_INVALID_PARAMETER; 00166 UC_PAAL_ERR_MSG("Firmware too large"); 00167 } 00168 else 00169 { 00170 /* erase */ 00171 erase_address = start_address; 00172 while (erase_address < end_address) 00173 { 00174 uint32_t sector_size = arm_uc_flashiap_get_sector_size(erase_address); 00175 00176 int32_t status = arm_uc_flashiap_erase(erase_address, sector_size); 00177 00178 UC_PAAL_TRACE("erase: %" PRIX32 " %" PRIX32 " %" PRId32, 00179 erase_address, 00180 sector_size, 00181 status); 00182 00183 if (status == ARM_UC_FLASHIAP_SUCCESS) 00184 { 00185 erase_address += sector_size; 00186 } 00187 else 00188 { 00189 result.code = ERR_INVALID_PARAMETER; 00190 break; 00191 } 00192 } 00193 } 00194 00195 if (result.error == ERR_NONE) 00196 { 00197 UC_PAAL_TRACE("program: %" PRIX32 " %" PRIX32, 00198 start_address, 00199 ARM_UC_PAL_HEADER_SIZE); 00200 00201 uint32_t page_size = arm_uc_flashiap_get_page_size(); 00202 00203 /* set default return code */ 00204 result.code = ERR_NONE; 00205 00206 for (uint32_t index = 0; 00207 index < ARM_UC_PAL_HEADER_SIZE; 00208 index += page_size) 00209 { 00210 /* write header */ 00211 int32_t status = arm_uc_flashiap_program(&buffer->ptr[index], 00212 start_address + index, 00213 page_size); 00214 00215 if (status != ARM_UC_FLASHIAP_SUCCESS) 00216 { 00217 /* set return code */ 00218 result.code = ERR_INVALID_PARAMETER; 00219 break; 00220 } 00221 } 00222 00223 if (result.error == ERR_NONE) 00224 { 00225 /* signal done */ 00226 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_PREPARE_DONE); 00227 } 00228 else 00229 { 00230 UC_PAAL_ERR_MSG("arm_uc_flashiap_program failed"); 00231 } 00232 } 00233 else 00234 { 00235 UC_PAAL_ERR_MSG("arm_uc_flashiap_erase failed"); 00236 } 00237 } 00238 else 00239 { 00240 UC_PAAL_ERR_MSG("arm_uc_create_internal_header_v2 failed"); 00241 } 00242 } 00243 00244 return result; 00245 } 00246 00247 /** 00248 * @brief Write a fragment to the indicated storage location. 00249 * @details The storage location must have been allocated using the Prepare 00250 * call. The call is expected to write the entire fragment before 00251 * signaling completion. 00252 * 00253 * @param location Storage location ID. 00254 * @param offset Offset in bytes to where the fragment should be written. 00255 * @param buffer Pointer to buffer struct with fragment. 00256 * @return Returns ERR_NONE on accept, and signals the event handler with 00257 * either DONE or ERROR when complete. 00258 * Returns ERR_INVALID_PARAMETER on reject, and no signal is sent. 00259 */ 00260 arm_uc_error_t ARM_UC_PAL_FlashIAP_Write(uint32_t location, 00261 uint32_t offset, 00262 const arm_uc_buffer_t* buffer) 00263 { 00264 arm_uc_error_t result = { .code = ERR_INVALID_PARAMETER }; 00265 00266 if (buffer && buffer->ptr) 00267 { 00268 /* find location address */ 00269 uint32_t physical_address = MBED_CONF_UPDATE_CLIENT_STORAGE_ADDRESS + 00270 (location * MBED_CONF_UPDATE_CLIENT_STORAGE_SIZE / 00271 MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS) + 00272 ARM_UC_PAL_HEADER_SIZE + 00273 offset; 00274 00275 UC_PAAL_TRACE("ARM_UC_PAL_FlashIAP_Write: %p %" PRIX32 " %" PRIX32 " %" PRIX32, 00276 buffer->ptr, 00277 buffer->size, 00278 physical_address, 00279 offset); 00280 00281 /* set default return code */ 00282 result.code = ERR_NONE; 00283 00284 uint32_t page_size = arm_uc_flashiap_get_page_size(); 00285 00286 for (uint32_t index = 0; index < buffer->size; index += page_size) 00287 { 00288 int status = arm_uc_flashiap_program(&buffer->ptr[index], 00289 physical_address + index, 00290 page_size); 00291 00292 if (status != ARM_UC_FLASHIAP_SUCCESS) 00293 { 00294 /* set return code */ 00295 result.code = ERR_INVALID_PARAMETER; 00296 break; 00297 } 00298 } 00299 00300 if (result.error == ERR_NONE) 00301 { 00302 /* signal done */ 00303 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_WRITE_DONE); 00304 } 00305 else 00306 { 00307 UC_PAAL_ERR_MSG("arm_uc_flashiap_program failed"); 00308 } 00309 } 00310 00311 return result; 00312 } 00313 00314 /** 00315 * @brief Close storage location for writing and flush pending data. 00316 * 00317 * @param location Storage location ID. 00318 * @return Returns ERR_NONE on accept, and signals the event handler with 00319 * either DONE or ERROR when complete. 00320 * Returns ERR_INVALID_PARAMETER on reject, and no signal is sent. 00321 */ 00322 arm_uc_error_t ARM_UC_PAL_FlashIAP_Finalize(uint32_t location) 00323 { 00324 arm_uc_error_t result = { .code = ERR_NONE }; 00325 00326 UC_PAAL_TRACE("ARM_UC_PAL_FlashIAP_Finalize"); 00327 00328 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_FINALIZE_DONE); 00329 00330 return result; 00331 } 00332 00333 /** 00334 * @brief Read a fragment from the indicated storage location. 00335 * @details The function will read until the buffer is full or the end of 00336 * the storage location has been reached. The actual amount of 00337 * bytes read is set in the buffer struct. 00338 * 00339 * @param location Storage location ID. 00340 * @param offset Offset in bytes to read from. 00341 * @param buffer Pointer to buffer struct to store fragment. buffer->size 00342 * contains the intended read size. 00343 * @return Returns ERR_NONE on accept, and signals the event handler with 00344 * either DONE or ERROR when complete. 00345 * Returns ERR_INVALID_PARAMETER on reject, and no signal is sent. 00346 * buffer->size contains actual bytes read on return. 00347 */ 00348 arm_uc_error_t ARM_UC_PAL_FlashIAP_Read(uint32_t location, 00349 uint32_t offset, 00350 arm_uc_buffer_t* buffer) 00351 { 00352 arm_uc_error_t result = { .code = ERR_INVALID_PARAMETER }; 00353 00354 if (buffer && buffer->ptr) 00355 { 00356 UC_PAAL_TRACE("ARM_UC_PAL_FlashIAP_Read: %" PRIX32 " %" PRIX32 " %" PRIX32, 00357 location, offset, buffer->size); 00358 00359 /* find location address */ 00360 uint32_t physical_address = MBED_CONF_UPDATE_CLIENT_STORAGE_ADDRESS + 00361 (location * MBED_CONF_UPDATE_CLIENT_STORAGE_SIZE / 00362 MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS) + 00363 ARM_UC_PAL_HEADER_SIZE + 00364 offset; 00365 00366 uint32_t read_size = buffer->size; 00367 00368 int status = arm_uc_flashiap_read(buffer->ptr, 00369 physical_address, 00370 read_size); 00371 00372 if (status == ARM_UC_FLASHIAP_SUCCESS) 00373 { 00374 /* set return code */ 00375 result.code = ERR_NONE; 00376 00377 /* signal done */ 00378 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_READ_DONE); 00379 } 00380 else 00381 { 00382 UC_PAAL_ERR_MSG("arm_uc_flashiap_read failed"); 00383 } 00384 } 00385 00386 return result; 00387 } 00388 00389 /** 00390 * @brief Set the firmware image in the slot to be the new active image. 00391 * @details This call is responsible for initiating the process for 00392 * applying a new/different image. Depending on the platform this 00393 * could be: 00394 * * An empty call, if the installer can deduce which slot to 00395 * choose from based on the firmware details. 00396 * * Setting a flag to indicate which slot to use next. 00397 * * Decompressing/decrypting/installing the firmware image on 00398 * top of another. 00399 * 00400 * @param location Storage location ID. 00401 * @return Returns ERR_NONE on accept, and signals the event handler with 00402 * either DONE or ERROR when complete. 00403 * Returns ERR_INVALID_PARAMETER on reject, and no signal is sent. 00404 */ 00405 arm_uc_error_t ARM_UC_PAL_FlashIAP_Activate(uint32_t location) 00406 { 00407 arm_uc_error_t result = { .code = ERR_NONE }; 00408 00409 UC_PAAL_TRACE("ARM_UC_PAL_FlashIAP_Activate"); 00410 00411 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_ACTIVATE_DONE); 00412 00413 return result; 00414 } 00415 00416 /** 00417 * @brief Get firmware details for the firmware image in the slot passed. 00418 * @details This call populates the passed details struct with information 00419 * about the firmware image in the slot passed. Only the fields 00420 * marked as supported in the capabilities bitmap will have valid 00421 * values. 00422 * 00423 * @param details Pointer to firmware details struct to be populated. 00424 * @return Returns ERR_NONE on accept, and signals the event handler with 00425 * either DONE or ERROR when complete. 00426 * Returns ERR_INVALID_PARAMETER on reject, and no signal is sent. 00427 */ 00428 arm_uc_error_t ARM_UC_PAL_FlashIAP_GetFirmwareDetails( 00429 uint32_t location, 00430 arm_uc_firmware_details_t* details) 00431 { 00432 arm_uc_error_t result = { .code = ERR_INVALID_PARAMETER }; 00433 00434 if (details) 00435 { 00436 UC_PAAL_TRACE("ARM_UC_PAL_FlashIAP_GetFirmwareDetails"); 00437 00438 /* find location address */ 00439 uint32_t physical_address = MBED_CONF_UPDATE_CLIENT_STORAGE_ADDRESS + 00440 (location * MBED_CONF_UPDATE_CLIENT_STORAGE_SIZE / 00441 MBED_CONF_UPDATE_CLIENT_STORAGE_LOCATIONS); 00442 00443 uint8_t buffer[ARM_UC_PAL_HEADER_SIZE] = { 0 }; 00444 00445 int status = arm_uc_flashiap_read(buffer, 00446 physical_address, 00447 ARM_UC_PAL_HEADER_SIZE); 00448 00449 if (status == ARM_UC_FLASHIAP_SUCCESS) 00450 { 00451 result = arm_uc_parse_internal_header_v2(buffer, details); 00452 00453 if (result.error == ERR_NONE) 00454 { 00455 /* signal done */ 00456 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_GET_FIRMWARE_DETAILS_DONE); 00457 } 00458 else 00459 { 00460 UC_PAAL_ERR_MSG("arm_uc_parse_internal_header_v2 failed"); 00461 } 00462 } 00463 else 00464 { 00465 UC_PAAL_ERR_MSG("arm_uc_flashiap_read failed"); 00466 } 00467 } 00468 00469 return result; 00470 } 00471 00472 /*****************************************************************************/ 00473 00474 arm_uc_error_t ARM_UC_PAL_FlashIAP_GetActiveDetails(arm_uc_firmware_details_t* details) 00475 { 00476 arm_uc_error_t result = { .code = ERR_INVALID_PARAMETER }; 00477 00478 if (details) 00479 { 00480 /* read details from memory if offset is set */ 00481 if (MBED_CONF_UPDATE_CLIENT_APPLICATION_DETAILS) 00482 { 00483 /* set default error code */ 00484 result.code = ERR_NOT_READY; 00485 00486 /* Use flash driver eventhough we are reading from internal flash. 00487 This will make it easier to use with uVisor. 00488 */ 00489 uint8_t version_buffer[8] = { 0 }; 00490 00491 /* read metadata magic and version from flash */ 00492 int rc = arm_uc_flashiap_read(version_buffer, 00493 MBED_CONF_UPDATE_CLIENT_APPLICATION_DETAILS, 00494 8); 00495 00496 if (rc == ARM_UC_FLASHIAP_SUCCESS) 00497 { 00498 /* read out header magic */ 00499 uint32_t headerMagic = arm_uc_parse_uint32(&version_buffer[0]); 00500 00501 /* read out header magic */ 00502 uint32_t headerVersion = arm_uc_parse_uint32(&version_buffer[4]); 00503 00504 /* choose version to decode */ 00505 switch(headerVersion) 00506 { 00507 case ARM_UC_INTERNAL_HEADER_VERSION_V2: 00508 { 00509 result.code = ERR_NONE; 00510 /* Check the header magic */ 00511 if (headerMagic != ARM_UC_INTERNAL_HEADER_MAGIC_V2) 00512 { 00513 UC_PAAL_ERR_MSG("firmware header is v2, but does not contain v2 magic"); 00514 result.code = ERR_NOT_READY; 00515 } 00516 00517 uint8_t read_buffer[ARM_UC_INTERNAL_HEADER_SIZE_V2] = { 0 }; 00518 /* Read the rest of the header */ 00519 if (result.error == ERR_NONE) 00520 { 00521 rc = arm_uc_flashiap_read(read_buffer, 00522 MBED_CONF_UPDATE_CLIENT_APPLICATION_DETAILS, 00523 ARM_UC_INTERNAL_HEADER_SIZE_V2); 00524 if (rc != 0) 00525 { 00526 result.code = ERR_NOT_READY; 00527 UC_PAAL_ERR_MSG("failed to read v2 header"); 00528 } 00529 } 00530 /* Parse the header */ 00531 if (result.error == ERR_NONE) 00532 { 00533 result = arm_uc_parse_internal_header_v2(read_buffer, details); 00534 if (result.error != ERR_NONE) 00535 { 00536 UC_PAAL_ERR_MSG("failed to parse v2 header"); 00537 } 00538 } 00539 break; 00540 } 00541 /* 00542 * Other firmware header versions can be supported here. 00543 */ 00544 default: 00545 { 00546 UC_PAAL_ERR_MSG("unrecognized firmware header version"); 00547 result.code = ERR_NOT_READY; 00548 } 00549 } 00550 } 00551 else 00552 { 00553 UC_PAAL_ERR_MSG("flash read failed"); 00554 } 00555 } 00556 else 00557 { 00558 /* offset not set - zero out struct */ 00559 memset(details, 0, sizeof(arm_uc_firmware_details_t)); 00560 00561 result.code = ERR_NONE; 00562 } 00563 00564 /* signal event if operation was successful */ 00565 if (result.error == ERR_NONE) 00566 { 00567 UC_PAAL_TRACE("callback"); 00568 00569 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_GET_ACTIVE_FIRMWARE_DETAILS_DONE); 00570 } 00571 } 00572 00573 return result; 00574 } 00575 00576 arm_uc_error_t ARM_UC_PAL_FlashIAP_GetInstallerDetails(arm_uc_installer_details_t* details) 00577 { 00578 arm_uc_error_t result = { .code = ERR_INVALID_PARAMETER }; 00579 00580 if (details) 00581 { 00582 /* only read from memory if offset is set */ 00583 if (MBED_CONF_UPDATE_CLIENT_BOOTLOADER_DETAILS) 00584 { 00585 uint8_t* arm = (uint8_t*) (MBED_CONF_UPDATE_CLIENT_BOOTLOADER_DETAILS + 00586 offsetof(arm_uc_installer_details_t, arm_hash)); 00587 00588 uint8_t* oem = (uint8_t*) (MBED_CONF_UPDATE_CLIENT_BOOTLOADER_DETAILS + 00589 offsetof(arm_uc_installer_details_t, oem_hash)); 00590 00591 uint8_t* layout = (uint8_t*) (MBED_CONF_UPDATE_CLIENT_BOOTLOADER_DETAILS + 00592 offsetof(arm_uc_installer_details_t, layout)); 00593 00594 /* populate installer details struct */ 00595 memcpy(&details->arm_hash, arm, ARM_UC_SHA256_SIZE); 00596 memcpy(&details->oem_hash, oem, ARM_UC_SHA256_SIZE); 00597 details->layout = arm_uc_parse_uint32(layout); 00598 } 00599 else 00600 { 00601 /* offset not set, zero details struct */ 00602 memset(details, 0, sizeof(arm_uc_installer_details_t)); 00603 } 00604 00605 arm_uc_pal_flashiap_signal_internal(ARM_UC_PAAL_EVENT_GET_INSTALLER_DETAILS_DONE); 00606 00607 result.code = ERR_NONE; 00608 } 00609 00610 return result; 00611 } 00612 00613 #endif
Generated on Tue Jul 12 2022 19:01:33 by 1.7.2