/* * 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 { template class UAVCAN_EXPORT ServiceResponseTransferListenerInstantiationHelper { public: // so much templating it hurts typedef typename TransferListenerInstantiationHelper::Type Type; }; /** * 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. */ template class UAVCAN_EXPORT ServiceCallResult { 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_; } 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 : public ITransferAcceptanceFilter, Noncopyable { const DataTypeDescriptor* data_type_descriptor_; ///< This will be initialized at the time of first call protected: class CallState : DeadlineHandler { ServiceClientBase& owner_; const ServiceCallID id_; virtual void handleDeadline(MonotonicTime); public: CallState(INode& node, ServiceClientBase& owner, ServiceCallID call_id) : DeadlineHandler(node.getScheduler()) , owner_(owner) , id_(call_id) { UAVCAN_ASSERT(id_.isValid()); DeadlineHandler::startWithDelay(owner_.request_timeout_); } bool doesMatch(ServiceCallID call_id) const { return call_id == id_; } 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.doesMatch(id); } }; MonotonicDuration request_timeout_; ServiceClientBase() : data_type_descriptor_(NULL) , request_timeout_(getDefaultRequestTimeout()) { } virtual ~ServiceClientBase() { } int prepareToCall(INode& node, const char* dtname, NodeID server_node_id, ServiceCallID& out_call_id); virtual void handleTimeout(ServiceCallID call_id) = 0; public: /** * It's not recommended to override default timeouts. * Change of this value will not affect pending calls. */ static MonotonicDuration getDefaultRequestTimeout() { return MonotonicDuration::fromMSec(500); } static MonotonicDuration getMinRequestTimeout() { return MonotonicDuration::fromMSec(10); } static MonotonicDuration getMaxRequestTimeout() { return MonotonicDuration::fromMSec(60000); } }; /** * Use this class to invoke services on remote nodes. * * @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 unsigned NumStaticCalls_ = 1 > class UAVCAN_EXPORT ServiceClient : public GenericSubscriber::Type> , public ServiceClientBase { public: typedef DataType_ DataType; typedef typename DataType::Request RequestType; typedef typename DataType::Response ResponseType; typedef ServiceCallResult ServiceCallResultType; typedef Callback_ Callback; enum { NumStaticCalls = NumStaticCalls_ }; private: typedef ServiceClient SelfType; typedef GenericPublisher PublisherType; typedef typename ServiceResponseTransferListenerInstantiationHelper::Type TransferListenerType; typedef GenericSubscriber SubscriberType; #if 0 typedef Multiset CallRegistry; CallRegistry call_registry_; #endif 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 handleTimeout(ServiceCallID call_id); 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) , publisher_(node, getDefaultRequestTimeout()) , callback_(callback) { setRequestTimeout(getDefaultRequestTimeout()); #if UAVCAN_DEBUG UAVCAN_ASSERT(getRequestTimeout() == getDefaultRequestTimeout()); // Making sure default values are OK #endif } virtual ~ServiceClient() { cancelAll(); } /** * Shall be called before first use. * Returns negative error code. */ int init() { return publisher_.init(); } /** * 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 has timed 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. */ int call(NodeID server_node_id, const RequestType& request, ServiceCallID& out_call_id); /** * Cancels certain call referred via call ID structure. */ void cancel(ServiceCallID call_id); /** * Cancels all pending calls. */ void cancelAll(); /** * Service response callback must be set prior service call. */ const Callback& getCallback() const { return callback_; } void setCallback(const Callback& cb) { callback_ = cb; } #if 0 unsigned getNumPendingCalls() const { return call_registry_.getSize(); } #endif #if 0 bool hasPendingCalls() const { return !call_registry_.isEmpty(); } #else bool hasPendingCalls() const { return false; } #endif /** * 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. * 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 } }; // ---------------------------------------------------------------------------- template void ServiceClient::invokeCallback(ServiceCallResultType& result) { if (try_implicit_cast(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 #if 0 return call_registry_.findFirst(CallStateMatchingPredicate(ServiceCallID(frame.getSrcNodeID(), frame.getTransferID()))) != NULL; #else (void)frame; return false; #endif } template void ServiceClient:: handleReceivedDataStruct(ReceivedDataStructure& response) { UAVCAN_ASSERT(response.getTransferType() == TransferTypeServiceResponse); ServiceCallID call_id(response.getSrcNodeID(), response.getTransferID()); cancel(call_id); ServiceCallResultType result(ServiceCallResultType::Success, call_id, response); // Mutable! invokeCallback(result); } template void ServiceClient::handleTimeout(ServiceCallID call_id) { cancel(call_id); ServiceCallResultType result(ServiceCallResultType::ErrorTimeout, call_id, SubscriberType::getReceivedStructStorage()); // Mutable! invokeCallback(result); } template int ServiceClient::addCallState(ServiceCallID call_id) { #if 0 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 (call_registry_.add(CallState(SubscriberType::getNode(), *this, call_id)) == NULL) { SubscriberType::stop(); return -ErrMemory; } return 0; #else (void)call_id; return -ErrNotInited; #endif } 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 (!try_implicit_cast(callback_, true)) { UAVCAN_TRACE("ServiceClient", "Invalid callback"); return -ErrInvalidConfiguration; } /* * Common procedures that don't depend on the struct data type */ TransferID transfer_id; 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) */ TransferListenerType* const tl = SubscriberType::getTransferListener(); if (tl == NULL) { UAVCAN_ASSERT(0); // Must have been created cancel(out_call_id); return -ErrLogic; } tl->installAcceptanceFilter(this); /* * Publishing the request */ const int publisher_res = publisher_.publish(request, TransferTypeServiceRequest, server_node_id, transfer_id); if (publisher_res < 0) { cancel(out_call_id); return publisher_res; } return 0; } template void ServiceClient::cancel(ServiceCallID call_id) { #if 0 call_registry_.remove(call_id); if (call_registry_.isEmpty()) { SubscriberType::stop(); } #else (void)call_id; #endif } template void ServiceClient::cancelAll() { #if 0 call_registry_.removeAll(); #endif SubscriberType::stop(); } } #endif // UAVCAN_NODE_SERVICE_CLIENT_HPP_INCLUDED