Devious Fish
Music daemons & more
profile for Perette at Stack Overflow, Q&A for professional and enthusiast programmers

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:

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 solenoid
  • game.lights - controls playfield lighting and enables optosensors
  • game.accessory1
  • game.accessory2
Actions are 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:

VibeRequest waveforms
VibeRequest waveforms and their relationship to how the motor is driven.

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 the float datatype.