DetectorGraph  2.0
beatmachine.cpp
Go to the documentation of this file.
1 // Copyright 2017 Nest Labs, Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include <iostream>
16 
17 #include "graph.hpp"
18 #include "detector.hpp"
19 #include "processorcontainer.hpp"
21 #include "dglogging.hpp"
22 #include "graphanalyzer.hpp"
23 
24 #include <map>
25 #include <chrono>
26 #include <thread>
27 #include <algorithm>
28 
29 using namespace DetectorGraph;
30 
31 using std::cout;
32 using std::endl;
33 
34 /**
35  * @file beatmachine.cpp
36  * @brief A Rube Goldbergesque contraption that uses TimeoutPublisherService APIs
37  *
38  * @section ex-bm-intro Introduction
39  * Writing examples is hard - you have to be creative and stuff. So naturally
40  * stuff went wild. This one is inspired by the Na Automata [1] and the Hey
41  * Jude Flowchart [2].
42  *
43  * Jokes aside, this example provides a useful but very basic (C++11 - only)
44  * concrete implementation for the DetectorGraph::TimeoutPublisherService class.
45  * This shows how Timers & Timeouts can be integrated with Detectors & Topics.
46  *
47  * In the example, five detectors attempt to sing Hey Jude together in a
48  * distributed fashion - not unlike when you try to sing bits of a song in a
49  * crowded concert without knowing the song's entire lyrics or melody. Here the
50  * four 'line-singing' detectors must act in synchrony to achieve the desired
51  * output.
52  *
53  * @section ex-bm-tps Implementing TimeoutPublisherService
54  * When porting the DetectorGraph library to your environment you'll need to
55  * provide a concrete subclass of DetectorGraph::TimeoutPublisherService
56  * similar to DetectorGraph::SleepBasedTimeoutPublisherService in this example.
57  * Note that features and accuracy were sacrificed in order to keep
58  * DetectorGraph::SleepBasedTimeoutPublisherService as simple as possible.
59  *
60  * @section ex-bm-utps Using the Timer APIs
61  * The first example of using the Time APIs is on `ThemeDetector` where it sets
62  * up a 75 BPM _Periodic Timer_ that publishes `RhytmBeats` every 0.8s and
63  * subscribes to it:
64  * @code
65 ThemeDetector(DetectorGraph::Graph* graph, TimeoutPublisherService* timeService) : DetectorGraph::Detector(graph)
66 {
67  SetupPeriodicPublishing<RhytmBeats>(RhytmBeats::kPeriod, timeService);
68  Subscribe<RhytmBeats>(this);
69  SetupPublishing<SongThemeState>(this);
70 }
71  * @endcode
72  *
73  * Another example, `ThenYouDetector` is a DetectorGraph::TimeoutPublisher that
74  * acts in a _fire and forget_ manner. This allows the detector to schedule the
75  * publishing of a `TopicState` to a `Topic` prompting a new graph evaluation
76  * in the future for that `TopicState`.
77  * The call to `PublishOnTimeout` basically says: "Post this data to its topic
78  * in X milliseconds".
79  * @code
80 class ThenYouDetector : public DetectorGraph::Detector
81 , ...
82 , public DetectorGraph::TimeoutPublisher<ThenYouState>
83 {
84 ...
85  ThenYouDetector(DetectorGraph::Graph* graph, DetectorGraph::TimeoutPublisherService* timeService)
86  : DetectorGraph::Detector(graph)
87 ...
88  {
89 ...
90  SetupTimeoutPublishing<ThenYouState>(this, timeService);
91  }
92 
93 ...
94  PublishOnTimeout(ThenYouState(kResponses[mThenYouIndex] + " " + kResponseEnd), RhytmBeats::kPeriod);
95  * @endcode
96  *
97  * A common DetectorGraph::TimeoutPublisher pattern is seen on `DontDetector`;
98  * it is both a DetectorGraph::TimeoutPublisher and a
99  * DetectorGraph::SubscriberInterface of the same Topic, `DontPause`. This
100  * achieves a simple timeout pattern where the detector is "called back" later.
101  * @code
102 class DontDetector : public DetectorGraph::Detector
103  ...
104 , public DetectorGraph::SubscriberInterface<DontPause>
105 , public DetectorGraph::TimeoutPublisher<DontPause>
106 {
107  DontDetector(DetectorGraph::Graph* graph, DetectorGraph::TimeoutPublisherService* timeService)
108  ...
109  {
110  ...
111  Subscribe<DontPause>(this);
112  SetupTimeoutPublishing<DontPause>(this, timeService);
113  }
114  ...
115  PublishOnTimeout(DontPause(), RhytmBeats::kPeriod);
116  * @endcode
117  *
118  *
119  * @section ex-bm-arch Architecture
120  * Below is the graph representation for this example.
121  * @dot "BeatMachine"
122 digraph GraphAnalyzer {
123  rankdir = "LR";
124  node[fontname=Helvetica];
125 
126  "ThenYouState" [label="0:ThenYouState",style=filled, shape=box, color=limegreen];
127  "RememberPause" [label="1:RememberPause",style=filled, shape=box, color=orange];
128  "RememberPause" -> "RememberDetector";
129  "DontPause" [label="2:DontPause",style=filled, shape=box, color=orange];
130  "DontPause" -> "DontDetector";
131  "RhytmBeats" [label="3:RhytmBeats",style=filled, shape=box, color=orange];
132  "RhytmBeats" -> "ThemeDetector";
133  "ThemeDetector" [label="4:ThemeDetector", color=blue];
134  "ThemeDetector" -> "SongThemeState";
135  "ThemeDetector" -> "RhytmBeats" [style=dotted, color=red];
136  "SongThemeState" [label="5:SongThemeState",style=filled, shape=box, color=red];
137  "SongThemeState" -> "DontDetector";
138  "SongThemeState" -> "RememberDetector";
139  "SongThemeState" -> "ThenYouDetector";
140  "SongThemeState" -> "TooManyNaNaNasDetector";
141  "TooManyNaNaNasDetector" [label="6:TooManyNaNaNasDetector", color=blue];
142  "TooManyNaNaNasDetector" -> "PlaybackState";
143  "PlaybackState" [label="7:PlaybackState",style=filled, shape=box, color=limegreen];
144  "ThenYouDetector" [label="8:ThenYouDetector", color=blue];
145  "ThenYouDetector" -> "ThenYouState" [style=dotted, color=red];
146  "RememberDetector" [label="9:RememberDetector", color=blue];
147  "RememberDetector" -> "RememberState";
148  "RememberDetector" -> "RememberPause" [style=dotted, color=red];
149  "RememberState" [label="10:RememberState",style=filled, shape=box, color=limegreen];
150  "DontDetector" [label="11:DontDetector", color=blue];
151  "DontDetector" -> "DontThemeState";
152  "DontDetector" -> "DontPause" [style=dotted, color=red];
153  "DontThemeState" [label="12:DontThemeState",style=filled, shape=box, color=limegreen];
154 }
155  * @enddot
156  *
157  * @section ex-bm-other-notes Other Notes
158  * Note that this example in contained in a single file for the sake
159  * of unity as an example. In real-world scenarios the suggested pattern is to
160  * split the code into:
161  *
162  @verbatim
163  detectorgraph/
164  include/
165  beatmachine.hpp (BeatMachine header)
166  src/
167  beatmachine.hpp (BeatMachine implementation)
168  detectors/
169  include/
170  ThemeDetector.hpp
171  DontDetector.hpp
172  RememberDetector.hpp
173  ThenYouDetector.hpp
174  SondEndDetector.hpp
175  src/
176  ThemeDetector.cpp
177  DontDetector.cpp
178  RememberDetector.cpp
179  ThenYouDetector.cpp
180  SondEndDetector.cpp
181  topicstates/
182  include/
183  RhytmBeats.hpp
184  SongLine.hpp
185  SongThemeState.hpp
186  DontThemeState.hpp
187  RememberState.hpp
188  ThenYouState.hpp
189  PlaybackState.hpp
190 @endverbatim
191  *
192  * @section ex-bm-refs References
193  * - [1] Na Automata - https://xkcd.com/851/
194  * - [2] Hey Jude Flowchart - http://loveallthis.tumblr.com/post/166124704
195  *
196  * Note that this examples take a bunch of poetic licenses to allow for more
197  * compact representation of the code (e.g. omitting namespaces)
198  */
199 
200 /// @cond DO_NOT_DOCUMENT
201 
202 
203 using WallClock = std::chrono::high_resolution_clock;
204 using SteadyClock = std::chrono::steady_clock;
205 using TimePoint = std::chrono::time_point<WallClock>;
206 using Milliseconds = std::chrono::milliseconds;
207 
208 class SleepBasedTimeoutPublisherService : public DetectorGraph::TimeoutPublisherService
209 {
210  using TimerMap = std::map<TimeoutPublisherHandle, TimeOffset>;
211  using TimerIterator = TimerMap::iterator;
212 public:
213  SleepBasedTimeoutPublisherService(DetectorGraph::Graph& arGraph)
215  , mMetronomeId(kInvalidTimeoutPublisherHandle)
216  {
217  }
218 
219  // Sleeps until the next timer goes off.
220  bool SleepTillBrooklyn()
221  {
222  if (mTimerMap.size() > 0)
223  {
224  TimerIterator minIt = GetNextTimeout();
225  TimeOffset deadline = minIt->second;
226  TimePoint deadlineTp = TimePoint(Milliseconds(deadline));
227  std::this_thread::sleep_until(deadlineTp);
228 
229  if (minIt->first == mMetronomeId)
230  {
231  MetronomeFired();
232  SetTimeout(mMetronomeTimerPeriod, mMetronomeId);
233  Start(mMetronomeId);
234  }
235  else
236  {
237  TimeoutExpired(minIt->first);
238  mTimerMap.erase(minIt);
239  }
240  return true;
241  }
242  return false;
243  }
244 
245  // BEGIN CONCRETE IMPLEMENTATIONS
246  // All methods below are necessary for the concrete implementation of TimeoutPublisherService
247  TimeOffset GetTime() const
248  {
249  return std::chrono::duration_cast<Milliseconds>(WallClock::now().time_since_epoch())
250  .count();
251  }
252  TimeOffset GetMonotonicTime() const
253  {
254  return std::chrono::duration_cast<Milliseconds>(SteadyClock::now().time_since_epoch())
255  .count();
256  }
257 protected:
258  void SetTimeout(const TimeOffset aMillisecondsFromNow, const TimeoutPublisherHandle aTimerId)
259  {
260  mTimerMap[aTimerId] = aMillisecondsFromNow + GetMonotonicTime();
261  }
262  void Start(const TimeoutPublisherHandle aTimerId) { }
263  void Cancel(const TimeoutPublisherHandle aTimerId) { mTimerMap.erase(aTimerId); }
264  void StartMetronome(const TimeOffset aPeriodInMilliseconds)
265  {
266  mMetronomeId = GetUniqueTimerHandle();
267  mMetronomeTimerPeriod = aPeriodInMilliseconds;
268  SetTimeout(mMetronomeTimerPeriod, mMetronomeId);
269  Start(mMetronomeId);
270  }
271  void CancelMetronome() { Cancel(mMetronomeId); }
272  // END CONCRETE IMPLEMENTATIONS
273 
274 private:
275  TimerIterator GetNextTimeout()
276  {
277  return std::min_element(mTimerMap.begin(), mTimerMap.end(),
278  [](const TimerMap::value_type& t1, const TimerMap::value_type& t2) -> bool {
279  return (t1.second < t2.second);
280  });
281  }
282 
283 private:
284  std::map<TimeoutPublisherHandle, TimeOffset> mTimerMap;
285  TimeoutPublisherHandle mMetronomeId;
286  TimeOffset mMetronomeTimerPeriod;
287 };
288 
289 struct RhytmBeats : public DetectorGraph::TopicState
290 {
291  static const TimeOffset kPeriod = 60000 / 75; // 75 bpm
292 };
293 
294 //! [TopicStates Inheritance Example]
295 struct SongLine
296 {
297  std::string line;
298  SongLine() : line() {}
299  SongLine(const std::string& aLine) : line(aLine) {}
300  friend std::ostream& operator<<(std::ostream& os, SongLine s) { return os << s.line; }
301 };
302 
303 struct SongThemeState : public DetectorGraph::TopicState, public SongLine
304 {
305  SongThemeState() {}
306  SongThemeState(const std::string& aLine) : SongLine(aLine) {}
307 };
308 
309 struct DontThemeState : public DetectorGraph::TopicState, public SongLine
310 {
311  DontThemeState() {}
312  DontThemeState(const std::string& aLine) : SongLine(aLine) {}
313 };
314 
315 struct RememberState : public DetectorGraph::TopicState, public SongLine
316 {
317  RememberState() {}
318  RememberState(const std::string& aLine) : SongLine(aLine) {}
319 };
320 
321 struct ThenYouState : public DetectorGraph::TopicState, public SongLine
322 {
323  ThenYouState() {}
324  ThenYouState(const std::string& aLine) : SongLine(aLine) {}
325 };
326 
327 struct PlaybackState : public DetectorGraph::TopicState
328 {
329  bool autoplay;
330  PlaybackState(bool aAutoplay = false) : autoplay(aAutoplay) {}
331 };
332 //! [TopicStates Inheritance Example]
333 
334 class ThemeDetector : public DetectorGraph::Detector
335 , public DetectorGraph::SubscriberInterface<RhytmBeats>
336 , public DetectorGraph::Publisher<SongThemeState>
337 {
338 public:
339 //! [ThemeDetector Constructor]
340  ThemeDetector(DetectorGraph::Graph* graph, DetectorGraph::TimeoutPublisherService* timeService)
341  : DetectorGraph::Detector(graph)
342  , kBeatsPerTheme({
343  {"Hey Jude", 2}, // 0
344  {"don't", 14}, // 1
345  {"Remember to", 6}, // 2
346  {"then you", 10}, // 3
347  {" ... better faster stronger ... ", 40}, // 4
348  {"Na Na Na", 12}, // 5
349  {"So let it out and let it in", 2}, // 6
350  {"...", 18}, // 7
351  })
352  , kThemeSequence({0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 6, 0, 7, 0, 7, 5, 0, 1, 2, 5, 5})
353  , mBeatsInSectionCount(0)
354  , mThemeIt()
355  {
356  SetupPeriodicPublishing<RhytmBeats>(RhytmBeats::kPeriod, timeService);
357  Subscribe<RhytmBeats>(this);
358  SetupPublishing<SongThemeState>(this);
359  mThemeIt = kThemeSequence.begin();
360  }
361 //! [ThemeDetector Constructor]
362 
363  virtual void Evaluate(const RhytmBeats&)
364  {
365  if (mBeatsInSectionCount++ == 0)
366  {
367  Publish(SongThemeState(kBeatsPerTheme[*mThemeIt].first));
368  }
369 
370  if (mBeatsInSectionCount == kBeatsPerTheme[*mThemeIt].second)
371  {
372  // Prep for next beat
373  if (++mThemeIt == kThemeSequence.end()) mThemeIt = kThemeSequence.begin();
374  mBeatsInSectionCount = 0;
375  }
376  }
377 private:
378  using BeatsAndThemePair = std::pair<std::string, int>;
379  const std::vector<BeatsAndThemePair> kBeatsPerTheme;
380  const std::vector<int> kThemeSequence;
381  int mBeatsInSectionCount;
382  std::vector<int>::const_iterator mThemeIt;
383 };
384 
385 struct DontPause : public DetectorGraph::TopicState {};
386 
387 class DontDetector : public DetectorGraph::Detector
388 , public DetectorGraph::SubscriberInterface<SongThemeState>
389 , public DetectorGraph::Publisher<DontThemeState>
390 , public DetectorGraph::SubscriberInterface<DontPause>
391 , public DetectorGraph::TimeoutPublisher<DontPause>
392 {
393 public:
394  DontDetector(DetectorGraph::Graph* graph, DetectorGraph::TimeoutPublisherService* timeService)
395  : DetectorGraph::Detector(graph)
396  , mDontIndex(0)
397  , kResponses({"make it bad, take a sad song and make it better",
398  "be afraid, you were made to go out and get her",
399  "let me down, you have found her, now go and get her"})
400  {
401  Subscribe<SongThemeState>(this);
402  SetupPublishing<DontThemeState>(this);
403 
404  Subscribe<DontPause>(this);
405  SetupTimeoutPublishing<DontPause>(this, timeService);
406  }
407 
408  virtual void Evaluate(const SongThemeState& aSongThemeState)
409  {
410  // If this is our cue
411  if (aSongThemeState.line.find("don't") != std::string::npos)
412  {
413  PublishOnTimeout(DontPause(), RhytmBeats::kPeriod);
414  }
415  }
416  virtual void Evaluate(const DontPause&)
417  {
418  Publish(DontThemeState(kResponses[mDontIndex]));
419  mDontIndex++;
420  mDontIndex %= kResponses.size();
421  }
422 private:
423  DontThemeState mDontState;
424  int mDontIndex;
425  const std::vector<std::string> kResponses;
426 };
427 
428 struct RememberPause : public DetectorGraph::TopicState {};
429 
430 class RememberDetector : public DetectorGraph::Detector
431 , public DetectorGraph::SubscriberInterface<SongThemeState>
432 , public DetectorGraph::Publisher<RememberState>
433 , public DetectorGraph::SubscriberInterface<RememberPause>
434 , public DetectorGraph::TimeoutPublisher<RememberPause>
435 {
436 public:
437  RememberDetector(DetectorGraph::Graph* graph, DetectorGraph::TimeoutPublisherService* timeService)
438  : DetectorGraph::Detector(graph)
439  , mRememberIndex(0)
440  , kResponses({"let her into your heart",
441  "let her under your skin"})
442  {
443  Subscribe<SongThemeState>(this);
444  SetupPublishing<RememberState>(this);
445 
446  Subscribe<RememberPause>(this);
447  SetupTimeoutPublishing<RememberPause>(this, timeService);
448  }
449 
450  virtual void Evaluate(const SongThemeState& aSongThemeState)
451  {
452  // If this is our cue
453  if (aSongThemeState.line.find("Remember") != std::string::npos)
454  {
455  PublishOnTimeout(RememberPause(), RhytmBeats::kPeriod);
456  }
457  }
458  virtual void Evaluate(const RememberPause&)
459  {
460  Publish(RememberState(kResponses[mRememberIndex]));
461  mRememberIndex++;
462  mRememberIndex %= kResponses.size();
463  }
464 private:
465  int mRememberIndex;
466  const std::vector<std::string> kResponses;
467 };
468 
469 class ThenYouDetector : public Detector
470 , public DetectorGraph::SubscriberInterface<SongThemeState>
471 , public DetectorGraph::TimeoutPublisher<ThenYouState>
472 {
473 public:
474  ThenYouDetector(DetectorGraph::Graph* graph, DetectorGraph::TimeoutPublisherService* timeService)
475  : DetectorGraph::Detector(graph)
476  , mThenYouIndex(0)
477  , kResponses({"can start", "begin"})
478  , kResponseEnd("to make it better")
479  {
480  Subscribe<SongThemeState>(this);
481  SetupTimeoutPublishing<ThenYouState>(this, timeService);
482  }
483 
484  virtual void Evaluate(const SongThemeState& aSongThemeState)
485  {
486  // If this is our cue
487  if (aSongThemeState.line.find("then you") != std::string::npos)
488  {
489  PublishOnTimeout(ThenYouState(kResponses[mThenYouIndex] + " " + kResponseEnd), RhytmBeats::kPeriod);
490  mThenYouIndex++;
491  mThenYouIndex %= kResponses.size();
492  }
493  }
494 private:
495  int mThenYouIndex;
496  const std::vector<std::string> kResponses;
497  const std::string kResponseEnd;
498 };
499 
500 class TooManyNaNaNasDetector : public DetectorGraph::Detector
501 , public DetectorGraph::SubscriberInterface<SongThemeState>
502 , public DetectorGraph::Publisher<PlaybackState>
503 {
504 public:
505  TooManyNaNaNasDetector(DetectorGraph::Graph* graph) : DetectorGraph::Detector(graph), nananaCount(0)
506  {
507  Subscribe<SongThemeState>(this);
508  SetupPublishing<PlaybackState>(this);
509  }
510 
511  virtual void Evaluate(const SongThemeState& aSongThemeState)
512  {
513  if (aSongThemeState.line.find("Na") != std::string::npos)
514  {
515  if (++nananaCount == kTotalNa)
516  {
517  Publish(PlaybackState(false));
518  }
519  }
520  }
521 
522 private:
523  int nananaCount;
524  const int kTotalNa = 4;
525 
526 };
527 
528 class BeatMachine : public DetectorGraph::ProcessorContainer
529 {
530 public:
531  BeatMachine()
532  : mTimeService(mGraph)
533  , mThemeDetector(&mGraph, &mTimeService)
534  , mDontDetector(&mGraph, &mTimeService)
535  , mRememberDetector(&mGraph, &mTimeService)
536  , mThenYouDetector(&mGraph, &mTimeService)
537  , mTooManyNaNaNasDetector(&mGraph)
538  , mCurrentPlaybackState()
539  {
540  }
541 
542  SleepBasedTimeoutPublisherService mTimeService;
543  ThemeDetector mThemeDetector;
544  DontDetector mDontDetector;
545  RememberDetector mRememberDetector;
546  ThenYouDetector mThenYouDetector;
547  TooManyNaNaNasDetector mTooManyNaNaNasDetector;
548 
549  PlaybackState mCurrentPlaybackState;
550 
551  void StartLoop()
552  {
553  mTimeService.StartPeriodicPublishing();
554  while(mCurrentPlaybackState.autoplay && mTimeService.SleepTillBrooklyn())
555  {
556  ProcessGraph();
557  }
558  }
559 
560  virtual void ProcessOutput()
561  {
562  auto themeTopic = mGraph.ResolveTopic<SongThemeState>();
563  if (themeTopic->HasNewValue())
564  {
565  cout << themeTopic->GetNewValue() << endl;
566  }
567 
568  auto dontsTopic = mGraph.ResolveTopic<DontThemeState>();
569  if (dontsTopic->HasNewValue())
570  {
571  cout << dontsTopic->GetNewValue() << endl;
572  }
573 
574  auto rememberTopic = mGraph.ResolveTopic<RememberState>();
575  if (rememberTopic->HasNewValue())
576  {
577  cout << rememberTopic->GetNewValue() << endl;
578  }
579 
580  auto thenYouTopic = mGraph.ResolveTopic<ThenYouState>();
581  if (thenYouTopic->HasNewValue())
582  {
583  cout << thenYouTopic->GetNewValue() << endl;
584  }
585 
586  auto playbackTopic = mGraph.ResolveTopic<PlaybackState>();
587  if (playbackTopic->HasNewValue())
588  {
589  mCurrentPlaybackState = playbackTopic->GetNewValue();
590  }
591  }
592 };
593 
594 int main()
595 {
596  BeatMachine beatMachine;
597 
598  beatMachine.ProcessData(PlaybackState(true));
599  beatMachine.StartLoop();
600 
601  DetectorGraph::GraphAnalyzer analyzer(beatMachine.mGraph);
602  analyzer.GenerateDotFile("beat_machine.dot");
603 }
604 
605 /// @endcond DO_NOT_DOCUMENT
Push data to a topic when timer expires.
Implements a graph of Topics & Detectors with Input/Output APIs.
Definition: graph.hpp:127
A Base class for a basic Graph container.
A service that provides Timer function to DetectorGraph Detectors.
Base struct for topic data types.
Definition: topicstate.hpp:52
Base class that implements a Publisher behavior.
Definition: publisher.hpp:66
A unit of logic in a DetectorGraph.
Definition: detector.hpp:68
A Pure interface that declares the Subscriber behavior.
Class that provides debugging/diagnostics to a DetectorGraph detector graph.