Skip to content

Instantly share code, notes, and snippets.

@dead-claudia
Created November 23, 2014 13:40
Show Gist options
  • Save dead-claudia/1336264efb1731bd0f60 to your computer and use it in GitHub Desktop.
Save dead-claudia/1336264efb1731bd0f60 to your computer and use it in GitHub Desktop.
A dummy equivalent implementation for the Finch Robot Python API. Saved me so much hassle in multiple CS assignments.
# Finch Robot API Dummy Implementation
# ====================================
#
# This is simply a dummy API implementation, printing each instruction to the
# console. It has been very faithful in helping me debug multiple CS
# assignments without constantly having to plug in the Finch robot. Various use
# cases I've experienced that this saved me a lot of hassle include sparing me
# from distraction from my 9 year old brother (10 years age difference is
# fun...), being more able to work on this at night, and being able to debug
# without the robot itself being with me.
#
# I thought this would be best posted online as a gist to where a lot more
# people could have access to it, catching bugs, or even use it themselves.
#
# If you're curious about the API, the docstrings are potentially more complete
# than the bundled PDF of the original drivers in API documentation. Also,
# unlike the original drivers, this does type-check in debug mode.
#
# If you're unfamiliar with the license, don't fear it. It actually legally
# guarantees you're allowed to do a lot more with it, protecting both of us.
#
# This file by Isiah Meadows is licensed under the BSD 2-Clause License:
#
# Copyright (c) 2014, Isiah Meadows
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from time import sleep, monotonic
from threading import Timer
from numbers import Real
# Arbitrary constants. Changed through dummyfinch.set_constant(name, value)
_c_defaults = {
'LIGHT_LEFT_SENSOR_READING': 0.0,
'LIGHT_RIGHT_SENSOR_READING': 0.0,
'TEMPERATURE_READING': 30.0,
'ACCELERATION_X_READING': 0.0,
'ACCELERATION_Y_READING': 0.0,
'ACCELERATION_Z_READING': 0.0,
'ACCELERATION_TAP': False,
'ACCELERATION_SHAKE': False,
'OBSTACLE_LEFT_SENSOR_READING': False,
'OBSTACLE_RIGHT_SENSOR_READING': False
}
_c = _c_defaults
# Types to check with dummyfile.set_constant(name, value)
_types = {
'LIGHT_LEFT_SENSOR_READING': float,
'LIGHT_RIGHT_SENSOR_READING': float,
'TEMPERATURE_READING': float,
'ACCELERATION_X_READING': float,
'ACCELERATION_Y_READING': float,
'ACCELERATION_Z_READING': float,
'ACCELERATION_TAP': bool,
'ACCELERATION_SHAKE': bool,
'OBSTACLE_LEFT_SENSOR_READING': bool,
'OBSTACLE_RIGHT_SENSOR_READING': bool
}
# Print a message from the Finch class, properly formatted.
def _print(msg):
print('Dummy Finch: ** %s **' % (msg))
# Print a message, coercing the second argument to a string if it isn't
# already.
def _print_coerce(msg, t):
_print('%s: %s' % (msg, str(t)))
# Get the proper string representation of a speed. Examples:
#
# -1.0 -> "1.00000 BACKWARD"
# 1.0 -> "1.00000 FORWARD"
# 0.0 -> "STOPPED"
def _get_response(val):
if val:
if val > 0:
direction = 'FORWARD'
else:
direction = 'BACKWARD'
val = -val
return '%s %f' % (direction, min(val, 1.0))
else:
return 'STOPPED'
# Here's the nice and meaty dummy implementation.
class Finch:
"""
A class representing a Finch robot. This just so happens to be a dummy
API implementation, purely meant for debugging. It also features type
asserts, to assist in debugging (e.g. not setting wheel speed to an
object).
The API is occasionally more verbosely documented here than in the PDF
reference. Just import this module in the REPL and type
`help(dummyfinch.Finch)` (or something different, based on how you imported
it) to access this documentation.
The API documentation for each method is in the following format:
type(argument_one) - description
type(argument_two) - description
return type - description
(or: `return void` for functions that don't have a return value)
Irrelevant sections are omitted for brevity
"""
def __init__(self):
"""
Initialize a Finch instance.
"""
self._active_end = 0
self._active_callback = None
self._buzzer_silent = True
_print('CONNECTED')
# A callback function used for .buzzer()
def _callback(self):
'Private'
_print('BUZZ: stopped')
self._active_callback = None
self._buzzer_silent = True
# A method used to reset and/or start a buzzer. Uses threading.Timer to
# asynchronously set a timer for when to shut off the "buzzer".
def _reset(self, timeout):
'Private'
if self._active_callback is None:
self._active_callback = Timer(timeout, self._callback)
self._active_callback.start()
elif self._active_end > timeout + monotonic():
self._active_callback.cancel()
self._active_callback = Timer(timeout, self._callback)
self._active_callback.start()
def led(self, *args):
"""
Control three LEDs (orbs).
1 argument:
str(color) - hex color string
Example: `.led('#00FF8B')`
3 arguments:
int(r) - red value
int(g) - green value
int(b) - blue
Example: `.led(0, 255, 139)`
return void - This is a function that does not return a value.
"""
if len(args) == 3:
assert type(args[0]) is int, 'r must be of type int'
assert type(args[1]) is int, 'g must be of type int'
assert type(args[2]) is int, 'b must be of type int'
# Convert each argument into an RGB value.
r, g, b = [int(x) % 256 for x in args]
elif len(args) == 1:
assert type(args[0]) is str, 'color must be of type str'
# Strip surrounding whitespace as to ease parsing.
color = args[0].strip()
assert len(color) == 7, 'len(color) must be 7'
assert color[0] == '#', "color must start with the character '#'"
if len(color) == 7 and color[0] == '#':
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
else:
# This fixes a bug in the initial string parsing, where the
# initial Finch API silently attempted improperly to set wheel
# speed to uninitialized values.
return
else:
assert False, 'args must be of length 3 or 1'
return
_print_coerce('LED', (r, g, b))
def buzzer(self, duration, frequency):
"""
Outputs sound. Does not wait until a note is done beeping.
float(duration) - duration to beep, in seconds (s).
int(frequency) - frequency, in hertz (HZ).
return void - This is a function that does not return a value.
"""
assert type(duration) is float, 'duration must be of type float'
assert type(duration) is int, 'frequency must be of type int'
# The threading.Timer class operates off of miliseconds, not seconds.
millisec = int(duration * 1000)
if self.buzzer_silent:
self.buzzer_silent = False
# And, we're buzzing...
_print('BUZZ: %s ms, %s hz' % (millisec))
self._reset(duration)
# Note that this is synchronous/blocking.
def buzzer_with_delay(self, duration, frequency):
"""
Outputs sound. Waits until a note is done beeping.
Real(duration) - duration to beep, in seconds (s).
int(frequency) - frequency, in hertz (HZ).
return void - This is a function that does not return a value.
"""
assert isinstance(duration, Real), 'duration must be a real number'
assert type(duration) is int, 'frequency must be of type int'
self.buzzer(duration, frequency)
sleep(duration*1.05)
def light(self):
"""
Get light sensor readings. The values ranges from 0.0 to 1.0.
return (float, float) - a tuple (left, right) of each sensor's
readings.
"""
t = (_c['LIGHT_LEFT_SENSOR_READING'], _c['LIGHT_RIGHT_SENSOR_READING'])
_print_coerce('LIGHT', t)
return t
def obstacle(self):
"""
Get obstacle sensor readings.
return (bool, bool) - a tuple (left, right), True if there is an object
in front of it.
"""
t = (_c['OBSTACLE_LEFT_SENSOR_READING'],
_c['OBSTACLE_RIGHT_SENSOR_READING'])
_print_coerce('OBSTACLE', t)
return t
def temperature(self):
"""
Gets the temperature, according to the Finch.
return float - The temperature in degrees Celcius.
"""
t = _c['TEMPERATURE_READING']
_print_coerce('TEMP', t)
return t
# This is rarely used, and is mostly irrelevant for this implementation. It
# is only implemented for completeness.
def convert_raw_accel(self, a):
"""
Converts the raw acceleration obtained from the hardware into G's
int(a) - The hardware reading. This should be 0 >= a >= 31, and in this
specific module, is checked as an invariant in debug mode.
return float - The acceleration in G's
"""
assert 0 <= a < 32, 'a should be within the hardware constraints of 0 >= a >= 31' # noqa
if a > 31:
# a isn't limited to just 6 bits. This works for any size. The
# actual Finch API makes a assumption that works for all hardware
# output, but not for all input.
a = (a % 32) - 32
return a * 1.6 / 32.0
def acceleration(self):
"""
Returns the (x, y, z, tap, shake). x, y, and z, are the acceleration
readings in units of G's, and range from -1.5 to 1.5.
When the finch is horizontal, z is close to 1, x, y close to 0.
When the finch stands on its tail, y, z are close to 0, x is close to
-1.
When the finch is held with its left wing down, x, z are close to 0,
y is close to 1.
tap, shake are boolean values -- true if the corresponding event has
happened.
return (int, int, int, bool, bool) - in order, the x, y, z, tap, and
shake values.
"""
t = (_c['ACCELERATION_X_READING'],
_c['ACCELERATION_Y_READING'],
_c['ACCELERATION_Z_READING'],
_c['ACCELERATION_TAP'],
_c['ACCELERATION_SHAKE'])
_print_coerce('ACCELERATION', t)
return t
def wheels(self, left, right):
"""
Controls the left and right wheels.
Values must range from -1.0 (full throttle reverse) to
1.0 (full throttle forward).
use left = right = 0.0 to stop.
return void - This is a function that does not return a value.
"""
assert isinstance(left, Real), 'left must be of type float or int'
assert isinstance(right, Real), 'right must be of type float or int'
assert -1.0 <= left <= 1.0, 'left must satisfy -1.0 >= left >= 1.0'
assert -1.0 <= right <= 1.0, 'right must satisfy -1.0 >= right >= 1.0'
left_response = _get_response(left)
right_response = _get_response(right)
_print('WHEELS: left: %s, right: %s' % (left_response, right_response))
def halt(self):
"""
Set all motors and LEDs to off.
return void - This is a function that does not return a value.
"""
# This does a *lot* more in the actual driver API.
_print('HALT')
def close(self):
"""
Close the current Finch instance.
return void - This is a function that does not return a value.
"""
# This does a *lot* more in the actual driver API.
_print('CLOSED')
# Better to check against a reference than equality to anything. None can't be
# used, because it still doesn't cover all possible input.
_default = object()
def set_constant(name, value=_default):
"""
Set a hard-coded constant used by this module's class instance methods.
This is an additional method that does not exist in the actual Finch driver
API.
Valid names, their respective types, and their default settings are below:
'LIGHT_LEFT_SENSOR_READING': float, 0.0
'LIGHT_RIGHT_SENSOR_READING': float, 0.0
'TEMPERATURE_READING': float, 30.0
'ACCELERATION_X_READING': float, 0.0
'ACCELERATION_Y_READING': float, 0.0
'ACCELERATION_Z_READING': float, 0.0
'ACCELERATION_TAP': bool, False
'ACCELERATION_SHAKE': bool, False
'OBSTACLE_LEFT_SENSOR_READING': bool, False
'OBSTACLE_RIGHT_SENSOR_READING': bool, False
Each setting should be relatively self-descriptive.
Arguments:
str(name) - The name to modify.
any(value) - The value to set. Omit to remove the custom setting, i.e.
reset the setting to the default value. This is converted to
its type as per above, and in debug mode, the type is strictly
enforced.
return void - This is a function that does not return a value.
"""
assert type(name) is str, 'name must be of type str'
name = str(name)
try:
if value is _default:
# Set default if the optional parameter isn't passed.
_c[name] = _c_defaults[name]
else:
assert type(value) is _types[name]
_c[name] = _types[name](value)
except KeyError as e:
# Alias the error as a more applicable type.
raise ValueError('name must be a valid constant.') from e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment