Squeeze Controller: hack a dynamo torch

By Lucy Hattersley. Posted

Is it a joystick? Is it a paddle? No it’s ‘The Squeeze’ – a new type of games controller. By Mike Cook

Have you seen those dynamo torches, the ones that you repeatedly squeeze to light three white LEDs? Well, this month we are going to take a pair of these and turn them into a unique games controller. A new type of controller offers the possibility of new types of games, or a better way to control some existing types of games.

This tutorial was written by Mike Cook and first appeared in The MagPi issue #83. Subscribe in print for 12-months and get a free Raspberry Pi.

Hack a dynamo torch

The torch, or flashlight for our American cousins, can come in many forms. These days there are lots of self-powering devices which involve actually generating the power needed to drive them by the efforts of the user. With a dynamo torch, the user repeatedly squeezes a lever to spin a magnet in a coil and generate electricity. We took one apart and measured the voltage the generator produced. As you can see in Figure 1, the output is AC with a peak-to-peak voltage of almost 80 V; when squeezed, the frequency rapidly rises to about 170Hz.

You'll need

 Figure 1 Oscilloscope trace of the raw output from the dynamo torch

Warning! High voltage

The dynamo torches in this project can produce high voltage, so be careful. This voltage is very high, but is loaded down by putting a white LED across it; this shorts the negative voltage and limits the positive voltage to about 3 V, which is the forward voltage drop across the LED. This is a cheap and nasty design. You can pump the leaver to sustain a voltage or just squeeze once for a pulse, as shown in Figure 2. This shows 24 rapid squeezes followed by a single squeeze and release; it is measured over five seconds. Note how the trace is changing so rapidly that we can’t see the individual waveform, only the envelope.

 Figure 2 Oscilloscope trace of 24 rapid squeezes and then a single squeeze at the end

Conditioning the signal

The idea is that we can condition this signal to make a games controller. Basically, we need to make it into a DC signal by adding a series diode and then getting just the peaks of this signal with an envelope follower, which is sometimes called a peak detector. This uses a capacitor to hold the peak voltage and a discharging resistor which controls the release of the peak. The schematic for this is shown in Figure 3. When the signal is passed through this circuit, you get the waveform shown in Figure 4. You will need two of these circuits.

 Figure 3 Schematic of the signal-conditioning interface

 Figure 4 Oscilloscope trace of the voltage output after conditioning

Building the circuit

We used a piece of 14 hole, by 10 row, stripboard and a single pin header row. This plugs into our ADC (see issue 68, page 42), component side down. The components were wired up as in Figure 5. Note that the track side shows where to cut the tracks and is flipped over right to left, just like you would see it. Figure 6 shows a photograph of the conditioning circuit. The band on the diode marks the cathode, and the strip down the capacitor marks the negative wire. Make sure you get them the right way round.

 Figure 5 Physical layout of the signal-conditioning interface

 Figure 6 Photograph of the signal-conditioning interface

Hacking the torch

First off, drill a 2 mm hole in the body of the torch, close to the front, as shown in Figure 7. Then flip off the front cover and pull out the LED and battery assembly (Figure 8). Be careful, because some of the wires are very thin and you don’t want to snap them. Undo the two tiny screws holding the battery cover and remove the batteries. Now insert a length of 1.5 mm screened cable through the hole you previously drilled and strip off a 20 mm length at the end. Gather up the screen, twist it, and tin it.

 Figure 7 Drilling a hole for the connection wire

 Figure 8 Torch with the front removed

Finishing off the torch

Use the cable’s sleeving (that you cut off) to insulate the twisted screening and solder this to the sleeved side of the LEDs (Figure 9). Then solder the core to the other side of the LED. Cut off the wire that used to go to the top of the battery housing and give all those long wires from the LED a bit of a trim. Glue the plastic lenses to the inside of the cover and slowly pull the cable back out of the torch. Fix the cable with a dab of hot-melt glue on the inside, before clipping the cover back in place.

 Figure 9 Attaching the connection wire

Tug of War

What better way to show off a new interface than with a new game? So in the TugOfWar.py listing you will find our new, two-player, ‘Tug of War’ game especially designed for this interface. Figure 10 shows the game in progress. The central meter shows the target, which is the reading you are aiming for. If your player’s input is the closest to the target and is also within ten of the target, the rope is nudged in your direction. The first player to pull the rope over the finishing point is the winner. Pressing the SPACE bar starts another game.

 Figure 10 Tug of War in play

A look at the code

The code follows the normal Pygame structure, and requires three images: rope, knot, and meter. It also requires a start sound and end sound. To smooth the input, a running average of the voltage readings is used. The scale variable is a sort of fiddle factor that allows you to adjust the output, so that you can get maximum meter deflection at the peak value from the torch. The checkTarget function will change the target you are aiming for at random intervals, to make the game a bit more challenging, so you need to look at the target and your input.

Tip! Glue up the switch

The switch on the torch that was used to change over to battery operation, once it has been modified, disconnects the torch output. We found that this got knocked occasionally, leaving us to believe that the interface had stopped working. So we used polystyrene cement to make sure the output was always switched on.

Tip! Removing the torch front

This can be tricky, but with a flat-blade screwdriver and some determination it can be removed. Mind that you don’t stab yourself with the screwdriver – always push away from your body.

In conclusion

We hope you have fun with this. Another good game to implement using this interface would be a SpaceX rocket landing game similar to the classic Lunar Lander. However, in our next tutorial we will show you how to use this interface to make a rather large LED Racer game.

#!/usr/bin/env python3
#Tug of war using squeeze controller
# By Mike Cook June 2019

import math, spidev, time
import os, pygame, sys, random

