The magnetic bouncer is a unique computer interface based on the repulsive force of magnets. Magnetic attraction is all very well, but is a bit limited. Bringing two like magnetic poles together so that they repel, however, is fascinating: you can feel the magnetic field as you move them closer together and around one another. A simple way to demonstrate this effect is with ring magnets on a pole: they will sit still with the upper magnet suspended in mid-air, but disturb them and they bounce around in a most satisfying manner. We wanted to be able to capture this bouncing action and feed it into our computer to act as a controller.
This tutorial was written by Mike Cook, author of Raspberry Pi Projects for Dummies, and first appeared in The MagPi #69. Download your free copy of The MagPi magazine #69.
Magnetic Bounce: Design a magnet interface for Raspberry Pi
The problem was how to convert this movement into an electrical signal. First off, we tried using a coil between the two magnets, because the bouncing caused a variation in the overall field, and magnetic field variation close to a coil generates an electrical signal. This worked quite well, but only when the magnets were moving, and we felt a direct measure of the magnetic field would be better. Therefore, we used a miniature Hall effect sensor between the two magnets. This meant we had to use an amplifier to remove any offset and increase the voltage from the sensor.
Also, for added flexibility, we made two such systems and mounted them on top of a box. Finally, we thought it would be fun to have this unique controller drive some LEDs in a sort of ‘executive toy’, so we mounted some LEDs on a small box to sit between the two magnetic bouncers.
The hardware using in Magnetic Bounce
The schematic of the bouncer electronics is shown in Figure 1. This shows the two SS39ET linear Hall effect sensors in a dotted circle to indicate they’re on a separate board between the two magnets. The rest of the circuit is built on a small piece of stripboard and is located inside the mounting box. This circuit connects to two analogue-to-digital converter channels connected to the Raspberry Pi’s GPIO pins. If you don’t have one already then you can simply use the one described in the MIDI Drum Sequencer project in The MagPi #68.
The LED part of the project is shown in Figure 2. It consists of a 16-LED ring and a 14-LED strip of the WS2812B type. You need to amplify the 3V3 logic signal out of the Raspberry Pi to be 5 V, to drive this. Figure 2 shows two ways to do so. The lower circuit uses the 74LS14 inverter IC and was used in our Infinity Mirror project in The MagPi #43. The circuit above is a simple FET drive circuit, although you do need a FET that will switch off 3V3. Note that due to the number of LEDs, you’ll need an external power supply: if all these LEDs were on fully then it would take 1.8 amps, which is too much to take from the Pi’s 5 V line.
Magnetic Bounce: The software
The first thing we need to do is to look at signals from the sensors and to adjust the preset pots to get the right offset. The code in the bounce_test.py listing will do that and display the results like an oscilloscope. It is written in the Pygame framework and simply reads the two sensors’ values and plots the result as a graph. The result is shown in Figure 3 – note how the top trace shows a magnet configuration of two opposed floating magnets and you can see that it basically consists of two frequencies. The bottom trace has three magnets all stuck together bouncing; due to the greater magnetic field and mass of this configuration, the trace does not decay as quickly as the top trace. The two amplifiers’ offset trim pots should be adjusted so that the trace just begins to rise from its lowest point.
Applications for Magnetic Bounce
First, we made a drawing application, a sort of Etch A Sketch arrangement. The program can use the two controllers as conventional Cartesian controls – x and y – or as polar co-ordinates: angle and radius. This code can be found on our GitHub page.
Instead of printing this, we thought of using the bounce controller to drive some LEDs in a sort of executive toy style. The idea was to use an LED ring combined with an LED strip placed vertically in the middle of the ring, a sort of totem pole.
One of the controllers determines the colour of the LED and the other controls the position. So a single lit LED rotates around the ring at a speed determined by one controller’s movement and each time it passes in front of the totem strip, it adds the current colour of LED to the bottom of the strip and shifts all the others up one. When the totem strip is full, there is a display animation. If at any time you stop bouncing the controllers, the totem slowly loses lit LEDs until all LEDs are off.
The code to do this is shown in the neopixel_bounce.py listing. You need to have installed the software for Pimoroni’s Unicorn HAT to run it. You also need to be in supervisor mode, so use gksudo idle3 or equivalent.
Magnetic Bounce: Kit You'll need
- 2 × Linear Hall effect sensors
- 8 × Ring magnets
- 1 × MPC602 amplifier
- 2 × 10 kΩ trim pots
- 1 × 8-pin DIL IC socket
- 1 × A/D converter, minimum of two channels
- 12 mm diameter dowel
- Various resistors, stripboard, and wood
Optional LED display:
- 16 LED WS2812B ring
- 14 LED WS2812B strip
- Empty toothpaste pump dispenser
- Sugru mouldable glue
- Plastic box
Magnetic Bouncer: Taking it further
There are many variations you can make to Neopixel_Bounce.py. Maybe the simplest is to change the display animation, or alter the drain animation to run into the ring. You could also build a Pi Zero into the box to make it self-contained. However, the application we are itching to try is to turn this controller into a weird type of theremin, which we shall show you next issue.
Step-01: Making the sensors
Make two sensor boards, for the SS39ET Hall effect sensor and capacitor. Cut two washers from 1 mm thick foam to prevent the magnets breaking if they bang into each other. Mount this above the sensor board on the dowels.
Step-02: Prepare the amplifier stripboard
Take a piece of 14×15-hole stripboard and cut the tracks as marked. Also drill a 3 mm hole in the top-left corner for mounting to the underside of the box.
Step-03: Building the amplifier board
Wire up the circuit as shown in the diagram (right). We used a 5-way DIN socket for the connection between the bounce controller and the A/D converter mounted on the GPIO pins.
Step-04: Build the base
We used 12 mm MDF board to make a box, 300 mm by 120 mm by 40 mm high. Drill two 12 mm holes 190 mm apart and cut the two dowel rods at 165 mm long, insert, and glue. Smooth the dowel with the finest sandpaper you can get and then apply a little beeswax polish to make it glide against the magnets.
Step-05: Building the LED box
We made a ridged strip of LEDs by soldering up 14 of the type that come on their own small PCB with tinned copper wire. We drilled several 2 mm holes in the box to allow the Sugru to get a good grip when mounting the strip vertically in the centre of the circle.
Step-06: Building the LED box
A Colgate toothpaste dispenser was disassembled so we just got the tube and top. The base exactly fits the LED ring. Slip some greaseproof paper or polyester sheet on the inside of the dispenser to act as a light diffuser. Glue this onto the LED ring with a PVA-type glue that dries transparent. Fix the LED box to the main box with self-adhesive Velcro strips.
#Neopixel Bounce - controlling LEDs with the bounce interface #**** must start IDLE3 with "gksudo idle3" *****# import time , spidev from neopixel import Adafruit_NeoPixel # LED strip configuration: LED_COUNT = 30 # Number of LED pixels. LED_PIN = 18 # GPIO pin connected to the pixels (must support PWM!). LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800KHz) LED_DMA = 5 # DMA channel to use for generating signal (try 5) LED_BRIGHTNESS = 140 # Set to 0 for darkest and 255 for brightest LED_CHANNEL = 0 # PWM channel LED_INVERT = True # True if using an inverting interface ws2812 = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL) ws2812.begin() length = LED_COUNT circleLength = 16 totemLength = 14 launchPoint = 11 # LED opposite totem inBuf = [0, 0] ; lastBuf = [0, 0] ; difBuf = [0, 0] totBuf = [ (0,i,0) for i in range(0,totemLength) ] def main(): print("Neopixel Bounce - Cnt C to stop") initHardware() ; curCol =(0,120,0) wipe() # clear all LEDs place=0 ; totCount = 0 ; stoped = True while True: wipeC(0,circleLength,(0,0,0)) # blank circle LEDs if not stoped : set_led(place,curCol[0],curCol[1],curCol[2]) # current colour if place == launchPoint and difBuf[1] > 4: # right place and moving for i in range(totemLength-1,0,-1): # add to totem t = totBuf[i-1] totBuf[i] = t totBuf[0]= curCol transToTot(length) # transfer all totCount +=1 if totCount > totemLength : runEffects() totCount = 0 wipeC(circleLength,circleLength +totemLength,(0,0,0)) for i in range(0,totemLength): totBuf[i] = (0,0,0) ws2812.show() readSensor() if stoped and difBuf[0] > 4: stoped = False s = mapV(difBuf[1],0,500,0.1,0.002) curCol = setCol() time.sleep(abs(s)) if difBuf[1] > 4 : place += 1 stoped = False stopedTime = time.time() if place >= circleLength: place=0 else: # slowly decay if time.time() - stopedTime > 2.0: stoped = True if totCount > -1 : if totCount >= totemLength : totCount -=1 totBuf[totCount] = (0,0,0) stopedTime = time.time() totCount -= 1 if totCount < 0: totCount = 0 transToTot(length) # transfer all ws2812.show() def runEffects(): # display when totem fills up for j in range(0,4): # ascending LEDs wipeC(circleLength,circleLength+totemLength,(0,0,0)) ws2812.show() time.sleep(0.3) for i in range(0,14): transToTot(circleLength+i+1) ws2812.show() time.sleep(0.1) for i in range(0,10): # flash totem LEDs wipeC(circleLength,circleLength +totemLength,(0,0,0)) ws2812.show() time.sleep(0.2) transToTot(length) ws2812.show() time.sleep(0.2) def transToTot(size): # transfer totem buffer to LEDs j=0 for i in range(circleLength,size): set_led(i,totBuf[j][0],totBuf[j][1],totBuf[j][2]) j+=1 def setCol(): # HSV colour space with S = V = 1 h = abs(inBuf[0]) while(h > 255): h -= 255 if h < 85: return (int(h * 3), int(255 - h * 3), 0) elif h < 170: h -= 85 return (int(255 - h * 3), 0, int(h * 3)) else: h -= 170 return (0, int(h * 3), int(255 - h * 3)) def wipeC(s, e,col): # wipe with a colour for i in range(s,e): set_led(i,col[0],col[1],col[2]) def mapV(x, in_min, in_max, out_min, out_max): return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; def wipe(): # everything off for i in range(0,length): set_led(i,0,0,0) # black ws2812.show() def set_led(i, r, g, b): if i < LED_COUNT: ws2812.setPixelColorRGB(i, r, g, b) def readSensor(): lastBuf[0] ⁼ inBuf[0] ; lastBuf[1] = inBuf[1] for i in range(0,2): adc = spi.xfer2([1,(8+i)<<4,0]) # request channel inBuf[i] = (adc[1] & 3)<<8 | adc[2] # join two bytes together difBuf[0] = abs(inBuf[0] - lastBuf[0]) # work out changes difBuf[1] = abs(inBuf[1] - lastBuf[1]) def initHardware(): global spi,lastX,lastY,ch0Low,ch1Low spi = spidev.SpiDev() spi.open(0,0) spi.max_speed_hz=1000000 # Main program logic: if __name__ == '__main__': try: main() except: # clear up the LEDs wipe() ws2812.sh
import pygame, os, time, random import spidev pygame.init() os.environ['SDL_VIDEO_WINDOW_POS'] = 'center' pygame.display.set_caption("Bounce Test") pygame.event.set_allowed(None) pygame.event.set_allowed([pygame.KEYDOWN,pygame.QUIT]) screenWidth = 1000 ; screenHight = 230 screen = pygame.display.set_mode([screenWidth,screenHight],0,32) textHeight= 20 font = pygame.font.Font(None, textHeight) backCol = (150,255,150) # background colour inBuf = [ 0, 0] def main(): n=0 loadResource() while(1): time.sleep(0.001) checkForEvent() readSensor() display(n) n +=1 if n > screenWidth: n=0 lastX = -1; lastY = 0 pygame.draw.rect( screen,backCol,(0,0,screenWidth,screenHight+2),0) def display(n): global lastX,lastY col = (180,64,0) y0 = ch0Low - inBuf[0]//9 y1 = ch1Low - inBuf[1]//9 if n != 0: pygame.draw.line(screen,col,(lastX ,lastY[0] ), (n ,y0 ),2) pygame.draw.line(screen,(0,64,180), (lastX ,lastY[1] ), (n ,y1 ),2) lastX = n lastY[0] = y0 ; lastY[1] = y1 pygame.display.update() def readSensor(): for i in range(0,2): adc = spi.xfer2([1,(8+i)<<4,0]) # request channel inBuf[i] = (adc[1] & 3)<<8 | adc[2] # join two bytes together def loadResource(): global spi,lastX,lastY,ch0Low,ch1Low spi = spidev.SpiDev() spi.open(0,0) spi.max_speed_hz=1000000 pygame.draw.rect(screen,backCol,(0,0,screenWidth, screenHight),0) lastX = -1 ; lastY = [0,0] ch0Low = screenHight/2 -2 ch1Low = screenHight -2 def terminate(): # close down the program pygame.quit() # close pygame os._exit(1) def checkForEvent(): # see if we need to quit event = pygame.event.poll() if event.type == pygame.QUIT : terminate() if event.type == pygame.KEYDOWN : if event.key == pygame.K_ESCAPE : terminate() if event.key == pygame.K_d : # screen dump os.system("scrot") # Main program logic: if __name__ == '__main__': main()