libuav original
Dependents: UAVCAN UAVCAN_Subscriber
firmware_update_trigger.hpp
00001 /* 00002 * Copyright (C) 2015 Pavel Kirienko <pavel.kirienko@gmail.com> 00003 */ 00004 00005 #ifndef UAVCAN_PROTOCOL_FIRMWARE_UPDATE_TRIGGER_HPP_INCLUDED 00006 #define UAVCAN_PROTOCOL_FIRMWARE_UPDATE_TRIGGER_HPP_INCLUDED 00007 00008 #include <uavcan/build_config.hpp> 00009 #include <uavcan/debug.hpp> 00010 #include <uavcan/node/service_client.hpp> 00011 #include <uavcan/util/method_binder.hpp> 00012 #include <uavcan/util/map.hpp> 00013 #include <uavcan/protocol/node_info_retriever.hpp> 00014 // UAVCAN types 00015 #include <uavcan/protocol/file/BeginFirmwareUpdate.hpp> 00016 00017 namespace uavcan 00018 { 00019 /** 00020 * Application-specific firmware version checking logic. 00021 * Refer to @ref FirmwareUpdateTrigger for details. 00022 */ 00023 class IFirmwareVersionChecker 00024 { 00025 public: 00026 /** 00027 * This value is limited by the pool block size minus some extra data. Please refer to the Map<> documentation 00028 * for details. If this size is set too high, the compilation will fail in the Map<> template. 00029 */ 00030 enum { MaxFirmwareFilePathLength = 40 }; 00031 00032 /** 00033 * This type is used to store path to firmware file that the target node will retrieve using the 00034 * service uavcan.protocol.file.Read. Note that the maximum length is limited due to some specifics of 00035 * libuavcan (@ref MaxFirmwareFilePathLength), this is NOT a protocol-level limitation. 00036 */ 00037 typedef MakeString<MaxFirmwareFilePathLength>::Type FirmwareFilePath; 00038 00039 /** 00040 * This method will be invoked when the class obtains a response to GetNodeInfo request. 00041 * 00042 * @param node_id Node ID that this GetNodeInfo response was received from. 00043 * 00044 * @param node_info Actual node info structure; refer to uavcan.protocol.GetNodeInfo for details. 00045 * 00046 * @param out_firmware_file_path The implementation should return the firmware image path via this argument. 00047 * Note that this path must be reachable via uavcan.protocol.file.Read service. 00048 * Refer to @ref FileServer and @ref BasicFileServer for details. 00049 * 00050 * @return True - the class will begin sending update requests. 00051 * False - the node will be ignored, no request will be sent. 00052 */ 00053 virtual bool shouldRequestFirmwareUpdate(NodeID node_id, const protocol::GetNodeInfo::Response& node_info, 00054 FirmwareFilePath& out_firmware_file_path) = 0; 00055 00056 /** 00057 * This method will be invoked when a node responds to the update request with an error. If the request simply 00058 * times out, this method will not be invoked. 00059 * Note that if by the time of arrival of the response the node is already removed, this method will not be called. 00060 * 00061 * SPECIAL CASE: If the node responds with ERROR_IN_PROGRESS, the class will assume that further requesting 00062 * is not needed anymore. This method will not be invoked. 00063 * 00064 * @param node_id Node ID that returned this error. 00065 * 00066 * @param error_response Contents of the error response. It contains error code and text. 00067 * 00068 * @param out_firmware_file_path New firmware path if a retry is needed. Note that this argument will be 00069 * initialized with old path, so if the same path needs to be reused, this 00070 * argument should be left unchanged. 00071 * 00072 * @return True - the class will continue sending update requests with new firmware path. 00073 * False - the node will be forgotten, new requests will not be sent. 00074 */ 00075 virtual bool shouldRetryFirmwareUpdate(NodeID node_id, 00076 const protocol::file::BeginFirmwareUpdate::Response& error_response, 00077 FirmwareFilePath& out_firmware_file_path) = 0; 00078 00079 /** 00080 * This node is invoked when the node responds to the update request with confirmation. 00081 * Note that if by the time of arrival of the response the node is already removed, this method will not be called. 00082 * 00083 * Implementation is optional; default one does nothing. 00084 * 00085 * @param node_id Node ID that confirmed the request. 00086 * 00087 * @param response Actual response. 00088 */ 00089 virtual void handleFirmwareUpdateConfirmation(NodeID node_id, 00090 const protocol::file::BeginFirmwareUpdate::Response& response) 00091 { 00092 (void)node_id; 00093 (void)response; 00094 } 00095 00096 virtual ~IFirmwareVersionChecker() { } 00097 }; 00098 00099 /** 00100 * This class subscribes to updates from @ref NodeInfoRetriever in order to detect nodes that need firmware 00101 * updates. The decision process of whether a firmware update is needed is relayed to the application via 00102 * @ref IFirmwareVersionChecker. If the application confirms that the update is needed, this class will begin 00103 * sending uavcan.protocol.file.BeginFirmwareUpdate periodically (period is configurable) to every node that 00104 * needs an update in a round-robin fashion. There are the following termination conditions for the periodical 00105 * sending process: 00106 * 00107 * - The node responds with confirmation. In this case the class will forget about the node on the assumption 00108 * that its job is done here. Confirmation will be reported to the application via the interface. 00109 * 00110 * - The node responds with an error, and the error code is not ERROR_IN_PROGRESS. In this case the class will 00111 * request the application via the interface mentioned above about its further actions - either give up or 00112 * retry, possibly with a different firmware. 00113 * 00114 * - The node responds with error ERROR_IN_PROGRESS. In this case the class behaves exactly in the same way as if 00115 * response was successful (because the firmware is alredy being updated, so the goal is fulfilled). 00116 * Confirmation will be reported to the application via the interface. 00117 * 00118 * - The node goes offline or restarts. In this case the node will be immediately forgotten, and the process 00119 * will repeat again later because the node info retriever re-queries GetNodeInfo every time when a node restarts. 00120 * 00121 * Since the target node (i.e. node that is being updated) will try to retrieve the specified firmware file using 00122 * the file services (uavcan.protocol.file.*), the provided firmware path must be reachable for the file server 00123 * (@ref FileServer, @ref BasicFileServer). Normally, an application that serves as UAVCAN firmware update server 00124 * will include at least the following components: 00125 * - this firmware update trigger; 00126 * - dynamic node ID allocation server; 00127 * - file server. 00128 * 00129 * Implementation details: the class uses memory pool to keep the list of nodes that have not responded yet, which 00130 * limits the maximum length of the path to the firmware file, which is covered in @ref IFirmwareVersionChecker. 00131 * To somewhat relieve the maximum path length limitation, the class can be supplied with a common prefix that 00132 * will be prepended to firmware pathes before sending requests. 00133 * Interval at which requests are being sent is configurable, but the default value should cover the needs of 00134 * virtually all use cases (as always). 00135 */ 00136 class FirmwareUpdateTrigger : public INodeInfoListener, 00137 private TimerBase 00138 { 00139 typedef MethodBinder<FirmwareUpdateTrigger*, 00140 void (FirmwareUpdateTrigger::*)(const ServiceCallResult<protocol::file::BeginFirmwareUpdate>&)> 00141 BeginFirmwareUpdateResponseCallback; 00142 00143 typedef IFirmwareVersionChecker::FirmwareFilePath FirmwareFilePath; 00144 00145 enum { DefaultRequestIntervalMs = 1000 }; ///< Shall not be less than default service response timeout. 00146 00147 struct NextNodeIDSearchPredicate : ::uavcan::Noncopyable 00148 { 00149 enum { DefaultOutput = 0xFFU }; 00150 00151 const FirmwareUpdateTrigger& owner; 00152 uint8_t output; 00153 00154 NextNodeIDSearchPredicate(const FirmwareUpdateTrigger& arg_owner) 00155 : owner(arg_owner) 00156 , output(DefaultOutput) 00157 { } 00158 00159 bool operator()(const NodeID node_id, const FirmwareFilePath&) 00160 { 00161 if (node_id.get() > owner.last_queried_node_id_ && 00162 !owner.begin_fw_update_client_.hasPendingCallToServer(node_id)) 00163 { 00164 output = min(output, node_id.get()); 00165 } 00166 return false; 00167 } 00168 }; 00169 00170 /* 00171 * State 00172 */ 00173 ServiceClient<protocol::file::BeginFirmwareUpdate, BeginFirmwareUpdateResponseCallback> begin_fw_update_client_; 00174 00175 IFirmwareVersionChecker& checker_; 00176 00177 NodeInfoRetriever* node_info_retriever_; 00178 00179 Map<NodeID, FirmwareFilePath> pending_nodes_; 00180 00181 MonotonicDuration request_interval_; 00182 00183 FirmwareFilePath common_path_prefix_; 00184 00185 mutable uint8_t last_queried_node_id_; 00186 00187 /* 00188 * Methods of INodeInfoListener 00189 */ 00190 virtual void handleNodeInfoUnavailable(NodeID node_id) 00191 { 00192 UAVCAN_TRACE("FirmwareUpdateTrigger", "Node ID %d could not provide GetNodeInfo response", int(node_id.get())); 00193 pending_nodes_.remove(node_id); // For extra paranoia 00194 } 00195 00196 virtual void handleNodeInfoRetrieved(const NodeID node_id, const protocol::GetNodeInfo::Response& node_info) 00197 { 00198 FirmwareFilePath firmware_file_path; 00199 const bool update_needed = checker_.shouldRequestFirmwareUpdate(node_id, node_info, firmware_file_path); 00200 if (update_needed) 00201 { 00202 UAVCAN_TRACE("FirmwareUpdateTrigger", "Node ID %d requires update; file path: %s", 00203 int(node_id.get()), firmware_file_path.c_str()); 00204 trySetPendingNode(node_id, firmware_file_path); 00205 } 00206 else 00207 { 00208 UAVCAN_TRACE("FirmwareUpdateTrigger", "Node ID %d does not need update", int(node_id.get())); 00209 pending_nodes_.remove(node_id); 00210 } 00211 } 00212 00213 virtual void handleNodeStatusChange(const NodeStatusMonitor::NodeStatusChangeEvent& event) 00214 { 00215 if (event.status.mode == protocol::NodeStatus::MODE_OFFLINE) 00216 { 00217 pending_nodes_.remove(event.node_id); 00218 UAVCAN_TRACE("FirmwareUpdateTrigger", "Node ID %d is offline hence forgotten", int(event.node_id.get())); 00219 } 00220 } 00221 00222 /* 00223 * Own methods 00224 */ 00225 INode& getNode() { return begin_fw_update_client_.getNode(); } 00226 00227 void trySetPendingNode(const NodeID node_id, const FirmwareFilePath& path) 00228 { 00229 if (UAVCAN_NULLPTR != pending_nodes_.insert(node_id, path)) 00230 { 00231 if (!TimerBase::isRunning()) 00232 { 00233 TimerBase::startPeriodic(request_interval_); 00234 UAVCAN_TRACE("FirmwareUpdateTrigger", "Timer started"); 00235 } 00236 } 00237 else 00238 { 00239 getNode().registerInternalFailure("FirmwareUpdateTrigger OOM"); 00240 } 00241 } 00242 00243 NodeID pickNextNodeID() const 00244 { 00245 // We can't do index search because indices are unstable in Map<> 00246 // First try - from the current node up 00247 NextNodeIDSearchPredicate s1(*this); 00248 (void)pending_nodes_.find<NextNodeIDSearchPredicate&>(s1); 00249 00250 if (s1.output != NextNodeIDSearchPredicate::DefaultOutput) 00251 { 00252 last_queried_node_id_ = s1.output; 00253 } 00254 else if (last_queried_node_id_ != 0) 00255 { 00256 // Nothing was found, resetting the selector and trying again 00257 UAVCAN_TRACE("FirmwareUpdateTrigger", "Node selector reset, last value: %d", int(last_queried_node_id_)); 00258 last_queried_node_id_ = 0; 00259 00260 NextNodeIDSearchPredicate s2(*this); 00261 (void)pending_nodes_.find<NextNodeIDSearchPredicate&>(s2); 00262 00263 if (s2.output != NextNodeIDSearchPredicate::DefaultOutput) 00264 { 00265 last_queried_node_id_ = s2.output; 00266 } 00267 } 00268 else 00269 { 00270 ; // Hopeless 00271 } 00272 UAVCAN_TRACE("FirmwareUpdateTrigger", "Next node ID to query: %d, pending nodes: %u, pending calls: %u", 00273 int(last_queried_node_id_), pending_nodes_.getSize(), 00274 begin_fw_update_client_.getNumPendingCalls()); 00275 return last_queried_node_id_; 00276 } 00277 00278 void handleBeginFirmwareUpdateResponse(const ServiceCallResult<protocol::file::BeginFirmwareUpdate>& result) 00279 { 00280 if (!result.isSuccessful()) 00281 { 00282 UAVCAN_TRACE("FirmwareUpdateTrigger", "Request to %d has timed out, will retry", 00283 int(result.getCallID().server_node_id.get())); 00284 return; 00285 } 00286 00287 FirmwareFilePath* const old_path = pending_nodes_.access(result.getCallID().server_node_id); 00288 if (old_path == UAVCAN_NULLPTR) 00289 { 00290 // The entry has been removed, assuming that it's not needed anymore 00291 return; 00292 } 00293 00294 const bool confirmed = 00295 result.getResponse().error == protocol::file::BeginFirmwareUpdate::Response::ERROR_OK || 00296 result.getResponse().error == protocol::file::BeginFirmwareUpdate::Response::ERROR_IN_PROGRESS; 00297 00298 if (confirmed) 00299 { 00300 UAVCAN_TRACE("FirmwareUpdateTrigger", "Node %d confirmed the update request", 00301 int(result.getCallID().server_node_id.get())); 00302 pending_nodes_.remove(result.getCallID().server_node_id); 00303 checker_.handleFirmwareUpdateConfirmation(result.getCallID().server_node_id, result.getResponse()); 00304 } 00305 else 00306 { 00307 UAVCAN_ASSERT(old_path != UAVCAN_NULLPTR); 00308 UAVCAN_ASSERT(TimerBase::isRunning()); 00309 // We won't have to call trySetPendingNode(), because we'll directly update the old path via the pointer 00310 00311 const bool update_needed = 00312 checker_.shouldRetryFirmwareUpdate(result.getCallID().server_node_id, result.getResponse(), *old_path); 00313 00314 if (!update_needed) 00315 { 00316 UAVCAN_TRACE("FirmwareUpdateTrigger", "Node %d does not need retry", 00317 int(result.getCallID().server_node_id.get())); 00318 pending_nodes_.remove(result.getCallID().server_node_id); 00319 } 00320 } 00321 } 00322 00323 virtual void handleTimerEvent(const TimerEvent&) 00324 { 00325 if (pending_nodes_.isEmpty()) 00326 { 00327 TimerBase::stop(); 00328 UAVCAN_TRACE("FirmwareUpdateTrigger", "Timer stopped"); 00329 return; 00330 } 00331 00332 const NodeID node_id = pickNextNodeID(); 00333 if (!node_id.isUnicast()) 00334 { 00335 return; 00336 } 00337 00338 FirmwareFilePath* const path = pending_nodes_.access(node_id); 00339 if (path == UAVCAN_NULLPTR) 00340 { 00341 UAVCAN_ASSERT(0); // pickNextNodeID() returned a node ID that is not present in the map 00342 return; 00343 } 00344 00345 protocol::file::BeginFirmwareUpdate::Request req; 00346 00347 req.source_node_id = getNode().getNodeID().get(); 00348 if (!common_path_prefix_.empty()) 00349 { 00350 req.image_file_remote_path.path += common_path_prefix_.c_str(); 00351 req.image_file_remote_path.path.push_back(protocol::file::Path::SEPARATOR); 00352 } 00353 req.image_file_remote_path.path += path->c_str(); 00354 00355 UAVCAN_TRACE("FirmwareUpdateTrigger", "Request to %d with path: %s", 00356 int(node_id.get()), req.image_file_remote_path.path.c_str()); 00357 00358 const int call_res = begin_fw_update_client_.call(node_id, req); 00359 if (call_res < 0) 00360 { 00361 getNode().registerInternalFailure("FirmwareUpdateTrigger call"); 00362 } 00363 } 00364 00365 public: 00366 FirmwareUpdateTrigger(INode& node, IFirmwareVersionChecker& checker) 00367 : TimerBase(node) 00368 , begin_fw_update_client_(node) 00369 , checker_(checker) 00370 , node_info_retriever_(UAVCAN_NULLPTR) 00371 , pending_nodes_(node.getAllocator()) 00372 , request_interval_(MonotonicDuration::fromMSec(DefaultRequestIntervalMs)) 00373 , last_queried_node_id_(0) 00374 { } 00375 00376 ~FirmwareUpdateTrigger() 00377 { 00378 if (node_info_retriever_ != UAVCAN_NULLPTR) 00379 { 00380 node_info_retriever_->removeListener(this); 00381 } 00382 } 00383 00384 /** 00385 * Starts the object. Once started, it can't be stopped unless destroyed. 00386 * 00387 * @param node_info_retriever The object will register itself against this retriever. 00388 * When the destructor is called, the object will unregister itself. 00389 * 00390 * @param common_path_prefix If set, this path will be prefixed to all firmware pathes provided by the 00391 * application interface. The prefix does not need to end with path separator; 00392 * the last trailing one will be removed (so use '//' if you need '/'). 00393 * By default the prefix is empty. 00394 * 00395 * @return Negative error code. 00396 */ 00397 int start(NodeInfoRetriever& node_info_retriever, 00398 const FirmwareFilePath& arg_common_path_prefix = FirmwareFilePath(), 00399 const TransferPriority priority = TransferPriority::OneHigherThanLowest) 00400 { 00401 /* 00402 * Configuring the node info retriever 00403 */ 00404 node_info_retriever_ = &node_info_retriever; 00405 00406 int res = node_info_retriever_->addListener(this); 00407 if (res < 0) 00408 { 00409 return res; 00410 } 00411 00412 /* 00413 * Initializing the prefix, removing only the last trailing path separator. 00414 */ 00415 common_path_prefix_ = arg_common_path_prefix; 00416 00417 if (!common_path_prefix_.empty() && 00418 *(common_path_prefix_.end() - 1) == protocol::file::Path::SEPARATOR) 00419 { 00420 common_path_prefix_.resize(uint8_t(common_path_prefix_.size() - 1U)); 00421 } 00422 00423 /* 00424 * Initializing the client 00425 */ 00426 res = begin_fw_update_client_.init(priority); 00427 if (res < 0) 00428 { 00429 return res; 00430 } 00431 begin_fw_update_client_.setCallback( 00432 BeginFirmwareUpdateResponseCallback(this, &FirmwareUpdateTrigger::handleBeginFirmwareUpdateResponse)); 00433 00434 // The timer will be started ad-hoc 00435 return 0; 00436 } 00437 00438 /** 00439 * Interval at which uavcan.protocol.file.BeginFirmwareUpdate requests are being sent. 00440 * Note that default value should be OK for any use case. 00441 */ 00442 MonotonicDuration getRequestInterval() const { return request_interval_; } 00443 void setRequestInterval(const MonotonicDuration interval) 00444 { 00445 if (interval.isPositive()) 00446 { 00447 request_interval_ = interval; 00448 if (TimerBase::isRunning()) // Restarting with new interval 00449 { 00450 TimerBase::startPeriodic(request_interval_); 00451 } 00452 } 00453 else 00454 { 00455 UAVCAN_ASSERT(0); 00456 } 00457 } 00458 00459 /** 00460 * This method is mostly needed for testing. 00461 * When triggering is not in progress, the class consumes zero CPU time. 00462 */ 00463 bool isTimerRunning() const { return TimerBase::isRunning(); } 00464 00465 unsigned getNumPendingNodes() const 00466 { 00467 const unsigned ret = pending_nodes_.getSize(); 00468 UAVCAN_ASSERT((ret > 0) ? isTimerRunning() : true); 00469 return ret; 00470 } 00471 00472 }; 00473 00474 } 00475 00476 #endif // Include guard
Generated on Tue Jul 12 2022 17:17:31 by 1.7.2