OOKwiz
on/off-keying for ESP32 and a variety of supported radio modules
OOKwiz.cpp
Go to the documentation of this file.
1 #include "OOKwiz.h"
2 #include "CLI.h"
3 #include "serial_output.h"
4 
5 volatile OOKwiz::Rx_State OOKwiz::rx_state = OOKwiz::RX_OFF;
6 bool OOKwiz::serial_cli_disable = false;
7 int OOKwiz::first_pulse_min_len;
8 int OOKwiz::pulse_gap_min_len;
9 int OOKwiz::min_nr_pulses;
10 int OOKwiz::max_nr_pulses;
11 int OOKwiz::pulse_gap_len_new_packet;
12 int OOKwiz::noise_penalty;
13 int OOKwiz::noise_threshold;
14 int OOKwiz::noise_score;
15 bool OOKwiz::no_noise_fix = false;
16 int OOKwiz::lost_packets = 0;
17 int64_t OOKwiz::last_transition;
18 hw_timer_t* OOKwiz::transitionTimer = nullptr;
19 int64_t OOKwiz::repeat_time_start = 0;
20 long OOKwiz::repeat_timeout;
21 bool OOKwiz::rx_active_high;
22 bool OOKwiz::tx_active_high;
23 RawTimings OOKwiz::isr_in;
24 RawTimings OOKwiz::isr_out;
25 BufferPair OOKwiz::loop_in;
26 BufferPair OOKwiz::loop_compare;
27 BufferTriplet OOKwiz::loop_ready;
28 int64_t OOKwiz::last_periodic = 0;
29 void (*OOKwiz::callback)(RawTimings, Pulsetrain, Meaning) = nullptr;
30 
31 /// @brief Starts OOKwiz. Loads settings, initializes the radio and starts receiving if it finds the appropriate settings.
32 /**
33  * If you set the GPIO pin for a button on your ESP32 in 'pin_rescue' and press it during boot, OOKwiz will not
34  * initialize SPI and the radio, possibly breaking an endless boot loop. Set 'rescue_active_high' if the button
35  * connects to VCC instead of GND.
36  *
37  * Normally, OOKwiz will start up in receive mode. If you set 'start_in_standby', it will start in standby mode instead.
38 */
39 /// @param skip_saved_defaults The settings in the SPIFFS file 'defaults' are not read when this
40 /// is true, leaving only the factory defaults from config.cpp.
41 /// @return true if setup succeeded, false if it could not complete, e.g. because the radio is not configured yet.
42 bool OOKwiz::setup(bool skip_saved_defaults) {
43 
44  // Make sure nothing is missed when we paste raw data to 'sim' or 'transmit' CLI commands.
45  Serial.setRxBufferSize(SERIAL_RX_BUFFER_SIZE);
46 
47  // Sometimes USB needs to wake up, and we want to see what OOKwiz does
48  // right after it is woken up with OOKwiz::setup().
49  delay(1000);
50 
51  // Tada !
52  INFO("\n\nOOKwiz version %s (built %s %s) initializing.\n", OOKWIZ_VERSION, __DATE__, __TIME__);
53 
54  // The factory defaults are loaded pre-main by Settings object constructor
55  if (skip_saved_defaults == true) {
56  INFO("OOKwiz::setup(true) called: not loading user defaults, factory settings only.\n");
57  } else {
58  // Try to get the runtime settings from SPIFFS
59  if (!Settings::fileExists("default") || !Settings::load("default")) {
60  INFO("No saved settings found, using factory settings.\n");
61  }
62  }
63 
64  // Skip the rest of OOKwiz::setup() by returning false when recue button pressed
65  int pin_rescue;
66  SETTING_WITH_DEFAULT(pin_rescue, -1);
67  if (pin_rescue != -1) {
68  PIN_INPUT(pin_rescue);
69  if (digitalRead(pin_rescue) == Settings::isSet("rescue_active_high")) {
70  INFO("Rescue button pressed at boot, skipping initialization.\n");
71  Settings::unset("serial_cli_disable");
72  return false;
73  }
74  }
75 
76  if (!Radio::setup()) {
77  ERROR("ERROR: Your radio doesn't set up correctly. Make sure you set the correct\n radio and pins, save settings and reboot.\n");
78  return false;
79  }
80 
82 
83  // These settings determine what a valid transmission is
84  SETTING_OR_ERROR(pulse_gap_len_new_packet);
85  SETTING_OR_ERROR(repeat_timeout);
86  SETTING_OR_ERROR(first_pulse_min_len);
87  SETTING_OR_ERROR(pulse_gap_min_len);
88  SETTING_OR_ERROR(min_nr_pulses);
89  SETTING_OR_ERROR(max_nr_pulses);
90  SETTING_OR_ERROR(noise_penalty);
91  SETTING_OR_ERROR(noise_threshold);
92  no_noise_fix = Settings::isSet("no_noise_fix");
93  rx_active_high = Settings::isSet("rx_active_high");
94  tx_active_high = Settings::isSet("tx_active_high");
95 
96  // Timer for pulse_gap_len_new_packet
97  transitionTimer = timerBegin(0, 80, true);
98  timerAttachInterrupt(transitionTimer, &ISR_transitionTimeout, false);
99  timerAlarmWrite(transitionTimer, pulse_gap_len_new_packet, true);
100  timerAlarmEnable(transitionTimer);
101  timerStart(transitionTimer);
102 
103  // The ISR that actually reads the data
104  attachInterrupt(Radio::pin_rx, ISR_transition, CHANGE);
105 
106  if (Settings::isSet("start_in_standby")) {
107  return standby();
108  } else {
109  return receive();
110  }
111 
112 }
113 
114 /// @brief To be called from your own `loop()` function.
115 /**
116  * Does the high-level processing of packets as soon as they are received and
117  * processed by the ISR functions. Handles the serial port output of each packet
118  * as well as calling the user's own callback function and the various device
119  * plugins.
120 */
121 /// @return always returns `true`
122 bool OOKwiz::loop() {
123  // Have CLI's loop check the serial port for data
124  if (!serial_cli_disable) {
125  CLI::loop();
126  }
127  // If the transitionTimer is not set up, we're not ready to do anything yet
128  if (transitionTimer == nullptr) {
129  return true;
130  }
131  // Stuff that happens only once a seccond
132  if (esp_timer_get_time() - last_periodic > 1000000) {
133  // If any of the core parameters have changed in settings,
134  // update their variables.
135  SETTING(repeat_timeout);
136  SETTING(first_pulse_min_len);
137  SETTING(pulse_gap_min_len);
138  SETTING(min_nr_pulses);
139  SETTING(max_nr_pulses);
140  no_noise_fix = Settings::isSet("no_noise_fix");
141  serial_cli_disable = Settings::isSet("serial_cli_disable");
142  // The timers are a bit more involved as their new values need to be written
143  int new_p_g_l_n_p = Settings::getInt("pulse_gap_len_new_packet", -1);
144  if (new_p_g_l_n_p != pulse_gap_len_new_packet) {
145  pulse_gap_len_new_packet = new_p_g_l_n_p;
146  timerAlarmWrite(transitionTimer, pulse_gap_len_new_packet, true);
147  }
148  int new_r_t = Settings::getInt("repeat_timeout", -1);
149  last_periodic = esp_timer_get_time();
150  }
151  // See if the packet in loop_compare has timed out
152  if (
153  loop_compare.train &&
154  esp_timer_get_time() - repeat_time_start > repeat_timeout
155  ) {
156  loop_ready.raw = loop_compare.raw;
157  loop_ready.train = loop_compare.train;
158  loop_compare.zap();
159  } else if (isr_out) {
160  // Process packet from ISRs if there is one
161  // So from here, we're processing a new RawTimings received by the ISRs
162  loop_in.raw = isr_out;
163  isr_out.zap();
164  // reject if not the required minimum number of pulses
165  if (loop_in.raw.intervals.size() < (min_nr_pulses * 2) + 1) {
166  return true;
167  }
168  // Remove last transition if number is even because in that case the
169  // last transition is the off state, which is not part of a train.
170  if (loop_in.raw.intervals.size() % 2 == 0) {
171  loop_in.raw.intervals.pop_back();
172  }
173  if (!no_noise_fix) {
174  // fix noise: too-short transitions found are merged into one with transitions before and after.
175  bool noisy = true;
176  while (noisy) {
177  noisy = false;
178  for (int n = 1; n < loop_in.raw.intervals.size() - 1; n++) {
179  if (loop_in.raw.intervals[n] < pulse_gap_min_len) {
180  int new_interval = loop_in.raw.intervals[n - 1] + loop_in.raw.intervals[n] + loop_in.raw.intervals[n + 1];
181  loop_in.raw.intervals.erase(loop_in.raw.intervals.begin() + n - 1, loop_in.raw.intervals.begin() + n + 2);
182  loop_in.raw.intervals.insert(loop_in.raw.intervals.begin() + n - 1, new_interval);
183  noisy = true;
184  break;
185  }
186  }
187  }
188  // Simply cut off last pulse and preceding gap if pulse too short.
189  if (loop_in.raw.intervals.back() < pulse_gap_min_len) {
190  loop_in.raw.intervals.pop_back();
191  loop_in.raw.intervals.pop_back();
192  }
193  // Check we still meet the required minimum number of pulses after noise removal.
194  if (loop_in.raw.intervals.size() < (min_nr_pulses * 2) + 1) {
195  return true;
196  }
197  }
198  // Release excess reserved memory
199  loop_in.raw.intervals.shrink_to_fit();
200  // And then go to normalizing, comparing, etc.
201  loop_in.train.fromRawTimings(loop_in.raw);
202  }
203  // This is split up so that simulate(Pulsetrain) can stick in a train
204  if (loop_in.train) {
205  // If there is no packet in loop_compare, just put the new one there
206  if (!loop_compare.train) {
207  loop_compare = loop_in;
208  loop_in.zap();
209  // Start the timer on it expiring and being handed to the user
210  repeat_time_start = esp_timer_get_time();
211  // Otherwise check if it's a duplicate
212  } else if (loop_in.train && loop_in.train.sameAs(loop_compare.train)) {
213  // If so just add to number of repeats
214  loop_compare.train.repeats++;
215  // Check if the observed gap is smaller than what we had and if so store.
216  int64_t gap = (esp_timer_get_time() - loop_compare.train.last_at) - loop_compare.train.duration;
217  if (gap < loop_compare.train.gap || loop_compare.train.gap == 0) {
218  loop_compare.train.gap = gap;
219  }
220  loop_compare.train.last_at = esp_timer_get_time();
221  loop_in.zap();
222  // Restart the repeat timer
223  repeat_time_start = esp_timer_get_time();
224  // It's no duplicate, so push out the packet in loop_compare and put this one there
225  } else {
226  loop_ready.raw = loop_compare.raw;
227  loop_ready.train = loop_compare.train;
228  loop_compare = loop_in;
229  loop_in.zap();
230  repeat_time_start = esp_timer_get_time();
231  }
232  loop_in.zap();
233  }
234  if (loop_ready.train) {
235  // Warn if we lost packets before this one
236  if (lost_packets) {
237  ERROR("\n\nWARNING: %i packets lost because loop() was not fast enough.\n", lost_packets);
238  lost_packets = 0;
239  }
240  // Print to Serial what needs to be printed
241  if (Settings::isSet("print_raw") ||
242  Settings::isSet("print_visualizer") ||
243  Settings::isSet("print_summary") ||
244  Settings::isSet("print_pulsetrain") ||
245  Settings::isSet("print_binlist") ||
246  Settings::isSet("print_meaning")
247  ) {
248  INFO("\n\n");
249  }
250  if (Settings::isSet("print_raw") && loop_ready.raw) {
251  INFO("%s\n", loop_ready.raw.toString().c_str());
252  }
253  if (Settings::isSet("print_visualizer")) {
254  // If we simulate a Pulsetrain, the raw buffer will be empty still,
255  // so we visualize the Pulsetrain instead.
256  if (loop_ready.raw) {
257  INFO("%s\n", loop_ready.raw.visualizer().c_str());
258  } else {
259  INFO("%s\n", loop_ready.train.visualizer().c_str());
260  }
261  }
262  if (Settings::isSet("print_summary")) {
263  INFO("%s\n", loop_ready.train.summary().c_str());
264  }
265  if (Settings::isSet("print_pulsetrain")) {
266  INFO("%s\n", loop_ready.train.toString().c_str());
267  }
268  if (Settings::isSet("print_binlist")) {
269  INFO("%s\n", loop_ready.train.binList().c_str());
270  }
271  // Process the received pulsetrain for meaning
272  // (Done here so errors and debug output ends up in logical spot)
273  loop_ready.meaning.fromPulsetrain(loop_ready.train);
274  if (loop_ready.meaning && Settings::isSet("print_meaning")) {
275  INFO("%s\n", loop_ready.meaning.toString().c_str());
276  }
277  // Pass what was received to all the device plugins, making their output show up
278  // at the right spot underneath the meaning output.
279  Device::new_packet(loop_ready.raw, loop_ready.train, loop_ready.meaning);
280  // received() can take it now.
281  if (callback != nullptr) {
282  callback(loop_ready.raw, loop_ready.train, loop_ready.meaning);
283  }
284  }
285  loop_ready.zap();
286  return true;
287 }
288 
289 void IRAM_ATTR OOKwiz::ISR_transition() {
290  int64_t t = esp_timer_get_time() - last_transition;
291  last_transition = esp_timer_get_time();
292  if (rx_state == RX_WAIT_PREAMBLE) {
293  // Set the state machine to put the transitions in isr_in
294  if (t > first_pulse_min_len && digitalRead(Radio::pin_rx) != rx_active_high) {
295  noise_score = 0;
296  isr_in.zap();
297  isr_in.intervals.reserve((max_nr_pulses * 2) + 1);
298  rx_state = RX_RECEIVING_DATA;
299  }
300  }
301  if (rx_state == RX_RECEIVING_DATA) {
302  // t < pulse_gap_min_len means it's noise
303  if (t < pulse_gap_min_len) {
304  noise_score += noise_penalty;
305  if (noise_score >= noise_threshold) {
306  process_raw();
307  return;
308  }
309  } else {
310  noise_score -= noise_score > 0;
311  }
312  isr_in.intervals.push_back(t);
313  // Longer would be too long: stop and process what we have
314  if (isr_in.intervals.size() == (max_nr_pulses * 2) + 1) {
315  process_raw();
316  }
317 
318  }
319  timerRestart(transitionTimer);
320 }
321 
322 void IRAM_ATTR OOKwiz::ISR_transitionTimeout() {
323  if (rx_state != RX_OFF) {
324  process_raw();
325  }
326 }
327 
328 void IRAM_ATTR OOKwiz::process_raw() {
329  if (!isr_out) {
330  isr_out = isr_in;
331  } else {
332  lost_packets++;
333  }
334  isr_in.zap();
335  rx_state = RX_WAIT_PREAMBLE;
336 }
337 
338 /// @brief Use this to supply your own function that will be called every time a packet is received.
339 /**
340  * The callback_function parameter has to be the function name of a function that takes the three
341  * packet representations as arguments and does not return anything. Here's an example sketch:
342  *
343  * ```
344  * setup() {
345  * Serial.begin(115200);
346  * OOKwiz::setup();
347  * OOKwiz::onReceive(myReceiveFunction);
348  * }
349  *
350  * loop() {
351  * OOKwiz::loop();
352  * }
353  *
354  * void myReceiveFunction(RawTimings raw, Pulsetrain train, Meaning meaning) {
355  * Serial.println("A packet was received and myReceiveFunction was called.");
356  * }
357  * ```
358  *
359  * Make sure your own function is defined exactly as like this, even if you don't need all the
360  * parameters. You may change the names of the function and the parameters, but nothing else.
361 */
362 /// @param callback_function The name of your own function, without parenthesis () after it.
363 /// @return always returns `true`
364 bool OOKwiz::onReceive(void (*callback_function)(RawTimings, Pulsetrain, Meaning)) {
365  callback = callback_function;
366  return true;
367 }
368 
369 /// @brief Tell OOKwiz to start receiving and processing packets.
370 /**
371  * OOKwiz starts in receive mode normally, so you would only need to call this if your
372  * code has turned off reception (with `standby()`) or if you configured OOKwiz to not
373  * start in receive mode by setting `start_in_standby`.
374 */
375 /// @return `true` if receive mode could be activated, `false` if not.
376 bool OOKwiz::receive() {
377  if (!Radio::radio_rx()) {
378  return false;
379  }
380  // Turns on the state machine
381  rx_state = RX_WAIT_PREAMBLE;
382  return true;
383 }
384 
385 bool OOKwiz::tryToBeNice(int ms) {
386  // Try and wait for max ms for current reception to end
387  // return false if it doesn't end, true if it does
388  long start = millis();
389  while (millis() - start < ms) {
390  if (rx_state == RX_WAIT_PREAMBLE) {
391  return true;
392  }
393  }
394  return false;
395 }
396 
397 /// @brief Pretends this string representation of a `RawTimings`, `Pulsetrain` or `Meaning` instance was just received by the radio.
398 /// @param str The string representation of what needs to be simulated
399 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
400 bool OOKwiz::simulate(String &str) {
401  if (RawTimings::maybe(str)) {
402  RawTimings raw;
403  if (raw.fromString(str)) {
404  return simulate(raw);
405  }
406  } else if (Pulsetrain::maybe(str)) {
407  Pulsetrain train;
408  if (train.fromString(str)) {
409  return simulate(train);
410  }
411  } else if (Meaning::maybe(str)) {
412  Meaning meaning;
413  if (meaning.fromString(str)) {
414  return simulate(meaning);
415  }
416  } else {
417  ERROR("ERROR: string does not look like RawTimings, Pulsetrain or Meaning.\n");
418  }
419  return false;
420 }
421 
422 /// @brief Pretends this `RawTimings` instance was just received by the radio.
423 /// @param raw the instance to be simulated
424 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
425 bool OOKwiz::simulate(RawTimings &raw) {
426  tryToBeNice(50);
427  isr_out = raw;
428  return true;
429 }
430 
431 /// @brief Pretends this `Pulsetrain` instance was just received by the radio.
432 /// @param train the instance to be simulated
433 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
434 bool OOKwiz::simulate(Pulsetrain &train) {
435  tryToBeNice(50);
436  loop_ready.train = train;
437  return true;
438 }
439 
440 /// @brief Pretends this `Meaning` instance was just received by the radio.
441 /// @param meaning the instance to be simulated
442 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
443 bool OOKwiz::simulate(Meaning &meaning) {
444  Pulsetrain train;
445  if (train.fromMeaning(meaning)) {
446  return simulate(train);
447  }
448  return false;
449 }
450 
451 /// @brief Transmits this string representation of a `RawTimings`, `Pulsetrain` or `Meaning` instance.
452 /// @param str The string representation of what needs to be simulated
453 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
454 bool OOKwiz::transmit(String &str) {
455  if (RawTimings::maybe(str)) {
456  RawTimings raw;
457  if (raw.fromString(str)) {
458  return transmit(raw);
459  }
460  } else if (Pulsetrain::maybe(str)) {
461  Pulsetrain train;
462  if (train.fromString(str)) {
463  return transmit(train);
464  }
465  } else if (Meaning::maybe(str)) {
466  Meaning meaning;
467  if (meaning.fromString(str)) {
468  return transmit(meaning);
469  }
470  } else if (str.indexOf(":") != -1) {
471  String plugin_name;
472  String tx_str;
473  tools::split(str, ":", plugin_name, tx_str);
474  return Device::transmit(plugin_name, tx_str);
475  } else {
476  ERROR("ERROR: string does not look like RawTimings, Pulsetrain or Meaning.\n");
477  }
478  return false;
479 }
480 
481 /// @brief Transmits this `RawTimings` instance.
482 /// @param raw the instance to be transmitted
483 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
484 bool OOKwiz::transmit(RawTimings &raw) {
485  bool rx_was_on = (rx_state != RX_OFF);
486  // Set receiver state machine off, remove any incomplete packet in buffer
487  if (rx_was_on) {
488  tryToBeNice(500);
489  rx_state = RX_OFF;
490  isr_in.zap();
491  }
492  if (!Radio::radio_tx()) {
493  ERROR("ERROR: Transceiver could not be set to transmit.\n");
494  return false;
495  }
496  INFO("Transmitting: %s\n", raw.toString().c_str());
497  INFO(" %s\n", raw.visualizer().c_str());
498  delay(100); // So INFO prints before we turn off interrupts
499  int64_t tx_timer = esp_timer_get_time();
500  noInterrupts();
501  {
502  bool bit = tx_active_high;
503  PIN_WRITE(Radio::pin_tx, bit);
504  for (uint16_t interval: raw.intervals) {
505  delayMicroseconds(interval);
506  bit = !bit;
507  PIN_WRITE(Radio::pin_tx, bit);
508  }
509  PIN_WRITE(Radio::pin_tx, !tx_active_high); // Just to make sure we end with TX off
510  }
511  interrupts();
512  tx_timer = esp_timer_get_time() - tx_timer;
513  INFO("Transmission done, took %i µs.\n", tx_timer);
514  delayMicroseconds(400);
515  // return to state it was in before transmit
516  if (rx_was_on) {
517  receive();
518  } else {
519  standby();
520  }
521  return true;
522 }
523 
524 /// @brief Transmits this `Pulsetrain` instance.
525 /// @param train the instance to be transmitted
526 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
527 bool OOKwiz::transmit(Pulsetrain &train) {
528  bool rx_was_on = (rx_state != RX_OFF);
529  // Set receiver state machine off, remove any incomplete packet in buffer
530  if (rx_was_on) {
531  tryToBeNice(500);
532  rx_state = RX_OFF;
533  isr_in.zap();
534  }
535  if (!Radio::radio_tx()) {
536  ERROR("ERROR: Transceiver could not be set to transmit.\n");
537  return false;
538  }
539  INFO("Transmitting %s\n", train.toString().c_str());
540  INFO(" %s\n", train.visualizer().c_str());
541  delay(100); // So INFO prints before we turn off interrupts
542  int64_t tx_timer = esp_timer_get_time();
543  for (int repeat = 0; repeat < train.repeats; repeat++) {
544  noInterrupts();
545  {
546  bool bit = tx_active_high;
547  PIN_WRITE(Radio::pin_tx, bit);
548  for (int transition : train.transitions) {
549  uint16_t t = train.bins[transition].average;
550  delayMicroseconds(t);
551  bit = !bit;
552  PIN_WRITE(Radio::pin_tx, bit);
553  }
554  PIN_WRITE(Radio::pin_tx, !tx_active_high); // Just to make sure we end with TX off
555  }
556  interrupts();
557  delayMicroseconds(train.gap);
558  }
559  tx_timer = esp_timer_get_time() - tx_timer;
560  INFO("Transmission done, took %i µs.\n", tx_timer);
561  delayMicroseconds(400);
562  // return to state it was in before transmit
563  if (rx_was_on) {
564  receive();
565  } else {
566  standby();
567  }
568  return true;
569 }
570 
571 /// @brief Transmits this `Meaning` instance.
572 /// @param meaning the instance to be transmitted
573 /// @return `true` if it worked, `false` if not. Will show error message telling you why it didn't work in latter case.
574 bool OOKwiz::transmit(Meaning &meaning) {
575  Pulsetrain train;
576  if (train.fromMeaning(meaning)) {
577  return transmit(train);
578  }
579  return false;
580 }
581 
582 /// @brief Sets radio standby mode, turning off reception
583 /// @return The counterpart to `receive()`, turns off reception.
584 bool OOKwiz::standby() {
585  if (rx_state != RX_OFF) {
586  tryToBeNice(500);
587  isr_in.zap();
588  rx_state = RX_OFF;
589  Radio::radio_standby();
590  }
591  return true;
592 }