/* * Copyright (C) 2014 Pavel Kirienko */ #ifndef UAVCAN_NODE_SERVICE_CLIENT_HPP_INCLUDED #define UAVCAN_NODE_SERVICE_CLIENT_HPP_INCLUDED #include #include #include #include #if !defined(UAVCAN_CPP_VERSION) || !defined(UAVCAN_CPP11) # error UAVCAN_CPP_VERSION #endif #if UAVCAN_CPP_VERSION >= UAVCAN_CPP11 # include #endif namespace uavcan { /** * This struct describes a pending service call. * Refer to @ref ServiceClient to learn more about service calls. */ struct ServiceCallID { NodeID server_node_id; TransferID transfer_id; ServiceCallID() { } ServiceCallID(NodeID arg_server_node_id, TransferID arg_transfer_id) : server_node_id(arg_server_node_id) , transfer_id(arg_transfer_id) { } bool operator==(const ServiceCallID rhs) const { return (rhs.server_node_id == server_node_id) && (rhs.transfer_id == transfer_id); } bool isValid() const { return server_node_id.isUnicast(); } }; /** * Object of this type will be returned to the application as a result of service call. * Note that application ALWAYS gets this result, even when it times out or fails because of some other reason. * The class is made noncopyable because it keeps a reference to a stack-allocated object. */ template class UAVCAN_EXPORT ServiceCallResult : Noncopyable { public: typedef ReceivedDataStructure ResponseFieldType; enum Status { Success, ErrorTimeout }; private: const Status status_; ///< Whether successful or not. Failure to decode the response causes timeout. ServiceCallID call_id_; ///< Identifies the call ResponseFieldType& response_; ///< Returned data structure. Value undefined if the service call has failed. public: ServiceCallResult(Status arg_status, ServiceCallID arg_call_id, ResponseFieldType& arg_response) : status_(arg_status) , call_id_(arg_call_id) , response_(arg_response) { UAVCAN_ASSERT(call_id_.isValid()); UAVCAN_ASSERT((status_ == Success) || (status_ == ErrorTimeout)); } /** * Shortcut to quickly check whether the call was successful. */ bool isSuccessful() const { return status_ == Success; } Status getStatus() const { return status_; } ServiceCallID getCallID() const { return call_id_; } /** * Returned reference points to a stack-allocated object. */ const ResponseFieldType& getResponse() const { return response_; } ResponseFieldType& getResponse() { return response_; } }; /** * This operator neatly prints the service call result prepended with extra data like Server Node ID. * The extra data will be represented as YAML comment. */ template static Stream& operator<<(Stream& s, const ServiceCallResult& scr) { s << "# Service call result [" << DataType::getDataTypeFullName() << "] " << (scr.isSuccessful() ? "OK" : "FAILURE") << " server_node_id=" << int(scr.getCallID().server_node_id.get()) << " tid=" << int(scr.getCallID().transfer_id.get()) << "\n"; if (scr.isSuccessful()) { s << scr.getResponse(); } else { s << "# (no data)"; } return s; } /** * Do not use directly. */ class ServiceClientBase : protected ITransferAcceptanceFilter , protected DeadlineHandler { const DataTypeDescriptor* data_type_descriptor_; ///< This will be initialized at the time of first call protected: class CallState : DeadlineHandler { ServiceClientBase& owner_; const ServiceCallID id_; bool timed_out_; virtual void handleDeadline(MonotonicTime); public: CallState(INode& node, ServiceClientBase& owner, ServiceCallID call_id) : DeadlineHandler(node.getScheduler()) , owner_(owner) , id_(call_id) , timed_out_(false) { UAVCAN_ASSERT(id_.isValid()); DeadlineHandler::startWithDelay(owner_.request_timeout_); } ServiceCallID getCallID() const { return id_; } bool hasTimedOut() const { return timed_out_; } static bool hasTimedOutPredicate(const CallState& cs) { return cs.hasTimedOut(); } bool operator==(const CallState& rhs) const { return (&owner_ == &rhs.owner_) && (id_ == rhs.id_); } }; struct CallStateMatchingPredicate { const ServiceCallID id; CallStateMatchingPredicate(ServiceCallID reference) : id(reference) { } bool operator()(const CallState& state) const { return (state.getCallID() == id) && !state.hasTimedOut(); } }; struct ServerSearchPredicate { const NodeID server_node_id; ServerSearchPredicate(NodeID nid) : server_node_id(nid) { } bool operator()(const CallState& state) const { return state.getCallID().server_node_id == server_node_id; } }; MonotonicDuration request_timeout_; ServiceClientBase(INode& node) : DeadlineHandler(node.getScheduler()) , data_type_descriptor_(UAVCAN_NULLPTR) , request_timeout_(getDefaultRequestTimeout()) { } virtual ~ServiceClientBase() { } int prepareToCall(INode& node, const char* dtname, NodeID server_node_id, ServiceCallID& out_call_id); public: /** * It's not recommended to override default timeouts. * Change of this value will not affect pending calls. */ static MonotonicDuration getDefaultRequestTimeout() { return MonotonicDuration::fromMSec(1000); } static MonotonicDuration getMinRequestTimeout() { return MonotonicDuration::fromMSec(10); } static MonotonicDuration getMaxRequestTimeout() { return MonotonicDuration::fromMSec(60000); } }; /** * Use this class to invoke services on remote nodes. * * This class can manage multiple concurrent calls to the same or different remote servers. Number of concurrent * calls is limited only by amount of available pool memory. * * Note that the reference passed to the callback points to a stack-allocated object, which means that the * reference invalidates once the callback returns. If you want to use this object after the callback execution, * you need to copy it somewhere. * * Note that by default, service client objects use lower priority than message publishers. Use @ref setPriority() * to override the default if necessary. * * @tparam DataType_ Service data type. * * @tparam Callback_ Service response will be delivered through the callback of this type. * In C++11 mode this type defaults to std::function<>. * In C++03 mode this type defaults to a plain function pointer; use binder to * call member functions as callbacks. */ template = UAVCAN_CPP11 typename Callback_ = std::function&)> #else typename Callback_ = void (*)(const ServiceCallResult&) #endif > class UAVCAN_EXPORT ServiceClient : public GenericSubscriber , public ServiceClientBase { public: typedef DataType_ DataType; typedef typename DataType::Request RequestType; typedef typename DataType::Response ResponseType; typedef ServiceCallResult ServiceCallResultType; typedef Callback_ Callback; private: typedef ServiceClient SelfType; typedef GenericPublisher PublisherType; typedef GenericSubscriber SubscriberType; typedef Multiset CallRegistry; struct TimeoutCallbackCaller { ServiceClient& owner; TimeoutCallbackCaller(ServiceClient& arg_owner) : owner(arg_owner) { } void operator()(const CallState& state) { if (state.hasTimedOut()) { UAVCAN_TRACE("ServiceClient::TimeoutCallbackCaller", "Timeout from nid=%d, tid=%d, dtname=%s", int(state.getCallID().server_node_id.get()), int(state.getCallID().transfer_id.get()), DataType::getDataTypeFullName()); typename SubscriberType::ReceivedDataStructureSpec rx_struct; // Default-initialized ServiceCallResultType result(ServiceCallResultType::ErrorTimeout, state.getCallID(), rx_struct); // Mutable! owner.invokeCallback(result); } } }; CallRegistry call_registry_; PublisherType publisher_; Callback callback_; virtual bool shouldAcceptFrame(const RxFrame& frame) const; // Called from the transfer listener void invokeCallback(ServiceCallResultType& result); virtual void handleReceivedDataStruct(ReceivedDataStructure& response); virtual void handleDeadline(MonotonicTime); int addCallState(ServiceCallID call_id); public: /** * @param node Node instance this client will be registered with. * @param callback Callback instance. Optional, can be assigned later. */ explicit ServiceClient(INode& node, const Callback& callback = Callback()) : SubscriberType(node) , ServiceClientBase(node) , call_registry_(node.getAllocator()) , publisher_(node, getDefaultRequestTimeout()) , callback_(callback) { setPriority(TransferPriority::MiddleLower); setRequestTimeout(getDefaultRequestTimeout()); #if UAVCAN_DEBUG UAVCAN_ASSERT(getRequestTimeout() == getDefaultRequestTimeout()); // Making sure default values are OK #endif } virtual ~ServiceClient() { cancelAllCalls(); } /** * Shall be called before first use. * Returns negative error code. */ int init() { return publisher_.init(); } /** * Shall be called before first use. * This overload allows to set the priority, otherwise it's the same. * Returns negative error code. */ int init(TransferPriority priority) { return publisher_.init(priority); } /** * Performs non-blocking service call. * This method transmits the service request and returns immediately. * * Service response will be delivered into the application via the callback. * Note that the callback will ALWAYS be called even if the service call times out; the * actual result of the call (success/failure) will be passed to the callback as well. * * Returns negative error code. */ int call(NodeID server_node_id, const RequestType& request); /** * Same as plain @ref call() above, but this overload also returns the call ID of the new call. * The call ID structure can be used to cancel this request later if needed. */ int call(NodeID server_node_id, const RequestType& request, ServiceCallID& out_call_id); /** * Cancels certain call referred via call ID structure. */ void cancelCall(ServiceCallID call_id); /** * Cancels all pending calls. */ void cancelAllCalls(); /** * Checks whether there's currently a pending call addressed to the specified node ID. */ bool hasPendingCallToServer(NodeID server_node_id) const; /** * This method allows to traverse pending calls. If the index is out of range, an invalid call ID will be returned. * Warning: average complexity is O(index); worst case complexity is O(size). */ ServiceCallID getCallIDByIndex(unsigned index) const; /** * Service response callback must be set prior service call. */ const Callback& getCallback() const { return callback_; } void setCallback(const Callback& cb) { callback_ = cb; } /** * Complexity is O(N) of number of pending calls. * Note that the number of pending calls will not be updated until the callback is executed. */ unsigned getNumPendingCalls() const { return call_registry_.getSize(); } /** * Complexity is O(1). * Note that the number of pending calls will not be updated until the callback is executed. */ bool hasPendingCalls() const { return !call_registry_.isEmpty(); } /** * Returns the number of failed attempts to decode received response. Generally, a failed attempt means either: * - Transient failure in the transport layer. * - Incompatible data types. */ uint32_t getResponseFailureCount() const { return SubscriberType::getFailureCount(); } /** * Request timeouts. Note that changing the request timeout will not affect calls that are already pending. * There is no such config as TX timeout - TX timeouts are configured automagically according to request timeouts. * Not recommended to change. */ MonotonicDuration getRequestTimeout() const { return request_timeout_; } void setRequestTimeout(MonotonicDuration timeout) { timeout = max(timeout, getMinRequestTimeout()); timeout = min(timeout, getMaxRequestTimeout()); publisher_.setTxTimeout(timeout); request_timeout_ = max(timeout, publisher_.getTxTimeout()); // No less than TX timeout } /** * Priority of outgoing request transfers. * The remote server is supposed to use the same priority for the response, but it's not guaranteed by * the specification. */ TransferPriority getPriority() const { return publisher_.getPriority(); } void setPriority(const TransferPriority prio) { publisher_.setPriority(prio); } }; // ---------------------------------------------------------------------------- template void ServiceClient::invokeCallback(ServiceCallResultType& result) { if (coerceOrFallback(callback_, true)) { callback_(result); } else { handleFatalError("Srv client clbk"); } } template bool ServiceClient::shouldAcceptFrame(const RxFrame& frame) const { UAVCAN_ASSERT(frame.getTransferType() == TransferTypeServiceResponse); // Other types filtered out by dispatcher return UAVCAN_NULLPTR != call_registry_.find(CallStateMatchingPredicate(ServiceCallID(frame.getSrcNodeID(), frame.getTransferID()))); } template void ServiceClient::handleReceivedDataStruct(ReceivedDataStructure& response) { UAVCAN_ASSERT(response.getTransferType() == TransferTypeServiceResponse); ServiceCallID call_id(response.getSrcNodeID(), response.getTransferID()); cancelCall(call_id); ServiceCallResultType result(ServiceCallResultType::Success, call_id, response); // Mutable! invokeCallback(result); } template void ServiceClient::handleDeadline(MonotonicTime) { UAVCAN_TRACE("ServiceClient", "Shared deadline event received"); /* * Invoking callbacks for timed out call state objects. */ TimeoutCallbackCaller callback_caller(*this); call_registry_.template forEach(callback_caller); /* * Removing timed out objects. * This operation cannot be merged with the previous one because that will not work with recursive calls. */ call_registry_.removeAllWhere(&CallState::hasTimedOutPredicate); /* * Subscriber does not need to be registered if we don't have any pending calls. * Removing it makes processing of incoming frames a bit faster. */ if (call_registry_.isEmpty()) { SubscriberType::stop(); } } template int ServiceClient::addCallState(ServiceCallID call_id) { if (call_registry_.isEmpty()) { const int subscriber_res = SubscriberType::startAsServiceResponseListener(); if (subscriber_res < 0) { UAVCAN_TRACE("ServiceClient", "Failed to start the subscriber, error: %i", subscriber_res); return subscriber_res; } } if (UAVCAN_NULLPTR == call_registry_.template emplace(SubscriberType::getNode(), *this, call_id)) { SubscriberType::stop(); return -ErrMemory; } return 0; } template int ServiceClient::call(NodeID server_node_id, const RequestType& request) { ServiceCallID dummy; return call(server_node_id, request, dummy); } template int ServiceClient::call(NodeID server_node_id, const RequestType& request, ServiceCallID& out_call_id) { if (!coerceOrFallback(callback_, true)) { UAVCAN_TRACE("ServiceClient", "Invalid callback"); return -ErrInvalidConfiguration; } /* * Common procedures that don't depend on the struct data type */ const int prep_res = prepareToCall(SubscriberType::getNode(), DataType::getDataTypeFullName(), server_node_id, out_call_id); if (prep_res < 0) { UAVCAN_TRACE("ServiceClient", "Failed to prepare the call, error: %i", prep_res); return prep_res; } /* * Initializing the call state - this will start the subscriber ad-hoc */ const int call_state_res = addCallState(out_call_id); if (call_state_res < 0) { UAVCAN_TRACE("ServiceClient", "Failed to add call state, error: %i", call_state_res); return call_state_res; } /* * Configuring the listener so it will accept only the matching responses * TODO move to init(), but this requires to somewhat refactor GenericSubscriber<> (remove TransferForwarder) */ TransferListenerWithFilter* const tl = SubscriberType::getTransferListener(); if (tl == UAVCAN_NULLPTR) { UAVCAN_ASSERT(0); // Must have been created cancelCall(out_call_id); return -ErrLogic; } tl->installAcceptanceFilter(this); /* * Publishing the request */ const int publisher_res = publisher_.publish(request, TransferTypeServiceRequest, server_node_id, out_call_id.transfer_id); if (publisher_res < 0) { cancelCall(out_call_id); return publisher_res; } UAVCAN_ASSERT(server_node_id == out_call_id.server_node_id); return publisher_res; } template void ServiceClient::cancelCall(ServiceCallID call_id) { call_registry_.removeFirstWhere(CallStateMatchingPredicate(call_id)); if (call_registry_.isEmpty()) { SubscriberType::stop(); } } template void ServiceClient::cancelAllCalls() { call_registry_.clear(); SubscriberType::stop(); } template bool ServiceClient::hasPendingCallToServer(NodeID server_node_id) const { return UAVCAN_NULLPTR != call_registry_.find(ServerSearchPredicate(server_node_id)); } template ServiceCallID ServiceClient::getCallIDByIndex(unsigned index) const { const CallState* const id = call_registry_.getByIndex(index); return (id == UAVCAN_NULLPTR) ? ServiceCallID() : id->getCallID(); } } #endif // UAVCAN_NODE_SERVICE_CLIENT_HPP_INCLUDED