pygame.init()
pygame.mixer.quit()
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)   
os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'
pygame.display.set_caption("Tug of War")
pygame.event.set_allowed(None)
pygame.event.set_allowed([pygame.KEYDOWN,pygame.QUIT])
screenWidth = 960 ; screenHight = 280 ; cp = screenWidth // 2
screen = pygame.display.set_mode([screenWidth,screenHight],0,32)
textHeight=22 ; font = pygame.font.Font(None, textHeight)
backCol = (160,160,160)
lastValue = [-10, -10, -10] # so you show on the first reading
screenUpdate = True ; random.seed()
nAv = 10 # number of samples to average
avPoint = [0,0,0] ; p1 = [0] * nAv ; p2 = [0] * nAv
runningAv = [p1,p2,[0]] ; average = [0.] * 3
target = 0.5 ; timeChange = 0 ; scale = 700

def main():
  global tugState, gameOver, winner
  print("Tug of War")
  init()
  while(1): # do forever
    timeChange = 0
    tugState = -cp # middle of screen   
    checkTarget()
    gameOver = False
    winner = -1 # no winer yet
    whistle.play() # start sound
    time.sleep(2.0)
    while not gameOver:
       checkForEvent()
       readVoltage()
       checkTug()
       checkTarget()
       if screenUpdate :
          drawScreen() 
          updateMeters()
    if winner == 0:      
       print("Blue Player is the winner")
       drawWords("Winner        ",123,159,(0,0,0),(20,178,155))
    else:   
       print("Yellow Player is the winner")
       drawWords("Winner            ",742,159,(0,0,0),
(20,178,155))
    pygame.display.update()
    end.play() # end sound
    print("Press space for another game")
    time.sleep(3.0)
    while gameOver:
      checkForEvent()
    
def checkTug():
  global tugState,screenUpdate,gameOver,winner
  #check to see if anyone has won
  if tugState <= -869:
    gameOver = True
    winner = 0
    return
  if tugState >= -37:
    gameOver = True
    winner = 1
    return
  #check to see if anyone has scored
  p1 = abs(average[0] - average[2])
  p2 = abs(average[1] - average[2])
  if p1 < p2 : #player 1 closest
    if p1 < 40:
      tugState -= 1
      screenUpdate = True
  else:
    if p2 < 40:
      tugState += 1
      screenUpdate = True       

def checkTarget():
   global target, timeChange
   if time.time() < timeChange:
      return
   temp = random.uniform(0.2,0.8) 
   target = int(temp*scale) 
   average[2] = target
   timeChange = time.time() + random.uniform(3.2,6.8)
   drawScreen()
   updateMeters()
   
def drawScreen():
   screen.fill(backCol)
   for i in range(0,3):
      screen.blit(meter, (meterPositionX[i],
meterPositionY[i]) )
   screen.blit(rope, (tugState,190) )
   drawWords("Target",447,159,(0,0,0),(20,178,155))
   drawWords("Blue Player",123,159,(0,0,0),
(20,178,155))
   drawWords("Yellow Player",742,159,(0,0,0),
(20,178,155))
   pygame.draw.line(screen,(0,0,0),(64,188),
(64,272),4)
   pygame.draw.line(screen,(0,0,0),(896,188),
(896,272),4)
   pygame.display.update()

def drawWords(words,x,y,col,backCol) :
    textSurface = font.render(
words, True, col, backCol)
    textRect = textSurface.get_rect()
    textRect.left = x # right for align right
    textRect.top = y    
    screen.blit(textSurface, textRect)
    return textRect
   
def init():
    global meter, rope, meterPositionX, meterPositionY, spi,whistle, end
    whistle = pygame.mixer.Sound("sounds/whistle.ogg")
    end = pygame.mixer.Sound("sounds/end.ogg")
    meter = pygame.image.load(
"images/MeterPC.png").convert_alpha()
    rope = pygame.image.load(
"images/rope.png").convert_alpha()
    meterPositionX=[10,638,324]
    meterPositionY=[10,10,10]
    spi = spidev.SpiDev()
    spi.open(0,0)
    spi.max_speed_hz=1000000     
    
def readVoltage():
   global screenUpdate, average, avPoint,lastValue, runningAv
   for i in range(0,2):
      adc = spi.xfer2([1,(8+i)<<4,0]) # request channel
      reading = (adc[1] & 3)<<8 | adc[2] # join two bytes together
      runningAv[i][avPoint[i]] = reading
      avPoint[i]+=1
      if avPoint[i] >= nAv:
        avPoint[i] = 0
      average[i] =  0 
      for j in range(0,nAv): # calculate new running average
         average[i] += runningAv[i][j]
      average[i] = average[i] / nAv      
      if abs(lastValue[i] - average[i]) > 8 or (
average[i] == 0 and lastValue[i] !=0):
         lastValue[i] = average[i]
         screenUpdate = True      

def updateMeters():
    global screenUpdate, average
    for i in range(0,3):
       plot = constrain(average[i]/scale,0.0,1.0)
       angle = (math.pi * ((-plot))) + 
(1.0 * math.pi)
       mpX = 146 + meterPositionX[i]
       mpY = 146 + meterPositionY[i]
       dx = mpX + 140 * math.cos(angle) 
       dy = mpY - 140 * math.sin(angle) 
       pygame.draw.line(screen,(50,50,50),(mpX,mpY),
(dx,dy),2)
       screenUpdate = False
    pygame.display.update()
    
def constrain(val, min_val, max_val):
    return min(max_val, max(min_val, val))
  
def terminate(): # close down the program
    print ("Closing down")
    pygame.mixer.quit()
    pygame.quit() # close pygame
    os._exit(1)
    
def checkForEvent(): # see if we need to quit
    global reading, screenUpdate, average, gameOver
    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_SPACE :
          gameOver = False

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.