Making a runaway proof camera trigger

Hi everyone,

I've been working on various camera trap systems with great success, but I wanted to make a "runaway" proof system. If the break beam becomes misaligned (which happens at times) or there is another issue I wanted it to "lock" after about 10 seconds of triggering so that it doesn't keep firing and burn out the camera and flashes.

I have 1 input and three outputs on an ATTINY85

SENSOR = 3 (input)
BEAM = 2 (output) - confirmation of sensor receiving, to GND via LED
TRIGGER = 1 (output) - shows camera is being activated, to camera via an LED and 4N35
ERROR_PIN = 0 (output) - shows an error, to GND via LED

So:

If SENSOR is LOW,

  • TRIGGER would go HIGH for maximum 10 seconds then,
  • TRIGGER would go LOW
  • ERROR_PIN would go HIGH, hanging there until if or when the SENSOR goes HIGH

If the SENSOR is HIGH,

  • Interrupt the "no signal"
  • TRIGGER would go LOW
  • ERROR_PIN would go LOW
  • BEAM would go HIGH until further notice

I've been struggling with this and have decided to put it to all of you. Any help greatly appreciated. Also how do I install Brown Out Detection? I had it on my old computer but can't seem to find it on Arduino IDE this time around.

Thanks!

You should be able to achieve this with a simple state machine.

State No_detection: (waiting for a detection at sensor , when there is a detection, set state to In_detection)

State In_Detection: trigger camera while there is a detection at sensor then set state to No_detection. If stuck in this state for more than 10 seconds , stop camera trigger and set state to Pending_no_detection.

State Pending_no_detection : wait here until no detection at sensor. If there is a beam misalignment etc., it stays in this state until it is corrected.

Quoll:
how do I install Brown Out Detection? I had it on my old computer but can't seem to find it on Arduino IDE this time around.

Please post a link to the boards platform you're using to add support for the ATtiny85 to the Arduino IDE.

Hi pert and thanks for the reply.

I'm new to all of this, how do I find the "boards platform?" I'm sure it's this one: GitHub - damellis/attiny: ATtiny microcontroller support for the Arduino IDE

Also thanks 6v6gt,
I'm new to this so have been trying to find some sort of tutorial that I can look at for a breakdown of how this exactly works. So far all I can seem to find is some silly 4 button press tutorial on several websites which raises more questions than it answers. Would you know how to start or know a resource that may be of help?

Thanks

Post the code you have already got working and I (or someone) will help you add (at least roughly) the state machine structure to it.

There is a state machine tutorial referenced in this thread: State machine for Arduino tutorial - Exhibition / Gallery - Arduino Forum

Thanks, I have no idea what I am doing. I started working on this, and it's about as far as I can get right now...

const int SENSOR = 3;
const int BEAM = 2;
const int TRIGGER = 1;
const int ERROR_PIN = 0;

int detect = 0;   
int noDetect = 0;         
int Pending = 0;    

void setup() {

  pinMode(SENSOR, INPUT);
  pinMode(BEAM, OUTPUT);
  pinMode(TRIGGER, OUTPUT);
  pinMode(ERROR_PIN, OUTPUT);

}

