#
# tborg/tests/test_tborg.py
#
import os
import logging
import unittest
import time
from unittest.mock import patch
from tborg import ConfigLogger, ThunderBorgException, ThunderBorg
LOG_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', 'logs'))
not os.path.isdir(LOG_PATH) and os.mkdir(LOG_PATH, 0o0775)
# def isclose(a, b, rel_tol, abs_tol):
# return abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol)
[docs]
class BaseTest(unittest.TestCase):
LOGGER_NAME = 'thunder-borg'
def __init__(self, name, filename=None):
super().__init__(name)
if filename:
full_path = os.path.abspath(os.path.join(LOG_PATH, filename))
cl = ConfigLogger()
cl.config(logger_name=self.LOGGER_NAME, file_path=full_path,
level=logging.DEBUG)
[docs]
@classmethod
def setUpClass(self):
ThunderBorg.DEFAULT_I2C_ADDRESS = 0x15
ThunderBorg.set_i2c_address(ThunderBorg.DEFAULT_I2C_ADDRESS)
tb = ThunderBorg()
tb.halt_motors()
tb.set_both_leds(0, 0, 0)
tb.set_led_battery_state(False)
tb.set_comms_failsafe(False)
tb.write_external_led_word(0, 0, 0, 0)
[docs]
class TestNoSetUp(BaseTest):
_LOG_FILENAME = 'tb-no-setup-method.log'
def __init__(self, name):
super().__init__(name, filename=self._LOG_FILENAME)
#@unittest.skip("Temporarily skipped")
[docs]
@patch.object(ThunderBorg, 'DEFAULT_I2C_ADDRESS', 0x20)
def test_find_address_with_invalid_default_address(self):
"""
Test that an invalid default address will cause a board to be
initialized if the `auto_set_addr` argument is `True`.
"""
default_address = 0x15
# Initialize the board by instantiating ThunderBorg.
ThunderBorg(logger_name=self.LOGGER_NAME, log_level=logging.DEBUG,
auto_set_addr=True)
boards = ThunderBorg.find_board()
msg = "fBoards found: {boards}"
self.assertEqual(ThunderBorg.DEFAULT_I2C_ADDRESS, 0x20, msg)
self.assertTrue(len(boards) > 0, msg)
self.assertEqual(boards[0], default_address, msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_config_with_invalid_address(self):
"""
Test that an invalid address creates the expected exception.
"""
err_msg = ("ThunderBorg could not be found; is it properly attached, "
"the correct address used, and the I2C driver module "
"loaded?")
with self.assertRaises(ThunderBorgException) as cm:
ThunderBorg(address=0x70, logger_name=self._LOG_FILENAME,
log_level=logging.DEBUG)
message = str(cm.exception)
self.assertEqual(message, err_msg)
#@unittest.skip("Temporarily skipped")
[docs]
@patch.object(ThunderBorg, '_I2C_ID_THUNDERBORG', 0x20)
def test_config_with_invalid_board_id(self):
"""
Test that an invalid board ID causes the expected exception.
"""
err_msg = ("ThunderBorg could not be found; is it properly attached, "
"the correct address used, and the I2C driver module "
"loaded?")
with self.assertRaises(ThunderBorgException) as cm:
ThunderBorg(logger_name=self._LOG_FILENAME,
log_level=logging.DEBUG)
message = str(cm.exception)
self.assertEqual(message, err_msg)
[docs]
class TestClassMethods(BaseTest):
_LOG_FILENAME = 'tb-class-method.log'
def __init__(self, name):
super().__init__(name, filename=self._LOG_FILENAME)
[docs]
def tearDown(self):
ThunderBorg.set_i2c_address(ThunderBorg.DEFAULT_I2C_ADDRESS)
#@unittest.skip("Temporarily skipped")
[docs]
def test_find_board(self):
"""
Test that the ThunderBorg.find_board() method finds a board.
"""
found = ThunderBorg.find_board()
found = found[0] if found else 0
msg = (f"Found address '0x{found:02X}', should be "
f"'0x{ThunderBorg.DEFAULT_I2C_ADDRESS:02X}'.")
self.assertEqual(found, ThunderBorg.DEFAULT_I2C_ADDRESS, msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_i2c_address_without_current_address(self):
"""
Test that the ThunderBorg.set_i2c_address() can set a different
address. Scans address range to find current address.
"""
# Set a new address
new_addr = 0x70
ThunderBorg.set_i2c_address(new_addr)
found = ThunderBorg.find_board()
found = found[0] if found else 0
msg = f"Found address '0x{found:02X}', should be '0x{new_addr:02X}'."
self.assertEqual(found, new_addr, msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_i2c_address_with_current_address(self):
"""
Test that the ThunderBorg.set_i2c_address() can set a different
address. The current address is provided.
"""
# Set a new address
new_addr = 0x70
cur_addr = ThunderBorg.DEFAULT_I2C_ADDRESS
ThunderBorg.set_i2c_address(new_addr, cur_addr=cur_addr)
found = ThunderBorg.find_board()
found = found[0] if found else 0
msg = f"Found address '0x{found:02X}', should be '0x{new_addr:02X}'."
self.assertEqual(found, new_addr, msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_i2c_address_with_address_range_invalid(self):
"""
Test that an exception is raised when the address is out of range.
"""
new_addr = 0x78
err_msg = "Error, I2C addresses must be in the range of 0x03 to 0x77"
with self.assertRaises(ThunderBorgException) as cm:
ThunderBorg.set_i2c_address(new_addr)
message = str(cm.exception)
self.assertEqual(message, err_msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_config_with_auto_set_address(self):
"""
Test that ``auto_set_addr`` if set to ``True`` causes the API to
find the first valid board and configure itself to that board.
"""
# First change the board address so it cannot be found at the
# default address.
new_addr = 0x70
ThunderBorg.set_i2c_address(new_addr)
# Now instantiate ThunderBorg.
ThunderBorg(logger_name=self._LOG_FILENAME, log_level=logging.DEBUG,
auto_set_addr=True)
[docs]
class TestThunderBorg(BaseTest):
_LOG_FILENAME = 'tb-instance.log'
def __init__(self, name):
super().__init__(name, filename=self._LOG_FILENAME)
[docs]
def setUp(self):
self._tb = ThunderBorg(logger_name=self._LOG_FILENAME,
log_level=logging.DEBUG)
[docs]
def tearDown(self):
self._tb.halt_motors()
self._tb.set_comms_failsafe(False)
self._tb.set_led_battery_state(False)
self._tb.set_led_one(0.0, 0.0, 0.0)
self._tb.set_led_two(0.0, 0.0, 0.0)
self._tb.set_battery_monitoring_limits(7.0, 36.3)
self._tb.write_external_led_word(0, 0, 0, 0)
self._tb.close_streams()
def _validate_tuples(self, t0, t1):
msg = "rgb0: {:0.2f}, rgb1: {:0.2f}"
for x, y in zip(t0, t1):
self.assertAlmostEqual(x, y, delta=0.01, msg=msg.format(x, y))
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_and_get_motor_one(self):
"""
Test that motor one responds to commands.
"""
# Test forward
speeds = (0.0, 0.25, 0.5, 0.75, 1.0, 1.25)
for speed in speeds:
self._tb.set_motor_one(speed)
rcvd_speed = self._tb.get_motor_one()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertLessEqual(rcvd_speed, 1.0, msg=msg)
time.sleep(1.0)
# Test reverse
speeds = (-0.0, -0.25, -0.5, -0.75, -1.0, -1.25)
for speed in speeds:
self._tb.set_motor_one(speed)
rcvd_speed = self._tb.get_motor_one()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertGreaterEqual(rcvd_speed, -1.0, msg=msg)
time.sleep(1.0)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_and_get_motor_two(self):
"""
Test that motor two responds to commands.
"""
# Test forward
speeds = (0.0, 0.25, 0.5, 0.75, 1.0, 1.25)
for speed in speeds:
self._tb.set_motor_two(speed)
rcvd_speed = self._tb.get_motor_two()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertLessEqual(rcvd_speed, 1.0, msg=msg)
time.sleep(1.0)
# Test reverse
speeds = (-0.0, -0.25, -0.5, -0.75, -1.0, -1.25)
for speed in speeds:
self._tb.set_motor_two(speed)
rcvd_speed = self._tb.get_motor_two()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertGreaterEqual(rcvd_speed, -1.0, msg=msg)
time.sleep(1.0)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_both_motors(self):
"""
Test that motors one and two responds to commands.
"""
# Test forward
speed = 0.5
self._tb.set_both_motors(speed)
rcvd_speed = self._tb.get_motor_one()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
rcvd_speed = self._tb.get_motor_two()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
# Test reverse
speed = -0.5
self._tb.set_both_motors(speed)
rcvd_speed = self._tb.get_motor_one()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
rcvd_speed = self._tb.get_motor_two()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_halt_motors(self):
"""
Test that halting the motors works properly.
"""
# Start motors and check that the board says they are moving.
speed = 0.5
self._tb.set_both_motors(speed)
rcvd_speed = self._tb.get_motor_one()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
rcvd_speed = self._tb.get_motor_two()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
# Halt the motors.
self._tb.halt_motors()
# Check that the board says they are not moving.
speed = 0.0
rcvd_speed = self._tb.get_motor_one()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
rcvd_speed = self._tb.get_motor_two()
msg = f"Speed sent: {speed}, speed received: {rcvd_speed}"
self.assertAlmostEqual(speed, rcvd_speed, delta=0.01, msg=msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_get_led_one(self):
"""
Test that the RBG colors set are the same as the one's returned.
"""
state = self._tb.get_led_battery_state()
msg = f"Default state should be False: {state}"
self.assertFalse(state, msg)
rgb_list = [(0, 0, 0), (1, 1, 1), (1.0, 0.5, 0.0), (0.2, 0.0, 0.2)]
for rgb in rgb_list:
self._tb.set_led_one(*rgb)
ret_rgb = self._tb.get_led_one()
self._validate_tuples(ret_rgb, rgb)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_get_led_two(self):
"""
Test that the RBG colors set are the same as the one's returned.
"""
state = self._tb.get_led_battery_state()
msg = f"Default state should be False: {state}"
self.assertFalse(state, msg)
rgb_list = [(0, 0, 0), (1, 1, 1), (1.0, 0.5, 0.0), (0.2, 0.0, 0.2)]
for rgb in rgb_list:
self._tb.set_led_two(*rgb)
ret_rgb = self._tb.get_led_two()
self._validate_tuples(ret_rgb, rgb)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_both_leds(self):
"""
Test that the RBG colors set are the same as the one's returned.
"""
state = self._tb.get_led_battery_state()
msg = f"Default state should be False: {state}"
self.assertFalse(state, msg)
rgb_list = [(0, 0, 0), (1, 1, 1), (1.0, 0.5, 0.0), (0.2, 0.0, 0.2)]
for rgb in rgb_list:
self._tb.set_both_leds(*rgb)
ret_rgb = self._tb.get_led_one()
self._validate_tuples(ret_rgb, rgb)
ret_rgb = self._tb.get_led_two()
self._validate_tuples(ret_rgb, rgb)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_and_get_led_battery_state(self):
"""
Test that the LED state changes.
"""
state = self._tb.get_led_battery_state()
msg = f"Default state should be False: {state}"
self.assertFalse(state, msg)
# Change the state of the LEDs to monitor the batteries.
self._tb.set_led_battery_state(True)
state = self._tb.get_led_battery_state()
msg = "Battery monitoring state should be True"
self.assertTrue(state, msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_and_get_comms_failsafe(self):
"""
Test that the failsafe changes states.
"""
failsafe = self._tb.get_comms_failsafe()
msg = f"Default failsafe should be False: {failsafe}"
self.assertFalse(failsafe, msg)
# Test that motors run continuously.
speed = 0.5
self._tb.set_both_motors(speed)
sleep = 1 # Seconds
time.sleep(sleep)
msg = f"Motors should run for {sleep} second."
m0_speed = self._tb.get_motor_one()
m1_speed = self._tb.get_motor_two()
self.assertAlmostEqual(m0_speed, speed, delta=0.1, msg=msg)
self.assertAlmostEqual(m1_speed, speed, delta=0.1, msg=msg)
self._tb.halt_motors()
# Turn on failsafe
self._tb.set_comms_failsafe(True)
failsafe = self._tb.get_comms_failsafe()
msg = f"Failsafe should be True: {failsafe}"
self.assertTrue(failsafe, msg)
# Start up motors
self._tb.set_both_motors(speed)
time.sleep(sleep)
msg = (f"Motors should run for 1/4 of a second with sleep of {sleep} "
"second(s).")
m0_speed = self._tb.get_motor_one()
m1_speed = self._tb.get_motor_two()
self.assertNotAlmostEqual(m0_speed, speed, delta=0.1, msg=msg)
self.assertNotAlmostEqual(m1_speed, speed, delta=0.1, msg=msg)
# Send keepalive
msg = "Motors are running for {} seconds"
interval = 0.25
sleep = 1.5 # Seconds
for itr in range(6):
self._tb.set_motor_one(0.5)
self._tb.set_motor_two(0.5)
time.sleep(interval)
m0_speed = self._tb.get_motor_one()
m1_speed = self._tb.get_motor_two()
t = (itr + 1) * interval
self.assertAlmostEqual(m0_speed, speed, delta=0.1,
msg=msg.format(t))
self.assertAlmostEqual(m1_speed, speed, delta=0.1,
msg=msg.format(t))
#@unittest.skip("Temporarily skipped")
[docs]
def test_get_drive_fault_one(self):
"""
Test that `get_drive_fault_one` returns data.
"""
fault = self._tb.get_drive_fault_one()
msg = "Fault value should be False, found: {}"
self.assertFalse(fault, msg.format(fault))
# Run motor one for a second.
speed = 0.5
self._tb.set_motor_one(speed)
self._tb.halt_motors()
# Test that fault is cleared.
fault = self._tb.get_drive_fault_one()
self.assertFalse(fault, msg.format(fault))
#@unittest.skip("Temporarily skipped")
[docs]
def test_get_drive_fault_two(self):
"""
Test that `get_drive_fault_two` returns data.
"""
fault = self._tb.get_drive_fault_two()
msg = "Fault value should be False, found: {}"
self.assertFalse(fault, msg.format(fault))
# Run motor one for a second.
speed = 0.5
self._tb.set_motor_two(speed)
self._tb.halt_motors()
# Test that fault is cleared.
fault = self._tb.get_drive_fault_two()
self.assertFalse(fault, msg.format(fault))
#@unittest.skip("Temporarily skipped")
[docs]
def test_get_battery_voltage(self):
"""
Test that the battery voltage is in range.
"""
vmin = ThunderBorg._BATTERY_MIN_DEFAULT
vmax = ThunderBorg._BATTERY_MAX_DEFAULT
voltage = self._tb.get_battery_voltage()
msg = (f"Voltage should be in the range of {vmin:0.02f} to "
f"{vmax:0.02f}, found {voltage:0.02f} volts")
self.assertTrue(vmin <= voltage <= vmax, msg)
#@unittest.skip("Temporarily skipped")
[docs]
def test_set_get_battery_monitoring_limits(self):
"""
Test that setting and getting the battery monitoring limits
functions properly.
.. note::
Set limits based on a fully charged LiIon battery pack. This could
be anywhere between 15.4 and 16.8 volts maximum depending on the
type of batteries used. The minimum should never be less than 3
volts per cell or in a four pack 12 volts.
"""
vmin = 12.0
vmax = 16.8
self._tb.set_battery_monitoring_limits(vmin, vmax)
voltage = self._tb.get_battery_voltage()
minimum, maximum = self._tb.get_battery_monitoring_limits()
msg = (f"Found minimum {minimum:0.2f} and maximum {maximum:0.2f} "
f"volts, should be minimum {vmin:0.2f} and maximum {vmax:0.2f} "
f"volts, actual voltage {voltage:0.2f}")
self.assertAlmostEqual(minimum, vmin, delta=0.1, msg=msg)
self.assertAlmostEqual(maximum, vmax, delta=0.1, msg=msg)
# Check that the actual voltage is within the above ranges.
self.assertTrue(vmin <= voltage <= vmax, msg)
[docs]
@unittest.skip("Temporarily skipped")
def test_write_external_led_word(self):
"""
Test that writing binary data with the `write_external_led_word`
method sets the LED correctly.
"""
# Check that both LEDs are off.
self._tb.write_external_led_word(0, 0, 0, 0)
# Turn on both LEDs.
color = (255, 64, 1, 0)
self._tb.write_external_led_word(*color)
#self._validate_tuples(led2, off)
[docs]
@unittest.skip("Temporarily skipped")
def test_set_external_led_colors(self):
"""
Test that setting external LEDs works correctly. This test requires
external LEDs.
"""
# Set a single LED to yellow.
yellow = [(1.0, 1.0, 0.0)]
self._tb.set_external_led_colors(yellow)