Magnetic Bounce: a magnet interface for Raspberry Pi

By Lucy Hattersley. Posted

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.

 Figure 1 Schematic of the magnetic bounce controller

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.

Construction details for both circuits are in the step-by-step guide, with extra photos on the GitHub page.

 Figure 2 Schematic of the LED display with two possible methods

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.

 Figure 3 Top trace, left controller; bottom trace, right controller

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.

How-To-1--Magnetic-Bouncer

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.

How-To-2--Magnetic-Bouncer

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.

How-To-3--Magnetic-Bouncer

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.

How-To-4--Magnetic-Bouncer

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.

How-To-5--Magnetic-Bouncer

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.

How-To-6--Magnetic-Bouncer

#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()

 

From The MagPi store

Subscribe

Subscribe to the newsletter

Get every issue delivered directly to your inbox and keep up to date with the latest news, offers, events, and more.