void loop() {

  detect = digitalRead(SENSOR);

  if (detect != HIGH) {

    } else {

digitalWrite(BEAM, HIGH);
digitalWrite(TRIGGER, LOW);
digitalWrite(ERROR_PIN, LOW);

  detect = digitalRead(SENSOR);

  if (detect != LOW){

  } else {

digitalWrite(BEAM, LOW);
digitalWrite(TRIGGER, HIGH);
digitalWrite(ERROR_PIN, LOW);

OK. I was hoping to see the code that led to your claim of working on various camera trap systems with great success. Anyway, I have added a state machine model around the supplied code, but not tested it.
I've not attempted to handle the error led. To make its operation more transparent and to help troubleshooting, I suggest you include Serial.print statements within the cases to see what is happening.

Incidentally, using pins 0 and 1 is usually a bad idea because these usually, but depending on the board, interfere with the serial commands needed for troubleshooting.

const int SENSOR = 3;
const int BEAM = 2;
const int TRIGGER = 1;
const int ERROR_PIN = 0;

bool detect = false ;


enum class State { NO_DETECTION = 1 , IN_DETECTION = 2 , PENDING_NO_DETECTION = 3 } ;
State state ;

uint32_t enteredDetectionAtMs = 0 ;


void setup() {
  Serial.begin( 115200 ) ;

  pinMode(SENSOR, INPUT);
  pinMode(BEAM, OUTPUT);
  pinMode(TRIGGER, OUTPUT);
  pinMode(ERROR_PIN, OUTPUT);

  state = State::NO_DETECTION ;

}

void loop() {

  detect = digitalRead(SENSOR);
  if ( detect == HIGH ) digitalWrite(BEAM, HIGH);
  else digitalWrite(BEAM, LOW); ;

  Serial.print ( "state=" ) ;
  Serial.println( (int) state ) ;

  switch ( state ) {

    case State::NO_DETECTION : {
        if ( detect == HIGH ) {
          state = State::IN_DETECTION ;
          enteredDetectionAtMs = millis() ;
        }
        break ;
      }

    case State::IN_DETECTION : {

        if ( millis() - enteredDetectionAtMs > 10000UL ) {  //!!
          state = State::PENDING_NO_DETECTION ;
          digitalWrite(TRIGGER, LOW);
        }
        else if ( detect == LOW ) {
          state = State::NO_DETECTION ;
          digitalWrite(TRIGGER, LOW);
        }
        else {
          digitalWrite(TRIGGER, HIGH);
        }
        break ;
      }

    case State::PENDING_NO_DETECTION : {
        if ( detect == LOW ) {
          state = State::NO_DETECTION ;
        }
        break ;
      }
  }
  delay( 500 ) ; // only for testing to reduce serial output
}

My are all built around hardware, I've barely used any microcontrollers - instead making the circuitry do all of the work.

Thanks for the help, I'll go through your code and see what I can learn from it.

https://kaluta.zenfolio.com/?q=camera%20trap - some of my camera trapped images

https://kaluta.zenfolio.com/?q=bat - All of these bat in flight photographs were taken with a camera trap system in the wild.

It's just been bugging me that I can't get a circuit based runaway fix nutted out, so I'm looking at microcontrollers as an option.

Thanks

6v6gt,

I ran the code to see what would happen and I get the message:

'Serial' was not declared in this scope

I have done some reading and it seems that serial is irrelevant with these attiny microcontrollers anyway.

Should I declare something or not worry about it?

Thanks

The pictures are great, especially the bats in flight. Very professional.

OK. I now see you are using an ATTINY85. The comments I made about the pin numbers are not relevant to this. Not all Arduino cores for this board directly support Use of Serial.

Just to get started, comment out the lines with Serial and try it again.

This core is the one I use for ATtiny85 and has direct Serial support and is maintained by an active forum user here, DrAzzy :

You appear to be using the “damellis” core. I’m not suggesting changing this at the moment but, for debugging, using Serial print is very useful. There may also be as simple way of adding software serial to this existing core.

Quoll:
I'm new to all of this, how do I find the "boards platform?" I'm sure it's this one: GitHub - damellis/attiny: ATtiny microcontroller support for the Arduino IDE

That platform doesn't provide options to change the BOD setting via the Arduino IDE's user interface.

If you were previously able to configure the BOD setting in the Arduino IDE, you were probably using ATTinyCore. ATTinyCore adds a Tools > B.O.D. Level menu to the Arduino IDE. You just choose your desired BOD configuration from that menu, then do a Tools > Burn Bootloader to set the fuses on the ATtiny85 to use the selected configuration.

Thanks pert, I am grateful for your help. It worked and I have BOD detection enabled again. A real help.

6v6gt,

The code didn't quite work at first, a few bits and pieces to tweak, but I've ALMOST got it working properly.

The only problem now is that after 10 seconds of the beam being received, the TRIGGER output goes HIGH. Other than that it is doing its job.

Here's the code. I changed a couple of names in it so it's a bit easier.

const int SENSOR = 3;
const int BEAM = 2;
const int TRIGGER = 1;
const int ERROR_PIN = 0;

bool detect = false ;


enum class State { BEAM_TRUE = 1 , BEAM_BREAK = 2 , PENDING_NO_DETECTION = 3 } ;
State state ;

uint32_t enteredDetectionAtMs = 0 ;


void setup() {


  pinMode(SENSOR, INPUT);
  pinMode(BEAM, OUTPUT);
  pinMode(TRIGGER, OUTPUT);
  pinMode(ERROR_PIN, OUTPUT);

  state = State::BEAM_TRUE ;

}

void loop() {

  detect = digitalRead(SENSOR);
  if ( detect == HIGH ) digitalWrite(BEAM, HIGH); 
  else digitalWrite(BEAM, LOW); ;

  switch ( state ) {

    case State::BEAM_TRUE : {
        if ( detect == LOW ) {
          state = State::BEAM_BREAK ;
          enteredDetectionAtMs = millis() ;
        }
        break ;
      }

    case State::BEAM_BREAK : {

        if ( millis() - enteredDetectionAtMs > 10000UL ) {  //!!
          state = State::PENDING_NO_DETECTION ;
          digitalWrite(TRIGGER, HIGH);
        }
        else if ( detect == LOW ) {
          state = State::BEAM_BREAK ;
          digitalWrite(TRIGGER, HIGH);
        }
        else {
          digitalWrite(TRIGGER, LOW);
          
        }
        break ;
      }

    case State::PENDING_NO_DETECTION : {
        if ( detect == HIGH ) {
          state = State::BEAM_TRUE ;
          digitalWrite(ERROR_PIN, LOW);
        }
        else {
          digitalWrite(ERROR_PIN, HIGH);
          digitalWrite(TRIGGER, LOW);
        }
        break ;
      }
  }

}

It's worth mentioning that the SENSOR pin is connected to the Vcc via a 4.7K resistor and is pulled down by a 547 transistor connected to GND. The sensor output goes HIGH when the beam is broken in this case.

Thanks again.

You can make your process state out of multiple T/F bits to reduce if (a&b&c...) logic to if (value) logic.

You have the beam-break trigger that should have a task just to watch the pin and update 2 process state bits; bit 0 is now and bit 1 is previous. When the bits differ (== 1 or 2) the trigger changed state, next read the 2 bits will be the same.

You have a bit for error detected when trigger time exceeds 10 seconds. A task should run when it is set that clears the error bit if state bit 0 (trigger pin now state) is HIGH. Error bit is process state bit 2, a red flag to the camera task.

A task for the camera if process state == 2 (trigger now LOW, trigger previous HIGH, no error) to mark start time and start the camera.

A task for the camera if process state == 0 (trigger now LOW, trigger previous LOW, no error) to check for timeout then stop the camera and set the process status error bit.

A task for the camera if process state == 1 (trigger now HIGH, trigger previous LOW, no error) to stop the camera.

Tasks here are code inside of if-else or switch-case structures inside of void loop(). They all run if only to see whether or not conditions are right: a pin state, a value, time, some condition passes and the task does its work.

Do Not Use Delaying Code, it only screws up responsiveness and timing badly.

When you're not programming a wodges of RAM PC, try using byte and char variables for small numbers like pin numbers and for-next indexes instead of ints and #defines that take twice the RAM. Uno has 2048 bytes of mind-your-head-don't-bump RAM for heap and stack, make a habit to check variable type when defining as it takes time to change.

OK: I see in the mean time you have had another suggestion (I'll look at it later), but since you were nearly there, I've made a few changes to the code you modified which should now perform as required:

const int SENSOR = 3;
const int BEAM = 2;
const int TRIGGER = 1;
const int ERROR_PIN = 0;

bool detect = false ;


enum class State { BEAM_TRUE = 1 , BEAM_BREAK = 2 , PENDING_NO_DETECTION = 3 } ;
State state ;

uint32_t enteredDetectionAtMs = 0 ;


void setup() {


  pinMode(SENSOR, INPUT);
  pinMode(BEAM, OUTPUT);
  pinMode(TRIGGER, OUTPUT);
  pinMode(ERROR_PIN, OUTPUT);

  state = State::BEAM_TRUE ;

}

void loop() {

  detect = digitalRead(SENSOR);
  if ( detect == HIGH ) digitalWrite(BEAM, HIGH);
  else digitalWrite(BEAM, LOW); ;

  switch ( state ) {

    case State::BEAM_TRUE : {

        if ( detect == LOW ) {
          state = State::BEAM_BREAK ;
          enteredDetectionAtMs = millis() ;
          digitalWrite(TRIGGER, HIGH); //!!
        }
        break ;
      }

    case State::BEAM_BREAK : {

        if ( millis() - enteredDetectionAtMs > 10000UL ) {
          state = State::PENDING_NO_DETECTION ;
          digitalWrite(TRIGGER, LOW);  //!!
          digitalWrite(ERROR_PIN, HIGH);  // !!
        }
        else if ( detect == HIGH ) {   //!!
          state = State::BEAM_TRUE ;   //!!
          digitalWrite(TRIGGER, LOW); //!!
        }

        break ;
      }

    case State::PENDING_NO_DETECTION : {
        if ( detect == HIGH ) {
          state = State::BEAM_TRUE ;
          digitalWrite(ERROR_PIN, LOW);
        }

        break ;
      }
  }
}

State::PENDING_NO_DETECTION could actually be named ERROR or similar because it lands here if there is a detection for longer than 10 seconds.

Thankyou 6v6gt. It works to perfection. I would have had no idea on how to do this but it's been a bit of fun learning today.

Cheers

I'm pleased that functioned OK.
It is a relatively simple application of a state machine model. Notice how most of the activities (say switching a led on) are done once in the transitions between the states, not continuously in the new state.
For example, you are in state BEAM_BREAK and you've been there too long (more than 10 seconds) so you move to state ERROR , you switch the error led on at the point of leaving state BEAM_BREAK, not when you are in state ERROR.

GoForSmoke:
You can make your process state out of multiple T/F bits to reduce if (a&b&c...) logic to if (value) logic.

You have the beam-break trigger that should have a task just to watch the pin and update 2 process state bits; bit 0 is now and bit 1 is previous. When the bits differ (== 1 or 2) the trigger changed state, next read the 2 bits will be the same.

You have a bit for error detected when trigger time exceeds 10 seconds. A task should run when it is set that clears the error bit if state bit 0 (trigger pin now state) is HIGH. Error bit is process state bit 2, a red flag to the camera task.

A task for the camera if process state == 2 (trigger now LOW, trigger previous HIGH, no error) to mark start time and start the camera.

A task for the camera if process state == 0 (trigger now LOW, trigger previous LOW, no error) to check for timeout then stop the camera and set the process status error bit.

A task for the camera if process state == 1 (trigger now HIGH, trigger previous LOW, no error) to stop the camera.

Tasks here are code inside of if-else or switch-case structures inside of void loop(). They all run if only to see whether or not conditions are right: a pin state, a value, time, some condition passes and the task does its work.

Do Not Use Delaying Code, it only screws up responsiveness and timing badly.

When you're not programming a wodges of RAM PC, try using byte and char variables for small numbers like pin numbers and for-next indexes instead of ints and #defines that take twice the RAM. Uno has 2048 bytes of mind-your-head-don't-bump RAM for heap and stack, make a habit to check variable type when defining as it takes time to change.

You've put quite some effort into this so I'll attempt a response.

At the outset I'll say that there is always a trade off between efficiency and transparency. An optimally efficient program may be difficult, especially for a beginner, to understand and maintain.

You've put some emphasis on efficient representation of states and testing the current state with bits representing states. Of course, it is a matter of style but I prefer the following representation:

enum class State : byte { BEAM_TRUE = 1 , BEAM_BREAK = 2 , PENDING_NO_DETECTION = 3 } ;
State state ;

However, I agree that say:
const int SENSOR = 3;

would have been better as
const byte SENSOR = 3;

Anyway, if you want to continue the discussion, you have the option of showcasing your solution model by rewriting the code which the OP has confirmed as working and we can compare them.

6v6gt's code, with a couple of extra tweaks for comfort works very well. I can't wait to try it out on an exciting new set of projects with the camera - once this lockdown is over.

This compiles, I have no way to try it out.

// see if you can get it to run on an ATtiny25
// Sketch uses 1130 bytes (3%) of program storage space. Maximum is 32256 bytes.
// Global variables use 12 bytes (0%) of dynamic memory, leaving 2036 bytes for local variables. Maximum is 2048 bytes.

const byte SENSOR = 3;
const byte BEAM = 2;
const byte TRIGGER = 1;
const byte ERROR_PIN = 0;

byte state; // bits 0=trigger state,1=previous trigger state, 2=error state
// state gets used as a value that answers a whole set of T/F conditions in one compare.

word startTimeout;  // can time 65.535 second intervals, the app needs 10 seconds.
const word tenSeconds = 10000U;


void setup()
{
  pinMode(SENSOR, INPUT);
  pinMode(BEAM, OUTPUT);
  pinMode(TRIGGER, OUTPUT);
  pinMode(ERROR_PIN, OUTPUT);
}

void loop()
{
  // You have the beam-break trigger that should have a
  // task just to watch the pin and update 2 process state bits;
  // bit 0 is now and bit 1 is previous.

  state &= 0b11111101; // erases bit 1 by ANDing with 0. all other bits preserved
  state += ( state & 1 ) * 2; // copies bit 0 to bit 1, the previous read bit
  state &= 0b11111110; // clears bit 0 before read
  state += digitalRead( SENSOR ); // bits 0 and 1 updated, 4 states in 1 value, 8 w/error.

  // You have a bit for error detected when trigger time exceeds 10 seconds.
  // A task should run when it is set that clears the error bit
  // if state bit 0 (sensor pin now state) is HIGH.
  // Error bit is process state bit 2, a red flag to the camera task.

  if ( state & 4 ) // if error bit is set
  {
    if ( state & 1 ) // if sensor reads the beam it must be aligned
    {
      state &= 0x11111011; // clears bit 2, the error bit
      digitalWrite( ERROR_PIN, LOW ); 
    }
  }

  switch ( state )
  {
    // A task for the camera if process state == 0 to check for timeout
    // then stop the camera and set the process status error bit.

    case  0  :  // sensor is held LOW
      if ( (word) millis() - startTimeout >= tenSeconds ) // else keep running
      {
       digitalWrite( TRIGGER, LOW );  // stop camera, timeout
       digitalWrite( ERROR_PIN, HIGH );  // stop camera, timeout
       state |= 0b100; // sets bit 2, the error bit
      }
      break;

    // A task for the camera if process state == 1 to stop the camera.

    case  1  :  // sensor reads HIGH, was just LOW
      digitalWrite( TRIGGER, LOW );  // stop camera
      break;

    // A task for the camera if process state == 2 to mark start time and start the camera.

    case  2  :  // beam just broke
      startTimeout = (word) millis(); // cast gets the low 16 bits
      digitalWrite( TRIGGER, HIGH );  // start camera
  }
}

OK. In principle, the two solutions are not that different. They are both state machine solutions applied to specific simple problem.

Yours has smaller runtime and probably a faster execution, but at the expense of some complexity. The compound state variable requires some careful design and hairy bit manipulation and may not be so easy for future maintenance, say adding new or removing old states.

As for testing, I'm sure it would work or maybe with some minor adjustment for example, the sensor is actually normally LOW and goes HIGH during the period it detects an object, which I got wrong as well, but the OP mentioned it in post #11.

However, I would be more hesitant to hand that over to a less experienced user for final testing and tweaking.

Mine is more or less derived from a standard design process. Identify the states and name them.
Create a dedicated selectable block for each state in which the state state transitions are handled, and set the initial state. There can also be a clean 1:1 relationship between the code and a state diagram. All that, of course, at the cost of some run time efficiency (code size and maybe execution speed).