How To Use A Break Beam Sensor & How To Make Your Own Light Gate On The Cheap

How To Use A Break Beam Sensor & How To Make Your Own Light Gate On The Cheap

So many uses for these cheap and versatile break beam sensors, let us show you what you need, how to use them, & how to code them for a variety of projects. You get bonus project and learn about polling and interrupt requests (IRQ).

We needed a light gate to perform various experiments for our Youtube Channel, website, and learning journey.

The cheapest light gates cost well over £100 and then you needed to buy a data logger too. We ended up with something we think is more versatile for a few pounds and had a heap of fun & learning making it.

We'll show you how to make a very cheap version, then add some bonus functionality by adding a button and screen (still very cheap). We will also show you how to recycle this to make other projects.

Learn how to make a security beam alarm, counter, speed trap, & measure acceleration.

These few lines of micro python code is all you need to get started. This will report back when the beam is broken or restored.

import machine 
import utime as time
from machine import Pin

beam_pin = Pin(15, Pin.IN, Pin.PULL_UP)
old_beam_value = beam_pin.value()

while True:
    if old_beam_value != beam_pin.value():
        old_beam_value = not old_beam_value
        print(old_beam_value)
    time.sleep(0.1)
    

You can build on this to do any action you can imagine when the beam is either broken or restored.

For example here is code which we used to count coins dropped into a money box. You could use it as an electronic turn style to count people or vehicles.

import machine 
import utime as time
from machine import Pin

beam_pin = Pin(15, Pin.IN, Pin.PULL_UP)
old_beam_value = beam_pin.value()
count = 0

while True:
    if old_beam_value != beam_pin.value():
        old_beam_value = not old_beam_value
#         print(old_beam_value)
        if old_beam_value == True:
            count = count + 1
            print(count)
    time.sleep(0.01)   

All we did was to set a variable to count, and increase that every time the beam was restored after being broken.

We also had to decrease the sleep time so we didn't miss any coins being dropped if they passed through the beam too quickly.

Instead of polling and hoping we don't miss the beam being broken or restored we can use an interrupt request or IRQ.

Soon we will be dealing with events that take a fraction of a second, so the code below does the same as our 1st simple code but uses an IRQ instead of polling.

import machine 
import utime as time
from machine import Pin

beam_pin = Pin(15, Pin.IN, Pin.PULL_UP)
old_beam_value = beam_pin.value()

def beam_change(pin):
    broken = pin.value() == 0
    if not broken:
        print('TRUE')
    else:
        print("FALSE")

beam_pin.irq(handler = beam_change, trigger = Pin.IRQ_FALLING | Pin.IRQ_RISING)

while True:
    time.sleep(0.1)

To set up an IRQ you run the irq method on a pin and give it a handler and trigger. The trigger is what you want to listen for on the pin to set off your code. We are listening for a rising or falling edge, so if the pin changes from low to high or vice versa.

The handler is the function you want to run when the event you are listening for occurs. In our case it's our beam_change function.

Here we are just printing true or false depending on if the beam is broken or restored like our first simple code version.

We now add code to remember when the beam was broken and calculate how long it was broken for when it is eventually restored.

import machine 
import utime as time
from machine import Pin

beam_pin = Pin(15, Pin.IN, Pin.PULL_UP)
old_beam_value = beam_pin.value()
last_time = time.ticks_us()

def beam_change(pin):
    global last_time
    broken = pin.value() == 0
    if not broken:
        elapsed_time = time.ticks_diff(time.ticks_us(), last_time)
        elapsed_time = elapsed_time / 1_000_000
        print('beam restored', elapsed_time)
    else:
        print("beam broken")
        last_time = time.ticks_us()

beam_pin.irq(handler = beam_change, trigger = Pin.IRQ_FALLING | Pin.IRQ_RISING)

while True:
    time.sleep(0.1)

time.ticks_us() gives us a number and next time we check again with time.ticks_us() it will give us the same number but with the number of microseconds since we last checked added to it.

time.ticks_diff(time_1,time_2) will give the difference between 2 points in time so we can measure elapsed time.

It's amazing we can measure millionths of a second with such cheap equipment. When Isaac Newton was born (1689) the pendulum clock had only been around for 23 years (1656). Before this there was little point in clocks showing seconds or even minutes due to their lack of accuracy.

The code is just remembering the point in time when the beam is broken and when it's restored using the new time to calculate the duration it was broken. We are dividing by 1,000,000 because we want to work in seconds not microseconds.

