diff --git a/README.md b/README.md index bb9183b20..f54889b18 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Azure DevOps Board Badge -Make without limit! Device Simulator Express, a Microsoft Garage project, allows you to code microcontrollers without the hardware on hand! You can program your Adafruit Circuit Playground Express (CPX) or your BBC micro:bit! Test and debug your code on the device simulator and see the same +Make without limit! Device Simulator Express, a Microsoft Garage project, allows you to code microcontrollers without the hardware on hand! You can program your Adafruit Circuit Playground Express (CPX), your BBC micro:bit or the Adafruit CLUE! Test and debug your code on the device simulator and see the same result when you plug in your actual microcontroller. Curious about the output of the device, the serial monitor allows you to observe the device output. @@ -18,7 +18,7 @@ monitor allows you to observe the device output. [bbc micro:bit](#bbc-microbit-simulator) -- [**Adafruit CLUE**](#adafruit-clue-simulator) (hidden behind preview flag) +- [**Adafruit CLUE**](#adafruit-clue-simulator) [Adafruit CLUE](#adafruit-clue-simulator) @@ -120,8 +120,6 @@ In Device Simulator Express, you can use keyboard to interact with the device: ## Adafruit CLUE Simulator -NOTE: This simulator is hidden under the preview mode flag. See below on how to enable the preview mode flag. - ### Features - IntelliSense and syntax highlighting for CircuitPython code for the following drivers and libraries: @@ -164,22 +162,6 @@ NOTE: This simulator is hidden under the preview mode flag. See below on how to - Refresh the simulator: Shift + R - Run the simulator: Shift + F -## How to enable preview flag - -Currently, we have our Adafruit CLUE simulator hidden behind a preview flag and we want you to try it out! - -### I. Open settings - -For Windows and Linux, you can use Ctrl + , or use `File -> Preferences -> Settings` in the top menu to navigate to settings. For Mac, you can use Cmd + , or use `Code -> Preferences -> Settings`. - - Adafruit CLUE - -### II. Search for our preview flag and enable it! - -In the top search bar, search for `DeviceSimulatorExpress.previewMode`. Then, check the checkbox for the setting that pops up when you serach. - - Adafruit CLUE - ## How to use To use Device Simulator Express, install the extension from the marketplace and reload VS Code. @@ -225,8 +207,12 @@ Before deploying the Python code to your CPX device, you need to format your dev - Download the lastest versions of the cpx libraries (link: https://learn.adafruit.com/welcome-to-circuitpython/circuitpython-libraries). - _For the micro:bit_: + - Download the firmware with the .hex file (link: https://microbit.org/get-started/user-guide/firmware/). +- _For the CLUE_: + - Download the latest versions of the cpx libraries and follow the instructions here (link:https://learn.adafruit.com/adafruit-clue/circuitpython). + 1. Plug in your device (make sure it’s formatted properly already). 2. Run the command `"Device Simulator Express: Deploy to Device"`. @@ -305,6 +291,7 @@ A `ThirdPartyNotices.txt` file is provided in the extension's source code listin - If you try to deploy to the CPX while it's plugged in but you still get an error saying it cannot find the board, make sure your device is formatted correctly and that its name matches `CIRCUITPY`. - If you can't get the Simulator communication working while debugging, try to open your `Settings` and check the port used under `"Device Simulator Express: Debugger Server Port"`. You can either change it (usually ports above 5000 should work) or try to free it, then start debugging again. - When you are using the serial monitor, if you get some unusual error messages, unplug the device and reload the VS Code windows. +- If you're using Ubuntu and having some problems with setting up the environment, try reviewing [this article's](https://www.digitalocean.com/community/tutorials/how-to-install-python-3-and-set-up-a-local-programming-environment-on-ubuntu-16-04) "Step 1" section on how to set up Python 3 on Ubuntu 16.04. Then, ensure that you've run `sudo apt-get install -y python3-venv` to allow for virtual environment creation. ## License diff --git a/adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz b/adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz deleted file mode 100644 index 85b10a468..000000000 Binary files a/adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz and /dev/null differ diff --git a/locales/en/package.i18n.json b/locales/en/package.i18n.json index 86e67f9b4..4f34f2536 100644 --- a/locales/en/package.i18n.json +++ b/locales/en/package.i18n.json @@ -13,6 +13,5 @@ "deviceSimulatorExpressExtension.configuration.title": "Device Simulator Express configuration", "deviceSimulatorExpressExtension.configuration.properties.configEnvOnChange": "When you change the Python interpreter, the Device Simulator Express will automatically configure itself for the required dependencies.", "deviceSimulatorExpressExtension.configuration.properties.debuggerPort": "The port the Server will listen on for communication with the debugger.", - "deviceSimulatorExpressExtension.configuration.properties.dependencyChecker": "Whether or not to ask if we can download dependencies. If unchecked, the extension will default to never download dependencies, except when automatically creating a virtual environment in the extension files.", - "deviceSimulatorExpressExtension.configuration.properties.previewMode": "Enable this to test out and play with the new Adafruit CLUE simulator!" + "deviceSimulatorExpressExtension.configuration.properties.dependencyChecker": "Whether or not to ask if we can download dependencies. If unchecked, the extension will default to never download dependencies, except when automatically creating a virtual environment in the extension files." } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e2b169c9b..777c7b6d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30807,9 +30807,9 @@ } }, "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, "typescript-react-intl": { diff --git a/package.json b/package.json index cc1b237bd..5a8555f52 100644 --- a/package.json +++ b/package.json @@ -144,12 +144,6 @@ "default": 5577, "description": "%deviceSimulatorExpressExtension.configuration.properties.debuggerPort%", "scope": "resource" - }, - "deviceSimulatorExpress.previewMode": { - "type": "boolean", - "default": false, - "description": "%deviceSimulatorExpressExtension.configuration.properties.previewMode%", - "scope": "resource" } } }, @@ -286,7 +280,7 @@ "tslint-microsoft-contrib": "^6.1.0", "tslint-react": "^3.6.0", "tslint-react-hooks": "^2.0.0", - "typescript": "^3.3.1", + "typescript": "^3.8.3", "typescript-react-intl": "^0.4.0", "version-from-git": "^1.1.1", "vsce": "^1.47.0", diff --git a/package.nls.json b/package.nls.json index ea3ad1667..e662c005a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -13,6 +13,5 @@ "deviceSimulatorExpressExtension.configuration.title": "Device Simulator Express configuration", "deviceSimulatorExpressExtension.configuration.properties.configEnvOnChange": "When you change the Python interpreter, the Device Simulator Express will automatically configure itself for the required dependencies.", "deviceSimulatorExpressExtension.configuration.properties.debuggerPort": "The port the Server will listen on for communication with the debugger.", - "deviceSimulatorExpressExtension.configuration.properties.dependencyChecker": "Whether or not to ask for dependency downloads. If unchecked, the extension will default to never download dependencies, except when automatically creating a virtual environment in the extension files.", - "deviceSimulatorExpressExtension.configuration.properties.previewMode": "Enable this to test out and play with the new Adafruit CLUE simulator!" + "deviceSimulatorExpressExtension.configuration.properties.dependencyChecker": "Whether or not to ask for dependency downloads. If unchecked, the extension will default to never download dependencies, except when automatically creating a virtual environment in the extension files." } \ No newline at end of file diff --git a/src/adafruit_circuitplayground/constants.py b/src/adafruit_circuitplayground/constants.py index a96083795..2c47a919d 100644 --- a/src/adafruit_circuitplayground/constants.py +++ b/src/adafruit_circuitplayground/constants.py @@ -1,6 +1,25 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. + +class EXPRESS_STATE: + BUTTON_A = "button_a" + BUTTON_B = "button_b" + ACCELERATION = "acceleration" + BRIGHTNESS = "brightness" + PIXELS = "pixels" + RED_LED = "red_led" + SWITCH = "switch" + TEMPERATURE = "temperature" + LIGHT = "light" + MOTION_X = "motion_x" + MOTION_Y = "motion_y" + MOTION_Z = "motion_z" + TOUCH = "touch" + SHAKE = "shake" + DETECT_TAPS = "detect_taps" + + ASSIGN_PIXEL_TYPE_ERROR = ( "The pixel color value type should be tuple, list or hexadecimal." ) @@ -32,18 +51,28 @@ TIME_DELAY = 0.03 -EVENTS_BUTTON_PRESS = ["button_a", "button_b", "switch"] -EVENTS_SENSOR_CHANGED = ["temperature", "light", "motion_x", "motion_y", "motion_z"] +EVENTS_BUTTON_PRESS = [ + EXPRESS_STATE.BUTTON_A, + EXPRESS_STATE.BUTTON_B, + EXPRESS_STATE.SWITCH, +] +EVENTS_SENSOR_CHANGED = [ + EXPRESS_STATE.TEMPERATURE, + EXPRESS_STATE.LIGHT, + EXPRESS_STATE.MOTION_X, + EXPRESS_STATE.MOTION_Y, + EXPRESS_STATE.MOTION_Z, +] ALL_EXPECTED_INPUT_EVENTS = [ - "button_a", - "button_b", - "switch", - "temperature", - "light", - "shake", - "motion_x", - "motion_y", - "motion_z", - "touch", + EXPRESS_STATE.BUTTON_A, + EXPRESS_STATE.BUTTON_B, + EXPRESS_STATE.SWITCH, + EXPRESS_STATE.TEMPERATURE, + EXPRESS_STATE.LIGHT, + EXPRESS_STATE.SHAKE, + EXPRESS_STATE.MOTION_X, + EXPRESS_STATE.MOTION_Y, + EXPRESS_STATE.MOTION_Z, + EXPRESS_STATE.TOUCH, ] diff --git a/src/adafruit_circuitplayground/express.py b/src/adafruit_circuitplayground/express.py index 40b82adff..7f2857508 100644 --- a/src/adafruit_circuitplayground/express.py +++ b/src/adafruit_circuitplayground/express.py @@ -14,67 +14,69 @@ from collections import namedtuple import common -Acceleration = namedtuple("acceleration", ["x", "y", "z"]) +Acceleration = namedtuple(CONSTANTS.EXPRESS_STATE.ACCELERATION, ["x", "y", "z"]) class Express: def __init__(self): # State in the Python process - self.__state = { - "brightness": 1.0, - "button_a": False, - "button_b": False, - "pixels": [ - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - ], - "red_led": False, - "switch": False, - "temperature": 0, - "light": 0, - "motion_x": 0, - "motion_y": 0, - "motion_z": 0, - "touch": [False] * 7, - "shake": False, - } + self.__state = {} + self.__state[CONSTANTS.EXPRESS_STATE.BRIGHTNESS] = 1.0 + self.__state[CONSTANTS.EXPRESS_STATE.BUTTON_A] = False + self.__state[CONSTANTS.EXPRESS_STATE.BUTTON_B] = False + self.__state[CONSTANTS.EXPRESS_STATE.PIXELS] = [ + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + ] + self.__state[CONSTANTS.EXPRESS_STATE.RED_LED] = False + self.__state[CONSTANTS.EXPRESS_STATE.SWITCH] = False + self.__state[CONSTANTS.EXPRESS_STATE.TEMPERATURE] = 0 + self.__state[CONSTANTS.EXPRESS_STATE.LIGHT] = 0 + self.__state[CONSTANTS.EXPRESS_STATE.MOTION_X] = 0 + self.__state[CONSTANTS.EXPRESS_STATE.MOTION_Y] = 0 + self.__state[CONSTANTS.EXPRESS_STATE.MOTION_Z] = 0 + self.__state[CONSTANTS.EXPRESS_STATE.TOUCH] = [False] * 7 + self.__state[CONSTANTS.EXPRESS_STATE.SHAKE] = False + self.__state[CONSTANTS.EXPRESS_STATE.DETECT_TAPS] = 0 self.pixels = Pixel(self.__state) @property def acceleration(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_ACCELERATION) return Acceleration( - self.__state["motion_x"], self.__state["motion_y"], self.__state["motion_z"] + self.__state[CONSTANTS.EXPRESS_STATE.MOTION_X], + self.__state[CONSTANTS.EXPRESS_STATE.MOTION_Y], + self.__state[CONSTANTS.EXPRESS_STATE.MOTION_Z], ) @property def button_a(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_BUTTON_A) - return self.__state["button_a"] + return self.__state[CONSTANTS.EXPRESS_STATE.BUTTON_A] @property def button_b(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_BUTTON_B) - return self.__state["button_b"] + return self.__state[CONSTANTS.EXPRESS_STATE.BUTTON_B] @property def detect_taps(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_DETECT_TAPS) - return self.__state["detect_taps"] + return self.__state[CONSTANTS.EXPRESS_STATE.DETECT_TAPS] @detect_taps.setter def detect_taps(self, value): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_DETECT_TAPS) value_int = int(value) - self.__state["detect_taps"] = ( + self.__state[CONSTANTS.EXPRESS_STATE.DETECT_TAPS] = ( value_int if (value_int == 1 or value_int == 2) else 1 ) @@ -88,28 +90,28 @@ def tapped(self): @property def red_led(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_RED_LED) - return self.__state["red_led"] + return self.__state[CONSTANTS.EXPRESS_STATE.RED_LED] @red_led.setter def red_led(self, value): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_RED_LED) - self.__state["red_led"] = bool(value) + self.__state[CONSTANTS.EXPRESS_STATE.RED_LED] = bool(value) self.__show() @property def switch(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_SWITCH) - return self.__state["switch"] + return self.__state[CONSTANTS.EXPRESS_STATE.SWITCH] @property def temperature(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_TEMPERATURE) - return self.__state["temperature"] + return self.__state[CONSTANTS.EXPRESS_STATE.TEMPERATURE] @property def light(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_LIGHT) - return self.__state["light"] + return self.__state[CONSTANTS.EXPRESS_STATE.LIGHT] def __show(self): if utils.debug_mode: @@ -121,7 +123,7 @@ def __show(self): def __touch(self, i): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_TOUCH) - return self.__state["touch"][i - 1] + return self.__state[CONSTANTS.EXPRESS_STATE.TOUCH][i - 1] @property def touch_A1(self): @@ -160,7 +162,7 @@ def adjust_touch_threshold(self, adjustment): def shake(self, shake_threshold=30): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_SHAKE) - return self.__state["shake"] + return self.__state[CONSTANTS.EXPRESS_STATE.SHAKE] def play_file(self, file_name): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_PLAY_FILE) diff --git a/src/adafruit_circuitplayground/pixel.py b/src/adafruit_circuitplayground/pixel.py index 758084f59..22fcfc8ae 100644 --- a/src/adafruit_circuitplayground/pixel.py +++ b/src/adafruit_circuitplayground/pixel.py @@ -36,7 +36,7 @@ def __getitem__(self, index): if not self.__valid_index(index): raise IndexError(CONSTANTS.INDEX_ERROR) telemetry_py.send_telemetry(TelemetryEvent.CPX_API_PIXELS) - return self.__state["pixels"][index] + return self.__state[CONSTANTS.EXPRESS_STATE.PIXELS][index] def __setitem__(self, index, val): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_PIXELS) @@ -46,11 +46,13 @@ def __setitem__(self, index, val): else: if not self.__valid_index(index): raise IndexError(CONSTANTS.INDEX_ERROR) - self.__state["pixels"][index] = self.__extract_pixel_value(val, is_slice) + self.__state[CONSTANTS.EXPRESS_STATE.PIXELS][ + index + ] = self.__extract_pixel_value(val, is_slice) self.__show_if_auto_write() def __iter__(self): - yield from self.__state["pixels"] + yield from self.__state[CONSTANTS.EXPRESS_STATE.PIXELS] def __enter__(self): return self @@ -59,18 +61,20 @@ def __repr__(self): return "[" + ", ".join([str(x) for x in self]) + "]" def __len__(self): - return len(self.__state["pixels"]) + return len(self.__state[CONSTANTS.EXPRESS_STATE.PIXELS]) def __valid_index(self, index): return ( type(index) is int - and index >= -len(self.__state["pixels"]) - and index < len(self.__state["pixels"]) + and index >= -len(self.__state[CONSTANTS.EXPRESS_STATE.PIXELS]) + and index < len(self.__state[CONSTANTS.EXPRESS_STATE.PIXELS]) ) def fill(self, val): - for index in range(len(self.__state["pixels"])): - self.__state["pixels"][index] = self.__extract_pixel_value(val) + for index in range(len(self.__state[CONSTANTS.EXPRESS_STATE.PIXELS])): + self.__state[CONSTANTS.EXPRESS_STATE.PIXELS][ + index + ] = self.__extract_pixel_value(val) self.__show_if_auto_write() def __extract_pixel_value(self, val, is_slice=False): @@ -113,14 +117,14 @@ def __valid_rgb_value(self, pixValue): @property def brightness(self): telemetry_py.send_telemetry(TelemetryEvent.CPX_API_BRIGHTNESS) - return self.__state["brightness"] + return self.__state[CONSTANTS.EXPRESS_STATE.BRIGHTNESS] @brightness.setter def brightness(self, brightness): if not self.__valid_brightness(brightness): raise ValueError(CONSTANTS.BRIGHTNESS_RANGE_ERROR) telemetry_py.send_telemetry(TelemetryEvent.CPX_API_BRIGHTNESS) - self.__state["brightness"] = brightness + self.__state[CONSTANTS.EXPRESS_STATE.BRIGHTNESS] = brightness self.__show_if_auto_write() def __valid_brightness(self, brightness): diff --git a/src/adafruit_circuitplayground/test/test_express.py b/src/adafruit_circuitplayground/test/test_express.py index b83ba3a23..0732d9641 100644 --- a/src/adafruit_circuitplayground/test/test_express.py +++ b/src/adafruit_circuitplayground/test/test_express.py @@ -4,19 +4,19 @@ import playsound from ..express import Express from ..pixel import Pixel +from .. import constants as CONSTANTS class TestExpress(object): def setup_method(self): self.cpx = Express() - self.__state = { - "brightness": 1.0, - "button_a": False, - "button_b": False, - "pixels": [(255, 0, 0)] * 10, - "red_led": False, - "switch": False, - } + self.__state = {} + self.__state[CONSTANTS.EXPRESS_STATE.BRIGHTNESS] = 1.0 + self.__state[CONSTANTS.EXPRESS_STATE.BUTTON_A] = False + self.__state[CONSTANTS.EXPRESS_STATE.BUTTON_B] = False + self.__state[CONSTANTS.EXPRESS_STATE.PIXELS] = [(255, 0, 0)] * 10 + self.__state[CONSTANTS.EXPRESS_STATE.RED_LED] = False + self.__state[CONSTANTS.EXPRESS_STATE.SWITCH] = False self.pixels = Pixel(self.__state) self.__speaker_enabled = False @@ -24,24 +24,24 @@ def test_acceleration(self): mock_motion_x = 10 mock_motion_y = -10 mock_motion_z = -20 - self.cpx._Express__state["motion_x"] = mock_motion_x - self.cpx._Express__state["motion_y"] = mock_motion_y - self.cpx._Express__state["motion_z"] = mock_motion_z + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.MOTION_X] = mock_motion_x + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.MOTION_Y] = mock_motion_y + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.MOTION_Z] = mock_motion_z accel = self.cpx.acceleration assert accel[0] == 10 assert accel[1] == -10 assert accel[2] == -20 def test_button_a(self): - self.cpx._Express__state["button_a"] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.BUTTON_A] = True assert self.cpx.button_a def test_button_b(self): - self.cpx._Express__state["button_b"] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.BUTTON_B] = True assert self.cpx.button_b def test_taps(self): - self.cpx._Express__state["detect_taps"] = 2 + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.DETECT_TAPS] = 2 assert 2 == self.cpx.detect_taps @pytest.mark.parametrize("taps, expected", [(1, 1), (2, 2), (3, 1)]) @@ -50,7 +50,7 @@ def test_taps_setter(self, taps, expected): assert expected == self.cpx.detect_taps def test_red_led(self): - self.cpx._Express__state["red_led"] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.RED_LED] = True assert self.cpx.red_led def test_red_led_int(self): @@ -62,47 +62,47 @@ def test_red_led_string(self): assert self.cpx.red_led def test_switch(self): - self.cpx._Express__state["switch"] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.SWITCH] = True assert self.cpx.switch def test_temperature(self): - self.cpx._Express__state["temperature"] = 31 + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TEMPERATURE] = 31 assert 31 == self.cpx.temperature def test_light(self): - self.cpx._Express__state["light"] = 255 + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.LIGHT] = 255 assert 255 == self.cpx.light def test_shake(self): - self.cpx._Express__state["shake"] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.SHAKE] = True assert self.cpx.shake() def test_touch_A1(self): - self.cpx._Express__state["touch"][0] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TOUCH][0] = True assert self.cpx.touch_A1 def test_touch_A2(self): - self.cpx._Express__state["touch"][1] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TOUCH][1] = True assert self.cpx.touch_A2 def test_touch_A3(self): - self.cpx._Express__state["touch"][2] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TOUCH][2] = True assert self.cpx.touch_A3 def test_touch_A4(self): - self.cpx._Express__state["touch"][3] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TOUCH][3] = True assert self.cpx.touch_A4 def test_touch_A5(self): - self.cpx._Express__state["touch"][4] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TOUCH][4] = True assert self.cpx.touch_A5 def test_touch_A6(self): - self.cpx._Express__state["touch"][5] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TOUCH][5] = True assert self.cpx.touch_A6 def test_touch_A7(self): - self.cpx._Express__state["touch"][6] = True + self.cpx._Express__state[CONSTANTS.EXPRESS_STATE.TOUCH][6] = True assert self.cpx.touch_A7 def test_play_file_mp4_wrong_type(self): diff --git a/src/adafruit_circuitplayground/test/test_pixel.py b/src/adafruit_circuitplayground/test/test_pixel.py index 521958552..e1179939b 100644 --- a/src/adafruit_circuitplayground/test/test_pixel.py +++ b/src/adafruit_circuitplayground/test/test_pixel.py @@ -1,20 +1,19 @@ import pytest from ..pixel import Pixel +from .. import constants as CONSTANTS class TestPixel(object): def setup_method(self): - self.pixel = Pixel( - { - "brightness": 1.0, - "button_a": False, - "button_b": False, - "pixels": [(255, 0, 0), (0, 255, 0), (0, 0, 255)], - "red_led": False, - "switch": False, - } - ) + state = {} + state[CONSTANTS.EXPRESS_STATE.BRIGHTNESS] = 1.0 + state[CONSTANTS.EXPRESS_STATE.BUTTON_A] = False + state[CONSTANTS.EXPRESS_STATE.BUTTON_B] = False + state[CONSTANTS.EXPRESS_STATE.PIXELS] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)] + state[CONSTANTS.EXPRESS_STATE.RED_LED] = False + state[CONSTANTS.EXPRESS_STATE.SWITCH] = False + self.pixel = Pixel(state) def test_get_item_out_of_bounds(self): with pytest.raises(IndexError): @@ -52,7 +51,10 @@ def test_valid_index(self, index, expected): def test_fill(self): self.pixel.fill((123, 123, 123)) - assert all(p == (123, 123, 123) for p in self.pixel._Pixel__state["pixels"]) + assert all( + p == (123, 123, 123) + for p in self.pixel._Pixel__state[CONSTANTS.EXPRESS_STATE.PIXELS] + ) @pytest.mark.parametrize( "val, expected", @@ -101,7 +103,7 @@ def test_valid_rgb_value(self, pixValue, expected): assert expected == self.pixel._Pixel__valid_rgb_value(pixValue) def test_get_brightness(self): - self.pixel._Pixel__state["brightness"] = 0.4 + self.pixel._Pixel__state[CONSTANTS.EXPRESS_STATE.BRIGHTNESS] = 0.4 assert 0.4 == pytest.approx(self.pixel.brightness) @pytest.mark.parametrize("brightness", [-0.1, 1.1]) diff --git a/src/base_circuitpython/__init__.py b/src/base_circuitpython/__init__.py index 5b5bbaf34..398a3936b 100644 --- a/src/base_circuitpython/__init__.py +++ b/src/base_circuitpython/__init__.py @@ -3,4 +3,6 @@ import sys abs_path = pathlib.Path(__file__).parent.absolute() +clue_path = os.path.join(abs_path, "../clue") sys.path.insert(0, os.path.join(abs_path)) +sys.path.insert(0, os.path.join(clue_path)) diff --git a/src/base_circuitpython/displayio/constants.py b/src/base_circuitpython/displayio/constants.py index e683684b9..bca672472 100644 --- a/src/base_circuitpython/displayio/constants.py +++ b/src/base_circuitpython/displayio/constants.py @@ -4,6 +4,7 @@ INCORR_SUBCLASS = "Layer must be a Group or TileGrid subclass." LAYER_ALREADY_IN_GROUP = "Layer already in a group." GROUP_FULL = "Group Full" +SCALE_TOO_SMALL = "scale must be >= 1" SCREEN_HEIGHT_WIDTH = 240 diff --git a/src/base_circuitpython/displayio/group.py b/src/base_circuitpython/displayio/group.py index 4704c67cf..c5dccf90a 100644 --- a/src/base_circuitpython/displayio/group.py +++ b/src/base_circuitpython/displayio/group.py @@ -8,6 +8,7 @@ import common import board +import sys # Group implementation loosely based on the # displayio.Group class in Adafruit CircuitPython @@ -33,12 +34,22 @@ class Group: """ def __init__( - self, max_size, scale=1, x=0, y=0, check_active_group_ref=True, auto_write=True + self, + max_size=sys.maxsize, + scale=1, + x=0, + y=0, + check_active_group_ref=True, + auto_write=True, ): self.__check_active_group_ref = check_active_group_ref self.__auto_write = auto_write self.__contents = [] self.__max_size = max_size + + if scale < 1: + raise ValueError(CONSTANTS.SCALE_TOO_SMALL) + self.__scale = scale """ .. attribute:: scale @@ -89,6 +100,9 @@ def scale(self): @scale.setter def scale(self, val): + if val < 1: + raise ValueError(CONSTANTS.SCALE_TOO_SMALL) + if self.__scale != val: self.__scale = val self.__elem_changed() diff --git a/src/base_circuitpython/displayio/test/test_group.py b/src/base_circuitpython/displayio/test/test_group.py index 7eee71159..4d943ffc8 100644 --- a/src/base_circuitpython/displayio/test/test_group.py +++ b/src/base_circuitpython/displayio/test/test_group.py @@ -53,6 +53,11 @@ def test_incorr_subclass(self, group_item): with pytest.raises(ValueError, match=CONSTANTS.INCORR_SUBCLASS): g1.append(group_item) + @pytest.mark.parametrize("scale", [(0), (-2)]) + def test_invalid_scale(self, scale): + with pytest.raises(ValueError, match=CONSTANTS.SCALE_TOO_SMALL): + g1 = Group(scale=scale) + def test_layer_already_in_group(self): g1 = Group(max_size=4) diff --git a/src/clue/adafruit_clue.py b/src/clue/adafruit_clue.py index 874e50e46..059573021 100644 --- a/src/clue/adafruit_clue.py +++ b/src/clue/adafruit_clue.py @@ -56,9 +56,7 @@ https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel """ -from common.telemetry_events import TelemetryEvent -from common.telemetry import telemetry_py -from common import utils +import common from base_circuitpython import base_cp_constants as CONSTANTS import neopixel import time @@ -115,8 +113,8 @@ def __init__( self._font = terminalio.FONT if font: self._font = font - self.text_group = displayio.Group(max_size=20, scale=text_scale) self.text_scale = text_scale + self.text_group = displayio.Group(max_size=20, scale=self.text_scale) if title: # Fail gracefully if title is longer than 60 characters. if len(title) > 60: @@ -130,12 +128,12 @@ def __init__( scale=title_scale, ) title.x = 0 - title.y = 8 - self._y = title.y + 18 * text_scale + title.y = 8 * self.text_scale + self._y = title.y + 18 * self.text_scale self.text_group.append(title) else: - self._y = 3 + self._y = 3 * self.text_scale self._lines = [] for num in range(1): @@ -205,7 +203,7 @@ def __init__(self): self.__state[CONSTANTS.CLUE_STATE.PROXIMITY] = 0 self.__state[CONSTANTS.CLUE_STATE.GESTURE] = "" self.__state[CONSTANTS.CLUE_STATE.HUMIDITY] = 0 - self.__state[CONSTANTS.CLUE_STATE.PRESSURE] = 0 + self.__state[CONSTANTS.CLUE_STATE.PRESSURE] = 1013 self.__state[CONSTANTS.CLUE_STATE.PIXEL] = neopixel.NeoPixel( pin=CONSTANTS.CLUE_PIN, n=1, pixel_order=neopixel.RGB ) @@ -247,7 +245,9 @@ def button_a(self): if clue.button_a: print("Button A pressed") """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_BUTTON_A) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_BUTTON_A + ) return self.__state[CONSTANTS.CLUE_STATE.BUTTON_A] @property @@ -261,7 +261,9 @@ def button_b(self): if clue.button_b: print("Button B pressed") """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_BUTTON_B) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_BUTTON_B + ) return self.__state[CONSTANTS.CLUE_STATE.BUTTON_B] @property @@ -273,7 +275,9 @@ def were_pressed(self): while True: print(clue.were_pressed) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_WERE_PRESSED) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_WERE_PRESSED + ) ret = self.__state[CONSTANTS.CLUE_STATE.PRESSED_BUTTONS].copy() self.__state[CONSTANTS.CLUE_STATE.PRESSED_BUTTONS].clear() return ret @@ -288,7 +292,9 @@ def acceleration(self): while True: print("Accel: {:.2f} {:.2f} {:.2f}".format(*clue.acceleration)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_ACCELERATION) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_ACCELERATION + ) return ( self.__state[CONSTANTS.CLUE_STATE.MOTION_X], self.__state[CONSTANTS.CLUE_STATE.MOTION_Y], @@ -307,7 +313,9 @@ def shake(self, shake_threshold=30, avg_count=10, total_delay=0.1): :param total_delay: The total time in seconds it takes to obtain avg_count readings from acceleration. (Default 0.1) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SHAKE) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SHAKE + ) is_shaken = self.__state[CONSTANTS.CLUE_STATE.GESTURE] == CONSTANTS.SHAKE return is_shaken @@ -322,7 +330,9 @@ def color(self): while True: print("Color: R: {} G: {} B: {} C: {}".format(*clue.color)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_COLOR) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_COLOR + ) return ( self.__state[CONSTANTS.CLUE_STATE.LIGHT_R], self.__state[CONSTANTS.CLUE_STATE.LIGHT_G], @@ -339,7 +349,9 @@ def temperature(self): from adafruit_clue import clue print("Temperature: {:.1f}C".format(clue.temperature)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_TEMPERATURE) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_TEMPERATURE + ) return self.__state[CONSTANTS.CLUE_STATE.TEMPERATURE] @property @@ -352,7 +364,9 @@ def magnetic(self): while True: print("Magnetic: {:.3f} {:.3f} {:.3f}".format(*clue.magnetic)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_MAGNETIC) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_MAGNETIC + ) return ( self.__state[CONSTANTS.CLUE_STATE.MAGNET_X], self.__state[CONSTANTS.CLUE_STATE.MAGNET_Y], @@ -370,7 +384,9 @@ def proximity(self): while True: print("Proximity: {}".format(clue.proximity)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_PROXIMITY) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_PROXIMITY + ) return self.__state[CONSTANTS.CLUE_STATE.PROXIMITY] @property @@ -379,7 +395,9 @@ def gyro(self): This example prints the values. Try moving the board to see how the printed values change. print("Gyro: {:.2f} {:.2f} {:.2f}".format(*clue.gyro)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_GYRO) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_GYRO + ) return ( self.__state[CONSTANTS.CLUE_STATE.GYRO_X], self.__state[CONSTANTS.CLUE_STATE.GYRO_Y], @@ -398,7 +416,9 @@ def gesture(self): while True: print("Gesture: {}".format(clue.gesture)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_GESTURE) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_GESTURE + ) gesture_mapping = {"": 0, "up": 1, "down": 2, "left": 3, "right": 4} return gesture_mapping.get(self.__state[CONSTANTS.CLUE_STATE.GESTURE], 0) @@ -412,7 +432,9 @@ def humidity(self): while True: print("Humidity: {:.1f}%".format(clue.humidity)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_HUMIDITY) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_HUMIDITY + ) return self.__state[CONSTANTS.CLUE_STATE.HUMIDITY] @property @@ -424,7 +446,9 @@ def pressure(self): from adafruit_clue import clue print("Pressure: {:.3f}hPa".format(clue.pressure)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_PRESSURE) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_PRESSURE + ) return self.__state[CONSTANTS.CLUE_STATE.PRESSURE] @property @@ -450,7 +474,9 @@ def altitude(self): POWER_CONSTANT, ) ) - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_ALTITUDE) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_ALTITUDE + ) return altitude @property @@ -464,12 +490,16 @@ def sea_level_pressure(self): clue.sea_level_pressure = 1015 print("Pressure: {:.3f}hPa".format(clue.pressure)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SEA_LEVEL_PRESSURE) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SEA_LEVEL_PRESSURE + ) return self.__state[CONSTANTS.CLUE_STATE.SEA_LEVEL_PRESSURE] @sea_level_pressure.setter def sea_level_pressure(self, value): - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SEA_LEVEL_PRESSURE) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SEA_LEVEL_PRESSURE + ) self.__state[CONSTANTS.CLUE_STATE.SEA_LEVEL_PRESSURE] = value @property @@ -482,7 +512,9 @@ def pixel(self): while True: clue.pixel.fill((255, 0, 255)) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_PIXEL) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_PIXEL + ) return self.__state[CONSTANTS.CLUE_STATE.PIXEL] @property @@ -500,8 +532,10 @@ def touch_0(self): if clue.touch_0: print("Touched pad 0") """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_TOUCH) - utils.print_for_unimplemented_functions(Clue.touch_0.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_TOUCH + ) + common.utils.print_for_unimplemented_functions("touch_0") @property def touch_1(self): @@ -518,8 +552,10 @@ def touch_1(self): if clue.touch_1: print("Touched pad 1") """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_TOUCH) - utils.print_for_unimplemented_functions(Clue.touch_1.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_TOUCH + ) + common.utils.print_for_unimplemented_functions("touch_1") @property def touch_2(self): @@ -536,8 +572,10 @@ def touch_2(self): if clue.touch_2: print("Touched pad 2") """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_TOUCH) - utils.print_for_unimplemented_functions(Clue.touch_2.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_TOUCH + ) + common.utils.print_for_unimplemented_functions("touch_2") @property def white_leds(self): @@ -550,12 +588,16 @@ def white_leds(self): from adafruit_clue import clue clue.white_leds = True """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_WHITE_LEDS) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_WHITE_LEDS + ) return self.__state[CONSTANTS.CLUE_STATE.WHITE_LEDS] @white_leds.setter def white_leds(self, value): - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_WHITE_LEDS) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_WHITE_LEDS + ) self.__set_leds(CONSTANTS.CLUE_STATE.WHITE_LEDS, value) @property @@ -569,12 +611,16 @@ def red_led(self): from adafruit_clue import clue clue.red_led = True """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_RED_LED) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_RED_LED + ) return self.__state[CONSTANTS.CLUE_STATE.RED_LED] @red_led.setter def red_led(self, value): - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_RED_LED) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_RED_LED + ) self.__set_leds(CONSTANTS.CLUE_STATE.RED_LED, value) def play_tone(self, frequency, duration): @@ -591,8 +637,10 @@ def play_tone(self, frequency, duration): from adafruit_clue import clue clue.play_tone(880, 1) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SOUND) - utils.print_for_unimplemented_functions(Clue.play_tone.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SOUND + ) + common.utils.print_for_unimplemented_functions(Clue.play_tone.__name__) def start_tone(self, frequency): """ Not Implemented! @@ -614,8 +662,10 @@ def start_tone(self, frequency): else: clue.stop_tone() """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SOUND) - utils.print_for_unimplemented_functions(Clue.start_tone.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SOUND + ) + common.utils.print_for_unimplemented_functions(Clue.start_tone.__name__) def stop_tone(self): """ Not Implemented! @@ -635,8 +685,10 @@ def stop_tone(self): else: clue.stop_tone() """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SOUND) - utils.print_for_unimplemented_functions(Clue.stop_tone.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SOUND + ) + common.utils.print_for_unimplemented_functions(Clue.stop_tone.__name__) @property def sound_level(self): @@ -651,8 +703,10 @@ def sound_level(self): while True: print(clue.sound_level) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SOUND) - utils.print_for_unimplemented_functions(Clue.sound_level.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SOUND + ) + common.utils.print_for_unimplemented_functions("sound_level") def loud_sound(self, sound_threshold=200): """Not Implemented! @@ -684,8 +738,10 @@ def loud_sound(self, sound_threshold=200): else: clue.pixel.fill(0) """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_SOUND) - utils.print_for_unimplemented_functions(Clue.loud_sound.__name__) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_SOUND + ) + common.utils.print_for_unimplemented_functions(Clue.loud_sound.__name__) @staticmethod def simple_text_display( @@ -745,7 +801,9 @@ def simple_text_display( clue_data[2].text = "Magnetic: {:.3f} {:.3f} {:.3f}".format(*clue.magnetic) clue_data.show() """ - telemetry_py.send_telemetry(TelemetryEvent.CLUE_API_TEXT_DISPLAY) + common.telemetry.telemetry_py.send_telemetry( + common.telemetry_events.TelemetryEvent.CLUE_API_TEXT_DISPLAY + ) return _ClueSimpleTextDisplay( title=title, title_color=title_color, @@ -775,7 +833,12 @@ def __set_leds(self, led, value): value = bool(value) self.__state[led] = value sendable_json = {led: value} - utils.send_to_simulator(sendable_json, CONSTANTS.CLUE) + if common.utils.debug_mode: + common.debugger_communication_client.debug_send_to_simulator( + sendable_json, CONSTANTS.CLUE + ) + else: + common.utils.send_to_simulator(sendable_json, CONSTANTS.CLUE) clue = Clue() # pylint: disable=invalid-name diff --git a/src/clue/adafruit_display_text/label.py b/src/clue/adafruit_display_text/label.py new file mode 100644 index 000000000..f7bab0b7f --- /dev/null +++ b/src/clue/adafruit_display_text/label.py @@ -0,0 +1,280 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Scott Shawcroft for Adafruit Industries LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_display_text.label` +==================================================== + +Displays text labels using CircuitPython's displayio. + +* Author(s): Scott Shawcroft + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import displayio + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git" + + +class Label(displayio.Group): + """A label displaying a string of text. The origin point set by ``x`` and ``y`` + properties will be the left edge of the bounding box, and in the center of a M + glyph (if its one line), or the (number of lines * linespacing + M)/2. That is, + it will try to have it be center-left as close as possible. + + :param Font font: A font class that has ``get_bounding_box`` and ``get_glyph``. + Must include a capital M for measuring character size. + :param str text: Text to display + :param int max_glyphs: The largest quantity of glyphs we will display + :param int color: Color of all text in RGB hex + :param double line_spacing: Line spacing of text to display""" + + def __init__( + self, + font, + *, + x=0, + y=0, + text=None, + max_glyphs=None, + color=0xFFFFFF, + background_color=None, + line_spacing=1.25, + **kwargs + ): + if not max_glyphs and not text: + raise RuntimeError("Please provide a max size, or initial text") + if not max_glyphs: + max_glyphs = len(text) + super().__init__(max_size=max_glyphs, auto_write=False, **kwargs) + self.width = max_glyphs + self.font = font + self._text = None + self._anchor_point = (0, 0) + self.x = x + self.y = y + + self.palette = displayio.Palette(2) + if background_color is not None: + self.palette[0] = background_color + self.palette.make_opaque(0) + self._transparent_background = False + else: + self.palette[0] = 0 + self.palette.make_transparent(0) + self._transparent_background = True + self.palette[1] = color + + bounds = self.font.get_bounding_box() + self.height = bounds[1] + self._line_spacing = line_spacing + self._boundingbox = None + + if text is not None: + self._update_text(str(text)) + + def _update_text(self, new_text): # pylint: disable=too-many-locals + x = 0 + y = 0 + i = 0 + old_c = 0 + y_offset = int( + ( + self.font.get_glyph(ord("M")).height + - new_text.count("\n") * self.height * self.line_spacing + ) + / 2 + ) + # print("y offset from baseline", y_offset) + left = right = top = bottom = 0 + for character in new_text: + if character == "\n": + y += int(self.height * self._line_spacing) + x = 0 + continue + glyph = self.font.get_glyph(ord(character)) + if not glyph: + continue + right = max(right, x + glyph.width) + if y == 0: # first line, find the Ascender height + top = min(top, -glyph.height + y_offset) + bottom = max(bottom, y - glyph.dy + y_offset) + position_y = y - glyph.height - glyph.dy + y_offset + position_x = x + glyph.dx + if ( + not self._text + or old_c >= len(self._text) + or character != self._text[old_c] + ): + try: + face = displayio.TileGrid( + glyph.bitmap, + pixel_shader=self.palette, + default_tile=glyph.tile_index, + tile_width=glyph.width, + tile_height=glyph.height, + position=(position_x, position_y), + ) + except TypeError: + face = displayio.TileGrid( + glyph.bitmap, + pixel_shader=self.palette, + default_tile=glyph.tile_index, + tile_width=glyph.width, + tile_height=glyph.height, + x=position_x, + y=position_y, + ) + if i < len(self): + self[i] = face + else: + self.append(face) + elif self._text and character == self._text[old_c]: + try: + self[i].position = (position_x, position_y) + except AttributeError: + self[i].x = position_x + self[i].y = position_y + + x += glyph.shift_x + + # TODO skip this for control sequences or non-printables. + i += 1 + old_c += 1 + # skip all non-prinables in the old string + while ( + self._text + and old_c < len(self._text) + and ( + self._text[old_c] == "\n" + or not self.font.get_glyph(ord(self._text[old_c])) + ) + ): + old_c += 1 + # Remove the rest + while len(self) > i: + self.pop() + self._text = new_text + self._boundingbox = (left, top, left + right, bottom - top) + + @property + def bounding_box(self): + """An (x, y, w, h) tuple that completely covers all glyphs. The + first two numbers are offset from the x, y origin of this group""" + return tuple(self._boundingbox) + + @property + def line_spacing(self): + """The amount of space between lines of text, in multiples of the font's + bounding-box height. (E.g. 1.0 is the bounding-box height)""" + return self._line_spacing + + @line_spacing.setter + def line_spacing(self, spacing): + if self._line_spacing != spacing: + self._line_spacing = spacing + self._Group__trigger_draw() + + @property + def color(self): + """Color of the text as an RGB hex number.""" + return self.palette[1] + + @color.setter + def color(self, new_color): + self.palette[1] = new_color + self._Group__trigger_draw() + + @property + def background_color(self): + """Color of the background as an RGB hex number.""" + if not self._transparent_background: + return self.palette[0] + return None + + @background_color.setter + def background_color(self, new_color): + if new_color is not None: + self.palette[0] = new_color + self.palette.make_opaque(0) + self._transparent_background = False + else: + self.palette[0] = 0 + self.palette.make_transparent(0) + self._transparent_background = True + + self._Group__trigger_draw() + + @property + def text(self): + """Text to display.""" + return self._text + + @text.setter + def text(self, new_text): + # APR 2, 2O2O -> + # Added manual trigger to update text since + # on-screen text updating is a lot faster this way. + + # Previously, the group was set to auto_write=True + # and each letter in the group was individually + # drawn and sent to the front-end display. + if new_text != self._text: + self._update_text(str(new_text)) + self._Group__trigger_draw() + + @property + def anchor_point(self): + """Point that anchored_position moves relative to. + Tuple with decimal percentage of width and height. + (E.g. (0,0) is top left, (1.0, 0.5): is middle right.)""" + return self._anchor_point + + @anchor_point.setter + def anchor_point(self, new_anchor_point): + if self._anchor_point != new_anchor_point: + self._Group__trigger_draw() + + @property + def anchored_position(self): + """Position relative to the anchor_point. Tuple containing x,y + pixel coordinates.""" + return ( + self.x - self._boundingbox[2] * self._anchor_point[0], + self.y - self._boundingbox[3] * self._anchor_point[1], + ) + + @anchored_position.setter + def anchored_position(self, new_position): + self.x = int(new_position[0] - (self._boundingbox[2] * self._anchor_point[0])) + self.y = int(new_position[1] - (self._boundingbox[3] * self._anchor_point[1])) + self._Group__trigger_draw() diff --git a/src/common/utils.py b/src/common/utils.py index 1af1c0eb5..d6d241b2f 100644 --- a/src/common/utils.py +++ b/src/common/utils.py @@ -24,8 +24,11 @@ def update_state_with_device_name(state, device_name): return updated_state -def create_message(state): - message = {"type": "state", "data": json.dumps(state)} +def create_message(msg, send_type="state"): + if isinstance(msg, dict): + msg = json.dumps(msg) + + message = {"type": send_type, "data": msg} return message @@ -41,6 +44,13 @@ def send_to_simulator(state, device_name): time.sleep(CONSTANTS.TIME_DELAY) +def send_print_to_simulator(raw_msg): + data_str = str(raw_msg) + message = create_message(data_str, "print") + print(json.dumps(message) + "\0", file=sys.__stdout__, flush=True) + time.sleep(CONSTANTS.TIME_DELAY) + + def remove_leading_slashes(string): string = string.lstrip("\\/") return string @@ -52,14 +62,6 @@ def escape_if_OSX(file_name): return file_name -def print_for_unimplemented_functions(function_name, one_more_call=False): - # Frame 0 is this function call - # Frame 1 is the call that calls this function, which is a microbit function - # Frame 2 is the call that calls the microbit function, which is in the user's file - # If one_more_call is True, then there is another frame between what was originally supposed to be frame 1 and 2. - frame_no = 2 if not one_more_call else 3 - line_number = sys._getframe(frame_no).f_lineno - user_file_name = sys._getframe(frame_no).f_code.co_filename - print( - f"'{function_name}' on line {line_number} in {user_file_name} is not implemented in the simulator but it will work on the actual device!" - ) +def print_for_unimplemented_functions(function_name): + msg = f"'{function_name}' is not implemented in the simulator but it will work on the actual device!\n" + send_print_to_simulator(msg) diff --git a/src/constants.ts b/src/constants.ts index fb77451e7..3654347d9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -22,7 +22,6 @@ export const CONFIG = { PYTHON_PATH: "python.pythonPath", SHOW_DEPENDENCY_INSTALL: "deviceSimulatorExpress.showDependencyInstall", SHOW_NEW_FILE_POPUP: "deviceSimulatorExpress.showNewFilePopup", - ENABLE_PREVIEW_MODE: "deviceSimulatorExpress.previewMode", }; export const CONSTANTS = { @@ -112,10 +111,6 @@ export const CONSTANTS = { "error.noProgramFoundDebug", "Cannot find a program to debug." ), - NO_PIP: localize( - "error.noPip", - "We found that you don't have Pip installed on your computer, please install it and try again." - ), NO_PYTHON_PATH: localize( "error.noPythonPath", "We found that you don't have Python 3 installed on your computer, please install the latest version, add it to your PATH and try again." @@ -533,7 +528,7 @@ export const STATUS_BAR_PRIORITY = { }; export const VERSIONS = { - MIN_PY_VERSION: "3.7.0", + MIN_PY_VERSION: "Python 3.7.0", }; export const HELPER_FILES = { @@ -547,6 +542,7 @@ export const HELPER_FILES = { export const GLOBAL_ENV_VARS = { PYTHON: "python", + PYTHON3: "python3", }; export const LANGUAGE_VARS = { PYTHON: { ID: "python", FILE_ENDS: ".py" }, diff --git a/src/extension.ts b/src/extension.ts index cae41100f..b733129f0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -132,6 +132,10 @@ export async function activate(context: vscode.ExtensionContext) { const openWebview = () => { if (currentPanel && currentPanel.webview) { messagingService.setWebview(currentPanel.webview); + currentPanel.webview.html = webviewService.getWebviewContent( + WEBVIEW_TYPES.SIMULATOR, + true + ); currentPanel.reveal(vscode.ViewColumn.Beside); } else { currentPanel = vscode.window.createWebviewPanel( @@ -275,7 +279,6 @@ export async function activate(context: vscode.ExtensionContext) { currentPanel, context ); - console.log("sent"); } currentPanel.onDidDispose( @@ -307,14 +310,8 @@ export async function activate(context: vscode.ExtensionContext) { const openSimulator: vscode.Disposable = vscode.commands.registerCommand( "deviceSimulatorExpress.common.openSimulator", async () => { - const isPreviewMode = getIsPreviewMode(); - const chosen_device = await vscode.window.showQuickPick( - Object.values(CONSTANTS.DEVICE_NAME_FORMAL).filter( - device => - isPreviewMode || - device !== CONSTANTS.DEVICE_NAME_FORMAL.CLUE - ) + Object.values(CONSTANTS.DEVICE_NAME_FORMAL) ); if (!chosen_device) { @@ -417,14 +414,8 @@ export async function activate(context: vscode.ExtensionContext) { const newFile: vscode.Disposable = vscode.commands.registerCommand( "deviceSimulatorExpress.common.newFile", async () => { - const isPreviewMode = getIsPreviewMode(); - const chosen_device = await vscode.window.showQuickPick( - Object.values(CONSTANTS.DEVICE_NAME_FORMAL).filter( - device => - isPreviewMode || - device !== CONSTANTS.DEVICE_NAME_FORMAL.CLUE - ) + Object.values(CONSTANTS.DEVICE_NAME_FORMAL) ); if (!chosen_device) { @@ -595,7 +586,7 @@ export async function activate(context: vscode.ExtensionContext) { // base_64 strings on UNIX systems. // added any incomplete data to beginning - let processedData = pythonProcessDataBuffer + const processedData = pythonProcessDataBuffer .join("") .concat(dataFromTheProcess); pythonProcessDataBuffer = []; @@ -794,14 +785,8 @@ export async function activate(context: vscode.ExtensionContext) { const deployToDevice: vscode.Disposable = vscode.commands.registerCommand( "deviceSimulatorExpress.common.deployToDevice", async () => { - const isPreviewMode = getIsPreviewMode(); - const chosen_device = await vscode.window.showQuickPick( - Object.values(CONSTANTS.DEVICE_NAME_FORMAL).filter( - device => - isPreviewMode || - device !== CONSTANTS.DEVICE_NAME_FORMAL.CLUE - ) + Object.values(CONSTANTS.DEVICE_NAME_FORMAL) ); if (!chosen_device) { @@ -1021,13 +1006,6 @@ export async function activate(context: vscode.ExtensionContext) { } ); - const getIsPreviewMode = (): boolean => { - const isPreviewMode: boolean = vscode.workspace - .getConfiguration() - .get(CONFIG.ENABLE_PREVIEW_MODE); - return isPreviewMode; - }; - context.subscriptions.push( installDependencies, runSimulator, diff --git a/src/install_dependencies.py b/src/install_dependencies.py index afadb9dd5..00fe81d37 100644 --- a/src/install_dependencies.py +++ b/src/install_dependencies.py @@ -3,7 +3,7 @@ import pathlib import os -os.chdir(pathlib.Path(__file__).parent.parent.absolute()) +os.chdir(str(pathlib.Path(__file__).parent.parent.absolute())) subprocess.check_call( [sys.executable, "-m", "pip", "install", "-r", "./out/requirements.txt"] ) diff --git a/src/latest_release_note.ts b/src/latest_release_note.ts index ba165ecba..65547e1aa 100644 --- a/src/latest_release_note.ts +++ b/src/latest_release_note.ts @@ -1,57 +1,26 @@ // TODO: find a better way of loading html into a string -export const LATEST_RELEASE_NOTE = `

Device Simulator Express Release Notes ⌨️🐍💞 (April 15, 2020)

+export const LATEST_RELEASE_NOTE = `

Device Simulator Express Release Notes 📝(April 20, 2020)

-

- We're unveiling a new addition to our DSX family of microcontroller simulators! Please welcome the Adafruit - CLUE - simulator 💕🔍.
- This change is hidden under a preview flag by default. See - here to learn how to enable - preview mode!
-

-

- Also, support for BBC micro:bit simulation is now officially released! 💖✨ Previously, it was hidden - behind a - preview flag. -

-

-

Features:

- +
+ LIGHTGESTUREPROXIMITY +
+

Changes:

Fixes:

+

-

Keep being a coding champ 🤩🏆🙌,
+ +

PS: You can read about the DSX team in the article here!

+

Create something wonderful ✨🙌,
      - The Device Simulator Express Team

`; diff --git a/src/micropython/microbit/__model/microbit_model.py b/src/micropython/microbit/__model/microbit_model.py index b9149e721..1f78e35ae 100644 --- a/src/micropython/microbit/__model/microbit_model.py +++ b/src/micropython/microbit/__model/microbit_model.py @@ -30,15 +30,11 @@ def __init__(self): def panic(self, n): # Due to the shim, there is another call frame. - utils.print_for_unimplemented_functions( - MicrobitModel.panic.__name__, one_more_call=True - ) + utils.print_for_unimplemented_functions(MicrobitModel.panic.__name__) def reset(self): # Due to the shim, there is another call frame. - utils.print_for_unimplemented_functions( - MicrobitModel.reset.__name__, one_more_call=True - ) + utils.print_for_unimplemented_functions(MicrobitModel.reset.__name__) def sleep(self, n): time.sleep(n / 1000) diff --git a/src/requirements.txt b/src/requirements.txt index 79749da82..66da4eb19 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -7,7 +7,6 @@ PyObjC; platform_system == "darwin" uflash==1.3.0 adafruit-circuitpython-fancyled==1.3.3 Pillow==7.0.0 -adafruit-circuitpython-display-text-DSX_CUSTOM_MAR172020.tar.gz adafruit-circuitpython-bitmap_font==1.1.0 adafruit-circuitpython-display-shapes==1.2.0 adafruit-circuitpython-neopixel==5.0.0 \ No newline at end of file diff --git a/src/service/setupService.ts b/src/service/setupService.ts index c68f78fbd..04c757a72 100644 --- a/src/service/setupService.ts +++ b/src/service/setupService.ts @@ -32,8 +32,11 @@ export class SetupService { context: vscode.ExtensionContext, needsResponse: boolean = false ) => { - const originalpythonExecutablePath = await this.getCurrentPythonExecutablePath(); - let pythonExecutablePath = originalpythonExecutablePath; + const originalPythonExecutablePath = await this.getCurrentPythonExecutablePath(); + if (originalPythonExecutablePath === "") { + return; + } + let pythonExecutablePath = originalPythonExecutablePath; const pythonExecutableName: string = os.platform() === "win32" ? HELPER_FILES.PYTHON_EXE @@ -71,7 +74,7 @@ export class SetupService { } else { pythonExecutablePath = await this.promptInstallVenv( context, - originalpythonExecutablePath, + originalPythonExecutablePath, pythonExecutableName ); this.telemetryAI.trackFeatureUsage( @@ -92,7 +95,7 @@ export class SetupService { TelemetryEventName.SETUP_HAS_VENV ); } - if (pythonExecutablePath === originalpythonExecutablePath) { + if (pythonExecutablePath === originalPythonExecutablePath) { // going with original interpreter, either because // already in venv or error in creating custom venv if (checkConfig(CONFIG.SHOW_DEPENDENCY_INSTALL)) { @@ -159,40 +162,151 @@ export class SetupService { return pythonExecutablePath; }; - public getCurrentPythonExecutablePath = async () => { - let originalpythonExecutablePath = ""; - + public getCurrentPythonExecutablePath = async ( + isTryingPython3: boolean = false + ) => { + let originalPythonExecutablePath = ""; + const systemPythonVar = isTryingPython3 + ? GLOBAL_ENV_VARS.PYTHON3 + : GLOBAL_ENV_VARS.PYTHON; // try to get name from interpreter try { - originalpythonExecutablePath = getConfig(CONFIG.PYTHON_PATH); + originalPythonExecutablePath = getConfig(CONFIG.PYTHON_PATH); } catch (err) { - originalpythonExecutablePath = GLOBAL_ENV_VARS.PYTHON; + originalPythonExecutablePath = systemPythonVar; } if ( - originalpythonExecutablePath === GLOBAL_ENV_VARS.PYTHON || - originalpythonExecutablePath === "" + originalPythonExecutablePath === GLOBAL_ENV_VARS.PYTHON3 || + originalPythonExecutablePath === GLOBAL_ENV_VARS.PYTHON || + originalPythonExecutablePath === "" ) { + // catching any instance where the python path needs to be resolved + // from an system variable this.telemetryAI.trackFeatureUsage( TelemetryEventName.SETUP_AUTO_RESOLVE_PYTHON_PATH ); try { const { stdout } = await this.executePythonCommand( - GLOBAL_ENV_VARS.PYTHON, + systemPythonVar, `-c "import sys; print(sys.executable)"` ); - originalpythonExecutablePath = stdout.trim(); + originalPythonExecutablePath = stdout.trim(); } catch (err) { this.telemetryAI.trackFeatureUsage( TelemetryEventName.SETUP_NO_PYTHON_PATH ); + if (isTryingPython3) { + // if trying python3 failed, that means that BOTH + // python and python3 failed as system variables + // so that means that there is no python + vscode.window + .showErrorMessage( + CONSTANTS.ERROR.NO_PYTHON_PATH, + DialogResponses.INSTALL_PYTHON + ) + .then((selection: vscode.MessageItem | undefined) => { + if (selection === DialogResponses.INSTALL_PYTHON) { + const okAction = () => { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_DOWNLOAD_PYTHON + ); + open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); + }; + showPrivacyModal( + okAction, + CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PYTHON + ); + } + }); + // no python installed, cannot get path + return ""; + } else { + // "python" didn't resolve to anything, trying "python3" + return this.getCurrentPythonExecutablePath(true); + } + } + if ( + !(await this.validatePythonVersion( + originalPythonExecutablePath + )) + ) { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_INVALID_PYTHON_VER + ); + if (isTryingPython3) { + // if we're trying python3, it means we already tried python and it + // all doesn't seem to work, but it got this far, so it means that + // their system python3 version is still not above 3.7, but they + // don't have a path selected. + vscode.window + .showInformationMessage( + CONSTANTS.ERROR.INVALID_PYTHON_PATH, + DialogResponses.INSTALL_PYTHON + ) + .then( + (installChoice: vscode.MessageItem | undefined) => { + if ( + installChoice === + DialogResponses.INSTALL_PYTHON + ) { + const okAction = () => { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_DOWNLOAD_PYTHON + ); + open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); + }; + showPrivacyModal( + okAction, + CONSTANTS.INFO + .THIRD_PARTY_WEBSITE_PYTHON + ); + } + } + ); + return ""; + } else { + // otherwise, we ran the "python" system variable + // and we can try python3 + return this.getCurrentPythonExecutablePath(true); + } + } + } else { + // should only be applicable if the user defined their own path + + // fix path to be absolute + if (!path.isAbsolute(originalPythonExecutablePath)) { + originalPythonExecutablePath = path.join( + vscode.workspace.rootPath, + originalPythonExecutablePath + ); + } + + if (!fs.existsSync(originalPythonExecutablePath)) { + await vscode.window.showErrorMessage( + CONSTANTS.ERROR.BAD_PYTHON_PATH + ); + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_INVALID_PYTHON_INTERPRETER_PATH + ); + return ""; + } + + if ( + !(await this.validatePythonVersion( + originalPythonExecutablePath + )) + ) { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_INVALID_PYTHON_VER + ); vscode.window - .showErrorMessage( - CONSTANTS.ERROR.NO_PYTHON_PATH, + .showInformationMessage( + CONSTANTS.ERROR.INVALID_PYTHON_PATH, DialogResponses.INSTALL_PYTHON ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.INSTALL_PYTHON) { + .then((installChoice: vscode.MessageItem | undefined) => { + if (installChoice === DialogResponses.INSTALL_PYTHON) { const okAction = () => { this.telemetryAI.trackFeatureUsage( TelemetryEventName.SETUP_DOWNLOAD_PYTHON @@ -205,49 +319,21 @@ export class SetupService { ); } }); - // no python installed, cannot get path return ""; } } - // fix path to be absolute - if (!path.isAbsolute(originalpythonExecutablePath)) { - originalpythonExecutablePath = path.join( - vscode.workspace.rootPath, - originalpythonExecutablePath - ); - } - - if (!fs.existsSync(originalpythonExecutablePath)) { - await vscode.window.showErrorMessage( - CONSTANTS.ERROR.BAD_PYTHON_PATH - ); - this.telemetryAI.trackFeatureUsage( - TelemetryEventName.SETUP_INVALID_PYTHON_INTERPRETER_PATH - ); - return ""; - } - if (!(await this.validatePythonVersion(originalpythonExecutablePath))) { - this.telemetryAI.trackFeatureUsage( - TelemetryEventName.SETUP_INVALID_PYTHON_VER - ); - return ""; - } - - return originalpythonExecutablePath; + return originalPythonExecutablePath; }; public isPipInstalled = async (pythonExecutablePath: string) => { try { - const { stdout } = await this.executePythonCommand( - pythonExecutablePath, - " -m pip" - ); + await this.executePythonCommand(pythonExecutablePath, " -m pip"); return true; } catch (err) { vscode.window .showErrorMessage( - CONSTANTS.ERROR.NO_PIP, + `We found that you may not have Pip installed on your interpreter at ${pythonExecutablePath}, please install it and try again.`, DialogResponses.INSTALL_PIP ) .then((selection: vscode.MessageItem | undefined) => { @@ -294,22 +380,6 @@ export class SetupService { "--version" ); if (stdout < VERSIONS.MIN_PY_VERSION) { - vscode.window - .showInformationMessage( - CONSTANTS.ERROR.INVALID_PYTHON_PATH, - DialogResponses.INSTALL_PYTHON - ) - .then((installChoice: vscode.MessageItem | undefined) => { - if (installChoice === DialogResponses.INSTALL_PYTHON) { - const okAction = () => { - open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); - }; - showPrivacyModal( - okAction, - CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PYTHON - ); - } - }); return false; } else { return true; @@ -398,7 +468,7 @@ export class SetupService { ); vscode.window .showErrorMessage( - `Virtual environment for download could not be completed. Using original interpreter at: ${pythonExecutable}.`, + `Virtual environment for download could not be completed. Using original interpreter at: ${pythonExecutable}. If you're on Linux, try running "sudo apt-get install python3-venv".`, DialogResponses.READ_INSTALL_MD ) .then((selection: vscode.MessageItem | undefined) => { diff --git a/src/view/components/clue/ClueImage.tsx b/src/view/components/clue/ClueImage.tsx index 0ad426c96..85694d4e0 100644 --- a/src/view/components/clue/ClueImage.tsx +++ b/src/view/components/clue/ClueImage.tsx @@ -10,7 +10,6 @@ import { ClueSvg, IRefObject } from "./Clue_svg"; interface EventTriggers { onMouseUp: (event: Event, buttonKey: string) => void; onMouseDown: (event: Event, buttonKey: string) => void; - onMouseLeave: (event: Event, buttonKey: string) => void; onKeyEvent: (event: KeyboardEvent, active: boolean, key: string) => void; } interface IProps { @@ -42,15 +41,13 @@ export class ClueImage extends React.Component { } } componentDidUpdate() { - if (this.svgRef.current) { - if (this.context === VIEW_STATE.PAUSE) { - disableAllButtons(this.svgRef.current.getButtons()); - } else if (this.context === VIEW_STATE.RUNNING) { - setupAllButtons( - this.props.eventTriggers, - this.svgRef.current.getButtons() - ); - } + if (this.context === VIEW_STATE.PAUSE && this.svgRef.current) { + disableAllButtons(this.svgRef.current.getButtons()); + } else if (this.context === VIEW_STATE.RUNNING && this.svgRef.current) { + setupAllButtons( + this.props.eventTriggers, + this.svgRef.current.getButtons() + ); } } componentWillUnmount() { @@ -89,24 +86,22 @@ export class ClueImage extends React.Component { ); } public updateButtonAttributes(key: BUTTONS_KEYS, isActive: boolean) { - if (this.svgRef.current) { - const button = this.svgRef.current.getButtons()[key].current; - if (button) { - button.focus(); - if (isActive) { - button.children[0].setAttribute( - "class", - BUTTON_STYLING_CLASSES.KEYPRESSED - ); - } else { - button.children[0].setAttribute( - "class", - BUTTON_STYLING_CLASSES.DEFAULT - ); - } - button.setAttribute("pressed", `${isActive}`); - button.setAttribute("aria-pressed", `${isActive}`); + const button = this.svgRef.current?.getButtons()[key].current; + if (button) { + button.focus(); + if (isActive) { + button.children[0].setAttribute( + "class", + BUTTON_STYLING_CLASSES.KEYPRESSED + ); + } else { + button.children[0].setAttribute( + "class", + BUTTON_STYLING_CLASSES.DEFAULT + ); } + button.setAttribute("pressed", `${isActive}`); + button.setAttribute("aria-pressed", `${isActive}`); } } } @@ -125,9 +120,7 @@ const setupButton = ( buttonElement.onmouseup = e => { eventTriggers.onMouseUp(e, key); }; - buttonElement.onmouseleave = e => { - eventTriggers.onMouseLeave(e, key); - }; + buttonElement.onkeydown = e => { // ensure that the keydown is enter, // or else it may register shortcuts twice @@ -155,7 +148,6 @@ const disableAllButtons = (buttonRefs: IRefObject) => { // to implement ref.current.onmousedown = null; ref.current.onmouseup = null; - ref.current.onmouseleave = null; ref.current.onkeydown = null; ref.current.onkeyup = null; ref.current.setAttribute("class", BUTTON_CLASSNAME.DEACTIVATED); diff --git a/src/view/components/clue/ClueSimulator.tsx b/src/view/components/clue/ClueSimulator.tsx index f0225d2e9..a15f101f8 100644 --- a/src/view/components/clue/ClueSimulator.tsx +++ b/src/view/components/clue/ClueSimulator.tsx @@ -7,14 +7,14 @@ import { VIEW_STATE, WEBVIEW_MESSAGES, } from "../../constants"; +import { ViewStateContext } from "../../context"; +import "../../styles/Simulator.css"; import "../../styles/Simulator.css"; import PlayLogo from "../../svgs/play_svg"; import StopLogo from "../../svgs/stop_svg"; import { sendMessage } from "../../utils/MessageUtils"; import ActionBar from "../simulator/ActionBar"; import { BUTTONS_KEYS, ClueImage } from "./ClueImage"; -import "../../styles/Simulator.css"; -import { ViewStateContext } from "../../context"; export const DEFAULT_CLUE_STATE: IClueState = { buttons: { button_a: false, button_b: false }, @@ -128,7 +128,6 @@ export class ClueSimulator extends React.Component { eventTriggers={{ onMouseDown: this.onMouseDown, onMouseUp: this.onMouseUp, - onMouseLeave: this.onMouseLeave, onKeyEvent: this.onKeyEvent, }} displayMessage={this.state.clue.displayMessage} @@ -194,18 +193,17 @@ export class ClueSimulator extends React.Component { }, }); }; + protected onMouseUp = (event: Event, key: string) => { event.preventDefault(); this.handleButtonClick(key, false); }; + protected onMouseDown = (event: Event, key: string) => { event.preventDefault(); this.handleButtonClick(key, true); }; - protected onMouseLeave = (event: Event, key: string) => { - event.preventDefault(); - console.log(`To implement onMouseLeave ${key}`); - }; + protected onKeyEvent(event: KeyboardEvent, active: boolean, key: string) { event.stopPropagation(); if ( @@ -213,57 +211,49 @@ export class ClueSimulator extends React.Component { this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(key, active); - if (this.imageRef.current) { - if (key === BUTTONS_KEYS.BTN_A) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_A, - active - ); - } else if (key === BUTTONS_KEYS.BTN_B) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_B, - active - ); - } else if (key === BUTTONS_KEYS.BTN_AB) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_AB, - active - ); - } + if (key === BUTTONS_KEYS.BTN_A) { + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_A, + active + ); + } else if (key === BUTTONS_KEYS.BTN_B) { + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_B, + active + ); + } else if (key === BUTTONS_KEYS.BTN_AB) { + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_AB, + active + ); } } else if ( [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.A) && this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(BUTTONS_KEYS.BTN_A, active); - if (this.imageRef.current) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_A, - active - ); - } + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_A, + active + ); } else if ( [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.B) && this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(BUTTONS_KEYS.BTN_B, active); - if (this.imageRef.current) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_B, - active - ); - } + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_B, + active + ); } else if ( [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.C) && this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(BUTTONS_KEYS.BTN_AB, active); - if (this.imageRef.current) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_AB, - active - ); - } + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_AB, + active + ); } else if (event.key === CONSTANTS.KEYBOARD_KEYS.CAPITAL_F) { this.togglePlayClick(); } else if (event.key === CONSTANTS.KEYBOARD_KEYS.CAPITAL_R) { diff --git a/src/view/components/clue/Clue_svg.tsx b/src/view/components/clue/Clue_svg.tsx index f5bfe5b92..bcbf4e6d2 100644 --- a/src/view/components/clue/Clue_svg.tsx +++ b/src/view/components/clue/Clue_svg.tsx @@ -2,10 +2,10 @@ // Licensed under the MIT license. import * as React from "react"; +import { CLUE_LEDS_COLORS, CONSTANTS } from "../../constants"; import "../../styles/SimulatorSvg.css"; -import { DEFAULT_CLUE_STATE } from "./ClueSimulator"; -import { CONSTANTS, CLUE_LEDS_COLORS } from "../../constants"; import svg from "../cpx/Svg_utils"; +import { DEFAULT_CLUE_STATE } from "./ClueSimulator"; export interface IRefObject { [key: string]: React.RefObject; } @@ -1124,8 +1124,8 @@ export class ClueSvg extends React.Component { } private updateDisplay() { - if (this.displayRef.current && this.props.displayImage) { - this.displayRef.current.setAttribute( + if (this.props.displayImage) { + this.displayRef.current?.setAttribute( "href", `data:image/png;base64,${this.props.displayImage}` ); @@ -1140,26 +1140,23 @@ export class ClueSvg extends React.Component { (255 - neopixel[1]) * CONSTANTS.LED_TINT_FACTOR},${neopixel[2] + (255 - neopixel[2]) * CONSTANTS.LED_TINT_FACTOR})`; - if (this.ledsRefs.neopixel.current) { - this.ledsRefs.neopixel.current.setAttribute("fill", rgbColor); - } - if (this.gradientRefs.neopixel.current) { - if (neopixel === DEFAULT_CLUE_STATE.leds.neopixel) { - this.gradientRefs.neopixel.current.setAttribute( - "stop-opacity", - "0" - ); - } else { - this.gradientRefs.neopixel.current.setAttribute( - "stop-opacity", - "1" - ); - } - this.gradientRefs.neopixel.current.setAttribute( - "stop-color", - rgbColor + this.ledsRefs.neopixel.current?.setAttribute("fill", rgbColor); + + if (neopixel === DEFAULT_CLUE_STATE.leds.neopixel) { + this.gradientRefs.neopixel.current?.setAttribute( + "stop-opacity", + "0" + ); + } else { + this.gradientRefs.neopixel.current?.setAttribute( + "stop-opacity", + "1" ); } + this.gradientRefs.neopixel.current?.setAttribute( + "stop-color", + rgbColor + ); } private updateLeds() { // update white led @@ -1167,25 +1164,21 @@ export class ClueSvg extends React.Component { this.ledsRefs.whiteLeds.map( (ledRef: React.RefObject) => { - if (ledRef.current && this.gradientRefs.whiteLed.current) { - svg.setLed( - isWhiteLedOn, - CLUE_LEDS_COLORS.WHITE_LEDS_OFF, - CLUE_LEDS_COLORS.WHITE_LEDS_ON, - ledRef.current, - this.gradientRefs.whiteLed.current - ); - } + svg.setLed( + isWhiteLedOn, + CLUE_LEDS_COLORS.WHITE_LEDS_OFF, + CLUE_LEDS_COLORS.WHITE_LEDS_ON, + ledRef.current, + this.gradientRefs.whiteLed.current + ); } ); - if (this.ledsRefs.redLed.current && this.gradientRefs.redLed.current) { - svg.setLed( - isRedLedOn, - CLUE_LEDS_COLORS.RED_LED_OFF, - CLUE_LEDS_COLORS.RED_LED_ON, - this.ledsRefs.redLed.current, - this.gradientRefs.redLed.current - ); - } + svg.setLed( + isRedLedOn, + CLUE_LEDS_COLORS.RED_LED_OFF, + CLUE_LEDS_COLORS.RED_LED_ON, + this.ledsRefs.redLed.current, + this.gradientRefs.redLed.current + ); } } diff --git a/src/view/components/cpx/CpxSimulator.tsx b/src/view/components/cpx/CpxSimulator.tsx index 24cf1728b..b0d77f441 100644 --- a/src/view/components/cpx/CpxSimulator.tsx +++ b/src/view/components/cpx/CpxSimulator.tsx @@ -368,11 +368,12 @@ class Simulator extends React.Component<{}, IState> { } private handleSwitchClick() { - let cpxState = this.state.cpx; - const switchIsOn: boolean = !this.state.cpx.switch; + const switchIsOn = !this.state.cpx.switch; updateSwitch(switchIsOn); - cpxState = { ...cpxState, switch: switchIsOn }; - this.setState({ ...this.state, ...cpxState }); + this.setState({ + ...this.state, + cpx: { ...this.state.cpx, switch: switchIsOn }, + }); return { switch: switchIsOn }; } diff --git a/src/view/components/cpx/Svg_utils.tsx b/src/view/components/cpx/Svg_utils.tsx index 29dab77de..92836b923 100644 --- a/src/view/components/cpx/Svg_utils.tsx +++ b/src/view/components/cpx/Svg_utils.tsx @@ -82,15 +82,15 @@ namespace svg { ledStatus: boolean, offColor: string, onColor: string, - ledElement: SVGElement, - gradientStopElement: SVGStopElement + ledElement: SVGElement | null, + gradientStopElement: SVGStopElement | null ) { if (ledStatus) { - ledElement.setAttribute("fill", onColor); - gradientStopElement.setAttribute("stop-opacity", "1"); + ledElement?.setAttribute("fill", onColor); + gradientStopElement?.setAttribute("stop-opacity", "1"); } else { - ledElement.setAttribute("fill", offColor); - gradientStopElement.setAttribute("stop-opacity", "0"); + ledElement?.setAttribute("fill", offColor); + gradientStopElement?.setAttribute("stop-opacity", "0"); } } } diff --git a/src/view/components/microbit/MicrobitImage.tsx b/src/view/components/microbit/MicrobitImage.tsx index c471ecb56..a216dceef 100644 --- a/src/view/components/microbit/MicrobitImage.tsx +++ b/src/view/components/microbit/MicrobitImage.tsx @@ -10,7 +10,6 @@ import { IRefObject, MicrobitSvg } from "./Microbit_svg"; interface EventTriggers { onMouseUp: (event: Event, buttonKey: string) => void; onMouseDown: (event: Event, buttonKey: string) => void; - onMouseLeave: (event: Event, buttonKey: string) => void; onKeyEvent: (event: KeyboardEvent, active: boolean, key: string) => void; } interface IProps { @@ -80,24 +79,22 @@ export class MicrobitImage extends React.Component { return ; } public updateButtonAttributes(key: BUTTONS_KEYS, isActive: boolean) { - if (this.svgRef.current) { - const button = this.svgRef.current.getButtons()[key].current; - if (button) { - button.focus(); - if (isActive) { - button.children[0].setAttribute( - "class", - BUTTON_STYLING_CLASSES.KEYPRESSED - ); - } else { - button.children[0].setAttribute( - "class", - BUTTON_STYLING_CLASSES.DEFAULT - ); - } - button.setAttribute("pressed", `${isActive}`); - button.setAttribute("aria-pressed", `${isActive}`); + const button = this.svgRef.current?.getButtons()[key].current; + if (button) { + button.focus(); + if (isActive) { + button.children[0].setAttribute( + "class", + BUTTON_STYLING_CLASSES.KEYPRESSED + ); + } else { + button.children[0].setAttribute( + "class", + BUTTON_STYLING_CLASSES.DEFAULT + ); } + button.setAttribute("pressed", `${isActive}`); + button.setAttribute("aria-pressed", `${isActive}`); } } } @@ -117,9 +114,7 @@ const setupButton = ( buttonElement.onmouseup = e => { eventTriggers.onMouseUp(e, key); }; - buttonElement.onmouseleave = e => { - eventTriggers.onMouseLeave(e, key); - }; + buttonElement.onkeydown = e => { // ensure that the keydown is enter, // or else it may register shortcuts twice @@ -147,7 +142,6 @@ const disableAllButtons = (buttonRefs: IRefObject) => { // to implement ref.current.onmousedown = null; ref.current.onmouseup = null; - ref.current.onmouseleave = null; ref.current.onkeydown = null; ref.current.onkeyup = null; ref.current.setAttribute("class", BUTTON_CLASSNAME.DEACTIVATED); diff --git a/src/view/components/microbit/MicrobitSimulator.tsx b/src/view/components/microbit/MicrobitSimulator.tsx index ff44ac7b2..d3507710c 100644 --- a/src/view/components/microbit/MicrobitSimulator.tsx +++ b/src/view/components/microbit/MicrobitSimulator.tsx @@ -124,7 +124,6 @@ export class MicrobitSimulator extends React.Component { eventTriggers={{ onMouseDown: this.onMouseDown, onMouseUp: this.onMouseUp, - onMouseLeave: this.onMouseLeave, onKeyEvent: this.onKeyEvent, }} leds={this.state.microbit.leds} @@ -186,18 +185,17 @@ export class MicrobitSimulator extends React.Component { }, }); }; + protected onMouseUp = (event: Event, key: string) => { event.preventDefault(); this.handleButtonClick(key, false); }; + protected onMouseDown = (event: Event, key: string) => { event.preventDefault(); this.handleButtonClick(key, true); }; - protected onMouseLeave = (event: Event, key: string) => { - event.preventDefault(); - console.log(`To implement onMouseLeave ${key}`); - }; + protected onKeyEvent(event: KeyboardEvent, active: boolean, key: string) { event.stopPropagation(); if ( @@ -205,57 +203,52 @@ export class MicrobitSimulator extends React.Component { this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(key, active); - if (this.imageRef.current) { - if (key === BUTTONS_KEYS.BTN_A) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_A, - active - ); - } else if (key === BUTTONS_KEYS.BTN_B) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_B, - active - ); - } else if (key === BUTTONS_KEYS.BTN_AB) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_AB, - active - ); - } + if (key === BUTTONS_KEYS.BTN_A) { + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_A, + active + ); + } else if (key === BUTTONS_KEYS.BTN_B) { + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_B, + active + ); + } else if (key === BUTTONS_KEYS.BTN_AB) { + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_AB, + active + ); } } else if ( [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.A) && this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(BUTTONS_KEYS.BTN_A, active); - if (this.imageRef.current) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_A, - active - ); - } + + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_A, + active + ); } else if ( [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.B) && this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(BUTTONS_KEYS.BTN_B, active); - if (this.imageRef.current) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_B, - active - ); - } + + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_B, + active + ); } else if ( [event.code, event.key].includes(CONSTANTS.KEYBOARD_KEYS.C) && this.context === VIEW_STATE.RUNNING ) { this.handleButtonClick(BUTTONS_KEYS.BTN_AB, active); - if (this.imageRef.current) { - this.imageRef.current.updateButtonAttributes( - BUTTONS_KEYS.BTN_AB, - active - ); - } + + this.imageRef.current?.updateButtonAttributes( + BUTTONS_KEYS.BTN_AB, + active + ); } else if (event.key === CONSTANTS.KEYBOARD_KEYS.CAPITAL_F) { this.togglePlayClick(); } else if (event.key === CONSTANTS.KEYBOARD_KEYS.CAPITAL_R) { diff --git a/src/view/components/microbit/__snapshots__/Microbit.spec.tsx.snap b/src/view/components/microbit/__snapshots__/Microbit.spec.tsx.snap index 8dc222774..1ab115851 100644 --- a/src/view/components/microbit/__snapshots__/Microbit.spec.tsx.snap +++ b/src/view/components/microbit/__snapshots__/Microbit.spec.tsx.snap @@ -2781,9 +2781,9 @@ Array [ onMouseLeave={[Function]} >