The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2370 lines
126KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce::midi_ci
  19. {
  20. class Device::Impl
  21. {
  22. template <typename This>
  23. static auto getProfileHostImpl (This& t) { return t.profileHost.has_value() ? &*t.profileHost : nullptr; }
  24. template <typename This>
  25. static auto getPropertyHostImpl (This& t) { return t.propertyHost.has_value() ? &*t.propertyHost : nullptr; }
  26. public:
  27. explicit Impl (const Options& opt)
  28. : options (getValidated (opt)),
  29. muid (getReallyRandomMuid())
  30. {
  31. if (options.getFeatures().isProfileConfigurationSupported())
  32. profileHost.emplace (options.getFunctionBlock(), profileDelegate, concreteBufferOutput);
  33. if (options.getFeatures().isPropertyExchangeSupported())
  34. propertyHost.emplace (options.getFunctionBlock(), propertyDelegate, concreteBufferOutput, cacheProvider);
  35. outgoing.reserve (options.getMaxSysExSize());
  36. }
  37. ~Impl()
  38. {
  39. if (concreteBufferOutput.hasSentMuid())
  40. {
  41. detail::MessageTypeUtils::send (concreteBufferOutput,
  42. options.getFunctionBlock().firstGroup,
  43. MUID::getBroadcast(),
  44. ChannelInGroup::wholeBlock,
  45. Message::InvalidateMUID { muid });
  46. }
  47. }
  48. void sendDiscovery()
  49. {
  50. {
  51. const auto aboutToRemove = std::move (discovered);
  52. for (const auto& pair : aboutToRemove)
  53. listeners.call ([&] (auto& l) { l.deviceRemoved (pair.first); });
  54. }
  55. const Message::Header header
  56. {
  57. ChannelInGroup::wholeBlock,
  58. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  59. detail::MessageMeta::implementationVersion,
  60. muid,
  61. MUID::getBroadcast(),
  62. };
  63. jassert (options.getOutputs().size() < 128);
  64. for (size_t i = 0; i < options.getOutputs().size(); ++i)
  65. {
  66. const Message::Discovery discovery
  67. {
  68. options.getDeviceInfo(),
  69. options.getFeatures().getSupportedCapabilities(),
  70. uint32_t (options.getMaxSysExSize()),
  71. std::byte (i % 128),
  72. };
  73. outgoing.clear();
  74. detail::Marshalling::Writer { outgoing } (header, discovery);
  75. options.getOutputs()[i]->processMessage ({ options.getFunctionBlock().firstGroup, outgoing });
  76. }
  77. }
  78. void sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint)
  79. {
  80. detail::MessageTypeUtils::send (concreteBufferOutput,
  81. options.getFunctionBlock().firstGroup,
  82. destination,
  83. ChannelInGroup::wholeBlock,
  84. endpoint);
  85. }
  86. void sendProfileInquiry (MUID receiver, ChannelInGroup address)
  87. {
  88. if (! supportsProfiles (receiver))
  89. return;
  90. detail::MessageTypeUtils::send (concreteBufferOutput,
  91. options.getFunctionBlock().firstGroup,
  92. receiver,
  93. address,
  94. Message::ProfileInquiry{});
  95. }
  96. void sendProfileDetailsInquiry (MUID receiver, ChannelInGroup address, Profile profile, std::byte target)
  97. {
  98. if (! supportsProfiles (receiver))
  99. return;
  100. detail::MessageTypeUtils::send (concreteBufferOutput,
  101. options.getFunctionBlock().firstGroup,
  102. receiver,
  103. address,
  104. Message::ProfileDetails { profile, target });
  105. }
  106. void sendProfileSpecificData (MUID receiver, ChannelInGroup address, Profile profile, Span<const std::byte> data)
  107. {
  108. if (! supportsProfiles (receiver))
  109. return;
  110. detail::MessageTypeUtils::send (concreteBufferOutput,
  111. options.getFunctionBlock().firstGroup,
  112. receiver,
  113. address,
  114. Message::ProfileSpecificData { profile, data });
  115. }
  116. void sendProfileEnablement (MUID m, ChannelInGroup address, Profile profile, int numChannels)
  117. {
  118. if (! supportsProfiles (m))
  119. return;
  120. // There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense!
  121. jassert (numChannels <= 256);
  122. if (numChannels > 0)
  123. {
  124. detail::MessageTypeUtils::send (concreteBufferOutput,
  125. options.getFunctionBlock().firstGroup,
  126. m,
  127. address,
  128. Message::ProfileOn { profile, (uint16_t) numChannels });
  129. }
  130. else
  131. {
  132. detail::MessageTypeUtils::send (concreteBufferOutput,
  133. options.getFunctionBlock().firstGroup,
  134. m,
  135. address,
  136. Message::ProfileOff { profile });
  137. }
  138. }
  139. void sendPropertyCapabilitiesInquiry (MUID m)
  140. {
  141. if (! supportsProperties (m))
  142. return;
  143. detail::MessageTypeUtils::send (concreteBufferOutput,
  144. options.getFunctionBlock().firstGroup,
  145. m,
  146. ChannelInGroup::wholeBlock,
  147. Message::PropertyExchangeCapabilities { std::byte { propertyDelegate.getNumSimultaneousRequestsSupported() }, {}, {} });
  148. }
  149. ErasedScopeGuard sendPropertyGetInquiry (MUID m,
  150. const PropertyRequestHeader& propertyHeader,
  151. std::function<void (const PropertyExchangeResult&)> callback)
  152. {
  153. const auto iter = discovered.find (m);
  154. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  155. return {};
  156. auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(),
  157. std::move (callback),
  158. detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m));
  159. if (! primed.isValid())
  160. return {};
  161. detail::MessageTypeUtils::send (concreteBufferOutput,
  162. options.getFunctionBlock().firstGroup,
  163. m,
  164. ChannelInGroup::wholeBlock,
  165. Message::PropertyGetData { { primed.id, Encodings::jsonTo7BitText (propertyHeader.toVarCondensed()) } });
  166. return std::move (primed.token);
  167. }
  168. void sendPropertySetInquiry (MUID m,
  169. const PropertyRequestHeader& propertyHeader,
  170. Span<const std::byte> propertyBody,
  171. std::function<void (const PropertyExchangeResult&)> callback)
  172. {
  173. const auto iter = discovered.find (m);
  174. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  175. return;
  176. const auto encoded = Encodings::tryEncode (propertyBody, propertyHeader.mutualEncoding);
  177. if (! encoded.has_value())
  178. {
  179. NullCheckedInvocation::invoke (callback, PropertyExchangeResult { PropertyExchangeResult::Error::invalidPayload });
  180. return;
  181. }
  182. auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(),
  183. std::move (callback),
  184. detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m));
  185. if (! primed.isValid())
  186. return;
  187. detail::PropertyHostUtils::send (concreteBufferOutput,
  188. options.getFunctionBlock().firstGroup,
  189. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  190. m,
  191. primed.id,
  192. Encodings::jsonTo7BitText (propertyHeader.toVarCondensed()),
  193. *encoded,
  194. cacheProvider.getMaxSysexSizeForMuid (m));
  195. }
  196. void sendPropertySubscriptionStart (MUID m,
  197. const PropertySubscriptionHeader& header,
  198. std::function<void (const PropertyExchangeResult&)> cb)
  199. {
  200. const auto resource = header.resource;
  201. auto wrappedCallback = [this, m, resource, callback = std::move (cb)] (const PropertyExchangeResult& result)
  202. {
  203. if (! result.getError().has_value())
  204. {
  205. const auto foundMuid = discovered.find (m);
  206. if (foundMuid != discovered.end())
  207. {
  208. const auto parsed = result.getHeaderAsSubscriptionHeader();
  209. // The responder should have given us a subscription ID so that we can reference the original subscription
  210. // whenever we get updates in the future, or if we want to end the subscription.
  211. jassert (parsed.subscribeId.isNotEmpty());
  212. const auto emplaceResult = foundMuid->second.subscriptions.insert ({ parsed.subscribeId, resource });
  213. // If this fails, the device gave us a subscribeId that it was already using for another subscription.
  214. jassertquiet (emplaceResult.second);
  215. }
  216. }
  217. NullCheckedInvocation::invoke (callback, result);
  218. };
  219. inquirePropertySubscribe (m, header, std::move (wrappedCallback));
  220. }
  221. void sendPropertySubscriptionEnd (MUID m,
  222. const String& subscribeId,
  223. std::function<void (const PropertyExchangeResult&)> cb)
  224. {
  225. const auto iter = discovered.find (m);
  226. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  227. {
  228. // Trying to send a subscription message to a device that doesn't exist (maybe it got removed), or
  229. // that doesn't support property exchange.
  230. jassertfalse;
  231. return;
  232. }
  233. if (iter->second.subscriptions.count ({ subscribeId, {} }) == 0)
  234. {
  235. // Trying to end a subscription that doesn't exist - perhaps it already ended.
  236. jassertfalse;
  237. return;
  238. }
  239. auto wrappedCallback = [this, m, subscribeId, callback = std::move (cb)] (const PropertyExchangeResult& result)
  240. {
  241. if (! result.getError().has_value())
  242. {
  243. const auto foundMuid = discovered.find (m);
  244. if (foundMuid != discovered.end())
  245. foundMuid->second.subscriptions.erase ({ subscribeId, {} });
  246. }
  247. NullCheckedInvocation::invoke (callback, result);
  248. };
  249. PropertySubscriptionHeader header;
  250. header.subscribeId = subscribeId;
  251. header.command = PropertySubscriptionCommand::end;
  252. inquirePropertySubscribe (m, header, std::move (wrappedCallback));
  253. }
  254. std::vector<Subscription> getOngoingSubscriptionsForMuid (MUID m) const
  255. {
  256. const auto iter = discovered.find (m);
  257. if (iter == discovered.end())
  258. return {};
  259. std::vector<Subscription> result;
  260. result.reserve (iter->second.subscriptions.size());
  261. for (const auto& [subscribeId, resource] : iter->second.subscriptions)
  262. result.push_back ({ subscribeId, resource });
  263. return result;
  264. }
  265. int countOngoingPropertyTransactions() const
  266. {
  267. return std::accumulate (discovered.begin(),
  268. discovered.end(),
  269. 0,
  270. [] (auto acc, const auto& pair)
  271. {
  272. return acc + pair.second.initiatorPropertyCaches.countOngoingTransactions();
  273. });
  274. }
  275. void processMessage (ump::BytesOnGroup msg)
  276. {
  277. // Queried before the property host to unconditionally register capabilities of property exchange hosts.
  278. FirstListener firstListener { this };
  279. LastListener lastListener { this };
  280. ResponderDelegate* const l[] { &firstListener,
  281. getProfileHostImpl (*this),
  282. getPropertyHostImpl (*this),
  283. &lastListener };
  284. const auto status = detail::Responder::processCompleteMessage (concreteBufferOutput, msg, l);
  285. if (status == Parser::Status::collidingMUID)
  286. {
  287. muid = getReallyRandomMuid();
  288. concreteBufferOutput.resetSentMuid();
  289. sendDiscovery();
  290. }
  291. }
  292. void addListener (Listener& l)
  293. {
  294. listeners.add (&l);
  295. }
  296. void removeListener (Listener& l)
  297. {
  298. listeners.remove (&l);
  299. }
  300. std::vector<MUID> getDiscoveredMuids() const
  301. {
  302. std::vector<MUID> result (discovered.size(), MUID::makeUnchecked (0));
  303. std::transform (discovered.begin(), discovered.end(), result.begin(), [] (const auto& p) { return p.first; });
  304. return result;
  305. }
  306. std::optional<Message::Discovery> getDiscoveryInfoForMuid (MUID m) const
  307. {
  308. const auto iter = discovered.find (m);
  309. return iter != discovered.end()
  310. ? std::optional<Message::Discovery> (iter->second.discovery)
  311. : std::nullopt;
  312. }
  313. std::optional<int> getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const
  314. {
  315. const auto iter = discovered.find (m);
  316. return iter != discovered.end()
  317. ? std::optional<int> ((int) iter->second.propertyExchangeResponse->numSimultaneousRequestsSupported)
  318. : std::nullopt;
  319. }
  320. const ChannelProfileStates* getProfileStateForMuid (MUID m, ChannelAddress address) const
  321. {
  322. const auto iter = discovered.find (m);
  323. return iter != discovered.end() ? iter->second.profileStates.getStateForDestination (address) : nullptr;
  324. }
  325. var getResourceListForMuid (MUID x) const
  326. {
  327. const auto iter = discovered.find (x);
  328. return iter != discovered.end() ? iter->second.resourceList : var();
  329. }
  330. var getDeviceInfoForMuid (MUID x) const
  331. {
  332. const auto iter = discovered.find (x);
  333. return iter != discovered.end() ? iter->second.deviceInfo : var();
  334. }
  335. var getChannelListForMuid (MUID x) const
  336. {
  337. const auto iter = discovered.find (x);
  338. return iter != discovered.end() ? iter->second.channelList : var();
  339. }
  340. MUID getMuid() const { return muid; }
  341. Options getOptions() const { return options; }
  342. ProfileHost* getProfileHost() { return getProfileHostImpl (*this); }
  343. const ProfileHost* getProfileHost() const { return getProfileHostImpl (*this); }
  344. PropertyHost* getPropertyHost() { return getPropertyHostImpl (*this); }
  345. const PropertyHost* getPropertyHost() const { return getPropertyHostImpl (*this); }
  346. private:
  347. class FirstListener : public ResponderDelegate
  348. {
  349. public:
  350. explicit FirstListener (Impl* d) : device (d) {}
  351. bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override
  352. {
  353. detail::MessageTypeUtils::visit (message, Visitor { device, &output });
  354. return false;
  355. }
  356. private:
  357. class Visitor : public detail::MessageTypeUtils::MessageVisitor
  358. {
  359. public:
  360. Visitor (Impl* d, ResponderOutput* o)
  361. : device (d), output (o) {}
  362. void visit (const Message::PropertyExchangeCapabilities& caps) const override { visitImpl (caps); }
  363. void visit (const Message::PropertyExchangeCapabilitiesResponse& caps) const override { visitImpl (caps); }
  364. using MessageVisitor::visit;
  365. private:
  366. template <typename Body>
  367. void visitImpl (const Body& t) const
  368. {
  369. const auto responderMUID = output->getIncomingHeader().source;
  370. const auto iter = device->discovered.find (responderMUID);
  371. if (iter == device->discovered.end())
  372. return;
  373. iter->second.propertyExchangeResponse = Message::PropertyExchangeCapabilitiesResponse { t.numSimultaneousRequestsSupported,
  374. t.majorVersion,
  375. t.minorVersion };
  376. }
  377. Impl* device = nullptr;
  378. ResponderOutput* output = nullptr;
  379. };
  380. Impl* device = nullptr;
  381. };
  382. class LastListener : public ResponderDelegate
  383. {
  384. public:
  385. explicit LastListener (Impl* d) : device (d) {}
  386. bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override
  387. {
  388. bool result = false;
  389. detail::MessageTypeUtils::visit (message, Visitor { device, &output, &result });
  390. return result;
  391. }
  392. private:
  393. class Visitor : public detail::MessageTypeUtils::MessageVisitor
  394. {
  395. public:
  396. Visitor (Impl* d, ResponderOutput* o, bool* b)
  397. : device (d), output (o), handled (b) {}
  398. void visit (const Message::Discovery& x) const override { visitImpl (x); }
  399. void visit (const Message::DiscoveryResponse& x) const override { visitImpl (x); }
  400. void visit (const Message::InvalidateMUID& x) const override { visitImpl (x); }
  401. void visit (const Message::EndpointInquiry& x) const override { visitImpl (x); }
  402. void visit (const Message::EndpointInquiryResponse& x) const override { visitImpl (x); }
  403. void visit (const Message::NAK& x) const override { visitImpl (x); }
  404. void visit (const Message::ProfileInquiryResponse& x) const override { visitImpl (x); }
  405. void visit (const Message::ProfileAdded& x) const override { visitImpl (x); }
  406. void visit (const Message::ProfileRemoved& x) const override { visitImpl (x); }
  407. void visit (const Message::ProfileEnabledReport& x) const override { visitImpl (x); }
  408. void visit (const Message::ProfileDisabledReport& x) const override { visitImpl (x); }
  409. void visit (const Message::ProfileDetailsResponse& x) const override { visitImpl (x); }
  410. void visit (const Message::ProfileSpecificData& x) const override { visitImpl (x); }
  411. void visit (const Message::PropertyExchangeCapabilitiesResponse& x) const override { visitImpl (x); }
  412. void visit (const Message::PropertyGetDataResponse& x) const override { visitImpl (x); }
  413. void visit (const Message::PropertySetDataResponse& x) const override { visitImpl (x); }
  414. void visit (const Message::PropertySubscribe& x) const override { visitImpl (x); }
  415. void visit (const Message::PropertySubscribeResponse& x) const override { visitImpl (x); }
  416. void visit (const Message::PropertyNotify& x) const override { visitImpl (x); }
  417. using MessageVisitor::visit;
  418. private:
  419. template <typename Body>
  420. void visitImpl (const Body& body) const { *handled = messageReceived (body); }
  421. bool messageReceived (const Message::Discovery& body) const
  422. {
  423. const auto replyPath = uint8_t (output->getIncomingHeader().version) >= 0x02 ? body.outputPathID : std::byte { 0x00 };
  424. detail::MessageTypeUtils::send (*output, Message::DiscoveryResponse
  425. {
  426. device->options.getDeviceInfo(),
  427. device->options.getFeatures().getSupportedCapabilities(),
  428. uint32_t (device->options.getMaxSysExSize()),
  429. replyPath,
  430. device->options.getFunctionBlock().identifier,
  431. });
  432. // TODO(reuk) rather than sending a new discovery inquiry, we should store the details from the incoming message
  433. const auto iter = device->discovered.find (output->getIncomingHeader().source);
  434. if (iter == device->discovered.end())
  435. {
  436. const auto initiator = output->getIncomingHeader().source;
  437. device->discovered.emplace (initiator, Discovered { body });
  438. device->listeners.call ([&] (auto& l) { l.deviceAdded (initiator); });
  439. device->sendEndpointInquiry (initiator, Message::EndpointInquiry { std::byte{} });
  440. }
  441. return true;
  442. }
  443. bool messageReceived (const Message::DiscoveryResponse& response) const
  444. {
  445. const auto responderMUID = output->getIncomingHeader().source;
  446. const auto iter = device->discovered.find (responderMUID);
  447. if (iter != device->discovered.end())
  448. {
  449. device->discovered.erase (iter);
  450. device->listeners.call ([&] (auto& l) { l.deviceRemoved (responderMUID); });
  451. const Message::Header header
  452. {
  453. ChannelInGroup::wholeBlock,
  454. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  455. detail::MessageMeta::implementationVersion,
  456. device->muid,
  457. MUID::getBroadcast(),
  458. };
  459. detail::MessageTypeUtils::send (*output, output->getIncomingGroup(), header, Message::InvalidateMUID { responderMUID });
  460. }
  461. else
  462. {
  463. const Message::Discovery discovery { response.device,
  464. response.capabilities,
  465. response.maximumSysexSize,
  466. response.outputPathID };
  467. device->discovered.emplace (responderMUID, Discovered { discovery });
  468. device->listeners.call ([&] (auto& l) { l.deviceAdded (responderMUID); });
  469. device->sendEndpointInquiry (output->getIncomingHeader().source, Message::EndpointInquiry { std::byte{} });
  470. }
  471. return true;
  472. }
  473. bool messageReceived (const Message::InvalidateMUID& invalidate) const
  474. {
  475. const auto targetMuid = invalidate.target;
  476. const auto iter = device->discovered.find (targetMuid);
  477. if (iter != device->discovered.end())
  478. {
  479. device->discovered.erase (iter);
  480. device->listeners.call ([&] (auto& l) { l.deviceRemoved (targetMuid); });
  481. }
  482. if (invalidate.target != device->muid)
  483. return false;
  484. device->muid = getReallyRandomMuid();
  485. device->concreteBufferOutput.resetSentMuid();
  486. device->sendDiscovery();
  487. return true;
  488. }
  489. bool messageReceived (const Message::EndpointInquiry& endpoint) const
  490. {
  491. // Only status 0 is defined at time of writing
  492. if (endpoint.status == std::byte{})
  493. {
  494. const auto& id = device->options.getProductInstanceId();
  495. const auto length = std::distance (id.begin(), std::find (id.begin(), id.end(), 0));
  496. if (length <= 0)
  497. return false;
  498. Message::EndpointInquiryResponse response;
  499. response.status = endpoint.status;
  500. response.data = Span<const std::byte> (reinterpret_cast<const std::byte*> (id.data()), (size_t) length);
  501. detail::MessageTypeUtils::send (*output, response);
  502. return true;
  503. }
  504. return false;
  505. }
  506. bool messageReceived (const Message::EndpointInquiryResponse& endpoint) const
  507. {
  508. const auto responderMUID = output->getIncomingHeader().source;
  509. const auto iter = device->discovered.find (responderMUID);
  510. if (iter == device->discovered.end())
  511. return false; // Got an endpoint response for a device we haven't discovered
  512. device->listeners.call ([&] (auto& l) { l.endpointReceived (responderMUID, endpoint); });
  513. return true;
  514. }
  515. bool messageReceived (const Message::NAK& nak) const
  516. {
  517. const auto responderMUID = output->getIncomingHeader().source;
  518. device->listeners.call ([&] (auto& l) { l.messageNotAcknowledged (responderMUID, nak); });
  519. return true;
  520. }
  521. bool messageReceived (const Message::ProfileInquiryResponse& response) const
  522. {
  523. const auto responderMUID = output->getIncomingHeader().source;
  524. const auto iter = device->discovered.find (responderMUID);
  525. if (iter == device->discovered.end())
  526. return false;
  527. const auto destination = output->getIncomingHeader().deviceID;
  528. auto* state = iter->second.profileStates.getStateForDestination (output->getChannelAddress());
  529. if (state == nullptr)
  530. return false;
  531. ChannelProfileStates newState;
  532. for (auto& enabled : response.enabledProfiles)
  533. newState.set (enabled, { 1, 1 });
  534. for (auto& disabled : response.disabledProfiles)
  535. newState.set (disabled, { 1, 0 });
  536. *state = newState;
  537. device->listeners.call ([&] (auto& l) { l.profileStateReceived (responderMUID, destination); });
  538. return true;
  539. }
  540. bool messageReceived (const Message::ProfileAdded& added) const
  541. {
  542. const auto responderMUID = output->getIncomingHeader().source;
  543. const auto iter = device->discovered.find (responderMUID);
  544. if (iter == device->discovered.end())
  545. return false;
  546. const auto address = output->getChannelAddress();
  547. auto* state = iter->second.profileStates.getStateForDestination (address);
  548. if (state == nullptr)
  549. return false;
  550. state->set (added.profile, { 1, 0 });
  551. device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), added.profile, true); });
  552. return true;
  553. }
  554. bool messageReceived (const Message::ProfileRemoved& removed) const
  555. {
  556. const auto responderMUID = output->getIncomingHeader().source;
  557. const auto iter = device->discovered.find (responderMUID);
  558. if (iter == device->discovered.end())
  559. return false;
  560. const auto address = output->getChannelAddress();
  561. auto* state = iter->second.profileStates.getStateForDestination (address);
  562. if (state == nullptr)
  563. return false;
  564. state->erase (removed.profile);
  565. device->listeners.call ([&] (auto& l) { l.profilePresenceChanged (responderMUID, address.getChannel(), removed.profile, false); });
  566. return true;
  567. }
  568. bool messageReceived (const Message::ProfileEnabledReport& x) const
  569. {
  570. const auto responderMUID = output->getIncomingHeader().source;
  571. const auto iter = device->discovered.find (responderMUID);
  572. if (iter == device->discovered.end())
  573. return false;
  574. const auto address = output->getChannelAddress();
  575. auto* state = iter->second.profileStates.getStateForDestination (address);
  576. if (state == nullptr)
  577. return false;
  578. const auto numChannels = jmax ((uint16_t) 1, x.numChannels);
  579. state->set (x.profile, { state->get (x.profile).supported, numChannels });
  580. device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, numChannels); });
  581. return true;
  582. }
  583. bool messageReceived (const Message::ProfileDisabledReport& x) const
  584. {
  585. const auto responderMUID = output->getIncomingHeader().source;
  586. const auto iter = device->discovered.find (responderMUID);
  587. if (iter == device->discovered.end())
  588. return false;
  589. const auto address = output->getChannelAddress();
  590. auto* state = iter->second.profileStates.getStateForDestination (address);
  591. if (state == nullptr)
  592. return false;
  593. state->set (x.profile, { state->get (x.profile).supported, 0 });
  594. device->listeners.call ([&] (auto& l) { l.profileEnablementChanged (responderMUID, address.getChannel(), x.profile, 0); });
  595. return true;
  596. }
  597. bool messageReceived (const Message::ProfileDetailsResponse& response) const
  598. {
  599. const auto responderMUID = output->getIncomingHeader().source;
  600. const auto destination = output->getIncomingHeader().deviceID;
  601. device->listeners.call ([&] (auto& l) { l.profileDetailsReceived (responderMUID, destination, response.profile, response.target, response.data); });
  602. return true;
  603. }
  604. bool messageReceived (const Message::ProfileSpecificData& data) const
  605. {
  606. const auto responderMUID = output->getIncomingHeader().source;
  607. const auto destination = output->getIncomingHeader().deviceID;
  608. device->listeners.call ([&] (auto& l) { l.profileSpecificDataReceived (responderMUID, destination, data.profile, data.data); });
  609. return true;
  610. }
  611. bool messageReceived (const Message::PropertyExchangeCapabilitiesResponse&) const
  612. {
  613. const auto source = output->getIncomingHeader().source;
  614. const auto iter = device->discovered.find (source);
  615. constexpr auto hasResource = [] (var obj, auto resource)
  616. {
  617. if (auto* array = obj.getArray())
  618. for (const auto& item : *array)
  619. if (item.isObject() && item.getProperty ("resource", {}) == var (resource))
  620. return true;
  621. return false;
  622. };
  623. const auto transaction = device->ongoingTransactions.emplace (device->ongoingTransactions.end());
  624. const auto onResourceListReceived = [this, iter, source, hasResource, transaction] (const PropertyExchangeResult& result)
  625. {
  626. const auto validateResponse = [] (const PropertyExchangeResult& r)
  627. {
  628. const auto parsed = r.getHeaderAsReplyHeader();
  629. return ! r.getError().has_value()
  630. && parsed.mediaType == PropertySubscriptionHeader().mediaType
  631. && parsed.status == 200;
  632. };
  633. const auto allDone = [this, source, transaction]
  634. {
  635. device->ongoingTransactions.erase (transaction);
  636. device->listeners.call ([source] (auto& l) { l.propertyExchangeCapabilitiesReceived (source); });
  637. };
  638. if (! validateResponse (result))
  639. {
  640. jassertfalse;
  641. allDone();
  642. return;
  643. }
  644. const auto bodyAsObj = Encodings::jsonFrom7BitText (result.getBody());
  645. iter->second.resourceList = bodyAsObj;
  646. const auto onChannelListReceived = [iter, allDone, validateResponse] (const PropertyExchangeResult& r)
  647. {
  648. if (validateResponse (r))
  649. iter->second.channelList = Encodings::jsonFrom7BitText (r.getBody());
  650. allDone();
  651. return;
  652. };
  653. const auto getChannelList = [this, bodyAsObj, source, allDone, hasResource, onChannelListReceived, transaction]
  654. {
  655. if (hasResource (bodyAsObj, "ChannelList"))
  656. {
  657. PropertyRequestHeader header;
  658. header.resource = "ChannelList";
  659. *transaction = device->sendPropertyGetInquiry (source, header, onChannelListReceived);
  660. return;
  661. }
  662. allDone();
  663. return;
  664. };
  665. if (hasResource (bodyAsObj, "DeviceInfo"))
  666. {
  667. PropertyRequestHeader header;
  668. header.resource = "DeviceInfo";
  669. *transaction = device->sendPropertyGetInquiry (source,
  670. header,
  671. [iter, getChannelList, validateResponse] (const PropertyExchangeResult& r)
  672. {
  673. if (validateResponse (r))
  674. iter->second.deviceInfo = Encodings::jsonFrom7BitText (r.getBody());
  675. getChannelList();
  676. });
  677. return;
  678. }
  679. return getChannelList();
  680. };
  681. PropertyRequestHeader header;
  682. header.resource = "ResourceList";
  683. *transaction = device->sendPropertyGetInquiry (source, header, onResourceListReceived);
  684. return true;
  685. }
  686. bool handlePropertyDataResponse (const Message::DynamicSizePropertyExchange& response) const
  687. {
  688. const auto responderMUID = output->getIncomingHeader().source;
  689. const auto iter = device->discovered.find (responderMUID);
  690. if (iter == device->discovered.end())
  691. return false;
  692. iter->second.initiatorPropertyCaches.addChunk (response.requestID, response);
  693. return true;
  694. }
  695. bool messageReceived (const Message::PropertyGetDataResponse& response) const
  696. {
  697. handlePropertyDataResponse (response);
  698. return true;
  699. }
  700. bool messageReceived (const Message::PropertySetDataResponse& response) const
  701. {
  702. handlePropertyDataResponse (Message::DynamicSizePropertyExchange { response.requestID,
  703. response.header,
  704. 1,
  705. 1,
  706. {} });
  707. return true;
  708. }
  709. bool messageReceived (const Message::PropertySubscribe& subscription) const
  710. {
  711. const auto responderMUID = output->getIncomingHeader().source;
  712. const auto iter = device->discovered.find (responderMUID);
  713. if (iter == device->discovered.end())
  714. return false;
  715. const auto request = subscription.requestID;
  716. const auto source = output->getIncomingHeader().source;
  717. const auto jsonHeader = Encodings::jsonFrom7BitText (subscription.header);
  718. const auto typedHeader = PropertySubscriptionHeader::parseCondensed (jsonHeader);
  719. const auto subscribeId = typedHeader.subscribeId;
  720. const auto callback = [this, request, source, subscribeId] (const PropertyExchangeResult& result)
  721. {
  722. if (result.getError().has_value())
  723. return;
  724. PropertySubscriptionData data;
  725. data.header = result.getHeaderAsSubscriptionHeader();
  726. data.body = result.getBody();
  727. if (data.header.command == PropertySubscriptionCommand::end)
  728. {
  729. const auto foundMuid = device->discovered.find (source);
  730. if (foundMuid != device->discovered.end())
  731. foundMuid->second.subscriptions.erase ({ data.header.subscribeId, {} });
  732. }
  733. if (data.header.command != PropertySubscriptionCommand::start)
  734. device->listeners.call ([source, &data] (auto& l) { l.propertySubscriptionDataReceived (source, data); });
  735. PropertyReplyHeader header;
  736. header.extended["subscribeId"] = subscribeId;
  737. const auto headerBytes = Encodings::jsonTo7BitText (header.toVarCondensed());
  738. detail::MessageTypeUtils::send (device->concreteBufferOutput,
  739. device->options.getFunctionBlock().firstGroup,
  740. source,
  741. ChannelInGroup::wholeBlock,
  742. Message::PropertySubscribeResponse { { request, headerBytes, 1, 1, {} } });
  743. };
  744. // Subscription events may be sent at any time by the responder, so there may not be
  745. // an existing transaction ID for new subscription messages.
  746. iter->second.responderPropertyCaches.primeCache (device->propertyDelegate.getNumSimultaneousRequestsSupported(),
  747. callback,
  748. subscription.requestID);
  749. iter->second.responderPropertyCaches.addChunk (subscription.requestID, subscription);
  750. return true;
  751. }
  752. bool messageReceived (const Message::PropertySubscribeResponse& response) const
  753. {
  754. handlePropertyDataResponse (response);
  755. return true;
  756. }
  757. bool messageReceived (const Message::PropertyNotify& notify) const
  758. {
  759. const auto responderMUID = output->getIncomingHeader().source;
  760. const auto iter = device->discovered.find (responderMUID);
  761. if (iter == device->discovered.end())
  762. return false;
  763. iter->second.initiatorPropertyCaches.notify (notify.requestID, notify.header);
  764. iter->second.responderPropertyCaches.notify (notify.requestID, notify.header);
  765. return true;
  766. }
  767. Impl* device = nullptr;
  768. ResponderOutput* output = nullptr;
  769. bool* handled = nullptr;
  770. };
  771. Impl* device = nullptr;
  772. };
  773. struct Discovered
  774. {
  775. explicit Discovered (Message::Discovery r) : discovery (r) {}
  776. Message::Discovery discovery;
  777. std::optional<Message::PropertyExchangeCapabilitiesResponse> propertyExchangeResponse;
  778. BlockProfileStates profileStates;
  779. InitiatorPropertyExchangeCache initiatorPropertyCaches;
  780. ResponderPropertyExchangeCache responderPropertyCaches;
  781. var resourceList, deviceInfo, channelList;
  782. std::set<Subscription> subscriptions; ///< subscribeIds of subscriptions that we initiated
  783. };
  784. class ConcreteBufferOutput : public BufferOutput
  785. {
  786. public:
  787. explicit ConcreteBufferOutput (Impl& d) : device (d) {}
  788. MUID getMuid() const override { return device.muid; }
  789. std::vector<std::byte>& getOutputBuffer() override { return device.outgoing; }
  790. void send (uint8_t group) override
  791. {
  792. sentMuid = true;
  793. for (auto* o : device.options.getOutputs())
  794. o->processMessage ({ group, getOutputBuffer() });
  795. }
  796. bool hasSentMuid() const { return sentMuid; }
  797. void resetSentMuid() { sentMuid = false; }
  798. private:
  799. Impl& device;
  800. bool sentMuid = false;
  801. };
  802. class CacheProviderImpl : public CacheProvider
  803. {
  804. public:
  805. explicit CacheProviderImpl (Impl& d) : device (d) {}
  806. std::set<MUID> getDiscoveredMuids() const override
  807. {
  808. std::set<MUID> result;
  809. for (const auto& d : device.discovered)
  810. result.insert (d.first);
  811. return result;
  812. }
  813. InitiatorPropertyExchangeCache* getCacheForMuidAsInitiator (MUID m) override
  814. {
  815. const auto iter = device.discovered.find (m);
  816. return iter != device.discovered.end() ? &iter->second.initiatorPropertyCaches : nullptr;
  817. }
  818. ResponderPropertyExchangeCache* getCacheForMuidAsResponder (MUID m) override
  819. {
  820. const auto iter = device.discovered.find (m);
  821. return iter != device.discovered.end() ? &iter->second.responderPropertyCaches : nullptr;
  822. }
  823. int getMaxSysexSizeForMuid (MUID m) const override
  824. {
  825. constexpr auto defaultResult = 1 << 16;
  826. const auto iter = device.discovered.find (m);
  827. return iter != device.discovered.end() ? jmin (defaultResult, (int) iter->second.discovery.maximumSysexSize) : defaultResult;
  828. }
  829. public:
  830. Impl& device;
  831. };
  832. class ProfileDelegateImpl : public ProfileDelegate
  833. {
  834. public:
  835. explicit ProfileDelegateImpl (Impl& d) : device (d) {}
  836. void profileEnablementRequested (MUID x, ProfileAtAddress profileAtAddress, int numChannels, bool enabled) override
  837. {
  838. if (auto* d = device.options.getProfileDelegate())
  839. return d->profileEnablementRequested (x, profileAtAddress, numChannels, enabled);
  840. if (! device.profileHost.has_value())
  841. return;
  842. if (enabled)
  843. device.profileHost->enableProfile (profileAtAddress, numChannels);
  844. else
  845. device.profileHost->disableProfile (profileAtAddress);
  846. }
  847. public:
  848. Impl& device;
  849. };
  850. class PropertyDelegateImpl : public PropertyDelegate
  851. {
  852. public:
  853. explicit PropertyDelegateImpl (Impl& d) : device (d) {}
  854. uint8_t getNumSimultaneousRequestsSupported() const override
  855. {
  856. if (auto* d = device.options.getPropertyDelegate())
  857. return d->getNumSimultaneousRequestsSupported();
  858. return 127;
  859. }
  860. PropertyReplyData propertyGetDataRequested (MUID m, const PropertyRequestHeader& header) override
  861. {
  862. if (auto* d = device.options.getPropertyDelegate())
  863. return d->propertyGetDataRequested (m, header);
  864. PropertyReplyData result;
  865. result.header.status = 404; // Resource not found, do not retry
  866. result.header.message = TRANS ("Handling for \"Inquiry: Get Property Data\" is not implemented.");
  867. return result;
  868. }
  869. PropertyReplyHeader propertySetDataRequested (MUID m, const PropertyRequestData& data) override
  870. {
  871. if (auto* d = device.options.getPropertyDelegate())
  872. return d->propertySetDataRequested (m, data);
  873. PropertyReplyHeader result;
  874. result.status = 404; // Resource not found, do not retry
  875. result.message = TRANS ("Handling for \"Inquiry: Set Property Data\" is not implemented.");
  876. return result;
  877. }
  878. bool subscriptionStartRequested (MUID m, const PropertySubscriptionHeader& data) override
  879. {
  880. if (auto* d = device.options.getPropertyDelegate())
  881. return d->subscriptionStartRequested (m, data);
  882. return false;
  883. }
  884. void subscriptionDidStart (MUID m, const String& id, const PropertySubscriptionHeader& data) override
  885. {
  886. if (auto* d = device.options.getPropertyDelegate())
  887. d->subscriptionDidStart (m, id, data);
  888. }
  889. void subscriptionWillEnd (MUID m, const ci::Subscription& subscription) override
  890. {
  891. if (auto* d = device.options.getPropertyDelegate())
  892. d->subscriptionWillEnd (m, subscription);
  893. }
  894. public:
  895. Impl& device;
  896. };
  897. static MUID getReallyRandomMuid()
  898. {
  899. Random random;
  900. random.setSeedRandomly();
  901. return MUID::makeRandom (random);
  902. }
  903. static DeviceOptions getValidated (DeviceOptions opt)
  904. {
  905. opt = opt.withMaxSysExSize (jmax ((size_t) 128, opt.getMaxSysExSize()));
  906. if (opt.getFeatures().isPropertyExchangeSupported())
  907. opt = opt.withMaxSysExSize (jmax ((size_t) 512, opt.getMaxSysExSize()));
  908. opt = opt.withFeatures (opt.getFeatures().withProcessInquirySupported (false));
  909. // You'll need to provide some outputs if you want the device to talk to the outside world!
  910. jassert (! opt.getOutputs().empty());
  911. return opt;
  912. }
  913. template <typename Member>
  914. bool supportsFlag (MUID m, Member member) const
  915. {
  916. const auto iter = discovered.find (m);
  917. return iter != discovered.end() && (Features (iter->second.discovery.capabilities).*member)();
  918. }
  919. bool supportsProfiles (MUID m) const
  920. {
  921. return supportsFlag (m, &Features::isProfileConfigurationSupported);
  922. }
  923. bool supportsProperties (MUID m) const
  924. {
  925. return supportsFlag (m, &Features::isPropertyExchangeSupported);
  926. }
  927. void inquirePropertySubscribe (MUID m,
  928. const PropertySubscriptionHeader& header,
  929. std::function<void (const PropertyExchangeResult&)> cb)
  930. {
  931. const auto iter = discovered.find (m);
  932. if (iter == discovered.end() || ! Features { iter->second.discovery.capabilities }.isPropertyExchangeSupported())
  933. {
  934. // Trying to send a subscription message to a device that doesn't exist (maybe it got removed), or
  935. // that doesn't support property exchange.
  936. jassertfalse;
  937. return;
  938. }
  939. auto primed = iter->second.initiatorPropertyCaches.primeCache (propertyDelegate.getNumSimultaneousRequestsSupported(),
  940. std::move (cb),
  941. detail::PropertyHostUtils::getTerminator (concreteBufferOutput, options.getFunctionBlock(), m));
  942. if (! primed.isValid())
  943. return;
  944. detail::PropertyHostUtils::send (concreteBufferOutput,
  945. options.getFunctionBlock().firstGroup,
  946. detail::MessageMeta::Meta<Message::PropertySubscribe>::subID2,
  947. m,
  948. primed.id,
  949. Encodings::jsonTo7BitText (header.toVarCondensed()),
  950. {},
  951. cacheProvider.getMaxSysexSizeForMuid (m));
  952. }
  953. DeviceOptions options;
  954. MUID muid;
  955. std::vector<std::byte> outgoing;
  956. std::map<MUID, Discovered> discovered;
  957. ListenerList<Listener> listeners;
  958. ConcreteBufferOutput concreteBufferOutput { *this };
  959. CacheProviderImpl cacheProvider { *this };
  960. ProfileDelegateImpl profileDelegate { *this };
  961. PropertyDelegateImpl propertyDelegate { *this };
  962. std::optional<ProfileHost> profileHost;
  963. std::optional<PropertyHost> propertyHost;
  964. std::list<ErasedScopeGuard> ongoingTransactions;
  965. };
  966. //==============================================================================
  967. Device::Device (const Options& opt) : pimpl (std::make_unique<Impl> (opt)) {}
  968. Device::~Device() = default;
  969. Device::Device (Device&&) noexcept = default;
  970. Device& Device::operator= (Device&&) noexcept = default;
  971. void Device::processMessage (ump::BytesOnGroup msg) { pimpl->processMessage (msg); }
  972. void Device::sendDiscovery() { pimpl->sendDiscovery(); }
  973. void Device::sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint) { pimpl->sendEndpointInquiry (destination, endpoint); }
  974. void Device::sendProfileInquiry (MUID destination, ChannelInGroup address) { pimpl->sendProfileInquiry (destination, address); }
  975. void Device::sendProfileDetailsInquiry (MUID destination, ChannelInGroup address, Profile profile, std::byte target)
  976. {
  977. pimpl->sendProfileDetailsInquiry (destination, address, profile, target);
  978. }
  979. void Device::sendProfileSpecificData (MUID destination, ChannelInGroup address, Profile profile, Span<const std::byte> data)
  980. {
  981. pimpl->sendProfileSpecificData (destination, address, profile, data);
  982. }
  983. void Device::sendProfileEnablement (MUID destination, ChannelInGroup address, Profile profile, int numChannels)
  984. {
  985. pimpl->sendProfileEnablement (destination, address, profile, numChannels);
  986. }
  987. void Device::sendPropertyCapabilitiesInquiry (MUID destination)
  988. {
  989. pimpl->sendPropertyCapabilitiesInquiry (destination);
  990. }
  991. ErasedScopeGuard Device::sendPropertyGetInquiry (MUID destination,
  992. const PropertyRequestHeader& header,
  993. std::function<void (const PropertyExchangeResult&)> onResult)
  994. {
  995. return pimpl->sendPropertyGetInquiry (destination, header, std::move (onResult));
  996. }
  997. void Device::sendPropertySetInquiry (MUID destination,
  998. const PropertyRequestHeader& header,
  999. Span<const std::byte> body,
  1000. std::function<void (const PropertyExchangeResult&)> onResult)
  1001. {
  1002. pimpl->sendPropertySetInquiry (destination, header, body, std::move (onResult));
  1003. }
  1004. void Device::sendPropertySubscriptionStart (MUID destination,
  1005. const PropertySubscriptionHeader& header,
  1006. std::function<void (const PropertyExchangeResult&)> onResult)
  1007. {
  1008. pimpl->sendPropertySubscriptionStart (destination, header, std::move (onResult));
  1009. }
  1010. void Device::sendPropertySubscriptionEnd (MUID destination,
  1011. const String& subscribeId,
  1012. std::function<void (const PropertyExchangeResult&)> onResult)
  1013. {
  1014. pimpl->sendPropertySubscriptionEnd (destination, subscribeId, std::move (onResult));
  1015. }
  1016. std::vector<Subscription> Device::getOngoingSubscriptionsForMuid (MUID m) const { return pimpl->getOngoingSubscriptionsForMuid (m); }
  1017. int Device::countOngoingPropertyTransactions() const { return pimpl->countOngoingPropertyTransactions(); }
  1018. void Device::addListener (Listener& l) { pimpl->addListener (l); }
  1019. void Device::removeListener (Listener& l) { pimpl->removeListener (l); }
  1020. MUID Device::getMuid() const { return pimpl->getMuid(); }
  1021. DeviceOptions Device::getOptions() const { return pimpl->getOptions(); }
  1022. std::vector<MUID> Device::getDiscoveredMuids() const { return pimpl->getDiscoveredMuids(); }
  1023. const ProfileHost* Device::getProfileHost() const { return pimpl->getProfileHost(); }
  1024. ProfileHost* Device::getProfileHost() { return pimpl->getProfileHost(); }
  1025. const PropertyHost* Device::getPropertyHost() const { return pimpl->getPropertyHost(); }
  1026. PropertyHost* Device::getPropertyHost() { return pimpl->getPropertyHost(); }
  1027. std::optional<Message::Discovery> Device::getDiscoveryInfoForMuid (MUID m) const { return pimpl->getDiscoveryInfoForMuid (m); }
  1028. const ChannelProfileStates* Device::getProfileStateForMuid (MUID m, ChannelAddress address) const { return pimpl->getProfileStateForMuid (m, address); }
  1029. std::optional<int> Device::getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const
  1030. {
  1031. return pimpl->getNumPropertyExchangeRequestsSupportedForMuid (m);
  1032. }
  1033. var Device::getResourceListForMuid (MUID x) const { return pimpl->getResourceListForMuid (x); }
  1034. var Device::getDeviceInfoForMuid (MUID x) const { return pimpl->getDeviceInfoForMuid (x); }
  1035. var Device::getChannelListForMuid (MUID x) const { return pimpl->getChannelListForMuid (x); }
  1036. //==============================================================================
  1037. //==============================================================================
  1038. #if JUCE_UNIT_TESTS
  1039. class DeviceTests : public UnitTest
  1040. {
  1041. public:
  1042. DeviceTests() : UnitTest ("Device", UnitTestCategories::midi) {}
  1043. void runTest() override
  1044. {
  1045. auto random = getRandom();
  1046. struct GroupOutput
  1047. {
  1048. uint8_t group;
  1049. std::vector<std::byte> bytes;
  1050. bool operator== (const GroupOutput& other) const
  1051. {
  1052. const auto tie = [] (const auto& x) { return std::tie (x.group, x.bytes); };
  1053. return tie (*this) == tie (other);
  1054. }
  1055. bool operator!= (const GroupOutput& other) const { return ! operator== (other); }
  1056. };
  1057. struct Output : public DeviceMessageHandler
  1058. {
  1059. void processMessage (ump::BytesOnGroup msg) override
  1060. {
  1061. messages.push_back ({ msg.group, std::vector<std::byte> (msg.bytes.begin(), msg.bytes.end()) });
  1062. }
  1063. std::vector<GroupOutput> messages;
  1064. };
  1065. const ump::DeviceInfo deviceInfo { { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 } },
  1066. { std::byte { 0x11 }, std::byte { 0x12 } },
  1067. { std::byte { 0x21 }, std::byte { 0x22 } },
  1068. { std::byte { 0x31 }, std::byte { 0x32 }, std::byte { 0x33 }, std::byte { 0x34 } } };
  1069. FunctionBlock functionBlock;
  1070. beginTest ("When receiving Discovery from a MUID that matches the Device MUID, reply with InvalidateMUID and initiate discovery");
  1071. {
  1072. Output output;
  1073. const auto options = DeviceOptions().withOutputs ({ &output })
  1074. .withFunctionBlock (functionBlock)
  1075. .withDeviceInfo (deviceInfo)
  1076. .withMaxSysExSize (512);
  1077. Device device { options };
  1078. const auto commonMUID = device.getMuid();
  1079. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1080. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1081. detail::MessageMeta::implementationVersion,
  1082. commonMUID,
  1083. MUID::getBroadcast() },
  1084. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1085. { std::byte { 0x15 }, std::byte { 0x16 } },
  1086. { std::byte { 0x25 }, std::byte { 0x26 } },
  1087. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1088. std::byte{},
  1089. 1024,
  1090. std::byte{} }) });
  1091. expect (device.getMuid() != commonMUID);
  1092. const std::vector<GroupOutput> responses
  1093. {
  1094. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1095. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  1096. detail::MessageMeta::implementationVersion,
  1097. commonMUID,
  1098. MUID::getBroadcast() },
  1099. Message::InvalidateMUID { commonMUID }) },
  1100. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1101. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1102. detail::MessageMeta::implementationVersion,
  1103. device.getMuid(),
  1104. MUID::getBroadcast() },
  1105. Message::Discovery { deviceInfo, std::byte{}, 512, std::byte{} }) },
  1106. };
  1107. expect (output.messages == responses);
  1108. }
  1109. beginTest ("When receiving Discovery from a MUID that does not match the Device MUID, reply with DiscoveryResponse and EndpointInquiry");
  1110. {
  1111. Output output;
  1112. const auto options = DeviceOptions().withOutputs ({ &output })
  1113. .withFunctionBlock (functionBlock)
  1114. .withDeviceInfo (deviceInfo)
  1115. .withMaxSysExSize (512);
  1116. Device device { options };
  1117. const auto responderMUID = device.getMuid();
  1118. const auto initiatorMUID = MUID::makeRandom (random);
  1119. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1120. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1121. detail::MessageMeta::implementationVersion,
  1122. initiatorMUID,
  1123. MUID::getBroadcast() },
  1124. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1125. { std::byte { 0x15 }, std::byte { 0x16 } },
  1126. { std::byte { 0x25 }, std::byte { 0x26 } },
  1127. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1128. std::byte{},
  1129. 1024,
  1130. std::byte{} }) });
  1131. expect (device.getMuid() == responderMUID);
  1132. const std::vector<GroupOutput> responses
  1133. {
  1134. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1135. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1136. detail::MessageMeta::implementationVersion,
  1137. responderMUID,
  1138. initiatorMUID },
  1139. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) },
  1140. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1141. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1142. detail::MessageMeta::implementationVersion,
  1143. responderMUID,
  1144. initiatorMUID },
  1145. Message::EndpointInquiry { std::byte{} }) },
  1146. };
  1147. expect (output.messages == responses);
  1148. }
  1149. beginTest ("Sending a V1 discovery message notifies the listener");
  1150. {
  1151. Output output;
  1152. const auto options = DeviceOptions().withOutputs ({ &output })
  1153. .withFunctionBlock (functionBlock)
  1154. .withDeviceInfo (deviceInfo)
  1155. .withMaxSysExSize (512);
  1156. Device device { options };
  1157. const auto responderMUID = device.getMuid();
  1158. const auto initiatorMUID = MUID::makeRandom (random);
  1159. constexpr uint8_t version = 0x01;
  1160. auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock,
  1161. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1162. std::byte { version },
  1163. initiatorMUID,
  1164. MUID::getBroadcast() },
  1165. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1166. { std::byte { 0x15 }, std::byte { 0x16 } },
  1167. { std::byte { 0x25 }, std::byte { 0x26 } },
  1168. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1169. std::byte{},
  1170. 1024,
  1171. std::byte{} });
  1172. // V1 message doesn't have an output path
  1173. bytes.pop_back();
  1174. device.processMessage ({ 0, bytes });
  1175. expect (device.getMuid() == responderMUID);
  1176. const std::vector<GroupOutput> responses
  1177. {
  1178. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1179. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1180. detail::MessageMeta::implementationVersion,
  1181. responderMUID,
  1182. initiatorMUID },
  1183. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) },
  1184. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1185. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1186. detail::MessageMeta::implementationVersion,
  1187. responderMUID,
  1188. initiatorMUID },
  1189. Message::EndpointInquiry { std::byte{} }) },
  1190. };
  1191. expect (output.messages == responses);
  1192. }
  1193. beginTest ("Sending a V2 discovery message notifies the input listener");
  1194. {
  1195. constexpr std::byte outputPathID { 5 };
  1196. const auto initiatorMUID = MUID::makeRandom (random);
  1197. constexpr std::byte version { 0x02 };
  1198. Output output;
  1199. const auto options = DeviceOptions().withOutputs ({ &output })
  1200. .withFunctionBlock (functionBlock)
  1201. .withDeviceInfo (deviceInfo)
  1202. .withMaxSysExSize (512);
  1203. Device device { options };
  1204. const auto responderMUID = device.getMuid();
  1205. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1206. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1207. version,
  1208. initiatorMUID,
  1209. MUID::getBroadcast() },
  1210. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1211. { std::byte { 0x15 }, std::byte { 0x16 } },
  1212. { std::byte { 0x25 }, std::byte { 0x26 } },
  1213. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1214. std::byte{},
  1215. 1024,
  1216. outputPathID }) });
  1217. expect (device.getMuid() == responderMUID);
  1218. const std::vector<GroupOutput> responses
  1219. {
  1220. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1221. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1222. detail::MessageMeta::implementationVersion,
  1223. responderMUID,
  1224. initiatorMUID },
  1225. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) },
  1226. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1227. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1228. detail::MessageMeta::implementationVersion,
  1229. responderMUID,
  1230. initiatorMUID },
  1231. Message::EndpointInquiry { std::byte{} }) },
  1232. };
  1233. expect (output.messages == responses);
  1234. }
  1235. beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields");
  1236. {
  1237. constexpr std::byte outputPathID { 10 };
  1238. const auto initiatorMUID = MUID::makeRandom (random);
  1239. constexpr std::byte version { 0x03 };
  1240. Output output;
  1241. const auto options = DeviceOptions().withOutputs ({ &output })
  1242. .withFunctionBlock (functionBlock)
  1243. .withDeviceInfo (deviceInfo)
  1244. .withMaxSysExSize (512);
  1245. Device device { options };
  1246. const auto responderMUID = device.getMuid();
  1247. auto bytes = getMessageBytes ({ ChannelInGroup::wholeBlock,
  1248. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1249. version,
  1250. initiatorMUID,
  1251. MUID::getBroadcast() },
  1252. Message::Discovery { ump::DeviceInfo { { std::byte { 0x05 }, std::byte { 0x06 }, std::byte { 0x07 } },
  1253. { std::byte { 0x15 }, std::byte { 0x16 } },
  1254. { std::byte { 0x25 }, std::byte { 0x26 } },
  1255. { std::byte { 0x35 }, std::byte { 0x36 }, std::byte { 0x37 }, std::byte { 0x38 } } },
  1256. std::byte{},
  1257. 1024,
  1258. outputPathID });
  1259. // Future versions might have more trailing bytes
  1260. bytes.insert (bytes.end(), { std::byte{}, std::byte{} });
  1261. device.processMessage ({ 0, bytes });
  1262. expect (device.getMuid() == responderMUID);
  1263. const std::vector<GroupOutput> responses
  1264. {
  1265. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1266. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1267. detail::MessageMeta::implementationVersion,
  1268. responderMUID,
  1269. initiatorMUID },
  1270. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, outputPathID, std::byte { 0x7f } }) },
  1271. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1272. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1273. detail::MessageMeta::implementationVersion,
  1274. responderMUID,
  1275. initiatorMUID },
  1276. Message::EndpointInquiry { std::byte{} }) },
  1277. };
  1278. expect (output.messages == responses);
  1279. }
  1280. beginTest ("When receiving an InvalidateMUID that matches the Device MUID, initiate discovery using a new MUID");
  1281. {
  1282. Output output;
  1283. const auto options = DeviceOptions().withOutputs ({ &output })
  1284. .withFunctionBlock (functionBlock)
  1285. .withDeviceInfo (deviceInfo)
  1286. .withMaxSysExSize (512);
  1287. Device device { options };
  1288. const auto deviceMUID = device.getMuid();
  1289. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1290. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  1291. detail::MessageMeta::implementationVersion,
  1292. MUID::makeRandom (random),
  1293. MUID::getBroadcast() },
  1294. Message::InvalidateMUID { deviceMUID }) });
  1295. expect (device.getMuid() != deviceMUID);
  1296. expect (Parser::parse (MUID::makeRandom (random), output.messages.front().bytes) == Message::Parsed { { ChannelInGroup::wholeBlock,
  1297. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1298. detail::MessageMeta::implementationVersion,
  1299. device.getMuid(),
  1300. MUID::getBroadcast() },
  1301. Message::Discovery { deviceInfo,
  1302. {},
  1303. 512,
  1304. {} } });
  1305. }
  1306. struct Listener : public DeviceListener
  1307. {
  1308. void deviceAdded (MUID x) override { added .push_back (x); }
  1309. void deviceRemoved (MUID x) override { removed.push_back (x); }
  1310. std::vector<MUID> added, removed;
  1311. };
  1312. beginTest ("When receiving a DiscoveryResponse, update the set of known devices, notify outputs, and request endpoint info");
  1313. {
  1314. Listener delegate;
  1315. Output output;
  1316. const auto options = DeviceOptions().withOutputs ({ &output })
  1317. .withFunctionBlock (functionBlock)
  1318. .withDeviceInfo (deviceInfo)
  1319. .withMaxSysExSize (512);
  1320. Device device { options };
  1321. device.addListener (delegate);
  1322. expect (device.getDiscoveredMuids().empty());
  1323. const auto deviceMUID = device.getMuid();
  1324. const auto responderMUID = MUID::makeRandom (random);
  1325. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1326. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1327. detail::MessageMeta::implementationVersion,
  1328. responderMUID,
  1329. deviceMUID },
  1330. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) });
  1331. expect (device.getDiscoveredMuids() == std::vector { responderMUID });
  1332. expect (delegate.added == std::vector { responderMUID });
  1333. std::vector<GroupOutput> responses
  1334. {
  1335. { 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1336. detail::MessageMeta::Meta<Message::EndpointInquiry>::subID2,
  1337. detail::MessageMeta::implementationVersion,
  1338. deviceMUID,
  1339. responderMUID },
  1340. Message::EndpointInquiry { std::byte{} }) },
  1341. };
  1342. expect (output.messages == responses);
  1343. beginTest ("When receiving a DiscoveryResponse with a MUID that matches a known device, invalidate that MUID");
  1344. {
  1345. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1346. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1347. detail::MessageMeta::implementationVersion,
  1348. responderMUID,
  1349. deviceMUID },
  1350. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) });
  1351. expect (device.getDiscoveredMuids().empty());
  1352. expect (delegate.removed == std::vector { responderMUID });
  1353. responses.push_back ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1354. detail::MessageMeta::Meta<Message::InvalidateMUID>::subID2,
  1355. detail::MessageMeta::implementationVersion,
  1356. deviceMUID,
  1357. MUID::getBroadcast() },
  1358. Message::InvalidateMUID { responderMUID }) });
  1359. expect (output.messages == responses);
  1360. }
  1361. }
  1362. beginTest ("After receiving an EndpointResponse, the listener is notified");
  1363. {
  1364. static constexpr std::byte dataBytes[] { std::byte { 0x01 }, std::byte { 0x7f }, std::byte { 0x41 } };
  1365. struct EndpointListener : public DeviceListener
  1366. {
  1367. EndpointListener (UnitTest& t, Device& d) : test (t), device (d) {}
  1368. void endpointReceived (MUID, Message::EndpointInquiryResponse) override { called = true; }
  1369. UnitTest& test;
  1370. Device& device;
  1371. bool called = false;
  1372. };
  1373. Output output;
  1374. const auto options = DeviceOptions().withOutputs ({ &output })
  1375. .withFunctionBlock (functionBlock)
  1376. .withDeviceInfo (deviceInfo)
  1377. .withMaxSysExSize (512);
  1378. Device device { options };
  1379. EndpointListener delegate { *this, device };
  1380. device.addListener (delegate);
  1381. const auto responderMUID = MUID::makeRandom (random);
  1382. const auto deviceMUID = device.getMuid();
  1383. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1384. detail::MessageMeta::Meta<Message::DiscoveryResponse>::subID2,
  1385. detail::MessageMeta::implementationVersion,
  1386. responderMUID,
  1387. deviceMUID },
  1388. Message::DiscoveryResponse { deviceInfo, std::byte{}, 512, std::byte{}, std::byte { 0x7f } }) });
  1389. expect (! delegate.called);
  1390. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1391. detail::MessageMeta::Meta<Message::EndpointInquiryResponse>::subID2,
  1392. detail::MessageMeta::implementationVersion,
  1393. responderMUID,
  1394. deviceMUID },
  1395. Message::EndpointInquiryResponse { std::byte{}, dataBytes }) });
  1396. expect (delegate.called);
  1397. }
  1398. beginTest ("If a device has not previously acted as a responder, modifying profiles does not emit events");
  1399. {
  1400. Output output;
  1401. const auto options = DeviceOptions().withOutputs ({ &output })
  1402. .withFunctionBlock (functionBlock)
  1403. .withDeviceInfo (deviceInfo)
  1404. .withMaxSysExSize (512)
  1405. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1406. Device device { options };
  1407. expect (device.getProfileHost() != nullptr);
  1408. const Profile profile { std::byte { 0x01 },
  1409. std::byte { 0x02 },
  1410. std::byte { 0x03 },
  1411. std::byte { 0x04 },
  1412. std::byte { 0x05 } };
  1413. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1414. expect (output.messages.empty());
  1415. beginTest ("The device reports profiles accurately");
  1416. {
  1417. const auto inquiryMUID = MUID::makeRandom (random);
  1418. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1419. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1420. detail::MessageMeta::implementationVersion,
  1421. inquiryMUID,
  1422. device.getMuid() },
  1423. Message::ProfileInquiry{}) });
  1424. const Profile disabledProfiles[] { profile };
  1425. expect (output.messages.size() == 1);
  1426. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1427. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1428. detail::MessageMeta::implementationVersion,
  1429. device.getMuid(),
  1430. inquiryMUID },
  1431. Message::ProfileInquiryResponse { {}, disabledProfiles }));
  1432. }
  1433. beginTest ("If a device has previously acted as a responder to profile inquiry, then modifying profiles emits events");
  1434. {
  1435. const auto numChannels = 0;
  1436. device.getProfileHost()->enableProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, numChannels);
  1437. expect (output.messages.size() == 2);
  1438. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1439. detail::MessageMeta::Meta<Message::ProfileEnabledReport>::subID2,
  1440. detail::MessageMeta::implementationVersion,
  1441. device.getMuid(),
  1442. MUID::getBroadcast() },
  1443. Message::ProfileEnabledReport { profile, numChannels }));
  1444. }
  1445. }
  1446. beginTest ("If a device receives a details inquiry message addressed to an unsupported profile, a NAK with a code of 0x04 is emitted");
  1447. {
  1448. Output output;
  1449. const auto options = DeviceOptions().withOutputs ({ &output })
  1450. .withFunctionBlock (functionBlock)
  1451. .withDeviceInfo (deviceInfo)
  1452. .withMaxSysExSize (512)
  1453. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1454. Device device { options };
  1455. expect (device.getProfileHost() != nullptr);
  1456. const auto inquiryMUID = MUID::makeRandom (random);
  1457. const Profile profile { std::byte { 0x01 },
  1458. std::byte { 0x02 },
  1459. std::byte { 0x03 },
  1460. std::byte { 0x04 },
  1461. std::byte { 0x05 } };
  1462. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1463. detail::MessageMeta::Meta<Message::ProfileDetails>::subID2,
  1464. detail::MessageMeta::implementationVersion,
  1465. inquiryMUID,
  1466. device.getMuid() },
  1467. Message::ProfileDetails { profile, std::byte { 0x02 } }) });
  1468. expect (output.messages.size() == 1);
  1469. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1470. detail::MessageMeta::Meta<Message::NAK>::subID2,
  1471. detail::MessageMeta::implementationVersion,
  1472. device.getMuid(),
  1473. inquiryMUID },
  1474. Message::NAK { detail::MessageMeta::Meta<Message::ProfileDetails>::subID2,
  1475. std::byte { 0x04 },
  1476. {},
  1477. {},
  1478. {} }));
  1479. }
  1480. beginTest ("If a device receives a set profile on and enables the profile, profile enabled report is emitted");
  1481. {
  1482. // Note: if there's no explicit profile delegate, the device will toggle profiles as requested.
  1483. Output output;
  1484. const auto options = DeviceOptions().withOutputs ({ &output })
  1485. .withFunctionBlock (functionBlock)
  1486. .withDeviceInfo (deviceInfo)
  1487. .withMaxSysExSize (512)
  1488. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1489. Device device { options };
  1490. expect (device.getProfileHost() != nullptr);
  1491. const Profile profile { std::byte { 0x01 },
  1492. std::byte { 0x02 },
  1493. std::byte { 0x03 },
  1494. std::byte { 0x04 },
  1495. std::byte { 0x05 } };
  1496. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1497. const auto inquiryMUID = MUID::makeRandom (random);
  1498. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1499. detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1500. detail::MessageMeta::implementationVersion,
  1501. inquiryMUID,
  1502. device.getMuid() },
  1503. Message::ProfileOn { profile, 1 }) });
  1504. expect (output.messages.size() == 1);
  1505. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1506. detail::MessageMeta::Meta<Message::ProfileEnabledReport>::subID2,
  1507. detail::MessageMeta::implementationVersion,
  1508. device.getMuid(),
  1509. MUID::getBroadcast() },
  1510. Message::ProfileEnabledReport { profile, 1 }));
  1511. }
  1512. struct DoNothingProfileDelegate : public ProfileDelegate
  1513. {
  1514. void profileEnablementRequested (MUID, ProfileAtAddress, int, bool) override {}
  1515. };
  1516. beginTest ("If a device receives a set profile on but then doesn't enable the profile, profile disabled report is emitted");
  1517. {
  1518. DoNothingProfileDelegate delegate;
  1519. Output output;
  1520. const auto options = DeviceOptions().withOutputs ({ &output })
  1521. .withFunctionBlock (functionBlock)
  1522. .withDeviceInfo (deviceInfo)
  1523. .withMaxSysExSize (512)
  1524. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true))
  1525. .withProfileDelegate (&delegate);
  1526. Device device { options };
  1527. expect (device.getProfileHost() != nullptr);
  1528. const Profile profile { std::byte { 0x01 },
  1529. std::byte { 0x02 },
  1530. std::byte { 0x03 },
  1531. std::byte { 0x04 },
  1532. std::byte { 0x05 } };
  1533. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1534. const auto inquiryMUID = MUID::makeRandom (random);
  1535. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1536. detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1537. detail::MessageMeta::implementationVersion,
  1538. inquiryMUID,
  1539. device.getMuid() },
  1540. Message::ProfileOn { profile, 1 }) });
  1541. expect (output.messages.size() == 1);
  1542. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1543. detail::MessageMeta::Meta<Message::ProfileDisabledReport>::subID2,
  1544. detail::MessageMeta::implementationVersion,
  1545. device.getMuid(),
  1546. MUID::getBroadcast() },
  1547. Message::ProfileDisabledReport { profile, {} }));
  1548. }
  1549. beginTest ("If a device receives a set profile on for an unsupported profile, NAK is emitted");
  1550. {
  1551. Output output;
  1552. const auto options = DeviceOptions().withOutputs ({ &output })
  1553. .withFunctionBlock (functionBlock)
  1554. .withDeviceInfo (deviceInfo)
  1555. .withMaxSysExSize (512)
  1556. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1557. Device device { options };
  1558. expect (device.getProfileHost() != nullptr);
  1559. const Profile profile { std::byte { 0x01 },
  1560. std::byte { 0x02 },
  1561. std::byte { 0x03 },
  1562. std::byte { 0x04 },
  1563. std::byte { 0x05 } };
  1564. const auto inquiryMUID = MUID::makeRandom (random);
  1565. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1566. detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1567. detail::MessageMeta::implementationVersion,
  1568. inquiryMUID,
  1569. device.getMuid() },
  1570. Message::ProfileOn { profile, 1 }) });
  1571. expect (output.messages.size() == 1);
  1572. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1573. detail::MessageMeta::Meta<Message::NAK>::subID2,
  1574. detail::MessageMeta::implementationVersion,
  1575. device.getMuid(),
  1576. inquiryMUID },
  1577. Message::NAK { detail::MessageMeta::Meta<Message::ProfileOn>::subID2,
  1578. {},
  1579. {},
  1580. {},
  1581. {} }));
  1582. }
  1583. beginTest ("If a device receives a set profile off and disables the profile, profile disabled report is emitted");
  1584. {
  1585. // Note: if there's no explicit profile delegate, the device will toggle profiles as requested.
  1586. Output output;
  1587. const auto options = DeviceOptions().withOutputs ({ &output })
  1588. .withFunctionBlock (functionBlock)
  1589. .withDeviceInfo (deviceInfo)
  1590. .withMaxSysExSize (512)
  1591. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1592. Device device { options };
  1593. expect (device.getProfileHost() != nullptr);
  1594. const Profile profile { std::byte { 0x01 },
  1595. std::byte { 0x02 },
  1596. std::byte { 0x03 },
  1597. std::byte { 0x04 },
  1598. std::byte { 0x05 } };
  1599. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1600. device.getProfileHost()->enableProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 0);
  1601. const auto inquiryMUID = MUID::makeRandom (random);
  1602. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1603. detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1604. detail::MessageMeta::implementationVersion,
  1605. inquiryMUID,
  1606. device.getMuid() },
  1607. Message::ProfileOff { profile }) });
  1608. expect (output.messages.size() == 1);
  1609. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1610. detail::MessageMeta::Meta<Message::ProfileDisabledReport>::subID2,
  1611. detail::MessageMeta::implementationVersion,
  1612. device.getMuid(),
  1613. MUID::getBroadcast() },
  1614. Message::ProfileDisabledReport { profile, {} }));
  1615. }
  1616. beginTest ("If a device receives a set profile off but then doesn't disable the profile, profile enabled report is emitted");
  1617. {
  1618. Output output;
  1619. DoNothingProfileDelegate delegate;
  1620. const auto options = DeviceOptions().withOutputs ({ &output })
  1621. .withFunctionBlock (functionBlock)
  1622. .withDeviceInfo (deviceInfo)
  1623. .withMaxSysExSize (512)
  1624. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true))
  1625. .withProfileDelegate (&delegate);
  1626. Device device { options };
  1627. expect (device.getProfileHost() != nullptr);
  1628. const Profile profile { std::byte { 0x01 },
  1629. std::byte { 0x02 },
  1630. std::byte { 0x03 },
  1631. std::byte { 0x04 },
  1632. std::byte { 0x05 } };
  1633. device.getProfileHost()->addProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1634. device.getProfileHost()->enableProfile ({ profile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) }, 1);
  1635. const auto inquiryMUID = MUID::makeRandom (random);
  1636. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1637. detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1638. detail::MessageMeta::implementationVersion,
  1639. inquiryMUID,
  1640. device.getMuid() },
  1641. Message::ProfileOff { profile }) });
  1642. expect (output.messages.size() == 1);
  1643. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1644. detail::MessageMeta::Meta<Message::ProfileEnabledReport>::subID2,
  1645. detail::MessageMeta::implementationVersion,
  1646. device.getMuid(),
  1647. MUID::getBroadcast() },
  1648. Message::ProfileEnabledReport { profile, 1 }));
  1649. }
  1650. beginTest ("If a device receives a set profile off for an unsupported profile, NAK is emitted");
  1651. {
  1652. Output output;
  1653. const auto options = DeviceOptions().withOutputs ({ &output })
  1654. .withFunctionBlock (functionBlock)
  1655. .withDeviceInfo (deviceInfo)
  1656. .withMaxSysExSize (512)
  1657. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1658. Device device { options };
  1659. expect (device.getProfileHost() != nullptr);
  1660. const Profile profile { std::byte { 0x01 },
  1661. std::byte { 0x02 },
  1662. std::byte { 0x03 },
  1663. std::byte { 0x04 },
  1664. std::byte { 0x05 } };
  1665. const auto inquiryMUID = MUID::makeRandom (random);
  1666. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1667. detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1668. detail::MessageMeta::implementationVersion,
  1669. inquiryMUID,
  1670. device.getMuid() },
  1671. Message::ProfileOff { profile }) });
  1672. expect (output.messages.size() == 1);
  1673. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeBlock,
  1674. detail::MessageMeta::Meta<Message::NAK>::subID2,
  1675. detail::MessageMeta::implementationVersion,
  1676. device.getMuid(),
  1677. inquiryMUID },
  1678. Message::NAK { detail::MessageMeta::Meta<Message::ProfileOff>::subID2,
  1679. {},
  1680. {},
  1681. {},
  1682. {} }));
  1683. }
  1684. const FunctionBlock realBlock { std::byte{}, 0, 3 };
  1685. beginTest ("If a device receives a profile inquiry addressed to a channel, that channel's profiles are emitted");
  1686. {
  1687. Output output;
  1688. const auto options = DeviceOptions().withOutputs ({ &output })
  1689. .withFunctionBlock (realBlock)
  1690. .withDeviceInfo (deviceInfo)
  1691. .withMaxSysExSize (512)
  1692. .withFeatures (DeviceFeatures{}.withProfileConfigurationSupported (true));
  1693. Device device { options };
  1694. auto& profileHost = *device.getProfileHost();
  1695. Profile channel0Profile { std::byte { 0x01 } };
  1696. Profile channel1Profile { std::byte { 0x02 } };
  1697. profileHost.addProfile ({ channel0Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel0) });
  1698. profileHost.addProfile ({ channel1Profile, ChannelAddress{}.withChannel (ChannelInGroup::channel1) });
  1699. const auto inquiryMUID = MUID::makeRandom (random);
  1700. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel0,
  1701. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1702. detail::MessageMeta::implementationVersion,
  1703. inquiryMUID,
  1704. device.getMuid() },
  1705. Message::ProfileInquiry{}) });
  1706. const Profile channel0Profiles[] { channel0Profile };
  1707. const Profile channel1Profiles[] { channel1Profile };
  1708. expect (output.messages.size() == 1);
  1709. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel0,
  1710. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1711. detail::MessageMeta::implementationVersion,
  1712. device.getMuid(),
  1713. inquiryMUID },
  1714. Message::ProfileInquiryResponse { {}, channel0Profiles }));
  1715. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::channel2,
  1716. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1717. detail::MessageMeta::implementationVersion,
  1718. inquiryMUID,
  1719. device.getMuid() },
  1720. Message::ProfileInquiry{}) });
  1721. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::channel2,
  1722. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1723. detail::MessageMeta::implementationVersion,
  1724. device.getMuid(),
  1725. inquiryMUID },
  1726. Message::ProfileInquiryResponse { {}, {} }));
  1727. Profile group0Profile { std::byte { 0x05 } };
  1728. Profile group1Profile { std::byte { 0x06 } };
  1729. const Profile group0Profiles[] { group0Profile };
  1730. const Profile group1Profiles[] { group1Profile };
  1731. beginTest ("If a device receives a profile inquiry addressed to a group, that group's profiles are emitted");
  1732. {
  1733. profileHost.addProfile ({ group0Profile, ChannelAddress{}.withGroup (0).withChannel (ChannelInGroup::wholeGroup) });
  1734. profileHost.addProfile ({ group1Profile, ChannelAddress{}.withGroup (1).withChannel (ChannelInGroup::wholeGroup) });
  1735. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1736. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1737. detail::MessageMeta::implementationVersion,
  1738. inquiryMUID,
  1739. device.getMuid() },
  1740. Message::ProfileInquiry{}) });
  1741. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup,
  1742. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1743. detail::MessageMeta::implementationVersion,
  1744. device.getMuid(),
  1745. inquiryMUID },
  1746. Message::ProfileInquiryResponse { {}, group0Profiles }));
  1747. device.processMessage ({ 2, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1748. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1749. detail::MessageMeta::implementationVersion,
  1750. inquiryMUID,
  1751. device.getMuid() },
  1752. Message::ProfileInquiry{}) });
  1753. expect (output.messages.back().bytes == getMessageBytes ({ ChannelInGroup::wholeGroup,
  1754. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1755. detail::MessageMeta::implementationVersion,
  1756. device.getMuid(),
  1757. inquiryMUID },
  1758. Message::ProfileInquiryResponse { {}, {} }));
  1759. }
  1760. 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");
  1761. {
  1762. Profile blockProfile { std::byte { 0x0a } };
  1763. profileHost.addProfile ({ blockProfile, ChannelAddress{}.withChannel (ChannelInGroup::wholeBlock) });
  1764. output.messages.clear();
  1765. device.processMessage ({ 1, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1766. detail::MessageMeta::Meta<Message::ProfileInquiry>::subID2,
  1767. detail::MessageMeta::implementationVersion,
  1768. inquiryMUID,
  1769. device.getMuid() },
  1770. Message::ProfileInquiry{}) });
  1771. const Profile blockProfiles[] { blockProfile };
  1772. expect (output.messages == std::vector<GroupOutput> { { 0, getMessageBytes ({ ChannelInGroup::channel0,
  1773. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1774. detail::MessageMeta::implementationVersion,
  1775. device.getMuid(),
  1776. inquiryMUID },
  1777. Message::ProfileInquiryResponse { {}, channel0Profiles }) },
  1778. { 0, getMessageBytes ({ ChannelInGroup::channel1,
  1779. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1780. detail::MessageMeta::implementationVersion,
  1781. device.getMuid(),
  1782. inquiryMUID },
  1783. Message::ProfileInquiryResponse { {}, channel1Profiles }) },
  1784. { 0, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1785. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1786. detail::MessageMeta::implementationVersion,
  1787. device.getMuid(),
  1788. inquiryMUID },
  1789. Message::ProfileInquiryResponse { {}, group0Profiles }) },
  1790. { 1, getMessageBytes ({ ChannelInGroup::wholeGroup,
  1791. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1792. detail::MessageMeta::implementationVersion,
  1793. device.getMuid(),
  1794. inquiryMUID },
  1795. Message::ProfileInquiryResponse { {}, group1Profiles }) },
  1796. { 1, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1797. detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2,
  1798. detail::MessageMeta::implementationVersion,
  1799. device.getMuid(),
  1800. inquiryMUID },
  1801. Message::ProfileInquiryResponse { {}, blockProfiles }) } });
  1802. }
  1803. }
  1804. // Property exchange
  1805. {
  1806. const auto inquiryMUID = MUID::makeRandom (random);
  1807. struct Delegate : public PropertyDelegate
  1808. {
  1809. uint8_t getNumSimultaneousRequestsSupported() const override { return 1; }
  1810. PropertyReplyData propertyGetDataRequested (MUID, const PropertyRequestHeader&) override { return {}; }
  1811. PropertyReplyHeader propertySetDataRequested (MUID, const PropertyRequestData&) override { return {}; }
  1812. bool subscriptionStartRequested (MUID, const PropertySubscriptionHeader&) override { return true; }
  1813. void subscriptionDidStart (MUID, const String&, const PropertySubscriptionHeader&) override {}
  1814. void subscriptionWillEnd (MUID, const Subscription&) override {}
  1815. };
  1816. Delegate delegate;
  1817. Output output;
  1818. const auto options = DeviceOptions().withOutputs ({ &output })
  1819. .withFunctionBlock (realBlock)
  1820. .withDeviceInfo (deviceInfo)
  1821. .withMaxSysExSize (512)
  1822. .withFeatures (DeviceFeatures{}.withPropertyExchangeSupported (true))
  1823. .withPropertyDelegate (&delegate);
  1824. Device device { options };
  1825. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1826. detail::MessageMeta::Meta<Message::Discovery>::subID2,
  1827. detail::MessageMeta::implementationVersion,
  1828. inquiryMUID,
  1829. MUID::getBroadcast() },
  1830. Message::Discovery { {}, DeviceFeatures{}.withPropertyExchangeSupported (true).getSupportedCapabilities(), 512, {} }) });
  1831. expect (output.messages.size() == 2);
  1832. output.messages.clear();
  1833. beginTest ("If a device receives too many concurrent property exchange requests, it responds with a retry status code.");
  1834. {
  1835. auto obj = std::make_unique<DynamicObject>();
  1836. obj->setProperty ("resource", "X-CustomProp");
  1837. const auto header = Encodings::jsonTo7BitText (obj.release());
  1838. for (const auto& requestID : { std::byte { 0 }, std::byte { 1 } })
  1839. {
  1840. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1841. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1842. detail::MessageMeta::implementationVersion,
  1843. inquiryMUID,
  1844. device.getMuid() },
  1845. Message::PropertySetData { { requestID, header, 0, 1, {} } }) });
  1846. }
  1847. expect (output.messages.size() == 1);
  1848. const auto parsed = Parser::parse (output.messages.back().bytes);
  1849. expect (parsed.has_value());
  1850. expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock,
  1851. detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
  1852. detail::MessageMeta::implementationVersion,
  1853. device.getMuid(),
  1854. inquiryMUID });
  1855. auto* body = std::get_if<Message::PropertySetDataResponse> (&parsed->body);
  1856. expect (body != nullptr);
  1857. expect (body->requestID == std::byte { 1 });
  1858. auto replyHeader = Encodings::jsonFrom7BitText (body->header);
  1859. expect (replyHeader.getProperty ("status", "") == var (343));
  1860. }
  1861. // Terminate ongoing message
  1862. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1863. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1864. detail::MessageMeta::implementationVersion,
  1865. inquiryMUID,
  1866. device.getMuid() },
  1867. Message::PropertySetData { { {}, {}, 0, 0, {} } }) });
  1868. output.messages.clear();
  1869. beginTest ("If a device receives an unexpectedly-terminated request, it responds with an error status code.");
  1870. {
  1871. auto obj = std::make_unique<DynamicObject>();
  1872. obj->setProperty ("resource", "X-CustomProp");
  1873. const auto header = Encodings::jsonTo7BitText (obj.release());
  1874. const std::byte requestID { 3 };
  1875. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1876. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1877. detail::MessageMeta::implementationVersion,
  1878. inquiryMUID,
  1879. device.getMuid() },
  1880. Message::PropertySetData { { requestID, header, 2, 1, {} } }) });
  1881. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1882. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1883. detail::MessageMeta::implementationVersion,
  1884. inquiryMUID,
  1885. device.getMuid() },
  1886. Message::PropertySetData { { requestID, header, 2, 0, {} } }) });
  1887. expect (output.messages.size() == 1);
  1888. const auto parsed = Parser::parse (output.messages.back().bytes);
  1889. expect (parsed.has_value());
  1890. expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock,
  1891. detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
  1892. detail::MessageMeta::implementationVersion,
  1893. device.getMuid(),
  1894. inquiryMUID });
  1895. auto* body = std::get_if<Message::PropertySetDataResponse> (&parsed->body);
  1896. expect (body != nullptr);
  1897. expect (body->requestID == requestID);
  1898. auto replyHeader = Encodings::jsonFrom7BitText (body->header);
  1899. expect (replyHeader.getProperty ("status", "") == var (400));
  1900. }
  1901. output.messages.clear();
  1902. beginTest ("If a request is terminated via notify, the device responds with an error status code.");
  1903. {
  1904. auto obj = std::make_unique<DynamicObject>();
  1905. obj->setProperty ("resource", "X-CustomProp");
  1906. const auto header = Encodings::jsonTo7BitText (obj.release());
  1907. const std::byte requestID { 100 };
  1908. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1909. detail::MessageMeta::Meta<Message::PropertySetData>::subID2,
  1910. detail::MessageMeta::implementationVersion,
  1911. inquiryMUID,
  1912. device.getMuid() },
  1913. Message::PropertySetData { { requestID, header, 2, 1, {} } }) });
  1914. auto notifyHeader = std::make_unique<DynamicObject>();
  1915. notifyHeader->setProperty ("status", 144);
  1916. device.processMessage ({ 0, getMessageBytes ({ ChannelInGroup::wholeBlock,
  1917. detail::MessageMeta::Meta<Message::PropertyNotify>::subID2,
  1918. detail::MessageMeta::implementationVersion,
  1919. inquiryMUID,
  1920. device.getMuid() },
  1921. Message::PropertyNotify { { requestID, Encodings::jsonTo7BitText (notifyHeader.release()), 1, 1, {} } }) });
  1922. expect (output.messages.size() == 1);
  1923. const auto parsed = Parser::parse (output.messages.back().bytes);
  1924. expect (parsed.has_value());
  1925. expect (parsed->header == Message::Header { ChannelInGroup::wholeBlock,
  1926. detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
  1927. detail::MessageMeta::implementationVersion,
  1928. device.getMuid(),
  1929. inquiryMUID });
  1930. auto* body = std::get_if<Message::PropertySetDataResponse> (&parsed->body);
  1931. expect (body != nullptr);
  1932. expect (body->requestID == requestID);
  1933. auto replyHeader = Encodings::jsonFrom7BitText (body->header);
  1934. expect (replyHeader.getProperty ("status", "") == var (400));
  1935. }
  1936. }
  1937. }
  1938. private:
  1939. template <typename Msg>
  1940. static std::vector<std::byte> getMessageBytes (const Message::Header& header, const Msg& body)
  1941. {
  1942. std::vector<std::byte> bytes;
  1943. detail::Marshalling::Writer { bytes } (header, body);
  1944. return bytes;
  1945. }
  1946. };
  1947. static DeviceTests deviceTests;
  1948. #endif
  1949. } // namespace juce::midi_ci