diff --git a/gulpfile.js b/gulpfile.js index 3c106798d..c642abfed 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,135 +1,136 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const gulp = require("gulp"); - -const ts = require("gulp-typescript"); -const sourcemaps = require("gulp-sourcemaps"); -const typescript = require("typescript"); -const del = require("del"); -const es = require("event-stream"); -const vsce = require("vsce"); -const nls = require("vscode-nls-dev"); - -const tsProject = ts.createProject("./tsconfig.json", { typescript }); - -const inlineMap = true; -const inlineSource = false; -const outDest = "out"; - -// A list of all locales supported by VSCode can be found here: https://code.visualstudio.com/docs/getstarted/locales -const languages = [{ folderName: "en", id: "en" }]; - -gulp.task("clean", () => { - return del( - [ - "out/*", - "package.nls.*.json", - "../../dist/*0.0.0-UNTRACKEDVERSION.vsix" - ], - { force: true } - ); -}); - -const pythonToMove = [ - "./src/adafruit_circuitplayground/*.*", - "./src/*.py", - "./src/requirements.txt", -]; - -gulp.task("python-compile", () => { - // the base option sets the relative root for the set of files, - // preserving the folder structure - return gulp.src(pythonToMove, { base: "./src/" }).pipe(gulp.dest("out")); -}); - -gulp.task("internal-compile", () => { - return compile(false); -}); - -gulp.task("internal-nls-compile", () => { - return compile(true); -}); - -gulp.task("add-locales", () => { - return gulp - .src(["package.nls.json"]) - .pipe(nls.createAdditionalLanguageFiles(languages, "locales")) - .pipe(gulp.dest(".")); -}); - -gulp.task("vsce:publish", () => { - return vsce.publish(); -}); - -gulp.task("vsce:package", () => { - return vsce.createVSIX({ - packagePath: "../../dist/deviceSimulatorExpress-0.0.0-UNTRACKEDVERSION.vsix" - }); -}); - -gulp.task( - "compile", - gulp.series("clean", "internal-compile", "python-compile", callback => { - callback(); - }) -); - -gulp.task( - "build", - gulp.series( - "clean", - "internal-nls-compile", - "python-compile", - "add-locales", - callback => { - callback(); - } - ) -); - -gulp.task( - "publish", - gulp.series("compile", "vsce:publish", callback => { - callback(); - }) -); - -gulp.task( - "package", - gulp.series("compile", "vsce:package", callback => { - callback(); - }) -); - -//---- internal - -function compile(buildNls) { - var r = tsProject - .src() - .pipe(sourcemaps.init()) - .pipe(tsProject()) - .js.pipe(buildNls ? nls.rewriteLocalizeCalls() : es.through()) - .pipe( - buildNls - ? nls.createAdditionalLanguageFiles(languages, "locales", "out") - : es.through() - ); - - if (inlineMap && inlineSource) { - r = r.pipe(sourcemaps.write()); - } else { - r = r.pipe( - sourcemaps.write("../out", { - // no inlined source - includeContent: inlineSource, - // Return relative source map root directories per file. - sourceRoot: "../src" - }) - ); - } - - return r.pipe(gulp.dest(outDest)); -} +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const gulp = require("gulp"); + +const ts = require("gulp-typescript"); +const sourcemaps = require("gulp-sourcemaps"); +const typescript = require("typescript"); +const del = require("del"); +const es = require("event-stream"); +const vsce = require("vsce"); +const nls = require("vscode-nls-dev"); + +const tsProject = ts.createProject("./tsconfig.json", { typescript }); + +const inlineMap = true; +const inlineSource = false; +const outDest = "out"; + +// A list of all locales supported by VSCode can be found here: https://code.visualstudio.com/docs/getstarted/locales +const languages = [{ folderName: "en", id: "en" }]; + +gulp.task("clean", () => { + return del( + [ + "out/*", + "package.nls.*.json", + "../../dist/*0.0.0-UNTRACKEDVERSION.vsix" + ], + { force: true } + ); +}); + +const pythonToMove = [ + "./src/adafruit_circuitplayground/*.*", + "./src/microbit/*.*", + "./src/*.py", + "./src/requirements.txt", +]; + +gulp.task("python-compile", () => { + // the base option sets the relative root for the set of files, + // preserving the folder structure + return gulp.src(pythonToMove, { base: "./src/" }).pipe(gulp.dest("out")); +}); + +gulp.task("internal-compile", () => { + return compile(false); +}); + +gulp.task("internal-nls-compile", () => { + return compile(true); +}); + +gulp.task("add-locales", () => { + return gulp + .src(["package.nls.json"]) + .pipe(nls.createAdditionalLanguageFiles(languages, "locales")) + .pipe(gulp.dest(".")); +}); + +gulp.task("vsce:publish", () => { + return vsce.publish(); +}); + +gulp.task("vsce:package", () => { + return vsce.createVSIX({ + packagePath: "../../dist/deviceSimulatorExpress-0.0.0-UNTRACKEDVERSION.vsix" + }); +}); + +gulp.task( + "compile", + gulp.series("clean", "internal-compile", "python-compile", callback => { + callback(); + }) +); + +gulp.task( + "build", + gulp.series( + "clean", + "internal-nls-compile", + "python-compile", + "add-locales", + callback => { + callback(); + } + ) +); + +gulp.task( + "publish", + gulp.series("compile", "vsce:publish", callback => { + callback(); + }) +); + +gulp.task( + "package", + gulp.series("compile", "vsce:package", callback => { + callback(); + }) +); + +//---- internal + +function compile(buildNls) { + var r = tsProject + .src() + .pipe(sourcemaps.init()) + .pipe(tsProject()) + .js.pipe(buildNls ? nls.rewriteLocalizeCalls() : es.through()) + .pipe( + buildNls + ? nls.createAdditionalLanguageFiles(languages, "locales", "out") + : es.through() + ); + + if (inlineMap && inlineSource) { + r = r.pipe(sourcemaps.write()); + } else { + r = r.pipe( + sourcemaps.write("../out", { + // no inlined source + includeContent: inlineSource, + // Return relative source map root directories per file. + sourceRoot: "../src" + }) + ); + } + + return r.pipe(gulp.dest(outDest)); +} diff --git a/src/microbit/__init__.py b/src/microbit/__init__.py new file mode 100644 index 000000000..3f308d8fb --- /dev/null +++ b/src/microbit/__init__.py @@ -0,0 +1 @@ +from .code_processing_shim import * \ No newline at end of file diff --git a/src/microbit/code_processing_shim.py b/src/microbit/code_processing_shim.py new file mode 100644 index 000000000..8479d5a18 --- /dev/null +++ b/src/microbit/code_processing_shim.py @@ -0,0 +1,50 @@ +from . import microbit_model +from . import image +from . import constants as CONSTANTS + +# EXAMPLE +# can be called simply as "show_message("string")" +def show_message(message): + microbit_model.mb.show_message(message) + + +display = microbit_model.mb.display + +microbit = microbit_model.mb +Image = image.Image + +def repr(image): + + ret_str = "Image(\'" + for index_y in range(0,image.height()): + ret_str += row_to_str(image, index_y) + + ret_str = ret_str + "\')" + + return ret_str + + +def str(image): + if type(image) is Image: + ret_str = "Image(\'\n" + for index_y in range(0,image.height()): + ret_str += "\t" + row_to_str(image,index_y) + "\n" + + ret_str = ret_str + "\')" + + return ret_str + else: + # if not image, call regular str class + return image.__str__() + + + +# method to help with string formation +def row_to_str(image, y): + new_str = "" + for x in range(0, image.width()): + new_str = new_str + str(image.get_pixel(x, y)) + + new_str = new_str + ":" + + return new_str \ No newline at end of file diff --git a/src/microbit/constants.py b/src/microbit/constants.py new file mode 100644 index 000000000..61705fbef --- /dev/null +++ b/src/microbit/constants.py @@ -0,0 +1,37 @@ +INDEX_ERR = "index out of bounds" +BRIGHTNESS_ERR = "brightness out of bounds" +SAME_SIZE_ERR = "images must be the same size" +UNSUPPORTED_ADD_TYPE = "unsupported types for __add__:" +LED_WIDTH = 5 +LED_HEIGHT = 5 + +NOT_IMPLEMENTED_ERROR = "This method is not implemented by the simulator" + +BOAT = ( + [0, 5, 0, 5, 0], + [0, 5, 0, 5, 0], + [0, 5, 0, 5, 0], + [9, 9, 9, 9, 9], + [0, 9, 9, 9, 0], +) + +HEART = [ + [0, 9, 0, 9, 0], + [9, 9, 9, 9, 9], + [9, 9, 9, 9, 9], + [0, 9, 9, 9, 0], + [0, 0, 9, 0, 0], +] + +BLANK = [ + [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], +] + + +COPY_ERR_MESSAGE = "please copy() first" + +LED_MAX = 5 diff --git a/src/microbit/display.py b/src/microbit/display.py new file mode 100644 index 000000000..250e4de50 --- /dev/null +++ b/src/microbit/display.py @@ -0,0 +1,63 @@ +from . import constants as CONSTANTS +from .image import Image + + +class Display: + def __init__(self): + # State in the Python process + self.__image = Image() + self.__on = True + + def scroll(self, message): + raise NotImplementedError(CONSTANTS.NOT_IMPLEMENTED_ERROR) + + def show(self, value, delay=400, wait=True, loop=False, clear=False): + if isinstance(value, Image): + width = ( + value.width() + if value.width() <= CONSTANTS.LED_WIDTH + else CONSTANTS.LED_WIDTH + ) + height = ( + value.height() + if value.height() <= CONSTANTS.LED_HEIGHT + else CONSTANTS.LED_HEIGHT + ) + self.__print() + elif isinstance(value, str): + pass + elif isinstance(value, float): + pass + elif isinstance(value, int): + pass + + def get_pixel(self, x, y): + return self.__image.get_pixel(x,y) + + def set_pixel(self, x, y, value): + self.__image.set_pixel(x, y, value) + + def clear(self): + for y in range(CONSTANTS.LED_WIDTH): + for x in range(CONSTANTS.LED_HEIGHT): + self.__LEDs[y][x] = 0 + + def on(self): + self.__on = True + + def off(self): + self.__on = False + + def is_on(self): + return self.__on + + def read_light_level(self): + raise NotImplementedError(CONSTANTS.NOT_IMPLEMENTED_ERROR) + + # Helpers + def __valid_pos(self, x, y): + return 0 <= x and x <= 4 and 0 <= y and y <= 4 + + def __print(self): + for i in range(5): + print(self.__LEDs[i]) diff --git a/src/microbit/image.py b/src/microbit/image.py new file mode 100644 index 000000000..3788ecc5b --- /dev/null +++ b/src/microbit/image.py @@ -0,0 +1,178 @@ +from . import microbit_model +from . import constants as CONSTANTS +from . import display + + +class Image: + def __init__(self, *args, **kwargs): + if len(args) == 0: + self.__LED = CONSTANTS.BLANK + elif len(args) == 1: + pattern = args[0] + if type(pattern) is str: + self.__LED = self.__string_to_array(pattern) + else: + self.__LED = pattern + else: + + width = args[0] + height = args[1] + + if width < 0 or height < 0: + # not in original, but ideally, + # image should fail non-silently + raise ValueError(CONSTANTS.INDEX_ERR) + + self.__LED = self.__create_leds(width,height) + + def width(self): + if len(self.__LED): + return len(self.__LED[0]) + else: + return 0 + + def height(self): + return len(self.__LED) + + def set_pixel(self, x, y, value): + try: + if not self.__valid_pos(x, y): + raise ValueError(CONSTANTS.INDEX_ERR) + elif not self.__valid_brightness(value): + raise ValueError(CONSTANTS.BRIGHTNESS_ERR) + else: + self.__LED[y][x] = value + except TypeError: + print(CONSTANTS.COPY_ERR_MESSAGE) + + def get_pixel(self, x, y): + if self.__valid_pos(x,y): + return self.__LED[y][x] + else: + raise ValueError(CONSTANTS.INDEX_ERR) + + def shift_up(self, n): + return self.__shift_vertical(n) + + def shift_down(self, n): + return self.__shift_vertical(n * -1) + + def shift_right(self, n): + return self.__shift_horizontal(n) + + def shift_left(self, n): + return self.__shift_horizontal(n * -1) + + def crop(self, x, y, w, h): + res = Image(w, h) + res.blit(self, x, y, w, h) + return res + + def copy(self): + return Image(self.__LED) + + def invert(self, value): + for y in range(0, self.height()): + for x in range(0, self.width()): + self.set_pixel(x, y, 9 - value) + + def fill(self, value): + for y in range(0, self.height()): + for x in range(0, self.width()): + self.set_pixel(x, y, value) + + + def blit(self, src, x, y, w, h, xdest=0, ydest=0): + for count_y in range(0, h): + for count_x in range(0, w): + if (self.__valid_pos(xdest + count_x, ydest + count_y) and + src.__valid_pos(x + count_x, y + count_y)): + transfer_pixel = src.get_pixel(x + count_x, y + count_y) + self.set_pixel(xdest + count_x, ydest + count_y, transfer_pixel) + + def __add__(self, other): + if not (type(other) is Image): + raise TypeError( + CONSTANTS.UNSUPPORTED_ADD_TYPE + f"'{type(self)}', '{type(other)}'" + ) + elif not (other.height() == self.height() and other.width() == self.width()): + raise ValueError(CONSTANTS.SAME_SIZE_ERR) + else: + res = Image(self.width(), self.height()) + + for y in range(0, self.height()): + for x in range(0, self.width()): + sum = other.get_pixel(x, y) + self.get_pixel(x, y) + display_result = self.__limit_result(9, sum) + res.set_pixel(x, y, display_result) + + return res + + def __mul__(self, other): + float_val = float(other) + res = Image(self.width(), self.height()) + + for y in range(0, self.height()): + for x in range(0, self.width()): + product = self.get_pixel(x, y) * float_val + res.set_pixel(x, y, self.__limit_result(9, product)) + + return res + + # helpers! + + def __create_leds(self, w, h): + arr = [] + for _ in range(0,h): + sub_arr = [] + for _ in range(0,w): + sub_arr.append(0) + arr.append(sub_arr) + return arr + + def __string_to_array(self, pattern): + arr = [] + sub_arr = [] + for elem in pattern: + if elem == ":": + arr.append(sub_arr) + sub_arr = [] + else: + sub_arr.append(int(elem)) + return arr + + def __limit_result(self, limit, result): + if result > limit: + return limit + else: + return result + + def __valid_brightness(self, value): + return 0 <= value and value <= 9 + + + def __valid_pos(self, x, y): + return 0 <= x and x < self.width() and 0 <= y and y < self.height() + + def __shift_vertical(self, n): + + res = Image(self.width(), self.height()) + if n > 0: + # up + res.blit(self, 0, n, self.width(), self.height() - n, 0, 0) + else: + # down + res.blit(self, 0, 0, self.width(), self.height() - abs(n), 0, abs(n)) + + return res + + def __shift_horizontal(self, n): + res = Image(self.width(), self.height()) + if n > 0: + # right + res.blit(self, 0, 0, self.width() - n, self.height(), n, 0) + else: + # left + res.blit(self, n, 0, self.width() - n, self.height(), 0, 0) + + return res \ No newline at end of file diff --git a/src/microbit/microbit_model.py b/src/microbit/microbit_model.py new file mode 100644 index 000000000..1d6ebec95 --- /dev/null +++ b/src/microbit/microbit_model.py @@ -0,0 +1,15 @@ +from .display import Display + +class MicrobitModel: + def __init__(self): + # State in the Python process + self.display = Display() + self.__state = { } + self.__debug_mode = False + self.__abs_path_to_code_file = '' + + # SAMPLE FUNCTION + def show_message(self, message): + print("message!! " + message) + +mb = MicrobitModel() \ No newline at end of file diff --git a/src/microbit/test/__init__.py b/src/microbit/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/microbit/test/test_display.py b/src/microbit/test/test_display.py new file mode 100644 index 000000000..582347bf0 --- /dev/null +++ b/src/microbit/test/test_display.py @@ -0,0 +1,76 @@ +import pytest + +from .. import constants as CONSTANTS +from ..display import Display +from ..image import Image + + +class TestDisplay(object): + def setup_method(self): + self.display = Display() + + @pytest.mark.parametrize("x, y, brightness", [(1, 1, 4), (2, 3, 6), (4, 4, 9)]) + def test_get_pixel(self, x, y, brightness): + self.display._Display__LEDs[y][x] = brightness + assert brightness == self.display.get_pixel(x, y) + + @pytest.mark.parametrize("x, y", [(5, 0), (0, -1), (0, 5)]) + def test_get_pixel_error(self, x, y): + with pytest.raises(ValueError, match=CONSTANTS.INDEX_ERR): + self.display.get_pixel(x, y) + + @pytest.mark.parametrize("x, y, brightness", [(1, 1, 4), (2, 3, 6), (4, 4, 9)]) + def test_set_pixel(self, x, y, brightness): + self.display.set_pixel(x, y, brightness) + assert brightness == self.display._Display__LEDs[y][x] + + @pytest.mark.parametrize( + "x, y, brightness, err_msg", + [ + (5, 0, 0, CONSTANTS.INDEX_ERR), + (0, -1, 0, CONSTANTS.INDEX_ERR), + (0, 0, -1, CONSTANTS.BRIGHTNESS_ERR), + ], + ) + def test_set_pixel_error(self, x, y, brightness, err_msg): + with pytest.raises(ValueError, match=err_msg): + self.display.set_pixel(x, y, brightness) + + def test_clear(self): + self.display._Display__LEDs[0][0] = 7 + self.display._Display__LEDs[3][4] = 6 + self.display._Display__LEDs[4][4] = 9 + assert not self.__is_clear() + self.display.clear() + assert self.__is_clear() + + def test_on(self): + self.display._Display__on = False + self.display.on() + assert self.display._Display__on + + def test_off(self): + self.display._Display__on = True + self.display.off() + assert False == self.display._Display__on + + @pytest.mark.parametrize("on", [True, False]) + def test_is_on(self, on): + self.display._Display__on = on + assert on == self.display.is_on() + + # Helpers + def __is_clear(self): + for y in range(CONSTANTS.LED_WIDTH): + for x in range(CONSTANTS.LED_HEIGHT): + if 0 != self.display._Display__LEDs[y][x]: + return False + return True + + def test_use_me(self): + img = Image(5, 5) + img.set_pixel(0, 0, 8) + img.set_pixel(0, 1, 9) + img.set_pixel(0, 2, 7) + img.set_pixel(2, 2, 6) + self.display.show(img) diff --git a/src/test_code/code.py b/src/test_code/code.py index 369561cc5..15f94fb04 100644 --- a/src/test_code/code.py +++ b/src/test_code/code.py @@ -1,19 +1,19 @@ -from adafruit_circuitplayground.express import cpx -import time - -cpx.pixels.brightness = 0.3 -cpx.pixels.fill((0, 0, 0)) # Turn off the NeoPixels if they're on! -cpx.pixels.show() - -while True: - if cpx.button_a: - cpx.pixels[2] = (0, 255, 0) - else: - cpx.pixels[2] = (0, 0, 0) - - if cpx.button_b: - cpx.pixels[7] = (0, 0, 255) - else: - cpx.pixels[7] = (0, 0, 0) - cpx.pixels.show() - +# from adafruit_circuitplayground.express import cpx +# import time + +# cpx.pixels.brightness = 0.3 +# cpx.pixels.fill((0, 0, 0)) # Turn off the NeoPixels if they're on! +# cpx.pixels.show() + +# while True: +# if cpx.button_a: +# cpx.pixels[2] = (0, 255, 0) +# else: +# cpx.pixels[2] = (0, 0, 0) + +# if cpx.button_b: +# cpx.pixels[7] = (0, 0, 255) +# else: +# cpx.pixels[7] = (0, 0, 0) +# cpx.pixels.show() +