Simple interface for Mbed Cloud Client
Embed:
(wiki syntax)
Show/hide line numbers
arm_uc_source_manager.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 #include "update-client-source-manager/arm_uc_source_manager.h" 00020 00021 #include "update-client-common/arm_uc_common.h" 00022 #include "update-client-source/arm_uc_source.h" 00023 00024 #include <stdint.h> 00025 #include <stdlib.h> 00026 00027 static const ARM_UPDATE_SOURCE* source_registry[MAX_SOURCES]; 00028 static ARM_SOURCE_SignalEvent_t event_cb; 00029 00030 // storage set aside for adding event_cb to the event queue 00031 static arm_uc_callback_t event_cb_storage = { 0 }; 00032 00033 typedef enum { 00034 QUERY_TYPE_UNKNOWN, 00035 QUERY_TYPE_MANIFEST_DEFAULT, 00036 QUERY_TYPE_MANIFEST_URL, 00037 QUERY_TYPE_FIRMWARE, 00038 QUERY_TYPE_KEYTABLE 00039 } query_type_t; 00040 00041 typedef struct { 00042 arm_uc_uri_t *uri; // the uri from which the resourced should be fetched 00043 arm_uc_buffer_t *buffer; // buffer given by caller to contain the results of the fetch 00044 uint32_t offset; // offset parameter passed to the source 00045 query_type_t type; // type of request, whether is manifest, firmware or keytable 00046 uint8_t excludes[MAX_SOURCES]; // records the tried and failed sources during a get request 00047 uint8_t current_source; // records the index of source use in the get request in progress 00048 } request_t; 00049 00050 // Hold information about the request in flight, there will always only be one request in flight 00051 static request_t request_in_flight; 00052 00053 /* ==================================================================== * 00054 * Private Functions * 00055 * ==================================================================== */ 00056 00057 /** 00058 * @brief Initialise a request_t struct, called when a new request 00059 * have been initiated from the hub 00060 */ 00061 static arm_uc_error_t ARM_UCSM_RequestStructInit(request_t* request); 00062 00063 /** 00064 * @brief The SourceRegistry is an array of ARM_UPDATE_SOURCE 00065 */ 00066 static arm_uc_error_t ARM_UCSM_SourceRegistryInit(); 00067 static arm_uc_error_t ARM_UCSM_SourceRegistryAdd(const ARM_UPDATE_SOURCE* source); 00068 static arm_uc_error_t ARM_UCSM_SourceRegistryRemove(const ARM_UPDATE_SOURCE* source); 00069 00070 /** 00071 * @brief return the index of the source with the smallest cost 00072 * @param url Struct containing URL. NULL for default Manifest. 00073 * @param type The type of current request. 00074 * @param excludes Pointer to an array of size MAX_SOURCES to indicate 00075 * sources we want to exclude in the search. Set excludes[i]=1 to 00076 * exclude source_registry[i] 00077 * @param index Used to return the index of the source with the smallest cost 00078 */ 00079 static arm_uc_error_t ARM_UCSM_SourceRegistryGetLowestCost(arm_uc_uri_t* uri, 00080 query_type_t type, 00081 uint8_t* excludes, 00082 uint32_t* index); 00083 00084 /** 00085 * @brief Find the source of lowest cost and call the corresponding method 00086 * depending on the type of the request. Retry with source of the next 00087 * smallest cost if previous sources failed until the source registry 00088 * is exhausted. 00089 */ 00090 static arm_uc_error_t ARM_UCSM_Get(request_t* req); 00091 00092 /** 00093 * @brief Catch callbacks from sources to enable error handling 00094 */ 00095 static void ARM_UCSM_CallbackWrapper(uint32_t event); 00096 00097 /** 00098 * @brief Retry get due to source being busy 00099 */ 00100 static void ARM_UCSM_AsyncRetryGet(uint32_t); 00101 00102 /** 00103 * @brief Initialise the `Request` struct 00104 */ 00105 static arm_uc_error_t ARM_UCSM_RequestStructInit(request_t* request) 00106 { 00107 for (uint32_t i=0; i<MAX_SOURCES; i++) 00108 { 00109 request->excludes[i] = 0; 00110 } 00111 00112 request->current_source = MAX_SOURCES; 00113 request->uri = NULL; 00114 request->offset = 0; 00115 request->type = QUERY_TYPE_UNKNOWN; 00116 request->buffer = NULL; 00117 00118 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00119 } 00120 00121 /** 00122 * @brief Initialise the source_registry array to NULL 00123 */ 00124 static arm_uc_error_t ARM_UCSM_SourceRegistryInit() 00125 { 00126 for(uint32_t i=0; i<MAX_SOURCES; i++) 00127 { 00128 source_registry[i] = NULL; 00129 } 00130 00131 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00132 } 00133 /** 00134 * @brief Returns the index of the given source in the source array, 00135 * or MAX_SOURCES if the source is not found 00136 */ 00137 static uint32_t ARM_UCSM_GetIndexOfSource(const ARM_UPDATE_SOURCE* source) 00138 { 00139 uint32_t index = MAX_SOURCES; 00140 00141 for(uint32_t i=0; i<MAX_SOURCES; i++) 00142 { 00143 if(source_registry[i] == source) 00144 { 00145 index = i; 00146 break; 00147 } 00148 } 00149 return index; 00150 } 00151 00152 /** 00153 * @brief Add pointer to source to the source_registry array 00154 */ 00155 static arm_uc_error_t ARM_UCSM_SourceRegistryAdd(const ARM_UPDATE_SOURCE* source) 00156 { 00157 uint8_t added = 0; 00158 00159 for(uint32_t i=0; i<MAX_SOURCES; i++) 00160 { 00161 if(source_registry[i] == NULL) 00162 { 00163 source_registry[i] = source; 00164 added = 1; 00165 break; 00166 } 00167 } 00168 00169 if (added == 0) // registry full 00170 { 00171 return (arm_uc_error_t){ SOMA_ERR_SOURCE_REGISTRY_FULL }; 00172 } 00173 00174 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00175 } 00176 00177 /** 00178 * @brief Remove pointer to source from the source_registry array 00179 */ 00180 static arm_uc_error_t ARM_UCSM_SourceRegistryRemove(const ARM_UPDATE_SOURCE* source) 00181 { 00182 uint32_t index = ARM_UCSM_GetIndexOfSource(source); 00183 00184 if (index == MAX_SOURCES) // source not found 00185 { 00186 return (arm_uc_error_t){ SOMA_ERR_SOURCE_NOT_FOUND }; 00187 } 00188 00189 source_registry[index] = NULL; 00190 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00191 } 00192 00193 /** 00194 * @brief return the index of the source with the smallest cost 00195 * @param url Struct containing URL. NULL for default Manifest. 00196 * @param type The type of current request. 00197 * @param excludes Pointer to an array of size MAX_SOURCES to indicate 00198 * sources we want to exclude in the search. Set excludes[i]=1 to 00199 * exclude source_registry[i] 00200 * @param index Used to return the index of the source with the smalllest cost 00201 */ 00202 static arm_uc_error_t ARM_UCSM_SourceRegistryGetLowestCost(arm_uc_uri_t* uri, 00203 query_type_t type, 00204 uint8_t* excludes, 00205 uint32_t* index) 00206 { 00207 uint32_t min_cost = UINT32_MAX; 00208 uint32_t min_cost_index = 0; 00209 arm_uc_error_t retval = (arm_uc_error_t){ SOMA_ERR_NONE }; 00210 00211 UC_SRCE_TRACE("+ARM_UCSM_SourceRegistryGetLowestCost"); 00212 00213 // loop through all sources 00214 for (uint32_t i=0; i<MAX_SOURCES; i++) 00215 { 00216 // if source is NULL or it has been explicitly excluded because of failure before 00217 if (source_registry[i] == NULL || (excludes != NULL && excludes[i] == 1)) 00218 { 00219 continue; 00220 } 00221 00222 ARM_SOURCE_CAPABILITIES cap = source_registry[i]->GetCapabilities(); 00223 00224 uint32_t cost = UINT32_MAX; 00225 if (uri == NULL && type == QUERY_TYPE_MANIFEST_DEFAULT && cap.manifest_default == 1) 00226 { 00227 retval = source_registry[i]->GetManifestDefaultCost(&cost); 00228 } 00229 else if (uri != NULL && type == QUERY_TYPE_MANIFEST_URL && cap.manifest_url == 1) 00230 { 00231 retval = source_registry[i]->GetManifestURLCost(uri, &cost); 00232 } 00233 else if (uri != NULL && type == QUERY_TYPE_FIRMWARE && cap.firmware == 1) 00234 { 00235 retval = source_registry[i]->GetFirmwareURLCost(uri, &cost); 00236 } 00237 else if (uri != NULL && type == QUERY_TYPE_KEYTABLE && cap.keytable == 1) 00238 { 00239 retval = source_registry[i]->GetKeytableURLCost(uri, &cost); 00240 } 00241 00242 if (retval.code != SRCE_ERR_NONE) // get cost from source i failed 00243 { 00244 // cost is invalid at this point, hence skip to next iteration 00245 UC_SRCE_TRACE("-ARM_UCSM_SourceRegistryGetLowestCost: invalid cost for %" PRIu32, i); 00246 continue; 00247 } 00248 00249 // record the cost and i if cost is lower 00250 if (min_cost > cost) 00251 { 00252 min_cost = cost; 00253 min_cost_index = i; 00254 } 00255 } 00256 00257 if (min_cost == UINT32_MAX) 00258 { 00259 UC_SRCE_TRACE("-ARM_UCSM_SourceRegistryGetLowestCost: Error - No route"); 00260 return (arm_uc_error_t){ SOMA_ERR_NO_ROUTE_TO_SOURCE }; 00261 } 00262 00263 *index = min_cost_index; 00264 UC_SRCE_TRACE("-ARM_UCSM_SourceRegistryGetLowestCost: index = %" PRIu32, min_cost_index); 00265 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00266 } 00267 00268 /** 00269 * @brief Find the source of lowest cost and call the corresponding method 00270 * depending on the type of the request. Retry with source of the next 00271 * smallest cost if previous sources failed until the source registry 00272 * is exhausted. 00273 */ 00274 static arm_uc_error_t ARM_UCSM_Get(request_t* req) 00275 { 00276 UC_SRCE_TRACE("+ARM_UCSM_Get"); 00277 if (req->uri != NULL) 00278 { 00279 UC_SRCE_TRACE("with %" PRIx32 ", host [%s], path [%s], type %" PRIu32, 00280 req->uri, req->uri->host,req->uri->path, req->type); 00281 } 00282 else 00283 { 00284 UC_SRCE_TRACE("with NULL, type %" PRIu32, 00285 req->type); 00286 } 00287 00288 uint32_t index = 0; 00289 00290 // get the source of lowest cost 00291 arm_uc_error_t retval = ARM_UCSM_SourceRegistryGetLowestCost(req->uri, 00292 req->type, 00293 req->excludes, 00294 &index); 00295 if (retval.code != SOMA_ERR_NONE) 00296 { 00297 UC_SRCE_TRACE("-ARM_UCSM_Get: error retval.code %" PRIu32, retval.code); 00298 return retval; 00299 } 00300 00301 if ((req->uri == NULL) && (req->type == QUERY_TYPE_MANIFEST_DEFAULT)) 00302 { 00303 UC_SRCE_TRACE("calling source %" PRIu32 " GetManifestDefault", index); 00304 retval = source_registry[index]->GetManifestDefault(req->buffer, req->offset); 00305 } 00306 else if ((req->uri != NULL) && (req->type == QUERY_TYPE_MANIFEST_URL)) 00307 { 00308 UC_SRCE_TRACE("calling source %" PRIu32 " GetManifestURL with %" PRIx32, index, req->uri); 00309 retval = source_registry[index]->GetManifestURL(req->uri, req->buffer, req->offset); 00310 } 00311 else if ((req->uri != NULL) && (req->type == QUERY_TYPE_FIRMWARE)) 00312 { 00313 UC_SRCE_TRACE("calling source %" PRIu32 " GetFirmwareFragment with %" PRIx32, index, req->uri); 00314 retval = source_registry[index]->GetFirmwareFragment(req->uri, req->buffer, req->offset); 00315 } 00316 else if ((req->uri != NULL) && (req->type == QUERY_TYPE_KEYTABLE)) 00317 { 00318 UC_SRCE_TRACE("calling source %" PRIu32 " GetKeytableURL with %" PRIx32, index, req->uri); 00319 retval = source_registry[index]->GetKeytableURL(req->uri, req->buffer); 00320 } 00321 else 00322 { 00323 if (req->uri == NULL ) { 00324 UC_SRCE_TRACE("-ARM_UCSM_Get: Error - Invalid parameter (URI == NULL)"); 00325 } else { 00326 UC_SRCE_TRACE("-ARM_UCSM_Get: Error - Invalid parameter (unknown request type)"); 00327 } 00328 return (arm_uc_error_t){ SOMA_ERR_INVALID_PARAMETER }; 00329 } 00330 00331 if (retval.code == SRCE_ERR_BUSY) 00332 { 00333 UC_SRCE_TRACE("-ARM_UCSM_Get: Error - Busy -> PostCallback AsyncRetryGet"); 00334 ARM_UC_PostCallback(&event_cb_storage, ARM_UCSM_AsyncRetryGet, 0); 00335 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00336 } 00337 else if (retval.error != ERR_NONE) 00338 { 00339 // failure, try source with the next smallest cost 00340 req->excludes[index] = 1; 00341 UC_SRCE_TRACE("-ARM_UCSM_Get: Error - failure (try source with the next smallest cost)"); 00342 return ARM_UCSM_Get(req); 00343 } 00344 00345 // record the index of source handling the get request currently 00346 req->current_source = index; 00347 UC_SRCE_TRACE("-ARM_UCSM_Get: Using source %" PRIu32, index); 00348 00349 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00350 } 00351 00352 /** 00353 * @brief If source is busy ARM_UCSM_AsyncRetryGet is registered with 00354 the event queue so it is called again to retry the same source 00355 */ 00356 static void ARM_UCSM_AsyncRetryGet(uint32_t unused) 00357 { 00358 (void) unused; 00359 00360 UC_SRCE_TRACE("+ARM_UCSM_AsyncRetryGet"); 00361 arm_uc_error_t retval = ARM_UCSM_Get(&request_in_flight); 00362 if (retval.error != ERR_NONE) 00363 { 00364 ARM_UCSM_RequestStructInit(&request_in_flight); 00365 ARM_UC_PostCallback(&event_cb_storage, event_cb, ARM_UC_SM_EVENT_ERROR); 00366 } 00367 UC_SRCE_TRACE("-ARM_UCSM_AsyncRetryGet"); 00368 } 00369 00370 /** 00371 * @brief Translate source event into source manager event 00372 */ 00373 static ARM_UC_SM_Event_t ARM_UCSM_TranslateEvent(uint32_t source_event) 00374 { 00375 ARM_UC_SM_Event_t event = ARM_UC_SM_EVENT_ERROR; 00376 00377 switch(source_event) 00378 { 00379 case EVENT_NOTIFICATION: 00380 event = ARM_UC_SM_EVENT_NOTIFICATION; 00381 break; 00382 case EVENT_MANIFEST: 00383 event = ARM_UC_SM_EVENT_MANIFEST; 00384 break; 00385 case EVENT_FIRMWARE: 00386 event = ARM_UC_SM_EVENT_FIRMWARE; 00387 break; 00388 case EVENT_KEYTABLE: 00389 event = ARM_UC_SM_EVENT_KEYTABLE; 00390 break; 00391 case EVENT_ERROR: 00392 event = ARM_UC_SM_EVENT_ERROR; 00393 break; 00394 case EVENT_ERROR_BUFFER_SIZE: 00395 event = ARM_UC_SM_EVENT_ERROR_BUFFER_SIZE; 00396 break; 00397 } 00398 00399 return event; 00400 } 00401 00402 /** 00403 * @brief Catch callbacks from sources to enable error handling 00404 */ 00405 static void ARM_UCSM_CallbackWrapper(uint32_t source_event) 00406 { 00407 UC_SRCE_TRACE("+ARM_UCSM_CallbackWrapper"); 00408 UC_SRCE_TRACE("source_event == %" PRIu32, source_event); 00409 ARM_UC_SM_Event_t event = ARM_UCSM_TranslateEvent(source_event); 00410 00411 if (event == ARM_UC_SM_EVENT_ERROR && request_in_flight.type != QUERY_TYPE_UNKNOWN) 00412 { 00413 UC_SRCE_TRACE("ARM_UCSM_TranslateEvent event error == %" PRIu32, event); 00414 request_in_flight.excludes[request_in_flight.current_source] = 1; 00415 arm_uc_error_t retval = ARM_UCSM_Get(&request_in_flight); 00416 if (retval.code != SOMA_ERR_NONE) 00417 { 00418 UC_SRCE_TRACE("ARM_UCSM_Get() retval.code == %" PRIu32, retval.code); 00419 ARM_UCSM_RequestStructInit(&request_in_flight); 00420 ARM_UC_PostCallback(&event_cb_storage, event_cb, event); 00421 } 00422 } 00423 else 00424 { 00425 UC_SRCE_TRACE(""); 00426 ARM_UCSM_RequestStructInit(&request_in_flight); 00427 ARM_UC_PostCallback(&event_cb_storage, event_cb, event); 00428 } 00429 UC_SRCE_TRACE("-ARM_UCSM_CallbackWrapper"); 00430 } 00431 00432 /* ==================================================================== * 00433 * Public API * 00434 * ==================================================================== */ 00435 00436 /* further documentation of the API can be found in source_manager.h */ 00437 00438 arm_uc_error_t ARM_UCSM_Initialize(ARM_SOURCE_SignalEvent_t callback) 00439 { 00440 // remember the callback 00441 event_cb = callback; 00442 00443 // init source_registry to NULL 00444 return ARM_UCSM_SourceRegistryInit(); 00445 } 00446 00447 arm_uc_error_t ARM_UCSM_Uninitialize() 00448 { 00449 for (size_t i = 0; i < MAX_SOURCES; i++) 00450 { 00451 if (source_registry[i] != NULL) 00452 { 00453 source_registry[i]->Uninitialize(); 00454 source_registry[i] = NULL; 00455 } 00456 } 00457 return (arm_uc_error_t){ERR_NONE}; 00458 } 00459 00460 arm_uc_error_t ARM_UCSM_AddSource(const ARM_UPDATE_SOURCE* source) 00461 { 00462 if (ARM_UCSM_GetIndexOfSource(source) != MAX_SOURCES) 00463 { 00464 // Source already added, don't add again 00465 // TODO: should this return SOMA_ERR_NONE or a new error 00466 // SOMA_ERR_ALREADY_PRESENT? 00467 return (arm_uc_error_t){ SOMA_ERR_NONE }; 00468 } 00469 source->Initialize(ARM_UCSM_CallbackWrapper); 00470 return ARM_UCSM_SourceRegistryAdd(source); 00471 } 00472 00473 arm_uc_error_t ARM_UCSM_RemoveSource(const ARM_UPDATE_SOURCE* source) 00474 { 00475 arm_uc_error_t err = ARM_UCSM_SourceRegistryRemove(source); 00476 if (err.code == SOMA_ERR_NONE) 00477 { 00478 // Call 'uninitialize' only if the source was found (and removed) 00479 source->Uninitialize(); 00480 } 00481 return err; 00482 } 00483 00484 /* All the `Get` APIs map into `ARM_UCSM_Get` */ 00485 00486 arm_uc_error_t ARM_UCSM_GetManifest(arm_uc_buffer_t* buffer, uint32_t offset) 00487 { 00488 UC_SRCE_TRACE("+ARM_UCSM_GetManifest"); 00489 ARM_UCSM_RequestStructInit(&request_in_flight); 00490 request_in_flight.buffer = buffer; 00491 request_in_flight.offset = offset; 00492 request_in_flight.type = QUERY_TYPE_MANIFEST_DEFAULT; 00493 00494 arm_uc_error_t retval = ARM_UCSM_Get(&request_in_flight); 00495 if (retval.code != SOMA_ERR_NONE) 00496 { 00497 ARM_UCSM_RequestStructInit(&request_in_flight); 00498 } 00499 00500 UC_SRCE_TRACE("-ARM_UCSM_GetManifest"); 00501 return retval; 00502 } 00503 00504 arm_uc_error_t ARM_UCSM_GetManifestFrom(arm_uc_uri_t* uri, 00505 arm_uc_buffer_t* buffer, 00506 uint32_t offset) 00507 { 00508 UC_SRCE_TRACE("+ARM_UCSM_GetManifestFrom"); 00509 ARM_UCSM_RequestStructInit(&request_in_flight); 00510 request_in_flight.uri = uri; 00511 request_in_flight.buffer = buffer; 00512 request_in_flight.offset = offset; 00513 request_in_flight.type = QUERY_TYPE_MANIFEST_URL; 00514 00515 arm_uc_error_t retval = ARM_UCSM_Get(&request_in_flight); 00516 if (retval.code != SOMA_ERR_NONE) 00517 { 00518 ARM_UCSM_RequestStructInit(&request_in_flight); 00519 } 00520 00521 UC_SRCE_TRACE("-ARM_UCSM_GetManifestFrom"); 00522 return retval; 00523 } 00524 00525 arm_uc_error_t ARM_UCSM_GetFirmwareFragment(arm_uc_uri_t* uri, 00526 arm_uc_buffer_t* buffer, 00527 uint32_t offset) 00528 { 00529 UC_SRCE_TRACE("+ARM_UCSM_GetFirmwareFragment"); 00530 ARM_UCSM_RequestStructInit(&request_in_flight); 00531 request_in_flight.uri = uri; 00532 request_in_flight.buffer = buffer; 00533 request_in_flight.offset = offset; 00534 request_in_flight.type = QUERY_TYPE_FIRMWARE; 00535 00536 arm_uc_error_t retval = ARM_UCSM_Get(&request_in_flight); 00537 if (retval.code != SOMA_ERR_NONE) 00538 { 00539 ARM_UCSM_RequestStructInit(&request_in_flight); 00540 } 00541 00542 UC_SRCE_TRACE("-ARM_UCSM_GetFirmwareFragment"); 00543 return retval; 00544 } 00545 00546 arm_uc_error_t ARM_UCSM_GetKeytable(arm_uc_uri_t* uri, arm_uc_buffer_t* buffer) 00547 { 00548 UC_SRCE_TRACE("+ARM_UCSM_GetKeytable"); 00549 ARM_UCSM_RequestStructInit(&request_in_flight); 00550 request_in_flight.uri = uri; 00551 request_in_flight.buffer = buffer; 00552 request_in_flight.type = QUERY_TYPE_KEYTABLE; 00553 00554 arm_uc_error_t retval = ARM_UCSM_Get(&request_in_flight); 00555 if (retval.code != SOMA_ERR_NONE) 00556 { 00557 ARM_UCSM_RequestStructInit(&request_in_flight); 00558 } 00559 00560 UC_SRCE_TRACE("+ARM_UCSM_GetKeytable"); 00561 return retval; 00562 } 00563 00564 ARM_UC_SOURCE_MANAGER_t ARM_UC_SourceManager = { 00565 .Initialize = ARM_UCSM_Initialize, 00566 .Uninitialize = ARM_UCSM_Uninitialize, 00567 .AddSource = ARM_UCSM_AddSource, 00568 .RemoveSource = ARM_UCSM_RemoveSource, 00569 .GetManifest = ARM_UCSM_GetManifest, 00570 .GetManifestFrom = ARM_UCSM_GetManifestFrom, 00571 .GetFirmwareFragment = ARM_UCSM_GetFirmwareFragment, 00572 .GetKeytable = ARM_UCSM_GetKeytable 00573 };
Generated on Tue Jul 12 2022 19:01:33 by 1.7.2