DeviceRecorder.hpp
1 #pragma once
2 #include <State/Value.hpp>
3 
4 #include <ossia/detail/parse_strict.hpp>
5 #include <ossia/network/value/detail/value_conversion_impl.hpp>
6 
7 #include <QDateTime>
8 #include <QFile>
9 
10 #include <AvndProcesses/AddressTools.hpp>
11 #include <halp/audio.hpp>
37 namespace csv2
38 {
39 namespace trim_policy
40 {
42 {
43 public:
44  static std::pair<size_t, size_t> trim(const char* buffer, size_t start, size_t end)
45  {
46  (void)(buffer); // to silence unused parameter warning
47  return {start, end};
48  }
49 };
50 
51 template <char... character_list>
53 {
54 private:
55  constexpr static bool is_trim_char(char) { return false; }
56 
57  template <class... Tail>
58  constexpr static bool is_trim_char(char c, char head, Tail... tail)
59  {
60  return c == head || is_trim_char(c, tail...);
61  }
62 
63 public:
64  static std::pair<size_t, size_t> trim(const char* buffer, size_t start, size_t end)
65  {
66  size_t new_start = start, new_end = end;
67  while(new_start != new_end && is_trim_char(buffer[new_start], character_list...))
68  ++new_start;
69  while(new_start != new_end && is_trim_char(buffer[new_end - 1], character_list...))
70  --new_end;
71  return {new_start, new_end};
72  }
73 };
74 
76 } // namespace trim_policy
77 
78 template <char character>
79 struct delimiter
80 {
81  constexpr static char value = character;
82 };
83 
84 template <char character>
86 {
87  constexpr static char value = character;
88 };
89 
90 template <bool flag>
92 {
93  constexpr static bool value = flag;
94 };
95 
96 template <
99  class trim_policy = trim_policy::trim_whitespace>
100 class Reader
101 {
102  const char* buffer_{nullptr}; // pointer to memory-mapped data
103  size_t buffer_size_{0}; // mapped length of buffer
104  size_t header_start_{0}; // start index of header (cache)
105  size_t header_end_{0}; // end index of header (cache)
106 
107 public:
108  bool parse_view(std::string_view sv)
109  {
110  buffer_ = sv.data();
111  buffer_size_ = sv.size();
112  return buffer_size_ > 0;
113  }
114 
115  class RowIterator;
116  class Row;
117  class CellIterator;
118 
119  class Cell
120  {
121  const char* buffer_{nullptr}; // Pointer to memory-mapped buffer
122  size_t start_{0}; // Start index of cell content
123  size_t end_{0}; // End index of cell content
124  bool escaped_{false}; // Does the cell have escaped content?
125  friend class Row;
126  friend class CellIterator;
127 
128  public:
129  // returns a view on the cell's contents if C++17 available
130  std::string_view read_view() const
131  {
132  const auto new_start_end = trim_policy::trim(buffer_, start_, end_);
133  return std::string_view(
134  buffer_ + new_start_end.first, new_start_end.second - new_start_end.first);
135  }
136  // Returns the raw_value of the cell without handling escaped
137  // content, e.g., cell containing """foo""" will be returned
138  // as is
139  template <typename Container>
140  void read_raw_value(Container& result) const
141  {
142  if(start_ >= end_)
143  return;
144  result.reserve(end_ - start_);
145  for(size_t i = start_; i < end_; ++i)
146  result.push_back(buffer_[i]);
147  }
148 
149  // If cell is escaped, convert and return correct cell contents,
150  // e.g., """foo""" => ""foo""
151  template <typename Container>
152  void read_value(Container& result) const
153  {
154  if(start_ >= end_)
155  return;
156  result.reserve(end_ - start_);
157  const auto new_start_end = trim_policy::trim(buffer_, start_, end_);
158  for(size_t i = new_start_end.first; i < new_start_end.second; ++i)
159  result.push_back(buffer_[i]);
160  for(size_t i = 1; i < result.size(); ++i)
161  {
162  if(result[i] == quote_character::value
163  && result[i - 1] == quote_character::value)
164  {
165  result.erase(i - 1, 1);
166  }
167  }
168  }
169  };
170 
171  class Row
172  {
173  const char* buffer_{nullptr}; // Pointer to memory-mapped buffer
174  size_t start_{0}; // Start index of row content
175  size_t end_{0}; // End index of row content
176  friend class RowIterator;
177  friend class Reader;
178 
179  public:
180  // address of row
181  const char* address() const { return buffer_; }
182  // returns the char length of the row
183  size_t length() const { return end_ - start_; }
184 
185  // Returns the raw_value of the row
186  template <typename Container>
187  void read_raw_value(Container& result) const
188  {
189  if(start_ >= end_)
190  return;
191  result.reserve(end_ - start_);
192  for(size_t i = start_; i < end_; ++i)
193  result.push_back(buffer_[i]);
194  }
195 
197  {
198  friend class Row;
199  const char* buffer_;
200  size_t buffer_size_;
201  size_t start_;
202  size_t current_;
203  size_t end_;
204 
205  public:
206  CellIterator(const char* buffer, size_t buffer_size, size_t start, size_t end)
207  : buffer_(buffer)
208  , buffer_size_(buffer_size)
209  , start_(start)
210  , current_(start_)
211  , end_(end)
212  {
213  }
214 
215  CellIterator& operator++()
216  {
217  current_ += 1;
218  return *this;
219  }
220 
221  Cell operator*()
222  {
223  bool escaped{false};
224  class Cell cell;
225  cell.buffer_ = buffer_;
226  cell.start_ = current_;
227  cell.end_ = end_;
228 
229  size_t last_quote_location = 0;
230  bool quote_opened = false;
231  for(auto i = current_; i < end_; i++)
232  {
233  current_ = i;
234  if(buffer_[i] == delimiter::value && !quote_opened)
235  {
236  // actual delimiter
237  // end of cell
238  cell.end_ = current_;
239  cell.escaped_ = escaped;
240  return cell;
241  }
242  else
243  {
244  if(buffer_[i] == quote_character::value)
245  {
246  if(!quote_opened)
247  {
248  // first quote for this cell
249  quote_opened = true;
250  last_quote_location = i;
251  }
252  else
253  {
254  escaped = (last_quote_location == i - 1);
255  last_quote_location += (i - last_quote_location) * size_t(!escaped);
256  quote_opened = escaped || (buffer_[i + 1] != delimiter::value);
257  }
258  }
259  }
260  }
261  cell.end_ = current_ + 1;
262  return cell;
263  }
264 
265  bool operator!=(const CellIterator& rhs) { return current_ != rhs.current_; }
266  };
267 
268  CellIterator begin() const
269  {
270  return CellIterator(buffer_, end_ - start_, start_, end_);
271  }
272  CellIterator end() const { return CellIterator(buffer_, end_ - start_, end_, end_); }
273  };
274 
276  {
277  friend class Reader;
278  const char* buffer_;
279  size_t buffer_size_;
280  size_t start_;
281  size_t end_;
282 
283  public:
284  RowIterator(const char* buffer, size_t buffer_size, size_t start)
285  : buffer_(buffer)
286  , buffer_size_(buffer_size)
287  , start_(start)
288  , end_(start_)
289  {
290  }
291 
292  RowIterator& operator++()
293  {
294  start_ = end_ + 1;
295  end_ = start_;
296  return *this;
297  }
298 
299  Row operator*()
300  {
301  Row result;
302  result.buffer_ = buffer_;
303  result.start_ = start_;
304  result.end_ = end_;
305 
306  if(const char* ptr = static_cast<const char*>(
307  memchr(&buffer_[start_], '\n', (buffer_size_ - start_))))
308  {
309  end_ = start_ + (ptr - &buffer_[start_]);
310  result.end_ = end_;
311  start_ = end_ + 1;
312  }
313  else
314  {
315  // last row
316  end_ = buffer_size_;
317  result.end_ = end_;
318  }
319  return result;
320  }
321 
322  bool operator!=(const RowIterator& rhs) { return start_ != rhs.start_; }
323  };
324 
325  RowIterator begin() const
326  {
327  if(buffer_size_ == 0)
328  return end();
329  if(first_row_is_header::value)
330  {
331  const auto header_indices = header_indices_();
332  return RowIterator(
333  buffer_, buffer_size_,
334  header_indices.second > 0 ? header_indices.second + 1 : 0);
335  }
336  else
337  {
338  return RowIterator(buffer_, buffer_size_, 0);
339  }
340  }
341 
342  RowIterator end() const
343  {
344  return RowIterator(buffer_, buffer_size_, buffer_size_ + 1);
345  }
346 
347 private:
348  std::pair<size_t, size_t> header_indices_() const
349  {
350  size_t start = 0, end = 0;
351 
352  if(const char* ptr
353  = static_cast<const char*>(memchr(&buffer_[start], '\n', (buffer_size_ - start))))
354  {
355  end = start + (ptr - &buffer_[start]);
356  }
357  return {start, end};
358  }
359 
360 public:
361  Row header() const
362  {
363  size_t start = 0, end = 0;
364  Row result;
365  result.buffer_ = buffer_;
366  result.start_ = start;
367  result.end_ = end;
368 
369  if(const char* ptr
370  = static_cast<const char*>(memchr(&buffer_[start], '\n', (buffer_size_ - start))))
371  {
372  end = start + (ptr - &buffer_[start]);
373  result.end_ = end;
374  }
375  return result;
376  }
377 
381  size_t rows(bool ignore_empty_lines = false) const
382  {
383  size_t result{0};
384  if(!buffer_ || buffer_size_ == 0)
385  return result;
386 
387  // Count the first row if not header
388  if(not first_row_is_header::value
389  and (not ignore_empty_lines or *(static_cast<const char*>(buffer_)) != '\r'))
390  ++result;
391 
392  for(const char* p = buffer_;
393  (p = static_cast<const char*>(memchr(p, '\n', (buffer_ + buffer_size_) - p)));
394  ++p)
395  {
396  if(ignore_empty_lines and (p >= buffer_ + buffer_size_ - 1 or *(p + 1) == '\r'))
397  continue;
398  ++result;
399  }
400  return result;
401  }
402 
403  size_t cols() const { return header().length(); }
404 };
405 }
406 
407 namespace avnd_tools
408 {
409 
416 {
417  halp_meta(name, "CSV recorder")
418  halp_meta(author, "ossia team")
419  halp_meta(category, "Control/Recording")
420  halp_meta(description, "Record the messages of a device at regular interval")
421  halp_meta(c_name, "avnd_device_recorder")
422  halp_meta(uuid, "7161ca22-5684-48f2-bde7-88933500a7fb")
423  halp_meta(manual_url, "https://ossia.io/score-docs/processes/csv-recorder.html#csv-recorder")
424 
425  // Threaded worker
426  struct recorder_thread
427  {
428  QFile f{};
429  std::string filename;
430  std::vector<ossia::net::node_base*> roots;
431  std::chrono::steady_clock::time_point first_ts;
432  fmt::memory_buffer buf;
433  bool active{};
434  bool first_is_timestamp = false;
435  int num_params = 0;
436 
437  void setActive(bool b)
438  {
439  active = b;
440  if(!b)
441  f.close();
442  else
443  reopen();
444  }
445 
446  void reopen()
447  {
448  f.close();
449 
450  auto filename = QByteArray::fromStdString(this->filename);
451  filename.replace("%t", QDateTime::currentDateTimeUtc().toString().toUtf8());
452  f.setFileName(filename);
453  if(filename.isEmpty())
454  return;
455 
456  if(!active)
457  return;
458 
459  f.open(QIODevice::WriteOnly);
460  if(!f.isOpen())
461  return;
462 
463  f.write("timestamp");
464  num_params = 0;
465  for(auto in : this->roots)
466  {
467  if(auto p = in->get_parameter())
468  {
469  f.write(",");
470  f.write(QByteArray::fromStdString(p->get_node().osc_address()));
471 
472  num_params++;
473  }
474  }
475  f.write("\n");
476  f.flush();
477 
478  first_ts = std::chrono::steady_clock::now();
479  buf.clear();
480  buf.reserve(512);
481  }
482 
483  void write()
484  {
485  if(!f.isOpen())
486  return;
487 
488  using namespace std::chrono;
489  const auto ts
490  = duration_cast<milliseconds>(steady_clock::now() - first_ts).count();
491  write(ts);
492  }
493 
494  void write(int64_t timestamp)
495  {
496  f.write(QString::number(timestamp).toUtf8());
497  for(auto in : this->roots)
498  {
499  if(auto p = in->get_parameter())
500  {
501  f.write(",");
502  buf.clear();
503 
504  ossia::apply(ossia::detail::fmt_writer{buf}, p->value());
505 
506  std::string_view sv(buf.data(), buf.data() + buf.size());
507  if(sv.find_first_of(", \"\n\r\t;") != std::string_view::npos)
508  {
509  // FIXME quote escaping
510  f.write("\"", 1);
511  f.write(buf.data(), buf.size());
512  f.write("\"", 1);
513  }
514  else
515  {
516  f.write(buf.data(), buf.size());
517  }
518  }
519  }
520  f.write("\n");
521  f.flush();
522  }
523  };
524 
526  {
527  QFile f{};
528  std::string filename;
529  std::vector<ossia::net::node_base*> roots;
530  std::chrono::steady_clock::time_point first_ts;
531 
532  // FIXME boost::multi_array
533  boost::container::flat_map<int, ossia::net::parameter_base*> m_map;
534  boost::container::flat_map<int64_t, std::vector<ossia::value>> m_vec_ts;
535  std::vector<std::vector<ossia::value>> m_vec_no_ts;
536  bool active{};
537  bool loops{};
538  bool first_is_timestamp = false;
539  int num_params{};
540 
541  void setActive(bool b)
542  {
543  active = b;
544  if(!b)
545  f.close();
546  else
547  reopen();
548  }
549 
550  void setLoops(bool b) { loops = b; }
551  void reopen()
552  {
553  f.close();
554 
555  auto filename = QByteArray::fromStdString(this->filename).trimmed();
556  filename.replace("%t", QDateTime::currentDateTimeUtc().toString().toUtf8());
557  f.setFileName(filename);
558  if(filename.isEmpty())
559  return;
560 
561  if(!active)
562  return;
563 
564  f.open(QIODevice::ReadOnly);
565  if(!f.isOpen())
566  return;
567  if(f.size() <= 0)
568  return;
569 
570  // FIXME not valid when the OSC device changes
571  // We need to parse the header instead and have a map.
572  num_params = 0;
573  for(auto in : this->roots)
574  {
575  if(auto p = in->get_parameter())
576  {
577  num_params++;
578  }
579  }
580 
581  auto data = (const char*)f.map(0, f.size());
582  m_map.clear();
583 
584  csv2::Reader<> r;
585  r.parse_view({data, data + f.size()});
586  int columns = r.cols();
587 
588  auto header = r.header();
589 
590  boost::container::flat_map<std::string, ossia::net::parameter_base*> params;
591 
592  for(auto node : roots)
593  if(auto p = node->get_parameter())
594  params[node->osc_address()] = p;
595 
596  std::string v;
597  v.reserve(128);
598  int i = 0;
599  auto header_it = header.begin();
600  if(first_is_timestamp)
601  ++header_it;
602  for(; header_it != header.end(); ++header_it)
603  {
604  auto addr = *header_it;
605  v.clear();
606  addr.read_raw_value(v);
607  if(auto it = params.find(v); it != params.end())
608  {
609  m_map[i] = it->second;
610  }
611  i++;
612  }
613 
614  m_vec_ts.clear();
615  m_vec_no_ts.clear();
616  if(first_is_timestamp)
617  {
618  m_vec_ts.reserve(r.rows());
619  for(const auto& row : r)
620  {
621  parse_row_with_timestamps(columns, row, v);
622  v.clear();
623  }
624  }
625  else
626  {
627  m_vec_no_ts.reserve(r.rows());
628  for(const auto& row : r)
629  {
630  parse_row_no_timestamps(columns, row, v);
631  v.clear();
632  }
633  }
634  first_ts = std::chrono::steady_clock::now();
635  }
636 
637  void parse_cell_impl(
638  const std::string& v, ossia::net::parameter_base& param, ossia::value& out)
639  {
640  if(!v.empty())
641  {
642  std::optional<ossia::value> res;
643  if(v.starts_with('"') && v.ends_with('"'))
644  res = State::parseValue(std::string_view(v).substr(1, v.size() - 2));
645  else
646  res = State::parseValue(v);
647 
648  if(res)
649  {
650  out = std::move(*res);
651  if(auto t = param.get_value_type(); out.get_type() != t)
652  {
653  ossia::convert(out, t);
654  }
655  }
656  }
657  }
658 
659  void
660  parse_cell(const auto& cell, std::string& v, std::vector<ossia::value>& vec, int i)
661  {
662  if(auto param = m_map[i])
663  {
664  v.clear();
665  cell.read_value(v);
666  parse_cell_impl(v, *param, vec[i]);
667  v.clear();
668  }
669  }
670 
671  void parse_row_no_timestamps(int columns, auto& row, std::string& v)
672  {
673  auto& vec = this->m_vec_no_ts.emplace_back(columns);
674  int i = 0;
675 
676  for(auto it = row.begin(); it != row.end(); ++it)
677  {
678  parse_cell(*it, v, vec, i);
679  i++;
680  }
681  }
682 
683  void parse_row_with_timestamps(int columns, auto& row, std::string& v)
684  {
685  if(row.length() <= 1)
686  return;
687 
688  auto it = row.begin();
689  const auto& ts = *it;
690 
691  ts.read_value(v);
692  auto tstamp = ossia::parse_strict<int64_t>(v);
693  if(!tstamp)
694  return;
695  auto& vec = this->m_vec_ts[*tstamp];
696  vec.resize(columns - 1);
697  int i = 0;
698 
699  for(++it; it != row.end(); ++it)
700  {
701  parse_cell(*it, v, vec, i);
702  i++;
703  }
704  }
705 
706  void read()
707  {
708  if(first_is_timestamp)
709  {
710  if(m_vec_ts.empty())
711  return;
712 
713  using namespace std::chrono;
714  auto ts = duration_cast<milliseconds>(steady_clock::now() - first_ts).count();
715  if(loops)
716  ts %= m_vec_ts.rbegin()->first + 1;
717  read_ts(ts);
718  }
719  else
720  {
721  if(m_vec_no_ts.empty())
722  return;
723 
724  using namespace std::chrono;
725  auto ts = duration_cast<milliseconds>(steady_clock::now() - first_ts).count();
726  if(loops && ts >= std::ssize(m_vec_no_ts))
727  ts = 0;
728  read_no_ts(ts);
729  }
730  }
731 
732  void read_no_ts(int64_t timestamp)
733  {
734  if(timestamp < 0)
735  return;
736  if(timestamp >= std::ssize(m_vec_no_ts))
737  return;
738  auto it = m_vec_no_ts.begin() + timestamp;
739  if(it != m_vec_no_ts.end())
740  {
741  int i = 0;
742  for(auto& v : *it)
743  {
744  if(v.valid())
745  {
746  if(auto p = m_map.find(i); p != m_map.end())
747  {
748  p->second->push_value(v);
749  }
750  }
751  i++;
752  }
753  }
754  }
755  void read_ts(int64_t timestamp)
756  {
757  auto it = m_vec_ts.lower_bound(timestamp);
758  if(it != m_vec_ts.end())
759  {
760  int i = 0;
761  for(auto& v : it->second)
762  {
763  if(v.valid())
764  {
765  if(auto p = m_map.find(i); p != m_map.end())
766  {
767  p->second->push_value(v);
768  }
769  }
770  i++;
771  }
772  }
773  }
774  };
775  std::shared_ptr<recorder_thread> record_impl = std::make_shared<recorder_thread>();
776  std::shared_ptr<player_thread> play_impl = std::make_shared<player_thread>();
777 
778  // Object definition
779  struct inputs_t
780  {
781  PatternSelector pattern;
782  halp::time_chooser<"Interval", halp::range{.min = 0.00001, .max = 5., .init = 0.25}>
783  time;
784  struct : halp::lineedit<"File pattern", "">
785  {
786  void update(DeviceRecorder& self) { self.update(); }
787  } filename;
788  struct
789  {
790  halp__enum("Mode", None, None, Record, Playback, Loop)
791  void update(DeviceRecorder& self) { self.setMode(); }
792  } mode;
793  struct ts : halp::toggle<"Timestamped", halp::default_on_toggle>
794  {
795  halp_meta(description, "Set to true to use the first column as timestamp")
796  } timestamped;
797  } inputs;
798 
799  struct
800  {
801  } outputs;
802 
804  {
805  std::shared_ptr<recorder_thread> recorder;
806  std::shared_ptr<player_thread> player;
807  std::string path;
808  std::vector<ossia::net::node_base*> roots;
809  bool first_is_timestamp{};
810 
811  void operator()()
812  {
813  using namespace std;
814  swap(recorder->filename, path);
815  swap(recorder->roots, roots);
816  player->filename = recorder->filename;
817  player->roots = recorder->roots;
818  player->first_is_timestamp = first_is_timestamp;
819  recorder->first_is_timestamp = first_is_timestamp;
820  recorder->reopen();
821  player->reopen();
822  }
823  };
824 
826  {
827  std::shared_ptr<recorder_thread> recorder;
828  std::shared_ptr<player_thread> player;
829  std::string path;
830  bool first_is_timestamp{};
831  void operator()()
832  {
833  using namespace std;
834  swap(recorder->filename, path);
835  player->filename = recorder->filename;
836  player->first_is_timestamp = first_is_timestamp;
837  recorder->first_is_timestamp = first_is_timestamp;
838  recorder->reopen();
839  player->reopen();
840  }
841  };
842 
844  {
845  std::shared_ptr<recorder_thread> recorder;
846  void operator()() { recorder->write(); }
847  };
848 
850  {
851  std::shared_ptr<player_thread> player;
852  void operator()() { player->read(); }
853  };
854 
856  {
857  std::shared_ptr<recorder_thread> recorder;
858  std::shared_ptr<player_thread> player;
859  using mode_type = decltype(DeviceRecorder::inputs_t{}.mode.value);
860  mode_type mode{};
861  void operator()()
862  {
863  recorder->setActive(mode == mode_type::Record);
864  player->setActive(mode == mode_type::Playback || mode == mode_type::Loop);
865  player->setLoops(mode == mode_type::Loop);
866  }
867  };
868 
869  using worker_message = ossia::variant<
870  std::unique_ptr<reset_message>, reset_path_message, process_message,
872 
873  struct
874  {
875  std::function<void(worker_message)> request;
876  static void work(worker_message&& mess)
877  {
878  ossia::visit([&]<typename M>(M&& msg) {
879  if constexpr(requires { *msg; })
880  (*std::forward<M>(msg))();
881  else
882  std::forward<M>(msg)();
883  }, std::move(mess));
884  }
885  } worker;
886 
887  using tick = halp::tick_musical;
888 
889  void setMode()
890  {
891  worker.request(activate_message{record_impl, play_impl, inputs.mode.value});
892  }
893  void update()
894  {
895  worker.request(
896  reset_path_message{record_impl, play_impl, inputs.filename, inputs.timestamped});
897  }
898 
899  void operator()(const halp::tick_musical& tk)
900  {
901  int64_t elapsed_ns = 0.;
902  if(!first_message_sent_pos)
903  first_message_sent_pos = tk.position_in_nanoseconds;
904  if(last_message_sent_pos)
905  elapsed_ns = tk.position_in_nanoseconds - *last_message_sent_pos;
906 
907  if(elapsed_ns > 0 && elapsed_ns < inputs.time.value * 1e9)
908  return;
909  last_message_sent_pos = tk.position_in_nanoseconds;
910 
911  if(!m_path)
912  return;
913 
914  if(!std::exchange(started, true))
915  {
916  inputs.pattern.reprocess();
917  worker.request(std::unique_ptr<reset_message>(new reset_message{
918  record_impl, play_impl, inputs.filename, roots, inputs.timestamped}));
919  }
920 
921  switch(inputs.mode)
922  {
923  case decltype(inputs.mode)::None:
924  break;
925  case decltype(inputs.mode)::Record:
926  worker.request(process_message{record_impl});
927  break;
928  case decltype(inputs.mode)::Playback:
929  case decltype(inputs.mode)::Loop:
930  worker.request(playback_message{play_impl});
931  break;
932  }
933  }
934 
935  bool started{};
936  std::optional<int64_t> first_message_sent_pos;
937  std::optional<int64_t> last_message_sent_pos;
938 };
939 }
Definition: DeviceRecorder.hpp:120
Definition: DeviceRecorder.hpp:197
Definition: DeviceRecorder.hpp:172
Definition: DeviceRecorder.hpp:276
Definition: DeviceRecorder.hpp:101
size_t rows(bool ignore_empty_lines=false) const
Definition: DeviceRecorder.hpp:381
Definition: DeviceRecorder.hpp:38
Definition: DeviceRecorder.hpp:856
Definition: DeviceRecorder.hpp:794
Definition: DeviceRecorder.hpp:780
Definition: DeviceRecorder.hpp:850
Definition: DeviceRecorder.hpp:526
Definition: DeviceRecorder.hpp:844
Definition: DeviceRecorder.hpp:804
Definition: DeviceRecorder.hpp:826
Definition: DeviceRecorder.hpp:416
Definition: AddressTools.hpp:21
Definition: AddressTools.hpp:28
Definition: DeviceRecorder.hpp:80
Definition: DeviceRecorder.hpp:92
Definition: DeviceRecorder.hpp:86
Definition: DeviceRecorder.hpp:42
Definition: DeviceRecorder.hpp:53