Skip to content

Virtual Wheel of Items with a Physical Knob using Servos Motors

james tichenor edited this page Jul 2, 2018 · 1 revision

Project Description

Immersive Experience, Week 1: Federico, Fahmida, Chaeri

This tutorial will take you through connecting two servo motors to a Servo Hat that sends information via Spacebrew to Unity.

What we set out to do was to see if we could control two servo motors using the Servo Hat (attached to a Raspberry Pi) to send information to Unity via Spacebrew, so that a physical object could control a virtual output.

Once we were able to get the servo motor readings on the Raspberry Pi, we were able to control one servo motor digitally by moving the other servo motor manually. That was super cool. We were then inspired to think about how both servo motors could play a role for a virtual experience. What we decided to do, and what this tutorial will walk through, is to use one servo motor (which will function as a physical knob) to influence the movement of the second servo motor (which will function as an indicator) to dial through a series of virtual items (scrumptious meals).

Hardware Needed

Software Needed

  • Python
  • Unity
  • Spacebrew

Additional resources

Step 1: Make a diagram

Here's what ours looked like.

This helped us understand the ways in which pieces of information would need to communicate to each other. It also helped us chunk the project so that we could work on one small piece at a time.

Step 2: Connect one servo motor to the Servo Hat & Raspberry Pi.

We soldered female headers to the Servo Hat so that it could be mounted on top of the Raspberry Pi, male headers to plug the servos.

Lastly we soldered more female headers on the hat to access the rest of the GPIO: this is necessary to work with the ADC later.

Step 3: Now connect two servo motors to the Servo Hat & Raspberry Pi.

# Simple demo of of the PCA9685 PWM servo/LED controller library.
# Move 2 servos

from __future__ import division
import time

# Import the PCA9685 module.
import Adafruit_PCA9685


# Uncomment to enable debug output.
#import logging
#logging.basicConfig(level=logging.DEBUG)

# Initialise the PCA9685 using the default address (0x40).
pwm = Adafruit_PCA9685.PCA9685()

# Alternatively specify a different address and/or bus:
#pwm = Adafruit_PCA9685.PCA9685(address=0x41, busnum=2)

# Configure min and max servo pulse lengths
servo_min = 150  # Min pulse length out of 4096
servo_max = 600  # Max pulse length out of 4096

# Helper function to make setting a servo pulse width simpler.
def set_servo_pulse(channel, pulse):
    pulse_length = 1000000    # 1,000,000 us per second
    pulse_length //= 60       # 60 Hz
    print('{0}us per period'.format(pulse_length))
    pulse_length //= 4096     # 12 bits of resolution
    print('{0}us per bit'.format(pulse_length))
    pulse *= 1000
    pulse //= pulse_length
    pwm.set_pwm(channel, 0, pulse)

# Set frequency to 60hz, good for servos.
pwm.set_pwm_freq(60)

print('Moving servo on channel 0 and 4, press Ctrl-C to quit...')
while True:
    # Move servo on channel O between extremes.
    pwm.set_pwm(0, 0, servo_min)
    pwm.set_pwm(4, 0, servo_min)
    time.sleep(1)
    pwm.set_pwm(0, 0, servo_max)
    pwm.set_pwm(4, 0, servo_max) 
    time.sleep(1)

Step 4: Getting the readings from the servo motors in Raspberry Pi using an ADC

Here is how we connected the MCP3008 to our circuit.

What our readings looked like:

Step 5: Controlling one servo motor digitally by moving the other servo motor manually

Step 6: Sending the servo motor data to Spacebrew

# RaspberryPi code to use a servo as input and broadcast/receive data via SpaceBrew
# By moving one servo, the other will move to the same position.
# Be sure to connect the Publisher and Subscriber in SpaceBrew

from __future__ import division
import sys
import time
import subprocess
from pySpacebrew.spacebrew import Spacebrew

# Import the PCA9685 module.
import Adafruit_PCA9685

import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM)
DEBUG = 0

# read SPI data from MCP3008 chip, 8 possible adc's (0 thru 7)
def readadc(adcnum, clockpin, mosipin, misopin, cspin):
        if ((adcnum > 7) or (adcnum < 0)):
                return -1
        GPIO.output(cspin, True)

        GPIO.output(clockpin, False)  # start clock low
        GPIO.output(cspin, False)     # bring CS low

        commandout = adcnum
        commandout |= 0x18  # start bit + single-ended bit
        commandout <<= 3    # we only need to send 5 bits here
        for i in range(5):
                if (commandout & 0x80):
                        GPIO.output(mosipin, True)
                else:
                        GPIO.output(mosipin, False)
                commandout <<= 1
                GPIO.output(clockpin, True)
                GPIO.output(clockpin, False)

        adcout = 0
        # read in one empty bit, one null bit and 10 ADC bits
        for i in range(12):
                GPIO.output(clockpin, True)
                GPIO.output(clockpin, False)
                adcout <<= 1
                if (GPIO.input(misopin)):
                        adcout |= 0x1

        GPIO.output(cspin, True)
        
        adcout >>= 1       # first bit is 'null' so drop it
        return adcout

