OOKwiz
on/off-keying for ESP32 and a variety of supported radio modules
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros
Pulsetrain.cpp
Go to the documentation of this file.
1 #include <algorithm> // for std::sort
2 #include "Pulsetrain.h"
3 #include "RawTimings.h"
4 #include "Meaning.h"
5 #include "Settings.h"
6 #include "serial_output.h"
7 #include "tools.h"
8 
9 // Note that anything marked IRAM_ATTR is used by the ISRs in OOKwiz.cpp and
10 // SHALL NOT have Serial output
11 
12 /// @brief See if String might be a representation of Pulsetrain. No guarantees until you try to convert it, but silent.
13 /// @param str String that we are curious about
14 /// @return `true` if it might be a Pulsetrain String, `false` if not.
15 bool Pulsetrain::maybe(String str) {
16  if (str.length() < 10) {
17  return false;
18  }
19  for (int n = 0; n < 10; n++) {
20  if (!isDigit(str.charAt(n))) {
21  return false;
22  }
23  }
24  DEBUG("Pulsetrain::maybe() returns true.\n");
25  return true;
26 }
27 
28 /// @brief If you try to evaluate the instance as a bool (e.g. `if (myPulsetrain) ...`) this will be `true` if there's transitions stored.
29 IRAM_ATTR Pulsetrain::operator bool() {
30  return (transitions.size() > 0);
31 }
32 
33 /// @brief empty out all information about the stored pulses
34 void IRAM_ATTR Pulsetrain::zap() {
35  transitions.clear();
36  bins.clear();
37  gap = 0;
38  repeats = 0;
39  last_at = 0;
40 }
41 
42 /// @brief Compare to other Pulsetrains to see if same packet. Ignores minor timing differences. Used internally by ISR processing to see if packet is a repeat.
43 /// @param other_train Pulsetrain we're comparing this one to
44 /// @return `true` if same, `false` if not
45 bool IRAM_ATTR Pulsetrain::sameAs(const Pulsetrain &other_train) {
46  if (transitions.size() != other_train.transitions.size()) {
47  return false;
48  }
49  if (bins.size() != other_train.bins.size()) {
50  return false;
51  }
52  for (int n = 0; n < transitions.size(); n++) {
53  if (transitions[n] != other_train.transitions[n]) {
54  return false;
55  }
56  }
57  for (int m = 0; m < bins.size(); m++) {
58  if (abs(bins[m].average - abs(other_train.bins[m].average)) > 100) {
59  return false;
60  }
61  }
62  return true;
63 }
64 
65 /// @brief Get the String representation, which looks like `2010101100110101001101010010110011001100101100101,190,575,5906*6@132`
66 /// @return the String representation
67 String Pulsetrain::toString() const {
68  if (transitions.size() == 0) {
69  return "<empty Pulsetrain>";
70  }
71  String res = "";
72  for (int transition: transitions) {
73  res += transition;
74  }
75  for (auto bin : bins) {
76  res += ",";
77  res += bin.average;
78  }
79  if (repeats > 1) {
80  snprintf_append(res, 20, "*%i@%i", repeats, gap);
81  }
82  return res;
83 }
84 
85 /// @brief Read a String representation like above, and store in this instance
86 /// @return `true` if it worked, `false` (with error message) if it didn't.
87 bool Pulsetrain::fromString(String in) {
88  zap();
89  int first_comma = in.indexOf(",");
90  if (first_comma == -1) {
91  ERROR("ERROR: cannot convert String to Pulsetrain, no commas present.\n");
92  return false;
93  }
94  // fill transitions and deduce number of bins
95  int num_bins = 0;
96  for (int n = 0; n < first_comma; n++) {
97  int digit = in.charAt(n);
98  if (!isDigit(digit)) {
99  zap();
100  ERROR("ERROR: cannot convert String to Pulsetrain, non-digits in wrong place.\n");
101  return false;
102  }
103  digit -= 48; // "0" is 48 in ASCII
104  transitions.push_back(digit);
105  if (digit > num_bins) {
106  num_bins = digit;
107  }
108  }
109  num_bins++;
110  int end_binlist = in.indexOf("*");
111  if (end_binlist == -1) {
112  end_binlist = in.length();
113  repeats = 1;
114  } else {
115  int at_sign = in.indexOf("@");
116  if (at_sign == -1) {
117  zap();
118  ERROR("ERROR: cannot convert String to Pulsetrain, * but no @ found.\n");
119  return false;
120  }
121  repeats = in.substring(end_binlist + 1, at_sign).toInt();
122  gap = in.substring(at_sign + 1).toInt();
123  if (gap == 0 || repeats == 0) {
124  zap();
125  ERROR("ERROR: cannot convert String to Pulsetrain, invalid values for repeats or gap.\n");
126  return false;
127  }
128  }
129  int bin_start = first_comma + 1;
130  for (int n = 0; n < num_bins; n++) {
131  pulseBin new_bin;
132  int next_comma = in.indexOf(",", bin_start);
133  if (next_comma == -1) {
134  next_comma = in.length();
135  }
136  new_bin.average = in.substring(bin_start, next_comma).toInt();
137  new_bin.min = new_bin.average;
138  new_bin.max = new_bin.average;
139  if (new_bin.average == 0) {
140  zap();
141  ERROR("ERROR: cannot convert String to Pulsetrain, invalid bin value found.\n");
142  return false;
143  }
144  bin_start = next_comma + 1;
145  bins.push_back(new_bin);
146  }
147  for (int transition : transitions) {
148  bins[transition].count++;
149  duration += bins[transition].average;
150  }
151  return true;
152 }
153 
154 /// @brief Summary String a la `25 pulses over 24287 µs, repeated 6 times with gaps of 132 µs`
155 /// @return the String in question
156 String Pulsetrain::summary() const {
157  String res = "";
158  snprintf_append(res, 80, "%i pulses over %i µs", (transitions.size() + 1) / 2, duration);
159  if (repeats > 1) {
160  snprintf_append(res, 80, ", repeated %i times with gaps of %i µs", repeats, gap);
161  }
162  return res;
163 }
164 
165 /// @brief Convert RawTimings to Pulsetrain
166 /// @param raw the RawTimings instance to convert from
167 /// @return Always `true`
168 bool IRAM_ATTR Pulsetrain::fromRawTimings(const RawTimings &raw) {
169  int bin_width;
170  SETTING_WITH_DEFAULT(bin_width, 150);
171  // First copy the intervals and sort them
172  std::vector<uint16_t> sorted = raw.intervals;
173  std::sort(sorted.begin(), sorted.end());
174  // Then make the bins by starting a new one every time bin_width is exceeded
175  bool just_begun = true;
176  for (auto interval : sorted) {
177  if (just_begun || interval > bins.back().min + bin_width) {
178  just_begun = false;
179  pulseBin new_bin;
180  new_bin.min = interval;
181  bins.push_back(new_bin);
182  }
183  bins.back().max = interval;
184  }
185  // Walk intervals, add bin number to transitions, update count in its bin, find total duration
186  duration = 0;
187  for (auto interval : raw.intervals) {
188  duration += interval;
189  for (int m = 0; m < bins.size(); m++) {
190  if (interval >= bins[m].min && interval <= bins[m].max) {
191  transitions.push_back(m);
192  bins[m].average += interval; // use average for total first, which is why .average is a long
193  bins[m].count++;
194  break;
195  }
196  }
197  }
198  // Averages
199  for (auto& bin : bins) {
200  if (bin.count > 0) {
201  bin.average = bin.average / bin.count;
202  }
203  }
204  // Set other metadata about this Pulsetrain
205  first_at = esp_timer_get_time();
206  last_at = esp_timer_get_time();
207  repeats = 1;
208  return true;
209 }
210 
211 /// @brief Pulsetrain to RawTimings
212 /// @return RawTimings instance
213 RawTimings Pulsetrain::toRawTimings() {
214  RawTimings res;
215  res.fromPulsetrain(*this);
216  return res;
217 }
218 
219 /// @brief Get information about the bins in this Pulsetrain, such as lowest, average and highest interval as well as number of pulses in each bin.
220 /// @return multi-line String with bin information, 5 columns with header
221 String Pulsetrain::binList() {
222  String res = "";
223  snprintf_append(res, 50, " bin min avg max count");
224  for (int m = 0; m < bins.size(); m++) {
225  snprintf_append(res, 50, "\n%4i %7i %7i %7i %6i", m, bins[m].min, bins[m].average, bins[m].max, bins[m].count);
226  }
227  return res;
228 }
229 
230 /// @brief Returns the viasualizer (the blocky time-graph) for the pulses in this Pulsetrain instance
231 /// @param base µs per (half-character) block. Every interval gets at least one block so all pulses are guaranteed visible
232 /// @return visualizer String
233 String Pulsetrain::visualizer() {
234  int visualizer_pixel;
235  SETTING_WITH_DEFAULT(visualizer_pixel, 200);
236  return visualizer(visualizer_pixel);
237 }
238 
239 /// @brief The visualizer like above, with base taken from `visualizer_pixel` setting.
240 /// @return visualizer String
241 String Pulsetrain::visualizer(int base) {
242  if (base == 0) {
243  return "";
244  }
245  uint8_t multiples[bins.size()];
246  for (int m = 0; m < bins.size(); m++) {
247  multiples[m] = max(((int)bins[m].average + (base / 2)) / base, 1);
248  }
249  String ones_and_zeroes;
250  String curstate;
251  for (int n = 0; n < transitions.size(); n++) {
252  curstate = (n % 2 == 0) ? "1" : "0";
253  for (int m = 0; m < multiples[transitions[n]]; m++) {
254  ones_and_zeroes += curstate;
255  }
256  }
257  ones_and_zeroes += "0";
258  String output;
259  for (int n = 0; n < ones_and_zeroes.length(); n += 2) {
260  String chunk = ones_and_zeroes.substring(n, n + 2);
261  if (chunk == "11") {
262  output += "▀";
263  } else if (chunk == "00") {
264  output += " ";
265  } else if (chunk == "01") {
266  output += "▝";
267  } else if (chunk == "10") {
268  output += "▘";
269  }
270  }
271  return output;
272 }
273 
274 bool Pulsetrain::fromMeaning(const Meaning &meaning) {
275  zap();
276  // First make bins for all the different timings found
277  for (const auto& el : meaning.elements) {
278  if (el.type == PULSE || el.type == GAP) {
279  addToBins(el.time1);
280  }
281  if (el.type == PWM) {
282  addToBins(el.time1);
283  addToBins(el.time2);
284  }
285  if (el.type == PPM) {
286  addToBins(el.time1);
287  addToBins(el.time2);
288  addToBins(el.time3);
289  }
290  }
291  // sort bins by average interval
292  std::sort(bins.begin(), bins.end(), [](pulseBin a, pulseBin b) {
293  return a.average < b.average;
294  });
295  // Now traverse the elements again, filling in the transitions
296  for (int n = 0; n < meaning.elements.size(); n++) {
297  MeaningElement el = meaning.elements[n];
298  if (el.type == PULSE) {
299  // If we're about to write a low-state time, we need to fill the space before
300  if (transitions.size() % 2) {
301  // Which we use the prevous datablock's timing for, if applicable
302  if (n > 0 && meaning.elements[n - 1].type == PPM) {
303  transitions.push_back(binFromTime(meaning.elements[n - 1].time3));
304  } else if (n > 0 && meaning.elements[n - 1].type == PWM) {
305  transitions.push_back(binFromTime(meaning.elements[n - 1].time1));
306  } else {
307  zap();
308  ERROR("ERROR: cannot have a pulse where a gap is expected at element %i.\n", n);
309  return false;
310  }
311  }
312  transitions.push_back(binFromTime(el.time1));
313  }
314  if (el.type == GAP) {
315  if (transitions.size() % 2 == 0) {
316  zap();
317  ERROR("ERROR: cannot have a gap where a pulse is expected at element %i.\n", n);
318  return false;
319  }
320  transitions.push_back(binFromTime(el.time1));
321  }
322  if (el.type == PWM || el.type == PPM) {
323  // Create a copy of el's data in tmp_data
324  int data_bytes = (el.data_len + 7) / 8;
325  uint8_t tmp_data[data_bytes];
326  for (int m = 0; m < data_bytes; m++) {
327  tmp_data[m] = el.data[m];
328  }
329  // Shift left until the first bit that went in is
330  // aligned in the MSB of the first byte.
331  int shift_left_by = (8 - (el.data_len % 8)) % 8;
332  for (int j = 0; j < shift_left_by; j++) {
333  tools::shiftOutBit(tmp_data, el.data_len);
334  }
335  if (el.type == PWM) {
336  for (int m = 0; m < el.data_len; m++) {
337  if (tools::shiftOutBit(tmp_data, el.data_len)) {
338  transitions.push_back(binFromTime(el.time2));
339  transitions.push_back(binFromTime(el.time1));
340  } else {
341  transitions.push_back(binFromTime(el.time1));
342  transitions.push_back(binFromTime(el.time2));
343  }
344  }
345  }
346  if (el.type == PPM) {
347  if (transitions.size() % 2) {
348  transitions.push_back(binFromTime(el.time3));
349  }
350  for (int m = 0; m < el.data_len; m++) {
351  if (tools::shiftOutBit(tmp_data, el.data_len)) {
352  transitions.push_back(binFromTime(el.time2));
353  transitions.push_back(binFromTime(el.time3));
354  } else {
355  transitions.push_back(binFromTime(el.time1));
356  transitions.push_back(binFromTime(el.time3));
357  }
358  }
359  transitions.pop_back(); // retract last filler, as this may be the end
360  }
361  }
362  }
363  // Now update bin counts, duration, repeats, gap.
364  for (int transition : transitions) {
365  bins[transition].count++;
366  duration += bins[transition].average;
367  }
368  repeats = meaning.repeats;
369  gap = meaning.gap;
370  return true;
371 }
372 
373 /// @brief Pulsetrain to Meaning
374 /// @return Meaning instance
375 Meaning Pulsetrain::toMeaning() {
376  Meaning res;
377  res.fromPulsetrain(*this);
378  return res;
379 }
380 
381 void Pulsetrain::addToBins(int time) {
382  for (auto bin : bins) {
383  if (bin.average == time || bins.size() == MAX_BINS) {
384  return;
385  }
386  }
387  pulseBin new_bin;
388  new_bin.min = time;
389  new_bin.max = time;
390  new_bin.average = time;
391  bins.push_back(new_bin);
392  return;
393 }
394 
395 int Pulsetrain::binFromTime(int time) {
396  for (int m = 0; m < bins.size(); m++) {
397  if (bins[m].average == time) {
398  return m;
399  }
400  }
401  return -1;
402 }