/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce::midi_ci { class Device::Impl : private SubscriptionManagerDelegate { template static auto getProfileHostImpl (This& t) { return t.profileHost.has_value() ? &*t.profileHost : nullptr; } template static auto getPropertyHostImpl (This& t) { return t.propertyHost.has_value() ? &*t.propertyHost : nullptr; } public: explicit Impl (const Options& opt) : options (getValidated (opt)), muid (getReallyRandomMuid()) { if (options.getFeatures().isProfileConfigurationSupported()) profileHost.emplace (options.getFunctionBlock(), profileDelegate, concreteBufferOutput); if (options.getFeatures().isPropertyExchangeSupported()) propertyHost.emplace (options.getFunctionBlock(), propertyDelegate, concreteBufferOutput, cacheProvider); outgoing.reserve (options.getMaxSysExSize()); } ~Impl() override { if (concreteBufferOutput.hasSentMuid()) { detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, MUID::getBroadcast(), ChannelInGroup::wholeBlock, Message::InvalidateMUID { muid }); } } void sendDiscovery() { { const auto aboutToRemove = std::move (discovered); for (const auto& pair : aboutToRemove) listeners.call ([&] (auto& l) { l.deviceRemoved (pair.first); }); } const Message::Header header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, muid, MUID::getBroadcast(), }; jassert (options.getOutputs().size() < 128); for (size_t i = 0; i < options.getOutputs().size(); ++i) { const Message::Discovery discovery { options.getDeviceInfo(), options.getFeatures().getSupportedCapabilities(), uint32_t (options.getMaxSysExSize()), std::byte (i % 128), }; outgoing.clear(); detail::Marshalling::Writer { outgoing } (header, discovery); options.getOutputs()[i]->processMessage ({ options.getFunctionBlock().firstGroup, outgoing }); } } void sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint) { detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, destination, ChannelInGroup::wholeBlock, endpoint); } void sendProfileInquiry (MUID receiver, ChannelInGroup address) { if (! supportsProfiles (receiver)) return; detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, receiver, address, Message::ProfileInquiry{}); } void sendProfileDetailsInquiry (MUID receiver, ChannelInGroup address, Profile profile, std::byte target) { if (! supportsProfiles (receiver)) return; detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, receiver, address, Message::ProfileDetails { profile, target }); } void sendProfileSpecificData (MUID receiver, ChannelInGroup address, Profile profile, Span data) { if (! supportsProfiles (receiver)) return; detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, receiver, address, Message::ProfileSpecificData { profile, data }); } void sendProfileEnablement (MUID m, ChannelInGroup address, Profile profile, int numChannels) { if (! supportsProfiles (m)) return; // There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense! jassert (numChannels <= 256); if (numChannels > 0) { const auto channelsToSend = address == ChannelInGroup::wholeBlock || address == ChannelInGroup::wholeGroup ? 0 : numChannels; detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, m, address, Message::ProfileOn { profile, (uint16_t) channelsToSend }); } else { detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, m, address, Message::ProfileOff { profile }); } } void sendPropertyCapabilitiesInquiry (MUID m) { if (! supportsProperties (m)) return; detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, m, ChannelInGroup::wholeBlock, Message::PropertyExchangeCapabilities { std::byte { propertyDelegate.getNumSimultaneousRequestsSupported() }, {}, {} }); } std::optional sendPropertyGetInquiry (MUID m, const PropertyRequestHeader& header, std::function onResult) { const auto iter = discovered.find (m); if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported()) return {}; const auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(), std::move (onResult)); if (! primed.has_value()) return {}; const auto id = iter->second.initiatorPropertyCaches.getRequestIdForToken (*primed); jassert (id.has_value()); detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, m, ChannelInGroup::wholeBlock, Message::PropertyGetData { { id->asByte(), Encodings::jsonTo7BitText (header.toVarCondensed()) } }); return RequestKey { m, *primed }; } std::optional sendPropertySetInquiry (MUID m, const PropertyRequestHeader& header, Span body, std::function onResult) { const auto encoded = Encodings::tryEncode (body, header.mutualEncoding); if (! encoded.has_value()) return {}; const auto iter = discovered.find (m); if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported()) return {}; const auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(), std::move (onResult)); if (! primed.has_value()) return {}; const auto id = iter->second.initiatorPropertyCaches.getRequestIdForToken (*primed); jassert (id.has_value()); detail::PropertyHostUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, detail::MessageMeta::Meta::subID2, m, id->asByte(), Encodings::jsonTo7BitText (header.toVarCondensed()), *encoded, cacheProvider.getMaxSysexSizeForMuid (m)); return RequestKey { m, *primed }; } void abortPropertyRequest (RequestKey k) override { const auto iter = discovered.find (k.getMuid()); if (iter == discovered.end()) return; const auto id = iter->second.initiatorPropertyCaches.getRequestIdForToken (k.getKey()); if (! id.has_value() || ! iter->second.initiatorPropertyCaches.terminate (k.getKey())) return; const Message::Header notifyHeader { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, muid, k.getMuid(), }; const auto jsonHeader = Encodings::jsonTo7BitText (JSONUtils::makeObjectWithKeyFirst ({ { "status", 144 } }, "status")); detail::MessageTypeUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, notifyHeader, Message::PropertyNotify { { id->asByte(), jsonHeader, 1, 1, {} } }); } std::optional getIdForRequestKey (RequestKey key) const { const auto iter = discovered.find (key.getMuid()); if (iter == discovered.end()) return {}; return iter->second.initiatorPropertyCaches.getRequestIdForToken (key.getKey()); } std::vector getOngoingRequests() const { std::vector result; for (auto& i : discovered) for (const auto& token : i.second.initiatorPropertyCaches.getOngoingTransactions()) result.emplace_back (i.first, token); return result; } SubscriptionKey beginSubscription (MUID m, const PropertySubscriptionHeader& header) { return subscriptionManager.beginSubscription (m, header); } void endSubscription (SubscriptionKey key) { subscriptionManager.endSubscription (key); } std::vector getOngoingSubscriptions() const { return subscriptionManager.getOngoingSubscriptions(); } std::optional getSubscribeIdForKey (SubscriptionKey key) const { return subscriptionManager.getSubscribeIdForKey (key); } std::optional getResourceForKey (SubscriptionKey key) const { return subscriptionManager.getResourceForKey (key); } bool sendPendingMessages() { return subscriptionManager.sendPendingMessages(); } void processMessage (ump::BytesOnGroup msg) { // Queried before the property host to unconditionally register capabilities of property exchange hosts. FirstListener firstListener { this }; LastListener lastListener { this }; ResponderDelegate* const l[] { &firstListener, getProfileHostImpl (*this), getPropertyHostImpl (*this), &lastListener }; const auto status = detail::Responder::processCompleteMessage (concreteBufferOutput, msg, l); if (status == Parser::Status::collidingMUID) { muid = getReallyRandomMuid(); concreteBufferOutput.resetSentMuid(); sendDiscovery(); } } void addListener (Listener& l) { listeners.add (&l); } void removeListener (Listener& l) { listeners.remove (&l); } std::vector getDiscoveredMuids() const { std::vector result (discovered.size(), MUID::makeUnchecked (0)); std::transform (discovered.begin(), discovered.end(), result.begin(), [] (const auto& p) { return p.first; }); return result; } std::optional getDiscoveryInfoForMuid (MUID m) const { const auto iter = discovered.find (m); return iter != discovered.end() ? std::optional (iter->second.discovery) : std::nullopt; } std::optional getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const { const auto iter = discovered.find (m); return iter != discovered.end() ? std::optional ((int) iter->second.propertyExchangeResponse->numSimultaneousRequestsSupported) : std::nullopt; } const ChannelProfileStates* getProfileStateForMuid (MUID m, ChannelAddress address) const { const auto iter = discovered.find (m); return iter != discovered.end() ? iter->second.profileStates.getStateForDestination (address) : nullptr; } var getResourceListForMuid (MUID x) const { const auto iter = discovered.find (x); return iter != discovered.end() ? iter->second.resourceList : var(); } var getDeviceInfoForMuid (MUID x) const { const auto iter = discovered.find (x); return iter != discovered.end() ? iter->second.deviceInfo : var(); } var getChannelListForMuid (MUID x) const { const auto iter = discovered.find (x); return iter != discovered.end() ? iter->second.channelList : var(); } MUID getMuid() const { return muid; } Options getOptions() const { return options; } ProfileHost* getProfileHost() { return getProfileHostImpl (*this); } const ProfileHost* getProfileHost() const { return getProfileHostImpl (*this); } PropertyHost* getPropertyHost() { return getPropertyHostImpl (*this); } const PropertyHost* getPropertyHost() const { return getPropertyHostImpl (*this); } private: class FirstListener : public ResponderDelegate { public: explicit FirstListener (Impl* d) : device (d) {} bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override { detail::MessageTypeUtils::visit (message, Visitor { device, &output }); return false; } private: class Visitor : public detail::MessageTypeUtils::MessageVisitor { public: Visitor (Impl* d, ResponderOutput* o) : device (d), output (o) {} void visit (const Message::PropertyExchangeCapabilities& caps) const override { visitImpl (caps); } void visit (const Message::PropertyExchangeCapabilitiesResponse& caps) const override { visitImpl (caps); } using MessageVisitor::visit; private: template void visitImpl (const Body& t) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return; iter->second.propertyExchangeResponse = Message::PropertyExchangeCapabilitiesResponse { t.numSimultaneousRequestsSupported, t.majorVersion, t.minorVersion }; } Impl* device = nullptr; ResponderOutput* output = nullptr; }; Impl* device = nullptr; }; class LastListener : public ResponderDelegate { public: explicit LastListener (Impl* d) : device (d) {} bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override { bool result = false; detail::MessageTypeUtils::visit (message, Visitor { device, &output, &result }); return result; } private: class Visitor : public detail::MessageTypeUtils::MessageVisitor { public: Visitor (Impl* d, ResponderOutput* o, bool* b) : device (d), output (o), handled (b) {} void visit (const Message::Discovery& x) const override { visitImpl (x); } void visit (const Message::DiscoveryResponse& x) const override { visitImpl (x); } void visit (const Message::InvalidateMUID& x) const override { visitImpl (x); } void visit (const Message::EndpointInquiry& x) const override { visitImpl (x); } void visit (const Message::EndpointInquiryResponse& x) const override { visitImpl (x); } void visit (const Message::NAK& x) const override { visitImpl (x); } void visit (const Message::ProfileInquiryResponse& x) const override { visitImpl (x); } void visit (const Message::ProfileAdded& x) const override { visitImpl (x); } void visit (const Message::ProfileRemoved& x) const override { visitImpl (x); } void visit (const Message::ProfileEnabledReport& x) const override { visitImpl (x); } void visit (const Message::ProfileDisabledReport& x) const override { visitImpl (x); } void visit (const Message::ProfileDetailsResponse& x) const override { visitImpl (x); } void visit (const Message::ProfileSpecificData& x) const override { visitImpl (x); } void visit (const Message::PropertyExchangeCapabilitiesResponse& x) const override { visitImpl (x); } void visit (const Message::PropertyGetDataResponse& x) const override { visitImpl (x); } void visit (const Message::PropertySetDataResponse& x) const override { visitImpl (x); } void visit (const Message::PropertySubscribe& x) const override { visitImpl (x); } void visit (const Message::PropertySubscribeResponse& x) const override { visitImpl (x); } void visit (const Message::PropertyNotify& x) const override { visitImpl (x); } using MessageVisitor::visit; private: template void visitImpl (const Body& body) const { *handled = messageReceived (body); } bool messageReceived (const Message::Discovery& body) const { const auto replyPath = uint8_t (output->getIncomingHeader().version) >= 0x02 ? body.outputPathID : std::byte { 0x00 }; detail::MessageTypeUtils::send (*output, Message::DiscoveryResponse { device->options.getDeviceInfo(), device->options.getFeatures().getSupportedCapabilities(), uint32_t (device->options.getMaxSysExSize()), replyPath, device->options.getFunctionBlock().identifier, }); // TODO(reuk) rather than sending a new discovery inquiry, we should store the details from the incoming message const auto iter = device->discovered.find (output->getIncomingHeader().source); if (iter == device->discovered.end()) { const auto initiator = output->getIncomingHeader().source; device->discovered.emplace (initiator, Discovered { body }); device->listeners.call ([&] (auto& l) { l.deviceAdded (initiator); }); device->sendEndpointInquiry (initiator, Message::EndpointInquiry { std::byte{} }); } return true; } bool messageReceived (const Message::DiscoveryResponse& response) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter != device->discovered.end()) { device->discovered.erase (iter); device->listeners.call ([&] (auto& l) { l.deviceRemoved (responderMUID); }); const Message::Header header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device->muid, MUID::getBroadcast(), }; detail::MessageTypeUtils::send (*output, output->getIncomingGroup(), header, Message::InvalidateMUID { responderMUID }); } else { const Message::Discovery discovery { response.device, response.capabilities, response.maximumSysexSize, response.outputPathID }; device->discovered.emplace (responderMUID, Discovered { discovery }); device->listeners.call ([&] (auto& l) { l.deviceAdded (responderMUID); }); device->sendEndpointInquiry (output->getIncomingHeader().source, Message::EndpointInquiry { std::byte{} }); } return true; } bool messageReceived (const Message::InvalidateMUID& invalidate) const { const auto targetMuid = invalidate.target; const auto iter = device->discovered.find (targetMuid); if (iter != device->discovered.end()) { device->subscriptionManager.endSubscriptionsFromResponder (targetMuid); device->discovered.erase (iter); device->listeners.call ([&] (auto& l) { l.deviceRemoved (targetMuid); }); } if (invalidate.target != device->muid) return false; device->muid = getReallyRandomMuid(); device->concreteBufferOutput.resetSentMuid(); device->sendDiscovery(); return true; } bool messageReceived (const Message::EndpointInquiry& endpoint) const { // Only status 0 is defined at time of writing if (endpoint.status == std::byte{}) { const auto& id = device->options.getProductInstanceId(); const auto length = std::distance (id.begin(), std::find (id.begin(), id.end(), 0)); if (length <= 0) return false; Message::EndpointInquiryResponse response; response.status = endpoint.status; response.data = Span (reinterpret_cast (id.data()), (size_t) length); detail::MessageTypeUtils::send (*output, response); return true; } return false; } bool messageReceived (const Message::EndpointInquiryResponse& endpoint) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; // Got an endpoint response for a device we haven't discovered device->listeners.call ([&] (auto& l) { l.endpointReceived (responderMUID, endpoint); }); return true; } bool messageReceived (const Message::NAK& nak) const { const auto responderMUID = output->getIncomingHeader().source; device->listeners.call ([&] (auto& l) { l.messageNotAcknowledged (responderMUID, nak); }); return true; } bool messageReceived (const Message::ProfileInquiryResponse& response) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto destination = output->getIncomingHeader().deviceID; auto* state = iter->second.profileStates.getStateForDestination (output->getChannelAddress()); if (state == nullptr) return false; ChannelProfileStates newState; for (auto& enabled : response.enabledProfiles) newState.set (enabled, { 1, 1 }); for (auto& disabled : response.disabledProfiles) newState.set (disabled, { 1, 0 }); *state = newState; device->listeners.call ([&] (auto& l) { l.profileStateReceived (responderMUID, destination); }); return true; } bool messageReceived (const Message::ProfileAdded& added) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto address = output->getChannelAddress(); auto* state = iter->second.profileStates.getStateForDestination (address); if (state == nullptr) return false; state->set (added.profile, { 1, 0 }); device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), added.profile, true); }); return true; } bool messageReceived (const Message::ProfileRemoved& removed) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto address = output->getChannelAddress(); auto* state = iter->second.profileStates.getStateForDestination (address); if (state == nullptr) return false; state->erase (removed.profile); device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), removed.profile, false); }); return true; } bool messageReceived (const Message::ProfileEnabledReport& x) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto address = output->getChannelAddress(); auto* state = iter->second.profileStates.getStateForDestination (address); if (state == nullptr) return false; const auto numChannels = jmax ((uint16_t) 1, x.numChannels); state->set (x.profile, { state->get (x.profile).supported, numChannels }); device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, numChannels); }); return true; } bool messageReceived (const Message::ProfileDisabledReport& x) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto address = output->getChannelAddress(); auto* state = iter->second.profileStates.getStateForDestination (address); if (state == nullptr) return false; state->set (x.profile, { state->get (x.profile).supported, 0 }); device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, 0); }); return true; } bool messageReceived (const Message::ProfileDetailsResponse& response) const { const auto responderMUID = output->getIncomingHeader().source; const auto destination = output->getIncomingHeader().deviceID; device->listeners.call ([&] (auto& l) { l.profileDetailsReceived (responderMUID, destination, response.profile, response.target, response.data); }); return true; } bool messageReceived (const Message::ProfileSpecificData& data) const { const auto responderMUID = output->getIncomingHeader().source; const auto destination = output->getIncomingHeader().deviceID; device->listeners.call ([&] (auto& l) { l.profileSpecificDataReceived (responderMUID, destination, data.profile, data.data); }); return true; } bool messageReceived (const Message::PropertyExchangeCapabilitiesResponse&) const { const auto source = output->getIncomingHeader().source; const auto iter = device->discovered.find (source); constexpr auto hasResource = [] (var obj, auto resource) { if (auto* array = obj.getArray()) for (const auto& item : *array) if (item.isObject() && item.getProperty ("resource", {}) == var (resource)) return true; return false; }; const auto onResourceListReceived = [this, iter, source, hasResource] (const PropertyExchangeResult& result) { const auto validateResponse = [] (const PropertyExchangeResult& r) { const auto parsed = r.getHeaderAsReplyHeader(); return ! r.getError().has_value() && parsed.mediaType == PropertySubscriptionHeader().mediaType && parsed.status == 200; }; const auto allDone = [this, source] { device->listeners.call ([source] (auto& l) { l.propertyExchangeCapabilitiesReceived (source); }); }; if (! validateResponse (result)) { jassertfalse; allDone(); return; } const auto bodyAsObj = Encodings::jsonFrom7BitText (result.getBody()); iter->second.resourceList = bodyAsObj; const auto onChannelListReceived = [iter, allDone, validateResponse] (const PropertyExchangeResult& r) { if (validateResponse (r)) iter->second.channelList = Encodings::jsonFrom7BitText (r.getBody()); allDone(); return; }; const auto getChannelList = [this, bodyAsObj, source, allDone, hasResource, onChannelListReceived] { if (hasResource (bodyAsObj, "ChannelList")) { PropertyRequestHeader header; header.resource = "ChannelList"; device->sendPropertyGetInquiry (source, header, onChannelListReceived); return; } allDone(); return; }; if (hasResource (bodyAsObj, "DeviceInfo")) { PropertyRequestHeader header; header.resource = "DeviceInfo"; device->sendPropertyGetInquiry (source, header, [iter, getChannelList, validateResponse] (const PropertyExchangeResult& r) { if (validateResponse (r)) iter->second.deviceInfo = Encodings::jsonFrom7BitText (r.getBody()); getChannelList(); }); return; } return getChannelList(); }; PropertyRequestHeader header; header.resource = "ResourceList"; device->sendPropertyGetInquiry (source, header, onResourceListReceived); return true; } bool handlePropertyDataResponse (const Message::DynamicSizePropertyExchange& response) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto request = RequestID::create (response.requestID); if (! request.has_value()) return false; iter->second.initiatorPropertyCaches.addChunk (*request, response); return true; } bool messageReceived (const Message::PropertyGetDataResponse& response) const { handlePropertyDataResponse (response); return true; } bool messageReceived (const Message::PropertySetDataResponse& response) const { handlePropertyDataResponse (Message::DynamicSizePropertyExchange { response.requestID, response.header, 1, 1, {} }); return true; } bool messageReceived (const Message::PropertySubscribe& subscription) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto request = subscription.requestID; const auto source = output->getIncomingHeader().source; const auto jsonHeader = Encodings::jsonFrom7BitText (subscription.header); const auto typedHeader = PropertySubscriptionHeader::parseCondensed (jsonHeader); const auto subscribeId = typedHeader.subscribeId; const auto callback = [this, request, source, subscribeId] (const PropertyExchangeResult& result) { if (result.getError().has_value()) return; PropertySubscriptionData data; data.header = result.getHeaderAsSubscriptionHeader(); data.body = result.getBody(); if (data.header.command == PropertySubscriptionCommand::end) device->subscriptionManager.endSubscriptionFromResponder (source, subscribeId); if (data.header.command != PropertySubscriptionCommand::start) device->listeners.call ([source, &data] (auto& l) { l.propertySubscriptionDataReceived (source, data); }); PropertyReplyHeader header; const auto headerBytes = Encodings::jsonTo7BitText (header.toVarCondensed()); detail::MessageTypeUtils::send (device->concreteBufferOutput, device->options.getFunctionBlock().firstGroup, source, ChannelInGroup::wholeBlock, Message::PropertySubscribeResponse { { request, headerBytes, 1, 1, {} } }); }; const auto requestID = RequestID::create (subscription.requestID); if (! requestID.has_value()) return false; // Subscription events may be sent at any time by the responder, so there may not be // an existing transaction ID for new subscription messages. iter->second.responderPropertyCaches.primeCache (device->propertyDelegate.getNumSimultaneousRequestsSupported(), callback, *requestID); iter->second.responderPropertyCaches.addChunk (*requestID, subscription); return true; } bool messageReceived (const Message::PropertySubscribeResponse& response) const { handlePropertyDataResponse (response); return true; } bool messageReceived (const Message::PropertyNotify& notify) const { const auto responderMUID = output->getIncomingHeader().source; const auto iter = device->discovered.find (responderMUID); if (iter == device->discovered.end()) return false; const auto requestID = RequestID::create (notify.requestID); if (! requestID.has_value()) return false; iter->second.initiatorPropertyCaches.notify (*requestID, notify.header); iter->second.responderPropertyCaches.notify (*requestID, notify.header); return true; } Impl* device = nullptr; ResponderOutput* output = nullptr; bool* handled = nullptr; }; Impl* device = nullptr; }; struct Discovered { explicit Discovered (Message::Discovery r) : discovery (r) {} Message::Discovery discovery; std::optional propertyExchangeResponse; BlockProfileStates profileStates; InitiatorPropertyExchangeCache initiatorPropertyCaches; ResponderPropertyExchangeCache responderPropertyCaches; var resourceList, deviceInfo, channelList; }; class ConcreteBufferOutput : public BufferOutput { public: explicit ConcreteBufferOutput (Impl& d) : device (d) {} MUID getMuid() const override { return device.muid; } std::vector& getOutputBuffer() override { return device.outgoing; } void send (uint8_t group) override { sentMuid = true; for (auto* o : device.options.getOutputs()) o->processMessage ({ group, getOutputBuffer() }); } bool hasSentMuid() const { return sentMuid; } void resetSentMuid() { sentMuid = false; } private: Impl& device; bool sentMuid = false; }; class CacheProviderImpl : public CacheProvider { public: explicit CacheProviderImpl (Impl& d) : device (d) {} std::set getDiscoveredMuids() const override { std::set result; for (const auto& d : device.discovered) result.insert (d.first); return result; } InitiatorPropertyExchangeCache* getCacheForMuidAsInitiator (MUID m) override { const auto iter = device.discovered.find (m); return iter != device.discovered.end() ? &iter->second.initiatorPropertyCaches : nullptr; } ResponderPropertyExchangeCache* getCacheForMuidAsResponder (MUID m) override { const auto iter = device.discovered.find (m); return iter != device.discovered.end() ? &iter->second.responderPropertyCaches : nullptr; } int getMaxSysexSizeForMuid (MUID m) const override { constexpr auto defaultResult = 1 << 16; const auto iter = device.discovered.find (m); return iter != device.discovered.end() ? jmin (defaultResult, (int) iter->second.discovery.maximumSysexSize) : defaultResult; } private: Impl& device; }; class ProfileDelegateImpl : public ProfileDelegate { public: explicit ProfileDelegateImpl (Impl& d) : device (d) {} void profileEnablementRequested (MUID x, ProfileAtAddress profileAtAddress, int numChannels, bool enabled) override { if (auto* d = device.options.getProfileDelegate()) return d->profileEnablementRequested (x, profileAtAddress, numChannels, enabled); if (! device.profileHost.has_value()) return; device.profileHost->setProfileEnablement (profileAtAddress, enabled ? jmax (1, numChannels) : 0); } private: Impl& device; }; class PropertyDelegateImpl : public PropertyDelegate { public: explicit PropertyDelegateImpl (Impl& d) : device (d) {} uint8_t getNumSimultaneousRequestsSupported() const override { if (auto* d = device.options.getPropertyDelegate()) return d->getNumSimultaneousRequestsSupported(); return 127; } PropertyReplyData propertyGetDataRequested (MUID m, const PropertyRequestHeader& header) override { if (auto* d = device.options.getPropertyDelegate()) return d->propertyGetDataRequested (m, header); PropertyReplyData result; result.header.status = 404; // Resource not found, do not retry result.header.message = TRANS ("Handling for \"Inquiry: Get Property Data\" is not implemented."); return result; } PropertyReplyHeader propertySetDataRequested (MUID m, const PropertyRequestData& data) override { if (auto* d = device.options.getPropertyDelegate()) return d->propertySetDataRequested (m, data); PropertyReplyHeader result; result.status = 404; // Resource not found, do not retry result.message = TRANS ("Handling for \"Inquiry: Set Property Data\" is not implemented."); return result; } bool subscriptionStartRequested (MUID m, const PropertySubscriptionHeader& data) override { if (auto* d = device.options.getPropertyDelegate()) return d->subscriptionStartRequested (m, data); return false; } void subscriptionDidStart (MUID m, const String& id, const PropertySubscriptionHeader& data) override { if (auto* d = device.options.getPropertyDelegate()) d->subscriptionDidStart (m, id, data); } void subscriptionWillEnd (MUID m, const ci::Subscription& subscription) override { if (auto* d = device.options.getPropertyDelegate()) d->subscriptionWillEnd (m, subscription); } private: Impl& device; }; std::optional sendPropertySubscribe (MUID m, const PropertySubscriptionHeader& header, std::function onResult) override { const auto iter = discovered.find (m); if (iter == discovered.end()) return {}; const auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(), std::move (onResult)); if (! primed.has_value()) return {}; const auto id = iter->second.initiatorPropertyCaches.getRequestIdForToken (*primed); jassert (id.has_value()); detail::PropertyHostUtils::send (concreteBufferOutput, options.getFunctionBlock().firstGroup, detail::MessageMeta::Meta::subID2, m, id->asByte(), Encodings::jsonTo7BitText (header.toVarCondensed()), {}, cacheProvider.getMaxSysexSizeForMuid (m)); return RequestKey (m, *primed); } void propertySubscriptionChanged (SubscriptionKey key, const std::optional& subscribeId) override { listeners.call ([&] (auto& l) { l.propertySubscriptionChanged (key, subscribeId); }); } static MUID getReallyRandomMuid() { Random random; return MUID::makeRandom (random); } static DeviceOptions getValidated (DeviceOptions opt) { opt = opt.withMaxSysExSize (jmax ((size_t) 128, opt.getMaxSysExSize())); if (opt.getFeatures().isPropertyExchangeSupported()) opt = opt.withMaxSysExSize (jmax ((size_t) 512, opt.getMaxSysExSize())); opt = opt.withFeatures (opt.getFeatures().withProcessInquirySupported (false)); // You'll need to provide some outputs if you want the device to talk to the outside world! jassert (! opt.getOutputs().empty()); return opt; } template bool supportsFlag (MUID m, Member member) const { const auto iter = discovered.find (m); return iter != discovered.end() && (Features (iter->second.discovery.capabilities).*member)(); } bool supportsProfiles (MUID m) const { return supportsFlag (m, &Features::isProfileConfigurationSupported); } bool supportsProperties (MUID m) const { return supportsFlag (m, &Features::isPropertyExchangeSupported); } DeviceOptions options; MUID muid; std::vector outgoing; std::map discovered; SubscriptionManager subscriptionManager { *this }; ListenerList listeners; ConcreteBufferOutput concreteBufferOutput { *this }; CacheProviderImpl cacheProvider { *this }; ProfileDelegateImpl profileDelegate { *this }; PropertyDelegateImpl propertyDelegate { *this }; std::optional profileHost; std::optional propertyHost; }; //============================================================================== Device::Device (const Options& opt) : pimpl (std::make_unique (opt)) {} Device::~Device() = default; Device::Device (Device&&) noexcept = default; Device& Device::operator= (Device&&) noexcept = default; void Device::processMessage (ump::BytesOnGroup msg) { pimpl->processMessage (msg); } void Device::sendDiscovery() { pimpl->sendDiscovery(); } void Device::sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint) { pimpl->sendEndpointInquiry (destination, endpoint); } void Device::sendProfileInquiry (MUID destination, ChannelInGroup address) { pimpl->sendProfileInquiry (destination, address); } void Device::sendProfileDetailsInquiry (MUID destination, ChannelInGroup address, Profile profile, std::byte target) { pimpl->sendProfileDetailsInquiry (destination, address, profile, target); } void Device::sendProfileSpecificData (MUID destination, ChannelInGroup address, Profile profile, Span data) { pimpl->sendProfileSpecificData (destination, address, profile, data); } void Device::sendProfileEnablement (MUID destination, ChannelInGroup address, Profile profile, int numChannels) { pimpl->sendProfileEnablement (destination, address, profile, numChannels); } void Device::sendPropertyCapabilitiesInquiry (MUID destination) { pimpl->sendPropertyCapabilitiesInquiry (destination); } std::optional Device::sendPropertyGetInquiry (MUID m, const PropertyRequestHeader& header, std::function onResult) { return pimpl->sendPropertyGetInquiry (m, header, std::move (onResult)); } std::optional Device::sendPropertySetInquiry (MUID m, const PropertyRequestHeader& header, Span body, std::function onResult) { return pimpl->sendPropertySetInquiry (m, header, body, std::move (onResult)); } void Device::abortPropertyRequest (RequestKey key) { pimpl->abortPropertyRequest (key); } std::optional Device::getIdForRequestKey (RequestKey key) const { return pimpl->getIdForRequestKey (key); } std::vector Device::getOngoingRequests() const { return pimpl->getOngoingRequests(); } SubscriptionKey Device::beginSubscription (MUID m, const PropertySubscriptionHeader& header) { return pimpl->beginSubscription (m, header); } void Device::endSubscription (SubscriptionKey key) { pimpl->endSubscription (key); } std::vector Device::getOngoingSubscriptions() const { return pimpl->getOngoingSubscriptions(); } std::optional Device::getSubscribeIdForKey (SubscriptionKey key) const { return pimpl->getSubscribeIdForKey (key); } std::optional Device::getResourceForKey (SubscriptionKey key) const { return pimpl->getResourceForKey (key); } bool Device::sendPendingMessages() { return pimpl->sendPendingMessages(); } void Device::addListener (Listener& l) { pimpl->addListener (l); } void Device::removeListener (Listener& l) { pimpl->removeListener (l); } MUID Device::getMuid() const { return pimpl->getMuid(); } DeviceOptions Device::getOptions() const { return pimpl->getOptions(); } std::vector Device::getDiscoveredMuids() const { return pimpl->getDiscoveredMuids(); } const ProfileHost* Device::getProfileHost() const { return pimpl->getProfileHost(); } ProfileHost* Device::getProfileHost() { return pimpl->getProfileHost(); } const PropertyHost* Device::getPropertyHost() const { return pimpl->getPropertyHost(); } PropertyHost* Device::getPropertyHost() { return pimpl->getPropertyHost(); } std::optional Device::getDiscoveryInfoForMuid (MUID m) const { return pimpl->getDiscoveryInfoForMuid (m); } const ChannelProfileStates* Device::getProfileStateForMuid (MUID m, ChannelAddress address) const { return pimpl->getProfileStateForMuid (m, address); } std::optional Device::getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const { return pimpl->getNumPropertyExchangeRequestsSupportedForMuid (m); } var Device::getResourceListForMuid (MUID x) const { return pimpl->getResourceListForMuid (x); } var Device::getDeviceInfoForMuid (MUID x) const { return pimpl->getDeviceInfoForMuid (x); } var Device::getChannelListForMuid (MUID x) const { return pimpl->getChannelListForMuid (x); } //============================================================================== //============================================================================== #if JUCE_UNIT_TESTS class DeviceTests : public UnitTest { public: DeviceTests() : UnitTest ("Device", UnitTestCategories::midi) {} void runTest() override { auto random = getRandom(); struct GroupOutput { uint8_t group; std::vector bytes; bool operator== (const GroupOutput& other) const { const auto tie = [] (const auto& x) { return std::tie (x.group, x.bytes); }; return tie (*this) == tie (other); } bool operator!= (const GroupOutput& other) const { return ! operator== (other); } }; struct Output : public DeviceMessageHandler { void processMessage (ump::BytesOnGroup msg) override { messages.push_back ({ msg.group, std::vector (msg.bytes.begin(), msg.bytes.end()) }); } std::vector messages; }; const ump::DeviceInfo deviceInfo { { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 } }, { std::byte { 0x11 }, std::byte { 0x12 } }, { std::byte { 0x21 }, std::byte { 0x22 } }, { std::byte { 0x31 }, std::byte { 0x32 }, std::byte { 0x33 }, std::byte { 0x34 } } }; FunctionBlock functionBlock; beginTest ("When receiving Discovery from a MUID that matches the Device MUID, reply with InvalidateMUID and initiate discovery"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; const auto commonMUID = device.getMuid(); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, commonMUID, MUID::getBroadcast() }, Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, { std::byte { 0x15 }, std::byte { 0x16 } }, { std::byte { 0x25 }, std::byte { 0x26 } }, { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, std::byte{}, 1024, std::byte{} }) }); expect (device.getMuid() != commonMUID); const std::vector responses { { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, commonMUID, MUID::getBroadcast() }, Message::InvalidateMUID { commonMUID }) }, { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), MUID::getBroadcast() }, Message::Discovery { deviceInfo, std::byte{}, 512, std::byte{} }) }, }; expect (output.messages == responses); } beginTest ("When receiving Discovery from a MUID that does not match the Device MUID, reply with DiscoveryResponse and EndpointInquiry"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; const auto responderMUID = device.getMuid(); const auto initiatorMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, initiatorMUID, MUID::getBroadcast() }, Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, { std::byte { 0x15 }, std::byte { 0x16 } }, { std::byte { 0x25 }, std::byte { 0x26 } }, { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, std::byte{}, 1024, std::byte{} }) }); expect (device.getMuid() == responderMUID); const std::vector responses { { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }, { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::EndpointInquiry { std::byte{} }) }, }; expect (output.messages == responses); } beginTest ("Sending a V1 discovery message notifies the listener"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; const auto responderMUID = device.getMuid(); const auto initiatorMUID = MUID::makeRandom (random); constexpr uint8_t version = 0x01; auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, std::byte { version }, initiatorMUID, MUID::getBroadcast() }, Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, { std::byte { 0x15 }, std::byte { 0x16 } }, { std::byte { 0x25 }, std::byte { 0x26 } }, { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, std::byte{}, 1024, std::byte{} }); // V1 message doesn't have an output path bytes.pop_back(); device.processMessage ({ 0, bytes }); expect (device.getMuid() == responderMUID); const std::vector responses { { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }, { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::EndpointInquiry { std::byte{} }) }, }; expect (output.messages == responses); } beginTest ("Sending a V2 discovery message notifies the input listener"); { constexpr std::byte outputPathID { 5 }; const auto initiatorMUID = MUID::makeRandom (random); constexpr std::byte version { 0x02 }; Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; const auto responderMUID = device.getMuid(); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, version, initiatorMUID, MUID::getBroadcast() }, Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, { std::byte { 0x15 }, std::byte { 0x16 } }, { std::byte { 0x25 }, std::byte { 0x26 } }, { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, std::byte{}, 1024, outputPathID }) }); expect (device.getMuid() == responderMUID); const std::vector responses { { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) }, { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::EndpointInquiry { std::byte{} }) }, }; expect (output.messages == responses); } beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields"); { constexpr std::byte outputPathID { 10 }; const auto initiatorMUID = MUID::makeRandom (random); constexpr std::byte version { 0x03 }; Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; const auto responderMUID = device.getMuid(); auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, version, initiatorMUID, MUID::getBroadcast() }, Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } }, { std::byte { 0x15 }, std::byte { 0x16 } }, { std::byte { 0x25 }, std::byte { 0x26 } }, { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } }, std::byte{}, 1024, outputPathID }); // Future versions might have more trailing bytes bytes.insert (bytes.end(), { std::byte{}, std::byte{} }); device.processMessage ({ 0, bytes }); expect (device.getMuid() == responderMUID); const std::vector responses { { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) }, { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, initiatorMUID }, Message::EndpointInquiry { std::byte{} }) }, }; expect (output.messages == responses); } beginTest ("When receiving an InvalidateMUID that matches the Device MUID, initiate discovery using a new MUID"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; const auto deviceMUID = device.getMuid(); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, MUID::makeRandom (random), MUID::getBroadcast() }, Message::InvalidateMUID { deviceMUID }) }); expect (device.getMuid() != deviceMUID); expect (Parser::parse (MUID::makeRandom (random), output.messages.front().bytes) == Message::Parsed { { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), MUID::getBroadcast() }, Message::Discovery { deviceInfo, {}, 512, {} } }); } struct Listener : public DeviceListener { void deviceAdded (MUID x) override { added .push_back (x); } void deviceRemoved (MUID x) override { removed.push_back (x); } std::vector added, removed; }; beginTest ("When receiving a DiscoveryResponse, update the set of known devices, notify outputs, and request endpoint info"); { Listener delegate; Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; device.addListener (delegate); expect (device.getDiscoveredMuids().empty()); const auto deviceMUID = device.getMuid(); const auto responderMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, deviceMUID }, Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }); expect (device.getDiscoveredMuids() == std::vector { responderMUID }); expect (delegate.added == std::vector { responderMUID }); std::vector responses { { 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, deviceMUID, responderMUID }, Message::EndpointInquiry { std::byte{} }) }, }; expect (output.messages == responses); beginTest ("When receiving a DiscoveryResponse with a MUID that matches a known device, invalidate that MUID"); { device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, deviceMUID }, Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }); expect (device.getDiscoveredMuids().empty()); expect (delegate.removed == std::vector { responderMUID }); responses.push_back ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, deviceMUID, MUID::getBroadcast() }, Message::InvalidateMUID { responderMUID }) }); expect (output.messages == responses); } } beginTest ("After receiving an EndpointResponse, the listener is notified"); { static constexpr std::byte dataBytes[] { std::byte { 0x01 }, std::byte { 0x7f }, std::byte { 0x41 } }; struct EndpointListener : public DeviceListener { EndpointListener (UnitTest& t, Device& d) : test (t), device (d) {} void endpointReceived (MUID, Message::EndpointInquiryResponse) override { called = true; } UnitTest& test; Device& device; bool called = false; }; Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512); Device device { options }; EndpointListener delegate { *this, device }; device.addListener (delegate); const auto responderMUID = MUID::makeRandom (random); const auto deviceMUID = device.getMuid(); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, deviceMUID }, Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) }); expect (! delegate.called); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, responderMUID, deviceMUID }, Message::EndpointInquiryResponse { std::byte{}, dataBytes }) }); expect (delegate.called); } beginTest ("If a device has not previously acted as a responder, modifying profiles does not emit events"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); Device device { options }; expect (device.getProfileHost() != nullptr); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); expect (output.messages.empty()); beginTest ("The device reports profiles accurately"); { const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileInquiry{}) }); const Profile disabledProfiles[] { profile }; expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, disabledProfiles })); } beginTest ("If a device has previously acted as a responder to profile inquiry, then modifying profiles emits events"); { device.getProfileHost()->setProfileEnablement ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 1); expect (output.messages.size() == 2); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), MUID::getBroadcast() }, Message::ProfileEnabledReport { profile, 0 })); } } beginTest ("If a device receives a details inquiry message addressed to an unsupported profile, a NAK with a code of 0x04 is emitted"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); Device device { options }; expect (device.getProfileHost() != nullptr); const auto inquiryMUID = MUID::makeRandom (random); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileDetails { profile, std::byte { 0x02 } }) }); expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::NAK { detail::MessageMeta::Meta::subID2, std::byte { 0x04 }, {}, {}, {} })); } beginTest ("If a device receives a set profile on and enables the profile, profile enabled report is emitted"); { // Note: if there's no explicit profile delegate, the device will toggle profiles as requested. Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); Device device { options }; expect (device.getProfileHost() != nullptr); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileOn { profile, 0 }) }); expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), MUID::getBroadcast() }, Message::ProfileEnabledReport { profile, 0 })); } struct DoNothingProfileDelegate : public ProfileDelegate { void profileEnablementRequested (MUID, ProfileAtAddress, int, bool) override {} }; beginTest ("If a device receives a set profile on but then doesn't enable the profile, profile disabled report is emitted"); { DoNothingProfileDelegate delegate; Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)) .withProfileDelegate (&delegate); Device device { options }; expect (device.getProfileHost() != nullptr); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileOn { profile, 1 }) }); expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), MUID::getBroadcast() }, Message::ProfileDisabledReport { profile, {} })); } beginTest ("If a device receives a set profile on for an unsupported profile, NAK is emitted"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); Device device { options }; expect (device.getProfileHost() != nullptr); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileOn { profile, 1 }) }); expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::NAK { detail::MessageMeta::Meta::subID2, {}, {}, {}, {} })); } beginTest ("If a device receives a set profile off and disables the profile, profile disabled report is emitted"); { // Note: if there's no explicit profile delegate, the device will toggle profiles as requested. Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); Device device { options }; expect (device.getProfileHost() != nullptr); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); device.getProfileHost()->setProfileEnablement ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 0); const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileOff { profile }) }); expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), MUID::getBroadcast() }, Message::ProfileDisabledReport { profile, {} })); } beginTest ("If a device receives a set profile off but then doesn't disable the profile, profile enabled report is emitted"); { Output output; DoNothingProfileDelegate delegate; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)) .withProfileDelegate (&delegate); Device device { options }; expect (device.getProfileHost() != nullptr); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); device.getProfileHost()->setProfileEnablement ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 1); const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileOff { profile }) }); expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), MUID::getBroadcast() }, Message::ProfileEnabledReport { profile, 0 })); } beginTest ("If a device receives a set profile off for an unsupported profile, NAK is emitted"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (functionBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); Device device { options }; expect (device.getProfileHost() != nullptr); const Profile profile { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 }, std::byte { 0x04 }, std::byte { 0x05 } }; const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileOff { profile }) }); expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::NAK { detail::MessageMeta::Meta::subID2, {}, {}, {}, {} })); } const FunctionBlock realBlock { std::byte{}, 0, 3 }; beginTest ("If a device receives a profile inquiry addressed to a channel, that channel's profiles are emitted"); { Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (realBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true)); Device device { options }; auto& profileHost = *device.getProfileHost(); Profile channel0Profile { std::byte { 0x01 } }; Profile channel1Profile { std::byte { 0x02 } }; profileHost.addProfile ({ channel0Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel0) }); profileHost.addProfile ({ channel1Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel1) }); const auto inquiryMUID = MUID::makeRandom (random); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel0, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileInquiry{}) }); const Profile channel0Profiles[] { channel0Profile }; const Profile channel1Profiles[] { channel1Profile }; expect (output.messages.size() == 1); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel0, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, channel0Profiles })); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel2, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileInquiry{}) }); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel2, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, {} })); Profile group0Profile { std::byte { 0x05 } }; Profile group1Profile { std::byte { 0x06 } }; const Profile group0Profiles[] { group0Profile }; const Profile group1Profiles[] { group1Profile }; beginTest ("If a device receives a profile inquiry addressed to a group, that group's profiles are emitted"); { profileHost.addProfile ({ group0Profile, ChannelAddress{}.withGroup (0).withChannel (ChannelInGroup::wholeGroup) }); profileHost.addProfile ({ group1Profile, ChannelAddress{}.withGroup (1).withChannel (ChannelInGroup::wholeGroup) }); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeGroup, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileInquiry{}) }); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, group0Profiles })); device.processMessage ({ 2, getMessageBytes ({ ChannelInGroup::wholeGroup, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileInquiry{}) }); expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, {} })); } beginTest ("If a device receives a profile inquiry addressed to a block, the profiles for member channels, then member groups, then the block are emitted"); { Profile blockProfile { std::byte { 0x0a } }; profileHost.addProfile ({ blockProfile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }); output.messages.clear(); device.processMessage ({ 1, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::ProfileInquiry{}) }); const Profile blockProfiles[] { blockProfile }; expect (output.messages == std::vector { { 0, getMessageBytes ({ ChannelInGroup::channel0, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, channel0Profiles }) }, { 0, getMessageBytes ({ ChannelInGroup::channel1, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, channel1Profiles }) }, { 0, getMessageBytes ({ ChannelInGroup::wholeGroup, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, group0Profiles }) }, { 1, getMessageBytes ({ ChannelInGroup::wholeGroup, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, group1Profiles }) }, { 1, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }, Message::ProfileInquiryResponse { {}, blockProfiles }) } }); } } // Property exchange { const auto inquiryMUID = MUID::makeRandom (random); struct Delegate : public PropertyDelegate { uint8_t getNumSimultaneousRequestsSupported() const override { return 1; } PropertyReplyData propertyGetDataRequested (MUID, const PropertyRequestHeader&) override { return {}; } PropertyReplyHeader propertySetDataRequested (MUID, const PropertyRequestData&) override { return {}; } bool subscriptionStartRequested (MUID, const PropertySubscriptionHeader&) override { return true; } void subscriptionDidStart (MUID, const String&, const PropertySubscriptionHeader&) override {} void subscriptionWillEnd (MUID, const Subscription&) override {} }; Delegate delegate; Output output; const auto options = DeviceOptions().withOutputs ({ &output }) .withFunctionBlock (realBlock) .withDeviceInfo (deviceInfo) .withMaxSysExSize (512) .withFeatures (DeviceFeatures{}.withPropertyExchangeSupported (true)) .withPropertyDelegate (&delegate); Device device { options }; device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, MUID::getBroadcast() }, Message::Discovery { {}, DeviceFeatures{}.withPropertyExchangeSupported (true).getSupportedCapabilities(), 512, {} }) }); expect (output.messages.size() == 2); output.messages.clear(); beginTest ("If a device receives too many concurrent property exchange requests, it responds with a retry status code."); { auto obj = std::make_unique(); obj->setProperty ("resource", "X-CustomProp"); const auto header = Encodings::jsonTo7BitText (obj.release()); for (const auto& requestID : { std::byte { 0 }, std::byte { 1 } }) { device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySetData { { requestID, header, 0, 1, {} } }) }); } expect (output.messages.size() == 1); const auto parsed = Parser::parse (output.messages.back().bytes); expect (parsed.has_value()); expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }); auto* body = std::get_if (&parsed->body); expect (body != nullptr); expect (body->requestID == std::byte { 1 }); auto replyHeader = Encodings::jsonFrom7BitText (body->header); expect (replyHeader.getProperty ("status", "") == var (343)); } // Terminate ongoing message device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySetData { { {}, {}, 0, 0, {} } }) }); output.messages.clear(); beginTest ("If a device receives an unexpectedly-terminated request, it responds with an error status code."); { auto obj = std::make_unique(); obj->setProperty ("resource", "X-CustomProp"); const auto header = Encodings::jsonTo7BitText (obj.release()); const std::byte requestID { 3 }; device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySetData { { requestID, header, 2, 1, {} } }) }); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySetData { { requestID, header, 2, 0, {} } }) }); expect (output.messages.size() == 1); const auto parsed = Parser::parse (output.messages.back().bytes); expect (parsed.has_value()); expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }); auto* body = std::get_if (&parsed->body); expect (body != nullptr); expect (body->requestID == requestID); auto replyHeader = Encodings::jsonFrom7BitText (body->header); expect (replyHeader.getProperty ("status", "") == var (400)); } output.messages.clear(); const auto makeStatusHeader = [] (int status) { auto ptr = std::make_unique(); ptr->setProperty ("status", status); return Encodings::jsonTo7BitText (ptr.release()); }; const auto successHeader = makeStatusHeader (200); const auto retryHeader = makeStatusHeader (343); const auto cancelHeader = makeStatusHeader (144); // Common rules for PE section 10: There is no reply message associated with any Notify message. beginTest ("If a request is terminated via notify, the device does not respond"); { auto obj = std::make_unique(); obj->setProperty ("resource", "X-CustomProp"); const auto header = Encodings::jsonTo7BitText (obj.release()); const std::byte requestID { 100 }; device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySetData { { requestID, header, 2, 1, {} } }) }); expect (device.getPropertyHost()->countOngoingTransactions() == 1); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertyNotify { { requestID, cancelHeader, 1, 1, {} } }) }); expect (device.getPropertyHost()->countOngoingTransactions() == 0); expect (output.messages.empty()); } beginTest ("Sending too many property requests simultaneously fails"); { PropertyRequestHeader header; header.resource = "X-CustomProp"; const auto a = device.sendPropertyGetInquiry (inquiryMUID, header, [] (const PropertyExchangeResult&) {}); expect (a.has_value()); expect (device.getOngoingRequests() == std::vector { *a }); // Our device only supports 1 simultaneous request, so this should fail to send const auto b = device.sendPropertyGetInquiry (inquiryMUID, header, [] (const PropertyExchangeResult&) {}); expect (! b.has_value()); expect (device.getOngoingRequests() == std::vector { *a }); // Reply to the first request device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertyGetDataResponse { { device.getIdForRequestKey (*a)->asByte(), successHeader, 1, 1, {} } }) }); // Now that a response to the first request has been received, there should be no // requests in progress. expect (device.getOngoingRequests().empty()); } output.messages.clear(); beginTest ("Aborting a property request sends a property notify"); { PropertyRequestHeader header; header.resource = "X-CustomProp"; bool callbackCalled = false; const auto a = device.sendPropertyGetInquiry (inquiryMUID, header, [&] (const PropertyExchangeResult&) { callbackCalled = true; }); expect (a.has_value()); expect (device.getOngoingRequests() == std::vector { *a }); expect (! callbackCalled); const auto requestID = device.getIdForRequestKey (*a); device.abortPropertyRequest (*a); expect (device.getOngoingRequests().empty()); expect (! callbackCalled); expect (output.messages.size() == 2); const auto inquiry = Parser::parse (output.messages.front().bytes); expect (inquiry.has_value()); expect (inquiry->header == Message::Header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }); const auto notify = Parser::parse (output.messages.back().bytes); expect (notify.has_value()); expect (notify->header == Message::Header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }); auto* body = std::get_if (¬ify->body); expect (body != nullptr); expect (body->requestID == requestID->asByte()); expect (body->thisChunkNum == 1); expect (body->totalNumChunks == 1); const auto replyHeader = Encodings::jsonFrom7BitText (body->header); expect (replyHeader.getProperty ("status", "") == var (144)); } output.messages.clear(); beginTest ("Aborting a completed property request does nothing"); { PropertyRequestHeader header; header.resource = "X-CustomProp"; const auto a = device.sendPropertyGetInquiry (inquiryMUID, header, [&] (const PropertyExchangeResult&) {}); expect (a.has_value()); expect (device.getOngoingRequests() == std::vector { *a }); // Reply to the get data request device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertyGetDataResponse { { device.getIdForRequestKey (*a)->asByte(), successHeader, 1, 1, {} } }) }); // After replying, there should be no ongoing requests expect (device.getOngoingRequests().empty()); expect (output.messages.size() == 1); // This request has already finished device.abortPropertyRequest (*a); expect (device.getOngoingRequests().empty()); expect (output.messages.size() == 1); } output.messages.clear(); beginTest ("Beginning a subscription and ending it before the remote device replies causes a property notify to be sent"); { PropertySubscriptionHeader header; header.command = PropertySubscriptionCommand::start; header.resource = "X-CustomProp"; const auto a = device.beginSubscription (inquiryMUID, header); expect (device.getOngoingSubscriptions() == std::vector { a }); // Sending a subscription request uses a request slot expect (device.getOngoingRequests().size() == 1); // subscription id is empty until the responder confirms the subscription expect (! device.getSubscribeIdForKey (a).has_value()); expect (device.getResourceForKey (a) == header.resource); expect (output.messages.size() == 1); { const auto parsed = Parser::parse (output.messages.back().bytes); expect (parsed.has_value()); expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }); auto* body = std::get_if (&parsed->body); expect (body != nullptr); const auto bodyHeader = Encodings::jsonFrom7BitText (body->header); expect (bodyHeader.getProperty ("command", "") == var ("start")); } output.messages.clear(); const auto requestID = device.getIdForRequestKey (device.getOngoingRequests().back()); device.endSubscription (a); expect (output.messages.size() == 1); { const auto parsed = Parser::parse (output.messages.back().bytes); expect (parsed.has_value()); expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }); auto* body = std::get_if (&parsed->body); expect (body != nullptr); const auto bodyHeader = Encodings::jsonFrom7BitText (body->header); expect (bodyHeader.getProperty ("status", "") == var (144)); } expect (device.getOngoingSubscriptions().empty()); // The start request is no longer in progress because it was terminated by the notify expect (device.getOngoingRequests().empty()); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribeResponse { { requestID->asByte(), successHeader, 1, 1, {} } }) }); expect (device.getOngoingRequests().empty()); output.messages.clear(); // There shouldn't be any queued messages. device.sendPendingMessages(); expect (output.messages.empty()); expect (device.getOngoingRequests().empty()); } output.messages.clear(); beginTest ("Starting a new subscription while the device is waiting for a previous subscription to be confirmed queues further requests"); { PropertySubscriptionHeader header; header.command = PropertySubscriptionCommand::start; header.resource = "X-CustomProp"; const auto a = device.beginSubscription (inquiryMUID, header); const auto b = device.beginSubscription (inquiryMUID, header); const auto c = device.beginSubscription (inquiryMUID, header); expect (device.getOngoingSubscriptions() == std::vector { a, b, c }); expect (device.getOngoingRequests().size() == 1); // subscription id is empty until the responder confirms the subscription expect (device.getResourceForKey (a) == header.resource); expect (device.getResourceForKey (b) == header.resource); expect (device.getResourceForKey (c) == header.resource); expect (output.messages.size() == 1); // The device has sent a subscription start for a, but not for c, // so it should send a notify to end subscription a, but shouldn't emit any // messages related to subscription c. device.endSubscription (a); device.endSubscription (c); expect (device.getOngoingSubscriptions() == std::vector { b }); expect (output.messages.size() == 2); expect (device.getOngoingRequests().empty()); // There should still be requests related to subscription b pending device.sendPendingMessages(); expect (output.messages.size() == 3); expect (device.getOngoingRequests().size() == 1); // Now, we should send a terminate request for subscription b device.endSubscription (b); expect (device.getOngoingSubscriptions().empty()); expect (output.messages.size() == 4); expect (device.getOngoingRequests().empty()); } output.messages.clear(); beginTest ("If the device receives a retry or notify in response to a subscription start request, the subscription is retried or terminated as necessary"); { PropertySubscriptionHeader header; header.command = PropertySubscriptionCommand::start; header.resource = "X-CustomProp"; const auto a = device.beginSubscription (inquiryMUID, header); expect (device.getOngoingSubscriptions() == std::vector { a }); expect (device.getOngoingRequests().size() == 1); expect (output.messages.size() == 1); const auto request0 = device.getOngoingRequests().back(); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribeResponse { { device.getIdForRequestKey (request0)->asByte(), retryHeader, 1, 1, {} } }) }); // The subscription is still active from the perspective of the device, but the // first request is over and should be retried expect (device.getOngoingSubscriptions() == std::vector { a }); expect (device.getOngoingRequests().empty()); expect (output.messages.size() == 1); device.sendPendingMessages(); expect (device.getOngoingSubscriptions() == std::vector { a }); expect (device.getOngoingRequests().size() == 1); expect (output.messages.size() == 2); const auto request1 = device.getOngoingRequests().back(); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertyNotify { { device.getIdForRequestKey (request1)->asByte(), cancelHeader, 1, 1, {} } }) }); expect (device.getOngoingSubscriptions().empty()); expect (device.getOngoingRequests().empty()); expect (output.messages.size() == 2); } beginTest ("If the device receives a retry or notify in response to a subscription end request, the subscription is retried as necessary"); { PropertySubscriptionHeader header; header.command = PropertySubscriptionCommand::start; header.resource = "X-CustomProp"; const auto a = device.beginSubscription (inquiryMUID, header); expect (device.getOngoingSubscriptions() == std::vector { a }); expect (device.getResourceForKey (a) == header.resource); const auto subscriptionResponseHeader = Encodings::jsonTo7BitText ([] { auto ptr = std::make_unique(); ptr->setProperty ("status", 200); ptr->setProperty ("subscribeId", "newId"); return ptr.release(); }()); // Accept the subscription device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribeResponse { { device.getIdForRequestKey (device.getOngoingRequests().back())->asByte(), subscriptionResponseHeader, 1, 1, {} } }) }); // The subscription is still active from the perspective of the device, but the // request is over and should be retried expect (device.getOngoingSubscriptions() == std::vector { a }); // Now that the subscription was accepted, the subscription id should be non-empty expect (device.getResourceForKey (a) == header.resource); expect (device.getSubscribeIdForKey (a) == "newId"); expect (device.getOngoingRequests().empty()); device.endSubscription (a); expect (device.getOngoingSubscriptions().empty()); expect (device.getOngoingRequests().size() == 1); // The responder is busy, can't process the subscription end device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribeResponse { { device.getIdForRequestKey (device.getOngoingRequests().back())->asByte(), retryHeader, 1, 1, {} } }) }); expect (device.getOngoingSubscriptions().empty()); expect (device.getOngoingRequests().empty()); device.sendPendingMessages(); expect (device.getOngoingSubscriptions().empty()); expect (device.getOngoingRequests().size() == 1); // The responder told us to immediately terminate our request to end the subscription! // It's unclear how this should behave, so we'll just ignore the failure and assume // the subscription is really over. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertyNotify { { device.getIdForRequestKey (device.getOngoingRequests().back())->asByte(), cancelHeader, 1, 1, {} } }) }); expect (device.getOngoingSubscriptions().empty()); expect (device.getOngoingRequests().empty()); output.messages.clear(); device.sendPendingMessages(); expect (device.getOngoingSubscriptions().empty()); expect (device.getOngoingRequests().empty()); expect (output.messages.empty()); } output.messages.clear(); const auto startResponseHeader = [&] { auto ptr = std::make_unique(); ptr->setProperty ("status", 200); ptr->setProperty ("subscribeId", "newId"); return Encodings::jsonTo7BitText (ptr.release()); }(); beginTest ("The responder can terminate a subscription"); { PropertySubscriptionHeader header; header.command = PropertySubscriptionCommand::start; header.resource = "X-CustomProp"; const auto a = device.beginSubscription (inquiryMUID, header); expect (device.getOngoingRequests().size() == 1); expect (device.getOngoingSubscriptions().size() == 1); expect (device.getResourceForKey (a) == "X-CustomProp"); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribeResponse { { device.getIdForRequestKey (device.getOngoingRequests().back())->asByte(), startResponseHeader, 1, 1, {} } }) }); expect (device.getOngoingRequests().empty()); expect (device.getOngoingSubscriptions().size() == 1); expect (output.messages.size() == 1); output.messages.clear(); expect (device.getResourceForKey (a) == "X-CustomProp"); expect (device.getSubscribeIdForKey (a) == "newId"); const auto endRequestHeader = [&] { auto ptr = std::make_unique(); ptr->setProperty ("command", "end"); ptr->setProperty ("subscribeId", "newId"); return Encodings::jsonTo7BitText (ptr.release()); }(); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribe { { std::byte { 0x42 }, endRequestHeader, 1, 1, {} } }) }); expect (device.getOngoingRequests().empty()); expect (device.getOngoingSubscriptions().empty()); expect (output.messages.size() == 1); { const auto parsed = Parser::parse (output.messages.back().bytes); expect (parsed.has_value()); expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, device.getMuid(), inquiryMUID }); auto* body = std::get_if (&parsed->body); expect (body != nullptr); const auto bodyHeader = Encodings::jsonFrom7BitText (body->header); expect (bodyHeader.getProperty ("status", "") == var (200)); } } beginTest ("Invalidating a MUID clears subscriptions to that MUID"); { PropertySubscriptionHeader header; header.command = PropertySubscriptionCommand::start; header.resource = "X-CustomProp"; const auto a = device.beginSubscription (inquiryMUID, header); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribeResponse { { device.getIdForRequestKey (device.getOngoingRequests().back())->asByte(), startResponseHeader, 1, 1, {} } }) }); expect (device.getOngoingSubscriptions() == std::vector { a }); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, MUID::getBroadcast() }, Message::InvalidateMUID { inquiryMUID }) }); expect (device.getOngoingSubscriptions().empty()); } beginTest ("Disconnecting and then connecting with the same MUID doesn't reuse SubscribeKeys"); { expect (device.getDiscoveredMuids().empty()); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, MUID::getBroadcast() }, Message::Discovery { {}, DeviceFeatures{}.withPropertyExchangeSupported (true).getSupportedCapabilities(), 512, {} }) }); expect (device.getDiscoveredMuids().size() == 1); PropertySubscriptionHeader header; header.command = PropertySubscriptionCommand::start; header.resource = "X-CustomProp"; const auto subscription = device.beginSubscription (inquiryMUID, header); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, device.getMuid() }, Message::PropertySubscribeResponse { { device.getIdForRequestKey (device.getOngoingRequests().back())->asByte(), startResponseHeader, 1, 1, {} } }) }); expect (device.getSubscribeIdForKey (subscription) == "newId"); expect (device.getResourceForKey (subscription) == "X-CustomProp"); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, MUID::getBroadcast() }, Message::InvalidateMUID { inquiryMUID }) }); expect (device.getDiscoveredMuids().empty()); device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock, detail::MessageMeta::Meta::subID2, detail::MessageMeta::implementationVersion, inquiryMUID, MUID::getBroadcast() }, Message::Discovery { {}, DeviceFeatures{}.withPropertyExchangeSupported (true).getSupportedCapabilities(), 512, {} }) }); expect (device.getDiscoveredMuids().size() == 1); const auto newSubscription = device.beginSubscription (inquiryMUID, header); expect (subscription != newSubscription); expect (device.getOngoingSubscriptions() == std::vector { newSubscription }); } } } private: template static std::vector getMessageBytes (const Message::Header& header, const Msg& body) { std::vector bytes; detail::Marshalling::Writer { bytes } (header, body); return bytes; } }; static DeviceTests deviceTests; #endif } // namespace juce::midi_ci