OOKwiz
on/off-keying for ESP32 and a variety of supported radio modules
Meaning.cpp
Go to the documentation of this file.
1 #include "config.h"
2 #include "Meaning.h"
3 #include "Pulsetrain.h"
4 #include "RawTimings.h"
5 #include "serial_output.h"
6 #include "tools.h"
7 
8 
9 // Helpful URL: https://gabor.heja.hu/blog/2020/03/16/receiving-and-decoding-433-mhz-radio-signals-from-wireless-devices/
10 
11 /// @brief See if String might be a representation of Maening. No guarantees until you try to convert it, but silent.
12 /// @param str String that we are curious about
13 /// @return `true` if it might be a Meaning String, `false` if not.
14 bool Meaning::maybe(String str) {
15  if (str.indexOf("(") != -1) {
16  DEBUG("Meaning::maybe() returns true.\n");
17  return true;
18  }
19  return false;
20 }
21 
22 /// @brief If you try to evaluate the instance as a bool, for instance in `if (myMeaning) ...`, it will be `true` if this holds Meaning elements.
23 Meaning::operator bool() {
24  return (elements.size() > 0);
25 }
26 
27 /// @brief empty out all Meaning elements
28 void Meaning::zap() {
29  elements.clear();
30  suspected_incomplete = false;
31 }
32 
33 
34 /// @brief Convert Pulsetrain to Meaning
35 /// @param train Pulsetrain we want to convert
36 /// @return `true` if there was data found, `false` otherwise.
38  // Clear out current Meaning
39  zap();
40  // Easy stuff, just copy
41  repeats = train.repeats;
42  gap = train.gap;
43  // Copy the bins from the Pulsetrain in prevalence
44  typedef struct prevalence_t {
45  int bin;
46  int count;
47  } prevalence_t;
48  prevalence_t prevalence[train.bins.size()];
49  for (int n = 0; n < train.bins.size(); n++) {
50  prevalence[n].bin = n;
51  prevalence[n].count = train.bins[n].count;
52  }
53  // bubblesort by .count, i.e. the number of times that length occurred.
54  prevalence_t temp;
55  for (int i = 0; i < train.bins.size(); i++) {
56  for (int j = 0; j < train.bins.size() - i - 1; j++) {
57  if (prevalence[j].count < prevalence[j + 1].count) {
58  temp = prevalence[j];
59  prevalence[j] = prevalence[j + 1];
60  prevalence[j + 1] = temp;
61  }
62  }
63  }
64  // If the two most prevalent pulse lengths are most of the signal, it's likely PWM
65  bool likely_PWM = false;
66  if (
67  train.bins.size() >= 2 &&
68  abs(prevalence[0].count - prevalence[1].count) <= 2 // &&
69  // train.transitions.size() - (prevalence[0].count + prevalence[1].count) <= 5
70  ) {
71  likely_PWM = true;
72  DEBUG("likely_PWM set.\n", likely_PWM);
73  }
74  // If the three most prevalent pulse lengths are most of the signal, prevalence of #2 and
75  // #3 add up to the #1, and together they are most of the signal, it's likely PPM.
76  bool likely_PPM = false;
77  if (
78  train.bins.size() >= 3 &&
79  prevalence[0].count - (prevalence[1].count + prevalence[2].count) >= -2 &&
80  prevalence[0].count - (prevalence[1].count + prevalence[2].count) <= 4 // &&
81  // train.transitions.size() - (prevalence[0].count + prevalence[1].count + prevalence[2].count) <= 7
82  ) {
83  likely_PPM = true;
84  DEBUG("likely_PPM set.\n", likely_PPM);
85  }
86  // If we don't know the modulation, we're not going to do anything useful, so give up
87  if (!likely_PWM && !likely_PPM) {
88  DEBUG("Could not parse Pulsetrain, no likely modulation found.\n");
89  return false;
90  }
91  // Now we walk the train's transitions and decipher
92  bool something_decoded = false;
93  for (int n = 0; n < train.transitions.size(); n++) {
94  int r;
95  if (likely_PWM) {
96  r = parsePWM(train, n, train.transitions.size() - 1, prevalence[0].bin, prevalence[1].bin);
97  } else if (likely_PPM) {
98  r = parsePPM(train, n, train.transitions.size() - 1, prevalence[1].bin, prevalence[2].bin, prevalence[0].bin);
99  }
100  if (r == -1) {
101  return false;
102  } else if (r > 0) {
103  n += r - 1; // Minus 1 bc the next iteration will also increment 1
104  something_decoded = true;
105  continue;
106  }
107  // Otherwise add as a pulse or gap
108  if (n % 2 == 0) {
109  if (!addPulse(train.bins[train.transitions[n]].average)) {
110  return false;
111  }
112  } else {
113  if (!addGap(train.bins[train.transitions[n]].average)) {
114  return false;
115  }
116  }
117  }
118  if (!something_decoded) {
119  zap();
120  return false;
121  }
122  if (train.repeats > 1) {
123  suspected_incomplete = false;
124  }
125  return (elements.size() > 0);
126 }
127 
128 /// @brief Decode PWM data with specified timings from given range in Pulsetrain to a new Meaning element. Normally called by `fromPulsetrain`, but can be used from user code also.
129 /// @param train Pulsetrain we're reading from
130 /// @param from start at this interval
131 /// @param to end before this interval
132 /// @param space bin number (NOT time in µs) for space (first if bit 0)
133 /// @param mark bin number (NOT time in µs) for mark (first if bit 1)
134 /// @return Number of intervals read before read error (mark-mark, space-space or bin number not mark or space)
135 int Meaning::parsePWM(const Pulsetrain &train, int from, int to, int space, int mark) {
136  DEBUG ("Entered parsePWM with from: %i, to: %i space: %i, mark: %i\n", from, to, space, mark);
137  uint8_t tmp_data[MAX_MEANING_DATA] = { 0 };
138  int transitions_parsed = 0;
139  int num_bits = 0;
140  for (int n = from; n <= to; n += 2) {
141  int current = train.transitions[n];
142  int next = train.transitions[n + 1];
143  if (current == space && next == mark) {
144  num_bits++;
145  tools::shiftInBit(tmp_data, num_bits, 0);
146  transitions_parsed += 2;
147  } else if (current == mark && next == space) {
148  num_bits++;
149  tools::shiftInBit(tmp_data, num_bits, 1);
150  transitions_parsed += 2;
151  } else {
152  break;
153  }
154  }
155  if (num_bits % 4 != 0) {
156  suspected_incomplete = true;
157  }
158  if (num_bits >= 8) {
159  MeaningElement new_element;
160  new_element.data_len = num_bits;
161  int len_in_bytes = (num_bits + 7) / 8;
162  // Write data
163  for (int n = 0; n < len_in_bytes; n++) {
164  new_element.data.insert(new_element.data.begin(), tmp_data[n]); // reverses order
165  }
166  new_element.type = PWM;
167  new_element.time1 = train.bins[space].average;
168  new_element.time2 = train.bins[mark].average;
169  elements.push_back(new_element);
170  return transitions_parsed;
171  } else {
172  return 0;
173  }
174 }
175 
176 /// @brief Decode PPM data with specified timings from given range in Pulsetrain to a new Meaning element. Normally called by `fromPulsetrain`, but can be used from user code also.
177 /// @param train Pulsetrain we're reading from
178 /// @param from start at this interval
179 /// @param to end before this interval
180 /// @param space bin number (NOT time in µs) for space (first if bit 0)
181 /// @param mark bin number (NOT time in µs) for mark (first if bit 1)
182 /// @param filler bin number for delineator interval between the mark and space intervals
183 /// @return Number of intervals read before read error
184 int Meaning::parsePPM(const Pulsetrain &train, int from, int to, int space, int mark, int filler) {
185  DEBUG ("Entered parsePPM with from: %i, to: %i space: %i, mark: %i, filler: %i\n", from, to, space, mark, filler);
186  uint8_t tmp_data[MAX_MEANING_DATA] = { 0 };
187  int transitions_parsed = 0;
188  int num_bits = 0;
189  int previous = -1;
190  for (int n = from; n <= to; n++) {
191  int current = train.transitions[n];
192  if (current == space && previous == filler) {
193  num_bits++;
194  tools::shiftInBit(tmp_data, num_bits, 0);
195  transitions_parsed++;
196  } else if (current == mark && previous == filler) {
197  num_bits++;
198  tools::shiftInBit(tmp_data, num_bits, 1);
199  transitions_parsed++;
200  } else if (current == filler) {
201  if (previous == filler) {
202  break;
203  }
204  transitions_parsed++;
205  } else {
206  break;
207  }
208  previous = current;
209  }
210  if (num_bits % 4 != 0) {
211  suspected_incomplete = true;
212  }
213  if (num_bits >= 8) {
214  MeaningElement new_element;
215  new_element.data_len = num_bits;
216  int len_in_bytes = (num_bits + 7) / 8;
217  // Write data
218  for (int n = 0; n < len_in_bytes; n++) {
219  new_element.data.insert(new_element.data.begin(), tmp_data[n]); // reverses order
220  }
221  new_element.type = PPM;
222  new_element.time1 = train.bins[space].average;
223  new_element.time2 = train.bins[mark].average;
224  new_element.time3 = train.bins[filler].average;
225  elements.push_back(new_element);
226  return transitions_parsed;
227  } else {
228  return 0;
229  }
230 }
231 
232 /// @brief Meaning to Pulsetrain
233 /// @return Pulsetrain instance
235  Pulsetrain res;
236  res.fromMeaning(*this);
237  return res;
238 }
239 
240 /// @brief Get the String representation, which looks like `pulse(5906) + pwm(timing 190/575, 24 bits 0x1772A4)`
241 /// @return the String representation
243  String res = "";
244  for (const auto& element : elements) {
245  switch (element.type) {
246  case PULSE:
247  snprintf_append(res, 20, "pulse(%i)", element.time1);
248  break;
249  case GAP:
250  snprintf_append(res, 20, "gap(%i)", element.time1);
251  break;
252  case PWM:
253  snprintf_append(res, 60, "pwm(timing %i/%i, %i bits 0x", element.time1, element.time2, element.data_len);
254  for (int m = 0; m < (element.data_len + 7) / 8; m++) {
255  snprintf_append(res, 5, "%02X", element.data[m]);
256  }
257  res = res + ")";
258  break;
259  case PPM:
260  snprintf_append(res, 60, "ppm(timing %i/%i/%i, %i bits 0x", element.time1, element.time2, element.time3, element.data_len);
261  for (int m = 0; m < (element.data_len + 7) / 8; m++) {
262  snprintf_append(res, 5, "%02X", element.data[m]);
263  }
264  res = res + ")";
265  break;
266  }
267  res += " + ";
268  }
269  res = res.substring(0, res.length() - 3); // cut off the last " + "
270  if (repeats > 1) {
271  snprintf_append(res, 40, " Repeated %i times with %i µs gap.", repeats, gap);
272  }
273  if (suspected_incomplete) {
274  res = res + " (SUSPECTED INCOMPLETE)";
275  }
276  return res;
277 }
278 
279 /// @brief Read a String representation like above, and store in this instance
280 /// @return `true` if it worked, `false` (with error message) if it didn't.
281 bool Meaning::fromString(String in) {
282  in.toLowerCase();
283  repeats = 1;
284  int rptd = in.indexOf("repeated");
285  if (rptd != -1) {
286  String str_repeats = in.substring(rptd);
287  repeats = tools::nthNumberFrom(str_repeats, 0);
288  gap = tools::nthNumberFrom(str_repeats, 1);
289  in = in.substring(0, rptd);
290  if (repeats == 0 or gap == 0) {
291  ERROR("ERROR: cannot convert String to Meaning: invalid values for repeats or gap.\n");
292  return false;
293  }
294  }
295  String work;
296  bool done = false;
297  while (!done) {
298  int plus = in.indexOf("+");
299  if (plus != -1) {
300  work = in.substring(0, plus);
301  in = in.substring(plus + 1);
302  } else {
303  work = in;
304  done = true;
305  }
306  tools::trim(work);
307  int open_bracket = work.indexOf("(");
308  int closing_bracket = work.indexOf(")");
309  if (open_bracket == -1 || closing_bracket == -1) {
310  ERROR("ERROR: cannot convert String to Meaning: incorrect element '%s'.\n", work);
311  return false;
312  }
313  if (work.startsWith("pulse")) {
314  int num = tools::nthNumberFrom(work, 0);
315  if (num == -1) {
316  ERROR("ERROR: cannot convert String to Meaning: no length found in '%s'.\n", work);
317  return false;
318  }
319  addPulse(num);
320  }
321  if (work.startsWith("gap")) {
322  int num = tools::nthNumberFrom(work, 0);
323  if (num == -1) {
324  ERROR("ERROR: cannot convert String to Meaning: no length found in '%s'.\n", work);
325  return false;
326  }
327  addGap(num);
328  }
329  if (work.startsWith("ppm")) {
330  int time1 = tools::nthNumberFrom(work, 0);
331  int time2 = tools::nthNumberFrom(work, 1);
332  int time3 = tools::nthNumberFrom(work, 2);
333  int bits = tools::nthNumberFrom(work, 3);
334  int check_zero = tools::nthNumberFrom(work, 4);
335  if (time1 < 1 || time2 < 1 || time3 < 1 || check_zero != 0) {
336  ERROR("ERROR: cannot convert String to Meaning: '%s' malformed.\n", work);
337  return false;
338  }
339  int data_start = work.indexOf("0x");
340  int data_end = work.indexOf(")");
341  if (data_start == -1 || data_end < data_start) {
342  ERROR("ERROR: cannot convert String to Meaning: '%s' malformed.\n", work);
343  return false;
344  }
345  String hex_data = work.substring(data_start + 2, data_end);
346  int bytes_expected = (bits + 7) / 8;
347  if (hex_data.length() != bytes_expected * 2) {
348  ERROR("ERROR: cannot convert String to Meaning: %i bits means %i data bytes in hex expected.\n", bits, bytes_expected);
349  return false;
350  }
351  uint8_t tmp_data[bytes_expected];
352  for (int n = 0; n < bytes_expected; n++) {
353  tmp_data[n] = strtoul(hex_data.substring(n * 2, (n * 2) + 2).c_str(), nullptr, 16);
354  }
355  addPPM(time1, time2, time3, bits, tmp_data);
356  }
357  if (work.startsWith("pwm")) {
358  int time1 = tools::nthNumberFrom(work, 0);
359  int time2 = tools::nthNumberFrom(work, 1);
360  int bits = tools::nthNumberFrom(work, 2);
361  int check_zero = tools::nthNumberFrom(work, 3);
362  if (time1 < 1 || time2 < 1 || check_zero != 0) {
363  ERROR("ERROR: cannot convert String to Meaning: '%s' malformed.\n", work);
364  return false;
365  }
366  int data_start = work.indexOf("0x");
367  int data_end = work.indexOf(")");
368  if (data_start == -1 || data_end < data_start) {
369  ERROR("ERROR: cannot convert String to Meaning: '%s' malformed.\n", work);
370  return false;
371  }
372  String hex_data = work.substring(data_start + 2, data_end);
373  tools::trim(hex_data);
374  int bytes_expected = (bits + 7) / 8;
375  if (hex_data.length() != bytes_expected * 2) {
376  ERROR("ERROR: cannot convert String to Meaning: %i bits means %i data bytes in hex expected.\n", bits, bytes_expected);
377  return false;
378  }
379  uint8_t tmp_data[bytes_expected];
380  for (int n = 0; n < bytes_expected; n++) {
381  tmp_data[n] = strtoul(hex_data.substring(n * 2, (n * 2) + 2).c_str(), nullptr, 16);
382  }
383  addPWM(time1, time2, bits, tmp_data);
384  }
385  }
386  return true;
387 }
388 
389 /// @brief Adds a "pulse" Meaning element
390 /// @param pulse_time time in µs
391 /// @return `true`
392 bool Meaning::addPulse(uint16_t pulse_time) {
393  MeaningElement new_element;
394  new_element.type = PULSE;
395  new_element.time1 = pulse_time;
396  elements.push_back(new_element);
397  return true;
398 }
399 
400 /// @brief Adds a "gap"" Meaning element
401 /// @param gap_time time in µs
402 /// @return `true`
403 bool Meaning::addGap(uint16_t gap_time) {
404  MeaningElement new_element;
405  new_element.type = GAP;
406  new_element.time1 = gap_time;
407  elements.push_back(new_element);
408  return true;
409 }
410 
411 /// @brief Adds a new meaning element with the specified PPM-encoded data
412 /// @param space time in µs
413 /// @param mark time in µs
414 /// @param filler time in µs
415 /// @param bits Length of data at tmp_data IN BITS, not bytes
416 /// @param tmp_data pointer to `uint8_t` array with the data
417 /// @return `true`
418 bool Meaning::addPPM(int space, int mark, int filler, int bits, uint8_t* tmp_data) {
419  MeaningElement new_element;
420  int len_in_bytes = (bits + 7) / 8;
421  new_element.data_len = bits;
422  for (int n = 0; n < len_in_bytes; n++) {
423  new_element.data.push_back(tmp_data[n]);
424  }
425  new_element.type = PPM;
426  new_element.time1 = space;
427  new_element.time2 = mark;
428  new_element.time3 = filler;
429  elements.push_back(new_element);
430  return true;
431 }
432 
433 /// @brief Adds a new meaning element with the specified PWM-encoded data
434 /// @param space time in µs
435 /// @param mark time in µs
436 /// @param bits Length of data at tmp_data IN BITS, not bytes
437 /// @param tmp_data pointer to `uint8_t` array with the data
438 /// @return `true`
439 bool Meaning::addPWM(int space, int mark, int bits, uint8_t* tmp_data) {
440  MeaningElement new_element;
441  int len_in_bytes = (bits + 7) / 8;
442  new_element.data_len = bits;
443  for (int n = 0; n < len_in_bytes; n++) {
444  new_element.data.push_back(tmp_data[n]);
445  }
446  new_element.type = PWM;
447  new_element.time1 = space;
448  new_element.time2 = mark;
449  elements.push_back(new_element);
450  return true;
451 }