Software
Contents
The Skeez Ball software is written in Python using the RPi.GPIO library to do the low-level control. RPi.GPIO is flexible, but in this case flexibility creates obsurity. Thus, we wrap RPi.GPIO in libraries that provide easier access to what we need. Those then come together into a single library that is used to implement the hardware test, and can be used to implement games.1. The Libraries
First, for off-Pi development and testing, we have GPIO Simulator (gpiosim.py) (Software/gpiosim.html), which provides the same API as RPi.GPIO. Instead of controlling output, though, you get printed output. And in place of reading pins for input, you can provide a script that describes what those pin inputs should be at what times.But RPi.GPIO is very low level, so we provide better wrappers:
- IO Pins (iopins.py) (Software/iopins.html) provide the classes InputPin and OutputPin, which provide simple read and write access to the GPIO pins.
- Sensors (sensors.py) (Software/sensors.html) provide a class that monitors multiple input pins and queues received events. There is also support to simulate events from a script for development and testing.
- Ticket dispenser (dispense.py) (Software/dispense.html) provides control and monitoring for a ticket dispenser. There is also a fake dispenser that can be substituted for those without a working dispenser.
- Configuration file reader (configfile.py) (Software/configfile.html) provides for reading and merging configuration files.
- Hardware Controller (hwcontrol.py) (Software/hwcontrol.html) brings the above libraries together, along with configuration file support and motor control, into a flexible, all-in-one, ready-to-go library. This is the library we will focus on for the rest of this document.
2. The Game Hardware library
Rather than deal with all the individual classes and
configuration, the hwcontrol.Game
object wraps these
up into one neat package that encompasses all of them. To avoid
hard-coded values, it relies on configuration files for settings,
one built-in and the other a user-provided file so that users can
override default settings.
2.1. Loading the library
To load the library:
from hwcontrol import Game # Make an instance game = Game("~/.skeezballrc")
Instantiating the object will read the configuration files and initialize all inputs and outputs. The remainder of this page discusses how to utilize the facilities provided.
The configurations files are located in a directory named
Config
, which is a sister directory to the one in
which the main module is found.
2.2. Controlling switches
The output pins are:
game.bollocks
- controls the ball release solenoidgame.lights
- controls playfield lighting and enables optosensorsgame.accessory1
game.accessory2
on()
or off()
. It is also
possible to floating()
,1 but this is not applicable to these
outputs. Full documentation on OutputPin
is in
iopins.py (Software/iopins.html)
For example:
game.lights.on() game.bollocks.off()
Upon Game
instance destruction (including program
termination, unless something goes wrong), all outputs are turned
off.
2.3. Sensors
All sensed events are registered in a queue. To wait for an event:
event = game.sense.getEvent()
To check if there's an event, use:
event = game.sense.getEvent (0.0)
Return value is None
if there is no event pending.
The numeric parameter specifies wait time in seconds. Thus, to wait
up to for 1.5 seconds for an event::
event = game.sense.getEvent (1.5)
Full documentation on Sensors
is in sensors.py (Software/sensors.html)
2.4. Ticket dispenser
This class controls the ticket dispenser, counting dispensed tickets and reporting jams or dispenser empty.
Example:
count = game.dispenser.dispense (5) if (count != 5): if game.dispenser.jammed(): print ("Ticket dispenser jammed.") elif game.dispenser.empty(): print ("Ticket dispenser is empty.") else: print ("Ticket dispenser: unknown malfunction.")
Full documentation documentation for Dispenser
is
in dispense.py (Software/dispense.html)
2.5. Vibrating
In concept, you could do this:if (game.player1.consent()): game.player1.throttle (motor: 0, throttle: 50) else: game.player1.allStop()
However, this example is contrived: throttle()
already does this very check and action for you. When consent is
removed, the vibes are stopped. Thus, this could be better written
like:
if not game.player1.throttle (0, 50): print ("Player 1, consent removed. Game exiting.") return
Note: Regardless of the numbering provided by the underlying motor controller, we number each player's motors starting at 0. Each player has their own set of vibes.
2.5.1. Complex vibration
This allows vibe activity to be queued. Activity patterns are expressed as "waveforms". Each waveform plays for a given duration (unless consent is removed, in which case it stops).
To clear the stimulation queue and stop any current vibration, without terminating the thread:
game.playern.clearStimulation()
For testing: you can queue the request and invoke `game.playern.testRequest ("test-name")`. The process will block while performing vibration, but return to the caller automatically. To make a vibration request:
# Usage: game.playern.stimulate (duration in seconds, left-waveform, right-waveform) game.player1.stimulate (8, Waveform (period=5), None)
2.5.2. Waveform diagram
Waveforms describe how the vibrator's throttle will be adjusted over time. This diagram explains everything:

