DIY Scenery/Automation Tracking

Do you want to track scenery in your next production? Do you want to spit that information out via open sound control(OSC) to a media server? Do you want to do that for under $100?!

This tutorial/write up will explain how. This can be used in conjunction with Touchdesigner, Max/Msp, Isadora, even Qlab if you’re handy with OSC.

The things you need are:
-Raspberry Pi, I recommend the new ones(B+ or greater)
-GPIO Breakout for your Raspberry Pi
-Quadrature Incremental Rotary Encoder (~200ppr should be OK for the PI)
-A Network HUB
-Appropriate cabling

First things first, follow this tutorial to get the OSC Modules on your Pi:
http://www.design.ianshelanskey.com/technology/raspberry-pi-and-osc-part-1/

Did you do that? good.

Now go look at this one to find out how to set up an OSC Client on your Raspberry Pi:
http://www.design.ianshelanskey.com/technology/raspberry-pi-and-osc-part-2/

Bam, Half done.

Next thing you should do is acquaint yourself with the GPIO. Here is a good tutorial for that. http://makezine.com/projects/tutorial-raspberry-pi-gpio-pins-and-python/

 

Let’s talk a bit about the most enthralling of topics, Quadrature Incremental Encoders!

Quadrature is a fancy way of saying that signals are 90 degrees out of phase from each other. As demonstrated on this chart:

Quadrature_Diagram.svg

Believe it or not, by measuring these signals we can tell if something is going forwards or backwards and how fast that thing is moving. The logic works like this:

-Measure A and B, Remember what A is.
-If A or B changes, Measure again.
-If B and Old A = (1,0) OR (0,1), you’re going forward.
-If B and Old A = (1,1) OR (0,0), you’re going backward.

Go ahead and stare at that diagram for a while until it makes sense, I know I did.

This brings us to the how we are actually going to do this. Every time the pins changes it calls an interrupt in the program which checks if we are moving forward or backward and adds or subtracts the total count respectively. This count gets fed into an OSC message and sent to your server where you can do fancy math to project on your moving scenery. Since this whole process happens relative to where the encoder starts reading its important to have a way of calibrating(i.e resetting the total count, or just restarting the program.)

Cool? good. Lets start programming!

Here is the whole code first and I will explain what going on in smaller chunks afterwards:

import OSC
import RPi.GPIO as GPIO
import time, threading

send_address = ‘192.168.0.11’ ,10000
receive_address = ‘192.168.0.17’ ,9000

panelNum = 1
pinA = 23
pinB = 24

panel = OSC.OSCClient()
s = OSC.OSCServer(receive_address)
panel.connect(send_address)
s.addDefaultHandlers()