Now it's trivial to measure speed/velocity which is just distance divided by time. We attach a piece of card to the object we want to measure the speed of. Our previous code already measures time so we just add in a variable we set the the length of the card and it's done.

import machine 
import utime as time
from machine import Pin

beam_pin = Pin(15, Pin.IN, Pin.PULL_UP)
old_beam_value = beam_pin.value()
last_time = time.ticks_us()
velocity = 0
distance = 0.035

def beam_change(pin):
    global last_time, velocity, distance
    broken = pin.value() == 0
    if not broken:
        elapsed_time = time.ticks_diff(time.ticks_us(), last_time)
        elapsed_time = elapsed_time / 1_000_000
        velocity = distance/elapsed_time 
        print('beam restored', velocity)
    else:
        print("beam broken")
        last_time = time.ticks_us()

beam_pin.irq(handler = beam_change, trigger = Pin.IRQ_FALLING | Pin.IRQ_RISING)

while True:
    time.sleep(0.1)

The formula for average acceleration is the change in speed/velocity divided by the time taken for that change.

We could use 2 light gates and have the card pass through both, but it's simpler to use card with 2 tabs which will break the beam twice.

The code below includes a reset button to take multiple measurements, and an SSD1306 screen. You could replace the screen setup and update_screen function to output the results to any kind of screen you choose. Or you could save the results to the device or any number of alternative options to suit your needs.

import machine 
import utime as time
from machine import Pin
import ssd1306

clockPin = 5
dataPin = 4
bus = 0

i2c = machine.I2C(bus,sda=machine.Pin(dataPin),scl=machine.Pin(clockPin))
display = ssd1306.SSD1306_I2C(128,64,i2c)
display.fill_rect(0,0,128,64,0)
display.fill_rect(10,40,60,12,0)
display.show()

reset_pin = Pin(1, Pin.IN, Pin.PULL_UP)
beam_pin = Pin(15, Pin.IN, Pin.PULL_UP)
old_beam_value = beam_pin.value()
last_time = time.ticks_us()
distance = 0.035
MODE_WAITING = 0
MODE_PASSED_B = 1
MODE_COMPLETE = 2
velocity_1 = 0.0
velocity_2 = 0.0
time_at_b = 0.0
time_at_d = 0.0
acceleration = 0.0

mode = MODE_WAITING

def beam_change(pin):
    global last_time, distance, velocity_1, velocity_2, acceleration, mode, time_at_b, time_at_d
    broken = pin.value() == 0
    if not broken:
        elapsed_time = time.ticks_diff(time.ticks_us(), last_time)
        elapsed_time = elapsed_time / 1_000_000
 
        print('beam restoreded', elapsed_time)
        if mode == MODE_WAITING:
            time_at_b = time.ticks_us()
            velocity_1 = distance / elapsed_time
            mode = MODE_PASSED_B

        elif mode == MODE_PASSED_B:
            time_at_d = time.ticks_us()
            velocity_2 = distance / elapsed_time
            acceleration_time = time.ticks_diff(time_at_d, time_at_b) / 1_000_000
            mode = MODE_COMPLETE
            print('vel 2: ', velocity_2)
            acceleration = (velocity_2 - velocity_1) / acceleration_time
            print('acc: ', acceleration)
            update_screen()
            
    else:
        last_time = time.ticks_us()
        print('beam broken')
        
def reset():
    global last_time, distance, velocity_1, velocity_2, acceleration, mode, time_at_b, time_at_d
    velocity_1 = 0.0
    velocity_2 = 0.0
    time_at_b = 0.0
    time_at_d = 0.0
    acceleration = 0.0
    print('RESET')
    time.sleep(1)
    mode = MODE_WAITING
    display.fill_rect(0,0,128,64,0)
    display.show()
    display.text('RESET :)',30,30)
    display.show()
    time.sleep(1)
    update_screen()
    
def update_screen():
    display.fill_rect(0,0,128,64,0)
    display.text('vel 1: ' + str(velocity_1),0,16)
    display.text('vel 2: ' + str(velocity_2),0,36)
    display.text('acc: ' + str(acceleration),0,54)
    display.show()

beam_pin.irq(handler = beam_change, trigger = Pin.IRQ_FALLING | Pin.IRQ_RISING)

while True:
    if reset_pin.value() == 0:
        reset()
    time.sleep(0.1)

All the code is also on github https://github.com/gurgleapps/light-gate

It took a long time to make this video, at the end we wanted to add the skateboard acceleration clip and in our haste didn't sanity check the results. Jon quite rightly points out there was an error in the measurement. Best to take multiple measurements, and it wasn't very scientific holding the device in my hand.