# change these as desired - they're the pins connected from the
# SPI port on the ADC to the Cobbler
SPICLK = 18
SPIMISO = 23
SPIMOSI = 24
SPICS = 25

# set up the SPI interface pins
GPIO.setup(SPIMOSI, GPIO.OUT)
GPIO.setup(SPIMISO, GPIO.IN)
GPIO.setup(SPICLK, GPIO.OUT)
GPIO.setup(SPICS, GPIO.OUT)

# 10k trim pot connected to adc #0
potentiometer_adc = 0;

last_read = 0       # this keeps track of the last potentiometer value
tolerance = 5       # to keep from being jittery we'll only change
                    # volume when the pot has moved more than 5 'counts'

# Initialise the PCA9685 using the default address (0x40).
pwm = Adafruit_PCA9685.PCA9685()

# Alternatively specify a different address and/or bus:
#pwm = Adafruit_PCA9685.PCA9685(address=0x41, busnum=2)

# Configure min and max servo pulse lengths
servo_min = 118  # Min pulse length out of 4096
servo_max = 600  # Max pulse length out of 4096

#set servo potentiometers minimum and maximum reading values 
pot_min = 37
pot_max = 772
#set servo potentiometers minimum and maximum values to be published
degree_min = 0
degree_max = 180

# Helper function to make setting a servo pulse width simpler.
def set_servo_pulse(channel, pulse):
    pulse_length = 1000000    # 1,000,000 us per second
    pulse_length //= 60       # 60 Hz
    print('{0}us per period'.format(pulse_length))
    pulse_length //= 4096     # 12 bits of resolution
    print('{0}us per bit'.format(pulse_length))
    pulse *= 1000
    pulse //= pulse_length
    pwm.set_pwm(channel, 0, pulse)

# Set frequency to 60hz, good for servos.
pwm.set_pwm_freq(60)

def translate(value, leftMin, leftMax,  rightMin, rightMax):
    # Figure out how 'wide' each range is
    leftSpan = leftMax - leftMin
    rightSpan = rightMax - rightMin

    # Convert the left range into a 0-1 range (float)
    valueScaled = float(value - leftMin) / float(leftSpan)

    # Convert the 0-1 range into a value in the right range.
    return rightMin + (valueScaled * rightSpan)

# Setting up Spacebrew
brew = Spacebrew("Servo_Carousel_RPI", description="Move a servo and let the other one follow",  server="sandbox.spacebrew.cc", port=9000)
# publish servo 1 position - range
brew.addPublisher("position", "range");
# listen servo 2 position - range
brew.addSubscriber("rotation", "range")
#Spacebrew Server Status
connected = False

#CHECK_FREQ = 2 # check mail every 2 seconds

def handleRotation(value):
    if DEBUG:
        print("Received: "+str(value))
    servo2pos = translate(potValue, 0, 180, servo_min, servo_max)
    servo2pos = round(servo2pos)
    servo2pos = int(servo2pos)
    pwm.set_pwm(4, 0, servo2pos)

brew.subscribe("rotation", handleRotation)

try:
    brew.start()
    print('Servo carousel is taking off...CTRL+C to exit')
    
    while True:
        if (connected == True):

            # we'll assume that the pot didn't move
             trim_pot_changed = False

            # read the analog pin
             trim_pot = readadc(potentiometer_adc, SPICLK, SPIMOSI, SPIMISO, SPICS)
            # how much has it changed since the last read?
             pot_adjust = abs(trim_pot - last_read)
            #print values to debug
             if DEBUG:
                     print "trim_pot:", trim_pot
                     print "pot_adjust:", pot_adjust
                     print "last_read", last_read
            #adjust tolerance
             if ( pot_adjust > tolerance ):
                    trim_pot_changed = True
            #print values for debug
             if DEBUG:
                     print "trim_pot_changed", trim_pot_changed
            #if the servo has been moved
             if ( trim_pot_changed ):
                    #convert the readings from the servo into a value between 0 and 180 degrees
                     potValue = translate(trim_pot, pot_min, pot_max, degree_min, degree_max )
                     potValue = round(potValue)          # round out decimal value
                     potValue = int(potValue)            # cast as integer
                    #publish to spacebrew
                     brew.publish("position", potValue)
                     # servo2pos = translate(potValue, 0, 180, servo_min, servo_max)
                     # servo2pos = round(servo2pos)
                     # servo2pos = int(servo2pos)
                     # pwm.set_pwm(4, 0, servo2pos)

                     if DEBUG:
                             print "potValue", potValue, "degrees"
                             #print "tri_pot_changed", potValue

                     # save the potentiometer reading for the next loop
                     last_read = trim_pot

        connected = True
       # hang out and do nothing for a half second
        time.sleep(0.2)
finally:
    GPIO.cleanup()
    brew.stop()

Step 7: Spatially organizing all the elements so it looks like dial

