Hello,
I am not quite familiar with C++ but I was studying the render.cpp example for the ultrasonic sensor with Pure Data, hoping to be able to add two more sensors of the same kind to it.

After noticing that they tend to become noisy when together in PureData, and reading some older posts on this forum of people solving this problem using a render.cpp file together with a Pure Data patch, I am positive that this way might make the reading a bit cleaner (it's not terrible right now but there are outliers quite frequently, some of them are negative numbers).

I'd like to keep it simple and copy-paste the sections of the code that refer to the ultrasonic sensor, adding the additional pins used and naming 2 more variables to receive in pure data (like distance 1, distance2 and distance3). This way is not really elegant, so if anyone have suggestions I'm happy to hear them.

Anyway, approaching this task I stumble upon a series of doubts. The first one is about this section (line 34-50 of the example/PureData/custom-render-ultrasonic-distance-sensor):

#define ULTRASONIC_DISTANCE
#ifdef ULTRASONIC_DISTANCE
#define ULTRASONIC_DISTANCE_DIGITAL_OUT // undefine this to use ANALOG out instead
#include <libraries/PulseIn/PulseIn.h>
PulseIn pulseIn;
unsigned int gPulseTriggerIntervalMs = 60; // how often to send out a trigger.
unsigned int gPulseTriggerIntervalSamples; // Set in setup() based on the above
int gPulseMinLength = 7; //to avoid spurious readings
float gPulseRescale = 58; // taken from the datasheet
#ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
unsigned int gPulseTrigDigitalOutPin = 0; //channel to be connected to the module's TRIGGER pin - check the pin diagram in the IDE
#else // ULTRASONIC_DISTANCE_DIGITAL_OUT
unsigned int gPulseTrigAnalogOutPin = 0; //channel to be connected to the module's TRIGGER pin
#endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
unsigned int gPulseEchoDigitalInPin = 1; //channel to be connected to the modules's ECHO pin via resistor divider. Check the pin diagram in the IDE
int gPulseTriggerCount = 0;
#endif // ULTRASONIC_DISTANCE

Since I want to add 2 more couples of digital pins, do I have to #define again ULTRASONIC_DISTANCE, so that each of them can define a ULTRASONIC_DISTANCE_DIGITAL_OUT(1, 2 and 3)?

Sorry if the question sounds dumb, I have no easy ways to approach this given my current knowledge of C++.

    One common way of going from "one" instance to "more" instances is to use arrays. In its simplest form, each variable which is associated with one instance should be turned from a scalar (e.g.: float myvar = 0;) to an array (e.g.: float myvar[3] = {0, 1, 2};). Better even than having a 3 hardcoded in there, one would normally have a constant expression or a #define for the number of items. Then, when accessing each instance, you'd do that in a for loop.

    So the code you pasted could become:

    #define ULTRASONIC_DISTANCE
    #ifdef ULTRASONIC_DISTANCE
    #define ULTRASONIC_DISTANCE_DIGITAL_OUT // undefine this to use ANALOG out instead
    #include <libraries/PulseIn/PulseIn.h>
    enum { kNumSensors = 3 };
    PulseIn pulseIn[kNumSensors];
    unsigned int gPulseTriggerIntervalMs = 60; // how often to send out a trigger.
    unsigned int gPulseTriggerIntervalSamples; // Set in setup() based on the above
    int gPulseMinLength = 7; //to avoid spurious readings
    float gPulseRescale = 58; // taken from the datasheet
    #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
    unsigned int gPulseTrigDigitalOutPin[kNumSensors] = {0, 2, 4}; //channel to be connected to the module's TRIGGER pin - check the pin diagram in the IDE
    #else // ULTRASONIC_DISTANCE_DIGITAL_OUT
    unsigned int gPulseTrigAnalogOutPin[kNumSensors] = {0, 1, 2}; //channel to be connected to the module's TRIGGER pin
    #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
    unsigned int gPulseEchoDigitalInPin[kNumSensors] = {1, 3, 5}; //channel to be connected to the modules's ECHO pin via resistor divider. Check the pin diagram in the IDE
    int gPulseTriggerCount [kNumSensors] = {0, 0, 0};
    #endif // ULTRASONIC_DISTANCE

    don't worry about the use of enum { kNumSensors = 3 }; if you are unfamiliar with that syntax ... C and C++ are a bit confusing in this respect.

    The rest of the code:

    #ifdef ULTRASONIC_DISTANCE
        gPulseTriggerIntervalSamples = context->digitalSampleRate * (gPulseTriggerIntervalMs / 1000.f);
    #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
        pinMode(context, 0, gPulseTrigDigitalOutPin, OUTPUT); // writing to TRIGGER pin
    #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
        pinMode(context, 0, gPulseEchoDigitalInPin, INPUT); // reading from ECHO pin
        pulseIn.setup(context, gPulseEchoDigitalInPin, HIGH); //detect HIGH pulses on the ECHO pin
    #endif // ULTRASONIC_DISTANCE

    becomes

    #ifdef ULTRASONIC_DISTANCE
        gPulseTriggerIntervalSamples = context->digitalSampleRate * (gPulseTriggerIntervalMs / 1000.f);
        for(unsigned int n = 0; n < kNumSensors; ++n)
        {
    #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
            pinMode(context, 0, gPulseTrigDigitalOutPin[n], OUTPUT); // writing to TRIGGER pin
    #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
            pinMode(context, 0, gPulseEchoDigitalInPin[n], INPUT); // reading from ECHO pin
            pulseIn.setup(context, gPulseEchoDigitalInPin[n], HIGH); //detect HIGH pulses on the ECHO pin
        }
    #endif // ULTRASONIC_DISTANCE

    And

    #ifdef ULTRASONIC_DISTANCE
        // this has to be at the bottom in case you use analogOut instead of digitalOut
        for(unsigned int n = 0; n < context->digitalFrames; ++n){
            gPulseTriggerCount++;
            bool state;
            if(gPulseTriggerCount == gPulseTriggerIntervalSamples){
                gPulseTriggerCount = 0;
                state = HIGH;
            } else {
                state = LOW;
            }
    #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
            digitalWrite(context, n, gPulseTrigDigitalOutPin, state); //write the state to the trig pin
    #else // ULTRASONIC_DISTANCE_DIGITAL_OUT
            // the below assumes analogFrames <= digitalFrames
            unsigned int frame = n / (context->digitalFrames / context->analogFrames);
            analogWrite(context, frame, gPulseTrigAnalogOutPin, state); //write the state to the trig pin
    #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
            int pulseLength = pulseIn.hasPulsed(context, n); // will return the pulse duration(in samples) if a pulse just ended
            float duration = 1e6 * pulseLength / context->digitalSampleRate; // pulse duration in microseconds
            static float distance = 0;
            if(pulseLength >= gPulseMinLength){
                // rescaling according to the datasheet
                distance = duration / gPulseRescale;
                // send to Pd
                libpd_float("distance", distance);
            }
        }
    #endif // ULTRASONIC_DISTANCE

    should be

    #ifdef ULTRASONIC_DISTANCE
        // this has to be at the bottom in case you use analogOut instead of digitalOut
        for(unsigned int n = 0; n < context->digitalFrames; ++n){
            for(unsigned int p = 0; p < kNumSensors; ++p)
            {
                gPulseTriggerCount[p]++;
                bool state;
                if(gPulseTriggerCount[p] == gPulseTriggerIntervalSamples){
                     gPulseTriggerCount[p] = 0;
                     state = HIGH;
                } else {
                    state = LOW;
                }
    #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
                digitalWrite(context, n, gPulseTrigDigitalOutPin[p], state); //write the state to the trig pin
    #else // ULTRASONIC_DISTANCE_DIGITAL_OUT
                // the below assumes analogFrames <= digitalFrames
                unsigned int frame = n / (context->digitalFrames / context->analogFrames);
                analogWrite(context, frame, gPulseTrigAnalogOutPin[p], state); //write the state to the trig pin
    #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
                int pulseLength = pulseIn[p].hasPulsed(context, n); // will return the pulse duration(in samples) if a pulse just ended
                float duration = 1e6 * pulseLength / context->digitalSampleRate; // pulse duration in microseconds
                if(pulseLength >= gPulseMinLength){
                    // rescaling according to the datasheet
                    float distance = duration / gPulseRescale;
                    // send to Pd
                    libpd_start_message(2);
                    libpd_add_float(p);
                    libpd_add_float(distance);
                    libpd_finish_list("distance");
                }
            }
        }
    #endif // ULTRASONIC_DISTANCE

    (note here that libpd_float() has been replaced by 4 lines which create a list of two floats. The first float is the sensor number, the second float is the distance.
    All of the above untested.

    To further dispel confusion, the conditional around ULTRASONIC_DIGITAL_OUT could probably be removed as I'd expect most people to use the digital outputs instead of the analog outs.

    However ...

    robinm After noticing that they tend to become noisy when together in PureData, and reading some older posts on this forum of people solving this problem using a render.cpp file together with a Pure Data patch, I am positive that this way might make the reading a bit cleaner (it's not terrible right now but there are outliers quite frequently, some of them are negative numbers).

    The most likely cause for negative numbers and "noise" is that the sensors are not placed in the same location and they are not well decoupled physically so that triggers from one are picked up by others (crosstalk) (unless there is some coding issue where variables are shared in a way that they shouldn't). Unfortunately the code above won't fix that and I don't think you strictly need a C++ implementation to get around that. One way to try and minimise the problem would be to have one single sensor active at any single time:

    send sensor 0 trigger ... wait for sensor 0 echo (or max timeout), then
    send sensor 1 trigger ... wait for sensor 1 echo (or max timeout), then
    send sensor 2 trigger ... wait for sensor 2 echo (or max timeout)

    This approach has two drawbacks: it sets a hard max limit on the measurable distance (because of the timeout) and it reduces the sampling frequency for each sensor, but it could be the only solution if the sensors locations cannot be changed to remove crosstalk. The third part of C++ code above would become more complex with these changes.

    If you look at examples/PureData/ultrasonic-distance-sensor, the basic patch for one sensor looks like this (I removed the comments for clarity):

    alt text

    and here is the multi-sensor version with throttling (done with a counter and [select]) to remove crosstalk:

    alt text

      giuliomoro Thanks for the thorough explanation. I had no idea that I could use arrays for this purpose, I thought the most efficient way to do this would've been to make some kind of class and call it three times, honestly this way is very clear and I'm going to try it out. The only doubt regarding this remains

      giuliomoro (note here that libpd_float() has been replaced by 4 lines which create a list of two floats. The first float is the sensor number, the second float is the distance.

      how to call this from Pure Data - if I understand correctly, I'll use 1 receive object [r distance] that will spit out a list of three indexed floats, one for each sensor's value?
      Either ways I'm gonna test it out and play around with it a bit, I'll let you know if it works like this.

      giuliomoro To further dispel confusion, the conditional around ULTRASONIC_DIGITAL_OUT could probably be removed as I'd expect most people to use the digital outputs instead of the analog outs.

      I'm curious what could be the pros and cons of using analog outputs instead of digital outputs in this case. I saw a post on the forum about this but didn't see any immediate advantage of using analog, might still give this a try if I have some spare time.

      giuliomoro This approach has two drawbacks: it sets a hard max limit on the measurable distance (because of the timeout) and it reduces the sampling frequency for each sensor, but it could be the only solution if the sensors locations cannot be changed to remove crosstalk. The third part of C++ code above would become more complex with these changes.

      Interesting. I tried to keep this phenomenon into account and placed them facing all outwards from Bela, hoping to limit the amount of spurious reflection each one could get from one another - moreover, I thought I was somehow handling the problem by adding a 50ms delay between each sensor's reading and printing, like this:

      alt text
      (note that there's an additional radar sensor using digital in 17, and also I tweaked the correction offset from -21.1 to - 428 since I'm using a pretty huge audio block size, and this value seemed to counteract the shifting of sensors' values). I see now how this way can not entirely prevent crosstalk, I didn't think of using [select] cause I was afraid of introducing too much latency between one sensor and the other but I see that it'll probably add just like 20ms more to the total process which is not a big deal.

      Time to test this out!

        Tried the Pure Data "select" approach and I have to say, it works really well 😃 Feel like I've spent a crazy amount of time just trying to figure out how to deal with those random outliers, when the solution would have been this quick! Thanks @giuliomoro for the precious help.

        Testing the C++ code "alternative" though, I get few errors:

        alt text

        Referring to these lines:

        alt text

        alt text

        I have no idea what the first 3 errors mean, while the last one might be some mistake from my end.
        Even though I don't think I will need this code anymore, I'm still curious if @giuliomoro or anybody else could give me a hint on how to fix it.
        In this test I was using a project containing only the render.cpp and a _main.pd patch containing simply
        [r distance]
        |
        [print]

        Thanks so much!

          robinm I have no idea what the first 3 errors mean, while the last one might be some mistake from my end.

          The first one was a missing [n], while the rest was due to a missing brace. Here are the fixed chunks:

          #define ULTRASONIC_DISTANCE
          #ifdef ULTRASONIC_DISTANCE
          #define ULTRASONIC_DISTANCE_DIGITAL_OUT // undefine this to use ANALOG out instead
          #include <libraries/PulseIn/PulseIn.h>
          enum { kNumSensors = 3 };
          PulseIn pulseIn[kNumSensors];
          unsigned int gPulseTriggerIntervalMs = 60; // how often to send out a trigger.
          unsigned int gPulseTriggerIntervalSamples; // Set in setup() based on the above
          int gPulseMinLength = 7; //to avoid spurious readings
          float gPulseRescale = 58; // taken from the datasheet
          #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
          unsigned int gPulseTrigDigitalOutPin[kNumSensors] = {0, 2, 4}; //channel to be connected to the module's TRIGGER pin - check the pin diagram in the IDE
          #else // ULTRASONIC_DISTANCE_DIGITAL_OUT
          unsigned int gPulseTrigAnalogOutPin[kNumSensors] = {0, 1, 2}; //channel to be connected to the module's TRIGGER pin
          #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
          unsigned int gPulseEchoDigitalInPin[kNumSensors] = {1, 3, 5}; //channel to be connected to the modules's ECHO pin via resistor divider. Check the pin diagram in the IDE
          int gPulseTriggerCount [kNumSensors] = {0, 0, 0};
          #endif // ULTRASONIC_DISTANCE
          #ifdef ULTRASONIC_DISTANCE
                  gPulseTriggerIntervalSamples = context->digitalSampleRate * (gPulseTriggerIntervalMs / 1000.f);
                  for(unsigned int p = 0; p < kNumSensors; ++p)
                  {
          #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
                          pinMode(context, 0, gPulseTrigDigitalOutPin[p], OUTPUT); // writing to TRIGGER pin
          #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
                          pinMode(context, 0, gPulseEchoDigitalInPin[p], INPUT); // reading from ECHO pin
                          pulseIn[p].setup(context, gPulseEchoDigitalInPin[p], HIGH); //detect HIGH pulses on the ECHO pin
                  }
          #endif // ULTRASONIC_DISTANCE

          and

          #ifdef ULTRASONIC_DISTANCE
                  // this has to be at the bottom in case you use analogOut instead of digitalOut
                  for(unsigned int n = 0; n < context->digitalFrames; ++n)
                  {
                          for(unsigned int p = 0; p < kNumSensors; ++p)
                          {
                                  gPulseTriggerCount[p]++;
                                  bool state;
                                  if(gPulseTriggerCount[p] == gPulseTriggerIntervalSamples){
                                          gPulseTriggerCount[p] = 0;
                                          state = HIGH;
                                  } else {
                                          state = LOW;
                                  }
          #ifdef ULTRASONIC_DISTANCE_DIGITAL_OUT
                                  digitalWrite(context, n, gPulseTrigDigitalOutPin[p], state); //write the state to the trig pin
          #else // ULTRASONIC_DISTANCE_DIGITAL_OUT
                                  // the below assumes analogFrames <= digitalFrames
                                  unsigned int frame = n / (context->digitalFrames / context->analogFrames);
                                  analogWrite(context, frame, gPulseTrigAnalogOutPin[p], state); //write the state to the trig pin
          #endif // ULTRASONIC_DISTANCE_DIGITAL_OUT
                                  int pulseLength = pulseIn[p].hasPulsed(context, n); // will return the pulse duration(in samples) if a pulse just ended
                                  float duration = 1e6 * pulseLength / context->digitalSampleRate; // pulse duration in microseconds
                                  if(pulseLength >= gPulseMinLength){
                                          // rescaling according to the datasheet
                                          float distance = duration / gPulseRescale;
                                          // send to Pd
                                          libpd_start_message(2);
                                          libpd_add_float(p);
                                          libpd_add_float(distance);
                                          libpd_finish_list("distance");
                                  }
                          }
                  }
          #endif // ULTRASONIC_DISTANCE

          This should build, can you test it and let me know if it works?

          On the pd side, you will need

          [r distance]
          |
          [route 0 1 2]
          |     |     |
          |     |     [print sensor2:]
          |     [print sensor1:]
          [print sensor0:] 

          The above builds and runs, but I cannot guarantee it works. Could you give it a try and let me know the outcome? If you see the same negative values issues you were seeing previously, then it's a crosstalk issue, otherwise it may have been a coding problem.

          robinm had no idea that I could use arrays for this purpose, I thought the most efficient way to do this would've been to make some kind of class and call it three times, honestly this way is very clear and I'm going to try it out. The only doubt regarding this remains

          well .... a better way of doing it would have been with a class which includes all the state variables and then an array (maybe a std::array or std::vector) containing three instances of the class. The above method is quick and dirty and should be easier to understand than writing a class.

          robinm I'm curious what could be the pros and cons of using analog outputs instead of digital outputs in this case. I saw a post on the forum about this but didn't see any immediate advantage of using analog, might still give this a try if I have some spare time.

          The 3.3V out from the digital out is enough to trigger the sensor, so either approach would work just fine. Which one to favour really depends on what channels you have available. For instance, on BelaMini one would have no analog outputs and has no choice but to use digital outs. On the other hand, on a Bela one may have the analog outs already busy and prefer to use a spare digital channel, or viceversa have all the analog outs free with no spare digital channel and therefore prefer to use the analog outs.

            giuliomoro The above builds and runs, but I cannot guarantee it works. Could you give it a try and let me know the outcome? If you see the same negative values issues you were seeing previously, then it's a crosstalk issue, otherwise it may have been a coding problem.

            After the edits everything above works just fine, thanks. Apparently no crosstalk. I'm a bit confused whether the problem was that or the code.. I guess I'm gonna try to run this render.cpp together with my main sound patch, which is quite heavy, to see if something goes wrong there and I get messed up values - even though if it was crosstalk, it should've appeared even in this lighter code right?
            I'll let you know, but this triplet worked very well like you wrote it.

            giuliomoro well .... a better way of doing it would have been with a class which includes all the state variables and then an array (maybe a std::array or std::vector) containing three instances of the class. The above method is quick and dirty and should be easier to understand than writing a class.

            absolutely! I could pretty much follow and not just trust, which is nice when I have to handle code I'm really unfamiliar with. Thanks for keeping it basic 🙂

            giuliomoro The 3.3V out from the digital out is enough to trigger the sensor, so either approach would work just fine. Which one to favour really depends on what channels you have available. For instance, on BelaMini one would have no analog outputs and has no choice but to use digital outs. On the other hand, on a Bela one may have the analog outs already busy and prefer to use a spare digital channel, or viceversa have all the analog outs free with no spare digital channel and therefore prefer to use the analog outs.

            Understandable, got it.

            2 years later

            Hey! I don't know if this is the right place to put this, but I just tried the Pure Data custom-render-ultrasonic-distance-sensor example with more then one sensor and realised there is a typo in the custom render cpp file. On line 899 it says

            pulseIn[p].setup(context, gPulseEchoDigitalInPin[n], HIGH); //detect HIGH pulses on the ECHO pin

            where it should say

            pulseIn[p].setup(context, gPulseEchoDigitalInPin[p], HIGH); //detect HIGH pulses on the ECHO pin

            took me a while to realise that so if anyone else was wondering ...