Light installation strobe interval advice with HC-S04 sensors: pics&code incl.

Hello Arduino community!

This is my first time posting on the forum so I hope I’m follow the correct protocol with everything! (unsure if this should go in 'Project Guidance’ or ‘Sensors’.. but I thought I’d post it here to start)

I recently built a light installation for a festival (Img.1), using the HC-SR04 ultrasonic range sensors to control a relay module switching LED baton lights. The installation was supposed to comprise of 3 triangular prism towers arranged facing each other (Img.2: ariel view sketch).

The original plan was that once people enter the triangular arrangement of towers, all the inner lights (coloured) would turn on (RELAY 1 in code - if between 80-300cm). As you move closer towards each tower (RELAY 1 if average<60cm), the light facing you would stobe faster.

The outside lights (RELAY 2 and RELAY3), would strobe based on the proximity value only when it’s <260cm, otherwise they will perform a fast ‘blink’ at random intervals between 10000-20000ms.. the idea was that the outside lights would subtly ‘beckon’ festival goers into the otherwise dark installation which would all activate once people were nearby/inside.

Anyway I ran into a few issues on the day. One Arduino broke (hence why there are only 2 towers in the first picture) but more importantly the program wasn’t running anywhere near as well as it did when I tested it in my studio.

I think directly using the distance value calculated from the HC-SR04 for the relay switch interval value wasn’t a good idea. If the sensors weren’t getting an ‘echo’ response, it seemed as though it delayed the program. When all 3 sensors for each tower were getting a response, the program would run well and the strobe-rate would be correct. Otherwise the behaviour was a bit strange/unpredictable. I think this is why in a studio with walls for the 'ping' to bounce off, it seems to work well.

I experimented with averaging for RELAY1 to deal with some of unstable sensor reading values, but it seemed to delay the immediacy of the interaction with the installation. I think I got rid of it on the final version I used, but it's still in this version of the code.

I’ll post the code in the next reply - it makes the post too long unfortunately.. I’m really sorry it’s probably a huge mess - I’m still quite new to Arduino and it’s an amalgamation of lots of things from other places.. definitely a bit of cowboy coding going on!

I’m mainly looking to just get a reliable interval value, so the lights are responsive when in an outside environment where they may get no ‘echo’ response (if nobody is in-front of the towers). I thought that the strobe would just initiate when the distance value is < (x), although clearly I’m missing something that’s delaying the program. Now I’m thinking about it, there’s definitely something wrong with how much is going on in the loop().. so I’m assuming I need to take some functions out of there somehow?

If anybody could give me any pointers on how to get a good, stable strobe rate (relay switch interval) with the HC-S04 distance readings that would be amazing!

Please feel free to correct me on any other errors in my programming as well.

Thanks so much for taking the time to help out!!!


Img.1


Img.2

#define trigPinA 13
#define echoPinA 12
#define trigPinB 11
#define echoPinB 10
#define trigPinC 9
#define echoPinC 8

// constants won't change. Used here to set a pin number:
const byte RELAY1 = 2;
const byte RELAY2 = 3;
const byte RELAY3 = 4;

// Variables will change:
int RELAY1State = LOW;
int RELAY2State = LOW;
int RELAY3State = LOW;

// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long RELAY1previousMillis = 0;
unsigned long RELAY2previousMillis = 0;
unsigned long RELAY3previousMillis = 0;

const unsigned int onTime = 100;

// Used to track if LED should be on or off
boolean RELAY2state = true;
boolean RELAY3state = true;

// Interval is how long we wait
int RELAY2interval = onTime;
int RELAY3interval = onTime;


const int numReadings = 10;

int readings[numReadings];      // the readings from the analog input
int readIndex = 0;              // the index of the current reading
int total = 0;                  // the running total
int average = 0;                // the average


void setup() {
  Serial.begin (9600);
  pinMode(trigPinA, OUTPUT);
  pinMode(echoPinA, INPUT);
  pinMode(trigPinB, OUTPUT);
  pinMode(echoPinB, INPUT);
  pinMode(trigPinC, OUTPUT);
  pinMode(echoPinC, INPUT);
  // set the digital pin as output:
  pinMode (RELAY1, OUTPUT);
  pinMode (RELAY2, OUTPUT);
  pinMode (RELAY3, OUTPUT);

    for (int thisReading = 0; thisReading < numReadings; thisReading++) {
    readings[thisReading] = 0;
  }
}


void loop() {
  //PROXIMITY A
  long durationA, distanceA;
  digitalWrite(trigPinA, LOW);  // Added this line
  delayMicroseconds(2); // Added this line
  digitalWrite(trigPinA, HIGH);
  delayMicroseconds(5); // Added this line
  digitalWrite(trigPinA, LOW);
  durationA = pulseIn(echoPinA, HIGH);
  distanceA = (durationA/2 / 29.1);

   //DistanceA averaging:
   // subtract the last reading:
   total = total - readings[readIndex];
   // read from the sensor:
   readings[readIndex] = distanceA;
   // add the reading to the total:
   total = total + readings[readIndex];
   // advance to the next position in the array:
   readIndex = readIndex + 1;

   // if we're at the end of the array...
   if (readIndex >= numReadings) {
   // ...wrap around to the beginning:
    readIndex = 0;
    }

   // calculate the average:
   average = total / numReadings;
   // send it to the computer as ASCII digits
   Serial.println(average);
   delay(1);        // delay in between reads for stability

   Serial.print(distanceA);
   Serial.println(" cm A");
  
  //PROXIMITY B
  long durationB, distanceB;
  digitalWrite(trigPinB, LOW);  // Added this line
  delayMicroseconds(2); // Added this line
  digitalWrite(trigPinB, HIGH);
  delayMicroseconds(5); // Added this line
  digitalWrite(trigPinB, LOW);
  durationB = pulseIn(echoPinB, HIGH);
  distanceB = (durationB/2) / 29.1;


  Serial.print(distanceB);
  Serial.println(" cm B");
  

  //PROXIMITY C
  long durationC, distanceC;
  digitalWrite(trigPinC, LOW);  // Added this line
  delayMicroseconds(2); // Added this line
  digitalWrite(trigPinC, HIGH);
  delayMicroseconds(5); // Added this line
  digitalWrite(trigPinC, LOW);
  durationC = pulseIn(echoPinC, HIGH);
  distanceC = (durationC/2) / 29.1;


  Serial.print(distanceC);
  Serial.println(" cm C");


  
  // here is where you'd put code that needs to be running all the time.

  // check to see if it's time to blink the LED; that is, if the difference
  // between the current time and last time you blinked the LED is bigger than
  // the interval at which you want to blink the LED.
  unsigned long RELAY1currentMillis = millis();
  unsigned long RELAY2currentMillis = millis();
  unsigned long RELAY3currentMillis = millis();



//RELAY 1
if (average <60) { // DO FLASH WITHIN 60CM (MIN) DISTANCE
   int RELAY1interval = distanceA;
   if (RELAY1currentMillis - RELAY1previousMillis >= RELAY1interval) {
    // save the last time you blinked the LED
    RELAY1previousMillis = RELAY1currentMillis;

    // if the LED is off turn it on and vice-versa:
    if (RELAY1State == LOW) {
      RELAY1State = HIGH;
    } else {
      RELAY1State = LOW;
    }

    // set the LED with the ledState of the variable:
    digitalWrite(RELAY1, RELAY1State);
  }
} 

else if ((average > 80) && (average < 300)) { // TURN ON BETWEEN MAX(300cm)/MIN(80cm)
    digitalWrite(RELAY1, LOW);
}

else  { // TURN OFF in all other contexts
    digitalWrite(RELAY1, HIGH);
}
 

//RELAY 2
if (distanceB < 260) {  // DO FLASH WITHIN 260CM (MIN) DISTANCE
    int RELAY2interval = distanceB;
   if (RELAY2currentMillis - RELAY2previousMillis >= RELAY2interval) {
    // save the last time you blinked the LED
    RELAY2previousMillis = RELAY2currentMillis;

    // if the LED is off turn it on and vice-versa:
    if (RELAY2State == LOW) {
      RELAY2State = HIGH;
    } else {
      RELAY2State = LOW;
    }

    // set the LED with the ledState of the variable:
    digitalWrite(RELAY2, RELAY2State);
  }
}
  
  else { //OTHERWISE 'BLINK' RANDOMLY
  int offTime = random(10000,20000);

  // Set Pin 13 to state of RELAY3state each timethrough loop()
  // If RELAY3State hasn't changed, neither will the pin
  digitalWrite(3, RELAY2state);
 
  // Grab snapshot of current time, this keeps all timing
  // consistent, regardless of how much code is inside the next if-statement
  //----REMOVED   unsigned long currentMillis = millis();
 
  // Compare to previous capture to see if enough time has passed
  if ((unsigned long)(RELAY2currentMillis - RELAY2previousMillis) >= RELAY2interval) {
    // Change wait interval, based on current LED state
    if (RELAY2state) {
      // LED is currently on, set time to stay off
      RELAY2interval = onTime;
    } else {
      // LED is currently off, set time to stay on
      RELAY2interval = offTime;
    }
    // Toggle the LED's state
    RELAY2state = !(RELAY2state);
 
    // Save the current time to compare "later"
    RELAY2previousMillis = RELAY2currentMillis;
  }
  }

//RELAY 3
if (distanceC < 260) {  // DO FLASH WITHIN 260CM (MIN) DISTANCE
  int RELAY3interval = distanceC;
   if (RELAY3currentMillis - RELAY3previousMillis >= RELAY3interval) {
    // save the last time you blinked the LED
    RELAY3previousMillis = RELAY3currentMillis;

    // if the LED is off turn it on and vice-versa:
    if (RELAY3State == LOW) {
      RELAY3State = HIGH;
    } else {
      RELAY3State = LOW;
    }

    // set the LED with the ledState of the variable:
    digitalWrite(RELAY3, RELAY3State);
  }
}

  else { //OTHERWISE 'BLINK' RANDOMLY
int offTime = random(10000,20000);

  // Set Pin 13 to state of RELAY3state each timethrough loop()
  // If RELAY3State hasn't changed, neither will the pin
  digitalWrite(4, RELAY3state);
 
  // Grab snapshot of current time, this keeps all timing
  // consistent, regardless of how much code is inside the next if-statement
  //----REMOVED unsigned long currentMillis = millis();
 
  // Compare to previous capture to see if enough time has passed
  if ((unsigned long)(RELAY3currentMillis - RELAY3previousMillis) >= RELAY3interval) {
    // Change wait interval, based on current LED state
    if (RELAY3state) {
      // LED is currently on, set time to stay off
      RELAY3interval = onTime;
    } else {
      // LED is currently off, set time to stay on
      RELAY3interval = offTime;
    }
    // Toggle the LED's state, Fancy, eh!?
    RELAY3state = !(RELAY3state);
 
    // Save the current time to compare "later"
    RELAY3previousMillis = RELAY3currentMillis;
  }
  }
    
  
 
}

First you talk about strobe (typically several flashes per second) but then you really do a slow on/off switch of 10-20 seconds. I was already wondering how you would do a strobe with relays, as they're too slow for that.

For your code: the pulseIn() function has a default timeout of 1 second, so reading three sensors means your program is stuck for over 3 seconds at a time. That's likely to give performance issues.

Add a timeout to the pulseIn() call. Your distance limit is 3 meters, make that 5 meters, sound travels 10 meters (5 meters there and back) in 33 ms, so you can set your timeout to that value. Will make your code much more responsive.

durationA = pulseIn(echoPinA, HIGH, 33);

wvmarle:
First you talk about strobe (typically several flashes per second) but then you really do a slow on/off switch of 10-20 seconds. I was already wondering how you would do a strobe with relays, as they're too slow for that.

For your code: the pulseIn() function has a default timeout of 1 second, so reading three sensors means your program is stuck for over 3 seconds at a time. That's likely to give performance issues.

Add a timeout to the pulseIn() call. Your distance limit is 3 meters, make that 5 meters, sound travels 10 meters (5 meters there and back) in 33333 µs, so you can set your timeout to that value. Will make your code much more responsive.

durationA = pulseIn(echoPinA, HIGH, 33333);

Thanks for your reply wvmarle! The flashing was pretty fast when the distance was <10cm.. I guess the interval would be 10ms?.. The lights definitely strobed pretty fast (they almost just look on)! So the realys seemed to handle it quite well. There's a video on them in action here if you're interested: Matthew Woodham on Instagram: "Field Maneuvers Interactive LED Prism Tower Installation 03. #arduino #proximity #sensor #interactive #light #installation #fieldmaneuvers"

I've just remembered that in my final code, I actually divided the distance calculation by 3 to make the interval smaller... so it was actually like this:

distanceA = ((durationA/2 / 29.1))/3;

Bit of a gross way to get a smaller, more strobe-rate intervals but just had to find something that worked at the time. Apologies I should've checked more the differences between versions! Let me know if you know a more elegant way to implement that.

That's super useful to know about the timeout - I didn't know that! That must be what's delaying it, thanks so much!

I'll test out 33333 as the timeout argument - is the pulseIn timeout value in µs then?

Thanks again.

10 ms = 100 per second. That's a very busy relay, you'll hear it rattling.

And yes that timeout is µs.

It was a busy relay - I've used those cheapo relay modules from ebay for a few projects and they seem to handle fast switching really well so far.. not had any problems with them yet even over fairly long periods.

Cool I'll try that µs timeout argument, thanks again for that!

Is there a better way to reduce the interval rate or is just dividing the distance good enough?

Cheers!

You really should use the NewPing library...it allows you to:

  • ping very quickly (sez up to 30 Hz)
  • set a maximum distance where pings beyond that distance are read as no ping
  • eliminates the one second delay from pulseIn
  • makes your code cleaner and more compact
  • allows you to easily use a median reading (to eliminate the occasional bogus hit)

etc., etc., etc.

Read about it here and here.

Also, how far apart are your sensors and where are they pointing? They may be interfering with each other.

Adding the timeout to pulseIn() does most what newPing does. It's that long blocking that appears to be the real problem OP has (lights not flashing fast enough).

Thanks for the heads up about the NewPing library Dave - I missed that, seems really handy! Definitely could use the median reading feature as well for when the readings need to be more stable.

The sensors were on a triangular prism tower (so pointing in different directions) and a good distant apart from each other so I don't think they were interfering too much - I found the problem when only one tower was powered up as well so it wouldn't be the other towers either.

Good to know that can be a problem as well, I tried to account for interference but could probably be a bit more careful about that.

For now I guess I'll try adding a timeout as I need a quick fix for a temporary version of this installation on a film shoot in a couple weeks, and I don't know if I'll have time to rewrite the sketch with the NewPing library just yet.

Thanks again both of you for your input.