Decide how many elements you’d like to have in your scene, and use Google Poly to find the 3d models that you’d like to use. which elements you’d like to have in your scene. Then rotate each item so that they are spatially laid out in the order that you’d like.

Step 8: Set up a subscriber to be able to get the data range from raspberry pi through spacebrew

Add an event listener in the setup function of SpacebrewEvents.cs script:

sbClient.addEventListener (this.gameObject, "foodPointer"); 

Make individual scripts for each item that will animate. Then you’ll have to update the subscriber events with the controls for the specific script. Inside the function OnSpacebrewEvent get the data from Spacebrew and parse the data into a float. The float will then be sent to a custom function into each object’s specific script. We use the same function for all the objects.

if (_msg.name == "foodPointer") {
			//print ("Message Received: "+_msg.value);
			
			pointerRotation = float.Parse(_msg.value, System.Globalization.CultureInfo.InvariantCulture.NumberFormat);
			
                        // Access the script of the Hotdogs on the left
                        // First point at the Object
			GameObject hotdog_0 = GameObject.Find("BaseBoARd/YourObjectsGoHere/hotdog_0");
			// then point at the script 
                        scaleHotdog_0 scaleHD_0 = hotdog_0.GetComponent <scaleHotdog_0> ();
                        // Lastly pass the float to the controlScale function of the script
			scaleHD_0.controlScale(pointerRotation);

                        // Access the script of the Hotdogs on the right
			GameObject hotdog_180 = GameObject.Find("BaseBoARd/YourObjectsGoHere/hotdog_180");
			ScaleHotdog_180 scaleHD_180 = hotdog_180.GetComponent <ScaleHotdog_180> ();
			scaleHD_180.controlScale(pointerRotation);
                        
                        // Access the script of the burrito
			GameObject burrito = GameObject.Find("BaseBoARd/YourObjectsGoHere/burrito");
			BurritoScale scaleBurrito = burrito.GetComponent <BurritoScale> ();
			scaleBurrito.controlScale(pointerRotation);

			GameObject taco = GameObject.Find("BaseBoARd/YourObjectsGoHere/taco");
			TacoScale scaleTaco = taco.GetComponent <TacoScale> ();
			scaleTaco.controlScale(pointerRotation);

			GameObject burger = GameObject.Find("BaseBoARd/YourObjectsGoHere/burger");
			BurgerScale scaleBurger = burger.GetComponent <BurgerScale> ();
			scaleBurger.controlScale(pointerRotation);

			GameObject sandwich = GameObject.Find("BaseBoARd/YourObjectsGoHere/sandwich");
			SandwichScale scaleSandwich = sandwich.GetComponent <SandwichScale> ();
			scaleSandwich.controlScale(pointerRotation);
		}

Each 3D model has a script to change the scale. That's the script with the function we will call from the OnSpacebrewEvent.

All the model have the same script with different ranges in the Map functions:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SandwichScale : MonoBehaviour {

	float maxScale = 80f; 
	float minScale = 0f;

	float currScale;

	// Use this for initialization
	void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
		
	}

	static float Map (float from1, float to1, float from2, float to2, float value) {
    	return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
	}
	public void controlScale(float _val){

		if (_val > 130f && _val < 140f ) {
			transform.localScale = new Vector3 (maxScale, maxScale, maxScale);
		} else if (_val > 120f && _val <= 130f) {
			currScale = Map(120f, 130f, minScale, maxScale, _val);
			print("Current scale is: "+currScale);
			transform.localScale = new Vector3  (currScale, currScale, currScale);
		} else if (_val > 140f && _val <= 150f) {
			currScale = Map(140f, 150f, maxScale, minScale, _val);
			print("Current scale is: "+currScale);
			transform.localScale = new Vector3  (currScale, currScale, currScale);
		} else if (_val < 120f || _val > 150f ) {
			transform.localScale = new Vector3 (minScale, minScale, minScale);
		}
	}
}

The first and last 3d Objects have a slightly different code.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;



public class scaleHotdog_0 : MonoBehaviour {

	float maxScale = 70f; 
	float minScale = 0f;

	float currScale;
	public Renderer rend;

	// Use this for initialization
	void Start () {
		rend = GetComponent<Renderer>();
	}
	
	// Update is called once per frame
	void Update () {
		
	}

	static float Map (float from1, float to1, float from2, float to2, float value) {
    	return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
	}
	public void controlScale(float _val){

		if (_val < 20f) {
			transform.localScale = new Vector3 (maxScale, maxScale, maxScale);
		} else if (_val >= 20f && _val <= 30f) {
			currScale = Map(20f, 30f, maxScale, minScale, _val);
			print("Current scale is: "+currScale);
			transform.localScale = new Vector3  (currScale, currScale, currScale);
		} else if (_val > 30) {
			transform.localScale = new Vector3  (0f, 0f, 0f);
		}
	}
}

Step 9: Create a physical container

We fit all of our electronics inside a small box made of MDF.

Lining up our food items and the physical indicator with the fiducial image.

What we came up with!

Clone this wiki locally