-
Notifications
You must be signed in to change notification settings - Fork 11
Virtual Wheel of Items with a Physical Knob using Servos Motors
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).
- 2 servo motors
- Raspberry Pi 3 Model B+
- 5v 2A Power Plug
- Adafruit 16-Channel PWM / Servo HAT for Raspberry Pi
- MCP3008 - Analog to Digital Converter for Raspberry Pi (more info about this here: https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/overview)
- We soldered few of these female headers to the Servo Hat for easier wiring
- Python
- Unity
- Spacebrew
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.
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.
# 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)
Here is how we connected the MCP3008 to our circuit.
What our readings looked like:
# 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()
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.
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);
}
}
}
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!