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
- Two dynamo torches
- A/D converter (ADC), e.g. MCP3008
- Assorted electronic components
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.
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.
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.
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.
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.
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.
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()