2.5.3. Waveform code examples
That's perfectly clear, right? Perhaps some examples can clarify, so let's look at some examples of waveforms—but first we need to import the waveform library:
from waveform import Waveform, SawtoothWave, SineWave, SquareWave
Waveform
is the base class, and so can reference
instances of the other classes once instantiated. A 5-second
default up-and-down waveform:
up_and_down = SawtoothWave (period = 5)A similar waveform, with a rounder shape and minimum and maximum thresholds adjusted:
up_and_down_gentle = SineWave (period = 5, minimum=20, maximum=80)The phase can be shifted to get down-then-up instead of up-then-down. Valid phase offsets are 0 <= offset < 1:
down_and_up = SawtoothWave (period = 5, offset=0.5)It's possible to take take only a portion of the waveform. For example, to use only the down portion:
repeating_down = SawtoothWave (period = 5, offset = 0.5, portion = 0.5)In this example,
portion
selects half the waveform,
and offset
selects the last half (the declining
portion).
Instead of the up-then-down waveforms, a throttle pattern can be specified:
pulsing = SquareWave (period = 6.5, rates = [70, 70, 0, 80, 80, 0, 90, 0, 100, 100, 0, 0, 0])In this case, the 13 values are split over 6.5 seconds, so 1/2 second per value. Repeating values repeats those thresholds, thus, this series will produce a long-long-short-long buzz pattern, with each buzz getting a little stronger.
All the other parameters can be applied here too, allowing you
to slice-and-dice, phase shift, or scale the values with
portion
, offset
, minimum
and/or maximum
.
The waveform will be truncated or repeated to fulfill the VibeRequest.
llsl = SquareWave (period = 6.5, rates = [70, 70, 0, 80, 80, 0, 90, 0, 100, 100, 0, 0, 0]) vibe = VibeRequest (20, llsl, llsl) game.player1.stimulate (vibe)
2.5.4. Copying and altering waveforms
Once you've defined a waveform, you might want a slight
variation: perhaps a different amplitude, or phase shifted version.
The copyWith
method provides a copy of a waveform,
allowing you to change existing waveform parameters to your
needs.
llsl = SquareWave (period = 6.5, rates = [70, 70, 0, 80, 80, 0, 90, 0, 100, 100, 0, 0, 0]) grade_crossing = llsl.copyWith (period = 17) vibe = VibeRequest (20, grade_crossing, grade_crossing) game.player1.stimulate (vibe)You can override any or all of the parameters used to initially create waveforms.
2.5.5. Motor parameters
By default, the game is configured for vibrators, running them to full throttle. Alternate motor parameters allow the vibes to be run more gently, to increase the lower threshold of the throttle, or adjust the starting intensity. Furthermore, alternate devices that can be utilize the motor output, such as TENS units, behave differently from motors and require different motor parameters to make intensities match up.from hwcontrol import Game, Waveform, MotorParameters game = Game("~/.skeezballrc") # We can now get the names of alternate parameter sets parameter_set_names: list[str] = MotorParameters.getMotorParameterSets()The list of names returned is suitable for display to the user. I.e., things like, "Gentle", "Medium itensity", "Full throttle", and "TENS". These names and their parameters are set in a configuration file.
To change a motor's parameters:
if chosen_parameter_name := getUserChoice (parameter_set_names): game.player1.setMotorParameters (motor_number: 0, name: chosen_parameter_name)In this example,
getUserChoice()
is a stand-in for
whatever method you provide the user for choosing one of the set
names. Perhaps it provides a dialog box.Footnotes:
- 1.
Yes, it should be
float
, but this could create confusion with thefloat
datatype.