GPIO.setmode(GPIO.BCM)
GPIO.setup(23, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
GPIO.setup(24, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)

error = 0
counts = 0

Encoder_A,Encoder_B = GPIO.input(pinA),GPIO.input(pinB)
Encoder_B_old = GPIO.input(pinB)

def reset(addr,tags,stuff,source):
global counts
global error
counts = 0
error = 0

def encodercount(term):
global counts
global Encoder_A
global Encoder_B
global Encoder_B_old
global error

     Encoder_A,Encoder_B = GPIO.input(pinA),GPIO.input(pinB)

if ((Encoder_A,Encoder_B_old) == (1,0)) or ((Encoder_A,Encoder_B_old) == (0,1)):
counts += 1

elif ((Encoder_A,Encoder_B_old) == (1,1)) or ((Encoder_A,Encoder_B_old) == (0,0)):
counts -= 1

     else:
error += 1

     Encoder_B_old = Encoder_B

GPIO.add_event_detect(pinA, GPIO.BOTH, callback = encodercount)
GPIO.add_event_detect(pinB, GPIO.BOTH, callback = encodercount)
s.addMsgHandler(“/reset”, reset)
st = threading.Thread(target = s.serve_forever)
st.start()

while True:
msg = OSC.OSCMessage()
msg.setAddress(“/panel/”+str(panelNum))
msg.append(counts)
panel.send(msg)
time.sleep(.1)

First, let’s import the module we will need.

import OSC
import RPi.GPIO as GPIO
import time, threading

The OSC, GPIO, and time modules should look familiar from the links above. I imported threading because I wanted to have a way to reset the count of my encoders(more about this later).

Next up:

send_address = ‘192.168.0.11’ ,10000
receive_address = ‘192.168.0.17’ ,9000

pinA = 23
pinB = 24

Setting the IP addresses and ports to send OSC across and assigning names to pin numbers.
send_address should be the IP of the machine you want to send OSC to.
recieve_address is the Pi’s IP.

panel = OSC.OSCClient()
s = OSC.OSCServer(receive_address)
panel.connect(send_address)
s.addDefaultHandlers()

Creating a new OSC Client called ‘panel’. Opening an OSC server on the pi  called ‘s’ to receive a calibration command. Then connecting to the Media server via ‘panel.connect(send_address)’. The OSC server needs the next line ‘s.addDefaultHandlers()’ for any OSC address it receives that it doesn’t have a handler for.

GPIO.setmode(GPIO.BCM)
GPIO.setup(23, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
GPIO.setup(24, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)

Here I am setting the GPIO up. The first line is telling the PI how I am calling the pins, the next two line are assigning the pins as inputs and using “pull_up_down = GPIO.PUD_DOWN”. This command assigns the pin as “normally open”, meaning that it will return a 1 if the sensor returns a 1. As opposed to “GPIO.PUD_UP” which is “normally closed”, and will return 1 if the sensor returns a 0.

error = 0
counts = 0

Error is more for debugging and analyzing problems, count is where the magic happens. This is the variable that gets sent to the server.

Encoder_A,Encoder_B = GPIO.input(pinA),GPIO.input(pinB)
Encoder_B_old = GPIO.input(pinB)

Here we go. Setting these variables are what drives the whole shebang. This is measuring the first state of our encoder.

def reset(addr,tags,stuff,source):
global counts
global error
counts = 0
error = 0

Our first callback function. This function is attached to our OSC server handler for the message ‘/reset’. Anytime the Pi receives an OSC message with the address ‘/reset’ this function will run. This is assigned further down in the program.

def encodercount(term):
global counts
global Encoder_A
global Encoder_B
global Encoder_B_old
global error

       Encoder_A,Encoder_B = GPIO.input(pinA),GPIO.input(pinB)

if ((Encoder_A,Encoder_B_old) == (1,0)) or ((Encoder_A,Encoder_B_old) == (0,1)):
counts += 1

elif ((Encoder_A,Encoder_B_old) == (1,1)) or ((Encoder_A,Encoder_B_old) == (0,0)):
counts -= 1

       else:
error += 1

       Encoder_B_old = Encoder_B

Our next callback function. This is called anytime either pinA or pinB change. This is the logic from before done as a series of ‘if’ statements. The important part here is that the variable ‘Encoder_B’ gets set to ‘Encoder_B_old’.

GPIO.add_event_detect(pinA, GPIO.BOTH, callback = encodercount)
GPIO.add_event_detect(pinB, GPIO.BOTH, callback = encodercount)

This chunk creates the interrupts that call the callback functions we just created. ‘GPIO.add_event_detect(pinA, GPIO.BOTH, callback = encodercount)’ listens on pinA for a rising or falling edge of the signal. Once it hears one, it calls the ‘encodercount’ function from before. Here is something to take note of:

THE ENCODER DOESN’T HAVE A CLOCK SPEED. IT DOES NOT CARE THAT THE PI CAN’T KEEP UP WITH HOW FAST IT IS SENDING INFORMATION, IT WILL KEEP SENDING INFORMATION.

Meaning, if the Pi is still processing from the last interrupt and it gets a new signal, it will drop that frame of information. THIS IS BAD.

For instance, let’s say you’re moving forward and received (0,1) on your last read. A = 0 B = 1. If you drop the very next frame(1,1), and read the one after that which is (1,0), your ‘if’ statement will check (1,0) against your old A which would end up being (0,0). The count would reverse in that frame.

How can you prevent this from happening you ask?! There are a few ways.

  1. Buy an encoder with less pulses per revolution. If you buy one that sends 10000 pulses per revolution, there is a good chance the Pi won’t be able to keep up with it no matter what. It’s OK to sacrifice precision for accuracy in this circumstance. You might not know when that object moves 0.0001″ but at least youre not dropping counts.
  2. Gear the encoder up. If you attach a larger wheel to the encoder, it will slow down the amount of pulses you are getting at the Pi.
  3. Get a faster processor. The Pi might not cut it. An alternative would be using a PCIe Digital Input/Output card with a computer with a faster processor than the Pi, so pretty much anything.

 

s.addMsgHandler(“/reset”, reset)
st = threading.Thread(target = s.serve_forever)
st.start()

 

This is the bit for the OSC server on the PI, listening for ‘/reset’ and starting the separate thread.

while True:
msg = OSC.OSCMessage()
msg.setAddress(“/p/”+str(panelNum))
msg.append(counts)
panel.send(msg)
time.sleep(.1)

At last, the end of the code. This runs a loop that sends and OSC message with the current count back to the server every 0.1 seconds. This is separated from the callback functions for efficiency’s sake. If I were to try to send an OSC message each time the count increased the Pi would never be able to keep up. At the server I filter this number to keep it smooth as the panel moves across stage. The line – msg.setAddress(“/p/”+str(panelNum)) – sets the address of the osc command that arrives back at the server. You can change this to any name that you want.

In Touchdesigner, the setup is pretty simple to use the information. Make an OSC Chop that looks at port 10000 and the address ‘/p/*’. The ‘*’ means that it will show anything that starts with ‘/p/’. I send this to a filter to smooth out the stutter of the time.sleep(.1) send time. I can then take that to a masking widget, or into a Geometry COMP to use it to generate content.

I talked with a rep from Figure 53(Qlab) about animating masks or warps using OSC. They said that they were interested in implementing it in future releases. Right now, you can animate content transformations via OSC, which might be enough for what your project needs.