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
404  {
405  const auto& h = header();
406  int n = 0;
407  for(const auto& e : h)
408  {
409  n++;
410  }
411  return n;
412  }
413 };
414 }
415 
416 namespace avnd_tools
417 {
418 
425 {
426  halp_meta(name, "CSV recorder")
427  halp_meta(author, "ossia team")
428  halp_meta(category, "Control/Recording")
429  halp_meta(description, "Record the messages of a device at regular interval")
430  halp_meta(c_name, "avnd_device_recorder")
431  halp_meta(uuid, "7161ca22-5684-48f2-bde7-88933500a7fb")
432  halp_meta(manual_url, "https://ossia.io/score-docs/processes/csv-recorder.html#csv-recorder")
433 
434  // Threaded worker
435  struct recorder_thread
436  {
437  QFile f{};
438  std::string filename;
439  std::vector<ossia::net::node_base*> roots;
440  std::chrono::steady_clock::time_point first_ts;
441  fmt::memory_buffer buf;
442  bool active{};
443  bool first_is_timestamp = false;
444  int num_params = 0;
445 
446  void setActive(bool b)
447  {
448  active = b;
449  if(!b)
450  f.close();
451  else
452  reopen();
453  }
454 
455  void reopen()
456  {
457  f.close();
458 
459  auto filename = QByteArray::fromStdString(this->filename);
460  filename.replace("%t", QDateTime::currentDateTimeUtc().toString().toUtf8());
461  f.setFileName(filename);
462  if(filename.isEmpty())
463  return;
464 
465  if(!active)
466  return;
467 
468  f.open(QIODevice::WriteOnly);
469  if(!f.isOpen())
470  return;
471 
472  f.write("timestamp");
473  num_params = 0;
474  for(auto in : this->roots)
475  {
476  if(auto p = in->get_parameter())
477  {
478  f.write(",");
479  f.write(QByteArray::fromStdString(p->get_node().osc_address()));
480 
481  num_params++;
482  }
483  }
484  f.write("\n");
485  f.flush();
486 
487  first_ts = std::chrono::steady_clock::now();
488  buf.clear();
489  buf.reserve(512);
490  }
491 
492  void write()
493  {
494  if(!f.isOpen())
495  return;
496 
497  using namespace std::chrono;
498  const auto ts
499  = duration_cast<milliseconds>(steady_clock::now() - first_ts).count();
500  write(ts);
501  }
502 
503  void write(int64_t timestamp)
504  {
505  f.write(QString::number(timestamp).toUtf8());
506  for(auto in : this->roots)
507  {
508  if(auto p = in->get_parameter())
509  {
510  f.write(",");
511  buf.clear();
512 
513  ossia::apply(ossia::detail::fmt_writer{buf}, p->value());
514 
515  std::string_view sv(buf.data(), buf.data() + buf.size());
516  if(sv.find_first_of(", \"\n\r\t;") != std::string_view::npos)
517  {
518  // FIXME quote escaping
519  f.write("\"", 1);
520  f.write(buf.data(), buf.size());
521  f.write("\"", 1);
522  }
523  else
524  {
525  f.write(buf.data(), buf.size());
526  }
527  }
528  }
529  f.write("\n");
530  f.flush();
531  }
532  };
533 
535  {
536  QFile f{};
537  std::string filename;
538  std::vector<ossia::net::node_base*> roots;
539  std::chrono::steady_clock::time_point first_ts;
540  int64_t nots_index{};
541 
542  // FIXME boost::multi_array
543  boost::container::flat_map<int, ossia::net::parameter_base*> m_map;
544  boost::container::flat_map<int64_t, std::vector<ossia::value>> m_vec_ts;
545  std::vector<std::vector<ossia::value>> m_vec_no_ts;
546  bool active{};
547  bool loops{};
548  bool first_is_timestamp = false;
549  int num_params{};
550 
551  void setActive(bool b)
552  {
553  active = b;
554  if(!b)
555  f.close();
556  else
557  reopen();
558  }
559 
560  void setLoops(bool b) { loops = b; }
561  void reopen()
562  {
563  f.close();
564 
565  auto filename = QByteArray::fromStdString(this->filename).trimmed();
566  filename.replace("%t", QDateTime::currentDateTimeUtc().toString().toUtf8());
567  f.setFileName(filename);
568  if(filename.isEmpty())
569  return;
570 
571  if(!active)
572  return;
573 
574  f.open(QIODevice::ReadOnly);
575  if(!f.isOpen())
576  return;
577  if(f.size() <= 0)
578  return;
579 
580  // FIXME not valid when the OSC device changes
581  // We need to parse the header instead and have a map.
582  num_params = 0;
583  for(auto in : this->roots)
584  {
585  if(auto p = in->get_parameter())
586  {
587  num_params++;
588  }
589  }
590 
591  auto data = (const char*)f.map(0, f.size());
592  m_map.clear();
593 
594  csv2::Reader<> r;
595  r.parse_view({data, data + f.size()});
596  int columns = r.cols();
597 
598  auto header = r.header();
599 
600  boost::container::flat_map<std::string, ossia::net::parameter_base*> params;
601 
602  for(auto node : roots)
603  if(auto p = node->get_parameter())
604  params[node->osc_address()] = p;
605 
606  std::string v;
607  v.reserve(128);
608  int i = 0;
609  auto header_it = header.begin();
610  if(first_is_timestamp)
611  ++header_it;
612  for(; header_it != header.end(); ++header_it)
613  {
614  auto addr = *header_it;
615  v.clear();
616  addr.read_raw_value(v);
617  if(auto it = params.find(v); it != params.end())
618  {
619  m_map[i] = it->second;
620  }
621  i++;
622  }
623 
624  m_vec_ts.clear();
625  m_vec_no_ts.clear();
626  if(first_is_timestamp)
627  {
628  m_vec_ts.reserve(r.rows());
629  for(const auto& row : r)
630  {
631  parse_row_with_timestamps(columns, row, v);
632  v.clear();
633  }
634  }
635  else
636  {
637  m_vec_no_ts.reserve(r.rows());
638  for(const auto& row : r)
639  {
640  parse_row_no_timestamps(columns, row, v);
641  v.clear();
642  }
643  }
644  first_ts = std::chrono::steady_clock::now();
645  }
646 
647  void parse_cell_impl(
648  const std::string& v, ossia::net::parameter_base& param, ossia::value& out)
649  {
650  if(!v.empty())
651  {
652  std::optional<ossia::value> res;
653  if(v.starts_with('"') && v.ends_with('"'))
654  res = State::parseValue(std::string_view(v).substr(1, v.size() - 2));
655  else
656  res = State::parseValue(v);
657 
658  if(res)
659  {
660  out = std::move(*res);
661  if(auto t = param.get_value_type(); out.get_type() != t)
662  {
663  ossia::convert(out, t);
664  }
665  }
666  }
667  }
668 
669  void
670  parse_cell(const auto& cell, std::string& v, std::vector<ossia::value>& vec, int i)
671  {
672  if(auto param = m_map[i])
673  {
674  v.clear();
675  cell.read_value(v);
676  parse_cell_impl(v, *param, vec[i]);
677  v.clear();
678  }
679  }
680 
681  void parse_row_no_timestamps(int columns, auto& row, std::string& v)
682  {
683  auto& vec = this->m_vec_no_ts.emplace_back(columns);
684  int i = 0;
685 
686  for(auto it = row.begin(); it != row.end(); ++it)
687  {
688  parse_cell(*it, v, vec, i);
689  i++;
690  }
691  }
692 
693  void parse_row_with_timestamps(int columns, auto& row, std::string& v)
694  {
695  if(row.length() <= 1)
696  return;
697 
698  auto it = row.begin();
699  const auto& ts = *it;
700 
701  ts.read_value(v);
702  auto tstamp = ossia::parse_strict<int64_t>(v);
703  if(!tstamp)
704  return;
705  auto& vec = this->m_vec_ts[*tstamp];
706  vec.resize(columns - 1);
707  int i = 0;
708 
709  for(++it; it != row.end(); ++it)
710  {
711  parse_cell(*it, v, vec, i);
712  i++;
713  }
714  }
715 
716  void read()
717  {
718  if(first_is_timestamp)
719  {
720  if(m_vec_ts.empty())
721  return;
722 
723  using namespace std::chrono;
724  auto ts = duration_cast<milliseconds>(steady_clock::now() - first_ts).count();
725  if(loops)
726  ts %= m_vec_ts.rbegin()->first + 1;
727  read_ts(ts);
728  }
729  else
730  {
731  if(m_vec_no_ts.empty())
732  return;
733 
734  using namespace std::chrono;
735  if(loops && nots_index >= std::ssize(m_vec_no_ts))
736  nots_index = 0;
737  read_no_ts(nots_index++);
738  }
739  }
740 
741  void read_no_ts(int64_t timestamp)
742  {
743  if(timestamp < 0)
744  return;
745  if(timestamp >= std::ssize(m_vec_no_ts))
746  return;
747  auto it = m_vec_no_ts.begin() + timestamp;
748  if(it != m_vec_no_ts.end())
749  {
750  int i = 0;
751  for(auto& v : *it)
752  {
753  if(v.valid())
754  {
755  if(auto p = m_map.find(i); p != m_map.end())
756  {
757  p->second->push_value(v);
758  }
759  }
760  i++;
761  }
762  }
763  }
764  void read_ts(int64_t timestamp)
765  {
766  auto it = m_vec_ts.lower_bound(timestamp);
767  if(it != m_vec_ts.end())
768  {
769  int i = 0;
770  for(auto& v : it->second)
771  {
772  if(v.valid())
773  {
774  if(auto p = m_map.find(i); p != m_map.end())
775  {
776  p->second->push_value(v);
777  }
778  }
779  i++;
780  }
781  }
782  }
783  };
784  std::shared_ptr<recorder_thread> record_impl = std::make_shared<recorder_thread>();
785  std::shared_ptr<player_thread> play_impl = std::make_shared<player_thread>();
786 
787  // Object definition
788  struct inputs_t
789  {
790  PatternSelector pattern;
791  halp::time_chooser<"Interval", halp::range{.min = 0.00001, .max = 5., .init = 0.25}>
792  time;
793  struct : halp::lineedit<"File pattern", "">
794  {
795  void update(DeviceRecorder& self) { self.update(); }
796  } filename;
797  struct
798  {
799  halp__enum("Mode", None, None, Record, Playback, Loop)
800  void update(DeviceRecorder& self) { self.setMode(); }
801  } mode;
802  struct ts : halp::toggle<"Timestamped", halp::default_on_toggle>
803  {
804  halp_meta(description, "Set to true to use the first column as timestamp")
805  } timestamped;
806  } inputs;
807 
808  struct
809  {
810  } outputs;
811 
813  {
814  std::shared_ptr<recorder_thread> recorder;
815  std::shared_ptr<player_thread> player;
816  std::string path;
817  std::vector<ossia::net::node_base*> roots;
818  bool first_is_timestamp{};
819 
820  void operator()()
821  {
822  using namespace std;
823  swap(recorder->filename, path);
824  swap(recorder->roots, roots);
825  player->filename = recorder->filename;
826  player->roots = recorder->roots;
827  player->first_is_timestamp = first_is_timestamp;
828  recorder->first_is_timestamp = first_is_timestamp;
829  recorder->reopen();
830  player->reopen();
831  }
832  };
833 
835  {
836  std::shared_ptr<recorder_thread> recorder;
837  std::shared_ptr<player_thread> player;
838  std::string path;
839  bool first_is_timestamp{};
840  void operator()()
841  {
842  using namespace std;
843  swap(recorder->filename, path);
844  player->filename = recorder->filename;
845  player->first_is_timestamp = first_is_timestamp;
846  recorder->first_is_timestamp = first_is_timestamp;
847  recorder->reopen();
848  player->reopen();
849  }
850  };
851 
853  {
854  std::shared_ptr<recorder_thread> recorder;
855  void operator()() { recorder->write(); }
856  };
857 
859  {
860  std::shared_ptr<player_thread> player;
861  void operator()() { player->read(); }
862  };
863 
865  {
866  std::shared_ptr<recorder_thread> recorder;
867  std::shared_ptr<player_thread> player;
868  using mode_type = decltype(DeviceRecorder::inputs_t{}.mode.value);
869  mode_type mode{};
870  void operator()()
871  {
872  recorder->setActive(mode == mode_type::Record);
873  player->setActive(mode == mode_type::Playback || mode == mode_type::Loop);
874  player->setLoops(mode == mode_type::Loop);
875  }
876  };
877 
878  using worker_message = ossia::variant<
879  std::unique_ptr<reset_message>, reset_path_message, process_message,
881 
882  struct
883  {
884  std::function<void(worker_message)> request;
885  static void work(worker_message&& mess)
886  {
887  ossia::visit([&]<typename M>(M&& msg) {
888  if constexpr(requires { *msg; })
889  (*std::forward<M>(msg))();
890  else
891  std::forward<M>(msg)();
892  }, std::move(mess));
893  }
894  } worker;
895 
896  using tick = halp::tick_musical;
897 
898  void setMode()
899  {
900  worker.request(activate_message{record_impl, play_impl, inputs.mode.value});
901  }
902  void update()
903  {
904  worker.request(
905  reset_path_message{record_impl, play_impl, inputs.filename, inputs.timestamped});
906  }
907 
908  void operator()(const halp::tick_musical& tk)
909  {
910  int64_t elapsed_ns = 0.;
911  if(!first_message_sent_pos)
912  first_message_sent_pos = tk.position_in_nanoseconds;
913  if(last_message_sent_pos)
914  elapsed_ns = tk.position_in_nanoseconds - *last_message_sent_pos;
915 
916  if(elapsed_ns > 0 && elapsed_ns < inputs.time.value * 1e9)
917  return;
918  last_message_sent_pos = tk.position_in_nanoseconds;
919 
920  if(!m_path)
921  return;
922 
923  if(!std::exchange(started, true))
924  {
925  inputs.pattern.reprocess();
926  worker.request(std::unique_ptr<reset_message>(new reset_message{
927  record_impl, play_impl, inputs.filename, roots, inputs.timestamped}));
928  }
929 
930  switch(inputs.mode)
931  {
932  case decltype(inputs.mode)::None:
933  break;
934  case decltype(inputs.mode)::Record:
935  worker.request(process_message{record_impl});
936  break;
937  case decltype(inputs.mode)::Playback:
938  case decltype(inputs.mode)::Loop:
939  worker.request(playback_message{play_impl});
940  break;
941  }
942  }
943 
944  bool started{};
945  std::optional<int64_t> first_message_sent_pos;
946  std::optional<int64_t> last_message_sent_pos;
947 };
948 }
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:865
Definition: DeviceRecorder.hpp:803
Definition: DeviceRecorder.hpp:789
Definition: DeviceRecorder.hpp:859
Definition: DeviceRecorder.hpp:535
Definition: DeviceRecorder.hpp:853
Definition: DeviceRecorder.hpp:813
Definition: DeviceRecorder.hpp:835
Definition: DeviceRecorder.hpp:425
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