From 8dc2aee53a41860bd0dc7afb581b590d338ab5fe Mon Sep 17 00:00:00 2001 From: Vandy Liu Date: Mon, 10 Feb 2020 15:05:09 -0800 Subject: [PATCH] first commit for sensors --- src/microbit/__init__.py | 10 +- src/microbit/__model/accelerometer.py | 120 +++++++++++++++++++++++ src/microbit/__model/constants.py | 25 +++++ src/microbit/__model/display.py | 15 ++- src/microbit/__model/microbit_model.py | 18 +++- src/microbit/test/test_accelerometer.py | 95 ++++++++++++++++++ src/microbit/test/test_display.py | 14 +++ src/microbit/test/test_microbit_model.py | 15 +++ src/microbit/test/test_shim.py | 5 + 9 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 src/microbit/__model/accelerometer.py create mode 100644 src/microbit/test/test_accelerometer.py diff --git a/src/microbit/__init__.py b/src/microbit/__init__.py index f6e847f48..a87cd59dc 100644 --- a/src/microbit/__init__.py +++ b/src/microbit/__init__.py @@ -4,6 +4,7 @@ button_a = __mb.button_a button_b = __mb.button_b display = __mb.display +accelerometer = __mb.accelerometer def sleep(n): @@ -21,4 +22,11 @@ def running_time(): Return the number of milliseconds since the board was switched on or restarted. """ - __mb.running_time() + return __mb.running_time() + + +def temperature(): + """ + Return the temperature of the micro:bit in degrees Celcius. + """ + return __mb.temperature() diff --git a/src/microbit/__model/accelerometer.py b/src/microbit/__model/accelerometer.py new file mode 100644 index 000000000..732503500 --- /dev/null +++ b/src/microbit/__model/accelerometer.py @@ -0,0 +1,120 @@ +import enum + +from . import constants as CONSTANTS + + +class Accelerometer: + def __init__(self): + self.__x = 0 + self.__y = 0 + self.__z = 0 + self.__current_gesture = "" + self.__prev_gestures = set() + self.__gestures = [] + + def get_x(self): + """ + Get the acceleration measurement in the ``x`` axis, as a positive or + negative integer, depending on the direction. The measurement is given in + milli-g. + """ + return self.__x + + def get_y(self): + """ + Get the acceleration measurement in the ``y`` axis, as a positive or + negative integer, depending on the direction. The measurement is given in + milli-g. + """ + return self.__y + + def get_z(self): + """ + Get the acceleration measurement in the ``z`` axis, as a positive or + negative integer, depending on the direction. The measurement is given in + milli-g. + """ + return self.__z + + def get_values(self): + """ + Get the acceleration measurements in all axes at once, as a three-element + tuple of integers ordered as X, Y, Z. + """ + return (self.__x, self.__y, self.__z) + + def current_gesture(self): + """ + Return the name of the current gesture. + """ + self.__add_current_gesture_to_gesture_lists() + return self.__current_gesture + + def is_gesture(self, name): + """ + Return True or False to indicate if the named gesture is currently active. + """ + self.__add_current_gesture_to_gesture_lists() + if name not in CONSTANTS.GESTURES: + raise ValueError(CONSTANTS.INVALID_GESTURE_ERR) + return name == self.__current_gesture + + def was_gesture(self, name): + """ + Return True or False to indicate if the named gesture was active since the + last call. + """ + self.__add_current_gesture_to_gesture_lists() + if name not in CONSTANTS.GESTURES: + raise ValueError(CONSTANTS.INVALID_GESTURE_ERR) + was_gesture = name in self.__prev_gestures + self.__prev_gestures.clear() + return was_gesture + + def get_gestures(self): + """ + Return a tuple of the gesture history. The most recent is listed last. + Also clears the gesture history before returning. + """ + self.__add_current_gesture_to_gesture_lists() + gestures = tuple(self.__gestures) + self.__gestures.clear() + return gestures + + # Helpers and Hidden Functions + + def __set_x(self, x): + self.__x = self.__get_valid_acceleration(x) + + def __set_y(self, y): + self.__y = self.__get_valid_acceleration(y) + + def __set_z(self, z): + self.__z = self.__get_valid_acceleration(z) + + def __get_valid_acceleration(self, acceleration): + if acceleration < CONSTANTS.MIN_ACCELERATION: + return CONSTANTS.MIN_ACCELERATION + elif acceleration > CONSTANTS.MAX_ACCELERATION: + return CONSTANTS.MAX_ACCELERATION + else: + return acceleration + + def __get_accel(self, axis): + if axis == "x": + return self.get_x() + elif axis == "y": + return self.get_y() + elif axis == "z": + return self.get_z() + + def __set_gesture(self, gesture): + if gesture in CONSTANTS.GESTURES: + self.__current_gesture = gesture + else: + self.__current_gesture = "" + + def __add_current_gesture_to_gesture_lists(self): + if self.__current_gesture in CONSTANTS.GESTURES: + self.__gestures.append(self.__current_gesture) + self.__prev_gestures.add(self.__current_gesture) diff --git a/src/microbit/__model/constants.py b/src/microbit/__model/constants.py index 7f566ea90..b8c87bf9c 100644 --- a/src/microbit/__model/constants.py +++ b/src/microbit/__model/constants.py @@ -112,6 +112,30 @@ BRIGHTNESS_MIN = 0 BRIGHTNESS_MAX = 9 +# sensor max/min values +MAX_TEMPERATURE = 125 +MIN_TEMPERATURE = -55 +MAX_LIGHT_LEVEL = 255 +MIN_LIGHT_LEVEL = 0 +MAX_ACCELERATION = 1023 +MIN_ACCELERATION = -1023 + +GESTURES = set( + [ + "up", + "down", + "left", + "right", + "face up", + "face down", + "freefall", + "3g", + "6g", + "8g", + "shake", + ] +) + # error messages BRIGHTNESS_ERR = "brightness out of bounds" COPY_ERR_MESSAGE = "please call copy function first" @@ -120,5 +144,6 @@ NOT_IMPLEMENTED_ERROR = "This method is not implemented by the simulator" UNSUPPORTED_ADD_TYPE = "unsupported types for __add__:" SAME_SIZE_ERR = "images must be the same size" +INVALID_GESTURE_ERR = "invalid gesture" TIME_DELAY = 0.03 diff --git a/src/microbit/__model/display.py b/src/microbit/__model/display.py index 2dfd7cc36..e5920a79f 100644 --- a/src/microbit/__model/display.py +++ b/src/microbit/__model/display.py @@ -13,9 +13,10 @@ class Display: def __init__(self): self.__image = Image() self.__on = True - self.__current_pid = None + self.__light_level = 0 self.__blank_image = Image() + self.__current_pid = None self.__lock = threading.Lock() def scroll(self, value, delay=150, wait=True, loop=False, monospace=False): @@ -226,13 +227,19 @@ def is_on(self): def read_light_level(self): """ - Not implemented yet. - Use the display's LEDs in reverse-bias mode to sense the amount of light falling on the display. Returns an integer between 0 and 255 representing the light level, with larger meaning more light. """ - raise NotImplementedError(CONSTANTS.NOT_IMPLEMENTED_ERROR) + return self.__light_level + + def __set_light_level(self, level): + if level < CONSTANTS.MIN_LIGHT_LEVEL: + self.__light_level = CONSTANTS.MIN_LIGHT_LEVEL + elif level > CONSTANTS.MAX_LIGHT_LEVEL: + self.__light_level = CONSTANTS.MAX_LIGHT_LEVEL + else: + self.__light_level = level # Helpers diff --git a/src/microbit/__model/microbit_model.py b/src/microbit/__model/microbit_model.py index a1de5045d..a6d585545 100644 --- a/src/microbit/__model/microbit_model.py +++ b/src/microbit/__model/microbit_model.py @@ -1,17 +1,22 @@ import time +from .accelerometer import Accelerometer from .button import Button from .display import Display +from . import constants as CONSTANTS class MicrobitModel: def __init__(self): # State in the Python process + self.accelerometer = Accelerometer() self.button_a = Button() self.button_b = Button() - self.__start_time = time.time() self.display = Display() + self.__start_time = time.time() + self.__temperature = 0 + def sleep(self, n): time.sleep(n / 1000) @@ -19,5 +24,16 @@ def running_time(self): print(f"time. time: {time.time()}") return time.time() - self.__start_time + def temperature(self): + return self.__temperature + + def __set_temperature(self, temperature): + if temperature < CONSTANTS.MIN_TEMPERATURE: + self.__temperature = CONSTANTS.MIN_TEMPERATURE + elif temperature > CONSTANTS.MAX_TEMPERATURE: + self.__temperature = CONSTANTS.MAX_TEMPERATURE + else: + self.__temperature = temperature + __mb = MicrobitModel() diff --git a/src/microbit/test/test_accelerometer.py b/src/microbit/test/test_accelerometer.py new file mode 100644 index 000000000..fc118fada --- /dev/null +++ b/src/microbit/test/test_accelerometer.py @@ -0,0 +1,95 @@ +import pytest +from unittest import mock + +from ..__model import constants as CONSTANTS +from ..__model.accelerometer import Accelerometer + + +class TestAccelerometer(object): + def setup_method(self): + self.accelerometer = Accelerometer() + + @pytest.mark.parametrize( + "accel, expected", + [ + (CONSTANTS.MIN_ACCELERATION - 10, CONSTANTS.MIN_ACCELERATION), + (CONSTANTS.MIN_ACCELERATION, CONSTANTS.MIN_ACCELERATION), + (100, 100), + (CONSTANTS.MAX_ACCELERATION, CONSTANTS.MAX_ACCELERATION), + (CONSTANTS.MAX_ACCELERATION + 1, CONSTANTS.MAX_ACCELERATION), + ], + ) + def test_x_y_z(self, accel, expected): + self.accelerometer._Accelerometer__set_x(accel) + assert expected == self.accelerometer.get_x() + self.accelerometer._Accelerometer__set_y(accel) + assert expected == self.accelerometer.get_y() + self.accelerometer._Accelerometer__set_z(accel) + assert expected == self.accelerometer.get_z() + + @pytest.mark.parametrize( + "accels, expected", + [ + ((23, 25, 26), (23, 25, 26)), + ((204, 234, -534), (204, 234, -534)), + ( + (CONSTANTS.MIN_ACCELERATION - 10, 234, CONSTANTS.MAX_ACCELERATION), + (CONSTANTS.MIN_ACCELERATION, 234, CONSTANTS.MAX_ACCELERATION), + ), + ], + ) + def test_get_values(self, accels, expected): + self.accelerometer._Accelerometer__set_x(accels[0]) + self.accelerometer._Accelerometer__set_y(accels[1]) + self.accelerometer._Accelerometer__set_z(accels[2]) + assert expected == self.accelerometer.get_values() + + @pytest.mark.parametrize("gesture", ["up", "face down", "freefall", "8g"]) + def test_current_gesture(self, gesture): + self.accelerometer._Accelerometer__set_gesture(gesture) + assert gesture == self.accelerometer.current_gesture() + + @pytest.mark.parametrize("gesture", ["up", "face down", "freefall", "8g"]) + def test_is_gesture(self, gesture): + self.accelerometer._Accelerometer__set_gesture(gesture) + assert self.accelerometer.is_gesture(gesture) + for g in CONSTANTS.GESTURES: + if g != gesture: + assert not self.accelerometer.is_gesture(g) + + def test_is_gesture_error(self): + with pytest.raises(ValueError): + self.accelerometer.is_gesture("sideways") + + def test_was_gesture(self): + mock_gesture_up = "up" + mock_gesture_down = "down" + + assert not self.accelerometer.was_gesture(mock_gesture_up) + self.accelerometer._Accelerometer__set_gesture(mock_gesture_up) + self.accelerometer.current_gesture() # Call is needed for gesture detection so it can be added to the lists. + self.accelerometer._Accelerometer__set_gesture("") + assert self.accelerometer.was_gesture(mock_gesture_up) + assert not self.accelerometer.was_gesture(mock_gesture_up) + + def test_was_gesture_error(self): + with pytest.raises(ValueError): + self.accelerometer.was_gesture("sideways") + + def test_get_gestures(self): + mock_gesture_up = "up" + mock_gesture_down = "down" + mock_gesture_freefall = "freefall" + self.accelerometer._Accelerometer__set_gesture(mock_gesture_up) + self.accelerometer.current_gesture() # Call is needed for gesture detection so it can be added to the lists. + self.accelerometer._Accelerometer__set_gesture(mock_gesture_down) + self.accelerometer.current_gesture() + self.accelerometer._Accelerometer__set_gesture(mock_gesture_freefall) + self.accelerometer.current_gesture() + self.accelerometer._Accelerometer__set_gesture("") + assert ( + mock_gesture_up, + mock_gesture_down, + mock_gesture_freefall, + ) == self.accelerometer.get_gestures() + assert () == self.accelerometer.get_gestures() diff --git a/src/microbit/test/test_display.py b/src/microbit/test/test_display.py index fc5c0ab34..dc5beafb1 100644 --- a/src/microbit/test/test_display.py +++ b/src/microbit/test/test_display.py @@ -157,6 +157,20 @@ def test_async_tests(self): self.display.show("6", delay=0) assert Image._Image__same_image(Image(STR_SIX), self.display._Display__image) + @pytest.mark.parametrize( + "light_level, expected", + [ + (CONSTANTS.MIN_LIGHT_LEVEL - 10, CONSTANTS.MIN_LIGHT_LEVEL), + (CONSTANTS.MIN_LIGHT_LEVEL, CONSTANTS.MIN_LIGHT_LEVEL), + (100, 100), + (CONSTANTS.MAX_LIGHT_LEVEL, CONSTANTS.MAX_LIGHT_LEVEL), + (CONSTANTS.MAX_LIGHT_LEVEL + 10, CONSTANTS.MAX_LIGHT_LEVEL), + ], + ) + def test_temperature(self, light_level, expected): + self.display._Display__set_light_level(light_level) + assert expected == self.display.read_light_level() + # Helpers def __is_clear(self): i = Image(CONSTANTS.BLANK_5X5) diff --git a/src/microbit/test/test_microbit_model.py b/src/microbit/test/test_microbit_model.py index 4ce6ca0a2..a6436ec71 100644 --- a/src/microbit/test/test_microbit_model.py +++ b/src/microbit/test/test_microbit_model.py @@ -3,6 +3,7 @@ import pytest from unittest import mock from ..__model.microbit_model import MicrobitModel +from ..__model import constants as CONSTANTS class TestMicrobitModel(object): @@ -24,3 +25,17 @@ def test_running_time(self): assert mock_end_time - mock_start_time == pytest.approx( self.__mb.running_time() ) + + @pytest.mark.parametrize( + "temperature, expected", + [ + (CONSTANTS.MIN_TEMPERATURE - 10, CONSTANTS.MIN_TEMPERATURE), + (CONSTANTS.MIN_TEMPERATURE, CONSTANTS.MIN_TEMPERATURE), + (0, 0), + (CONSTANTS.MAX_TEMPERATURE, CONSTANTS.MAX_TEMPERATURE), + (CONSTANTS.MAX_TEMPERATURE + 5, CONSTANTS.MAX_TEMPERATURE), + ], + ) + def test_temperature(self, temperature, expected): + self.__mb._MicrobitModel__set_temperature(temperature) + assert expected == self.__mb.temperature() diff --git a/src/microbit/test/test_shim.py b/src/microbit/test/test_shim.py index 25c1afac8..fb25d2c16 100644 --- a/src/microbit/test/test_shim.py +++ b/src/microbit/test/test_shim.py @@ -20,3 +20,8 @@ def test_running_time(self): MicrobitModel.running_time = mock.Mock() running_time() MicrobitModel.running_time.assert_called_once() + + def test_temperature(self): + MicrobitModel.temperature = mock.Mock() + temperature() + MicrobitModel.temperature.asser_called_once()